/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-06-30 01:43:39 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080630014339-rsuztydpl2w5ml83
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
             old broadcast-UDP-to-port-49001 method.

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
2
 
33
3
from __future__ import division
34
4
 
41
11
import gnutls.crypto
42
12
import gnutls.connection
43
13
import gnutls.errors
44
 
import gnutls.library.functions
45
 
import gnutls.library.constants
46
 
import gnutls.library.types
47
14
import ConfigParser
48
15
import sys
49
16
import re
51
18
import signal
52
19
from sets import Set
53
20
import subprocess
54
 
import atexit
55
 
import stat
56
 
import logging
57
 
import logging.handlers
58
21
 
59
22
import dbus
60
23
import gobject
61
24
import avahi
62
25
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.
 
26
 
 
27
# This variable is used to optionally bind to a specified
 
28
# interface.
88
29
serviceInterface = avahi.IF_UNSPEC
89
 
# From the Avahi example code:
 
30
# It is a global variable to fit in with the rest of the
 
31
# variables from the Avahi server example code:
90
32
serviceName = "Mandos"
91
33
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
92
34
servicePort = None                      # Not known at startup
102
44
class Client(object):
103
45
    """A representation of a client host served by this server.
104
46
    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
 
47
    password:  string
 
48
    fqdn:      string, FQDN (used by the checker)
110
49
    created:   datetime.datetime()
111
50
    last_seen: datetime.datetime() or None if not yet seen
112
51
    timeout:   datetime.timedelta(); How long from last_seen until
113
52
                                     this client is invalid
114
53
    interval:  datetime.timedelta(); How often to start a new checker
 
54
    timeout_milliseconds: Used by gobject.timeout_add()
 
55
    interval_milliseconds: - '' -
115
56
    stop_hook: If set, called by stop() as stop_hook(self)
116
57
    checker:   subprocess.Popen(); a running checker process used
117
58
                                   to see if the client lives.
119
60
    checker_initiator_tag: a gobject event source tag, or None
120
61
    stop_initiator_tag:    - '' -
121
62
    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
63
    """
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
64
    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.."""
 
65
                 dn=None, password=None, passfile=None, fqdn=None,
 
66
                 timeout=None, interval=-1):
160
67
        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()
 
68
        self.dn = dn
 
69
        if password:
 
70
            self.password = password
 
71
        elif passfile:
 
72
            self.password = open(passfile).readall()
171
73
        else:
172
 
            raise RuntimeError(u"No secret or secfile for client %s"
 
74
            raise RuntimeError(u"No Password or Passfile for client %s"
173
75
                               % self.name)
174
76
        self.fqdn = fqdn                # string
175
77
        self.created = datetime.datetime.now()
176
78
        self.last_seen = None
177
79
        if timeout is None:
178
 
            self.timeout = options.timeout
179
 
        else:
180
 
            self.timeout = string_to_delta(timeout)
 
80
            timeout = options.timeout
 
81
        self.timeout = timeout
 
82
        self.timeout_milliseconds = ((self.timeout.days
 
83
                                      * 24 * 60 * 60 * 1000)
 
84
                                     + (self.timeout.seconds * 1000)
 
85
                                     + (self.timeout.microseconds
 
86
                                        // 1000))
181
87
        if interval == -1:
182
 
            self.interval = options.interval
 
88
            interval = options.interval
183
89
        else:
184
 
            self.interval = string_to_delta(interval)
 
90
            interval = string_to_delta(interval)
 
91
        self.interval = interval
 
92
        self.interval_milliseconds = ((self.interval.days
 
93
                                       * 24 * 60 * 60 * 1000)
 
94
                                      + (self.interval.seconds * 1000)
 
95
                                      + (self.interval.microseconds
 
96
                                         // 1000))
185
97
        self.stop_hook = stop_hook
186
98
        self.checker = None
187
99
        self.checker_initiator_tag = None
188
100
        self.stop_initiator_tag = None
189
101
        self.checker_callback_tag = None
190
 
        self.check_command = checker
191
102
    def start(self):
192
 
        """Start this client's checker and timeout hooks"""
 
103
        """Start this clients checker and timeout hooks"""
193
104
        # Schedule a new checker to be started an 'interval' from now,
194
105
        # and every interval from then on.
195
 
        self.checker_initiator_tag = gobject.timeout_add\
196
 
                                     (self._interval_milliseconds,
197
 
                                      self.start_checker)
 
