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