/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: 2013-09-29 15:52:19 UTC
  • Revision ID: teddy@recompile.se-20130929155219-rer4z6583z1qu7kj
* debian/mandos-client.README.Debian: Update Linux documentation link.
* mandos-keygen: Change default key and subkey type to RSA and default
                 key length to 4096 bits.

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-2016 Teddy Hogeborn
7
 
# Copyright © 2009-2016 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
32
32
 
33
33
import sys
34
34
import os
 
35
import signal
35
36
 
36
37
import datetime
37
38
 
39
40
import urwid
40
41
 
41
42
from dbus.mainloop.glib import DBusGMainLoop
42
 
from gi.repository import GLib
 
43
try:
 
44
    import gobject
 
45
except ImportError:
 
46
    from gi.repository import GObject as gobject
43
47
 
44
48
import dbus
45
49
 
46
50
import locale
47
51
 
48
 
if sys.version_info.major == 2:
 
52
if sys.version_info[0] == 2:
49
53
    str = unicode
50
54
 
51
55
locale.setlocale(locale.LC_ALL, '')
57
61
domain = 'se.recompile'
58
62
server_interface = domain + '.Mandos'
59
63
client_interface = domain + '.Mandos.Client'
60
 
version = "1.7.7"
61
 
 
62
 
try:
63
 
    dbus.OBJECT_MANAGER_IFACE
64
 
except AttributeError:
65
 
    dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
 
64
version = "1.6.0"
66
65
 
67
66
def isoformat_to_datetime(iso):
68
67
    "Parse an ISO 8601 date string to a datetime.datetime()"
89
88
        self.proxy = proxy_object # Mandos Client proxy object
90
89
        self.properties = dict() if properties is None else properties
