/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-08-02 09:36:40 UTC
  • Revision ID: teddy@recompile.se-20150802093640-nc0n17rbmqlbaxuf
Add D-Bus annotations on a few properties on the Client object.

The D-Bus property "Secret" on the interface
"se.recompile.Mandos.Client" should have the annotation
"org.freedesktop.DBus.Property.EmitsChangedSignal" set to
"invalidates".  Also, the properties "Created", "Fingerprint", "Name",
and "ObjectPath" should have the same annotation set to "const".

* mandos (ClientDBus.Name_dbus_property): Set annotation
                    "org.freedesktop.DBus.Property.EmitsChangedSignal"
                    to "const".
  (ClientDBus.Fingerprint_dbus_property): - '' -
  (ClientDBus.Created_dbus_property): - '' -
  (ClientDBus.ObjectPath_dbus_property): - '' -
  (ClientDBus.Secret_dbus_property): Set annotation
                    "org.freedesktop.DBus.Property.EmitsChangedSignal"
                    to "invalidates".

Show diffs side-by-side

added added

removed removed

Lines of Context:
395
395
                    logger.error(bad_states[state] + ": %r", error)
396
396
            self.cleanup()
397
397
        elif state == avahi.SERVER_RUNNING:
398
 
            self.add()
 
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)
399
410
        else:
400
411
            if error is None:
401
412
                logger.debug("Unknown state: %r", state)
424
435
            .format(self.name)))
425
436
        return ret
426
437
 
 
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()
427
447
 
