lib/ronin/web/cli/commands/browser.rb
# frozen_string_literal: true
#
# ronin-web - A collection of useful web helper methods and commands.
#
# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-web is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ronin-web. If not, see <https://www.gnu.org/licenses/>.
#
require_relative '../command'
require_relative '../browser_options'
require_relative '../browser_shell'
require_relative '../js_shell'
require 'command_kit/colors'
module Ronin
module Web
class CLI
module Commands
#
# Screenshots one or more URLs.
#
# ## Usage
#
# ronin-web browser [options] [URL]
#
# ## Options
#
# -B, --browser NAME|PATH The browser name or path to execute
# -W, --width WIDTH Sets the width of the browser viewport (Default: 1024)
# -H, --height HEIGHT Sets the height of the browser viewport (Default: 768)
# --headless Run the browser in headless mode
# --visible Open a visible browser
# -x, --x INT Sets the position of the browser X coordinate
# -y, --y INT Sets the position of the browser Y coordinate
# --inject-js JS Injects JavaScript into every page
# --inject-js-file FILE Injects a JavaScript file into every page
# --bypass-csp Enables bypassing CSP
# --print-urls Print all requested URLs
# --print-status Print the status of all requested URLs
# --print-requests Print all requests sent by the browser
# --print-responses Print responses to all requests
# --print-traffic Print requests and responses
# --print-headers Print headers of requests/responses
# --print-body Print request/response bodies
# --shell Starts an interactive shell
# --js-shell Starts an interactive JavaScript shell
# -h, --help Print help information
#
# ## Arguments
#
# [URL] The initial URL to visit
#
# @since 2.0.0
#
class Browser < Command
include BrowserOptions
include CommandKit::Colors
usage '[options] [URL]'
option :headless, desc: 'Run the browser in headless mode' do
@mode = :headless
end
option :visible, desc: 'Open a visible browser' do
@mode = :visible
end
option :x, short: '-x',
value: {
type: Integer
},
desc: "Sets the position of the browser X coordinate"
option :y, short: '-y',
value: {
type: Integer
},
desc: "Sets the position of the browser Y coordinate"
option :inject_js, value: {
type: String,
usage: 'JS'
},
desc: 'Injects JavaScript into every page'
option :inject_js_file, value: {
type: String,
usage: 'FILE'
},
desc: 'Injects a JavaScript file into every page'
option :bypass_csp, desc: 'Enables bypassing CSP'
option :print_urls, desc: 'Print all requested URLs'
option :print_status, desc: 'Print the status of all requested URLs'
option :print_requests, desc: 'Print all requests sent by the browser'
option :print_responses, desc: 'Print responses to all requests'
option :print_traffic, desc: 'Print requests and responses'
option :print_headers, desc: 'Print headers of requests/responses'
option :print_cookies, desc: 'Print Set-Cookie headers from all responses'
option :print_body, desc: 'Print request/response bodies'
option :shell, desc: 'Starts an interactive shell'
option :js_shell, desc: 'Starts an interactive JavaScript shell'
argument :url, required: false,
desc: 'The initial URL to visit'
description "Automates a web browser"
man_page 'ronin-web-browser.1'
#
# Initializes the `ronin-web browser` command.
#
# @param [Hash{Symbol => Object}] kwargs
# Additional keywords for the command.
#
def initialize(**kwargs)
super(**kwargs)
@mode = if stdout.tty? then :visible
else :headless
end
end
#
# Runs the `ronin-web browser` command.
#
# @param [String, nil] url
# The optional URL to visit.
#
def run(url=nil)
unless (url || options[:shell] || options[:js_shell])
print_error "must specify a URL or --shell / --js-shell"
exit(-1)
end
configure_browser
open_browser(url)
if options[:shell] || options[:js_shell]
start_shell
else
wait_until_closed
end
close_browser
end
#
# Configures the browser and registers callbacks.
#
def configure_browser
if options[:x] || options[:y]
browser.position = {
left: options.fetch(:x,0),
top: options.fetch(:y,0)
}
end
browser.bypass_csp = true if options[:bypass_csp]
if options[:inject_js_file]
browser.inject_js(File.read(options[:inject_js_file]))
elsif options[:inject_js]
browser.inject_js(options[:inject_js])
end
if options[:print_status]
browser.every_response(&method(:print_url_status))
elsif options[:print_cookies]
browser.every_response(&method(:print_cookies))
elsif options[:print_urls]
browser.every_url(&method(:puts))
elsif options[:print_traffic]
browser.every_request(&method(:print_request))
browser.every_response(&method(:print_response))
else
browser.every_request(&method(:print_request)) if options[:print_requests]
browser.every_response(&method(:print_response)) if options[:print_responses]
end
end
#
# Open the browser window.
#
# @param [String, nil] url
# The optional URL to visit.
#
def open_browser(url=nil)
browser.goto(url) if url
end
#
# Starts an interactive browser shell.
#
def start_shell
# start the shell then immediately quit the browser once exited
if options[:js_shell]
JSShell.start(browser)
elsif options[:shell]
BrowserShell.start(browser)
end
end
#
# Waits until the browser is done or if the user exits the command.
#
def wait_until_closed
if @mode == :visible
# wait for the browser window to be closed
browser.wait_until_closed
else
# wait until there's no network traffic
browser.network.wait_for_idle { browser.quit }
end
end
#
# Close the browser.
#
def close_browser
browser.quit
end
#
# Additional keyword arguments for `Ronin::Web::Browser.new`.
#
# @return [Hash{Symbol => Object}]
#
def browser_kwargs
kwargs = super()
case @mode
when :headless then kwargs[:headless] = true
when :visible then kwargs[:visible] = true
end
return kwargs
end
#
# Prints the status and URL of a response.
#
# @param [Ferrum::Network::Response] response
# The respones object.
#
def print_url_status(response)
if response.status < 300
puts "#{colors.bright_green(response.status)} #{colors.green(response.url)}"
elsif response.status < 400
puts "#{colors.bright_yellow(response.status)} #{colors.yellow(response.url)}"
elsif response.status < 500
puts "#{colors.bright_red(response.status)} #{colors.red(response.url)}"
else
puts "#{colors.bold(colors.bright_red(response.status))} #{colors.bold(colors.red(response.url))}"
end
end
#
# Prints a request from the browser.
#
# @param [Ferrum::Network::InterceptedRequest] request
#
def print_request(request)
sigil = colors.bold(colors.bright_white('>'))
puts "#{sigil} #{colors.bold(colors.bright_cyan(request.method))} #{colors.cyan(request.url)}"
if options[:print_headers]
print_headers(sigil,request.headers)
end
if options[:print_body] && (body = request.body)
print_body(sigil,body)
end
end
#
# Prints a response.
#
# @param [Ferrum::Network::Response] response
# The respones object.
#
def print_response(response)
sigil = colors.bold(colors.bright_white('<'))
print "#{sigil} "
print_url_status(response)
if options[:print_headers]
print_headers(sigil,response.headers)
end
if options[:print_body]
print_body(sigil,response.body)
end
end
#
# Prints headers.
#
# @param [String] sigil
# The "sigil" representing either a request (`>`) or
# a response (`<`).
#
# @param [Hash{String => String}] headers
# The header names and values.
#
def print_headers(sigil,headers)
headers.each do |name,value|
puts "#{sigil} #{colors.bright_white(name)}: #{value}"
end
end
#
# Prints the request or response body.
#
# @param [String] sigil
# The "sigil" representing either a request (`>`) or
# a response (`<`).
#
# @param [String] body
# the request or response body.
#
def print_body(sigil,body)
puts sigil
response.body.each_line do |line|
puts "#{sigil} #{line}"
end
end
#
# Prints the `Set-Cookie` header for each HTTP response.
#
# @param [Ferrum::Network::Response] response
# A response from the browser.
#
def print_cookies(response)
if (set_cookie = respones.headers['set-cookie'])
puts set_cookie
end
end
end
end
end
end
end