/mandos/trunk

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

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2009-01-20 02:23:59 UTC
  • Revision ID: teddy@fukt.bsnet.se-20090120022359-0go4voqdz5pxc1pu
* mandos (Client.CheckerCompleted): Changed signature to "nxs"; return
                                    exitstatus and waitstatus, not
                                    just a bool and waitstatus.  All
                                    emitters changed.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
 
2
# -*- mode: python; coding: utf-8 -*-
 
3
 
4
# Mandos server - give out binary blobs to connecting clients.
 
5
 
6
# This program is partly derived from an example program for an Avahi
 
7
# service publisher, downloaded from
 
8
# <http://avahi.org/wiki/PythonPublishExample>.  This includes the
 
9
# methods "add" and "remove" in the "AvahiService" class, the
 
10
# "server_state_changed" and "entry_group_state_changed" functions,
 
11
# and some lines in "main".
 
12
 
13
# Everything else is
 
14
# Copyright © 2008,2009 Teddy Hogeborn
 
15
# Copyright © 2008,2009 Björn Påhlsson
 
16
 
17
# This program is free software: you can redistribute it and/or modify
 
18
# it under the terms of the GNU General Public License as published by
 
19
# the Free Software Foundation, either version 3 of the License, or
 
20
# (at your option) any later version.
 
21
#
 
22
#     This program is distributed in the hope that it will be useful,
 
23
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
 
24
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
25
#     GNU General Public License for more details.
 
26
 
27
# You should have received a copy of the GNU General Public License
 
28
# along with this program.  If not, see
 
29
# <http://www.gnu.org/licenses/>.
 
30
 
31
# Contact the authors at <mandos@fukt.bsnet.se>.
 
32
 
33
 
 
34
from __future__ import division, with_statement, absolute_import
2
35
 
3
36
import SocketServer
4
37
import socket
5
 
import select
6
 
from optparse import OptionParser
 
38
import optparse
7
39
import datetime
8
40
import errno
9
41
import gnutls.crypto
10
42
import gnutls.connection
11
43
import gnutls.errors
 
44
import gnutls.library.functions
 
45
import gnutls.library.constants
 
46
import gnutls.library.types
12
47
import ConfigParser
13
 
 
14
 
class Client(object):
15
 
    def __init__(self, name=None, dn=None, password=None,
16
 
                 passfile=None, fqdn=None, timeout=None,
17
 
                 interval=-1):
18
 
        self.name = name
19
 
        self.dn = dn
20
 
        if password:
21
 
            self.password = password
22
 
        elif passfile:
23
 
            self.password = open(passfile).readall()
 
48
import sys
 
49
import re
 
50
import os
 
51
import signal
 
52
from sets import Set
 
53
import subprocess
 
54
import atexit
 
55
import stat
 
56
import logging
 
57
import logging.handlers
 
58
import pwd
 
59
from contextlib import closing
 
60
 
 
61
import dbus
 
62
import dbus.service
 
63
import gobject
 
64
import avahi
 
65
from dbus.mainloop.glib import DBusGMainLoop
 
66
import ctypes
 
67
import ctypes.util
 
68
 
 
69
version = "1.0.5"
 
70
 
 
71
logger = logging.Logger('mandos')
 
72
syslogger = (logging.handlers.SysLogHandler
 
73
             (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
 
74
              address = "/dev/log"))
 
75
syslogger.setFormatter(logging.Formatter
 
76
                       ('Mandos: %(levelname)s: %(message)s'))
 
77
logger.addHandler(syslogger)
 
78
 
 
79
console = logging.StreamHandler()
 
80
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
 
81
                                       ' %(message)s'))
 
82
logger.addHandler(console)
 
83
 
 
84
class AvahiError(Exception):
 
85
    def __init__(self, value, *args, **kwargs):
 
86
        self.value = value
 
87
        super(AvahiError, self).__init__(value, *args, **kwargs)
 
88
    def __unicode__(self):
 
89
        return unicode(repr(self.value))
 
90
 
 
91
class AvahiServiceError(AvahiError):
 
92
    pass
 
93
 
 
94
class AvahiGroupError(AvahiError):
 
95
    pass
 
96
 
 
97
 
 
98
class AvahiService(object):
 
99
    """An Avahi (Zeroconf) service.
 
100
    Attributes:
 
101
    interface: integer; avahi.IF_UNSPEC or an interface index.
 
102
               Used to optionally bind to the specified interface.
 
103
    name: string; Example: 'Mandos'
 
104
    type: string; Example: '_mandos._tcp'.
 
105
                  See <http://www.dns-sd.org/ServiceTypes.html>
 
106
    port: integer; what port to announce
 
107
    TXT: list of strings; TXT record for the service
 
108
    domain: string; Domain to publish on, default to .local if empty.
 
109
    host: string; Host to publish records for, default is localhost
 
110
    max_renames: integer; maximum number of renames
 
111
    rename_count: integer; counter so we only rename after collisions
 
112
                  a sensible number of times
 
113
    """
 
114
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
 
115
                 servicetype = None, port = None, TXT = None,
 
116
                 domain = "", host = "", max_renames = 32768):
 
117
        self.interface = interface
 
118
        self.name = name
 
119
        self.type = servicetype
 
120
        self.port = port
 
121
        self.TXT = TXT if TXT is not None else []
 
122
        self.domain = domain
 
123
        self.host = host
 
124
        self.rename_count = 0
 
125
        self.max_renames = max_renames
 
126
    def rename(self):
 
127
        """Derived from the Avahi example code"""
 
128
        if self.rename_count >= self.max_renames:
 
129
            logger.critical(u"No suitable Zeroconf service name found"
 
130
                            u" after %i retries, exiting.",
 
131
                            self.rename_count)
 
132
            raise AvahiServiceError(u"Too many renames")
 
133
        self.name = server.GetAlternativeServiceName(self.name)
 
134
        logger.info(u"Changing Zeroconf service name to %r ...",
 
135
                    str(self.name))
 
136
        syslogger.setFormatter(logging.Formatter
 
137
                               ('Mandos (%s): %%(levelname)s:'
 
138
                                ' %%(message)s' % self.name))
 
139
        self.remove()
 
140
        self.add()
 
141
        self.rename_count += 1
 
142
    def remove(self):
 
143
        """Derived from the Avahi example code"""
 
144
        if group is not None:
 
145
            group.Reset()
 
146
    def add(self):
 
147
        """Derived from the Avahi example code"""
 
148
        global group
 
149
        if group is None:
 
150
            group = dbus.Interface(bus.get_object
 
151
                                   (avahi.DBUS_NAME,
 
152
                                    server.EntryGroupNew()),
 
153
                                   avahi.DBUS_INTERFACE_ENTRY_GROUP)
 
154
            group.connect_to_signal('StateChanged',
 
155
                                    entry_group_state_changed)
 
156
        logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
 
157
                     service.name, service.type)
 
158
        group.AddService(
 
159
                self.interface,         # interface
 
160
                avahi.PROTO_INET6,      # protocol
 
161
                dbus.UInt32(0),         # flags
 
162
                self.name, self.type,
 
163
                self.domain, self.host,
 
164
                dbus.UInt16(self.port),
 
165
                avahi.string_array_to_txt_array(self.TXT))
 
166
        group.Commit()
 
167
 
 
168
# From the Avahi example code:
 
