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 multitask
.add(self
.proxy_relay(reqtype
))
146 def onPOST(self
, client
, *args
):
147 """Serve a POST request."""
148 return self
.on_query(client
, "POST", *args
)
150 def onGET(self
, client
, *args
):
151 """Serve a GET request."""
152 return self
.on_query(client
, "GET", *args
)
154 def proxy_relay(self
, reqtype
):
156 session
= self
.client
.session
157 p
= self
.proxies
[session
]
160 # (yield multitask.sleep(0.01))
165 req
= "%s %s %s\n" % (reqtype
, self
.hr
.path
, "HTTP/1.1")
166 if self
.debuglevel
> 0:
168 yield p
.ss
.write(req
)
170 conntype
= self
.hr
.headers
.get('Connection', "")
171 keepalive
= conntype
.lower() == 'keep-alive'
173 self
.hr
.headers
['Connection'] = 'keep-alive'
174 self
.hr
.close_connection
= 0
177 hdrs
= str(self
.hr
.headers
)
178 if self
.debuglevel
> 0:
180 yield p
.ss
.write(hdrs
)
181 yield p
.ss
.write('\r\n')
184 if self
.hr
.headers
.has_key('content-length'):
185 max_chunk_size
= 10*1024*1024
186 size_remaining
= int(self
.hr
.headers
["content-length"])
188 if self
.debuglevel
> 0:
189 print "size_remaining", size_remaining
190 while size_remaining
:
191 chunk_size
= min(size_remaining
, max_chunk_size
)
192 data
= self
.hr
.rfile
.read(chunk_size
)
193 if self
.debuglevel
> 0:
194 print "proxy rfile read", repr(data
)
195 yield multitask
.send(p
.sock
, data
)
196 size_remaining
-= len(data
)
198 # now read response and write back
199 # HTTP/1.0 200 OK status line etc.
200 responseline
= (yield p
.ss
.readline())
201 yield self
.client
.writeMessage(responseline
)
206 line
= (yield p
.ss
.readline())
207 if self
.debuglevel
> 0:
208 print "reading from proxy", repr(line
)
210 if line
in ['\n', '\r\n']:
212 except StopIteration:
213 if self
.debuglevel
> 0:
214 print "proxy read stopiter"
215 # TODO: close connection
217 if self
.debuglevel
> 0:
218 print 'proxy read error', \
219 (traceback
and traceback
.print_exc() or None)
220 # TODO: close connection
224 # Examine the headers and look for a Connection directive
225 respheaders
= mimetools
.Message(f
, 0)
226 if self
.debuglevel
> 0:
227 print "response headers", str(respheaders
)
228 remote
= self
.client
.remote
229 rcooks
= httpd
.process_cookies(respheaders
, remote
, "Set-Cookie", False)
230 rcooks
['session'] = self
.hr
.response_cookies
['session'].value
# nooo
231 rcooks
['session']['expires'] = \
232 self
.hr
.response_cookies
['session']['expires']
233 self
.hr
.response_cookies
= rcooks
234 if self
.debuglevel
> 0:
235 print "rcooks", str(rcooks
)
237 # override connection: keep-alive hack
238 #responseline = responseline.split(" ")
239 #print "responseline:", responseline
240 #if responseline[1] != "200":
241 # respheaders['Connection'] = 'close'
243 # send all but Set-Cookie headers
244 del respheaders
['Set-Cookie'] # being replaced
245 yield self
.client
.writeMessage(str(respheaders
))
247 # now replacement cookies
248 for k
, v
in rcooks
.items():
250 yield self
.client
.writeMessage(val
+"\r\n")
252 # check connection for "closed" header
254 conntype
= respheaders
.get('Connection', "")
255 if conntype
.lower() == 'close':
256 self
.hr
.close_connection
= 1
257 elif (conntype
.lower() == 'keep-alive' and
258 self
.hr
.protocol_version
>= "HTTP/1.1"):
259 self
.hr
.close_connection
= 0
262 if self
.debuglevel
> 0:
263 print "writing to client body"
264 yield self
.client
.writeMessage("\r\n")
266 if respheaders
.has_key('content-length'):
267 max_chunk_size
= 10*1024*1024
268 size_remaining
= int(respheaders
["content-length"])
269 while size_remaining
:
270 chunk_size
= min(size_remaining
, max_chunk_size
)
271 data
= (yield p
.ss
.read(chunk_size
))
272 if self
.debuglevel
> 0:
273 print "reading from proxy expecting", \
274 size_remaining
, repr(data
)
275 yield self
.client
.writeMessage(data
)
276 size_remaining
-= len(data
)
281 data
= (yield p
.ss
.read(1024))
282 except httpd
.ConnectionClosed
:
284 if self
.debuglevel
> 0:
285 print "reading from proxy", repr(data
)
288 yield self
.client
.writeMessage(data
)
290 if not keepalive
: #self.hr.close_connection:
291 if self
.debuglevel
> 0:
292 print 'proxy wants client to close_connection'
294 yield self
.client
.connectionClosed()
295 raise httpd
.ConnectionClosed
296 except httpd
.ConnectionClosed
:
297 if self
.debuglevel
> 0:
298 print 'close_connection done'
302 except httpd
.ConnectionClosed
:
303 # whoops, remote end has died: remove client and
304 # remove proxy session, we cannot do anything else,
305 # there's nothing there to talk to.
306 self
.client
.removeConnection()
307 self
.proxies
.pop(session
)
310 if self
.debuglevel
> 0:
311 print traceback
.print_exc()