2
2
# -*- mode: python; coding: utf-8 -*-
 
 
4
# Mandos Monitor - Control and monitor the Mandos server
 
 
6
# Copyright © 2009-2019 Teddy Hogeborn
 
 
7
# Copyright © 2009-2019 Björn Påhlsson
 
 
9
# This file is part of Mandos.
 
 
11
# Mandos is free software: you can redistribute it and/or modify it
 
 
12
# under the terms of the GNU General Public License as published by
 
 
13
# the Free Software Foundation, either version 3 of the License, or
 
 
14
# (at your option) any later version.
 
 
16
#     Mandos is distributed in the hope that it will be useful, but
 
 
17
#     WITHOUT ANY WARRANTY; without even the implied warranty of
 
 
18
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
 
19
#     GNU General Public License for more details.
 
 
21
# You should have received a copy of the GNU General Public License
 
 
22
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
 
 
24
# Contact the authors at <mandos@recompile.se>.
 
4
 
from __future__ import division, absolute_import, with_statement
 
 
27
from __future__ import (division, absolute_import, print_function,
 
 
30
    from future_builtins import *
 
9
39
import urwid.curses_display
 
12
42
from dbus.mainloop.glib import DBusGMainLoop
 
 
43
from gi.repository import GLib
 
 
51
if sys.version_info.major == 2:
 
 
54
locale.setlocale(locale.LC_ALL, '')
 
 
56
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
 
19
58
# Some useful constants
 
20
 
domain = 'se.bsnet.fukt'
 
 
59
domain = 'se.recompile'
 
21
60
server_interface = domain + '.Mandos'
 
22
61
client_interface = domain + '.Mandos.Client'
 
25
 
# Always run in monochrome mode
 
26
 
urwid.curses_display.curses.has_colors = lambda : False
 
28
 
# Urwid doesn't support blinking, but we want it.  Since we have no
 
29
 
# use for underline on its own, we make underline also always blink.
 
30
 
urwid.curses_display.curses.A_UNDERLINE |= (
 
31
 
    urwid.curses_display.curses.A_BLINK)
 
 
65
    dbus.OBJECT_MANAGER_IFACE
 
 
66
except AttributeError:
 
 
67
    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
 
33
87
class MandosClientPropertyCache(object):
 
34
88
    """This wraps a Mandos Client D-Bus proxy object, caches the
 
35
89
    properties and calls a hook function when any of them are
 
38
 
    def __init__(self, proxy_object=None, properties=None, *args,
 
40
 
        self.proxy = proxy_object # Mandos Client proxy object
 
42
 
        if properties is None:
 
43
 
            self.properties = dict()
 
45
 
            self.properties = properties
 
46
 
        self.proxy.connect_to_signal("PropertyChanged",
 
47
 
                                     self.property_changed,
 
51
 
        if properties is None:
 
52
 
            self.properties.update(self.proxy.GetAll(client_interface,
 
54
 
                                                     dbus.PROPERTIES_IFACE))
 
55
 
        super(MandosClientPropertyCache, self).__init__(
 
56
 
            proxy_object=proxy_object,
 
57
 
            properties=properties, *args, **kwargs)
 
59
 
    def property_changed(self, property=None, value=None):
 
60
 
        """This is called whenever we get a PropertyChanged signal
 
61
 
        It updates the changed property in the "properties" dict.
 
 
92
    def __init__(self, proxy_object=None, properties=None, **kwargs):
 
 
93
        self.proxy = proxy_object  # Mandos Client proxy object
 
 
94
        self.properties = dict() if properties is None else properties
 
 
95
        self.property_changed_match = (
 
 
96
            self.proxy.connect_to_signal("PropertiesChanged",
 
 
97
                                         self.properties_changed,
 
 
98
                                         dbus.PROPERTIES_IFACE,
 
 
101
        if properties is None:
 
 
102
            self.properties.update(self.proxy.GetAll(
 
 
104
                dbus_interface=dbus.PROPERTIES_IFACE))
 
 
106
        super(MandosClientPropertyCache, self).__init__(**kwargs)
 
 
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.
 
63
112
        # Update properties dict with new value
 
64
 
        self.properties[property] = value
 
 
113
        if interface == client_interface:
 
 
114
            self.properties.update(properties)
 
 
117
        self.property_changed_match.remove()
 
67
120
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
 
68
121
    """A Mandos Client which is visible on the screen.
 
71
124
    def __init__(self, server_proxy_object=None, update_hook=None,
 
72
 
                 delete_hook=None, *args, **kwargs):
 
 
125
                 delete_hook=None, logger=None, **kwargs):
 
73
126
        # Called on update
 
74
127
        self.update_hook = update_hook
 
75
128
        # Called on delete
 
76
129
        self.delete_hook = delete_hook
 
77
130
        # Mandos Server proxy object
 
78
131
        self.server_proxy_object = server_proxy_object
 
 
135
        self._update_timer_callback_tag = None
 
80
137
        # The widget shown normally
 
81
138
        self._text_widget = urwid.Text("")
 
82
139
        # The widget shown when we have focus
 
83
140
        self._focus_text_widget = urwid.Text("")
 
84
 
        super(MandosClientWidget, self).__init__(
 
85
 
            update_hook=update_hook, delete_hook=delete_hook,
 
 
141
        super(MandosClientWidget, self).__init__(**kwargs)
 
88
143
        self.opened = False
 
 
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 = (GLib.timeout_add
 
 
178
        elif not (flag or self._update_timer_callback_tag is None):
 
 
179
            GLib.source_remove(self._update_timer_callback_tag)
 
 
180
            self._update_timer_callback_tag = None
 
 
182
    def checker_completed(self, exitstatus, condition, command):
 
 
184
            self.logger('Checker for client {} (command "{}")'
 
 
185
                        ' succeeded'.format(self.properties["Name"],
 
 
190
        if os.WIFEXITED(condition):
 
 
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)))
 
 
202
    def checker_started(self, command):
 
 
203
        """Server signals that a checker started."""
 
 
204
        self.logger('Client {} started checker "{}"'
 
 
205
                    .format(self.properties["Name"],
 
 
208
    def got_secret(self):
 
 
209
        self.logger('Client {} received its secret'
 
 
210
                    .format(self.properties["Name"]))
 
 
212
    def need_approval(self, timeout, default):
 
 
214
            message = 'Client {} needs approval within {} seconds'
 
 
216
            message = 'Client {} will get its secret in {} seconds'
 
 
217
        self.logger(message.format(self.properties["Name"],
 
 
220
    def rejected(self, reason):
 
 
221
        self.logger('Client {} was rejected; reason: {}'
 
 
222
                    .format(self.properties["Name"], reason))
 
90
224
    def selectable(self):
 
91
225
        """Make this a "selectable" widget.
 
92
226
        This overrides the method from urwid.FlowWidget."""
 
95
 
    def rows(self, (maxcol,), focus=False):
 
 
229
    def rows(self, maxcolrow, focus=False):
 
96
230
        """How many rows this widget will occupy might depend on
 
97
231
        whether we have focus or not.
 
98
232
        This overrides the method from urwid.FlowWidget"""
 
99
 
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
 
233
        return self.current_widget(focus).rows(maxcolrow, focus=focus)
 
101
235
    def current_widget(self, focus=False):
 
102
236
        if focus or self.opened:
 
103
237
            return self._focus_widget
 
104
238
        return self._widget
 
106
240
    def update(self):
 
107
241
        "Called when what is visible on the screen should be updated."
 
108
242
        # How to add standout mode to a style
 
109
 
        with_standout = { u"normal": u"standout",
 
110
 
                          u"bold": u"bold-standout",
 
112
 
                              u"underline-blink-standout",
 
113
 
                          u"bold-underline-blink":
 
114
 
                              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",
 
117
251
        # Rebuild focus and non-focus widgets using current properties
 
118
 
        self._text = (u'name="%(name)s", enabled=%(enabled)s'
 
 
253
        # Base part of a client. Name!
 
 
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(
 
 
260
                milliseconds=self.properties["ApprovalDelay"])
 
 
261
            last_approval_request = isoformat_to_datetime(
 
 
262
                self.properties["LastApprovalRequest"])
 
 
263
            if last_approval_request is not None:
 
 
264
                timer = max(timeout - (datetime.datetime.utcnow()
 
 
265
                                       - last_approval_request),
 
 
266
                            datetime.timedelta())
 
 
268
                timer = datetime.timedelta()
 
 
269
            if self.properties["ApprovedByDefault"]:
 
 
270
                message = "Approval in {}. (d)eny?"
 
 
272
                message = "Denial in {}. (a)pprove?"
 
 
273
            message = message.format(str(timer).rsplit(".", 1)[0])
 
 
274
            self.using_timer(True)
 
 
275
        elif self.properties["LastCheckerStatus"] != 0:
 
 
276
            # When checker has failed, show timer until client expires
 
 
277
            expires = self.properties["Expires"]
 
 
279
                timer = datetime.timedelta(0)
 
 
281
                expires = (datetime.datetime.strptime
 
 
282
                           (expires, '%Y-%m-%dT%H:%M:%S.%f'))
 
 
283
                timer = max(expires - datetime.datetime.utcnow(),
 
 
284
                            datetime.timedelta())
 
 
285
            message = ('A checker has failed! Time until client'
 
 
287
                       .format(str(timer).rsplit(".", 1)[0]))
 
 
288
            self.using_timer(True)
 
 
291
            self.using_timer(False)
 
 
292
        self._text = "{}{}".format(base, message)
 
120
294
        if not urwid.supports_unicode():
 
121
295
            self._text = self._text.encode("ascii", "replace")
 
122
 
        textlist = [(u"normal", u"BLARGH: "), (u"bold", self._text)]
 
 
296
        textlist = [("normal", self._text)]
 
123
297
        self._text_widget.set_text(textlist)
 
124
298
        self._focus_text_widget.set_text([(with_standout[text[0]],
 
 
132
306
        # Run update hook, if any
 
133
307
        if self.update_hook is not None:
 
134
308
            self.update_hook()
 
 
310
    def update_timer(self):
 
 
311
        """called by GLib. Will indefinitely loop until
 
 
312
        GLib.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
            GLib.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 = ()
 
137
324
        if self.delete_hook is not None:
 
138
325
            self.delete_hook(self)
 
140
 
    def render(self, (maxcol,), focus=False):
 
 
326
        return super(MandosClientWidget, self).delete(**kwargs)
 
 
328
    def render(self, maxcolrow, focus=False):
 
141
329
        """Render differently if we have focus.
 
142
330
        This overrides the method from urwid.FlowWidget"""
 
143
 
        return self.current_widget(focus).render((maxcol,),
 
 
331
        return self.current_widget(focus).render(maxcolrow,
 
146
 
    def keypress(self, (maxcol,), key):
 
 
334
    def keypress(self, maxcolrow, key):
 
148
336
        This overrides the method from urwid.FlowWidget"""
 
149
 
        if key == u"e" or key == u"+":
 
151
 
        elif key == u"d" or key == u"-":
 
153
 
        elif key == u"r" or key == u"_":
 
 
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)
 
 
346
            self.proxy.Approve(dbus.Boolean(True, variant_level=1),
 
 
347
                               dbus_interface=client_interface,
 
 
350
            self.proxy.Approve(dbus.Boolean(False, variant_level=1),
 
 
351
                               dbus_interface=client_interface,
 
 
353
        elif key == "R" or key == "_" or key == "ctrl k":
 
154
354
            self.server_proxy_object.RemoveClient(self.proxy
 
157
 
            self.proxy.StartChecker()
 
159
 
            self.proxy.StopChecker()
 
161
 
            self.proxy.CheckedOK()
 
 
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,
 
163
 
#         elif key == u"p" or key == "=":
 
 
369
#         elif key == "p" or key == "=":
 
164
370
#             self.proxy.pause()
 
165
 
#         elif key == u"u" or key == ":":
 
 
371
#         elif key == "u" or key == ":":
 
166
372
#             self.proxy.unpause()
 
167
 
#         elif key == u"RET":
 
172
 
    def property_changed(self, property=None, value=None,
 
174
 
        """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.
 
175
380
        This overrides the method from MandosClientPropertyCache"""
 
176
 
        property_name = unicode(property)
 
177
 
        old_value = self.properties.get(property_name)
 
178
 
        super(MandosClientWidget, self).property_changed(
 
179
 
            property=property, value=value, *args, **kwargs)
 
180
 
        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):
 
 
197
404
    """This is the entire user interface - the whole screen
 
198
405
    with boxes, lists of client widgets, etc.
 
200
 
    def __init__(self, max_log_length=1000):
 
 
407
    def __init__(self, max_log_length=1000, log_level=1):
 
201
408
        DBusGMainLoop(set_as_default=True)
 
203
410
        self.screen = urwid.curses_display.Screen()
 
205
412
        self.screen.register_palette((
 
207
 
                 u"default", u"default", None),
 
209
 
                 u"default", u"default", u"bold"),
 
211
 
                 u"default", u"default", u"underline"),
 
213
 
                 u"default", u"default", u"standout"),
 
214
 
                (u"bold-underline-blink",
 
215
 
                 u"default", u"default", (u"bold", u"underline")),
 
217
 
                 u"default", u"default", (u"bold", u"standout")),
 
218
 
                (u"underline-blink-standout",
 
219
 
                 u"default", u"default", (u"underline", u"standout")),
 
220
 
                (u"bold-underline-blink-standout",
 
221
 
                 u"default", u"default", (u"bold", u"underline",
 
 
414
                 "default", "default", None),
 
 
416
                 "bold", "default", "bold"),
 
 
418
                 "underline,blink", "default", "underline,blink"),
 
 
420
                 "standout", "default", "standout"),
 
 
