File: webrick/httpproxy.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: WEBrick#17
has properties
constant: NullReader #18
  class: HTTPProxyServer#26
inherits from
  HTTPServer ( WEBrick )
has properties
method: initialize / 1 #27
method: service / 2 #33
method: proxy_auth / 2 #43
constant: HopByHop #51
constant: ShouldNotTransfer #53
method: split_field / 1 #54
method: choose_header / 2 #56
method: set_cookie / 2 #72
method: set_via / 1 #88
method: proxy_uri / 2 #98
method: proxy_service / 2 #102
method: proxy_connect / 2 #169
method: do_OPTIONS / 2 #250

Class Hierarchy

Code

   1  #
   2  # httpproxy.rb -- HTTPProxy Class
   3  #
   4  # Author: IPR -- Internet Programming with Ruby -- writers
   5  # Copyright (c) 2002 GOTO Kentaro
   6  # Copyright (c) 2002 Internet Programming with Ruby writers. All rights
   7  # reserved.
   8  #
   9  # $IPR: httpproxy.rb,v 1.18 2003/03/08 18:58:10 gotoyuzo Exp $
  10  # $kNotwork: straw.rb,v 1.3 2002/02/12 15:13:07 gotoken Exp $
  11 
  12  require "webrick/httpserver"
  13  require "net/http"
  14 
  15  Net::HTTP::version_1_2 if RUBY_VERSION < "1.7"
  16 
  17  module WEBrick
  18    NullReader = Object.new
  19    class << NullReader
  20      def read(*args)
  21        nil
  22      end
  23      alias gets read
  24    end
  25 
  26    class HTTPProxyServer < HTTPServer
  27      def initialize(config)
  28        super
  29        c = @config
  30        @via = "#{c[:HTTPVersion]} #{c[:ServerName]}:#{c[:Port]}"
  31      end
  32 
  33      def service(req, res)
  34        if req.request_method == "CONNECT"
  35          proxy_connect(req, res)
  36        elsif req.unparsed_uri =~ %r!^http://!
  37          proxy_service(req, res)
  38        else
  39          super(req, res)
  40        end
  41      end
  42 
  43      def proxy_auth(req, res)
  44        if proc = @config[:ProxyAuthProc]
  45          proc.call(req, res)
  46        end
  47        req.header.delete("proxy-authorization")
  48      end
  49 
  50      # Some header fields should not be transferred.
  51      HopByHop = %w( connection keep-alive proxy-authenticate upgrade
  52                     proxy-authorization te trailers transfer-encoding )
  53      ShouldNotTransfer = %w( set-cookie proxy-connection )
  54      def split_field(f) f ? f.split(/,\s+/).collect{|i| i.downcase } : [] end
  55 
  56      def choose_header(src, dst)
  57        connections = split_field(src['connection'])
  58        src.each{|key, value|
  59          key = key.downcase
  60          if HopByHop.member?(key)          || # RFC2616: 13.5.1
  61             connections.member?(key)       || # RFC2616: 14.10
  62             ShouldNotTransfer.member?(key)    # pragmatics
  63            @logger.debug("choose_header: `#{key}: #{value}'")
  64            next
  65          end
  66          dst[key] = value
  67        }
  68      end
  69 
  70      # Net::HTTP is stupid about the multiple header fields.
  71      # Here is workaround:
  72      def set_cookie(src, dst)
  73        if str = src['set-cookie']
  74          cookies = []
  75          str.split(/,\s*/).each{|token|
  76            if /^[^=]+;/o =~ token
  77              cookies[-1] << ", " << token
  78            elsif /=/o =~ token
  79              cookies << token
  80            else
  81              cookies[-1] << ", " << token
  82            end
  83          }
  84          dst.cookies.replace(cookies)
  85        end
  86      end
  87 
  88      def set_via(h)
  89        if @config[:ProxyVia]
  90          if  h['via']
  91            h['via'] << ", " << @via
  92          else
  93            h['via'] = @via
  94          end
  95        end
  96      end
  97 
  98      def proxy_uri(req, res)
  99        @config[:ProxyURI]
 100      end
 101 
 102      def proxy_service(req, res)
 103        # Proxy Authentication
 104        proxy_auth(req, res)      
 105 
 106        # Create Request-URI to send to the origin server
 107        uri  = req.request_uri
 108        path = uri.path.dup
 109        path << "?" << uri.query if uri.query
 110 
 111        # Choose header fields to transfer
 112        header = Hash.new
 113        choose_header(req, header)
 114        set_via(header)
 115 
 116        # select upstream proxy server
 117        if proxy = proxy_uri(req, res)
 118          proxy_host = proxy.host
 119          proxy_port = proxy.port
 120          if proxy.userinfo
 121            credentials = "Basic " + [proxy.userinfo].pack("m*")
 122            credentials.chomp!
 123            header['proxy-authorization'] = credentials
 124          end
 125        end
 126 
 127        response = nil
 128        begin
 129          http = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port)
 130          http.start{
 131            if @config[:ProxyTimeout]
 132              ##################################   these issues are 
 133              http.open_timeout = 30   # secs  #   necessary (maybe bacause
 134              http.read_timeout = 60   # secs  #   Ruby's bug, but why?)
 135              ##################################
 136            end
 137            case req.request_method
 138            when "GET"  then response = http.get(path, header)
 139            when "POST" then response = http.post(path, req.body || "", header)
 140            when "HEAD" then response = http.head(path, header)
 141            else
 142              raise HTTPStatus::MethodNotAllowed,
 143                "unsupported method `#{req.request_method}'."
 144            end
 145          }
 146        rescue => err
 147          logger.debug("#{err.class}: #{err.message}")
 148          raise HTTPStatus::ServiceUnavailable, err.message
 149        end
 150    
 151        # Persistent connction requirements are mysterious for me.
 152        # So I will close the connection in every response.
 153        res['proxy-connection'] = "close"
 154        res['connection'] = "close"
 155 
 156        # Convert Net::HTTP::HTTPResponse to WEBrick::HTTPProxy
 157        res.status = response.code.to_i
 158        choose_header(response, res)
 159        set_cookie(response, res)
 160        set_via(res)
 161        res.body = response.body
 162 
 163        # Process contents
 164        if handler = @config[:ProxyContentHandler]
 165          handler.call(req, res)
 166        end
 167      end
 168 
 169      def proxy_connect(req, res)
 170        # Proxy Authentication
 171        proxy_auth(req, res)
 172 
 173        ua = Thread.current[:WEBrickSocket]  # User-Agent
 174        raise HTTPStatus::InternalServerError,
 175          "[BUG] cannot get socket" unless ua
 176 
 177        host, port = req.unparsed_uri.split(":", 2)
 178        # Proxy authentication for upstream proxy server
 179        if proxy = proxy_uri(req, res)
 180          proxy_request_line = "CONNECT #{host}:#{port} HTTP/1.0"
 181          if proxy.userinfo
 182            credentials = "Basic " + [proxy.userinfo].pack("m*")
 183            credentials.chomp!
 184          end
 185          host, port = proxy.host, proxy.port
 186        end
 187 
 188        begin
 189          @logger.debug("CONNECT: upstream proxy is `#{host}:#{port}'.")
 190          os = TCPSocket.new(host, port)     # origin server
 191 
 192          if proxy
 193            @logger.debug("CONNECT: sending a Request-Line")
 194            os << proxy_request_line << CRLF
 195            @logger.debug("CONNECT: > #{proxy_request_line}")
 196            if credentials
 197              @logger.debug("CONNECT: sending a credentials")
 198              os << "Proxy-Authorization: " << credentials << CRLF
 199            end
 200            os << CRLF
 201            proxy_status_line = os.gets(LF)
 202            @logger.debug("CONNECT: read a Status-Line form the upstream server")
 203            @logger.debug("CONNECT: < #{proxy_status_line}")
 204            if %r{^HTTP/\d+\.\d+\s+200\s*} =~ proxy_status_line
 205              while line = os.gets(LF)
 206                break if /\A(#{CRLF}|#{LF})\z/om =~ line
 207              end
 208            else
 209              raise HTTPStatus::BadGateway
 210            end
 211          end
 212          @logger.debug("CONNECT #{host}:#{port}: succeeded")
 213          res.status = HTTPStatus::RC_OK
 214        rescue => ex
 215          @logger.debug("CONNECT #{host}:#{port}: failed `#{ex.message}'")
 216          res.set_error(ex)
 217          raise HTTPStatus::EOFError
 218        ensure
 219          if handler = @config[:ProxyContentHandler]
 220            handler.call(req, res)
 221          end
 222          res.send_response(ua)
 223          access_log(@config, req, res)
 224 
 225          # Should clear request-line not to send the sesponse twice.
 226          # see: HTTPServer#run
 227          req.parse(NullReader) rescue nil
 228        end
 229 
 230        begin
 231          while fds = IO::select([ua, os])
 232            if fds[0].member?(ua)
 233              buf = ua.sysread(1024);
 234              @logger.debug("CONNECT: #{buf.size} byte from User-Agent")
 235              os.syswrite(buf)
 236            elsif fds[0].member?(os)
 237              buf = os.sysread(1024);
 238              @logger.debug("CONNECT: #{buf.size} byte from #{host}:#{port}")
 239              ua.syswrite(buf)
 240            end
 241          end
 242        rescue => ex
 243          os.close
 244          @logger.debug("CONNECT #{host}:#{port}: closed")
 245        end
 246 
 247        raise HTTPStatus::EOFError
 248      end
 249 
 250      def do_OPTIONS(req, res)
 251        res['allow'] = "GET,HEAD,POST,OPTIONS,CONNECT"
 252      end
 253    end
 254  end