/mandos/release

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/release
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
1
#!/usr/bin/python
2
# -*- mode: python; coding: utf-8 -*-
3
4
from __future__ import division, absolute_import, with_statement
5
6
import sys
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
7
import os
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
8
import signal
9
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
10
import datetime
11
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
12
import urwid.curses_display
13
import urwid
14
15
from dbus.mainloop.glib import DBusGMainLoop
16
import gobject
17
18
import dbus
19
20
import UserList
21
237.2.174 by Teddy Hogeborn
More consistent terminology: Clients are no longer "invalid" - they
22
import locale
23
24
locale.setlocale(locale.LC_ALL, u'')
25
24.1.153 by Björn Påhlsson
early commit to ease todays coding
26
import logging
27
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
28
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
29
# Some useful constants
30
domain = 'se.bsnet.fukt'
31
server_interface = domain + '.Mandos'
32
client_interface = domain + '.Mandos.Client'
24.1.153 by Björn Påhlsson
early commit to ease todays coding
33
version = "1.0.15"
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
34
35
# Always run in monochrome mode
36
urwid.curses_display.curses.has_colors = lambda : False
37
38
# Urwid doesn't support blinking, but we want it.  Since we have no
39
# use for underline on its own, we make underline also always blink.
40
urwid.curses_display.curses.A_UNDERLINE |= (
41
    urwid.curses_display.curses.A_BLINK)
42
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
43
def isoformat_to_datetime(iso):
44
    "Parse an ISO 8601 date string to a datetime.datetime()"
45
    if not iso:
46
        return None
47
    d, t = iso.split(u"T", 1)
48
    year, month, day = d.split(u"-", 2)
49
    hour, minute, second = t.split(u":", 2)
50
    second, fraction = divmod(float(second), 1)
51
    return datetime.datetime(int(year),
52
                             int(month),
53
                             int(day),
54
                             int(hour),
55
                             int(minute),
56
                             int(second),           # Whole seconds
57
                             int(fraction*1000000)) # Microseconds
58
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
59
class MandosClientPropertyCache(object):
60
    """This wraps a Mandos Client D-Bus proxy object, caches the
61
    properties and calls a hook function when any of them are
62
    changed.
63
    """
237.2.174 by Teddy Hogeborn
More consistent terminology: Clients are no longer "invalid" - they
64
    def __init__(self, proxy_object=None, *args, **kwargs):
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
65
        self.proxy = proxy_object # Mandos Client proxy object
66
        
237.2.174 by Teddy Hogeborn
More consistent terminology: Clients are no longer "invalid" - they
67
        self.properties = dict()
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
68
        self.proxy.connect_to_signal(u"PropertyChanged",
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
69
                                     self.property_changed,
70
                                     client_interface,
71
                                     byte_arrays=True)
24.1.153 by Björn Påhlsson
early commit to ease todays coding
72
        
237.2.174 by Teddy Hogeborn
More consistent terminology: Clients are no longer "invalid" - they
73
        self.properties.update(
74
            self.proxy.GetAll(client_interface,
75
                              dbus_interface = dbus.PROPERTIES_IFACE))
24.1.154 by Björn Påhlsson
merge
76
77
        #XXX This break good super behaviour!
78
#        super(MandosClientPropertyCache, self).__init__(
79
#            *args, **kwargs)
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
80
    
81
    def property_changed(self, property=None, value=None):
82
        """This is called whenever we get a PropertyChanged signal
83
        It updates the changed property in the "properties" dict.
84
        """
85
        # Update properties dict with new value
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
86
        self.properties[property] = value
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
87
88
89
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
90
    """A Mandos Client which is visible on the screen.
91
    """
92
    
93
    def __init__(self, server_proxy_object=None, update_hook=None,
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
94
                 delete_hook=None, logger=None, *args, **kwargs):
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
95
        # Called on update
96
        self.update_hook = update_hook
97
        # Called on delete
98
        self.delete_hook = delete_hook
99
        # Mandos Server proxy object
