/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: 2012-05-06 16:13:00 UTC
  • Revision ID: teddy@recompile.se-20120506161300-43rls2rr4qub3zhw
* mandos: Use a class decorator instead of a metaclass to provide
          alternate D-Bus interface names on D-Bus object attributes.
  (alternate_dbus_interfaces): New class decorator.
  (AlternateDBusNamesMetaclass, ClientDBusTransitional,
   MandosDBusServiceTransitional): Removed; all users changed.
  (ClientDbus, MandosDBusService): Use new "alternate_dbus_interfaces"
                                   class decorator.

Show diffs side-by-side

added added

removed removed

Lines of Context:
65
65
import types
66
66
import binascii
67
67
import tempfile
 
68
import itertools
68
69
 
69
70
import dbus
70
71
import dbus.service
85
86
    except ImportError:
86
87
        SO_BINDTODEVICE = None
87
88
 
88
 
version = "1.4.1"
 
89
version = "1.5.3"
89
90
stored_state_file = "clients.pickle"
90
91
 
91
92
logger = logging.getLogger()
175
176
    
176
177
    def encrypt(self, data, password):
177
178
        self.gnupg.passphrase = self.password_encode(password)
178
 
        with open(os.devnull) as devnull:
 
179
        with open(os.devnull, "w") as devnull:
179
180
            try:
