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