/mandos/release

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/release

« back to all changes in this revision

Viewing changes to server.py

  • Committer: Björn Påhlsson
  • Date: 2008-07-20 02:52:20 UTC
  • Revision ID: belorn@braxen-20080720025220-r5u0388uy9iu23h6
Added following support:
Pluginbased client handler
rewritten Mandos client
       Avahi instead of udp server discovery
       openpgp encrypted key support
Passprompt stand alone application for direct console input
Added logging for Mandos server

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
 
32
2
 
33
3
from __future__ import division
34
4
 
51
21
import signal
52
22
from sets import Set
53
23
import subprocess
54
 
import atexit
55
 
import stat
56
 
import logging
57
 
import logging.handlers
58
24
 
59
25
import dbus
60
26
import gobject
62
28
from dbus.mainloop.glib import DBusGMainLoop
63
29
import ctypes
64
30
 
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.
 
31
import logging
 
32
import logging.handlers
75
33
 
 
34
# logghandler.setFormatter(logging.Formatter('%(levelname)s %(message)s')
76
35
 
77
36
logger = logging.Logger('mandos')
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
 
37
logger.addHandler(logging.handlers.SysLogHandler(facility = logging.handlers.SysLogHandler.LOG_DAEMON))
84
38
 
85
39
# This variable is used to optionally bind to a specified interface.
86
40
# It is a global variable to fit in with the other variables from the
87
 
# Avahi example code.
 
41
# Avahi server example code.
88
42
serviceInterface = avahi.IF_UNSPEC
89
 
# From the Avahi example code:
90
 
serviceName = None
 
43
# From the Avahi server example code:
 
44
serviceName = "Mandos"
91
45
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
92
46
servicePort = None                      # Not known at startup
93
47
serviceTXT = []                         # TXT record for the service
152
106
    interval = property(lambda self: self._interval,
153
107
                        _set_interval)
154
108
    del _set_interval
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.."""
 
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):
160
112
        self.name = name
161
 
        logger.debug(u"Creating client %r", self.name)
162
113
        # Uppercase and remove spaces from fingerprint
163
114
        # for later comparison purposes with return value of
164
115
        # the fingerprint() function
165
116
        self.fingerprint = fingerprint.upper().replace(u" ", u"")
166
 
        logger.debug(u"  Fingerprint: %s", self.fingerprint)
167
117
        if secret:
168
118
            self.secret = secret.decode(u"base64")
169
119
        elif secfile:
176
126
        self.fqdn = fqdn                # string
177
127
        self.created = datetime.datetime.now()
178
128
        self.last_seen = None
179
 
        self.timeout = string_to_delta(timeout)
180
 
        self.interval = string_to_delta(interval)
 
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
181
137
        self.stop_hook = stop_hook
182
138
        self.checker = None
183
139
        self.checker_initiator_tag = None
185
141
        self.checker_callback_tag = None
186
142
        self.check_command = checker
187
143
    def start(self):
188
 
        """Start this client's checker and timeout hooks"""
 
144
        """Start this clients checker and timeout hooks"""
189
145
        # Schedule a new checker to be started an 'interval' from now,
190
146
        # and every interval from then on.
191
147
        self.checker_initiator_tag = gobject.timeout_add\
201
157
        """Stop this client.
202
158
        The possibility that this client might be restarted is left
203
159
        open, but not currently used."""
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:
 
160
        logger.debug(u"Stopping client %s", self.name)
 
161
        self.secret = None
 
162
        if self.stop_initiator_tag:
212
163
            gobject.source_remove(self.stop_initiator_tag)
213
164
            self.stop_initiator_tag = None
214
 
        if hasattr(self, "checker_initiator_tag") \
215
 
               and self.checker_initiator_tag:
 
165
        if self.checker_initiator_tag:
216
166
            gobject.source_remove(self.checker_initiator_tag)
217
167
            self.checker_initiator_tag = None
218
168
        self.stop_checker()
221
171
        # Do not run this again if called by a gobject.timeout_add
222
172
        return False
223
173
    def __del__(self):
224
 
        self.stop_hook = None
225
 
        self.stop()
 
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()
226
184
    def checker_callback(self, pid, condition):
227
185
        """The checker has completed, so take appropriate actions."""
228
186
        now = datetime.datetime.now()
229
 
        self.checker_callback_tag = None
230
 
        self.checker = None
231
187
        if os.WIFEXITED(condition) \
232
188
               and (os.WEXITSTATUS(condition) == 0):
