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