├── maproxy ├── __init__.py ├── proxyserver.py ├── iomanager.py └── session.py ├── MANIFEST.in ├── CHANGES ├── demos ├── tcp2ssl.py ├── tcp2tcp.py ├── ssl2tcp.py ├── certificate.pem ├── ssl2ssl.py ├── privatekey.pem ├── all.py └── logging_proxy.py ├── setup.py ├── README.rst └── LICENSE /maproxy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE CHANGES 2 | recursive-include docs * 3 | recursive-include demos * 4 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | v0.0.6 , 06/22/2914 -- Adding PyPI , demos , ... 2 | v0.0.7 , 06/22/2914 -- adding some Demos 3 | 4 | 5 | -------------------------------------------------------------------------------- /demos/tcp2ssl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import tornado.ioloop 4 | from maproxy.proxyserver import ProxyServer 5 | # HTTP->HTTPS 6 | # "server_ssl_options=True" simply means "connect to server with SSL" 7 | server = ProxyServer("www.google.com",443, server_ssl_options=True) 8 | server.listen(82) 9 | print("http://127.0.0.1:82 -> https://www.google.com:443") 10 | tornado.ioloop.IOLoop.instance().start(); -------------------------------------------------------------------------------- /demos/tcp2tcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import tornado.ioloop 4 | import maproxy.proxyserver 5 | 6 | 7 | 8 | # HTTP->HTTP 9 | # On your computer, browse to "http://127.0.0.1:81/" and you'll get http://www.google.com 10 | server = maproxy.proxyserver.ProxyServer("www.google.com",80) 11 | server.listen(81) 12 | print("http://127.0.0.1:81 -> http://www.google.com") 13 | tornado.ioloop.IOLoop.instance().start() 14 | -------------------------------------------------------------------------------- /demos/ssl2tcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import tornado.ioloop 4 | import maproxy.proxyserver 5 | 6 | # HTTPS->HTTP 7 | ssl_certs={ "certfile": "./certificate.pem", 8 | "keyfile": "./privatekey.pem" } 9 | # "client_ssl_options=ssl_certs" simply means "listen using SSL" 10 | server = maproxy.proxyserver.ProxyServer("www.google.com",80, 11 | client_ssl_options=ssl_certs) 12 | server.listen(83) 13 | print("https://127.0.0.1:83 -> http://www.google.com") 14 | tornado.ioloop.IOLoop.instance().start() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | 5 | setup(name='maproxy', 6 | 7 | version = "0.0.12", 8 | author = "Zvika Ferentz", 9 | author_email = "zvika dot ferentz at gmail", 10 | description = ("My first attempt to create a simple and awesome " 11 | "TCP proxy using Tornado"), 12 | 13 | 14 | 15 | # For now, let's just build a module, not a package 16 | packages=['maproxy'], 17 | 18 | keywords = "TCP proxy ssl http https certificates", 19 | long_description=open('README.rst').read(), 20 | 21 | install_requires=["tornado >= 3.2"], 22 | 23 | classifiers=[ 24 | "Development Status :: 2 - Pre-Alpha", 25 | "Topic :: Internet :: Proxy Servers"] 26 | ) 27 | 28 | 29 | -------------------------------------------------------------------------------- /demos/certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC/zCCAeegAwIBAgIJAN+KtdpJV+1qMA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV 3 | BAMMC3d3dy53dGYub3JnMB4XDTE0MDYyMjA0MzkzOVoXDTI0MDYxOTA0MzkzOVow 4 | FjEUMBIGA1UEAwwLd3d3Lnd0Zi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 5 | ggEKAoIBAQDhyFquQGTe5M26bvXL+p708+mTMVe99+qLSWY9STl5jknCN0deI8JK 6 | Q3KmMEkhtVdoH6xB+V8ELd4zt7+74d4teiWqLJSaYo5tM+IhDl3BnXBDHmwbrXXE 7 | pYmMp21Aj04OcusfvkBLyufxzWOLrRCIo/fncUNQaP5COAHyQO1dDICRVPZivJaq 8 | ss6NQm8ipS0XdfPp8w1DPs6BFz813aJ+guRxAE98b9K3YJg7BFV/D5I5onBlTmsg 9 | 926b3bZ2VTbs5zNojSRovMGWTBJdTwkqjjCEA0Zmz+0BNw6Sy93zpN5VsTrn+BmD 10 | SpfL4ZoW2OG0WqX+76/KZ9Az9Df/pvodAgMBAAGjUDBOMB0GA1UdDgQWBBRauSed 11 | w3vSYdFiM+DaxXzeySvOWTAfBgNVHSMEGDAWgBRauSedw3vSYdFiM+DaxXzeySvO 12 | WTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQDKdMqP48Zk/aU3YZHL 13 | LtymIgwGSkC72ci8oSzqtGw36hzgZWW22iU1P4vvPm/O1HS9SYiSGq7PvAjKozim 14 | oU0s/bImxvwEF5fYzwVFYrc5Tzgd32kKkIKsJqtBYcPI3Uxp5xYiVLBUainCUcdV 15 | p8qwUS5D7sFoNGFoFYZ82vAY0aZ5kJEgO+G1sYQ+BL7Y8KCPEqKMSkiTggGpujt+ 16 | aFOrPxGoLcb8EzLg7g/PF3E6RZCsCWLXZkTYujpsPyBv1Y3M7Fmb8VWX+02D6GaS 17 | aY2RWdLFdVhmtg6hZkmnTGgnOC/nRJHohfeRKHg+PCXq+pImnKwThoNz4SduA2wG 18 | HC0f 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /demos/ssl2ssl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import tornado.ioloop 4 | import maproxy.proxyserver 5 | 6 | # HTTPS->HTTP 7 | ssl_certs={ "certfile": "./certificate.pem", 8 | "keyfile": "./privatekey.pem" } 9 | 10 | # "client_ssl_options=ssl_certs" simply means "listen using SSL" 11 | # "server_ssl_options=True" simply means "connect to server with SSL" 12 | server1 = maproxy.proxyserver.ProxyServer("www.google.com",443, 13 | client_ssl_options=ssl_certs, 14 | server_ssl_options=True) 15 | server1.listen(83) 16 | print("https://127.0.0.1:83 -> http://www.google.com") 17 | 18 | # "client_ssl_options=ssl_certs" simply means "listen using SSL" 19 | # "server_ssl_options=ssl_certs" simply means "connect to server with client-certificates" 20 | server2 = maproxy.proxyserver.ProxyServer("www.google.com",443, 21 | client_ssl_options=ssl_certs, 22 | server_ssl_options=ssl_certs) 23 | server2.listen(84) 24 | print("https://127.0.0.1:84 -> http://www.google.com (using client-certificates)") 25 | 26 | tornado.ioloop.IOLoop.instance().start() -------------------------------------------------------------------------------- /demos/privatekey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA4charkBk3uTNum71y/qe9PPpkzFXvffqi0lmPUk5eY5JwjdH 3 | XiPCSkNypjBJIbVXaB+sQflfBC3eM7e/u+HeLXolqiyUmmKObTPiIQ5dwZ1wQx5s 4 | G611xKWJjKdtQI9ODnLrH75AS8rn8c1ji60QiKP353FDUGj+QjgB8kDtXQyAkVT2 5 | YryWqrLOjUJvIqUtF3Xz6fMNQz7OgRc/Nd2ifoLkcQBPfG/St2CYOwRVfw+SOaJw 6 | ZU5rIPdum922dlU27OczaI0kaLzBlkwSXU8JKo4whANGZs/tATcOksvd86TeVbE6 7 | 5/gZg0qXy+GaFtjhtFql/u+vymfQM/Q3/6b6HQIDAQABAoIBAQCCkec/FiY/cHo4 8 | 8qpayBjc96GAaeygA5sz6cKidpIyZcLp+iXfnzZg1BidWxcv0zs1D/wCO0BjnlL9 9 | /al38esWyai2fQmDLrPHG1YOX8yAh5fAePt0FiAhFMoy+TAJQdaWLIck2FU+f50b 10 | DPggcnk5S/m1cp7HBbDkgpc9jaa3Q8mKATDA259DwykYAvPgt0XPvb7GpevrLqZg 11 | mM9b78Gl2S1ayRb7xz5IHr5ZnmHkVoK8V3kmYSI+SOSJRYqfGpj/Hgi6K4eIxezv 12 | d+rqe/ViwfuBeARmJWfaJdvJfwNuGpbTKf0YresYqJ0ggD2hw2vj1uSxBGFDjSpn 13 | mRVWG0kBAoGBAPSJMpnOlPu8vALc7ATrTxIHXkdx54SggcJ9D/jdnCPoRllVNxn6 14 | 7/fE7IQYd6EfF4lWducxPMfkdBwoK6yI6Eyo5YO9fLEKk3luGeux/cFW8ad3n1Fo 15 | cV8stw/XOHaPhpdq+qymXZKAEGMqrxyq7WlTkxHM0o1D72vNUbWVQBkRAoGBAOxe 16 | FqvIdb3lRlUOamHeo/S0CyWrzsdkY7adjYeksnKwGeJTDJuvEIZ28fDmgnfH9dNR 17 | SEzwj7OdDkk5WTrTlmovv9D0HtcJctCJbfsZZfpD54zjg8Wrb/6Ux2RMrE8DTCXq 18 | WztDt7xiOexf8Dnqd5qWorjcRNYr6jwcePnJ6HBNAoGAW4YyQzD3wBTOxb+MMvcj 19 | fBr35YOzZIdyqamHXd0MAMCB/BOR8Q5j3Hd/Ep3ZwJtTgtqy3CsolaRi9NrwJb6E 20 | O5UHejxkvBq6Qbu8xeOzlzaEceqq3ZxauoWQ6sPh5TYo6Oloc1A9O4TlHUivi+pJ 21 | u59FL2da8vaXWODbETyQZhECgYAC4qMkNa46QzI4l5R03WLi+c+pBg/gHzmYYRP5 22 | M+l5vOyT9q+QtvJcsdcCOc4d6DL4AWYAim82ohQqkKimLy8G3M5anqBBv7vHD+Zn 23 | ykeUZn/NGHnjT9RuJyLH9qejz0Z+r/2tG4aCpjBO0lz8WABdwrj5yLaOZrrVQQO5 24 | CN3tgQKBgQDkslydio13nmw6dDHksFArCFXG3W9tWUVD8IFIO9zWYosK9Ei9+MaN 25 | wyOAMYDHjMRblxIhos16oB9Yms0As+Eam0M/gpDh+NKETkqr9KxK+9Rtq9w+EUBs 26 | 4HUFJx8XKWXRrj3q1y+PdE6nFNW2dTZUR+tV7nEBRb85mvBPKh/heA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /demos/all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from maproxy.iomanager import IOManager 5 | from maproxy.proxyserver import ProxyServer 6 | import signal 7 | 8 | 9 | 10 | g_IOManager=IOManager() 11 | 12 | 13 | if __name__ == '__main__': 14 | 15 | # Add standard signal handlers - 16 | # call the "sto()" method when the user hits Ctrl-C 17 | signal.signal(signal.SIGINT, lambda sig,frame: g_IOManager.stop()) 18 | signal.signal(signal.SIGTERM, lambda sig,frame: g_IOManager.stop()) 19 | 20 | 21 | bUseSSL=True 22 | ssl_certs={ "certfile": os.path.join(os.path.dirname(sys.argv[0]), "certificate.pem"), 23 | "keyfile": os.path.join(os.path.dirname(sys.argv[0]), "privatekey.pem") } 24 | 25 | if not os.path.isfile(ssl_certs["certfile"]) or \ 26 | not os.path.isfile(ssl_certs["keyfile"]): 27 | print("Warning: SSL-Proxy is disabled . certificate file(s) not found") 28 | bUseSSL=False 29 | 30 | # HTTP->HTTP 31 | # On your computer, browse to "http://127.0.0.1:81/" and you'll get http://www.google.com 32 | server = ProxyServer("www.google.com",80) 33 | server.listen(81) 34 | g_IOManager.add(server) 35 | print("http://127.0.0.1:81 -> http://www.google.com") 36 | 37 | # HTTP->HTTPS 38 | # "server_ssl_options=True" simply means "connect to server with SSL" 39 | server = ProxyServer("www.google.com",443, server_ssl_options=True) 40 | server.listen(82) 41 | g_IOManager.add(server) 42 | print("http://127.0.0.1:82 -> https://www.google.com:443") 43 | 44 | 45 | if bUseSSL: 46 | # HTTPS->HTTP 47 | # "client_ssl_options=ssl_certs" simply means "listen using SSL" 48 | server = ProxyServer("www.google.com",80, client_ssl_options=ssl_certs) 49 | server.listen(83) 50 | g_IOManager.add(server) 51 | print("https://127.0.0.1:83 -> http://www.google.com") 52 | 53 | 54 | # HTTPS->HTTPS 55 | # (Listens on SSL , connect to SSL) 56 | server = ProxyServer("www.google.com",443, client_ssl_options=ssl_certs,server_ssl_options=True) 57 | server.listen(84) 58 | g_IOManager.add(server) 59 | print("https://127.0.0.1:84 -> https://www.google.com:443 ") 60 | 61 | 62 | # HTTP->HTTPS , use specific client-certificate 63 | # "server_ssl_options=ssl_certs" means "connect to SSL-server using the provided client-certificates" 64 | server = ProxyServer("www.google.com",443, server_ssl_options=ssl_certs) 65 | server.listen(85) 66 | g_IOManager.add(server) 67 | print("http://127.0.0.1:85 -> https://www.google.com:443 (Connect with client-certificates)") 68 | 69 | 70 | print("Starting...") 71 | # the next call to start) is blocking (thread=False) 72 | # So we simply wait for Ctrl-C 73 | g_IOManager.start(thread=False) 74 | print("Stopping...") 75 | g_IOManager.stop(gracefully=True,wait=False) 76 | print("Stopped...") 77 | print("Done") -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Ma`Proxy 3 | =========== 4 | 5 | Ma`Proxy is a simple TCP proxy based on `Tornado `_. 6 | 7 | Well, maybe not that simple, since it supports: 8 | 9 | * TCP -> TCP 10 | simple reverse proxy. 11 | Whatever data goes in , goes out 12 | 13 | * TCP -> SSL 14 | proxy to encrypt incoming data. 15 | a.k.a stunnel 16 | 17 | * SSL -> TCP 18 | proxy to decrypt incoming data 19 | a.k.a SSL-terminator or SSL-decryptor 20 | 21 | * SSL- > SSL 22 | whatever gets in will be decrypted and then encrypted again 23 | 24 | * Each SSL can be used with SSL certificates. including client-certificates !! 25 | 26 | 27 | Examples: 28 | ---------- 29 | Let's start with the simplest example - no bells and whistles - a simple TCP proxy:: 30 | 31 | #!/usr/bin/env python 32 | import tornado.ioloop 33 | import maproxy.proxyserver 34 | 35 | # HTTP->HTTP: On your computer, browse to "http://127.0.0.1:81/" and you'll get http://www.google.com 36 | server = maproxy.proxyserver.ProxyServer("www.google.com",80) 37 | server.listen(81) 38 | print("http://127.0.0.1:81 -> http://www.google.com") 39 | tornado.ioloop.IOLoop.instance().start() 40 | 41 | We are creating a proxy (reverse proxy, to be more accurate) that listens locally on port 81 (0.0.0.0:81) 42 | and redirect all calls to www.google.com (port 80) . 43 | Note that: 44 | 1. This is NOT an HTTP-proxy , since it operates in the lower TCP layer . this proxy has nothing to do with HTTP 45 | 2. we are actually listening on all the IP addresses, not only on 127.0.0.1 . 46 | 47 | Now, Let's say that you'd like to listen on a "clear" (non-encrypted) connection but connect to an SSL website, 48 | for example - create a proxy http://127.0.0.1:82 -> https://127.0.0.1:443 , simply update the "server" line:: 49 | 50 | #!/usr/bin/env python 51 | import tornado.ioloop 52 | import maproxy.proxyserver 53 | 54 | # HTTP->HTTP: On your computer, browse to "http://127.0.0.1:81/" and you'll get http://www.google.com 55 | server = maproxy.proxyserver.ProxyServer("www.google.com",443,server_ssl_options=True) 56 | server.listen(82) 57 | print("http://127.0.0.1:82 -> https://www.google.com",) 58 | tornado.ioloop.IOLoop.instance().start() 59 | 60 | Alternatively, you can listen on SSL port and redirect the connection to a clear-text server. 61 | In order to listen on SSL-port, you need to specify SSL server-certificates as "client_ssl_options":: 62 | 63 | #!/usr/bin/env python 64 | import tornado.ioloop 65 | import maproxy.proxyserver 66 | 67 | # HTTPS->HTTP 68 | ssl_certs={ "certfile": "./certificate.pem", 69 | "keyfile": "./privatekey.pem" } 70 | # "client_ssl_options=ssl_certs" simply means "listen using SSL" 71 | server = maproxy.proxyserver.ProxyServer("www.google.com",80, 72 | client_ssl_options=ssl_certs) 73 | server.listen(83) 74 | print("https://127.0.0.1:83 -> http://www.google.com") 75 | tornado.ioloop.IOLoop.instance().start() 76 | 77 | 78 | In the "demos" section of the source-code, you will also find: 79 | 80 | * how to connect using SSL client-certificate 81 | * how to inherit the "Session" object (that we internally use) 82 | and create a logging-proxy (proxy that logs everything) . 83 | 84 | 85 | 86 | Installation: 87 | -------------- 88 | 89 | pip install maproxy 90 | 91 | **Source Code**: https://github.com/zferentz/maproxy 92 | 93 | **Contact Me**: zvika d-o-t ferentz a-t gmail d,o,t com *(if you can't figure it out - please don't contact me :) )* 94 | 95 | 96 | -------------------------------------------------------------------------------- /maproxy/proxyserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import tornado 3 | import tornado.tcpserver 4 | import maproxy.session 5 | 6 | 7 | 8 | 9 | class ProxyServer(tornado.tcpserver.TCPServer): 10 | """ 11 | TCP Proxy Server . 12 | 13 | """ 14 | def __init__(self, 15 | target_server,target_port, 16 | client_ssl_options=None,server_ssl_options=None, 17 | session_factory=maproxy.session.SessionFactory(), 18 | *args,**kwargs): 19 | """ 20 | ProxyServer initializer functin (constructor) . 21 | Input Parameters: 22 | target_server : the proxied-server IP 23 | target_port : the proxied-server port 24 | client_ssl_options : Configure this proxy as SSL terminator (decrypt all data). 25 | Standard Tornado's SSL options dictionary 26 | (e.g.: keyfile and certfile to specify Server-Certificate) 27 | server_ssl_options : Encrypt all outgoing data with SSL. this variables has 3 options: 28 | 1. True: enable SSL . default settings 29 | 2. False/None: disalbe SSL 30 | 3. Standard Tornado's SSL options dictionary 31 | (e.g.: keyfile and certfile to specify Client-Certificate) 32 | args,kwargs : will be passed directly to the Tornado engine 33 | """ 34 | assert(session_factory , issubclass(session_factory.__class__,maproxy.session.SessionFactory)) 35 | self.session_factory=session_factory 36 | 37 | 38 | # First, get the server's address and port . 39 | # This is the proxied server that we'll connect to 40 | self.target_server=target_server 41 | self.target_port=target_port 42 | 43 | # Now, remember the SSL potions 44 | # client_ssl_options : use it if you want an SSL listener (if you want that the proxy will have an SSL listener) 45 | # server_ssl_options: use it if you want an SSL connection to the proxy server (if your target server is SSL) 46 | self.client_ssl_options=client_ssl_options 47 | self.server_ssl_options=server_ssl_options 48 | 49 | # Nromalize SSL options: 50 | # If the server is SSL, the tornado expects a dictionary, so let's change the True to {} 51 | # If the caller provided "ssl=False", we need to make it None 52 | if self.server_ssl_options is True: 53 | self.server_ssl_options={} 54 | if self.server_ssl_options is False: 55 | self.server_ssl_options=None 56 | if self.client_ssl_options is False: 57 | self.client_ssl_options=None 58 | 59 | # Session-List 60 | self.SessionsList=[] 61 | 62 | # call Tornado's Engine . pass args/kwargs directly 63 | super(ProxyServer,self).__init__(ssl_options=self.client_ssl_options,*args,**kwargs) 64 | 65 | 66 | 67 | 68 | def handle_stream(self, stream, address): 69 | """ 70 | The proxy will call this function for every new connection as a callback 71 | This is the Session starting point: we initiate a new session and add it to the sessions-list 72 | """ 73 | assert isinstance(stream,tornado.iostream.IOStream) 74 | #session=maproxy.session.Session(stream,address,self) 75 | session=self.session_factory.new() # Use the factory to create new session 76 | session.new_connection(stream,address,self) 77 | self.SessionsList.append(session) 78 | 79 | def remove_session(self,session): 80 | assert ( isinstance(session, maproxy.session.Session) ) 81 | assert ( session.p2s_state==maproxy.session.Session.State.CLOSED ) 82 | assert ( session.c2p_state ==maproxy.session.Session.State.CLOSED ) 83 | self.SessionsList.remove(session) 84 | self.session_factory.delete(session) 85 | 86 | def get_connections_count(self): 87 | return len(self.SessionsList) -------------------------------------------------------------------------------- /demos/logging_proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # logging_proxy.py: Demonstrates how to inherit the Session class so that we can easily get notifications of I/O events . 4 | # The idea - by overriding 6 simple function, we get all the I/O events we need in order to fully monitor 5 | # the connection (new connnection,got-data, close-connection) 6 | 7 | 8 | import tornado.ioloop 9 | import maproxy.proxyserver 10 | import maproxy.session 11 | import string # for the "filter" 12 | 13 | 14 | 15 | 16 | class LoggingSession(maproxy.session.Session): 17 | """ 18 | This class simply overrides the major "session" functions of the parent-class. 19 | The idea is very simple: every time the proxy has done "something" , we will 20 | "intercept" the call , print the data and . 21 | Note that the actually code is very small. all my comments and explanations taking too much space ! 22 | 23 | There are 6 functions that we would like to monitor: 24 | - new_connection : New session 25 | - on_p2s_done_connect : When the session is connected to the server 26 | - on_c2p_done_read : C->S data 27 | - on_p2s_done_read : C<-S data 28 | - on_c2p_close : Client closes he connection 29 | - on_p2s_close : Server closes he connection 30 | 31 | """ 32 | 33 | # Class variable: counter for the number of connections 34 | running_counter=0 35 | 36 | 37 | def __init__(self,*args,**kwargs): 38 | """ 39 | Currently overriding the "__init__" is not really required since the parent's 40 | __init__ is doing absolutely nothing, but it is a good practice for 41 | the future... (future updates) 42 | """ 43 | super(LoggingSession,self).__init__(*args,**kwargs) 44 | 45 | 46 | #def new_connection(self,stream : tornado.iostream.IOStream ,address,proxy): 47 | def new_connection(self,stream ,address,proxy): 48 | """ 49 | Override the maproxy.session.Session.new_connection() function 50 | This function is called by the framework (proxyserver) for every new session 51 | """ 52 | # Let's increment the "autonumber" (remember: this is single-threaded, so on lock is required) 53 | LoggingSession.running_counter+=1 54 | self.connid=LoggingSession.running_counter 55 | print("#%-3d: New Connection on %s" % (self.connid,address)) 56 | super(LoggingSession,self).new_connection(stream,address,proxy) 57 | 58 | def on_p2s_done_connect(self): 59 | """ 60 | Override the maproxy.session.Session.on_p2s_done_connect() function 61 | This function is called by the framework (proxyserver) when the session is 62 | connected to the target-server 63 | """ 64 | print("#%-3d: Server connected" % (self.connid)) 65 | super(LoggingSession,self).on_p2s_done_connect() 66 | 67 | 68 | 69 | def on_c2p_done_read(self,data): 70 | """ 71 | Override the maproxy.session.Session.on_c2p_done_read(data) function 72 | This function is called by the framework (proxyserver) when we get data from the client 73 | (to the target-server) 74 | """ 75 | # First, let's call the parent-class function (on_cp2_done_read), 76 | # this will minimize network delay and complete the operation 77 | super(LoggingSession,self).on_c2p_done_read(data) 78 | 79 | # Now let simply print the data (print just the printable characters) 80 | print("#%-3d:C->S (%d bytes):\n%s" % (self.connid,len(data),filter(lambda x: x in string.printable, data)) ) 81 | 82 | 83 | def on_p2s_done_read(self,data): 84 | """ 85 | Override the maproxy.session.Session.on_p2s_done_read(data) function 86 | This function is called by the framework (proxyserver) when we get data from the server 87 | (to the client) 88 | """ 89 | # First, let's call the parent-class function (on_p2s_done_read), 90 | # this will minimize network delay and complete the operation 91 | super(LoggingSession,self).on_p2s_done_read(data) 92 | 93 | # Now let simply print the data (print just the printable characters) 94 | print("#%-3d:C<-S (%d bytes):\n%s" % (self.connid,len(data),filter(lambda x: x in string.printable, data)) ) 95 | 96 | def on_c2p_close(self): 97 | """ 98 | Override the maproxy.session.Session.on_c2p_close() function. 99 | This function is called by the framework (proxyserver) when the client closes the connection 100 | """ 101 | print("#%-3d: C->S Closed" % (self.connid)) 102 | super(LoggingSession,self).on_c2p_close() 103 | 104 | def on_p2s_close(self): 105 | """ 106 | Override the maproxy.session.Session.on_p2s_close() function. 107 | This function is called by the framework (proxyserver) when the server closes the connection 108 | """ 109 | print("#%-3d: C<-S Closed" % (self.connid)) 110 | super(LoggingSession,self).on_p2s_close() 111 | 112 | 113 | 114 | 115 | 116 | class LoggingSessionFactory(maproxy.session.SessionFactory): 117 | """ 118 | This session-factory will be used by the proxy when new sessions 119 | need to be generated . 120 | We only need a "new" function that will generate a session object 121 | that derives from maproxy.session.Session. 122 | The session that we create is our lovely LoggingSession that we declared 123 | earlier 124 | """ 125 | def __init__(self): 126 | super(LoggingSessionFactory,self).__init__() 127 | def new(self,*args,**kwargs): 128 | return LoggingSession(*args,**kwargs) 129 | 130 | 131 | 132 | 133 | # HTTP->HTTP 134 | # On your computer, browse to "http://127.0.0.1:81/" and you'll get http://www.google.com 135 | # The only "special" argument is the "session_factory" that ponits to a new instance of LoggingSessionFactory. 136 | # By using our special session-factory, the proxy will create the 137 | # LoggingSession instances (instead of default Session instances) 138 | server = maproxy.proxyserver.ProxyServer("www.google.com",80,session_factory=LoggingSessionFactory()) 139 | server.listen(81) 140 | print("http://127.0.0.1:81 -> http://www.google.com") 141 | tornado.ioloop.IOLoop.instance().start() 142 | -------------------------------------------------------------------------------- /maproxy/iomanager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import tornado.tcpserver 4 | import threading 5 | import time 6 | import os 7 | import functools 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | class IOManager(object): 17 | """ 18 | The IOManager is responsible for managing one or more servers/proxies. 19 | You can add/remove servers/proxies as well as manage (start/stop/...) them. 20 | """ 21 | def __init__(self): 22 | self._ioloop_thread=None 23 | self._servers={} # id->server 24 | self._ioloop=tornado.ioloop.IOLoop.instance(); 25 | 26 | # Some "status flags" - so external entities will be able to be notified... 27 | self._running=threading.Event() 28 | self._stopping=threading.Event() 29 | self._stopped=threading.Event() 30 | self._stopped.set() 31 | 32 | 33 | 34 | def __del__(self): 35 | self._ioloop.close() 36 | 37 | def get_servers_count(self): 38 | return len(self._servers) 39 | 40 | def get_connections_count(self): 41 | n=0 42 | for id,server in self._servers.items(): 43 | assert isinstance(server , tornado.tcpserver.TCPServer) 44 | n+=server.get_connections_count() 45 | return n 46 | 47 | 48 | 49 | 50 | def ioloop(self): 51 | return self._ioloop 52 | 53 | #def add(self,server : tornado.tcpserver.TCPServer ): 54 | def add(self,server ): 55 | """ 56 | Add a TCPServer instace (or derivation) 57 | """ 58 | assert isinstance(server, tornado.tcpserver.TCPServer) 59 | assert self._servers.get(id(server)) is None , "Already Exists" 60 | self._servers[id(server)]=server 61 | return id(server) 62 | 63 | def remove(self,server): 64 | server.stop() 65 | del self._servers[id(server)] 66 | 67 | 68 | 69 | #def start(self,thread:bool=True ): 70 | def start(self,thread=True ): 71 | """ 72 | Start to listen on all servers, and start the IOLoop 73 | """ 74 | for id,server in self._servers.items(): 75 | assert isinstance(server , tornado.tcpserver.TCPServer) 76 | server .start() 77 | 78 | if os.name=="nt": 79 | # On Windows, add a simple callback (freq:1sec) to display current number of connections 80 | import ctypes 81 | self.fan="|/-\\" 82 | self.fan_index=0 83 | 84 | def timeout(): 85 | status="" 86 | if self._running.is_set(): 87 | status+="running " 88 | else: 89 | status+="not-running" 90 | if self._stopping.is_set(): 91 | status+="stopping " 92 | ctypes.windll.kernel32.SetConsoleTitleA( "%c %s (%d Connections) "% (self.fan[self.fan_index],status,self.get_connections_count())) 93 | self.fan_index=(self.fan_index+1) % 4 94 | self._ioloop.add_timeout( time.time()+1 , timeout) 95 | 96 | self._ioloop.add_timeout( time.time()+1 , timeout) 97 | 98 | self._stopped.clear() 99 | self._running.set() 100 | 101 | # The next call is the blocking call 102 | 103 | if not thread: 104 | # Run the ioloop in this thread (default, blocking call) 105 | self._ioloop_thread=None 106 | self._ioloop.start() 107 | else: 108 | # run the ioloop in a new thread 109 | def _ioloop_thread(): 110 | self._ioloop.start() 111 | self._ioloop_thread=threading.Thread( target=_ioloop_thread) 112 | self._ioloop_thread.start() 113 | 114 | 115 | def stop(self,gracefully=False,wait=False): 116 | """ 117 | Stop the servers. By default, this function stops the server immediately (not-gracefully) , 118 | all current connections will be terminated. You can set this behavior using the "gracefully" parametr. 119 | gracefully = True: wait forever 120 | gracefully = False: don't wait at all . terminate 121 | gracefull = timeout (integer/float) : how much time (seconds) to wait... 122 | NOTE: This is a nonblocking stop. it means that it will START the stop procedure but will not wait 123 | In fact, I don't think that it's possible to have a blocking stop in this layer/level 124 | (since we assume that this will be a callback from within ab IOLoop-callback..) 125 | 126 | wait : if the ioloop was started as another thread, you can "wait" for the stop operation 127 | 128 | TODO (feature): terminate with RST (linger) , ... 129 | """ 130 | if self._ioloop_thread and self._ioloop_thread.ident != threading.get_ident(): 131 | # If called from another thread - run this procedure from the ioloop... 132 | self._ioloop.add_callback ( g_IOManager.stop , gracefully=gracefully) 133 | if wait: 134 | self._ioloop_thread.join() 135 | return 136 | assert wait is False , "You cannot run stop(wait=True) while not starting with start(thread=True)" 137 | 138 | 139 | self._stopping.set() 140 | 141 | def stop_procedure(): 142 | self._ioloop.stop() 143 | self._running.clear() 144 | self._stopping.clear() 145 | self._stopped.set() 146 | 147 | def stop_if_no_connections(deadline): 148 | current_time=time.time() 149 | if self.get_connections_count() == 0 or (deadline and deadline-current_time<=0): 150 | # No more connections , or deadline has been reached/exceeded 151 | stop_procedure() 152 | return 153 | 154 | # We either need to wait 1-second (untless next-check), or less 155 | wait_time=current_time+1 if deadline is None else min( current_time+1,deadline) 156 | self._ioloop.add_timeout( wait_time , functools.partial(stop_if_no_connections ,deadline) ) 157 | 158 | 159 | 160 | 161 | # First, stop listening... 162 | for id,server in self._servers.items(): 163 | assert isinstance(server , tornado.tcpserver.TCPServer) 164 | server.stop() 165 | if not gracefully or self.get_connections_count()==0: 166 | stop_procedure() 167 | return 168 | 169 | if gracefully is True: 170 | gracefully_timeout=None # Wait forever... 171 | else: 172 | assert( isinstance(gracefully,int) or isinstance(gracefully,float) ) 173 | gracefully_timeout=time.time()+gracefully 174 | 175 | self._ioloop.add_callback(stop_if_no_connections , gracefully_timeout) 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /maproxy/session.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import tornado 4 | import socket 5 | import maproxy.proxyserver 6 | 7 | 8 | 9 | class Session(object): 10 | """ 11 | The Session class if the heart of the system. 12 | - We create the session when a client connects to the server (proxy). this connection is "c2p" 13 | - We create a connection to the server (p2s) 14 | - Each connection (c2p,p2s) has a state (Session.State) , can be CLOSED,CONNECTING,CONNECTED 15 | - Initially, when c2p is created we : 16 | - create the p-s connection 17 | - start read from c2p 18 | - Completion Routings: 19 | - on_XXX_done_read: 20 | When we get data from one side, we initiate a "start_write" to the other side . 21 | Exception: if the target is not connected yet, we queue the data so we can send it later 22 | - on_p2s_connected: 23 | When p2s is connected ,we start read from the server . 24 | if queued data is available (data that was sent from the c2p) we initiate a "start_write" immediately 25 | - on_XXX_done_write: 26 | When we're done "sending" data , we check if there's more data to send in the queue. 27 | if there is - we initiate another "start_write" with the queued data 28 | - on_XXX_close: 29 | When one side closes the connection, we either initiate a "start_close" on the other side, or (if already closed) - remove the session 30 | - I/O routings: 31 | - XXX_start_read: simply start read from the socket (we assume and validate that only one read goes at a time) 32 | - XXX_start_write: if currently writing , add data to queue. if not writing - perform io_write... 33 | 34 | 35 | 36 | 37 | """ 38 | class LoggerOptions: 39 | """ 40 | Logging options - which messages/notifications we would like to log... 41 | The logging is for development&maintenance. In production set all to False 42 | """ 43 | # Log charactaristics 44 | LOG_SESSION_ID=True # for each log, add the session-id 45 | # Log different operations 46 | LOG_NEW_SESSION_OP=False 47 | LOG_READ_OP=False 48 | LOG_WRITE_OP=False 49 | LOG_CLOSE_OP=False 50 | LOG_CONNECT_OP=False 51 | LOG_REMOVE_SESSION=False 52 | 53 | class State: 54 | """ 55 | Each socket has a state. 56 | We will use the state to identify whether the connection is open or closed 57 | """ 58 | CLOSED,CONNECTING,CONNECTED=range(3) 59 | 60 | def __init__(self): 61 | pass 62 | #def new_connection(self,stream : tornado.iostream.IOStream ,address,proxy): 63 | def new_connection(self,stream ,address,proxy): 64 | # First,validation 65 | assert isinstance(proxy,maproxy.proxyserver.ProxyServer) 66 | assert isinstance(stream,tornado.iostream.IOStream) 67 | 68 | # Logging 69 | self.logger_nesting_level=0 # logger_nesting_level is the current "nesting level" 70 | if Session.LoggerOptions.LOG_NEW_SESSION_OP: 71 | self.log("New Session") 72 | 73 | 74 | # Remember our "parent" ProxyServer 75 | self.proxy=proxy 76 | 77 | # R/W flags for each socket 78 | # Using the flags, we can tell if we're waiting for I/O completion 79 | # NOTE: the "c2p" and "p2s" prefixes are NOT the direction of the IO, 80 | # they represent the SOCKETS : 81 | # c2p means the socket from the client to the proxy 82 | # p2s means the socket from the proxy to the server 83 | self.c2p_reading=False # whether we're reading from the client 84 | self.c2p_writing=False # whether we're writing to the client 85 | self.p2s_writing=False # whether we're writing to the server 86 | self.p2s_reading=False # whether we're reading from the server 87 | 88 | # Init the Client->Proxy stream 89 | self.c2p_stream=stream 90 | self.c2p_address=address 91 | # Client->Proxy is connected 92 | self.c2p_state=Session.State.CONNECTED 93 | 94 | # Here we will put incoming data while we're still waiting for the target-server's connection 95 | self.c2s_queued_data=[] # Data that was read from the Client, and needs to be sent to the Server 96 | self.s2c_queued_data=[] # Data that was read from the Server , and needs to be sent to the client 97 | 98 | # send data immediately to the client ... (Disable Nagle TCP algorithm) 99 | self.c2p_stream.set_nodelay(True) 100 | # Let us now when the client disconnects (callback on_c2p_close) 101 | self.c2p_stream.set_close_callback( self.on_c2p_close) 102 | 103 | # Create the Proxy->Server socket and stream 104 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) 105 | 106 | if self.proxy.server_ssl_options is not None: 107 | # if the "server_ssl_options" where specified, it means that when we connect, we need to wrap with SSL 108 | # so we need to use the SSLIOStream stream 109 | self.p2s_stream = tornado.iostream.SSLIOStream(s,ssl_options=self.proxy.server_ssl_options) 110 | else: 111 | # use the standard IOStream stream 112 | self.p2s_stream = tornado.iostream.IOStream(s) 113 | # send data immediately to the server... (Disable Nagle TCP algorithm) 114 | self.p2s_stream.set_nodelay(True) 115 | 116 | # Let us now when the server disconnects (callback on_p2s_close) 117 | self.p2s_stream.set_close_callback( self.on_p2s_close ) 118 | # P->S state is "connecting" 119 | self.p2s_state=self.p2s_state=Session.State.CONNECTING 120 | self.p2s_stream.connect(( proxy.target_server, proxy.target_port), self.on_p2s_done_connect ) 121 | 122 | 123 | # We can actually start reading immediatelly from the C->P socket 124 | self.c2p_start_read() 125 | 126 | # Each member-function can call this method to log data (currently to screen) 127 | def log(self,msg): 128 | prefix=str(id(self))+":" if Session.LoggerOptions.LOG_SESSION_ID else "" 129 | prefix+=self.logger_nesting_level*" "*4 130 | logging.debug(prefix + msg) 131 | 132 | 133 | 134 | # Logging decorator (enter/exit) 135 | def logger(enabled=True): 136 | """ 137 | We use this decorator to wrap functions and log the input/ouput of each function 138 | Since this decorator accepts a parameter, it must return an "inner" decorator....(Python stuff) 139 | """ 140 | def inner_decorator(func): 141 | 142 | 143 | def log_wrapper(self,*args,**kwargs): 144 | 145 | msg="%s (%s,%s)" % (func.__name__,args,kwargs) 146 | 147 | 148 | self.log(msg) 149 | self.logger_nesting_level+=1 150 | r=func(self,*args,**kwargs) 151 | self.logger_nesting_level-=1 152 | self.log("%s -> %s" % (msg,str(r)) ) 153 | return r 154 | return log_wrapper if enabled else func 155 | 156 | 157 | return inner_decorator 158 | 159 | 160 | 161 | ################ 162 | ## Start Read ## 163 | ################ 164 | @logger(LoggerOptions.LOG_READ_OP) 165 | def c2p_start_read(self): 166 | """ 167 | Start read from client 168 | """ 169 | assert( not self.c2p_reading) 170 | self.c2p_reading=True 171 | try: 172 | self.c2p_stream.read_until_close(lambda x: None,self.on_c2p_done_read) 173 | except tornado.iostream.StreamClosedError: 174 | self.c2p_reading=False 175 | 176 | @logger(LoggerOptions.LOG_READ_OP) 177 | def p2s_start_read(self): 178 | """ 179 | Start read from server 180 | """ 181 | assert( not self.p2s_reading) 182 | self.p2s_reading=True 183 | try: 184 | self.p2s_stream.read_until_close(lambda x:None,self.on_p2s_done_read) 185 | except tornado.iostream.StreamClosedError: 186 | self.p2s_reading=False 187 | 188 | 189 | ############################## 190 | ## Read Completion Routines ## 191 | ############################## 192 | @logger(LoggerOptions.LOG_READ_OP) 193 | def on_c2p_done_read(self,data): 194 | # # We got data from the client (C->P ) . Send data to the server 195 | assert(self.c2p_reading) 196 | assert(data) 197 | self.p2s_start_write(data) 198 | 199 | 200 | @logger(LoggerOptions.LOG_READ_OP) 201 | def on_p2s_done_read(self,data): 202 | # got data from Server to Proxy . if the client is still connected - send the data to the client 203 | assert( self.p2s_reading) 204 | assert(data) 205 | self.c2p_start_write(data) 206 | 207 | 208 | ##################### 209 | ## Write to stream ## 210 | ##################### 211 | @logger(LoggerOptions.LOG_WRITE_OP) 212 | def _c2p_io_write(self,data): 213 | if data is None: 214 | # None means (gracefully) close-socket (a "close request" that was queued...) 215 | self.c2p_state=Session.State.CLOSED 216 | try: 217 | self.c2p_stream.close() 218 | except tornado.iostream.StreamClosedError: 219 | self.c2p_writing=False 220 | else: 221 | self.c2p_writing=True 222 | try: 223 | self.c2p_stream.write(data,callback=self.on_c2p_done_write) 224 | except tornado.iostream.StreamClosedError: 225 | # Cancel the write, we will get on_close instead... 226 | self.c2p_writing=False 227 | @logger(LoggerOptions.LOG_WRITE_OP) 228 | def _p2s_io_write(self,data): 229 | if data is None: 230 | # None means (gracefully) close-socket (a "close request" that was queued...) 231 | self.p2s_state=Session.State.CLOSED 232 | try: 233 | self.p2s_stream.close() 234 | except tornado.iostream.StreamClosedError: 235 | # Cancel the write. we will get on_close instead 236 | self.p2s_writing=False 237 | else: 238 | self.p2s_writing=True 239 | try: 240 | self.p2s_stream.write(data,callback=self.on_p2s_done_write) 241 | except tornado.iostream.StreamClosedError: 242 | # Cancel the write. we will get on_close instead 243 | self.p2s_writing=False 244 | 245 | 246 | ################# 247 | ## Start Write ## 248 | ################# 249 | @logger(LoggerOptions.LOG_WRITE_OP) 250 | def c2p_start_write(self,data): 251 | """ 252 | Write to client.if there's a pending write-operation, add it to the S->C (s2c) queue 253 | """ 254 | # If not connected - do nothing... 255 | if self.c2p_state != Session.State.CONNECTED: return 256 | 257 | if not self.c2p_writing: 258 | # If we're not currently writing 259 | assert( not self.s2c_queued_data ) # we expect the queue to be empty 260 | 261 | # Start the "real" write I/O operation 262 | self._c2p_io_write(data) 263 | else: 264 | # Just add to the queue 265 | self.s2c_queued_data.append(data) 266 | 267 | @logger(LoggerOptions.LOG_WRITE_OP) 268 | def p2s_start_write(self,data): 269 | """ 270 | Write to the server. 271 | If not connected yet - queue the data 272 | If there's a pending write-operation , add it to the C->S (c2s) queue 273 | """ 274 | 275 | # If still connecting to the server - queue the data... 276 | if self.p2s_state == Session.State.CONNECTING: 277 | self.c2s_queued_data.append(data) # TODO: is it better here to append (to list) or concatenate data (to buffer) ? 278 | return 279 | # If not connected - do nothing 280 | if self.p2s_state == Session.State.CLOSED: 281 | return 282 | assert(self.p2s_state == Session.State.CONNECTED) 283 | 284 | if not self.p2s_writing: 285 | # Start the "real" write I/O operation 286 | self._p2s_io_write(data) 287 | else: 288 | # Just add to the queue 289 | self.c2s_queued_data.append(data) 290 | 291 | 292 | ############################## 293 | ## Write Competion Routines ## 294 | ############################## 295 | @logger(LoggerOptions.LOG_WRITE_OP) 296 | def on_c2p_done_write(self): 297 | """ 298 | A start_write C->P (write to client) is done . 299 | if there is queued-data to send - send it 300 | """ 301 | assert(self.c2p_writing) 302 | if self.s2c_queued_data: 303 | # more data in the queue, write next item as well.. 304 | self._c2p_io_write( self.s2c_queued_data.pop(0)) 305 | return 306 | self.c2p_writing=False 307 | 308 | 309 | 310 | @logger(LoggerOptions.LOG_WRITE_OP) 311 | def on_p2s_done_write(self): 312 | """ 313 | A start_write P->S (write to server) is done . 314 | if there is queued-data to send - send it 315 | """ 316 | assert(self.p2s_writing) 317 | if self.c2s_queued_data: 318 | # more data in the queue, write next item as well.. 319 | self._p2s_io_write( self.c2s_queued_data.pop(0)) 320 | return 321 | self.p2s_writing=False 322 | 323 | 324 | 325 | ###################### 326 | ## Close Connection ## 327 | ###################### 328 | @logger(LoggerOptions.LOG_CLOSE_OP) 329 | def c2p_start_close(self,gracefully=True): 330 | """ 331 | Close c->p connection 332 | if gracefully is True then we simply add None to the queue, and start a write-operation 333 | if gracefully is False then this is a "brutal" close: 334 | - mark the stream is closed 335 | - we "reset" (empty) the queued-data 336 | - if the other side (p->s) already closed, remove the session 337 | 338 | """ 339 | if self.c2p_state == Session.State.CLOSED: 340 | return 341 | if gracefully: 342 | self.c2p_start_write(None) 343 | return 344 | 345 | self.c2p_state = Session.State.CLOSED 346 | self.s2c_queued_data=[] 347 | self.c2p_stream.close() 348 | if self.p2s_state == Session.State.CLOSED: 349 | self.remove_session() 350 | 351 | 352 | @logger(LoggerOptions.LOG_CLOSE_OP) 353 | def p2s_start_close(self,gracefully=True): 354 | """ 355 | Close p->s connection 356 | if gracefully is True then we simply add None to the queue, and start a write-operation 357 | if gracefully is False then this is a "brutal" close: 358 | - mark the stream is closed 359 | - we "reset" (empty) the queued-data 360 | - if the other side (p->s) already closed, remove the session 361 | 362 | """ 363 | if self.p2s_state == Session.State.CLOSED: 364 | return 365 | if gracefully: 366 | self.p2s_start_write(None) 367 | return 368 | 369 | self.p2s_state = Session.State.CLOSED 370 | self.c2s_queued_data=[] 371 | self.p2s_stream.close() 372 | if self.c2p_state == Session.State.CLOSED: 373 | self.remove_session() 374 | 375 | 376 | @logger(LoggerOptions.LOG_CLOSE_OP) 377 | def on_c2p_close(self): 378 | """ 379 | Client closed the connection. 380 | we need to: 381 | 1. update the c2p-state 382 | 2. if there's no more data to the server (c2s_queued_data is empty) - we can close the p2s connection 383 | 3. if p2s already closed - we can remove the session 384 | """ 385 | self.c2p_state=Session.State.CLOSED 386 | if self.p2s_state == Session.State.CLOSED: 387 | self.remove_session() 388 | else: 389 | self.p2s_start_close(gracefully=True) 390 | 391 | 392 | @logger(LoggerOptions.LOG_CLOSE_OP) 393 | def on_p2s_close(self): 394 | """ 395 | Server closed the connection. 396 | We need to update the satte, and if the client closed as well - delete the session 397 | """ 398 | self.p2s_state=Session.State.CLOSED 399 | if self.c2p_state == Session.State.CLOSED: 400 | self.remove_session() 401 | else: 402 | self.c2p_start_close(gracefully=True) 403 | 404 | ######################## 405 | ## Connect-Completion ## 406 | ######################## 407 | @logger(LoggerOptions.LOG_CONNECT_OP) 408 | def on_p2s_done_connect(self): 409 | assert(self.p2s_state==Session.State.CONNECTING) 410 | self.p2s_state=Session.State.CONNECTED 411 | # Start reading from the socket 412 | self.p2s_start_read() 413 | assert(not self.p2s_writing) # As expect no current write-operation ... 414 | 415 | # If we have pending-data to write, start writing... 416 | if self.c2s_queued_data: 417 | # TRICKY: get thte frst item , and write it... 418 | # this is tricky since the "start-write" will 419 | # write this item even if there are queued-items... (since self.p2s_writing=False) 420 | self.p2s_start_write( self.c2s_queued_data.pop(0) ) 421 | 422 | ########### 423 | ## UTILS ## 424 | ########### 425 | @logger(LoggerOptions.LOG_REMOVE_SESSION) 426 | def remove_session(self): 427 | self.proxy.remove_session(self) 428 | 429 | 430 | class SessionFactory(object): 431 | """ 432 | This is the default session-factory. it simply returns a "Session" object 433 | """ 434 | def __init__(self): 435 | pass 436 | 437 | def new(self,*args,**kwargs): 438 | """ 439 | The caller needs a Session objet (constructed with *args,**kwargs). 440 | In this implementation we're simply creating a new object. you can enhance and create a pool or add logs.. 441 | """ 442 | return Session(*args,**kwargs) 443 | def delete(self,session): 444 | """ 445 | Delete a session object 446 | """ 447 | assert( isinstance(session,Session)) 448 | del session 449 | --------------------------------------------------------------------------------