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

Source Code for Module x2go.sshproxy

  1  # -*- coding: utf-8 -*- 
  2   
  3  # Copyright (C) 2010-2015 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 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 # translate between X2GoSession options and paramiko.SSHCLient.connect() options 162 # if <hostname>:<port> is used for sshproxy_host, then this <port> is used 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 # allow more trailing whitespace tolerance in hostnames 203 self.hostname = self.hostname.strip() 204 self.local_host = self.local_host.strip() 205 self.remote_host = self.remote_host.strip() 206 207 # do not use explicitly given keys if look_for_keys has got activated 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 # enforce IPv4 for localhost addresses!!! 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 # since Paramiko 1.7.7.1 there is compression available, let's use it if present... 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 # if there is no private key, we will use the given password, if any 355 else: 356 # create a random password if password is empty to trigger host key validity check 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
385 - def check_host(self):
386 """\ 387 Wraps around a Paramiko/SSH host key check. 388 389 """ 390 _hostname = self.hostname 391 392 # force into IPv4 for localhost connections 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
402 - def run(self):
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
426 - def get_local_proxy_host(self):
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
436 - def get_local_proxy_port(self):
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
446 - def get_remote_host(self):
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
456 - def get_remote_port(self):
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
466 - def stop_thread(self):
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
482 - def __del__(self):
483 """\ 484 Class desctructor. 485 486 """ 487 self.stop_thread()
488