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