233
189
            logger.debug(u"Checker for %(name)s succeeded",
237
193
            self.stop_initiator_tag = gobject.timeout_add\
238
194
                                      (self._timeout_milliseconds,
239
195
                                       self.stop)
240
 
        elif not os.WIFEXITED(condition):
 
196
        if not os.WIFEXITED(condition):
241
197
            logger.warning(u"Checker for %(name)s crashed?",
242
198
                           vars(self))
243
199
        else:
244
200
            logger.debug(u"Checker for %(name)s failed",
245
201
                         vars(self))
 
202
            self.checker = None
 
203
        self.checker_callback_tag = None
246
204
    def start_checker(self):
247
205
        """Start a new checker subprocess if one is not running.
248
206
        If a checker already exists, leave it running and do
249
207
        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.
258
208
        if self.checker is None:
 
209
            logger.debug(u"Starting checker for %s",
 
210
                         self.name)
259
211
            try:
260
212
                command = self.check_command % self.fqdn
261
213
            except TypeError:
265
217
                try:
266
218
                    command = self.check_command % escaped_attrs
267
219
                except TypeError, error:
268
 
                    logger.critical(u'Could not format string "%s":'
269
 
                                    u' %s', self.check_command, error)
 
220
                    logger.critical(u'Could not format string "%s": %s',
 
221
                                    self.check_command, error)
270
222
                    return True # Try again later
271
223
            try:
272
 
                logger.debug(u"Starting checker %r for %s",
273
 
                             command, self.name)
274
224
                self.checker = subprocess.\
275
225
                               Popen(command,
 
226
                                     stdout=subprocess.PIPE,
276
227
                                     close_fds=True, shell=True,
277
228
                                     cwd="/")
278
 
                self.checker_callback_tag = gobject.child_watch_add\
279
 
                                            (self.checker.pid,
280
 
                                             self.checker_callback)
 
229
                self.checker_callback_tag = gobject.\
 
230
                                            child_watch_add(self.checker.pid,
 
231
                                                            self.\
 
232
                                                            checker_callback)
281
233
            except subprocess.OSError, error:
282
234
                logger.error(u"Failed to start subprocess: %s",
283
235
                             error)
285
237
        return True
286
238
    def stop_checker(self):
287
239
        """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
291
240
        if not hasattr(self, "checker") or self.checker is None:
292
241
            return
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
 
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)
302
247
        self.checker = None
303
248
    def still_valid(self, now=None):
304
249
        """Has the timeout not yet passed for this client?"""
311
256
 
312
257
 
313
258
def peer_certificate(session):
314
 
    "Return the peer's OpenPGP certificate as a bytestring"
315
259
    # If not an OpenPGP certificate...
316
260
    if gnutls.library.functions.gnutls_certificate_type_get\
317
261
            (session._c_object) \
328
272
 
329
273
 
330
274
def fingerprint(openpgp):
331
 
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
332
275
    # New empty GnuTLS certificate
333
276
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
334
277
    gnutls.library.functions.gnutls_openpgp_crt_init\
373
316
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
374
317
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
375
318
        #                "+DHE-DSS"))
376
 
        priority = "NORMAL"
377
 
        if self.server.options.priority:
378
 
            priority = self.server.options.priority
 
319
        priority = "SECURE256"
 
320
        
379
321
        gnutls.library.functions.gnutls_priority_set_direct\
380
322
            (session._c_object, priority, None);
381
323
        
394
336
            return
395
337
        logger.debug(u"Fingerprint: %s", fpr)
396
338
        client = None
397
 
        for c in self.server.clients:
 
339
        for c in clients:
398
340
            if c.fingerprint == fpr:
399
341
                client = c
400
342
                break
501
443
 
502
444
 
503
445
def add_service():
504
 
    """Derived from the Avahi example code"""
 
446
    """From the Avahi server example code"""
505
447
    global group, serviceName, serviceType, servicePort, serviceTXT, \
506
448
           domain, host
507
449
    if group is None:
526
468
 
527
469
 
528
470
def remove_service():
529
 
    """From the Avahi example code"""
 
471
    """From the Avahi server example code"""
530
472
    global group
531
473
    
532
474
    if not group is None:
534
476
 
535
477
 
536
478
def server_state_changed(state):
537
 
    """Derived from the Avahi example code"""
 
479
    """From the Avahi server example code"""
538
480
    if state == avahi.SERVER_COLLISION:
539
481
        logger.warning(u"Server name collision")
540
482
        remove_service()
543
485
 
544
486
 
545
487
def entry_group_state_changed(state, error):
546
 
    """Derived from the Avahi example code"""
 
488
    """From the Avahi server example code"""
547
489
    global serviceName, server, rename_count
548
490
    
549
491
    logger.debug(u"state change: %i", state)
561
503
            add_service()
562
504
            
563
505
        else:
564
 
            logger.error(u"No suitable service name found after %i"
565
 
                         u" retries, exiting.", n_rename)
566
 
            killme(1)
 
