2
2
# -*- mode: python; coding: utf-8 -*-
 
 
4
# Mandos Monitor - Control and monitor the Mandos server
 
 
6
# Copyright © 2009-2016 Teddy Hogeborn
 
 
7
# Copyright © 2009-2016 Björn Påhlsson
 
 
9
# This program is free software: you can redistribute it and/or modify
 
 
10
# it under the terms of the GNU General Public License as published by
 
 
11
# the Free Software Foundation, either version 3 of the License, or
 
 
12
# (at your option) any later version.
 
 
14
#     This program is distributed in the hope that it will be useful,
 
 
15
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
 
 
16
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
 
17
#     GNU General Public License for more details.
 
 
19
# You should have received a copy of the GNU General Public License
 
 
20
# along with this program.  If not, see
 
 
21
# <http://www.gnu.org/licenses/>.
 
 
23
# Contact the authors at <mandos@recompile.se>.
 
4
 
from __future__ import division, absolute_import, with_statement
 
 
26
from __future__ import (division, absolute_import, print_function,
 
 
29
    from future_builtins import *
 
 
15
41
from dbus.mainloop.glib import DBusGMainLoop
 
 
43
    from gi.repository import GObject
 
 
45
    import gobject as GObject
 
24
 
locale.setlocale(locale.LC_ALL, u'')
 
 
51
if sys.version_info.major == 2:
 
 
54
locale.setlocale(locale.LC_ALL, '')
 
27
57
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
 
29
59
# Some useful constants
 
30
 
domain = 'se.bsnet.fukt'
 
 
60
domain = 'se.recompile'
 
31
61
server_interface = domain + '.Mandos'
 
32
62
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)
 
 
66
    dbus.OBJECT_MANAGER_IFACE
 
 
67
except AttributeError:
 
 
68
    dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
 
 
70
def isoformat_to_datetime(iso):
 
 
71
    "Parse an ISO 8601 date string to a datetime.datetime()"
 
 
74
    d, t = iso.split("T", 1)
 
 
75
    year, month, day = d.split("-", 2)
 
 
76
    hour, minute, second = t.split(":", 2)
 
 
77
    second, fraction = divmod(float(second), 1)
 
 
78
    return datetime.datetime(int(year),
 
 
83
                             int(second),           # Whole seconds
 
 
84
                             int(fraction*1000000)) # Microseconds
 
43
86
class MandosClientPropertyCache(object):
 
44
87
    """This wraps a Mandos Client D-Bus proxy object, caches the
 
45
88
    properties and calls a hook function when any of them are
 
48
 
    def __init__(self, proxy_object=None, *args, **kwargs):
 
 
91
    def __init__(self, proxy_object=None, properties=None, **kwargs):
 
49
92
        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))
 
61
 
        #XXX This break good super behaviour!
 
62
 
#        super(MandosClientPropertyCache, self).__init__(
 
 
93
        self.properties = dict() if properties is None else properties
 
 
94
        self.property_changed_match = (
 
 
95
            self.proxy.connect_to_signal("PropertiesChanged",
 
 
96
                                         self.properties_changed,
 
 
97
                                         dbus.PROPERTIES_IFACE,
 
 
100
        if properties is None:
 
 
101
            self.properties.update(
 
 
102
                self.proxy.GetAll(client_interface,
 
 
104
                                  = dbus.PROPERTIES_IFACE))
 
 
106
        super(MandosClientPropertyCache, self).__init__(**kwargs)
 
65
 
    def property_changed(self, property=None, value=None):
 
66
 
        """This is called whenever we get a PropertyChanged signal
 
67
 
        It updates the changed property in the "properties" dict.
 
 
108
    def properties_changed(self, interface, properties, invalidated):
 
 
