/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2016-06-03 17:23:10 UTC
  • Revision ID: teddy@recompile.se-20160603172310-ohavcfobfjb5v2hr
mandos: Bug fix: Only use the --no-use-agent option for GPG 1

* mandos (PGPEngine.__init__): Only use option "--no-use-agent" with
                               GPG 1, as GPG 2 does not support it.

Show diffs side-by-side

added added

removed removed

Lines of Context:
6
6
# This program is partly derived from an example program for an Avahi
7
7
# service publisher, downloaded from
8
8
# <http://avahi.org/wiki/PythonPublishExample>.  This includes the
9
 
# methods "add" and "remove" in the "AvahiService" class, the
10
 
# "server_state_changed" and "entry_group_state_changed" functions,
11
 
# and some lines in "main".
 
9
# methods "add", "remove", "server_state_changed",
 
10
# "entry_group_state_changed", "cleanup", and "activate" in the
 
11
# "AvahiService" class, and some lines in "main".
12
12
13
13
# Everything else is
14
 
# Copyright © 2008 Teddy Hogeborn
15
 
# Copyright © 2008 Björn Påhlsson
 
14
# Copyright © 2008-2016 Teddy Hogeborn
 
15
# Copyright © 2008-2016 Björn Påhlsson
16
16
17
17
# This program is free software: you can redistribute it and/or modify
18
18
# it under the terms of the GNU General Public License as published by
28
28
# along with this program.  If not, see
29
29
# <http://www.gnu.org/licenses/>.
30
30
31
 
# Contact the authors at <mandos@fukt.bsnet.se>.
 
31
# Contact the authors at <mandos@recompile.se>.
32
32
33
33
 
34
 
from __future__ import division, with_statement, absolute_import
35
 
 
36
 
import SocketServer
 
34
from __future__ import (division, absolute_import, print_function,
 
35
                        unicode_literals)
 
36
 
 
37
try:
 
38
    from future_builtins import *
 
39
except ImportError:
 
40
    pass
 
41
 
 
42
try:
 
43
    import SocketServer as socketserver
 
44
except ImportError:
 
45
    import socketserver
37
46
import socket
38
 
from optparse import OptionParser
 
47
import argparse
39
48
import datetime
40
49
import errno
41
 
import gnutls.crypto
42
 
import gnutls.connection
43
 
import gnutls.errors
44
 
import gnutls.library.functions
45
 
import gnutls.library.constants
46
 
import gnutls.library.types
47
 
import ConfigParser
 
50
try:
 
51
    import ConfigParser as configparser
 
52
except ImportError:
 
53
    import configparser
48
54
import sys
49
55
import re
50
56
import os
51
57
import signal
52
 
from sets import Set
53
58
import subprocess
54
59
import atexit
55
60
import stat
56
61
import logging
57
62
import logging.handlers
58
63
import pwd
59
 
from contextlib import closing
 
64
import contextlib
 
65
import struct
 
66
import fcntl
 
67
import functools
 
68
try:
 
69
    import cPickle as pickle
 
70
except ImportError:
 
71
    import pickle
 
72
import multiprocessing
 
73
import types
 
74
import binascii
 
75
import tempfile
 
76
import itertools
 
77
import collections
 
78
import codecs
60
79
 
61
80
import dbus
62
81
import dbus.service
63
 
import gobject
64
 
import avahi
 
82
from gi.repository import GLib
65
83
from dbus.mainloop.glib import DBusGMainLoop
66
84
import ctypes
67
85
import ctypes.util
68
 
 
69
 
version = "1.0.2"
70
 
 
71
 
logger = logging.Logger('mandos')
72
 
syslogger = (logging.handlers.SysLogHandler
73
 
             (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
74
 
              address = "/dev/log"))
75
 
syslogger.setFormatter(logging.Formatter
76
 
                       ('Mandos: %(levelname)s: %(message)s'))
77
 
logger.addHandler(syslogger)
78
 
 
79
 
console = logging.StreamHandler()
80
 
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
81
 
                                       ' %(message)s'))
82
 
logger.addHandler(console)
 
86
import xml.dom.minidom
 
87
import inspect
 
88
 
 
89
try:
 
90
    SO_BINDTODEVICE = socket.SO_BINDTODEVICE
 
91
except AttributeError:
 
92
    try:
 
93
        from IN import SO_BINDTODEVICE
 
94
    except ImportError:
 
95
        SO_BINDTODEVICE = None
 
96
 
 
97
if sys.version_info.major == 2:
 
98
    str = unicode
 
99
 
 
100
version = "1.7.7"
 
101
stored_state_file = "clients.pickle"
 
102
 
 
103
logger = logging.getLogger()
 
104
syslogger = None
 
105
 
 
106
try:
 
107
    if_nametoindex = ctypes.cdll.LoadLibrary(
 
108
        ctypes.util.find_library("c")).if_nametoindex
 
109
except (OSError, AttributeError):
 
110
    
 
111
    def if_nametoindex(interface):
 
112
        "Get an interface index the hard way, i.e. using fcntl()"
 
113
        SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
 
114
        with contextlib.closing(socket.socket()) as s:
 
115
            ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
 
116
                                struct.pack(b"16s16x", interface))
 
117
        interface_index = struct.unpack("I", ifreq[16:20])[0]
 
118
        return interface_index
 
119
 
 
120
 
 
121
def copy_function(func):
 
122
    """Make a copy of a function"""
 
123
    if sys.version_info.major == 2:
 
124
        return types.FunctionType(func.func_code,
 
125
                                  func.func_globals,
 
126
                                  func.func_name,
 
127
                                  func.func_defaults,
 
128
                                  func.func_closure)
 
129
    else:
 
130
        return types.FunctionType(func.__code__,
 
131
                                  func.__globals__,
 
132
                                  func.__name__,
 
133
                                  func.__defaults__,
 
134
                                  func.__closure__)
 
135
 
 
136
 
 
137
def initlogger(debug, level=logging.WARNING):
 
138
    """init logger and add loglevel"""
 
139
    
 
140
    global syslogger
 
141
    syslogger = (logging.handlers.SysLogHandler(
 
142
        facility = logging.handlers.SysLogHandler.LOG_DAEMON,
 
143
        address = "/dev/log"))
 
144
    syslogger.setFormatter(logging.Formatter
 
145
                           ('Mandos [%(process)d]: %(levelname)s:'
 
146
                            ' %(message)s'))
 
147
    logger.addHandler(syslogger)
 
148
    
 
149
    if debug:
 
150
        console = logging.StreamHandler()
 
151
        console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
 
152
                                               ' [%(process)d]:'
 
153
                                               ' %(levelname)s:'
 
154
                                               ' %(message)s'))
 
155
        logger.addHandler(console)
 
156
    logger.setLevel(level)
 
157
 
 
158
 
 
159
class PGPError(Exception):
 
160
    """Exception if encryption/decryption fails"""
 
161
    pass
 
162
 
 
163
 
 
164
class PGPEngine(object):
 
165
    """A simple class for OpenPGP symmetric encryption & decryption"""
 
166
    
 
167
    def __init__(self):
 
168
        self.tempdir = tempfile.mkdtemp(prefix="mandos-")
 
169
        self.gpg = "gpg"
 
170
        try:
 
171
            output = subprocess.check_output(["gpgconf"])
 
172
            for line in output.splitlines():
 
173
                name, text, path = line.split(b":")
 
174
                if name == "gpg":
 
175
                    self.gpg = path
 
176
                    break
 
177
        except OSError as e:
 
178
            if e.errno != errno.ENOENT:
 
179
                raise
 
180
        self.gnupgargs = ['--batch',
 
181
                          '--homedir', self.tempdir,
 
182
                          '--force-mdc',
 
183
                          '--quiet']
 
184
        # Only GPG version 1 has the --no-use-agent option.
 
185
        if self.gpg == "gpg" or self.gpg.endswith("/gpg"):
 
186
            self.gnupgargs.append("--no-use-agent")
 
187
    
 
188
    def __enter__(self):
 
189
        return self
 
190
    
 
191
    def __exit__(self, exc_type, exc_value, traceback):
 
192
        self._cleanup()
 
193
        return False
 
194
    
 
195
    def __del__(self):
 
196
        self._cleanup()
 
197
    
 
198
    def _cleanup(self):
 
199
        if self.tempdir is not None:
 
200
            # Delete contents of tempdir
 
201
            for root, dirs, files in os.walk(self.tempdir,
 
202
                                             topdown = False):
 
203
                for filename in files:
 
204
                    os.remove(os.path.join(root, filename))
 
205
                for dirname in dirs:
 
206
                    os.rmdir(os.path.join(root, dirname))
 
207
            # Remove tempdir
 
208
            os.rmdir(self.tempdir)
 
209
            self.tempdir = None
 
210
    
 
211
    def password_encode(self, password):
 
212
        # Passphrase can not be empty and can not contain newlines or
 
213
        # NUL bytes.  So we prefix it and hex encode it.
 
214
        encoded = b"mandos" + binascii.hexlify(password)
 
215
        if len(encoded) > 2048:
 
216
            # GnuPG can't handle long passwords, so encode differently
 
217
            encoded = (b"mandos" + password.replace(b"\\", b"\\\\")
 
218
                       .replace(b"\n", b"\\n")
 
219
                       .replace(b"\0", b"\\x00"))
 
220
        return encoded
 
221
    
 
222
    def encrypt(self, data, password):
 
223
        passphrase = self.password_encode(password)
 
224
        with tempfile.NamedTemporaryFile(
 
225
                dir=self.tempdir) as passfile:
 
226
            passfile.write(passphrase)
 
227
            passfile.flush()
 
228
            proc = subprocess.Popen([self.gpg, '--symmetric',
 
229
                                     '--passphrase-file',
 
230
                                     passfile.name]
 
231
                                    + self.gnupgargs,
 
232
                                    stdin = subprocess.PIPE,
 
233
                                    stdout = subprocess.PIPE,
 
234
                                    stderr = subprocess.PIPE)
 
235
            ciphertext, err = proc.communicate(input = data)
 
236
        if proc.returncode != 0:
 
237
            raise PGPError(err)
 
238
        return ciphertext
 
239
    
 
240
    def decrypt(self, data, password):
 
241
        passphrase = self.password_encode(password)
 
242
        with tempfile.NamedTemporaryFile(
 
243
                dir = self.tempdir) as passfile:
 
244
            passfile.write(passphrase)
 
245
            passfile.flush()
 
246
            proc = subprocess.Popen([self.gpg, '--decrypt',
 
247
                                     '--passphrase-file',
 
248
                                     passfile.name]
 
249
                                    + self.gnupgargs,
 
250
                                    stdin = subprocess.PIPE,
 
251
                                    stdout = subprocess.PIPE,
 
252
                                    stderr = subprocess.PIPE)
 
253
            decrypted_plaintext, err = proc.communicate(input = data)
 
254
        if proc.returncode != 0:
 
255
            raise PGPError(err)
 
256
        return decrypted_plaintext
 
257
 
 
258
# Pretend that we have an Avahi module
 
259
class Avahi(object):
 
260
    """This isn't so much a class as it is a module-like namespace.
 
261
    It is instantiated once, and simulates having an Avahi module."""
 
262
    IF_UNSPEC = -1              # avahi-common/address.h
 
263
    PROTO_UNSPEC = -1           # avahi-common/address.h
 
264
    PROTO_INET = 0              # avahi-common/address.h
 
265
    PROTO_INET6 = 1             # avahi-common/address.h
 
266
    DBUS_NAME = "org.freedesktop.Avahi"
 
267
    DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup"
 
268
    DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
 
269
    DBUS_PATH_SERVER = "/"
 
270
    def string_array_to_txt_array(self, t):
 
271
        return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
 
272
                           for s in t), signature="ay")
 
273
    ENTRY_GROUP_ESTABLISHED = 2 # avahi-common/defs.h
 
274
    ENTRY_GROUP_COLLISION = 3   # avahi-common/defs.h
 
275
    ENTRY_GROUP_FAILURE = 4     # avahi-common/defs.h
 
276
    SERVER_INVALID = 0          # avahi-common/defs.h
 
277
    SERVER_REGISTERING = 1      # avahi-common/defs.h
 
278
    SERVER_RUNNING = 2          # avahi-common/defs.h
 
279
    SERVER_COLLISION = 3        # avahi-common/defs.h
 
280
    SERVER_FAILURE = 4          # avahi-common/defs.h
 
281
avahi = Avahi()
83
282
 
84
283
class AvahiError(Exception):
85
 
    def __init__(self, value):
 
284
    def __init__(self, value, *args, **kwargs):
86
285
        self.value = value
87
 
        super(AvahiError, self).__init__()
88
 
    def __str__(self):
89
 
        return repr(self.value)
 
286
        return super(AvahiError, self).__init__(value, *args,
 
287
                                                **kwargs)
 
288
 
90
289
 
91
290
class AvahiServiceError(AvahiError):
92
291
    pass
93
292
 
 
293
 
94
294
class AvahiGroupError(AvahiError):
95
295
    pass
96
296
 
97
297
 
98
298
class AvahiService(object):
99
299
    """An Avahi (Zeroconf) service.
 
300
    
100
301
    Attributes:
101
302
    interface: integer; avahi.IF_UNSPEC or an interface index.
102
303
               Used to optionally bind to the specified interface.
103
304
    name: string; Example: 'Mandos'
104
305
    type: string; Example: '_mandos._tcp'.
105
 
                  See <http://www.dns-sd.org/ServiceTypes.html>
 
306
     See <https://www.iana.org/assignments/service-names-port-numbers>
106
307
    port: integer; what port to announce
107
308
    TXT: list of strings; TXT record for the service
108
309
    domain: string; Domain to publish on, default to .local if empty.
110
311
    max_renames: integer; maximum number of renames
111
312
    rename_count: integer; counter so we only rename after collisions
112
313
                  a sensible number of times
 
314
    group: D-Bus Entry Group
 
315
    server: D-Bus Server
 
316
    bus: dbus.SystemBus()
113
317
    """
114
 
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
115
 
                 servicetype = None, port = None, TXT = None,
116
 
                 domain = "", host = "", max_renames = 32768):
 
318
    
 
319
    def __init__(self,
 
320
                 interface = avahi.IF_UNSPEC,
 
321
                 name = None,
 
322
                 servicetype = None,
 
323
                 port = None,
 
324
                 TXT = None,
 
325
                 domain = "",
 
326
                 host = "",
 
327
                 max_renames = 32768,
 
328
                 protocol = avahi.PROTO_UNSPEC,
 
329
                 bus = None):
117
330
        self.interface = interface
118
331
        self.name = name
119
332
        self.type = servicetype
123
336
        self.host = host
124
337
        self.rename_count = 0
125
338
        self.max_renames = max_renames
126
 
    def rename(self):
 
339
        self.protocol = protocol
 
340
        self.group = None       # our entry group
 
341
        self.server = None
 
342
        self.bus = bus
 
343
        self.entry_group_state_changed_match = None
 
344
    
 
345
    def rename(self, remove=True):
127
346
        """Derived from the Avahi example code"""
128
347
        if self.rename_count >= self.max_renames:
129
 
            logger.critical(u"No suitable Zeroconf service name found"
130
 
                            u" after %i retries, exiting.",
 
348
            logger.critical("No suitable Zeroconf service name found"
 
349
                            " after %i retries, exiting.",
131
350
                            self.rename_count)
132
351
            raise AvahiServiceError("Too many renames")
133
 
        self.name = server.GetAlternativeServiceName(self.name)
134
 
        logger.info(u"Changing Zeroconf service name to %r ...",
135
 
                    str(self.name))
136
 
        syslogger.setFormatter(logging.Formatter
137
 
                               ('Mandos (%s): %%(levelname)s:'
138
 
                                ' %%(message)s' % self.name))
139
 
        self.remove()
140
 
        self.add()
 
352
        self.name = str(
 
353
            self.server.GetAlternativeServiceName(self.name))
141
354
        self.rename_count += 1
 
355
        logger.info("Changing Zeroconf service name to %r ...",
 
356
                    self.name)
 
357
        if remove:
 
358
            self.remove()
 
359
        try:
 
360
            self.add()
 
361
        except dbus.exceptions.DBusException as error:
 
362
            if (error.get_dbus_name()
 
363
                == "org.freedesktop.Avahi.CollisionError"):
 
364
                logger.info("Local Zeroconf service name collision.")
 
365
                return self.rename(remove=False)
 
366
            else:
 
367
                logger.critical("D-Bus Exception", exc_info=error)
 
368
                self.cleanup()
 
369
                os._exit(1)
 
370
    
142
371
    def remove(self):
143
372
        """Derived from the Avahi example code"""
144
 
        if group is not None:
145
 
            group.Reset()
 
373
        if self.entry_group_state_changed_match is not None:
 
374
            self.entry_group_state_changed_match.remove()
 
375
            self.entry_group_state_changed_match = None
 
376
        if self.group is not None:
 
377
            self.group.Reset()
 
378
    
146
379
    def add(self):
147
380
        """Derived from the Avahi example code"""
148
 
        global group
149
 
        if group is None:
150
 
            group = dbus.Interface(bus.get_object
151
 
                                   (avahi.DBUS_NAME,
152
 
                                    server.EntryGroupNew()),
153
 
                                   avahi.DBUS_INTERFACE_ENTRY_GROUP)
154
 
            group.connect_to_signal('StateChanged',
155
 
                                    entry_group_state_changed)
156
 
        logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
157
 
                     service.name, service.type)
158
 
        group.AddService(
159
 
                self.interface,         # interface
160
 
                avahi.PROTO_INET6,      # protocol
161
 
                dbus.UInt32(0),         # flags
162
 
                self.name, self.type,
163
 
                self.domain, self.host,
164
 
                dbus.UInt16(self.port),
165
 
                avahi.string_array_to_txt_array(self.TXT))
166
 
        group.Commit()
167
 
 
168
 
# From the Avahi example code:
169
 
group = None                            # our entry group
170
 
# End of Avahi example code
171
 
 
172
 
 
173
 
def _datetime_to_dbus(dt, variant_level=0):
174
 
    """Convert a UTC datetime.datetime() to a D-Bus type."""
175
 
    return dbus.String(dt.isoformat(), variant_level=variant_level)
176
 
 
177
 
 
178
 
class Client(dbus.service.Object):
 
381
        self.remove()
 
382
        if self.group is None:
 
383
            self.group = dbus.Interface(
 
384
                self.bus.get_object(avahi.DBUS_NAME,
 
385
                                    self.server.EntryGroupNew()),
 
386
                avahi.DBUS_INTERFACE_ENTRY_GROUP)
 
387
        self.entry_group_state_changed_match = (
 
388
            self.group.connect_to_signal(
 
389
                'StateChanged', self.entry_group_state_changed))
 
390
        logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
 
391
                     self.name, self.type)
 
392
        self.group.AddService(
 
393
            self.interface,
 
394
            self.protocol,
 
395
            dbus.UInt32(0),     # flags
 
396
            self.name, self.type,
 
397
            self.domain, self.host,
 
398
            dbus.UInt16(self.port),
 
399
            avahi.string_array_to_txt_array(self.TXT))
 
400
        self.group.Commit()
 
401
    
 
402
    def entry_group_state_changed(self, state, error):
 
403
        """Derived from the Avahi example code"""
 
404
        logger.debug("Avahi entry group state change: %i", state)
 
405
        
 
406
        if state == avahi.ENTRY_GROUP_ESTABLISHED:
 
407
            logger.debug("Zeroconf service established.")
 
408
        elif state == avahi.ENTRY_GROUP_COLLISION:
 
409
            logger.info("Zeroconf service name collision.")
 
410
            self.rename()
 
411
        elif state == avahi.ENTRY_GROUP_FAILURE:
 
412
            logger.critical("Avahi: Error in group state changed %s",
 
413
                            str(error))
 
414
            raise AvahiGroupError("State changed: {!s}".format(error))
 
415
    
 
416
    def cleanup(self):
 
417
        """Derived from the Avahi example code"""
 
418
        if self.group is not None:
 
419
            try:
 
420
                self.group.Free()
 
421
            except (dbus.exceptions.UnknownMethodException,
 
422
                    dbus.exceptions.DBusException):
 
423
                pass
 
424
            self.group = None
 
425
        self.remove()
 
426
    
 
427
    def server_state_changed(self, state, error=None):
 
428
        """Derived from the Avahi example code"""
 
429
        logger.debug("Avahi server state change: %i", state)
 
430
        bad_states = {
 
431
            avahi.SERVER_INVALID: "Zeroconf server invalid",
 
432
            avahi.SERVER_REGISTERING: None,
 
433
            avahi.SERVER_COLLISION: "Zeroconf server name collision",
 
434
            avahi.SERVER_FAILURE: "Zeroconf server failure",
 
435
        }
 
436
        if state in bad_states:
 
437
            if bad_states[state] is not None:
 
438
                if error is None:
 
439
                    logger.error(bad_states[state])
 
440
                else:
 
441
                    logger.error(bad_states[state] + ": %r", error)
 
442
            self.cleanup()
 
443
        elif state == avahi.SERVER_RUNNING:
 
444
            try:
 
445
                self.add()
 
446
            except dbus.exceptions.DBusException as error:
 
447
                if (error.get_dbus_name()
 
448
                    == "org.freedesktop.Avahi.CollisionError"):
 
449
                    logger.info("Local Zeroconf service name"
 
450
                                " collision.")
 
451
                    return self.rename(remove=False)
 
452
                else:
 
453
                    logger.critical("D-Bus Exception", exc_info=error)
 
454
                    self.cleanup()
 
455
                    os._exit(1)
 
456
        else:
 
457
            if error is None:
 
458
                logger.debug("Unknown state: %r", state)
 
459
            else:
 
460
                logger.debug("Unknown state: %r: %r", state, error)
 
461
    
 
462
    def activate(self):
 
463
        """Derived from the Avahi example code"""
 
464
        if self.server is None:
 
465
            self.server = dbus.Interface(
 
466
                self.bus.get_object(avahi.DBUS_NAME,
 
467
                                    avahi.DBUS_PATH_SERVER,
 
468
                                    follow_name_owner_changes=True),
 
469
                avahi.DBUS_INTERFACE_SERVER)
 
470
        self.server.connect_to_signal("StateChanged",
 
471
                                      self.server_state_changed)
 
472
        self.server_state_changed(self.server.GetState())
 
473
 
 
474
 
 
475
class AvahiServiceToSyslog(AvahiService):
 
476
    def rename(self, *args, **kwargs):
 
477
        """Add the new name to the syslog messages"""
 
478
        ret = AvahiService.rename(self, *args, **kwargs)
 
479
        syslogger.setFormatter(logging.Formatter(
 
480
            'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
 
481
            .format(self.name)))
 
482
        return ret
 
483
 
 
484
# Pretend that we have a GnuTLS module
 
485
class GnuTLS(object):
 
486
    """This isn't so much a class as it is a module-like namespace.
 
487
    It is instantiated once, and simulates having a GnuTLS module."""
 
488
    
 
489
    _library = ctypes.cdll.LoadLibrary(
 
490
        ctypes.util.find_library("gnutls"))
 
491
    _need_version = b"3.3.0"
 
492
    def __init__(self):
 
493
        # Need to use class name "GnuTLS" here, since this method is
 
494
        # called before the assignment to the "gnutls" global variable
 
495
        # happens.
 
496
        if GnuTLS.check_version(self._need_version) is None:
 
497
            raise GnuTLS.Error("Needs GnuTLS {} or later"
 
498
                               .format(self._need_version))
 
