/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-22 06:23:59 UTC
  • mfrom: (24.1.1 mandos)
  • Revision ID: teddy@fukt.bsnet.se-20080722062359-qti3ecst69bq3ltk
Merge.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
 
2
# -*- mode: python; coding: utf-8 -*-
 
3
 
4
# Mandos server - give out binary blobs to connecting clients.
 
5
 
6
# This program is partly derived from an example program for an Avahi
 
7
# service publisher, downloaded from
 
8
# <http://avahi.org/wiki/PythonPublishExample>.  This includes the
 
9
# following functions: "add_service", "remove_service",
 
10
# "server_state_changed", "entry_group_state_changed", and some lines
 
11
# in "main".
 
12
 
13
# Everything else is Copyright © 2007-2008 Teddy Hogeborn and Björn
 
14
# Påhlsson.
 
15
 
16
# This program is free software: you can redistribute it and/or modify
 
17
# it under the terms of the GNU General Public License as published by
 
18
# the Free Software Foundation, either version 3 of the License, or
 
19
# (at your option) any later version.
 
20
#
 
21
#     This program is distributed in the hope that it will be useful,
 
22
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
 
23
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
24
#     GNU General Public License for more details.
 
25
 
26
# You should have received a copy of the GNU General Public License
 
27
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
28
 
29
# Contact the authors at <https://www.fukt.bsnet.se/~belorn/> and
 
30
# <https://www.fukt.bsnet.se/~teddy/>.
 
31
2
32
 
3
33
from __future__ import division
4
34
 
21
51
import signal
22
52
from sets import Set
23
53
import subprocess
 
54
import atexit
 
55
import stat
 
56
import logging
 
57
import logging.handlers
24
58
 
25
59
import dbus
26
60
import gobject
28
62
from dbus.mainloop.glib import DBusGMainLoop
29
63
import ctypes
30
64
 
31
 
import logging
32
 
import logging.handlers
 
65
# Brief description of the operation of this program:
 
66
 
67
# This server announces itself as a Zeroconf service.  Connecting
 
68
# clients use the TLS protocol, with the unusual quirk that this
 
69
# server program acts as a TLS "client" while the connecting clients
 
70
# acts as a TLS "server".  The clients (acting as a TLS "server") must
 
71
# supply an OpenPGP certificate, and the fingerprint of this
 
72
# certificate is used by this server to look up (in a list read from a
 
73
# file at start time) which binary blob to give the client.  No other
 
74
# authentication or authorization is done by this server.
33
75
 
34
 
# logghandler.setFormatter(logging.Formatter('%(levelname)s %(message)s')
35
76
 
36
77
logger = logging.Logger('mandos')
37
 
logger.addHandler(logging.handlers.SysLogHandler(facility = logging.handlers.SysLogHandler.LOG_DAEMON))
 
78
syslogger = logging.handlers.SysLogHandler\
 
79
            (facility = logging.handlers.SysLogHandler.LOG_DAEMON)
 
80
syslogger.setFormatter(logging.Formatter\
 
81
                        ('%(levelname)s: %(message)s'))
 
82
logger.addHandler(syslogger)
 
83
del syslogger
38
84
 
39
85
# This variable is used to optionally bind to a specified interface.
40
86
# It is a global variable to fit in with the other variables from the
41
 
# Avahi server example code.
 
87
# Avahi example code.
42
88
serviceInterface = avahi.IF_UNSPEC
43
 
# From the Avahi server example code:
44
 
serviceName = "Mandos"
 
89
# From the Avahi example code:
 
90
serviceName = None
45
91
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
46
92
servicePort = None                      # Not known at startup
47
93
serviceTXT = []                         # TXT record for the service
106
152
    interval = property(lambda self: self._interval,
107
153
                        _set_interval)
108
154
    del _set_interval
109
 
    def __init__(self, name=None, options=None, stop_hook=None,
110
 
                 fingerprint=None, secret=None, secfile=None, fqdn=None,
111
 
                 timeout=None, interval=-1, checker=None):
 
