/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-10-08 21:03:05 UTC
  • Revision ID: teddy@recompile.se-20141008210305-hhizwsh7eadi3gsv
Update Debian package standards version to 3.9.6.

* debian/control (Standards-Version): Changed to "3.9.6".

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
68
68
import binascii
69
69
import tempfile
70
70
import itertools
 
71
import collections
71
72
 
72
73
import dbus
73
74
import dbus.service
78
79
import ctypes.util
79
80
import xml.dom.minidom
80
81
import inspect
81
 
import GnuPGInterface
82
82
 
83
83
try:
84
84
    SO_BINDTODEVICE = socket.SO_BINDTODEVICE
88
88
    except ImportError:
89
89
        SO_BINDTODEVICE = None
90
90
 
91
 
version = "1.5.3"
 
91
if sys.version_info.major == 2:
 
92
    str = unicode
 
93
 
 
94
version = "1.6.9"
92
95
stored_state_file = "clients.pickle"
93
96
 
94
97
logger = logging.getLogger()
95
 
syslogger = (logging.handlers.SysLogHandler
96
 
             (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
97
 
              address = str("/dev/log")))
 
98
syslogger = None
98
99
 
99
100
try:
100
101
    if_nametoindex = (ctypes.cdll.LoadLibrary
106
107
        SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
107
108
        with contextlib.closing(socket.socket()) as s:
108
109
            ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
109
 
                                struct.pack(str("16s16x"),
110
 
                                            interface))
111
 
        interface_index = struct.unpack(str("I"),
112
 
                                        ifreq[16:20])[0]
 
110
                                struct.pack(b"16s16x", interface))
 
111
        interface_index = struct.unpack("I", ifreq[16:20])[0]
113
112
        return interface_index
114
113
 
115
114
 
116
115
def initlogger(debug, level=logging.WARNING):
117
116
    """init logger and add loglevel"""
118
117
    
 
118
    global syslogger
 
119
    syslogger = (logging.handlers.SysLogHandler
 
120
                 (facility =
 
121
                  logging.handlers.SysLogHandler.LOG_DAEMON,
 
122
                  address = "/dev/log"))
119
123
    syslogger.setFormatter(logging.Formatter
120
124
                           ('Mandos [%(process)d]: %(levelname)s:'
121
125
                            ' %(message)s'))
139
143
class PGPEngine(object):
140
144
    """A simple class for OpenPGP symmetric encryption & decryption"""
141
145
    def __init__(self):
142
 
        self.gnupg = GnuPGInterface.GnuPG()
143
146
        self.tempdir = tempfile.mkdtemp(prefix="mandos-")
144
 
        self.gnupg = GnuPGInterface.GnuPG()
145
 
        self.gnupg.options.meta_interactive = False
146
 
        self.gnupg.options.homedir = self.tempdir
147
 
        self.gnupg.options.extra_args.extend(['--force-mdc',
148
 
                                              '--quiet',
149
 
                                              '--no-use-agent'])
 
147
        self.gnupgargs = ['--batch',
 
148
                          '--home', self.tempdir,
 
149
                          '--force-mdc',
 
150
                          '--quiet',
 
151
                          '--no-use-agent']
150
152
    
151
153
    def __enter__(self):
152
154
        return self
153
155
    
154
 
    def __exit__ (self, exc_type, exc_value, traceback):
 
156
    def __exit__(self, exc_type, exc_value, traceback):
155
157
        self._cleanup()
156
158
        return False
157
159
    
174
176
    def password_encode(self, password):
175
177
        # Passphrase can not be empty and can not contain newlines or
176
178
        # NUL bytes.  So we prefix it and hex encode it.
177
 
        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
178
186
    
179
187
    def encrypt(self, data, password):
180
 
        self.gnupg.passphrase = self.password_encode(password)
181
 
        with open(os.devnull, "w") as devnull:
182
 
            try:
183
 
                proc = self.gnupg.run(['--symmetric'],
184
 
                                      create_fhs=['stdin', 'stdout'],
185
 
                                      attach_fhs={'stderr': devnull})
186
 
                with contextlib.closing(proc.handles['stdin']) as f:
187
 
                    f.write(data)
188
 
                with contextlib.closing(proc.handles['stdout']) as f:
189
 
                    ciphertext = f.read()
190
 
                proc.wait()
191
 
            except IOError as e:
192
 
                raise PGPError(e)
193
 
        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)
194
203
        return ciphertext
195
204
    
196
205
    def decrypt(self, data, password):
197
 
        self.gnupg.passphrase = self.password_encode(password)
198
 
        with open(os.devnull, "w") as devnull:
199
 
            try:
200
 
                proc = self.gnupg.run(['--decrypt'],
201
 
                                      create_fhs=['stdin', 'stdout'],
202
 
                                      attach_fhs={'stderr': devnull})
203
 
                with contextlib.closing(proc.handles['stdin']) as f:
204
 
                    f.write(data)
205
 
                with contextlib.closing(proc.handles['stdout']) as f:
206
 
                    decrypted_plaintext = f.read()
207
 
                proc.wait()
208
 
            except IOError as e:
209
 
                raise PGPError(e)
210
 
        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)
211
222
        return decrypted_plaintext
212
223
 
213
224
 
214
225
class AvahiError(Exception):
215
226
    def __init__(self, value, *args, **kwargs):
216
227
        self.value = value
217
 
        super(AvahiError, self).__init__(value, *args, **kwargs)
218
 
    def __unicode__(self):
219
 
        return unicode(repr(self.value))
 
228
        return super(AvahiError, self).__init__(value, *args,
 
229
                                                **kwargs)
220
230
 
221
231
class AvahiServiceError(AvahiError):
222
232
    pass
233
243
               Used to optionally bind to the specified interface.
234
244
    name: string; Example: 'Mandos'
235
245
    type: string; Example: '_mandos._tcp'.
236
 
                  See <http://www.dns-sd.org/ServiceTypes.html>
 
246
     See <https://www.iana.org/assignments/service-names-port-numbers>
237
247
    port: integer; what port to announce
238
248
    TXT: list of strings; TXT record for the service
239
249
    domain: string; Domain to publish on, default to .local if empty.
272
282
                            " after %i retries, exiting.",
273
283
                            self.rename_count)
274
284
            raise AvahiServiceError("Too many renames")
275
 
        self.name = unicode(self.server
276
 
                            .GetAlternativeServiceName(self.name))
 
285
        self.name = str(self.server
 
286
                        .GetAlternativeServiceName(self.name))
277
287
        logger.info("Changing Zeroconf service name to %r ...",
278
288
                    self.name)
279
289
        self.remove()
327
337
            self.rename()
328
338
        elif state == avahi.ENTRY_GROUP_FAILURE:
329
339
            logger.critical("Avahi: Error in group state changed %s",
330
 
                            unicode(error))
331
 
            raise AvahiGroupError("State changed: {0!s}"
 
340
                            str(error))
 
341
            raise AvahiGroupError("State changed: {!s}"
332
342
                                  .format(error))
333
343
    
334
344
    def cleanup(self):
385
395
        """Add the new name to the syslog messages"""
386
396
        ret = AvahiService.rename(self)
