1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 """\
21 Python X2Go helper functions, constants etc.
22
23 """
24 __NAME__ = 'x2goutils-pylib'
25
26 import sys
27 import os
28 import locale
29 import re
30 import types
31 import copy
32 import socket
33 import gevent
34 import string
35 import subprocess
36 import distutils.version
37 import paramiko
38
39
40 from defaults import X2GOCLIENT_OS as _X2GOCLIENT_OS
41 from defaults import X2GO_SESSIONPROFILE_DEFAULTS as _X2GO_SESSIONPROFILE_DEFAULTS
42 from defaults import X2GO_MIMEBOX_ACTIONS as _X2GO_MIMEBOX_ACTIONS
43 from defaults import pack_methods_nx3
44
45 from defaults import BACKENDS as _BACKENDS
46
47 import x2go_exceptions
48
49 if _X2GOCLIENT_OS != 'Windows':
50 import Xlib
51 from defaults import X_DISPLAY as _X_DISPLAY
52
53 if _X2GOCLIENT_OS == 'Windows':
54 import win32gui
55 import win32print
56 import win32con
57
59
60 """\
61 Test if a given compression method is valid for NX3 Proxy.
62
63 @return: C{True} if C{method} is in the hard-coded list of NX3 compression methods.
64 @rtype: C{bool}
65
66 """
67 return method in pack_methods_nx3
68
69
71 """\
72 Return the X2Go session meta information as returned by the
73 C{x2golistsessions} server command for session C{session_name}.
74
75 @param session_name: name of a session
76 @type session_name: C{str}
77 @param stdout: raw output from the ,,x2golistsessions'' command, as list of strings
78 @type stdout: C{list}
79
80 @return: the output line that contains C{<session_name>}
81 @rtype: C{str} or C{None}
82
83 """
84 sessions = stdout.read().split("\n")
85 for line in sessions:
86
87 if not line:
88 continue
89 if session_name == line.split("|")[1]:
90 return line
91 return None
92
93
95 """\
96 Normalizes string, converts to lowercase, removes non-alpha characters,
97 converts spaces to hyphens and replaces round brackets by pointed brackets.
98
99 @param value: a string that shall be sluggified
100 @type value: C{str}
101
102 @return: the sluggified string
103 @rtype: C{str}
104
105 """
106 import unicodedata
107 value = unicodedata.normalize('NFKD', unicode(value)).encode('ascii', 'ignore')
108 value = re.sub('[^\w\s-]', '', value).strip().lower()
109 value = re.sub('[(]', '<', value).strip().lower()
110 value = re.sub('[)]', '>', value).strip().lower()
111 return value
112
114 """\
115 Generate a session profile ID as used in x2goclient's sessions config file.
116
117 @return: profile ID
118 @rtype: C{str}
119
120 """
121 import datetime
122 return datetime.datetime.utcnow().strftime('%Y%m%d%H%m%S%f')
123
124
126 """\
127 Check an ini file data structure passed on by a user app or class.
128
129 @param data_structure: an ini file date structure
130 @type data_structure: C{dict} of C{dict}s
131
132 @return: C{True} if C{data_structure} matches that of an ini file data structure
133 @rtype: C{bool}
134
135 """
136 if data_structure is None:
137 return False
138 if type(data_structure) is not types.DictType:
139 return False
140 for sub_dict in data_structure.values():
141 if type(sub_dict) is not types.DictType:
142 return False
143 return True
144
145
147 """\
148 Check the data structure of a default session profile passed by a user app.
149
150 @param data_structure: an ini file date structure
151 @type data_structure: C{dict} of C{dict}s
152
153 @return: C{True} if C{data_structure} matches that of an ini file data structure
154 @rtype: C{bool}
155
156 """
157 if data_structure is None:
158 return False
159 if type(data_structure) is not types.DictType:
160 return False
161 return True
162
163
165 """\
166 Convert session profile options as used in x2goclient's sessions file to
167 Python X2Go session parameters.
168
169 @param options: a dictionary of options, parameter names as in the X2Go ,,sessions'' file
170 @type options: C{dict}
171
172 @return: session options as used in C{X2GoSession} instances
173 @rtype: C{dict}
174
175 """
176 _params = copy.deepcopy(options)
177
178
179 _known_options = _X2GO_SESSIONPROFILE_DEFAULTS.keys()
180 for p in _params.keys():
181 if p not in _known_options:
182 del _params[p]
183
184 _rename_dict = {
185 'host': 'server',
186 'user': 'username',
187 'soundsystem': 'snd_system',
188 'sndport': 'snd_port',
189 'type': 'kbtype',
190 'layout': 'kblayout',
191 'variant': 'kbvariant',
192 'speed': 'link',
193 'sshport': 'port',
194 'useexports': 'allow_share_local_folders',
195 'restoreexports': 'restore_shared_local_folders',
196 'usemimebox': 'allow_mimebox',
197 'mimeboxextensions': 'mimebox_extensions',
198 'mimeboxaction': 'mimebox_action',
199 'print': 'printing',
200 'name': 'profile_name',
201 'key': 'key_filename',
202 'command': 'cmd',
203 'rdpserver': 'rdp_server',
204 'rdpoptions': 'rdp_options',
205 'xdmcpserver': 'xdmcp_server',
206 'useiconv': 'convert_encoding',
207 'iconvto': 'server_encoding',
208 'iconvfrom': 'client_encoding',
209 'usesshproxy': 'use_sshproxy',
210 'sshproxyhost': 'sshproxy_host',
211 'sshproxyport': 'sshproxy_port',
212 'sshproxyuser': 'sshproxy_user',
213 'sshproxykeyfile': 'sshproxy_key_filename',
214 'sessiontitle': 'session_title',
215 'setsessiontitle': 'set_session_title',
216 'published': 'published_applications',
217 'autostart': 'auto_start_or_resume',
218 'autoconnect': 'auto_connect',
219 'forwardsshagent': 'forward_sshagent',
220 'autologin': 'look_for_keys',
221 'sshproxyautologin': 'sshproxy_look_for_keys',
222 'uniquehostkeyaliases': 'unique_hostkey_aliases',
223 }
224 _speed_dict = {
225 '0': 'modem',
226 '1': 'isdn',
227 '2': 'adsl',
228 '3': 'wan',
229 '4': 'lan',
230 }
231
232 for opt, val in options.iteritems():
233
234
235 if opt in _rename_dict.keys():
236 del _params[opt]
237 opt = _rename_dict[opt]
238 if opt in _known_options:
239 _type = type(_known_options[opt])
240 _params[opt] = _type(val)
241 else:
242 _params[opt] = val
243
244
245 if opt == 'link':
246 val = str(val).lower()
247 if val in _speed_dict.keys():
248 val = _speed_dict[val]
249 val = val.lower()
250 _params['link'] = val
251
252
253 if opt in ('share_local_folders', 'mimebox_extensions'):
254 if type(val) is types.StringType:
255 if val:
256 _params[opt] = val.split(',')
257 else:
258 _params[opt] = []
259
260 if _params['cmd'] == "XFCE4": _params['cmd'] = "XFCE"
261 if _params['look_for_keys']:
262 _params['allow_agent'] = True
263 if _params['sshproxy_look_for_keys']:
264 _params['sshproxy_allow_agent'] = True
265
266
267 if _params['quality']:
268 _params['pack'] = '%s-%s' % (_params['pack'], _params['quality'])
269
270 del _params['quality']
271
272 del _params['fstunnel']
273
274 if _params.has_key('export'):
275
276 _export = _params['export']
277 del _params['export']
278 if type(_export) is types.DictType:
279
280
281 _params['share_local_folders'] = _export.keys()
282
283 else:
284
285
286
287
288 _export = _export.replace(",", ";")
289
290 _export = _export.strip().strip('"').strip().strip(';').strip()
291 _export_list = [ f for f in _export.split(';') if f ]
292
293 _params['share_local_folders'] = []
294 for _shared_folder in _export_list:
295
296 if not ":" in _shared_folder: _shared_folder = "%s:1" % _shared_folder
297 if _shared_folder.split(":")[-1] == "1":
298 _params['share_local_folders'].append(":".join(_shared_folder.split(":")[:-1]))
299
300 if options['fullscreen']:
301 _params['geometry'] = 'fullscreen'
302 elif options['maxdim']:
303 _params['geometry'] = 'maximize'
304 else:
305 _params['geometry'] = '%sx%s' % (options['width'], options['height'])
306 del _params['width']
307 del _params['height']
308 del _params['fullscreen']
309 del _params['maxdim']
310
311 if not options['sound']:
312 _params['snd_system'] = 'none'
313 del _params['sound']
314
315 if not options['rootless']:
316 _params['session_type'] = 'desktop'
317 else:
318 _params['session_type'] = 'application'
319 del _params['rootless']
320
321 if _params['mimebox_action'] not in _X2GO_MIMEBOX_ACTIONS.keys():
322 _params['mimebox_action'] = 'OPEN'
323
324 if not options['usekbd']:
325 _params['kbtype'] = 'null/null'
326 _params['kblayout'] = 'null'
327 _params['kbvariant'] = 'null'
328 del _params['usekbd']
329
330 if not _params['kbtype'].strip(): _params['kbtype'] = 'null/null'
331 if not _params['kblayout'].strip(): _params['kblayout'] = 'null'
332 if not _params['kbvariant'].strip(): _params['kbvariant'] = 'null'
333
334 if not options['setdpi']:
335 del _params['dpi']
336 del _params['setdpi']
337
338 if options['sshproxysameuser']:
339 _params['sshproxy_user'] = _params['username']
340 del _params['sshproxysameuser']
341 if options['sshproxysamepass']:
342 _params['sshproxy_reuse_authinfo'] = True
343 _params['sshproxy_key_filename'] = _params['key_filename']
344 del _params['sshproxysamepass']
345
346 if _params['use_sshproxy']:
347
348
349 if options.has_key('sshproxytunnel'):
350 if not options['sshproxytunnel'].startswith('DEPRECATED'):
351 _params['server'] = options['sshproxytunnel'].split(":")[-2]
352 _params['port'] = options['sshproxytunnel'].split(":")[-1]
353 try: del _params['sshproxytunnel']
354 except KeyError: pass
355
356
357
358 _ignored_options = [
359 'startsoundsystem',
360 'soundtunnel',
361 'defsndport',
362 'icon',
363 'xinerama',
364 'multidisp',
365 'display',
366 'krblogin',
367 'directrdp',
368 'directrdpsettings',
369 'rdpclient',
370 'rdpport',
371 'sshproxytype',
372 ]
373 for i in _ignored_options:
374 del _params[i]
375
376 return _params
377
378
380 """\
381 Sorts session profile names by their timestamp (as used in the file format's section name).
382
383 @param session_infos: a dictionary of session infos as reported by L{X2GoClient.list_sessions()}
384 @type session_infos: C{dict}
385
386 @return: a timestamp-sorted list of session names found in C{session_infos}
387 @rtype: C{list}
388
389 """
390 session_names = session_infos.keys()
391 sortable_session_names = [ '%s|%s' % (session_name.split('-')[-1].split('_')[0], session_name) for session_name in session_names ]
392 sortable_session_names.sort()
393 return [ session_name.split('|')[1] for session_name in sortable_session_names ]
394
395
397 """\
398 Imitates the behaviour of the GNU/touch command.
399
400 @param filename: name of the file to touch
401 @type filename: C{str}
402 @param mode: the file mode (as used for Python file objects)
403 @type mode: C{str}
404
405 """
406 if not os.path.isdir(os.path.dirname(filename)):
407 os.makedirs(os.path.dirname(filename), mode=00700)
408 f = open(filename, mode=mode)
409 f.close()
410
411
413 """\
414 Imitates the behaviour of the GNU/uniq command.
415
416 @param seq: a list/sequence containing consecutive duplicates.
417 @type seq: C{list}
418
419 @return: list that has been clean up from the consecutive duplicates
420 @rtype: C{list}
421
422 """
423
424 noDupes = []
425 [noDupes.append(i) for i in seq if not noDupes.count(i)]
426 return noDupes
427
428
430 """\
431 Render a list of all-known-to-Python character encodings (including
432 all known aliases)
433
434 """
435 from encodings.aliases import aliases
436 _raw_encname_list = []
437 _raw_encname_list.extend(aliases.keys())
438 _raw_encname_list.extend(aliases.values())
439 _raw_encname_list.sort()
440 _encname_list = []
441 for _raw_encname in _raw_encname_list:
442 _encname = _raw_encname.upper()
443 _encname = _encname.replace('_', '-')
444 _encname_list.append(_encname)
445 _encname_list.sort()
446 _encname_list = unique(_encname_list)
447 return _encname_list
448
449
451 """\
452 Try to remove a file, wait for unlocking, remove it once removing is possible...
453
454 @param dirname: directory name the file is in
455 @type dirname: C{str}
456 @param filename: name of the file to be removed
457 @type filename: C{str}
458
459 """
460 _not_removed = True
461 while _not_removed:
462 try:
463 os.remove(os.path.join(dirname, filename))
464 _not_removed = False
465 except:
466
467 gevent.sleep(5)
468
469
471 """\
472 Detect an unused IP socket.
473
474 @param bind_address: IP address to bind to
475 @type bind_address: C{str}
476 @param preferred_port: IP socket port that shall be tried first for availability
477 @type preferred_port: C{str}
478
479 @return: free local IP socket port that can be used for binding
480 @rtype: C{str}
481
482 """
483 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
484 try:
485 if preferred_port:
486 sock.bind((bind_address, preferred_port))
487 ipaddr, port = sock.getsockname()
488 else:
489 raise
490 except:
491 sock.bind(('', 0))
492 ipaddr, port = sock.getsockname()
493 return port
494
495
497 """\
498 Detect systems default character encoding.
499
500 @return: The system's local character encoding.
501 @rtype: C{str}
502
503 """
504 try:
505 encoding = locale.getdefaultlocale()[1]
506 if encoding is None:
507 raise BaseException
508 except:
509 try:
510 encoding = sys.getdefaultencoding()
511 except:
512 encoding = 'ascii'
513 return encoding
514
515
517 """\
518 Test if a given path is an absolute path name.
519
520 @param path: test this path for absolutism...
521 @type path: C{str}
522
523 @return: Returns C{True} if path is an absolute path name
524 @rtype: C{bool}
525
526 """
527 return bool((path.startswith('/') or re.match('^[%s]\:\\\\' % string.ascii_letters, path)))
528
529
531 """\
532 Wrapper for: xprop -root _XKB_RULES_NAMES
533
534 @return: A Python dictionary that contains the current X11 keyboard rules.
535 @rtype: C{dict}
536
537 """
538 p = subprocess.Popen(['xprop', '-root', '_XKB_RULES_NAMES',], stdout=subprocess.PIPE, )
539 _rn_list = p.stdout.read().split('"')
540 _rn_dict = {
541 'rules': _rn_list[1],
542 'model': _rn_list[3],
543 'layout': _rn_list[5],
544 'variant': _rn_list[7],
545 'options': _rn_list[9],
546 }
547 return _rn_dict
548
550 """\
551 Detect the current local screen's color depth.
552
553 @return: the local color depth in bits
554 @rtype: C{int}
555
556 """
557 if _X2GOCLIENT_OS != 'Windows':
558 try:
559 p = subprocess.Popen(['xwininfo', '-root',], stdout=subprocess.PIPE, )
560 _depth_line = [ _info.strip() for _info in p.stdout.read().split('\n') if 'Depth:' in _info ][0]
561 _depth = _depth_line.split(' ')[1]
562 return int(_depth)
563 except IndexError:
564
565 return 24
566 except OSError:
567
568 return 24
569
570 else:
571
572 dc = win32gui.GetDC(None)
573 _depth = win32print.GetDeviceCaps(dc, win32con.BITSPIXEL) * win32print.GetDeviceCaps(dc, win32con.PLANES)
574 win32gui.ReleaseDC(None, dc)
575 return _depth
576
578 """\
579 Test if color depth of this session is compatible with the
580 local screen's color depth.
581
582 @param depth_session: color depth of the session
583 @type depth_session: C{int}
584 @param depth_local: color depth of local screen
585 @type depth_local: C{int}
586
587 @return: Does the session color depth work with the local display?
588 @rtype: C{bool}
589
590 """
591 if depth_session == 0:
592 return True
593 if depth_session == depth_local:
594 return True
595 if ( ( depth_session == 24 or depth_session == 32 ) and ( depth_local == 24 or depth_local == 32 ) ):
596 return True;
597 if ( ( depth_session == 16 or depth_session == 17 ) and ( depth_local == 16 or depth_local == 17 ) ):
598 return True;
599 return False
600
601
603 """\
604 Find a session window by its X2GO session ID.
605
606 @param session_name: session name/ID of an X2Go session window
607 @type session_name: C{str}
608
609 @return: the window object (or ID) of the searched for session window
610 @rtype: C{obj} on Unix, C{int} on Windows
611
612 """
613 if _X2GOCLIENT_OS != 'Windows':
614
615 display = _X_DISPLAY
616 if display:
617 root = display.screen().root
618
619 success = False
620 windowIDs_obj = root.get_full_property(display.intern_atom('_NET_CLIENT_LIST'), Xlib.X.AnyPropertyType)
621
622 if windowIDs_obj is None:
623
624 windowIDs_obj = root.get_full_property(display.intern_atom('_NET_CLIENT_LIST_STACKING'), Xlib.X.AnyPropertyType)
625
626 if windowIDs_obj is not None:
627 windowIDs = windowIDs_obj.value
628
629 for windowID in windowIDs:
630 window = display.create_resource_object('window', windowID)
631 try:
632 name = window.get_wm_name()
633 except Xlib.error.BadWindow:
634 continue
635 if name is not None and "X2GO-{session_name}".format(session_name=session_name) == name:
636 success = True
637 break
638
639 if success:
640 return window
641
642 else:
643
644 windows = []
645 window = None
646
647 def _callback(hwnd, extra):
648 if win32gui.GetWindowText(hwnd) == "X2GO-%s" % session_name:
649 windows.append(hwnd)
650
651 win32gui.EnumWindows(_callback, None)
652 if len(windows): window = windows[0]
653
654 return window
655
656
658 """\
659 Get the geometry of the current screen's desktop.
660
661 @return: a (<width>, <height>) tuple will be returned
662 @rtype: C{tuple}
663
664 """
665 if _X2GOCLIENT_OS != 'Windows':
666 display = _X_DISPLAY
667 if display:
668 root = display.screen().root
669 return (root.get_geometry().width, root.get_geometry().height)
670
671 return None
672
674 """\
675 Get the geometry of the current screen's work area by
676 wrapping around::
677
678 xprop -root '_NET_WORKAREA'
679
680 @return: a (<width>, <height>) tuple will be returned
681 @rtype: C{tuple}
682
683 """
684 if _X2GOCLIENT_OS != 'Windows':
685 p = subprocess.Popen(['xprop', '-root', '_NET_WORKAREA',], stdout=subprocess.PIPE, )
686 _list = p.stdout.read().rstrip('\n').split(',')
687 if len(_list) == 4:
688 return (_list[2].strip(), _list[3].strip())
689 else:
690 return None
691 else:
692
693 return None
694
695
697 """\
698 Set title of session window.
699
700 @param session_window: session window instance
701 @type session_window: C{obj}
702 @param session_title: session title to be set for C{session_window}
703 @type session_title: C{str}
704
705 """
706 if _X2GOCLIENT_OS != 'Windows':
707 try:
708 session_window.set_wm_name(str(session_title))
709 session_window.set_wm_icon_name(str(session_title))
710 _X_DISPLAY.sync()
711 except Xlib.error.BadWindow:
712 pass
713
714 else:
715 win32gui.SetWindowText(session_window, session_title)
716
717
719 """\
720 Raise session window. Not functional for Unix-like operating systems.
721
722 @param session_window: session window instance
723 @type session_window: C{obj}
724
725 """
726 if _X2GOCLIENT_OS != 'Windows':
727 pass
728 else:
729 if session_window is not None:
730 win32gui.SetForegroundWindow(session_window)
731
732
734 """\
735 Merge sort two sorted lists
736
737 @param l1: first sorted list
738 @type l1: C{list}
739 @param l2: second sorted list
740 @type l2: C{list}
741
742 @return: the merge result of both sorted lists
743 @rtype: C{list}
744
745 """
746 ordered_list = []
747
748
749
750 l1 = l1[:]
751 l2 = l2[:]
752
753 while (l1 and l2):
754 if l1[0] not in l2:
755 item = l1.pop(0)
756 elif l2[0] not in l1:
757 item = l2.pop(0)
758 elif l1[0] in l2:
759 item = l1.pop(0)
760 l2.remove(item)
761 if item not in ordered_list:
762 ordered_list.append(item)
763
764
765 ordered_list.extend(l1 if l1 else l2)
766
767 return ordered_list
768
770 """\
771 Compare <version_a> with <version_b> using operator <op>.
772 In the background C{distutils.version.LooseVersion} is
773 used for the comparison operation.
774
775 @param version_a: a version string
776 @type version_a: C{str}
777 @param op: an operator provide as string (e.g. '<', '>', '==', '>=' etc.)
778 @type op: C{str}
779 @param version_b: another version string that is to be compared with <version_a>
780 @type version_b: C{str}
781
782 """
783
784
785
786 ver_a = distutils.version.LooseVersion(version_a)
787 ver_b = distutils.version.LooseVersion(version_b)
788
789 return eval("ver_a %s ver_b" % op)
790
792 """\
793 A simple progress status iterator class.
794
795 """
796 - def __init__(self, progress_event, progress_func=range(0, 100, 10)):
797 """\
798 @param progress_event: a threading.Event() object that gets notified on progress
799 @type progress_event: C{obj}
800 @param progress_func: a function that delivers a value between 0 and 100 (progress percentage value)
801 @type progress_func: C{func}
802
803 """
804 self.ev = progress_event
805 self.progress_func = progress_func
806
808 """\
809 Intialize the L{ProgressStatus} iterator object.
810
811 """
812 self.status = self.progress_func()
813 return self
814
816 """\
817 On each iteration wait for the progress event to get triggered from an outside
818 part of the application.
819
820 Once the event fires read the progress status from the progress retrieval function
821 and clear the event afterwards (so we wait for the next firing of the event).
822
823 """
824 if self.status < 100 and self.status != -1:
825 self.ev.wait()
826 self.status = self.progress_func()
827 self.ev.clear()
828 return self.status
829 else:
830 raise StopIteration
831
833
834 _this_class = None
835 if type(backend) not in (types.StringType, types.UnicodeType): return backend
836 backend = backend.upper()
837 available_backends = [ k for k in _BACKENDS[class_name].keys() if k != 'default' ]
838
839 if backend == 'default': backend = _BACKENDS[class_name]['default']
840 if backend in available_backends:
841 exec("from {backend} import {class_name} as _this_class".format(backend=_BACKENDS[class_name][backend], class_name=class_name))
842 else:
843 raise x2go_exceptions.X2GoBackendException('unknown backend name %s for class %s' % (backend, class_name))
844 return _this_class
845
846 -def genkeypair(local_username, client_address, key_type='RSA'):
847 """\
848 Generate an SSH pub/priv key pair without writing the private key to file.
849
850 @param local_username: the key is for this user
851 @type local_username: C{unicode}
852 @param client_address: the key is only valid for this client
853 @type client_address: C{unicode}
854 @param key_type: either of: RSA, DSA
855 @type key_type: C{unicode}
856
857 """
858 key = None
859 pubkey = None
860
861
862 if unicode(key_type) == u'RSA':
863 key = paramiko.RSAKey.generate(2048)
864 elif unicode(key_type) == u'DSA':
865 key = paramiko.DSSKey.generate(1024)
866
867 if key:
868
869
870 if key_type == "RSA":
871 pubkey_type = 'ssh-rsa'
872 elif key_type == "DSA":
873 pubkey_type = 'ssh-dss'
874
875
876
877 pubkey = "no-X11-forwarding,no-pty,no-user-rc {pubkey_type} {pubkey} {local_username}@{client_address}".format(pubkey=key.get_base64(), pubkey_type=pubkey_type, local_username=local_username, client_address=client_address)
878
879 return (pubkey, key)
880
881
883 """\
884 Python equivalent to the shell command "which".
885
886 @param basename: the basename of an application to be search for in $PATH
887 @type basename: C{str}
888
889 @return: full path to the application
890 @rtype: C{str}
891
892 """
893 if _X2GOCLIENT_OS == 'Windows':
894 delim = ";"
895 else:
896 delim = ":"
897
898 for path in os.environ["PATH"].split(delim):
899 if os.path.exists(os.path.join(path, basename)):
900 return os.path.join(path, basename)
901
902 return None
903