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-2015 Teddy Hogeborn
7
# Copyright © 2009-2015 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
54
locale.setlocale(locale.LC_ALL, '')
56
57
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
58
59
# Some useful constants
59
60
domain = 'se.recompile'
60
61
server_interface = domain + '.Mandos'
61
62
client_interface = domain + '.Mandos.Client'
65
dbus.OBJECT_MANAGER_IFACE
66
except AttributeError:
67
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
70
65
def isoformat_to_datetime(iso):
71
66
"Parse an ISO 8601 date string to a datetime.datetime()"
83
int(second), # Whole seconds
84
int(fraction*1000000)) # Microseconds
78
int(second), # Whole seconds
79
int(fraction*1000000)) # Microseconds
87
81
class MandosClientPropertyCache(object):
88
82
"""This wraps a Mandos Client D-Bus proxy object, caches the
92
86
def __init__(self, proxy_object=None, properties=None, **kwargs):
93
self.proxy = proxy_object # Mandos Client proxy object
87
self.proxy = proxy_object # Mandos Client proxy object
94
88
self.properties = dict() if properties is None else properties
95
89
self.property_changed_match = (
96
90
self.proxy.connect_to_signal("PropertiesChanged",
97
91
self.properties_changed,
98
92
dbus.PROPERTIES_IFACE,
101
95
if properties is None:
102
self.properties.update(self.proxy.GetAll(
104
dbus_interface=dbus.PROPERTIES_IFACE))
96
self.properties.update(
97
self.proxy.GetAll(client_interface,
99
= dbus.PROPERTIES_IFACE))
106
101
super(MandosClientPropertyCache, self).__init__(**kwargs)
108
103
def properties_changed(self, interface, properties, invalidated):
109
104
"""This is called whenever we get a PropertiesChanged signal
110
105
It updates the changed properties in the "properties" dict.
112
107
# Update properties dict with new value
113
if interface == client_interface:
114
self.properties.update(properties)
108
self.properties.update(properties)
116
110
def delete(self):
117
111
self.property_changed_match.remove()
165
159
byte_arrays=True))
166
160
self.logger('Created client {}'
167
161
.format(self.properties["Name"]), level=0)
169
163
def using_timer(self, flag):
170
164
"""Call this method with True or False when timer should be
171
165
activated or deactivated.
173
167
if flag and self._update_timer_callback_tag is None:
174
168
# Will update the shown timer value every second
175
self._update_timer_callback_tag = (GLib.timeout_add
169
self._update_timer_callback_tag = (gobject.timeout_add
177
171
self.update_timer))
178
172
elif not (flag or self._update_timer_callback_tag is None):
179
GLib.source_remove(self._update_timer_callback_tag)
173
gobject.source_remove(self._update_timer_callback_tag)
180
174
self._update_timer_callback_tag = None
182
def checker_completed(self, exitstatus, condition, command):
176
def checker_completed(self, exitstatus, signal, command):
183
177
if exitstatus == 0:
184
178
self.logger('Checker for client {} (command "{}")'
185
179
' succeeded'.format(self.properties["Name"],
190
if os.WIFEXITED(condition):
191
185
self.logger('Checker for client {} (command "{}") failed'
192
186
' with exit code {}'
193
187
.format(self.properties["Name"], command,
194
os.WEXITSTATUS(condition)))
195
elif os.WIFSIGNALED(condition):
196
190
self.logger('Checker for client {} (command "{}") was'
197
191
' killed by signal {}'
198
192
.format(self.properties["Name"], command,
199
os.WTERMSIG(condition)))
195
self.logger('Checker for client {} completed'
197
.format(self.properties["Name"]))
202
200
def checker_started(self, command):
203
201
"""Server signals that a checker started."""
204
202
self.logger('Client {} started checker "{}"'
205
203
.format(self.properties["Name"],
206
204
command), level=0)
208
206
def got_secret(self):
209
207
self.logger('Client {} received its secret'
210
208
.format(self.properties["Name"]))
212
210
def need_approval(self, timeout, default):
214
212
message = 'Client {} needs approval within {} seconds'
216
214
message = 'Client {} will get its secret in {} seconds'
217
215
self.logger(message.format(self.properties["Name"],
220
218
def rejected(self, reason):
221
219
self.logger('Client {} was rejected; reason: {}'
222
220
.format(self.properties["Name"], reason))
224
222
def selectable(self):
225
223
"""Make this a "selectable" widget.
226
224
This overrides the method from urwid.FlowWidget."""
229
227
def rows(self, maxcolrow, focus=False):
230
228
"""How many rows this widget will occupy might depend on
231
229
whether we have focus or not.
232
230
This overrides the method from urwid.FlowWidget"""
233
231
return self.current_widget(focus).rows(maxcolrow, focus=focus)
235
233
def current_widget(self, focus=False):
236
234
if focus or self.opened:
237
235
return self._focus_widget
238
236
return self._widget
240
238
def update(self):
241
239
"Called when what is visible on the screen should be updated."
242
240
# How to add standout mode to a style
243
with_standout = {"normal": "standout",
244
"bold": "bold-standout",
246
"underline-blink-standout",
247
"bold-underline-blink":
248
"bold-underline-blink-standout",
241
with_standout = { "normal": "standout",
242
"bold": "bold-standout",
244
"underline-blink-standout",
245
"bold-underline-blink":
246
"bold-underline-blink-standout",
251
249
# Rebuild focus and non-focus widgets using current properties
253
251
# Base part of a client. Name!
254
252
base = '{name}: '.format(name=self.properties["Name"])
255
253
if not self.properties["Enabled"]:
256
254
message = "DISABLED"
257
255
self.using_timer(False)
258
256
elif self.properties["ApprovalPending"]:
259
timeout = datetime.timedelta(
260
milliseconds=self.properties["ApprovalDelay"])
257
timeout = datetime.timedelta(milliseconds
261
260
last_approval_request = isoformat_to_datetime(
262
261
self.properties["LastApprovalRequest"])
263
262
if last_approval_request is not None:
306
305
# Run update hook, if any
307
306
if self.update_hook is not None:
308
307
self.update_hook()
310
309
def update_timer(self):
311
"""called by GLib. Will indefinitely loop until
312
GLib.source_remove() on tag is called
310
"""called by gobject. Will indefinitely loop until
311
gobject.source_remove() on tag is called"""
315
313
return True # Keep calling this
317
315
def delete(self, **kwargs):
318
316
if self._update_timer_callback_tag is not None:
319
GLib.source_remove(self._update_timer_callback_tag)
317
gobject.source_remove(self._update_timer_callback_tag)
320
318
self._update_timer_callback_tag = None
321
319
for match in self.match_objects:
324
322
if self.delete_hook is not None:
325
323
self.delete_hook(self)
326
324
return super(MandosClientWidget, self).delete(**kwargs)
328
326
def render(self, maxcolrow, focus=False):
329
327
"""Render differently if we have focus.
330
328
This overrides the method from urwid.FlowWidget"""
331
329
return self.current_widget(focus).render(maxcolrow,
334
332
def keypress(self, maxcolrow, key):
336
334
This overrides the method from urwid.FlowWidget"""
338
self.proxy.Set(client_interface, "Enabled",
339
dbus.Boolean(True), ignore_reply=True,
340
dbus_interface=dbus.PROPERTIES_IFACE)
336
self.proxy.Enable(dbus_interface = client_interface,
342
self.proxy.Set(client_interface, "Enabled", False,
344
dbus_interface=dbus.PROPERTIES_IFACE)
339
self.proxy.Disable(dbus_interface = client_interface,
346
342
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
347
dbus_interface=client_interface,
343
dbus_interface = client_interface,
348
344
ignore_reply=True)
350
346
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
351
dbus_interface=client_interface,
347
dbus_interface = client_interface,
352
348
ignore_reply=True)
353
349
elif key == "R" or key == "_" or key == "ctrl k":
354
350
self.server_proxy_object.RemoveClient(self.proxy
356
352
ignore_reply=True)
358
self.proxy.Set(client_interface, "CheckerRunning",
359
dbus.Boolean(True), ignore_reply=True,
360
dbus_interface=dbus.PROPERTIES_IFACE)
354
self.proxy.StartChecker(dbus_interface = client_interface,
362
self.proxy.Set(client_interface, "CheckerRunning",
363
dbus.Boolean(False), ignore_reply=True,
364
dbus_interface=dbus.PROPERTIES_IFACE)
357
self.proxy.StopChecker(dbus_interface = client_interface,
366
self.proxy.CheckedOK(dbus_interface=client_interface,
360
self.proxy.CheckedOK(dbus_interface = client_interface,
367
361
ignore_reply=True)
369
363
# elif key == "p" or key == "=":
378
372
def properties_changed(self, interface, properties, invalidated):
379
373
"""Call self.update() if any properties changed.
380
374
This overrides the method from MandosClientPropertyCache"""
381
old_values = {key: self.properties.get(key)
382
for key in properties.keys()}
375
old_values = { key: self.properties.get(key)
376
for key in properties.keys() }
383
377
super(MandosClientWidget, self).properties_changed(
384
378
interface, properties, invalidated)
385
379
if any(old_values[key] != self.properties.get(key)
393
387
use them as an excuse to shift focus away from this widget.
395
389
def keypress(self, *args, **kwargs):
396
ret = (super(ConstrainedListBox, self)
397
.keypress(*args, **kwargs))
390
ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
398
391
if ret in ("up", "down"):
407
400
def __init__(self, max_log_length=1000, log_level=1):
408
401
DBusGMainLoop(set_as_default=True)
410
403
self.screen = urwid.curses_display.Screen()
412
405
self.screen.register_palette((
414
407
"default", "default", None),
420
413
"standout", "default", "standout"),
421
414
("bold-underline-blink",
422
"bold,underline,blink", "default",
423
"bold,underline,blink"),
415
"bold,underline,blink", "default", "bold,underline,blink"),
424
416
("bold-standout",
425
417
"bold,standout", "default", "bold,standout"),
426
418
("underline-blink-standout",
430
422
"bold,underline,blink,standout", "default",
431
423
"bold,underline,blink,standout"),
434
426
if urwid.supports_unicode():
435
self.divider = "─" # \u2500
427
self.divider = "─" # \u2500
428
#self.divider = "━" # \u2501
437
self.divider = "_" # \u005f
430
#self.divider = "-" # \u002d
431
self.divider = "_" # \u005f
439
433
self.screen.start()
441
435
self.size = self.screen.get_cols_rows()
443
437
self.clients = urwid.SimpleListWalker([])
444
438
self.clients_dict = {}
446
440
# We will add Text widgets to this list
447
self.log = urwid.SimpleListWalker([])
448
442
self.max_log_length = max_log_length
450
444
self.log_level = log_level
452
446
# We keep a reference to the log widget so we can remove it
453
447
# from the ListWalker without it getting destroyed
454
448
self.logbox = ConstrainedListBox(self.log)
456
450
# This keeps track of whether self.uilist currently has
457
451
# self.logbox in it or not
458
452
self.log_visible = True
459
453
self.log_wrap = "any"
462
456
self.log_message_raw(("bold",
463
457
"Mandos Monitor version " + version))
464
458
self.log_message_raw(("bold",
465
459
"q: Quit ?: Help"))
467
461
self.busname = domain + '.Mandos'
468
self.main_loop = GLib.MainLoop()
470
def client_not_found(self, key_id, address):
471
self.log_message("Client with address {} and key ID {} could"
472
" not be found".format(address, key_id))
462
self.main_loop = gobject.MainLoop()
464
def client_not_found(self, fingerprint, address):
465
self.log_message("Client with address {} and fingerprint {}"
466
" could not be found"
467
.format(address, fingerprint))
474
469
def rebuild(self):
475
470
"""This rebuilds the User Interface.
476
471
Call this when the widget layout needs to change"""
478
# self.uilist.append(urwid.ListBox(self.clients))
473
#self.uilist.append(urwid.ListBox(self.clients))
479
474
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
481
# header=urwid.Divider(),
476
#header=urwid.Divider(),
483
footer=urwid.Divider(
484
div_char=self.divider)))
479
urwid.Divider(div_char=
485
481
if self.log_visible:
486
482
self.uilist.append(self.logbox)
487
483
self.topwidget = urwid.Pile(self.uilist)
489
485
def log_message(self, message, level=1):
490
486
"""Log message formatted with timestamp"""
491
487
if level < self.log_level:
493
489
timestamp = datetime.datetime.now().isoformat()
494
490
self.log_message_raw("{}: {}".format(timestamp, message),
497
493
def log_message_raw(self, markup, level=1):
498
494
"""Add a log message to the log buffer."""
499
495
if level < self.log_level:
501
497
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
502
if self.max_log_length:
503
if 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)-1,
498
if (self.max_log_length
499
and len(self.log) > self.max_log_length):
500
del self.log[0:len(self.log)-self.max_log_length-1]
501
self.logbox.set_focus(len(self.logbox.body.contents),
506
502
coming_from="above")
509
505
def toggle_log_display(self):
510
506
"""Toggle visibility of the log buffer."""
511
507
self.log_visible = not self.log_visible
513
509
self.log_message("Log visibility changed to: {}"
514
510
.format(self.log_visible), level=0)
516
512
def change_log_display(self):
517
513
"""Change type of log display.
518
514
Currently, this toggles wrapping of text lines."""
524
520
textwidget.set_wrap_mode(self.log_wrap)
525
521
self.log_message("Wrap mode: {}".format(self.log_wrap),
528
def find_and_remove_client(self, path, interfaces):
524
def find_and_remove_client(self, path, name):
529
525
"""Find a client by its object path and remove it.
531
This is connected to the InterfacesRemoved signal from the
527
This is connected to the ClientRemoved signal from the
532
528
Mandos server object."""
533
if client_interface not in interfaces:
534
# Not a Mandos client object; ignore
537
530
client = self.clients_dict[path]
540
self.log_message("Unknown client {!r} removed"
533
self.log_message("Unknown client {!r} ({!r}) removed"
545
def add_new_client(self, path, ifs_and_props):
546
"""Find a client by its object path and remove it.
548
This is connected to the InterfacesAdded signal from the
549
Mandos server object.
551
if client_interface not in ifs_and_props:
552
# Not a Mandos client object; ignore
538
def add_new_client(self, path):
554
539
client_proxy_object = self.bus.get_object(self.busname, path)
555
self.add_client(MandosClientWidget(
556
server_proxy_object=self.mandos_serv,
557
proxy_object=client_proxy_object,
558
update_hook=self.refresh,
559
delete_hook=self.remove_client,
560
logger=self.log_message,
561
properties=dict(ifs_and_props[client_interface])),
540
self.add_client(MandosClientWidget(server_proxy_object
543
=client_proxy_object,
564
552
def add_client(self, client, path=None):
565
553
self.clients.append(client)
568
556
self.clients_dict[path] = client
569
557
self.clients.sort(key=lambda c: c.properties["Name"])
572
560
def remove_client(self, client, path=None):
573
561
self.clients.remove(client)
575
563
path = client.proxy.object_path
576
564
del self.clients_dict[path]
579
567
def refresh(self):
580
568
"""Redraw the screen"""
581
569
canvas = self.topwidget.render(self.size, focus=True)
582
570
self.screen.draw_screen(self.size, canvas)
585
573
"""Start the main loop and exit when it's done."""
586
574
self.bus = dbus.SystemBus()
587
575
mandos_dbus_objc = self.bus.get_object(
588
576
self.busname, "/", follow_name_owner_changes=True)
589
self.mandos_serv = dbus.Interface(
590
mandos_dbus_objc, dbus_interface=server_interface)
577
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
592
581
mandos_clients = (self.mandos_serv
593
582
.GetAllClientsWithProperties())
594
583
if not mandos_clients:
595
self.log_message_raw(("bold",
596
"Note: Server has no clients."))
584
self.log_message_raw(("bold", "Note: Server has no clients."))
597
585
except dbus.exceptions.DBusException:
598
self.log_message_raw(("bold",
599
"Note: No Mandos server running."))
586
self.log_message_raw(("bold", "Note: No Mandos server running."))
600
587
mandos_clients = dbus.Dictionary()
602
589
(self.mandos_serv
603
.connect_to_signal("InterfacesRemoved",
590
.connect_to_signal("ClientRemoved",
604
591
self.find_and_remove_client,
605
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
592
dbus_interface=server_interface,
606
593
byte_arrays=True))
607
594
(self.mandos_serv
608
.connect_to_signal("InterfacesAdded",
595
.connect_to_signal("ClientAdded",
609
596
self.add_new_client,
610
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
597
dbus_interface=server_interface,
611
598
byte_arrays=True))
612
599
(self.mandos_serv
613
600
.connect_to_signal("ClientNotFound",
617
604
for path, client in mandos_clients.items():
618
605
client_proxy_object = self.bus.get_object(self.busname,
620
self.add_client(MandosClientWidget(
621
server_proxy_object=self.mandos_serv,
622
proxy_object=client_proxy_object,
624
update_hook=self.refresh,
625
delete_hook=self.remove_client,
626
logger=self.log_message),
607
self.add_client(MandosClientWidget(server_proxy_object
610
=client_proxy_object,
630
self._input_callback_tag = (
632
GLib.IOChannel.unix_new(sys.stdin.fileno()),
633
GLib.PRIORITY_DEFAULT, GLib.IO_IN,
621
self._input_callback_tag = (gobject.io_add_watch
635
625
self.main_loop.run()
636
626
# Main loop has finished, we should close everything now
637
GLib.source_remove(self._input_callback_tag)
638
with warnings.catch_warnings():
639
warnings.simplefilter("ignore", BytesWarning)
627
gobject.source_remove(self._input_callback_tag)
643
631
self.main_loop.quit()
645
633
def process_input(self, source, condition):
646
634
keys = self.screen.get_input()
647
translations = {"ctrl n": "down", # Emacs
648
"ctrl p": "up", # Emacs
649
"ctrl v": "page down", # Emacs
650
"meta v": "page up", # Emacs
651
" ": "page down", # less
652
"f": "page down", # less
653
"b": "page up", # less
635
translations = { "ctrl n": "down", # Emacs
636
"ctrl p": "up", # Emacs
637
"ctrl v": "page down", # Emacs
638
"meta v": "page up", # Emacs
639
" ": "page down", # less
640
"f": "page down", # less
641
"b": "page up", # less
659
647
key = translations[key]
660
648
except KeyError: # :-)
663
651
if key == "q" or key == "Q":
714
702
self.log_level = 0
715
703
self.log_message("Verbose mode: On")
716
# elif (key == "end" or key == "meta >" or key == "G"
718
# pass # xxx end-of-buffer
719
# elif (key == "home" or key == "meta <" or key == "g"
721
# pass # xxx beginning-of-buffer
722
# elif key == "ctrl e" or key == "$":
723
# pass # xxx move-end-of-line
724
# elif key == "ctrl a" or key == "^":
725
# pass # xxx move-beginning-of-line
726
# elif key == "ctrl b" or key == "meta (" or key == "h":
728
# elif key == "ctrl f" or key == "meta )" or key == "l":
731
# pass # scroll up log
733
# pass # scroll down log
704
#elif (key == "end" or key == "meta >" or key == "G"
706
# pass # xxx end-of-buffer
707
#elif (key == "home" or key == "meta <" or key == "g"
709
# pass # xxx beginning-of-buffer
710
#elif key == "ctrl e" or key == "$":
711
# pass # xxx move-end-of-line
712
#elif key == "ctrl a" or key == "^":
713
# pass # xxx move-beginning-of-line
714
#elif key == "ctrl b" or key == "meta (" or key == "h":
716
#elif key == "ctrl f" or key == "meta )" or key == "l":
719
# pass # scroll up log
721
# pass # scroll down log
734
722
elif self.topwidget.selectable():
735
723
self.topwidget.keypress(self.size, key)
740
727
ui = UserInterface()