require 'spec_helper' describe Arachni::Browser do before( :all ) do @url = Arachni::Utilities.normalize_url( web_server_url_for( :browser ) ) end before( :each ) do clear_hit_count @browser = described_class.new end after( :each ) do Arachni::Options.reset Arachni::Framework.reset @browser.shutdown clear_hit_count end let(:subject) { @browser } let(:ua) { Arachni::Options.http.user_agent } def transitions_from_array( transitions ) transitions.map do |t| element, event = t.first.to_a options = {} if element == :page && event == :load options.merge!( url: @browser.watir.url, cookies: {} ) end if element.is_a? Hash element = described_class::ElementLocator.new( element ) end Arachni::Page::DOM::Transition.new( element, event, options ).complete end end def hit_count Typhoeus::Request.get( "#{@url}/hit-count" ).body.to_i end def image_hit_count Typhoeus::Request.get( "#{@url}/image-hit-count" ).body.to_i end def clear_hit_count Typhoeus::Request.get( "#{@url}/clear-hit-count" ) end it 'supports HTTPS' do url = web_server_url_for( :browser_https ) @browser.start_capture pages = @browser.load( url ).flush_pages pages_should_have_form_with_input( pages, 'ajax-token' ) pages_should_have_form_with_input( pages, 'by-ajax' ) end describe '.has_executable?' do context 'when there is no executable browser' do it 'returns false' do allow(Selenium::WebDriver::PhantomJS).to receive(:path){ false } expect(described_class.has_executable?).to be_falsey end end context 'when there is an executable browser' do it 'returns true' do allow(Selenium::WebDriver::PhantomJS).to receive(:path){ __FILE__ } expect(described_class.has_executable?).to be_truthy end end end describe '.executable' do it 'returns the path to the browser executable' do stub = __FILE__ allow(Selenium::WebDriver::PhantomJS).to receive(:path){ stub } expect(described_class.executable).to eq(stub) end end describe '#initialize' do describe :concurrency do it 'sets the HTTP request concurrency' end describe :ignore_scope do context true do it 'ignores scope restrictions' do @browser.shutdown @browser = described_class.new( ignore_scope: true ) Arachni::Options.scope.exclude_path_patterns << /sleep/ subject.load @url + '/ajax_sleep' expect(subject.to_page).to be_truthy end end context false do it 'enforces scope restrictions' do @browser.shutdown @browser = described_class.new( ignore_scope: false ) Arachni::Options.scope.exclude_path_patterns << /sleep/ subject.load @url + '/ajax_sleep' expect(subject.to_page.code).to eq(0) end end context :default do it 'enforces scope restrictions' do @browser.shutdown @browser = described_class.new( ignore_scope: false ) Arachni::Options.scope.exclude_path_patterns << /sleep/ subject.load @url + '/ajax_sleep' expect(subject.to_page.code).to eq(0) end end end describe :width do it 'sets the window width' do @browser.shutdown width = 100 @browser = described_class.new( width: width ) expect(subject.javascript.run('return window.innerWidth')).to eq(width) end it 'defaults to 1600' do expect(subject.javascript.run('return window.innerWidth')).to eq(1600) end end describe :height do it 'sets the window height' do @browser.shutdown height = 100 @browser = described_class.new( height: height ) expect(subject.javascript.run('return window.innerHeight')).to eq(height) end it 'defaults to 1200' do expect(subject.javascript.run('return window.innerHeight')).to eq(1200) end end describe :store_pages do describe 'default' do it 'stores snapshot pages' do @browser.shutdown @browser = described_class.new expect(@browser.load( @url + '/explore' ).flush_pages).to be_any end it 'stores captured pages' do @browser.shutdown @browser = described_class.new @browser.start_capture expect(@browser.load( @url + '/with-ajax' ).flush_pages).to be_any end end describe true do it 'stores snapshot pages' do @browser.shutdown @browser = described_class.new( store_pages: true ) expect(@browser.load( @url + '/explore' ).trigger_events.flush_pages).to be_any end it 'stores captured pages' do @browser.shutdown @browser = described_class.new( store_pages: true ) @browser.start_capture expect(@browser.load( @url + '/with-ajax' ).flush_pages).to be_any end end describe false do it 'stores snapshot pages' do @browser.shutdown @browser = described_class.new( store_pages: false ) expect(@browser.load( @url + '/explore' ).trigger_events.flush_pages).to be_empty end it 'stores captured pages' do @browser.shutdown @browser = described_class.new( store_pages: false ) @browser.start_capture expect(@browser.load( @url + '/with-ajax' ).flush_pages).to be_empty end end end context 'when browser process spawn fails' do it "raises #{described_class::Error::Spawn}" do allow_any_instance_of(described_class).to receive(:spawn_phantomjs) { nil } expect { described_class.new }.to raise_error described_class::Error::Spawn end end end describe '#source_with_line_numbers' do it 'prefixes each source code line with a number' do subject.load @url lines = subject.source.lines.to_a expect(lines).to be_any subject.source_with_line_numbers.lines.each.with_index do |l, i| expect(l).to eq("#{i+1} - #{lines[i]}") end end end describe '#load_delay' do it 'returns nil' do subject.load @url expect(subject.load_delay).to be_nil end context 'when the page has JS timeouts' do it 'returns the maximum time the browser should wait for the page based on Timeout' do subject.load( "#{@url}load_delay" ) expect(subject.load_delay).to eq(2000) end end end describe '#wait_for_timers' do it 'returns' do subject.load @url expect(subject.wait_for_timers).to be_nil end context 'when the page has JS timeouts' do it 'waits for them to complete' do subject.load( "#{@url}load_delay" ) seconds = subject.load_delay / 1000 time = Time.now subject.wait_for_timers expect(Time.now - time).to be > seconds end it "caps them at #{Arachni::OptionGroups::HTTP}#request_timeout" do subject.load( "#{@url}load_delay" ) Arachni::Options.http.request_timeout = 100 time = Time.now subject.wait_for_timers expect(Time.now - time).to be < 0.2 end end end describe '#capture_snapshot' do let(:sink_url) do "#{@url}script_sink?input=#{@browser.javascript.log_execution_flow_sink_stub(1)}" end let(:ajax_url) do "#{@url}with-ajax" end let(:captured) { subject.capture_snapshot } context 'when a snapshot has not been previously seen' do before :each do subject.load( @url + '/with-ajax', take_snapshot: false ) end it 'calls #on_new_page callbacks' do received = [] subject.on_new_page do |page| received << page end expect(captured).to eq(received) end context '#store_pages?' do context true do subject { @browser.shutdown; @browser = described_class.new( store_pages: true )} it 'stores it in #page_snapshots' do captured = subject.capture_snapshot expect(subject.page_snapshots).to eq(captured) end it 'returns it' do expect(captured.size).to eq(1) expect(captured.first).to eq(subject.to_page) end end context false do subject { @browser.shutdown; @browser = described_class.new( store_pages: false ) } it 'does not store it' do subject.capture_snapshot expect(subject.page_snapshots).to be_empty end it 'returns an empty array' do expect(captured).to be_empty end end end end context 'when a snapshot has already been seen' do before :each do subject.load( @url + '/with-ajax', take_snapshot: false ) end it 'ignores it' do expect(subject.capture_snapshot).to be_any expect(subject.capture_snapshot).to be_empty end end context 'when a snapshot has sink data' do before :each do subject.load sink_url, take_snapshot: false end it 'calls #on_new_page_with_sink callbacks' do sinks = [] subject.on_new_page_with_sink do |page| sinks << page.dom.execution_flow_sinks end subject.capture_snapshot expect(sinks.size).to eq(1) end context 'and has already been seen' do it 'calls #on_new_page_with_sink callbacks' do sinks = [] subject.on_new_page_with_sink do |page| sinks << page.dom.execution_flow_sinks end subject.capture_snapshot subject.capture_snapshot expect(sinks.size).to eq(2) end end context '#store_pages?' do context true do subject { @browser.shutdown; @browser = described_class.new( store_pages: true )} it 'stores it in #page_snapshots_with_sinks' do subject.capture_snapshot expect(subject.page_snapshots_with_sinks).to be_any end end context false do subject { @browser.shutdown; @browser = described_class.new( store_pages: false )} it 'does not store it in #page_snapshots_with_sinks' do subject.capture_snapshot expect(subject.page_snapshots_with_sinks).to be_empty end end end end context 'when a transition has been given' do before :each do subject.load( ajax_url, take_snapshot: false ) end it 'pushes it to the existing transitions' do transition = { stuff: :here } captured = subject.capture_snapshot( stuff: :here ) expect(captured.first.dom.transitions).to include transition end end context 'when there are multiple windows open' do before :each do subject.load( ajax_url, take_snapshot: false ) end it 'captures snapshots from all windows' do subject.javascript.run( 'window.open()' ) subject.watir.windows.last.use subject.load sink_url, take_snapshot: false expect(subject.capture_snapshot.map(&:url).sort).to eq( [ajax_url, sink_url].sort ) end end context 'when an error occurs' do it 'ignores it' do allow(subject.watir).to receive(:windows) { raise } expect(subject.capture_snapshot( blah: :stuff )).to be_empty end end end describe '#flush_page_snapshots_with_sinks' do it 'returns pages with data-flow sink data' do @browser.load "#{@url}/lots_of_sinks?input=#{@browser.javascript.log_data_flow_sink_stub( function: { name: 'blah' } )}" @browser.explore_and_flush expect(@browser.page_snapshots_with_sinks.map(&:dom).map(&:data_flow_sinks)).to eq( @browser.flush_page_snapshots_with_sinks.map(&:dom).map(&:data_flow_sinks) ) end it 'returns pages with execution-flow sink data' do @browser.load "#{@url}/lots_of_sinks?input=#{@browser.javascript.log_execution_flow_sink_stub( function: { name: 'blah' } )}" @browser.explore_and_flush expect(@browser.page_snapshots_with_sinks.map(&:dom).map(&:execution_flow_sinks)).to eq( @browser.flush_page_snapshots_with_sinks.map(&:dom).map(&:execution_flow_sinks) ) end it 'empties the data-flow sink page buffer' do @browser.load "#{@url}/lots_of_sinks?input=#{@browser.javascript.log_data_flow_sink_stub( function: { name: 'blah' } )}" @browser.explore_and_flush @browser.flush_page_snapshots_with_sinks.map(&:dom).map(&:data_flow_sinks) expect(@browser.page_snapshots_with_sinks).to be_empty end it 'empties the execution-flow sink page buffer' do @browser.load "#{@url}/lots_of_sinks?input=#{@browser.javascript.log_execution_flow_sink_stub( function: { name: 'blah' } )}" @browser.explore_and_flush @browser.flush_page_snapshots_with_sinks.map(&:dom).map(&:execution_flow_sinks) expect(@browser.page_snapshots_with_sinks).to be_empty end end describe '#on_new_page_with_sink' do it 'assigns blocks to handle each page with execution-flow sink data' do @browser.load "#{@url}/lots_of_sinks?input=#{@browser.javascript.log_execution_flow_sink_stub( function: { name: 'blah' } )}" sinks = [] @browser.on_new_page_with_sink do |page| sinks << page.dom.execution_flow_sinks end @browser.explore_and_flush expect(sinks.size).to eq(2) expect(sinks).to eq(@browser.page_snapshots_with_sinks.map(&:dom). map(&:execution_flow_sinks)) end it 'assigns blocks to handle each page with data-flow sink data' do @browser.javascript.taint = 'taint' @browser.load "#{@url}/lots_of_sinks?input=#{@browser.javascript.log_data_flow_sink_stub( @browser.javascript.taint, function: { name: 'blah' } )}" sinks = [] @browser.on_new_page_with_sink do |page| sinks << page.dom.data_flow_sinks end @browser.explore_and_flush expect(sinks.size).to eq(2) expect(sinks).to eq(@browser.page_snapshots_with_sinks.map(&:dom). map(&:data_flow_sinks)) end end describe '#on_fire_event' do it 'gets called before each event is triggered' do @browser.load "#{@url}/trigger_events" calls = [] @browser.on_fire_event do |element, event| calls << [element.opening_tag, event] end @browser.fire_event @browser.watir.div( id: 'my-div' ), :click @browser.fire_event @browser.watir.div( id: 'my-div' ), :mouseover expect(calls).to eq([ [ "