/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

Merge general D-Bus annotations support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
# "AvahiService" class, and some lines in "main".
12
12
13
13
# Everything else is
14
 
# Copyright © 2008-2011 Teddy Hogeborn
15
 
# Copyright © 2008-2011 Björn Påhlsson
 
14
# Copyright © 2008-2012 Teddy Hogeborn
 
15
# Copyright © 2008-2012 Björn Påhlsson
16
16
17
17
# This program is free software: you can redistribute it and/or modify
18
18
# it under the terms of the GNU General Public License as published by
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
 
 
89
 
version = "1.4.1"
 
89
version = "1.5.3"
90
90
stored_state_file = "clients.pickle"
91
91
 
92
92
logger = logging.getLogger()
111
111
        return interface_index
112
112
 
113
113
 
114
 
def initlogger(level=logging.WARNING):
 
114
def initlogger(debug, level=logging.WARNING):
115
115
    """init logger and add loglevel"""
116
116
    
117
117
    syslogger.setFormatter(logging.Formatter
119
119
                            ' %(message)s'))
120
120
    logger.addHandler(syslogger)
121
121
    
122
 
    console = logging.StreamHandler()
123
 
    console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
124
 
                                           ' [%(process)d]:'
125
 
                                           ' %(levelname)s:'
126
 
                                           ' %(message)s'))
127
 
    logger.addHandler(console)
 
122
    if debug:
 
123
        console = logging.StreamHandler()
 
124
        console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
 
125
                                               ' [%(process)d]:'
 
126
                                               ' %(levelname)s:'
 
127
                                               ' %(message)s'))
 
128
        logger.addHandler(console)
128
129
    logger.setLevel(level)
129
130
 
130
131
 
131
 
class CryptoError(Exception):
 
132
class PGPError(Exception):
 
133
    """Exception if encryption/decryption fails"""
132
134
    pass
133
135
 
134
136
 
135
 
class Crypto(object):
 
137
class PGPEngine(object):
136
138
    """A simple class for OpenPGP symmetric encryption & decryption"""
137
139
    def __init__(self):
138
140
        self.gnupg = GnuPGInterface.GnuPG()
141
143
        self.gnupg.options.meta_interactive = False
142
144
        self.gnupg.options.homedir = self.tempdir
143
145
        self.gnupg.options.extra_args.extend(['--force-mdc',
144
 
                                              '--quiet'])
 
146
                                              '--quiet',
 
147
                                              '--no-use-agent'])
145
148
    
146
149
    def __enter__(self):
147
150
        return self
173
176
    
174
177
    def encrypt(self, data, password):
175
178
        self.gnupg.passphrase = self.password_encode(password)
176
 
        with open(os.devnull) as devnull:
 
179
        with open(os.devnull, "w") as devnull:
177
180
            try:
178
181
                proc = self.gnupg.run(['--symmetric'],
179
182
                                      create_fhs=['stdin', 'stdout'],
184
187
                    ciphertext = f.read()
185
188
                proc.wait()
186
189
            except IOError as e:
187
 
                raise CryptoError(e)
 
190
                raise PGPError(e)
188
191
        self.gnupg.passphrase = None
189
192
        return ciphertext
190
193
    
191
194
    def decrypt(self, data, password):
192
195
        self.gnupg.passphrase = self.password_encode(password)
193
 
        with open(os.devnull) as devnull:
 
196
        with open(os.devnull, "w") as devnull:
194
197
            try:
195
198
                proc = self.gnupg.run(['--decrypt'],
196
199
                                      create_fhs=['stdin', 'stdout'],
197
200
                                      attach_fhs={'stderr': devnull})
198
 
                with contextlib.closing(proc.handles['stdin'] ) as f:
 
201
                with contextlib.closing(proc.handles['stdin']) as f:
199
202
                    f.write(data)
200
203
                with contextlib.closing(proc.handles['stdout']) as f:
201
204
                    decrypted_plaintext = f.read()
202
205
                proc.wait()
203
206
            except IOError as e:
204
 
                raise CryptoError(e)
 
207
                raise PGPError(e)
205
208
        self.gnupg.passphrase = None
206
209
        return decrypted_plaintext
207
210
 
411
414
    interval:   datetime.timedelta(); How often to start a new checker
412
415
    last_approval_request: datetime.datetime(); (UTC) or None
413
416
    last_checked_ok: datetime.datetime(); (UTC) or None
414
 
 
415
417
    last_checker_status: integer between 0 and 255 reflecting exit
416
418
                         status of last checker. -1 reflects crashed
417
 
                         checker, or None.
 
419
                         checker, -2 means no checker completed yet.
418
420
    last_enabled: datetime.datetime(); (UTC) or None
419
421
    name:       string; from the config file, used in log messages and
420
422
                        D-Bus identifiers
421
423
    secret:     bytestring; sent verbatim (over TLS) to client
422
424
    timeout:    datetime.timedelta(); How long from last_checked_ok
423
425
                                      until this client is disabled
424
 
    extended_timeout:   extra long timeout when password has been sent
 
426
    extended_timeout:   extra long timeout when secret has been sent
425
427
    runtime_expansions: Allowed attributes for runtime expansion.
426
428
    expires:    datetime.datetime(); time (UTC) when a client will be
427
429
                disabled, or None
431
433
                          "created", "enabled", "fingerprint",
432
434
                          "host", "interval", "last_checked_ok",
433
435
                          "last_enabled", "name", "timeout")
 
436
    client_defaults = { "timeout": "5m",
 
437
                        "extended_timeout": "15m",
 
438
                        "interval": "2m",
 
439
                        "checker": "fping -q -- %%(host)s",
 
440
                        "host": "",
 
441
                        "approval_delay": "0s",
 
442
                        "approval_duration": "1s",
 
443
                        "approved_by_default": "True",
 
444
                        "enabled": "True",
 
445
                        }
