/mandos/trunk

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

« back to all changes in this revision

Viewing changes to server.py

  • Committer: Teddy Hogeborn
  • Date: 2008-07-20 06:33:48 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080720063348-jscgy5p0itrgvlo8
* mandos-clients.conf ([foo]): Uncommented.
  ([foo]/secret): New.
  ([foo]/secfile): Commented out.
  ([foo]/checker): Changed to "fping -q -- %%(fqdn)s".
  ([foo]/timeout): New.

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

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
2
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
3
 
33
4
from __future__ import division
34
5
 
53
24
import subprocess
54
25
import atexit
55
26
import stat
56
 
import logging
57
 
import logging.handlers
58
27
 
59
28
import dbus
60
29
import gobject
62
31
from dbus.mainloop.glib import DBusGMainLoop
63
32
import ctypes
64
33
 
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.
75
 
 
 
34
import logging
 
35
import logging.handlers
76
36
 
77
37
logger = logging.Logger('mandos')
78
38
syslogger = logging.handlers.SysLogHandler\
84
44
 
85
45
# This variable is used to optionally bind to a specified interface.
86
46
# It is a global variable to fit in with the other variables from the
87
 
# Avahi example code.
 
47
# Avahi server example code.
88
48
serviceInterface = avahi.IF_UNSPEC
89
 
# From the Avahi example code:
 
49
# From the Avahi server example code:
90
50
serviceName = "Mandos"
91
51
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
92
52
servicePort = None                      # Not known at startup
155
115
    def __init__(self, name=None, options=None, stop_hook=None,
156
116
                 fingerprint=None, secret=None, secfile=None,
157
117
                 fqdn=None, timeout=None, interval=-1, checker=None):
158
 
        """Note: the 'checker' argument sets the 'checker_command'
159
 
        attribute and not the 'checker' attribute.."""
160
118
        self.name = name
161
119
        # Uppercase and remove spaces from fingerprint
162
120
        # for later comparison purposes with return value of
189
147
        self.checker_callback_tag = None
190
148
        self.check_command = checker
191
149
    def start(self):
192
 
        """Start this client's checker and timeout hooks"""
 
150
        """Start this clients checker and timeout hooks"""
193
151
        # Schedule a new checker to be started an 'interval' from now,
194
152
        # and every interval from then on.
195
153
        self.checker_initiator_tag = gobject.timeout_add\
205
163
        """Stop this client.
206
164
        The possibility that this client might be restarted is left
207
165
        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:
 
166
        logger.debug(u"Stopping client %s", self.name)
 
167
        self.secret = None
 
168
        if self.stop_initiator_tag:
216
169
            gobject.source_remove(self.stop_initiator_tag)
217
170
            self.stop_initiator_tag = None
218
 
        if hasattr(self, "checker_initiator_tag") \
219
 
               and self.checker_initiator_tag:
 
171
        if self.checker_initiator_tag:
220
172
            gobject.source_remove(self.checker_initiator_tag)
221
173
            self.checker_initiator_tag = None
222
174
        self.stop_checker()
225
177
        # Do not run this again if called by a gobject.timeout_add
226
178
        return False
227
179
    def __del__(self):
228
 
        self.stop_hook = None
229
 
        self.stop()
 
180
        # Some code duplication here and in stop()
 
181
        if hasattr(self, "stop_initiator_tag") \
 
182
               and self.stop_initiator_tag:
 
183
            gobject.source_remove(self.stop_initiator_tag)
 
184
            self.stop_initiator_tag = None
 
185
        if hasattr(self, "checker_initiator_tag") \
 
186
               and self.checker_initiator_tag:
 
187
            gobject.source_remove(self.checker_initiator_tag)
 
188
            self.checker_initiator_tag = None
 
189
        self.stop_checker()
230
190
    def checker_callback(self, pid, condition):
231
191
        """The checker has completed, so take appropriate actions."""
232
192
        now = datetime.datetime.now()
233
 
        self.checker_callback_tag = None
234
 
        self.checker = None
235
193
        if os.WIFEXITED(condition) \
236
194
               and (os.WEXITSTATUS(condition) == 0):
237
195
            logger.debug(u"Checker for %(name)s succeeded",
247
205
        else:
248
206
            logger.debug(u"Checker for %(name)s failed",
249
207
                         vars(self))
 
208
            self.checker = None
 
209
        self.checker_callback_tag = None
250
210
    def start_checker(self):
251
211
        """Start a new checker subprocess if one is not running.
252
212
        If a checker already exists, leave it running and do
253
213
        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
214
        if self.checker is None:
263
215
            try:
264
216
                command = self.check_command % self.fqdn
289
241
        return True
290
242
    def stop_checker(self):
