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
173
client_interface,
164
174
byte_arrays=True))
165
self.logger('Created client {}'
166
.format(self.properties["Name"]), level=0)
175
#self.logger('Created client %s' % (self.properties["Name"]))
177
def property_changed(self, property=None, value=None):
178
super(self, MandosClientWidget).property_changed(property,
180
if property == "ApprovalPending":
181
using_timer(bool(value))
182
if property == "LastCheckerStatus":
183
using_timer(value != 0)
184
#self.logger('Checker for client %s (command "%s")'
186
# % (self.properties["Name"], command))
168
188
def using_timer(self, flag):
169
189
"""Call this method with True or False when timer should be
170
190
activated or deactivated.
172
if flag and self._update_timer_callback_tag is None:
192
old = self._update_timer_callback_lock
194
self._update_timer_callback_lock += 1
196
self._update_timer_callback_lock -= 1
197
if old == 0 and self._update_timer_callback_lock:
173
198
# Will update the shown timer value every second
174
self._update_timer_callback_tag = (GLib.timeout_add
199
self._update_timer_callback_tag = (gobject.timeout_add
176
201
self.update_timer))
177
elif not (flag or self._update_timer_callback_tag is None):
178
GLib.source_remove(self._update_timer_callback_tag)
202
elif old and self._update_timer_callback_lock == 0:
203
gobject.source_remove(self._update_timer_callback_tag)
179
204
self._update_timer_callback_tag = None
181
206
def checker_completed(self, exitstatus, condition, command):
182
207
if exitstatus == 0:
183
self.logger('Checker for client {} (command "{}")'
184
' succeeded'.format(self.properties["Name"],
189
211
if os.WIFEXITED(condition):
190
self.logger('Checker for client {} (command "{}") failed'
192
.format(self.properties["Name"], command,
193
os.WEXITSTATUS(condition)))
212
self.logger('Checker for client %s (command "%s")'
213
' failed with exit code %s'
214
% (self.properties["Name"], command,
215
os.WEXITSTATUS(condition)))
194
216
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)))
217
self.logger('Checker for client %s (command "%s")'
218
' was killed by signal %s'
219
% (self.properties["Name"], command,
220
os.WTERMSIG(condition)))
221
elif os.WCOREDUMP(condition):
222
self.logger('Checker for client %s (command "%s")'
224
% (self.properties["Name"], command))
226
self.logger('Checker for client %s completed'
201
230
def checker_started(self, command):
202
"""Server signals that a checker started."""
203
self.logger('Client {} started checker "{}"'
204
.format(self.properties["Name"],
231
"""Server signals that a checker started. This could be useful
232
to log in the future. """
233
#self.logger('Client %s started checker "%s"'
234
# % (self.properties["Name"], unicode(command)))
207
237
def got_secret(self):
208
self.logger('Client {} received its secret'
209
.format(self.properties["Name"]))
238
self.logger('Client %s received its secret'
239
% self.properties["Name"])
211
241
def need_approval(self, timeout, default):
213
message = 'Client {} needs approval within {} seconds'
243
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"],
245
message = 'Client %s will get its secret in %s seconds'
247
% (self.properties["Name"], timeout/1000))
248
self.using_timer(True)
219
250
def rejected(self, reason):
220
self.logger('Client {} was rejected; reason: {}'
221
.format(self.properties["Name"], reason))
251
self.logger('Client %s was rejected; reason: %s'
252
% (self.properties["Name"], reason))
223
254
def selectable(self):
224
255
"""Make this a "selectable" widget.
225
256
This overrides the method from urwid.FlowWidget."""
228
259
def rows(self, maxcolrow, focus=False):
229
260
"""How many rows this widget will occupy might depend on
230
261
whether we have focus or not.
231
262
This overrides the method from urwid.FlowWidget"""
232
263
return self.current_widget(focus).rows(maxcolrow, focus=focus)
234
265
def current_widget(self, focus=False):
235
266
if focus or self.opened:
236
267
return self._focus_widget
237
268
return self._widget
239
270
def update(self):
240
271
"Called when what is visible on the screen should be updated."
241
272
# 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",
273
with_standout = { "normal": "standout",
274
"bold": "bold-standout",
276
"underline-blink-standout",
277
"bold-underline-blink":
278
"bold-underline-blink-standout",
250
281
# Rebuild focus and non-focus widgets using current properties
252
283
# Base part of a client. Name!
253
base = '{name}: '.format(name=self.properties["Name"])
285
% {"name": self.properties["Name"]})
254
286
if not self.properties["Enabled"]:
255
287
message = "DISABLED"
256
self.using_timer(False)
257
288
elif self.properties["ApprovalPending"]:
258
timeout = datetime.timedelta(
259
milliseconds=self.properties["ApprovalDelay"])
289
timeout = datetime.timedelta(milliseconds
260
292
last_approval_request = isoformat_to_datetime(
261
293
self.properties["LastApprovalRequest"])
262
294
if last_approval_request is not None:
263
timer = max(timeout - (datetime.datetime.utcnow()
264
- last_approval_request),
265
datetime.timedelta())
295
timer = timeout - (datetime.datetime.utcnow()
296
- last_approval_request)
267
298
timer = datetime.timedelta()
268
299
if self.properties["ApprovedByDefault"]:
269
message = "Approval in {}. (d)eny?"
300
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)
302
message = "Denial in %s. (a)pprove?"
303
message = message % unicode(timer).rsplit(".", 1)[0]
274
304
elif self.properties["LastCheckerStatus"] != 0:
275
# When checker has failed, show timer until client expires
305
# When checker has failed, print a timer until client expires
276
306
expires = self.properties["Expires"]
277
307
if expires == "":
278
308
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())
310
expires = datetime.datetime.strptime(expires,
311
'%Y-%m-%dT%H:%M:%S.%f')
312
timer = expires - datetime.datetime.utcnow()
284
313
message = ('A checker has failed! Time until client'
286
.format(str(timer).rsplit(".", 1)[0]))
287
self.using_timer(True)
315
% unicode(timer).rsplit(".", 1)[0])
289
317
message = "enabled"
290
self.using_timer(False)
291
self._text = "{}{}".format(base, message)
318
self._text = "%s%s" % (base, message)
293
320
if not urwid.supports_unicode():
294
321
self._text = self._text.encode("ascii", "replace")
295
322
textlist = [("normal", self._text)]
305
332
# Run update hook, if any
306
333
if self.update_hook is not None:
307
334
self.update_hook()
309
336
def update_timer(self):
310
"""called by GLib. Will indefinitely loop until
311
GLib.source_remove() on tag is called
337
"""called by gobject. Will indefinitely loop until
338
gobject.source_remove() on tag is called"""
314
340
return True # Keep calling this
316
def delete(self, **kwargs):
342
def delete(self, *args, **kwargs):
317
343
if self._update_timer_callback_tag is not None:
318
GLib.source_remove(self._update_timer_callback_tag)
344
gobject.source_remove(self._update_timer_callback_tag)
319
345
self._update_timer_callback_tag = None
320
346
for match in self.match_objects:
322
348
self.match_objects = ()
323
349
if self.delete_hook is not None:
324
350
self.delete_hook(self)
325
return super(MandosClientWidget, self).delete(**kwargs)
351
return super(MandosClientWidget, self).delete(*args, **kwargs)
327
353
def render(self, maxcolrow, focus=False):
328
354
"""Render differently if we have focus.
329
355
This overrides the method from urwid.FlowWidget"""
330
356
return self.current_widget(focus).render(maxcolrow,
333
359
def keypress(self, maxcolrow, key):
335
361
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)
363
self.proxy.Enable(dbus_interface = client_interface,
341
self.proxy.Set(client_interface, "Enabled", False,
343
dbus_interface=dbus.PROPERTIES_IFACE)
366
self.proxy.Disable(dbus_interface = client_interface,
345
369
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
346
dbus_interface=client_interface,
370
dbus_interface = client_interface,
347
371
ignore_reply=True)
349
373
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
350
dbus_interface=client_interface,
374
dbus_interface = client_interface,
351
375
ignore_reply=True)
352
376
elif key == "R" or key == "_" or key == "ctrl k":
353
377
self.server_proxy_object.RemoveClient(self.proxy
355
379
ignore_reply=True)
357
self.proxy.Set(client_interface, "CheckerRunning",
358
dbus.Boolean(True), ignore_reply=True,
359
dbus_interface=dbus.PROPERTIES_IFACE)
381
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)
384
self.proxy.StopChecker(dbus_interface = client_interface,
365
self.proxy.CheckedOK(dbus_interface=client_interface,
387
self.proxy.CheckedOK(dbus_interface = client_interface,
366
388
ignore_reply=True)
368
390
# elif key == "p" or key == "=":
403
424
"""This is the entire user interface - the whole screen
404
425
with boxes, lists of client widgets, etc.
406
def __init__(self, max_log_length=1000, log_level=1):
427
def __init__(self, max_log_length=1000):
407
428
DBusGMainLoop(set_as_default=True)
409
430
self.screen = urwid.curses_display.Screen()
411
432
self.screen.register_palette((
413
434
"default", "default", None),
415
"bold", "default", "bold"),
436
"default", "default", "bold"),
416
437
("underline-blink",
417
"underline,blink", "default", "underline,blink"),
438
"default", "default", "underline"),
419
"standout", "default", "standout"),
440
"default", "default", "standout"),
420
441
("bold-underline-blink",
421
"bold,underline,blink", "default",
422
"bold,underline,blink"),
442
"default", "default", ("bold", "underline")),
423
443
("bold-standout",
424
"bold,standout", "default", "bold,standout"),
444
"default", "default", ("bold", "standout")),
425
445
("underline-blink-standout",
426
"underline,blink,standout", "default",
427
"underline,blink,standout"),
446
"default", "default", ("underline", "standout")),
428
447
("bold-underline-blink-standout",
429
"bold,underline,blink,standout", "default",
430
"bold,underline,blink,standout"),
448
"default", "default", ("bold", "underline",
433
452
if urwid.supports_unicode():
434
self.divider = "─" # \u2500
453
self.divider = "─" # \u2500
454
#self.divider = "━" # \u2501
436
self.divider = "_" # \u005f
456
#self.divider = "-" # \u002d
457
self.divider = "_" # \u005f
438
459
self.screen.start()
440
461
self.size = self.screen.get_cols_rows()
442
463
self.clients = urwid.SimpleListWalker([])
443
464
self.clients_dict = {}
445
466
# We will add Text widgets to this list
447
468
self.max_log_length = max_log_length
449
self.log_level = log_level
451
470
# We keep a reference to the log widget so we can remove it
452
471
# from the ListWalker without it getting destroyed
453
472
self.logbox = ConstrainedListBox(self.log)
455
474
# This keeps track of whether self.uilist currently has
456
475
# self.logbox in it or not
457
476
self.log_visible = True
458
477
self.log_wrap = "any"
461
480
self.log_message_raw(("bold",
462
481
"Mandos Monitor version " + version))
463
482
self.log_message_raw(("bold",
464
483
"q: Quit ?: Help"))
466
485
self.busname = domain + '.Mandos'
467
self.main_loop = GLib.MainLoop()
486
self.main_loop = gobject.MainLoop()
469
488
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))
489
self.log_message(("Client with address %s and fingerprint %s"
490
" could not be found" % (address,
474
493
def rebuild(self):
475
494
"""This rebuilds the User Interface.
476
495
Call this when the widget layout needs to change"""
478
# self.uilist.append(urwid.ListBox(self.clients))
497
#self.uilist.append(urwid.ListBox(self.clients))
479
498
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
481
# header=urwid.Divider(),
500
#header=urwid.Divider(),
483
footer=urwid.Divider(
484
div_char=self.divider)))
503
urwid.Divider(div_char=
485
505
if self.log_visible:
486
506
self.uilist.append(self.logbox)
487
507
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:
509
def log_message(self, message):
493
510
timestamp = datetime.datetime.now().isoformat()
494
self.log_message_raw("{}: {}".format(timestamp, message),
497
def log_message_raw(self, markup, level=1):
511
self.log_message_raw(timestamp + ": " + message)
513
def log_message_raw(self, markup):
498
514
"""Add a log message to the log buffer."""
499
if level < self.log_level:
501
515
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]
516
if (self.max_log_length
517
and len(self.log) > self.max_log_length):
518
del self.log[0:len(self.log)-self.max_log_length-1]
505
519
self.logbox.set_focus(len(self.logbox.body.contents),
506
520
coming_from="above")
509
523
def toggle_log_display(self):
510
524
"""Toggle visibility of the log buffer."""
511
525
self.log_visible = not self.log_visible
513
self.log_message("Log visibility changed to: {}"
514
.format(self.log_visible), level=0)
527
#self.log_message("Log visibility changed to: "
528
# + unicode(self.log_visible))
516
530
def change_log_display(self):
517
531
"""Change type of log display.
518
532
Currently, this toggles wrapping of text lines."""
522
536
self.log_wrap = "clip"
523
537
for textwidget in self.log:
524
538
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):
539
#self.log_message("Wrap mode: " + self.log_wrap)
541
def find_and_remove_client(self, path, name):
529
542
"""Find a client by its object path and remove it.
531
This is connected to the InterfacesRemoved signal from the
544
This is connected to the ClientRemoved signal from the
532
545
Mandos server object."""
533
if client_interface not in interfaces:
534
# Not a Mandos client object; ignore
537
547
client = self.clients_dict[path]
540
self.log_message("Unknown client {!r} removed"
550
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
555
def add_new_client(self, path):
554
556
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])),
557
self.add_client(MandosClientWidget(server_proxy_object
560
=client_proxy_object,
564
569
def add_client(self, client, path=None):
565
570
self.clients.append(client)
567
572
path = client.proxy.object_path
568
573
self.clients_dict[path] = client
569
self.clients.sort(key=lambda c: c.properties["Name"])
574
self.clients.sort(None, lambda c: c.properties["Name"])
572
577
def remove_client(self, client, path=None):
573
578
self.clients.remove(client)
575
580
path = client.proxy.object_path
576
581
del self.clients_dict[path]
582
if not self.clients_dict:
583
# Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
584
# is completely emptied, we need to recreate it.
585
self.clients = urwid.SimpleListWalker([])
579
589
def refresh(self):
580
590
"""Redraw the screen"""
581
591
canvas = self.topwidget.render(self.size, focus=True)
582
592
self.screen.draw_screen(self.size, canvas)
585
595
"""Start the main loop and exit when it's done."""
586
596
self.bus = dbus.SystemBus()
587
597
mandos_dbus_objc = self.bus.get_object(
588
598
self.busname, "/", follow_name_owner_changes=True)
589
self.mandos_serv = dbus.Interface(
590
mandos_dbus_objc, dbus_interface=server_interface)
599
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
592
603
mandos_clients = (self.mandos_serv
593
604
.GetAllClientsWithProperties())
594
if not mandos_clients:
595
self.log_message_raw(("bold",
596
"Note: Server has no clients."))
597
605
except dbus.exceptions.DBusException:
598
self.log_message_raw(("bold",
599
"Note: No Mandos server running."))
600
606
mandos_clients = dbus.Dictionary()
602
608
(self.mandos_serv
603
.connect_to_signal("InterfacesRemoved",
609
.connect_to_signal("ClientRemoved",
604
610
self.find_and_remove_client,
605
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
611
dbus_interface=server_interface,
606
612
byte_arrays=True))
607
613
(self.mandos_serv
608
.connect_to_signal("InterfacesAdded",
614
.connect_to_signal("ClientAdded",
609
615
self.add_new_client,
610
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
616
dbus_interface=server_interface,
611
617
byte_arrays=True))
612
618
(self.mandos_serv
613
619
.connect_to_signal("ClientNotFound",
614
620
self.client_not_found,
615
621
dbus_interface=server_interface,
616
622
byte_arrays=True))
617
for path, client in mandos_clients.items():
623
for path, client in mandos_clients.iteritems():
618
624
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),
626
self.add_client(MandosClientWidget(server_proxy_object
629
=client_proxy_object,
630
self._input_callback_tag = (GLib.io_add_watch
640
self._input_callback_tag = (gobject.io_add_watch
631
641
(sys.stdin.fileno(),
633
643
self.process_input))
634
644
self.main_loop.run()
635
645
# Main loop has finished, we should close everything now
636
GLib.source_remove(self._input_callback_tag)
646
gobject.source_remove(self._input_callback_tag)
637
647
self.screen.stop()
640
650
self.main_loop.quit()
642
652
def process_input(self, source, condition):
643
653
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
654
translations = { "ctrl n": "down", # Emacs
655
"ctrl p": "up", # Emacs
656
"ctrl v": "page down", # Emacs
657
"meta v": "page up", # Emacs
658
" ": "page down", # less
659
"f": "page down", # less
660
"b": "page up", # less
656
666
key = translations[key]
657
667
except KeyError: # :-)
660
670
if key == "q" or key == "Q":
663
673
elif key == "window resize":
664
674
self.size = self.screen.get_cols_rows()
666
elif key == "ctrl l":
676
elif key == "\f": # Ctrl-L
669
678
elif key == "l" or key == "D":
670
679
self.toggle_log_display()
704
711
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
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
731
731
elif self.topwidget.selectable():
732
732
self.topwidget.keypress(self.size, key)
737
736
ui = UserInterface()
740
739
except KeyboardInterrupt:
742
except Exception as e:
743
ui.log_message(str(e))
742
ui.log_message(unicode(e))