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:
173
# Will update the shown timer value every second
174
self._update_timer_callback_tag = (GLib.timeout_add
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
176
205
self.update_timer))
177
elif not (flag or self._update_timer_callback_tag is None):
178
GLib.source_remove(self._update_timer_callback_tag)
206
elif old and self._update_timer_callback_lock == 0:
207
gobject.source_remove(self._update_timer_callback_tag)
179
208
self._update_timer_callback_tag = None
181
210
def checker_completed(self, exitstatus, condition, command):
182
211
if exitstatus == 0:
183
self.logger('Checker for client {} (command "{}")'
184
' succeeded'.format(self.properties["Name"],
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))
221
if not self.last_checker_failed:
222
self.last_checker_failed = True
223
self.using_timer(True)
189
224
if os.WIFEXITED(condition):
190
self.logger('Checker for client {} (command "{}") failed'
192
.format(self.properties["Name"], command,
193
os.WEXITSTATUS(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)))
194
229
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)))
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'
201
243
def checker_started(self, command):
202
"""Server signals that a checker started."""
203
self.logger('Client {} started checker "{}"'
204
.format(self.properties["Name"],
244
#self.logger('Client %s started checker "%s"'
245
# % (self.properties["Name"], unicode(command)))
207
248
def got_secret(self):
208
self.logger('Client {} received its secret'
209
.format(self.properties["Name"]))
249
self.last_checker_failed = False
250
self.logger('Client %s received its secret'
251
% self.properties["Name"])
211
253
def need_approval(self, timeout, default):
213
message = 'Client {} needs approval within {} seconds'
255
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"],
257
message = 'Client %s will get its secret in %s seconds'
259
% (self.properties["Name"], timeout/1000))
260
self.using_timer(True)
219
262
def rejected(self, reason):
220
self.logger('Client {} was rejected; reason: {}'
221
.format(self.properties["Name"], reason))
263
self.logger('Client %s was rejected; reason: %s'
264
% (self.properties["Name"], reason))
223
266
def selectable(self):
224
267
"""Make this a "selectable" widget.
225
268
This overrides the method from urwid.FlowWidget."""
228
271
def rows(self, maxcolrow, focus=False):
229
272
"""How many rows this widget will occupy might depend on
230
273
whether we have focus or not.
231
274
This overrides the method from urwid.FlowWidget"""
232
275
return self.current_widget(focus).rows(maxcolrow, focus=focus)
234
277
def current_widget(self, focus=False):
235
278
if focus or self.opened:
236
279
return self._focus_widget
237
280
return self._widget
239
282
def update(self):
240
283
"Called when what is visible on the screen should be updated."
241
284
# 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",
285
with_standout = { "normal": "standout",
286
"bold": "bold-standout",
288
"underline-blink-standout",
289
"bold-underline-blink":
290
"bold-underline-blink-standout",
250
293
# Rebuild focus and non-focus widgets using current properties
252
295
# Base part of a client. Name!
253
base = '{name}: '.format(name=self.properties["Name"])
297
% {"name": self.properties["Name"]})
254
298
if not self.properties["Enabled"]:
255
299
message = "DISABLED"
256
self.using_timer(False)
257
300
elif self.properties["ApprovalPending"]:
258
timeout = datetime.timedelta(
259
milliseconds=self.properties["ApprovalDelay"])
301
timeout = datetime.timedelta(milliseconds
260
304
last_approval_request = isoformat_to_datetime(
261
305
self.properties["LastApprovalRequest"])
262
306
if last_approval_request is not None:
263
timer = max(timeout - (datetime.datetime.utcnow()
264
- last_approval_request),
265
datetime.timedelta())
307
timer = timeout - (datetime.datetime.utcnow()
308
- last_approval_request)
267
310
timer = datetime.timedelta()
268
311
if self.properties["ApprovedByDefault"]:
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())
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)
284
325
message = ('A checker has failed! Time until client'
286
.format(str(timer).rsplit(".", 1)[0]))
287
self.using_timer(True)
327
% unicode(timer).rsplit(".", 1)[0])
289
329
message = "enabled"
290
self.using_timer(False)
291
self._text = "{}{}".format(base, message)
330
self._text = "%s%s" % (base, message)
293
332
if not urwid.supports_unicode():
294
333
self._text = self._text.encode("ascii", "replace")
295
334
textlist = [("normal", self._text)]
305
344
# Run update hook, if any
306
345
if self.update_hook is not None:
307
346
self.update_hook()
309
348
def update_timer(self):
310
"""called by GLib. Will indefinitely loop until
311
GLib.source_remove() on tag is called
314
351
return True # Keep calling this
316
def delete(self, **kwargs):
353
def delete(self, *args, **kwargs):
317
354
if self._update_timer_callback_tag is not None:
318
GLib.source_remove(self._update_timer_callback_tag)
355
gobject.source_remove(self._update_timer_callback_tag)
319
356
self._update_timer_callback_tag = None
320
357
for match in self.match_objects:
322
359
self.match_objects = ()
323
360
if self.delete_hook is not None:
324
361
self.delete_hook(self)
325
return super(MandosClientWidget, self).delete(**kwargs)
362
return super(MandosClientWidget, self).delete(*args, **kwargs)
327
364
def render(self, maxcolrow, focus=False):
328
365
"""Render differently if we have focus.
329
366
This overrides the method from urwid.FlowWidget"""
330
367
return self.current_widget(focus).render(maxcolrow,
333
370
def keypress(self, maxcolrow, key):
335
372
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)
374
self.proxy.Enable(dbus_interface = client_interface,
341
self.proxy.Set(client_interface, "Enabled", False,
343
dbus_interface=dbus.PROPERTIES_IFACE)
377
self.proxy.Disable(dbus_interface = client_interface,
345
380
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
346
dbus_interface=client_interface,
381
dbus_interface = client_interface,
347
382
ignore_reply=True)
349
384
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
350
dbus_interface=client_interface,
385
dbus_interface = client_interface,
351
386
ignore_reply=True)
352
387
elif key == "R" or key == "_" or key == "ctrl k":
353
388
self.server_proxy_object.RemoveClient(self.proxy
355
390
ignore_reply=True)
357
self.proxy.Set(client_interface, "CheckerRunning",
358
dbus.Boolean(True), ignore_reply=True,
359
dbus_interface=dbus.PROPERTIES_IFACE)
392
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)
395
self.proxy.StopChecker(dbus_interface = client_interface,
365
self.proxy.CheckedOK(dbus_interface=client_interface,
398
self.proxy.CheckedOK(dbus_interface = client_interface,
366
399
ignore_reply=True)
368
401
# elif key == "p" or key == "=":
403
435
"""This is the entire user interface - the whole screen
404
436
with boxes, lists of client widgets, etc.
406
def __init__(self, max_log_length=1000, log_level=1):
438
def __init__(self, max_log_length=1000):
407
439
DBusGMainLoop(set_as_default=True)
409
441
self.screen = urwid.curses_display.Screen()
411
443
self.screen.register_palette((
413
445
"default", "default", None),
415
"bold", "default", "bold"),
447
"default", "default", "bold"),
416
448
("underline-blink",
417
"underline,blink", "default", "underline,blink"),
449
"default", "default", "underline"),
419
"standout", "default", "standout"),
451
"default", "default", "standout"),
420
452
("bold-underline-blink",
421
"bold,underline,blink", "default",
422
"bold,underline,blink"),
453
"default", "default", ("bold", "underline")),
423
454
("bold-standout",
424
"bold,standout", "default", "bold,standout"),
455
"default", "default", ("bold", "standout")),
425
456
("underline-blink-standout",
426
"underline,blink,standout", "default",
427
"underline,blink,standout"),
457
"default", "default", ("underline", "standout")),
428
458
("bold-underline-blink-standout",
429
"bold,underline,blink,standout", "default",
430
"bold,underline,blink,standout"),
459
"default", "default", ("bold", "underline",
433
463
if urwid.supports_unicode():
434
self.divider = "─" # \u2500
464
self.divider = "─" # \u2500
465
#self.divider = "━" # \u2501
436
self.divider = "_" # \u005f
467
#self.divider = "-" # \u002d
468
self.divider = "_" # \u005f
438
470
self.screen.start()
440
472
self.size = self.screen.get_cols_rows()
442
474
self.clients = urwid.SimpleListWalker([])
443
475
self.clients_dict = {}
445
477
# We will add Text widgets to this list
447
479
self.max_log_length = max_log_length
449
self.log_level = log_level
451
481
# We keep a reference to the log widget so we can remove it
452
482
# from the ListWalker without it getting destroyed
453
483
self.logbox = ConstrainedListBox(self.log)
455
485
# This keeps track of whether self.uilist currently has
456
486
# self.logbox in it or not
457
487
self.log_visible = True
458
488
self.log_wrap = "any"
461
491
self.log_message_raw(("bold",
462
492
"Mandos Monitor version " + version))
463
493
self.log_message_raw(("bold",
464
494
"q: Quit ?: Help"))
466
496
self.busname = domain + '.Mandos'
467
self.main_loop = GLib.MainLoop()
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,
469
541
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))
542
self.log_message(("Client with address %s and fingerprint %s"
543
" could not be found" % (address,
474
546
def rebuild(self):
475
547
"""This rebuilds the User Interface.
476
548
Call this when the widget layout needs to change"""
478
# self.uilist.append(urwid.ListBox(self.clients))
550
#self.uilist.append(urwid.ListBox(self.clients))
479
551
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
481
# header=urwid.Divider(),
553
#header=urwid.Divider(),
483
footer=urwid.Divider(
484
div_char=self.divider)))
556
urwid.Divider(div_char=
485
558
if self.log_visible:
486
559
self.uilist.append(self.logbox)
487
561
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:
563
def log_message(self, message):
493
564
timestamp = datetime.datetime.now().isoformat()
494
self.log_message_raw("{}: {}".format(timestamp, message),
497
def log_message_raw(self, markup, level=1):
565
self.log_message_raw(timestamp + ": " + message)
567
def log_message_raw(self, markup):
498
568
"""Add a log message to the log buffer."""
499
if level < self.log_level:
501
569
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]
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]
505
573
self.logbox.set_focus(len(self.logbox.body.contents),
506
574
coming_from="above")
509
577
def toggle_log_display(self):
510
578
"""Toggle visibility of the log buffer."""
511
579
self.log_visible = not self.log_visible
513
self.log_message("Log visibility changed to: {}"
514
.format(self.log_visible), level=0)
581
#self.log_message("Log visibility changed to: "
582
# + unicode(self.log_visible))
516
584
def change_log_display(self):
517
585
"""Change type of log display.
518
586
Currently, this toggles wrapping of text lines."""
522
590
self.log_wrap = "clip"
523
591
for textwidget in self.log:
524
592
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):
593
#self.log_message("Wrap mode: " + self.log_wrap)
595
def find_and_remove_client(self, path, name):
529
596
"""Find a client by its object path and remove it.
531
This is connected to the InterfacesRemoved signal from the
598
This is connected to the ClientRemoved signal from the
532
599
Mandos server object."""
533
if client_interface not in interfaces:
534
# Not a Mandos client object; ignore
537
601
client = self.clients_dict[path]
540
self.log_message("Unknown client {!r} removed"
604
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
609
def add_new_client(self, path):
554
610
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])),
611
self.add_client(MandosClientWidget(server_proxy_object
614
=client_proxy_object,
564
623
def add_client(self, client, path=None):
565
624
self.clients.append(client)
567
626
path = client.proxy.object_path
568
627
self.clients_dict[path] = client
569
self.clients.sort(key=lambda c: c.properties["Name"])
628
self.clients.sort(None, lambda c: c.properties["Name"])
572
631
def remove_client(self, client, path=None):
573
632
self.clients.remove(client)
575
634
path = client.proxy.object_path
576
635
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([])
579
643
def refresh(self):
580
644
"""Redraw the screen"""
581
645
canvas = self.topwidget.render(self.size, focus=True)
582
646
self.screen.draw_screen(self.size, canvas)
585
649
"""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),
630
self._input_callback_tag = (GLib.io_add_watch
651
self._input_callback_tag = (gobject.io_add_watch
631
652
(sys.stdin.fileno(),
633
654
self.process_input))
634
655
self.main_loop.run()
635
656
# Main loop has finished, we should close everything now
636
GLib.source_remove(self._input_callback_tag)
657
gobject.source_remove(self._input_callback_tag)
637
658
self.screen.stop()
640
661
self.main_loop.quit()
642
663
def process_input(self, source, condition):
643
664
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
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
656
677
key = translations[key]
657
678
except KeyError: # :-)
660
681
if key == "q" or key == "Q":
663
684
elif key == "window resize":
664
685
self.size = self.screen.get_cols_rows()
666
elif key == "ctrl l":
687
elif key == "\f": # Ctrl-L
669
689
elif key == "l" or key == "D":
670
690
self.toggle_log_display()