/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:
90
 
serviceName = None
 
49
# From the Avahi server example code:
 
50
serviceName = "Mandos"
91
51
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
92
52
servicePort = None                      # Not known at startup
93
53
serviceTXT = []                         # TXT record for the service
152
112
    interval = property(lambda self: self._interval,
153
113
                        _set_interval)
154
114
    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.."""
 
115
    def __init__(self, name=None, options=None, stop_hook=None,
 
116
                 fingerprint=None, secret=None, secfile=None,
 
117
                 fqdn=None, timeout=None, interval=-1, checker=None):
160
118
        self.name = name
161
 
        logger.debug(u"Creating client %r", self.name)
162
119
        # Uppercase and remove spaces from fingerprint
163
120
        # for later comparison purposes with return value of
164
121
        # the fingerprint() function
165
122
        self.fingerprint = fingerprint.upper().replace(u" ", u"")
166
 
        logger.debug(u"  Fingerprint: %s", self.fingerprint)
167
123
        if secret:
168
124
            self.secret = secret.decode(u"base64")
169
125
        elif secfile:
176
132
        self.fqdn = fqdn                # string
177
133
        self.created = datetime.datetime.now()
178
134
        self.last_seen = None
179
 
        self.timeout = string_to_delta(timeout)
180
 
        self.interval = string_to_delta(interval)
 
135
        if timeout is None:
 
136
            self.timeout = options.timeout
 
137
        else:
 
138
            self.timeout = string_to_delta(timeout)
 
139
        if interval == -1:
 
140
            self.interval = options.interval
 
141
        else:
 
142
            self.interval = string_to_delta(interval)
181
143
        self.stop_hook = stop_hook
182
144
        self.checker = None
183
145
        self.checker_initiator_tag = None
185
147
        self.checker_callback_tag = None
186
148
        self.check_command = checker
187
149
    def start(self):
188
 
        """Start this client's checker and timeout hooks"""
 
150
        """Start this clients checker and timeout hooks"""
189
151
        # Schedule a new checker to be started an 'interval' from now,
190
152
        # and every interval from then on.
191
153
        self.checker_initiator_tag = gobject.timeout_add\
201
163
        """Stop this client.
202
164
        The possibility that this client might be restarted is left
203
165
        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:
 
166
        logger.debug(u"Stopping client %s", self.name)
 
167
        self.secret = None
 
168
        if self.stop_initiator_tag:
212
169
            gobject.source_remove(self.stop_initiator_tag)
213
170
            self.stop_initiator_tag = None
214
 
        if hasattr(self, "checker_initiator_tag") \
215
 
               and self.checker_initiator_tag:
 
171
        if self.checker_initiator_tag:
216
172
            gobject.source_remove(self.checker_initiator_tag)
217
173
            self.checker_initiator_tag = None
218
174
        self.stop_checker()
221
177
        # Do not run this again if called by a gobject.timeout_add
222
178
        return False
223
179
    def __del__(self):
224
 
        self.stop_hook = None
225
 
        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()
226
190
    def checker_callback(self, pid, condition):
227
191
        """The checker has completed, so take appropriate actions."""
228
192
        now = datetime.datetime.now()
229
 
        self.checker_callback_tag = None
230
 
        self.checker = None
231
193
        if os.WIFEXITED(condition) \
232
194
               and (os.WEXITSTATUS(condition) == 0):
233
195
            logger.debug(u"Checker for %(name)s succeeded",
243
205
        else:
244
206
            logger.debug(u"Checker for %(name)s failed",
245
207
                         vars(self))
 
208
            self.checker = None
 
209
        self.checker_callback_tag = None
246
210
    def start_checker(self):
247
211
        """Start a new checker subprocess if one is not running.
248
212
        If a checker already exists, leave it running and do
249
213
        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
214
        if self.checker is None:
259
215
            try:
260
216
                command = self.check_command % self.fqdn
285
241
        return True
286
242
    def stop_checker(self):
