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
17
17
# GNU General Public License for more details.
19
19
# You should have received a copy of the GNU General Public License
20
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
# along with this program. If not, see
21
# <http://www.gnu.org/licenses/>.
22
23
# Contact the authors at <mandos@recompile.se>.
25
26
from __future__ import (division, absolute_import, print_function,
29
from future_builtins import *
52
60
domain = 'se.recompile'
53
61
server_interface = domain + '.Mandos'
54
62
client_interface = domain + '.Mandos.Client'
57
# Always run in monochrome mode
58
urwid.curses_display.curses.has_colors = lambda : False
60
# Urwid doesn't support blinking, but we want it. Since we have no
61
# use for underline on its own, we make underline also always blink.
62
urwid.curses_display.curses.A_UNDERLINE |= (
63
urwid.curses_display.curses.A_BLINK)
65
65
def isoformat_to_datetime(iso):
66
66
"Parse an ISO 8601 date string to a datetime.datetime()"
83
83
properties and calls a hook function when any of them are
86
def __init__(self, proxy_object=None, *args, **kwargs):
86
def __init__(self, proxy_object=None, properties=None, **kwargs):
87
87
self.proxy = proxy_object # Mandos Client proxy object
89
self.properties = dict()
88
self.properties = dict() if properties is None else properties
90
89
self.property_changed_match = (
91
90
self.proxy.connect_to_signal("PropertyChanged",
92
self.property_changed,
91
self._property_changed,
96
self.properties.update(
97
self.proxy.GetAll(client_interface,
98
dbus_interface = dbus.PROPERTIES_IFACE))
100
#XXX This breaks good super behaviour
101
# 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)
104
107
def property_changed(self, property=None, value=None):
105
108
"""This is called whenever we get a PropertyChanged signal
108
111
# Update properties dict with new value
109
112
self.properties[property] = value
111
def delete(self, *args, **kwargs):
112
115
self.property_changed_match.remove()
113
super(MandosClientPropertyCache, self).__init__(
117
118
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
121
122
def __init__(self, server_proxy_object=None, update_hook=None,
122
delete_hook=None, logger=None, *args, **kwargs):
123
delete_hook=None, logger=None, **kwargs):
123
124
# Called on update
124
125
self.update_hook = update_hook
125
126
# Called on delete
130
131
self.logger = logger
132
133
self._update_timer_callback_tag = None
133
self._update_timer_callback_lock = 0
135
135
# The widget shown normally
136
136
self._text_widget = urwid.Text("")
137
137
# The widget shown when we have focus
138
138
self._focus_text_widget = urwid.Text("")
139
super(MandosClientWidget, self).__init__(
140
update_hook=update_hook, delete_hook=delete_hook,
139
super(MandosClientWidget, self).__init__(**kwargs)
143
141
self.opened = False
145
last_checked_ok = isoformat_to_datetime(self.properties
148
if self.properties ["LastCheckerStatus"] != 0:
149
self.using_timer(True)
151
if self.need_approval:
152
self.using_timer(True)
154
143
self.match_objects = (
155
144
self.proxy.connect_to_signal("CheckerCompleted",
156
145
self.checker_completed,
173
162
client_interface,
174
163
byte_arrays=True))
175
#self.logger('Created client {0}'
176
# .format(self.properties["Name"]))
178
def property_changed(self, property=None, value=None):
179
super(self, MandosClientWidget).property_changed(property,
181
if property == "ApprovalPending":
182
using_timer(bool(value))
183
if property == "LastCheckerStatus":
184
using_timer(value != 0)
185
#self.logger('Checker for client {0} (command "{1}") was '
186
# ' successful'.format(self.properties["Name"],
164
self.logger('Created client {0}'
165
.format(self.properties["Name"]), level=0)
189
167
def using_timer(self, flag):
190
168
"""Call this method with True or False when timer should be
191
169
activated or deactivated.
193
old = self._update_timer_callback_lock
195
self._update_timer_callback_lock += 1
197
self._update_timer_callback_lock -= 1
198
if old == 0 and self._update_timer_callback_lock:
171
if flag and self._update_timer_callback_tag is None:
199
172
# Will update the shown timer value every second
200
173
self._update_timer_callback_tag = (gobject.timeout_add
202
175
self.update_timer))
203
elif old and self._update_timer_callback_lock == 0:
176
elif not (flag or self._update_timer_callback_tag is None):
204
177
gobject.source_remove(self._update_timer_callback_tag)
205
178
self._update_timer_callback_tag = None
207
180
def checker_completed(self, exitstatus, condition, command):
208
181
if exitstatus == 0:
182
self.logger('Checker for client {0} (command "{1}")'
183
' succeeded'.format(self.properties["Name"],
232
208
def checker_started(self, command):
233
"""Server signals that a checker started. This could be useful
234
to log in the future. """
235
#self.logger('Client {0} started checker "{1}"'
236
# .format(self.properties["Name"],
209
"""Server signals that a checker started."""
210
self.logger('Client {0} started checker "{1}"'
211
.format(self.properties["Name"],
240
214
def got_secret(self):
241
215
self.logger('Client {0} received its secret'
280
253
"bold-underline-blink":
281
254
"bold-underline-blink-standout",
284
257
# Rebuild focus and non-focus widgets using current properties
286
259
# Base part of a client. Name!
287
260
base = '{name}: '.format(name=self.properties["Name"])
288
261
if not self.properties["Enabled"]:
289
262
message = "DISABLED"
263
self.using_timer(False)
290
264
elif self.properties["ApprovalPending"]:
291
265
timeout = datetime.timedelta(milliseconds
292
266
= self.properties
294
268
last_approval_request = isoformat_to_datetime(
295
269
self.properties["LastApprovalRequest"])
296
270
if last_approval_request is not None:
297
timer = timeout - (datetime.datetime.utcnow()
298
- last_approval_request)
271
timer = max(timeout - (datetime.datetime.utcnow()
272
- last_approval_request),
273
datetime.timedelta())
300
275
timer = datetime.timedelta()
301
276
if self.properties["ApprovedByDefault"]:
302
277
message = "Approval in {0}. (d)eny?"
304
279
message = "Denial in {0}. (a)pprove?"
305
message = message.format(unicode(timer).rsplit(".", 1)[0])
280
message = message.format(str(timer).rsplit(".", 1)[0])
281
self.using_timer(True)
306
282
elif self.properties["LastCheckerStatus"] != 0:
307
# When checker has failed, print a timer until client expires
283
# When checker has failed, show timer until client expires
308
284
expires = self.properties["Expires"]
309
285
if expires == "":
310
286
timer = datetime.timedelta(0)
312
expires = datetime.datetime.strptime(expires,
313
'%Y-%m-%dT%H:%M:%S.%f')
314
timer = expires - datetime.datetime.utcnow()
288
expires = (datetime.datetime.strptime
289
(expires, '%Y-%m-%dT%H:%M:%S.%f'))
290
timer = max(expires - datetime.datetime.utcnow(),
291
datetime.timedelta())
315
292
message = ('A checker has failed! Time until client'
316
293
' gets disabled: {0}'
317
.format(unicode(timer).rsplit(".", 1)[0]))
294
.format(str(timer).rsplit(".", 1)[0]))
295
self.using_timer(True)
319
297
message = "enabled"
298
self.using_timer(False)
320
299
self._text = "{0}{1}".format(base, message)
322
301
if not urwid.supports_unicode():
323
302
self._text = self._text.encode("ascii", "replace")
324
303
textlist = [("normal", self._text)]
342
321
return True # Keep calling this
344
def delete(self, *args, **kwargs):
323
def delete(self, **kwargs):
345
324
if self._update_timer_callback_tag is not None:
346
325
gobject.source_remove(self._update_timer_callback_tag)
347
326
self._update_timer_callback_tag = None
350
329
self.match_objects = ()
351
330
if self.delete_hook is not None:
352
331
self.delete_hook(self)
353
return super(MandosClientWidget, self).delete(*args, **kwargs)
332
return super(MandosClientWidget, self).delete(**kwargs)
355
334
def render(self, maxcolrow, focus=False):
356
335
"""Render differently if we have focus.
401
def property_changed(self, property=None, value=None,
380
def property_changed(self, property=None, **kwargs):
403
381
"""Call self.update() if old value is not new value.
404
382
This overrides the method from MandosClientPropertyCache"""
405
property_name = unicode(property)
383
property_name = str(property)
406
384
old_value = self.properties.get(property_name)
407
385
super(MandosClientWidget, self).property_changed(
408
property=property, value=value, *args, **kwargs)
386
property=property, **kwargs)
409
387
if self.properties.get(property_name) != old_value:
415
393
"down" key presses, thus not allowing any containing widgets to
416
394
use them as an excuse to shift focus away from this widget.
418
def keypress(self, maxcolrow, key):
419
ret = super(ConstrainedListBox, self).keypress(maxcolrow, key)
396
def keypress(self, *args, **kwargs):
397
ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
420
398
if ret in ("up", "down"):
426
404
"""This is the entire user interface - the whole screen
427
405
with boxes, lists of client widgets, etc.
429
def __init__(self, max_log_length=1000):
407
def __init__(self, max_log_length=1000, log_level=1):
430
408
DBusGMainLoop(set_as_default=True)
432
410
self.screen = urwid.curses_display.Screen()
436
414
"default", "default", None),
438
"default", "default", "bold"),
416
"bold", "default", "bold"),
439
417
("underline-blink",
440
"default", "default", "underline"),
418
"underline,blink", "default", "underline,blink"),
442
"default", "default", "standout"),
420
"standout", "default", "standout"),
443
421
("bold-underline-blink",
444
"default", "default", ("bold", "underline")),
422
"bold,underline,blink", "default", "bold,underline,blink"),
445
423
("bold-standout",
446
"default", "default", ("bold", "standout")),
424
"bold,standout", "default", "bold,standout"),
447
425
("underline-blink-standout",
448
"default", "default", ("underline", "standout")),
426
"underline,blink,standout", "default",
427
"underline,blink,standout"),
449
428
("bold-underline-blink-standout",
450
"default", "default", ("bold", "underline",
429
"bold,underline,blink,standout", "default",
430
"bold,underline,blink,standout"),
454
433
if urwid.supports_unicode():
508
489
self.uilist.append(self.logbox)
509
490
self.topwidget = urwid.Pile(self.uilist)
511
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:
512
496
timestamp = datetime.datetime.now().isoformat()
513
self.log_message_raw(timestamp + ": " + message)
497
self.log_message_raw("{0}: {1}".format(timestamp, message),
515
def log_message_raw(self, markup):
500
def log_message_raw(self, markup, level=1):
516
501
"""Add a log message to the log buffer."""
502
if level < self.log_level:
517
504
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
518
505
if (self.max_log_length
519
506
and len(self.log) > self.max_log_length):
526
513
"""Toggle visibility of the log buffer."""
527
514
self.log_visible = not self.log_visible
529
#self.log_message("Log visibility changed to: "
530
# + unicode(self.log_visible))
516
self.log_message("Log visibility changed to: {0}"
517
.format(self.log_visible), level=0)
532
519
def change_log_display(self):
533
520
"""Change type of log display.
538
525
self.log_wrap = "clip"
539
526
for textwidget in self.log:
540
527
textwidget.set_wrap_mode(self.log_wrap)
541
#self.log_message("Wrap mode: " + self.log_wrap)
528
self.log_message("Wrap mode: {0}".format(self.log_wrap),
543
531
def find_and_remove_client(self, path, name):
544
532
"""Find a client by its object path and remove it.
605
588
mandos_clients = (self.mandos_serv
606
589
.GetAllClientsWithProperties())
590
if not mandos_clients:
591
self.log_message_raw(("bold", "Note: Server has no clients."))
607
592
except dbus.exceptions.DBusException:
593
self.log_message_raw(("bold", "Note: No Mandos server running."))
608
594
mandos_clients = dbus.Dictionary()
610
596
(self.mandos_serv
622
608
self.client_not_found,
623
609
dbus_interface=server_interface,
624
610
byte_arrays=True))
625
for path, client in mandos_clients.iteritems():
611
for path, client in mandos_clients.items():
626
612
client_proxy_object = self.bus.get_object(self.busname,
628
614
self.add_client(MandosClientWidget(server_proxy_object