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
#self.logger('Client %s started checker "%s"'
246
# % (self.properties["Name"], unicode(command)))
207
249
def got_secret(self):
208
self.logger('Client {} received its secret'
209
.format(self.properties["Name"]))
250
self.last_checker_failed = False
251
self.logger('Client %s received its secret'
252
% self.properties["Name"])
211
254
def need_approval(self, timeout, default):
213
message = 'Client {} needs approval within {} seconds'
256
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"],
258
message = 'Client %s will get its secret in %s seconds'
260
% (self.properties["Name"], timeout/1000))
261
self.using_timer(True)
219
263
def rejected(self, reason):
220
self.logger('Client {} was rejected; reason: {}'
221
.format(self.properties["Name"], reason))
264
self.logger('Client %s was rejected; reason: %s'
265
% (self.properties["Name"], reason))
223
267
def selectable(self):
224
268
"""Make this a "selectable" widget.
225
269
This overrides the method from urwid.FlowWidget."""
228
272
def rows(self, maxcolrow, focus=False):
229
273
"""How many rows this widget will occupy might depend on
230
274
whether we have focus or not.
231
275
This overrides the method from urwid.FlowWidget"""
232
276
return self.current_widget(focus).rows(maxcolrow, focus=focus)
234
278
def current_widget(self, focus=False):
235
279
if focus or self.opened:
236
280
return self._focus_widget
237
281
return self._widget
239
283
def update(self):
240
284
"Called when what is visible on the screen should be updated."
241
285
# 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",
286
with_standout = { "normal": "standout",
287
"bold": "bold-standout",
289
"underline-blink-standout",
290
"bold-underline-blink":
291
"bold-underline-blink-standout",
250
294
# Rebuild focus and non-focus widgets using current properties
252
296
# Base part of a client. Name!
253
base = '{name}: '.format(name=self.properties["Name"])
298
% {"name": self.properties["Name"]})
254
299
if not self.properties["Enabled"]:
255
300
message = "DISABLED"
256
self.using_timer(False)
257
301
elif self.properties["ApprovalPending"]:
258
timeout = datetime.timedelta(
259
milliseconds=self.properties["ApprovalDelay"])
302
timeout = datetime.timedelta(milliseconds
260
305
last_approval_request = isoformat_to_datetime(
261
306
self.properties["LastApprovalRequest"])
262
307
if last_approval_request is not None:
263
timer = max(timeout - (datetime.datetime.utcnow()
264
- last_approval_request),
265
datetime.timedelta())
308
timer = timeout - (datetime.datetime.utcnow()
309
- last_approval_request)
267
311
timer = datetime.timedelta()
268
312
if self.properties["ApprovedByDefault"]:
269
message = "Approval in {}. (d)eny?"
313
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
315
message = "Denial in %s. (a)pprove?"
316
message = message % unicode(timer).rsplit(".", 1)[0]
317
elif self.last_checker_failed:
318
# When checker has failed, print a timer until client expires
276
319
expires = self.properties["Expires"]
277
320
if expires == "":
278
321
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())
323
expires = datetime.datetime.strptime(expires,
324
'%Y-%m-%dT%H:%M:%S.%f')
325
timer = expires - datetime.datetime.utcnow()
284
326
message = ('A checker has failed! Time until client'
286
.format(str(timer).rsplit(".", 1)[0]))
287
self.using_timer(True)
328
% unicode(timer).rsplit(".", 1)[0])
289
330
message = "enabled"
290
self.using_timer(False)
291
self._text = "{}{}".format(base, message)
331
self._text = "%s%s" % (base, message)
293
333
if not urwid.supports_unicode():
294
334
self._text = self._text.encode("ascii", "replace")
295
335
textlist = [("normal", self._text)]
305
345
# Run update hook, if any
306
346
if self.update_hook is not None:
307
347
self.update_hook()
309
349
def update_timer(self):
310
"""called by GLib. Will indefinitely loop until
311
GLib.source_remove() on tag is called
350
"""called by gobject. Will indefinitely loop until
351
gobject.source_remove() on tag is called"""
314
353
return True # Keep calling this
316
def delete(self, **kwargs):
355
def delete(self, *args, **kwargs):
317
356
if self._update_timer_callback_tag is not None:
318
GLib.source_remove(self._update_timer_callback_tag)
357
gobject.source_remove(self._update_timer_callback_tag)
319
358
self._update_timer_callback_tag = None
320
359
for match in self.match_objects:
322
361
self.match_objects = ()
323
362
if self.delete_hook is not None:
324
363
self.delete_hook(self)
325
return super(MandosClientWidget, self).delete(**kwargs)
364
return super(MandosClientWidget, self).delete(*args, **kwargs)
327
366
def render(self, maxcolrow, focus=False):
328
367
"""Render differently if we have focus.
329
368
This overrides the method from urwid.FlowWidget"""
330
369
return self.current_widget(focus).render(maxcolrow,
333
372
def keypress(self, maxcolrow, key):
335
374
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)
376
self.proxy.Enable(dbus_interface = client_interface,
341
self.proxy.Set(client_interface, "Enabled", False,
343
dbus_interface=dbus.PROPERTIES_IFACE)
379
self.proxy.Disable(dbus_interface = client_interface,
345
382
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
346
dbus_interface=client_interface,
383
dbus_interface = client_interface,
347
384
ignore_reply=True)
349
386
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
350
dbus_interface=client_interface,
387
dbus_interface = client_interface,
351
388
ignore_reply=True)
352
389
elif key == "R" or key == "_" or key == "ctrl k":
353
390
self.server_proxy_object.RemoveClient(self.proxy
355
392
ignore_reply=True)
357
self.proxy.Set(client_interface, "CheckerRunning",
358
dbus.Boolean(True), ignore_reply=True,
359
dbus_interface=dbus.PROPERTIES_IFACE)
394
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)
397
self.proxy.StopChecker(dbus_interface = client_interface,
365
self.proxy.CheckedOK(dbus_interface=client_interface,
400
self.proxy.CheckedOK(dbus_interface = client_interface,
366
401
ignore_reply=True)
368
403
# elif key == "p" or key == "=":
403
437
"""This is the entire user interface - the whole screen
404
438
with boxes, lists of client widgets, etc.
406
def __init__(self, max_log_length=1000, log_level=1):
440
def __init__(self, max_log_length=1000):
407
441
DBusGMainLoop(set_as_default=True)
409
443
self.screen = urwid.curses_display.Screen()
411
445
self.screen.register_palette((
413
447
"default", "default", None),
415
"bold", "default", "bold"),
449
"default", "default", "bold"),
416
450
("underline-blink",
417
"underline,blink", "default", "underline,blink"),
451
"default", "default", "underline"),
419
"standout", "default", "standout"),
453
"default", "default", "standout"),
420
454
("bold-underline-blink",
421
"bold,underline,blink", "default",
422
"bold,underline,blink"),
455
"default", "default", ("bold", "underline")),
423
456
("bold-standout",
424
"bold,standout", "default", "bold,standout"),
457
"default", "default", ("bold", "standout")),
425
458
("underline-blink-standout",
426
"underline,blink,standout", "default",
427
"underline,blink,standout"),
459
"default", "default", ("underline", "standout")),
428
460
("bold-underline-blink-standout",
429
"bold,underline,blink,standout", "default",
430
"bold,underline,blink,standout"),
461
"default", "default", ("bold", "underline",
433
465
if urwid.supports_unicode():
434
self.divider = "─" # \u2500
466
self.divider = "─" # \u2500
467
#self.divider = "━" # \u2501
436
self.divider = "_" # \u005f
469
#self.divider = "-" # \u002d
470
self.divider = "_" # \u005f
438
472
self.screen.start()
440
474
self.size = self.screen.get_cols_rows()
442
476
self.clients = urwid.SimpleListWalker([])
443
477
self.clients_dict = {}
445
479
# We will add Text widgets to this list
447
481
self.max_log_length = max_log_length
449
self.log_level = log_level
451
483
# We keep a reference to the log widget so we can remove it
452
484
# from the ListWalker without it getting destroyed
453
485
self.logbox = ConstrainedListBox(self.log)
455
487
# This keeps track of whether self.uilist currently has
456
488
# self.logbox in it or not
457
489
self.log_visible = True
458
490
self.log_wrap = "any"
461
493
self.log_message_raw(("bold",
462
494
"Mandos Monitor version " + version))
463
495
self.log_message_raw(("bold",
464
496
"q: Quit ?: Help"))
466
498
self.busname = domain + '.Mandos'
467
self.main_loop = GLib.MainLoop()
499
self.main_loop = gobject.MainLoop()
500
self.bus = dbus.SystemBus()
501
mandos_dbus_objc = self.bus.get_object(
502
self.busname, "/", follow_name_owner_changes=True)
503
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
507
mandos_clients = (self.mandos_serv
508
.GetAllClientsWithProperties())
509
except dbus.exceptions.DBusException:
510
mandos_clients = dbus.Dictionary()
513
.connect_to_signal("ClientRemoved",
514
self.find_and_remove_client,
515
dbus_interface=server_interface,
518
.connect_to_signal("ClientAdded",
520
dbus_interface=server_interface,
523
.connect_to_signal("ClientNotFound",
524
self.client_not_found,
525
dbus_interface=server_interface,
527
for path, client in mandos_clients.iteritems():
528
client_proxy_object = self.bus.get_object(self.busname,
530
self.add_client(MandosClientWidget(server_proxy_object
533
=client_proxy_object,
469
543
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))
544
self.log_message(("Client with address %s and fingerprint %s"
545
" could not be found" % (address,
474
548
def rebuild(self):
475
549
"""This rebuilds the User Interface.
476
550
Call this when the widget layout needs to change"""
478
# self.uilist.append(urwid.ListBox(self.clients))
552
#self.uilist.append(urwid.ListBox(self.clients))
479
553
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
481
# header=urwid.Divider(),
555
#header=urwid.Divider(),
483
footer=urwid.Divider(
484
div_char=self.divider)))
558
urwid.Divider(div_char=
485
560
if self.log_visible:
486
561
self.uilist.append(self.logbox)
487
563
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:
565
def log_message(self, message):
493
566
timestamp = datetime.datetime.now().isoformat()
494
self.log_message_raw("{}: {}".format(timestamp, message),
497
def log_message_raw(self, markup, level=1):
567
self.log_message_raw(timestamp + ": " + message)
569
def log_message_raw(self, markup):
498
570
"""Add a log message to the log buffer."""
499
if level < self.log_level:
501
571
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]
572
if (self.max_log_length
573
and len(self.log) > self.max_log_length):
574
del self.log[0:len(self.log)-self.max_log_length-1]
505
575
self.logbox.set_focus(len(self.logbox.body.contents),
506
576
coming_from="above")
509
579
def toggle_log_display(self):
510
580
"""Toggle visibility of the log buffer."""
511
581
self.log_visible = not self.log_visible
513
self.log_message("Log visibility changed to: {}"
514
.format(self.log_visible), level=0)
583
#self.log_message("Log visibility changed to: "
584
# + unicode(self.log_visible))
516
586
def change_log_display(self):
517
587
"""Change type of log display.
518
588
Currently, this toggles wrapping of text lines."""
522
592
self.log_wrap = "clip"
523
593
for textwidget in self.log:
524
594
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):
595
#self.log_message("Wrap mode: " + self.log_wrap)
597
def find_and_remove_client(self, path, name):
529
598
"""Find a client by its object path and remove it.
531
This is connected to the InterfacesRemoved signal from the
600
This is connected to the ClientRemoved signal from the
532
601
Mandos server object."""
533
if client_interface not in interfaces:
534
# Not a Mandos client object; ignore
537
603
client = self.clients_dict[path]
540
self.log_message("Unknown client {!r} removed"
606
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
611
def add_new_client(self, path):
554
612
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])),
613
self.add_client(MandosClientWidget(server_proxy_object
616
=client_proxy_object,
564
625
def add_client(self, client, path=None):
565
626
self.clients.append(client)
567
628
path = client.proxy.object_path
568
629
self.clients_dict[path] = client
569
self.clients.sort(key=lambda c: c.properties["Name"])
630
self.clients.sort(None, lambda c: c.properties["Name"])
572
633
def remove_client(self, client, path=None):
573
634
self.clients.remove(client)
575
636
path = client.proxy.object_path
576
637
del self.clients_dict[path]
638
if not self.clients_dict:
639
# Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
640
# is completely emptied, we need to recreate it.
641
self.clients = urwid.SimpleListWalker([])
579
645
def refresh(self):
580
646
"""Redraw the screen"""
581
647
canvas = self.topwidget.render(self.size, focus=True)
582
648
self.screen.draw_screen(self.size, canvas)
585
651
"""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
653
self._input_callback_tag = (gobject.io_add_watch
631
654
(sys.stdin.fileno(),
633
656
self.process_input))
634
657
self.main_loop.run()
635
658
# Main loop has finished, we should close everything now
636
GLib.source_remove(self._input_callback_tag)
659
gobject.source_remove(self._input_callback_tag)
637
660
self.screen.stop()
640
663
self.main_loop.quit()
642
665
def process_input(self, source, condition):
643
666
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
667
translations = { "ctrl n": "down", # Emacs
668
"ctrl p": "up", # Emacs
669
"ctrl v": "page down", # Emacs
670
"meta v": "page up", # Emacs
671
" ": "page down", # less
672
"f": "page down", # less
673
"b": "page up", # less
656
679
key = translations[key]
657
680
except KeyError: # :-)
660
683
if key == "q" or key == "Q":
663
686
elif key == "window resize":
664
687
self.size = self.screen.get_cols_rows()
666
elif key == "ctrl l":
689
elif key == "\f": # Ctrl-L
669
691
elif key == "l" or key == "D":
670
692
self.toggle_log_display()