summaryrefslogtreecommitdiffstats
path: root/Tools/iExploder/iexploder-1.7.2/src/browser_harness.rb
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/iExploder/iexploder-1.7.2/src/browser_harness.rb')
-rwxr-xr-xTools/iExploder/iexploder-1.7.2/src/browser_harness.rb389
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