/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 01:50:31 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080721015031-ivjiclli2j06hwgs
* Makefile: Include targets for all binaries.

* plugins.d/Makefile: Do nothing but call the parent Makefile.

* server.py: Added copyright statement and information.
  (Client.__init__): Added doc string.
  (Client.stop): Bug fix: Only log message if stopping for the first
                 time.  Check if the "stop_initiator_tag" and
                 "checker_initiator_tag" attributes exist before using
                 them.
  (Client.__del__): Call self.stop() instead of doing things here.
  (Client.checker_callback): Set self.checker_callback_tag and
                             self.checker to None unconditionally and
                             immediately.
  (Client.stop_checker): Added some checks to handle multiple calls.
  (tcp_handler.handle): Use "self.server.clients" instead of "clients".
  (__main__): Moved all code to the "main" function.
  (main): New.  New option "--address".  Instantiate IPv6_TCPServer
          with "options.address".  Log before starting main loop.
  (main.cleanup) Use "clients.pop()" to remove clients from the set as
                 they are stopped.

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