/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 12:07:07 UTC
  • Revision ID: teddy@recompile.se-20150523120707-t5fq0brh2kxkvw8g
mandos: Some more minor changes to prepare for Python 3.

Show diffs side-by-side

added added

removed removed

Lines of Context:
78
78
import tempfile
79
79
import itertools
80
80
import collections
81
 
import codecs
82
81
 
83
82
import dbus
84
83
import dbus.service
424
423
            .format(self.name)))
425
424
        return ret
426
425
 
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()
436
426
 
437
427
class Client(object):
438
428
    """A representation of a client host served by this server.
465
455
    last_checker_status: integer between 0 and 255 reflecting exit
466
456
                         status of last checker. -1 reflects crashed
467
457
                         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
470
458
    last_enabled: datetime.datetime(); (UTC) or None
471
459
    name:       string; from the config file, used in log messages and
472
460
                        D-Bus identifiers
646
634
        # Also start a new checker *right now*.
647
635
        self.start_checker()
648
636
    
649
 
    def checker_callback(self, source, condition, connection,
650
 
                         command):
 
637
    def checker_callback(self, pid, condition, command):
651
638
        """The checker has completed, so take appropriate actions."""
652
639
        self.checker_callback_tag = None
653
640
        self.checker = None
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
 
641
        if os.WIFEXITED(condition):
 
642
            self.last_checker_status = os.WEXITSTATUS(condition)
661
643
            if self.last_checker_status == 0:
662
644
                logger.info("Checker for %(name)s succeeded",
663
645
                            vars(self))
666
648
                logger.info("Checker for %(name)s failed", vars(self))
667
649
        else:
668
650
            self.last_checker_status = -1
669
 
            self.last_checker_signal = -returncode
670
651
            logger.warning("Checker for %(name)s crashed?",
671
652
                           vars(self))
672
 
        return False
673
653
    
674
654
    def checked_ok(self):
675
655
        """Assert that the client has been seen, alive and well."""
676
656
        self.last_checked_ok = datetime.datetime.utcnow()
677
657
        self.last_checker_status = 0
678
 
        self.last_checker_signal = None
679
658
        self.bump_timeout()
680
659
    
681
660
    def bump_timeout(self, timeout=None):
707
686
        # than 'timeout' for the client to be disabled, which is as it
708
687
        # should be.
709
688
        
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
 
689
        # If a checker exists, make sure it is not a zombie
 
690
        try:
 
691
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
692
        except AttributeError:
 
693
            pass
 
694
        except OSError as error:
 
695
            if error.errno != errno.ECHILD:
 
696
                raise
 
697
        else:
 
698
            if pid:
 
699
                logger.warning("Checker was a zombie")
 
700
                gobject.source_remove(self.checker_callback_tag)
 
701
                self.checker_callback(pid, status,
 
702
                                      self.current_checker_command)
714
703
        # Start a new checker if needed
715
704
        if self.checker is None:
716
705
            # Escape attributes for the shell
725
714
                             exc_info=error)
726
715
                return True     # Try again later
727
716
            self.current_checker_command = 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)
 
717
            try:
 
718
                logger.info("Starting checker %r for %s", command,
 
719
                            self.name)
 
720
                # We don't need to redirect stdout and stderr, since
 
721
                # in normal mode, that is already done by daemon(),
 
722
                # and in debug mode we don't want to.  (Stdin is
 
723
                # always replaced by /dev/null.)
 
724
                # The exception is when not debugging but nevertheless
 
725
                # running in the foreground; use the previously
 
726
                # created wnull.
 
727
                popen_args = {}
 
728
                if (not self.server_settings["debug"]
 
729
                    and self.server_settings["foreground"]):
 
730
                    popen_args.update({"stdout": wnull,
 
731
                                       "stderr": wnull })
 
732
                self.checker = subprocess.Popen(command,
 
733
                                                close_fds=True,
 
734
                                                shell=True,
 
735
                                                cwd="/",
 
736
                                                **popen_args)
 
737
            except OSError as error:
 
738
                logger.error("Failed to start subprocess",
 
739
                             exc_info=error)
 
740
                return True
 
741
            self.checker_callback_tag = gobject.child_watch_add(
 
742
                self.checker.pid, self.checker_callback, data=command)
 
743
            # The checker may have completed before the gobject
 
744
            # watch was added.  Check for this.
 
745
            try:
 
746
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
747
            except OSError as error:
 
748
                if error.errno == errno.ECHILD:
 
749
                    # This should never happen
 
750
                    logger.error("Child process vanished",
 
751
                                 exc_info=error)
 
752
                    return True
 
753
                raise
 
754
            if pid:
 
755
                gobject.source_remove(self.checker_callback_tag)
 
756
                self.checker_callback(pid, status, command)
753
757
        # Re-run this periodically if run by gobject.timeout_add
754
758
        return True
755
759
    
761
765
        if getattr(self, "checker", None) is None:
762
766
            return
763
767
        logger.debug("Stopping checker for %(name)s", vars(self))
764
 
        self.checker.terminate()
 
768
        try:
 
769
            self.checker.terminate()
 
770
            #time.sleep(0.5)
 
771
            #if self.checker.poll() is None:
 
772
            #    self.checker.kill()
 
773
        except OSError as error:
 
774
            if error.errno != errno.ESRCH: # No such process
 
775
                raise
765
776
        self.checker = None
766
777
 
767
778
 
1099
1110
                interface_names.add(alt_interface)
1100
1111
                # Is this a D-Bus signal?
1101
1112
                if getattr(attribute, "_dbus_is_signal", False):
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
 
1113
                    # Extract the original non-method undecorated
 
1114
                    # function by black magic
 
1115
                    nonmethod_func = (dict(
 
1116
                        zip(attribute.func_code.co_freevars,
 
1117
                            attribute.__closure__))
 
1118
                                      ["func"].cell_contents)
1111
1119
                    # Create a new, but exactly alike, function
1112
1120
                    # object, and decorate it to be a new D-Bus signal
1113
1121
                    # 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__)
1128
1122
                    new_function = (dbus.service.signal(
1129
 
                        alt_interface,
1130
 
                        attribute._dbus_signature)(new_function))
 
1123
                        alt_interface, attribute._dbus_signature)
 
1124
                                    (types.FunctionType(
 
1125
                                        nonmethod_func.func_code,
 
1126
                                        nonmethod_func.func_globals,
 
1127
                                        nonmethod_func.func_name,
 
1128
                                        nonmethod_func.func_defaults,
 
1129
                                        nonmethod_func.func_closure)))
1131
1130
                    # Copy annotations, if any
1132
1131
                    try:
1133
1132
                        new_function._dbus_annotations = dict(
1356
1355
            DBusObjectWithProperties.__del__(self, *args, **kwargs)
1357
1356
        Client.__del__(self, *args, **kwargs)
1358
1357
    
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:
 
1358
    def checker_callback(self, pid, condition, command,
 
1359
                         *args, **kwargs):
 
1360
        self.checker_callback_tag = None
 
1361
        self.checker = None
 
1362
        if os.WIFEXITED(condition):
 
1363
            exitstatus = os.WEXITSTATUS(condition)
1366
1364
            # Emit D-Bus signal
1367
1365
            self.CheckerCompleted(dbus.Int16(exitstatus),
1368
 
                                  dbus.Int64(0),
 
1366
                                  dbus.Int64(condition),
1369
1367
                                  dbus.String(command))
1370
1368
        else:
1371
1369
            # Emit D-Bus signal
1372
1370
            self.CheckerCompleted(dbus.Int16(-1),
1373
 
                                  dbus.Int64(
1374
 
                                      self.last_checker_signal),
 
1371
                                  dbus.Int64(condition),
1375
1372
                                  dbus.String(command))
1376
 
        return ret
 
1373
        
 
1374
        return Client.checker_callback(self, pid, condition, command,
 
1375
                                       *args, **kwargs)
1377
1376
    
1378
1377
    def start_checker(self, *args, **kwargs):
1379
1378
        old_checker_pid = getattr(self.checker, "pid", None)
1672
1671
        self._pipe = child_pipe
1673
1672
        self._pipe.send(('init', fpr, address))
1674
1673
        if not self._pipe.recv():
1675
 
            raise KeyError(fpr)
 
1674
            raise KeyError()
1676
1675
    
1677
1676
    def __getattribute__(self, name):
1678
1677
        if name == '_pipe':
2141
2140
        
2142
2141
        if command == 'getattr':
2143
2142
            attrname = request[1]
2144
 
            if isinstance(client_object.__getattribute__(attrname),
2145
 
                          collections.Callable):
 
2143
            if callable(client_object.__getattribute__(attrname)):
2146
2144
                parent_pipe.send(('function', ))
2147
2145
            else:
2148
2146
                parent_pipe.send((
2183
2181
    # avoid excessive use of external libraries.
2184
2182
    
2185
2183
    # New type for defining tokens, syntax, and semantics all-in-one
 
2184
    Token = collections.namedtuple("Token",
 
2185
                                   ("regexp", # To match token; if
 
2186
                                              # "value" is not None,
 
2187
                                              # must have a "group"
 
2188
                                              # containing digits
 
2189
                                    "value",  # datetime.timedelta or
 
2190
                                              # None
 
2191
                                    "followers")) # Tokens valid after
 
2192
                                                  # this token
2186
2193
    Token = collections.namedtuple("Token", (
2187
2194
        "regexp",  # To match token; if "value" is not None, must have
2188
2195
                   # a "group" containing digits
2223
2230
    # Define starting values
2224
2231
    value = datetime.timedelta() # Value so far
2225
2232
    found_token = None
2226
 
    followers = frozenset((token_duration, )) # Following valid tokens
 
2233
    followers = frozenset((token_duration,)) # Following valid tokens
2227
2234
    s = duration                # String left to parse
2228
2235
    # Loop until end token is found
2229
2236
    while found_token is not token_end:
2246
2253
                break
2247
2254
        else:
2248
2255
            # No currently valid tokens were found
2249
 
            raise ValueError("Invalid RFC 3339 duration: {!r}"
2250
 
                             .format(duration))
 
2256
            raise ValueError("Invalid RFC 3339 duration")
2251
2257
    # End token found
2252
2258
    return value
2253
2259
 
2504
2510
            pidfilename = "/var/run/mandos.pid"
2505
2511
        pidfile = None
2506
2512
        try:
2507
 
            pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
 
2513
            pidfile = open(pidfilename, "w")
2508
2514
        except IOError as e:
2509
2515
            logger.error("Could not open file %r", pidfilename,
2510
2516
                         exc_info=e)
2569
2575
            old_bus_name = dbus.service.BusName(
2570
2576
                "se.bsnet.fukt.Mandos", bus,
2571
2577
                do_not_queue=True)
2572
 
        except dbus.exceptions.DBusException as e:
 
2578
        except dbus.exceptions.NameExistsException as e:
2573
2579
            logger.error("Disabling D-Bus:", exc_info=e)
2574
2580
            use_dbus = False
2575
2581
            server_settings["use_dbus"] = False
2648
2654
                    pass
2649
2655
            
2650
2656
            # Clients who has passed its expire date can still be
2651
 
            # enabled if its last checker was successful.  A Client
 
2657
            # enabled if its last checker was successful.  Clients
2652
2658
            # whose checker succeeded before we stored its state is
2653
2659
            # assumed to have successfully run all checkers during
2654
2660
            # downtime.
2706
2712
    
2707
2713
    if not foreground:
2708
2714
        if pidfile is not None:
2709
 
            pid = os.getpid()
2710
2715
            try:
2711
2716
                with pidfile:
2712
 
                    print(pid, file=pidfile)
 
2717
                    pid = os.getpid()
 
2718
                    pidfile.write("{}\n".format(pid).encode("utf-8"))
2713
2719
            except IOError:
2714
2720
                logger.error("Could not write to file %r with PID %d",
2715
2721
                             pidfilename, pid)