2
2
# -*- mode: python; coding: utf-8 -*-
4
4
# Mandos Monitor - Control and monitor the Mandos server
6
# Copyright © 2009-2018 Teddy Hogeborn
7
# Copyright © 2009-2018 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,
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()
120
114
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
121
115
"""A Mandos Client which is visible on the screen.
124
118
def __init__(self, server_proxy_object=None, update_hook=None,
125
119
delete_hook=None, logger=None, **kwargs):
126
120
# Called on update
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
176
def checker_completed(self, exitstatus, condition, command):
183
177
if exitstatus == 0:
184
178
self.logger('Checker for client {} (command "{}")'
198
192
.format(self.properties["Name"], command,
199
193
os.WTERMSIG(condition)))
202
196
def checker_started(self, command):
203
197
"""Server signals that a checker started."""
204
198
self.logger('Client {} started checker "{}"'
205
199
.format(self.properties["Name"],
206
200
command), level=0)
208
202
def got_secret(self):
209
203
self.logger('Client {} received its secret'
210
204
.format(self.properties["Name"]))
212
206
def need_approval(self, timeout, default):
214
208
message = 'Client {} needs approval within {} seconds'
216
210
message = 'Client {} will get its secret in {} seconds'
217
211
self.logger(message.format(self.properties["Name"],
220
214
def rejected(self, reason):
221
215
self.logger('Client {} was rejected; reason: {}'
222
216
.format(self.properties["Name"], reason))
224
218
def selectable(self):
225
219
"""Make this a "selectable" widget.
226
220
This overrides the method from urwid.FlowWidget."""
229
223
def rows(self, maxcolrow, focus=False):
230
224
"""How many rows this widget will occupy might depend on
231
225
whether we have focus or not.
232
226
This overrides the method from urwid.FlowWidget"""
233
227
return self.current_widget(focus).rows(maxcolrow, focus=focus)
235
229
def current_widget(self, focus=False):
236
230
if focus or self.opened:
237
231
return self._focus_widget
238
232
return self._widget
240
234
def update(self):
241
235
"Called when what is visible on the screen should be updated."
242
236
# 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",
237
with_standout = { "normal": "standout",
238
"bold": "bold-standout",
240
"underline-blink-standout",
241
"bold-underline-blink":
242
"bold-underline-blink-standout",
251
245
# Rebuild focus and non-focus widgets using current properties
253
247
# Base part of a client. Name!
254
248
base = '{name}: '.format(name=self.properties["Name"])
255
249
if not self.properties["Enabled"]:
256
250
message = "DISABLED"
257
251
self.using_timer(False)
258
252
elif self.properties["ApprovalPending"]:
259
timeout = datetime.timedelta(
260
milliseconds=self.properties["ApprovalDelay"])
253
timeout = datetime.timedelta(milliseconds
261
256
last_approval_request = isoformat_to_datetime(
262
257
self.properties["LastApprovalRequest"])
263
258
if last_approval_request is not None:
306
301
# Run update hook, if any
307
302
if self.update_hook is not None:
308
303
self.update_hook()
310
305
def update_timer(self):
311
"""called by GLib. Will indefinitely loop until
312
GLib.source_remove() on tag is called
306
"""called by gobject. Will indefinitely loop until
307
gobject.source_remove() on tag is called"""
315
309
return True # Keep calling this
317
311
def delete(self, **kwargs):
318
312
if self._update_timer_callback_tag is not None:
319
GLib.source_remove(self._update_timer_callback_tag)
313
gobject.source_remove(self._update_timer_callback_tag)
320
314
self._update_timer_callback_tag = None
321
315
for match in self.match_objects:
324
318
if self.delete_hook is not None:
325
319
self.delete_hook(self)
326
320
return super(MandosClientWidget, self).delete(**kwargs)
328
322
def render(self, maxcolrow, focus=False):
329
323
"""Render differently if we have focus.
330
324
This overrides the method from urwid.FlowWidget"""
331
325
return self.current_widget(focus).render(maxcolrow,
334
328
def keypress(self, maxcolrow, key):
336
330
This overrides the method from urwid.FlowWidget"""
338
332
self.proxy.Set(client_interface, "Enabled",
339
dbus.Boolean(True), ignore_reply=True,
340
dbus_interface=dbus.PROPERTIES_IFACE)
333
dbus.Boolean(True), ignore_reply = True,
334
dbus_interface = dbus.PROPERTIES_IFACE)
342
336
self.proxy.Set(client_interface, "Enabled", False,
344
dbus_interface=dbus.PROPERTIES_IFACE)
338
dbus_interface = dbus.PROPERTIES_IFACE)
346
340
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
347
dbus_interface=client_interface,
341
dbus_interface = client_interface,
348
342
ignore_reply=True)
350
344
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
351
dbus_interface=client_interface,
345
dbus_interface = client_interface,
352
346
ignore_reply=True)
353
347
elif key == "R" or key == "_" or key == "ctrl k":
354
348
self.server_proxy_object.RemoveClient(self.proxy
356
350
ignore_reply=True)
358
352
self.proxy.Set(client_interface, "CheckerRunning",
359
dbus.Boolean(True), ignore_reply=True,
360
dbus_interface=dbus.PROPERTIES_IFACE)
353
dbus.Boolean(True), ignore_reply = True,
354
dbus_interface = dbus.PROPERTIES_IFACE)
362
356
self.proxy.Set(client_interface, "CheckerRunning",
363
dbus.Boolean(False), ignore_reply=True,
364
dbus_interface=dbus.PROPERTIES_IFACE)
357
dbus.Boolean(False), ignore_reply = True,
358
dbus_interface = dbus.PROPERTIES_IFACE)
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
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()
462
self.main_loop = gobject.MainLoop()
470
464
def client_not_found(self, fingerprint, address):
471
465
self.log_message("Client with address {} and fingerprint {}"
472
466
" could not be found"
473
467
.format(address, fingerprint))
475
469
def rebuild(self):
476
470
"""This rebuilds the User Interface.
477
471
Call this when the widget layout needs to change"""
479
# self.uilist.append(urwid.ListBox(self.clients))
473
#self.uilist.append(urwid.ListBox(self.clients))
480
474
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
482
# header=urwid.Divider(),
476
#header=urwid.Divider(),
484
footer=urwid.Divider(
485
div_char=self.divider)))
479
urwid.Divider(div_char=
486
481
if self.log_visible:
487
482
self.uilist.append(self.logbox)
488
483
self.topwidget = urwid.Pile(self.uilist)
490
485
def log_message(self, message, level=1):
491
486
"""Log message formatted with timestamp"""
492
487
if level < self.log_level:
494
489
timestamp = datetime.datetime.now().isoformat()
495
490
self.log_message_raw("{}: {}".format(timestamp, message),
498
493
def log_message_raw(self, markup, level=1):
499
494
"""Add a log message to the log buffer."""
500
495
if level < self.log_level:
502
497
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
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]
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]
506
501
self.logbox.set_focus(len(self.logbox.body.contents),
507
502
coming_from="above")
510
505
def toggle_log_display(self):
511
506
"""Toggle visibility of the log buffer."""
512
507
self.log_visible = not self.log_visible
514
509
self.log_message("Log visibility changed to: {}"
515
510
.format(self.log_visible), level=0)
517
512
def change_log_display(self):
518
513
"""Change type of log display.
519
514
Currently, this toggles wrapping of text lines."""
525
520
textwidget.set_wrap_mode(self.log_wrap)
526
521
self.log_message("Wrap mode: {}".format(self.log_wrap),
529
def find_and_remove_client(self, path, interfaces):
524
def find_and_remove_client(self, path, name):
530
525
"""Find a client by its object path and remove it.
532
This is connected to the InterfacesRemoved signal from the
527
This is connected to the ClientRemoved signal from the
533
528
Mandos server object."""
534
if client_interface not in interfaces:
535
# Not a Mandos client object; ignore
538
530
client = self.clients_dict[path]
541
self.log_message("Unknown client {!r} removed"
533
self.log_message("Unknown client {!r} ({!r}) removed"
546
def add_new_client(self, path, ifs_and_props):
547
"""Find a client by its object path and remove it.
549
This is connected to the InterfacesAdded signal from the
550
Mandos server object.
552
if client_interface not in ifs_and_props:
553
# Not a Mandos client object; ignore
538
def add_new_client(self, path):
555
539
client_proxy_object = self.bus.get_object(self.busname, path)
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])),
540
self.add_client(MandosClientWidget(server_proxy_object
543
=client_proxy_object,
565
552
def add_client(self, client, path=None):
566
553
self.clients.append(client)
569
556
self.clients_dict[path] = client
570
557
self.clients.sort(key=lambda c: c.properties["Name"])
573
560
def remove_client(self, client, path=None):
574
561
self.clients.remove(client)
576
563
path = client.proxy.object_path
577
564
del self.clients_dict[path]
580
567
def refresh(self):
581
568
"""Redraw the screen"""
582
569
canvas = self.topwidget.render(self.size, focus=True)
583
570
self.screen.draw_screen(self.size, canvas)
586
573
"""Start the main loop and exit when it's done."""
587
574
self.bus = dbus.SystemBus()
588
575
mandos_dbus_objc = self.bus.get_object(
589
576
self.busname, "/", follow_name_owner_changes=True)
590
self.mandos_serv = dbus.Interface(
591
mandos_dbus_objc, dbus_interface=server_interface)
577
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
593
581
mandos_clients = (self.mandos_serv
594
582
.GetAllClientsWithProperties())
595
583
if not mandos_clients:
596
self.log_message_raw(("bold",
597
"Note: Server has no clients."))
584
self.log_message_raw(("bold", "Note: Server has no clients."))
598
585
except dbus.exceptions.DBusException:
599
self.log_message_raw(("bold",
600
"Note: No Mandos server running."))
586
self.log_message_raw(("bold", "Note: No Mandos server running."))
601
587
mandos_clients = dbus.Dictionary()
603
589
(self.mandos_serv
604
.connect_to_signal("InterfacesRemoved",
590
.connect_to_signal("ClientRemoved",
605
591
self.find_and_remove_client,
606
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
592
dbus_interface=server_interface,
607
593
byte_arrays=True))
608
594
(self.mandos_serv
609
.connect_to_signal("InterfacesAdded",
595
.connect_to_signal("ClientAdded",
610
596
self.add_new_client,
611
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
597
dbus_interface=server_interface,
612
598
byte_arrays=True))
613
599
(self.mandos_serv
614
600
.connect_to_signal("ClientNotFound",
618
604
for path, client in mandos_clients.items():
619
605
client_proxy_object = self.bus.get_object(self.busname,
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),
607
self.add_client(MandosClientWidget(server_proxy_object
610
=client_proxy_object,
631
self._input_callback_tag = (GLib.io_add_watch
621
self._input_callback_tag = (gobject.io_add_watch
632
622
(sys.stdin.fileno(),
634
624
self.process_input))
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)
627
gobject.source_remove(self._input_callback_tag)
638
628
self.screen.stop()
641
631
self.main_loop.quit()
643
633
def process_input(self, source, condition):
644
634
keys = self.screen.get_input()
645
translations = {"ctrl n": "down", # Emacs
646
"ctrl p": "up", # Emacs
647
"ctrl v": "page down", # Emacs
648
"meta v": "page up", # Emacs
649
" ": "page down", # less
650
"f": "page down", # less
651
"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
657
647
key = translations[key]
658
648
except KeyError: # :-)
661
651
if key == "q" or key == "Q":
712
702
self.log_level = 0
713
703
self.log_message("Verbose mode: On")
714
# elif (key == "end" or key == "meta >" or key == "G"
716
# pass # xxx end-of-buffer
717
# elif (key == "home" or key == "meta <" or key == "g"
719
# pass # xxx beginning-of-buffer
720
# elif key == "ctrl e" or key == "$":
721
# pass # xxx move-end-of-line
722
# elif key == "ctrl a" or key == "^":
723
# pass # xxx move-beginning-of-line
724
# elif key == "ctrl b" or key == "meta (" or key == "h":
726
# elif key == "ctrl f" or key == "meta )" or key == "l":
729
# pass # scroll up log
731
# 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
732
722
elif self.topwidget.selectable():
733
723
self.topwidget.keypress(self.size, key)
738
727
ui = UserInterface()