/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: 2012-01-15 21:01:13 UTC
  • Revision ID: teddy@recompile.se-20120115210113-mzpkowq0opshtu30
* mandos.xml (CHECKING): Don't claim that a successful secret request
                         is equivalent to a successful checker.

Show diffs side-by-side

added added

removed removed

Lines of Context:
3
3
4
4
# Mandos Monitor - Control and monitor the Mandos server
5
5
6
 
# Copyright © 2009-2013 Teddy Hogeborn
7
 
# Copyright © 2009-2013 Björn Påhlsson
 
6
# Copyright © 2009-2012 Teddy Hogeborn
 
7
# Copyright © 2009-2012 Björn Påhlsson
8
8
9
9
# This program is free software: you can redistribute it and/or modify
10
10
# it under the terms of the GNU General Public License as published by
17
17
#     GNU General Public License for more details.
18
18
19
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/>.
 
20
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
21
23
22
# Contact the authors at <mandos@recompile.se>.
24
23
25
24
 
26
25
from __future__ import (division, absolute_import, print_function,
27
26
                        unicode_literals)
28
 
try:
29
 
    from future_builtins import *
30
 
except ImportError:
31
 
    pass
32
27
 
33
28
import sys
34
29
import os
40
35
import urwid
41
36
 
42
37
from dbus.mainloop.glib import DBusGMainLoop
43
 
try:
44
 
    import gobject
45
 
except ImportError:
46
 
    from gi.repository import GObject as gobject
 
38
import gobject
47
39
 
48
40
import dbus
49
41
 
 
42
import UserList
 
43
 
50
44
import locale
51
45
 
52
 
if sys.version_info[0] == 2:
53
 
    str = unicode
54
 
 
55
46
locale.setlocale(locale.LC_ALL, '')
56
47
 
57
48
import logging
61
52
domain = 'se.recompile'
62
53
server_interface = domain + '.Mandos'
63
54
client_interface = domain + '.Mandos.Client'
64
 
version = "1.6.2"
 
55
version = "1.5.2"
 
56
 
 
57
# Always run in monochrome mode
 
58
urwid.curses_display.curses.has_colors = lambda : False
 
59
 
 
60
# Urwid doesn't support blinking, but we want it.  Since we have no
 
61
# use for underline on its own, we make underline also always blink.
 
62
urwid.curses_display.curses.A_UNDERLINE |= (
 
63
    urwid.curses_display.curses.A_BLINK)
65
64
 
66
65
def isoformat_to_datetime(iso):
67
66
    "Parse an ISO 8601 date string to a datetime.datetime()"
84
83
    properties and calls a hook function when any of them are
85
84
    changed.
86
85
    """
87
 
    def __init__(self, proxy_object=None, properties=None, **kwargs):
 
86
    def __init__(self, proxy_object=None, *args, **kwargs):
88
87
        self.proxy = proxy_object # Mandos Client proxy object
89
 
        self.properties = dict() if properties is None else properties
 
88
        
 
89
        self.properties = dict()
90
90
        self.property_changed_match = (
91
91
            self.proxy.connect_to_signal("PropertyChanged",
92
 
                                         self._property_changed,
 
92
                                         self.property_changed,
93
93
                                         client_interface,
94
94
                                         byte_arrays=True))
95
95
        
96
 
        if properties is None:
97
 
            self.properties.update(
98
 
                self.proxy.GetAll(client_interface,
99
 
                                  dbus_interface
100
 
                                  = dbus.PROPERTIES_IFACE))
101
 
        
102
 
        super(MandosClientPropertyCache, self).__init__(**kwargs)
103
 
    
104
 
    def _property_changed(self, property, value):
105
 
        """Helper which takes positional arguments"""
106
 
        return self.property_changed(property=property, value=value)
 
96
        self.properties.update(
 
97
            self.proxy.GetAll(client_interface,
 
98
                              dbus_interface = dbus.PROPERTIES_IFACE))
 
99
 
 
100
        #XXX This breaks good super behaviour
 
101
#        super(MandosClientPropertyCache, self).__init__(
 
102
#            *args, **kwargs)
107
103
    
108
104
    def property_changed(self, property=None, value=None):
109
105
        """This is called whenever we get a PropertyChanged signal
112
108
        # Update properties dict with new value
