class Proxy
A HTTP middleware for proxying requests to a given set of hosts. Typically used for implementing virtual servers.
Definitions
HOP_HEADERS = [...]
HTTP hop headers which should not be passed through the proxy.
Implementation
HOP_HEADERS = [
'connection',
'keep-alive',
'public',
'proxy-authenticate',
'transfer-encoding',
'upgrade',
]
def initialize(app, hosts)
Initialize the proxy middleware.
Signature
-
parameter
app
Protocol::HTTP::Middleware
The middleware to use if a request can't be proxied.
-
parameter
hosts
Hash(String, Service::Proxy)
The host applications to proxy to.
Implementation
def initialize(app, hosts)
super(app)
@server_context = nil
@hosts = hosts
@clients = {}
@count = 0
end
attr :count
The number of requests that have been proxied.
Signature
-
attribute
Integer
def close
Close all the connections to the upstream hosts.
Implementation
def close
@clients.each_value(&:close)
super
end
def connect(endpoint)
Establish a connection to the specified upstream endpoint.
Signature
-
parameter
endpoint
Async::HTTP::Endpoint
Implementation
def connect(endpoint)
@clients[endpoint] ||= Async::HTTP::Client.new(endpoint)
end
def lookup(request)
Lookup the appropriate host for the given request.
Signature
-
parameter
request
Protocol::HTTP::Request
-
returns
Service::Proxy
Implementation
def lookup(request)
# Trailing dot and port is ignored/normalized.
if authority = request.authority&.sub(/(\.)?(:\d+)?$/, '')
return @hosts[authority]
end
end
def prepare_headers(headers)
Prepare the headers to be sent to an upstream host. In particular, we delete all connection and hop headers.
Implementation
def prepare_headers(headers)
if connection = headers[CONNECTION]
headers.extract(connection)
end
headers.extract(HOP_HEADERS)
end
def prepare_request(request, host)
Prepare the request to be proxied to the specified host.
In particular, we set appropriate VIA = 'via'
, FORWARDED = 'forwarded'
, X_FORWARDED_FOR = 'x-forwarded-for'
and X_FORWARDED_PROTO = 'x-forwarded-proto'
headers.
Implementation
def prepare_request(request, host)
forwarded = []
Console.logger.debug(self) do |buffer|
buffer.puts "Request authority: #{request.authority}"
buffer.puts "Host authority: #{host.authority}"
buffer.puts "Request: #{request.method} #{request.path} #{request.version}"
buffer.puts "Request headers: #{request.headers.inspect}"
end
# The authority of the request must match the authority of the endpoint we are proxying to, otherwise SNI and other things won't work correctly.
request.authority = host.authority
if address = request.remote_address
request.headers.add(X_FORWARDED_FOR, address.ip_address)
forwarded << "for=#{address.ip_address}"
end
if scheme = request.scheme
request.headers.add(X_FORWARDED_PROTO, scheme)
forwarded << "proto=#{scheme}"
end
unless forwarded.empty?
request.headers.add(FORWARDED, forwarded.join(';'))
end
request.headers.add(VIA, "#{request.version} #{self.class}")
self.prepare_headers(request.headers)
return request
end
def call(request)
Proxy the request if the authority matches a specific host.
Signature
-
parameter
request
Protocol::HTTP::Request
Implementation
def call(request)
if host = lookup(request)
@count += 1
request = self.prepare_request(request, host)
client = connect(host.endpoint)
client.call(request)
else
super
end
rescue
Console.logger.error(self) {$!}
return Protocol::HTTP::Response[502, {'content-type' => 'text/plain'}, ["#{$!.inspect}: #{$!.backtrace.join("\n")}"]]
end