180
181
                proc = self.gnupg.run(['--symmetric'],
181
182
                                      create_fhs=['stdin', 'stdout'],
192
193
    
193
194
    def decrypt(self, data, password):
194
195
        self.gnupg.passphrase = self.password_encode(password)
195
 
        with open(os.devnull) as devnull:
 
196
        with open(os.devnull, "w") as devnull:
196
197
            try:
197
198
                proc = self.gnupg.run(['--decrypt'],
198
199
                                      create_fhs=['stdin', 'stdout'],
199
200
                                      attach_fhs={'stderr': devnull})
200
 
                with contextlib.closing(proc.handles['stdin'] ) as f:
 
201
                with contextlib.closing(proc.handles['stdin']) as f:
201
202
                    f.write(data)
202
203
                with contextlib.closing(proc.handles['stdout']) as f:
203
204
                    decrypted_plaintext = f.read()
208
209
        return decrypted_plaintext
209
210
 
210
211
 
211
 
 
212
212
class AvahiError(Exception):
213
213
    def __init__(self, value, *args, **kwargs):
214
214
        self.value = value
243
243
    server: D-Bus Server
244
244
    bus: dbus.SystemBus()
245
245
    """
 
246
    
246
247
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
247
248
                 servicetype = None, port = None, TXT = None,
248
249
                 domain = "", host = "", max_renames = 32768,
261
262
        self.server = None
262
263
        self.bus = bus
263
264
        self.entry_group_state_changed_match = None
 
265
    
264
266
    def rename(self):
265
267
        """Derived from the Avahi example code"""
266
268
        if self.rename_count >= self.max_renames:
276
278
        try:
277
279
            self.add()
278
280
        except dbus.exceptions.DBusException as error:
279
 
            logger.critical("DBusException: %s", error)
 
281
            logger.critical("D-Bus Exception", exc_info=error)
280
282
            self.cleanup()
281
283
            os._exit(1)
282
284
        self.rename_count += 1
 
285
    
283
286
    def remove(self):
284
287
        """Derived from the Avahi example code"""
285
288
        if self.entry_group_state_changed_match is not None:
287
290
            self.entry_group_state_changed_match = None
288
291
        if self.group is not None:
289
292
            self.group.Reset()
 
293
    
290
294
    def add(self):
291
295
        """Derived from the Avahi example code"""
292
296
        self.remove()
309
313
            dbus.UInt16(self.port),
310
314
            avahi.string_array_to_txt_array(self.TXT))
311
315
        self.group.Commit()
 
316
    
312
317
    def entry_group_state_changed(self, state, error):
313
318
        """Derived from the Avahi example code"""
314
319
        logger.debug("Avahi entry group state change: %i", state)
321
326
        elif state == avahi.ENTRY_GROUP_FAILURE:
322
327
            logger.critical("Avahi: Error in group state changed %s",
323
328
                            unicode(error))
324
 
            raise AvahiGroupError("State changed: %s"
325
 
                                  % unicode(error))
 
329
            raise AvahiGroupError("State changed: {0!s}"
 
330
                                  .format(error))
 
331
    
326
332
    def cleanup(self):
327
333
        """Derived from the Avahi example code"""
328
334
        if self.group is not None:
333
339
                pass
334
340
            self.group = None
335
341
        self.remove()
 
342
    
336
343
    def server_state_changed(self, state, error=None):
337
344
        """Derived from the Avahi example code"""
338
345
        logger.debug("Avahi server state change: %i", state)
357
364
                logger.debug("Unknown state: %r", state)
358
365
            else:
359
366
                logger.debug("Unknown state: %r: %r", state, error)
 
367
    
360
368
    def activate(self):
361
369
        """Derived from the Avahi example code"""
362
370
        if self.server is None:
374
382
        """Add the new name to the syslog messages"""
375
383
        ret = AvahiService.rename(self)
376
384
        syslogger.setFormatter(logging.Formatter
377
 
                               ('Mandos (%s) [%%(process)d]:'
378
 
                                ' %%(levelname)s: %%(message)s'
379
 
                                % self.name))
 
385
                               ('Mandos ({0}) [%(process)d]:'
 
386
                                ' %(levelname)s: %(message)s'
 
387
                                .format(self.name)))
380
388
        return ret
381
389
 
382
390
def timedelta_to_milliseconds(td):
384
392
    return ((td.days * 24 * 60 * 60 * 1000)
385
393
            + (td.seconds * 1000)
386
394
            + (td.microseconds // 1000))
387
 
        
 
395
 
388
396
class Client(object):
389
397
    """A representation of a client host served by this server.
390
398
    
415
423
    last_checked_ok: datetime.datetime(); (UTC) or None
416
424
    last_checker_status: integer between 0 and 255 reflecting exit
417
425
                         status of last checker. -1 reflects crashed
418
 
                         checker, or None.
 
426
                         checker, -2 means no checker completed yet.
419
427
    last_enabled: datetime.datetime(); (UTC) or None
420
428
    name:       string; from the config file, used in log messages and
421
429
                        D-Bus identifiers
422
430
    secret:     bytestring; sent verbatim (over TLS) to client
423
431
    timeout:    datetime.timedelta(); How long from last_checked_ok
424
432
                                      until this client is disabled
425
 
    extended_timeout:   extra long timeout when password has been sent
 
433
    extended_timeout:   extra long timeout when secret has been sent
426
434
    runtime_expansions: Allowed attributes for runtime expansion.
427
435
    expires:    datetime.datetime(); time (UTC) when a client will be
428
436
                disabled, or None
457
465
    
458
466
    def approval_delay_milliseconds(self):
459
467
        return timedelta_to_milliseconds(self.approval_delay)
460
 
 
 
468
    
461
469
    @staticmethod
462
470
    def config_parser(config):
463
471
        """Construct a new dict of client settings of this form:
488
496
                          "rb") as secfile:
489
497
                    client["secret"] = secfile.read()
490
498
            else:
491
 
                raise TypeError("No secret or secfile for section %s"
492
 
                                % section)
 
499
                raise TypeError("No secret or secfile for section {0}"
 
500
                                .format(section))
493
501
            client["timeout"] = string_to_delta(section["timeout"])
494
502
            client["extended_timeout"] = string_to_delta(
495
503
                section["extended_timeout"])
501
509
            client["checker_command"] = section["checker"]
502
510
            client["last_approval_request"] = None
503
511
            client["last_checked_ok"] = None
504
 
            client["last_checker_status"] = None
 
512
            client["last_checker_status"] = -2
505
513
        
506
514
        return settings
507
 
        
508
 
        
 
515
    
509
516
    def __init__(self, settings, name = None):
510
 
        """Note: the 'checker' key in 'config' sets the
511
 
        'checker_command' attribute and *not* the 'checker'
512
 
        attribute."""
513
517
        self.name = name
514
518
        # adding all client settings
515
519
        for setting, value in settings.iteritems():
524
528
        else:
525
529
            self.last_enabled = None
526
530
            self.expires = None
527
 
       
 
531
        
528
532
        logger.debug("Creating client %r", self.name)
529
533
        # Uppercase and remove spaces from fingerprint for later
530
534
        # comparison purposes with return value from the fingerprint()
532
536
        logger.debug("  Fingerprint: %s", self.fingerprint)
533
537
        self.created = settings.get("created",
534
538
                                    datetime.datetime.utcnow())
535
 
 
 
539
        
536
540
        # attributes specific for this server instance
537
541
        self.checker = None
538
542
        self.checker_initiator_tag = None
626
630
            logger.warning("Checker for %(name)s crashed?",
627
631
                           vars(self))
628
632
    
629
 
    def checked_ok(self, timeout=None):
630
 
        """Bump up the timeout for this client.
631
 
        
632
 
        This should only be called when the client has been seen,
633
 
        alive and well.
634
 
        """
 
633
    def checked_ok(self):
 
634
        """Assert that the client has been seen, alive and well."""
 
635
        self.last_checked_ok = datetime.datetime.utcnow()
 
636
        self.last_checker_status = 0
 
637
        self.bump_timeout()
 
638
    
 
639
    def bump_timeout(self, timeout=None):
 
640
        """Bump up the timeout for this client."""
635
641
        if timeout is None:
636
642
            timeout = self.timeout
637
 
        self.last_checked_ok = datetime.datetime.utcnow()
638
643
        if self.disable_initiator_tag is not None:
639
644
            gobject.source_remove(self.disable_initiator_tag)
640
645
        if getattr(self, "enabled", False):
691
696
                try:
692
697
                    command = self.checker_command % escaped_attrs
693
698
                except TypeError as error:
694
 
                    logger.error('Could not format string "%s":'
695
 
                                 ' %s', self.checker_command, error)
 
699
                    logger.error('Could not format string "%s"',
 
700
                                 self.checker_command, exc_info=error)
696
701
                    return True # Try again later
697
702
            self.current_checker_command = command
698
703
            try:
716
721
                    gobject.source_remove(self.checker_callback_tag)
717
722
                    self.checker_callback(pid, status, command)
718
723
            except OSError as error:
719
 
                logger.error("Failed to start subprocess: %s",
720
 
                             error)
 
724
                logger.error("Failed to start subprocess",
 
725
                             exc_info=error)
721
726
        # Re-run this periodically if run by gobject.timeout_add
722
727
        return True
723
728
    
730
735
            return
731
736
        logger.debug("Stopping checker for %(name)s", vars(self))
732
737
        try:
733
 
            os.kill(self.checker.pid, signal.SIGTERM)
 
738
            self.checker.terminate()
734
739
            #time.sleep(0.5)
735
740
            #if self.checker.poll() is None:
736
 
            #    os.kill(self.checker.pid, signal.SIGKILL)
 
741
            #    self.checker.kill()
737
742
        except OSError as error:
738
743
            if error.errno != errno.ESRCH: # No such process
739
744
                raise
756
761
    # "Set" method, so we fail early here:
757
762
    if byte_arrays and signature != "ay":
758
763
        raise ValueError("Byte arrays not supported for non-'ay'"
759
 
                         " signature %r" % signature)
 
764
                         " signature {0!r}".format(signature))
760
765
    def decorator(func):
761
766
        func._dbus_is_property = True
762
767
        func._dbus_interface = dbus_interface
770
775
    return decorator
771
776
 
772
777
 
 
778
def dbus_interface_annotations(dbus_interface):
 
779
    """Decorator for marking functions returning interface annotations
 
780
    
 
781
    Usage:
 
782
    
 
783
    @dbus_interface_annotations("org.example.Interface")
 
784
    def _foo(self):  # Function name does not matter
 
785
        return {"org.freedesktop.DBus.Deprecated": "true",
 
786
                "org.freedesktop.DBus.Property.EmitsChangedSignal":
 
787
                    "false"}
 
788
    """
 
789
    def decorator(func):
 
790
        func._dbus_is_interface = True
 
791
        func._dbus_interface = dbus_interface
 
792
        func._dbus_name = dbus_interface
 
793
        return func
 
794
    return decorator
 
795
 
 
796
 
 
797
def dbus_annotations(annotations):
 
798
    """Decorator to annotate D-Bus methods, signals or properties
 
799
    Usage:
 
800
    
 
801
    @dbus_service_property("org.example.Interface", signature="b",
 
802
                           access="r")
 
803
    @dbus_annotations({{"org.freedesktop.DBus.Deprecated": "true",
 
804
                        "org.freedesktop.DBus.Property."
 
805
                        "EmitsChangedSignal": "false"})
 
806
    def Property_dbus_property(self):
 
807
        return dbus.Boolean(False)
 
808
    """
 
809
    def decorator(func):
 
810
        func._dbus_annotations = annotations
 
811
        return func
 
812
    return decorator
 
813
 
 
814
 
773
815
class DBusPropertyException(dbus.exceptions.DBusException):
774
816
    """A base class for D-Bus property-related exceptions
775
817
    """
798
840
    """
799
841
    
800
842
    @staticmethod
801
 
    def _is_dbus_property(obj):
802
 
        return getattr(obj, "_dbus_is_property", False)
 
843
    def _is_dbus_thing(thing):
 
844
        """Returns a function testing if an attribute is a D-Bus thing
 
845
        
 
846
        If called like _is_dbus_thing("method") it returns a function
 
847
        suitable for use as predicate to inspect.getmembers().
 
848
        """
 
849
        return lambda obj: getattr(obj, "_dbus_is_{0}".format(thing),
 
850
                                   False)
803
851
    
804
 
    def _get_all_dbus_properties(self):
 
852
    def _get_all_dbus_things(self, thing):
805
853
        """Returns a generator of (name, attribute) pairs
806
854
        """
807
 
        return ((prop.__get__(self)._dbus_name, prop.__get__(self))
 
855
        return ((getattr(athing.__get__(self), "_dbus_name",
 
856
                         name),
 
857
                 athing.__get__(self))
808
858
                for cls in self.__class__.__mro__
809
 
                for name, prop in
810
 
                inspect.getmembers(cls, self._is_dbus_property))
 
859
                for name, athing in
 
860
                inspect.getmembers(cls,
 
861
                                   self._is_dbus_thing(thing)))
811
862
    
812
863
    def _get_dbus_property(self, interface_name, property_name):
813
864
        """Returns a bound method if one exists which is a D-Bus
815
866
        """
816
867
        for cls in  self.__class__.__mro__:
817
868
            for name, value in (inspect.getmembers
818
 
                                (cls, self._is_dbus_property)):
 
869
                                (cls,
 
870
                                 self._is_dbus_thing("property"))):
819
871
                if (value._dbus_name == property_name
820
872
                    and value._dbus_interface == interface_name):
821
873
                    return value.__get__(self)
850
902
            # signatures other than "ay".
851
903
            if prop._dbus_signature != "ay":
852
904
                raise ValueError
853
 
            value = dbus.ByteArray(''.join(unichr(byte)
854
 
                                           for byte in value))
 
905
            value = dbus.ByteArray(b''.join(chr(byte)
 
906
                                            for byte in value))
855
907
        prop(value)
856
908
    
857
909
    @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s",
863
915
        Note: Will not include properties with access="write".
864
916
        """
865
917
        properties = {}
866
 
        for name, prop in self._get_all_dbus_properties():
 
918
        for name, prop in self._get_all_dbus_things("property"):
867
919
            if (interface_name
868
920
                and interface_name != prop._dbus_interface):
869
921
                # Interface non-empty but did not match
884
936
                         path_keyword='object_path',
885
937
                         connection_keyword='connection')
886
938
    def Introspect(self, object_path, connection):
887
 
        """Standard D-Bus method, overloaded to insert property tags.
 
939
        """Overloading of standard D-Bus method.
 
940
        
 
941
        Inserts property tags and interface annotation tags.
888
942
        """
889
943
        xmlstring = dbus.service.Object.Introspect(self, object_path,
890
944
                                                   connection)
897
951
                e.setAttribute("access", prop._dbus_access)
898
952
                return e
899
953
            for if_tag in document.getElementsByTagName("interface"):
 
954
                # Add property tags
900
955
                for tag in (make_tag(document, name, prop)
901
956
                            for name, prop
902
 
                            in self._get_all_dbus_properties()
 
957
                            in self._get_all_dbus_things("property")
903
958
                            if prop._dbus_interface
904
959
                            == if_tag.getAttribute("name")):
905
960
                    if_tag.appendChild(tag)
 
961
                # Add annotation tags
 
962
                for typ in ("method", "signal", "property"):
 
963
                    for tag in if_tag.getElementsByTagName(typ):
 
964
                        annots = dict()
 
965
                        for name, prop in (self.
 
966
                                           _get_all_dbus_things(typ)):
 
967
                            if (name == tag.getAttribute("name")
 
968
                                and prop._dbus_interface
 
969
                                == if_tag.getAttribute("name")):
 
970
                                annots.update(getattr
 
971
                                              (prop,
 
972
                                               "_dbus_annotations",
 
973
                                               {}))
 
974
                        for name, value in annots.iteritems():
 
975
                            ann_tag = document.createElement(
 
976
                                "annotation")
 
977
                            ann_tag.setAttribute("name", name)
 
978
                            ann_tag.setAttribute("value", value)
 
979
                            tag.appendChild(ann_tag)
 
980
                # Add interface annotation tags
 
981
                for annotation, value in dict(
 
982
                    itertools.chain(
 
983
                        *(annotations().iteritems()
 
984
                          for name, annotations in
 
985
                          self._get_all_dbus_things("interface")
 
986
                          if name == if_tag.getAttribute("name")
 
987
                          ))).iteritems():
 
988
                    ann_tag = document.createElement("annotation")
 
989
                    ann_tag.setAttribute("name", annotation)
 
990
                    ann_tag.setAttribute("value", value)
 
991
                    if_tag.appendChild(ann_tag)
906
992
                # Add the names to the return values for the
907
993
                # "org.freedesktop.DBus.Properties" methods
908
994
                if (if_tag.getAttribute("name")
923
1009
        except (AttributeError, xml.dom.DOMException,
924
1010
                xml.parsers.expat.ExpatError) as error:
925
1011
            logger.error("Failed to override Introspection method",
926
 
                         error)
 
1012
                         exc_info=error)
927
1013
        return xmlstring
928
1014
 
929
1015
 
935
1021
                       variant_level=variant_level)
