Package x2go :: Module sshproxy
[frames] | no frames]

Source Code for Module x2go.sshproxy

  1  # -*- coding: utf-8 -*- 
  2   
  3  # Copyright (C) 2010-2014 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de> 
  4  # 
  5  # Python X2Go is free software; you can redistribute it and/or modify 
  6  # it under the terms of the GNU Affero General Public License as published by 
  7  # the Free Software Foundation; either version 3 of the License, or 
  8  # (at your option) any later version. 
  9  # 
 10  # Python X2Go is distributed in the hope that it will be useful, 
 11  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 13  # GNU Affero General Public License for more details. 
 14  # 
 15  # You should have received a copy of the GNU Affero General Public License 
 16  # along with this program; if not, write to the 
 17  # Free Software Foundation, Inc., 
 18  # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. 
 19   
 20  """\ 
 21  L{X2GoSSHProxy} class - providing a forwarding tunnel for connecting to servers behind firewalls. 
 22   
 23  """ 
 24  __NAME__ = 'x2gosshproxy-pylib' 
 25   
 26  # modules 
 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  # Python X2Go modules 
 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   
51 -class X2GoSSHProxy(paramiko.SSHClient, threading.Thread):
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 # translate between X2GoSession options and paramiko.SSHCLient.connect() options 158 # if <hostname>:<port> is used for sshproxy_host, then this <port> is used 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 # allow more trailing whitespace tolerance in hostnames 186 self.hostname = self.hostname.strip() 187 self.local_host = self.local_host.strip() 188 self.remote_host = self.remote_host.strip() 189 190 # do not use explicitly given keys if look_for_keys has got activated 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 # enforce IPv4 for localhost addresses!!! 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 # since Paramiko 1.7.7.1 there is compression available, let's use it if present... 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 # if there is no private key, we will use the given password, if any 338 else: 339 # create a random password if password is empty to trigger host key validity check 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
368 - def check_host(self):
369 """\ 370 Wraps around a Paramiko/SSH host key check. 371 372 """ 373 _hostname = self.hostname 374 375 # force into IPv4 for localhost connections 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
385 - def run(self):
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
418 - def get_local_proxy_host(self):
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
428 - def get_local_proxy_port(self):
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
438 - def get_remote_host(self):
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
448 - def get_remote_port(self):
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
458 - def stop_thread(self):
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
474 - def __del__(self):
475 """\ 476 Class desctructor. 477 478 """ 479 self.stop_thread()
480