169
group = None                            # our entry group
 
170
# End of Avahi example code
 
171
 
 
172
 
 
173
def _datetime_to_dbus(dt, variant_level=0):
 
174
    """Convert a UTC datetime.datetime() to a D-Bus type."""
 
175
    return dbus.String(dt.isoformat(), variant_level=variant_level)
 
176
 
 
177
 
 
178
class Client(dbus.service.Object):
 
179
    """A representation of a client host served by this server.
 
180
    Attributes:
 
181
    name:       string; from the config file, used in log messages and
 
182
                        D-Bus identifiers
 
183
    fingerprint: string (40 or 32 hexadecimal digits); used to
 
184
                 uniquely identify the client
 
185
    secret:     bytestring; sent verbatim (over TLS) to client
 
186
    host:       string; available for use by the checker command
 
187
    created:    datetime.datetime(); (UTC) object creation
 
188
    last_enabled: datetime.datetime(); (UTC)
 
189
    enabled:    bool()
 
190
    last_checked_ok: datetime.datetime(); (UTC) or None
 
191
    timeout:    datetime.timedelta(); How long from last_checked_ok
 
192
                                      until this client is invalid
 
193
    interval:   datetime.timedelta(); How often to start a new checker
 
194
    disable_hook:  If set, called by disable() as disable_hook(self)
 
195
    checker:    subprocess.Popen(); a running checker process used
 
196
                                    to see if the client lives.
 
197
                                    'None' if no process is running.
 
198
    checker_initiator_tag: a gobject event source tag, or None
 
199
    disable_initiator_tag:    - '' -
 
200
    checker_callback_tag:  - '' -
 
201
    checker_command: string; External command which is run to check if
 
202
                     client lives.  %() expansions are done at
 
203
                     runtime with vars(self) as dict, so that for
 
204
                     instance %(name)s can be used in the command.
 
205
    use_dbus: bool(); Whether to provide D-Bus interface and signals
 
206
    dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
 
207
    """
 
208
    def timeout_milliseconds(self):
 
209
        "Return the 'timeout' attribute in milliseconds"
 
