/mandos/release

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

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2015-07-12 01:07:14 UTC
  • mto: (237.7.594 trunk)
  • mto: This revision was merged to the branch mainline in revision 325.
  • Revision ID: teddy@recompile.se-20150712010714-peiodtm9ay0nbjs2
plugins.d/mandos-client.c (main): See why it failed to bring up interface.

Show diffs side-by-side

added added

removed removed

Lines of Context:
36
36
 
37
37
from future_builtins import *
38
38
 
39
 
import SocketServer as socketserver
 
39
try:
 
40
    import SocketServer as socketserver
 
41
except ImportError:
 
42
    import socketserver
40
43
import socket
41
44
import argparse
42
45
import datetime
47
50
import gnutls.library.functions
48
51
import gnutls.library.constants
49
52
import gnutls.library.types
50
 
import ConfigParser as configparser
 
53
try:
 
54
    import ConfigParser as configparser
 
55
except ImportError:
 
56
    import configparser
51
57
import sys
52
58
import re
53
59
import os
62
68
import struct
63
69
import fcntl
64
70
import functools
65
 
import cPickle as pickle
 
71
try:
 
72
    import cPickle as pickle
 
73
except ImportError:
 
74
    import pickle
66
75
import multiprocessing
67
76
import types
68
77
import binascii
69
78
import tempfile
70
79
import itertools
71
80
import collections
 
81
import codecs
72
82
 
73
83
import dbus
74
84
import dbus.service
75
 
import gobject
 
85
try:
 
86
    import gobject
 
87
except ImportError:
 
88
    from gi.repository import GObject as gobject
76
89
import avahi
77
90
from dbus.mainloop.glib import DBusGMainLoop
78
91
import ctypes
411
424
            .format(self.name)))
412
425
        return ret
413
426
 
 
427
def call_pipe(connection,       # : multiprocessing.Connection
 
428
              func, *args, **kwargs):
 
429
    """This function is meant to be called by multiprocessing.Process
 
430
    
 
431
    This function runs func(*args, **kwargs), and writes the resulting
 
432
    return value on the provided multiprocessing.Connection.
 
433
    """
 
434
    connection.send(func(*args, **kwargs))
 
435
    connection.close()
414
436
 
