/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

  • Committer: Teddy Hogeborn
  • Date: 2014-08-09 13:18:46 UTC
  • mto: (237.7.304 trunk)
  • mto: This revision was merged to the branch mainline in revision 323.
  • Revision ID: teddy@recompile.se-20140809131846-2emd272mf9c9wihe
mandos: Replace unicode() with str().

This is in preparation for the coming Python 3 conversion.

* mandos: Replace "str" with "unicode".  All callers changed.

Show diffs side-by-side

added added

removed removed

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