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
51
if sys.version_info.major == 2:
51
54
locale.setlocale(locale.LC_ALL, '')
54
56
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
56
58
# Some useful constants
57
59
domain = 'se.recompile'
58
60
server_interface = domain + '.Mandos'
59
61
client_interface = domain + '.Mandos.Client'
63
65
dbus.OBJECT_MANAGER_IFACE
64
66
except AttributeError:
65
67
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
67
70
def isoformat_to_datetime(iso):
68
71
"Parse an ISO 8601 date string to a datetime.datetime()"
80
int(second), # Whole seconds
81
int(fraction*1000000)) # Microseconds
83
int(second), # Whole seconds
84
int(fraction*1000000)) # Microseconds
83
87
class MandosClientPropertyCache(object):
84
88
"""This wraps a Mandos Client D-Bus proxy object, caches the
88
92
def __init__(self, proxy_object=None, properties=None, **kwargs):
89
self.proxy = proxy_object # Mandos Client proxy object
93
self.proxy = proxy_object # Mandos Client proxy object
90
94
self.properties = dict() if properties is None else properties
91
95
self.property_changed_match = (
92
96
self.proxy.connect_to_signal("PropertiesChanged",
93
97
self.properties_changed,
94
98
dbus.PROPERTIES_IFACE,
97
101
if properties is None:
98
self.properties.update(
99
self.proxy.GetAll(client_interface,
101
= dbus.PROPERTIES_IFACE))
102
self.properties.update(self.proxy.GetAll(
104
dbus_interface=dbus.PROPERTIES_IFACE))
103
106
super(MandosClientPropertyCache, self).__init__(**kwargs)
105
108
def properties_changed(self, interface, properties, invalidated):
106
109
"""This is called whenever we get a PropertiesChanged signal
107
110
It updates the changed properties in the "properties" dict.
117
120
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
118
121
"""A Mandos Client which is visible on the screen.
121
124
def __init__(self, server_proxy_object=None, update_hook=None,
122
125
delete_hook=None, logger=None, **kwargs):
123
126
# Called on update
175
178
elif not (flag or self._update_timer_callback_tag is None):
176
179
GLib.source_remove(self._update_timer_callback_tag)
177
180
self._update_timer_callback_tag = None
179
182
def checker_completed(self, exitstatus, condition, command):
180
183
if exitstatus == 0:
181
184
self.logger('Checker for client {} (command "{}")'
195
198
.format(self.properties["Name"], command,
196
199
os.WTERMSIG(condition)))
199
202
def checker_started(self, command):
200
203
"""Server signals that a checker started."""
201
204
self.logger('Client {} started checker "{}"'
202
205
.format(self.properties["Name"],
203
206
command), level=0)
205
208
def got_secret(self):
206
209
self.logger('Client {} received its secret'
207
210
.format(self.properties["Name"]))
209
212
def need_approval(self, timeout, default):
211
214
message = 'Client {} needs approval within {} seconds'
213
216
message = 'Client {} will get its secret in {} seconds'
214
217
self.logger(message.format(self.properties["Name"],
217
220
def rejected(self, reason):
218
221
self.logger('Client {} was rejected; reason: {}'
219
222
.format(self.properties["Name"], reason))
221
224
def selectable(self):
222
225
"""Make this a "selectable" widget.
223
226
This overrides the method from urwid.FlowWidget."""
226
229
def rows(self, maxcolrow, focus=False):
227
230
"""How many rows this widget will occupy might depend on
228
231
whether we have focus or not.
229
232
This overrides the method from urwid.FlowWidget"""
230
233
return self.current_widget(focus).rows(maxcolrow, focus=focus)
232
235
def current_widget(self, focus=False):
233
236
if focus or self.opened:
234
237
return self._focus_widget
235
238
return self._widget
237
240
def update(self):
238
241
"Called when what is visible on the screen should be updated."
239
242
# 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",
243
with_standout = {"normal": "standout",
244
"bold": "bold-standout",
246
"underline-blink-standout",
247
"bold-underline-blink":
248
"bold-underline-blink-standout",
248
251
# Rebuild focus and non-focus widgets using current properties
250
253
# Base part of a client. Name!
251
254
base = '{name}: '.format(name=self.properties["Name"])
252
255
if not self.properties["Enabled"]:
253
256
message = "DISABLED"
254
257
self.using_timer(False)
255
258
elif self.properties["ApprovalPending"]:
256
timeout = datetime.timedelta(milliseconds
259
timeout = datetime.timedelta(
260
milliseconds=self.properties["ApprovalDelay"])
259
261
last_approval_request = isoformat_to_datetime(
260
262
self.properties["LastApprovalRequest"])
261
263
if last_approval_request is not None:
304
306
# Run update hook, if any
305
307
if self.update_hook is not None:
306
308
self.update_hook()
308
310
def update_timer(self):
309
311
"""called by GLib. Will indefinitely loop until
310
312
GLib.source_remove() on tag is called
313
315
return True # Keep calling this
315
317
def delete(self, **kwargs):
316
318
if self._update_timer_callback_tag is not None:
317
319
GLib.source_remove(self._update_timer_callback_tag)
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 == "=":
376
378
def properties_changed(self, interface, properties, invalidated):
377
379
"""Call self.update() if any properties changed.
378
380
This overrides the method from MandosClientPropertyCache"""
379
old_values = { key: self.properties.get(key)
380
for key in properties.keys() }
381
old_values = {key: self.properties.get(key)
382
for key in properties.keys()}
381
383
super(MandosClientWidget, self).properties_changed(
382
384
interface, properties, invalidated)
383
385
if any(old_values[key] != self.properties.get(key)
391
393
use them as an excuse to shift focus away from this widget.
393
395
def keypress(self, *args, **kwargs):
394
ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
396
ret = (super(ConstrainedListBox, self)
397
.keypress(*args, **kwargs))
395
398
if ret in ("up", "down"):
404
407
def __init__(self, max_log_length=1000, log_level=1):
405
408
DBusGMainLoop(set_as_default=True)
407
410
self.screen = urwid.curses_display.Screen()
409
412
self.screen.register_palette((
411
414
"default", "default", None),
417
420
"standout", "default", "standout"),
418
421
("bold-underline-blink",
419
"bold,underline,blink", "default", "bold,underline,blink"),
422
"bold,underline,blink", "default",
423
"bold,underline,blink"),
420
424
("bold-standout",
421
425
"bold,standout", "default", "bold,standout"),
422
426
("underline-blink-standout",
426
430
"bold,underline,blink,standout", "default",
427
431
"bold,underline,blink,standout"),
430
434
if urwid.supports_unicode():
431
self.divider = "─" # \u2500
432
#self.divider = "━" # \u2501
435
self.divider = "─" # \u2500
434
#self.divider = "-" # \u002d
435
self.divider = "_" # \u005f
437
self.divider = "_" # \u005f
437
439
self.screen.start()
439
441
self.size = self.screen.get_cols_rows()
441
443
self.clients = urwid.SimpleListWalker([])
442
444
self.clients_dict = {}
444
446
# We will add Text widgets to this list
447
self.log = urwid.SimpleListWalker([])
446
448
self.max_log_length = max_log_length
448
450
self.log_level = log_level
450
452
# We keep a reference to the log widget so we can remove it
451
453
# from the ListWalker without it getting destroyed
452
454
self.logbox = ConstrainedListBox(self.log)
454
456
# This keeps track of whether self.uilist currently has
455
457
# self.logbox in it or not
456
458
self.log_visible = True
457
459
self.log_wrap = "any"
460
462
self.log_message_raw(("bold",
461
463
"Mandos Monitor version " + version))
462
464
self.log_message_raw(("bold",
463
465
"q: Quit ?: Help"))
465
467
self.busname = domain + '.Mandos'
466
468
self.main_loop = GLib.MainLoop()
468
470
def client_not_found(self, fingerprint, address):
469
471
self.log_message("Client with address {} and fingerprint {}"
470
472
" could not be found"
471
473
.format(address, fingerprint))
473
475
def rebuild(self):
474
476
"""This rebuilds the User Interface.
475
477
Call this when the widget layout needs to change"""
477
#self.uilist.append(urwid.ListBox(self.clients))
479
# self.uilist.append(urwid.ListBox(self.clients))
478
480
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
480
#header=urwid.Divider(),
482
# header=urwid.Divider(),
483
urwid.Divider(div_char=
484
footer=urwid.Divider(
485
div_char=self.divider)))
485
486
if self.log_visible:
486
487
self.uilist.append(self.logbox)
487
488
self.topwidget = urwid.Pile(self.uilist)
489
490
def log_message(self, message, level=1):
490
491
"""Log message formatted with timestamp"""
491
492
if level < self.log_level:
493
494
timestamp = datetime.datetime.now().isoformat()
494
495
self.log_message_raw("{}: {}".format(timestamp, message),
497
498
def log_message_raw(self, markup, level=1):
498
499
"""Add a log message to the log buffer."""
499
500
if level < self.log_level:
501
502
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),
503
if self.max_log_length:
504
if len(self.log) > self.max_log_length:
505
del self.log[0:len(self.log)-self.max_log_length-1]
506
self.logbox.set_focus(len(self.logbox.body.contents)-1,
506
507
coming_from="above")
509
510
def toggle_log_display(self):
510
511
"""Toggle visibility of the log buffer."""
511
512
self.log_visible = not self.log_visible
513
514
self.log_message("Log visibility changed to: {}"
514
515
.format(self.log_visible), level=0)
516
517
def change_log_display(self):
517
518
"""Change type of log display.
518
519
Currently, this toggles wrapping of text lines."""
524
525
textwidget.set_wrap_mode(self.log_wrap)
525
526
self.log_message("Wrap mode: {}".format(self.log_wrap),
528
529
def find_and_remove_client(self, path, interfaces):
529
530
"""Find a client by its object path and remove it.
531
532
This is connected to the InterfacesRemoved signal from the
532
533
Mandos server object."""
533
534
if client_interface not in interfaces:
552
553
# Not a Mandos client object; ignore
554
555
client_proxy_object = self.bus.get_object(self.busname, path)
555
self.add_client(MandosClientWidget(server_proxy_object
558
=client_proxy_object,
566
= dict(ifs_and_props[
556
self.add_client(MandosClientWidget(
557
server_proxy_object=self.mandos_serv,
558
proxy_object=client_proxy_object,
559
update_hook=self.refresh,
560
delete_hook=self.remove_client,
561
logger=self.log_message,
562
properties=dict(ifs_and_props[client_interface])),
570
565
def add_client(self, client, path=None):
571
566
self.clients.append(client)
574
569
self.clients_dict[path] = client
575
570
self.clients.sort(key=lambda c: c.properties["Name"])
578
573
def remove_client(self, client, path=None):
579
574
self.clients.remove(client)
581
576
path = client.proxy.object_path
582
577
del self.clients_dict[path]
585
580
def refresh(self):
586
581
"""Redraw the screen"""
587
582
canvas = self.topwidget.render(self.size, focus=True)
588
583
self.screen.draw_screen(self.size, canvas)
591
586
"""Start the main loop and exit when it's done."""
592
587
self.bus = dbus.SystemBus()
593
588
mandos_dbus_objc = self.bus.get_object(
594
589
self.busname, "/", follow_name_owner_changes=True)
595
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
590
self.mandos_serv = dbus.Interface(
591
mandos_dbus_objc, dbus_interface=server_interface)
599
593
mandos_clients = (self.mandos_serv
600
594
.GetAllClientsWithProperties())
601
595
if not mandos_clients:
602
self.log_message_raw(("bold", "Note: Server has no clients."))
596
self.log_message_raw(("bold",
597
"Note: Server has no clients."))
603
598
except dbus.exceptions.DBusException:
604
self.log_message_raw(("bold", "Note: No Mandos server running."))
599
self.log_message_raw(("bold",
600
"Note: No Mandos server running."))
605
601
mandos_clients = dbus.Dictionary()
607
603
(self.mandos_serv
608
604
.connect_to_signal("InterfacesRemoved",
609
605
self.find_and_remove_client,
611
= dbus.OBJECT_MANAGER_IFACE,
606
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
612
607
byte_arrays=True))
613
608
(self.mandos_serv
614
609
.connect_to_signal("InterfacesAdded",
615
610
self.add_new_client,
617
= dbus.OBJECT_MANAGER_IFACE,
611
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
618
612
byte_arrays=True))
619
613
(self.mandos_serv
620
614
.connect_to_signal("ClientNotFound",
624
618
for path, client in mandos_clients.items():
625
619
client_proxy_object = self.bus.get_object(self.busname,
627
self.add_client(MandosClientWidget(server_proxy_object
630
=client_proxy_object,
621
self.add_client(MandosClientWidget(
622
server_proxy_object=self.mandos_serv,
623
proxy_object=client_proxy_object,
625
update_hook=self.refresh,
626
delete_hook=self.remove_client,
627
logger=self.log_message),
641
self._input_callback_tag = (GLib.io_add_watch
631
self._input_callback_tag = (
633
GLib.IOChannel.unix_new(sys.stdin.fileno()),
634
GLib.PRIORITY_DEFAULT, GLib.IO_IN,
645
636
self.main_loop.run()
646
637
# Main loop has finished, we should close everything now
647
638
GLib.source_remove(self._input_callback_tag)
648
639
self.screen.stop()
651
642
self.main_loop.quit()
653
644
def process_input(self, source, condition):
654
645
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
646
translations = {"ctrl n": "down", # Emacs
647
"ctrl p": "up", # Emacs
648
"ctrl v": "page down", # Emacs
649
"meta v": "page up", # Emacs
650
" ": "page down", # less
651
"f": "page down", # less
652
"b": "page up", # less
667
658
key = translations[key]
668
659
except KeyError: # :-)
671
662
if key == "q" or key == "Q":
722
713
self.log_level = 0
723
714
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
715
# elif (key == "end" or key == "meta >" or key == "G"
717
# pass # xxx end-of-buffer
718
# elif (key == "home" or key == "meta <" or key == "g"
720
# pass # xxx beginning-of-buffer
721
# elif key == "ctrl e" or key == "$":
722
# pass # xxx move-end-of-line
723
# elif key == "ctrl a" or key == "^":
724
# pass # xxx move-beginning-of-line
725
# elif key == "ctrl b" or key == "meta (" or key == "h":
727
# elif key == "ctrl f" or key == "meta )" or key == "l":
730
# pass # scroll up log
732
# pass # scroll down log
742
733
elif self.topwidget.selectable():
743
734
self.topwidget.keypress(self.size, key)
747
739
ui = UserInterface()