421
                ("bold-underline-blink",
 
 
422
                 "bold,underline,blink", "default",
 
 
423
                 "bold,underline,blink"),
 
 
425
                 "bold,standout", "default", "bold,standout"),
 
 
426
                ("underline-blink-standout",
 
 
427
                 "underline,blink,standout", "default",
 
 
428
                 "underline,blink,standout"),
 
 
429
                ("bold-underline-blink-standout",
 
 
430
                 "bold,underline,blink,standout", "default",
 
 
431
                 "bold,underline,blink,standout"),
 
225
434
        if urwid.supports_unicode():
 
226
 
            #self.divider = u"─" # \u2500
 
227
 
            self.divider = u"━" # \u2501
 
 
435
            self.divider = "─"  # \u2500
 
229
 
            #self.divider = u"-" # \u002d
 
230
 
            self.divider = u"_" # \u005f
 
 
437
            self.divider = "_"  # \u005f
 
232
439
        self.screen.start()
 
234
441
        self.size = self.screen.get_cols_rows()
 
236
443
        self.clients = urwid.SimpleListWalker([])
 
237
444
        self.clients_dict = {}
 
239
446
        # We will add Text widgets to this list
 
 
447
        self.log = urwid.SimpleListWalker([])
 
241
448
        self.max_log_length = max_log_length
 
 
