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
 
 
22
# Some useful constants
 
 
23
domain = 'se.bsnet.fukt'
 
 
24
server_interface = domain + '.Mandos'
 
 
25
client_interface = domain + '.Mandos.Client'
 
 
28
# Always run in monochrome mode
 
 
29
urwid.curses_display.curses.has_colors = lambda : False
 
 
31
# Urwid doesn't support blinking, but we want it.  Since we have no
 
 
32
# use for underline on its own, we make underline also always blink.
 
 
33
urwid.curses_display.curses.A_UNDERLINE |= (
 
 
34
    urwid.curses_display.curses.A_BLINK)
 
 
36
class MandosClientPropertyCache(object):
 
 
37
    """This wraps a Mandos Client D-Bus proxy object, caches the
 
 
38
    properties and calls a hook function when any of them are
 
 
41
    def __init__(self, proxy_object=None, properties=None, *args,
 
 
43
        self.proxy = proxy_object # Mandos Client proxy object
 
 
45
        if properties is None:
 
 
46
            self.properties = dict()
 
 
48
            self.properties = properties
 
 
49
        self.proxy.connect_to_signal(u"PropertyChanged",
 
 
50
                                     self.property_changed,
 
 
54
        if properties is None:
 
 
55
            self.properties.update(self.proxy.GetAll(client_interface,
 
 
57
                                                     dbus.PROPERTIES_IFACE))
 
 
58
        super(MandosClientPropertyCache, self).__init__(
 
 
59
            proxy_object=proxy_object,
 
 
60
            properties=properties, *args, **kwargs)
 
 
62
    def property_changed(self, property=None, value=None):
 
 
63
        """This is called whenever we get a PropertyChanged signal
 
 
64
        It updates the changed property in the "properties" dict.
 
 
66
        # Update properties dict with new value
 
 
67
        self.properties[property] = value
 
 
70
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
 
 
71
    """A Mandos Client which is visible on the screen.
 
 
74
    def __init__(self, server_proxy_object=None, update_hook=None,
 
 
75
                 delete_hook=None, logger=None, *args, **kwargs):
 
 
77
        self.update_hook = update_hook
 
 
79
        self.delete_hook = delete_hook
 
 
80
        # Mandos Server proxy object
 
 
81
        self.server_proxy_object = server_proxy_object
 
 
85
        # The widget shown normally
 
 
86
        self._text_widget = urwid.Text(u"")
 
 
87
        # The widget shown when we have focus
 
 
88
        self._focus_text_widget = urwid.Text(u"")
 
 
89
        super(MandosClientWidget, self).__init__(
 
 
90
            update_hook=update_hook, delete_hook=delete_hook,
 
 
94
        self.proxy.connect_to_signal(u"CheckerCompleted",
 
 
95
                                     self.checker_completed,
 
 
98
        self.proxy.connect_to_signal(u"CheckerStarted",
 
 
102
        self.proxy.connect_to_signal(u"GotSecret",
 
 
106
        self.proxy.connect_to_signal(u"Rejected",
 
 
111
    def checker_completed(self, exitstatus, condition, command):
 
 
113
            self.logger(u'Checker for client %s (command "%s")'
 
 
115
                        % (self.properties[u"name"], command))
 
 
117
        if os.WIFEXITED(condition):
 
 
118
            self.logger(u'Checker for client %s (command "%s")'
 
 
119
                        u' failed with exit code %s'
 
 
120
                        % (self.properties[u"name"], command,
 
 
121
                           os.WEXITSTATUS(condition)))
 
 
123
        if os.WIFSIGNALED(condition):
 
 
124
            self.logger(u'Checker for client %s (command "%s")'
 
 
125
                        u' was killed by signal %s'
 
 
126
                        % (self.properties[u"name"], command,
 
 
127
                           os.WTERMSIG(condition)))
 
 
129
        if os.WCOREDUMP(condition):
 
 
130
            self.logger(u'Checker for client %s (command "%s")'
 
 
132
                        % (self.properties[u"name"], command))
 
 
133
        self.logger(u'Checker for client %s completed mysteriously')
 
 
135
    def checker_started(self, command):
 
 
136
        self.logger(u'Client %s started checker "%s"'
 
 
137
                    % (self.properties[u"name"], unicode(command)))
 
 
139
    def got_secret(self):
 
 
140
        self.logger(u'Client %s received its secret'
 
 
141
                    % self.properties[u"name"])
 
 
144
        self.logger(u'Client %s was rejected'
 
 
145
                    % self.properties[u"name"])
 
 
147
    def selectable(self):
 
 
148
        """Make this a "selectable" widget.
 
 
