/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-12-11 23:40:35 UTC
  • Revision ID: belorn@braxen-20071211234035-m1nsu41vuzkak69h
Python based server
Added client configfile

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
2
2
 
3
 
from __future__ import division
4
 
 
5
3
import SocketServer
6
4
import socket
7
5
import select
12
10
import gnutls.connection
13
11
import gnutls.errors
14
12
import ConfigParser
15
 
import sys
16
 
import re
17
 
import os
18
 
import signal
19
 
from sets import Set
20
 
import subprocess
21
13
 
22
14
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):
 
15
    def __init__(self, name=None, dn=None, password=None,
 
16
                 passfile=None, fqdn=None, timeout=None,
 
17
                 interval=-1):
26
18
        self.name = name
27
19
        self.dn = dn
28
20
        if password:
33
25
            print "No Password or Passfile in client config file"
34
26
            # raise RuntimeError XXX
35
27
            self.password = "gazonk"
36
 
        self.fqdn = fqdn                # string
37
 
        self.created = datetime.datetime.now()
38
 
        self.last_seen = None           # datetime.datetime()
 
28
        self.fqdn = fqdn
 
29
        # self.created = ...
 
30
        self.last_seen = None
39
31
        if timeout is None:
40
 
            timeout = options.timeout
41
 
        self.timeout = timeout          # datetime.timedelta()
 
32
            timeout = self.server.options.timeout
 
33
        self.timeout = timeout
42
34
        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()
 
35
            interval = self.server.options.interval
 
36
        self.interval = interval
 
37
 
 
38
def server_bind(self):
 
39
    if self.options.interface:
 
40
        if not hasattr(socket, "SO_BINDTODEVICE"):
 
41
            # From /usr/include/asm-i486/socket.h
 
42
            socket.SO_BINDTODEVICE = 25
70
43
        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)
 
44
            self.socket.setsockopt(socket.SOL_SOCKET,
 
45
                                   socket.SO_BINDTODEVICE,
 
46
                                   self.options.interface)
 
47
        except socket.error, error:
 
48
            if error[0] == errno.EPERM:
 
49
                print "Warning: Denied permission to bind to interface", \
 
50
                      self.options.interface
 
51
            else:
 
52
                raise error
 
53
    return super(type(self), self).server_bind()
 
54
 
 
55
 
 
56
def init_with_options(self, *args, **kwargs):
 
57
    if "options" in kwargs:
 
58
        self.options = kwargs["options"]
 
59
        del kwargs["options"]
 
60
    if "clients" in kwargs:
 
61
        self.clients = kwargs["clients"]
 
62
        del kwargs["clients"]
 
63
    if "credentials" in kwargs:
 
64
        self.credentials = kwargs["credentials"]
 
65
        del kwargs["credentials"]
 
66
    return super(type(self), self).__init__(*args, **kwargs)
151
67
 
152
68
 
153
69
class udp_handler(SocketServer.DatagramRequestHandler, object):
157
73
 
158
74
 
159
75
class IPv6_UDPServer(SocketServer.UDPServer, object):
160
 
    __metaclass__ = server_metaclass
 
76
    __init__ = init_with_options
 
77
    address_family = socket.AF_INET6
 
78
    allow_reuse_address = True
 
79
    server_bind = server_bind
161
80
    def verify_request(self, request, client_address):
162
81
        print "UDP request came"
163
82
        return request[0] == "Marco"
181
100
            session.bye()
182
101
            return
183
102
        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:
 
103
            session.send(dict((client.dn, client.password)
 
104
                              for client in self.server.clients)
 
105
                         [session.peer_certificate.subject])
 
106
        except KeyError:
189
107
            session.send("gazonk")
190
108
            # Log maybe? XXX
191
109
        session.bye()
192
110
 
193
 
 
194
111
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
 
 
 
112
    __init__ = init_with_options
 
113
    address_family = socket.AF_INET6
 
114
    allow_reuse_address = True
 
115
    request_queue_size = 1024
 
116
    server_bind = server_bind
 
117
 
 
118
 
 
119
in6addr_any = "::"
 
120
 
 
121
cred = None
231
122
 
232
123
def main():
233
124
    parser = OptionParser()
253
144
    parser.add_option("-t", "--timeout", type="string", # Parsed later
254
145
                      default="15m",
255
146
                      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
147
    (options, args) = parser.parse_args()
262
 
 
263
 
    if options.check:
264
 
        import doctest
265
 
        doctest.testmod()
266
 
        sys.exit()
267
148
    
268
 
    # Parse the time arguments
 
149
    # Parse the time argument
269
150
    try:
270
 
        options.timeout = string_to_delta(options.timeout)
271
 
    except ValueError:
 
151
        suffix=options.timeout[-1]
 
152
        value=int(options.timeout[:-1])
 
153
        if suffix == "d":
 
154
            options.timeout = datetime.timedelta(value)
 
155
        elif suffix == "s":
 
156
            options.timeout = datetime.timedelta(0, value)
 
157
        elif suffix == "m":
 
158
            options.timeout = datetime.timedelta(0, 0, 0, 0, value)
 
159
        elif suffix == "h":
 
160
            options.timeout = datetime.timedelta(0, 0, 0, 0, 0, value)
 
161
        elif suffix == "w":
 
162
            options.timeout = datetime.timedelta(0, 0, 0, 0, 0, 0,
 
163
                                                 value)
 
164
        else:
 
165
            raise ValueError
 
166
    except (ValueError, IndexError):
272
167
        parser.error("option --timeout: Unparseable time")
273
168
    
274
 
    try:
275
 
        options.interval = string_to_delta(options.interval)
276
 
    except ValueError:
277
 
        parser.error("option --interval: Unparseable time")
278
 
    
279
169
    cert = gnutls.crypto.X509Certificate(open(options.cert).read())
280
170
    key = gnutls.crypto.X509PrivateKey(open(options.key).read())
281
171
    ca = gnutls.crypto.X509Certificate(open(options.ca).read())
286
176
    defaults = {}
287
177
    client_config_object = ConfigParser.SafeConfigParser(defaults)
288
178
    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())
 
179
    clients = [Client(name=section,
 
180
                      **(dict(client_config_object.items(section))))
 
181
               for section in client_config_object.sections()]
293
182
    
294
 
    in6addr_any = "::"
295
183
    udp_server = IPv6_UDPServer((in6addr_any, options.port),
296
184
                                udp_handler,
297
185
                                options=options)
303
191
                                credentials=cred)
304
192
    
305
193
    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()
 
194
        in_, out, err = select.select((udp_server,
 
195
                                       tcp_server), (), ())
 
196
        for server in in_:
 
197
            server.handle_request()
351
198
 
352
199
 
353
200
if __name__ == "__main__":