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()
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))
467
self.main_loop = gobject.MainLoop()
469
def client_not_found(self, fingerprint, address):
470
self.log_message("Client with address {0} and fingerprint"
471
" {1} could not be found"
472
.format(address, fingerprint))
474
474
def rebuild(self):
475
475
"""This rebuilds the User Interface.
476
476
Call this when the widget layout needs to change"""
478
# self.uilist.append(urwid.ListBox(self.clients))
478
#self.uilist.append(urwid.ListBox(self.clients))
479
479
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
481
# header=urwid.Divider(),
481
#header=urwid.Divider(),
483
footer=urwid.Divider(
484
div_char=self.divider)))
484
urwid.Divider(div_char=
485
486
if self.log_visible:
486
487
self.uilist.append(self.logbox)
487
488
self.topwidget = urwid.Pile(self.uilist)
489
def log_message(self, message, level=1):
490
def log_message(self, message):
490
491
"""Log message formatted with timestamp"""
491
if level < self.log_level:
493
492
timestamp = datetime.datetime.now().isoformat()
494
self.log_message_raw("{}: {}".format(timestamp, message),
497
def log_message_raw(self, markup, level=1):
493
self.log_message_raw(timestamp + ": " + message)
495
def log_message_raw(self, markup):
498
496
"""Add a log message to the log buffer."""
499
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
self.log_message("Log visibility changed to: {}"
514
.format(self.log_visible), level=0)
509
#self.log_message("Log visibility changed to: "
510
# + str(self.log_visible))
516
512
def change_log_display(self):
517
513
"""Change type of log display.
518
514
Currently, this toggles wrapping of text lines."""
522
518
self.log_wrap = "clip"
523
519
for textwidget in self.log:
524
520
textwidget.set_wrap_mode(self.log_wrap)
525
self.log_message("Wrap mode: {}".format(self.log_wrap),
528
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):
529
524
"""Find a client by its object path and remove it.
531
This is connected to the InterfacesRemoved signal from the
526
This is connected to the ClientRemoved signal from the
532
527
Mandos server object."""
533
if client_interface not in interfaces:
534
# Not a Mandos client object; ignore
537
529
client = self.clients_dict[path]
540
self.log_message("Unknown client {!r} removed"
532
self.log_message("Unknown client {0!r} ({1!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
537
def add_new_client(self, path):
554
538
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])),
539
self.add_client(MandosClientWidget(server_proxy_object
542
=client_proxy_object,
564
551
def add_client(self, client, path=None):
565
552
self.clients.append(client)
568
555
self.clients_dict[path] = client
569
556
self.clients.sort(key=lambda c: c.properties["Name"])
572
559
def remove_client(self, client, path=None):
573
560
self.clients.remove(client)
575
562
path = client.proxy.object_path
576
563
del self.clients_dict[path]
579
566
def refresh(self):
580
567
"""Redraw the screen"""
581
568
canvas = self.topwidget.render(self.size, focus=True)
582
569
self.screen.draw_screen(self.size, canvas)
585
572
"""Start the main loop and exit when it's done."""
586
573
self.bus = dbus.SystemBus()
587
574
mandos_dbus_objc = self.bus.get_object(
588
575
self.busname, "/", follow_name_owner_changes=True)
589
self.mandos_serv = dbus.Interface(
590
mandos_dbus_objc, dbus_interface=server_interface)
576
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
592
580
mandos_clients = (self.mandos_serv
593
581
.GetAllClientsWithProperties())
594
582
if not mandos_clients:
595
self.log_message_raw(("bold",
596
"Note: Server has no clients."))
583
self.log_message_raw(("bold", "Note: Server has no clients."))
597
584
except dbus.exceptions.DBusException:
598
self.log_message_raw(("bold",
599
"Note: No Mandos server running."))
585
self.log_message_raw(("bold", "Note: No Mandos server running."))
600
586
mandos_clients = dbus.Dictionary()
602
588
(self.mandos_serv
603
.connect_to_signal("InterfacesRemoved",
589
.connect_to_signal("ClientRemoved",
604
590
self.find_and_remove_client,
605
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
591
dbus_interface=server_interface,
606
592
byte_arrays=True))
607
593
(self.mandos_serv
608
.connect_to_signal("InterfacesAdded",
594
.connect_to_signal("ClientAdded",
609
595
self.add_new_client,
610
dbus_interface=dbus.OBJECT_MANAGER_IFACE,
596
dbus_interface=server_interface,
611
597
byte_arrays=True))
612
598
(self.mandos_serv
613
599
.connect_to_signal("ClientNotFound",
617
603
for path, client in mandos_clients.items():
618
604
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),
606
self.add_client(MandosClientWidget(server_proxy_object
609
=client_proxy_object,
630
self._input_callback_tag = (
632
GLib.IOChannel.unix_new(sys.stdin.fileno()),
633
GLib.PRIORITY_DEFAULT, GLib.IO_IN,
620
self._input_callback_tag = (gobject.io_add_watch
635
624
self.main_loop.run()
636
625
# 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)
626
gobject.source_remove(self._input_callback_tag)
643
630
self.main_loop.quit()
645
632
def process_input(self, source, condition):
646
633
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
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
659
646
key = translations[key]
660
647
except KeyError: # :-)
663
650
if key == "q" or key == "Q":
666
653
elif key == "window resize":
667
654
self.size = self.screen.get_cols_rows()
669
elif key == "ctrl l":
656
elif key == "\f": # Ctrl-L
672
658
elif key == "l" or key == "D":
673
659
self.toggle_log_display()
707
691
self.topwidget.set_focus(self.logbox)
710
if self.log_level == 0:
712
self.log_message("Verbose mode: Off")
715
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
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
734
711
elif self.topwidget.selectable():
735
712
self.topwidget.keypress(self.size, key)
740
716
ui = UserInterface()