109
        """This is called whenever we get a PropertiesChanged signal
 
 
110
        It updates the changed properties in the "properties" dict.
 
69
112
        # Update properties dict with new value
 
70
 
        self.properties[property] = value
 
 
113
        if interface == client_interface:
 
 
114
            self.properties.update(properties)
 
 
117
        self.property_changed_match.remove()
 
73
120
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
 
 
86
133
        self.logger = logger
 
 
135
        self._update_timer_callback_tag = None
 
88
137
        # The widget shown normally
 
89
 
        self._text_widget = urwid.Text(u"")
 
 
138
        self._text_widget = urwid.Text("")
 
90
139
        # The widget shown when we have focus
 
91
 
        self._focus_text_widget = urwid.Text(u"")
 
92
 
        super(MandosClientWidget, self).__init__(
 
93
 
            update_hook=update_hook, delete_hook=delete_hook,
 
 
140
        self._focus_text_widget = urwid.Text("")
 
 
141
        super(MandosClientWidget, self).__init__(**kwargs)
 
96
143
        self.opened = False
 
97
 
        self.proxy.connect_to_signal(u"CheckerCompleted",
 
98
 
                                     self.checker_completed,
 
101
 
        self.proxy.connect_to_signal(u"CheckerStarted",
 
102
 
                                     self.checker_started,
 
105
 
        self.proxy.connect_to_signal(u"GotSecret",
 
109
 
        self.proxy.connect_to_signal(u"NeedApproval",
 
113
 
        self.proxy.connect_to_signal(u"Rejected",
 
 
145
        self.match_objects = (
 
 
146
            self.proxy.connect_to_signal("CheckerCompleted",
 
 
147
                                         self.checker_completed,
 
 
150
            self.proxy.connect_to_signal("CheckerStarted",
 
 
151
                                         self.checker_started,
 
 
154
            self.proxy.connect_to_signal("GotSecret",
 
 
158
            self.proxy.connect_to_signal("NeedApproval",
 
 
162
            self.proxy.connect_to_signal("Rejected",
 
 
166
        self.logger('Created client {}'
 
 
167
                    .format(self.properties["Name"]), level=0)
 
 
169
    def using_timer(self, flag):
 
 
170
        """Call this method with True or False when timer should be
 
 
171
        activated or deactivated.
 
 
173
        if flag and self._update_timer_callback_tag is None:
 
 
174
            # Will update the shown timer value every second
 
 
