diff options
Diffstat (limited to 'Tools/iExploder/iexploder-1.7.2/src/browser_harness.rb')
-rwxr-xr-x | Tools/iExploder/iexploder-1.7.2/src/browser_harness.rb | 389 |
1 files changed, 389 insertions, 0 deletions
diff --git a/Tools/iExploder/iexploder-1.7.2/src/browser_harness.rb b/Tools/iExploder/iexploder-1.7.2/src/browser_harness.rb new file mode 100755 index 0000000..c5bec80 --- /dev/null +++ b/Tools/iExploder/iexploder-1.7.2/src/browser_harness.rb @@ -0,0 +1,389 @@ +#!/usr/bin/ruby +# iExploder browser Harness (test a single web browser) +# +# Copyright 2010 Thomas Stromberg - All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#---------------------------------------------------------------------------- +# PLEASE NOTE: +# +# You must disable automatic session restoring for this to be useful. +# +# chrome --incognito +# opera --nosession -newprivatetab +# firefox -private +require 'cgi' +require 'open-uri' +require 'optparse' +require './iexploder.rb' +require './scanner.rb' + +MAC_CRASH_PATH = "#{ENV['HOME']}/Library/Logs/CrashReporter" +TESTCASE_URL = "http://127.0.0.1:3100/iexploder.cgi" + +class BrowserHarness + def initialize(port, config_path, log_dir, test_dir, watchdog_timer, scan_timer) + @app_base_url = "http://127.0.0.1:#{port}/" + @app_url = "#{@app_base_url}iexploder.cgi" + @port = port + @log_dir = log_dir + @server_log_path = "#{log_dir}/iexploder_webserver-#{port}.log" + @client_log_path = "#{log_dir}/iexploder_harness-#{port}.log" + @test_dir = test_dir + @watchdog_timer = watchdog_timer + @scan_timer = scan_timer + @config_path = config_path + + @ie = IExploder.new(@config_path) + @ie.cgi_url = @app_url + + @browser_id = nil + @browser_name = nil + msg("Client log: #{@client_log_path}") + msg("Server log: #{@server_log_path}") + @server_pid = launch_server() + end + + def msg(text) + now = Time.now() + msg = ">>> #{@browser_name}:#{@port} | #{now}: #{text}" + puts msg + STDOUT.flush + + f = File.open(@client_log_path, 'a') + f.puts msg + f.close + end + + def launch_server() + args = ['./webserver.rb', "-p#{@port}", "-c#{@config_path}", "-l#{@server_log_path}"] + pids = fork { exec(*args) } + msg("Server args: #{args.inspect}") + msg("Server pid: #{pids.inspect}") + return pids + end + + def launch_browser(args, url) + if ! File.exist?(args[0]) + msg("First argument does not appear to be an executable file: #{args[0]}") + kill_server() + exit + end + + browser = File.basename(args[0]) + @browser_name = File.basename(browser) + if browser =~ /\.app$/ + pids = launch_mac_browser(args, url) + else + pids = launch_posix_browser(args, url) + end + sleep(@scan_timer * 3) + if ! File.size?(@server_log_path) + puts "#{@server_log_path} was never written to. Unable to launch browser?" + kill_server() + exit + end + return pids + end + + def launch_posix_browser(args, url) + browser = File.basename(args[0]) + msg("Killing browser processes: #{browser}") + system("pkill #{browser} && pkill -9 #{browser}") + args = args + [url] + msg("Launching browser: #{args.inspect}") + browser_pid = fork { + exec(*args) + } + return [browser_pid] + end + + def find_pids(text) + # Only tested on Mac OS X. + pids = [] + `ps -x`.each do |proc_line| + if proc_line =~ /^ *(\d+).*#{text}/ + pid = $1.to_i + # Do not include yourself. + if pid != Process.pid + pids << $1.to_i + end + end + end + return pids + end + + def launch_mac_browser(args, url) + # This is dedicated to Safari. + if args.length > 1 + msg(".app type launches do not support arguments, ignoring #{args[1..99].inspect}") + end + browser = args[0] + pids = find_pids(browser) + if pids + kill_pids(find_pids(browser)) + sleep(2) + end + command = "open -a \"#{browser}\" \"#{url}\"" + msg(".app open command: #{command}") + system(command) + return find_pids(browser) + end + + def kill_pids(pids) + pids.each do |pid| + msg("Killing #{pid}") + begin + Process.kill("INT", pid) + sleep(0.5) + Process.kill("KILL", pid) + rescue + sleep(0.1) + end + end + end + + def encode_browser() + return @browser_id.gsub(' ', '_').gsub(';', '').gsub('/', '-').gsub(/[\(\):\!\@\#\$\%\^\&\*\+=\{\}\[\]\'\"\<\>\?\|\\]/, '').gsub(/_$/, '').gsub(/^_/, '') + end + + def kill_server() + kill_pids([@server_pid]) + end + + def parse_test_url(value) + current_vars = nil + test_num = nil + subtest_data = nil + lookup_values = false + if value =~ /iexploder.cgi(.*)/ + current_vars = $1 + if current_vars =~ /[&\?]t=(\d+)/ + test_num = $1 + end + if current_vars =~ /[&\?]s=([\d_,]+)/ + subtest_data = $1 + end + if current_vars =~ /[&\?]l=(\w+)/ + lookup_value = $1 + end + else + msg("Unable to parse url in #{value}") + return [nil, nil, nil, nil] + end + return [current_vars, test_num, subtest_data, lookup_value] + end + + def check_log_status() + timestamp, uri, user_agent = open("#{@app_base_url}last_page.cgi").read().chomp.split(' ') + age = (Time.now() - timestamp.to_i).to_i + if not @browser_id + @browser_id = CGI.unescape(user_agent) + msg("My browser is #{@browser_id}") + end + + + return [age, uri] + end + + def save_testcase(url, case_type=nil) + msg("Saving testcase: #{url}") + vars, test_num, subtest_data, lookup_value = parse_test_url(url) + if not case_type + case_type = 'testcase' + end + + testcase_name = ([case_type, encode_browser(), 'TEST', test_num, subtest_data].join('-')).gsub(/-$/, '') + ".html" + testcase_path = "#{@test_dir}/#{testcase_name}" + data = open(url).read() + # Slow down our redirection time, and replace our testcase urls. + data.gsub!(/0;URL=\/iexploder.*?\"/, "1;URL=#{testcase_name}\"") + data.gsub!(/window\.location=\"\/iexploder.*?\"/, "window\.location=\"#{testcase_name}\"") + + # I wish I did not have to do this, but the reality is that I can't imitate header fuzzing + # without a webservice in the backend. Change all URL's to use a well known localhost + # port. + data.gsub!(/\/iexploder.cgi/, TESTCASE_URL) + + f = File.open(testcase_path, 'w') + f.write(data) + f.close + msg("Wrote testcase #{testcase_path}") + return testcase_path + end + + def calculate_next_url(test_num, subtest_data) + @ie.test_num = test_num.to_i + @ie.subtest_data = subtest_data + if subtest_data and subtest_data.length > 0 + (width, offsets) = @ie.parseSubTestData(subtest_data) + # We increment within combo_creator + (width, offsets, lines) = combine_combo_creator(@ie.config['html_tags_per_page'], width, offsets) + return @ie.generateTestUrl(@ie.nextTestNum(), width, offsets) + else + return @ie.generateTestUrl(@ie.nextTestNum()) + end + end + + def find_crash_logs(max_age) + crashed_files = [] + check_files = Dir.glob("*core*") + if File.exists?(MAC_CRASH_PATH) + check_files = check_files + Dir.glob("#{MAC_CRASH_PATH}/*.*") + end + check_files.each do |file| + mtime = File.stat(file).mtime + age = (Time.now() - mtime).to_i + if age < max_age + msg("#{file} is only #{age}s old: #{mtime}") + crashed_files << file + end + end + return crashed_files + end + + def test_browser(args, test_num, random_mode=false) + # NOTE: random_mode is not yet supported. + + browser_pids = [] + subtest_data = nil + @ie.test_num = test_num + @ie.random_mode = random_mode + next_url = @ie.generateTestUrl(test_num) + + while next_url + msg("Starting at: #{next_url}") + if browser_pids + kill_pids(browser_pids) + end + browser_pids = launch_browser(args, next_url) + test_is_running = true + crash_files = [] + + while test_is_running + sleep(@scan_timer) + begin + age, request_uri = check_log_status() + rescue + msg("Failed to get status. webserver likely crashed.") + kill_pids([@server_pid]) + @server_pid = launch_server() + next_url = @ie.generateTestUrl(test_num) + test_is_running = false + next + end + vars, test_num, subtest_data, lookup_value = parse_test_url(request_uri) + if lookup_value == 'survived_redirect' + msg("We survived #{vars}. Bummer, could not repeat crash. Moving on.") + test_is_running = false + next_url = calculate_next_url(test_num, subtest_data) + next + elsif age > @watchdog_timer + msg("Stuck at #{vars}, waited for #{@watchdog_timer}s. Killing browser.") + kill_pids(browser_pids) + current_url = "#{@app_url}#{vars}" +# save_testcase(current_url, 'possible') + crash_files = find_crash_logs(@watchdog_timer + (@scan_timer * 2)) + if crash_files.length > 0 + msg("Found recent crash logs: #{crash_files.inspect} - last page: #{current_url}") + end + + if vars =~ /THE_END/ + msg("We hung at the end. Saving a testcase just in case.") + save_testcase(current_url) + next_url = calculate_next_url(test_num, nil) + test_is_running = false + next + end + + # This is for subtesting + if subtest_data + if lookup_value + msg("Confirmed crashing/hanging page at #{current_url} - saving testcase.") + save_testcase(current_url) + next_url = calculate_next_url(test_num, nil) + test_is_running = false + next + else + msg("Stopped at #{current_url}. Attempting to reproduce simplified crash/hang condition.") + browser_pids = launch_browser(args, "#{current_url}&l=test_redirect") + end + # Normal testing goes here + else + if lookup_value + msg("Reproducible crash/hang at #{current_url}, generating smaller test case.") + url = current_url.gsub(/&l=(\w+)/, '') + browser_pids = launch_browser(args, "#{url}&s=0") + else + msg("Stopped at #{current_url}. Attempting to reproduce crash/hang condition.") + browser_pids = launch_browser(args, "#{current_url}&l=test_redirect") + end + end + elsif age > @scan_timer + msg("Waiting for #{vars} to finish loading... (#{age}s of #{@watchdog_timer}s)") + end + end + end + end +end + +if $0 == __FILE__ + options = { + :port => rand(16000).to_i + 16000, + :test_dir => File.dirname($0) + '/../output', + :log_dir => File.dirname($0) + '/../output', + :test_num => nil, + :watchdog_timer => 60, + :scan_timer => 5, + :config_path => 'config.yaml', + :random_mode => false + } + + optparse = OptionParser.new do |opts| + opts.banner = "Usage: browser_harness.rb [options] -- <browser path> <browser options>" + opts.on( '-t', '--test NUM', 'Test to start at' ) { |test_num| options[:test_num] = test_num.to_i } + opts.on( '-p', '--port NUM', 'Listen on TCP port NUM (random)' ) { |port| options[:port] = port.to_i } + opts.on( '-c', '--config PATH', 'Use PATH for configuration file' ) { |path| options[:config_path] = path } + opts.on( '-d', '--testdir PATH', 'Use PATH to save testcases (/tmp)' ) { |path| options[:test_dir] = path } + opts.on( '-l', '--logdir PATH', 'Use PATH to save logs (/tmp)' ) { |path| options[:log_dir] = path } + opts.on( '-w', '--watchdog NUM', 'How many seconds to wait for pages to load (45s)' ) { |sec| options[:watchdog_timer] = sec.to_i } + opts.on( '-r', '--random', 'Generate test numbers pseudo-randomly' ) { options[:random_mode] = true } + opts.on( '-s', '--scan NUM', 'How often to check for new log data (5s)' ) { |sec| options[:scan_timer] = sec.to_i } + opts.on( '-h', '--help', 'Display this screen' ) { puts opts; exit } + end + optparse.parse! + + if options[:port] == 0 + puts "Unable to parse port option. Try adding -- as an argument before you specify your browser location." + exit + end + + if ARGV.length < 1 + puts "No browser specified. Perhaps you need some --help?" + exit + end + puts "options: #{options.inspect}" + puts "browser: #{ARGV.inspect}" + + harness = BrowserHarness.new( + options[:port], + options[:config_path], + options[:log_dir], + options[:test_dir], + options[:watchdog_timer], + options[:scan_timer] + ) + + harness.test_browser(ARGV, options[:test_num], options[:random_mode]) +end |