149
        This overrides the method from urwid.FlowWidget."""
 
 
152
    def rows(self, (maxcol,), focus=False):
 
 
153
        """How many rows this widget will occupy might depend on
 
 
154
        whether we have focus or not.
 
 
155
        This overrides the method from urwid.FlowWidget"""
 
 
156
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
 
158
    def current_widget(self, focus=False):
 
 
159
        if focus or self.opened:
 
 
160
            return self._focus_widget
 
 
164
        "Called when what is visible on the screen should be updated."
 
 
165
        # How to add standout mode to a style
 
 
166
        with_standout = { u"normal": u"standout",
 
 
167
                          u"bold": u"bold-standout",
 
 
169
                              u"underline-blink-standout",
 
 
170
                          u"bold-underline-blink":
 
 
171
                              u"bold-underline-blink-standout",
 
 
174
        # Rebuild focus and non-focus widgets using current properties
 
 
175
        self._text = (u'%(name)s: %(enabled)s'
 
 
176
                      % { u"name": self.properties[u"name"],
 
 
179
                               if self.properties[u"enabled"]
 
 
181
        if not urwid.supports_unicode():
 
 
182
            self._text = self._text.encode("ascii", "replace")
 
 
183
        textlist = [(u"normal", self._text)]
 
 
184
        self._text_widget.set_text(textlist)
 
 
185
        self._focus_text_widget.set_text([(with_standout[text[0]],
 
 
187
                                          if isinstance(text, tuple)
 
 
189
                                          for text in textlist])
 
 
190
        self._widget = self._text_widget
 
 
191
        self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
 
 
193
        # Run update hook, if any
 
 
194
        if self.update_hook is not None:
 
 
198
        if self.delete_hook is not None:
 
 
199
            self.delete_hook(self)
 
 
201
    def render(self, (maxcol,), focus=False):
 
 
202
        """Render differently if we have focus.
 
 
203
        This overrides the method from urwid.FlowWidget"""
 
 
204
        return self.current_widget(focus).render((maxcol,),
 
 
207
    def keypress(self, (maxcol,), key):
 
 
209
        This overrides the method from urwid.FlowWidget"""
 
 
210
        if key == u"e" or key == u"+":
 
 
212
        elif key == u"d" or key == u"-":
 
 
214
        elif key == u"r" or key == u"_" or key == u"ctrl k":
 
 
215
            self.server_proxy_object.RemoveClient(self.proxy
 
 
218
            self.proxy.StartChecker()
 
 
220
            self.proxy.StopChecker()
 
 
222
            self.proxy.CheckedOK()
 
 
224
#         elif key == u"p" or key == "=":
 
 
226
#         elif key == u"u" or key == ":":
 
 
227
#             self.proxy.unpause()
 
 
228
#         elif key == u"RET":
 
 
233
    def property_changed(self, property=None, value=None,
 
 
235
        """Call self.update() if old value is not new value.
 
 
236
        This overrides the method from MandosClientPropertyCache"""
 
 
237
        property_name = unicode(property)
 
 
238
        old_value = self.properties.get(property_name)
 
 
239
        super(MandosClientWidget, self).property_changed(
 
 
240
            property=property, value=value, *args, **kwargs)
 
 
241
        if self.properties.get(property_name) != old_value:
 
 
245
class ConstrainedListBox(urwid.ListBox):
 
 
246
    """Like a normal urwid.ListBox, but will consume all "up" or
 
 
247
    "down" key presses, thus not allowing any containing widgets to
 
 
248
    use them as an excuse to shift focus away from this widget.
 
 
250
    def keypress(self, (maxcol, maxrow), key):
 
 
251
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
 
 
252
        if ret in (u"up", u"down"):
 
 
257
class UserInterface(object):
 
 
258
    """This is the entire user interface - the whole screen
 
 
259
    with boxes, lists of client widgets, etc.
 
 
261
    def __init__(self, max_log_length=1000):
 
 
262
        DBusGMainLoop(set_as_default=True)
 
 
264
        self.screen = urwid.curses_display.Screen()
 
 
266
        self.screen.register_palette((
 
 
268
                 u"default", u"default", None),
 
 
270
                 u"default", u"default", u"bold"),
 
 
272
                 u"default", u"default", u"underline"),
 
 
274
                 u"default", u"default", u"standout"),
 
 
275
                (u"bold-underline-blink",
 
 
276
                 u"default", u"default", (u"bold", u"underline")),
 
 
