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