Package x2go :: Module sshproxy

[frames] | no frames]

# Source Code for Module x2go.sshproxy

      1  # -*- coding: utf-8 -*-
      2
      3  # Copyright (C) 2010-2013 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
     33  import string
     34  import random
     35
     36  # Python X2Go modules
     37  import forward
     38  import checkhosts
     39  import log
     40  import utils
     41  import x2go_exceptions
     42
     43  from x2go.defaults import CURRENT_LOCAL_USER as _CURRENT_LOCAL_USER
     44  from x2go.defaults import LOCAL_HOME as _LOCAL_HOME
     45  from x2go.defaults import X2GO_SSH_ROOTDIR as _X2GO_SSH_ROOTDIR
     46
     47  import x2go._paramiko
     48  x2go._paramiko.monkey_patch_paramiko()
     49
     50 -class X2GoSSHProxy(paramiko.SSHClient, threading.Thread):
     51      """\
     52      X2GoSSHProxy can be used to proxy X2Go connections through a firewall via SSH.
     53
     54      """
     55      fw_tunnel = None
     56
     57 -    def __init__(self, hostname=None, port=22, username=None, password=None, force_password_auth=False, key_filename=None,
     58                   local_host='localhost', local_port=22022, remote_host='localhost', remote_port=22,
     59                   known_hosts=None, add_to_known_hosts=False, pkey=None, look_for_keys=False, allow_agent=False,
     60                   sshproxy_host=None, sshproxy_port=22, sshproxy_user=None,
     61                   sshproxy_password=None, sshproxy_force_password_auth=False, sshproxy_key_filename=None, sshproxy_pkey=None,
     62                   sshproxy_look_for_keys=False, sshproxy_allow_agent=False,
     63                   sshproxy_tunnel=None,
     64                   ssh_rootdir=os.path.join(_LOCAL_HOME, _X2GO_SSH_ROOTDIR),
     65                   session_instance=None,
     66                   logger=None, loglevel=log.loglevel_DEFAULT, ):
     67          """\
     68          Initialize an X2GoSSHProxy instance. Use an instance of this class to tunnel X2Go requests through
     69          a proxying SSH server (i.e. to subLANs that are separated by firewalls or to private IP subLANs that
     70          are NATted behind routers).
     71
     72          @param username: login user name to be used on the SSH proxy host
     73          @type username: C{str}
     74          @param password: user's password on the SSH proxy host, with private key authentication it will be
     75              used to unlock the key (if needed)
     76          @type password: C{str}
     77          @param key_filename: name of a SSH private key file
     78          @type key_filename: C{str}
     79          @param pkey: a private DSA/RSA key object (as provided by Paramiko/SSH)
     80          @type pkey: C{RSA/DSA key instance}
     81          @param force_password_auth: enforce password authentication even if a key(file) is present
     82          @type force_password_auth: C{bool}
     83          @param look_for_keys: look for key files with standard names and try those if any can be found
     84          @type look_for_keys: C{bool}
     85          @param allow_agent: try authentication via a locally available SSH agent
     86          @type allow_agent: C{bool}
     87          @param local_host: bind SSH tunnel to the C{local_host} IP socket address (default: localhost)
     88          @type local_host: C{str}
     89          @param local_port: IP socket port to bind the SSH tunnel to (default; 22022)
     90          @type local_port: C{int}
     91          @param remote_host: remote endpoint of the SSH proxying/forwarding tunnel (default: localhost)
     92          @type remote_host: C{str}
     93          @param remote_port: remote endpoint's IP socket port for listening SSH daemon (default: 22)
     94          @type remote_port: C{int}
     95          @param known_hosts: full path to a custom C{known_hosts} file
     96          @type known_hosts: C{str}
     97          @param add_to_known_hosts: automatically add host keys of unknown SSH hosts to the C{known_hosts} file
     98          @type add_to_known_hosts: C{bool}
     99          @param hostname: alias for C{local_host}
    100          @type hostname: C{str}
    101          @param port: alias for C{local_port}
    102          @type port: C{int}
    103          @param sshproxy_host: alias for C{hostname}
    104          @type sshproxy_host: C{str}
    105          @param sshproxy_port: alias for C{post}
    106          @type sshproxy_port: C{int}
    107          @param sshproxy_user: alias for C{username}
    108          @type sshproxy_user: C{str}
    109          @param sshproxy_password: alias for C{password}
    110          @type sshproxy_password: C{str}
    111          @param sshproxy_key_filename: alias for C{key_filename}
    112          @type sshproxy_key_filename: C{str}
    113          @param sshproxy_pkey: alias for C{pkey}
    114          @type sshproxy_pkey: C{RSA/DSA key instance} (Paramiko)
    115          @param sshproxy_force_password_auth: alias for C{force_password_auth}
    116          @type sshproxy_force_password_auth: C{bool}
    117          @param sshproxy_look_for_keys: alias for C{look_for_keys}
    118          @type sshproxy_look_for_keys: C{bool}
    119          @param sshproxy_allow_agent: alias for C{allow_agent}
    120          @type sshproxy_allow_agent: C{bool}
    121
    122          @param sshproxy_tunnel: a string of the format <local_host>:<local_port>:<remote_host>:<remote_port>
    123              which will override---if used---the options: C{local_host}, C{local_port}, C{remote_host} and C{remote_port}
    124          @type sshproxy_tunnel: C{str}
    125
    126          @param ssh_rootdir: local user's SSH base directory (default: ~/.ssh)
    127          @type ssh_rootdir: C{str}
    128          @param session_instance: the L{X2GoSession} instance that builds up this SSH proxying tunnel
    129          @type session_instance: L{X2GoSession} instance
    130          @param logger: you can pass an L{X2GoLogger} object to the
    131              L{X2GoSSHProxy} constructor
    132          @type logger: L{X2GoLogger} instance
    133          @param loglevel: if no L{X2GoLogger} object has been supplied a new one will be
    134              constructed with the given loglevel
    135          @type loglevel: int
    136
    137          @raise X2GoSSHProxyAuthenticationException: if the SSH proxy caused a C{paramiko.AuthenticationException}
    138          @raise X2GoSSHProxyException: if the SSH proxy caused a C{paramiko.SSHException}
    139          """
    140          if logger is None:
    141              self.logger = log.X2GoLogger(loglevel=loglevel)
    142          else:
    143              self.logger = copy.deepcopy(logger)
    144          self.logger.tag = __NAME__
    145
    146          self.hostname, self.port, self.username = hostname, port, username
    147
    148          if sshproxy_port: self.port = sshproxy_port
    149
    150          # translate between X2GoSession options and paramiko.SSHCLient.connect() options
    151          # if <hostname>:<port> is used for sshproxy_host, then this <port> is used
    152          if sshproxy_host:
    153              if sshproxy_host.find(':'):
    154                  self.hostname = sshproxy_host.split(':')[0]
    155                  try: self.port = int(sshproxy_host.split(':')[1])
    156                  except IndexError: pass
    157              else:
    158                  self.hostname = sshproxy_host
    159
    160          if sshproxy_user: self.username = sshproxy_user
    161          if sshproxy_password: password = sshproxy_password
    162          if sshproxy_force_password_auth: force_password_auth = sshproxy_force_password_auth
    163          if sshproxy_key_filename: key_filename = sshproxy_key_filename
    164          if sshproxy_pkey: pkey = sshproxy_pkey
    165          if sshproxy_look_for_keys: look_for_keys = sshproxy_look_for_keys
    166          if sshproxy_allow_agent: allow_agent = sshproxy_allow_agent
    167          if sshproxy_tunnel:
    168              self.local_host, self.local_port, self.remote_host, self.remote_port = sshproxy_tunnel.split(':')
    169              self.local_port = int(self.local_port)
    170              self.remote_port = int(self.remote_port)
    171          else:
    172              self.local_host = local_host
    173              self.local_port = int(local_port)
    174              self.remote_host = remote_host
    175              self.remote_port = int(remote_port)
    176
    177          # allow more trailing whitespace tolerance in hostnames
    178          self.hostname = self.hostname.strip()
    179          self.local_host = self.local_host.strip()
    180          self.remote_host = self.remote_host.strip()
    181
    182          # enforce IPv4 for localhost addresses!!!
    183          _hostname = self.hostname
    184          if _hostname in ('localhost', 'localhost.localdomain'):
    185              _hostname = '127.0.0.1'
    186          if self.local_host in ('localhost', 'localhost.localdomain'):
    187              self.local_host = '127.0.0.1'
    188          if self.remote_host in ('localhost', 'localhost.localdomain'):
    189              self.remote_host = '127.0.0.1'
    190
    191          if username is None:
    192              username = _CURRENT_LOCAL_USER
    193
    194          self._keepalive = True
    195          self.session_instance = session_instance
    196
    197          self.client_instance = None
    198          if self.session_instance is not None:
    199              self.client_instance = self.session_instance.get_client_instance()
    200
    201          self.ssh_rootdir = ssh_rootdir
    202          paramiko.SSHClient.__init__(self)
    203
    204          self.known_hosts = known_hosts
    205          if self.known_hosts:
    206              utils.touch_file(self.known_hosts)
    207              self.load_host_keys(self.known_hosts)
    208
    209          if not add_to_known_hosts and session_instance:
    210              self.set_missing_host_key_policy(checkhosts.X2GoInteractiveAddPolicy(caller=self, session_instance=session_instance))
    211
    212          if add_to_known_hosts:
    213              self.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    214
    215          try:
    216              if key_filename or pkey or look_for_keys or allow_agent or (password and force_password_auth):
    217                  try:
    218                      if password and force_password_auth:
    219                          self.connect(_hostname, port=self.port,
    220                                       username=self.username,
    221                                       password=password,
    222                                       key_filename=None,
    223                                       pkey=None,
    224                                       look_for_keys=False,
    225                                       allow_agent=False,
    226                                      )
    227                      elif (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey:
    228                          self.connect(_hostname, port=self.port,
    229                                       username=self.username,
    230                                       key_filename=key_filename,
    231                                       pkey=pkey,
    232                                       look_for_keys=look_for_keys,
    233                                       allow_agent=allow_agent,
    234                                      )
    235                      else:
    236                          self.connect(_hostname, port=self.port,
    237                                       username=self.username,
    238                                       look_for_keys=look_for_keys,
    239                                       allow_agent=allow_agent,
    240                                      )
    241
    242                  except x2go_exceptions.AuthenticationException, e:
    243                      self.close()
    244                      raise x2go_exceptions.X2GoSSHProxyAuthenticationException('pubkey auth mechanisms both failed')
    245                  except x2go_exceptions.SSHException, e:
    246                      self.close()
    247                      raise x2go_exceptions.X2GoSSHProxyAuthenticationException('interactive authentication required')
    248                  except:
    249                      raise
    250
    251                  # since Paramiko 1.7.7.1 there is compression available, let's use it if present...
    252                  t = self.get_transport()
    253                  if x2go._paramiko.PARAMIKO_FEATURE['use-compression']:
    254                      t.use_compression(compress=True)
    255
    256              # if there is no private key, we will use the given password, if any
    257              else:
    258                  # create a random password if password is empty to trigger host key validity check
    259                  if not password:
    260                      password = "".join([random.choice(string.letters+string.digits) for x in range(1, 20)])
    261                  try:
    262                      self.connect(_hostname, port=self.port,
    263                                   username=self.username,
    264                                   password=password,
    265                                   look_for_keys=False,
    266                                   allow_agent=False,
    267                                  )
    268                  except x2go_exceptions.AuthenticationException:
    269                      self.close()
    270                      raise x2go_exceptions.X2GoSSHProxyAuthenticationException('interactive auth mechanisms failed')
    271                  except:
    272                      self.close()
    273                      raise
    274
    275          except (x2go_exceptions.SSHException, IOError), e:
    276              self.close()
    277              raise x2go_exceptions.X2GoSSHProxyException(str(e))
    278          except:
    279              self.close()
    280              raise
    281
    282          self.set_missing_host_key_policy(paramiko.RejectPolicy())
    283          threading.Thread.__init__(self)
    284          self.daemon = True
    285
    286 -    def check_host(self):
    287          """\
    288          Wraps around a Paramiko/SSH host key check.
    289
    290          """
    291          _hostname = self.hostname
    292
    293          # force into IPv4 for localhost connections
    294          if _hostname in ('localhost', 'localhost.localdomain'):
    295              _hostname = '127.0.0.1'
    296
    297          _valid = False
    298          (_valid, _hostname, _port, _fingerprint, _fingerprint_type) = checkhosts.check_ssh_host_key(self, _hostname, port=self.port)
    299          if not _valid and self.session_instance:
    300              _valid = self.session_instance.HOOK_check_host_dialog(self.remote_host, self.remote_port, fingerprint=_fingerprint, fingerprint_type=_fingerprint_type)
    301          return _valid
    302
    303 -    def run(self):
    304          """\
    305          Start the SSH proxying tunnel...
    306
    307          @raise X2GoSSHProxyException: if the SSH proxy could not retrieve an SSH transport for proxying a X2Go server-client connection
    308
    309          """
    310          if self.get_transport() is not None and self.get_transport().is_authenticated():
    311              self.local_port = utils.detect_unused_port(bind_address=self.local_host, preferred_port=self.local_port)
    312              if self.client_instance is not None:
    313                  _profile_id = self.session_instance.get_profile_id()
    314                  if self.client_instance.session_profiles.has_profile(_profile_id):
    315                      self.client_instance.session_profiles.update_value(_profile_id,
    316                                                                         'sshproxytunnel',
    317                                                                         '%s:%s:%s:%s' % (self.local_host, self.local_port, self.remote_host, self.remote_port)
    318                                                                        )
    319                  self.client_instance.session_profiles.write_user_config = True
    320                  self.client_instance.session_profiles.write()
    321              self.fw_tunnel = forward.start_forward_tunnel(local_host=self.local_host,
    322                                                            local_port=self.local_port,
    323                                                            remote_host=self.remote_host,
    324                                                            remote_port=self.remote_port,
    325                                                            ssh_transport=self.get_transport(),
    326                                                            logger=self.logger, )
    327              self.logger('SSH proxy tunnel via [%s]:%s has been set up' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
    328              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)
    329
    330              while self._keepalive:
    331                  gevent.sleep(.1)
    332
    333          else:
    334              raise x2go_exceptions.X2GoSSHProxyException('SSH proxy connection could not retrieve an SSH transport')
    335
    336 -    def get_local_proxy_host(self):
    337          """\
    338          Retrieve the local IP socket address this SSH proxying tunnel is (about to) bind/bound to.
    339
    340          @return: local IP socket address
    341          @rtype: C{str}
    342
    343          """
    344          return self.local_host
    345
    346 -    def get_local_proxy_port(self):
    347          """\
    348          Retrieve the local IP socket port this SSH proxying tunnel is (about to) bind/bound to.
    349
    350          @return: local IP socket port
    351          @rtype: C{int}
    352
    353          """
    354          return self.local_port
    355
    356 -    def get_remote_host(self):
    357          """\
    358          Retrieve the remote IP socket address at the remote end of the SSH proxying tunnel.
    359
    360          @return: remote IP socket address
    361          @rtype: C{str}
    362
    363          """
    364          return self.remote_host
    365
    366 -    def get_remote_port(self):
    367          """\
    368          Retrieve the remote IP socket port of the target system's SSH daemon.
    369
    370          @return: remote SSH port
    371          @rtype: C{int}
    372
    373          """
    374          return self.remote_port
    375
    376 -    def stop_thread(self):
    377          """\
    378          Tear down the SSH proxying tunnel.
    379
    380          """
    381          if self.fw_tunnel is not None and self.fw_tunnel.is_active:
    382              self.logger('taking down SSH proxy tunnel via [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
    383          try: forward.stop_forward_tunnel(self.fw_tunnel)
    384          except: pass
    385          self.fw_tunnel = None
    386          self._keepalive = False
    387          if self.get_transport() is not None:
    388              self.logger('closing SSH proxy connection to [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
    389          self.close()
    390          self.password = self.sshproxy_password = None
    391
    392 -    def __del__(self):
    393          """\
    394          Class desctructor.
    395
    396          """
    397          self.stop_thread()
    398