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
def isoformat_to_datetime(iso):
 
 
41
    "Parse an ISO 8601 date string to a datetime.datetime()"
 
 
44
    d, t = iso.split(u"T", 1)
 
 
45
    year, month, day = d.split(u"-", 2)
 
 
46
    hour, minute, second = t.split(u":", 2)
 
 
47
    second, fraction = divmod(float(second), 1)
 
 
48
    return datetime.datetime(int(year),
 
 
53
                             int(second),           # Whole seconds
 
 
54
                             int(fraction*1000000)) # Microseconds
 
 
56
class MandosClientPropertyCache(object):
 
 
57
    """This wraps a Mandos Client D-Bus proxy object, caches the
 
 
58
    properties and calls a hook function when any of them are
 
 
61
    def __init__(self, proxy_object=None, *args, **kwargs):
 
 
62
        self.proxy = proxy_object # Mandos Client proxy object
 
 
64
        self.properties = dict()
 
 
65
        self.proxy.connect_to_signal(u"PropertyChanged",
 
 
66
                                     self.property_changed,
 
 
70
        self.properties.update(
 
 
71
            self.proxy.GetAll(client_interface,
 
 
72
                              dbus_interface = dbus.PROPERTIES_IFACE))
 
 
73
        super(MandosClientPropertyCache, self).__init__(
 
 
74
            proxy_object=proxy_object, *args, **kwargs)
 
 
76
    def property_changed(self, property=None, value=None):
 
 
77
        """This is called whenever we get a PropertyChanged signal
 
 
78
        It updates the changed property in the "properties" dict.
 
 
80
        # Update properties dict with new value
 
 
81
        self.properties[property] = value
 
 
84
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
 
 
85
    """A Mandos Client which is visible on the screen.
 
 
88
    def __init__(self, server_proxy_object=None, update_hook=None,
 
 
89
                 delete_hook=None, logger=None, *args, **kwargs):
 
 
91
        self.update_hook = update_hook
 
 
93
        self.delete_hook = delete_hook
 
 
94
        # Mandos Server proxy object
 
 
95
        self.server_proxy_object = server_proxy_object
 
 
99
        self._update_timer_callback_tag = None
 
 
100
        self.last_checker_failed = False
 
 
102
        # The widget shown normally
 
 
103
        self._text_widget = urwid.Text(u"")
 
 
104
        # The widget shown when we have focus
 
 
105
        self._focus_text_widget = urwid.Text(u"")
 
 
106
        super(MandosClientWidget, self).__init__(
 
 
107
            update_hook=update_hook, delete_hook=delete_hook,
 
 
111
        self.proxy.connect_to_signal(u"CheckerCompleted",
 
 
112
                                     self.checker_completed,
 
 
115
        self.proxy.connect_to_signal(u"CheckerStarted",
 
 
116
                                     self.checker_started,
 
 
119
        self.proxy.connect_to_signal(u"GotSecret",
 
 
123
        self.proxy.connect_to_signal(u"Rejected",
 
 
127
        last_checked_ok = isoformat_to_datetime(self.properties
 
 
129
        if last_checked_ok is None:
 
 
130
            self.last_checker_failed = True
 
 
132
            self.last_checker_failed = ((datetime.datetime.utcnow()
 
 
136
                                         self.properties["interval"]))
 
 
137
        if self.last_checker_failed:
 
 
138
            self._update_timer_callback_tag = (gobject.timeout_add
 
 
142
    def checker_completed(self, exitstatus, condition, command):
 
 
144
            if self.last_checker_failed:
 
 
145
                self.last_checker_failed = False
 
 
146
                gobject.source_remove(self._update_timer_callback_tag)
 
 
147
                self._update_timer_callback_tag = None
 
 
148
            self.logger(u'Checker for client %s (command "%s")'
 
 
150
                        % (self.properties[u"name"], command))
 
 
154
        if not self.last_checker_failed:
 
 
155
            self.last_checker_failed = True
 
 
156
            self._update_timer_callback_tag = (gobject.timeout_add
 
 
159
        if os.WIFEXITED(condition):
 
 
160
            self.logger(u'Checker for client %s (command "%s")'
 
 
161
                        u' failed with exit code %s'
 
 
162
                        % (self.properties[u"name"], command,
 
 
163
                           os.WEXITSTATUS(condition)))
 
 
164
        elif os.WIFSIGNALED(condition):
 
 
165
            self.logger(u'Checker for client %s (command "%s")'
 
 
166
                        u' was killed by signal %s'
 
 
167
                        % (self.properties[u"name"], command,
 
 
168
                           os.WTERMSIG(condition)))
 
 
169
        elif os.WCOREDUMP(condition):
 
 
170
            self.logger(u'Checker for client %s (command "%s")'
 
 
172
                        % (self.properties[u"name"], command))
 
 
174
            self.logger(u'Checker for client %s completed mysteriously')
 
 
177
    def checker_started(self, command):
 
 
178
        self.logger(u'Client %s started checker "%s"'
 
 
179
                    % (self.properties[u"name"], unicode(command)))
 
 
181
    def got_secret(self):
 
 
182
        self.logger(u'Client %s received its secret'
 
 
183
                    % self.properties[u"name"])
 
 
186
        self.logger(u'Client %s was rejected'
 
 
187
                    % self.properties[u"name"])
 
 
189
    def selectable(self):
 
 
190
        """Make this a "selectable" widget.
 
 
