2
2
# -*- mode: python; coding: utf-8 -*-
4
# Mandos Monitor - Control and monitor the Mandos server
6
# Copyright © 2009-2011 Teddy Hogeborn
7
# Copyright © 2009-2011 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>.
25
from __future__ import (division, absolute_import, print_function,
4
from __future__ import division, absolute_import, with_statement
66
44
"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)
47
d, t = iso.split(u"T", 1)
48
year, month, day = d.split(u"-", 2)
49
hour, minute, second = t.split(u":", 2)
72
50
second, fraction = divmod(float(second), 1)
73
51
return datetime.datetime(int(year),
87
65
self.proxy = proxy_object # Mandos Client proxy object
89
67
self.properties = dict()
90
self.property_changed_match = (
91
self.proxy.connect_to_signal("PropertyChanged",
92
self.property_changed,
68
self.proxy.connect_to_signal(u"PropertyChanged",
69
self.property_changed,
96
73
self.properties.update(
97
74
self.proxy.GetAll(client_interface,
98
75
dbus_interface = dbus.PROPERTIES_IFACE))
100
#XXX This breaks good super behaviour
77
#XXX This break good super behaviour!
101
78
# super(MandosClientPropertyCache, self).__init__(
102
79
# *args, **kwargs)
134
106
self.last_checker_failed = False
136
108
# The widget shown normally
137
self._text_widget = urwid.Text("")
109
self._text_widget = urwid.Text(u"")
138
110
# The widget shown when we have focus
139
self._focus_text_widget = urwid.Text("")
111
self._focus_text_widget = urwid.Text(u"")
140
112
super(MandosClientWidget, self).__init__(
141
113
update_hook=update_hook, delete_hook=delete_hook,
161
133
if self.need_approval:
162
134
self.using_timer(True)
164
self.match_objects = (
165
self.proxy.connect_to_signal("CheckerCompleted",
166
self.checker_completed,
169
self.proxy.connect_to_signal("CheckerStarted",
170
self.checker_started,
173
self.proxy.connect_to_signal("GotSecret",
177
self.proxy.connect_to_signal("NeedApproval",
181
self.proxy.connect_to_signal("Rejected",
185
#self.logger('Created client %s' % (self.properties["Name"]))
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",
187
157
def property_changed(self, property=None, value=None):
188
158
super(self, MandosClientWidget).property_changed(property,
190
if property == "ApprovalPending":
160
if property == u"ApprovalPending":
191
161
using_timer(bool(value))
193
163
def using_timer(self, flag):
223
192
self.last_checker_failed = True
224
193
self.using_timer(True)
225
194
if os.WIFEXITED(condition):
226
self.logger('Checker for client %s (command "%s")'
227
' failed with exit code %s'
228
% (self.properties["Name"], command,
195
self.logger(u'Checker for client %s (command "%s")'
196
u' failed with exit code %s'
197
% (self.properties[u"Name"], command,
229
198
os.WEXITSTATUS(condition)))
230
199
elif os.WIFSIGNALED(condition):
231
self.logger('Checker for client %s (command "%s")'
232
' was killed by signal %s'
233
% (self.properties["Name"], command,
200
self.logger(u'Checker for client %s (command "%s")'
201
u' was killed by signal %s'
202
% (self.properties[u"Name"], command,
234
203
os.WTERMSIG(condition)))
235
204
elif os.WCOREDUMP(condition):
236
self.logger('Checker for client %s (command "%s")'
238
% (self.properties["Name"], command))
205
self.logger(u'Checker for client %s (command "%s")'
207
% (self.properties[u"Name"], command))
240
self.logger('Checker for client %s completed'
209
self.logger(u'Checker for client %s completed'
244
213
def checker_started(self, command):
245
#self.logger('Client %s started checker "%s"'
246
# % (self.properties["Name"], unicode(command)))
214
#self.logger(u'Client %s started checker "%s"'
215
# % (self.properties[u"Name"], unicode(command)))
249
218
def got_secret(self):
250
219
self.last_checker_failed = False
251
self.logger('Client %s received its secret'
252
% self.properties["Name"])
220
self.logger(u'Client %s received its secret'
221
% self.properties[u"Name"])
254
223
def need_approval(self, timeout, default):
256
message = 'Client %s needs approval within %s seconds'
225
message = u'Client %s needs approval within %s seconds'
258
message = 'Client %s will get its secret in %s seconds'
227
message = u'Client %s will get its secret in %s seconds'
259
228
self.logger(message
260
% (self.properties["Name"], timeout/1000))
229
% (self.properties[u"Name"], timeout/1000))
261
230
self.using_timer(True)
263
232
def rejected(self, reason):
264
self.logger('Client %s was rejected; reason: %s'
265
% (self.properties["Name"], reason))
233
self.logger(u'Client %s was rejected; reason: %s'
234
% (self.properties[u"Name"], reason))
267
236
def selectable(self):
268
237
"""Make this a "selectable" widget.
269
238
This overrides the method from urwid.FlowWidget."""
272
def rows(self, maxcolrow, focus=False):
241
def rows(self, (maxcol,), focus=False):
273
242
"""How many rows this widget will occupy might depend on
274
243
whether we have focus or not.
275
244
This overrides the method from urwid.FlowWidget"""
276
return self.current_widget(focus).rows(maxcolrow, focus=focus)
245
return self.current_widget(focus).rows((maxcol,), focus=focus)
278
247
def current_widget(self, focus=False):
279
248
if focus or self.opened:
283
252
def update(self):
284
253
"Called when what is visible on the screen should be updated."
285
254
# How to add standout mode to a style
286
with_standout = { "normal": "standout",
287
"bold": "bold-standout",
289
"underline-blink-standout",
290
"bold-underline-blink":
291
"bold-underline-blink-standout",
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",
294
263
# Rebuild focus and non-focus widgets using current properties
296
265
# Base part of a client. Name!
298
% {"name": self.properties["Name"]})
299
if not self.properties["Enabled"]:
301
elif self.properties["ApprovalPending"]:
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"]:
302
271
timeout = datetime.timedelta(milliseconds
303
272
= self.properties
305
274
last_approval_request = isoformat_to_datetime(
306
self.properties["LastApprovalRequest"])
275
self.properties[u"LastApprovalRequest"])
307
276
if last_approval_request is not None:
308
277
timer = timeout - (datetime.datetime.utcnow()
309
278
- last_approval_request)
311
280
timer = datetime.timedelta()
312
if self.properties["ApprovedByDefault"]:
313
message = "Approval in %s. (d)eny?"
281
if self.properties[u"ApprovedByDefault"]:
282
message = u"Approval in %s. (d)eny?"
315
message = "Denial in %s. (a)pprove?"
284
message = u"Denial in %s. (a)pprove?"
316
285
message = message % unicode(timer).rsplit(".", 1)[0]
317
286
elif self.last_checker_failed:
318
# When checker has failed, print a timer until client expires
319
expires = self.properties["Expires"]
321
timer = datetime.timedelta(0)
323
expires = datetime.datetime.strptime(expires,
324
'%Y-%m-%dT%H:%M:%S.%f')
325
timer = expires - datetime.datetime.utcnow()
326
message = ('A checker has failed! Time until client'
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'
328
297
% unicode(timer).rsplit(".", 1)[0])
331
300
self._text = "%s%s" % (base, message)
333
302
if not urwid.supports_unicode():
334
303
self._text = self._text.encode("ascii", "replace")
335
textlist = [("normal", self._text)]
304
textlist = [(u"normal", self._text)]
336
305
self._text_widget.set_text(textlist)
337
306
self._focus_text_widget.set_text([(with_standout[text[0]],
347
316
self.update_hook()
349
318
def update_timer(self):
350
"""called by gobject. Will indefinitely loop until
351
gobject.source_remove() on tag is called"""
353
321
return True # Keep calling this
355
def delete(self, *args, **kwargs):
356
324
if self._update_timer_callback_tag is not None:
357
325
gobject.source_remove(self._update_timer_callback_tag)
358
326
self._update_timer_callback_tag = None
359
for match in self.match_objects:
361
self.match_objects = ()
362
327
if self.delete_hook is not None:
363
328
self.delete_hook(self)
364
return super(MandosClientWidget, self).delete(*args, **kwargs)
366
def render(self, maxcolrow, focus=False):
330
def render(self, (maxcol,), focus=False):
367
331
"""Render differently if we have focus.
368
332
This overrides the method from urwid.FlowWidget"""
369
return self.current_widget(focus).render(maxcolrow,
333
return self.current_widget(focus).render((maxcol,),
372
def keypress(self, maxcolrow, key):
336
def keypress(self, (maxcol,), key):
374
338
This overrides the method from urwid.FlowWidget"""
376
self.proxy.Enable(dbus_interface = client_interface,
379
self.proxy.Disable(dbus_interface = client_interface,
340
self.proxy.Enable(dbus_interface = client_interface)
342
self.proxy.Disable(dbus_interface = client_interface)
382
344
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
383
dbus_interface = client_interface,
345
dbus_interface = client_interface)
386
347
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
387
dbus_interface = client_interface,
389
elif key == "R" or key == "_" or key == "ctrl k":
348
dbus_interface = client_interface)
349
elif key == u"r" or key == u"_" or key == u"ctrl k":
390
350
self.server_proxy_object.RemoveClient(self.proxy
394
self.proxy.StartChecker(dbus_interface = client_interface,
397
self.proxy.StopChecker(dbus_interface = client_interface,
400
self.proxy.CheckedOK(dbus_interface = client_interface,
353
self.proxy.StartChecker(dbus_interface = client_interface)
355
self.proxy.StopChecker(dbus_interface = client_interface)
357
self.proxy.CheckedOK(dbus_interface = client_interface)
403
# elif key == "p" or key == "=":
359
# elif key == u"p" or key == "=":
404
360
# self.proxy.pause()
405
# elif key == "u" or key == ":":
361
# elif key == u"u" or key == ":":
406
362
# self.proxy.unpause()
363
# elif key == u"RET":
426
382
"down" key presses, thus not allowing any containing widgets to
427
383
use them as an excuse to shift focus away from this widget.
429
def keypress(self, maxcolrow, key):
430
ret = super(ConstrainedListBox, self).keypress(maxcolrow, key)
431
if ret in ("up", "down"):
385
def keypress(self, (maxcol, maxrow), key):
386
ret = super(ConstrainedListBox, self).keypress((maxcol,
388
if ret in (u"up", u"down"):
443
400
self.screen = urwid.curses_display.Screen()
445
402
self.screen.register_palette((
447
"default", "default", None),
449
"default", "default", "bold"),
451
"default", "default", "underline"),
453
"default", "default", "standout"),
454
("bold-underline-blink",
455
"default", "default", ("bold", "underline")),
457
"default", "default", ("bold", "standout")),
458
("underline-blink-standout",
459
"default", "default", ("underline", "standout")),
460
("bold-underline-blink-standout",
461
"default", "default", ("bold", "underline",
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",
465
422
if urwid.supports_unicode():
466
self.divider = "─" # \u2500
467
#self.divider = "━" # \u2501
423
self.divider = u"─" # \u2500
424
#self.divider = u"━" # \u2501
469
#self.divider = "-" # \u002d
470
self.divider = "_" # \u005f
426
#self.divider = u"-" # \u002d
427
self.divider = u"_" # \u005f
472
429
self.screen.start()
487
444
# This keeps track of whether self.uilist currently has
488
445
# self.logbox in it or not
489
446
self.log_visible = True
490
self.log_wrap = "any"
447
self.log_wrap = u"any"
493
self.log_message_raw(("bold",
494
"Mandos Monitor version " + version))
495
self.log_message_raw(("bold",
450
self.log_message_raw((u"bold",
451
u"Mandos Monitor version " + version))
452
self.log_message_raw((u"bold",
498
455
self.busname = domain + '.Mandos'
499
456
self.main_loop = gobject.MainLoop()
500
457
self.bus = dbus.SystemBus()
501
458
mandos_dbus_objc = self.bus.get_object(
502
self.busname, "/", follow_name_owner_changes=True)
459
self.busname, u"/", follow_name_owner_changes=True)
503
460
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
505
462
= server_interface)
510
467
mandos_clients = dbus.Dictionary()
512
469
(self.mandos_serv
513
.connect_to_signal("ClientRemoved",
470
.connect_to_signal(u"ClientRemoved",
514
471
self.find_and_remove_client,
515
472
dbus_interface=server_interface,
516
473
byte_arrays=True))
517
474
(self.mandos_serv
518
.connect_to_signal("ClientAdded",
475
.connect_to_signal(u"ClientAdded",
519
476
self.add_new_client,
520
477
dbus_interface=server_interface,
521
478
byte_arrays=True))
522
479
(self.mandos_serv
523
.connect_to_signal("ClientNotFound",
480
.connect_to_signal(u"ClientNotFound",
524
481
self.client_not_found,
525
482
dbus_interface=server_interface,
526
483
byte_arrays=True))
573
530
and len(self.log) > self.max_log_length):
574
531
del self.log[0:len(self.log)-self.max_log_length-1]
575
532
self.logbox.set_focus(len(self.logbox.body.contents),
533
coming_from=u"above")
579
536
def toggle_log_display(self):
580
537
"""Toggle visibility of the log buffer."""
581
538
self.log_visible = not self.log_visible
583
#self.log_message("Log visibility changed to: "
540
#self.log_message(u"Log visibility changed to: "
584
541
# + unicode(self.log_visible))
586
543
def change_log_display(self):
587
544
"""Change type of log display.
588
545
Currently, this toggles wrapping of text lines."""
589
if self.log_wrap == "clip":
590
self.log_wrap = "any"
546
if self.log_wrap == u"clip":
547
self.log_wrap = u"any"
592
self.log_wrap = "clip"
549
self.log_wrap = u"clip"
593
550
for textwidget in self.log:
594
551
textwidget.set_wrap_mode(self.log_wrap)
595
#self.log_message("Wrap mode: " + self.log_wrap)
552
#self.log_message(u"Wrap mode: " + self.log_wrap)
597
554
def find_and_remove_client(self, path, name):
598
"""Find a client by its object path and remove it.
555
"""Find an client from its object path and remove it.
600
557
This is connected to the ClientRemoved signal from the
601
558
Mandos server object."""
665
620
def process_input(self, source, condition):
666
621
keys = self.screen.get_input()
667
translations = { "ctrl n": "down", # Emacs
668
"ctrl p": "up", # Emacs
669
"ctrl v": "page down", # Emacs
670
"meta v": "page up", # Emacs
671
" ": "page down", # less
672
"f": "page down", # less
673
"b": "page up", # less
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
680
635
except KeyError: # :-)
683
if key == "q" or key == "Q":
638
if key == u"q" or key == u"Q":
686
elif key == "window resize":
641
elif key == u"window resize":
687
642
self.size = self.screen.get_cols_rows()
689
elif key == "\f": # Ctrl-L
644
elif key == u"\f": # Ctrl-L
691
elif key == "l" or key == "D":
646
elif key == u"l" or key == u"D":
692
647
self.toggle_log_display()
694
elif key == "w" or key == "i":
649
elif key == u"w" or key == u"i":
695
650
self.change_log_display()
697
elif key == "?" or key == "f1" or key == "esc":
652
elif key == u"?" or key == u"f1" or key == u"esc":
698
653
if not self.log_visible:
699
654
self.log_visible = True
701
self.log_message_raw(("bold",
705
"l: Log window toggle",
706
"TAB: Switch window",
708
self.log_message_raw(("bold",
714
"s: Start new checker",
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",
721
676
if self.topwidget.get_focus() is self.logbox:
722
677
self.topwidget.set_focus(0)
724
679
self.topwidget.set_focus(self.logbox)
726
#elif (key == "end" or key == "meta >" or key == "G"
681
#elif (key == u"end" or key == u"meta >" or key == u"G"
728
683
# pass # xxx end-of-buffer
729
#elif (key == "home" or key == "meta <" or key == "g"
684
#elif (key == u"home" or key == u"meta <" or key == u"g"
731
686
# pass # xxx beginning-of-buffer
732
#elif key == "ctrl e" or key == "$":
687
#elif key == u"ctrl e" or key == u"$":
733
688
# pass # xxx move-end-of-line
734
#elif key == "ctrl a" or key == "^":
689
#elif key == u"ctrl a" or key == u"^":
735
690
# pass # xxx move-beginning-of-line
736
#elif key == "ctrl b" or key == "meta (" or key == "h":
691
#elif key == u"ctrl b" or key == u"meta (" or key == u"h":
737
692
# pass # xxx left
738
#elif key == "ctrl f" or key == "meta )" or key == "l":
693
#elif key == u"ctrl f" or key == u"meta )" or key == u"l":
739
694
# pass # xxx right
741
696
# pass # scroll up log
743
698
# pass # scroll down log
744
699
elif self.topwidget.selectable():
745
700
self.topwidget.keypress(self.size, key)