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,
 
 
86
130
        self.logger = logger
 
 
132
        self._update_timer_callback_tag = None
 
 
133
        self._update_timer_callback_lock = 0
 
88
135
        # The widget shown normally
 
89
 
        self._text_widget = urwid.Text(u"")
 
 
136
        self._text_widget = urwid.Text("")
 
90
137
        # The widget shown when we have focus
 
91
 
        self._focus_text_widget = urwid.Text(u"")
 
 
138
        self._focus_text_widget = urwid.Text("")
 
92
139
        super(MandosClientWidget, self).__init__(
 
93
140
            update_hook=update_hook, delete_hook=delete_hook,
 
96
143
        self.opened = False
 
97
 
        self.proxy.connect_to_signal(u"CheckerCompleted",
 
98
 
                                     self.checker_completed,
 
101
 
        self.proxy.connect_to_signal(u"CheckerStarted",
 
102
 
                                     self.checker_started,
 
105
 
        self.proxy.connect_to_signal(u"GotSecret",
 
109
 
        self.proxy.connect_to_signal(u"NeedApproval",
 
113
 
        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 %s' % (self.properties["Name"]))
 
 
177
    def property_changed(self, property=None, value=None):
 
 
178
        super(self, MandosClientWidget).property_changed(property,
 
 
180
        if property == "ApprovalPending":
 
 
181
            using_timer(bool(value))
 
 
182
        if property == "LastCheckerStatus":
 
 
183
            using_timer(value != 0)
 
 
184
            #self.logger('Checker for client %s (command "%s")'
 
 
186
            #            % (self.properties["Name"], command))
 
 
188
    def using_timer(self, flag):
 
 
189
        """Call this method with True or False when timer should be
 
 
190
        activated or deactivated.
 
 
192
        old = self._update_timer_callback_lock
 
 
194
            self._update_timer_callback_lock += 1
 
 
196
            self._update_timer_callback_lock -= 1
 
 
197
        if old == 0 and self._update_timer_callback_lock:
 
 
198
            # Will update the shown timer value every second
 
 
199
            self._update_timer_callback_tag = (gobject.timeout_add
 
 
202
        elif old and self._update_timer_callback_lock == 0:
 
 
203
            gobject.source_remove(self._update_timer_callback_tag)
 
 
204
            self._update_timer_callback_tag = None
 
118
206
    def checker_completed(self, exitstatus, condition, command):
 
119
207
        if exitstatus == 0:
 
120
 
            #self.logger(u'Checker for client %s (command "%s")'
 
122
 
            #            % (self.properties[u"name"], command))
 
124
211
        if os.WIFEXITED(condition):
 
125
 
            self.logger(u'Checker for client %s (command "%s")'
 
126
 
                        u' failed with exit code %s'
 
127
 
                        % (self.properties[u"name"], command,
 
 
212
            self.logger('Checker for client %s (command "%s")'
 
 
213
                        ' failed with exit code %s'
 
 
214
                        % (self.properties["Name"], command,
 
128
215
                           os.WEXITSTATUS(condition)))
 
130
 
        if os.WIFSIGNALED(condition):
 
131
 
            self.logger(u'Checker for client %s (command "%s")'
 
132
 
                        u' was killed by signal %s'
 
133
 
                        % (self.properties[u"name"], command,
 
 
216
        elif os.WIFSIGNALED(condition):
 
 
217
            self.logger('Checker for client %s (command "%s")'
 
 
218
                        ' was killed by signal %s'
 
 
219
                        % (self.properties["Name"], command,
 
134
220
                           os.WTERMSIG(condition)))
 
136
 
        if os.WCOREDUMP(condition):
 
137
 
            self.logger(u'Checker for client %s (command "%s")'
 
139
 
                        % (self.properties[u"name"], command))
 
140
 
        self.logger(u'Checker for client %s completed mysteriously')
 
 
221
        elif os.WCOREDUMP(condition):
 
 
222
            self.logger('Checker for client %s (command "%s")'
 
 
224
                        % (self.properties["Name"], command))
 
 
226
            self.logger('Checker for client %s completed'
 
142
230
    def checker_started(self, command):
 
143
 
        #self.logger(u'Client %s started checker "%s"'
 
144
 
        #            % (self.properties[u"name"], unicode(command)))
 
 
231
        """Server signals that a checker started. This could be useful
 
 
232
           to log in the future. """
 
 
233
        #self.logger('Client %s started checker "%s"'
 
 
234
        #            % (self.properties["Name"], unicode(command)))
 
147
237
    def got_secret(self):
 
148
 
        self.logger(u'Client %s received its secret'
 
149
 
                    % self.properties[u"name"])
 
 
238
        self.logger('Client %s received its secret'
 
 
239
                    % self.properties["Name"])
 
151
241
    def need_approval(self, timeout, default):
 
153
 
            message = u'Client %s needs approval within %s seconds'
 
 
243
            message = 'Client %s needs approval within %s seconds'
 
155
 
            message = u'Client %s will get its secret in %s seconds'
 
 
245
            message = 'Client %s will get its secret in %s seconds'
 
156
246
        self.logger(message
 
157
 
                    % (self.properties[u"name"], timeout/1000))
 
 
247
                    % (self.properties["Name"], timeout/1000))
 
 
248
        self.using_timer(True)
 
159
250
    def rejected(self, reason):
 
160
 
        self.logger(u'Client %s was rejected; reason: %s'
 
161
 
                    % (self.properties[u"name"], reason))
 
 
251
        self.logger('Client %s was rejected; reason: %s'
 
 
252
                    % (self.properties["Name"], reason))
 
163
254
    def selectable(self):
 
164
255
        """Make this a "selectable" widget.
 
165
256
        This overrides the method from urwid.FlowWidget."""
 
168
 
    def rows(self, (maxcol,), focus=False):
 
 
259
    def rows(self, maxcolrow, focus=False):
 
169
260
        """How many rows this widget will occupy might depend on
 
170
261
        whether we have focus or not.
 
171
262
        This overrides the method from urwid.FlowWidget"""
 
172
 
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
 
263
        return self.current_widget(focus).rows(maxcolrow, focus=focus)
 
174
265
    def current_widget(self, focus=False):
 
175
266
        if focus or self.opened:
 
 
179
270
    def update(self):
 
180
271
        "Called when what is visible on the screen should be updated."
 
181
272
        # How to add standout mode to a style
 
182
 
        with_standout = { u"normal": u"standout",
 
183
 
                          u"bold": u"bold-standout",
 
185
 
                              u"underline-blink-standout",
 
186
 
                          u"bold-underline-blink":
 
187
 
                              u"bold-underline-blink-standout",
 
 
273
        with_standout = { "normal": "standout",
 
 
274
                          "bold": "bold-standout",
 
 
276
                              "underline-blink-standout",
 
 
277
                          "bold-underline-blink":
 
 
278
                              "bold-underline-blink-standout",
 
190
281
        # Rebuild focus and non-focus widgets using current properties
 
192
283
        # Base part of a client. Name!
 
193
 
        self._text = (u'%(name)s: '
 
194
 
                      % {u"name": self.properties[u"name"]})
 
196
 
        if self.properties[u"approved_pending"]:
 
197
 
            if self.properties[u"approved_by_default"]:
 
198
 
                self._text += u"Connection established to client. (d)eny?"
 
200
 
                self._text += u"Seeks approval to send secret. (a)pprove?"
 
 
285
                      % {"name": self.properties["Name"]})
 
 
286
        if not self.properties["Enabled"]:
 
 
288
        elif self.properties["ApprovalPending"]:
 
 
289
            timeout = datetime.timedelta(milliseconds
 
 
292
            last_approval_request = isoformat_to_datetime(
 
 
293
                self.properties["LastApprovalRequest"])
 
 
294
            if last_approval_request is not None:
 
 
295
                timer = timeout - (datetime.datetime.utcnow()
 
 
296
                                   - last_approval_request)
 
 
298
                timer = datetime.timedelta()
 
 
299
            if self.properties["ApprovedByDefault"]:
 
 
300
                message = "Approval in %s. (d)eny?"
 
 
302
                message = "Denial in %s. (a)pprove?"
 
 
303
            message = message % unicode(timer).rsplit(".", 1)[0]
 
 
304
        elif self.properties["LastCheckerStatus"] != 0:
 
 
305
            # When checker has failed, print a timer until client expires
 
 
306
            expires = self.properties["Expires"]
 
 
308
                timer = datetime.timedelta(0)
 
 
310
                expires = datetime.datetime.strptime(expires,
 
 
311
                                                     '%Y-%m-%dT%H:%M:%S.%f')
 
 
312
                timer = expires - datetime.datetime.utcnow()
 
 
313
            message = ('A checker has failed! Time until client'
 
 
315
                           % unicode(timer).rsplit(".", 1)[0])
 
202
 
            self._text += (u'%(enabled)s'
 
205
 
                                if self.properties[u"enabled"]
 
 
318
        self._text = "%s%s" % (base, message)
 
207
320
        if not urwid.supports_unicode():
 
208
321
            self._text = self._text.encode("ascii", "replace")
 
209
 
        textlist = [(u"normal", self._text)]
 
 
322
        textlist = [("normal", self._text)]
 
210
323
        self._text_widget.set_text(textlist)
 
211
324
        self._focus_text_widget.set_text([(with_standout[text[0]],
 
 
220
333
        if self.update_hook is not None:
 
221
334
            self.update_hook()
 
 
336
    def update_timer(self):
 
 
337
        """called by gobject. Will indefinitely loop until
 
 
338
        gobject.source_remove() on tag is called"""
 
 
340
        return True             # Keep calling this
 
 
342
    def delete(self, *args, **kwargs):
 
 
343
        if self._update_timer_callback_tag is not None:
 
 
344
            gobject.source_remove(self._update_timer_callback_tag)
 
 
345
            self._update_timer_callback_tag = None
 
 
346
        for match in self.match_objects:
 
 
348
        self.match_objects = ()
 
224
349
        if self.delete_hook is not None:
 
225
350
            self.delete_hook(self)
 
 
351
        return super(MandosClientWidget, self).delete(*args, **kwargs)
 
227
 
    def render(self, (maxcol,), focus=False):
 
 
353
    def render(self, maxcolrow, focus=False):
 
228
354
        """Render differently if we have focus.
 
229
355
        This overrides the method from urwid.FlowWidget"""
 
230
 
        return self.current_widget(focus).render((maxcol,),
 
 
356
        return self.current_widget(focus).render(maxcolrow,
 
233
 
    def keypress(self, (maxcol,), key):
 
 
359
    def keypress(self, maxcolrow, key):
 
235
361
        This overrides the method from urwid.FlowWidget"""
 
237
 
            self.proxy.Enable(dbus_interface = client_interface)
 
239
 
            self.proxy.Disable(dbus_interface = client_interface)
 
 
363
            self.proxy.Enable(dbus_interface = client_interface,
 
 
366
            self.proxy.Disable(dbus_interface = client_interface,
 
241
369
            self.proxy.Approve(dbus.Boolean(True, variant_level=1),
 
242
 
                               dbus_interface = client_interface)
 
 
370
                               dbus_interface = client_interface,
 
244
373
            self.proxy.Approve(dbus.Boolean(False, variant_level=1),
 
245
 
                                  dbus_interface = client_interface)
 
246
 
        elif key == u"r" or key == u"_" or key == u"ctrl k":
 
 
374
                                  dbus_interface = client_interface,
 
 
376
        elif key == "R" or key == "_" or key == "ctrl k":
 
247
377
            self.server_proxy_object.RemoveClient(self.proxy
 
250
 
            self.proxy.StartChecker(dbus_interface = client_interface)
 
252
 
            self.proxy.StopChecker(dbus_interface = client_interface)
 
254
 
            self.proxy.CheckedOK(dbus_interface = client_interface)
 
 
381
            self.proxy.StartChecker(dbus_interface = client_interface,
 
 
384
            self.proxy.StopChecker(dbus_interface = client_interface,
 
 
387
            self.proxy.CheckedOK(dbus_interface = client_interface,
 
256
 
#         elif key == u"p" or key == "=":
 
 
390
#         elif key == "p" or key == "=":
 
257
391
#             self.proxy.pause()
 
258
 
#         elif key == u"u" or key == ":":
 
 
392
#         elif key == "u" or key == ":":
 
259
393
#             self.proxy.unpause()
 
260
 
#         elif key == u"RET":
 
263
 
#            self.proxy.Approve(True)
 
265
 
#            self.proxy.Approve(False)
 
 
300
430
        self.screen = urwid.curses_display.Screen()
 
302
432
        self.screen.register_palette((
 
304
 
                 u"default", u"default", None),
 
306
 
                 u"default", u"default", u"bold"),
 
308
 
                 u"default", u"default", u"underline"),
 
310
 
                 u"default", u"default", u"standout"),
 
311
 
                (u"bold-underline-blink",
 
312
 
                 u"default", u"default", (u"bold", u"underline")),
 
314
 
                 u"default", u"default", (u"bold", u"standout")),
 
315
 
                (u"underline-blink-standout",
 
316
 
                 u"default", u"default", (u"underline", u"standout")),
 
317
 
                (u"bold-underline-blink-standout",
 
318
 
                 u"default", u"default", (u"bold", u"underline",
 
 
434
                 "default", "default", None),
 
 
436
                 "default", "default", "bold"),
 
 
438
                 "default", "default", "underline"),
 
 
440
                 "default", "default", "standout"),
 
 
441
                ("bold-underline-blink",
 
 
442
                 "default", "default", ("bold", "underline")),
 
 
444
                 "default", "default", ("bold", "standout")),
 
 
445
                ("underline-blink-standout",
 
 
446
                 "default", "default", ("underline", "standout")),
 
 
447
                ("bold-underline-blink-standout",
 
 
448
                 "default", "default", ("bold", "underline",
 
322
452
        if urwid.supports_unicode():
 
323
 
            self.divider = u"─" # \u2500
 
324
 
            #self.divider = u"━" # \u2501
 
 
453
            self.divider = "─" # \u2500
 
 
454
            #self.divider = "━" # \u2501
 
326
 
            #self.divider = u"-" # \u002d
 
327
 
            self.divider = u"_" # \u005f
 
 
456
            #self.divider = "-" # \u002d
 
 
457
            self.divider = "_" # \u005f
 
329
459
        self.screen.start()
 
 
344
474
        # This keeps track of whether self.uilist currently has
 
345
475
        # self.logbox in it or not
 
346
476
        self.log_visible = True
 
347
 
        self.log_wrap = u"any"
 
 
477
        self.log_wrap = "any"
 
350
 
        self.log_message_raw((u"bold",
 
351
 
                              u"Mandos Monitor version " + version))
 
352
 
        self.log_message_raw((u"bold",
 
 
480
        self.log_message_raw(("bold",
 
 
481
                              "Mandos Monitor version " + version))
 
 
482
        self.log_message_raw(("bold",
 
355
485
        self.busname = domain + '.Mandos'
 
356
486
        self.main_loop = gobject.MainLoop()
 
357
 
        self.bus = dbus.SystemBus()
 
358
 
        mandos_dbus_objc = self.bus.get_object(
 
359
 
            self.busname, u"/", follow_name_owner_changes=True)
 
360
 
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
364
 
            mandos_clients = (self.mandos_serv
 
365
 
                              .GetAllClientsWithProperties())
 
366
 
        except dbus.exceptions.DBusException:
 
367
 
            mandos_clients = dbus.Dictionary()
 
370
 
         .connect_to_signal(u"ClientRemoved",
 
371
 
                            self.find_and_remove_client,
 
372
 
                            dbus_interface=server_interface,
 
375
 
         .connect_to_signal(u"ClientAdded",
 
377
 
                            dbus_interface=server_interface,
 
380
 
         .connect_to_signal(u"ClientNotFound",
 
381
 
                            self.client_not_found,
 
382
 
                            dbus_interface=server_interface,
 
384
 
        for path, client in mandos_clients.iteritems():
 
385
 
            client_proxy_object = self.bus.get_object(self.busname,
 
387
 
            self.add_client(MandosClientWidget(server_proxy_object
 
390
 
                                               =client_proxy_object,
 
400
488
    def client_not_found(self, fingerprint, address):
 
401
 
        self.log_message((u"Client with address %s and fingerprint %s"
 
402
 
                          u" could not be found" % (address,
 
 
489
        self.log_message(("Client with address %s and fingerprint %s"
 
 
490
                          " could not be found" % (address,
 
405
493
    def rebuild(self):
 
 
427
517
            and len(self.log) > self.max_log_length):
 
428
518
            del self.log[0:len(self.log)-self.max_log_length-1]
 
429
519
        self.logbox.set_focus(len(self.logbox.body.contents),
 
430
 
                              coming_from=u"above")
 
433
523
    def toggle_log_display(self):
 
434
524
        """Toggle visibility of the log buffer."""
 
435
525
        self.log_visible = not self.log_visible
 
437
 
        self.log_message(u"Log visibility changed to: "
 
438
 
                         + unicode(self.log_visible))
 
 
527
        #self.log_message("Log visibility changed to: "
 
 
528
        #                 + unicode(self.log_visible))
 
440
530
    def change_log_display(self):
 
441
531
        """Change type of log display.
 
442
532
        Currently, this toggles wrapping of text lines."""
 
443
 
        if self.log_wrap == u"clip":
 
444
 
            self.log_wrap = u"any"
 
 
533
        if self.log_wrap == "clip":
 
 
534
            self.log_wrap = "any"
 
446
 
            self.log_wrap = u"clip"
 
 
536
            self.log_wrap = "clip"
 
447
537
        for textwidget in self.log:
 
448
538
            textwidget.set_wrap_mode(self.log_wrap)
 
449
 
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
 
539
        #self.log_message("Wrap mode: " + self.log_wrap)
 
451
541
    def find_and_remove_client(self, path, name):
 
452
 
        """Find an client from its object path and remove it.
 
 
542
        """Find a client by its object path and remove it.
 
454
544
        This is connected to the ClientRemoved signal from the
 
455
545
        Mandos server object."""
 
 
503
595
        """Start the main loop and exit when it's done."""
 
 
596
        self.bus = dbus.SystemBus()
 
 
597
        mandos_dbus_objc = self.bus.get_object(
 
 
598
            self.busname, "/", follow_name_owner_changes=True)
 
 
599
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
 
603
            mandos_clients = (self.mandos_serv
 
 
604
                              .GetAllClientsWithProperties())
 
 
605
        except dbus.exceptions.DBusException:
 
 
606
            mandos_clients = dbus.Dictionary()
 
 
609
         .connect_to_signal("ClientRemoved",
 
 
610
                            self.find_and_remove_client,
 
 
611
                            dbus_interface=server_interface,
 
 
614
         .connect_to_signal("ClientAdded",
 
 
616
                            dbus_interface=server_interface,
 
 
619
         .connect_to_signal("ClientNotFound",
 
 
620
                            self.client_not_found,
 
 
621
                            dbus_interface=server_interface,
 
 
623
        for path, client in mandos_clients.iteritems():
 
 
624
            client_proxy_object = self.bus.get_object(self.busname,
 
 
626
            self.add_client(MandosClientWidget(server_proxy_object
 
 
629
                                               =client_proxy_object,
 
505
640
        self._input_callback_tag = (gobject.io_add_watch
 
506
641
                                    (sys.stdin.fileno(),
 
 
532
667
            except KeyError:    # :-)
 
535
 
            if key == u"q" or key == u"Q":
 
 
670
            if key == "q" or key == "Q":
 
538
 
            elif key == u"window resize":
 
 
673
            elif key == "window resize":
 
539
674
                self.size = self.screen.get_cols_rows()
 
541
 
            elif key == u"\f":  # Ctrl-L
 
 
676
            elif key == "\f":  # Ctrl-L
 
543
 
            elif key == u"l" or key == u"D":
 
 
678
            elif key == "l" or key == "D":
 
544
679
                self.toggle_log_display()
 
546
 
            elif key == u"w" or key == u"i":
 
 
681
            elif key == "w" or key == "i":
 
547
682
                self.change_log_display()
 
549
 
            elif key == u"?" or key == u"f1" or key == u"esc":
 
 
684
            elif key == "?" or key == "f1" or key == "esc":
 
550
685
                if not self.log_visible:
 
551
686
                    self.log_visible = True
 
553
 
                self.log_message_raw((u"bold",
 
557
 
                                            u"l: Log window toggle",
 
558
 
                                            u"TAB: Switch window",
 
560
 
                self.log_message_raw((u"bold",
 
566
 
                                             u"s: Start new checker",
 
 
688
                self.log_message_raw(("bold",
 
 
692
                                            "l: Log window toggle",
 
 
693
                                            "TAB: Switch window",
 
 
695
                self.log_message_raw(("bold",
 
 
701
                                             "s: Start new checker",
 
573
708
                if self.topwidget.get_focus() is self.logbox:
 
574
709
                    self.topwidget.set_focus(0)
 
576
711
                    self.topwidget.set_focus(self.logbox)
 
578
 
            #elif (key == u"end" or key == u"meta >" or key == u"G"
 
 
713
            #elif (key == "end" or key == "meta >" or key == "G"
 
580
715
            #    pass            # xxx end-of-buffer
 
581
 
            #elif (key == u"home" or key == u"meta <" or key == u"g"
 
 
716
            #elif (key == "home" or key == "meta <" or key == "g"
 
583
718
            #    pass            # xxx beginning-of-buffer
 
584
 
            #elif key == u"ctrl e" or key == u"$":
 
 
719
            #elif key == "ctrl e" or key == "$":
 
585
720
            #    pass            # xxx move-end-of-line
 
586
 
            #elif key == u"ctrl a" or key == u"^":
 
 
721
            #elif key == "ctrl a" or key == "^":
 
587
722
            #    pass            # xxx move-beginning-of-line
 
588
 
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
 
723
            #elif key == "ctrl b" or key == "meta (" or key == "h":
 
589
724
            #    pass            # xxx left
 
590
 
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
 
725
            #elif key == "ctrl f" or key == "meta )" or key == "l":
 
591
726
            #    pass            # xxx right
 
593
728
            #    pass            # scroll up log
 
595
730
            #    pass            # scroll down log
 
596
731
            elif self.topwidget.selectable():
 
597
732
                self.topwidget.keypress(self.size, key)