4
4
# Mandos Monitor - Control and monitor the Mandos server
 
6
 
# Copyright © 2009-2012 Teddy Hogeborn
 
7
 
# Copyright © 2009-2012 Björn Påhlsson
 
 
6
# Copyright © 2009-2014 Teddy Hogeborn
 
 
7
# Copyright © 2009-2014 Björn Påhlsson
 
9
9
# This program is free software: you can redistribute it and/or modify
 
10
10
# it under the terms of the GNU General Public License as published by
 
 
53
60
domain = 'se.recompile'
 
54
61
server_interface = domain + '.Mandos'
 
55
62
client_interface = domain + '.Mandos.Client'
 
58
 
# Always run in monochrome mode
 
59
 
urwid.curses_display.curses.has_colors = lambda : False
 
61
 
# Urwid doesn't support blinking, but we want it.  Since we have no
 
62
 
# use for underline on its own, we make underline also always blink.
 
63
 
urwid.curses_display.curses.A_UNDERLINE |= (
 
64
 
    urwid.curses_display.curses.A_BLINK)
 
66
65
def isoformat_to_datetime(iso):
 
67
66
    "Parse an ISO 8601 date string to a datetime.datetime()"
 
 
84
83
    properties and calls a hook function when any of them are
 
87
 
    def __init__(self, proxy_object=None, *args, **kwargs):
 
 
86
    def __init__(self, proxy_object=None, properties=None, **kwargs):
 
88
87
        self.proxy = proxy_object # Mandos Client proxy object
 
90
 
        self.properties = dict()
 
 
88
        self.properties = dict() if properties is None else properties
 
