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-2017 Teddy Hogeborn
7
# Copyright © 2009-2017 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)