Skip to content

Instantly share code, notes, and snippets.

@buzztaiki
Last active July 29, 2016 20:51
Show Gist options
  • Save buzztaiki/6078145 to your computer and use it in GitHub Desktop.
Save buzztaiki/6078145 to your computer and use it in GitHub Desktop.
webrickでreverse-proxyをするサンプル。
# HTTP Reverse proxy server
# Original Source: http://rubyforge.org/snippet/download.php?type=snippet&id=162
# Use case: you have several services running as different users
# for security purposes (they might even be chrooted).
# In production we use apache but for testing I prefer to use
# webrick because I find it more flexible for unit testing.
# The proxy mapping is modelled on the ProxyPass directive
# of apache. For example:
#
# original URL proxies private URL
# ------------------------ ==> --------------------------
# /marmalade/index.html localhost:8081/index.html
# /apps/vegemite?id=123 localhost:8082/apps/someservlet?id=123
#
# Its not designed to be mod_rewrite (eg. query_string cannot be transformed),
# but you can specify proxy rules that match a fragment of the original
# URL and replace it with something else while also sending the new URL
# to the proxied host and port. So the rules in that example are specified thus:
#
# serverConfig = {
# :Port => 80,
# :ProxyRules => [
# ProxyRule.new('^/marmalade/', 'http://localhost:8081', '/')
# ProxyRule.new('vegemite', 'http://localhost:8082', 'someservlet')
# ]
# }
# server = HTTPReverseProxyServer.new(serverConfig)
require 'webrick'
require 'webrick/httputils'
require 'net/http'
require 'uri'
ProxyRule = Struct.new("ProxyRule", :pattern, :url, :replacement)
class HTTPReverseProxyServer < WEBrick::HTTPServer
def service(req, res)
rule = find_proxy_rule(req)
if rule.nil?
super(req, res)
else
service_proxy(req, res, rule)
end
end
def find_proxy_rule(req)
find_proxy_rule_by_parameter(req) || first_matching_proxy_rule(req)
end
# find the *first* matching pattern in the proxy map
def first_matching_proxy_rule(req)
@config[:ProxyRules].find { |rule|
re = Regexp.new(rule.pattern)
m = re.match(req.path)
not m.nil?
}
end
def find_proxy_rule_by_parameter(req)
query = req.query
key = @config[:ProxyRemoteHostParameter]
host = query[key]
if host
logger.debug("found remote host parameter: #{key}=#{host}")
ProxyRule.new('.*', host, '\&')
end
end
def map_to_proxyURI(req, rule)
uri = URI.parse(rule.url)
uri.path = req.path.sub(%r!#{rule.pattern}!, rule.replacement)
query = req.query.reject {|k, v|
k == @config[:ProxyRemoteHostParameter]
}.map {|k, v|
[k, WEBrick::HTTPUtils.escape(v)].join('=')
}.join('&')
uri.query = query unless query.empty?
uri
end
def service_proxy(req, res, rule)
uri = map_to_proxyURI(req, rule)
path = [uri.path, uri.query].reject(&:nil?).join('?')
# convert WEBrick header (values wrapped in an array) into Net::HTTP header (simple values)
header = {}
req.header.keys.each do |key|
next if key == 'host' && (not @config[:ProxyPreserveHost])
header[key] = req.header[key][0]
end
header['x-forwarded-for'] = req.peeraddr[2] # the name of the requesting host
# send the new request to the private server (hacked from WEBrick::HTTPProxyServer)
response = nil
begin
http = http_of(uri)
http.start {
case req.request_method
when "GET" then response = http.get(path, header)
when "POST" then response = http.post(path, req.body || "", header)
when "HEAD" then response = http.head(path, header)
else
raise HTTPStatus::MethodNotAllowed,
"unsupported method `#{req.request_method}'."
end
}
rescue => err
logger.debug("#{err.class}: #{err.message}")
raise HTTPStatus::ServiceUnavailable, err.message
end
res['connection'] = "close"
# Convert Net::HTTP::HTTPResponse to WEBrick::HTTPResponse
res.status = response.code.to_i
choose_header(response, res)
rewrite_location(req, rule, response, res)
res.body = response.body
# Process contents
if handler = @config[:ProxyContentHandler]
handler.call(req, res)
end
end
def http_of(uri)
if uri.scheme == 'https'
Net::HTTP.new(uri.host, uri.port).tap {|http|
http.use_ssl = true
}
else
Net::HTTP.new(uri.host, uri.port)
end
end
## copy from webrick
# Some header fields should not be transferred.
HopByHop = %w( connection keep-alive proxy-authenticate upgrade
proxy-authorization te trailers transfer-encoding )
def split_field(f) f ? f.split(/,\s+/).collect{|i| i.downcase } : [] end
def choose_header(src, dst)
connections = split_field(src['connection'])
src.each{|key, value|
key = key.downcase
if HopByHop.member?(key) || # RFC2616: 13.5.1
connections.member?(key) # RFC2616: 14.10
@logger.debug("choose_header: `#{key}: #{value}'")
next
end
dst[key] = value
}
end
def rewrite_location(req, rule, src, dst)
loc = src['location']
if loc
uri = URI.parse(loc)
uri.host = req.host
uri.port = req.port
uri.scheme = req.request_uri.scheme
uri.path = uri.path.sub(%r!#{rule.pattern}!, rule.replacement)
dst['location'] = uri.to_s
end
end
end
if $0 == __FILE__
server = HTTPReverseProxyServer.new(
Port: 9999,
ProxyRules: [
ProxyRule.new('.*', 'http://127.0.0.1:8080', '\&')
# ProxyRule.new('.*', 'https://127.0.0.1:8443', '\&')
],
ProxyPreserveHost: false,
# ProxyRemoteHostParameter: 'rproxy_remote_host',
)
trap 'INT' do server.shutdown end
server.start
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment