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-2013 Teddy Hogeborn
7
# Copyright © 2009-2013 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,
92
87
def __init__(self, proxy_object=None, properties=None, **kwargs):
93
self.proxy = proxy_object # Mandos Client proxy object
88
self.proxy = proxy_object # Mandos Client proxy object
94
89
self.properties = dict() if properties is None else properties
95
90
self.property_changed_match = (
96
self.proxy.connect_to_signal("PropertiesChanged",
97
self.properties_changed,
98
dbus.PROPERTIES_IFACE,
91
self.proxy.connect_to_signal("PropertyChanged",
92
self._property_changed,
101
96
if properties is None:
102
self.properties.update(self.proxy.GetAll(
104
dbus_interface=dbus.PROPERTIES_IFACE))
97
self.properties.update(
98
self.proxy.GetAll(client_interface,
100
= dbus.PROPERTIES_IFACE))
106
102
super(MandosClientPropertyCache, self).__init__(**kwargs)
108
def properties_changed(self, interface, properties, invalidated):
109
"""This is called whenever we get a PropertiesChanged signal
110
It updates the changed properties in the "properties" dict.
104
def _property_changed(self, property, value):
105
"""Helper which takes positional arguments"""
106
return self.property_changed(property=property, value=value)
108
def property_changed(self, property=None, value=None):
109
"""This is called whenever we get a PropertyChanged signal
110
It updates the changed property in the "properties" dict.
112
112
# Update properties dict with new value
113
if interface == client_interface:
114
self.properties.update(properties)
113
self.properties[property] = value
116
115
def delete(self):
117
116
self.property_changed_match.remove()
164
163
client_interface,
165
164
byte_arrays=True))
166
self.logger('Created client {}'
167
.format(self.properties["Name"]), level=0)
165
#self.logger('Created client {0}'
166
# .format(self.properties["Name"]))
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 = (GLib.timeout_add
174
self._update_timer_callback_tag = (gobject.timeout_add
177
176
self.update_timer))
178
177
elif not (flag or self._update_timer_callback_tag is None):
179
GLib.source_remove(self._update_timer_callback_tag)
178
gobject.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
self.logger('Checker for client {} (command "{}")'
185
' succeeded'.format(self.properties["Name"],
190
186
if os.WIFEXITED(condition):
191
self.logger('Checker for client {} (command "{}") failed'
187
self.logger('Checker for client {0} (command "{1}")'
188
' failed with exit code {2}'
193
189
.format(self.properties["Name"], command,
194
190
os.WEXITSTATUS(condition)))
195
191
elif os.WIFSIGNALED(condition):
196
self.logger('Checker for client {} (command "{}") was'
197
' killed by signal {}'
192
self.logger('Checker for client {0} (command "{1}") was'
193
' killed by signal {2}'
198
194
.format(self.properties["Name"], command,
199
195
os.WTERMSIG(condition)))
196
elif os.WCOREDUMP(condition):
197
self.logger('Checker for client {0} (command "{1}")'
199
.format(self.properties["Name"], command))
201
self.logger('Checker for client {0} completed'
203
.format(self.properties["Name"]))
202
206
def checker_started(self, command):
203
"""Server signals that a checker started."""
204
self.logger('Client {} started checker "{}"'
205
.format(self.properties["Name"],
207
"""Server signals that a checker started. This could be useful
208
to log in the future. """
209
#self.logger('Client {0} started checker "{1}"'
210
# .format(self.properties["Name"],
208
214
def got_secret(self):
209
self.logger('Client {} received its secret'
215
self.logger('Client {0} received its secret'
210
216
.format(self.properties["Name"]))
212
218
def need_approval(self, timeout, default):
214
message = 'Client {} needs approval within {} seconds'
220
message = 'Client {0} needs approval within {1} seconds'
216
message = 'Client {} will get its secret in {} seconds'
222
message = 'Client {0} will get its secret in {1} seconds'
217
223
self.logger(message.format(self.properties["Name"],
220
226
def rejected(self, reason):
221
self.logger('Client {} was rejected; reason: {}'
227
self.logger('Client {0} was rejected; reason: {1}'
222
228
.format(self.properties["Name"], reason))
224
230
def selectable(self):
225
231
"""Make this a "selectable" widget.
226
232
This overrides the method from urwid.FlowWidget."""
229
235
def rows(self, maxcolrow, focus=False):
230
236
"""How many rows this widget will occupy might depend on
231
237
whether we have focus or not.
232
238
This overrides the method from urwid.FlowWidget"""
233
239
return self.current_widget(focus).rows(maxcolrow, focus=focus)
235
241
def current_widget(self, focus=False):
236
242
if focus or self.opened:
237
243
return self._focus_widget
238
244
return self._widget
240
246
def update(self):
241
247
"Called when what is visible on the screen should be updated."
242
248
# 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",
249
with_standout = { "normal": "standout",
250
"bold": "bold-standout",
252
"underline-blink-standout",
253
"bold-underline-blink":
254
"bold-underline-blink-standout",
251
257
# Rebuild focus and non-focus widgets using current properties
253
259
# Base part of a client. Name!
254
260
base = '{name}: '.format(name=self.properties["Name"])
255
261
if not self.properties["Enabled"]:
256
262
message = "DISABLED"
257
263
self.using_timer(False)
258
264
elif self.properties["ApprovalPending"]:
259
timeout = datetime.timedelta(
260
milliseconds=self.properties["ApprovalDelay"])
265
timeout = datetime.timedelta(milliseconds
261
268
last_approval_request = isoformat_to_datetime(
262
269
self.properties["LastApprovalRequest"])
263
270
if last_approval_request is not None:
324
330
if self.delete_hook is not None:
325
331
self.delete_hook(self)
326
332
return super(MandosClientWidget, self).delete(**kwargs)
328
334
def render(self, maxcolrow, focus=False):
329
335
"""Render differently if we have focus.
330
336
This overrides the method from urwid.FlowWidget"""
331
337
return self.current_widget(focus).render(maxcolrow,
334
340
def keypress(self, maxcolrow, key):
336
342
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)
344
self.proxy.Enable(dbus_interface = client_interface,
342
self.proxy.Set(client_interface, "Enabled", False,
344
dbus_interface=dbus.PROPERTIES_IFACE)
347
self.proxy.Disable(dbus_interface = client_interface,
346
350
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
347
dbus_interface=client_interface,
351
dbus_interface = client_interface,
348
352
ignore_reply=True)
350
354
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
351
dbus_interface=client_interface,
355
dbus_interface = client_interface,
352
356
ignore_reply=True)
353
357
elif key == "R" or key == "_" or key == "ctrl k":
354
358
self.server_proxy_object.RemoveClient(self.proxy
356
360
ignore_reply=True)
358
self.proxy.Set(client_interface, "CheckerRunning",
359
dbus.Boolean(True), ignore_reply=True,
360
dbus_interface=dbus.PROPERTIES_IFACE)
362
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)
365
self.proxy.StopChecker(dbus_interface = client_interface,
366
self.proxy.CheckedOK(dbus_interface=client_interface,
368
self.proxy.CheckedOK(dbus_interface = client_interface,
367
369
ignore_reply=True)
369
371
# elif key == "p" or key == "=":
378
def properties_changed(self, interface, properties, invalidated):
379
"""Call self.update() if any properties changed.
380
def property_changed(self, property=None, **kwargs):
381
"""Call self.update() if old value is not new value.
380
382
This overrides the method from MandosClientPropertyCache"""
381
old_values = {key: self.properties.get(key)
382
for key in properties.keys()}
383
super(MandosClientWidget, self).properties_changed(
384
interface, properties, invalidated)
385
if any(old_values[key] != self.properties.get(key)
386
for key in old_values):
383
property_name = str(property)
384
old_value = self.properties.get(property_name)
385
super(MandosClientWidget, self).property_changed(
386
property=property, **kwargs)
387
if self.properties.get(property_name) != old_value:
430
429
"bold,underline,blink,standout", "default",
431
430
"bold,underline,blink,standout"),
434
433
if urwid.supports_unicode():
435
self.divider = "─" # \u2500
434
self.divider = "─" # \u2500
435
#self.divider = "━" # \u2501
437
self.divider = "_" # \u005f
437
#self.divider = "-" # \u002d
438
self.divider = "_" # \u005f
439
440
self.screen.start()
441
442
self.size = self.screen.get_cols_rows()
443
444
self.clients = urwid.SimpleListWalker([])
444
445
self.clients_dict = {}
446
447
# We will add Text widgets to this list
447
self.log = urwid.SimpleListWalker([])
448
449
self.max_log_length = max_log_length
450
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 = GLib.MainLoop()
467
self.main_loop = gobject.MainLoop()
470
469
def client_not_found(self, fingerprint, address):
471
self.log_message("Client with address {} and fingerprint {}"
472
" could not be found"
470
self.log_message("Client with address {0} and fingerprint"
471
" {1} 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(),
484
footer=urwid.Divider(
485
div_char=self.divider)))
484
urwid.Divider(div_char=
486
486
if self.log_visible:
487
487
self.uilist.append(self.logbox)
488
488
self.topwidget = urwid.Pile(self.uilist)
490
def log_message(self, message, level=1):
490
def log_message(self, message):
491
491
"""Log message formatted with timestamp"""
492
if level < self.log_level:
494
492
timestamp = datetime.datetime.now().isoformat()
495
self.log_message_raw("{}: {}".format(timestamp, message),
498
def log_message_raw(self, markup, level=1):
493
self.log_message_raw(timestamp + ": " + message)
495
def log_message_raw(self, markup):
499
496
"""Add a log message to the log buffer."""
500
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
self.log_message("Log visibility changed to: {}"
515
.format(self.log_visible), level=0)
509
#self.log_message("Log visibility changed to: "
510
# + str(self.log_visible))
517
512
def change_log_display(self):
518
513
"""Change type of log display.
519
514
Currently, this toggles wrapping of text lines."""
523
518
self.log_wrap = "clip"
524
519
for textwidget in self.log:
525
520
textwidget.set_wrap_mode(self.log_wrap)
526
self.log_message("Wrap mode: {}".format(self.log_wrap),
529
def find_and_remove_client(self, path, interfaces):
521
#self.log_message("Wrap mode: " + self.log_wrap)
523
def find_and_remove_client(self, path, name):
530
524
"""Find a client by its object path and remove it.
532
This is connected to the InterfacesRemoved signal from the
526
This is connected to the ClientRemoved signal from the
533
527
Mandos server object."""
534
if client_interface not in interfaces:
535
# Not a Mandos client object; ignore
538
529
client = self.clients_dict[path]
541
self.log_message("Unknown client {!r} removed"
532
self.log_message("Unknown client {0!r} ({1!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
537
def add_new_client(self, path):
555
538
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])),
539
self.add_client(MandosClientWidget(server_proxy_object
542
=client_proxy_object,
565
551
def add_client(self, client, path=None):
566
552
self.clients.append(client)
569
555
self.clients_dict[path] = client
570
556
self.clients.sort(key=lambda c: c.properties["Name"])
573
559
def remove_client(self, client, path=None):
574
560
self.clients.remove(client)
576
562
path = client.proxy.object_path
577
563
del self.clients_dict[path]
580
566
def refresh(self):
581
567
"""Redraw the screen"""
582
568
canvas = self.topwidget.render(self.size, focus=True)
583
569
self.screen.draw_screen(self.size, canvas)
586
572
"""Start the main loop and exit when it's done."""
587
573
self.bus = dbus.SystemBus()
588
574
mandos_dbus_objc = self.bus.get_object(
589
575
self.busname, "/", follow_name_owner_changes=True)
590
self.mandos_serv = dbus.Interface(
591
mandos_dbus_objc, dbus_interface=server_interface)
576
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
593
580
mandos_clients = (self.mandos_serv
594
581
.GetAllClientsWithProperties())
595
582
if not mandos_clients:
596
self.log_message_raw(("bold",
597
"Note: Server has no clients."))
583
self.log_message_raw(("bold", "Note: Server has no clients."))
598
584
except dbus.exceptions.DBusException:
599
self.log_message_raw(("bold",
600
"Note: No Mandos server running."))
585
self.log_message_raw(("bold", "Note: No Mandos server running."))
601
586
mandos_clients = dbus.Dictionary()
603
588
(self.mandos_serv
604
.connect_to_signal("InterfacesRemoved",
589
.connect_to_signal("ClientRemoved",
605
590
self.find_and_remove_client,
606
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
591
dbus_interface=server_interface,
607
592
byte_arrays=True))
608
593
(self.mandos_serv
609
.connect_to_signal("InterfacesAdded",
594
.connect_to_signal("ClientAdded",
610
595
self.add_new_client,
611
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
596
dbus_interface=server_interface,
612
597
byte_arrays=True))
613
598
(self.mandos_serv
614
599
.connect_to_signal("ClientNotFound",
618
603
for path, client in mandos_clients.items():
619
604
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),
606
self.add_client(MandosClientWidget(server_proxy_object
609
=client_proxy_object,
631
self._input_callback_tag = (
633
GLib.IOChannel.unix_new(sys.stdin.fileno()),
634
GLib.PRIORITY_DEFAULT, GLib.IO_IN,
620
self._input_callback_tag = (gobject.io_add_watch
636
624
self.main_loop.run()
637
625
# 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)
626
gobject.source_remove(self._input_callback_tag)
644
630
self.main_loop.quit()
646
632
def process_input(self, source, condition):
647
633
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
634
translations = { "ctrl n": "down", # Emacs
635
"ctrl p": "up", # Emacs
636
"ctrl v": "page down", # Emacs
637
"meta v": "page up", # Emacs
638
" ": "page down", # less
639
"f": "page down", # less
640
"b": "page up", # less
660
646
key = translations[key]
661
647
except KeyError: # :-)
664
650
if key == "q" or key == "Q":
667
653
elif key == "window resize":
668
654
self.size = self.screen.get_cols_rows()
670
elif key == "ctrl l":
656
elif key == "\f": # Ctrl-L
673
658
elif key == "l" or key == "D":
674
659
self.toggle_log_display()
708
691
self.topwidget.set_focus(self.logbox)
711
if self.log_level == 0:
713
self.log_message("Verbose mode: Off")
716
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
693
#elif (key == "end" or key == "meta >" or key == "G"
695
# pass # xxx end-of-buffer
696
#elif (key == "home" or key == "meta <" or key == "g"
698
# pass # xxx beginning-of-buffer
699
#elif key == "ctrl e" or key == "$":
700
# pass # xxx move-end-of-line
701
#elif key == "ctrl a" or key == "^":
702
# pass # xxx move-beginning-of-line
703
#elif key == "ctrl b" or key == "meta (" or key == "h":
705
#elif key == "ctrl f" or key == "meta )" or key == "l":
708
# pass # scroll up log
710
# pass # scroll down log
735
711
elif self.topwidget.selectable():
736
712
self.topwidget.keypress(self.size, key)
741
716
ui = UserInterface()