/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: 2008-08-09 15:56:13 UTC
  • mfrom: (24.1.29 mandos)
  • Revision ID: teddy@fukt.bsnet.se-20080809155613-pm1o10yh44nafc0g
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
# 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 © 2007-2008 Teddy Hogeborn & Björn 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 <mandos@fukt.bsnet.se>.
 
30
 
31
 
 
32
from __future__ import division
 
33
 
 
34
import SocketServer
 
35
import socket
 
36
import select
 
37
from optparse import OptionParser
 
38
import datetime
 
39
import errno
 
40
import gnutls.crypto
 
41
import gnutls.connection
 
42
import gnutls.errors
 
43
import gnutls.library.functions
 
44
import gnutls.library.constants
 
45
import gnutls.library.types
 
46
import ConfigParser
 
47
import sys
 
48
import re
 
49
import os
 
50
import signal
 
51
from sets import Set
 
52
import subprocess
 
53
import atexit
 
54
import stat
 
55
import logging
 
56
import logging.handlers
 
57
 
 
58
import dbus
 
59
import gobject
 
60
import avahi
 
61
from dbus.mainloop.glib import DBusGMainLoop
 
62
import ctypes
 
63
 
 
64
 
 
65
logger = logging.Logger('mandos')
 
66
syslogger = logging.handlers.SysLogHandler\
 
67
            (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
 
68
             address = "/dev/log")
 
69
syslogger.setFormatter(logging.Formatter\
 
70
                        ('Mandos: %(levelname)s: %(message)s'))
 
71
logger.addHandler(syslogger)
 
72
 
 
73
 
 
74
class AvahiError(Exception):
 
75
    def __init__(self, value):
 
76
        self.value = value
 
77
    def __str__(self):
 
78
        return repr(self.value)
 
79
 
 
80
class AvahiServiceError(AvahiError):
 
81
    pass
 
82
 
 
83
class AvahiGroupError(AvahiError):
 
84
    pass
 
85
 
 
86
 
 
87
class AvahiService(object):
 
88
    """An Avahi (Zeroconf) service.
 
89
    Attributes:
 
90
    interface: integer; avahi.IF_UNSPEC or an interface index.
 
91
               Used to optionally bind to the specified interface.
 
92
    name: string; Example: 'Mandos'
 
93
    type: string; Example: '_mandos._tcp'.
 
94
                  See <http://www.dns-sd.org/ServiceTypes.html>
 
95
    port: integer; what port to announce
 
96
    TXT: list of strings; TXT record for the service
 
97
    domain: string; Domain to publish on, default to .local if empty.
 
98
    host: string; Host to publish records for, default is localhost
 
99
    max_renames: integer; maximum number of renames
 
100
    rename_count: integer; counter so we only rename after collisions
 
101
                  a sensible number of times
 
102
    """
 
103
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
 
104
                 type = None, port = None, TXT = None, domain = "",
 
105
                 host = "", max_renames = 32768):
 
106
        self.interface = interface
 
107
        self.name = name
 
108
        self.type = type
 
109
        self.port = port
 
110
        if TXT is None:
 
111
            self.TXT = []
 
112
        else:
 
113
            self.TXT = TXT
 
114
        self.domain = domain
 
115
        self.host = host
 
116
        self.rename_count = 0
 
117
    def rename(self):
 
118
        """Derived from the Avahi example code"""
 
119
        if self.rename_count >= self.max_renames:
 
120
            logger.critical(u"No suitable service name found after %i"
 
121
                            u" retries, exiting.", rename_count)
 
122
            raise AvahiServiceError("Too many renames")
 
123
        name = server.GetAlternativeServiceName(name)
 
124
        logger.error(u"Changing name to %r ...", name)
 
125
        syslogger.setFormatter(logging.Formatter\
 
126
                               ('Mandos (%s): %%(levelname)s:'
 
127
                               ' %%(message)s' % name))
 
128
        self.remove()
 
129
        self.add()
 
130
        self.rename_count += 1
 
131
    def remove(self):
 
132
        """Derived from the Avahi example code"""
 
133
        if group is not None:
 
134
            group.Reset()
 
135
    def add(self):
 
136
        """Derived from the Avahi example code"""
 
137
        global group
 
138
        if group is None:
 
139
            group = dbus.Interface\
 
140
                    (bus.get_object(avahi.DBUS_NAME,
 
141
                                    server.EntryGroupNew()),
 
142
                     avahi.DBUS_INTERFACE_ENTRY_GROUP)
 
