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 *
81
133
self.logger = logger
135
self._update_timer_callback_tag = None
136
self._update_timer_callback_lock = 0
83
138
# The widget shown normally
84
self._text_widget = urwid.Text(u"")
139
self._text_widget = urwid.Text("")
85
140
# The widget shown when we have focus
86
self._focus_text_widget = urwid.Text(u"")
141
self._focus_text_widget = urwid.Text("")
87
142
super(MandosClientWidget, self).__init__(
88
143
update_hook=update_hook, delete_hook=delete_hook,
91
146
self.opened = False
92
self.proxy.connect_to_signal(u"CheckerCompleted",
93
self.checker_completed,
96
self.proxy.connect_to_signal(u"CheckerStarted",
100
self.proxy.connect_to_signal(u"GotSecret",
104
self.proxy.connect_to_signal(u"Rejected",
148
last_checked_ok = isoformat_to_datetime(self.properties
151
if self.properties ["LastCheckerStatus"] != 0:
152
self.using_timer(True)
154
if self.need_approval:
155
self.using_timer(True)
157
self.match_objects = (
158
self.proxy.connect_to_signal("CheckerCompleted",
159
self.checker_completed,
162
self.proxy.connect_to_signal("CheckerStarted",
163
self.checker_started,
166
self.proxy.connect_to_signal("GotSecret",
170
self.proxy.connect_to_signal("NeedApproval",
174
self.proxy.connect_to_signal("Rejected",
178
#self.logger('Created client {0}'
179
# .format(self.properties["Name"]))
181
def property_changed(self, property=None, value=None):
182
super(self, MandosClientWidget).property_changed(property,
184
if property == "ApprovalPending":
185
using_timer(bool(value))
186
if property == "LastCheckerStatus":
187
using_timer(value != 0)
188
#self.logger('Checker for client {0} (command "{1}") was '
189
# ' successful'.format(self.properties["Name"],
192
def using_timer(self, flag):
193
"""Call this method with True or False when timer should be
194
activated or deactivated.
196
old = self._update_timer_callback_lock
198
self._update_timer_callback_lock += 1
200
self._update_timer_callback_lock -= 1
201
if old == 0 and self._update_timer_callback_lock:
202
# Will update the shown timer value every second
203
self._update_timer_callback_tag = (gobject.timeout_add
206
elif old and self._update_timer_callback_lock == 0:
207
gobject.source_remove(self._update_timer_callback_tag)
208
self._update_timer_callback_tag = None
109
210
def checker_completed(self, exitstatus, condition, command):
110
211
if exitstatus == 0:
111
self.logger(u'Checker for client %s (command "%s")'
113
% (self.properties[u"name"], command))
115
215
if os.WIFEXITED(condition):
116
self.logger(u'Checker for client %s (command "%s")'
117
u' failed with exit code %s'
118
% (self.properties[u"name"], command,
119
os.WEXITSTATUS(condition)))
121
if os.WIFSIGNALED(condition):
122
self.logger(u'Checker for client %s (command "%s")'
123
u' was killed by signal %s'
124
% (self.properties[u"name"], command,
125
os.WTERMSIG(condition)))
127
if os.WCOREDUMP(condition):
128
self.logger(u'Checker for client %s (command "%s")'
130
% (self.properties[u"name"], command))
131
self.logger(u'Checker for client %s completed mysteriously')
216
self.logger('Checker for client {0} (command "{1}")'
217
' failed with exit code {2}'
218
.format(self.properties["Name"], command,
219
os.WEXITSTATUS(condition)))
220
elif os.WIFSIGNALED(condition):
221
self.logger('Checker for client {0} (command "{1}") was'
222
' killed by signal {2}'
223
.format(self.properties["Name"], command,
224
os.WTERMSIG(condition)))
225
elif os.WCOREDUMP(condition):
226
self.logger('Checker for client {0} (command "{1}")'
228
.format(self.properties["Name"], command))
230
self.logger('Checker for client {0} completed'
232
.format(self.properties["Name"]))
133
235
def checker_started(self, command):
134
self.logger(u'Client %s started checker "%s"'
135
% (self.properties[u"name"], unicode(command)))
236
"""Server signals that a checker started. This could be useful
237
to log in the future. """
238
#self.logger('Client {0} started checker "{1}"'
239
# .format(self.properties["Name"],
137
243
def got_secret(self):
138
self.logger(u'Client %s received its secret'
139
% self.properties[u"name"])
142
self.logger(u'Client %s was rejected'
143
% self.properties[u"name"])
244
self.logger('Client {0} received its secret'
245
.format(self.properties["Name"]))
247
def need_approval(self, timeout, default):
249
message = 'Client {0} needs approval within {1} seconds'
251
message = 'Client {0} will get its secret in {1} seconds'
252
self.logger(message.format(self.properties["Name"],
254
self.using_timer(True)
256
def rejected(self, reason):
257
self.logger('Client {0} was rejected; reason: {1}'
258
.format(self.properties["Name"], reason))
145
260
def selectable(self):
146
261
"""Make this a "selectable" widget.
147
262
This overrides the method from urwid.FlowWidget."""
150
def rows(self, (maxcol,), focus=False):
265
def rows(self, maxcolrow, focus=False):
151
266
"""How many rows this widget will occupy might depend on
152
267
whether we have focus or not.
153
268
This overrides the method from urwid.FlowWidget"""
154
return self.current_widget(focus).rows((maxcol,), focus=focus)
269
return self.current_widget(focus).rows(maxcolrow, focus=focus)
156
271
def current_widget(self, focus=False):
157
272
if focus or self.opened:
161
276
def update(self):
162
277
"Called when what is visible on the screen should be updated."
163
278
# How to add standout mode to a style
164
with_standout = { u"normal": u"standout",
165
u"bold": u"bold-standout",
167
u"underline-blink-standout",
168
u"bold-underline-blink":
169
u"bold-underline-blink-standout",
279
with_standout = { "normal": "standout",
280
"bold": "bold-standout",
282
"underline-blink-standout",
283
"bold-underline-blink":
284
"bold-underline-blink-standout",
172
287
# Rebuild focus and non-focus widgets using current properties
173
self._text = (u'%(name)s: %(enabled)s'
174
% { u"name": self.properties[u"name"],
177
if self.properties[u"enabled"]
289
# Base part of a client. Name!
290
base = '{name}: '.format(name=self.properties["Name"])
291
if not self.properties["Enabled"]:
293
elif self.properties["ApprovalPending"]:
294
timeout = datetime.timedelta(milliseconds
297
last_approval_request = isoformat_to_datetime(
298
self.properties["LastApprovalRequest"])
299
if last_approval_request is not None:
300
timer = timeout - (datetime.datetime.utcnow()
301
- last_approval_request)
303
timer = datetime.timedelta()
304
if self.properties["ApprovedByDefault"]:
305
message = "Approval in {0}. (d)eny?"
307
message = "Denial in {0}. (a)pprove?"
308
message = message.format(unicode(timer).rsplit(".", 1)[0])
309
elif self.properties["LastCheckerStatus"] != 0:
310
# When checker has failed, show timer until client expires
311
expires = self.properties["Expires"]
313
timer = datetime.timedelta(0)
315
expires = (datetime.datetime.strptime
316
(expires, '%Y-%m-%dT%H:%M:%S.%f'))
317
timer = expires - datetime.datetime.utcnow()
318
message = ('A checker has failed! Time until client'
319
' gets disabled: {0}'
320
.format(unicode(timer).rsplit(".", 1)[0]))
323
self._text = "{0}{1}".format(base, message)
179
325
if not urwid.supports_unicode():
180
326
self._text = self._text.encode("ascii", "replace")
181
textlist = [(u"normal", self._text)]
327
textlist = [("normal", self._text)]
182
328
self._text_widget.set_text(textlist)
183
329
self._focus_text_widget.set_text([(with_standout[text[0]],
192
338
if self.update_hook is not None:
193
339
self.update_hook()
341
def update_timer(self):
342
"""called by gobject. Will indefinitely loop until
343
gobject.source_remove() on tag is called"""
345
return True # Keep calling this
347
def delete(self, *args, **kwargs):
348
if self._update_timer_callback_tag is not None:
349
gobject.source_remove(self._update_timer_callback_tag)
350
self._update_timer_callback_tag = None
351
for match in self.match_objects:
353
self.match_objects = ()
196
354
if self.delete_hook is not None:
197
355
self.delete_hook(self)
356
return super(MandosClientWidget, self).delete(*args, **kwargs)
199
def render(self, (maxcol,), focus=False):
358
def render(self, maxcolrow, focus=False):
200
359
"""Render differently if we have focus.
201
360
This overrides the method from urwid.FlowWidget"""
202
return self.current_widget(focus).render((maxcol,),
361
return self.current_widget(focus).render(maxcolrow,
205
def keypress(self, (maxcol,), key):
364
def keypress(self, maxcolrow, key):
207
366
This overrides the method from urwid.FlowWidget"""
208
if key == u"e" or key == u"+":
210
elif key == u"d" or key == u"-":
212
elif key == u"r" or key == u"_" or key == u"ctrl k":
368
self.proxy.Enable(dbus_interface = client_interface,
371
self.proxy.Disable(dbus_interface = client_interface,
374
self.proxy.Approve(dbus.Boolean(True, variant_level=1),
375
dbus_interface = client_interface,
378
self.proxy.Approve(dbus.Boolean(False, variant_level=1),
379
dbus_interface = client_interface,
381
elif key == "R" or key == "_" or key == "ctrl k":
213
382
self.server_proxy_object.RemoveClient(self.proxy
216
self.proxy.StartChecker()
218
self.proxy.StopChecker()
220
self.proxy.CheckedOK()
386
self.proxy.StartChecker(dbus_interface = client_interface,
389
self.proxy.StopChecker(dbus_interface = client_interface,
392
self.proxy.CheckedOK(dbus_interface = client_interface,
222
# elif key == u"p" or key == "=":
395
# elif key == "p" or key == "=":
223
396
# self.proxy.pause()
224
# elif key == u"u" or key == ":":
397
# elif key == "u" or key == ":":
225
398
# self.proxy.unpause()
226
# elif key == u"RET":
262
435
self.screen = urwid.curses_display.Screen()
264
437
self.screen.register_palette((
266
u"default", u"default", None),
268
u"default", u"default", u"bold"),
270
u"default", u"default", u"underline"),
272
u"default", u"default", u"standout"),
273
(u"bold-underline-blink",
274
u"default", u"default", (u"bold", u"underline")),
276
u"default", u"default", (u"bold", u"standout")),
277
(u"underline-blink-standout",
278
u"default", u"default", (u"underline", u"standout")),
279
(u"bold-underline-blink-standout",
280
u"default", u"default", (u"bold", u"underline",
439
"default", "default", None),
441
"default", "default", "bold"),
443
"default", "default", "underline"),
445
"default", "default", "standout"),
446
("bold-underline-blink",
447
"default", "default", ("bold", "underline")),
449
"default", "default", ("bold", "standout")),
450
("underline-blink-standout",
451
"default", "default", ("underline", "standout")),
452
("bold-underline-blink-standout",
453
"default", "default", ("bold", "underline",
284
457
if urwid.supports_unicode():
285
self.divider = u"─" # \u2500
286
#self.divider = u"━" # \u2501
458
self.divider = "─" # \u2500
459
#self.divider = "━" # \u2501
288
#self.divider = u"-" # \u002d
289
self.divider = u"_" # \u005f
461
#self.divider = "-" # \u002d
462
self.divider = "_" # \u005f
291
464
self.screen.start()
306
479
# This keeps track of whether self.uilist currently has
307
480
# self.logbox in it or not
308
481
self.log_visible = True
309
self.log_wrap = u"any"
482
self.log_wrap = "any"
312
self.log_message_raw((u"bold",
313
u"Mandos Monitor version " + version))
314
self.log_message_raw((u"bold",
485
self.log_message_raw(("bold",
486
"Mandos Monitor version " + version))
487
self.log_message_raw(("bold",
317
490
self.busname = domain + '.Mandos'
318
491
self.main_loop = gobject.MainLoop()
319
self.bus = dbus.SystemBus()
320
mandos_dbus_objc = self.bus.get_object(
321
self.busname, u"/", follow_name_owner_changes=True)
322
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
326
mandos_clients = (self.mandos_serv
327
.GetAllClientsWithProperties())
328
except dbus.exceptions.DBusException:
329
mandos_clients = dbus.Dictionary()
332
.connect_to_signal(u"ClientRemoved",
333
self.find_and_remove_client,
334
dbus_interface=server_interface,
337
.connect_to_signal(u"ClientAdded",
339
dbus_interface=server_interface,
342
.connect_to_signal(u"ClientNotFound",
343
self.client_not_found,
344
dbus_interface=server_interface,
346
for path, client in mandos_clients.iteritems():
347
client_proxy_object = self.bus.get_object(self.busname,
349
self.add_client(MandosClientWidget(server_proxy_object
352
=client_proxy_object,
362
493
def client_not_found(self, fingerprint, address):
363
self.log_message((u"Client with address %s and fingerprint %s"
364
u" could not be found" % (address,
494
self.log_message("Client with address {0} and fingerprint"
495
" {1} could not be found"
496
.format(address, fingerprint))
367
498
def rebuild(self):
368
499
"""This rebuilds the User Interface.
369
500
Call this when the widget layout needs to change"""
371
502
#self.uilist.append(urwid.ListBox(self.clients))
372
self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
503
self.uilist.append(urwid.Frame(ConstrainedListBox(self.
373
505
#header=urwid.Divider(),
375
footer=urwid.Divider(div_char=self.divider)))
508
urwid.Divider(div_char=
376
510
if self.log_visible:
377
511
self.uilist.append(self.logbox)
379
512
self.topwidget = urwid.Pile(self.uilist)
381
514
def log_message(self, message):
382
515
timestamp = datetime.datetime.now().isoformat()
383
self.log_message_raw(timestamp + u": " + message)
516
self.log_message_raw(timestamp + ": " + message)
385
518
def log_message_raw(self, markup):
386
519
"""Add a log message to the log buffer."""
389
522
and len(self.log) > self.max_log_length):
390
523
del self.log[0:len(self.log)-self.max_log_length-1]
391
524
self.logbox.set_focus(len(self.logbox.body.contents),
392
coming_from=u"above")
395
528
def toggle_log_display(self):
396
529
"""Toggle visibility of the log buffer."""
397
530
self.log_visible = not self.log_visible
399
self.log_message(u"Log visibility changed to: "
400
+ unicode(self.log_visible))
532
#self.log_message("Log visibility changed to: "
533
# + unicode(self.log_visible))
402
535
def change_log_display(self):
403
536
"""Change type of log display.
404
537
Currently, this toggles wrapping of text lines."""
405
if self.log_wrap == u"clip":
406
self.log_wrap = u"any"
538
if self.log_wrap == "clip":
539
self.log_wrap = "any"
408
self.log_wrap = u"clip"
541
self.log_wrap = "clip"
409
542
for textwidget in self.log:
410
543
textwidget.set_wrap_mode(self.log_wrap)
411
self.log_message(u"Wrap mode: " + self.log_wrap)
544
#self.log_message("Wrap mode: " + self.log_wrap)
413
546
def find_and_remove_client(self, path, name):
414
"""Find an client from its object path and remove it.
547
"""Find a client by its object path and remove it.
416
549
This is connected to the ClientRemoved signal from the
417
550
Mandos server object."""
465
600
"""Start the main loop and exit when it's done."""
601
self.bus = dbus.SystemBus()
602
mandos_dbus_objc = self.bus.get_object(
603
self.busname, "/", follow_name_owner_changes=True)
604
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
608
mandos_clients = (self.mandos_serv
609
.GetAllClientsWithProperties())
610
except dbus.exceptions.DBusException:
611
mandos_clients = dbus.Dictionary()
614
.connect_to_signal("ClientRemoved",
615
self.find_and_remove_client,
616
dbus_interface=server_interface,
619
.connect_to_signal("ClientAdded",
621
dbus_interface=server_interface,
624
.connect_to_signal("ClientNotFound",
625
self.client_not_found,
626
dbus_interface=server_interface,
628
for path, client in mandos_clients.iteritems():
629
client_proxy_object = self.bus.get_object(self.busname,
631
self.add_client(MandosClientWidget(server_proxy_object
634
=client_proxy_object,
467
645
self._input_callback_tag = (gobject.io_add_watch
468
646
(sys.stdin.fileno(),
494
672
except KeyError: # :-)
497
if key == u"q" or key == u"Q":
675
if key == "q" or key == "Q":
500
elif key == u"window resize":
678
elif key == "window resize":
501
679
self.size = self.screen.get_cols_rows()
503
elif key == u"\f": # Ctrl-L
681
elif key == "\f": # Ctrl-L
505
elif key == u"l" or key == u"D":
683
elif key == "l" or key == "D":
506
684
self.toggle_log_display()
508
elif key == u"w" or key == u"i":
686
elif key == "w" or key == "i":
509
687
self.change_log_display()
511
elif key == u"?" or key == u"f1" or key == u"esc":
689
elif key == "?" or key == "f1" or key == "esc":
512
690
if not self.log_visible:
513
691
self.log_visible = True
515
self.log_message_raw((u"bold",
519
u"l: Log window toggle",
520
u"TAB: Switch window",
522
self.log_message_raw((u"bold",
528
u"s: Start new checker",
693
self.log_message_raw(("bold",
697
"l: Log window toggle",
698
"TAB: Switch window",
700
self.log_message_raw(("bold",
706
"s: Start new checker",
533
713
if self.topwidget.get_focus() is self.logbox:
534
714
self.topwidget.set_focus(0)
536
716
self.topwidget.set_focus(self.logbox)
538
#elif (key == u"end" or key == u"meta >" or key == u"G"
718
#elif (key == "end" or key == "meta >" or key == "G"
540
720
# pass # xxx end-of-buffer
541
#elif (key == u"home" or key == u"meta <" or key == u"g"
721
#elif (key == "home" or key == "meta <" or key == "g"
543
723
# pass # xxx beginning-of-buffer
544
#elif key == u"ctrl e" or key == u"$":
724
#elif key == "ctrl e" or key == "$":
545
725
# pass # xxx move-end-of-line
546
#elif key == u"ctrl a" or key == u"^":
726
#elif key == "ctrl a" or key == "^":
547
727
# pass # xxx move-beginning-of-line
548
#elif key == u"ctrl b" or key == u"meta (" or key == u"h":
728
#elif key == "ctrl b" or key == "meta (" or key == "h":
549
729
# pass # xxx left
550
#elif key == u"ctrl f" or key == u"meta )" or key == u"l":
730
#elif key == "ctrl f" or key == "meta )" or key == "l":
551
731
# pass # xxx right
553
733
# pass # scroll up log
555
735
# pass # scroll down log
556
736
elif self.topwidget.selectable():
557
737
self.topwidget.keypress(self.size, key)