/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-22 06:23:29 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080722062329-jaex5y5mslf6sqjx
* mandos-clients.conf ([DEFAULT]): New section.

* plugins.d/mandosclient.c (start_mandos_communication): Only print if
                                                         debugging.
                                                         Print server
                                                         name.
                                                         Bug fix: Loop
                                                         until suc-
                                                         cess.

* server.py (serverName): Set via option, not globally.
  (Client.__init__): Removed argument "options".  Require "timeout"
                     and "interval" arguments.
  (tcp_handler.handle): Set "priority" from self.server.options.
  (main): Removed "--timeout" and "--interval" options.  New options
          "--priority" and "--servicename".  Add defaults for
          "timeout" and "interval".  Set "serviceName" from options.
          Do not pass "options" to "Client()".

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