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'')
 
 
26
# Some useful constants
 
 
27
domain = 'se.bsnet.fukt'
 
 
28
server_interface = domain + '.Mandos'
 
 
29
client_interface = domain + '.Mandos.Client'
 
 
32
# Always run in monochrome mode
 
 
33
urwid.curses_display.curses.has_colors = lambda : False
 
 
35
# Urwid doesn't support blinking, but we want it.  Since we have no
 
 
36
# use for underline on its own, we make underline also always blink.
 
 
37
urwid.curses_display.curses.A_UNDERLINE |= (
 
 
38
    urwid.curses_display.curses.A_BLINK)
 
 
40
class MandosClientPropertyCache(object):
 
 
41
    """This wraps a Mandos Client D-Bus proxy object, caches the
 
 
42
    properties and calls a hook function when any of them are
 
 
45
    def __init__(self, proxy_object=None, *args, **kwargs):
 
 
46
        self.proxy = proxy_object # Mandos Client proxy object
 
 
48
        self.properties = dict()
 
 
49
        self.proxy.connect_to_signal(u"PropertyChanged",
 
 
50
                                     self.property_changed,
 
 
54
        self.properties.update(
 
 
55
            self.proxy.GetAll(client_interface,
 
 
56
                              dbus_interface = dbus.PROPERTIES_IFACE))
 
 
57
        super(MandosClientPropertyCache, self).__init__(
 
 
58
            proxy_object=proxy_object, *args, **kwargs)
 
 
60
    def property_changed(self, property=None, value=None):
 
 
61
        """This is called whenever we get a PropertyChanged signal
 
 
62
        It updates the changed property in the "properties" dict.
 
 
64
        # Update properties dict with new value
 
 
65
        self.properties[property] = value
 
 
68
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
 
 
69
    """A Mandos Client which is visible on the screen.
 
 
72
    def __init__(self, server_proxy_object=None, update_hook=None,
 
 
73
                 delete_hook=None, logger=None, *args, **kwargs):
 
 
75
        self.update_hook = update_hook
 
 
77
        self.delete_hook = delete_hook
 
 
78
        # Mandos Server proxy object
 
 
79
        self.server_proxy_object = server_proxy_object
 
 
83
        # The widget shown normally
 
 
84
        self._text_widget = urwid.Text(u"")
 
 
85
        # The widget shown when we have focus
 
 
86
        self._focus_text_widget = urwid.Text(u"")
 
 
87
        super(MandosClientWidget, self).__init__(
 
 
88
            update_hook=update_hook, delete_hook=delete_hook,
 
 
92
        self.proxy.connect_to_signal(u"CheckerCompleted",
 
 
93
                                     self.checker_completed,
 
 
96
        self.proxy.connect_to_signal(u"CheckerStarted",
 
 
100
        self.proxy.connect_to_signal(u"GotSecret",
 
 
104
        self.proxy.connect_to_signal(u"Rejected",
 
 
109
    def checker_completed(self, exitstatus, condition, command):
 
 
111
            self.logger(u'Checker for client %s (command "%s")'
 
 
113
                        % (self.properties[u"name"], command))
 
 
115
        if os.WIFEXITED(condition):
 
 
116
            self.logger(u'Checker for client %s (command "%s")'
 
 
117
                        u' failed with exit code %s'
 
 
118
                        % (self.properties[u"name"], command,
 
 
119
                           os.WEXITSTATUS(condition)))
 
 
121
        if os.WIFSIGNALED(condition):
 
 
122
            self.logger(u'Checker for client %s (command "%s")'
 
 
123
                        u' was killed by signal %s'
 
 
124
                        % (self.properties[u"name"], command,
 
 
125
                           os.WTERMSIG(condition)))
 
 
127
        if os.WCOREDUMP(condition):
 
 
128
            self.logger(u'Checker for client %s (command "%s")'
 
 
130
                        % (self.properties[u"name"], command))
 
 
131
        self.logger(u'Checker for client %s completed mysteriously')
 
 
133
    def checker_started(self, command):
 
 
134
        self.logger(u'Client %s started checker "%s"'
 
 
135
                    % (self.properties[u"name"], unicode(command)))
 
 
137
    def got_secret(self):
 
 
138
        self.logger(u'Client %s received its secret'
 
 
139
                    % self.properties[u"name"])
 
 
142
        self.logger(u'Client %s was rejected'
 
 
143
                    % self.properties[u"name"])
 
 
145
    def selectable(self):
 
 
146
        """Make this a "selectable" widget.
 
 
