##// END OF EJS Templates
Fix ssh tunnel for Python3...
Zahari -
Show More
@@ -1,356 +1,357 b''
1 """Basic ssh tunnel utilities, and convenience functions for tunneling
1 """Basic ssh tunnel utilities, and convenience functions for tunneling
2 zeromq connections.
2 zeromq connections.
3
3
4 Authors
4 Authors
5 -------
5 -------
6 * Min RK
6 * Min RK
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2010-2011 The IPython Development Team
10 # Copyright (C) 2010-2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16
16
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 from __future__ import print_function
22 from __future__ import print_function
23
23
24 import os,sys, atexit
24 import os,sys, atexit
25 import signal
25 import signal
26 import socket
26 import socket
27 from multiprocessing import Process
27 from multiprocessing import Process
28 from getpass import getpass, getuser
28 from getpass import getpass, getuser
29 import warnings
29 import warnings
30 from IPython.utils.py3compat import xrange
30
31
31 try:
32 try:
32 with warnings.catch_warnings():
33 with warnings.catch_warnings():
33 warnings.simplefilter('ignore', DeprecationWarning)
34 warnings.simplefilter('ignore', DeprecationWarning)
34 import paramiko
35 import paramiko
35 except ImportError:
36 except ImportError:
36 paramiko = None
37 paramiko = None
37 else:
38 else:
38 from .forward import forward_tunnel
39 from .forward import forward_tunnel
39
40
40 try:
41 try:
41 from IPython.external import pexpect
42 from IPython.external import pexpect
42 except ImportError:
43 except ImportError:
43 pexpect = None
44 pexpect = None
44
45
45 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
46 # Code
47 # Code
47 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
48
49
49 # select_random_ports copied from IPython.parallel.util
50 # select_random_ports copied from IPython.parallel.util
50 _random_ports = set()
51 _random_ports = set()
51
52
52 def select_random_ports(n):
53 def select_random_ports(n):
53 """Selects and return n random ports that are available."""
54 """Selects and return n random ports that are available."""
54 ports = []
55 ports = []
55 for i in xrange(n):
56 for i in xrange(n):
56 sock = socket.socket()
57 sock = socket.socket()
57 sock.bind(('', 0))
58 sock.bind(('', 0))
58 while sock.getsockname()[1] in _random_ports:
59 while sock.getsockname()[1] in _random_ports:
59 sock.close()
60 sock.close()
60 sock = socket.socket()
61 sock = socket.socket()
61 sock.bind(('', 0))
62 sock.bind(('', 0))
62 ports.append(sock)
63 ports.append(sock)
63 for i, sock in enumerate(ports):
64 for i, sock in enumerate(ports):
64 port = sock.getsockname()[1]
65 port = sock.getsockname()[1]
65 sock.close()
66 sock.close()
66 ports[i] = port
67 ports[i] = port
67 _random_ports.add(port)
68 _random_ports.add(port)
68 return ports
69 return ports
69
70
70
71
71 #-----------------------------------------------------------------------------
72 #-----------------------------------------------------------------------------
72 # Check for passwordless login
73 # Check for passwordless login
73 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
74
75
75 def try_passwordless_ssh(server, keyfile, paramiko=None):
76 def try_passwordless_ssh(server, keyfile, paramiko=None):
76 """Attempt to make an ssh connection without a password.
77 """Attempt to make an ssh connection without a password.
77 This is mainly used for requiring password input only once
78 This is mainly used for requiring password input only once
78 when many tunnels may be connected to the same server.
79 when many tunnels may be connected to the same server.
79
80
80 If paramiko is None, the default for the platform is chosen.
81 If paramiko is None, the default for the platform is chosen.
81 """
82 """
82 if paramiko is None:
83 if paramiko is None:
83 paramiko = sys.platform == 'win32'
84 paramiko = sys.platform == 'win32'
84 if not paramiko:
85 if not paramiko:
85 f = _try_passwordless_openssh
86 f = _try_passwordless_openssh
86 else:
87 else:
87 f = _try_passwordless_paramiko
88 f = _try_passwordless_paramiko
88 return f(server, keyfile)
89 return f(server, keyfile)
89
90
90 def _try_passwordless_openssh(server, keyfile):
91 def _try_passwordless_openssh(server, keyfile):
91 """Try passwordless login with shell ssh command."""
92 """Try passwordless login with shell ssh command."""
92 if pexpect is None:
93 if pexpect is None:
93 raise ImportError("pexpect unavailable, use paramiko")
94 raise ImportError("pexpect unavailable, use paramiko")
94 cmd = 'ssh -f '+ server
95 cmd = 'ssh -f '+ server
95 if keyfile:
96 if keyfile:
96 cmd += ' -i ' + keyfile
97 cmd += ' -i ' + keyfile
97 cmd += ' exit'
98 cmd += ' exit'
98 p = pexpect.spawn(cmd)
99 p = pexpect.spawn(cmd)
99 while True:
100 while True:
100 try:
101 try:
101 p.expect('[Pp]assword:', timeout=.1)
102 p.expect('[Pp]assword:', timeout=.1)
102 except pexpect.TIMEOUT:
103 except pexpect.TIMEOUT:
103 continue
104 continue
104 except pexpect.EOF:
105 except pexpect.EOF:
105 return True
106 return True
106 else:
107 else:
107 return False
108 return False
108
109
109 def _try_passwordless_paramiko(server, keyfile):
110 def _try_passwordless_paramiko(server, keyfile):
110 """Try passwordless login with paramiko."""
111 """Try passwordless login with paramiko."""
111 if paramiko is None:
112 if paramiko is None:
112 msg = "Paramiko unavaliable, "
113 msg = "Paramiko unavaliable, "
113 if sys.platform == 'win32':
114 if sys.platform == 'win32':
114 msg += "Paramiko is required for ssh tunneled connections on Windows."
115 msg += "Paramiko is required for ssh tunneled connections on Windows."
115 else:
116 else:
116 msg += "use OpenSSH."
117 msg += "use OpenSSH."
117 raise ImportError(msg)
118 raise ImportError(msg)
118 username, server, port = _split_server(server)
119 username, server, port = _split_server(server)
119 client = paramiko.SSHClient()
120 client = paramiko.SSHClient()
120 client.load_system_host_keys()
121 client.load_system_host_keys()
121 client.set_missing_host_key_policy(paramiko.WarningPolicy())
122 client.set_missing_host_key_policy(paramiko.WarningPolicy())
122 try:
123 try:
123 client.connect(server, port, username=username, key_filename=keyfile,
124 client.connect(server, port, username=username, key_filename=keyfile,
124 look_for_keys=True)
125 look_for_keys=True)
125 except paramiko.AuthenticationException:
126 except paramiko.AuthenticationException:
126 return False
127 return False
127 else:
128 else:
128 client.close()
129 client.close()
129 return True
130 return True
130
131
131
132
132 def tunnel_connection(socket, addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
133 def tunnel_connection(socket, addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
133 """Connect a socket to an address via an ssh tunnel.
134 """Connect a socket to an address via an ssh tunnel.
134
135
135 This is a wrapper for socket.connect(addr), when addr is not accessible
136 This is a wrapper for socket.connect(addr), when addr is not accessible
136 from the local machine. It simply creates an ssh tunnel using the remaining args,
137 from the local machine. It simply creates an ssh tunnel using the remaining args,
137 and calls socket.connect('tcp://localhost:lport') where lport is the randomly
138 and calls socket.connect('tcp://localhost:lport') where lport is the randomly
138 selected local port of the tunnel.
139 selected local port of the tunnel.
139
140
140 """
141 """
141 new_url, tunnel = open_tunnel(addr, server, keyfile=keyfile, password=password, paramiko=paramiko, timeout=timeout)
142 new_url, tunnel = open_tunnel(addr, server, keyfile=keyfile, password=password, paramiko=paramiko, timeout=timeout)
142 socket.connect(new_url)
143 socket.connect(new_url)
143 return tunnel
144 return tunnel
144
145
145
146
146 def open_tunnel(addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
147 def open_tunnel(addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
147 """Open a tunneled connection from a 0MQ url.
148 """Open a tunneled connection from a 0MQ url.
148
149
149 For use inside tunnel_connection.
150 For use inside tunnel_connection.
150
151
151 Returns
152 Returns
152 -------
153 -------
153
154
154 (url, tunnel): The 0MQ url that has been forwarded, and the tunnel object
155 (url, tunnel): The 0MQ url that has been forwarded, and the tunnel object
155 """
156 """
156
157
157 lport = select_random_ports(1)[0]
158 lport = select_random_ports(1)[0]
158 transport, addr = addr.split('://')
159 transport, addr = addr.split('://')
159 ip,rport = addr.split(':')
160 ip,rport = addr.split(':')
160 rport = int(rport)
161 rport = int(rport)
161 if paramiko is None:
162 if paramiko is None:
162 paramiko = sys.platform == 'win32'
163 paramiko = sys.platform == 'win32'
163 if paramiko:
164 if paramiko:
164 tunnelf = paramiko_tunnel
165 tunnelf = paramiko_tunnel
165 else:
166 else:
166 tunnelf = openssh_tunnel
167 tunnelf = openssh_tunnel
167
168
168 tunnel = tunnelf(lport, rport, server, remoteip=ip, keyfile=keyfile, password=password, timeout=timeout)
169 tunnel = tunnelf(lport, rport, server, remoteip=ip, keyfile=keyfile, password=password, timeout=timeout)
169 return 'tcp://127.0.0.1:%i'%lport, tunnel
170 return 'tcp://127.0.0.1:%i'%lport, tunnel
170
171
171 def openssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
172 def openssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
172 """Create an ssh tunnel using command-line ssh that connects port lport
173 """Create an ssh tunnel using command-line ssh that connects port lport
173 on this machine to localhost:rport on server. The tunnel
174 on this machine to localhost:rport on server. The tunnel
174 will automatically close when not in use, remaining open
175 will automatically close when not in use, remaining open
175 for a minimum of timeout seconds for an initial connection.
176 for a minimum of timeout seconds for an initial connection.
176
177
177 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
178 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
178 as seen from `server`.
179 as seen from `server`.
179
180
180 keyfile and password may be specified, but ssh config is checked for defaults.
181 keyfile and password may be specified, but ssh config is checked for defaults.
181
182
182 Parameters
183 Parameters
183 ----------
184 ----------
184
185
185 lport : int
186 lport : int
186 local port for connecting to the tunnel from this machine.
187 local port for connecting to the tunnel from this machine.
187 rport : int
188 rport : int
188 port on the remote machine to connect to.
189 port on the remote machine to connect to.
189 server : str
190 server : str
190 The ssh server to connect to. The full ssh server string will be parsed.
191 The ssh server to connect to. The full ssh server string will be parsed.
191 user@server:port
192 user@server:port
192 remoteip : str [Default: 127.0.0.1]
193 remoteip : str [Default: 127.0.0.1]
193 The remote ip, specifying the destination of the tunnel.
194 The remote ip, specifying the destination of the tunnel.
194 Default is localhost, which means that the tunnel would redirect
195 Default is localhost, which means that the tunnel would redirect
195 localhost:lport on this machine to localhost:rport on the *server*.
196 localhost:lport on this machine to localhost:rport on the *server*.
196
197
197 keyfile : str; path to public key file
198 keyfile : str; path to public key file
198 This specifies a key to be used in ssh login, default None.
199 This specifies a key to be used in ssh login, default None.
199 Regular default ssh keys will be used without specifying this argument.
200 Regular default ssh keys will be used without specifying this argument.
200 password : str;
201 password : str;
201 Your ssh password to the ssh server. Note that if this is left None,
202 Your ssh password to the ssh server. Note that if this is left None,
202 you will be prompted for it if passwordless key based login is unavailable.
203 you will be prompted for it if passwordless key based login is unavailable.
203 timeout : int [default: 60]
204 timeout : int [default: 60]
204 The time (in seconds) after which no activity will result in the tunnel
205 The time (in seconds) after which no activity will result in the tunnel
205 closing. This prevents orphaned tunnels from running forever.
206 closing. This prevents orphaned tunnels from running forever.
206 """
207 """
207 if pexpect is None:
208 if pexpect is None:
208 raise ImportError("pexpect unavailable, use paramiko_tunnel")
209 raise ImportError("pexpect unavailable, use paramiko_tunnel")
209 ssh="ssh "
210 ssh="ssh "
210 if keyfile:
211 if keyfile:
211 ssh += "-i " + keyfile
212 ssh += "-i " + keyfile
212
213
213 if ':' in server:
214 if ':' in server:
214 server, port = server.split(':')
215 server, port = server.split(':')
215 ssh += " -p %s" % port
216 ssh += " -p %s" % port
216
217
217 cmd = "%s -f -S none -L 127.0.0.1:%i:%s:%i %s sleep %i" % (
218 cmd = "%s -f -S none -L 127.0.0.1:%i:%s:%i %s sleep %i" % (
218 ssh, lport, remoteip, rport, server, timeout)
219 ssh, lport, remoteip, rport, server, timeout)
219 tunnel = pexpect.spawn(cmd)
220 tunnel = pexpect.spawn(cmd)
220 failed = False
221 failed = False
221 while True:
222 while True:
222 try:
223 try:
223 tunnel.expect('[Pp]assword:', timeout=.1)
224 tunnel.expect('[Pp]assword:', timeout=.1)
224 except pexpect.TIMEOUT:
225 except pexpect.TIMEOUT:
225 continue
226 continue
226 except pexpect.EOF:
227 except pexpect.EOF:
227 if tunnel.exitstatus:
228 if tunnel.exitstatus:
228 print (tunnel.exitstatus)
229 print (tunnel.exitstatus)
229 print (tunnel.before)
230 print (tunnel.before)
230 print (tunnel.after)
231 print (tunnel.after)
231 raise RuntimeError("tunnel '%s' failed to start"%(cmd))
232 raise RuntimeError("tunnel '%s' failed to start"%(cmd))
232 else:
233 else:
233 return tunnel.pid
234 return tunnel.pid
234 else:
235 else:
235 if failed:
236 if failed:
236 print("Password rejected, try again")
237 print("Password rejected, try again")
237 password=None
238 password=None
238 if password is None:
239 if password is None:
239 password = getpass("%s's password: "%(server))
240 password = getpass("%s's password: "%(server))
240 tunnel.sendline(password)
241 tunnel.sendline(password)
241 failed = True
242 failed = True
242
243
243 def _split_server(server):
244 def _split_server(server):
244 if '@' in server:
245 if '@' in server:
245 username,server = server.split('@', 1)
246 username,server = server.split('@', 1)
246 else:
247 else:
247 username = getuser()
248 username = getuser()
248 if ':' in server:
249 if ':' in server:
249 server, port = server.split(':')
250 server, port = server.split(':')
250 port = int(port)
251 port = int(port)
251 else:
252 else:
252 port = 22
253 port = 22
253 return username, server, port
254 return username, server, port
254
255
255 def paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
256 def paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
256 """launch a tunner with paramiko in a subprocess. This should only be used
257 """launch a tunner with paramiko in a subprocess. This should only be used
257 when shell ssh is unavailable (e.g. Windows).
258 when shell ssh is unavailable (e.g. Windows).
258
259
259 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
260 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
260 as seen from `server`.
261 as seen from `server`.
261
262
262 If you are familiar with ssh tunnels, this creates the tunnel:
263 If you are familiar with ssh tunnels, this creates the tunnel:
263
264
264 ssh server -L localhost:lport:remoteip:rport
265 ssh server -L localhost:lport:remoteip:rport
265
266
266 keyfile and password may be specified, but ssh config is checked for defaults.
267 keyfile and password may be specified, but ssh config is checked for defaults.
267
268
268
269
269 Parameters
270 Parameters
270 ----------
271 ----------
271
272
272 lport : int
273 lport : int
273 local port for connecting to the tunnel from this machine.
274 local port for connecting to the tunnel from this machine.
274 rport : int
275 rport : int
275 port on the remote machine to connect to.
276 port on the remote machine to connect to.
276 server : str
277 server : str
277 The ssh server to connect to. The full ssh server string will be parsed.
278 The ssh server to connect to. The full ssh server string will be parsed.
278 user@server:port
279 user@server:port
279 remoteip : str [Default: 127.0.0.1]
280 remoteip : str [Default: 127.0.0.1]
280 The remote ip, specifying the destination of the tunnel.
281 The remote ip, specifying the destination of the tunnel.
281 Default is localhost, which means that the tunnel would redirect
282 Default is localhost, which means that the tunnel would redirect
282 localhost:lport on this machine to localhost:rport on the *server*.
283 localhost:lport on this machine to localhost:rport on the *server*.
283
284
284 keyfile : str; path to public key file
285 keyfile : str; path to public key file
285 This specifies a key to be used in ssh login, default None.
286 This specifies a key to be used in ssh login, default None.
286 Regular default ssh keys will be used without specifying this argument.
287 Regular default ssh keys will be used without specifying this argument.
287 password : str;
288 password : str;
288 Your ssh password to the ssh server. Note that if this is left None,
289 Your ssh password to the ssh server. Note that if this is left None,
289 you will be prompted for it if passwordless key based login is unavailable.
290 you will be prompted for it if passwordless key based login is unavailable.
290 timeout : int [default: 60]
291 timeout : int [default: 60]
291 The time (in seconds) after which no activity will result in the tunnel
292 The time (in seconds) after which no activity will result in the tunnel
292 closing. This prevents orphaned tunnels from running forever.
293 closing. This prevents orphaned tunnels from running forever.
293
294
294 """
295 """
295 if paramiko is None:
296 if paramiko is None:
296 raise ImportError("Paramiko not available")
297 raise ImportError("Paramiko not available")
297
298
298 if password is None:
299 if password is None:
299 if not _try_passwordless_paramiko(server, keyfile):
300 if not _try_passwordless_paramiko(server, keyfile):
300 password = getpass("%s's password: "%(server))
301 password = getpass("%s's password: "%(server))
301
302
302 p = Process(target=_paramiko_tunnel,
303 p = Process(target=_paramiko_tunnel,
303 args=(lport, rport, server, remoteip),
304 args=(lport, rport, server, remoteip),
304 kwargs=dict(keyfile=keyfile, password=password))
305 kwargs=dict(keyfile=keyfile, password=password))
305 p.daemon=False
306 p.daemon=False
306 p.start()
307 p.start()
307 atexit.register(_shutdown_process, p)
308 atexit.register(_shutdown_process, p)
308 return p
309 return p
309
310
310 def _shutdown_process(p):
311 def _shutdown_process(p):
311 if p.is_alive():
312 if p.is_alive():
312 p.terminate()
313 p.terminate()
313
314
314 def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None):
315 def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None):
315 """Function for actually starting a paramiko tunnel, to be passed
316 """Function for actually starting a paramiko tunnel, to be passed
316 to multiprocessing.Process(target=this), and not called directly.
317 to multiprocessing.Process(target=this), and not called directly.
317 """
318 """
318 username, server, port = _split_server(server)
319 username, server, port = _split_server(server)
319 client = paramiko.SSHClient()
320 client = paramiko.SSHClient()
320 client.load_system_host_keys()
321 client.load_system_host_keys()
321 client.set_missing_host_key_policy(paramiko.WarningPolicy())
322 client.set_missing_host_key_policy(paramiko.WarningPolicy())
322
323
323 try:
324 try:
324 client.connect(server, port, username=username, key_filename=keyfile,
325 client.connect(server, port, username=username, key_filename=keyfile,
325 look_for_keys=True, password=password)
326 look_for_keys=True, password=password)
326 # except paramiko.AuthenticationException:
327 # except paramiko.AuthenticationException:
327 # if password is None:
328 # if password is None:
328 # password = getpass("%s@%s's password: "%(username, server))
329 # password = getpass("%s@%s's password: "%(username, server))
329 # client.connect(server, port, username=username, password=password)
330 # client.connect(server, port, username=username, password=password)
330 # else:
331 # else:
331 # raise
332 # raise
332 except Exception as e:
333 except Exception as e:
333 print ('*** Failed to connect to %s:%d: %r' % (server, port, e))
334 print ('*** Failed to connect to %s:%d: %r' % (server, port, e))
334 sys.exit(1)
335 sys.exit(1)
335
336
336 # Don't let SIGINT kill the tunnel subprocess
337 # Don't let SIGINT kill the tunnel subprocess
337 signal.signal(signal.SIGINT, signal.SIG_IGN)
338 signal.signal(signal.SIGINT, signal.SIG_IGN)
338
339
339 try:
340 try:
340 forward_tunnel(lport, remoteip, rport, client.get_transport())
341 forward_tunnel(lport, remoteip, rport, client.get_transport())
341 except KeyboardInterrupt:
342 except KeyboardInterrupt:
342 print ('SIGINT: Port forwarding stopped cleanly')
343 print ('SIGINT: Port forwarding stopped cleanly')
343 sys.exit(0)
344 sys.exit(0)
344 except Exception as e:
345 except Exception as e:
345 print ("Port forwarding stopped uncleanly: %s"%e)
346 print ("Port forwarding stopped uncleanly: %s"%e)
346 sys.exit(255)
347 sys.exit(255)
347
348
348 if sys.platform == 'win32':
349 if sys.platform == 'win32':
349 ssh_tunnel = paramiko_tunnel
350 ssh_tunnel = paramiko_tunnel
350 else:
351 else:
351 ssh_tunnel = openssh_tunnel
352 ssh_tunnel = openssh_tunnel
352
353
353
354
354 __all__ = ['tunnel_connection', 'ssh_tunnel', 'openssh_tunnel', 'paramiko_tunnel', 'try_passwordless_ssh']
355 __all__ = ['tunnel_connection', 'ssh_tunnel', 'openssh_tunnel', 'paramiko_tunnel', 'try_passwordless_ssh']
355
356
356
357
General Comments 0
You need to be logged in to leave comments. Login now