175
            self._update_timer_callback_tag = (GObject.timeout_add
 
 
178
        elif not (flag or self._update_timer_callback_tag is None):
 
 
179
            GObject.source_remove(self._update_timer_callback_tag)
 
 
180
            self._update_timer_callback_tag = None
 
118
182
    def checker_completed(self, exitstatus, condition, command):
 
119
183
        if exitstatus == 0:
 
120
 
            #self.logger(u'Checker for client %s (command "%s")'
 
122
 
            #            % (self.properties[u"name"], command))
 
 
184
            self.logger('Checker for client {} (command "{}")'
 
 
185
                        ' succeeded'.format(self.properties["Name"],
 
124
190
        if os.WIFEXITED(condition):
 
125
 
            self.logger(u'Checker for client %s (command "%s")'
 
126
 
                        u' failed with exit code %s'
 
127
 
                        % (self.properties[u"name"], command,
 
128
 
                           os.WEXITSTATUS(condition)))
 
130
 
        if os.WIFSIGNALED(condition):
 
131
 
            self.logger(u'Checker for client %s (command "%s")'
 
132
 
                        u' was killed by signal %s'
 
133
 
                        % (self.properties[u"name"], command,
 
134
 
                           os.WTERMSIG(condition)))
 
136
 
        if os.WCOREDUMP(condition):
 
137
 
            self.logger(u'Checker for client %s (command "%s")'
 
139
 
                        % (self.properties[u"name"], command))
 
140
 
        self.logger(u'Checker for client %s completed mysteriously')
 
 
191
            self.logger('Checker for client {} (command "{}") failed'
 
 
193
                        .format(self.properties["Name"], command,
 
 
194
                                os.WEXITSTATUS(condition)))
 
 
195
        elif os.WIFSIGNALED(condition):
 
 
196
            self.logger('Checker for client {} (command "{}") was'
 
 
197
                        ' killed by signal {}'
 
 
198
                        .format(self.properties["Name"], command,
 
 
199
                                os.WTERMSIG(condition)))
 
142
202
    def checker_started(self, command):
 
143
 
        #self.logger(u'Client %s started checker "%s"'
 
144
 
        #            % (self.properties[u"name"], unicode(command)))
 
 
203
        """Server signals that a checker started."""
 
 
204
        self.logger('Client {} started checker "{}"'
 
 
205
                    .format(self.properties["Name"],
 
147
208
    def got_secret(self):
 
148
 
        self.logger(u'Client %s received its secret'
 
149
 
                    % self.properties[u"name"])
 
 
209
        self.logger('Client {} received its secret'
 
 
210
                    .format(self.properties["Name"]))
 
151
212
    def need_approval(self, timeout, default):
 
153
 
            message = u'Client %s needs approval within %s seconds'
 
 
214
            message = 'Client {} needs approval within {} seconds'
 
155
 
            message = u'Client %s will get its secret in %s seconds'
 
157
 
                    % (self.properties[u"name"], timeout/1000))
 
 
216
            message = 'Client {} will get its secret in {} seconds'
 
 
217
        self.logger(message.format(self.properties["Name"],
 
159
220
    def rejected(self, reason):
 
160
 
        self.logger(u'Client %s was rejected; reason: %s'
 
161
 
                    % (self.properties[u"name"], reason))
 
 
221
        self.logger('Client {} was rejected; reason: {}'
 
 
222
                    .format(self.properties["Name"], reason))
 
163
224
    def selectable(self):
 
164
225
        """Make this a "selectable" widget.
 
165
226
        This overrides the method from urwid.FlowWidget."""
 
168
 
    def rows(self, (maxcol,), focus=False):
 
 
229
    def rows(self, maxcolrow, focus=False):
 
169
230
        """How many rows this widget will occupy might depend on
 
170
231
        whether we have focus or not.
 
171
232
        This overrides the method from urwid.FlowWidget"""
 
172
 
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
 
233
        return self.current_widget(focus).rows(maxcolrow, focus=focus)
 
174
235
    def current_widget(self, focus=False):
 
175
236
        if focus or self.opened:
 
 
179
240
    def update(self):
 
180
241
        "Called when what is visible on the screen should be updated."
 
181
242
        # How to add standout mode to a style
 
182
 
        with_standout = { u"normal": u"standout",
 
183
 
                          u"bold": u"bold-standout",
 
185
 
                              u"underline-blink-standout",
 
186
 
                          u"bold-underline-blink":
 
187
 
                              u"bold-underline-blink-standout",
 
 
243
        with_standout = { "normal": "standout",
 
 
244
                          "bold": "bold-standout",
 
 
246
                              "underline-blink-standout",
 
 
247
                          "bold-underline-blink":
 
 
248
                              "bold-underline-blink-standout",
 
190
251
        # Rebuild focus and non-focus widgets using current properties
 
192
253
        # Base part of a client. Name!
 
193
 
        self._text = (u'%(name)s: '
 
194
 
                      % {u"name": self.properties[u"name"]})
 
196
 
        if self.properties[u"approved_pending"]:
 
197
 
            if self.properties[u"approved_by_default"]:
 
198
 
                self._text += u"Connection established to client. (d)eny?"
 
200
 
                self._text += u"Seeks approval to send secret. (a)pprove?"
 
 
254
        base = '{name}: '.format(name=self.properties["Name"])
 
 
255
        if not self.properties["Enabled"]:
 
 
257
            self.using_timer(False)
 
 
258
        elif self.properties["ApprovalPending"]:
 
 
259
            timeout = datetime.timedelta(milliseconds
 
 
262
            last_approval_request = isoformat_to_datetime(
 
 
263
                self.properties["LastApprovalRequest"])
 
 
264
            if last_approval_request is not None:
 
 
265
                timer = max(timeout - (datetime.datetime.utcnow()
 
 
266
                                       - last_approval_request),
 
 
267
                            datetime.timedelta())
 
 
269
                timer = datetime.timedelta()
 
 
270
            if self.properties["ApprovedByDefault"]:
 
 
271
                message = "Approval in {}. (d)eny?"
 
 
273
                message = "Denial in {}. (a)pprove?"
 
 
274
            message = message.format(str(timer).rsplit(".", 1)[0])
 
 
275
            self.using_timer(True)
 
 
276
        elif self.properties["LastCheckerStatus"] != 0:
 
 
277
            # When checker has failed, show timer until client expires
 
 
278
            expires = self.properties["Expires"]
 
 
280
                timer = datetime.timedelta(0)
 
 
282
                expires = (datetime.datetime.strptime
 
 
283
                           (expires, '%Y-%m-%dT%H:%M:%S.%f'))
 
 
284
                timer = max(expires - datetime.datetime.utcnow(),
 
 
285
                            datetime.timedelta())
 
 
286
            message = ('A checker has failed! Time until client'
 
 
288
                       .format(str(timer).rsplit(".", 1)[0]))
 
 
289
            self.using_timer(True)
 
202
 
            self._text += (u'%(enabled)s'
 
205
 
                                if self.properties[u"enabled"]
 
 
292
            self.using_timer(False)
 
 
293
        self._text = "{}{}".format(base, message)
 
207
295
        if not urwid.supports_unicode():
 
208
296
            self._text = self._text.encode("ascii", "replace")
 
209
 
        textlist = [(u"normal", self._text)]
 
 
297
        textlist = [("normal", self._text)]
 
210
298
        self._text_widget.set_text(textlist)
 
211
299
        self._focus_text_widget.set_text([(with_standout[text[0]],
 
 
220
308
        if self.update_hook is not None:
 
221
309
            self.update_hook()
 
 
311
    def update_timer(self):
 
 
312
        """called by GObject. Will indefinitely loop until
 
 
313
        GObject.source_remove() on tag is called"""
 
 
315
        return True             # Keep calling this
 
 
317
    def delete(self, **kwargs):
 
 
318
        if self._update_timer_callback_tag is not None:
 
 
319
            GObject.source_remove(self._update_timer_callback_tag)
 
 
320
            self._update_timer_callback_tag = None
 
 
321
        for match in self.match_objects:
 
 
323
        self.match_objects = ()
 
224
324
        if self.delete_hook is not None:
 
225
325
            self.delete_hook(self)
 
 
326
        return super(MandosClientWidget, self).delete(**kwargs)
 
227
 
    def render(self, (maxcol,), focus=False):
 
 
328
    def render(self, maxcolrow, focus=False):
 
228
329
        """Render differently if we have focus.
 
229
330
        This overrides the method from urwid.FlowWidget"""
 
230
 
        return self.current_widget(focus).render((maxcol,),
 
 
331
        return self.current_widget(focus).render(maxcolrow,
 
233
 
    def keypress(self, (maxcol,), key):
 
 
334
    def keypress(self, maxcolrow, key):
 
235
336
        This overrides the method from urwid.FlowWidget"""
 
237
 
            self.proxy.Enable(dbus_interface = client_interface)
 
239
 
            self.proxy.Disable(dbus_interface = client_interface)
 
 
338
            self.proxy.Set(client_interface, "Enabled",
 
 
339
                           dbus.Boolean(True), ignore_reply = True,
 
 
340
                           dbus_interface = dbus.PROPERTIES_IFACE)
 
 
342
            self.proxy.Set(client_interface, "Enabled", False,
 
 
344
                           dbus_interface = dbus.PROPERTIES_IFACE)
 
241
346
            self.proxy.Approve(dbus.Boolean(True, variant_level=1),
 
242
 
                               dbus_interface = client_interface)
 
 
347
                               dbus_interface = client_interface,
 
244
350
            self.proxy.Approve(dbus.Boolean(False, variant_level=1),
 
245
 
                                  dbus_interface = client_interface)
 
246
 
        elif key == u"r" or key == u"_" or key == u"ctrl k":
 
 
351
                                  dbus_interface = client_interface,
 
 
353
        elif key == "R" or key == "_" or key == "ctrl k":
 
247
354
            self.server_proxy_object.RemoveClient(self.proxy
 
250
 
            self.proxy.StartChecker(dbus_interface = client_interface)
 
252
 
            self.proxy.StopChecker(dbus_interface = client_interface)
 
254
 
            self.proxy.CheckedOK(dbus_interface = client_interface)
 
 
358
            self.proxy.Set(client_interface, "CheckerRunning",
 
 
359
                           dbus.Boolean(True), ignore_reply = True,
 
 
360
                           dbus_interface = dbus.PROPERTIES_IFACE)
 
 
362
            self.proxy.Set(client_interface, "CheckerRunning",
 
 
363
                           dbus.Boolean(False), ignore_reply = True,
 
 
364
                           dbus_interface = dbus.PROPERTIES_IFACE)
 
 
366
            self.proxy.CheckedOK(dbus_interface = client_interface,
 
256
 
#         elif key == u"p" or key == "=":
 
 
369
#         elif key == "p" or key == "=":
 
257
370
#             self.proxy.pause()
 
258
 
#         elif key == u"u" or key == ":":
 
 
371
#         elif key == "u" or key == ":":
 
259
372
#             self.proxy.unpause()
 
260
 
#         elif key == u"RET":
 
263
 
#            self.proxy.Approve(True)
 
265
 
#            self.proxy.Approve(False)
 
269
 
    def property_changed(self, property=None, value=None,
 
271
 
        """Call self.update() if old value is not new value.
 
 
378
    def properties_changed(self, interface, properties, invalidated):
 
 
379
        """Call self.update() if any properties changed.
 
272
380
        This overrides the method from MandosClientPropertyCache"""
 
273
 
        property_name = unicode(property)
 
274
 
        old_value = self.properties.get(property_name)
 
275
 
        super(MandosClientWidget, self).property_changed(
 
276
 
            property=property, value=value, *args, **kwargs)
 
277
 
        if self.properties.get(property_name) != old_value:
 
 
381
        old_values = { key: self.properties.get(key)
 
 
382
                       for key in properties.keys() }
 
 
383
        super(MandosClientWidget, self).properties_changed(
 
 
384
            interface, properties, invalidated)
 
 
385
        if any(old_values[key] != self.properties.get(key)
 
 
386
               for key in old_values):
 
 
294
403
    """This is the entire user interface - the whole screen
 
295
404
    with boxes, lists of client widgets, etc.
 
297
 
    def __init__(self, max_log_length=1000):
 
 
406
    def __init__(self, max_log_length=1000, log_level=1):
 
298
407
        DBusGMainLoop(set_as_default=True)
 
300
409
        self.screen = urwid.curses_display.Screen()
 
302
411
        self.screen.register_palette((
 
304
 
                 u"default", u"default", None),
 
306
 
                 u"default", u"default", u"bold"),
 
308
 
                 u"default", u"default", u"underline"),
 
310
 
                 u"default", u"default", u"standout"),
 
311
 
                (u"bold-underline-blink",
 
312
 
                 u"default", u"default", (u"bold", u"underline")),
 
314
 
                 u"default", u"default", (u"bold", u"standout")),
 
315
 
                (u"underline-blink-standout",
 
316
 
                 u"default", u"default", (u"underline", u"standout")),
 
317
 
                (u"bold-underline-blink-standout",
 
318
 
                 u"default", u"default", (u"bold", u"underline",
 
 
413
                 "default", "default", None),
 
 
415
                 "bold", "default", "bold"),
 
 
417
                 "underline,blink", "default", "underline,blink"),
 
 
419
                 "standout", "default", "standout"),
 
 
420
                ("bold-underline-blink",
 
 
421
                 "bold,underline,blink", "default", "bold,underline,blink"),
 
 
423
                 "bold,standout", "default", "bold,standout"),
 
 
424
                ("underline-blink-standout",
 
 
425
                 "underline,blink,standout", "default",
 
 
426
                 "underline,blink,standout"),
 
 
427
                ("bold-underline-blink-standout",
 
 
428
                 "bold,underline,blink,standout", "default",
 
 
429
                 "bold,underline,blink,standout"),
 
322
432
        if urwid.supports_unicode():
 
323
 
            self.divider = u"─" # \u2500
 
324
 
            #self.divider = u"━" # \u2501
 
 
433
            self.divider = "─" # \u2500
 
 
434
            #self.divider = "━" # \u2501
 
326
 
            #self.divider = u"-" # \u002d
 
327
 
            self.divider = u"_" # \u005f
 
 
436
            #self.divider = "-" # \u002d
 
 
437
            self.divider = "_" # \u005f
 
329
439
        self.screen.start()
 
 
344
456
        # This keeps track of whether self.uilist currently has
 
345
457
        # self.logbox in it or not
 
346
458
        self.log_visible = True
 
347
 
        self.log_wrap = u"any"
 
 
459
        self.log_wrap = "any"
 
350
 
        self.log_message_raw((u"bold",
 
351
 
                              u"Mandos Monitor version " + version))
 
352
 
        self.log_message_raw((u"bold",
 
 
462
        self.log_message_raw(("bold",
 
 
463
                              "Mandos Monitor version " + version))
 
 
464
        self.log_message_raw(("bold",
 
355
467
        self.busname = domain + '.Mandos'
 
356
 
        self.main_loop = gobject.MainLoop()
 
357
 
        self.bus = dbus.SystemBus()
 
358
 
        mandos_dbus_objc = self.bus.get_object(
 
359
 
            self.busname, u"/", follow_name_owner_changes=True)
 
360
 
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
364
 
            mandos_clients = (self.mandos_serv
 
365
 
                              .GetAllClientsWithProperties())
 
366
 
        except dbus.exceptions.DBusException:
 
367
 
            mandos_clients = dbus.Dictionary()
 
370
 
         .connect_to_signal(u"ClientRemoved",
 
371
 
                            self.find_and_remove_client,
 
372
 
                            dbus_interface=server_interface,
 
375
 
         .connect_to_signal(u"ClientAdded",
 
377
 
                            dbus_interface=server_interface,
 
380
 
         .connect_to_signal(u"ClientNotFound",
 
381
 
                            self.client_not_found,
 
382
 
                            dbus_interface=server_interface,
 
384
 
        for path, client in mandos_clients.iteritems():
 
385
 
            client_proxy_object = self.bus.get_object(self.busname,
 
387
 
            self.add_client(MandosClientWidget(server_proxy_object
 
390
 
                                               =client_proxy_object,
 
 
468
        self.main_loop = GObject.MainLoop()
 
400
470
    def client_not_found(self, fingerprint, address):
 
401
 
        self.log_message((u"Client with address %s and fingerprint %s"
 
402
 
                          u" could not be found" % (address,
 
 
471
        self.log_message("Client with address {} and fingerprint {}"
 
 
472
                         " could not be found"
 
 
473
                         .format(address, fingerprint))
 
405
475
    def rebuild(self):
 
406
476
        """This rebuilds the User Interface.
 
407
477
        Call this when the widget layout needs to change"""
 
409
479
        #self.uilist.append(urwid.ListBox(self.clients))
 
410
 
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
 
480
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.
 
411
482
                                       #header=urwid.Divider(),
 
413
 
                                       footer=urwid.Divider(div_char=self.divider)))
 
 
485
                                       urwid.Divider(div_char=
 
414
487
        if self.log_visible:
 
415
488
            self.uilist.append(self.logbox)
 
417
489
        self.topwidget = urwid.Pile(self.uilist)
 
419
 
    def log_message(self, message):
 
 
491
    def log_message(self, message, level=1):
 
 
492
        """Log message formatted with timestamp"""
 
 
493
        if level < self.log_level:
 
420
495
        timestamp = datetime.datetime.now().isoformat()
 
421
 
        self.log_message_raw(timestamp + u": " + message)
 
 
496
        self.log_message_raw("{}: {}".format(timestamp, message),
 
423
 
    def log_message_raw(self, markup):
 
 
499
    def log_message_raw(self, markup, level=1):
 
424
500
        """Add a log message to the log buffer."""
 
 
501
        if level < self.log_level:
 
425
503
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
426
504
        if (self.max_log_length
 
427
505
            and len(self.log) > self.max_log_length):
 
428
506
            del self.log[0:len(self.log)-self.max_log_length-1]
 
429
507
        self.logbox.set_focus(len(self.logbox.body.contents),
 
430
 
                              coming_from=u"above")
 
433
511
    def toggle_log_display(self):
 
434
512
        """Toggle visibility of the log buffer."""
 
435
513
        self.log_visible = not self.log_visible
 
437
 
        self.log_message(u"Log visibility changed to: "
 
438
 
                         + unicode(self.log_visible))
 
 
515
        self.log_message("Log visibility changed to: {}"
 
 
516
                         .format(self.log_visible), level=0)
 
440
518
    def change_log_display(self):
 
441
519
        """Change type of log display.
 
442
520
        Currently, this toggles wrapping of text lines."""
 
443
 
        if self.log_wrap == u"clip":
 
444
 
            self.log_wrap = u"any"
 
 
521
        if self.log_wrap == "clip":
 
 
522
            self.log_wrap = "any"
 
446
 
            self.log_wrap = u"clip"
 
 
524
            self.log_wrap = "clip"
 
447
525
        for textwidget in self.log:
 
448
526
            textwidget.set_wrap_mode(self.log_wrap)
 
449
 
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
 
527
        self.log_message("Wrap mode: {}".format(self.log_wrap),
 
451
 
    def find_and_remove_client(self, path, name):
 
452
 
        """Find an client from its object path and remove it.
 
 
530
    def find_and_remove_client(self, path, interfaces):
 
 
531
        """Find a client by its object path and remove it.
 
454
 
        This is connected to the ClientRemoved signal from the
 
 
533
        This is connected to the InterfacesRemoved signal from the
 
455
534
        Mandos server object."""
 
 
535
        if client_interface not in interfaces:
 
 
536
            # Not a Mandos client object; ignore
 
457
539
            client = self.clients_dict[path]
 
 
542
            self.log_message("Unknown client {!r} removed"
 
461
 
        self.remove_client(client, path)
 
463
 
    def add_new_client(self, path):
 
 
547
    def add_new_client(self, path, ifs_and_props):
 
 
548
        """Find a client by its object path and remove it.
 
 
550
        This is connected to the InterfacesAdded signal from the
 
 
551
        Mandos server object.
 
 
553
        if client_interface not in ifs_and_props:
 
 
554
            # Not a Mandos client object; ignore
 
464
556
        client_proxy_object = self.bus.get_object(self.busname, path)
 
465
557
        self.add_client(MandosClientWidget(server_proxy_object
 
466
558
                                           =self.mandos_serv,
 
 
503
593
        """Start the main loop and exit when it's done."""
 
 
594
        self.bus = dbus.SystemBus()
 
 
595
        mandos_dbus_objc = self.bus.get_object(
 
 
596
            self.busname, "/", follow_name_owner_changes=True)
 
 
597
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
 
601
            mandos_clients = (self.mandos_serv
 
 
602
                              .GetAllClientsWithProperties())
 
 
603
            if not mandos_clients:
 
 
604
                self.log_message_raw(("bold", "Note: Server has no clients."))
 
 
605
        except dbus.exceptions.DBusException:
 
 
606
            self.log_message_raw(("bold", "Note: No Mandos server running."))
 
 
607
            mandos_clients = dbus.Dictionary()
 
 
610
         .connect_to_signal("InterfacesRemoved",
 
 
611
                            self.find_and_remove_client,
 
 
613
                            = dbus.OBJECT_MANAGER_IFACE,
 
 
616
         .connect_to_signal("InterfacesAdded",
 
 
619
                            = dbus.OBJECT_MANAGER_IFACE,
 
 
622
         .connect_to_signal("ClientNotFound",
 
 
623
                            self.client_not_found,
 
 
624
                            dbus_interface=server_interface,
 
 
626
        for path, client in mandos_clients.items():
 
 
627
            client_proxy_object = self.bus.get_object(self.busname,
 
 
629
            self.add_client(MandosClientWidget(server_proxy_object
 
 
632
                                               =client_proxy_object,
 
505
 
        self._input_callback_tag = (gobject.io_add_watch
 
 
643
        self._input_callback_tag = (GObject.io_add_watch
 
506
644
                                    (sys.stdin.fileno(),
 
508
646
                                     self.process_input))
 
509
647
        self.main_loop.run()
 
510
648
        # Main loop has finished, we should close everything now
 
511
 
        gobject.source_remove(self._input_callback_tag)
 
 
649
        GObject.source_remove(self._input_callback_tag)
 
512
650
        self.screen.stop()
 
 
532
670
            except KeyError:    # :-)
 
535
 
            if key == u"q" or key == u"Q":
 
 
673
            if key == "q" or key == "Q":
 
538
 
            elif key == u"window resize":
 
 
676
            elif key == "window resize":
 
539
677
                self.size = self.screen.get_cols_rows()
 
541
 
            elif key == u"\f":  # Ctrl-L
 
 
679
            elif key == "ctrl l":
 
543
 
            elif key == u"l" or key == u"D":
 
 
682
            elif key == "l" or key == "D":
 
544
683
                self.toggle_log_display()
 
546
 
            elif key == u"w" or key == u"i":
 
 
685
            elif key == "w" or key == "i":
 
547
686
                self.change_log_display()
 
549
 
            elif key == u"?" or key == u"f1" or key == u"esc":
 
 
688
            elif key == "?" or key == "f1" or key == "esc":
 
550
689
                if not self.log_visible:
 
551
690
                    self.log_visible = True
 
553
 
                self.log_message_raw((u"bold",
 
557
 
                                            u"l: Log window toggle",
 
558
 
                                            u"TAB: Switch window",
 
560
 
                self.log_message_raw((u"bold",
 
566
 
                                             u"s: Start new checker",
 
 
692
                self.log_message_raw(("bold",
 
 
696
                                            "l: Log window toggle",
 
 
697
                                            "TAB: Switch window",
 
 
698
                                            "w: Wrap (log lines)",
 
 
699
                                            "v: Toggle verbose log",
 
 
701
                self.log_message_raw(("bold",
 
 
707
                                             "s: Start new checker",
 
573
714
                if self.topwidget.get_focus() is self.logbox:
 
574
715
                    self.topwidget.set_focus(0)
 
576
717
                    self.topwidget.set_focus(self.logbox)
 
578
 
            #elif (key == u"end" or key == u"meta >" or key == u"G"
 
 
720
                if self.log_level == 0:
 
 
722
                    self.log_message("Verbose mode: Off")
 
 
725
                    self.log_message("Verbose mode: On")
 
 
726
            #elif (key == "end" or key == "meta >" or key == "G"
 
580
728
            #    pass            # xxx end-of-buffer
 
581
 
            #elif (key == u"home" or key == u"meta <" or key == u"g"
 
 
729
            #elif (key == "home" or key == "meta <" or key == "g"
 
583
731
            #    pass            # xxx beginning-of-buffer
 
584
 
            #elif key == u"ctrl e" or key == u"$":
 
 
732
            #elif key == "ctrl e" or key == "$":
 
585
733
            #    pass            # xxx move-end-of-line
 
586
 
            #elif key == u"ctrl a" or key == u"^":
 
 
734
            #elif key == "ctrl a" or key == "^":
 
587
735
            #    pass            # xxx move-beginning-of-line
 
588
 
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
 
736
            #elif key == "ctrl b" or key == "meta (" or key == "h":
 
589
737
            #    pass            # xxx left
 
590
 
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
 
738
            #elif key == "ctrl f" or key == "meta )" or key == "l":
 
591
739
            #    pass            # xxx right
 
593
741
            #    pass            # scroll up log
 
595
743
            #    pass            # scroll down log
 
596
744
            elif self.topwidget.selectable():
 
597
745
                self.topwidget.keypress(self.size, key)