210
        return ((self.timeout.days * 24 * 60 * 60 * 1000)
 
211
                + (self.timeout.seconds * 1000)
 
212
                + (self.timeout.microseconds // 1000))
 
213
    
 
214
    def interval_milliseconds(self):
 
215
        "Return the 'interval' attribute in milliseconds"
 
216
        return ((self.interval.days * 24 * 60 * 60 * 1000)
 
217
                + (self.interval.seconds * 1000)
 
218
                + (self.interval.microseconds // 1000))
 
219
    
 
220
    def __init__(self, name = None, disable_hook=None, config=None,
 
221
                 use_dbus=True):
 
222
        """Note: the 'checker' key in 'config' sets the
 
223
        'checker_command' attribute and *not* the 'checker'
 
224
        attribute."""
 
225
        self.name = name
 
226
        if config is None:
 
227
            config = {}
 
228
        logger.debug(u"Creating client %r", self.name)
 
229
        self.use_dbus = False   # During __init__
 
230
        # Uppercase and remove spaces from fingerprint for later
 
231
        # comparison purposes with return value from the fingerprint()
 
232
        # function
 
233
        self.fingerprint = (config["fingerprint"].upper()
 
234
                            .replace(u" ", u""))
 
235
        logger.debug(u"  Fingerprint: %s", self.fingerprint)
 
236
        if "secret" in config:
 
237
            self.secret = config["secret"].decode(u"base64")
 
238
        elif "secfile" in config:
 
239
            with closing(open(os.path.expanduser
 
240
                              (os.path.expandvars
 
241
                               (config["secfile"])))) as secfile:
 
242
                self.secret = secfile.read()
24
243
        else:
25
 
            print "No Password or Passfile in client config file"
26
 
            # raise RuntimeError XXX
27
 
            self.password = "gazonk"
28
 
        self.fqdn = fqdn
29
 
        # self.created = ...
30
 
        self.last_seen = None
31
 
        if timeout is None:
32
 
            timeout = self.server.options.timeout
33
 
        self.timeout = timeout
34
 
        if interval == -1:
35
 
            interval = self.server.options.interval
36
 
        self.interval = interval
37
 
 
38
 
def server_bind(self):
39
 
    if self.options.interface:
40
 
        if not hasattr(socket, "SO_BINDTODEVICE"):
41
 
            # From /usr/include/asm-i486/socket.h
42
 
            socket.SO_BINDTODEVICE = 25
43
 
        try:
44
 
            self.socket.setsockopt(socket.SOL_SOCKET,
45
 
                                   socket.SO_BINDTODEVICE,
46
 
                                   self.options.interface)
47
 
        except socket.error, error:
48
 
            if error[0] == errno.EPERM:
49
 
                print "Warning: Denied permission to bind to interface", \
50
 
                      self.options.interface
 
244
            raise TypeError(u"No secret or secfile for client %s"
 
245
                            % self.name)
 
246
        self.host = config.get("host", "")
 
247
        self.created = datetime.datetime.utcnow()
 
248
        self.enabled = False
 
249
        self.last_enabled = None
 
250
        self.last_checked_ok = None
 
251
        self.timeout = string_to_delta(config["timeout"])
 
252
        self.interval = string_to_delta(config["interval"])
 
253
        self.disable_hook = disable_hook
 
254
        self.checker = None
 
255
        self.checker_initiator_tag = None
 
256
        self.disable_initiator_tag = None
 
257
        self.checker_callback_tag = None
 
258
        self.checker_command = config["checker"]
 
259
        self.last_connect = None
 
260
        # Only now, when this client is initialized, can it show up on
 
261
        # the D-Bus
 
262
        self.use_dbus = use_dbus
 
263
        if self.use_dbus:
 
264
            self.dbus_object_path = (dbus.ObjectPath
 
265
                                     ("/clients/"
 
266
                                      + self.name.replace(".", "_")))
 
267
            dbus.service.Object.__init__(self, bus,
 
268
                                         self.dbus_object_path)
 
269
    
 
270
    def enable(self):
 
271
        """Start this client's checker and timeout hooks"""
 
272
        self.last_enabled = datetime.datetime.utcnow()
 
273
        # Schedule a new checker to be started an 'interval' from now,
 
274
        # and every interval from then on.
 
275
        self.checker_initiator_tag = (gobject.timeout_add
 
276
                                      (self.interval_milliseconds(),
 
277
                                       self.start_checker))
 
278
        # Also start a new checker *right now*.
 
279
        self.start_checker()
 
280
        # Schedule a disable() when 'timeout' has passed
 
281
        self.disable_initiator_tag = (gobject.timeout_add
 
282
                                   (self.timeout_milliseconds(),
 
283
                                    self.disable))
 
284
        self.enabled = True
 
285
        if self.use_dbus:
 
286
            # Emit D-Bus signals
 
287
            self.PropertyChanged(dbus.String(u"enabled"),
 
288
                                 dbus.Boolean(True, variant_level=1))
 
289
            self.PropertyChanged(dbus.String(u"last_enabled"),
 
290
                                 (_datetime_to_dbus(self.last_enabled,
 
291
                                                    variant_level=1)))
 
292
    
 
293
    def disable(self):
 
294
        """Disable this client."""
 
295
        if not getattr(self, "enabled", False):
 
296
            return False
 
297
        logger.info(u"Disabling client %s", self.name)
 
298
        if getattr(self, "disable_initiator_tag", False):
 
299
            gobject.source_remove(self.disable_initiator_tag)
 
300
            self.disable_initiator_tag = None
 
301
        if getattr(self, "checker_initiator_tag", False):
 
302
            gobject.source_remove(self.checker_initiator_tag)
 
303
            self.checker_initiator_tag = None
 
304
        self.stop_checker()
 
305
        if self.disable_hook:
 
306
            self.disable_hook(self)
 
307
        self.enabled = False
 
308
        if self.use_dbus:
 
309
            # Emit D-Bus signal
 
310
            self.PropertyChanged(dbus.String(u"enabled"),
 
311
                                 dbus.Boolean(False, variant_level=1))
 
312
        # Do not run this again if called by a gobject.timeout_add
 
313
        return False
 
314
    
 
315
    def __del__(self):
 
316
        self.disable_hook = None
 
317
        self.disable()
 
318
    
 
319
    def checker_callback(self, pid, condition, command):
 
320
        """The checker has completed, so take appropriate actions."""
 
321
        self.checker_callback_tag = None
 
322
        self.checker = None
 
323
        if self.use_dbus:
 
324
            # Emit D-Bus signal
 
325
            self.PropertyChanged(dbus.String(u"checker_running"),
 
326
                                 dbus.Boolean(False, variant_level=1))
 
327
        if os.WIFEXITED(condition):
 
328
            exitstatus = os.WEXITSTATUS(condition)
 
329
            if exitstatus == 0:
 
330
                logger.info(u"Checker for %(name)s succeeded",
 
331
                            vars(self))
 
332
                self.bump_timeout()
51
333
            else:
52
 
                raise error
53
 
    return super(type(self), self).server_bind()
54
 
 
55
 
 
56
 
def init_with_options(self, *args, **kwargs):
57
 
    if "options" in kwargs:
58
 
        self.options = kwargs["options"]
59
 
        del kwargs["options"]
60
 
    if "clients" in kwargs:
61
 
        self.clients = kwargs["clients"]
62
 
        del kwargs["clients"]
63
 
    if "credentials" in kwargs:
64
 
        self.credentials = kwargs["credentials"]
65
 
        del kwargs["credentials"]
66
 
    return super(type(self), self).__init__(*args, **kwargs)
67
 
 
68
 
 
69
 
class udp_handler(SocketServer.DatagramRequestHandler, object):
70
 
    def handle(self):
71
 
        self.wfile.write("Polo")
72
 
        print "UDP request answered"
73
 
 
74
 
 
75
 
class IPv6_UDPServer(SocketServer.UDPServer, object):
76
 
    __init__ = init_with_options
77
 
    address_family = socket.AF_INET6
78
 
    allow_reuse_address = True
79
 
    server_bind = server_bind
80
 
    def verify_request(self, request, client_address):
81
 
        print "UDP request came"
82
 
        return request[0] == "Marco"
83
 
 
84
 
 
85
 
class tcp_handler(SocketServer.BaseRequestHandler, object):
86
 
    def handle(self):
87
 
        print "TCP request came"
88
 
        print "Request:", self.request
89
 
        print "Client Address:", self.client_address
90
 
        print "Server:", self.server
91
 
        session = gnutls.connection.ServerSession(self.request,
92
 
                                                  self.server.credentials)
93
 
        session.handshake()
94
 
        if session.peer_certificate:
95
 
            print "DN:", session.peer_certificate.subject
96
 
        try:
97
 
            session.verify_peer()
98
 
        except gnutls.errors.CertificateError, error:
99
 
            print "Verify failed", error
100
 
            session.bye()
101
 
            return
102
 
        try:
103
 
            session.send(dict((client.dn, client.password)
104
 
                              for client in self.server.clients)
105
 
                         [session.peer_certificate.subject])
106
 
        except KeyError:
107
 
            session.send("gazonk")
108
 
            # Log maybe? XXX
 
334
                logger.info(u"Checker for %(name)s failed",
 
335
                            vars(self))
 
336
            if self.use_dbus:
 
337
                # Emit D-Bus signal
 
338
                self.CheckerCompleted(dbus.Int16(exitstatus),
 
339
                                      dbus.Int64(condition),
 
340
                                      dbus.String(command))
 
341
        else:
 
342
            logger.warning(u"Checker for %(name)s crashed?",
 
343
                           vars(self))
 
344
            if self.use_dbus:
 
345
                # Emit D-Bus signal
 
346
                self.CheckerCompleted(dbus.Int16(-1),
 
347
                                      dbus.Int64(condition),
 
348
                                      dbus.String(command))
 
349
    
 
350
    def bump_timeout(self):
 
351
        """Bump up the timeout for this client.
 
352
        This should only be called when the client has been seen,
 
353
        alive and well.
 
354
        """
 
355
        self.last_checked_ok = datetime.datetime.utcnow()
 
356
        gobject.source_remove(self.disable_initiator_tag)
 
357
        self.disable_initiator_tag = (gobject.timeout_add
 
358
                                      (self.timeout_milliseconds(),
 
359
                                       self.disable))
 
360
        if self.use_dbus:
 
361
            # Emit D-Bus signal
 
362
            self.PropertyChanged(
 
363
                dbus.String(u"last_checked_ok"),
 
364
                (_datetime_to_dbus(self.last_checked_ok,
 
365
                                   variant_level=1)))
 
366
    
 
367
    def start_checker(self):
 
368
        """Start a new checker subprocess if one is not running.
 
369
        If a checker already exists, leave it running and do
 
370
        nothing."""
 
371
        # The reason for not killing a running checker is that if we
 
372
        # did that, then if a checker (for some reason) started
 
373
        # running slowly and taking more than 'interval' time, the
 
374
        # client would inevitably timeout, since no checker would get
 
375
        # a chance to run to completion.  If we instead leave running
 
376
        # checkers alone, the checker would have to take more time
 
377
        # than 'timeout' for the client to be declared invalid, which
 
378
        # is as it should be.
 
379
        if self.checker is None:
 
380
            try:
 
381
                # In case checker_command has exactly one % operator
 
382
                command = self.checker_command % self.host
 
383
            except TypeError:
 
384
                # Escape attributes for the shell
 
385
                escaped_attrs = dict((key, re.escape(str(val)))
 
386
                                     for key, val in
 
387
                                     vars(self).iteritems())
 
388
                try:
 
389
                    command = self.checker_command % escaped_attrs
 
390
                except TypeError, error:
 
391
                    logger.error(u'Could not format string "%s":'
 
392
                                 u' %s', self.checker_command, error)
 
393
                    return True # Try again later
 
394
            try:
 
395
                logger.info(u"Starting checker %r for %s",
 
396
                            command, self.name)
 
397
                # We don't need to redirect stdout and stderr, since
 
398
                # in normal mode, that is already done by daemon(),
 
399
                # and in debug mode we don't want to.  (Stdin is
 
400
                # always replaced by /dev/null.)
 
401
                self.checker = subprocess.Popen(command,
 
402
                                                close_fds=True,
 
403
                                                shell=True, cwd="/")
 
404
                if self.use_dbus:
 
405
                    # Emit D-Bus signal
 
406
                    self.CheckerStarted(command)
 
407
                    self.PropertyChanged(
 
408
                        dbus.String("checker_running"),
 
409
                        dbus.Boolean(True, variant_level=1))
 
410
                self.checker_callback_tag = (gobject.child_watch_add
 
411
                                             (self.checker.pid,
 
412
                                              self.checker_callback,
 
413
                                              data=command))
 
414
            except OSError, error:
 
415
                logger.error(u"Failed to start subprocess: %s",
 
416
                             error)
 
417
        # Re-run this periodically if run by gobject.timeout_add
 
418
        return True
 
419
    
 
420
    def stop_checker(self):
 
421
        """Force the checker process, if any, to stop."""
 
422
        if self.checker_callback_tag:
 
423
            gobject.source_remove(self.checker_callback_tag)
 
424
            self.checker_callback_tag = None
 
425
        if getattr(self, "checker", None) is None:
 
426
            return
 
427
        logger.debug(u"Stopping checker for %(name)s", vars(self))
 
428
        try:
 
429
            os.kill(self.checker.pid, signal.SIGTERM)
 
430
            #os.sleep(0.5)
 
431
            #if self.checker.poll() is None:
 
432
            #    os.kill(self.checker.pid, signal.SIGKILL)
 
433
        except OSError, error:
 
434
            if error.errno != errno.ESRCH: # No such process
 
435
                raise
 
436
        self.checker = None
 
437
        if self.use_dbus:
 
438
            self.PropertyChanged(dbus.String(u"checker_running"),
 
439
                                 dbus.Boolean(False, variant_level=1))
 
440
    
 
441
    def still_valid(self):
 
442
        """Has the timeout not yet passed for this client?"""
 
443
        if not getattr(self, "enabled", False):
 
444
            return False
 
445
        now = datetime.datetime.utcnow()
 
446
        if self.last_checked_ok is None:
 
447
            return now < (self.created + self.timeout)
 
448
        else:
 
449
            return now < (self.last_checked_ok + self.timeout)
 
450
    
 
451
    ## D-Bus methods & signals
 
452
    _interface = u"se.bsnet.fukt.Mandos.Client"
 
453
    
 
454
    # BumpTimeout - method
 
455
    BumpTimeout = dbus.service.method(_interface)(bump_timeout)
 
456
    BumpTimeout.__name__ = "BumpTimeout"
 
457
    
 
458
    # CheckerCompleted - signal
 
459
    @dbus.service.signal(_interface, signature="nxs")
 
460
    def CheckerCompleted(self, exitcode, waitstatus, command):
 
461
        "D-Bus signal"
 
462
        pass
 
463
    
 
464
    # CheckerStarted - signal
 
465
    @dbus.service.signal(_interface, signature="s")
 
466
    def CheckerStarted(self, command):
 
467
        "D-Bus signal"
 
468
        pass
 
469
    
 
470
    # GetAllProperties - method
 
471
    @dbus.service.method(_interface, out_signature="a{sv}")
 
472
    def GetAllProperties(self):
 
473
        "D-Bus method"
 
474
        return dbus.Dictionary({
 
475
                dbus.String("name"):
 
476
                    dbus.String(self.name, variant_level=1),
 
477
                dbus.String("fingerprint"):
 
478
                    dbus.String(self.fingerprint, variant_level=1),
 
479
                dbus.String("host"):
 
480
                    dbus.String(self.host, variant_level=1),
 
481
                dbus.String("created"):
 
482
                    _datetime_to_dbus(self.created, variant_level=1),
 
483
                dbus.String("last_enabled"):
 
484
                    (_datetime_to_dbus(self.last_enabled,
 
485
                                       variant_level=1)
 
486
                     if self.last_enabled is not None
 
487
                     else dbus.Boolean(False, variant_level=1)),
 
488
                dbus.String("enabled"):
 
489
                    dbus.Boolean(self.enabled, variant_level=1),
 
490
                dbus.String("last_checked_ok"):
 
491
                    (_datetime_to_dbus(self.last_checked_ok,
 
492
                                       variant_level=1)
 
493
                     if self.last_checked_ok is not None
 
494
                     else dbus.Boolean (False, variant_level=1)),
 
495
                dbus.String("timeout"):
 
496
                    dbus.UInt64(self.timeout_milliseconds(),
 
497
                                variant_level=1),
 
498
                dbus.String("interval"):
 
499
                    dbus.UInt64(self.interval_milliseconds(),
 
500
                                variant_level=1),
 
501
                dbus.String("checker"):
 
502
                    dbus.String(self.checker_command,
 
503
                                variant_level=1),
 
504
                dbus.String("checker_running"):
 
505
                    dbus.Boolean(self.checker is not None,
 
506
                                 variant_level=1),
 
507
                dbus.String("object_path"):
 
508
                    dbus.ObjectPath(self.dbus_object_path,
 
509
                                    variant_level=1)
 
510
                }, signature="sv")
 
511
    
 
512
    # IsStillValid - method
 
513
    IsStillValid = (dbus.service.method(_interface, out_signature="b")
 
514
                    (still_valid))
 
515
    IsStillValid.__name__ = "IsStillValid"
 
516
    
 
517
    # PropertyChanged - signal
 
518
    @dbus.service.signal(_interface, signature="sv")
 
519
    def PropertyChanged(self, property, value):
 
520
        "D-Bus signal"
 
521
        pass
 
522
    
 
523
    # SetChecker - method
 
524
    @dbus.service.method(_interface, in_signature="s")
 
525
    def SetChecker(self, checker):
 
526
        "D-Bus setter method"
 
527
        self.checker_command = checker
 
528
        # Emit D-Bus signal
 
529
        self.PropertyChanged(dbus.String(u"checker"),
 
530
                             dbus.String(self.checker_command,
 
531
                                         variant_level=1))
 
532
    
 
533
    # SetHost - method
 
534
    @dbus.service.method(_interface, in_signature="s")
 
535
    def SetHost(self, host):
 
536
        "D-Bus setter method"
 
537
        self.host = host
 
538
        # Emit D-Bus signal
 
539
        self.PropertyChanged(dbus.String(u"host"),
 
540
                             dbus.String(self.host, variant_level=1))
 
541
    
 
542
    # SetInterval - method
 
543
    @dbus.service.method(_interface, in_signature="t")
 
544
    def SetInterval(self, milliseconds):
 
545
        self.interval = datetime.timedelta(0, 0, 0, milliseconds)
 
546
        # Emit D-Bus signal
 
547
        self.PropertyChanged(dbus.String(u"interval"),
 
548
                             (dbus.UInt64(self.interval_milliseconds(),
 
549
                                          variant_level=1)))
 
550
    
 
551
    # SetSecret - method
 
552
    @dbus.service.method(_interface, in_signature="ay",
 
553
                         byte_arrays=True)
 
554
    def SetSecret(self, secret):
 
555
        "D-Bus setter method"
 
556
        self.secret = str(secret)
 
557
    
 
558
    # SetTimeout - method
 
559
    @dbus.service.method(_interface, in_signature="t")
 
560
    def SetTimeout(self, milliseconds):
 
561
        self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
 
562
        # Emit D-Bus signal
 
563
        self.PropertyChanged(dbus.String(u"timeout"),
 
564
                             (dbus.UInt64(self.timeout_milliseconds(),
 
565
                                          variant_level=1)))
 
566
    
 
567
    # Enable - method
 
568
    Enable = dbus.service.method(_interface)(enable)
 
569
    Enable.__name__ = "Enable"
 
570
    
 
571
    # StartChecker - method
 
572
    @dbus.service.method(_interface)
 
573
    def StartChecker(self):
 
574
        "D-Bus method"
 
575
        self.start_checker()
 
576
    
 
577
    # Disable - method
 
578
    @dbus.service.method(_interface)
 
579
    def Disable(self):
 
580
        "D-Bus method"
 
581
        self.disable()
 
582
    
 
583
    # StopChecker - method
 
584
    StopChecker = dbus.service.method(_interface)(stop_checker)
 
585
    StopChecker.__name__ = "StopChecker"
 
586
    
 
587
    del _interface
 
588
 
 
589
 
 
590
def peer_certificate(session):
 
591
    "Return the peer's OpenPGP certificate as a bytestring"
 
592
    # If not an OpenPGP certificate...
 
593
    if (gnutls.library.functions
 
594
        .gnutls_certificate_type_get(session._c_object)
 
595
        != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
 
596
        # ...do the normal thing
 
597
        return session.peer_certificate
 
598
    list_size = ctypes.c_uint()
 
599
    cert_list = (gnutls.library.functions
 
600
                 .gnutls_certificate_get_peers
 
601
                 (session._c_object, ctypes.byref(list_size)))
 
602
    if list_size.value == 0:
 
603
        return None
 
604
    cert = cert_list[0]
 
605
    return ctypes.string_at(cert.data, cert.size)
 
606
 
 
607
 
 
608
def fingerprint(openpgp):
 
609
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
 
610
    # New GnuTLS "datum" with the OpenPGP public key
 
611
    datum = (gnutls.library.types
 
612
             .gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
 
613
                                         ctypes.POINTER
 
614
                                         (ctypes.c_ubyte)),
 
615
                             ctypes.c_uint(len(openpgp))))
 
616
    # New empty GnuTLS certificate
 
617
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
 
618
    (gnutls.library.functions
 
619
     .gnutls_openpgp_crt_init(ctypes.byref(crt)))
 
620
    # Import the OpenPGP public key into the certificate
 
621
    (gnutls.library.functions
 
622
     .gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
 
623
                                gnutls.library.constants
 
624
                                .GNUTLS_OPENPGP_FMT_RAW))
 
625
    # Verify the self signature in the key
 
626
    crtverify = ctypes.c_uint()
 
627
    (gnutls.library.functions
 
628
     .gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
 
629
    if crtverify.value != 0:
 
630
        gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
631
        raise gnutls.errors.CertificateSecurityError("Verify failed")
 
632
    # New buffer for the fingerprint
 
633
    buf = ctypes.create_string_buffer(20)
 
634
    buf_len = ctypes.c_size_t()
 
635
    # Get the fingerprint from the certificate into the buffer
 
636
    (gnutls.library.functions
 
637
     .gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
 
638
                                         ctypes.byref(buf_len)))
 
639
    # Deinit the certificate
 
640
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
641
    # Convert the buffer to a Python bytestring
 
642
    fpr = ctypes.string_at(buf, buf_len.value)
 
643
    # Convert the bytestring to hexadecimal notation
 
644
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
 
645
    return hex_fpr
 
646
 
 
647
 
 
648
class TCP_handler(SocketServer.BaseRequestHandler, object):
 
649
    """A TCP request handler class.
 
650
    Instantiated by IPv6_TCPServer for each request to handle it.
 
651
    Note: This will run in its own forked process."""
 
652
    
 
653
    def handle(self):
 
654
        logger.info(u"TCP connection from: %s",
 
655
                    unicode(self.client_address))
 
656
        session = (gnutls.connection
 
657
                   .ClientSession(self.request,
 
658
                                  gnutls.connection
 
659
                                  .X509Credentials()))
 
660
        
 
661
        line = self.request.makefile().readline()
 
662
        logger.debug(u"Protocol version: %r", line)
 
663
        try:
 
664
            if int(line.strip().split()[0]) > 1:
 
665
                raise RuntimeError
 
666
        except (ValueError, IndexError, RuntimeError), error:
 
667
            logger.error(u"Unknown protocol version: %s", error)
 
668
            return
 
669
        
 
670
        # Note: gnutls.connection.X509Credentials is really a generic
 
671
        # GnuTLS certificate credentials object so long as no X.509
 
672
        # keys are added to it.  Therefore, we can use it here despite
 
673
        # using OpenPGP certificates.
 
674
        
 
675
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
 
676
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
 
677
        #                "+DHE-DSS"))
 
678
        # Use a fallback default, since this MUST be set.
 
679
        priority = self.server.settings.get("priority", "NORMAL")
 
680
        (gnutls.library.functions
 
681
         .gnutls_priority_set_direct(session._c_object,
 
682
                                     priority, None))
 
683
        
 
684
        try:
 
685
            session.handshake()
 
686
        except gnutls.errors.GNUTLSError, error:
 
687
            logger.warning(u"Handshake failed: %s", error)
 
688
            # Do not run session.bye() here: the session is not
 
689
            # established.  Just abandon the request.
 
690
            return
 
691
        try:
 
692
            fpr = fingerprint(peer_certificate(session))
 
693
        except (TypeError, gnutls.errors.GNUTLSError), error:
 
694
            logger.warning(u"Bad certificate: %s", error)
 
695
            session.bye()
 
696
            return
 
697
        logger.debug(u"Fingerprint: %s", fpr)
 
698
        for c in self.server.clients:
 
699
            if c.fingerprint == fpr:
 
700
                client = c
 
701
                break
 
702
        else:
 
703
            logger.warning(u"Client not found for fingerprint: %s",
 
704
                           fpr)
 
705
            session.bye()
 
706
            return
 
707
        # Have to check if client.still_valid(), since it is possible
 
708
        # that the client timed out while establishing the GnuTLS
 
709
        # session.
 
710
        if not client.still_valid():
 
711
            logger.warning(u"Client %(name)s is invalid",
 
712
                           vars(client))
 
713
            session.bye()
 
714
            return
 
715
        ## This won't work here, since we're in a fork.
 
716
        # client.bump_timeout()
 
717
        sent_size = 0
 
718
        while sent_size < len(client.secret):
 
719
            sent = session.send(client.secret[sent_size:])
 
720
            logger.debug(u"Sent: %d, remaining: %d",
 
721
                         sent, len(client.secret)
 
722
                         - (sent_size + sent))
 
723
            sent_size += sent
109
724
        session.bye()
110
725
 
111
 
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
112
 
    __init__ = init_with_options
 
726
 
 
727
class IPv6_TCPServer(SocketServer.ForkingMixIn,
 
728
                     SocketServer.TCPServer, object):
 
729
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
 
730
    Attributes:
 
731
        settings:       Server settings
 
732
        clients:        Set() of Client objects
 
733
        enabled:        Boolean; whether this server is activated yet
 
734
    """
113
735
    address_family = socket.AF_INET6
114
 
    allow_reuse_address = True
115
 
    request_queue_size = 1024
116
 
    server_bind = server_bind
117
 
 
118
 
 
119
 
in6addr_any = "::"
120
 
 
121
 
cred = None
 
736
    def __init__(self, *args, **kwargs):
 
737
        if "settings" in kwargs:
 
738
            self.settings = kwargs["settings"]
 
739
            del kwargs["settings"]
 
740
        if "clients" in kwargs:
 
741
            self.clients = kwargs["clients"]
 
742
            del kwargs["clients"]
 
743
        self.enabled = False
 
744
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
 
745
    def server_bind(self):
 
746
        """This overrides the normal server_bind() function
 
747
        to bind to an interface if one was specified, and also NOT to
 
748
        bind to an address or port if they were not specified."""
 
749
        if self.settings["interface"]:
 
750
            # 25 is from /usr/include/asm-i486/socket.h
 
751
            SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
 
752
            try:
 
753
                self.socket.setsockopt(socket.SOL_SOCKET,
 
754
                                       SO_BINDTODEVICE,
 
755
                                       self.settings["interface"])
 
756
            except socket.error, error:
 
757
                if error[0] == errno.EPERM:
 
758
                    logger.error(u"No permission to"
 
759
                                 u" bind to interface %s",
 
760
                                 self.settings["interface"])
 
761
                else:
 
762
                    raise error
 
763
        # Only bind(2) the socket if we really need to.
 
764
        if self.server_address[0] or self.server_address[1]:
 
765
            if not self.server_address[0]:
 
766
                in6addr_any = "::"
 
767
                self.server_address = (in6addr_any,
 
768
                                       self.server_address[1])
 
769
            elif not self.server_address[1]:
 
770
                self.server_address = (self.server_address[0],
 
771
                                       0)
 
772
#                 if self.settings["interface"]:
 
773
#                     self.server_address = (self.server_address[0],
 
774
#                                            0, # port
 
775
#                                            0, # flowinfo
 
776
#                                            if_nametoindex
 
777
#                                            (self.settings
 
778
#                                             ["interface"]))
 
779
            return super(IPv6_TCPServer, self).server_bind()
 
780
    def server_activate(self):
 
781
        if self.enabled:
 
782
            return super(IPv6_TCPServer, self).server_activate()
 
783
    def enable(self):
 
784
        self.enabled = True
 
785
 
 
786
 
 
787
def string_to_delta(interval):
 
788
    """Parse a string and return a datetime.timedelta
 
789
 
 
790
    >>> string_to_delta('7d')
 
791
    datetime.timedelta(7)
 
792
    >>> string_to_delta('60s')
 
793
    datetime.timedelta(0, 60)
 
794
    >>> string_to_delta('60m')
 
795
    datetime.timedelta(0, 3600)
 
796
    >>> string_to_delta('24h')
 
797
    datetime.timedelta(1)
 
798
    >>> string_to_delta(u'1w')
 
799
    datetime.timedelta(7)
 
800
    >>> string_to_delta('5m 30s')
 
801
    datetime.timedelta(0, 330)
 
802
    """
 
803
    timevalue = datetime.timedelta(0)
 
804
    for s in interval.split():
 
805
        try:
 
806
            suffix = unicode(s[-1])
 
807
            value = int(s[:-1])
 
808
            if suffix == u"d":
 
809
                delta = datetime.timedelta(value)
 
810
            elif suffix == u"s":
 
811
                delta = datetime.timedelta(0, value)
 
812
            elif suffix == u"m":
 
813
                delta = datetime.timedelta(0, 0, 0, 0, value)
 
814
            elif suffix == u"h":
 
815
                delta = datetime.timedelta(0, 0, 0, 0, 0, value)
 
816
            elif suffix == u"w":
 
817
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
 
818
            else:
 
819
                raise ValueError
 
820
        except (ValueError, IndexError):
 
821
            raise ValueError
 
822
        timevalue += delta
 
823
    return timevalue
 
824
 
 
825
 
 
826
def server_state_changed(state):
 
827
    """Derived from the Avahi example code"""
 
828
    if state == avahi.SERVER_COLLISION:
 
829
        logger.error(u"Zeroconf server name collision")
 
830
        service.remove()
 
831
    elif state == avahi.SERVER_RUNNING:
 
832
        service.add()
 
833
 
 
834
 
 
835
def entry_group_state_changed(state, error):
 
836
    """Derived from the Avahi example code"""
 
837
    logger.debug(u"Avahi state change: %i", state)
 
838
    
 
839
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
 
840
        logger.debug(u"Zeroconf service established.")
 
841
    elif state == avahi.ENTRY_GROUP_COLLISION:
 
842
        logger.warning(u"Zeroconf service name collision.")
 
843
        service.rename()
 
844
    elif state == avahi.ENTRY_GROUP_FAILURE:
 
845
        logger.critical(u"Avahi: Error in group state changed %s",
 
846
                        unicode(error))
 
847
        raise AvahiGroupError(u"State changed: %s" % unicode(error))
 
848
 
 
849
def if_nametoindex(interface):
 
850
    """Call the C function if_nametoindex(), or equivalent"""
 
851
    global if_nametoindex
 
852
    try:
 
853
        if_nametoindex = (ctypes.cdll.LoadLibrary
 
854
                          (ctypes.util.find_library("c"))
 
855
                          .if_nametoindex)
 
856
    except (OSError, AttributeError):
 
857
        if "struct" not in sys.modules:
 
858
            import struct
 
859
        if "fcntl" not in sys.modules:
 
860
            import fcntl
 
861
        def if_nametoindex(interface):
 
862
            "Get an interface index the hard way, i.e. using fcntl()"
 
863
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
 
864
            with closing(socket.socket()) as s:
 
865
                ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
 
866
                                    struct.pack("16s16x", interface))
 
867
            interface_index = struct.unpack("I", ifreq[16:20])[0]
 
868
            return interface_index
 
869
    return if_nametoindex(interface)
 
870
 
 
871
 
 
872
def daemon(nochdir = False, noclose = False):
 
873
    """See daemon(3).  Standard BSD Unix function.
 
874
    This should really exist as os.daemon, but it doesn't (yet)."""
 
875
    if os.fork():
 
876
        sys.exit()
 
877
    os.setsid()
 
878
    if not nochdir:
 
879
        os.chdir("/")
 
880
    if os.fork():
 
881
        sys.exit()
 
882
    if not noclose:
 
883
        # Close all standard open file descriptors
 
884
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
885
        if not stat.S_ISCHR(os.fstat(null).st_mode):
 
886
            raise OSError(errno.ENODEV,
 
887
                          "/dev/null not a character device")
 
888
        os.dup2(null, sys.stdin.fileno())
 
889
        os.dup2(null, sys.stdout.fileno())
 
890
        os.dup2(null, sys.stderr.fileno())
 
891
        if null > 2:
 
892
            os.close(null)
 
893
 
122
894
 
123
895
def main():
124
 
    parser = OptionParser()
 
896
    parser = optparse.OptionParser(version = "%%prog %s" % version)
125
897
    parser.add_option("-i", "--interface", type="string",
126
 
                      default="eth0", metavar="IF",
127
 
                      help="Interface to bind to")
128
 
    parser.add_option("--cert", type="string", default="cert.pem",
129
 
                      metavar="FILE",
130
 
                      help="Public key certificate to use")
131
 
    parser.add_option("--key", type="string", default="key.pem",
132
 
                      metavar="FILE",
133
 
                      help="Private key to use")
134
 
    parser.add_option("--ca", type="string", default="ca.pem",
135
 
                      metavar="FILE",
136
 
                      help="Certificate Authority certificate to use")
137
 
    parser.add_option("--crl", type="string", default="crl.pem",
138
 
                      metavar="FILE",
139
 
                      help="Certificate Revokation List to use")
140
 
    parser.add_option("-p", "--port", type="int", default=49001,
 
898
                      metavar="IF", help="Bind to interface IF")
 
899
    parser.add_option("-a", "--address", type="string",
 
900
                      help="Address to listen for requests on")
 
901
    parser.add_option("-p", "--port", type="int",
141
902
                      help="Port number to receive requests on")
142
 
    parser.add_option("--dh", type="int", metavar="BITS",
143
 
                      help="DH group to use")
144
 
    parser.add_option("-t", "--timeout", type="string", # Parsed later
145
 
                      default="15m",
146
 
                      help="Amount of downtime allowed for clients")
147
 
    (options, args) = parser.parse_args()
148
 
    
149
 
    # Parse the time argument
150
 
    try:
151
 
        suffix=options.timeout[-1]
152
 
        value=int(options.timeout[:-1])
153
 
        if suffix == "d":
154
 
            options.timeout = datetime.timedelta(value)
155
 
        elif suffix == "s":
156
 
            options.timeout = datetime.timedelta(0, value)
157
 
        elif suffix == "m":
158
 
            options.timeout = datetime.timedelta(0, 0, 0, 0, value)
159
 
        elif suffix == "h":
160
 
            options.timeout = datetime.timedelta(0, 0, 0, 0, 0, value)
161
 
        elif suffix == "w":
162
 
            options.timeout = datetime.timedelta(0, 0, 0, 0, 0, 0,
163
 
                                                 value)
164
 
        else:
165
 
            raise ValueError
166
 
    except (ValueError, IndexError):
167
 
        parser.error("option --timeout: Unparseable time")
168
 
    
169
 
    cert = gnutls.crypto.X509Certificate(open(options.cert).read())
170
 
    key = gnutls.crypto.X509PrivateKey(open(options.key).read())
171
 
    ca = gnutls.crypto.X509Certificate(open(options.ca).read())
172
 
    crl = gnutls.crypto.X509CRL(open(options.crl).read())
173
 
    cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
174
 
    
175
 
    # Parse config file
176
 
    defaults = {}
177
 
    client_config_object = ConfigParser.SafeConfigParser(defaults)
178
 
    client_config_object.read("mandos-clients.conf")
179
 
    clients = [Client(name=section,
180
 
                      **(dict(client_config_object.items(section))))
181
 
               for section in client_config_object.sections()]
182
 
    
183
 
    udp_server = IPv6_UDPServer((in6addr_any, options.port),
184
 
                                udp_handler,
185
 
                                options=options)
186
 
    
187
 
    tcp_server = IPv6_TCPServer((in6addr_any, options.port),
188
 
                                tcp_handler,
189
 
                                options=options,
190
 
                                clients=clients,
191
 
                                credentials=cred)
192
 
    
193
 
    while True:
194
 
        in_, out, err = select.select((udp_server,
195
 
                                       tcp_server), (), ())
196
 
        for server in in_:
197
 
            server.handle_request()
198
 
 
199
 
 
200
 
if __name__ == "__main__":
 
903
    parser.add_option("--check", action="store_true",
 
904
                      help="Run self-test")
 
905
    parser.add_option("--debug", action="store_true",
 
906
                      help="Debug mode; run in foreground and log to"
 
907
                      " terminal")
 
908
    parser.add_option("--priority", type="string", help="GnuTLS"
 
909
                      " priority string (see GnuTLS documentation)")
 
910
    parser.add_option("--servicename", type="string", metavar="NAME",
 
911
                      help="Zeroconf service name")
 
912
    parser.add_option("--configdir", type="string",
 
913
                      default="/etc/mandos", metavar="DIR",
 
914
                      help="Directory to search for configuration"
 
915
                      " files")
 
916
    parser.add_option("--no-dbus", action="store_false",
 
917
                      dest="use_dbus",
 
918
                      help="Do not provide D-Bus system bus"
 
919
                      " interface")
 
920
    options = parser.parse_args()[0]
 
921
    
 
922
    if options.check:
 
923
        import doctest
 
924
        doctest.testmod()
 
925
        sys.exit()
 
926
    
 
927
    # Default values for config file for server-global settings
 
928
    server_defaults = { "interface": "",
 
929
                        "address": "",
 
930
                        "port": "",
 
931
                        "debug": "False",
 
932
                        "priority":
 
933
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
 
934
                        "servicename": "Mandos",
 
935
                        "use_dbus": "True",
 
936
                        }
 
937
    
 
938
    # Parse config file for server-global settings
 
939
    server_config = ConfigParser.SafeConfigParser(server_defaults)
 
940
    del server_defaults
 
941
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
 
942
    # Convert the SafeConfigParser object to a dict
 
943
    server_settings = server_config.defaults()
 
944
    # Use getboolean on the boolean config options
 
945
    server_settings["debug"] = (server_config.getboolean
 
946
                                ("DEFAULT", "debug"))
 
947
    server_settings["use_dbus"] = (server_config.getboolean
 
948
                                   ("DEFAULT", "use_dbus"))
 
949
    del server_config
 
950
    
 
951
    # Override the settings from the config file with command line
 
952
    # options, if set.
 
953
    for option in ("interface", "address", "port", "debug",
 
954
                   "priority", "servicename", "configdir",
 
955
                   "use_dbus"):
 
956
        value = getattr(options, option)
 
957
        if value is not None:
 
958
            server_settings[option] = value
 
959
    del options
 
960
    # Now we have our good server settings in "server_settings"
 
961
    
 
962
    # For convenience
 
963
    debug = server_settings["debug"]
 
964
    use_dbus = server_settings["use_dbus"]
 
965
    
 
966
    if not debug:
 
967
        syslogger.setLevel(logging.WARNING)
 
968
        console.setLevel(logging.WARNING)
 
969
    
 
970
    if server_settings["servicename"] != "Mandos":
 
971
        syslogger.setFormatter(logging.Formatter
 
972
                               ('Mandos (%s): %%(levelname)s:'
 
973
                                ' %%(message)s'
 
974
                                % server_settings["servicename"]))
 
975
    
 
976
    # Parse config file with clients
 
977
    client_defaults = { "timeout": "1h",
 
978
                        "interval": "5m",
 
979
                        "checker": "fping -q -- %%(host)s",
 
980
                        "host": "",
 
981
                        }
 
982
    client_config = ConfigParser.SafeConfigParser(client_defaults)
 
983
    client_config.read(os.path.join(server_settings["configdir"],
 
984
                                    "clients.conf"))
 
985
    
 
986
    clients = Set()
 
987
    tcp_server = IPv6_TCPServer((server_settings["address"],
 
988
                                 server_settings["port"]),
 
989
                                TCP_handler,
 
990
                                settings=server_settings,
 
991
                                clients=clients)
 
992
    pidfilename = "/var/run/mandos.pid"
 
993
    try:
 
994
        pidfile = open(pidfilename, "w")
 
995
    except IOError, error:
 
996
        logger.error("Could not open file %r", pidfilename)
 
997
    
 
998
    try:
 
999
        uid = pwd.getpwnam("_mandos").pw_uid
 
1000
        gid = pwd.getpwnam("_mandos").pw_gid
 
1001
    except KeyError:
 
1002
        try:
 
1003
            uid = pwd.getpwnam("mandos").pw_uid
 
1004
            gid = pwd.getpwnam("mandos").pw_gid
 
1005
        except KeyError:
 
1006
            try:
 
1007
                uid = pwd.getpwnam("nobody").pw_uid
 
1008
                gid = pwd.getpwnam("nogroup").pw_gid
 
1009
            except KeyError:
 
1010
                uid = 65534
 
1011
                gid = 65534
 
1012
    try:
 
1013
        os.setuid(uid)
 
1014
        os.setgid(gid)
 
1015
    except OSError, error:
 
1016
        if error[0] != errno.EPERM:
 
1017
            raise error
 
1018
    
 
1019
    global service
 
1020
    service = AvahiService(name = server_settings["servicename"],
 
1021
                           servicetype = "_mandos._tcp", )
 
1022
    if server_settings["interface"]:
 
1023
        service.interface = (if_nametoindex
 
1024
                             (server_settings["interface"]))
 
1025
    
 
1026
    global main_loop
 
1027
    global bus
 
1028
    global server
 
1029
    # From the Avahi example code
 
1030
    DBusGMainLoop(set_as_default=True )
 
1031
    main_loop = gobject.MainLoop()
 
1032
    bus = dbus.SystemBus()
 
1033
    server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
 
1034
                                           avahi.DBUS_PATH_SERVER),
 
1035
                            avahi.DBUS_INTERFACE_SERVER)
 