499
    
 
500
    # Unless otherwise indicated, the constants and types below are
 
501
    # all from the gnutls/gnutls.h C header file.
 
502
    
 
503
    # Constants
 
504
    E_SUCCESS = 0
 
505
    E_INTERRUPTED = -52
 
506
    E_AGAIN = -28
 
507
    CRT_OPENPGP = 2
 
508
    CLIENT = 2
 
509
    SHUT_RDWR = 0
 
510
    CRD_CERTIFICATE = 1
 
511
    E_NO_CERTIFICATE_FOUND = -49
 
512
    OPENPGP_FMT_RAW = 0         # gnutls/openpgp.h
 
513
    
 
514
    # Types
 
515
    class session_int(ctypes.Structure):
 
516
        _fields_ = []
 
517
    session_t = ctypes.POINTER(session_int)
 
518
    class certificate_credentials_st(ctypes.Structure):
 
519
        _fields_ = []
 
520
    certificate_credentials_t = ctypes.POINTER(
 
521
        certificate_credentials_st)
 
522
    certificate_type_t = ctypes.c_int
 
523
    class datum_t(ctypes.Structure):
 
524
        _fields_ = [('data', ctypes.POINTER(ctypes.c_ubyte)),
 
525
                    ('size', ctypes.c_uint)]
 
526
    class openpgp_crt_int(ctypes.Structure):
 
527
        _fields_ = []
 
528
    openpgp_crt_t = ctypes.POINTER(openpgp_crt_int)
 
529
    openpgp_crt_fmt_t = ctypes.c_int # gnutls/openpgp.h
 
530
    log_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
 
531
    credentials_type_t = ctypes.c_int
 
532
    transport_ptr_t = ctypes.c_void_p
 
533
    close_request_t = ctypes.c_int
 
534
    
 
535
    # Exceptions
 
536
    class Error(Exception):
 
537
        # We need to use the class name "GnuTLS" here, since this
 
538
        # exception might be raised from within GnuTLS.__init__,
 
539
        # which is called before the assignment to the "gnutls"
 
540
        # global variable has happened.
 
541
        def __init__(self, message = None, code = None, args=()):
 
542
            # Default usage is by a message string, but if a return
 
543
            # code is passed, convert it to a string with
 
544
            # gnutls.strerror()
 
545
            self.code = code
 
546
            if message is None and code is not None:
 
547
                message = GnuTLS.strerror(code)
 
548
            return super(GnuTLS.Error, self).__init__(
 
549
                message, *args)
 
550
    
 
551
    class CertificateSecurityError(Error):
 
552
        pass
 
553
    
 
554
    # Classes
 
555
    class Credentials(object):
 
556
        def __init__(self):
 
557
            self._c_object = gnutls.certificate_credentials_t()
 
558
            gnutls.certificate_allocate_credentials(
 
559
                ctypes.byref(self._c_object))
 
560
            self.type = gnutls.CRD_CERTIFICATE
 
561
        
 
562
        def __del__(self):
 
563
            gnutls.certificate_free_credentials(self._c_object)
 
564
    
 
565
    class ClientSession(object):
 
566
        def __init__(self, socket, credentials = None):
 
567
            self._c_object = gnutls.session_t()
 
568
            gnutls.init(ctypes.byref(self._c_object), gnutls.CLIENT)
 
569
            gnutls.set_default_priority(self._c_object)
 
570
            gnutls.transport_set_ptr(self._c_object, socket.fileno())
 
571
            gnutls.handshake_set_private_extensions(self._c_object,
 
572
                                                    True)
 
573
            self.socket = socket
 
574
            if credentials is None:
 
575
                credentials = gnutls.Credentials()
 
576
            gnutls.credentials_set(self._c_object, credentials.type,
 
577
                                   ctypes.cast(credentials._c_object,
 
578
                                               ctypes.c_void_p))
 
579
            self.credentials = credentials
 
580
        
 
581
        def __del__(self):
 
582
            gnutls.deinit(self._c_object)
 
583
        
 
584
        def handshake(self):
 
585
            return gnutls.handshake(self._c_object)
 
586
        
 
587
        def send(self, data):
 
588
            data = bytes(data)
 
589
            data_len = len(data)
 
590
            while data_len > 0:
 
591
                data_len -= gnutls.record_send(self._c_object,
 
592
                                               data[-data_len:],
 
593
                                               data_len)
 
594
        
 
595
        def bye(self):
 
596
            return gnutls.bye(self._c_object, gnutls.SHUT_RDWR)
 
597
    
 
598
    # Error handling functions
 
599
    def _error_code(result):
 
600
        """A function to raise exceptions on errors, suitable
 
601
        for the 'restype' attribute on ctypes functions"""
 
602
        if result >= 0:
 
603
            return result
 
604
        if result == gnutls.E_NO_CERTIFICATE_FOUND:
 
605
            raise gnutls.CertificateSecurityError(code = result)
 
606
        raise gnutls.Error(code = result)
 
607
    
 
608
    def _retry_on_error(result, func, arguments):
 
609
        """A function to retry on some errors, suitable
 
610
        for the 'errcheck' attribute on ctypes functions"""
 
611
        while result < 0:
 
612
            if result not in (gnutls.E_INTERRUPTED, gnutls.E_AGAIN):
 
613
                return _error_code(result)
 
614
            result = func(*arguments)
 
615
        return result
 
616
    
 
617
    # Unless otherwise indicated, the function declarations below are
 
618
    # all from the gnutls/gnutls.h C header file.
 
619
    
 
620
    # Functions
 
621
    priority_set_direct = _library.gnutls_priority_set_direct
 
622
    priority_set_direct.argtypes = [session_t, ctypes.c_char_p,
 
623
                                    ctypes.POINTER(ctypes.c_char_p)]
 
624
    priority_set_direct.restype = _error_code
 
625
    
 
626
    init = _library.gnutls_init
 
627
    init.argtypes = [ctypes.POINTER(session_t), ctypes.c_int]
 
628
    init.restype = _error_code
 
629
    
 
630
    set_default_priority = _library.gnutls_set_default_priority
 
631
    set_default_priority.argtypes = [session_t]
 
632
    set_default_priority.restype = _error_code
 
633
    
 
634
    record_send = _library.gnutls_record_send
 
635
    record_send.argtypes = [session_t, ctypes.c_void_p,
 
636
                            ctypes.c_size_t]
 
637
    record_send.restype = ctypes.c_ssize_t
 
638
    record_send.errcheck = _retry_on_error
 
639
    
 
640
    certificate_allocate_credentials = (
 
641
        _library.gnutls_certificate_allocate_credentials)
 
642
    certificate_allocate_credentials.argtypes = [
 
643
        ctypes.POINTER(certificate_credentials_t)]
 
644
    certificate_allocate_credentials.restype = _error_code
 
645
    
 
646
    certificate_free_credentials = (
 
647
        _library.gnutls_certificate_free_credentials)
 
648
    certificate_free_credentials.argtypes = [certificate_credentials_t]
 
649
    certificate_free_credentials.restype = None
 
650
    
 
651
    handshake_set_private_extensions = (
 
652
        _library.gnutls_handshake_set_private_extensions)
 
653
    handshake_set_private_extensions.argtypes = [session_t,
 
654
                                                 ctypes.c_int]
 
655
    handshake_set_private_extensions.restype = None
 
656
    
 
657
    credentials_set = _library.gnutls_credentials_set
 
658
    credentials_set.argtypes = [session_t, credentials_type_t,
 
659
                                ctypes.c_void_p]
 
660
    credentials_set.restype = _error_code
 
661
    
 
662
    strerror = _library.gnutls_strerror
 
663
    strerror.argtypes = [ctypes.c_int]
 
664
    strerror.restype = ctypes.c_char_p
 
665
    
 
666
    certificate_type_get = _library.gnutls_certificate_type_get
 
667
    certificate_type_get.argtypes = [session_t]
 
668
    certificate_type_get.restype = _error_code
 
669
    
 
670
    certificate_get_peers = _library.gnutls_certificate_get_peers
 
671
    certificate_get_peers.argtypes = [session_t,
 
672
                                      ctypes.POINTER(ctypes.c_uint)]
 
673
    certificate_get_peers.restype = ctypes.POINTER(datum_t)
 
674
    
 
675
    global_set_log_level = _library.gnutls_global_set_log_level
 
676
    global_set_log_level.argtypes = [ctypes.c_int]
 
677
    global_set_log_level.restype = None
 
678
    
 
679
    global_set_log_function = _library.gnutls_global_set_log_function
 
680
    global_set_log_function.argtypes = [log_func]
 
681
    global_set_log_function.restype = None
 
682
    
 
683
    deinit = _library.gnutls_deinit
 
684
    deinit.argtypes = [session_t]
 
685
    deinit.restype = None
 
686
    
 
687
    handshake = _library.gnutls_handshake
 
688
    handshake.argtypes = [session_t]
 
689
    handshake.restype = _error_code
 
690
    handshake.errcheck = _retry_on_error
 
691
    
 
692
    transport_set_ptr = _library.gnutls_transport_set_ptr
 
693
    transport_set_ptr.argtypes = [session_t, transport_ptr_t]
 
694
    transport_set_ptr.restype = None
 
695
    
 
696
    bye = _library.gnutls_bye
 
697
    bye.argtypes = [session_t, close_request_t]
 
698
    bye.restype = _error_code
 
699
    bye.errcheck = _retry_on_error
 
700
    
 
701
    check_version = _library.gnutls_check_version
 
702
    check_version.argtypes = [ctypes.c_char_p]
 
703
    check_version.restype = ctypes.c_char_p
 
704
    
 
705
    # All the function declarations below are from gnutls/openpgp.h
 
706
    
 
707
    openpgp_crt_init = _library.gnutls_openpgp_crt_init
 
708
    openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
 
709
    openpgp_crt_init.restype = _error_code
 
710
    
 
711
    openpgp_crt_import = _library.gnutls_openpgp_crt_import
 
712
    openpgp_crt_import.argtypes = [openpgp_crt_t,
 
713
                                   ctypes.POINTER(datum_t),
 
714
                                   openpgp_crt_fmt_t]
 
715
    openpgp_crt_import.restype = _error_code
 
716
    
 
717
    openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
 
718
    openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
 
719
                                        ctypes.POINTER(ctypes.c_uint)]
 
720
    openpgp_crt_verify_self.restype = _error_code
 
721
    
 
722
    openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
 
723
    openpgp_crt_deinit.argtypes = [openpgp_crt_t]
 
724
    openpgp_crt_deinit.restype = None
 
725
    
 
726
    openpgp_crt_get_fingerprint = (
 
727
        _library.gnutls_openpgp_crt_get_fingerprint)
 
728
    openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
 
729
                                            ctypes.c_void_p,
 
730
                                            ctypes.POINTER(
 
731
                                                ctypes.c_size_t)]
 
732
    openpgp_crt_get_fingerprint.restype = _error_code
 
733
    
 
734
    # Remove non-public functions
 
735
    del _error_code, _retry_on_error
 
736
# Create the global "gnutls" object, simulating a module
 
737
gnutls = GnuTLS()
 
738
 
 
739
def call_pipe(connection,       # : multiprocessing.Connection
 
740
              func, *args, **kwargs):
 
741
    """This function is meant to be called by multiprocessing.Process
 
742
    
 
743
    This function runs func(*args, **kwargs), and writes the resulting
 
744
    return value on the provided multiprocessing.Connection.
 
745
    """
 
746
    connection.send(func(*args, **kwargs))
 
747
    connection.close()
 
748
 
 
749
class Client(object):
179
750
    """A representation of a client host served by this server.
 
751
    
180
752
    Attributes:
181
 
    name:       string; from the config file, used in log messages
 
753
    approved:   bool(); 'None' if not yet approved/disapproved
 
754
    approval_delay: datetime.timedelta(); Time to wait for approval
 
755
    approval_duration: datetime.timedelta(); Duration of one approval
 
756
    checker:    subprocess.Popen(); a running checker process used
 
757
                                    to see if the client lives.
 
758
                                    'None' if no process is running.
 
759
    checker_callback_tag: a GLib event source tag, or None
 
760
    checker_command: string; External command which is run to check
 
761
                     if client lives.  %() expansions are done at
 
762
                     runtime with vars(self) as dict, so that for
 
763
                     instance %(name)s can be used in the command.
 
764
    checker_initiator_tag: a GLib event source tag, or None
 
765
    created:    datetime.datetime(); (UTC) object creation
 
766
    client_structure: Object describing what attributes a client has
 
767
                      and is used for storing the client at exit
 
768
    current_checker_command: string; current running checker_command
 
769
    disable_initiator_tag: a GLib event source tag, or None
 
770
    enabled:    bool()
182
771
    fingerprint: string (40 or 32 hexadecimal digits); used to
183
772
                 uniquely identify the client
184
 
    secret:     bytestring; sent verbatim (over TLS) to client
185
773
    host:       string; available for use by the checker command
186
 
    created:    datetime.datetime(); (UTC) object creation
187
 
    last_enabled: datetime.datetime(); (UTC)
188
 
    enabled:    bool()
 
774
    interval:   datetime.timedelta(); How often to start a new checker
 
775
    last_approval_request: datetime.datetime(); (UTC) or None
189
776
    last_checked_ok: datetime.datetime(); (UTC) or None
 
777
    last_checker_status: integer between 0 and 255 reflecting exit
 
778
                         status of last checker. -1 reflects crashed
 
779
                         checker, -2 means no checker completed yet.
 
780
    last_checker_signal: The signal which killed the last checker, if
 
781
                         last_checker_status is -1
 
782
    last_enabled: datetime.datetime(); (UTC) or None
 
783
    name:       string; from the config file, used in log messages and
 
784
                        D-Bus identifiers
 
785
    secret:     bytestring; sent verbatim (over TLS) to client
190
786
    timeout:    datetime.timedelta(); How long from last_checked_ok
191
 
                                      until this client is invalid
192
 
    interval:   datetime.timedelta(); How often to start a new checker
193
 
    disable_hook:  If set, called by disable() as disable_hook(self)
194
 
    checker:    subprocess.Popen(); a running checker process used
195
 
                                    to see if the client lives.
196
 
                                    'None' if no process is running.
197
 
    checker_initiator_tag: a gobject event source tag, or None
198
 
    disable_initiator_tag:    - '' -
199
 
    checker_callback_tag:  - '' -
200
 
    checker_command: string; External command which is run to check if
201
 
                     client lives.  %() expansions are done at
202
 
                     runtime with vars(self) as dict, so that for
203
 
                     instance %(name)s can be used in the command.
204
 
    dbus_object_path: dbus.ObjectPath
205
 
    Private attibutes:
206
 
    _timeout: Real variable for 'timeout'
207
 
    _interval: Real variable for 'interval'
208
 
    _timeout_milliseconds: Used when calling gobject.timeout_add()
209
 
    _interval_milliseconds: - '' -
 
787
                                      until this client is disabled
 
788
    extended_timeout:   extra long timeout when secret has been sent
 
789
    runtime_expansions: Allowed attributes for runtime expansion.
 
790
    expires:    datetime.datetime(); time (UTC) when a client will be
 
791
                disabled, or None
 
792
    server_settings: The server_settings dict from main()
210
793
    """
211
 
    def _set_timeout(self, timeout):
212
 
        "Setter function for the 'timeout' attribute"
213
 
        self._timeout = timeout
