/mandos/release

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

« back to all changes in this revision

Viewing changes to mandos-monitor

  • Committer: Teddy Hogeborn
  • Date: 2015-07-20 00:59:17 UTC
  • mto: (237.7.594 trunk)
  • mto: This revision was merged to the branch mainline in revision 325.
  • Revision ID: teddy@recompile.se-20150720005917-ud7fxa6wcv9y4ta6
mandos-client: Bug fix: don't crash if --dh-params was not used.

* plugins.d/mandos-client.c (main): Bug fix: Check if dh_params_file
                                    is NULL before using it in the
                                    workaround for Debian bug #633582.

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