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
class MandosClientPropertyCache(object):
44
"""This wraps a Mandos Client D-Bus proxy object, caches the
45
properties and calls a hook function when any of them are
48
def __init__(self, proxy_object=None, *args, **kwargs):
49
self.proxy = proxy_object # Mandos Client proxy object
51
self.properties = dict()
52
self.proxy.connect_to_signal(u"PropertyChanged",
53
self.property_changed,
57
self.properties.update(
58
self.proxy.GetAll(client_interface,
59
dbus_interface = dbus.PROPERTIES_IFACE))
60
super(MandosClientPropertyCache, self).__init__(
63
def property_changed(self, property=None, value=None):
64
"""This is called whenever we get a PropertyChanged signal
65
It updates the changed property in the "properties" dict.
67
# Update properties dict with new value
68
self.properties[property] = value
71
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
72
"""A Mandos Client which is visible on the screen.
75
def __init__(self, server_proxy_object=None, update_hook=None,
76
delete_hook=None, logger=None, *args, **kwargs):
78
self.update_hook = update_hook
80
self.delete_hook = delete_hook
81
# Mandos Server proxy object
82
self.server_proxy_object = server_proxy_object
86
# The widget shown normally
87
self._text_widget = urwid.Text(u"")
88
# The widget shown when we have focus
89
self._focus_text_widget = urwid.Text(u"")
90
super(MandosClientWidget, self).__init__(
91
update_hook=update_hook, delete_hook=delete_hook,
95
self.proxy.connect_to_signal(u"CheckerCompleted",
96
self.checker_completed,
99
self.proxy.connect_to_signal(u"CheckerStarted",
100
self.checker_started,
103
self.proxy.connect_to_signal(u"GotSecret",
107
self.proxy.connect_to_signal(u"NeedApproval",
111
self.proxy.connect_to_signal(u"Rejected",
116
def checker_completed(self, exitstatus, condition, command):
118
#self.logger(u'Checker for client %s (command "%s")'
120
# % (self.properties[u"name"], command))
122
if os.WIFEXITED(condition):
123
self.logger(u'Checker for client %s (command "%s")'
124
u' failed with exit code %s'
125
% (self.properties[u"name"], command,
126
os.WEXITSTATUS(condition)))
128
if os.WIFSIGNALED(condition):
129
self.logger(u'Checker for client %s (command "%s")'
130
u' was killed by signal %s'
131
% (self.properties[u"name"], command,
132
os.WTERMSIG(condition)))
134
if os.WCOREDUMP(condition):
135
self.logger(u'Checker for client %s (command "%s")'
137
% (self.properties[u"name"], command))
138
self.logger(u'Checker for client %s completed mysteriously')
140
def checker_started(self, command):
141
#self.logger(u'Client %s started checker "%s"'
142
# % (self.properties[u"name"], unicode(command)))
145
def got_secret(self):
146
self.logger(u'Client %s received its secret'
147
% self.properties[u"name"])
149
def need_approval(self, timeout, default):
151
message = u'Client %s needs approval within %s seconds'
153
message = u'Client %s will get its secret in %s seconds'
155
% (self.properties[u"name"], timeout/1000))
157
def rejected(self, reason):
158
self.logger(u'Client %s was rejected; reason: %s'
159
% (self.properties[u"name"], reason))
161
def selectable(self):
162
"""Make this a "selectable" widget.
163
This overrides the method from urwid.FlowWidget."""
166
def rows(self, (maxcol,), focus=False):
167
"""How many rows this widget will occupy might depend on
168
whether we have focus or not.
169
This overrides the method from urwid.FlowWidget"""
170
return self.current_widget(focus).rows((maxcol,), focus=focus)
172
def current_widget(self, focus=False):
173
if focus or self.opened:
174
return self._focus_widget
178
"Called when what is visible on the screen should be updated."
179
# How to add standout mode to a style
180
with_standout = { u"normal": u"standout",
181
u"bold": u"bold-standout",
183
u"underline-blink-standout",
184
u"bold-underline-blink":
185
u"bold-underline-blink-standout",
188
# Rebuild focus and non-focus widgets using current properties
189
self._text = (u'%(name)s: %(enabled)s'
190
% { u"name": self.properties[u"name"],
193
if self.properties[u"enabled"]
195
if not urwid.supports_unicode():
196
self._text = self._text.encode("ascii", "replace")
197
textlist = [(u"normal", self._text)]
198
self._text_widget.set_text(textlist)
199
self._focus_text_widget.set_text([(with_standout[text[0]],
201
if isinstance(text, tuple)
203
for text in textlist])
204
self._widget = self._text_widget
205
self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
207
# Run update hook, if any
208
if self.update_hook is not None:
212
if self.delete_hook is not None:
213
self.delete_hook(self)
215
def render(self, (maxcol,), focus=False):
216
"""Render differently if we have focus.
217
This overrides the method from urwid.FlowWidget"""
218
return self.current_widget(focus).render((maxcol,),
221
def keypress(self, (maxcol,), key):
223
This overrides the method from urwid.FlowWidget"""
224
if key == u"e" or key == u"+":
226
elif key == u"d" or key == u"-":
228
elif key == u"r" or key == u"_" or key == u"ctrl k":
229
self.server_proxy_object.RemoveClient(self.proxy
232
self.proxy.StartChecker()
234
self.proxy.StopChecker()
236
self.proxy.CheckedOK()
238
# elif key == u"p" or key == "=":
240
# elif key == u"u" or key == ":":
241
# self.proxy.unpause()
242
# elif key == u"RET":
245
self.proxy.Approve(True)
247
self.proxy.Approve(False)
251
def property_changed(self, property=None, value=None,
253
"""Call self.update() if old value is not new value.
254
This overrides the method from MandosClientPropertyCache"""
255
property_name = unicode(property)
256
old_value = self.properties.get(property_name)
257
super(MandosClientWidget, self).property_changed(
258
property=property, value=value, *args, **kwargs)
259
if self.properties.get(property_name) != old_value:
263
class ConstrainedListBox(urwid.ListBox):
264
"""Like a normal urwid.ListBox, but will consume all "up" or
265
"down" key presses, thus not allowing any containing widgets to
266
use them as an excuse to shift focus away from this widget.
268
def keypress(self, (maxcol, maxrow), key):
269
ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
270
if ret in (u"up", u"down"):
275
class UserInterface(object):
276
"""This is the entire user interface - the whole screen
277
with boxes, lists of client widgets, etc.
279
def __init__(self, max_log_length=1000):
280
DBusGMainLoop(set_as_default=True)
282
self.screen = urwid.curses_display.Screen()
284
self.screen.register_palette((
286
u"default", u"default", None),
288
u"default", u"default", u"bold"),
290
u"default", u"default", u"underline"),
292
u"default", u"default", u"standout"),
293
(u"bold-underline-blink",
294
u"default", u"default", (u"bold", u"underline")),
296
u"default", u"default", (u"bold", u"standout")),
297
(u"underline-blink-standout",
298
u"default", u"default", (u"underline", u"standout")),
299
(u"bold-underline-blink-standout",
300
u"default", u"default", (u"bold", u"underline",
304
if urwid.supports_unicode():
305
self.divider = u"─" # \u2500
306
#self.divider = u"━" # \u2501
308
#self.divider = u"-" # \u002d
309
self.divider = u"_" # \u005f
313
self.size = self.screen.get_cols_rows()
315
self.clients = urwid.SimpleListWalker([])
316
self.clients_dict = {}
318
# We will add Text widgets to this list
320
self.max_log_length = max_log_length
322
# We keep a reference to the log widget so we can remove it
323
# from the ListWalker without it getting destroyed
324
self.logbox = ConstrainedListBox(self.log)
326
# This keeps track of whether self.uilist currently has
327
# self.logbox in it or not
328
self.log_visible = True
329
self.log_wrap = u"any"
332
self.log_message_raw((u"bold",
333
u"Mandos Monitor version " + version))
334
self.log_message_raw((u"bold",
337
self.busname = domain + '.Mandos'
338
self.main_loop = gobject.MainLoop()
339
self.bus = dbus.SystemBus()
340
mandos_dbus_objc = self.bus.get_object(
341
self.busname, u"/", follow_name_owner_changes=True)
342
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
346
mandos_clients = (self.mandos_serv
347
.GetAllClientsWithProperties())
348
except dbus.exceptions.DBusException:
349
mandos_clients = dbus.Dictionary()
352
.connect_to_signal(u"ClientRemoved",
353
self.find_and_remove_client,
354
dbus_interface=server_interface,
357
.connect_to_signal(u"ClientAdded",
359
dbus_interface=server_interface,
362
.connect_to_signal(u"ClientNotFound",
363
self.client_not_found,
364
dbus_interface=server_interface,
366
for path, client in mandos_clients.iteritems():
367
client_proxy_object = self.bus.get_object(self.busname,
369
self.add_client(MandosClientWidget(server_proxy_object
372
=client_proxy_object,
382
def client_not_found(self, fingerprint, address):
383
self.log_message((u"Client with address %s and fingerprint %s"
384
u" could not be found" % (address,
388
"""This rebuilds the User Interface.
389
Call this when the widget layout needs to change"""
391
#self.uilist.append(urwid.ListBox(self.clients))
392
self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
393
#header=urwid.Divider(),
395
footer=urwid.Divider(div_char=self.divider)))
397
self.uilist.append(self.logbox)
399
self.topwidget = urwid.Pile(self.uilist)
401
def log_message(self, message):
402
timestamp = datetime.datetime.now().isoformat()
403
self.log_message_raw(timestamp + u": " + message)
405
def log_message_raw(self, markup):
406
"""Add a log message to the log buffer."""
407
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
408
if (self.max_log_length
409
and len(self.log) > self.max_log_length):
410
del self.log[0:len(self.log)-self.max_log_length-1]
411
self.logbox.set_focus(len(self.logbox.body.contents),
412
coming_from=u"above")
415
def toggle_log_display(self):
416
"""Toggle visibility of the log buffer."""
417
self.log_visible = not self.log_visible
419
self.log_message(u"Log visibility changed to: "
420
+ unicode(self.log_visible))
422
def change_log_display(self):
423
"""Change type of log display.
424
Currently, this toggles wrapping of text lines."""
425
if self.log_wrap == u"clip":
426
self.log_wrap = u"any"
428
self.log_wrap = u"clip"
429
for textwidget in self.log:
430
textwidget.set_wrap_mode(self.log_wrap)
431
self.log_message(u"Wrap mode: " + self.log_wrap)
433
def find_and_remove_client(self, path, name):
434
"""Find an client from its object path and remove it.
436
This is connected to the ClientRemoved signal from the
437
Mandos server object."""
439
client = self.clients_dict[path]
443
self.remove_client(client, path)
445
def add_new_client(self, path):
446
client_proxy_object = self.bus.get_object(self.busname, path)
447
self.add_client(MandosClientWidget(server_proxy_object
450
=client_proxy_object,
459
def add_client(self, client, path=None):
460
self.clients.append(client)
462
path = client.proxy.object_path
463
self.clients_dict[path] = client
464
self.clients.sort(None, lambda c: c.properties[u"name"])
467
def remove_client(self, client, path=None):
468
self.clients.remove(client)
470
path = client.proxy.object_path
471
del self.clients_dict[path]
472
if not self.clients_dict:
473
# Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
474
# is completely emptied, we need to recreate it.
475
self.clients = urwid.SimpleListWalker([])
480
"""Redraw the screen"""
481
canvas = self.topwidget.render(self.size, focus=True)
482
self.screen.draw_screen(self.size, canvas)
485
"""Start the main loop and exit when it's done."""
487
self._input_callback_tag = (gobject.io_add_watch
492
# Main loop has finished, we should close everything now
493
gobject.source_remove(self._input_callback_tag)
497
self.main_loop.quit()
499
def process_input(self, source, condition):
500
keys = self.screen.get_input()
501
translations = { u"ctrl n": u"down", # Emacs
502
u"ctrl p": u"up", # Emacs
503
u"ctrl v": u"page down", # Emacs
504
u"meta v": u"page up", # Emacs
505
u" ": u"page down", # less
506
u"f": u"page down", # less
507
u"b": u"page up", # less
513
key = translations[key]
514
except KeyError: # :-)
517
if key == u"q" or key == u"Q":
520
elif key == u"window resize":
521
self.size = self.screen.get_cols_rows()
523
elif key == u"\f": # Ctrl-L
525
elif key == u"l" or key == u"D":
526
self.toggle_log_display()
528
elif key == u"w" or key == u"i":
529
self.change_log_display()
531
elif key == u"?" or key == u"f1" or key == u"esc":
532
if not self.log_visible:
533
self.log_visible = True
535
self.log_message_raw((u"bold",
539
u"l: Log window toggle",
540
u"TAB: Switch window",
542
self.log_message_raw((u"bold",
548
u"s: Start new checker",
553
if self.topwidget.get_focus() is self.logbox:
554
self.topwidget.set_focus(0)
556
self.topwidget.set_focus(self.logbox)
558
#elif (key == u"end" or key == u"meta >" or key == u"G"
560
# pass # xxx end-of-buffer
561
#elif (key == u"home" or key == u"meta <" or key == u"g"
563
# pass # xxx beginning-of-buffer
564
#elif key == u"ctrl e" or key == u"$":
565
# pass # xxx move-end-of-line
566
#elif key == u"ctrl a" or key == u"^":
567
# pass # xxx move-beginning-of-line
568
#elif key == u"ctrl b" or key == u"meta (" or key == u"h":
570
#elif key == u"ctrl f" or key == u"meta )" or key == u"l":
573
# pass # scroll up log
575
# pass # scroll down log
576
elif self.topwidget.selectable():
577
self.topwidget.keypress(self.size, key)
585
ui.log_message(unicode(e))