/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: 2019-11-03 19:17:57 UTC
  • Revision ID: teddy@recompile.se-20191103191757-1hdpp0u5fxa8iumo
INSTALL: Add "-" argument to "su" invocations.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
 
1
#!/usr/bin/python3 -bbI
2
2
# -*- mode: python; coding: utf-8 -*-
3
3
#
4
4
# Mandos Monitor - Control and monitor the Mandos server
5
5
#
6
 
# Copyright © 2009-2017 Teddy Hogeborn
7
 
# Copyright © 2009-2017 Björn Påhlsson
8
 
#
9
 
# This program is free software: you can redistribute it and/or modify
10
 
# it under the terms of the GNU General Public License as published by
 
6
# Copyright © 2009-2019 Teddy Hogeborn
 
7
# Copyright © 2009-2019 Björn Påhlsson
 
8
#
 
9
# This file is part of Mandos.
 
10
#
 
11
# Mandos is free software: you can redistribute it and/or modify it
 
12
# under the terms of the GNU General Public License as published by
11
13
# the Free Software Foundation, either version 3 of the License, or
12
14
# (at your option) any later version.
13
15
#
14
 
#     This program is distributed in the hope that it will be useful,
15
 
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
 
16
#     Mandos is distributed in the hope that it will be useful, but
 
17
#     WITHOUT ANY WARRANTY; without even the implied warranty of
16
18
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
19
#     GNU General Public License for more details.
18
20
#
19
21
# You should have received a copy of the GNU General Public License
20
 
# along with this program.  If not, see
21
 
# <http://www.gnu.org/licenses/>.
 
22
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
22
23
#
23
24
# Contact the authors at <mandos@recompile.se>.
24
25
#
32
33
 
33
34
import sys
34
35
import os
35
 
 
 
36
import warnings
36
37
import datetime
 
38
import locale
 
39
import logging
37
40
 
38
41
import urwid.curses_display
39
42
import urwid
43
46
 
44
47
import dbus
45
48
 
46
 
import locale
47
 
 
48
 
import logging
49
 
 
50
49
if sys.version_info.major == 2:
51
50
    str = unicode
52
51
 
53
 
locale.setlocale(locale.LC_ALL, '')
54
 
 
55
 
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
 
52
log = logging.getLogger(os.path.basename(sys.argv[0]))
 
53
logging.basicConfig(level="NOTSET", # Show all messages
 
54
                    format="%(message)s") # Show basic log messages
 
55
 
 
56
logging.captureWarnings(True)   # Show warnings via the logging system
 
57
 
 
58
locale.setlocale(locale.LC_ALL, "")
 
59
 
 
60
logging.getLogger("dbus.proxies").setLevel(logging.CRITICAL)
56
61
 
57
62
# Some useful constants
58
 
domain = 'se.recompile'
59
 
server_interface = domain + '.Mandos'
60
 
client_interface = domain + '.Mandos.Client'
61
 
version = "1.7.15"
 
63
domain = "se.recompile"
 
64
server_interface = domain + ".Mandos"
 
65
client_interface = domain + ".Mandos.Client"
 
66
version = "1.8.9"
62
67
 
63
68
try:
64
69
    dbus.OBJECT_MANAGER_IFACE
121
126
    """
122
127
 
123
128
    def __init__(self, server_proxy_object=None, update_hook=None,
124
 
                 delete_hook=None, logger=None, **kwargs):
 
129
                 delete_hook=None, **kwargs):
125
130
        # Called on update
126
131
        self.update_hook = update_hook
127
132
        # Called on delete
128
133
        self.delete_hook = delete_hook
129
134
        # Mandos Server proxy object
130
135
        self.server_proxy_object = server_proxy_object
131
 
        # Logger
132
 
        self.logger = logger
133
136
 
134
137
        self._update_timer_callback_tag = None
135
138
 
162
165
                                         self.rejected,
163
166
                                         client_interface,
164
167
                                         byte_arrays=True))
165
 
        self.logger('Created client {}'
166
 
                    .format(self.properties["Name"]), level=0)
 
168
        log.debug("Created client %s", self.properties["Name"])
167
169
 
168
170
    def using_timer(self, flag):
169
171
        """Call this method with True or False when timer should be
