From f9c630b5d67360ed586c81b86783d6332c4eda57 Mon Sep 17 00:00:00 2001 From: zinduolis Date: Mon, 25 Aug 2025 22:24:15 +1000 Subject: [PATCH] Update .gitignore to include secrets for local GitHub Actions testing; refactor spec_helper.rb for improved fork handling --- .gitignore | 3 + spec/spec_helper.rb | 255 +++++++++++++++++++++++++++----------------- 2 files changed, 159 insertions(+), 99 deletions(-) diff --git a/.gitignore b/.gitignore index 4322aed64..64282a1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ node_modules/ # Generated files out/ doc/rdoc/ + +# Secrets for testing github actions locally +.secrets \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4eb5c992d..9571463f9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,17 +1,21 @@ +# frozen_string_literal: true # # Copyright (c) 2006-2025 Wade Alcorn - wade@bindshell.net # Browser Exploitation Framework (BeEF) - https://beefproject.com # See the file 'doc/COPYING' for copying permission # + # Set external and internal character encodings to UTF-8 Encoding.default_external = Encoding::UTF_8 Encoding.default_internal = Encoding::UTF_8 +require 'logger' +require 'net/http' +require 'uri' + require 'core/loader.rb' -# @note We need to load variables that 'beef' usually does for us - -# @todo review this config (this isn't used or is shadowed by the monkey patching, needs a further look to fix properly) +# We need to load variables that 'beef' usually does for us config = BeEF::Core::Configuration.new('config.yaml') $home_dir = Dir.pwd $root_dir = Dir.pwd @@ -26,13 +30,13 @@ require 'browserstack/local' require 'byebug' # Require supports -Dir['spec/support/*.rb'].each do |f| - require f -end +Dir['spec/support/*.rb'].each { |f| require f } ENV['RACK_ENV'] ||= 'test' # Set the environment to test ARGV.clear +SERVER_START_TIMEOUT = Integer(ENV['SERVER_START_TIMEOUT'] || 20) + ## BrowserStack config # Monkey patch to avoid reset sessions @@ -42,29 +46,83 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base end end -TASK_ID = (ENV['TASK_ID'] || 0).to_i +TASK_ID = (ENV['TASK_ID'] || 0).to_i CONFIG_FILE = ENV['CONFIG_FILE'] || 'windows/win10/win10_chrome_81.config.yml' -CONFIG = YAML.safe_load(File.read("./spec/support/browserstack/#{CONFIG_FILE}")) +CONFIG = YAML.safe_load(File.read("./spec/support/browserstack/#{CONFIG_FILE}")) CONFIG['user'] = ENV['BROWSERSTACK_USERNAME'] || '' -CONFIG['key'] = ENV['BROWSERSTACK_ACCESS_KEY'] || '' +CONFIG['key'] = ENV['BROWSERSTACK_ACCESS_KEY'] || '' -## DB config +## DB config for unit tests (in-memory). We will disconnect these before forking the server. ActiveRecord::Base.logger = nil OTR::ActiveRecord.configure_from_hash!(adapter: 'sqlite3', database: ':memory:') - -# otr-activerecord requires manually establishing the connection with the following line -# Also a check to confirm that the correct Gem version is installed to require it, likely easier for old systems. if Gem.loaded_specs['otr-activerecord'].version > Gem::Version.create('1.4.2') OTR::ActiveRecord.establish_connection! end ActiveRecord::Schema.verbose = false -# Migrate (if required) -ActiveRecord::Migration.verbose = false # silence activerecord migration stdout messages +# Migrate (if required) for the in-memory schema used by non-server specs +ActiveRecord::Migration.verbose = false ActiveRecord::Migrator.migrations_paths = [File.join('core', 'main', 'ar-migrations')] -context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) -if context.needs_migration? - ActiveRecord::Migrator.new(:up, context.migrations, context.schema_migration, context.internal_metadata).migrate +mem_context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) +if mem_context.needs_migration? + ActiveRecord::Migrator + .new(:up, mem_context.migrations, mem_context.schema_migration, mem_context.internal_metadata) + .migrate +end + +# ------------------------------------------------------------------- +# Console logger shims +# Some extensions may call Console.level= or BeEF::Core::Console.level= +# Ensure both are safe. +# ------------------------------------------------------------------- +module BeEF + module Core + module Console + class << self + attr_accessor :logger + def level=(val) + (self.logger ||= Logger.new($stdout)).level = val + end + def level + (self.logger ||= Logger.new($stdout)).level + end + # Proxy common logger methods if called directly (info, warn, error, etc.) + def method_missing(m, *args, &blk) + lg = (self.logger ||= Logger.new($stdout)) + return lg.public_send(m, *args, &blk) if lg.respond_to?(m) + super + end + def respond_to_missing?(m, include_priv = false) + (self.logger ||= Logger.new($stdout)).respond_to?(m, include_priv) || super + end + end + end + end +end +BeEF::Core::Console.logger ||= Logger.new($stdout) + +# Some code may reference a top-level ::Console constant (not namespaced) +unless defined?(::Console) && ::Console.respond_to?(:level=) + module ::Console + class << self + attr_accessor :logger + def level=(val) + (self.logger ||= Logger.new($stdout)).level = val + end + def level + (self.logger ||= Logger.new($stdout)).level + end + # Proxy to logger for typical logging calls + def method_missing(m, *args, &blk) + lg = (self.logger ||= Logger.new($stdout)) + return lg.public_send(m, *args, &blk) if lg.respond_to?(m) + super + end + def respond_to_missing?(m, include_priv = false) + (self.logger ||= Logger.new($stdout)).respond_to?(m, include_priv) || super + end + end + end end RSpec.configure do |config| @@ -76,9 +134,9 @@ RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = :expect end + config.around do |example| ActiveRecord::Base.transaction do - # byebug example.run raise ActiveRecord::Rollback end @@ -94,146 +152,145 @@ RSpec.configure do |config| exit 0 ensure print_info 'Shutting down server' - Process.kill('KILL', server_pid) - Process.kill('KILL', server_pids) + Process.kill('KILL', server_pid) if server_pid + Array(server_pids).compact.each { |pid| Process.kill('KILL', pid) } end -######################################## + ######################################## -def reset_beef_db - begin - db_file = BeEF::Core::Configuration.instance.get('beef.database.file') - File.delete(db_file) if File.exist?(db_file) + def reset_beef_db + db_file = BeEF::Core::Configuration.instance.get('beef.database.file') + File.delete(db_file) if File.exist?(db_file) rescue => e - print_error("Could not remove '#{db_file}' database file: #{e.message}") + print_error("Could not remove '#{db_file}' database file: #{e.message}") end -end -require 'socket' + require 'socket' def port_available? socket = TCPSocket.new(@host, @port) socket.close - false # If a connection is made, the port is in use, so it's not available. - rescue Errno::ECONNREFUSED - true # If the connection is refused, the port is not in use, so it's available. - rescue Errno::EADDRNOTAVAIL - true # If the connection is refused, the port is not in use, so it's available. + false # If a connection is made, the port is in use. + rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL + true # Connection refused/unavailable => port is free. end def configure_beef # Reset or re-initialise the configuration to a default state @config = BeEF::Core::Configuration.instance - - @config.set('beef.credentials.user', "beef") - @config.set('beef.credentials.passwd', "beef") + @config.set('beef.credentials.user', 'beef') + @config.set('beef.credentials.passwd','beef') @config.set('beef.http.https.enable', false) end # Load the server def load_beef_extensions_and_modules - # Load BeEF extensions - BeEF::Extensions.load + # Load BeEF extensions + BeEF::Extensions.load + # Load BeEF modules only if they are not already loaded + BeEF::Modules.load if @config.get('beef.module').nil? + end - # Load BeEF modules only if they are not already loaded - BeEF::Modules.load if @config.get('beef.module').nil? + # --- HARD fork-safety: disconnect every pool/adapter we can find --- + def disconnect_all_active_record! + if defined?(ActiveRecord::Base) + # Disconnect every connection pool explicitly + handler = ActiveRecord::Base.connection_handler + handler.connection_pool_list.each { |pool| pool.disconnect! } if handler.respond_to?(:connection_pool_list) + ActiveRecord::Base.clear_active_connections! + ActiveRecord::Base.clear_all_connections! + end + OTR::ActiveRecord.disconnect! if defined?(OTR::ActiveRecord) end def start_beef_server configure_beef @port = @config.get('beef.http.port') - @host = @config.get('beef.http.host') @host = '127.0.0.1' unless port_available? print_error "Port #{@port} is already in use. Exiting." - exit + exit 1 end load_beef_extensions_and_modules - - # Grab DB file and regenerate if requested + + # DB file for BeEF runtime (not the in-memory test DB) db_file = @config.get('beef.database.file') if BeEF::Core::Console::CommandLine.parse[:resetdb] File.delete(db_file) if File.exist?(db_file) end - # Load up DB and migrate if necessary - ActiveRecord::Base.logger = nil - OTR::ActiveRecord.configure_from_hash!(adapter:'sqlite3', database: db_file) - # otr-activerecord require you to manually establish the connection with the following line - #Also a check to confirm that the correct Gem version is installed to require it, likely easier for old systems. - if Gem.loaded_specs['otr-activerecord'].version > Gem::Version.create('1.4.2') - OTR::ActiveRecord.establish_connection! - end + # ***** IMPORTANT: close any and all AR/OTR connections before forking ***** + disconnect_all_active_record! - # Migrate (if required) - ActiveRecord::Migration.verbose = false # silence activerecord migration stdout messages - ActiveRecord::Migrator.migrations_paths = [File.join('core', 'main', 'ar-migrations')] - context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) - if context.needs_migration? - ActiveRecord::Migrator.new(:up, context.migrations, context.schema_migration, context.internal_metadata).migrate - end - - BeEF::Core::Migration.instance.update_db! - - # Spawn HTTP Server - # print_info "Starting HTTP Hook Server" - http_hook_server = BeEF::Core::Server.instance - http_hook_server.prepare - - # Generate a token for the server to respond with - BeEF::Core::Crypto::api_token - - # Initiate server start-up - BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) pid = fork do + # Child: establish a fresh connection to the file DB + OTR::ActiveRecord.configure_from_hash!(adapter: 'sqlite3', database: db_file) + if Gem.loaded_specs['otr-activerecord'].version > Gem::Version.create('1.4.2') + OTR::ActiveRecord.establish_connection! + end + + # Apply migrations for runtime DB + ActiveRecord::Migration.verbose = false + ActiveRecord::Migrator.migrations_paths = [File.join('core', 'main', 'ar-migrations')] + context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) + if context.needs_migration? + ActiveRecord::Migrator + .new(:up, context.migrations, context.schema_migration, context.internal_metadata) + .migrate + end + + BeEF::Core::Migration.instance.update_db! + + # Spawn HTTP Server + http_hook_server = BeEF::Core::Server.instance + http_hook_server.prepare + + # Generate a token for the server to respond with + BeEF::Core::Crypto::api_token + + # Fire pre_http_start hooks (Dns extension, etc.) + BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) + + # Start server (blocking call) http_hook_server.start end - return pid + pid end def beef_server_running?(uri_str) - begin - uri = URI.parse(uri_str) - response = Net::HTTP.get_response(uri) - response.is_a?(Net::HTTPSuccess) - rescue Errno::ECONNREFUSED - return false # Connection refused means the server is not running - rescue StandardError => e - return false # Any other error means the server is not running - end + uri = URI.parse(uri_str) + response = Net::HTTP.get_response(uri) + response.is_a?(Net::HTTPSuccess) + rescue Errno::ECONNREFUSED, StandardError + false end def wait_for_beef_server_to_start(uri_str, timeout: 5) - start_time = Time.now # Record the time we started - until beef_server_running?(uri_str) || (Time.now - start_time) > timeout do - sleep 0.1 # Wait a bit before checking again + start_time = Time.now + until beef_server_running?(uri_str) || (Time.now - start_time) > timeout + sleep 0.1 end - beef_server_running?(uri_str) # Return the result of the check + beef_server_running?(uri_str) end def start_beef_server_and_wait - puts "Starting BeEF server" + puts 'Starting BeEF server' pid = start_beef_server puts "BeEF server started with PID: #{pid}" - if wait_for_beef_server_to_start('http://localhost:3000', timeout: SERVER_START_TIMEOUT) - # print_info "Server started successfully." - else - print_error "Server failed to start within timeout." + unless wait_for_beef_server_to_start('http://localhost:3000', timeout: SERVER_START_TIMEOUT) + print_error 'Server failed to start within timeout.' end pid end def stop_beef_server(pid) - exit if pid.nil? - # Shutting down server - Process.kill("KILL", pid) unless pid.nil? - Process.wait(pid) unless pid.nil? # Ensure the process has exited and the port is released - pid = nil + return if pid.nil? + Process.kill('KILL', pid) + Process.wait(pid) end - -end +end \ No newline at end of file