100
        self.server_proxy_object = server_proxy_object
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
101
        # Logger
102
        self.logger = logger
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
103
        
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
104
        self._update_timer_callback_tag = None
237.2.205 by Teddy Hogeborn
* DBUS-API: Document new "LastApprovalRequest" client property.
105
        self._update_timer_callback_lock = 0
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
106
        self.last_checker_failed = False
107
        
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
108
        # The widget shown normally
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
109
        self._text_widget = urwid.Text(u"")
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
110
        # The widget shown when we have focus
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
111
        self._focus_text_widget = urwid.Text(u"")
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
112
        super(MandosClientWidget, self).__init__(
113
            update_hook=update_hook, delete_hook=delete_hook,
114
            *args, **kwargs)
115
        self.update()
116
        self.opened = False
237.2.205 by Teddy Hogeborn
* DBUS-API: Document new "LastApprovalRequest" client property.
117
        
118
        last_checked_ok = isoformat_to_datetime(self.properties
119
                                                [u"LastCheckedOK"])
120
        if last_checked_ok is None:
121
            self.last_checker_failed = True
122
        else:
123
            self.last_checker_failed = ((datetime.datetime.utcnow()
124
                                         - last_checked_ok)
125
                                        > datetime.timedelta
126
                                        (milliseconds=
127
                                         self.properties
128
                                         [u"Interval"]))
129
        
130
        if self.last_checker_failed:
131
            self.using_timer(True)
132
        
133
        if self.need_approval:
134
            self.using_timer(True)
135
        
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
136
        self.proxy.connect_to_signal(u"CheckerCompleted",
137
                                     self.checker_completed,
138
                                     client_interface,
139
                                     byte_arrays=True)
140
        self.proxy.connect_to_signal(u"CheckerStarted",
141
                                     self.checker_started,
142
                                     client_interface,
143
                                     byte_arrays=True)
144
        self.proxy.connect_to_signal(u"GotSecret",
145
                                     self.got_secret,
146
                                     client_interface,
147
                                     byte_arrays=True)
24.1.153 by Björn Påhlsson
early commit to ease todays coding
148
        self.proxy.connect_to_signal(u"NeedApproval",
149
                                     self.need_approval,
150
                                     client_interface,
151
                                     byte_arrays=True)
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
152
        self.proxy.connect_to_signal(u"Rejected",
153
                                     self.rejected,
154
                                     client_interface,
155
                                     byte_arrays=True)
237.2.205 by Teddy Hogeborn
* DBUS-API: Document new "LastApprovalRequest" client property.
156
    
157
    def property_changed(self, property=None, value=None):
158
        super(self, MandosClientWidget).property_changed(property,
159
                                                         value)
160
        if property == u"ApprovalPending":
161
            using_timer(bool(value))
162
        
163
    def using_timer(self, flag):
164
        """Call this method with True or False when timer should be
165
        activated or deactivated.
166
        """
167
        old = self._update_timer_callback_lock
168
        if flag:
169
            self._update_timer_callback_lock += 1
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
170
        else:
237.2.205 by Teddy Hogeborn
* DBUS-API: Document new "LastApprovalRequest" client property.
171
            self._update_timer_callback_lock -= 1
172
        if old == 0 and self._update_timer_callback_lock:
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
173
            self._update_timer_callback_tag = (gobject.timeout_add
174
                                               (1000,
175
                                                self.update_timer))
237.2.205 by Teddy Hogeborn
* DBUS-API: Document new "LastApprovalRequest" client property.
176
        elif old and self._update_timer_callback_lock == 0:
177
            gobject.source_remove(self._update_timer_callback_tag)
178
            self._update_timer_callback_tag = None
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
179
    
180
    def checker_completed(self, exitstatus, condition, command):
181
        if exitstatus == 0:
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
182
            if self.last_checker_failed:
183
                self.last_checker_failed = False
237.2.205 by Teddy Hogeborn
* DBUS-API: Document new "LastApprovalRequest" client property.
184
                self.using_timer(False)