506
            logger.error(u"No suitable service name found "
 
507
                         u"after %i retries, exiting.",
 
508
                         n_rename)
 
509
            main_loop.quit()
567
510
    elif state == avahi.ENTRY_GROUP_FAILURE:
568
511
        logger.error(u"Error in group state changed %s",
569
512
                     unicode(error))
570
 
        killme(1)
 
513
        main_loop.quit()
 
514
        return
571
515
 
572
516
 
573
517
def if_nametoindex(interface):
589
533
        return interface_index
590
534
 
591
535
 
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
 
    
 
536
if __name__ == '__main__':
628
537
    parser = OptionParser()
629
538
    parser.add_option("-i", "--interface", type="string",
630
539
                      default=None, metavar="IF",
631
540
                      help="Bind to interface IF")
632
 
    parser.add_option("-a", "--address", type="string", default=None,
633
 
                      help="Address to listen for requests on")
 
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")
634
553
    parser.add_option("-p", "--port", type="int", default=None,
635
554
                      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")
636
561
    parser.add_option("--check", action="store_true", default=False,
637
562
                      help="Run self-test")
638
563
    parser.add_option("--debug", action="store_true", default=False,
639
564
                      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")
646
565
    (options, args) = parser.parse_args()
647
566
    
648
567
    if options.check:
650
569
        doctest.testmod()
651
570
        sys.exit()
652
571
    
 
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
    
653
582
    # Parse config file
654
 
    defaults = { "timeout": "1h",
655
 
                 "interval": "5m",
656
 
                 "checker": "fping -q -- %%(fqdn)s",
657
 
                 }
 
583
    defaults = { "checker": "sleep 1; fping -q -- %%(fqdn)s" }
658
584
    client_config = ConfigParser.SafeConfigParser(defaults)
659
 
    #client_config.readfp(open("global.conf"), "global.conf")
 
585
    #client_config.readfp(open("secrets.conf"), "secrets.conf")
660
586
    client_config.read("mandos-clients.conf")
661
587
    
662
 
    global serviceName
663
 
    serviceName = options.servicename;
664
 
    
665
 
    global main_loop
666
 
    global bus
667
 
    global server
668
 
    # From the Avahi example code
 
588
    # From the Avahi server example code
669
589
    DBusGMainLoop(set_as_default=True )
670
590
    main_loop = gobject.MainLoop()
671
591
    bus = dbus.SystemBus()
676
596
    
677
597
    debug = options.debug
678
598
    
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
 
    
687
599
    clients = Set()
688
600
    def remove_from_clients(client):
689
601
        clients.remove(client)
690
602
        if not clients:
691
603
            logger.debug(u"No clients left, exiting")
692
 
            killme()
 
604
            main_loop.quit()
693
605
    
694
 
    clients.update(Set(Client(name=section,
 
606
    clients.update(Set(Client(name=section, options=options,
695
607
                              stop_hook = remove_from_clients,
696
608
                              **(dict(client_config\
697
609
                                      .items(section))))
698
610
                       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
 
    
724
611
    for client in clients:
725
612
        client.start()
726
613
    
727
 
    tcp_server = IPv6_TCPServer((options.address, options.port),
 
614
    tcp_server = IPv6_TCPServer((None, options.port),
728
615
                                tcp_handler,
729
616
                                options=options,
730
617
                                clients=clients)
731
618
    # Find out what random port we got
732
 
    global servicePort
733
619
    servicePort = tcp_server.socket.getsockname()[1]
734
620
    logger.debug(u"Now listening on port %d", servicePort)
735
621
    
736
622
    if options.interface is not None:
737
 
        global serviceInterface
738
623
        serviceInterface = if_nametoindex(options.interface)
739
624
    
740
 
    # From the Avahi example code
 
625
    # From the Avahi server example code
741
626
    server.connect_to_signal("StateChanged", server_state_changed)
742
 
    try:
743
 
        server_state_changed(server.GetState())
744
 
    except dbus.exceptions.DBusException, error:
745
 
        logger.critical(u"DBusException: %s", error)
746
 
        killme(1)
 
627
    server_state_changed(server.GetState())
747
628
    # End of Avahi example code
748
629
    
749
630
    gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
751
632
                         tcp_server.handle_request(*args[2:],
752
633
                                                   **kwargs) or True)
753
634
    try:
754
 
        logger.debug("Starting main loop")
755
 
        main_loop_started = True
756
635
        main_loop.run()
757
636
    except KeyboardInterrupt:
758
 
        if debug:
759
 
            print
 
637
        print
760
638
    
761
 
    sys.exit(exitstatus)
 
639
    # Cleanup here
762
640
 
763
 
if __name__ == '__main__':
764
 
    main()
 
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()