/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: 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:
 
43
# From the Avahi server example code:
90
44
serviceName = "Mandos"
91
45
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
92
46
servicePort = None                      # Not known at startup
153
107
                        _set_interval)
154
108
    del _set_interval
155
109
    def __init__(self, name=None, options=None, stop_hook=None,
156
 
                 fingerprint=None, secret=None, secfile=None,
157
 
                 fqdn=None, timeout=None, interval=-1, checker=None):
158
 
        """Note: the 'checker' argument sets the 'checker_command'
159
 
        attribute and not the 'checker' attribute.."""
 
110
                 fingerprint=None, secret=None, secfile=None, fqdn=None,
 
111
                 timeout=None, interval=-1, checker=None):
160
112
        self.name = name
161
113
        # Uppercase and remove spaces from fingerprint
162
114
        # for later comparison purposes with return value of
175
127
        self.created = datetime.datetime.now()
176
128
        self.last_seen = None
177
129
        if timeout is None:
178
 
            self.timeout = options.timeout
179
 
        else:
180
 
            self.timeout = string_to_delta(timeout)
 
130
            timeout = options.timeout
 
131
        self.timeout = timeout
181
132
        if interval == -1:
182
 
            self.interval = options.interval
 
133
            interval = options.interval
183
134
        else:
184
 
            self.interval = string_to_delta(interval)
 
135
            interval = string_to_delta(interval)
 
136
        self.interval = interval
185
137
        self.stop_hook = stop_hook
186
138
        self.checker = None
187
139
        self.checker_initiator_tag = None
189
141
        self.checker_callback_tag = None
190
142
        self.check_command = checker
191
143
    def start(self):
192
 
        """Start this client's checker and timeout hooks"""
 
144
        """Start this clients checker and timeout hooks"""
193
145
        # Schedule a new checker to be started an 'interval' from now,
194
146
        # and every interval from then on.
195
147
        self.checker_initiator_tag = gobject.timeout_add\
205
157
        """Stop this client.
206
158
        The possibility that this client might be restarted is left
207
159
        open, but not currently used."""
208
 
        # If this client doesn't have a secret, it is already stopped.
209
 
        if self.secret:
210
 
            logger.debug(u"Stopping client %s", self.name)
211
 
            self.secret = None
212
 
        else:
213
 
            return False
214
 
        if hasattr(self, "stop_initiator_tag") \
215
 
               and self.stop_initiator_tag:
 
160
        logger.debug(u"Stopping client %s", self.name)
 
161
        self.secret = None
 
162
        if self.stop_initiator_tag:
216
163
            gobject.source_remove(self.stop_initiator_tag)
217
164
            self.stop_initiator_tag = None
218
 
        if hasattr(self, "checker_initiator_tag") \
219
 
               and self.checker_initiator_tag:
 
165
        if self.checker_initiator_tag:
220
166
            gobject.source_remove(self.checker_initiator_tag)
221
167
            self.checker_initiator_tag = None
222
168
        self.stop_checker()
225
171
        # Do not run this again if called by a gobject.timeout_add
226
172
        return False
227
173
    def __del__(self):
228
 
        self.stop_hook = None
229
 
        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()
230
184
    def checker_callback(self, pid, condition):
231
185
        """The checker has completed, so take appropriate actions."""
232
186
        now = datetime.datetime.now()
233
 
        self.checker_callback_tag = None
234
 
        self.checker = None
235
187
        if os.WIFEXITED(condition) \
236
188
               and (os.WEXITSTATUS(condition) == 0):
