/mandos/trunk

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

« back to all changes in this revision

Viewing changes to mandos-monitor

  • Committer: Teddy Hogeborn
  • Date: 2015-07-20 04:03:32 UTC
  • Revision ID: teddy@recompile.se-20150720040332-hw6s7nh14ju259z3
Update copyright year.

* debian/copyright (Copyright): Update copyright year.
* intro.xml (COPYRIGHT): - '' -
* mandos-clients.conf.xml (COPYRIGHT): - '' -
* mandos-ctl.xml (COPYRIGHT): - '' -
* mandos-keygen.xml (COPYRIGHT): - '' -
* mandos-monitor.xml (COPYRIGHT): - '' -
* mandos.conf.xml (COPYRIGHT): - '' -
* mandos.xml (COPYRIGHT): - '' -
* plugin-runner.c: - '' -
* plugin-runner.xml (COPYRIGHT): - '' -
* plugins.d/askpass-fifo.c: - '' -
* plugins.d/askpass-fifo.xml (COPYRIGHT): - '' -
* plugins.d/mandos-client.xml (COPYRIGHT): - '' -
* plugins.d/password-prompt.c: - '' -
* plugins.d/password-prompt.xml (COPYRIGHT): - '' -
* plugins.d/plymouth.c: - '' -
* plugins.d/plymouth.xml (COPYRIGHT): - '' -
* plugins.d/splashy.c: - '' -
* plugins.d/splashy.xml (COPYRIGHT): - '' -
* plugins.d/usplash.c: - '' -
* plugins.d/usplash.xml (COPYRIGHT): - '' -

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
2
2
# -*- mode: python; coding: utf-8 -*-
 
3
 
4
# Mandos Monitor - Control and monitor the Mandos server
 
5
 
6
# Copyright © 2009-2015 Teddy Hogeborn
 
7
# Copyright © 2009-2015 Björn Påhlsson
 
8
 
9
# This program is free software: you can redistribute it and/or modify
 
10
# it under the terms of the GNU General Public License as published by
 
11
# the Free Software Foundation, either version 3 of the License, or
 
12
# (at your option) any later version.
 
13
#
 
14
#     This program is distributed in the hope that it will be useful,
 
15
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
 
16
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
17
#     GNU General Public License for more details.
 
18
 
19
# You should have received a copy of the GNU General Public License
 
20
# along with this program.  If not, see
 
21
# <http://www.gnu.org/licenses/>.
 
22
 
23
# Contact the authors at <mandos@recompile.se>.
 
24
3
25
 
4
 
from __future__ import division, absolute_import, with_statement
 
26
from __future__ import (division, absolute_import, print_function,
 
27
                        unicode_literals)
 
28
try:
 
29
    from future_builtins import *
 
30
except ImportError:
 
31
    pass
5
32
 
6
33
import sys
7
34
import os
8
 
import signal
9
35
 
10
36
import datetime
11
37
 
13
39
import urwid
14
40
 
15
41
from dbus.mainloop.glib import DBusGMainLoop
16
 
import gobject
 
42
try:
 
43
    import gobject
 
44
except ImportError:
 
45
    from gi.repository import GObject as gobject
17
46
 
18
47
import dbus
19
48
 
20
 
import UserList
21
 
 
22
49
import locale
23
50
 
24
 
locale.setlocale(locale.LC_ALL, u'')
 
51
if sys.version_info.major == 2:
 
52
    str = unicode
 
53
 
 
54
locale.setlocale(locale.LC_ALL, '')
 
55
 
 
56
import logging
 
57
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
25
58
 
26
59
# Some useful constants
27
 
domain = 'se.bsnet.fukt'
 
60
domain = 'se.recompile'
28
61
server_interface = domain + '.Mandos'
29
62
client_interface = domain + '.Mandos.Client'
30
 
version = "1.0.14"
31
 
 
32
 
# Always run in monochrome mode
33
 
urwid.curses_display.curses.has_colors = lambda : False
34
 
 
35
 
# Urwid doesn't support blinking, but we want it.  Since we have no
36
 
# use for underline on its own, we make underline also always blink.
37
 
urwid.curses_display.curses.A_UNDERLINE |= (
38
 
    urwid.curses_display.curses.A_BLINK)
 
63
version = "1.6.9"
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
 
    def checker_completed(self, exitstatus, condition, command):
 
176
    def checker_completed(self, exitstatus, signal, 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
 
        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)))
164
 
        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)))
169
 
        elif os.WCOREDUMP(condition):
170
 
            self.logger(u'Checker for client %s (command "%s")'
171
 
                        u' dumped core'
172
 
                        % (self.properties[u"name"], command))
 
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))
173
194
        else:
174
 
            self.logger(u'Checker for client %s completed mysteriously')
 
195
            self.logger('Checker for client {} completed'
 
196
                        ' mysteriously'
 
197
                        .format(self.properties["Name"]))
175
198
        self.update()
176
199
    
177
200
    def checker_started(self, command):
178
 
        self.logger(u'Client %s started checker "%s"'
179
 
                    % (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)
180
205
    
181
206
    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"])
 
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))
188
221
    
189
222
    def selectable(self):
190
223
        """Make this a "selectable" widget.
191
224
        This overrides the method from urwid.FlowWidget."""
192
225
        return True
193
226
    
194
 
    def rows(self, (maxcol,), focus=False):
 
227
    def rows(self, maxcolrow, focus=False):
195
228
        """How many rows this widget will occupy might depend on
196
229
        whether we have focus or not.
197
230
        This overrides the method from urwid.FlowWidget"""
198
 
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
231
        return self.current_widget(focus).rows(maxcolrow, focus=focus)
199
232
    
200
233
    def current_widget(self, focus=False):
201
234
        if focus or self.opened:
205
238
    def update(self):
206
239
        "Called when what is visible on the screen should be updated."
207
240
        # 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",
 
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",
214
247
                          }
215
248
        
216
249
        # 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"")})
 
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
        
240
293
        if not urwid.supports_unicode():
241
294
            self._text = self._text.encode("ascii", "replace")
242
 
        textlist = [(u"normal", self._text)]
 
295
        textlist = [("normal", self._text)]
243
296
        self._text_widget.set_text(textlist)
244
297
        self._focus_text_widget.set_text([(with_standout[text[0]],
245
298
                                           text[1])
254
307
            self.update_hook()
255
308
    
256
309
    def update_timer(self):
257
 
        "called by gobject"
 
310
        """called by gobject. Will indefinitely loop until
 
311
        gobject.source_remove() on tag is called"""
258
312
        self.update()
259
313
        return True             # Keep calling this
260
314
    
261
 
    def delete(self):
 
315
    def delete(self, **kwargs):
262
316
        if self._update_timer_callback_tag is not None:
263
317
            gobject.source_remove(self._update_timer_callback_tag)
264
318
            self._update_timer_callback_tag = None
 
319
        for match in self.match_objects:
 
320
            match.remove()
 
321
        self.match_objects = ()
265
322
        if self.delete_hook is not None:
266
323
            self.delete_hook(self)
 
324
        return super(MandosClientWidget, self).delete(**kwargs)
267
325
    
268
 
    def render(self, (maxcol,), focus=False):
 
326
    def render(self, maxcolrow, focus=False):
269
327
        """Render differently if we have focus.
270
328
        This overrides the method from urwid.FlowWidget"""
271
 
        return self.current_widget(focus).render((maxcol,),
 
329
        return self.current_widget(focus).render(maxcolrow,
272
330
                                                 focus=focus)
273
331
    
274
 
    def keypress(self, (maxcol,), key):
 
332
    def keypress(self, maxcolrow, key):
275
333
        """Handle keys.
276
334
        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":
 
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":
282
350
            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()
 
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)
290
362
        # xxx
291
 
#         elif key == u"p" or key == "=":
 
363
#         elif key == "p" or key == "=":
292
364
#             self.proxy.pause()
293
 
#         elif key == u"u" or key == ":":
 
365
#         elif key == "u" or key == ":":
294
366
#             self.proxy.unpause()
295
 
#         elif key == u"RET":
 
367
#         elif key == "RET":
296
368
#             self.open()
297
369
        else:
298
370
            return key
299
371
    
300
 
    def property_changed(self, property=None, value=None,
301
 
                         *args, **kwargs):
302
 
        """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.
303
374
        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:
 
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):
309
381
            self.update()
310
382
 
311
383
 
314
386
    "down" key presses, thus not allowing any containing widgets to
315
387
    use them as an excuse to shift focus away from this widget.
316
388
    """
317
 
    def keypress(self, (maxcol, maxrow), key):
318
 
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
319
 
        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"):
320
392
            return
321
393
        return ret
322
394
 
325
397
    """This is the entire user interface - the whole screen
326
398
    with boxes, lists of client widgets, etc.
327
399
    """
328
 
    def __init__(self, max_log_length=1000):
 
400
    def __init__(self, max_log_length=1000, log_level=1):
329
401
        DBusGMainLoop(set_as_default=True)
330
402
        
331
403
        self.screen = urwid.curses_display.Screen()
332
404
        
333
405
        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")),
 
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"),
351
424
                ))
352
425
        
353
426
        if urwid.supports_unicode():
354
 
            self.divider = u"─" # \u2500
355
 
            #self.divider = u"━" # \u2501
 
427
            self.divider = "─" # \u2500
 
428
            #self.divider = "━" # \u2501
356
429
        else:
357
 
            #self.divider = u"-" # \u002d
358
 
            self.divider = u"_" # \u005f
 
430
            #self.divider = "-" # \u002d
 
431
            self.divider = "_" # \u005f
359
432
        
360
433
        self.screen.start()
361
434
        
368
441
        self.log = []
369
442
        self.max_log_length = max_log_length
370
443
        
 
444
        self.log_level = log_level
 
445
        
371
446
        # We keep a reference to the log widget so we can remove it
372
447
        # from the ListWalker without it getting destroyed
373
448
        self.logbox = ConstrainedListBox(self.log)
375
450
        # This keeps track of whether self.uilist currently has
376
451
        # self.logbox in it or not
377
452
        self.log_visible = True
378
 
        self.log_wrap = u"any"
 
453
        self.log_wrap = "any"
379
454
        
380
455
        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"))
 
456
        self.log_message_raw(("bold",
 
457
                              "Mandos Monitor version " + version))
 
458
        self.log_message_raw(("bold",
 
459
                              "q: Quit  ?: Help"))
385
460
        
386
461
        self.busname = domain + '.Mandos'
387
462
        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
463
    
431
464
    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)))
 
465
        self.log_message("Client with address {} and fingerprint {}"
 
466
                         " could not be found"
 
467
                         .format(address, fingerprint))
435
468
    
436
469
    def rebuild(self):
437
470
        """This rebuilds the User Interface.
438
471
        Call this when the widget layout needs to change"""
439
472
        self.uilist = []
440
473
        #self.uilist.append(urwid.ListBox(self.clients))
441
 
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
474
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.
 
475
                                                          clients),
442
476
                                       #header=urwid.Divider(),
443
477
                                       header=None,
444
 
                                       footer=urwid.Divider(div_char=self.divider)))
 
478
                                       footer=
 
479
                                       urwid.Divider(div_char=
 
480
                                                     self.divider)))
445
481
        if self.log_visible:
446
482
            self.uilist.append(self.logbox)
447
 
            pass
448
483
        self.topwidget = urwid.Pile(self.uilist)
449
484
    
450
 
    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
451
489
        timestamp = datetime.datetime.now().isoformat()
452
 
        self.log_message_raw(timestamp + u": " + message)
 
490
        self.log_message_raw("{}: {}".format(timestamp, message),
 
491
                             level=level)
453
492
    
454
 
    def log_message_raw(self, markup):
 
493
    def log_message_raw(self, markup, level=1):
455
494
        """Add a log message to the log buffer."""
 
495
        if level < self.log_level:
 
496
            return
456
497
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
457
498
        if (self.max_log_length
458
499
            and len(self.log) > self.max_log_length):
459
500
            del self.log[0:len(self.log)-self.max_log_length-1]
460
501
        self.logbox.set_focus(len(self.logbox.body.contents),
461
 
                              coming_from=u"above")
 
502
                              coming_from="above")
462
503
        self.refresh()
463
504
    
464
505
    def toggle_log_display(self):
465
506
        """Toggle visibility of the log buffer."""
466
507
        self.log_visible = not self.log_visible
467
508
        self.rebuild()
468
 
        self.log_message(u"Log visibility changed to: "
469
 
                         + unicode(self.log_visible))
 
509
        self.log_message("Log visibility changed to: {}"
 
510
                         .format(self.log_visible), level=0)
470
511
    
471
512
    def change_log_display(self):
472
513
        """Change type of log display.
473
514
        Currently, this toggles wrapping of text lines."""
474
 
        if self.log_wrap == u"clip":
475
 
            self.log_wrap = u"any"
 
515
        if self.log_wrap == "clip":
 
516
            self.log_wrap = "any"
476
517
        else:
477
 
            self.log_wrap = u"clip"
 
518
            self.log_wrap = "clip"
478
519
        for textwidget in self.log:
479
520
            textwidget.set_wrap_mode(self.log_wrap)
480
 
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
521
        self.log_message("Wrap mode: {}".format(self.log_wrap),
 
522
                         level=0)
481
523
    
482
524
    def find_and_remove_client(self, path, name):
483
 
        """Find an client from its object path and remove it.
 
525
        """Find a client by its object path and remove it.
484
526
        
485
527
        This is connected to the ClientRemoved signal from the
486
528
        Mandos server object."""
488
530
            client = self.clients_dict[path]
489
531
        except KeyError:
490
532
            # not found?
 
533
            self.log_message("Unknown client {!r} ({!r}) removed"
 
534
                             .format(name, path))
491
535
            return
492
 
        self.remove_client(client, path)
 
536
        client.delete()
493
537
    
494
538
    def add_new_client(self, path):
495
539
        client_proxy_object = self.bus.get_object(self.busname, path)
510
554
        if path is None:
511
555
            path = client.proxy.object_path
512
556
        self.clients_dict[path] = client
513
 
        self.clients.sort(None, lambda c: c.properties[u"name"])
 
557
        self.clients.sort(key=lambda c: c.properties["Name"])
514
558
        self.refresh()
515
559
    
516
560
    def remove_client(self, client, path=None):
518
562
        if path is None:
519
563
            path = client.proxy.object_path
520
564
        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
565
        self.refresh()
527
566
    
528
567
    def refresh(self):
532
571
    
533
572
    def run(self):
534
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
        
535
620
        self.refresh()
536
621
        self._input_callback_tag = (gobject.io_add_watch
537
622
                                    (sys.stdin.fileno(),
547
632
    
548
633
    def process_input(self, source, condition):
549
634
        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
 
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
559
644
                         }
560
645
        for key in keys:
561
646
            try:
563
648
            except KeyError:    # :-)
564
649
                pass
565
650
            
566
 
            if key == u"q" or key == u"Q":
 
651
            if key == "q" or key == "Q":
567
652
                self.stop()
568
653
                break
569
 
            elif key == u"window resize":
 
654
            elif key == "window resize":
570
655
                self.size = self.screen.get_cols_rows()
571
656
                self.refresh()
572
 
            elif key == u"\f":  # Ctrl-L
 
657
            elif key == "ctrl l":
 
658
                self.screen.clear()
573
659
                self.refresh()
574
 
            elif key == u"l" or key == u"D":
 
660
            elif key == "l" or key == "D":
575
661
                self.toggle_log_display()
576
662
                self.refresh()
577
 
            elif key == u"w" or key == u"i":
 
663
            elif key == "w" or key == "i":
578
664
                self.change_log_display()
579
665
                self.refresh()
580
 
            elif key == u"?" or key == u"f1" or key == u"esc":
 
666
            elif key == "?" or key == "f1" or key == "esc":
581
667
                if not self.log_visible:
582
668
                    self.log_visible = True
583
669
                    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"))))
 
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"))))
600
690
                self.refresh()
601
 
            elif key == u"tab":
 
691
            elif key == "tab":
602
692
                if self.topwidget.get_focus() is self.logbox:
603
693
                    self.topwidget.set_focus(0)
604
694
                else:
605
695
                    self.topwidget.set_focus(self.logbox)
606
696
                self.refresh()
607
 
            #elif (key == u"end" or key == u"meta >" or key == u"G"
608
 
            #      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 == ">"):
609
706
            #    pass            # xxx end-of-buffer
610
 
            #elif (key == u"home" or key == u"meta <" or key == u"g"
611
 
            #      or key == u"<"):
 
707
            #elif (key == "home" or key == "meta <" or key == "g"
 
708
            #      or key == "<"):
612
709
            #    pass            # xxx beginning-of-buffer
613
 
            #elif key == u"ctrl e" or key == u"$":
 
710
            #elif key == "ctrl e" or key == "$":
614
711
            #    pass            # xxx move-end-of-line
615
 
            #elif key == u"ctrl a" or key == u"^":
 
712
            #elif key == "ctrl a" or key == "^":
616
713
            #    pass            # xxx move-beginning-of-line
617
 
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
714
            #elif key == "ctrl b" or key == "meta (" or key == "h":
618
715
            #    pass            # xxx left
619
 
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
716
            #elif key == "ctrl f" or key == "meta )" or key == "l":
620
717
            #    pass            # xxx right
621
 
            #elif key == u"a":
 
718
            #elif key == "a":
622
719
            #    pass            # scroll up log
623
 
            #elif key == u"z":
 
720
            #elif key == "z":
624
721
            #    pass            # scroll down log
625
722
            elif self.topwidget.selectable():
626
723
                self.topwidget.keypress(self.size, key)
630
727
ui = UserInterface()
631
728
try:
632
729
    ui.run()
633
 
except Exception, e:
634
 
    ui.log_message(unicode(e))
 
730
except KeyboardInterrupt:
 
731
    ui.screen.stop()
 
732
except Exception as e:
 
733
    ui.log_message(str(e))
635
734
    ui.screen.stop()
636
735
    raise