#!/www/python/bin/python """http_monitor HTTP proxy to print out useful information about requests/responses: the request-line, status-line, cookies, etc. """ # created 2001/05/18, GPW __revision__ = "$Id$" import sys, string, re from time import time, localtime, strftime import socket, SocketServer import rfc822 from urlparse import urlparse, urlunparse from optik import Option, OptionParser from mems.lib.script_util import run def error (msg): sys.stderr.write("error: " + msg + "\n") def parse_url (url): # parse url, return (host, port, path) tuple # path really means path, params, query, fragment all together (scheme, netloc, path, params, query, fragment) = urlparse(url) assert scheme.lower() == "http", "non-HTTP request" if ':' in netloc: (host, port) = netloc.split(':') port = int(port) else: host = netloc port = 80 path = urlunparse(('', '', path, params, query, fragment)) return (host, port, path) #_http_token_re = re.compile( # r'[\0x00-\x1F^\(\)\<\>\@\,\;\:\\\"\/\[\]\?\=\{\}\ \t]+') def process_headers (infile, outfile): headers = rfc822.Message(infile) connection = headers.get('connection') if connection: connection_tokens = re.split(r',\s*', connection) #print "removing connection headers:", for token in connection_tokens: #print token, try: del headers[token] except KeyError: pass #print headers['Connection'] = "close" # we don't do persistent connections for line in headers.headers: outfile.write(line) outfile.write('\r\n') # rfc822.py swallows the blank line return headers def get_content_length (headers): try: return int(headers['content-length']) except KeyError, ValueError: return None def transfer_content (infile, outfile, content_length): block_size = 1024 bytes_transferred = 0 bytes_left = content_length while 1: if bytes_left is not None and bytes_left < block_size: block_size = bytes_left data = infile.read(block_size) outfile.write(data) bytes_transferred += len(data) if bytes_left is not None: bytes_left -= len(data) if not data or bytes_left == 0: break # HTTP proxy server implemented using this as a handler class ProxyHandler (SocketServer.BaseRequestHandler): def setup (self): self.client_rfile = self.request.makefile('rb') self.client_wfile = self.request.makefile('wb', 0) # unbuffered # server_rfile and server_wfile are created after we've # read the request line from the client (can't do it # until we know what server to talk to, of course!) self.server_rfile = None self.server_wfile = None def handle (self): #print "ProxyHandler.handle: got a connection from %s" % \ # `self.client_address` tstamp = strftime("%H:%M:%S", localtime(time())) print "--[ new connection from %s:%s at %s ]--------------------" % \ (self.client_address + (tstamp,)) (method, url, protocol) = self.start_request() print "req: %s %s %s" % (method, url, protocol) (host, port, path) = parse_url(url) protocol = "HTTP/1.0" #print "s< %s %s %s" % (method, url, protocol) self.server_connect(host, port) self.server_wfile.write("%s %s %s\r\n" % (method, url, protocol)) self.transfer_request() self.process_response() def start_request (self): """ Read the first line of the request from the client. Should be something like GET http://foo.com/bar HTTP/1.1 Return (method, uri, protocol) tuple that results from splitting this string on whitespace. """ line = self.client_rfile.readline() request = line.split() assert len(request) == 3, "bogus request" return tuple(request) def server_connect (self, host, port): """ Connect to the HTTP server at (host, port). Creates two files from the resulting socket, one for reading and one for writing, which are stored in the server_rfile and server_wfile attributes. """ addr = socket.gethostbyname_ex(host)[2][0] server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: server.connect((addr, port)) except socket.error, err: error("%s:%s: %s" % (addr, port, err[1])) else: self.server_rfile = server.makefile('rb') self.server_wfile = server.makefile('wb', 0) def transfer_request (self): """ Read and parse the request headers from the client, and send them to the server. Then, if there is a request body, read it and send it to the server (all in one chunk, since HTTP request bodies are usually small). """ #print "reading request headers..." headers = process_headers(self.client_rfile, self.server_wfile) #print "request headers:" #print headers cookies = headers.getallmatchingheaders('cookie') for cookie in cookies: cookie = cookie[cookie.find(':')+1:].strip() print "req: cookie:", cookie content_length = get_content_length(headers) if content_length is not None: #print "reading request content..." content = self.client_rfile.read(content_length) #print "finished" self.server_wfile.write(content) print "request content:" sys.stdout.write(content) sys.stdout.write("\n") def process_response (self): # We read lines from self.server_rfile, parse them on the fly as # needed, and pass them to the client (self.client_wfile). # Read the server's status line (something like "HTTP/1.1 200 OK") line = self.server_rfile.readline() print "rsp: " + `line` self.client_wfile.write(line) headers = process_headers(self.server_rfile, self.client_wfile) #print "response headers:" #print headers cookies = headers.getallmatchingheaders('set-cookie') for cookie in cookies: cookie = cookie[cookie.find(':')+1:].strip() print "rsp: set-cookie:", cookie # read content -- just echo server's output to client content_length = get_content_length(headers) #print "reading response content (length=%s)" % content_length transfer_content(self.server_rfile, self.client_wfile, content_length) #print "finished" # Workaround bug in Python 2.1's SocketServer.ThreadingMixIn class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): def close_request(self, request): pass def main (): usage = "usage: %prog [-t|-f] [-b bind_addr] [-p port]" option_list = [ Option('-t', '--threaded', action='store_const', dest='mode', const="threaded", help="start a multi-threaded server"), Option('-f', '--forking', action='store_const', dest='mode', const="forking", help="start a forking (multi-process) server"), Option('-b', '--bind-to', type='string', dest='bind_address', default="localhost", metavar="ADDR", help="bind to ADDR (default: localhost)"), Option('-p', '--port', type='int', dest='port', default=8000, help="listen to port PORT (default: 8000)") ] parser = OptionParser(usage, option_list) parser.set_defaults(mode="single") (options, args) = parser.parse_args() server_klass = {'single': SocketServer.TCPServer, 'threaded': ThreadingTCPServer, 'forking': SocketServer.ForkingTCPServer}[options.mode] try: server = server_klass((options.bind_address, options.port), ProxyHandler) except socket.error, err: error("%s:%s: %s" % (options.bind_address, options.port, err[1])) sys.exit(1) server.serve_forever() if __name__ == "__main__": run()