91
90
        self.property_changed_match = (
92
 
            self.proxy.connect_to_signal("PropertiesChanged",
93
 
                                         self.properties_changed,
94
 
                                         dbus.PROPERTIES_IFACE,
 
91
            self.proxy.connect_to_signal("PropertyChanged",
 
92
                                         self._property_changed,
 
93
                                         client_interface,
95
94
                                         byte_arrays=True))
96
95
        
97
96
        if properties is None:
102
101
        
103
102
        super(MandosClientPropertyCache, self).__init__(**kwargs)
104
103
    
105
 
    def properties_changed(self, interface, properties, invalidated):
106
 
        """This is called whenever we get a PropertiesChanged signal
107
 
        It updates the changed properties in the "properties" dict.
 
104
    def _property_changed(self, property, value):
 
105
        """Helper which takes positional arguments"""
 
106
        return self.property_changed(property=property, value=value)
 
107
    
 
108
    def property_changed(self, property=None, value=None):
 
109
        """This is called whenever we get a PropertyChanged signal
 
110
        It updates the changed property in the "properties" dict.
108
111
        """
109
112
        # Update properties dict with new value
110
 
        if interface == client_interface:
111
 
            self.properties.update(properties)
 
113
        self.properties[property] = value
112
114
    
113
115
    def delete(self):
114
116
        self.property_changed_match.remove()
160
162
                                         self.rejected,
161
163
                                         client_interface,
162
164
                                         byte_arrays=True))
163
 
        self.logger('Created client {}'
164
 
                    .format(self.properties["Name"]), level=0)
 
165
        #self.logger('Created client {0}'
 
166
        #            .format(self.properties["Name"]))
165
167
    
166
168
    def using_timer(self, flag):
167
169
        """Call this method with True or False when timer should be
169
171
        """
170
172
        if flag and self._update_timer_callback_tag is None:
171
173
            # Will update the shown timer value every second
172
 
            self._update_timer_callback_tag = (GLib.timeout_add
 
174
            self._update_timer_callback_tag = (gobject.timeout_add
173
175
                                               (1000,
174
176
                                                self.update_timer))
175
177
        elif not (flag or self._update_timer_callback_tag is None):
176
 
            GLib.source_remove(self._update_timer_callback_tag)
 
178
            gobject.source_remove(self._update_timer_callback_tag)
177
179
            self._update_timer_callback_tag = None
178
180
    
179
181
    def checker_completed(self, exitstatus, condition, command):
180
182
        if exitstatus == 0:
181
 
            self.logger('Checker for client {} (command "{}")'
182
 
                        ' succeeded'.format(self.properties["Name"],
183
 
                                            command), level=0)
184
183
            self.update()
185
184
            return
186
185
        # Checker failed
187
186
        if os.WIFEXITED(condition):
188
 
            self.logger('Checker for client {} (command "{}") failed'
189
 
                        ' with exit code {}'
 
187
            self.logger('Checker for client {0} (command "{1}")'
 
188
                        ' failed with exit code {2}'
190
189
                        .format(self.properties["Name"], command,
191
190
                                os.WEXITSTATUS(condition)))
192
191
        elif os.WIFSIGNALED(condition):
193
 
            self.logger('Checker for client {} (command "{}") was'
194
 
                        ' killed by signal {}'
 
192
            self.logger('Checker for client {0} (command "{1}") was'
 
193
                        ' killed by signal {2}'
195
194
                        .format(self.properties["Name"], command,
196
195
                                os.WTERMSIG(condition)))
 
196
        elif os.WCOREDUMP(condition):
 
197
            self.logger('Checker for client {0} (command "{1}")'
 
198
                        ' dumped core'
 
199
                        .format(self.properties["Name"], command))
 
200
        else:
 
201
            self.logger('Checker for client {0} completed'
 
202
                        ' mysteriously'
 
203
                        .format(self.properties["Name"]))
197
204
        self.update()
198
205
    
199
206
    def checker_started(self, command):
200
 
        """Server signals that a checker started."""
201
 
        self.logger('Client {} started checker "{}"'
202
 
                    .format(self.properties["Name"],
203
 
                            command), level=0)
 
207
        """Server signals that a checker started. This could be useful
 
208
           to log in the future. """
 
209
        #self.logger('Client {0} started checker "{1}"'
 
210
        #            .format(self.properties["Name"],
 
211
        #                    str(command)))
 
212
        pass
204
213
    
205
214
    def got_secret(self):
206
 
        self.logger('Client {} received its secret'
 
215
        self.logger('Client {0} received its secret'
207
216
                    .format(self.properties["Name"]))
208
217
    
209
218
    def need_approval(self, timeout, default):
210
219
        if not default:
211
 
            message = 'Client {} needs approval within {} seconds'
 
220
            message = 'Client {0} needs approval within {1} seconds'
212
221
        else:
213
 
            message = 'Client {} will get its secret in {} seconds'
 
222
            message = 'Client {0} will get its secret in {1} seconds'
214
223
        self.logger(message.format(self.properties["Name"],
215
224
                                   timeout/1000))
216
225
    
217
226
    def rejected(self, reason):
218
 
        self.logger('Client {} was rejected; reason: {}'
 
227
        self.logger('Client {0} was rejected; reason: {1}'
219
228
                    .format(self.properties["Name"], reason))
220
229
    
221
230
    def selectable(self):
265
274
            else:
266
275
                timer = datetime.timedelta()
267
276
            if self.properties["ApprovedByDefault"]:
268
 
                message = "Approval in {}. (d)eny?"
 
277
                message = "Approval in {0}. (d)eny?"
269
278
            else:
270
 
                message = "Denial in {}. (a)pprove?"
 
279
                message = "Denial in {0}. (a)pprove?"
271
280
            message = message.format(str(timer).rsplit(".", 1)[0])
272
281
            self.using_timer(True)
273
282
        elif self.properties["LastCheckerStatus"] != 0:
281
290
                timer = max(expires - datetime.datetime.utcnow(),
282
291
                            datetime.timedelta())
283
292
            message = ('A checker has failed! Time until client'
284
 
                       ' gets disabled: {}'
 
293
                       ' gets disabled: {0}'
285
294
                       .format(str(timer).rsplit(".", 1)[0]))
286
295
            self.using_timer(True)
287
296
        else:
288
297
            message = "enabled"
289
298
            self.using_timer(False)
290
 
        self._text = "{}{}".format(base, message)
 
299
        self._text = "{0}{1}".format(base, message)
291
300
        
292
301
        if not urwid.supports_unicode():
293
302
            self._text = self._text.encode("ascii", "replace")
306
315
            self.update_hook()
307
316
    
308
317
    def update_timer(self):
309
 
        """called by GLib. Will indefinitely loop until
310
 
        GLib.source_remove() on tag is called
311
 
        """
 
318
        """called by gobject. Will indefinitely loop until
 
319
        gobject.source_remove() on tag is called"""
312
320
        self.update()
313
321
        return True             # Keep calling this
314
322
    
315
323
    def delete(self, **kwargs):
316
324
        if self._update_timer_callback_tag is not None:
317
 
            GLib.source_remove(self._update_timer_callback_tag)
 
325
            gobject.source_remove(self._update_timer_callback_tag)
318
326
            self._update_timer_callback_tag = None
319
327
        for match in self.match_objects:
320
328
            match.remove()
333
341
        """Handle keys.
334
342
        This overrides the method from urwid.FlowWidget"""
335
343
        if key == "+":
336
 
            self.proxy.Set(client_interface, "Enabled",
337
 
                           dbus.Boolean(True), ignore_reply = True,
338
 
                           dbus_interface = dbus.PROPERTIES_IFACE)
 
344
            self.proxy.Enable(dbus_interface = client_interface,
 
345
                              ignore_reply=True)
339
346
        elif key == "-":
340
 
            self.proxy.Set(client_interface, "Enabled", False,
341
 
                           ignore_reply = True,
342
 
                           dbus_interface = dbus.PROPERTIES_IFACE)
 
347
            self.proxy.Disable(dbus_interface = client_interface,
 
348
                               ignore_reply=True)
343
349
        elif key == "a":
344
350
            self.proxy.Approve(dbus.Boolean(True, variant_level=1),
345
351
                               dbus_interface = client_interface,
353
359
                                                  .object_path,
354
360
                                                  ignore_reply=True)
355
361
        elif key == "s":
356
 
            self.proxy.Set(client_interface, "CheckerRunning",
357
 
                           dbus.Boolean(True), ignore_reply = True,
358
 
                           dbus_interface = dbus.PROPERTIES_IFACE)
 
362
            self.proxy.StartChecker(dbus_interface = client_interface,
 
363
                                    ignore_reply=True)
359
364
        elif key == "S":
360
 
            self.proxy.Set(client_interface, "CheckerRunning",
361
 
                           dbus.Boolean(False), ignore_reply = True,
362
 
                           dbus_interface = dbus.PROPERTIES_IFACE)
 
365
            self.proxy.StopChecker(dbus_interface = client_interface,
 
366
                                   ignore_reply=True)
363
367
        elif key == "C":
364
368
            self.proxy.CheckedOK(dbus_interface = client_interface,
365
369
                                 ignore_reply=True)
373
377
        else:
374
378
            return key
375
379
    
376
 
    def properties_changed(self, interface, properties, invalidated):
377
 
        """Call self.update() if any properties changed.
 
380
    def property_changed(self, property=None, **kwargs):
 
381
        """Call self.update() if old value is not new value.
378
382
        This overrides the method from MandosClientPropertyCache"""
379
 
        old_values = { key: self.properties.get(key)
380
 
                       for key in properties.keys() }
381
 
        super(MandosClientWidget, self).properties_changed(
382
 
            interface, properties, invalidated)
383
 
        if any(old_values[key] != self.properties.get(key)
384
 
               for key in old_values):
 
383
        property_name = str(property)
 
384
        old_value = self.properties.get(property_name)
 
385
        super(MandosClientWidget, self).property_changed(
 
386
            property=property, **kwargs)
 
387
        if self.properties.get(property_name) != old_value:
385
388
            self.update()
386
389
 
387
390
 
401
404
    """This is the entire user interface - the whole screen
402
405
    with boxes, lists of client widgets, etc.
403
406
    """
404
 
    def __init__(self, max_log_length=1000, log_level=1):
 
407
    def __init__(self, max_log_length=1000):
405
408
        DBusGMainLoop(set_as_default=True)
406
409
        
407
410
        self.screen = urwid.curses_display.Screen()
445
448
        self.log = []
446
449
        self.max_log_length = max_log_length
447
450
        
448
 
        self.log_level = log_level
449
 
        
450
451
        # We keep a reference to the log widget so we can remove it
451
452
        # from the ListWalker without it getting destroyed
452
453
        self.logbox = ConstrainedListBox(self.log)
463
464
                              "q: Quit  ?: Help"))