936
1022
 
937
1023
 
938
 
class AlternateDBusNamesMetaclass(DBusObjectWithProperties
939
 
                                  .__metaclass__):
940
 
    """Applied to an empty subclass of a D-Bus object, this metaclass
941
 
    will add additional D-Bus attributes matching a certain pattern.
 
1024
def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
 
1025
    """A class decorator; applied to a subclass of
 
1026
    dbus.service.Object, it will add alternate D-Bus attributes with
 
1027
    interface names according to the "alt_interface_names" mapping.
 
1028
    Usage:
 
1029
    
 
1030
    @alternate_dbus_names({"org.example.Interface":
 
1031
                               "net.example.AlternateInterface"})
 
1032
    class SampleDBusObject(dbus.service.Object):
 
1033
        @dbus.service.method("org.example.Interface")
 
1034
        def SampleDBusMethod():
 
1035
            pass
 
1036
    
 
1037
    The above "SampleDBusMethod" on "SampleDBusObject" will be
 
1038
    reachable via two interfaces: "org.example.Interface" and
 
1039
    "net.example.AlternateInterface", the latter of which will have
 
1040
    its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
 
1041
    "true", unless "deprecate" is passed with a False value.
 
1042
    
 
1043
    This works for methods and signals, and also for D-Bus properties
 
