2
2
# -*- mode: python; coding: utf-8 -*-
4
from __future__ import division, absolute_import, with_statement
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
21
# <http://www.gnu.org/licenses/>.
23
# Contact the authors at <mandos@recompile.se>.
26
from __future__ import (division, absolute_import, print_function,
29
from future_builtins import *
9
37
import urwid.curses_display
30
65
urwid.curses_display.curses.A_UNDERLINE |= (
31
66
urwid.curses_display.curses.A_BLINK)
68
def isoformat_to_datetime(iso):
69
"Parse an ISO 8601 date string to a datetime.datetime()"
72
d, t = iso.split("T", 1)
73
year, month, day = d.split("-", 2)
74
hour, minute, second = t.split(":", 2)
75
second, fraction = divmod(float(second), 1)
76
return datetime.datetime(int(year),
81
int(second), # Whole seconds
82
int(fraction*1000000)) # Microseconds
33
84
class MandosClientPropertyCache(object):
34
85
"""This wraps a Mandos Client D-Bus proxy object, caches the
35
86
properties and calls a hook function when any of them are
38
def __init__(self, proxy_object=None, properties=None, *args,
89
def __init__(self, proxy_object=None, properties=None, **kwargs):
40
90
self.proxy = proxy_object # Mandos Client proxy object
42
if properties is None:
43
self.properties = dict()
45
self.properties = properties
46
self.proxy.connect_to_signal("PropertyChanged",
47
self.property_changed,
51
if properties is None:
52
self.properties.update(self.proxy.GetAll(client_interface,
54
dbus.PROPERTIES_IFACE))
55
super(MandosClientPropertyCache, self).__init__(
56
proxy_object=proxy_object,
57
properties=properties, *args, **kwargs)
91
self.properties = dict() if properties is None else properties
92
self.property_changed_match = (
93
self.proxy.connect_to_signal("PropertyChanged",
94
self._property_changed,
98
if properties is None:
99
self.properties.update(
100
self.proxy.GetAll(client_interface,
102
= dbus.PROPERTIES_IFACE))
104
super(MandosClientPropertyCache, self).__init__(**kwargs)
106
def _property_changed(self, property, value):
107
"""Helper which takes positional arguments"""
108
return self.property_changed(property=property, value=value)
59
110
def property_changed(self, property=None, value=None):
60
111
"""This is called whenever we get a PropertyChanged signal
71
125
def __init__(self, server_proxy_object=None, update_hook=None,
72
delete_hook=None, *args, **kwargs):
126
delete_hook=None, logger=None, **kwargs):
73
127
# Called on update
74
128
self.update_hook = update_hook
75
129
# Called on delete
76
130
self.delete_hook = delete_hook
77
131
# Mandos Server proxy object
78
132
self.server_proxy_object = server_proxy_object
136
self._update_timer_callback_tag = None
137
self._update_timer_callback_lock = 0
80
139
# The widget shown normally
81
140
self._text_widget = urwid.Text("")
82
141
# The widget shown when we have focus
83
142
self._focus_text_widget = urwid.Text("")
84
super(MandosClientWidget, self).__init__(
85
update_hook=update_hook, delete_hook=delete_hook,
143
super(MandosClientWidget, self).__init__(**kwargs)
88
145
self.opened = False
147
last_checked_ok = isoformat_to_datetime(self.properties
150
if self.properties ["LastCheckerStatus"] != 0:
151
self.using_timer(True)
153
if self.need_approval:
154
self.using_timer(True)
156
self.match_objects = (
157
self.proxy.connect_to_signal("CheckerCompleted",
158
self.checker_completed,
161
self.proxy.connect_to_signal("CheckerStarted",
162
self.checker_started,
165
self.proxy.connect_to_signal("GotSecret",
169
self.proxy.connect_to_signal("NeedApproval",
173
self.proxy.connect_to_signal("Rejected",
177
#self.logger('Created client {0}'
178
# .format(self.properties["Name"]))
180
def property_changed(self, property=None, value=None):
181
super(self, MandosClientWidget).property_changed(property,
183
if property == "ApprovalPending":
184
using_timer(bool(value))
185
if property == "LastCheckerStatus":
186
using_timer(value != 0)
187
#self.logger('Checker for client {0} (command "{1}") was '
188
# ' successful'.format(self.properties["Name"],
191
def using_timer(self, flag):
192
"""Call this method with True or False when timer should be
193
activated or deactivated.
195
old = self._update_timer_callback_lock
197
self._update_timer_callback_lock += 1
199
self._update_timer_callback_lock -= 1
200
if old == 0 and self._update_timer_callback_lock:
201
# Will update the shown timer value every second
202
self._update_timer_callback_tag = (gobject.timeout_add
205
elif old and self._update_timer_callback_lock == 0:
206
gobject.source_remove(self._update_timer_callback_tag)
207
self._update_timer_callback_tag = None
209
def checker_completed(self, exitstatus, condition, command):
214
if os.WIFEXITED(condition):
215
self.logger('Checker for client {0} (command "{1}")'
216
' failed with exit code {2}'
217
.format(self.properties["Name"], command,
218
os.WEXITSTATUS(condition)))
219
elif os.WIFSIGNALED(condition):
220
self.logger('Checker for client {0} (command "{1}") was'
221
' killed by signal {2}'
222
.format(self.properties["Name"], command,
223
os.WTERMSIG(condition)))
224
elif os.WCOREDUMP(condition):
225
self.logger('Checker for client {0} (command "{1}")'
227
.format(self.properties["Name"], command))
229
self.logger('Checker for client {0} completed'
231
.format(self.properties["Name"]))
234
def checker_started(self, command):
235
"""Server signals that a checker started. This could be useful
236
to log in the future. """
237
#self.logger('Client {0} started checker "{1}"'
238
# .format(self.properties["Name"],
242
def got_secret(self):
243
self.logger('Client {0} received its secret'
244
.format(self.properties["Name"]))
246
def need_approval(self, timeout, default):
248
message = 'Client {0} needs approval within {1} seconds'
250
message = 'Client {0} will get its secret in {1} seconds'
251
self.logger(message.format(self.properties["Name"],
253
self.using_timer(True)
255
def rejected(self, reason):
256
self.logger('Client {0} was rejected; reason: {1}'
257
.format(self.properties["Name"], reason))
90
259
def selectable(self):
91
260
"""Make this a "selectable" widget.
92
261
This overrides the method from urwid.FlowWidget."""
95
def rows(self, (maxcol,), focus=False):
264
def rows(self, maxcolrow, focus=False):
96
265
"""How many rows this widget will occupy might depend on
97
266
whether we have focus or not.
98
267
This overrides the method from urwid.FlowWidget"""
99
return self.current_widget(focus).rows((maxcol,), focus=focus)
268
return self.current_widget(focus).rows(maxcolrow, focus=focus)
101
270
def current_widget(self, focus=False):
102
271
if focus or self.opened:
106
275
def update(self):
107
276
"Called when what is visible on the screen should be updated."
108
277
# How to add standout mode to a style
109
with_standout = { u"normal": u"standout",
110
u"bold": u"bold-standout",
112
u"underline-blink-standout",
113
u"bold-underline-blink":
114
u"bold-underline-blink-standout",
278
with_standout = { "normal": "standout",
279
"bold": "bold-standout",
281
"underline-blink-standout",
282
"bold-underline-blink":
283
"bold-underline-blink-standout",
117
286
# Rebuild focus and non-focus widgets using current properties
118
self._text = (u'name="%(name)s", enabled=%(enabled)s'
288
# Base part of a client. Name!
289
base = '{name}: '.format(name=self.properties["Name"])
290
if not self.properties["Enabled"]:
292
elif self.properties["ApprovalPending"]:
293
timeout = datetime.timedelta(milliseconds
296
last_approval_request = isoformat_to_datetime(
297
self.properties["LastApprovalRequest"])
298
if last_approval_request is not None:
299
timer = timeout - (datetime.datetime.utcnow()
300
- last_approval_request)
302
timer = datetime.timedelta()
303
if self.properties["ApprovedByDefault"]:
304
message = "Approval in {0}. (d)eny?"
306
message = "Denial in {0}. (a)pprove?"
307
message = message.format(unicode(timer).rsplit(".", 1)[0])
308
elif self.properties["LastCheckerStatus"] != 0:
309
# When checker has failed, show timer until client expires
310
expires = self.properties["Expires"]
312
timer = datetime.timedelta(0)
314
expires = (datetime.datetime.strptime
315
(expires, '%Y-%m-%dT%H:%M:%S.%f'))
316
timer = expires - datetime.datetime.utcnow()
317
message = ('A checker has failed! Time until client'
318
' gets disabled: {0}'
319
.format(unicode(timer).rsplit(".", 1)[0]))
322
self._text = "{0}{1}".format(base, message)
120
324
if not urwid.supports_unicode():
121
325
self._text = self._text.encode("ascii", "replace")
122
textlist = [(u"normal", u"BLARGH: "), (u"bold", self._text)]
326
textlist = [("normal", self._text)]
123
327
self._text_widget.set_text(textlist)
124
328
self._focus_text_widget.set_text([(with_standout[text[0]],
133
337
if self.update_hook is not None:
134
338
self.update_hook()
340
def update_timer(self):
341
"""called by gobject. Will indefinitely loop until
342
gobject.source_remove() on tag is called"""
344
return True # Keep calling this
346
def delete(self, **kwargs):
347
if self._update_timer_callback_tag is not None:
348
gobject.source_remove(self._update_timer_callback_tag)
349
self._update_timer_callback_tag = None
350
for match in self.match_objects:
352
self.match_objects = ()
137
353
if self.delete_hook is not None:
138
354
self.delete_hook(self)
355
return super(MandosClientWidget, self).delete(**kwargs)
140
def render(self, (maxcol,), focus=False):
357
def render(self, maxcolrow, focus=False):
141
358
"""Render differently if we have focus.
142
359
This overrides the method from urwid.FlowWidget"""
143
return self.current_widget(focus).render((maxcol,),
360
return self.current_widget(focus).render(maxcolrow,
146
def keypress(self, (maxcol,), key):
363
def keypress(self, maxcolrow, key):
148
365
This overrides the method from urwid.FlowWidget"""
149
if key == u"e" or key == u"+":
151
elif key == u"d" or key == u"-":
153
elif key == u"r" or key == u"_":
367
self.proxy.Enable(dbus_interface = client_interface,
370
self.proxy.Disable(dbus_interface = client_interface,
373
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
374
dbus_interface = client_interface,
377
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
378
dbus_interface = client_interface,
380
elif key == "R" or key == "_" or key == "ctrl k":
154
381
self.server_proxy_object.RemoveClient(self.proxy
157
self.proxy.StartChecker()
159
self.proxy.StopChecker()
161
self.proxy.CheckedOK()
385
self.proxy.StartChecker(dbus_interface = client_interface,
388
self.proxy.StopChecker(dbus_interface = client_interface,
391
self.proxy.CheckedOK(dbus_interface = client_interface,
163
# elif key == u"p" or key == "=":
394
# elif key == "p" or key == "=":
164
395
# self.proxy.pause()
165
# elif key == u"u" or key == ":":
396
# elif key == "u" or key == ":":
166
397
# self.proxy.unpause()
167
# elif key == u"RET":
172
def property_changed(self, property=None, value=None,
403
def property_changed(self, property=None, **kwargs):
174
404
"""Call self.update() if old value is not new value.
175
405
This overrides the method from MandosClientPropertyCache"""
176
406
property_name = unicode(property)
177
407
old_value = self.properties.get(property_name)
178
408
super(MandosClientWidget, self).property_changed(
179
property=property, value=value, *args, **kwargs)
409
property=property, **kwargs)
180
410
if self.properties.get(property_name) != old_value:
203
433
self.screen = urwid.curses_display.Screen()
205
435
self.screen.register_palette((
207
u"default", u"default", None),
209
u"default", u"default", u"bold"),
211
u"default", u"default", u"underline"),
213
u"default", u"default", u"standout"),
214
(u"bold-underline-blink",
215
u"default", u"default", (u"bold", u"underline")),
217
u"default", u"default", (u"bold", u"standout")),
218
(u"underline-blink-standout",
219
u"default", u"default", (u"underline", u"standout")),
220
(u"bold-underline-blink-standout",
221
u"default", u"default", (u"bold", u"underline",
437
"default", "default", None),
439
"default", "default", "bold"),
441
"default", "default", "underline"),
443
"default", "default", "standout"),
444
("bold-underline-blink",
445
"default", "default", ("bold", "underline")),
447
"default", "default", ("bold", "standout")),
448
("underline-blink-standout",
449
"default", "default", ("underline", "standout")),
450
("bold-underline-blink-standout",
451
"default", "default", ("bold", "underline",
225
455
if urwid.supports_unicode():
226
#self.divider = u"─" # \u2500
227
self.divider = u"━" # \u2501
456
self.divider = "─" # \u2500
457
#self.divider = "━" # \u2501
229
#self.divider = u"-" # \u002d
230
self.divider = u"_" # \u005f
459
#self.divider = "-" # \u002d
460
self.divider = "_" # \u005f
232
462
self.screen.start()
247
477
# This keeps track of whether self.uilist currently has
248
478
# self.logbox in it or not
249
479
self.log_visible = True
250
self.log_wrap = u"any"
480
self.log_wrap = "any"
253
self.log_message(u"Message")
254
self.log_message(u"Message0 Message1 Message2 Message3 Message4 Message5 Message6 Message7 Message8 Message9")
255
self.log_message(u"Message10 Message11 Message12 Message13 Message14 Message15 Message16 Message17 Message18 Message19")
256
self.log_message(u"Message20 Message21 Message22 Message23 Message24 Message25 Message26 Message27 Message28 Message29")
483
self.log_message_raw(("bold",
484
"Mandos Monitor version " + version))
485
self.log_message_raw(("bold",
258
488
self.busname = domain + '.Mandos'
259
489
self.main_loop = gobject.MainLoop()
260
self.bus = dbus.SystemBus()
261
mandos_dbus_objc = self.bus.get_object(
262
self.busname, u"/", follow_name_owner_changes=True)
263
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
267
mandos_clients = (self.mandos_serv
268
.GetAllClientsWithProperties())
269
except dbus.exceptions.DBusException:
270
mandos_clients = dbus.Dictionary()
273
.connect_to_signal("ClientRemoved",
274
self.find_and_remove_client,
275
dbus_interface=server_interface,
278
.connect_to_signal("ClientAdded",
280
dbus_interface=server_interface,
282
for path, client in mandos_clients.iteritems():
283
client_proxy_object = self.bus.get_object(self.busname,
285
self.add_client(MandosClientWidget(server_proxy_object
288
=client_proxy_object,
293
=self.remove_client),
491
def client_not_found(self, fingerprint, address):
492
self.log_message("Client with address {0} and fingerprint"
493
" {1} could not be found"
494
.format(address, fingerprint))
296
496
def rebuild(self):
297
497
"""This rebuilds the User Interface.
298
498
Call this when the widget layout needs to change"""
300
500
#self.uilist.append(urwid.ListBox(self.clients))
301
self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
501
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
302
503
#header=urwid.Divider(),
304
footer=urwid.Divider(div_char=self.divider)))
506
urwid.Divider(div_char=
305
508
if self.log_visible:
306
509
self.uilist.append(self.logbox)
308
510
self.topwidget = urwid.Pile(self.uilist)
310
def log_message(self, markup):
512
def log_message(self, message):
513
timestamp = datetime.datetime.now().isoformat()
514
self.log_message_raw(timestamp + ": " + message)
516
def log_message_raw(self, markup):
311
517
"""Add a log message to the log buffer."""
312
518
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
313
519
if (self.max_log_length
314
520
and len(self.log) > self.max_log_length):
315
521
del self.log[0:len(self.log)-self.max_log_length-1]
522
self.logbox.set_focus(len(self.logbox.body.contents),
317
526
def toggle_log_display(self):
318
527
"""Toggle visibility of the log buffer."""
319
528
self.log_visible = not self.log_visible
321
self.log_message(u"Log visibility changed to: "
322
+ unicode(self.log_visible))
530
#self.log_message("Log visibility changed to: "
531
# + unicode(self.log_visible))
324
533
def change_log_display(self):
325
534
"""Change type of log display.
326
535
Currently, this toggles wrapping of text lines."""
327
if self.log_wrap == u"clip":
328
self.log_wrap = u"any"
536
if self.log_wrap == "clip":
537
self.log_wrap = "any"
330
self.log_wrap = u"clip"
539
self.log_wrap = "clip"
331
540
for textwidget in self.log:
332
541
textwidget.set_wrap_mode(self.log_wrap)
333
self.log_message(u"Wrap mode: " + self.log_wrap)
542
#self.log_message("Wrap mode: " + self.log_wrap)
335
544
def find_and_remove_client(self, path, name):
336
"""Find an client from its object path and remove it.
545
"""Find a client by its object path and remove it.
338
547
This is connected to the ClientRemoved signal from the
339
548
Mandos server object."""
386
598
"""Start the main loop and exit when it's done."""
599
self.bus = dbus.SystemBus()
600
mandos_dbus_objc = self.bus.get_object(
601
self.busname, "/", follow_name_owner_changes=True)
602
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
606
mandos_clients = (self.mandos_serv
607
.GetAllClientsWithProperties())
608
except dbus.exceptions.DBusException:
609
mandos_clients = dbus.Dictionary()
612
.connect_to_signal("ClientRemoved",
613
self.find_and_remove_client,
614
dbus_interface=server_interface,
617
.connect_to_signal("ClientAdded",
619
dbus_interface=server_interface,
622
.connect_to_signal("ClientNotFound",
623
self.client_not_found,
624
dbus_interface=server_interface,
626
for path, client in mandos_clients.iteritems():
627
client_proxy_object = self.bus.get_object(self.busname,
629
self.add_client(MandosClientWidget(server_proxy_object
632
=client_proxy_object,
388
643
self._input_callback_tag = (gobject.io_add_watch
389
644
(sys.stdin.fileno(),
415
670
except KeyError: # :-)
418
if key == u"q" or key == u"Q":
673
if key == "q" or key == "Q":
421
elif key == u"window resize":
676
elif key == "window resize":
422
677
self.size = self.screen.get_cols_rows()
424
elif key == u"\f": # Ctrl-L
679
elif key == "\f": # Ctrl-L
426
elif key == u"l" or key == u"D":
681
elif key == "l" or key == "D":
427
682
self.toggle_log_display()
429
elif key == u"w" or key == u"i":
684
elif key == "w" or key == "i":
430
685
self.change_log_display()
432
elif key == u"?" or key == u"f1":
433
self.log_message(u"Help!")
687
elif key == "?" or key == "f1" or key == "esc":
688
if not self.log_visible:
689
self.log_visible = True
691
self.log_message_raw(("bold",
695
"l: Log window toggle",
696
"TAB: Switch window",
698
self.log_message_raw(("bold",
704
"s: Start new checker",
436
711
if self.topwidget.get_focus() is self.logbox:
437
712
self.topwidget.set_focus(0)
439
714
self.topwidget.set_focus(self.logbox)
441
elif (key == u"end" or key == u"meta >" or key == u"G"
443
pass # xxx end-of-buffer
444
elif (key == u"home" or key == u"meta <" or key == u"g"
446
pass # xxx beginning-of-buffer
447
elif key == u"ctrl e" or key == u"$":
448
pass # xxx move-end-of-line
449
elif key == u"ctrl a" or key == u"^":
450
pass # xxx move-beginning-of-line
451
elif key == u"ctrl b" or key == u"meta (" or key == u"h":
453
elif key == u"ctrl f" or key == u"meta )" or key == u"l":
458
pass # scroll down log
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
459
734
elif self.topwidget.selectable():
460
735
self.topwidget.keypress(self.size, key)