237.2.187 by Teddy Hogeborn
* mandos-monitor (MandosClientWidget.checker_completed): Do not log a
185
            #self.logger(u'Checker for client %s (command "%s")'
186
            #            u' was successful'
187
            #            % (self.properties[u"Name"], command))
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
188
            self.update()
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
189
            return
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
190
        # Checker failed
191
        if not self.last_checker_failed:
192
            self.last_checker_failed = True
237.2.205 by Teddy Hogeborn
* DBUS-API: Document new "LastApprovalRequest" client property.
193
            self.using_timer(True)
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
194
        if os.WIFEXITED(condition):
195
            self.logger(u'Checker for client %s (command "%s")'
196
                        u' failed with exit code %s'
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
197
                        % (self.properties[u"Name"], command,
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
198
                           os.WEXITSTATUS(condition)))
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
199
        elif os.WIFSIGNALED(condition):
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
200
            self.logger(u'Checker for client %s (command "%s")'
201
                        u' was killed by signal %s'
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
202
                        % (self.properties[u"Name"], command,
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
203
                           os.WTERMSIG(condition)))
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
204
        elif os.WCOREDUMP(condition):
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
205
            self.logger(u'Checker for client %s (command "%s")'
206
                        u' dumped core'
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
207
                        % (self.properties[u"Name"], command))
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
208
        else:
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
209
            self.logger(u'Checker for client %s completed'
210
                        u' mysteriously')
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
211
        self.update()
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
212
    
213
    def checker_started(self, command):
24.1.153 by Björn Påhlsson
early commit to ease todays coding
214
        #self.logger(u'Client %s started checker "%s"'
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
215
        #            % (self.properties[u"Name"], unicode(command)))
24.1.153 by Björn Påhlsson
early commit to ease todays coding
216
        pass
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
217
    
218
    def got_secret(self):
24.1.159 by Björn Påhlsson
added approval to mandos-ctl
219
        self.last_checker_failed = False
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
220
        self.logger(u'Client %s received its secret'
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
221
                    % self.properties[u"Name"])
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
222
    
24.1.153 by Björn Påhlsson
early commit to ease todays coding
223
    def need_approval(self, timeout, default):
224
        if not default:
225
            message = u'Client %s needs approval within %s seconds'
226
        else:
227
            message = u'Client %s will get its secret in %s seconds'
228
        self.logger(message
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
229
                    % (self.properties[u"Name"], timeout/1000))
237.2.205 by Teddy Hogeborn
* DBUS-API: Document new "LastApprovalRequest" client property.
230
        self.using_timer(True)
24.1.153 by Björn Påhlsson
early commit to ease todays coding
231
    
232
    def rejected(self, reason):
233
        self.logger(u'Client %s was rejected; reason: %s'
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
234
                    % (self.properties[u"Name"], reason))
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
235
    
236
    def selectable(self):
237
        """Make this a "selectable" widget.
238
        This overrides the method from urwid.FlowWidget."""
239
        return True
240
    
241
    def rows(self, (maxcol,), focus=False):
242
        """How many rows this widget will occupy might depend on
243
        whether we have focus or not.
244
        This overrides the method from urwid.FlowWidget"""
245
        return self.current_widget(focus).rows((maxcol,), focus=focus)
246
    
247
    def current_widget(self, focus=False):
248
        if focus or self.opened:
249
            return self._focus_widget
250
        return self._widget
251
    
252
    def update(self):
253
        "Called when what is visible on the screen should be updated."
254
        # How to add standout mode to a style
255
        with_standout = { u"normal": u"standout",
256
                          u"bold": u"bold-standout",
257
                          u"underline-blink":
258
                              u"underline-blink-standout",
259
                          u"bold-underline-blink":
260
                              u"bold-underline-blink-standout",
261
                          }
24.1.154 by Björn Påhlsson
merge
262
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
263
        # Rebuild focus and non-focus widgets using current properties
