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