Package x2go :: Module sshproxy

[frames] | no frames]

# Source Code for Module x2go.sshproxy

      1  # -*- coding: utf-8 -*-
      2
      3  # Copyright (C) 2010-2016 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