2
2
# -*- mode: python; coding: utf-8 -*-
 
4
 
# Mandos Monitor - Control and monitor the Mandos server
 
6
 
# Copyright © 2009-2011 Teddy Hogeborn
 
7
 
# Copyright © 2009-2011 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 <http://www.gnu.org/licenses/>.
 
22
 
# Contact the authors at <mandos@fukt.bsnet.se>.
 
25
 
from __future__ import division, absolute_import, print_function, unicode_literals
 
 
4
from __future__ import division, absolute_import, with_statement
 
33
9
import urwid.curses_display
 
 
61
30
urwid.curses_display.curses.A_UNDERLINE |= (
 
62
31
    urwid.curses_display.curses.A_BLINK)
 
64
 
def isoformat_to_datetime(iso):
 
65
 
    "Parse an ISO 8601 date string to a datetime.datetime()"
 
68
 
    d, t = iso.split("T", 1)
 
69
 
    year, month, day = d.split("-", 2)
 
70
 
    hour, minute, second = t.split(":", 2)
 
71
 
    second, fraction = divmod(float(second), 1)
 
72
 
    return datetime.datetime(int(year),
 
77
 
                             int(second),           # Whole seconds
 
78
 
                             int(fraction*1000000)) # Microseconds
 
80
33
class MandosClientPropertyCache(object):
 
81
34
    """This wraps a Mandos Client D-Bus proxy object, caches the
 
82
35
    properties and calls a hook function when any of them are
 
85
 
    def __init__(self, proxy_object=None, *args, **kwargs):
 
 
38
    def __init__(self, proxy_object=None, properties=None, *args,
 
86
40
        self.proxy = proxy_object # Mandos Client proxy object
 
88
 
        self.properties = dict()
 
 
42
        if properties is None:
 
 
43
            self.properties = dict()
 
 
45
            self.properties = properties
 
89
46
        self.proxy.connect_to_signal("PropertyChanged",
 
90
47
                                     self.property_changed,
 
94
 
        self.properties.update(
 
95
 
            self.proxy.GetAll(client_interface,
 
96
 
                              dbus_interface = dbus.PROPERTIES_IFACE))
 
98
 
        #XXX This break good super behaviour!
 
99
 
#        super(MandosClientPropertyCache, self).__init__(
 
 
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)
 
102
59
    def property_changed(self, property=None, value=None):
 
103
60
        """This is called whenever we get a PropertyChanged signal
 
 
114
71
    def __init__(self, server_proxy_object=None, update_hook=None,
 
115
 
                 delete_hook=None, logger=None, *args, **kwargs):
 
 
72
                 delete_hook=None, *args, **kwargs):
 
116
73
        # Called on update
 
117
74
        self.update_hook = update_hook
 
118
75
        # Called on delete
 
119
76
        self.delete_hook = delete_hook
 
120
77
        # Mandos Server proxy object
 
121
78
        self.server_proxy_object = server_proxy_object
 
125
 
        self._update_timer_callback_tag = None
 
126
 
        self._update_timer_callback_lock = 0
 
127
 
        self.last_checker_failed = False
 
129
80
        # The widget shown normally
 
130
81
        self._text_widget = urwid.Text("")
 
 
137
88
        self.opened = False
 
139
 
        last_checked_ok = isoformat_to_datetime(self.properties
 
141
 
        if last_checked_ok is None:
 
142
 
            self.last_checker_failed = True
 
144
 
            self.last_checker_failed = ((datetime.datetime.utcnow()
 
151
 
        if self.last_checker_failed:
 
152
 
            self.using_timer(True)
 
154
 
        if self.need_approval:
 
155
 
            self.using_timer(True)
 
157
 
        self.proxy.connect_to_signal("CheckerCompleted",
 
158
 
                                     self.checker_completed,
 
161
 
        self.proxy.connect_to_signal("CheckerStarted",
 
162
 
                                     self.checker_started,
 
165
 
        self.proxy.connect_to_signal("GotSecret",
 
169
 
        self.proxy.connect_to_signal("NeedApproval",
 
173
 
        self.proxy.connect_to_signal("Rejected",
 
178
 
    def property_changed(self, property=None, value=None):
 
179
 
        super(self, MandosClientWidget).property_changed(property,
 
181
 
        if property == "ApprovalPending":
 
182
 
            using_timer(bool(value))
 
184
 
    def using_timer(self, flag):
 
185
 
        """Call this method with True or False when timer should be
 
186
 
        activated or deactivated.
 
188
 
        old = self._update_timer_callback_lock
 
190
 
            self._update_timer_callback_lock += 1
 
192
 
            self._update_timer_callback_lock -= 1
 
193
 
        if old == 0 and self._update_timer_callback_lock:
 
194
 
            self._update_timer_callback_tag = (gobject.timeout_add
 
197
 
        elif old and self._update_timer_callback_lock == 0:
 
198
 
            gobject.source_remove(self._update_timer_callback_tag)
 
199
 
            self._update_timer_callback_tag = None
 
201
 
    def checker_completed(self, exitstatus, condition, command):
 
203
 
            if self.last_checker_failed:
 
204
 
                self.last_checker_failed = False
 
205
 
                self.using_timer(False)
 
206
 
            #self.logger('Checker for client %s (command "%s")'
 
208
 
            #            % (self.properties["Name"], command))
 
212
 
        if not self.last_checker_failed:
 
213
 
            self.last_checker_failed = True
 
214
 
            self.using_timer(True)
 
215
 
        if os.WIFEXITED(condition):
 
216
 
            self.logger('Checker for client %s (command "%s")'
 
217
 
                        ' failed with exit code %s'
 
218
 
                        % (self.properties["Name"], command,
 
219
 
                           os.WEXITSTATUS(condition)))
 
220
 
        elif os.WIFSIGNALED(condition):
 
221
 
            self.logger('Checker for client %s (command "%s")'
 
222
 
                        ' was killed by signal %s'
 
223
 
                        % (self.properties["Name"], command,
 
224
 
                           os.WTERMSIG(condition)))
 
225
 
        elif os.WCOREDUMP(condition):
 
226
 
            self.logger('Checker for client %s (command "%s")'
 
228
 
                        % (self.properties["Name"], command))
 
230
 
            self.logger('Checker for client %s completed'
 
234
 
    def checker_started(self, command):
 
235
 
        #self.logger('Client %s started checker "%s"'
 
236
 
        #            % (self.properties["Name"], unicode(command)))
 
239
 
    def got_secret(self):
 
240
 
        self.last_checker_failed = False
 
241
 
        self.logger('Client %s received its secret'
 
242
 
                    % self.properties["Name"])
 
244
 
    def need_approval(self, timeout, default):
 
246
 
            message = 'Client %s needs approval within %s seconds'
 
248
 
            message = 'Client %s will get its secret in %s seconds'
 
250
 
                    % (self.properties["Name"], timeout/1000))
 
251
 
        self.using_timer(True)
 
253
 
    def rejected(self, reason):
 
254
 
        self.logger('Client %s was rejected; reason: %s'
 
255
 
                    % (self.properties["Name"], reason))
 
257
90
    def selectable(self):
 
258
91
        """Make this a "selectable" widget.
 
259
92
        This overrides the method from urwid.FlowWidget."""
 
262
 
    def rows(self, maxcolrow, focus=False):
 
 
95
    def rows(self, (maxcol,), focus=False):
 
263
96
        """How many rows this widget will occupy might depend on
 
264
97
        whether we have focus or not.
 
265
98
        This overrides the method from urwid.FlowWidget"""
 
266
 
        return self.current_widget(focus).rows(maxcolrow, focus=focus)
 
 
99
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
268
101
    def current_widget(self, focus=False):
 
269
102
        if focus or self.opened:
 
 
273
106
    def update(self):
 
274
107
        "Called when what is visible on the screen should be updated."
 
275
108
        # How to add standout mode to a style
 
276
 
        with_standout = { "normal": "standout",
 
277
 
                          "bold": "bold-standout",
 
279
 
                              "underline-blink-standout",
 
280
 
                          "bold-underline-blink":
 
281
 
                              "bold-underline-blink-standout",
 
 
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",
 
284
117
        # Rebuild focus and non-focus widgets using current properties
 
286
 
        # Base part of a client. Name!
 
288
 
                      % {"name": self.properties["Name"]})
 
289
 
        if not self.properties["Enabled"]:
 
291
 
        elif self.properties["ApprovalPending"]:
 
292
 
            timeout = datetime.timedelta(milliseconds
 
295
 
            last_approval_request = isoformat_to_datetime(
 
296
 
                self.properties["LastApprovalRequest"])
 
297
 
            if last_approval_request is not None:
 
298
 
                timer = timeout - (datetime.datetime.utcnow()
 
299
 
                                   - last_approval_request)
 
301
 
                timer = datetime.timedelta()
 
302
 
            if self.properties["ApprovedByDefault"]:
 
303
 
                message = "Approval in %s. (d)eny?"
 
305
 
                message = "Denial in %s. (a)pprove?"
 
306
 
            message = message % unicode(timer).rsplit(".", 1)[0]
 
307
 
        elif self.last_checker_failed:
 
308
 
            timeout = datetime.timedelta(milliseconds
 
311
 
            last_ok = isoformat_to_datetime(
 
312
 
                max((self.properties["LastCheckedOK"]
 
313
 
                     or self.properties["Created"]),
 
314
 
                    self.properties["LastEnabled"]))
 
315
 
            timer = timeout - (datetime.datetime.utcnow() - last_ok)
 
316
 
            message = ('A checker has failed! Time until client'
 
318
 
                           % unicode(timer).rsplit(".", 1)[0])
 
321
 
        self._text = "%s%s" % (base, message)
 
 
118
        self._text = (u'name="%(name)s", enabled=%(enabled)s'
 
323
120
        if not urwid.supports_unicode():
 
324
121
            self._text = self._text.encode("ascii", "replace")
 
325
 
        textlist = [("normal", self._text)]
 
 
122
        textlist = [(u"normal", u"BLARGH: "), (u"bold", self._text)]
 
326
123
        self._text_widget.set_text(textlist)
 
327
124
        self._focus_text_widget.set_text([(with_standout[text[0]],
 
 
336
133
        if self.update_hook is not None:
 
337
134
            self.update_hook()
 
339
 
    def update_timer(self):
 
342
 
        return True             # Keep calling this
 
344
136
    def delete(self):
 
345
 
        if self._update_timer_callback_tag is not None:
 
346
 
            gobject.source_remove(self._update_timer_callback_tag)
 
347
 
            self._update_timer_callback_tag = None
 
348
137
        if self.delete_hook is not None:
 
349
138
            self.delete_hook(self)
 
351
 
    def render(self, maxcolrow, focus=False):
 
 
140
    def render(self, (maxcol,), focus=False):
 
352
141
        """Render differently if we have focus.
 
353
142
        This overrides the method from urwid.FlowWidget"""
 
354
 
        return self.current_widget(focus).render(maxcolrow,
 
 
143
        return self.current_widget(focus).render((maxcol,),
 
357
 
    def keypress(self, maxcolrow, key):
 
 
146
    def keypress(self, (maxcol,), key):
 
359
148
        This overrides the method from urwid.FlowWidget"""
 
361
 
            self.proxy.Enable(dbus_interface = client_interface)
 
363
 
            self.proxy.Disable(dbus_interface = client_interface)
 
365
 
            self.proxy.Approve(dbus.Boolean(True, variant_level=1),
 
366
 
                               dbus_interface = client_interface)
 
368
 
            self.proxy.Approve(dbus.Boolean(False, variant_level=1),
 
369
 
                                  dbus_interface = client_interface)
 
370
 
        elif key == "R" or key == "_" or key == "ctrl k":
 
 
149
        if key == u"e" or key == u"+":
 
 
151
        elif key == u"d" or key == u"-":
 
 
153
        elif key == u"r" or key == u"_":
 
371
154
            self.server_proxy_object.RemoveClient(self.proxy
 
374
 
            self.proxy.StartChecker(dbus_interface = client_interface)
 
376
 
            self.proxy.StopChecker(dbus_interface = client_interface)
 
378
 
            self.proxy.CheckedOK(dbus_interface = client_interface)
 
 
157
            self.proxy.StartChecker()
 
 
159
            self.proxy.StopChecker()
 
 
161
            self.proxy.CheckedOK()
 
380
 
#         elif key == "p" or key == "=":
 
 
163
#         elif key == u"p" or key == "=":
 
381
164
#             self.proxy.pause()
 
382
 
#         elif key == "u" or key == ":":
 
 
165
#         elif key == u"u" or key == ":":
 
383
166
#             self.proxy.unpause()
 
 
167
#         elif key == u"RET":
 
 
403
186
    "down" key presses, thus not allowing any containing widgets to
 
404
187
    use them as an excuse to shift focus away from this widget.
 
406
 
    def keypress(self, maxcolrow, key):
 
407
 
        ret = super(ConstrainedListBox, self).keypress(maxcolrow, key)
 
408
 
        if ret in ("up", "down"):
 
 
189
    def keypress(self, (maxcol, maxrow), key):
 
 
190
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
 
 
191
        if ret in (u"up", u"down"):
 
 
420
203
        self.screen = urwid.curses_display.Screen()
 
422
205
        self.screen.register_palette((
 
424
 
                 "default", "default", None),
 
426
 
                 "default", "default", "bold"),
 
428
 
                 "default", "default", "underline"),
 
430
 
                 "default", "default", "standout"),
 
431
 
                ("bold-underline-blink",
 
432
 
                 "default", "default", ("bold", "underline")),
 
434
 
                 "default", "default", ("bold", "standout")),
 
435
 
                ("underline-blink-standout",
 
436
 
                 "default", "default", ("underline", "standout")),
 
437
 
                ("bold-underline-blink-standout",
 
438
 
                 "default", "default", ("bold", "underline",
 
 
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",
 
442
225
        if urwid.supports_unicode():
 
443
 
            self.divider = "─" # \u2500
 
444
 
            #self.divider = "━" # \u2501
 
 
226
            #self.divider = u"─" # \u2500
 
 
227
            self.divider = u"━" # \u2501
 
446
 
            #self.divider = "-" # \u002d
 
447
 
            self.divider = "_" # \u005f
 
 
229
            #self.divider = u"-" # \u002d
 
 
230
            self.divider = u"_" # \u005f
 
449
232
        self.screen.start()
 
 
464
247
        # This keeps track of whether self.uilist currently has
 
465
248
        # self.logbox in it or not
 
466
249
        self.log_visible = True
 
467
 
        self.log_wrap = "any"
 
 
250
        self.log_wrap = u"any"
 
470
 
        self.log_message_raw(("bold",
 
471
 
                              "Mandos Monitor version " + version))
 
472
 
        self.log_message_raw(("bold",
 
 
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")
 
475
258
        self.busname = domain + '.Mandos'
 
476
259
        self.main_loop = gobject.MainLoop()
 
477
260
        self.bus = dbus.SystemBus()
 
478
261
        mandos_dbus_objc = self.bus.get_object(
 
479
 
            self.busname, "/", follow_name_owner_changes=True)
 
 
262
            self.busname, u"/", follow_name_owner_changes=True)
 
480
263
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
482
265
                                          = server_interface)
 
 
 
293
                                               =self.remove_client),
 
520
 
    def client_not_found(self, fingerprint, address):
 
521
 
        self.log_message(("Client with address %s and fingerprint %s"
 
522
 
                          " could not be found" % (address,
 
525
296
    def rebuild(self):
 
526
297
        """This rebuilds the User Interface.
 
527
298
        Call this when the widget layout needs to change"""
 
529
300
        #self.uilist.append(urwid.ListBox(self.clients))
 
530
 
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.
 
 
301
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
532
302
                                       #header=urwid.Divider(),
 
535
 
                                       urwid.Divider(div_char=
 
 
304
                                       footer=urwid.Divider(div_char=self.divider)))
 
537
305
        if self.log_visible:
 
538
306
            self.uilist.append(self.logbox)
 
540
308
        self.topwidget = urwid.Pile(self.uilist)
 
542
 
    def log_message(self, message):
 
543
 
        timestamp = datetime.datetime.now().isoformat()
 
544
 
        self.log_message_raw(timestamp + ": " + message)
 
546
 
    def log_message_raw(self, markup):
 
 
310
    def log_message(self, markup):
 
547
311
        """Add a log message to the log buffer."""
 
548
312
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
549
313
        if (self.max_log_length
 
550
314
            and len(self.log) > self.max_log_length):
 
551
315
            del self.log[0:len(self.log)-self.max_log_length-1]
 
552
 
        self.logbox.set_focus(len(self.logbox.body.contents),
 
556
317
    def toggle_log_display(self):
 
557
318
        """Toggle visibility of the log buffer."""
 
558
319
        self.log_visible = not self.log_visible
 
560
 
        #self.log_message("Log visibility changed to: "
 
561
 
        #                 + unicode(self.log_visible))
 
 
321
        self.log_message(u"Log visibility changed to: "
 
 
322
                         + unicode(self.log_visible))
 
563
324
    def change_log_display(self):
 
564
325
        """Change type of log display.
 
565
326
        Currently, this toggles wrapping of text lines."""
 
566
 
        if self.log_wrap == "clip":
 
567
 
            self.log_wrap = "any"
 
 
327
        if self.log_wrap == u"clip":
 
 
328
            self.log_wrap = u"any"
 
569
 
            self.log_wrap = "clip"
 
 
330
            self.log_wrap = u"clip"
 
570
331
        for textwidget in self.log:
 
571
332
            textwidget.set_wrap_mode(self.log_wrap)
 
572
 
        #self.log_message("Wrap mode: " + self.log_wrap)
 
 
333
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
574
335
    def find_and_remove_client(self, path, name):
 
575
336
        """Find an client from its object path and remove it.
 
 
640
400
    def process_input(self, source, condition):
 
641
401
        keys = self.screen.get_input()
 
642
 
        translations = { "ctrl n": "down",      # Emacs
 
643
 
                         "ctrl p": "up",        # Emacs
 
644
 
                         "ctrl v": "page down", # Emacs
 
645
 
                         "meta v": "page up",   # Emacs
 
646
 
                         " ": "page down",      # less
 
647
 
                         "f": "page down",      # less
 
648
 
                         "b": "page up",        # less
 
 
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
 
 
655
415
            except KeyError:    # :-)
 
658
 
            if key == "q" or key == "Q":
 
 
418
            if key == u"q" or key == u"Q":
 
661
 
            elif key == "window resize":
 
 
421
            elif key == u"window resize":
 
662
422
                self.size = self.screen.get_cols_rows()
 
664
 
            elif key == "\f":  # Ctrl-L
 
 
424
            elif key == u"\f":  # Ctrl-L
 
666
 
            elif key == "l" or key == "D":
 
 
426
            elif key == u"l" or key == u"D":
 
667
427
                self.toggle_log_display()
 
669
 
            elif key == "w" or key == "i":
 
 
429
            elif key == u"w" or key == u"i":
 
670
430
                self.change_log_display()
 
672
 
            elif key == "?" or key == "f1" or key == "esc":
 
673
 
                if not self.log_visible:
 
674
 
                    self.log_visible = True
 
676
 
                self.log_message_raw(("bold",
 
680
 
                                            "l: Log window toggle",
 
681
 
                                            "TAB: Switch window",
 
683
 
                self.log_message_raw(("bold",
 
689
 
                                             "s: Start new checker",
 
 
432
            elif key == u"?" or key == u"f1":
 
 
433
                self.log_message(u"Help!")
 
696
436
                if self.topwidget.get_focus() is self.logbox:
 
697
437
                    self.topwidget.set_focus(0)
 
699
439
                    self.topwidget.set_focus(self.logbox)
 
701
 
            #elif (key == "end" or key == "meta >" or key == "G"
 
703
 
            #    pass            # xxx end-of-buffer
 
704
 
            #elif (key == "home" or key == "meta <" or key == "g"
 
706
 
            #    pass            # xxx beginning-of-buffer
 
707
 
            #elif key == "ctrl e" or key == "$":
 
708
 
            #    pass            # xxx move-end-of-line
 
709
 
            #elif key == "ctrl a" or key == "^":
 
710
 
            #    pass            # xxx move-beginning-of-line
 
711
 
            #elif key == "ctrl b" or key == "meta (" or key == "h":
 
713
 
            #elif key == "ctrl f" or key == "meta )" or key == "l":
 
716
 
            #    pass            # scroll up log
 
718
 
            #    pass            # scroll down log
 
 
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
 
719
459
            elif self.topwidget.selectable():
 
720
460
                self.topwidget.keypress(self.size, key)