/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-06-23 00:58:49 UTC
  • Revision ID: teddy@recompile.se-20120623005849-02wj82cng433rt2k
* clients.conf: Convert all time intervals to new RFC 3339 syntax.
* mandos: All client options for time intervals now take an RFC 3339
          duration.
  (rfc3339_duration_to_delta): New function.
  (string_to_delta): Try rfc3339_duration_to_delta first.
* mandos-clients.conf.xml (OPTIONS/timeout): Document new format.
  (EXAMPLE): Update to new interval format.
  (SEE ALSO): Reference RFC 3339.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python2.7
 
1
#!/usr/bin/python
2
2
# -*- mode: python; coding: utf-8 -*-
3
3
4
4
# Mandos server - give out binary blobs to connecting clients.
11
11
# "AvahiService" class, and some lines in "main".
12
12
13
13
# Everything else is
14
 
# Copyright © 2008-2014 Teddy Hogeborn
15
 
# Copyright © 2008-2014 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
79
79
import ctypes.util
80
80
import xml.dom.minidom
81
81
import inspect
 
82
import GnuPGInterface
82
83
 
83
84
try:
84
85
    SO_BINDTODEVICE = socket.SO_BINDTODEVICE
88
89
    except ImportError:
89
90
        SO_BINDTODEVICE = None
90
91
 
91
 
version = "1.6.7"
 
92
version = "1.6.0"
92
93
stored_state_file = "clients.pickle"
93
94
 
94
95
logger = logging.getLogger()
95
 
syslogger = None
 
96
syslogger = (logging.handlers.SysLogHandler
 
97
             (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
 
98
              address = str("/dev/log")))
96
99
 
97
100
try:
98
101
    if_nametoindex = (ctypes.cdll.LoadLibrary
114
117
def initlogger(debug, level=logging.WARNING):
115
118
    """init logger and add loglevel"""
116
119
    
117
 
    global syslogger
118
 
    syslogger = (logging.handlers.SysLogHandler
119
 
                 (facility =
120
 
                  logging.handlers.SysLogHandler.LOG_DAEMON,
121
 
                  address = str("/dev/log")))
122
120
    syslogger.setFormatter(logging.Formatter
123
121
                           ('Mandos [%(process)d]: %(levelname)s:'
124
122
                            ' %(message)s'))
142
140
class PGPEngine(object):
143
141
    """A simple class for OpenPGP symmetric encryption & decryption"""
144
142
    def __init__(self):
 
143
        self.gnupg = GnuPGInterface.GnuPG()
145
144
        self.tempdir = tempfile.mkdtemp(prefix="mandos-")
146
 
        self.gnupgargs = ['--batch',
147
 
                          '--home', self.tempdir,
148
 
                          '--force-mdc',
149
 
                          '--quiet',
150
 
                          '--no-use-agent']
 
145
        self.gnupg = GnuPGInterface.GnuPG()
 
146
        self.gnupg.options.meta_interactive = False
 
147
        self.gnupg.options.homedir = self.tempdir
 
148
        self.gnupg.options.extra_args.extend(['--force-mdc',
 
149
                                              '--quiet',
 
150
                                              '--no-use-agent'])
151
151
    
152
152
    def __enter__(self):
153
153
        return self
175
175
    def password_encode(self, password):
176
176
        # Passphrase can not be empty and can not contain newlines or
177
177
        # NUL bytes.  So we prefix it and hex encode it.
178
 
        encoded = b"mandos" + binascii.hexlify(password)
179
 
        if len(encoded) > 2048:
180
 
            # GnuPG can't handle long passwords, so encode differently
181
 
            encoded = (b"mandos" + password.replace(b"\\", b"\\\\")
182
 
                       .replace(b"\n", b"\\n")
183
 
                       .replace(b"\0", b"\\x00"))
184
 
        return encoded
 
178
        return b"mandos" + binascii.hexlify(password)
185
179
    
186
180
    def encrypt(self, data, password):
187
 
        passphrase = self.password_encode(password)
188
 
        with tempfile.NamedTemporaryFile(dir=self.tempdir
189
 
                                         ) as passfile:
190
 
            passfile.write(passphrase)
191
 
            passfile.flush()
192
 
            proc = subprocess.Popen(['gpg', '--symmetric',
193
 
                                     '--passphrase-file',
194
 
                                     passfile.name]
195
 
                                    + self.gnupgargs,
196
 
                                    stdin = subprocess.PIPE,
197
 
                                    stdout = subprocess.PIPE,
198
 
                                    stderr = subprocess.PIPE)
199
 
            ciphertext, err = proc.communicate(input = data)
200
 
        if proc.returncode != 0:
201
 
            raise PGPError(err)
 
181
        self.gnupg.passphrase = self.password_encode(password)
 
182
        with open(os.devnull, "w") as devnull:
 
183
            try:
 
184
                proc = self.gnupg.run(['--symmetric'],
 
185
                                      create_fhs=['stdin', 'stdout'],
 
186
                                      attach_fhs={'stderr': devnull})
 
187
                with contextlib.closing(proc.handles['stdin']) as f:
 
188
                    f.write(data)
 
189
                with contextlib.closing(proc.handles['stdout']) as f:
 
190
                    ciphertext = f.read()
 
191
                proc.wait()
 
192
            except IOError as e:
 
193
                raise PGPError(e)
 
194
        self.gnupg.passphrase = None
202
195
        return ciphertext
203
196
    
204
197
    def decrypt(self, data, password):
205
 
        passphrase = self.password_encode(password)
206
 
        with tempfile.NamedTemporaryFile(dir = self.tempdir
207
 
                                         ) as passfile:
208
 
            passfile.write(passphrase)
209
 
            passfile.flush()
210
 
            proc = subprocess.Popen(['gpg', '--decrypt',
211
 
                                     '--passphrase-file',
212
 
                                     passfile.name]
213
 
                                    + self.gnupgargs,
214
 
                                    stdin = subprocess.PIPE,
215
 
                                    stdout = subprocess.PIPE,
216
 
                                    stderr = subprocess.PIPE)
217
 
            decrypted_plaintext, err = proc.communicate(input
218
 
                                                        = data)
219
 
        if proc.returncode != 0:
220
 
            raise PGPError(err)
 
198
        self.gnupg.passphrase = self.password_encode(password)
 
199
        with open(os.devnull, "w") as devnull:
 
200
            try:
 
201
                proc = self.gnupg.run(['--decrypt'],
 
202
                                      create_fhs=['stdin', 'stdout'],
 
203
                                      attach_fhs={'stderr': devnull})
 
204
                with contextlib.closing(proc.handles['stdin']) as f:
 
205
                    f.write(data)
 
206
                with contextlib.closing(proc.handles['stdout']) as f:
 
207
                    decrypted_plaintext = f.read()
 
208
                proc.wait()
 
209
            except IOError as e:
 
210
                raise PGPError(e)
 
211
        self.gnupg.passphrase = None
221
212
        return decrypted_plaintext
222
213
 
223
214
 
243
234
               Used to optionally bind to the specified interface.
244
235
    name: string; Example: 'Mandos'
245
236
    type: string; Example: '_mandos._tcp'.
246
 
     See <https://www.iana.org/assignments/service-names-port-numbers>
 
237
                  See <http://www.dns-sd.org/ServiceTypes.html>
247
238
    port: integer; what port to announce
248
239
    TXT: list of strings; TXT record for the service
249
240
    domain: string; Domain to publish on, default to .local if empty.
338
329
        elif state == avahi.ENTRY_GROUP_FAILURE:
339
330
            logger.critical("Avahi: Error in group state changed %s",
340
331
                            unicode(error))
341
 
            raise AvahiGroupError("State changed: {!s}"
 
332
            raise AvahiGroupError("State changed: {0!s}"
342
333
                                  .format(error))
343
334
    
344
335
    def cleanup(self):
395
386
        """Add the new name to the syslog messages"""
396
387
        ret = AvahiService.rename(self)
397
388
        syslogger.setFormatter(logging.Formatter
398
 
                               ('Mandos ({}) [%(process)d]:'
 
389
                               ('Mandos ({0}) [%(process)d]:'
399
390
                                ' %(levelname)s: %(message)s'
400
391
                                .format(self.name)))
401
392
        return ret
402
393
 
403
394
 
 
395
def timedelta_to_milliseconds(td):
 
396
    "Convert a datetime.timedelta() to milliseconds"
 
397
    return ((td.days * 24 * 60 * 60 * 1000)
 
398
            + (td.seconds * 1000)
 
399
            + (td.microseconds // 1000))
 
400
 
 
401
 
404
402
class Client(object):
405
403
    """A representation of a client host served by this server.
406
404
    
442
440
    runtime_expansions: Allowed attributes for runtime expansion.
443
441
    expires:    datetime.datetime(); time (UTC) when a client will be
444
442
                disabled, or None
445
 
    server_settings: The server_settings dict from main()
446
443
    """
447
444
    
448
445
    runtime_expansions = ("approval_delay", "approval_duration",
461
458
                        "enabled": "True",
462
459
                        }
463
460
    
 
461
    def timeout_milliseconds(self):
 
462
        "Return the 'timeout' attribute in milliseconds"
 
463
        return timedelta_to_milliseconds(self.timeout)
 
464
    
 
465
    def extended_timeout_milliseconds(self):
 
466
        "Return the 'extended_timeout' attribute in milliseconds"
 
467
        return timedelta_to_milliseconds(self.extended_timeout)
 
468
    
 
469
    def interval_milliseconds(self):
 
470
        "Return the 'interval' attribute in milliseconds"
 
471
        return timedelta_to_milliseconds(self.interval)
 
472
    
 
473
    def approval_delay_milliseconds(self):
 
474
        return timedelta_to_milliseconds(self.approval_delay)
 
475
    
464
476
    @staticmethod
465
477
    def config_parser(config):
466
478
        """Construct a new dict of client settings of this form:
491
503
                          "rb") as secfile:
492
504
                    client["secret"] = secfile.read()
493
505
            else:
494
 
                raise TypeError("No secret or secfile for section {}"
 
506
                raise TypeError("No secret or secfile for section {0}"
495
507
                                .format(section))
496
508
            client["timeout"] = string_to_delta(section["timeout"])
497
509
            client["extended_timeout"] = string_to_delta(
508
520
        
509
521
        return settings
510
522
    
511
 
    def __init__(self, settings, name = None, server_settings=None):
 
523
    def __init__(self, settings, name = None):
512
524
        self.name = name
513
 
        if server_settings is None:
514
 
            server_settings = {}
515
 
        self.server_settings = server_settings
516
525
        # adding all client settings
517
 
        for setting, value in settings.items():
 
526
        for setting, value in settings.iteritems():
518
527
            setattr(self, setting, value)
519
528
        
520
529
        if self.enabled:
603
612
        if self.checker_initiator_tag is not None:
604
613
            gobject.source_remove(self.checker_initiator_tag)
605
614
        self.checker_initiator_tag = (gobject.timeout_add
606
 
                                      (int(self.interval
607
 
                                           .total_seconds() * 1000),
 
615
                                      (self.interval_milliseconds(),
608
616
                                       self.start_checker))
609
617
        # Schedule a disable() when 'timeout' has passed
610
618
        if self.disable_initiator_tag is not None:
611
619
            gobject.source_remove(self.disable_initiator_tag)
612
620
        self.disable_initiator_tag = (gobject.timeout_add
613
 
                                      (int(self.timeout
614
 
                                           .total_seconds() * 1000),
615
 
                                       self.disable))
 
621
                                   (self.timeout_milliseconds(),
 
622
                                    self.disable))
616
623
        # Also start a new checker *right now*.
617
624
        self.start_checker()
618
625
    
649
656
            self.disable_initiator_tag = None
650
657
        if getattr(self, "enabled", False):
651
658
            self.disable_initiator_tag = (gobject.timeout_add
652
 
                                          (int(timeout.total_seconds()
653
 
                                               * 1000), self.disable))
 
659
                                          (timedelta_to_milliseconds
 
660
                                           (timeout), self.disable))
654
661
            self.expires = datetime.datetime.utcnow() + timeout
655
662
    
656
663
    def need_approval(self):
673
680
        # If a checker exists, make sure it is not a zombie
674
681
        try:
675
682
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
676
 
        except AttributeError:
677
 
            pass
678
 
        except OSError as error:
679
 
            if error.errno != errno.ECHILD:
680
 
                raise
 
683
        except (AttributeError, OSError) as error:
 
684
            if (isinstance(error, OSError)
 
685
                and error.errno != errno.ECHILD):
 
686
                raise error
681
687
        else:
682
688
            if pid:
683
689
                logger.warning("Checker was a zombie")
687
693
        # Start a new checker if needed
688
694
        if self.checker is None:
689
695
            # Escape attributes for the shell
690
 
            escaped_attrs = { attr:
691
 
                                  re.escape(unicode(getattr(self,
692
 
                                                            attr)))
693
 
                              for attr in self.runtime_expansions }
 
696
            escaped_attrs = dict(
 
697
                (attr, re.escape(unicode(getattr(self, attr))))
 
698
                for attr in
 
699
                self.runtime_expansions)
694
700
            try:
695
701
                command = self.checker_command % escaped_attrs
696
702
            except TypeError as error:
705
711
                # in normal mode, that is already done by daemon(),
706
712
                # and in debug mode we don't want to.  (Stdin is
707
713
                # always replaced by /dev/null.)
708
 
                # The exception is when not debugging but nevertheless
709
 
                # running in the foreground; use the previously
710
 
                # created wnull.
711
 
                popen_args = {}
712
 
                if (not self.server_settings["debug"]
713
 
                    and self.server_settings["foreground"]):
714
 
                    popen_args.update({"stdout": wnull,
715
 
                                       "stderr": wnull })
716
714
                self.checker = subprocess.Popen(command,
717
715
                                                close_fds=True,
718
 
                                                shell=True, cwd="/",
719
 
                                                **popen_args)
 
716
                                                shell=True, cwd="/")
720
717
            except OSError as error:
721
718
                logger.error("Failed to start subprocess",
722
719
                             exc_info=error)
723
 
                return True
724
720
            self.checker_callback_tag = (gobject.child_watch_add
725
721
                                         (self.checker.pid,
726
722
                                          self.checker_callback,
727
723
                                          data=command))
728
724
            # The checker may have completed before the gobject
729
725
            # watch was added.  Check for this.
730
 
            try:
731
 
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
732
 
            except OSError as error:
733
 
                if error.errno == errno.ECHILD:
734
 
                    # This should never happen
735
 
                    logger.error("Child process vanished",
736
 
                                 exc_info=error)
737
 
                    return True
738
 
                raise
 
726
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
739
727
            if pid:
740
728
                gobject.source_remove(self.checker_callback_tag)
741
729
                self.checker_callback(pid, status, command)
777
765
    # "Set" method, so we fail early here:
778
766
    if byte_arrays and signature != "ay":
779
767
        raise ValueError("Byte arrays not supported for non-'ay'"
780
 
                         " signature {!r}".format(signature))
 
768
                         " signature {0!r}".format(signature))
781
769
    def decorator(func):
782
770
        func._dbus_is_property = True
783
771
        func._dbus_interface = dbus_interface
862
850
        If called like _is_dbus_thing("method") it returns a function
863
851
        suitable for use as predicate to inspect.getmembers().
864
852
        """
865
 
        return lambda obj: getattr(obj, "_dbus_is_{}".format(thing),
 
853
        return lambda obj: getattr(obj, "_dbus_is_{0}".format(thing),
866
854
                                   False)
867
855
    
868
856
    def _get_all_dbus_things(self, thing):
917
905
            # The byte_arrays option is not supported yet on
918
906
            # signatures other than "ay".
919
907
            if prop._dbus_signature != "ay":
920
 
                raise ValueError("Byte arrays not supported for non-"
921
 
                                 "'ay' signature {!r}"
922
 
                                 .format(prop._dbus_signature))
 
908
                raise ValueError
923
909
            value = dbus.ByteArray(b''.join(chr(byte)
924
910
                                            for byte in value))
925
911
        prop(value)
989
975
                                              (prop,
990
976
                                               "_dbus_annotations",
991
977
                                               {}))
992
 
                        for name, value in annots.items():
 
978
                        for name, value in annots.iteritems():
993
979
                            ann_tag = document.createElement(
994
980
                                "annotation")
995
981
                            ann_tag.setAttribute("name", name)
998
984
                # Add interface annotation tags
999
985
                for annotation, value in dict(
1000
986
                    itertools.chain.from_iterable(
1001
 
                        annotations().items()
 
987
                        annotations().iteritems()
1002
988
                        for name, annotations in
1003
989
                        self._get_all_dbus_things("interface")
1004
990
                        if name == if_tag.getAttribute("name")
1005
 
                        )).items():
 
991
                        )).iteritems():
1006
992
                    ann_tag = document.createElement("annotation")
1007
993
                    ann_tag.setAttribute("name", annotation)
1008
994
                    ann_tag.setAttribute("value", value)
1064
1050
    """
1065
1051
    def wrapper(cls):
1066
1052
        for orig_interface_name, alt_interface_name in (
1067
 
            alt_interface_names.items()):
 
1053
            alt_interface_names.iteritems()):
1068
1054
            attr = {}
1069
1055
            interface_names = set()
1070
1056
            # Go though all attributes of the class
1083
1069
                interface_names.add(alt_interface)
1084
1070
                # Is this a D-Bus signal?
1085
1071
                if getattr(attribute, "_dbus_is_signal", False):
1086
 
                    # Extract the original non-method undecorated
1087
 
                    # function by black magic
 
1072
                    # Extract the original non-method function by
 
1073
                    # black magic
1088
1074
                    nonmethod_func = (dict(
1089
1075
                            zip(attribute.func_code.co_freevars,
1090
1076
                                attribute.__closure__))["func"]
1187
1173
                                        attribute.func_closure)))
1188
1174
            if deprecate:
1189
1175
                # Deprecate all alternate interfaces
1190
 
                iname="_AlternateDBusNames_interface_annotation{}"
 
1176
                iname="_AlternateDBusNames_interface_annotation{0}"
1191
1177
                for interface_name in interface_names:
1192
1178
                    @dbus_interface_annotations(interface_name)
1193
1179
                    def func(self):
1202
1188
            if interface_names:
1203
1189
                # Replace the class with a new subclass of it with
1204
1190
                # methods, signals, etc. as created above.
1205
 
                cls = type(b"{}Alternate".format(cls.__name__),
 
1191
                cls = type(b"{0}Alternate".format(cls.__name__),
1206
1192
                           (cls,), attr)
1207
1193
        return cls
1208
1194
    return wrapper
1249
1235
                   to the D-Bus.  Default: no transform
1250
1236
        variant_level: D-Bus variant level.  Default: 1
1251
1237
        """
1252
 
        attrname = "_{}".format(dbus_name)
 
1238
        attrname = "_{0}".format(dbus_name)
1253
1239
        def setter(self, value):
1254
1240
            if hasattr(self, "dbus_object_path"):
1255
1241
                if (not hasattr(self, attrname) or
1285
1271
    approval_delay = notifychangeproperty(dbus.UInt64,
1286
1272
                                          "ApprovalDelay",
1287
1273
                                          type_func =
1288
 
                                          lambda td: td.total_seconds()
1289
 
                                          * 1000)
 
1274
                                          timedelta_to_milliseconds)
1290
1275
    approval_duration = notifychangeproperty(
1291
1276
        dbus.UInt64, "ApprovalDuration",
1292
 
        type_func = lambda td: td.total_seconds() * 1000)
 
1277
        type_func = timedelta_to_milliseconds)
1293
1278
    host = notifychangeproperty(dbus.String, "Host")
1294
1279
    timeout = notifychangeproperty(dbus.UInt64, "Timeout",
1295
 
                                   type_func = lambda td:
1296
 
                                       td.total_seconds() * 1000)
 
1280
                                   type_func =
 
1281
                                   timedelta_to_milliseconds)
1297
1282
    extended_timeout = notifychangeproperty(
1298
1283
        dbus.UInt64, "ExtendedTimeout",
1299
 
        type_func = lambda td: td.total_seconds() * 1000)
 
1284
        type_func = timedelta_to_milliseconds)
1300
1285
    interval = notifychangeproperty(dbus.UInt64,
1301
1286
                                    "Interval",
1302
1287
                                    type_func =
1303
 
                                    lambda td: td.total_seconds()
1304
 
                                    * 1000)
 
1288
                                    timedelta_to_milliseconds)
1305
1289
    checker_command = notifychangeproperty(dbus.String, "Checker")
1306
1290
    
1307
1291
    del notifychangeproperty
1335
1319
                                       *args, **kwargs)
1336
1320
    
1337
1321
    def start_checker(self, *args, **kwargs):
1338
 
        old_checker_pid = getattr(self.checker, "pid", None)
 
1322
        old_checker = self.checker
 
1323
        if self.checker is not None:
 
1324
            old_checker_pid = self.checker.pid
 
1325
        else:
 
1326
            old_checker_pid = None
1339
1327
        r = Client.start_checker(self, *args, **kwargs)
1340
1328
        # Only if new checker process was started
1341
1329
        if (self.checker is not None
1350
1338
    
1351
1339
    def approve(self, value=True):
1352
1340
        self.approved = value
1353
 
        gobject.timeout_add(int(self.approval_duration.total_seconds()
1354
 
                                * 1000), self._reset_approved)
 
1341
        gobject.timeout_add(timedelta_to_milliseconds
 
1342
                            (self.approval_duration),
 
1343
                            self._reset_approved)
1355
1344
        self.send_changedstate()
1356
1345
    
1357
1346
    ## D-Bus methods, signals & properties
1460
1449
                           access="readwrite")
1461
1450
    def ApprovalDelay_dbus_property(self, value=None):
1462
1451
        if value is None:       # get
1463
 
            return dbus.UInt64(self.approval_delay.total_seconds()
1464
 
                               * 1000)
 
1452
            return dbus.UInt64(self.approval_delay_milliseconds())
1465
1453
        self.approval_delay = datetime.timedelta(0, 0, 0, value)
1466
1454
    
1467
1455
    # ApprovalDuration - property
1469
1457
                           access="readwrite")
1470
1458
    def ApprovalDuration_dbus_property(self, value=None):
1471
1459
        if value is None:       # get
1472
 
            return dbus.UInt64(self.approval_duration.total_seconds()
1473
 
                               * 1000)
 
1460
            return dbus.UInt64(timedelta_to_milliseconds(
 
1461
                    self.approval_duration))
1474
1462
        self.approval_duration = datetime.timedelta(0, 0, 0, value)
1475
1463
    
1476
1464
    # Name - property
1542
1530
                           access="readwrite")
1543
1531
    def Timeout_dbus_property(self, value=None):
1544
1532
        if value is None:       # get
1545
 
            return dbus.UInt64(self.timeout.total_seconds() * 1000)
 
1533
            return dbus.UInt64(self.timeout_milliseconds())
1546
1534
        old_timeout = self.timeout
1547
1535
        self.timeout = datetime.timedelta(0, 0, 0, value)
1548
1536
        # Reschedule disabling
1559
1547
                gobject.source_remove(self.disable_initiator_tag)
1560
1548
                self.disable_initiator_tag = (
1561
1549
                    gobject.timeout_add(
1562
 
                        int((self.expires - now).total_seconds()
1563
 
                            * 1000), self.disable))
 
1550
                        timedelta_to_milliseconds(self.expires - now),
 
1551
                        self.disable))
1564
1552
    
1565
1553
    # ExtendedTimeout - property
1566
1554
    @dbus_service_property(_interface, signature="t",
1567
1555
                           access="readwrite")
1568
1556
    def ExtendedTimeout_dbus_property(self, value=None):
1569
1557
        if value is None:       # get
1570
 
            return dbus.UInt64(self.extended_timeout.total_seconds()
1571
 
                               * 1000)
 
1558
            return dbus.UInt64(self.extended_timeout_milliseconds())
1572
1559
        self.extended_timeout = datetime.timedelta(0, 0, 0, value)
1573
1560
    
1574
1561
    # Interval - property
1576
1563
                           access="readwrite")
1577
1564
    def Interval_dbus_property(self, value=None):
1578
1565
        if value is None:       # get
1579
 
            return dbus.UInt64(self.interval.total_seconds() * 1000)
 
1566
            return dbus.UInt64(self.interval_milliseconds())
1580
1567
        self.interval = datetime.timedelta(0, 0, 0, value)
1581
1568
        if getattr(self, "checker_initiator_tag", None) is None:
1582
1569
            return
1687
1674
            logger.debug("Protocol version: %r", line)
1688
1675
            try:
1689
1676
                if int(line.strip().split()[0]) > 1:
1690
 
                    raise RuntimeError(line)
 
1677
                    raise RuntimeError
1691
1678
            except (ValueError, IndexError, RuntimeError) as error:
1692
1679
                logger.error("Unknown protocol version: %s", error)
1693
1680
                return
1742
1729
                        if self.server.use_dbus:
1743
1730
                            # Emit D-Bus signal
1744
1731
                            client.NeedApproval(
1745
 
                                client.approval_delay.total_seconds()
1746
 
                                * 1000, client.approved_by_default)
 
1732
                                client.approval_delay_milliseconds(),
 
1733
                                client.approved_by_default)
1747
1734
                    else:
1748
1735
                        logger.warning("Client %s was not approved",
1749
1736
                                       client.name)
1755
1742
                    #wait until timeout or approved
1756
1743
                    time = datetime.datetime.now()
1757
1744
                    client.changedstate.acquire()
1758
 
                    client.changedstate.wait(delay.total_seconds())
 
1745
                    client.changedstate.wait(
 
1746
                        float(timedelta_to_milliseconds(delay)
 
1747
                              / 1000))
1759
1748
                    client.changedstate.release()
1760
1749
                    time2 = datetime.datetime.now()
1761
1750
                    if (time2 - time) >= delay:
1898
1887
    
1899
1888
    def add_pipe(self, parent_pipe, proc):
1900
1889
        """Dummy function; override as necessary"""
1901
 
        raise NotImplementedError()
 
1890
        raise NotImplementedError
1902
1891
 
1903
1892
 
1904
1893
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1980
1969
                if self.address_family == socket.AF_INET6:
1981
1970
                    any_address = "::" # in6addr_any
1982
1971
                else:
1983
 
                    any_address = "0.0.0.0" # INADDR_ANY
 
1972
                    any_address = socket.INADDR_ANY
1984
1973
                self.server_address = (any_address,
1985
1974
                                       self.server_address[1])
1986
1975
            elif not self.server_address[1]:
2169
2158
    token_duration = Token(re.compile(r"P"), None,
2170
2159
                           frozenset((token_year, token_month,
2171
2160
                                      token_day, token_time,
2172
 
                                      token_week)))
 
2161
                                      token_week))),
2173
2162
    # Define starting values
2174
2163
    value = datetime.timedelta() # Value so far
2175
2164
    found_token = None
2176
 
    followers = frozenset((token_duration,)) # Following valid tokens
 
2165
    followers = frozenset(token_duration,) # Following valid tokens
2177
2166
    s = duration                # String left to parse
2178
2167
    # Loop until end token is found
2179
2168
    while found_token is not token_end:
2239
2228
            elif suffix == "w":
2240
2229
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
2241
2230
            else:
2242
 
                raise ValueError("Unknown suffix {!r}"
 
2231
                raise ValueError("Unknown suffix {0!r}"
2243
2232
                                 .format(suffix))
2244
 
        except IndexError as e:
 
2233
        except (ValueError, IndexError) as e:
2245
2234
            raise ValueError(*(e.args))
2246
2235
        timevalue += delta
2247
2236
    return timevalue
2262
2251
        # Close all standard open file descriptors
2263
2252
        null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2264
2253
        if not stat.S_ISCHR(os.fstat(null).st_mode):
2265
 
            raise OSError(errno.ENODEV, "{} not a character device"
 
2254
            raise OSError(errno.ENODEV,
 
2255
                          "{0} not a character device"
2266
2256
                          .format(os.devnull))
2267
2257
        os.dup2(null, sys.stdin.fileno())
2268
2258
        os.dup2(null, sys.stdout.fileno())
2278
2268
    
2279
2269
    parser = argparse.ArgumentParser()
2280
2270
    parser.add_argument("-v", "--version", action="version",
2281
 
                        version = "%(prog)s {}".format(version),
 
2271
                        version = "%(prog)s {0}".format(version),
2282
2272
                        help="show version number and exit")
2283
2273
    parser.add_argument("-i", "--interface", metavar="IF",
2284
2274
                        help="Bind to interface IF")
2290
2280
                        help="Run self-test")
2291
2281
    parser.add_argument("--debug", action="store_true",
2292
2282
                        help="Debug mode; run in foreground and log"
2293
 
                        " to terminal", default=None)
 
2283
                        " to terminal")
2294
2284
    parser.add_argument("--debuglevel", metavar="LEVEL",
2295
2285
                        help="Debug level for stdout output")
2296
2286
    parser.add_argument("--priority", help="GnuTLS"
2303
2293
                        " files")
2304
2294
    parser.add_argument("--no-dbus", action="store_false",
2305
2295
                        dest="use_dbus", help="Do not provide D-Bus"
2306
 
                        " system bus interface", default=None)
 
2296
                        " system bus interface")
2307
2297
    parser.add_argument("--no-ipv6", action="store_false",
2308
 
                        dest="use_ipv6", help="Do not use IPv6",
2309
 
                        default=None)
 
2298
                        dest="use_ipv6", help="Do not use IPv6")
2310
2299
    parser.add_argument("--no-restore", action="store_false",
2311
2300
                        dest="restore", help="Do not restore stored"
2312
 
                        " state", default=None)
 
2301
                        " state")
2313
2302
    parser.add_argument("--socket", type=int,
2314
2303
                        help="Specify a file descriptor to a network"
2315
2304
                        " socket to use instead of creating one")
2316
2305
    parser.add_argument("--statedir", metavar="DIR",
2317
2306
                        help="Directory to save/restore state in")
2318
2307
    parser.add_argument("--foreground", action="store_true",
2319
 
                        help="Run in foreground", default=None)
2320
 
    parser.add_argument("--no-zeroconf", action="store_false",
2321
 
                        dest="zeroconf", help="Do not use Zeroconf",
2322
 
                        default=None)
 
2308
                        help="Run in foreground")
2323
2309
    
2324
2310
    options = parser.parse_args()
2325
2311
    
2326
2312
    if options.check:
2327
2313
        import doctest
2328
 
        fail_count, test_count = doctest.testmod()
2329
 
        sys.exit(os.EX_OK if fail_count == 0 else 1)
 
2314
        doctest.testmod()
 
2315
        sys.exit()
2330
2316
    
2331
2317
    # Default values for config file for server-global settings
2332
2318
    server_defaults = { "interface": "",
2334
2320
                        "port": "",
2335
2321
                        "debug": "False",
2336
2322
                        "priority":
2337
 
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
 
2323
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2338
2324
                        "servicename": "Mandos",
2339
2325
                        "use_dbus": "True",
2340
2326
                        "use_ipv6": "True",
2343
2329
                        "socket": "",
2344
2330
                        "statedir": "/var/lib/mandos",
2345
2331
                        "foreground": "False",
2346
 
                        "zeroconf": "True",
2347
2332
                        }
2348
2333
    
2349
2334
    # Parse config file for server-global settings
2376
2361
    for option in ("interface", "address", "port", "debug",
2377
2362
                   "priority", "servicename", "configdir",
2378
2363
                   "use_dbus", "use_ipv6", "debuglevel", "restore",
2379
 
                   "statedir", "socket", "foreground", "zeroconf"):
 
2364
                   "statedir", "socket", "foreground"):
2380
2365
        value = getattr(options, option)
2381
2366
        if value is not None:
2382
2367
            server_settings[option] = value
2385
2370
    for option in server_settings.keys():
2386
2371
        if type(server_settings[option]) is str:
2387
2372
            server_settings[option] = unicode(server_settings[option])
2388
 
    # Force all boolean options to be boolean
2389
 
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
2390
 
                   "foreground", "zeroconf"):
2391
 
        server_settings[option] = bool(server_settings[option])
2392
2373
    # Debug implies foreground
2393
2374
    if server_settings["debug"]:
2394
2375
        server_settings["foreground"] = True
2396
2377
    
2397
2378
    ##################################################################
2398
2379
    
2399
 
    if (not server_settings["zeroconf"] and
2400
 
        not (server_settings["port"]
2401
 
             or server_settings["socket"] != "")):
2402
 
            parser.error("Needs port or socket to work without"
2403
 
                         " Zeroconf")
2404
 
    
2405
2380
    # For convenience
2406
2381
    debug = server_settings["debug"]
2407
2382
    debuglevel = server_settings["debuglevel"]
2410
2385
    stored_state_path = os.path.join(server_settings["statedir"],
2411
2386
                                     stored_state_file)
2412
2387
    foreground = server_settings["foreground"]
2413
 
    zeroconf = server_settings["zeroconf"]
2414
2388
    
2415
2389
    if debug:
2416
2390
        initlogger(debug, logging.DEBUG)
2423
2397
    
2424
2398
    if server_settings["servicename"] != "Mandos":
2425
2399
        syslogger.setFormatter(logging.Formatter
2426
 
                               ('Mandos ({}) [%(process)d]:'
 
2400
                               ('Mandos ({0}) [%(process)d]:'
2427
2401
                                ' %(levelname)s: %(message)s'
2428
2402
                                .format(server_settings
2429
2403
                                        ["servicename"])))
2437
2411
    global mandos_dbus_service
2438
2412
    mandos_dbus_service = None
2439
2413
    
2440
 
    socketfd = None
2441
 
    if server_settings["socket"] != "":
2442
 
        socketfd = server_settings["socket"]
2443
2414
    tcp_server = MandosServer((server_settings["address"],
2444
2415
                               server_settings["port"]),
2445
2416
                              ClientHandler,
2449
2420
                              gnutls_priority=
2450
2421
                              server_settings["priority"],
2451
2422
                              use_dbus=use_dbus,
2452
 
                              socketfd=socketfd)
 
2423
                              socketfd=(server_settings["socket"]
 
2424
                                        or None))
2453
2425
    if not foreground:
2454
 
        pidfilename = "/run/mandos.pid"
2455
 
        if not os.path.isdir("/run/."):
2456
 
            pidfilename = "/var/run/mandos.pid"
 
2426
        pidfilename = "/var/run/mandos.pid"
2457
2427
        pidfile = None
2458
2428
        try:
2459
2429
            pidfile = open(pidfilename, "w")
2476
2446
        os.setuid(uid)
2477
2447
    except OSError as error:
2478
2448
        if error.errno != errno.EPERM:
2479
 
            raise
 
2449
            raise error
2480
2450
    
2481
2451
    if debug:
2482
2452
        # Enable all possible GnuTLS debugging
2525
2495
            use_dbus = False
2526
2496
            server_settings["use_dbus"] = False
2527
2497
            tcp_server.use_dbus = False
2528
 
    if zeroconf:
2529
 
        protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2530
 
        service = AvahiServiceToSyslog(name =
2531
 
                                       server_settings["servicename"],
2532
 
                                       servicetype = "_mandos._tcp",
2533
 
                                       protocol = protocol, bus = bus)
2534
 
        if server_settings["interface"]:
2535
 
            service.interface = (if_nametoindex
2536
 
                                 (str(server_settings["interface"])))
 
2498
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
 
2499
    service = AvahiServiceToSyslog(name =
 
2500
                                   server_settings["servicename"],
 
2501
                                   servicetype = "_mandos._tcp",
 
2502
                                   protocol = protocol, bus = bus)
 
2503
    if server_settings["interface"]:
 
2504
        service.interface = (if_nametoindex
 
2505
                             (str(server_settings["interface"])))
2537
2506
    
2538
2507
    global multiprocessing_manager
2539
2508
    multiprocessing_manager = multiprocessing.Manager()
2546
2515
    old_client_settings = {}
2547
2516
    clients_data = {}
2548
2517
    
2549
 
    # This is used to redirect stdout and stderr for checker processes
2550
 
    global wnull
2551
 
    wnull = open(os.devnull, "w") # A writable /dev/null
2552
 
    # Only used if server is running in foreground but not in debug
2553
 
    # mode
2554
 
    if debug or not foreground:
2555
 
        wnull.close()
2556
 
    
2557
2518
    # Get client data and settings from last running state.
2558
2519
    if server_settings["restore"]:
2559
2520
        try:
2563
2524
            os.remove(stored_state_path)
2564
2525
        except IOError as e:
2565
2526
            if e.errno == errno.ENOENT:
2566
 
                logger.warning("Could not load persistent state: {}"
 
2527
                logger.warning("Could not load persistent state: {0}"
2567
2528
                                .format(os.strerror(e.errno)))
2568
2529
            else:
2569
2530
                logger.critical("Could not load persistent state:",
2574
2535
                           "EOFError:", exc_info=e)
2575
2536
    
2576
2537
    with PGPEngine() as pgp:
2577
 
        for client_name, client in clients_data.items():
2578
 
            # Skip removed clients
2579
 
            if client_name not in client_settings:
2580
 
                continue
2581
 
            
 
2538
        for client_name, client in clients_data.iteritems():
2582
2539
            # Decide which value to use after restoring saved state.
2583
2540
            # We have three different values: Old config file,
2584
2541
            # new config file, and saved state.
2605
2562
                if datetime.datetime.utcnow() >= client["expires"]:
2606
2563
                    if not client["last_checked_ok"]:
2607
2564
                        logger.warning(
2608
 
                            "disabling client {} - Client never "
 
2565
                            "disabling client {0} - Client never "
2609
2566
                            "performed a successful checker"
2610
2567
                            .format(client_name))
2611
2568
                        client["enabled"] = False
2612
2569
                    elif client["last_checker_status"] != 0:
2613
2570
                        logger.warning(
2614
 
                            "disabling client {} - Client last"
2615
 
                            " checker failed with error code {}"
 
2571
                            "disabling client {0} - Client "
 
2572
                            "last checker failed with error code {1}"
2616
2573
                            .format(client_name,
2617
2574
                                    client["last_checker_status"]))
2618
2575
                        client["enabled"] = False
2621
2578
                                             .utcnow()
2622
2579
                                             + client["timeout"])
2623
2580
                        logger.debug("Last checker succeeded,"
2624
 
                                     " keeping {} enabled"
 
2581
                                     " keeping {0} enabled"
2625
2582
                                     .format(client_name))
2626
2583
            try:
2627
2584
                client["secret"] = (
2630
2587
                                ["secret"]))
2631
2588
            except PGPError:
2632
2589
                # If decryption fails, we use secret from new settings
2633
 
                logger.debug("Failed to decrypt {} old secret"
 
2590
                logger.debug("Failed to decrypt {0} old secret"
2634
2591
                             .format(client_name))
2635
2592
                client["secret"] = (
2636
2593
                    client_settings[client_name]["secret"])
2644
2601
        clients_data[client_name] = client_settings[client_name]
2645
2602
    
2646
2603
    # Create all client objects
2647
 
    for client_name, client in clients_data.items():
 
2604
    for client_name, client in clients_data.iteritems():
2648
2605
        tcp_server.clients[client_name] = client_class(
2649
 
            name = client_name, settings = client,
2650
 
            server_settings = server_settings)
 
2606
            name = client_name, settings = client)
2651
2607
    
2652
2608
    if not tcp_server.clients:
2653
2609
        logger.warning("No clients defined")
2733
2689
    
2734
2690
    def cleanup():
2735
2691
        "Cleanup function; run on exit"
2736
 
        if zeroconf:
2737
 
            service.cleanup()
 
2692
        service.cleanup()
2738
2693
        
2739
2694
        multiprocessing.active_children()
2740
 
        wnull.close()
2741
2695
        if not (tcp_server.clients or client_settings):
2742
2696
            return
2743
2697
        
2754
2708
                
2755
2709
                # A list of attributes that can not be pickled
2756
2710
                # + secret.
2757
 
                exclude = { "bus", "changedstate", "secret",
2758
 
                            "checker", "server_settings" }
 
2711
                exclude = set(("bus", "changedstate", "secret",
 
2712
                               "checker"))
2759
2713
                for name, typ in (inspect.getmembers
2760
2714
                                  (dbus.service.Object)):
2761
2715
                    exclude.add(name)
2784
2738
                except NameError:
2785
2739
                    pass
2786
2740
            if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
2787
 
                logger.warning("Could not save persistent state: {}"
 
2741
                logger.warning("Could not save persistent state: {0}"
2788
2742
                               .format(os.strerror(e.errno)))
2789
2743
            else:
2790
2744
                logger.warning("Could not save persistent state:",
2791
2745
                               exc_info=e)
2792
 
                raise
 
2746
                raise e
2793
2747
        
2794
2748
        # Delete all clients, and settings from config
2795
2749
        while tcp_server.clients:
2819
2773
    tcp_server.server_activate()
2820
2774
    
2821
2775
    # Find out what port we got
2822
 
    if zeroconf:
2823
 
        service.port = tcp_server.socket.getsockname()[1]
 
2776
    service.port = tcp_server.socket.getsockname()[1]
2824
2777
    if use_ipv6:
2825
2778
        logger.info("Now listening on address %r, port %d,"
2826
2779
                    " flowinfo %d, scope_id %d",
2832
2785
    #service.interface = tcp_server.socket.getsockname()[3]
2833
2786
    
2834
2787
    try:
2835
 
        if zeroconf:
2836
 
            # From the Avahi example code
2837
 
            try:
2838
 
                service.activate()
2839
 
            except dbus.exceptions.DBusException as error:
2840
 
                logger.critical("D-Bus Exception", exc_info=error)
2841
 
                cleanup()
2842
 
                sys.exit(1)
2843
 
            # End of Avahi example code
 
2788
        # From the Avahi example code
 
2789
        try:
 
2790
            service.activate()
 
2791
        except dbus.exceptions.DBusException as error:
 
2792
            logger.critical("D-Bus Exception", exc_info=error)
 
2793
            cleanup()
 
2794
            sys.exit(1)
 
2795
        # End of Avahi example code
2844
2796
        
2845
2797
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2846
2798
                             lambda *args, **kwargs: