Last active
July 29, 2016 20:51
-
-
Save buzztaiki/6078145 to your computer and use it in GitHub Desktop.
webrickでreverse-proxyをするサンプル。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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