1036
    # End of Avahi example code
 
1037
    if use_dbus:
 
1038
        bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
 
1039
    
 
1040
    clients.update(Set(Client(name = section,
 
1041
                              config
 
1042
                              = dict(client_config.items(section)),
 
1043
                              use_dbus = use_dbus)
 
1044
                       for section in client_config.sections()))
 
1045
    if not clients:
 
1046
        logger.warning(u"No clients defined")
 
1047
    
 
1048
    if debug:
 
1049
        # Redirect stdin so all checkers get /dev/null
 
1050
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
1051
        os.dup2(null, sys.stdin.fileno())
 
1052
        if null > 2:
 
1053
            os.close(null)
 
1054
    else:
 
1055
        # No console logging
 
1056
        logger.removeHandler(console)
 
1057
        # Close all input and output, do double fork, etc.
 
1058
        daemon()
 
1059
    
 
1060
    try:
 
1061
        pid = os.getpid()
 
1062
        pidfile.write(str(pid) + "\n")
 
1063
        pidfile.close()
 
1064
        del pidfile
 
1065
    except IOError:
 
1066
        logger.error(u"Could not write to file %r with PID %d",
 
1067
                     pidfilename, pid)
 
1068
    except NameError:
 
1069
        # "pidfile" was never created
 
