Facebook
From Abhinav Singh, 4 Years ago, written in Python.
Embed
Download Paste or View Raw
Hits: 192
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4.    proxy.py
  5.    ~~~~~~~~
  6.  
  7.    HTTP Proxy Server in Python.
  8. """
  9. import os
  10. import sys
  11. import errno
  12. import base64
  13. import socket
  14. import select
  15. import logging
  16. import argparse
  17. import datetime
  18. import threading
  19. from collections import namedtuple
  20.  
  21. if os.name != 'nt':
  22.     import resource
  23.  
  24. VERSION = (0, 4)
  25. __version__ = '.'.join(map(str, VERSION[0:2]))
  26. __description__ = 'Lightweight HTTP, HTTPS, WebSockets Proxy Server in a single Python file'
  27. __author__ = 'Abhinav Singh'
  28. __author_email__ = 'twitter.com/imoracle'
  29. __homepage__ = 'https://github.com/abhinavsingh/proxy.py'
  30. __download_url__ = '%s/archive/master.zip' % __homepage__
  31. __license__ = 'BSD'
  32.  
  33. logger = logging.getLogger(__name__)
  34.  
  35. PY3 = sys.version_info[0] == 3
  36.  
  37. if PY3:    # pragma: no cover
  38.     text_type = str
  39.     binary_type = bytes
  40.     from urllib import parse as urlparse
  41. else:   # pragma: no cover
  42.     text_type = unicode
  43.     binary_type = str
  44.     import urlparse
  45.  
  46.  
  47. def text_(s, encoding='utf-8', errors='strict'):    # pragma: no cover
  48.     """Utility to ensure text-like usability.
  49.  
  50.    If ``s`` is an instance of ``binary_type``, return
  51.    ``s.decode(encoding, errors)``, otherwise return ``s``"""
  52.     if isinstance(s, binary_type):
  53.         return s.decode(encoding, errors)
  54.     return s
  55.  
  56.  
  57. def bytes_(s, encoding='utf-8', errors='strict'):   # pragma: no cover
  58.     """Utility to ensure binary-like usability.
  59.  
  60.    If ``s`` is an instance of ``text_type``, return
  61.    ``s.encode(encoding, errors)``, otherwise return ``s``"""
  62.     if isinstance(s, text_type):
  63.         return s.encode(encoding, errors)
  64.     return s
  65.  
  66.  
  67. version = bytes_(__version__)
  68. CRLF, COLON, SP = b'\r\n', b':', b' '
  69. PROXY_AGENT_HEADER = b'Proxy-agent: proxy.py v' + version
  70.  
  71. PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT = CRLF.join([
  72.     b'HTTP/1.1 200 Connection established',
  73.     PROXY_AGENT_HEADER,
  74.     CRLF
  75. ])
  76.  
  77. BAD_GATEWAY_RESPONSE_PKT = CRLF.join([
  78.     b'HTTP/1.1 502 Bad Gateway',
  79.     PROXY_AGENT_HEADER,
  80.     b'Content-Length: 11',
  81.     b'Connection: close',
  82.     CRLF
  83. ]) + b'Bad Gateway'
  84.  
  85. PROXY_AUTHENTICATION_REQUIRED_RESPONSE_PKT = CRLF.join([
  86.     b'HTTP/1.1 407 Proxy Authentication Required',
  87.     PROXY_AGENT_HEADER,
  88.     b'Content-Length: 29',
  89.     b'Connection: close',
  90.     CRLF
  91. ]) + b'Proxy Authentication Required'
  92.  
  93.  
  94. class ChunkParser(object):
  95.     """HTTP chunked encoding response parser."""
  96.  
  97.     states = namedtuple('ChunkParserStates', (
  98.         'WAITING_FOR_SIZE',
  99.         'WAITING_FOR_DATA',
  100.         'COMPLETE'
  101.     ))(1, 2, 3)
  102.  
  103.     def __init__(self):
  104.         self.state = ChunkParser.states.WAITING_FOR_SIZE
  105.         self.body = b''     # Parsed chunks
  106.         self.chunk = b''    # Partial chunk received
  107.         self.size = None    # Expected size of next following chunk
  108.  
  109.     def parse(self, data):
  110.         more = True if len(data) > 0 else False
  111.         while more:
  112.             more, data = self.process(data)
  113.  
  114.     def process(self, data):
  115.         if self.state == ChunkParser.states.WAITING_FOR_SIZE:
  116.             # Consume prior chunk in buffer
  117.             # in case chunk size without CRLF was received
  118.             data = self.chunk + data
  119.             self.chunk = b''
  120.             # Extract following chunk data size
  121.             line, data = HttpParser.split(data)
  122.             if not line:    # CRLF not received
  123.                 self.chunk = data
  124.                 data = b''
  125.             else:
  126.                 self.size = int(line, 16)
  127.                 self.state = ChunkParser.states.WAITING_FOR_DATA
  128.         elif self.state == ChunkParser.states.WAITING_FOR_DATA:
  129.             remaining = self.size - len(self.chunk)
  130.             self.chunk += data[:remaining]
  131.             data = data[remaining:]
  132.             if len(self.chunk) == self.size:
  133.                 data = data[len(CRLF):]
  134.                 self.body += self.chunk
  135.                 if self.size == 0:
  136.                     self.state = ChunkParser.states.COMPLETE
  137.                 else:
  138.                     self.state = ChunkParser.states.WAITING_FOR_SIZE
  139.                 self.chunk = b''
  140.                 self.size = None
  141.         return len(data) > 0, data
  142.  
  143.  
  144. class HttpParser(object):
  145.     """HTTP request/response parser."""
  146.  
  147.     states = namedtuple('HttpParserStates', (
  148.         'INITIALIZED',
  149.         'LINE_RCVD',
  150.         'RCVING_HEADERS',
  151.         'HEADERS_COMPLETE',
  152.         'RCVING_BODY',
  153.         'COMPLETE'))(1, 2, 3, 4, 5, 6)
  154.  
  155.     types = namedtuple('HttpParserTypes', (
  156.         'REQUEST_PARSER',
  157.         'RESPONSE_PARSER'
  158.     ))(1, 2)
  159.  
  160.     def __init__(self, parser_type):
  161.         assert parser_type in (HttpParser.types.REQUEST_PARSER, HttpParser.types.RESPONSE_PARSER)
  162.         self.type = parser_type
  163.         self.state = HttpParser.states.INITIALIZED
  164.  
  165.         self.raw = b''
  166.         self.buffer = b''
  167.  
  168.         self.headers = dict()
  169.         self.body = None
  170.  
  171.         self.method = None
  172.         self.url = None
  173.         self.code = None
  174.         self.reason = None
  175.         self.version = None
  176.  
  177.         self.chunk_parser = None
  178.  
  179.     def is_chunked_encoded_response(self):
  180.         return self.type == HttpParser.types.RESPONSE_PARSER and \
  181.             b'transfer-encoding' in self.headers and \
  182.             self.headers[b'transfer-encoding'][1].lower() == b'chunked'
  183.  
  184.     def parse(self, data):
  185.         self.raw += data
  186.         data = self.buffer + data
  187.         self.buffer = b''
  188.  
  189.         more = True if len(data) > 0 else False
  190.         while more:
  191.             more, data = self.process(data)
  192.         self.buffer = data
  193.  
  194.     def process(self, data):
  195.         if self.state in (HttpParser.states.HEADERS_COMPLETE,
  196.                           HttpParser.states.RCVING_BODY,
  197.                           HttpParser.states.COMPLETE) and \
  198.                 (self.method == b'POST' or self.type == HttpParser.types.RESPONSE_PARSER):
  199.             if not self.body:
  200.                 self.body = b''
  201.  
  202.             if b'content-length' in self.headers:
  203.                 self.state = HttpParser.states.RCVING_BODY
  204.                 self.body += data
  205.                 if len(self.body) >= int(self.headers[b'content-length'][1]):
  206.                     self.state = HttpParser.states.COMPLETE
  207.             elif self.is_chunked_encoded_response():
  208.                 if not self.chunk_parser:
  209.                     self.chunk_parser = ChunkParser()
  210.                 self.chunk_parser.parse(data)
  211.                 if self.chunk_parser.state == ChunkParser.states.COMPLETE:
  212.                     self.body = self.chunk_parser.body
  213.                     self.state = HttpParser.states.COMPLETE
  214.  
  215.             return False, b''
  216.  
  217.         line, data = HttpParser.split(data)
  218.         if line is False:
  219.             return line, data
  220.  
  221.         if self.state == HttpParser.states.INITIALIZED:
  222.             self.process_line(line)
  223.         elif self.state in (HttpParser.states.LINE_RCVD, HttpParser.states.RCVING_HEADERS):
  224.             self.process_header(line)
  225.  
  226.         # When connect request is received without a following host header
  227.         # See `TestHttpParser.test_connect_request_without_host_header_request_parse` for details
  228.         if self.state == HttpParser.states.LINE_RCVD and \
  229.                 self.type == HttpParser.types.REQUEST_PARSER and \
  230.                 self.method == b'CONNECT' and \
  231.                 data == CRLF:
  232.             self.state = HttpParser.states.COMPLETE
  233.  
  234.         # When raw request has ended with \r\n\r\n and no more http headers are expected
  235.         # See `TestHttpParser.test_request_parse_without_content_length` and
  236.         # `TestHttpParser.test_response_parse_without_content_length` for details
  237.         elif self.state == HttpParser.states.HEADERS_COMPLETE and \
  238.                 self.type == HttpParser.types.REQUEST_PARSER and \
  239.                 self.method != b'POST' and \
  240.                 self.raw.endswith(CRLF * 2):
  241.             self.state = HttpParser.states.COMPLETE
  242.         elif self.state == HttpParser.states.HEADERS_COMPLETE and \
  243.                 self.type == HttpParser.types.REQUEST_PARSER and \
  244.                 self.method == b'POST' and \
  245.                 (b'content-length' not in self.headers or
  246.                  (b'content-length' in self.headers and
  247.                   int(self.headers[b'content-length'][1]) == 0)) and \
  248.                 self.raw.endswith(CRLF * 2):
  249.             self.state = HttpParser.states.COMPLETE
  250.  
  251.         return len(data) > 0, data
  252.  
  253.     def process_line(self, data):
  254.         line = data.split(SP)
  255.         if self.type == HttpParser.types.REQUEST_PARSER:
  256.             self.method = line[0].upper()
  257.             self.url = urlparse.urlsplit(line[1])
  258.             self.version = line[2]
  259.         else:
  260.             self.version = line[0]
  261.             self.code = line[1]
  262.             self.reason = b' '.join(line[2:])
  263.         self.state = HttpParser.states.LINE_RCVD
  264.  
  265.     def process_header(self, data):
  266.         if len(data) == 0:
  267.             if self.state == HttpParser.states.RCVING_HEADERS:
  268.                 self.state = HttpParser.states.HEADERS_COMPLETE
  269.             elif self.state == HttpParser.states.LINE_RCVD:
  270.                 self.state = HttpParser.states.RCVING_HEADERS
  271.         else:
  272.             self.state = HttpParser.states.RCVING_HEADERS
  273.             parts = data.split(COLON)
  274.             key = parts[0].strip()
  275.             value = COLON.join(parts[1:]).strip()
  276.             self.headers[key.lower()] = (key, value)
  277.  
  278.     def build_url(self):
  279.         if not self.url:
  280.             return b'/None'
  281.  
  282.         url = self.url.path
  283.         if url == b'':
  284.             url = b'/'
  285.         if not self.url.query == b'':
  286.             url += b'?' + self.url.query
  287.         if not self.url.fragment == b'':
  288.             url += b'#' + self.url.fragment
  289.         return url
  290.  
  291.     def build(self, del_headers=None, add_headers=None):
  292.         req = b' '.join([self.method, self.build_url(), self.version])
  293.         req += CRLF
  294.  
  295.         if not del_headers:
  296.             del_headers = []
  297.         for k in self.headers:
  298.             if k not in del_headers:
  299.                 req += self.build_header(self.headers[k][0], self.headers[k][1]) + CRLF
  300.  
  301.         if not add_headers:
  302.             add_headers = []
  303.         for k in add_headers:
  304.             req += self.build_header(k[0], k[1]) + CRLF
  305.  
  306.         req += CRLF
  307.         if self.body:
  308.             req += self.body
  309.  
  310.         return req
  311.  
  312.     @staticmethod
  313.     def build_header(k, v):
  314.         return k + b': ' + v
  315.  
  316.     @staticmethod
  317.     def split(data):
  318.         pos = data.find(CRLF)
  319.         if pos == -1:
  320.             return False, data
  321.         line = data[:pos]
  322.         data = data[pos + len(CRLF):]
  323.         return line, data
  324.  
  325.  
  326. class Connection(object):
  327.     """TCP server/client connection abstraction."""
  328.  
  329.     def __init__(self, what):
  330.         self.conn = None
  331.         self.buffer = b''
  332.         self.closed = False
  333.         self.what = what  # server or client
  334.  
  335.     def send(self, data):
  336.         # TODO: Gracefully handle BrokenPipeError exceptions
  337.         return self.conn.send(data)
  338.  
  339.     def recv(self, bufsiz=8192):
  340.         try:
  341.             data = self.conn.recv(bufsiz)
  342.             if len(data) == 0:
  343.                 logger.debug('rcvd 0 bytes from %s' % self.what)
  344.                 return None
  345.             logger.debug('rcvd %d bytes from %s' % (len(data), self.what))
  346.             return data
  347.         except Exception as e:
  348.             if e.errno == errno.ECONNRESET:
  349.                 logger.debug('%r' % e)
  350.             else:
  351.                 logger.exception(
  352.                     'Exception while receiving from connection %s %r with reason %r' % (self.what, self.conn, e))
  353.             return None
  354.  
  355.     def close(self):
  356.         self.conn.close()
  357.         self.closed = True
  358.  
  359.     def buffer_size(self):
  360.         return len(self.buffer)
  361.  
  362.     def has_buffer(self):
  363.         return self.buffer_size() > 0
  364.  
  365.     def queue(self, data):
  366.         self.buffer += data
  367.  
  368.     def flush(self):
  369.         sent = self.send(self.buffer)
  370.         self.buffer = self.buffer[sent:]
  371.         logger.debug('flushed %d bytes to %s' % (sent, self.what))
  372.  
  373.  
  374. class Server(Connection):
  375.     """Establish connection to destination server."""
  376.  
  377.     def __init__(self, host, port):
  378.         super(Server, self).__init__(b'server')
  379.         self.addr = (host, int(port))
  380.  
  381.     def __del__(self):
  382.         if self.conn:
  383.             self.close()
  384.  
  385.     def connect(self):
  386.         self.conn = socket.create_connection((self.addr[0], self.addr[1]))
  387.  
  388.  
  389. class Client(Connection):
  390.     """Accepted client connection."""
  391.  
  392.     def __init__(self, conn, addr):
  393.         super(Client, self).__init__(b'client')
  394.         self.conn = conn
  395.         self.addr = addr
  396.  
  397.  
  398. class ProxyError(Exception):
  399.     pass
  400.  
  401.  
  402. class ProxyConnectionFailed(ProxyError):
  403.  
  404.     def __init__(self, host, port, reason):
  405.         self.host = host
  406.         self.port = port
  407.         self.reason = reason
  408.  
  409.     def __str__(self):
  410.         return '<ProxyConnectionFailed - %s:%s - %s>' % (self.host, self.port, self.reason)
  411.  
  412.  
  413. class ProxyAuthenticationFailed(ProxyError):
  414.     pass
  415.  
  416.  
  417. class Proxy(threading.Thread):
  418.     """HTTP proxy implementation.
  419.  
  420.    Accepts `Client` connection object and act as a proxy between client and server.
  421.    """
  422.  
  423.     def __init__(self, client, auth_code=None, server_recvbuf_size=8192, client_recvbuf_size=8192):
  424.         super(Proxy, self).__init__()
  425.  
  426.         self.start_time = self._now()
  427.         self.last_activity = self.start_time
  428.  
  429.         self.auth_code = auth_code
  430.         self.client = client
  431.         self.client_recvbuf_size = client_recvbuf_size
  432.         self.server = None
  433.         self.server_recvbuf_size = server_recvbuf_size
  434.  
  435.         self.request = HttpParser(HttpParser.types.REQUEST_PARSER)
  436.         self.response = HttpParser(HttpParser.types.RESPONSE_PARSER)
  437.  
  438.     @staticmethod
  439.     def _now():
  440.         return datetime.datetime.utcnow()
  441.  
  442.     def _inactive_for(self):
  443.         return (self._now() - self.last_activity).seconds
  444.  
  445.     def _is_inactive(self):
  446.         return self._inactive_for() > 30
  447.  
  448.     def _process_request(self, data):
  449.         # once we have connection to the server
  450.         # we don't parse the http request packets
  451.         # any further, instead just pipe incoming
  452.         # data from client to server
  453.         if self.server and not self.server.closed:
  454.             self.server.queue(data)
  455.             return
  456.  
  457.         # parse http request
  458.         self.request.parse(data)
  459.  
  460.         # once http request parser has reached the state complete
  461.         # we attempt to establish connection to destination server
  462.         if self.request.state == HttpParser.states.COMPLETE:
  463.             logger.debug('request parser is in state complete')
  464.  
  465.             if self.auth_code:
  466.                 if b'proxy-authorization' not in self.request.headers or \
  467.                         self.request.headers[b'proxy-authorization'][1] != self.auth_code:
  468.                     raise ProxyAuthenticationFailed()
  469.  
  470.             if self.request.method == b'CONNECT':
  471.                 host, port = self.request.url.path.split(COLON)
  472.             elif self.request.url:
  473.                 host, port = self.request.url.hostname, self.request.url.port if self.request.url.port else 80
  474.             else:
  475.                 raise Exception('Invalid request\n%s' % self.request.raw)
  476.  
  477.             self.server = Server(host, port)
  478.             try:
  479.                 logger.debug('connecting to server %s:%s' % (host, port))
  480.                 self.server.connect()
  481.                 logger.debug('connected to server %s:%s' % (host, port))
  482.             except Exception as e:  # TimeoutError, socket.gaierror
  483.                 self.server.closed = True
  484.                 raise ProxyConnectionFailed(host, port, repr(e))
  485.  
  486.             # for http connect methods (https requests)
  487.             # queue appropriate response for client
  488.             # notifying about established connection
  489.             if self.request.method == b'CONNECT':
  490.                 self.client.queue(PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT)
  491.             # for usual http requests, re-build request packet
  492.             # and queue for the server with appropriate headers
  493.             else:
  494.                 self.server.queue(self.request.build(
  495.                     del_headers=[b'proxy-authorization', b'proxy-connection', b'connection', b'keep-alive'],
  496.                     add_headers=[(b'Via', b'1.1 proxy.py v%s' % version), (b'Connection', b'Close')]
  497.                 ))
  498.  
  499.     def _process_response(self, data):
  500.         # parse incoming response packet
  501.         # only for non-https requests
  502.         if not self.request.method == b'CONNECT':
  503.             self.response.parse(data)
  504.  
  505.         # queue data for client
  506.         self.client.queue(data)
  507.  
  508.     def _access_log(self):
  509.         host, port = self.server.addr if self.server else (None, None)
  510.         if self.request.method == b'CONNECT':
  511.             logger.info(
  512.                 '%s:%s - %s %s:%s' % (self.client.addr[0], self.client.addr[1], self.request.method, host, port))
  513.         elif self.request.method:
  514.             logger.info('%s:%s - %s %s:%s%s - %s %s - %s bytes' % (
  515.                 self.client.addr[0], self.client.addr[1], self.request.method, host, port, self.request.build_url(),
  516.                 self.response.code, self.response.reason, len(self.response.raw)))
  517.  
  518.     def _get_waitable_lists(self):
  519.         rlist, wlist, xlist = [self.client.conn], [], []
  520.         if self.client.has_buffer():
  521.             wlist.append(self.client.conn)
  522.         if self.server and not self.server.closed:
  523.             rlist.append(self.server.conn)
  524.         if self.server and not self.server.closed and self.server.has_buffer():
  525.             wlist.append(self.server.conn)
  526.         return rlist, wlist, xlist
  527.  
  528.     def _process_wlist(self, w):
  529.         if self.client.conn in w:
  530.             logger.debug('client is ready for writes, flushing client buffer')
  531.             self.client.flush()
  532.  
  533.         if self.server and not self.server.closed and self.server.conn in w:
  534.             logger.debug('server is ready for writes, flushing server buffer')
  535.             self.server.flush()
  536.  
  537.     def _process_rlist(self, r):
  538.         """Returns True if connection to client must be closed."""
  539.         if self.client.conn in r:
  540.             logger.debug('client is ready for reads, reading')
  541.             data = self.client.recv(self.client_recvbuf_size)
  542.             self.last_activity = self._now()
  543.  
  544.             if not data:
  545.                 logger.debug('client closed connection, breaking')
  546.                 return True
  547.  
  548.             try:
  549.                 self._process_request(data)
  550.             except (ProxyAuthenticationFailed, ProxyConnectionFailed) as e:
  551.                 logger.exception(e)
  552.                 self.client.queue(Proxy._get_response_pkt_by_exception(e))
  553.                 self.client.flush()
  554.                 return True
  555.  
  556.         if self.server and not self.server.closed and self.server.conn in r:
  557.             logger.debug('server is ready for reads, reading')
  558.             data = self.server.recv(self.server_recvbuf_size)
  559.             self.last_activity = self._now()
  560.  
  561.             if not data:
  562.                 logger.debug('server closed connection')
  563.                 self.server.close()
  564.             else:
  565.                 self._process_response(data)
  566.  
  567.         return False
  568.  
  569.     def _process(self):
  570.         while True:
  571.             rlist, wlist, xlist = self._get_waitable_lists()
  572.             r, w, x = select.select(rlist, wlist, xlist, 1)
  573.  
  574.             self._process_wlist(w)
  575.             if self._process_rlist(r):
  576.                 break
  577.  
  578.             if self.client.buffer_size() == 0:
  579.                 if self.response.state == HttpParser.states.COMPLETE:
  580.                     logger.debug('client buffer is empty and response state is complete, breaking')
  581.                     break
  582.  
  583.                 if self._is_inactive():
  584.                     logger.debug('client buffer is empty and maximum inactivity has reached, breaking')
  585.                     break
  586.  
  587.     @staticmethod
  588.     def _get_response_pkt_by_exception(e):
  589.         if e.__class__.__name__ == 'ProxyAuthenticationFailed':
  590.             return PROXY_AUTHENTICATION_REQUIRED_RESPONSE_PKT
  591.         if e.__class__.__name__ == 'ProxyConnectionFailed':
  592.             return BAD_GATEWAY_RESPONSE_PKT
  593.  
  594.     def run(self):
  595.         logger.debug('Proxying connection %r' % self.client.conn)
  596.         try:
  597.             self._process()
  598.         except KeyboardInterrupt:
  599.             pass
  600.         except Exception as e:
  601.             logger.exception('Exception while handling connection %r with reason %r' % (self.client.conn, e))
  602.         finally:
  603.             logger.debug(
  604.                 'closing client connection with pending client buffer size %d bytes' % self.client.buffer_size())
  605.             self.client.close()
  606.             if self.server:
  607.                 logger.debug(
  608.                     'closed client connection with pending server buffer size %d bytes' % self.server.buffer_size())
  609.             self._access_log()
  610.             logger.debug('Closing proxy for connection %r at address %r' % (self.client.conn, self.client.addr))
  611.  
  612.  
  613. class TCP(object):
  614.     """TCP server implementation.
  615.  
  616.    Subclass MUST implement `handle` method. It accepts an instance of accepted `Client` connection.
  617.    """
  618.  
  619.     def __init__(self, hostname='127.0.0.1', port=8899, backlog=100):
  620.         self.hostname = hostname
  621.         self.port = port
  622.         self.backlog = backlog
  623.         self.socket = None
  624.  
  625.     def handle(self, client):
  626.         raise NotImplementedError()
  627.  
  628.     def run(self):
  629.         try:
  630.             logger.info('Starting server on port %d' % self.port)
  631.             self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  632.             self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  633.             self.socket.bind((self.hostname, self.port))
  634.             self.socket.listen(self.backlog)
  635.             while True:
  636.                 conn, addr = self.socket.accept()
  637.                 client = Client(conn, addr)
  638.                 self.handle(client)
  639.         except Exception as e:
  640.             logger.exception('Exception while running the server %r' % e)
  641.         finally:
  642.             logger.info('Closing server socket')
  643.             self.socket.close()
  644.  
  645.  
  646. class HTTP(TCP):
  647.     """HTTP proxy server implementation.
  648.  
  649.    Spawns new process to proxy accepted client connection.
  650.    """
  651.  
  652.     def __init__(self, hostname='127.0.0.1', port=8899, backlog=100,
  653.                  auth_code=None, server_recvbuf_size=8192, client_recvbuf_size=8192):
  654.         super(HTTP, self).__init__(hostname, port, backlog)
  655.         self.auth_code = auth_code
  656.         self.client_recvbuf_size = client_recvbuf_size
  657.         self.server_recvbuf_size = server_recvbuf_size
  658.  
  659.     def handle(self, client):
  660.         proxy = Proxy(client,
  661.                       auth_code=self.auth_code,
  662.                       server_recvbuf_size=self.server_recvbuf_size,
  663.                       client_recvbuf_size=self.client_recvbuf_size)
  664.         proxy.daemon = True
  665.         proxy.start()
  666.  
  667.  
  668. def set_open_file_limit(soft_limit):
  669.     """Configure open file description soft limit on supported OS."""
  670.     if os.name != 'nt':  # resource module not available on Windows OS
  671.         curr_soft_limit, curr_hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
  672.         if curr_soft_limit < soft_limit < curr_hard_limit:
  673.             resource.setrlimit(resource.RLIMIT_NOFILE, (soft_limit, curr_hard_limit))
  674.             logger.info('Open file descriptor soft limit set to %d' % soft_limit)
  675.  
  676.  
  677. def main():
  678.     parser = argparse.ArgumentParser(
  679.         description='proxy.py v%s' % __version__,
  680.         epilog='Having difficulty using proxy.py? Report at: %s/issues/new' % __homepage__
  681.     )
  682.  
  683.     parser.add_argument('--hostname', default='127.0.0.1', help='Default: 127.0.0.1')
  684.     parser.add_argument('--port', default='8899', help='Default: 8899')
  685.     parser.add_argument('--backlog', default='100', help='Default: 100. '
  686.                                                          'Maximum number of pending connections to proxy server')
  687.     parser.add_argument('--basic-auth', default=None, help='Default: No authentication. '
  688.                                                            'Specify colon separated user:password '
  689.                                                            'to enable basic authentication.')
  690.     parser.add_argument('--server-recvbuf-size', default='8192', help='Default: 8 KB. '
  691.                                                                       'Maximum amount of data received from the '
  692.                                                                       'server in a single recv() operation. Bump this '
  693.                                                                       'value for faster downloads at the expense of '
  694.                                                                       'increased RAM.')
  695.     parser.add_argument('--client-recvbuf-size', default='8192', help='Default: 8 KB. '
  696.                                                                       'Maximum amount of data received from the '
  697.                                                                       'client in a single recv() operation. Bump this '
  698.                                                                       'value for faster uploads at the expense of '
  699.                                                                       'increased RAM.')
  700.     parser.add_argument('--open-file-limit', default='1024', help='Default: 1024. '
  701.                                                                   'Maximum number of files (TCP connections) '
  702.                                                                   'that proxy.py can open concurrently.')
  703.     parser.add_argument('--log-level', default='INFO', help='DEBUG, INFO (default), WARNING, ERROR, CRITICAL')
  704.     args = parser.parse_args()
  705.  
  706.     logging.basicConfig(level=getattr(logging, args.log_level),
  707.                         format='%(asctime)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s')
  708.  
  709.     try:
  710.         set_open_file_limit(int(args.open_file_limit))
  711.  
  712.         auth_code = None
  713.         if args.basic_auth:
  714.             auth_code = b'Basic %s' % base64.b64encode(bytes_(args.basic_auth))
  715.  
  716.         proxy = HTTP(hostname=args.hostname,
  717.                      port=int(args.port),
  718.                      backlog=int(args.backlog),
  719.                      auth_code=auth_code,
  720.                      server_recvbuf_size=int(args.server_recvbuf_size),
  721.                      client_recvbuf_size=int(args.client_recvbuf_size))
  722.         proxy.run()
  723.     except KeyboardInterrupt:
  724.         pass
  725.  
  726.  
  727. if __name__ == '__main__':
  728.     main()
  729.