387
397
        syslogger.setFormatter(logging.Formatter
388
 
                               ('Mandos ({0}) [%(process)d]:'
 
398
                               ('Mandos ({}) [%(process)d]:'
389
399
                                ' %(levelname)s: %(message)s'
390
400
                                .format(self.name)))
391
401
        return ret
392
402
 
393
403
 
394
 
def timedelta_to_milliseconds(td):
395
 
    "Convert a datetime.timedelta() to milliseconds"
396
 
    return ((td.days * 24 * 60 * 60 * 1000)
397
 
            + (td.seconds * 1000)
398
 
            + (td.microseconds // 1000))
399
 
 
400
 
 
401
404
class Client(object):
402
405
    """A representation of a client host served by this server.
403
406
    
439
442
    runtime_expansions: Allowed attributes for runtime expansion.
440
443
    expires:    datetime.datetime(); time (UTC) when a client will be
441
444
                disabled, or None
 
445
    server_settings: The server_settings dict from main()
442
446
    """
443
447
    
444
448
    runtime_expansions = ("approval_delay", "approval_duration",
445
 
                          "created", "enabled", "fingerprint",
446
 
                          "host", "interval", "last_checked_ok",
 
449
                          "created", "enabled", "expires",
 
450
                          "fingerprint", "host", "interval",
 
451
                          "last_approval_request", "last_checked_ok",
447
452
                          "last_enabled", "name", "timeout")
448
 
    client_defaults = { "timeout": "5m",
449
 
                        "extended_timeout": "15m",
450
 
                        "interval": "2m",
 
453
    client_defaults = { "timeout": "PT5M",
 
454
                        "extended_timeout": "PT15M",
 
455
                        "interval": "PT2M",
451
456
                        "checker": "fping -q -- %%(host)s",
452
457
                        "host": "",
453
 
                        "approval_delay": "0s",
454
 
                        "approval_duration": "1s",
 
458
                        "approval_delay": "PT0S",
 
459
                        "approval_duration": "PT1S",
455
460
                        "approved_by_default": "True",
456
461
                        "enabled": "True",
457
462
                        }
458
463
    
459
 
    def timeout_milliseconds(self):
460
 
        "Return the 'timeout' attribute in milliseconds"
461
 
        return timedelta_to_milliseconds(self.timeout)
462
 
    
463
 
    def extended_timeout_milliseconds(self):
464
 
        "Return the 'extended_timeout' attribute in milliseconds"
465
 
        return timedelta_to_milliseconds(self.extended_timeout)
466
 
    
467
 
    def interval_milliseconds(self):
468
 
        "Return the 'interval' attribute in milliseconds"
469
 
        return timedelta_to_milliseconds(self.interval)
470
 
    
471
 
    def approval_delay_milliseconds(self):
472
 
        return timedelta_to_milliseconds(self.approval_delay)
473
 
    
474
464
    @staticmethod
475
465
    def config_parser(config):
476
466
        """Construct a new dict of client settings of this form:
501
491
                          "rb") as secfile:
502
492
                    client["secret"] = secfile.read()
503
493
            else:
504
 
                raise TypeError("No secret or secfile for section {0}"
 
494
                raise TypeError("No secret or secfile for section {}"
505
495
                                .format(section))
506
496
            client["timeout"] = string_to_delta(section["timeout"])
507
497
            client["extended_timeout"] = string_to_delta(
518
508
        
519
509
        return settings
520
510
    
521
 
    def __init__(self, settings, name = None):
 
511
    def __init__(self, settings, name = None, server_settings=None):
522
512
        self.name = name
 
513
        if server_settings is None:
 
514
            server_settings = {}
 
515
        self.server_settings = server_settings
523
516
        # adding all client settings
524
 
        for setting, value in settings.iteritems():
 
517
        for setting, value in settings.items():
525
518
            setattr(self, setting, value)
526
519
        
527
520
        if self.enabled:
610
603
        if self.checker_initiator_tag is not None:
611
604
            gobject.source_remove(self.checker_initiator_tag)
612
605
        self.checker_initiator_tag = (gobject.timeout_add
613
 
                                      (self.interval_milliseconds(),
 
606
                                      (int(self.interval
 
607
                                           .total_seconds() * 1000),
614
608
                                       self.start_checker))
615
609
        # Schedule a disable() when 'timeout' has passed
616
610
        if self.disable_initiator_tag is not None:
617
611
            gobject.source_remove(self.disable_initiator_tag)
618
612
        self.disable_initiator_tag = (gobject.timeout_add
619
 
                                   (self.timeout_milliseconds(),
620
 
                                    self.disable))
 
613
                                      (int(self.timeout
 
614
                                           .total_seconds() * 1000),
 
615
                                       self.disable))
621
616
        # Also start a new checker *right now*.
622
617
        self.start_checker()
623
618
    
654
649
            self.disable_initiator_tag = None
655
650
        if getattr(self, "enabled", False):
656
651
            self.disable_initiator_tag = (gobject.timeout_add
657
 
                                          (timedelta_to_milliseconds
658
 
                                           (timeout), self.disable))
 
652
                                          (int(timeout.total_seconds()
 
653
                                               * 1000), self.disable))
659
654
            self.expires = datetime.datetime.utcnow() + timeout
660
655
    
661
656
    def need_approval(self):
678
673
        # If a checker exists, make sure it is not a zombie
679
674
        try:
680
675
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
681
 
        except (AttributeError, OSError) as error:
682
 
            if (isinstance(error, OSError)
683
 
                and error.errno != errno.ECHILD):
684
 
                raise error
 
676
        except AttributeError:
 
677
            pass
 
678
        except OSError as error:
 
679
            if error.errno != errno.ECHILD:
 
680
                raise
685
681
        else:
686
682
            if pid:
687
683
                logger.warning("Checker was a zombie")
690
686
                                      self.current_checker_command)
691
687
        # Start a new checker if needed
692
688
        if self.checker is None:
 
689
            # Escape attributes for the shell
 
690
            escaped_attrs = { attr:
 
691
                                  re.escape(str(getattr(self, attr)))
 
692
                              for attr in self.runtime_expansions }
693
693
            try:
694
 
                # In case checker_command has exactly one % operator
695
 
                command = self.checker_command % self.host
696
 
            except TypeError:
697
 
                # Escape attributes for the shell
698
 
                escaped_attrs = dict(
699
 
                    (attr,
700
 
                     re.escape(unicode(str(getattr(self, attr, "")),
701
 
                                       errors=
702
 
                                       'replace')))
703
 
                    for attr in
704
 
                    self.runtime_expansions)
705
 
                
706
 
                try:
707
 
                    command = self.checker_command % escaped_attrs
708
 
                except TypeError as error:
709
 
                    logger.error('Could not format string "%s"',
710
 
                                 self.checker_command, exc_info=error)
711
 
                    return True # Try again later
 
694
                command = self.checker_command % escaped_attrs
 
695
            except TypeError as error:
 
696
                logger.error('Could not format string "%s"',
 
697
                             self.checker_command, exc_info=error)
 
698
                return True # Try again later
712
699
            self.current_checker_command = command
713
700
            try:
714
701
                logger.info("Starting checker %r for %s",
717
704
                # in normal mode, that is already done by daemon(),
718
705
                # and in debug mode we don't want to.  (Stdin is
719
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 })
720
715
                self.checker = subprocess.Popen(command,
721
716
                                                close_fds=True,
722
 
                                                shell=True, cwd="/")
723
 
                self.checker_callback_tag = (gobject.child_watch_add
724
 
                                             (self.checker.pid,
725
 
                                              self.checker_callback,
726
 
                                              data=command))
727
 
                # The checker may have completed before the gobject
728
 
                # watch was added.  Check for this.
729
 
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
730
 
                if pid:
731
 
                    gobject.source_remove(self.checker_callback_tag)
732
 
                    self.checker_callback(pid, status, command)
 
717
                                                shell=True, cwd="/",
 
718
                                                **popen_args)
733
719
            except OSError as error:
734
720
                logger.error("Failed to start subprocess",
735
721
                             exc_info=error)
 
722
                return True
 
723
            self.checker_callback_tag = (gobject.child_watch_add
 
724
                                         (self.checker.pid,
 
725
                                          self.checker_callback,
 
726
                                          data=command))
 
727
            # The checker may have completed before the gobject
 
728
            # watch was added.  Check for this.
 
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
 
738
            if pid:
 
739
                gobject.source_remove(self.checker_callback_tag)
 
740
                self.checker_callback(pid, status, command)
736
741
        # Re-run this periodically if run by gobject.timeout_add
737
742
        return True
738
743
    
771
776
    # "Set" method, so we fail early here:
772
777
    if byte_arrays and signature != "ay":
773
778
        raise ValueError("Byte arrays not supported for non-'ay'"
774
 
                         " signature {0!r}".format(signature))
 
779
                         " signature {!r}".format(signature))
775
780
    def decorator(func):
776
781
        func._dbus_is_property = True
777
782
        func._dbus_interface = dbus_interface
808
813
    """Decorator to annotate D-Bus methods, signals or properties
809
814
    Usage:
810
815
    
 
816
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true",
 
817
                       "org.freedesktop.DBus.Property."
 
818
                       "EmitsChangedSignal": "false"})
811
819
    @dbus_service_property("org.example.Interface", signature="b",
812
820
                           access="r")
813
 
    @dbus_annotations({{"org.freedesktop.DBus.Deprecated": "true",
814
 
                        "org.freedesktop.DBus.Property."
815
 
                        "EmitsChangedSignal": "false"})
816
821
    def Property_dbus_property(self):
817
822
        return dbus.Boolean(False)
818
823
    """
825
830
class DBusPropertyException(dbus.exceptions.DBusException):
826
831
    """A base class for D-Bus property-related exceptions
827
832
    """
828
 
    def __unicode__(self):
829
 
        return unicode(str(self))
830
 
 
 
833
    pass
831
834
 
832
835
class DBusPropertyAccessException(DBusPropertyException):
833
836
    """A property's access permissions disallows an operation.
856
859
        If called like _is_dbus_thing("method") it returns a function
857
860
        suitable for use as predicate to inspect.getmembers().
858
861
        """
859
 
        return lambda obj: getattr(obj, "_dbus_is_{0}".format(thing),
 
862
        return lambda obj: getattr(obj, "_dbus_is_{}".format(thing),
860
863
                                   False)
861
864
    
862
865
    def _get_all_dbus_things(self, thing):
911
914
            # The byte_arrays option is not supported yet on
912
915
            # signatures other than "ay".
913
916
            if prop._dbus_signature != "ay":
914
 
                raise ValueError
 
917
                raise ValueError("Byte arrays not supported for non-"
 
918
                                 "'ay' signature {!r}"
 
919
                                 .format(prop._dbus_signature))
915
920
            value = dbus.ByteArray(b''.join(chr(byte)
916
921
                                            for byte in value))
917
922
        prop(value)
941
946
                                           value.variant_level+1)
942
947
        return dbus.Dictionary(properties, signature="sv")
943
948
    
 
949
    @dbus.service.signal(dbus.PROPERTIES_IFACE, signature="sa{sv}as")
 
950
    def PropertiesChanged(self, interface_name, changed_properties,
 
951
                          invalidated_properties):
 
952
        """Standard D-Bus PropertiesChanged() signal, see D-Bus
 
953
        standard.
 
954
        """
 
955
        pass
 
956
    
944
957
    @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
945
958
                         out_signature="s",
946
959
                         path_keyword='object_path',
981
994
                                              (prop,
982
995
                                               "_dbus_annotations",
983
996
                                               {}))
984
 
                        for name, value in annots.iteritems():
 
997
                        for name, value in annots.items():
985
998
                            ann_tag = document.createElement(
986
999
                                "annotation")
987
1000
                            ann_tag.setAttribute("name", name)
990
1003
                # Add interface annotation tags
991
1004
                for annotation, value in dict(
992
1005
                    itertools.chain.from_iterable(
993
 
                        annotations().iteritems()
 
1006
                        annotations().items()
994
1007
                        for name, annotations in
995
1008
                        self._get_all_dbus_things("interface")
996
1009
                        if name == if_tag.getAttribute("name")
997
 
                        )).iteritems():
 
1010
                        )).items():
998
1011
                    ann_tag = document.createElement("annotation")
999
1012
                    ann_tag.setAttribute("name", annotation)
1000
1013
                    ann_tag.setAttribute("value", value)
1023
1036
        return xmlstring
1024
1037
 
1025
1038
 
1026
 
def datetime_to_dbus (dt, variant_level=0):
 
1039
def datetime_to_dbus(dt, variant_level=0):
1027
1040
    """Convert a UTC datetime.datetime() to a D-Bus type."""
1028
1041
    if dt is None:
1029
1042
        return dbus.String("", variant_level = variant_level)
1037
1050
    interface names according to the "alt_interface_names" mapping.
1038
1051
    Usage:
1039
1052
    
1040
 
    @alternate_dbus_names({"org.example.Interface":
1041
 
                               "net.example.AlternateInterface"})
 
1053
    @alternate_dbus_interfaces({"org.example.Interface":
 
1054
                                    "net.example.AlternateInterface"})
1042
1055
    class SampleDBusObject(dbus.service.Object):
1043
1056
        @dbus.service.method("org.example.Interface")
1044
1057
        def SampleDBusMethod():
1056
1069
    """
1057
1070
    def wrapper(cls):
1058
1071
        for orig_interface_name, alt_interface_name in (
1059
 
            alt_interface_names.iteritems()):
 
1072
            alt_interface_names.items()):
1060
1073
            attr = {}
1061
1074
            interface_names = set()
1062
1075
            # Go though all attributes of the class
1075
1088
                interface_names.add(alt_interface)
1076
1089
                # Is this a D-Bus signal?
1077
1090
                if getattr(attribute, "_dbus_is_signal", False):
1078
 
                    # Extract the original non-method function by
1079
 
                    # black magic
 
1091
                    # Extract the original non-method undecorated
 
1092
                    # function by black magic
1080
1093
                    nonmethod_func = (dict(
1081
1094
                            zip(attribute.func_code.co_freevars,
1082
1095
                                attribute.__closure__))["func"]
1179
1192
                                        attribute.func_closure)))
