2
2
# -*- mode: python; coding: utf-8 -*-
4
4
# Mandos Monitor - Control and monitor the Mandos server
6
6
# Copyright © 2009-2016 Teddy Hogeborn
7
7
# Copyright © 2009-2016 Björn Påhlsson
9
9
# This program is free software: you can redistribute it and/or modify
10
10
# it under the terms of the GNU General Public License as published by
11
11
# the Free Software Foundation, either version 3 of the License, or
15
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
17
# GNU General Public License for more details.
19
19
# You should have received a copy of the GNU General Public License
20
20
# along with this program. If not, see
21
21
# <http://www.gnu.org/licenses/>.
23
23
# Contact the authors at <mandos@recompile.se>.
26
26
from __future__ import (division, absolute_import, print_function,
41
41
from dbus.mainloop.glib import DBusGMainLoop
43
from gi.repository import GObject
45
import gobject as GObject
42
from gi.repository import GLib
51
50
if sys.version_info.major == 2:
54
53
locale.setlocale(locale.LC_ALL, '')
57
55
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
59
57
# Some useful constants
60
58
domain = 'se.recompile'
61
59
server_interface = domain + '.Mandos'
62
60
client_interface = domain + '.Mandos.Client'
66
64
dbus.OBJECT_MANAGER_IFACE
67
65
except AttributeError:
68
66
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
70
69
def isoformat_to_datetime(iso):
71
70
"Parse an ISO 8601 date string to a datetime.datetime()"
83
int(second), # Whole seconds
84
int(fraction*1000000)) # Microseconds
82
int(second), # Whole seconds
83
int(fraction*1000000)) # Microseconds
86
86
class MandosClientPropertyCache(object):
87
87
"""This wraps a Mandos Client D-Bus proxy object, caches the
91
91
def __init__(self, proxy_object=None, properties=None, **kwargs):
92
self.proxy = proxy_object # Mandos Client proxy object
92
self.proxy = proxy_object # Mandos Client proxy object
93
93
self.properties = dict() if properties is None else properties
94
94
self.property_changed_match = (
95
95
self.proxy.connect_to_signal("PropertiesChanged",
96
96
self.properties_changed,
97
97
dbus.PROPERTIES_IFACE,
100
100
if properties is None:
101
self.properties.update(
102
self.proxy.GetAll(client_interface,
104
= dbus.PROPERTIES_IFACE))
101
self.properties.update(self.proxy.GetAll(
103
dbus_interface=dbus.PROPERTIES_IFACE))
106
105
super(MandosClientPropertyCache, self).__init__(**kwargs)
108
107
def properties_changed(self, interface, properties, invalidated):
109
108
"""This is called whenever we get a PropertiesChanged signal
110
109
It updates the changed properties in the "properties" dict.
120
119
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
121
120
"""A Mandos Client which is visible on the screen.
124
123
def __init__(self, server_proxy_object=None, update_hook=None,
125
124
delete_hook=None, logger=None, **kwargs):
126
125
# Called on update
165
164
byte_arrays=True))
166
165
self.logger('Created client {}'
167
166
.format(self.properties["Name"]), level=0)
169
168
def using_timer(self, flag):
170
169
"""Call this method with True or False when timer should be
171
170
activated or deactivated.
173
172
if flag and self._update_timer_callback_tag is None:
174
173
# Will update the shown timer value every second
175
self._update_timer_callback_tag = (GObject.timeout_add
174
self._update_timer_callback_tag = (GLib.timeout_add
177
176
self.update_timer))
178
177
elif not (flag or self._update_timer_callback_tag is None):
179
GObject.source_remove(self._update_timer_callback_tag)
178
GLib.source_remove(self._update_timer_callback_tag)
180
179
self._update_timer_callback_tag = None
182
181
def checker_completed(self, exitstatus, condition, command):
183
182
if exitstatus == 0:
184
183
self.logger('Checker for client {} (command "{}")'
198
197
.format(self.properties["Name"], command,
199
198
os.WTERMSIG(condition)))
202
201
def checker_started(self, command):
203
202
"""Server signals that a checker started."""
204
203
self.logger('Client {} started checker "{}"'
205
204
.format(self.properties["Name"],
206
205
command), level=0)
208
207
def got_secret(self):
209
208
self.logger('Client {} received its secret'
210
209
.format(self.properties["Name"]))
212
211
def need_approval(self, timeout, default):
214
213
message = 'Client {} needs approval within {} seconds'
216
215
message = 'Client {} will get its secret in {} seconds'
217
216
self.logger(message.format(self.properties["Name"],
220
219
def rejected(self, reason):
221
220
self.logger('Client {} was rejected; reason: {}'
222
221
.format(self.properties["Name"], reason))
224
223
def selectable(self):
225
224
"""Make this a "selectable" widget.
226
225
This overrides the method from urwid.FlowWidget."""
229
228
def rows(self, maxcolrow, focus=False):
230
229
"""How many rows this widget will occupy might depend on
231
230
whether we have focus or not.
232
231
This overrides the method from urwid.FlowWidget"""
233
232
return self.current_widget(focus).rows(maxcolrow, focus=focus)
235
234
def current_widget(self, focus=False):
236
235
if focus or self.opened:
237
236
return self._focus_widget
238
237
return self._widget
240
239
def update(self):
241
240
"Called when what is visible on the screen should be updated."
242
241
# 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",
242
with_standout = {"normal": "standout",
243
"bold": "bold-standout",
245
"underline-blink-standout",
246
"bold-underline-blink":
247
"bold-underline-blink-standout",
251
250
# Rebuild focus and non-focus widgets using current properties
253
252
# Base part of a client. Name!
254
253
base = '{name}: '.format(name=self.properties["Name"])
255
254
if not self.properties["Enabled"]:
256
255
message = "DISABLED"
257
256
self.using_timer(False)
258
257
elif self.properties["ApprovalPending"]:
259
timeout = datetime.timedelta(milliseconds
258
timeout = datetime.timedelta(
259
milliseconds=self.properties["ApprovalDelay"])
262
260
last_approval_request = isoformat_to_datetime(
263
261
self.properties["LastApprovalRequest"])
264
262
if last_approval_request is not None:
307
305
# Run update hook, if any
308
306
if self.update_hook is not None:
309
307
self.update_hook()
311
309
def update_timer(self):
312
"""called by GObject. Will indefinitely loop until
313
GObject.source_remove() on tag is called"""
310
"""called by GLib. Will indefinitely loop until
311
GLib.source_remove() on tag is called
315
314
return True # Keep calling this
317
316
def delete(self, **kwargs):
318
317
if self._update_timer_callback_tag is not None:
319
GObject.source_remove(self._update_timer_callback_tag)
318
GLib.source_remove(self._update_timer_callback_tag)
320
319
self._update_timer_callback_tag = None
321
320
for match in self.match_objects:
324
323
if self.delete_hook is not None:
325
324
self.delete_hook(self)
326
325
return super(MandosClientWidget, self).delete(**kwargs)
328
327
def render(self, maxcolrow, focus=False):
329
328
"""Render differently if we have focus.
330
329
This overrides the method from urwid.FlowWidget"""
331
330
return self.current_widget(focus).render(maxcolrow,
334
333
def keypress(self, maxcolrow, key):
336
335
This overrides the method from urwid.FlowWidget"""
338
337
self.proxy.Set(client_interface, "Enabled",
339
dbus.Boolean(True), ignore_reply = True,
340
dbus_interface = dbus.PROPERTIES_IFACE)
338
dbus.Boolean(True), ignore_reply=True,
339
dbus_interface=dbus.PROPERTIES_IFACE)
342
341
self.proxy.Set(client_interface, "Enabled", False,
344
dbus_interface = dbus.PROPERTIES_IFACE)
343
dbus_interface=dbus.PROPERTIES_IFACE)
346
345
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
347
dbus_interface = client_interface,
346
dbus_interface=client_interface,
348
347
ignore_reply=True)
350
349
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
351
dbus_interface = client_interface,
350
dbus_interface=client_interface,
352
351
ignore_reply=True)
353
352
elif key == "R" or key == "_" or key == "ctrl k":
354
353
self.server_proxy_object.RemoveClient(self.proxy
356
355
ignore_reply=True)
358
357
self.proxy.Set(client_interface, "CheckerRunning",
359
dbus.Boolean(True), ignore_reply = True,
360
dbus_interface = dbus.PROPERTIES_IFACE)
358
dbus.Boolean(True), ignore_reply=True,
359
dbus_interface=dbus.PROPERTIES_IFACE)
362
361
self.proxy.Set(client_interface, "CheckerRunning",
363
dbus.Boolean(False), ignore_reply = True,
364
dbus_interface = dbus.PROPERTIES_IFACE)
362
dbus.Boolean(False), ignore_reply=True,
363
dbus_interface=dbus.PROPERTIES_IFACE)
366
self.proxy.CheckedOK(dbus_interface = client_interface,
365
self.proxy.CheckedOK(dbus_interface=client_interface,
367
366
ignore_reply=True)
369
368
# elif key == "p" or key == "=":
378
377
def properties_changed(self, interface, properties, invalidated):
379
378
"""Call self.update() if any properties changed.
380
379
This overrides the method from MandosClientPropertyCache"""
381
old_values = { key: self.properties.get(key)
382
for key in properties.keys() }
380
old_values = {key: self.properties.get(key)
381
for key in properties.keys()}
383
382
super(MandosClientWidget, self).properties_changed(
384
383
interface, properties, invalidated)
385
384
if any(old_values[key] != self.properties.get(key)
393
392
use them as an excuse to shift focus away from this widget.
395
394
def keypress(self, *args, **kwargs):
396
ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
395
ret = (super(ConstrainedListBox, self)
396
.keypress(*args, **kwargs))
397
397
if ret in ("up", "down"):
406
406
def __init__(self, max_log_length=1000, log_level=1):
407
407
DBusGMainLoop(set_as_default=True)
409
409
self.screen = urwid.curses_display.Screen()
411
411
self.screen.register_palette((
413
413
"default", "default", None),
419
419
"standout", "default", "standout"),
420
420
("bold-underline-blink",
421
"bold,underline,blink", "default", "bold,underline,blink"),
421
"bold,underline,blink", "default",
422
"bold,underline,blink"),
422
423
("bold-standout",
423
424
"bold,standout", "default", "bold,standout"),
424
425
("underline-blink-standout",
428
429
"bold,underline,blink,standout", "default",
429
430
"bold,underline,blink,standout"),
432
433
if urwid.supports_unicode():
433
self.divider = "─" # \u2500
434
#self.divider = "━" # \u2501
434
self.divider = "─" # \u2500
436
#self.divider = "-" # \u002d
437
self.divider = "_" # \u005f
436
self.divider = "_" # \u005f
439
438
self.screen.start()
441
440
self.size = self.screen.get_cols_rows()
443
442
self.clients = urwid.SimpleListWalker([])
444
443
self.clients_dict = {}
446
445
# We will add Text widgets to this list
448
447
self.max_log_length = max_log_length
450
449
self.log_level = log_level
452
451
# We keep a reference to the log widget so we can remove it
453
452
# from the ListWalker without it getting destroyed
454
453
self.logbox = ConstrainedListBox(self.log)
456
455
# This keeps track of whether self.uilist currently has
457
456
# self.logbox in it or not
458
457
self.log_visible = True
459
458
self.log_wrap = "any"
462
461
self.log_message_raw(("bold",
463
462
"Mandos Monitor version " + version))
464
463
self.log_message_raw(("bold",
465
464
"q: Quit ?: Help"))
467
466
self.busname = domain + '.Mandos'
468
self.main_loop = GObject.MainLoop()
467
self.main_loop = GLib.MainLoop()
470
469
def client_not_found(self, fingerprint, address):
471
470
self.log_message("Client with address {} and fingerprint {}"
472
471
" could not be found"
473
472
.format(address, fingerprint))
475
474
def rebuild(self):
476
475
"""This rebuilds the User Interface.
477
476
Call this when the widget layout needs to change"""
479
#self.uilist.append(urwid.ListBox(self.clients))
478
# self.uilist.append(urwid.ListBox(self.clients))
480
479
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
482
#header=urwid.Divider(),
481
# header=urwid.Divider(),
485
urwid.Divider(div_char=
483
footer=urwid.Divider(
484
div_char=self.divider)))
487
485
if self.log_visible:
488
486
self.uilist.append(self.logbox)
489
487
self.topwidget = urwid.Pile(self.uilist)
491
489
def log_message(self, message, level=1):
492
490
"""Log message formatted with timestamp"""
493
491
if level < self.log_level:
495
493
timestamp = datetime.datetime.now().isoformat()
496
494
self.log_message_raw("{}: {}".format(timestamp, message),
499
497
def log_message_raw(self, markup, level=1):
500
498
"""Add a log message to the log buffer."""
501
499
if level < self.log_level:
503
501
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
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]
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]
507
505
self.logbox.set_focus(len(self.logbox.body.contents),
508
506
coming_from="above")
511
509
def toggle_log_display(self):
512
510
"""Toggle visibility of the log buffer."""
513
511
self.log_visible = not self.log_visible
515
513
self.log_message("Log visibility changed to: {}"
516
514
.format(self.log_visible), level=0)
518
516
def change_log_display(self):
519
517
"""Change type of log display.
520
518
Currently, this toggles wrapping of text lines."""
526
524
textwidget.set_wrap_mode(self.log_wrap)
527
525
self.log_message("Wrap mode: {}".format(self.log_wrap),
530
528
def find_and_remove_client(self, path, interfaces):
531
529
"""Find a client by its object path and remove it.
533
531
This is connected to the InterfacesRemoved signal from the
534
532
Mandos server object."""
535
533
if client_interface not in interfaces:
554
552
# Not a Mandos client object; ignore
556
554
client_proxy_object = self.bus.get_object(self.busname, path)
557
self.add_client(MandosClientWidget(server_proxy_object
560
=client_proxy_object,
568
= dict(ifs_and_props[
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])),
572
564
def add_client(self, client, path=None):
573
565
self.clients.append(client)
576
568
self.clients_dict[path] = client
577
569
self.clients.sort(key=lambda c: c.properties["Name"])
580
572
def remove_client(self, client, path=None):
581
573
self.clients.remove(client)
583
575
path = client.proxy.object_path
584
576
del self.clients_dict[path]
587
579
def refresh(self):
588
580
"""Redraw the screen"""
589
581
canvas = self.topwidget.render(self.size, focus=True)
590
582
self.screen.draw_screen(self.size, canvas)
593
585
"""Start the main loop and exit when it's done."""
594
586
self.bus = dbus.SystemBus()
595
587
mandos_dbus_objc = self.bus.get_object(
596
588
self.busname, "/", follow_name_owner_changes=True)
597
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
589
self.mandos_serv = dbus.Interface(
590
mandos_dbus_objc, dbus_interface=server_interface)
601
592
mandos_clients = (self.mandos_serv
602
593
.GetAllClientsWithProperties())
603
594
if not mandos_clients:
604
self.log_message_raw(("bold", "Note: Server has no clients."))
595
self.log_message_raw(("bold",
596
"Note: Server has no clients."))
605
597
except dbus.exceptions.DBusException:
606
self.log_message_raw(("bold", "Note: No Mandos server running."))
598
self.log_message_raw(("bold",
599
"Note: No Mandos server running."))
607
600
mandos_clients = dbus.Dictionary()
609
602
(self.mandos_serv
610
603
.connect_to_signal("InterfacesRemoved",
611
604
self.find_and_remove_client,
613
= dbus.OBJECT_MANAGER_IFACE,
605
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
614
606
byte_arrays=True))
615
607
(self.mandos_serv
616
608
.connect_to_signal("InterfacesAdded",
617
609
self.add_new_client,
619
= dbus.OBJECT_MANAGER_IFACE,
610
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
620
611
byte_arrays=True))
621
612
(self.mandos_serv
622
613
.connect_to_signal("ClientNotFound",
626
617
for path, client in mandos_clients.items():
627
618
client_proxy_object = self.bus.get_object(self.busname,
629
self.add_client(MandosClientWidget(server_proxy_object
632
=client_proxy_object,
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),
643
self._input_callback_tag = (GObject.io_add_watch
630
self._input_callback_tag = (GLib.io_add_watch
644
631
(sys.stdin.fileno(),
646
633
self.process_input))
647
634
self.main_loop.run()
648
635
# Main loop has finished, we should close everything now
649
GObject.source_remove(self._input_callback_tag)
636
GLib.source_remove(self._input_callback_tag)
650
637
self.screen.stop()
653
640
self.main_loop.quit()
655
642
def process_input(self, source, condition):
656
643
keys = self.screen.get_input()
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
644
translations = {"ctrl n": "down", # Emacs
645
"ctrl p": "up", # Emacs
646
"ctrl v": "page down", # Emacs
647
"meta v": "page up", # Emacs
648
" ": "page down", # less
649
"f": "page down", # less
650
"b": "page up", # less
669
656
key = translations[key]
670
657
except KeyError: # :-)
673
660
if key == "q" or key == "Q":
724
711
self.log_level = 0
725
712
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
713
# elif (key == "end" or key == "meta >" or key == "G"
715
# pass # xxx end-of-buffer
716
# elif (key == "home" or key == "meta <" or key == "g"
718
# pass # xxx beginning-of-buffer
719
# elif key == "ctrl e" or key == "$":
720
# pass # xxx move-end-of-line
721
# elif key == "ctrl a" or key == "^":
722
# pass # xxx move-beginning-of-line
723
# elif key == "ctrl b" or key == "meta (" or key == "h":
725
# elif key == "ctrl f" or key == "meta )" or key == "l":
728
# pass # scroll up log
730
# pass # scroll down log
744
731
elif self.topwidget.selectable():
745
732
self.topwidget.keypress(self.size, key)