415
437
class Client(object):
416
438
    """A representation of a client host served by this server.
443
465
    last_checker_status: integer between 0 and 255 reflecting exit
444
466
                         status of last checker. -1 reflects crashed
445
467
                         checker, -2 means no checker completed yet.
 
468
    last_checker_signal: The signal which killed the last checker, if
 
469
                         last_checker_status is -1
446
470
    last_enabled: datetime.datetime(); (UTC) or None
447
471
    name:       string; from the config file, used in log messages and
448
472
                        D-Bus identifiers
622
646
        # Also start a new checker *right now*.
623
647
        self.start_checker()
624
648
    
625
 
    def checker_callback(self, pid, condition, command):
 
649
    def checker_callback(self, source, condition, connection,
 
650
                         command):
626
651
        """The checker has completed, so take appropriate actions."""
627
652
        self.checker_callback_tag = None
628
653
        self.checker = None
629
 
        if os.WIFEXITED(condition):
630
 
            self.last_checker_status = os.WEXITSTATUS(condition)
 
654
        # Read return code from connection (see call_pipe)
 
655
        returncode = connection.recv()
 
656
        connection.close()
 
657
        
 
658
        if returncode >= 0:
 
659
            self.last_checker_status = returncode
 
660
            self.last_checker_signal = None
631
661
            if self.last_checker_status == 0:
632
662
                logger.info("Checker for %(name)s succeeded",
633
663
                            vars(self))
636
666
                logger.info("Checker for %(name)s failed", vars(self))
637
667
        else:
638
668
            self.last_checker_status = -1
 
669
            self.last_checker_signal = -returncode
639
670
            logger.warning("Checker for %(name)s crashed?",
640
671
                           vars(self))
 
672
        return False
641
673
    
642
674
    def checked_ok(self):
643
675
        """Assert that the client has been seen, alive and well."""
644
676
        self.last_checked_ok = datetime.datetime.utcnow()
645
677
        self.last_checker_status = 0
 
678
        self.last_checker_signal = None
646
679
        self.bump_timeout()
647
680
    
648
681
    def bump_timeout(self, timeout=None):
674
707
        # than 'timeout' for the client to be disabled, which is as it
675
708
        # should be.
676
709
        
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)
 
710
        if self.checker is not None and not self.checker.is_alive():
 
711
            logger.warning("Checker was not alive; joining")
 
712
            self.checker.join()
 
713
            self.checker = None
691
714
        # Start a new checker if needed
692
715
        if self.checker is None:
693
716
            # Escape attributes for the shell
702
725
                             exc_info=error)
703
726
                return True     # Try again later
704
727
            self.current_checker_command = 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)
 
728
            logger.info("Starting checker %r for %s", command,
 
729
                        self.name)
 
730
            # We don't need to redirect stdout and stderr, since
 
731
            # in normal mode, that is already done by daemon(),
 
732
            # and in debug mode we don't want to.  (Stdin is
 
733
            # always replaced by /dev/null.)
 
734
            # The exception is when not debugging but nevertheless
 
735
            # running in the foreground; use the previously
 
736
            # created wnull.
 
737
            popen_args = { "close_fds": True,
 
738
                           "shell": True,
 
739
                           "cwd": "/" }
 
740
            if (not self.server_settings["debug"]
 
741
                and self.server_settings["foreground"]):
 
742
                popen_args.update({"stdout": wnull,
 
743
                                   "stderr": wnull })
 
744
            pipe = multiprocessing.Pipe(duplex = False)
 
745
            self.checker = multiprocessing.Process(
 
746
                target = call_pipe,
 
747
                args = (pipe[1], subprocess.call, command),
 
748
                kwargs = popen_args)
 
749
            self.checker.start()
 
750
            self.checker_callback_tag = gobject.io_add_watch(
 
751
                pipe[0].fileno(), gobject.IO_IN,
 
752
                self.checker_callback, pipe[0], command)
745
753
        # Re-run this periodically if run by gobject.timeout_add
746
754
        return True
747
755
    
753
761
        if getattr(self, "checker", None) is None:
754
762
            return
755
763
        logger.debug("Stopping checker for %(name)s", vars(self))
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
 
764
        self.checker.terminate()
764
765
        self.checker = None
765
766
 
766
767
 
1098
1099
                interface_names.add(alt_interface)
1099
1100
                # Is this a D-Bus signal?
1100
1101
                if getattr(attribute, "_dbus_is_signal", False):
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)
 
1102
                    if sys.version_info.major == 2:
 
1103
                        # Extract the original non-method undecorated
 
1104
                        # function by black magic
 
1105
                        nonmethod_func = (dict(
 
1106
                            zip(attribute.func_code.co_freevars,
 
1107
                                attribute.__closure__))
 
1108
                                          ["func"].cell_contents)
 
1109
                    else:
 
1110
                        nonmethod_func = attribute
1107
1111
                    # Create a new, but exactly alike, function
1108
1112
                    # object, and decorate it to be a new D-Bus signal
1109
1113
                    # with the alternate D-Bus interface name
 
1114
                    if sys.version_info.major == 2:
 
1115
                        new_function = types.FunctionType(
 
1116
                            nonmethod_func.func_code,
 
1117
                            nonmethod_func.func_globals,
 
1118
                            nonmethod_func.func_name,
 
1119
                            nonmethod_func.func_defaults,
 
1120
                            nonmethod_func.func_closure)
 
1121
                    else:
 
1122
                        new_function = types.FunctionType(
 
1123
                            nonmethod_func.__code__,
 
1124
                            nonmethod_func.__globals__,
 
1125
                            nonmethod_func.__name__,
 
1126
                            nonmethod_func.__defaults__,
 
1127
                            nonmethod_func.__closure__)
1110
1128
                    new_function = (dbus.service.signal(
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)))
 
1129
                        alt_interface,
 
1130
                        attribute._dbus_signature)(new_function))
1118
1131
                    # Copy annotations, if any
1119
1132
                    try:
1120
1133
                        new_function._dbus_annotations = dict(
1343
1356
            DBusObjectWithProperties.__del__(self, *args, **kwargs)
1344
1357
        Client.__del__(self, *args, **kwargs)
1345
1358
    
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)
 
1359
    def checker_callback(self, source, condition,
 
1360
                         connection, command, *args, **kwargs):
 
1361
        ret = Client.checker_callback(self, source, condition,
 
1362
                                      connection, command, *args,
 
1363
                                      **kwargs)
 
1364
        exitstatus = self.last_checker_status
 
1365
        if exitstatus >= 0:
1352
1366
            # Emit D-Bus signal
1353
1367
            self.CheckerCompleted(dbus.Int16(exitstatus),
1354
 
                                  dbus.Int64(condition),
 
1368
                                  dbus.Int64(0),
1355
1369
                                  dbus.String(command))
1356
1370
        else:
1357
1371
            # Emit D-Bus signal
1358
1372
            self.CheckerCompleted(dbus.Int16(-1),
1359
 
                                  dbus.Int64(condition),
 
1373
                                  dbus.Int64(
 
1374
                                      self.last_checker_signal),
1360
1375
                                  dbus.String(command))
1361
 
        
1362
 
        return Client.checker_callback(self, pid, condition, command,
1363
 
                                       *args, **kwargs)
 
1376
        return ret
1364
1377
    
1365
1378
    def start_checker(self, *args, **kwargs):
1366
1379
        old_checker_pid = getattr(self.checker, "pid", None)
1659
1672
        self._pipe = child_pipe
1660
1673
        self._pipe.send(('init', fpr, address))
1661
1674
        if not self._pipe.recv():
1662
 
            raise KeyError()
 
1675
            raise KeyError(fpr)
1663
1676
    
1664
1677
    def __getattribute__(self, name):
1665
1678
        if name == '_pipe':
2128
2141
        
2129
2142
        if command == 'getattr':
2130
2143
            attrname = request[1]
2131
 
            if callable(client_object.__getattribute__(attrname)):
 
2144
            if isinstance(client_object.__getattribute__(attrname),
 
2145
                          collections.Callable):
2132
2146
                parent_pipe.send(('function', ))
2133
2147
            else:
2134
2148
                parent_pipe.send((
2169
2183
    # avoid excessive use of external libraries.
2170
2184
    
2171
2185
    # 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
2181
2186
    Token = collections.namedtuple("Token", (
2182
2187
        "regexp",  # To match token; if "value" is not None, must have
2183
2188
                   # a "group" containing digits
2218
2223
    # Define starting values
2219
2224
    value = datetime.timedelta() # Value so far
2220
2225
    found_token = None
2221
 
    followers = frozenset((token_duration,)) # Following valid tokens
 
2226
    followers = frozenset((token_duration, )) # Following valid tokens
2222
2227
    s = duration                # String left to parse
2223
2228
    # Loop until end token is found
2224
2229
    while found_token is not token_end:
2241
2246
                break
2242
2247
        else:
2243
2248
            # No currently valid tokens were found
2244
 
            raise ValueError("Invalid RFC 3339 duration")
 
2249
            raise ValueError("Invalid RFC 3339 duration: {!r}"
 
2250
                             .format(duration))
2245
2251
    # End token found
2246
2252
    return value
2247
2253
 
2498
2504
            pidfilename = "/var/run/mandos.pid"
2499
2505
        pidfile = None
2500
2506
        try:
2501
 
            pidfile = open(pidfilename, "w")
 
2507
            pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
2502
2508
        except IOError as e:
2503
2509
            logger.error("Could not open file %r", pidfilename,
2504
2510
                         exc_info=e)
2563
2569
            old_bus_name = dbus.service.BusName(
2564
2570
                "se.bsnet.fukt.Mandos", bus,
2565
2571
                do_not_queue=True)
2566
 
        except dbus.exceptions.NameExistsException as e:
 
2572
        except dbus.exceptions.DBusException as e:
2567
2573
            logger.error("Disabling D-Bus:", exc_info=e)
2568
2574
            use_dbus = False
2569
2575
            server_settings["use_dbus"] = False
2642
2648
                    pass
2643
2649
            
2644
2650
            # Clients who has passed its expire date can still be
2645
 
            # enabled if its last checker was successful.  Clients
 
2651
            # enabled if its last checker was successful.  A Client
2646
2652
            # whose checker succeeded before we stored its state is
2647
2653
            # assumed to have successfully run all checkers during
2648
2654
            # downtime.
2700
2706
    
2701
2707
    if not foreground:
2702
2708
        if pidfile is not None:
 
2709
            pid = os.getpid()
2703
2710
            try:
2704
2711
                with pidfile:
2705
 
                    pid = os.getpid()
2706
 
                    pidfile.write("{}\n".format(pid).encode("utf-8"))
 
2712
                    print(pid, file=pidfile)
2707
2713
            except IOError:
2708
2714
                logger.error("Could not write to file %r with PID %d",
2709
2715
                             pidfilename, pid)