1180
1193
            if deprecate:
1181
1194
                # Deprecate all alternate interfaces
1182
 
                iname="_AlternateDBusNames_interface_annotation{0}"
 
1195
                iname="_AlternateDBusNames_interface_annotation{}"
1183
1196
                for interface_name in interface_names:
1184
1197
                    @dbus_interface_annotations(interface_name)
1185
1198
                    def func(self):
1194
1207
            if interface_names:
1195
1208
                # Replace the class with a new subclass of it with
1196
1209
                # methods, signals, etc. as created above.
1197
 
                cls = type(b"{0}Alternate".format(cls.__name__),
 
1210
                cls = type(b"{}Alternate".format(cls.__name__),
1198
1211
                           (cls,), attr)
1199
1212
        return cls
1200
1213
    return wrapper
1213
1226
    runtime_expansions = (Client.runtime_expansions
1214
1227
                          + ("dbus_object_path",))
1215
1228
    
 
1229
    _interface = "se.recompile.Mandos.Client"
 
1230
    
1216
1231
    # dbus.service.Object doesn't use super(), so we can't either.
1217
1232
    
1218
1233
    def __init__(self, bus = None, *args, **kwargs):
1220
1235
        Client.__init__(self, *args, **kwargs)
1221
1236
        # Only now, when this client is initialized, can it show up on
1222
1237
        # the D-Bus
1223
 
        client_object_name = unicode(self.name).translate(
 
1238
        client_object_name = str(self.name).translate(
1224
1239
            {ord("."): ord("_"),
1225
1240
             ord("-"): ord("_")})
1226
1241
        self.dbus_object_path = (dbus.ObjectPath
1230
1245
    
1231
1246
    def notifychangeproperty(transform_func,
1232
1247
                             dbus_name, type_func=lambda x: x,
1233
 
                             variant_level=1):
 
1248
                             variant_level=1, invalidate_only=False,
 
1249
                             _interface=_interface):
1234
1250
        """ Modify a variable so that it's a property which announces
1235
1251
        its changes to DBus.
1236
1252
        
1241
1257
                   to the D-Bus.  Default: no transform
1242
1258
        variant_level: D-Bus variant level.  Default: 1
1243
1259
        """
1244
 
        attrname = "_{0}".format(dbus_name)
 
1260
        attrname = "_{}".format(dbus_name)
1245
1261
        def setter(self, value):
1246
1262
            if hasattr(self, "dbus_object_path"):
1247
1263
                if (not hasattr(self, attrname) or
1248
1264
                    type_func(getattr(self, attrname, None))
1249
1265
                    != type_func(value)):
1250
 
                    dbus_value = transform_func(type_func(value),
1251
 
                                                variant_level
1252
 
                                                =variant_level)
1253
 
                    self.PropertyChanged(dbus.String(dbus_name),
1254
 
                                         dbus_value)
 
1266
                    if invalidate_only:
 
1267
                        self.PropertiesChanged(_interface,
 
1268
                                               dbus.Dictionary(),
 
1269
                                               dbus.Array
 
1270
                                               ((dbus_name,)))
 
1271
                    else:
 
1272
                        dbus_value = transform_func(type_func(value),
 
1273
                                                    variant_level
 
1274
                                                    =variant_level)
 
1275
                        self.PropertyChanged(dbus.String(dbus_name),
 
1276
                                             dbus_value)
 
1277
                        self.PropertiesChanged(_interface,
 
1278
                                               dbus.Dictionary({
 
1279
                                    dbus.String(dbus_name):
 
1280
                                        dbus_value }), dbus.Array())
1255
1281
            setattr(self, attrname, value)
1256
1282
        
1257
1283
        return property(lambda self: getattr(self, attrname), setter)
1277
1303
    approval_delay = notifychangeproperty(dbus.UInt64,
1278
1304
                                          "ApprovalDelay",
1279
1305
                                          type_func =
1280
 
                                          timedelta_to_milliseconds)
 
1306
                                          lambda td: td.total_seconds()
 
1307
                                          * 1000)
1281
1308
    approval_duration = notifychangeproperty(
1282
1309
        dbus.UInt64, "ApprovalDuration",
1283
 
        type_func = timedelta_to_milliseconds)
 
1310
        type_func = lambda td: td.total_seconds() * 1000)
