2020-03-12 16:37:13 +00:00
|
|
|
# frozen_string_literal: true
|
2020-03-12 10:14:54 +00:00
|
|
|
|
2020-03-12 16:37:13 +00:00
|
|
|
require 'roda/proxy/version'
|
2023-04-02 13:53:52 +00:00
|
|
|
require 'net_http_unix'
|
|
|
|
|
2020-03-12 16:37:13 +00:00
|
|
|
# :nodoc:
|
2020-03-12 10:14:54 +00:00
|
|
|
class Roda
|
2020-03-12 16:37:13 +00:00
|
|
|
# :nodoc:
|
2020-03-12 10:14:54 +00:00
|
|
|
module RodaPlugins
|
2020-03-12 16:37:13 +00:00
|
|
|
# Roda plugin for simple API proxying
|
2020-03-12 10:14:54 +00:00
|
|
|
module Proxy
|
|
|
|
|
2020-03-12 16:37:13 +00:00
|
|
|
# Respond to the configure method to set the destination when proxying
|
|
|
|
# Expects the following options:
|
|
|
|
# [to] Required. The scheme and host of the proxy. Should not end with a slash.
|
2020-03-13 10:47:21 +00:00
|
|
|
# [path_prefix] Optional. The path to append to the above for proxying.
|
|
|
|
# The current request path will be prefixed on to this value.
|
|
|
|
# Should begin and end with a +/+. Defaults to +/+.
|
|
|
|
# For example, if the path prefix is +/foo/+ and the request received
|
|
|
|
# by Roda is +GET /postcode/lookup+, The proxied request will be dispatched
|
|
|
|
# to +GET /home/postcode/lookup+
|
2020-03-12 16:37:13 +00:00
|
|
|
# Example:
|
|
|
|
# plugin :proxy, to: 'https://foo.bar', path: '/my/api'
|
2020-03-12 10:14:54 +00:00
|
|
|
def self.configure(app, opts = {})
|
|
|
|
app.opts[:proxy_to] = opts.fetch(:to, nil)
|
2020-03-13 10:47:21 +00:00
|
|
|
app.opts[:proxy_path] = opts.fetch(:path_prefix, '/')
|
2020-03-12 10:14:54 +00:00
|
|
|
|
|
|
|
raise 'Proxy host not set, use "plugin :proxy, to: http://example.com"' unless app.opts[:proxy_to]
|
|
|
|
end
|
|
|
|
|
2020-03-12 16:37:13 +00:00
|
|
|
# :nodoc:
|
2020-03-12 10:14:54 +00:00
|
|
|
module RequestMethods
|
2020-03-12 16:37:13 +00:00
|
|
|
|
|
|
|
# Proxies the request, forwarding all headers except +Host+ which is
|
|
|
|
# rewritten to be the destination host. The response headers, body and
|
|
|
|
# status are returned to the client.
|
2020-03-12 10:14:54 +00:00
|
|
|
def proxy
|
2023-04-02 13:53:52 +00:00
|
|
|
client = NetX::HTTPUnix.new(_sock_url)
|
|
|
|
request_class = Net::HTTP.const_get(:"#{env['REQUEST_METHOD'].to_s.downcase.capitalize}")
|
|
|
|
req = request_class.new(_proxy_url.to_s, _proxy_headers)
|
2023-04-03 14:37:48 +00:00
|
|
|
env_body = env["rack.input"].read
|
|
|
|
env_content_type = env['CONTENT_TYPE']
|
|
|
|
unless env_body.nil?
|
|
|
|
req.body = env_body
|
|
|
|
end
|
|
|
|
unless env_content_type.nil?
|
|
|
|
req.content_type = env_content_type
|
|
|
|
end
|
2023-04-02 13:53:52 +00:00
|
|
|
f_response = client.request(req)
|
2023-04-03 14:37:48 +00:00
|
|
|
f_response.content_length = nil #prevent duplicate header Content-Length and content-length
|
2020-03-12 16:37:13 +00:00
|
|
|
_respond(f_response)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Conditionally proxies when +condition+ is true and with selective probability.
|
|
|
|
# For instance, to proxy 50% of the time:
|
|
|
|
# r.proxy_when(probability: 0.5)
|
|
|
|
# Condition can be a truthy value or a block / lambda, in which case
|
|
|
|
# the result from the +#call+ is expected to be truthy.
|
|
|
|
# r.proxy_when( r.env['HTTP_PROXY_ME'] == 'true' )
|
|
|
|
# The two parameters can be combined, the probability is evaluated first.
|
|
|
|
# r.proxy_when( r.env['HTTP_PROXY_ME'] == 'true', probability: 0.8 )
|
|
|
|
# If and only if this method choses not to proxy is the block evaluated.
|
|
|
|
# The block is then expected to return a meaningful response to Roda.
|
|
|
|
def proxy_when(condition = true, probability: 1.0)
|
|
|
|
shall_proxy = Random.rand(0.0..1.0) <= probability
|
|
|
|
|
|
|
|
if shall_proxy && ( condition.respond_to?(:call) ? condition.call : condition )
|
|
|
|
proxy
|
2020-03-13 11:33:40 +00:00
|
|
|
else
|
|
|
|
yield(self)
|
2020-03-12 16:37:13 +00:00
|
|
|
end
|
2020-03-12 10:14:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2023-04-02 13:53:52 +00:00
|
|
|
def _sock_url
|
|
|
|
roda_class.opts[:proxy_to]
|
|
|
|
end
|
2020-03-12 10:14:54 +00:00
|
|
|
def _proxy_url
|
2023-04-02 13:53:52 +00:00
|
|
|
uri = URI("#{roda_class.opts[:proxy_path]}#{env['PATH_INFO'][1..-1]}")
|
2023-04-03 14:37:48 +00:00
|
|
|
if !env['QUERY_STRING'].empty?
|
|
|
|
uri.query = env['QUERY_STRING']
|
|
|
|
end
|
2023-04-02 13:53:52 +00:00
|
|
|
uri
|
2020-03-12 10:14:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def _proxy_headers
|
2023-04-03 14:37:48 +00:00
|
|
|
env_host = env['HTTP_HOST'].to_s
|
|
|
|
env_new = env
|
2020-03-12 16:37:13 +00:00
|
|
|
.select { |k, _v| k.start_with? 'HTTP_' }
|
|
|
|
.reject { |k, _v| k == 'HTTP_HOST' }
|
2020-03-12 10:14:54 +00:00
|
|
|
.transform_keys do |k|
|
|
|
|
k.sub(/^HTTP_/, '')
|
2020-03-12 16:37:13 +00:00
|
|
|
.split('_')
|
|
|
|
.map(&:capitalize)
|
|
|
|
.join('-')
|
2020-03-13 10:47:21 +00:00
|
|
|
end
|
2023-04-03 14:37:48 +00:00
|
|
|
if env_host
|
|
|
|
env_new.merge!({'Host' => env_host})
|
|
|
|
end
|
|
|
|
env_new
|
2020-03-12 10:14:54 +00:00
|
|
|
end
|
|
|
|
|
2020-03-12 16:37:13 +00:00
|
|
|
def _respond(proxied_response)
|
2023-04-02 15:21:58 +00:00
|
|
|
response.status = proxied_response.code.to_i
|
2020-03-26 11:36:27 +00:00
|
|
|
proxied_response
|
2023-04-02 15:21:58 +00:00
|
|
|
.each_header.to_h
|
2020-03-26 11:36:27 +00:00
|
|
|
.reject { |k, v| k.downcase == 'transfer-encoding' }
|
|
|
|
.each { |k, v| response[k] = v }
|
2020-03-12 16:37:13 +00:00
|
|
|
response.write(proxied_response.body)
|
|
|
|
end
|
2023-04-03 14:37:48 +00:00
|
|
|
|
2020-03-12 10:14:54 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
register_plugin :proxy, Proxy
|
|
|
|
end
|
|
|
|
end
|