113
109
        self.properties[property] = value
114
110
    
115
 
    def delete(self):
 
111
    def delete(self, *args, **kwargs):
116
112
        self.property_changed_match.remove()
 
113
        super(MandosClientPropertyCache, self).__init__(
 
114
            *args, **kwargs)
117
115
 
118
116
 
119
117
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
121
119
    """
122
120
    
123
121
    def __init__(self, server_proxy_object=None, update_hook=None,
124
 
                 delete_hook=None, logger=None, **kwargs):
 
122
                 delete_hook=None, logger=None, *args, **kwargs):
125
123
        # Called on update
126
124
        self.update_hook = update_hook
127
125
        # Called on delete
132
130
        self.logger = logger
133
131
        
134
132
        self._update_timer_callback_tag = None
 
133
        self._update_timer_callback_lock = 0
135
134
        
136
135
        # The widget shown normally
137
136
        self._text_widget = urwid.Text("")
138
137
        # The widget shown when we have focus
139
138
        self._focus_text_widget = urwid.Text("")
140
 
        super(MandosClientWidget, self).__init__(**kwargs)
 
139
        super(MandosClientWidget, self).__init__(
 
140
            update_hook=update_hook, delete_hook=delete_hook,
 
141
            *args, **kwargs)
141
142
        self.update()
142
143
        self.opened = False
143
144
        
 
145
        last_checked_ok = isoformat_to_datetime(self.properties
 
146
                                                ["LastCheckedOK"])
 
147
        
 
148
        if self.properties ["LastCheckerStatus"] != 0:
 
149
            self.using_timer(True)
 
150
        
 
151
        if self.need_approval:
 
152
            self.using_timer(True)
 
153
        
144
154
        self.match_objects = (
145
155
            self.proxy.connect_to_signal("CheckerCompleted",
146
156
                                         self.checker_completed,
162
172
                                         self.rejected,
163
173
                                         client_interface,
164
174
                                         byte_arrays=True))
165
 
        #self.logger('Created client {0}'
166
 
        #            .format(self.properties["Name"]))
 
175
        #self.logger('Created client %s' % (self.properties["Name"]))
 
176
    
 
177
    def property_changed(self, property=None, value=None):
 
178
        super(self, MandosClientWidget).property_changed(property,
 
179
                                                         value)
 
180
        if property == "ApprovalPending":
 
181
            using_timer(bool(value))
 
182
        if property == "LastCheckerStatus":
 
183
            using_timer(value != 0)
 
184
            #self.logger('Checker for client %s (command "%s")'
 
185
            #            ' was successful'
 
186
            #            % (self.properties["Name"], command))
167
187
    
168
188
    def using_timer(self, flag):
169
189
        """Call this method with True or False when timer should be
170
190
        activated or deactivated.
171
191
        """
172
 
        if flag and self._update_timer_callback_tag is None:
 
192
        old = self._update_timer_callback_lock
 
193
        if flag:
 
194
            self._update_timer_callback_lock += 1
 
195
        else:
 
196
            self._update_timer_callback_lock -= 1
 
197
        if old == 0 and self._update_timer_callback_lock:
173
198
            # Will update the shown timer value every second
174
199
            self._update_timer_callback_tag = (gobject.timeout_add
175
200
                                               (1000,
176
201
                                                self.update_timer))
177
 
        elif not (flag or self._update_timer_callback_tag is None):
 
202
        elif old and self._update_timer_callback_lock == 0:
178
203
            gobject.source_remove(self._update_timer_callback_tag)
179
204
            self._update_timer_callback_tag = None
180
205
    
184
209
            return
185
210
        # Checker failed
186
211
        if os.WIFEXITED(condition):
187
 
            self.logger('Checker for client {0} (command "{1}")'
188
 
                        ' failed with exit code {2}'
189
 
                        .format(self.properties["Name"], command,
190
 
                                os.WEXITSTATUS(condition)))
 
212
            self.logger('Checker for client %s (command "%s")'
 
213
                        ' failed with exit code %s'
 
214
                        % (self.properties["Name"], command,
 
215
                           os.WEXITSTATUS(condition)))
191
216
        elif os.WIFSIGNALED(condition):
192
 
            self.logger('Checker for client {0} (command "{1}") was'
193
 
                        ' killed by signal {2}'
194
 
                        .format(self.properties["Name"], command,
195
 
                                os.WTERMSIG(condition)))
 
217
            self.logger('Checker for client %s (command "%s")'
 
218
                        ' was killed by signal %s'
 
219
                        % (self.properties["Name"], command,
 
220
                           os.WTERMSIG(condition)))
196
221
        elif os.WCOREDUMP(condition):
197
 
            self.logger('Checker for client {0} (command "{1}")'
 
222
            self.logger('Checker for client %s (command "%s")'
198
223
                        ' dumped core'
199
 
                        .format(self.properties["Name"], command))
 
224
                        % (self.properties["Name"], command))
200
225
        else:
201
 
            self.logger('Checker for client {0} completed'
202
 
                        ' mysteriously'
203
 
                        .format(self.properties["Name"]))
 
226
            self.logger('Checker for client %s completed'
 
227
                        ' mysteriously')
204
228
        self.update()
205
229
    
206
230
    def checker_started(self, command):
207
231
        """Server signals that a checker started. This could be useful
