/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 mandos

  • Committer: Teddy Hogeborn
  • Date: 2015-05-23 10:41:35 UTC
  • Revision ID: teddy@recompile.se-20150523104135-s08sbkurj1wf0jrz
mandos-keygen: Update copyright year.

Show diffs side-by-side

added added

removed removed

Lines of Context:
36
36
 
37
37
from future_builtins import *
38
38
 
39
 
try:
40
 
    import SocketServer as socketserver
41
 
except ImportError:
42
 
    import socketserver
 
39
import SocketServer as socketserver
43
40
import socket
44
41
import argparse
45
42
import datetime
50
47
import gnutls.library.functions
51
48
import gnutls.library.constants
52
49
import gnutls.library.types
53
 
try:
54
 
    import ConfigParser as configparser
55
 
except ImportError:
56
 
    import configparser
 
50
import ConfigParser as configparser
57
51
import sys
58
52
import re
59
53
import os
68
62
import struct
69
63
import fcntl
70
64
import functools
71
 
try:
72
 
    import cPickle as pickle
73
 
except ImportError:
74
 
    import pickle
 
65
import cPickle as pickle
75
66
import multiprocessing
76
67
import types
77
68
import binascii
78
69
import tempfile
79
70
import itertools
80
71
import collections
81
 
import codecs
82
72
 
83
73
import dbus
84
74
import dbus.service
85
 
try:
86
 
    import gobject
87
 
except ImportError:
88
 
    from gi.repository import GObject as gobject
 
75
import gobject
89
76
import avahi
90
77
from dbus.mainloop.glib import DBusGMainLoop
91
78
import ctypes
395
382
                    logger.error(bad_states[state] + ": %r", error)
396
383
            self.cleanup()
397
384
        elif state == avahi.SERVER_RUNNING:
398
 
            try:
399
 
                self.add()
400
 
            except dbus.exceptions.DBusException as error:
401
 
                if (error.get_dbus_name()
402
 
                    == "org.freedesktop.Avahi.CollisionError"):
403
 
                    logger.info("Local Zeroconf service name"
404
 
                                " collision.")
405
 
                    return self.rename(remove=False)
406
 
                else:
407
 
                    logger.critical("D-Bus Exception", exc_info=error)
408
 
                    self.cleanup()
409
 
                    os._exit(1)
 
385
            self.add()
410
386
        else:
411
387
            if error is None:
412
388
                logger.debug("Unknown state: %r", state)
435
411
            .format(self.name)))
436
412
        return ret
437
413
 
438
 
def call_pipe(connection,       # : multiprocessing.Connection
439
 
              func, *args, **kwargs):
440
 
    """This function is meant to be called by multiprocessing.Process
441
 
    
442
 
    This function runs func(*args, **kwargs), and writes the resulting
443
 
    return value on the provided multiprocessing.Connection.
444
 
    """
445
 
    connection.send(func(*args, **kwargs))
446
 
    connection.close()
447
414
 
448
415
class Client(object):
449
416
    """A representation of a client host served by this server.
476
443
    last_checker_status: integer between 0 and 255 reflecting exit
477
444
                         status of last checker. -1 reflects crashed
478
445
                         checker, -2 means no checker completed yet.
479
 
    last_checker_signal: The signal which killed the last checker, if
480
 
                         last_checker_status is -1
481
446
    last_enabled: datetime.datetime(); (UTC) or None
482
447
    name:       string; from the config file, used in log messages and
483
448
                        D-Bus identifiers
657
622
        # Also start a new checker *right now*.
658
623
        self.start_checker()
659
624
    
660
 
    def checker_callback(self, source, condition, connection,
661
 
                         command):
 
625
    def checker_callback(self, pid, condition, command):
662
626
        """The checker has completed, so take appropriate actions."""
663
627
        self.checker_callback_tag = None
664
628
        self.checker = None
665
 
        # Read return code from connection (see call_pipe)
666
 
        returncode = connection.recv()
667
 
        connection.close()
668
 
        
669
 
        if returncode >= 0:
670
 
            self.last_checker_status = returncode
671
 
            self.last_checker_signal = None
 
629
        if os.WIFEXITED(condition):
 
630
            self.last_checker_status = os.WEXITSTATUS(condition)
672
631
            if self.last_checker_status == 0:
673
632
                logger.info("Checker for %(name)s succeeded",
674
633
                            vars(self))
677
636
                logger.info("Checker for %(name)s failed", vars(self))
678
637
        else:
679
638
            self.last_checker_status = -1
680
 
            self.last_checker_signal = -returncode
681
639
            logger.warning("Checker for %(name)s crashed?",
682
640
                           vars(self))
683
 
        return False
684
641
    
685
642
    def checked_ok(self):
686
643
        """Assert that the client has been seen, alive and well."""
687
644
        self.last_checked_ok = datetime.datetime.utcnow()
688
645
        self.last_checker_status = 0
689
 
        self.last_checker_signal = None
690
646
        self.bump_timeout()
691
647
    
692
648
    def bump_timeout(self, timeout=None):
718
674
        # than 'timeout' for the client to be disabled, which is as it
719
675
        # should be.
720
676
        
721
 
        if self.checker is not None and not self.checker.is_alive():
722
 
            logger.warning("Checker was not alive; joining")
723
 
            self.checker.join()
724
 
            self.checker = None
 
677
        # If a checker exists, make sure it is not a zombie
 
678
        try:
 
679
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
680
        except AttributeError:
 
681
            pass
 
682
        except OSError as error:
 
683
            if error.errno != errno.ECHILD:
 
684
                raise
 
685
        else:
 
686
            if pid:
 
687
                logger.warning("Checker was a zombie")
 
688
                gobject.source_remove(self.checker_callback_tag)
 
689
                self.checker_callback(pid, status,
 
690
                                      self.current_checker_command)
725
691
        # Start a new checker if needed
726
692
        if self.checker is None:
727
693
            # Escape attributes for the shell
736
702
                             exc_info=error)
737
703
                return True     # Try again later
738
704
            self.current_checker_command = command
739
 
            logger.info("Starting checker %r for %s", command,
740
 
                        self.name)
741
 
            # We don't need to redirect stdout and stderr, since
742
 
            # in normal mode, that is already done by daemon(),
743
 
            # and in debug mode we don't want to.  (Stdin is
744
 
            # always replaced by /dev/null.)
745
 
            # The exception is when not debugging but nevertheless
746
 
            # running in the foreground; use the previously
747
 
            # created wnull.
748
 
            popen_args = { "close_fds": True,
749
 
                           "shell": True,
750
 
                           "cwd": "/" }
751
 
            if (not self.server_settings["debug"]
752
 
                and self.server_settings["foreground"]):
753
 
                popen_args.update({"stdout": wnull,
754
 
                                   "stderr": wnull })
755
 
            pipe = multiprocessing.Pipe(duplex = False)
756
 
            self.checker = multiprocessing.Process(
757
 
                target = call_pipe,
758
 
                args = (pipe[1], subprocess.call, command),
759
 
                kwargs = popen_args)
760
 
            self.checker.start()
761
 
            self.checker_callback_tag = gobject.io_add_watch(
762
 
                pipe[0].fileno(), gobject.IO_IN,
763
 
                self.checker_callback, pipe[0], command)
 
705
            try:
 
706
                logger.info("Starting checker %r for %s", command,
 
707
                            self.name)
 
708
                # We don't need to redirect stdout and stderr, since
 
709
                # in normal mode, that is already done by daemon(),
 
710
                # and in debug mode we don't want to.  (Stdin is
 
711
                # always replaced by /dev/null.)
 
712
                # The exception is when not debugging but nevertheless
 
713
                # running in the foreground; use the previously
 
714
                # created wnull.
 
715
                popen_args = {}
 
716
                if (not self.server_settings["debug"]
 
717
                    and self.server_settings["foreground"]):
 
718
                    popen_args.update({"stdout": wnull,
 
719
                                       "stderr": wnull })
 
720
                self.checker = subprocess.Popen(command,
 
721
                                                close_fds=True,
 
722
                                                shell=True,
 
723
                                                cwd="/",
 
724
                                                **popen_args)
 
725
            except OSError as error:
 
726
                logger.error("Failed to start subprocess",
 
727
                             exc_info=error)
 
728
                return True
 
729
            self.checker_callback_tag = gobject.child_watch_add(
 
730
                self.checker.pid, self.checker_callback, data=command)
 
731
            # The checker may have completed before the gobject
 
732
            # watch was added.  Check for this.
 
733
            try:
 
734
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
735
            except OSError as error:
 
736
                if error.errno == errno.ECHILD:
 
737
                    # This should never happen
 
738
                    logger.error("Child process vanished",
 
739
                                 exc_info=error)
 
740
                    return True
 
741
                raise
 
742
            if pid:
 
743
                gobject.source_remove(self.checker_callback_tag)
 
744
                self.checker_callback(pid, status, command)
764
745
        # Re-run this periodically if run by gobject.timeout_add
765
746
        return True
766
747
    
772
753
        if getattr(self, "checker", None) is None:
773
754
            return
774
755
        logger.debug("Stopping checker for %(name)s", vars(self))
775
 
        self.checker.terminate()
 
756
        try:
 
757
            self.checker.terminate()
 
758
            #time.sleep(0.5)
 
759
            #if self.checker.poll() is None:
 
760
            #    self.checker.kill()
 
761
        except OSError as error:
 
762
            if error.errno != errno.ESRCH: # No such process
 
763
                raise
776
764
        self.checker = None
777
765
 
778
766
 
1110
1098
                interface_names.add(alt_interface)
1111
1099
                # Is this a D-Bus signal?
1112
1100
                if getattr(attribute, "_dbus_is_signal", False):
1113
 
                    if sys.version_info.major == 2:
1114
 
                        # Extract the original non-method undecorated
1115
 
                        # function by black magic
1116
 
                        nonmethod_func = (dict(
1117
 
                            zip(attribute.func_code.co_freevars,
1118
 
                                attribute.__closure__))
1119
 
                                          ["func"].cell_contents)
1120
 
                    else:
1121
 
                        nonmethod_func = attribute
 
1101
                    # Extract the original non-method undecorated
 
1102
                    # function by black magic
 
1103
                    nonmethod_func = (dict(
 
1104
                        zip(attribute.func_code.co_freevars,
 
1105
                            attribute.__closure__))
 
1106
                                      ["func"].cell_contents)
1122
1107
                    # Create a new, but exactly alike, function
1123
1108
                    # object, and decorate it to be a new D-Bus signal
1124
1109
                    # with the alternate D-Bus interface name
1125
 
                    if sys.version_info.major == 2:
1126
 
                        new_function = types.FunctionType(
1127
 
                            nonmethod_func.func_code,
1128
 
                            nonmethod_func.func_globals,
1129
 
                            nonmethod_func.func_name,
1130
 
                            nonmethod_func.func_defaults,
1131
 
                            nonmethod_func.func_closure)
1132
 
                    else:
1133
 
                        new_function = types.FunctionType(
1134
 
                            nonmethod_func.__code__,
1135
 
                            nonmethod_func.__globals__,
1136
 
                            nonmethod_func.__name__,
1137
 
                            nonmethod_func.__defaults__,
1138
 
                            nonmethod_func.__closure__)
1139
1110
                    new_function = (dbus.service.signal(
1140
 
                        alt_interface,
1141
 
                        attribute._dbus_signature)(new_function))
 
1111
                        alt_interface, attribute._dbus_signature)
 
1112
                                    (types.FunctionType(
 
1113
                                        nonmethod_func.func_code,
 
1114
                                        nonmethod_func.func_globals,
 
1115
                                        nonmethod_func.func_name,
 
1116
                                        nonmethod_func.func_defaults,
 
1117
                                        nonmethod_func.func_closure)))
1142
1118
                    # Copy annotations, if any
1143
1119
                    try:
1144
1120
                        new_function._dbus_annotations = dict(
1367
1343
            DBusObjectWithProperties.__del__(self, *args, **kwargs)
1368
1344
        Client.__del__(self, *args, **kwargs)
1369
1345
    
1370
 
    def checker_callback(self, source, condition,
1371
 
                         connection, command, *args, **kwargs):
1372
 
        ret = Client.checker_callback(self, source, condition,
1373
 
                                      connection, command, *args,
1374
 
                                      **kwargs)
1375
 
        exitstatus = self.last_checker_status
1376
 
        if exitstatus >= 0:
 
1346
    def checker_callback(self, pid, condition, command,
 
1347
                         *args, **kwargs):
 
1348
        self.checker_callback_tag = None
 
1349
        self.checker = None
 
1350
        if os.WIFEXITED(condition):
 
1351
            exitstatus = os.WEXITSTATUS(condition)
1377
1352
            # Emit D-Bus signal
1378
1353
            self.CheckerCompleted(dbus.Int16(exitstatus),
1379
 
                                  dbus.Int64(0),
 
1354
                                  dbus.Int64(condition),
1380
1355
                                  dbus.String(command))
1381
1356
        else:
1382
1357
            # Emit D-Bus signal
1383
1358
            self.CheckerCompleted(dbus.Int16(-1),
1384
 
                                  dbus.Int64(
1385
 
                                      self.last_checker_signal),
 
1359
                                  dbus.Int64(condition),
1386
1360
                                  dbus.String(command))
1387
 
        return ret
 
1361
        
 
1362
        return Client.checker_callback(self, pid, condition, command,
 
1363
                                       *args, **kwargs)
1388
1364
    
1389
1365
    def start_checker(self, *args, **kwargs):
1390
1366
        old_checker_pid = getattr(self.checker, "pid", None)
1683
1659
        self._pipe = child_pipe
1684
1660
        self._pipe.send(('init', fpr, address))
1685
1661
        if not self._pipe.recv():
1686
 
            raise KeyError(fpr)
 
1662
            raise KeyError()
1687
1663
    
1688
1664
    def __getattribute__(self, name):
1689
1665
        if name == '_pipe':
2152
2128
        
2153
2129
        if command == 'getattr':
2154
2130
            attrname = request[1]
2155
 
            if isinstance(client_object.__getattribute__(attrname),
2156
 
                          collections.Callable):
 
2131
            if callable(client_object.__getattribute__(attrname)):
2157
2132
                parent_pipe.send(('function', ))
2158
2133
            else:
2159
2134
                parent_pipe.send((
2194
2169
    # avoid excessive use of external libraries.
2195
2170
    
2196
2171
    # New type for defining tokens, syntax, and semantics all-in-one
 
2172
    Token = collections.namedtuple("Token",
 
2173
                                   ("regexp", # To match token; if
 
2174
                                              # "value" is not None,
 
2175
                                              # must have a "group"
 
2176
                                              # containing digits
 
2177
                                    "value",  # datetime.timedelta or
 
2178
                                              # None
 
2179
                                    "followers")) # Tokens valid after
 
2180
                                                  # this token
2197
2181
    Token = collections.namedtuple("Token", (
2198
2182
        "regexp",  # To match token; if "value" is not None, must have
2199
2183
                   # a "group" containing digits
2234
2218
    # Define starting values
2235
2219
    value = datetime.timedelta() # Value so far
2236
2220
    found_token = None
2237
 
    followers = frozenset((token_duration, )) # Following valid tokens
 
2221
    followers = frozenset((token_duration,)) # Following valid tokens
2238
2222
    s = duration                # String left to parse
2239
2223
    # Loop until end token is found
2240
2224
    while found_token is not token_end:
2257
2241
                break
2258
2242
        else:
2259
2243
            # No currently valid tokens were found
2260
 
            raise ValueError("Invalid RFC 3339 duration: {!r}"
2261
 
                             .format(duration))
 
2244
            raise ValueError("Invalid RFC 3339 duration")
2262
2245
    # End token found
2263
2246
    return value
2264
2247
 
2397
2380
                        "debug": "False",
2398
2381
                        "priority":
2399
2382
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2400
 
                        ":+SIGN-DSA-SHA256",
 
2383
                        ":+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
2401
2384
                        "servicename": "Mandos",
2402
2385
                        "use_dbus": "True",
2403
2386
                        "use_ipv6": "True",
2515
2498
            pidfilename = "/var/run/mandos.pid"
2516
2499
        pidfile = None
2517
2500
        try:
2518
 
            pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
 
2501
            pidfile = open(pidfilename, "w")
2519
2502
        except IOError as e:
2520
2503
            logger.error("Could not open file %r", pidfilename,
2521
2504
                         exc_info=e)
2580
2563
            old_bus_name = dbus.service.BusName(
2581
2564
                "se.bsnet.fukt.Mandos", bus,
2582
2565
                do_not_queue=True)
2583
 
        except dbus.exceptions.DBusException as e:
 
2566
        except dbus.exceptions.NameExistsException as e:
2584
2567
            logger.error("Disabling D-Bus:", exc_info=e)
2585
2568
            use_dbus = False
2586
2569
            server_settings["use_dbus"] = False
2659
2642
                    pass
2660
2643
            
2661
2644
            # Clients who has passed its expire date can still be
2662
 
            # enabled if its last checker was successful.  A Client
 
2645
            # enabled if its last checker was successful.  Clients
2663
2646
            # whose checker succeeded before we stored its state is
2664
2647
            # assumed to have successfully run all checkers during
2665
2648
            # downtime.
2717
2700
    
2718
2701
    if not foreground:
2719
2702
        if pidfile is not None:
2720
 
            pid = os.getpid()
2721
2703
            try:
2722
2704
                with pidfile:
2723
 
                    print(pid, file=pidfile)
 
2705
                    pid = os.getpid()
 
2706
                    pidfile.write("{}\n".format(pid).encode("utf-8"))
2724
2707
            except IOError:
2725
2708
                logger.error("Could not write to file %r with PID %d",
2726
2709
                             pidfilename, pid)