diff --git a/.gitignore b/.gitignore index 6cbe93b0..0a1c1ace 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ .project .pydevproject .settings + +*.pyc +dist +proxy.py.egg-info diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..a3758376 --- /dev/null +++ b/LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 2013 by Abhinav Singh and contributors. + +Some rights reserved. + +Redistribution and use in source and binary forms of the software as well +as documentation, with or without modification, are permitted provided +that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +* The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..689e50f5 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include LICENSE +include README.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..69e58c85 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +proxy.py +======== + +HTTP Proxy Server in Python. diff --git a/proxy.py b/proxy.py index 3f59976d..7fe71f5d 100644 --- a/proxy.py +++ b/proxy.py @@ -1,23 +1,33 @@ +#!/usr/bin/env python # -*- coding: utf-8 -*- """ proxy.py ~~~~~~~~ - HTTP proxy implementation. + HTTP Proxy Server in Python. + + :copyright: (c) 2013 by Abhinav Singh. + :license: BSD, see LICENSE for more details. """ +VERSION = (0, 1) +__version__ = '.'.join(map(str, VERSION[0:2])) +__description__ = 'HTTP Proxy Server in Python' +__author__ = 'Abhinav Singh' +__author_email__ = 'mailsforabhinav@gmail.com' +__homepage__ = 'https://github.com/abhinavsingh/proxy.py' +__license__ = 'BSD' + import multiprocessing import datetime import urlparse +import argparse import logging import socket import select -logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger('proxy.py') -CRLF = '\r\n' -COLON = ':' -SP = ' ' +CRLF, COLON, SP = '\r\n', ':', ' ' HTTP_REQUEST_PARSER = 1 HTTP_RESPONSE_PARSER = 2 @@ -34,6 +44,7 @@ CHUNK_PARSER_STATE_WAITING_FOR_DATA = 2 CHUNK_PARSER_STATE_COMPLETE = 3 class ChunkParser(object): + """HTTP chunked encoding response parser.""" def __init__(self): self.state = CHUNK_PARSER_STATE_WAITING_FOR_SIZE @@ -66,6 +77,7 @@ class ChunkParser(object): return len(data) > 0, data class HttpParser(object): + """HTTP request/response parser.""" def __init__(self, type=None): self.state = HTTP_PARSER_STATE_INITIALIZED @@ -196,14 +208,16 @@ class HttpParser(object): data = data[pos+len(CRLF):] return line, data -class ProxyConnectionFailed(Exception): +class ProxyError(Exception): + pass + +class ProxyConnectionFailed(ProxyError): pass class Proxy(multiprocessing.Process): """HTTP proxy implementation. - Accepts connection object and act as a proxy between - client and server. + Accepts connection object and act as a proxy between client and server. """ def __init__(self, conn, addr): @@ -247,7 +261,7 @@ class Proxy(multiprocessing.Process): logger.debug('rcvd %d bytes from %s' % (len(data), what)) return data except Exception as e: - logger.debug('Exception while receiving from connection %r with reason %r' % (self.conn[what], e)) + logger.error('Exception while receiving from connection %r with reason %r' % (self.conn[what], e)) return None def _recv_from_client(self): @@ -326,9 +340,9 @@ class Proxy(multiprocessing.Process): def _access_log(self): host, port = self._server_host_port() if self.parser['client'].method == "CONNECT": - logger.debug("%r %s %s:%s (%s secs)" % (self.addr, self.parser['client'].method, host, port, self._inactive_for())) + logger.info("%s:%s - %s %s:%s" % (self.addr[0], self.addr[1], self.parser['client'].method, host, port)) else: - logger.debug("%r %s %s:%s%s %s %s %s bytes (%s secs)" % (self.addr, self.parser['client'].method, host, port, self.parser['client'].build_url(), self.parser['server'].code, self.parser['server'].reason, len(self.parser['server'].raw), self._inactive_for())) + logger.info("%s:%s - %s %s:%s%s - %s %s - %s bytes" % (self.addr[0], self.addr[1], self.parser['client'].method, host, port, self.parser['client'].build_url(), self.parser['server'].code, self.parser['server'].reason, len(self.parser['server'].raw))) def run(self): logger.debug('Proxying connection %r at address %r' % (self.conn['client'], self.addr)) @@ -386,7 +400,7 @@ class Proxy(multiprocessing.Process): logger.debug('client buffer is empty and maximum inactivity has reached, breaking') break except Exception as e: - logger.debug('Exception while handling connection %r with reason %r' % (self.conn['client'], e)) + logger.error('Exception while handling connection %r with reason %r' % (self.conn['client'], e)) finally: logger.debug("closing client connection with client pending buffer size %d bytes, server pending buffer size %d bytes" % (len(self.buffer['client']), len(self.buffer['server']))) self.conn['client'].close() @@ -405,9 +419,9 @@ class Server(object): self.port = port self.backlog = backlog - def start(self): + def run(self): try: - logger.debug('Starting server on port %d' % self.port) + logger.info('Starting proxy server on port %d' % self.port) self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind((self.hostname, self.port)) @@ -420,10 +434,30 @@ class Server(object): proc.start() logger.debug('Started process %r to handle connection %r' % (proc, conn)) except Exception as e: - logger.debug('Exception while running the server %r' % e) + logger.error('Exception while running the server %r' % e) finally: - logger.debug('Closing server socket') + logger.info('Closing server socket') self.socket.close() +def main(): + parser = argparse.ArgumentParser( + description='proxy.py v%s' % __version__, + epilog='Having difficulty using proxy.py? Report at: %s/issues/new' % __homepage__ + ) + parser.add_argument('--hostname', default='127.0.0.1', help='Default: 127.0.0.1') + parser.add_argument('--port', default='8899', help='Default: 8899') + parser.add_argument('--log-level', default='INFO', help='DEBUG, INFO, WARNING, ERROR, CRITICAL') + args = parser.parse_args() + + hostname = args.hostname + port = int(args.port) + logging.basicConfig(level=getattr(logging, args.log_level), format='%(levelname)s - %(asctime)s - %(message)s') + + try: + server = Server(hostname, port) + server.run() + except KeyboardInterrupt: + pass + if __name__ == '__main__': - Server().start() + main() diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..5339f0ec --- /dev/null +++ b/setup.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + + HTTP Proxy Server in Python. + + :copyright: (c) 2013 by Abhinav Singh. + :license: BSD, see LICENSE for more details. +""" +from setuptools import setup +import proxy + +classifiers = [ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: BSD License', + 'Operating System :: MacOS', + 'Operating System :: POSIX', + 'Operating System :: Unix', + 'Operating System :: Microsoft', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + 'Topic :: Internet :: Proxy Servers', + 'Topic :: Internet :: WWW/HTTP :: HTTP Servers', +] + +setup( + name = 'proxy.py', + version = proxy.__version__, + description = proxy.__description__, + long_description = open('README.md').read().strip(), + author = proxy.__author__, + author_email = proxy.__author_email__, + url = proxy.__homepage__, + license = proxy.__license__, + py_modules = ['proxy'], + scripts = ['proxy.py'], + install_requires = [], + classifiers = classifiers +)