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