├── README.md ├── .gitignore ├── .project ├── .pydevproject └── src └── ptunnel.py /README.md: -------------------------------------------------------------------------------- 1 | PTUNNEL 2 | ======= 3 | 4 | Small python utility to tunnel connection though HTTPS proxy - via CONNECT method. 5 | 6 | Provided freely for any use via GPL v3 license. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #* 2 | *$ 3 | *.BAK 4 | *.BASE 5 | *.OTHER 6 | *.THIS 7 | *.Z 8 | *.bak 9 | *.class 10 | *.elc 11 | *.ln 12 | *.log 13 | *.o 14 | *.obj 15 | *.olb 16 | *.old 17 | *.orig 18 | *.pyc 19 | *.pyo 20 | *.rej 21 | *~ 22 | ,* 23 | .#* 24 | .DS_Store 25 | .bzr 26 | .del-* 27 | .make.state 28 | .nse_depinfo 29 | .svn 30 | .~*~ 31 | CVS.adm 32 | RCS 33 | RCSLOG 34 | SCCS 35 | _$* 36 | _svn 37 | src/connect_test.py 38 | src/test.py 39 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | ptunnel 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Default 6 | python 2.6 7 | 8 | /ptunnel/src 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/ptunnel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Created on Dec 25, 2010 4 | 5 | @author: ivan 6 | ''' 7 | 8 | import SocketServer 9 | import socket 10 | import optparse 11 | import logging 12 | logging.basicConfig() 13 | log=logging.root 14 | import sys 15 | import threading 16 | import os 17 | import urlparse 18 | 19 | REMOTE_TIMEOUT_CONNECT=10 20 | REMOTE_TIMEOUT_RECEIVE=None 21 | 22 | class ArgumentError(ValueError): pass 23 | 24 | def _define_options(oparser): 25 | oparser.add_option('-v', '--verbose', action='store_true', help="print more information") 26 | oparser.add_option('-d', '--direct', action='store_true', ) 27 | oparser.add_option('', '--debug', action='store_true') 28 | oparser.add_option('-p', '--proxy') 29 | 30 | def _parse_args(args): 31 | parsed_args=[] 32 | if len(args)<1: 33 | raise ArgumentError("Tunnel definition arguments are mandatory") 34 | for item in args: 35 | tunnel_def=item.split(':') 36 | if len(tunnel_def)!= 3: 37 | raise ArgumentError("Invalid tunnel def - must have 3 parts") 38 | try: 39 | for idx in [0,2]: 40 | tunnel_def[idx]=int(tunnel_def[idx]) 41 | except: 42 | raise ArgumentError("First and third value in tunnel definition must be int") 43 | parsed_args.append(tunnel_def) 44 | return parsed_args 45 | 46 | 47 | def _system_proxy_url(): 48 | proxy=os.environ.get('http_proxy') 49 | if proxy: 50 | return urlparse.urlparse(proxy).netloc 51 | 52 | 53 | 54 | def _parse_options(oparser, args): 55 | opts, args=oparser.parse_args(args) 56 | 57 | proxy=opts.proxy or _system_proxy_url() 58 | if not proxy: 59 | raise ArgumentError("Proxy must be defined") 60 | proxy=proxy.split(':') 61 | if len(proxy)!=2: 62 | raise ArgumentError("Proxy definition must be in form host:port") 63 | try: 64 | proxy[1]=int(proxy[1]) 65 | except: 66 | raise ArgumentError("Port in proxy definition must be numeric") 67 | opts.proxy=proxy 68 | args=_parse_args(args) 69 | 70 | return opts, args 71 | 72 | servers=[] 73 | class TunnelServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): 74 | def __init__(self, tunnel, proxy, direct): 75 | self.tunnel=tunnel 76 | self.proxy=proxy 77 | self.direct_as_fallback=direct 78 | SocketServer.TCPServer.__init__(self,('localhost', tunnel[0]),Tunnel) 79 | 80 | class BackForwarder(threading.Thread): 81 | def __init__(self, remote_socket, server_socket, closed_callback=None): 82 | super(BackForwarder, self).__init__() 83 | self.setDaemon(True) 84 | self.remote_socket=remote_socket 85 | self.server_socket=server_socket 86 | self.closed_callback=closed_callback 87 | self.start() 88 | 89 | def run(self): 90 | self.remote_socket.settimeout(REMOTE_TIMEOUT_RECEIVE) 91 | while 1: 92 | try: 93 | data = self.remote_socket.recv( 1024 ) 94 | if not data: break 95 | self.server_socket.send( data ) 96 | except Exception, e: 97 | log.debug("Remote Connection closed (%s, %s)"% (str(type(e)), str(e))) 98 | break 99 | log.info("connection to %s closed" % str(self.remote_socket)) 100 | if self.closed_callback: 101 | self.closed_callback() 102 | 103 | class Tunnel(SocketServer.BaseRequestHandler): 104 | def connect_remote(self): 105 | self.remote_socket=socket.socket(socket.AF_INET, socket.SOCK_STREAM) 106 | self.remote_socket.settimeout(REMOTE_TIMEOUT_CONNECT) 107 | self.remote_socket.connect(tuple(self.server.tunnel[1:])) 108 | self.remote_disconnected=False 109 | 110 | CONNECT = "CONNECT %s:%d HTTP/1.0\r\n\r\n" 111 | def connect_remote_via_proxy(self): 112 | sock = socket.create_connection(self.server.proxy) 113 | sock.sendall(Tunnel.CONNECT % tuple(self.server.tunnel[1:])) 114 | s = "" 115 | while s[-4:] != "\r\n\r\n": 116 | s += sock.recv(1) 117 | print repr(s) 118 | self.remote_socket=sock 119 | self.remote_disconnected=False 120 | 121 | def notify_closed_remote(self): 122 | log.debug("Trying to end server connection") 123 | self.remote_disconnected=True 124 | 125 | 126 | def handle(self): 127 | log.info("Client %s connected for tunnel %s" % (str(self.client_address), str(self.server.tunnel))) 128 | 129 | try: 130 | try: 131 | self.connect_remote_via_proxy() 132 | except IOError, e: 133 | log.warning("Connection to proxy failed with %s" % str(e)) 134 | if self.server.direct_as_fallback: 135 | self.connect_remote() 136 | else: 137 | raise 138 | except: 139 | log.exception("Connection to remote server %s failed" % str(self.server.tunnel[1:])) 140 | return 141 | 142 | fwd=BackForwarder(self.remote_socket, self.request, self.notify_closed_remote) 143 | self.request.settimeout(1) 144 | while not self.remote_disconnected: 145 | try: 146 | data = self.request.recv( 1024 ) 147 | if not data: break 148 | self.remote_socket.send( data ) 149 | except socket.timeout: 150 | pass 151 | except Exception, e: 152 | log.info("Local Connection closed by server (%s, %s)"% (str(type(e)), str(e))) 153 | break 154 | self.remote_socket.close() 155 | log.info("Client %s left" % str(self.client_address)) 156 | 157 | class ServerThread(threading.Thread): 158 | def __init__(self, tunnel, opts): 159 | self.server=TunnelServer(tunnel, opts.proxy, opts.direct) 160 | super(ServerThread, self).__init__() 161 | 162 | def run(self): 163 | self.server.serve_forever() 164 | 165 | def wait_to_terminate(): 166 | for s in servers: 167 | s.join() 168 | 169 | def start_servers(args, opts): 170 | for tunnel in args: 171 | s=ServerThread(tunnel, opts) 172 | servers.append(s) 173 | s.start() 174 | 175 | def main(args=sys.argv[1:]): 176 | oparser=optparse.OptionParser(usage= "%s [options] port:host:port [port:host:port ...]") 177 | _define_options(oparser) 178 | try: 179 | opts, args = _parse_options(oparser, args) 180 | except (optparse.OptionError, ArgumentError): 181 | log.exception("Invalid arguments") 182 | oparser.print_usage() 183 | sys.exit(1) 184 | if opts.verbose: 185 | log.setLevel(logging.INFO) 186 | if opts.debug: 187 | log.setLevel(logging.DEBUG) 188 | log.info('Tunelling %s' % ', '.join(map(lambda x: "%d->%s:%d" % tuple(x),args))) 189 | 190 | start_servers(args, opts) 191 | wait_to_terminate() 192 | 193 | if __name__=='__main__': 194 | main() 195 | 196 | 197 | --------------------------------------------------------------------------------