2
2
# -*- mode: python; coding: utf-8 -*-
4
# Mandos Monitor - Control and monitor the Mandos server
6
# Copyright © 2009-2012 Teddy Hogeborn
7
# Copyright © 2009-2012 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
11
# the Free Software Foundation, either version 3 of the License, or
12
# (at your option) any later version.
14
# This program is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# GNU General Public License for more details.
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/>.
22
# Contact the authors at <mandos@recompile.se>.
4
from __future__ import division, absolute_import, with_statement
25
from __future__ import (division, absolute_import, print_function,
33
62
urwid.curses_display.curses.A_UNDERLINE |= (
34
63
urwid.curses_display.curses.A_BLINK)
65
def isoformat_to_datetime(iso):
66
"Parse an ISO 8601 date string to a datetime.datetime()"
69
d, t = iso.split("T", 1)
70
year, month, day = d.split("-", 2)
71
hour, minute, second = t.split(":", 2)
72
second, fraction = divmod(float(second), 1)
73
return datetime.datetime(int(year),
78
int(second), # Whole seconds
79
int(fraction*1000000)) # Microseconds
36
81
class MandosClientPropertyCache(object):
37
82
"""This wraps a Mandos Client D-Bus proxy object, caches the
38
83
properties and calls a hook function when any of them are
41
def __init__(self, proxy_object=None, properties=None, *args,
86
def __init__(self, proxy_object=None, *args, **kwargs):
43
87
self.proxy = proxy_object # Mandos Client proxy object
45
if properties is None:
46
self.properties = dict()
48
self.properties = properties
49
self.proxy.connect_to_signal(u"PropertyChanged",
50
self.property_changed,
89
self.properties = dict()
90
self.property_changed_match = (
91
self.proxy.connect_to_signal("PropertyChanged",
92
self.property_changed,
54
if properties is None:
55
self.properties.update(self.proxy.GetAll(client_interface,
57
dbus.PROPERTIES_IFACE))
58
super(MandosClientPropertyCache, self).__init__(
59
proxy_object=proxy_object,
60
properties=properties, *args, **kwargs)
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__(
62
104
def property_changed(self, property=None, value=None):
63
105
"""This is called whenever we get a PropertyChanged signal
83
130
self.logger = logger
132
self._update_timer_callback_tag = None
133
self._update_timer_callback_lock = 0
85
135
# The widget shown normally
86
self._text_widget = urwid.Text(u"")
136
self._text_widget = urwid.Text("")
87
137
# The widget shown when we have focus
88
self._focus_text_widget = urwid.Text(u"")
138
self._focus_text_widget = urwid.Text("")
89
139
super(MandosClientWidget, self).__init__(
90
140
update_hook=update_hook, delete_hook=delete_hook,
93
143
self.opened = False
94
self.proxy.connect_to_signal(u"CheckerCompleted",
95
self.checker_completed,
98
self.proxy.connect_to_signal(u"CheckerStarted",
102
self.proxy.connect_to_signal(u"GotSecret",
106
self.proxy.connect_to_signal(u"Rejected",
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
self.match_objects = (
155
self.proxy.connect_to_signal("CheckerCompleted",
156
self.checker_completed,
159
self.proxy.connect_to_signal("CheckerStarted",
160
self.checker_started,
163
self.proxy.connect_to_signal("GotSecret",
167
self.proxy.connect_to_signal("NeedApproval",
171
self.proxy.connect_to_signal("Rejected",
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"],
189
def using_timer(self, flag):
190
"""Call this method with True or False when timer should be
191
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:
199
# Will update the shown timer value every second
200
self._update_timer_callback_tag = (gobject.timeout_add
203
elif old and self._update_timer_callback_lock == 0:
204
gobject.source_remove(self._update_timer_callback_tag)
205
self._update_timer_callback_tag = None
111
207
def checker_completed(self, exitstatus, condition, command):
112
208
if exitstatus == 0:
113
self.logger(u'Checker for client %s (command "%s")'
115
% (self.properties[u"name"], command))
117
212
if os.WIFEXITED(condition):
118
self.logger(u'Checker for client %s (command "%s")'
119
u' failed with exit code %s'
120
% (self.properties[u"name"], command,
121
os.WEXITSTATUS(condition)))
123
if os.WIFSIGNALED(condition):
124
self.logger(u'Checker for client %s (command "%s")'
125
u' was killed by signal %s'
126
% (self.properties[u"name"], command,
127
os.WTERMSIG(condition)))
129
if os.WCOREDUMP(condition):
130
self.logger(u'Checker for client %s (command "%s")'
132
% (self.properties[u"name"], command))
133
self.logger(u'Checker for client %s completed mysteriously')
213
self.logger('Checker for client {0} (command "{1}")'
214
' failed with exit code {2}'
215
.format(self.properties["Name"], command,
216
os.WEXITSTATUS(condition)))
217
elif os.WIFSIGNALED(condition):
218
self.logger('Checker for client {0} (command "{1}") was'
219
' killed by signal {2}'
220
.format(self.properties["Name"], command,
221
os.WTERMSIG(condition)))
222
elif os.WCOREDUMP(condition):
223
self.logger('Checker for client {0} (command "{1}")'
225
.format(self.properties["Name"], command))
227
self.logger('Checker for client {0} completed'
229
.format(self.properties["Name"]))
135
232
def checker_started(self, command):
136
self.logger(u'Client %s started checker "%s"'
137
% (self.properties[u"name"], unicode(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"],
139
240
def got_secret(self):
140
self.logger(u'Client %s received its secret'
141
% self.properties[u"name"])
144
self.logger(u'Client %s was rejected'
145
% self.properties[u"name"])
241
self.logger('Client {0} received its secret'
242
.format(self.properties["Name"]))
244
def need_approval(self, timeout, default):
246
message = 'Client {0} needs approval within {1} seconds'
248
message = 'Client {0} will get its secret in {1} seconds'
249
self.logger(message.format(self.properties["Name"],
251
self.using_timer(True)
253
def rejected(self, reason):
254
self.logger('Client {0} was rejected; reason: {1}'
255
.format(self.properties["Name"], reason))
147
257
def selectable(self):
148
258
"""Make this a "selectable" widget.
149
259
This overrides the method from urwid.FlowWidget."""
152
def rows(self, (maxcol,), focus=False):
262
def rows(self, maxcolrow, focus=False):
153
263
"""How many rows this widget will occupy might depend on
154
264
whether we have focus or not.
155
265
This overrides the method from urwid.FlowWidget"""
156
return self.current_widget(focus).rows((maxcol,), focus=focus)
266
return self.current_widget(focus).rows(maxcolrow, focus=focus)
158
268
def current_widget(self, focus=False):
159
269
if focus or self.opened:
163
273
def update(self):
164
274
"Called when what is visible on the screen should be updated."
165
275
# How to add standout mode to a style
166
with_standout = { u"normal": u"standout",
167
u"bold": u"bold-standout",
169
u"underline-blink-standout",
170
u"bold-underline-blink":
171
u"bold-underline-blink-standout",
276
with_standout = { "normal": "standout",
277
"bold": "bold-standout",
279
"underline-blink-standout",
280
"bold-underline-blink":
281
"bold-underline-blink-standout",
174
284
# Rebuild focus and non-focus widgets using current properties
175
self._text = (u'%(name)s: %(enabled)s'
176
% { u"name": self.properties[u"name"],
179
if self.properties[u"enabled"]
286
# Base part of a client. Name!
287
base = '{name}: '.format(name=self.properties["Name"])
288
if not self.properties["Enabled"]:
290
elif self.properties["ApprovalPending"]:
291
timeout = datetime.timedelta(milliseconds
294
last_approval_request = isoformat_to_datetime(
295
self.properties["LastApprovalRequest"])
296
if last_approval_request is not None:
297
timer = timeout - (datetime.datetime.utcnow()
298
- last_approval_request)
300
timer = datetime.timedelta()
301
if self.properties["ApprovedByDefault"]:
302
message = "Approval in {0}. (d)eny?"
304
message = "Denial in {0}. (a)pprove?"
305
message = message.format(unicode(timer).rsplit(".", 1)[0])
306
elif self.properties["LastCheckerStatus"] != 0:
307
# When checker has failed, print a timer until client expires
308
expires = self.properties["Expires"]
310
timer = datetime.timedelta(0)
312
expires = datetime.datetime.strptime(expires,
313
'%Y-%m-%dT%H:%M:%S.%f')
314
timer = expires - datetime.datetime.utcnow()
315
message = ('A checker has failed! Time until client'
316
' gets disabled: {0}'
317
.format(unicode(timer).rsplit(".", 1)[0]))
320
self._text = "{0}{1}".format(base, message)
181
322
if not urwid.supports_unicode():
182
323
self._text = self._text.encode("ascii", "replace")
183
textlist = [(u"normal", self._text)]
324
textlist = [("normal", self._text)]
184
325
self._text_widget.set_text(textlist)
185
326
self._focus_text_widget.set_text([(with_standout[text[0]],
194
335
if self.update_hook is not None:
195
336
self.update_hook()
338
def update_timer(self):
339
"""called by gobject. Will indefinitely loop until
340
gobject.source_remove() on tag is called"""
342
return True # Keep calling this
344
def delete(self, *args, **kwargs):
345
if self._update_timer_callback_tag is not None:
346
gobject.source_remove(self._update_timer_callback_tag)
347
self._update_timer_callback_tag = None
348
for match in self.match_objects:
350
self.match_objects = ()
198
351
if self.delete_hook is not None:
199
352
self.delete_hook(self)
353
return super(MandosClientWidget, self).delete(*args, **kwargs)
201
def render(self, (maxcol,), focus=False):
355
def render(self, maxcolrow, focus=False):
202
356
"""Render differently if we have focus.
203
357
This overrides the method from urwid.FlowWidget"""
204
return self.current_widget(focus).render((maxcol,),
358
return self.current_widget(focus).render(maxcolrow,
207
def keypress(self, (maxcol,), key):
361
def keypress(self, maxcolrow, key):
209
363
This overrides the method from urwid.FlowWidget"""
210
if key == u"e" or key == u"+":
212
elif key == u"d" or key == u"-":
214
elif key == u"r" or key == u"_" or key == u"ctrl k":
365
self.proxy.Enable(dbus_interface = client_interface,
368
self.proxy.Disable(dbus_interface = client_interface,
371
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
372
dbus_interface = client_interface,
375
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
376
dbus_interface = client_interface,
378
elif key == "R" or key == "_" or key == "ctrl k":
215
379
self.server_proxy_object.RemoveClient(self.proxy
218
self.proxy.StartChecker()
220
self.proxy.StopChecker()
222
self.proxy.CheckedOK()
383
self.proxy.StartChecker(dbus_interface = client_interface,
386
self.proxy.StopChecker(dbus_interface = client_interface,
389
self.proxy.CheckedOK(dbus_interface = client_interface,
224
# elif key == u"p" or key == "=":
392
# elif key == "p" or key == "=":
225
393
# self.proxy.pause()
226
# elif key == u"u" or key == ":":
394
# elif key == "u" or key == ":":
227
395
# self.proxy.unpause()
228
# elif key == u"RET":
264
432
self.screen = urwid.curses_display.Screen()
266
434
self.screen.register_palette((
268
u"default", u"default", None),
270
u"default", u"default", u"bold"),
272
u"default", u"default", u"underline"),
274
u"default", u"default", u"standout"),
275
(u"bold-underline-blink",
276
u"default", u"default", (u"bold", u"underline")),
278
u"default", u"default", (u"bold", u"standout")),
279
(u"underline-blink-standout",
280
u"default", u"default", (u"underline", u"standout")),
281
(u"bold-underline-blink-standout",
282
u"default", u"default", (u"bold", u"underline",
436
"default", "default", None),
438
"default", "default", "bold"),
440
"default", "default", "underline"),
442
"default", "default", "standout"),
443
("bold-underline-blink",
444
"default", "default", ("bold", "underline")),
446
"default", "default", ("bold", "standout")),
447
("underline-blink-standout",
448
"default", "default", ("underline", "standout")),
449
("bold-underline-blink-standout",
450
"default", "default", ("bold", "underline",
286
454
if urwid.supports_unicode():
287
self.divider = u"─" # \u2500
288
#self.divider = u"━" # \u2501
455
self.divider = "─" # \u2500
456
#self.divider = "━" # \u2501
290
#self.divider = u"-" # \u002d
291
self.divider = u"_" # \u005f
458
#self.divider = "-" # \u002d
459
self.divider = "_" # \u005f
293
461
self.screen.start()
308
476
# This keeps track of whether self.uilist currently has
309
477
# self.logbox in it or not
310
478
self.log_visible = True
311
self.log_wrap = u"any"
479
self.log_wrap = "any"
314
self.log_message_raw((u"bold",
315
u"Mandos Monitor version " + version))
316
self.log_message_raw((u"bold",
482
self.log_message_raw(("bold",
483
"Mandos Monitor version " + version))
484
self.log_message_raw(("bold",
319
487
self.busname = domain + '.Mandos'
320
488
self.main_loop = gobject.MainLoop()
321
self.bus = dbus.SystemBus()
322
mandos_dbus_objc = self.bus.get_object(
323
self.busname, u"/", follow_name_owner_changes=True)
324
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
328
mandos_clients = (self.mandos_serv
329
.GetAllClientsWithProperties())
330
except dbus.exceptions.DBusException:
331
mandos_clients = dbus.Dictionary()
334
.connect_to_signal(u"ClientRemoved",
335
self.find_and_remove_client,
336
dbus_interface=server_interface,
339
.connect_to_signal(u"ClientAdded",
341
dbus_interface=server_interface,
344
.connect_to_signal(u"ClientNotFound",
345
self.client_not_found,
346
dbus_interface=server_interface,
348
for path, client in mandos_clients.iteritems():
349
client_proxy_object = self.bus.get_object(self.busname,
351
self.add_client(MandosClientWidget(server_proxy_object
354
=client_proxy_object,
364
490
def client_not_found(self, fingerprint, address):
365
self.log_message((u"Client with address %s and fingerprint %s"
366
u" could not be found" % (address,
491
self.log_message("Client with address {0} and fingerprint"
492
" {1} could not be found"
493
.format(address, fingerprint))
369
495
def rebuild(self):
370
496
"""This rebuilds the User Interface.
371
497
Call this when the widget layout needs to change"""
373
499
#self.uilist.append(urwid.ListBox(self.clients))
374
self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
500
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
375
502
#header=urwid.Divider(),
377
footer=urwid.Divider(div_char=self.divider)))
505
urwid.Divider(div_char=
378
507
if self.log_visible:
379
508
self.uilist.append(self.logbox)
381
509
self.topwidget = urwid.Pile(self.uilist)
383
511
def log_message(self, message):
384
512
timestamp = datetime.datetime.now().isoformat()
385
self.log_message_raw(timestamp + u": " + message)
513
self.log_message_raw(timestamp + ": " + message)
387
515
def log_message_raw(self, markup):
388
516
"""Add a log message to the log buffer."""
391
519
and len(self.log) > self.max_log_length):
392
520
del self.log[0:len(self.log)-self.max_log_length-1]
393
521
self.logbox.set_focus(len(self.logbox.body.contents),
394
coming_from=u"above")
397
525
def toggle_log_display(self):
398
526
"""Toggle visibility of the log buffer."""
399
527
self.log_visible = not self.log_visible
401
self.log_message(u"Log visibility changed to: "
402
+ unicode(self.log_visible))
529
#self.log_message("Log visibility changed to: "
530
# + unicode(self.log_visible))
404
532
def change_log_display(self):
405
533
"""Change type of log display.
406
534
Currently, this toggles wrapping of text lines."""
407
if self.log_wrap == u"clip":
408
self.log_wrap = u"any"
535
if self.log_wrap == "clip":
536
self.log_wrap = "any"
410
self.log_wrap = u"clip"
538
self.log_wrap = "clip"
411
539
for textwidget in self.log:
412
540
textwidget.set_wrap_mode(self.log_wrap)
413
self.log_message(u"Wrap mode: " + self.log_wrap)
541
#self.log_message("Wrap mode: " + self.log_wrap)
415
543
def find_and_remove_client(self, path, name):
416
"""Find an client from its object path and remove it.
544
"""Find a client by its object path and remove it.
418
546
This is connected to the ClientRemoved signal from the
419
547
Mandos server object."""
466
597
"""Start the main loop and exit when it's done."""
598
self.bus = dbus.SystemBus()
599
mandos_dbus_objc = self.bus.get_object(
600
self.busname, "/", follow_name_owner_changes=True)
601
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
605
mandos_clients = (self.mandos_serv
606
.GetAllClientsWithProperties())
607
except dbus.exceptions.DBusException:
608
mandos_clients = dbus.Dictionary()
611
.connect_to_signal("ClientRemoved",
612
self.find_and_remove_client,
613
dbus_interface=server_interface,
616
.connect_to_signal("ClientAdded",
618
dbus_interface=server_interface,
621
.connect_to_signal("ClientNotFound",
622
self.client_not_found,
623
dbus_interface=server_interface,
625
for path, client in mandos_clients.iteritems():
626
client_proxy_object = self.bus.get_object(self.busname,
628
self.add_client(MandosClientWidget(server_proxy_object
631
=client_proxy_object,
468
642
self._input_callback_tag = (gobject.io_add_watch
469
643
(sys.stdin.fileno(),
495
669
except KeyError: # :-)
498
if key == u"q" or key == u"Q":
672
if key == "q" or key == "Q":
501
elif key == u"window resize":
675
elif key == "window resize":
502
676
self.size = self.screen.get_cols_rows()
504
elif key == u"\f": # Ctrl-L
678
elif key == "\f": # Ctrl-L
506
elif key == u"l" or key == u"D":
680
elif key == "l" or key == "D":
507
681
self.toggle_log_display()
509
elif key == u"w" or key == u"i":
683
elif key == "w" or key == "i":
510
684
self.change_log_display()
512
elif key == u"?" or key == u"f1" or key == u"esc":
686
elif key == "?" or key == "f1" or key == "esc":
513
687
if not self.log_visible:
514
688
self.log_visible = True
516
self.log_message_raw((u"bold",
520
u"l: Log window toggle",
521
u"TAB: Switch window",
523
self.log_message_raw((u"bold",
529
u"s: Start new checker",
690
self.log_message_raw(("bold",
694
"l: Log window toggle",
695
"TAB: Switch window",
697
self.log_message_raw(("bold",
703
"s: Start new checker",
534
710
if self.topwidget.get_focus() is self.logbox:
535
711
self.topwidget.set_focus(0)
537
713
self.topwidget.set_focus(self.logbox)
539
#elif (key == u"end" or key == u"meta >" or key == u"G"
715
#elif (key == "end" or key == "meta >" or key == "G"
541
717
# pass # xxx end-of-buffer
542
#elif (key == u"home" or key == u"meta <" or key == u"g"
718
#elif (key == "home" or key == "meta <" or key == "g"
544
720
# pass # xxx beginning-of-buffer
545
#elif key == u"ctrl e" or key == u"$":
721
#elif key == "ctrl e" or key == "$":
546
722
# pass # xxx move-end-of-line
547
#elif key == u"ctrl a" or key == u"^":
723
#elif key == "ctrl a" or key == "^":
548
724
# pass # xxx move-beginning-of-line
549
#elif key == u"ctrl b" or key == u"meta (" or key == u"h":
725
#elif key == "ctrl b" or key == "meta (" or key == "h":
550
726
# pass # xxx left
551
#elif key == u"ctrl f" or key == u"meta )" or key == u"l":
727
#elif key == "ctrl f" or key == "meta )" or key == "l":
552
728
# pass # xxx right
554
730
# pass # scroll up log
556
732
# pass # scroll down log
557
733
elif self.topwidget.selectable():
558
734
self.topwidget.keypress(self.size, key)