/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:
209
209
        return decrypted_plaintext
210
210
 
211
211
 
212
 
 
213
212
class AvahiError(Exception):
214
213
    def __init__(self, value, *args, **kwargs):
215
214
        self.value = value
244
243
    server: D-Bus Server
245
244
    bus: dbus.SystemBus()
246
245
    """
 
246
    
247
247
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
248
248
                 servicetype = None, port = None, TXT = None,
249
249
                 domain = "", host = "", max_renames = 32768,
262
262
        self.server = None
263
263
        self.bus = bus
264
264
        self.entry_group_state_changed_match = None
 
265
    
265
266
    def rename(self):
266
267
        """Derived from the Avahi example code"""
267
268
        if self.rename_count >= self.max_renames:
281
282
            self.cleanup()
282
283
            os._exit(1)
283
284
        self.rename_count += 1
 
285
    
284
286
    def remove(self):
285
287
        """Derived from the Avahi example code"""
286
288
        if self.entry_group_state_changed_match is not None:
288
290
            self.entry_group_state_changed_match = None
289
291
        if self.group is not None:
290
292
            self.group.Reset()
 
293
    
291
294
    def add(self):
292
295
        """Derived from the Avahi example code"""
293
296
        self.remove()
310
313
            dbus.UInt16(self.port),
311
314
            avahi.string_array_to_txt_array(self.TXT))
312
315
        self.group.Commit()
 
316
    
313
317
    def entry_group_state_changed(self, state, error):
314
318
        """Derived from the Avahi example code"""
315
319
        logger.debug("Avahi entry group state change: %i", state)
322
326
        elif state == avahi.ENTRY_GROUP_FAILURE:
323
327
            logger.critical("Avahi: Error in group state changed %s",
324
328
                            unicode(error))
325
 
            raise AvahiGroupError("State changed: %s"
326
 
                                  % unicode(error))
 
329
            raise AvahiGroupError("State changed: {0!s}"
 
330
                                  .format(error))
 
331
    
327
332
    def cleanup(self):
328
333
        """Derived from the Avahi example code"""
329
334
        if self.group is not None:
334
339
                pass
335
340
            self.group = None
336
341
        self.remove()
 
342
    
337
343
    def server_state_changed(self, state, error=None):
338
344
        """Derived from the Avahi example code"""
339
345
        logger.debug("Avahi server state change: %i", state)
358
364
                logger.debug("Unknown state: %r", state)
359
365
            else:
360
366
                logger.debug("Unknown state: %r: %r", state, error)
 
367
    
361
368
    def activate(self):
362
369
        """Derived from the Avahi example code"""
363
370
        if self.server is None:
375
382
        """Add the new name to the syslog messages"""
376
383
        ret = AvahiService.rename(self)
377
384
        syslogger.setFormatter(logging.Formatter
378
 
                               ('Mandos (%s) [%%(process)d]:'
379
 
                                ' %%(levelname)s: %%(message)s'
380
 
                                % self.name))
 
385
                               ('Mandos ({0}) [%(process)d]:'
 
386
                                ' %(levelname)s: %(message)s'
 
387
                                .format(self.name)))
381
388
        return ret
382
389
 
383
390
def timedelta_to_milliseconds(td):
385
392
    return ((td.days * 24 * 60 * 60 * 1000)
386
393
            + (td.seconds * 1000)
387
394
            + (td.microseconds // 1000))
388
 
        
 
395
 
389
396
class Client(object):
390
397
    """A representation of a client host served by this server.
391
398
    
458
465
    
459
466
    def approval_delay_milliseconds(self):
460
467
        return timedelta_to_milliseconds(self.approval_delay)
461
 
 
 
468
    
462
469
    @staticmethod
463
470
    def config_parser(config):
464
471
        """Construct a new dict of client settings of this form:
489
496
                          "rb") as secfile:
490
497
                    client["secret"] = secfile.read()
491
498
            else:
492
 
                raise TypeError("No secret or secfile for section %s"
493
 
                                % section)
 
499
                raise TypeError("No secret or secfile for section {0}"
 
500
                                .format(section))
494
501
            client["timeout"] = string_to_delta(section["timeout"])
495
502
            client["extended_timeout"] = string_to_delta(
496
503
                section["extended_timeout"])
505
512
            client["last_checker_status"] = -2
506
513
        
507
514
        return settings
508
 
        
509
 
        
 
515
    
510
516
    def __init__(self, settings, name = None):
511
 
        """Note: the 'checker' key in 'config' sets the
512
 
        'checker_command' attribute and *not* the 'checker'
513
 
        attribute."""
514
517
        self.name = name
515
518
        # adding all client settings
516
519
        for setting, value in settings.iteritems():
525
528
        else:
526
529
            self.last_enabled = None
527
530
            self.expires = None
528
 
       
 
531
        
529
532
        logger.debug("Creating client %r", self.name)
530
533
        # Uppercase and remove spaces from fingerprint for later
531
534
        # comparison purposes with return value from the fingerprint()
533
536
        logger.debug("  Fingerprint: %s", self.fingerprint)
534
537
        self.created = settings.get("created",
535
538
                                    datetime.datetime.utcnow())
536
 
 
 
539
        
537
540
        # attributes specific for this server instance
538
541
        self.checker = None
539
542
        self.checker_initiator_tag = None
758
761
    # "Set" method, so we fail early here:
759
762
    if byte_arrays and signature != "ay":
760
763
        raise ValueError("Byte arrays not supported for non-'ay'"
761
 
                         " signature %r" % signature)
 
764
                         " signature {0!r}".format(signature))
762
765
    def decorator(func):
763
766
        func._dbus_is_property = True
764
767
        func._dbus_interface = dbus_interface
773
776
 
774
777
 
775
778
def dbus_interface_annotations(dbus_interface):
776
 
    """Decorator for marking functions returning interface annotations.
 
779
    """Decorator for marking functions returning interface annotations
777
780
    
778
781
    Usage:
779
782
    
1018
1021
                       variant_level=variant_level)
1019
1022
 
1020
1023
 
1021
 
class AlternateDBusNamesMetaclass(DBusObjectWithProperties
1022
 
                                  .__metaclass__):
1023
 
    """Applied to an empty subclass of a D-Bus object, this metaclass
1024
 
    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).
1025
1046
    """
1026
 
    def __new__(mcs, name, bases, attr):
1027
 
        # Go through all the base classes which could have D-Bus
1028
 
        # methods, signals, or properties in them
1029
 
        old_interface_names = []
1030
 
        for base in (b for b in bases
1031
 
                     if issubclass(b, dbus.service.Object)):
1032
 
            # Go though all attributes of the base class
1033
 
            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):
1034
1054
                # Ignore non-D-Bus attributes, and D-Bus attributes
1035
1055
                # with the wrong interface name
1036
1056
                if (not hasattr(attribute, "_dbus_interface")
1037
1057
                    or not attribute._dbus_interface
1038
 
                    .startswith("se.recompile.Mandos")):
 
1058
                    .startswith(orig_interface_name)):
1039
1059
                    continue
1040
1060
                # Create an alternate D-Bus interface name based on
1041
1061
                # the current name
1042
1062
                alt_interface = (attribute._dbus_interface
1043
 
                                 .replace("se.recompile.Mandos",
1044
 
                                          "se.bsnet.fukt.Mandos"))
1045
 
                if alt_interface != attribute._dbus_interface:
1046
 
                    old_interface_names.append(alt_interface)
 
1063
                                 .replace(orig_interface_name,
 
1064
                                          alt_interface_name))
 
1065
                interface_names.add(alt_interface)
1047
1066
                # Is this a D-Bus signal?
1048
1067
                if getattr(attribute, "_dbus_is_signal", False):
1049
1068
                    # Extract the original non-method function by
1071
1090
                    except AttributeError:
1072
1091
                        pass
1073
1092
                    # Define a creator of a function to call both the
1074
 
                    # old and new functions, so both the old and new
1075
 
                    # 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
1076
1096
                    def fixscope(func1, func2):
1077
1097
                        """This function is a scope container to pass
1078
1098
                        func1 and func2 to the "call_both" function
1085
1105
                        return call_both
1086
1106
                    # Create the "call_both" function and add it to
1087
1107
                    # the class
1088
 
                    attr[attrname] = fixscope(attribute,
1089
 
                                              new_function)
 
1108
                    attr[attrname] = fixscope(attribute, new_function)
1090
1109
                # Is this a D-Bus method?
1091
1110
                elif getattr(attribute, "_dbus_is_method", False):
1092
1111
                    # Create a new, but exactly alike, function
1148
1167
                                        attribute.func_name,
1149
1168
                                        attribute.func_defaults,
1150
1169
                                        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
1163
 
        return type.__new__(mcs, name, bases, attr)
1164
 
 
1165
 
 
 
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"})
1166
1195
class ClientDBus(Client, DBusObjectWithProperties):
1167
1196
    """A Client class using D-Bus
1168
1197
    
1188
1217
                                 ("/clients/" + client_object_name))
1189
1218
        DBusObjectWithProperties.__init__(self, self.bus,
1190
1219
                                          self.dbus_object_path)
1191
 
        
 
1220
    
1192
1221
    def notifychangeproperty(transform_func,
1193
1222
                             dbus_name, type_func=lambda x: x,
1194
1223
                             variant_level=1):
1217
1246
        
1218
1247
        return property(lambda self: getattr(self, attrname), setter)
1219
1248
    
1220
 
    
1221
1249
    expires = notifychangeproperty(datetime_to_dbus, "Expires")
1222
1250
    approvals_pending = notifychangeproperty(dbus.Boolean,
1223
1251
                                             "ApprovalPending",
1311
1339
                            (self.approval_duration),
1312
1340
                            self._reset_approved)
1313
1341
    
1314
 
    
1315
1342
    ## D-Bus methods, signals & properties
1316
1343
    _interface = "se.recompile.Mandos.Client"
1317
1344
    
1604
1631
        self._pipe.send(('setattr', name, value))
1605
1632
 
1606
1633
 
1607
 
class ClientDBusTransitional(ClientDBus):
1608
 
    __metaclass__ = AlternateDBusNamesMetaclass
1609
 
 
1610
 
 
1611
1634
class ClientHandler(socketserver.BaseRequestHandler, object):
1612
1635
    """A class to handle client connections.
1613
1636
    
1741
1764
                    try:
1742
1765
                        sent = session.send(client.secret[sent_size:])
1743
1766
                    except gnutls.errors.GNUTLSError as error:
1744
 
                        logger.warning("gnutls send failed")
 
1767
                        logger.warning("gnutls send failed",
 
1768
                                       exc_info=error)
1745
1769
                        return
1746
1770
                    logger.debug("Sent: %d, remaining: %d",
1747
1771
                                 sent, len(client.secret)
1761
1785
                try:
1762
1786
                    session.bye()
1763
1787
                except gnutls.errors.GNUTLSError as error:
1764
 
                    logger.warning("GnuTLS bye failed")
 
1788
                    logger.warning("GnuTLS bye failed",
 
1789
                                   exc_info=error)
1765
1790
    
1766
1791
    @staticmethod
1767
1792
    def peer_certificate(session):
2079
2104
            elif suffix == "w":
2080
2105
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
2081
2106
            else:
2082
 
                raise ValueError("Unknown suffix %r" % suffix)
 
2107
                raise ValueError("Unknown suffix {0!r}"
 
2108
                                 .format(suffix))
2083
2109
        except (ValueError, IndexError) as e:
2084
2110
            raise ValueError(*(e.args))
2085
2111
        timevalue += delta
2102
2128
        null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2103
2129
        if not stat.S_ISCHR(os.fstat(null).st_mode):
2104
2130
            raise OSError(errno.ENODEV,
2105
 
                          "%s not a character device"
2106
 
                          % os.devnull)
 
2131
                          "{0} not a character device"
 
2132
                          .format(os.devnull))
2107
2133
        os.dup2(null, sys.stdin.fileno())
2108
2134
        os.dup2(null, sys.stdout.fileno())
2109
2135
        os.dup2(null, sys.stderr.fileno())
2118
2144
    
2119
2145
    parser = argparse.ArgumentParser()
2120
2146
    parser.add_argument("-v", "--version", action="version",
2121
 
                        version = "%%(prog)s %s" % version,
 
2147
                        version = "%(prog)s {0}".format(version),
2122
2148
                        help="show version number and exit")
2123
2149
    parser.add_argument("-i", "--interface", metavar="IF",
2124
2150
                        help="Bind to interface IF")
2227
2253
    
2228
2254
    if server_settings["servicename"] != "Mandos":
2229
2255
        syslogger.setFormatter(logging.Formatter
2230
 
                               ('Mandos (%s) [%%(process)d]:'
2231
 
                                ' %%(levelname)s: %%(message)s'
2232
 
                                % server_settings["servicename"]))
 
2256
                               ('Mandos ({0}) [%(process)d]:'
 
2257
                                ' %(levelname)s: %(message)s'
 
2258
                                .format(server_settings
 
2259
                                        ["servicename"])))
2233
2260
    
2234
2261
    # Parse config file with clients
2235
2262
    client_config = configparser.SafeConfigParser(Client
2253
2280
        pidfilename = "/var/run/mandos.pid"
2254
2281
        try:
2255
2282
            pidfile = open(pidfilename, "w")
2256
 
        except IOError:
2257
 
            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)
2258
2286
    
2259
2287
    for name in ("_mandos", "mandos", "nobody"):
2260
2288
        try:
2314
2342
                            ("se.bsnet.fukt.Mandos", bus,
2315
2343
                             do_not_queue=True))
2316
2344
        except dbus.exceptions.NameExistsException as e:
2317
 
            logger.error(unicode(e) + ", disabling D-Bus")
 
2345
            logger.error("Disabling D-Bus:", exc_info=e)
2318
2346
            use_dbus = False
2319
2347
            server_settings["use_dbus"] = False
2320
2348
            tcp_server.use_dbus = False
2332
2360
    
2333
2361
    client_class = Client
2334
2362
    if use_dbus:
2335
 
        client_class = functools.partial(ClientDBusTransitional,
2336
 
                                         bus = bus)
 
2363
        client_class = functools.partial(ClientDBus, bus = bus)
2337
2364
    
2338
2365
    client_settings = Client.config_parser(client_config)
2339
2366
    old_client_settings = {}
2347
2374
                                                     (stored_state))
2348
2375
            os.remove(stored_state_path)
2349
2376
        except IOError as e:
2350
 
            logger.warning("Could not load persistent state: {0}"
2351
 
                           .format(e))
2352
 
            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)
2353
2383
                raise
2354
2384
        except EOFError as e:
2355
2385
            logger.warning("Could not load persistent state: "
2356
 
                           "EOFError: {0}".format(e))
 
2386
                           "EOFError:", exc_info=e)
2357
2387
    
2358
2388
    with PGPEngine() as pgp:
2359
2389
        for client_name, client in clients_data.iteritems():
2412
2442
                             .format(client_name))
2413
2443
                client["secret"] = (
2414
2444
                    client_settings[client_name]["secret"])
2415
 
 
2416
2445
    
2417
2446
    # Add/remove clients based on new changes made to config
2418
2447
    for client_name in (set(old_client_settings)
2421
2450
    for client_name in (set(client_settings)
2422
2451
                        - set(old_client_settings)):
2423
2452
        clients_data[client_name] = client_settings[client_name]
2424
 
 
 
2453
    
2425
2454
    # Create all client objects
2426
2455
    for client_name, client in clients_data.iteritems():
2427
2456
        tcp_server.clients[client_name] = client_class(
2429
2458
    
2430
2459
    if not tcp_server.clients:
2431
2460
        logger.warning("No clients defined")
2432
 
        
 
2461
    
2433
2462
    if not debug:
2434
2463
        try:
2435
2464
            with pidfile:
2449
2478
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2450
2479
    
2451
2480
    if use_dbus:
 
2481
        @alternate_dbus_interfaces({"se.recompile.Mandos":
 
2482
                                        "se.bsnet.fukt.Mandos"})
2452
2483
        class MandosDBusService(DBusObjectWithProperties):
2453
2484
            """A D-Bus proxy object"""
2454
2485
            def __init__(self):
2508
2539
            
2509
2540
            del _interface
2510
2541
        
2511
 
        class MandosDBusServiceTransitional(MandosDBusService):
2512
 
            __metaclass__ = AlternateDBusNamesMetaclass
2513
 
        mandos_dbus_service = MandosDBusServiceTransitional()
 
2542
        mandos_dbus_service = MandosDBusService()
2514
2543
    
2515
2544
    def cleanup():
2516
2545
        "Cleanup function; run on exit"
2557
2586
                pickle.dump((clients, client_settings), stored_state)
2558
2587
            os.rename(tempname, stored_state_path)
2559
2588
        except (IOError, OSError) as e:
2560
 
            logger.warning("Could not save persistent state: {0}"
2561
 
                           .format(e))
2562
2589
            if not debug:
2563
2590
                try:
2564
2591
                    os.remove(tempname)
2565
2592
                except NameError:
2566
2593
                    pass
2567
 
            if e.errno not in set((errno.ENOENT, errno.EACCES,
2568
 
                                   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)
2569
2600
                raise e
2570
2601
        
2571
2602
        # Delete all clients, and settings from config
2599
2630
    service.port = tcp_server.socket.getsockname()[1]
2600
2631
    if use_ipv6:
2601
2632
        logger.info("Now listening on address %r, port %d,"
2602
 
                    " flowinfo %d, scope_id %d"
2603
 
                    % tcp_server.socket.getsockname())
 
2633
                    " flowinfo %d, scope_id %d",
 
2634
                    *tcp_server.socket.getsockname())
2604
2635
    else:                       # IPv4
2605
 
        logger.info("Now listening on address %r, port %d"
2606
 
                    % tcp_server.socket.getsockname())
 
2636
        logger.info("Now listening on address %r, port %d",
 
2637
                    *tcp_server.socket.getsockname())
2607
2638
    
2608
2639
    #service.interface = tcp_server.socket.getsockname()[3]
2609
2640