├── 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 |
--------------------------------------------------------------------------------