214
 
        self._timeout_milliseconds = ((self.timeout.days
215
 
                                       * 24 * 60 * 60 * 1000)
216
 
                                      + (self.timeout.seconds * 1000)
217
 
                                      + (self.timeout.microseconds
218
 
                                         // 1000))
219
 
        # Emit D-Bus signal
220
 
        self.PropertyChanged(dbus.String(u"timeout"),
221
 
                             (dbus.UInt64(self._timeout_milliseconds,
222
 
                                          variant_level=1)))
223
 
    timeout = property(lambda self: self._timeout, _set_timeout)
224
 
    del _set_timeout
225
 
    
226
 
    def _set_interval(self, interval):
227
 
        "Setter function for the 'interval' attribute"
228
 
        self._interval = interval
229
 
        self._interval_milliseconds = ((self.interval.days
230
 
                                        * 24 * 60 * 60 * 1000)
231
 
                                       + (self.interval.seconds
232
 
                                          * 1000)
233
 
                                       + (self.interval.microseconds
234
 
                                          // 1000))
235
 
        # Emit D-Bus signal
236
 
        self.PropertyChanged(dbus.String(u"interval"),
237
 
                             (dbus.UInt64(self._interval_milliseconds,
238
 
                                          variant_level=1)))
239
 
    interval = property(lambda self: self._interval, _set_interval)
240
 
    del _set_interval
241
 
    
242
 
    def __init__(self, name = None, disable_hook=None, config=None):
243
 
        """Note: the 'checker' key in 'config' sets the
244
 
        'checker_command' attribute and *not* the 'checker'
245
 
        attribute."""
246
 
        self.dbus_object_path = (dbus.ObjectPath
247
 
                                 ("/Mandos/clients/"
248
 
                                  + name.replace(".", "_")))
249
 
        dbus.service.Object.__init__(self, bus,
250
 
                                     self.dbus_object_path)
251
 
        if config is None:
252
 
            config = {}
 
794
    
 
795
    runtime_expansions = ("approval_delay", "approval_duration",
 
796
                          "created", "enabled", "expires",
 
797
                          "fingerprint", "host", "interval",
 
798
                          "last_approval_request", "last_checked_ok",
 
799
                          "last_enabled", "name", "timeout")
 
800
    client_defaults = {
 
801
        "timeout": "PT5M",
 
802
        "extended_timeout": "PT15M",
 
803
        "interval": "PT2M",
 
804
        "checker": "fping -q -- %%(host)s",
 
805
        "host": "",
 
806
        "approval_delay": "PT0S",
 
807
        "approval_duration": "PT1S",
 
808
        "approved_by_default": "True",
 
809
        "enabled": "True",
 
810
    }
 
811
    
 
812
    @staticmethod
 
813
    def config_parser(config):
 
814
        """Construct a new dict of client settings of this form:
 
815
        { client_name: {setting_name: value, ...}, ...}
 
816
        with exceptions for any special settings as defined above.
 
817
        NOTE: Must be a pure function. Must return the same result
 
818
        value given the same arguments.
 
819
        """
 
820
        settings = {}
 
821
        for client_name in config.sections():
 
822
            section = dict(config.items(client_name))
 
823
            client = settings[client_name] = {}
 
824
            
 
825
            client["host"] = section["host"]
 
826
            # Reformat values from string types to Python types
 
827
            client["approved_by_default"] = config.getboolean(
 
828
                client_name, "approved_by_default")
 
829
            client["enabled"] = config.getboolean(client_name,
 
830
                                                  "enabled")
 
831
            
 
832
            # Uppercase and remove spaces from fingerprint for later
 
833
            # comparison purposes with return value from the
 
834
            # fingerprint() function
 
835
            client["fingerprint"] = (section["fingerprint"].upper()
 
836
                                     .replace(" ", ""))
 
837
            if "secret" in section:
 
838
                client["secret"] = codecs.decode(section["secret"]
 
839
                                                 .encode("utf-8"),
 
840
                                                 "base64")
 
841
            elif "secfile" in section:
 
842
                with open(os.path.expanduser(os.path.expandvars
 
843
                                             (section["secfile"])),
 
844
                          "rb") as secfile:
 
845
                    client["secret"] = secfile.read()
 
846
            else:
 
847
                raise TypeError("No secret or secfile for section {}"
 
848
                                .format(section))
 
849
            client["timeout"] = string_to_delta(section["timeout"])
 
850
            client["extended_timeout"] = string_to_delta(
 
851
                section["extended_timeout"])
 
852
            client["interval"] = string_to_delta(section["interval"])
 
853
            client["approval_delay"] = string_to_delta(
 
854
                section["approval_delay"])
 
855
            client["approval_duration"] = string_to_delta(
 
856
                section["approval_duration"])
 
857
            client["checker_command"] = section["checker"]
 
858
            client["last_approval_request"] = None
 
859
            client["last_checked_ok"] = None
 
860
            client["last_checker_status"] = -2
 
861
        
 
862
        return settings
 
863
    
 
864
    def __init__(self, settings, name = None, server_settings=None):
253
865
        self.name = name
254
 
        logger.debug(u"Creating client %r", self.name)
255
 
        # Uppercase and remove spaces from fingerprint for later
256
 
        # comparison purposes with return value from the fingerprint()
257
 
        # function
258
 
        self.fingerprint = (config["fingerprint"].upper()
259
 
                            .replace(u" ", u""))
260
 
        logger.debug(u"  Fingerprint: %s", self.fingerprint)
261
 
        if "secret" in config:
262
 
            self.secret = config["secret"].decode(u"base64")
263
 
        elif "secfile" in config:
264
 
            with closing(open(os.path.expanduser
265
 
                              (os.path.expandvars
266
 
                               (config["secfile"])))) as secfile:
267
 
                self.secret = secfile.read()
 
866
        if server_settings is None:
 
867
            server_settings = {}
 
868
        self.server_settings = server_settings
 
869
        # adding all client settings
 
870
        for setting, value in settings.items():
 
871
            setattr(self, setting, value)
 
872
        
 
873
        if self.enabled:
 
874
            if not hasattr(self, "last_enabled"):
 
875
                self.last_enabled = datetime.datetime.utcnow()
 
876
            if not hasattr(self, "expires"):
 
877
                self.expires = (datetime.datetime.utcnow()
 
878
                                + self.timeout)
268
879
        else:
269
 
            raise TypeError(u"No secret or secfile for client %s"
270
 
                            % self.name)
271
 
        self.host = config.get("host", "")
272
 
        self.created = datetime.datetime.utcnow()
273
 
        self.enabled = False
274
 
        self.last_enabled = None
275
 
        self.last_checked_ok = None
276
 
        self.timeout = string_to_delta(config["timeout"])
277
 
        self.interval = string_to_delta(config["interval"])
278
 
        self.disable_hook = disable_hook
 
880
            self.last_enabled = None
 
881
            self.expires = None
 
882
        
 
883
        logger.debug("Creating client %r", self.name)
 
884
        logger.debug("  Fingerprint: %s", self.fingerprint)
 
885
        self.created = settings.get("created",
 
886
                                    datetime.datetime.utcnow())
 
887
        
 
888
        # attributes specific for this server instance
279
889
        self.checker = None
280
890
        self.checker_initiator_tag = None
281
891
        self.disable_initiator_tag = None
282
892
        self.checker_callback_tag = None
283
 
        self.checker_command = config["checker"]
 
893
        self.current_checker_command = None
 
894
        self.approved = None
 
895
        self.approvals_pending = 0
 
896
        self.changedstate = multiprocessing_manager.Condition(
 
897
            multiprocessing_manager.Lock())
 
898
        self.client_structure = [attr
 
899
                                 for attr in self.__dict__.keys()
 
900
                                 if not attr.startswith("_")]
 
901
        self.client_structure.append("client_structure")
 
902
        
 
903
        for name, t in inspect.getmembers(
 
904
                type(self), lambda obj: isinstance(obj, property)):
 
905
            if not name.startswith("_"):
 
906
                self.client_structure.append(name)
 
907
    
 
908
    # Send notice to process children that client state has changed
 
909
    def send_changedstate(self):
 
910
        with self.changedstate:
 
911
            self.changedstate.notify_all()
284
912
    
285
913
    def enable(self):
286
914
        """Start this client's checker and timeout hooks"""
 
915
        if getattr(self, "enabled", False):
 
916
            # Already enabled
 
917
            return
 
918
        self.expires = datetime.datetime.utcnow() + self.timeout
 
919
        self.enabled = True
287
920
        self.last_enabled = datetime.datetime.utcnow()
 
921
        self.init_checker()
 
922
        self.send_changedstate()
 
923
    
 
924
    def disable(self, quiet=True):
 
925
        """Disable this client."""
 
926
        if not getattr(self, "enabled", False):
 
927
            return False
 
928
        if not quiet:
 
929
            logger.info("Disabling client %s", self.name)
 
930
        if getattr(self, "disable_initiator_tag", None) is not None:
 
931
            GLib.source_remove(self.disable_initiator_tag)
 
932
            self.disable_initiator_tag = None
 
933
        self.expires = None
 
934
        if getattr(self, "checker_initiator_tag", None) is not None:
 
935
            GLib.source_remove(self.checker_initiator_tag)
 
936
            self.checker_initiator_tag = None
 
937
        self.stop_checker()
 
938
        self.enabled = False
 
939
        if not quiet:
 
940
            self.send_changedstate()
 
941
        # Do not run this again if called by a GLib.timeout_add
 
942
        return False
 
943
    
 
944
    def __del__(self):
 
945
        self.disable()
 
946
    
 
947
    def init_checker(self):
288
948
        # Schedule a new checker to be started an 'interval' from now,
289
949
        # and every interval from then on.
290
 
        self.checker_initiator_tag = (gobject.timeout_add
291
 
                                      (self._interval_milliseconds,
292
 
                                       self.start_checker))
 
950
        if self.checker_initiator_tag is not None:
 
951
            GLib.source_remove(self.checker_initiator_tag)
 
952
        self.checker_initiator_tag = GLib.timeout_add(
 
953
            int(self.interval.total_seconds() * 1000),
 
954
            self.start_checker)
 
955
        # Schedule a disable() when 'timeout' has passed
 
956
        if self.disable_initiator_tag is not None:
 
957
            GLib.source_remove(self.disable_initiator_tag)
 
958
        self.disable_initiator_tag = GLib.timeout_add(
 
959
            int(self.timeout.total_seconds() * 1000), self.disable)
293
960
        # Also start a new checker *right now*.
294
961
        self.start_checker()
295
 
        # Schedule a disable() when 'timeout' has passed
296
 
        self.disable_initiator_tag = (gobject.timeout_add
297
 
                                   (self._timeout_milliseconds,
298
 
                                    self.disable))
299
 
        self.enabled = True
300
 
        # Emit D-Bus signal
301
 
        self.PropertyChanged(dbus.String(u"enabled"),
302
 
                             dbus.Boolean(True, variant_level=1))
303
 
        self.PropertyChanged(dbus.String(u"last_enabled"),
304
 
                             (_datetime_to_dbus(self.last_enabled,
305
 
                                                variant_level=1)))
306
 
    
307
 
    def disable(self):
308
 
        """Disable this client."""
309
 
        if not getattr(self, "enabled", False):
310
 
            return False
311
 
        logger.info(u"Disabling client %s", self.name)
312
 
        if getattr(self, "disable_initiator_tag", False):
313
 
            gobject.source_remove(self.disable_initiator_tag)
314
 
            self.disable_initiator_tag = None
315
 
        if getattr(self, "checker_initiator_tag", False):
316
 
            gobject.source_remove(self.checker_initiator_tag)
317
 
            self.checker_initiator_tag = None
318
 
        self.stop_checker()
319
 
        if self.disable_hook:
320
 
            self.disable_hook(self)
321
 
        self.enabled = False
322
 
        # Emit D-Bus signal
323
 
        self.PropertyChanged(dbus.String(u"enabled"),
324
 
                             dbus.Boolean(False, variant_level=1))
325
 
        # Do not run this again if called by a gobject.timeout_add
326
 
        return False
327
 
    
328
 
    def __del__(self):
329
 
        self.disable_hook = None
330
 
        self.disable()
331
 
    
332
 
    def checker_callback(self, pid, condition, command):
 
962
    
 
963
    def checker_callback(self, source, condition, connection,
 
964
                         command):
333
965
        """The checker has completed, so take appropriate actions."""
334
966
        self.checker_callback_tag = None
335
967
        self.checker = None
336
 
        # Emit D-Bus signal
337
 
        self.PropertyChanged(dbus.String(u"checker_running"),
338
 
                             dbus.Boolean(False, variant_level=1))
339
 
        if (os.WIFEXITED(condition)
340
 
            and (os.WEXITSTATUS(condition) == 0)):
341
 
            logger.info(u"Checker for %(name)s succeeded",
342
 
                        vars(self))
343
 
            # Emit D-Bus signal
344
 
            self.CheckerCompleted(dbus.Boolean(True),
345
 
                                  dbus.UInt16(condition),
346
 
                                  dbus.String(command))
347
 
            self.bump_timeout()
348
 
        elif not os.WIFEXITED(condition):
349
 
            logger.warning(u"Checker for %(name)s crashed?",
 
968
        # Read return code from connection (see call_pipe)
 
969
        returncode = connection.recv()
 
970
        connection.close()
 
971
        
 
972
        if returncode >= 0:
 
973
            self.last_checker_status = returncode
 
974
            self.last_checker_signal = None
 
975
            if self.last_checker_status == 0:
 
976
                logger.info("Checker for %(name)s succeeded",
 
977
                            vars(self))
 
978
                self.checked_ok()
 
979
            else:
 
980
                logger.info("Checker for %(name)s failed", vars(self))
 
981
        else:
 
982
            self.last_checker_status = -1
 
983
            self.last_checker_signal = -returncode
 
984
            logger.warning("Checker for %(name)s crashed?",
350
985
                           vars(self))
351
 
            # Emit D-Bus signal
352
 
            self.CheckerCompleted(dbus.Boolean(False),
353
 
                                  dbus.UInt16(condition),
354
 
                                  dbus.String(command))
355
 
        else:
356
 
            logger.info(u"Checker for %(name)s failed",
357
 
                        vars(self))
358
 
            # Emit D-Bus signal
359
 
            self.CheckerCompleted(dbus.Boolean(False),
360
 
                                  dbus.UInt16(condition),
361
 
                                  dbus.String(command))
 
986
        return False
362
987
    
363
 
    def bump_timeout(self):
364
 
        """Bump up the timeout for this client.
365
 
        This should only be called when the client has been seen,
366
 
        alive and well.
367
 
        """
 
988
    def checked_ok(self):
 
989
        """Assert that the client has been seen, alive and well."""
368
990
        self.last_checked_ok = datetime.datetime.utcnow()
369
 
        gobject.source_remove(self.disable_initiator_tag)
370
 
        self.disable_initiator_tag = (gobject.timeout_add
371
 
                                      (self._timeout_milliseconds,
372
 
                                       self.disable))
373
 
        self.PropertyChanged(dbus.String(u"last_checked_ok"),
374
 
                             (_datetime_to_dbus(self.last_checked_ok,
375
 
                                                variant_level=1)))
 
991
        self.last_checker_status = 0
 
992
        self.last_checker_signal = None
 
993
        self.bump_timeout()
 
994
    
 
995
    def bump_timeout(self, timeout=None):
 
996
        """Bump up the timeout for this client."""
 
997
        if timeout is None:
 
998
            timeout = self.timeout
 
999
        if self.disable_initiator_tag is not None:
 
1000
            GLib.source_remove(self.disable_initiator_tag)
 
1001
            self.disable_initiator_tag = None
 
1002
        if getattr(self, "enabled", False):
 
1003
            self.disable_initiator_tag = GLib.timeout_add(
 
1004
                int(timeout.total_seconds() * 1000), self.disable)
 
1005
            self.expires = datetime.datetime.utcnow() + timeout
 
1006
    
 
1007
    def need_approval(self):
 
1008
        self.last_approval_request = datetime.datetime.utcnow()
376
1009
    
377
1010
    def start_checker(self):
378
1011
        """Start a new checker subprocess if one is not running.
 
1012
        
379
1013
        If a checker already exists, leave it running and do
380
1014
        nothing."""
381
1015
        # The reason for not killing a running checker is that if we
382
 
        # did that, then if a checker (for some reason) started
383
 
        # running slowly and taking more than 'interval' time, the
384
 
        # client would inevitably timeout, since no checker would get
385
 
        # a chance to run to completion.  If we instead leave running
 
1016
        # did that, and if a checker (for some reason) started running
 
1017
        # slowly and taking more than 'interval' time, then the client
 
1018
        # would inevitably timeout, since no checker would get a
 
1019
        # chance to run to completion.  If we instead leave running
386
1020
        # checkers alone, the checker would have to take more time
387
 
        # than 'timeout' for the client to be declared invalid, which
388
 
        # is as it should be.
 
1021
        # than 'timeout' for the client to be disabled, which is as it
 
1022
        # should be.
 
1023
        
 
1024
        if self.checker is not None and not self.checker.is_alive():
 
1025
            logger.warning("Checker was not alive; joining")
 
1026
            self.checker.join()
 
1027
            self.checker = None
 
1028
        # Start a new checker if needed
389
1029
        if self.checker is None:
390
 
            try:
391
 
                # In case checker_command has exactly one % operator
392
 
                command = self.checker_command % self.host
393
 
            except TypeError:
394
 
                # Escape attributes for the shell
395
 
                escaped_attrs = dict((key, re.escape(str(val)))
396
 
                                     for key, val in
397
 
                                     vars(self).iteritems())
398
 
                try:
399
 
                    command = self.checker_command % escaped_attrs
400
 
                except TypeError, error:
401
 
                    logger.error(u'Could not format string "%s":'
402
 
                                 u' %s', self.checker_command, error)
403
 
                    return True # Try again later
404
 
            try:
405
 
                logger.info(u"Starting checker %r for %s",
406
 
                            command, self.name)
407
 
                # We don't need to redirect stdout and stderr, since
408
 
                # in normal mode, that is already done by daemon(),
409
 
                # and in debug mode we don't want to.  (Stdin is
410
 
                # always replaced by /dev/null.)
411
 
                self.checker = subprocess.Popen(command,
412
 
                                                close_fds=True,
413
 
                                                shell=True, cwd="/")
414
 
                # Emit D-Bus signal
415
 
                self.CheckerStarted(command)
416
 
                self.PropertyChanged(dbus.String("checker_running"),
417
 
                                     dbus.Boolean(True, variant_level=1))
418
 
                self.checker_callback_tag = (gobject.child_watch_add
419
 
                                             (self.checker.pid,
420
 
                                              self.checker_callback,
421
 
                                              data=command))
422
 
            except OSError, error:
423
 
                logger.error(u"Failed to start subprocess: %s",
424
 
                             error)
425
 
        # Re-run this periodically if run by gobject.timeout_add
 
1030
            # Escape attributes for the shell
 
1031
            escaped_attrs = {
 
1032
                attr: re.escape(str(getattr(self, attr)))
 
1033
                for attr in self.runtime_expansions }
 
1034
            try:
 
1035
                command = self.checker_command % escaped_attrs
 
1036
            except TypeError as error:
 
1037
                logger.error('Could not format string "%s"',
 
1038
                             self.checker_command,
 
1039
                             exc_info=error)
 
1040
                return True     # Try again later
 
1041
            self.current_checker_command = command
 
1042
            logger.info("Starting checker %r for %s", command,
 
1043
                        self.name)
 
1044
            # We don't need to redirect stdout and stderr, since
 
1045
            # in normal mode, that is already done by daemon(),
 
1046
            # and in debug mode we don't want to.  (Stdin is
 
1047
            # always replaced by /dev/null.)
 
1048
            # The exception is when not debugging but nevertheless
 
1049
            # running in the foreground; use the previously
 
1050
            # created wnull.
 
1051
            popen_args = { "close_fds": True,
 
1052
                           "shell": True,
 
1053
                           "cwd": "/" }
 
1054
            if (not self.server_settings["debug"]
 
1055
                and self.server_settings["foreground"]):
 
1056
                popen_args.update({"stdout": wnull,
 
1057
                                   "stderr": wnull })
 
1058
            pipe = multiprocessing.Pipe(duplex = False)
 
1059
            self.checker = multiprocessing.Process(
 
1060
                target = call_pipe,
 
1061
                args = (pipe[1], subprocess.call, command),
 
1062
                kwargs = popen_args)
 
1063
            self.checker.start()
 
1064
            self.checker_callback_tag = GLib.io_add_watch(
 
1065
                pipe[0].fileno(), GLib.IO_IN,
 
1066
                self.checker_callback, pipe[0], command)
 
1067
        # Re-run this periodically if run by GLib.timeout_add
426
1068
        return True
427
1069
    
428
1070
    def stop_checker(self):
429
1071
        """Force the checker process, if any, to stop."""
430
1072
        if self.checker_callback_tag:
431
 
            gobject.source_remove(self.checker_callback_tag)
 
1073
            GLib.source_remove(self.checker_callback_tag)
432
1074
            self.checker_callback_tag = None
433
1075
        if getattr(self, "checker", None) is None:
434
1076
            return
435
 
        logger.debug(u"Stopping checker for %(name)s", vars(self))
436
 
        try:
437
 
            os.kill(self.checker.pid, signal.SIGTERM)
438
 
            #os.sleep(0.5)
439
 
            #if self.checker.poll() is None:
440
 
            #    os.kill(self.checker.pid, signal.SIGKILL)
441
 
        except OSError, error:
442
 
            if error.errno != errno.ESRCH: # No such process
443
 
                raise
 
1077
        logger.debug("Stopping checker for %(name)s", vars(self))
 
1078
        self.checker.terminate()
444
1079
        self.checker = None
445
 
        self.PropertyChanged(dbus.String(u"checker_running"),
446
 
                             dbus.Boolean(False, variant_level=1))
447
 
    
448
 
    def still_valid(self):
449
 
        """Has the timeout not yet passed for this client?"""
450
 
        if not getattr(self, "enabled", False):
451
 
            return False
452
 
        now = datetime.datetime.utcnow()
453
 
        if self.last_checked_ok is None:
454
 
            return now < (self.created + self.timeout)
 
1080
 
 
1081
 
 
1082
def dbus_service_property(dbus_interface,
 
1083
                          signature="v",
 
1084
                          access="readwrite",
 
1085
                          byte_arrays=False):
 
1086
    """Decorators for marking methods of a DBusObjectWithProperties to
 
1087
    become properties on the D-Bus.
 
1088
    
 
1089
    The decorated method will be called with no arguments by "Get"
 
1090
    and with one argument by "Set".
 
1091
    
 
1092
    The parameters, where they are supported, are the same as
 
1093
    dbus.service.method, except there is only "signature", since the
 
1094
    type from Get() and the type sent to Set() is the same.
 
1095
    """
 
1096
    # Encoding deeply encoded byte arrays is not supported yet by the
 
1097
    # "Set" method, so we fail early here:
 
1098
    if byte_arrays and signature != "ay":
 
1099
        raise ValueError("Byte arrays not supported for non-'ay'"
 
1100
                         " signature {!r}".format(signature))
 
1101
    
 
1102
    def decorator(func):
 
1103
        func._dbus_is_property = True
 
1104
        func._dbus_interface = dbus_interface
 
1105
        func._dbus_signature = signature
 
1106
        func._dbus_access = access
 
1107
        func._dbus_name = func.__name__
 
1108
        if func._dbus_name.endswith("_dbus_property"):
 
1109
            func._dbus_name = func._dbus_name[:-14]
 
1110
        func._dbus_get_args_options = {'byte_arrays': byte_arrays }
 
1111
        return func
 
1112
    
 
1113
    return decorator
 
1114
 
 
1115
 
 
1116
def dbus_interface_annotations(dbus_interface):
 
1117
    """Decorator for marking functions returning interface annotations
 
1118
    
 
1119
    Usage:
 
1120
    
 
1121
    @dbus_interface_annotations("org.example.Interface")
 
1122
    def _foo(self):  # Function name does not matter
 
1123
        return {"org.freedesktop.DBus.Deprecated": "true",
 
1124
                "org.freedesktop.DBus.Property.EmitsChangedSignal":
 
1125
                    "false"}
 
1126
    """
 
1127
    
 
1128
    def decorator(func):
 
1129
        func._dbus_is_interface = True
 
1130
        func._dbus_interface = dbus_interface
 
1131
        func._dbus_name = dbus_interface
 
1132
        return func
 
1133
    
 
1134
    return decorator
 
1135
 
 
1136
 
 
1137
def dbus_annotations(annotations):
 
1138
    """Decorator to annotate D-Bus methods, signals or properties
 
1139
    Usage:
 
1140
    
 
1141
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true",
 
1142
                       "org.freedesktop.DBus.Property."
 
1143
                       "EmitsChangedSignal": "false"})
 
1144
    @dbus_service_property("org.example.Interface", signature="b",
 
1145
                           access="r")
 
1146
    def Property_dbus_property(self):
 
1147
        return dbus.Boolean(False)
 
1148
    
 
1149
    See also the DBusObjectWithAnnotations class.
 
1150
    """
 
1151
    
 
1152
    def decorator(func):
 
1153
        func._dbus_annotations = annotations
 
1154
        return func
 
1155
    
 
1156
    return decorator
 
1157
 
 
1158
 
 
1159
class DBusPropertyException(dbus.exceptions.DBusException):
 
1160
    """A base class for D-Bus property-related exceptions
 
1161
    """
 
1162
    pass
 
1163
 
 
1164
 
 
1165
class DBusPropertyAccessException(DBusPropertyException):
 
1166
    """A property's access permissions disallows an operation.
 
1167
    """
 
1168
    pass
 
1169
 
 
1170
 
 
1171
class DBusPropertyNotFound(DBusPropertyException):
 
1172
    """An attempt was made to access a non-existing property.
 
1173
    """
 
1174
    pass
 
1175
 
 
1176
 
 
1177
class DBusObjectWithAnnotations(dbus.service.Object):
 
1178
    """A D-Bus object with annotations.
 
1179
    
 
1180
    Classes inheriting from this can use the dbus_annotations
 
1181
    decorator to add annotations to methods or signals.
 
1182
    """
 
1183
    
 
1184
    @staticmethod
 
1185
    def _is_dbus_thing(thing):
 
1186
        """Returns a function testing if an attribute is a D-Bus thing
 
1187
        
 
1188
        If called like _is_dbus_thing("method") it returns a function
 
1189
        suitable for use as predicate to inspect.getmembers().
 
1190
        """
 
1191
        return lambda obj: getattr(obj, "_dbus_is_{}".format(thing),
 
1192
                                   False)
 
1193
    
 
1194
    def _get_all_dbus_things(self, thing):
 
1195
        """Returns a generator of (name, attribute) pairs
 
1196
        """
 
1197
        return ((getattr(athing.__get__(self), "_dbus_name", name),
 
1198
                 athing.__get__(self))
 
1199
                for cls in self.__class__.__mro__
 
1200
                for name, athing in
 
1201
                inspect.getmembers(cls, self._is_dbus_thing(thing)))
 
1202
    
 
1203
    @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
 
1204
                         out_signature = "s",
 
1205
                         path_keyword = 'object_path',
 
1206
                         connection_keyword = 'connection')
 
1207
    def Introspect(self, object_path, connection):
 
1208
        """Overloading of standard D-Bus method.
 
1209
        
 
1210
        Inserts annotation tags on methods and signals.
 
1211
        """
 
1212
        xmlstring = dbus.service.Object.Introspect(self, object_path,
 
1213
                                                   connection)
 
1214
        try:
 
1215
            document = xml.dom.minidom.parseString(xmlstring)
 
1216
            
 
1217
            for if_tag in document.getElementsByTagName("interface"):
 
1218
                # Add annotation tags
 
1219
                for typ in ("method", "signal"):
 
1220
                    for tag in if_tag.getElementsByTagName(typ):
 
1221
                        annots = dict()
 
1222
                        for name, prop in (self.
 
1223
                                           _get_all_dbus_things(typ)):
 
1224
                            if (name == tag.getAttribute("name")
 
1225
                                and prop._dbus_interface
 
1226
                                == if_tag.getAttribute("name")):
 
1227
                                annots.update(getattr(
 
1228
                                    prop, "_dbus_annotations", {}))
 
1229
                        for name, value in annots.items():
 
1230
                            ann_tag = document.createElement(
 
1231
                                "annotation")
 
1232
                            ann_tag.setAttribute("name", name)
 
1233
                            ann_tag.setAttribute("value", value)
 
1234
                            tag.appendChild(ann_tag)
 
1235
                # Add interface annotation tags
 
1236
                for annotation, value in dict(
 
1237
                    itertools.chain.from_iterable(
 
1238
                        annotations().items()
 
1239
                        for name, annotations
 
1240
                        in self._get_all_dbus_things("interface")
 
1241
                        if name == if_tag.getAttribute("name")
 
1242
                        )).items():
 
1243
                    ann_tag = document.createElement("annotation")
 
1244
                    ann_tag.setAttribute("name", annotation)
 
1245
                    ann_tag.setAttribute("value", value)
 
1246
                    if_tag.appendChild(ann_tag)
 
1247
                # Fix argument name for the Introspect method itself
 
1248
                if (if_tag.getAttribute("name")
 
1249
                                == dbus.INTROSPECTABLE_IFACE):
 
1250
                    for cn in if_tag.getElementsByTagName("method"):
 
1251
                        if cn.getAttribute("name") == "Introspect":
 
1252
                            for arg in cn.getElementsByTagName("arg"):
 
1253
                                if (arg.getAttribute("direction")
 
1254
                                    == "out"):
 
1255
                                    arg.setAttribute("name",
 
1256
                                                     "xml_data")
 
1257
            xmlstring = document.toxml("utf-8")
 
1258
            document.unlink()
 
1259
        except (AttributeError, xml.dom.DOMException,
 
1260
                xml.parsers.expat.ExpatError) as error:
 
1261
            logger.error("Failed to override Introspection method",
 
1262
                         exc_info=error)
 
1263
        return xmlstring
 
1264
 
 
1265
 
 
1266
class DBusObjectWithProperties(DBusObjectWithAnnotations):
 
1267
    """A D-Bus object with properties.
 
1268
    
 
1269
    Classes inheriting from this can use the dbus_service_property
 
1270
    decorator to expose methods as D-Bus properties.  It exposes the
 
1271
    standard Get(), Set(), and GetAll() methods on the D-Bus.
 
1272
    """
 
1273
    
 
1274
    def _get_dbus_property(self, interface_name, property_name):
 
1275
        """Returns a bound method if one exists which is a D-Bus
 
1276
        property with the specified name and interface.
 
1277
        """
 
1278
        for cls in self.__class__.__mro__:
 
1279
            for name, value in inspect.getmembers(
 
1280
                    cls, self._is_dbus_thing("property")):
 
1281
                if (value._dbus_name == property_name
 
1282
                    and value._dbus_interface == interface_name):
 
1283
                    return value.__get__(self)
 
1284
        
 
1285
        # No such property
 
1286
        raise DBusPropertyNotFound("{}:{}.{}".format(
 
1287
            self.dbus_object_path, interface_name, property_name))
 
1288
    
 
1289
    @classmethod
 
1290
    def _get_all_interface_names(cls):
 
1291
        """Get a sequence of all interfaces supported by an object"""
 
1292
        return (name for name in set(getattr(getattr(x, attr),
 
1293
                                             "_dbus_interface", None)
 
1294
                                     for x in (inspect.getmro(cls))
 
1295
                                     for attr in dir(x))
 
1296
                if name is not None)
 
1297
    
 
1298
    @dbus.service.method(dbus.PROPERTIES_IFACE,
 
1299
                         in_signature="ss",
 
1300
                         out_signature="v")
 
1301
    def Get(self, interface_name, property_name):
 
1302
        """Standard D-Bus property Get() method, see D-Bus standard.
 
1303
        """
 
1304
        prop = self._get_dbus_property(interface_name, property_name)
 
1305
        if prop._dbus_access == "write":
 
1306
            raise DBusPropertyAccessException(property_name)
 
1307
        value = prop()
 
1308
        if not hasattr(value, "variant_level"):
 
1309
            return value
 
1310
        return type(value)(value, variant_level=value.variant_level+1)
 
1311
    
 
1312
    @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
 
1313
    def Set(self, interface_name, property_name, value):
 
1314
        """Standard D-Bus property Set() method, see D-Bus standard.
 
1315
        """
 
1316
        prop = self._get_dbus_property(interface_name, property_name)
 
1317
        if prop._dbus_access == "read":
 
1318
            raise DBusPropertyAccessException(property_name)
 
1319
        if prop._dbus_get_args_options["byte_arrays"]:
 
1320
            # The byte_arrays option is not supported yet on
 
1321
            # signatures other than "ay".
 
1322
            if prop._dbus_signature != "ay":
 
1323
                raise ValueError("Byte arrays not supported for non-"
 
1324
                                 "'ay' signature {!r}"
 
1325
                                 .format(prop._dbus_signature))
 
1326
            value = dbus.ByteArray(b''.join(chr(byte)
 
1327
                                            for byte in value))
 
1328
        prop(value)
 
1329
    
 
1330
    @dbus.service.method(dbus.PROPERTIES_IFACE,
 
1331
                         in_signature="s",
 
1332
                         out_signature="a{sv}")
 
1333
    def GetAll(self, interface_name):
 
1334
        """Standard D-Bus property GetAll() method, see D-Bus
 
1335
        standard.
 
1336
        
 
1337
        Note: Will not include properties with access="write".
 
1338
        """
 
1339
        properties = {}
 
1340
        for name, prop in self._get_all_dbus_things("property"):
 
1341
            if (interface_name
 
1342
                and interface_name != prop._dbus_interface):
 
1343
                # Interface non-empty but did not match
 
1344
                continue
 
1345
            # Ignore write-only properties
 
1346
            if prop._dbus_access == "write":
 
1347
                continue
 
1348
            value = prop()
 
1349
            if not hasattr(value, "variant_level"):
 
1350
                properties[name] = value
 
1351
                continue
 
1352
            properties[name] = type(value)(
 
1353
                value, variant_level = value.variant_level + 1)
 
1354
        return dbus.Dictionary(properties, signature="sv")
 
1355
    
 
1356
    @dbus.service.signal(dbus.PROPERTIES_IFACE, signature="sa{sv}as")
 
1357
    def PropertiesChanged(self, interface_name, changed_properties,
 
1358
                          invalidated_properties):
 
1359
        """Standard D-Bus PropertiesChanged() signal, see D-Bus
 
1360
        standard.
 
1361
        """
 
1362
        pass
 
1363
    
 
1364
    @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
 
1365
                         out_signature="s",
 
1366
                         path_keyword='object_path',
 
1367
                         connection_keyword='connection')
 
1368
    def Introspect(self, object_path, connection):
 
1369
        """Overloading of standard D-Bus method.
 
1370
        
 
1371
        Inserts property tags and interface annotation tags.
 
1372
        """
 
1373
        xmlstring = DBusObjectWithAnnotations.Introspect(self,
 
1374
                                                         object_path,
 
1375
                                                         connection)
 
1376
        try:
 
1377
            document = xml.dom.minidom.parseString(xmlstring)
 
1378
            
 
1379
            def make_tag(document, name, prop):
 
1380
                e = document.createElement("property")
 
1381
                e.setAttribute("name", name)
 
1382
                e.setAttribute("type", prop._dbus_signature)
 
1383
                e.setAttribute("access", prop._dbus_access)
 
1384
                return e
 
1385
            
 
1386
            for if_tag in document.getElementsByTagName("interface"):
 
1387
                # Add property tags
 
1388
                for tag in (make_tag(document, name, prop)
 
1389
                            for name, prop
 
1390
                            in self._get_all_dbus_things("property")
 
1391
                            if prop._dbus_interface
 
1392
                            == if_tag.getAttribute("name")):
 
1393
                    if_tag.appendChild(tag)
 
1394
                # Add annotation tags for properties
 
1395
                for tag in if_tag.getElementsByTagName("property"):
 
1396
                    annots = dict()
 
1397
                    for name, prop in self._get_all_dbus_things(
 
1398
                            "property"):
 
1399
                        if (name == tag.getAttribute("name")
 
1400
                            and prop._dbus_interface
 
1401
                            == if_tag.getAttribute("name")):
 
1402
                            annots.update(getattr(
 
1403
                                prop, "_dbus_annotations", {}))
 
1404
                    for name, value in annots.items():
 
1405
                        ann_tag = document.createElement(
 
1406
                            "annotation")
 
1407
                        ann_tag.setAttribute("name", name)
 
1408
                        ann_tag.setAttribute("value", value)
 
1409
                        tag.appendChild(ann_tag)
 
1410
                # Add the names to the return values for the
 
1411
                # "org.freedesktop.DBus.Properties" methods
 
1412
                if (if_tag.getAttribute("name")
 
1413
                    == "org.freedesktop.DBus.Properties"):
 
1414
                    for cn in if_tag.getElementsByTagName("method"):
 
1415
                        if cn.getAttribute("name") == "Get":
 
1416
                            for arg in cn.getElementsByTagName("arg"):
 
1417
                                if (arg.getAttribute("direction")
 
1418
                                    == "out"):
 
1419
                                    arg.setAttribute("name", "value")
 
1420
                        elif cn.getAttribute("name") == "GetAll":
 
1421
                            for arg in cn.getElementsByTagName("arg"):
 
1422
                                if (arg.getAttribute("direction")
 
1423
                                    == "out"):
 
1424
                                    arg.setAttribute("name", "props")
 
1425
            xmlstring = document.toxml("utf-8")
 
1426
            document.unlink()
 
1427
        except (AttributeError, xml.dom.DOMException,
 
1428
                xml.parsers.expat.ExpatError) as error:
 
1429
            logger.error("Failed to override Introspection method",
 
1430
                         exc_info=error)
 
1431
        return xmlstring
 
1432
 
 
1433
try:
 
1434
    dbus.OBJECT_MANAGER_IFACE
 
1435
except AttributeError:
 
1436
    dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
 
1437
 
 
1438
class DBusObjectWithObjectManager(DBusObjectWithAnnotations):
 
1439
    """A D-Bus object with an ObjectManager.
 
1440
    
 
1441
    Classes inheriting from this exposes the standard
 
1442
    GetManagedObjects call and the InterfacesAdded and
 
1443
    InterfacesRemoved signals on the standard
 
1444
    "org.freedesktop.DBus.ObjectManager" interface.
 
1445
    
 
1446
    Note: No signals are sent automatically; they must be sent
 
1447
    manually.
 
1448
    """
 
1449
    @dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
 
1450
                         out_signature = "a{oa{sa{sv}}}")
 
1451
    def GetManagedObjects(self):
 
1452
        """This function must be overridden"""
 
1453
        raise NotImplementedError()
 
1454
    
 
1455
    @dbus.service.signal(dbus.OBJECT_MANAGER_IFACE,
 
1456
                         signature = "oa{sa{sv}}")
 
1457
    def InterfacesAdded(self, object_path, interfaces_and_properties):
 
1458
        pass
 
1459
    
 
1460
    @dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature = "oas")
 
1461
    def InterfacesRemoved(self, object_path, interfaces):
 
1462
        pass
 
1463
    
 
1464
    @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
 
1465
                         out_signature = "s",
 
1466
                         path_keyword = 'object_path',
 
1467
                         connection_keyword = 'connection')
 