171
173
        """
172
174
        if flag and self._update_timer_callback_tag is None:
173
175
            # Will update the shown timer value every second
174
 
            self._update_timer_callback_tag = (GLib.timeout_add
175
 
                                               (1000,
176
 
                                                self.update_timer))
 
176
            self._update_timer_callback_tag = (
 
177
                GLib.timeout_add(1000,
 
178
                                 glib_safely(self.update_timer)))
177
179
        elif not (flag or self._update_timer_callback_tag is None):
178
180
            GLib.source_remove(self._update_timer_callback_tag)
179
181
            self._update_timer_callback_tag = None
180
182
 
181
183
    def checker_completed(self, exitstatus, condition, command):
182
184
        if exitstatus == 0:
183
 
            self.logger('Checker for client {} (command "{}")'
184
 
                        ' succeeded'.format(self.properties["Name"],
185
 
                                            command), level=0)
 
185
            log.debug('Checker for client %s (command "%s")'
 
186
                      " succeeded", self.properties["Name"], command)
186
187
            self.update()
187
188
            return
188
189
        # Checker failed
189
190
        if os.WIFEXITED(condition):
190
 
            self.logger('Checker for client {} (command "{}") failed'
191
 
                        ' with exit code {}'
192
 
                        .format(self.properties["Name"], command,
193
 
                                os.WEXITSTATUS(condition)))
 
191
            log.info('Checker for client %s (command "%s") failed'
 
192
                     " with exit code %d", self.properties["Name"],
 
193
                     command, os.WEXITSTATUS(condition))
194
194
        elif os.WIFSIGNALED(condition):
195
 
            self.logger('Checker for client {} (command "{}") was'
196
 
                        ' killed by signal {}'
197
 
                        .format(self.properties["Name"], command,
198
 
                                os.WTERMSIG(condition)))
 
195
            log.info('Checker for client %s (command "%s") was'
 
196
                     " killed by signal %d", self.properties["Name"],
 
197
                     command, os.WTERMSIG(condition))
199
198
        self.update()
200
199
 
201
200
    def checker_started(self, command):
202
201
        """Server signals that a checker started."""
203
 
        self.logger('Client {} started checker "{}"'
204
 
                    .format(self.properties["Name"],
205
 
                            command), level=0)
 
202
        log.debug('Client %s started checker "%s"',
 
203
                  self.properties["Name"], command)
206
204
 
207
205
    def got_secret(self):
208
 
        self.logger('Client {} received its secret'
209
 
                    .format(self.properties["Name"]))
 
206
        log.info("Client %s received its secret",
 
207
                 self.properties["Name"])
210
208
 
211
209
    def need_approval(self, timeout, default):
212
210
        if not default:
213
 
            message = 'Client {} needs approval within {} seconds'
 
211
            message = "Client %s needs approval within %f seconds"
214
212
        else:
215
 
            message = 'Client {} will get its secret in {} seconds'
216
 
        self.logger(message.format(self.properties["Name"],
217
 
                                   timeout/1000))
 
213
            message = "Client %s will get its secret in %f seconds"
 
214
        log.info(message, self.properties["Name"], timeout/1000)
218
215
 
219
216
    def rejected(self, reason):
220
 
        self.logger('Client {} was rejected; reason: {}'
221
 
                    .format(self.properties["Name"], reason))
 
217
        log.info("Client %s was rejected; reason: %s",
 
218
                 self.properties["Name"], reason)
222
219
 
223
220
    def selectable(self):
224
221
        """Make this a "selectable" widget.
250
247
        # Rebuild focus and non-focus widgets using current properties
251
248
 
252
249
        # Base part of a client. Name!
253
 
        base = '{name}: '.format(name=self.properties["Name"])
 
250
        base = "{name}: ".format(name=self.properties["Name"])
254
251
        if not self.properties["Enabled"]:
255
252
            message = "DISABLED"
256
253
            self.using_timer(False)
278
275
                timer = datetime.timedelta(0)
