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