23
23
# SOFTWARE.
24
24
25
25
"""
26
- Throxy (Throttling Proxy)
26
+ Throxy (Throttling Proxy), a simple HTTP proxy.
27
27
28
- This is a simple HTTP proxy, with the following features:
28
+ To use it, run this script on your local machine and adjust your
29
+ browser settings to use 127.0.0.1:8080 as HTTP proxy.
29
30
30
31
* Simulates a slow connection (like dial-up).
32
+ * Adjustable bandwidth limit for download and upload.
31
33
* Optionally dumps HTTP headers and content for debugging.
34
+ * Supports gzip decompression for debugging.
32
35
* Supports multiple connections, without threading.
33
- * Written in pure Python.
34
-
35
- To use it, run this script on your local machine and adjust your
36
- browser settings to use 127.0.0.1:8080 as HTTP proxy.
36
+ * Only one source file, written in pure Python.
37
37
"""
38
38
39
39
import sys
53
53
54
54
55
55
class Header :
56
+ """HTTP (request or reply) header parser."""
56
57
57
58
def __init__ (self ):
58
59
self .data = ''
59
60
self .lines = []
60
61
self .complete = False
61
62
62
63
def append (self , new_data ):
64
+ """
65
+ Add more data to the header.
66
+
67
+ Any data after the end of the header is returned, as it may
68
+ contain content, or even the start of the next request.
69
+ """
63
70
self .data += new_data
64
71
while not self .complete :
65
72
newline = self .data .find ('\n ' )
66
73
if newline < 0 :
67
- break # No complete line found.
74
+ break # No complete line found
68
75
line = self .data [:newline ].rstrip ('\r ' )
69
76
if len (line ):
70
77
self .lines .append (line )
@@ -76,15 +83,14 @@ def append(self, new_data):
76
83
self .gzip_data = cStringIO .StringIO ()
77
84
self .data = self .data [newline + 1 :]
78
85
if self .complete :
79
- # Give remaining data back to caller. It may contain
80
- # content, or even the start of the next request.
81
86
rest = self .data
82
87
self .data = ''
83
88
return rest
84
89
else :
85
90
return ''
86
91
87
92
def extract (self , name , default = '' ):
93
+ """Extract a header field."""
88
94
name = name .lower ()
89
95
for line in self .lines :
90
96
if not line .count (':' ):
@@ -95,10 +101,10 @@ def extract(self, name, default=''):
95
101
return default
96
102
97
103
def extract_host (self ):
104
+ """Extract host and perform DNS lookup."""
98
105
self .host = self .extract ('Host' )
99
106
if self .host is None :
100
107
return
101
- # print "client %s:%d wants to talk to" % self.client.addr, self.host
102
108
if self .host .count (':' ):
103
109
self .host_name , self .host_port = self .host .split (':' )
104
110
else :
@@ -108,6 +114,7 @@ def extract_host(self):
108
114
self .host_addr = (self .host_ip , self .host_port )
109
115
110
116
def extract_request (self ):
117
+ """Extract path from HTTP request."""
111
118
match = request_match (self .lines [0 ])
112
119
if not match :
113
120
raise ValueError ("malformed request line " + self .lines [0 ])
@@ -120,17 +127,20 @@ def extract_request(self):
120
127
self .path = self .url [len (prefix ):]
121
128
122
129
def dump_title (self , from_addr , to_addr , direction , what ):
130
+ """Print a title before dumping headers or content."""
123
131
print '==== %s %s (%s:%d => %s:%d) ====' % (
124
132
direction , what ,
125
133
from_addr [0 ], from_addr [1 ],
126
134
to_addr [0 ], to_addr [1 ])
127
135
128
136
def dump (self , from_addr , to_addr , direction = 'sending' ):
137
+ """Dump header lines to stdout."""
129
138
self .dump_title (from_addr , to_addr , direction , 'headers' )
130
139
print '\n ' .join (self .lines )
131
140
print
132
141
133
142
def dump_content (self , content , from_addr , to_addr , direction = 'sending' ):
143
+ """Dump content to stdout."""
134
144
self .dump_title (from_addr , to_addr , direction , 'content' )
135
145
if self .content_encoding :
136
146
print "(%d bytes of %s with %s encoding)" % (len (content ),
@@ -155,25 +165,27 @@ def dump_content(self, content, from_addr, to_addr, direction='sending'):
155
165
if len (content ) < limit or limit == 0 :
156
166
print content
157
167
else :
158
- print content [:limit ] + '(truncated after %d bytes)' % limit
168
+ print content [:limit ] + '(showing only %d bytes)' % limit
159
169
print
160
170
161
171
def gunzip (self ):
172
+ """Decompress gzip content."""
162
173
if self .gzip_data .tell () > options .gzip_size_limit :
163
174
raise IOError ("More than %d bytes" % options .gzip_size_limit )
164
- self .gzip_data .seek (0 ) # seek to start of data
175
+ self .gzip_data .seek (0 ) # Seek to start of data
165
176
try :
166
177
gzip_file = gzip .GzipFile (
167
178
fileobj = self .gzip_data , mode = 'rb' )
168
179
result = gzip_file .read ()
169
180
gzip_file .close ()
170
181
except struct .error :
171
182
raise IOError ("Caught struct.error from gzip module" )
172
- self .gzip_data .seek (0 , 2 ) # seek to end of data
183
+ self .gzip_data .seek (0 , 2 ) # Seek to end of data
173
184
return result
174
185
175
186
176
187
class ThrottleSender (asyncore .dispatcher ):
188
+ """Data connection with send buffer and bandwidth limit."""
177
189
178
190
def __init__ (self , kbps , channel = None ):
179
191
if channel is None :
@@ -187,9 +199,11 @@ def __init__(self, kbps, channel=None):
187
199
self .buffer = []
188
200
189
201
def log_sent_bytes (self , bytes ):
202
+ """Add timestamp and byte count to transmit log."""
190
203
self .transmit_log .append ((time .time (), bytes ))
191
204
192
205
def trim_log (self , horizon ):
206
+ """Forget transmit log entries that are too old."""
193
207
while len (self .transmit_log ) and self .transmit_log [0 ][0 ] <= horizon :
194
208
self .transmit_log .pop (0 )
195
209
@@ -201,11 +215,11 @@ def weighted_bytes(self):
201
215
return 0
202
216
weighted = 0.0
203
217
for timestamp , bytes in self .transmit_log :
204
- age = now - timestamp # Event's age in seconds.
218
+ age = now - timestamp # Event's age in seconds
205
219
assert 0 <= age <= self .interval
206
220
weight = 2.0 * (self .interval - age ) / self .interval
207
221
assert 0.0 <= weight <= 2.0
208
- weighted += weight * bytes # Newer entries count more.
222
+ weighted += weight * bytes # Newer entries count more
209
223
return int (weighted / self .interval )
210
224
211
225
def weighted_kbps (self ):
@@ -217,14 +231,15 @@ def sendable(self):
217
231
return max (0 , self .bytes_per_second - self .weighted_bytes ())
218
232
219
233
def writable (self ):
234
+ """Check if this channel is ready to write some data."""
220
235
return (len (self .buffer ) and
221
236
self .sendable () / 2 > self .fragment_size )
222
237
223
238
def handle_write (self ):
239
+ """Write some data to the socket."""
224
240
max_bytes = self .sendable () / 2
225
241
if max_bytes < self .fragment_size :
226
242
return
227
- # print "sendable", max_bytes
228
243
bytes = self .send (self .buffer [0 ][:max_bytes ])
229
244
self .log_sent_bytes (bytes )
230
245
if bytes == len (self .buffer [0 ]):
@@ -234,6 +249,7 @@ def handle_write(self):
234
249
235
250
236
251
class ClientChannel (ThrottleSender ):
252
+ """A client connection."""
237
253
238
254
def __init__ (self , channel , addr ):
239
255
ThrottleSender .__init__ (self , options .download , channel )
@@ -244,9 +260,11 @@ def __init__(self, channel, addr):
244
260
self .handle_connect ()
245
261
246
262
def readable (self ):
263
+ """Check if this channel is ready to receive some data."""
247
264
return self .server is None or len (self .server .buffer ) == 0
248
265
249
266
def handle_read (self ):
267
+ """Read some data from the client."""
250
268
data = self .recv (8192 )
251
269
while len (data ):
252
270
if self .content_length :
@@ -274,16 +292,19 @@ def handle_read(self):
274
292
self .server = ServerChannel (self , self .header )
275
293
276
294
def handle_connect (self ):
295
+ """Print connect message to stderr."""
277
296
if not options .quiet :
278
297
print >> sys .stderr , "client %s:%d connected" % self .addr
279
298
280
299
def handle_close (self ):
300
+ """Print disconnect message to stderr."""
281
301
self .close ()
282
302
if not options .quiet :
283
303
print >> sys .stderr , "client %s:%d disconnected" % self .addr
284
304
285
305
286
306
class ServerChannel (ThrottleSender ):
307
+ """Connection to HTTP server."""
287
308
288
309
def __init__ (self , client , header ):
289
310
ThrottleSender .__init__ (self , options .upload )
@@ -295,6 +316,7 @@ def __init__(self, client, header):
295
316
self .header = Header ()
296
317
297
318
def send_header (self , header ):
319
+ """Send HTTP request header to the server."""
298
320
header .extract_request ()
299
321
self .send_line (' ' .join (
300
322
(header .method , header .path , header .proto )))
@@ -307,9 +329,11 @@ def send_header(self, header):
307
329
self .send_line ('' )
308
330
309
331
def send_line (self , line ):
332
+ """Send one line of the request header to the server."""
310
333
self .buffer .append (line + '\r \n ' )
311
334
312
335
def receive_header (self , header ):
336
+ """Send HTTP reply header to the client."""
313
337
for line in header .lines :
314
338
if not (line .startswith ('Keep-Alive: ' ) or
315
339
line .startswith ('Connection: ' ) or
@@ -318,12 +342,15 @@ def receive_header(self, header):
318
342
self .receive_line ('' )
319
343
320
344
def receive_line (self , line ):
345
+ """Send one line of the reply header to the client."""
321
346
self .client .buffer .append (line + '\r \n ' )
322
347
323
348
def readable (self ):
349
+ """Check if this channel is ready to receive some data."""
324
350
return len (self .client .buffer ) == 0
325
351
326
352
def handle_read (self ):
353
+ """Read some data from the server."""
327
354
data = self .recv (8192 )
328
355
if not self .header .complete :
329
356
data = self .header .append (data )
@@ -338,26 +365,30 @@ def handle_read(self):
338
365
self .client .buffer .append (data )
339
366
340
367
def handle_connect (self ):
368
+ """Print connect message to stderr."""
341
369
if not options .quiet :
342
370
print >> sys .stderr , "server %s:%d connected" % self .addr
343
371
344
372
def handle_close (self ):
345
- self . client . should_close = True
373
+ """Print disconnect message to stderr."""
346
374
self .close ()
347
375
if not options .quiet :
348
376
print >> sys .stderr , "server %s:%d disconnected" % self .addr
349
377
350
378
351
379
class ProxyServer (asyncore .dispatcher ):
380
+ """Listen for client connections."""
352
381
353
382
def __init__ (self ):
354
383
asyncore .dispatcher .__init__ (self )
355
384
self .create_socket (socket .AF_INET , socket .SOCK_STREAM )
356
- self .bind ((options .interface , options .port ))
385
+ self .addr = (options .interface , options .port )
386
+ self .bind (self .addr )
357
387
self .listen (5 )
358
- print >> sys .stderr , "listening on port" , options . port
388
+ print >> sys .stderr , "listening on %s:%d" % self . addr
359
389
360
390
def handle_accept (self ):
391
+ """Accept a new connection from a client."""
361
392
channel , addr = self .accept ()
362
393
if addr [0 ] == '127.0.0.1' or options .allow_remote :
363
394
ClientChannel (channel , addr )
0 commit comments