# frozen_string_literal: true
#
# ronin-support-web - A web support library for ronin-rb.
#
# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
#
# ronin-support-web 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-support-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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ronin-support-web. If not, see .
#
require 'ronin/support/network/http'
require 'addressable/uri'
require 'nokogiri'
require 'json'
module Ronin
module Support
module Web
#
# Web Agent represents a stripped-down web browser, which can request
# URLs, follow redirects, and parse responses.
#
# ## Features
#
# * Automatically follows redirects.
# * Provides low-level HTTP methods.
# * Provides high-level methods for requesting and parsing HTML, XML, or
# JSON.
# * Maintains a persistent connection pool.
#
# ## Anti-Features
#
# * Does not cache files or write to the disk.
# * Does not evaluate JavaScript.
#
class Agent
#
# Base-class for all {Agent} exceptions.
#
class Error < RuntimeError
end
#
# Indicates that too many redirects were encountered in succession.
#
class TooManyRedirects < Error
end
#
# Indicates that the response does not have a compatible or expected
# `Content-Type` header.
#
class ContentTypeError < Error
end
# The proxy to send requests through.
#
# @return [URI::HTTP, Addressable::URI, nil]
attr_reader :proxy
# The `User-Agent` header value.
#
# @return [String, nil]
attr_reader :user_agent
# Maximum number of redirects to follow.
#
# @return [Integer]
attr_reader :max_redirects
#
# Initializes the Web agent.
#
# @param [String, URI::HTTP, Addressable::URI, nil] proxy
# The optional proxy to send requests through.
#
# @param [String, :random, :chrome, :chrome_linux, :chrome_macos, :chrome_windows, :chrome_iphone, :chrome_ipad, :chrome_android, :firefox, :firefox_linux, :firefox_macos, :firefox_windows, :firefox_iphone, :firefox_ipad, :firefox_android, :safari, :safari_macos, :safari_iphone, :safari_ipad, :edge, :linux, :macos, :windows, :iphone, :ipad, :android, nil] user_agent
# The default `User-Agent` string to add to each request.
#
# @param [Boolean, Hash{Symbol => Object}, nil] ssl
# Additional SSL/TLS configuration.
#
# @option ssl [String, nil] :ca_bundle
# The path to the CA bundle directory or file.
#
# @option ssl [OpenSSL::X509::Store, nil] :cert_store
# The certificate store to use for the SSL/TLS connection.
#
# @option ssl [Array<(name, version, bits, alg_bits)>, nil] :ciphers
# The accepted ciphers to use for the SSL/TLS connection.
#
# @option ssl [Integer, nil] :timeout
# The connection timeout limit.
#
# @option ssl [1, 1.1, 1.2, Symbol, nil] :version
# The desired SSL/TLS version.
#
# @option ssl [1, 1.1, 1.2, Symbol, nil] :min_version
# The minimum SSL/TLS version.
#
# @option ssl [1, 1.1, 1.2, Symbol, nil] :max_version
# The maximum SSL/TLS version.
#
# @option ssl [Proc, nil] :verify_callback
# The callback to use when verifying the server's certificate.
#
# @option ssl [Integer, nil] :verify_depth
# The verification depth limit.
#
# @option ssl [:none, :peer, :fail_if_no_peer_cert, true, false, Integer, nil] :verify
# The verification mode.
#
# @option ssl [Boolean, nil] :verify_hostname
# Indicates whether to verify the server's hostname.
#
def initialize(follow_redirects: true,
max_redirects: 20,
# HTTP options
proxy: Support::Network::HTTP.proxy,
ssl: nil,
user_agent: Support::Network::HTTP.user_agent)
@follow_redirects = follow_redirects
@max_redirects = max_redirects
# HTTP options
@proxy = proxy
@ssl = ssl
@user_agent = user_agent
@sessions = {}
end
#
# Indicates whether redirects will automatically be followed.
#
# @return [Boolean]
#
def follow_redirects?
@follow_redirects
end
#
# @!macro request_kwargs
# @option kwargs [String, nil] :query
# The query-string to append to the request path.
#
# @option kwargs [Hash, nil] :query_params
# The query-params to append to the request path.
#
# @option kwargs [String, nil] :user
# The user to authenticate as.
#
# @option kwargs [String, nil] :password
# The password to authenticate with.
#
# @option kwargs [Hash{Symbol,String => String}, nil] :headers
# Additional HTTP headers to use for the request.
#
# @option kwargs [String, :text, :xml, :html, :json, nil] :content_type
# The `Content-Type` header value for the request.
# If a Symbol is given it will be resolved to a common MIME type:
# * `:text` - `text/plain`
# * `:xml` - `text/xml`
# * `:html` - `text/html`
# * `:json` - `application/json`
#
# @option kwargs [String, :text, :xml, :html, :json, nil] :accept
# The `Accept` header value for the request.
# If a Symbol is given it will be resolved to a common MIME type:
# * `:text` - `text/plain`
# * `:xml` - `text/xml`
# * `:html` - `text/html`
# * `:json` - `application/json`
#
# @option kwargs [String, Hash{String => String}, Ronin::Support::Network::HTTP::Cookie, nil] :cookie
# Additional `Cookie` header.
# * If a `Hash` is given, it will be converted to a `String` using
# [Ronin::Support::Network::HTTP::Cookie](https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP/Cookie.html).
# * If the cookie value is empty, the `Cookie` header will not be
# set.
#
# @option kwargs [String, nil] :body
# The body of the request.
#
# @option kwargs [Hash, String, nil] :form_data
# The form data that may be sent in the body of the request.
#
# @option kwargs [#to_json, nil] :json
# The JSON data that will be sent in the body of the request.
# Will also default the `Content-Type` header to
# `application/json`, unless already set.
#
#
# Performs and arbitrary HTTP request.
#
# @param [Symbol, String] method
# The HTTP method to use for the request.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @raise [ArgumentError]
# The `:method` option did not match a known `Net::HTTP` request
# class.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#request-instance_method
#
# @api public
#
def http_request(method,url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).request(
method, uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Sends an arbitrary HTTP request and returns the response status.
#
# @param [Symbol, String] method
# The HTTP method to use for the request.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @return [Integer]
# The status code of the response.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#response_status-instance_method
#
# @api public
#
def http_response_status(method=:head,url,**kwargs)
uri = normalize_url(url)
session_for(uri).response_status(
method, uri.request_uri, user: uri.user,
password: uri.password,
**kwargs
)
end
#
# Sends a HTTP request and determines if the response status was 200.
#
# @param [Symbol, String] method
# The HTTP method to use for the request.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @return [Boolean]
# Indicates that the response status was 200.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#ok%3F-instance_method
#
# @api public
#
def http_ok?(method=:head,url,**kwargs)
uri = normalize_url(url)
session_for(uri).ok?(
method, uri.request_uri, user: uri.user,
password: uri.password,
**kwargs
)
end
#
# Sends an arbitrary HTTP request and returns the response headers.
#
# @param [Symbol, String] method
# The HTTP method to use for the request.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @return [Hash{String => String}]
# The response headers.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#response_headers-instance_method
#
# @api public
#
def http_response_headers(method=:head,url,**kwargs)
uri = normalize_url(url)
session_for(uri).response_headers(
method, uri.request_uri, user: uri.user,
password: uri.password,
**kwargs
)
end
#
# Sends an HTTP request and returns the `Server` header.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @return [String, nil]
# The `Server` header.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#server_header-instance_method
#
# @api public
#
def http_server_header(url,**kwargs)
uri = normalize_url(url)
session_for(uri).server_header(
user: uri.user,
password: uri.password,
path: uri.request_uri,
**kwargs
)
end
#
# Sends an HTTP request and returns the `X-Powered-By` header.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @return [String, nil]
# The `X-Powered-By` header.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#powered_by_header-instance_method
#
# @api public
#
def http_powered_by_header(url,**kwargs)
uri = normalize_url(url)
session_for(uri).powered_by_header(
user: uri.user,
password: uri.password,
path: uri.request_uri,
**kwargs
)
end
#
# Sends an arbitrary HTTP request and returns the response body.
#
# @param [Symbol, String] method
# The HTTP method to use for the request.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @return [String]
# The response body.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#response_body-instance_method
#
# @api public
#
def http_response_body(method=:get,url,**kwargs)
uri = normalize_url(url)
session_for(uri).response_body(
method, uri.request_uri, user: uri.user,
password: uri.password,
**kwargs
)
end
#
# Performs a `COPY` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#copy-instance_method
#
# @api public
#
def http_copy(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).copy(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Performs a `DELETE` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#delete-instance_method
#
# @api public
#
def http_delete(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).delete(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Performs a `GET` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#get-instance_method
#
# @api public
#
def http_get(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).get(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Performs a `GET` request for the given URI and returns the response
# headers.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @return [Hash{String => String}]
# The response headers.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#get_headers-instance_method
#
# @api public
#
def http_get_headers(url,**kwargs)
uri = normalize_url(url)
session_for(uri).get_headers(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs
)
end
#
# Sends an HTTP request and returns the parsed `Set-Cookie`
# header(s).
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @return [Array, nil]
# The parsed `SetCookie` header(s).
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#get_cookies-instance_method
#
# @api public
#
def http_get_cookies(url,**kwargs)
uri = normalize_url(url)
session_for(uri).get_cookies(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs
)
end
#
# Performs a `GET` request for the given URI and returns the response
# body.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @return [String]
# The response body.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#get_body-instance_method
#
# @api public
#
def http_get_body(url,**kwargs)
uri = normalize_url(url)
session_for(uri).get_body(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs
)
end
#
# Performs a `HEAD` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#head-instance_method
#
# @api public
#
def http_head(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).head(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Performs a `LOCK` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#lock-instance_method
#
# @api public
#
def http_lock(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).lock(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Performs a `MKCOL` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#mkcol-instance_method
#
# @api public
#
def http_mkcol(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).mkcol(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Performs a `MOVE` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#move-instance_method
#
# @api public
#
def http_move(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).move(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Performs a `OPTIONS` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#options-instance_method
#
# @api public
#
def http_options(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).options(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Performs a `OPTIONS` HTTP request for the given URI and parses the
# `Allow` response header.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @return [Array]
# The allowed HTTP request methods for the given URL.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#allowed_methods-instance_method
#
# @api public
#
def http_allowed_methods(url,**kwargs)
uri = normalize_url(url)
session_for(uri).allowed_methods(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs
)
end
#
# Performs a `PATCH` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#patch-instance_method
#
# @api public
#
def http_patch(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).patch(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Performs a `POST` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#post-instance_method
#
# @api public
#
def http_post(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).post(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Performs a `POST` request on the given URI and returns the response
# headers.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @return [Hash{String => String}]
# The response headers.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#post_headers-instance_method
#
# @api public
#
def http_post_headers(url,**kwargs)
uri = normalize_url(url)
session_for(uri).post_headers(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs
)
end
#
# Performs a `POST` request for the given URI and returns the
# response body.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @return [String]
# The response body.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#post_body-instance_method
#
# @api public
#
def http_post_body(url,**kwargs)
uri = normalize_url(url)
session_for(uri).post_body(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs
)
end
#
# Performs a `PROPFIND` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#propfind-instance_method
#
# @api public
#
def http_propfind(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).propfind(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
alias http_prop_find http_propfind
#
# Performs a `PROPPATCH` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#proppatch-instance_method
#
# @api public
#
def http_proppatch(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).proppatch(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
alias http_prop_patch http_proppatch
#
# Performs a `PUT` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#put-instance_method
#
# @api public
#
def http_put(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).put(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Performs a `TRACE` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#trace-instance_method
#
# @api public
#
def http_trace(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).trace(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Performs a `UNLOCK` request for the given URI.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#unlock-instance_method
#
# @api public
#
def http_unlock(url,**kwargs,&block)
uri = normalize_url(url)
session_for(uri).unlock(
uri.request_uri, user: uri.user,
password: uri.password,
**kwargs, &block
)
end
#
# Gets a URL and returns the response.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP GET request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @raise [TooManyRedirects]
# Maximum number of redirects reached.
#
# @note This method will follow redirects by default.
#
# @example
# response = agent.get('https://example.com/')
# # => #
#
def get(url, follow_redirects: @follow_redirects,
max_redirects: @max_redirects,
**kwargs)
response = http_get(url,**kwargs)
if follow_redirects && response.kind_of?(Net::HTTPRedirection)
redirect_count = 0
while response.kind_of?(Net::HTTPRedirection)
if redirect_count >= max_redirects
raise(TooManyRedirects,"maximum number of redirects reached: #{url.inspect}")
end
location = response['Location']
response = http_get(location)
redirect_count += 1
end
end
yield response if block_given?
return response
end
#
# Gets the URL and returns the parsed HTML.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP GET request for.
#
# @!macro request_kwargs
#
# @return [Nokogiri::HTML::Document]
# The parsed HTML response.
#
# @raise [ContentTypeError]
# Did not receive a response with a `Content-Type` of `text/html`.
#
# @raise [TooManyRedirects]
# Maximum number of redirects reached.
#
# @note This method will follow redirects by default.
#
# @example
# doc = agent.get_html('https://example.com/page.html')
# # => #
#
def get_html(url,**kwargs)
response = get(url,**kwargs)
unless response.content_type.include?('text/html')
raise(ContentTypeError,"response 'Content-Type' was not 'text/html': #{response.content_type.inspect}")
end
return Nokogiri::HTML(response.body)
end
#
# Gets the URL and returns the parsed XML.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP GET request for.
#
# @!macro request_kwargs
#
# @return [Nokogiri::XML::Document]
# The parsed XML response.
#
# @raise [ContentTypeError]
# Did not receive a response with a `Content-Type` of `text/xml`.
#
# @raise [TooManyRedirects]
# Maximum number of redirects reached.
#
# @note This method will follow redirects by default.
#
# @example
# doc = agent.get_xml('https://example.com/data.xml')
# # => #
#
def get_xml(url,**kwargs)
response = get(url,**kwargs)
unless response.content_type.include?('text/xml')
raise(ContentTypeError,"response 'Content-Type' was not 'text/xml': #{response.content_type.inspect}")
end
return Nokogiri::XML(response.body)
end
#
# Gets the URL and returns the parsed JSON.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP GET request for.
#
# @!macro request_kwargs
#
# @return [Hash{String => Object}, Array]
# The parsed JSON.
#
# @raise [ContentTypeError]
# Did not receive a response with a `Content-Type` of
# `application/json`.
#
# @raise [TooManyRedirects]
# Maximum number of redirects reached.
#
# @note This method will follow redirects by default.
#
# @example
# json = agent.get_json('https://example.com/data.json')
# # => {...}
#
def get_json(url,**kwargs)
response = get(url,**kwargs)
unless response.content_type.include?('application/json')
raise(ContentTypeError,"response 'Content-Type' was not 'application/json': #{response.content_type.inspect}")
end
return ::JSON.parse(response.body)
end
#
# Performs an HTTP POST to the URL.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP GET request for.
#
# @!macro request_kwargs
#
# @yield [response]
# If a block is given it will be passed the received HTTP response.
#
# @yieldparam [Net::HTTPResponse] response
# The received HTTP response object.
#
# @return [Net::HTTPResponse]
# The HTTP response object.
#
# @raise [TooManyRedirects]
# Maximum number of redirects reached.
#
# @note
# If the response is an HTTP redirect, then {#get} will be called to
# follow any redirects.
#
# @example
# response = agent.post('https://example.com/form', form_data: {'foo' => 'bar'})
# # => #
#
def post(url, follow_redirects: @follow_redirects,
max_redirects: @max_redirects,
**kwargs)
response = http_post(url,**kwargs)
if follow_redirects && response.kind_of?(Net::HTTPRedirection)
location = response['Location']
response = begin
get(location, follow_redirects: follow_redirects,
max_redirects: max_redirects - 1)
rescue TooManyRedirects
raise(TooManyRedirects,"maximum number of redirects reached: #{url.inspect}")
end
end
yield response if block_given?
return response
end
#
# Performs an HTTP POST to the URL and parses the HTML response.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP POST request for.
#
# @!macro request_kwargs
#
# @return [Nokogiri::HTML::Document]
# The parsed HTML response.
#
# @raise [TooManyRedirects]
# Maximum number of redirects reached.
#
# @raise [ContentTypeError]
# Did not receive a response with a `Content-Type` of
# `text/html`.
#
# @note
# If the response is an HTTP redirect, then {#get} will be called to
# follow any redirects.
#
# @example Send a POST request and parses the HTML response:
# doc = agent.post_html 'https://example.com/form', form_data: {foo: 'bar'})
# # => #
#
def post_html(url,**kwargs)
response = post(url,**kwargs)
unless response.content_type.include?('text/html')
raise(ContentTypeError,"response 'Content-Type' was not 'text/html': #{response.content_type.inspect}")
end
return Nokogiri::HTML(response.body)
end
#
# Performs an HTTP POST to the URL and parses the XML response.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP POST request for.
#
# @!macro request_kwargs
#
# @return [Nokogiri::XML::Document]
# The parsed XML response.
#
# @raise [TooManyRedirects]
# Maximum number of redirects reached.
#
# @raise [ContentTypeError]
# Did not receive a response with a `Content-Type` of
# `text/xml`.
#
# @note
# If the response is an HTTP redirect, then {#get} will be called to
# follow any redirects.
#
# @example Send a POST request to the form and parses the XML response:
# doc = agent.post_xml 'https://example.com/form', form_data: {foo: 'bar'}
# # => #
#
def post_xml(url,**kwargs)
response = post(url,**kwargs)
unless response.content_type.include?('text/xml')
raise(ContentTypeError,"response 'Content-Type' was not 'application/json': #{response.content_type.inspect}")
end
return Nokogiri::XML(response.body)
end
#
# Performs an HTTP POST to the URL and parses the JSON response.
#
# @param [URI::HTTP, Addressable::URI, String] url
# The URL to create the HTTP POST request for.
#
# @!macro request_kwargs
#
# @return [Hash{String => Object}, Array]
# The parses JSON response.
#
# @raise [TooManyRedirects]
# Maximum number of redirects reached.
#
# @raise [ContentTypeError]
# Did not receive a response with a `Content-Type` of
# `application/json`.
#
# @note
# If the response is an HTTP redirect, then {#get} will be called to
# follow any redirects.
#
# @example Send a POST request to the form and parse the JSON response:
# json = agent.post_json 'https://example.com/form', form_data: {foo: 'bar'}
# # => {...}
#
# @example Send a POST request containing JSON and parse the JSON response:
# json = agent.post_json 'https://example.com/api/end-point', json: {foo: 'bar'}
# # => {...}
#
def post_json(url,**kwargs)
response = post(url,**kwargs)
unless response.content_type.include?('application/json')
raise(ContentTypeError,"response 'Content-Type' was not 'application/json': #{response.content_type.inspect}")
end
return ::JSON.parse(response.body)
end
private
#
# Normalizes a URL.
#
# @param [URI::HTTP, Addressable::URI, String, Object] url
# The URL or URI to normalize.
#
# @return [URI::HTTP, Addressable::URI]
# The parsed URL.
#
def normalize_url(url)
case url
when URI::HTTP, Addressable::URI then url
when String then Addressable::URI.parse(url)
else
raise(ArgumentError,"url must be a URI::HTTP, Addressable::URI, or a String: #{url.inspect}")
end
end
#
# Fetches an existing HTTP session or creates a new one for the given
# URI.
#
# @param [URI::HTTP] uri
# The URL to retrieve or create an HTTP session for.
#
# @return [Ronin::Support::Network::HTTP]
# The HTTP session.
#
def session_for(uri)
key = [uri.scheme, uri.host, uri.port]
@sessions[key] ||= Support::Network::HTTP.connect_uri(
uri, proxy: @proxy,
ssl: (@ssl if uri.scheme == 'https'),
user_agent: @user_agent
)
end
end
end
end
end