1284
1311
    host = notifychangeproperty(dbus.String, "Host")
1285
1312
    timeout = notifychangeproperty(dbus.UInt64, "Timeout",
1286
 
                                   type_func =
1287
 
                                   timedelta_to_milliseconds)
 
1313
                                   type_func = lambda td:
 
1314
                                       td.total_seconds() * 1000)
1288
1315
    extended_timeout = notifychangeproperty(
1289
1316
        dbus.UInt64, "ExtendedTimeout",
1290
 
        type_func = timedelta_to_milliseconds)
 
1317
        type_func = lambda td: td.total_seconds() * 1000)
1291
1318
    interval = notifychangeproperty(dbus.UInt64,
1292
1319
                                    "Interval",
1293
1320
                                    type_func =
1294
 
                                    timedelta_to_milliseconds)
 
1321
                                    lambda td: td.total_seconds()
 
1322
                                    * 1000)
1295
1323
    checker_command = notifychangeproperty(dbus.String, "Checker")
 
1324
    secret = notifychangeproperty(dbus.ByteArray, "Secret",
 
1325
                                  invalidate_only=True)
1296
1326
    
1297
1327
    del notifychangeproperty
1298
1328
    
1325
1355
                                       *args, **kwargs)
1326
1356
    
1327
1357
    def start_checker(self, *args, **kwargs):
1328
 
        old_checker = self.checker
1329
 
        if self.checker is not None:
1330
 
            old_checker_pid = self.checker.pid
1331
 
        else:
1332
 
            old_checker_pid = None
 
1358
        old_checker_pid = getattr(self.checker, "pid", None)
1333
1359
        r = Client.start_checker(self, *args, **kwargs)
1334
1360
        # Only if new checker process was started
1335
1361
        if (self.checker is not None
1344
1370
    
1345
1371
    def approve(self, value=True):
1346
1372
        self.approved = value
1347
 
        gobject.timeout_add(timedelta_to_milliseconds
1348
 
                            (self.approval_duration),
1349
 
                            self._reset_approved)
 
1373
        gobject.timeout_add(int(self.approval_duration.total_seconds()
 
1374
                                * 1000), self._reset_approved)
1350
1375
        self.send_changedstate()
1351
1376
    
1352
1377
    ## D-Bus methods, signals & properties
1353
 
    _interface = "se.recompile.Mandos.Client"
1354
1378
    
1355
1379
    ## Interfaces
1356
1380
    
1357
 
    @dbus_interface_annotations(_interface)
1358
 
    def _foo(self):
1359
 
        return { "org.freedesktop.DBus.Property.EmitsChangedSignal":
1360
 
                     "false"}
1361
 
    
1362
1381
    ## Signals
1363
1382
    
1364
1383
    # CheckerCompleted - signal
1374
1393
        pass
1375
1394
    
1376
1395
    # PropertyChanged - signal
 
1396
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1377
1397
    @dbus.service.signal(_interface, signature="sv")
1378
1398
    def PropertyChanged(self, property, value):
1379
1399
        "D-Bus signal"
1455
1475
                           access="readwrite")
1456
1476
    def ApprovalDelay_dbus_property(self, value=None):
1457
1477
        if value is None:       # get
1458
 
            return dbus.UInt64(self.approval_delay_milliseconds())
 
1478
            return dbus.UInt64(self.approval_delay.total_seconds()
 
1479
                               * 1000)
1459
1480
        self.approval_delay = datetime.timedelta(0, 0, 0, value)
1460
1481
    
1461
1482
    # ApprovalDuration - property
1463
1484
                           access="readwrite")
1464
1485
    def ApprovalDuration_dbus_property(self, value=None):
1465
1486
        if value is None:       # get
1466
 
            return dbus.UInt64(timedelta_to_milliseconds(
1467
 
                    self.approval_duration))
 
1487
            return dbus.UInt64(self.approval_duration.total_seconds()
 
1488
                               * 1000)
1468
1489
        self.approval_duration = datetime.timedelta(0, 0, 0, value)
1469
1490
    
1470
1491
    # Name - property
1483
1504
    def Host_dbus_property(self, value=None):
1484
1505
        if value is None:       # get
1485
1506
            return dbus.String(self.host)
1486
 
        self.host = unicode(value)
 
1507
        self.host = str(value)
1487
1508
    
1488
1509
    # Created - property
1489
1510
    @dbus_service_property(_interface, signature="s", access="read")
1536
1557
                           access="readwrite")
1537
1558
    def Timeout_dbus_property(self, value=None):
1538
1559
        if value is None:       # get
1539
 
            return dbus.UInt64(self.timeout_milliseconds())
 
1560
            return dbus.UInt64(self.timeout.total_seconds() * 1000)
1540
1561
        old_timeout = self.timeout
1541
1562
        self.timeout = datetime.timedelta(0, 0, 0, value)
1542
1563
        # Reschedule disabling
1553
1574
                gobject.source_remove(self.disable_initiator_tag)
1554
1575
                self.disable_initiator_tag = (
1555
1576
                    gobject.timeout_add(
1556
 
                        timedelta_to_milliseconds(self.expires - now),
1557
 
                        self.disable))
 
1577
                        int((self.expires - now).total_seconds()
 
1578
                            * 1000), self.disable))
1558
1579
    
1559
1580
    # ExtendedTimeout - property
1560
1581
    @dbus_service_property(_interface, signature="t",
1561
1582
                           access="readwrite")
1562
1583
    def ExtendedTimeout_dbus_property(self, value=None):
1563
1584
        if value is None:       # get
1564
 
            return dbus.UInt64(self.extended_timeout_milliseconds())
 
1585
            return dbus.UInt64(self.extended_timeout.total_seconds()
 
1586
                               * 1000)
1565
1587
        self.extended_timeout = datetime.timedelta(0, 0, 0, value)
1566
1588
    
1567
1589
    # Interval - property
1569
1591
                           access="readwrite")
1570
1592
    def Interval_dbus_property(self, value=None):
1571
1593
        if value is None:       # get
1572
 
            return dbus.UInt64(self.interval_milliseconds())
 
1594
            return dbus.UInt64(self.interval.total_seconds() * 1000)
1573
1595
        self.interval = datetime.timedelta(0, 0, 0, value)
1574
1596
        if getattr(self, "checker_initiator_tag", None) is None:
1575
1597
            return
1586
1608
    def Checker_dbus_property(self, value=None):
1587
1609
        if value is None:       # get
1588
1610
            return dbus.String(self.checker_command)
1589
 
        self.checker_command = unicode(value)
 
1611
        self.checker_command = str(value)
1590
1612
    
1591
1613
    # CheckerRunning - property
