From 4efe39b87d73f8c9026f5324f2da227d12b5dbc8 Mon Sep 17 00:00:00 2001 From: lkcl Date: Wed, 14 Jul 2010 01:03:26 +0100 Subject: [PATCH] add HTTP Proxy Server (to 127.0.0.1 port 60001) --- ProxyServer.py | 224 +++++++++++++++++++++++++++++++++++++++++++++++++ httpd.py | 92 ++++++++++---------- 2 files changed, 273 insertions(+), 43 deletions(-) create mode 100644 ProxyServer.py diff --git a/ProxyServer.py b/ProxyServer.py new file mode 100644 index 0000000..4d3e20f --- /dev/null +++ b/ProxyServer.py @@ -0,0 +1,224 @@ +""" HTTP Proxy Server + +This module acts as an HTTP Proxy Server. However it adds the +multitaskhttpd auto-generated session cookie to the headers. + +It also changes the connection type to use keep-alive, so that +the connection stays open to the server, resulting in an +"apparent" persistent connection. + +Thus, a service on the receiving end of this proxy server may reliably +keep persistent state, even though the browsers connecting to it +may be doing HTTP 1.0 or have an unreliable internet connection. + +""" + + +__version__ = "0.6" + +__all__ = ["SimpleHTTPRequestHandler"] + +import os +import posixpath +import BaseHTTPServer +import urllib +import urlparse +import traceback +import cgi +import shutil +import mimetools +import multitask +import socket +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +import httpd + +class NotConnected(Exception): + pass + +class ProxyConnection: + + auto_open = 1 + debuglevel = 0 + strict = 0 + + def __init__(self): + self.sock = None + self.host = "127.0.0.1" + self.port = 60001 + + + def connect(self): + """Connect to the host and port specified in __init__.""" + msg = "getaddrinfo returns an empty list" + for res in socket.getaddrinfo(self.host, self.port, 0, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + try: + self.sock = socket.socket(af, socktype, proto) + if self.debuglevel > 0: + print "connect: (%s, %s)" % (self.host, self.port) + self.sock.connect(sa) + except socket.error, msg: + if self.debuglevel > 0: + print 'connect fail:', (self.host, self.port) + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + + def close(self): + """Close the connection to the HTTP server.""" + if self.sock: + self.sock.close() # close it manually... there may be other refs + self.sock = None + + def send(self, str): + """Send `str' to the server.""" + if self.sock is None: + if self.auto_open: + self.connect() + else: + raise NotConnected() + + # send the data to the server. if we get a broken pipe, then close + # the socket. we want to reconnect when somebody tries to send again. + # + # NOTE: we DO propagate the error, though, because we cannot simply + # ignore the error... the caller will know if they can retry. + if self.debuglevel > 0: + print "send:", repr(str) + try: + yield multitask.send(self.sock, str) + except socket.error, v: + if v[0] == 32: # Broken pipe + self.close() + raise + + def read(self, num_bytes=1024): + data = (yield multitask.recv(self.sock, num_bytes)) + + +class ProxyServerRequestHandler(object): + + """Simple HTTP request handler with GET and HEAD commands. + + This serves files from the current directory and any of its + subdirectories. The MIME type for files is determined by + calling the .guess_type() method. + + The GET and HEAD requests are identical except that the HEAD + request omits the actual contents of the file. + + """ + + server_version = "SimpleHTTP/" + __version__ + + def on_query(self, client, reqtype, *args): + """Serve a request.""" + self.client = client + self.hr = args[0] + if not hasattr(self.client, "proxy"): + self.client.proxy = ProxyConnection() + self.client.proxy.connect() + + multitask.add(self.proxy_relay(reqtype)) + + return True + + def onPOST(self, client, *args): + """Serve a POST request.""" + return self.on_query(client, "POST", *args) + + def onGET(self, client, *args): + """Serve a GET request.""" + return self.on_query(client, "GET", *args) + + def proxy_relay(self, reqtype): + + p = self.client.proxy + + # send command + req = "%s %s %s\n" % (reqtype, self.hr.path, self.hr.request_version) + print "req", req + yield multitask.send(p.sock, req) + + # send headers + hdrs = str(self.hr.headers) + print "hdrs", hdrs + yield multitask.send(p.sock, hdrs) + yield multitask.send(p.sock, "\n") + + # now content + if self.hr.headers.has_key('content-length'): + max_chunk_size = 10*1024*1024 + size_remaining = int(self.hr.headers["content-length"]) + L = [] + print "size_remaining", size_remaining + while size_remaining: + chunk_size = min(size_remaining, max_chunk_size) + data = self.hr.rfile.read(chunk_size) + yield multitask.send(p.sock, data) + size_remaining -= len(L[-1]) + + # now read response and write back + res = '' + while True: + #data = p.read() + data = (yield multitask.recv(p.sock, 1024)) + print "reading from proxy", repr(data) + if data == '': + break + res += data + + f = StringIO(res) + requestline = f.readline() + yield self.client.writeMessage(requestline) + + # Examine the headers and look for a Connection directive + respheaders = mimetools.Message(f, 0) + print "response headers", str(respheaders) + remote = self.client.remote + rcooks = httpd.process_cookies(respheaders, remote, "Set-Cookie", False) + rcooks['session'] = self.hr.response_cookies['session'].value # nooo + rcooks['session']['expires'] = \ + self.hr.response_cookies['session']['expires'] + self.hr.response_cookies = rcooks + print "rcooks", str(rcooks) + + # send all but Set-Cookie headers + del respheaders['Set-Cookie'] # being replaced + yield self.client.writeMessage(str(respheaders)) + + # now replacement cookies + for k, v in rcooks.items(): + val = v.output() + yield self.client.writeMessage(val+"\r\n") + + # check connection for "closed" header + conntype = respheaders.get('Connection', "") + if conntype.lower() == 'close': + self.hr.close_connection = 1 + elif (conntype.lower() == 'keep-alive' and + self.hr.protocol_version >= "HTTP/1.1"): + self.hr.close_connection = 0 + + # write rest of data + print "writing to client body" + yield self.client.writeMessage("\r\n") + yield self.client.writeMessage(f.read()) + if self.hr.close_connection: + try: + yield self.client.connectionClosed() + except httpd.ConnectionClosed: + print 'close_connection done' + pass + + raise StopIteration + diff --git a/httpd.py b/httpd.py index 7b5e2a6..55c1090 100644 --- a/httpd.py +++ b/httpd.py @@ -101,6 +101,39 @@ class MultitaskHTTPRequestHandler(BaseHTTPRequestHandler): val = v.OutputString() self.send_header("Set-Cookie", val) +def process_cookies(headers, remote, cookie_key="Cookie", add_sess=True): + ch = headers.getheaders(cookie_key) + print "messageReceived cookieheaders=", '; '.join(ch) + res = [] + for c in ch: + c = c.split(";") + if len(c) == 0: + continue + c = map(strip, c) + c = filter(lambda x: x, c) + res += c + has_sess = False + response_cookies = SimpleCookie() + for c in res: + print "found cookie", repr(c) + name, value = c.split("=") + response_cookies[name] = value + #response_cookies[name]['path'] = "/" + #response_cookies[name]['domain'] = remote[0] + #response_cookies[name]['version'] = 0 + if name == "session": + response_cookies[name]['expires'] = 50000 + has_sess = True + if not add_sess: + return response_cookies + if not has_sess: + response_cookies['session'] = uuid.uuid4().hex + response_cookies['session']['expires'] = 50000 + #response_cookies['session']['path'] = '/' + #response_cookies['session']['domain'] = remote[0] + #response_cookies['session']['version'] = 0 + return response_cookies + class ConnectionClosed: 'raised when the client closed the connection' @@ -467,35 +500,7 @@ class Client(Protocol): def messageReceived(self, msg): if _debug: print 'messageReceived cmd=', msg.command, msg.path - ch = msg.headers.getheaders("Cookie") - ch += msg.headers.getheaders("cookie") - print "messageReceived cookieheaders=", '; '.join(ch) - res = [] - for c in ch: - c = c.split(";") - if len(c) == 0: - continue - c = map(strip, c) - c = filter(lambda x: x, c) - res += c - has_sess = False - msg.response_cookies = SimpleCookie() - for c in res: - print "found cookie", repr(c) - name, value = c.split("=") - msg.response_cookies[name] = value - #msg.response_cookies[name]['path'] = "/" - #msg.response_cookies[name]['domain'] = self.remote[0] - #msg.response_cookies[name]['expires'] = 'None' - #msg.response_cookies[name]['version'] = 0 - if name == "session": - has_sess = True - if not has_sess: - msg.response_cookies['session'] = uuid.uuid4().hex - #msg.response_cookies['session']['expires'] = 'None' - #msg.response_cookies['session']['path'] = '/' - #msg.response_cookies['session']['domain'] = self.remote[0] - #msg.response_cookies['session']['version'] = 0 + msg.response_cookies = process_cookies(msg.headers, self.remote) if msg.headers.has_key('content-length'): max_chunk_size = 10*1024*1024 @@ -672,22 +677,23 @@ class HTTPServer(object): if result is True or result is None: if session not in self.clients: self.clients[session] = [inst]; inst._clients=self.clients[session] - self.clients[session].append(client) - msg.wfile.seek(0) - data = msg.wfile.read() - msg.wfile.seek(0) - msg.wfile.truncate() - yield client.writeMessage(data) - if close_connection: - if _debug: - print 'close_connection requested' - try: - yield client.connectionClosed() - except ClientClosed: + if result is None: + msg.wfile.seek(0) + data = msg.wfile.read() + msg.wfile.seek(0) + msg.wfile.truncate() + yield client.writeMessage(data) + if close_connection: if _debug: - print 'close_connection done' - pass + print 'close_connection requested' + try: + yield client.connectionClosed() + except ConnectionClosed: + if _debug: + print 'close_connection done' + pass else: + print "result", result yield client.rejectConnection(reason='Rejected in onConnect') except StopIteration: raise except: -- 2.30.2