/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: Teddy Hogeborn
  • Date: 2008-07-19 18:43:24 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080719184324-iwhoa5in75xa0u2u
* mandos-clients.conf ([foo]/dn, [foo]/password, [braxen_client]/dn,
                       [braxen_client]/password): Removed.
  ([foo]/fingerprint, [braxen_client]/fingerprint): New.
  ([foo]/checker): New.
  ([foo]/secfile): New.
  ([braxen_client]/secret): New.

* server.py: New "--debug" option to set debug flag.  Removed "cert",
             "key", "ca", "crl", and "cred" variables.  Added default
             value for "checker" config file setting.  Do not pass
             credentials to IPv6_TCPServer constructor.
  (debug): New global debug flag.  Used by most debugging output code.
  (Client.__init__): Keyword argument "dn" replaced by "fingerprint",
                     "password" renamed to "secret", and "passfile"
                     renamed to "secfile".  New keyword argument
                     "checker". All callers changed.
  (Client.dn): Removed.
  (Client.fingerprint): New.
  (Client.password): Renamed to "secret"; all users changed.
  (Client.passfile): Renamed to "secfile"; all users changed.
  (Client.timeout, Client.interval): Changed to be properties; now
                                     automatically updates the
                                     "_timeout_milliseconds" and
                                     "_interval_milliseconds" values.
  (Client.timeout_milliseconds): Renamed to "_timeout_milliseconds".
  (Client.interval_milliseconds): Renamed to "_interval_milliseconds".
  (Client.check_command): New.
  (Client.start_checker): Use the new "check_command" attribute.
  (peer_certificate, fingerprint): New functions.

  (tcp_handler.handle): Use ClientSession with empty credentials
                        object instead of ServerSession.  Set gnutls
                        priority string.  Do not verify peer.  Use
                        fingerprint instead of DN when searching for
                        clients.  Bug fix: Loop sending data so even large
                        secret data strings are sent.
  (IPv6_TCPServer.credentials): Removed.
  (if_nametoindex): Do not import ctypes since that is now imported
                    globally.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
2
 
# -*- mode: python; coding: utf-8 -*-
3
2
 
4
3
from __future__ import division
5
4
 
22
21
import signal
23
22
from sets import Set
24
23
import subprocess
25
 
import atexit
26
 
import stat
27
24
 
28
25
import dbus
29
26
import gobject
31
28
from dbus.mainloop.glib import DBusGMainLoop
32
29
import ctypes
33
30
 
34
 
import logging
35
 
import logging.handlers
36
 
 
37
 
logger = logging.Logger('mandos')
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
44
 
 
45
31
# This variable is used to optionally bind to a specified interface.
46
32
# It is a global variable to fit in with the other variables from the
47
33
# Avahi server example code.
113
99
                        _set_interval)
114
100
    del _set_interval
115
101
    def __init__(self, name=None, options=None, stop_hook=None,
116
 
                 fingerprint=None, secret=None, secfile=None,
117
 
                 fqdn=None, timeout=None, interval=-1, checker=None):
 
102
                 fingerprint=None, secret=None, secfile=None, fqdn=None,
 
103
                 timeout=None, interval=-1, checker=None):
118
104
        self.name = name
119
105
        # Uppercase and remove spaces from fingerprint
120
106
        # for later comparison purposes with return value of
133
119
        self.created = datetime.datetime.now()
134
120
        self.last_seen = None
135
121
        if timeout is None:
136
 
            self.timeout = options.timeout
137
 
        else:
138
 
            self.timeout = string_to_delta(timeout)
 
122
            timeout = options.timeout
 
123
        self.timeout = timeout
139
124
        if interval == -1:
140
 
            self.interval = options.interval
 
125
            interval = options.interval
141
126
        else:
142
 
            self.interval = string_to_delta(interval)
 
127
            interval = string_to_delta(interval)
 
128
        self.interval = interval
143
129
        self.stop_hook = stop_hook
144
130
        self.checker = None
145
131
        self.checker_initiator_tag = None
163
149
        """Stop this client.
164
150
        The possibility that this client might be restarted is left
165
151
        open, but not currently used."""
166
 
        logger.debug(u"Stopping client %s", self.name)
 
152
        if debug:
 
153
            sys.stderr.write(u"Stopping client %s\n" % self.name)
167
154
        self.secret = None
168
155
        if self.stop_initiator_tag:
169
156
            gobject.source_remove(self.stop_initiator_tag)
192
179
        now = datetime.datetime.now()
193
180
        if os.WIFEXITED(condition) \
