Class: Nonnative::HTTPProxy

Inherits:
HTTPService
  • Object
show all
Defined in:
lib/nonnative/http_proxy_server.rb

Overview

HTTP service implementing a simple forward proxy.

The upstream host is configured via service settings (see HTTPProxyServer).

Supported HTTP verbs: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS.

Constant Summary collapse

NON_FORWARDABLE_RESPONSE_HEADERS =
%w[
  connection
  content-encoding
  content-length
  keep-alive
  location
  proxy-authenticate
  proxy-authorization
  proxy-authentication-info
  proxy-connection
  set-cookie
  status
  te
  trailer
  transfer-encoding
  upgrade
].freeze
NON_FORWARDABLE_REQUEST_HEADERS =
%w[
  host
  accept-encoding
  version
  proxy-authenticate
  proxy-authorization
  proxy-connection
  connection
  keep-alive
  te
  trailer
  transfer-encoding
  upgrade
].freeze
GATEWAY_TIMEOUT_ERRORS =
[
  RestClient::Exceptions::Timeout
].freeze
GATEWAY_CONNECTION_ERRORS =
[
  RestClient::ServerBrokeConnection,
  Errno::ECONNREFUSED,
  Errno::EHOSTUNREACH,
  SocketError
].freeze
PATH_ESCAPE_PATTERN =

Match malformed percent escapes and bytes outside RFC 3986's path-safe set. Valid %HH escapes remain intact; each match is percent-encoded before building the upstream URI.

%r{%(?![\da-fA-F]{2})|[^A-Za-z0-9\-._~!$&'()*+,;=:@/%]}

Instance Method Summary collapse

Instance Method Details

#api_response(method:, url:, headers:, payload: nil) ⇒ RestClient::Response

Executes the upstream request and returns the response.

Parameters:

  • method (Symbol)

    HTTP verb name (e.g. :get)

  • url (String)

    upstream URL

  • headers (Hash)

    request headers

  • payload (String, nil) (defaults to: nil)

    request payload

Returns:

  • (RestClient::Response)

    response for error statuses, otherwise RestClient return value

Raises:

  • (Sinatra::Halt)

    with a gateway status when the upstream is unavailable or times out



114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/nonnative/http_proxy_server.rb', line 114

def api_response(method:, url:, headers:, payload: nil)
  options = { method:, url:, headers: }
  options[:payload] = payload unless payload.nil?

  RestClient::Request.execute(options)
rescue *GATEWAY_TIMEOUT_ERRORS
  halt 504
rescue *GATEWAY_CONNECTION_ERRORS
  halt 502
rescue RestClient::Exception => e
  return e.response if e.response

  halt 502
end

#build_url(request, settings) ⇒ String

Builds the upstream URL for the given request.

Parameters:

  • request (Sinatra::Request)

    the incoming request

  • settings (Sinatra::Base)

    Sinatra settings, expected to include host, scheme, and port

Returns:

  • (String)

    upstream URL



98
99
100
101
102
103
104
# File 'lib/nonnative/http_proxy_server.rb', line 98

def build_url(request, settings)
  uri_class = settings.scheme == 'http' ? URI::HTTP : URI::HTTPS
  query = request.query_string
  path = escape_path(request.path_info)

  uri_class.build(host: settings.host, port: settings.port, path:, query: query.empty? ? nil : query).to_s
end

#forward_request_headers(request) ⇒ Hash{String=>String}

Extracts request headers from the Rack environment and normalizes them to standard HTTP names.

Certain hop-by-hop or proxy-specific headers are removed.

Parameters:

  • request (Sinatra::Request)

    the incoming request

Returns:

  • (Hash{String=>String})

    headers to forward to the upstream



82
83
84
85
86
87
88
89
90
91
# File 'lib/nonnative/http_proxy_server.rb', line 82

def forward_request_headers(request)
  headers = request.env.each_with_object({}) do |(header, value), result|
    next unless forward_request_header?(header)

    result[normalized_request_header_name(header)] = value
  end

  connection_headers = request_connection_headers(headers)
  headers.reject { |header, _| non_forwardable_request_header?(header, connection_headers) }
end

#retrieve_payload(request, verb) ⇒ String?

Extracts the request payload for verbs that can carry a body.

Parameters:

  • request (Sinatra::Request)

    the incoming request

  • verb (String)

    HTTP verb name (e.g. "post")

Returns:

  • (String, nil)

    request payload for body-carrying verbs



134
135
136
137
138
139
# File 'lib/nonnative/http_proxy_server.rb', line 134

def retrieve_payload(request, verb)
  return unless %w[post put patch delete].include?(verb)

  payload = request.body.read
  payload unless payload.empty?
end