/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 server.py

  • Committer: Teddy Hogeborn
  • Date: 2008-07-21 04:42:08 UTC
  • mfrom: (15.1.3 mandos)
  • Revision ID: teddy@fukt.bsnet.se-20080721044208-y8v1sjmvalfiehrv
Merge.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
# following functions: "add_service", "remove_service",
 
10
# "server_state_changed", "entry_group_state_changed", and some lines
 
11
# in "main".
 
12
 
13
# Everything else is Copyright © 2007-2008 Teddy Hogeborn and Björn
 
14
# Påhlsson.
 
15
 
16
# This program is free software: you can redistribute it and/or modify
 
17
# it under the terms of the GNU General Public License as published by
 
18
# the Free Software Foundation, either version 3 of the License, or
 
19
# (at your option) any later version.
 
20
#
 
21
#     This program is distributed in the hope that it will be useful,
 
22
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
 
23
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
24
#     GNU General Public License for more details.
 
25
 
26
# 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
 
36
import socket
 
37
import select
 
38
from optparse import OptionParser
 
39
import datetime
 
40
import errno
 
41
import gnutls.crypto
 
42
import gnutls.connection
 
43
import gnutls.errors
 
44
import gnutls.library.functions
 
45
import gnutls.library.constants
 
46
import gnutls.library.types
 
47
import ConfigParser
 
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
 
 
59
import dbus
 
60
import gobject
 
61
import avahi
 
62
from dbus.mainloop.glib import DBusGMainLoop
 
63
import ctypes
 
64
 
 
65
# Brief description of the operation of this program:
 
66
 
67
# This server announces itself as a Zeroconf service.  Connecting
 
68
# clients use the TLS protocol, with the unusual quirk that this
 
69
# server program acts as a TLS "client" while the connecting clients
 
70
# acts as a TLS "server".  The clients (acting as a TLS "server") must
 
71
# supply an OpenPGP certificate, and the fingerprint of this
 
72
# certificate is used by this server to look up (in a list read from a
 
73
# file at start time) which binary blob to give the client.  No other
 
74
# authentication or authorization is done by this server.
 
75
 
 
76
 
 
77
logger = logging.Logger('mandos')
 
78
syslogger = logging.handlers.SysLogHandler\
 
79
            (facility = logging.handlers.SysLogHandler.LOG_DAEMON)
 
80
syslogger.setFormatter(logging.Formatter\
 
81
                        ('%(levelname)s: %(message)s'))
 
82
logger.addHandler(syslogger)
 
83
del syslogger
 
84
 
 
85
# This variable is used to optionally bind to a specified interface.
 
86
# It is a global variable to fit in with the other variables from the
 
87
# Avahi example code.
 
88
serviceInterface = avahi.IF_UNSPEC
 
89
# From the Avahi example code:
 
90
serviceName = "Mandos"
 
91
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
 
92
servicePort = None                      # Not known at startup
 
93
serviceTXT = []                         # TXT record for the service
 
94
domain = ""                  # Domain to publish on, default to .local
 
95
host = ""          # Host to publish records for, default to localhost
 
96
group = None #our entry group
 
97
rename_count = 12       # Counter so we only rename after collisions a
 
98
                        # sensible number of times
 
99
# End of Avahi example code
 
100
 
 
101
 
 
102
class Client(object):
 
