Class: WEBrick::HTTPProxyServer

Inherits:
HTTPServer show all
Defined in:
lib/webrick/httpproxy.rb

Constant Summary collapse

HopByHop =

Some header fields shuold not be transfered.

%w( connection keep-alive proxy-authenticate upgrade
proxy-authorization te trailers transfer-encoding )
ShouldNotTransfer =
%w( set-cookie proxy-connection )

Instance Attribute Summary

Attributes inherited from GenericServer

#config, #listeners, #logger, #status, #tokens

Instance Method Summary collapse

Methods inherited from HTTPServer

#access_log, #lookup_server, #mount, #mount_proc, #run, #search_servlet, #unmount, #virtual_host

Methods inherited from GenericServer

#[], #listen, #run, #setup_ssl_context, #shutdown, #ssl_context, #start, #stop

Constructor Details

#initialize(config) ⇒ HTTPProxyServer

Returns a new instance of HTTPProxyServer.



27
28
29
30
31
# File 'lib/webrick/httpproxy.rb', line 27

def initialize(config)
  super
  c = @config
  @via = "#{c[:HTTPVersion]} #{c[:ServerName]}:#{c[:Port]}"
end

Instance Method Details

#choose_header(src, dst) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/webrick/httpproxy.rb', line 56

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

#do_OPTIONS(req, res) ⇒ Object



250
251
252
# File 'lib/webrick/httpproxy.rb', line 250

def do_OPTIONS(req, res)
  res['allow'] = "GET,HEAD,POST,OPTIONS,CONNECT"
end

#proxy_auth(req, res) ⇒ Object



43
44
45
46
47
48
# File 'lib/webrick/httpproxy.rb', line 43

def proxy_auth(req, res)
  if proc = @config[:ProxyAuthProc]
    proc.call(req, res)
  end
  req.header.delete("proxy-authorization")
end

#proxy_connect(req, res) ⇒ Object

Raises:



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/webrick/httpproxy.rb', line 169

def proxy_connect(req, res)
  # Proxy Authentication
  proxy_auth(req, res)

  ua = Thread.current[:WEBrickSocket]  # User-Agent
  raise HTTPStatus::InternalServerError,
    "[BUG] cannot get socket" unless ua

  host, port = req.unparsed_uri.split(":", 2)
  # Proxy authentication for upstream proxy server
  if proxy = proxy_uri(req, res)
    proxy_request_line = "CONNECT #{host}:#{port} HTTP/1.0"
    if proxy.userinfo
      credentials = "Basic " + [proxy.userinfo].pack("m*")
      credentials.chomp!
    end
    host, port = proxy.host, proxy.port
  end

  begin
    @logger.debug("CONNECT: upstream proxy is `#{host}:#{port}'.")
    os = TCPSocket.new(host, port)     # origin server

    if proxy
      @logger.debug("CONNECT: sending a Request-Line")
      os << proxy_request_line << CRLF
      @logger.debug("CONNECT: > #{proxy_request_line}")
      if credentials
        @logger.debug("CONNECT: sending a credentials")
        os << "Proxy-Authorization: " << credentials << CRLF
      end
      os << CRLF
      proxy_status_line = os.gets(LF)
      @logger.debug("CONNECT: read a Status-Line form the upstream server")
      @logger.debug("CONNECT: < #{proxy_status_line}")
      if %r{^HTTP/\d+\.\d+\s+200\s*} =~ proxy_status_line
        while line = os.gets(LF)
          break if /\A(#{CRLF}|#{LF})\z/om =~ line
        end
      else
        raise HTTPStatus::BadGateway
      end
    end
    @logger.debug("CONNECT #{host}:#{port}: succeeded")
    res.status = HTTPStatus::RC_OK
  rescue => ex
    @logger.debug("CONNECT #{host}:#{port}: failed `#{ex.message}'")
    res.set_error(ex)
    raise HTTPStatus::EOFError
  ensure
    if handler = @config[:ProxyContentHandler]
      handler.call(req, res)
    end
    res.send_response(ua)
    access_log(@config, req, res)

    # Should clear request-line not to send the sesponse twice.
    # see: HTTPServer#run
    req.parse(NullReader) rescue nil
  end

  begin
    while fds = IO::select([ua, os])
      if fds[0].member?(ua)
        buf = ua.sysread(1024);
        @logger.debug("CONNECT: #{buf.size} byte from User-Agent")
        os.syswrite(buf)
      elsif fds[0].member?(os)
        buf = os.sysread(1024);
        @logger.debug("CONNECT: #{buf.size} byte from #{host}:#{port}")
        ua.syswrite(buf)
      end
    end
  rescue => ex
    os.close
    @logger.debug("CONNECT #{host}:#{port}: closed")
  end

  raise HTTPStatus::EOFError
end

#proxy_service(req, res) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/webrick/httpproxy.rb', line 102

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*")
      credentials.chomp!
      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
    }
  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

#proxy_uri(req, res) ⇒ Object



98
99
100
# File 'lib/webrick/httpproxy.rb', line 98

def proxy_uri(req, res)
  @config[:ProxyURI]
end

#service(req, res) ⇒ Object



33
34
35
36
37
38
39
40
41
# File 'lib/webrick/httpproxy.rb', line 33

def service(req, res)
  if req.request_method == "CONNECT"
    proxy_connect(req, res)
  elsif req.unparsed_uri =~ %r!^http://!
    proxy_service(req, res)
  else
    super(req, res)
  end
end

Net::HTTP is stupid about the multiple header fields. Here is workaround:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/webrick/httpproxy.rb', line 72

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

#set_via(h) ⇒ Object



88
89
90
91
92
93
94
95
96
# File 'lib/webrick/httpproxy.rb', line 88

def set_via(h)
  if @config[:ProxyVia]
    if  h['via']
      h['via'] << ", " << @via
    else
      h['via'] = @via
    end
  end
end

#split_field(f) ⇒ Object



54
# File 'lib/webrick/httpproxy.rb', line 54

def split_field(f) f ? f.split(/,\s+/).collect{|i| i.downcase } : [] end