191
        This overrides the method from urwid.FlowWidget."""
 
 
194
    def rows(self, (maxcol,), focus=False):
 
 
195
        """How many rows this widget will occupy might depend on
 
 
196
        whether we have focus or not.
 
 
197
        This overrides the method from urwid.FlowWidget"""
 
 
198
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
 
200
    def current_widget(self, focus=False):
 
 
201
        if focus or self.opened:
 
 
202
            return self._focus_widget
 
 
206
        "Called when what is visible on the screen should be updated."
 
 
207
        # How to add standout mode to a style
 
 
208
        with_standout = { u"normal": u"standout",
 
 
209
                          u"bold": u"bold-standout",
 
 
211
                              u"underline-blink-standout",
 
 
212
                          u"bold-underline-blink":
 
 
213
                              u"bold-underline-blink-standout",
 
 
216
        # Rebuild focus and non-focus widgets using current properties
 
 
217
        self._text = (u'%(name)s: %(enabled)s%(timer)s'
 
 
218
                      % { u"name": self.properties[u"name"],
 
 
221
                               if self.properties[u"enabled"]
 
 
223
                          u"timer": (unicode(datetime.timedelta
 
 
229
                                                - isoformat_to_datetime
 
 
230
                                                (max((self.properties
 
 
235
                                                    self.properties[u"last_enabled"]))))
 
 
236
                                     if (self.last_checker_failed
 
 
240
        if not urwid.supports_unicode():
 
 
241
            self._text = self._text.encode("ascii", "replace")
 
 
242
        textlist = [(u"normal", self._text)]
 
 
243
        self._text_widget.set_text(textlist)
 
 
244
        self._focus_text_widget.set_text([(with_standout[text[0]],
 
 
246
                                          if isinstance(text, tuple)
 
 
248
                                          for text in textlist])
 
 
249
        self._widget = self._text_widget
 
 
250
        self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
 
 
252
        # Run update hook, if any
 
 
253
        if self.update_hook is not None:
 
 
256
    def update_timer(self):
 
 
259
        return True             # Keep calling this
 
 
262
        if self._update_timer_callback_tag is not None:
 
 
263
            gobject.source_remove(self._update_timer_callback_tag)
 
 
264
            self._update_timer_callback_tag = None
 
 
265
        if self.delete_hook is not None:
 
 
266
            self.delete_hook(self)
 
 
268
    def render(self, (maxcol,), focus=False):
 
 
269
        """Render differently if we have focus.
 
 
