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