Small fix in Header netloc
[sproxy.git] / sproxy
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import sys
5 import os
6 import logging
7 from time import ctime
8 import socket
9 import threading
10 import subprocess
11 from gzip import GzipFile
12 from StringIO import StringIO
13 from OpenSSL import SSL
14 import urlparse
15 import base64
16
17
18 IPV4_IN_IPV6_PREFIX = '::ffff:'
19
20 def format_ip_port(ip, port, *spam):
21     "Build a nice printable string for a given sockaddr"
22
23     if ip.startswith(IPV4_IN_IPV6_PREFIX):
24         ip = ip[len(IPV4_IN_IPV6_PREFIX):]
25     if ip == '':
26         ip = '*'
27     if ':' in ip:
28         return '[%s]:%s' % (ip, port)
29     else:
30         return '%s:%s' % (ip, port)
31
32 class HttpBase:
33     "Base class for both requests & responses"
34
35     # Derived class can override that value:
36     content_length_default = 0
37
38     def __init__(self):
39         self.line1 = None
40         self.headers = []
41         self.data = ''
42         self.headers_complete = False
43     
44     def set_line1(self, line1):
45         self.line1 = line1
46     
47     def get_line1(self, *args, **kargs):
48         return self.line1
49     
50     def add_header_line(self, line):
51         # RFC 2616 section 2.2:
52         #  Field names are case insensitive
53         # RFC 2616 section 4.2:
54         #  Multiple message-header fields with the same field-name MAY be
55         #  present in a message if and only if the entire field-value for that
56         #  header field is defined as a comma-separated list [i.e., #(values)].
57         #  It MUST be possible to combine the multiple header fields into one
58         #  "field-name: field-value" pair, without changing the semantics of the
59         #  message, by appending each subsequent field-value to the first, each
60         #  separated by a comma. 
61         # A proxy MUST NOT change the order of fields.
62         sp = line.split(':', 1)
63         #logging.debug(repr(sp))
64         if len(sp)==2:
65             self.headers.append((sp[0].strip(), sp[1].strip(' \t')))
66         else:
67             # FIXME headers can be on multiple lines
68             # See RFC 2616 section 2.2:
69             # HTTP/1.1 header field values can be folded onto multiple lines if
70             # the continuation line begins with a space or horizontal tab.
71             self.headers.append((line,))
72
73     def get_header_value(self, key):
74         for header in self.headers:
75             if header[0] == key:
76                 return header[1]
77         return None
78     
79     def all_headers(self, *args, **kargs):
80         result = ''
81         line1 = self.get_line1(*args, **kargs)
82         if line1 is not None:
83             result += line1+'\r\n'
84         for header in self.headers:
85             result += ': '.join(header)+'\r\n'
86         result += '\r\n'
87         return result
88
89     def is_data_complete(self):
90         if not self.headers_complete:
91             return False
92         if self.get_header_value('Transfer-Encoding')=='chunked':
93             if not hasattr(self, 'chunk_size'):
94                 self.chunk_size = 0
95                 self.chunk_data = ''
96             while True:
97                 if self.chunk_size == 0:
98                     hex_chunk_size = self.pop_data_line()
99                     if options.debug_length:
100                         logging.debug('hex chunk_size=%s', hex_chunk_size) # TODO extensions
101                     if hex_chunk_size is None:
102                         return False # need more data
103                     self.chunk_size = int(hex_chunk_size, 16) # CRLF
104                     if options.debug_length:
105                         logging.debug('chunk_size=%s', self.chunk_size)
106                     if self.chunk_size == 0:
107                         logging.warning('chunk-transfer trailer? :%s', repr(self.data))
108                         self.data = self.chunk_data # TODO trailers
109                         # remove any Transfert-Encoding: chunked
110                         # update Content-Length
111                         content_length_updated = False
112                         i = 0
113                         while i < len(self.headers):
114                             key = self.headers[i][0].lower()
115                             if key == 'transfer-encoding':
116                                 del self.headers[i]
117                             else:
118                                 if key == 'content-length':
119                                     self.headers[i] = ('Content-Length', str(len(self.data)))
120                                     content_length_updated = True
121                                 i += 1
122                         if not content_length_updated:
123                             self.headers.append(('Content-Length', str(len(self.data))))
124                         #self.headers_complete = False
125                         break # we're done with chunking
126                     else:
127                         self.chunk_size += 2
128                 l = len(self.data)
129                 if self.chunk_size <= l:
130                     l = self.chunk_size
131                     need_more_data = False
132                 else:
133                     need_more_data = True
134                 self.chunk_data += self.data[:l]
135                 self.data = self.data[l:]
136                 self.chunk_size -= l
137                 if need_more_data:
138                     return False
139                 else:
140                     self.chunk_data = self.chunk_data[:-2] # CRLF
141
142         l = self.get_header_value('Content-Length')
143         if l is not None:
144             l = int(l) # TODO execpt
145         else:
146             l = self.content_length_default
147         if options.debug_length:
148             logging.debug('Expected length=%s', l)
149             logging.debug('Current length=%s', len(self.data))
150         return len(self.data) >= l
151
152     def pop_data_line(self):
153         """
154         Extract a line separated by CRLF from the data buffer
155         Returns None if there is no CRLF left
156         """
157         p = self.data.find('\r\n')
158         if p == -1:
159             return None
160         line = self.data[:p]
161         self.data = self.data[p+2:]
162         return line
163
164     def recv_from(self, sock):
165         self.data = '' # unparsed data
166         while True:
167             try:
168                 new_raw_data = sock.recv(1500) # usual IP MTU, for speed
169             except SSL.Error, err:
170                 logging.debug('SSL.Error during sock.recv: %s', repr(err))
171                 return # connection failure
172             if not new_raw_data:
173                 return # connection was closed
174             self.data += new_raw_data
175             while not self.headers_complete:
176                 line = self.pop_data_line()
177                 if line is None:
178                     break # no more token, continue recv
179                 if line == '':
180                     if self.line1 is not None:
181                         self.headers_complete = True
182                     # else
183                     # See RFC 2616 section 4.1:
184                     # If the server is reading the protocol stream at the beginning of a
185                     #  message and receives a CRLF first, it should ignore the CRLF
186                 elif self.line1 is None:
187                     self.set_line1(line)
188                 else:
189                     self.add_header_line(line)
190             if self.is_data_complete():
191                 return
192     
193     def send_to(self, sock, abs_path=False):
194         """
195         Sends that http information to an opened socked
196         If abs_path is true, it will remove the scheme/hostname part
197         Otherwise, it will produce a full absolute url
198         """
199         try:
200             sock.sendall(self.all_headers(abs_path=abs_path))
201             if self.data != '':
202                 sock.sendall(self.data)
203         except (socket.error, SSL.SysCallError), err:
204             logging.error('Error during sock.send: %s', err.args[1])
205             # do nothing
206
207     def debug_dump_line1(self):
208         line1 = self.get_line1()
209         if line1 is not None:
210             logging.debug(self.get_line1())
211
212     def debug_dump_headers(self):
213         for header in self.headers:
214             if len(header)==2:
215                 logging.debug('%s: %s', repr(header[0]), repr(header[1]))
216             else:
217                 logging.debug('%s (NO VALUE)', repr(header[0]))
218     def debug_dump_data(self):
219         if self.data:
220             data_length = len(self.data)
221             truncate = data_length > options.dump_length
222             if truncate:
223                 printed_data = repr(self.data[:options.dump_length])+'...'
224             else:
225                 printed_data = repr(self.data)
226
227             logging.debug('data: (%s bytes) %s', data_length, printed_data)
228
229     def debug_dump(self, title='DUMP'):
230         if options.log_full_transfers:
231             l = len(title)
232             logging.debug(title+' '+('-'*(80-l-1)))
233             self.debug_dump_line1()
234             self.debug_dump_headers()
235             self.debug_dump_data()
236             logging.debug('-'*80)
237         else:
238             self.debug_dump_line1()
239     
240     def clean_hop_headers(self):
241         #remove any Proxy-* header, and hop by hop headers
242         i = 0
243         while i < len(self.headers):
244             key = self.headers[i][0].lower()
245             if key.startswith('proxy-') or key in ('connection', 'keep-alive', 'te', 'trailers', 'transfer-encoding', 'upgrade'):
246                 del self.headers[i]
247             else:
248                 i += 1
249     
250
251 class HttpRequest(HttpBase):
252     # default is no data for requests
253     content_length_default = 0
254
255     def __init__(self):
256         HttpBase.__init__(self)
257         self.http_method = ''
258         self.http_version = 'HTTP/1.1'
259         self.parsed_url = None
260
261     def recv_from(self, sock):
262         HttpBase.recv_from(self, sock)
263         if options.debug_raw_messages:
264             self.debug_dump('REQUEST RAW')
265     
266     def send_to(self, sock, *args, **kargs):
267         HttpBase.send_to(self, sock, *args, **kargs)
268         if options.debug_raw_messages:
269             self.debug_dump('REQUEST PATCHED')
270
271     
272     def set_line1(self, line1):
273         self.line1 = line1
274         splits = line1.split(' ')
275         if len(splits) == 3:
276             self.http_method, url, self.http_version = splits
277             self.parsed_url = urlparse.urlparse(url)
278         else:
279             logging.error("Can't parse http request line %s", line1)
280
281     def get_line1(self, abs_path=False, *args, **kargs):
282         if self.http_method:
283             if abs_path:
284                 url = urlparse.urlunparse(['', ''] + list(self.parsed_url[2:]))
285             else:
286                 url = self.parsed_url.geturl()
287             return self.http_method + ' ' + url + ' ' + self.http_version
288         return self.line1
289
290     def clean_host_request(self):
291         def split_it(host_port):
292             # 'www.google.com:80' -> 'www.google.com', '80'
293             # 'www.google.com' -> 'www.google.com', ''
294             sp = host_port.split(':', 1)
295             if len(sp) == 2:
296                 return sp
297             else:
298                 return sp[0], None
299         def join_it(host, port):
300             result = host
301             if port:
302                 result += ':' + str(port)
303             return result
304
305         if self.parsed_url.scheme and not self.parsed_url.netloc:
306             logging.debug('emptying scheme for netloc')
307             self.parsed_url = urlparse.ParseResult('', self.parsed_url.scheme, *self.parsed_url[2:])
308
309         request_hostname = self.parsed_url.hostname
310         request_port = self.parsed_url.port
311         request_netloc = join_it(request_hostname, request_port)
312         header_necloc = self.get_header_value('Host')
313         if header_necloc:
314             header_hostname, header_port = split_it(header_necloc)
315         else:
316             header_hostname, header_port = None, None
317
318         if not request_hostname and header_hostname:
319             # copy "Host" header into request netloc
320             self.parsed_url = urlparse.ParseResult(self.parsed_url.scheme, header_hostname, *self.parsed_url[2:])
321         elif request_hostname:
322             if request_netloc != header_necloc and header_necloc:
323                 # RFC 2616, section 5.2: Host header must be ignored FIXME
324                 logging.warning('Ignoring necloc value %s in request. Header "Host" value is %s', request_netloc, header_necloc)
325                 for i in range(len(self.headers)):
326                     if self.headers[i][0].lower()=='host':
327                         self.headers[i] = ('Host', request_netloc)
328                 # Patch header here
329             elif not header_necloc:
330                 self.headers.append(('Host', request_netloc))
331
332     def set_default_scheme(self, scheme):
333         if not self.parsed_url.scheme:
334             self.parsed_url = urlparse.ParseResult(scheme, *self.parsed_url[1:])
335
336     def check_headers_valid(self):
337         if not self.http_method:
338             raise HttpErrorResponse(400, 'Bad Request', 'Http method is required')
339         if not self.parsed_url:
340             raise HttpErrorResponse(400, 'Bad Request', 'Http url is required')
341         if not self.http_version:
342             raise HttpErrorResponse(400, 'Bad Request', 'Http version is required')
343
344 class HttpResponse(HttpBase):
345     # for responses, default is data until connection closed :
346     content_length_default = sys.maxint
347     
348     def recv_from(self, sock):
349         HttpBase.recv_from(self, sock)
350         if options.debug_raw_messages:
351             self.debug_dump('RESPONSE RAW')
352     
353     def send_to(self, sock, *args, **kargs):
354         HttpBase.send_to(self, sock, *args, **kargs)
355         if options.debug_raw_messages:
356             self.debug_dump('RESPONSE PATCHED')
357
358
359     def decompress_data(self):
360         compression_scheme = self.get_header_value('Content-Encoding')
361         if compression_scheme == 'gzip':
362             gzf = GzipFile(fileobj=StringIO(self.data))
363             try:
364                 plain_data = gzf.read()
365             except IOError, err:
366                 logging.error('Error while decompressing gzip data: %s', err)
367                 self.debug_dump('RESPONSE BEFORE DECOMPRESSION')
368                 #raise
369             else:
370                 # remove any Content-Encoding header
371                 # update Content-Length
372                 i = 0
373                 while i < len(self.headers):
374                     key = self.headers[i][0].lower()
375                     if key == 'content-encoding':
376                         del self.headers[i]
377                     else:
378                         if key == 'content-length':
379                             self.headers[i] = ('Content-Length', str(len(plain_data)))
380                         i += 1
381                 self.data = plain_data
382
383
384 class HttpErrorResponse(HttpResponse):
385     def __init__(self, errcode, errtitle, errmsg=None):
386         HttpResponse.__init__(self)
387         self.set_line1('HTTP/1.1 %s %s' % (errcode, errtitle))
388         self.add_header_line('Server: Spy proxy')
389         self.add_header_line('Date: %s' % ctime())
390         if errmsg is None:
391             self.data = errtitle
392         else:
393             self.data = errmsg
394         self.headers.append(('Content-Length', str(len(self.data))))
395
396
397 def get_connected_sock(hostname, port):
398     try:
399         port = int(port)
400     except ValueError:
401         HttpErrorResponse(500, 'Internal error', "Can't connect to port %s" % port)
402
403     try:
404         addrinfo = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
405     except socket.gaierror, err:
406         if err.args[1] == -2: # Name or service not known
407             raise
408
409         logging.debug('Connection to %s failed: %s', format_ip_port(hostname, port), err.args[1])
410         raise HttpErrorResponse(404, 'Unknown host', 'Can\'resolve %s.' % hostname)
411
412     for family, socktype, proto, canonname, sockaddr in addrinfo:
413         if options.debug_connections:
414             logging.debug('Connecting to %s ...', format_ip_port(*sockaddr))
415         sock = socket.socket(family, socktype, proto)
416         try:
417             sock.connect(sockaddr)
418         except socket.gaierror, err:
419             if err.args[0] != -2:
420                 raise
421             if options.debug_connections:
422                 logging.debug('Connection to %s failed: %s', format_ip_port(*sockaddr), err.args[1])
423             sock = None
424         except socket.error, err:
425             if err.args[0] not in (111, 113): # Connection refused, No route to host
426                 raise
427             if options.debug_connections:
428                 logging.debug('Connection to %s failed: %s', format_ip_port(*sockaddr), err.args[1])
429             sock = None
430         else:
431             # Connection successfull
432             if options.debug_connections:
433                 logging.debug('Connected to %s', format_ip_port(*sockaddr))
434             break
435     
436     if sock is None:
437         raise HttpErrorResponse(404, 'Not found', 'Can\'t connect to %s.' % hostname)
438     return sock
439
440
441 def run_request_http(request):
442     sock = get_connected_sock(request.parsed_url.hostname, request.parsed_url.port or 80)
443     request.send_to(sock, abs_path=True)
444     response = HttpResponse()
445     response.recv_from(sock)
446     sock.shutdown(socket.SHUT_RDWR)
447     sock.close()
448     response.decompress_data()
449     return response
450
451
452 def run_request_https(request):
453     sock = get_connected_sock(request.parsed_url.hostname, request.parsed_url.port or 443)
454     ssl_context = SSL.Context(SSL.SSLv23_METHOD)
455     ssl_sock = SSL.Connection(ssl_context, sock)
456     ssl_sock.set_connect_state()
457     request.send_to(ssl_sock, abs_path=True)
458     response = HttpResponse()
459     response.recv_from(ssl_sock)
460     ssl_sock.shutdown()
461     ssl_sock.close()
462     response.decompress_data()
463     return response
464
465 def make_https_sslcontext(hostname):
466     # To generate a certificate:
467     # openssl req -nodes -new -x509 -keyout certs/ca.key -out certs/ca.crt -days 10000 -subj "/O=Spy Proxy/CN=*" -newkey rsa:2048
468     #
469     # openssl req -nodes -new -subj "/CN=*.nirgal.com" -days 10000 -keyout certs/nirgal.com.key -out certs/nirgal.com.csr
470     # openssl x509 -req -in certs/nirgal.com.csr -out certs/nirgal.com.crt -CA certs/ca.crt -CAkey certs/ca.key [-CAcreateserial]
471     # openssl req -batch -nodes -new -subj "/CN=*.nirgal.com" -days 10000 -keyout certs/nirgal.com.key | openssl x509 -req -out certs/nirgal.com.crt -CA certs/ca.crt -CAkey certs/ca.key 
472
473     keyfile = os.path.join('certs', hostname+'.key')
474     crtfile = os.path.join('certs', hostname+'.crt')
475     csrfile = os.path.join('certs', hostname+'.csr')
476
477     ssl_context = SSL.Context(SSL.SSLv23_METHOD)
478     if not os.path.exists(keyfile) or not os.path.exists(crtfile):
479         logging.debug('Generating custom SSL certificates for %s', hostname)
480         if not os.path.exists('certs/ca.srl'):
481             extra_args  = [ '-CAcreateserial' ]
482         else:
483             extra_args = [ ]
484         subprocess.call(['openssl', 'req', '-nodes', '-new', '-subj', '/CN='+hostname, '-days', '10000', '-keyout', keyfile, '-out', csrfile])
485         subprocess.call(['openssl', 'x509', '-req', '-in', csrfile, '-out', crtfile, '-CA', 'certs/ca.crt', '-CAkey', 'certs/ca.key'] + extra_args )
486     ssl_context.use_privatekey_file (keyfile)
487     ssl_context.use_certificate_file(crtfile)
488     return ssl_context
489
490 class ProxyConnectionIn(threading.Thread):
491     def __init__(self, clientsocket):
492         threading.Thread.__init__(self)
493         self.clientsocket = clientsocket
494
495     def check_proxy_auth(self, request_in):
496         # RFC2617
497         if not options.auth:
498             return
499         proxy_auth = request_in.get_header_value('Proxy-Authorization')
500         if proxy_auth is not None and proxy_auth.startswith('Basic '):
501             proxy_auth = proxy_auth[len('Basic '):]
502             # logging.debug('proxy_auth raw: %s', proxy_auth)
503             proxy_auth = base64.b64decode(proxy_auth)
504             #logging.debug('proxy_auth: %s', proxy_auth)
505         if proxy_auth != options.auth:
506             response = HttpErrorResponse('407', 
507                 'Proxy Authentication Required', 
508                 'Proxy requires an authorization.')
509             response.add_header_line('Proxy-Authenticate: Basic realm="Spy proxy"')
510             raise response
511
512     def run(self):
513         request_in = HttpRequest()
514         request_in.recv_from(self.clientsocket)
515         response = None
516
517         try:
518             self.check_proxy_auth(request_in) # raises 407
519             request_in.check_headers_valid() # raises 400
520
521             if request_in.http_method in ('OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE'):
522                 if request_in.parsed_url and request_in.parsed_url.scheme == 'http':
523                     
524                     request_in.clean_host_request()
525                     request_in.clean_hop_headers()
526                     request_in.headers.append(('Connection', 'close'))
527
528                     request_in.debug_dump('REQUEST')
529
530                     response = run_request_http(request_in)
531                     response.clean_hop_headers()
532                     response.headers.append(('Connection', 'close'))
533
534                     response.debug_dump('RESPONSE')
535
536                 else:
537                     raise HttpErrorResponse(501, 'Not implemented', 'Unsupported scheme %s.' % request_in.parsed_url.scheme)
538
539                 logging.info("%s %s %s %s %s", request_in.http_method, request_in.parsed_url.geturl(), request_in.http_version, response.line1[9:12], len(response.data) or '-')
540
541             elif request_in.http_method == 'CONNECT':
542                 # TODO: tunnel mode
543                 request_in.clean_host_request()
544                 HttpErrorResponse(200, 'Proceed', '').send_to(self.clientsocket)
545
546                 ssl_context = make_https_sslcontext(request_in.parsed_url.hostname)
547                 ssl_sock = SSL.Connection(ssl_context, self.clientsocket)
548                 ssl_sock.set_accept_state()
549
550                 request_in_ssl = HttpRequest()
551                 request_in_ssl.recv_from(ssl_sock)
552
553                 request_in_ssl.check_headers_valid() # raises 400
554
555                 request_in_ssl.clean_hop_headers()
556                 request_in_ssl.headers.append(('Connection', 'close'))
557
558                 request_in_ssl.clean_host_request()
559                 request_in_ssl.set_default_scheme('https')
560
561                 response = run_request_https(request_in_ssl)
562                 response.clean_hop_headers()
563                 response.headers.append(('Connection', 'close'))
564
565                 response.send_to(ssl_sock)
566                 
567                 logging.info("%s %s %s %s %s", request_in_ssl.http_method, request_in_ssl.parsed_url.geturl(), request_in_ssl.http_version, response.line1[9:12], len(response.data) or '-')
568                 ssl_sock.shutdown()
569                 ssl_sock.close()
570                 #self.clientsocket.shutdown(socket.SHUT_RDWR)
571                 #self.clientsocket.close()
572                 return # bypass classic socket shutdown
573             else:
574                 request_in.debug_dump('REQUEST')
575                 logging.error('Method %s not supported', request_in.http_method)
576                 # FIXME RFC 2616, section 14.7: We should return an "Allow" header
577                 self.clientsocket.send('HTTP/1.1 405 Method not allowed\r\n\r\nSorry method %s is not supported by the proxy.\r\n' % request_in.http_method)
578                 self.clientsocket.close()
579
580         except HttpErrorResponse, error:
581             response = error
582
583         response.send_to(self.clientsocket)
584         try:
585             self.clientsocket.shutdown(socket.SHUT_RDWR)
586         except socket.error, err:
587             logging.error('Error during socket.shutdown: %s', err.args[1])
588         self.clientsocket.close()
589
590
591 def main():
592     if options.debug:
593         loglevel = logging.DEBUG
594     else:
595         loglevel = logging.INFO
596     logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
597
598     if options.listen_host:
599         addrinfo = socket.getaddrinfo(options.listen_host, options.listen_port, socket.AF_UNSPEC, socket.SOCK_STREAM)
600         family, socktype, proto, canonname, sockaddr = addrinfo[0]
601     else:
602         if socket.has_ipv6:
603             family = socket.AF_INET6
604         else:
605             family = socket.AF_INET
606         socktype = socket.SOCK_STREAM
607         try:
608             options.listen_port = int(options.listen_port)
609         except ValueError:
610             options.listen_port = socket.getservbyname(options.listen_port)
611         sockaddr = (options.listen_host, options.listen_port)
612
613     # TODO: multicast require more stuff
614     # see http://code.activestate.com/recipes/442490/
615
616     serversocket = socket.socket(family, socktype) #, proto) # TODO
617     serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
618     logging.info('Listening on %s', format_ip_port(*sockaddr))
619     serversocket.bind(sockaddr)
620     serversocket.listen(30)
621
622     while True:
623         try:
624             clientsocket, address = serversocket.accept()
625         except KeyboardInterrupt:
626             logging.info('Ctrl+C received. Shutting down.')
627             break
628
629         logging.debug('thread count: %s', threading.activeCount())
630         if options.debug_connections:
631             logging.debug('Connection from %s', format_ip_port(*address))
632         cnx_thread = ProxyConnectionIn(clientsocket)
633         cnx_thread.start()
634
635
636 if __name__ == '__main__':
637     from optparse import OptionParser #, OptionGroup
638     
639     parser = OptionParser(usage='%prog [options]')
640     
641     parser.add_option('-b', '--bind',
642         action='store', type='str', dest='listen_host', default='',
643         metavar='HOST',
644         help="listen address, default='%default'")
645
646     parser.add_option('-p', '--port',
647         action='store', type='str', dest='listen_port', default='3128',
648         metavar='PORT',
649         help="listen port, default=%default")
650
651     parser.add_option('--auth',
652         action='store', type='str', dest='auth', default='',
653         metavar='LOGIN:PASSWORD',
654         help="proxy authentification, default='%default'")
655
656     parser.add_option('--log-full-transfers',
657         action='store_true', dest='log_full_transfers', default=False,
658         help="log full requests and responses")
659     
660     parser.add_option('--dump-length',
661         action='store', type='int', dest='dump_length', default=160,
662         help="length of data dump")
663     
664     parser.add_option('-d', '--debug',
665         action='store_true', dest='debug', default=False,
666         help="debug mode")
667     
668     parser.add_option('--debug-raw-messages',
669         action='store_true', dest='debug_raw_messages', default=False,
670         help="dump raw messages before they are patched")
671    
672     parser.add_option('--debug-connections',
673         action='store_true', dest='debug_connections', default=False,
674         help="dump connections information")
675     
676     parser.add_option('--debug-length',
677         action='store_true', dest='debug_length', default=False,
678         help="dump lengthes information")
679     
680
681     options, args = parser.parse_args()
682     main(*args)
683