194
181
               and (os.WEXITSTATUS(condition) == 0):
195
 
            logger.debug(u"Checker for %(name)s succeeded",
196
 
                         vars(self))
 
182
            if debug:
 
183
                sys.stderr.write(u"Checker for %(name)s succeeded\n"
 
184
                                 % vars(self))
197
185
            self.last_seen = now
198
186
            gobject.source_remove(self.stop_initiator_tag)
199
187
            self.stop_initiator_tag = gobject.timeout_add\
200
188
                                      (self._timeout_milliseconds,
201
189
                                       self.stop)
202
 
        elif not os.WIFEXITED(condition):
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
 
190
        elif debug:
 
191
            if not os.WIFEXITED(condition):
 
192
                sys.stderr.write(u"Checker for %(name)s crashed?\n"
 
193
                                 % vars(self))
 
194
            else:
 
195
                sys.stderr.write(u"Checker for %(name)s failed\n"
 
196
                                 % vars(self))
 
197
        self.checker = None
209
198
        self.checker_callback_tag = None
210
199
    def start_checker(self):
211
200
        """Start a new checker subprocess if one is not running.
212
201
        If a checker already exists, leave it running and do
213
202
        nothing."""
214
203
        if self.checker is None:
 
204
            if debug:
 
205
                sys.stderr.write(u"Starting checker for %s\n"
 
206
                                 % self.name)
215
207
            try:
216
208
                command = self.check_command % self.fqdn
217
209
            except TypeError:
218
210
                escaped_attrs = dict((key, re.escape(str(val)))
219
211
                                     for key, val in
220
212
                                     vars(self).iteritems())
221
 
                try:
222
 
                    command = self.check_command % escaped_attrs
223
 
                except TypeError, error:
224
 
                    logger.critical(u'Could not format string "%s":'
225
 
                                    u' %s', self.check_command, error)
226
 
                    return True # Try again later
 
213
                command = self.check_command % escaped_attrs
227
214
            try:
228
 
                logger.debug(u"Starting checker %r for %s",
229
 
                             command, self.name)
230
215
                self.checker = subprocess.\
231
216
                               Popen(command,
 
217
                                     stdout=subprocess.PIPE,
232
218
                                     close_fds=True, shell=True,
233
219
                                     cwd="/")
234
 
                self.checker_callback_tag = gobject.child_watch_add\
235
 
                                            (self.checker.pid,
236
 
                                             self.checker_callback)
 
220
                self.checker_callback_tag = gobject.\
 
221
                                            child_watch_add(self.checker.pid,
 
222
                                                            self.\
 
223
                                                            checker_callback)
237
224
            except subprocess.OSError, error:
238
 
                logger.error(u"Failed to start subprocess: %s",
239
 
                             error)
 
225
                sys.stderr.write(u"Failed to start subprocess: %s\n"
 
226
                                 % error)
240
227
        # Re-run this periodically if run by gobject.timeout_add
241
228
        return True
242
229
    def stop_checker(self):
260
247
 
261
248
 
262
249
def peer_certificate(session):
263
 
    "Return an OpenPGP data packet string for the peer's certificate"
264
250
    # If not an OpenPGP certificate...
265
251
    if gnutls.library.functions.gnutls_certificate_type_get\
266
252
            (session._c_object) \
277
263
 
278
264
 
279
265
def fingerprint(openpgp):
280
 
    "Convert an OpenPGP data string to a hexdigit fingerprint string"
281
266
    # New empty GnuTLS certificate
282
267
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
283
268
    gnutls.library.functions.gnutls_openpgp_crt_init\
