Make sure all headers end in "\r\n".
[cheatproxy.git] / cheatproxy.py
blob3ec455b1beffdb7a86ac99ebacb38829ce7be7a7
1 #!/usr/bin/env python
3 """A basic HTTP proxy server that intercepts communication with
4 BitTorrent trackers and optionally spoofs the amount of data
5 uploaded. Free from artificial colours and preservatives. Web 2.0
6 compatible."""
8 import getopt
9 import logging
10 import select
11 import socket
12 import sys
13 import urlparse
15 import BaseHTTPServer
17 import cheatbt
19 class CheatHandler(BaseHTTPServer.BaseHTTPRequestHandler):
20 """Used by HTTPServer to handle HTTP requests"""
22 mappings = {}
24 def do_GET(self):
25 """Called by BaseHTTPRequestHandler when a GET request is
26 received from a client."""
28 cheatpath = cheatbt.cheat(self.path, CheatHandler.mappings)
30 logger = logging.getLogger("cheatproxy")
31 logger.info(cheatpath)
33 (scheme, netloc, path, params, query, fragment) = \
34 urlparse.urlparse(cheatpath, 'http')
36 # TODO: https support.
37 if scheme != 'http' or fragment or not netloc:
38 self.send_error(501)
39 return
41 soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
43 try:
44 if self._connect_to(netloc, soc):
45 request = urlparse.urlunparse(('', '', path, params, query, ''))
46 soc.send("%s %s %s\r\n" % (self.command, request,
47 self.request_version))
48 self.headers['Connection'] = 'close'
49 del self.headers['Proxy-Connection']
50 # This is naughty. But rfc822.Message, which self.headers is a
51 # subclass of, insists on converting headers to lowercase when
52 # accessed conventionally (i.e. as a dict).
53 for header in self.headers.headers:
54 soc.send(header.strip() + '\r\n')
55 logger.debug(repr(header))
56 soc.send("\r\n")
57 self._read_write(soc)
58 finally:
59 soc.close()
60 self.connection.close()
62 def _connect_to(self, netloc, soc):
63 """Attempt to establish a connection to the tracker."""
65 i = netloc.find(':')
66 if i >= 0:
67 host_port = (netloc[:i], int(netloc[i+1:]))
68 else:
69 host_port = (netloc, 80)
71 try:
72 soc.connect(host_port)
73 except socket.error:
74 self.send_error(502)
75 return False
77 return True
79 def _read_write(self, soc, max_idling=20):
80 """Pass data between the remote server and the client. I
81 think."""
83 rlist = [self.connection, soc]
84 wlist = []
85 count = 0
86 while True:
87 count += 1
88 (ins, _, exs) = select.select(rlist, wlist, rlist, 3)
89 if exs:
90 break
91 if ins:
92 for i in ins:
93 if i is soc:
94 out = self.connection
95 else:
96 out = soc
97 data = i.recv(8192)
98 if data:
99 out.send(data)
100 count = 0
101 else:
102 pass
103 if count == max_idling:
104 break
106 def usage():
107 """Prints usage information and exits."""
109 print """
110 usage: %s [-b host] [-p port] [-f file] [-v] [-d] [-h]
112 -b host IP or hostname to bind to. Default is localhost.
113 -p port Port to listen on. Default is 8000.
114 -f file Mappings file.
115 -v Verbose output.
116 -d Debug output.
117 -h What you're reading.
118 """ % sys.argv[0]
119 sys.exit(1)
121 def main():
122 host = "localhost"
123 port = 8000
125 rootlogger = logging.getLogger("")
126 ch = logging.StreamHandler()
127 ch.setFormatter(
128 logging.Formatter("%(asctime)s:%(name)s:%(levelname)s %(message)s"))
129 rootlogger.addHandler(ch)
131 try:
132 opts, _ = getopt.getopt(sys.argv[1:], "b:f:p:hvd")
133 except getopt.GetoptError:
134 usage()
136 for opt, val in opts:
137 if opt == "-b":
138 host = val
139 if opt == "-p":
140 port = int(val)
141 if opt == "-f":
142 CheatHandler.mappings = cheatbt.load_mappings(val)
143 if opt == "-v":
144 rootlogger.setLevel(logging.INFO)
145 if opt == "-d":
146 rootlogger.setLevel(logging.DEBUG)
147 if opt == "-h":
148 usage()
150 httpd = BaseHTTPServer.HTTPServer((host, port), CheatHandler)
152 logger = logging.getLogger("cheatproxy")
153 logger.info("listening on %s:%d" % (host, port))
155 httpd.serve_forever()
157 if __name__ == "__main__":
158 try:
159 main()
160 except KeyboardInterrupt:
161 logging.shutdown()
162 sys.exit()