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