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
21
# <http://www.gnu.org/licenses/>.
23
# Contact the authors at <mandos@recompile.se>.
4
from __future__ import division, absolute_import, with_statement
26
from __future__ import (division, absolute_import, print_function,
44
67
"Parse an ISO 8601 date string to a datetime.datetime()"
47
d, t = iso.split(u"T", 1)
48
year, month, day = d.split(u"-", 2)
49
hour, minute, second = t.split(u":", 2)
70
d, t = iso.split("T", 1)
71
year, month, day = d.split("-", 2)
72
hour, minute, second = t.split(":", 2)
50
73
second, fraction = divmod(float(second), 1)
51
74
return datetime.datetime(int(year),
116
144
self.opened = False
118
146
last_checked_ok = isoformat_to_datetime(self.properties
120
if last_checked_ok is None:
121
self.last_checker_failed = True
123
self.last_checker_failed = ((datetime.datetime.utcnow()
130
if self.last_checker_failed:
149
if self.properties ["LastCheckerStatus"] != 0:
131
150
self.using_timer(True)
133
152
if self.need_approval:
134
153
self.using_timer(True)
136
self.proxy.connect_to_signal(u"CheckerCompleted",
137
self.checker_completed,
140
self.proxy.connect_to_signal(u"CheckerStarted",
141
self.checker_started,
144
self.proxy.connect_to_signal(u"GotSecret",
148
self.proxy.connect_to_signal(u"NeedApproval",
152
self.proxy.connect_to_signal(u"Rejected",
155
self.match_objects = (
156
self.proxy.connect_to_signal("CheckerCompleted",
157
self.checker_completed,
160
self.proxy.connect_to_signal("CheckerStarted",
161
self.checker_started,
164
self.proxy.connect_to_signal("GotSecret",
168
self.proxy.connect_to_signal("NeedApproval",
172
self.proxy.connect_to_signal("Rejected",
176
#self.logger('Created client {0}'
177
# .format(self.properties["Name"]))
157
179
def property_changed(self, property=None, value=None):
158
180
super(self, MandosClientWidget).property_changed(property,
160
if property == u"ApprovalPending":
182
if property == "ApprovalPending":
161
183
using_timer(bool(value))
184
if property == "LastCheckerStatus":
185
using_timer(value != 0)
186
#self.logger('Checker for client {0} (command "{1}") was '
187
# ' successful'.format(self.properties["Name"],
163
190
def using_timer(self, flag):
164
191
"""Call this method with True or False when timer should be
165
192
activated or deactivated.
180
208
def checker_completed(self, exitstatus, condition, command):
181
209
if exitstatus == 0:
182
if self.last_checker_failed:
183
self.last_checker_failed = False
184
self.using_timer(False)
185
#self.logger(u'Checker for client %s (command "%s")'
187
# % (self.properties[u"Name"], command))
191
if not self.last_checker_failed:
192
self.last_checker_failed = True
193
self.using_timer(True)
194
213
if os.WIFEXITED(condition):
195
self.logger(u'Checker for client %s (command "%s")'
196
u' failed with exit code %s'
197
% (self.properties[u"Name"], command,
198
os.WEXITSTATUS(condition)))
214
self.logger('Checker for client {0} (command "{1}")'
215
' failed with exit code {2}'
216
.format(self.properties["Name"], command,
217
os.WEXITSTATUS(condition)))
199
218
elif os.WIFSIGNALED(condition):
200
self.logger(u'Checker for client %s (command "%s")'
201
u' was killed by signal %s'
202
% (self.properties[u"Name"], command,
203
os.WTERMSIG(condition)))
219
self.logger('Checker for client {0} (command "{1}") was'
220
' killed by signal {2}'
221
.format(self.properties["Name"], command,
222
os.WTERMSIG(condition)))
204
223
elif os.WCOREDUMP(condition):
205
self.logger(u'Checker for client %s (command "%s")'
207
% (self.properties[u"Name"], command))
224
self.logger('Checker for client {0} (command "{1}")'
226
.format(self.properties["Name"], command))
209
self.logger(u'Checker for client %s completed'
228
self.logger('Checker for client {0} completed'
230
.format(self.properties["Name"]))
213
233
def checker_started(self, command):
214
#self.logger(u'Client %s started checker "%s"'
215
# % (self.properties[u"Name"], unicode(command)))
234
"""Server signals that a checker started. This could be useful
235
to log in the future. """
236
#self.logger('Client {0} started checker "{1}"'
237
# .format(self.properties["Name"],
218
241
def got_secret(self):
219
self.last_checker_failed = False
220
self.logger(u'Client %s received its secret'
221
% self.properties[u"Name"])
242
self.logger('Client {0} received its secret'
243
.format(self.properties["Name"]))
223
245
def need_approval(self, timeout, default):
225
message = u'Client %s needs approval within %s seconds'
247
message = 'Client {0} needs approval within {1} seconds'
227
message = u'Client %s will get its secret in %s seconds'
229
% (self.properties[u"Name"], timeout/1000))
249
message = 'Client {0} will get its secret in {1} seconds'
250
self.logger(message.format(self.properties["Name"],
230
252
self.using_timer(True)
232
254
def rejected(self, reason):
233
self.logger(u'Client %s was rejected; reason: %s'
234
% (self.properties[u"Name"], reason))
255
self.logger('Client {0} was rejected; reason: {1}'
256
.format(self.properties["Name"], reason))
236
258
def selectable(self):
237
259
"""Make this a "selectable" widget.
238
260
This overrides the method from urwid.FlowWidget."""
241
def rows(self, (maxcol,), focus=False):
263
def rows(self, maxcolrow, focus=False):
242
264
"""How many rows this widget will occupy might depend on
243
265
whether we have focus or not.
244
266
This overrides the method from urwid.FlowWidget"""
245
return self.current_widget(focus).rows((maxcol,), focus=focus)
267
return self.current_widget(focus).rows(maxcolrow, focus=focus)
247
269
def current_widget(self, focus=False):
248
270
if focus or self.opened:
252
274
def update(self):
253
275
"Called when what is visible on the screen should be updated."
254
276
# How to add standout mode to a style
255
with_standout = { u"normal": u"standout",
256
u"bold": u"bold-standout",
258
u"underline-blink-standout",
259
u"bold-underline-blink":
260
u"bold-underline-blink-standout",
277
with_standout = { "normal": "standout",
278
"bold": "bold-standout",
280
"underline-blink-standout",
281
"bold-underline-blink":
282
"bold-underline-blink-standout",
263
285
# Rebuild focus and non-focus widgets using current properties
265
287
# Base part of a client. Name!
266
base = (u'%(name)s: '
267
% {u"name": self.properties[u"Name"]})
268
if not self.properties[u"Enabled"]:
269
message = u"DISABLED"
270
elif self.properties[u"ApprovalPending"]:
288
base = '{name}: '.format(name=self.properties["Name"])
289
if not self.properties["Enabled"]:
291
elif self.properties["ApprovalPending"]:
271
292
timeout = datetime.timedelta(milliseconds
272
293
= self.properties
274
295
last_approval_request = isoformat_to_datetime(
275
self.properties[u"LastApprovalRequest"])
296
self.properties["LastApprovalRequest"])
276
297
if last_approval_request is not None:
277
298
timer = timeout - (datetime.datetime.utcnow()
278
299
- last_approval_request)
280
301
timer = datetime.timedelta()
281
if self.properties[u"ApprovedByDefault"]:
282
message = u"Approval in %s. (d)eny?"
284
message = u"Denial in %s. (a)pprove?"
285
message = message % unicode(timer).rsplit(".", 1)[0]
286
elif self.last_checker_failed:
287
timeout = datetime.timedelta(milliseconds
290
last_ok = isoformat_to_datetime(
291
max((self.properties[u"LastCheckedOK"]
292
or self.properties[u"Created"]),
293
self.properties[u"LastEnabled"]))
294
timer = timeout - (datetime.datetime.utcnow() - last_ok)
295
message = (u'A checker has failed! Time until client'
296
u' gets disabled: %s'
297
% unicode(timer).rsplit(".", 1)[0])
302
if self.properties["ApprovedByDefault"]:
303
message = "Approval in {0}. (d)eny?"
305
message = "Denial in {0}. (a)pprove?"
306
message = message.format(unicode(timer).rsplit(".", 1)[0])
307
elif self.properties["LastCheckerStatus"] != 0:
308
# When checker has failed, show timer until client expires
309
expires = self.properties["Expires"]
311
timer = datetime.timedelta(0)
313
expires = (datetime.datetime.strptime
314
(expires, '%Y-%m-%dT%H:%M:%S.%f'))
315
timer = expires - datetime.datetime.utcnow()
316
message = ('A checker has failed! Time until client'
317
' gets disabled: {0}'
318
.format(unicode(timer).rsplit(".", 1)[0]))
300
self._text = "%s%s" % (base, message)
321
self._text = "{0}{1}".format(base, message)
302
323
if not urwid.supports_unicode():
303
324
self._text = self._text.encode("ascii", "replace")
304
textlist = [(u"normal", self._text)]
325
textlist = [("normal", self._text)]
305
326
self._text_widget.set_text(textlist)
306
327
self._focus_text_widget.set_text([(with_standout[text[0]],
316
337
self.update_hook()
318
339
def update_timer(self):
340
"""called by gobject. Will indefinitely loop until
341
gobject.source_remove() on tag is called"""
321
343
return True # Keep calling this
345
def delete(self, *args, **kwargs):
324
346
if self._update_timer_callback_tag is not None:
325
347
gobject.source_remove(self._update_timer_callback_tag)
326
348
self._update_timer_callback_tag = None
349
for match in self.match_objects:
351
self.match_objects = ()
327
352
if self.delete_hook is not None:
328
353
self.delete_hook(self)
354
return super(MandosClientWidget, self).delete(*args, **kwargs)
330
def render(self, (maxcol,), focus=False):
356
def render(self, maxcolrow, focus=False):
331
357
"""Render differently if we have focus.
332
358
This overrides the method from urwid.FlowWidget"""
333
return self.current_widget(focus).render((maxcol,),
359
return self.current_widget(focus).render(maxcolrow,
336
def keypress(self, (maxcol,), key):
362
def keypress(self, maxcolrow, key):
338
364
This overrides the method from urwid.FlowWidget"""
340
self.proxy.Enable(dbus_interface = client_interface)
342
self.proxy.Disable(dbus_interface = client_interface)
366
self.proxy.Enable(dbus_interface = client_interface,
369
self.proxy.Disable(dbus_interface = client_interface,
344
372
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
345
dbus_interface = client_interface)
373
dbus_interface = client_interface,
347
376
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
348
dbus_interface = client_interface)
349
elif key == u"r" or key == u"_" or key == u"ctrl k":
377
dbus_interface = client_interface,
379
elif key == "R" or key == "_" or key == "ctrl k":
350
380
self.server_proxy_object.RemoveClient(self.proxy
353
self.proxy.StartChecker(dbus_interface = client_interface)
355
self.proxy.StopChecker(dbus_interface = client_interface)
357
self.proxy.CheckedOK(dbus_interface = client_interface)
384
self.proxy.StartChecker(dbus_interface = client_interface,
387
self.proxy.StopChecker(dbus_interface = client_interface,
390
self.proxy.CheckedOK(dbus_interface = client_interface,
359
# elif key == u"p" or key == "=":
393
# elif key == "p" or key == "=":
360
394
# self.proxy.pause()
361
# elif key == u"u" or key == ":":
395
# elif key == "u" or key == ":":
362
396
# self.proxy.unpause()
363
# elif key == u"RET":
382
416
"down" key presses, thus not allowing any containing widgets to
383
417
use them as an excuse to shift focus away from this widget.
385
def keypress(self, (maxcol, maxrow), key):
386
ret = super(ConstrainedListBox, self).keypress((maxcol,
388
if ret in (u"up", u"down"):
419
def keypress(self, maxcolrow, key):
420
ret = super(ConstrainedListBox, self).keypress(maxcolrow, key)
421
if ret in ("up", "down"):
400
433
self.screen = urwid.curses_display.Screen()
402
435
self.screen.register_palette((
404
u"default", u"default", None),
406
u"default", u"default", u"bold"),
408
u"default", u"default", u"underline"),
410
u"default", u"default", u"standout"),
411
(u"bold-underline-blink",
412
u"default", u"default", (u"bold", u"underline")),
414
u"default", u"default", (u"bold", u"standout")),
415
(u"underline-blink-standout",
416
u"default", u"default", (u"underline", u"standout")),
417
(u"bold-underline-blink-standout",
418
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",
422
455
if urwid.supports_unicode():
423
self.divider = u"─" # \u2500
424
#self.divider = u"━" # \u2501
456
self.divider = "─" # \u2500
457
#self.divider = "━" # \u2501
426
#self.divider = u"-" # \u002d
427
self.divider = u"_" # \u005f
459
#self.divider = "-" # \u002d
460
self.divider = "_" # \u005f
429
462
self.screen.start()
444
477
# This keeps track of whether self.uilist currently has
445
478
# self.logbox in it or not
446
479
self.log_visible = True
447
self.log_wrap = u"any"
480
self.log_wrap = "any"
450
self.log_message_raw((u"bold",
451
u"Mandos Monitor version " + version))
452
self.log_message_raw((u"bold",
483
self.log_message_raw(("bold",
484
"Mandos Monitor version " + version))
485
self.log_message_raw(("bold",
455
488
self.busname = domain + '.Mandos'
456
489
self.main_loop = gobject.MainLoop()
457
self.bus = dbus.SystemBus()
458
mandos_dbus_objc = self.bus.get_object(
459
self.busname, u"/", follow_name_owner_changes=True)
460
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
464
mandos_clients = (self.mandos_serv
465
.GetAllClientsWithProperties())
466
except dbus.exceptions.DBusException:
467
mandos_clients = dbus.Dictionary()
470
.connect_to_signal(u"ClientRemoved",
471
self.find_and_remove_client,
472
dbus_interface=server_interface,
475
.connect_to_signal(u"ClientAdded",
477
dbus_interface=server_interface,
480
.connect_to_signal(u"ClientNotFound",
481
self.client_not_found,
482
dbus_interface=server_interface,
484
for path, client in mandos_clients.iteritems():
485
client_proxy_object = self.bus.get_object(self.busname,
487
self.add_client(MandosClientWidget(server_proxy_object
490
=client_proxy_object,
500
491
def client_not_found(self, fingerprint, address):
501
self.log_message((u"Client with address %s and fingerprint %s"
502
u" could not be found" % (address,
492
self.log_message("Client with address {0} and fingerprint"
493
" {1} could not be found"
494
.format(address, fingerprint))
505
496
def rebuild(self):
506
497
"""This rebuilds the User Interface.
530
520
and len(self.log) > self.max_log_length):
531
521
del self.log[0:len(self.log)-self.max_log_length-1]
532
522
self.logbox.set_focus(len(self.logbox.body.contents),
533
coming_from=u"above")
536
526
def toggle_log_display(self):
537
527
"""Toggle visibility of the log buffer."""
538
528
self.log_visible = not self.log_visible
540
#self.log_message(u"Log visibility changed to: "
530
#self.log_message("Log visibility changed to: "
541
531
# + unicode(self.log_visible))
543
533
def change_log_display(self):
544
534
"""Change type of log display.
545
535
Currently, this toggles wrapping of text lines."""
546
if self.log_wrap == u"clip":
547
self.log_wrap = u"any"
536
if self.log_wrap == "clip":
537
self.log_wrap = "any"
549
self.log_wrap = u"clip"
539
self.log_wrap = "clip"
550
540
for textwidget in self.log:
551
541
textwidget.set_wrap_mode(self.log_wrap)
552
#self.log_message(u"Wrap mode: " + self.log_wrap)
542
#self.log_message("Wrap mode: " + self.log_wrap)
554
544
def find_and_remove_client(self, path, name):
555
"""Find an client from its object path and remove it.
545
"""Find a client by its object path and remove it.
557
547
This is connected to the ClientRemoved signal from the
558
548
Mandos server object."""
606
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,
608
643
self._input_callback_tag = (gobject.io_add_watch
609
644
(sys.stdin.fileno(),
620
655
def process_input(self, source, condition):
621
656
keys = self.screen.get_input()
622
translations = { u"ctrl n": u"down", # Emacs
623
u"ctrl p": u"up", # Emacs
624
u"ctrl v": u"page down", # Emacs
625
u"meta v": u"page up", # Emacs
626
u" ": u"page down", # less
627
u"f": u"page down", # less
628
u"b": u"page up", # less
657
translations = { "ctrl n": "down", # Emacs
658
"ctrl p": "up", # Emacs
659
"ctrl v": "page down", # Emacs
660
"meta v": "page up", # Emacs
661
" ": "page down", # less
662
"f": "page down", # less
663
"b": "page up", # less
635
670
except KeyError: # :-)
638
if key == u"q" or key == u"Q":
673
if key == "q" or key == "Q":
641
elif key == u"window resize":
676
elif key == "window resize":
642
677
self.size = self.screen.get_cols_rows()
644
elif key == u"\f": # Ctrl-L
679
elif key == "\f": # Ctrl-L
646
elif key == u"l" or key == u"D":
681
elif key == "l" or key == "D":
647
682
self.toggle_log_display()
649
elif key == u"w" or key == u"i":
684
elif key == "w" or key == "i":
650
685
self.change_log_display()
652
elif key == u"?" or key == u"f1" or key == u"esc":
687
elif key == "?" or key == "f1" or key == "esc":
653
688
if not self.log_visible:
654
689
self.log_visible = True
656
self.log_message_raw((u"bold",
660
u"l: Log window toggle",
661
u"TAB: Switch window",
663
self.log_message_raw((u"bold",
669
u"s: Start new checker",
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",
676
711
if self.topwidget.get_focus() is self.logbox:
677
712
self.topwidget.set_focus(0)
679
714
self.topwidget.set_focus(self.logbox)
681
#elif (key == u"end" or key == u"meta >" or key == u"G"
716
#elif (key == "end" or key == "meta >" or key == "G"
683
718
# pass # xxx end-of-buffer
684
#elif (key == u"home" or key == u"meta <" or key == u"g"
719
#elif (key == "home" or key == "meta <" or key == "g"
686
721
# pass # xxx beginning-of-buffer
687
#elif key == u"ctrl e" or key == u"$":
722
#elif key == "ctrl e" or key == "$":
688
723
# pass # xxx move-end-of-line
689
#elif key == u"ctrl a" or key == u"^":
724
#elif key == "ctrl a" or key == "^":
690
725
# pass # xxx move-beginning-of-line
691
#elif key == u"ctrl b" or key == u"meta (" or key == u"h":
726
#elif key == "ctrl b" or key == "meta (" or key == "h":
692
727
# pass # xxx left
693
#elif key == u"ctrl f" or key == u"meta )" or key == u"l":
728
#elif key == "ctrl f" or key == "meta )" or key == "l":
694
729
# pass # xxx right
696
731
# pass # scroll up log
698
733
# pass # scroll down log
699
734
elif self.topwidget.selectable():
700
735
self.topwidget.keypress(self.size, key)