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 (stdin, stdout, stderr) = self._x2go_exec_command('which fusermount')
674
675
676 return bool(stdout.read())
677
679 """\
680 Returns (and caches) the control session's remote username.
681
682 @return: SSH transport's user name
683 @rtype: C{str}
684
685 @raise X2GoControlSessionException: on SSH connection loss
686
687 """
688 if self._remote_username is None:
689 if self.get_transport() is not None:
690 try:
691 self._remote_username = self.get_transport().get_username()
692 except:
693 self.session_died = True
694 raise x2go_exceptions.X2GoControlSessionException('Lost connection to X2Go server')
695 return self._remote_username
696
698 """\
699 Returns (and caches) the control session's remote host (name or ip).
700
701 @return: SSH transport's peer name
702 @rtype: C{tuple}
703
704 @raise X2GoControlSessionException: on SSH connection loss
705
706 """
707 if self._remote_peername is None:
708 if self.get_transport() is not None:
709 try:
710 self._remote_peername = self.get_transport().getpeername()
711 except:
712 self.session_died = True
713 raise x2go_exceptions.X2GoControlSessionException('Lost connection to X2Go server')
714 return self._remote_peername
715
716 @property
718 """\
719 Generate (and cache) a temporary RSA host key for the lifetime of this control session.
720
721 """
722 if self._session_auth_rsakey is None:
723 self._session_auth_rsakey = paramiko.RSAKey.generate(defaults.RSAKEY_STRENGTH)
724 return self._session_auth_rsakey
725
727 """\
728 Manipulate the control session's profile name.
729
730 @param profile_name: new profile name for this control session
731 @type profile_name: C{str}
732
733 """
734 self.profile_name = profile_name
735
737 """\
738 Wraps around a Paramiko/SSH host key check.
739
740 @param hostname: the remote X2Go server's hostname
741 @type hostname: C{str}
742 @param port: the SSH port of the remote X2Go server
743 @type port: C{int}
744
745 @return: C{True} if the host key check succeeded, C{False} otherwise
746 @rtype: C{bool}
747
748 """
749
750 hostname = hostname.strip()
751
752
753 if hostname in ('localhost', 'localhost.localdomain'):
754 hostname = '127.0.0.1'
755
756 return checkhosts.check_ssh_host_key(self, hostname, port=port)
757
758 - def connect(self, hostname, port=22, username=None, password=None, passphrase=None, pkey=None,
759 key_filename=None, timeout=None, allow_agent=False, look_for_keys=False,
760 use_sshproxy=False, sshproxy_host=None, sshproxy_port=22, sshproxy_user=None, sshproxy_password=None, sshproxy_force_password_auth=False,
761 sshproxy_key_filename=None, sshproxy_pkey=None, sshproxy_look_for_keys=False, sshproxy_passphrase='', sshproxy_allow_agent=False,
762 sshproxy_tunnel=None,
763 add_to_known_hosts=None,
764 forward_sshagent=None,
765 unique_hostkey_aliases=None,
766 force_password_auth=False,
767 session_instance=None,
768 ):
769 """\
770 Connect to an X2Go server and authenticate to it. This method is directly
771 inherited from the C{paramiko.SSHClient} class. The features of the Paramiko
772 SSH client connect method are recited here. The parameters C{add_to_known_hosts},
773 C{force_password_auth}, C{session_instance} and all SSH proxy related parameters
774 have been added as X2Go specific parameters
775
776 The server's host key is checked against the system host keys
777 (see C{load_system_host_keys}) and any local host keys (C{load_host_keys}).
778 If the server's hostname is not found in either set of host keys, the missing host
779 key policy is used (see C{set_missing_host_key_policy}). The default policy is
780 to reject the key and raise an C{SSHException}.
781
782 Authentication is attempted in the following order of priority:
783
784 - The C{pkey} or C{key_filename} passed in (if any)
785 - Any key we can find through an SSH agent
786 - Any "id_rsa" or "id_dsa" key discoverable in C{~/.ssh/}
787 - Plain username/password auth, if a password was given
788
789 If a private key requires a password to unlock it, and a password is
790 passed in, that password will be used to attempt to unlock the key.
791
792 @param hostname: the server to connect to
793 @type hostname: C{str}
794 @param port: the server port to connect to
795 @type port: C{int}
796 @param username: the username to authenticate as (defaults to the
797 current local username)
798 @type username: C{str}
799 @param password: a password to use for authentication or for unlocking
800 a private key
801 @type password: C{str}
802 @param passphrase: a passphrase to use for unlocking
803 a private key in case the password is already needed for two-factor
804 authentication
805 @type passphrase: C{str}
806 @param key_filename: the filename, or list of filenames, of optional
807 private key(s) to try for authentication
808 @type key_filename: C{str} or list(str)
809 @param pkey: an optional private key to use for authentication
810 @type pkey: C{PKey}
811 @param forward_sshagent: forward SSH agent authentication requests to the X2Go client-side
812 (will update the class property of the same name)
813 @type forward_sshagent: C{bool}
814 @param unique_hostkey_aliases: update the unique_hostkey_aliases class property
815 @type unique_hostkey_aliases: C{bool}
816 @param timeout: an optional timeout (in seconds) for the TCP connect
817 @type timeout: float
818 @param look_for_keys: set to C{True} to enable searching for discoverable
819 private key files in C{~/.ssh/}
820 @type look_for_keys: C{bool}
821 @param allow_agent: set to C{True} to enable connecting to a local SSH agent
822 for acquiring authentication information
823 @type allow_agent: C{bool}
824 @param add_to_known_hosts: non-paramiko option, if C{True} paramiko.AutoAddPolicy()
825 is used as missing-host-key-policy. If set to C{False} paramiko.RejectPolicy()
826 is used
827 @type add_to_known_hosts: C{bool}
828 @param force_password_auth: non-paramiko option, disable pub/priv key authentication
829 completely, even if the C{pkey} or the C{key_filename} parameter is given
830 @type force_password_auth: C{bool}
831 @param session_instance: an instance L{X2GoSession} using this L{X2GoControlSession}
832 instance.
833 @type session_instance: C{obj}
834 @param use_sshproxy: connect through an SSH proxy
835 @type use_sshproxy: C{True} if an SSH proxy is to be used for tunneling the connection
836 @param sshproxy_host: hostname of the SSH proxy server
837 @type sshproxy_host: C{str}
838 @param sshproxy_port: port of the SSH proxy server
839 @type sshproxy_port: C{int}
840 @param sshproxy_user: username that we use for authenticating against C{<sshproxy_host>}
841 @type sshproxy_user: C{str}
842 @param sshproxy_password: a password to use for SSH proxy authentication or for unlocking
843 a private key
844 @type sshproxy_password: C{str}
845 @param sshproxy_passphrase: a passphrase to use for unlocking
846 a private key needed for the SSH proxy host in case the sshproxy_password is already needed for
847 two-factor authentication
848 @type sshproxy_passphrase: C{str}
849 @param sshproxy_force_password_auth: enforce using a given C{sshproxy_password} even if a key(file) is given
850 @type sshproxy_force_password_auth: C{bool}
851 @param sshproxy_key_filename: local file location of the private key file
852 @type sshproxy_key_filename: C{str}
853 @param sshproxy_pkey: an optional private key to use for SSH proxy authentication
854 @type sshproxy_pkey: C{PKey}
855 @param sshproxy_look_for_keys: set to C{True} to enable connecting to a local SSH agent
856 for acquiring authentication information (for SSH proxy authentication)
857 @type sshproxy_look_for_keys: C{bool}
858 @param sshproxy_allow_agent: set to C{True} to enable connecting to a local SSH agent
859 for acquiring authentication information (for SSH proxy authentication)
860 @type sshproxy_allow_agent: C{bool}
861 @param sshproxy_tunnel: the SSH proxy tunneling parameters, format is: <local-address>:<local-port>:<remote-address>:<remote-port>
862 @type sshproxy_tunnel: C{str}
863
864 @return: C{True} if an authenticated SSH transport could be retrieved by this method
865 @rtype: C{bool}
866
867 @raise BadHostKeyException: if the server's host key could not be
868 verified
869 @raise AuthenticationException: if authentication failed
870 @raise SSHException: if there was any other error connecting or
871 establishing an SSH session
872 @raise socket.error: if a socket error occurred while connecting
873 @raise X2GoSSHProxyException: any SSH proxy exception is passed through while establishing the SSH proxy connection and tunneling setup
874 @raise X2GoSSHAuthenticationException: any SSH proxy authentication exception is passed through while establishing the SSH proxy connection and tunneling setup
875 @raise X2GoRemoteHomeException: if the remote home directory does not exist or is not accessible
876 @raise X2GoControlSessionException: if the remote peer has died event unexpectedly
877
878 """
879 _fake_hostname = None
880
881 if hostname and type(hostname) not in (types.UnicodeType, types.StringType):
882 hostname = [hostname]
883 if hostname and type(hostname) is types.ListType:
884 hostname = random.choice(hostname)
885
886 if not username:
887 self.logger('no username specified, cannot connect without username', loglevel=log.loglevel_ERROR)
888 raise paramiko.AuthenticationException('no username specified, cannot connect without username')
889
890 if type(password) not in (types.StringType, types.UnicodeType):
891 password = ''
892 if type(sshproxy_password) not in (types.StringType, types.UnicodeType):
893 sshproxy_password = ''
894
895 if unique_hostkey_aliases is None:
896 unique_hostkey_aliases = self.unique_hostkey_aliases
897
898
899 if unique_hostkey_aliases:
900 if port != 22: _fake_hostname = "[%s]:%s" % (hostname, port)
901 else: _fake_hostname = hostname
902
903 if add_to_known_hosts is None:
904 add_to_known_hosts = self.add_to_known_hosts
905
906 if forward_sshagent is None:
907 forward_sshagent = self.forward_sshagent
908
909 if look_for_keys:
910 key_filename = None
911 pkey = None
912
913 _twofactorauth = False
914 if password and (passphrase is None) and not force_password_auth: passphrase = password
915
916 if use_sshproxy and sshproxy_host and sshproxy_user:
917 try:
918 if not sshproxy_tunnel:
919 sshproxy_tunnel = "localhost:44444:%s:%s" % (hostname, port)
920 self.sshproxy_session = sshproxy.X2GoSSHProxy(known_hosts=self.known_hosts,
921 add_to_known_hosts=add_to_known_hosts,
922 sshproxy_host=sshproxy_host,
923 sshproxy_port=sshproxy_port,
924 sshproxy_user=sshproxy_user,
925 sshproxy_password=sshproxy_password,
926 sshproxy_passphrase=sshproxy_passphrase,
927 sshproxy_force_password_auth=sshproxy_force_password_auth,
928 sshproxy_key_filename=sshproxy_key_filename,
929 sshproxy_pkey=sshproxy_pkey,
930 sshproxy_look_for_keys=sshproxy_look_for_keys,
931 sshproxy_allow_agent=sshproxy_allow_agent,
932 sshproxy_tunnel=sshproxy_tunnel,
933 session_instance=session_instance,
934 logger=self.logger,
935 )
936 hostname = self.sshproxy_session.get_local_proxy_host()
937 port = self.sshproxy_session.get_local_proxy_port()
938 _fake_hostname = self.sshproxy_session.get_remote_host()
939 _fake_port = self.sshproxy_session.get_remote_port()
940 if _fake_port != 22:
941 _fake_hostname = "[%s]:%s" % (_fake_hostname, _fake_port)
942
943 except:
944 if self.sshproxy_session:
945 self.sshproxy_session.stop_thread()
946 self.sshproxy_session = None
947 raise
948
949 if self.sshproxy_session is not None:
950 self.sshproxy_session.start()
951
952
953
954 gevent.sleep(.1)
955 port = self.sshproxy_session.get_local_proxy_port()
956
957 if not add_to_known_hosts and session_instance:
958 self.set_missing_host_key_policy(checkhosts.X2GoInteractiveAddPolicy(caller=self, session_instance=session_instance, fake_hostname=_fake_hostname))
959
960 if add_to_known_hosts:
961 self.set_missing_host_key_policy(checkhosts.X2GoAutoAddPolicy(caller=self, session_instance=session_instance, fake_hostname=_fake_hostname))
962
963
964 hostname = hostname.strip()
965
966 self.logger('connecting to [%s]:%s' % (hostname, port), loglevel=log.loglevel_NOTICE)
967
968 self.load_session_host_keys()
969
970 _hostname = hostname
971
972 if _hostname in ('localhost', 'localhost.localdomain'):
973 _hostname = '127.0.0.1'
974
975
976 if forward_sshagent is not None:
977 self.forward_sshagent = forward_sshagent
978
979 if timeout and self.low_latency:
980 timeout = timeout * 2
981
982 if key_filename and "~" in key_filename:
983 key_filename = os.path.expanduser(key_filename)
984
985 if key_filename or pkey or look_for_keys or allow_agent or (password and force_password_auth):
986 try:
987 if password and force_password_auth:
988 self.logger('trying password based SSH authentication with server', loglevel=log.loglevel_DEBUG)
989 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, pkey=None,
990 key_filename=None, timeout=timeout, allow_agent=False,
991 look_for_keys=False)
992 elif (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey:
993 self.logger('trying SSH pub/priv key authentication with server', loglevel=log.loglevel_DEBUG)
994 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, pkey=pkey,
995 key_filename=key_filename, timeout=timeout, allow_agent=False,
996 look_for_keys=False)
997 else:
998 self.logger('trying SSH key discovery or agent authentication with server', loglevel=log.loglevel_DEBUG)
999 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, pkey=None,
1000 key_filename=None, timeout=timeout, allow_agent=allow_agent,
1001 look_for_keys=look_for_keys)
1002
1003 except (paramiko.PasswordRequiredException, paramiko.SSHException), e:
1004 self.close()
1005 if type(e) == paramiko.SSHException and str(e).startswith('Two-factor authentication requires a password'):
1006 self.logger('X2Go Server requests two-factor authentication', loglevel=log.loglevel_NOTICE)
1007 _twofactorauth = True
1008 if passphrase is not None:
1009 self.logger('unlock SSH private key file with provided password', loglevel=log.loglevel_INFO)
1010 try:
1011 if not password: password = None
1012 if (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey:
1013 self.logger('re-trying SSH pub/priv key authentication with server', loglevel=log.loglevel_DEBUG)
1014 try:
1015 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, passphrase=passphrase, pkey=pkey,
1016 key_filename=key_filename, timeout=timeout, allow_agent=False,
1017 look_for_keys=False)
1018 except TypeError:
1019 if _twofactorauth and password and passphrase and password != passphrase:
1020 self.logger('your version of Paramiko/SSH does not support authentication workflows which require SSH key decryption in combination with two-factor authentication', loglevel=log.loglevel_WARN)
1021 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, pkey=pkey,
1022 key_filename=key_filename, timeout=timeout, allow_agent=False,
1023 look_for_keys=False)
1024 else:
1025 self.logger('re-trying SSH key discovery now with passphrase for unlocking the key(s)', loglevel=log.loglevel_DEBUG)
1026 try:
1027 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, passphrase=passphrase, pkey=None,
1028 key_filename=None, timeout=timeout, allow_agent=allow_agent,
1029 look_for_keys=look_for_keys)
1030 except TypeError:
1031 if _twofactorauth and password and passphrase and password != passphrase:
1032 self.logger('your version of Paramiko/SSH does not support authentication workflows which require SSH key decryption in combination with two-factor authentication', loglevel=log.loglevel_WARN)
1033 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, pkey=None,
1034 key_filename=None, timeout=timeout, allow_agent=allow_agent,
1035 look_for_keys=look_for_keys)
1036
1037 except paramiko.AuthenticationException, auth_e:
1038
1039 raise paramiko.AuthenticationException(str(auth_e))
1040
1041 except paramiko.SSHException, auth_e:
1042 if str(auth_e) == 'No authentication methods available':
1043 raise paramiko.AuthenticationException('Interactive password authentication required!')
1044 else:
1045 self.close()
1046 if self.sshproxy_session:
1047 self.sshproxy_session.stop_thread()
1048 raise auth_e
1049
1050 else:
1051 self.close()
1052 if self.sshproxy_session:
1053 self.sshproxy_session.stop_thread()
1054 raise e
1055
1056 except paramiko.AuthenticationException, e:
1057 self.close()
1058 if password:
1059 self.logger('next auth mechanism we\'ll try is password authentication', loglevel=log.loglevel_DEBUG)
1060 try:
1061 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password,
1062 key_filename=None, pkey=None, timeout=timeout, allow_agent=False, look_for_keys=False)
1063 except:
1064 self.close()
1065 if self.sshproxy_session:
1066 self.sshproxy_session.stop_thread()
1067 raise
1068 else:
1069 self.close()
1070 if self.sshproxy_session:
1071 self.sshproxy_session.stop_thread()
1072 raise e
1073
1074 except paramiko.SSHException, e:
1075 if str(e) == 'No authentication methods available':
1076 raise paramiko.AuthenticationException('Interactive password authentication required!')
1077 else:
1078 self.close()
1079 if self.sshproxy_session:
1080 self.sshproxy_session.stop_thread()
1081 raise e
1082
1083 except:
1084 self.close()
1085 if self.sshproxy_session:
1086 self.sshproxy_session.stop_thread()
1087 raise
1088
1089
1090 else:
1091
1092 if not password:
1093 password = "".join([random.choice(string.letters+string.digits) for x in range(1, 20)])
1094 self.logger('performing SSH password authentication with server', loglevel=log.loglevel_DEBUG)
1095
1096 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password,
1097 timeout=timeout, allow_agent=False, look_for_keys=False)
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109 self.set_missing_host_key_policy(paramiko.RejectPolicy())
1110
1111 self.hostname = hostname
1112 self.port = port
1113
1114
1115 ssh_transport = self.get_transport()
1116 ssh_transport.reverse_tunnels = {}
1117
1118
1119 ssh_transport._x2go_session_marker = True
1120 try:
1121 self._session_password = base64.b64encode(password)
1122 except TypeError:
1123 self._session_password = None
1124
1125 if ssh_transport is not None:
1126
1127
1128 if x2go._paramiko.PARAMIKO_FEATURE['use-compression']:
1129 ssh_transport.use_compression(compress=False)
1130
1131 ssh_transport.set_keepalive(5)
1132
1133 self.session_died = False
1134 self.query_server_features(force=True)
1135 if self.forward_sshagent:
1136 if x2go._paramiko.PARAMIKO_FEATURE['forward-ssh-agent']:
1137 try:
1138 self.agent_chan = ssh_transport.open_session()
1139 self.agent_handler = paramiko.agent.AgentRequestHandler(self.agent_chan)
1140 self.logger('Requesting SSH agent forwarding for control session of connected session profile %s' % self.profile_name, loglevel=log.loglevel_INFO)
1141 except EOFError, e:
1142
1143 self.session_died = True
1144 raise x2go_exceptions.X2GoControlSessionException('The SSH connection was dropped while setting up SSH agent forwarding socket.')
1145 else:
1146 self.logger('SSH agent forwarding is not available in the Paramiko version used with this instance of Python X2Go', loglevel=log.loglevel_WARN)
1147
1148 else:
1149 self.close()
1150 if self.sshproxy_session:
1151 self.sshproxy_session.stop_thread()
1152
1153 self._remote_home = None
1154 if not self.home_exists():
1155 self.close()
1156 if self.sshproxy_session:
1157 self.sshproxy_session.stop_thread()
1158 raise x2go_exceptions.X2GoRemoteHomeException('remote home directory does not exist')
1159
1160 return (self.get_transport() is not None)
1161
1163 """\
1164 Drop an associated terminal session.
1165
1166 @param terminal_session: the terminal session object to remove from the list of associated terminals
1167 @type terminal_session: C{X2GoTerminalSession*}
1168
1169 """
1170 for t_name in self.associated_terminals.keys():
1171 if self.associated_terminals[t_name] == terminal_session:
1172 del self.associated_terminals[t_name]
1173 if self.terminated_terminals.has_key(t_name):
1174 del self.terminated_terminals[t_name]
1175
1177 """\
1178 Disconnect this control session from the remote server.
1179
1180 @return: report success or failure after having disconnected
1181 @rtype: C{bool}
1182
1183 """
1184 if self.associated_terminals:
1185 t_names = self.associated_terminals.keys()
1186 for t_obj in self.associated_terminals.values():
1187 try:
1188 if not self.session_died:
1189 t_obj.suspend()
1190 except x2go_exceptions.X2GoTerminalSessionException:
1191 pass
1192 except x2go_exceptions.X2GoControlSessionException:
1193 self.session_died
1194 t_obj.__del__()
1195 for t_name in t_names:
1196 try:
1197 del self.associated_terminals[t_name]
1198 except KeyError:
1199 pass
1200
1201 self._remote_home = None
1202 self._remote_group = {}
1203
1204 self._session_auth_rsakey = None
1205
1206
1207 self._transport_lock.release()
1208
1209
1210 if self.agent_handler is not None:
1211 self.agent_handler.close()
1212
1213 if self.agent_chan is not None:
1214 try:
1215 self.agent_chan.close()
1216 except EOFError:
1217 pass
1218
1219 retval = False
1220 try:
1221 if self.get_transport() is not None:
1222 retval = self.get_transport().is_active()
1223 try:
1224 self.close()
1225 except IOError:
1226 pass
1227 except AttributeError:
1228
1229
1230 pass
1231
1232
1233 if self.sshproxy_session is not None:
1234 self.sshproxy_session.stop_thread()
1235
1236 return retval
1237
1239 """\
1240 Test if the remote home directory exists.
1241
1242 @return: C{True} if the home directory exists, C{False} otherwise
1243 @rtype: C{bool}
1244
1245 """
1246 (_stdin, _stdout, _stderr) = self._x2go_exec_command('stat -tL "%s"' % self._x2go_remote_home, loglevel=log.loglevel_DEBUG)
1247 if _stdout.read():
1248 return True
1249 return False
1250
1251
1253 """\
1254 Test if the connection to the remote X2Go server is still alive.
1255
1256 @return: C{True} if the connection is still alive, C{False} otherwise
1257 @rtype: C{bool}
1258
1259 """
1260 try:
1261 if self._x2go_exec_command('echo', loglevel=log.loglevel_DEBUG):
1262 return True
1263 except x2go_exceptions.X2GoControlSessionException:
1264 self.session_died = True
1265 self.disconnect()
1266 return False
1267
1269 """\
1270 Test if the connection to the remote X2Go server died on the way.
1271
1272 @return: C{True} if the connection has died, C{False} otherwise
1273 @rtype: C{bool}
1274
1275 """
1276 return self.session_died
1277
1279 """\
1280 Retrieve the menu tree of published applications from the remote X2Go server.
1281
1282 The C{raw} option lets this method return a C{list} of C{dict} elements. Each C{dict} elements has a
1283 C{desktop} key containing a shortened version of the text output of a .desktop file and an C{icon} key
1284 which contains the desktop base64-encoded icon data.
1285
1286 The {very_raw} lets this method return the output of the C{x2gogetapps} script as is.
1287
1288 @param lang: locale/language identifier
1289 @type lang: C{str}
1290 @param refresh: force reload of the menu tree from X2Go server
1291 @type refresh: C{bool}
1292 @param raw: retrieve a raw output of the server list of published applications
1293 @type raw: C{bool}
1294 @param very_raw: retrieve a very raw output of the server list of published applications
1295 @type very_raw: C{bool}
1296
1297 @return: an i18n capable menu tree packed as a Python dictionary
1298 @rtype: C{list}
1299
1300 """
1301 self._already_querying_published_applications.acquire()
1302
1303 if defaults.X2GOCLIENT_OS != 'Windows' and lang is None:
1304 lang = locale.getdefaultlocale()[0]
1305 elif lang is None:
1306 lang = 'en'
1307
1308 if 'X2GO_PUBLISHED_APPLICATIONS' in self.get_server_features():
1309 if self._published_applications_menu is {} or \
1310 not self._published_applications_menu.has_key(lang) or \
1311 raw or very_raw or refresh or \
1312 (self.published_applications_no_submenus != max_no_submenus):
1313
1314 self.published_applications_no_submenus = max_no_submenus
1315
1316
1317
1318 self.logger('querying server (%s) for list of published applications' % self.profile_name, loglevel=log.loglevel_NOTICE)
1319 (stdin, stdout, stderr) = self._x2go_exec_command('which x2gogetapps >/dev/null && x2gogetapps')
1320 _raw_output = stdout.read()
1321
1322 if very_raw:
1323 self.logger('published applications query for %s finished, return very raw output' % self.profile_name, loglevel=log.loglevel_NOTICE)
1324 self._already_querying_published_applications.release()
1325 return _raw_output
1326
1327
1328
1329 _raw_menu_items = _raw_output.split('</desktop>\n')
1330 _raw_menu_items = [ i.replace('<desktop>\n', '') for i in _raw_menu_items ]
1331 _menu = []
1332 for _raw_menu_item in _raw_menu_items:
1333 if '<icon>\n' in _raw_menu_item and '</icon>' in _raw_menu_item:
1334 _menu_item = _raw_menu_item.split('<icon>\n')[0] + _raw_menu_item.split('</icon>\n')[1]
1335 _icon_base64 = _raw_menu_item.split('<icon>\n')[1].split('</icon>\n')[0]
1336 else:
1337 _menu_item = _raw_menu_item
1338 _icon_base64 = None
1339 if _menu_item:
1340 _menu.append({ 'desktop': _menu_item, 'icon': _icon_base64, })
1341 _menu_item = None
1342 _icon_base64 = None
1343
1344 if raw:
1345 self.logger('published applications query for %s finished, returning raw output' % self.profile_name, loglevel=log.loglevel_NOTICE)
1346 self._already_querying_published_applications.release()
1347 return _menu
1348
1349 if len(_menu) > max_no_submenus >= 0:
1350 _render_submenus = True
1351 else:
1352 _render_submenus = False
1353
1354
1355
1356 _category_map = {
1357 lang: {
1358 'Multimedia': [],
1359 'Development': [],
1360 'Education': [],
1361 'Games': [],
1362 'Graphics': [],
1363 'Internet': [],
1364 'Office': [],
1365 'System': [],
1366 'Utilities': [],
1367 'Other Applications': [],
1368 'TOP': [],
1369 }
1370 }
1371 _empty_menus = _category_map[lang].keys()
1372
1373 for item in _menu:
1374
1375 _menu_entry_name = ''
1376 _menu_entry_fallback_name = ''
1377 _menu_entry_comment = ''
1378 _menu_entry_fallback_comment = ''
1379 _menu_entry_exec = ''
1380 _menu_entry_cat = ''
1381 _menu_entry_shell = False
1382
1383 lang_regio = lang
1384 lang_only = lang_regio.split('_')[0]
1385
1386 for line in item['desktop'].split('\n'):
1387 if re.match('^Name\[%s\]=.*' % lang_regio, line) or re.match('Name\[%s\]=.*' % lang_only, line):
1388 _menu_entry_name = line.split("=")[1].strip()
1389 elif re.match('^Name=.*', line):
1390 _menu_entry_fallback_name = line.split("=")[1].strip()
1391 elif re.match('^Comment\[%s\]=.*' % lang_regio, line) or re.match('Comment\[%s\]=.*' % lang_only, line):
1392 _menu_entry_comment = line.split("=")[1].strip()
1393 elif re.match('^Comment=.*', line):
1394 _menu_entry_fallback_comment = line.split("=")[1].strip()
1395 elif re.match('^Exec=.*', line):
1396 _menu_entry_exec = line.split("=")[1].strip()
1397 elif re.match('^Terminal=.*(t|T)(r|R)(u|U)(e|E).*', line):
1398 _menu_entry_shell = True
1399 elif re.match('^Categories=.*', line):
1400 if 'X2Go-Top' in line:
1401 _menu_entry_cat = 'TOP'
1402 elif 'Audio' in line or 'Video' in line:
1403 _menu_entry_cat = 'Multimedia'
1404 elif 'Development' in line:
1405 _menu_entry_cat = 'Development'
1406 elif 'Education' in line:
1407 _menu_entry_cat = 'Education'
1408 elif 'Game' in line:
1409 _menu_entry_cat = 'Games'
1410 elif 'Graphics' in line:
1411 _menu_entry_cat = 'Graphics'
1412 elif 'Network' in line:
1413 _menu_entry_cat = 'Internet'
1414 elif 'Office' in line:
1415 _menu_entry_cat = 'Office'
1416 elif 'Settings' in line:
1417 continue
1418 elif 'System' in line:
1419 _menu_entry_cat = 'System'
1420 elif 'Utility' in line:
1421 _menu_entry_cat = 'Utilities'
1422 else:
1423 _menu_entry_cat = 'Other Applications'
1424
1425 if not _menu_entry_exec:
1426 continue
1427 else:
1428
1429 _menu_entry_exec = _menu_entry_exec.replace('%f', '').replace('%F','').replace('%u','').replace('%U','')
1430 if _menu_entry_shell:
1431 _menu_entry_exec = "x-terminal-emulator -e '%s'" % _menu_entry_exec
1432
1433 if not _menu_entry_cat:
1434 _menu_entry_cat = 'Other Applications'
1435
1436 if not _render_submenus:
1437 _menu_entry_cat = 'TOP'
1438
1439 if _menu_entry_cat in _empty_menus:
1440 _empty_menus.remove(_menu_entry_cat)
1441
1442 if not _menu_entry_name: _menu_entry_name = _menu_entry_fallback_name
1443 if not _menu_entry_comment: _menu_entry_comment = _menu_entry_fallback_comment
1444 if not _menu_entry_comment: _menu_entry_comment = _menu_entry_name
1445
1446 _menu_entry_icon = item['icon']
1447
1448 _category_map[lang][_menu_entry_cat].append(
1449 {
1450 'name': _menu_entry_name,
1451 'comment': _menu_entry_comment,
1452 'exec': _menu_entry_exec,
1453 'icon': _menu_entry_icon,
1454 }
1455 )
1456
1457 for _cat in _empty_menus:
1458 del _category_map[lang][_cat]
1459
1460 for _cat in _category_map[lang].keys():
1461 _sorted = sorted(_category_map[lang][_cat], key=lambda k: k['name'])
1462 _category_map[lang][_cat] = _sorted
1463
1464 self._published_applications_menu.update(_category_map)
1465 self.logger('published applications query for %s finished, return menu tree' % self.profile_name, loglevel=log.loglevel_NOTICE)
1466
1467 else:
1468
1469 pass
1470
1471 self._already_querying_published_applications.release()
1472 return self._published_applications_menu
1473
1474 - def start(self, **kwargs):
1475 """\
1476 Start a new X2Go session.
1477
1478 The L{X2GoControlSession.start()} method accepts any parameter
1479 that can be passed to any of the C{X2GoTerminalSession} backend class
1480 constructors.
1481
1482 @param kwargs: parameters that get passed through to the control session's
1483 L{resume()} method, only the C{session_name} parameter will get removed
1484 before pass-through
1485 @type kwargs: C{dict}
1486
1487 @return: return value of the cascaded L{resume()} method, denoting the success or failure
1488 of the session startup
1489 @rtype: C{bool}
1490
1491 """
1492 if 'session_name' in kwargs.keys():
1493 del kwargs['session_name']
1494 return self.resume(**kwargs)
1495
1496 - def resume(self, session_name=None, session_instance=None, session_list=None, **kwargs):
1497 """\
1498 Resume a running/suspended X2Go session.
1499
1500 The L{X2GoControlSession.resume()} method accepts any parameter
1501 that can be passed to any of the C{X2GoTerminalSession*} backend class constructors.
1502
1503 @return: True if the session could be successfully resumed
1504 @rtype: C{bool}
1505
1506 @raise X2GoUserException: if the remote user is not allowed to launch/resume X2Go sessions.
1507
1508 """
1509 if self.get_transport() is not None:
1510
1511 if not self.is_x2gouser(self.get_transport().get_username()):
1512 raise x2go_exceptions.X2GoUserException('remote user %s is not allowed to run X2Go commands' % self.get_transport().get_username())
1513
1514 session_info = None
1515 try:
1516 if session_name is not None:
1517 if session_list:
1518 session_info = session_list[session_name]
1519 else:
1520 session_info = self.list_sessions()[session_name]
1521 except KeyError:
1522 _success = False
1523
1524 _terminal = self._terminal_backend(self,
1525 profile_name=self.profile_name,
1526 session_info=session_info,
1527 info_backend=self._info_backend,
1528 list_backend=self._list_backend,
1529 proxy_backend=self._proxy_backend,
1530 client_rootdir=self.client_rootdir,
1531 session_instance=session_instance,
1532 sessions_rootdir=self.sessions_rootdir,
1533 **kwargs)
1534
1535 _success = False
1536 try:
1537 if session_name is not None:
1538 _success = _terminal.resume()
1539 else:
1540 _success = _terminal.start()
1541 except x2go_exceptions.X2GoTerminalSessionException:
1542 _success = False
1543
1544 if _success:
1545 while not _terminal.ok():
1546 gevent.sleep(.2)
1547
1548 if _terminal.ok():
1549 self.associated_terminals[_terminal.get_session_name()] = _terminal
1550 self.get_transport().reverse_tunnels[_terminal.get_session_name()] = {
1551 'sshfs': (0, None),
1552 'snd': (0, None),
1553 }
1554
1555 return _terminal or None
1556
1557 return None
1558
1559 - def share_desktop(self, desktop=None, user=None, display=None, share_mode=0, **kwargs):
1560 """\
1561 Share another already running desktop session. Desktop sharing can be run
1562 in two different modes: view-only and full-access mode.
1563
1564 @param desktop: desktop ID of a sharable desktop in format C{<user>@<display>}
1565 @type desktop: C{str}
1566 @param user: user name and display number can be given separately, here give the
1567 name of the user who wants to share a session with you
1568 @type user: C{str}
1569 @param display: user name and display number can be given separately, here give the
1570 number of the display that a user allows you to be shared with
1571 @type display: C{str}
1572 @param share_mode: desktop sharing mode, 0 stands for VIEW-ONLY, 1 for FULL-ACCESS mode
1573 @type share_mode: C{int}
1574
1575 @return: True if the session could be successfully shared
1576 @rtype: C{bool}
1577
1578 @raise X2GoDesktopSharingException: if C{username} and C{dislpay} do not relate to a
1579 sharable desktop session
1580
1581 """
1582 if desktop:
1583 user = desktop.split('@')[0]
1584 display = desktop.split('@')[1]
1585 if not (user and display):
1586 raise x2go_exceptions.X2GoDesktopSharingException('Need user name and display number of shared desktop.')
1587
1588 cmd = '%sXSHAD%sXSHAD%s' % (share_mode, user, display)
1589
1590 kwargs['cmd'] = cmd
1591 kwargs['session_type'] = 'shared'
1592
1593 return self.start(**kwargs)
1594
1596 """\
1597 List all desktop-like sessions of current user (or of users that have
1598 granted desktop sharing) on the connected server.
1599
1600 @param raw: if C{True}, the raw output of the server-side X2Go command
1601 C{x2golistdesktops} is returned.
1602 @type raw: C{bool}
1603
1604 @return: a list of X2Go desktops available for sharing
1605 @rtype: C{list}
1606
1607 @raise X2GoTimeOutException: on command execution timeouts, with the server-side C{x2golistdesktops}
1608 command this can sometimes happen. Make sure you ignore these time-outs and to try again
1609
1610 """
1611 if raw:
1612 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistdesktops")
1613 return stdout.read(), stderr.read()
1614
1615 else:
1616
1617
1618
1619
1620 if self.low_latency:
1621 maxwait = maxwait * 2
1622
1623 timeout = gevent.Timeout(maxwait)
1624 timeout.start()
1625 try:
1626 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistdesktops")
1627 _stdout_read = stdout.read()
1628 _listdesktops = _stdout_read.split('\n')
1629 except gevent.timeout.Timeout:
1630
1631
1632
1633 raise x2go_exceptions.X2GoTimeOutException('x2golistdesktop command timed out')
1634 finally:
1635 timeout.cancel()
1636
1637 return _listdesktops
1638
1639 - def list_mounts(self, session_name, raw=False, maxwait=20):
1640 """\
1641 List all mounts for a given session of the current user on the connected server.
1642
1643 @param session_name: name of a session to query a list of mounts for
1644 @type session_name: C{str}
1645 @param raw: if C{True}, the raw output of the server-side X2Go command
1646 C{x2golistmounts} is returned.
1647 @type raw: C{bool}
1648 @param maxwait: stop processing C{x2golistmounts} after C{<maxwait>} seconds
1649 @type maxwait: C{int}
1650
1651 @return: a list of client-side mounts for X2Go session C{<session_name>} on the server
1652 @rtype: C{list}
1653
1654 @raise X2GoTimeOutException: on command execution timeouts, queries with the server-side
1655 C{x2golistmounts} query should normally be processed quickly, a time-out may hint that the
1656 control session has lost its connection to the X2Go server
1657
1658 """
1659 if raw:
1660 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistmounts %s" % session_name)
1661 return stdout.read(), stderr.read()
1662
1663 else:
1664
1665 if self.low_latency:
1666 maxwait = maxwait * 2
1667
1668
1669
1670 timeout = gevent.Timeout(maxwait)
1671 timeout.start()
1672 try:
1673 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistmounts %s" % session_name)
1674 _stdout_read = stdout.read()
1675 _listmounts = {session_name: [ line for line in _stdout_read.split('\n') if line ] }
1676 except gevent.timeout.Timeout:
1677
1678
1679 raise x2go_exceptions.X2GoTimeOutException('x2golistmounts command timed out')
1680 finally:
1681 timeout.cancel()
1682
1683 return _listmounts
1684
1686 """\
1687 List all sessions of current user on the connected server.
1688
1689 @param raw: if C{True}, the raw output of the server-side X2Go command
1690 C{x2golistsessions} is returned.
1691 @type raw: C{bool}
1692
1693 @return: normally an instance of a C{X2GoServerSessionList*} backend is returned. However,
1694 if the raw argument is set, the plain text output of the server-side C{x2golistsessions}
1695 command is returned
1696 @rtype: C{X2GoServerSessionList} instance or str
1697
1698 @raise X2GoControlSessionException: on command execution timeouts, if this happens the control session will
1699 be interpreted as disconnected due to connection loss
1700 """
1701 if raw:
1702 if 'X2GO_LIST_SHADOWSESSIONS' in self._x2go_server_features:
1703 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && { x2golistsessions; x2golistshadowsessions; }")
1704 else:
1705 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistsessions")
1706 return stdout.read(), stderr.read()
1707
1708 else:
1709
1710
1711
1712 _listsessions = {}
1713 _success = False
1714 _count = 0
1715 _maxwait = 20
1716
1717
1718
1719 while not _success and _count < _maxwait:
1720 _count += 1
1721 try:
1722 if 'X2GO_LIST_SHADOWSESSIONS' in self._x2go_server_features:
1723 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && { x2golistsessions; x2golistshadowsessions; }")
1724 else:
1725 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistsessions")
1726 _stdout_read = stdout.read()
1727 _listsessions = self._list_backend(_stdout_read, info_backend=self._info_backend).sessions
1728 _success = True
1729 except KeyError:
1730 gevent.sleep(1)
1731 except IndexError:
1732 gevent.sleep(1)
1733 except ValueError:
1734 gevent.sleep(1)
1735
1736 if _count >= _maxwait:
1737 self.session_died = True
1738 self.disconnect()
1739 raise x2go_exceptions.X2GoControlSessionException('x2golistsessions command failed after we have tried 20 times')
1740
1741
1742 if _success and not self.session_died:
1743 for _session_name, _terminal in self.associated_terminals.items():
1744 if _session_name in _listsessions.keys():
1745
1746 if hasattr(self.associated_terminals[_session_name], 'session_info') and not self.associated_terminals[_session_name].is_session_info_protected():
1747 self.associated_terminals[_session_name].session_info.update(_listsessions[_session_name])
1748 else:
1749 self.associated_terminals[_session_name].__del__()
1750 try: del self.associated_terminals[_session_name]
1751 except KeyError: pass
1752 self.terminated_terminals.append(_session_name)
1753 if _terminal.is_suspended():
1754 self.associated_terminals[_session_name].__del__()
1755 try: del self.associated_terminals[_session_name]
1756 except KeyError: pass
1757
1758 return _listsessions
1759
1760 - def clean_sessions(self, destroy_terminals=True, published_applications=False):
1761 """\
1762 Find X2Go terminals that have previously been started by the
1763 connected user on the remote X2Go server and terminate them.
1764
1765 @param destroy_terminals: destroy the terminal session instances after cleanup
1766 @type destroy_terminals: C{bool}
1767 @param published_applications: also clean up published applications providing sessions
1768 @type published_applications: C{bool}
1769
1770 """
1771 session_list = self.list_sessions()
1772 if published_applications:
1773 session_names = session_list.keys()
1774 else:
1775 session_names = [ _sn for _sn in session_list.keys() if not session_list[_sn].is_published_applications_provider() ]
1776 for session_name in session_names:
1777 if self.associated_terminals.has_key(session_name):
1778 self.associated_terminals[session_name].terminate()
1779 if destroy_terminals:
1780 if self.associated_terminals[session_name] is not None:
1781 self.associated_terminals[session_name].__del__()
1782 try: del self.associated_terminals[session_name]
1783 except KeyError: pass
1784 else:
1785 self.terminate(session_name=session_name)
1786
1788 """\
1789 Returns C{True} if this control session is connected to the remote server (that
1790 is: if it has a valid Paramiko/SSH transport object).
1791
1792 @return: X2Go session connected?
1793 @rtype: C{bool}
1794
1795 """
1796 return self.get_transport() is not None and self.get_transport().is_authenticated()
1797
1799 """\
1800 Returns C{True} if the given X2Go session is in running state,
1801 C{False} else.
1802
1803 @param session_name: X2Go name of the session to be queried
1804 @type session_name: C{str}
1805
1806 @return: X2Go session running? If C{<session_name>} is not listable by the L{list_sessions()} method then C{None} is returned
1807 @rtype: C{bool} or C{None}
1808
1809 """
1810 session_infos = self.list_sessions()
1811 if session_name in session_infos.keys():
1812 return session_infos[session_name].is_running()
1813 return None
1814
1816 """\
1817 Returns C{True} if the given X2Go session is in suspended state,
1818 C{False} else.
1819
1820 @return: X2Go session suspended? If C{<session_name>} is not listable by the L{list_sessions()} method then C{None} is returned
1821 @rtype: C{bool} or C{None}
1822
1823 """
1824 session_infos = self.list_sessions()
1825 if session_name in session_infos.keys():
1826 return session_infos[session_name].is_suspended()
1827 return None
1828
1830 """\
1831 Returns C{True} if the X2Go session with name C{<session_name>} has been seen
1832 by this control session and--in the meantime--has been terminated.
1833
1834 If C{<session_name>} has not been seen, yet, the method will return C{None}.
1835
1836 @return: X2Go session has terminated?
1837 @rtype: C{bool} or C{None}
1838
1839 """
1840 session_infos = self.list_sessions()
1841 if session_name in self.terminated_terminals:
1842 return True
1843 if session_name not in session_infos.keys() and session_name in self.associated_terminals.keys():
1844
1845 self.terminate(session_name)
1846 return True
1847 if self.is_suspended(session_name) or self.is_running(session_name):
1848 return False
1849
1850 return None
1851
1853 """\
1854 Suspend X2Go session with name C{<session_name>} on the connected
1855 server.
1856
1857 @param session_name: X2Go name of the session to be suspended
1858 @type session_name: C{str}
1859
1860 @return: C{True} if the session could be successfully suspended
1861 @rtype: C{bool}
1862
1863 """
1864 _ret = False
1865 _session_names = [ t.get_session_name() for t in self.associated_terminals.values() ]
1866 if session_name in _session_names:
1867
1868 self.logger('suspending associated terminal session: %s' % session_name, loglevel=log.loglevel_DEBUG)
1869 (stdin, stdout, stderr) = self._x2go_exec_command("x2gosuspend-session %s" % session_name, loglevel=log.loglevel_DEBUG)
1870 stdout.read()
1871 stderr.read()
1872 if self.associated_terminals.has_key(session_name):
1873 if self.associated_terminals[session_name] is not None:
1874 self.associated_terminals[session_name].__del__()
1875 try: del self.associated_terminals[session_name]
1876 except KeyError: pass
1877 _ret = True
1878
1879 else:
1880
1881 self.logger('suspending non-associated terminal session: %s' % session_name, loglevel=log.loglevel_DEBUG)
1882 (stdin, stdout, stderr) = self._x2go_exec_command("x2gosuspend-session %s" % session_name, loglevel=log.loglevel_DEBUG)
1883 stdout.read()
1884 stderr.read()
1885 _ret = True
1886
1887 return _ret
1888
1889 - def terminate(self, session_name, destroy_terminals=True):
1890 """\
1891 Terminate X2Go session with name C{<session_name>} on the connected
1892 server.
1893
1894 @param session_name: X2Go name of the session to be terminated
1895 @type session_name: C{str}
1896
1897 @return: C{True} if the session could be successfully terminated
1898 @rtype: C{bool}
1899
1900 """
1901
1902 _ret = False
1903 if session_name in self.associated_terminals.keys():
1904
1905 self.logger('terminating associated session: %s' % session_name, loglevel=log.loglevel_DEBUG)
1906 (stdin, stdout, stderr) = self._x2go_exec_command("x2goterminate-session %s" % session_name, loglevel=log.loglevel_DEBUG)
1907 stdout.read()
1908 stderr.read()
1909
1910 if destroy_terminals:
1911 if self.associated_terminals[session_name] is not None:
1912 self.associated_terminals[session_name].__del__()
1913 try: del self.associated_terminals[session_name]
1914 except KeyError: pass
1915
1916 self.terminated_terminals.append(session_name)
1917 _ret = True
1918
1919 else:
1920
1921 self.logger('terminating non-associated session: %s' % session_name, loglevel=log.loglevel_DEBUG)
1922 (stdin, stdout, stderr) = self._x2go_exec_command("x2goterminate-session %s" % session_name, loglevel=log.loglevel_DEBUG)
1923 stdout.read()
1924 stderr.read()
1925 _ret = True
1926
1927 return _ret
1928