##// END OF EJS Templates
added preliminary ssh tunneling support for clients
Min RK -
Show More
@@ -1,7 +1,7 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2
2
3 #
3 #
4 # This file is adapted from a paramiko demo, and thus LGPL 2.1.
4 # This file is adapted from a paramiko demo, and thus licensed under LGPL 2.1.
5 # Original Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
5 # Original Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
6 # Edits Copyright (C) 2010 The IPython Team
6 # Edits Copyright (C) 2010 The IPython Team
7 #
7 #
@@ -83,7 +83,7 b' class Handler (SocketServer.BaseRequestHandler):'
83 self.request.send(data)
83 self.request.send(data)
84 chan.close()
84 chan.close()
85 self.request.close()
85 self.request.close()
86 verbose('Tunnel closed from %r' % (self.request.getpeername(),))
86 verbose('Tunnel closed ')
87
87
88
88
89 def forward_tunnel(local_port, remote_host, remote_port, transport):
89 def forward_tunnel(local_port, remote_host, remote_port, transport):
@@ -94,7 +94,7 b' def forward_tunnel(local_port, remote_host, remote_port, transport):'
94 chain_host = remote_host
94 chain_host = remote_host
95 chain_port = remote_port
95 chain_port = remote_port
96 ssh_transport = transport
96 ssh_transport = transport
97 ForwardServer(('', local_port), SubHander).serve_forever()
97 ForwardServer(('127.0.0.1', local_port), SubHander).serve_forever()
98
98
99
99
100 def verbose(s):
100 def verbose(s):
@@ -19,6 +19,7 b' import zmq'
19 from zmq.eventloop import ioloop, zmqstream
19 from zmq.eventloop import ioloop, zmqstream
20
20
21 from IPython.external.decorator import decorator
21 from IPython.external.decorator import decorator
22 from IPython.zmq import tunnel
22
23
23 import streamsession as ss
24 import streamsession as ss
24 # from remotenamespace import RemoteNamespace
25 # from remotenamespace import RemoteNamespace
@@ -117,7 +118,33 b' class Client(object):'
117
118
118 addr : bytes; zmq url, e.g. 'tcp://127.0.0.1:10101'
119 addr : bytes; zmq url, e.g. 'tcp://127.0.0.1:10101'
119 The address of the controller's registration socket.
120 The address of the controller's registration socket.
120
121 [Default: 'tcp://127.0.0.1:10101']
122 context : zmq.Context
123 Pass an existing zmq.Context instance, otherwise the client will create its own
124 username : bytes
125 set username to be passed to the Session object
126 debug : bool
127 flag for lots of message printing for debug purposes
128
129 #-------------- ssh related args ----------------
130 # These are args for configuring the ssh tunnel to be used
131 # credentials are used to forward connections over ssh to the Controller
132 # Note that the ip given in `addr` needs to be relative to sshserver
133 # The most basic case is to leave addr as pointing to localhost (127.0.0.1),
134 # and set sshserver as the same machine the Controller is on. However,
135 # the only requirement is that sshserver is able to see the Controller
136 # (i.e. is within the same trusted network).
137
138 sshserver : str
139 A string of the form passed to ssh, i.e. 'server.tld' or 'user@server.tld:port'
140 If keyfile or password is specified, and this is not, it will default to
141 the ip given in addr.
142 keyfile : str; path to public key file
143 This specifies a key to be used in ssh login, default None.
144 Regular default ssh keys will be used without specifying this argument.
145 password : str;
146 Your ssh password to sshserver. Note that if this is left None,
147 you will be prompted for it if passwordless key based login is unavailable.
121
148
122 Attributes
149 Attributes
123 ----------
150 ----------
@@ -159,6 +186,7 b' class Client(object):'
159
186
160
187
161 _connected=False
188 _connected=False
189 _ssh=False
162 _engines=None
190 _engines=None
163 _addr='tcp://127.0.0.1:10101'
191 _addr='tcp://127.0.0.1:10101'
164 _registration_socket=None
192 _registration_socket=None
@@ -173,18 +201,33 b' class Client(object):'
173 history = None
201 history = None
174 debug = False
202 debug = False
175
203
176 def __init__(self, addr='tcp://127.0.0.1:10101', context=None, username=None, debug=False):
204 def __init__(self, addr='tcp://127.0.0.1:10101', context=None, username=None, debug=False,
205 sshserver=None, keyfile=None, password=None, paramiko=None):
177 if context is None:
206 if context is None:
178 context = zmq.Context()
207 context = zmq.Context()
179 self.context = context
208 self.context = context
180 self._addr = addr
209 self._addr = addr
210 self._ssh = bool(sshserver or keyfile or password)
211 if self._ssh and sshserver is None:
212 # default to the same
213 sshserver = addr.split('://')[1].split(':')[0]
214 if self._ssh and password is None:
215 if tunnel.try_passwordless_ssh(sshserver, keyfile, paramiko):
216 password=False
217 else:
218 password = getpass("SSH Password for %s: "%sshserver)
219 ssh_kwargs = dict(keyfile=keyfile, password=password, paramiko=paramiko)
220
181 if username is None:
221 if username is None:
182 self.session = ss.StreamSession()
222 self.session = ss.StreamSession()
183 else:
223 else:
184 self.session = ss.StreamSession(username)
224 self.session = ss.StreamSession(username)
185 self._registration_socket = self.context.socket(zmq.PAIR)
225 self._registration_socket = self.context.socket(zmq.XREQ)
186 self._registration_socket.setsockopt(zmq.IDENTITY, self.session.session)
226 self._registration_socket.setsockopt(zmq.IDENTITY, self.session.session)
187 self._registration_socket.connect(addr)
227 if self._ssh:
228 tunnel.tunnel_connection(self._registration_socket, addr, sshserver, **ssh_kwargs)
229 else:
230 self._registration_socket.connect(addr)
188 self._engines = {}
231 self._engines = {}
189 self._ids = set()
232 self._ids = set()
190 self.outstanding=set()
233 self.outstanding=set()
@@ -198,7 +241,7 b' class Client(object):'
198 }
241 }
199 self._queue_handlers = {'execute_reply' : self._handle_execute_reply,
242 self._queue_handlers = {'execute_reply' : self._handle_execute_reply,
200 'apply_reply' : self._handle_apply_reply}
243 'apply_reply' : self._handle_apply_reply}
201 self._connect()
244 self._connect(sshserver, ssh_kwargs)
202
245
203
246
204 @property
247 @property
@@ -229,12 +272,19 b' class Client(object):'
229 targets = [targets]
272 targets = [targets]
230 return [self._engines[t] for t in targets], list(targets)
273 return [self._engines[t] for t in targets], list(targets)
231
274
232 def _connect(self):
275 def _connect(self, sshserver, ssh_kwargs):
233 """setup all our socket connections to the controller. This is called from
276 """setup all our socket connections to the controller. This is called from
234 __init__."""
277 __init__."""
235 if self._connected:
278 if self._connected:
236 return
279 return
237 self._connected=True
280 self._connected=True
281
282 def connect_socket(s, addr):
283 if self._ssh:
284 return tunnel.tunnel_connection(s, addr, sshserver, **ssh_kwargs)
285 else:
286 return s.connect(addr)
287
238 self.session.send(self._registration_socket, 'connection_request')
288 self.session.send(self._registration_socket, 'connection_request')
239 idents,msg = self.session.recv(self._registration_socket,mode=0)
289 idents,msg = self.session.recv(self._registration_socket,mode=0)
240 if self.debug:
290 if self.debug:
@@ -245,23 +295,23 b' class Client(object):'
245 if content.queue:
295 if content.queue:
246 self._mux_socket = self.context.socket(zmq.PAIR)
296 self._mux_socket = self.context.socket(zmq.PAIR)
247 self._mux_socket.setsockopt(zmq.IDENTITY, self.session.session)
297 self._mux_socket.setsockopt(zmq.IDENTITY, self.session.session)
248 self._mux_socket.connect(content.queue)
298 connect_socket(self._mux_socket, content.queue)
249 if content.task:
299 if content.task:
250 self._task_socket = self.context.socket(zmq.PAIR)
300 self._task_socket = self.context.socket(zmq.PAIR)
251 self._task_socket.setsockopt(zmq.IDENTITY, self.session.session)
301 self._task_socket.setsockopt(zmq.IDENTITY, self.session.session)
252 self._task_socket.connect(content.task)
302 connect_socket(self._task_socket, content.task)
253 if content.notification:
303 if content.notification:
254 self._notification_socket = self.context.socket(zmq.SUB)
304 self._notification_socket = self.context.socket(zmq.SUB)
255 self._notification_socket.connect(content.notification)
305 connect_socket(self._notification_socket, content.notification)
256 self._notification_socket.setsockopt(zmq.SUBSCRIBE, "")
306 self._notification_socket.setsockopt(zmq.SUBSCRIBE, "")
257 if content.query:
307 if content.query:
258 self._query_socket = self.context.socket(zmq.PAIR)
308 self._query_socket = self.context.socket(zmq.PAIR)
259 self._query_socket.setsockopt(zmq.IDENTITY, self.session.session)
309 self._query_socket.setsockopt(zmq.IDENTITY, self.session.session)
260 self._query_socket.connect(content.query)
310 connect_socket(self._query_socket, content.query)
261 if content.control:
311 if content.control:
262 self._control_socket = self.context.socket(zmq.PAIR)
312 self._control_socket = self.context.socket(zmq.PAIR)
263 self._control_socket.setsockopt(zmq.IDENTITY, self.session.session)
313 self._control_socket.setsockopt(zmq.IDENTITY, self.session.session)
264 self._control_socket.connect(content.control)
314 connect_socket(self._control_socket, content.control)
265 self._update_engines(dict(content.engines))
315 self._update_engines(dict(content.engines))
266
316
267 else:
317 else:
@@ -852,4 +902,4 b' class AsynClient(Client):'
852 for stream in (self.queue_stream, self.notifier_stream,
902 for stream in (self.queue_stream, self.notifier_stream,
853 self.task_stream, self.control_stream):
903 self.task_stream, self.control_stream):
854 stream.flush()
904 stream.flush()
855 No newline at end of file
905
@@ -1,12 +1,21 b''
1 """Basic ssh tunneling utilities."""
1
2
3 #-----------------------------------------------------------------------------
4 # Copyright (C) 2008-2010 The IPython Development Team
5 #
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING, distributed as part of this software.
8 #-----------------------------------------------------------------------------
2
9
3 #-----------------------------------------
10
11
12 #-----------------------------------------------------------------------------
4 # Imports
13 # Imports
5 #-----------------------------------------
14 #-----------------------------------------------------------------------------
6
15
7 from __future__ import print_function
16 from __future__ import print_function
8
17
9 import os,sys
18 import os,sys, atexit
10 from multiprocessing import Process
19 from multiprocessing import Process
11 from getpass import getpass, getuser
20 from getpass import getpass, getuser
12
21
@@ -16,20 +25,137 b' except ImportError:'
16 paramiko = None
25 paramiko = None
17 else:
26 else:
18 from forward import forward_tunnel
27 from forward import forward_tunnel
28
29 try:
30 from IPython.external import pexpect
31 except ImportError:
32 pexpect = None
33
34 from IPython.zmq.parallel.entry_point import select_random_ports
35
36 #-----------------------------------------------------------------------------
37 # Code
38 #-----------------------------------------------------------------------------
39
40 #-----------------------------------------------------------------------------
41 # Check for passwordless login
42 #-----------------------------------------------------------------------------
43
44 def try_passwordless_ssh(server, keyfile, paramiko=None):
45 """Attempt to make an ssh connection without a password.
46 This is mainly used for requiring password input only once
47 when many tunnels may be connected to the same server.
19
48
20 from IPython.external import pexpect
49 If paramiko is None, the default for the platform is chosen.
50 """
51 if paramiko is None:
52 paramiko = sys.platform == 'win32'
53 if not paramiko:
54 f = _try_passwordless_openssh
55 else:
56 f = _try_passwordless_paramiko
57 return f(server, keyfile)
21
58
59 def _try_passwordless_openssh(server, keyfile):
60 """Try passwordless login with shell ssh command."""
61 if pexpect is None:
62 raise ImportError("pexpect unavailable, use paramiko")
63 cmd = 'ssh -f '+ server
64 if keyfile:
65 cmd += ' -i ' + keyfile
66 cmd += ' exit'
67 p = pexpect.spawn(cmd)
68 while True:
69 try:
70 p.expect('[Ppassword]:', timeout=.1)
71 except pexpect.TIMEOUT:
72 continue
73 except pexpect.EOF:
74 return True
75 else:
76 return False
22
77
23 def launch_ssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, timeout=15):
78 def _try_passwordless_paramiko(server, keyfile):
79 """Try passwordless login with paramiko."""
80 if paramiko is None:
81 raise ImportError("paramiko unavailable, use openssh")
82 username, server, port = _split_server(server)
83 client = paramiko.SSHClient()
84 client.load_system_host_keys()
85 client.set_missing_host_key_policy(paramiko.WarningPolicy())
86 try:
87 client.connect(server, port, username=username, key_filename=keyfile,
88 look_for_keys=True)
89 except paramiko.AuthenticationException:
90 return False
91 else:
92 client.close()
93 return True
94
95
96 def tunnel_connection(socket, addr, server, keyfile=None, password=None, paramiko=None):
97 """Connect a socket to an address via an ssh tunnel.
98
99 This is a wrapper for socket.connect(addr), when addr is not accessible
100 from the local machine. It simply creates an ssh tunnel using the remaining args,
101 and calls socket.connect('tcp://localhost:lport') where lport is the randomly
102 selected local port of the tunnel.
103
104 """
105 lport = select_random_ports(1)[0]
106 transport, addr = addr.split('://')
107 ip,rport = addr.split(':')
108 rport = int(rport)
109 if paramiko is None:
110 paramiko = sys.platform == 'win32'
111 if paramiko:
112 tunnelf = paramiko_tunnel
113 else:
114 tunnelf = openssh_tunnel
115 tunnel = tunnelf(lport, rport, server, remoteip=ip, keyfile=keyfile, password=password)
116 socket.connect('tcp://127.0.0.1:%i'%lport)
117 return tunnel
118
119 def openssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=15):
24 """Create an ssh tunnel using command-line ssh that connects port lport
120 """Create an ssh tunnel using command-line ssh that connects port lport
25 on this machine to localhost:rport on server. The tunnel
121 on this machine to localhost:rport on server. The tunnel
26 will automatically close when not in use, remaining open
122 will automatically close when not in use, remaining open
27 for a minimum of timeout seconds for an initial connection.
123 for a minimum of timeout seconds for an initial connection.
124
125 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
126 as seen from `server`.
127
128 keyfile and password may be specified, but ssh config is checked for defaults.
129
130 Parameters
131 ----------
132
133 lport : int
134 local port for connecting to the tunnel from this machine.
135 rport : int
136 port on the remote machine to connect to.
137 server : str
138 The ssh server to connect to. The full ssh server string will be parsed.
139 user@server:port
140 remoteip : str [Default: 127.0.0.1]
141 The remote ip, specifying the destination of the tunnel.
142 Default is localhost, which means that the tunnel would redirect
143 localhost:lport on this machine to localhost:rport on the *server*.
144
145 keyfile : str; path to public key file
146 This specifies a key to be used in ssh login, default None.
147 Regular default ssh keys will be used without specifying this argument.
148 password : str;
149 Your ssh password to the ssh server. Note that if this is left None,
150 you will be prompted for it if passwordless key based login is unavailable.
151
28 """
152 """
153 if pexpect is None:
154 raise ImportError("pexpect unavailable, use paramiko_tunnel")
29 ssh="ssh "
155 ssh="ssh "
30 if keyfile:
156 if keyfile:
31 ssh += "-i " + keyfile
157 ssh += "-i " + keyfile
32 cmd = ssh + " -f -L %i:127.0.0.1:%i %s sleep %i"%(lport, rport, server, timeout)
158 cmd = ssh + " -f -L 127.0.0.1:%i:127.0.0.1:%i %s sleep %i"%(lport, rport, server, timeout)
33 tunnel = pexpect.spawn(cmd)
159 tunnel = pexpect.spawn(cmd)
34 failed = False
160 failed = False
35 while True:
161 while True:
@@ -48,7 +174,10 b" def launch_ssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, "
48 else:
174 else:
49 if failed:
175 if failed:
50 print("Password rejected, try again")
176 print("Password rejected, try again")
51 tunnel.sendline(getpass())
177 password=None
178 if password is None:
179 password = getpass("%s's password: "%(server))
180 tunnel.sendline(password)
52 failed = True
181 failed = True
53
182
54 def _split_server(server):
183 def _split_server(server):
@@ -63,28 +192,62 b' def _split_server(server):'
63 port = 22
192 port = 22
64 return username, server, port
193 return username, server, port
65
194
66 def launch_paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None):
195 def paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=15):
67 """launch a tunner with paramiko in a subprocess"""
196 """launch a tunner with paramiko in a subprocess. This should only be used
197 when shell ssh is unavailable (e.g. Windows).
198
199 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
200 as seen from `server`.
201
202 keyfile and password may be specified, but ssh config is checked for defaults.
203
204 Parameters
205 ----------
206
207 lport : int
208 local port for connecting to the tunnel from this machine.
209 rport : int
210 port on the remote machine to connect to.
211 server : str
212 The ssh server to connect to. The full ssh server string will be parsed.
213 user@server:port
214 remoteip : str [Default: 127.0.0.1]
215 The remote ip, specifying the destination of the tunnel.
216 Default is localhost, which means that the tunnel would redirect
217 localhost:lport on this machine to localhost:rport on the *server*.
218
219 keyfile : str; path to public key file
220 This specifies a key to be used in ssh login, default None.
221 Regular default ssh keys will be used without specifying this argument.
222 password : str;
223 Your ssh password to the ssh server. Note that if this is left None,
224 you will be prompted for it if passwordless key based login is unavailable.
225
226 """
68 if paramiko is None:
227 if paramiko is None:
69 raise ImportError("Paramiko not available")
228 raise ImportError("Paramiko not available")
70 server = _split_server(server)
229
71 if keyfile is None:
230 if password is None:
72 passwd = getpass("%s@%s's password: "%(server[0], server[1]))
231 if not _check_passwordless_paramiko(server, keyfile):
73 else:
232 password = getpass("%s's password: "%(server))
74 passwd = None
233
75 p = Process(target=_paramiko_tunnel,
234 p = Process(target=_paramiko_tunnel,
76 args=(lport, rport, server, remoteip),
235 args=(lport, rport, server, remoteip),
77 kwargs=dict(keyfile=keyfile, password=passwd))
236 kwargs=dict(keyfile=keyfile, password=password))
78 p.daemon=False
237 p.daemon=False
79 p.start()
238 p.start()
239 atexit.register(_shutdown_process, p)
80 return p
240 return p
81
241
242 def _shutdown_process(p):
243 if p.isalive():
244 p.terminate()
82
245
83 def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None):
246 def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None):
84 """function for actually starting a paramiko tunnel, to be passed
247 """function for actually starting a paramiko tunnel, to be passed
85 to multiprocessing.Process(target=this).
248 to multiprocessing.Process(target=this).
86 """
249 """
87 username, server, port = server
250 username, server, port = _split_server(server)
88 client = paramiko.SSHClient()
251 client = paramiko.SSHClient()
89 client.load_system_host_keys()
252 client.load_system_host_keys()
90 client.set_missing_host_key_policy(paramiko.WarningPolicy())
253 client.set_missing_host_key_policy(paramiko.WarningPolicy())
@@ -92,20 +255,34 b' def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None'
92 try:
255 try:
93 client.connect(server, port, username=username, key_filename=keyfile,
256 client.connect(server, port, username=username, key_filename=keyfile,
94 look_for_keys=True, password=password)
257 look_for_keys=True, password=password)
258 # except paramiko.AuthenticationException:
259 # if password is None:
260 # password = getpass("%s@%s's password: "%(username, server))
261 # client.connect(server, port, username=username, password=password)
262 # else:
263 # raise
95 except Exception as e:
264 except Exception as e:
96 print ('*** Failed to connect to %s:%d: %r' % (server, port, e))
265 print ('*** Failed to connect to %s:%d: %r' % (server, port, e))
97 sys.exit(1)
266 sys.exit(1)
98
267
99 print ('Now forwarding port %d to %s:%d ...' % (lport, server, rport))
268 # print ('Now forwarding port %d to %s:%d ...' % (lport, server, rport))
100
269
101 try:
270 try:
102 forward_tunnel(lport, remoteip, rport, client.get_transport())
271 forward_tunnel(lport, remoteip, rport, client.get_transport())
103 except KeyboardInterrupt:
272 except KeyboardInterrupt:
104 print ('C-c: Port forwarding stopped.')
273 print ('SIGINT: Port forwarding stopped cleanly')
105 sys.exit(0)
274 sys.exit(0)
275 except Exception as e:
276 print ("Port forwarding stopped uncleanly: %s"%e)
277 sys.exit(255)
278
279 if sys.platform == 'win32':
280 ssh_tunnel = paramiko_tunnel
281 else:
282 ssh_tunnel = openssh_tunnel
106
283
107
284
108 __all__ = ['launch_ssh_tunnel', 'launch_paramiko_tunnel']
285 __all__ = ['tunnel_connection', 'ssh_tunnel', 'openssh_tunnel', 'paramiko_tunnel', 'try_passwordless_ssh']
109
286
110
287
111
288
General Comments 0
You need to be logged in to leave comments. Login now