270
        This overrides the method from urwid.FlowWidget"""
 
 
271
        return self.current_widget(focus).render((maxcol,),
 
 
274
    def keypress(self, (maxcol,), key):
 
 
276
        This overrides the method from urwid.FlowWidget"""
 
 
277
        if key == u"e" or key == u"+":
 
 
279
        elif key == u"d" or key == u"-":
 
 
281
        elif key == u"r" or key == u"_" or key == u"ctrl k":
 
 
282
            self.server_proxy_object.RemoveClient(self.proxy
 
 
285
            self.proxy.StartChecker()
 
 
287
            self.proxy.StopChecker()
 
 
289
            self.proxy.CheckedOK()
 
 
291
#         elif key == u"p" or key == "=":
 
 
293
#         elif key == u"u" or key == ":":
 
 
294
#             self.proxy.unpause()
 
 
295
#         elif key == u"RET":
 
 
300
    def property_changed(self, property=None, value=None,
 
 
302
        """Call self.update() if old value is not new value.
 
 
303
        This overrides the method from MandosClientPropertyCache"""
 
 
304
        property_name = unicode(property)
 
 
305
        old_value = self.properties.get(property_name)
 
 
306
        super(MandosClientWidget, self).property_changed(
 
 
307
            property=property, value=value, *args, **kwargs)
 
 
308
        if self.properties.get(property_name) != old_value:
 
 
312
class ConstrainedListBox(urwid.ListBox):
 
 
313
    """Like a normal urwid.ListBox, but will consume all "up" or
 
 
314
    "down" key presses, thus not allowing any containing widgets to
 
 
315
    use them as an excuse to shift focus away from this widget.
 
 
317
    def keypress(self, (maxcol, maxrow), key):
 
 
318
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
 
 
319
        if ret in (u"up", u"down"):
 
 
324
class UserInterface(object):
 
 
325
    """This is the entire user interface - the whole screen
 
 
326
    with boxes, lists of client widgets, etc.
 
 
328
    def __init__(self, max_log_length=1000):
 
 
329
        DBusGMainLoop(set_as_default=True)
 
 
331
        self.screen = urwid.curses_display.Screen()
 
 
333
        self.screen.register_palette((
 
 
335
                 u"default", u"default", None),
 
 
337
                 u"default", u"default", u"bold"),
 
 
339
                 u"default", u"default", u"underline"),
 
 
341
                 u"default", u"default", u"standout"),
 
 
342
                (u"bold-underline-blink",
 
 
343
                 u"default", u"default", (u"bold", u"underline")),
 
 
345
                 u"default", u"default", (u"bold", u"standout")),
 
 
346
                (u"underline-blink-standout",
 
 
347
                 u"default", u"default", (u"underline", u"standout")),
 
 
