/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-21 22:51:46 UTC
  • mfrom: (15.1.4 mandos)
  • Revision ID: teddy@fukt.bsnet.se-20080721225146-55gbo7fqocy4m930
* plugins.d/mandosclient.c (pgp_packet_decrypt): Cast "0" argument to
                                                 gpgme_data_seek.
 (start_mandos_communication): Change "ip" arg to "const char *".  New
                               variable "written".  Remove setsockopt.
                               Bug fix: Do not change decrypted_buffer.
 (resolve_callback): Removed AVAHI_GCC_UNUSED from "interface".

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