82
int(second), # Whole seconds
83
int(fraction*1000000)) # Microseconds
78
int(second), # Whole seconds
79
int(fraction*1000000)) # Microseconds
86
81
class MandosClientPropertyCache(object):
87
82
"""This wraps a Mandos Client D-Bus proxy object, caches the
88
83
properties and calls a hook function when any of them are
91
def __init__(self, proxy_object=None, properties=None, **kwargs):
92
self.proxy = proxy_object # Mandos Client proxy object
93
self.properties = dict() if properties is None else properties
86
def __init__(self, proxy_object=None, *args, **kwargs):
87
self.proxy = proxy_object # Mandos Client proxy object
89
self.properties = dict()
94
90
self.property_changed_match = (
95
self.proxy.connect_to_signal("PropertiesChanged",
96
self.properties_changed,
97
dbus.PROPERTIES_IFACE,
91
self.proxy.connect_to_signal("PropertyChanged",
92
self.property_changed,
100
if properties is None:
101
self.properties.update(self.proxy.GetAll(
103
dbus_interface=dbus.PROPERTIES_IFACE))
105
super(MandosClientPropertyCache, self).__init__(**kwargs)
107
def properties_changed(self, interface, properties, invalidated):
108
"""This is called whenever we get a PropertiesChanged signal
109
It updates the changed properties in the "properties" dict.
96
self.properties.update(
97
self.proxy.GetAll(client_interface,
98
dbus_interface = dbus.PROPERTIES_IFACE))
100
#XXX This breaks good super behaviour
101
# super(MandosClientPropertyCache, self).__init__(
104
def property_changed(self, property=None, value=None):
105
"""This is called whenever we get a PropertyChanged signal
106
It updates the changed property in the "properties" dict.
111
108
# Update properties dict with new value
112
if interface == client_interface:
113
self.properties.update(properties)
109
self.properties[property] = value
111
def delete(self, *args, **kwargs):
116
112
self.property_changed_match.remove()
113
super(MandosClientPropertyCache, self).__init__(
119
117
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
120
118
"""A Mandos Client which is visible on the screen.
123
121
def __init__(self, server_proxy_object=None, update_hook=None,
124
delete_hook=None, logger=None, **kwargs):
122
delete_hook=None, logger=None, *args, **kwargs):
125
123
# Called on update
126
124
self.update_hook = update_hook
127
125
# Called on delete
163
183
client_interface,
164
184
byte_arrays=True))
165
self.logger('Created client {}'
166
.format(self.properties["Name"]), level=0)
185
#self.logger('Created client %s' % (self.properties["Name"]))
187
def property_changed(self, property=None, value=None):
188
super(self, MandosClientWidget).property_changed(property,
190
if property == "ApprovalPending":
191
using_timer(bool(value))
168
193
def using_timer(self, flag):
169
194
"""Call this method with True or False when timer should be
170
195
activated or deactivated.
172
if flag and self._update_timer_callback_tag is None:
197
old = self._update_timer_callback_lock
199
self._update_timer_callback_lock += 1
201
self._update_timer_callback_lock -= 1
202
if old == 0 and self._update_timer_callback_lock:
173
203
# Will update the shown timer value every second
174
self._update_timer_callback_tag = (GLib.timeout_add
204
self._update_timer_callback_tag = (gobject.timeout_add
176
206
self.update_timer))
177
elif not (flag or self._update_timer_callback_tag is None):
178
GLib.source_remove(self._update_timer_callback_tag)
207
elif old and self._update_timer_callback_lock == 0:
208
gobject.source_remove(self._update_timer_callback_tag)
179
209
self._update_timer_callback_tag = None
181
211
def checker_completed(self, exitstatus, condition, command):
182
212
if exitstatus == 0:
183
self.logger('Checker for client {} (command "{}")'
184
' succeeded'.format(self.properties["Name"],
213
if self.last_checker_failed:
214
self.last_checker_failed = False
215
self.using_timer(False)
216
#self.logger('Checker for client %s (command "%s")'
218
# % (self.properties["Name"], command))
222
if not self.last_checker_failed:
223
self.last_checker_failed = True
224
self.using_timer(True)
189
225
if os.WIFEXITED(condition):
190
self.logger('Checker for client {} (command "{}") failed'
192
.format(self.properties["Name"], command,
193
os.WEXITSTATUS(condition)))
226
self.logger('Checker for client %s (command "%s")'
227
' failed with exit code %s'
228
% (self.properties["Name"], command,
229
os.WEXITSTATUS(condition)))
194
230
elif os.WIFSIGNALED(condition):
195
self.logger('Checker for client {} (command "{}") was'
196
' killed by signal {}'
197
.format(self.properties["Name"], command,
198
os.WTERMSIG(condition)))
231
self.logger('Checker for client %s (command "%s")'
232
' was killed by signal %s'
233
% (self.properties["Name"], command,
234
os.WTERMSIG(condition)))
235
elif os.WCOREDUMP(condition):
236
self.logger('Checker for client %s (command "%s")'
238
% (self.properties["Name"], command))
240
self.logger('Checker for client %s completed'
201
244
def checker_started(self, command):
202
"""Server signals that a checker started."""
203
self.logger('Client {} started checker "{}"'
204
.format(self.properties["Name"],
245
"""Server signals that a checker started. This could be useful
246
to log in the future. """
247
#self.logger('Client %s started checker "%s"'
248
# % (self.properties["Name"], unicode(command)))
207
251
def got_secret(self):
208
self.logger('Client {} received its secret'
209
.format(self.properties["Name"]))
252
self.last_checker_failed = False
253
self.logger('Client %s received its secret'
254
% self.properties["Name"])
211
256
def need_approval(self, timeout, default):
213
message = 'Client {} needs approval within {} seconds'
258
message = 'Client %s needs approval within %s seconds'
215
message = 'Client {} will get its secret in {} seconds'
216
self.logger(message.format(self.properties["Name"],
260
message = 'Client %s will get its secret in %s seconds'
262
% (self.properties["Name"], timeout/1000))
263
self.using_timer(True)
219
265
def rejected(self, reason):
220
self.logger('Client {} was rejected; reason: {}'
221
.format(self.properties["Name"], reason))
266
self.logger('Client %s was rejected; reason: %s'
267
% (self.properties["Name"], reason))
223
269
def selectable(self):
224
270
"""Make this a "selectable" widget.
225
271
This overrides the method from urwid.FlowWidget."""
228
274
def rows(self, maxcolrow, focus=False):
229
275
"""How many rows this widget will occupy might depend on
230
276
whether we have focus or not.
231
277
This overrides the method from urwid.FlowWidget"""
232
278
return self.current_widget(focus).rows(maxcolrow, focus=focus)
234
280
def current_widget(self, focus=False):
235
281
if focus or self.opened:
236
282
return self._focus_widget
237
283
return self._widget
239
285
def update(self):
240
286
"Called when what is visible on the screen should be updated."
241
287
# How to add standout mode to a style
242
with_standout = {"normal": "standout",
243
"bold": "bold-standout",
245
"underline-blink-standout",
246
"bold-underline-blink":
247
"bold-underline-blink-standout",
288
with_standout = { "normal": "standout",
289
"bold": "bold-standout",
291
"underline-blink-standout",
292
"bold-underline-blink":
293
"bold-underline-blink-standout",
250
296
# Rebuild focus and non-focus widgets using current properties
252
298
# Base part of a client. Name!
253
base = '{name}: '.format(name=self.properties["Name"])
300
% {"name": self.properties["Name"]})
254
301
if not self.properties["Enabled"]:
255
302
message = "DISABLED"
256
self.using_timer(False)
257
303
elif self.properties["ApprovalPending"]:
258
timeout = datetime.timedelta(
259
milliseconds=self.properties["ApprovalDelay"])
304
timeout = datetime.timedelta(milliseconds
260
307
last_approval_request = isoformat_to_datetime(
261
308
self.properties["LastApprovalRequest"])
262
309
if last_approval_request is not None:
263
timer = max(timeout - (datetime.datetime.utcnow()
264
- last_approval_request),
265
datetime.timedelta())
310
timer = timeout - (datetime.datetime.utcnow()
311
- last_approval_request)
267
313
timer = datetime.timedelta()
268
314
if self.properties["ApprovedByDefault"]:
269
message = "Approval in {}. (d)eny?"
315
message = "Approval in %s. (d)eny?"
271
message = "Denial in {}. (a)pprove?"
272
message = message.format(str(timer).rsplit(".", 1)[0])
273
self.using_timer(True)
274
elif self.properties["LastCheckerStatus"] != 0:
275
# When checker has failed, show timer until client expires
317
message = "Denial in %s. (a)pprove?"
318
message = message % unicode(timer).rsplit(".", 1)[0]
319
elif self.last_checker_failed:
320
# When checker has failed, print a timer until client expires
276
321
expires = self.properties["Expires"]
277
322
if expires == "":
278
323
timer = datetime.timedelta(0)
280
expires = (datetime.datetime.strptime
281
(expires, '%Y-%m-%dT%H:%M:%S.%f'))
282
timer = max(expires - datetime.datetime.utcnow(),
283
datetime.timedelta())
325
expires = datetime.datetime.strptime(expires,
326
'%Y-%m-%dT%H:%M:%S.%f')
327
timer = expires - datetime.datetime.utcnow()
284
328
message = ('A checker has failed! Time until client'
286
.format(str(timer).rsplit(".", 1)[0]))
287
self.using_timer(True)
330
% unicode(timer).rsplit(".", 1)[0])
289
332
message = "enabled"
290
self.using_timer(False)
291
self._text = "{}{}".format(base, message)
333
self._text = "%s%s" % (base, message)
293
335
if not urwid.supports_unicode():
294
336
self._text = self._text.encode("ascii", "replace")
295
337
textlist = [("normal", self._text)]
305
347
# Run update hook, if any
306
348
if self.update_hook is not None:
307
349
self.update_hook()
309
351
def update_timer(self):
310
"""called by GLib. Will indefinitely loop until
311
GLib.source_remove() on tag is called
352
"""called by gobject. Will indefinitely loop until
353
gobject.source_remove() on tag is called"""
314
355
return True # Keep calling this
316
def delete(self, **kwargs):
357
def delete(self, *args, **kwargs):
317
358
if self._update_timer_callback_tag is not None:
318
GLib.source_remove(self._update_timer_callback_tag)
359
gobject.source_remove(self._update_timer_callback_tag)
319
360
self._update_timer_callback_tag = None
320
361
for match in self.match_objects:
322
363
self.match_objects = ()
323
364
if self.delete_hook is not None:
324
365
self.delete_hook(self)
325
return super(MandosClientWidget, self).delete(**kwargs)
366
return super(MandosClientWidget, self).delete(*args, **kwargs)
327
368
def render(self, maxcolrow, focus=False):
328
369
"""Render differently if we have focus.
329
370
This overrides the method from urwid.FlowWidget"""
330
371
return self.current_widget(focus).render(maxcolrow,
333
374
def keypress(self, maxcolrow, key):
335
376
This overrides the method from urwid.FlowWidget"""
337
self.proxy.Set(client_interface, "Enabled",
338
dbus.Boolean(True), ignore_reply=True,
339
dbus_interface=dbus.PROPERTIES_IFACE)
378
self.proxy.Enable(dbus_interface = client_interface,
341
self.proxy.Set(client_interface, "Enabled", False,
343
dbus_interface=dbus.PROPERTIES_IFACE)
381
self.proxy.Disable(dbus_interface = client_interface,
345
384
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
346
dbus_interface=client_interface,
385
dbus_interface = client_interface,
347
386
ignore_reply=True)
349
388
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
350
dbus_interface=client_interface,
389
dbus_interface = client_interface,
351
390
ignore_reply=True)
352
391
elif key == "R" or key == "_" or key == "ctrl k":
353
392
self.server_proxy_object.RemoveClient(self.proxy
355
394
ignore_reply=True)
357
self.proxy.Set(client_interface, "CheckerRunning",
358
dbus.Boolean(True), ignore_reply=True,
359
dbus_interface=dbus.PROPERTIES_IFACE)
396
self.proxy.StartChecker(dbus_interface = client_interface,
361
self.proxy.Set(client_interface, "CheckerRunning",
362
dbus.Boolean(False), ignore_reply=True,
363
dbus_interface=dbus.PROPERTIES_IFACE)
399
self.proxy.StopChecker(dbus_interface = client_interface,
365
self.proxy.CheckedOK(dbus_interface=client_interface,
402
self.proxy.CheckedOK(dbus_interface = client_interface,
366
403
ignore_reply=True)
368
405
# elif key == "p" or key == "=":
403
439
"""This is the entire user interface - the whole screen
404
440
with boxes, lists of client widgets, etc.
406
def __init__(self, max_log_length=1000, log_level=1):
442
def __init__(self, max_log_length=1000):
407
443
DBusGMainLoop(set_as_default=True)
409
445
self.screen = urwid.curses_display.Screen()
411
447
self.screen.register_palette((
413
449
"default", "default", None),
415
"bold", "default", "bold"),
451
"default", "default", "bold"),
416
452
("underline-blink",
417
"underline,blink", "default", "underline,blink"),
453
"default", "default", "underline"),
419
"standout", "default", "standout"),
455
"default", "default", "standout"),
420
456
("bold-underline-blink",
421
"bold,underline,blink", "default",
422
"bold,underline,blink"),
457
"default", "default", ("bold", "underline")),
423
458
("bold-standout",
424
"bold,standout", "default", "bold,standout"),
459
"default", "default", ("bold", "standout")),
425
460
("underline-blink-standout",
426
"underline,blink,standout", "default",
427
"underline,blink,standout"),
461
"default", "default", ("underline", "standout")),
428
462
("bold-underline-blink-standout",
429
"bold,underline,blink,standout", "default",
430
"bold,underline,blink,standout"),
463
"default", "default", ("bold", "underline",
433
467
if urwid.supports_unicode():
434
self.divider = "─" # \u2500
468
self.divider = "─" # \u2500
469
#self.divider = "━" # \u2501
436
self.divider = "_" # \u005f
471
#self.divider = "-" # \u002d
472
self.divider = "_" # \u005f
438
474
self.screen.start()
440
476
self.size = self.screen.get_cols_rows()
442
478
self.clients = urwid.SimpleListWalker([])
443
479
self.clients_dict = {}
445
481
# We will add Text widgets to this list
447
483
self.max_log_length = max_log_length
449
self.log_level = log_level
451
485
# We keep a reference to the log widget so we can remove it
452
486
# from the ListWalker without it getting destroyed
453
487
self.logbox = ConstrainedListBox(self.log)
455
489
# This keeps track of whether self.uilist currently has
456
490
# self.logbox in it or not
457
491
self.log_visible = True
458
492
self.log_wrap = "any"
461
495
self.log_message_raw(("bold",
462
496
"Mandos Monitor version " + version))
463
497
self.log_message_raw(("bold",
464
498
"q: Quit ?: Help"))
466
500
self.busname = domain + '.Mandos'
467
self.main_loop = GLib.MainLoop()
501
self.main_loop = gobject.MainLoop()
469
503
def client_not_found(self, fingerprint, address):
470
self.log_message("Client with address {} and fingerprint {}"
471
" could not be found"
472
.format(address, fingerprint))
504
self.log_message(("Client with address %s and fingerprint %s"
505
" could not be found" % (address,
474
508
def rebuild(self):
475
509
"""This rebuilds the User Interface.
476
510
Call this when the widget layout needs to change"""
478
# self.uilist.append(urwid.ListBox(self.clients))
512
#self.uilist.append(urwid.ListBox(self.clients))
479
513
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
481
# header=urwid.Divider(),
515
#header=urwid.Divider(),
483
footer=urwid.Divider(
484
div_char=self.divider)))
518
urwid.Divider(div_char=
485
520
if self.log_visible:
486
521
self.uilist.append(self.logbox)
487
522
self.topwidget = urwid.Pile(self.uilist)
489
def log_message(self, message, level=1):
490
"""Log message formatted with timestamp"""
491
if level < self.log_level:
524
def log_message(self, message):
493
525
timestamp = datetime.datetime.now().isoformat()
494
self.log_message_raw("{}: {}".format(timestamp, message),
497
def log_message_raw(self, markup, level=1):
526
self.log_message_raw(timestamp + ": " + message)
528
def log_message_raw(self, markup):
498
529
"""Add a log message to the log buffer."""
499
if level < self.log_level:
501
530
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
502
if self.max_log_length:
503
if len(self.log) > self.max_log_length:
504
del self.log[0:len(self.log)-self.max_log_length-1]
531
if (self.max_log_length
532
and len(self.log) > self.max_log_length):
533
del self.log[0:len(self.log)-self.max_log_length-1]
505
534
self.logbox.set_focus(len(self.logbox.body.contents),
506
535
coming_from="above")
509
538
def toggle_log_display(self):
510
539
"""Toggle visibility of the log buffer."""
511
540
self.log_visible = not self.log_visible
513
self.log_message("Log visibility changed to: {}"
514
.format(self.log_visible), level=0)
542
#self.log_message("Log visibility changed to: "
543
# + unicode(self.log_visible))
516
545
def change_log_display(self):
517
546
"""Change type of log display.
518
547
Currently, this toggles wrapping of text lines."""
522
551
self.log_wrap = "clip"
523
552
for textwidget in self.log:
524
553
textwidget.set_wrap_mode(self.log_wrap)
525
self.log_message("Wrap mode: {}".format(self.log_wrap),
528
def find_and_remove_client(self, path, interfaces):
554
#self.log_message("Wrap mode: " + self.log_wrap)
556
def find_and_remove_client(self, path, name):
529
557
"""Find a client by its object path and remove it.
531
This is connected to the InterfacesRemoved signal from the
559
This is connected to the ClientRemoved signal from the
532
560
Mandos server object."""
533
if client_interface not in interfaces:
534
# Not a Mandos client object; ignore
537
562
client = self.clients_dict[path]
540
self.log_message("Unknown client {!r} removed"
565
self.log_message("Unknown client %r (%r) removed", name,
545
def add_new_client(self, path, ifs_and_props):
546
"""Find a client by its object path and remove it.
548
This is connected to the InterfacesAdded signal from the
549
Mandos server object.
551
if client_interface not in ifs_and_props:
552
# Not a Mandos client object; ignore
570
def add_new_client(self, path):
554
571
client_proxy_object = self.bus.get_object(self.busname, path)
555
self.add_client(MandosClientWidget(
556
server_proxy_object=self.mandos_serv,
557
proxy_object=client_proxy_object,
558
update_hook=self.refresh,
559
delete_hook=self.remove_client,
560
logger=self.log_message,
561
properties=dict(ifs_and_props[client_interface])),
572
self.add_client(MandosClientWidget(server_proxy_object
575
=client_proxy_object,
564
584
def add_client(self, client, path=None):
565
585
self.clients.append(client)
567
587
path = client.proxy.object_path
568
588
self.clients_dict[path] = client
569
self.clients.sort(key=lambda c: c.properties["Name"])
589
self.clients.sort(None, lambda c: c.properties["Name"])
572
592
def remove_client(self, client, path=None):
573
593
self.clients.remove(client)
575
595
path = client.proxy.object_path
576
596
del self.clients_dict[path]
597
if not self.clients_dict:
598
# Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
599
# is completely emptied, we need to recreate it.
600
self.clients = urwid.SimpleListWalker([])
579
604
def refresh(self):
580
605
"""Redraw the screen"""
581
606
canvas = self.topwidget.render(self.size, focus=True)
582
607
self.screen.draw_screen(self.size, canvas)
585
610
"""Start the main loop and exit when it's done."""
586
611
self.bus = dbus.SystemBus()
587
612
mandos_dbus_objc = self.bus.get_object(
588
613
self.busname, "/", follow_name_owner_changes=True)
589
self.mandos_serv = dbus.Interface(
590
mandos_dbus_objc, dbus_interface=server_interface)
614
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
592
618
mandos_clients = (self.mandos_serv
593
619
.GetAllClientsWithProperties())
594
if not mandos_clients:
595
self.log_message_raw(("bold",
596
"Note: Server has no clients."))
597
620
except dbus.exceptions.DBusException:
598
self.log_message_raw(("bold",
599
"Note: No Mandos server running."))
600
621
mandos_clients = dbus.Dictionary()
602
623
(self.mandos_serv
603
.connect_to_signal("InterfacesRemoved",
624
.connect_to_signal("ClientRemoved",
604
625
self.find_and_remove_client,
605
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
626
dbus_interface=server_interface,
606
627
byte_arrays=True))
607
628
(self.mandos_serv
608
.connect_to_signal("InterfacesAdded",
629
.connect_to_signal("ClientAdded",
609
630
self.add_new_client,
610
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
631
dbus_interface=server_interface,
611
632
byte_arrays=True))
612
633
(self.mandos_serv
613
634
.connect_to_signal("ClientNotFound",
614
635
self.client_not_found,
615
636
dbus_interface=server_interface,
616
637
byte_arrays=True))
617
for path, client in mandos_clients.items():
638
for path, client in mandos_clients.iteritems():
618
639
client_proxy_object = self.bus.get_object(self.busname,
620
self.add_client(MandosClientWidget(
621
server_proxy_object=self.mandos_serv,
622
proxy_object=client_proxy_object,
624
update_hook=self.refresh,
625
delete_hook=self.remove_client,
626
logger=self.log_message),
641
self.add_client(MandosClientWidget(server_proxy_object
644
=client_proxy_object,
630
self._input_callback_tag = (GLib.io_add_watch
655
self._input_callback_tag = (gobject.io_add_watch
631
656
(sys.stdin.fileno(),
633
658
self.process_input))
634
659
self.main_loop.run()
635
660
# Main loop has finished, we should close everything now
636
GLib.source_remove(self._input_callback_tag)
661
gobject.source_remove(self._input_callback_tag)
637
662
self.screen.stop()
640
665
self.main_loop.quit()
642
667
def process_input(self, source, condition):
643
668
keys = self.screen.get_input()
644
translations = {"ctrl n": "down", # Emacs
645
"ctrl p": "up", # Emacs
646
"ctrl v": "page down", # Emacs
647
"meta v": "page up", # Emacs
648
" ": "page down", # less
649
"f": "page down", # less
650
"b": "page up", # less
669
translations = { "ctrl n": "down", # Emacs
670
"ctrl p": "up", # Emacs
671
"ctrl v": "page down", # Emacs
672
"meta v": "page up", # Emacs
673
" ": "page down", # less
674
"f": "page down", # less
675
"b": "page up", # less
656
681
key = translations[key]
657
682
except KeyError: # :-)
660
685
if key == "q" or key == "Q":
663
688
elif key == "window resize":
664
689
self.size = self.screen.get_cols_rows()
666
elif key == "ctrl l":
691
elif key == "\f": # Ctrl-L
669
693
elif key == "l" or key == "D":
670
694
self.toggle_log_display()