1468
    def Introspect(self, object_path, connection):
 
1469
        """Overloading of standard D-Bus method.
 
1470
        
 
1471
        Override return argument name of GetManagedObjects to be
 
1472
        "objpath_interfaces_and_properties"
 
1473
        """
 
1474
        xmlstring = DBusObjectWithAnnotations.Introspect(self,
 
1475
                                                         object_path,
 
1476
                                                         connection)
 
1477
        try:
 
1478
            document = xml.dom.minidom.parseString(xmlstring)
 
1479
            
 
1480
            for if_tag in document.getElementsByTagName("interface"):
 
1481
                # Fix argument name for the GetManagedObjects method
 
1482
                if (if_tag.getAttribute("name")
 
1483
                                == dbus.OBJECT_MANAGER_IFACE):
 
1484
                    for cn in if_tag.getElementsByTagName("method"):
 
1485
                        if (cn.getAttribute("name")
 
1486
                            == "GetManagedObjects"):
 
1487
                            for arg in cn.getElementsByTagName("arg"):
 
1488
                                if (arg.getAttribute("direction")
 
1489
                                    == "out"):
 
1490
                                    arg.setAttribute(
 
1491
                                        "name",
 
1492
                                        "objpath_interfaces"
 
1493
                                        "_and_properties")
 
1494
            xmlstring = document.toxml("utf-8")
 
1495
            document.unlink()
 
1496
        except (AttributeError, xml.dom.DOMException,
 
1497
                xml.parsers.expat.ExpatError) as error:
 
1498
            logger.error("Failed to override Introspection method",
 
1499
                         exc_info = error)
 
1500
        return xmlstring
 
1501
 
 
1502
def datetime_to_dbus(dt, variant_level=0):
 
1503
    """Convert a UTC datetime.datetime() to a D-Bus type."""
 
1504
    if dt is None:
 
1505
        return dbus.String("", variant_level = variant_level)
 
1506
    return dbus.String(dt.isoformat(), variant_level=variant_level)
 
1507
 
 
1508
 
 
1509
def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
 
1510
    """A class decorator; applied to a subclass of
 
1511
    dbus.service.Object, it will add alternate D-Bus attributes with
 
1512
    interface names according to the "alt_interface_names" mapping.
 
1513
    Usage:
 
1514
    
 
1515
    @alternate_dbus_interfaces({"org.example.Interface":
 
1516
                                    "net.example.AlternateInterface"})
 
1517
    class SampleDBusObject(dbus.service.Object):
 
1518
        @dbus.service.method("org.example.Interface")
 
1519
        def SampleDBusMethod():
 
1520
            pass
 
1521
    
 
1522
    The above "SampleDBusMethod" on "SampleDBusObject" will be
 
1523
    reachable via two interfaces: "org.example.Interface" and
 
1524
    "net.example.AlternateInterface", the latter of which will have
 
1525
    its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
 
1526
    "true", unless "deprecate" is passed with a False value.
 
1527
    
 
1528
    This works for methods and signals, and also for D-Bus properties
 
1529
    (from DBusObjectWithProperties) and interfaces (from the
 
1530
    dbus_interface_annotations decorator).
 
1531
    """
 
1532
    
 
1533
    def wrapper(cls):
 
1534
        for orig_interface_name, alt_interface_name in (
 
1535
                alt_interface_names.items()):
 
1536
            attr = {}
 
1537
            interface_names = set()
 
1538
            # Go though all attributes of the class
 
1539
            for attrname, attribute in inspect.getmembers(cls):
 
1540
                # Ignore non-D-Bus attributes, and D-Bus attributes
 
1541
                # with the wrong interface name
 
1542
                if (not hasattr(attribute, "_dbus_interface")
 
1543
                    or not attribute._dbus_interface.startswith(
 
1544
                        orig_interface_name)):
 
1545
                    continue
 
1546
                # Create an alternate D-Bus interface name based on
 
1547
                # the current name
 
1548
                alt_interface = attribute._dbus_interface.replace(
 
1549
                    orig_interface_name, alt_interface_name)
 
1550
                interface_names.add(alt_interface)
 
1551
                # Is this a D-Bus signal?
 
1552
                if getattr(attribute, "_dbus_is_signal", False):
 
1553
                    # Extract the original non-method undecorated
 
1554
                    # function by black magic
 
1555
                    if sys.version_info.major == 2:
 
1556
                        nonmethod_func = (dict(
 
1557
                            zip(attribute.func_code.co_freevars,
 
1558
                                attribute.__closure__))
 
1559
                                          ["func"].cell_contents)
 
1560
                    else:
 
1561
                        nonmethod_func = (dict(
 
1562
                            zip(attribute.__code__.co_freevars,
 
1563
                                attribute.__closure__))
 
1564
                                          ["func"].cell_contents)
 
1565
                    # Create a new, but exactly alike, function
 
1566
                    # object, and decorate it to be a new D-Bus signal
 
1567
                    # with the alternate D-Bus interface name
 
1568
                    new_function = copy_function(nonmethod_func)
 
1569
                    new_function = (dbus.service.signal(
 
1570
                        alt_interface,
 
1571
                        attribute._dbus_signature)(new_function))
 
1572
                    # Copy annotations, if any
 
1573
                    try:
 
1574
                        new_function._dbus_annotations = dict(
 
1575
                            attribute._dbus_annotations)
 
1576
                    except AttributeError:
 
1577
                        pass
 
1578
                    # Define a creator of a function to call both the
 
1579
                    # original and alternate functions, so both the
 
1580
                    # original and alternate signals gets sent when
 
1581
                    # the function is called
 
1582
                    def fixscope(func1, func2):
 
1583
                        """This function is a scope container to pass
 
1584
                        func1 and func2 to the "call_both" function
 
1585
                        outside of its arguments"""
 
1586
                        
 
1587
                        @functools.wraps(func2)
 
1588
                        def call_both(*args, **kwargs):
 
1589
                            """This function will emit two D-Bus
 
1590
                            signals by calling func1 and func2"""
 
1591
                            func1(*args, **kwargs)
 
1592
                            func2(*args, **kwargs)
 
1593
                        # Make wrapper function look like a D-Bus signal
 
1594
                        for name, attr in inspect.getmembers(func2):
 
1595
                            if name.startswith("_dbus_"):
 
1596
                                setattr(call_both, name, attr)
 
1597
                        
 
1598
                        return call_both
 
1599
                    # Create the "call_both" function and add it to
 
1600
                    # the class
 
1601
                    attr[attrname] = fixscope(attribute, new_function)
 
1602
                # Is this a D-Bus method?
 
1603
                elif getattr(attribute, "_dbus_is_method", False):
 
1604
                    # Create a new, but exactly alike, function
 
1605
                    # object.  Decorate it to be a new D-Bus method
 
1606
                    # with the alternate D-Bus interface name.  Add it
 
1607
                    # to the class.
 
1608
                    attr[attrname] = (
 
1609
                        dbus.service.method(
 
1610
                            alt_interface,
 
1611
                            attribute._dbus_in_signature,
 
1612
                            attribute._dbus_out_signature)
 
1613
                        (copy_function(attribute)))
 
1614
                    # Copy annotations, if any
 
1615
                    try:
 
1616
                        attr[attrname]._dbus_annotations = dict(
 
1617
                            attribute._dbus_annotations)
 
1618
                    except AttributeError:
 
1619
                        pass
 
1620
                # Is this a D-Bus property?
 
1621
                elif getattr(attribute, "_dbus_is_property", False):
 
1622
                    # Create a new, but exactly alike, function
 
1623
                    # object, and decorate it to be a new D-Bus
 
1624
                    # property with the alternate D-Bus interface
 
1625
                    # name.  Add it to the class.
 
1626
                    attr[attrname] = (dbus_service_property(
 
1627
                        alt_interface, attribute._dbus_signature,
 
1628
                        attribute._dbus_access,
 
1629
                        attribute._dbus_get_args_options
 
1630
                        ["byte_arrays"])
 
1631
                                      (copy_function(attribute)))
 
1632
                    # Copy annotations, if any
 
1633
                    try:
 
1634
                        attr[attrname]._dbus_annotations = dict(
 
1635
                            attribute._dbus_annotations)
 
1636
                    except AttributeError:
 
1637
                        pass
 
1638
                # Is this a D-Bus interface?
 
1639
                elif getattr(attribute, "_dbus_is_interface", False):
 
1640
                    # Create a new, but exactly alike, function
 
1641
                    # object.  Decorate it to be a new D-Bus interface
 
1642
                    # with the alternate D-Bus interface name.  Add it
 
1643
                    # to the class.
 
1644
                    attr[attrname] = (
 
1645
                        dbus_interface_annotations(alt_interface)
 
1646
                        (copy_function(attribute)))
 
1647
            if deprecate:
 
1648
                # Deprecate all alternate interfaces
 
1649
                iname="_AlternateDBusNames_interface_annotation{}"
 
1650
                for interface_name in interface_names:
 
1651
                    
 
1652
                    @dbus_interface_annotations(interface_name)
 
1653
                    def func(self):
 
1654
                        return { "org.freedesktop.DBus.Deprecated":
 
1655
                                 "true" }
 
1656
                    # Find an unused name
 
1657
                    for aname in (iname.format(i)
 
1658
                                  for i in itertools.count()):
 
1659
                        if aname not in attr:
 
1660
                            attr[aname] = func
 
1661
                            break
 
1662
            if interface_names:
 
1663
                # Replace the class with a new subclass of it with
 
1664
                # methods, signals, etc. as created above.
 
1665
                if sys.version_info.major == 2:
 
1666
                    cls = type(b"{}Alternate".format(cls.__name__),
 
1667
                               (cls, ), attr)
 
1668
                else:
 
1669
                    cls = type("{}Alternate".format(cls.__name__),
 
1670
                               (cls, ), attr)
 
1671
        return cls
 
1672
    
 
1673
    return wrapper
 
1674
 
 
1675
 
 
1676
@alternate_dbus_interfaces({"se.recompile.Mandos":
 
1677
                            "se.bsnet.fukt.Mandos"})
 
1678
class ClientDBus(Client, DBusObjectWithProperties):
 
1679
    """A Client class using D-Bus
 
1680
    
 
1681
    Attributes:
 
1682
    dbus_object_path: dbus.ObjectPath
 
1683
    bus: dbus.SystemBus()
 
1684
    """
 
1685
    
 
1686
    runtime_expansions = (Client.runtime_expansions
 
1687
                          + ("dbus_object_path", ))
 
1688
    
 
1689
    _interface = "se.recompile.Mandos.Client"
 
1690
    
 
1691
    # dbus.service.Object doesn't use super(), so we can't either.
 
1692
    
 
1693
    def __init__(self, bus = None, *args, **kwargs):
 
1694
        self.bus = bus
 
1695
        Client.__init__(self, *args, **kwargs)
 
1696
        # Only now, when this client is initialized, can it show up on
 
1697
        # the D-Bus
 
1698
        client_object_name = str(self.name).translate(
 
1699
            {ord("."): ord("_"),
 
1700
             ord("-"): ord("_")})
 
1701
        self.dbus_object_path = dbus.ObjectPath(
 
1702
            "/clients/" + client_object_name)
 
1703
        DBusObjectWithProperties.__init__(self, self.bus,
 
1704
                                          self.dbus_object_path)
 
1705
    
 
1706
    def notifychangeproperty(transform_func, dbus_name,
 
1707
                             type_func=lambda x: x,
 
1708
                             variant_level=1,
 
1709
                             invalidate_only=False,
 
1710
                             _interface=_interface):
 
1711
        """ Modify a variable so that it's a property which announces
 
1712
        its changes to DBus.
 
1713
        
 
1714
        transform_fun: Function that takes a value and a variant_level
 
1715
                       and transforms it to a D-Bus type.
 
1716
        dbus_name: D-Bus name of the variable
 
1717
        type_func: Function that transform the value before sending it
 
1718
                   to the D-Bus.  Default: no transform
 
1719
        variant_level: D-Bus variant level.  Default: 1
 
1720
        """
 
1721
        attrname = "_{}".format(dbus_name)
 
1722
        
 
1723
        def setter(self, value):
 
1724
            if hasattr(self, "dbus_object_path"):
 
1725
                if (not hasattr(self, attrname) or
 
1726
                    type_func(getattr(self, attrname, None))
 
1727
                    != type_func(value)):
 
1728
                    if invalidate_only:
 
1729
                        self.PropertiesChanged(
 
1730
                            _interface, dbus.Dictionary(),
 
1731
                            dbus.Array((dbus_name, )))
 
1732
                    else:
 
1733
                        dbus_value = transform_func(
 
1734
                            type_func(value),
 
1735
                            variant_level = variant_level)
 
1736
                        self.PropertyChanged(dbus.String(dbus_name),
 
1737
                                             dbus_value)
 
1738
                        self.PropertiesChanged(
 
1739
                            _interface,
 
1740
                            dbus.Dictionary({ dbus.String(dbus_name):
 
1741
                                              dbus_value }),
 
1742
                            dbus.Array())
 
1743
            setattr(self, attrname, value)
 
1744
        
 
1745
        return property(lambda self: getattr(self, attrname), setter)
 
1746
    
 
1747
    expires = notifychangeproperty(datetime_to_dbus, "Expires")
 
1748
    approvals_pending = notifychangeproperty(dbus.Boolean,
 
1749
                                             "ApprovalPending",
 
1750
                                             type_func = bool)
 
1751
    enabled = notifychangeproperty(dbus.Boolean, "Enabled")
 
1752
    last_enabled = notifychangeproperty(datetime_to_dbus,
 
1753
                                        "LastEnabled")
 
1754
    checker = notifychangeproperty(
 
1755
        dbus.Boolean, "CheckerRunning",
 
1756
        type_func = lambda checker: checker is not None)
 
1757
    last_checked_ok = notifychangeproperty(datetime_to_dbus,
 
1758
                                           "LastCheckedOK")
 
1759
    last_checker_status = notifychangeproperty(dbus.Int16,
 
1760
                                               "LastCheckerStatus")
 
1761
    last_approval_request = notifychangeproperty(
 
1762
        datetime_to_dbus, "LastApprovalRequest")
 
1763
    approved_by_default = notifychangeproperty(dbus.Boolean,
 
1764
                                               "ApprovedByDefault")
 
1765
    approval_delay = notifychangeproperty(
 
1766
        dbus.UInt64, "ApprovalDelay",
 
1767
        type_func = lambda td: td.total_seconds() * 1000)
 
1768
    approval_duration = notifychangeproperty(
 
1769
        dbus.UInt64, "ApprovalDuration",
 
1770
        type_func = lambda td: td.total_seconds() * 1000)
 
1771
    host = notifychangeproperty(dbus.String, "Host")
 
1772
    timeout = notifychangeproperty(
 
1773
        dbus.UInt64, "Timeout",
 
1774
        type_func = lambda td: td.total_seconds() * 1000)
 
1775
    extended_timeout = notifychangeproperty(
 
1776
        dbus.UInt64, "ExtendedTimeout",
 
1777
        type_func = lambda td: td.total_seconds() * 1000)
 
1778
    interval = notifychangeproperty(
 
1779
        dbus.UInt64, "Interval",
 
1780
        type_func = lambda td: td.total_seconds() * 1000)
 
1781
    checker_command = notifychangeproperty(dbus.String, "Checker")
 
1782
    secret = notifychangeproperty(dbus.ByteArray, "Secret",
 
1783
                                  invalidate_only=True)
 
1784
    
 
1785
    del notifychangeproperty
 
1786
    
 
1787
    def __del__(self, *args, **kwargs):
 
1788
        try:
 
1789
            self.remove_from_connection()
 
