3 This module acts as an HTTP Proxy Server. However it adds the
4 multitaskhttpd auto-generated session cookie to the headers.
6 It also changes the connection type to use keep-alive, so that
7 the connection stays open to the server, resulting in an
8 "apparent" persistent connection.
10 Thus, a service on the receiving end of this proxy server may reliably
11 keep persistent state, even though the browsers connecting to it
12 may be doing HTTP 1.0 or have an unreliable internet connection.
19 __all__
= ["SimpleHTTPRequestHandler"]
33 from cStringIO
import StringIO
35 from StringIO
import StringIO
39 class NotConnected(Exception):
42 class ProxyConnection
:
51 self
.host
= "127.0.0.1"
56 """Connect to the host and port specified in __init__."""
57 msg
= "getaddrinfo returns an empty list"
58 for res
in socket
.getaddrinfo(self
.host
, self
.port
, 0,
60 af
, socktype
, proto
, canonname
, sa
= res
62 self
.sock
= socket
.socket(af
, socktype
, proto
)
63 if self
.debuglevel
> 0:
64 print "connect: (%s, %s)" % (self
.host
, self
.port
)
66 except socket
.error
, msg
:
67 if self
.debuglevel
> 0:
68 print 'connect fail:', (self
.host
, self
.port
)
75 raise socket
.error
, msg
77 self
.ss
= httpd
.SockStream(self
.sock
)
80 """Close the connection to the HTTP server."""
82 self
.sock
.close() # close it manually... there may be other refs
86 """Send `str' to the server."""
93 # send the data to the server. if we get a broken pipe, then close
94 # the socket. we want to reconnect when somebody tries to send again.
96 # NOTE: we DO propagate the error, though, because we cannot simply
97 # ignore the error... the caller will know if they can retry.
98 if self
.debuglevel
> 0:
99 print "send:", repr(str)
101 yield multitask
.send(self
.sock
, str)
102 except socket
.error
, v
:
103 if v
[0] == 32: # Broken pipe
107 def read(self
, num_bytes
=1024):
108 data
= (yield multitask
.recv(self
.sock
, num_bytes
))
111 class ProxyServerRequestHandler(object):
113 """Simple HTTP request handler with GET and HEAD commands.
115 This serves files from the current directory and any of its
116 subdirectories. The MIME type for files is determined by
117 calling the .guess_type() method.
119 The GET and HEAD requests are identical except that the HEAD
120 request omits the actual contents of the file.
125 server_version
= "SimpleHTTP/" + __version__
127 def on_query(self
, client
, reqtype
, *args
):
128 """Serve a request."""
132 if self
.debuglevel
> 0:
133 print "on_query", reqtype
, repr(self
.hr
.headers
), \
135 session
= self
.client
.session
136 p
= self
.proxies
.get(session
, None)
138 proxy
= ProxyConnection()
140 self
.proxies
[session
] = proxy
142 yield self
.proxy_relay(reqtype
)
145 # (yield multitask.sleep(0.01))
149 def onPOST(self
, client
, *args
):
150 """Serve a POST request."""
151 yield self
.on_query(client
, "POST", *args
)
153 def onGET(self
, client
, *args
):
154 """Serve a GET request."""
155 yield self
.on_query(client
, "GET", *args
)
157 def proxy_relay(self
, reqtype
):
159 session
= self
.client
.session
160 p
= self
.proxies
[session
]
166 req
= "%s %s %s\n" % (reqtype
, self
.hr
.path
, "HTTP/1.1")
167 if self
.debuglevel
> 0:
169 yield p
.ss
.write(req
)
171 conntype
= self
.hr
.headers
.get('Connection', "")
172 keepalive
= conntype
.lower() == 'keep-alive'
174 self
.hr
.headers
['Connection'] = 'keep-alive'
175 self
.hr
.close_connection
= 0
178 hdrs
= str(self
.hr
.headers
)
179 if self
.debuglevel
> 0:
181 yield p
.ss
.write(hdrs
)
182 yield p
.ss
.write('\r\n')
185 if self
.hr
.headers
.has_key('content-length'):
186 max_chunk_size
= 10*1024*1024
187 size_remaining
= int(self
.hr
.headers
["content-length"])
189 if self
.debuglevel
> 0:
190 print "size_remaining", size_remaining
191 while size_remaining
:
192 chunk_size
= min(size_remaining
, max_chunk_size
)
193 data
= self
.hr
.rfile
.read(chunk_size
)
194 if self
.debuglevel
> 0:
195 print "proxy rfile read", repr(data
)
196 yield multitask
.send(p
.sock
, data
)
197 size_remaining
-= len(data
)
199 # now read response and write back
200 # HTTP/1.0 200 OK status line etc.
201 responseline
= (yield p
.ss
.readline())
202 yield self
.client
.writeMessage(responseline
)
207 line
= (yield p
.ss
.readline())
208 if self
.debuglevel
> 0:
209 print "reading from proxy", repr(line
)
211 if line
in ['\n', '\r\n']:
213 except StopIteration:
214 if self
.debuglevel
> 0:
215 print "proxy read stopiter"
216 # TODO: close connection
218 if self
.debuglevel
> 0:
219 print 'proxy read error', \
220 (traceback
and traceback
.print_exc() or None)
221 # TODO: close connection
225 # Examine the headers and look for a Connection directive
226 respheaders
= mimetools
.Message(f
, 0)
227 if self
.debuglevel
> 0:
228 print "response headers", str(respheaders
)
229 remote
= self
.client
.remote
230 rcooks
= httpd
.process_cookies(respheaders
, remote
, "Set-Cookie", False)
231 rcooks
['session'] = self
.hr
.response_cookies
['session'].value
# nooo
232 rcooks
['session']['expires'] = \
233 self
.hr
.response_cookies
['session']['expires']
234 self
.hr
.response_cookies
= rcooks
235 if self
.debuglevel
> 0:
236 print "rcooks", str(rcooks
)
238 # override connection: keep-alive hack
239 #responseline = responseline.split(" ")
240 #print "responseline:", responseline
241 #if responseline[1] != "200":
242 # respheaders['Connection'] = 'close'
244 # send all but Set-Cookie headers
245 del respheaders
['Set-Cookie'] # being replaced
246 yield self
.client
.writeMessage(str(respheaders
))
248 # now replacement cookies
249 for k
, v
in rcooks
.items():
251 yield self
.client
.writeMessage(val
+"\r\n")
253 # check connection for "closed" header
255 conntype
= respheaders
.get('Connection', "")
256 if conntype
.lower() == 'close':
257 self
.hr
.close_connection
= 1
258 elif (conntype
.lower() == 'keep-alive' and
259 self
.hr
.protocol_version
>= "HTTP/1.1"):
260 self
.hr
.close_connection
= 0
263 if self
.debuglevel
> 0:
264 print "writing to client body"
265 yield self
.client
.writeMessage("\r\n")
267 if respheaders
.has_key('content-length'):
268 max_chunk_size
= 10*1024*1024
269 size_remaining
= int(respheaders
["content-length"])
270 while size_remaining
:
271 chunk_size
= min(size_remaining
, max_chunk_size
)
272 data
= (yield p
.ss
.read(chunk_size
))
273 if self
.debuglevel
> 0:
274 print "reading from proxy expecting", \
275 size_remaining
, repr(data
)
276 yield self
.client
.writeMessage(data
)
277 size_remaining
-= len(data
)
282 data
= (yield p
.ss
.read(1024))
283 except httpd
.ConnectionClosed
:
285 if self
.debuglevel
> 0:
286 print "reading from proxy", repr(data
)
289 yield self
.client
.writeMessage(data
)
291 if not keepalive
: #self.hr.close_connection:
292 if self
.debuglevel
> 0:
293 print 'proxy wants client to close_connection'
295 yield self
.client
.connectionClosed()
297 raise httpd
.ConnectionClosed
298 except httpd
.ConnectionClosed
:
299 if self
.debuglevel
> 0:
300 print 'close_connection done'
304 except httpd
.ConnectionClosed
:
305 # whoops, remote end has died: remove client and
306 # remove proxy session, we cannot do anything else,
307 # there's nothing there to talk to.
308 self
.client
.removeConnection()
309 self
.proxies
.pop(session
)
312 if self
.debuglevel
> 0:
313 print traceback
.print_exc()