434
446
    
435
447
    def timeout_milliseconds(self):
436
448
        "Return the 'timeout' attribute in milliseconds"
446
458
    
447
459
    def approval_delay_milliseconds(self):
448
460
        return timedelta_to_milliseconds(self.approval_delay)
449
 
    
450
 
    def __init__(self, name = None, config=None):
 
461
 
 
462
    @staticmethod
 
463
    def config_parser(config):
 
464
        """Construct a new dict of client settings of this form:
 
465
        { client_name: {setting_name: value, ...}, ...}
 
466
        with exceptions for any special settings as defined above.
 
467
        NOTE: Must be a pure function. Must return the same result
 
468
        value given the same arguments.
 
469
        """
 
470
        settings = {}
 
471
        for client_name in config.sections():
 
472
            section = dict(config.items(client_name))
 
473
            client = settings[client_name] = {}
 
474
            
 
475
            client["host"] = section["host"]
 
476
            # Reformat values from string types to Python types
 
477
            client["approved_by_default"] = config.getboolean(
 
478
                client_name, "approved_by_default")
 
479
            client["enabled"] = config.getboolean(client_name,
 
480
                                                  "enabled")
 
481
            
 
482
            client["fingerprint"] = (section["fingerprint"].upper()
 
483
                                     .replace(" ", ""))
 
484
            if "secret" in section:
 
485
                client["secret"] = section["secret"].decode("base64")
 
486
            elif "secfile" in section:
 
487
                with open(os.path.expanduser(os.path.expandvars
 
488
                                             (section["secfile"])),
 
489
                          "rb") as secfile:
 
490
                    client["secret"] = secfile.read()
 
491
            else:
 
492
                raise TypeError("No secret or secfile for section %s"
 
493
                                % section)
 
494
            client["timeout"] = string_to_delta(section["timeout"])
 
495
            client["extended_timeout"] = string_to_delta(
 
496
                section["extended_timeout"])
 
497
            client["interval"] = string_to_delta(section["interval"])
 
498
            client["approval_delay"] = string_to_delta(
 
499
                section["approval_delay"])
 
500
            client["approval_duration"] = string_to_delta(
 
501
                section["approval_duration"])
 
502
            client["checker_command"] = section["checker"]
 
503
            client["last_approval_request"] = None
 
504
            client["last_checked_ok"] = None
 
505
            client["last_checker_status"] = -2
 
506
        
 
507
        return settings
 
508
        
 
509
        
 
510
    def __init__(self, settings, name = None):
451
511
        """Note: the 'checker' key in 'config' sets the
452
512
        'checker_command' attribute and *not* the 'checker'
453
513
        attribute."""
454
514
        self.name = name
455
 
        if config is None:
456
 
            config = {}
 
515
        # adding all client settings
 
516
        for setting, value in settings.iteritems():
 
517
            setattr(self, setting, value)
 
518
        
 
519
        if self.enabled:
 
520
            if not hasattr(self, "last_enabled"):
 
521
                self.last_enabled = datetime.datetime.utcnow()
 
522
            if not hasattr(self, "expires"):
 
523
                self.expires = (datetime.datetime.utcnow()
 
524
                                + self.timeout)
 
525
        else:
 
526
            self.last_enabled = None
 
527
            self.expires = None
 
528
       
457
529
        logger.debug("Creating client %r", self.name)
458
530
        # Uppercase and remove spaces from fingerprint for later
459
531
        # comparison purposes with return value from the fingerprint()
460
532
        # function
461
 
        self.fingerprint = (config["fingerprint"].upper()
462
 
                            .replace(" ", ""))
463
533
        logger.debug("  Fingerprint: %s", self.fingerprint)
464
 
        if "secret" in config:
465
 
            self.secret = config["secret"].decode("base64")
466
 
        elif "secfile" in config:
467
 
            with open(os.path.expanduser(os.path.expandvars
468
 
                                         (config["secfile"])),
469
 
                      "rb") as secfile:
470
 
                self.secret = secfile.read()
471
 
        else:
472
 
            raise TypeError("No secret or secfile for client %s"
473
 
                            % self.name)
474
 
        self.host = config.get("host", "")
475
 
        self.created = datetime.datetime.utcnow()
476
 
        self.enabled = config.get("enabled", True)
477
 
        self.last_approval_request = None
478
 
        if self.enabled:
479
 
            self.last_enabled = datetime.datetime.utcnow()
480
 
        else:
481
 
            self.last_enabled = None
482
 
        self.last_checked_ok = None
483
 
        self.last_checker_status = None
484
 
        self.timeout = string_to_delta(config["timeout"])
485
 
        self.extended_timeout = string_to_delta(config
486
 
                                                ["extended_timeout"])
487
 
        self.interval = string_to_delta(config["interval"])
 
534
        self.created = settings.get("created",
 
535
                                    datetime.datetime.utcnow())
 
536
 
 
537
        # attributes specific for this server instance
488
538
        self.checker = None
489
539
        self.checker_initiator_tag = None
490
540
        self.disable_initiator_tag = None
491
 
        if self.enabled:
492
 
            self.expires = datetime.datetime.utcnow() + self.timeout
493
 
        else:
494
 
            self.expires = None
495
541
        self.checker_callback_tag = None
496
 
        self.checker_command = config["checker"]
497
542
        self.current_checker_command = None
498
543
        self.approved = None
499
 
        self.approved_by_default = config.get("approved_by_default",
500
 
                                              True)
501
544
        self.approvals_pending = 0
502
 
        self.approval_delay = string_to_delta(
503
 
            config["approval_delay"])
504
 
        self.approval_duration = string_to_delta(
505
 
            config["approval_duration"])
506
545
        self.changedstate = (multiprocessing_manager
507
546
                             .Condition(multiprocessing_manager
508
547
                                        .Lock()))
575
614
        self.checker_callback_tag = None
576
615
        self.checker = None
577
616
        if os.WIFEXITED(condition):
578
 
            self.last_checker_status =  os.WEXITSTATUS(condition)
 
617
            self.last_checker_status = os.WEXITSTATUS(condition)
579
618
            if self.last_checker_status == 0:
580
619
                logger.info("Checker for %(name)s succeeded",
581
620
                            vars(self))
588
627
            logger.warning("Checker for %(name)s crashed?",
589
628
                           vars(self))
590
629
    
591
 
    def checked_ok(self, timeout=None):
592
 
        """Bump up the timeout for this client.
593
 
        
594
 
        This should only be called when the client has been seen,
595
 
        alive and well.
596
 
        """
 
630
    def checked_ok(self):
 
631
        """Assert that the client has been seen, alive and well."""
 
632
        self.last_checked_ok = datetime.datetime.utcnow()
 
633
        self.last_checker_status = 0
 
634
        self.bump_timeout()
 
635
    
 
636
    def bump_timeout(self, timeout=None):
 
637
        """Bump up the timeout for this client."""
597
638
        if timeout is None:
598
639
            timeout = self.timeout
599
 
        self.last_checked_ok = datetime.datetime.utcnow()
600
640
        if self.disable_initiator_tag is not None:
601
641
            gobject.source_remove(self.disable_initiator_tag)
602
642
        if getattr(self, "enabled", False):
732
772
    return decorator
733
773
 
734
774
 
 
775
def dbus_interface_annotations(dbus_interface):
 
776
    """Decorator for marking functions returning interface annotations.
 
777
    
 
778
    Usage:
 
779
    
 
780
    @dbus_interface_annotations("org.example.Interface")
 
781
    def _foo(self):  # Function name does not matter
 
782
        return {"org.freedesktop.DBus.Deprecated": "true",
 
783
                "org.freedesktop.DBus.Property.EmitsChangedSignal":
 
784
                    "false"}
 
785
    """
 
786
    def decorator(func):
 
787
        func._dbus_is_interface = True
 
788
        func._dbus_interface = dbus_interface
 
789
        func._dbus_name = dbus_interface
 
790
        return func
 
791
    return decorator
 
792
 
 
793
 
 
794
def dbus_annotations(annotations):
 
795
    """Decorator to annotate D-Bus methods, signals or properties
 
796
    Usage:
 
797
    
 
798
    @dbus_service_property("org.example.Interface", signature="b",
 
799
                           access="r")
 
800
    @dbus_annotations({{"org.freedesktop.DBus.Deprecated": "true",
 
801
                        "org.freedesktop.DBus.Property."
 
802
                        "EmitsChangedSignal": "false"})
 
803
    def Property_dbus_property(self):
 
804
        return dbus.Boolean(False)
 
805
    """
 
806
    def decorator(func):
 
807
        func._dbus_annotations = annotations
 
808
        return func
 
809
    return decorator
 
810
 
 
811
 
735
812
class DBusPropertyException(dbus.exceptions.DBusException):
736
813
    """A base class for D-Bus property-related exceptions
737
814
    """
760
837
    """
761
838
    
762
839
    @staticmethod
763
 
    def _is_dbus_property(obj):
764
 
        return getattr(obj, "_dbus_is_property", False)
 
840
    def _is_dbus_thing(thing):
 
841
        """Returns a function testing if an attribute is a D-Bus thing
 
842
        
 
843
        If called like _is_dbus_thing("method") it returns a function
 
844
        suitable for use as predicate to inspect.getmembers().
 
845
        """
 
846
        return lambda obj: getattr(obj, "_dbus_is_{0}".format(thing),
 
847
                                   False)
765
848
    
766
 
    def _get_all_dbus_properties(self):
 
849
    def _get_all_dbus_things(self, thing):
767
850
        """Returns a generator of (name, attribute) pairs
768
851
        """
769
 
        return ((prop.__get__(self)._dbus_name, prop.__get__(self))
 
852
        return ((getattr(athing.__get__(self), "_dbus_name",
 
853
                         name),
 
854
                 athing.__get__(self))
770
855
                for cls in self.__class__.__mro__
771
 
                for name, prop in
772
 
                inspect.getmembers(cls, self._is_dbus_property))
 
856
                for name, athing in
 
857
                inspect.getmembers(cls,
 
858
                                   self._is_dbus_thing(thing)))
773
859
    
774
860
    def _get_dbus_property(self, interface_name, property_name):
775
861
        """Returns a bound method if one exists which is a D-Bus
777
863
        """
778
864
        for cls in  self.__class__.__mro__:
779
865
            for name, value in (inspect.getmembers
780
 
                                (cls, self._is_dbus_property)):
 
866
                                (cls,
 
867
                                 self._is_dbus_thing("property"))):
781
868
                if (value._dbus_name == property_name
782
869
                    and value._dbus_interface == interface_name):
783
870
                    return value.__get__(self)
812
899
            # signatures other than "ay".
813
900
            if prop._dbus_signature != "ay":
814
901
                raise ValueError
815
 
            value = dbus.ByteArray(''.join(unichr(byte)
816
 
                                           for byte in value))
 
902
            value = dbus.ByteArray(b''.join(chr(byte)
 
903
                                            for byte in value))
817
904
        prop(value)
818
905
    
819
906
    @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s",
825
912
        Note: Will not include properties with access="write".
826
913
        """
827
914
        properties = {}
828
 
        for name, prop in self._get_all_dbus_properties():
 
915
        for name, prop in self._get_all_dbus_things("property"):
829
916
            if (interface_name
830
917
                and interface_name != prop._dbus_interface):
831
918
                # Interface non-empty but did not match
846
933
                         path_keyword='object_path',
847
934
                         connection_keyword='connection')
848
935
    def Introspect(self, object_path, connection):
849
 
        """Standard D-Bus method, overloaded to insert property tags.
 
936
        """Overloading of standard D-Bus method.
 
937
        
 
938
        Inserts property tags and interface annotation tags.
850
939
        """
851
940
        xmlstring = dbus.service.Object.Introspect(self, object_path,
852
941
                                                   connection)
859
948
                e.setAttribute("access", prop._dbus_access)
860
949
                return e
861
950
            for if_tag in document.getElementsByTagName("interface"):
 
951
                # Add property tags
862
952
                for tag in (make_tag(document, name, prop)
863
953
                            for name, prop
864
 
                            in self._get_all_dbus_properties()
 
954
                            in self._get_all_dbus_things("property")
865
955
                            if prop._dbus_interface
866
956
                            == if_tag.getAttribute("name")):
867
957
                    if_tag.appendChild(tag)
 
958
                # Add annotation tags
 
959
                for typ in ("method", "signal", "property"):
 
960
                    for tag in if_tag.getElementsByTagName(typ):
 
961
                        annots = dict()
 
962
                        for name, prop in (self.
 
963
                                           _get_all_dbus_things(typ)):
 
964
                            if (name == tag.getAttribute("name")
 
965
                                and prop._dbus_interface
 
966
                                == if_tag.getAttribute("name")):
 
967
                                annots.update(getattr
 
968
                                              (prop,
 
969
                                               "_dbus_annotations",
 
970
                                               {}))
 
971
                        for name, value in annots.iteritems():
 
972
                            ann_tag = document.createElement(
 
973
                                "annotation")
 
974
                            ann_tag.setAttribute("name", name)
 
975
                            ann_tag.setAttribute("value", value)
 
976
                            tag.appendChild(ann_tag)
 
977
                # Add interface annotation tags
 
978
                for annotation, value in dict(
 
979
                    itertools.chain(
 
980
                        *(annotations().iteritems()
 
981
                          for name, annotations in
 
982
                          self._get_all_dbus_things("interface")
 
983
                          if name == if_tag.getAttribute("name")
 
984
                          ))).iteritems():
 
985
                    ann_tag = document.createElement("annotation")
 
986
                    ann_tag.setAttribute("name", annotation)
 
987
                    ann_tag.setAttribute("value", value)
 
988
                    if_tag.appendChild(ann_tag)
868
989
                # Add the names to the return values for the
869
990
                # "org.freedesktop.DBus.Properties" methods
870
991
                if (if_tag.getAttribute("name")
905
1026
    def __new__(mcs, name, bases, attr):
906
1027
        # Go through all the base classes which could have D-Bus
907
1028
        # methods, signals, or properties in them
 
1029
        old_interface_names = []
908
1030
        for base in (b for b in bases
909
1031
                     if issubclass(b, dbus.service.Object)):
910
1032
            # Go though all attributes of the base class
920
1042
                alt_interface = (attribute._dbus_interface
921
1043
                                 .replace("se.recompile.Mandos",
922
1044
                                          "se.bsnet.fukt.Mandos"))
 
1045
                if alt_interface != attribute._dbus_interface:
 
1046
                    old_interface_names.append(alt_interface)
923
1047
                # Is this a D-Bus signal?
924
1048
                if getattr(attribute, "_dbus_is_signal", False):
925
1049
                    # Extract the original non-method function by
940
1064
                                nonmethod_func.func_name,
941
1065
                                nonmethod_func.func_defaults,
942
1066
                                nonmethod_func.func_closure)))
 
1067
                    # Copy annotations, if any
 
1068
                    try:
 
1069
                        new_function._dbus_annotations = (
 
1070
                            dict(attribute._dbus_annotations))
 
1071
                    except AttributeError:
 
1072
                        pass
943
1073
                    # Define a creator of a function to call both the
944
1074
                    # old and new functions, so both the old and new
945
1075
                    # signals gets sent when the function is called
973
1103
                                        attribute.func_name,
974
1104
                                        attribute.func_defaults,
975
1105
                                        attribute.func_closure)))
 
1106
                    # Copy annotations, if any
 
1107
                    try:
 
1108
                        attr[attrname]._dbus_annotations = (
 
1109
                            dict(attribute._dbus_annotations))
 
1110
                    except AttributeError:
 
1111
                        pass
976
1112
                # Is this a D-Bus property?
977
1113
                elif getattr(attribute, "_dbus_is_property", False):
978
1114
                    # Create a new, but exactly alike, function
992
1128
                                        attribute.func_name,
993
1129
                                        attribute.func_defaults,
994
1130
                                        attribute.func_closure)))
 
1131
                    # Copy annotations, if any
 
1132
                    try:
 
1133
                        attr[attrname]._dbus_annotations = (
 
1134
                            dict(attribute._dbus_annotations))
 
1135
                    except AttributeError:
 
1136
                        pass
 
1137
                # Is this a D-Bus interface?
 
1138
                elif getattr(attribute, "_dbus_is_interface", False):
 
1139
                    # Create a new, but exactly alike, function
 
1140
                    # object.  Decorate it to be a new D-Bus interface
 
1141
                    # with the alternate D-Bus interface name.  Add it
 
1142
                    # to the class.
 
1143
                    attr[attrname] = (dbus_interface_annotations
 
1144
                                      (alt_interface)
 
1145
                                      (types.FunctionType
 
1146
                                       (attribute.func_code,
 
1147
                                        attribute.func_globals,
 
1148
                                        attribute.func_name,
 
1149
                                        attribute.func_defaults,
 
1150
                                        attribute.func_closure)))
 
1151
        # Deprecate all old interfaces
 
1152
        basename="_AlternateDBusNamesMetaclass_interface_annotation{0}"
 
1153
        for old_interface_name in old_interface_names:
 
1154
            @dbus_interface_annotations(old_interface_name)
 
1155
            def func(self):
 
1156
                return { "org.freedesktop.DBus.Deprecated": "true" }
 
1157
            # Find an unused name
 
1158
            for aname in (basename.format(i) for i in
 
1159
                          itertools.count()):
 
1160
                if aname not in attr:
 
1161
                    attr[aname] = func
 
1162
                    break
995
1163
        return type.__new__(mcs, name, bases, attr)
996
1164
 
997
1165
 
1011
1179
    def __init__(self, bus = None, *args, **kwargs):
1012
1180
        self.bus = bus
1013
1181
        Client.__init__(self, *args, **kwargs)
1014
 
        
1015
 
        self._approvals_pending = 0
1016
1182
        # Only now, when this client is initialized, can it show up on
1017
1183
        # the D-Bus
1018
1184
        client_object_name = unicode(self.name).translate(
1064
1230
                                       checker is not None)
1065
1231
    last_checked_ok = notifychangeproperty(datetime_to_dbus,
1066
1232
                                           "LastCheckedOK")
 
1233
    last_checker_status = notifychangeproperty(dbus.Int16,
 
1234
                                               "LastCheckerStatus")
1067
1235
    last_approval_request = notifychangeproperty(
1068
1236
        datetime_to_dbus, "LastApprovalRequest")
1069
1237
    approved_by_default = notifychangeproperty(dbus.Boolean,
1070
1238
                                               "ApprovedByDefault")
1071
 
    approval_delay = notifychangeproperty(dbus.UInt16,
 
1239
    approval_delay = notifychangeproperty(dbus.UInt64,
1072
1240
                                          "ApprovalDelay",
1073
1241
                                          type_func =
1074
1242
                                          timedelta_to_milliseconds)
1075
1243
    approval_duration = notifychangeproperty(
1076
 
        dbus.UInt16, "ApprovalDuration",
 
1244
        dbus.UInt64, "ApprovalDuration",
1077
1245
        type_func = timedelta_to_milliseconds)
1078
1246
    host = notifychangeproperty(dbus.String, "Host")
1079
 
    timeout = notifychangeproperty(dbus.UInt16, "Timeout",
 
1247
    timeout = notifychangeproperty(dbus.UInt64, "Timeout",
1080
1248
                                   type_func =
1081
1249
                                   timedelta_to_milliseconds)
1082
1250
    extended_timeout = notifychangeproperty(
1083
 
        dbus.UInt16, "ExtendedTimeout",
 
1251
        dbus.UInt64, "ExtendedTimeout",
1084
1252
        type_func = timedelta_to_milliseconds)
1085
 
    interval = notifychangeproperty(dbus.UInt16,
 
1253
    interval = notifychangeproperty(dbus.UInt64,
1086
1254
                                    "Interval",
1087
1255
                                    type_func =
1088
1256
                                    timedelta_to_milliseconds)
1147
1315
    ## D-Bus methods, signals & properties
1148
1316
    _interface = "se.recompile.Mandos.Client"
1149
1317
    
 
1318
    ## Interfaces
 
1319
    
 
1320
    @dbus_interface_annotations(_interface)
 
1321
    def _foo(self):
 
1322
        return { "org.freedesktop.DBus.Property.EmitsChangedSignal":
 
1323
                     "false"}
 
1324
    
1150
1325
    ## Signals
1151
1326
    
1152
1327
    # CheckerCompleted - signal
1188
1363
        "D-Bus signal"
1189
1364
        return self.need_approval()
1190
1365
    
1191
 
    # NeRwequest - signal
1192
 
    @dbus.service.signal(_interface, signature="s")
1193
 
    def NewRequest(self, ip):
1194
 
        """D-Bus signal