24.1.154 by Björn Påhlsson
merge
264
265
        # Base part of a client. Name!
24.1.156 by Björn Påhlsson
merge
266
        base = (u'%(name)s: '
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
267
                      % {u"name": self.properties[u"Name"]})
268
        if not self.properties[u"Enabled"]:
24.1.156 by Björn Påhlsson
merge
269
            message = u"DISABLED"
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
270
        elif self.properties[u"ApprovalPending"]:
237.2.205 by Teddy Hogeborn
* DBUS-API: Document new "LastApprovalRequest" client property.
271
            timeout = datetime.timedelta(milliseconds
272
                                         = self.properties
273
                                         [u"ApprovalDelay"])
274
            last_approval_request = isoformat_to_datetime(
275
                self.properties[u"LastApprovalRequest"])
276
            if last_approval_request is not None:
277
                timer = timeout - (datetime.datetime.utcnow()
278
                                   - last_approval_request)
279
            else:
280
                timer = datetime.timedelta()
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
281
            if self.properties[u"ApprovedByDefault"]:
237.2.205 by Teddy Hogeborn
* DBUS-API: Document new "LastApprovalRequest" client property.
282
                message = u"Approval in %s. (d)eny?"
24.1.159 by Björn Påhlsson
added approval to mandos-ctl
283
            else:
237.2.205 by Teddy Hogeborn
* DBUS-API: Document new "LastApprovalRequest" client property.
284
                message = u"Denial in %s. (a)pprove?"
285
            message = message % unicode(timer).rsplit(".", 1)[0]
24.1.156 by Björn Påhlsson
merge
286
        elif self.last_checker_failed:
287
            timeout = datetime.timedelta(milliseconds
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
288
                                         = self.properties
289
                                         [u"Timeout"])
24.1.156 by Björn Påhlsson
merge
290
            last_ok = isoformat_to_datetime(
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
291
                max((self.properties[u"LastCheckedOK"]
292
                     or self.properties[u"Created"]),
293
                    self.properties[u"LastEnabled"]))
24.1.156 by Björn Påhlsson
merge
294
            timer = timeout - (datetime.datetime.utcnow() - last_ok)
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
295
            message = (u'A checker has failed! Time until client'
237.2.204 by Teddy Hogeborn
* mandos (ClientDBus.__init__): Bug fix: Translate "-" in client names
296
                       u' gets disabled: %s'
24.1.158 by Björn Påhlsson
mandos-monitor: removed milisecondsseconds from countdown.
297
                           % unicode(timer).rsplit(".", 1)[0])
24.1.154 by Björn Påhlsson
merge
298
        else:
24.1.156 by Björn Påhlsson
merge
299
            message = u"enabled"
300
        self._text = "%s%s" % (base, message)
301
            
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
302
        if not urwid.supports_unicode():
303
            self._text = self._text.encode("ascii", "replace")
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
304
        textlist = [(u"normal", self._text)]
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
305
        self._text_widget.set_text(textlist)
306
        self._focus_text_widget.set_text([(with_standout[text[0]],
307
                                           text[1])
308
                                          if isinstance(text, tuple)
309
                                          else text
310
                                          for text in textlist])
311
        self._widget = self._text_widget
312
        self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
313
                                            "standout")
314
        # Run update hook, if any
315
        if self.update_hook is not None:
316
            self.update_hook()
317
    
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
318
    def update_timer(self):
319
        "called by gobject"
320
        self.update()
321
        return True             # Keep calling this
322
    
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
323
    def delete(self):
237.2.180 by Teddy Hogeborn
* mandos (AvahiService.entry_group_state_changed): Better debug log
324
        if self._update_timer_callback_tag is not None:
325
            gobject.source_remove(self._update_timer_callback_tag)
326
            self._update_timer_callback_tag = None
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
327
        if self.delete_hook is not None:
328
            self.delete_hook(self)
329
    
330
    def render(self, (maxcol,), focus=False):
331
        """Render differently if we have focus.
332
        This overrides the method from urwid.FlowWidget"""