1070
        pass
 
1071
    del pidfilename
 
1072
    
 
1073
    def cleanup():
 
1074
        "Cleanup function; run on exit"
 
1075
        global group
 
1076
        # From the Avahi example code
 
1077
        if not group is None:
 
1078
            group.Free()
 
1079
            group = None
 
1080
        # End of Avahi example code
 
1081
        
 
1082
        while clients:
 
1083
            client = clients.pop()
 
1084
            client.disable_hook = None
 
1085
            client.disable()
 
1086
    
 
1087
    atexit.register(cleanup)
 
1088
    
 
1089
    if not debug:
 
1090
        signal.signal(signal.SIGINT, signal.SIG_IGN)
 
1091
    signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
 
1092
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
 
1093
    
 
1094
    if use_dbus:
 
1095
        class MandosServer(dbus.service.Object):
 
1096
            """A D-Bus proxy object"""
 
1097
            def __init__(self):
 
1098
                dbus.service.Object.__init__(self, bus, "/")
 
1099
            _interface = u"se.bsnet.fukt.Mandos"
 
1100
            
 
1101
            @dbus.service.signal(_interface, signature="oa{sv}")
 
1102
            def ClientAdded(self, objpath, properties):
 
1103
                "D-Bus signal"
 
1104
                pass
 
