/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk
3 by Björn Påhlsson
Python based server
1
#!/usr/bin/python
2
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
3
from __future__ import division
4
3 by Björn Påhlsson
Python based server
5
import SocketServer
6
import socket
7
import select
8
from optparse import OptionParser
9
import datetime
10
import errno
11
import gnutls.crypto
12
import gnutls.connection
13
import gnutls.errors
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
14
import gnutls.library.functions
15
import gnutls.library.constants
16
import gnutls.library.types
3 by Björn Påhlsson
Python based server
17
import ConfigParser
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
18
import sys
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
19
import re
20
import os
21
import signal
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
22
from sets import Set
23
import subprocess
5 by Teddy Hogeborn
* server.py (server_metaclass): New.
24
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
25
import dbus
26
import gobject
27
import avahi
28
from dbus.mainloop.glib import DBusGMainLoop
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
29
import ctypes
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
30
13 by Björn Påhlsson
Added following support:
31
import logging
32
import logging.handlers
33
34
# logghandler.setFormatter(logging.Formatter('%(levelname)s %(message)s')
35
36
logger = logging.Logger('mandos')
37
logger.addHandler(logging.handlers.SysLogHandler(facility = logging.handlers.SysLogHandler.LOG_DAEMON))
38
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
39
# This variable is used to optionally bind to a specified interface.
40
# It is a global variable to fit in with the other variables from the
41
# Avahi server example code.
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
42
serviceInterface = avahi.IF_UNSPEC
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
43
# From the Avahi server example code:
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
44
serviceName = "Mandos"
45
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
46
servicePort = None                      # Not known at startup
47
serviceTXT = []                         # TXT record for the service
48
domain = ""                  # Domain to publish on, default to .local
49
host = ""          # Host to publish records for, default to localhost
50
group = None #our entry group
51
rename_count = 12       # Counter so we only rename after collisions a
52
                        # sensible number of times
53
# End of Avahi example code
54
55
3 by Björn Påhlsson
Python based server
56
class Client(object):
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
57
    """A representation of a client host served by this server.
58
    Attributes:
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
59
    name:      string; from the config file, used in log messages
60
    fingerprint: string (40 or 32 hexadecimal digits); used to
61
                 uniquely identify the client
62
    secret:    bytestring; sent verbatim (over TLS) to client
63
    fqdn:      string (FQDN); available for use by the checker command
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
64
    created:   datetime.datetime()
65
    last_seen: datetime.datetime() or None if not yet seen
66
    timeout:   datetime.timedelta(); How long from last_seen until
67
                                     this client is invalid
68
    interval:  datetime.timedelta(); How often to start a new checker
69
    stop_hook: If set, called by stop() as stop_hook(self)
70
    checker:   subprocess.Popen(); a running checker process used
71
                                   to see if the client lives.
72
                                   Is None if no process is running.
73
    checker_initiator_tag: a gobject event source tag, or None
74
    stop_initiator_tag:    - '' -
75
    checker_callback_tag:  - '' -
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
76
    checker_command: string; External command which is run to check if
77
                     client lives.  %()s expansions are done at
78
                     runtime with vars(self) as dict, so that for
79
                     instance %(name)s can be used in the command.
80
    Private attibutes:
81
    _timeout: Real variable for 'timeout'
82
    _interval: Real variable for 'interval'
83
    _timeout_milliseconds: Used by gobject.timeout_add()
84
    _interval_milliseconds: - '' -
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
85
    """
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
86
    def _set_timeout(self, timeout):
87
        "Setter function for 'timeout' attribute"
88
        self._timeout = timeout