278
                 u"default", u"default", (u"bold", u"standout")),
 
 
279
                (u"underline-blink-standout",
 
 
280
                 u"default", u"default", (u"underline", u"standout")),
 
 
281
                (u"bold-underline-blink-standout",
 
 
282
                 u"default", u"default", (u"bold", u"underline",
 
 
286
        if urwid.supports_unicode():
 
 
287
            self.divider = u"─" # \u2500
 
 
288
            #self.divider = u"━" # \u2501
 
 
290
            #self.divider = u"-" # \u002d
 
 
291
            self.divider = u"_" # \u005f
 
 
295
        self.size = self.screen.get_cols_rows()
 
 
297
        self.clients = urwid.SimpleListWalker([])
 
 
298
        self.clients_dict = {}
 
 
300
        # We will add Text widgets to this list
 
 
302
        self.max_log_length = max_log_length
 
 
304
        # We keep a reference to the log widget so we can remove it
 
 
305
        # from the ListWalker without it getting destroyed
 
 
306
        self.logbox = ConstrainedListBox(self.log)
 
 
308
        # This keeps track of whether self.uilist currently has
 
 
309
        # self.logbox in it or not
 
 
310
        self.log_visible = True
 
 
311
        self.log_wrap = u"any"
 
 
314
        self.log_message_raw((u"bold",
 
 
315
                              u"Mandos Monitor version " + version))
 
 
316
        self.log_message_raw((u"bold",
 
 
319
        self.busname = domain + '.Mandos'
 
 
320
        self.main_loop = gobject.MainLoop()
 
 
321
        self.bus = dbus.SystemBus()
 
 
322
        mandos_dbus_objc = self.bus.get_object(
 
 
323
            self.busname, u"/", follow_name_owner_changes=True)
 
 
324
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
 
328
            mandos_clients = (self.mandos_serv
 
 
329
                              .GetAllClientsWithProperties())
 
 
330
        except dbus.exceptions.DBusException:
 
 
331
            mandos_clients = dbus.Dictionary()
 
 
334
         .connect_to_signal(u"ClientRemoved",
 
 
335
                            self.find_and_remove_client,
 
 
336
                            dbus_interface=server_interface,
 
 
339
         .connect_to_signal(u"ClientAdded",
 
 
341
                            dbus_interface=server_interface,
 
 
344
         .connect_to_signal(u"ClientNotFound",
 
 
345
                            self.client_not_found,
 
 
346
                            dbus_interface=server_interface,
 
 
348
        for path, client in mandos_clients.iteritems():
 
 
349
            client_proxy_object = self.bus.get_object(self.busname,
 
 
351
            self.add_client(MandosClientWidget(server_proxy_object
 
 
354
                                               =client_proxy_object,
 
 
364
    def client_not_found(self, fingerprint, address):
 
 
365
        self.log_message((u"Client with address %s and fingerprint %s"
 
 
366
                          u" could not be found" % (address,
 
 
370
        """This rebuilds the User Interface.
 
 
371
        Call this when the widget layout needs to change"""
 
 
373
        #self.uilist.append(urwid.ListBox(self.clients))
 
 
374
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
 
375
                                       #header=urwid.Divider(),
 
 
377
                                       footer=urwid.Divider(div_char=self.divider)))
 
 
379
            self.uilist.append(self.logbox)
 
 
381
        self.topwidget = urwid.Pile(self.uilist)
 
 
383
    def log_message(self, message):
 
 
384
        timestamp = datetime.datetime.now().isoformat()
 
 
385
        self.log_message_raw(timestamp + u": " + message)
 
 
387
    def log_message_raw(self, markup):
 
 
388
        """Add a log message to the log buffer."""
 
 
389
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
 
390
        if (self.max_log_length
 
 
391
            and len(self.log) > self.max_log_length):
 
 
392
            del self.log[0:len(self.log)-self.max_log_length-1]
 
 
393
        self.logbox.set_focus(len(self.logbox.body.contents),
 
 
394
                              coming_from=u"above")
 
 
397
    def toggle_log_display(self):
 
 
398
        """Toggle visibility of the log buffer."""
 
 
399
        self.log_visible = not self.log_visible
 
 
401
        self.log_message(u"Log visibility changed to: "
 
 
402
                         + unicode(self.log_visible))
 
 
404
    def change_log_display(self):
 
 
405
        """Change type of log display.
 
 
406
        Currently, this toggles wrapping of text lines."""
 
 
407
        if self.log_wrap == u"clip":
 
 
408
            self.log_wrap = u"any"
 
 
410
            self.log_wrap = u"clip"
 
 
411
        for textwidget in self.log:
 
 
412
            textwidget.set_wrap_mode(self.log_wrap)
 
 
413
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
 
415
    def find_and_remove_client(self, path, name):
 
 
416
        """Find an client from its object path and remove it.
 
 
418
        This is connected to the ClientRemoved signal from the
 
 
419
        Mandos server object."""
 
 
421
            client = self.clients_dict[path]
 
 
425
        self.remove_client(client, path)
 
 
427
    def add_new_client(self, path, properties):
 
 
428
        client_proxy_object = self.bus.get_object(self.busname, path)
 
 
429
        self.add_client(MandosClientWidget(server_proxy_object
 
 
432
                                           =client_proxy_object,
 
 
433
                                           properties=properties,
 
 
437
                                           =self.remove_client),
 
 
440
    def add_client(self, client, path=None):
 
 
441
        self.clients.append(client)
 
 
443
            path = client.proxy.object_path
 
 
444
        self.clients_dict[path] = client
 
 
445
        self.clients.sort(None, lambda c: c.properties[u"name"])
 
 
448
    def remove_client(self, client, path=None):
 
 
449
        self.clients.remove(client)
 
 
451
            path = client.proxy.object_path
 
 
452
        del self.clients_dict[path]
 
 
453
        if not self.clients_dict:
 
 
454
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
 
 
455
            # is completely emptied, we need to recreate it.
 
 
456
            self.clients = urwid.SimpleListWalker([])
 
 
461
        """Redraw the screen"""
 
 
462
        canvas = self.topwidget.render(self.size, focus=True)
 
 
463
        self.screen.draw_screen(self.size, canvas)
 
 
466
        """Start the main loop and exit when it's done."""
 
 
468
        self._input_callback_tag = (gobject.io_add_watch
 
 
473
        # Main loop has finished, we should close everything now
 
 
474
        gobject.source_remove(self._input_callback_tag)
 
 
478
        self.main_loop.quit()
 
 
480
    def process_input(self, source, condition):
 
 
481
        keys = self.screen.get_input()
 
 
482
        translations = { u"ctrl n": u"down",      # Emacs
 
 
483
                         u"ctrl p": u"up",        # Emacs
 
 
484
                         u"ctrl v": u"page down", # Emacs
 
 
485
                         u"meta v": u"page up",   # Emacs
 
 
486
                         u" ": u"page down",      # less
 
 
487
                         u"f": u"page down",      # less
 
 
488
                         u"b": u"page up",        # less
 
 
494
                key = translations[key]
 
 
495
            except KeyError:    # :-)
 
 
498
            if key == u"q" or key == u"Q":
 
 
501
            elif key == u"window resize":
 
 
502
                self.size = self.screen.get_cols_rows()
 
 
504
            elif key == u"\f":  # Ctrl-L
 
 
506
            elif key == u"l" or key == u"D":
 
 
507
                self.toggle_log_display()
 
 
509
            elif key == u"w" or key == u"i":
 
 
510
                self.change_log_display()
 
 
512
            elif key == u"?" or key == u"f1" or key == u"esc":
 
 
513
                if not self.log_visible:
 
 
514
                    self.log_visible = True
 
 
516
                self.log_message_raw((u"bold",
 
 
520
                                            u"l: Log window toggle",
 
 
521
                                            u"TAB: Switch window",
 
 
523
                self.log_message_raw((u"bold",
 
 
529
                                             u"s: Start new checker",
 
 
534
                if self.topwidget.get_focus() is self.logbox:
 
 
535
                    self.topwidget.set_focus(0)
 
 
537
                    self.topwidget.set_focus(self.logbox)
 
 
539
            #elif (key == u"end" or key == u"meta >" or key == u"G"
 
 
541
            #    pass            # xxx end-of-buffer
 
 
542
            #elif (key == u"home" or key == u"meta <" or key == u"g"
 
 
544
            #    pass            # xxx beginning-of-buffer
 
 
545
            #elif key == u"ctrl e" or key == u"$":
 
 
546
            #    pass            # xxx move-end-of-line
 
 
547
            #elif key == u"ctrl a" or key == u"^":
 
 
548
            #    pass            # xxx move-beginning-of-line
 
 
549
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
 
551
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
 
554
            #    pass            # scroll up log
 
 
556
            #    pass            # scroll down log
 
 
557
            elif self.topwidget.selectable():
 
 
558
                self.topwidget.keypress(self.size, key)