1044
    (from DBusObjectWithProperties) and interfaces (from the
 
1045
    dbus_interface_annotations decorator).
942
1046
    """
943
 
    def __new__(mcs, name, bases, attr):
944
 
        # Go through all the base classes which could have D-Bus
945
 
        # methods, signals, or properties in them
946
 
        for base in (b for b in bases
947
 
                     if issubclass(b, dbus.service.Object)):
948
 
            # Go though all attributes of the base class
949
 
            for attrname, attribute in inspect.getmembers(base):
 
1047
    def wrapper(cls):
 
1048
        for orig_interface_name, alt_interface_name in (
 
1049
            alt_interface_names.iteritems()):
 
1050
            attr = {}
 
1051
            interface_names = set()
 
1052
            # Go though all attributes of the class
 
1053
            for attrname, attribute in inspect.getmembers(cls):
950
1054
                # Ignore non-D-Bus attributes, and D-Bus attributes
951
1055
                # with the wrong interface name
952
1056
                if (not hasattr(attribute, "_dbus_interface")
953
1057
                    or not attribute._dbus_interface
954
 
                    .startswith("se.recompile.Mandos")):
 
1058
                    .startswith(orig_interface_name)):
955
1059
                    continue
956
1060
                # Create an alternate D-Bus interface name based on
957
1061
                # the current name
958
1062
                alt_interface = (attribute._dbus_interface
959
 
                                 .replace("se.recompile.Mandos",
960
 
                                          "se.bsnet.fukt.Mandos"))
 
1063
                                 .replace(orig_interface_name,
 
1064
                                          alt_interface_name))
 
1065
                interface_names.add(alt_interface)
961
1066
                # Is this a D-Bus signal?
962
1067
                if getattr(attribute, "_dbus_is_signal", False):
963
1068
                    # Extract the original non-method function by
978
1083
                                nonmethod_func.func_name,
979
1084
                                nonmethod_func.func_defaults,
980
1085
                                nonmethod_func.func_closure)))
 
1086
                    # Copy annotations, if any
 
1087
                    try:
 
1088
                        new_function._dbus_annotations = (
 
1089
                            dict(attribute._dbus_annotations))
 
1090
                    except AttributeError:
 
1091
                        pass
981
1092
                    # Define a creator of a function to call both the
982
 
                    # old and new functions, so both the old and new
983
 
                    # signals gets sent when the function is called
 
1093
                    # original and alternate functions, so both the
 
1094
                    # original and alternate signals gets sent when
 
1095
                    # the function is called
984
1096
                    def fixscope(func1, func2):
985
1097
                        """This function is a scope container to pass