279
276
            else:
280
277
                expires = (datetime.datetime.strptime
281
 
                           (expires, '%Y-%m-%dT%H:%M:%S.%f'))
 
278
                           (expires, "%Y-%m-%dT%H:%M:%S.%f"))
282
279
                timer = max(expires - datetime.datetime.utcnow(),
283
280
                            datetime.timedelta())
284
 
            message = ('A checker has failed! Time until client'
285
 
                       ' gets disabled: {}'
 
281
            message = ("A checker has failed! Time until client"
 
282
                       " gets disabled: {}"
286
283
                       .format(str(timer).rsplit(".", 1)[0]))
287
284
            self.using_timer(True)
288
285
        else:
386
383
            self.update()
387
384
 
388
385
 
 
386
def glib_safely(func, retval=True):
 
387
    def safe_func(*args, **kwargs):
 
388
        try:
 
389
            return func(*args, **kwargs)
 
390
        except Exception:
 
391
            log.exception("")
 
392
            return retval
 
393
    return safe_func
 
394
 
 
395
 
389
396
class ConstrainedListBox(urwid.ListBox):
390
397
    """Like a normal urwid.ListBox, but will consume all "up" or
391
398
    "down" key presses, thus not allowing any containing widgets to
403
410
    """This is the entire user interface - the whole screen
404
411
    with boxes, lists of client widgets, etc.
405
412
    """
406
 
    def __init__(self, max_log_length=1000, log_level=1):
 
413
    def __init__(self, max_log_length=1000):
407
414
        DBusGMainLoop(set_as_default=True)
408
415
 
409
416
        self.screen = urwid.curses_display.Screen()
443
450
        self.clients_dict = {}
444
451
 
445
452
        # We will add Text widgets to this list
446
 
        self.log = []
 
453
        self.log = urwid.SimpleListWalker([])
447
454
        self.max_log_length = max_log_length
448
455
 
449
 
        self.log_level = log_level
450
 
 
451
456
        # We keep a reference to the log widget so we can remove it
452
457
        # from the ListWalker without it getting destroyed
453
458
        self.logbox = ConstrainedListBox(self.log)
457
462
        self.log_visible = True
458
463
        self.log_wrap = "any"
459
464
 
 
465
        self.loghandler = UILogHandler(self)
 
466
 
460
467
        self.rebuild()
461
 
        self.log_message_raw(("bold",
462
 
                              "Mandos Monitor version " + version))
463
 
        self.log_message_raw(("bold",
464
 
                              "q: Quit  ?: Help"))
 
468
        self.add_log_line(("bold",
 
469
                           "Mandos Monitor version " + version))
 
470
        self.add_log_line(("bold", "q: Quit  ?: Help"))
465
471
 
466
 
        self.busname = domain + '.Mandos'
 
472
        self.busname = domain + ".Mandos"
467
473
        self.main_loop = GLib.MainLoop()
468
474
 
469
 
    def client_not_found(self, fingerprint, address):
470
 
        self.log_message("Client with address {} and fingerprint {}"
471
 
                         " could not be found"
472
 
                         .format(address, fingerprint))
 
475
    def client_not_found(self, key_id, address):
 
476
        log.info("Client with address %s and key ID %s could"
 
477
                 " not be found", address, key_id)
473
478
 
474
479
    def rebuild(self):
475
480
        """This rebuilds the User Interface.
486
491
            self.uilist.append(self.logbox)
487
492
        self.topwidget = urwid.Pile(self.uilist)
488
493
 
489
 
    def log_message(self, message, level=1):
490
 
        """Log message formatted with timestamp"""
491
 
        if level < self.log_level:
492
 
            return
493
 
        timestamp = datetime.datetime.now().isoformat()
494
 
        self.log_message_raw("{}: {}".format(timestamp, message),
495
 
                             level=level)
496
 
 
497
 
    def log_message_raw(self, markup, level=1):
498
 
        """Add a log message to the log buffer."""
499
 
        if level < self.log_level:
500
 
            return
 
494
    def add_log_line(self, markup):
501
495
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
502
496
        if self.max_log_length:
503
497
            if len(self.log) > self.max_log_length:
504
 
                del self.log[0:len(self.log)-self.max_log_length-1]
505
 
        self.logbox.set_focus(len(self.logbox.body.contents),
 
498
                del self.log[0:(len(self.log) - self.max_log_length)]
 
499
        self.logbox.set_focus(len(self.logbox.body.contents)-1,
506
500
                              coming_from="above")
507
501
        self.refresh()
508
502
 
510
504
        """Toggle visibility of the log buffer."""
511
505
        self.log_visible = not self.log_visible
512
506
        self.rebuild()
513
 
        self.log_message("Log visibility changed to: {}"
514
 
                         .format(self.log_visible), level=0)
 
507
        log.debug("Log visibility changed to: %s", self.log_visible)
515
508
 
516
509
    def change_log_display(self):
517
510
        """Change type of log display.
522
515
            self.log_wrap = "clip"
523
516
        for textwidget in self.log:
524
517
            textwidget.set_wrap_mode(self.log_wrap)
525
 
        self.log_message("Wrap mode: {}".format(self.log_wrap),
526
 
                         level=0)
 
518
        log.debug("Wrap mode: %s", self.log_wrap)
527
519
 
528
520
    def find_and_remove_client(self, path, interfaces):
529
521
        """Find a client by its object path and remove it.
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
            log.warning("Unknown client %s removed", path)
542
533
            return
543
534
        client.delete()
544
535
 
557
548
            proxy_object=client_proxy_object,
558
549
            update_hook=self.refresh,
559
550
            delete_hook=self.remove_client,
560
 
            logger=self.log_message,
561
551
            properties=dict(ifs_and_props[client_interface])),
562
552
                        path=path)
