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 self.hostname, self.port, self.username = hostname, port, username
154
155 if sshproxy_port: self.port = sshproxy_port
156
157
158
159 if sshproxy_host:
160 if sshproxy_host.find(':'):
161 self.hostname = sshproxy_host.split(':')[0]
162 try: self.port = int(sshproxy_host.split(':')[1])
163 except IndexError: pass
164 else:
165 self.hostname = sshproxy_host
166
167 if sshproxy_user: self.username = sshproxy_user
168 if sshproxy_password: password = sshproxy_password
169 if sshproxy_passphrase: passphrase = sshproxy_passphrase
170 if sshproxy_force_password_auth: force_password_auth = sshproxy_force_password_auth
171 if sshproxy_key_filename: key_filename = sshproxy_key_filename
172 if sshproxy_pkey: pkey = sshproxy_pkey
173 if sshproxy_look_for_keys: look_for_keys = sshproxy_look_for_keys
174 if sshproxy_allow_agent: allow_agent = sshproxy_allow_agent
175 if sshproxy_tunnel:
176 self.local_host, self.local_port, self.remote_host, self.remote_port = sshproxy_tunnel.split(':')
177 self.local_port = int(self.local_port)
178 self.remote_port = int(self.remote_port)
179 else:
180 self.local_host = local_host
181 self.local_port = int(local_port)
182 self.remote_host = remote_host
183 self.remote_port = int(remote_port)
184
185
186 self.hostname = self.hostname.strip()
187 self.local_host = self.local_host.strip()
188 self.remote_host = self.remote_host.strip()
189
190
191 if look_for_keys:
192 key_filename = None
193 pkey = None
194
195 if key_filename and "~" in key_filename:
196 key_filename = os.path.expanduser(key_filename)
197
198 if password and (passphrase is None): passphrase = password
199
200
201 _hostname = self.hostname
202 if _hostname in ('localhost', 'localhost.localdomain'):
203 _hostname = '127.0.0.1'
204 if self.local_host in ('localhost', 'localhost.localdomain'):
205 self.local_host = '127.0.0.1'
206 if self.remote_host in ('localhost', 'localhost.localdomain'):
207 self.remote_host = '127.0.0.1'
208
209 if username is None:
210 username = _CURRENT_LOCAL_USER
211
212 if type(password) not in (types.StringType, types.UnicodeType):
213 password = ''
214
215 self._keepalive = True
216 self.session_instance = session_instance
217
218 self.client_instance = None
219 if self.session_instance is not None:
220 self.client_instance = self.session_instance.get_client_instance()
221
222 self.ssh_rootdir = ssh_rootdir
223 paramiko.SSHClient.__init__(self)
224
225 self.known_hosts = known_hosts
226 if self.known_hosts:
227 utils.touch_file(self.known_hosts)
228 self.load_host_keys(self.known_hosts)
229
230 if not add_to_known_hosts and session_instance:
231 self.set_missing_host_key_policy(checkhosts.X2GoInteractiveAddPolicy(caller=self, session_instance=session_instance))
232
233 if add_to_known_hosts:
234 self.set_missing_host_key_policy(paramiko.AutoAddPolicy())
235
236 try:
237 if key_filename or pkey or look_for_keys or allow_agent or (password and force_password_auth):
238 try:
239 if password and force_password_auth:
240 self.connect(_hostname, port=self.port,
241 username=self.username,
242 password=password,
243 key_filename=None,
244 pkey=None,
245 look_for_keys=False,
246 allow_agent=False,
247 )
248 elif (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey:
249 self.connect(_hostname, port=self.port,
250 username=self.username,
251 key_filename=key_filename,
252 pkey=pkey,
253 allow_agent=False,
254 look_for_keys=False,
255 )
256 else:
257 self.connect(_hostname, port=self.port,
258 username=self.username,
259 key_filename=None,
260 pkey=None,
261 look_for_keys=look_for_keys,
262 allow_agent=allow_agent,
263 )
264
265 except (paramiko.PasswordRequiredException, paramiko.SSHException), e:
266 self.close()
267 if type(e) == paramiko.SSHException and str(e).startswith('Two-factor authentication requires a password'):
268 self.logger('SSH proxy host requests two-factor authentication', loglevel=log.loglevel_NOTICE)
269 raise x2go_exceptions.X2GoSSHProxyException(str(e))
270
271 if passphrase is None:
272 try:
273 if not password: password = None
274 if (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey:
275 try:
276 self.connect(_hostname, port=self.port,
277 username=self.username,
278 password=password,
279 passphrase=passphrase,
280 key_filename=key_filename,
281 pkey=pkey,
282 allow_agent=False,
283 look_for_keys=False,
284 )
285 except TypeError:
286 self.connect(_hostname, port=self.port,
287 username=self.username,
288 password=passphrase,
289 key_filename=key_filename,
290 pkey=pkey,
291 allow_agent=False,
292 look_for_keys=False,
293 )
294 else:
295 try:
296 self.connect(_hostname, port=self.port,
297 username=self.username,
298 password=password,
299 passphrase=passphrase,
300 key_filename=None,
301 pkey=None,
302 look_for_keys=look_for_keys,
303 allow_agent=allow_agent,
304 )
305 except TypeError:
306 self.connect(_hostname, port=self.port,
307 username=self.username,
308 password=passphrase,
309 key_filename=None,
310 pkey=None,
311 look_for_keys=look_for_keys,
312 allow_agent=allow_agent,
313 )
314 except x2go_exceptions.AuthenticationException, auth_e:
315 raise x2go_exceptions.X2GoSSHProxyAuthenticationException(str(auth_e))
316
317 else:
318 if type(e) == paramiko.SSHException:
319 raise x2go_exceptions.X2GoSSHProxyException(str(e))
320 elif type(e) == paramiko.PasswordRequiredException:
321 raise x2go_exceptions.X2GoSSHProxyPasswordRequiredException(str(e))
322 except x2go_exceptions.AuthenticationException:
323 self.close()
324 raise x2go_exceptions.X2GoSSHProxyAuthenticationException('all authentication mechanisms with SSH proxy host failed')
325 except x2go_exceptions.SSHException:
326 self.close()
327 raise x2go_exceptions.X2GoSSHProxyAuthenticationException('with SSH proxy host password authentication is required')
328 except:
329 raise
330
331
332 t = self.get_transport()
333 if x2go._paramiko.PARAMIKO_FEATURE['use-compression']:
334 t.use_compression(compress=True)
335 t.set_keepalive(5)
336
337
338 else:
339
340 if not password:
341 password = "".join([random.choice(string.letters+string.digits) for x in range(1, 20)])
342 try:
343 self.connect(_hostname, port=self.port,
344 username=self.username,
345 password=password,
346 look_for_keys=False,
347 allow_agent=False,
348 )
349 except x2go_exceptions.AuthenticationException:
350 self.close()
351 raise x2go_exceptions.X2GoSSHProxyAuthenticationException('interactive auth mechanisms failed')
352 except:
353 self.close()
354 raise
355
356 except (x2go_exceptions.SSHException, IOError), e:
357 self.close()
358 raise x2go_exceptions.X2GoSSHProxyException(str(e))
359 except:
360 self.close()
361 raise
362
363
364 self.set_missing_host_key_policy(paramiko.RejectPolicy())
365 threading.Thread.__init__(self)
366 self.daemon = True
367
369 """\
370 Wraps around a Paramiko/SSH host key check.
371
372 """
373 _hostname = self.hostname
374
375
376 if _hostname in ('localhost', 'localhost.localdomain'):
377 _hostname = '127.0.0.1'
378
379 _valid = False
380 (_valid, _hostname, _port, _fingerprint, _fingerprint_type) = checkhosts.check_ssh_host_key(self, _hostname, port=self.port)
381 if not _valid and self.session_instance:
382 _valid = self.session_instance.HOOK_check_host_dialog(self.remote_host, self.remote_port, fingerprint=_fingerprint, fingerprint_type=_fingerprint_type)
383 return _valid
384
386 """\
387 Start the SSH proxying tunnel...
388
389 @raise X2GoSSHProxyException: if the SSH proxy could not retrieve an SSH transport for proxying a X2Go server-client connection
390
391 """
392 if self.get_transport() is not None and self.get_transport().is_authenticated():
393 self.local_port = utils.detect_unused_port(bind_address=self.local_host, preferred_port=self.local_port)
394 if self.client_instance is not None:
395 _profile_id = self.session_instance.get_profile_id()
396 if self.client_instance.session_profiles.has_profile(_profile_id):
397 self.client_instance.session_profiles.update_value(_profile_id,
398 'sshproxytunnel',
399 '%s:%s:%s:%s' % (self.local_host, self.local_port, self.remote_host, self.remote_port)
400 )
401 self.client_instance.session_profiles.write_user_config = True
402 self.client_instance.session_profiles.write()
403 self.fw_tunnel = forward.start_forward_tunnel(local_host=self.local_host,
404 local_port=self.local_port,
405 remote_host=self.remote_host,
406 remote_port=self.remote_port,
407 ssh_transport=self.get_transport(),
408 logger=self.logger, )
409 self.logger('SSH proxy tunnel via [%s]:%s has been set up' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
410 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)
411
412 while self._keepalive:
413 gevent.sleep(.1)
414
415 else:
416 raise x2go_exceptions.X2GoSSHProxyException('SSH proxy connection could not retrieve an SSH transport')
417
419 """\
420 Retrieve the local IP socket address this SSH proxying tunnel is (about to) bind/bound to.
421
422 @return: local IP socket address
423 @rtype: C{str}
424
425 """
426 return self.local_host
427
429 """\
430 Retrieve the local IP socket port this SSH proxying tunnel is (about to) bind/bound to.
431
432 @return: local IP socket port
433 @rtype: C{int}
434
435 """
436 return self.local_port
437
439 """\
440 Retrieve the remote IP socket address at the remote end of the SSH proxying tunnel.
441
442 @return: remote IP socket address
443 @rtype: C{str}
444
445 """
446 return self.remote_host
447
449 """\
450 Retrieve the remote IP socket port of the target system's SSH daemon.
451
452 @return: remote SSH port
453 @rtype: C{int}
454
455 """
456 return self.remote_port
457
459 """\
460 Tear down the SSH proxying tunnel.
461
462 """
463 if self.fw_tunnel is not None and self.fw_tunnel.is_active:
464 self.logger('taking down SSH proxy tunnel via [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
465 try: forward.stop_forward_tunnel(self.fw_tunnel)
466 except: pass
467 self.fw_tunnel = None
468 self._keepalive = False
469 if self.get_transport() is not None:
470 self.logger('closing SSH proxy connection to [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
471 self.close()
472 self.password = self.sshproxy_password = None
473
475 """\
476 Class desctructor.
477
478 """
479 self.stop_thread()
480