/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: 2019-08-24 14:43:51 UTC
  • Revision ID: teddy@recompile.se-20190824144351-2y0l31jpj496vrtu
Server: Add scaffolding for tests

* mandos: Add code to run tests via the unittest module, similar to
          the code in mandos-ctl.  Also shut down logging on exit.

Show diffs side-by-side

added added

removed removed

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