/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: Björn Påhlsson
  • Date: 2007-10-20 21:38:25 UTC
  • Revision ID: belorn@tower-20071020213825-abf6f0d1c33ee961
First working version with: IPv6, GnuTLS, X.509 certificates, DN
retrieval.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
 
3
 
from __future__ import division
4
 
 
5
 
import SocketServer
6
 
import socket
7
 
import select
8
 
from optparse import OptionParser
9
 
import datetime
10
 
import errno
11
 
import gnutls.crypto
12
 
import gnutls.connection
13
 
import gnutls.errors
14
 
import ConfigParser
15
 
import sys
16
 
import re
17
 
import os
18
 
import signal
19
 
from sets import Set
20
 
import subprocess
21
 
 
22
 
class Client(object):
23
 
    def __init__(self, name=None, options=None, dn=None,
24
 
                 password=None, passfile=None, fqdn=None,
25
 
                 timeout=None, interval=-1):
26
 
        self.name = name
27
 
        self.dn = dn
28
 
        if password:
29
 
            self.password = password
30
 
        elif passfile:
31
 
            self.password = open(passfile).readall()
32
 
        else:
33
 
            print "No Password or Passfile in client config file"
34
 
            # raise RuntimeError XXX
35
 
            self.password = "gazonk"
36
 
        self.fqdn = fqdn                # string
37
 
        self.created = datetime.datetime.now()
38
 
        self.last_seen = None           # datetime.datetime()
39
 
        if timeout is None:
40
 
            timeout = options.timeout
41
 
        self.timeout = timeout          # datetime.timedelta()
42
 
        if interval == -1:
43
 
            interval = options.interval
44
 
        else:
45
 
            interval = string_to_delta(interval)
46
 
        self.interval = interval        # datetime.timedelta()
47
 
        self.next_check = datetime.datetime.now() # datetime.datetime()
48
 
        # Note: next_check may be in the past if checker is not None
49
 
        self.checker = None             # or a subprocess.Popen()
50
 
    def check_action(self):
51
 
        """The checker said something and might have completed.
52
 
        Check if is has, and take appropriate actions."""
53
 
        if self.checker.poll() is None:
54
 
            # False alarm, no result yet
55
 
            #self.checker.read()
56
 
            #print "Checker for %(name)s said nothing?" % vars(self)
57
 
            return
58
 
        now = datetime.datetime.now()
59
 
        if self.checker.returncode == 0:
60
 
            print "Checker for %(name)s succeeded" % vars(self)
61
 
            self.last_seen = now
62
 
        else:
63
 
            print "Checker for %(name)s failed" % vars(self)
64
 
        while self.next_check <= now:
65
 
            self.next_check += self.interval
66
 
        self.checker = None
67
 
    handle_request = check_action
68
 
    def start_checker(self):
69
 
        self.stop_checker()
70
 
        try:
71
 
            self.checker = subprocess.Popen("sleep 10; fping -q -- %s"
72
 
                                            % re.escape(self.fqdn),
73
 
                                            stdout=subprocess.PIPE,
74
 
                                            close_fds=True,
75
 
                                            shell=True, cwd="/")
76
 
        except subprocess.OSError, e:
77
 
            print "Failed to start subprocess:", e
78
 
    def stop_checker(self):
79
 
        if self.checker is None:
80
 
            return
81
 
        os.kill(self.checker.pid, signal.SIGTERM)
82
 
        if self.checker.poll() is None:
83
 
            os.kill(self.checker.pid, signal.SIGKILL)
84
 
        self.checker = None
85
 
    __del__ = stop_checker
86
 
    def fileno(self):
87
 
        if self.checker is None:
88
 
            return None
89
 
        return self.checker.stdout.fileno()
90
 
    def next_stop(self):
91
 
        """The time when something must be done about this client
92
 
        May be in the past."""
93
 
        if self.last_seen is None:
94
 
            # This client has never been seen
95
 
            next_timeout = self.created + self.timeout
96
 
        else:
97
 
            next_timeout = self.last_seen + self.timeout
98
 
        if self.checker is None:
99
 
            return min(next_timeout, self.next_check)
100
 
        else:
101
 
            return next_timeout
102
 
    def still_valid(self, now=None):
103
 
        """Has this client's timeout not passed?"""
104
 
        if now is None:
105
 
            now = datetime.datetime.now()
106
 
        if self.last_seen is None:
107
 
            return now < (self.created + self.timeout)
108
 
        else:
109
 
            return now < (self.last_seen + self.timeout)
110
 
    def it_is_time_to_check(self, now=None):
111
 
        if now is None:
112
 
            now = datetime.datetime.now()
113
 
        return self.next_check <= now
114
 
 
115
 
 
116
 
class server_metaclass(type):
117
 
    "Common behavior for the UDP and TCP server classes"
118
 
    def __new__(cls, name, bases, attrs):
