# frozen_string_literal: true # # ronin-web-browser - An automated Chrome API. # # Copyright (c) 2022-2024 Hal Brodigan (postmodern.mod3@gmail.com) # # ronin-web-browser is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # ronin-web-browser is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with ronin-web-browser. If not, see . # require 'ronin/web/browser/cookie' require 'ronin/web/browser/cookie_file' require 'ronin/support/network/http' require 'ferrum' require 'uri' module Ronin module Web module Browser # # Represents an instance of a Chrome headless or visible browser. # class Agent < Ferrum::Browser # The configured proxy information. # # @return [Hash{Symbol => Object}, nil] attr_reader :proxy # # Initializes the browser agent. # # @param [Boolean] visible # Controls whether the browser will start in visible or headless mode. # # @param [Boolean] headless # Controls whether the browser will start in headless or visible mode. # # @param [String, URI::HTTP, Addressible::URI, Hash, nil] proxy # The proxy to send all browser requests through. # # @param [Hash{Symbol => Object}] kwargs # Additional keyword arguments for `Ferrum::Browser#initialize`. # def initialize(visible: false, headless: !visible, proxy: Ronin::Support::Network::HTTP.proxy, **kwargs) proxy = case proxy when Hash, nil then proxy when URI::HTTP, Addressable::URI { host: proxy.host, port: proxy.port, user: proxy.user, password: proxy.password } when String uri = URI(proxy) { host: uri.host, port: uri.port, user: uri.user, password: uri.password } else raise(ArgumentError,"invalid proxy value (#{proxy.inspect}), must be either a Hash, URI::HTTP, String, or nil") end @headless = headless @proxy = proxy super(headless: headless, proxy: proxy, **kwargs) end # # Opens a new browser. # # @param [Hash{Symbol => Object}] kwargs # Additional keyword arguments for {#initialize}. # # @yield [browser] # If a block is given, it will be passed the new browser object. # Once the block returns, `quit` will be called on the browser object. # # @yieldparam [Agent] browser # The newly created browser object. # # @return [Agent] # The opened browser object. # def self.open(**kwargs) browser = new(**kwargs) if block_given? yield browser browser.quit end return browser end # # Determines whether the browser was opened in headless mode. # # @return [Boolean] # def headless? @headless end # # Determines whether the browser was opened in visible mode. # # @return [Boolean] # def visible? !@headless end # # Determines whether the proxy was initialized with a proxy. # def proxy? !@proxy.nil? end # # Enables or disables bypassing CSP. # # @param [Boolean] mode # Controls whether to enable or disable CSP bypassing. # def bypass_csp=(mode) if mode then bypass_csp(enabled: true) else bypass_csp(enabled: false) end end # # Registers a callback for the given event type. # # @param [:request, :response, :dialog, String] event # The event to register a callback for. # For an exhaustive list of event String names, see the # [Chrome DevTools Protocol documentation](https://chromedevtools.github.io/devtools-protocol/1-3/) # # @yield [request] # If the event type is `:request` the given block will be passed the # request object. # # @yield [exchange] # If the event type is `:response` the given block will be passed the # network exchange object containing both the request and the response # objects. # # @yield [params, index, total] # Other event types will be passed a params Hash, index, and total. # # @yieldparam [Ferrum::Network::InterceptedRequest] request # A network request object. # # @yieldparam [Ferrum::Network::Exchange] exchange # A network exchange object containing both the request and respoonse # objects. # # @yieldparam [Hash{String => Object}] params # A params Hash containing the return value(s). # # @yieldparam [Integer] index # # @yieldparam [Integer] total # def on(event,&block) case event when :response super('Network.responseReceived') do |params,index,total| exchange = network.select(params['requestId']).last if exchange block.call(exchange,index,total) end end when :close super('Inspector.detached',&block) else super(event,&block) end end # # Passes every request to the given block. # # @yield [request] # The given block will be passed each request before it's sent. # # @yieldparam [Ferrum::Network::InterceptRequest] request # A network request object. # def every_request network.intercept on(:request) do |request| yield request request.continue end end # # Passes every response to the given block. # # @yield [response] # If the given block accepts a single argument, it will be passed # each response object. # # @yield [response, request] # If the given block accepts two arguments, it will be passed the # response and the request objects. # # @yieldparam [Ferrum::Network::Response] response # A respone object returned for a request. # # @yieldparam [Ferrum::Network::Request] request # The request object for the response. # def every_response(&block) on(:response) do |exchange,index,total| if block.arity == 2 yield exchange.response, exchange.request else yield exchange.response end end end # # Passes every requested URL to the given block. # # @yield [url] # The given block will be passed every URL. # # @yieldparam [String] url # A URL requested by the browser. # def every_url every_request do |request| yield request.url end end # # Passes every requested URL that matches the given pattern to the given # block. # # @param [String, Regexp] pattern # The pattern to filter the URLs by. # # @yield [url] # The given block will be passed every URL that matches the pattern. # # @yieldparam [String] url # A matching URL requested by the browser. # def every_url_like(pattern) every_url do |url| if pattern.match(url) yield url end end end # # The page's current URI. # # @return [URI::HTTP] # def page_uri URI.parse(url) end # # Queries the XPath or CSS-path query and returns the matching nodes. # # @return [Array] # The matching node. # def search(query) if query.start_with?('/') xpath(query) else css(query) end end # # Queries the XPath or CSS-path query and returns the first match. # # @return [Ferrum::Node, nil] # The first matching node. # def at(query) if query.start_with?('/') at_xpath(query) else at_css(query) end end # # Queries all `` links in the current page. # # @return [Array] # def links xpath('//a/@href').map(&:value) end # # All link URLs in the current page. # # @return [Array] # def urls page_uri = self.page_uri links.map { |link| page_uri.merge(link) } end alias eval_js evaluate alias load_js add_script_tag alias inject_js evaluate_on_new_document alias load_css add_style_tag # # Enumerates over all session cookies. # # @yield [cookie] # The given block will be passed each session cookie. # # @yieldparam [Ferrum::Cookies::Cookie] cookie # A cookie that ends with `sess` or `session`. # # @return [Enumerator] # If no block is given, then an Enumerator object will be returned. # def each_session_cookie return enum_for(__method__) unless block_given? cookies.each do |cookie| yield cookie if cookie.session? end end # # Fetches all session cookies. # # @return [Array] # The matching session cookies. # def session_cookies each_session_cookie.to_a end # # Sets a cookie. # # @param [String] name # The cookie name. # # @param [String] value # The cookie value. # # @param [Hash{Symbol => Object}] options # Additional cookie attributes. # def set_cookie(name,value,**options) cookies.set(name: name, value: value, **options) end # # Loads the cookies from the cookie file. # # @param [String] path # The path to the cookie file. # def load_cookies(path) CookieFile.new(path).each do |cookie| cookies.set(cookie) end end # # Saves the cookies to a cookie file. # # @param [String] path # The path to the output cookie file. # def save_cookies(path) CookieFile.save(path,cookies) end # # Waits indefinitely until the browser window is closed. # def wait_until_closed window_closed = false on('Inspector.detached') do window_closed = true end sleep(1) until window_closed end end end end end