1592
1614
    @dbus_service_property(_interface, signature="b",
1608
1630
    @dbus_service_property(_interface, signature="ay",
1609
1631
                           access="write", byte_arrays=True)
1610
1632
    def Secret_dbus_property(self, value):
1611
 
        self.secret = str(value)
 
1633
        self.secret = bytes(value)
1612
1634
    
1613
1635
    del _interface
1614
1636
 
1648
1670
    def handle(self):
1649
1671
        with contextlib.closing(self.server.child_pipe) as child_pipe:
1650
1672
            logger.info("TCP connection from: %s",
1651
 
                        unicode(self.client_address))
 
1673
                        str(self.client_address))
1652
1674
            logger.debug("Pipe FD: %d",
1653
1675
                         self.server.child_pipe.fileno())
1654
1676
            
1680
1702
            logger.debug("Protocol version: %r", line)
1681
1703
            try:
1682
1704
                if int(line.strip().split()[0]) > 1:
1683
 
                    raise RuntimeError
 
1705
                    raise RuntimeError(line)
1684
1706
            except (ValueError, IndexError, RuntimeError) as error:
1685
1707
                logger.error("Unknown protocol version: %s", error)
1686
1708
                return
1735
1757
                        if self.server.use_dbus:
1736
1758
                            # Emit D-Bus signal
1737
1759
                            client.NeedApproval(
1738
 
                                client.approval_delay_milliseconds(),
1739
 
                                client.approved_by_default)
 
1760
                                client.approval_delay.total_seconds()
 
1761
                                * 1000, client.approved_by_default)
1740
1762
                    else:
1741
1763
                        logger.warning("Client %s was not approved",
1742
1764
                                       client.name)
1748
1770
                    #wait until timeout or approved
1749
1771
                    time = datetime.datetime.now()
1750
1772
                    client.changedstate.acquire()
1751
 
                    client.changedstate.wait(
1752
 
                        float(timedelta_to_milliseconds(delay)
1753
 
                              / 1000))
 
1773
                    client.changedstate.wait(delay.total_seconds())
1754
1774
                    client.changedstate.release()
1755
1775
                    time2 = datetime.datetime.now()
1756
1776
                    if (time2 - time) >= delay:
1893
1913
    
1894
1914
    def add_pipe(self, parent_pipe, proc):
1895
1915
        """Dummy function; override as necessary"""
1896
 
        raise NotImplementedError
 
1916
        raise NotImplementedError()
1897
1917
 
1898
1918
 
1899
1919
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1906
1926
        use_ipv6:       Boolean; to use IPv6 or not
1907
1927
    """
1908
1928
    def __init__(self, server_address, RequestHandlerClass,
1909
 
                 interface=None, use_ipv6=True):
 
1929
                 interface=None, use_ipv6=True, socketfd=None):
 
1930
        """If socketfd is set, use that file descriptor instead of
 
1931
        creating a new one with socket.socket().
 
1932
        """
1910
1933
        self.interface = interface
1911
1934
        if use_ipv6:
1912
1935
            self.address_family = socket.AF_INET6
 
1936
        if socketfd is not None:
 
1937
            # Save the file descriptor
 
1938
            self.socketfd = socketfd
 
1939
            # Save the original socket.socket() function
 
1940
            self.socket_socket = socket.socket
 
1941
            # To implement --socket, we monkey patch socket.socket.
 
1942
            # 
 
1943
            # (When socketserver.TCPServer is a new-style class, we
 
1944
            # could make self.socket into a property instead of monkey
 
1945
            # patching socket.socket.)
 
1946
            # 
 
1947
            # Create a one-time-only replacement for socket.socket()
 
1948
            @functools.wraps(socket.socket)
 
1949
            def socket_wrapper(*args, **kwargs):
 
1950
                # Restore original function so subsequent calls are
 
1951
                # not affected.
 
1952
                socket.socket = self.socket_socket
 
1953
                del self.socket_socket
 
1954
                # This time only, return a new socket object from the
 
1955
                # saved file descriptor.
 
1956
                return socket.fromfd(self.socketfd, *args, **kwargs)
 
1957
            # Replace socket.socket() function with wrapper
 
1958
            socket.socket = socket_wrapper
 
1959
        # The socketserver.TCPServer.__init__ will call
 
1960
        # socket.socket(), which might be our replacement,
 
1961
        # socket_wrapper(), if socketfd was set.
1913
1962
        socketserver.TCPServer.__init__(self, server_address,
1914
1963
                                        RequestHandlerClass)
 
1964
    
1915
1965
    def server_bind(self):
1916
1966
        """This overrides the normal server_bind() function
1917
1967
        to bind to an interface if one was specified, and also NOT to
1925
1975
                try:
1926
1976
                    self.socket.setsockopt(socket.SOL_SOCKET,
1927
1977
                                           SO_BINDTODEVICE,
1928
 
                                           str(self.interface
1929
 
                                               + '\0'))
 
1978
                                           (self.interface + "\0")
 
1979
                                           .encode("utf-8"))
1930
1980
                except socket.error as error:
1931
1981
                    if error.errno == errno.EPERM:
1932
 
                        logger.error("No permission to"
1933
 
                                     " bind to interface %s",
1934
 
                                     self.interface)
 
1982
                        logger.error("No permission to bind to"
 
1983
                                     " interface %s", self.interface)
1935
1984
                    elif error.errno == errno.ENOPROTOOPT:
1936
1985
                        logger.error("SO_BINDTODEVICE not available;"
1937
1986
                                     " cannot bind to interface %s",
1938
1987
                                     self.interface)
1939
1988
                    elif error.errno == errno.ENODEV:
1940
 
                        logger.error("Interface %s does not"
1941
 
                                     " exist, cannot bind",
1942
 
                                     self.interface)
 
1989
                        logger.error("Interface %s does not exist,"
 
1990
                                     " cannot bind", self.interface)
1943
1991
                    else:
1944
1992
                        raise
1945
1993
        # Only bind(2) the socket if we really need to.
1948
1996
                if self.address_family == socket.AF_INET6:
1949
1997
                    any_address = "::" # in6addr_any
1950
1998
                else:
1951
 
                    any_address = socket.INADDR_ANY
 
1999
                    any_address = "0.0.0.0" # INADDR_ANY
1952
2000
                self.server_address = (any_address,
1953
2001
                                       self.server_address[1])
1954
2002
            elif not self.server_address[1]:
1975
2023
    """
1976
2024
    def __init__(self, server_address, RequestHandlerClass,
1977
2025
                 interface=None, use_ipv6=True, clients=None,
1978
 
                 gnutls_priority=None, use_dbus=True):
 
2026
                 gnutls_priority=None, use_dbus=True, socketfd=None):
1979
2027
        self.enabled = False
1980
2028
        self.clients = clients
1981
2029
        if self.clients is None:
1985
2033
        IPv6_TCPServer.__init__(self, server_address,
1986
2034
                                RequestHandlerClass,
1987
2035
                                interface = interface,
1988
 
                                use_ipv6 = use_ipv6)
 
2036
                                use_ipv6 = use_ipv6,
 
2037
                                socketfd = socketfd)
1989
2038
    def server_activate(self):
1990
2039
        if self.enabled:
1991
2040
            return socketserver.TCPServer.server_activate(self)
2069
2118
        return True
2070
2119
 
2071
2120
 
 
2121
def rfc3339_duration_to_delta(duration):
 
2122
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
 
2123
    
 
2124
    >>> rfc3339_duration_to_delta("P7D")
 
2125
    datetime.timedelta(7)
 
2126
    >>> rfc3339_duration_to_delta("PT60S")
 
2127
    datetime.timedelta(0, 60)
 
2128
    >>> rfc3339_duration_to_delta("PT60M")
 
2129
    datetime.timedelta(0, 3600)
 
2130
    >>> rfc3339_duration_to_delta("PT24H")
 
2131
    datetime.timedelta(1)
 
2132
    >>> rfc3339_duration_to_delta("P1W")
 
2133
    datetime.timedelta(7)
 
2134
    >>> rfc3339_duration_to_delta("PT5M30S")
 
2135
    datetime.timedelta(0, 330)
 
2136
    >>> rfc3339_duration_to_delta("P1DT3M20S")
 
2137
    datetime.timedelta(1, 200)
 
2138
    """
 
