4
4
# Mandos Monitor - Control and monitor the Mandos server
6
# Copyright © 2009-2012 Teddy Hogeborn
7
# Copyright © 2009-2012 Björn Påhlsson
6
# Copyright © 2009-2014 Teddy Hogeborn
7
# Copyright © 2009-2014 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
55
60
domain = 'se.recompile'
56
61
server_interface = domain + '.Mandos'
57
62
client_interface = domain + '.Mandos.Client'
60
# Always run in monochrome mode
61
urwid.curses_display.curses.has_colors = lambda : False
63
# Urwid doesn't support blinking, but we want it. Since we have no
64
# use for underline on its own, we make underline also always blink.
65
urwid.curses_display.curses.A_UNDERLINE |= (
66
urwid.curses_display.curses.A_BLINK)
68
65
def isoformat_to_datetime(iso):
69
66
"Parse an ISO 8601 date string to a datetime.datetime()"
86
83
properties and calls a hook function when any of them are
89
def __init__(self, proxy_object=None, *args, **kwargs):
86
def __init__(self, proxy_object=None, properties=None, **kwargs):
90
87
self.proxy = proxy_object # Mandos Client proxy object
92
self.properties = dict()
88
self.properties = dict() if properties is None else properties
93
89
self.property_changed_match = (
94
90
self.proxy.connect_to_signal("PropertyChanged",
95
self.property_changed,
91
self._property_changed,
99
self.properties.update(
100
self.proxy.GetAll(client_interface,
101
dbus_interface = dbus.PROPERTIES_IFACE))
103
#XXX This breaks good super behaviour
104
# super(MandosClientPropertyCache, self).__init__(
95
if properties is None:
96
self.properties.update(
97
self.proxy.GetAll(client_interface,
99
= dbus.PROPERTIES_IFACE))
101
super(MandosClientPropertyCache, self).__init__(**kwargs)
103
def _property_changed(self, property, value):
104
"""Helper which takes positional arguments"""
105
return self.property_changed(property=property, value=value)
107
107
def property_changed(self, property=None, value=None):
108
108
"""This is called whenever we get a PropertyChanged signal
111
111
# Update properties dict with new value
112
112
self.properties[property] = value
114
def delete(self, *args, **kwargs):
115
115
self.property_changed_match.remove()
116
super(MandosClientPropertyCache, self).__init__(
120
118
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
124
122
def __init__(self, server_proxy_object=None, update_hook=None,
125
delete_hook=None, logger=None, *args, **kwargs):
123
delete_hook=None, logger=None, **kwargs):
126
124
# Called on update
127
125
self.update_hook = update_hook
128
126
# Called on delete
133
131
self.logger = logger
135
133
self._update_timer_callback_tag = None
136
self._update_timer_callback_lock = 0
138
135
# The widget shown normally
139
136
self._text_widget = urwid.Text("")
140
137
# The widget shown when we have focus
141
138
self._focus_text_widget = urwid.Text("")
142
super(MandosClientWidget, self).__init__(
143
update_hook=update_hook, delete_hook=delete_hook,
139
super(MandosClientWidget, self).__init__(**kwargs)
146
141
self.opened = False
148
last_checked_ok = isoformat_to_datetime(self.properties
151
if self.properties ["LastCheckerStatus"] != 0:
152
self.using_timer(True)
154
if self.need_approval:
155
self.using_timer(True)
157
143
self.match_objects = (
158
144
self.proxy.connect_to_signal("CheckerCompleted",
159
145
self.checker_completed,
176
162
client_interface,
177
163
byte_arrays=True))
178
#self.logger('Created client {0}'
179
# .format(self.properties["Name"]))
181
def property_changed(self, property=None, value=None):
182
super(self, MandosClientWidget).property_changed(property,
184
if property == "ApprovalPending":
185
using_timer(bool(value))
186
if property == "LastCheckerStatus":
187
using_timer(value != 0)
188
#self.logger('Checker for client {0} (command "{1}") was '
189
# ' successful'.format(self.properties["Name"],
164
self.logger('Created client {0}'
165
.format(self.properties["Name"]), level=0)
192
167
def using_timer(self, flag):
193
168
"""Call this method with True or False when timer should be
194
169
activated or deactivated.
196
old = self._update_timer_callback_lock
198
self._update_timer_callback_lock += 1
200
self._update_timer_callback_lock -= 1
201
if old == 0 and self._update_timer_callback_lock:
171
if flag and self._update_timer_callback_tag is None:
202
172
# Will update the shown timer value every second
203
173
self._update_timer_callback_tag = (gobject.timeout_add
205
175
self.update_timer))
206
elif old and self._update_timer_callback_lock == 0:
176
elif not (flag or self._update_timer_callback_tag is None):
207
177
gobject.source_remove(self._update_timer_callback_tag)
208
178
self._update_timer_callback_tag = None
210
180
def checker_completed(self, exitstatus, condition, command):
211
181
if exitstatus == 0:
182
self.logger('Checker for client {0} (command "{1}")'
183
' succeeded'.format(self.properties["Name"],
235
208
def checker_started(self, command):
236
"""Server signals that a checker started. This could be useful
237
to log in the future. """
238
#self.logger('Client {0} started checker "{1}"'
239
# .format(self.properties["Name"],
209
"""Server signals that a checker started."""
210
self.logger('Client {0} started checker "{1}"'
211
.format(self.properties["Name"],
243
214
def got_secret(self):
244
215
self.logger('Client {0} received its secret'
283
253
"bold-underline-blink":
284
254
"bold-underline-blink-standout",
287
257
# Rebuild focus and non-focus widgets using current properties
289
259
# Base part of a client. Name!
290
260
base = '{name}: '.format(name=self.properties["Name"])
291
261
if not self.properties["Enabled"]:
292
262
message = "DISABLED"
263
self.using_timer(False)
293
264
elif self.properties["ApprovalPending"]:
294
265
timeout = datetime.timedelta(milliseconds
295
266
= self.properties
297
268
last_approval_request = isoformat_to_datetime(
298
269
self.properties["LastApprovalRequest"])
299
270
if last_approval_request is not None:
300
timer = timeout - (datetime.datetime.utcnow()
301
- last_approval_request)
271
timer = max(timeout - (datetime.datetime.utcnow()
272
- last_approval_request),
273
datetime.timedelta())
303
275
timer = datetime.timedelta()
304
276
if self.properties["ApprovedByDefault"]:
305
277
message = "Approval in {0}. (d)eny?"
307
279
message = "Denial in {0}. (a)pprove?"
308
message = message.format(unicode(timer).rsplit(".", 1)[0])
280
message = message.format(str(timer).rsplit(".", 1)[0])
281
self.using_timer(True)
309
282
elif self.properties["LastCheckerStatus"] != 0:
310
283
# When checker has failed, show timer until client expires
311
284
expires = self.properties["Expires"]
315
288
expires = (datetime.datetime.strptime
316
289
(expires, '%Y-%m-%dT%H:%M:%S.%f'))
317
timer = expires - datetime.datetime.utcnow()
290
timer = max(expires - datetime.datetime.utcnow(),
291
datetime.timedelta())
318
292
message = ('A checker has failed! Time until client'
319
293
' gets disabled: {0}'
320
.format(unicode(timer).rsplit(".", 1)[0]))
294
.format(str(timer).rsplit(".", 1)[0]))
295
self.using_timer(True)
322
297
message = "enabled"
298
self.using_timer(False)
323
299
self._text = "{0}{1}".format(base, message)
325
301
if not urwid.supports_unicode():
326
302
self._text = self._text.encode("ascii", "replace")
327
303
textlist = [("normal", self._text)]
345
321
return True # Keep calling this
347
def delete(self, *args, **kwargs):
323
def delete(self, **kwargs):
348
324
if self._update_timer_callback_tag is not None:
349
325
gobject.source_remove(self._update_timer_callback_tag)
350
326
self._update_timer_callback_tag = None
353
329
self.match_objects = ()
354
330
if self.delete_hook is not None:
355
331
self.delete_hook(self)
356
return super(MandosClientWidget, self).delete(*args, **kwargs)
332
return super(MandosClientWidget, self).delete(**kwargs)
358
334
def render(self, maxcolrow, focus=False):
359
335
"""Render differently if we have focus.
404
def property_changed(self, property=None, value=None,
380
def property_changed(self, property=None, **kwargs):
406
381
"""Call self.update() if old value is not new value.
407
382
This overrides the method from MandosClientPropertyCache"""
408
property_name = unicode(property)
383
property_name = str(property)
409
384
old_value = self.properties.get(property_name)
410
385
super(MandosClientWidget, self).property_changed(
411
property=property, value=value, *args, **kwargs)
386
property=property, **kwargs)
412
387
if self.properties.get(property_name) != old_value:
418
393
"down" key presses, thus not allowing any containing widgets to
419
394
use them as an excuse to shift focus away from this widget.
421
def keypress(self, maxcolrow, key):
422
ret = super(ConstrainedListBox, self).keypress(maxcolrow, key)
396
def keypress(self, *args, **kwargs):
397
ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
423
398
if ret in ("up", "down"):
429
404
"""This is the entire user interface - the whole screen
430
405
with boxes, lists of client widgets, etc.
432
def __init__(self, max_log_length=1000):
407
def __init__(self, max_log_length=1000, log_level=1):
433
408
DBusGMainLoop(set_as_default=True)
435
410
self.screen = urwid.curses_display.Screen()
439
414
"default", "default", None),
441
"default", "default", "bold"),
416
"bold", "default", "bold"),
442
417
("underline-blink",
443
"default", "default", "underline"),
418
"underline,blink", "default", "underline,blink"),
445
"default", "default", "standout"),
420
"standout", "default", "standout"),
446
421
("bold-underline-blink",
447
"default", "default", ("bold", "underline")),
422
"bold,underline,blink", "default", "bold,underline,blink"),
448
423
("bold-standout",
449
"default", "default", ("bold", "standout")),
424
"bold,standout", "default", "bold,standout"),
450
425
("underline-blink-standout",
451
"default", "default", ("underline", "standout")),
426
"underline,blink,standout", "default",
427
"underline,blink,standout"),
452
428
("bold-underline-blink-standout",
453
"default", "default", ("bold", "underline",
429
"bold,underline,blink,standout", "default",
430
"bold,underline,blink,standout"),
457
433
if urwid.supports_unicode():
511
489
self.uilist.append(self.logbox)
512
490
self.topwidget = urwid.Pile(self.uilist)
514
def log_message(self, message):
492
def log_message(self, message, level=1):
493
"""Log message formatted with timestamp"""
494
if level < self.log_level:
515
496
timestamp = datetime.datetime.now().isoformat()
516
self.log_message_raw(timestamp + ": " + message)
497
self.log_message_raw("{0}: {1}".format(timestamp, message),
518
def log_message_raw(self, markup):
500
def log_message_raw(self, markup, level=1):
519
501
"""Add a log message to the log buffer."""
502
if level < self.log_level:
520
504
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
521
505
if (self.max_log_length
522
506
and len(self.log) > self.max_log_length):
529
513
"""Toggle visibility of the log buffer."""
530
514
self.log_visible = not self.log_visible
532
#self.log_message("Log visibility changed to: "
533
# + unicode(self.log_visible))
516
self.log_message("Log visibility changed to: {0}"
517
.format(self.log_visible), level=0)
535
519
def change_log_display(self):
536
520
"""Change type of log display.
541
525
self.log_wrap = "clip"
542
526
for textwidget in self.log:
543
527
textwidget.set_wrap_mode(self.log_wrap)
544
#self.log_message("Wrap mode: " + self.log_wrap)
528
self.log_message("Wrap mode: {0}".format(self.log_wrap),
546
531
def find_and_remove_client(self, path, name):
547
532
"""Find a client by its object path and remove it.
608
588
mandos_clients = (self.mandos_serv
609
589
.GetAllClientsWithProperties())
590
if not mandos_clients:
591
self.log_message_raw(("bold", "Note: Server has no clients."))
610
592
except dbus.exceptions.DBusException:
593
self.log_message_raw(("bold", "Note: No Mandos server running."))
611
594
mandos_clients = dbus.Dictionary()
613
596
(self.mandos_serv
625
608
self.client_not_found,
626
609
dbus_interface=server_interface,
627
610
byte_arrays=True))
628
for path, client in mandos_clients.iteritems():
611
for path, client in mandos_clients.items():
629
612
client_proxy_object = self.bus.get_object(self.busname,
631
614
self.add_client(MandosClientWidget(server_proxy_object