39
39
import qubesadmin .vm
40
40
from qubesadmin .tools import xcffibhelpers
41
41
42
+ # pylint: disable=wrong-import-position
43
+ from Xlib import XK , X
44
+ from Xlib .display import Display
45
+
42
46
GUI_DAEMON_PATH = '/usr/bin/qubes-guid'
43
47
PACAT_DAEMON_PATH = '/usr/bin/pacat-simple-vchan'
44
48
QUBES_ICON_DIR = '/usr/share/icons/hicolor/128x128/devices'
45
49
50
+ def validator_key_sequence (sequence : str ) -> bool :
51
+ """ xside.c key sequence validation is not case sensitive and supports more
52
+ choices than Global Config's limited choices, so we replicate it here """
53
+ if not isinstance (sequence , str ):
54
+ return False
55
+ while '-' in sequence :
56
+ modifier , sequence = sequence .split ('-' , 1 )
57
+ if not modifier .lower () in \
58
+ ['shift' , 'ctrl' , 'alt' , 'mod1' , 'mod2' , 'mod3' , 'mod4' ]:
59
+ return False
60
+ return Display ().keysym_to_keycode (XK .string_to_keysym (sequence )) != \
61
+ X .NoSymbol
62
+
63
+ def validator_trayicon_mode (mode : str ) -> bool :
64
+ """ xside.c tray mode validation is replicated here """
65
+ if not isinstance (mode , str ):
66
+ return False
67
+ if mode in ['bg' , 'border1' , 'border2' , 'tint' ]:
68
+ return True
69
+ if mode .startswith ('tint' ):
70
+ if mode [4 :] in ['+border1' , '+border2' , '+saturation50' , '+whitehack' ]:
71
+ return True
72
+ return False
73
+
74
+ def validator_color (color : str ) -> bool :
75
+ """ xside.c `parse_color` validation code is replicated here """
76
+ if not isinstance (color , str ):
77
+ return False
78
+ if re .match (r"^0[xX](?:[0-9a-fA-F]{3}){1,2}$" , color .strip ()):
79
+ # Technically `parse_color` could parse values such as `0x0`
80
+ # Xlib.xobject.colormap conventions & standards are different.
81
+ return True
82
+ if Display ().screen ().default_colormap .alloc_named_color (color ) is not None :
83
+ return True
84
+ return False
85
+
46
86
GUI_DAEMON_OPTIONS = [
47
- ('allow_fullscreen' , 'bool' ),
48
- ('override_redirect_protection' , 'bool' ),
49
- ('override_redirect' , 'str' ),
50
- ('allow_utf8_titles' , 'bool' ),
51
- ('secure_copy_sequence' , 'str' ),
52
- ('secure_paste_sequence' , 'str' ),
53
- ('windows_count_limit' , 'int' ),
54
- ('trayicon_mode' , 'str' ),
55
- ('window_background_color' , 'str' ),
56
- ('startup_timeout' , 'int' ),
57
- ('max_clipboard_size' , 'int' ),
87
+ ('allow_fullscreen' , 'bool' , (lambda x : isinstance (x , bool ))),
88
+ ('override_redirect_protection' , 'bool' , (lambda x : isinstance (x , bool ))),
89
+ ('override_redirect' , 'str' , (lambda x : x in ['allow' , 'disable' ])),
90
+ ('allow_utf8_titles' , 'bool' , (lambda x : isinstance (x , bool ))),
91
+ ('secure_copy_sequence' , 'str' , validator_key_sequence ),
92
+ ('secure_paste_sequence' , 'str' , validator_key_sequence ),
93
+ ('windows_count_limit' , 'int' , (lambda x : isinstance (x , int ) and x > 0 )),
94
+ ('trayicon_mode' , 'str' , validator_trayicon_mode ),
95
+ ('window_background_color' , 'str' , validator_color ),
96
+ ('startup_timeout' , 'int' , (lambda x : isinstance (x , int ) and x >= 0 )),
97
+ ('max_clipboard_size' , 'int' , \
98
+ (lambda x : isinstance (x , int ) and 256 <= x <= 256000 )),
58
99
]
59
100
60
101
formatter = logging .Formatter (
@@ -108,12 +149,12 @@ def retrieve_gui_daemon_options(vm, guivm):
108
149
109
150
options = {}
110
151
111
- for name , kind in GUI_DAEMON_OPTIONS :
112
- feature_value = vm . features . get (
113
- 'gui-' + name . replace ( '_' , '-' ) , None )
152
+ for name , kind , validator in GUI_DAEMON_OPTIONS :
153
+ feature = 'gui-' + name . replace ( '_' , '-' )
154
+ feature_value = vm . features . get ( feature , None )
114
155
if feature_value is None :
115
- feature_value = guivm . features . get (
116
- 'gui-default-' + name . replace ( '_' , '-' ) , None )
156
+ feature = 'gui-' + name . replace ( '_' , '-' )
157
+ feature_value = guivm . features . get ( feature , None )
117
158
if feature_value is None :
118
159
continue
119
160
@@ -126,6 +167,15 @@ def retrieve_gui_daemon_options(vm, guivm):
126
167
else :
127
168
assert False , kind
128
169
170
+ if not validator (value ):
171
+ message = f"{ vm .name } : Ignoring invalid feature:\n " \
172
+ f"{ feature } ={ feature_value } "
173
+ log .error (message )
174
+ if not sys .stdout .isatty ():
175
+ subprocess .run (['notify-send' , '-a' , 'Qubes GUI Daemon' , \
176
+ '--icon' , 'dialog-warning' , message ], check = False )
177
+ continue
178
+
129
179
options [name ] = value
130
180
return options
131
181
@@ -141,7 +191,7 @@ def serialize_gui_daemon_options(options):
141
191
'' ,
142
192
'global: {' ,
143
193
]
144
- for name , kind in GUI_DAEMON_OPTIONS :
194
+ for name , kind , validator in GUI_DAEMON_OPTIONS :
145
195
if name in options :
146
196
value = options [name ]
147
197
if kind == 'bool' :
@@ -153,6 +203,14 @@ def serialize_gui_daemon_options(options):
153
203
else :
154
204
assert False , kind
155
205
206
+ if not validator (value ):
207
+ message = f"Ignoring invalid GUI property:\n { name } ={ value } "
208
+ log .error (message )
209
+ if not sys .stdout .isatty ():
210
+ subprocess .run (['notify-send' , '-a' , 'Qubes GUI Daemon' , \
211
+ '--icon' , 'dialog-warning' , message ], check = False )
212
+ continue
213
+
156
214
lines .append (' {} = {};' .format (name , serialized ))
157
215
lines .append ('}' )
158
216
lines .append ('' )
0 commit comments