237
189
            logger.debug(u"Checker for %(name)s succeeded",
241
193
            self.stop_initiator_tag = gobject.timeout_add\
242
194
                                      (self._timeout_milliseconds,
243
195
                                       self.stop)
244
 
        elif not os.WIFEXITED(condition):
 
196
        if not os.WIFEXITED(condition):
245
197
            logger.warning(u"Checker for %(name)s crashed?",
246
198
                           vars(self))
247
199
        else:
248
200
            logger.debug(u"Checker for %(name)s failed",
249
201
                         vars(self))
 
202
            self.checker = None
 
203
        self.checker_callback_tag = None
250
204
    def start_checker(self):
251
205
        """Start a new checker subprocess if one is not running.
252
206
        If a checker already exists, leave it running and do
253
207
        nothing."""
254
 
        # The reason for not killing a running checker is that if we
255
 
        # did that, then if a checker (for some reason) started
256
 
        # running slowly and taking more than 'interval' time, the
257
 
        # client would inevitably timeout, since no checker would get
258
 
        # a chance to run to completion.  If we instead leave running
259
 
        # checkers alone, the checker would have to take more time
260
 
        # than 'timeout' for the client to be declared invalid, which
261
 
        # is as it should be.
262
208
        if self.checker is None:
 
209
            logger.debug(u"Starting checker for %s",
 
210
                         self.name)
263
211
            try:
264
212
                command = self.check_command % self.fqdn
265
213
            except TypeError:
269
217
                try:
270
218
                    command = self.check_command % escaped_attrs
271
219
                except TypeError, error:
272
 
                    logger.critical(u'Could not format string "%s":'
273
 
                                    u' %s', self.check_command, error)
 
220
                    logger.critical(u'Could not format string "%s": %s',
 
221
                                    self.check_command, error)
274
222
                    return True # Try again later
275
223
            try:
276
 
                logger.debug(u"Starting checker %r for %s",
277
 
                             command, self.name)
278
224
                self.checker = subprocess.\
279
225
                               Popen(command,
 
226
                                     stdout=subprocess.PIPE,
280
227
                                     close_fds=True, shell=True,
281
228
                                     cwd="/")
282
 
                self.checker_callback_tag = gobject.child_watch_add\
283
 
                                            (self.checker.pid,
284
 
                                             self.checker_callback)
 
229
                self.checker_callback_tag = gobject.\
 
230
                                            child_watch_add(self.checker.pid,
 
231
                                                            self.\
 
232
                                                            checker_callback)
285
233
            except subprocess.OSError, error:
286
234
                logger.error(u"Failed to start subprocess: %s",
287
235
                             error)
289
237
        return True
290
238
    def stop_checker(self):
291
239
        """Force the checker process, if any, to stop."""
292
 
        if self.checker_callback_tag:
293
 
            gobject.source_remove(self.checker_callback_tag)
294
 
            self.checker_callback_tag = None
295
240
        if not hasattr(self, "checker") or self.checker is None:
296
241
            return
297
 
        logger.debug("Stopping checker for %(name)s", vars(self))
298
 
        try:
299
 
            os.kill(self.checker.pid, signal.SIGTERM)
300
 
            #os.sleep(0.5)
301
 
            #if self.checker.poll() is None:
302
 
            #    os.kill(self.checker.pid, signal.SIGKILL)
303
 
        except OSError, error:
304
 
            if error.errno != errno.ESRCH:
305
 
                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)
306
247
        self.checker = None
307
248
    def still_valid(self, now=None):
308
249
        """Has the timeout not yet passed for this client?"""
315
256
 
316
257
 
317
258
def peer_certificate(session):
318
 
    "Return the peer's OpenPGP certificate as a bytestring"
319
259
    # If not an OpenPGP certificate...
320
260
    if gnutls.library.functions.gnutls_certificate_type_get\
321
261
            (session._c_object) \
332
272
 
333
273
 
334
274
def fingerprint(openpgp):
335
 
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
336
275
    # New empty GnuTLS certificate
337
276
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
338
277
    gnutls.library.functions.gnutls_openpgp_crt_init\
397
336
            return
398
337
        logger.debug(u"Fingerprint: %s", fpr)
399
338
        client = None
400
 
        for c in self.server.clients:
 
339
        for c in clients:
401
340
            if c.fingerprint == fpr:
402
341
                client = c
403
342
                break
504
443
 
505
444
 
506
445
def add_service():
507
 
    """Derived from the Avahi example code"""
 
