/mandos/release

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