313
298
    Note: This will run in its own forked process."""
314
299
    
315
300
    def handle(self):
316
 
        logger.debug(u"TCP connection from: %s",
317
 
                     unicode(self.client_address))
 
301
        if debug:
 
302
            sys.stderr.write(u"TCP request came\n")
 
303
            sys.stderr.write(u"Request: %s\n" % self.request)
 
304
            sys.stderr.write(u"Client Address: %s\n"
 
305
                             % unicode(self.client_address))
 
306
            sys.stderr.write(u"Server: %s\n" % self.server)
318
307
        session = gnutls.connection.ClientSession(self.request,
319
308
                                                  gnutls.connection.\
320
309
                                                  X509Credentials())
330
319
        try:
331
320
            session.handshake()
332
321
        except gnutls.errors.GNUTLSError, error:
333
 
            logger.debug(u"Handshake failed: %s", error)
 
322
            if debug:
 
323
                sys.stderr.write(u"Handshake failed: %s\n" % error)
334
324
            # Do not run session.bye() here: the session is not
335
325
            # established.  Just abandon the request.
336
326
            return
337
327
        try:
338
328
            fpr = fingerprint(peer_certificate(session))
339
329
        except (TypeError, gnutls.errors.GNUTLSError), error:
340
 
            logger.debug(u"Bad certificate: %s", error)
 
330
            if debug:
 
331
                sys.stderr.write(u"Bad certificate: %s\n" % error)
341
332
            session.bye()
342
333
            return
343
 
        logger.debug(u"Fingerprint: %s", fpr)
 
334
        if debug:
 
335
            sys.stderr.write(u"Fingerprint: %s\n" % fpr)
344
336
        client = None
345
337
        for c in clients:
346
338
            if c.fingerprint == fpr:
350
342
        # that the client timed out while establishing the GnuTLS
351
343
        # session.
352
344
        if (not client) or (not client.still_valid()):
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)
 
345
            if debug:
 
346
                if client:
 
347
                    sys.stderr.write(u"Client %(name)s is invalid\n"
 
348
                                     % vars(client))
 
349
                else:
 
350
                    sys.stderr.write(u"Client not found for "
 
351
                                     u"fingerprint: %s\n" % fpr)
359
352
            session.bye()
360
353
            return
361
354
        sent_size = 0
362
355
        while sent_size < len(client.secret):
363
356
            sent = session.send(client.secret[sent_size:])
364
 
            logger.debug(u"Sent: %d, remaining: %d",
365
 
                         sent, len(client.secret)
366
 
                         - (sent_size + sent))
 
357
            if debug:
 
358
                sys.stderr.write(u"Sent: %d, remaining: %d\n"
 
359
                                 % (sent, len(client.secret)
 
360
                                    - (sent_size + sent)))
367
361
            sent_size += sent
368
362
        session.bye()
369
363
 
397
391
                                       self.options.interface)
398
392
            except socket.error, error:
399
393
                if error[0] == errno.EPERM:
400
 
                    logger.warning(u"No permission to"
401
 
                                   u" bind to interface %s",
402
 
                                   self.options.interface)
 
394
                    sys.stderr.write(u"Warning: No permission to" \
 
395
                                     u" bind to interface %s\n"
 
396
                                     % self.options.interface)
403
397
                else:
404
398
                    raise error
405
399
        # Only bind(2) the socket if we really need to.
459
453
                avahi.DBUS_INTERFACE_ENTRY_GROUP)
460
454
        group.connect_to_signal('StateChanged',
461
455
                                entry_group_state_changed)
462
 
    logger.debug(u"Adding service '%s' of type '%s' ...",
463
 
                 serviceName, serviceType)
 
456
    if debug:
 
457
        sys.stderr.write(u"Adding service '%s' of type '%s' ...\n"
 
458
                         % (serviceName, serviceType))
464
459
    
465
460
    group.AddService(
466
461
            serviceInterface,           # interface
484
479
def server_state_changed(state):
485
480
    """From the Avahi server example code"""
486
481
    if state == avahi.SERVER_COLLISION:
487
 
        logger.warning(u"Server name collision")
 
482
        sys.stderr.write(u"WARNING: Server name collision\n")
488
483
        remove_service()
489
484
    elif state == avahi.SERVER_RUNNING:
490
485
        add_service()
494
489
    """From the Avahi server example code"""
495
490
    global serviceName, server, rename_count
496
491
    
497
 
    logger.debug(u"state change: %i", state)
 
492
    if debug:
 
493
        sys.stderr.write(u"state change: %i\n" % state)
498
494
    
499
495
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
500
 
        logger.debug(u"Service established.")
 
496
        if debug:
 
497
            sys.stderr.write(u"Service established.\n")
501
498
    elif state == avahi.ENTRY_GROUP_COLLISION:
502
499
        
503
500
        rename_count = rename_count - 1
504
501
        if rename_count > 0:
505
502
            name = server.GetAlternativeServiceName(name)
506
 
            logger.warning(u"Service name collision, "
507
 
                           u"changing name to '%s' ...", name)
 
503
            sys.stderr.write(u"WARNING: Service name collision, "
 
504
                             u"changing name to '%s' ...\n" % name)
508
505
            remove_service()
509
506
            add_service()
510
507
            
511
508
        else:
512
 
            logger.error(u"No suitable service name found after %i"
513
 
                         u" retries, exiting.", n_rename)
514
 
            killme(1)
 
509
            sys.stderr.write(u"ERROR: No suitable service name found "
 
510
                             u"after %i retries, exiting.\n"
 
511
                             % n_rename)
 
512
            main_loop.quit()
515
513
    elif state == avahi.ENTRY_GROUP_FAILURE:
516
 
        logger.error(u"Error in group state changed %s",
517
 
                     unicode(error))
518
 
        killme(1)
 
514
        sys.stderr.write(u"Error in group state changed %s\n"
 
515
                         % unicode(error))
 
516
        main_loop.quit()
 
517
        return
519
518
 
520
519
 
521
520
def if_nametoindex(interface):
537
536
        return interface_index
538
537
 
539
538
 
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
 
 
570
539
if __name__ == '__main__':
571
 
    exitstatus = 0
572
 
    main_loop_started = False
573
540
    parser = OptionParser()
574
541
    parser.add_option("-i", "--interface", type="string",
575
542
                      default=None, metavar="IF",
576
543
                      help="Bind to interface IF")
 
544
    parser.add_option("--cert", type="string", default="cert.pem",
 
545
                      metavar="FILE",
 
546
                      help="Public key certificate PEM file to use")
 
547
    parser.add_option("--key", type="string", default="key.pem",
 
548
                      metavar="FILE",
 
549
                      help="Private key PEM file to use")
 
550
    parser.add_option("--ca", type="string", default="ca.pem",
 
551
                      metavar="FILE",
 
552
                      help="Certificate Authority certificate PEM file to use")
 
553
    parser.add_option("--crl", type="string", default="crl.pem",
 
554
                      metavar="FILE",
 
555
                      help="Certificate Revokation List PEM file to use")
577
556
    parser.add_option("-p", "--port", type="int", default=None,
578
557
                      help="Port number to receive requests on")
579
558
    parser.add_option("--timeout", type="string", # Parsed later
604
583
        parser.error("option --interval: Unparseable time")
605
584
    
606
585
    # Parse config file
607
 
    defaults = { "checker": "fping -q -- %%(fqdn)s" }
 
586
    defaults = { "checker": "sleep 1; fping -q -- %%(fqdn)s" }
608
587
    client_config = ConfigParser.SafeConfigParser(defaults)
609
588
    #client_config.readfp(open("secrets.conf"), "secrets.conf")
610
589
    client_config.read("mandos-clients.conf")
620
599
    
621
600
    debug = options.debug
622
601
    
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
 
    
631
602
    clients = Set()
632
603
    def remove_from_clients(client):
633
604
        clients.remove(client)
634
605
        if not clients:
635
 
            logger.debug(u"No clients left, exiting")
636
 
            killme()
 
606
            if debug:
 
607
                sys.stderr.write(u"No clients left, exiting\n")
 
608
            main_loop.quit()
637
609
    
638
610
    clients.update(Set(Client(name=section, options=options,
639
611
                              stop_hook = remove_from_clients,
640
612
                              **(dict(client_config\
641
613
                                      .items(section))))
642
614
                       for section in client_config.sections()))
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
 
    
667
615
    for client in clients:
668
616
        client.start()
669
617
    
673
621
                                clients=clients)
674
622
    # Find out what random port we got
675
623
    servicePort = tcp_server.socket.getsockname()[1]
676
 
    logger.debug(u"Now listening on port %d", servicePort)
 
624
    if debug:
 
625
        sys.stderr.write(u"Now listening on port %d\n" % servicePort)
677
626
    
678
627
    if options.interface is not None:
679
628
        serviceInterface = if_nametoindex(options.interface)
680
629
    
681
630
    # From the Avahi server example code
682
631
    server.connect_to_signal("StateChanged", server_state_changed)
683
 
    try:
684
 
        server_state_changed(server.GetState())
685
 
    except dbus.exceptions.DBusException, error:
686
 
        logger.critical(u"DBusException: %s", error)
687
 
        killme(1)
 
632
    server_state_changed(server.GetState())
688
633
    # End of Avahi example code
689
634
    
690
635
    gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
692
637
                         tcp_server.handle_request(*args[2:],
693
638
                                                   **kwargs) or True)
694
639
    try:
695
 
        main_loop_started = True
696
640
        main_loop.run()
697
641
    except KeyboardInterrupt:
698
 
        if debug:
699
 
            print
700
 
    
701
 
    sys.exit(exitstatus)
 
642
        print
 
643
    
 
644
    # Cleanup here
 
645
 
 
646
    # From the Avahi server example code
 
647
    if not group is None:
 
648
        group.Free()
 
649
    # End of Avahi example code
 
650
    
 
651
    for client in clients:
 
652
        client.stop_hook = None
 
653
        client.stop()