/mandos/release

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

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2014-08-09 13:18:46 UTC
  • mto: (237.7.304 trunk)
  • mto: This revision was merged to the branch mainline in revision 323.
  • Revision ID: teddy@recompile.se-20140809131846-2emd272mf9c9wihe
mandos: Replace unicode() with str().

This is in preparation for the coming Python 3 conversion.

* mandos: Replace "str" with "unicode".  All callers changed.

Show diffs side-by-side

added added

removed removed

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