/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-01-30 17:28:18 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080130172818-fnrq2xmtg1wa8wo2
* client.cpp (main): Get t_old early since it is used on error exits.

* server.py (Client.checker): New attribute.
  (Client.check_action, Client.start_checker, Client.stop_checker,
  Client.fileno, Client.next_stop, Client.still_valid,
  Client.it_is_time_to_check): New methods.
  (Client.handle_request): Alias to "check_action".
  (main): Stop all started checkers on exit.  Exit cleanly on keyboard
  interrupt.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
2
 
# -*- mode: python; coding: utf-8 -*-
3
 
4
 
# Mandos server - give out binary blobs to connecting clients.
5
 
6
 
# This program is partly derived from an example program for an Avahi
7
 
# service publisher, downloaded from
8
 
# <http://avahi.org/wiki/PythonPublishExample>.  This includes the
9
 
# following functions: "add_service", "remove_service",
10
 
# "server_state_changed", "entry_group_state_changed", and some lines
11
 
# in "main".
12
 
13
 
# Everything else is Copyright © 2007-2008 Teddy Hogeborn and Björn
14
 
# Påhlsson.
15
 
16
 
# This program is free software: you can redistribute it and/or modify
17
 
# it under the terms of the GNU General Public License as published by
18
 
# the Free Software Foundation, either version 3 of the License, or
19
 
# (at your option) any later version.
20
 
#
21
 
#     This program is distributed in the hope that it will be useful,
22
 
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24
 
#     GNU General Public License for more details.
25
 
26
 
# You should have received a copy of the GNU General Public License
27
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
28
 
29
 
# Contact the authors at <https://www.fukt.bsnet.se/~belorn/> and
30
 
# <https://www.fukt.bsnet.se/~teddy/>.
31
 
32
 
 
33
 
from __future__ import division
34
2
 
35
3
import SocketServer
36
4
import socket
41
9
import gnutls.crypto
42
10
import gnutls.connection
43
11
import gnutls.errors
44
 
import gnutls.library.functions
45
 
import gnutls.library.constants
46
 
import gnutls.library.types
47
12
import ConfigParser
48
13
import sys
49
14
import re
50
15
import os
51
16
import signal
52
 
from sets import Set
53
 
import subprocess
54
 
import atexit
55
 
import stat
56
 
import logging
57
 
import logging.handlers
58
 
 
59
 
import dbus
60
 
import gobject
61
 
import avahi
62
 
from dbus.mainloop.glib import DBusGMainLoop
63
 
import ctypes
64
 
 
65
 
# Brief description of the operation of this program:
66
 
67
 
# This server announces itself as a Zeroconf service.  Connecting
68
 
# clients use the TLS protocol, with the unusual quirk that this
69
 
# server program acts as a TLS "client" while the connecting clients
70
 
# acts as a TLS "server".  The clients (acting as a TLS "server") must
71
 
# supply an OpenPGP certificate, and the fingerprint of this
72
 
# certificate is used by this server to look up (in a list read from a
73
 
# file at start time) which binary blob to give the client.  No other
74
 
# authentication or authorization is done by this server.
75
 
 
76
 
 
77
 
logger = logging.Logger('mandos')
78
 
syslogger = logging.handlers.SysLogHandler\
79
 
            (facility = logging.handlers.SysLogHandler.LOG_DAEMON)
80
 
syslogger.setFormatter(logging.Formatter\
81
 
                        ('%(levelname)s: %(message)s'))
82
 
logger.addHandler(syslogger)
83
 
del syslogger
84
 
 
85
 
# This variable is used to optionally bind to a specified interface.
86
 
# It is a global variable to fit in with the other variables from the
87
 
# Avahi example code.
88
 
serviceInterface = avahi.IF_UNSPEC
89
 
# From the Avahi example code:
90
 
serviceName = "Mandos"
91
 
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
92
 
servicePort = None                      # Not known at startup
93
 
serviceTXT = []                         # TXT record for the service
94
 
domain = ""                  # Domain to publish on, default to .local
95
 
host = ""          # Host to publish records for, default to localhost
96
 
group = None #our entry group
97
 
rename_count = 12       # Counter so we only rename after collisions a
98
 
                        # sensible number of times
99
 
# End of Avahi example code
100
 
 
101
17
 
102
18
class Client(object):
103
 
    """A representation of a client host served by this server.
104
 
    Attributes:
105
 
    name:      string; from the config file, used in log messages
106
 
    fingerprint: string (40 or 32 hexadecimal digits); used to
107
 
                 uniquely identify the client
108
 
    secret:    bytestring; sent verbatim (over TLS) to client
109
 
    fqdn:      string (FQDN); available for use by the checker command
110
 
    created:   datetime.datetime()
111
 
    last_seen: datetime.datetime() or None if not yet seen
112
 
    timeout:   datetime.timedelta(); How long from last_seen until
113
 
                                     this client is invalid
114
 
    interval:  datetime.timedelta(); How often to start a new checker
115
 
    stop_hook: If set, called by stop() as stop_hook(self)
116
 
    checker:   subprocess.Popen(); a running checker process used
117
 
                                   to see if the client lives.
118
 
                                   Is None if no process is running.
119
 
    checker_initiator_tag: a gobject event source tag, or None
120
 
    stop_initiator_tag:    - '' -
121
 
    checker_callback_tag:  - '' -
122
 
    checker_command: string; External command which is run to check if
123
 
                     client lives.  %()s expansions are done at
124
 
                     runtime with vars(self) as dict, so that for
125
 
                     instance %(name)s can be used in the command.
126
 
    Private attibutes:
127
 
    _timeout: Real variable for 'timeout'
128
 
    _interval: Real variable for 'interval'
129
 
    _timeout_milliseconds: Used by gobject.timeout_add()
130
 
    _interval_milliseconds: - '' -
131
 
    """
132
 
    def _set_timeout(self, timeout):
133
 
        "Setter function for 'timeout' attribute"
134
 
        self._timeout = timeout
