Skip to content

Commit 69fac2b

Browse files
committed
Docstrings.
1 parent 1b2591a commit 69fac2b

File tree

1 file changed

+50
-19
lines changed

1 file changed

+50
-19
lines changed

throxy.py

+50-19
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@
2323
# SOFTWARE.
2424

2525
"""
26-
Throxy (Throttling Proxy)
26+
Throxy (Throttling Proxy), a simple HTTP proxy.
2727
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.
2930
3031
* Simulates a slow connection (like dial-up).
32+
* Adjustable bandwidth limit for download and upload.
3133
* Optionally dumps HTTP headers and content for debugging.
34+
* Supports gzip decompression for debugging.
3235
* 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.
3737
"""
3838

3939
import sys
@@ -53,18 +53,25 @@
5353

5454

5555
class Header:
56+
"""HTTP (request or reply) header parser."""
5657

5758
def __init__(self):
5859
self.data = ''
5960
self.lines = []
6061
self.complete = False
6162

6263
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+
"""
6370
self.data += new_data
6471
while not self.complete:
6572
newline = self.data.find('\n')
6673
if newline < 0:
67-
break # No complete line found.
74+
break # No complete line found
6875
line = self.data[:newline].rstrip('\r')
6976
if len(line):
7077
self.lines.append(line)
@@ -76,15 +83,14 @@ def append(self, new_data):
7683
self.gzip_data = cStringIO.StringIO()
7784
self.data = self.data[newline+1:]
7885
if self.complete:
79-
# Give remaining data back to caller. It may contain
80-
# content, or even the start of the next request.
8186
rest = self.data
8287
self.data = ''
8388
return rest
8489
else:
8590
return ''
8691

8792
def extract(self, name, default=''):
93+
"""Extract a header field."""
8894
name = name.lower()
8995
for line in self.lines:
9096
if not line.count(':'):
@@ -95,10 +101,10 @@ def extract(self, name, default=''):
95101
return default
96102

97103
def extract_host(self):
104+
"""Extract host and perform DNS lookup."""
98105
self.host = self.extract('Host')
99106
if self.host is None:
100107
return
101-
# print "client %s:%d wants to talk to" % self.client.addr, self.host
102108
if self.host.count(':'):
103109
self.host_name, self.host_port = self.host.split(':')
104110
else:
@@ -108,6 +114,7 @@ def extract_host(self):
108114
self.host_addr = (self.host_ip, self.host_port)
109115

110116
def extract_request(self):
117+
"""Extract path from HTTP request."""
111118
match = request_match(self.lines[0])
112119
if not match:
113120
raise ValueError("malformed request line " + self.lines[0])
@@ -120,17 +127,20 @@ def extract_request(self):
120127
self.path = self.url[len(prefix):]
121128

122129
def dump_title(self, from_addr, to_addr, direction, what):
130+
"""Print a title before dumping headers or content."""
123131
print '==== %s %s (%s:%d => %s:%d) ====' % (
124132
direction, what,
125133
from_addr[0], from_addr[1],
126134
to_addr[0], to_addr[1])
127135

128136
def dump(self, from_addr, to_addr, direction='sending'):
137+
"""Dump header lines to stdout."""
129138
self.dump_title(from_addr, to_addr, direction, 'headers')
130139
print '\n'.join(self.lines)
131140
print
132141

133142
def dump_content(self, content, from_addr, to_addr, direction='sending'):
143+
"""Dump content to stdout."""
134144
self.dump_title(from_addr, to_addr, direction, 'content')
135145
if self.content_encoding:
136146
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'):
155165
if len(content) < limit or limit == 0:
156166
print content
157167
else:
158-
print content[:limit] + '(truncated after %d bytes)' % limit
168+
print content[:limit] + '(showing only %d bytes)' % limit
159169
print
160170

161171
def gunzip(self):
172+
"""Decompress gzip content."""
162173
if self.gzip_data.tell() > options.gzip_size_limit:
163174
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
165176
try:
166177
gzip_file = gzip.GzipFile(
167178
fileobj=self.gzip_data, mode='rb')
168179
result = gzip_file.read()
169180
gzip_file.close()
170181
except struct.error:
171182
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
173184
return result
174185

175186

176187
class ThrottleSender(asyncore.dispatcher):
188+
"""Data connection with send buffer and bandwidth limit."""
177189

178190
def __init__(self, kbps, channel=None):
179191
if channel is None:
@@ -187,9 +199,11 @@ def __init__(self, kbps, channel=None):
187199
self.buffer = []
188200

189201
def log_sent_bytes(self, bytes):
202+
"""Add timestamp and byte count to transmit log."""
190203
self.transmit_log.append((time.time(), bytes))
191204

192205
def trim_log(self, horizon):
206+
"""Forget transmit log entries that are too old."""
193207
while len(self.transmit_log) and self.transmit_log[0][0] <= horizon:
194208
self.transmit_log.pop(0)
195209

@@ -201,11 +215,11 @@ def weighted_bytes(self):
201215
return 0
202216
weighted = 0.0
203217
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
205219
assert 0 <= age <= self.interval
206220
weight = 2.0 * (self.interval - age) / self.interval
207221
assert 0.0 <= weight <= 2.0
208-
weighted += weight * bytes # Newer entries count more.
222+
weighted += weight * bytes # Newer entries count more
209223
return int(weighted / self.interval)
210224

211225
def weighted_kbps(self):
@@ -217,14 +231,15 @@ def sendable(self):
217231
return max(0, self.bytes_per_second - self.weighted_bytes())
218232

219233
def writable(self):
234+
"""Check if this channel is ready to write some data."""
220235
return (len(self.buffer) and
221236
self.sendable() / 2 > self.fragment_size)
222237

223238
def handle_write(self):
239+
"""Write some data to the socket."""
224240
max_bytes = self.sendable() / 2
225241
if max_bytes < self.fragment_size:
226242
return
227-
# print "sendable", max_bytes
228243
bytes = self.send(self.buffer[0][:max_bytes])
229244
self.log_sent_bytes(bytes)
230245
if bytes == len(self.buffer[0]):
@@ -234,6 +249,7 @@ def handle_write(self):
234249

235250

236251
class ClientChannel(ThrottleSender):
252+
"""A client connection."""
237253

238254
def __init__(self, channel, addr):
239255
ThrottleSender.__init__(self, options.download, channel)
@@ -244,9 +260,11 @@ def __init__(self, channel, addr):
244260
self.handle_connect()
245261

246262
def readable(self):
263+
"""Check if this channel is ready to receive some data."""
247264
return self.server is None or len(self.server.buffer) == 0
248265

249266
def handle_read(self):
267+
"""Read some data from the client."""
250268
data = self.recv(8192)
251269
while len(data):
252270
if self.content_length:
@@ -274,16 +292,19 @@ def handle_read(self):
274292
self.server = ServerChannel(self, self.header)
275293

276294
def handle_connect(self):
295+
"""Print connect message to stderr."""
277296
if not options.quiet:
278297
print >> sys.stderr, "client %s:%d connected" % self.addr
279298

280299
def handle_close(self):
300+
"""Print disconnect message to stderr."""
281301
self.close()
282302
if not options.quiet:
283303
print >> sys.stderr, "client %s:%d disconnected" % self.addr
284304

285305

286306
class ServerChannel(ThrottleSender):
307+
"""Connection to HTTP server."""
287308

288309
def __init__(self, client, header):
289310
ThrottleSender.__init__(self, options.upload)
@@ -295,6 +316,7 @@ def __init__(self, client, header):
295316
self.header = Header()
296317

297318
def send_header(self, header):
319+
"""Send HTTP request header to the server."""
298320
header.extract_request()
299321
self.send_line(' '.join(
300322
(header.method, header.path, header.proto)))
@@ -307,9 +329,11 @@ def send_header(self, header):
307329
self.send_line('')
308330

309331
def send_line(self, line):
332+
"""Send one line of the request header to the server."""
310333
self.buffer.append(line + '\r\n')
311334

312335
def receive_header(self, header):
336+
"""Send HTTP reply header to the client."""
313337
for line in header.lines:
314338
if not (line.startswith('Keep-Alive: ') or
315339
line.startswith('Connection: ') or
@@ -318,12 +342,15 @@ def receive_header(self, header):
318342
self.receive_line('')
319343

320344
def receive_line(self, line):
345+
"""Send one line of the reply header to the client."""
321346
self.client.buffer.append(line + '\r\n')
322347

323348
def readable(self):
349+
"""Check if this channel is ready to receive some data."""
324350
return len(self.client.buffer) == 0
325351

326352
def handle_read(self):
353+
"""Read some data from the server."""
327354
data = self.recv(8192)
328355
if not self.header.complete:
329356
data = self.header.append(data)
@@ -338,26 +365,30 @@ def handle_read(self):
338365
self.client.buffer.append(data)
339366

340367
def handle_connect(self):
368+
"""Print connect message to stderr."""
341369
if not options.quiet:
342370
print >> sys.stderr, "server %s:%d connected" % self.addr
343371

344372
def handle_close(self):
345-
self.client.should_close = True
373+
"""Print disconnect message to stderr."""
346374
self.close()
347375
if not options.quiet:
348376
print >> sys.stderr, "server %s:%d disconnected" % self.addr
349377

350378

351379
class ProxyServer(asyncore.dispatcher):
380+
"""Listen for client connections."""
352381

353382
def __init__(self):
354383
asyncore.dispatcher.__init__(self)
355384
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)
357387
self.listen(5)
358-
print >> sys.stderr, "listening on port", options.port
388+
print >> sys.stderr, "listening on %s:%d" % self.addr
359389

360390
def handle_accept(self):
391+
"""Accept a new connection from a client."""
361392
channel, addr = self.accept()
362393
if addr[0] == '127.0.0.1' or options.allow_remote:
363394
ClientChannel(channel, addr)

0 commit comments

Comments
 (0)