287
243
        """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
244
        if not hasattr(self, "checker") or self.checker is None:
292
245
            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
 
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)
302
251
        self.checker = None
303
252
    def still_valid(self, now=None):
304
253
        """Has the timeout not yet passed for this client?"""
311
260
 
312
261
 
313
262
def peer_certificate(session):
314
 
    "Return the peer's OpenPGP certificate as a bytestring"
 
263
    "Return an OpenPGP data packet string for the peer's certificate"
315
264
    # If not an OpenPGP certificate...
316
265
    if gnutls.library.functions.gnutls_certificate_type_get\
317
266
            (session._c_object) \
328
277
 
329
278
 
330
279
def fingerprint(openpgp):
331
 
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
 
280
    "Convert an OpenPGP data string to a hexdigit fingerprint string"
332
281
    # New empty GnuTLS certificate
333
282
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
334
283
    gnutls.library.functions.gnutls_openpgp_crt_init\
373
322
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
374
323
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
375
324
        #                "+DHE-DSS"))
376
 
        priority = "NORMAL"
377
 
        if self.server.options.priority:
378
 
            priority = self.server.options.priority
 
325
        priority = "SECURE256"
 
326
        
379
327
        gnutls.library.functions.gnutls_priority_set_direct\
380
328
            (session._c_object, priority, None);
381
329
        
394
342
            return
395
343
        logger.debug(u"Fingerprint: %s", fpr)
396
344
        client = None
397
 
        for c in self.server.clients:
 
345
        for c in clients:
398
346
            if c.fingerprint == fpr:
399
347
                client = c
400
348
                break
501
449
 
502
450
 
503
451
def add_service():
504
 
    """Derived from the Avahi example code"""
 
452
    """From the Avahi server example code"""
505
453
    global group, serviceName, serviceType, servicePort, serviceTXT, \
506
454
           domain, host
507
455
    if group is None:
526
474
 
527
475
 
528
476
def remove_service():
529
 
    """From the Avahi example code"""
 
477
    """From the Avahi server example code"""
530
478
    global group
531
479
    
532
480
    if not group is None:
534
482
 
535
483
 
536
484
def server_state_changed(state):
537
 
    """Derived from the Avahi example code"""
 
485
    """From the Avahi server example code"""
538
486
    if state == avahi.SERVER_COLLISION:
539
487
        logger.warning(u"Server name collision")
540
488
        remove_service()
543
491
 
544
492
 
545
493
def entry_group_state_changed(state, error):
546
 
    """Derived from the Avahi example code"""
 
494
    """From the Avahi server example code"""
547
495
    global serviceName, server, rename_count
548
496
    
549
497
    logger.debug(u"state change: %i", state)
619
567
        sys.exit(status)
620
568
 
621
569
 
622
 
def main():
623
 
    global exitstatus
 
570
if __name__ == '__main__':
624
571
    exitstatus = 0
625
 
    global main_loop_started
626
572
    main_loop_started = False
627
 
    
628
573
    parser = OptionParser()
629
574
    parser.add_option("-i", "--interface", type="string",
630
575
                      default=None, metavar="IF",
631
576
                      help="Bind to interface IF")
632
 
    parser.add_option("-a", "--address", type="string", default=None,
633
 
                      help="Address to listen for requests on")
634
577
    parser.add_option("-p", "--port", type="int", default=None,
635
578
                      help="Port number to receive requests on")
 
579
    parser.add_option("--timeout", type="string", # Parsed later
 
580
                      default="1h",
 
581
                      help="Amount of downtime allowed for clients")
 
582
    parser.add_option("--interval", type="string", # Parsed later
 
583
                      default="5m",
 
584
                      help="How often to check that a client is up")
636
585
    parser.add_option("--check", action="store_true", default=False,
637
586
                      help="Run self-test")
638
587
    parser.add_option("--debug", action="store_true", default=False,
639
588
                      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
589
    (options, args) = parser.parse_args()
647
590
    
648
591
    if options.check:
650
593
        doctest.testmod()
651
594
        sys.exit()
652
595
    
 
596
    # Parse the time arguments
 
597
    try:
 
598
        options.timeout = string_to_delta(options.timeout)
 
599
    except ValueError:
 
600
        parser.error("option --timeout: Unparseable time")
 
601
    try:
 
602
        options.interval = string_to_delta(options.interval)
 
603
    except ValueError:
 
604
        parser.error("option --interval: Unparseable time")
 
605
    
653
606
    # Parse config file
654
 
    defaults = { "timeout": "1h",
655
 
                 "interval": "5m",
656
 
                 "checker": "fping -q -- %%(fqdn)s",
657
 
                 }
 
607
    defaults = { "checker": "fping -q -- %%(fqdn)s" }
658
608
    client_config = ConfigParser.SafeConfigParser(defaults)
659
 
    #client_config.readfp(open("global.conf"), "global.conf")
 
609
    #client_config.readfp(open("secrets.conf"), "secrets.conf")
660
610
    client_config.read("mandos-clients.conf")
661
611
    
662
 
    global serviceName
663
 
    serviceName = options.servicename;
664
 
    
665
 
    global main_loop
666
 
    global bus
667
 
    global server
668
 
    # From the Avahi example code
 
612
    # From the Avahi server example code
669
613
    DBusGMainLoop(set_as_default=True )
670
614
    main_loop = gobject.MainLoop()
671
615
    bus = dbus.SystemBus()
691
635
            logger.debug(u"No clients left, exiting")
692
636
            killme()
693
637
    
694
 
    clients.update(Set(Client(name=section,
 
638
    clients.update(Set(Client(name=section, options=options,
695
639
                              stop_hook = remove_from_clients,
696
640
                              **(dict(client_config\
697
641
                                      .items(section))))
703
647
    def cleanup():
704
648
        "Cleanup function; run on exit"
705
649
        global group
706
 
        # From the Avahi example code
 
650
        # From the Avahi server example code
707
651
        if not group is None:
708
652
            group.Free()
709
653
            group = None
710
654
        # End of Avahi example code
711
655
        
712
 
        while clients:
713
 
            client = clients.pop()
 
656
        for client in clients:
714
657
            client.stop_hook = None
715
658
            client.stop()
716
659
    
724
667
    for client in clients:
725
668
        client.start()
726
669
    
727
 
    tcp_server = IPv6_TCPServer((options.address, options.port),
 
670
    tcp_server = IPv6_TCPServer((None, options.port),
728
671
                                tcp_handler,
729
672
                                options=options,
730
673
                                clients=clients)
731
674
    # Find out what random port we got
732
 
    global servicePort
733
675
    servicePort = tcp_server.socket.getsockname()[1]
734
676
    logger.debug(u"Now listening on port %d", servicePort)
735
677
    
736
678
    if options.interface is not None:
737
 
        global serviceInterface
738
679
        serviceInterface = if_nametoindex(options.interface)
739
680
    
740
 
    # From the Avahi example code
 
681
    # From the Avahi server example code
741
682
    server.connect_to_signal("StateChanged", server_state_changed)
742
683
    try:
743
684
        server_state_changed(server.GetState())
751
692
                         tcp_server.handle_request(*args[2:],
752
693
                                                   **kwargs) or True)
753
694
    try:
754
 
        logger.debug("Starting main loop")
755
695
        main_loop_started = True
756
696
        main_loop.run()
757
697
    except KeyboardInterrupt:
759
699
            print
760
700
    
761
701
    sys.exit(exitstatus)
762
 
 
763
 
if __name__ == '__main__':
764
 
    main()