89
        self._timeout_milliseconds = ((self.timeout.days
90
                                       * 24 * 60 * 60 * 1000)
91
                                      + (self.timeout.seconds * 1000)
92
                                      + (self.timeout.microseconds
93
                                         // 1000))
94
    timeout = property(lambda self: self._timeout,
95
                       _set_timeout)
96
    del _set_timeout
97
    def _set_interval(self, interval):
98
        "Setter function for 'interval' attribute"
99
        self._interval = interval
100
        self._interval_milliseconds = ((self.interval.days
101
                                        * 24 * 60 * 60 * 1000)
102
                                       + (self.interval.seconds
103
                                          * 1000)
104
                                       + (self.interval.microseconds
105
                                          // 1000))
106
    interval = property(lambda self: self._interval,
107
                        _set_interval)
108
    del _set_interval
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
109
    def __init__(self, name=None, options=None, stop_hook=None,
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
110
                 fingerprint=None, secret=None, secfile=None, fqdn=None,
111
                 timeout=None, interval=-1, checker=None):
3 by Björn Påhlsson
Python based server
112
        self.name = name
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
113
        # Uppercase and remove spaces from fingerprint
114
        # for later comparison purposes with return value of
115
        # the fingerprint() function
116
        self.fingerprint = fingerprint.upper().replace(u" ", u"")
117
        if secret:
118
            self.secret = secret.decode(u"base64")
119
        elif secfile:
120
            sf = open(secfile)
121
            self.secret = sf.read()
122
            sf.close()
3 by Björn Påhlsson
Python based server
123
        else:
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
124
            raise RuntimeError(u"No secret or secfile for client %s"
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
125
                               % self.name)
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
126
        self.fqdn = fqdn                # string
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
127
        self.created = datetime.datetime.now()
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
128
        self.last_seen = None
3 by Björn Påhlsson
Python based server
129
        if timeout is None:
5 by Teddy Hogeborn
* server.py (server_metaclass): New.
130
            timeout = options.timeout
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
131
        self.timeout = timeout
3 by Björn Påhlsson
Python based server
132
        if interval == -1:
5 by Teddy Hogeborn
* server.py (server_metaclass): New.
133
            interval = options.interval
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
134
        else:
135
            interval = string_to_delta(interval)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
136
        self.interval = interval
137
        self.stop_hook = stop_hook
138
        self.checker = None
139
        self.checker_initiator_tag = None
140
        self.stop_initiator_tag = None
141
        self.checker_callback_tag = None
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
142
        self.check_command = checker
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
143
    def start(self):
144
        """Start this clients checker and timeout hooks"""
145
        # Schedule a new checker to be started an 'interval' from now,
146
        # and every interval from then on.
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
147
        self.checker_initiator_tag = gobject.timeout_add\
148
                                     (self._interval_milliseconds,
149
                                      self.start_checker)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
150
        # Also start a new checker *right now*.
151
        self.start_checker()
152
        # Schedule a stop() when 'timeout' has passed
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
153
        self.stop_initiator_tag = gobject.timeout_add\
154
                                  (self._timeout_milliseconds,
155
                                   self.stop)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
156
    def stop(self):
157
        """Stop this client.
158
        The possibility that this client might be restarted is left
159
        open, but not currently used."""
13 by Björn Påhlsson
Added following support:
160
        logger.debug(u"Stopping client %s", self.name)
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
161
        self.secret = None
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
162
        if self.stop_initiator_tag:
163
            gobject.source_remove(self.stop_initiator_tag)
164
            self.stop_initiator_tag = None
165
        if self.checker_initiator_tag:
166
            gobject.source_remove(self.checker_initiator_tag)
167
            self.checker_initiator_tag = None
168
        self.stop_checker()
169
        if self.stop_hook:
170
            self.stop_hook(self)
171
        # Do not run this again if called by a gobject.timeout_add
172
        return False
173
    def __del__(self):
174
        # Some code duplication here and in stop()
175
        if hasattr(self, "stop_initiator_tag") \
176
               and self.stop_initiator_tag:
177
            gobject.source_remove(self.stop_initiator_tag)
178
            self.stop_initiator_tag = None
179
        if hasattr(self, "checker_initiator_tag") \
180
               and self.checker_initiator_tag:
181
            gobject.source_remove(self.checker_initiator_tag)
182
            self.checker_initiator_tag = None
183
        self.stop_checker()
184
    def checker_callback(self, pid, condition):
185
        """The checker has completed, so take appropriate actions."""
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
186
        now = datetime.datetime.now()
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
187
        if os.WIFEXITED(condition) \
188
               and (os.WEXITSTATUS(condition) == 0):
13 by Björn Påhlsson
Added following support:
189
            logger.debug(u"Checker for %(name)s succeeded",
190
                         vars(self))
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
191
            self.last_seen = now
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
192
            gobject.source_remove(self.stop_initiator_tag)
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
193
            self.stop_initiator_tag = gobject.timeout_add\
194
                                      (self._timeout_milliseconds,
195
                                       self.stop)
13 by Björn Påhlsson
Added following support:
196
        if not os.WIFEXITED(condition):
197
            logger.warning(u"Checker for %(name)s crashed?",
198
                           vars(self))
199
        else:
200
            logger.debug(u"Checker for %(name)s failed",
201
                         vars(self))
202
            self.checker = None
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
203
        self.checker_callback_tag = None
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
204
    def start_checker(self):
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
205
        """Start a new checker subprocess if one is not running.
206
        If a checker already exists, leave it running and do
207
        nothing."""
208
        if self.checker is None:
13 by Björn Påhlsson
Added following support:
209
            logger.debug(u"Starting checker for %s",
210
                         self.name)
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
211
            try:
212
                command = self.check_command % self.fqdn
213
            except TypeError:
214
                escaped_attrs = dict((key, re.escape(str(val)))
215
                                     for key, val in
216
                                     vars(self).iteritems())
13 by Björn Påhlsson
Added following support:
217
                try:
218
                    command = self.check_command % escaped_attrs
219
                except TypeError, error:
220
                    logger.critical(u'Could not format string "%s": %s',
221
                                    self.check_command, error)
222
                    return True # Try again later
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
223
            try:
224
                self.checker = subprocess.\
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
225
                               Popen(command,
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
226
                                     stdout=subprocess.PIPE,
227
                                     close_fds=True, shell=True,
228
                                     cwd="/")
229
                self.checker_callback_tag = gobject.\
230
                                            child_watch_add(self.checker.pid,
231
                                                            self.\
232
                                                            checker_callback)
233
            except subprocess.OSError, error:
13 by Björn Påhlsson
Added following support:
234
                logger.error(u"Failed to start subprocess: %s",
235
                             error)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
236
        # Re-run this periodically if run by gobject.timeout_add
237
        return True
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
238
    def stop_checker(self):
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
239
        """Force the checker process, if any, to stop."""
240
        if not hasattr(self, "checker") or self.checker is None:
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
241
            return
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
242
        gobject.source_remove(self.checker_callback_tag)
243
        self.checker_callback_tag = None
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
244
        os.kill(self.checker.pid, signal.SIGTERM)
245
        if self.checker.poll() is None:
246
            os.kill(self.checker.pid, signal.SIGKILL)
247
        self.checker = None
248
    def still_valid(self, now=None):
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
249
        """Has the timeout not yet passed for this client?"""
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
250
        if now is None:
251
            now = datetime.datetime.now()
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
252
        if self.last_seen is None:
253
            return now < (self.created + self.timeout)
254
        else:
255
            return now < (self.last_seen + self.timeout)
3 by Björn Påhlsson
Python based server
256
257
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
258
def peer_certificate(session):
259
    # If not an OpenPGP certificate...
260
    if gnutls.library.functions.gnutls_certificate_type_get\
261
            (session._c_object) \
262
           != gnutls.library.constants.GNUTLS_CRT_OPENPGP:
263
        # ...do the normal thing
264
        return session.peer_certificate
265
    list_size = ctypes.c_uint()
266
    cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
267
        (session._c_object, ctypes.byref(list_size))
268
    if list_size.value == 0:
269
        return None
270
    cert = cert_list[0]
271
    return ctypes.string_at(cert.data, cert.size)
272
273
274
def fingerprint(openpgp):
275
    # New empty GnuTLS certificate
276
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
277
    gnutls.library.functions.gnutls_openpgp_crt_init\
278
        (ctypes.byref(crt))
279
    # New GnuTLS "datum" with the OpenPGP public key
280
    datum = gnutls.library.types.gnutls_datum_t\
281
        (ctypes.cast(ctypes.c_char_p(openpgp),
282
                     ctypes.POINTER(ctypes.c_ubyte)),
283
         ctypes.c_uint(len(openpgp)))
284
    # Import the OpenPGP public key into the certificate
285
    ret = gnutls.library.functions.gnutls_openpgp_crt_import\
286
        (crt,
287
         ctypes.byref(datum),
288
         gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
289
    # New buffer for the fingerprint
290
    buffer = ctypes.create_string_buffer(20)
291
    buffer_length = ctypes.c_size_t()
292
    # Get the fingerprint from the certificate into the buffer
293
    gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
294
        (crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
295
    # Deinit the certificate
296
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
297
    # Convert the buffer to a Python bytestring
298
    fpr = ctypes.string_at(buffer, buffer_length.value)
299
    # Convert the bytestring to hexadecimal notation
300
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
301
    return hex_fpr
302
303
3 by Björn Påhlsson
Python based server
304
class tcp_handler(SocketServer.BaseRequestHandler, object):
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
305
    """A TCP request handler class.
306
    Instantiated by IPv6_TCPServer for each request to handle it.
307
    Note: This will run in its own forked process."""
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
308
    
3 by Björn Påhlsson
Python based server
309
    def handle(self):
13 by Björn Påhlsson
Added following support:
310
        logger.debug(u"TCP connection from: %s",
311
                     unicode(self.client_address))
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
312
        session = gnutls.connection.ClientSession(self.request,
313
                                                  gnutls.connection.\
314
                                                  X509Credentials())
315
        
316
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
317
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
318
        #                "+DHE-DSS"))
319
        priority = "SECURE256"
320
        
321
        gnutls.library.functions.gnutls_priority_set_direct\
322
            (session._c_object, priority, None);
323
        
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
324
        try:
325
            session.handshake()
326
        except gnutls.errors.GNUTLSError, error:
13 by Björn Påhlsson
Added following support:
327
            logger.debug(u"Handshake failed: %s", error)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
328
            # Do not run session.bye() here: the session is not
329
            # established.  Just abandon the request.
330
            return
3 by Björn Påhlsson
Python based server
331
        try:
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
332
            fpr = fingerprint(peer_certificate(session))
333
        except (TypeError, gnutls.errors.GNUTLSError), error:
13 by Björn Påhlsson
Added following support:
334
            logger.debug(u"Bad certificate: %s", error)
3 by Björn Påhlsson
Python based server
335
            session.bye()
336
            return
13 by Björn Påhlsson
Added following support:
337
        logger.debug(u"Fingerprint: %s", fpr)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
338
        client = None
339
        for c in clients:
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
340
            if c.fingerprint == fpr:
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
341
                client = c
342
                break
343
        # Have to check if client.still_valid(), since it is possible
344
        # that the client timed out while establishing the GnuTLS
345
        # session.
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
346
        if (not client) or (not client.still_valid()):
13 by Björn Påhlsson
Added following support:
347
            if client:
348
                logger.debug(u"Client %(name)s is invalid",
349
                             vars(client))
350
            else:
351
                logger.debug(u"Client not found for fingerprint: %s",
352
                             fpr)
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
353
            session.bye()
354
            return
355
        sent_size = 0
356
        while sent_size < len(client.secret):
357
            sent = session.send(client.secret[sent_size:])
13 by Björn Påhlsson
Added following support:
358
            logger.debug(u"Sent: %d, remaining: %d",
359
                         sent, len(client.secret)
360
                         - (sent_size + sent))
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
361
            sent_size += sent
3 by Björn Påhlsson
Python based server
362
        session.bye()
363
5 by Teddy Hogeborn
* server.py (server_metaclass): New.
364
3 by Björn Påhlsson
Python based server
365
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
366
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
367
    Attributes:
368
        options:        Command line options
369
        clients:        Set() of Client objects
370
    """
371
    address_family = socket.AF_INET6
372
    def __init__(self, *args, **kwargs):
373
        if "options" in kwargs:
374
            self.options = kwargs["options"]
375
            del kwargs["options"]
376
        if "clients" in kwargs:
377
            self.clients = kwargs["clients"]
378
            del kwargs["clients"]
379
        return super(type(self), self).__init__(*args, **kwargs)
380
    def server_bind(self):
381
        """This overrides the normal server_bind() function
382
        to bind to an interface if one was specified, and also NOT to
383
        bind to an address or port if they were not specified."""
384
        if self.options.interface:
385
            if not hasattr(socket, "SO_BINDTODEVICE"):
386
                # From /usr/include/asm-i486/socket.h
387
                socket.SO_BINDTODEVICE = 25
388
            try:
389
                self.socket.setsockopt(socket.SOL_SOCKET,
390
                                       socket.SO_BINDTODEVICE,
391
                                       self.options.interface)
392
            except socket.error, error:
393
                if error[0] == errno.EPERM:
13 by Björn Påhlsson
Added following support:
394
                    logger.warning(u"No permission to"
395
                                   u" bind to interface %s",
396
                                   self.options.interface)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
397
                else:
398
                    raise error
399
        # Only bind(2) the socket if we really need to.
400
        if self.server_address[0] or self.server_address[1]:
401
            if not self.server_address[0]:
402
                in6addr_any = "::"
403
                self.server_address = (in6addr_any,
404
                                       self.server_address[1])
405
            elif self.server_address[1] is None:
406
                self.server_address = (self.server_address[0],
407
                                       0)
408
            return super(type(self), self).server_bind()
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
409
3 by Björn Påhlsson
Python based server
410
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
411
def string_to_delta(interval):
412
    """Parse a string and return a datetime.timedelta
413
414
    >>> string_to_delta('7d')
415
    datetime.timedelta(7)
416
    >>> string_to_delta('60s')
417
    datetime.timedelta(0, 60)
418
    >>> string_to_delta('60m')
419
    datetime.timedelta(0, 3600)
420
    >>> string_to_delta('24h')
421
    datetime.timedelta(1)
422
    >>> string_to_delta(u'1w')
423
    datetime.timedelta(7)
424
    """
425
    try:
426
        suffix=unicode(interval[-1])
427
        value=int(interval[:-1])
428
        if suffix == u"d":
429
            delta = datetime.timedelta(value)
430
        elif suffix == u"s":
431
            delta = datetime.timedelta(0, value)
432
        elif suffix == u"m":
433
            delta = datetime.timedelta(0, 0, 0, 0, value)
434
        elif suffix == u"h":
435
            delta = datetime.timedelta(0, 0, 0, 0, 0, value)
436
        elif suffix == u"w":
437
            delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
438
        else:
439
            raise ValueError
440
    except (ValueError, IndexError):
441
        raise ValueError
442
    return delta
443
8 by Teddy Hogeborn
* Makefile (client_debug): Bug fix; add quotes and / to CERT_ROOT.
444
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
445
def add_service():
446
    """From the Avahi server example code"""
447
    global group, serviceName, serviceType, servicePort, serviceTXT, \
448
           domain, host
449
    if group is None:
450
        group = dbus.Interface(
451
                bus.get_object( avahi.DBUS_NAME,
452
                                server.EntryGroupNew()),
453
                avahi.DBUS_INTERFACE_ENTRY_GROUP)
454
        group.connect_to_signal('StateChanged',
455
                                entry_group_state_changed)
13 by Björn Påhlsson
Added following support:
456
    logger.debug(u"Adding service '%s' of type '%s' ...",
457
                 serviceName, serviceType)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
458
    
459
    group.AddService(
460
            serviceInterface,           # interface
461
            avahi.PROTO_INET6,          # protocol
462
            dbus.UInt32(0),             # flags
463
            serviceName, serviceType,
464
            domain, host,
465
            dbus.UInt16(servicePort),
466
            avahi.string_array_to_txt_array(serviceTXT))
467
    group.Commit()
468
469
470
def remove_service():
471
    """From the Avahi server example code"""
472
    global group
473
    
474
    if not group is None:
475
        group.Reset()
476
477
478
def server_state_changed(state):
479
    """From the Avahi server example code"""
480
    if state == avahi.SERVER_COLLISION:
13 by Björn Påhlsson
Added following support:
481
        logger.warning(u"Server name collision")
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
482
        remove_service()
483
    elif state == avahi.SERVER_RUNNING:
484
        add_service()
485
486
487
def entry_group_state_changed(state, error):
488
    """From the Avahi server example code"""
489
    global serviceName, server, rename_count
490
    
13 by Björn Påhlsson
Added following support:
491
    logger.debug(u"state change: %i", state)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
492
    
493
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
13 by Björn Påhlsson
Added following support:
494
        logger.debug(u"Service established.")
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
495
    elif state == avahi.ENTRY_GROUP_COLLISION:
496
        
497
        rename_count = rename_count - 1
498
        if rename_count > 0:
499
            name = server.GetAlternativeServiceName(name)
13 by Björn Påhlsson
Added following support:
500
            logger.warning(u"Service name collision, "
501
                           u"changing name to '%s' ...", name)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
502
            remove_service()
503
            add_service()
504
            
505
        else:
13 by Björn Påhlsson
Added following support:
506
            logger.error(u"No suitable service name found "
507
                         u"after %i retries, exiting.",
508
                         n_rename)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
509
            main_loop.quit()
510
    elif state == avahi.ENTRY_GROUP_FAILURE:
13 by Björn Påhlsson
Added following support:
511
        logger.error(u"Error in group state changed %s",
512
                     unicode(error))
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
513
        main_loop.quit()
514
        return
515
516
517
def if_nametoindex(interface):
518
    """Call the C function if_nametoindex()"""
519
    try:
520
        libc = ctypes.cdll.LoadLibrary("libc.so.6")
521
        return libc.if_nametoindex(interface)
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
522
    except (OSError, AttributeError):
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
523
        if "struct" not in sys.modules:
524
            import struct
525
        if "fcntl" not in sys.modules:
526
            import fcntl
527
        SIOCGIFINDEX = 0x8933      # From /usr/include/linux/sockios.h
528
        s = socket.socket()
529
        ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
530
                            struct.pack("16s16x", interface))
531
        s.close()
532
        interface_index = struct.unpack("I", ifreq[16:20])[0]
533
        return interface_index
534
535
536
if __name__ == '__main__':
3 by Björn Påhlsson
Python based server
537
    parser = OptionParser()
538
    parser.add_option("-i", "--interface", type="string",
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
539
                      default=None, metavar="IF",
540
                      help="Bind to interface IF")
3 by Björn Påhlsson
Python based server
541
    parser.add_option("--cert", type="string", default="cert.pem",
542
                      metavar="FILE",
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
543
                      help="Public key certificate PEM file to use")
3 by Björn Påhlsson
Python based server
544
    parser.add_option("--key", type="string", default="key.pem",
545
                      metavar="FILE",
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
546
                      help="Private key PEM file to use")
3 by Björn Påhlsson
Python based server
547
    parser.add_option("--ca", type="string", default="ca.pem",
548
                      metavar="FILE",
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
549
                      help="Certificate Authority certificate PEM file to use")
3 by Björn Påhlsson
Python based server
550
    parser.add_option("--crl", type="string", default="crl.pem",
551
                      metavar="FILE",
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
552
                      help="Certificate Revokation List PEM file to use")
553
    parser.add_option("-p", "--port", type="int", default=None,
3 by Björn Påhlsson
Python based server
554
                      help="Port number to receive requests on")
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
555
    parser.add_option("--timeout", type="string", # Parsed later
556
                      default="1h",
3 by Björn Påhlsson
Python based server
557
                      help="Amount of downtime allowed for clients")
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
558
    parser.add_option("--interval", type="string", # Parsed later
559
                      default="5m",
560
                      help="How often to check that a client is up")
561
    parser.add_option("--check", action="store_true", default=False,
562
                      help="Run self-test")
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
563
    parser.add_option("--debug", action="store_true", default=False,
564
                      help="Debug mode")
3 by Björn Påhlsson
Python based server
565
    (options, args) = parser.parse_args()
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
566
    
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
567
    if options.check:
568
        import doctest
569
        doctest.testmod()
570
        sys.exit()
3 by Björn Påhlsson
Python based server
571
    
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
572
    # Parse the time arguments
3 by Björn Påhlsson
Python based server
573
    try:
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
574
        options.timeout = string_to_delta(options.timeout)
575
    except ValueError:
3 by Björn Påhlsson
Python based server
576
        parser.error("option --timeout: Unparseable time")
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
577
    try:
578
        options.interval = string_to_delta(options.interval)
579
    except ValueError:
580
        parser.error("option --interval: Unparseable time")
581
    
3 by Björn Påhlsson
Python based server
582
    # Parse config file
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
583
    defaults = { "checker": "sleep 1; fping -q -- %%(fqdn)s" }
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
584
    client_config = ConfigParser.SafeConfigParser(defaults)
585
    #client_config.readfp(open("secrets.conf"), "secrets.conf")
586
    client_config.read("mandos-clients.conf")
587
    
588
    # From the Avahi server example code
589
    DBusGMainLoop(set_as_default=True )
590
    main_loop = gobject.MainLoop()
591
    bus = dbus.SystemBus()
592
    server = dbus.Interface(
593
            bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
594
            avahi.DBUS_INTERFACE_SERVER )
595
    # End of Avahi example code
596
    
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
597
    debug = options.debug
598
    
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
599
    clients = Set()
600
    def remove_from_clients(client):
601
        clients.remove(client)
602
        if not clients:
13 by Björn Påhlsson
Added following support:
603
            logger.debug(u"No clients left, exiting")
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
604
            main_loop.quit()
605
    
606
    clients.update(Set(Client(name=section, options=options,
607
                              stop_hook = remove_from_clients,
608
                              **(dict(client_config\
609
                                      .items(section))))
610
                       for section in client_config.sections()))
611
    for client in clients:
612
        client.start()
613
    
614
    tcp_server = IPv6_TCPServer((None, options.port),
3 by Björn Påhlsson
Python based server
615
                                tcp_handler,
616
                                options=options,
12 by Teddy Hogeborn
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
617
                                clients=clients)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
618
    # Find out what random port we got
619
    servicePort = tcp_server.socket.getsockname()[1]
13 by Björn Påhlsson
Added following support:
620
    logger.debug(u"Now listening on port %d", servicePort)
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
621
    
622
    if options.interface is not None:
623
        serviceInterface = if_nametoindex(options.interface)
624
    
625
    # From the Avahi server example code
626
    server.connect_to_signal("StateChanged", server_state_changed)
627
    server_state_changed(server.GetState())
628
    # End of Avahi example code
629
    
630
    gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
631
                         lambda *args, **kwargs:
632
                         tcp_server.handle_request(*args[2:],
633
                                                   **kwargs) or True)
634
    try:
635
        main_loop.run()
636
    except KeyboardInterrupt:
637
        print
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
638
    
639
    # Cleanup here
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
640
641
    # From the Avahi server example code
642
    if not group is None:
643
        group.Free()
644
    # End of Avahi example code
645
    
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
646
    for client in clients:
11 by Teddy Hogeborn
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
647
        client.stop_hook = None
648
        client.stop()