333
        return self.current_widget(focus).render((maxcol,),
334
                                                 focus=focus)
335
    
336
    def keypress(self, (maxcol,), key):
337
        """Handle keys.
338
        This overrides the method from urwid.FlowWidget"""
24.1.154 by Björn Påhlsson
merge
339
        if key == u"+":
340
            self.proxy.Enable(dbus_interface = client_interface)
341
        elif key == u"-":
342
            self.proxy.Disable(dbus_interface = client_interface)
343
        elif key == u"a":
344
            self.proxy.Approve(dbus.Boolean(True, variant_level=1),
345
                               dbus_interface = client_interface)
346
        elif key == u"d":
347
            self.proxy.Approve(dbus.Boolean(False, variant_level=1),
348
                                  dbus_interface = client_interface)
237.2.171 by Teddy Hogeborn
* debian/rules: Only set BROKEN_PIE if binutils is a specific range of
349
        elif key == u"r" or key == u"_" or key == u"ctrl k":
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
350
            self.server_proxy_object.RemoveClient(self.proxy
351
                                                  .object_path)
352
        elif key == u"s":
24.1.154 by Björn Påhlsson
merge
353
            self.proxy.StartChecker(dbus_interface = client_interface)
237.2.170 by Teddy Hogeborn
* mandos-monitor (MandosClientWidget): Change "StopChecker" key to "S"
354
        elif key == u"S":
24.1.154 by Björn Påhlsson
merge
355
            self.proxy.StopChecker(dbus_interface = client_interface)
237.2.170 by Teddy Hogeborn
* mandos-monitor (MandosClientWidget): Change "StopChecker" key to "S"
356
        elif key == u"C":
24.1.154 by Björn Påhlsson
merge
357
            self.proxy.CheckedOK(dbus_interface = client_interface)
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
358
        # xxx
359
#         elif key == u"p" or key == "=":
360
#             self.proxy.pause()
361
#         elif key == u"u" or key == ":":
362
#             self.proxy.unpause()
363
#         elif key == u"RET":
364
#             self.open()
365
        else:
366
            return key
367
    
368
    def property_changed(self, property=None, value=None,
369
                         *args, **kwargs):
370
        """Call self.update() if old value is not new value.
371
        This overrides the method from MandosClientPropertyCache"""
372
        property_name = unicode(property)
373
        old_value = self.properties.get(property_name)
374
        super(MandosClientWidget, self).property_changed(
375
            property=property, value=value, *args, **kwargs)
376
        if self.properties.get(property_name) != old_value:
377
            self.update()
378
379
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
380
class ConstrainedListBox(urwid.ListBox):
381
    """Like a normal urwid.ListBox, but will consume all "up" or
382
    "down" key presses, thus not allowing any containing widgets to
383
    use them as an excuse to shift focus away from this widget.
384
    """
385
    def keypress(self, (maxcol, maxrow), key):
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
386
        ret = super(ConstrainedListBox, self).keypress((maxcol,
387
                                                        maxrow), key)
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
388
        if ret in (u"up", u"down"):
389
            return
390
        return ret
391
392
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
393
class UserInterface(object):
394
    """This is the entire user interface - the whole screen
395
    with boxes, lists of client widgets, etc.
396
    """
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
397
    def __init__(self, max_log_length=1000):
398
        DBusGMainLoop(set_as_default=True)
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
399
        
400
        self.screen = urwid.curses_display.Screen()
401
        
402
        self.screen.register_palette((
403
                (u"normal",
404
                 u"default", u"default", None),
405
                (u"bold",
406
                 u"default", u"default", u"bold"),
407
                (u"underline-blink",
408
                 u"default", u"default", u"underline"),
409
                (u"standout",
410
                 u"default", u"default", u"standout"),
411
                (u"bold-underline-blink",
412
                 u"default", u"default", (u"bold", u"underline")),
413
                (u"bold-standout",
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",
419
                                          u"standout")),
420
                ))
