From 16e076cf7064d64a2ec969d7ace6cbdd1dbe48d8 Mon Sep 17 00:00:00 2001 From: allfro Date: Thu, 19 Jul 2012 22:38:19 -0400 Subject: [PATCH] First release --- README.md | 149 ++++++++++++++++++++++++++++++++++++++++++- src/miproxy/proxy.py | 33 +++++++--- 2 files changed, 170 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 370f06e..c970497 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,147 @@ -pymiproxy -========= +pymiproxy - Python Micro Interceptor Proxy +========================================== + +A small and sweet man-in-the-middle proxy capable of doing HTTP and HTTP over SSL. + + +Introduction +============ + +pymiproxy is a small, lightweight, man-in-the-middle proxy capable of performing HTTP and HTTPS (or SSL) inspection. The +proxy provides a built-in certificate authority that is capable of generating certificates for SSL-based destinations. +Pymiproxy is also extensible and provides two methods for extending the proxy: method overloading, and a pluggable +interface. It is ideal for situations where you're in dire need of a cool proxy to tamper with out- and/or in-bound HTTP +data. + +Installation Requirements +========================= + +The following modules are required: + +- pyOpenSSL + + +Installation +============ + +Just run the following command at the command prompt: + + $ sudo python setup.py install + + +Usage +===== + +The module offers a few examples in the code. In brief, pymiproxy can be run right-away by issuing the following command +at the the command-prompt: + + $ python -m miproxy.proxy + +This will invoke pymiproxy with the *DebugInterceptor* plugin which simply outputs the first 100 bytes of each request +and response. The proxy runs on port 8080 and listens on all addresses. Go ahead and give it a try. + +=================================== +Extending or Implementing pymiproxy +=================================== + +There are two ways of extending the proxy: + + +- Develop and register an Interceptor plugin; or +- Overload the mitm_request, and mitm_response methods in the ProxyHandler class. + + +The decision on which method you choose to use is entirely dependant on whether or not you wish to push the data being +intercepted through a set of interceptors or not. + +------------------- +Interceptor Plugins +------------------- + +There are currently two types of interceptor plugins: + +- RequestInterceptorPlugins: executed prior to sending the request to the remote server; and +- ResponseInterceptorPlugins: executed prior to sending the response back to the client. + +The following flow is taken by pymiproxy in this mode: + +1. Client request received +2. Client request parsed +3. Client request processed/transformed by Request Interceptor plugins +4. Updated request sent to remote server +5. Response received by remote server +6. Response processed/transformed by Response Interceptor plugins +7. Updated response sent to client + +You can register as many plugins as you wish. However, keep in mind that plugins are executed in the order that they are +registered in. Take care in how you register your plugins if the result of one plugin is dependent on the result of +another. + +The following is a simple code example of how to run the proxy with plugins: + + from miproxy.proxy import RequestInterceptorPlugin, ResponseInterceptorPlugin, AsyncMitmProxy + + class DebugInterceptor(RequestInterceptorPlugin, ResponseInterceptorPlugin): + + def do_request(self, data): + print '>> %s' % repr(data[:100]) + return data + + def do_response(self, data): + print '<< %s' % repr(data[:100]) + return data + + + if __name__ == '__main__': + proxy = None + if not argv[1:]: + proxy = AsyncMitmProxy() + else: + proxy = AsyncMitmProxy(ca_file=argv[1]) + proxy.register_interceptor(DebugInterceptor) + try: + proxy.serve_forever() + except KeyboardInterrupt: + proxy.server_close() + + +------------------ +Method Overloading +------------------ + +The alternate approach to extending the proxy functionality is to subclass the ProxyHandler class and overload the +mitm_request and mitm_response methods. The following is a quick example: + + from miproxy.proxy import AsyncMitmProxy + + class MitmProxyHandler(ProxyHandler): + + def mitm_request(self, data): + print '>> %s' % repr(data[:100]) + return data + + def mitm_response(self, data): + print '<< %s' % repr(data[:100]) + return data + + + if __name__ == '__main__': + proxy = None + if not argv[1:]: + proxy = AsyncMitmProxy(RequestHandlerClass=MitmProxyHandler) + else: + proxy = AsyncMitmProxy(RequestHandlerClass=MitmProxyHandler, ca_file=argv[1]) + try: + proxy.serve_forever() + except KeyboardInterrupt: + proxy.server_close() + +Note: In both cases, the methods that process the data need to return the data back to the proxy handler. Otherwise, +you'll get an exception. + +Kudos +===== + +Thanks to the great documentation at python.org, GnuCitizen's PDP for the ideas, the pyOpenSSL group for making a great +OpenSSL API. -A small and sweet man-in-the-middle proxy capable of doing HTTP and HTTP over SSL. \ No newline at end of file diff --git a/src/miproxy/proxy.py b/src/miproxy/proxy.py index 060e8b8..c2c9ee2 100644 --- a/src/miproxy/proxy.py +++ b/src/miproxy/proxy.py @@ -2,12 +2,14 @@ from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from SocketServer import ThreadingMixIn +from os import path, mkdir, listdir from httplib import HTTPResponse +from tempfile import gettempdir from urlparse import urlparse from ssl import wrap_socket -from os import path, mkdir from socket import socket from re import compile +from sys import argv from OpenSSL.crypto import (X509Extension, X509, dump_privatekey, dump_certificate, load_certificate, load_privatekey, PKey, TYPE_RSA, X509Req) @@ -36,16 +38,25 @@ __all__ = [ class CertificateAuthority(object): - def __init__(self, ca_file='ca.pem'): - self.ca_file = 'ca.pem' - self._serial = 1 - if not path.exists('.ssl'): - mkdir('.ssl') + def __init__(self, ca_file='ca.pem', cache_dir=gettempdir()): + self.ca_file = ca_file + self.cache_dir = cache_dir + self._serial = self._get_serial() if not path.exists(ca_file): self._generate_ca() else: self._read_ca(ca_file) + def _get_serial(self): + s = 1 + for c in filter(lambda x: x.startswith('.pymp_'), listdir(self.cache_dir)): + c = load_certificate(FILETYPE_PEM, open(path.sep.join([self.cache_dir, c])).read()) + sc = c.get_serial_number() + if sc > s: + s = sc + del c + return s + def _generate_ca(self): # Generate key self.key = PKey() @@ -67,7 +78,7 @@ class CertificateAuthority(object): ]) self.cert.sign(self.key, "sha1") - with open('ca.pem', 'wb+') as f: + with open(self.ca_file, 'wb+') as f: f.write(dump_privatekey(FILETYPE_PEM, self.key)) f.write(dump_certificate(FILETYPE_PEM, self.cert)) @@ -76,7 +87,7 @@ class CertificateAuthority(object): self.key = load_privatekey(FILETYPE_PEM, open(file).read()) def __getitem__(self, cn): - cnp = path.sep.join(['.ssl', '.%s.pem' % cn]) + cnp = path.sep.join([self.cache_dir, '.pymp_%s.pem' % cn]) if not path.exists(cnp): # create certificate key = PKey() @@ -293,7 +304,11 @@ class DebugInterceptor(RequestInterceptorPlugin, ResponseInterceptorPlugin): if __name__ == '__main__': - proxy = AsyncMitmProxy() + proxy = None + if not argv[1:]: + proxy = AsyncMitmProxy() + else: + proxy = AsyncMitmProxy(ca_file=argv[1]) proxy.register_interceptor(DebugInterceptor) try: proxy.serve_forever()