208
232
           to log in the future. """
209
 
        #self.logger('Client {0} started checker "{1}"'
210
 
        #            .format(self.properties["Name"],
211
 
        #                    str(command)))
 
233
        #self.logger('Client %s started checker "%s"'
 
234
        #            % (self.properties["Name"], unicode(command)))
212
235
        pass
213
236
    
214
237
    def got_secret(self):
215
 
        self.logger('Client {0} received its secret'
216
 
                    .format(self.properties["Name"]))
 
238
        self.logger('Client %s received its secret'
 
239
                    % self.properties["Name"])
217
240
    
218
241
    def need_approval(self, timeout, default):
219
242
        if not default:
220
 
            message = 'Client {0} needs approval within {1} seconds'
 
243
            message = 'Client %s needs approval within %s seconds'
221
244
        else:
222
 
            message = 'Client {0} will get its secret in {1} seconds'
223
 
        self.logger(message.format(self.properties["Name"],
224
 
                                   timeout/1000))
 
245
            message = 'Client %s will get its secret in %s seconds'
 
246
        self.logger(message
 
247
                    % (self.properties["Name"], timeout/1000))
 
248
        self.using_timer(True)
225
249
    
226
250
    def rejected(self, reason):
227
 
        self.logger('Client {0} was rejected; reason: {1}'
228
 
                    .format(self.properties["Name"], reason))
 
251
        self.logger('Client %s was rejected; reason: %s'
 
252
                    % (self.properties["Name"], reason))
229
253
    
230
254
    def selectable(self):
231
255
        """Make this a "selectable" widget.
253
277
                          "bold-underline-blink":
254
278
                              "bold-underline-blink-standout",
255
279
                          }
256
 
        
 
280
 
257
281
        # Rebuild focus and non-focus widgets using current properties
258
 
        
 
282
 
259
283
        # Base part of a client. Name!
260
 
        base = '{name}: '.format(name=self.properties["Name"])
 
284
        base = ('%(name)s: '
 
285
                      % {"name": self.properties["Name"]})
261
286
        if not self.properties["Enabled"]:
262
287
            message = "DISABLED"
263
 
            self.using_timer(False)
264
288
        elif self.properties["ApprovalPending"]:
265
289
            timeout = datetime.timedelta(milliseconds
266
290
                                         = self.properties
268
292
            last_approval_request = isoformat_to_datetime(
269
293
                self.properties["LastApprovalRequest"])
270
294
            if last_approval_request is not None:
271
 
                timer = max(timeout - (datetime.datetime.utcnow()
272
 
                                       - last_approval_request),
273
 
                            datetime.timedelta())
 
295
                timer = timeout - (datetime.datetime.utcnow()
 
296
                                   - last_approval_request)
274
297
            else:
275
298
                timer = datetime.timedelta()
276
299
            if self.properties["ApprovedByDefault"]:
277
 
                message = "Approval in {0}. (d)eny?"
 
300
                message = "Approval in %s. (d)eny?"
278
301
            else:
279
 
                message = "Denial in {0}. (a)pprove?"
280
 
            message = message.format(str(timer).rsplit(".", 1)[0])
281
 
            self.using_timer(True)
 
302
                message = "Denial in %s. (a)pprove?"
 
303
            message = message % unicode(timer).rsplit(".", 1)[0]
282
304
        elif self.properties["LastCheckerStatus"] != 0:
283
 
            # When checker has failed, show timer until client expires
 
305
            # When checker has failed, print a timer until client expires
284
306
            expires = self.properties["Expires"]
285
307
            if expires == "":
286
308
                timer = datetime.timedelta(0)
287
309
            else:
288
 
                expires = (datetime.datetime.strptime
289
 
                           (expires, '%Y-%m-%dT%H:%M:%S.%f'))
290
 
                timer = max(expires - datetime.datetime.utcnow(),
291
 
                            datetime.timedelta())
 
310
                expires = datetime.datetime.strptime(expires,
 
311
                                                     '%Y-%m-%dT%H:%M:%S.%f')
 
312
                timer = expires - datetime.datetime.utcnow()
292
313
            message = ('A checker has failed! Time until client'
293
 
                       ' gets disabled: {0}'
294
 
                       .format(str(timer).rsplit(".", 1)[0]))
295
 
            self.using_timer(True)
 
314
                       ' gets disabled: %s'
 
315
                           % unicode(timer).rsplit(".", 1)[0])
296
316
        else:
297
317
            message = "enabled"
298
 
            self.using_timer(False)
299
 
        self._text = "{0}{1}".format(base, message)
300
 
        
 
318
        self._text = "%s%s" % (base, message)
 
319
            
301
320
        if not urwid.supports_unicode():
302
321
            self._text = self._text.encode("ascii", "replace")
303
322
        textlist = [("normal", self._text)]
320
339
        self.update()
321
340
        return True             # Keep calling this
322
341
    
323
 
    def delete(self, **kwargs):
 
342
    def delete(self, *args, **kwargs):
324
343
        if self._update_timer_callback_tag is not None:
325
344
            gobject.source_remove(self._update_timer_callback_tag)
326
345
            self._update_timer_callback_tag = None
329
348
        self.match_objects = ()
330
349
        if self.delete_hook is not None:
331
350
            self.delete_hook(self)
332
 
        return super(MandosClientWidget, self).delete(**kwargs)
 
351
        return super(MandosClientWidget, self).delete(*args, **kwargs)
333
352
    
334
353
    def render(self, maxcolrow, focus=False):
335
354
        """Render differently if we have focus.
377
396
        else:
378
397
            return key
379
398
    
380
 
    def property_changed(self, property=None, **kwargs):
 
399
    def property_changed(self, property=None, value=None,
 
400
                         *args, **kwargs):
381
401
        """Call self.update() if old value is not new value.
382
402
        This overrides the method from MandosClientPropertyCache"""
383
 
        property_name = str(property)
 
403
        property_name = unicode(property)
384
404
        old_value = self.properties.get(property_name)
385
405
        super(MandosClientWidget, self).property_changed(
386
 
            property=property, **kwargs)
 
406
            property=property, value=value, *args, **kwargs)
387
407
        if self.properties.get(property_name) != old_value:
388
408
            self.update()
389
409
 
393
413
    "down" key presses, thus not allowing any containing widgets to
394
414
    use them as an excuse to shift focus away from this widget.
395
415
    """
396
 
    def keypress(self, *args, **kwargs):
397
 
        ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
 
416
    def keypress(self, maxcolrow, key):
 
417
        ret = super(ConstrainedListBox, self).keypress(maxcolrow, key)
398
418
        if ret in ("up", "down"):
399
419
            return
400
420
        return ret
413
433
                ("normal",
414
434
                 "default", "default", None),
415
435
                ("bold",
416
 
                 "bold", "default", "bold"),
 
436
                 "default", "default", "bold"),
417
437
                ("underline-blink",
418
 
                 "underline,blink", "default", "underline,blink"),
 
438
                 "default", "default", "underline"),
419
439
                ("standout",
420
 
                 "standout", "default", "standout"),
 
440
                 "default", "default", "standout"),
421
441
                ("bold-underline-blink",
422
 
                 "bold,underline,blink", "default", "bold,underline,blink"),
 
442
                 "default", "default", ("bold", "underline")),
423
443
                ("bold-standout",
424
 
                 "bold,standout", "default", "bold,standout"),
 
444
                 "default", "default", ("bold", "standout")),
425
445
                ("underline-blink-standout",
426
 
                 "underline,blink,standout", "default",
427
 
                 "underline,blink,standout"),
 
446
                 "default", "default", ("underline", "standout")),
428
447
                ("bold-underline-blink-standout",
429
 
                 "bold,underline,blink,standout", "default",
430
 
                 "bold,underline,blink,standout"),
 
448
                 "default", "default", ("bold", "underline",
 
449
                                          "standout")),
431
450
                ))
432
451
        
433
452
        if urwid.supports_unicode():
467
486
        self.main_loop = gobject.MainLoop()
468
487
    
469
488
    def client_not_found(self, fingerprint, address):
470
 
        self.log_message("Client with address {0} and fingerprint"
471
 
                         " {1} could not be found"
472
 
                         .format(address, fingerprint))
 
489
        self.log_message(("Client with address %s and fingerprint %s"
 
490
                          " could not be found" % (address,
 
491
                                                    fingerprint)))
473
492
    
474
493
    def rebuild(self):
475
494
        """This rebuilds the User Interface.
488
507
        self.topwidget = urwid.Pile(self.uilist)
489
508
    
490
509
    def log_message(self, message):
491
 
        """Log message formatted with timestamp"""
492
510
        timestamp = datetime.datetime.now().isoformat()
493
511
        self.log_message_raw(timestamp + ": " + message)
494
512
    
507
525
        self.log_visible = not self.log_visible
508
526
        self.rebuild()
509
527
        #self.log_message("Log visibility changed to: "
510
 
        #                 + str(self.log_visible))
 
528
        #                 + unicode(self.log_visible))
511
529
    
512
530
    def change_log_display(self):
513
531
        """Change type of log display.
529
547
            client = self.clients_dict[path]
530
548
        except KeyError:
531
549
            # not found?
532
 
            self.log_message("Unknown client {0!r} ({1!r}) removed"
533
 
                             .format(name, path))
 
550
            self.log_message("Unknown client %r (%r) removed", name,
 
551
                             path)
534
552
            return
535
553
        client.delete()
536
554
    
553
571
        if path is None:
554
572
            path = client.proxy.object_path
555
573
        self.clients_dict[path] = client
556
 
        self.clients.sort(key=lambda c: c.properties["Name"])
 
574
        self.clients.sort(None, lambda c: c.properties["Name"])
557
575
        self.refresh()
558
576
    
559
577
    def remove_client(self, client, path=None):
561
579
        if path is None:
562
580
            path = client.proxy.object_path
563
581
        del self.clients_dict[path]
 
582
        if not self.clients_dict:
 
583
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
 
584
            # is completely emptied, we need to recreate it.
 
585
            self.clients = urwid.SimpleListWalker([])
 
586
            self.rebuild()
564
587
        self.refresh()
565
588
    
566
589
    def refresh(self):
579
602
        try:
580
603
            mandos_clients = (self.mandos_serv
581
604
                              .GetAllClientsWithProperties())
582
 
            if not mandos_clients:
583
 
                self.log_message_raw(("bold", "Note: Server has no clients."))
584
605
        except dbus.exceptions.DBusException:
585
 
            self.log_message_raw(("bold", "Note: No Mandos server running."))
586
606
            mandos_clients = dbus.Dictionary()
587
607
        
588
608
        (self.mandos_serv
600
620
                            self.client_not_found,
601
621
                            dbus_interface=server_interface,
602
622
                            byte_arrays=True))
603
 
        for path, client in mandos_clients.items():
 
623
        for path, client in mandos_clients.iteritems():
604
624
            client_proxy_object = self.bus.get_object(self.busname,
605
625
                                                      path)
606
626
            self.add_client(MandosClientWidget(server_proxy_object
615
635
                                               logger
616
636
                                               =self.log_message),
617
637
                            path=path)
618
 
        
 
638
 
619
639
        self.refresh()
620
640
        self._input_callback_tag = (gobject.io_add_watch
621
641
                                    (sys.stdin.fileno(),
718
738
    ui.run()
719
739
except KeyboardInterrupt:
720
740
    ui.screen.stop()
721
 
except Exception as e:
722
 
    ui.log_message(str(e))
 
741
except Exception, e:
 
742
    ui.log_message(unicode(e))
723
743
    ui.screen.stop()
724
744
    raise