143
            group.connect_to_signal('StateChanged',
 
144
                                    entry_group_state_changed)
 
145
        logger.debug(u"Adding service '%s' of type '%s' ...",
 
146
                     service.name, service.type)
 
147
        group.AddService(
 
148
                self.interface,         # interface
 
149
                avahi.PROTO_INET6,      # protocol
 
150
                dbus.UInt32(0),         # flags
 
151
                self.name, self.type,
 
152
                self.domain, self.host,
 
153
                dbus.UInt16(self.port),
 
154
                avahi.string_array_to_txt_array(self.TXT))
 
155
        group.Commit()
 
156
 
 
157
# From the Avahi example code:
 
158
group = None                            # our entry group
 
159
# End of Avahi example code
 
160
 
 
161
 
 
162
class Client(object):
 
163
    """A representation of a client host served by this server.
 
164
    Attributes:
 
165
    name:      string; from the config file, used in log messages
 
166
    fingerprint: string (40 or 32 hexadecimal digits); used to
 
167
                 uniquely identify the client
 
168
    secret:    bytestring; sent verbatim (over TLS) to client
 
169
    host:      string; available for use by the checker command
 
170
    created:   datetime.datetime(); object creation, not client host
 
171
    last_checked_ok: datetime.datetime() or None if not yet checked OK
 
172
    timeout:   datetime.timedelta(); How long from last_checked_ok
 
173
                                     until this client is invalid
 
174
    interval:  datetime.timedelta(); How often to start a new checker
 
175
    stop_hook: If set, called by stop() as stop_hook(self)
 
176
    checker:   subprocess.Popen(); a running checker process used
 
177
                                   to see if the client lives.
 
178
                                   'None' if no process is running.
 
179
    checker_initiator_tag: a gobject event source tag, or None
 
180
    stop_initiator_tag:    - '' -
 
181
    checker_callback_tag:  - '' -
 
182
    checker_command: string; External command which is run to check if
 
183
                     client lives.  %() expansions are done at
 
184
                     runtime with vars(self) as dict, so that for
 
185
                     instance %(name)s can be used in the command.
 
186
    Private attibutes:
 
187
    _timeout: Real variable for 'timeout'
 
188
    _interval: Real variable for 'interval'
 
189
    _timeout_milliseconds: Used when calling gobject.timeout_add()
 
190
    _interval_milliseconds: - '' -
 
191
    """
 
192
    def _set_timeout(self, timeout):
 
193
        "Setter function for 'timeout' attribute"
 
194
        self._timeout = timeout
 
