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