/mandos/trunk

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

« back to all changes in this revision

Viewing changes to server.py

  • Committer: Teddy Hogeborn
  • Date: 2008-07-19 18:43:24 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080719184324-iwhoa5in75xa0u2u
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
                       [braxen_client]/password): Removed.
  ([foo]/fingerprint, [braxen_client]/fingerprint): New.
  ([foo]/checker): New.
  ([foo]/secfile): New.
  ([braxen_client]/secret): New.

* server.py: New "--debug" option to set debug flag.  Removed "cert",
             "key", "ca", "crl", and "cred" variables.  Added default
             value for "checker" config file setting.  Do not pass
             credentials to IPv6_TCPServer constructor.
  (debug): New global debug flag.  Used by most debugging output code.
  (Client.__init__): Keyword argument "dn" replaced by "fingerprint",
                     "password" renamed to "secret", and "passfile"
                     renamed to "secfile".  New keyword argument
                     "checker". All callers changed.
  (Client.dn): Removed.
  (Client.fingerprint): New.
  (Client.password): Renamed to "secret"; all users changed.
  (Client.passfile): Renamed to "secfile"; all users changed.
  (Client.timeout, Client.interval): Changed to be properties; now
                                     automatically updates the
                                     "_timeout_milliseconds" and
                                     "_interval_milliseconds" values.
  (Client.timeout_milliseconds): Renamed to "_timeout_milliseconds".
  (Client.interval_milliseconds): Renamed to "_interval_milliseconds".
  (Client.check_command): New.
  (Client.start_checker): Use the new "check_command" attribute.
  (peer_certificate, fingerprint): New functions.

  (tcp_handler.handle): Use ClientSession with empty credentials
                        object instead of ServerSession.  Set gnutls
                        priority string.  Do not verify peer.  Use
                        fingerprint instead of DN when searching for
                        clients.  Bug fix: Loop sending data so even large
                        secret data strings are sent.
  (IPv6_TCPServer.credentials): Removed.
  (if_nametoindex): Do not import ctypes since that is now imported
                    globally.

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
import gnutls.crypto
12
12
import gnutls.connection
13
13
import gnutls.errors
 
14
import gnutls.library.functions
 
15
import gnutls.library.constants
 
16
import gnutls.library.types
14
17
import ConfigParser
15
18
import sys
16
19
import re
19
22
from sets import Set
20
23
import subprocess
21
24
 
 
25
import dbus
 
26
import gobject
 
27
import avahi
 
28
from dbus.mainloop.glib import DBusGMainLoop
 
29
import ctypes
 
30
 
 
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.
 
34
serviceInterface = avahi.IF_UNSPEC
 
35
# From the Avahi server example code:
 
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
 
22
48
class Client(object):
23
 
    def __init__(self, name=None, options=None, dn=None,
24
 
                 password=None, passfile=None, fqdn=None,
25
 
                 timeout=None, interval=-1):
 
49
    """A representation of a client host served by this server.
 
50
    Attributes:
 
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
 
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:  - '' -
 
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: - '' -
 
77
    """
 
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
 
101
    def __init__(self, name=None, options=None, stop_hook=None,
 
102
                 fingerprint=None, secret=None, secfile=None, fqdn=None,
 
103
                 timeout=None, interval=-1, checker=None):
26
104
        self.name = name
27
 
        self.dn = dn
28
 
        if password:
29
 
            self.password = password
30
 
        elif passfile:
31
 
            self.password = open(passfile).readall()
 
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()
32
115
        else:
33
 
            print "No Password or Passfile in client config file"
34
 
            # raise RuntimeError XXX
35
 
            self.password = "gazonk"
 
116
            raise RuntimeError(u"No secret or secfile for client %s"
 
117
                               % self.name)
36
118
        self.fqdn = fqdn                # string
37
119
        self.created = datetime.datetime.now()
38
 
        self.last_seen = None           # datetime.datetime()
 
120
        self.last_seen = None
39
121
        if timeout is None:
40
122
            timeout = options.timeout
41
 
        self.timeout = timeout          # datetime.timedelta()
 
123
        self.timeout = timeout
42
124
        if interval == -1:
43
125
            interval = options.interval
44
126
        else:
45
127
            interval = string_to_delta(interval)
