Package x2go :: Package backends :: Package control :: Module _stdout

[frames] | no frames]

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

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