1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 """\
21 L{X2GoSSHProxy} class - providing a forwarding tunnel for connecting to servers behind firewalls.
22
23 """
24 __NAME__ = 'x2gosshproxy-pylib'
25
26
27 import gevent
28 import os
29 import copy
30 import paramiko
31 import threading
32 import types
33
34 import string
35 import random
36
37
38 import forward
39 import checkhosts
40 import log
41 import utils
42 import x2go_exceptions
43
44 from x2go.defaults import CURRENT_LOCAL_USER as _CURRENT_LOCAL_USER
45 from x2go.defaults import LOCAL_HOME as _LOCAL_HOME
46 from x2go.defaults import X2GO_SSH_ROOTDIR as _X2GO_SSH_ROOTDIR
47
48 import x2go._paramiko
49 x2go._paramiko.monkey_patch_paramiko()
50
52 """\
53 X2GoSSHProxy can be used to proxy X2Go connections through a firewall via SSH.
54
55 """
56 fw_tunnel = None
57
58 - def __init__(self, hostname=None, port=22, username=None, password=None, passphrase=None, force_password_auth=False, key_filename=None,
59 local_host='localhost', local_port=22022, remote_host='localhost', remote_port=22,
60 known_hosts=None, add_to_known_hosts=False, pkey=None, look_for_keys=False, allow_agent=False,
61 sshproxy_host=None, sshproxy_port=22, sshproxy_user=None,
62 sshproxy_password=None, sshproxy_force_password_auth=False, sshproxy_key_filename=None, sshproxy_pkey=None, sshproxy_passphrase=None,
63 sshproxy_look_for_keys=False, sshproxy_allow_agent=False,
64 sshproxy_tunnel=None,
65 ssh_rootdir=os.path.join(_LOCAL_HOME, _X2GO_SSH_ROOTDIR),
66 session_instance=None,
67 logger=None, loglevel=log.loglevel_DEFAULT, ):
68 """\
69 Initialize an X2GoSSHProxy instance. Use an instance of this class to tunnel X2Go requests through
70 a proxying SSH server (i.e. to subLANs that are separated by firewalls or to private IP subLANs that
71 are NATted behind routers).
72
73 @param username: login user name to be used on the SSH proxy host
74 @type username: C{str}
75 @param password: user's password on the SSH proxy host, with private key authentication it will be
76 used to unlock the key (if needed)
77 @type password: C{str}
78 @param passphrase: a passphrase to use for unlocking
79 a private key in case the password is already needed for two-factor
80 authentication
81 @type passphrase: {str}
82 @param key_filename: name of a SSH private key file
83 @type key_filename: C{str}
84 @param pkey: a private DSA/RSA key object (as provided by Paramiko/SSH)
85 @type pkey: C{RSA/DSA key instance}
86 @param force_password_auth: enforce password authentication even if a key(file) is present
87 @type force_password_auth: C{bool}
88 @param look_for_keys: look for key files with standard names and try those if any can be found
89 @type look_for_keys: C{bool}
90 @param allow_agent: try authentication via a locally available SSH agent
91 @type allow_agent: C{bool}
92 @param local_host: bind SSH tunnel to the C{local_host} IP socket address (default: localhost)
93 @type local_host: C{str}
94 @param local_port: IP socket port to bind the SSH tunnel to (default; 22022)
95 @type local_port: C{int}
96 @param remote_host: remote endpoint of the SSH proxying/forwarding tunnel (default: localhost)
97 @type remote_host: C{str}
98 @param remote_port: remote endpoint's IP socket port for listening SSH daemon (default: 22)
99 @type remote_port: C{int}
100 @param known_hosts: full path to a custom C{known_hosts} file
101 @type known_hosts: C{str}
102 @param add_to_known_hosts: automatically add host keys of unknown SSH hosts to the C{known_hosts} file
103 @type add_to_known_hosts: C{bool}
104 @param hostname: alias for C{local_host}
105 @type hostname: C{str}
106 @param port: alias for C{local_port}
107 @type port: C{int}
108 @param sshproxy_host: alias for C{hostname}
109 @type sshproxy_host: C{str}
110 @param sshproxy_port: alias for C{post}
111 @type sshproxy_port: C{int}
112 @param sshproxy_user: alias for C{username}
113 @type sshproxy_user: C{str}
114 @param sshproxy_password: alias for C{password}
115 @type sshproxy_password: C{str}
116 @param sshproxy_passphrase: alias for C{passphrase}
117 @type sshproxy_passphrase: C{str}
118 @param sshproxy_key_filename: alias for C{key_filename}
119 @type sshproxy_key_filename: C{str}
120 @param sshproxy_pkey: alias for C{pkey}
121 @type sshproxy_pkey: C{RSA/DSA key instance} (Paramiko)
122 @param sshproxy_force_password_auth: alias for C{force_password_auth}
123 @type sshproxy_force_password_auth: C{bool}
124 @param sshproxy_look_for_keys: alias for C{look_for_keys}
125 @type sshproxy_look_for_keys: C{bool}
126 @param sshproxy_allow_agent: alias for C{allow_agent}
127 @type sshproxy_allow_agent: C{bool}
128
129 @param sshproxy_tunnel: a string of the format <local_host>:<local_port>:<remote_host>:<remote_port>
130 which will override---if used---the options: C{local_host}, C{local_port}, C{remote_host} and C{remote_port}
131 @type sshproxy_tunnel: C{str}
132
133 @param ssh_rootdir: local user's SSH base directory (default: ~/.ssh)
134 @type ssh_rootdir: C{str}
135 @param session_instance: the L{X2GoSession} instance that builds up this SSH proxying tunnel
136 @type session_instance: L{X2GoSession} instance
137 @param logger: you can pass an L{X2GoLogger} object to the
138 L{X2GoSSHProxy} constructor
139 @type logger: L{X2GoLogger} instance
140 @param loglevel: if no L{X2GoLogger} object has been supplied a new one will be
141 constructed with the given loglevel
142 @type loglevel: int
143
144 @raise X2GoSSHProxyAuthenticationException: if the SSH proxy caused a C{paramiko.AuthenticationException}
145 @raise X2GoSSHProxyException: if the SSH proxy caused a C{paramiko.SSHException}
146 """
147 if logger is None:
148 self.logger = log.X2GoLogger(loglevel=loglevel)
149 else:
150 self.logger = copy.deepcopy(logger)
151 self.logger.tag = __NAME__
152
153 if hostname and hostname in (types.UnicodeType, types.StringType):
154 hostname = [hostname]
155 if hostname and hostname in (types.ListType, types.TupleType):
156 hostname = random.choice(list(hostname))
157 self.hostname, self.port, self.username = hostname, port, username
158
159 if sshproxy_port: self.port = sshproxy_port
160
161
162
163 if sshproxy_host:
164 if sshproxy_host and type(sshproxy_host) in (types.UnicodeType, types.StringType):
165 sshproxy_host = [sshproxy_host]
166 if type(sshproxy_host) in (types.ListType, types.TupleType):
167 sshproxy_host = random.choice(list(sshproxy_host))
168 if sshproxy_host.find(':'):
169 self.hostname = sshproxy_host.split(':')[0]
170 try: self.port = int(sshproxy_host.split(':')[1])
171 except IndexError: pass
172 else:
173 self.hostname = sshproxy_host
174
175 if sshproxy_user: self.username = sshproxy_user
176 if sshproxy_password: password = sshproxy_password
177 if sshproxy_passphrase: passphrase = sshproxy_passphrase
178 if sshproxy_force_password_auth: force_password_auth = sshproxy_force_password_auth
179 if sshproxy_key_filename: key_filename = sshproxy_key_filename
180 if sshproxy_pkey: pkey = sshproxy_pkey
181 if sshproxy_look_for_keys: look_for_keys = sshproxy_look_for_keys
182 if sshproxy_allow_agent: allow_agent = sshproxy_allow_agent
183 if sshproxy_tunnel:
184 self.local_host, self.local_port, self.remote_host, self.remote_port = sshproxy_tunnel.split(':')
185 self.local_port = int(self.local_port)
186 self.remote_port = int(self.remote_port)
187 else:
188 if local_host and type(local_host) in (types.UnicodeType, types.StringType):
189 local_host = [local_host]
190 if local_host and type(local_host) in (types.ListType, types.TupleType):
191 local_host = random.choice(list(local_host))
192 if remote_host and type(remote_host) in (types.UnicodeType, types.StringType):
193 remote_host = [remote_host]
194 if remote_host and type(remote_host) in (types.ListType, types.TupleType):
195 remote_host = random.choice(remote_host)
196 print "LOCAL_HOST: ", local_host
197 self.local_host = local_host
198 self.local_port = int(local_port)
199 self.remote_host = remote_host
200 self.remote_port = int(remote_port)
201
202
203 self.hostname = self.hostname.strip()
204 self.local_host = self.local_host.strip()
205 self.remote_host = self.remote_host.strip()
206
207
208 if look_for_keys:
209 key_filename = None
210 pkey = None
211
212 if key_filename and "~" in key_filename:
213 key_filename = os.path.expanduser(key_filename)
214
215 if password and (passphrase is None): passphrase = password
216
217
218 _hostname = self.hostname
219 if _hostname in ('localhost', 'localhost.localdomain'):
220 _hostname = '127.0.0.1'
221 if self.local_host in ('localhost', 'localhost.localdomain'):
222 self.local_host = '127.0.0.1'
223 if self.remote_host in ('localhost', 'localhost.localdomain'):
224 self.remote_host = '127.0.0.1'
225
226 if username is None:
227 username = _CURRENT_LOCAL_USER
228
229 if type(password) not in (types.StringType, types.UnicodeType):
230 password = ''
231
232 self._keepalive = True
233 self.session_instance = session_instance
234
235 self.client_instance = None
236 if self.session_instance is not None:
237 self.client_instance = self.session_instance.get_client_instance()
238
239 self.ssh_rootdir = ssh_rootdir
240 paramiko.SSHClient.__init__(self)
241
242 self.known_hosts = known_hosts
243 if self.known_hosts:
244 utils.touch_file(self.known_hosts)
245 self.load_host_keys(self.known_hosts)
246
247 if not add_to_known_hosts and session_instance:
248 self.set_missing_host_key_policy(checkhosts.X2GoInteractiveAddPolicy(caller=self, session_instance=session_instance))
249
250 if add_to_known_hosts:
251 self.set_missing_host_key_policy(paramiko.AutoAddPolicy())
252
253 try:
254 if key_filename or pkey or look_for_keys or allow_agent or (password and force_password_auth):
255 try:
256 if password and force_password_auth:
257 self.connect(_hostname, port=self.port,
258 username=self.username,
259 password=password,
260 key_filename=None,
261 pkey=None,
262 look_for_keys=False,
263 allow_agent=False,
264 )
265 elif (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey:
266 self.connect(_hostname, port=self.port,
267 username=self.username,
268 key_filename=key_filename,
269 pkey=pkey,
270 allow_agent=False,
271 look_for_keys=False,
272 )
273 else:
274 self.connect(_hostname, port=self.port,
275 username=self.username,
276 key_filename=None,
277 pkey=None,
278 look_for_keys=look_for_keys,
279 allow_agent=allow_agent,
280 )
281
282 except (paramiko.PasswordRequiredException, paramiko.SSHException), e:
283 self.close()
284 if type(e) == paramiko.SSHException and str(e).startswith('Two-factor authentication requires a password'):
285 self.logger('SSH proxy host requests two-factor authentication', loglevel=log.loglevel_NOTICE)
286 raise x2go_exceptions.X2GoSSHProxyException(str(e))
287
288 if passphrase is None:
289 try:
290 if not password: password = None
291 if (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey:
292 try:
293 self.connect(_hostname, port=self.port,
294 username=self.username,
295 password=password,
296 passphrase=passphrase,
297 key_filename=key_filename,
298 pkey=pkey,
299 allow_agent=False,
300 look_for_keys=False,
301 )
302 except TypeError:
303 self.connect(_hostname, port=self.port,
304 username=self.username,
305 password=passphrase,
306 key_filename=key_filename,
307 pkey=pkey,
308 allow_agent=False,
309 look_for_keys=False,
310 )
311 else:
312 try:
313 self.connect(_hostname, port=self.port,
314 username=self.username,
315 password=password,
316 passphrase=passphrase,
317 key_filename=None,
318 pkey=None,
319 look_for_keys=look_for_keys,
320 allow_agent=allow_agent,
321 )
322 except TypeError:
323 self.connect(_hostname, port=self.port,
324 username=self.username,
325 password=passphrase,
326 key_filename=None,
327 pkey=None,
328 look_for_keys=look_for_keys,
329 allow_agent=allow_agent,
330 )
331 except x2go_exceptions.AuthenticationException, auth_e:
332 raise x2go_exceptions.X2GoSSHProxyAuthenticationException(str(auth_e))
333
334 else:
335 if type(e) == paramiko.SSHException:
336 raise x2go_exceptions.X2GoSSHProxyException(str(e))
337 elif type(e) == paramiko.PasswordRequiredException:
338 raise x2go_exceptions.X2GoSSHProxyPasswordRequiredException(str(e))
339 except x2go_exceptions.AuthenticationException:
340 self.close()
341 raise x2go_exceptions.X2GoSSHProxyAuthenticationException('all authentication mechanisms with SSH proxy host failed')
342 except x2go_exceptions.SSHException:
343 self.close()
344 raise x2go_exceptions.X2GoSSHProxyAuthenticationException('with SSH proxy host password authentication is required')
345 except:
346 raise
347
348
349 t = self.get_transport()
350 if x2go._paramiko.PARAMIKO_FEATURE['use-compression']:
351 t.use_compression(compress=False)
352 t.set_keepalive(5)
353
354
355 else:
356
357 if not password:
358 password = "".join([random.choice(string.letters+string.digits) for x in range(1, 20)])
359 try:
360 self.connect(_hostname, port=self.port,
361 username=self.username,
362 password=password,
363 look_for_keys=False,
364 allow_agent=False,
365 )
366 except x2go_exceptions.AuthenticationException:
367 self.close()
368 raise x2go_exceptions.X2GoSSHProxyAuthenticationException('interactive auth mechanisms failed')
369 except:
370 self.close()
371 raise
372
373 except (x2go_exceptions.SSHException, IOError), e:
374 self.close()
375 raise x2go_exceptions.X2GoSSHProxyException(str(e))
376 except:
377 self.close()
378 raise
379
380
381 self.set_missing_host_key_policy(paramiko.RejectPolicy())
382 threading.Thread.__init__(self)
383 self.daemon = True
384
386 """\
387 Wraps around a Paramiko/SSH host key check.
388
389 """
390 _hostname = self.hostname
391
392
393 if _hostname in ('localhost', 'localhost.localdomain'):
394 _hostname = '127.0.0.1'
395
396 _valid = False
397 (_valid, _hostname, _port, _fingerprint, _fingerprint_type) = checkhosts.check_ssh_host_key(self, _hostname, port=self.port)
398 if not _valid and self.session_instance:
399 _valid = self.session_instance.HOOK_check_host_dialog(self.remote_host, self.remote_port, fingerprint=_fingerprint, fingerprint_type=_fingerprint_type)
400 return _valid
401
403 """\
404 Start the SSH proxying tunnel...
405
406 @raise X2GoSSHProxyException: if the SSH proxy could not retrieve an SSH transport for proxying a X2Go server-client connection
407
408 """
409 if self.get_transport() is not None and self.get_transport().is_authenticated():
410 self.local_port = utils.detect_unused_port(bind_address=self.local_host, preferred_port=self.local_port)
411 self.fw_tunnel = forward.start_forward_tunnel(local_host=self.local_host,
412 local_port=self.local_port,
413 remote_host=self.remote_host,
414 remote_port=self.remote_port,
415 ssh_transport=self.get_transport(),
416 logger=self.logger, )
417 self.logger('SSH proxy tunnel via [%s]:%s has been set up' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
418 self.logger('SSH proxy tunnel startpoint is [%s]:%s, endpoint is [%s]:%s' % (self.local_host, self.local_port, self.remote_host, self.remote_port), loglevel=log.loglevel_NOTICE)
419
420 while self._keepalive:
421 gevent.sleep(.1)
422
423 else:
424 raise x2go_exceptions.X2GoSSHProxyException('SSH proxy connection could not retrieve an SSH transport')
425
427 """\
428 Retrieve the local IP socket address this SSH proxying tunnel is (about to) bind/bound to.
429
430 @return: local IP socket address
431 @rtype: C{str}
432
433 """
434 return self.local_host
435
437 """\
438 Retrieve the local IP socket port this SSH proxying tunnel is (about to) bind/bound to.
439
440 @return: local IP socket port
441 @rtype: C{int}
442
443 """
444 return self.local_port
445
447 """\
448 Retrieve the remote IP socket address at the remote end of the SSH proxying tunnel.
449
450 @return: remote IP socket address
451 @rtype: C{str}
452
453 """
454 return self.remote_host
455
457 """\
458 Retrieve the remote IP socket port of the target system's SSH daemon.
459
460 @return: remote SSH port
461 @rtype: C{int}
462
463 """
464 return self.remote_port
465
467 """\
468 Tear down the SSH proxying tunnel.
469
470 """
471 if self.fw_tunnel is not None and self.fw_tunnel.is_active:
472 self.logger('taking down SSH proxy tunnel via [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
473 try: forward.stop_forward_tunnel(self.fw_tunnel)
474 except: pass
475 self.fw_tunnel = None
476 self._keepalive = False
477 if self.get_transport() is not None:
478 self.logger('closing SSH proxy connection to [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
479 self.close()
480 self.password = self.sshproxy_password = None
481
483 """\
484 Class desctructor.
485
486 """
487 self.stop_thread()
488