2139
    
 
2140
    # Parsing an RFC 3339 duration with regular expressions is not
 
2141
    # possible - there would have to be multiple places for the same
 
2142
    # values, like seconds.  The current code, while more esoteric, is
 
2143
    # cleaner without depending on a parsing library.  If Python had a
 
2144
    # built-in library for parsing we would use it, but we'd like to
 
2145
    # avoid excessive use of external libraries.
 
2146
    
 
2147
    # New type for defining tokens, syntax, and semantics all-in-one
 
2148
    Token = collections.namedtuple("Token",
 
2149
                                   ("regexp", # To match token; if
 
2150
                                              # "value" is not None,
 
2151
                                              # must have a "group"
 
2152
                                              # containing digits
 
2153
                                    "value",  # datetime.timedelta or
 
2154
                                              # None
 
2155
                                    "followers")) # Tokens valid after
 
2156
                                                  # this token
 
2157
    # RFC 3339 "duration" tokens, syntax, and semantics; taken from
 
2158
    # the "duration" ABNF definition in RFC 3339, Appendix A.
 
2159
    token_end = Token(re.compile(r"$"), None, frozenset())
 
2160
    token_second = Token(re.compile(r"(\d+)S"),
 
2161
                         datetime.timedelta(seconds=1),
 
2162
                         frozenset((token_end,)))
 
2163
    token_minute = Token(re.compile(r"(\d+)M"),
 
2164
                         datetime.timedelta(minutes=1),
 
2165
                         frozenset((token_second, token_end)))
 
2166
    token_hour = Token(re.compile(r"(\d+)H"),
 
2167
                       datetime.timedelta(hours=1),
 
2168
                       frozenset((token_minute, token_end)))
 
2169
    token_time = Token(re.compile(r"T"),
 
2170
                       None,
 
2171
                       frozenset((token_hour, token_minute,
 
2172
                                  token_second)))
 
2173
    token_day = Token(re.compile(r"(\d+)D"),
 
2174
                      datetime.timedelta(days=1),
 
2175
                      frozenset((token_time, token_end)))
 
2176
    token_month = Token(re.compile(r"(\d+)M"),
 
2177
                        datetime.timedelta(weeks=4),
 
2178
                        frozenset((token_day, token_end)))
 
2179
    token_year = Token(re.compile(r"(\d+)Y"),
 
2180
                       datetime.timedelta(weeks=52),
 
2181
                       frozenset((token_month, token_end)))
 
2182
    token_week = Token(re.compile(r"(\d+)W"),
 
2183
                       datetime.timedelta(weeks=1),
 
2184
                       frozenset((token_end,)))
 
2185
    token_duration = Token(re.compile(r"P"), None,
 
2186
                           frozenset((token_year, token_month,
 
2187
                                      token_day, token_time,
 
2188
                                      token_week)))
 
2189
    # Define starting values
 
2190
    value = datetime.timedelta() # Value so far
 
2191
    found_token = None
 
2192
    followers = frozenset((token_duration,)) # Following valid tokens
 
2193
    s = duration                # String left to parse
 
2194
    # Loop until end token is found
 
2195
    while found_token is not token_end:
 
2196
        # Search for any currently valid tokens
 
2197
        for token in followers:
 
2198
            match = token.regexp.match(s)
 
2199
            if match is not None:
 
2200
                # Token found
 
2201
                if token.value is not None:
 
2202
                    # Value found, parse digits
 
2203
                    factor = int(match.group(1), 10)
 
2204
                    # Add to value so far
 
2205
                    value += factor * token.value
 
2206
                # Strip token from string
 
2207
                s = token.regexp.sub("", s, 1)
 
2208
                # Go to found token
 
2209
                found_token = token
 
2210
                # Set valid next tokens
 
2211
                followers = found_token.followers
 
2212
                break
 
2213
        else:
 
2214
            # No currently valid tokens were found
 
2215
            raise ValueError("Invalid RFC 3339 duration")
 
2216
    # End token found
 
2217
    return value
 
2218
 
 
2219
 
2072
2220
def string_to_delta(interval):
2073
2221
    """Parse a string and return a datetime.timedelta
2074
2222
    
2085
2233
    >>> string_to_delta('5m 30s')
2086
2234
    datetime.timedelta(0, 330)
2087
2235
    """
 
2236
    
 
2237
    try:
 
2238
        return rfc3339_duration_to_delta(interval)
 
2239
    except ValueError:
 
2240
        pass
 
2241
    
2088
2242
    timevalue = datetime.timedelta(0)
2089
2243
    for s in interval.split():
2090
2244
        try:
2091
 
            suffix = unicode(s[-1])
 
2245
            suffix = s[-1]
2092
2246
            value = int(s[:-1])
2093
2247
            if suffix == "d":
2094
2248
                delta = datetime.timedelta(value)
2101
2255
            elif suffix == "w":
2102
2256
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
2103
2257
            else:
2104
 
                raise ValueError("Unknown suffix {0!r}"
 
2258
                raise ValueError("Unknown suffix {!r}"
2105
2259
                                 .format(suffix))
2106
 
        except (ValueError, IndexError) as e:
 
2260
        except IndexError as e:
2107
2261
            raise ValueError(*(e.args))
2108
2262
        timevalue += delta
2109
2263
    return timevalue
2124
2278
        # Close all standard open file descriptors
2125
2279
        null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2126
2280
        if not stat.S_ISCHR(os.fstat(null).st_mode):
2127
 
            raise OSError(errno.ENODEV,
2128
 
                          "{0} not a character device"
 
2281
            raise OSError(errno.ENODEV, "{} not a character device"
2129
2282
                          .format(os.devnull))
2130
2283
        os.dup2(null, sys.stdin.fileno())
2131
2284
        os.dup2(null, sys.stdout.fileno())
2141
2294
    
2142
2295
    parser = argparse.ArgumentParser()
2143
2296
    parser.add_argument("-v", "--version", action="version",
2144
 
                        version = "%(prog)s {0}".format(version),
 
2297
                        version = "%(prog)s {}".format(version),
2145
2298
                        help="show version number and exit")
2146
2299
    parser.add_argument("-i", "--interface", metavar="IF",
2147
2300
                        help="Bind to interface IF")
2153
2306
                        help="Run self-test")
2154
2307
    parser.add_argument("--debug", action="store_true",
2155
2308
                        help="Debug mode; run in foreground and log"
2156
 
                        " to terminal")
 
2309
                        " to terminal", default=None)
2157
2310
    parser.add_argument("--debuglevel", metavar="LEVEL",
2158
2311
                        help="Debug level for stdout output")
2159
2312
    parser.add_argument("--priority", help="GnuTLS"
2166
2319
                        " files")
2167
2320
    parser.add_argument("--no-dbus", action="store_false",
2168
2321
                        dest="use_dbus", help="Do not provide D-Bus"
2169
 
                        " system bus interface")
 
2322
                        " system bus interface", default=None)
2170
2323
    parser.add_argument("--no-ipv6", action="store_false",
2171
 
                        dest="use_ipv6", help="Do not use IPv6")
 
2324
                        dest="use_ipv6", help="Do not use IPv6",
 
2325
                        default=None)
2172
2326
    parser.add_argument("--no-restore", action="store_false",
2173
2327
                        dest="restore", help="Do not restore stored"
2174
 
                        " state")
 
2328
                        " state", default=None)
 
2329
    parser.add_argument("--socket", type=int,
 
2330
                        help="Specify a file descriptor to a network"
 
2331
                        " socket to use instead of creating one")
2175
2332
    parser.add_argument("--statedir", metavar="DIR",
2176
2333
                        help="Directory to save/restore state in")
 
2334
    parser.add_argument("--foreground", action="store_true",
 
2335
                        help="Run in foreground", default=None)
 
2336
    parser.add_argument("--no-zeroconf", action="store_false",
 
2337
                        dest="zeroconf", help="Do not use Zeroconf",
 
2338
                        default=None)
2177
2339
    