450
        self.log_level = log_level
 
243
452
        # We keep a reference to the log widget so we can remove it
 
244
453
        # from the ListWalker without it getting destroyed
 
245
454
        self.logbox = ConstrainedListBox(self.log)
 
247
456
        # This keeps track of whether self.uilist currently has
 
248
457
        # self.logbox in it or not
 
249
458
        self.log_visible = True
 
250
 
        self.log_wrap = u"any"
 
 
459
        self.log_wrap = "any"
 
253
 
        self.log_message(u"Message")
 
254
 
        self.log_message(u"Message0 Message1 Message2 Message3 Message4 Message5 Message6 Message7 Message8 Message9")
 
255
 
        self.log_message(u"Message10 Message11 Message12 Message13 Message14 Message15 Message16 Message17 Message18 Message19")
 
256
 
        self.log_message(u"Message20 Message21 Message22 Message23 Message24 Message25 Message26 Message27 Message28 Message29")
 
 
462
        self.log_message_raw(("bold",
 
 
463
                              "Mandos Monitor version " + version))
 
 
464
        self.log_message_raw(("bold",
 
258
467
        self.busname = domain + '.Mandos'
 
259
 
        self.main_loop = gobject.MainLoop()
 
260
 
        self.bus = dbus.SystemBus()
 
261
 
        mandos_dbus_objc = self.bus.get_object(
 
262
 
            self.busname, u"/", follow_name_owner_changes=True)
 
263
 
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
267
 
            mandos_clients = (self.mandos_serv
 
268
 
                              .GetAllClientsWithProperties())
 
269
 
        except dbus.exceptions.DBusException:
 
270
 
            mandos_clients = dbus.Dictionary()
 
273
 
         .connect_to_signal("ClientRemoved",
 
274
 
                            self.find_and_remove_client,
 
275
 
                            dbus_interface=server_interface,
 
278
 
         .connect_to_signal("ClientAdded",
 
280
 
                            dbus_interface=server_interface,
 
282
 
        for path, client in mandos_clients.iteritems():
 
283
 
            client_proxy_object = self.bus.get_object(self.busname,
 
285
 
            self.add_client(MandosClientWidget(server_proxy_object
 
288
 
                                               =client_proxy_object,
 
293
 
                                               =self.remove_client),
 
 
468
        self.main_loop = GLib.MainLoop()
 
 
470
    def client_not_found(self, key_id, address):
 
 
471
        self.log_message("Client with address {} and key ID {} could"
 
 
472
                         " not be found".format(address, key_id))
 
296
474
    def rebuild(self):
 
297
475
        """This rebuilds the User Interface.
 
298
476
        Call this when the widget layout needs to change"""
 
300
 
        #self.uilist.append(urwid.ListBox(self.clients))
 
301
 
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
302
 
                                       #header=urwid.Divider(),
 
 
478
        # self.uilist.append(urwid.ListBox(self.clients))
 
 
479
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.
 
 
481
                                       # header=urwid.Divider(),
 
304
 
                                       footer=urwid.Divider(div_char=self.divider)))
 
 
483
                                       footer=urwid.Divider(
 
 
484
                                           div_char=self.divider)))
 
305
485
        if self.log_visible:
 
306
486
            self.uilist.append(self.logbox)
 
308
487
        self.topwidget = urwid.Pile(self.uilist)
 
310
 
    def log_message(self, markup):
 
 
489
    def log_message(self, message, level=1):
 
 
490
        """Log message formatted with timestamp"""
 
 
491
        if level < self.log_level:
 
 
493
        timestamp = datetime.datetime.now().isoformat()
 
 
494
        self.log_message_raw("{}: {}".format(timestamp, message),
 
 
497
    def log_message_raw(self, markup, level=1):
 
311
498
        """Add a log message to the log buffer."""
 
 
499
        if level < self.log_level:
 
312
501
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
313
 
        if (self.max_log_length
 
314
 
            and len(self.log) > self.max_log_length):
 
315
 
            del self.log[0:len(self.log)-self.max_log_length-1]
 
 
502
        if self.max_log_length:
 
 
503
            if len(self.log) > self.max_log_length:
 
 
504
                del self.log[0:len(self.log)-self.max_log_length-1]
 
 
505
        self.logbox.set_focus(len(self.logbox.body.contents)-1,
 
317
509
    def toggle_log_display(self):
 
318
510
        """Toggle visibility of the log buffer."""
 
319
511
        self.log_visible = not self.log_visible
 
321
 
        self.log_message(u"Log visibility changed to: "
 
322
 
                         + unicode(self.log_visible))
 
 
513
        self.log_message("Log visibility changed to: {}"
 
 
514
                         .format(self.log_visible), level=0)
 
324
516
    def change_log_display(self):
 
325
517
        """Change type of log display.
 
326
518
        Currently, this toggles wrapping of text lines."""
 
327
 
        if self.log_wrap == u"clip":
 
328
 
            self.log_wrap = u"any"
 
 
519
        if self.log_wrap == "clip":
 
 
520
            self.log_wrap = "any"
 
330
 
            self.log_wrap = u"clip"
 
 
522
            self.log_wrap = "clip"
 
331
523
        for textwidget in self.log:
 
332
524
            textwidget.set_wrap_mode(self.log_wrap)
 
333
 
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
335
 
    def find_and_remove_client(self, path, name):
 
336
 
        """Find an client from its object path and remove it.
 
338
 
        This is connected to the ClientRemoved signal from the
 
 
525
        self.log_message("Wrap mode: {}".format(self.log_wrap),
 
 
528
    def find_and_remove_client(self, path, interfaces):
 
 
529
        """Find a client by its object path and remove it.
 
 
531
        This is connected to the InterfacesRemoved signal from the
 
339
532
        Mandos server object."""
 
 
533
        if client_interface not in interfaces:
 
 
534
            # Not a Mandos client object; ignore
 
341
537
            client = self.clients_dict[path]
 
345
 
        self.remove_client(client, path)
 
347
 
    def add_new_client(self, path, properties):
 
 
540
            self.log_message("Unknown client {!r} removed"
 
 
545
    def add_new_client(self, path, ifs_and_props):
 
 
546
        """Find a client by its object path and remove it.
 
 
548
        This is connected to the InterfacesAdded signal from the
 
 
549
        Mandos server object.
 
 
551
        if client_interface not in ifs_and_props:
 
 
552
            # Not a Mandos client object; ignore
 
348
554
        client_proxy_object = self.bus.get_object(self.busname, path)
 
349
 
        self.add_client(MandosClientWidget(server_proxy_object
 
352
 
                                           =client_proxy_object,
 
353
 
                                           properties=properties,
 
357
 
                                           =self.remove_client),
 
 
555
        self.add_client(MandosClientWidget(
 
 
556
            server_proxy_object=self.mandos_serv,
 
 
557
            proxy_object=client_proxy_object,
 
 
558
            update_hook=self.refresh,
 
 
559
            delete_hook=self.remove_client,
 
 
560
            logger=self.log_message,
 
 
561
            properties=dict(ifs_and_props[client_interface])),
 
360
564
    def add_client(self, client, path=None):
 
361
565
        self.clients.append(client)
 
363
567
            path = client.proxy.object_path
 
364
568
        self.clients_dict[path] = client
 
365
 
        self.clients.sort(None, lambda c: c.properties[u"name"])
 
 
569
        self.clients.sort(key=lambda c: c.properties["Name"])
 
368
572
    def remove_client(self, client, path=None):
 
369
573
        self.clients.remove(client)
 
371
575
            path = client.proxy.object_path
 
372
576
        del self.clients_dict[path]
 
373
 
        if not self.clients_dict:
 
374
 
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
 
375
 
            # is completely emptied, we need to recreate it.
 
376
 
            self.clients = urwid.SimpleListWalker([])
 
380
579
    def refresh(self):
 
381
580
        """Redraw the screen"""
 
382
581
        canvas = self.topwidget.render(self.size, focus=True)
 
383
582
        self.screen.draw_screen(self.size, canvas)
 
386
585
        """Start the main loop and exit when it's done."""
 
 
586
        self.bus = dbus.SystemBus()
 
 
587
        mandos_dbus_objc = self.bus.get_object(
 
 
588
            self.busname, "/", follow_name_owner_changes=True)
 
 
589
        self.mandos_serv = dbus.Interface(
 
 
590
            mandos_dbus_objc, dbus_interface=server_interface)
 
 
592
            mandos_clients = (self.mandos_serv
 
 
593
                              .GetAllClientsWithProperties())
 
 
594
            if not mandos_clients:
 
 
595
                self.log_message_raw(("bold",
 
 
596
                                      "Note: Server has no clients."))
 
 
597
        except dbus.exceptions.DBusException:
 
 
598
            self.log_message_raw(("bold",
 
 
599
                                  "Note: No Mandos server running."))
 
 
600
            mandos_clients = dbus.Dictionary()
 
 
603
         .connect_to_signal("InterfacesRemoved",
 
 
604
                            self.find_and_remove_client,
 
 
605
                            dbus_interface=dbus.OBJECT_MANAGER_IFACE,
 
 
608
         .connect_to_signal("InterfacesAdded",
 
 
610
                            dbus_interface=dbus.OBJECT_MANAGER_IFACE,
 
 
613
         .connect_to_signal("ClientNotFound",
 
 
614
                            self.client_not_found,
 
 
615
                            dbus_interface=server_interface,
 
 
617
        for path, client in mandos_clients.items():
 
 
618
            client_proxy_object = self.bus.get_object(self.busname,
 
 
620
            self.add_client(MandosClientWidget(
 
 
621
                server_proxy_object=self.mandos_serv,
 
 
622
                proxy_object=client_proxy_object,
 
 
624
                update_hook=self.refresh,
 
 
625
                delete_hook=self.remove_client,
 
 
626
                logger=self.log_message),
 
388
 
        self._input_callback_tag = (gobject.io_add_watch
 
 
630
        self._input_callback_tag = (
 
 
632
                GLib.IOChannel.unix_new(sys.stdin.fileno()),
 
 
633
                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
 
392
635
        self.main_loop.run()
 
393
636
        # Main loop has finished, we should close everything now
 
394
 
        gobject.source_remove(self._input_callback_tag)
 
 
637
        GLib.source_remove(self._input_callback_tag)
 
 
638
        with warnings.catch_warnings():
 
 
639
            warnings.simplefilter("ignore", BytesWarning)
 
398
643
        self.main_loop.quit()
 
400
645
    def process_input(self, source, condition):
 
401
646
        keys = self.screen.get_input()
 
402
 
        translations = { u"ctrl n": u"down",      # Emacs
 
403
 
                         u"ctrl p": u"up",        # Emacs
 
404
 
                         u"ctrl v": u"page down", # Emacs
 
405
 
                         u"meta v": u"page up",   # Emacs
 
406
 
                         u" ": u"page down",      # less
 
407
 
                         u"f": u"page down",      # less
 
408
 
                         u"b": u"page up",        # less
 
 
647
        translations = {"ctrl n": "down",       # Emacs
 
 
648
                        "ctrl p": "up",         # Emacs
 
 
649
                        "ctrl v": "page down",  # Emacs
 
 
650
                        "meta v": "page up",    # Emacs
 
 
651
                        " ": "page down",       # less
 
 
652
                        "f": "page down",       # less
 
 
653
                        "b": "page up",         # less
 
414
659
                key = translations[key]
 
415
660
            except KeyError:    # :-)
 
418
 
            if key == u"q" or key == u"Q":
 
 
663
            if key == "q" or key == "Q":
 
421
 
            elif key == u"window resize":
 
 
666
            elif key == "window resize":
 
422
667
                self.size = self.screen.get_cols_rows()
 
424
 
            elif key == u"\f":  # Ctrl-L
 
 
669
            elif key == "ctrl l":
 
426
 
            elif key == u"l" or key == u"D":
 
 
672
            elif key == "l" or key == "D":
 
427
673
                self.toggle_log_display()
 
429
 
            elif key == u"w" or key == u"i":
 
 
675
            elif key == "w" or key == "i":
 
430
676
                self.change_log_display()
 
432
 
            elif key == u"?" or key == u"f1":
 
433
 
                self.log_message(u"Help!")
 
 
678
            elif key == "?" or key == "f1" or key == "esc":
 
 
679
                if not self.log_visible:
 
 
680
                    self.log_visible = True
 
 
682
                self.log_message_raw(("bold",
 
 
686
                                            "l: Log window toggle",
 
 
687
                                            "TAB: Switch window",
 
 
688
                                            "w: Wrap (log lines)",
 
 
689
                                            "v: Toggle verbose log",
 
 
691
                self.log_message_raw(("bold",
 
 
697
                                             "s: Start new checker",
 
436
704
                if self.topwidget.get_focus() is self.logbox:
 
437
705
                    self.topwidget.set_focus(0)
 
439
707
                    self.topwidget.set_focus(self.logbox)
 
441
 
            elif (key == u"end" or key == u"meta >" or key == u"G"
 
443
 
                pass            # xxx end-of-buffer
 
444
 
            elif (key == u"home" or key == u"meta <" or key == u"g"
 
446
 
                pass            # xxx beginning-of-buffer
 
447
 
            elif key == u"ctrl e" or key == u"$":
 
448
 
                pass            # xxx move-end-of-line
 
449
 
            elif key == u"ctrl a" or key == u"^":
 
450
 
                pass            # xxx move-beginning-of-line
 
451
 
            elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
453
 
            elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
458
 
                pass            # scroll down log
 
 
710
                if self.log_level == 0:
 
 
712
                    self.log_message("Verbose mode: Off")
 
 
715
                    self.log_message("Verbose mode: On")
 
 
716
            # elif (key == "end" or key == "meta >" or key == "G"
 
 
718
            #     pass            # xxx end-of-buffer
 
 
719
            # elif (key == "home" or key == "meta <" or key == "g"
 
 
721
            #     pass            # xxx beginning-of-buffer
 
 
722
            # elif key == "ctrl e" or key == "$":
 
 
723
            #     pass            # xxx move-end-of-line
 
 
724
            # elif key == "ctrl a" or key == "^":
 
 
725
            #     pass            # xxx move-beginning-of-line
 
 
726
            # elif key == "ctrl b" or key == "meta (" or key == "h":
 
 
728
            # elif key == "ctrl f" or key == "meta )" or key == "l":
 
 
731
            #     pass            # scroll up log
 
 
733
            #     pass            # scroll down log
 
459
734
            elif self.topwidget.selectable():
 
460
735
                self.topwidget.keypress(self.size, key)
 
464
740
ui = UserInterface()
 
 
743
except KeyboardInterrupt:
 
 
745
except Exception as e:
 
 
746
    ui.log_message(str(e))