155
    def __init__(self, name=None, stop_hook=None, fingerprint=None,
 
156
                 secret=None, secfile=None, fqdn=None, timeout=None,
 
157
                 interval=-1, checker=None):
 
158
        """Note: the 'checker' argument sets the 'checker_command'
 
159
        attribute and not the 'checker' attribute.."""
112
160
        self.name = name
 
161
        logger.debug(u"Creating client %r", self.name)
113
162
        # Uppercase and remove spaces from fingerprint
114
163
        # for later comparison purposes with return value of
115
164
        # the fingerprint() function
116
165
        self.fingerprint = fingerprint.upper().replace(u" ", u"")
 
166
        logger.debug(u"  Fingerprint: %s", self.fingerprint)
117
167
        if secret:
118
168
            self.secret = secret.decode(u"base64")
119
169
        elif secfile:
126
176
        self.fqdn = fqdn                # string
127
177
        self.created = datetime.datetime.now()
128
178
        self.last_seen = None
129
 
        if timeout is None:
130
 
            timeout = options.timeout
131
 
        self.timeout = timeout
132
 
        if interval == -1:
133
 
            interval = options.interval
134
 
        else:
135
 
            interval = string_to_delta(interval)
136
 
        self.interval = interval
 
179
        self.timeout = string_to_delta(timeout)
 
180
        self.interval = string_to_delta(interval)
137
181
        self.stop_hook = stop_hook
138
182
        self.checker = None
139
183
        self.checker_initiator_tag = None
141
185
        self.checker_callback_tag = None
142
186
        self.check_command = checker
143
187
    def start(self):
144
 
        """Start this clients checker and timeout hooks"""
 
188
        """Start this client's checker and timeout hooks"""
145
189
        # Schedule a new checker to be started an 'interval' from now,
146
190
        # and every interval from then on.
147
191
        self.checker_initiator_tag = gobject.timeout_add\
157
201
        """Stop this client.
158
202
        The possibility that this client might be restarted is left
159
203
        open, but not currently used."""
160
 
        logger.debug(u"Stopping client %s", self.name)
161
 
        self.secret = None
162
 
        if self.stop_initiator_tag:
 
204
        # If this client doesn't have a secret, it is already stopped.
 
205
        if self.secret:
 
206
            logger.debug(u"Stopping client %s", self.name)
 
207
            self.secret = None
 
208
        else:
 
209
            return False
 
210
        if hasattr(self, "stop_initiator_tag") \
 
211
               and self.stop_initiator_tag:
163
212
            gobject.source_remove(self.stop_initiator_tag)
164
213
            self.stop_initiator_tag = None
165
 
        if self.checker_initiator_tag:
 
214
        if hasattr(self, "checker_initiator_tag") \
 
215
               and self.checker_initiator_tag:
166
216
            gobject.source_remove(self.checker_initiator_tag)
167
217
            self.checker_initiator_tag = None
168
218
        self.stop_checker()
171
221
        # Do not run this again if called by a gobject.timeout_add
172
222
        return False
173
223
    def __del__(self):
174
 
        # Some code duplication here and in stop()
175
 
        if hasattr(self, "stop_initiator_tag") \
176
 
               and self.stop_initiator_tag:
177
 
            gobject.source_remove(self.stop_initiator_tag)
178
 
            self.stop_initiator_tag = None
179
 
        if hasattr(self, "checker_initiator_tag") \
180
 
               and self.checker_initiator_tag:
181
 
            gobject.source_remove(self.checker_initiator_tag)
182
 
            self.checker_initiator_tag = None
183
 
        self.stop_checker()
 
224
        self.stop_hook = None
 
225
        self.stop()
184
226
    def checker_callback(self, pid, condition):
185
227
        """The checker has completed, so take appropriate actions."""
186
228
        now = datetime.datetime.now()
 
229
        self.checker_callback_tag = None
 