2178
2340
    options = parser.parse_args()
2179
2341
    
2180
2342
    if options.check:
2181
2343
        import doctest
2182
 
        doctest.testmod()
2183
 
        sys.exit()
 
2344
        fail_count, test_count = doctest.testmod()
 
2345
        sys.exit(os.EX_OK if fail_count == 0 else 1)
2184
2346
    
2185
2347
    # Default values for config file for server-global settings
2186
2348
    server_defaults = { "interface": "",
2188
2350
                        "port": "",
2189
2351
                        "debug": "False",
2190
2352
                        "priority":
2191
 
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
 
2353
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
2192
2354
                        "servicename": "Mandos",
2193
2355
                        "use_dbus": "True",
2194
2356
                        "use_ipv6": "True",
2195
2357
                        "debuglevel": "",
2196
2358
                        "restore": "True",
2197
 
                        "statedir": "/var/lib/mandos"
 
2359
                        "socket": "",
 
2360
                        "statedir": "/var/lib/mandos",
 
2361
                        "foreground": "False",
 
2362
                        "zeroconf": "True",
2198
2363
                        }
2199
2364
    
2200
2365
    # Parse config file for server-global settings
2205
2370
    # Convert the SafeConfigParser object to a dict
2206
2371
    server_settings = server_config.defaults()
2207
2372
    # Use the appropriate methods on the non-string config options
2208
 
    for option in ("debug", "use_dbus", "use_ipv6"):
 
2373
    for option in ("debug", "use_dbus", "use_ipv6", "foreground"):
2209
2374
        server_settings[option] = server_config.getboolean("DEFAULT",
2210
2375
                                                           option)
2211
2376
    if server_settings["port"]:
2212
2377
        server_settings["port"] = server_config.getint("DEFAULT",
2213
2378
                                                       "port")
 
2379
    if server_settings["socket"]:
 
2380
        server_settings["socket"] = server_config.getint("DEFAULT",
 
2381
                                                         "socket")
 
2382
        # Later, stdin will, and stdout and stderr might, be dup'ed
 
2383
        # over with an opened os.devnull.  But we don't want this to
 
2384
        # happen with a supplied network socket.
 
2385
        if 0 <= server_settings["socket"] <= 2:
 
2386
            server_settings["socket"] = os.dup(server_settings
 
2387
                                               ["socket"])
2214
2388
    del server_config
2215
2389
    
2216
2390
    # Override the settings from the config file with command line
2218
2392
    for option in ("interface", "address", "port", "debug",
2219
2393
                   "priority", "servicename", "configdir",
2220
2394
                   "use_dbus", "use_ipv6", "debuglevel", "restore",
2221
 
                   "statedir"):
 
2395
                   "statedir", "socket", "foreground", "zeroconf"):
2222
2396
        value = getattr(options, option)
2223
2397
        if value is not None:
2224
2398
            server_settings[option] = value
2225
2399
    del options
2226
2400
    # Force all strings to be unicode
2227
2401
    for option in server_settings.keys():
2228
 
        if type(server_settings[option]) is str:
2229
 
            server_settings[option] = unicode(server_settings[option])
 
2402
        if isinstance(server_settings[option], bytes):
 
2403
            server_settings[option] = (server_settings[option]
 
2404
                                       .decode("utf-8"))
 
2405
    # Force all boolean options to be boolean
 
2406
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
 
2407
                   "foreground", "zeroconf"):
 
2408
        server_settings[option] = bool(server_settings[option])
 
2409
    # Debug implies foreground
 
2410
    if server_settings["debug"]:
 
2411
        server_settings["foreground"] = True
2230
2412
    # Now we have our good server settings in "server_settings"
2231
2413
    
2232
2414
    ##################################################################
2233
2415
    
 
2416
    if (not server_settings["zeroconf"] and
 
2417
        not (server_settings["port"]
 
2418
             or server_settings["socket"] != "")):
 
2419
            parser.error("Needs port or socket to work without"
 
2420
                         " Zeroconf")
 
2421
    
2234
2422
    # For convenience
2235
2423
    debug = server_settings["debug"]
2236
2424
    debuglevel = server_settings["debuglevel"]
2238
2426
    use_ipv6 = server_settings["use_ipv6"]
2239
2427
    stored_state_path = os.path.join(server_settings["statedir"],
2240
2428
                                     stored_state_file)
 
2429
    foreground = server_settings["foreground"]
 
2430
    zeroconf = server_settings["zeroconf"]
2241
2431
    
2242
2432
    if debug:
2243
2433
        initlogger(debug, logging.DEBUG)
2250
2440
    
2251
2441
    if server_settings["servicename"] != "Mandos":
2252
2442
        syslogger.setFormatter(logging.Formatter
2253
 
                               ('Mandos ({0}) [%(process)d]:'
 
2443
                               ('Mandos ({}) [%(process)d]:'
2254
2444
                                ' %(levelname)s: %(message)s'
2255
2445
                                .format(server_settings
2256
2446
                                        ["servicename"])))
2264
2454
    global mandos_dbus_service
2265
2455
    mandos_dbus_service = None
2266
2456
    
 
2457
    socketfd = None
 
2458
    if server_settings["socket"] != "":
 
2459
        socketfd = server_settings["socket"]
2267
2460
    tcp_server = MandosServer((server_settings["address"],
2268
2461
                               server_settings["port"]),
2269
2462
                              ClientHandler,
2272
2465
                              use_ipv6=use_ipv6,
2273
2466
                              gnutls_priority=
2274
2467
                              server_settings["priority"],
2275
 
                              use_dbus=use_dbus)
2276
 
    if not debug:
2277
 
        pidfilename = "/var/run/mandos.pid"
 
2468
                              use_dbus=use_dbus,
 
2469
                              socketfd=socketfd)
 
2470
    if not foreground:
 
2471
        pidfilename = "/run/mandos.pid"
 
2472
        if not os.path.isdir("/run/."):
 
2473
            pidfilename = "/var/run/mandos.pid"
 
2474
        pidfile = None
2278
2475
        try:
2279
2476
            pidfile = open(pidfilename, "w")
2280
2477
        except IOError as e:
2295
2492
        os.setgid(gid)
2296
2493
        os.setuid(uid)
2297
2494
    except OSError as error:
2298
 
        if error[0] != errno.EPERM:
2299
 
            raise error
 
2495
        if error.errno != errno.EPERM:
 
2496
            raise
2300
2497
    
2301
2498
    if debug:
2302
2499
        # Enable all possible GnuTLS debugging
2319
2516
            os.close(null)
2320
2517
    
2321
2518
    # Need to fork before connecting to D-Bus
2322
 
    if not debug:
 
2519
    if not foreground:
2323
2520
        # Close all input and output, do double fork, etc.
2324
2521
        daemon()
2325
2522
    
 
2523
    # multiprocessing will use threads, so before we use gobject we
 
2524
    # need to inform gobject that threads will be used.
2326
2525
    gobject.threads_init()
2327
2526
    
2328
2527
    global main_loop
2343
2542
            use_dbus = False
2344
2543
            server_settings["use_dbus"] = False
2345
2544
            tcp_server.use_dbus = False
2346
 
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2347
 
    service = AvahiServiceToSyslog(name =
2348
 
                                   server_settings["servicename"],
2349
 
                                   servicetype = "_mandos._tcp",
2350
 
                                   protocol = protocol, bus = bus)
2351
 
    if server_settings["interface"]:
2352
 
        service.interface = (if_nametoindex
2353
 
                             (str(server_settings["interface"])))
 
2545
    if zeroconf:
 
2546
        protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
 
2547
        service = AvahiServiceToSyslog(name =
 
2548
                                       server_settings["servicename"],
 
2549
                                       servicetype = "_mandos._tcp",
 
2550
                                       protocol = protocol, bus = bus)
 
2551
        if server_settings["interface"]:
 
2552
            service.interface = (if_nametoindex
 
2553
                                 (server_settings["interface"]
 
2554
                                  .encode("utf-8")))
2354
2555
    
2355
2556
    global multiprocessing_manager
2356
2557
    multiprocessing_manager = multiprocessing.Manager()