1105
            
 
1106
            @dbus.service.signal(_interface, signature="os")
 
1107
            def ClientRemoved(self, objpath, name):
 
1108
                "D-Bus signal"
 
1109
                pass
 
1110
            
 
1111
            @dbus.service.method(_interface, out_signature="ao")
 
1112
            def GetAllClients(self):
 
1113
                return dbus.Array(c.dbus_object_path for c in clients)
 
1114
            
 
1115
            @dbus.service.method(_interface, out_signature="a{oa{sv}}")
 
1116
            def GetAllClientsWithProperties(self):
 
1117
                return dbus.Dictionary(
 
1118
                    ((c.dbus_object_path, c.GetAllProperties())
 
1119
                     for c in clients),
 
1120
                    signature="oa{sv}")
 
1121
            
 
1122
            @dbus.service.method(_interface, in_signature="o")
 
1123
            def RemoveClient(self, object_path):
 
1124
                for c in clients:
 
1125
                    if c.dbus_object_path == object_path:
 
1126
                        clients.remove(c)
 
1127
                        # Don't signal anything except ClientRemoved
 
1128
                        c.use_dbus = False
 
1129
                        c.disable()
 
1130
                        # Emit D-Bus signal
 
1131
                        self.ClientRemoved(object_path, c.name)
 
