Package x2go :: Package backends :: Package control :: Module plain

[frames] | no frames]

# Source Code for Module x2go.backends.control.plain

       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{X2GoControlSession} class - core functions for handling your individual X2Go sessions.
      22
      23  This backend handles X2Go server implementations that respond via server-side PLAIN text output.
      24
      25  """
      26  __NAME__ = 'x2gocontrolsession-pylib'
      27
      28  # modules
      29  import os
      30  import types
      31  import paramiko
      32  import gevent
      33  import copy
      34  import string
      35  import random
      36  import re
      37  import locale
      38  import threading
      39  import cStringIO
      40  import base64
      41  import uuid
      42
      43  from gevent import socket
      44
      45  # Python X2Go modules
      46  import x2go.sshproxy as sshproxy
      47  import x2go.log as log
      48  import x2go.utils as utils
      49  import x2go.x2go_exceptions as x2go_exceptions
      50  import x2go.defaults as defaults
      51  import x2go.checkhosts as checkhosts
      52
      53  from x2go.defaults import BACKENDS as _BACKENDS
      54
      55  import x2go._paramiko
      56  x2go._paramiko.monkey_patch_paramiko()
      57
      58 -def _rerewrite_blanks(cmd):
      59      """\
      60      In command strings X2Go server scripts expect blanks being rewritten to ,,X2GO_SPACE_CHAR''.
      61      Commands get rewritten in the terminal sessions. This re-rewrite function helps
      62      displaying command string in log output.
      63
      64      @param cmd: command that has to be rewritten for log output
      65      @type cmd: C{str}
      66
      67      @return: the command with ,,X2GO_SPACE_CHAR'' re-replaced by blanks
      68      @rtype: C{str}
      69
      70      """
      71      # X2Go run command replace X2GO_SPACE_CHAR string with blanks
      72      if cmd:
      73          cmd = cmd.replace("X2GO_SPACE_CHAR", " ")
      74      return cmd
      75
      76 -def _rewrite_password(cmd, user=None, password=None):
      77      """\
      78      In command strings Python X2Go replaces some macros with actual values:
      79
      80        - X2GO_USER -> the user name under which the user is authenticated via SSH
      81        - X2GO_PASSWORD -> the password being used for SSH authentication
      82
      83      Both macros can be used to on-the-fly authenticate via RDP.
      84
      85      @param cmd: command that is to be sent to an X2Go server script
      86      @type cmd: C{str}
      87      @param user: the SSH authenticated user name
      88      @type password: the password being used for SSH authentication
      89
      90      @return: the command with macros replaced
      91      @rtype: C{str}
      92
      93      """
      94      # if there is a ,,-u X2GO_USER'' parameter in RDP options then we will replace
      95      # it by our X2Go session password
      96      if cmd and user:
      97          cmd = cmd.replace('X2GO_USER', user)
      98      # if there is a ,,-p X2GO_PASSWORD'' parameter in RDP options then we will replace
      99      # it by our X2Go session password
     100      if cmd and password:
     101          cmd = cmd.replace('X2GO_PASSWORD', password)
     102      return cmd
     103
     104
     105 -class X2GoControlSession(paramiko.SSHClient):
     106      """\
     107      In the Python X2Go concept, X2Go sessions fall into two parts: a control session and one to many terminal sessions.
     108
     109      The control session handles the SSH based communication between server and client. It is mainly derived from
     110      C{paramiko.SSHClient} and adds on X2Go related functionality.
     111
     112      """
     113 -    def __init__(self,
     114                   profile_name='UNKNOWN',
     115                   add_to_known_hosts=False,
     116                   known_hosts=None,
     117                   forward_sshagent=False,
     118                   unique_hostkey_aliases=False,
     119                   terminal_backend=_BACKENDS['X2GoTerminalSession']['default'],
     120                   info_backend=_BACKENDS['X2GoServerSessionInfo']['default'],
     121                   list_backend=_BACKENDS['X2GoServerSessionList']['default'],
     122                   proxy_backend=_BACKENDS['X2GoProxy']['default'],
     123                   client_rootdir=os.path.join(defaults.LOCAL_HOME, defaults.X2GO_CLIENT_ROOTDIR),
     124                   sessions_rootdir=os.path.join(defaults.LOCAL_HOME, defaults.X2GO_SESSIONS_ROOTDIR),
     125                   ssh_rootdir=os.path.join(defaults.LOCAL_HOME, defaults.X2GO_SSH_ROOTDIR),
     126                   logger=None, loglevel=log.loglevel_DEFAULT,
     127                   published_applications_no_submenus=0,
     128                   low_latency=False,
     129                   **kwargs):
     130          """\
     131          Initialize an X2Go control session. For each connected session profile there will be one SSH-based
     132          control session and one to many terminal sessions that all server-client-communicate via this one common control
     133          session.
     134
     135          A control session normally gets set up by an L{X2GoSession} instance. Do not use it directly!!!
     136
     137          @param profile_name: the profile name of the session profile this control session works for
     138          @type profile_name: C{str}
     139          @param add_to_known_hosts: Auto-accept server host validity?
     140          @type add_to_known_hosts: C{bool}
     141          @param known_hosts: the underlying Paramiko/SSH systems C{known_hosts} file
     142          @type known_hosts: C{str}
     143          @param forward_sshagent: forward SSH agent authentication requests to the X2Go client-side
     144          @type forward_sshagent: C{bool}
     145          @param unique_hostkey_aliases: instead of storing [<hostname>]:<port> in known_hosts file, use the
     146              (unique-by-design) profile ID
     147          @type unique_hostkey_aliases: C{bool}
     148          @param terminal_backend: X2Go terminal session backend to use
     149          @type terminal_backend: C{str}
     150          @param info_backend: backend for handling storage of server session information
     151          @type info_backend: C{X2GoServerSessionInfo*} instance
     152          @param list_backend: backend for handling storage of session list information
     153          @type list_backend: C{X2GoServerSessionList*} instance
     154          @param proxy_backend: backend for handling the X-proxy connections
     155          @type proxy_backend: C{X2GoProxy*} instance
     156          @param client_rootdir: client base dir (default: ~/.x2goclient)
     157          @type client_rootdir: C{str}
     158          @param sessions_rootdir: sessions base dir (default: ~/.x2go)
     159          @type sessions_rootdir: C{str}
     160          @param ssh_rootdir: ssh base dir (default: ~/.ssh)
     161          @type ssh_rootdir: C{str}
     162          @param published_applications_no_submenus: published applications menus with less items than C{published_applications_no_submenus}
     163              are rendered without submenus
     164          @type published_applications_no_submenus: C{int}
     165          @param logger: you can pass an L{X2GoLogger} object to the
     166              L{X2GoControlSession} constructor
     167          @type logger: L{X2GoLogger} instance
     168          @param loglevel: if no L{X2GoLogger} object has been supplied a new one will be
     169              constructed with the given loglevel
     170          @type loglevel: C{int}
     171          @param low_latency: set this boolean switch for weak connections, it will double all timeout values.
     172          @type low_latency: C{bool}
     173          @param kwargs: catch any non-defined parameters in C{kwargs}
     174          @type kwargs: C{dict}
     175
     176          """
     177          self.associated_terminals = {}
     178          self.terminated_terminals = []
     179
     180          self.profile_name = profile_name
     181          self.add_to_known_hosts = add_to_known_hosts
     182          self.known_hosts = known_hosts
     183          self.forward_sshagent = forward_sshagent
     184          self.unique_hostkey_aliases = unique_hostkey_aliases
     185
     186          self.hostname = None
     187          self.port = None
     188
     189          self.sshproxy_session = None
     190
     191          self._session_auth_rsakey = None
     192          self._remote_home = None
     193          self._remote_group = {}
     194          self._remote_username = None
     195          self._remote_peername = None
     196
     197          self._server_versions = None
     198          self._server_features = None
     199
     200          if logger is None:
     201              self.logger = log.X2GoLogger(loglevel=loglevel)
     202          else:
     203              self.logger = copy.deepcopy(logger)
     204          self.logger.tag = __NAME__
     205
     206          self._terminal_backend = terminal_backend
     207          self._info_backend = info_backend
     208          self._list_backend = list_backend
     209          self._proxy_backend = proxy_backend
     210
     211          self.client_rootdir = client_rootdir
     212          self.sessions_rootdir = sessions_rootdir
     213          self.ssh_rootdir = ssh_rootdir
     214
     215          self._published_applications_menu = {}
     216
     217          self.agent_chan = None
     218          self.agent_handler = None
     219
     220          paramiko.SSHClient.__init__(self)
     221          if self.add_to_known_hosts:
     222              self.set_missing_host_key_policy(paramiko.AutoAddPolicy())
     223
     224          self.session_died = False
     225
     226          self.low_latency = low_latency
     227
     228          self.published_applications_no_submenus = published_applications_no_submenus
     229          self._already_querying_published_applications = threading.Lock()
     230
     231          self._transport_lock = threading.Lock()
     232
     233 -    def get_hostname(self):
     234          """\
     235          Get the hostname as stored in the properties of this control session.
     236
     237          @return: the hostname of the connected X2Go server
     238          @rtype: C{str}
     239
     240          """
     241          return self.hostname
     242
     243 -    def get_port(self):
     244          """\
     245          Get the port number of the SSH connection as stored in the properties of this control session.
     246
     247          @return: the server-side port number of the control session's SSH connection
     248          @rtype: C{str}
     249
     250          """
     251          return self.port
     252
     253 -    def load_session_host_keys(self):
     254          """\
     255          Load known SSH host keys from the C{known_hosts} file.
     256
     257          If the file does not exist, create it first.
     258
     259          """
     260          if self.known_hosts is not None:
     261              utils.touch_file(self.known_hosts)
     262              self.load_host_keys(self.known_hosts)
     263
     264 -    def __del__(self):
     265          """\
     266          On instance descruction, do a proper session disconnect from the server.
     267
     268          """
     269          self.disconnect()
     270
     271 -    def test_sftpclient(self):
     272          ssh_transport = self.get_transport()
     273          try:
     274              self.sftp_client = paramiko.SFTPClient.from_transport(ssh_transport)
     275          except (AttributeError, paramiko.SFTPError):
     276              raise x2go_exceptions.X2GoSFTPClientException('failed to initialize SFTP channel')
     277
     278 -    def _x2go_sftp_put(self, local_path, remote_path, timeout=20):
     279          """
     280          Put a local file on the remote server via sFTP.
     281
     282          During sFTP operations, remote command execution gets blocked.
     283
     284          @param local_path: full local path name of the file to be put on the server
     285          @type local_path: C{str}
     286          @param remote_path: full remote path name of the server-side target location, path names have to be Unix-compliant
     287          @type remote_path: C{str}
     288          @param timeout: this SFTP put action should not take longer then the given value
     289          @type timeout: C{int}
     290
     291          @raise X2GoControlSessionException: if the SSH connection dropped out
     292
     293          """
     294          ssh_transport = self.get_transport()
     295          self._transport_lock.acquire()
     296          if ssh_transport and ssh_transport.is_authenticated():
     297              self.logger('sFTP-put: %s -> %s:%s' % (os.path.normpath(local_path), self.remote_peername(), remote_path), loglevel=log.loglevel_DEBUG)
     298
     299              if self.low_latency: timeout = timeout * 2
     300              timer = gevent.Timeout(timeout)
     301              timer.start()
     302
     303              try:
     304                  try:
     305                      self.sftp_client = paramiko.SFTPClient.from_transport(ssh_transport)
     306                  except paramiko.SFTPError:
     307                      self._transport_lock.release()
     308                      raise x2go_exceptions.X2GoSFTPClientException('failed to initialize SFTP channel')
     309                  try:
     310                      self.sftp_client.put(os.path.normpath(local_path), remote_path)
     311                  except (x2go_exceptions.SSHException, socket.error, IOError):
     312                      # react to connection dropped error for SSH connections
     313                      self.session_died = True
     314                      self._transport_lock.release()
     315                      raise x2go_exceptions.X2GoControlSessionException('The SSH connection was dropped during an sFTP put action.')
     316
     317              except gevent.timeout.Timeout:
     318                  self.session_died = True
     319                  self._transport_lock.release()
     320                  if self.sshproxy_session:
     321                      self.sshproxy_session.stop_thread()
     322                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session timed out during an SFTP write command')
     323              finally:
     324                  timer.cancel()
     325
     326              self.sftp_client = None
     327          self._transport_lock.release()
     328
     329 -    def _x2go_sftp_write(self, remote_path, content, timeout=20):
     330          """
     331          Create a text file on the remote server via sFTP.
     332
     333          During sFTP operations, remote command execution gets blocked.
     334
     335          @param remote_path: full remote path name of the server-side target location, path names have to be Unix-compliant
     336          @type remote_path: C{str}
     337          @param content: a text file, multi-line files use Unix-link EOL style
     338          @type content: C{str}
     339          @param timeout: this SFTP write action should not take longer then the given value
     340          @type timeout: C{int}
     341
     342          @raise X2GoControlSessionException: if the SSH connection dropped out
     343
     344          """
     345          ssh_transport = self.get_transport()
     346          self._transport_lock.acquire()
     347          if ssh_transport and ssh_transport.is_authenticated():
     348              self.logger('sFTP-write: opening remote file %s on host %s for writing' % (remote_path, self.remote_peername()), loglevel=log.loglevel_DEBUG)
     349
     350              if self.low_latency: timeout = timeout * 2
     351              timer = gevent.Timeout(timeout)
     352              timer.start()
     353
     354              try:
     355                  try:
     356                      self.sftp_client = paramiko.SFTPClient.from_transport(ssh_transport)
     357                  except paramiko.SFTPError:
     358                      self._transport_lock.release()
     359                      raise x2go_exceptions.X2GoSFTPClientException('failed to initialize SFTP channel')
     360                  try:
     361                      remote_fileobj = self.sftp_client.open(remote_path, 'w')
     362                      self.logger('sFTP-write: writing content: %s' % content, loglevel=log.loglevel_DEBUG_SFTPXFER)
     363                      remote_fileobj.write(content)
     364                      remote_fileobj.close()
     365                  except (x2go_exceptions.SSHException, socket.error, IOError):
     366                      self.session_died = True
     367                      self._transport_lock.release()
     368                      self.logger('sFTP-write: opening remote file %s on host %s failed' % (remote_path, self.remote_peername()), loglevel=log.loglevel_WARN)
     369                      if self.sshproxy_session:
     370                          self.sshproxy_session.stop_thread()
     371                      raise x2go_exceptions.X2GoControlSessionException('The SSH connection was dropped during an sFTP write action.')
     372
     373              except gevent.timeout.Timeout:
     374                  self.session_died = True
     375                  self._transport_lock.release()
     376                  if self.sshproxy_session:
     377                      self.sshproxy_session.stop_thread()
     378                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session timed out during an SFTP write command')
     379              finally:
     380                  timer.cancel()
     381
     382              self.sftp_client = None
     383          self._transport_lock.release()
     384
     385 -    def _x2go_sftp_remove(self, remote_path, timeout=20):
     386          """
     387          Remote a remote file from the server via sFTP.
     388
     389          During sFTP operations, remote command execution gets blocked.
     390
     391          @param remote_path: full remote path name of the server-side file to be removed, path names have to be Unix-compliant
     392          @type remote_path: C{str}
     393          @param timeout: this SFTP remove action should not take longer then the given value
     394          @type timeout: C{int}
     395
     396          @raise X2GoControlSessionException: if the SSH connection dropped out
     397
     398          """
     399          ssh_transport = self.get_transport()
     400          self._transport_lock.acquire()
     401          if ssh_transport and ssh_transport.is_authenticated():
     402              self.logger('sFTP-write: removing remote file %s on host %s' % (remote_path, self.remote_peername()), loglevel=log.loglevel_DEBUG)
     403
     404              if self.low_latency: timeout = timeout * 2
     405              timer = gevent.Timeout(timeout)
     406              timer.start()
     407
     408              try:
     409                  try:
     410                      self.sftp_client = paramiko.SFTPClient.from_transport(ssh_transport)
     411                  except paramiko.SFTPError:
     412                      self._transport_lock.release()
     413                      raise x2go_exceptions.X2GoSFTPClientException('failed to initialize SFTP channel')
     414                  try:
     415                      self.sftp_client.remove(remote_path)
     416                  except (x2go_exceptions.SSHException, socket.error, IOError):
     417                      self.session_died = True
     418                      self._transport_lock.release()
     419                      self.logger('sFTP-write: removing remote file %s on host %s failed' % (remote_path, self.remote_peername()), loglevel=log.loglevel_WARN)
     420                      if self.sshproxy_session:
     421                          self.sshproxy_session.stop_thread()
     422                      raise x2go_exceptions.X2GoControlSessionException('The SSH connection was dropped during an sFTP remove action.')
     423
     424              except gevent.timeout.Timeout:
     425                  self.session_died = True
     426                  self._transport_lock.release()
     427                  if self.sshproxy_session:
     428                      self.sshproxy_session.stop_thread()
     429                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session timed out during an SFTP write command')
     430              finally:
     431                  timer.cancel()
     432
     433              self.sftp_client = None
     434          self._transport_lock.release()
     435
     436 -    def _x2go_exec_command(self, cmd_line, loglevel=log.loglevel_INFO, timeout=20, **kwargs):
     437          """
     438          Execute an X2Go server-side command via SSH.
     439
     440          During SSH command executions, sFTP operations get blocked.
     441
     442          @param cmd_line: the command to be executed on the remote server
     443          @type cmd_line: C{str} or C{list}
     444          @param loglevel: use this loglevel for reporting about remote command execution
     445          @type loglevel: C{int}
     446          @param timeout: if commands take longer than C{<timeout>} to be executed, consider the control session connection
     447              to have died.
     448          @type timeout: C{int}
     449          @param kwargs: parameters that get passed through to the C{paramiko.SSHClient.exec_command()} method.
     450          @type kwargs: C{dict}
     451
     452          @return: C{True} if the command could be successfully executed on the remote X2Go server
     453          @rtype: C{bool}
     454
     455          @raise X2GoControlSessionException: if the command execution failed (due to a lost connection)
     456
     457          """
     458          if type(cmd_line) == types.ListType:
     459              cmd = " ".join(cmd_line)
     460          else:
     461              cmd = cmd_line
     462
     463          cmd_uuid = str(uuid.uuid1())
     464          cmd = 'echo X2GODATABEGIN:%s; PATH=/usr/local/bin:/usr/bin:/bin sh -c \"%s\"; echo X2GODATAEND:%s' % (cmd_uuid, cmd, cmd_uuid)
     465
     466          if self.session_died:
     467              self.logger("control session seams to be dead, not executing command ,,%s'' on X2Go server %s" % (_rerewrite_blanks(cmd), self.profile_name,), loglevel=loglevel)
     468              return (cStringIO.StringIO(), cStringIO.StringIO(), cStringIO.StringIO('failed to execute command'))
     469
     470          self._transport_lock.acquire()
     471
     472          _retval = None
     473          _password = None
     474
     475          ssh_transport = self.get_transport()
     476          if ssh_transport and ssh_transport.is_authenticated():
     477
     478              if self.low_latency: timeout = timeout * 2
     479              timer = gevent.Timeout(timeout)
     480              timer.start()
     481              try:
     482                  self.logger("executing command on X2Go server ,,%s'': %s" % (self.profile_name, _rerewrite_blanks(cmd)), loglevel=loglevel)
     483                  if self._session_password:
     484                      _password = base64.b64decode(self._session_password)
     485                  _retval = self.exec_command(_rewrite_password(cmd, user=self.get_transport().get_username(), password=_password), **kwargs)
     486              except AttributeError:
     487                  self.session_died = True
     488                  self._transport_lock.release()
     489                  if self.sshproxy_session:
     490                      self.sshproxy_session.stop_thread()
     491                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session has died unexpectedly')
     492              except EOFError:
     493                  self.session_died = True
     494                  self._transport_lock.release()
     495                  if self.sshproxy_session:
     496                      self.sshproxy_session.stop_thread()
     497                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session has died unexpectedly')
     498              except x2go_exceptions.SSHException:
     499                  self.session_died = True
     500                  self._transport_lock.release()
     501                  if self.sshproxy_session:
     502                      self.sshproxy_session.stop_thread()
     503                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session has died unexpectedly')
     504              except gevent.timeout.Timeout:
     505                  self.session_died = True
     506                  self._transport_lock.release()
     507                  if self.sshproxy_session:
     508                      self.sshproxy_session.stop_thread()
     509                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session command timed out')
     510              except socket.error:
     511                  self.session_died = True
     512                  self._transport_lock.release()
     513                  if self.sshproxy_session:
     514                      self.sshproxy_session.stop_thread()
     515                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session has died unexpectedly')
     516              finally:
     517                  timer.cancel()
     518
     519          else:
     520              self._transport_lock.release()
     521              raise x2go_exceptions.X2GoControlSessionException('the X2Go control session is not connected (while issuing SSH command=%s)' % cmd)
     522
     523          self._transport_lock.release()
     524
     525          # sanitized X2Go relevant data, protect against data injection via .bashrc files
     526          (_stdin, _stdout, _stderr) = _retval
     527          raw_stdout = _stdout.read()
     528
     529          sanitized_stdout = ''
     530          is_x2go_data = False
     531          for line in raw_stdout.split('\n'):
     532              if line.startswith('X2GODATABEGIN:'+cmd_uuid):
     533                  is_x2go_data = True
     534                  continue
     535              if not is_x2go_data: continue
     536              if line.startswith('X2GODATAEND:'+cmd_uuid): break
     537              sanitized_stdout += line + "\n"
     538
     539          _stdout_new = cStringIO.StringIO(sanitized_stdout)
     540
     541          _retval = (_stdin, _stdout_new, _stderr)
     542          return _retval
     543
     544      @property
     545 -    def _x2go_server_versions(self):
     546          """\
     547          Render a dictionary of server-side X2Go components and their versions. Results get cached
     548          once there has been one successful query.
     549
     550          """
     551          if self._server_versions is None:
     552              self._server_versions = {}
     553              (stdin, stdout, stderr) = self._x2go_exec_command('which x2goversion >/dev/null && x2goversion')
     554              _lines = stdout.read().split('\n')
     555              for _line in _lines:
     556                  if ':' not in _line: continue
     557                  comp = _line.split(':')[0].strip()
     558                  version = _line.split(':')[1].strip()
     559                  self._server_versions.update({comp: version})
     560              self.logger('server-side X2Go components and their versions are: %s' % self._server_versions, loglevel=log.loglevel_DEBUG)
     561          return self._server_versions
     562
     563 -    def query_server_versions(self, force=False):
     564          """\
     565          Do a query for the server-side list of X2Go components and their versions.
     566
     567          @param force: do not use the cached component list, really ask the server (again)
     568          @type force: C{bool}
     569
     570          @return: dictionary of X2Go components (as keys) and their versions (as values)
     571          @rtype: C{list}
     572
     573          """
     574          if force:
     575              self._server_versions = None
     576          return self._x2go_server_versions
     577      get_server_versions = query_server_versions
     578
     579      @property
     580 -    def _x2go_server_features(self):
     581          """\
     582          Render a list of server-side X2Go features. Results get cached once there has been one successful query.
     583
     584          """
     585          if self._server_features is None:
     586              (stdin, stdout, stderr) = self._x2go_exec_command('which x2gofeaturelist >/dev/null && x2gofeaturelist')
     587              self._server_features = stdout.read().split('\n')
     588              self._server_features = [ f for f in self._server_features if f ]
     589              self._server_features.sort()
     590              self.logger('server-side X2Go features are: %s' % self._server_features, loglevel=log.loglevel_DEBUG)
     591          return self._server_features
     592
     593 -    def query_server_features(self, force=False):
     594          """\
     595          Do a query for the server-side list of X2Go features.
     596
     597          @param force: do not use the cached feature list, really ask the server (again)
     598          @type force: C{bool}
     599
     600          @return: list of X2Go feature names
     601          @rtype: C{list}
     602
     603          """
     604          if force:
     605              self._server_features = None
     606          return self._x2go_server_features
     607      get_server_features = query_server_features
     608
     609      @property
     610 -    def _x2go_remote_home(self):
     611          """\
     612          Retrieve and cache the remote home directory location.
     613
     614          """
     615          if self._remote_home is None:
     616              (stdin, stdout, stderr) = self._x2go_exec_command('echo $HOME')
     617              stdout_r = stdout.read()
     618              if stdout_r:
     619                  self._remote_home = stdout_r.split()[0]
     620                  self.logger('remote user\' home directory: %s' % self._remote_home, loglevel=log.loglevel_DEBUG)
     621              return self._remote_home
     622          else:
     623              return self._remote_home
     624
     625 -    def _x2go_remote_group(self, group):
     626          """\
     627          Retrieve and cache the members of a server-side POSIX group.
     628
     629          @param group: remote POSIX group name
     630          @type group: C{str}
     631
     632          @return: list of POSIX group members
     633          @rtype: C{list}
     634
     635          """
     636          if not self._remote_group.has_key(group):
     637              (stdin, stdout, stderr) = self._x2go_exec_command('getent group %s | cut -d":" -f4' % group)
     638              self._remote_group[group] = stdout.read().split('\n')[0].split(',')
     639              self.logger('remote %s group: %s' % (group, self._remote_group[group]), loglevel=log.loglevel_DEBUG)
     640              return self._remote_group[group]
     641          else:
     642              return self._remote_group[group]
     643
     644 -    def is_x2gouser(self, username):
     645          """\
     646          Is the remote user allowed to launch X2Go sessions?
     647
     648          FIXME: this method is currently non-functional.
     649
     650          @param username: remote user name
     651          @type username: C{str}
     652
     653          @return: C{True} if the remote user is allowed to launch X2Go sessions
     654          @rtype: C{bool}
     655
     656          """
     657          ###
     658          ### FIXME:
     659          ###
     660          # discussion about server-side access restriction based on posix group membership or similar currently
     661          # in process (as of 20110517, mg)
     662          #return username in self._x2go_remote_group('x2gousers')
     663          return True
     664
     665 -    def is_sshfs_available(self):
     666          """\
     667          Check if the remote user is allowed to use SSHFS mounts.
     668
     669          @return: C{True} if the user is allowed to connect client-side shares to the X2Go session
     670          @rtype: C{bool}
     671
     672          """
     673          (stdin, stdout, stderr) = self._x2go_exec_command('which fusermount')
     674
     675          # if which returns the full path of fusermount, the current use is allowed to execute it
     676          return bool(stdout.read())
     677
     678 -    def remote_username(self):
     679          """\
     680          Returns (and caches) the control session's remote username.
     681
     682          @return: SSH transport's user name
     683          @rtype: C{str}
     684
     685          @raise X2GoControlSessionException: on SSH connection loss
     686
     687          """
     688          if self._remote_username is None:
     689              if self.get_transport() is not None:
     690                  try:
     691                      self._remote_username = self.get_transport().get_username()
     692                  except:
     693                      self.session_died = True
     694                      raise x2go_exceptions.X2GoControlSessionException('Lost connection to X2Go server')
     695          return self._remote_username
     696
     697 -    def remote_peername(self):
     698          """\
     699          Returns (and caches) the control session's remote host (name or ip).
     700
     701          @return: SSH transport's peer name
     702          @rtype: C{tuple}
     703
     704          @raise X2GoControlSessionException: on SSH connection loss
     705
     706          """
     707          if self._remote_peername is None:
     708              if self.get_transport() is not None:
     709                  try:
     710                      self._remote_peername = self.get_transport().getpeername()
     711                  except:
     712                      self.session_died = True
     713                      raise x2go_exceptions.X2GoControlSessionException('Lost connection to X2Go server')
     714          return self._remote_peername
     715
     716      @property
     717 -    def _x2go_session_auth_rsakey(self):
     718          """\
     719          Generate (and cache) a temporary RSA host key for the lifetime of this control session.
     720
     721          """
     722          if self._session_auth_rsakey is None:
     723              self._session_auth_rsakey = paramiko.RSAKey.generate(defaults.RSAKEY_STRENGTH)
     724          return self._session_auth_rsakey
     725
     726 -    def set_profile_name(self, profile_name):
     727          """\
     728          Manipulate the control session's profile name.
     729
     730          @param profile_name: new profile name for this control session
     731          @type profile_name: C{str}
     732
     733          """
     734          self.profile_name = profile_name
     735
     736 -    def check_host(self, hostname, port=22):
     737          """\
     738          Wraps around a Paramiko/SSH host key check.
     739
     740          @param hostname: the remote X2Go server's hostname
     741          @type hostname: C{str}
     742          @param port: the SSH port of the remote X2Go server
     743          @type port: C{int}
     744
     745          @return: C{True} if the host key check succeeded, C{False} otherwise
     746          @rtype: C{bool}
     747
     748          """
     749          # trailing whitespace tolerance
     750          hostname = hostname.strip()
     751
     752          # force into IPv4 for localhost connections
     753          if hostname in ('localhost', 'localhost.localdomain'):
     754              hostname = '127.0.0.1'
     755
     756          return checkhosts.check_ssh_host_key(self, hostname, port=port)
     757
     758 -    def connect(self, hostname, port=22, username=None, password=None, passphrase=None, pkey=None,
     759                  key_filename=None, timeout=None, allow_agent=False, look_for_keys=False,
     760                  use_sshproxy=False, sshproxy_host=None, sshproxy_port=22, sshproxy_user=None, sshproxy_password=None, sshproxy_force_password_auth=False,
     761                  sshproxy_key_filename=None, sshproxy_pkey=None, sshproxy_look_for_keys=False, sshproxy_passphrase='', sshproxy_allow_agent=False,
     762                  sshproxy_tunnel=None,
     763                  add_to_known_hosts=None,
     764                  forward_sshagent=None,
     765                  unique_hostkey_aliases=None,
     766                  force_password_auth=False,
     767                  session_instance=None,
     768          ):
     769          """\
     770          Connect to an X2Go server and authenticate to it. This method is directly
     771          inherited from the C{paramiko.SSHClient} class. The features of the Paramiko
     772          SSH client connect method are recited here. The parameters C{add_to_known_hosts},
     773          C{force_password_auth}, C{session_instance} and all SSH proxy related parameters
     774          have been added as X2Go specific parameters
     775
     776          The server's host key is checked against the system host keys
     777          (see C{load_system_host_keys}) and any local host keys (C{load_host_keys}).
     778          If the server's hostname is not found in either set of host keys, the missing host
     779          key policy is used (see C{set_missing_host_key_policy}).  The default policy is
     780          to reject the key and raise an C{SSHException}.
     781
     782          Authentication is attempted in the following order of priority:
     783
     784              - The C{pkey} or C{key_filename} passed in (if any)
     785              - Any key we can find through an SSH agent
     786              - Any "id_rsa" or "id_dsa" key discoverable in C{~/.ssh/}
     787              - Plain username/password auth, if a password was given
     788
     789          If a private key requires a password to unlock it, and a password is
     790          passed in, that password will be used to attempt to unlock the key.
     791
     792          @param hostname: the server to connect to
     793          @type hostname: C{str}
     794          @param port: the server port to connect to
     795          @type port: C{int}
     796          @param username: the username to authenticate as (defaults to the
     797              current local username)
     798          @type username: C{str}
     799          @param password: a password to use for authentication or for unlocking
     800              a private key
     801          @type password: C{str}
     802          @param passphrase: a passphrase to use for unlocking
     803              a private key in case the password is already needed for two-factor
     804              authentication
     805          @type passphrase: C{str}
     806          @param key_filename: the filename, or list of filenames, of optional
     807              private key(s) to try for authentication
     808          @type key_filename: C{str} or list(str)
     809          @param pkey: an optional private key to use for authentication
     810          @type pkey: C{PKey}
     811          @param forward_sshagent: forward SSH agent authentication requests to the X2Go client-side
     812              (will update the class property of the same name)
     813          @type forward_sshagent: C{bool}
     814          @param unique_hostkey_aliases: update the unique_hostkey_aliases class property
     815          @type unique_hostkey_aliases: C{bool}
     816          @param timeout: an optional timeout (in seconds) for the TCP connect
     817          @type timeout: float
     818          @param look_for_keys: set to C{True} to enable searching for discoverable
     819              private key files in C{~/.ssh/}
     820          @type look_for_keys: C{bool}
     821          @param allow_agent: set to C{True} to enable connecting to a local SSH agent
     822              for acquiring authentication information
     823          @type allow_agent: C{bool}
     824          @param add_to_known_hosts: non-paramiko option, if C{True} paramiko.AutoAddPolicy()
     825              is used as missing-host-key-policy. If set to C{False} paramiko.RejectPolicy()
     826              is used
     827          @type add_to_known_hosts: C{bool}
     828          @param force_password_auth: non-paramiko option, disable pub/priv key authentication
     829              completely, even if the C{pkey} or the C{key_filename} parameter is given
     830          @type force_password_auth: C{bool}
     831          @param session_instance: an instance L{X2GoSession} using this L{X2GoControlSession}
     832              instance.
     833          @type session_instance: C{obj}
     834          @param use_sshproxy: connect through an SSH proxy
     835          @type use_sshproxy: C{True} if an SSH proxy is to be used for tunneling the connection
     836          @param sshproxy_host: hostname of the SSH proxy server
     837          @type sshproxy_host: C{str}
     838          @param sshproxy_port: port of the SSH proxy server
     839          @type sshproxy_port: C{int}
     840          @param sshproxy_user: username that we use for authenticating against C{<sshproxy_host>}
     841          @type sshproxy_user: C{str}
     842          @param sshproxy_password: a password to use for SSH proxy authentication or for unlocking
     843              a private key
     844          @type sshproxy_password: C{str}
     845          @param sshproxy_passphrase: a passphrase to use for unlocking
     846              a private key needed for the SSH proxy host in case the sshproxy_password is already needed for
     847              two-factor authentication
     848          @type sshproxy_passphrase: C{str}
     849          @param sshproxy_force_password_auth: enforce using a given C{sshproxy_password} even if a key(file) is given
     850          @type sshproxy_force_password_auth: C{bool}
     851          @param sshproxy_key_filename: local file location of the private key file
     852          @type sshproxy_key_filename: C{str}
     853          @param sshproxy_pkey: an optional private key to use for SSH proxy authentication
     854          @type sshproxy_pkey: C{PKey}
     855          @param sshproxy_look_for_keys: set to C{True} to enable connecting to a local SSH agent
     856              for acquiring authentication information (for SSH proxy authentication)
     857          @type sshproxy_look_for_keys: C{bool}
     858          @param sshproxy_allow_agent: set to C{True} to enable connecting to a local SSH agent
     859              for acquiring authentication information (for SSH proxy authentication)
     860          @type sshproxy_allow_agent: C{bool}
     861          @param sshproxy_tunnel: the SSH proxy tunneling parameters, format is: <local-address>:<local-port>:<remote-address>:<remote-port>
     862          @type sshproxy_tunnel: C{str}
     863
     864          @return: C{True} if an authenticated SSH transport could be retrieved by this method
     865          @rtype: C{bool}
     866
     867          @raise BadHostKeyException: if the server's host key could not be
     868              verified
     869          @raise AuthenticationException: if authentication failed
     870          @raise SSHException: if there was any other error connecting or
     871              establishing an SSH session
     872          @raise socket.error: if a socket error occurred while connecting
     873          @raise X2GoSSHProxyException: any SSH proxy exception is passed through while establishing the SSH proxy connection and tunneling setup
     874          @raise X2GoSSHAuthenticationException: any SSH proxy authentication exception is passed through while establishing the SSH proxy connection and tunneling setup
     875          @raise X2GoRemoteHomeException: if the remote home directory does not exist or is not accessible
     876          @raise X2GoControlSessionException: if the remote peer has died event unexpectedly
     877
     878          """
     879          _fake_hostname = None
     880
     881          if hostname and type(hostname) not in (types.UnicodeType, types.StringType):
     882              hostname = [hostname]
     883          if hostname and type(hostname) is types.ListType:
     884              hostname = random.choice(hostname)
     885
     886          if not username:
     887              self.logger('no username specified, cannot connect without username', loglevel=log.loglevel_ERROR)
     888              raise paramiko.AuthenticationException('no username specified, cannot connect without username')
     889
     890          if type(password) not in (types.StringType, types.UnicodeType):
     891              password = ''
     892          if type(sshproxy_password) not in (types.StringType, types.UnicodeType):
     893              sshproxy_password = ''
     894
     895          if unique_hostkey_aliases is None:
     896              unique_hostkey_aliases = self.unique_hostkey_aliases
     897          # prep the fake hostname with the real hostname, so we trigger the corresponding code path in
     898          # x2go.checkhosts and either of its missing host key policies
     899          if unique_hostkey_aliases:
     900              if port != 22: _fake_hostname = "[%s]:%s" % (hostname, port)
     901              else: _fake_hostname = hostname
     902
     903          if add_to_known_hosts is None:
     904              add_to_known_hosts = self.add_to_known_hosts
     905
     906          if forward_sshagent is None:
     907              forward_sshagent = self.forward_sshagent
     908
     909          if look_for_keys:
     910              key_filename = None
     911              pkey = None
     912
     913          _twofactorauth = False
     914          if password and (passphrase is None) and not force_password_auth: passphrase = password
     915
     916          if use_sshproxy and sshproxy_host and sshproxy_user:
     917              try:
     918                  if not sshproxy_tunnel:
     919                      sshproxy_tunnel = "localhost:44444:%s:%s" % (hostname, port)
     920                  self.sshproxy_session = sshproxy.X2GoSSHProxy(known_hosts=self.known_hosts,
     921                                                                add_to_known_hosts=add_to_known_hosts,
     922                                                                sshproxy_host=sshproxy_host,
     923                                                                sshproxy_port=sshproxy_port,
     924                                                                sshproxy_user=sshproxy_user,
     925                                                                sshproxy_password=sshproxy_password,
     926                                                                sshproxy_passphrase=sshproxy_passphrase,
     927                                                                sshproxy_force_password_auth=sshproxy_force_password_auth,
     928                                                                sshproxy_key_filename=sshproxy_key_filename,
     929                                                                sshproxy_pkey=sshproxy_pkey,
     930                                                                sshproxy_look_for_keys=sshproxy_look_for_keys,
     931                                                                sshproxy_allow_agent=sshproxy_allow_agent,
     932                                                                sshproxy_tunnel=sshproxy_tunnel,
     933                                                                session_instance=session_instance,
     934                                                                logger=self.logger,
     935                                                               )
     936                  hostname = self.sshproxy_session.get_local_proxy_host()
     937                  port = self.sshproxy_session.get_local_proxy_port()
     938                  _fake_hostname = self.sshproxy_session.get_remote_host()
     939                  _fake_port = self.sshproxy_session.get_remote_port()
     940                  if _fake_port != 22:
     941                      _fake_hostname = "[%s]:%s" % (_fake_hostname, _fake_port)
     942
     943              except:
     944                  if self.sshproxy_session:
     945                      self.sshproxy_session.stop_thread()
     946                  self.sshproxy_session = None
     947                  raise
     948
     949              if self.sshproxy_session is not None:
     950                  self.sshproxy_session.start()
     951
     952                  # divert port to sshproxy_session's local forwarding port (it might have changed due to
     953                  # SSH connection errors
     954                  gevent.sleep(.1)
     955                  port = self.sshproxy_session.get_local_proxy_port()
     956
     957          if not add_to_known_hosts and session_instance:
     958              self.set_missing_host_key_policy(checkhosts.X2GoInteractiveAddPolicy(caller=self, session_instance=session_instance, fake_hostname=_fake_hostname))
     959
     960          if add_to_known_hosts:
     961              self.set_missing_host_key_policy(checkhosts.X2GoAutoAddPolicy(caller=self, session_instance=session_instance, fake_hostname=_fake_hostname))
     962
     963          # trailing whitespace tolerance in hostname
     964          hostname = hostname.strip()
     965
     966          self.logger('connecting to [%s]:%s' % (hostname, port), loglevel=log.loglevel_NOTICE)
     967
     968          self.load_session_host_keys()
     969
     970          _hostname = hostname
     971          # enforce IPv4 for localhost address
     972          if _hostname in ('localhost', 'localhost.localdomain'):
     973              _hostname = '127.0.0.1'
     974
     975          # update self.forward_sshagent via connect method parameter
     976          if forward_sshagent is not None:
     977              self.forward_sshagent = forward_sshagent
     978
     979          if timeout and self.low_latency:
     980              timeout = timeout * 2
     981
     982          if key_filename and "~" in key_filename:
     983              key_filename = os.path.expanduser(key_filename)
     984
     985          if key_filename or pkey or look_for_keys or allow_agent or (password and force_password_auth):
     986              try:
     987                  if password and force_password_auth:
     988                      self.logger('trying password based SSH authentication with server', loglevel=log.loglevel_DEBUG)
     989                      paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, pkey=None,
     990                                                 key_filename=None, timeout=timeout, allow_agent=False,
     991                                                 look_for_keys=False)
     992                  elif (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey:
     993                      self.logger('trying SSH pub/priv key authentication with server', loglevel=log.loglevel_DEBUG)
     994                      paramiko.SSHClient.connect(self, _hostname, port=port, username=username, pkey=pkey,
     995                                                 key_filename=key_filename, timeout=timeout, allow_agent=False,
     996                                                 look_for_keys=False)
     997                  else:
     998                      self.logger('trying SSH key discovery or agent authentication with server', loglevel=log.loglevel_DEBUG)
     999                      paramiko.SSHClient.connect(self, _hostname, port=port, username=username, pkey=None,
    1000                                                 key_filename=None, timeout=timeout, allow_agent=allow_agent,
    1001                                                 look_for_keys=look_for_keys)
    1002
    1003              except (paramiko.PasswordRequiredException, paramiko.SSHException), e:
    1004                  self.close()
    1005                  if type(e) == paramiko.SSHException and str(e).startswith('Two-factor authentication requires a password'):
    1006                      self.logger('X2Go Server requests two-factor authentication', loglevel=log.loglevel_NOTICE)
    1007                      _twofactorauth = True
    1008                  if passphrase is not None:
    1009                      self.logger('unlock SSH private key file with provided password', loglevel=log.loglevel_INFO)
    1010                      try:
    1011                          if not password: password = None
    1012                          if (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey:
    1013                              self.logger('re-trying SSH pub/priv key authentication with server', loglevel=log.loglevel_DEBUG)
    1014                              try:
    1015                                  paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, passphrase=passphrase, pkey=pkey,
    1016                                                             key_filename=key_filename, timeout=timeout, allow_agent=False,
    1017                                                             look_for_keys=False)
    1018                              except TypeError:
    1019                                  if _twofactorauth and password and passphrase and password != passphrase:
    1020                                      self.logger('your version of Paramiko/SSH does not support authentication workflows which require SSH key decryption in combination with two-factor authentication', loglevel=log.loglevel_WARN)
    1021                                  paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, pkey=pkey,
    1022                                                             key_filename=key_filename, timeout=timeout, allow_agent=False,
    1023                                                             look_for_keys=False)
    1024                          else:
    1025                              self.logger('re-trying SSH key discovery now with passphrase for unlocking the key(s)', loglevel=log.loglevel_DEBUG)
    1026                              try:
    1027                                  paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, passphrase=passphrase, pkey=None,
    1028                                                             key_filename=None, timeout=timeout, allow_agent=allow_agent,
    1029                                                             look_for_keys=look_for_keys)
    1030                              except TypeError:
    1031                                  if _twofactorauth and password and passphrase and password != passphrase:
    1032                                      self.logger('your version of Paramiko/SSH does not support authentication workflows which require SSH key decryption in combination with two-factor authentication', loglevel=log.loglevel_WARN)
    1033                                  paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, pkey=None,
    1034                                                             key_filename=None, timeout=timeout, allow_agent=allow_agent,
    1035                                                             look_for_keys=look_for_keys)
    1036
    1037                      except paramiko.AuthenticationException, auth_e:
    1038                          # the provided password cannot be used to unlock any private SSH key file (i.e. wrong password)
    1039                          raise paramiko.AuthenticationException(str(auth_e))
    1040
    1041                      except paramiko.SSHException, auth_e:
    1042                          if str(auth_e) == 'No authentication methods available':
    1043                              raise paramiko.AuthenticationException('Interactive password authentication required!')
    1044                          else:
    1045                              self.close()
    1046                              if self.sshproxy_session:
    1047                                  self.sshproxy_session.stop_thread()
    1048                              raise auth_e
    1049
    1050                  else:
    1051                      self.close()
    1052                      if self.sshproxy_session:
    1053                          self.sshproxy_session.stop_thread()
    1054                      raise e
    1055
    1056              except paramiko.AuthenticationException, e:
    1057                  self.close()
    1058                  if password:
    1059                      self.logger('next auth mechanism we\'ll try is password authentication', loglevel=log.loglevel_DEBUG)
    1060                      try:
    1061                          paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password,
    1062                                                     key_filename=None, pkey=None, timeout=timeout, allow_agent=False, look_for_keys=False)
    1063                      except:
    1064                          self.close()
    1065                          if self.sshproxy_session:
    1066                              self.sshproxy_session.stop_thread()
    1067                          raise
    1068                  else:
    1069                      self.close()
    1070                      if self.sshproxy_session:
    1071                          self.sshproxy_session.stop_thread()
    1072                      raise e
    1073
    1074              except paramiko.SSHException, e:
    1075                  if str(e) == 'No authentication methods available':
    1076                      raise paramiko.AuthenticationException('Interactive password authentication required!')
    1077                  else:
    1078                      self.close()
    1079                      if self.sshproxy_session:
    1080                          self.sshproxy_session.stop_thread()
    1081                      raise e
    1082
    1083              except:
    1084                  self.close()
    1085                  if self.sshproxy_session:
    1086                      self.sshproxy_session.stop_thread()
    1087                  raise
    1088
    1089          # if there is no private key (and no agent auth), we will use the given password, if any
    1090          else:
    1091              # create a random password if password is empty to trigger host key validity check
    1092              if not password:
    1093                  password = "".join([random.choice(string.letters+string.digits) for x in range(1, 20)])
    1094              self.logger('performing SSH password authentication with server', loglevel=log.loglevel_DEBUG)
    1095              #try:
    1096              paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password,
    1097                                             timeout=timeout, allow_agent=False, look_for_keys=False)
    1098              #except paramiko.AuthenticationException, e:
    1099              #    self.close()
    1100              #    if self.sshproxy_session:
    1101              #        self.sshproxy_session.stop_thread()
    1102              #    raise e
    1103              #except:
    1104              #    self.close()
    1105              #    if self.sshproxy_session:
    1106              #        self.sshproxy_session.stop_thread()
    1107              #    raise
    1108
    1109          self.set_missing_host_key_policy(paramiko.RejectPolicy())
    1110
    1111          self.hostname = hostname
    1112          self.port = port
    1113
    1114          # preparing reverse tunnels
    1115          ssh_transport = self.get_transport()
    1116          ssh_transport.reverse_tunnels = {}
    1117
    1118          # mark Paramiko/SSH transport as X2GoControlSession
    1119          ssh_transport._x2go_session_marker = True
    1120          try:
    1121              self._session_password = base64.b64encode(password)
    1122          except TypeError:
    1123              self._session_password = None
    1124
    1125          if ssh_transport is not None:
    1126
    1127              # since Paramiko 1.7.7.1 there is compression available, let's use it if present...
    1128              if x2go._paramiko.PARAMIKO_FEATURE['use-compression']:
    1129                  ssh_transport.use_compression(compress=False)
    1130              # enable keep alive callbacks
    1131              ssh_transport.set_keepalive(5)
    1132
    1133              self.session_died = False
    1134              self.query_server_features(force=True)
    1135              if self.forward_sshagent:
    1136                  if x2go._paramiko.PARAMIKO_FEATURE['forward-ssh-agent']:
    1137                      try:
    1138                          self.agent_chan = ssh_transport.open_session()
    1139                          self.agent_handler = paramiko.agent.AgentRequestHandler(self.agent_chan)
    1140                          self.logger('Requesting SSH agent forwarding for control session of connected session profile %s' % self.profile_name, loglevel=log.loglevel_INFO)
    1141                      except EOFError, e:
    1142                          # if we come across an EOFError here, we must assume the session is dead...
    1143                          self.session_died = True
    1144                          raise x2go_exceptions.X2GoControlSessionException('The SSH connection was dropped while setting up SSH agent forwarding socket.')
    1145                  else:
    1146                      self.logger('SSH agent forwarding is not available in the Paramiko version used with this instance of Python X2Go', loglevel=log.loglevel_WARN)
    1147
    1148          else:
    1149              self.close()
    1150              if self.sshproxy_session:
    1151                  self.sshproxy_session.stop_thread()
    1152
    1153          self._remote_home = None
    1154          if not self.home_exists():
    1155              self.close()
    1156              if self.sshproxy_session:
    1157                  self.sshproxy_session.stop_thread()
    1158              raise x2go_exceptions.X2GoRemoteHomeException('remote home directory does not exist')
    1159
    1160          return (self.get_transport() is not None)
    1161
    1162 -    def dissociate(self, terminal_session):
    1163          """\
    1164          Drop an associated terminal session.
    1165
    1166          @param terminal_session: the terminal session object to remove from the list of associated terminals
    1167          @type terminal_session: C{X2GoTerminalSession*}
    1168
    1169          """
    1170          for t_name in self.associated_terminals.keys():
    1171              if self.associated_terminals[t_name] == terminal_session:
    1172                  del self.associated_terminals[t_name]
    1173                  if self.terminated_terminals.has_key(t_name):
    1174                      del self.terminated_terminals[t_name]
    1175
    1176 -    def disconnect(self):
    1177          """\
    1178          Disconnect this control session from the remote server.
    1179
    1180          @return: report success or failure after having disconnected
    1181          @rtype: C{bool}
    1182
    1183          """
    1184          if self.associated_terminals:
    1185              t_names = self.associated_terminals.keys()
    1186              for t_obj in self.associated_terminals.values():
    1187                  try:
    1188                      if not self.session_died:
    1189                          t_obj.suspend()
    1190                  except x2go_exceptions.X2GoTerminalSessionException:
    1191                      pass
    1192                  except x2go_exceptions.X2GoControlSessionException:
    1193                      self.session_died
    1194                  t_obj.__del__()
    1195              for t_name in t_names:
    1196                  try:
    1197                      del self.associated_terminals[t_name]
    1198                  except KeyError:
    1199                      pass
    1200
    1201          self._remote_home = None
    1202          self._remote_group = {}
    1203
    1204          self._session_auth_rsakey = None
    1205
    1206          # in any case, release out internal transport lock
    1207          self._transport_lock.release()
    1208
    1209          # close SSH agent auth forwarding objects
    1210          if self.agent_handler is not None:
    1211              self.agent_handler.close()
    1212
    1213          if self.agent_chan is not None:
    1214              try:
    1215                  self.agent_chan.close()
    1216              except EOFError:
    1217                  pass
    1218
    1219          retval = False
    1220          try:
    1221              if self.get_transport() is not None:
    1222                  retval = self.get_transport().is_active()
    1223                  try:
    1224                      self.close()
    1225                  except IOError:
    1226                      pass
    1227          except AttributeError:
    1228              # if the Paramiko _transport object has not yet been initialized, ignore it
    1229              # but state that this method call did not close the SSH client, but was already closed
    1230              pass
    1231
    1232          # take down sshproxy_session no matter what happened to the control session itself
    1233          if self.sshproxy_session is not None:
    1234              self.sshproxy_session.stop_thread()
    1235
    1236          return retval
    1237
    1238 -    def home_exists(self):
    1239          """\
    1240          Test if the remote home directory exists.
    1241
    1242          @return: C{True} if the home directory exists, C{False} otherwise
    1243          @rtype: C{bool}
    1244
    1245          """
    1246          (_stdin, _stdout, _stderr) = self._x2go_exec_command('stat -tL "%s"' % self._x2go_remote_home, loglevel=log.loglevel_DEBUG)
    1247          if _stdout.read():
    1248              return True
    1249          return False
    1250
    1251
    1252 -    def is_alive(self):
    1253          """\
    1254          Test if the connection to the remote X2Go server is still alive.
    1255
    1256          @return: C{True} if the connection is still alive, C{False} otherwise
    1257          @rtype: C{bool}
    1258
    1259          """
    1260          try:
    1261              if self._x2go_exec_command('echo', loglevel=log.loglevel_DEBUG):
    1262                  return True
    1263          except x2go_exceptions.X2GoControlSessionException:
    1264              self.session_died = True
    1265              self.disconnect()
    1266          return False
    1267
    1268 -    def has_session_died(self):
    1269          """\
    1270          Test if the connection to the remote X2Go server died on the way.
    1271
    1272          @return: C{True} if the connection has died, C{False} otherwise
    1273          @rtype: C{bool}
    1274
    1275          """
    1276          return self.session_died
    1277
    1278 -    def get_published_applications(self, lang=None, refresh=False, raw=False, very_raw=False, max_no_submenus=defaults.PUBAPP_MAX_NO_SUBMENUS):
    1279          """\
    1280          Retrieve the menu tree of published applications from the remote X2Go server.
    1281
    1282          The C{raw} option lets this method return a C{list} of C{dict} elements. Each C{dict} elements has a
    1283          C{desktop} key containing a shortened version of the text output of a .desktop file and an C{icon} key
    1284          which contains the desktop base64-encoded icon data.
    1285
    1286          The {very_raw} lets this method return the output of the C{x2gogetapps} script as is.
    1287
    1288          @param lang: locale/language identifier
    1289          @type lang: C{str}
    1290          @param refresh: force reload of the menu tree from X2Go server
    1291          @type refresh: C{bool}
    1292          @param raw: retrieve a raw output of the server list of published applications
    1293          @type raw: C{bool}
    1294          @param very_raw: retrieve a very raw output of the server list of published applications
    1295          @type very_raw: C{bool}
    1296
    1297          @return: an i18n capable menu tree packed as a Python dictionary
    1298          @rtype: C{list}
    1299
    1300          """
    1301          self._already_querying_published_applications.acquire()
    1302
    1303          if defaults.X2GOCLIENT_OS != 'Windows' and lang is None:
    1304              lang = locale.getdefaultlocale()[0]
    1305          elif lang is None:
    1306              lang = 'en'
    1307
    1308          if 'X2GO_PUBLISHED_APPLICATIONS' in self.get_server_features():
    1309              if self._published_applications_menu is {} or \
    1310                 not self._published_applications_menu.has_key(lang) or \
    1311                 raw or very_raw or refresh or \
    1312                 (self.published_applications_no_submenus != max_no_submenus):
    1313
    1314                  self.published_applications_no_submenus = max_no_submenus
    1315
    1316                  ### STAGE 1: retrieve menu from server
    1317
    1318                  self.logger('querying server (%s) for list of published applications' % self.profile_name, loglevel=log.loglevel_NOTICE)
    1319                  (stdin, stdout, stderr) = self._x2go_exec_command('which x2gogetapps >/dev/null && x2gogetapps')
    1320                  _raw_output = stdout.read()
    1321
    1322                  if very_raw:
    1323                      self.logger('published applications query for %s finished, return very raw output' % self.profile_name, loglevel=log.loglevel_NOTICE)
    1324                      self._already_querying_published_applications.release()
    1325                      return _raw_output
    1326
    1327                  ### STAGE 2: dissect the text file retrieved from server, cut into single menu elements
    1328
    1329                  _raw_menu_items = _raw_output.split('</desktop>\n')
    1330                  _raw_menu_items = [ i.replace('<desktop>\n', '') for i in _raw_menu_items ]
    1331                  _menu = []
    1332                  for _raw_menu_item in _raw_menu_items:
    1333                      if '<icon>\n' in _raw_menu_item and '</icon>' in _raw_menu_item:
    1334                          _menu_item = _raw_menu_item.split('<icon>\n')[0] + _raw_menu_item.split('</icon>\n')[1]
    1335                          _icon_base64 = _raw_menu_item.split('<icon>\n')[1].split('</icon>\n')[0]
    1336                      else:
    1337                          _menu_item = _raw_menu_item
    1338                          _icon_base64 = None
    1339                      if _menu_item:
    1340                          _menu.append({ 'desktop': _menu_item, 'icon': _icon_base64, })
    1341                          _menu_item = None
    1342                          _icon_base64 = None
    1343
    1344                  if raw:
    1345                      self.logger('published applications query for %s finished, returning raw output' % self.profile_name, loglevel=log.loglevel_NOTICE)
    1346                      self._already_querying_published_applications.release()
    1347                      return _menu
    1348
    1349                  if len(_menu) > max_no_submenus >= 0:
    1350                      _render_submenus = True
    1351                  else:
    1352                      _render_submenus = False
    1353
    1354                  # STAGE 3: create menu structure in a Python dictionary
    1355
    1356                  _category_map = {
    1357                      lang: {
    1358                          'Multimedia': [],
    1359                          'Development': [],
    1360                          'Education': [],
    1361                          'Games': [],
    1362                          'Graphics': [],
    1363                          'Internet': [],
    1364                          'Office': [],
    1365                          'System': [],
    1366                          'Utilities': [],
    1367                          'Other Applications': [],
    1368                          'TOP': [],
    1369                      }
    1370                  }
    1371                  _empty_menus = _category_map[lang].keys()
    1372
    1373                  for item in _menu:
    1374
    1375                      _menu_entry_name = ''
    1376                      _menu_entry_fallback_name = ''
    1377                      _menu_entry_comment = ''
    1378                      _menu_entry_fallback_comment = ''
    1379                      _menu_entry_exec = ''
    1380                      _menu_entry_cat = ''
    1381                      _menu_entry_shell = False
    1382
    1383                      lang_regio = lang
    1384                      lang_only = lang_regio.split('_')[0]
    1385
    1386                      for line in item['desktop'].split('\n'):
    1387                          if re.match('^Name\[%s\]=.*' % lang_regio, line) or re.match('Name\[%s\]=.*' % lang_only, line):
    1388                              _menu_entry_name = line.split("=")[1].strip()
    1389                          elif re.match('^Name=.*', line):
    1390                              _menu_entry_fallback_name = line.split("=")[1].strip()
    1391                          elif re.match('^Comment\[%s\]=.*' % lang_regio, line) or re.match('Comment\[%s\]=.*' % lang_only, line):
    1392                              _menu_entry_comment = line.split("=")[1].strip()
    1393                          elif re.match('^Comment=.*', line):
    1394                              _menu_entry_fallback_comment = line.split("=")[1].strip()
    1395                          elif re.match('^Exec=.*', line):
    1396                              _menu_entry_exec = line.split("=")[1].strip()
    1397                          elif re.match('^Terminal=.*(t|T)(r|R)(u|U)(e|E).*', line):
    1398                              _menu_entry_shell = True
    1399                          elif re.match('^Categories=.*', line):
    1400                              if 'X2Go-Top' in line:
    1401                                  _menu_entry_cat = 'TOP'
    1402                              elif 'Audio' in line or 'Video' in line:
    1403                                  _menu_entry_cat = 'Multimedia'
    1404                              elif 'Development' in line:
    1405                                  _menu_entry_cat = 'Development'
    1406                              elif 'Education' in line:
    1407                                  _menu_entry_cat = 'Education'
    1408                              elif 'Game' in line:
    1409                                  _menu_entry_cat = 'Games'
    1410                              elif 'Graphics' in line:
    1411                                  _menu_entry_cat = 'Graphics'
    1412                              elif 'Network' in line:
    1413                                  _menu_entry_cat = 'Internet'
    1414                              elif 'Office' in line:
    1415                                  _menu_entry_cat = 'Office'
    1416                              elif 'Settings' in line:
    1417                                  continue
    1418                              elif 'System' in line:
    1419                                  _menu_entry_cat = 'System'
    1420                              elif 'Utility' in line:
    1421                                  _menu_entry_cat = 'Utilities'
    1422                              else:
    1423                                  _menu_entry_cat = 'Other Applications'
    1424
    1425                      if not _menu_entry_exec:
    1426                          continue
    1427                      else:
    1428                          # FIXME: strip off any noted options (%f, %F, %u, %U, ...), this can be more intelligent
    1429                          _menu_entry_exec = _menu_entry_exec.replace('%f', '').replace('%F','').replace('%u','').replace('%U','')
    1430                          if _menu_entry_shell:
    1431                              _menu_entry_exec = "x-terminal-emulator -e '%s'" % _menu_entry_exec
    1432
    1433                      if not _menu_entry_cat:
    1434                          _menu_entry_cat = 'Other Applications'
    1435
    1436                      if not _render_submenus:
    1437                          _menu_entry_cat = 'TOP'
    1438
    1439                      if _menu_entry_cat in _empty_menus:
    1440                          _empty_menus.remove(_menu_entry_cat)
    1441
    1442                      if not _menu_entry_name: _menu_entry_name = _menu_entry_fallback_name
    1443                      if not _menu_entry_comment: _menu_entry_comment = _menu_entry_fallback_comment
    1444                      if not _menu_entry_comment: _menu_entry_comment = _menu_entry_name
    1445
    1446                      _menu_entry_icon = item['icon']
    1447
    1448                      _category_map[lang][_menu_entry_cat].append(
    1449                          {
    1450                              'name': _menu_entry_name,
    1451                              'comment': _menu_entry_comment,
    1452                              'exec': _menu_entry_exec,
    1453                              'icon': _menu_entry_icon,
    1454                          }
    1455                      )
    1456
    1457                  for _cat in _empty_menus:
    1458                      del _category_map[lang][_cat]
    1459
    1460                  for _cat in _category_map[lang].keys():
    1461                      _sorted = sorted(_category_map[lang][_cat], key=lambda k: k['name'])
    1462                      _category_map[lang][_cat] = _sorted
    1463
    1464                  self._published_applications_menu.update(_category_map)
    1465                  self.logger('published applications query for %s finished, return menu tree' % self.profile_name, loglevel=log.loglevel_NOTICE)
    1466
    1467          else:
    1468              # FIXME: ignoring the absence of the published applications feature for now, handle it appropriately later
    1469              pass
    1470
    1471          self._already_querying_published_applications.release()
    1472          return self._published_applications_menu
    1473
    1474 -    def start(self, **kwargs):
    1475          """\
    1476          Start a new X2Go session.
    1477
    1478          The L{X2GoControlSession.start()} method accepts any parameter
    1479          that can be passed to any of the C{X2GoTerminalSession} backend class
    1480          constructors.
    1481
    1482          @param kwargs: parameters that get passed through to the control session's
    1483              L{resume()} method, only the C{session_name} parameter will get removed
    1484              before pass-through
    1485          @type kwargs: C{dict}
    1486
    1487          @return: return value of the cascaded L{resume()} method, denoting the success or failure
    1488              of the session startup
    1489          @rtype: C{bool}
    1490
    1491          """
    1492          if 'session_name' in kwargs.keys():
    1493              del kwargs['session_name']
    1494          return self.resume(**kwargs)
    1495
    1496 -    def resume(self, session_name=None, session_instance=None, session_list=None, **kwargs):
    1497          """\
    1498          Resume a running/suspended X2Go session.
    1499
    1500          The L{X2GoControlSession.resume()} method accepts any parameter
    1501          that can be passed to any of the C{X2GoTerminalSession*} backend class constructors.
    1502
    1503          @return: True if the session could be successfully resumed
    1504          @rtype: C{bool}
    1505
    1506          @raise X2GoUserException: if the remote user is not allowed to launch/resume X2Go sessions.
    1507
    1508          """
    1509          if self.get_transport() is not None:
    1510
    1511              if not self.is_x2gouser(self.get_transport().get_username()):
    1512                  raise x2go_exceptions.X2GoUserException('remote user %s is not allowed to run X2Go commands' % self.get_transport().get_username())
    1513
    1514              session_info = None
    1515              try:
    1516                  if session_name is not None:
    1517                      if session_list:
    1518                          session_info = session_list[session_name]
    1519                      else:
    1520                          session_info = self.list_sessions()[session_name]
    1521              except KeyError:
    1522                  _success = False
    1523
    1524              _terminal = self._terminal_backend(self,
    1525                                                 profile_name=self.profile_name,
    1526                                                 session_info=session_info,
    1527                                                 info_backend=self._info_backend,
    1528                                                 list_backend=self._list_backend,
    1529                                                 proxy_backend=self._proxy_backend,
    1530                                                 client_rootdir=self.client_rootdir,
    1531                                                 session_instance=session_instance,
    1532                                                 sessions_rootdir=self.sessions_rootdir,
    1533                                                 **kwargs)
    1534
    1535              _success = False
    1536              try:
    1537                  if session_name is not None:
    1538                      _success = _terminal.resume()
    1539                  else:
    1540                      _success = _terminal.start()
    1541              except x2go_exceptions.X2GoTerminalSessionException:
    1542                  _success = False
    1543
    1544              if _success:
    1545                  while not _terminal.ok():
    1546                      gevent.sleep(.2)
    1547
    1548                  if _terminal.ok():
    1549                      self.associated_terminals[_terminal.get_session_name()] = _terminal
    1550                      self.get_transport().reverse_tunnels[_terminal.get_session_name()] = {
    1551                          'sshfs': (0, None),
    1552                          'snd': (0, None),
    1553                      }
    1554
    1555                      return _terminal or None
    1556
    1557          return None
    1558
    1559 -    def share_desktop(self, desktop=None, user=None, display=None, share_mode=0, **kwargs):
    1560          """\
    1561          Share another already running desktop session. Desktop sharing can be run
    1562          in two different modes: view-only and full-access mode.
    1563
    1564          @param desktop: desktop ID of a sharable desktop in format C{<user>@<display>}
    1565          @type desktop: C{str}
    1566          @param user: user name and display number can be given separately, here give the
    1567              name of the user who wants to share a session with you
    1568          @type user: C{str}
    1569          @param display: user name and display number can be given separately, here give the
    1570              number of the display that a user allows you to be shared with
    1571          @type display: C{str}
    1572          @param share_mode: desktop sharing mode, 0 stands for VIEW-ONLY, 1 for  FULL-ACCESS mode
    1573          @type share_mode: C{int}
    1574
    1575          @return: True if the session could be successfully shared
    1576          @rtype: C{bool}
    1577
    1578          @raise X2GoDesktopSharingException: if C{username} and C{dislpay} do not relate to a
    1579              sharable desktop session
    1580
    1581          """
    1582          if desktop:
    1583              user = desktop.split('@')[0]
    1584              display = desktop.split('@')[1]
    1585          if not (user and display):
    1586              raise x2go_exceptions.X2GoDesktopSharingException('Need user name and display number of shared desktop.')
    1587
    1588          cmd = '%sXSHAD%sXSHAD%s' % (share_mode, user, display)
    1589
    1590          kwargs['cmd'] = cmd
    1591          kwargs['session_type'] = 'shared'
    1592
    1593          return self.start(**kwargs)
    1594
    1595 -    def list_desktops(self, raw=False, maxwait=20):
    1596          """\
    1597          List all desktop-like sessions of current user (or of users that have
    1598          granted desktop sharing) on the connected server.
    1599
    1600          @param raw: if C{True}, the raw output of the server-side X2Go command
    1601              C{x2golistdesktops} is returned.
    1602          @type raw: C{bool}
    1603
    1604          @return: a list of X2Go desktops available for sharing
    1605          @rtype: C{list}
    1606
    1607          @raise X2GoTimeOutException: on command execution timeouts, with the server-side C{x2golistdesktops}
    1608              command this can sometimes happen. Make sure you ignore these time-outs and to try again
    1609
    1610          """
    1611          if raw:
    1612              (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistdesktops")
    1613              return stdout.read(), stderr.read()
    1614
    1615          else:
    1616
    1617              # this _success loop will catch errors in case the x2golistsessions output is corrupt
    1618              # this should not be needed and is a workaround for the current X2Go server implementation
    1619
    1620              if self.low_latency:
    1621                  maxwait = maxwait * 2
    1622
    1623              timeout = gevent.Timeout(maxwait)
    1624              timeout.start()
    1625              try:
    1626                  (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistdesktops")
    1627                  _stdout_read = stdout.read()
    1628                  _listdesktops = _stdout_read.split('\n')
    1629              except gevent.timeout.Timeout:
    1630                  # if we do not get a reply here after <maxwait> seconds we will raise a time out, we have to
    1631                  # make sure that we catch this at places where we want to ignore timeouts (e.g. in the
    1632                  # desktop list cache)
    1633                  raise x2go_exceptions.X2GoTimeOutException('x2golistdesktop command timed out')
    1634              finally:
    1635                  timeout.cancel()
    1636
    1637              return _listdesktops
    1638
    1639 -    def list_mounts(self, session_name, raw=False, maxwait=20):
    1640          """\
    1641          List all mounts for a given session of the current user on the connected server.
    1642
    1643          @param session_name: name of a session to query a list of mounts for
    1644          @type session_name: C{str}
    1645          @param raw: if C{True}, the raw output of the server-side X2Go command
    1646              C{x2golistmounts} is returned.
    1647          @type raw: C{bool}
    1648          @param maxwait: stop processing C{x2golistmounts} after C{<maxwait>} seconds
    1649          @type maxwait: C{int}
    1650
    1651          @return: a list of client-side mounts for X2Go session C{<session_name>} on the server
    1652          @rtype: C{list}
    1653
    1654          @raise X2GoTimeOutException: on command execution timeouts, queries with the server-side
    1655              C{x2golistmounts} query should normally be processed quickly, a time-out may hint that the
    1656              control session has lost its connection to the X2Go server
    1657
    1658          """
    1659          if raw:
    1660              (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistmounts %s" % session_name)
    1661              return stdout.read(), stderr.read()
    1662
    1663          else:
    1664
    1665              if self.low_latency:
    1666                  maxwait = maxwait * 2
    1667
    1668              # this _success loop will catch errors in case the x2golistmounts output is corrupt
    1669
    1670              timeout = gevent.Timeout(maxwait)
    1671              timeout.start()
    1672              try:
    1673                  (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistmounts %s" % session_name)
    1674                  _stdout_read = stdout.read()
    1675                  _listmounts = {session_name: [ line for line in _stdout_read.split('\n') if line ] }
    1676              except gevent.timeout.Timeout:
    1677                  # if we do not get a reply here after <maxwait> seconds we will raise a time out, we have to
    1678                  # make sure that we catch this at places where we want to ignore timeouts
    1679                  raise x2go_exceptions.X2GoTimeOutException('x2golistmounts command timed out')
    1680              finally:
    1681                  timeout.cancel()
    1682
    1683              return _listmounts
    1684
    1685 -    def list_sessions(self, raw=False):
    1686          """\
    1687          List all sessions of current user on the connected server.
    1688
    1689          @param raw: if C{True}, the raw output of the server-side X2Go command
    1690              C{x2golistsessions} is returned.
    1691          @type raw: C{bool}
    1692
    1693          @return: normally an instance of a C{X2GoServerSessionList*} backend is returned. However,
    1694              if the raw argument is set, the plain text output of the server-side C{x2golistsessions}
    1695              command is returned
    1696          @rtype: C{X2GoServerSessionList} instance or str
    1697
    1698          @raise X2GoControlSessionException: on command execution timeouts, if this happens the control session will
    1699              be interpreted as disconnected due to connection loss
    1700          """
    1701          if raw:
    1702              if 'X2GO_LIST_SHADOWSESSIONS' in self._x2go_server_features:
    1703                  (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && { x2golistsessions; x2golistshadowsessions; }")
    1704              else:
    1705                  (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistsessions")
    1706              return stdout.read(), stderr.read()
    1707
    1708          else:
    1709
    1710              # this _success loop will catch errors in case the x2golistsessions output is corrupt
    1711              # this should not be needed and is a workaround for the current X2Go server implementation
    1712              _listsessions = {}
    1713              _success = False
    1714              _count = 0
    1715              _maxwait = 20
    1716
    1717              # we will try this 20 times before giving up... we might simply catch the x2golistsessions
    1718              # output in the middle of creating a session in the database...
    1719              while not _success and _count < _maxwait:
    1720                  _count += 1
    1721                  try:
    1722                      if 'X2GO_LIST_SHADOWSESSIONS' in self._x2go_server_features:
    1723                          (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && { x2golistsessions; x2golistshadowsessions; }")
    1724                      else:
    1725                          (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistsessions")
    1726                      _stdout_read = stdout.read()
    1727                      _listsessions = self._list_backend(_stdout_read, info_backend=self._info_backend).sessions
    1728                      _success = True
    1729                  except KeyError:
    1730                      gevent.sleep(1)
    1731                  except IndexError:
    1732                      gevent.sleep(1)
    1733                  except ValueError:
    1734                      gevent.sleep(1)
    1735
    1736              if _count >= _maxwait:
    1737                  self.session_died = True
    1738                  self.disconnect()
    1739                  raise x2go_exceptions.X2GoControlSessionException('x2golistsessions command failed after we have tried 20 times')
    1740
    1741              # update internal variables when list_sessions() is called
    1742              if _success and not self.session_died:
    1743                  for _session_name, _terminal in self.associated_terminals.items():
    1744                      if _session_name in _listsessions.keys():
    1745                          # update the whole session_info object within the terminal session
    1746                          if hasattr(self.associated_terminals[_session_name], 'session_info') and not self.associated_terminals[_session_name].is_session_info_protected():
    1747                              self.associated_terminals[_session_name].session_info.update(_listsessions[_session_name])
    1748                      else:
    1749                          self.associated_terminals[_session_name].__del__()
    1750                          try: del self.associated_terminals[_session_name]
    1751                          except KeyError: pass
    1752                          self.terminated_terminals.append(_session_name)
    1753                      if _terminal.is_suspended():
    1754                          self.associated_terminals[_session_name].__del__()
    1755                          try: del self.associated_terminals[_session_name]
    1756                          except KeyError: pass
    1757
    1758              return _listsessions
    1759
    1760 -    def clean_sessions(self, destroy_terminals=True, published_applications=False):
    1761          """\
    1762          Find X2Go terminals that have previously been started by the
    1763          connected user on the remote X2Go server and terminate them.
    1764
    1765          @param destroy_terminals: destroy the terminal session instances after cleanup
    1766          @type destroy_terminals: C{bool}
    1767          @param published_applications: also clean up published applications providing sessions
    1768          @type published_applications: C{bool}
    1769
    1770          """
    1771          session_list = self.list_sessions()
    1772          if published_applications:
    1773              session_names = session_list.keys()
    1774          else:
    1775              session_names = [ _sn for _sn in session_list.keys() if not session_list[_sn].is_published_applications_provider() ]
    1776          for session_name in session_names:
    1777              if self.associated_terminals.has_key(session_name):
    1778                  self.associated_terminals[session_name].terminate()
    1779                  if destroy_terminals:
    1780                      if self.associated_terminals[session_name] is not None:
    1781                          self.associated_terminals[session_name].__del__()
    1782                      try: del self.associated_terminals[session_name]
    1783                      except KeyError: pass
    1784              else:
    1785                  self.terminate(session_name=session_name)
    1786
    1787 -    def is_connected(self):
    1788          """\
    1789          Returns C{True} if this control session is connected to the remote server (that
    1790          is: if it has a valid Paramiko/SSH transport object).
    1791
    1792          @return: X2Go session connected?
    1793          @rtype: C{bool}
    1794
    1795          """
    1796          return self.get_transport() is not None and self.get_transport().is_authenticated()
    1797
    1798 -    def is_running(self, session_name):
    1799          """\
    1800          Returns C{True} if the given X2Go session is in running state,
    1801          C{False} else.
    1802
    1803          @param session_name: X2Go name of the session to be queried
    1804          @type session_name: C{str}
    1805
    1806          @return: X2Go session running? If C{<session_name>} is not listable by the L{list_sessions()} method then C{None} is returned
    1807          @rtype: C{bool} or C{None}
    1808
    1809          """
    1810          session_infos = self.list_sessions()
    1811          if session_name in session_infos.keys():
    1812              return session_infos[session_name].is_running()
    1813          return None
    1814
    1815 -    def is_suspended(self, session_name):
    1816          """\
    1817          Returns C{True} if the given X2Go session is in suspended state,
    1818          C{False} else.
    1819
    1820          @return: X2Go session suspended? If C{<session_name>} is not listable by the L{list_sessions()} method then C{None} is returned
    1821          @rtype: C{bool} or C{None}
    1822
    1823          """
    1824          session_infos = self.list_sessions()
    1825          if session_name in session_infos.keys():
    1826              return session_infos[session_name].is_suspended()
    1827          return None
    1828
    1829 -    def has_terminated(self, session_name):
    1830          """\
    1831          Returns C{True} if the X2Go session with name C{<session_name>} has been seen
    1832          by this control session and--in the meantime--has been terminated.
    1833
    1834          If C{<session_name>} has not been seen, yet, the method will return C{None}.
    1835
    1836          @return: X2Go session has terminated?
    1837          @rtype: C{bool} or C{None}
    1838
    1839          """
    1840          session_infos = self.list_sessions()
    1841          if session_name in self.terminated_terminals:
    1842              return True
    1843          if session_name not in session_infos.keys() and session_name in self.associated_terminals.keys():
    1844              # do a post-mortem tidy up
    1845              self.terminate(session_name)
    1846              return True
    1847          if self.is_suspended(session_name) or self.is_running(session_name):
    1848              return False
    1849
    1850          return None
    1851
    1852 -    def suspend(self, session_name):
    1853          """\
    1854          Suspend X2Go session with name C{<session_name>} on the connected
    1855          server.
    1856
    1857          @param session_name: X2Go name of the session to be suspended
    1858          @type session_name: C{str}
    1859
    1860          @return: C{True} if the session could be successfully suspended
    1861          @rtype: C{bool}
    1862
    1863          """
    1864          _ret = False
    1865          _session_names = [ t.get_session_name() for t in self.associated_terminals.values() ]
    1866          if session_name in _session_names:
    1867
    1868              self.logger('suspending associated terminal session: %s' % session_name, loglevel=log.loglevel_DEBUG)
    1869              (stdin, stdout, stderr) = self._x2go_exec_command("x2gosuspend-session %s" % session_name, loglevel=log.loglevel_DEBUG)
    1870              stdout.read()
    1871              stderr.read()
    1872              if self.associated_terminals.has_key(session_name):
    1873                  if self.associated_terminals[session_name] is not None:
    1874                      self.associated_terminals[session_name].__del__()
    1875                  try: del self.associated_terminals[session_name]
    1876                  except KeyError: pass
    1877              _ret = True
    1878
    1879          else:
    1880
    1881              self.logger('suspending non-associated terminal session: %s' % session_name, loglevel=log.loglevel_DEBUG)
    1882              (stdin, stdout, stderr) = self._x2go_exec_command("x2gosuspend-session %s" % session_name, loglevel=log.loglevel_DEBUG)
    1883              stdout.read()
    1884              stderr.read()
    1885              _ret = True
    1886
    1887          return _ret
    1888
    1889 -    def terminate(self, session_name, destroy_terminals=True):
    1890          """\
    1891          Terminate X2Go session with name C{<session_name>} on the connected
    1892          server.
    1893
    1894          @param session_name: X2Go name of the session to be terminated
    1895          @type session_name: C{str}
    1896
    1897          @return: C{True} if the session could be successfully terminated
    1898          @rtype: C{bool}
    1899
    1900          """
    1901
    1902          _ret = False
    1903          if session_name in self.associated_terminals.keys():
    1904
    1905              self.logger('terminating associated session: %s' % session_name, loglevel=log.loglevel_DEBUG)
    1906              (stdin, stdout, stderr) = self._x2go_exec_command("x2goterminate-session %s" % session_name, loglevel=log.loglevel_DEBUG)
    1907              stdout.read()
    1908              stderr.read()
    1909
    1910              if destroy_terminals:
    1911                  if self.associated_terminals[session_name] is not None:
    1912                      self.associated_terminals[session_name].__del__()
    1913                  try: del self.associated_terminals[session_name]
    1914                  except KeyError: pass
    1915
    1916              self.terminated_terminals.append(session_name)
    1917              _ret = True
    1918
    1919          else:
    1920
    1921              self.logger('terminating non-associated session: %s' % session_name, loglevel=log.loglevel_DEBUG)
    1922              (stdin, stdout, stderr) = self._x2go_exec_command("x2goterminate-session %s" % session_name, loglevel=log.loglevel_DEBUG)
    1923              stdout.read()
    1924              stderr.read()
    1925              _ret = True
    1926
    1927          return _ret
    1928