103
    """A representation of a client host served by this server.
 
104
    Attributes:
 
105
    name:      string; from the config file, used in log messages
 
106
    fingerprint: string (40 or 32 hexadecimal digits); used to
 
107
                 uniquely identify the client
 
108
    secret:    bytestring; sent verbatim (over TLS) to client
 
109
    fqdn:      string (FQDN); available for use by the checker command
 
110
    created:   datetime.datetime()
 
111
    last_seen: datetime.datetime() or None if not yet seen
 
112
    timeout:   datetime.timedelta(); How long from last_seen until
 
113
                                     this client is invalid
 
114
    interval:  datetime.timedelta(); How often to start a new checker
 
115
    stop_hook: If set, called by stop() as stop_hook(self)
 
116
    checker:   subprocess.Popen(); a running checker process used
 
117
                                   to see if the client lives.
 
118
                                   Is None if no process is running.
 
119
    checker_initiator_tag: a gobject event source tag, or None
 
120
    stop_initiator_tag:    - '' -
 
121
    checker_callback_tag:  - '' -
 
122
    checker_command: string; External command which is run to check if
 
123
                     client lives.  %()s expansions are done at
 
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: - '' -
 
131
    """
 
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.."""
 
160
        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
 
186
        self.checker = None
 
187
        self.checker_initiator_tag = None
 
188
        self.stop_initiator_tag = None
 
189
        self.checker_callback_tag = None
 
190
        self.check_command = checker
 
191
    def start(self):
 
192
        """Start this client's checker and timeout hooks"""
 
193
        # Schedule a new checker to be started an 'interval' from now,
 
194
        # and every interval from then on.
 
195
        self.checker_initiator_tag = gobject.timeout_add\
 
196
                                     (self._interval_milliseconds,
 
197
                                      self.start_checker)
 
198
        # Also start a new checker *right now*.
 
199
        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):
 
231
        """The checker has completed, so take appropriate actions."""
 
232
        now = datetime.datetime.now()
 
233
        self.checker_callback_tag = None
 
234
        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?",
 
246
                           vars(self))
 
247
        else:
 
248
            logger.debug(u"Checker for %(name)s failed",
 
249
                         vars(self))
 
250
    def start_checker(self):
 
251
        """Start a new checker subprocess if one is not running.
 
252
        If a checker already exists, leave it running and do
 
253
        nothing."""
 
254
        # 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
 
259
        # 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.
 
262
        if self.checker is None:
 
263
            try:
 
264
                command = self.check_command % self.fqdn
 
265
            except TypeError:
 
266
                escaped_attrs = dict((key, re.escape(str(val)))
 
267
                                     for key, val in
 
268
                                     vars(self).iteritems())
 
269
                try:
 
270
                    command = self.check_command % escaped_attrs
 
271
                except TypeError, error:
 
272
                    logger.critical(u'Could not format string "%s":'
 
273
                                    u' %s', self.check_command, error)
 
274
                    return True # Try again later
 
275
            try:
 
276
                logger.debug(u"Starting checker %r for %s",
 
277
                             command, self.name)
 
278
                self.checker = subprocess.\
 
279
                               Popen(command,
 
280
                                     close_fds=True, shell=True,
 
281
                                     cwd="/")
 
282
                self.checker_callback_tag = gobject.child_watch_add\
 
283
                                            (self.checker.pid,
 
284
                                             self.checker_callback)
 
285
            except subprocess.OSError, error:
 
286
                logger.error(u"Failed to start subprocess: %s",
 
287
                             error)
 
288
        # Re-run this periodically if run by gobject.timeout_add
 
289
        return True
 
290
    def stop_checker(self):
 
291
        """Force the checker process, if any, to stop."""
 
292
        if self.checker_callback_tag:
 
293
            gobject.source_remove(self.checker_callback_tag)
 
294
            self.checker_callback_tag = None
 
295
        if not hasattr(self, "checker") or self.checker is None:
 
296
            return
 
297
        logger.debug("Stopping checker for %(name)s", vars(self))
 
298
        try:
 
299
            os.kill(self.checker.pid, signal.SIGTERM)
 
300
            #os.sleep(0.5)
 
301
            #if self.checker.poll() is None:
 
302
            #    os.kill(self.checker.pid, signal.SIGKILL)
 
303
        except OSError, error:
 
304
            if error.errno != errno.ESRCH:
 
305
                raise
 
306
        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.
 
368
    Note: This will run in its own forked process."""
 
369
    
 
370
    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.
 
428
    Attributes:
 
429
        options:        Command line options
 
430
        clients:        Set() of Client objects
 
431
    """
 
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)
 