46
 
        self.interval = interval        # datetime.timedelta()
47
 
        self.next_check = datetime.datetime.now() # datetime.datetime()
48
 
        # Note: next_check may be in the past if checker is not None
49
 
        self.checker = None             # or a subprocess.Popen()
50
 
    def check_action(self):
51
 
        """The checker said something and might have completed.
52
 
        Check if is has, and take appropriate actions."""
53
 
        if self.checker.poll() is None:
54
 
            # False alarm, no result yet
55
 
            #self.checker.read()
56
 
            #print "Checker for %(name)s said nothing?" % vars(self)
57
 
            return
 
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
 
134
        self.check_command = checker
 
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.
 
139
        self.checker_initiator_tag = gobject.timeout_add\
 
140
                                     (self._interval_milliseconds,
 
141
                                      self.start_checker)
 
142
        # Also start a new checker *right now*.
 
143
        self.start_checker()
 
144
        # Schedule a stop() when 'timeout' has passed
 
145
        self.stop_initiator_tag = gobject.timeout_add\
 
146
                                  (self._timeout_milliseconds,
 
147
                                   self.stop)
 
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."""
 
152
        if debug:
 
153
            sys.stderr.write(u"Stopping client %s\n" % self.name)
 
154
        self.secret = None
 
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."""
58
179
        now = datetime.datetime.now()
59
 
        if self.checker.returncode == 0:
60
 
            print "Checker for %(name)s succeeded" % vars(self)
 
180
        if os.WIFEXITED(condition) \
 
181
               and (os.WEXITSTATUS(condition) == 0):
 
182
            if debug:
 
183
                sys.stderr.write(u"Checker for %(name)s succeeded\n"
 
184
                                 % vars(self))
61
185
            self.last_seen = now
62
 
        else:
63
 
            print "Checker for %(name)s failed" % vars(self)
64
 
        while self.next_check <= now:
65
 
            self.next_check += self.interval
 
186
            gobject.source_remove(self.stop_initiator_tag)
 
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))
66
197
        self.checker = None
67
 
    handle_request = check_action
 
198
        self.checker_callback_tag = None
68
199
    def start_checker(self):
69
 
        self.stop_checker()
70
 
        try:
71
 
            self.checker = subprocess.Popen("sleep 10; fping -q -- %s"
72
 
                                            % re.escape(self.fqdn),
73
 
                                            stdout=subprocess.PIPE,
74
 
                                            close_fds=True,
75
 
                                            shell=True, cwd="/")
76
 
        except subprocess.OSError, e:
77
 
            print "Failed to start subprocess:", e
 
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:
 
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
 
214
            try:
 
215
                self.checker = subprocess.\
 
