82
int(second), # Whole seconds
83
int(fraction*1000000)) # Microseconds
81
int(second), # Whole seconds
82
int(fraction*1000000)) # Microseconds
86
84
class MandosClientPropertyCache(object):
87
85
"""This wraps a Mandos Client D-Bus proxy object, caches the
88
86
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
89
def __init__(self, proxy_object=None, *args, **kwargs):
90
self.proxy = proxy_object # Mandos Client proxy object
92
self.properties = dict()
94
93
self.property_changed_match = (
95
self.proxy.connect_to_signal("PropertiesChanged",
96
self.properties_changed,
97
dbus.PROPERTIES_IFACE,
94
self.proxy.connect_to_signal("PropertyChanged",
95
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.
99
self.properties.update(
100
self.proxy.GetAll(client_interface,
101
dbus_interface = dbus.PROPERTIES_IFACE))
103
#XXX This breaks good super behaviour
104
# super(MandosClientPropertyCache, self).__init__(
107
def property_changed(self, property=None, value=None):
108
"""This is called whenever we get a PropertyChanged signal
109
It updates the changed property in the "properties" dict.
111
111
# Update properties dict with new value
112
if interface == client_interface:
113
self.properties.update(properties)
112
self.properties[property] = value
114
def delete(self, *args, **kwargs):
116
115
self.property_changed_match.remove()
116
super(MandosClientPropertyCache, self).__init__(
119
120
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
120
121
"""A Mandos Client which is visible on the screen.
123
124
def __init__(self, server_proxy_object=None, update_hook=None,
124
delete_hook=None, logger=None, **kwargs):
125
delete_hook=None, logger=None, *args, **kwargs):
125
126
# Called on update
126
127
self.update_hook = update_hook
127
128
# Called on delete
163
176
client_interface,
164
177
byte_arrays=True))
165
self.logger('Created client {}'
166
.format(self.properties["Name"]), level=0)
178
#self.logger('Created client {0}'
179
# .format(self.properties["Name"]))
181
def property_changed(self, property=None, value=None):
182
super(self, MandosClientWidget).property_changed(property,
184
if property == "ApprovalPending":
185
using_timer(bool(value))
186
if property == "LastCheckerStatus":
187
using_timer(value != 0)
188
#self.logger('Checker for client {0} (command "{1}") was '
189
# ' successful'.format(self.properties["Name"],
168
192
def using_timer(self, flag):
169
193
"""Call this method with True or False when timer should be
170
194
activated or deactivated.
172
if flag and self._update_timer_callback_tag is None:
196
old = self._update_timer_callback_lock
198
self._update_timer_callback_lock += 1
200
self._update_timer_callback_lock -= 1
201
if old == 0 and self._update_timer_callback_lock:
173
202
# Will update the shown timer value every second
174
self._update_timer_callback_tag = (GLib.timeout_add
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"],
189
215
if os.WIFEXITED(condition):
190
self.logger('Checker for client {} (command "{}") failed'
216
self.logger('Checker for client {0} (command "{1}")'
217
' failed with exit code {2}'
192
218
.format(self.properties["Name"], command,
193
219
os.WEXITSTATUS(condition)))
194
220
elif os.WIFSIGNALED(condition):
195
self.logger('Checker for client {} (command "{}") was'
196
' killed by signal {}'
221
self.logger('Checker for client {0} (command "{1}") was'
222
' killed by signal {2}'
197
223
.format(self.properties["Name"], command,
198
224
os.WTERMSIG(condition)))
225
elif os.WCOREDUMP(condition):
226
self.logger('Checker for client {0} (command "{1}")'
228
.format(self.properties["Name"], command))
230
self.logger('Checker for client {0} completed'
232
.format(self.properties["Name"]))
201
235
def checker_started(self, command):
202
"""Server signals that a checker started."""
203
self.logger('Client {} started checker "{}"'
204
.format(self.properties["Name"],
236
"""Server signals that a checker started. This could be useful
237
to log in the future. """
238
#self.logger('Client {0} started checker "{1}"'
239
# .format(self.properties["Name"],
207
243
def got_secret(self):
208
self.logger('Client {} received its secret'
244
self.logger('Client {0} received its secret'
209
245
.format(self.properties["Name"]))
211
247
def need_approval(self, timeout, default):
213
message = 'Client {} needs approval within {} seconds'
249
message = 'Client {0} needs approval within {1} seconds'
215
message = 'Client {} will get its secret in {} seconds'
251
message = 'Client {0} will get its secret in {1} seconds'
216
252
self.logger(message.format(self.properties["Name"],
254
self.using_timer(True)
219
256
def rejected(self, reason):
220
self.logger('Client {} was rejected; reason: {}'
257
self.logger('Client {0} was rejected; reason: {1}'
221
258
.format(self.properties["Name"], reason))
223
260
def selectable(self):
224
261
"""Make this a "selectable" widget.
225
262
This overrides the method from urwid.FlowWidget."""
228
265
def rows(self, maxcolrow, focus=False):
229
266
"""How many rows this widget will occupy might depend on
230
267
whether we have focus or not.
231
268
This overrides the method from urwid.FlowWidget"""
232
269
return self.current_widget(focus).rows(maxcolrow, focus=focus)
234
271
def current_widget(self, focus=False):
235
272
if focus or self.opened:
236
273
return self._focus_widget
237
274
return self._widget
239
276
def update(self):
240
277
"Called when what is visible on the screen should be updated."
241
278
# 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",
279
with_standout = { "normal": "standout",
280
"bold": "bold-standout",
282
"underline-blink-standout",
283
"bold-underline-blink":
284
"bold-underline-blink-standout",
250
287
# Rebuild focus and non-focus widgets using current properties
253
290
base = '{name}: '.format(name=self.properties["Name"])
254
291
if not self.properties["Enabled"]:
255
292
message = "DISABLED"
256
self.using_timer(False)
257
293
elif self.properties["ApprovalPending"]:
258
timeout = datetime.timedelta(
259
milliseconds=self.properties["ApprovalDelay"])
294
timeout = datetime.timedelta(milliseconds
260
297
last_approval_request = isoformat_to_datetime(
261
298
self.properties["LastApprovalRequest"])
262
299
if last_approval_request is not None:
263
timer = max(timeout - (datetime.datetime.utcnow()
264
- last_approval_request),
265
datetime.timedelta())
300
timer = timeout - (datetime.datetime.utcnow()
301
- last_approval_request)
267
303
timer = datetime.timedelta()
268
304
if self.properties["ApprovedByDefault"]:
269
message = "Approval in {}. (d)eny?"
305
message = "Approval in {0}. (d)eny?"
271
message = "Denial in {}. (a)pprove?"
272
message = message.format(str(timer).rsplit(".", 1)[0])
273
self.using_timer(True)
307
message = "Denial in {0}. (a)pprove?"
308
message = message.format(unicode(timer).rsplit(".", 1)[0])
274
309
elif self.properties["LastCheckerStatus"] != 0:
275
310
# When checker has failed, show timer until client expires
276
311
expires = self.properties["Expires"]
305
337
# Run update hook, if any
306
338
if self.update_hook is not None:
307
339
self.update_hook()
309
341
def update_timer(self):
310
"""called by GLib. Will indefinitely loop until
311
GLib.source_remove() on tag is called
342
"""called by gobject. Will indefinitely loop until
343
gobject.source_remove() on tag is called"""
314
345
return True # Keep calling this
316
def delete(self, **kwargs):
347
def delete(self, *args, **kwargs):
317
348
if self._update_timer_callback_tag is not None:
318
GLib.source_remove(self._update_timer_callback_tag)
349
gobject.source_remove(self._update_timer_callback_tag)
319
350
self._update_timer_callback_tag = None
320
351
for match in self.match_objects:
322
353
self.match_objects = ()
323
354
if self.delete_hook is not None:
324
355
self.delete_hook(self)
325
return super(MandosClientWidget, self).delete(**kwargs)
356
return super(MandosClientWidget, self).delete(*args, **kwargs)
327
358
def render(self, maxcolrow, focus=False):
328
359
"""Render differently if we have focus.
329
360
This overrides the method from urwid.FlowWidget"""
330
361
return self.current_widget(focus).render(maxcolrow,
333
364
def keypress(self, maxcolrow, key):
335
366
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)
368
self.proxy.Enable(dbus_interface = client_interface,
341
self.proxy.Set(client_interface, "Enabled", False,
343
dbus_interface=dbus.PROPERTIES_IFACE)
371
self.proxy.Disable(dbus_interface = client_interface,
345
374
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
346
dbus_interface=client_interface,
375
dbus_interface = client_interface,
347
376
ignore_reply=True)
349
378
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
350
dbus_interface=client_interface,
379
dbus_interface = client_interface,
351
380
ignore_reply=True)
352
381
elif key == "R" or key == "_" or key == "ctrl k":
353
382
self.server_proxy_object.RemoveClient(self.proxy
355
384
ignore_reply=True)
357
self.proxy.Set(client_interface, "CheckerRunning",
358
dbus.Boolean(True), ignore_reply=True,
359
dbus_interface=dbus.PROPERTIES_IFACE)
386
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)
389
self.proxy.StopChecker(dbus_interface = client_interface,
365
self.proxy.CheckedOK(dbus_interface=client_interface,
392
self.proxy.CheckedOK(dbus_interface = client_interface,
366
393
ignore_reply=True)
368
395
# elif key == "p" or key == "=":
403
429
"""This is the entire user interface - the whole screen
404
430
with boxes, lists of client widgets, etc.
406
def __init__(self, max_log_length=1000, log_level=1):
432
def __init__(self, max_log_length=1000):
407
433
DBusGMainLoop(set_as_default=True)
409
435
self.screen = urwid.curses_display.Screen()
411
437
self.screen.register_palette((
413
439
"default", "default", None),
415
"bold", "default", "bold"),
441
"default", "default", "bold"),
416
442
("underline-blink",
417
"underline,blink", "default", "underline,blink"),
443
"default", "default", "underline"),
419
"standout", "default", "standout"),
445
"default", "default", "standout"),
420
446
("bold-underline-blink",
421
"bold,underline,blink", "default",
422
"bold,underline,blink"),
447
"default", "default", ("bold", "underline")),
423
448
("bold-standout",
424
"bold,standout", "default", "bold,standout"),
449
"default", "default", ("bold", "standout")),
425
450
("underline-blink-standout",
426
"underline,blink,standout", "default",
427
"underline,blink,standout"),
451
"default", "default", ("underline", "standout")),
428
452
("bold-underline-blink-standout",
429
"bold,underline,blink,standout", "default",
430
"bold,underline,blink,standout"),
453
"default", "default", ("bold", "underline",
433
457
if urwid.supports_unicode():
434
self.divider = "─" # \u2500
458
self.divider = "─" # \u2500
459
#self.divider = "━" # \u2501
436
self.divider = "_" # \u005f
461
#self.divider = "-" # \u002d
462
self.divider = "_" # \u005f
438
464
self.screen.start()
440
466
self.size = self.screen.get_cols_rows()
442
468
self.clients = urwid.SimpleListWalker([])
443
469
self.clients_dict = {}
445
471
# We will add Text widgets to this list
447
473
self.max_log_length = max_log_length
449
self.log_level = log_level
451
475
# We keep a reference to the log widget so we can remove it
452
476
# from the ListWalker without it getting destroyed
453
477
self.logbox = ConstrainedListBox(self.log)
455
479
# This keeps track of whether self.uilist currently has
456
480
# self.logbox in it or not
457
481
self.log_visible = True
458
482
self.log_wrap = "any"
461
485
self.log_message_raw(("bold",
462
486
"Mandos Monitor version " + version))
463
487
self.log_message_raw(("bold",
464
488
"q: Quit ?: Help"))
466
490
self.busname = domain + '.Mandos'
467
self.main_loop = GLib.MainLoop()
491
self.main_loop = gobject.MainLoop()
469
493
def client_not_found(self, fingerprint, address):
470
self.log_message("Client with address {} and fingerprint {}"
471
" could not be found"
494
self.log_message("Client with address {0} and fingerprint"
495
" {1} could not be found"
472
496
.format(address, fingerprint))
474
498
def rebuild(self):
475
499
"""This rebuilds the User Interface.
476
500
Call this when the widget layout needs to change"""
478
# self.uilist.append(urwid.ListBox(self.clients))
502
#self.uilist.append(urwid.ListBox(self.clients))
479
503
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
481
# header=urwid.Divider(),
505
#header=urwid.Divider(),
483
footer=urwid.Divider(
484
div_char=self.divider)))
508
urwid.Divider(div_char=
485
510
if self.log_visible:
486
511
self.uilist.append(self.logbox)
487
512
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:
514
def log_message(self, message):
493
515
timestamp = datetime.datetime.now().isoformat()
494
self.log_message_raw("{}: {}".format(timestamp, message),
497
def log_message_raw(self, markup, level=1):
516
self.log_message_raw(timestamp + ": " + message)
518
def log_message_raw(self, markup):
498
519
"""Add a log message to the log buffer."""
499
if level < self.log_level:
501
520
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]
521
if (self.max_log_length
522
and len(self.log) > self.max_log_length):
523
del self.log[0:len(self.log)-self.max_log_length-1]
505
524
self.logbox.set_focus(len(self.logbox.body.contents),
506
525
coming_from="above")
509
528
def toggle_log_display(self):
510
529
"""Toggle visibility of the log buffer."""
511
530
self.log_visible = not self.log_visible
513
self.log_message("Log visibility changed to: {}"
514
.format(self.log_visible), level=0)
532
#self.log_message("Log visibility changed to: "
533
# + unicode(self.log_visible))
516
535
def change_log_display(self):
517
536
"""Change type of log display.
518
537
Currently, this toggles wrapping of text lines."""
522
541
self.log_wrap = "clip"
523
542
for textwidget in self.log:
524
543
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):
544
#self.log_message("Wrap mode: " + self.log_wrap)
546
def find_and_remove_client(self, path, name):
529
547
"""Find a client by its object path and remove it.
531
This is connected to the InterfacesRemoved signal from the
549
This is connected to the ClientRemoved signal from the
532
550
Mandos server object."""
533
if client_interface not in interfaces:
534
# Not a Mandos client object; ignore
537
552
client = self.clients_dict[path]
540
self.log_message("Unknown client {!r} removed"
555
self.log_message("Unknown client {0!r} ({1!r}) removed"
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
560
def add_new_client(self, path):
554
561
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])),
562
self.add_client(MandosClientWidget(server_proxy_object
565
=client_proxy_object,
564
574
def add_client(self, client, path=None):
565
575
self.clients.append(client)
567
577
path = client.proxy.object_path
568
578
self.clients_dict[path] = client
569
self.clients.sort(key=lambda c: c.properties["Name"])
579
self.clients.sort(None, lambda c: c.properties["Name"])
572
582
def remove_client(self, client, path=None):
573
583
self.clients.remove(client)
575
585
path = client.proxy.object_path
576
586
del self.clients_dict[path]
587
if not self.clients_dict:
588
# Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
589
# is completely emptied, we need to recreate it.
590
self.clients = urwid.SimpleListWalker([])
579
594
def refresh(self):
580
595
"""Redraw the screen"""
581
596
canvas = self.topwidget.render(self.size, focus=True)
582
597
self.screen.draw_screen(self.size, canvas)
585
600
"""Start the main loop and exit when it's done."""
586
601
self.bus = dbus.SystemBus()
587
602
mandos_dbus_objc = self.bus.get_object(
588
603
self.busname, "/", follow_name_owner_changes=True)
589
self.mandos_serv = dbus.Interface(
590
mandos_dbus_objc, dbus_interface=server_interface)
604
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
592
608
mandos_clients = (self.mandos_serv
593
609
.GetAllClientsWithProperties())
594
if not mandos_clients:
595
self.log_message_raw(("bold",
596
"Note: Server has no clients."))
597
610
except dbus.exceptions.DBusException:
598
self.log_message_raw(("bold",
599
"Note: No Mandos server running."))
600
611
mandos_clients = dbus.Dictionary()
602
613
(self.mandos_serv
603
.connect_to_signal("InterfacesRemoved",
614
.connect_to_signal("ClientRemoved",
604
615
self.find_and_remove_client,
605
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
616
dbus_interface=server_interface,
606
617
byte_arrays=True))
607
618
(self.mandos_serv
608
.connect_to_signal("InterfacesAdded",
619
.connect_to_signal("ClientAdded",
609
620
self.add_new_client,
610
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
621
dbus_interface=server_interface,
611
622
byte_arrays=True))
612
623
(self.mandos_serv
613
624
.connect_to_signal("ClientNotFound",
614
625
self.client_not_found,
615
626
dbus_interface=server_interface,
616
627
byte_arrays=True))
617
for path, client in mandos_clients.items():
628
for path, client in mandos_clients.iteritems():
618
629
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),
631
self.add_client(MandosClientWidget(server_proxy_object
634
=client_proxy_object,
630
self._input_callback_tag = (GLib.io_add_watch
645
self._input_callback_tag = (gobject.io_add_watch
631
646
(sys.stdin.fileno(),
633
648
self.process_input))
634
649
self.main_loop.run()
635
650
# Main loop has finished, we should close everything now
636
GLib.source_remove(self._input_callback_tag)
651
gobject.source_remove(self._input_callback_tag)
637
652
self.screen.stop()
640
655
self.main_loop.quit()
642
657
def process_input(self, source, condition):
643
658
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
659
translations = { "ctrl n": "down", # Emacs
660
"ctrl p": "up", # Emacs
661
"ctrl v": "page down", # Emacs
662
"meta v": "page up", # Emacs
663
" ": "page down", # less
664
"f": "page down", # less
665
"b": "page up", # less
656
671
key = translations[key]
657
672
except KeyError: # :-)
660
675
if key == "q" or key == "Q":
663
678
elif key == "window resize":
664
679
self.size = self.screen.get_cols_rows()
666
elif key == "ctrl l":
681
elif key == "\f": # Ctrl-L
669
683
elif key == "l" or key == "D":
670
684
self.toggle_log_display()
704
716
self.topwidget.set_focus(self.logbox)
707
if self.log_level == 0:
709
self.log_message("Verbose mode: Off")
712
self.log_message("Verbose mode: On")
713
# elif (key == "end" or key == "meta >" or key == "G"
715
# pass # xxx end-of-buffer
716
# elif (key == "home" or key == "meta <" or key == "g"
718
# pass # xxx beginning-of-buffer
719
# elif key == "ctrl e" or key == "$":
720
# pass # xxx move-end-of-line
721
# elif key == "ctrl a" or key == "^":
722
# pass # xxx move-beginning-of-line
723
# elif key == "ctrl b" or key == "meta (" or key == "h":
725
# elif key == "ctrl f" or key == "meta )" or key == "l":
728
# pass # scroll up log
730
# pass # scroll down log
718
#elif (key == "end" or key == "meta >" or key == "G"
720
# pass # xxx end-of-buffer
721
#elif (key == "home" or key == "meta <" or key == "g"
723
# pass # xxx beginning-of-buffer
724
#elif key == "ctrl e" or key == "$":
725
# pass # xxx move-end-of-line
726
#elif key == "ctrl a" or key == "^":
727
# pass # xxx move-beginning-of-line
728
#elif key == "ctrl b" or key == "meta (" or key == "h":
730
#elif key == "ctrl f" or key == "meta )" or key == "l":
733
# pass # scroll up log
735
# pass # scroll down log
731
736
elif self.topwidget.selectable():
732
737
self.topwidget.keypress(self.size, key)
737
741
ui = UserInterface()
740
744
except KeyboardInterrupt:
742
except Exception as e:
743
ui.log_message(str(e))
747
ui.log_message(unicode(e))