441
    def server_bind(self):
 
442
        """This overrides the normal server_bind() function
 
443
        to bind to an interface if one was specified, and also NOT to
 
444
        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
 
460
        # Only bind(2) the socket if we really need to.
 
461
        if self.server_address[0] or self.server_address[1]:
 
462
            if not self.server_address[0]:
 
463
                in6addr_any = "::"
 
464
                self.server_address = (in6addr_any,
 
465
                                       self.server_address[1])
 
466
            elif self.server_address[1] is None:
 
467
                self.server_address = (self.server_address[0],
 
468
                                       0)
 
469
            return super(type(self), self).server_bind()
 
470
 
 
471
 
 
472
def string_to_delta(interval):
 
473
    """Parse a string and return a datetime.timedelta
 
474
 
 
475
    >>> string_to_delta('7d')
 
476
    datetime.timedelta(7)
 
477
    >>> string_to_delta('60s')
 
478
    datetime.timedelta(0, 60)
 
479
    >>> string_to_delta('60m')
 
480
    datetime.timedelta(0, 3600)
 
481
    >>> string_to_delta('24h')
 
482
    datetime.timedelta(1)
 
483
    >>> string_to_delta(u'1w')
 
484
    datetime.timedelta(7)
 
485
    """
 
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):
 
596
    """See daemon(3).  Standard BSD Unix function.
 
597
    This should really exist as os.daemon, but it doesn't (yet)."""
 
598
    if os.fork():
 
599
        sys.exit()
 
600
    os.setsid()
 
601
    if not nochdir:
 
602
        os.chdir("/")
 
603
    if not noclose:
 
604
        # Close all standard open file descriptors
 
605
        null = os.open("/dev/null", os.O_NOCTTY | os.O_RDWR)
 
606
        if not stat.S_ISCHR(os.fstat(null).st_mode):
 
607
            raise OSError(errno.ENODEV,
 
608
                          "/dev/null not a character device")
 
609
        os.dup2(null, sys.stdin.fileno())
 
610
        os.dup2(null, sys.stdout.fileno())
 
611
        os.dup2(null, sys.stderr.fileno())
 
612
        if null > 2:
 
613
            os.close(null)
 
614
 
 
615
 
 
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
def main():
 
626
    global exitstatus
 
627
    exitstatus = 0
 
628
    global main_loop_started
 
629
    main_loop_started = False
 
630
    
 
631
    parser = OptionParser()
 
632
    parser.add_option("-i", "--interface", type="string",
 
633
                      default=None, metavar="IF",
 
634
                      help="Bind to interface IF")
 
635
    parser.add_option("-a", "--address", type="string", default=None,
 
636
                      help="Address to listen for requests on")
 
637
    parser.add_option("-p", "--port", type="int", default=None,
 
638
                      help="Port number to receive requests on")
 
639
    parser.add_option("--timeout", type="string", # Parsed later
 
640
                      default="1h",
 
641
                      help="Amount of downtime allowed for clients")
 
642
    parser.add_option("--interval", type="string", # Parsed later
 
643
                      default="5m",
 
644
                      help="How often to check that a client is up")
 
645
    parser.add_option("--check", action="store_true", default=False,
 
646
                      help="Run self-test")
 
647
    parser.add_option("--debug", action="store_true", default=False,
 
648
                      help="Debug mode")
 
649
    (options, args) = parser.parse_args()
 
650
    
 
651
    if options.check:
 
652
        import doctest
 
653
        doctest.testmod()
 
654
        sys.exit()
 
655
    
 
656
    # Parse the time arguments
 
657
    try:
 
658
        options.timeout = string_to_delta(options.timeout)
 
659
    except ValueError:
 
660
        parser.error("option --timeout: Unparseable time")
 
661
    try:
 
662
        options.interval = string_to_delta(options.interval)
 
663
    except ValueError:
 
664
        parser.error("option --interval: Unparseable time")
 
665
    
 
666
    # Parse config file
 