91
89
        self.property_changed_match = (
 
92
90
            self.proxy.connect_to_signal("PropertyChanged",
 
93
 
                                         self.property_changed,
 
 
91
                                         self._property_changed,
 
97
 
        self.properties.update(
 
98
 
            self.proxy.GetAll(client_interface,
 
99
 
                              dbus_interface = dbus.PROPERTIES_IFACE))
 
101
 
        #XXX This breaks good super behaviour
 
102
 
#        super(MandosClientPropertyCache, self).__init__(
 
 
95
        if properties is None:
 
 
96
            self.properties.update(
 
 
97
                self.proxy.GetAll(client_interface,
 
 
99
                                  = dbus.PROPERTIES_IFACE))
 
 
101
        super(MandosClientPropertyCache, self).__init__(**kwargs)
 
 
103
    def _property_changed(self, property, value):
 
 
104
        """Helper which takes positional arguments"""
 
 
105
        return self.property_changed(property=property, value=value)
 
105
107
    def property_changed(self, property=None, value=None):
 
106
108
        """This is called whenever we get a PropertyChanged signal
 
 
109
111
        # Update properties dict with new value
 
110
112
        self.properties[property] = value
 
112
 
    def delete(self, *args, **kwargs):
 
113
115
        self.property_changed_match.remove()
 
114
 
        super(MandosClientPropertyCache, self).__init__(
 
118
118
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
 
 
122
122
    def __init__(self, server_proxy_object=None, update_hook=None,
 
123
 
                 delete_hook=None, logger=None, *args, **kwargs):
 
 
123
                 delete_hook=None, logger=None, **kwargs):
 
124
124
        # Called on update
 
125
125
        self.update_hook = update_hook
 
126
126
        # Called on delete
 
 
131
131
        self.logger = logger
 
133
133
        self._update_timer_callback_tag = None
 
134
 
        self._update_timer_callback_lock = 0
 
136
135
        # The widget shown normally
 
137
136
        self._text_widget = urwid.Text("")
 
138
137
        # The widget shown when we have focus
 
139
138
        self._focus_text_widget = urwid.Text("")
 
140
 
        super(MandosClientWidget, self).__init__(
 
141
 
            update_hook=update_hook, delete_hook=delete_hook,
 
 
139
        super(MandosClientWidget, self).__init__(**kwargs)
 
144
141
        self.opened = False
 
146
 
        last_checked_ok = isoformat_to_datetime(self.properties
 
149
 
        if self.properties ["LastCheckerStatus"] != 0:
 
150
 
            self.using_timer(True)
 
152
 
        if self.need_approval:
 
153
 
            self.using_timer(True)
 
155
143
        self.match_objects = (
 
156
144
            self.proxy.connect_to_signal("CheckerCompleted",
 
157
145
                                         self.checker_completed,
 
 
174
162
                                         client_interface,
 
175
163
                                         byte_arrays=True))
 
176
 
        #self.logger('Created client {0}'
 
177
 
        #            .format(self.properties["Name"]))
 
179
 
    def property_changed(self, property=None, value=None):
 
180
 
        super(self, MandosClientWidget).property_changed(property,
 
182
 
        if property == "ApprovalPending":
 
183
 
            using_timer(bool(value))
 
184
 
        if property == "LastCheckerStatus":
 
185
 
            using_timer(value != 0)
 
186
 
            #self.logger('Checker for client {0} (command "{1}") was '
 
187
 
            #            ' successful'.format(self.properties["Name"],
 
 
164
        self.logger('Created client {0}'
 
 
165
                    .format(self.properties["Name"]), level=0)
 
190
167
    def using_timer(self, flag):
 
191
168
        """Call this method with True or False when timer should be
 
192
169
        activated or deactivated.
 
194
 
        old = self._update_timer_callback_lock
 
196
 
            self._update_timer_callback_lock += 1
 
198
 
            self._update_timer_callback_lock -= 1
 
199
 
        if old == 0 and self._update_timer_callback_lock:
 
 
171
        if flag and self._update_timer_callback_tag is None:
 
200
172
            # Will update the shown timer value every second
 
201
173
            self._update_timer_callback_tag = (gobject.timeout_add
 
203
175
                                                self.update_timer))
 
204
 
        elif old and self._update_timer_callback_lock == 0:
 
 
176
        elif not (flag or self._update_timer_callback_tag is None):
 
205
177
            gobject.source_remove(self._update_timer_callback_tag)
 
206
178
            self._update_timer_callback_tag = None
 
208
180
    def checker_completed(self, exitstatus, condition, command):
 
209
181
        if exitstatus == 0:
 
 
182
            self.logger('Checker for client {0} (command "{1}")'
 
 
183
                        ' succeeded'.format(self.properties["Name"],
 
 
233
208
    def checker_started(self, command):
 
234
 
        """Server signals that a checker started. This could be useful
 
235
 
           to log in the future. """
 
236
 
        #self.logger('Client {0} started checker "{1}"'
 
237
 
        #            .format(self.properties["Name"],
 
 
209
        """Server signals that a checker started."""
 
 
210
        self.logger('Client {0} started checker "{1}"'
 
 
211
                    .format(self.properties["Name"],
 
241
214
    def got_secret(self):
 
242
215
        self.logger('Client {0} received its secret'
 
 
281
253
                          "bold-underline-blink":
 
282
254
                              "bold-underline-blink-standout",
 
285
257
        # Rebuild focus and non-focus widgets using current properties
 
287
259
        # Base part of a client. Name!
 
288
260
        base = '{name}: '.format(name=self.properties["Name"])
 
289
261
        if not self.properties["Enabled"]:
 
290
262
            message = "DISABLED"
 
 
263
            self.using_timer(False)
 
291
264
        elif self.properties["ApprovalPending"]:
 
292
265
            timeout = datetime.timedelta(milliseconds
 
293
266
                                         = self.properties
 
 
295
268
            last_approval_request = isoformat_to_datetime(
 
296
269
                self.properties["LastApprovalRequest"])
 
297
270
            if last_approval_request is not None:
 
298
 
                timer = timeout - (datetime.datetime.utcnow()
 
299
 
                                   - last_approval_request)
 
 
271
                timer = max(timeout - (datetime.datetime.utcnow()
 
 
272
                                       - last_approval_request),
 
 
273
                            datetime.timedelta())
 
301
275
                timer = datetime.timedelta()
 
302
276
            if self.properties["ApprovedByDefault"]:
 
303
277
                message = "Approval in {0}. (d)eny?"
 
305
279
                message = "Denial in {0}. (a)pprove?"
 
306
 
            message = message.format(unicode(timer).rsplit(".", 1)[0])
 
 
280
            message = message.format(str(timer).rsplit(".", 1)[0])
 
 
281
            self.using_timer(True)
 
307
282
        elif self.properties["LastCheckerStatus"] != 0:
 
308
283
            # When checker has failed, show timer until client expires
 
309
284
            expires = self.properties["Expires"]
 
 
313
288
                expires = (datetime.datetime.strptime
 
314
289
                           (expires, '%Y-%m-%dT%H:%M:%S.%f'))
 
315
 
                timer = expires - datetime.datetime.utcnow()
 
 
290
                timer = max(expires - datetime.datetime.utcnow(),
 
 
291
                            datetime.timedelta())
 
316
292
            message = ('A checker has failed! Time until client'
 
317
293
                       ' gets disabled: {0}'
 
318
 
                       .format(unicode(timer).rsplit(".", 1)[0]))
 
 
294
                       .format(str(timer).rsplit(".", 1)[0]))
 
 
295
            self.using_timer(True)
 
320
297
            message = "enabled"
 
 
298
            self.using_timer(False)
 
321
299
        self._text = "{0}{1}".format(base, message)
 
323
301
        if not urwid.supports_unicode():
 
324
302
            self._text = self._text.encode("ascii", "replace")
 
325
303
        textlist = [("normal", self._text)]
 
 
343
321
        return True             # Keep calling this
 
345
 
    def delete(self, *args, **kwargs):
 
 
323
    def delete(self, **kwargs):
 
346
324
        if self._update_timer_callback_tag is not None:
 
347
325
            gobject.source_remove(self._update_timer_callback_tag)
 
348
326
            self._update_timer_callback_tag = None
 
 
351
329
        self.match_objects = ()
 
352
330
        if self.delete_hook is not None:
 
353
331
            self.delete_hook(self)
 
354
 
        return super(MandosClientWidget, self).delete(*args, **kwargs)
 
 
332
        return super(MandosClientWidget, self).delete(**kwargs)
 
356
334
    def render(self, maxcolrow, focus=False):
 
357
335
        """Render differently if we have focus.
 
 
402
 
    def property_changed(self, property=None, value=None,
 
 
380
    def property_changed(self, property=None, **kwargs):
 
404
381
        """Call self.update() if old value is not new value.
 
405
382
        This overrides the method from MandosClientPropertyCache"""
 
406
 
        property_name = unicode(property)
 
 
383
        property_name = str(property)
 
407
384
        old_value = self.properties.get(property_name)
 
408
385
        super(MandosClientWidget, self).property_changed(
 
409
 
            property=property, value=value, *args, **kwargs)
 
 
386
            property=property, **kwargs)
 
410
387
        if self.properties.get(property_name) != old_value:
 
 
416
393
    "down" key presses, thus not allowing any containing widgets to
 
417
394
    use them as an excuse to shift focus away from this widget.
 
419
 
    def keypress(self, maxcolrow, key):
 
420
 
        ret = super(ConstrainedListBox, self).keypress(maxcolrow, key)
 
 
396
    def keypress(self, *args, **kwargs):
 
 
397
        ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
 
421
398
        if ret in ("up", "down"):
 
 
427
404
    """This is the entire user interface - the whole screen
 
428
405
    with boxes, lists of client widgets, etc.
 
430
 
    def __init__(self, max_log_length=1000):
 
 
407
    def __init__(self, max_log_length=1000, log_level=1):
 
431
408
        DBusGMainLoop(set_as_default=True)
 
433
410
        self.screen = urwid.curses_display.Screen()
 
 
437
414
                 "default", "default", None),
 
439
 
                 "default", "default", "bold"),
 
 
416
                 "bold", "default", "bold"),
 
440
417
                ("underline-blink",
 
441
 
                 "default", "default", "underline"),
 
 
418
                 "underline,blink", "default", "underline,blink"),
 
443
 
                 "default", "default", "standout"),
 
 
420
                 "standout", "default", "standout"),
 
