require 'spec_helper'
require 'msf/core'
require 'msf/core/exploit/remote/browser_exploit_server'

describe Msf::Exploit::Remote::BrowserExploitServer do

  subject(:server) do
    mod = Msf::Exploit.allocate
    mod.extend described_class
    mod.send(:initialize, {})
    mod
  end

  let(:service_double) do
    service = double("service")
    service.stub(:server_name=)
    service.stub(:add_resource)
    service
  end

  let(:profile_name) do
    "random"
  end

  let(:expected_os_name) do
    "linux"
  end

  let(:expected_user_agent) do
    "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)"
  end

  let(:exploit_page) do
    server.instance_variable_get(:@exploit_receiver_page)
  end

  let(:expected_profile) do
    {
      :source=>"script",
      :os_name=>"Microsoft Windows",
      :os_flavor=>"XP",
      :ua_name=>"MSIE",
      :ua_ver=>"8.0",
      :arch=>"x86",
      :office=>"null",
      :activex=>"true",
      :proxy=>false,
      :language=>"en-us",
      :tried=>true
    }
  end

  before do
    Rex::ServiceManager.stub(:start => service_double)
  end

  before(:each) do
    server.start_service
  end

  describe ".get_module_resource" do
    it "should give me a URI to access the exploit page" do
      module_resource = server.get_module_resource
      module_resource.should match(exploit_page)
    end
  end

  describe ".get_bad_requirements" do
    let(:rejected_requirements) do
      server.get_bad_requirements(fake_profile)
    end

    context 'when given the expected profile' do
      it "should not contain any bad requirements" do
        server.get_bad_requirements(expected_profile).should eq([])
      end
    end

    context 'when attempting to match :os_name' do
      let(:fake_profile) do
        { :os_name => expected_os_name }
      end

      before do
        server.instance_variable_set(:@requirements, {:os_name => /win/i})
      end

      it "should have identify :os_name as a requirement not met" do
        rejected_requirements.should eq([:os_name])
      end
    end

    context 'when attempting to match :ua_ver' do
      context 'against version 25.0' do
        let(:expected_ua_ver) { '25.0' }
        let(:fake_profile) do
          { :ua_ver => expected_ua_ver }
        end

        before do
          server.instance_variable_set(:@requirements, {:ua_ver => ua_ver})
        end

        context "with the regex /26\.0$/" do
          let(:ua_ver) { /26\.0$/ }
          it "should reject :ua_ver" do
            rejected_requirements.should include(:ua_ver)
          end
        end

        context "with the regex /25\.0$/" do
          let(:ua_ver) { /25\.0$/ }
          it "should accept :ua_ver" do
            rejected_requirements.should_not include(:ua_ver)
          end
        end

        context "with a Proc that checks if version is between 1-5" do
          let(:ua_ver) { lambda{ |ver| ver.to_i.between?(1, 5) } }
          it "should reject :ua_ver" do
            rejected_requirements.should include(:ua_ver)
          end
        end

        context "with a Proc that checks if version is between 20-26" do
          let(:ua_ver) { lambda{ |ver| ver.to_i.between?(20, 26) } }
          it "should accept :ua_ver" do
            rejected_requirements.should_not include(:ua_ver)
          end
        end
      end
    end
  end

  describe ".init_profile" do
    it "should initialize an empety profile for tag 'random'" do
      server.init_profile(profile_name)
      ivar_target_profile = server.instance_variable_get(:@target_profiles)
      ivar_target_profile.should eq({profile_name=>{}})
    end
  end

  describe ".get_profile" do
    it "should return nil when a profile isn't found" do
      server.init_profile(profile_name)
      p = server.get_profile("non_existent_profile")
      p.should be_nil
    end

    it "should return a profile if found" do
      server.init_profile(profile_name)
      p = server.get_profile(profile_name)
      p.should eq({})
    end
  end

  describe ".update_profile" do
    it "should update my target profile's :os_name information" do
      server.init_profile(profile_name)
      profile = server.get_profile(profile_name)
      server.update_profile(profile, :os_name, expected_os_name)
      profile = server.get_profile(profile_name)
      profile[:os_name].should eq(expected_os_name)
    end
  end

  describe ".get_detection_html" do
    it "should return the detection code that the client will get" do
      html = server.get_detection_html(expected_user_agent)
      html.should_not eq('')
    end
  end

  describe ".on_request_exploit" do
    it "should raise a NoMethodError if called" do
      fake_cli = nil
      fake_request = nil
      fake_browser_info = nil
      lambda {
        server.on_request_exploit(fake_cli, fake_request, fake_browser_info)
      }.should raise_error
    end
  end

  describe ".get_target" do
    it "should return a target" do
      #
      # Using Object for Msf::Module::Target
      #
      expected_object = Object
      server.instance_variable_set(:@target, expected_object)
      server.get_target.should eq(expected_object)
    end
  end

  describe ".try_set_target" do
    it "should try to set a target based on requirements" do
      #
      # This testcase needs to be better somehow, but not sure how to actually create
      # a Msf::Module::Target. All we're able to test here is making sure the method
      # doesn't raise anything by exercising the code.
      #
      server.instance_variable_set(:@requirements, {:os_name => /win/i})
      server.instance_variable_set(:@target, Object)
      server.try_set_target(expected_profile)
      server.get_target.should eq(Object)
    end
  end

  describe ".extract_requirements" do
    it "should find all the recognizable keys" do
      requirements = {:os_flavor=>"XP", :ua_name=>"MSIE", :ua_ver=>"8.0"}
      matches = server.extract_requirements(requirements)
      matches.should eq(requirements)
    end

    it "should make sure the keys are always symbols" do
      requirements = {'os_flavor'=>"XP", 'ua_name'=>"MSIE"}
      matches = server.extract_requirements(requirements)
      matches.each do |k,v|
        k.class.should eq(Symbol)
      end
    end
  end

  describe '.on_request_uri' do
    let(:cli)     { double(:peerhost => '0.0.0.0') }
    let(:cookie)  { '' }
    let(:headers) { {'Cookie' => cookie, 'User-Agent' => ''} }
    let(:body)    { '' }
    let(:cookie_name) { Msf::Exploit::Remote::BrowserExploitServer::DEFAULT_COOKIE_NAME }
    let(:request) do
      double(:body => body, :headers => headers, :uri => server.get_resource )
    end

    before do
      server.stub(:send_redirect)
      server.stub(:send_response)
      server.stub(:send_not_found)
    end

    context 'when a new visitor requests the exploit' do
      it 'calls send_response once' do
        server.should_receive(:send_response).once
        server.on_request_uri(cli, request)
      end

      it 'serves the os.js detection script' do
        server.should_receive(:send_response) do |cli, html, headers|
          expect(html).to include('window.os_detect')
        end
        server.on_request_uri(cli, request)
      end
    end

    context 'when a returning visitor requests the exploit' do
      let(:body) { '' }
      let(:tag) { 'joe' }
      let(:cookie) { "#{cookie_name}=#{tag}" }

      before { server.init_profile(tag) }

      it 'calls send_redirect once' do
        server.should_receive(:send_redirect).once
        server.on_request_uri(cli, request)
      end

      it 'redirects to the exploit URL' do
        server.should_receive(:send_redirect) do |cli, url|
          expect(url).to end_with("#{exploit_page}/")
        end
        server.on_request_uri(cli, request)
      end
    end

    context 'when a returning visitor from a previous msf run requests the exploit' do
      let(:body) { '' }
      let(:tag) { 'joe' }
      let(:cookie) { "#{cookie_name}=#{tag}" }

      it 'calls send_response once' do
        server.should_receive(:send_response).once
        server.on_request_uri(cli, request)
      end

      it 'serves the os.js detection script' do
        server.should_receive(:send_response) do |cli, html, headers|
          expect(html).to include('window.os_detect')
        end
        server.on_request_uri(cli, request)
      end
    end
  end

end