464
465
        
465
466
        self.busname = domain + '.Mandos'
466
 
        self.main_loop = GLib.MainLoop()
 
467
        self.main_loop = gobject.MainLoop()
467
468
    
468
469
    def client_not_found(self, fingerprint, address):
469
 
        self.log_message("Client with address {} and fingerprint {}"
470
 
                         " could not be found"
 
470
        self.log_message("Client with address {0} and fingerprint"
 
471
                         " {1} could not be found"
471
472
                         .format(address, fingerprint))
472
473
    
473
474
    def rebuild(self):
486
487
            self.uilist.append(self.logbox)
487
488
        self.topwidget = urwid.Pile(self.uilist)
488
489
    
489
 
    def log_message(self, message, level=1):
 
490
    def log_message(self, message):
490
491
        """Log message formatted with timestamp"""
491
 
        if level < self.log_level:
492
 
            return
493
492
        timestamp = datetime.datetime.now().isoformat()
494
 
        self.log_message_raw("{}: {}".format(timestamp, message),
495
 
                             level=level)
 
493
        self.log_message_raw(timestamp + ": " + message)
496
494
    
497
 
    def log_message_raw(self, markup, level=1):
 
495
    def log_message_raw(self, markup):
498
496
        """Add a log message to the log buffer."""
499
 
        if level < self.log_level:
500
 
            return
501
497
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
502
498
        if (self.max_log_length
503
499
            and len(self.log) > self.max_log_length):
510
506
        """Toggle visibility of the log buffer."""
511
507
        self.log_visible = not self.log_visible
512
508
        self.rebuild()
513
 
        self.log_message("Log visibility changed to: {}"
514
 
                         .format(self.log_visible), level=0)
 
509
        #self.log_message("Log visibility changed to: "
 
510
        #                 + str(self.log_visible))
515
511
    
516
512
    def change_log_display(self):
517
513
        """Change type of log display.
522
518
            self.log_wrap = "clip"
523
519
        for textwidget in self.log:
524
520
            textwidget.set_wrap_mode(self.log_wrap)
525
 
        self.log_message("Wrap mode: {}".format(self.log_wrap),
526
 
                         level=0)
 
521
        #self.log_message("Wrap mode: " + self.log_wrap)
527
522
    
528
 
    def find_and_remove_client(self, path, interfaces):
 
523
    def find_and_remove_client(self, path, name):
529
524
        """Find a client by its object path and remove it.
530
525
        
531
 
        This is connected to the InterfacesRemoved signal from the
 
526
        This is connected to the ClientRemoved signal from the
532
527
        Mandos server object."""
533
 
        if client_interface not in interfaces:
534
 
            # Not a Mandos client object; ignore
535
 
            return
536
528
        try:
537
529
            client = self.clients_dict[path]
538
530
        except KeyError:
539
531
            # not found?
540
 
            self.log_message("Unknown client {!r} removed"
541
 
                             .format(path))
 
532
            self.log_message("Unknown client {0!r} ({1!r}) removed"
 
533
                             .format(name, path))
542
534
            return
543
535
        client.delete()
544
536
    
545
 
    def add_new_client(self, path, ifs_and_props):
546
 
        """Find a client by its object path and remove it.
547
 
        
548
 
        This is connected to the InterfacesAdded signal from the
549
 
        Mandos server object.
550
 
        """
551
 
        if client_interface not in ifs_and_props:
552
 
            # Not a Mandos client object; ignore
553
 
            return
 
537
    def add_new_client(self, path):
554
538
        client_proxy_object = self.bus.get_object(self.busname, path)
555
539
        self.add_client(MandosClientWidget(server_proxy_object
556
540
                                           =self.mandos_serv,
561
545
                                           delete_hook
562
546
                                           =self.remove_client,
563
547
                                           logger
564
 
                                           =self.log_message,
565
 
                                           properties
566
 
                                           = dict(ifs_and_props[
567
 
                                               client_interface])),
 
548
                                           =self.log_message),
568
549
                        path=path)
569
550
    
570
551
    def add_client(self, client, path=None):
605
586
            mandos_clients = dbus.Dictionary()
606
587
        
607
588
        (self.mandos_serv
608
 
         .connect_to_signal("InterfacesRemoved",
 
589
         .connect_to_signal("ClientRemoved",
609
590
                            self.find_and_remove_client,
610
 
                            dbus_interface
611
 
                            = dbus.OBJECT_MANAGER_IFACE,
 
591
                            dbus_interface=server_interface,
612
592
                            byte_arrays=True))
613
593
        (self.mandos_serv
614
 
         .connect_to_signal("InterfacesAdded",
 
594
         .connect_to_signal("ClientAdded",
615
595
                            self.add_new_client,
616
 
                            dbus_interface
617
 
                            = dbus.OBJECT_MANAGER_IFACE,
 
596
                            dbus_interface=server_interface,
618
597
                            byte_arrays=True))
619
598
        (self.mandos_serv
620
599
         .connect_to_signal("ClientNotFound",
638
617
                            path=path)
639
618
        
640
619
        self.refresh()
641
 
        self._input_callback_tag = (GLib.io_add_watch
 
620
        self._input_callback_tag = (gobject.io_add_watch
642
621
                                    (sys.stdin.fileno(),
643
 
                                     GLib.IO_IN,
 
622
                                     gobject.IO_IN,
644
623
                                     self.process_input))
645
624
        self.main_loop.run()
646
625
        # Main loop has finished, we should close everything now
647
 
        GLib.source_remove(self._input_callback_tag)
 
626
        gobject.source_remove(self._input_callback_tag)
648
627
        self.screen.stop()
649
628
    
650
629
    def stop(self):
674
653
            elif key == "window resize":
675
654
                self.size = self.screen.get_cols_rows()
676
655
                self.refresh()
677
 
            elif key == "ctrl l":
678
 
                self.screen.clear()
 
656
            elif key == "\f":  # Ctrl-L
679
657
                self.refresh()
680
658
            elif key == "l" or key == "D":
681
659
                self.toggle_log_display()
693
671
                                            "?: Help",
694
672
                                            "l: Log window toggle",
695
673
                                            "TAB: Switch window",
696
 
                                            "w: Wrap (log lines)",
697
 
                                            "v: Toggle verbose log",
698
 
                                            ))))
 
674
                                            "w: Wrap (log)"))))
699
675
                self.log_message_raw(("bold",
700
676
                                      "  "
701
677
                                      .join(("Clients:",
714
690
                else:
715
691
                    self.topwidget.set_focus(self.logbox)
716
692
                self.refresh()
717
 
            elif key == "v":
718
 
                if self.log_level == 0:
719
 
                    self.log_level = 1
720
 
                    self.log_message("Verbose mode: Off")
721
 
                else:
722
 
                    self.log_level = 0
723
 
                    self.log_message("Verbose mode: On")
724
693
            #elif (key == "end" or key == "meta >" or key == "G"
725
694
            #      or key == ">"):
726
695
            #    pass            # xxx end-of-buffer