216
                               Popen(command,
 
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
78
229
    def stop_checker(self):
79
 
        if self.checker is None:
 
230
        """Force the checker process, if any, to stop."""
 
231
        if not hasattr(self, "checker") or self.checker is None:
80
232
            return
 
233
        gobject.source_remove(self.checker_callback_tag)
 
234
        self.checker_callback_tag = None
81
235
        os.kill(self.checker.pid, signal.SIGTERM)
82
236
        if self.checker.poll() is None:
83
237
            os.kill(self.checker.pid, signal.SIGKILL)
84
238
        self.checker = None
85
 
    __del__ = stop_checker
86
 
    def fileno(self):
87
 
        if self.checker is None:
88
 
            return None
89
 
        return self.checker.stdout.fileno()
90
 
    def next_stop(self):
91
 
        """The time when something must be done about this client
92
 
        May be in the past."""
93
 
        if self.last_seen is None:
94
 
            # This client has never been seen
95
 
            next_timeout = self.created + self.timeout
96
 
        else:
97
 
            next_timeout = self.last_seen + self.timeout
98
 
        if self.checker is None:
99
 
            return min(next_timeout, self.next_check)
100
 
        else:
101
 
            return next_timeout
102
239
    def still_valid(self, now=None):
103
 
        """Has this client's timeout not passed?"""
 
240
        """Has the timeout not yet passed for this client?"""
104
241
        if now is None:
105
242
            now = datetime.datetime.now()
106
243
        if self.last_seen is None:
107
244
            return now < (self.created + self.timeout)
108
245
        else:
109
246
            return now < (self.last_seen + self.timeout)
110
 
    def it_is_time_to_check(self, now=None):
111
 
        if now is None:
112
 
            now = datetime.datetime.now()
113
 
        return self.next_check <= now
114
 
 
115
 
 
116
 
class server_metaclass(type):
117
 
    "Common behavior for the UDP and TCP server classes"
118
 
    def __new__(cls, name, bases, attrs):
119
 
        attrs["address_family"] = socket.AF_INET6
120
 
        attrs["allow_reuse_address"] = True
121
 
        def server_bind(self):
122
 
            if self.options.interface:
123
 
                if not hasattr(socket, "SO_BINDTODEVICE"):
124
 
                    # From /usr/include/asm-i486/socket.h
125
 
                    socket.SO_BINDTODEVICE = 25
126
 
                try:
127
 
                    self.socket.setsockopt(socket.SOL_SOCKET,
128
 
                                           socket.SO_BINDTODEVICE,
129
 
                                           self.options.interface)
130
 
                except socket.error, error:
131
 
                    if error[0] == errno.EPERM:
132
 
                        print "Warning: No permission to bind to interface", \
133
 
                              self.options.interface
134
 
                    else:
135
 
                        raise error
136
 
            return super(type(self), self).server_bind()
137
 
        attrs["server_bind"] = server_bind
138
 
        def init(self, *args, **kwargs):
139
 
            if "options" in kwargs:
140
 
                self.options = kwargs["options"]
141
 
                del kwargs["options"]
142
 
            if "clients" in kwargs:
143
 
                self.clients = kwargs["clients"]
144
 
                del kwargs["clients"]
145
 
            if "credentials" in kwargs:
146
 
                self.credentials = kwargs["credentials"]
147
 
                del kwargs["credentials"]
148
 
            return super(type(self), self).__init__(*args, **kwargs)
149
 
        attrs["__init__"] = init
150
 
        return type.__new__(cls, name, bases, attrs)
151
 
 
152
 
 
153
 
class udp_handler(SocketServer.DatagramRequestHandler, object):
154
 
    def handle(self):
155
 
        self.wfile.write("Polo")
156
 
        print "UDP request answered"
157
 
 
158
 
 
159
 
class IPv6_UDPServer(SocketServer.UDPServer, object):
160
 
    __metaclass__ = server_metaclass
161
 
    def verify_request(self, request, client_address):
162
 
        print "UDP request came"
163
 
        return request[0] == "Marco"
 
247
 
 
248
 
 
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
164
293
 
165
294
 
166
295
class tcp_handler(SocketServer.BaseRequestHandler, object):
 
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."""
 
299
    
167
300
    def handle(self):
168
 
        print "TCP request came"
169
 
        print "Request:", self.request
170
 
        print "Client Address:", self.client_address
171
 
        print "Server:", self.server
172
 
        session = gnutls.connection.ServerSession(self.request,
173
 
                                                  self.server.credentials)
174
 
        session.handshake()
175
 
        if session.peer_certificate:
176
 
            print "DN:", session.peer_certificate.subject
177
 
        try:
178
 
            session.verify_peer()
179
 
        except gnutls.errors.CertificateError, error:
180
 
            print "Verify failed", error
181
 
            session.bye()
182
 
            return
183
 
        try:
184
 
            session.send([client.password
185
 
                          for client in self.server.clients
186
 
                          if (client.dn ==
187
 
                              session.peer_certificate.subject)][0])
188
 
        except IndexError:
189
 
            session.send("gazonk")
190
 
            # Log maybe? XXX
 
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
        
 
319
        try:
 
320
            session.handshake()
 
321
        except gnutls.errors.GNUTLSError, error:
 
322
            if debug:
 
323
                sys.stderr.write(u"Handshake failed: %s\n" % error)
 
324
            # Do not run session.bye() here: the session is not
 
325
            # established.  Just abandon the request.
 
326
            return
 
327
        try:
 
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)
 
332
            session.bye()
 
333
            return
 
334
        if debug:
 
335
            sys.stderr.write(u"Fingerprint: %s\n" % fpr)
 
336
        client = None
 
337
        for c in clients:
 
338
            if c.fingerprint == fpr:
 
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.
 
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
191
362
        session.bye()
192
363
 
193
364
 
194
365
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
195
 
    __metaclass__ = server_metaclass
 
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:
 
394
                    sys.stderr.write(u"Warning: No permission to" \
 
395
                                     u" bind to interface %s\n"
 
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()
196
409
 
197
410
 
198
411
def string_to_delta(interval):
229
442
    return delta
230
443
 
231
444
 
232
 
def main():
 
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)
 
456
    if debug:
 
457
        sys.stderr.write(u"Adding service '%s' of type '%s' ...\n"
 
458
                         % (serviceName, serviceType))
 
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:
 
482
        sys.stderr.write(u"WARNING: Server name collision\n")
 
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
    
 
492
    if debug:
 
493
        sys.stderr.write(u"state change: %i\n" % state)
 
494
    
 
495
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
 
496
        if debug:
 
497
            sys.stderr.write(u"Service established.\n")
 
498
    elif state == avahi.ENTRY_GROUP_COLLISION:
 
499
        
 
500
        rename_count = rename_count - 1
 
501
        if rename_count > 0:
 
502
            name = server.GetAlternativeServiceName(name)
 
503
            sys.stderr.write(u"WARNING: Service name collision, "
 
504
                             u"changing name to '%s' ...\n" % name)
 
505
            remove_service()
 
506
            add_service()
 
507
            
 
508
        else:
 
509
            sys.stderr.write(u"ERROR: No suitable service name found "
 
510
                             u"after %i retries, exiting.\n"
 
511
                             % n_rename)
 
512
            main_loop.quit()
 
513
    elif state == avahi.ENTRY_GROUP_FAILURE:
 
514
        sys.stderr.write(u"Error in group state changed %s\n"
 
515
                         % unicode(error))
 
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)
 
525
    except (OSError, AttributeError):
 
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__':
233
540
    parser = OptionParser()
234
541
    parser.add_option("-i", "--interface", type="string",
235
 
                      default="eth0", metavar="IF",
236
 
                      help="Interface to bind to")
 
542
                      default=None, metavar="IF",
 
543
                      help="Bind to interface IF")
237
544
    parser.add_option("--cert", type="string", default="cert.pem",
238
545
                      metavar="FILE",
239
 
                      help="Public key certificate to use")
 
546
                      help="Public key certificate PEM file to use")
240
547
    parser.add_option("--key", type="string", default="key.pem",
241
548
                      metavar="FILE",
242
 
                      help="Private key to use")
 
549
                      help="Private key PEM file to use")
243
550
    parser.add_option("--ca", type="string", default="ca.pem",
244
551
                      metavar="FILE",
245
 
                      help="Certificate Authority certificate to use")
 
552
                      help="Certificate Authority certificate PEM file to use")
246
553
    parser.add_option("--crl", type="string", default="crl.pem",
247
554
                      metavar="FILE",
248
 
                      help="Certificate Revokation List to use")
249
 
    parser.add_option("-p", "--port", type="int", default=49001,
 
555
                      help="Certificate Revokation List PEM file to use")
 
556
    parser.add_option("-p", "--port", type="int", default=None,
250
557
                      help="Port number to receive requests on")
251
 
    parser.add_option("--dh", type="int", metavar="BITS",
252
 
                      help="DH group to use")
253
 
    parser.add_option("-t", "--timeout", type="string", # Parsed later
254
 
                      default="15m",
 
558
    parser.add_option("--timeout", type="string", # Parsed later
 
559
                      default="1h",
255
560
                      help="Amount of downtime allowed for clients")
256
561
    parser.add_option("--interval", type="string", # Parsed later
257
562
                      default="5m",
258
563
                      help="How often to check that a client is up")
259
564
    parser.add_option("--check", action="store_true", default=False,
260
565
                      help="Run self-test")
 
566
    parser.add_option("--debug", action="store_true", default=False,
 
567
                      help="Debug mode")
261
568
    (options, args) = parser.parse_args()
262
 
 
 
569
    
263
570
    if options.check:
264
571
        import doctest
265
572
        doctest.testmod()
270
577
        options.timeout = string_to_delta(options.timeout)
271
578
    except ValueError:
272
579
        parser.error("option --timeout: Unparseable time")
273
 
    
274
580
    try:
275
581
        options.interval = string_to_delta(options.interval)
276
582
    except ValueError:
277
583
        parser.error("option --interval: Unparseable time")
278
584
    
279
 
    cert = gnutls.crypto.X509Certificate(open(options.cert).read())
280
 
    key = gnutls.crypto.X509PrivateKey(open(options.key).read())
281
 
    ca = gnutls.crypto.X509Certificate(open(options.ca).read())
282
 
    crl = gnutls.crypto.X509CRL(open(options.crl).read())
283
 
    cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
284
 
    
285
585
    # Parse config file
286
 
    defaults = {}
287
 
    client_config_object = ConfigParser.SafeConfigParser(defaults)
288
 
    client_config_object.read("mandos-clients.conf")
289
 
    clients = Set(Client(name=section, options=options,
290
 
                         **(dict(client_config_object\
291
 
                                 .items(section))))
292
 
                  for section in client_config_object.sections())
293
 
    
294
 
    in6addr_any = "::"
295
 
    udp_server = IPv6_UDPServer((in6addr_any, options.port),
296
 
                                udp_handler,
297
 
                                options=options)
298
 
    
299
 
    tcp_server = IPv6_TCPServer((in6addr_any, options.port),
 
586
    defaults = { "checker": "sleep 1; fping -q -- %%(fqdn)s" }
 
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
    
 
600
    debug = options.debug
 
601
    
 
602
    clients = Set()
 
603
    def remove_from_clients(client):
 
604
        clients.remove(client)
 
605
        if not clients:
 
606
            if debug:
 
607
                sys.stderr.write(u"No clients left, exiting\n")
 
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),
300
619
                                tcp_handler,
301
620
                                options=options,
302
 
                                clients=clients,
303
 
                                credentials=cred)
304
 
    
305
 
    while True:
306
 
        if not clients:
307
 
            break
308
 
        try:
309
 
            next_stop = min(client.next_stop() for client in clients)
310
 
            now = datetime.datetime.now()
311
 
            if next_stop > now:
312
 
                delay = next_stop - now
313
 
                delay_seconds = (delay.days * 24 * 60 * 60
314
 
                                 + delay.seconds
315
 
                                 + delay.microseconds / 1000000)
316
 
                clients_with_checkers = tuple(client for client in
317
 
                                              clients
318
 
                                              if client.checker
319
 
                                              is not None)
320
 
                input_checks = (udp_server, tcp_server) \
321
 
                               + clients_with_checkers
322
 
                print "Waiting for network",
323
 
                if clients_with_checkers:
324
 
                    print "and checkers for:",
325
 
                    for client in clients_with_checkers:
326
 
                        print client.name,
327
 
                print
328
 
                input, out, err = select.select(input_checks, (), (),
329
 
                                                delay_seconds)
330
 
                for obj in input:
331
 
                    obj.handle_request()
332
 
            # start new checkers
333
 
            for client in clients:
334
 
                if client.it_is_time_to_check(now=now) and \
335
 
                       client.checker is None:
336
 
                    print "Starting checker for client %(name)s" \
337
 
                          % vars(client)
338
 
                    client.start_checker()
339
 
            # delete timed-out clients
340
 
            for client in clients.copy():
341
 
                if not client.still_valid(now=now):
342
 
                    # log xxx
343
 
                    print "Removing client %(name)s" % vars(client)
344
 
                    clients.remove(client)
345
 
        except KeyboardInterrupt:
346
 
            break
 
621
                                clients=clients)
 
622
    # Find out what random port we got
 
623
    servicePort = tcp_server.socket.getsockname()[1]
 
624
    if debug:
 
625
        sys.stderr.write(u"Now listening on port %d\n" % servicePort)
 
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
347
643
    
348
644
    # Cleanup here
 
645
 
 
646
    # From the Avahi server example code
 
647
    if not group is None:
 
648
        group.Free()
 
649
    # End of Avahi example code
 
650
    
349
651
    for client in clients:
350
 
        client.stop_checker()
351
 
 
352
 
 
353
 
if __name__ == "__main__":
354
 
    main()
355
 
 
 
652
        client.stop_hook = None
 
653
        client.stop()