119
 
        attrs["address_family"] = socket.AF_INET6
120
 
        attrs["allow_reuse_address"] = True
121
 
        def server_bind(self):
122
 
            if self.options.interface:
123
 
                if not hasattr(socket, "SO_BINDTODEVICE"):
124
 
                    # From /usr/include/asm-i486/socket.h
125
 
                    socket.SO_BINDTODEVICE = 25
126
 
                try:
127
 
                    self.socket.setsockopt(socket.SOL_SOCKET,
128
 
                                           socket.SO_BINDTODEVICE,
129
 
                                           self.options.interface)
130
 
                except socket.error, error:
131
 
                    if error[0] == errno.EPERM:
132
 
                        print "Warning: No permission to bind to interface", \
133
 
                              self.options.interface
134
 
                    else:
135
 
                        raise error
136
 
            return super(type(self), self).server_bind()
137
 
        attrs["server_bind"] = server_bind
138
 
        def init(self, *args, **kwargs):
139
 
            if "options" in kwargs:
140
 
                self.options = kwargs["options"]
141
 
                del kwargs["options"]
142
 
            if "clients" in kwargs:
143
 
                self.clients = kwargs["clients"]
144
 
                del kwargs["clients"]
145
 
            if "credentials" in kwargs:
146
 
                self.credentials = kwargs["credentials"]
147
 
                del kwargs["credentials"]
148
 
            return super(type(self), self).__init__(*args, **kwargs)
149
 
        attrs["__init__"] = init
150
 
        return type.__new__(cls, name, bases, attrs)
151
 
 
152
 
 
153
 
class udp_handler(SocketServer.DatagramRequestHandler, object):
154
 
    def handle(self):
155
 
        self.wfile.write("Polo")
156
 
        print "UDP request answered"
157
 
 
158
 
 
159
 
class IPv6_UDPServer(SocketServer.UDPServer, object):
160
 
    __metaclass__ = server_metaclass
161
 
    def verify_request(self, request, client_address):
162
 
        print "UDP request came"
163
 
        return request[0] == "Marco"
164
 
 
165
 
 
166
 
class tcp_handler(SocketServer.BaseRequestHandler, object):
167
 
    def handle(self):
168
 
        print "TCP request came"
169
 
        print "Request:", self.request
170
 
        print "Client Address:", self.client_address
171
 
        print "Server:", self.server
172
 
        session = gnutls.connection.ServerSession(self.request,
173
 
                                                  self.server.credentials)
174
 
        session.handshake()
175
 
        if session.peer_certificate:
176
 
            print "DN:", session.peer_certificate.subject
177
 
        try:
178
 
            session.verify_peer()
179
 
        except gnutls.errors.CertificateError, error:
180
 
            print "Verify failed", error
181
 
            session.bye()
182
 
            return
183
 
        try:
184
 
            session.send([client.password
185
 
                          for client in self.server.clients
186
 
                          if (client.dn ==
187
 
                              session.peer_certificate.subject)][0])
188
 
        except IndexError:
189
 
            session.send("gazonk")
190
 
            # Log maybe? XXX
191
 
        session.bye()
192
 
 
193
 
 
194
 
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
195
 
    __metaclass__ = server_metaclass
196
 
 
197
 
 
198
 
def string_to_delta(interval):
199
 
    """Parse a string and return a datetime.timedelta
200
 
 
201
 
    >>> string_to_delta('7d')
202
 
    datetime.timedelta(7)
203
 
    >>> string_to_delta('60s')
204
 
    datetime.timedelta(0, 60)
205
 
    >>> string_to_delta('60m')
206
 
    datetime.timedelta(0, 3600)
207
 
    >>> string_to_delta('24h')
208
 
    datetime.timedelta(1)
209
 
    >>> string_to_delta(u'1w')
210
 
    datetime.timedelta(7)
211
 
    """
212
 
    try:
213
 
        suffix=unicode(interval[-1])
214
 
        value=int(interval[:-1])
215
 
        if suffix == u"d":
216
 
            delta = datetime.timedelta(value)
217
 
        elif suffix == u"s":
218
 
            delta = datetime.timedelta(0, value)
219
 
        elif suffix == u"m":
220
 
            delta = datetime.timedelta(0, 0, 0, 0, value)
221
 
        elif suffix == u"h":
222
 
            delta = datetime.timedelta(0, 0, 0, 0, 0, value)
223
 
        elif suffix == u"w":
224
 
            delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
225
 
        else:
226
 
            raise ValueError
227
 
    except (ValueError, IndexError):
228
 
        raise ValueError
229
 
    return delta
230
 
 
231
 
 
232
 
def main():
233
 
    parser = OptionParser()
234
 
    parser.add_option("-i", "--interface", type="string",
235
 
                      default="eth0", metavar="IF",
236
 
                      help="Interface to bind to")
