/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk

« back to all changes in this revision

Viewing changes to mandos-monitor

  • Committer: Teddy Hogeborn
  • Date: 2015-08-10 07:34:37 UTC
  • Revision ID: teddy@recompile.se-20150810073437-3m8jgt13nqric6vf
Revert change to D-Bus API.

The D-Bus API signal CheckerCompleted is documented to provide a
wait(2) status value.  Since the server switched to using subprocess
to run checkers, it no longer has access to a wait(2) status value.  A
previous change to work around this made the D-Bus API incompatible.
Revert this change by constructing a fake wait(2) status value; this
keeps the D-Bus API stable.

* DBUS-API (CheckerCompleted): Revert incompatible change.
* mandos (ClientDBus.checker_callback): Construct fake wait(2) status.
* mandos-monitor (MandosClientWidget.checker_completed): Revert to
                                                         using
                                                         original API
                                                         with wait(2)
                                                         condition
                                                         value.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
2
2
# -*- mode: python; coding: utf-8 -*-
 
3
 
4
# Mandos Monitor - Control and monitor the Mandos server
 
5
 
6
# Copyright © 2009-2015 Teddy Hogeborn
 
7
# Copyright © 2009-2015 Björn Påhlsson
 
8
 
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.
 
13
#
 
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.
 
18
 
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/>.
 
22
 
23
# Contact the authors at <mandos@recompile.se>.
 
24
3
25
 
4
 
from __future__ import division, absolute_import, with_statement
 
26
from __future__ import (division, absolute_import, print_function,
 
27
                        unicode_literals)
 
28
try:
 
29
    from future_builtins import *
 
30
except ImportError:
 
31
    pass
5
32
 
6
33
import sys
7
34
import os
8
 
import signal
9
35
 
10
36
import datetime
11
37
 
13
39
import urwid
14
40
 
15
41
from dbus.mainloop.glib import DBusGMainLoop
16
 
import gobject
 
42
try:
 
43
    import gobject
 
44
except ImportError:
 
45
    from gi.repository import GObject as gobject
17
46
 
18
47
import dbus
19
48
 
20
 
import UserList
21
 
 
22
49
import locale
23
50
 
24
 
locale.setlocale(locale.LC_ALL, u'')
 
51
if sys.version_info.major == 2:
 
52
    str = unicode
 
53
 
 
54
locale.setlocale(locale.LC_ALL, '')
 
55
 
 
56
import logging
 
57
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
25
58
 
26
59
# Some useful constants
27
 
domain = 'se.bsnet.fukt'
 
60
domain = 'se.recompile'
28
61
server_interface = domain + '.Mandos'
29
62
client_interface = domain + '.Mandos.Client'
30
 
version = "1.0.14"
31
 
 
32
 
# Always run in monochrome mode
33
 
urwid.curses_display.curses.has_colors = lambda : False
34
 
 
35
 
# Urwid doesn't support blinking, but we want it.  Since we have no
36
 
# use for underline on its own, we make underline also always blink.
37
 
urwid.curses_display.curses.A_UNDERLINE |= (
38
 
    urwid.curses_display.curses.A_BLINK)
 
63
version = "1.6.9"
 
64
 
 
65
def isoformat_to_datetime(iso):
 
66
    "Parse an ISO 8601 date string to a datetime.datetime()"
 
67
    if not iso:
 
68
        return None
 
69
    d, t = iso.split("T", 1)
 
70
    year, month, day = d.split("-", 2)
 
71
    hour, minute, second = t.split(":", 2)
 
72
    second, fraction = divmod(float(second), 1)
 
73
    return datetime.datetime(int(year),
 
74
                             int(month),
 
75
                             int(day),
 
76
                             int(hour),
 
77
                             int(minute),
 
78
                             int(second),           # Whole seconds
 
79
                             int(fraction*1000000)) # Microseconds
39
80
 
40
81
class MandosClientPropertyCache(object):
41
82
    """This wraps a Mandos Client D-Bus proxy object, caches the
42
83
    properties and calls a hook function when any of them are
43
84
    changed.
44
85
    """
45
 
    def __init__(self, proxy_object=None, *args, **kwargs):
 
86
    def __init__(self, proxy_object=None, properties=None, **kwargs):
46
87
        self.proxy = proxy_object # Mandos Client proxy object
47
 
        
48
 
        self.properties = dict()
49
 
        self.proxy.connect_to_signal(u"PropertyChanged",
50
 
                                     self.property_changed,
51
 
                                     client_interface,
52
 
                                     byte_arrays=True)
53
 
 
54
 
        self.properties.update(
55
 
            self.proxy.GetAll(client_interface,
56
 
                              dbus_interface = dbus.PROPERTIES_IFACE))
57
 
        super(MandosClientPropertyCache, self).__init__(
58
 
            proxy_object=proxy_object, *args, **kwargs)
 
88
        self.properties = dict() if properties is None else properties
 
89
        self.property_changed_match = (
 
90
            self.proxy.connect_to_signal("PropertiesChanged",
 
91
                                         self.properties_changed,
 
92
                                         dbus.PROPERTIES_IFACE,
 
93
                                         byte_arrays=True))
 
94
        
 
95
        if properties is None:
 
96
            self.properties.update(
 
97
                self.proxy.GetAll(client_interface,
 
98
                                  dbus_interface
 
99
                                  = dbus.PROPERTIES_IFACE))
 
100
        
 
101
        super(MandosClientPropertyCache, self).__init__(**kwargs)
59
102
    
60
 
    def property_changed(self, property=None, value=None):
61
 
        """This is called whenever we get a PropertyChanged signal
62
 
        It updates the changed property in the "properties" dict.
 
103
    def properties_changed(self, interface, properties, invalidated):
 
104
        """This is called whenever we get a PropertiesChanged signal
 
105
        It updates the changed properties in the "properties" dict.
63
106
        """
64
107
        # Update properties dict with new value
65
 
        self.properties[property] = value
 
108
        self.properties.update(properties)
 
109
    
 
110
    def delete(self):
 
111
        self.property_changed_match.remove()
66
112
 
67
113
 
68
114
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
70
116
    """
71
117
    
72
118
    def __init__(self, server_proxy_object=None, update_hook=None,
73
 
                 delete_hook=None, logger=None, *args, **kwargs):
 
119
                 delete_hook=None, logger=None, **kwargs):
74
120
        # Called on update
75
121
        self.update_hook = update_hook
76
122
        # Called on delete
80
126
        # Logger
81
127
        self.logger = logger
82
128
        
 
129
        self._update_timer_callback_tag = None
 
130
        
83
131
        # The widget shown normally
84
 
        self._text_widget = urwid.Text(u"")
 
132
        self._text_widget = urwid.Text("")
85
133
        # The widget shown when we have focus
86
 
        self._focus_text_widget = urwid.Text(u"")
87
 
        super(MandosClientWidget, self).__init__(
88
 
            update_hook=update_hook, delete_hook=delete_hook,
89
 
            *args, **kwargs)
 
134
        self._focus_text_widget = urwid.Text("")
 
135
        super(MandosClientWidget, self).__init__(**kwargs)
90
136
        self.update()
91
137
        self.opened = False
92
 
        self.proxy.connect_to_signal(u"CheckerCompleted",
93
 
                                     self.checker_completed,
94
 
                                     client_interface,
95
 
                                     byte_arrays=True)
96
 
        self.proxy.connect_to_signal(u"CheckerStarted",
97
 
                                     self.checker_started,
98
 
                                     client_interface,
99
 
                                     byte_arrays=True)
100
 
        self.proxy.connect_to_signal(u"GotSecret",
101
 
                                     self.got_secret,
102
 
                                     client_interface,
103
 
                                     byte_arrays=True)
104
 
        self.proxy.connect_to_signal(u"Rejected",
105
 
                                     self.rejected,
106
 
                                     client_interface,
107
 
                                     byte_arrays=True)
 
138
        
 
139
        self.match_objects = (
 
140
            self.proxy.connect_to_signal("CheckerCompleted",
 
141
                                         self.checker_completed,
 
142
                                         client_interface,
 
143
                                         byte_arrays=True),
 
144
            self.proxy.connect_to_signal("CheckerStarted",
 
145
                                         self.checker_started,
 
146
                                         client_interface,
 
147
                                         byte_arrays=True),
 
148
            self.proxy.connect_to_signal("GotSecret",
 
149
                                         self.got_secret,
 
150
                                         client_interface,
 
151
                                         byte_arrays=True),
 
152
            self.proxy.connect_to_signal("NeedApproval",
 
153
                                         self.need_approval,
 
154
                                         client_interface,
 
155
                                         byte_arrays=True),
 
156
            self.proxy.connect_to_signal("Rejected",
 
157
                                         self.rejected,
 
158
                                         client_interface,
 
159
                                         byte_arrays=True))
 
160
        self.logger('Created client {}'
 
161
                    .format(self.properties["Name"]), level=0)
 
162
    
 
163
    def using_timer(self, flag):
 
164
        """Call this method with True or False when timer should be
 
165
        activated or deactivated.
 
166
        """
 
167
        if flag and self._update_timer_callback_tag is None:
 
168
            # Will update the shown timer value every second
 
169
            self._update_timer_callback_tag = (gobject.timeout_add
 
170
                                               (1000,
 
171
                                                self.update_timer))
 
172
        elif not (flag or self._update_timer_callback_tag is None):
 
173
            gobject.source_remove(self._update_timer_callback_tag)
 
174
            self._update_timer_callback_tag = None
108
175
    
109
176
    def checker_completed(self, exitstatus, condition, command):
110
177
        if exitstatus == 0:
111
 
            self.logger(u'Checker for client %s (command "%s")'
112
 
                        u' was successful'
113
 
                        % (self.properties[u"name"], command))
 
178
            self.logger('Checker for client {} (command "{}")'
 
179
                        ' succeeded'.format(self.properties["Name"],
 
180
                                            command), level=0)
 
181
            self.update()
114
182
            return
 
183
        # Checker failed
115
184
        if os.WIFEXITED(condition):
116
 
            self.logger(u'Checker for client %s (command "%s")'
117
 
                        u' failed with exit code %s'
118
 
                        % (self.properties[u"name"], command,
119
 
                           os.WEXITSTATUS(condition)))
120
 
            return
121
 
        if os.WIFSIGNALED(condition):
122
 
            self.logger(u'Checker for client %s (command "%s")'
123
 
                        u' was killed by signal %s'
124
 
                        % (self.properties[u"name"], command,
125
 
                           os.WTERMSIG(condition)))
126
 
            return
127
 
        if os.WCOREDUMP(condition):
128
 
            self.logger(u'Checker for client %s (command "%s")'
129
 
                        u' dumped core'
130
 
                        % (self.properties[u"name"], command))
131
 
        self.logger(u'Checker for client %s completed mysteriously')
 
185
            self.logger('Checker for client {} (command "{}") failed'
 
186
                        ' with exit code {}'
 
187
                        .format(self.properties["Name"], command,
 
188
                                os.WEXITSTATUS(condition)))
 
189
        elif os.WIFSIGNALED(condition):
 
190
            self.logger('Checker for client {} (command "{}") was'
 
191
                        ' killed by signal {}'
 
192
                        .format(self.properties["Name"], command,
 
193
                                os.WTERMSIG(condition)))
 
194
        self.update()
132
195
    
133
196
    def checker_started(self, command):
134
 
        self.logger(u'Client %s started checker "%s"'
135
 
                    % (self.properties[u"name"], unicode(command)))
 
197
        """Server signals that a checker started."""
 
198
        self.logger('Client {} started checker "{}"'
 
199
                    .format(self.properties["Name"],
 
200
                            command), level=0)
136
201
    
137
202
    def got_secret(self):
138
 
        self.logger(u'Client %s received its secret'
139
 
                    % self.properties[u"name"])
140
 
    
141
 
    def rejected(self):
142
 
        self.logger(u'Client %s was rejected'
143
 
                    % self.properties[u"name"])
 
203
        self.logger('Client {} received its secret'
 
204
                    .format(self.properties["Name"]))
 
205
    
 
206
    def need_approval(self, timeout, default):
 
207
        if not default:
 
208
            message = 'Client {} needs approval within {} seconds'
 
209
        else:
 
210
            message = 'Client {} will get its secret in {} seconds'
 
211
        self.logger(message.format(self.properties["Name"],
 
212
                                   timeout/1000))
 
213
    
 
214
    def rejected(self, reason):
 
215
        self.logger('Client {} was rejected; reason: {}'
 
216
                    .format(self.properties["Name"], reason))
144
217
    
145
218
    def selectable(self):
146
219
        """Make this a "selectable" widget.
147
220
        This overrides the method from urwid.FlowWidget."""
148
221
        return True
149
222
    
150
 
    def rows(self, (maxcol,), focus=False):
 
223
    def rows(self, maxcolrow, focus=False):
151
224
        """How many rows this widget will occupy might depend on
152
225
        whether we have focus or not.
153
226
        This overrides the method from urwid.FlowWidget"""
154
 
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
227
        return self.current_widget(focus).rows(maxcolrow, focus=focus)
155
228
    
156
229
    def current_widget(self, focus=False):
157
230
        if focus or self.opened:
161
234
    def update(self):
162
235
        "Called when what is visible on the screen should be updated."
163
236
        # How to add standout mode to a style
164
 
        with_standout = { u"normal": u"standout",
165
 
                          u"bold": u"bold-standout",
166
 
                          u"underline-blink":
167
 
                              u"underline-blink-standout",
168
 
                          u"bold-underline-blink":
169
 
                              u"bold-underline-blink-standout",
 
237
        with_standout = { "normal": "standout",
 
238
                          "bold": "bold-standout",
 
239
                          "underline-blink":
 
240
                              "underline-blink-standout",
 
241
                          "bold-underline-blink":
 
242
                              "bold-underline-blink-standout",
170
243
                          }
171
244
        
172
245
        # Rebuild focus and non-focus widgets using current properties
173
 
        self._text = (u'%(name)s: %(enabled)s'
174
 
                      % { u"name": self.properties[u"name"],
175
 
                          u"enabled":
176
 
                              (u"enabled"
177
 
                               if self.properties[u"enabled"]
178
 
                               else u"DISABLED")})
 
246
        
 
247
        # Base part of a client. Name!
 
248
        base = '{name}: '.format(name=self.properties["Name"])
 
249
        if not self.properties["Enabled"]:
 
250
            message = "DISABLED"
 
251
            self.using_timer(False)
 
252
        elif self.properties["ApprovalPending"]:
 
253
            timeout = datetime.timedelta(milliseconds
 
254
                                         = self.properties
 
255
                                         ["ApprovalDelay"])
 
256
            last_approval_request = isoformat_to_datetime(
 
257
                self.properties["LastApprovalRequest"])
 
258
            if last_approval_request is not None:
 
259
                timer = max(timeout - (datetime.datetime.utcnow()
 
260
                                       - last_approval_request),
 
261
                            datetime.timedelta())
 
262
            else:
 
263
                timer = datetime.timedelta()
 
264
            if self.properties["ApprovedByDefault"]:
 
265
                message = "Approval in {}. (d)eny?"
 
266
            else:
 
267
                message = "Denial in {}. (a)pprove?"
 
268
            message = message.format(str(timer).rsplit(".", 1)[0])
 
269
            self.using_timer(True)
 
270
        elif self.properties["LastCheckerStatus"] != 0:
 
271
            # When checker has failed, show timer until client expires
 
272
            expires = self.properties["Expires"]
 
273
            if expires == "":
 
274
                timer = datetime.timedelta(0)
 
275
            else:
 
276
                expires = (datetime.datetime.strptime
 
277
                           (expires, '%Y-%m-%dT%H:%M:%S.%f'))
 
278
                timer = max(expires - datetime.datetime.utcnow(),
 
279
                            datetime.timedelta())
 
280
            message = ('A checker has failed! Time until client'
 
281
                       ' gets disabled: {}'
 
282
                       .format(str(timer).rsplit(".", 1)[0]))
 
283
            self.using_timer(True)
 
284
        else:
 
285
            message = "enabled"
 
286
            self.using_timer(False)
 
287
        self._text = "{}{}".format(base, message)
 
288
        
179
289
        if not urwid.supports_unicode():
180
290
            self._text = self._text.encode("ascii", "replace")
181
 
        textlist = [(u"normal", self._text)]
 
291
        textlist = [("normal", self._text)]
182
292
        self._text_widget.set_text(textlist)
183
293
        self._focus_text_widget.set_text([(with_standout[text[0]],
184
294
                                           text[1])
192
302
        if self.update_hook is not None:
193
303
            self.update_hook()
194
304
    
195
 
    def delete(self):
 
305
    def update_timer(self):
 
306
        """called by gobject. Will indefinitely loop until
 
307
        gobject.source_remove() on tag is called"""
 
308
        self.update()
 
309
        return True             # Keep calling this
 
310
    
 
311
    def delete(self, **kwargs):
 
312
        if self._update_timer_callback_tag is not None:
 
313
            gobject.source_remove(self._update_timer_callback_tag)
 
314
            self._update_timer_callback_tag = None
 
315
        for match in self.match_objects:
 
316
            match.remove()
 
317
        self.match_objects = ()
196
318
        if self.delete_hook is not None:
197
319
            self.delete_hook(self)
 
320
        return super(MandosClientWidget, self).delete(**kwargs)
198
321
    
199
 
    def render(self, (maxcol,), focus=False):
 
322
    def render(self, maxcolrow, focus=False):
200
323
        """Render differently if we have focus.
201
324
        This overrides the method from urwid.FlowWidget"""
202
 
        return self.current_widget(focus).render((maxcol,),
 
325
        return self.current_widget(focus).render(maxcolrow,
203
326
                                                 focus=focus)
204
327
    
205
 
    def keypress(self, (maxcol,), key):
 
328
    def keypress(self, maxcolrow, key):
206
329
        """Handle keys.
207
330
        This overrides the method from urwid.FlowWidget"""
208
 
        if key == u"e" or key == u"+":
209
 
            self.proxy.Enable()
210
 
        elif key == u"d" or key == u"-":
211
 
            self.proxy.Disable()
212
 
        elif key == u"r" or key == u"_" or key == u"ctrl k":
 
331
        if key == "+":
 
332
            self.proxy.Set(client_interface, "Enabled",
 
333
                           dbus.Boolean(True), ignore_reply = True,
 
334
                           dbus_interface = dbus.PROPERTIES_IFACE)
 
335
        elif key == "-":
 
336
            self.proxy.Set(client_interface, "Enabled", False,
 
337
                           ignore_reply = True,
 
338
                           dbus_interface = dbus.PROPERTIES_IFACE)
 
339
        elif key == "a":
 
340
            self.proxy.Approve(dbus.Boolean(True, variant_level=1),
 
341
                               dbus_interface = client_interface,
 
342
                               ignore_reply=True)
 
343
        elif key == "d":
 
344
            self.proxy.Approve(dbus.Boolean(False, variant_level=1),
 
345
                                  dbus_interface = client_interface,
 
346
                               ignore_reply=True)
 
347
        elif key == "R" or key == "_" or key == "ctrl k":
213
348
            self.server_proxy_object.RemoveClient(self.proxy
214
 
                                                  .object_path)
215
 
        elif key == u"s":
216
 
            self.proxy.StartChecker()
217
 
        elif key == u"S":
218
 
            self.proxy.StopChecker()
219
 
        elif key == u"C":
220
 
            self.proxy.CheckedOK()
 
349
                                                  .object_path,
 
350
                                                  ignore_reply=True)
 
351
        elif key == "s":
 
352
            self.proxy.Set(client_interface, "CheckerRunning",
 
353
                           dbus.Boolean(True), ignore_reply = True,
 
354
                           dbus_interface = dbus.PROPERTIES_IFACE)
 
355
        elif key == "S":
 
356
            self.proxy.Set(client_interface, "CheckerRunning",
 
357
                           dbus.Boolean(False), ignore_reply = True,
 
358
                           dbus_interface = dbus.PROPERTIES_IFACE)
 
359
        elif key == "C":
 
360
            self.proxy.CheckedOK(dbus_interface = client_interface,
 
361
                                 ignore_reply=True)
221
362
        # xxx
222
 
#         elif key == u"p" or key == "=":
 
363
#         elif key == "p" or key == "=":
223
364
#             self.proxy.pause()
224
 
#         elif key == u"u" or key == ":":
 
365
#         elif key == "u" or key == ":":
225
366
#             self.proxy.unpause()
226
 
#         elif key == u"RET":
 
367
#         elif key == "RET":
227
368
#             self.open()
228
369
        else:
229
370
            return key
230
371
    
231
 
    def property_changed(self, property=None, value=None,
232
 
                         *args, **kwargs):
233
 
        """Call self.update() if old value is not new value.
 
372
    def properties_changed(self, interface, properties, invalidated):
 
373
        """Call self.update() if any properties changed.
234
374
        This overrides the method from MandosClientPropertyCache"""
235
 
        property_name = unicode(property)
236
 
        old_value = self.properties.get(property_name)
237
 
        super(MandosClientWidget, self).property_changed(
238
 
            property=property, value=value, *args, **kwargs)
239
 
        if self.properties.get(property_name) != old_value:
 
375
        old_values = { key: self.properties.get(key)
 
376
                       for key in properties.keys() }
 
377
        super(MandosClientWidget, self).properties_changed(
 
378
            interface, properties, invalidated)
 
379
        if any(old_values[key] != self.properties.get(key)
 
380
               for key in old_values):
240
381
            self.update()
241
382
 
242
383
 
245
386
    "down" key presses, thus not allowing any containing widgets to
246
387
    use them as an excuse to shift focus away from this widget.
247
388
    """
248
 
    def keypress(self, (maxcol, maxrow), key):
249
 
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
250
 
        if ret in (u"up", u"down"):
 
389
    def keypress(self, *args, **kwargs):
 
390
        ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
 
391
        if ret in ("up", "down"):
251
392
            return
252
393
        return ret
253
394
 
256
397
    """This is the entire user interface - the whole screen
257
398
    with boxes, lists of client widgets, etc.
258
399
    """
259
 
    def __init__(self, max_log_length=1000):
 
400
    def __init__(self, max_log_length=1000, log_level=1):
260
401
        DBusGMainLoop(set_as_default=True)
261
402
        
262
403
        self.screen = urwid.curses_display.Screen()
263
404
        
264
405
        self.screen.register_palette((
265
 
                (u"normal",
266
 
                 u"default", u"default", None),
267
 
                (u"bold",
268
 
                 u"default", u"default", u"bold"),
269
 
                (u"underline-blink",
270
 
                 u"default", u"default", u"underline"),
271
 
                (u"standout",
272
 
                 u"default", u"default", u"standout"),
273
 
                (u"bold-underline-blink",
274
 
                 u"default", u"default", (u"bold", u"underline")),
275
 
                (u"bold-standout",
276
 
                 u"default", u"default", (u"bold", u"standout")),
277
 
                (u"underline-blink-standout",
278
 
                 u"default", u"default", (u"underline", u"standout")),
279
 
                (u"bold-underline-blink-standout",
280
 
                 u"default", u"default", (u"bold", u"underline",
281
 
                                          u"standout")),
 
406
                ("normal",
 
407
                 "default", "default", None),
 
408
                ("bold",
 
409
                 "bold", "default", "bold"),
 
410
                ("underline-blink",
 
411
                 "underline,blink", "default", "underline,blink"),
 
412
                ("standout",
 
413
                 "standout", "default", "standout"),
 
414
                ("bold-underline-blink",
 
415
                 "bold,underline,blink", "default", "bold,underline,blink"),
 
416
                ("bold-standout",
 
417
                 "bold,standout", "default", "bold,standout"),
 
418
                ("underline-blink-standout",
 
419
                 "underline,blink,standout", "default",
 
420
                 "underline,blink,standout"),
 
421
                ("bold-underline-blink-standout",
 
422
                 "bold,underline,blink,standout", "default",
 
423
                 "bold,underline,blink,standout"),
282
424
                ))
283
425
        
284
426
        if urwid.supports_unicode():
285
 
            self.divider = u"─" # \u2500
286
 
            #self.divider = u"━" # \u2501
 
427
            self.divider = "─" # \u2500
 
428
            #self.divider = "━" # \u2501
287
429
        else:
288
 
            #self.divider = u"-" # \u002d
289
 
            self.divider = u"_" # \u005f
 
430
            #self.divider = "-" # \u002d
 
431
            self.divider = "_" # \u005f
290
432
        
291
433
        self.screen.start()
292
434
        
299
441
        self.log = []
300
442
        self.max_log_length = max_log_length
301
443
        
 
444
        self.log_level = log_level
 
445
        
302
446
        # We keep a reference to the log widget so we can remove it
303
447
        # from the ListWalker without it getting destroyed
304
448
        self.logbox = ConstrainedListBox(self.log)
306
450
        # This keeps track of whether self.uilist currently has
307
451
        # self.logbox in it or not
308
452
        self.log_visible = True
309
 
        self.log_wrap = u"any"
 
453
        self.log_wrap = "any"
310
454
        
311
455
        self.rebuild()
312
 
        self.log_message_raw((u"bold",
313
 
                              u"Mandos Monitor version " + version))
314
 
        self.log_message_raw((u"bold",
315
 
                              u"q: Quit  ?: Help"))
 
456
        self.log_message_raw(("bold",
 
457
                              "Mandos Monitor version " + version))
 
458
        self.log_message_raw(("bold",
 
459
                              "q: Quit  ?: Help"))
316
460
        
317
461
        self.busname = domain + '.Mandos'
318
462
        self.main_loop = gobject.MainLoop()
319
 
        self.bus = dbus.SystemBus()
320
 
        mandos_dbus_objc = self.bus.get_object(
321
 
            self.busname, u"/", follow_name_owner_changes=True)
322
 
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
323
 
                                          dbus_interface
324
 
                                          = server_interface)
325
 
        try:
326
 
            mandos_clients = (self.mandos_serv
327
 
                              .GetAllClientsWithProperties())
328
 
        except dbus.exceptions.DBusException:
329
 
            mandos_clients = dbus.Dictionary()
330
 
        
331
 
        (self.mandos_serv
332
 
         .connect_to_signal(u"ClientRemoved",
333
 
                            self.find_and_remove_client,
334
 
                            dbus_interface=server_interface,
335
 
                            byte_arrays=True))
336
 
        (self.mandos_serv
337
 
         .connect_to_signal(u"ClientAdded",
338
 
                            self.add_new_client,
339
 
                            dbus_interface=server_interface,
340
 
                            byte_arrays=True))
341
 
        (self.mandos_serv
342
 
         .connect_to_signal(u"ClientNotFound",
343
 
                            self.client_not_found,
344
 
                            dbus_interface=server_interface,
345
 
                            byte_arrays=True))
346
 
        for path, client in mandos_clients.iteritems():
347
 
            client_proxy_object = self.bus.get_object(self.busname,
348
 
                                                      path)
349
 
            self.add_client(MandosClientWidget(server_proxy_object
350
 
                                               =self.mandos_serv,
351
 
                                               proxy_object
352
 
                                               =client_proxy_object,
353
 
                                               properties=client,
354
 
                                               update_hook
355
 
                                               =self.refresh,
356
 
                                               delete_hook
357
 
                                               =self.remove_client,
358
 
                                               logger
359
 
                                               =self.log_message),
360
 
                            path=path)
361
463
    
362
464
    def client_not_found(self, fingerprint, address):
363
 
        self.log_message((u"Client with address %s and fingerprint %s"
364
 
                          u" could not be found" % (address,
365
 
                                                    fingerprint)))
 
465
        self.log_message("Client with address {} and fingerprint {}"
 
466
                         " could not be found"
 
467
                         .format(address, fingerprint))
366
468
    
367
469
    def rebuild(self):
368
470
        """This rebuilds the User Interface.
369
471
        Call this when the widget layout needs to change"""
370
472
        self.uilist = []
371
473
        #self.uilist.append(urwid.ListBox(self.clients))
372
 
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
474
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.
 
475
                                                          clients),
373
476
                                       #header=urwid.Divider(),
374
477
                                       header=None,
375
 
                                       footer=urwid.Divider(div_char=self.divider)))
 
478
                                       footer=
 
479
                                       urwid.Divider(div_char=
 
480
                                                     self.divider)))
376
481
        if self.log_visible:
377
482
            self.uilist.append(self.logbox)
378
 
            pass
379
483
        self.topwidget = urwid.Pile(self.uilist)
380
484
    
381
 
    def log_message(self, message):
 
485
    def log_message(self, message, level=1):
 
486
        """Log message formatted with timestamp"""
 
487
        if level < self.log_level:
 
488
            return
382
489
        timestamp = datetime.datetime.now().isoformat()
383
 
        self.log_message_raw(timestamp + u": " + message)
 
490
        self.log_message_raw("{}: {}".format(timestamp, message),
 
491
                             level=level)
384
492
    
385
 
    def log_message_raw(self, markup):
 
493
    def log_message_raw(self, markup, level=1):
386
494
        """Add a log message to the log buffer."""
 
495
        if level < self.log_level:
 
496
            return
387
497
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
388
498
        if (self.max_log_length
389
499
            and len(self.log) > self.max_log_length):
390
500
            del self.log[0:len(self.log)-self.max_log_length-1]
391
501
        self.logbox.set_focus(len(self.logbox.body.contents),
392
 
                              coming_from=u"above")
 
502
                              coming_from="above")
393
503
        self.refresh()
394
504
    
395
505
    def toggle_log_display(self):
396
506
        """Toggle visibility of the log buffer."""
397
507
        self.log_visible = not self.log_visible
398
508
        self.rebuild()
399
 
        self.log_message(u"Log visibility changed to: "
400
 
                         + unicode(self.log_visible))
 
509
        self.log_message("Log visibility changed to: {}"
 
510
                         .format(self.log_visible), level=0)
401
511
    
402
512
    def change_log_display(self):
403
513
        """Change type of log display.
404
514
        Currently, this toggles wrapping of text lines."""
405
 
        if self.log_wrap == u"clip":
406
 
            self.log_wrap = u"any"
 
515
        if self.log_wrap == "clip":
 
516
            self.log_wrap = "any"
407
517
        else:
408
 
            self.log_wrap = u"clip"
 
518
            self.log_wrap = "clip"
409
519
        for textwidget in self.log:
410
520
            textwidget.set_wrap_mode(self.log_wrap)
411
 
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
521
        self.log_message("Wrap mode: {}".format(self.log_wrap),
 
522
                         level=0)
412
523
    
413
524
    def find_and_remove_client(self, path, name):
414
 
        """Find an client from its object path and remove it.
 
525
        """Find a client by its object path and remove it.
415
526
        
416
527
        This is connected to the ClientRemoved signal from the
417
528
        Mandos server object."""
419
530
            client = self.clients_dict[path]
420
531
        except KeyError:
421
532
            # not found?
 
533
            self.log_message("Unknown client {!r} ({!r}) removed"
 
534
                             .format(name, path))
422
535
            return
423
 
        self.remove_client(client, path)
 
536
        client.delete()
424
537
    
425
538
    def add_new_client(self, path):
426
539
        client_proxy_object = self.bus.get_object(self.busname, path)
441
554
        if path is None:
442
555
            path = client.proxy.object_path
443
556
        self.clients_dict[path] = client
444
 
        self.clients.sort(None, lambda c: c.properties[u"name"])
 
557
        self.clients.sort(key=lambda c: c.properties["Name"])
445
558
        self.refresh()
446
559
    
447
560
    def remove_client(self, client, path=None):
449
562
        if path is None:
450
563
            path = client.proxy.object_path
451
564
        del self.clients_dict[path]
452
 
        if not self.clients_dict:
453
 
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
454
 
            # is completely emptied, we need to recreate it.
455
 
            self.clients = urwid.SimpleListWalker([])
456
 
            self.rebuild()
457
565
        self.refresh()
458
566
    
459
567
    def refresh(self):
463
571
    
464
572
    def run(self):
465
573
        """Start the main loop and exit when it's done."""
 
574
        self.bus = dbus.SystemBus()
 
575
        mandos_dbus_objc = self.bus.get_object(
 
576
            self.busname, "/", follow_name_owner_changes=True)
 
577
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
578
                                          dbus_interface
 
579
                                          = server_interface)
 
580
        try:
 
581
            mandos_clients = (self.mandos_serv
 
582
                              .GetAllClientsWithProperties())
 
583
            if not mandos_clients:
 
584
                self.log_message_raw(("bold", "Note: Server has no clients."))
 
585
        except dbus.exceptions.DBusException:
 
586
            self.log_message_raw(("bold", "Note: No Mandos server running."))
 
587
            mandos_clients = dbus.Dictionary()
 
588
        
 
589
        (self.mandos_serv
 
590
         .connect_to_signal("ClientRemoved",
 
591
                            self.find_and_remove_client,
 
592
                            dbus_interface=server_interface,
 
593
                            byte_arrays=True))
 
594
        (self.mandos_serv
 
595
         .connect_to_signal("ClientAdded",
 
596
                            self.add_new_client,
 
597
                            dbus_interface=server_interface,
 
598
                            byte_arrays=True))
 
599
        (self.mandos_serv
 
600
         .connect_to_signal("ClientNotFound",
 
601
                            self.client_not_found,
 
602
                            dbus_interface=server_interface,
 
603
                            byte_arrays=True))
 
604
        for path, client in mandos_clients.items():
 
605
            client_proxy_object = self.bus.get_object(self.busname,
 
606
                                                      path)
 
607
            self.add_client(MandosClientWidget(server_proxy_object
 
608
                                               =self.mandos_serv,
 
609
                                               proxy_object
 
610
                                               =client_proxy_object,
 
611
                                               properties=client,
 
612
                                               update_hook
 
613
                                               =self.refresh,
 
614
                                               delete_hook
 
615
                                               =self.remove_client,
 
616
                                               logger
 
617
                                               =self.log_message),
 
618
                            path=path)
 
619
        
466
620
        self.refresh()
467
621
        self._input_callback_tag = (gobject.io_add_watch
468
622
                                    (sys.stdin.fileno(),
478
632
    
479
633
    def process_input(self, source, condition):
480
634
        keys = self.screen.get_input()
481
 
        translations = { u"ctrl n": u"down",      # Emacs
482
 
                         u"ctrl p": u"up",        # Emacs
483
 
                         u"ctrl v": u"page down", # Emacs
484
 
                         u"meta v": u"page up",   # Emacs
485
 
                         u" ": u"page down",      # less
486
 
                         u"f": u"page down",      # less
487
 
                         u"b": u"page up",        # less
488
 
                         u"j": u"down",           # vi
489
 
                         u"k": u"up",             # vi
 
635
        translations = { "ctrl n": "down",      # Emacs
 
636
                         "ctrl p": "up",        # Emacs
 
637
                         "ctrl v": "page down", # Emacs
 
638
                         "meta v": "page up",   # Emacs
 
639
                         " ": "page down",      # less
 
640
                         "f": "page down",      # less
 
641
                         "b": "page up",        # less
 
642
                         "j": "down",           # vi
 
643
                         "k": "up",             # vi
490
644
                         }
491
645
        for key in keys:
492
646
            try:
494
648
            except KeyError:    # :-)
495
649
                pass
496
650
            
497
 
            if key == u"q" or key == u"Q":
 
651
            if key == "q" or key == "Q":
498
652
                self.stop()
499
653
                break
500
 
            elif key == u"window resize":
 
654
            elif key == "window resize":
501
655
                self.size = self.screen.get_cols_rows()
502
656
                self.refresh()
503
 
            elif key == u"\f":  # Ctrl-L
 
657
            elif key == "ctrl l":
 
658
                self.screen.clear()
504
659
                self.refresh()
505
 
            elif key == u"l" or key == u"D":
 
660
            elif key == "l" or key == "D":
506
661
                self.toggle_log_display()
507
662
                self.refresh()
508
 
            elif key == u"w" or key == u"i":
 
663
            elif key == "w" or key == "i":
509
664
                self.change_log_display()
510
665
                self.refresh()
511
 
            elif key == u"?" or key == u"f1" or key == u"esc":
 
666
            elif key == "?" or key == "f1" or key == "esc":
512
667
                if not self.log_visible:
513
668
                    self.log_visible = True
514
669
                    self.rebuild()
515
 
                self.log_message_raw((u"bold",
516
 
                                      u"  ".
517
 
                                      join((u"q: Quit",
518
 
                                            u"?: Help",
519
 
                                            u"l: Log window toggle",
520
 
                                            u"TAB: Switch window",
521
 
                                            u"w: Wrap (log)"))))
522
 
                self.log_message_raw((u"bold",
523
 
                                      u"  "
524
 
                                      .join((u"Clients:",
525
 
                                             u"e: Enable",
526
 
                                             u"d: Disable",
527
 
                                             u"r: Remove",
528
 
                                             u"s: Start new checker",
529
 
                                             u"S: Stop checker",
530
 
                                             u"C: Checker OK"))))
 
670
                self.log_message_raw(("bold",
 
671
                                      "  ".
 
672
                                      join(("q: Quit",
 
673
                                            "?: Help",
 
674
                                            "l: Log window toggle",
 
675
                                            "TAB: Switch window",
 
676
                                            "w: Wrap (log lines)",
 
677
                                            "v: Toggle verbose log",
 
678
                                            ))))
 
679
                self.log_message_raw(("bold",
 
680
                                      "  "
 
681
                                      .join(("Clients:",
 
682
                                             "+: Enable",
 
683
                                             "-: Disable",
 
684
                                             "R: Remove",
 
685
                                             "s: Start new checker",
 
686
                                             "S: Stop checker",
 
687
                                             "C: Checker OK",
 
688
                                             "a: Approve",
 
689
                                             "d: Deny"))))
531
690
                self.refresh()
532
 
            elif key == u"tab":
 
691
            elif key == "tab":
533
692
                if self.topwidget.get_focus() is self.logbox:
534
693
                    self.topwidget.set_focus(0)
535
694
                else:
536
695
                    self.topwidget.set_focus(self.logbox)
537
696
                self.refresh()
538
 
            #elif (key == u"end" or key == u"meta >" or key == u"G"
539
 
            #      or key == u">"):
 
697
            elif key == "v":
 
698
                if self.log_level == 0:
 
699
                    self.log_level = 1
 
700
                    self.log_message("Verbose mode: Off")
 
701
                else:
 
702
                    self.log_level = 0
 
703
                    self.log_message("Verbose mode: On")
 
704
            #elif (key == "end" or key == "meta >" or key == "G"
 
705
            #      or key == ">"):
540
706
            #    pass            # xxx end-of-buffer
541
 
            #elif (key == u"home" or key == u"meta <" or key == u"g"
542
 
            #      or key == u"<"):
 
707
            #elif (key == "home" or key == "meta <" or key == "g"
 
708
            #      or key == "<"):
543
709
            #    pass            # xxx beginning-of-buffer
544
 
            #elif key == u"ctrl e" or key == u"$":
 
710
            #elif key == "ctrl e" or key == "$":
545
711
            #    pass            # xxx move-end-of-line
546
 
            #elif key == u"ctrl a" or key == u"^":
 
712
            #elif key == "ctrl a" or key == "^":
547
713
            #    pass            # xxx move-beginning-of-line
548
 
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
714
            #elif key == "ctrl b" or key == "meta (" or key == "h":
549
715
            #    pass            # xxx left
550
 
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
716
            #elif key == "ctrl f" or key == "meta )" or key == "l":
551
717
            #    pass            # xxx right
552
 
            #elif key == u"a":
 
718
            #elif key == "a":
553
719
            #    pass            # scroll up log
554
 
            #elif key == u"z":
 
720
            #elif key == "z":
555
721
            #    pass            # scroll down log
556
722
            elif self.topwidget.selectable():
557
723
                self.topwidget.keypress(self.size, key)
561
727
ui = UserInterface()
562
728
try:
563
729
    ui.run()
564
 
except Exception, e:
565
 
    ui.log_message(unicode(e))
 
730
except KeyboardInterrupt:
 
731
    ui.screen.stop()
 
732
except Exception as e:
 
733
    ui.log_message(str(e))
566
734
    ui.screen.stop()
567
735
    raise