/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: 2008-08-29 06:38:27 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080829063827-hbjl6t92tyjl5305
* mandos-clients.conf.xml (ENTITY TIMESTAMP): New.  Automatically
                                              updated by Emacs
                                              time-stamp by using
                                              Emacs local variables.
  (/refentry/refentryinfo/date): New; set to "&TIMESTAMP;".
* mandos-keygen.xml: - '' -
* mandos.conf.xml: - '' -
* mandos.xml: - '' -
* plugin-runner.xml: - '' -
* plugins.d/password-request.xml: - '' -

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
# -*- mode: python; coding: utf-8 -*-
3
 
 
4
 
from __future__ import division, absolute_import, with_statement
5
 
 
6
 
import sys
7
 
import os
8
 
import signal
9
 
 
10
 
import datetime
11
 
 
12
 
import urwid.curses_display
13
 
import urwid
14
 
 
15
 
from dbus.mainloop.glib import DBusGMainLoop
16
 
import gobject
17
 
 
18
 
import dbus
19
 
 
20
 
import UserList
21
 
 
22
 
import locale
23
 
 
24
 
locale.setlocale(locale.LC_ALL, u'')
25
 
 
26
 
import logging
27
 
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
28
 
 
29
 
# Some useful constants
30
 
domain = 'se.bsnet.fukt'
31
 
server_interface = domain + '.Mandos'
32
 
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)
42
 
 
43
 
class MandosClientPropertyCache(object):
44
 
    """This wraps a Mandos Client D-Bus proxy object, caches the
45
 
    properties and calls a hook function when any of them are
46
 
    changed.
47
 
    """
48
 
    def __init__(self, proxy_object=None, *args, **kwargs):
49
 
        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)
62
 
    
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.
66
 
        """
67
 
        # Update properties dict with new value
68
 
        self.properties[property] = value
69
 
 
70
 
 
71
 
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
72
 
    """A Mandos Client which is visible on the screen.
73
 
    """
74
 
    
75
 
    def __init__(self, server_proxy_object=None, update_hook=None,
76
 
                 delete_hook=None, logger=None, *args, **kwargs):
77
 
        # Called on update
78
 
        self.update_hook = update_hook
79
 
        # Called on delete
80
 
        self.delete_hook = delete_hook
81
 
        # Mandos Server proxy object
82
 
        self.server_proxy_object = server_proxy_object
83
 
        # Logger
84
 
        self.logger = logger
85
 
        
86
 
        # The widget shown normally
87
 
        self._text_widget = urwid.Text(u"")
88
 
        # 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)
93
 
        self.update()
94
 
        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):
113
 
        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')
135
 
    
136
 
    def checker_started(self, command):
137
 
        self.logger(u'Client %s started checker "%s"'
138
 
                    % (self.properties[u"name"], unicode(command)))
139
 
    
140
 
    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"])
147
 
    
148
 
    def selectable(self):
149
 
        """Make this a "selectable" widget.
150
 
        This overrides the method from urwid.FlowWidget."""
151
 
        return True
152
 
    
153
 
    def rows(self, (maxcol,), focus=False):
154
 
        """How many rows this widget will occupy might depend on
155
 
        whether we have focus or not.
156
 
        This overrides the method from urwid.FlowWidget"""
157
 
        return self.current_widget(focus).rows((maxcol,), focus=focus)
158
 
    
159
 
    def current_widget(self, focus=False):
160
 
        if focus or self.opened:
161
 
            return self._focus_widget
162
 
        return self._widget
163
 
    
164
 
    def update(self):
165
 
        "Called when what is visible on the screen should be updated."
166
 
        # 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",
173
 
                          }
174
 
        
175
 
        # 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")})
182
 
        if not urwid.supports_unicode():
183
 
            self._text = self._text.encode("ascii", "replace")
184
 
        textlist = [(u"normal", self._text)]
185
 
        self._text_widget.set_text(textlist)
186
 
        self._focus_text_widget.set_text([(with_standout[text[0]],
187
 
                                           text[1])
188
 
                                          if isinstance(text, tuple)
189
 
                                          else text
190
 
                                          for text in textlist])
191
 
        self._widget = self._text_widget
192
 
        self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
193
 
                                            "standout")
194
 
        # Run update hook, if any
195
 
        if self.update_hook is not None:
196
 
            self.update_hook()
197
 
    
198
 
    def delete(self):
199
 
        if self.delete_hook is not None:
200
 
            self.delete_hook(self)
201
 
    
202
 
    def render(self, (maxcol,), focus=False):
203
 
        """Render differently if we have focus.
204
 
        This overrides the method from urwid.FlowWidget"""
205
 
        return self.current_widget(focus).render((maxcol,),
206
 
                                                 focus=focus)
207
 
    
208
 
    def keypress(self, (maxcol,), key):
209
 
        """Handle keys.
210
 
        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":
216
 
            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()
224
 
        # xxx
225
 
#         elif key == u"p" or key == "=":
226
 
#             self.proxy.pause()
227
 
#         elif key == u"u" or key == ":":
228
 
#             self.proxy.unpause()
229
 
#         elif key == u"RET":
230
 
#             self.open()
231
 
        else:
232
 
            return key
233
 
    
234
 
    def property_changed(self, property=None, value=None,
235
 
                         *args, **kwargs):
236
 
        """Call self.update() if old value is not new value.
237
 
        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:
243
 
            self.update()
244
 
 
245
 
 
246
 
class ConstrainedListBox(urwid.ListBox):
247
 
    """Like a normal urwid.ListBox, but will consume all "up" or
248
 
    "down" key presses, thus not allowing any containing widgets to
249
 
    use them as an excuse to shift focus away from this widget.
250
 
    """
251
 
    def keypress(self, (maxcol, maxrow), key):
252
 
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
253
 
        if ret in (u"up", u"down"):
254
 
            return
255
 
        return ret
256
 
 
257
 
 
258
 
class UserInterface(object):
259
 
    """This is the entire user interface - the whole screen
260
 
    with boxes, lists of client widgets, etc.
261
 
    """
262
 
    def __init__(self, max_log_length=1000):
263
 
        DBusGMainLoop(set_as_default=True)
264
 
        
265
 
        self.screen = urwid.curses_display.Screen()
266
 
        
267
 
        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")),
285
 
                ))
286
 
        
287
 
        if urwid.supports_unicode():
288
 
            self.divider = u"─" # \u2500
289
 
            #self.divider = u"━" # \u2501
290
 
        else:
291
 
            #self.divider = u"-" # \u002d
292
 
            self.divider = u"_" # \u005f
293
 
        
294
 
        self.screen.start()
295
 
        
296
 
        self.size = self.screen.get_cols_rows()
297
 
        
298
 
        self.clients = urwid.SimpleListWalker([])
299
 
        self.clients_dict = {}
300
 
        
301
 
        # We will add Text widgets to this list
302
 
        self.log = []
303
 
        self.max_log_length = max_log_length
304
 
        
305
 
        # We keep a reference to the log widget so we can remove it
306
 
        # from the ListWalker without it getting destroyed
307
 
        self.logbox = ConstrainedListBox(self.log)
308
 
        
309
 
        # This keeps track of whether self.uilist currently has
310
 
        # self.logbox in it or not
311
 
        self.log_visible = True
312
 
        self.log_wrap = u"any"
313
 
        
314
 
        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"))
319
 
        
320
 
        self.busname = domain + '.Mandos'
321
 
        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
 
    
365
 
    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)))
369
 
    
370
 
    def rebuild(self):
371
 
        """This rebuilds the User Interface.
372
 
        Call this when the widget layout needs to change"""
373
 
        self.uilist = []
374
 
        #self.uilist.append(urwid.ListBox(self.clients))
375
 
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
376
 
                                       #header=urwid.Divider(),
377
 
                                       header=None,
378
 
                                       footer=urwid.Divider(div_char=self.divider)))
379
 
        if self.log_visible:
380
 
            self.uilist.append(self.logbox)
381
 
            pass
382
 
        self.topwidget = urwid.Pile(self.uilist)
383
 
    
384
 
    def log_message(self, message):
385
 
        timestamp = datetime.datetime.now().isoformat()
386
 
        self.log_message_raw(timestamp + u": " + message)
387
 
    
388
 
    def log_message_raw(self, markup):
389
 
        """Add a log message to the log buffer."""
390
 
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
391
 
        if (self.max_log_length
392
 
            and len(self.log) > self.max_log_length):
393
 
            del self.log[0:len(self.log)-self.max_log_length-1]
394
 
        self.logbox.set_focus(len(self.logbox.body.contents),
395
 
                              coming_from=u"above")
396
 
        self.refresh()
397
 
    
398
 
    def toggle_log_display(self):
399
 
        """Toggle visibility of the log buffer."""
400
 
        self.log_visible = not self.log_visible
401
 
        self.rebuild()
402
 
        self.log_message(u"Log visibility changed to: "
403
 
                         + unicode(self.log_visible))
404
 
    
405
 
    def change_log_display(self):
406
 
        """Change type of log display.
407
 
        Currently, this toggles wrapping of text lines."""
408
 
        if self.log_wrap == u"clip":
409
 
            self.log_wrap = u"any"
410
 
        else:
411
 
            self.log_wrap = u"clip"
412
 
        for textwidget in self.log:
413
 
            textwidget.set_wrap_mode(self.log_wrap)
414
 
        self.log_message(u"Wrap mode: " + self.log_wrap)
415
 
    
416
 
    def find_and_remove_client(self, path, name):
417
 
        """Find an client from its object path and remove it.
418
 
        
419
 
        This is connected to the ClientRemoved signal from the
420
 
        Mandos server object."""
421
 
        try:
422
 
            client = self.clients_dict[path]
423
 
        except KeyError:
424
 
            # not found?
425
 
            return
426
 
        self.remove_client(client, path)
427
 
    
428
 
    def add_new_client(self, path):
429
 
        client_proxy_object = self.bus.get_object(self.busname, path)
430
 
        self.add_client(MandosClientWidget(server_proxy_object
431
 
                                           =self.mandos_serv,
432
 
                                           proxy_object
433
 
                                           =client_proxy_object,
434
 
                                           update_hook
435
 
                                           =self.refresh,
436
 
                                           delete_hook
437
 
                                           =self.remove_client,
438
 
                                           logger
439
 
                                           =self.log_message),
440
 
                        path=path)
441
 
    
442
 
    def add_client(self, client, path=None):
443
 
        self.clients.append(client)
444
 
        if path is None:
445
 
            path = client.proxy.object_path
446
 
        self.clients_dict[path] = client
447
 
        self.clients.sort(None, lambda c: c.properties[u"name"])
448
 
        self.refresh()
449
 
    
450
 
    def remove_client(self, client, path=None):
451
 
        self.clients.remove(client)
452
 
        if path is None:
453
 
            path = client.proxy.object_path
454
 
        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
 
        self.refresh()
461
 
    
462
 
    def refresh(self):
463
 
        """Redraw the screen"""
464
 
        canvas = self.topwidget.render(self.size, focus=True)
465
 
        self.screen.draw_screen(self.size, canvas)
466
 
    
467
 
    def run(self):
468
 
        """Start the main loop and exit when it's done."""
469
 
        self.refresh()
470
 
        self._input_callback_tag = (gobject.io_add_watch
471
 
                                    (sys.stdin.fileno(),
472
 
                                     gobject.IO_IN,
473
 
                                     self.process_input))
474
 
        self.main_loop.run()
475
 
        # Main loop has finished, we should close everything now
476
 
        gobject.source_remove(self._input_callback_tag)
477
 
        self.screen.stop()
478
 
    
479
 
    def stop(self):
480
 
        self.main_loop.quit()
481
 
    
482
 
    def process_input(self, source, condition):
483
 
        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
493
 
                         }
494
 
        for key in keys:
495
 
            try:
496
 
                key = translations[key]
497
 
            except KeyError:    # :-)
498
 
                pass
499
 
            
500
 
            if key == u"q" or key == u"Q":
501
 
                self.stop()
502
 
                break
503
 
            elif key == u"window resize":
504
 
                self.size = self.screen.get_cols_rows()
505
 
                self.refresh()
506
 
            elif key == u"\f":  # Ctrl-L
507
 
                self.refresh()
508
 
            elif key == u"l" or key == u"D":
509
 
                self.toggle_log_display()
510
 
                self.refresh()
511
 
            elif key == u"w" or key == u"i":
512
 
                self.change_log_display()
513
 
                self.refresh()
514
 
            elif key == u"?" or key == u"f1" or key == u"esc":
515
 
                if not self.log_visible:
516
 
                    self.log_visible = True
517
 
                    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"))))
534
 
                self.refresh()
535
 
            elif key == u"tab":
536
 
                if self.topwidget.get_focus() is self.logbox:
537
 
                    self.topwidget.set_focus(0)
538
 
                else:
539
 
                    self.topwidget.set_focus(self.logbox)
540
 
                self.refresh()
541
 
            #elif (key == u"end" or key == u"meta >" or key == u"G"
542
 
            #      or key == u">"):
543
 
            #    pass            # xxx end-of-buffer
544
 
            #elif (key == u"home" or key == u"meta <" or key == u"g"
545
 
            #      or key == u"<"):
546
 
            #    pass            # xxx beginning-of-buffer
547
 
            #elif key == u"ctrl e" or key == u"$":
548
 
            #    pass            # xxx move-end-of-line
549
 
            #elif key == u"ctrl a" or key == u"^":
550
 
            #    pass            # xxx move-beginning-of-line
551
 
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
552
 
            #    pass            # xxx left
553
 
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
554
 
            #    pass            # xxx right
555
 
            #elif key == u"a":
556
 
            #    pass            # scroll up log
557
 
            #elif key == u"z":
558
 
            #    pass            # scroll down log
559
 
            elif self.topwidget.selectable():
560
 
                self.topwidget.keypress(self.size, key)
561
 
                self.refresh()
562
 
        return True
563
 
 
564
 
ui = UserInterface()
565
 
try:
566
 
    ui.run()
567
 
except Exception, e:
568
 
    ui.log_message(unicode(e))
569
 
    ui.screen.stop()
570
 
    raise