237
 
    parser.add_option("--cert", type="string", default="cert.pem",
238
 
                      metavar="FILE",
239
 
                      help="Public key certificate to use")
240
 
    parser.add_option("--key", type="string", default="key.pem",
241
 
                      metavar="FILE",
242
 
                      help="Private key to use")
243
 
    parser.add_option("--ca", type="string", default="ca.pem",
244
 
                      metavar="FILE",
245
 
                      help="Certificate Authority certificate to use")
246
 
    parser.add_option("--crl", type="string", default="crl.pem",
247
 
                      metavar="FILE",
248
 
                      help="Certificate Revokation List to use")
249
 
    parser.add_option("-p", "--port", type="int", default=49001,
250
 
                      help="Port number to receive requests on")
251
 
    parser.add_option("--dh", type="int", metavar="BITS",
252
 
                      help="DH group to use")
253
 
    parser.add_option("-t", "--timeout", type="string", # Parsed later
254
 
                      default="15m",
255
 
                      help="Amount of downtime allowed for clients")
256
 
    parser.add_option("--interval", type="string", # Parsed later
257
 
                      default="5m",
258
 
                      help="How often to check that a client is up")
259
 
    parser.add_option("--check", action="store_true", default=False,
260
 
                      help="Run self-test")
261
 
    (options, args) = parser.parse_args()
262
 
 
263
 
    if options.check:
264
 
        import doctest
265
 
        doctest.testmod()
266
 
        sys.exit()
267
 
    
268
 
    # Parse the time arguments
269
 
    try:
270
 
        options.timeout = string_to_delta(options.timeout)
271
 
    except ValueError:
272
 
        parser.error("option --timeout: Unparseable time")
273
 
    
274
 
    try:
275
 
        options.interval = string_to_delta(options.interval)
276
 
    except ValueError:
277
 
        parser.error("option --interval: Unparseable time")
278
 
    
279
 
    cert = gnutls.crypto.X509Certificate(open(options.cert).read())
280
 
    key = gnutls.crypto.X509PrivateKey(open(options.key).read())
281
 
    ca = gnutls.crypto.X509Certificate(open(options.ca).read())
282
 
    crl = gnutls.crypto.X509CRL(open(options.crl).read())
283
 
    cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
284
 
    
285
 
    # Parse config file
286
 
    defaults = {}
287
 
    client_config_object = ConfigParser.SafeConfigParser(defaults)
288
 
    client_config_object.read("mandos-clients.conf")
289
 
    clients = Set(Client(name=section, options=options,
290
 
                         **(dict(client_config_object\
291
 
                                 .items(section))))
292
 
                  for section in client_config_object.sections())
293
 
    
294
 
    in6addr_any = "::"
295
 
    udp_server = IPv6_UDPServer((in6addr_any, options.port),
296
 
                                udp_handler,
297
 
                                options=options)
298
 
    
299
 
    tcp_server = IPv6_TCPServer((in6addr_any, options.port),
300
 
                                tcp_handler,
301
 
                                options=options,
302
 
                                clients=clients,
303
 
                                credentials=cred)
304
 
    
305
 
    while True:
306
 
        if not clients:
307
 
            break
308
 
        try:
309
 
            next_stop = min(client.next_stop() for client in clients)
310
 
            now = datetime.datetime.now()
311
 
            if next_stop > now:
312
 
                delay = next_stop - now
313
 
                delay_seconds = (delay.days * 24 * 60 * 60
314
 
                                 + delay.seconds
315
 
                                 + delay.microseconds / 1000000)
316
 
                clients_with_checkers = tuple(client for client in
317
 
                                              clients
318
 
                                              if client.checker
319
 
                                              is not None)
320
 
                input_checks = (udp_server, tcp_server) \
321
 
                               + clients_with_checkers
322
 
                print "Waiting for network",
323
 
                if clients_with_checkers:
324
 
                    print "and checkers for:",
325
 
                    for client in clients_with_checkers:
326
 
                        print client.name,
327
 
                print
328
 
                input, out, err = select.select(input_checks, (), (),
329
 
                                                delay_seconds)
330
 
                for obj in input:
331
 
                    obj.handle_request()
332
 
            # start new checkers
333
 
            for client in clients:
334
 
                if client.it_is_time_to_check(now=now) and \
335
 
                       client.checker is None:
336
 
                    print "Starting checker for client %(name)s" \
337
 
                          % vars(client)
338
 
                    client.start_checker()
339
 
            # delete timed-out clients
340
 
            for client in clients.copy():
341
 
                if not client.still_valid(now=now):
342
 
                    # log xxx
343
 
                    print "Removing client %(name)s" % vars(client)
344
 
                    clients.remove(client)
345
 
        except KeyboardInterrupt:
346
 
            break
347
 
    
348
 
    # Cleanup here
349
 
    for client in clients:
350
 
        client.stop_checker()
351
 
 
352
 
 
353
 
if __name__ == "__main__":
354
 
    main()
355