421
        
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
422
        if urwid.supports_unicode():
237.2.171 by Teddy Hogeborn
* debian/rules: Only set BROKEN_PIE if binutils is a specific range of
423
            self.divider = u"─" # \u2500
424
            #self.divider = u"━" # \u2501
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
425
        else:
426
            #self.divider = u"-" # \u002d
427
            self.divider = u"_" # \u005f
428
        
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
429
        self.screen.start()
430
        
431
        self.size = self.screen.get_cols_rows()
432
        
433
        self.clients = urwid.SimpleListWalker([])
434
        self.clients_dict = {}
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
435
        
436
        # We will add Text widgets to this list
437
        self.log = []
438
        self.max_log_length = max_log_length
439
        
440
        # We keep a reference to the log widget so we can remove it
441
        # from the ListWalker without it getting destroyed
442
        self.logbox = ConstrainedListBox(self.log)
443
        
444
        # This keeps track of whether self.uilist currently has
445
        # self.logbox in it or not
446
        self.log_visible = True
447
        self.log_wrap = u"any"
448
        
449
        self.rebuild()
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
450
        self.log_message_raw((u"bold",
451
                              u"Mandos Monitor version " + version))
452
        self.log_message_raw((u"bold",
453
                              u"q: Quit  ?: Help"))
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
454
        
455
        self.busname = domain + '.Mandos'
456
        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,
461
                                          dbus_interface
462
                                          = server_interface)
463
        try:
464
            mandos_clients = (self.mandos_serv
465
                              .GetAllClientsWithProperties())
466
        except dbus.exceptions.DBusException:
467
            mandos_clients = dbus.Dictionary()
468
        
469
        (self.mandos_serv
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
470
         .connect_to_signal(u"ClientRemoved",
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
471
                            self.find_and_remove_client,
472
                            dbus_interface=server_interface,
473
                            byte_arrays=True))
474
        (self.mandos_serv
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
475
         .connect_to_signal(u"ClientAdded",
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
476
                            self.add_new_client,
477
                            dbus_interface=server_interface,
478
                            byte_arrays=True))
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
479
        (self.mandos_serv
480
         .connect_to_signal(u"ClientNotFound",
481
                            self.client_not_found,
482
                            dbus_interface=server_interface,
483
                            byte_arrays=True))
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
484
        for path, client in mandos_clients.iteritems():
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
485
            client_proxy_object = self.bus.get_object(self.busname,
486
                                                      path)
487
            self.add_client(MandosClientWidget(server_proxy_object
488
                                               =self.mandos_serv,
489
                                               proxy_object
490
                                               =client_proxy_object,
491
                                               properties=client,
492
                                               update_hook
493
                                               =self.refresh,
494
                                               delete_hook
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
495
                                               =self.remove_client,
496
                                               logger
497
                                               =self.log_message),
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
498
                            path=path)
499
    
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
500
    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,
503
                                                    fingerprint)))
504
    
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
505
    def rebuild(self):
506
        """This rebuilds the User Interface.
507
        Call this when the widget layout needs to change"""
508
        self.uilist = []
509
        #self.uilist.append(urwid.ListBox(self.clients))
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
510
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.
511
                                                          clients),
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
512
                                       #header=urwid.Divider(),
513
                                       header=None,
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
514
                                       footer=
515
                                       urwid.Divider(div_char=
516
                                                     self.divider)))
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
517
        if self.log_visible:
518
            self.uilist.append(self.logbox)
519
            pass
520
        self.topwidget = urwid.Pile(self.uilist)
521
    
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
522
    def log_message(self, message):
523
        timestamp = datetime.datetime.now().isoformat()
524
        self.log_message_raw(timestamp + u": " + message)
525
    
526
    def log_message_raw(self, markup):
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
527
        """Add a log message to the log buffer."""
528
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
529
        if (self.max_log_length
530
            and len(self.log) > self.max_log_length):
531
            del self.log[0:len(self.log)-self.max_log_length-1]