1132
                        return
 
1133
                raise KeyError
 
1134
            
 
1135
            del _interface
 
1136
        
 
1137
        mandos_server = MandosServer()
 
1138
    
 
1139
    for client in clients:
 
1140
        if use_dbus:
 
1141
            # Emit D-Bus signal
 
1142
            mandos_server.ClientAdded(client.dbus_object_path,
 
1143
                                      client.GetAllProperties())
 
1144
        client.enable()
 
1145
    
 
1146
    tcp_server.enable()
 
1147
    tcp_server.server_activate()
 
1148
    
 
1149
    # Find out what port we got
 
1150
    service.port = tcp_server.socket.getsockname()[1]
 
1151
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
 
1152
                u" scope_id %d" % tcp_server.socket.getsockname())
 
1153
    
 
1154
    #service.interface = tcp_server.socket.getsockname()[3]
 
1155
    
 
1156
    try:
 
1157
        # From the Avahi example code
 
1158
        server.connect_to_signal("StateChanged", server_state_changed)
 
1159
        try:
 
1160
            server_state_changed(server.GetState())
 
1161
        except dbus.exceptions.DBusException, error:
 
1162
            logger.critical(u"DBusException: %s", error)
 
1163
            sys.exit(1)
 
1164
        # End of Avahi example code
 
1165
        
 
1166
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
 
1167
                             lambda *args, **kwargs:
 
1168
                             (tcp_server.handle_request
 
1169
                              (*args[2:], **kwargs) or True))
 
1170
        
 
1171
        logger.debug(u"Starting main loop")
 
1172
        main_loop.run()
 
1173
    except AvahiError, error:
 
1174
        logger.critical(u"AvahiError: %s", error)
 
1175
        sys.exit(1)
 
1176
    except KeyboardInterrupt:
 
1177
        if debug:
 
1178
            print
 
1179
 
 
1180
if __name__ == '__main__':
201
1181
    main()
202