1790
        except LookupError:
 
1791
            pass
 
1792
        if hasattr(DBusObjectWithProperties, "__del__"):
 
1793
            DBusObjectWithProperties.__del__(self, *args, **kwargs)
 
1794
        Client.__del__(self, *args, **kwargs)
 
1795
    
 
1796
    def checker_callback(self, source, condition,
 
1797
                         connection, command, *args, **kwargs):
 
1798
        ret = Client.checker_callback(self, source, condition,
 
1799
                                      connection, command, *args,
 
1800
                                      **kwargs)
 
1801
        exitstatus = self.last_checker_status
 
1802
        if exitstatus >= 0:
 
1803
            # Emit D-Bus signal
 
1804
            self.CheckerCompleted(dbus.Int16(exitstatus),
 
1805
                                  # This is specific to GNU libC
 
1806
                                  dbus.Int64(exitstatus << 8),
 
1807
                                  dbus.String(command))
455
1808
        else:
456
 
            return now < (self.last_checked_ok + self.timeout)
457
 
    
458
 
    ## D-Bus methods & signals
459
 
    _interface = u"org.mandos_system.Mandos.Client"
460
 
    
461
 
    # BumpTimeout - method
462
 
    BumpTimeout = dbus.service.method(_interface)(bump_timeout)
463
 
    BumpTimeout.__name__ = "BumpTimeout"
 
1809
            # Emit D-Bus signal
 
1810
            self.CheckerCompleted(dbus.Int16(-1),
 
1811
                                  dbus.Int64(
 
1812
                                      # This is specific to GNU libC
 
1813
                                      (exitstatus << 8)
 
1814
                                      | self.last_checker_signal),
 
1815
                                  dbus.String(command))
 
1816
        return ret
 
1817
    
 
1818
    def start_checker(self, *args, **kwargs):
 
1819
        old_checker_pid = getattr(self.checker, "pid", None)
 
1820
        r = Client.start_checker(self, *args, **kwargs)
 
1821
        # Only if new checker process was started
 
1822
        if (self.checker is not None
 
1823
            and old_checker_pid != self.checker.pid):
 
1824
            # Emit D-Bus signal
 
1825
            self.CheckerStarted(self.current_checker_command)
 
1826
        return r
 
1827
    
 
1828
    def _reset_approved(self):
 
1829
        self.approved = None
 
1830
        return False
 
1831
    
 
1832
    def approve(self, value=True):
 
1833
        self.approved = value
 
1834
        GLib.timeout_add(int(self.approval_duration.total_seconds()
 
1835
                             * 1000), self._reset_approved)
 
1836
        self.send_changedstate()
 
1837
    
 
1838
    ## D-Bus methods, signals & properties
 
1839
    
 
1840
    ## Interfaces
 
1841
    
 
1842
    ## Signals
464
1843
    
465
1844
    # CheckerCompleted - signal
466
 
    @dbus.service.signal(_interface, signature="bqs")
467
 
    def CheckerCompleted(self, success, condition, command):
 
1845
    @dbus.service.signal(_interface, signature="nxs")
 
1846
    def CheckerCompleted(self, exitcode, waitstatus, command):
468
1847
        "D-Bus signal"
469
1848
        pass
470
1849
    
474
1853
        "D-Bus signal"
475
1854
        pass
476
1855
    
477
 
    # GetAllProperties - method
478
 
    @dbus.service.method(_interface, out_signature="a{sv}")
479
 
    def GetAllProperties(self):
480
 
        "D-Bus method"
481
 
        return dbus.Dictionary({
482
 
                dbus.String("name"):
483
 
                    dbus.String(self.name, variant_level=1),
484
 
                dbus.String("fingerprint"):
485
 
                    dbus.String(self.fingerprint, variant_level=1),
486
 
                dbus.String("host"):
487
 
                    dbus.String(self.host, variant_level=1),
488
 
                dbus.String("created"):
489
 
                    _datetime_to_dbus(self.created, variant_level=1),
490
 
                dbus.String("last_enabled"):
491
 
                    (_datetime_to_dbus(self.last_enabled,
492
 
                                       variant_level=1)
493
 
                     if self.last_enabled is not None
494
 
                     else dbus.Boolean(False, variant_level=1)),
495
 
                dbus.String("enabled"):
496
 
                    dbus.Boolean(self.enabled, variant_level=1),
497
 
                dbus.String("last_checked_ok"):
498
 
                    (_datetime_to_dbus(self.last_checked_ok,
499
 
                                       variant_level=1)
500
 
                     if self.last_checked_ok is not None
501
 
                     else dbus.Boolean (False, variant_level=1)),
502
 
                dbus.String("timeout"):
503
 
                    dbus.UInt64(self._timeout_milliseconds,
504
 
                                variant_level=1),
505
 
                dbus.String("interval"):
506
 
                    dbus.UInt64(self._interval_milliseconds,
507
 
                                variant_level=1),
508
 
                dbus.String("checker"):
509
 
                    dbus.String(self.checker_command,
510
 
                                variant_level=1),
511
 
                dbus.String("checker_running"):
512
 
                    dbus.Boolean(self.checker is not None,
513
 
                                 variant_level=1),
514
 
                }, signature="sv")
515
 
    
516
 
    # IsStillValid - method
517
 
    IsStillValid = (dbus.service.method(_interface, out_signature="b")
518
 
                    (still_valid))
519
 
    IsStillValid.__name__ = "IsStillValid"
520
 
    
521
1856
    # PropertyChanged - signal
 
1857
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
522
1858
    @dbus.service.signal(_interface, signature="sv")
523
1859
    def PropertyChanged(self, property, value):
524
1860
        "D-Bus signal"
525
1861
        pass
526
1862
    
527
 
    # SetChecker - method
528
 
    @dbus.service.method(_interface, in_signature="s")
529
 
    def SetChecker(self, checker):
530
 
        "D-Bus setter method"
531
 
        self.checker_command = checker
532
 
    
533
 
    # SetHost - method
534
 
    @dbus.service.method(_interface, in_signature="s")
535
 
    def SetHost(self, host):
536
 
        "D-Bus setter method"
537
 
        self.host = host
538
 
    
539
 
    # SetInterval - method
540
 
    @dbus.service.method(_interface, in_signature="t")
541
 
    def SetInterval(self, milliseconds):
542
 
        self.interval = datetime.timdeelta(0, 0, 0, milliseconds)
543
 
    
544
 
    # SetSecret - method
545
 
    @dbus.service.method(_interface, in_signature="ay",
546
 
                         byte_arrays=True)
547
 
    def SetSecret(self, secret):
548
 
        "D-Bus setter method"
549
 
        self.secret = str(secret)
550
 
    
551
 
    # SetTimeout - method
552
 
    @dbus.service.method(_interface, in_signature="t")
553
 
    def SetTimeout(self, milliseconds):
554
 
        self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
 
1863
    # GotSecret - signal
 
1864
    @dbus.service.signal(_interface)
 
1865
    def GotSecret(self):
 
1866
        """D-Bus signal
 
1867
        Is sent after a successful transfer of secret from the Mandos
 
1868
        server to mandos-client
 
1869
        """
 
1870
        pass
 
1871
    
 
1872
    # Rejected - signal
 
1873
    @dbus.service.signal(_interface, signature="s")
 
1874
    def Rejected(self, reason):
 
1875
        "D-Bus signal"
 
1876
        pass
 
1877
    
 
1878
    # NeedApproval - signal
 
1879
    @dbus.service.signal(_interface, signature="tb")
 
1880
    def NeedApproval(self, timeout, default):
 
1881
        "D-Bus signal"
 
1882
        return self.need_approval()
 
1883
    
 
1884
    ## Methods
 
1885
    
 
1886
    # Approve - method
 
1887
    @dbus.service.method(_interface, in_signature="b")
 
1888
    def Approve(self, value):
 
1889
        self.approve(value)
 
1890
    
 
1891
    # CheckedOK - method
 
1892
    @dbus.service.method(_interface)
 
1893
    def CheckedOK(self):
 
1894
        self.checked_ok()
555
1895
    
556
1896
    # Enable - method
557
 
    Enable = dbus.service.method(_interface)(enable)
558
 
    Enable.__name__ = "Enable"
 
1897
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
 
1898
    @dbus.service.method(_interface)
 
1899
    def Enable(self):
 
1900
        "D-Bus method"
 
1901
        self.enable()
559
1902
    
560
1903
    # StartChecker - method
 
1904
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
561
1905
    @dbus.service.method(_interface)
562
1906
    def StartChecker(self):
563
1907
        "D-Bus method"
564
1908
        self.start_checker()
565
1909
    
566
1910
    # Disable - method
 
1911
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
567
1912
    @dbus.service.method(_interface)
568
1913
    def Disable(self):
569
1914
        "D-Bus method"
570
1915
        self.disable()
571
1916
    
572
1917
    # StopChecker - method
573
 
    StopChecker = dbus.service.method(_interface)(stop_checker)
574
 
    StopChecker.__name__ = "StopChecker"
 
1918
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
 
1919
    @dbus.service.method(_interface)
 
1920
    def StopChecker(self):
 
1921
        self.stop_checker()
 
1922
    
 
1923
    ## Properties
 
1924
    
 
1925
    # ApprovalPending - property
 
1926
    @dbus_service_property(_interface, signature="b", access="read")
 
1927
    def ApprovalPending_dbus_property(self):
 
1928
        return dbus.Boolean(bool(self.approvals_pending))
 
1929
    
 
1930
    # ApprovedByDefault - property
 
1931
    @dbus_service_property(_interface,
 
1932
                           signature="b",
 
1933
                           access="readwrite")
 
1934
    def ApprovedByDefault_dbus_property(self, value=None):
 
1935
        if value is None:       # get
 
1936
            return dbus.Boolean(self.approved_by_default)
 
1937
        self.approved_by_default = bool(value)
 
1938
    
 
1939
    # ApprovalDelay - property
 
1940
    @dbus_service_property(_interface,
 
1941
                           signature="t",
 
1942
                           access="readwrite")
 
1943
    def ApprovalDelay_dbus_property(self, value=None):
 
1944
        if value is None:       # get
 
1945
            return dbus.UInt64(self.approval_delay.total_seconds()
 
1946
                               * 1000)
 
1947
        self.approval_delay = datetime.timedelta(0, 0, 0, value)
 
1948
    
 
1949
    # ApprovalDuration - property
 
1950
    @dbus_service_property(_interface,
 
1951
                           signature="t",
 
1952
                           access="readwrite")
 
1953
    def ApprovalDuration_dbus_property(self, value=None):
 
1954
        if value is None:       # get
 
1955
            return dbus.UInt64(self.approval_duration.total_seconds()
 
1956
                               * 1000)
 
1957
        self.approval_duration = datetime.timedelta(0, 0, 0, value)
 
1958
    
 
1959
    # Name - property
 
1960
    @dbus_annotations(
 
1961
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
 
1962
    @dbus_service_property(_interface, signature="s", access="read")
 
1963
    def Name_dbus_property(self):
 
1964
        return dbus.String(self.name)
 
1965
    
 
1966
    # Fingerprint - property
 
1967
    @dbus_annotations(
 
1968
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
 
1969
    @dbus_service_property(_interface, signature="s", access="read")
 
1970
    def Fingerprint_dbus_property(self):
 
1971
        return dbus.String(self.fingerprint)
 
1972
    
 
1973
    # Host - property
 
1974
    @dbus_service_property(_interface,
 
1975
                           signature="s",
 
1976
                           access="readwrite")
 
1977
    def Host_dbus_property(self, value=None):
 
1978
        if value is None:       # get
 
1979
            return dbus.String(self.host)
 
1980
        self.host = str(value)
 
1981
    
 
1982
    # Created - property
 
1983
    @dbus_annotations(
 
1984
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
 
1985
    @dbus_service_property(_interface, signature="s", access="read")
 
1986
    def Created_dbus_property(self):
 
1987
        return datetime_to_dbus(self.created)
 
1988
    
 
1989
    # LastEnabled - property
 
1990
    @dbus_service_property(_interface, signature="s", access="read")
 
1991
    def LastEnabled_dbus_property(self):
 
1992
        return datetime_to_dbus(self.last_enabled)
 
1993
    
 
1994
    # Enabled - property
 
1995
    @dbus_service_property(_interface,
 
1996
                           signature="b",
 
1997
                           access="readwrite")
 
1998
    def Enabled_dbus_property(self, value=None):
 
1999
        if value is None:       # get
 
2000
            return dbus.Boolean(self.enabled)
 
2001
        if value:
 
2002
            self.enable()
 
2003
        else:
 
2004
            self.disable()
 
2005
    
 
2006
    # LastCheckedOK - property
 
2007
    @dbus_service_property(_interface,
 
2008
                           signature="s",
 
2009
                           access="readwrite")
 
2010
    def LastCheckedOK_dbus_property(self, value=None):
 
2011
        if value is not None:
 
2012
            self.checked_ok()
 
2013
            return
 
2014
        return datetime_to_dbus(self.last_checked_ok)
 
2015
    
 
2016
    # LastCheckerStatus - property
 
2017
    @dbus_service_property(_interface, signature="n", access="read")
 
2018
    def LastCheckerStatus_dbus_property(self):
 
2019
        return dbus.Int16(self.last_checker_status)
 
2020
    
 
2021
    # Expires - property
 
2022
    @dbus_service_property(_interface, signature="s", access="read")
 
2023
    def Expires_dbus_property(self):
 
2024
        return datetime_to_dbus(self.expires)
 
2025
    
 
2026
    # LastApprovalRequest - property
 
2027
    @dbus_service_property(_interface, signature="s", access="read")
 
2028
    def LastApprovalRequest_dbus_property(self):
 
2029
        return datetime_to_dbus(self.last_approval_request)
 
2030
    
 
2031
    # Timeout - property
 
2032
    @dbus_service_property(_interface,
 
2033
                           signature="t",
 
2034
                           access="readwrite")
 
2035
    def Timeout_dbus_property(self, value=None):
 
2036
        if value is None:       # get
 
2037
            return dbus.UInt64(self.timeout.total_seconds() * 1000)
 
2038
        old_timeout = self.timeout
 
2039
        self.timeout = datetime.timedelta(0, 0, 0, value)
 
2040
        # Reschedule disabling
 
2041
        if self.enabled:
 
2042
            now = datetime.datetime.utcnow()
 
2043
            self.expires += self.timeout - old_timeout
 
2044
            if self.expires <= now:
 
2045
                # The timeout has passed
 
2046
                self.disable()
 
2047
            else:
 
2048
                if (getattr(self, "disable_initiator_tag", None)
 
2049
                    is None):
 
2050
                    return
 
2051
                GLib.source_remove(self.disable_initiator_tag)
 
2052
                self.disable_initiator_tag = GLib.timeout_add(
 
2053
                    int((self.expires - now).total_seconds() * 1000),
 
2054
                    self.disable)
 
2055
    
 
2056
    # ExtendedTimeout - property
 
2057
    @dbus_service_property(_interface,
 
2058
                           signature="t",
 
2059
                           access="readwrite")
 
2060
    def ExtendedTimeout_dbus_property(self, value=None):
 
2061
        if value is None:       # get
 
2062
            return dbus.UInt64(self.extended_timeout.total_seconds()
 
2063
                               * 1000)
 
2064
        self.extended_timeout = datetime.timedelta(0, 0, 0, value)
 
2065
    
 
2066
    # Interval - property
 
2067
    @dbus_service_property(_interface,
 
2068
                           signature="t",
 
2069
                           access="readwrite")
 
2070
    def Interval_dbus_property(self, value=None):
 
2071
        if value is None:       # get
 
2072
            return dbus.UInt64(self.interval.total_seconds() * 1000)
 
2073
        self.interval = datetime.timedelta(0, 0, 0, value)
 
2074
        if getattr(self, "checker_initiator_tag", None) is None:
 
2075
            return
 
2076
        if self.enabled:
 
2077
            # Reschedule checker run
 
2078
            GLib.source_remove(self.checker_initiator_tag)
 
2079
            self.checker_initiator_tag = GLib.timeout_add(
 
2080
                value, self.start_checker)
 
2081
            self.start_checker() # Start one now, too
 
2082
    
 
2083
    # Checker - property
 
2084
    @dbus_service_property(_interface,
 
2085
                           signature="s",
 
2086
                           access="readwrite")
 
2087
    def Checker_dbus_property(self, value=None):
 
2088
        if value is None:       # get
 
2089
            return dbus.String(self.checker_command)
 
2090
        self.checker_command = str(value)
 
2091
    
 
2092
    # CheckerRunning - property
 
2093
    @dbus_service_property(_interface,
 
2094
                           signature="b",
 
2095
                           access="readwrite")
 
2096
    def CheckerRunning_dbus_property(self, value=None):
 
2097
        if value is None:       # get
 
2098
            return dbus.Boolean(self.checker is not None)
 
2099
        if value:
 
2100
            self.start_checker()
 
2101
        else:
 
2102
            self.stop_checker()
 
2103
    
 
2104
    # ObjectPath - property
 
2105
    @dbus_annotations(
 
2106
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const",
 
2107
         "org.freedesktop.DBus.Deprecated": "true"})
 
2108
    @dbus_service_property(_interface, signature="o", access="read")
 
2109
    def ObjectPath_dbus_property(self):
 
2110
        return self.dbus_object_path # is already a dbus.ObjectPath
 
2111
    
 
2112
    # Secret = property
 
2113
    @dbus_annotations(
 
2114
        {"org.freedesktop.DBus.Property.EmitsChangedSignal":
 
2115
         "invalidates"})
 
2116
    @dbus_service_property(_interface,
 
2117
                           signature="ay",
 
2118
                           access="write",
 
2119
                           byte_arrays=True)
 
2120
    def Secret_dbus_property(self, value):
 
2121
        self.secret = bytes(value)
575
2122
    
576
2123
    del _interface
577
2124
 
578
2125
 
579
 
def peer_certificate(session):
580
 
    "Return the peer's OpenPGP certificate as a bytestring"
581
 
    # If not an OpenPGP certificate...
582
 
    if (gnutls.library.functions
583
 
        .gnutls_certificate_type_get(session._c_object)
584
 
        != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
585
 
        # ...do the normal thing
586
 
        return session.peer_certificate
587
 
    list_size = ctypes.c_uint()
588
 
    cert_list = (gnutls.library.functions
589
 
                 .gnutls_certificate_get_peers
590
 
                 (session._c_object, ctypes.byref(list_size)))
591
 
    if list_size.value == 0:
592
 
        return None
593
 
    cert = cert_list[0]
594
 
    return ctypes.string_at(cert.data, cert.size)
595
 
 
596
 
 
597
 
def fingerprint(openpgp):
598
 
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
599
 
    # New GnuTLS "datum" with the OpenPGP public key
600
 
    datum = (gnutls.library.types
601
 
             .gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
602
 
                                         ctypes.POINTER
603
 
                                         (ctypes.c_ubyte)),
604
 
                             ctypes.c_uint(len(openpgp))))
605
 
    # New empty GnuTLS certificate
606
 
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
607
 
    (gnutls.library.functions
608
 
     .gnutls_openpgp_crt_init(ctypes.byref(crt)))
609
 
    # Import the OpenPGP public key into the certificate
610
 
    (gnutls.library.functions
611
 
     .gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
612
 
                                gnutls.library.constants
613
 
                                .GNUTLS_OPENPGP_FMT_RAW))
614
 
    # Verify the self signature in the key
615
 
    crtverify = ctypes.c_uint()
616
 
    (gnutls.library.functions
617
 
     .gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
618
 
    if crtverify.value != 0:
619
 
        gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
620
 
        raise gnutls.errors.CertificateSecurityError("Verify failed")
621
 
    # New buffer for the fingerprint
622
 
    buf = ctypes.create_string_buffer(20)
623
 
    buf_len = ctypes.c_size_t()
624
 
    # Get the fingerprint from the certificate into the buffer
625
 
    (gnutls.library.functions
626
 
     .gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
627
 
                                         ctypes.byref(buf_len)))
628
 
    # Deinit the certificate
629
 
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
630
 
    # Convert the buffer to a Python bytestring
631
 
    fpr = ctypes.string_at(buf, buf_len.value)
632
 
    # Convert the bytestring to hexadecimal notation
633
 
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
634
 
    return hex_fpr
635
 
 
636
 
 
637
 
class TCP_handler(SocketServer.BaseRequestHandler, object):
638
 
    """A TCP request handler class.
639
 
    Instantiated by IPv6_TCPServer for each request to handle it.
 
2126
class ProxyClient(object):
 
2127
    def __init__(self, child_pipe, fpr, address):
 
2128
        self._pipe = child_pipe
 
2129
        self._pipe.send(('init', fpr, address))
 
2130
        if not self._pipe.recv():
 
2131
            raise KeyError(fpr)
 
2132
    
 
2133
    def __getattribute__(self, name):
 
2134
        if name == '_pipe':
 
2135
            return super(ProxyClient, self).__getattribute__(name)
 
2136
        self._pipe.send(('getattr', name))
 
2137
        data = self._pipe.recv()
 
2138
        if data[0] == 'data':
 
2139
            return data[1]
 
2140
        if data[0] == 'function':
 
2141
            
 
2142
            def func(*args, **kwargs):
 
2143
                self._pipe.send(('funcall', name, args, kwargs))
 
2144
                return self._pipe.recv()[1]
 
2145
            
 
2146
            return func
 
2147
    
 
2148
    def __setattr__(self, name, value):
 
2149
        if name == '_pipe':
 
2150
            return super(ProxyClient, self).__setattr__(name, value)
 
2151
        self._pipe.send(('setattr', name, value))
 
2152
 
 
2153
 
 
2154
class ClientHandler(socketserver.BaseRequestHandler, object):
 
2155
    """A class to handle client connections.
 
2156
    
 
2157
    Instantiated once for each connection to handle it.
