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()
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
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()
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]
506
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),
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 = (
633
GLib.IOChannel.unix_new(sys.stdin.fileno()),
634
GLib.PRIORITY_DEFAULT, GLib.IO_IN,
621
self._input_callback_tag = (gobject.io_add_watch
636
625
self.main_loop.run()
637
626
# Main loop has finished, we should close everything now
638
GLib.source_remove(self._input_callback_tag)
639
with warnings.catch_warnings():
640
warnings.simplefilter("ignore", BytesWarning)
627
gobject.source_remove(self._input_callback_tag)
644
631
self.main_loop.quit()
646
633
def process_input(self, source, condition):
647
634
keys = self.screen.get_input()
648
translations = {"ctrl n": "down", # Emacs
649
"ctrl p": "up", # Emacs
650
"ctrl v": "page down", # Emacs
651
"meta v": "page up", # Emacs
652
" ": "page down", # less
653
"f": "page down", # less
654
"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
660
647
key = translations[key]
661
648
except KeyError: # :-)
664
651
if key == "q" or key == "Q":
715
702
self.log_level = 0
716
703
self.log_message("Verbose mode: On")
717
# elif (key == "end" or key == "meta >" or key == "G"
719
# pass # xxx end-of-buffer
720
# elif (key == "home" or key == "meta <" or key == "g"
722
# pass # xxx beginning-of-buffer
723
# elif key == "ctrl e" or key == "$":
724
# pass # xxx move-end-of-line
725
# elif key == "ctrl a" or key == "^":
726
# pass # xxx move-beginning-of-line
727
# elif key == "ctrl b" or key == "meta (" or key == "h":
729
# elif key == "ctrl f" or key == "meta )" or key == "l":
732
# pass # scroll up log
734
# 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
735
722
elif self.topwidget.selectable():
736
723
self.topwidget.keypress(self.size, key)
741
727
ui = UserInterface()