563
553
 
583
573
 
584
574
    def run(self):
585
575
        """Start the main loop and exit when it's done."""
 
576
        log.addHandler(self.loghandler)
 
577
        self.orig_log_propagate = log.propagate
 
578
        log.propagate = False
 
579
        self.orig_log_level = log.level
 
580
        log.setLevel("INFO")
586
581
        self.bus = dbus.SystemBus()
587
582
        mandos_dbus_objc = self.bus.get_object(
588
583
            self.busname, "/", follow_name_owner_changes=True)
592
587
            mandos_clients = (self.mandos_serv
593
588
                              .GetAllClientsWithProperties())
594
589
            if not mandos_clients:
595
 
                self.log_message_raw(("bold",
596
 
                                      "Note: Server has no clients."))
 
590
                log.warning("Note: Server has no clients.")
597
591
        except dbus.exceptions.DBusException:
598
 
            self.log_message_raw(("bold",
599
 
                                  "Note: No Mandos server running."))
 
592
            log.warning("Note: No Mandos server running.")
600
593
            mandos_clients = dbus.Dictionary()
601
594
 
602
595
        (self.mandos_serv
622
615
                proxy_object=client_proxy_object,
623
616
                properties=client,
624
617
                update_hook=self.refresh,
625
 
                delete_hook=self.remove_client,
626
 
                logger=self.log_message),
 
618
                delete_hook=self.remove_client),
627
619
                            path=path)
628
620
 
629
621
        self.refresh()
630
 
        self._input_callback_tag = (GLib.io_add_watch
631
 
                                    (sys.stdin.fileno(),
632
 
                                     GLib.IO_IN,
633
 
                                     self.process_input))
 
622
        self._input_callback_tag = (
 
623
            GLib.io_add_watch(
 
624
                GLib.IOChannel.unix_new(sys.stdin.fileno()),
 
625
                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
 
626
                glib_safely(self.process_input)))
634
627
        self.main_loop.run()
635
628
        # Main loop has finished, we should close everything now
636
629
        GLib.source_remove(self._input_callback_tag)
637
 
        self.screen.stop()
 
630
        with warnings.catch_warnings():
 
631
            warnings.simplefilter("ignore", BytesWarning)
 
632
            self.screen.stop()
638
633
 
639
634
    def stop(self):
640
635
        self.main_loop.quit()
 
636
        log.removeHandler(self.loghandler)
 