640
2158
    Note: This will run in its own forked process."""
641
2159
    
642
2160
    def handle(self):
643
 
        logger.info(u"TCP connection from: %s",
644
 
                    unicode(self.client_address))
645
 
        session = (gnutls.connection
646
 
                   .ClientSession(self.request,
647
 
                                  gnutls.connection
648
 
                                  .X509Credentials()))
649
 
        
650
 
        line = self.request.makefile().readline()
651
 
        logger.debug(u"Protocol version: %r", line)
652
 
        try:
653
 
            if int(line.strip().split()[0]) > 1:
654
 
                raise RuntimeError
655
 
        except (ValueError, IndexError, RuntimeError), error:
656
 
            logger.error(u"Unknown protocol version: %s", error)
657
 
            return
658
 
        
659
 
        # Note: gnutls.connection.X509Credentials is really a generic
660
 
        # GnuTLS certificate credentials object so long as no X.509
661
 
        # keys are added to it.  Therefore, we can use it here despite
662
 
        # using OpenPGP certificates.
663
 
        
664
 
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
665
 
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
666
 
        #                "+DHE-DSS"))
667
 
        # Use a fallback default, since this MUST be set.
668
 
        priority = self.server.settings.get("priority", "NORMAL")
669
 
        (gnutls.library.functions
670
 
         .gnutls_priority_set_direct(session._c_object,
671
 
                                     priority, None))
672
 
        
673
 
        try:
674
 
            session.handshake()
675
 
        except gnutls.errors.GNUTLSError, error:
676
 
            logger.warning(u"Handshake failed: %s", error)
677
 
            # Do not run session.bye() here: the session is not
678
 
            # established.  Just abandon the request.
679
 
            return
680
 
        try:
681
 
            fpr = fingerprint(peer_certificate(session))
682
 
        except (TypeError, gnutls.errors.GNUTLSError), error:
683
 
            logger.warning(u"Bad certificate: %s", error)
684
 
            session.bye()
685
 
            return
686
 
        logger.debug(u"Fingerprint: %s", fpr)
687
 
        for c in self.server.clients:
688
 
            if c.fingerprint == fpr:
689
 
                client = c
690
 
                break
691
 
        else:
692
 
            logger.warning(u"Client not found for fingerprint: %s",
693
 
                           fpr)
694
 
            session.bye()
695
 
            return
696
 
        # Have to check if client.still_valid(), since it is possible
697
 
        # that the client timed out while establishing the GnuTLS
698
 
        # session.
699
 
        if not client.still_valid():
700
 
            logger.warning(u"Client %(name)s is invalid",
701
 
                           vars(client))
702
 
            session.bye()
703
 
            return
704
 
        ## This won't work here, since we're in a fork.
705
 
        # client.bump_timeout()
706
 
        sent_size = 0
707
 
        while sent_size < len(client.secret):
708
 
            sent = session.send(client.secret[sent_size:])
709
 
            logger.debug(u"Sent: %d, remaining: %d",
710
 
                         sent, len(client.secret)
711
 
                         - (sent_size + sent))
712
 
            sent_size += sent
713
 
        session.bye()
714
 
 
715
 
 
716
 
class IPv6_TCPServer(SocketServer.ForkingMixIn,
717
 
                     SocketServer.TCPServer, object):
718
 
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
 
2161
        with contextlib.closing(self.server.child_pipe) as child_pipe:
 
2162
            logger.info("TCP connection from: %s",
 
2163
                        str(self.client_address))
 
2164
            logger.debug("Pipe FD: %d",
 
2165
                         self.server.child_pipe.fileno())
 
2166
            
 
2167
            session = gnutls.ClientSession(self.request)
 
2168
            
 
2169
            #priority = ':'.join(("NONE", "+VERS-TLS1.1",
 
2170
            #                      "+AES-256-CBC", "+SHA1",
 
2171
            #                      "+COMP-NULL", "+CTYPE-OPENPGP",
 
2172
            #                      "+DHE-DSS"))
 
2173
            # Use a fallback default, since this MUST be set.
 
2174
            priority = self.server.gnutls_priority
 
2175
            if priority is None:
 
2176
                priority = "NORMAL"
 
2177
            gnutls.priority_set_direct(session._c_object, priority,
 
2178
                                       None)
 
2179
            
 
2180
            # Start communication using the Mandos protocol
 
2181
            # Get protocol number
 
2182
            line = self.request.makefile().readline()
 
2183
            logger.debug("Protocol version: %r", line)
 
2184
            try:
 
2185
                if int(line.strip().split()[0]) > 1:
 
2186
                    raise RuntimeError(line)
 
2187
            except (ValueError, IndexError, RuntimeError) as error:
 
2188
                logger.error("Unknown protocol version: %s", error)
 
2189
                return
 
2190
            
 
2191
            # Start GnuTLS connection
 
2192
            try:
 
2193
                session.handshake()
 
2194
            except gnutls.Error as error:
 
2195
                logger.warning("Handshake failed: %s", error)
 
2196
                # Do not run session.bye() here: the session is not
 
2197
                # established.  Just abandon the request.
 
2198
                return
 
2199
            logger.debug("Handshake succeeded")
 
2200
            
 
2201
            approval_required = False
 
2202
            try:
 
2203
                try:
 
2204
                    fpr = self.fingerprint(
 
2205
                        self.peer_certificate(session))
 
2206
                except (TypeError, gnutls.Error) as error:
 
2207
                    logger.warning("Bad certificate: %s", error)
 
2208
                    return
 
2209
                logger.debug("Fingerprint: %s", fpr)
 
2210
                
 
2211
                try:
 
2212
                    client = ProxyClient(child_pipe, fpr,
 
2213
                                         self.client_address)
 
2214
                except KeyError:
 
2215
                    return
 
2216
                
 
2217
                if client.approval_delay:
 
2218
                    delay = client.approval_delay
 
2219
                    client.approvals_pending += 1
 
2220
                    approval_required = True
 
2221
                
 
2222
                while True:
 
2223
                    if not client.enabled:
 
2224
                        logger.info("Client %s is disabled",
 
2225
                                    client.name)
 
2226
                        if self.server.use_dbus:
 
2227
                            # Emit D-Bus signal
 
2228
                            client.Rejected("Disabled")
 
2229
                        return
 
2230
                    
 
2231
                    if client.approved or not client.approval_delay:
 
2232
                        #We are approved or approval is disabled
 
2233
                        break
 
2234
                    elif client.approved is None:
 
2235
                        logger.info("Client %s needs approval",
 
2236
                                    client.name)
 
2237
                        if self.server.use_dbus:
 
2238
                            # Emit D-Bus signal
 
2239
                            client.NeedApproval(
 
2240
                                client.approval_delay.total_seconds()
 
2241
                                * 1000, client.approved_by_default)
 
2242
                    else:
 
2243
                        logger.warning("Client %s was not approved",
 
2244
                                       client.name)
 
2245
                        if self.server.use_dbus:
 
2246
                            # Emit D-Bus signal
 
2247
                            client.Rejected("Denied")
 
2248
                        return
 
2249
                    
 
2250
                    #wait until timeout or approved
 
2251
                    time = datetime.datetime.now()
 
2252
                    client.changedstate.acquire()
 
2253
                    client.changedstate.wait(delay.total_seconds())
 
2254
                    client.changedstate.release()
 
2255
                    time2 = datetime.datetime.now()
 
2256
                    if (time2 - time) >= delay:
 
2257
                        if not client.approved_by_default:
 
2258
                            logger.warning("Client %s timed out while"
 
2259
                                           " waiting for approval",
 
2260
                                           client.name)
 
2261
                            if self.server.use_dbus:
 
2262
                                # Emit D-Bus signal
 
2263
                                client.Rejected("Approval timed out")
 
2264
                            return
 
2265
                        else:
 
2266
                            break
 
2267
                    else:
 
2268
                        delay -= time2 - time
 
2269
                
 
2270
                try:
 
2271
                    session.send(client.secret)
 
2272
                except gnutls.Error as error:
 
2273
                    logger.warning("gnutls send failed",
 
2274
                                   exc_info = error)
 
2275
                    return
 
2276
                
 
2277
                logger.info("Sending secret to %s", client.name)
 
2278
                # bump the timeout using extended_timeout
 
2279
                client.bump_timeout(client.extended_timeout)
 
2280
                if self.server.use_dbus:
 
2281
                    # Emit D-Bus signal
 
2282
                    client.GotSecret()
 
2283
            
 
2284
            finally:
 
2285
                if approval_required:
 
2286
                    client.approvals_pending -= 1
 
2287
                try:
 
2288
                    session.bye()
 
2289
                except gnutls.Error as error:
 
2290
                    logger.warning("GnuTLS bye failed",
 
2291
                                   exc_info=error)
 
2292
    
 
2293
    @staticmethod
 
2294
    def peer_certificate(session):
 
2295
        "Return the peer's OpenPGP certificate as a bytestring"
 
2296
        # If not an OpenPGP certificate...
 
2297
        if (gnutls.certificate_type_get(session._c_object)
 
2298
            != gnutls.CRT_OPENPGP):
 
2299
            # ...return invalid data
 
2300
            return b""
 
2301
        list_size = ctypes.c_uint(1)
 
2302
        cert_list = (gnutls.certificate_get_peers
 
2303
                     (session._c_object, ctypes.byref(list_size)))
 
2304
        if not bool(cert_list) and list_size.value != 0:
 
2305
            raise gnutls.Error("error getting peer certificate")
 
2306
        if list_size.value == 0:
 
2307
            return None
 
2308
        cert = cert_list[0]
 
2309
        return ctypes.string_at(cert.data, cert.size)
 
2310
    
 
2311
    @staticmethod
 
2312
    def fingerprint(openpgp):
 
2313
        "Convert an OpenPGP bytestring to a hexdigit fingerprint"
 
2314
        # New GnuTLS "datum" with the OpenPGP public key
 
2315
        datum = gnutls.datum_t(
 
2316
            ctypes.cast(ctypes.c_char_p(openpgp),
 
2317
                        ctypes.POINTER(ctypes.c_ubyte)),
 
2318
            ctypes.c_uint(len(openpgp)))
 
2319
        # New empty GnuTLS certificate
 
2320
        crt = gnutls.openpgp_crt_t()
 
2321
        gnutls.openpgp_crt_init(ctypes.byref(crt))
 
2322
        # Import the OpenPGP public key into the certificate
 
2323
        gnutls.openpgp_crt_import(crt, ctypes.byref(datum),
 
2324
                                  gnutls.OPENPGP_FMT_RAW)
 
2325
        # Verify the self signature in the key
 
2326
        crtverify = ctypes.c_uint()
 
2327
        gnutls.openpgp_crt_verify_self(crt, 0,
 
2328
                                       ctypes.byref(crtverify))
 
2329
        if crtverify.value != 0:
 
2330
            gnutls.openpgp_crt_deinit(crt)
 
2331
            raise gnutls.CertificateSecurityError("Verify failed")
 
2332
        # New buffer for the fingerprint
 
2333
        buf = ctypes.create_string_buffer(20)
 
2334
        buf_len = ctypes.c_size_t()
 
2335
        # Get the fingerprint from the certificate into the buffer
 
2336
        gnutls.openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
 
2337
                                           ctypes.byref(buf_len))
 
2338
        # Deinit the certificate
 
2339
        gnutls.openpgp_crt_deinit(crt)
 
2340
        # Convert the buffer to a Python bytestring
 
2341
        fpr = ctypes.string_at(buf, buf_len.value)
 
2342
        # Convert the bytestring to hexadecimal notation
 
2343
        hex_fpr = binascii.hexlify(fpr).upper()
 
2344
        return hex_fpr
 
2345
 
 
2346
 
 
2347
class MultiprocessingMixIn(object):
 
2348
    """Like socketserver.ThreadingMixIn, but with multiprocessing"""
 
2349
    
 
2350
    def sub_process_main(self, request, address):
 
2351
        try:
 
2352
            self.finish_request(request, address)
 
2353
        except Exception:
 
2354
            self.handle_error(request, address)
 
2355
        self.close_request(request)
 
2356
    
 
2357
    def process_request(self, request, address):
 
2358
        """Start a new process to process the request."""
 
2359
        proc = multiprocessing.Process(target = self.sub_process_main,
 
2360
                                       args = (request, address))
 
2361
        proc.start()
 
2362
        return proc
 
2363
 
 
2364
 
 
2365
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
 
2366
    """ adds a pipe to the MixIn """
 
2367
    
 
2368
    def process_request(self, request, client_address):
 
2369
        """Overrides and wraps the original process_request().
 
2370
        
 
2371
        This function creates a new pipe in self.pipe
 
2372
        """
 
2373
        parent_pipe, self.child_pipe = multiprocessing.Pipe()
 
2374
        
 
2375
        proc = MultiprocessingMixIn.process_request(self, request,
 
2376
                                                    client_address)
 
2377
        self.child_pipe.close()
 
2378
        self.add_pipe(parent_pipe, proc)
 
2379
    
 
2380
    def add_pipe(self, parent_pipe, proc):
 
2381
        """Dummy function; override as necessary"""
 
2382
        raise NotImplementedError()
 
2383
 
 
2384
 
 
2385
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
 
2386
                     socketserver.TCPServer, object):
 
2387
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
 
2388
    
719
2389
    Attributes:
720
 
        settings:       Server settings
721
 
        clients:        Set() of Client objects
722
2390
        enabled:        Boolean; whether this server is activated yet
 
2391
        interface:      None or a network interface name (string)
 
2392
        use_ipv6:       Boolean; to use IPv6 or not
723
2393
    """
724
 
    address_family = socket.AF_INET6
725
 
    def __init__(self, *args, **kwargs):
726
 
        if "settings" in kwargs:
727
 
            self.settings = kwargs["settings"]
728
 
            del kwargs["settings"]
729
 
        if "clients" in kwargs:
730
 
            self.clients = kwargs["clients"]
731
 
            del kwargs["clients"]
732
 
        self.enabled = False
733
 
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
 
2394
    
 
2395
    def __init__(self, server_address, RequestHandlerClass,
 
2396
                 interface=None,
 
2397
                 use_ipv6=True,
 
2398
                 socketfd=None):
 
2399
        """If socketfd is set, use that file descriptor instead of
 
2400
        creating a new one with socket.socket().
 
2401
        """
 
2402
        self.interface = interface
 
2403
        if use_ipv6:
 
2404
            self.address_family = socket.AF_INET6
 
2405
        if socketfd is not None:
 
2406
            # Save the file descriptor
 
2407
            self.socketfd = socketfd
 
2408
            # Save the original socket.socket() function
 
2409
            self.socket_socket = socket.socket
 
2410
            # To implement --socket, we monkey patch socket.socket.
 
2411
            # 
 
2412
            # (When socketserver.TCPServer is a new-style class, we
 
2413
            # could make self.socket into a property instead of monkey
 
2414
            # patching socket.socket.)
 
2415
            # 
 
2416
            # Create a one-time-only replacement for socket.socket()
 
2417
            @functools.wraps(socket.socket)
 
2418
            def socket_wrapper(*args, **kwargs):
 
2419
                # Restore original function so subsequent calls are
 
2420
                # not affected.
 
2421
                socket.socket = self.socket_socket
 
2422
                del self.socket_socket
 
2423
                # This time only, return a new socket object from the
 
2424
                # saved file descriptor.
 
2425
                return socket.fromfd(self.socketfd, *args, **kwargs)
 
2426
            # Replace socket.socket() function with wrapper
 
2427
            socket.socket = socket_wrapper
 
2428
        # The socketserver.TCPServer.__init__ will call
 
2429
        # socket.socket(), which might be our replacement,
 
2430
        # socket_wrapper(), if socketfd was set.
 
2431
        socketserver.TCPServer.__init__(self, server_address,
 
2432
                                        RequestHandlerClass)
 
2433
    
734
2434
    def server_bind(self):
735
2435
        """This overrides the normal server_bind() function
736
2436
        to bind to an interface if one was specified, and also NOT to
737
2437
        bind to an address or port if they were not specified."""
738
 
        if self.settings["interface"]:
739
 
            # 25 is from /usr/include/asm-i486/socket.h
740
 
            SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
741
 
            try:
742
 
                self.socket.setsockopt(socket.SOL_SOCKET,
743
 
                                       SO_BINDTODEVICE,
744
 
                                       self.settings["interface"])
745
 
            except socket.error, error:
746
 
                if error[0] == errno.EPERM:
747
 
                    logger.error(u"No permission to"
748
 
                                 u" bind to interface %s",
749
 
                                 self.settings["interface"])
750
 
                else:
751
 
                    raise error
 
2438
        if self.interface is not None:
 
2439
            if SO_BINDTODEVICE is None:
 
2440
                logger.error("SO_BINDTODEVICE does not exist;"
 
2441
                             " cannot bind to interface %s",
 
2442
                             self.interface)
 
2443
            else:
 
2444
                try:
 
2445
                    self.socket.setsockopt(
 
2446
                        socket.SOL_SOCKET, SO_BINDTODEVICE,
 
2447
                        (self.interface + "\0").encode("utf-8"))
 
2448
                except socket.error as error:
 
2449
                    if error.errno == errno.EPERM:
 
2450
                        logger.error("No permission to bind to"
 
2451
                                     " interface %s", self.interface)
 
2452
                    elif error.errno == errno.ENOPROTOOPT:
 
2453
                        logger.error("SO_BINDTODEVICE not available;"
 
2454
                                     " cannot bind to interface %s",
 
2455
                                     self.interface)
 
2456
                    elif error.errno == errno.ENODEV:
 
2457
                        logger.error("Interface %s does not exist,"
 
2458
                                     " cannot bind", self.interface)
 
2459
                    else:
 
2460
                        raise
752
2461
        # Only bind(2) the socket if we really need to.
753
2462
        if self.server_address[0] or self.server_address[1]:
754
2463
            if not self.server_address[0]:
755
 
                in6addr_any = "::"
