[前][次][番号順一覧][スレッド一覧]

ruby-changes:3323

From: ko1@a...
Date: 31 Dec 2007 23:18:29 +0900
Subject: [ruby-changes:3323] gotoyuzo - Ruby:r14816 (trunk): * lib/webrick/httpproxy.rb (WEBrick::HTTPProxyServer#proxy_service):

gotoyuzo	2007-12-31 23:17:41 +0900 (Mon, 31 Dec 2007)

  New Revision: 14816

  Added files:
    trunk/test/webrick/test_httpproxy.rb
  Modified files:
    trunk/ChangeLog
    trunk/lib/webrick/httpproxy.rb
    trunk/test/webrick/utils.rb

  Log:
    * 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.


  http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/trunk/test/webrick/test_httpproxy.rb?revision=14816&view=markup
  http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/trunk/lib/webrick/httpproxy.rb?r1=14816&r2=14815
  http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/trunk/ChangeLog?r1=14816&r2=14815
  http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/trunk/test/webrick/utils.rb?r1=14816&r2=14815

Index: ChangeLog
===================================================================
--- ChangeLog	(revision 14815)
+++ ChangeLog	(revision 14816)
@@ -1,3 +1,11 @@
+Mon Dec 31 23:17:22 2007  GOTOU Yuuzou  <gotoyuzo@n...>
+
+	* 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@r...>
 
 	* thread_pthread.c (native_sleep): timespec tv_sec may overflow on
Index: lib/webrick/httpproxy.rb
===================================================================
--- lib/webrick/httpproxy.rb	(revision 14815)
+++ lib/webrick/httpproxy.rb	(revision 14816)
@@ -23,6 +23,16 @@
     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 @@
 
     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 @@
       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 @@
       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
Index: test/webrick/test_httpproxy.rb
===================================================================
--- test/webrick/test_httpproxy.rb	(revision 0)
+++ test/webrick/test_httpproxy.rb	(revision 14816)
@@ -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
Index: test/webrick/utils.rb
===================================================================
--- test/webrick/utils.rb	(revision 14815)
+++ test/webrick/utils.rb	(revision 14816)
@@ -17,16 +17,20 @@
   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
 

--
ML: ruby-changes@q...
Info: http://www.atdot.net/~ko1/quickml

[前][次][番号順一覧][スレッド一覧]