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-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
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
13
11
# the Free Software Foundation, either version 3 of the License, or
14
12
# (at your option) any later version.
16
# Mandos is distributed in the hope that it will be useful, but
17
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# This program is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
17
# GNU General Public License for more details.
21
19
# You should have received a copy of the GNU General Public License
22
# along with Mandos. If not, see <http://www.gnu.org/licenses/>.
20
# along with this program. If not, see
21
# <http://www.gnu.org/licenses/>.
24
23
# Contact the authors at <mandos@recompile.se>.
27
26
from __future__ import (division, absolute_import, print_function,
49
48
if sys.version_info.major == 2:
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)
51
locale.setlocale(locale.LC_ALL, '')
54
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
63
56
# Some useful constants
64
domain = "se.recompile"
65
server_interface = domain + ".Mandos"
66
client_interface = domain + ".Mandos.Client"
57
domain = 'se.recompile'
58
server_interface = domain + '.Mandos'
59
client_interface = domain + '.Mandos.Client'
70
63
dbus.OBJECT_MANAGER_IFACE
71
64
except AttributeError:
72
65
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
75
67
def isoformat_to_datetime(iso):
76
68
"Parse an ISO 8601 date string to a datetime.datetime()"
88
int(second), # Whole seconds
89
int(fraction*1000000)) # Microseconds
92
class MandosClientPropertyCache:
80
int(second), # Whole seconds
81
int(fraction*1000000)) # Microseconds
83
class MandosClientPropertyCache(object):
93
84
"""This wraps a Mandos Client D-Bus proxy object, caches the
94
85
properties and calls a hook function when any of them are
97
88
def __init__(self, proxy_object=None, properties=None, **kwargs):
98
self.proxy = proxy_object # Mandos Client proxy object
89
self.proxy = proxy_object # Mandos Client proxy object
99
90
self.properties = dict() if properties is None else properties
100
91
self.property_changed_match = (
101
92
self.proxy.connect_to_signal("PropertiesChanged",
102
93
self.properties_changed,
103
94
dbus.PROPERTIES_IFACE,
104
95
byte_arrays=True))
106
97
if properties is None:
107
self.properties.update(self.proxy.GetAll(
109
dbus_interface=dbus.PROPERTIES_IFACE))
98
self.properties.update(
99
self.proxy.GetAll(client_interface,
101
= dbus.PROPERTIES_IFACE))
111
103
super(MandosClientPropertyCache, self).__init__(**kwargs)
113
105
def properties_changed(self, interface, properties, invalidated):
114
106
"""This is called whenever we get a PropertiesChanged signal
115
107
It updates the changed properties in the "properties" dict.
167
161
client_interface,
168
162
byte_arrays=True))
169
log.debug("Created client %s", self.properties["Name"])
163
self.logger('Created client {}'
164
.format(self.properties["Name"]), level=0)
171
166
def using_timer(self, flag):
172
167
"""Call this method with True or False when timer should be
173
168
activated or deactivated.
175
170
if flag and self._update_timer_callback_tag is None:
176
171
# Will update the shown timer value every second
177
self._update_timer_callback_tag = (
178
GLib.timeout_add(1000,
179
glib_safely(self.update_timer)))
172
self._update_timer_callback_tag = (GLib.timeout_add
180
175
elif not (flag or self._update_timer_callback_tag is None):
181
176
GLib.source_remove(self._update_timer_callback_tag)
182
177
self._update_timer_callback_tag = None
184
179
def checker_completed(self, exitstatus, condition, command):
185
180
if exitstatus == 0:
186
log.debug('Checker for client %s (command "%s")'
187
" succeeded", self.properties["Name"], command)
181
self.logger('Checker for client {} (command "{}")'
182
' succeeded'.format(self.properties["Name"],
191
187
if os.WIFEXITED(condition):
192
log.info('Checker for client %s (command "%s") failed'
193
" with exit code %d", self.properties["Name"],
194
command, os.WEXITSTATUS(condition))
188
self.logger('Checker for client {} (command "{}") failed'
190
.format(self.properties["Name"], command,
191
os.WEXITSTATUS(condition)))
195
192
elif os.WIFSIGNALED(condition):
196
log.info('Checker for client %s (command "%s") was'
197
" killed by signal %d", self.properties["Name"],
198
command, os.WTERMSIG(condition))
193
self.logger('Checker for client {} (command "{}") was'
194
' killed by signal {}'
195
.format(self.properties["Name"], command,
196
os.WTERMSIG(condition)))
201
199
def checker_started(self, command):
202
200
"""Server signals that a checker started."""
203
log.debug('Client %s started checker "%s"',
204
self.properties["Name"], command)
201
self.logger('Client {} started checker "{}"'
202
.format(self.properties["Name"],
206
205
def got_secret(self):
207
log.info("Client %s received its secret",
208
self.properties["Name"])
206
self.logger('Client {} received its secret'
207
.format(self.properties["Name"]))
210
209
def need_approval(self, timeout, default):
212
message = "Client %s needs approval within %f seconds"
211
message = 'Client {} needs approval within {} seconds'
214
message = "Client %s will get its secret in %f seconds"
215
log.info(message, self.properties["Name"], timeout/1000)
213
message = 'Client {} will get its secret in {} seconds'
214
self.logger(message.format(self.properties["Name"],
217
217
def rejected(self, reason):
218
log.info("Client %s was rejected; reason: %s",
219
self.properties["Name"], reason)
218
self.logger('Client {} was rejected; reason: {}'
219
.format(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(
257
milliseconds=self.properties["ApprovalDelay"])
256
timeout = datetime.timedelta(milliseconds
258
259
last_approval_request = isoformat_to_datetime(
259
260
self.properties["LastApprovalRequest"])
260
261
if last_approval_request is not None:
321
322
if self.delete_hook is not None:
322
323
self.delete_hook(self)
323
324
return super(MandosClientWidget, self).delete(**kwargs)
325
326
def render(self, maxcolrow, focus=False):
326
327
"""Render differently if we have focus.
327
328
This overrides the method from urwid.FlowWidget"""
328
329
return self.current_widget(focus).render(maxcolrow,
331
332
def keypress(self, maxcolrow, key):
333
334
This overrides the method from urwid.FlowWidget"""
335
336
self.proxy.Set(client_interface, "Enabled",
336
dbus.Boolean(True), ignore_reply=True,
337
dbus_interface=dbus.PROPERTIES_IFACE)
337
dbus.Boolean(True), ignore_reply = True,
338
dbus_interface = dbus.PROPERTIES_IFACE)
339
340
self.proxy.Set(client_interface, "Enabled", False,
341
dbus_interface=dbus.PROPERTIES_IFACE)
342
dbus_interface = dbus.PROPERTIES_IFACE)
343
344
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
344
dbus_interface=client_interface,
345
dbus_interface = client_interface,
345
346
ignore_reply=True)
347
348
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
348
dbus_interface=client_interface,
349
dbus_interface = client_interface,
349
350
ignore_reply=True)
350
351
elif key == "R" or key == "_" or key == "ctrl k":
351
352
self.server_proxy_object.RemoveClient(self.proxy
353
354
ignore_reply=True)
355
356
self.proxy.Set(client_interface, "CheckerRunning",
356
dbus.Boolean(True), ignore_reply=True,
357
dbus_interface=dbus.PROPERTIES_IFACE)
357
dbus.Boolean(True), ignore_reply = True,
358
dbus_interface = dbus.PROPERTIES_IFACE)
359
360
self.proxy.Set(client_interface, "CheckerRunning",
360
dbus.Boolean(False), ignore_reply=True,
361
dbus_interface=dbus.PROPERTIES_IFACE)
361
dbus.Boolean(False), ignore_reply = True,
362
dbus_interface = dbus.PROPERTIES_IFACE)
363
self.proxy.CheckedOK(dbus_interface=client_interface,
364
self.proxy.CheckedOK(dbus_interface = client_interface,
364
365
ignore_reply=True)
366
367
# elif key == "p" or key == "=":
387
def glib_safely(func, retval=True):
388
def safe_func(*args, **kwargs):
390
return func(*args, **kwargs)
397
388
class ConstrainedListBox(urwid.ListBox):
398
389
"""Like a normal urwid.ListBox, but will consume all "up" or
399
390
"down" key presses, thus not allowing any containing widgets to
400
391
use them as an excuse to shift focus away from this widget.
402
393
def keypress(self, *args, **kwargs):
403
ret = (super(ConstrainedListBox, self)
404
.keypress(*args, **kwargs))
394
ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
405
395
if ret in ("up", "down"):
400
class UserInterface(object):
411
401
"""This is the entire user interface - the whole screen
412
402
with boxes, lists of client widgets, etc.
414
def __init__(self, max_log_length=1000):
404
def __init__(self, max_log_length=1000, log_level=1):
415
405
DBusGMainLoop(set_as_default=True)
417
407
self.screen = urwid.curses_display.Screen()
419
409
self.screen.register_palette((
421
411
"default", "default", None),
437
426
"bold,underline,blink,standout", "default",
438
427
"bold,underline,blink,standout"),
441
430
if urwid.supports_unicode():
442
self.divider = "─" # \u2500
431
self.divider = "─" # \u2500
432
#self.divider = "━" # \u2501
444
self.divider = "_" # \u005f
434
#self.divider = "-" # \u002d
435
self.divider = "_" # \u005f
446
437
self.screen.start()
448
439
self.size = self.screen.get_cols_rows()
450
441
self.clients = urwid.SimpleListWalker([])
451
442
self.clients_dict = {}
453
444
# We will add Text widgets to this list
454
self.log = urwid.SimpleListWalker([])
455
446
self.max_log_length = max_log_length
448
self.log_level = log_level
457
450
# We keep a reference to the log widget so we can remove it
458
451
# from the ListWalker without it getting destroyed
459
452
self.logbox = ConstrainedListBox(self.log)
461
454
# This keeps track of whether self.uilist currently has
462
455
# self.logbox in it or not
463
456
self.log_visible = True
464
457
self.log_wrap = "any"
466
self.loghandler = UILogHandler(self)
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"
460
self.log_message_raw(("bold",
461
"Mandos Monitor version " + version))
462
self.log_message_raw(("bold",
465
self.busname = domain + '.Mandos'
474
466
self.main_loop = GLib.MainLoop()
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)
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))
480
473
def rebuild(self):
481
474
"""This rebuilds the User Interface.
482
475
Call this when the widget layout needs to change"""
484
# self.uilist.append(urwid.ListBox(self.clients))
477
#self.uilist.append(urwid.ListBox(self.clients))
485
478
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
487
# header=urwid.Divider(),
480
#header=urwid.Divider(),
489
footer=urwid.Divider(
490
div_char=self.divider)))
483
urwid.Divider(div_char=
491
485
if self.log_visible:
492
486
self.uilist.append(self.logbox)
493
487
self.topwidget = urwid.Pile(self.uilist)
495
def add_log_line(self, markup):
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:
496
501
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
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,
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),
501
506
coming_from="above")
504
509
def toggle_log_display(self):
505
510
"""Toggle visibility of the log buffer."""
506
511
self.log_visible = not self.log_visible
508
log.debug("Log visibility changed to: %s", self.log_visible)
513
self.log_message("Log visibility changed to: {}"
514
.format(self.log_visible), level=0)
510
516
def change_log_display(self):
511
517
"""Change type of log display.
512
518
Currently, this toggles wrapping of text lines."""
559
574
self.clients_dict[path] = client
560
575
self.clients.sort(key=lambda c: c.properties["Name"])
563
578
def remove_client(self, client, path=None):
564
579
self.clients.remove(client)
566
581
path = client.proxy.object_path
567
582
del self.clients_dict[path]
570
585
def refresh(self):
571
586
"""Redraw the screen"""
572
587
canvas = self.topwidget.render(self.size, focus=True)
573
588
self.screen.draw_screen(self.size, canvas)
576
591
"""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
582
592
self.bus = dbus.SystemBus()
583
593
mandos_dbus_objc = self.bus.get_object(
584
594
self.busname, "/", follow_name_owner_changes=True)
585
self.mandos_serv = dbus.Interface(
586
mandos_dbus_objc, dbus_interface=server_interface)
595
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
588
599
mandos_clients = (self.mandos_serv
589
600
.GetAllClientsWithProperties())
590
601
if not mandos_clients:
591
log.warning("Note: Server has no clients.")
602
self.log_message_raw(("bold", "Note: Server has no clients."))
592
603
except dbus.exceptions.DBusException:
593
log.warning("Note: No Mandos server running.")
604
self.log_message_raw(("bold", "Note: No Mandos server running."))
594
605
mandos_clients = dbus.Dictionary()
596
607
(self.mandos_serv
597
608
.connect_to_signal("InterfacesRemoved",
598
609
self.find_and_remove_client,
599
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
611
= dbus.OBJECT_MANAGER_IFACE,
600
612
byte_arrays=True))
601
613
(self.mandos_serv
602
614
.connect_to_signal("InterfacesAdded",
603
615
self.add_new_client,
604
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
617
= dbus.OBJECT_MANAGER_IFACE,
605
618
byte_arrays=True))
606
619
(self.mandos_serv
607
620
.connect_to_signal("ClientNotFound",
611
624
for path, client in mandos_clients.items():
612
625
client_proxy_object = self.bus.get_object(self.busname,
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),
627
self.add_client(MandosClientWidget(server_proxy_object
630
=client_proxy_object,
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)))
641
self._input_callback_tag = (GLib.io_add_watch
628
645
self.main_loop.run()
629
646
# Main loop has finished, we should close everything now
630
647
GLib.source_remove(self._input_callback_tag)
631
with warnings.catch_warnings():
632
warnings.simplefilter("ignore", BytesWarning)
636
651
self.main_loop.quit()
637
log.removeHandler(self.loghandler)
638
log.propagate = self.orig_log_propagate
640
653
def process_input(self, source, condition):
641
654
keys = self.screen.get_input()
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
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
654
667
key = translations[key]
655
668
except KeyError: # :-)
658
671
if key == "q" or key == "Q":
701
715
self.topwidget.set_focus(self.logbox)
704
if log.level < logging.INFO:
705
log.setLevel(logging.INFO)
706
log.info("Verbose mode: Off")
718
if self.log_level == 0:
720
self.log_message("Verbose mode: Off")
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
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
728
742
elif self.topwidget.selectable():
729
743
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:
751
with warnings.catch_warnings():
752
warnings.filterwarnings("ignore", "", BytesWarning)
755
with warnings.catch_warnings():
756
warnings.filterwarnings("ignore", "", BytesWarning)
752
except Exception as e:
753
ui.log_message(str(e))