# frozen_string_literal: true
require "forwardable"
require "base64"
require "time"
require "http/errors"
require "http/headers"
require "http/request/body"
require "http/request/writer"
require "http/version"
require "http/uri"
module HTTP
class Request
extend Forwardable
include HTTP::Headers::Mixin
# The method given was not understood
class UnsupportedMethodError < RequestError; end
# The scheme of given URI was not understood
class UnsupportedSchemeError < RequestError; end
# Default User-Agent header value
USER_AGENT = "http.rb/#{HTTP::VERSION}"
METHODS = [
# RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
:options, :get, :head, :post, :put, :delete, :trace, :connect,
# RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV
:propfind, :proppatch, :mkcol, :copy, :move, :lock, :unlock,
# RFC 3648: WebDAV Ordered Collections Protocol
:orderpatch,
# RFC 3744: WebDAV Access Control Protocol
:acl,
# RFC 6352: vCard Extensions to WebDAV -- CardDAV
:report,
# RFC 5789: PATCH Method for HTTP
:patch,
# draft-reschke-webdav-search: WebDAV Search
:search
].freeze
# Allowed schemes
SCHEMES = %i[http https ws wss].freeze
# Default ports of supported schemes
PORTS = {
:http => 80,
:https => 443,
:ws => 80,
:wss => 443
}.freeze
# Method is given as a lowercase symbol e.g. :get, :post
attr_reader :verb
# Scheme is normalized to be a lowercase symbol e.g. :http, :https
attr_reader :scheme
# "Request URI" as per RFC 2616
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
attr_reader :uri
attr_reader :proxy, :body, :version
# @option opts [String] :version
# @option opts [#to_s] :verb HTTP request method
# @option opts [HTTP::URI, #to_s] :uri
# @option opts [Hash] :headers
# @option opts [Hash] :proxy
# @option opts [String, Enumerable, IO, nil] :body
def initialize(opts)
@verb = opts.fetch(:verb).to_s.downcase.to_sym
@uri = normalize_uri(opts.fetch(:uri))
@scheme = @uri.scheme.to_s.downcase.to_sym if @uri.scheme
raise(UnsupportedMethodError, "unknown method: #{verb}") unless METHODS.include?(@verb)
raise(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme)
@proxy = opts[:proxy] || {}
@body = request_body(opts[:body], opts)
@version = opts[:version] || "1.1"
@headers = HTTP::Headers.coerce(opts[:headers] || {})
@headers[Headers::HOST] ||= default_host_header_value
@headers[Headers::USER_AGENT] ||= USER_AGENT
end
# Returns new Request with updated uri
def redirect(uri, verb = @verb)
headers = self.headers.dup
headers.delete(Headers::HOST)
self.class.new(
:verb => verb,
:uri => @uri.join(uri),
:headers => headers,
:proxy => proxy,
:body => body,
:version => version
)
end
# Stream the request to a socket
def stream(socket)
include_proxy_headers if using_proxy? && !@uri.https?
Request::Writer.new(socket, body, headers, headline).stream
end
# Is this request using a proxy?
def using_proxy?
proxy && proxy.keys.size >= 2
end
# Is this request using an authenticated proxy?
def using_authenticated_proxy?
proxy && proxy.keys.size >= 4
end
def include_proxy_headers
headers.merge!(proxy[:proxy_headers]) if proxy.key?(:proxy_headers)
include_proxy_authorization_header if using_authenticated_proxy?
end
# Compute and add the Proxy-Authorization header
def include_proxy_authorization_header
headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header
end
def proxy_authorization_header
digest = Base64.strict_encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}")
"Basic #{digest}"
end
# Setup tunnel through proxy for SSL request
def connect_using_proxy(socket)
Request::Writer.new(socket, nil, proxy_connect_headers, proxy_connect_header).connect_through_proxy
end
# Compute HTTP request header for direct or proxy request
def headline
request_uri =
if using_proxy? && !uri.https?
uri.omit(:fragment)
else
uri.request_uri
end
"#{verb.to_s.upcase} #{request_uri} HTTP/#{version}"
end
# Compute HTTP request header SSL proxy connection
def proxy_connect_header
"CONNECT #{host}:#{port} HTTP/#{version}"
end
# Headers to send with proxy connect request
def proxy_connect_headers
connect_headers = HTTP::Headers.coerce(
Headers::HOST => headers[Headers::HOST],
Headers::USER_AGENT => headers[Headers::USER_AGENT]
)
connect_headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header if using_authenticated_proxy?
connect_headers.merge!(proxy[:proxy_headers]) if proxy.key?(:proxy_headers)
connect_headers
end
# Host for tcp socket
def socket_host
using_proxy? ? proxy[:proxy_address] : host
end
# Port for tcp socket
def socket_port
using_proxy? ? proxy[:proxy_port] : port
end
private
# Transforms body to an object suitable for streaming.
def request_body(body, opts)
body = Request::Body.new(body) unless body.is_a?(Request::Body)
body = opts[:auto_deflate].deflated_body(body) if opts[:auto_deflate]
body
end
# @!attribute [r] host
# @return [String]
def_delegator :@uri, :host
# @!attribute [r] port
# @return [Fixnum]
def port
@uri.port || @uri.default_port
end
# @return [String] Default host (with port if needed) header value.
def default_host_header_value
PORTS[@scheme] != port ? "#{host}:#{port}" : host
end
# @return [HTTP::URI] URI with all componentes but query being normalized.
def normalize_uri(uri)
uri = HTTP::URI.parse uri
HTTP::URI.new(
:scheme => uri.normalized_scheme,
:authority => uri.normalized_authority,
:path => uri.normalized_path,
:query => uri.query,
:fragment => uri.normalized_fragment
)
end
end
end