1
#!/usr/bin/python3 -bbI
 
2
2
# -*- mode: python; coding: utf-8 -*-
 
4
4
# Mandos Monitor - Control and monitor the Mandos server
 
6
 
# Copyright © 2009-2016 Teddy Hogeborn
 
7
 
# Copyright © 2009-2016 Björn Påhlsson
 
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
 
 
9
# This file is part of Mandos.
 
 
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.
 
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.
 
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/>.
 
23
24
# Contact the authors at <mandos@recompile.se>.
 
26
26
from __future__ import (division, absolute_import, print_function,
 
29
30
    from future_builtins import *
 
30
31
except ImportError:
 
38
41
import urwid.curses_display
 
 
50
49
if sys.version_info.major == 2:
 
53
 
locale.setlocale(locale.LC_ALL, '')
 
55
 
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
 
 
54
# Show warnings by default
 
 
55
if not sys.warnoptions:
 
 
56
    warnings.simplefilter("default")
 
 
58
log = logging.getLogger(os.path.basename(sys.argv[0]))
 
 
59
logging.basicConfig(level="NOTSET", # Show all messages
 
 
60
                    format="%(message)s") # Show basic log messages
 
 
62
logging.captureWarnings(True)   # Show warnings via the logging system
 
 
64
locale.setlocale(locale.LC_ALL, "")
 
 
66
logging.getLogger("dbus.proxies").setLevel(logging.CRITICAL)
 
57
68
# Some useful constants
 
58
 
domain = 'se.recompile'
 
59
 
server_interface = domain + '.Mandos'
 
60
 
client_interface = domain + '.Mandos.Client'
 
 
69
domain = "se.recompile"
 
 
70
server_interface = domain + ".Mandos"
 
 
71
client_interface = domain + ".Mandos.Client"
 
64
75
    dbus.OBJECT_MANAGER_IFACE
 
 
123
134
    def __init__(self, server_proxy_object=None, update_hook=None,
 
124
 
                 delete_hook=None, logger=None, **kwargs):
 
 
135
                 delete_hook=None, **kwargs):
 
125
136
        # Called on update
 
126
137
        self.update_hook = update_hook
 
127
138
        # Called on delete
 
128
139
        self.delete_hook = delete_hook
 
129
140
        # Mandos Server proxy object
 
130
141
        self.server_proxy_object = server_proxy_object
 
134
143
        self._update_timer_callback_tag = None
 
 
172
180
        if flag and self._update_timer_callback_tag is None:
 
173
181
            # Will update the shown timer value every second
 
174
 
            self._update_timer_callback_tag = (GLib.timeout_add
 
 
182
            self._update_timer_callback_tag = (
 
 
183
                GLib.timeout_add(1000,
 
 
184
                                 glib_safely(self.update_timer)))
 
177
185
        elif not (flag or self._update_timer_callback_tag is None):
 
178
186
            GLib.source_remove(self._update_timer_callback_tag)
 
179
187
            self._update_timer_callback_tag = None
 
181
189
    def checker_completed(self, exitstatus, condition, command):
 
182
190
        if exitstatus == 0:
 
183
 
            self.logger('Checker for client {} (command "{}")'
 
184
 
                        ' succeeded'.format(self.properties["Name"],
 
 
191
            log.debug('Checker for client %s (command "%s")'
 
 
192
                      " succeeded", self.properties["Name"], command)
 
189
196
        if os.WIFEXITED(condition):
 
190
 
            self.logger('Checker for client {} (command "{}") failed'
 
192
 
                        .format(self.properties["Name"], command,
 
193
 
                                os.WEXITSTATUS(condition)))
 
 
197
            log.info('Checker for client %s (command "%s") failed'
 
 
198
                     " with exit code %d", self.properties["Name"],
 
 
199
                     command, os.WEXITSTATUS(condition))
 
194
200
        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)))
 
 
201
            log.info('Checker for client %s (command "%s") was'
 
 
202
                     " killed by signal %d", self.properties["Name"],
 
 
203
                     command, os.WTERMSIG(condition))
 
201
206
    def checker_started(self, command):
 
202
207
        """Server signals that a checker started."""
 
203
 
        self.logger('Client {} started checker "{}"'
 
204
 
                    .format(self.properties["Name"],
 
 
208
        log.debug('Client %s started checker "%s"',
 
 
209
                  self.properties["Name"], command)
 
207
211
    def got_secret(self):
 
208
 
        self.logger('Client {} received its secret'
 
209
 
                    .format(self.properties["Name"]))
 
 
212
        log.info("Client %s received its secret",
 
 
213
                 self.properties["Name"])
 
211
215
    def need_approval(self, timeout, default):
 
213
 
            message = 'Client {} needs approval within {} seconds'
 
 
217
            message = "Client %s needs approval within %f seconds"
 
215
 
            message = 'Client {} will get its secret in {} seconds'
 
216
 
        self.logger(message.format(self.properties["Name"],
 
 
219
            message = "Client %s will get its secret in %f seconds"
 
 
220
        log.info(message, self.properties["Name"], timeout/1000)
 
219
222
    def rejected(self, reason):
 
220
 
        self.logger('Client {} was rejected; reason: {}'
 
221
 
                    .format(self.properties["Name"], reason))
 
 
223
        log.info("Client %s was rejected; reason: %s",
 
 
224
                 self.properties["Name"], reason)
 
223
226
    def selectable(self):
 
224
227
        """Make this a "selectable" widget.
 
 
278
281
                timer = datetime.timedelta(0)
 
280
283
                expires = (datetime.datetime.strptime
 
281
 
                           (expires, '%Y-%m-%dT%H:%M:%S.%f'))
 
 
284
                           (expires, "%Y-%m-%dT%H:%M:%S.%f"))
 
282
285
                timer = max(expires - datetime.datetime.utcnow(),
 
283
286
                            datetime.timedelta())
 
284
 
            message = ('A checker has failed! Time until client'
 
 
287
            message = ("A checker has failed! Time until client"
 
286
289
                       .format(str(timer).rsplit(".", 1)[0]))
 
287
290
            self.using_timer(True)
 
 
402
 
class UserInterface(object):
 
403
416
    """This is the entire user interface - the whole screen
 
404
417
    with boxes, lists of client widgets, etc.
 
406
 
    def __init__(self, max_log_length=1000, log_level=1):
 
 
419
    def __init__(self, max_log_length=1000):
 
407
420
        DBusGMainLoop(set_as_default=True)
 
409
422
        self.screen = urwid.curses_display.Screen()
 
 
457
468
        self.log_visible = True
 
458
469
        self.log_wrap = "any"
 
 
471
        self.loghandler = UILogHandler(self)
 
461
 
        self.log_message_raw(("bold",
 
462
 
                              "Mandos Monitor version " + version))
 
463
 
        self.log_message_raw(("bold",
 
 
474
        self.add_log_line(("bold",
 
 
475
                           "Mandos Monitor version " + version))
 
 
476
        self.add_log_line(("bold", "q: Quit  ?: Help"))
 
466
 
        self.busname = domain + '.Mandos'
 
 
478
        self.busname = domain + ".Mandos"
 
467
479
        self.main_loop = GLib.MainLoop()
 
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))
 
 
481
    def client_not_found(self, key_id, address):
 
 
482
        log.info("Client with address %s and key ID %s could"
 
 
483
                 " not be found", address, key_id)
 
474
485
    def rebuild(self):
 
475
486
        """This rebuilds the User Interface.
 
 
486
497
            self.uilist.append(self.logbox)
 
487
498
        self.topwidget = urwid.Pile(self.uilist)
 
489
 
    def log_message(self, message, level=1):
 
490
 
        """Log message formatted with timestamp"""
 
491
 
        if level < self.log_level:
 
493
 
        timestamp = datetime.datetime.now().isoformat()
 
494
 
        self.log_message_raw("{}: {}".format(timestamp, message),
 
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
    def add_log_line(self, markup):
 
501
501
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
502
502
        if self.max_log_length:
 
503
503
            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),
 
 
504
                del self.log[0:(len(self.log) - self.max_log_length)]
 
 
505
        self.logbox.set_focus(len(self.logbox.body.contents)-1,
 
506
506
                              coming_from="above")
 
 
592
593
            mandos_clients = (self.mandos_serv
 
593
594
                              .GetAllClientsWithProperties())
 
594
595
            if not mandos_clients:
 
595
 
                self.log_message_raw(("bold",
 
596
 
                                      "Note: Server has no clients."))
 
 
596
                log.warning("Note: Server has no clients.")
 
597
597
        except dbus.exceptions.DBusException:
 
598
 
            self.log_message_raw(("bold",
 
599
 
                                  "Note: No Mandos server running."))
 
 
598
            log.warning("Note: No Mandos server running.")
 
600
599
            mandos_clients = dbus.Dictionary()
 
602
601
        (self.mandos_serv
 
 
622
621
                proxy_object=client_proxy_object,
 
623
622
                properties=client,
 
624
623
                update_hook=self.refresh,
 
625
 
                delete_hook=self.remove_client,
 
626
 
                logger=self.log_message),
 
 
624
                delete_hook=self.remove_client),
 
630
 
        self._input_callback_tag = (GLib.io_add_watch
 
 
628
        self._input_callback_tag = (
 
 
630
                GLib.IOChannel.unix_new(sys.stdin.fileno()),
 
 
631
                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
 
 
632
                glib_safely(self.process_input)))
 
634
633
        self.main_loop.run()
 
635
634
        # Main loop has finished, we should close everything now
 
636
635
        GLib.source_remove(self._input_callback_tag)
 
 
636
        with warnings.catch_warnings():
 
 
637
            warnings.simplefilter("ignore", BytesWarning)
 
640
641
        self.main_loop.quit()
 
 
642
        log.removeHandler(self.loghandler)
 
 
643
        log.propagate = self.orig_log_propagate
 
642
645
    def process_input(self, source, condition):
 
643
646
        keys = self.screen.get_input()
 
 
704
706
                    self.topwidget.set_focus(self.logbox)
 
707
 
                if self.log_level == 0:
 
709
 
                    self.log_message("Verbose mode: Off")
 
 
709
                if log.level < logging.INFO:
 
 
710
                    log.setLevel(logging.INFO)
 
 
711
                    log.info("Verbose mode: Off")
 
712
 
                    self.log_message("Verbose mode: On")
 
 
713
                    log.setLevel(logging.NOTSET)
 
 
714
                    log.info("Verbose mode: On")
 
713
715
            # elif (key == "end" or key == "meta >" or key == "G"
 
714
716
            #       or key == ">"):
 
715
717
            #     pass            # xxx end-of-buffer
 
 
 
739
class UILogHandler(logging.Handler):
 
 
740
    def __init__(self, ui, *args, **kwargs):
 
 
742
        super(UILogHandler, self).__init__(*args, **kwargs)
 
 
744
            logging.Formatter("%(asctime)s: %(message)s"))
 
 
745
    def emit(self, record):
 
 
746
        msg = self.format(record)
 
 
747
        if record.levelno > logging.INFO:
 
 
749
        self.ui.add_log_line(msg)
 
737
752
ui = UserInterface()
 
740
755
except KeyboardInterrupt:
 
742
 
except Exception as e:
 
743
 
    ui.log_message(str(e))
 
 
756
    with warnings.catch_warnings():
 
 
757
        warnings.filterwarnings("ignore", "", BytesWarning)
 
 
760
    with warnings.catch_warnings():
 
 
761
        warnings.filterwarnings("ignore", "", BytesWarning)