195
        self._timeout_milliseconds = ((self.timeout.days
 
196
                                       * 24 * 60 * 60 * 1000)
 
197
                                      + (self.timeout.seconds * 1000)
 
198
                                      + (self.timeout.microseconds
 
199
                                         // 1000))
 
200
    timeout = property(lambda self: self._timeout,
 
201
                       _set_timeout)
 
202
    del _set_timeout
 
203
    def _set_interval(self, interval):
 
204
        "Setter function for 'interval' attribute"
 
205
        self._interval = interval
 
206
        self._interval_milliseconds = ((self.interval.days
 
207
                                        * 24 * 60 * 60 * 1000)
 
208
                                       + (self.interval.seconds
 
209
                                          * 1000)
 
210
                                       + (self.interval.microseconds
 
211
                                          // 1000))
 
212
    interval = property(lambda self: self._interval,
 
213
                        _set_interval)
 
214
    del _set_interval
 
215
    def __init__(self, name = None, stop_hook=None, config={}):
 
216
        """Note: the 'checker' key in 'config' sets the
 
217
        'checker_command' attribute and *not* the 'checker'
 
218
        attribute."""
 
219
        self.name = name
 
220
        logger.debug(u"Creating client %r", self.name)
 
221
        # Uppercase and remove spaces from fingerprint for later
 
222
        # comparison purposes with return value from the fingerprint()
 
223
        # function
 
224
        self.fingerprint = config["fingerprint"].upper()\
 
225
                           .replace(u" ", u"")
 
226
        logger.debug(u"  Fingerprint: %s", self.fingerprint)
 
227
        if "secret" in config:
 
228
            self.secret = config["secret"].decode(u"base64")
 
229
        elif "secfile" in config:
 
230
            sf = open(config["secfile"])
 
231
            self.secret = sf.read()
 
232
            sf.close()
 
233
        else:
 
234
            raise TypeError(u"No secret or secfile for client %s"
 
235
                            % self.name)
 
236
        self.host = config.get("host", "")
 
237
        self.created = datetime.datetime.now()
 
238
        self.last_checked_ok = None
 
239
        self.timeout = string_to_delta(config["timeout"])
 
240
        self.interval = string_to_delta(config["interval"])
 
241
        self.stop_hook = stop_hook
 
242
        self.checker = None
 
243
        self.checker_initiator_tag = None
 
244
        self.stop_initiator_tag = None
 
245
        self.checker_callback_tag = None
 
246
        self.check_command = config["checker"]
 
247
    def start(self):
 
248
        """Start this client's checker and timeout hooks"""
 
249
        # Schedule a new checker to be started an 'interval' from now,
 
250
        # and every interval from then on.
 
251
        self.checker_initiator_tag = gobject.timeout_add\
 
252
                                     (self._interval_milliseconds,
 
253
                                      self.start_checker)
 
254
        # Also start a new checker *right now*.
 
255
        self.start_checker()
 
256
        # Schedule a stop() when 'timeout' has passed
 
257
        self.stop_initiator_tag = gobject.timeout_add\
 
258
                                  (self._timeout_milliseconds,
 
259
                                   self.stop)
 
260
    def stop(self):
 
261
        """Stop this client.
 
262
        The possibility that a client might be restarted is left open,
 
263
        but not currently used."""
 
264
        # If this client doesn't have a secret, it is already stopped.
 
265
        if hasattr(self, "secret") and self.secret:
 
266
            logger.info(u"Stopping client %s", self.name)
 
267
            self.secret = None
 
268
        else:
 
269
            return False
 
270
        if getattr(self, "stop_initiator_tag", False):
 
271
            gobject.source_remove(self.stop_initiator_tag)
 
272
            self.stop_initiator_tag = None
 
273
        if getattr(self, "checker_initiator_tag", False):
 
274
            gobject.source_remove(self.checker_initiator_tag)
 
275
            self.checker_initiator_tag = None
 
276
        self.stop_checker()
 
277
        if self.stop_hook:
 
278
            self.stop_hook(self)
 
279
        # Do not run this again if called by a gobject.timeout_add
 
280
        return False
 
281
    def __del__(self):
 
282
        self.stop_hook = None
 
283
        self.stop()
 
284
    def checker_callback(self, pid, condition):
 
285
        """The checker has completed, so take appropriate actions."""
 
286
        now = datetime.datetime.now()
 
287
        self.checker_callback_tag = None
 
288
        self.checker = None
 
289
        if os.WIFEXITED(condition) \
 
290
               and (os.WEXITSTATUS(condition) == 0):
 
291
            logger.info(u"Checker for %(name)s succeeded",
 
292
                        vars(self))
 
293
            self.last_checked_ok = now
 
294
            gobject.source_remove(self.stop_initiator_tag)
 
295
            self.stop_initiator_tag = gobject.timeout_add\
 
296
                                      (self._timeout_milliseconds,
 
297
                                       self.stop)
 
298
        elif not os.WIFEXITED(condition):
 
299
            logger.warning(u"Checker for %(name)s crashed?",
 
300
                           vars(self))
 
301
        else:
 
302
            logger.info(u"Checker for %(name)s failed",
 
303
                        vars(self))
 
304
    def start_checker(self):
 
305
        """Start a new checker subprocess if one is not running.
 
306
        If a checker already exists, leave it running and do
 
307
        nothing."""
 
308
        # The reason for not killing a running checker is that if we
 
309
        # did that, then if a checker (for some reason) started
 
310
        # running slowly and taking more than 'interval' time, the
 
311
        # client would inevitably timeout, since no checker would get
 
312
        # a chance to run to completion.  If we instead leave running
 
313
        # checkers alone, the checker would have to take more time
 
314
        # than 'timeout' for the client to be declared invalid, which
 
315
        # is as it should be.
 
316
        if self.checker is None:
 
317
            try:
 
318
                # In case check_command has exactly one % operator
 
319
                command = self.check_command % self.host
 
320
            except TypeError:
 
321
                # Escape attributes for the shell
 
322
                escaped_attrs = dict((key, re.escape(str(val)))
 
323
                                     for key, val in
 
324
                                     vars(self).iteritems())
 
325
                try:
 
326
                    command = self.check_command % escaped_attrs
 
327
                except TypeError, error:
 
328
                    logger.error(u'Could not format string "%s":'
 
329
                                 u' %s', self.check_command, error)
 
330
                    return True # Try again later
 
331
            try:
 
332
                logger.info(u"Starting checker %r for %s",
 
333
                            command, self.name)
 
334
                self.checker = subprocess.Popen(command,
 
335
                                                close_fds=True,
 
336
                                                shell=True, cwd="/")
 
337
                self.checker_callback_tag = gobject.child_watch_add\
 
338
                                            (self.checker.pid,
 
339
                                             self.checker_callback)
 
340
            except subprocess.OSError, error:
 
341
                logger.error(u"Failed to start subprocess: %s",
 
342
                             error)
 
343
        # Re-run this periodically if run by gobject.timeout_add
 
344
        return True
 
345
    def stop_checker(self):
 
346
        """Force the checker process, if any, to stop."""
 
347
        if self.checker_callback_tag:
 
348
            gobject.source_remove(self.checker_callback_tag)
 
349
            self.checker_callback_tag = None
 
350
        if getattr(self, "checker", None) is None:
 
351
            return
 
352
        logger.debug(u"Stopping checker for %(name)s", vars(self))
 
353
        try:
 
354
            os.kill(self.checker.pid, signal.SIGTERM)
 
355
            #os.sleep(0.5)
 
356
            #if self.checker.poll() is None:
 
357
            #    os.kill(self.checker.pid, signal.SIGKILL)
 
358
        except OSError, error:
 
359
            if error.errno != errno.ESRCH: # No such process
 
360
                raise
 
361
        self.checker = None
 
362
    def still_valid(self):
 
363
        """Has the timeout not yet passed for this client?"""
 
364
        now = datetime.datetime.now()
 
365
        if self.last_checked_ok is None:
 
366
            return now < (self.created + self.timeout)
 
367
        else:
 
368
            return now < (self.last_checked_ok + self.timeout)
 
369
 
 
370
 
 
371
def peer_certificate(session):
 
372
    "Return the peer's OpenPGP certificate as a bytestring"
 
373
    # If not an OpenPGP certificate...
 
374
    if gnutls.library.functions.gnutls_certificate_type_get\
 
375
            (session._c_object) \
 
376
           != gnutls.library.constants.GNUTLS_CRT_OPENPGP:
 
377
        # ...do the normal thing
 
378
        return session.peer_certificate
 
379
    list_size = ctypes.c_uint()
 
380
    cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
 
381
        (session._c_object, ctypes.byref(list_size))
 
382
    if list_size.value == 0:
 
383
        return None
 
384
    cert = cert_list[0]
 
385
    return ctypes.string_at(cert.data, cert.size)
 
386
 
 
387
 
 
388
def fingerprint(openpgp):
 
389
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
 
390
    # New GnuTLS "datum" with the OpenPGP public key
 
391
    datum = gnutls.library.types.gnutls_datum_t\
 
392
        (ctypes.cast(ctypes.c_char_p(openpgp),
 
393
                     ctypes.POINTER(ctypes.c_ubyte)),
 
394
         ctypes.c_uint(len(openpgp)))
 
395
    # New empty GnuTLS certificate
 
396
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
 
397
    gnutls.library.functions.gnutls_openpgp_crt_init\
 
398
        (ctypes.byref(crt))
 
399
    # Import the OpenPGP public key into the certificate
 
400
    gnutls.library.functions.gnutls_openpgp_crt_import\
 
401
                    (crt, ctypes.byref(datum),
 
402
                     gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
 
403
    # New buffer for the fingerprint
 
404
    buffer = ctypes.create_string_buffer(20)
 
405
    buffer_length = ctypes.c_size_t()
 
406
    # Get the fingerprint from the certificate into the buffer
 
407
    gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
 
408
        (crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
 
409
    # Deinit the certificate
 
410
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
411
    # Convert the buffer to a Python bytestring
 
412
    fpr = ctypes.string_at(buffer, buffer_length.value)
 
413
    # Convert the bytestring to hexadecimal notation
 
414
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
 
415
    return hex_fpr
 
416
 
 
417
 
 
418
class tcp_handler(SocketServer.BaseRequestHandler, object):
 
419
    """A TCP request handler class.
 
420
    Instantiated by IPv6_TCPServer for each request to handle it.
 
421
    Note: This will run in its own forked process."""
 
422
    
 
423
    def handle(self):
 
424
        logger.info(u"TCP connection from: %s",
 
425
                     unicode(self.client_address))
 
426
        session = gnutls.connection.ClientSession\
 
427
                  (self.request, gnutls.connection.X509Credentials())
 
428
        
 
429
        line = self.request.makefile().readline()
 
430
        logger.debug(u"Protocol version: %r", line)
 
431
        try:
 
432
            if int(line.strip().split()[0]) > 1:
 
433
                raise RuntimeError
 
434
        except (ValueError, IndexError, RuntimeError), error:
 
435
            logger.error(u"Unknown protocol version: %s", error)
 
436
            return
 
437
        
 
438
        # Note: gnutls.connection.X509Credentials is really a generic
 
439
        # GnuTLS certificate credentials object so long as no X.509
 
440
        # keys are added to it.  Therefore, we can use it here despite
 
441
        # using OpenPGP certificates.
 
442
        
 
443
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
 
444
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
 
445
        #                "+DHE-DSS"))
 
446
        priority = "NORMAL"             # Fallback default, since this
 
447
                                        # MUST be set.
 
448
        if self.server.settings["priority"]:
 
449
            priority = self.server.settings["priority"]
 
450
        gnutls.library.functions.gnutls_priority_set_direct\
 
451
            (session._c_object, priority, None);
 
452
        
 
453
        try:
 
454
            session.handshake()
 
455
        except gnutls.errors.GNUTLSError, error:
 
456
            logger.warning(u"Handshake failed: %s", error)
 
457
            # Do not run session.bye() here: the session is not
 
458
            # established.  Just abandon the request.
 
459
            return
 
460
        try:
 
461
            fpr = fingerprint(peer_certificate(session))
 
462
        except (TypeError, gnutls.errors.GNUTLSError), error:
 
463
            logger.warning(u"Bad certificate: %s", error)
 
464
            session.bye()
 
465
            return
 
466
        logger.debug(u"Fingerprint: %s", fpr)
 
467
        client = None
 
468
        for c in self.server.clients:
 
469
            if c.fingerprint == fpr:
 
470
                client = c
 
471
                break
 
472
        if not client:
 
473
            logger.warning(u"Client not found for fingerprint: %s",
 
474
                           fpr)
 
475
            session.bye()
 
476
            return
 
477
        # Have to check if client.still_valid(), since it is possible
 
478
        # that the client timed out while establishing the GnuTLS
 
479
        # session.
 
480
        if not client.still_valid():
 
481
            logger.warning(u"Client %(name)s is invalid",
 
482
                           vars(client))
 
483
            session.bye()
 
484
            return
 
485
        sent_size = 0
 
486
        while sent_size < len(client.secret):
 
487
            sent = session.send(client.secret[sent_size:])
 
488
            logger.debug(u"Sent: %d, remaining: %d",
 
489
                         sent, len(client.secret)
 
490
                         - (sent_size + sent))
 
491
            sent_size += sent
 
492
        session.bye()
 
493
 
 
494
 
 
495
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
 
496
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
 
497
    Attributes:
 
498
        settings:       Server settings
 
499
        clients:        Set() of Client objects
 
500
    """
 
501
    address_family = socket.AF_INET6
 
502
    def __init__(self, *args, **kwargs):
 
503
        if "settings" in kwargs:
 
504
            self.settings = kwargs["settings"]
 
505
            del kwargs["settings"]
 
506
        if "clients" in kwargs:
 
507
            self.clients = kwargs["clients"]
 
508
            del kwargs["clients"]
 
509
        return super(type(self), self).__init__(*args, **kwargs)
 
510
    def server_bind(self):
 
511
        """This overrides the normal server_bind() function
 
512
        to bind to an interface if one was specified, and also NOT to
 
513
        bind to an address or port if they were not specified."""
 
514
        if self.settings["interface"]:
 
515
            # 25 is from /usr/include/asm-i486/socket.h
 
516
            SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
 
517
            try:
 
518
                self.socket.setsockopt(socket.SOL_SOCKET,
 
519
                                       SO_BINDTODEVICE,
 
520
                                       self.settings["interface"])
 
521
            except socket.error, error:
 
522
                if error[0] == errno.EPERM:
 
523
                    logger.error(u"No permission to"
 
524
                                 u" bind to interface %s",
 
525
                                 self.settings["interface"])
 
526
                else:
 
527
                    raise error
 
528
        # Only bind(2) the socket if we really need to.
 
529
        if self.server_address[0] or self.server_address[1]:
 
530
            if not self.server_address[0]:
 
531
                in6addr_any = "::"
 
532
                self.server_address = (in6addr_any,
 
533
                                       self.server_address[1])
 
534
            elif not self.server_address[1]:
 
535
                self.server_address = (self.server_address[0],
 
536
                                       0)
 
537
#                 if self.settings["interface"]:
 
538
#                     self.server_address = (self.server_address[0],
 
539
#                                            0, # port
 
540
#                                            0, # flowinfo
 
541
#                                            if_nametoindex
 
542
#                                            (self.settings
 
543
#                                             ["interface"]))
 
544
            return super(type(self), self).server_bind()
 
545
 
 
546
 
 
547
def string_to_delta(interval):
 
548
    """Parse a string and return a datetime.timedelta
 
549
 
 
550
    >>> string_to_delta('7d')
 
551
    datetime.timedelta(7)
 
552
    >>> string_to_delta('60s')
 
553
    datetime.timedelta(0, 60)
 
554
    >>> string_to_delta('60m')
 
555
    datetime.timedelta(0, 3600)
 
556
    >>> string_to_delta('24h')
 
557
    datetime.timedelta(1)
 
558
    >>> string_to_delta(u'1w')
 
559
    datetime.timedelta(7)
 
560
    """
 
561
    try:
 
562
        suffix=unicode(interval[-1])
 
563
        value=int(interval[:-1])
 
564
        if suffix == u"d":
 
565
            delta = datetime.timedelta(value)
 
566
        elif suffix == u"s":
 
567
            delta = datetime.timedelta(0, value)
 
568
        elif suffix == u"m":
 
569
            delta = datetime.timedelta(0, 0, 0, 0, value)
 
570
        elif suffix == u"h":
 
571
            delta = datetime.timedelta(0, 0, 0, 0, 0, value)
 
572
        elif suffix == u"w":
 
573
            delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
 
574
        else:
 
575
            raise ValueError
 
576
    except (ValueError, IndexError):
 
577
        raise ValueError
 
578
    return delta
 
579
 
 
580
 
 
581
def server_state_changed(state):
 
582
    """Derived from the Avahi example code"""
 
583
    if state == avahi.SERVER_COLLISION:
 
584
        logger.error(u"Server name collision")
 
585
        service.remove()
 
586
    elif state == avahi.SERVER_RUNNING:
 
587
        service.add()
 
588
 
 
589
 
 
590
def entry_group_state_changed(state, error):
 
591
    """Derived from the Avahi example code"""
 
592
    logger.debug(u"state change: %i", state)
 
593
    
 
594
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
 
595
        logger.debug(u"Service established.")
 
596
    elif state == avahi.ENTRY_GROUP_COLLISION:
 
597
        logger.warning(u"Service name collision.")
 
598
        service.rename()
 
599
    elif state == avahi.ENTRY_GROUP_FAILURE:
 
600
        logger.critical(u"Error in group state changed %s",
 
601
                        unicode(error))
 
602
        raise AvahiGroupError("State changed: %s", str(error))
 
603
 
 
604
def if_nametoindex(interface):
 
605
    """Call the C function if_nametoindex(), or equivalent"""
 
606
    global if_nametoindex
 
607
    try:
 
608
        if "ctypes.util" not in sys.modules:
 
609
            import ctypes.util
 
610
        if_nametoindex = ctypes.cdll.LoadLibrary\
 
611
            (ctypes.util.find_library("c")).if_nametoindex
 
612
    except (OSError, AttributeError):
 
613
        if "struct" not in sys.modules:
 
614
            import struct
 
615
        if "fcntl" not in sys.modules:
 
616
            import fcntl
 
617
        def if_nametoindex(interface):
 
618
            "Get an interface index the hard way, i.e. using fcntl()"
 
619
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
 
620
            s = socket.socket()
 
621
            ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
 
622
                                struct.pack("16s16x", interface))
 
623
            s.close()
 
624
            interface_index = struct.unpack("I", ifreq[16:20])[0]
 
625
            return interface_index
 
626
    return if_nametoindex(interface)
 
627
 
 
628
 
 
629
def daemon(nochdir = False, noclose = False):
 
630
    """See daemon(3).  Standard BSD Unix function.
 
631
    This should really exist as os.daemon, but it doesn't (yet)."""
 
632
    if os.fork():
 
633
        sys.exit()
 
634
    os.setsid()
 
635
    if not nochdir:
 
636
        os.chdir("/")
 
637
    if os.fork():
 
638
        sys.exit()
 
639
    if not noclose:
 
640
        # Close all standard open file descriptors
 
641
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
642
        if not stat.S_ISCHR(os.fstat(null).st_mode):
 
643
            raise OSError(errno.ENODEV,
 
644
                          "/dev/null not a character device")
 
645
        os.dup2(null, sys.stdin.fileno())
 
646
        os.dup2(null, sys.stdout.fileno())
 
647
        os.dup2(null, sys.stderr.fileno())
 
648
        if null > 2:
 
649
            os.close(null)
 
650
 
 
651
 
 
652
def main():
 
653
    global main_loop_started
 
654
    main_loop_started = False
 
655
    
 
656
    parser = OptionParser()
 
657
    parser.add_option("-i", "--interface", type="string",
 
658
                      metavar="IF", help="Bind to interface IF")
 
659
    parser.add_option("-a", "--address", type="string",
 
660
                      help="Address to listen for requests on")
 
661
    parser.add_option("-p", "--port", type="int",
 
662
                      help="Port number to receive requests on")
 
663
    parser.add_option("--check", action="store_true", default=False,
 
664
                      help="Run self-test")
 
665
    parser.add_option("--debug", action="store_true",
 
666
                      help="Debug mode; run in foreground and log to"
 
667
                      " terminal")
 
668
    parser.add_option("--priority", type="string", help="GnuTLS"
 
669
                      " priority string (see GnuTLS documentation)")
 
670
    parser.add_option("--servicename", type="string", metavar="NAME",
 
671
                      help="Zeroconf service name")
 
672
    parser.add_option("--configdir", type="string",
 
673
                      default="/etc/mandos", metavar="DIR",
 
674
                      help="Directory to search for configuration"
 
675
                      " files")
 
676
    (options, args) = parser.parse_args()
 
677
    
 
678
    if options.check:
 
679
        import doctest
 
680
        doctest.testmod()
 
681
        sys.exit()
 
682
    
 
683
    # Default values for config file for server-global settings
 
684
    server_defaults = { "interface": "",
 
685
                        "address": "",
 
686
                        "port": "",
 
687
                        "debug": "False",
 
688
                        "priority":
 
689
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
 
690
                        "servicename": "Mandos",
 
691
                        }
 
692
    
 
693
    # Parse config file for server-global settings
 
694
    server_config = ConfigParser.SafeConfigParser(server_defaults)
 
695
    del server_defaults
 
696
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
 
697
    server_section = "server"
 
698
    # Convert the SafeConfigParser object to a dict
 
699
    server_settings = dict(server_config.items(server_section))
 
700
    # Use getboolean on the boolean config option
 
701
    server_settings["debug"] = server_config.getboolean\
 
702
                               (server_section, "debug")
 
703
    del server_config
 
704
    
 
705
    # Override the settings from the config file with command line
 
706
    # options, if set.
 
707
    for option in ("interface", "address", "port", "debug",
 
708
                   "priority", "servicename", "configdir"):
 
709
        value = getattr(options, option)
 
710
        if value is not None:
 
711
            server_settings[option] = value
 
712
    del options
 
713
    # Now we have our good server settings in "server_settings"
 
714
    
 
715
    debug = server_settings["debug"]
 
716
    
 
717
    if not debug:
 
718
        syslogger.setLevel(logging.WARNING)
 
719
    
 
720
    if server_settings["servicename"] != "Mandos":
 
721
        syslogger.setFormatter(logging.Formatter\
 
722
                               ('Mandos (%s): %%(levelname)s:'
 
723
                                ' %%(message)s'
 
724
                                % server_settings["servicename"]))
 
725
    
 
726
    # Parse config file with clients
 
727
    client_defaults = { "timeout": "1h",
 
728
                        "interval": "5m",
 
729
                        "checker": "fping -q -- %%(host)s",
 
730
                        }
 
731
    client_config = ConfigParser.SafeConfigParser(client_defaults)
 
732
    client_config.read(os.path.join(server_settings["configdir"],
 
733
                                    "clients.conf"))
 
734
    
 
735
    global service
 
736
    service = AvahiService(name = server_settings["servicename"],
 
737
                           type = "_mandos._tcp", );
 
738
    if server_settings["interface"]:
 
739
        service.interface = if_nametoindex(server_settings["interface"])
 
740
    
 
741
    global main_loop
 
742
    global bus
 
743
    global server
 
744
    # From the Avahi example code
 
745
    DBusGMainLoop(set_as_default=True )
 
746
    main_loop = gobject.MainLoop()
 
747
    bus = dbus.SystemBus()
 
748
    server = dbus.Interface(
 
749
            bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
 
750
            avahi.DBUS_INTERFACE_SERVER )
 
751
    # End of Avahi example code
 
752
    
 
753
    if debug:
 
754
        console = logging.StreamHandler()
 
755
        # console.setLevel(logging.DEBUG)
 
756
        console.setFormatter(logging.Formatter\
 
757
                             ('%(levelname)s: %(message)s'))
 
758
        logger.addHandler(console)
 
759
        del console
 
760
    
 
761
    clients = Set()
 
762
    def remove_from_clients(client):
 
763
        clients.remove(client)
 
764
        if not clients:
 
765
            logger.critical(u"No clients left, exiting")
 
766
            sys.exit()
 
767
    
 
768
    clients.update(Set(Client(name = section,
 
769
                              stop_hook = remove_from_clients,
 
770
                              config
 
771
                              = dict(client_config.items(section)))
 
772
                       for section in client_config.sections()))
 
773
    if not clients:
 
774
        logger.critical(u"No clients defined")
 
775
        sys.exit(1)
 
776
    
 
777
    if not debug:
 
778
        daemon()
 
779
    
 
780
    pidfilename = "/var/run/mandos/mandos.pid"
 
781
    pid = os.getpid()
 
782
    try:
 
783
        pidfile = open(pidfilename, "w")
 
784
        pidfile.write(str(pid) + "\n")
 
785
        pidfile.close()
 
786
        del pidfile
 
787
    except IOError, err:
 
788
        logger.error(u"Could not write %s file with PID %d",
 
789
                     pidfilename, os.getpid())
 
790
    
 
791
    def cleanup():
 
792
        "Cleanup function; run on exit"
 
793
        global group
 
794
        # From the Avahi example code
 
795
        if not group is None:
 
796
            group.Free()
 
797
            group = None
 
798
        # End of Avahi example code
 
799
        
 
800
        while clients:
 
801
            client = clients.pop()
 
802
            client.stop_hook = None
 
803
            client.stop()
 
804
    
 
805
    atexit.register(cleanup)
 
806
    
 
807
    if not debug:
 
808
        signal.signal(signal.SIGINT, signal.SIG_IGN)
 
809
    signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
 
810
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
 
811
    
 
812
    for client in clients:
 
813
        client.start()
 
814
    
 
815
    tcp_server = IPv6_TCPServer((server_settings["address"],
 
816
                                 server_settings["port"]),
 
817
                                tcp_handler,
 
818
                                settings=server_settings,
 
819
                                clients=clients)
 
820
    # Find out what port we got
 
821
    service.port = tcp_server.socket.getsockname()[1]
 
822
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
 
823
                u" scope_id %d" % tcp_server.socket.getsockname())
 
824
    
 
825
    #service.interface = tcp_server.socket.getsockname()[3]
 
826
    
 
827
    try:
 
828
        # From the Avahi example code
 
829
        server.connect_to_signal("StateChanged", server_state_changed)
 
830
        try:
 
831
            server_state_changed(server.GetState())
 
832
        except dbus.exceptions.DBusException, error:
 
833
            logger.critical(u"DBusException: %s", error)
 
834
            sys.exit(1)
 
835
        # End of Avahi example code
 
836
        
 
837
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
 
838
                             lambda *args, **kwargs:
 
839
                             tcp_server.handle_request\
 
840
                             (*args[2:], **kwargs) or True)
 
841
        
 
842
        logger.debug(u"Starting main loop")
 
843
        main_loop_started = True
 
844
        main_loop.run()
 
845
    except AvahiError, error:
 
846
        logger.critical(u"AvahiError: %s" + unicode(error))
 
847
        sys.exit(1)
 
848
    except KeyboardInterrupt:
 
849
        if debug:
 
850
            print
 
851
 
 
852
if __name__ == '__main__':
 
853
    main()