/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk
3 by Björn Påhlsson
Python based server
1
#!/usr/bin/python
2
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
3
from __future__ import division
4
3 by Björn Påhlsson
Python based server
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
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
15
import sys
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
16
import re
17
import os
18
import signal
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
19
from sets import Set
20
import subprocess
5 by Teddy Hogeborn
* server.py (server_metaclass): New.
21
3 by Björn Påhlsson
Python based server
22
class Client(object):
5 by Teddy Hogeborn
* server.py (server_metaclass): New.
23
    def __init__(self, name=None, options=None, dn=None,
24
                 password=None, passfile=None, fqdn=None,
25
                 timeout=None, interval=-1):
3 by Björn Påhlsson
Python based server
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"
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
36
        self.fqdn = fqdn                # string
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
37
        self.created = datetime.datetime.now()
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
38
        self.last_seen = None           # datetime.datetime()
3 by Björn Påhlsson
Python based server
39
        if timeout is None:
5 by Teddy Hogeborn
* server.py (server_metaclass): New.
40
            timeout = options.timeout
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
41
        self.timeout = timeout          # datetime.timedelta()
3 by Björn Påhlsson
Python based server
42
        if interval == -1:
5 by Teddy Hogeborn
* server.py (server_metaclass): New.
43
            interval = options.interval
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
44
        else:
45
            interval = string_to_delta(interval)
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
46
        self.interval = interval        # datetime.timedelta()
47
        self.next_check = datetime.datetime.now() # datetime.datetime()
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
48
        # Note: next_check may be in the past if checker is not None
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
49
        self.checker = None             # or a subprocess.Popen()
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
50
    def check_action(self):
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
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()
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
56
            #print "Checker for %(name)s said nothing?" % vars(self)
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
57
            return
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
58
        now = datetime.datetime.now()
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
59
        if self.checker.returncode == 0:
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
60
            print "Checker for %(name)s succeeded" % vars(self)
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
61
            self.last_seen = now
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
62
        else:
63
            print "Checker for %(name)s failed" % vars(self)
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
64
        while self.next_check <= now:
65
            self.next_check += self.interval
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
66
        self.checker = None
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
67
    handle_request = check_action
68
    def start_checker(self):
69
        self.stop_checker()
70
        try:
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
71
            self.checker = subprocess.Popen("sleep 10; fping -q -- %s"
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
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):
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
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
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
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()
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
106
        if self.last_seen is None:
107
            return now < (self.created + self.timeout)
108
        else:
109
            return now < (self.last_seen + self.timeout)
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
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
3 by Björn Påhlsson
Python based server
114
5 by Teddy Hogeborn
* server.py (server_metaclass): New.
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)
3 by Björn Påhlsson
Python based server
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):
5 by Teddy Hogeborn
* server.py (server_metaclass): New.
160
    __metaclass__ = server_metaclass
3 by Björn Påhlsson
Python based server
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:
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
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:
3 by Björn Påhlsson
Python based server
189
            session.send("gazonk")
190
            # Log maybe? XXX
191
        session.bye()
192
5 by Teddy Hogeborn
* server.py (server_metaclass): New.
193
3 by Björn Påhlsson
Python based server
194
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
5 by Teddy Hogeborn
* server.py (server_metaclass): New.
195
    __metaclass__ = server_metaclass
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
196
3 by Björn Påhlsson
Python based server
197
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
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
8 by Teddy Hogeborn
* Makefile (client_debug): Bug fix; add quotes and / to CERT_ROOT.
231
3 by Björn Påhlsson
Python based server
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")
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
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")
3 by Björn Påhlsson
Python based server
261
    (options, args) = parser.parse_args()
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
262
263
    if options.check:
264
        import doctest
265
        doctest.testmod()
266
        sys.exit()
3 by Björn Påhlsson
Python based server
267
    
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
268
    # Parse the time arguments
3 by Björn Påhlsson
Python based server
269
    try:
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
270
        options.timeout = string_to_delta(options.timeout)
271
    except ValueError:
3 by Björn Påhlsson
Python based server
272
        parser.error("option --timeout: Unparseable time")
273
    
4 by Teddy Hogeborn
* server.py (Client.created, Client.next_check): New.
274
    try:
275
        options.interval = string_to_delta(options.interval)
276
    except ValueError:
277
        parser.error("option --interval: Unparseable time")
278
    
3 by Björn Påhlsson
Python based server
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")
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
289
    clients = Set(Client(name=section, options=options,
290
                         **(dict(client_config_object\
291
                                 .items(section))))
292
                  for section in client_config_object.sections())
3 by Björn Påhlsson
Python based server
293
    
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
294
    in6addr_any = "::"
3 by Björn Påhlsson
Python based server
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:
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
306
        if not clients:
307
            break
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
308
        try:
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
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)
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
330
                for obj in input:
331
                    obj.handle_request()
10 by Teddy Hogeborn
* server.py: Bug fix: Do "from __future__ import division".
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)
9 by Teddy Hogeborn
* client.cpp (main): Get t_old early since it is used on error exits.
345
        except KeyboardInterrupt:
346
            break
347
    
348
    # Cleanup here
349
    for client in clients:
350
        client.stop_checker()
3 by Björn Påhlsson
Python based server
351
352
353
if __name__ == "__main__":
354
    main()
355