756
 
                self.server_address = (in6addr_any,
 
2464
                if self.address_family == socket.AF_INET6:
 
2465
                    any_address = "::" # in6addr_any
 
2466
                else:
 
2467
                    any_address = "0.0.0.0" # INADDR_ANY
 
2468
                self.server_address = (any_address,
757
2469
                                       self.server_address[1])
758
2470
            elif not self.server_address[1]:
759
 
                self.server_address = (self.server_address[0],
760
 
                                       0)
761
 
#                 if self.settings["interface"]:
 
2471
                self.server_address = (self.server_address[0], 0)
 
2472
#                 if self.interface:
762
2473
#                     self.server_address = (self.server_address[0],
763
2474
#                                            0, # port
764
2475
#                                            0, # flowinfo
765
2476
#                                            if_nametoindex
766
 
#                                            (self.settings
767
 
#                                             ["interface"]))
768
 
            return super(IPv6_TCPServer, self).server_bind()
 
2477
#                                            (self.interface))
 
2478
            return socketserver.TCPServer.server_bind(self)
 
2479
 
 
2480
 
 
2481
class MandosServer(IPv6_TCPServer):
 
2482
    """Mandos server.
 
2483
    
 
2484
    Attributes:
 
2485
        clients:        set of Client objects
 
2486
        gnutls_priority GnuTLS priority string
 
2487
        use_dbus:       Boolean; to emit D-Bus signals or not
 
2488
    
 
2489
    Assumes a GLib.MainLoop event loop.
 
2490
    """
 
2491
    
 
2492
    def __init__(self, server_address, RequestHandlerClass,
 
2493
                 interface=None,
 
2494
                 use_ipv6=True,
 
2495
                 clients=None,
 
2496
                 gnutls_priority=None,
 
2497
                 use_dbus=True,
 
2498
                 socketfd=None):
 
2499
        self.enabled = False
 
2500
        self.clients = clients
 
2501
        if self.clients is None:
 
2502
            self.clients = {}
 
2503
        self.use_dbus = use_dbus
 
2504
        self.gnutls_priority = gnutls_priority
 
2505
        IPv6_TCPServer.__init__(self, server_address,
 
2506
                                RequestHandlerClass,
 
2507
                                interface = interface,
 
2508
                                use_ipv6 = use_ipv6,
 
2509
                                socketfd = socketfd)
 
2510
    
769
2511
    def server_activate(self):
770
2512
        if self.enabled:
771
 
            return super(IPv6_TCPServer, self).server_activate()
 
2513
            return socketserver.TCPServer.server_activate(self)
 
2514
    
772
2515
    def enable(self):
773
2516
        self.enabled = True
 
2517
    
 
2518
    def add_pipe(self, parent_pipe, proc):
 
2519
        # Call "handle_ipc" for both data and EOF events
 
2520
        GLib.io_add_watch(
 
2521
            parent_pipe.fileno(),
 
2522
            GLib.IO_IN | GLib.IO_HUP,
 
2523
            functools.partial(self.handle_ipc,
 
2524
                              parent_pipe = parent_pipe,
 
2525
                              proc = proc))
 
2526
    
 
2527
    def handle_ipc(self, source, condition,
 
2528
                   parent_pipe=None,
 
2529
                   proc = None,
 
2530
                   client_object=None):
 
2531
        # error, or the other end of multiprocessing.Pipe has closed
 
2532
        if condition & (GLib.IO_ERR | GLib.IO_HUP):
 
2533
            # Wait for other process to exit
 
2534
            proc.join()
 
2535
            return False
 
2536
        
 
2537
        # Read a request from the child
 
2538
        request = parent_pipe.recv()
 
2539
        command = request[0]
 
2540
        
 
2541
        if command == 'init':
 
2542
            fpr = request[1]
 
2543
            address = request[2]
 
2544
            
 
2545
            for c in self.clients.values():
 
2546
                if c.fingerprint == fpr:
 
2547
                    client = c
 
2548
                    break
 
2549
            else:
 
2550
                logger.info("Client not found for fingerprint: %s, ad"
 
2551
                            "dress: %s", fpr, address)
 
2552
                if self.use_dbus:
 
2553
                    # Emit D-Bus signal
 
2554
                    mandos_dbus_service.ClientNotFound(fpr,
 
2555
                                                       address[0])
 
2556
                parent_pipe.send(False)
 
2557
                return False
 
2558
            
 
2559
            GLib.io_add_watch(
 
2560
                parent_pipe.fileno(),
 
2561
                GLib.IO_IN | GLib.IO_HUP,
 
2562
                functools.partial(self.handle_ipc,
 
2563
                                  parent_pipe = parent_pipe,
 
2564
                                  proc = proc,
 
2565
                                  client_object = client))
 
2566
            parent_pipe.send(True)
 
2567
            # remove the old hook in favor of the new above hook on
 
2568
            # same fileno
 
2569
            return False
 
2570
        if command == 'funcall':
 
2571
            funcname = request[1]
 
2572
            args = request[2]
 
2573
            kwargs = request[3]
 
2574
            
 
2575
            parent_pipe.send(('data', getattr(client_object,
 
2576
                                              funcname)(*args,
 
2577
                                                        **kwargs)))
 
2578
        
 
2579
        if command == 'getattr':
 
2580
            attrname = request[1]
 
2581
            if isinstance(client_object.__getattribute__(attrname),
 
2582
                          collections.Callable):
 
2583
                parent_pipe.send(('function', ))
 
2584
            else:
 
2585
                parent_pipe.send((
 
2586
                    'data', client_object.__getattribute__(attrname)))
 
2587
        
 
2588
        if command == 'setattr':
 
2589
            attrname = request[1]
 
2590
            value = request[2]
 
2591
            setattr(client_object, attrname, value)
 
2592
        
 
2593
        return True
 
2594
 
 
2595
 
 
2596
def rfc3339_duration_to_delta(duration):
 
2597
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
 
2598
    
 
2599
    >>> rfc3339_duration_to_delta("P7D")
 
2600
    datetime.timedelta(7)
 
2601
    >>> rfc3339_duration_to_delta("PT60S")
 
2602
    datetime.timedelta(0, 60)
 
2603
    >>> rfc3339_duration_to_delta("PT60M")
 
2604
    datetime.timedelta(0, 3600)
 
2605
    >>> rfc3339_duration_to_delta("PT24H")
 
2606
    datetime.timedelta(1)
 
2607
    >>> rfc3339_duration_to_delta("P1W")
 
2608
    datetime.timedelta(7)
 
2609
    >>> rfc3339_duration_to_delta("PT5M30S")
 
2610
    datetime.timedelta(0, 330)
 
2611
    >>> rfc3339_duration_to_delta("P1DT3M20S")
 
2612
    datetime.timedelta(1, 200)
 
2613
    """
 
2614
    
 
2615
    # Parsing an RFC 3339 duration with regular expressions is not
 
2616
    # possible - there would have to be multiple places for the same
 
2617
    # values, like seconds.  The current code, while more esoteric, is
 
2618
    # cleaner without depending on a parsing library.  If Python had a
 
2619
    # built-in library for parsing we would use it, but we'd like to
 
2620
    # avoid excessive use of external libraries.
 
2621
    
 
2622
    # New type for defining tokens, syntax, and semantics all-in-one
 
2623
    Token = collections.namedtuple("Token", (
 
2624
        "regexp",  # To match token; if "value" is not None, must have
 
2625
                   # a "group" containing digits
 
2626
        "value",   # datetime.timedelta or None
 
2627
        "followers"))           # Tokens valid after this token
 
2628
    # RFC 3339 "duration" tokens, syntax, and semantics; taken from
 
2629
    # the "duration" ABNF definition in RFC 3339, Appendix A.
 
2630
    token_end = Token(re.compile(r"$"), None, frozenset())
 
2631
    token_second = Token(re.compile(r"(\d+)S"),
 
2632
                         datetime.timedelta(seconds=1),
 
2633
                         frozenset((token_end, )))
 
2634
    token_minute = Token(re.compile(r"(\d+)M"),
 
2635
                         datetime.timedelta(minutes=1),
 
2636
                         frozenset((token_second, token_end)))
 
2637
    token_hour = Token(re.compile(r"(\d+)H"),
 
2638
                       datetime.timedelta(hours=1),
 
2639
                       frozenset((token_minute, token_end)))
 
2640
    token_time = Token(re.compile(r"T"),
 
2641
                       None,
 
2642
                       frozenset((token_hour, token_minute,
 
2643
                                  token_second)))
 
2644
    token_day = Token(re.compile(r"(\d+)D"),
 
2645
                      datetime.timedelta(days=1),
 
2646
                      frozenset((token_time, token_end)))
 
2647
    token_month = Token(re.compile(r"(\d+)M"),
 
2648
                        datetime.timedelta(weeks=4),
 
2649
                        frozenset((token_day, token_end)))
 
2650
    token_year = Token(re.compile(r"(\d+)Y"),
 
2651
                       datetime.timedelta(weeks=52),
 
2652
                       frozenset((token_month, token_end)))
 
2653
    token_week = Token(re.compile(r"(\d+)W"),
 
2654
                       datetime.timedelta(weeks=1),
 
2655
                       frozenset((token_end, )))
 
2656
    token_duration = Token(re.compile(r"P"), None,
 
2657
                           frozenset((token_year, token_month,
 
2658
                                      token_day, token_time,
 
2659
                                      token_week)))
 
2660
    # Define starting values
 
2661
    value = datetime.timedelta() # Value so far
 
2662
    found_token = None
 
2663
    followers = frozenset((token_duration, )) # Following valid tokens
 
2664
    s = duration                # String left to parse
 
2665
    # Loop until end token is found
 
2666
    while found_token is not token_end:
 
2667
        # Search for any currently valid tokens
 
2668
        for token in followers:
 
2669
            match = token.regexp.match(s)
 
2670
            if match is not None:
 
2671
                # Token found
 
2672
                if token.value is not None:
 
2673
                    # Value found, parse digits
 
2674
                    factor = int(match.group(1), 10)
 
2675
                    # Add to value so far
 
2676
                    value += factor * token.value
 
2677
                # Strip token from string
 
2678
                s = token.regexp.sub("", s, 1)
 
2679
                # Go to found token
 
2680
                found_token = token
 
2681
                # Set valid next tokens
 
2682
                followers = found_token.followers
 
2683
                break
 
2684
        else:
 
2685
            # No currently valid tokens were found
 
2686
            raise ValueError("Invalid RFC 3339 duration: {!r}"
 
2687
                             .format(duration))
 
2688
    # End token found
 
2689
    return value
774
2690
 
775
2691
 
776
2692
def string_to_delta(interval):
777
2693
    """Parse a string and return a datetime.timedelta
778
 
 
 
2694
    
779
2695
    >>> string_to_delta('7d')
780
2696
    datetime.timedelta(7)
781
2697
    >>> string_to_delta('60s')
784
2700
    datetime.timedelta(0, 3600)
785
2701
    >>> string_to_delta('24h')
786
2702
    datetime.timedelta(1)
787
 
    >>> string_to_delta(u'1w')
 
2703
    >>> string_to_delta('1w')
788
2704
    datetime.timedelta(7)
789
2705
    >>> string_to_delta('5m 30s')
790
2706
    datetime.timedelta(0, 330)
791
2707
    """
 
2708
    
 
2709
    try:
 
2710
        return rfc3339_duration_to_delta(interval)
 
2711
    except ValueError:
 
2712
        pass
 
2713
    
792
2714
    timevalue = datetime.timedelta(0)
793
2715
    for s in interval.split():
794
2716
        try:
795
 
            suffix = unicode(s[-1])
 
2717
            suffix = s[-1]
796
2718
            value = int(s[:-1])
797
 
            if suffix == u"d":
 
2719
            if suffix == "d":
798
2720
                delta = datetime.timedelta(value)
799
 
            elif suffix == u"s":
 
2721
            elif suffix == "s":
800
2722
                delta = datetime.timedelta(0, value)
801
 
            elif suffix == u"m":
 
2723
            elif suffix == "m":
802
2724
                delta = datetime.timedelta(0, 0, 0, 0, value)
803
 
            elif suffix == u"h":
 
2725
            elif suffix == "h":
804
2726
                delta = datetime.timedelta(0, 0, 0, 0, 0, value)
805
 
            elif suffix == u"w":
 
2727
            elif suffix == "w":
806
2728
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
807
2729
            else:
808
 
                raise ValueError
809
 
        except (ValueError, IndexError):
810
 
            raise ValueError
 
2730
                raise ValueError("Unknown suffix {!r}".format(suffix))
 
2731
        except IndexError as e:
 
2732
            raise ValueError(*(e.args))
811
2733
        timevalue += delta
812
2734
    return timevalue
813
2735
 
814
2736
 
815
 
def server_state_changed(state):
816
 
    """Derived from the Avahi example code"""
817
 
    if state == avahi.SERVER_COLLISION:
818
 
        logger.error(u"Zeroconf server name collision")
819
 
        service.remove()
820
 
    elif state == avahi.SERVER_RUNNING:
821
 
        service.add()
822
 
 
823
 
 
824
 
def entry_group_state_changed(state, error):
825
 
    """Derived from the Avahi example code"""
826
 
    logger.debug(u"Avahi state change: %i", state)
827
 
    
828
 
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
829
 
        logger.debug(u"Zeroconf service established.")
830
 
    elif state == avahi.ENTRY_GROUP_COLLISION:
831
 
        logger.warning(u"Zeroconf service name collision.")
832
 
        service.rename()
833
 
    elif state == avahi.ENTRY_GROUP_FAILURE:
834
 
        logger.critical(u"Avahi: Error in group state changed %s",
835
 
                        unicode(error))
836
 
        raise AvahiGroupError("State changed: %s", str(error))
837
 
 
838
 
def if_nametoindex(interface):
839
 
    """Call the C function if_nametoindex(), or equivalent"""
840
 
    global if_nametoindex
841
 
    try:
842
 
        if_nametoindex = (ctypes.cdll.LoadLibrary
843
 
                          (ctypes.util.find_library("c"))
844
 
                          .if_nametoindex)
845
 
    except (OSError, AttributeError):
846
 
        if "struct" not in sys.modules:
847
 
            import struct
848
 
        if "fcntl" not in sys.modules:
849
 
            import fcntl
850
 
        def if_nametoindex(interface):
851
 
            "Get an interface index the hard way, i.e. using fcntl()"
852
 
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
853
 
            with closing(socket.socket()) as s:
854
 
                ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
855
 
                                    struct.pack("16s16x", interface))
856
 
            interface_index = struct.unpack("I", ifreq[16:20])[0]
857
 
            return interface_index
858
 
    return if_nametoindex(interface)
859
 
 
860
 
 
861
2737
def daemon(nochdir = False, noclose = False):
862
2738
    """See daemon(3).  Standard BSD Unix function.
 
2739
    
863
2740
    This should really exist as os.daemon, but it doesn't (yet)."""
864
2741
    if os.fork():
865
2742
        sys.exit()
870
2747
        sys.exit()
871
2748
    if not noclose:
872
2749
        # Close all standard open file descriptors
873
 
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
2750
        null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
874
2751
        if not stat.S_ISCHR(os.fstat(null).st_mode):
875
2752
            raise OSError(errno.ENODEV,
876
 
                          "/dev/null not a character device")
 
2753
                          "{} not a character device"
 
2754
                          .format(os.devnull))
877
2755
        os.dup2(null, sys.stdin.fileno())
878
2756
        os.dup2(null, sys.stdout.fileno())
879
2757
        os.dup2(null, sys.stderr.fileno())
882
2760
 
883
2761
 
884
2762
def main():
885
 
    parser = OptionParser(version = "%%prog %s" % version)
886
 
    parser.add_option("-i", "--interface", type="string",
887
 
                      metavar="IF", help="Bind to interface IF")
888
 
    parser.add_option("-a", "--address", type="string",
889
 
                      help="Address to listen for requests on")
890
 
    parser.add_option("-p", "--port", type="int",
891
 
                      help="Port number to receive requests on")
892
 
    parser.add_option("--check", action="store_true", default=False,
893
 
                      help="Run self-test")
894
 
    parser.add_option("--debug", action="store_true",
895
 
                      help="Debug mode; run in foreground and log to"
896
 
                      " terminal")
897
 
    parser.add_option("--priority", type="string", help="GnuTLS"
898
 
                      " priority string (see GnuTLS documentation)")
899
 
    parser.add_option("--servicename", type="string", metavar="NAME",
900
 
                      help="Zeroconf service name")
901
 
    parser.add_option("--configdir", type="string",
902
 
                      default="/etc/mandos", metavar="DIR",
903
 
                      help="Directory to search for configuration"
904
 
                      " files")
905
 
    options = parser.parse_args()[0]
 
2763
    
 
2764
    ##################################################################
 
2765
    # Parsing of options, both command line and config file
 
2766
    
 
2767
    parser = argparse.ArgumentParser()
 
2768
    parser.add_argument("-v", "--version", action="version",
 
2769
                        version = "%(prog)s {}".format(version),
 
2770
                        help="show version number and exit")
 
2771
    parser.add_argument("-i", "--interface", metavar="IF",
 
2772
                        help="Bind to interface IF")
 
2773
    parser.add_argument("-a", "--address",
 
2774
                        help="Address to listen for requests on")
 
2775
    parser.add_argument("-p", "--port", type=int,
 
2776
                        help="Port number to receive requests on")
 
2777
    parser.add_argument("--check", action="store_true",
 
2778
                        help="Run self-test")
 
2779
    parser.add_argument("--debug", action="store_true",
 
2780
                        help="Debug mode; run in foreground and log"
 
2781
                        " to terminal", default=None)
 
2782
    parser.add_argument("--debuglevel", metavar="LEVEL",
 
2783
                        help="Debug level for stdout output")
 
2784
    parser.add_argument("--priority", help="GnuTLS"
 
2785
                        " priority string (see GnuTLS documentation)")
 
2786
    parser.add_argument("--servicename",
 
2787
                        metavar="NAME", help="Zeroconf service name")
 
2788
    parser.add_argument("--configdir",
 
2789
                        default="/etc/mandos", metavar="DIR",
 
2790
                        help="Directory to search for configuration"
 
2791
                        " files")
 
2792
    parser.add_argument("--no-dbus", action="store_false",
 
2793
                        dest="use_dbus", help="Do not provide D-Bus"
 
2794
                        " system bus interface", default=None)
 
2795
    parser.add_argument("--no-ipv6", action="store_false",
 
2796
                        dest="use_ipv6", help="Do not use IPv6",
 
2797
                        default=None)
 
2798
    parser.add_argument("--no-restore", action="store_false",
 
2799
                        dest="restore", help="Do not restore stored"
 
2800
                        " state", default=None)
 
2801
    parser.add_argument("--socket", type=int,
 
2802
                        help="Specify a file descriptor to a network"
 
2803
                        " socket to use instead of creating one")
 
2804
    parser.add_argument("--statedir", metavar="DIR",
 
2805
                        help="Directory to save/restore state in")
 
2806
    parser.add_argument("--foreground", action="store_true",
 
2807
                        help="Run in foreground", default=None)
 
2808
    parser.add_argument("--no-zeroconf", action="store_false",
 
2809
                        dest="zeroconf", help="Do not use Zeroconf",
 
2810
                        default=None)
 
2811
    
 
2812
    options = parser.parse_args()
906
2813
    
907
2814
    if options.check:
908
2815
        import doctest
909
 
        doctest.testmod()
910
 
        sys.exit()
 
2816
        fail_count, test_count = doctest.testmod()
 
2817
        sys.exit(os.EX_OK if fail_count == 0 else 1)
911
2818
    
912
2819
    # Default values for config file for server-global settings
913
2820
    server_defaults = { "interface": "",
915
2822
                        "port": "",
916
2823
                        "debug": "False",
917
2824
                        "priority":
918
 
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
 
2825
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
 
2826
                        ":+SIGN-DSA-SHA256",
919
2827
                        "servicename": "Mandos",
920
 
                        }
 
2828
                        "use_dbus": "True",
 
2829
                        "use_ipv6": "True",
 
2830
                        "debuglevel": "",
 
2831
                        "restore": "True",
 
2832
                        "socket": "",
 
2833
                        "statedir": "/var/lib/mandos",
 
2834
                        "foreground": "False",
 
2835
                        "zeroconf": "True",
 
2836
                    }
921
2837
    
922
2838
    # Parse config file for server-global settings
923
 
    server_config = ConfigParser.SafeConfigParser(server_defaults)
 
2839
    server_config = configparser.SafeConfigParser(server_defaults)
924
2840
    del server_defaults
925
2841
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
926
2842
    # Convert the SafeConfigParser object to a dict
927
2843
    server_settings = server_config.defaults()
928
 
    # Use getboolean on the boolean config option
929
 
    server_settings["debug"] = (server_config.getboolean
930
 
                                ("DEFAULT", "debug"))
 
2844
    # Use the appropriate methods on the non-string config options
 
2845
    for option in ("debug", "use_dbus", "use_ipv6", "foreground"):
 
2846
        server_settings[option] = server_config.getboolean("DEFAULT",
 
2847
                                                           option)
 
2848
    if server_settings["port"]:
 
2849
        server_settings["port"] = server_config.getint("DEFAULT",
 
2850
                                                       "port")
 
2851
    if server_settings["socket"]:
 
2852
        server_settings["socket"] = server_config.getint("DEFAULT",
 
2853
                                                         "socket")
 
2854
        # Later, stdin will, and stdout and stderr might, be dup'ed
 
2855
        # over with an opened os.devnull.  But we don't want this to
 
2856
        # happen with a supplied network socket.
 
2857
        if 0 <= server_settings["socket"] <= 2:
 
2858
            server_settings["socket"] = os.dup(server_settings
 
2859
                                               ["socket"])
931
2860
    del server_config
932
2861
    
933
2862
    # Override the settings from the config file with command line
934
2863
    # options, if set.
935
2864
    for option in ("interface", "address", "port", "debug",
936
 
                   "priority", "servicename", "configdir"):
 
2865
                   "priority", "servicename", "configdir", "use_dbus",
 
2866
                   "use_ipv6", "debuglevel", "restore", "statedir",
 
2867
                   "socket", "foreground", "zeroconf"):
937
2868
        value = getattr(options, option)
938
2869
        if value is not None:
939
2870
            server_settings[option] = value
940
2871
    del options
 
2872
    # Force all strings to be unicode
 
2873
    for option in server_settings.keys():
 
2874
        if isinstance(server_settings[option], bytes):
 
2875
            server_settings[option] = (server_settings[option]
 
2876
                                       .decode("utf-8"))
 
2877
    # Force all boolean options to be boolean
 
2878
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
 
2879
                   "foreground", "zeroconf"):
 
2880
        server_settings[option] = bool(server_settings[option])
 
2881
    # Debug implies foreground
 
2882
    if server_settings["debug"]:
 
2883
        server_settings["foreground"] = True
941
2884
    # Now we have our good server settings in "server_settings"
942
2885
    
 
2886
    ##################################################################
 
2887
    
 
2888
    if (not server_settings["zeroconf"]
 
2889
        and not (server_settings["port"]
 
2890
                 or server_settings["socket"] != "")):
 
2891
        parser.error("Needs port or socket to work without Zeroconf")
 
2892
    
 
2893
    # For convenience
943
2894
    debug = server_settings["debug"]
 
2895
    debuglevel = server_settings["debuglevel"]
 
2896
    use_dbus = server_settings["use_dbus"]
 
2897
    use_ipv6 = server_settings["use_ipv6"]
 
2898
    stored_state_path = os.path.join(server_settings["statedir"],
 
2899
                                     stored_state_file)
 
2900
    foreground = server_settings["foreground"]
 
2901
    zeroconf = server_settings["zeroconf"]
944
2902
    
945
 
    if not debug:
946
 
        syslogger.setLevel(logging.WARNING)
947
 
        console.setLevel(logging.WARNING)
 
2903
    if debug:
 
2904
        initlogger(debug, logging.DEBUG)
 
2905
    else:
 
2906
        if not debuglevel:
 
2907
            initlogger(debug)
 
2908
        else:
 
2909
            level = getattr(logging, debuglevel.upper())
 
2910
            initlogger(debug, level)
948
2911
    
949
2912
    if server_settings["servicename"] != "Mandos":
950
 
        syslogger.setFormatter(logging.Formatter
951
 
                               ('Mandos (%s): %%(levelname)s:'
952
 
                                ' %%(message)s'
953
 
                                % server_settings["servicename"]))
 
2913
        syslogger.setFormatter(
 
2914
            logging.Formatter('Mandos ({}) [%(process)d]:'
 
2915
                              ' %(levelname)s: %(message)s'.format(
 
2916
                                  server_settings["servicename"])))
954
2917
    
955
2918
    # Parse config file with clients
956
 
    client_defaults = { "timeout": "1h",
957
 
                        "interval": "5m",
958
 
                        "checker": "fping -q -- %(host)s",
959
 
                        "host": "",
960
 
                        }
961
 
    client_config = ConfigParser.SafeConfigParser(client_defaults)
 
2919
    client_config = configparser.SafeConfigParser(Client
 
2920
                                                  .client_defaults)
962
2921
    client_config.read(os.path.join(server_settings["configdir"],
963
2922
                                    "clients.conf"))
964
2923
    
965
 
    clients = Set()
966
 
    tcp_server = IPv6_TCPServer((server_settings["address"],
967
 
                                 server_settings["port"]),
968
 
                                TCP_handler,
969
 
                                settings=server_settings,
970
 
                                clients=clients)
971
 
    pidfilename = "/var/run/mandos.pid"
972
 
    try:
973
 
        pidfile = open(pidfilename, "w")
974
 
    except IOError, error:
975
 
        logger.error("Could not open file %r", pidfilename)
976
 
    
977
 
    try:
978
 
        uid = pwd.getpwnam("_mandos").pw_uid
979
 
    except KeyError:
980
 
        try:
981
 
            uid = pwd.getpwnam("mandos").pw_uid
982
 
        except KeyError:
983
 
            try:
984
 
                uid = pwd.getpwnam("nobody").pw_uid
985
 
            except KeyError:
986
 
                uid = 65534
987
 
    try:
988
 
        gid = pwd.getpwnam("_mandos").pw_gid
989
 
    except KeyError:
990
 
        try:
991
 
            gid = pwd.getpwnam("mandos").pw_gid
992
 
        except KeyError:
993
 
            try:
994
 
                gid = pwd.getpwnam("nogroup").pw_gid
995
 
            except KeyError:
996
 
                gid = 65534
997
 
    try:
 
2924
    global mandos_dbus_service
 
2925
    mandos_dbus_service = None
 
2926
    
 
2927
    socketfd = None
 
2928
    if server_settings["socket"] != "":
 
2929
        socketfd = server_settings["socket"]
 
2930
    tcp_server = MandosServer(
 
2931
        (server_settings["address"], server_settings["port"]),
 
2932
        ClientHandler,
 
2933
        interface=(server_settings["interface"] or None),
 
2934
        use_ipv6=use_ipv6,
 
2935
        gnutls_priority=server_settings["priority"],
 
2936
        use_dbus=use_dbus,
 
2937
        socketfd=socketfd)
 
2938
    if not foreground:
 
2939
        pidfilename = "/run/mandos.pid"
 
2940
        if not os.path.isdir("/run/."):
 
2941
            pidfilename = "/var/run/mandos.pid"
 
2942
        pidfile = None
 
2943
        try:
 
2944
            pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
 
2945
        except IOError as e:
 
2946
            logger.error("Could not open file %r", pidfilename,
 
2947
                         exc_info=e)
 
2948
    
 
2949
    for name, group in (("_mandos", "_mandos"),
 
2950
                        ("mandos", "mandos"),
 
2951
                        ("nobody", "nogroup")):
 
2952
        try:
 
2953
            uid = pwd.getpwnam(name).pw_uid
 
2954
            gid = pwd.getpwnam(group).pw_gid
 
2955
            break
 
2956
        except KeyError:
 
2957
            continue
 
2958
    else:
 
2959
        uid = 65534
 
2960
        gid = 65534
 
2961
    try:
 
2962
        os.setgid(gid)
998
2963
        os.setuid(uid)
999
 
        os.setgid(gid)
1000
 
    except OSError, error:
1001
 
        if error[0] != errno.EPERM:
1002
 
            raise error
1003
 
    
1004
 
    global service
1005
 
    service = AvahiService(name = server_settings["servicename"],
1006
 
                           servicetype = "_mandos._tcp", )
1007
 
    if server_settings["interface"]:
1008
 
        service.interface = (if_nametoindex
1009
 
                             (server_settings["interface"]))
 
2964
        if debug:
 
2965
            logger.debug("Did setuid/setgid to {}:{}".format(uid,
 
2966
                                                             gid))
 
2967
    except OSError as error:
 
2968
        logger.warning("Failed to setuid/setgid to {}:{}: {}"
 
2969
                       .format(uid, gid, os.strerror(error.errno)))
 
2970
        if error.errno != errno.EPERM:
 
2971
            raise
 
2972
    
 
2973
    if debug:
 
2974
        # Enable all possible GnuTLS debugging
 
2975
        
 
2976
        # "Use a log level over 10 to enable all debugging options."
 
2977
        # - GnuTLS manual
 
2978
        gnutls.global_set_log_level(11)
 
2979
        
 
2980
        @gnutls.log_func
 
2981
        def debug_gnutls(level, string):
 
2982
            logger.debug("GnuTLS: %s", string[:-1])
 
2983
        
 
2984
        gnutls.global_set_log_function(debug_gnutls)
 
2985
        
 
2986
        # Redirect stdin so all checkers get /dev/null
 
2987
        null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
 
2988
        os.dup2(null, sys.stdin.fileno())
 
2989
        if null > 2:
 
2990
            os.close(null)
 
2991
    
 
2992
    # Need to fork before connecting to D-Bus
 
2993
    if not foreground:
 
2994
        # Close all input and output, do double fork, etc.
 
2995
        daemon()
 
2996
    
 
2997
    # multiprocessing will use threads, so before we use GLib we need
 
2998
    # to inform GLib that threads will be used.
 
2999
    GLib.threads_init()
1010
3000
    
1011
3001
    global main_loop
1012
 
    global bus
1013
 
    global server
1014
3002
    # From the Avahi example code
1015
 
    DBusGMainLoop(set_as_default=True )
1016
 
    main_loop = gobject.MainLoop()
 
3003
    DBusGMainLoop(set_as_default=True)
 
3004
    main_loop = GLib.MainLoop()
1017
3005
    bus = dbus.SystemBus()
1018
 
    server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1019
 
                                           avahi.DBUS_PATH_SERVER),
1020
 
                            avahi.DBUS_INTERFACE_SERVER)