237.2.171 by Teddy Hogeborn
* debian/rules: Only set BROKEN_PIE if binutils is a specific range of
532
        self.logbox.set_focus(len(self.logbox.body.contents),
533
                              coming_from=u"above")
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
534
        self.refresh()
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
535
    
536
    def toggle_log_display(self):
537
        """Toggle visibility of the log buffer."""
538
        self.log_visible = not self.log_visible
539
        self.rebuild()
237.2.193 by teddy at bsnet
* mandos-monitor.xml: New.
540
        #self.log_message(u"Log visibility changed to: "
541
        #                 + unicode(self.log_visible))
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
542
    
543
    def change_log_display(self):
544
        """Change type of log display.
545
        Currently, this toggles wrapping of text lines."""
546
        if self.log_wrap == u"clip":
547
            self.log_wrap = u"any"
548
        else:
549
            self.log_wrap = u"clip"
550
        for textwidget in self.log:
551
            textwidget.set_wrap_mode(self.log_wrap)
237.2.193 by teddy at bsnet
* mandos-monitor.xml: New.
552
        #self.log_message(u"Wrap mode: " + self.log_wrap)
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
553
    
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
554
    def find_and_remove_client(self, path, name):
555
        """Find an client from its object path and remove it.
556
        
557
        This is connected to the ClientRemoved signal from the
558
        Mandos server object."""
559
        try:
560
            client = self.clients_dict[path]
561
        except KeyError:
562
            # not found?
563
            return
564
        self.remove_client(client, path)
565
    
237.2.174 by Teddy Hogeborn
More consistent terminology: Clients are no longer "invalid" - they
566
    def add_new_client(self, path):
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
567
        client_proxy_object = self.bus.get_object(self.busname, path)
568
        self.add_client(MandosClientWidget(server_proxy_object
569
                                           =self.mandos_serv,
570
                                           proxy_object
571
                                           =client_proxy_object,
572
                                           update_hook
573
                                           =self.refresh,
574
                                           delete_hook
237.2.174 by Teddy Hogeborn
More consistent terminology: Clients are no longer "invalid" - they
575
                                           =self.remove_client,
576
                                           logger
577
                                           =self.log_message),
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
578
                        path=path)
579
    
580
    def add_client(self, client, path=None):
581
        self.clients.append(client)
582
        if path is None:
583
            path = client.proxy.object_path
584
        self.clients_dict[path] = client
237.2.185 by Teddy Hogeborn
Rename all D-Bus properties to conform to D-Bus naming conventions;
585
        self.clients.sort(None, lambda c: c.properties[u"Name"])
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
586
        self.refresh()
587
    
588
    def remove_client(self, client, path=None):
589
        self.clients.remove(client)
590
        if path is None:
591
            path = client.proxy.object_path
592
        del self.clients_dict[path]
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
593
        if not self.clients_dict:
594
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
595
            # is completely emptied, we need to recreate it.
596
            self.clients = urwid.SimpleListWalker([])
597
            self.rebuild()
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
598
        self.refresh()
599
    
600
    def refresh(self):
601
        """Redraw the screen"""
602
        canvas = self.topwidget.render(self.size, focus=True)
603
        self.screen.draw_screen(self.size, canvas)
604
    
605
    def run(self):
606
        """Start the main loop and exit when it's done."""
607
        self.refresh()
608
        self._input_callback_tag = (gobject.io_add_watch
609
                                    (sys.stdin.fileno(),
610
                                     gobject.IO_IN,
611
                                     self.process_input))
612
        self.main_loop.run()
613
        # Main loop has finished, we should close everything now
614
        gobject.source_remove(self._input_callback_tag)
615
        self.screen.stop()
616
    
617
    def stop(self):
618
        self.main_loop.quit()
619
    
620
    def process_input(self, source, condition):
621
        keys = self.screen.get_input()
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
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
629
                         u"j": u"down",           # vi
630
                         u"k": u"up",             # vi
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
631
                         }