291
243
        """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
244
        if not hasattr(self, "checker") or self.checker is None:
296
245
            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
 
246
        gobject.source_remove(self.checker_callback_tag)
 
247
        self.checker_callback_tag = None
 
248
        os.kill(self.checker.pid, signal.SIGTERM)
 
249
        if self.checker.poll() is None:
 
250
            os.kill(self.checker.pid, signal.SIGKILL)
306
251
        self.checker = None
307
252
    def still_valid(self, now=None):
308
253
        """Has the timeout not yet passed for this client?"""
315
260
 
316
261
 
317
262
def peer_certificate(session):
318
 
    "Return the peer's OpenPGP certificate as a bytestring"
 
263
    "Return an OpenPGP data packet string for the peer's certificate"
319
264
    # If not an OpenPGP certificate...
320
265
    if gnutls.library.functions.gnutls_certificate_type_get\
321
266
            (session._c_object) \
332
277
 
333
278
 
334
279
def fingerprint(openpgp):
335
 
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
 
280
    "Convert an OpenPGP data string to a hexdigit fingerprint string"
336
281
    # New empty GnuTLS certificate
337
282
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
338
283
    gnutls.library.functions.gnutls_openpgp_crt_init\
397
342
            return
398
343
        logger.debug(u"Fingerprint: %s", fpr)
399
344
        client = None
400
 
        for c in self.server.clients:
 
345
        for c in clients:
401
346
            if c.fingerprint == fpr:
402
347
                client = c
403
348
                break
504
449
 
505
450
 
506
451
def add_service():
507
 
    """Derived from the Avahi example code"""
 
452
    """From the Avahi server example code"""
508
453
    global group, serviceName, serviceType, servicePort, serviceTXT, \
509
454
           domain, host
510
455
    if group is None:
529
474
 
530
475
 
531
476
def remove_service():
532
 
    """From the Avahi example code"""
 
477
    """From the Avahi server example code"""
533
478
    global group
534
479
    
535
480
    if not group is None:
537
482
 
538
483
 
539
484
def server_state_changed(state):
540
 
    """Derived from the Avahi example code"""
 
485
    """From the Avahi server example code"""
541
486
    if state == avahi.SERVER_COLLISION:
542
487
        logger.warning(u"Server name collision")
543
488
        remove_service()
546
491
 
547
492
 
548
493
def entry_group_state_changed(state, error):
549
 
    """Derived from the Avahi example code"""
 
494
    """From the Avahi server example code"""
550
495
    global serviceName, server, rename_count
551
496
    
552
497
    logger.debug(u"state change: %i", state)
622
567
        sys.exit(status)
623
568
 
624
569
 
625
 
def main():
626
 
    global exitstatus
 
570
if __name__ == '__main__':
627
571
    exitstatus = 0
628
 
    global main_loop_started
629
572
    main_loop_started = False
630
 
    
631
573
    parser = OptionParser()
632
574
    parser.add_option("-i", "--interface", type="string",
633
575
                      default=None, metavar="IF",
634
576
                      help="Bind to interface IF")
635
 
    parser.add_option("-a", "--address", type="string", default=None,
636
 
                      help="Address to listen for requests on")
637
577
    parser.add_option("-p", "--port", type="int", default=None,
638
578
                      help="Port number to receive requests on")
639
579
    parser.add_option("--timeout", type="string", # Parsed later
666
606
    # Parse config file
667
607
    defaults = { "checker": "fping -q -- %%(fqdn)s" }
668
608
    client_config = ConfigParser.SafeConfigParser(defaults)
669
 
    #client_config.readfp(open("global.conf"), "global.conf")
 
609
    #client_config.readfp(open("secrets.conf"), "secrets.conf")
670
610
    client_config.read("mandos-clients.conf")
671
611
    
672
 
    global main_loop
673
 
    global bus
674
 
    global server
675
 
    # From the Avahi example code
 
612
    # From the Avahi server example code
676
613
    DBusGMainLoop(set_as_default=True )
677
614
    main_loop = gobject.MainLoop()
678
615
    bus = dbus.SystemBus()
710
647
    def cleanup():
711
648
        "Cleanup function; run on exit"
712
649
        global group
713
 
        # From the Avahi example code
 
650
        # From the Avahi server example code
714
651
        if not group is None:
715
652
            group.Free()
716
653
            group = None
717
654
        # End of Avahi example code
718
655
        
719
 
        while clients:
720
 
            client = clients.pop()
 
656
        for client in clients:
721
657
            client.stop_hook = None
722
658
            client.stop()
723
659
    
731
667
    for client in clients:
732
668
        client.start()
733
669
    
734
 
    tcp_server = IPv6_TCPServer((options.address, options.port),
 
670
    tcp_server = IPv6_TCPServer((None, options.port),
735
671
                                tcp_handler,
736
672
                                options=options,
737
673
                                clients=clients)
738
674
    # Find out what random port we got
739
 
    global servicePort
740
675
    servicePort = tcp_server.socket.getsockname()[1]
741
676
    logger.debug(u"Now listening on port %d", servicePort)
742
677
    
743
678
    if options.interface is not None:
744
 
        global serviceInterface
745
679
        serviceInterface = if_nametoindex(options.interface)
746
680
    
747
 
    # From the Avahi example code
 
681
    # From the Avahi server example code
748
682
    server.connect_to_signal("StateChanged", server_state_changed)
749
683
    try:
750
684
        server_state_changed(server.GetState())
758
692
                         tcp_server.handle_request(*args[2:],
759
693
                                                   **kwargs) or True)
760
694
    try:
761
 
        logger.debug("Starting main loop")
762
695
        main_loop_started = True
763
696
        main_loop.run()
764
697
    except KeyboardInterrupt:
766
699
            print
767
700
    
768
701
    sys.exit(exitstatus)
769
 
 
770
 
if __name__ == '__main__':
771
 
    main()