348
                (u"bold-underline-blink-standout",
 
 
349
                 u"default", u"default", (u"bold", u"underline",
 
 
353
        if urwid.supports_unicode():
 
 
354
            self.divider = u"─" # \u2500
 
 
355
            #self.divider = u"━" # \u2501
 
 
357
            #self.divider = u"-" # \u002d
 
 
358
            self.divider = u"_" # \u005f
 
 
362
        self.size = self.screen.get_cols_rows()
 
 
364
        self.clients = urwid.SimpleListWalker([])
 
 
365
        self.clients_dict = {}
 
 
367
        # We will add Text widgets to this list
 
 
369
        self.max_log_length = max_log_length
 
 
371
        # We keep a reference to the log widget so we can remove it
 
 
372
        # from the ListWalker without it getting destroyed
 
 
373
        self.logbox = ConstrainedListBox(self.log)
 
 
375
        # This keeps track of whether self.uilist currently has
 
 
376
        # self.logbox in it or not
 
 
377
        self.log_visible = True
 
 
378
        self.log_wrap = u"any"
 
 
381
        self.log_message_raw((u"bold",
 
 
382
                              u"Mandos Monitor version " + version))
 
 
383
        self.log_message_raw((u"bold",
 
 
386
        self.busname = domain + '.Mandos'
 
 
387
        self.main_loop = gobject.MainLoop()
 
 
388
        self.bus = dbus.SystemBus()
 
 
389
        mandos_dbus_objc = self.bus.get_object(
 
 
390
            self.busname, u"/", follow_name_owner_changes=True)
 
 
391
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
 
395
            mandos_clients = (self.mandos_serv
 
 
396
                              .GetAllClientsWithProperties())
 
 
397
        except dbus.exceptions.DBusException:
 
 
398
            mandos_clients = dbus.Dictionary()
 
 
401
         .connect_to_signal(u"ClientRemoved",
 
 
402
                            self.find_and_remove_client,
 
 
403
                            dbus_interface=server_interface,
 
 
406
         .connect_to_signal(u"ClientAdded",
 
 
408
                            dbus_interface=server_interface,
 
 
411
         .connect_to_signal(u"ClientNotFound",
 
 
412
                            self.client_not_found,
 
 
413
                            dbus_interface=server_interface,
 
 
415
        for path, client in mandos_clients.iteritems():
 
 
416
            client_proxy_object = self.bus.get_object(self.busname,
 
 
418
            self.add_client(MandosClientWidget(server_proxy_object
 
 
421
                                               =client_proxy_object,
 
 
431
    def client_not_found(self, fingerprint, address):
 
 
432
        self.log_message((u"Client with address %s and fingerprint %s"
 
 
433
                          u" could not be found" % (address,
 
 
437
        """This rebuilds the User Interface.
 
 
438
        Call this when the widget layout needs to change"""
 
 
440
        #self.uilist.append(urwid.ListBox(self.clients))
 
 
441
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
 
442
                                       #header=urwid.Divider(),
 
 
444
                                       footer=urwid.Divider(div_char=self.divider)))
 
 
446
            self.uilist.append(self.logbox)
 
 
448
        self.topwidget = urwid.Pile(self.uilist)
 
 
450
    def log_message(self, message):
 
 
451
        timestamp = datetime.datetime.now().isoformat()
 
 
452
        self.log_message_raw(timestamp + u": " + message)
 
 
454
    def log_message_raw(self, markup):
 
 
455
        """Add a log message to the log buffer."""
 
 
456
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
 
457
        if (self.max_log_length
 
 
458
            and len(self.log) > self.max_log_length):
 
 
459
            del self.log[0:len(self.log)-self.max_log_length-1]
 
 
460
        self.logbox.set_focus(len(self.logbox.body.contents),
 
 
461
                              coming_from=u"above")
 
 
464
    def toggle_log_display(self):
 
 
465
        """Toggle visibility of the log buffer."""
 
 
466
        self.log_visible = not self.log_visible
 
 
468
        self.log_message(u"Log visibility changed to: "
 
 
469
                         + unicode(self.log_visible))
 
 
471
    def change_log_display(self):
 
 
472
        """Change type of log display.
 
 
473
        Currently, this toggles wrapping of text lines."""
 
 
474
        if self.log_wrap == u"clip":
 
 
475
            self.log_wrap = u"any"
 
 
477
            self.log_wrap = u"clip"
 
 
478
        for textwidget in self.log:
 
 
479
            textwidget.set_wrap_mode(self.log_wrap)
 
 
480
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
 
482
    def find_and_remove_client(self, path, name):
 
 
483
        """Find an client from its object path and remove it.
 
 
485
        This is connected to the ClientRemoved signal from the
 
 
486
        Mandos server object."""
 
 
488
            client = self.clients_dict[path]
 
 
492
        self.remove_client(client, path)
 
 
494
    def add_new_client(self, path):
 
 
495
        client_proxy_object = self.bus.get_object(self.busname, path)
 
 
496
        self.add_client(MandosClientWidget(server_proxy_object
 
 
499
                                           =client_proxy_object,
 
 
508
    def add_client(self, client, path=None):
 
 
509
        self.clients.append(client)
 
 
511
            path = client.proxy.object_path
 
 
512
        self.clients_dict[path] = client
 
 
513
        self.clients.sort(None, lambda c: c.properties[u"name"])
 
 
516
    def remove_client(self, client, path=None):
 
 
517
        self.clients.remove(client)
 
 
519
            path = client.proxy.object_path
 
 
520
        del self.clients_dict[path]
 
 
521
        if not self.clients_dict:
 
 
522
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
 
 
523
            # is completely emptied, we need to recreate it.
 
 
524
            self.clients = urwid.SimpleListWalker([])
 
 
529
        """Redraw the screen"""
 
 
530
        canvas = self.topwidget.render(self.size, focus=True)
 
 
531
        self.screen.draw_screen(self.size, canvas)
 
 
534
        """Start the main loop and exit when it's done."""
 
 
536
        self._input_callback_tag = (gobject.io_add_watch
 
 
541
        # Main loop has finished, we should close everything now
 
 
542
        gobject.source_remove(self._input_callback_tag)
 
 
546
        self.main_loop.quit()
 
 
548
    def process_input(self, source, condition):
 
 
549
        keys = self.screen.get_input()
 
 
550
        translations = { u"ctrl n": u"down",      # Emacs
 
 
551
                         u"ctrl p": u"up",        # Emacs
 
 
552
                         u"ctrl v": u"page down", # Emacs
 
 
553
                         u"meta v": u"page up",   # Emacs
 
 
554
                         u" ": u"page down",      # less
 
 
555
                         u"f": u"page down",      # less
 
 
556
                         u"b": u"page up",        # less
 
 
562
                key = translations[key]
 
 
563
            except KeyError:    # :-)
 
 
566
            if key == u"q" or key == u"Q":
 
 
569
            elif key == u"window resize":
 
 
570
                self.size = self.screen.get_cols_rows()
 
 
572
            elif key == u"\f":  # Ctrl-L
 
 
574
            elif key == u"l" or key == u"D":
 
 
575
                self.toggle_log_display()
 
 
577
            elif key == u"w" or key == u"i":
 
 
578
                self.change_log_display()
 
 
580
            elif key == u"?" or key == u"f1" or key == u"esc":
 
 
581
                if not self.log_visible:
 
 
582
                    self.log_visible = True
 
 
584
                self.log_message_raw((u"bold",
 
 
588
                                            u"l: Log window toggle",
 
 
589
                                            u"TAB: Switch window",
 
 
591
                self.log_message_raw((u"bold",
 
 
597
                                             u"s: Start new checker",
 
 
602
                if self.topwidget.get_focus() is self.logbox:
 
 
603
                    self.topwidget.set_focus(0)
 
 
605
                    self.topwidget.set_focus(self.logbox)
 
 
607
            #elif (key == u"end" or key == u"meta >" or key == u"G"
 
 
609
            #    pass            # xxx end-of-buffer
 
 
610
            #elif (key == u"home" or key == u"meta <" or key == u"g"
 
 
612
            #    pass            # xxx beginning-of-buffer
 
 
613
            #elif key == u"ctrl e" or key == u"$":
 
 
614
            #    pass            # xxx move-end-of-line
 
 
615
            #elif key == u"ctrl a" or key == u"^":
 
 
616
            #    pass            # xxx move-beginning-of-line
 
 
617
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
 
619
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
 
622
            #    pass            # scroll up log
 
 
624
            #    pass            # scroll down log
 
 
625
            elif self.topwidget.selectable():
 
 
626
                self.topwidget.keypress(self.size, key)
 
 
634
    ui.log_message(unicode(e))