1195
 
        Is sent after a client request a password.
1196
 
        """
1197
 
        pass
1198
 
    
1199
1366
    ## Methods
1200
1367
    
1201
1368
    # Approve - method
1279
1446
    def Host_dbus_property(self, value=None):
1280
1447
        if value is None:       # get
1281
1448
            return dbus.String(self.host)
1282
 
        self.host = value
 
1449
        self.host = unicode(value)
1283
1450
    
1284
1451
    # Created - property
1285
1452
    @dbus_service_property(_interface, signature="s", access="read")
1311
1478
            return
1312
1479
        return datetime_to_dbus(self.last_checked_ok)
1313
1480
    
 
1481
    # LastCheckerStatus - property
 
1482
    @dbus_service_property(_interface, signature="n",
 
1483
                           access="read")
 
1484
    def LastCheckerStatus_dbus_property(self):
 
1485
        return dbus.Int16(self.last_checker_status)
 
1486
    
1314
1487
    # Expires - property
1315
1488
    @dbus_service_property(_interface, signature="s", access="read")
1316
1489
    def Expires_dbus_property(self):
1328
1501
        if value is None:       # get
1329
1502
            return dbus.UInt64(self.timeout_milliseconds())
1330
1503
        self.timeout = datetime.timedelta(0, 0, 0, value)
1331
 
        if getattr(self, "disable_initiator_tag", None) is None:
1332
 
            return
1333
1504
        # Reschedule timeout
1334
 
        gobject.source_remove(self.disable_initiator_tag)
1335
 
        self.disable_initiator_tag = None
1336
 
        self.expires = None
1337
 
        time_to_die = timedelta_to_milliseconds((self
1338
 
                                                 .last_checked_ok
1339
 
                                                 + self.timeout)
1340
 
                                                - datetime.datetime
1341
 
                                                .utcnow())
1342
 
        if time_to_die <= 0:
1343
 
            # The timeout has passed
1344
 
            self.disable()
1345
 
        else:
1346
 
            self.expires = (datetime.datetime.utcnow()
1347
 
                            + datetime.timedelta(milliseconds =
1348
 
                                                 time_to_die))
1349
 
            self.disable_initiator_tag = (gobject.timeout_add
1350
 
                                          (time_to_die, self.disable))
 
1505
        if self.enabled:
 
1506
            now = datetime.datetime.utcnow()
 
1507
            time_to_die = timedelta_to_milliseconds(
 
1508
                (self.last_checked_ok + self.timeout) - now)
 
1509
            if time_to_die <= 0:
 
1510
                # The timeout has passed
 
1511
                self.disable()
 
1512
            else:
 
1513
                self.expires = (now +
 
1514
                                datetime.timedelta(milliseconds =
 
1515
                                                   time_to_die))
 
1516
                if (getattr(self, "disable_initiator_tag", None)
 
1517
                    is None):
 
1518
                    return
 
1519
                gobject.source_remove(self.disable_initiator_tag)
 
1520
                self.disable_initiator_tag = (gobject.timeout_add
 
1521
                                              (time_to_die,
 
1522
                                               self.disable))
1351
1523
    
1352
1524
    # ExtendedTimeout - property
1353
1525
    @dbus_service_property(_interface, signature="t",
1379
1551
    def Checker_dbus_property(self, value=None):
1380
1552
        if value is None:       # get
1381
1553
            return dbus.String(self.checker_command)
1382
 
        self.checker_command = value
 
1554
        self.checker_command = unicode(value)
1383
1555
    
1384
1556
    # CheckerRunning - property
1385
1557
    @dbus_service_property(_interface, signature="b",
1509
1681
                except KeyError:
1510
1682
                    return
1511
1683
                
1512
 
                if self.server.use_dbus:
1513
 
                    # Emit D-Bus signal
1514
 
                    client.NewRequest(str(self.client_address))
1515
 
                
1516
1684
                if client.approval_delay:
1517
1685
                    delay = client.approval_delay
1518
1686
                    client.approvals_pending += 1
1582
1750
                
1583
1751
                logger.info("Sending secret to %s", client.name)
1584
1752
                # bump the timeout using extended_timeout
1585
 
                client.checked_ok(client.extended_timeout)
 
1753
                client.bump_timeout(client.extended_timeout)
1586
1754
                if self.server.use_dbus:
1587
1755
                    # Emit D-Bus signal
1588
1756
                    client.GotSecret()
1664
1832
    def sub_process_main(self, request, address):
1665
1833
        try:
1666
1834
            self.finish_request(request, address)
1667
 
        except:
 
1835
        except Exception:
1668
1836
            self.handle_error(request, address)
1669
1837
        self.close_request(request)
1670
1838
    
1931
2099
        sys.exit()
1932
2100
    if not noclose:
1933
2101
        # Close all standard open file descriptors
1934
 
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
2102
        null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
1935
2103
        if not stat.S_ISCHR(os.fstat(null).st_mode):
1936
2104
            raise OSError(errno.ENODEV,
1937
2105
                          "%s not a character device"
1938
 
                          % os.path.devnull)
 
2106
                          % os.devnull)
1939
2107
        os.dup2(null, sys.stdin.fileno())
1940
2108
        os.dup2(null, sys.stdout.fileno())
1941
2109
        os.dup2(null, sys.stderr.fileno())
2049
2217
                                     stored_state_file)
2050
2218
    
2051
2219
    if debug:
2052
 
        initlogger(logging.DEBUG)
 
2220
        initlogger(debug, logging.DEBUG)
2053
2221
    else:
2054
2222
        if not debuglevel:
2055
 
            initlogger()
 
2223
            initlogger(debug)
2056
2224
        else:
2057
2225
            level = getattr(logging, debuglevel.upper())
2058
 
            initlogger(level)
 
2226
            initlogger(debug, level)
2059
2227
    
2060
2228
    if server_settings["servicename"] != "Mandos":
2061
2229
        syslogger.setFormatter(logging.Formatter
2064
2232
                                % server_settings["servicename"]))
2065
2233
    
2066
2234
    # Parse config file with clients
2067
 
    client_defaults = { "timeout": "5m",
2068
 
                        "extended_timeout": "15m",
2069
 
                        "interval": "2m",
2070
 
                        "checker": "fping -q -- %%(host)s",
2071
 
                        "host": "",
2072
 
                        "approval_delay": "0s",
2073
 
                        "approval_duration": "1s",
2074
 
                        }
2075
 
    client_config = configparser.SafeConfigParser(client_defaults)
 
2235
    client_config = configparser.SafeConfigParser(Client
 
2236
                                                  .client_defaults)
2076
2237
    client_config.read(os.path.join(server_settings["configdir"],
2077
2238
                                    "clients.conf"))
2078
2239
    
2131
2292
         .gnutls_global_set_log_function(debug_gnutls))
2132
2293
        
2133
2294
        # Redirect stdin so all checkers get /dev/null
2134
 
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
2295
        null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2135
2296
        os.dup2(null, sys.stdin.fileno())
2136
2297
        if null > 2:
2137
2298
            os.close(null)
2138
 
    else:
2139
 
        # No console logging
2140
 
        logger.removeHandler(console)
2141
2299
    
2142
2300
    # Need to fork before connecting to D-Bus
2143
2301
    if not debug:
2144
2302
        # Close all input and output, do double fork, etc.
2145
2303
        daemon()
2146
2304
    
 
2305
    gobject.threads_init()
 
2306
    
2147
2307
    global main_loop
2148
2308
    # From the Avahi example code
2149
 
    DBusGMainLoop(set_as_default=True )
 
2309
    DBusGMainLoop(set_as_default=True)
2150
2310
    main_loop = gobject.MainLoop()
2151
2311
    bus = dbus.SystemBus()
2152
2312
    # End of Avahi example code
2179
2339
        client_class = functools.partial(ClientDBusTransitional,
2180
2340
                                         bus = bus)
2181
2341
    
2182
 
    special_settings = {
2183
 
        # Some settings need to be accessd by special methods;
2184
 
        # booleans need .getboolean(), etc.  Here is a list of them:
2185
 
        "approved_by_default":
2186
 
            lambda section:
2187
 
            client_config.getboolean(section, "approved_by_default"),
2188
 
        "enabled":
2189
 
            lambda section:
2190
 
            client_config.getboolean(section, "enabled"),
2191
 
        }
2192
 
    # Construct a new dict of client settings of this form:
2193
 
    # { client_name: {setting_name: value, ...}, ...}
2194
 
    # with exceptions for any special settings as defined above
2195
 
    client_settings = dict((clientname,
2196
 
                           dict((setting,
2197
 
                                 (value
2198
 
                                  if setting not in special_settings
2199
 
                                  else special_settings[setting]
2200
 
                                  (clientname)))
2201
 
                                for setting, value in
2202
 
                                client_config.items(clientname)))
2203
 
                          for clientname in client_config.sections())
2204
 
    
 
2342
    client_settings = Client.config_parser(client_config)
2205
2343
    old_client_settings = {}
2206
 
    clients_data = []
 
2344
    clients_data = {}
2207
2345
    
2208
2346
    # Get client data and settings from last running state.
2209
2347
    if server_settings["restore"]:
2217
2355
                           .format(e))
2218
2356
            if e.errno != errno.ENOENT:
2219
2357
                raise
 
2358
        except EOFError as e:
 
2359
            logger.warning("Could not load persistent state: "
 
2360
                           "EOFError: {0}".format(e))
2220
2361
    
2221
 
    with Crypto() as crypt:
2222
 
        for client in clients_data:
2223
 
            client_name = client["name"]
2224
 
            
 
2362
    with PGPEngine() as pgp:
 
2363
        for client_name, client in clients_data.iteritems():
2225
2364
            # Decide which value to use after restoring saved state.
2226
2365
            # We have three different values: Old config file,
2227
2366
            # new config file, and saved state.
2235
2374
                    if (name != "secret" and
2236
2375
                        value != old_client_settings[client_name]
2237
2376
                        [name]):
2238
 
                        setattr(client, name, value)
 
2377
                        client[name] = value
2239
2378
                except KeyError:
2240
2379
                    pass
2241
2380
            
2242
2381
            # Clients who has passed its expire date can still be
2243
 
            # enabled if its last checker was sucessful.  Clients
2244
 
            # whose checker failed before we stored its state is
2245
 
            # assumed to have failed all checkers during downtime.
2246
 
            if client["enabled"] and client["last_checked_ok"]:
2247
 
                if ((datetime.datetime.utcnow()
2248
 
                     - client["last_checked_ok"])
2249
 
                    > client["interval"]):
2250
 
                    if client["last_checker_status"] != 0:
 
2382
            # enabled if its last checker was successful.  Clients
 
2383
            # whose checker succeeded before we stored its state is
 
2384
            # assumed to have successfully run all checkers during
 
2385
            # downtime.
 
2386
            if client["enabled"]:
 
2387
                if datetime.datetime.utcnow() >= client["expires"]:
 
2388
                    if not client["last_checked_ok"]:
 
2389
                        logger.warning(
 
2390
                            "disabling client {0} - Client never "
 
2391
                            "performed a successful checker"
 
2392
                            .format(client_name))
 
2393
                        client["enabled"] = False
 
2394
                    elif client["last_checker_status"] != 0:
 
2395
                        logger.warning(
 
2396
                            "disabling client {0} - Client "
 
2397
                            "last checker failed with error code {1}"
 
2398
                            .format(client_name,
 
2399
                                    client["last_checker_status"]))
2251
2400
                        client["enabled"] = False
2252
2401
                    else:
2253
2402
                        client["expires"] = (datetime.datetime
2254
2403
                                             .utcnow()
2255
2404
                                             + client["timeout"])
2256
 
            
2257
 
            client["changedstate"] = (multiprocessing_manager
2258
 
                                      .Condition
2259
 
                                      (multiprocessing_manager
2260
 
                                       .Lock()))
2261
 
            if use_dbus:
2262
 
                new_client = (ClientDBusTransitional.__new__
2263
 
                              (ClientDBusTransitional))
2264
 
                tcp_server.clients[client_name] = new_client
2265
 
                new_client.bus = bus
2266
 
                for name, value in client.iteritems():
2267
 
                    setattr(new_client, name, value)
2268
 
                client_object_name = unicode(client_name).translate(
2269
 
                    {ord("."): ord("_"),
2270
 
                     ord("-"): ord("_")})
2271
 
                new_client.dbus_object_path = (dbus.ObjectPath
2272
 
                                               ("/clients/"
2273
 
                                                + client_object_name))
2274
 
                DBusObjectWithProperties.__init__(new_client,
2275
 
                                                  new_client.bus,
2276
 
                                                  new_client
2277
 
                                                  .dbus_object_path)
2278
 
            else:
2279
 
                tcp_server.clients[client_name] = (Client.__new__
2280
 
                                                   (Client))
2281
 
                for name, value in client.iteritems():
2282
 
                    setattr(tcp_server.clients[client_name],
2283
 
                            name, value)
2284
 
            
 
2405
                        logger.debug("Last checker succeeded,"
 
2406
                                     " keeping {0} enabled"
 
2407
                                     .format(client_name))
2285
2408
            try:
2286
 
                tcp_server.clients[client_name].secret = (
2287
 
                    crypt.decrypt(tcp_server.clients[client_name]
2288
 
                                  .encrypted_secret,
2289
 
                                  client_settings[client_name]
2290
 
                                  ["secret"]))
2291
 
            except CryptoError:
 
2409
                client["secret"] = (
 
2410
                    pgp.decrypt(client["encrypted_secret"],
 
2411
                                client_settings[client_name]
 
2412
                                ["secret"]))
 
2413
            except PGPError:
2292
2414
                # If decryption fails, we use secret from new settings
2293
 
                tcp_server.clients[client_name].secret = (
 
2415
                logger.debug("Failed to decrypt {0} old secret"
 
2416
                             .format(client_name))
 
2417
                client["secret"] = (
2294
2418
                    client_settings[client_name]["secret"])
 
2419
 
2295
2420
    
2296
 
    # Create/remove clients based on new changes made to config
2297
 
    for clientname in set(old_client_settings) - set(client_settings):
2298
 
        del tcp_server.clients[clientname]
2299
 
    for clientname in set(client_settings) - set(old_client_settings):
2300
 
        tcp_server.clients[clientname] = (client_class(name
2301
 
                                                       = clientname,
2302
 
                                                       config =
2303
 
                                                       client_settings
2304
 
                                                       [clientname]))
 
2421
    # Add/remove clients based on new changes made to config
 
2422
    for client_name in (set(old_client_settings)
 
2423
                        - set(client_settings)):
 
2424
        del clients_data[client_name]
 
2425
    for client_name in (set(client_settings)
 
2426
                        - set(old_client_settings)):
 
2427
        clients_data[client_name] = client_settings[client_name]
 
2428
 
 
2429
    # Create all client objects
 
2430
    for client_name, client in clients_data.iteritems():
 
2431
        tcp_server.clients[client_name] = client_class(
 
2432
            name = client_name, settings = client)
2305
2433
    
2306
2434
    if not tcp_server.clients:
2307
2435
        logger.warning("No clients defined")
2319
2447
            # "pidfile" was never created
2320
2448
            pass
2321
2449
        del pidfilename
2322
 
        
2323
2450
        signal.signal(signal.SIGINT, signal.SIG_IGN)
2324
2451
    
2325
2452
    signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2326
2453
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2327
2454
    
2328
2455
    if use_dbus:
2329
 
        class MandosDBusService(dbus.service.Object):
 
2456
        class MandosDBusService(DBusObjectWithProperties):
2330
2457
            """A D-Bus proxy object"""
2331
2458
            def __init__(self):
2332
2459
                dbus.service.Object.__init__(self, bus, "/")
2333
2460
            _interface = "se.recompile.Mandos"
2334
2461
            
 
2462
            @dbus_interface_annotations(_interface)
 
2463
            def _foo(self):
 
2464
                return { "org.freedesktop.DBus.Property"
 
2465
                         ".EmitsChangedSignal":
 
2466
                             "false"}
 
2467
            
2335
2468
            @dbus.service.signal(_interface, signature="o")
2336
2469
            def ClientAdded(self, objpath):
2337
2470
                "D-Bus signal"
2394
2527
        # Store client before exiting. Secrets are encrypted with key
2395
2528
        # based on what config file has. If config file is
2396
2529
        # removed/edited, old secret will thus be unrecovable.
2397
 
        clients = []
2398
 
        with Crypto() as crypt:
 
2530
        clients = {}
 
2531
        with PGPEngine() as pgp:
2399
2532
            for client in tcp_server.clients.itervalues():
2400
2533
                key = client_settings[client.name]["secret"]
2401
 
                client.encrypted_secret = crypt.encrypt(client.secret,
2402
 
                                                        key)
 
2534
                client.encrypted_secret = pgp.encrypt(client.secret,
 
2535
                                                      key)
2403
2536
                client_dict = {}
2404
2537
                
2405
 
                # A list of attributes that will not be stored when
2406
 
                # shutting down.
2407
 
                exclude = set(("bus", "changedstate", "secret"))
 
2538
                # A list of attributes that can not be pickled
 
2539
                # + secret.
 
2540
                exclude = set(("bus", "changedstate", "secret",
 
2541
                               "checker"))
2408
2542
                for name, typ in (inspect.getmembers
2409
2543
                                  (dbus.service.Object)):
2410
2544
                    exclude.add(name)
2415
2549
                    if attr not in exclude:
2416
2550
                        client_dict[attr] = getattr(client, attr)
2417
2551
                
2418
 
                clients.append(client_dict)
 
2552
                clients[client.name] = client_dict
2419
2553
                del client_settings[client.name]["secret"]
2420
2554
        
2421
2555
        try:
2422
 
            with os.fdopen(os.open(stored_state_path,
2423
 
                                   os.O_CREAT|os.O_WRONLY|os.O_TRUNC,
2424
 
                                   0600), "wb") as stored_state:
 
2556
            tempfd, tempname = tempfile.mkstemp(suffix=".pickle",
 
2557
                                                prefix="clients-",
 
2558
                                                dir=os.path.dirname
 
2559
                                                (stored_state_path))
 
2560
            with os.fdopen(tempfd, "wb") as stored_state:
2425
2561
                pickle.dump((clients, client_settings), stored_state)
 
2562
            os.rename(tempname, stored_state_path)
2426
2563
        except (IOError, OSError) as e:
2427
2564
            logger.warning("Could not save persistent state: {0}"
2428
2565
                           .format(e))
2429
 
            if e.errno not in (errno.ENOENT, errno.EACCES):
2430
 
                raise
 
2566
            if not debug:
 
2567
                try:
 
2568
                    os.remove(tempname)
 
2569
                except NameError:
 
2570
                    pass
 
2571
            if e.errno not in set((errno.ENOENT, errno.EACCES,
 
2572
                                   errno.EEXIST)):
 
2573
                raise e
2431
2574
        
2432
2575
        # Delete all clients, and settings from config
2433
2576
        while tcp_server.clients:
2497
2640
    # Must run before the D-Bus bus name gets deregistered
2498
2641
    cleanup()
2499
2642
 
2500
 
 
2501
2643
if __name__ == '__main__':
2502
2644
    main()