/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: 2014-10-05 19:39:25 UTC
  • Revision ID: teddy@recompile.se-20141005193925-jjqx2n2dt3icy28u
* mandos.service ([Unit]/Documentation): New.

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