667
    defaults = { "checker": "fping -q -- %%(fqdn)s" }
 
668
    client_config = ConfigParser.SafeConfigParser(defaults)
 
669
    #client_config.readfp(open("global.conf"), "global.conf")
 
670
    client_config.read("mandos-clients.conf")
 
671
    
 
672
    global main_loop
 
673
    global bus
 
674
    global server
 
675
    # From the Avahi example code
 
676
    DBusGMainLoop(set_as_default=True )
 
677
    main_loop = gobject.MainLoop()
 
678
    bus = dbus.SystemBus()
 
679
    server = dbus.Interface(
 
680
            bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
 
681
            avahi.DBUS_INTERFACE_SERVER )
 
682
    # End of Avahi example code
 
683
    
 
684
    debug = options.debug
 
685
    
 
686
    if debug:
 
687
        console = logging.StreamHandler()
 
688
        # console.setLevel(logging.DEBUG)
 
689
        console.setFormatter(logging.Formatter\
 
690
                             ('%(levelname)s: %(message)s'))
 
691
        logger.addHandler(console)
 
692
        del console
 
693
    
 
694
    clients = Set()
 
695
    def remove_from_clients(client):
 
696
        clients.remove(client)
 
697
        if not clients:
 
698
            logger.debug(u"No clients left, exiting")
 
699
            killme()
 
700
    
 
701
    clients.update(Set(Client(name=section, options=options,
 
702
                              stop_hook = remove_from_clients,
 
703
                              **(dict(client_config\
 
704
                                      .items(section))))
 
705
                       for section in client_config.sections()))
 
706
    
 
707
    if not debug:
 
708
        daemon(False, False)
 
709
    
 
710
    def cleanup():
 
711
        "Cleanup function; run on exit"
 
712
        global group
 
713
        # From the Avahi example code
 
714
        if not group is None:
 
715
            group.Free()
 
716
            group = None
 
717
        # End of Avahi example code
 
718
        
 
719
        while clients:
 
720
            client = clients.pop()
 
721
            client.stop_hook = None
 
722
            client.stop()
 
723
    
 
724
    atexit.register(cleanup)
 
725
    
 
726
    if not debug:
 
727
        signal.signal(signal.SIGINT, signal.SIG_IGN)
 
728
    signal.signal(signal.SIGHUP, lambda signum, frame: killme())
 
729
    signal.signal(signal.SIGTERM, lambda signum, frame: killme())
 
730
    
 
731
    for client in clients:
 
732
        client.start()
 
733
    
 
734
    tcp_server = IPv6_TCPServer((options.address, options.port),
 
735
                                tcp_handler,
 
736
                                options=options,
 
737
                                clients=clients)
 
738
    # Find out what random port we got
 
739
    global servicePort
 
740
    servicePort = tcp_server.socket.getsockname()[1]
 
741
    logger.debug(u"Now listening on port %d", servicePort)
 
742
    
 
743
    if options.interface is not None:
 
744
        global serviceInterface
 
745
        serviceInterface = if_nametoindex(options.interface)
 
746
    
 
747
    # From the Avahi example code
 
748
    server.connect_to_signal("StateChanged", server_state_changed)
 
749
    try:
 
750
        server_state_changed(server.GetState())
 
751
    except dbus.exceptions.DBusException, error:
 
752
        logger.critical(u"DBusException: %s", error)
 
753
        killme(1)
 
754
    # End of Avahi example code
 
755
    
 
756
    gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
 
757
                         lambda *args, **kwargs:
 
758
                         tcp_server.handle_request(*args[2:],
 
759
                                                   **kwargs) or True)
 
760
    try:
 
761
        logger.debug("Starting main loop")
 
762
        main_loop_started = True
 
763
        main_loop.run()
 
764
    except KeyboardInterrupt:
 
765
        if debug:
 
766
            print
 
767
    
 
768
    sys.exit(exitstatus)
 
769
 
 
770
if __name__ == '__main__':
 
771
    main()