446
    """From the Avahi server example code"""
508
447
    global group, serviceName, serviceType, servicePort, serviceTXT, \
509
448
           domain, host
510
449
    if group is None:
529
468
 
530
469
 
531
470
def remove_service():
532
 
    """From the Avahi example code"""
 
471
    """From the Avahi server example code"""
533
472
    global group
534
473
    
535
474
    if not group is None:
537
476
 
538
477
 
539
478
def server_state_changed(state):
540
 
    """Derived from the Avahi example code"""
 
479
    """From the Avahi server example code"""
541
480
    if state == avahi.SERVER_COLLISION:
542
481
        logger.warning(u"Server name collision")
543
482
        remove_service()
546
485
 
547
486
 
548
487
def entry_group_state_changed(state, error):
549
 
    """Derived from the Avahi example code"""
 
488
    """From the Avahi server example code"""
550
489
    global serviceName, server, rename_count
551
490
    
552
491
    logger.debug(u"state change: %i", state)
564
503
            add_service()
565
504
            
566
505
        else:
567
 
            logger.error(u"No suitable service name found after %i"
568
 
                         u" retries, exiting.", n_rename)
569
 
            killme(1)
 
506
            logger.error(u"No suitable service name found "
 
507
                         u"after %i retries, exiting.",
 
508
                         n_rename)
 
509
            main_loop.quit()
570
510
    elif state == avahi.ENTRY_GROUP_FAILURE:
571
511
        logger.error(u"Error in group state changed %s",
572
512
                     unicode(error))
573
 
        killme(1)
 
513
        main_loop.quit()
 
514
        return
574
515
 
575
516
 
576
517
def if_nametoindex(interface):
592
533
        return interface_index
593
534
 
594
535
 
595
 
def daemon(nochdir, noclose):
596
 
    """See daemon(3).  Standard BSD Unix function.
597
 
    This should really exist as os.daemon, but it doesn't (yet)."""
598
 
    if os.fork():
599
 
        sys.exit()
600
 
    os.setsid()
601
 
    if not nochdir:
602
 
        os.chdir("/")
603
 
    if not noclose:
604
 
        # Close all standard open file descriptors
605
 
        null = os.open("/dev/null", os.O_NOCTTY | os.O_RDWR)
606
 
        if not stat.S_ISCHR(os.fstat(null).st_mode):
607
 
            raise OSError(errno.ENODEV,
608
 
                          "/dev/null not a character device")
609
 
        os.dup2(null, sys.stdin.fileno())
610
 
        os.dup2(null, sys.stdout.fileno())
611
 
        os.dup2(null, sys.stderr.fileno())
612
 
        if null > 2:
613
 
            os.close(null)
614
 
 
615
 
 
616
 
def killme(status = 0):
617
 
    logger.debug("Stopping server with exit status %d", status)
618
 
    exitstatus = status
619
 
    if main_loop_started:
620
 
        main_loop.quit()
621
 
    else:
622
 
        sys.exit(status)
623
 
 
624
 
 
625
 
def main():
626
 
    global exitstatus
627
 
    exitstatus = 0
628
 
    global main_loop_started
629
 
    main_loop_started = False
630
 
    
 
536
if __name__ == '__main__':
631
537
    parser = OptionParser()
632
538
    parser.add_option("-i", "--interface", type="string",
633
539
                      default=None, metavar="IF",
634
540
                      help="Bind to interface IF")