637
        log.propagate = self.orig_log_propagate
641
638
 
642
639
    def process_input(self, source, condition):
643
640
        keys = self.screen.get_input()
676
673
                if not self.log_visible:
677
674
                    self.log_visible = True
678
675
                    self.rebuild()
679
 
                self.log_message_raw(("bold",
680
 
                                      "  ".
681
 
                                      join(("q: Quit",
682
 
                                            "?: Help",
683
 
                                            "l: Log window toggle",
684
 
                                            "TAB: Switch window",
685
 
                                            "w: Wrap (log lines)",
686
 
                                            "v: Toggle verbose log",
687
 
                                            ))))
688
 
                self.log_message_raw(("bold",
689
 
                                      "  "
690
 
                                      .join(("Clients:",
691
 
                                             "+: Enable",
692
 
                                             "-: Disable",
693
 
                                             "R: Remove",
694
 
                                             "s: Start new checker",
695
 
                                             "S: Stop checker",
696
 
                                             "C: Checker OK",
697
 
                                             "a: Approve",
698
 
                                             "d: Deny"))))
 
676
                self.add_log_line(("bold",
 
677
                                   "  ".join(("q: Quit",
 
678
                                              "?: Help",
 
679
                                              "l: Log window toggle",
 
680
                                              "TAB: Switch window",
 
681
                                              "w: Wrap (log lines)",
 
682
                                              "v: Toggle verbose log",
 
683
                                   ))))
 
684
                self.add_log_line(("bold",
 
685
                                   "  ".join(("Clients:",
 
686
                                              "+: Enable",
 
687
                                              "-: Disable",
 
688
                                              "R: Remove",
 
689
                                              "s: Start new checker",
 
690
                                              "S: Stop checker",
 
691
                                              "C: Checker OK",
 
692
                                              "a: Approve",
 
693
                                              "d: Deny",
 
694
                                   ))))
699
695
                self.refresh()
700
696
            elif key == "tab":
701
697
                if self.topwidget.get_focus() is self.logbox:
704
700
                    self.topwidget.set_focus(self.logbox)
705
701
                self.refresh()
706
702
            elif key == "v":
707
 
                if self.log_level == 0:
708
 
                    self.log_level = 1
709
 
                    self.log_message("Verbose mode: Off")
 
703
                if log.level < logging.INFO:
 
704
                    log.setLevel(logging.INFO)
 
705
                    log.info("Verbose mode: Off")
710
706
                else:
711
 
                    self.log_level = 0
712
 
                    self.log_message("Verbose mode: On")
 
707
                    log.setLevel(logging.NOTSET)
 
708
                    log.info("Verbose mode: On")
713
709
            # elif (key == "end" or key == "meta >" or key == "G"
714
710
            #       or key == ">"):
715
711
            #     pass            # xxx end-of-buffer
734
730
        return True
735
731
 
736
732
 
 
733
class UILogHandler(logging.Handler):
 
734
    def __init__(self, ui, *args, **kwargs):
 
735
        self.ui = ui
 
736
        super(UILogHandler, self).__init__(*args, **kwargs)
 
737
        self.setFormatter(
 
738
            logging.Formatter("%(asctime)s: %(message)s"))
 
739
    def emit(self, record):
 
740
        msg = self.format(record)
 
741
        if record.levelno > logging.INFO:
 
742
            msg = ("bold", msg)
 
743
        self.ui.add_log_line(msg)
 
744
 
 
745
 
737
746
ui = UserInterface()
738
747
try:
739
748
    ui.run()
740
749
except KeyboardInterrupt:
741
 
    ui.screen.stop()
742
 
except Exception as e:
743
 
    ui.log_message(str(e))
744
 
    ui.screen.stop()
 
750
    with warnings.catch_warnings():
 
751
        warnings.filterwarnings("ignore", "", BytesWarning)
 
752
        ui.screen.stop()
 
753
except Exception:
 
754
    with warnings.catch_warnings():
 
755
        warnings.filterwarnings("ignore", "", BytesWarning)
 
756
        ui.screen.stop()
745
757
    raise