230
        self.checker = None
187
231
        if os.WIFEXITED(condition) \
188
232
               and (os.WEXITSTATUS(condition) == 0):
189
233
            logger.debug(u"Checker for %(name)s succeeded",
193
237
            self.stop_initiator_tag = gobject.timeout_add\
194
238
                                      (self._timeout_milliseconds,
195
239
                                       self.stop)
196
 
        if not os.WIFEXITED(condition):
 
240
        elif not os.WIFEXITED(condition):
197
241
            logger.warning(u"Checker for %(name)s crashed?",
198
242
                           vars(self))
199
243
        else:
200
244
            logger.debug(u"Checker for %(name)s failed",
201
245
                         vars(self))
202
 
            self.checker = None
203
 
        self.checker_callback_tag = None
204
246
    def start_checker(self):
205
247
        """Start a new checker subprocess if one is not running.
206
248
        If a checker already exists, leave it running and do
207
249
        nothing."""
 
250
        # The reason for not killing a running checker is that if we
 
251
        # did that, then if a checker (for some reason) started
 
252
        # running slowly and taking more than 'interval' time, the
 
253
        # client would inevitably timeout, since no checker would get
 
254
        # a chance to run to completion.  If we instead leave running
 
255
        # checkers alone, the checker would have to take more time
 
256
        # than 'timeout' for the client to be declared invalid, which
 
257
        # is as it should be.
208
258
        if self.checker is None:
209
 
            logger.debug(u"Starting checker for %s",
210
 
                         self.name)
211
259
            try:
212
260
                command = self.check_command % self.fqdn
213
261
            except TypeError:
217
265
                try:
218
266
                    command = self.check_command % escaped_attrs
219
267
                except TypeError, error:
220
 
                    logger.critical(u'Could not format string "%s": %s',
221
 
                                    self.check_command, error)
 
268
                    logger.critical(u'Could not format string "%s":'
 
269
                                    u' %s', self.check_command, error)
222
270
                    return True # Try again later
223
271
            try:
 
272
                logger.debug(u"Starting checker %r for %s",
 
273
                             command, self.name)
224
274
                self.checker = subprocess.\
225
275
                               Popen(command,
226
 
                                     stdout=subprocess.PIPE,
227
276
                                     close_fds=True, shell=True,
228
277
                                     cwd="/")
229
 
                self.checker_callback_tag = gobject.\
230
 
                                            child_watch_add(self.checker.pid,
231
 
                                                            self.\
232
 
                                                            checker_callback)
 
278
                self.checker_callback_tag = gobject.child_watch_add\
 
279
                                            (self.checker.pid,
 
280
                                             self.checker_callback)
233
281
            except subprocess.OSError, error:
234
282
                logger.error(u"Failed to start subprocess: %s",
235
283
                             error)
237
285
        return True
238
286
    def stop_checker(self):
239
287
        """Force the checker process, if any, to stop."""
 
288
        if self.checker_callback_tag:
 
289
            gobject.source_remove(self.checker_callback_tag)
 
290
            self.checker_callback_tag = None
240
291
        if not hasattr(self, "checker") or self.checker is None:
241
292
            return
242
 
        gobject.source_remove(self.checker_callback_tag)
243
 
        self.checker_callback_tag = None
244
 
        os.kill(self.checker.pid, signal.SIGTERM)
245
 
        if self.checker.poll() is None:
246
 
            os.kill(self.checker.pid, signal.SIGKILL)
 
293
        logger.debug("Stopping checker for %(name)s", vars(self))
 
294
        try:
 
295
            os.kill(self.checker.pid, signal.SIGTERM)
 
296
            #os.sleep(0.5)
 
297
            #if self.checker.poll() is None:
 
298
            #    os.kill(self.checker.pid, signal.SIGKILL)
 
299
        except OSError, error:
 
300
            if error.errno != errno.ESRCH:
 
301
                raise
247
302
        self.checker = None
248
303
    def still_valid(self, now=None):
249
304
        """Has the timeout not yet passed for this client?"""
256
311
 
257
312
 
258
313
def peer_certificate(session):
 
314
    "Return the peer's OpenPGP certificate as a bytestring"
259
315
    # If not an OpenPGP certificate...
260
316
    if gnutls.library.functions.gnutls_certificate_type_get\
261
317
            (session._c_object) \
272
328
 
273
329
 
274
330
def fingerprint(openpgp):
 
331
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
275
332
    # New empty GnuTLS certificate
276
333
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
277
334
    gnutls.library.functions.gnutls_openpgp_crt_init\
316
373
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
317
374
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
318
375
        #                "+DHE-DSS"))
319
 
        priority = "SECURE256"
320
 
        
 
376
        priority = "NORMAL"
 
377
        if self.server.options.priority:
 
378
            priority = self.server.options.priority
321
379
        gnutls.library.functions.gnutls_priority_set_direct\
322
380
            (session._c_object, priority, None);
323
381
        
336
394
            return
337
395
        logger.debug(u"Fingerprint: %s", fpr)
338
396
        client = None
339
 
        for c in clients:
 
397
        for c in self.server.clients:
340
398
            if c.fingerprint == fpr:
341
399
                client = c
342
400
                break
443
501
 
444
502
 
445
503
def add_service():
446
 
    """From the Avahi server example code"""
 
504
    """Derived from the Avahi example code"""
447
505
    global group, serviceName, serviceType, servicePort, serviceTXT, \
448
506
           domain, host
449
507
    if group is None:
468
526
 
469
527
 
470
528
def remove_service():
471
 
    """From the Avahi server example code"""
 
529
    """From the Avahi example code"""
472
530
    global group
473
531
    
474
532
    if not group is None:
476
534
 
477
535
 
478
536
def server_state_changed(state):
479
 
    """From the Avahi server example code"""
 
537
    """Derived from the Avahi example code"""
480
538
    if state == avahi.SERVER_COLLISION:
481
539
        logger.warning(u"Server name collision")
482
540
        remove_service()
485
543
 
486
544
 
487
545
def entry_group_state_changed(state, error):
488
 
    """From the Avahi server example code"""
 
546
    """Derived from the Avahi example code"""
489
547
    global serviceName, server, rename_count
490
548
    
491
549
    logger.debug(u"state change: %i", state)
503
561
            add_service()
504
562
            
505
563
        else:
506
 
            logger.error(u"No suitable service name found "
507
 
                         u"after %i retries, exiting.",
508
 
                         n_rename)
509
 
            main_loop.quit()
 
564
            logger.error(u"No suitable service name found after %i"
 
565
                         u" retries, exiting.", n_rename)
 
566
            killme(1)
510
567
    elif state == avahi.ENTRY_GROUP_FAILURE:
511
568
        logger.error(u"Error in group state changed %s",
512
569
                     unicode(error))
513
 
        main_loop.quit()
514
 
        return
 
570
        killme(1)
515
571
 
516
572
 
517
573
def if_nametoindex(interface):
533
589
        return interface_index
534
590
 
535
591
 
536
 
if __name__ == '__main__':
 
592
def daemon(nochdir, noclose):
 
593
    """See daemon(3).  Standard BSD Unix function.
 
594
    This should really exist as os.daemon, but it doesn't (yet)."""
 
595
    if os.fork():
 
596
        sys.exit()
 
597
    os.setsid()
 
598
    if not nochdir:
 
599
        os.chdir("/")
 
600
    if not noclose:
 
601
        # Close all standard open file descriptors
 
602
        null = os.open("/dev/null", os.O_NOCTTY | os.O_RDWR)
 
603
        if not stat.S_ISCHR(os.fstat(null).st_mode):
 
604
            raise OSError(errno.ENODEV,
 
605
                          "/dev/null not a character device")
 
606
        os.dup2(null, sys.stdin.fileno())
 
607
        os.dup2(null, sys.stdout.fileno())
 
608
        os.dup2(null, sys.stderr.fileno())
 
609
        if null > 2:
 
610
            os.close(null)
 
611
 
 
612
 
 
613
def killme(status = 0):
 
614
    logger.debug("Stopping server with exit status %d", status)
 
615
    exitstatus = status
 
616
    if main_loop_started:
 
617
        main_loop.quit()
 
618
    else:
 
619
        sys.exit(status)
 
620
 
 
621
 
 
622
def main():
 
623
    global exitstatus
 
624
    exitstatus = 0
 
625
    global main_loop_started
 
626
    main_loop_started = False
 
627
    
537
628
    parser = OptionParser()
538
629
    parser.add_option("-i", "--interface", type="string",
539
630
                      default=None, metavar="IF",
540
631
                      help="Bind to interface IF")
541
 
    parser.add_option("--cert", type="string", default="cert.pem",
542
 
                      metavar="FILE",
543
 
                      help="Public key certificate PEM file to use")
544
 
    parser.add_option("--key", type="string", default="key.pem",
545
 
                      metavar="FILE",
546
 
                      help="Private key PEM file to use")
547
 
    parser.add_option("--ca", type="string", default="ca.pem",
548
 
                      metavar="FILE",
549
 
                      help="Certificate Authority certificate PEM file to use")
550
 
    parser.add_option("--crl", type="string", default="crl.pem",
551
 
                      metavar="FILE",
552
 
                      help="Certificate Revokation List PEM file to use")
 
632
    parser.add_option("-a", "--address", type="string", default=None,
 
633
                      help="Address to listen for requests on")
553
634
    parser.add_option("-p", "--port", type="int", default=None,
554
635
                      help="Port number to receive requests on")
555
 
    parser.add_option("--timeout", type="string", # Parsed later
556
 
                      default="1h",
557
 
                      help="Amount of downtime allowed for clients")
558
 
    parser.add_option("--interval", type="string", # Parsed later
559
 
                      default="5m",
560
 
                      help="How often to check that a client is up")
561
636
    parser.add_option("--check", action="store_true", default=False,
562
637
                      help="Run self-test")
563
638
    parser.add_option("--debug", action="store_true", default=False,
564
639
                      help="Debug mode")
 
640
    parser.add_option("--priority", type="string",
 
641
                      default="SECURE256",
 
642
                      help="GnuTLS priority string"
 
643
                      " (see GnuTLS documentation)")
 
644
    parser.add_option("--servicename", type="string",
 
645
                      default="Mandos", help="Zeroconf service name")
565
646
    (options, args) = parser.parse_args()
566
647
    
567
648
    if options.check:
569
650
        doctest.testmod()
570
651
        sys.exit()
571
652
    
572
 
    # Parse the time arguments
573
 
    try:
574
 
        options.timeout = string_to_delta(options.timeout)
575
 
    except ValueError:
576
 
        parser.error("option --timeout: Unparseable time")
577
 
    try:
578
 
        options.interval = string_to_delta(options.interval)
579
 
    except ValueError:
580
 
        parser.error("option --interval: Unparseable time")
581
 
    
582
653
    # Parse config file
583
 
    defaults = { "checker": "sleep 1; fping -q -- %%(fqdn)s" }
 
654
    defaults = { "timeout": "1h",
 
655
                 "interval": "5m",
 
656
                 "checker": "fping -q -- %%(fqdn)s",
 
657
                 }
584
658
    client_config = ConfigParser.SafeConfigParser(defaults)
585
 
    #client_config.readfp(open("secrets.conf"), "secrets.conf")
 
659
    #client_config.readfp(open("global.conf"), "global.conf")
586
660
    client_config.read("mandos-clients.conf")
587
661
    
588
 
    # From the Avahi server example code
 
662
    global serviceName
 
663
    serviceName = options.servicename;
 
664
    
 
665
    global main_loop
 
666
    global bus
 
667
    global server
 
668
    # From the Avahi example code
589
669
    DBusGMainLoop(set_as_default=True )
590
670
    main_loop = gobject.MainLoop()
591
671
    bus = dbus.SystemBus()
596
676
    
597
677
    debug = options.debug
598
678
    
 
679
    if debug:
 
680
        console = logging.StreamHandler()
 
681
        # console.setLevel(logging.DEBUG)
 
682
        console.setFormatter(logging.Formatter\
 
683
                             ('%(levelname)s: %(message)s'))
 
684
        logger.addHandler(console)
 
685
        del console
 
686
    
599
687
    clients = Set()
600
688
    def remove_from_clients(client):
601
689
        clients.remove(client)
602
690
        if not clients:
603
691
            logger.debug(u"No clients left, exiting")
604
 
            main_loop.quit()
 
692
            killme()
605
693
    
606
 
    clients.update(Set(Client(name=section, options=options,
 
694
    clients.update(Set(Client(name=section,
607
695
                              stop_hook = remove_from_clients,
608
696
                              **(dict(client_config\
609
697
                                      .items(section))))
610
698
                       for section in client_config.sections()))
 
699
    
 
700
    if not debug:
 
701
        daemon(False, False)
 
702
    
 
703
    def cleanup():
 
704
        "Cleanup function; run on exit"
 
705
        global group
 
706
        # From the Avahi example code
 
707
        if not group is None:
 
708
            group.Free()
 
709
            group = None
 
710
        # End of Avahi example code
 
711
        
 
712
        while clients:
 
713
            client = clients.pop()
 
714
            client.stop_hook = None
 
715
            client.stop()
 
716
    
 
717
    atexit.register(cleanup)
 
718
    
 
719
    if not debug:
 
720
        signal.signal(signal.SIGINT, signal.SIG_IGN)
 
721
    signal.signal(signal.SIGHUP, lambda signum, frame: killme())
 
722
    signal.signal(signal.SIGTERM, lambda signum, frame: killme())
 
723
    
611
724
    for client in clients:
612
725
        client.start()
613
726
    
614
 
    tcp_server = IPv6_TCPServer((None, options.port),
 
727
    tcp_server = IPv6_TCPServer((options.address, options.port),
615
728
                                tcp_handler,
616
729
                                options=options,
617
730
                                clients=clients)
618
731
    # Find out what random port we got
 
732
    global servicePort
619
733
    servicePort = tcp_server.socket.getsockname()[1]
620
734
    logger.debug(u"Now listening on port %d", servicePort)
621
735
    
622
736
    if options.interface is not None:
 
737
        global serviceInterface
623
738
        serviceInterface = if_nametoindex(options.interface)
624
739
    
625
 
    # From the Avahi server example code
 
740
    # From the Avahi example code
626
741
    server.connect_to_signal("StateChanged", server_state_changed)
627
 
    server_state_changed(server.GetState())
 
742
    try:
 
743
        server_state_changed(server.GetState())
 
744
    except dbus.exceptions.DBusException, error:
 
745
        logger.critical(u"DBusException: %s", error)
 
746
        killme(1)
628
747
    # End of Avahi example code
629
748
    
630
749
    gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
632
751
                         tcp_server.handle_request(*args[2:],
633
752
                                                   **kwargs) or True)
634
753
    try:
 
754
        logger.debug("Starting main loop")
 
755
        main_loop_started = True
635
756
        main_loop.run()
636
757
    except KeyboardInterrupt:
637
 
        print
 
758
        if debug:
 
759
            print
638
760
    
639
 
    # Cleanup here
 
761
    sys.exit(exitstatus)
640
762
 
641
 
    # From the Avahi server example code
642
 
    if not group is None:
643
 
        group.Free()
644
 
    # End of Avahi example code
645
 
    
646
 
    for client in clients:
647
 
        client.stop_hook = None
648
 
        client.stop()
 
763
if __name__ == '__main__':
 
764
    main()