428
448
class Client(object):
429
449
    """A representation of a client host served by this server.
456
476
    last_checker_status: integer between 0 and 255 reflecting exit
457
477
                         status of last checker. -1 reflects crashed
458
478
                         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
459
481
    last_enabled: datetime.datetime(); (UTC) or None
460
482
    name:       string; from the config file, used in log messages and
461
483
                        D-Bus identifiers
635
657
        # Also start a new checker *right now*.
636
658
        self.start_checker()
637
659
    
638
 
    def checker_callback(self, pid, condition, command):
 
660
    def checker_callback(self, source, condition, connection,
 
661
                         command):
639
662
        """The checker has completed, so take appropriate actions."""
640
663
        self.checker_callback_tag = None
641
664
        self.checker = None
642
 
        if os.WIFEXITED(condition):
643
 
            self.last_checker_status = os.WEXITSTATUS(condition)
 
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
644
672
            if self.last_checker_status == 0:
645
673
                logger.info("Checker for %(name)s succeeded",
646
674
                            vars(self))
649
677
                logger.info("Checker for %(name)s failed", vars(self))
650
678
        else:
651
679
            self.last_checker_status = -1
 
680
            self.last_checker_signal = -returncode
652
681
            logger.warning("Checker for %(name)s crashed?",
653
682
                           vars(self))
 
683
        return False
654
684
    
655
685
    def checked_ok(self):
656
686
        """Assert that the client has been seen, alive and well."""
657
687
        self.last_checked_ok = datetime.datetime.utcnow()
658
688
        self.last_checker_status = 0
 
689
        self.last_checker_signal = None
659
690
        self.bump_timeout()
660
691
    
661
692
    def bump_timeout(self, timeout=None):
687
718
        # than 'timeout' for the client to be disabled, which is as it
688
719
        # should be.
689
720
        
690
 
        # If a checker exists, make sure it is not a zombie
691
 
        try:
692
 
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
693
 
        except AttributeError:
694
 
            pass
695
 
        except OSError as error:
696
 
            if error.errno != errno.ECHILD:
697
 
                raise
698
 
        else:
699
 
            if pid:
700
 
                logger.warning("Checker was a zombie")
701
 
                gobject.source_remove(self.checker_callback_tag)
702
 
                self.checker_callback(pid, status,
703
 
                                      self.current_checker_command)
 
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
704
725
        # Start a new checker if needed
705
726
        if self.checker is None:
706
727
            # Escape attributes for the shell
715
736
                             exc_info=error)
716
737
                return True     # Try again later
717
738
            self.current_checker_command = command
718
 
            try:
719
 
                logger.info("Starting checker %r for %s", command,
720
 
                            self.name)
721
 
                # We don't need to redirect stdout and stderr, since
722
 
                # in normal mode, that is already done by daemon(),
723
 
                # and in debug mode we don't want to.  (Stdin is
724
 
                # always replaced by /dev/null.)
725
 
                # The exception is when not debugging but nevertheless
726
 
                # running in the foreground; use the previously
727
 
                # created wnull.
728
 
                popen_args = {}
729
 
                if (not self.server_settings["debug"]
730
 
                    and self.server_settings["foreground"]):
731
 
                    popen_args.update({"stdout": wnull,
732
 
                                       "stderr": wnull })
733
 
                self.checker = subprocess.Popen(command,
734
 
                                                close_fds=True,
735
 
                                                shell=True,
736
 
                                                cwd="/",
737
 
                                                **popen_args)
738
 
            except OSError as error:
739
 
                logger.error("Failed to start subprocess",
740
 
                             exc_info=error)
741
 
                return True
742
 
            self.checker_callback_tag = gobject.child_watch_add(
743
 
                self.checker.pid, self.checker_callback, data=command)
744
 
            # The checker may have completed before the gobject
745
 
            # watch was added.  Check for this.
746
 
            try:
747
 
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
748
 
            except OSError as error:
749
 
                if error.errno == errno.ECHILD:
750
 
                    # This should never happen
751
 
                    logger.error("Child process vanished",
752
 
                                 exc_info=error)
753
 
                    return True
754
 
                raise
755
 
            if pid:
756
 
                gobject.source_remove(self.checker_callback_tag)
757
 
                self.checker_callback(pid, status, 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)
758
764
        # Re-run this periodically if run by gobject.timeout_add
759
765
        return True
760
766
    
766
772
        if getattr(self, "checker", None) is None:
767
773
            return
768
774
        logger.debug("Stopping checker for %(name)s", vars(self))
769
 
        try:
770
 
            self.checker.terminate()
771
 
            #time.sleep(0.5)
772
 
            #if self.checker.poll() is None:
773
 
            #    self.checker.kill()
774
 
        except OSError as error:
775
 
            if error.errno != errno.ESRCH: # No such process
776
 
                raise
 
775
        self.checker.terminate()
777
776
        self.checker = None
778
777
 
779
778
 
1111
1110
                interface_names.add(alt_interface)
1112
1111
                # Is this a D-Bus signal?
1113
1112
                if getattr(attribute, "_dbus_is_signal", False):
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)
 
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
1120
1122
                    # Create a new, but exactly alike, function
1121
1123
                    # object, and decorate it to be a new D-Bus signal
1122
1124
                    # 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__)
1123
1139
                    new_function = (dbus.service.signal(
1124
 
                        alt_interface, attribute._dbus_signature)
1125
 
                                    (types.FunctionType(
1126
 
                                        nonmethod_func.func_code,
1127
 
                                        nonmethod_func.func_globals,
1128
 
                                        nonmethod_func.func_name,
1129
 
                                        nonmethod_func.func_defaults,
1130
 
                                        nonmethod_func.func_closure)))
 
1140
                        alt_interface,
 
1141
                        attribute._dbus_signature)(new_function))
1131
1142
                    # Copy annotations, if any
1132
1143
                    try:
1133
1144
                        new_function._dbus_annotations = dict(
1356
1367
            DBusObjectWithProperties.__del__(self, *args, **kwargs)
1357
1368
        Client.__del__(self, *args, **kwargs)
1358
1369
    
1359
 
    def checker_callback(self, pid, condition, command,
1360
 
                         *args, **kwargs):
1361
 
        self.checker_callback_tag = None
1362
 
        self.checker = None
1363
 
        if os.WIFEXITED(condition):
1364
 
            exitstatus = os.WEXITSTATUS(condition)
 
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:
1365
1377
            # Emit D-Bus signal
1366
1378
            self.CheckerCompleted(dbus.Int16(exitstatus),
1367
 
                                  dbus.Int64(condition),
 
1379
                                  dbus.Int64(0),
1368
1380
                                  dbus.String(command))
1369
1381
        else:
1370
1382
            # Emit D-Bus signal
1371
1383
            self.CheckerCompleted(dbus.Int16(-1),
1372
 
                                  dbus.Int64(condition),
 
1384
                                  dbus.Int64(
 
1385
                                      self.last_checker_signal),
1373
1386
                                  dbus.String(command))
1374
 
        
1375
 
        return Client.checker_callback(self, pid, condition, command,
1376
 
                                       *args, **kwargs)
 
1387
        return ret
1377
1388
    
1378
1389
    def start_checker(self, *args, **kwargs):
1379
1390
        old_checker_pid = getattr(self.checker, "pid", None)
1513
1524
        self.approval_duration = datetime.timedelta(0, 0, 0, value)
1514
1525
    
1515
1526
    # Name - property
 
1527
    @dbus_annotations(
 
1528
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
1516
1529
    @dbus_service_property(_interface, signature="s", access="read")
1517
1530
    def Name_dbus_property(self):
1518
1531
        return dbus.String(self.name)
1519
1532
    
1520
1533
    # Fingerprint - property
 
1534
    @dbus_annotations(
 
1535
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
1521
1536
    @dbus_service_property(_interface, signature="s", access="read")
1522
1537
    def Fingerprint_dbus_property(self):
1523
1538
        return dbus.String(self.fingerprint)
1532
1547
        self.host = str(value)
1533
1548
    
1534
1549
    # Created - property
 
1550
    @dbus_annotations(
 
1551
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
1535
1552
    @dbus_service_property(_interface, signature="s", access="read")
1536
1553
    def Created_dbus_property(self):
1537
1554
        return datetime_to_dbus(self.created)
1652
1669
            self.stop_checker()
1653
1670
    
1654
1671
    # ObjectPath - property
 
1672
    @dbus_annotations(
 
1673
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
1655
1674
    @dbus_service_property(_interface, signature="o", access="read")
1656
1675
    def ObjectPath_dbus_property(self):
1657
1676
        return self.dbus_object_path # is already a dbus.ObjectPath
1658
1677
    
1659
1678
    # Secret = property
 
1679
    @dbus_annotations(
 
1680
        {"org.freedesktop.DBus.Property.EmitsChangedSignal":
 
1681
         "invalidates"})
1660
1682
    @dbus_service_property(_interface,
1661
1683
                           signature="ay",
1662
1684
                           access="write",
2141
2163
        
2142
2164
        if command == 'getattr':
2143
2165
            attrname = request[1]
2144
 
            if callable(client_object.__getattribute__(attrname)):
 
2166
            if isinstance(client_object.__getattribute__(attrname),
 
2167
                          collections.Callable):
2145
2168
                parent_pipe.send(('function', ))
2146
2169
            else:
2147
2170
                parent_pipe.send((
2182
2205
    # avoid excessive use of external libraries.
2183
2206
    
2184
2207
    # New type for defining tokens, syntax, and semantics all-in-one
2185
 
    Token = collections.namedtuple("Token",
2186
 
                                   ("regexp", # To match token; if
2187
 
                                              # "value" is not None,
2188
 
                                              # must have a "group"
2189
 
                                              # containing digits
2190
 
                                    "value",  # datetime.timedelta or
2191
 
                                              # None
2192
 
                                    "followers")) # Tokens valid after
2193
 
                                                  # this token
2194
2208
    Token = collections.namedtuple("Token", (
2195
2209
        "regexp",  # To match token; if "value" is not None, must have
2196
2210
                   # a "group" containing digits
2231
2245
    # Define starting values
2232
2246
    value = datetime.timedelta() # Value so far
2233
2247
    found_token = None
2234
 
    followers = frozenset((token_duration,)) # Following valid tokens
 
2248
    followers = frozenset((token_duration, )) # Following valid tokens
2235
2249
    s = duration                # String left to parse
2236
2250
    # Loop until end token is found
2237
2251
    while found_token is not token_end:
2394
2408
                        "debug": "False",
2395
2409
                        "priority":
2396
2410
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2397
 
                        ":+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
 
2411
                        ":+SIGN-DSA-SHA256",
2398
2412
                        "servicename": "Mandos",
2399
2413
                        "use_dbus": "True",
2400
2414
                        "use_ipv6": "True",
2656
2670
                    pass
2657
2671
            
2658
2672
            # Clients who has passed its expire date can still be
2659
 
            # enabled if its last checker was successful.  Clients
 
2673
            # enabled if its last checker was successful.  A Client
2660
2674
            # whose checker succeeded before we stored its state is
2661
2675
            # assumed to have successfully run all checkers during
2662
2676
            # downtime.