635
 
    parser.add_option("-a", "--address", type="string", default=None,
636
 
                      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")
637
553
    parser.add_option("-p", "--port", type="int", default=None,
638
554
                      help="Port number to receive requests on")
639
555
    parser.add_option("--timeout", type="string", # Parsed later
664
580
        parser.error("option --interval: Unparseable time")
665
581
    
666
582
    # Parse config file
667
 
    defaults = { "checker": "fping -q -- %%(fqdn)s" }
 
583
    defaults = { "checker": "sleep 1; fping -q -- %%(fqdn)s" }
668
584
    client_config = ConfigParser.SafeConfigParser(defaults)
669
 
    #client_config.readfp(open("global.conf"), "global.conf")
 
585
    #client_config.readfp(open("secrets.conf"), "secrets.conf")
670
586
    client_config.read("mandos-clients.conf")
671
587
    
672
 
    global main_loop
673
 
    global bus
674
 
    global server
675
 
    # From the Avahi example code
 
588
    # From the Avahi server example code
676
589
    DBusGMainLoop(set_as_default=True )
677
590
    main_loop = gobject.MainLoop()
678
591
    bus = dbus.SystemBus()
683
596
    
684
597
    debug = options.debug
685
598
    
686
 
    if debug:
687
 
        console = logging.StreamHandler()
688
 
        # console.setLevel(logging.DEBUG)
689
 
        console.setFormatter(logging.Formatter\
690
 
                             ('%(levelname)s: %(message)s'))
691
 
        logger.addHandler(console)
692
 
        del console
693
 
    
694
599
    clients = Set()
695
600
    def remove_from_clients(client):
696
601
        clients.remove(client)
697
602
        if not clients:
698
603
            logger.debug(u"No clients left, exiting")
699
 
            killme()
 
604
            main_loop.quit()
700
605
    
701
606
    clients.update(Set(Client(name=section, options=options,
702
607
                              stop_hook = remove_from_clients,
703
608
                              **(dict(client_config\
704
609
                                      .items(section))))
705
610
                       for section in client_config.sections()))
706
 
    
707
 
    if not debug:
708
 
        daemon(False, False)
709
 
    
710
 
    def cleanup():
711
 
        "Cleanup function; run on exit"
712
 
        global group
713
 
        # From the Avahi example code
714
 
        if not group is None:
715
 
            group.Free()
716
 
            group = None
717
 
        # End of Avahi example code
718
 
        
719
 
        while clients:
720
 
            client = clients.pop()
721
 
            client.stop_hook = None
722
 
            client.stop()
723
 
    
724
 
    atexit.register(cleanup)
725
 
    
726
 
    if not debug:
727
 
        signal.signal(signal.SIGINT, signal.SIG_IGN)
728
 
    signal.signal(signal.SIGHUP, lambda signum, frame: killme())
729
 
    signal.signal(signal.SIGTERM, lambda signum, frame: killme())
730
 
    
731
611
    for client in clients:
732
612
        client.start()
733
613
    
734
 
    tcp_server = IPv6_TCPServer((options.address, options.port),
 
614
    tcp_server = IPv6_TCPServer((None, options.port),
735
615
                                tcp_handler,
736
616
                                options=options,
737
617
                                clients=clients)
738
618
    # Find out what random port we got
739
 
    global servicePort
740
619
    servicePort = tcp_server.socket.getsockname()[1]
741
620
    logger.debug(u"Now listening on port %d", servicePort)
742
621
    
743
622
    if options.interface is not None:
744
 
        global serviceInterface
745
623
        serviceInterface = if_nametoindex(options.interface)
746
624
    
747
 
    # From the Avahi example code
 
625
    # From the Avahi server example code
748
626
    server.connect_to_signal("StateChanged", server_state_changed)
749
 
    try:
750
 
        server_state_changed(server.GetState())
751
 
    except dbus.exceptions.DBusException, error:
752
 
        logger.critical(u"DBusException: %s", error)
753
 
        killme(1)
 
627
    server_state_changed(server.GetState())
754
628
    # End of Avahi example code
755
629
    
756
630
    gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
758
632
                         tcp_server.handle_request(*args[2:],
759
633
                                                   **kwargs) or True)
760
634
    try:
761
 
        logger.debug("Starting main loop")
762
 
        main_loop_started = True
763
635
        main_loop.run()
764
636
    except KeyboardInterrupt:
765
 
        if debug:
766
 
            print
 
637
        print
767
638
    
768
 
    sys.exit(exitstatus)
 
639
    # Cleanup here
769
640
 
770
 
if __name__ == '__main__':
771
 
    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()