106
        self.checker_initiator_tag = gobject.\
 
107
                                     timeout_add(self.interval_milliseconds,
 
108
                                                 self.start_checker)
198
109
        # Also start a new checker *right now*.
199
110
        self.start_checker()
200
111
        # Schedule a stop() when 'timeout' has passed
201
 
        self.stop_initiator_tag = gobject.timeout_add\
202
 
                                  (self._timeout_milliseconds,
203
 
                                   self.stop)
 
112
        self.stop_initiator_tag = gobject.\
 
113
                                     timeout_add(self.timeout_milliseconds,
 
114
                                                 self.stop)
204
115
    def stop(self):
205
116
        """Stop this client.
206
117
        The possibility that this client might be restarted is left
207
118
        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:
 
119
        # print "Stopping client", self.name
 
120
        self.password = None
 
121
        if self.stop_initiator_tag:
216
122
            gobject.source_remove(self.stop_initiator_tag)
217
123
            self.stop_initiator_tag = None
218
 
        if hasattr(self, "checker_initiator_tag") \
219
 
               and self.checker_initiator_tag:
 
124
        if self.checker_initiator_tag:
220
125
            gobject.source_remove(self.checker_initiator_tag)
221
126
            self.checker_initiator_tag = None
222
127
        self.stop_checker()
225
130
        # Do not run this again if called by a gobject.timeout_add
226
131
        return False
227
132
    def __del__(self):
228
 
        self.stop_hook = None
229
 
        self.stop()
 
133
        # Some code duplication here and in stop()
 
134
        if hasattr(self, "stop_initiator_tag") \
 
135
               and self.stop_initiator_tag:
 
136
            gobject.source_remove(self.stop_initiator_tag)
 
137
            self.stop_initiator_tag = None
 
138
        if hasattr(self, "checker_initiator_tag") \
 
139
               and self.checker_initiator_tag:
 
140
            gobject.source_remove(self.checker_initiator_tag)
 
141
            self.checker_initiator_tag = None
 
142
        self.stop_checker()
230
143
    def checker_callback(self, pid, condition):
231
144
        """The checker has completed, so take appropriate actions."""
232
145
        now = datetime.datetime.now()
233
 
        self.checker_callback_tag = None
234
 
        self.checker = None
235
146
        if os.WIFEXITED(condition) \
236
147
               and (os.WEXITSTATUS(condition) == 0):
237
 
            logger.debug(u"Checker for %(name)s succeeded",
238
 
                         vars(self))
 
148
            #print "Checker for %(name)s succeeded" % vars(self)
239
149
            self.last_seen = now
240
150
            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))
 
151
            self.stop_initiator_tag = gobject.\
 
152
                                      timeout_add(self.timeout_milliseconds,
 
153
                                                  self.stop)
 
154
        #else:
 
155
        #    if not os.WIFEXITED(condition):
 
156
        #        print "Checker for %(name)s crashed?" % vars(self)
 
157
        #    else:
 
158
        #        print "Checker for %(name)s failed" % vars(self)
 
159
        self.checker = None
 
160
        self.checker_callback_tag = None
250
161
    def start_checker(self):
251
162
        """Start a new checker subprocess if one is not running.
252
163
        If a checker already exists, leave it running and do
253
164
        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
165
        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)
 
166
            #print "Starting checker for", self.name
 
167
            try:
278
168
                self.checker = subprocess.\
