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))