2363
2564
    old_client_settings = {}
2364
2565
    clients_data = {}
2365
2566
    
 
2567
    # This is used to redirect stdout and stderr for checker processes
 
2568
    global wnull
 
2569
    wnull = open(os.devnull, "w") # A writable /dev/null
 
2570
    # Only used if server is running in foreground but not in debug
 
2571
    # mode
 
2572
    if debug or not foreground:
 
2573
        wnull.close()
 
2574
    
2366
2575
    # Get client data and settings from last running state.
2367
2576
    if server_settings["restore"]:
2368
2577
        try:
2372
2581
            os.remove(stored_state_path)
2373
2582
        except IOError as e:
2374
2583
            if e.errno == errno.ENOENT:
2375
 
                logger.warning("Could not load persistent state: {0}"
 
2584
                logger.warning("Could not load persistent state: {}"
2376
2585
                                .format(os.strerror(e.errno)))
2377
2586
            else:
2378
2587
                logger.critical("Could not load persistent state:",
2383
2592
                           "EOFError:", exc_info=e)
2384
2593
    
2385
2594
    with PGPEngine() as pgp:
2386
 
        for client_name, client in clients_data.iteritems():
 
2595
        for client_name, client in clients_data.items():
 
2596
            # Skip removed clients
 
2597
            if client_name not in client_settings:
 
2598
                continue
 
2599
            
2387
2600
            # Decide which value to use after restoring saved state.
2388
2601
            # We have three different values: Old config file,
2389
2602
            # new config file, and saved state.
2410
2623
                if datetime.datetime.utcnow() >= client["expires"]:
2411
2624
                    if not client["last_checked_ok"]:
2412
2625
                        logger.warning(
2413
 
                            "disabling client {0} - Client never "
 
2626
                            "disabling client {} - Client never "
2414
2627
                            "performed a successful checker"
2415
2628
                            .format(client_name))
2416
2629
                        client["enabled"] = False
2417
2630
                    elif client["last_checker_status"] != 0:
2418
2631
                        logger.warning(
2419
 
                            "disabling client {0} - Client "
2420
 
                            "last checker failed with error code {1}"
 
2632
                            "disabling client {} - Client last"
 
2633
                            " checker failed with error code {}"
2421
2634
                            .format(client_name,
2422
2635
                                    client["last_checker_status"]))
2423
2636
                        client["enabled"] = False
2426
2639
                                             .utcnow()
2427
2640
                                             + client["timeout"])
2428
2641
                        logger.debug("Last checker succeeded,"
2429
 
                                     " keeping {0} enabled"
 
2642
                                     " keeping {} enabled"
2430
2643
                                     .format(client_name))
2431
2644
            try:
2432
2645
                client["secret"] = (
2435
2648
                                ["secret"]))
2436
2649
            except PGPError:
2437
2650
                # If decryption fails, we use secret from new settings
2438
 
                logger.debug("Failed to decrypt {0} old secret"
 
2651
                logger.debug("Failed to decrypt {} old secret"
2439
2652
                             .format(client_name))
2440
2653
                client["secret"] = (
2441
2654
                    client_settings[client_name]["secret"])
2449
2662
        clients_data[client_name] = client_settings[client_name]
2450
2663
    
2451
2664
    # Create all client objects
2452
 
    for client_name, client in clients_data.iteritems():
 
2665
    for client_name, client in clients_data.items():
2453
2666
        tcp_server.clients[client_name] = client_class(
2454
 
            name = client_name, settings = client)
 
2667
            name = client_name, settings = client,
 
2668
            server_settings = server_settings)
2455
2669
    
2456
2670
    if not tcp_server.clients:
2457
2671
        logger.warning("No clients defined")
2458
2672
    
2459
 
    if not debug:
2460
 
        try:
2461
 
            with pidfile:
2462
 
                pid = os.getpid()
2463
 
                pidfile.write(str(pid) + "\n".encode("utf-8"))
2464
 
            del pidfile
2465
 
        except IOError:
2466
 
            logger.error("Could not write to file %r with PID %d",
2467
 
                         pidfilename, pid)
2468
 
        except NameError:
2469
 
            # "pidfile" was never created
2470
 
            pass
 
2673
    if not foreground:
 
2674
        if pidfile is not None:
 
2675
            try:
 
2676
                with pidfile:
 
2677
                    pid = os.getpid()
 
2678
                    pidfile.write("{}\n".format(pid).encode("utf-8"))
 
2679
            except IOError:
 
2680
                logger.error("Could not write to file %r with PID %d",
 
2681
                             pidfilename, pid)
 
2682
        del pidfile
2471
2683
        del pidfilename
2472
 
        signal.signal(signal.SIGINT, signal.SIG_IGN)
2473
2684
    
2474
2685
    signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2475
2686
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2516
2727
            def GetAllClientsWithProperties(self):
2517
2728
                "D-Bus method"
2518
2729
                return dbus.Dictionary(
2519
 
                    ((c.dbus_object_path, c.GetAll(""))
2520
 
                     for c in tcp_server.clients.itervalues()),
 
2730
                    { c.dbus_object_path: c.GetAll("")
 
2731
                      for c in tcp_server.clients.itervalues() },
2521
2732
                    signature="oa{sv}")
2522
2733
            
2523
2734
            @dbus.service.method(_interface, in_signature="o")
2540
2751
    
2541
2752
    def cleanup():
2542
2753
        "Cleanup function; run on exit"
2543
 
        service.cleanup()
 
2754
        if zeroconf:
 
2755
            service.cleanup()
2544
2756
        
2545
2757
        multiprocessing.active_children()
 
2758
        wnull.close()
2546
2759
        if not (tcp_server.clients or client_settings):
2547
2760
            return
2548
2761
        
2559
2772
                
2560
2773
                # A list of attributes that can not be pickled
2561
2774
                # + secret.
2562
 
                exclude = set(("bus", "changedstate", "secret",
2563
 
                               "checker"))
 
2775
                exclude = { "bus", "changedstate", "secret",
 
2776
                            "checker", "server_settings" }
2564
2777
                for name, typ in (inspect.getmembers
2565
2778
                                  (dbus.service.Object)):
2566
2779
                    exclude.add(name)
2589
2802
                except NameError:
2590
2803
                    pass
2591
2804
            if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
2592
 
                logger.warning("Could not save persistent state: {0}"
 
2805
                logger.warning("Could not save persistent state: {}"
2593
2806
                               .format(os.strerror(e.errno)))
2594
2807
            else:
2595
2808
                logger.warning("Could not save persistent state:",
2596
2809
                               exc_info=e)
2597
 
                raise e
 
2810
                raise
2598
2811
        
2599
2812
        # Delete all clients, and settings from config
2600
2813
        while tcp_server.clients:
2624
2837
    tcp_server.server_activate()
2625
2838
    
2626
2839
    # Find out what port we got
2627
 
    service.port = tcp_server.socket.getsockname()[1]
 
2840
    if zeroconf:
 
2841
        service.port = tcp_server.socket.getsockname()[1]
2628
2842
    if use_ipv6:
2629
2843
        logger.info("Now listening on address %r, port %d,"
2630
2844
                    " flowinfo %d, scope_id %d",
2636
2850
    #service.interface = tcp_server.socket.getsockname()[3]
2637
2851
    
2638
2852
    try:
2639
 
        # From the Avahi example code
2640
 
        try:
2641
 
            service.activate()
2642
 
        except dbus.exceptions.DBusException as error:
2643
 
            logger.critical("D-Bus Exception", exc_info=error)
2644
 
            cleanup()
2645
 
            sys.exit(1)
2646
 
        # End of Avahi example code
 
2853
        if zeroconf:
 
2854
            # From the Avahi example code
 
2855
            try:
 
2856
                service.activate()
 
2857
            except dbus.exceptions.DBusException as error:
 
2858
                logger.critical("D-Bus Exception", exc_info=error)
 
2859
                cleanup()
 
2860
                sys.exit(1)
 
2861
            # End of Avahi example code
2647
2862
        
2648
2863
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2649
2864
                             lambda *args, **kwargs: