/mandos/release

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

« back to all changes in this revision

Viewing changes to mandos-monitor

  • Committer: teddy at bsnet
  • Date: 2010-08-23 19:23:15 UTC
  • mto: (24.1.154 mandos)
  • mto: This revision was merged to the branch mainline in revision 270.
  • Revision ID: teddy@fukt.bsnet.se-20100823192315-pefgye0l6cavcejs
* debian/control (mandos/Depends): Added "python-urwid".
* mandos (Client.approved_by_default): Changed default to "True".
  (Client.approved_delay): Changed default to "0s".
  (ClientDBus.GotSecret, ClientDBus.Rejected,
  ClientDBus.NeedApproval): Emit "PropertyChanged" signal for the
                            "approved_pending" property.

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