Show More
@@ -1,7 +1,7 | |||
|
1 | 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 | 5 | # Original Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> |
|
6 | 6 | # Edits Copyright (C) 2010 The IPython Team |
|
7 | 7 | # |
@@ -83,7 +83,7 class Handler (SocketServer.BaseRequestHandler): | |||
|
83 | 83 | self.request.send(data) |
|
84 | 84 | chan.close() |
|
85 | 85 | self.request.close() |
|
86 |
verbose('Tunnel closed |
|
|
86 | verbose('Tunnel closed ') | |
|
87 | 87 | |
|
88 | 88 | |
|
89 | 89 | def forward_tunnel(local_port, remote_host, remote_port, transport): |
@@ -94,7 +94,7 def forward_tunnel(local_port, remote_host, remote_port, transport): | |||
|
94 | 94 | chain_host = remote_host |
|
95 | 95 | chain_port = remote_port |
|
96 | 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 | 100 | def verbose(s): |
@@ -19,6 +19,7 import zmq | |||
|
19 | 19 | from zmq.eventloop import ioloop, zmqstream |
|
20 | 20 | |
|
21 | 21 | from IPython.external.decorator import decorator |
|
22 | from IPython.zmq import tunnel | |
|
22 | 23 | |
|
23 | 24 | import streamsession as ss |
|
24 | 25 | # from remotenamespace import RemoteNamespace |
@@ -117,7 +118,33 class Client(object): | |||
|
117 | 118 | |
|
118 | 119 | addr : bytes; zmq url, e.g. 'tcp://127.0.0.1:10101' |
|
119 | 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 | 149 | Attributes |
|
123 | 150 | ---------- |
@@ -159,6 +186,7 class Client(object): | |||
|
159 | 186 | |
|
160 | 187 | |
|
161 | 188 | _connected=False |
|
189 | _ssh=False | |
|
162 | 190 | _engines=None |
|
163 | 191 | _addr='tcp://127.0.0.1:10101' |
|
164 | 192 | _registration_socket=None |
@@ -173,18 +201,33 class Client(object): | |||
|
173 | 201 | history = None |
|
174 | 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 | 206 | if context is None: |
|
178 | 207 | context = zmq.Context() |
|
179 | 208 | self.context = context |
|
180 | 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 | 221 | if username is None: |
|
182 | 222 | self.session = ss.StreamSession() |
|
183 | 223 | else: |
|
184 | 224 | self.session = ss.StreamSession(username) |
|
185 |
self._registration_socket = self.context.socket(zmq. |
|
|
225 | self._registration_socket = self.context.socket(zmq.XREQ) | |
|
186 | 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 | 231 | self._engines = {} |
|
189 | 232 | self._ids = set() |
|
190 | 233 | self.outstanding=set() |
@@ -198,7 +241,7 class Client(object): | |||
|
198 | 241 | } |
|
199 | 242 | self._queue_handlers = {'execute_reply' : self._handle_execute_reply, |
|
200 | 243 | 'apply_reply' : self._handle_apply_reply} |
|
201 | self._connect() | |
|
244 | self._connect(sshserver, ssh_kwargs) | |
|
202 | 245 | |
|
203 | 246 | |
|
204 | 247 | @property |
@@ -229,12 +272,19 class Client(object): | |||
|
229 | 272 | targets = [targets] |
|
230 | 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 | 276 | """setup all our socket connections to the controller. This is called from |
|
234 | 277 | __init__.""" |
|
235 | 278 | if self._connected: |
|
236 | 279 | return |
|
237 | 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 | 288 | self.session.send(self._registration_socket, 'connection_request') |
|
239 | 289 | idents,msg = self.session.recv(self._registration_socket,mode=0) |
|
240 | 290 | if self.debug: |
@@ -245,23 +295,23 class Client(object): | |||
|
245 | 295 | if content.queue: |
|
246 | 296 | self._mux_socket = self.context.socket(zmq.PAIR) |
|
247 | 297 | self._mux_socket.setsockopt(zmq.IDENTITY, self.session.session) |
|
248 |
self._mux_socket |
|
|
298 | connect_socket(self._mux_socket, content.queue) | |
|
249 | 299 | if content.task: |
|
250 | 300 | self._task_socket = self.context.socket(zmq.PAIR) |
|
251 | 301 | self._task_socket.setsockopt(zmq.IDENTITY, self.session.session) |
|
252 |
self._task_socket |
|
|
302 | connect_socket(self._task_socket, content.task) | |
|
253 | 303 | if content.notification: |
|
254 | 304 | self._notification_socket = self.context.socket(zmq.SUB) |
|
255 |
self._notification_socket |
|
|
305 | connect_socket(self._notification_socket, content.notification) | |
|
256 | 306 | self._notification_socket.setsockopt(zmq.SUBSCRIBE, "") |
|
257 | 307 | if content.query: |
|
258 | 308 | self._query_socket = self.context.socket(zmq.PAIR) |
|
259 | 309 | self._query_socket.setsockopt(zmq.IDENTITY, self.session.session) |
|
260 |
self._query_socket |
|
|
310 | connect_socket(self._query_socket, content.query) | |
|
261 | 311 | if content.control: |
|
262 | 312 | self._control_socket = self.context.socket(zmq.PAIR) |
|
263 | 313 | self._control_socket.setsockopt(zmq.IDENTITY, self.session.session) |
|
264 |
self._control_socket |
|
|
314 | connect_socket(self._control_socket, content.control) | |
|
265 | 315 | self._update_engines(dict(content.engines)) |
|
266 | 316 | |
|
267 | 317 | else: |
@@ -852,4 +902,4 class AsynClient(Client): | |||
|
852 | 902 | for stream in (self.queue_stream, self.notifier_stream, |
|
853 | 903 | self.task_stream, self.control_stream): |
|
854 | 904 | stream.flush() |
|
855 | No newline at end of file | |
|
905 |
@@ -1,12 +1,21 | |||
|
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 | 13 | # Imports |
|
5 | #----------------------------------------- | |
|
14 | #----------------------------------------------------------------------------- | |
|
6 | 15 | |
|
7 | 16 | from __future__ import print_function |
|
8 | 17 | |
|
9 | import os,sys | |
|
18 | import os,sys, atexit | |
|
10 | 19 | from multiprocessing import Process |
|
11 | 20 | from getpass import getpass, getuser |
|
12 | 21 | |
@@ -16,20 +25,137 except ImportError: | |||
|
16 | 25 | paramiko = None |
|
17 | 26 | else: |
|
18 | 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 | 120 | """Create an ssh tunnel using command-line ssh that connects port lport |
|
25 | 121 | on this machine to localhost:rport on server. The tunnel |
|
26 | 122 | will automatically close when not in use, remaining open |
|
27 | 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 | 155 | ssh="ssh " |
|
30 | 156 | if keyfile: |
|
31 | 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 | 159 | tunnel = pexpect.spawn(cmd) |
|
34 | 160 | failed = False |
|
35 | 161 | while True: |
@@ -48,7 +174,10 def launch_ssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, | |||
|
48 | 174 | else: |
|
49 | 175 | if failed: |
|
50 | 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 | 181 | failed = True |
|
53 | 182 | |
|
54 | 183 | def _split_server(server): |
@@ -63,28 +192,62 def _split_server(server): | |||
|
63 | 192 | port = 22 |
|
64 | 193 | return username, server, port |
|
65 | 194 | |
|
66 |
def |
|
|
67 |
"""launch a tunner with paramiko in a subprocess |
|
|
195 | def paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=15): | |
|
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 | 227 | if paramiko is None: |
|
69 | 228 | raise ImportError("Paramiko not available") |
|
70 | server = _split_server(server) | |
|
71 |
if |
|
|
72 | passwd = getpass("%s@%s's password: "%(server[0], server[1])) | |
|
73 | else: | |
|
74 | passwd = None | |
|
229 | ||
|
230 | if password is None: | |
|
231 | if not _check_passwordless_paramiko(server, keyfile): | |
|
232 | password = getpass("%s's password: "%(server)) | |
|
233 | ||
|
75 | 234 | p = Process(target=_paramiko_tunnel, |
|
76 | 235 | args=(lport, rport, server, remoteip), |
|
77 | kwargs=dict(keyfile=keyfile, password=passwd)) | |
|
236 | kwargs=dict(keyfile=keyfile, password=password)) | |
|
78 | 237 | p.daemon=False |
|
79 | 238 | p.start() |
|
239 | atexit.register(_shutdown_process, p) | |
|
80 | 240 | return p |
|
81 | 241 | |
|
242 | def _shutdown_process(p): | |
|
243 | if p.isalive(): | |
|
244 | p.terminate() | |
|
82 | 245 | |
|
83 | 246 | def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None): |
|
84 | 247 | """function for actually starting a paramiko tunnel, to be passed |
|
85 | 248 | to multiprocessing.Process(target=this). |
|
86 | 249 | """ |
|
87 | username, server, port = server | |
|
250 | username, server, port = _split_server(server) | |
|
88 | 251 | client = paramiko.SSHClient() |
|
89 | 252 | client.load_system_host_keys() |
|
90 | 253 | client.set_missing_host_key_policy(paramiko.WarningPolicy()) |
@@ -92,20 +255,34 def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None | |||
|
92 | 255 | try: |
|
93 | 256 | client.connect(server, port, username=username, key_filename=keyfile, |
|
94 | 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 | 264 | except Exception as e: |
|
96 | 265 | print ('*** Failed to connect to %s:%d: %r' % (server, port, e)) |
|
97 | 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 | 270 | try: |
|
102 | 271 | forward_tunnel(lport, remoteip, rport, client.get_transport()) |
|
103 | 272 | except KeyboardInterrupt: |
|
104 |
print (' |
|
|
273 | print ('SIGINT: Port forwarding stopped cleanly') | |
|
105 | 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