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