444
421
                ("bold-underline-blink",
 
445
 
                 "default", "default", ("bold", "underline")),
 
 
422
                 "bold,underline,blink", "default", "bold,underline,blink"),
 
446
423
                ("bold-standout",
 
447
 
                 "default", "default", ("bold", "standout")),
 
 
424
                 "bold,standout", "default", "bold,standout"),
 
448
425
                ("underline-blink-standout",
 
449
 
                 "default", "default", ("underline", "standout")),
 
 
426
                 "underline,blink,standout", "default",
 
 
427
                 "underline,blink,standout"),
 
450
428
                ("bold-underline-blink-standout",
 
451
 
                 "default", "default", ("bold", "underline",
 
 
429
                 "bold,underline,blink,standout", "default",
 
 
430
                 "bold,underline,blink,standout"),
 
455
433
        if urwid.supports_unicode():
 
 
509
489
            self.uilist.append(self.logbox)
 
510
490
        self.topwidget = urwid.Pile(self.uilist)
 
512
 
    def log_message(self, message):
 
 
492
    def log_message(self, message, level=1):
 
 
493
        """Log message formatted with timestamp"""
 
 
494
        if level < self.log_level:
 
513
496
        timestamp = datetime.datetime.now().isoformat()
 
514
 
        self.log_message_raw(timestamp + ": " + message)
 
 
497
        self.log_message_raw("{0}: {1}".format(timestamp, message),
 
516
 
    def log_message_raw(self, markup):
 
 
500
    def log_message_raw(self, markup, level=1):
 
517
501
        """Add a log message to the log buffer."""
 
 
502
        if level < self.log_level:
 
518
504
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
519
505
        if (self.max_log_length
 
520
506
            and len(self.log) > self.max_log_length):
 
 
527
513
        """Toggle visibility of the log buffer."""
 
528
514
        self.log_visible = not self.log_visible
 
530
 
        #self.log_message("Log visibility changed to: "
 
531
 
        #                 + unicode(self.log_visible))
 
 
516
        self.log_message("Log visibility changed to: {0}"
 
 
517
                         .format(self.log_visible), level=0)
 
533
519
    def change_log_display(self):
 
534
520
        """Change type of log display.
 
 
539
525
            self.log_wrap = "clip"
 
540
526
        for textwidget in self.log:
 
541
527
            textwidget.set_wrap_mode(self.log_wrap)
 
542
 
        #self.log_message("Wrap mode: " + self.log_wrap)
 
 
528
        self.log_message("Wrap mode: {0}".format(self.log_wrap),
 
544
531
    def find_and_remove_client(self, path, name):
 
545
532
        """Find a client by its object path and remove it.
 
 
606
588
            mandos_clients = (self.mandos_serv
 
607
589
                              .GetAllClientsWithProperties())
 
 
590
            if not mandos_clients:
 
 
591
                self.log_message_raw(("bold", "Note: Server has no clients."))
 
608
592
        except dbus.exceptions.DBusException:
 
 
593
            self.log_message_raw(("bold", "Note: No Mandos server running."))
 
609
594
            mandos_clients = dbus.Dictionary()
 
611
596
        (self.mandos_serv
 
 
623
608
                            self.client_not_found,
 
624
609
                            dbus_interface=server_interface,
 
625
610
                            byte_arrays=True))
 
626
 
        for path, client in mandos_clients.iteritems():
 
 
611
        for path, client in mandos_clients.items():
 
627
612
            client_proxy_object = self.bus.get_object(self.busname,
 
629
614
            self.add_client(MandosClientWidget(server_proxy_object