986
1098
                        func1 and func2 to the "call_both" function
993
1105
                        return call_both
994
1106
                    # Create the "call_both" function and add it to
995
1107
                    # the class
996
 
                    attr[attrname] = fixscope(attribute,
997
 
                                              new_function)
 
1108
                    attr[attrname] = fixscope(attribute, new_function)
998
1109
                # Is this a D-Bus method?
999
1110
                elif getattr(attribute, "_dbus_is_method", False):
1000
1111
                    # Create a new, but exactly alike, function
1011
1122
                                        attribute.func_name,
1012
1123
                                        attribute.func_defaults,
1013
1124
                                        attribute.func_closure)))
 
1125
                    # Copy annotations, if any
 
1126
                    try:
 
1127
                        attr[attrname]._dbus_annotations = (
 
1128
                            dict(attribute._dbus_annotations))
 
1129
                    except AttributeError:
 
1130
                        pass
1014
1131
                # Is this a D-Bus property?
1015
1132
                elif getattr(attribute, "_dbus_is_property", False):
1016
1133
                    # Create a new, but exactly alike, function
1030
1147
                                        attribute.func_name,
1031
1148
                                        attribute.func_defaults,
1032
1149
                                        attribute.func_closure)))
1033
 
        return type.__new__(mcs, name, bases, attr)
1034
 
 
1035
 
 
 
1150
                    # Copy annotations, if any
 
1151
                    try:
 
1152
                        attr[attrname]._dbus_annotations = (
 
1153
                            dict(attribute._dbus_annotations))
 
1154
                    except AttributeError:
 
1155
                        pass
 
1156
                # Is this a D-Bus interface?
 
1157
                elif getattr(attribute, "_dbus_is_interface", False):
 
1158
                    # Create a new, but exactly alike, function
 
1159
                    # object.  Decorate it to be a new D-Bus interface
 
1160
                    # with the alternate D-Bus interface name.  Add it
 
1161
                    # to the class.
 
1162
                    attr[attrname] = (dbus_interface_annotations
 
1163
                                      (alt_interface)
 
1164
                                      (types.FunctionType
 
1165
                                       (attribute.func_code,
 
1166
                                        attribute.func_globals,
 
1167
                                        attribute.func_name,
 
1168
                                        attribute.func_defaults,
 
1169
                                        attribute.func_closure)))
 
1170
            if deprecate:
 
1171
                # Deprecate all alternate interfaces
 
1172
                iname="_AlternateDBusNames_interface_annotation{0}"
 
1173
                for interface_name in interface_names:
 
1174
                    @dbus_interface_annotations(interface_name)
 
1175
                    def func(self):
 
1176
                        return { "org.freedesktop.DBus.Deprecated":
 
1177
                                     "true" }
 
1178
                    # Find an unused name
 
1179
                    for aname in (iname.format(i)
 
1180
                                  for i in itertools.count()):
 
1181
                        if aname not in attr:
 
1182
                            attr[aname] = func
 
1183
                            break
 
1184
            if interface_names:
 
1185
                # Replace the class with a new subclass of it with
 
1186
                # methods, signals, etc. as created above.
 
1187
                cls = type(b"{0}Alternate".format(cls.__name__),
 
1188
                           (cls,), attr)
 
1189
        return cls
 
1190
    return wrapper
 
1191
 
 
1192
 
 
1193
@alternate_dbus_interfaces({"se.recompile.Mandos":
 
1194
                                "se.bsnet.fukt.Mandos"})
1036
1195
class ClientDBus(Client, DBusObjectWithProperties):
1037
1196
    """A Client class using D-Bus
1038
1197
    
1049
1208
    def __init__(self, bus = None, *args, **kwargs):
1050
1209
        self.bus = bus
1051
1210
        Client.__init__(self, *args, **kwargs)
1052
 
        self._approvals_pending = 0
1053
 
        
1054
 
        self._approvals_pending = 0
1055
1211
        # Only now, when this client is initialized, can it show up on
1056
1212
        # the D-Bus
1057
1213
        client_object_name = unicode(self.name).translate(
1061
1217
                                 ("/clients/" + client_object_name))
1062
1218
        DBusObjectWithProperties.__init__(self, self.bus,
1063
1219
                                          self.dbus_object_path)
1064
 
        
 
1220
    
1065
1221
    def notifychangeproperty(transform_func,
1066
1222
                             dbus_name, type_func=lambda x: x,
1067
1223
                             variant_level=1):
1090
1246
        
1091
1247
        return property(lambda self: getattr(self, attrname), setter)
1092
1248
    
1093
 
    
1094
1249
    expires = notifychangeproperty(datetime_to_dbus, "Expires")
1095
1250
    approvals_pending = notifychangeproperty(dbus.Boolean,
1096
1251
                                             "ApprovalPending",
1103
1258
                                       checker is not None)
1104
1259
    last_checked_ok = notifychangeproperty(datetime_to_dbus,
1105
1260
                                           "LastCheckedOK")
 
1261
    last_checker_status = notifychangeproperty(dbus.Int16,
 
1262
                                               "LastCheckerStatus")
1106
1263
    last_approval_request = notifychangeproperty(
1107
1264
        datetime_to_dbus, "LastApprovalRequest")
1108
1265
    approved_by_default = notifychangeproperty(dbus.Boolean,
1182
1339
                            (self.approval_duration),
1183
1340
                            self._reset_approved)
1184
1341
    
1185
 
    
1186
1342
    ## D-Bus methods, signals & properties
1187
1343
    _interface = "se.recompile.Mandos.Client"
1188
1344
    
 
1345
    ## Interfaces
 
1346
    
 
1347
    @dbus_interface_annotations(_interface)
 
1348
    def _foo(self):
 
1349
        return { "org.freedesktop.DBus.Property.EmitsChangedSignal":
 
1350
                     "false"}
 
1351
    
1189
1352
    ## Signals
1190
1353
    
1191
1354
    # CheckerCompleted - signal
1227
1390
        "D-Bus signal"
1228
1391
        return self.need_approval()
1229
1392
    
1230
 
    # NeRwequest - signal
1231
 
    @dbus.service.signal(_interface, signature="s")
1232
 
    def NewRequest(self, ip):
1233
 
        """D-Bus signal
