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
27
from __future__ import (division, absolute_import, print_function,
48
49
if sys.version_info.major == 2:
51
locale.setlocale(locale.LC_ALL, '')
54
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
53
log = logging.getLogger(os.path.basename(sys.argv[0]))
54
logging.basicConfig(level="NOTSET", # Show all messages
55
format="%(message)s") # Show basic log messages
57
logging.captureWarnings(True) # Show warnings via the logging system
59
locale.setlocale(locale.LC_ALL, "")
61
logging.getLogger("dbus.proxies").setLevel(logging.CRITICAL)
56
63
# Some useful constants
57
domain = 'se.recompile'
58
server_interface = domain + '.Mandos'
59
client_interface = domain + '.Mandos.Client'
64
domain = "se.recompile"
65
server_interface = domain + ".Mandos"
66
client_interface = domain + ".Mandos.Client"
63
70
dbus.OBJECT_MANAGER_IFACE
64
71
except AttributeError:
65
72
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
67
75
def isoformat_to_datetime(iso):
68
76
"Parse an ISO 8601 date string to a datetime.datetime()"
80
int(second), # Whole seconds
81
int(fraction*1000000)) # Microseconds
83
class MandosClientPropertyCache(object):
88
int(second), # Whole seconds
89
int(fraction*1000000)) # Microseconds
92
class MandosClientPropertyCache:
84
93
"""This wraps a Mandos Client D-Bus proxy object, caches the
85
94
properties and calls a hook function when any of them are
88
97
def __init__(self, proxy_object=None, properties=None, **kwargs):
89
self.proxy = proxy_object # Mandos Client proxy object
98
self.proxy = proxy_object # Mandos Client proxy object
90
99
self.properties = dict() if properties is None else properties
91
100
self.property_changed_match = (
92
101
self.proxy.connect_to_signal("PropertiesChanged",
93
102
self.properties_changed,
94
103
dbus.PROPERTIES_IFACE,
95
104
byte_arrays=True))
97
106
if properties is None:
98
self.properties.update(
99
self.proxy.GetAll(client_interface,
101
= dbus.PROPERTIES_IFACE))
107
self.properties.update(self.proxy.GetAll(
109
dbus_interface=dbus.PROPERTIES_IFACE))
103
111
super(MandosClientPropertyCache, self).__init__(**kwargs)
105
113
def properties_changed(self, interface, properties, invalidated):
106
114
"""This is called whenever we get a PropertiesChanged signal
107
115
It updates the changed properties in the "properties" dict.
161
167
client_interface,
162
168
byte_arrays=True))
163
self.logger('Created client {}'
164
.format(self.properties["Name"]), level=0)
169
log.debug("Created client %s", self.properties["Name"])
166
171
def using_timer(self, flag):
167
172
"""Call this method with True or False when timer should be
168
173
activated or deactivated.
170
175
if flag and self._update_timer_callback_tag is None:
171
176
# Will update the shown timer value every second
172
self._update_timer_callback_tag = (GLib.timeout_add
177
self._update_timer_callback_tag = (
178
GLib.timeout_add(1000,
179
glib_safely(self.update_timer)))
175
180
elif not (flag or self._update_timer_callback_tag is None):
176
181
GLib.source_remove(self._update_timer_callback_tag)
177
182
self._update_timer_callback_tag = None
179
184
def checker_completed(self, exitstatus, condition, command):
180
185
if exitstatus == 0:
181
self.logger('Checker for client {} (command "{}")'
182
' succeeded'.format(self.properties["Name"],
186
log.debug('Checker for client %s (command "%s")'
187
" succeeded", self.properties["Name"], command)
187
191
if os.WIFEXITED(condition):
188
self.logger('Checker for client {} (command "{}") failed'
190
.format(self.properties["Name"], command,
191
os.WEXITSTATUS(condition)))
192
log.info('Checker for client %s (command "%s") failed'
193
" with exit code %d", self.properties["Name"],
194
command, os.WEXITSTATUS(condition))
192
195
elif os.WIFSIGNALED(condition):
193
self.logger('Checker for client {} (command "{}") was'
194
' killed by signal {}'
195
.format(self.properties["Name"], command,
196
os.WTERMSIG(condition)))
196
log.info('Checker for client %s (command "%s") was'
197
" killed by signal %d", self.properties["Name"],
198
command, os.WTERMSIG(condition))
199
201
def checker_started(self, command):
200
202
"""Server signals that a checker started."""
201
self.logger('Client {} started checker "{}"'
202
.format(self.properties["Name"],
203
log.debug('Client %s started checker "%s"',
204
self.properties["Name"], command)
205
206
def got_secret(self):
206
self.logger('Client {} received its secret'
207
.format(self.properties["Name"]))
207
log.info("Client %s received its secret",
208
self.properties["Name"])
209
210
def need_approval(self, timeout, default):
211
message = 'Client {} needs approval within {} seconds'
212
message = "Client %s needs approval within %f seconds"
213
message = 'Client {} will get its secret in {} seconds'
214
self.logger(message.format(self.properties["Name"],
214
message = "Client %s will get its secret in %f seconds"
215
log.info(message, self.properties["Name"], timeout/1000)
217
217
def rejected(self, reason):
218
self.logger('Client {} was rejected; reason: {}'
219
.format(self.properties["Name"], reason))
218
log.info("Client %s was rejected; reason: %s",
219
self.properties["Name"], reason)
221
221
def selectable(self):
222
222
"""Make this a "selectable" widget.
223
223
This overrides the method from urwid.FlowWidget."""
226
226
def rows(self, maxcolrow, focus=False):
227
227
"""How many rows this widget will occupy might depend on
228
228
whether we have focus or not.
229
229
This overrides the method from urwid.FlowWidget"""
230
230
return self.current_widget(focus).rows(maxcolrow, focus=focus)
232
232
def current_widget(self, focus=False):
233
233
if focus or self.opened:
234
234
return self._focus_widget
235
235
return self._widget
237
237
def update(self):
238
238
"Called when what is visible on the screen should be updated."
239
239
# How to add standout mode to a style
240
with_standout = { "normal": "standout",
241
"bold": "bold-standout",
243
"underline-blink-standout",
244
"bold-underline-blink":
245
"bold-underline-blink-standout",
240
with_standout = {"normal": "standout",
241
"bold": "bold-standout",
243
"underline-blink-standout",
244
"bold-underline-blink":
245
"bold-underline-blink-standout",
248
248
# Rebuild focus and non-focus widgets using current properties
250
250
# Base part of a client. Name!
251
base = '{name}: '.format(name=self.properties["Name"])
251
base = "{name}: ".format(name=self.properties["Name"])
252
252
if not self.properties["Enabled"]:
253
253
message = "DISABLED"
254
254
self.using_timer(False)
255
255
elif self.properties["ApprovalPending"]:
256
timeout = datetime.timedelta(milliseconds
256
timeout = datetime.timedelta(
257
milliseconds=self.properties["ApprovalDelay"])
259
258
last_approval_request = isoformat_to_datetime(
260
259
self.properties["LastApprovalRequest"])
261
260
if last_approval_request is not None:
322
321
if self.delete_hook is not None:
323
322
self.delete_hook(self)
324
323
return super(MandosClientWidget, self).delete(**kwargs)
326
325
def render(self, maxcolrow, focus=False):
327
326
"""Render differently if we have focus.
328
327
This overrides the method from urwid.FlowWidget"""
329
328
return self.current_widget(focus).render(maxcolrow,
332
331
def keypress(self, maxcolrow, key):
334
333
This overrides the method from urwid.FlowWidget"""
336
335
self.proxy.Set(client_interface, "Enabled",
337
dbus.Boolean(True), ignore_reply = True,
338
dbus_interface = dbus.PROPERTIES_IFACE)
336
dbus.Boolean(True), ignore_reply=True,
337
dbus_interface=dbus.PROPERTIES_IFACE)
340
339
self.proxy.Set(client_interface, "Enabled", False,
342
dbus_interface = dbus.PROPERTIES_IFACE)
341
dbus_interface=dbus.PROPERTIES_IFACE)
344
343
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
345
dbus_interface = client_interface,
344
dbus_interface=client_interface,
346
345
ignore_reply=True)
348
347
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
349
dbus_interface = client_interface,
348
dbus_interface=client_interface,
350
349
ignore_reply=True)
351
350
elif key == "R" or key == "_" or key == "ctrl k":
352
351
self.server_proxy_object.RemoveClient(self.proxy
354
353
ignore_reply=True)
356
355
self.proxy.Set(client_interface, "CheckerRunning",
357
dbus.Boolean(True), ignore_reply = True,
358
dbus_interface = dbus.PROPERTIES_IFACE)
356
dbus.Boolean(True), ignore_reply=True,
357
dbus_interface=dbus.PROPERTIES_IFACE)
360
359
self.proxy.Set(client_interface, "CheckerRunning",
361
dbus.Boolean(False), ignore_reply = True,
362
dbus_interface = dbus.PROPERTIES_IFACE)
360
dbus.Boolean(False), ignore_reply=True,
361
dbus_interface=dbus.PROPERTIES_IFACE)
364
self.proxy.CheckedOK(dbus_interface = client_interface,
363
self.proxy.CheckedOK(dbus_interface=client_interface,
365
364
ignore_reply=True)
367
366
# elif key == "p" or key == "=":
387
def glib_safely(func, retval=True):
388
def safe_func(*args, **kwargs):
390
return func(*args, **kwargs)
388
397
class ConstrainedListBox(urwid.ListBox):
389
398
"""Like a normal urwid.ListBox, but will consume all "up" or
390
399
"down" key presses, thus not allowing any containing widgets to
391
400
use them as an excuse to shift focus away from this widget.
393
402
def keypress(self, *args, **kwargs):
394
ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
403
ret = (super(ConstrainedListBox, self)
404
.keypress(*args, **kwargs))
395
405
if ret in ("up", "down"):
400
class UserInterface(object):
401
411
"""This is the entire user interface - the whole screen
402
412
with boxes, lists of client widgets, etc.
404
def __init__(self, max_log_length=1000, log_level=1):
414
def __init__(self, max_log_length=1000):
405
415
DBusGMainLoop(set_as_default=True)
407
417
self.screen = urwid.curses_display.Screen()
409
419
self.screen.register_palette((
411
421
"default", "default", None),
426
437
"bold,underline,blink,standout", "default",
427
438
"bold,underline,blink,standout"),
430
441
if urwid.supports_unicode():
431
self.divider = "─" # \u2500
432
#self.divider = "━" # \u2501
442
self.divider = "─" # \u2500
434
#self.divider = "-" # \u002d
435
self.divider = "_" # \u005f
444
self.divider = "_" # \u005f
437
446
self.screen.start()
439
448
self.size = self.screen.get_cols_rows()
441
450
self.clients = urwid.SimpleListWalker([])
442
451
self.clients_dict = {}
444
453
# We will add Text widgets to this list
454
self.log = urwid.SimpleListWalker([])
446
455
self.max_log_length = max_log_length
448
self.log_level = log_level
450
457
# We keep a reference to the log widget so we can remove it
451
458
# from the ListWalker without it getting destroyed
452
459
self.logbox = ConstrainedListBox(self.log)
454
461
# This keeps track of whether self.uilist currently has
455
462
# self.logbox in it or not
456
463
self.log_visible = True
457
464
self.log_wrap = "any"
466
self.loghandler = UILogHandler(self)
460
self.log_message_raw(("bold",
461
"Mandos Monitor version " + version))
462
self.log_message_raw(("bold",
465
self.busname = domain + '.Mandos'
469
self.add_log_line(("bold",
470
"Mandos Monitor version " + version))
471
self.add_log_line(("bold", "q: Quit ?: Help"))
473
self.busname = domain + ".Mandos"
466
474
self.main_loop = GLib.MainLoop()
468
def client_not_found(self, fingerprint, address):
469
self.log_message("Client with address {} and fingerprint {}"
470
" could not be found"
471
.format(address, fingerprint))
476
def client_not_found(self, key_id, address):
477
log.info("Client with address %s and key ID %s could"
478
" not be found", address, key_id)
473
480
def rebuild(self):
474
481
"""This rebuilds the User Interface.
475
482
Call this when the widget layout needs to change"""
477
#self.uilist.append(urwid.ListBox(self.clients))
484
# self.uilist.append(urwid.ListBox(self.clients))
478
485
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
480
#header=urwid.Divider(),
487
# header=urwid.Divider(),
483
urwid.Divider(div_char=
489
footer=urwid.Divider(
490
div_char=self.divider)))
485
491
if self.log_visible:
486
492
self.uilist.append(self.logbox)
487
493
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:
495
def add_log_line(self, markup):
501
496
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
502
if (self.max_log_length
503
and 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),
497
if self.max_log_length:
498
if len(self.log) > self.max_log_length:
499
del self.log[0:(len(self.log) - self.max_log_length)]
500
self.logbox.set_focus(len(self.logbox.body.contents)-1,
506
501
coming_from="above")
509
504
def toggle_log_display(self):
510
505
"""Toggle visibility of the log buffer."""
511
506
self.log_visible = not self.log_visible
513
self.log_message("Log visibility changed to: {}"
514
.format(self.log_visible), level=0)
508
log.debug("Log visibility changed to: %s", self.log_visible)
516
510
def change_log_display(self):
517
511
"""Change type of log display.
518
512
Currently, this toggles wrapping of text lines."""
574
559
self.clients_dict[path] = client
575
560
self.clients.sort(key=lambda c: c.properties["Name"])
578
563
def remove_client(self, client, path=None):
579
564
self.clients.remove(client)
581
566
path = client.proxy.object_path
582
567
del self.clients_dict[path]
585
570
def refresh(self):
586
571
"""Redraw the screen"""
587
572
canvas = self.topwidget.render(self.size, focus=True)
588
573
self.screen.draw_screen(self.size, canvas)
591
576
"""Start the main loop and exit when it's done."""
577
log.addHandler(self.loghandler)
578
self.orig_log_propagate = log.propagate
579
log.propagate = False
580
self.orig_log_level = log.level
592
582
self.bus = dbus.SystemBus()
593
583
mandos_dbus_objc = self.bus.get_object(
594
584
self.busname, "/", follow_name_owner_changes=True)
595
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
585
self.mandos_serv = dbus.Interface(
586
mandos_dbus_objc, dbus_interface=server_interface)
599
588
mandos_clients = (self.mandos_serv
600
589
.GetAllClientsWithProperties())
601
590
if not mandos_clients:
602
self.log_message_raw(("bold", "Note: Server has no clients."))
591
log.warning("Note: Server has no clients.")
603
592
except dbus.exceptions.DBusException:
604
self.log_message_raw(("bold", "Note: No Mandos server running."))
593
log.warning("Note: No Mandos server running.")
605
594
mandos_clients = dbus.Dictionary()
607
596
(self.mandos_serv
608
597
.connect_to_signal("InterfacesRemoved",
609
598
self.find_and_remove_client,
611
= dbus.OBJECT_MANAGER_IFACE,
599
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
612
600
byte_arrays=True))
613
601
(self.mandos_serv
614
602
.connect_to_signal("InterfacesAdded",
615
603
self.add_new_client,
617
= dbus.OBJECT_MANAGER_IFACE,
604
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
618
605
byte_arrays=True))
619
606
(self.mandos_serv
620
607
.connect_to_signal("ClientNotFound",
624
611
for path, client in mandos_clients.items():
625
612
client_proxy_object = self.bus.get_object(self.busname,
627
self.add_client(MandosClientWidget(server_proxy_object
630
=client_proxy_object,
614
self.add_client(MandosClientWidget(
615
server_proxy_object=self.mandos_serv,
616
proxy_object=client_proxy_object,
618
update_hook=self.refresh,
619
delete_hook=self.remove_client),
641
self._input_callback_tag = (GLib.io_add_watch
623
self._input_callback_tag = (
625
GLib.IOChannel.unix_new(sys.stdin.fileno()),
626
GLib.PRIORITY_DEFAULT, GLib.IO_IN,
627
glib_safely(self.process_input)))
645
628
self.main_loop.run()
646
629
# Main loop has finished, we should close everything now
647
630
GLib.source_remove(self._input_callback_tag)
631
with warnings.catch_warnings():
632
warnings.simplefilter("ignore", BytesWarning)
651
636
self.main_loop.quit()
637
log.removeHandler(self.loghandler)
638
log.propagate = self.orig_log_propagate
653
640
def process_input(self, source, condition):
654
641
keys = self.screen.get_input()
655
translations = { "ctrl n": "down", # Emacs
656
"ctrl p": "up", # Emacs
657
"ctrl v": "page down", # Emacs
658
"meta v": "page up", # Emacs
659
" ": "page down", # less
660
"f": "page down", # less
661
"b": "page up", # less
642
translations = {"ctrl n": "down", # Emacs
643
"ctrl p": "up", # Emacs
644
"ctrl v": "page down", # Emacs
645
"meta v": "page up", # Emacs
646
" ": "page down", # less
647
"f": "page down", # less
648
"b": "page up", # less
667
654
key = translations[key]
668
655
except KeyError: # :-)
671
658
if key == "q" or key == "Q":
715
701
self.topwidget.set_focus(self.logbox)
718
if self.log_level == 0:
720
self.log_message("Verbose mode: Off")
704
if log.level < logging.INFO:
705
log.setLevel(logging.INFO)
706
log.info("Verbose mode: Off")
723
self.log_message("Verbose mode: On")
724
#elif (key == "end" or key == "meta >" or key == "G"
726
# pass # xxx end-of-buffer
727
#elif (key == "home" or key == "meta <" or key == "g"
729
# pass # xxx beginning-of-buffer
730
#elif key == "ctrl e" or key == "$":
731
# pass # xxx move-end-of-line
732
#elif key == "ctrl a" or key == "^":
733
# pass # xxx move-beginning-of-line
734
#elif key == "ctrl b" or key == "meta (" or key == "h":
736
#elif key == "ctrl f" or key == "meta )" or key == "l":
739
# pass # scroll up log
741
# pass # scroll down log
708
log.setLevel(logging.NOTSET)
709
log.info("Verbose mode: On")
710
# elif (key == "end" or key == "meta >" or key == "G"
712
# pass # xxx end-of-buffer
713
# elif (key == "home" or key == "meta <" or key == "g"
715
# pass # xxx beginning-of-buffer
716
# elif key == "ctrl e" or key == "$":
717
# pass # xxx move-end-of-line
718
# elif key == "ctrl a" or key == "^":
719
# pass # xxx move-beginning-of-line
720
# elif key == "ctrl b" or key == "meta (" or key == "h":
722
# elif key == "ctrl f" or key == "meta )" or key == "l":
725
# pass # scroll up log
727
# pass # scroll down log
742
728
elif self.topwidget.selectable():
743
729
self.topwidget.keypress(self.size, key)
734
class UILogHandler(logging.Handler):
735
def __init__(self, ui, *args, **kwargs):
737
super(UILogHandler, self).__init__(*args, **kwargs)
739
logging.Formatter("%(asctime)s: %(message)s"))
740
def emit(self, record):
741
msg = self.format(record)
742
if record.levelno > logging.INFO:
744
self.ui.add_log_line(msg)
747
747
ui = UserInterface()
750
750
except KeyboardInterrupt:
752
except Exception as e:
753
ui.log_message(str(e))
751
with warnings.catch_warnings():
752
warnings.filterwarnings("ignore", "", BytesWarning)
755
with warnings.catch_warnings():
756
warnings.filterwarnings("ignore", "", BytesWarning)