632
        for key in keys:
633
            try:
634
                key = translations[key]
635
            except KeyError:    # :-)
636
                pass
637
            
638
            if key == u"q" or key == u"Q":
639
                self.stop()
640
                break
641
            elif key == u"window resize":
642
                self.size = self.screen.get_cols_rows()
643
                self.refresh()
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
644
            elif key == u"\f":  # Ctrl-L
645
                self.refresh()
646
            elif key == u"l" or key == u"D":
647
                self.toggle_log_display()
648
                self.refresh()
649
            elif key == u"w" or key == u"i":
650
                self.change_log_display()
651
                self.refresh()
237.2.171 by Teddy Hogeborn
* debian/rules: Only set BROKEN_PIE if binutils is a specific range of
652
            elif key == u"?" or key == u"f1" or key == u"esc":
237.2.170 by Teddy Hogeborn
* mandos-monitor (MandosClientWidget): Change "StopChecker" key to "S"
653
                if not self.log_visible:
654
                    self.log_visible = True
655
                    self.rebuild()
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
656
                self.log_message_raw((u"bold",
657
                                      u"  ".
658
                                      join((u"q: Quit",
659
                                            u"?: Help",
660
                                            u"l: Log window toggle",
661
                                            u"TAB: Switch window",
662
                                            u"w: Wrap (log)"))))
663
                self.log_message_raw((u"bold",
664
                                      u"  "
665
                                      .join((u"Clients:",
24.1.155 by Björn Påhlsson
mandos server: Added debuglevel that adjust at what level information
666
                                             u"+: Enable",
667
                                             u"-: Disable",
237.2.172 by Teddy Hogeborn
* mandos (MandosServer.handle_ipc): Better log message.
668
                                             u"r: Remove",
669
                                             u"s: Start new checker",
670
                                             u"S: Stop checker",
24.1.154 by Björn Påhlsson
merge
671
                                             u"C: Checker OK",
24.1.155 by Björn Påhlsson
mandos server: Added debuglevel that adjust at what level information
672
                                             u"a: Approve",
673
                                             u"d: Deny"))))
237.2.168 by Teddy Hogeborn
* mandos-monitor (MandosClientPropertyCache): Remove conversion of
674
                self.refresh()
675
            elif key == u"tab":
676
                if self.topwidget.get_focus() is self.logbox:
677
                    self.topwidget.set_focus(0)
678
                else:
679
                    self.topwidget.set_focus(self.logbox)
680
                self.refresh()
237.2.170 by Teddy Hogeborn
* mandos-monitor (MandosClientWidget): Change "StopChecker" key to "S"
681
            #elif (key == u"end" or key == u"meta >" or key == u"G"
682
            #      or key == u">"):
683
            #    pass            # xxx end-of-buffer
684
            #elif (key == u"home" or key == u"meta <" or key == u"g"
685
            #      or key == u"<"):
686
            #    pass            # xxx beginning-of-buffer
687
            #elif key == u"ctrl e" or key == u"$":
688
            #    pass            # xxx move-end-of-line
689
            #elif key == u"ctrl a" or key == u"^":
690
            #    pass            # xxx move-beginning-of-line
691
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
692
            #    pass            # xxx left
693
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
694
            #    pass            # xxx right
695
            #elif key == u"a":
696
            #    pass            # scroll up log
697
            #elif key == u"z":
698
            #    pass            # scroll down log
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
699
            elif self.topwidget.selectable():
700
                self.topwidget.keypress(self.size, key)
701
                self.refresh()
702
        return True
703
704
ui = UserInterface()
705
try:
706
    ui.run()
24.1.159 by Björn Påhlsson
added approval to mandos-ctl
707
except KeyboardInterrupt:
708
    ui.screen.stop()
237.2.174 by Teddy Hogeborn
More consistent terminology: Clients are no longer "invalid" - they
709
except Exception, e:
710
    ui.log_message(unicode(e))
237.2.167 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
711
    ui.screen.stop()
712
    raise