/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-20 06:33:48 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080720063348-jscgy5p0itrgvlo8
* mandos-clients.conf ([foo]): Uncommented.
  ([foo]/secret): New.
  ([foo]/secfile): Commented out.
  ([foo]/checker): Changed to "fping -q -- %%(fqdn)s".
  ([foo]/timeout): New.

* server.py: New modeline for Python and Emacs.  Set a logging format.
  (Client.__init__): Bug fix: Choose either the value from the options
                     object or pass the argument through string_to_delta
                     for both "timeout" and "interval".
  (Client.checker_callback): Bug fix: Do not log spurious "Checker for
                             <foo> failed" messages.
  (Client.start_checker): Moved "Starting checker" log message down to
                          just before actually starting the subprocess.
                          Do not redirect the subprocesses' stdout to a
                          pipe.
  (peer_certificate, fingerprint): Added docstrings.
  (entry_group_state_changed): Call "killme()" instead of
                               "main_loop.quit()".
  (daemon, killme): New functions.
  (exitstatus, main_loop_started): New global variables.
  (__main__): Removed the "--cert", "--key", "--ca", and "--crl"
              options.  Removed the sleep command from the default
              checker.  Add a console logger in debug mode.  Call
              "killme()" instead of "main_loop.quit()" when there are no
              more clients.  Call "daemon()" if not in debug mode.
              Register "cleanup()" to run at exit.  Ignore some
              signals.  Catch DBusException to detect another running
              server and exit cleanly.  Exit with "exitstatus".
  (cleanup): New function.

Show diffs side-by-side

added added

removed removed

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