# New HTTP proxy - Amit Patel, amitp@cs.stanford.edu # Thu 14 Aug 1997 - Initial design # Fri 15 Aug 1997 - Implemented single connection proxy. # Sun 17 Aug 1997 - Implemented multiconnection proxy. # - Extensive testing # Mon 18 Aug 1997 - Added special URLs: # http://_proxy/location - retreives host/port # http://_proxy/start/* - redirects to ishin # - More testing # Fri 22 Aug 1997 - Added special URL: # http://_proxy/start/ - redirects to /ui/ # http://_proxy/ui/* - redirects to ishin # - Also, the /start/ URL will append the hostname to the # URL so that applets will be granted permission to # communicate with that host. # - Any request for a local document will be redirected # to http://theory.stanford.edu/ # Mon 25 Aug 1997 - Fixed bug that caused sockets to block when partial # data was received from a web server. # Tue 26 Aug 1997 - Added test code for non-GET requests from UI applet # Thu 28 Aug 1997 - Investigated the POST form method, which requires # changes to the proxy data transfer system. # Fri 29 Aug 1997 - Simplified the sending of HTML responses by # writing html_output function, which adds proper # headers automatically. # - Simplified the handling of blocked sites by # using glob patterns instead of explicit if/else code. # - Added the HTTPRequest handler, which takes over # handling from Request if it's GET/POST # - Fixed a bug with form handling: the connection # must be *blocking* in order for the HTTPRequest # class to work. It can't receive partial data # as it's processing the headers. # Mon 8 Sep 1997 - Split CopyFile into several handlers: CopyData, # HTTPReply, DataReader, and BufferedWriter. # - HTTPReply reads in HTTP reply headers and writes # them out. The headers are needed because important # information, such as content-type, is used to # dispatch on the file type. # - DataReader just reads in data from a connection. # - CopyData writes the data to an output connection. # - BufferedWriter waits until all data is read before # writing the data out. It also runs the data through # a series of filters. # - Analyzed the design of these new classes and determined # that poor performance would result # - Redesigned data copying classes # Tue 9 Sep 1997 - Added an Options class that encapsulates all user # options # - Rewrote Excite Ad filter to fit into the BufferedWriter # & DataReader framework # - Moved blocked site functions and data into a separate # class # Wed 10 Sep 1997 - Added the JavaClassFilter to perform class substitutions # on Java classes, and tested it on some of Sun's applets # Thu 11 Sep 1997 - Added Safe$Class redirection to redirect requests for # certain classes to a different site # - Added timing code to see which handlers are taking a # long time # Wed 18 Sep 1997 - Fixed bug in Java class substitution: if the class # being loaded is a substituted class, no further # substitutions should be made # Mon 22 Sep 1997 - Fixed bug in UIP class that reports blocked sites # to the user interface applet. # - Fixed bug in class redirection code. # Tue 23 Sep 1997 - Changed code to determine filters to use a table # based on URL and MIME types. # Changes made by Insik Shin, ishin@cs.stanford.edu # Tue 2 Sep 1997 - Added communication with the UI through a second # listener (at port 3332). The second listener is # in the UIListener class. When the UI listener # receives a request, it uses the UIPRcvRequest and # UIPSndRequest classes to receive and send messages. # - Changed HTTPRequest to keep track of the remote # socket address. # Mon 8 Sep 1997 - Added Excite Ad filtering (with a small bug that # only shows up if the ad was not received in one # chunk) # - Added control of ad filtering, site blocking, and # Java class substitution into the UI # - Added support in the proxy to control these features # - Added UI support for querying the list of blocked sites # Notes: # - Determine whether we should be using select for WRITE as well as READ. # - See http://www.nightmare.com/medusa/async_sockets.html for information # about the event-based model used in this new Proxy. # - A socket is closed if upon recv(1,socket.MSG_PEEK), it # raises (_,'Bad file number') # - The CONNECT method should be routed to a different request handler # - Binary data includes a 'Content-length' header. # - The User-Agent header can include multiple words, each of the form # [/]. The proxy could append "MURI-Python-Proxy/??" from string import * from select import select from time import time import sys, array import httplib, urlparse, socket, rfc822, mimetools, BaseHTTPServer import regex, regsub HOST = socket.gethostname() PORT = 3333 UIPORT = 3332 class Options: CUT_ADS = 1 XFORM_CLASS = 1 BLOCKING = 1 USE_PROXY = 1 # If 0, the proxy exits # Patch for httplib to support 1.1 httplib.replypat = regsub.gsub('\\.', '\\\\.', "HTTP/1.[0-9]+") + \ '[ \t]+\([0-9][0-9][0-9]\)\(.*\)' httplib.replyprog = regex.compile(httplib.replypat) # End patch, hopefully def stripsite(url): url = urlparse.urlparse(url) return url[1], urlparse.urlunparse( (0,0,url[2],url[3],url[4],url[5]) ) def glob_to_regex(glob): a = '^' + glob + '$' b = regsub.gsub('[.]','[.]',a) c = regsub.gsub('[*]','.*',b) return regex.compile(c) def html_output(file, data, code=200): response = BaseHTTPServer.BaseHTTPRequestHandler.responses[code][0] file.write("HTTP/1.0 %s %s\n" % (code, response)) file.write("Server: MURI Python Proxy\n") file.write("Content-type: text/html\n") file.write("Content-length: "+`len(data)`+'\n') file.write("\n") file.write(data+"\n") file.close() def ExciteAdFilter(s): """Search for an advertisement, and remove it from the HTML""" start_ad = regex.compile('') end_ad = regex.compile('') i = start_ad.search(s) if i < 0: return s j = end_ad.search(s,i) if j < 0: return s # Remove the section from i to j # Note that the "Ad Stop" section is left in -- Amit's laziness return s[:i]+s[j:] def GenericAdFilter(site): return (lambda s,site=site: _GenericAdFilter(s,site)) def _GenericAdFilter(s,site): """Search for an IMG reference to a blocked site, and remove it""" start_ad = regex.compile('<[iI][mM][gG] ') end_ad = regex.compile('>') srcpat = regex.compile('[sS][rR][cC] *= *"?\([^ "]*\)') results = [] ad_count = 0 while 1: i = start_ad.search(s) if i < 0: break j = end_ad.search(s,i) if j < 0: break # Save the first part of the string results.append(s[:i]) # Remove the section from i to j only if it's a blocked site j = j+1 # Add the > at the end img, s = s[i:j], s[j:] p = srcpat.search(img) if p >= 0: file = srcpat.group(1) if find(file,'http:') < 0: file = site+file else: file = file[7:] if blocker.matches(file): img = "" ad_count = ad_count+1 # Save the img tag results.append(img) # Save anything left in the string results.append(s) # Display something if ad_count > 0: print ' Filtered',ad_count,'ad(s).' # Concatenate all the pieces and return return join(results,'') class JavaClassSubstituter: """Transform Java bytecode in the string 's', by performing each of the substitutions in substs, a mapping from str->str, and then return the modified string""" # Add all class substitutions here substs = { 'java/awt/Frame': 'SafeFrame', # 'java/lang/Thread': 'SafeThread', } def __init__(self,s): self.s = s # input string self.a = array.array('c') # output array self.p = 0 # position in the input # Transfer the magic number magic = self.read(4) if magic != '\xca\xfe\xba\xbe': # Not a Java file self.write(self.s) return self.write(magic) self.transfer(4) # Read the number of constants, a short int (2 bytes) nc = self.read(2) self.write(nc) num_const = ord(nc[0])*256 + ord(nc[1]) # ################# CHECK THIS # ## I've heard that the number of constants is one more than # ## the actual number of constants (!) num_const = num_const - 1 print 'Java (Processing',len(s),'byte Java class with ', \ num_const,'constants)' for i in range(num_const): self.const_subst() # Transfer the rest of the bytecode self.write(self.s[self.p:]) def read(self,n): """`Read' n bytes from the input string""" r = self.s[self.p:self.p+n] self.p = self.p+n # Pad the string with '\0' if needed; this is an ugly hack but # it helps us avoid errors! :( return r + '\0' * (n-len(r)) def write(self,s): """`Write' a string to the output array""" self.a.fromstring(s) def transfer(self,n): """Copy n bytes from the input to the output""" self.write(self.read(n)) def output(self): """Turn the output array back into a string and return it""" return self.a.tostring() # These are constant sizes for various constant types const_sizes = {7:2,8:2, 3:4,4:4,9:4,10:4,11:4,12:4, 5:8,6:8} def const_subst(self): """Handle one entry in the constant pool""" flag = self.read(1) self.write(flag) flag = ord(flag) if flag==1: # It's a string strsize = self.read(2) size = ord(strsize[0])*256 + ord(strsize[1]) s = self.read(size) print 'Java - String',`size`,`s` if s in self.substs.keys(): # Change this string into another s = self.substs[s] strsize = chr(0) + chr(len(s)) print 'Java ==> ',`len(s)`,`s` self.write(strsize) self.write(s) elif flag > 1 and flag < 13: # It's a constant of some other sort self.transfer(self.const_sizes[flag]) else: print '* Unknown constant code, flag is',flag pass def JavaClassFilter(s): j = JavaClassSubstituter(s) return j.output() class Handler: """Handlers are objects that can be used as network connections (i.e., with a tofile method) but also have a method to process input from that connection.""" pass class DataReader(Handler): """Read data from an input connection, and send it somewhere.""" def __init__(self, infile, name): self.infile = infile self.name = name def __repr__(self): # __repr__ is a special function that is used to display the # object as a string return '#' def fileno(self): # Fileno is used to tell the system which file handle is being # waited on return self.infile.fileno() def process(self): BUFSIZE = 32768 # how many bytes to transfer at once try: data = self.infile.read(BUFSIZE) except IOError: # Broken pipe? data = None if not data: # This connection is dead self.close() return [] else: # This connection may have more data try: self.read(data) except IOError: # Broken pipe? self.close() return [] return [self] def read(self, data): raise "DataReader: unimplemented method" def close(self): self.infile.close() class CopyData(DataReader): """Directly send any data that is read to an output connection""" def __init__(self, infile, outfile, name): DataReader.__init__(self, infile, name) self.outfile = outfile def read(self, data): self.outfile.write(data) def close(self): DataReader.close(self) self.outfile.close() class BufferedWriter(DataReader): """Save all the data and send it through a list of filters, then write""" def __init__(self, infile, outfile, name, filters): DataReader.__init__(self, infile, name) self.outfile = outfile self.filters = filters self.buffer = [] def read(self, data): self.buffer.append(data) def close(self): DataReader.close(self) s = join(self.buffer,'') for f in self.filters: s = f(s) self.outfile.write(s) self.outfile.close() def set_header(header, key, value): h = header.headers for i in range(len(h)): if h[i][:len(key)] == key: h[i] = h[i][:len(key)] + ' ' + value + '\r\n' return print 'NO HEADER FOUND' class HTTPReply(Handler): def __init__(self, infile, outfile, name, socket, site, document): self.infile = infile self.outfile = outfile self.name = name self.socket = socket self.site = site self.document = document socket.setblocking(1) def __repr__(self): return '#' def fileno(self): return self.infile.fileno() def process(self): self.reply = self.infile.readline() self.headers = rfc822.Message(self.infile, 0) # print '|',self.reply, # print '|',join(self.headers.headers,'| ') if self.document[-6:] == ".class": print '|',join(self.headers.headers,'| ') # MAKE SURE THE HEADER IS THERE # length = atoi(self.headers['Content-length']) # length = length + 100 # set_header(self.headers, 'Content-length', `length`) print '*',join(self.headers.headers,'* ') self.outfile.write(self.reply) self.outfile.write(join(self.headers.headers, '')+'\n') self.socket.setblocking(0) self.socket.close() filters = self.find_filters() if filters: chained = BufferedWriter(self.infile, self.outfile, self.name, filters) else: chained = CopyData(self.infile, self.outfile, self.name) return [chained] def find_filters(self): try: type = self.headers['Content-type'] except KeyError: type = 'text/plain' filter_table = [ (Options.CUT_ADS,'*.excite.com/*','text/html',[ExciteAdFilter]), (Options.CUT_ADS,'*','text/html',[GenericAdFilter(self.site)]), (Options.XFORM_CLASS,'*.class','*',[JavaClassFilter]), # (Options.XFORM_CLASS,'*.cnn.com/*Headline.class','*', # [JavaClassFilter,CnnFilter]) (Options.XFORM_CLASS,'*','application/java-vm',[JavaClassFilter]), ] # Add all the exceptions to the substitution table - ew for x in JavaClassSubstituter.substs.values(): filter_table = [ (1,'*/'+x+'.class','*',[]) ] + filter_table # Find the substitutions appropriate for this document filters = [] for enabled,urlpat,mimetype,result in filter_table: if enabled and glob_to_regex(urlpat).match( self.site+self.document) >= 0 \ and glob_to_regex(mimetype).match(type) >= 0: filters = result break return filters class Blocker: blocked = [ '*/cgi-bin/fetch_ad*', 'www.doubleclick.net/*', 'ad.doubleclick.net/*', 'ad.linkexchange.com/*', 'ads.lycos.com/*', '*cnn*.com/*&AdID=*', '*cnn*.com/ads/*', '*cnn*.com/images/*/explore.anim.gif', '*cnn*.com/images/*/pathnet.warner.gif', 'www.morningstar.net/ads/*', 'images.yahoo.com/adv/*', 'adserv.newcentury.net/*' '*.yahoo.com/promotions/*', '*.infoseek.com/ads/*', '*.excite.com/img/ads/*', '*.hotbot.com/ads/*', '*.wired.com/advertising/*' 'ads.washingtonpost.com/*' ] def __init__(self): self.blocked = [] # Process the glob expressions, then compile into regexes # Store the regexes in the object, not the class self.blocked = map(glob_to_regex,Blocker.blocked) def matches(self, s): """Returns 1 if the string matches one of the blocked sites""" for b in self.blocked: if b.match(s) >= 0: return 1 return 0 # Define an instance of a global blocking variable blocker = Blocker() def check_hooks(site, document, headers, fi, fo, address): # This is a temporary function in place until I figure out how # to keep this stuff modular if site=='_proxy' and document=='/start/': site = "http://%s:%s/http://_proxy/ui/" % (HOST, PORT) html_output(fo, """ Redirecting to the UI Applet Select to start the UI """ % (site, site)) fi.close() return [] if site=='_proxy' and document=='/location': html_output(fo, "Proxy running at: %s %s" % (HOST, PORT)) fi.close() return [] if site=='_proxy' and document=='/exit': html_output(fo, "The proxy has terminated.") fi.close() Options.USE_PROXY = 0 return [] # Scan for blocked sites & documents if Options.BLOCKING and blocker.matches(site+document): print "|___X-> Aborted" UIPSndRequest(address,3331," -----> Blocked").process() html_output(fo, "Document blocked", code=404) fi.close() return [] # If it wasn't a special document, then return 0 return 0 class HTTPRequest(Handler): def __init__(self, infile, outfile, words, reqno, address): self.infile = infile self.outfile = outfile self.words = words self.reqno = reqno self.address = address def fileno(self): return self.infile.fileno() def __repr__(self): return '#' % (self.words[0], self.reqno) def process(self): fi = self.infile fo = self.outfile headers = rfc822.Message(fi, 0) # Figure out where we want to connect site, document = stripsite(self.words[1]) if not site and 'Host' in headers.keys(): site = headers['Host'] # If there was no machine name, then go to theory if not site: site = 'theory.stanford.edu' # This allows us to use the proxy without setting up a proxy # in the web browser. Just use a URL of the form: # http://proxymachine:3333/http://... if document[:8] == '/http://': site, document = stripsite(document[1:]) # Display the site/document pair output = '|_____> [%s] %s' % (site, document) print output[:79] UIPSndRequest(self.address[0], 3331, '* '+site+document).process() # Hook in here to have custom handlers result = check_hooks(site, document, headers, fi, fo, self.address[0]) if type(result) == type([]): # It returned a list, which means it handled it return result # Redirection of safe classes end_name = split(document,'/')[-1] if end_name[-6:] == '.class' and find(site,'~ishin/proxy') < 0: # This is a Java class, so let's determine whether it needs # to be redirected. if end_name[:-6] in JavaClassSubstituter.substs.values(): # This class is one of our inserted classes, so it should # be loaded from a different place site = 'www-cs-students.stanford.edu' document = '/~ishin/proxy/' + split(document,'/')[-1] print '| ===> Redirecting to',site+document # Redirection to Insik's PUI page if site=='_proxy' and document[:4]=='/ui/': # All the pages on _proxy/ui/ have to be redirected site = 'www-cs-students.stanford.edu' if document=='/ui/': document = document + 'PUI.html' document = '/~ishin/proxy/' + document[4:] # Create a new connection try: s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # If the site has a : in it, that means that it's # a site followed by a port number i = find(site,':') if i >= 0: port = atoi(site[i+1:]) site = site[:i] else: port = 80 # Unfortunately I don't know how to make this time out s2.connect(site, port) s2.setblocking(0) except socket.error, err: # Error connecting, so don't return this handler html_output(fo, "Could not connect", code=500) fi.close() s2.close() print '* *** Socket Error:',err return [] # Treat socket s2 like a pair of files (in2,out2) in2 = s2.makefile('r') out2 = s2.makefile('w',0) # Forward the request to the remote server out2.write('%s %s %s\n' % (self.words[0], document, self.words[2])) out2.write(join(headers.headers, '')+'\n') # Additional data may be sent along with this request if 'Content-length' in headers.keys(): # There is additional data to transfer data = fi.read(atoi(headers['Content-length'])) print '| * Extra data being sent:',`data`[:40] out2.write(data) # Handler for the reply: # This handler will copy the reply headers from the web server # to the browser, then invoke the chained handler. Note that we # kill the current handler by not including it. handlers = [ HTTPReply(in2, fo, 'Reply '+`self.reqno`, s2, site, document) ] # Try transferring any available data data = None # fi.read(1) ??????????????? if data: out2.write(data) # Add a handler to copy any additional data handlers.append(CopyData(fi, out2, 'Request '+`self.reqno`)) out2.close() else: # assume the connection is closed out2.close() fi.close() return handlers class Request(Handler): """Accept a request from the web browser, and send it to the web server.""" # Request number, to keep track of them all next_reqno = 1 def __init__(self, infile, outfile, address): self.infile = infile self.outfile = outfile self.address = address # Assign this request a new number self.reqno = Request.next_reqno Request.next_reqno = Request.next_reqno+1 def fileno(self): return self.infile.fileno() def __repr__(self): site = ' from %s' % self.address[0] if self.address[0] == '127.0.0.1': site = ' (local)' return '#' % (self.reqno, site) def process(self): fi = self.infile fo = self.outfile line = fi.readline() while line[-1:] == '\r' or line[-1:] == '\n': line = line[:-1] words = split(line) if len(words) == 2: words.append('HTTP/0.9') if len(words) != 3 or not (words[0] in ['HEAD','GET','POST']): # Send error 400 html_output(fo, "Bad Request: %s" % line, code=400) print ' ??? '+line[:70] # Nothing left to do here, so don't return self fi.close() return [] else: return HTTPRequest(fi, fo, words, self.reqno, self.address).process() class Listener(Handler): """Accept and create connections from web browsers""" def __init__(self, PORT): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind('', PORT) s.setblocking(0) s.listen(5) self.conn = s self.port = PORT def __repr__(self): return '#' def fileno(self): return self.conn.fileno() def process(self): try: request, address = self.conn.accept() except socket.error: # Connection attempted by browser, but aborted(?). # Note that we must always include self in the return # value for Listener because the Listener should always # be kept up. return [self] # Create a handler for this connection # Note that this connection has to be blocking, unfortunately, # because after it is select()-ed, the RFC822 header library # needs to read all the headers at once. If the connection is # non-blocking, then the headers might be read only partially, # and then HTTPRequest will be confused. request.setblocking(1) handler = Request( request.makefile('r'), request.makefile('w', 0), address ) # Note that Python is garbage collected, and although this # particular object is being closed, the socket itself won't # be closed until all references (in particular, the two # makefiles above) are closed. request.close() # Return self AND the handler, indicating that both should # be put in the list of handlers return [self, handler] ###### User Interface classes class UIPSndRequest(Handler): """User Interface Protocol Handler for sending UI messages.""" def __init__ (self, host, port, message): self.host = host self.port = port self.message = message def process (self): # Create a new connection try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(self.host, self.port) s.setblocking(0) except socket.error, err: # Error connecting, so don't return this handler s.close() # Don't print 111 because it just means the UI applet # isn't up and running. if err[0] != 111: print '* *** UIP Socket Error',err[0],err[1] return [] # Treat socket s like a pair of files (infile,outfile) self.infile = s.makefile('r') self.outfile = s.makefile('w',0) s.close() # send message self.outfile.write(self.message) return [] class UIPRequest(Handler): """User Interface Protocol Handler for receivinb messages from UI. Accept UI's messages, and send their reply back to UI""" # Request number, to keep track of them all next_reqno = 1 def __init__(self, infile, outfile, address): self.infile = infile self.outfile = outfile self.address = address # Assign this request a new number self.reqno = Request.next_reqno Request.next_reqno = Request.next_reqno+1 def fileno(self): return self.infile.fileno() def __repr__(self): site = ' from %s' % self.address[0] if self.address[0] == '127.0.0.1': site = ' (local)' return '#' % (self.reqno, site) def process(self): fi = self.infile fo = self.outfile line = fi.readline() while line[-1:] == '\r' or line[-1:] == '\n': line = line[:-1] words = split(line) # Handle messages print ' -->', words if words == ['CONNECT']: fo.write("CONNECT OK\n") elif words == ['Ad','Filtering']: Options.CUT_ADS = 1 fo.write("OK\n") elif words == ['No','Ad','Filtering']: Options.CUT_ADS = 0 fo.write("OK\n") elif words == ['Class','Substitution']: Options.XFORM_CLASS = 1 fo.write("OK\n") elif words == ['No','Class','Substitution']: Options.XFORM_CLASS = 0 fo.write("OK\n") elif words == ['Site','Blocking']: Options.BLOCKING = 1 fo.write("OK\n") elif words == ['No','Site','Blocking']: Options.BLOCKING = 0 fo.write("OK\n") elif words == ['Asking','Blocked','Sites']: for b in Blocker.blocked: fo.write(" " + b +"\n"); elif words == ['ALL']: fo.write("MESSAGE ALL\n") elif words == ['MIDDLE']: fo.write("MESSAGE MIDDLE\n") elif words == ['NOTHING']: fo.write("MESSAGE NOTHING\n") else: fo.write("UNKNOWN") fi.close() fo.close() return [] class UIListener(Handler): """Accept and create connections from UI""" def __init__(self, UIPORT): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind('', UIPORT) s.setblocking(0) s.listen(1) self.conn = s def __repr__(self): return '#' def fileno(self): return self.conn.fileno() def process(self): try: request, address = self.conn.accept() except socket.error: # Connection attempted by UI, but aborted(?). # Note that we must always include self in the return # value for UIListener because the UIListener should always # be kept up. return [self] request.setblocking(1) handler = UIPRequest( request.makefile('r'), request.makefile('w', 0), address ) request.close() # Return self AND the handler, indicating that both should # be put in the list of handlers return [self, handler] ####### End of User Interface classes def proxy(): # At first, spawn two listeners, one for HTTP connections and the # other for UI connections. listener = Listener(PORT) UIlistener = UIListener(UIPORT) connections = [listener, UIlistener] while Options.USE_PROXY: retval = select(connections, [], [], 300.0) ready = retval[0] status = '' for c in connections: if c == listener: pass elif c in ready: status = status + '@' else: status = status + '.' status = (status+' '*6)[:6] if ready: # Pick one socket, and prefer the listener over others if listener in ready and len(ready) > 1: ready = listener else: ready = ready[0] print '|'+status, ready i = connections.index(ready) try: # The result of processing this handler is a list # of replacement handlers. In some sense this is # like the Actor model, where an actor processes # input and then replaces itself with other actors. # We put the new handlers at the end to give other # handlers a chance to run. del connections[i] before_time = time() connections = connections + ready.process() after_time = time() time_elapsed = after_time - before_time if time_elapsed > 0.1: print '| (took %2.5f seconds)' % time_elapsed except IOError, err: # If there was an I/O error, let's er um, print it sys.last_traceback = None print "* ", err else: print ' '+status, connections def test(): print 'Starting proxy on',HOST,PORT,'; press Ctrl-C to stop.' try: proxy() except KeyboardInterrupt: print 'Terminating.' if __name__ == '__main__': test()