279
 
                               Popen(command,
 
169
                               Popen("sleep 1; fping -q -- %s"
 
170
                                     % re.escape(self.fqdn),
 
171
                                     stdout=subprocess.PIPE,
280
172
                                     close_fds=True, shell=True,
281
173
                                     cwd="/")
282
 
                self.checker_callback_tag = gobject.child_watch_add\
283
 
                                            (self.checker.pid,
284
 
                                             self.checker_callback)
 
174
                self.checker_callback_tag = gobject.\
 
175
                                            child_watch_add(self.checker.pid,
 
176
                                                            self.\
 
177
                                                            checker_callback)
285
178
            except subprocess.OSError, error:
286
 
                logger.error(u"Failed to start subprocess: %s",
287
 
                             error)
 
179
                sys.stderr.write(u"Failed to start subprocess: %s\n"
 
180
                                 % error)
288
181
        # Re-run this periodically if run by gobject.timeout_add
289
182
        return True
290
183
    def stop_checker(self):
291
184
        """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
185
        if not hasattr(self, "checker") or self.checker is None:
296
186
            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
 
187
        gobject.source_remove(self.checker_callback_tag)
 
188
        self.checker_callback_tag = None
 
189
        os.kill(self.checker.pid, signal.SIGTERM)
 
190
        if self.checker.poll() is None:
 
191
            os.kill(self.checker.pid, signal.SIGKILL)
306
192
        self.checker = None
307
193
    def still_valid(self, now=None):
308
194
        """Has the timeout not yet passed for this client?"""
314
200
            return now < (self.last_seen + self.timeout)
315
201
 
316
202
 
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
363
 
 
364
 
 
365
203
class tcp_handler(SocketServer.BaseRequestHandler, object):
366
204
    """A TCP request handler class.
367
205
    Instantiated by IPv6_TCPServer for each request to handle it.
368
206
    Note: This will run in its own forked process."""
369
 
    
370
207
    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
 
        
 
208
        #print u"TCP request came"
 
209
        #print u"Request:", self.request
 
210
        #print u"Client Address:", self.client_address
 
211
        #print u"Server:", self.server
 
212
        session = gnutls.connection.ServerSession(self.request,
 
213
                                                  self.server\
 
214
                                                  .credentials)
385
215
        try:
386
216
            session.handshake()
387
217
        except gnutls.errors.GNUTLSError, error:
388
 
            logger.debug(u"Handshake failed: %s", error)
 
218
            #sys.stderr.write(u"Handshake failed: %s\n" % error)
389
219
            # Do not run session.bye() here: the session is not
390
220
            # established.  Just abandon the request.
391
221
            return
 
222
        #if session.peer_certificate:
 
223
        #    print "DN:", session.peer_certificate.subject
392
224
        try:
393
 
            fpr = fingerprint(peer_certificate(session))
394
 
        except (TypeError, gnutls.errors.GNUTLSError), error:
395
 
            logger.debug(u"Bad certificate: %s", error)
 
225
            session.verify_peer()
 
226
        except gnutls.errors.CertificateError, error:
 
227
            #sys.stderr.write(u"Verify failed: %s\n" % error)
396
228
            session.bye()
397
229
            return
398
 
        logger.debug(u"Fingerprint: %s", fpr)
399
230
        client = None
400
 
        for c in self.server.clients:
401
 
            if c.fingerprint == fpr:
 
231
        for c in clients:
 
232
            if c.dn == session.peer_certificate.subject:
402
233
                client = c
403
234
                break
404
235
        # Have to check if client.still_valid(), since it is possible
405
236
        # that the client timed out while establishing the GnuTLS
406
237
        # 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
 
238
        if client and client.still_valid():
 
239
            session.send(client.password)
 
240
        else:
 
241
            #if client:
 
242
            #    sys.stderr.write(u"Client %(name)s is invalid\n"
 
243
            #                     % vars(client))
 
244
            #else:
 
245
            #    sys.stderr.write(u"Client not found for DN: %s\n"
 
246
            #                     % session.peer_certificate.subject)
 
247
            #session.send("gazonk")
 
248
            pass
423
249
        session.bye()
424
250
 
425
251
 
428
254
    Attributes:
429
255
        options:        Command line options
430
256
        clients:        Set() of Client objects
 
257
        credentials:    GnuTLS X.509 credentials
431
258
    """
432
259
    address_family = socket.AF_INET6
433
260
    def __init__(self, *args, **kwargs):
437
264
        if "clients" in kwargs:
438
265
            self.clients = kwargs["clients"]
439
266
            del kwargs["clients"]
 
267
        if "credentials" in kwargs:
 
268
            self.credentials = kwargs["credentials"]
 
269
            del kwargs["credentials"]
440
270
        return super(type(self), self).__init__(*args, **kwargs)
441
271
    def server_bind(self):
442
272
        """This overrides the normal server_bind() function
452
282
                                       self.options.interface)
453
283
            except socket.error, error:
454
284
                if error[0] == errno.EPERM:
455
 
                    logger.warning(u"No permission to"
456
 
                                   u" bind to interface %s",
457
 
                                   self.options.interface)
 
285
                    sys.stderr.write(u"Warning: No permission to bind to interface %s\n"
 
286
                                     % self.options.interface)
458
287
                else:
459
288
                    raise error
460
289
        # Only bind(2) the socket if we really need to.
504
333
 
505
334
 
506
335
def add_service():
507
 
    """Derived from the Avahi example code"""
 
336
    """From the Avahi server example code"""
508
337
    global group, serviceName, serviceType, servicePort, serviceTXT, \
509
338
           domain, host
510
339
    if group is None:
514
343
                avahi.DBUS_INTERFACE_ENTRY_GROUP)
515
344
        group.connect_to_signal('StateChanged',
516
345
                                entry_group_state_changed)
517
 
    logger.debug(u"Adding service '%s' of type '%s' ...",
518
 
                 serviceName, serviceType)
 
346
    
 
347
    # print "Adding service '%s' of type '%s' ..." % (serviceName,
 
348
    #                                                 serviceType)
519
349
    
520
350
    group.AddService(
521
351
            serviceInterface,           # interface
529
359
 
530
360
 
531
361
def remove_service():
532
 
    """From the Avahi example code"""
 
362
    """From the Avahi server example code"""
533
363
    global group
534
364
    
535
365
    if not group is None:
537
367
 
538
368
 
539
369
def server_state_changed(state):
540
 
    """Derived from the Avahi example code"""
 
370
    """From the Avahi server example code"""
541
371
    if state == avahi.SERVER_COLLISION:
542
 
        logger.warning(u"Server name collision")
 
372
        print "WARNING: Server name collision"
543
373
        remove_service()
544
374
    elif state == avahi.SERVER_RUNNING:
545
375
        add_service()
546
376
 
547
377
 
548
378
def entry_group_state_changed(state, error):
549
 
    """Derived from the Avahi example code"""
 
379
    """From the Avahi server example code"""
550
380
    global serviceName, server, rename_count
551
381
    
552
 
    logger.debug(u"state change: %i", state)
 
382
    # print "state change: %i" % state
553
383
    
554
384
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
555
 
        logger.debug(u"Service established.")
 
385
        pass
 
386
        # print "Service established."
556
387
    elif state == avahi.ENTRY_GROUP_COLLISION:
557
388
        
558
389
        rename_count = rename_count - 1
559
390
        if rename_count > 0:
560
391
            name = server.GetAlternativeServiceName(name)
561
 
            logger.warning(u"Service name collision, "
562
 
                           u"changing name to '%s' ...", name)
 
392
            print "WARNING: Service name collision, changing name to '%s' ..." % name
563
393
            remove_service()
564
394
            add_service()
565
395
            
566
396
        else:
567
 
            logger.error(u"No suitable service name found after %i"
568
 
                         u" retries, exiting.", n_rename)
569
 
            killme(1)
 
397
            print "ERROR: No suitable service name found after %i retries, exiting." % n_rename
 
398
            main_loop.quit()
570
399
    elif state == avahi.ENTRY_GROUP_FAILURE:
571
 
        logger.error(u"Error in group state changed %s",
572
 
                     unicode(error))
573
 
        killme(1)
 
400
        print "Error in group state changed", error
 
401
        main_loop.quit()
 
402
        return
574
403
 
575
404
 
576
405
def if_nametoindex(interface):
577
406
    """Call the C function if_nametoindex()"""
578
407
    try:
 
408
        if "ctypes" not in sys.modules:
 
409
            import ctypes
579
410
        libc = ctypes.cdll.LoadLibrary("libc.so.6")
580
411
        return libc.if_nametoindex(interface)
581
 
    except (OSError, AttributeError):
 
412
    except (ImportError, OSError, AttributeError):
582
413
        if "struct" not in sys.modules:
583
414
            import struct
584
415
        if "fcntl" not in sys.modules:
592
423
        return interface_index
593
424
 
594
425
 
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
 
def main():
626
 
    global exitstatus
627
 
    exitstatus = 0
628
 
    global main_loop_started
629
 
    main_loop_started = False
630
 
    
 
426
if __name__ == '__main__':
631
427
    parser = OptionParser()
632
428
    parser.add_option("-i", "--interface", type="string",
633
429
                      default=None, metavar="IF",
634
430
                      help="Bind to interface IF")
635
 
    parser.add_option("-a", "--address", type="string", default=None,
636
 
                      help="Address to listen for requests on")
 
431
    parser.add_option("--cert", type="string", default="cert.pem",
 
432
                      metavar="FILE",
 
433
                      help="Public key certificate PEM file to use")
 
434
    parser.add_option("--key", type="string", default="key.pem",
 
435
                      metavar="FILE",
 
436
                      help="Private key PEM file to use")
 
437
    parser.add_option("--ca", type="string", default="ca.pem",
 
438
                      metavar="FILE",
 
439
                      help="Certificate Authority certificate PEM file to use")
 
440
    parser.add_option("--crl", type="string", default="crl.pem",
 
441
                      metavar="FILE",
 
442
                      help="Certificate Revokation List PEM file to use")
637
443
    parser.add_option("-p", "--port", type="int", default=None,
638
444
                      help="Port number to receive requests on")
639
445
    parser.add_option("--timeout", type="string", # Parsed later
644
450
                      help="How often to check that a client is up")
645
451
    parser.add_option("--check", action="store_true", default=False,
646
452
                      help="Run self-test")
647
 
    parser.add_option("--debug", action="store_true", default=False,
648
 
                      help="Debug mode")
649
453
    (options, args) = parser.parse_args()
650
454
    
651
455
    if options.check:
663
467
    except ValueError:
664
468
        parser.error("option --interval: Unparseable time")
665
469
    
 
470
    cert = gnutls.crypto.X509Certificate(open(options.cert).read())
 
471
    key = gnutls.crypto.X509PrivateKey(open(options.key).read())
 
472
    ca = gnutls.crypto.X509Certificate(open(options.ca).read())
 
473
    crl = gnutls.crypto.X509CRL(open(options.crl).read())
 
474
    cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
 
475
    
666
476
    # Parse config file
667
 
    defaults = { "checker": "fping -q -- %%(fqdn)s" }
 
477
    defaults = {}
668
478
    client_config = ConfigParser.SafeConfigParser(defaults)
669
 
    #client_config.readfp(open("global.conf"), "global.conf")
 
479
    #client_config.readfp(open("secrets.conf"), "secrets.conf")
670
480
    client_config.read("mandos-clients.conf")
671
481
    
672
 
    global main_loop
673
 
    global bus
674
 
    global server
675
 
    # From the Avahi example code
 
482
    # From the Avahi server example code
676
483
    DBusGMainLoop(set_as_default=True )
677
484
    main_loop = gobject.MainLoop()
678
485
    bus = dbus.SystemBus()
681
488
            avahi.DBUS_INTERFACE_SERVER )
682
489
    # End of Avahi example code
683
490
    
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
491
    clients = Set()
695
492
    def remove_from_clients(client):
696
493
        clients.remove(client)
697
494
        if not clients:
698
 
            logger.debug(u"No clients left, exiting")
699
 
            killme()
 
495
            print "No clients left, exiting"
 
496
            main_loop.quit()
700
497
    
701
498
    clients.update(Set(Client(name=section, options=options,
702
499
                              stop_hook = remove_from_clients,
703
500
                              **(dict(client_config\
704
501
                                      .items(section))))
705
502
                       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
503
    for client in clients:
732
504
        client.start()
733
505
    
734
 
    tcp_server = IPv6_TCPServer((options.address, options.port),
 
506
    tcp_server = IPv6_TCPServer((None, options.port),
735
507
                                tcp_handler,
736
508
                                options=options,
737
 
                                clients=clients)
 
509
                                clients=clients,
 
510
                                credentials=cred)
738
511
    # Find out what random port we got
739
 
    global servicePort
740
512
    servicePort = tcp_server.socket.getsockname()[1]
741
 
    logger.debug(u"Now listening on port %d", servicePort)
 
513
    #sys.stderr.write("Now listening on port %d\n" % servicePort)
742
514
    
743
515
    if options.interface is not None:
744
 
        global serviceInterface
745
516
        serviceInterface = if_nametoindex(options.interface)
746
517
    
747
 
    # From the Avahi example code
 
518
    # From the Avahi server example code
748
519
    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)
 
520
    server_state_changed(server.GetState())
754
521
    # End of Avahi example code
755
522
    
756
523
    gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
758
525
                         tcp_server.handle_request(*args[2:],
759
526
                                                   **kwargs) or True)
760
527
    try:
761
 
        logger.debug("Starting main loop")
762
 
        main_loop_started = True
763
528
        main_loop.run()
764
529
    except KeyboardInterrupt:
765
 
        if debug:
766
 
            print
 
530
        print
767
531
    
768
 
    sys.exit(exitstatus)
 
532
    # Cleanup here
769
533
 
770
 
if __name__ == '__main__':
771
 
    main()
 
534
    # From the Avahi server example code
 
535
    if not group is None:
 
536
        group.Free()
 
537
    # End of Avahi example code
 
538
    
 
539
    for client in clients:
 
540
        client.stop_hook = None
 
541
        client.stop()