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