2
# -*- mode: python; coding: utf-8 -*-
4
from __future__ import division, absolute_import, with_statement
12
import urwid.curses_display
15
from dbus.mainloop.glib import DBusGMainLoop
24
locale.setlocale(locale.LC_ALL, u'')
27
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
29
# Some useful constants
30
domain = 'se.bsnet.fukt'
31
server_interface = domain + '.Mandos'
32
client_interface = domain + '.Mandos.Client'
35
# Always run in monochrome mode
36
urwid.curses_display.curses.has_colors = lambda : False
38
# Urwid doesn't support blinking, but we want it. Since we have no
39
# use for underline on its own, we make underline also always blink.
40
urwid.curses_display.curses.A_UNDERLINE |= (
41
urwid.curses_display.curses.A_BLINK)
43
def isoformat_to_datetime(iso):
44
"Parse an ISO 8601 date string to a datetime.datetime()"
47
d, t = iso.split(u"T", 1)
48
year, month, day = d.split(u"-", 2)
49
hour, minute, second = t.split(u":", 2)
50
second, fraction = divmod(float(second), 1)
51
return datetime.datetime(int(year),
56
int(second), # Whole seconds
57
int(fraction*1000000)) # Microseconds
59
class MandosClientPropertyCache(object):
60
"""This wraps a Mandos Client D-Bus proxy object, caches the
61
properties and calls a hook function when any of them are
64
def __init__(self, proxy_object=None, *args, **kwargs):
65
self.proxy = proxy_object # Mandos Client proxy object
67
self.properties = dict()
68
self.proxy.connect_to_signal(u"PropertyChanged",
69
self.property_changed,
73
self.properties.update(
74
self.proxy.GetAll(client_interface,
75
dbus_interface = dbus.PROPERTIES_IFACE))
77
#XXX This break good super behaviour!
78
# super(MandosClientPropertyCache, self).__init__(
81
def property_changed(self, property=None, value=None):
82
"""This is called whenever we get a PropertyChanged signal
83
It updates the changed property in the "properties" dict.
85
# Update properties dict with new value
86
self.properties[property] = value
89
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
90
"""A Mandos Client which is visible on the screen.
93
def __init__(self, server_proxy_object=None, update_hook=None,
94
delete_hook=None, logger=None, *args, **kwargs):
96
self.update_hook = update_hook
98
self.delete_hook = delete_hook
99
# Mandos Server proxy object
100
self.server_proxy_object = server_proxy_object
104
self._update_timer_callback_tag = None
105
self.last_checker_failed = False
107
# The widget shown normally
108
self._text_widget = urwid.Text(u"")
109
# The widget shown when we have focus
110
self._focus_text_widget = urwid.Text(u"")
111
super(MandosClientWidget, self).__init__(
112
update_hook=update_hook, delete_hook=delete_hook,
116
self.proxy.connect_to_signal(u"CheckerCompleted",
117
self.checker_completed,
120
self.proxy.connect_to_signal(u"CheckerStarted",
121
self.checker_started,
124
self.proxy.connect_to_signal(u"GotSecret",
128
self.proxy.connect_to_signal(u"NeedApproval",
132
self.proxy.connect_to_signal(u"Rejected",
136
last_checked_ok = isoformat_to_datetime(self.properties
138
if last_checked_ok is None:
139
self.last_checker_failed = True
141
self.last_checker_failed = ((datetime.datetime.utcnow()
145
self.properties["interval"]))
146
if self.last_checker_failed:
147
self._update_timer_callback_tag = (gobject.timeout_add
151
def checker_completed(self, exitstatus, condition, command):
153
if self.last_checker_failed:
154
self.last_checker_failed = False
155
gobject.source_remove(self._update_timer_callback_tag)
156
self._update_timer_callback_tag = None
157
self.logger(u'Checker for client %s (command "%s")'
159
% (self.properties[u"name"], command))
163
if not self.last_checker_failed:
164
self.last_checker_failed = True
165
self._update_timer_callback_tag = (gobject.timeout_add
168
if os.WIFEXITED(condition):
169
self.logger(u'Checker for client %s (command "%s")'
170
u' failed with exit code %s'
171
% (self.properties[u"name"], command,
172
os.WEXITSTATUS(condition)))
173
elif os.WIFSIGNALED(condition):
174
self.logger(u'Checker for client %s (command "%s")'
175
u' was killed by signal %s'
176
% (self.properties[u"name"], command,
177
os.WTERMSIG(condition)))
178
elif os.WCOREDUMP(condition):
179
self.logger(u'Checker for client %s (command "%s")'
181
% (self.properties[u"name"], command))
183
self.logger(u'Checker for client %s completed mysteriously')
186
def checker_started(self, command):
187
#self.logger(u'Client %s started checker "%s"'
188
# % (self.properties[u"name"], unicode(command)))
191
def got_secret(self):
192
self.logger(u'Client %s received its secret'
193
% self.properties[u"name"])
195
def need_approval(self, timeout, default):
197
message = u'Client %s needs approval within %s seconds'
199
message = u'Client %s will get its secret in %s seconds'
201
% (self.properties[u"name"], timeout/1000))
203
def rejected(self, reason):
204
self.logger(u'Client %s was rejected; reason: %s'
205
% (self.properties[u"name"], reason))
207
def selectable(self):
208
"""Make this a "selectable" widget.
209
This overrides the method from urwid.FlowWidget."""
212
def rows(self, (maxcol,), focus=False):
213
"""How many rows this widget will occupy might depend on
214
whether we have focus or not.
215
This overrides the method from urwid.FlowWidget"""
216
return self.current_widget(focus).rows((maxcol,), focus=focus)
218
def current_widget(self, focus=False):
219
if focus or self.opened:
220
return self._focus_widget
224
"Called when what is visible on the screen should be updated."
225
# How to add standout mode to a style
226
with_standout = { u"normal": u"standout",
227
u"bold": u"bold-standout",
229
u"underline-blink-standout",
230
u"bold-underline-blink":
231
u"bold-underline-blink-standout",
234
# Rebuild focus and non-focus widgets using current properties
236
# Base part of a client. Name!
237
base = (u'%(name)s: '
238
% {u"name": self.properties[u"name"]})
239
if not self.properties[u"enabled"]:
240
message = u"DISABLED"
241
elif self.last_checker_failed:
242
timeout = datetime.timedelta(milliseconds
243
= self.properties[u"timeout"])
244
last_ok = isoformat_to_datetime(
245
max((self.properties["last_checked_ok"]
246
or self.properties["created"]),
247
self.properties[u"last_enabled"]))
248
timer = timeout - (datetime.datetime.utcnow() - last_ok)
249
message = (u'A checker has failed! Time until client gets diabled: %s'
250
% unicode(timer).rsplit(".", 1)[0])
251
elif self.properties[u"approved_pending"]:
252
if self.properties[u"approved_by_default"]:
253
message = u"Connection established to client. (d)eny?"
255
message = u"Seeks approval to send secret. (a)pprove?"
258
self._text = "%s%s" % (base, message)
260
if not urwid.supports_unicode():
261
self._text = self._text.encode("ascii", "replace")
262
textlist = [(u"normal", self._text)]
263
self._text_widget.set_text(textlist)
264
self._focus_text_widget.set_text([(with_standout[text[0]],
266
if isinstance(text, tuple)
268
for text in textlist])
269
self._widget = self._text_widget
270
self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
272
# Run update hook, if any
273
if self.update_hook is not None:
276
def update_timer(self):
279
return True # Keep calling this
282
if self._update_timer_callback_tag is not None:
283
gobject.source_remove(self._update_timer_callback_tag)
284
self._update_timer_callback_tag = None
285
if self.delete_hook is not None:
286
self.delete_hook(self)
288
def render(self, (maxcol,), focus=False):
289
"""Render differently if we have focus.
290
This overrides the method from urwid.FlowWidget"""
291
return self.current_widget(focus).render((maxcol,),
294
def keypress(self, (maxcol,), key):
296
This overrides the method from urwid.FlowWidget"""
298
self.proxy.Enable(dbus_interface = client_interface)
300
self.proxy.Disable(dbus_interface = client_interface)
302
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
303
dbus_interface = client_interface)
305
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
306
dbus_interface = client_interface)
307
elif key == u"r" or key == u"_" or key == u"ctrl k":
308
self.server_proxy_object.RemoveClient(self.proxy
311
self.proxy.StartChecker(dbus_interface = client_interface)
313
self.proxy.StopChecker(dbus_interface = client_interface)
315
self.proxy.CheckedOK(dbus_interface = client_interface)
317
# elif key == u"p" or key == "=":
319
# elif key == u"u" or key == ":":
320
# self.proxy.unpause()
321
# elif key == u"RET":
324
# self.proxy.Approve(True)
326
# self.proxy.Approve(False)
330
def property_changed(self, property=None, value=None,
332
"""Call self.update() if old value is not new value.
333
This overrides the method from MandosClientPropertyCache"""
334
property_name = unicode(property)
335
old_value = self.properties.get(property_name)
336
super(MandosClientWidget, self).property_changed(
337
property=property, value=value, *args, **kwargs)
338
if self.properties.get(property_name) != old_value:
342
class ConstrainedListBox(urwid.ListBox):
343
"""Like a normal urwid.ListBox, but will consume all "up" or
344
"down" key presses, thus not allowing any containing widgets to
345
use them as an excuse to shift focus away from this widget.
347
def keypress(self, (maxcol, maxrow), key):
348
ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
349
if ret in (u"up", u"down"):
354
class UserInterface(object):
355
"""This is the entire user interface - the whole screen
356
with boxes, lists of client widgets, etc.
358
def __init__(self, max_log_length=1000):
359
DBusGMainLoop(set_as_default=True)
361
self.screen = urwid.curses_display.Screen()
363
self.screen.register_palette((
365
u"default", u"default", None),
367
u"default", u"default", u"bold"),
369
u"default", u"default", u"underline"),
371
u"default", u"default", u"standout"),
372
(u"bold-underline-blink",
373
u"default", u"default", (u"bold", u"underline")),
375
u"default", u"default", (u"bold", u"standout")),
376
(u"underline-blink-standout",
377
u"default", u"default", (u"underline", u"standout")),
378
(u"bold-underline-blink-standout",
379
u"default", u"default", (u"bold", u"underline",
383
if urwid.supports_unicode():
384
self.divider = u"─" # \u2500
385
#self.divider = u"━" # \u2501
387
#self.divider = u"-" # \u002d
388
self.divider = u"_" # \u005f
392
self.size = self.screen.get_cols_rows()
394
self.clients = urwid.SimpleListWalker([])
395
self.clients_dict = {}
397
# We will add Text widgets to this list
399
self.max_log_length = max_log_length
401
# We keep a reference to the log widget so we can remove it
402
# from the ListWalker without it getting destroyed
403
self.logbox = ConstrainedListBox(self.log)
405
# This keeps track of whether self.uilist currently has
406
# self.logbox in it or not
407
self.log_visible = True
408
self.log_wrap = u"any"
411
self.log_message_raw((u"bold",
412
u"Mandos Monitor version " + version))
413
self.log_message_raw((u"bold",
416
self.busname = domain + '.Mandos'
417
self.main_loop = gobject.MainLoop()
418
self.bus = dbus.SystemBus()
419
mandos_dbus_objc = self.bus.get_object(
420
self.busname, u"/", follow_name_owner_changes=True)
421
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
425
mandos_clients = (self.mandos_serv
426
.GetAllClientsWithProperties())
427
except dbus.exceptions.DBusException:
428
mandos_clients = dbus.Dictionary()
431
.connect_to_signal(u"ClientRemoved",
432
self.find_and_remove_client,
433
dbus_interface=server_interface,
436
.connect_to_signal(u"ClientAdded",
438
dbus_interface=server_interface,
441
.connect_to_signal(u"ClientNotFound",
442
self.client_not_found,
443
dbus_interface=server_interface,
445
for path, client in mandos_clients.iteritems():
446
client_proxy_object = self.bus.get_object(self.busname,
448
self.add_client(MandosClientWidget(server_proxy_object
451
=client_proxy_object,
461
def client_not_found(self, fingerprint, address):
462
self.log_message((u"Client with address %s and fingerprint %s"
463
u" could not be found" % (address,
467
"""This rebuilds the User Interface.
468
Call this when the widget layout needs to change"""
470
#self.uilist.append(urwid.ListBox(self.clients))
471
self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
472
#header=urwid.Divider(),
474
footer=urwid.Divider(div_char=self.divider)))
476
self.uilist.append(self.logbox)
478
self.topwidget = urwid.Pile(self.uilist)
480
def log_message(self, message):
481
timestamp = datetime.datetime.now().isoformat()
482
self.log_message_raw(timestamp + u": " + message)
484
def log_message_raw(self, markup):
485
"""Add a log message to the log buffer."""
486
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
487
if (self.max_log_length
488
and len(self.log) > self.max_log_length):
489
del self.log[0:len(self.log)-self.max_log_length-1]
490
self.logbox.set_focus(len(self.logbox.body.contents),
491
coming_from=u"above")
494
def toggle_log_display(self):
495
"""Toggle visibility of the log buffer."""
496
self.log_visible = not self.log_visible
498
self.log_message(u"Log visibility changed to: "
499
+ unicode(self.log_visible))
501
def change_log_display(self):
502
"""Change type of log display.
503
Currently, this toggles wrapping of text lines."""
504
if self.log_wrap == u"clip":
505
self.log_wrap = u"any"
507
self.log_wrap = u"clip"
508
for textwidget in self.log:
509
textwidget.set_wrap_mode(self.log_wrap)
510
self.log_message(u"Wrap mode: " + self.log_wrap)
512
def find_and_remove_client(self, path, name):
513
"""Find an client from its object path and remove it.
515
This is connected to the ClientRemoved signal from the
516
Mandos server object."""
518
client = self.clients_dict[path]
522
self.remove_client(client, path)
524
def add_new_client(self, path):
525
client_proxy_object = self.bus.get_object(self.busname, path)
526
self.add_client(MandosClientWidget(server_proxy_object
529
=client_proxy_object,
538
def add_client(self, client, path=None):
539
self.clients.append(client)
541
path = client.proxy.object_path
542
self.clients_dict[path] = client
543
self.clients.sort(None, lambda c: c.properties[u"name"])
546
def remove_client(self, client, path=None):
547
self.clients.remove(client)
549
path = client.proxy.object_path
550
del self.clients_dict[path]
551
if not self.clients_dict:
552
# Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
553
# is completely emptied, we need to recreate it.
554
self.clients = urwid.SimpleListWalker([])
559
"""Redraw the screen"""
560
canvas = self.topwidget.render(self.size, focus=True)
561
self.screen.draw_screen(self.size, canvas)
564
"""Start the main loop and exit when it's done."""
566
self._input_callback_tag = (gobject.io_add_watch
571
# Main loop has finished, we should close everything now
572
gobject.source_remove(self._input_callback_tag)
576
self.main_loop.quit()
578
def process_input(self, source, condition):
579
keys = self.screen.get_input()
580
translations = { u"ctrl n": u"down", # Emacs
581
u"ctrl p": u"up", # Emacs
582
u"ctrl v": u"page down", # Emacs
583
u"meta v": u"page up", # Emacs
584
u" ": u"page down", # less
585
u"f": u"page down", # less
586
u"b": u"page up", # less
592
key = translations[key]
593
except KeyError: # :-)
596
if key == u"q" or key == u"Q":
599
elif key == u"window resize":
600
self.size = self.screen.get_cols_rows()
602
elif key == u"\f": # Ctrl-L
604
elif key == u"l" or key == u"D":
605
self.toggle_log_display()
607
elif key == u"w" or key == u"i":
608
self.change_log_display()
610
elif key == u"?" or key == u"f1" or key == u"esc":
611
if not self.log_visible:
612
self.log_visible = True
614
self.log_message_raw((u"bold",
618
u"l: Log window toggle",
619
u"TAB: Switch window",
621
self.log_message_raw((u"bold",
627
u"s: Start new checker",
634
if self.topwidget.get_focus() is self.logbox:
635
self.topwidget.set_focus(0)
637
self.topwidget.set_focus(self.logbox)
639
#elif (key == u"end" or key == u"meta >" or key == u"G"
641
# pass # xxx end-of-buffer
642
#elif (key == u"home" or key == u"meta <" or key == u"g"
644
# pass # xxx beginning-of-buffer
645
#elif key == u"ctrl e" or key == u"$":
646
# pass # xxx move-end-of-line
647
#elif key == u"ctrl a" or key == u"^":
648
# pass # xxx move-beginning-of-line
649
#elif key == u"ctrl b" or key == u"meta (" or key == u"h":
651
#elif key == u"ctrl f" or key == u"meta )" or key == u"l":
654
# pass # scroll up log
656
# pass # scroll down log
657
elif self.topwidget.selectable():
658
self.topwidget.keypress(self.size, key)
666
ui.log_message(unicode(e))