135
 
        self._timeout_milliseconds = ((self.timeout.days
136
 
                                       * 24 * 60 * 60 * 1000)
137
 
                                      + (self.timeout.seconds * 1000)
138
 
                                      + (self.timeout.microseconds
139
 
                                         // 1000))
140
 
    timeout = property(lambda self: self._timeout,
141
 
                       _set_timeout)
142
 
    del _set_timeout
143
 
    def _set_interval(self, interval):
144
 
        "Setter function for 'interval' attribute"
145
 
        self._interval = interval
146
 
        self._interval_milliseconds = ((self.interval.days
147
 
                                        * 24 * 60 * 60 * 1000)
148
 
                                       + (self.interval.seconds
149
 
                                          * 1000)
150
 
                                       + (self.interval.microseconds
151
 
                                          // 1000))
152
 
    interval = property(lambda self: self._interval,
153
 
                        _set_interval)
154
 
    del _set_interval
155
 
    def __init__(self, name=None, options=None, stop_hook=None,
156
 
                 fingerprint=None, secret=None, secfile=None,
157
 
                 fqdn=None, timeout=None, interval=-1, checker=None):
158
 
        """Note: the 'checker' argument sets the 'checker_command'
159
 
        attribute and not the 'checker' attribute.."""
 
19
    def __init__(self, name=None, options=None, dn=None,
 
20
                 password=None, passfile=None, fqdn=None,
 
21
                 timeout=None, interval=-1):
160
22
        self.name = name
161
 
        # Uppercase and remove spaces from fingerprint
162
 
        # for later comparison purposes with return value of
163
 
        # the fingerprint() function
164
 
        self.fingerprint = fingerprint.upper().replace(u" ", u"")
165
 
        if secret:
166
 
            self.secret = secret.decode(u"base64")
167
 
        elif secfile:
168
 
            sf = open(secfile)
169
 
            self.secret = sf.read()
170
 
            sf.close()
 
23
        self.dn = dn
 
24
        if password:
 
25
            self.password = password
 
26
        elif passfile:
 
27
            self.password = open(passfile).readall()
171
28
        else:
172
 
            raise RuntimeError(u"No secret or secfile for client %s"
173
 
                               % self.name)
 
29
            print "No Password or Passfile in client config file"
 
30
            # raise RuntimeError XXX
 
31
            self.password = "gazonk"
174
32
        self.fqdn = fqdn                # string
175
33
        self.created = datetime.datetime.now()
176
 
        self.last_seen = None
 
34
        self.last_seen = None           # datetime.datetime()
177
35
        if timeout is None:
178
 
            self.timeout = options.timeout
179
 
        else:
180
 
            self.timeout = string_to_delta(timeout)
 
36
            timeout = options.timeout
 
37
        self.timeout = timeout          # datetime.timedelta()
181
38
        if interval == -1:
182
 
            self.interval = options.interval
183
 
        else:
184
 
            self.interval = string_to_delta(interval)
185
 
        self.stop_hook = stop_hook
186
 
        self.checker = None
187
 
        self.checker_initiator_tag = None
188
 
        self.stop_initiator_tag = None
189
 
        self.checker_callback_tag = None
190
 
        self.check_command = checker
191
 
    def start(self):
192
 
        """Start this client's checker and timeout hooks"""
193
 
        # Schedule a new checker to be started an 'interval' from now,
194
 
        # and every interval from then on.
195
 
        self.checker_initiator_tag = gobject.timeout_add\
196
 
                                     (self._interval_milliseconds,
197
 
                                      self.start_checker)
198
 
        # Also start a new checker *right now*.
199
 
        self.start_checker()
200
 
        # Schedule a stop() when 'timeout' has passed
201
 
        self.stop_initiator_tag = gobject.timeout_add\
202
 
                                  (self._timeout_milliseconds,
203
 
                                   self.stop)
204
 
    def stop(self):
205
 
        """Stop this client.
206
 
        The possibility that this client might be restarted is left
207
 
        open, but not currently used."""
208
 
        # If this client doesn't have a secret, it is already stopped.
209
 
        if self.secret:
210
 
            logger.debug(u"Stopping client %s", self.name)
211
 
            self.secret = None
212
 
        else:
213
 
            return False
214
 
        if hasattr(self, "stop_initiator_tag") \
215
 
               and self.stop_initiator_tag:
216
 
            gobject.source_remove(self.stop_initiator_tag)
217
 
            self.stop_initiator_tag = None
218
 
        if hasattr(self, "checker_initiator_tag") \
219
 
               and self.checker_initiator_tag:
220
 
            gobject.source_remove(self.checker_initiator_tag)
221
 
            self.checker_initiator_tag = None
222
 
        self.stop_checker()
223
 
        if self.stop_hook:
224
 
            self.stop_hook(self)
225
 
        # Do not run this again if called by a gobject.timeout_add
226
 
        return False
227
 
    def __del__(self):
228
 
        self.stop_hook = None
229
 
        self.stop()
230
 
    def checker_callback(self, pid, condition):
231
 
        """The checker has completed, so take appropriate actions."""
232
 
        now = datetime.datetime.now()
233
 
        self.checker_callback_tag = None
234
 
        self.checker = None
235
 
        if os.WIFEXITED(condition) \
236
 
               and (os.WEXITSTATUS(condition) == 0):
237
 
            logger.debug(u"Checker for %(name)s succeeded",
238
 
                         vars(self))
 
39
            interval = options.interval
 
40
        self.interval = interval        # datetime.timedelta()
 
41
        self.next_check = datetime.datetime.now() # datetime.datetime()
 
42
        self.checker = None             # or a subprocess.Popen()
 
43
    def check_action(self, now=None):
 
44
        """The checker said something and might have completed.
 
45
        Check if is has, and take appropriate actions."""
 
46
        if self.checker.poll() is None:
 
47
            # False alarm, no result yet
 
48
            #self.checker.read()
 
49
            return
 
50
        if now is None:
 
51
            now = datetime.datetime.now()
 
52
        if self.checker.returncode == 0:
239
53
            self.last_seen = now
240
 
            gobject.source_remove(self.stop_initiator_tag)
241
 
            self.stop_initiator_tag = gobject.timeout_add\
242
 
                                      (self._timeout_milliseconds,
243
 
                                       self.stop)
244
 
        elif not os.WIFEXITED(condition):
245
 
            logger.warning(u"Checker for %(name)s crashed?",
246
 
                           vars(self))
247
 
        else:
248
 
            logger.debug(u"Checker for %(name)s failed",
249
 
                         vars(self))
 
54
        while self.next_check <= now:
 
55
            self.next_check += self.interval
 
56
    handle_request = check_action
250
57
    def start_checker(self):
251
 
        """Start a new checker subprocess if one is not running.
252
 
        If a checker already exists, leave it running and do
253
 
        nothing."""
254
 
        # The reason for not killing a running checker is that if we
255
 
        # did that, then if a checker (for some reason) started
256
 
        # running slowly and taking more than 'interval' time, the
257
 
        # client would inevitably timeout, since no checker would get
258
 
        # a chance to run to completion.  If we instead leave running
259
 
        # checkers alone, the checker would have to take more time
260
 
        # than 'timeout' for the client to be declared invalid, which
261
 
        # is as it should be.
262
 
        if self.checker is None:
263
 
            try:
264
 
                command = self.check_command % self.fqdn
265
 
            except TypeError:
266
 
                escaped_attrs = dict((key, re.escape(str(val)))
267
 
                                     for key, val in
268
 
                                     vars(self).iteritems())
269
 
                try:
270
 
                    command = self.check_command % escaped_attrs
271
 
                except TypeError, error:
272
 
                    logger.critical(u'Could not format string "%s":'
273
 
                                    u' %s', self.check_command, error)
274
 
                    return True # Try again later
275
 
            try:
276
 
                logger.debug(u"Starting checker %r for %s",
277
 
                             command, self.name)
278
 
                self.checker = subprocess.\
279
 
                               Popen(command,
280
 
                                     close_fds=True, shell=True,
281
 
                                     cwd="/")
282
 
                self.checker_callback_tag = gobject.child_watch_add\
283
 
                                            (self.checker.pid,
284
 
                                             self.checker_callback)
285
 
            except subprocess.OSError, error:
286
 
                logger.error(u"Failed to start subprocess: %s",
287
 
                             error)
288
 
        # Re-run this periodically if run by gobject.timeout_add
289
 
        return True
 
58
        self.stop_checker()
 
59
        try:
 
60
            self.checker = subprocess.Popen("sleep 1; fping -q -- %s"
 
61
                                            % re.escape(self.fqdn),
 
62
                                            stdout=subprocess.PIPE,
 
63
                                            close_fds=True,
 
64
                                            shell=True, cwd="/")
 
65
        except subprocess.OSError, e:
 
66
            print "Failed to start subprocess:", e
290
67
    def stop_checker(self):
291
 
        """Force the checker process, if any, to stop."""
292
 
        if self.checker_callback_tag:
293
 
            gobject.source_remove(self.checker_callback_tag)
294
 
            self.checker_callback_tag = None
295
 
        if not hasattr(self, "checker") or self.checker is None:
 
68
        if self.checker is None:
296
69
            return
297
 
        logger.debug("Stopping checker for %(name)s", vars(self))
298
 
        try:
299
 
            os.kill(self.checker.pid, signal.SIGTERM)
300
 
            #os.sleep(0.5)
301
 
            #if self.checker.poll() is None:
302
 
            #    os.kill(self.checker.pid, signal.SIGKILL)
303
 
        except OSError, error:
304
 
            if error.errno != errno.ESRCH:
305
 
                raise
 
70
        os.kill(self.checker.pid, signal.SIGTERM)
 
71
        if self.checker.poll() is None:
 
72
            os.kill(self.checker.pid, signal.SIGKILL)
306
73
        self.checker = None
 
74
    __del__ = stop_checker
 
75
    def fileno(self):
 
76
        if self.checker is None:
 
77
            return None
 
78
        return self.checker.stdout.fileno()
 
79
    def next_stop(self):
 
80
        """The time when something must be done about this client"""
 
81
        return min(self.last_seen + self.timeout, self.next_check)
307
82
    def still_valid(self, now=None):
308
 
        """Has the timeout not yet passed for this client?"""
309
 
        if now is None:
310
 
            now = datetime.datetime.now()
311
 
        if self.last_seen is None:
312
 
            return now < (self.created + self.timeout)
313
 
        else:
314
 
            return now < (self.last_seen + self.timeout)
315
 
 
316
 
 
317
 
def peer_certificate(session):
318
 
    "Return the peer's OpenPGP certificate as a bytestring"
319
 
    # If not an OpenPGP certificate...
320
 
    if gnutls.library.functions.gnutls_certificate_type_get\
321
 
            (session._c_object) \
322
 
           != gnutls.library.constants.GNUTLS_CRT_OPENPGP:
323
 
        # ...do the normal thing
324
 
        return session.peer_certificate
325
 
    list_size = ctypes.c_uint()
326
 
    cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
327
 
        (session._c_object, ctypes.byref(list_size))
328
 
    if list_size.value == 0:
329
 
        return None
330
 
    cert = cert_list[0]
331
 
    return ctypes.string_at(cert.data, cert.size)
332
 
 
333
 
 
334
 
def fingerprint(openpgp):
335
 
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
336
 
    # New empty GnuTLS certificate
337
 
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
338
 
    gnutls.library.functions.gnutls_openpgp_crt_init\
339
 
        (ctypes.byref(crt))
340
 
    # New GnuTLS "datum" with the OpenPGP public key
341
 
    datum = gnutls.library.types.gnutls_datum_t\
342
 
        (ctypes.cast(ctypes.c_char_p(openpgp),
343
 
                     ctypes.POINTER(ctypes.c_ubyte)),
344
 
         ctypes.c_uint(len(openpgp)))
345
 
    # Import the OpenPGP public key into the certificate
346
 
    ret = gnutls.library.functions.gnutls_openpgp_crt_import\
347
 
        (crt,
348
 
         ctypes.byref(datum),
349
 
         gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
350
 
    # New buffer for the fingerprint
351
 
    buffer = ctypes.create_string_buffer(20)
352
 
    buffer_length = ctypes.c_size_t()
353
 
    # Get the fingerprint from the certificate into the buffer
354
 
    gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
355
 
        (crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
356
 
    # Deinit the certificate
357
 
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
358
 
    # Convert the buffer to a Python bytestring
359
 
    fpr = ctypes.string_at(buffer, buffer_length.value)
360
 
    # Convert the bytestring to hexadecimal notation
361
 
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
362
 
    return hex_fpr
 
83
        """Has this client's timeout not passed?"""
 
84
        if now is None:
 
85
            now = datetime.datetime.now()
 
86
        return now < (self.last_seen + timeout)
 
87
    def it_is_time_to_check(self, now=None):
 
88
        if now is None:
 
89
            now = datetime.datetime.now()
 
90
        return self.next_check <= now
 
91
 
 
92
 
 
93
class server_metaclass(type):
 
94
    "Common behavior for the UDP and TCP server classes"
 
95
    def __new__(cls, name, bases, attrs):
 
96
        attrs["address_family"] = socket.AF_INET6
 
97
        attrs["allow_reuse_address"] = True
 
98
        def server_bind(self):
 
99
            if self.options.interface:
 
100
                if not hasattr(socket, "SO_BINDTODEVICE"):
 
101
                    # From /usr/include/asm-i486/socket.h
 
102
                    socket.SO_BINDTODEVICE = 25
 
103
                try:
 
104
                    self.socket.setsockopt(socket.SOL_SOCKET,
 
105
                                           socket.SO_BINDTODEVICE,
 
106
                                           self.options.interface)
 
107
                except socket.error, error:
 
108
                    if error[0] == errno.EPERM:
 
109
                        print "Warning: No permission to bind to interface", \
 
110
                              self.options.interface
 
111
                    else:
 
112
                        raise error
 
113
            return super(type(self), self).server_bind()
 
114
        attrs["server_bind"] = server_bind
 
115
        def init(self, *args, **kwargs):
 
116
            if "options" in kwargs:
 
117
                self.options = kwargs["options"]
 
118
                del kwargs["options"]
 
119
            if "clients" in kwargs:
 
120
                self.clients = kwargs["clients"]
 
121
                del kwargs["clients"]
 
122
            if "credentials" in kwargs:
 
123
                self.credentials = kwargs["credentials"]
 
124
                del kwargs["credentials"]
 
125
            return super(type(self), self).__init__(*args, **kwargs)
 
126
        attrs["__init__"] = init
 
127
        return type.__new__(cls, name, bases, attrs)
 
128
 
 
129
 
 
130
class udp_handler(SocketServer.DatagramRequestHandler, object):
 
131
    def handle(self):
 
132
        self.wfile.write("Polo")
 
133
        print "UDP request answered"
 
134
 
 
135
 
 
136
class IPv6_UDPServer(SocketServer.UDPServer, object):
 
137
    __metaclass__ = server_metaclass
 
138
    def verify_request(self, request, client_address):
 
139
        print "UDP request came"
 
140
        return request[0] == "Marco"
363
141
 
364
142
 
365
143
class tcp_handler(SocketServer.BaseRequestHandler, object):
366
 
    """A TCP request handler class.
367
 
    Instantiated by IPv6_TCPServer for each request to handle it.
368
 
    Note: This will run in its own forked process."""
369
 
    
370
144
    def handle(self):
371
 
        logger.debug(u"TCP connection from: %s",
372
 
                     unicode(self.client_address))
373
 
        session = gnutls.connection.ClientSession(self.request,
374
 
                                                  gnutls.connection.\
375
 
                                                  X509Credentials())
376
 
        
377
 
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
378
 
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
379
 
        #                "+DHE-DSS"))
380
 
        priority = "SECURE256"
381
 
        
382
 
        gnutls.library.functions.gnutls_priority_set_direct\
383
 
            (session._c_object, priority, None);
384
 
        
385
 
        try:
386
 
            session.handshake()
387
 
        except gnutls.errors.GNUTLSError, error:
388
 
            logger.debug(u"Handshake failed: %s", error)
389
 
            # Do not run session.bye() here: the session is not
390
 
            # established.  Just abandon the request.
391
 
            return
392
 
        try:
393
 
            fpr = fingerprint(peer_certificate(session))
394
 
        except (TypeError, gnutls.errors.GNUTLSError), error:
395
 
            logger.debug(u"Bad certificate: %s", error)
396
 
            session.bye()
397
 
            return
398
 
        logger.debug(u"Fingerprint: %s", fpr)
399
 
        client = None
400
 
        for c in self.server.clients:
401
 
            if c.fingerprint == fpr:
402
 
                client = c
403
 
                break
404
 
        # Have to check if client.still_valid(), since it is possible
405
 
        # that the client timed out while establishing the GnuTLS
406
 
        # session.
407
 
        if (not client) or (not client.still_valid()):
408
 
            if client:
409
 
                logger.debug(u"Client %(name)s is invalid",
410
 
                             vars(client))
411
 
            else:
412
 
                logger.debug(u"Client not found for fingerprint: %s",
413
 
                             fpr)
414
 
            session.bye()
415
 
            return
416
 
        sent_size = 0
417
 
        while sent_size < len(client.secret):
418
 
            sent = session.send(client.secret[sent_size:])
419
 
            logger.debug(u"Sent: %d, remaining: %d",
420
 
                         sent, len(client.secret)
421
 
                         - (sent_size + sent))
422
 
            sent_size += sent
 
145
        print "TCP request came"
 
146
        print "Request:", self.request
 
147
        print "Client Address:", self.client_address
 
148
        print "Server:", self.server
 
149
        session = gnutls.connection.ServerSession(self.request,
 
150
                                                  self.server.credentials)
 
151
        session.handshake()
 
152
        if session.peer_certificate:
 
153
            print "DN:", session.peer_certificate.subject
 
154
        try:
 
155
            session.verify_peer()
 
156
        except gnutls.errors.CertificateError, error:
 
157
            print "Verify failed", error
 
158
            session.bye()
 
159
            return
 
160
        try:
 
161
            session.send(dict((client.dn, client.password)
 
162
                              for client in self.server.clients)
 
163
                         [session.peer_certificate.subject])
 
164
        except KeyError:
 
165
            session.send("gazonk")
 
166
            # Log maybe? XXX
423
167
        session.bye()
424
168
 
425
169
 
426
170
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
427
 
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
428
 
    Attributes:
429
 
        options:        Command line options
430
 
        clients:        Set() of Client objects
431
 
    """
432
 
    address_family = socket.AF_INET6
433
 
    def __init__(self, *args, **kwargs):
434
 
        if "options" in kwargs:
435
 
            self.options = kwargs["options"]
436
 
            del kwargs["options"]
437
 
        if "clients" in kwargs:
438
 
            self.clients = kwargs["clients"]
439
 
            del kwargs["clients"]
440
 
        return super(type(self), self).__init__(*args, **kwargs)
441
 
    def server_bind(self):
442
 
        """This overrides the normal server_bind() function
443
 
        to bind to an interface if one was specified, and also NOT to
444
 
        bind to an address or port if they were not specified."""
445
 
        if self.options.interface:
446
 
            if not hasattr(socket, "SO_BINDTODEVICE"):
447
 
                # From /usr/include/asm-i486/socket.h
448
 
                socket.SO_BINDTODEVICE = 25
449
 
            try:
450
 
                self.socket.setsockopt(socket.SOL_SOCKET,
451
 
                                       socket.SO_BINDTODEVICE,
452
 
                                       self.options.interface)
453
 
            except socket.error, error:
454
 
                if error[0] == errno.EPERM:
455
 
                    logger.warning(u"No permission to"
456
 
                                   u" bind to interface %s",
457
 
                                   self.options.interface)
458
 
                else:
459
 
                    raise error
460
 
        # Only bind(2) the socket if we really need to.
461
 
        if self.server_address[0] or self.server_address[1]:
462
 
            if not self.server_address[0]:
463
 
                in6addr_any = "::"
464
 
                self.server_address = (in6addr_any,
465
 
                                       self.server_address[1])
466
 
            elif self.server_address[1] is None:
467
 
                self.server_address = (self.server_address[0],
468
 
                                       0)
469
 
            return super(type(self), self).server_bind()
470
 
 
 
171
    __metaclass__ = server_metaclass
 
172
    request_queue_size = 1024
 
173
 
 
174
 
 
175
in6addr_any = "::"
471
176
 
472
177
def string_to_delta(interval):
473
178
    """Parse a string and return a datetime.timedelta
503
208
    return delta
504
209
 
505
210
 
506
 
def add_service():
507
 
    """Derived from the Avahi example code"""
508
 
    global group, serviceName, serviceType, servicePort, serviceTXT, \
509
 
           domain, host
510
 
    if group is None:
511
 
        group = dbus.Interface(
512
 
                bus.get_object( avahi.DBUS_NAME,
513
 
                                server.EntryGroupNew()),
514
 
                avahi.DBUS_INTERFACE_ENTRY_GROUP)
515
 
        group.connect_to_signal('StateChanged',
516
 
                                entry_group_state_changed)
517
 
    logger.debug(u"Adding service '%s' of type '%s' ...",
518
 
                 serviceName, serviceType)
519
 
    
520
 
    group.AddService(
521
 
            serviceInterface,           # interface
522
 
            avahi.PROTO_INET6,          # protocol
523
 
            dbus.UInt32(0),             # flags
524
 
            serviceName, serviceType,
525
 
            domain, host,
526
 
            dbus.UInt16(servicePort),
527
 
            avahi.string_array_to_txt_array(serviceTXT))
528
 
    group.Commit()
529
 
 
530
 
 
531
 
def remove_service():
532
 
    """From the Avahi example code"""
533
 
    global group
534
 
    
535
 
    if not group is None:
536
 
        group.Reset()
537
 
 
538
 
 
539
 
def server_state_changed(state):
540
 
    """Derived from the Avahi example code"""
541
 
    if state == avahi.SERVER_COLLISION:
542
 
        logger.warning(u"Server name collision")
543
 
        remove_service()
544
 
    elif state == avahi.SERVER_RUNNING:
545
 
        add_service()
546
 
 
547
 
 
548
 
def entry_group_state_changed(state, error):
549
 
    """Derived from the Avahi example code"""
550
 
    global serviceName, server, rename_count
551
 
    
552
 
    logger.debug(u"state change: %i", state)
553
 
    
554
 
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
555
 
        logger.debug(u"Service established.")
556
 
    elif state == avahi.ENTRY_GROUP_COLLISION:
557
 
        
558
 
        rename_count = rename_count - 1
559
 
        if rename_count > 0:
560
 
            name = server.GetAlternativeServiceName(name)
561
 
            logger.warning(u"Service name collision, "
562
 
                           u"changing name to '%s' ...", name)
563
 
            remove_service()
564
 
            add_service()
565
 
            
566
 
        else:
567
 
            logger.error(u"No suitable service name found after %i"
568
 
                         u" retries, exiting.", n_rename)
569
 
            killme(1)
570
 
    elif state == avahi.ENTRY_GROUP_FAILURE:
571
 
        logger.error(u"Error in group state changed %s",
572
 
                     unicode(error))
573
 
        killme(1)
574
 
 
575
 
 
576
 
def if_nametoindex(interface):
577
 
    """Call the C function if_nametoindex()"""
578
 
    try:
579
 
        libc = ctypes.cdll.LoadLibrary("libc.so.6")
580
 
        return libc.if_nametoindex(interface)
581
 
    except (OSError, AttributeError):
582
 
        if "struct" not in sys.modules:
583
 
            import struct
584
 
        if "fcntl" not in sys.modules:
585
 
            import fcntl
586
 
        SIOCGIFINDEX = 0x8933      # From /usr/include/linux/sockios.h
587
 
        s = socket.socket()
588
 
        ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
589
 
                            struct.pack("16s16x", interface))
590
 
        s.close()
591
 
        interface_index = struct.unpack("I", ifreq[16:20])[0]
592
 
        return interface_index
593
 
 
594
 
 
595
 
def daemon(nochdir, noclose):
596
 
    """See daemon(3).  Standard BSD Unix function.
597
 
    This should really exist as os.daemon, but it doesn't (yet)."""
598
 
    if os.fork():
599
 
        sys.exit()
600
 
    os.setsid()
601
 
    if not nochdir:
602
 
        os.chdir("/")
603
 
    if not noclose:
604
 
        # Close all standard open file descriptors
605
 
        null = os.open("/dev/null", os.O_NOCTTY | os.O_RDWR)
606
 
        if not stat.S_ISCHR(os.fstat(null).st_mode):
607
 
            raise OSError(errno.ENODEV,
608
 
                          "/dev/null not a character device")
609
 
        os.dup2(null, sys.stdin.fileno())
610
 
        os.dup2(null, sys.stdout.fileno())
611
 
        os.dup2(null, sys.stderr.fileno())
612
 
        if null > 2:
613
 
            os.close(null)
614
 
 
615
 
 
616
 
def killme(status = 0):
617
 
    logger.debug("Stopping server with exit status %d", status)
618
 
    exitstatus = status
619
 
    if main_loop_started:
620
 
        main_loop.quit()
621
 
    else:
622
 
        sys.exit(status)
623
 
 
624
 
 
625
211
def main():
626
 
    global exitstatus
627
 
    exitstatus = 0
628
 
    global main_loop_started
629
 
    main_loop_started = False
630
 
    
631
212
    parser = OptionParser()
632
213
    parser.add_option("-i", "--interface", type="string",
633
 
                      default=None, metavar="IF",
634
 
                      help="Bind to interface IF")
635
 
    parser.add_option("-a", "--address", type="string", default=None,
636
 
                      help="Address to listen for requests on")
637
 
    parser.add_option("-p", "--port", type="int", default=None,
 
214
                      default="eth0", metavar="IF",
 
215
                      help="Interface to bind to")
 
216
    parser.add_option("--cert", type="string", default="cert.pem",
 
217
                      metavar="FILE",
 
218
                      help="Public key certificate to use")
 
219
    parser.add_option("--key", type="string", default="key.pem",
 
220
                      metavar="FILE",
 
221
                      help="Private key to use")
 
222
    parser.add_option("--ca", type="string", default="ca.pem",
 
223
                      metavar="FILE",
 
224
                      help="Certificate Authority certificate to use")
 
225
    parser.add_option("--crl", type="string", default="crl.pem",
 
226
                      metavar="FILE",
 
227
                      help="Certificate Revokation List to use")
 
228
    parser.add_option("-p", "--port", type="int", default=49001,
638
229
                      help="Port number to receive requests on")
639
 
    parser.add_option("--timeout", type="string", # Parsed later
640
 
                      default="1h",
 
230
    parser.add_option("--dh", type="int", metavar="BITS",
 
231
                      help="DH group to use")
 
232
    parser.add_option("-t", "--timeout", type="string", # Parsed later
 
233
                      default="15m",
641
234
                      help="Amount of downtime allowed for clients")
642
235
    parser.add_option("--interval", type="string", # Parsed later
643
236
                      default="5m",
644
237
                      help="How often to check that a client is up")
645
238
    parser.add_option("--check", action="store_true", default=False,
646
239
                      help="Run self-test")
647
 
    parser.add_option("--debug", action="store_true", default=False,
648
 
                      help="Debug mode")
649
240
    (options, args) = parser.parse_args()
650
 
    
 
241
 
651
242
    if options.check:
652
243
        import doctest
653
244
        doctest.testmod()
658
249
        options.timeout = string_to_delta(options.timeout)
659
250
    except ValueError:
660
251
        parser.error("option --timeout: Unparseable time")
 
252
    
661
253
    try:
662
254
        options.interval = string_to_delta(options.interval)
663
255
    except ValueError:
664
256
        parser.error("option --interval: Unparseable time")
665
257
    
 
258
    cert = gnutls.crypto.X509Certificate(open(options.cert).read())
 
259
    key = gnutls.crypto.X509PrivateKey(open(options.key).read())
 
260
    ca = gnutls.crypto.X509Certificate(open(options.ca).read())
 
261
    crl = gnutls.crypto.X509CRL(open(options.crl).read())
 
262
    cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
 
263
    
666
264
    # Parse config file
667
 
    defaults = { "checker": "fping -q -- %%(fqdn)s" }
668
 
    client_config = ConfigParser.SafeConfigParser(defaults)
669
 
    #client_config.readfp(open("global.conf"), "global.conf")
670
 
    client_config.read("mandos-clients.conf")
671
 
    
672
 
    global main_loop
673
 
    global bus
674
 
    global server
675
 
    # From the Avahi example code
676
 
    DBusGMainLoop(set_as_default=True )
677
 
    main_loop = gobject.MainLoop()
678
 
    bus = dbus.SystemBus()
679
 
    server = dbus.Interface(
680
 
            bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
681
 
            avahi.DBUS_INTERFACE_SERVER )
682
 
    # End of Avahi example code
683
 
    
684
 
    debug = options.debug
685
 
    
686
 
    if debug:
687
 
        console = logging.StreamHandler()
688
 
        # console.setLevel(logging.DEBUG)
689
 
        console.setFormatter(logging.Formatter\
690
 
                             ('%(levelname)s: %(message)s'))
691
 
        logger.addHandler(console)
692
 
        del console
693
 
    
694
 
    clients = Set()
695
 
    def remove_from_clients(client):
696
 
        clients.remove(client)
697
 
        if not clients:
698
 
            logger.debug(u"No clients left, exiting")
699
 
            killme()
700
 
    
701
 
    clients.update(Set(Client(name=section, options=options,
702
 
                              stop_hook = remove_from_clients,
703
 
                              **(dict(client_config\
704
 
                                      .items(section))))
705
 
                       for section in client_config.sections()))
706
 
    
707
 
    if not debug:
708
 
        daemon(False, False)
709
 
    
710
 
    def cleanup():
711
 
        "Cleanup function; run on exit"
712
 
        global group
713
 
        # From the Avahi example code
714
 
        if not group is None:
715
 
            group.Free()
716
 
            group = None
717
 
        # End of Avahi example code
718
 
        
719
 
        while clients:
720
 
            client = clients.pop()
721
 
            client.stop_hook = None
722
 
            client.stop()
723
 
    
724
 
    atexit.register(cleanup)
725
 
    
726
 
    if not debug:
727
 
        signal.signal(signal.SIGINT, signal.SIG_IGN)
728
 
    signal.signal(signal.SIGHUP, lambda signum, frame: killme())
729
 
    signal.signal(signal.SIGTERM, lambda signum, frame: killme())
730
 
    
731
 
    for client in clients:
732
 
        client.start()
733
 
    
734
 
    tcp_server = IPv6_TCPServer((options.address, options.port),
 
265
    defaults = {}
 
266
    client_config_object = ConfigParser.SafeConfigParser(defaults)
 
267
    client_config_object.read("mandos-clients.conf")
 
268
    clients = [Client(name=section, options=options,
 
269
                      **(dict(client_config_object.items(section))))
 
270
               for section in client_config_object.sections()]
 
271
    
 
272
    udp_server = IPv6_UDPServer((in6addr_any, options.port),
 
273
                                udp_handler,
 
274
                                options=options)
 
275
    
 
276
    tcp_server = IPv6_TCPServer((in6addr_any, options.port),
735
277
                                tcp_handler,
736
278
                                options=options,
737
 
                                clients=clients)
738
 
    # Find out what random port we got
739
 
    global servicePort
740
 
    servicePort = tcp_server.socket.getsockname()[1]
741
 
    logger.debug(u"Now listening on port %d", servicePort)
742
 
    
743
 
    if options.interface is not None:
744
 
        global serviceInterface
745
 
        serviceInterface = if_nametoindex(options.interface)
746
 
    
747
 
    # From the Avahi example code
748
 
    server.connect_to_signal("StateChanged", server_state_changed)
749
 
    try:
750
 
        server_state_changed(server.GetState())
751
 
    except dbus.exceptions.DBusException, error:
752
 
        logger.critical(u"DBusException: %s", error)
753
 
        killme(1)
754
 
    # End of Avahi example code
755
 
    
756
 
    gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
757
 
                         lambda *args, **kwargs:
758
 
                         tcp_server.handle_request(*args[2:],
759
 
                                                   **kwargs) or True)
760
 
    try:
761
 
        logger.debug("Starting main loop")
762
 
        main_loop_started = True
763
 
        main_loop.run()
764
 
    except KeyboardInterrupt:
765
 
        if debug:
766
 
            print
767
 
    
768
 
    sys.exit(exitstatus)
769
 
 
770
 
if __name__ == '__main__':
 
279
                                clients=clients,
 
280
                                credentials=cred)
 
281
    
 
282
    while True:
 
283
        try:
 
284
            input, out, err = select.select((udp_server,
 
285
                                             tcp_server), (), ())
 
286
            if not input:
 
287
                pass
 
288
            else:
 
289
                for obj in input:
 
290
                    obj.handle_request()
 
291
        except KeyboardInterrupt:
 
292
            break
 
293
    
 
294
    # Cleanup here
 
295
    for client in clients:
 
296
        client.stop_checker()
 
297
 
 
298
 
 
299
if __name__ == "__main__":
771
300
    main()
 
301