1234
 
        Is sent after a client request a password.
1235
 
        """
1236
 
        pass
1237
 
    
1238
1393
    ## Methods
1239
1394
    
1240
1395
    # Approve - method
1350
1505
            return
1351
1506
        return datetime_to_dbus(self.last_checked_ok)
1352
1507
    
 
1508
    # LastCheckerStatus - property
 
1509
    @dbus_service_property(_interface, signature="n",
 
1510
                           access="read")
 
1511
    def LastCheckerStatus_dbus_property(self):
 
1512
        return dbus.Int16(self.last_checker_status)
 
1513
    
1353
1514
    # Expires - property
1354
1515
    @dbus_service_property(_interface, signature="s", access="read")
1355
1516
    def Expires_dbus_property(self):
1470
1631
        self._pipe.send(('setattr', name, value))
1471
1632
 
1472
1633
 
1473
 
class ClientDBusTransitional(ClientDBus):
1474
 
    __metaclass__ = AlternateDBusNamesMetaclass
1475
 
 
1476
 
 
1477
1634
class ClientHandler(socketserver.BaseRequestHandler, object):
1478
1635
    """A class to handle client connections.
1479
1636
    
1547
1704
                except KeyError:
1548
1705
                    return
1549
1706
                
1550
 
                if self.server.use_dbus:
1551
 
                    # Emit D-Bus signal
1552
 
                    client.NewRequest(str(self.client_address))
1553
 
                
1554
1707
                if client.approval_delay:
1555
1708
                    delay = client.approval_delay
1556
1709
                    client.approvals_pending += 1
1611
1764
                    try:
1612
1765
                        sent = session.send(client.secret[sent_size:])
1613
1766
                    except gnutls.errors.GNUTLSError as error:
1614
 
                        logger.warning("gnutls send failed")
 
1767
                        logger.warning("gnutls send failed",
 
1768
                                       exc_info=error)
1615
1769
                        return
1616
1770
                    logger.debug("Sent: %d, remaining: %d",
1617
1771
                                 sent, len(client.secret)
1620
1774
                
1621
1775
                logger.info("Sending secret to %s", client.name)
1622
1776
                # bump the timeout using extended_timeout
1623
 
                client.checked_ok(client.extended_timeout)
 
1777
                client.bump_timeout(client.extended_timeout)
1624
1778
                if self.server.use_dbus:
1625
1779
                    # Emit D-Bus signal
1626
1780
                    client.GotSecret()
1631
1785
                try:
1632
1786
                    session.bye()
1633
1787
                except gnutls.errors.GNUTLSError as error:
1634
 
                    logger.warning("GnuTLS bye failed")
 
1788
                    logger.warning("GnuTLS bye failed",
 
1789
                                   exc_info=error)
1635
1790
    
1636
1791
    @staticmethod
1637
1792
    def peer_certificate(session):
1949
2104
            elif suffix == "w":
1950
2105
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1951
2106
            else:
1952
 
                raise ValueError("Unknown suffix %r" % suffix)
 
2107
                raise ValueError("Unknown suffix {0!r}"
 
2108
                                 .format(suffix))
1953
2109
        except (ValueError, IndexError) as e:
1954
2110
            raise ValueError(*(e.args))
1955
2111
        timevalue += delta
1969
2125
        sys.exit()
1970
2126
    if not noclose:
1971
2127
        # Close all standard open file descriptors
1972
 
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
2128
        null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
1973
2129
        if not stat.S_ISCHR(os.fstat(null).st_mode):
1974
2130
            raise OSError(errno.ENODEV,
1975
 
                          "%s not a character device"
1976
 
                          % os.path.devnull)
 
2131
                          "{0} not a character device"
 
2132
                          .format(os.devnull))
1977
2133
        os.dup2(null, sys.stdin.fileno())
1978
2134
        os.dup2(null, sys.stdout.fileno())
1979
2135
        os.dup2(null, sys.stderr.fileno())
1988
2144
    
1989
2145
    parser = argparse.ArgumentParser()
1990
2146
    parser.add_argument("-v", "--version", action="version",
1991
 
                        version = "%%(prog)s %s" % version,
 
2147
                        version = "%(prog)s {0}".format(version),
1992
2148
                        help="show version number and exit")
1993
2149
    parser.add_argument("-i", "--interface", metavar="IF",
1994
2150
                        help="Bind to interface IF")
2097
2253
    
2098
2254
    if server_settings["servicename"] != "Mandos":