147
        This overrides the method from urwid.FlowWidget."""
 
 
150
    def rows(self, (maxcol,), focus=False):
 
 
151
        """How many rows this widget will occupy might depend on
 
 
152
        whether we have focus or not.
 
 
153
        This overrides the method from urwid.FlowWidget"""
 
 
154
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
 
156
    def current_widget(self, focus=False):
 
 
157
        if focus or self.opened:
 
 
158
            return self._focus_widget
 
 
162
        "Called when what is visible on the screen should be updated."
 
 
163
        # How to add standout mode to a style
 
 
164
        with_standout = { u"normal": u"standout",
 
 
165
                          u"bold": u"bold-standout",
 
 
167
                              u"underline-blink-standout",
 
 
168
                          u"bold-underline-blink":
 
 
169
                              u"bold-underline-blink-standout",
 
 
172
        # Rebuild focus and non-focus widgets using current properties
 
 
173
        self._text = (u'%(name)s: %(enabled)s'
 
 
174
                      % { u"name": self.properties[u"name"],
 
 
177
                               if self.properties[u"enabled"]
 
 
179
        if not urwid.supports_unicode():
 
 
180
            self._text = self._text.encode("ascii", "replace")
 
 
181
        textlist = [(u"normal", self._text)]
 
 
182
        self._text_widget.set_text(textlist)
 
 
183
        self._focus_text_widget.set_text([(with_standout[text[0]],
 
 
185
                                          if isinstance(text, tuple)
 
 
187
                                          for text in textlist])
 
 
188
        self._widget = self._text_widget
 
 
189
        self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
 
 
191
        # Run update hook, if any
 
 
192
        if self.update_hook is not None:
 
 
196
        if self.delete_hook is not None:
 
 
197
            self.delete_hook(self)
 
 
199
    def render(self, (maxcol,), focus=False):
 
 
200
        """Render differently if we have focus.
 
 
201
        This overrides the method from urwid.FlowWidget"""
 
 
202
        return self.current_widget(focus).render((maxcol,),
 
 
205
    def keypress(self, (maxcol,), key):
 
 
207
        This overrides the method from urwid.FlowWidget"""
 
 
208
        if key == u"e" or key == u"+":
 
 
210
        elif key == u"d" or key == u"-":
 
 
212
        elif key == u"r" or key == u"_" or key == u"ctrl k":
 
 
213
            self.server_proxy_object.RemoveClient(self.proxy
 
 
216
            self.proxy.StartChecker()
 
 
218
            self.proxy.StopChecker()
 
 
220
            self.proxy.CheckedOK()
 
 
222
#         elif key == u"p" or key == "=":
 
 
224
#         elif key == u"u" or key == ":":
 
 
225
#             self.proxy.unpause()
 
 
226
#         elif key == u"RET":
 
 
231
    def property_changed(self, property=None, value=None,
 
 
233
        """Call self.update() if old value is not new value.
 
 
234
        This overrides the method from MandosClientPropertyCache"""
 
 
235
        property_name = unicode(property)
 
 
236
        old_value = self.properties.get(property_name)
 
 
237
        super(MandosClientWidget, self).property_changed(
 
 
238
            property=property, value=value, *args, **kwargs)
 
 
239
        if self.properties.get(property_name) != old_value:
 
 
243
class ConstrainedListBox(urwid.ListBox):
 
 
244
    """Like a normal urwid.ListBox, but will consume all "up" or
 
 
245
    "down" key presses, thus not allowing any containing widgets to
 
 
246
    use them as an excuse to shift focus away from this widget.
 
 
248
    def keypress(self, (maxcol, maxrow), key):
 
 
249
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
 
 
250
        if ret in (u"up", u"down"):
 
 
255
class UserInterface(object):
 
 
256
    """This is the entire user interface - the whole screen
 
 
257
    with boxes, lists of client widgets, etc.
 
 
259
    def __init__(self, max_log_length=1000):
 
 
260
        DBusGMainLoop(set_as_default=True)
 
 
262
        self.screen = urwid.curses_display.Screen()
 
 
264
        self.screen.register_palette((
 
 
266
                 u"default", u"default", None),
 
 
268
                 u"default", u"default", u"bold"),
 
 
270
                 u"default", u"default", u"underline"),
 
 
272
                 u"default", u"default", u"standout"),
 
 
273
                (u"bold-underline-blink",
 
 
274
                 u"default", u"default", (u"bold", u"underline")),
 
 
276
                 u"default", u"default", (u"bold", u"standout")),
 
 
277
                (u"underline-blink-standout",
 
 
278
                 u"default", u"default", (u"underline", u"standout")),
 
 
279
                (u"bold-underline-blink-standout",
 
 
280
                 u"default", u"default", (u"bold", u"underline",
 
 
284
        if urwid.supports_unicode():
 
 
285
            self.divider = u"─" # \u2500
 
 
286
            #self.divider = u"━" # \u2501
 
 
288
            #self.divider = u"-" # \u002d
 
 
289
            self.divider = u"_" # \u005f
 
 
293
        self.size = self.screen.get_cols_rows()
 
 
295
        self.clients = urwid.SimpleListWalker([])
 
 
296
        self.clients_dict = {}
 
 
298
        # We will add Text widgets to this list
 
 
300
        self.max_log_length = max_log_length
 
 
302
        # We keep a reference to the log widget so we can remove it
 
 
303
        # from the ListWalker without it getting destroyed
 
 
304
        self.logbox = ConstrainedListBox(self.log)
 
 
306
        # This keeps track of whether self.uilist currently has
 
 
307
        # self.logbox in it or not
 
 
308
        self.log_visible = True
 
 
309
        self.log_wrap = u"any"
 
 
312
        self.log_message_raw((u"bold",
 
 
313
                              u"Mandos Monitor version " + version))
 
 
314
        self.log_message_raw((u"bold",
 
 
317
        self.busname = domain + '.Mandos'
 
 
318
        self.main_loop = gobject.MainLoop()
 
 
319
        self.bus = dbus.SystemBus()
 
 
320
        mandos_dbus_objc = self.bus.get_object(
 
 
321
            self.busname, u"/", follow_name_owner_changes=True)
 
 
322
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
 
326
            mandos_clients = (self.mandos_serv
 
 
327
                              .GetAllClientsWithProperties())
 
 
328
        except dbus.exceptions.DBusException:
 
 
329
            mandos_clients = dbus.Dictionary()
 
 
332
         .connect_to_signal(u"ClientRemoved",
 
 
333
                            self.find_and_remove_client,
 
 
334
                            dbus_interface=server_interface,
 
 
337
         .connect_to_signal(u"ClientAdded",
 
 
339
                            dbus_interface=server_interface,
 
 
342
         .connect_to_signal(u"ClientNotFound",
 
 
343
                            self.client_not_found,
 
 
344
                            dbus_interface=server_interface,
 
 
346
        for path, client in mandos_clients.iteritems():
 
 
347
            client_proxy_object = self.bus.get_object(self.busname,
 
 
349
            self.add_client(MandosClientWidget(server_proxy_object
 
 
352
                                               =client_proxy_object,
 
 
362
    def client_not_found(self, fingerprint, address):
 
 
363
        self.log_message((u"Client with address %s and fingerprint %s"
 
 
364
                          u" could not be found" % (address,
 
 
368
        """This rebuilds the User Interface.
 
 
369
        Call this when the widget layout needs to change"""
 
 
371
        #self.uilist.append(urwid.ListBox(self.clients))
 
 
372
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
 
373
                                       #header=urwid.Divider(),
 
 
375
                                       footer=urwid.Divider(div_char=self.divider)))
 
 
377
            self.uilist.append(self.logbox)
 
 
379
        self.topwidget = urwid.Pile(self.uilist)
 
 
381
    def log_message(self, message):
 
 
382
        timestamp = datetime.datetime.now().isoformat()
 
 
383
        self.log_message_raw(timestamp + u": " + message)
 
 
385
    def log_message_raw(self, markup):
 
 
386
        """Add a log message to the log buffer."""
 
 
387
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
 
388
        if (self.max_log_length
 
 
389
            and len(self.log) > self.max_log_length):
 
 
390
            del self.log[0:len(self.log)-self.max_log_length-1]
 
 
391
        self.logbox.set_focus(len(self.logbox.body.contents),
 
 
392
                              coming_from=u"above")
 
 
395
    def toggle_log_display(self):
 
 
396
        """Toggle visibility of the log buffer."""
 
 
397
        self.log_visible = not self.log_visible
 
 
399
        self.log_message(u"Log visibility changed to: "
 
 
400
                         + unicode(self.log_visible))
 
 
402
    def change_log_display(self):
 
 
403
        """Change type of log display.
 
 
404
        Currently, this toggles wrapping of text lines."""
 
 
405
        if self.log_wrap == u"clip":
 
 
406
            self.log_wrap = u"any"
 
 
408
            self.log_wrap = u"clip"
 
 
409
        for textwidget in self.log:
 
 
410
            textwidget.set_wrap_mode(self.log_wrap)
 
 
411
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
 
413
    def find_and_remove_client(self, path, name):
 
 
414
        """Find an client from its object path and remove it.
 
 
416
        This is connected to the ClientRemoved signal from the
 
 
417
        Mandos server object."""
 
 
419
            client = self.clients_dict[path]
 
 
423
        self.remove_client(client, path)
 
 
425
    def add_new_client(self, path):
 
 
426
        client_proxy_object = self.bus.get_object(self.busname, path)
 
 
427
        self.add_client(MandosClientWidget(server_proxy_object
 
 
430
                                           =client_proxy_object,
 
 
439
    def add_client(self, client, path=None):
 
 
440
        self.clients.append(client)
 
 
442
            path = client.proxy.object_path
 
 
443
        self.clients_dict[path] = client
 
 
444
        self.clients.sort(None, lambda c: c.properties[u"name"])
 
 
447
    def remove_client(self, client, path=None):
 
 
448
        self.clients.remove(client)
 
 
450
            path = client.proxy.object_path
 
 
451
        del self.clients_dict[path]
 
 
452
        if not self.clients_dict:
 
 
453
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
 
 
454
            # is completely emptied, we need to recreate it.
 
 
455
            self.clients = urwid.SimpleListWalker([])
 
 
460
        """Redraw the screen"""
 
 
461
        canvas = self.topwidget.render(self.size, focus=True)
 
 
462
        self.screen.draw_screen(self.size, canvas)
 
 
465
        """Start the main loop and exit when it's done."""
 
 
467
        self._input_callback_tag = (gobject.io_add_watch
 
 
472
        # Main loop has finished, we should close everything now
 
 
473
        gobject.source_remove(self._input_callback_tag)
 
 
477
        self.main_loop.quit()
 
 
479
    def process_input(self, source, condition):
 
 
480
        keys = self.screen.get_input()
 
 
481
        translations = { u"ctrl n": u"down",      # Emacs
 
 
482
                         u"ctrl p": u"up",        # Emacs
 
 
483
                         u"ctrl v": u"page down", # Emacs
 
 
484
                         u"meta v": u"page up",   # Emacs
 
 
485
                         u" ": u"page down",      # less
 
 
486
                         u"f": u"page down",      # less
 
 
487
                         u"b": u"page up",        # less
 
 
493
                key = translations[key]
 
 
494
            except KeyError:    # :-)
 
 
497
            if key == u"q" or key == u"Q":
 
 
500
            elif key == u"window resize":
 
 
501
                self.size = self.screen.get_cols_rows()
 
 
503
            elif key == u"\f":  # Ctrl-L
 
 
505
            elif key == u"l" or key == u"D":
 
 
506
                self.toggle_log_display()
 
 
508
            elif key == u"w" or key == u"i":
 
 
509
                self.change_log_display()
 
 
511
            elif key == u"?" or key == u"f1" or key == u"esc":
 
 
512
                if not self.log_visible:
 
 
513
                    self.log_visible = True
 
 
515
                self.log_message_raw((u"bold",
 
 
519
                                            u"l: Log window toggle",
 
 
520
                                            u"TAB: Switch window",
 
 
522
                self.log_message_raw((u"bold",
 
 
528
                                             u"s: Start new checker",
 
 
533
                if self.topwidget.get_focus() is self.logbox:
 
 
534
                    self.topwidget.set_focus(0)
 
 
536
                    self.topwidget.set_focus(self.logbox)
 
 
538
            #elif (key == u"end" or key == u"meta >" or key == u"G"
 
 
540
            #    pass            # xxx end-of-buffer
 
 
541
            #elif (key == u"home" or key == u"meta <" or key == u"g"
 
 
543
            #    pass            # xxx beginning-of-buffer
 
 
544
            #elif key == u"ctrl e" or key == u"$":
 
 
545
            #    pass            # xxx move-end-of-line
 
 
546
            #elif key == u"ctrl a" or key == u"^":
 
 
547
            #    pass            # xxx move-beginning-of-line
 
 
548
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
 
550
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
 
553
            #    pass            # scroll up log
 
 
555
            #    pass            # scroll down log
 
 
556
            elif self.topwidget.selectable():
 
 
557
                self.topwidget.keypress(self.size, key)
 
 
565
    ui.log_message(unicode(e))