1021
3006
    # End of Avahi example code
1022
 
    bus_name = dbus.service.BusName(u"org.mandos-system.Mandos", bus)
1023
 
    
1024
 
    clients.update(Set(Client(name = section,
1025
 
                              config
1026
 
                              = dict(client_config.items(section)))
1027
 
                       for section in client_config.sections()))
1028
 
    if not clients:
1029
 
        logger.critical(u"No clients defined")
1030
 
        sys.exit(1)
1031
 
    
1032
 
    if debug:
1033
 
        # Redirect stdin so all checkers get /dev/null
1034
 
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1035
 
        os.dup2(null, sys.stdin.fileno())
1036
 
        if null > 2:
1037
 
            os.close(null)
1038
 
    else:
1039
 
        # No console logging
1040
 
        logger.removeHandler(console)
1041
 
        # Close all input and output, do double fork, etc.
1042
 
        daemon()
1043
 
    
1044
 
    try:
1045
 
        pid = os.getpid()
1046
 
        pidfile.write(str(pid) + "\n")
1047
 
        pidfile.close()
 
3007
    if use_dbus:
 
3008
        try:
 
3009
            bus_name = dbus.service.BusName("se.recompile.Mandos",
 
3010
                                            bus,
 
3011
                                            do_not_queue=True)
 
3012
            old_bus_name = dbus.service.BusName(
 
3013
                "se.bsnet.fukt.Mandos", bus,
 
3014
                do_not_queue=True)
 
3015
        except dbus.exceptions.DBusException as e:
 
3016
            logger.error("Disabling D-Bus:", exc_info=e)
 
3017
            use_dbus = False
 
3018
            server_settings["use_dbus"] = False
 
3019
            tcp_server.use_dbus = False
 
3020
    if zeroconf:
 
3021
        protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
 
3022
        service = AvahiServiceToSyslog(
 
3023
            name = server_settings["servicename"],
 
3024
            servicetype = "_mandos._tcp",
 
3025
            protocol = protocol,
 
3026
            bus = bus)
 
3027
        if server_settings["interface"]:
 
3028
            service.interface = if_nametoindex(
 
3029
                server_settings["interface"].encode("utf-8"))
 
3030
    
 
3031
    global multiprocessing_manager
 
3032
    multiprocessing_manager = multiprocessing.Manager()
 
3033
    
 
3034
    client_class = Client
 
3035
    if use_dbus:
 
3036
        client_class = functools.partial(ClientDBus, bus = bus)
 
3037
    
 
3038
    client_settings = Client.config_parser(client_config)
 
3039
    old_client_settings = {}
 
3040
    clients_data = {}
 
3041
    
 
3042
    # This is used to redirect stdout and stderr for checker processes
 
3043
    global wnull
 
3044
    wnull = open(os.devnull, "w") # A writable /dev/null
 
3045
    # Only used if server is running in foreground but not in debug
 
3046
    # mode
 
3047
    if debug or not foreground:
 
3048
        wnull.close()
 
3049
    
 
3050
    # Get client data and settings from last running state.
 
3051
    if server_settings["restore"]:
 
3052
        try:
 
3053
            with open(stored_state_path, "rb") as stored_state:
 
3054
                if sys.version_info.major == 2:                
 
3055
                    clients_data, old_client_settings = pickle.load(
 
3056
                        stored_state)
 
3057
                else:
 
3058
                    bytes_clients_data, bytes_old_client_settings = (
 
3059
                        pickle.load(stored_state, encoding = "bytes"))
 
3060
                    ### Fix bytes to strings
 
3061
                    ## clients_data
 
3062
                    # .keys()
 
3063
                    clients_data = { (key.decode("utf-8")
 
3064
                                      if isinstance(key, bytes)
 
3065
                                      else key): value
 
3066
                                     for key, value in
 
3067
                                     bytes_clients_data.items() }
 
3068
                    del bytes_clients_data
 
3069
                    for key in clients_data:
 
3070
                        value = { (k.decode("utf-8")
 
3071
                                   if isinstance(k, bytes) else k): v
 
3072
                                  for k, v in
 
3073
                                  clients_data[key].items() }
 
3074
                        clients_data[key] = value
 
3075
                        # .client_structure
 
3076
                        value["client_structure"] = [
 
3077
                            (s.decode("utf-8")
 
3078
                             if isinstance(s, bytes)
 
3079
                             else s) for s in
 
3080
                            value["client_structure"] ]
 
3081
                        # .name & .host
 
3082
                        for k in ("name", "host"):
 
3083
                            if isinstance(value[k], bytes):
 
3084
                                value[k] = value[k].decode("utf-8")
 
3085
                    ## old_client_settings
 
3086
                    # .keys()
 
3087
                    old_client_settings = {
 
3088
                        (key.decode("utf-8")
 
3089
                         if isinstance(key, bytes)
 
3090
                         else key): value
 
3091
                        for key, value in
 
3092
                        bytes_old_client_settings.items() }
 
3093
                    del bytes_old_client_settings
 
3094
                    # .host
 
3095
                    for value in old_client_settings.values():
 
3096
                        if isinstance(value["host"], bytes):
 
3097
                            value["host"] = (value["host"]
 
3098
                                             .decode("utf-8"))
 
3099
            os.remove(stored_state_path)
 
3100
        except IOError as e:
 
3101
            if e.errno == errno.ENOENT:
 
3102
                logger.warning("Could not load persistent state:"
 
3103
                               " {}".format(os.strerror(e.errno)))
 
3104
            else:
 
3105
                logger.critical("Could not load persistent state:",
 
3106
                                exc_info=e)
 
3107
                raise
 
3108
        except EOFError as e:
 
3109
            logger.warning("Could not load persistent state: "
 
3110
                           "EOFError:",
 
3111
                           exc_info=e)
 
3112
    
 
3113
    with PGPEngine() as pgp:
 
3114
        for client_name, client in clients_data.items():
 
3115
            # Skip removed clients
 
3116
            if client_name not in client_settings:
 
3117
                continue
 
3118
            
 
3119
            # Decide which value to use after restoring saved state.
 
3120
            # We have three different values: Old config file,
 
3121
            # new config file, and saved state.
 
3122
            # New config value takes precedence if it differs from old
 
3123
            # config value, otherwise use saved state.
 
3124
            for name, value in client_settings[client_name].items():
 
3125
                try:
 
3126
                    # For each value in new config, check if it
 
3127
                    # differs from the old config value (Except for
 
3128
                    # the "secret" attribute)
 
3129
                    if (name != "secret"
 
3130
                        and (value !=
 
3131
                             old_client_settings[client_name][name])):
 
3132
                        client[name] = value
 
3133
                except KeyError:
 
3134
                    pass
 
3135
            
 
3136
            # Clients who has passed its expire date can still be
 
3137
            # enabled if its last checker was successful.  A Client
 
3138
            # whose checker succeeded before we stored its state is
 
3139
            # assumed to have successfully run all checkers during
 
3140
            # downtime.
 
3141
            if client["enabled"]:
 
3142
                if datetime.datetime.utcnow() >= client["expires"]:
 
3143
                    if not client["last_checked_ok"]:
 
3144
                        logger.warning(
 
3145
                            "disabling client {} - Client never "
 
3146
                            "performed a successful checker".format(
 
3147
                                client_name))
 
3148
                        client["enabled"] = False
 
3149
                    elif client["last_checker_status"] != 0:
 
3150
                        logger.warning(
 
3151
                            "disabling client {} - Client last"
 
3152
                            " checker failed with error code"
 
3153
                            " {}".format(
 
3154
                                client_name,
 
3155
                                client["last_checker_status"]))
 
3156
                        client["enabled"] = False
 
3157
                    else:
 
3158
                        client["expires"] = (
 
3159
                            datetime.datetime.utcnow()
 
3160
                            + client["timeout"])
 
3161
                        logger.debug("Last checker succeeded,"
 
3162
                                     " keeping {} enabled".format(
 
3163
                                         client_name))
 
3164
            try:
 
3165
                client["secret"] = pgp.decrypt(
 
3166
                    client["encrypted_secret"],
 
3167
                    client_settings[client_name]["secret"])
 
3168
            except PGPError:
 
3169
                # If decryption fails, we use secret from new settings
 
3170
                logger.debug("Failed to decrypt {} old secret".format(
 
3171
                    client_name))
 
3172
                client["secret"] = (client_settings[client_name]
 
3173
                                    ["secret"])
 
3174
    
 
3175
    # Add/remove clients based on new changes made to config
 
3176
    for client_name in (set(old_client_settings)
 
3177
                        - set(client_settings)):
 
3178
        del clients_data[client_name]
 
3179
    for client_name in (set(client_settings)
 
3180
                        - set(old_client_settings)):
 
3181
        clients_data[client_name] = client_settings[client_name]
 
3182
    
 
3183
    # Create all client objects
 
3184
    for client_name, client in clients_data.items():
 
3185
        tcp_server.clients[client_name] = client_class(
 
3186
            name = client_name,
 
3187
            settings = client,
 
3188
            server_settings = server_settings)
 
3189
    
 
3190
    if not tcp_server.clients:
 
3191
        logger.warning("No clients defined")
 
3192
    
 
3193
    if not foreground:
 
3194
        if pidfile is not None:
 
3195
            pid = os.getpid()
 
3196
            try:
 
3197
                with pidfile:
 
3198
                    print(pid, file=pidfile)
 
3199
            except IOError:
 
3200
                logger.error("Could not write to file %r with PID %d",
 
3201
                             pidfilename, pid)
1048
3202
        del pidfile
1049
 
    except IOError:
1050
 
        logger.error(u"Could not write to file %r with PID %d",
1051
 
                     pidfilename, pid)
1052
 
    except NameError:
1053
 
        # "pidfile" was never created
1054
 
        pass
1055
 
    del pidfilename
 
3203
        del pidfilename
 
3204
    
 
3205
    for termsig in (signal.SIGHUP, signal.SIGTERM):
 
3206
        GLib.unix_signal_add(GLib.PRIORITY_HIGH, termsig,
 
3207
                             lambda: main_loop.quit() and False)
 
3208
    
 
3209
    if use_dbus:
 
3210
        
 
3211
        @alternate_dbus_interfaces(
 
3212
            { "se.recompile.Mandos": "se.bsnet.fukt.Mandos" })
 
3213
        class MandosDBusService(DBusObjectWithObjectManager):
 
3214
            """A D-Bus proxy object"""
 
3215
            
 
3216
            def __init__(self):
 
3217
                dbus.service.Object.__init__(self, bus, "/")
 
3218
            
 
3219
            _interface = "se.recompile.Mandos"
 
3220
            
 
3221
            @dbus.service.signal(_interface, signature="o")
 
3222
            def ClientAdded(self, objpath):
 
3223
                "D-Bus signal"
 
3224
                pass
 
3225
            
 
3226
            @dbus.service.signal(_interface, signature="ss")
 
3227
            def ClientNotFound(self, fingerprint, address):
 
3228
                "D-Bus signal"
 
3229
                pass
 
3230
            
 
3231
            @dbus_annotations({"org.freedesktop.DBus.Deprecated":
 
3232
                               "true"})
 
3233
            @dbus.service.signal(_interface, signature="os")
 
3234
            def ClientRemoved(self, objpath, name):
 
3235
                "D-Bus signal"
 
3236
                pass
 
3237
            
 
3238
            @dbus_annotations({"org.freedesktop.DBus.Deprecated":
 
3239
                               "true"})
 
3240
            @dbus.service.method(_interface, out_signature="ao")
 
3241
            def GetAllClients(self):
 
3242
                "D-Bus method"
 
3243
                return dbus.Array(c.dbus_object_path for c in
 
3244
                                  tcp_server.clients.values())
 
3245
            
 
3246
            @dbus_annotations({"org.freedesktop.DBus.Deprecated":
 
3247
                               "true"})
 
3248
            @dbus.service.method(_interface,
 
3249
                                 out_signature="a{oa{sv}}")
 
3250
            def GetAllClientsWithProperties(self):
 
3251
                "D-Bus method"
 
3252
                return dbus.Dictionary(
 
3253
                    { c.dbus_object_path: c.GetAll(
 
3254
                        "se.recompile.Mandos.Client")
 
3255
                      for c in tcp_server.clients.values() },
 
3256
                    signature="oa{sv}")
 
3257
            
 
3258
            @dbus.service.method(_interface, in_signature="o")
 
3259
            def RemoveClient(self, object_path):
 
3260
                "D-Bus method"
 
3261
                for c in tcp_server.clients.values():
 
3262
                    if c.dbus_object_path == object_path:
 
3263
                        del tcp_server.clients[c.name]
 
3264
                        c.remove_from_connection()
 
3265
                        # Don't signal the disabling
 
3266
                        c.disable(quiet=True)
 
3267
                        # Emit D-Bus signal for removal
 
3268
                        self.client_removed_signal(c)
 
3269
                        return
 
3270
                raise KeyError(object_path)
 
3271
            
 
3272
            del _interface
 
3273
            
 
3274
            @dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
 
3275
                                 out_signature = "a{oa{sa{sv}}}")
 
3276
            def GetManagedObjects(self):
 
3277
                """D-Bus method"""
 
3278
                return dbus.Dictionary(
 
3279
                    { client.dbus_object_path:
 
3280
                      dbus.Dictionary(
 
3281
                          { interface: client.GetAll(interface)
 
3282
                            for interface in
 
3283
                                 client._get_all_interface_names()})
 
3284
                      for client in tcp_server.clients.values()})
 
3285
            
 
3286
            def client_added_signal(self, client):
 
3287
                """Send the new standard signal and the old signal"""
 
3288
                if use_dbus:
 
3289
                    # New standard signal
 
3290
                    self.InterfacesAdded(
 
3291
                        client.dbus_object_path,
 
3292
                        dbus.Dictionary(
 
3293
                            { interface: client.GetAll(interface)
 
3294
                              for interface in
 
3295
                              client._get_all_interface_names()}))
 
3296
                    # Old signal
 
3297
                    self.ClientAdded(client.dbus_object_path)
 
3298
            
 
3299
            def client_removed_signal(self, client):
 
3300
                """Send the new standard signal and the old signal"""
 
3301
                if use_dbus:
 
3302
                    # New standard signal
 
3303
                    self.InterfacesRemoved(
 
3304
                        client.dbus_object_path,
 
3305
                        client._get_all_interface_names())
 
3306
                    # Old signal
 
3307
                    self.ClientRemoved(client.dbus_object_path,
 
3308
                                       client.name)
 
3309
        
 
3310
        mandos_dbus_service = MandosDBusService()
1056
3311
    
1057
3312
    def cleanup():
1058
3313
        "Cleanup function; run on exit"
1059
 
        global group
1060
 
        # From the Avahi example code
1061
 
        if not group is None:
1062
 
            group.Free()
1063
 
            group = None
1064
 
        # End of Avahi example code
1065
 
        
1066
 
        while clients:
1067
 
            client = clients.pop()
1068
 
            client.disable_hook = None
1069
 
            client.disable()
 
3314
        if zeroconf:
 
3315
            service.cleanup()
 
3316
        
 
3317
        multiprocessing.active_children()
 
3318
        wnull.close()
 
3319
        if not (tcp_server.clients or client_settings):
 
3320
            return
 
3321
        
 
3322
        # Store client before exiting. Secrets are encrypted with key
 
3323
        # based on what config file has. If config file is
 
3324
        # removed/edited, old secret will thus be unrecovable.
 
3325
        clients = {}
 
3326
        with PGPEngine() as pgp:
 
3327
            for client in tcp_server.clients.values():
 
3328
                key = client_settings[client.name]["secret"]
 
3329
                client.encrypted_secret = pgp.encrypt(client.secret,
 
3330
                                                      key)
 
3331
                client_dict = {}
 
3332
                
 
3333
                # A list of attributes that can not be pickled
 
3334
                # + secret.
 
3335
                exclude = { "bus", "changedstate", "secret",
 
3336
                            "checker", "server_settings" }
 
3337
                for name, typ in inspect.getmembers(dbus.service
 
3338
                                                    .Object):
 
3339
                    exclude.add(name)
 
3340
                
 
3341
                client_dict["encrypted_secret"] = (client
 
3342
                                                   .encrypted_secret)
 
3343
                for attr in client.client_structure:
 
3344
                    if attr not in exclude:
 
3345
                        client_dict[attr] = getattr(client, attr)
 
3346
                
 
3347
                clients[client.name] = client_dict
 
3348
                del client_settings[client.name]["secret"]
 
3349
        
 
3350
        try:
 
3351
            with tempfile.NamedTemporaryFile(
 
3352
                    mode='wb',
 
3353
                    suffix=".pickle",
 
3354
                    prefix='clients-',
 
3355
                    dir=os.path.dirname(stored_state_path),
 
3356
                    delete=False) as stored_state:
 
3357
                pickle.dump((clients, client_settings), stored_state,
 
3358
                            protocol = 2)
 
3359
                tempname = stored_state.name
 
3360
            os.rename(tempname, stored_state_path)
 
3361
        except (IOError, OSError) as e:
 
3362
            if not debug:
 
3363
                try:
 
3364
                    os.remove(tempname)
 
3365
                except NameError:
 
3366
                    pass
 
3367
            if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
 
3368
                logger.warning("Could not save persistent state: {}"
 
3369
                               .format(os.strerror(e.errno)))
 
3370
            else:
 
3371
                logger.warning("Could not save persistent state:",
 
3372
                               exc_info=e)
 
3373
                raise
 
3374
        
 
3375
        # Delete all clients, and settings from config
 
3376
        while tcp_server.clients:
 
3377
            name, client = tcp_server.clients.popitem()
 
3378
            if use_dbus:
 
3379
                client.remove_from_connection()
 
3380
            # Don't signal the disabling
 
3381
            client.disable(quiet=True)
 
3382
            # Emit D-Bus signal for removal
 
3383
            if use_dbus:
 
3384
                mandos_dbus_service.client_removed_signal(client)
 
3385
        client_settings.clear()
1070
3386
    
1071
3387
    atexit.register(cleanup)
1072
3388
    
1073
 
    if not debug:
1074
 
        signal.signal(signal.SIGINT, signal.SIG_IGN)
1075
 
    signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1076
 
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1077
 
    
1078
 
    class MandosServer(dbus.service.Object):
1079
 
        """A D-Bus proxy object"""
1080
 
        def __init__(self):
1081
 
            dbus.service.Object.__init__(self, bus,
1082
 
                                         "/Mandos")
1083
 
        _interface = u"org.mandos_system.Mandos"
1084
 
        
1085
 
        @dbus.service.signal(_interface, signature="oa{sv}")
1086
 
        def ClientAdded(self, objpath, properties):
1087
 
            "D-Bus signal"
1088
 
            pass
1089
 
        
1090
 
        @dbus.service.signal(_interface, signature="o")
1091
 
        def ClientRemoved(self, objpath):
1092
 
            "D-Bus signal"
1093
 
            pass
1094
 
        
1095
 
        @dbus.service.method(_interface, out_signature="ao")
1096
 
        def GetAllClients(self):
1097
 
            return dbus.Array(c.dbus_object_path for c in clients)
1098
 
        
1099
 
        @dbus.service.method(_interface, out_signature="a{oa{sv}}")
1100
 
        def GetAllClientsWithProperties(self):
1101
 
            return dbus.Dictionary(
1102
 
                ((c.dbus_object_path, c.GetAllProperties())
1103
 
                 for c in clients),
1104
 
                signature="oa{sv}")
1105
 
        
1106
 
        @dbus.service.method(_interface, in_signature="o")
1107
 
        def RemoveClient(self, object_path):
1108
 
            for c in clients:
1109
 
                if c.dbus_object_path == object_path:
1110
 
                    c.disable()
1111
 
                    clients.remove(c)
1112
 
                    return
1113
 
            raise KeyError
1114
 
        
1115
 
        del _interface
1116
 
    
1117
 
    mandos_server = MandosServer()
1118
 
    
1119
 
    for client in clients:
1120
 
        # Emit D-Bus signal
1121
 
        mandos_server.ClientAdded(client.dbus_object_path,
1122
 
                                  client.GetAllProperties())
1123
 
        client.enable()
 
3389
    for client in tcp_server.clients.values():
 
3390
        if use_dbus:
 
3391
            # Emit D-Bus signal for adding
 
3392
            mandos_dbus_service.client_added_signal(client)
 
3393
        # Need to initiate checking of clients
 
3394
        if client.enabled:
 
3395
            client.init_checker()
1124
3396
    
1125
3397
    tcp_server.enable()
1126
3398
    tcp_server.server_activate()
1127
3399
    
1128
3400
    # Find out what port we got
1129
 
    service.port = tcp_server.socket.getsockname()[1]
1130
 
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1131
 
                u" scope_id %d" % tcp_server.socket.getsockname())
 
3401
    if zeroconf:
 
3402
        service.port = tcp_server.socket.getsockname()[1]
 
3403
    if use_ipv6:
 
3404
        logger.info("Now listening on address %r, port %d,"
 
3405
                    " flowinfo %d, scope_id %d",
 
3406
                    *tcp_server.socket.getsockname())
 
3407
    else:                       # IPv4
 
3408
        logger.info("Now listening on address %r, port %d",
 
3409
                    *tcp_server.socket.getsockname())
1132
3410
    
1133
3411
    #service.interface = tcp_server.socket.getsockname()[3]
1134
3412
    
1135
3413
    try:
1136
 
        # From the Avahi example code
1137
 
        server.connect_to_signal("StateChanged", server_state_changed)
1138
 
        try:
1139
 
            server_state_changed(server.GetState())
1140
 
        except dbus.exceptions.DBusException, error:
1141
 
            logger.critical(u"DBusException: %s", error)
1142
 
            sys.exit(1)
1143
 
        # End of Avahi example code
1144
 
        
1145
 
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1146
 
                             lambda *args, **kwargs:
1147
 
                             (tcp_server.handle_request
1148
 
                              (*args[2:], **kwargs) or True))
1149
 
        
1150
 
        logger.debug(u"Starting main loop")
 
3414
        if zeroconf:
 
3415
            # From the Avahi example code
 
3416
            try:
 
3417
                service.activate()
 
3418
            except dbus.exceptions.DBusException as error:
 
3419
                logger.critical("D-Bus Exception", exc_info=error)
 
3420
                cleanup()
 
3421
                sys.exit(1)
 
3422
            # End of Avahi example code
 
3423
        
 
3424
        GLib.io_add_watch(tcp_server.fileno(), GLib.IO_IN,
 
3425
                          lambda *args, **kwargs:
 
3426
                          (tcp_server.handle_request
 
3427
                           (*args[2:], **kwargs) or True))
 
3428
        
 
3429
        logger.debug("Starting main loop")
1151
3430
        main_loop.run()
1152
 
    except AvahiError, error:
1153
 
        logger.critical(u"AvahiError: %s" + unicode(error))
 
3431
    except AvahiError as error:
 
3432
        logger.critical("Avahi Error", exc_info=error)
 
3433
        cleanup()
1154
3434
        sys.exit(1)
1155
3435
    except KeyboardInterrupt:
1156
3436
        if debug:
1157
 
            print
 
3437
            print("", file=sys.stderr)
 
3438
        logger.debug("Server received KeyboardInterrupt")
 
3439
    logger.debug("Server exiting")
 
3440
    # Must run before the D-Bus bus name gets deregistered
 
3441
    cleanup()
 
3442
 
1158
3443
 
1159
3444
if __name__ == '__main__':
1160
3445
    main()