2099
2255
        syslogger.setFormatter(logging.Formatter
2100
 
                               ('Mandos (%s) [%%(process)d]:'
2101
 
                                ' %%(levelname)s: %%(message)s'
2102
 
                                % server_settings["servicename"]))
 
2256
                               ('Mandos ({0}) [%(process)d]:'
 
2257
                                ' %(levelname)s: %(message)s'
 
2258
                                .format(server_settings
 
2259
                                        ["servicename"])))
2103
2260
    
2104
2261
    # Parse config file with clients
2105
2262
    client_config = configparser.SafeConfigParser(Client
2123
2280
        pidfilename = "/var/run/mandos.pid"
2124
2281
        try:
2125
2282
            pidfile = open(pidfilename, "w")
2126
 
        except IOError:
2127
 
            logger.error("Could not open file %r", pidfilename)
 
2283
        except IOError as e:
 
2284
            logger.error("Could not open file %r", pidfilename,
 
2285
                         exc_info=e)
2128
2286
    
2129
 
    try:
2130
 
        uid = pwd.getpwnam("_mandos").pw_uid
2131
 
        gid = pwd.getpwnam("_mandos").pw_gid
2132
 
    except KeyError:
 
2287
    for name in ("_mandos", "mandos", "nobody"):
2133
2288
        try:
2134
 
            uid = pwd.getpwnam("mandos").pw_uid
2135
 
            gid = pwd.getpwnam("mandos").pw_gid
 
2289
            uid = pwd.getpwnam(name).pw_uid
 
2290
            gid = pwd.getpwnam(name).pw_gid
 
2291
            break
2136
2292
        except KeyError:
2137
 
            try:
2138
 
                uid = pwd.getpwnam("nobody").pw_uid
2139
 
                gid = pwd.getpwnam("nobody").pw_gid
2140
 
            except KeyError:
2141
 
                uid = 65534
2142
 
                gid = 65534
 
2293
            continue
 
2294
    else:
 
2295
        uid = 65534
 
2296
        gid = 65534
2143
2297
    try:
2144
2298
        os.setgid(gid)
2145
2299
        os.setuid(uid)
2162
2316
         .gnutls_global_set_log_function(debug_gnutls))
2163
2317
        
2164
2318
        # Redirect stdin so all checkers get /dev/null
2165
 
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
2319
        null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2166
2320
        os.dup2(null, sys.stdin.fileno())
2167
2321
        if null > 2:
2168
2322
            os.close(null)
2176
2330
    
2177
2331
    global main_loop
2178
2332
    # From the Avahi example code
2179
 
    DBusGMainLoop(set_as_default=True )
 
2333
    DBusGMainLoop(set_as_default=True)
2180
2334
    main_loop = gobject.MainLoop()
2181
2335
    bus = dbus.SystemBus()
2182
2336
    # End of Avahi example code
2188
2342
                            ("se.bsnet.fukt.Mandos", bus,
2189
2343
                             do_not_queue=True))
2190
2344
        except dbus.exceptions.NameExistsException as e:
2191
 
            logger.error(unicode(e) + ", disabling D-Bus")
 
2345
            logger.error("Disabling D-Bus:", exc_info=e)
2192
2346
            use_dbus = False
2193
2347
            server_settings["use_dbus"] = False
2194
2348
            tcp_server.use_dbus = False
2206
2360
    
2207
2361
    client_class = Client
2208
2362
    if use_dbus:
2209
 
        client_class = functools.partial(ClientDBusTransitional,
2210
 
                                         bus = bus)
 
2363
        client_class = functools.partial(ClientDBus, bus = bus)
2211
2364
    
2212
2365
    client_settings = Client.config_parser(client_config)
2213
2366
    old_client_settings = {}
2221
2374
                                                     (stored_state))
2222
2375
            os.remove(stored_state_path)
2223
2376
        except IOError as e:
2224
 
            logger.warning("Could not load persistent state: {0}"
2225
 
                           .format(e))
2226
 
            if e.errno != errno.ENOENT:
 
2377
            if e.errno == errno.ENOENT:
 
2378
                logger.warning("Could not load persistent state: {0}"
 
2379
                                .format(os.strerror(e.errno)))
 
2380
            else:
 
2381
                logger.critical("Could not load persistent state:",
 
2382
                                exc_info=e)
2227
2383
                raise
2228
2384
        except EOFError as e:
2229
2385
            logger.warning("Could not load persistent state: "
2230
 
                           "EOFError: {0}".format(e))
 
2386
                           "EOFError:", exc_info=e)
2231
2387
    
2232
2388
    with PGPEngine() as pgp:
2233
2389
        for client_name, client in clients_data.iteritems():
2250
2406
            
2251
2407
            # Clients who has passed its expire date can still be
2252
2408
            # enabled if its last checker was successful.  Clients
2253
 
            # whose checker failed before we stored its state is
2254
 
            # assumed to have failed all checkers during downtime.
 
2409
            # whose checker succeeded before we stored its state is
 
2410
            # assumed to have successfully run all checkers during
 
2411
            # downtime.
2255
2412
            if client["enabled"]:
2256
2413
                if datetime.datetime.utcnow() >= client["expires"]:
2257
2414
                    if not client["last_checked_ok"]:
2258
2415
                        logger.warning(
2259
2416
                            "disabling client {0} - Client never "
2260
 
                            "performed a successfull checker"
2261
 
                            .format(client["name"]))
 
2417
                            "performed a successful checker"
 
2418
                            .format(client_name))
2262
2419
                        client["enabled"] = False
2263
2420
                    elif client["last_checker_status"] != 0:
2264
2421
                        logger.warning(
2265
2422
                            "disabling client {0} - Client "
2266
2423
                            "last checker failed with error code {1}"
2267
 
                            .format(client["name"],
 
2424
                            .format(client_name,
2268
2425
                                    client["last_checker_status"]))
2269
2426
                        client["enabled"] = False
2270
2427
                    else:
2273
2430
                                             + client["timeout"])
2274
2431
                        logger.debug("Last checker succeeded,"
2275
2432
                                     " keeping {0} enabled"
2276
 
                                     .format(client["name"]))
 
2433
                                     .format(client_name))
2277
2434
            try:
2278
2435
                client["secret"] = (
2279
2436
                    pgp.decrypt(client["encrypted_secret"],
2285
2442
                             .format(client_name))
2286
2443
                client["secret"] = (
2287
2444
                    client_settings[client_name]["secret"])
2288
 
 
2289
2445
    
2290
2446
    # Add/remove clients based on new changes made to config
2291
2447
    for client_name in (set(old_client_settings)
2294
2450
    for client_name in (set(client_settings)
2295
2451
                        - set(old_client_settings)):
2296
2452
        clients_data[client_name] = client_settings[client_name]
2297
 
 
2298
 
    # Create clients all clients
 
2453
    
 
2454
    # Create all client objects
2299
2455
    for client_name, client in clients_data.iteritems():
2300
2456
        tcp_server.clients[client_name] = client_class(
2301
2457
            name = client_name, settings = client)
2302
2458
    
2303
2459
    if not tcp_server.clients:
2304
2460
        logger.warning("No clients defined")
2305
 
        
 
2461
    
2306
2462
    if not debug:
2307
2463
        try:
2308
2464
            with pidfile:
2322
2478
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2323
2479
    
2324
2480
    if use_dbus:
2325
 
        class MandosDBusService(dbus.service.Object):
 
2481
        @alternate_dbus_interfaces({"se.recompile.Mandos":
 
2482
                                        "se.bsnet.fukt.Mandos"})
 
2483
        class MandosDBusService(DBusObjectWithProperties):
2326
2484
            """A D-Bus proxy object"""
2327
2485
            def __init__(self):
2328
2486
                dbus.service.Object.__init__(self, bus, "/")
2329
2487
            _interface = "se.recompile.Mandos"
2330
2488
            
 
2489
            @dbus_interface_annotations(_interface)
 
2490
            def _foo(self):
 
2491
                return { "org.freedesktop.DBus.Property"
 
2492
                         ".EmitsChangedSignal":
 
2493
                             "false"}
 
2494
            
2331
2495
            @dbus.service.signal(_interface, signature="o")
2332
2496
            def ClientAdded(self, objpath):
2333
2497
                "D-Bus signal"
2375
2539
            
2376
2540
            del _interface
2377
2541
        
2378
 
        class MandosDBusServiceTransitional(MandosDBusService):
2379
 
            __metaclass__ = AlternateDBusNamesMetaclass
2380
 
        mandos_dbus_service = MandosDBusServiceTransitional()
 
2542
        mandos_dbus_service = MandosDBusService()
2381
2543
    
2382
2544
    def cleanup():
2383
2545
        "Cleanup function; run on exit"
2424
2586
                pickle.dump((clients, client_settings), stored_state)
2425
2587
            os.rename(tempname, stored_state_path)
2426
2588
        except (IOError, OSError) as e:
2427
 
            logger.warning("Could not save persistent state: {0}"
2428
 
                           .format(e))
2429
2589
            if not debug:
2430
2590
                try:
2431
2591
                    os.remove(tempname)
2432
2592
                except NameError:
2433
2593
                    pass
2434
 
            if e.errno not in set((errno.ENOENT, errno.EACCES,
2435
 
                                   errno.EEXIST)):
 
2594
            if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
 
2595
                logger.warning("Could not save persistent state: {0}"
 
2596
                               .format(os.strerror(e.errno)))
 
2597
            else:
 
2598
                logger.warning("Could not save persistent state:",
 
2599
                               exc_info=e)
2436
2600
                raise e
2437
2601
        
2438
2602
        # Delete all clients, and settings from config
2466
2630
    service.port = tcp_server.socket.getsockname()[1]
2467
2631
    if use_ipv6:
2468
2632
        logger.info("Now listening on address %r, port %d,"
2469
 
                    " flowinfo %d, scope_id %d"
2470
 
                    % tcp_server.socket.getsockname())
 
2633
                    " flowinfo %d, scope_id %d",
 
2634
                    *tcp_server.socket.getsockname())
2471
2635
    else:                       # IPv4
2472
 
        logger.info("Now listening on address %r, port %d"
2473
 
                    % tcp_server.socket.getsockname())
 
2636
        logger.info("Now listening on address %r, port %d",
 
2637
                    *tcp_server.socket.getsockname())
2474
2638
    
2475
2639
    #service.interface = tcp_server.socket.getsockname()[3]
2476
2640
    
2479
2643
        try:
2480
2644
            service.activate()
2481
2645
        except dbus.exceptions.DBusException as error:
2482
 
            logger.critical("DBusException: %s", error)
 
2646
            logger.critical("D-Bus Exception", exc_info=error)
2483
2647
            cleanup()
2484
2648
            sys.exit(1)
2485
2649
        # End of Avahi example code
2492
2656
        logger.debug("Starting main loop")
2493
2657
        main_loop.run()
2494
2658
    except AvahiError as error:
2495
 
        logger.critical("AvahiError: %s", error)
 
2659
        logger.critical("Avahi Error", exc_info=error)
2496
2660
        cleanup()
2497
2661
        sys.exit(1)
2498
2662
    except KeyboardInterrupt: