/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: 2014-08-09 13:12:55 UTC
  • Revision ID: teddy@recompile.se-20140809131255-lp31j98u2pl0xpe6
mandos: Stop using str() and remove unnecessary unicode() calls.

* mandos (if_nametoindex): Use "bytes" literal instead of str().
  (initlogger): Use a unicode string for log device.
  (AvahiError.__unicode__): Removed.
  (DBusPropertyException.__unicode__): - '' -
  (ClientDBus.Secret_dbus_property): Use bytes() instead of str().
  (IPv6_TCPServer.server_bind): Use .encode() instead of str().
  (string_to_delta): Removed unnecessary unicode() call.
  (main): Use "isinstance(x, bytes)" instead of "type(x) is str", use
          .decode() instead of unicode(), and use .encode() instead of
          str().

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