aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ChangeLog8
-rw-r--r--lib/webrick/httpproxy.rb240
-rw-r--r--test/webrick/test_httpproxy.rb281
-rw-r--r--test/webrick/utils.rb10
4 files changed, 434 insertions, 105 deletions
diff --git a/ChangeLog b/ChangeLog
index 33b796e2ca..2eaa1a2604 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,11 @@
+Mon Dec 31 23:17:22 2007 GOTOU Yuuzou <gotoyuzo@notwork.org>
+
+ * lib/webrick/httpproxy.rb (WEBrick::HTTPProxyServer#proxy_service):
+ call do_XXX which corespond with request method.
+ (WEBrick::HTTPProxyServer#do_CONNECT,do_GET,do_POST,do_HEAD): added.
+
+ * test/webrick/test_httpproxy.rb: add test for WEBrick::HTTPProxyServer.
+
Mon Dec 31 22:53:29 2007 Yukihiro Matsumoto <matz@ruby-lang.org>
* thread_pthread.c (native_sleep): timespec tv_sec may overflow on
diff --git a/lib/webrick/httpproxy.rb b/lib/webrick/httpproxy.rb
index 32603e763a..49e618d4e6 100644
--- a/lib/webrick/httpproxy.rb
+++ b/lib/webrick/httpproxy.rb
@@ -23,6 +23,16 @@ module WEBrick
alias gets read
end
+ FakeProxyURI = Object.new
+ class << FakeProxyURI
+ def method_missing(meth, *args)
+ if %w(scheme host port path query userinfo).member?(meth.to_s)
+ return nil
+ end
+ super
+ end
+ end
+
class HTTPProxyServer < HTTPServer
def initialize(config={}, default=Config::HTTP)
super(config, default)
@@ -32,7 +42,7 @@ module WEBrick
def service(req, res)
if req.request_method == "CONNECT"
- proxy_connect(req, res)
+ do_CONNECT(req, res)
elsif req.unparsed_uri =~ %r!^http://!
proxy_service(req, res)
else
@@ -47,125 +57,32 @@ module WEBrick
req.header.delete("proxy-authorization")
end
- # Some header fields shuold not be transfered.
- HopByHop = %w( connection keep-alive proxy-authenticate upgrade
- proxy-authorization te trailers transfer-encoding )
- ShouldNotTransfer = %w( set-cookie proxy-connection )
- 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
- ShouldNotTransfer.member?(key) # pragmatics
- @logger.debug("choose_header: `#{key}: #{value}'")
- next
- end
- dst[key] = value
- }
- end
-
- # Net::HTTP is stupid about the multiple header fields.
- # Here is workaround:
- def set_cookie(src, dst)
- if str = src['set-cookie']
- cookies = []
- str.split(/,\s*/).each{|token|
- if /^[^=]+;/o =~ token
- cookies[-1] << ", " << token
- elsif /=/o =~ token
- cookies << token
- else
- cookies[-1] << ", " << token
- end
- }
- dst.cookies.replace(cookies)
- end
- end
-
- def set_via(h)
- if @config[:ProxyVia]
- if h['via']
- h['via'] << ", " << @via
- else
- h['via'] = @via
- end
- end
- end
-
def proxy_uri(req, res)
- @config[:ProxyURI]
+ # should return upstream proxy server's URI
+ return @config[:ProxyURI]
end
def proxy_service(req, res)
# Proxy Authentication
proxy_auth(req, res)
- # Create Request-URI to send to the origin server
- uri = req.request_uri
- path = uri.path.dup
- path << "?" << uri.query if uri.query
-
- # Choose header fields to transfer
- header = Hash.new
- choose_header(req, header)
- set_via(header)
-
- # select upstream proxy server
- if proxy = proxy_uri(req, res)
- proxy_host = proxy.host
- proxy_port = proxy.port
- if proxy.userinfo
- credentials = "Basic " + [proxy.userinfo].pack("m").delete("\n")
- header['proxy-authorization'] = credentials
- end
- end
-
- response = nil
begin
- http = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port)
- http.start{
- if @config[:ProxyTimeout]
- ################################## these issues are
- http.open_timeout = 30 # secs # necessary (maybe bacause
- http.read_timeout = 60 # secs # Ruby's bug, but why?)
- ##################################
- end
- 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
- }
+ self.send("do_#{req.request_method}", req, res)
+ rescue NoMethodError
+ raise HTTPStatus::MethodNotAllowed,
+ "unsupported method `#{req.request_method}'."
rescue => err
logger.debug("#{err.class}: #{err.message}")
raise HTTPStatus::ServiceUnavailable, err.message
end
-
- # Persistent connction requirements are mysterious for me.
- # So I will close the connection in every response.
- res['proxy-connection'] = "close"
- res['connection'] = "close"
-
- # Convert Net::HTTP::HTTPResponse to WEBrick::HTTPProxy
- res.status = response.code.to_i
- choose_header(response, res)
- set_cookie(response, res)
- set_via(res)
- res.body = response.body
# Process contents
if handler = @config[:ProxyContentHandler]
handler.call(req, res)
end
end
-
- def proxy_connect(req, res)
+
+ def do_CONNECT(req, res)
# Proxy Authentication
proxy_auth(req, res)
@@ -245,8 +162,127 @@ module WEBrick
raise HTTPStatus::EOFError
end
+ def do_GET(req, res)
+ perform_proxy_request(req, res) do |http, path, header|
+ http.get(path, header)
+ end
+ end
+
+ def do_HEAD(req, res)
+ perform_proxy_request(req, res) do |http, path, header|
+ http.head(path, header)
+ end
+ end
+
+ def do_POST(req, res)
+ perform_proxy_request(req, res) do |http, path, header|
+ http.post(path, req.body || "", header)
+ end
+ end
+
def do_OPTIONS(req, res)
res['allow'] = "GET,HEAD,POST,OPTIONS,CONNECT"
end
+
+ private
+
+ # Some header fields shuold not be transfered.
+ HopByHop = %w( connection keep-alive proxy-authenticate upgrade
+ proxy-authorization te trailers transfer-encoding )
+ ShouldNotTransfer = %w( set-cookie proxy-connection )
+ 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
+ ShouldNotTransfer.member?(key) # pragmatics
+ @logger.debug("choose_header: `#{key}: #{value}'")
+ next
+ end
+ dst[key] = value
+ }
+ end
+
+ # Net::HTTP is stupid about the multiple header fields.
+ # Here is workaround:
+ def set_cookie(src, dst)
+ if str = src['set-cookie']
+ cookies = []
+ str.split(/,\s*/).each{|token|
+ if /^[^=]+;/o =~ token
+ cookies[-1] << ", " << token
+ elsif /=/o =~ token
+ cookies << token
+ else
+ cookies[-1] << ", " << token
+ end
+ }
+ dst.cookies.replace(cookies)
+ end
+ end
+
+ def set_via(h)
+ if @config[:ProxyVia]
+ if h['via']
+ h['via'] << ", " << @via
+ else
+ h['via'] = @via
+ end
+ end
+ end
+
+ def setup_proxy_header(req, res)
+ # Choose header fields to transfer
+ header = Hash.new
+ choose_header(req, header)
+ set_via(header)
+ return header
+ end
+
+ def setup_upstream_proxy_authentication(req, res, header)
+ if upstream = proxy_uri(req, res)
+ if upstream.userinfo
+ header['proxy-authorization'] =
+ "Basic " + [upstream.userinfo].pack("m").delete("\n")
+ end
+ return upstream
+ end
+ return FakeProxyURI
+ end
+
+ def perform_proxy_request(req, res)
+ uri = req.request_uri
+ path = uri.path.dup
+ path << "?" << uri.query if uri.query
+ header = setup_proxy_header(req, res)
+ upstream = setup_upstream_proxy_authentication(req, res, header)
+ response = nil
+
+ http = Net::HTTP.new(uri.host, uri.port, upstream.host, upstream.port)
+ http.start do
+ if @config[:ProxyTimeout]
+ ################################## these issues are
+ http.open_timeout = 30 # secs # necessary (maybe bacause
+ http.read_timeout = 60 # secs # Ruby's bug, but why?)
+ ##################################
+ end
+ response = yield(http, path, header)
+ end
+
+ # Persistent connction requirements are mysterious for me.
+ # So I will close the connection in every response.
+ res['proxy-connection'] = "close"
+ res['connection'] = "close"
+
+ # Convert Net::HTTP::HTTPResponse to WEBrick::HTTPResponse
+ res.status = response.code.to_i
+ choose_header(response, res)
+ set_cookie(response, res)
+ set_via(res)
+ res.body = response.body
+ end
end
end
diff --git a/test/webrick/test_httpproxy.rb b/test/webrick/test_httpproxy.rb
new file mode 100644
index 0000000000..67862543a8
--- /dev/null
+++ b/test/webrick/test_httpproxy.rb
@@ -0,0 +1,281 @@
+require "test/unit"
+require "net/http"
+require "webrick"
+require "webrick/httpproxy"
+begin
+ require "webrick/ssl"
+ require "net/https"
+ require File.expand_path("../openssl/utils.rb", File.dirname(__FILE__))
+rescue LoadError
+ # test_connect will be skipped
+end
+require File.expand_path("utils.rb", File.dirname(__FILE__))
+
+class TestWEBrickHTTPProxy < Test::Unit::TestCase
+ def test_fake_proxy
+ assert_nil(WEBrick::FakeProxyURI.scheme)
+ assert_nil(WEBrick::FakeProxyURI.host)
+ assert_nil(WEBrick::FakeProxyURI.port)
+ assert_nil(WEBrick::FakeProxyURI.path)
+ assert_nil(WEBrick::FakeProxyURI.userinfo)
+ assert_raise(NoMethodError){ WEBrick::FakeProxyURI.foo }
+ end
+
+ def test_proxy
+ # Testing GET or POST to the proxy server
+ # Note that the proxy server works as the origin server.
+ # +------+
+ # V |
+ # client -------> proxy ---+
+ # GET / POST GET / POST
+ #
+ proxy_handler_called = request_handler_called = 0
+ config = {
+ :ServerName => "localhost.localdomain",
+ :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1 },
+ :RequestHandler => Proc.new{|req, res| request_handler_called += 1 }
+ }
+ TestWEBrick.start_httpproxy(config){|server, addr, port|
+ server.mount_proc("/"){|req, res|
+ res.body = "#{req.request_method} #{req.path} #{req.body}"
+ }
+ http = Net::HTTP.new(addr, port, addr, port)
+
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res|
+ assert_equal("1.1 localhost.localdomain:#{port}", res["via"])
+ assert_equal("GET / ", res.body)
+ }
+ assert_equal(1, proxy_handler_called)
+ assert_equal(2, request_handler_called)
+
+ req = Net::HTTP::Head.new("/")
+ http.request(req){|res|
+ assert_equal("1.1 localhost.localdomain:#{port}", res["via"])
+ assert_nil(res.body)
+ }
+ assert_equal(2, proxy_handler_called)
+ assert_equal(4, request_handler_called)
+
+ req = Net::HTTP::Post.new("/")
+ req.body = "post-data"
+ http.request(req){|res|
+ assert_equal("1.1 localhost.localdomain:#{port}", res["via"])
+ assert_equal("POST / post-data", res.body)
+ }
+ assert_equal(3, proxy_handler_called)
+ assert_equal(6, request_handler_called)
+ }
+ end
+
+ def test_no_proxy
+ # Testing GET or POST to the proxy server without proxy request.
+ #
+ # client -------> proxy
+ # GET / POST
+ #
+ proxy_handler_called = request_handler_called = 0
+ config = {
+ :ServerName => "localhost.localdomain",
+ :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1 },
+ :RequestHandler => Proc.new{|req, res| request_handler_called += 1 }
+ }
+ TestWEBrick.start_httpproxy(config){|server, addr, port|
+ server.mount_proc("/"){|req, res|
+ res.body = "#{req.request_method} #{req.path} #{req.body}"
+ }
+ http = Net::HTTP.new(addr, port)
+
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res|
+ assert_nil(res["via"])
+ assert_equal("GET / ", res.body)
+ }
+ assert_equal(0, proxy_handler_called)
+ assert_equal(1, request_handler_called)
+
+ req = Net::HTTP::Head.new("/")
+ http.request(req){|res|
+ assert_nil(res["via"])
+ assert_nil(res.body)
+ }
+ assert_equal(0, proxy_handler_called)
+ assert_equal(2, request_handler_called)
+
+ req = Net::HTTP::Post.new("/")
+ req.body = "post-data"
+ http.request(req){|res|
+ assert_nil(res["via"])
+ assert_equal("POST / post-data", res.body)
+ }
+ assert_equal(0, proxy_handler_called)
+ assert_equal(3, request_handler_called)
+ }
+ end
+
+ def make_certificate(key, cn)
+ subject = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=#{cn}")
+ exts = [
+ ["keyUsage", "keyEncipherment,digitalSignature", true],
+ ]
+ cert = OpenSSL::TestUtils.issue_cert(
+ subject, key, 1, Time.now, Time.now + 3600, exts,
+ nil, nil, OpenSSL::Digest::SHA1.new
+ )
+ return cert
+ end
+
+ def test_connect
+ # Testing CONNECT to proxy server
+ #
+ # client -----------> proxy -----------> https
+ # 1. CONNECT establish TCP
+ # 2. ---- establish SSL session --->
+ # 3. ------- GET or POST ---------->
+ #
+ key = OpenSSL::TestUtils::TEST_KEY_RSA1024
+ cert = make_certificate(key, "127.0.0.1")
+ s_config = {
+ :SSLEnable =>true,
+ :ServerName => "localhost",
+ :SSLCertificate => cert,
+ :SSLPrivateKey => key,
+ }
+ config = {
+ :ServerName => "localhost.localdomain",
+ :RequestHandler => Proc.new{|req, res|
+ assert_equal("CONNECT", req.request_method)
+ },
+ }
+ TestWEBrick.start_httpserver(s_config){|s_server, s_addr, s_port|
+ s_server.mount_proc("/"){|req, res|
+ res.body = "SSL #{req.request_method} #{req.path} #{req.body}"
+ }
+ TestWEBrick.start_httpproxy(config){|server, addr, port|
+ http = Net::HTTP.new("127.0.0.1", s_port, addr, port)
+ http.use_ssl = true
+ http.verify_callback = Proc.new do |preverify_ok, store_ctx|
+ store_ctx.current_cert.to_der == cert.to_der
+ end
+
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res|
+ assert_equal("SSL GET / ", res.body)
+ }
+
+ req = Net::HTTP::Post.new("/")
+ req.body = "post-data"
+ http.request(req){|res|
+ assert_equal("SSL POST / post-data", res.body)
+ }
+ }
+ }
+ end if defined?(OpenSSL)
+
+ def test_upstream_proxy
+ # Testing GET or POST through the upstream proxy server
+ # Note that the upstream proxy server works as the origin server.
+ # +------+
+ # V |
+ # client -------> proxy -------> proxy ---+
+ # GET / POST GET / POST GET / POST
+ #
+ up_proxy_handler_called = up_request_handler_called = 0
+ proxy_handler_called = request_handler_called = 0
+ up_config = {
+ :ServerName => "localhost.localdomain",
+ :ProxyContentHandler => Proc.new{|req, res| up_proxy_handler_called += 1},
+ :RequestHandler => Proc.new{|req, res| up_request_handler_called += 1}
+ }
+ TestWEBrick.start_httpproxy(up_config){|up_server, up_addr, up_port|
+ up_server.mount_proc("/"){|req, res|
+ res.body = "#{req.request_method} #{req.path} #{req.body}"
+ }
+ config = {
+ :ServerName => "localhost.localdomain",
+ :ProxyURI => URI.parse("http://localhost:#{up_port}"),
+ :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1},
+ :RequestHandler => Proc.new{|req, res| request_handler_called += 1},
+ }
+ TestWEBrick.start_httpproxy(config){|server, addr, port|
+ http = Net::HTTP.new(up_addr, up_port, addr, port)
+
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res|
+ via = res["via"].split(/,\s+/)
+ assert(via.include?("1.1 localhost.localdomain:#{up_port}"))
+ assert(via.include?("1.1 localhost.localdomain:#{port}"))
+ assert_equal("GET / ", res.body)
+ }
+ assert_equal(1, up_proxy_handler_called)
+ assert_equal(2, up_request_handler_called)
+ assert_equal(1, proxy_handler_called)
+ assert_equal(1, request_handler_called)
+
+ req = Net::HTTP::Head.new("/")
+ http.request(req){|res|
+ via = res["via"].split(/,\s+/)
+ assert(via.include?("1.1 localhost.localdomain:#{up_port}"))
+ assert(via.include?("1.1 localhost.localdomain:#{port}"))
+ assert_nil(res.body)
+ }
+ assert_equal(2, up_proxy_handler_called)
+ assert_equal(4, up_request_handler_called)
+ assert_equal(2, proxy_handler_called)
+ assert_equal(2, request_handler_called)
+
+ req = Net::HTTP::Post.new("/")
+ req.body = "post-data"
+ http.request(req){|res|
+ via = res["via"].split(/,\s+/)
+ assert(via.include?("1.1 localhost.localdomain:#{up_port}"))
+ assert(via.include?("1.1 localhost.localdomain:#{port}"))
+ assert_equal("POST / post-data", res.body)
+ }
+ assert_equal(3, up_proxy_handler_called)
+ assert_equal(6, up_request_handler_called)
+ assert_equal(3, proxy_handler_called)
+ assert_equal(3, request_handler_called)
+
+ if defined?(OpenSSL)
+ # Testing CONNECT to the upstream proxy server
+ #
+ # client -------> proxy -------> proxy -------> https
+ # 1. CONNECT CONNECT establish TCP
+ # 2. -------- establish SSL session ------>
+ # 3. ---------- GET or POST -------------->
+ #
+ key = OpenSSL::TestUtils::TEST_KEY_RSA1024
+ cert = make_certificate(key, "127.0.0.1")
+ s_config = {
+ :SSLEnable =>true,
+ :ServerName => "localhost",
+ :SSLCertificate => cert,
+ :SSLPrivateKey => key,
+ }
+ TestWEBrick.start_httpserver(s_config){|s_server, s_addr, s_port|
+ s_server.mount_proc("/"){|req, res|
+ res.body = "SSL #{req.request_method} #{req.path} #{req.body}"
+ }
+ http = Net::HTTP.new("127.0.0.1", s_port, addr, port)
+ http.use_ssl = true
+ http.verify_callback = Proc.new do |preverify_ok, store_ctx|
+ store_ctx.current_cert.to_der == cert.to_der
+ end
+
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res|
+ assert_equal("SSL GET / ", res.body)
+ }
+
+ req = Net::HTTP::Post.new("/")
+ req.body = "post-data"
+ http.request(req){|res|
+ assert_equal("SSL POST / post-data", res.body)
+ }
+ }
+ end
+ }
+ }
+ end
+end
diff --git a/test/webrick/utils.rb b/test/webrick/utils.rb
index ba03156145..dace41a8f3 100644
--- a/test/webrick/utils.rb
+++ b/test/webrick/utils.rb
@@ -17,16 +17,20 @@ module TestWEBrick
def start_server(klass, config={}, &block)
server = klass.new({
:BindAddress => "127.0.0.1", :Port => 0,
+ :ShutdownSocketWithoutClose =>true,
+ :ServerType => Thread,
:Logger => WEBrick::Log.new(NullWriter),
:AccessLog => [[NullWriter, ""]]
}.update(config))
begin
- thread = Thread.start{ server.start }
+ server.start
addr = server.listeners[0].addr
block.yield([server, addr[3], addr[1]])
ensure
- server.stop
- thread.join
+ server.shutdown
+ until server.status == :Stop
+ sleep 0.1
+ end
end
end