/mandos/release

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/release

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2015-05-23 10:41:35 UTC
  • mto: (237.7.304 trunk)
  • mto: This revision was merged to the branch mainline in revision 325.
  • Revision ID: teddy@recompile.se-20150523104135-s08sbkurj1wf0jrz
mandos-keygen: Update copyright year.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python3 -bI
2
 
# -*- mode: python; after-save-hook: (lambda () (let ((command (if (fboundp 'file-local-name) (file-local-name (buffer-file-name)) (or (file-remote-p (buffer-file-name) 'localname) (buffer-file-name))))) (if (= (progn (if (get-buffer "*Test*") (kill-buffer "*Test*")) (process-file-shell-command (format "%s --check" (shell-quote-argument command)) nil "*Test*")) 0) (let ((w (get-buffer-window "*Test*"))) (if w (delete-window w))) (progn (with-current-buffer "*Test*" (compilation-mode)) (display-buffer "*Test*" '(display-buffer-in-side-window)))))); coding: utf-8 -*-
3
 
#
 
1
#!/usr/bin/python2.7
 
2
# -*- mode: python; coding: utf-8 -*-
 
3
4
4
# Mandos server - give out binary blobs to connecting clients.
5
 
#
 
5
6
6
# This program is partly derived from an example program for an Avahi
7
7
# service publisher, downloaded from
8
8
# <http://avahi.org/wiki/PythonPublishExample>.  This includes the
9
9
# methods "add", "remove", "server_state_changed",
10
10
# "entry_group_state_changed", "cleanup", and "activate" in the
11
11
# "AvahiService" class, and some lines in "main".
12
 
#
 
12
13
13
# Everything else is
14
 
# Copyright © 2008-2019 Teddy Hogeborn
15
 
# Copyright © 2008-2019 Björn Påhlsson
16
 
#
17
 
# This file is part of Mandos.
18
 
#
19
 
# Mandos is free software: you can redistribute it and/or modify it
20
 
# under the terms of the GNU General Public License as published by
 
14
# Copyright © 2008-2015 Teddy Hogeborn
 
15
# Copyright © 2008-2015 Björn Påhlsson
 
16
 
17
# This program is free software: you can redistribute it and/or modify
 
18
# it under the terms of the GNU General Public License as published by
21
19
# the Free Software Foundation, either version 3 of the License, or
22
20
# (at your option) any later version.
23
21
#
24
 
#     Mandos is distributed in the hope that it will be useful, but
25
 
#     WITHOUT ANY WARRANTY; without even the implied warranty of
 
22
#     This program is distributed in the hope that it will be useful,
 
23
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
26
24
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
27
25
#     GNU General Public License for more details.
28
 
#
 
26
29
27
# You should have received a copy of the GNU General Public License
30
 
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
31
 
#
 
28
# along with this program.  If not, see
 
29
# <http://www.gnu.org/licenses/>.
 
30
32
31
# Contact the authors at <mandos@recompile.se>.
33
 
#
 
32
34
33
 
35
34
from __future__ import (division, absolute_import, print_function,
36
35
                        unicode_literals)
37
36
 
38
 
try:
39
 
    from future_builtins import *
40
 
except ImportError:
41
 
    pass
 
37
from future_builtins import *
42
38
 
43
 
try:
44
 
    import SocketServer as socketserver
45
 
except ImportError:
46
 
    import socketserver
 
39
import SocketServer as socketserver
47
40
import socket
48
41
import argparse
49
42
import datetime
50
43
import errno
51
 
try:
52
 
    import ConfigParser as configparser
53
 
except ImportError:
54
 
    import configparser
 
44
import gnutls.crypto
 
45
import gnutls.connection
 
46
import gnutls.errors
 
47
import gnutls.library.functions
 
48
import gnutls.library.constants
 
49
import gnutls.library.types
 
50
import ConfigParser as configparser
55
51
import sys
56
52
import re
57
53
import os
66
62
import struct
67
63
import fcntl
68
64
import functools
69
 
try:
70
 
    import cPickle as pickle
71
 
except ImportError:
72
 
    import pickle
 
65
import cPickle as pickle
73
66
import multiprocessing
74
67
import types
75
68
import binascii
76
69
import tempfile
77
70
import itertools
78
71
import collections
79
 
import codecs
80
 
import unittest
81
72
 
82
73
import dbus
83
74
import dbus.service
84
 
import gi
85
 
from gi.repository import GLib
 
75
import gobject
 
76
import avahi
86
77
from dbus.mainloop.glib import DBusGMainLoop
87
78
import ctypes
88
79
import ctypes.util
89
80
import xml.dom.minidom
90
81
import inspect
91
82
 
92
 
if sys.version_info.major == 2:
93
 
    __metaclass__ = type
94
 
    str = unicode
95
 
 
96
 
# Show warnings by default
97
 
if not sys.warnoptions:
98
 
    import warnings
99
 
    warnings.simplefilter("default")
100
 
 
101
 
# Try to find the value of SO_BINDTODEVICE:
102
83
try:
103
 
    # This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and
104
 
    # newer, and it is also the most natural place for it:
105
84
    SO_BINDTODEVICE = socket.SO_BINDTODEVICE
106
85
except AttributeError:
107
86
    try:
108
 
        # This is where SO_BINDTODEVICE was up to and including Python
109
 
        # 2.6, and also 3.2:
110
87
        from IN import SO_BINDTODEVICE
111
88
    except ImportError:
112
 
        # In Python 2.7 it seems to have been removed entirely.
113
 
        # Try running the C preprocessor:
114
 
        try:
115
 
            cc = subprocess.Popen(["cc", "--language=c", "-E",
116
 
                                   "/dev/stdin"],
117
 
                                  stdin=subprocess.PIPE,
118
 
                                  stdout=subprocess.PIPE)
119
 
            stdout = cc.communicate(
120
 
                "#include <sys/socket.h>\nSO_BINDTODEVICE\n")[0]
121
 
            SO_BINDTODEVICE = int(stdout.splitlines()[-1])
122
 
        except (OSError, ValueError, IndexError):
123
 
            # No value found
124
 
            SO_BINDTODEVICE = None
125
 
 
126
 
if sys.version_info < (3, 2):
127
 
    configparser.Configparser = configparser.SafeConfigParser
128
 
 
129
 
version = "1.8.9"
 
89
        SO_BINDTODEVICE = None
 
90
 
 
91
if sys.version_info.major == 2:
 
92
    str = unicode
 
93
 
 
94
version = "1.6.9"
130
95
stored_state_file = "clients.pickle"
131
96
 
132
97
logger = logging.getLogger()
133
 
logging.captureWarnings(True)   # Show warnings via the logging system
134
98
syslogger = None
135
99
 
136
100
try:
137
101
    if_nametoindex = ctypes.cdll.LoadLibrary(
138
102
        ctypes.util.find_library("c")).if_nametoindex
139
103
except (OSError, AttributeError):
140
 
 
 
104
    
141
105
    def if_nametoindex(interface):
142
106
        "Get an interface index the hard way, i.e. using fcntl()"
143
107
        SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
148
112
        return interface_index
149
113
 
150
114
 
151
 
def copy_function(func):
152
 
    """Make a copy of a function"""
153
 
    if sys.version_info.major == 2:
154
 
        return types.FunctionType(func.func_code,
155
 
                                  func.func_globals,
156
 
                                  func.func_name,
157
 
                                  func.func_defaults,
158
 
                                  func.func_closure)
159
 
    else:
160
 
        return types.FunctionType(func.__code__,
161
 
                                  func.__globals__,
162
 
                                  func.__name__,
163
 
                                  func.__defaults__,
164
 
                                  func.__closure__)
165
 
 
166
 
 
167
115
def initlogger(debug, level=logging.WARNING):
168
116
    """init logger and add loglevel"""
169
 
 
 
117
    
170
118
    global syslogger
171
119
    syslogger = (logging.handlers.SysLogHandler(
172
 
        facility=logging.handlers.SysLogHandler.LOG_DAEMON,
173
 
        address="/dev/log"))
 
120
        facility = logging.handlers.SysLogHandler.LOG_DAEMON,
 
121
        address = "/dev/log"))
174
122
    syslogger.setFormatter(logging.Formatter
175
123
                           ('Mandos [%(process)d]: %(levelname)s:'
176
124
                            ' %(message)s'))
177
125
    logger.addHandler(syslogger)
178
 
 
 
126
    
179
127
    if debug:
180
128
        console = logging.StreamHandler()
181
129
        console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
191
139
    pass
192
140
 
193
141
 
194
 
class PGPEngine:
 
142
class PGPEngine(object):
195
143
    """A simple class for OpenPGP symmetric encryption & decryption"""
196
 
 
 
144
    
197
145
    def __init__(self):
198
146
        self.tempdir = tempfile.mkdtemp(prefix="mandos-")
199
 
        self.gpg = "gpg"
200
 
        try:
201
 
            output = subprocess.check_output(["gpgconf"])
202
 
            for line in output.splitlines():
203
 
                name, text, path = line.split(b":")
204
 
                if name == b"gpg":
205
 
                    self.gpg = path
206
 
                    break
207
 
        except OSError as e:
208
 
            if e.errno != errno.ENOENT:
209
 
                raise
210
147
        self.gnupgargs = ['--batch',
211
 
                          '--homedir', self.tempdir,
 
148
                          '--home', self.tempdir,
212
149
                          '--force-mdc',
213
 
                          '--quiet']
214
 
        # Only GPG version 1 has the --no-use-agent option.
215
 
        if self.gpg == b"gpg" or self.gpg.endswith(b"/gpg"):
216
 
            self.gnupgargs.append("--no-use-agent")
217
 
 
 
150
                          '--quiet',
 
151
                          '--no-use-agent']
 
152
    
218
153
    def __enter__(self):
219
154
        return self
220
 
 
 
155
    
221
156
    def __exit__(self, exc_type, exc_value, traceback):
222
157
        self._cleanup()
223
158
        return False
224
 
 
 
159
    
225
160
    def __del__(self):
226
161
        self._cleanup()
227
 
 
 
162
    
228
163
    def _cleanup(self):
229
164
        if self.tempdir is not None:
230
165
            # Delete contents of tempdir
231
166
            for root, dirs, files in os.walk(self.tempdir,
232
 
                                             topdown=False):
 
167
                                             topdown = False):
233
168
                for filename in files:
234
169
                    os.remove(os.path.join(root, filename))
235
170
                for dirname in dirs:
237
172
            # Remove tempdir
238
173
            os.rmdir(self.tempdir)
239
174
            self.tempdir = None
240
 
 
 
175
    
241
176
    def password_encode(self, password):
242
177
        # Passphrase can not be empty and can not contain newlines or
243
178
        # NUL bytes.  So we prefix it and hex encode it.
248
183
                       .replace(b"\n", b"\\n")
249
184
                       .replace(b"\0", b"\\x00"))
250
185
        return encoded
251
 
 
 
186
    
252
187
    def encrypt(self, data, password):
253
188
        passphrase = self.password_encode(password)
254
189
        with tempfile.NamedTemporaryFile(
255
190
                dir=self.tempdir) as passfile:
256
191
            passfile.write(passphrase)
257
192
            passfile.flush()
258
 
            proc = subprocess.Popen([self.gpg, '--symmetric',
 
193
            proc = subprocess.Popen(['gpg', '--symmetric',
259
194
                                     '--passphrase-file',
260
195
                                     passfile.name]
261
196
                                    + self.gnupgargs,
262
 
                                    stdin=subprocess.PIPE,
263
 
                                    stdout=subprocess.PIPE,
264
 
                                    stderr=subprocess.PIPE)
265
 
            ciphertext, err = proc.communicate(input=data)
 
197
                                    stdin = subprocess.PIPE,
 
198
                                    stdout = subprocess.PIPE,
 
199
                                    stderr = subprocess.PIPE)
 
200
            ciphertext, err = proc.communicate(input = data)
266
201
        if proc.returncode != 0:
267
202
            raise PGPError(err)
268
203
        return ciphertext
269
 
 
 
204
    
270
205
    def decrypt(self, data, password):
271
206
        passphrase = self.password_encode(password)
272
207
        with tempfile.NamedTemporaryFile(
273
 
                dir=self.tempdir) as passfile:
 
208
                dir = self.tempdir) as passfile:
274
209
            passfile.write(passphrase)
275
210
            passfile.flush()
276
 
            proc = subprocess.Popen([self.gpg, '--decrypt',
 
211
            proc = subprocess.Popen(['gpg', '--decrypt',
277
212
                                     '--passphrase-file',
278
213
                                     passfile.name]
279
214
                                    + self.gnupgargs,
280
 
                                    stdin=subprocess.PIPE,
281
 
                                    stdout=subprocess.PIPE,
282
 
                                    stderr=subprocess.PIPE)
283
 
            decrypted_plaintext, err = proc.communicate(input=data)
 
215
                                    stdin = subprocess.PIPE,
 
216
                                    stdout = subprocess.PIPE,
 
217
                                    stderr = subprocess.PIPE)
 
218
            decrypted_plaintext, err = proc.communicate(input = data)
284
219
        if proc.returncode != 0:
285
220
            raise PGPError(err)
286
221
        return decrypted_plaintext
287
222
 
288
223
 
289
 
# Pretend that we have an Avahi module
290
 
class avahi:
291
 
    """This isn't so much a class as it is a module-like namespace."""
292
 
    IF_UNSPEC = -1               # avahi-common/address.h
293
 
    PROTO_UNSPEC = -1            # avahi-common/address.h
294
 
    PROTO_INET = 0               # avahi-common/address.h
295
 
    PROTO_INET6 = 1              # avahi-common/address.h
296
 
    DBUS_NAME = "org.freedesktop.Avahi"
297
 
    DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup"
298
 
    DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
299
 
    DBUS_PATH_SERVER = "/"
300
 
 
301
 
    @staticmethod
302
 
    def string_array_to_txt_array(t):
303
 
        return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
304
 
                           for s in t), signature="ay")
305
 
    ENTRY_GROUP_ESTABLISHED = 2  # avahi-common/defs.h
306
 
    ENTRY_GROUP_COLLISION = 3    # avahi-common/defs.h
307
 
    ENTRY_GROUP_FAILURE = 4      # avahi-common/defs.h
308
 
    SERVER_INVALID = 0           # avahi-common/defs.h
309
 
    SERVER_REGISTERING = 1       # avahi-common/defs.h
310
 
    SERVER_RUNNING = 2           # avahi-common/defs.h
311
 
    SERVER_COLLISION = 3         # avahi-common/defs.h
312
 
    SERVER_FAILURE = 4           # avahi-common/defs.h
313
 
 
314
 
 
315
224
class AvahiError(Exception):
316
225
    def __init__(self, value, *args, **kwargs):
317
226
        self.value = value
327
236
    pass
328
237
 
329
238
 
330
 
class AvahiService:
 
239
class AvahiService(object):
331
240
    """An Avahi (Zeroconf) service.
332
 
 
 
241
    
333
242
    Attributes:
334
243
    interface: integer; avahi.IF_UNSPEC or an interface index.
335
244
               Used to optionally bind to the specified interface.
347
256
    server: D-Bus Server
348
257
    bus: dbus.SystemBus()
349
258
    """
350
 
 
 
259
    
351
260
    def __init__(self,
352
 
                 interface=avahi.IF_UNSPEC,
353
 
                 name=None,
354
 
                 servicetype=None,
355
 
                 port=None,
356
 
                 TXT=None,
357
 
                 domain="",
358
 
                 host="",
359
 
                 max_renames=32768,
360
 
                 protocol=avahi.PROTO_UNSPEC,
361
 
                 bus=None):
 
261
                 interface = avahi.IF_UNSPEC,
 
262
                 name = None,
 
263
                 servicetype = None,
 
264
                 port = None,
 
265
                 TXT = None,
 
266
                 domain = "",
 
267
                 host = "",
 
268
                 max_renames = 32768,
 
269
                 protocol = avahi.PROTO_UNSPEC,
 
270
                 bus = None):
362
271
        self.interface = interface
363
272
        self.name = name
364
273
        self.type = servicetype
373
282
        self.server = None
374
283
        self.bus = bus
375
284
        self.entry_group_state_changed_match = None
376
 
 
 
285
    
377
286
    def rename(self, remove=True):
378
287
        """Derived from the Avahi example code"""
379
288
        if self.rename_count >= self.max_renames:
399
308
                logger.critical("D-Bus Exception", exc_info=error)
400
309
                self.cleanup()
401
310
                os._exit(1)
402
 
 
 
311
    
403
312
    def remove(self):
404
313
        """Derived from the Avahi example code"""
405
314
        if self.entry_group_state_changed_match is not None:
407
316
            self.entry_group_state_changed_match = None
408
317
        if self.group is not None:
409
318
            self.group.Reset()
410
 
 
 
319
    
411
320
    def add(self):
412
321
        """Derived from the Avahi example code"""
413
322
        self.remove()
430
339
            dbus.UInt16(self.port),
431
340
            avahi.string_array_to_txt_array(self.TXT))
432
341
        self.group.Commit()
433
 
 
 
342
    
434
343
    def entry_group_state_changed(self, state, error):
435
344
        """Derived from the Avahi example code"""
436
345
        logger.debug("Avahi entry group state change: %i", state)
437
 
 
 
346
        
438
347
        if state == avahi.ENTRY_GROUP_ESTABLISHED:
439
348
            logger.debug("Zeroconf service established.")
440
349
        elif state == avahi.ENTRY_GROUP_COLLISION:
444
353
            logger.critical("Avahi: Error in group state changed %s",
445
354
                            str(error))
446
355
            raise AvahiGroupError("State changed: {!s}".format(error))
447
 
 
 
356
    
448
357
    def cleanup(self):
449
358
        """Derived from the Avahi example code"""
450
359
        if self.group is not None:
455
364
                pass
456
365
            self.group = None
457
366
        self.remove()
458
 
 
 
367
    
459
368
    def server_state_changed(self, state, error=None):
460
369
        """Derived from the Avahi example code"""
461
370
        logger.debug("Avahi server state change: %i", state)
473
382
                    logger.error(bad_states[state] + ": %r", error)
474
383
            self.cleanup()
475
384
        elif state == avahi.SERVER_RUNNING:
476
 
            try:
477
 
                self.add()
478
 
            except dbus.exceptions.DBusException as error:
479
 
                if (error.get_dbus_name()
480
 
                    == "org.freedesktop.Avahi.CollisionError"):
481
 
                    logger.info("Local Zeroconf service name"
482
 
                                " collision.")
483
 
                    return self.rename(remove=False)
484
 
                else:
485
 
                    logger.critical("D-Bus Exception", exc_info=error)
486
 
                    self.cleanup()
487
 
                    os._exit(1)
 
385
            self.add()
488
386
        else:
489
387
            if error is None:
490
388
                logger.debug("Unknown state: %r", state)
491
389
            else:
492
390
                logger.debug("Unknown state: %r: %r", state, error)
493
 
 
 
391
    
494
392
    def activate(self):
495
393
        """Derived from the Avahi example code"""
496
394
        if self.server is None:
507
405
class AvahiServiceToSyslog(AvahiService):
508
406
    def rename(self, *args, **kwargs):
509
407
        """Add the new name to the syslog messages"""
510
 
        ret = super(AvahiServiceToSyslog, self).rename(*args, **kwargs)
 
408
        ret = AvahiService.rename(self, *args, **kwargs)
511
409
        syslogger.setFormatter(logging.Formatter(
512
410
            'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
513
411
            .format(self.name)))
514
412
        return ret
515
413
 
516
414
 
517
 
# Pretend that we have a GnuTLS module
518
 
class gnutls:
519
 
    """This isn't so much a class as it is a module-like namespace."""
520
 
 
521
 
    library = ctypes.util.find_library("gnutls")
522
 
    if library is None:
523
 
        library = ctypes.util.find_library("gnutls-deb0")
524
 
    _library = ctypes.cdll.LoadLibrary(library)
525
 
    del library
526
 
 
527
 
    # Unless otherwise indicated, the constants and types below are
528
 
    # all from the gnutls/gnutls.h C header file.
529
 
 
530
 
    # Constants
531
 
    E_SUCCESS = 0
532
 
    E_INTERRUPTED = -52
533
 
    E_AGAIN = -28
534
 
    CRT_OPENPGP = 2
535
 
    CRT_RAWPK = 3
536
 
    CLIENT = 2
537
 
    SHUT_RDWR = 0
538
 
    CRD_CERTIFICATE = 1
539
 
    E_NO_CERTIFICATE_FOUND = -49
540
 
    X509_FMT_DER = 0
541
 
    NO_TICKETS = 1<<10
542
 
    ENABLE_RAWPK = 1<<18
543
 
    CTYPE_PEERS = 3
544
 
    KEYID_USE_SHA256 = 1        # gnutls/x509.h
545
 
    OPENPGP_FMT_RAW = 0         # gnutls/openpgp.h
546
 
 
547
 
    # Types
548
 
    class session_int(ctypes.Structure):
549
 
        _fields_ = []
550
 
    session_t = ctypes.POINTER(session_int)
551
 
 
552
 
    class certificate_credentials_st(ctypes.Structure):
553
 
        _fields_ = []
554
 
    certificate_credentials_t = ctypes.POINTER(
555
 
        certificate_credentials_st)
556
 
    certificate_type_t = ctypes.c_int
557
 
 
558
 
    class datum_t(ctypes.Structure):
559
 
        _fields_ = [('data', ctypes.POINTER(ctypes.c_ubyte)),
560
 
                    ('size', ctypes.c_uint)]
561
 
 
562
 
    class openpgp_crt_int(ctypes.Structure):
563
 
        _fields_ = []
564
 
    openpgp_crt_t = ctypes.POINTER(openpgp_crt_int)
565
 
    openpgp_crt_fmt_t = ctypes.c_int  # gnutls/openpgp.h
566
 
    log_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
567
 
    credentials_type_t = ctypes.c_int
568
 
    transport_ptr_t = ctypes.c_void_p
569
 
    close_request_t = ctypes.c_int
570
 
 
571
 
    # Exceptions
572
 
    class Error(Exception):
573
 
        def __init__(self, message=None, code=None, args=()):
574
 
            # Default usage is by a message string, but if a return
575
 
            # code is passed, convert it to a string with
576
 
            # gnutls.strerror()
577
 
            self.code = code
578
 
            if message is None and code is not None:
579
 
                message = gnutls.strerror(code)
580
 
            return super(gnutls.Error, self).__init__(
581
 
                message, *args)
582
 
 
583
 
    class CertificateSecurityError(Error):
584
 
        pass
585
 
 
586
 
    # Classes
587
 
    class Credentials:
588
 
        def __init__(self):
589
 
            self._c_object = gnutls.certificate_credentials_t()
590
 
            gnutls.certificate_allocate_credentials(
591
 
                ctypes.byref(self._c_object))
592
 
            self.type = gnutls.CRD_CERTIFICATE
593
 
 
594
 
        def __del__(self):
595
 
            gnutls.certificate_free_credentials(self._c_object)
596
 
 
597
 
    class ClientSession:
598
 
        def __init__(self, socket, credentials=None):
599
 
            self._c_object = gnutls.session_t()
600
 
            gnutls_flags = gnutls.CLIENT
601
 
            if gnutls.check_version(b"3.5.6"):
602
 
                gnutls_flags |= gnutls.NO_TICKETS
603
 
            if gnutls.has_rawpk:
604
 
                gnutls_flags |= gnutls.ENABLE_RAWPK
605
 
            gnutls.init(ctypes.byref(self._c_object), gnutls_flags)
606
 
            del gnutls_flags
607
 
            gnutls.set_default_priority(self._c_object)
608
 
            gnutls.transport_set_ptr(self._c_object, socket.fileno())
609
 
            gnutls.handshake_set_private_extensions(self._c_object,
610
 
                                                    True)
611
 
            self.socket = socket
612
 
            if credentials is None:
613
 
                credentials = gnutls.Credentials()
614
 
            gnutls.credentials_set(self._c_object, credentials.type,
615
 
                                   ctypes.cast(credentials._c_object,
616
 
                                               ctypes.c_void_p))
617
 
            self.credentials = credentials
618
 
 
619
 
        def __del__(self):
620
 
            gnutls.deinit(self._c_object)
621
 
 
622
 
        def handshake(self):
623
 
            return gnutls.handshake(self._c_object)
624
 
 
625
 
        def send(self, data):
626
 
            data = bytes(data)
627
 
            data_len = len(data)
628
 
            while data_len > 0:
629
 
                data_len -= gnutls.record_send(self._c_object,
630
 
                                               data[-data_len:],
631
 
                                               data_len)
632
 
 
633
 
        def bye(self):
634
 
            return gnutls.bye(self._c_object, gnutls.SHUT_RDWR)
635
 
 
636
 
    # Error handling functions
637
 
    def _error_code(result):
638
 
        """A function to raise exceptions on errors, suitable
639
 
        for the 'restype' attribute on ctypes functions"""
640
 
        if result >= 0:
641
 
            return result
642
 
        if result == gnutls.E_NO_CERTIFICATE_FOUND:
643
 
            raise gnutls.CertificateSecurityError(code=result)
644
 
        raise gnutls.Error(code=result)
645
 
 
646
 
    def _retry_on_error(result, func, arguments):
647
 
        """A function to retry on some errors, suitable
648
 
        for the 'errcheck' attribute on ctypes functions"""
649
 
        while result < 0:
650
 
            if result not in (gnutls.E_INTERRUPTED, gnutls.E_AGAIN):
651
 
                return _error_code(result)
652
 
            result = func(*arguments)
653
 
        return result
654
 
 
655
 
    # Unless otherwise indicated, the function declarations below are
656
 
    # all from the gnutls/gnutls.h C header file.
657
 
 
658
 
    # Functions
659
 
    priority_set_direct = _library.gnutls_priority_set_direct
660
 
    priority_set_direct.argtypes = [session_t, ctypes.c_char_p,
661
 
                                    ctypes.POINTER(ctypes.c_char_p)]
662
 
    priority_set_direct.restype = _error_code
663
 
 
664
 
    init = _library.gnutls_init
665
 
    init.argtypes = [ctypes.POINTER(session_t), ctypes.c_int]
666
 
    init.restype = _error_code
667
 
 
668
 
    set_default_priority = _library.gnutls_set_default_priority
669
 
    set_default_priority.argtypes = [session_t]
670
 
    set_default_priority.restype = _error_code
671
 
 
672
 
    record_send = _library.gnutls_record_send
673
 
    record_send.argtypes = [session_t, ctypes.c_void_p,
674
 
                            ctypes.c_size_t]
675
 
    record_send.restype = ctypes.c_ssize_t
676
 
    record_send.errcheck = _retry_on_error
677
 
 
678
 
    certificate_allocate_credentials = (
679
 
        _library.gnutls_certificate_allocate_credentials)
680
 
    certificate_allocate_credentials.argtypes = [
681
 
        ctypes.POINTER(certificate_credentials_t)]
682
 
    certificate_allocate_credentials.restype = _error_code
683
 
 
684
 
    certificate_free_credentials = (
685
 
        _library.gnutls_certificate_free_credentials)
686
 
    certificate_free_credentials.argtypes = [
687
 
        certificate_credentials_t]
688
 
    certificate_free_credentials.restype = None
689
 
 
690
 
    handshake_set_private_extensions = (
691
 
        _library.gnutls_handshake_set_private_extensions)
692
 
    handshake_set_private_extensions.argtypes = [session_t,
693
 
                                                 ctypes.c_int]
694
 
    handshake_set_private_extensions.restype = None
695
 
 
696
 
    credentials_set = _library.gnutls_credentials_set
697
 
    credentials_set.argtypes = [session_t, credentials_type_t,
698
 
                                ctypes.c_void_p]
699
 
    credentials_set.restype = _error_code
700
 
 
701
 
    strerror = _library.gnutls_strerror
702
 
    strerror.argtypes = [ctypes.c_int]
703
 
    strerror.restype = ctypes.c_char_p
704
 
 
705
 
    certificate_type_get = _library.gnutls_certificate_type_get
706
 
    certificate_type_get.argtypes = [session_t]
707
 
    certificate_type_get.restype = _error_code
708
 
 
709
 
    certificate_get_peers = _library.gnutls_certificate_get_peers
710
 
    certificate_get_peers.argtypes = [session_t,
711
 
                                      ctypes.POINTER(ctypes.c_uint)]
712
 
    certificate_get_peers.restype = ctypes.POINTER(datum_t)
713
 
 
714
 
    global_set_log_level = _library.gnutls_global_set_log_level
715
 
    global_set_log_level.argtypes = [ctypes.c_int]
716
 
    global_set_log_level.restype = None
717
 
 
718
 
    global_set_log_function = _library.gnutls_global_set_log_function
719
 
    global_set_log_function.argtypes = [log_func]
720
 
    global_set_log_function.restype = None
721
 
 
722
 
    deinit = _library.gnutls_deinit
723
 
    deinit.argtypes = [session_t]
724
 
    deinit.restype = None
725
 
 
726
 
    handshake = _library.gnutls_handshake
727
 
    handshake.argtypes = [session_t]
728
 
    handshake.restype = _error_code
729
 
    handshake.errcheck = _retry_on_error
730
 
 
731
 
    transport_set_ptr = _library.gnutls_transport_set_ptr
732
 
    transport_set_ptr.argtypes = [session_t, transport_ptr_t]
733
 
    transport_set_ptr.restype = None
734
 
 
735
 
    bye = _library.gnutls_bye
736
 
    bye.argtypes = [session_t, close_request_t]
737
 
    bye.restype = _error_code
738
 
    bye.errcheck = _retry_on_error
739
 
 
740
 
    check_version = _library.gnutls_check_version
741
 
    check_version.argtypes = [ctypes.c_char_p]
742
 
    check_version.restype = ctypes.c_char_p
743
 
 
744
 
    _need_version = b"3.3.0"
745
 
    if check_version(_need_version) is None:
746
 
        raise self.Error("Needs GnuTLS {} or later"
747
 
                         .format(_need_version))
748
 
 
749
 
    _tls_rawpk_version = b"3.6.6"
750
 
    has_rawpk = bool(check_version(_tls_rawpk_version))
751
 
 
752
 
    if has_rawpk:
753
 
        # Types
754
 
        class pubkey_st(ctypes.Structure):
755
 
            _fields = []
756
 
        pubkey_t = ctypes.POINTER(pubkey_st)
757
 
 
758
 
        x509_crt_fmt_t = ctypes.c_int
759
 
 
760
 
        # All the function declarations below are from gnutls/abstract.h
761
 
        pubkey_init = _library.gnutls_pubkey_init
762
 
        pubkey_init.argtypes = [ctypes.POINTER(pubkey_t)]
763
 
        pubkey_init.restype = _error_code
764
 
 
765
 
        pubkey_import = _library.gnutls_pubkey_import
766
 
        pubkey_import.argtypes = [pubkey_t, ctypes.POINTER(datum_t),
767
 
                                  x509_crt_fmt_t]
768
 
        pubkey_import.restype = _error_code
769
 
 
770
 
        pubkey_get_key_id = _library.gnutls_pubkey_get_key_id
771
 
        pubkey_get_key_id.argtypes = [pubkey_t, ctypes.c_int,
772
 
                                      ctypes.POINTER(ctypes.c_ubyte),
773
 
                                      ctypes.POINTER(ctypes.c_size_t)]
774
 
        pubkey_get_key_id.restype = _error_code
775
 
 
776
 
        pubkey_deinit = _library.gnutls_pubkey_deinit
777
 
        pubkey_deinit.argtypes = [pubkey_t]
778
 
        pubkey_deinit.restype = None
779
 
    else:
780
 
        # All the function declarations below are from gnutls/openpgp.h
781
 
 
782
 
        openpgp_crt_init = _library.gnutls_openpgp_crt_init
783
 
        openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
784
 
        openpgp_crt_init.restype = _error_code
785
 
 
786
 
        openpgp_crt_import = _library.gnutls_openpgp_crt_import
787
 
        openpgp_crt_import.argtypes = [openpgp_crt_t,
788
 
                                       ctypes.POINTER(datum_t),
789
 
                                       openpgp_crt_fmt_t]
790
 
        openpgp_crt_import.restype = _error_code
791
 
 
792
 
        openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
793
 
        openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
794
 
                                            ctypes.POINTER(ctypes.c_uint)]
795
 
        openpgp_crt_verify_self.restype = _error_code
796
 
 
797
 
        openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
798
 
        openpgp_crt_deinit.argtypes = [openpgp_crt_t]
799
 
        openpgp_crt_deinit.restype = None
800
 
 
801
 
        openpgp_crt_get_fingerprint = (
802
 
            _library.gnutls_openpgp_crt_get_fingerprint)
803
 
        openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
804
 
                                                ctypes.c_void_p,
805
 
                                                ctypes.POINTER(
806
 
                                                    ctypes.c_size_t)]
807
 
        openpgp_crt_get_fingerprint.restype = _error_code
808
 
 
809
 
    if check_version(b"3.6.4"):
810
 
        certificate_type_get2 = _library.gnutls_certificate_type_get2
811
 
        certificate_type_get2.argtypes = [session_t, ctypes.c_int]
812
 
        certificate_type_get2.restype = _error_code
813
 
 
814
 
    # Remove non-public functions
815
 
    del _error_code, _retry_on_error
816
 
 
817
 
 
818
 
def call_pipe(connection,       # : multiprocessing.Connection
819
 
              func, *args, **kwargs):
820
 
    """This function is meant to be called by multiprocessing.Process
821
 
 
822
 
    This function runs func(*args, **kwargs), and writes the resulting
823
 
    return value on the provided multiprocessing.Connection.
824
 
    """
825
 
    connection.send(func(*args, **kwargs))
826
 
    connection.close()
827
 
 
828
 
 
829
 
class Client:
 
415
class Client(object):
830
416
    """A representation of a client host served by this server.
831
 
 
 
417
    
832
418
    Attributes:
833
419
    approved:   bool(); 'None' if not yet approved/disapproved
834
420
    approval_delay: datetime.timedelta(); Time to wait for approval
835
421
    approval_duration: datetime.timedelta(); Duration of one approval
836
 
    checker: multiprocessing.Process(); a running checker process used
837
 
             to see if the client lives. 'None' if no process is
838
 
             running.
839
 
    checker_callback_tag: a GLib event source tag, or None
 
422
    checker:    subprocess.Popen(); a running checker process used
 
423
                                    to see if the client lives.
 
424
                                    'None' if no process is running.
 
425
    checker_callback_tag: a gobject event source tag, or None
840
426
    checker_command: string; External command which is run to check
841
427
                     if client lives.  %() expansions are done at
842
428
                     runtime with vars(self) as dict, so that for
843
429
                     instance %(name)s can be used in the command.
844
 
    checker_initiator_tag: a GLib event source tag, or None
 
430
    checker_initiator_tag: a gobject event source tag, or None
845
431
    created:    datetime.datetime(); (UTC) object creation
846
432
    client_structure: Object describing what attributes a client has
847
433
                      and is used for storing the client at exit
848
434
    current_checker_command: string; current running checker_command
849
 
    disable_initiator_tag: a GLib event source tag, or None
 
435
    disable_initiator_tag: a gobject event source tag, or None
850
436
    enabled:    bool()
851
437
    fingerprint: string (40 or 32 hexadecimal digits); used to
852
 
                 uniquely identify an OpenPGP client
853
 
    key_id: string (64 hexadecimal digits); used to uniquely identify
854
 
            a client using raw public keys
 
438
                 uniquely identify the client
855
439
    host:       string; available for use by the checker command
856
440
    interval:   datetime.timedelta(); How often to start a new checker
857
441
    last_approval_request: datetime.datetime(); (UTC) or None
859
443
    last_checker_status: integer between 0 and 255 reflecting exit
860
444
                         status of last checker. -1 reflects crashed
861
445
                         checker, -2 means no checker completed yet.
862
 
    last_checker_signal: The signal which killed the last checker, if
863
 
                         last_checker_status is -1
864
446
    last_enabled: datetime.datetime(); (UTC) or None
865
447
    name:       string; from the config file, used in log messages and
866
448
                        D-Bus identifiers
873
455
                disabled, or None
874
456
    server_settings: The server_settings dict from main()
875
457
    """
876
 
 
 
458
    
877
459
    runtime_expansions = ("approval_delay", "approval_duration",
878
 
                          "created", "enabled", "expires", "key_id",
 
460
                          "created", "enabled", "expires",
879
461
                          "fingerprint", "host", "interval",
880
462
                          "last_approval_request", "last_checked_ok",
881
463
                          "last_enabled", "name", "timeout")
890
472
        "approved_by_default": "True",
891
473
        "enabled": "True",
892
474
    }
893
 
 
 
475
    
894
476
    @staticmethod
895
477
    def config_parser(config):
896
478
        """Construct a new dict of client settings of this form:
903
485
        for client_name in config.sections():
904
486
            section = dict(config.items(client_name))
905
487
            client = settings[client_name] = {}
906
 
 
 
488
            
907
489
            client["host"] = section["host"]
908
490
            # Reformat values from string types to Python types
909
491
            client["approved_by_default"] = config.getboolean(
910
492
                client_name, "approved_by_default")
911
493
            client["enabled"] = config.getboolean(client_name,
912
494
                                                  "enabled")
913
 
 
914
 
            # Uppercase and remove spaces from key_id and fingerprint
915
 
            # for later comparison purposes with return value from the
916
 
            # key_id() and fingerprint() functions
917
 
            client["key_id"] = (section.get("key_id", "").upper()
918
 
                                .replace(" ", ""))
 
495
            
 
496
            # Uppercase and remove spaces from fingerprint for later
 
497
            # comparison purposes with return value from the
 
498
            # fingerprint() function
919
499
            client["fingerprint"] = (section["fingerprint"].upper()
920
500
                                     .replace(" ", ""))
921
501
            if "secret" in section:
922
 
                client["secret"] = codecs.decode(section["secret"]
923
 
                                                 .encode("utf-8"),
924
 
                                                 "base64")
 
502
                client["secret"] = section["secret"].decode("base64")
925
503
            elif "secfile" in section:
926
504
                with open(os.path.expanduser(os.path.expandvars
927
505
                                             (section["secfile"])),
942
520
            client["last_approval_request"] = None
943
521
            client["last_checked_ok"] = None
944
522
            client["last_checker_status"] = -2
945
 
 
 
523
        
946
524
        return settings
947
 
 
948
 
    def __init__(self, settings, name=None, server_settings=None):
 
525
    
 
526
    def __init__(self, settings, name = None, server_settings=None):
949
527
        self.name = name
950
528
        if server_settings is None:
951
529
            server_settings = {}
953
531
        # adding all client settings
954
532
        for setting, value in settings.items():
955
533
            setattr(self, setting, value)
956
 
 
 
534
        
957
535
        if self.enabled:
958
536
            if not hasattr(self, "last_enabled"):
959
537
                self.last_enabled = datetime.datetime.utcnow()
963
541
        else:
964
542
            self.last_enabled = None
965
543
            self.expires = None
966
 
 
 
544
        
967
545
        logger.debug("Creating client %r", self.name)
968
 
        logger.debug("  Key ID: %s", self.key_id)
969
546
        logger.debug("  Fingerprint: %s", self.fingerprint)
970
547
        self.created = settings.get("created",
971
548
                                    datetime.datetime.utcnow())
972
 
 
 
549
        
973
550
        # attributes specific for this server instance
974
551
        self.checker = None
975
552
        self.checker_initiator_tag = None
981
558
        self.changedstate = multiprocessing_manager.Condition(
982
559
            multiprocessing_manager.Lock())
983
560
        self.client_structure = [attr
984
 
                                 for attr in self.__dict__.keys()
 
561
                                 for attr in self.__dict__.iterkeys()
985
562
                                 if not attr.startswith("_")]
986
563
        self.client_structure.append("client_structure")
987
 
 
 
564
        
988
565
        for name, t in inspect.getmembers(
989
566
                type(self), lambda obj: isinstance(obj, property)):
990
567
            if not name.startswith("_"):
991
568
                self.client_structure.append(name)
992
 
 
 
569
    
993
570
    # Send notice to process children that client state has changed
994
571
    def send_changedstate(self):
995
572
        with self.changedstate:
996
573
            self.changedstate.notify_all()
997
 
 
 
574
    
998
575
    def enable(self):
999
576
        """Start this client's checker and timeout hooks"""
1000
577
        if getattr(self, "enabled", False):
1005
582
        self.last_enabled = datetime.datetime.utcnow()
1006
583
        self.init_checker()
1007
584
        self.send_changedstate()
1008
 
 
 
585
    
1009
586
    def disable(self, quiet=True):
1010
587
        """Disable this client."""
1011
588
        if not getattr(self, "enabled", False):
1013
590
        if not quiet:
1014
591
            logger.info("Disabling client %s", self.name)
1015
592
        if getattr(self, "disable_initiator_tag", None) is not None:
1016
 
            GLib.source_remove(self.disable_initiator_tag)
 
593
            gobject.source_remove(self.disable_initiator_tag)
1017
594
            self.disable_initiator_tag = None
1018
595
        self.expires = None
1019
596
        if getattr(self, "checker_initiator_tag", None) is not None:
1020
 
            GLib.source_remove(self.checker_initiator_tag)
 
597
            gobject.source_remove(self.checker_initiator_tag)
1021
598
            self.checker_initiator_tag = None
1022
599
        self.stop_checker()
1023
600
        self.enabled = False
1024
601
        if not quiet:
1025
602
            self.send_changedstate()
1026
 
        # Do not run this again if called by a GLib.timeout_add
 
603
        # Do not run this again if called by a gobject.timeout_add
1027
604
        return False
1028
 
 
 
605
    
1029
606
    def __del__(self):
1030
607
        self.disable()
1031
 
 
 
608
    
1032
609
    def init_checker(self):
1033
610
        # Schedule a new checker to be started an 'interval' from now,
1034
611
        # and every interval from then on.
1035
612
        if self.checker_initiator_tag is not None:
1036
 
            GLib.source_remove(self.checker_initiator_tag)
1037
 
        self.checker_initiator_tag = GLib.timeout_add(
 
613
            gobject.source_remove(self.checker_initiator_tag)
 
614
        self.checker_initiator_tag = gobject.timeout_add(
1038
615
            int(self.interval.total_seconds() * 1000),
1039
616
            self.start_checker)
1040
617
        # Schedule a disable() when 'timeout' has passed
1041
618
        if self.disable_initiator_tag is not None:
1042
 
            GLib.source_remove(self.disable_initiator_tag)
1043
 
        self.disable_initiator_tag = GLib.timeout_add(
 
619
            gobject.source_remove(self.disable_initiator_tag)
 
620
        self.disable_initiator_tag = gobject.timeout_add(
1044
621
            int(self.timeout.total_seconds() * 1000), self.disable)
1045
622
        # Also start a new checker *right now*.
1046
623
        self.start_checker()
1047
 
 
1048
 
    def checker_callback(self, source, condition, connection,
1049
 
                         command):
 
624
    
 
625
    def checker_callback(self, pid, condition, command):
1050
626
        """The checker has completed, so take appropriate actions."""
1051
 
        # Read return code from connection (see call_pipe)
1052
 
        returncode = connection.recv()
1053
 
        connection.close()
1054
 
        if self.checker is not None:
1055
 
            self.checker.join()
1056
627
        self.checker_callback_tag = None
1057
628
        self.checker = None
1058
 
 
1059
 
        if returncode >= 0:
1060
 
            self.last_checker_status = returncode
1061
 
            self.last_checker_signal = None
 
629
        if os.WIFEXITED(condition):
 
630
            self.last_checker_status = os.WEXITSTATUS(condition)
1062
631
            if self.last_checker_status == 0:
1063
632
                logger.info("Checker for %(name)s succeeded",
1064
633
                            vars(self))
1067
636
                logger.info("Checker for %(name)s failed", vars(self))
1068
637
        else:
1069
638
            self.last_checker_status = -1
1070
 
            self.last_checker_signal = -returncode
1071
639
            logger.warning("Checker for %(name)s crashed?",
1072
640
                           vars(self))
1073
 
        return False
1074
 
 
 
641
    
1075
642
    def checked_ok(self):
1076
643
        """Assert that the client has been seen, alive and well."""
1077
644
        self.last_checked_ok = datetime.datetime.utcnow()
1078
645
        self.last_checker_status = 0
1079
 
        self.last_checker_signal = None
1080
646
        self.bump_timeout()
1081
 
 
 
647
    
1082
648
    def bump_timeout(self, timeout=None):
1083
649
        """Bump up the timeout for this client."""
1084
650
        if timeout is None:
1085
651
            timeout = self.timeout
1086
652
        if self.disable_initiator_tag is not None:
1087
 
            GLib.source_remove(self.disable_initiator_tag)
 
653
            gobject.source_remove(self.disable_initiator_tag)
1088
654
            self.disable_initiator_tag = None
1089
655
        if getattr(self, "enabled", False):
1090
 
            self.disable_initiator_tag = GLib.timeout_add(
 
656
            self.disable_initiator_tag = gobject.timeout_add(
1091
657
                int(timeout.total_seconds() * 1000), self.disable)
1092
658
            self.expires = datetime.datetime.utcnow() + timeout
1093
 
 
 
659
    
1094
660
    def need_approval(self):
1095
661
        self.last_approval_request = datetime.datetime.utcnow()
1096
 
 
 
662
    
1097
663
    def start_checker(self):
1098
664
        """Start a new checker subprocess if one is not running.
1099
 
 
 
665
        
1100
666
        If a checker already exists, leave it running and do
1101
667
        nothing."""
1102
668
        # The reason for not killing a running checker is that if we
1107
673
        # checkers alone, the checker would have to take more time
1108
674
        # than 'timeout' for the client to be disabled, which is as it
1109
675
        # should be.
1110
 
 
1111
 
        if self.checker is not None and not self.checker.is_alive():
1112
 
            logger.warning("Checker was not alive; joining")
1113
 
            self.checker.join()
1114
 
            self.checker = None
 
676
        
 
677
        # If a checker exists, make sure it is not a zombie
 
678
        try:
 
679
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
680
        except AttributeError:
 
681
            pass
 
682
        except OSError as error:
 
683
            if error.errno != errno.ECHILD:
 
684
                raise
 
685
        else:
 
686
            if pid:
 
687
                logger.warning("Checker was a zombie")
 
688
                gobject.source_remove(self.checker_callback_tag)
 
689
                self.checker_callback(pid, status,
 
690
                                      self.current_checker_command)
1115
691
        # Start a new checker if needed
1116
692
        if self.checker is None:
1117
693
            # Escape attributes for the shell
1118
694
            escaped_attrs = {
1119
695
                attr: re.escape(str(getattr(self, attr)))
1120
 
                for attr in self.runtime_expansions}
 
696
                for attr in self.runtime_expansions }
1121
697
            try:
1122
698
                command = self.checker_command % escaped_attrs
1123
699
            except TypeError as error:
1126
702
                             exc_info=error)
1127
703
                return True     # Try again later
1128
704
            self.current_checker_command = command
1129
 
            logger.info("Starting checker %r for %s", command,
1130
 
                        self.name)
1131
 
            # We don't need to redirect stdout and stderr, since
1132
 
            # in normal mode, that is already done by daemon(),
1133
 
            # and in debug mode we don't want to.  (Stdin is
1134
 
            # always replaced by /dev/null.)
1135
 
            # The exception is when not debugging but nevertheless
1136
 
            # running in the foreground; use the previously
1137
 
            # created wnull.
1138
 
            popen_args = {"close_fds": True,
1139
 
                          "shell": True,
1140
 
                          "cwd": "/"}
1141
 
            if (not self.server_settings["debug"]
1142
 
                and self.server_settings["foreground"]):
1143
 
                popen_args.update({"stdout": wnull,
1144
 
                                   "stderr": wnull})
1145
 
            pipe = multiprocessing.Pipe(duplex=False)
1146
 
            self.checker = multiprocessing.Process(
1147
 
                target=call_pipe,
1148
 
                args=(pipe[1], subprocess.call, command),
1149
 
                kwargs=popen_args)
1150
 
            self.checker.start()
1151
 
            self.checker_callback_tag = GLib.io_add_watch(
1152
 
                GLib.IOChannel.unix_new(pipe[0].fileno()),
1153
 
                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
1154
 
                self.checker_callback, pipe[0], command)
1155
 
        # Re-run this periodically if run by GLib.timeout_add
 
705
            try:
 
706
                logger.info("Starting checker %r for %s", command,
 
707
                            self.name)
 
708
                # We don't need to redirect stdout and stderr, since
 
709
                # in normal mode, that is already done by daemon(),
 
710
                # and in debug mode we don't want to.  (Stdin is
 
711
                # always replaced by /dev/null.)
 
712
                # The exception is when not debugging but nevertheless
 
713
                # running in the foreground; use the previously
 
714
                # created wnull.
 
715
                popen_args = {}
 
716
                if (not self.server_settings["debug"]
 
717
                    and self.server_settings["foreground"]):
 
718
                    popen_args.update({"stdout": wnull,
 
719
                                       "stderr": wnull })
 
720
                self.checker = subprocess.Popen(command,
 
721
                                                close_fds=True,
 
722
                                                shell=True,
 
723
                                                cwd="/",
 
724
                                                **popen_args)
 
725
            except OSError as error:
 
726
                logger.error("Failed to start subprocess",
 
727
                             exc_info=error)
 
728
                return True
 
729
            self.checker_callback_tag = gobject.child_watch_add(
 
730
                self.checker.pid, self.checker_callback, data=command)
 
731
            # The checker may have completed before the gobject
 
732
            # watch was added.  Check for this.
 
733
            try:
 
734
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
735
            except OSError as error:
 
736
                if error.errno == errno.ECHILD:
 
737
                    # This should never happen
 
738
                    logger.error("Child process vanished",
 
739
                                 exc_info=error)
 
740
                    return True
 
741
                raise
 
742
            if pid:
 
743
                gobject.source_remove(self.checker_callback_tag)
 
744
                self.checker_callback(pid, status, command)
 
745
        # Re-run this periodically if run by gobject.timeout_add
1156
746
        return True
1157
 
 
 
747
    
1158
748
    def stop_checker(self):
1159
749
        """Force the checker process, if any, to stop."""
1160
750
        if self.checker_callback_tag:
1161
 
            GLib.source_remove(self.checker_callback_tag)
 
751
            gobject.source_remove(self.checker_callback_tag)
1162
752
            self.checker_callback_tag = None
1163
753
        if getattr(self, "checker", None) is None:
1164
754
            return
1165
755
        logger.debug("Stopping checker for %(name)s", vars(self))
1166
 
        self.checker.terminate()
 
756
        try:
 
757
            self.checker.terminate()
 
758
            #time.sleep(0.5)
 
759
            #if self.checker.poll() is None:
 
760
            #    self.checker.kill()
 
761
        except OSError as error:
 
762
            if error.errno != errno.ESRCH: # No such process
 
763
                raise
1167
764
        self.checker = None
1168
765
 
1169
766
 
1173
770
                          byte_arrays=False):
1174
771
    """Decorators for marking methods of a DBusObjectWithProperties to
1175
772
    become properties on the D-Bus.
1176
 
 
 
773
    
1177
774
    The decorated method will be called with no arguments by "Get"
1178
775
    and with one argument by "Set".
1179
 
 
 
776
    
1180
777
    The parameters, where they are supported, are the same as
1181
778
    dbus.service.method, except there is only "signature", since the
1182
779
    type from Get() and the type sent to Set() is the same.
1186
783
    if byte_arrays and signature != "ay":
1187
784
        raise ValueError("Byte arrays not supported for non-'ay'"
1188
785
                         " signature {!r}".format(signature))
1189
 
 
 
786
    
1190
787
    def decorator(func):
1191
788
        func._dbus_is_property = True
1192
789
        func._dbus_interface = dbus_interface
1195
792
        func._dbus_name = func.__name__
1196
793
        if func._dbus_name.endswith("_dbus_property"):
1197
794
            func._dbus_name = func._dbus_name[:-14]
1198
 
        func._dbus_get_args_options = {'byte_arrays': byte_arrays}
 
795
        func._dbus_get_args_options = {'byte_arrays': byte_arrays }
1199
796
        return func
1200
 
 
 
797
    
1201
798
    return decorator
1202
799
 
1203
800
 
1204
801
def dbus_interface_annotations(dbus_interface):
1205
802
    """Decorator for marking functions returning interface annotations
1206
 
 
 
803
    
1207
804
    Usage:
1208
 
 
 
805
    
1209
806
    @dbus_interface_annotations("org.example.Interface")
1210
807
    def _foo(self):  # Function name does not matter
1211
808
        return {"org.freedesktop.DBus.Deprecated": "true",
1212
809
                "org.freedesktop.DBus.Property.EmitsChangedSignal":
1213
810
                    "false"}
1214
811
    """
1215
 
 
 
812
    
1216
813
    def decorator(func):
1217
814
        func._dbus_is_interface = True
1218
815
        func._dbus_interface = dbus_interface
1219
816
        func._dbus_name = dbus_interface
1220
817
        return func
1221
 
 
 
818
    
1222
819
    return decorator
1223
820
 
1224
821
 
1225
822
def dbus_annotations(annotations):
1226
823
    """Decorator to annotate D-Bus methods, signals or properties
1227
824
    Usage:
1228
 
 
 
825
    
1229
826
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true",
1230
827
                       "org.freedesktop.DBus.Property."
1231
828
                       "EmitsChangedSignal": "false"})
1233
830
                           access="r")
1234
831
    def Property_dbus_property(self):
1235
832
        return dbus.Boolean(False)
1236
 
 
1237
 
    See also the DBusObjectWithAnnotations class.
1238
833
    """
1239
 
 
 
834
    
1240
835
    def decorator(func):
1241
836
        func._dbus_annotations = annotations
1242
837
        return func
1243
 
 
 
838
    
1244
839
    return decorator
1245
840
 
1246
841
 
1262
857
    pass
1263
858
 
1264
859
 
1265
 
class DBusObjectWithAnnotations(dbus.service.Object):
1266
 
    """A D-Bus object with annotations.
1267
 
 
1268
 
    Classes inheriting from this can use the dbus_annotations
1269
 
    decorator to add annotations to methods or signals.
 
860
class DBusObjectWithProperties(dbus.service.Object):
 
861
    """A D-Bus object with properties.
 
862
    
 
863
    Classes inheriting from this can use the dbus_service_property
 
864
    decorator to expose methods as D-Bus properties.  It exposes the
 
865
    standard Get(), Set(), and GetAll() methods on the D-Bus.
1270
866
    """
1271
 
 
 
867
    
1272
868
    @staticmethod
1273
869
    def _is_dbus_thing(thing):
1274
870
        """Returns a function testing if an attribute is a D-Bus thing
1275
 
 
 
871
        
1276
872
        If called like _is_dbus_thing("method") it returns a function
1277
873
        suitable for use as predicate to inspect.getmembers().
1278
874
        """
1279
875
        return lambda obj: getattr(obj, "_dbus_is_{}".format(thing),
1280
876
                                   False)
1281
 
 
 
877
    
1282
878
    def _get_all_dbus_things(self, thing):
1283
879
        """Returns a generator of (name, attribute) pairs
1284
880
        """
1287
883
                for cls in self.__class__.__mro__
1288
884
                for name, athing in
1289
885
                inspect.getmembers(cls, self._is_dbus_thing(thing)))
1290
 
 
1291
 
    @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1292
 
                         out_signature="s",
1293
 
                         path_keyword='object_path',
1294
 
                         connection_keyword='connection')
1295
 
    def Introspect(self, object_path, connection):
1296
 
        """Overloading of standard D-Bus method.
1297
 
 
1298
 
        Inserts annotation tags on methods and signals.
1299
 
        """
1300
 
        xmlstring = dbus.service.Object.Introspect(self, object_path,
1301
 
                                                   connection)
1302
 
        try:
1303
 
            document = xml.dom.minidom.parseString(xmlstring)
1304
 
 
1305
 
            for if_tag in document.getElementsByTagName("interface"):
1306
 
                # Add annotation tags
1307
 
                for typ in ("method", "signal"):
1308
 
                    for tag in if_tag.getElementsByTagName(typ):
1309
 
                        annots = dict()
1310
 
                        for name, prop in (self.
1311
 
                                           _get_all_dbus_things(typ)):
1312
 
                            if (name == tag.getAttribute("name")
1313
 
                                and prop._dbus_interface
1314
 
                                == if_tag.getAttribute("name")):
1315
 
                                annots.update(getattr(
1316
 
                                    prop, "_dbus_annotations", {}))
1317
 
                        for name, value in annots.items():
1318
 
                            ann_tag = document.createElement(
1319
 
                                "annotation")
1320
 
                            ann_tag.setAttribute("name", name)
1321
 
                            ann_tag.setAttribute("value", value)
1322
 
                            tag.appendChild(ann_tag)
1323
 
                # Add interface annotation tags
1324
 
                for annotation, value in dict(
1325
 
                    itertools.chain.from_iterable(
1326
 
                        annotations().items()
1327
 
                        for name, annotations
1328
 
                        in self._get_all_dbus_things("interface")
1329
 
                        if name == if_tag.getAttribute("name")
1330
 
                        )).items():
1331
 
                    ann_tag = document.createElement("annotation")
1332
 
                    ann_tag.setAttribute("name", annotation)
1333
 
                    ann_tag.setAttribute("value", value)
1334
 
                    if_tag.appendChild(ann_tag)
1335
 
                # Fix argument name for the Introspect method itself
1336
 
                if (if_tag.getAttribute("name")
1337
 
                    == dbus.INTROSPECTABLE_IFACE):
1338
 
                    for cn in if_tag.getElementsByTagName("method"):
1339
 
                        if cn.getAttribute("name") == "Introspect":
1340
 
                            for arg in cn.getElementsByTagName("arg"):
1341
 
                                if (arg.getAttribute("direction")
1342
 
                                    == "out"):
1343
 
                                    arg.setAttribute("name",
1344
 
                                                     "xml_data")
1345
 
            xmlstring = document.toxml("utf-8")
1346
 
            document.unlink()
1347
 
        except (AttributeError, xml.dom.DOMException,
1348
 
                xml.parsers.expat.ExpatError) as error:
1349
 
            logger.error("Failed to override Introspection method",
1350
 
                         exc_info=error)
1351
 
        return xmlstring
1352
 
 
1353
 
 
1354
 
class DBusObjectWithProperties(DBusObjectWithAnnotations):
1355
 
    """A D-Bus object with properties.
1356
 
 
1357
 
    Classes inheriting from this can use the dbus_service_property
1358
 
    decorator to expose methods as D-Bus properties.  It exposes the
1359
 
    standard Get(), Set(), and GetAll() methods on the D-Bus.
1360
 
    """
1361
 
 
 
886
    
1362
887
    def _get_dbus_property(self, interface_name, property_name):
1363
888
        """Returns a bound method if one exists which is a D-Bus
1364
889
        property with the specified name and interface.
1369
894
                if (value._dbus_name == property_name
1370
895
                    and value._dbus_interface == interface_name):
1371
896
                    return value.__get__(self)
1372
 
 
 
897
        
1373
898
        # No such property
1374
899
        raise DBusPropertyNotFound("{}:{}.{}".format(
1375
900
            self.dbus_object_path, interface_name, property_name))
1376
 
 
1377
 
    @classmethod
1378
 
    def _get_all_interface_names(cls):
1379
 
        """Get a sequence of all interfaces supported by an object"""
1380
 
        return (name for name in set(getattr(getattr(x, attr),
1381
 
                                             "_dbus_interface", None)
1382
 
                                     for x in (inspect.getmro(cls))
1383
 
                                     for attr in dir(x))
1384
 
                if name is not None)
1385
 
 
 
901
    
1386
902
    @dbus.service.method(dbus.PROPERTIES_IFACE,
1387
903
                         in_signature="ss",
1388
904
                         out_signature="v")
1396
912
        if not hasattr(value, "variant_level"):
1397
913
            return value
1398
914
        return type(value)(value, variant_level=value.variant_level+1)
1399
 
 
 
915
    
1400
916
    @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
1401
917
    def Set(self, interface_name, property_name, value):
1402
918
        """Standard D-Bus property Set() method, see D-Bus standard.
1411
927
                raise ValueError("Byte arrays not supported for non-"
1412
928
                                 "'ay' signature {!r}"
1413
929
                                 .format(prop._dbus_signature))
1414
 
            value = dbus.ByteArray(bytes(value))
 
930
            value = dbus.ByteArray(b''.join(chr(byte)
 
931
                                            for byte in value))
1415
932
        prop(value)
1416
 
 
 
933
    
1417
934
    @dbus.service.method(dbus.PROPERTIES_IFACE,
1418
935
                         in_signature="s",
1419
936
                         out_signature="a{sv}")
1420
937
    def GetAll(self, interface_name):
1421
938
        """Standard D-Bus property GetAll() method, see D-Bus
1422
939
        standard.
1423
 
 
 
940
        
1424
941
        Note: Will not include properties with access="write".
1425
942
        """
1426
943
        properties = {}
1437
954
                properties[name] = value
1438
955
                continue
1439
956
            properties[name] = type(value)(
1440
 
                value, variant_level=value.variant_level + 1)
 
957
                value, variant_level = value.variant_level + 1)
1441
958
        return dbus.Dictionary(properties, signature="sv")
1442
 
 
 
959
    
1443
960
    @dbus.service.signal(dbus.PROPERTIES_IFACE, signature="sa{sv}as")
1444
961
    def PropertiesChanged(self, interface_name, changed_properties,
1445
962
                          invalidated_properties):
1447
964
        standard.
1448
965
        """
1449
966
        pass
1450
 
 
 
967
    
1451
968
    @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1452
969
                         out_signature="s",
1453
970
                         path_keyword='object_path',
1454
971
                         connection_keyword='connection')
1455
972
    def Introspect(self, object_path, connection):
1456
973
        """Overloading of standard D-Bus method.
1457
 
 
 
974
        
1458
975
        Inserts property tags and interface annotation tags.
1459
976
        """
1460
 
        xmlstring = DBusObjectWithAnnotations.Introspect(self,
1461
 
                                                         object_path,
1462
 
                                                         connection)
 
977
        xmlstring = dbus.service.Object.Introspect(self, object_path,
 
978
                                                   connection)
1463
979
        try:
1464
980
            document = xml.dom.minidom.parseString(xmlstring)
1465
 
 
 
981
            
1466
982
            def make_tag(document, name, prop):
1467
983
                e = document.createElement("property")
1468
984
                e.setAttribute("name", name)
1469
985
                e.setAttribute("type", prop._dbus_signature)
1470
986
                e.setAttribute("access", prop._dbus_access)
1471
987
                return e
1472
 
 
 
988
            
1473
989
            for if_tag in document.getElementsByTagName("interface"):
1474
990
                # Add property tags
1475
991
                for tag in (make_tag(document, name, prop)
1478
994
                            if prop._dbus_interface
1479
995
                            == if_tag.getAttribute("name")):
1480
996
                    if_tag.appendChild(tag)
1481
 
                # Add annotation tags for properties
1482
 
                for tag in if_tag.getElementsByTagName("property"):
1483
 
                    annots = dict()
1484
 
                    for name, prop in self._get_all_dbus_things(
1485
 
                            "property"):
1486
 
                        if (name == tag.getAttribute("name")
1487
 
                            and prop._dbus_interface
1488
 
                            == if_tag.getAttribute("name")):
1489
 
                            annots.update(getattr(
1490
 
                                prop, "_dbus_annotations", {}))
1491
 
                    for name, value in annots.items():
1492
 
                        ann_tag = document.createElement(
1493
 
                            "annotation")
1494
 
                        ann_tag.setAttribute("name", name)
1495
 
                        ann_tag.setAttribute("value", value)
1496
 
                        tag.appendChild(ann_tag)
 
997
                # Add annotation tags
 
998
                for typ in ("method", "signal", "property"):
 
999
                    for tag in if_tag.getElementsByTagName(typ):
 
1000
                        annots = dict()
 
1001
                        for name, prop in (self.
 
1002
                                           _get_all_dbus_things(typ)):
 
1003
                            if (name == tag.getAttribute("name")
 
1004
                                and prop._dbus_interface
 
1005
                                == if_tag.getAttribute("name")):
 
1006
                                annots.update(getattr(
 
1007
                                    prop, "_dbus_annotations", {}))
 
1008
                        for name, value in annots.items():
 
1009
                            ann_tag = document.createElement(
 
1010
                                "annotation")
 
1011
                            ann_tag.setAttribute("name", name)
 
1012
                            ann_tag.setAttribute("value", value)
 
1013
                            tag.appendChild(ann_tag)
 
1014
                # Add interface annotation tags
 
1015
                for annotation, value in dict(
 
1016
                    itertools.chain.from_iterable(
 
1017
                        annotations().items()
 
1018
                        for name, annotations
 
1019
                        in self._get_all_dbus_things("interface")
 
1020
                        if name == if_tag.getAttribute("name")
 
1021
                        )).items():
 
1022
                    ann_tag = document.createElement("annotation")
 
1023
                    ann_tag.setAttribute("name", annotation)
 
1024
                    ann_tag.setAttribute("value", value)
 
1025
                    if_tag.appendChild(ann_tag)
1497
1026
                # Add the names to the return values for the
1498
1027
                # "org.freedesktop.DBus.Properties" methods
1499
1028
                if (if_tag.getAttribute("name")
1518
1047
        return xmlstring
1519
1048
 
1520
1049
 
1521
 
try:
1522
 
    dbus.OBJECT_MANAGER_IFACE
1523
 
except AttributeError:
1524
 
    dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
1525
 
 
1526
 
 
1527
 
class DBusObjectWithObjectManager(DBusObjectWithAnnotations):
1528
 
    """A D-Bus object with an ObjectManager.
1529
 
 
1530
 
    Classes inheriting from this exposes the standard
1531
 
    GetManagedObjects call and the InterfacesAdded and
1532
 
    InterfacesRemoved signals on the standard
1533
 
    "org.freedesktop.DBus.ObjectManager" interface.
1534
 
 
1535
 
    Note: No signals are sent automatically; they must be sent
1536
 
    manually.
1537
 
    """
1538
 
    @dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
1539
 
                         out_signature="a{oa{sa{sv}}}")
1540
 
    def GetManagedObjects(self):
1541
 
        """This function must be overridden"""
1542
 
        raise NotImplementedError()
1543
 
 
1544
 
    @dbus.service.signal(dbus.OBJECT_MANAGER_IFACE,
1545
 
                         signature="oa{sa{sv}}")
1546
 
    def InterfacesAdded(self, object_path, interfaces_and_properties):
1547
 
        pass
1548
 
 
1549
 
    @dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature="oas")
1550
 
    def InterfacesRemoved(self, object_path, interfaces):
1551
 
        pass
1552
 
 
1553
 
    @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1554
 
                         out_signature="s",
1555
 
                         path_keyword='object_path',
1556
 
                         connection_keyword='connection')
1557
 
    def Introspect(self, object_path, connection):
1558
 
        """Overloading of standard D-Bus method.
1559
 
 
1560
 
        Override return argument name of GetManagedObjects to be
1561
 
        "objpath_interfaces_and_properties"
1562
 
        """
1563
 
        xmlstring = DBusObjectWithAnnotations.Introspect(self,
1564
 
                                                         object_path,
1565
 
                                                         connection)
1566
 
        try:
1567
 
            document = xml.dom.minidom.parseString(xmlstring)
1568
 
 
1569
 
            for if_tag in document.getElementsByTagName("interface"):
1570
 
                # Fix argument name for the GetManagedObjects method
1571
 
                if (if_tag.getAttribute("name")
1572
 
                    == dbus.OBJECT_MANAGER_IFACE):
1573
 
                    for cn in if_tag.getElementsByTagName("method"):
1574
 
                        if (cn.getAttribute("name")
1575
 
                            == "GetManagedObjects"):
1576
 
                            for arg in cn.getElementsByTagName("arg"):
1577
 
                                if (arg.getAttribute("direction")
1578
 
                                    == "out"):
1579
 
                                    arg.setAttribute(
1580
 
                                        "name",
1581
 
                                        "objpath_interfaces"
1582
 
                                        "_and_properties")
1583
 
            xmlstring = document.toxml("utf-8")
1584
 
            document.unlink()
1585
 
        except (AttributeError, xml.dom.DOMException,
1586
 
                xml.parsers.expat.ExpatError) as error:
1587
 
            logger.error("Failed to override Introspection method",
1588
 
                         exc_info=error)
1589
 
        return xmlstring
1590
 
 
1591
 
 
1592
1050
def datetime_to_dbus(dt, variant_level=0):
1593
1051
    """Convert a UTC datetime.datetime() to a D-Bus type."""
1594
1052
    if dt is None:
1595
 
        return dbus.String("", variant_level=variant_level)
 
1053
        return dbus.String("", variant_level = variant_level)
1596
1054
    return dbus.String(dt.isoformat(), variant_level=variant_level)
1597
1055
 
1598
1056
 
1601
1059
    dbus.service.Object, it will add alternate D-Bus attributes with
1602
1060
    interface names according to the "alt_interface_names" mapping.
1603
1061
    Usage:
1604
 
 
 
1062
    
1605
1063
    @alternate_dbus_interfaces({"org.example.Interface":
1606
1064
                                    "net.example.AlternateInterface"})
1607
1065
    class SampleDBusObject(dbus.service.Object):
1608
1066
        @dbus.service.method("org.example.Interface")
1609
1067
        def SampleDBusMethod():
1610
1068
            pass
1611
 
 
 
1069
    
1612
1070
    The above "SampleDBusMethod" on "SampleDBusObject" will be
1613
1071
    reachable via two interfaces: "org.example.Interface" and
1614
1072
    "net.example.AlternateInterface", the latter of which will have
1615
1073
    its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
1616
1074
    "true", unless "deprecate" is passed with a False value.
1617
 
 
 
1075
    
1618
1076
    This works for methods and signals, and also for D-Bus properties
1619
1077
    (from DBusObjectWithProperties) and interfaces (from the
1620
1078
    dbus_interface_annotations decorator).
1621
1079
    """
1622
 
 
 
1080
    
1623
1081
    def wrapper(cls):
1624
1082
        for orig_interface_name, alt_interface_name in (
1625
1083
                alt_interface_names.items()):
1642
1100
                if getattr(attribute, "_dbus_is_signal", False):
1643
1101
                    # Extract the original non-method undecorated
1644
1102
                    # function by black magic
1645
 
                    if sys.version_info.major == 2:
1646
 
                        nonmethod_func = (dict(
1647
 
                            zip(attribute.func_code.co_freevars,
1648
 
                                attribute.__closure__))
1649
 
                                          ["func"].cell_contents)
1650
 
                    else:
1651
 
                        nonmethod_func = (dict(
1652
 
                            zip(attribute.__code__.co_freevars,
1653
 
                                attribute.__closure__))
1654
 
                                          ["func"].cell_contents)
 
1103
                    nonmethod_func = (dict(
 
1104
                        zip(attribute.func_code.co_freevars,
 
1105
                            attribute.__closure__))
 
1106
                                      ["func"].cell_contents)
1655
1107
                    # Create a new, but exactly alike, function
1656
1108
                    # object, and decorate it to be a new D-Bus signal
1657
1109
                    # with the alternate D-Bus interface name
1658
 
                    new_function = copy_function(nonmethod_func)
1659
1110
                    new_function = (dbus.service.signal(
1660
 
                        alt_interface,
1661
 
                        attribute._dbus_signature)(new_function))
 
1111
                        alt_interface, attribute._dbus_signature)
 
1112
                                    (types.FunctionType(
 
1113
                                        nonmethod_func.func_code,
 
1114
                                        nonmethod_func.func_globals,
 
1115
                                        nonmethod_func.func_name,
 
1116
                                        nonmethod_func.func_defaults,
 
1117
                                        nonmethod_func.func_closure)))
1662
1118
                    # Copy annotations, if any
1663
1119
                    try:
1664
1120
                        new_function._dbus_annotations = dict(
1665
1121
                            attribute._dbus_annotations)
1666
1122
                    except AttributeError:
1667
1123
                        pass
1668
 
 
1669
1124
                    # Define a creator of a function to call both the
1670
1125
                    # original and alternate functions, so both the
1671
1126
                    # original and alternate signals gets sent when
1674
1129
                        """This function is a scope container to pass
1675
1130
                        func1 and func2 to the "call_both" function
1676
1131
                        outside of its arguments"""
1677
 
 
1678
 
                        @functools.wraps(func2)
 
1132
                        
1679
1133
                        def call_both(*args, **kwargs):
1680
1134
                            """This function will emit two D-Bus
1681
1135
                            signals by calling func1 and func2"""
1682
1136
                            func1(*args, **kwargs)
1683
1137
                            func2(*args, **kwargs)
1684
 
                        # Make wrapper function look like a D-Bus
1685
 
                        # signal
1686
 
                        for name, attr in inspect.getmembers(func2):
1687
 
                            if name.startswith("_dbus_"):
1688
 
                                setattr(call_both, name, attr)
1689
 
 
 
1138
                        
1690
1139
                        return call_both
1691
1140
                    # Create the "call_both" function and add it to
1692
1141
                    # the class
1702
1151
                            alt_interface,
1703
1152
                            attribute._dbus_in_signature,
1704
1153
                            attribute._dbus_out_signature)
1705
 
                        (copy_function(attribute)))
 
1154
                        (types.FunctionType(attribute.func_code,
 
1155
                                            attribute.func_globals,
 
1156
                                            attribute.func_name,
 
1157
                                            attribute.func_defaults,
 
1158
                                            attribute.func_closure)))
1706
1159
                    # Copy annotations, if any
1707
1160
                    try:
1708
1161
                        attr[attrname]._dbus_annotations = dict(
1720
1173
                        attribute._dbus_access,
1721
1174
                        attribute._dbus_get_args_options
1722
1175
                        ["byte_arrays"])
1723
 
                                      (copy_function(attribute)))
 
1176
                                      (types.FunctionType(
 
1177
                                          attribute.func_code,
 
1178
                                          attribute.func_globals,
 
1179
                                          attribute.func_name,
 
1180
                                          attribute.func_defaults,
 
1181
                                          attribute.func_closure)))
1724
1182
                    # Copy annotations, if any
1725
1183
                    try:
1726
1184
                        attr[attrname]._dbus_annotations = dict(
1735
1193
                    # to the class.
1736
1194
                    attr[attrname] = (
1737
1195
                        dbus_interface_annotations(alt_interface)
1738
 
                        (copy_function(attribute)))
 
1196
                        (types.FunctionType(attribute.func_code,
 
1197
                                            attribute.func_globals,
 
1198
                                            attribute.func_name,
 
1199
                                            attribute.func_defaults,
 
1200
                                            attribute.func_closure)))
1739
1201
            if deprecate:
1740
1202
                # Deprecate all alternate interfaces
1741
 
                iname = "_AlternateDBusNames_interface_annotation{}"
 
1203
                iname="_AlternateDBusNames_interface_annotation{}"
1742
1204
                for interface_name in interface_names:
1743
 
 
 
1205
                    
1744
1206
                    @dbus_interface_annotations(interface_name)
1745
1207
                    def func(self):
1746
 
                        return {"org.freedesktop.DBus.Deprecated":
1747
 
                                "true"}
 
1208
                        return { "org.freedesktop.DBus.Deprecated":
 
1209
                                 "true" }
1748
1210
                    # Find an unused name
1749
1211
                    for aname in (iname.format(i)
1750
1212
                                  for i in itertools.count()):
1754
1216
            if interface_names:
1755
1217
                # Replace the class with a new subclass of it with
1756
1218
                # methods, signals, etc. as created above.
1757
 
                if sys.version_info.major == 2:
1758
 
                    cls = type(b"{}Alternate".format(cls.__name__),
1759
 
                               (cls, ), attr)
1760
 
                else:
1761
 
                    cls = type("{}Alternate".format(cls.__name__),
1762
 
                               (cls, ), attr)
 
1219
                cls = type(b"{}Alternate".format(cls.__name__),
 
1220
                           (cls, ), attr)
1763
1221
        return cls
1764
 
 
 
1222
    
1765
1223
    return wrapper
1766
1224
 
1767
1225
 
1769
1227
                            "se.bsnet.fukt.Mandos"})
1770
1228
class ClientDBus(Client, DBusObjectWithProperties):
1771
1229
    """A Client class using D-Bus
1772
 
 
 
1230
    
1773
1231
    Attributes:
1774
1232
    dbus_object_path: dbus.ObjectPath
1775
1233
    bus: dbus.SystemBus()
1776
1234
    """
1777
 
 
 
1235
    
1778
1236
    runtime_expansions = (Client.runtime_expansions
1779
1237
                          + ("dbus_object_path", ))
1780
 
 
 
1238
    
1781
1239
    _interface = "se.recompile.Mandos.Client"
1782
 
 
 
1240
    
1783
1241
    # dbus.service.Object doesn't use super(), so we can't either.
1784
 
 
1785
 
    def __init__(self, bus=None, *args, **kwargs):
 
1242
    
 
1243
    def __init__(self, bus = None, *args, **kwargs):
1786
1244
        self.bus = bus
1787
1245
        Client.__init__(self, *args, **kwargs)
1788
1246
        # Only now, when this client is initialized, can it show up on
1794
1252
            "/clients/" + client_object_name)
1795
1253
        DBusObjectWithProperties.__init__(self, self.bus,
1796
1254
                                          self.dbus_object_path)
1797
 
 
 
1255
    
1798
1256
    def notifychangeproperty(transform_func, dbus_name,
1799
1257
                             type_func=lambda x: x,
1800
1258
                             variant_level=1,
1802
1260
                             _interface=_interface):
1803
1261
        """ Modify a variable so that it's a property which announces
1804
1262
        its changes to DBus.
1805
 
 
 
1263
        
1806
1264
        transform_fun: Function that takes a value and a variant_level
1807
1265
                       and transforms it to a D-Bus type.
1808
1266
        dbus_name: D-Bus name of the variable
1811
1269
        variant_level: D-Bus variant level.  Default: 1
1812
1270
        """
1813
1271
        attrname = "_{}".format(dbus_name)
1814
 
 
 
1272
        
1815
1273
        def setter(self, value):
1816
1274
            if hasattr(self, "dbus_object_path"):
1817
1275
                if (not hasattr(self, attrname) or
1824
1282
                    else:
1825
1283
                        dbus_value = transform_func(
1826
1284
                            type_func(value),
1827
 
                            variant_level=variant_level)
 
1285
                            variant_level = variant_level)
1828
1286
                        self.PropertyChanged(dbus.String(dbus_name),
1829
1287
                                             dbus_value)
1830
1288
                        self.PropertiesChanged(
1831
1289
                            _interface,
1832
 
                            dbus.Dictionary({dbus.String(dbus_name):
1833
 
                                             dbus_value}),
 
1290
                            dbus.Dictionary({ dbus.String(dbus_name):
 
1291
                                              dbus_value }),
1834
1292
                            dbus.Array())
1835
1293
            setattr(self, attrname, value)
1836
 
 
 
1294
        
1837
1295
        return property(lambda self: getattr(self, attrname), setter)
1838
 
 
 
1296
    
1839
1297
    expires = notifychangeproperty(datetime_to_dbus, "Expires")
1840
1298
    approvals_pending = notifychangeproperty(dbus.Boolean,
1841
1299
                                             "ApprovalPending",
1842
 
                                             type_func=bool)
 
1300
                                             type_func = bool)
1843
1301
    enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1844
1302
    last_enabled = notifychangeproperty(datetime_to_dbus,
1845
1303
                                        "LastEnabled")
1846
1304
    checker = notifychangeproperty(
1847
1305
        dbus.Boolean, "CheckerRunning",
1848
 
        type_func=lambda checker: checker is not None)
 
1306
        type_func = lambda checker: checker is not None)
1849
1307
    last_checked_ok = notifychangeproperty(datetime_to_dbus,
1850
1308
                                           "LastCheckedOK")
1851
1309
    last_checker_status = notifychangeproperty(dbus.Int16,
1856
1314
                                               "ApprovedByDefault")
1857
1315
    approval_delay = notifychangeproperty(
1858
1316
        dbus.UInt64, "ApprovalDelay",
1859
 
        type_func=lambda td: td.total_seconds() * 1000)
 
1317
        type_func = lambda td: td.total_seconds() * 1000)
1860
1318
    approval_duration = notifychangeproperty(
1861
1319
        dbus.UInt64, "ApprovalDuration",
1862
 
        type_func=lambda td: td.total_seconds() * 1000)
 
1320
        type_func = lambda td: td.total_seconds() * 1000)
1863
1321
    host = notifychangeproperty(dbus.String, "Host")
1864
1322
    timeout = notifychangeproperty(
1865
1323
        dbus.UInt64, "Timeout",
1866
 
        type_func=lambda td: td.total_seconds() * 1000)
 
1324
        type_func = lambda td: td.total_seconds() * 1000)
1867
1325
    extended_timeout = notifychangeproperty(
1868
1326
        dbus.UInt64, "ExtendedTimeout",
1869
 
        type_func=lambda td: td.total_seconds() * 1000)
 
1327
        type_func = lambda td: td.total_seconds() * 1000)
1870
1328
    interval = notifychangeproperty(
1871
1329
        dbus.UInt64, "Interval",
1872
 
        type_func=lambda td: td.total_seconds() * 1000)
 
1330
        type_func = lambda td: td.total_seconds() * 1000)
1873
1331
    checker_command = notifychangeproperty(dbus.String, "Checker")
1874
1332
    secret = notifychangeproperty(dbus.ByteArray, "Secret",
1875
1333
                                  invalidate_only=True)
1876
 
 
 
1334
    
1877
1335
    del notifychangeproperty
1878
 
 
 
1336
    
1879
1337
    def __del__(self, *args, **kwargs):
1880
1338
        try:
1881
1339
            self.remove_from_connection()
1884
1342
        if hasattr(DBusObjectWithProperties, "__del__"):
1885
1343
            DBusObjectWithProperties.__del__(self, *args, **kwargs)
1886
1344
        Client.__del__(self, *args, **kwargs)
1887
 
 
1888
 
    def checker_callback(self, source, condition,
1889
 
                         connection, command, *args, **kwargs):
1890
 
        ret = Client.checker_callback(self, source, condition,
1891
 
                                      connection, command, *args,
1892
 
                                      **kwargs)
1893
 
        exitstatus = self.last_checker_status
1894
 
        if exitstatus >= 0:
 
1345
    
 
1346
    def checker_callback(self, pid, condition, command,
 
1347
                         *args, **kwargs):
 
1348
        self.checker_callback_tag = None
 
1349
        self.checker = None
 
1350
        if os.WIFEXITED(condition):
 
1351
            exitstatus = os.WEXITSTATUS(condition)
1895
1352
            # Emit D-Bus signal
1896
1353
            self.CheckerCompleted(dbus.Int16(exitstatus),
1897
 
                                  # This is specific to GNU libC
1898
 
                                  dbus.Int64(exitstatus << 8),
 
1354
                                  dbus.Int64(condition),
1899
1355
                                  dbus.String(command))
1900
1356
        else:
1901
1357
            # Emit D-Bus signal
1902
1358
            self.CheckerCompleted(dbus.Int16(-1),
1903
 
                                  dbus.Int64(
1904
 
                                      # This is specific to GNU libC
1905
 
                                      (exitstatus << 8)
1906
 
                                      | self.last_checker_signal),
 
1359
                                  dbus.Int64(condition),
1907
1360
                                  dbus.String(command))
1908
 
        return ret
1909
 
 
 
1361
        
 
1362
        return Client.checker_callback(self, pid, condition, command,
 
1363
                                       *args, **kwargs)
 
1364
    
1910
1365
    def start_checker(self, *args, **kwargs):
1911
1366
        old_checker_pid = getattr(self.checker, "pid", None)
1912
1367
        r = Client.start_checker(self, *args, **kwargs)
1916
1371
            # Emit D-Bus signal
1917
1372
            self.CheckerStarted(self.current_checker_command)
1918
1373
        return r
1919
 
 
 
1374
    
1920
1375
    def _reset_approved(self):
1921
1376
        self.approved = None
1922
1377
        return False
1923
 
 
 
1378
    
1924
1379
    def approve(self, value=True):
1925
1380
        self.approved = value
1926
 
        GLib.timeout_add(int(self.approval_duration.total_seconds()
1927
 
                             * 1000), self._reset_approved)
 
1381
        gobject.timeout_add(int(self.approval_duration.total_seconds()
 
1382
                                * 1000), self._reset_approved)
1928
1383
        self.send_changedstate()
1929
 
 
1930
 
    #  D-Bus methods, signals & properties
1931
 
 
1932
 
    #  Interfaces
1933
 
 
1934
 
    #  Signals
1935
 
 
 
1384
    
 
1385
    ## D-Bus methods, signals & properties
 
1386
    
 
1387
    ## Interfaces
 
1388
    
 
1389
    ## Signals
 
1390
    
1936
1391
    # CheckerCompleted - signal
1937
1392
    @dbus.service.signal(_interface, signature="nxs")
1938
1393
    def CheckerCompleted(self, exitcode, waitstatus, command):
1939
1394
        "D-Bus signal"
1940
1395
        pass
1941
 
 
 
1396
    
1942
1397
    # CheckerStarted - signal
1943
1398
    @dbus.service.signal(_interface, signature="s")
1944
1399
    def CheckerStarted(self, command):
1945
1400
        "D-Bus signal"
1946
1401
        pass
1947
 
 
 
1402
    
1948
1403
    # PropertyChanged - signal
1949
1404
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1950
1405
    @dbus.service.signal(_interface, signature="sv")
1951
1406
    def PropertyChanged(self, property, value):
1952
1407
        "D-Bus signal"
1953
1408
        pass
1954
 
 
 
1409
    
1955
1410
    # GotSecret - signal
1956
1411
    @dbus.service.signal(_interface)
1957
1412
    def GotSecret(self):
1960
1415
        server to mandos-client
1961
1416
        """
1962
1417
        pass
1963
 
 
 
1418
    
1964
1419
    # Rejected - signal
1965
1420
    @dbus.service.signal(_interface, signature="s")
1966
1421
    def Rejected(self, reason):
1967
1422
        "D-Bus signal"
1968
1423
        pass
1969
 
 
 
1424
    
1970
1425
    # NeedApproval - signal
1971
1426
    @dbus.service.signal(_interface, signature="tb")
1972
1427
    def NeedApproval(self, timeout, default):
1973
1428
        "D-Bus signal"
1974
1429
        return self.need_approval()
1975
 
 
1976
 
    #  Methods
1977
 
 
 
1430
    
 
1431
    ## Methods
 
1432
    
1978
1433
    # Approve - method
1979
1434
    @dbus.service.method(_interface, in_signature="b")
1980
1435
    def Approve(self, value):
1981
1436
        self.approve(value)
1982
 
 
 
1437
    
1983
1438
    # CheckedOK - method
1984
1439
    @dbus.service.method(_interface)
1985
1440
    def CheckedOK(self):
1986
1441
        self.checked_ok()
1987
 
 
 
1442
    
1988
1443
    # Enable - method
1989
 
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1990
1444
    @dbus.service.method(_interface)
1991
1445
    def Enable(self):
1992
1446
        "D-Bus method"
1993
1447
        self.enable()
1994
 
 
 
1448
    
1995
1449
    # StartChecker - method
1996
 
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1997
1450
    @dbus.service.method(_interface)
1998
1451
    def StartChecker(self):
1999
1452
        "D-Bus method"
2000
1453
        self.start_checker()
2001
 
 
 
1454
    
2002
1455
    # Disable - method
2003
 
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
2004
1456
    @dbus.service.method(_interface)
2005
1457
    def Disable(self):
2006
1458
        "D-Bus method"
2007
1459
        self.disable()
2008
 
 
 
1460
    
2009
1461
    # StopChecker - method
2010
 
    @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
2011
1462
    @dbus.service.method(_interface)
2012
1463
    def StopChecker(self):
2013
1464
        self.stop_checker()
2014
 
 
2015
 
    #  Properties
2016
 
 
 
1465
    
 
1466
    ## Properties
 
1467
    
2017
1468
    # ApprovalPending - property
2018
1469
    @dbus_service_property(_interface, signature="b", access="read")
2019
1470
    def ApprovalPending_dbus_property(self):
2020
1471
        return dbus.Boolean(bool(self.approvals_pending))
2021
 
 
 
1472
    
2022
1473
    # ApprovedByDefault - property
2023
1474
    @dbus_service_property(_interface,
2024
1475
                           signature="b",
2027
1478
        if value is None:       # get
2028
1479
            return dbus.Boolean(self.approved_by_default)
2029
1480
        self.approved_by_default = bool(value)
2030
 
 
 
1481
    
2031
1482
    # ApprovalDelay - property
2032
1483
    @dbus_service_property(_interface,
2033
1484
                           signature="t",
2037
1488
            return dbus.UInt64(self.approval_delay.total_seconds()
2038
1489
                               * 1000)
2039
1490
        self.approval_delay = datetime.timedelta(0, 0, 0, value)
2040
 
 
 
1491
    
2041
1492
    # ApprovalDuration - property
2042
1493
    @dbus_service_property(_interface,
2043
1494
                           signature="t",
2047
1498
            return dbus.UInt64(self.approval_duration.total_seconds()
2048
1499
                               * 1000)
2049
1500
        self.approval_duration = datetime.timedelta(0, 0, 0, value)
2050
 
 
 
1501
    
2051
1502
    # Name - property
2052
 
    @dbus_annotations(
2053
 
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2054
1503
    @dbus_service_property(_interface, signature="s", access="read")
2055
1504
    def Name_dbus_property(self):
2056
1505
        return dbus.String(self.name)
2057
 
 
2058
 
    # KeyID - property
2059
 
    @dbus_annotations(
2060
 
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2061
 
    @dbus_service_property(_interface, signature="s", access="read")
2062
 
    def KeyID_dbus_property(self):
2063
 
        return dbus.String(self.key_id)
2064
 
 
 
1506
    
2065
1507
    # Fingerprint - property
2066
 
    @dbus_annotations(
2067
 
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2068
1508
    @dbus_service_property(_interface, signature="s", access="read")
2069
1509
    def Fingerprint_dbus_property(self):
2070
1510
        return dbus.String(self.fingerprint)
2071
 
 
 
1511
    
2072
1512
    # Host - property
2073
1513
    @dbus_service_property(_interface,
2074
1514
                           signature="s",
2077
1517
        if value is None:       # get
2078
1518
            return dbus.String(self.host)
2079
1519
        self.host = str(value)
2080
 
 
 
1520
    
2081
1521
    # Created - property
2082
 
    @dbus_annotations(
2083
 
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2084
1522
    @dbus_service_property(_interface, signature="s", access="read")
2085
1523
    def Created_dbus_property(self):
2086
1524
        return datetime_to_dbus(self.created)
2087
 
 
 
1525
    
2088
1526
    # LastEnabled - property
2089
1527
    @dbus_service_property(_interface, signature="s", access="read")
2090
1528
    def LastEnabled_dbus_property(self):
2091
1529
        return datetime_to_dbus(self.last_enabled)
2092
 
 
 
1530
    
2093
1531
    # Enabled - property
2094
1532
    @dbus_service_property(_interface,
2095
1533
                           signature="b",
2101
1539
            self.enable()
2102
1540
        else:
2103
1541
            self.disable()
2104
 
 
 
1542
    
2105
1543
    # LastCheckedOK - property
2106
1544
    @dbus_service_property(_interface,
2107
1545
                           signature="s",
2111
1549
            self.checked_ok()
2112
1550
            return
2113
1551
        return datetime_to_dbus(self.last_checked_ok)
2114
 
 
 
1552
    
2115
1553
    # LastCheckerStatus - property
2116
1554
    @dbus_service_property(_interface, signature="n", access="read")
2117
1555
    def LastCheckerStatus_dbus_property(self):
2118
1556
        return dbus.Int16(self.last_checker_status)
2119
 
 
 
1557
    
2120
1558
    # Expires - property
2121
1559
    @dbus_service_property(_interface, signature="s", access="read")
2122
1560
    def Expires_dbus_property(self):
2123
1561
        return datetime_to_dbus(self.expires)
2124
 
 
 
1562
    
2125
1563
    # LastApprovalRequest - property
2126
1564
    @dbus_service_property(_interface, signature="s", access="read")
2127
1565
    def LastApprovalRequest_dbus_property(self):
2128
1566
        return datetime_to_dbus(self.last_approval_request)
2129
 
 
 
1567
    
2130
1568
    # Timeout - property
2131
1569
    @dbus_service_property(_interface,
2132
1570
                           signature="t",
2147
1585
                if (getattr(self, "disable_initiator_tag", None)
2148
1586
                    is None):
2149
1587
                    return
2150
 
                GLib.source_remove(self.disable_initiator_tag)
2151
 
                self.disable_initiator_tag = GLib.timeout_add(
 
1588
                gobject.source_remove(self.disable_initiator_tag)
 
1589
                self.disable_initiator_tag = gobject.timeout_add(
2152
1590
                    int((self.expires - now).total_seconds() * 1000),
2153
1591
                    self.disable)
2154
 
 
 
1592
    
2155
1593
    # ExtendedTimeout - property
2156
1594
    @dbus_service_property(_interface,
2157
1595
                           signature="t",
2161
1599
            return dbus.UInt64(self.extended_timeout.total_seconds()
2162
1600
                               * 1000)
2163
1601
        self.extended_timeout = datetime.timedelta(0, 0, 0, value)
2164
 
 
 
1602
    
2165
1603
    # Interval - property
2166
1604
    @dbus_service_property(_interface,
2167
1605
                           signature="t",
2174
1612
            return
2175
1613
        if self.enabled:
2176
1614
            # Reschedule checker run
2177
 
            GLib.source_remove(self.checker_initiator_tag)
2178
 
            self.checker_initiator_tag = GLib.timeout_add(
 
1615
            gobject.source_remove(self.checker_initiator_tag)
 
1616
            self.checker_initiator_tag = gobject.timeout_add(
2179
1617
                value, self.start_checker)
2180
 
            self.start_checker()  # Start one now, too
2181
 
 
 
1618
            self.start_checker() # Start one now, too
 
1619
    
2182
1620
    # Checker - property
2183
1621
    @dbus_service_property(_interface,
2184
1622
                           signature="s",
2187
1625
        if value is None:       # get
2188
1626
            return dbus.String(self.checker_command)
2189
1627
        self.checker_command = str(value)
2190
 
 
 
1628
    
2191
1629
    # CheckerRunning - property
2192
1630
    @dbus_service_property(_interface,
2193
1631
                           signature="b",
2199
1637
            self.start_checker()
2200
1638
        else:
2201
1639
            self.stop_checker()
2202
 
 
 
1640
    
2203
1641
    # ObjectPath - property
2204
 
    @dbus_annotations(
2205
 
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const",
2206
 
         "org.freedesktop.DBus.Deprecated": "true"})
2207
1642
    @dbus_service_property(_interface, signature="o", access="read")
2208
1643
    def ObjectPath_dbus_property(self):
2209
 
        return self.dbus_object_path  # is already a dbus.ObjectPath
2210
 
 
 
1644
        return self.dbus_object_path # is already a dbus.ObjectPath
 
1645
    
2211
1646
    # Secret = property
2212
 
    @dbus_annotations(
2213
 
        {"org.freedesktop.DBus.Property.EmitsChangedSignal":
2214
 
         "invalidates"})
2215
1647
    @dbus_service_property(_interface,
2216
1648
                           signature="ay",
2217
1649
                           access="write",
2218
1650
                           byte_arrays=True)
2219
1651
    def Secret_dbus_property(self, value):
2220
1652
        self.secret = bytes(value)
2221
 
 
 
1653
    
2222
1654
    del _interface
2223
1655
 
2224
1656
 
2225
 
class ProxyClient:
2226
 
    def __init__(self, child_pipe, key_id, fpr, address):
 
1657
class ProxyClient(object):
 
1658
    def __init__(self, child_pipe, fpr, address):
2227
1659
        self._pipe = child_pipe
2228
 
        self._pipe.send(('init', key_id, fpr, address))
 
1660
        self._pipe.send(('init', fpr, address))
2229
1661
        if not self._pipe.recv():
2230
 
            raise KeyError(key_id or fpr)
2231
 
 
 
1662
            raise KeyError()
 
1663
    
2232
1664
    def __getattribute__(self, name):
2233
1665
        if name == '_pipe':
2234
1666
            return super(ProxyClient, self).__getattribute__(name)
2237
1669
        if data[0] == 'data':
2238
1670
            return data[1]
2239
1671
        if data[0] == 'function':
2240
 
 
 
1672
            
2241
1673
            def func(*args, **kwargs):
2242
1674
                self._pipe.send(('funcall', name, args, kwargs))
2243
1675
                return self._pipe.recv()[1]
2244
 
 
 
1676
            
2245
1677
            return func
2246
 
 
 
1678
    
2247
1679
    def __setattr__(self, name, value):
2248
1680
        if name == '_pipe':
2249
1681
            return super(ProxyClient, self).__setattr__(name, value)
2252
1684
 
2253
1685
class ClientHandler(socketserver.BaseRequestHandler, object):
2254
1686
    """A class to handle client connections.
2255
 
 
 
1687
    
2256
1688
    Instantiated once for each connection to handle it.
2257
1689
    Note: This will run in its own forked process."""
2258
 
 
 
1690
    
2259
1691
    def handle(self):
2260
1692
        with contextlib.closing(self.server.child_pipe) as child_pipe:
2261
1693
            logger.info("TCP connection from: %s",
2262
1694
                        str(self.client_address))
2263
1695
            logger.debug("Pipe FD: %d",
2264
1696
                         self.server.child_pipe.fileno())
2265
 
 
2266
 
            session = gnutls.ClientSession(self.request)
2267
 
 
2268
 
            # priority = ':'.join(("NONE", "+VERS-TLS1.1",
2269
 
            #                       "+AES-256-CBC", "+SHA1",
2270
 
            #                       "+COMP-NULL", "+CTYPE-OPENPGP",
2271
 
            #                       "+DHE-DSS"))
 
1697
            
 
1698
            session = gnutls.connection.ClientSession(
 
1699
                self.request, gnutls.connection .X509Credentials())
 
1700
            
 
1701
            # Note: gnutls.connection.X509Credentials is really a
 
1702
            # generic GnuTLS certificate credentials object so long as
 
1703
            # no X.509 keys are added to it.  Therefore, we can use it
 
1704
            # here despite using OpenPGP certificates.
 
1705
            
 
1706
            #priority = ':'.join(("NONE", "+VERS-TLS1.1",
 
1707
            #                      "+AES-256-CBC", "+SHA1",
 
1708
            #                      "+COMP-NULL", "+CTYPE-OPENPGP",
 
1709
            #                      "+DHE-DSS"))
2272
1710
            # Use a fallback default, since this MUST be set.
2273
1711
            priority = self.server.gnutls_priority
2274
1712
            if priority is None:
2275
1713
                priority = "NORMAL"
2276
 
            gnutls.priority_set_direct(session._c_object,
2277
 
                                       priority.encode("utf-8"),
2278
 
                                       None)
2279
 
 
 
1714
            gnutls.library.functions.gnutls_priority_set_direct(
 
1715
                session._c_object, priority, None)
 
1716
            
2280
1717
            # Start communication using the Mandos protocol
2281
1718
            # Get protocol number
2282
1719
            line = self.request.makefile().readline()
2287
1724
            except (ValueError, IndexError, RuntimeError) as error:
2288
1725
                logger.error("Unknown protocol version: %s", error)
2289
1726
                return
2290
 
 
 
1727
            
2291
1728
            # Start GnuTLS connection
2292
1729
            try:
2293
1730
                session.handshake()
2294
 
            except gnutls.Error as error:
 
1731
            except gnutls.errors.GNUTLSError as error:
2295
1732
                logger.warning("Handshake failed: %s", error)
2296
1733
                # Do not run session.bye() here: the session is not
2297
1734
                # established.  Just abandon the request.
2298
1735
                return
2299
1736
            logger.debug("Handshake succeeded")
2300
 
 
 
1737
            
2301
1738
            approval_required = False
2302
1739
            try:
2303
 
                if gnutls.has_rawpk:
2304
 
                    fpr = b""
2305
 
                    try:
2306
 
                        key_id = self.key_id(
2307
 
                            self.peer_certificate(session))
2308
 
                    except (TypeError, gnutls.Error) as error:
2309
 
                        logger.warning("Bad certificate: %s", error)
2310
 
                        return
2311
 
                    logger.debug("Key ID: %s", key_id)
2312
 
 
2313
 
                else:
2314
 
                    key_id = b""
2315
 
                    try:
2316
 
                        fpr = self.fingerprint(
2317
 
                            self.peer_certificate(session))
2318
 
                    except (TypeError, gnutls.Error) as error:
2319
 
                        logger.warning("Bad certificate: %s", error)
2320
 
                        return
2321
 
                    logger.debug("Fingerprint: %s", fpr)
2322
 
 
2323
 
                try:
2324
 
                    client = ProxyClient(child_pipe, key_id, fpr,
 
1740
                try:
 
1741
                    fpr = self.fingerprint(
 
1742
                        self.peer_certificate(session))
 
1743
                except (TypeError,
 
1744
                        gnutls.errors.GNUTLSError) as error:
 
1745
                    logger.warning("Bad certificate: %s", error)
 
1746
                    return
 
1747
                logger.debug("Fingerprint: %s", fpr)
 
1748
                
 
1749
                try:
 
1750
                    client = ProxyClient(child_pipe, fpr,
2325
1751
                                         self.client_address)
2326
1752
                except KeyError:
2327
1753
                    return
2328
 
 
 
1754
                
2329
1755
                if client.approval_delay:
2330
1756
                    delay = client.approval_delay
2331
1757
                    client.approvals_pending += 1
2332
1758
                    approval_required = True
2333
 
 
 
1759
                
2334
1760
                while True:
2335
1761
                    if not client.enabled:
2336
1762
                        logger.info("Client %s is disabled",
2339
1765
                            # Emit D-Bus signal
2340
1766
                            client.Rejected("Disabled")
2341
1767
                        return
2342
 
 
 
1768
                    
2343
1769
                    if client.approved or not client.approval_delay:
2344
 
                        # We are approved or approval is disabled
 
1770
                        #We are approved or approval is disabled
2345
1771
                        break
2346
1772
                    elif client.approved is None:
2347
1773
                        logger.info("Client %s needs approval",
2358
1784
                            # Emit D-Bus signal
2359
1785
                            client.Rejected("Denied")
2360
1786
                        return
2361
 
 
2362
 
                    # wait until timeout or approved
 
1787
                    
 
1788
                    #wait until timeout or approved
2363
1789
                    time = datetime.datetime.now()
2364
1790
                    client.changedstate.acquire()
2365
1791
                    client.changedstate.wait(delay.total_seconds())
2378
1804
                            break
2379
1805
                    else:
2380
1806
                        delay -= time2 - time
2381
 
 
2382
 
                try:
2383
 
                    session.send(client.secret)
2384
 
                except gnutls.Error as error:
2385
 
                    logger.warning("gnutls send failed",
2386
 
                                   exc_info=error)
2387
 
                    return
2388
 
 
 
1807
                
 
1808
                sent_size = 0
 
1809
                while sent_size < len(client.secret):
 
1810
                    try:
 
1811
                        sent = session.send(client.secret[sent_size:])
 
1812
                    except gnutls.errors.GNUTLSError as error:
 
1813
                        logger.warning("gnutls send failed",
 
1814
                                       exc_info=error)
 
1815
                        return
 
1816
                    logger.debug("Sent: %d, remaining: %d", sent,
 
1817
                                 len(client.secret) - (sent_size
 
1818
                                                       + sent))
 
1819
                    sent_size += sent
 
1820
                
2389
1821
                logger.info("Sending secret to %s", client.name)
2390
1822
                # bump the timeout using extended_timeout
2391
1823
                client.bump_timeout(client.extended_timeout)
2392
1824
                if self.server.use_dbus:
2393
1825
                    # Emit D-Bus signal
2394
1826
                    client.GotSecret()
2395
 
 
 
1827
            
2396
1828
            finally:
2397
1829
                if approval_required:
2398
1830
                    client.approvals_pending -= 1
2399
1831
                try:
2400
1832
                    session.bye()
2401
 
                except gnutls.Error as error:
 
1833
                except gnutls.errors.GNUTLSError as error:
2402
1834
                    logger.warning("GnuTLS bye failed",
2403
1835
                                   exc_info=error)
2404
 
 
 
1836
    
2405
1837
    @staticmethod
2406
1838
    def peer_certificate(session):
2407
 
        "Return the peer's certificate as a bytestring"
2408
 
        try:
2409
 
            cert_type = gnutls.certificate_type_get2(session._c_object,
2410
 
                                                     gnutls.CTYPE_PEERS)
2411
 
        except AttributeError:
2412
 
            cert_type = gnutls.certificate_type_get(session._c_object)
2413
 
        if gnutls.has_rawpk:
2414
 
            valid_cert_types = frozenset((gnutls.CRT_RAWPK,))
2415
 
        else:
2416
 
            valid_cert_types = frozenset((gnutls.CRT_OPENPGP,))
2417
 
        # If not a valid certificate type...
2418
 
        if cert_type not in valid_cert_types:
2419
 
            logger.info("Cert type %r not in %r", cert_type,
2420
 
                        valid_cert_types)
2421
 
            # ...return invalid data
2422
 
            return b""
 
1839
        "Return the peer's OpenPGP certificate as a bytestring"
 
1840
        # If not an OpenPGP certificate...
 
1841
        if (gnutls.library.functions.gnutls_certificate_type_get(
 
1842
                session._c_object)
 
1843
            != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
 
1844
            # ...do the normal thing
 
1845
            return session.peer_certificate
2423
1846
        list_size = ctypes.c_uint(1)
2424
 
        cert_list = (gnutls.certificate_get_peers
 
1847
        cert_list = (gnutls.library.functions
 
1848
                     .gnutls_certificate_get_peers
2425
1849
                     (session._c_object, ctypes.byref(list_size)))
2426
1850
        if not bool(cert_list) and list_size.value != 0:
2427
 
            raise gnutls.Error("error getting peer certificate")
 
1851
            raise gnutls.errors.GNUTLSError("error getting peer"
 
1852
                                            " certificate")
2428
1853
        if list_size.value == 0:
2429
1854
            return None
2430
1855
        cert = cert_list[0]
2431
1856
        return ctypes.string_at(cert.data, cert.size)
2432
 
 
2433
 
    @staticmethod
2434
 
    def key_id(certificate):
2435
 
        "Convert a certificate bytestring to a hexdigit key ID"
2436
 
        # New GnuTLS "datum" with the public key
2437
 
        datum = gnutls.datum_t(
2438
 
            ctypes.cast(ctypes.c_char_p(certificate),
2439
 
                        ctypes.POINTER(ctypes.c_ubyte)),
2440
 
            ctypes.c_uint(len(certificate)))
2441
 
        # XXX all these need to be created in the gnutls "module"
2442
 
        # New empty GnuTLS certificate
2443
 
        pubkey = gnutls.pubkey_t()
2444
 
        gnutls.pubkey_init(ctypes.byref(pubkey))
2445
 
        # Import the raw public key into the certificate
2446
 
        gnutls.pubkey_import(pubkey,
2447
 
                             ctypes.byref(datum),
2448
 
                             gnutls.X509_FMT_DER)
2449
 
        # New buffer for the key ID
2450
 
        buf = ctypes.create_string_buffer(32)
2451
 
        buf_len = ctypes.c_size_t(len(buf))
2452
 
        # Get the key ID from the raw public key into the buffer
2453
 
        gnutls.pubkey_get_key_id(pubkey,
2454
 
                                 gnutls.KEYID_USE_SHA256,
2455
 
                                 ctypes.cast(ctypes.byref(buf),
2456
 
                                             ctypes.POINTER(ctypes.c_ubyte)),
2457
 
                                 ctypes.byref(buf_len))
2458
 
        # Deinit the certificate
2459
 
        gnutls.pubkey_deinit(pubkey)
2460
 
 
2461
 
        # Convert the buffer to a Python bytestring
2462
 
        key_id = ctypes.string_at(buf, buf_len.value)
2463
 
        # Convert the bytestring to hexadecimal notation
2464
 
        hex_key_id = binascii.hexlify(key_id).upper()
2465
 
        return hex_key_id
2466
 
 
 
1857
    
2467
1858
    @staticmethod
2468
1859
    def fingerprint(openpgp):
2469
1860
        "Convert an OpenPGP bytestring to a hexdigit fingerprint"
2470
1861
        # New GnuTLS "datum" with the OpenPGP public key
2471
 
        datum = gnutls.datum_t(
 
1862
        datum = gnutls.library.types.gnutls_datum_t(
2472
1863
            ctypes.cast(ctypes.c_char_p(openpgp),
2473
1864
                        ctypes.POINTER(ctypes.c_ubyte)),
2474
1865
            ctypes.c_uint(len(openpgp)))
2475
1866
        # New empty GnuTLS certificate
2476
 
        crt = gnutls.openpgp_crt_t()
2477
 
        gnutls.openpgp_crt_init(ctypes.byref(crt))
 
1867
        crt = gnutls.library.types.gnutls_openpgp_crt_t()
 
1868
        gnutls.library.functions.gnutls_openpgp_crt_init(
 
1869
            ctypes.byref(crt))
2478
1870
        # Import the OpenPGP public key into the certificate
2479
 
        gnutls.openpgp_crt_import(crt, ctypes.byref(datum),
2480
 
                                  gnutls.OPENPGP_FMT_RAW)
 
1871
        gnutls.library.functions.gnutls_openpgp_crt_import(
 
1872
            crt, ctypes.byref(datum),
 
1873
            gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
2481
1874
        # Verify the self signature in the key
2482
1875
        crtverify = ctypes.c_uint()
2483
 
        gnutls.openpgp_crt_verify_self(crt, 0,
2484
 
                                       ctypes.byref(crtverify))
 
1876
        gnutls.library.functions.gnutls_openpgp_crt_verify_self(
 
1877
            crt, 0, ctypes.byref(crtverify))
2485
1878
        if crtverify.value != 0:
2486
 
            gnutls.openpgp_crt_deinit(crt)
2487
 
            raise gnutls.CertificateSecurityError(code
2488
 
                                                  =crtverify.value)
 
1879
            gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
1880
            raise gnutls.errors.CertificateSecurityError(
 
1881
                "Verify failed")
2489
1882
        # New buffer for the fingerprint
2490
1883
        buf = ctypes.create_string_buffer(20)
2491
1884
        buf_len = ctypes.c_size_t()
2492
1885
        # Get the fingerprint from the certificate into the buffer
2493
 
        gnutls.openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
2494
 
                                           ctypes.byref(buf_len))
 
1886
        gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint(
 
1887
            crt, ctypes.byref(buf), ctypes.byref(buf_len))
2495
1888
        # Deinit the certificate
2496
 
        gnutls.openpgp_crt_deinit(crt)
 
1889
        gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
2497
1890
        # Convert the buffer to a Python bytestring
2498
1891
        fpr = ctypes.string_at(buf, buf_len.value)
2499
1892
        # Convert the bytestring to hexadecimal notation
2501
1894
        return hex_fpr
2502
1895
 
2503
1896
 
2504
 
class MultiprocessingMixIn:
 
1897
class MultiprocessingMixIn(object):
2505
1898
    """Like socketserver.ThreadingMixIn, but with multiprocessing"""
2506
 
 
 
1899
    
2507
1900
    def sub_process_main(self, request, address):
2508
1901
        try:
2509
1902
            self.finish_request(request, address)
2510
1903
        except Exception:
2511
1904
            self.handle_error(request, address)
2512
1905
        self.close_request(request)
2513
 
 
 
1906
    
2514
1907
    def process_request(self, request, address):
2515
1908
        """Start a new process to process the request."""
2516
 
        proc = multiprocessing.Process(target=self.sub_process_main,
2517
 
                                       args=(request, address))
 
1909
        proc = multiprocessing.Process(target = self.sub_process_main,
 
1910
                                       args = (request, address))
2518
1911
        proc.start()
2519
1912
        return proc
2520
1913
 
2521
1914
 
2522
 
class MultiprocessingMixInWithPipe(MultiprocessingMixIn):
 
1915
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
2523
1916
    """ adds a pipe to the MixIn """
2524
 
 
 
1917
    
2525
1918
    def process_request(self, request, client_address):
2526
1919
        """Overrides and wraps the original process_request().
2527
 
 
 
1920
        
2528
1921
        This function creates a new pipe in self.pipe
2529
1922
        """
2530
1923
        parent_pipe, self.child_pipe = multiprocessing.Pipe()
2531
 
 
 
1924
        
2532
1925
        proc = MultiprocessingMixIn.process_request(self, request,
2533
1926
                                                    client_address)
2534
1927
        self.child_pipe.close()
2535
1928
        self.add_pipe(parent_pipe, proc)
2536
 
 
 
1929
    
2537
1930
    def add_pipe(self, parent_pipe, proc):
2538
1931
        """Dummy function; override as necessary"""
2539
1932
        raise NotImplementedError()
2540
1933
 
2541
1934
 
2542
1935
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
2543
 
                     socketserver.TCPServer):
 
1936
                     socketserver.TCPServer, object):
2544
1937
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
2545
 
 
 
1938
    
2546
1939
    Attributes:
2547
1940
        enabled:        Boolean; whether this server is activated yet
2548
1941
        interface:      None or a network interface name (string)
2549
1942
        use_ipv6:       Boolean; to use IPv6 or not
2550
1943
    """
2551
 
 
 
1944
    
2552
1945
    def __init__(self, server_address, RequestHandlerClass,
2553
1946
                 interface=None,
2554
1947
                 use_ipv6=True,
2564
1957
            self.socketfd = socketfd
2565
1958
            # Save the original socket.socket() function
2566
1959
            self.socket_socket = socket.socket
2567
 
 
2568
1960
            # To implement --socket, we monkey patch socket.socket.
2569
 
            #
 
1961
            # 
2570
1962
            # (When socketserver.TCPServer is a new-style class, we
2571
1963
            # could make self.socket into a property instead of monkey
2572
1964
            # patching socket.socket.)
2573
 
            #
 
1965
            # 
2574
1966
            # Create a one-time-only replacement for socket.socket()
2575
1967
            @functools.wraps(socket.socket)
2576
1968
            def socket_wrapper(*args, **kwargs):
2588
1980
        # socket_wrapper(), if socketfd was set.
2589
1981
        socketserver.TCPServer.__init__(self, server_address,
2590
1982
                                        RequestHandlerClass)
2591
 
 
 
1983
    
2592
1984
    def server_bind(self):
2593
1985
        """This overrides the normal server_bind() function
2594
1986
        to bind to an interface if one was specified, and also NOT to
2595
1987
        bind to an address or port if they were not specified."""
2596
 
        global SO_BINDTODEVICE
2597
1988
        if self.interface is not None:
2598
1989
            if SO_BINDTODEVICE is None:
2599
 
                # Fall back to a hard-coded value which seems to be
2600
 
                # common enough.
2601
 
                logger.warning("SO_BINDTODEVICE not found, trying 25")
2602
 
                SO_BINDTODEVICE = 25
2603
 
            try:
2604
 
                self.socket.setsockopt(
2605
 
                    socket.SOL_SOCKET, SO_BINDTODEVICE,
2606
 
                    (self.interface + "\0").encode("utf-8"))
2607
 
            except socket.error as error:
2608
 
                if error.errno == errno.EPERM:
2609
 
                    logger.error("No permission to bind to"
2610
 
                                 " interface %s", self.interface)
2611
 
                elif error.errno == errno.ENOPROTOOPT:
2612
 
                    logger.error("SO_BINDTODEVICE not available;"
2613
 
                                 " cannot bind to interface %s",
2614
 
                                 self.interface)
2615
 
                elif error.errno == errno.ENODEV:
2616
 
                    logger.error("Interface %s does not exist,"
2617
 
                                 " cannot bind", self.interface)
2618
 
                else:
2619
 
                    raise
 
1990
                logger.error("SO_BINDTODEVICE does not exist;"
 
1991
                             " cannot bind to interface %s",
 
1992
                             self.interface)
 
1993
            else:
 
1994
                try:
 
1995
                    self.socket.setsockopt(
 
1996
                        socket.SOL_SOCKET, SO_BINDTODEVICE,
 
1997
                        (self.interface + "\0").encode("utf-8"))
 
1998
                except socket.error as error:
 
1999
                    if error.errno == errno.EPERM:
 
2000
                        logger.error("No permission to bind to"
 
2001
                                     " interface %s", self.interface)
 
2002
                    elif error.errno == errno.ENOPROTOOPT:
 
2003
                        logger.error("SO_BINDTODEVICE not available;"
 
2004
                                     " cannot bind to interface %s",
 
2005
                                     self.interface)
 
2006
                    elif error.errno == errno.ENODEV:
 
2007
                        logger.error("Interface %s does not exist,"
 
2008
                                     " cannot bind", self.interface)
 
2009
                    else:
 
2010
                        raise
2620
2011
        # Only bind(2) the socket if we really need to.
2621
2012
        if self.server_address[0] or self.server_address[1]:
2622
 
            if self.server_address[1]:
2623
 
                self.allow_reuse_address = True
2624
2013
            if not self.server_address[0]:
2625
2014
                if self.address_family == socket.AF_INET6:
2626
 
                    any_address = "::"  # in6addr_any
 
2015
                    any_address = "::" # in6addr_any
2627
2016
                else:
2628
 
                    any_address = "0.0.0.0"  # INADDR_ANY
 
2017
                    any_address = "0.0.0.0" # INADDR_ANY
2629
2018
                self.server_address = (any_address,
2630
2019
                                       self.server_address[1])
2631
2020
            elif not self.server_address[1]:
2641
2030
 
2642
2031
class MandosServer(IPv6_TCPServer):
2643
2032
    """Mandos server.
2644
 
 
 
2033
    
2645
2034
    Attributes:
2646
2035
        clients:        set of Client objects
2647
2036
        gnutls_priority GnuTLS priority string
2648
2037
        use_dbus:       Boolean; to emit D-Bus signals or not
2649
 
 
2650
 
    Assumes a GLib.MainLoop event loop.
 
2038
    
 
2039
    Assumes a gobject.MainLoop event loop.
2651
2040
    """
2652
 
 
 
2041
    
2653
2042
    def __init__(self, server_address, RequestHandlerClass,
2654
2043
                 interface=None,
2655
2044
                 use_ipv6=True,
2665
2054
        self.gnutls_priority = gnutls_priority
2666
2055
        IPv6_TCPServer.__init__(self, server_address,
2667
2056
                                RequestHandlerClass,
2668
 
                                interface=interface,
2669
 
                                use_ipv6=use_ipv6,
2670
 
                                socketfd=socketfd)
2671
 
 
 
2057
                                interface = interface,
 
2058
                                use_ipv6 = use_ipv6,
 
2059
                                socketfd = socketfd)
 
2060
    
2672
2061
    def server_activate(self):
2673
2062
        if self.enabled:
2674
2063
            return socketserver.TCPServer.server_activate(self)
2675
 
 
 
2064
    
2676
2065
    def enable(self):
2677
2066
        self.enabled = True
2678
 
 
 
2067
    
2679
2068
    def add_pipe(self, parent_pipe, proc):
2680
2069
        # Call "handle_ipc" for both data and EOF events
2681
 
        GLib.io_add_watch(
2682
 
            GLib.IOChannel.unix_new(parent_pipe.fileno()),
2683
 
            GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
 
2070
        gobject.io_add_watch(
 
2071
            parent_pipe.fileno(),
 
2072
            gobject.IO_IN | gobject.IO_HUP,
2684
2073
            functools.partial(self.handle_ipc,
2685
 
                              parent_pipe=parent_pipe,
2686
 
                              proc=proc))
2687
 
 
 
2074
                              parent_pipe = parent_pipe,
 
2075
                              proc = proc))
 
2076
    
2688
2077
    def handle_ipc(self, source, condition,
2689
2078
                   parent_pipe=None,
2690
 
                   proc=None,
 
2079
                   proc = None,
2691
2080
                   client_object=None):
2692
2081
        # error, or the other end of multiprocessing.Pipe has closed
2693
 
        if condition & (GLib.IO_ERR | GLib.IO_HUP):
 
2082
        if condition & (gobject.IO_ERR | gobject.IO_HUP):
2694
2083
            # Wait for other process to exit
2695
2084
            proc.join()
2696
2085
            return False
2697
 
 
 
2086
        
2698
2087
        # Read a request from the child
2699
2088
        request = parent_pipe.recv()
2700
2089
        command = request[0]
2701
 
 
 
2090
        
2702
2091
        if command == 'init':
2703
 
            key_id = request[1].decode("ascii")
2704
 
            fpr = request[2].decode("ascii")
2705
 
            address = request[3]
2706
 
 
2707
 
            for c in self.clients.values():
2708
 
                if key_id == "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855":
2709
 
                    continue
2710
 
                if key_id and c.key_id == key_id:
2711
 
                    client = c
2712
 
                    break
2713
 
                if fpr and c.fingerprint == fpr:
 
2092
            fpr = request[1]
 
2093
            address = request[2]
 
2094
            
 
2095
            for c in self.clients.itervalues():
 
2096
                if c.fingerprint == fpr:
2714
2097
                    client = c
2715
2098
                    break
2716
2099
            else:
2717
 
                logger.info("Client not found for key ID: %s, address"
2718
 
                            ": %s", key_id or fpr, address)
 
2100
                logger.info("Client not found for fingerprint: %s, ad"
 
2101
                            "dress: %s", fpr, address)
2719
2102
                if self.use_dbus:
2720
2103
                    # Emit D-Bus signal
2721
 
                    mandos_dbus_service.ClientNotFound(key_id or fpr,
 
2104
                    mandos_dbus_service.ClientNotFound(fpr,
2722
2105
                                                       address[0])
2723
2106
                parent_pipe.send(False)
2724
2107
                return False
2725
 
 
2726
 
            GLib.io_add_watch(
2727
 
                GLib.IOChannel.unix_new(parent_pipe.fileno()),
2728
 
                GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
 
2108
            
 
2109
            gobject.io_add_watch(
 
2110
                parent_pipe.fileno(),
 
2111
                gobject.IO_IN | gobject.IO_HUP,
2729
2112
                functools.partial(self.handle_ipc,
2730
 
                                  parent_pipe=parent_pipe,
2731
 
                                  proc=proc,
2732
 
                                  client_object=client))
 
2113
                                  parent_pipe = parent_pipe,
 
2114
                                  proc = proc,
 
2115
                                  client_object = client))
2733
2116
            parent_pipe.send(True)
2734
2117
            # remove the old hook in favor of the new above hook on
2735
2118
            # same fileno
2738
2121
            funcname = request[1]
2739
2122
            args = request[2]
2740
2123
            kwargs = request[3]
2741
 
 
 
2124
            
2742
2125
            parent_pipe.send(('data', getattr(client_object,
2743
2126
                                              funcname)(*args,
2744
2127
                                                        **kwargs)))
2745
 
 
 
2128
        
2746
2129
        if command == 'getattr':
2747
2130
            attrname = request[1]
2748
 
            if isinstance(client_object.__getattribute__(attrname),
2749
 
                          collections.Callable):
 
2131
            if callable(client_object.__getattribute__(attrname)):
2750
2132
                parent_pipe.send(('function', ))
2751
2133
            else:
2752
2134
                parent_pipe.send((
2753
2135
                    'data', client_object.__getattribute__(attrname)))
2754
 
 
 
2136
        
2755
2137
        if command == 'setattr':
2756
2138
            attrname = request[1]
2757
2139
            value = request[2]
2758
2140
            setattr(client_object, attrname, value)
2759
 
 
 
2141
        
2760
2142
        return True
2761
2143
 
2762
2144
 
2763
2145
def rfc3339_duration_to_delta(duration):
2764
2146
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
2765
 
 
2766
 
    >>> rfc3339_duration_to_delta("P7D") == datetime.timedelta(7)
2767
 
    True
2768
 
    >>> rfc3339_duration_to_delta("PT60S") == datetime.timedelta(0, 60)
2769
 
    True
2770
 
    >>> rfc3339_duration_to_delta("PT60M") == datetime.timedelta(0, 3600)
2771
 
    True
2772
 
    >>> rfc3339_duration_to_delta("PT24H") == datetime.timedelta(1)
2773
 
    True
2774
 
    >>> rfc3339_duration_to_delta("P1W") == datetime.timedelta(7)
2775
 
    True
2776
 
    >>> rfc3339_duration_to_delta("PT5M30S") == datetime.timedelta(0, 330)
2777
 
    True
2778
 
    >>> rfc3339_duration_to_delta("P1DT3M20S") == datetime.timedelta(1, 200)
2779
 
    True
 
2147
    
 
2148
    >>> rfc3339_duration_to_delta("P7D")
 
2149
    datetime.timedelta(7)
 
2150
    >>> rfc3339_duration_to_delta("PT60S")
 
2151
    datetime.timedelta(0, 60)
 
2152
    >>> rfc3339_duration_to_delta("PT60M")
 
2153
    datetime.timedelta(0, 3600)
 
2154
    >>> rfc3339_duration_to_delta("PT24H")
 
2155
    datetime.timedelta(1)
 
2156
    >>> rfc3339_duration_to_delta("P1W")
 
2157
    datetime.timedelta(7)
 
2158
    >>> rfc3339_duration_to_delta("PT5M30S")
 
2159
    datetime.timedelta(0, 330)
 
2160
    >>> rfc3339_duration_to_delta("P1DT3M20S")
 
2161
    datetime.timedelta(1, 200)
2780
2162
    """
2781
 
 
 
2163
    
2782
2164
    # Parsing an RFC 3339 duration with regular expressions is not
2783
2165
    # possible - there would have to be multiple places for the same
2784
2166
    # values, like seconds.  The current code, while more esoteric, is
2785
2167
    # cleaner without depending on a parsing library.  If Python had a
2786
2168
    # built-in library for parsing we would use it, but we'd like to
2787
2169
    # avoid excessive use of external libraries.
2788
 
 
 
2170
    
2789
2171
    # New type for defining tokens, syntax, and semantics all-in-one
 
2172
    Token = collections.namedtuple("Token",
 
2173
                                   ("regexp", # To match token; if
 
2174
                                              # "value" is not None,
 
2175
                                              # must have a "group"
 
2176
                                              # containing digits
 
2177
                                    "value",  # datetime.timedelta or
 
2178
                                              # None
 
2179
                                    "followers")) # Tokens valid after
 
2180
                                                  # this token
2790
2181
    Token = collections.namedtuple("Token", (
2791
2182
        "regexp",  # To match token; if "value" is not None, must have
2792
2183
                   # a "group" containing digits
2824
2215
                           frozenset((token_year, token_month,
2825
2216
                                      token_day, token_time,
2826
2217
                                      token_week)))
2827
 
    # Define starting values:
2828
 
    # Value so far
2829
 
    value = datetime.timedelta()
 
2218
    # Define starting values
 
2219
    value = datetime.timedelta() # Value so far
2830
2220
    found_token = None
2831
 
    # Following valid tokens
2832
 
    followers = frozenset((token_duration, ))
2833
 
    # String left to parse
2834
 
    s = duration
 
2221
    followers = frozenset((token_duration,)) # Following valid tokens
 
2222
    s = duration                # String left to parse
2835
2223
    # Loop until end token is found
2836
2224
    while found_token is not token_end:
2837
2225
        # Search for any currently valid tokens
2853
2241
                break
2854
2242
        else:
2855
2243
            # No currently valid tokens were found
2856
 
            raise ValueError("Invalid RFC 3339 duration: {!r}"
2857
 
                             .format(duration))
 
2244
            raise ValueError("Invalid RFC 3339 duration")
2858
2245
    # End token found
2859
2246
    return value
2860
2247
 
2861
2248
 
2862
2249
def string_to_delta(interval):
2863
2250
    """Parse a string and return a datetime.timedelta
2864
 
 
2865
 
    >>> string_to_delta('7d') == datetime.timedelta(7)
2866
 
    True
2867
 
    >>> string_to_delta('60s') == datetime.timedelta(0, 60)
2868
 
    True
2869
 
    >>> string_to_delta('60m') == datetime.timedelta(0, 3600)
2870
 
    True
2871
 
    >>> string_to_delta('24h') == datetime.timedelta(1)
2872
 
    True
2873
 
    >>> string_to_delta('1w') == datetime.timedelta(7)
2874
 
    True
2875
 
    >>> string_to_delta('5m 30s') == datetime.timedelta(0, 330)
2876
 
    True
 
2251
    
 
2252
    >>> string_to_delta('7d')
 
2253
    datetime.timedelta(7)
 
2254
    >>> string_to_delta('60s')
 
2255
    datetime.timedelta(0, 60)
 
2256
    >>> string_to_delta('60m')
 
2257
    datetime.timedelta(0, 3600)
 
2258
    >>> string_to_delta('24h')
 
2259
    datetime.timedelta(1)
 
2260
    >>> string_to_delta('1w')
 
2261
    datetime.timedelta(7)
 
2262
    >>> string_to_delta('5m 30s')
 
2263
    datetime.timedelta(0, 330)
2877
2264
    """
2878
 
 
 
2265
    
2879
2266
    try:
2880
2267
        return rfc3339_duration_to_delta(interval)
2881
2268
    except ValueError:
2882
2269
        pass
2883
 
 
 
2270
    
2884
2271
    timevalue = datetime.timedelta(0)
2885
2272
    for s in interval.split():
2886
2273
        try:
2904
2291
    return timevalue
2905
2292
 
2906
2293
 
2907
 
def daemon(nochdir=False, noclose=False):
 
2294
def daemon(nochdir = False, noclose = False):
2908
2295
    """See daemon(3).  Standard BSD Unix function.
2909
 
 
 
2296
    
2910
2297
    This should really exist as os.daemon, but it doesn't (yet)."""
2911
2298
    if os.fork():
2912
2299
        sys.exit()
2930
2317
 
2931
2318
 
2932
2319
def main():
2933
 
 
 
2320
    
2934
2321
    ##################################################################
2935
2322
    # Parsing of options, both command line and config file
2936
 
 
 
2323
    
2937
2324
    parser = argparse.ArgumentParser()
2938
2325
    parser.add_argument("-v", "--version", action="version",
2939
 
                        version="%(prog)s {}".format(version),
 
2326
                        version = "%(prog)s {}".format(version),
2940
2327
                        help="show version number and exit")
2941
2328
    parser.add_argument("-i", "--interface", metavar="IF",
2942
2329
                        help="Bind to interface IF")
2978
2365
    parser.add_argument("--no-zeroconf", action="store_false",
2979
2366
                        dest="zeroconf", help="Do not use Zeroconf",
2980
2367
                        default=None)
2981
 
 
 
2368
    
2982
2369
    options = parser.parse_args()
2983
 
 
 
2370
    
 
2371
    if options.check:
 
2372
        import doctest
 
2373
        fail_count, test_count = doctest.testmod()
 
2374
        sys.exit(os.EX_OK if fail_count == 0 else 1)
 
2375
    
2984
2376
    # Default values for config file for server-global settings
2985
 
    if gnutls.has_rawpk:
2986
 
        priority = ("SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA"
2987
 
                    ":!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA")
2988
 
    else:
2989
 
        priority = ("SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2990
 
                    ":+SIGN-DSA-SHA256")
2991
 
    server_defaults = {"interface": "",
2992
 
                       "address": "",
2993
 
                       "port": "",
2994
 
                       "debug": "False",
2995
 
                       "priority": priority,
2996
 
                       "servicename": "Mandos",
2997
 
                       "use_dbus": "True",
2998
 
                       "use_ipv6": "True",
2999
 
                       "debuglevel": "",
3000
 
                       "restore": "True",
3001
 
                       "socket": "",
3002
 
                       "statedir": "/var/lib/mandos",
3003
 
                       "foreground": "False",
3004
 
                       "zeroconf": "True",
3005
 
                       }
3006
 
    del priority
3007
 
 
 
2377
    server_defaults = { "interface": "",
 
2378
                        "address": "",
 
2379
                        "port": "",
 
2380
                        "debug": "False",
 
2381
                        "priority":
 
2382
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
 
2383
                        ":+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
 
2384
                        "servicename": "Mandos",
 
2385
                        "use_dbus": "True",
 
2386
                        "use_ipv6": "True",
 
2387
                        "debuglevel": "",
 
2388
                        "restore": "True",
 
2389
                        "socket": "",
 
2390
                        "statedir": "/var/lib/mandos",
 
2391
                        "foreground": "False",
 
2392
                        "zeroconf": "True",
 
2393
                    }
 
2394
    
3008
2395
    # Parse config file for server-global settings
3009
 
    server_config = configparser.ConfigParser(server_defaults)
 
2396
    server_config = configparser.SafeConfigParser(server_defaults)
3010
2397
    del server_defaults
3011
2398
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
3012
 
    # Convert the ConfigParser object to a dict
 
2399
    # Convert the SafeConfigParser object to a dict
3013
2400
    server_settings = server_config.defaults()
3014
2401
    # Use the appropriate methods on the non-string config options
3015
 
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
3016
 
                   "foreground", "zeroconf"):
 
2402
    for option in ("debug", "use_dbus", "use_ipv6", "foreground"):
3017
2403
        server_settings[option] = server_config.getboolean("DEFAULT",
3018
2404
                                                           option)
3019
2405
    if server_settings["port"]:
3029
2415
            server_settings["socket"] = os.dup(server_settings
3030
2416
                                               ["socket"])
3031
2417
    del server_config
3032
 
 
 
2418
    
3033
2419
    # Override the settings from the config file with command line
3034
2420
    # options, if set.
3035
2421
    for option in ("interface", "address", "port", "debug",
3053
2439
    if server_settings["debug"]:
3054
2440
        server_settings["foreground"] = True
3055
2441
    # Now we have our good server settings in "server_settings"
3056
 
 
 
2442
    
3057
2443
    ##################################################################
3058
 
 
 
2444
    
3059
2445
    if (not server_settings["zeroconf"]
3060
2446
        and not (server_settings["port"]
3061
2447
                 or server_settings["socket"] != "")):
3062
2448
        parser.error("Needs port or socket to work without Zeroconf")
3063
 
 
 
2449
    
3064
2450
    # For convenience
3065
2451
    debug = server_settings["debug"]
3066
2452
    debuglevel = server_settings["debuglevel"]
3070
2456
                                     stored_state_file)
3071
2457
    foreground = server_settings["foreground"]
3072
2458
    zeroconf = server_settings["zeroconf"]
3073
 
 
 
2459
    
3074
2460
    if debug:
3075
2461
        initlogger(debug, logging.DEBUG)
3076
2462
    else:
3079
2465
        else:
3080
2466
            level = getattr(logging, debuglevel.upper())
3081
2467
            initlogger(debug, level)
3082
 
 
 
2468
    
3083
2469
    if server_settings["servicename"] != "Mandos":
3084
2470
        syslogger.setFormatter(
3085
2471
            logging.Formatter('Mandos ({}) [%(process)d]:'
3086
2472
                              ' %(levelname)s: %(message)s'.format(
3087
2473
                                  server_settings["servicename"])))
3088
 
 
 
2474
    
3089
2475
    # Parse config file with clients
3090
 
    client_config = configparser.ConfigParser(Client.client_defaults)
 
2476
    client_config = configparser.SafeConfigParser(Client
 
2477
                                                  .client_defaults)
3091
2478
    client_config.read(os.path.join(server_settings["configdir"],
3092
2479
                                    "clients.conf"))
3093
 
 
 
2480
    
3094
2481
    global mandos_dbus_service
3095
2482
    mandos_dbus_service = None
3096
 
 
 
2483
    
3097
2484
    socketfd = None
3098
2485
    if server_settings["socket"] != "":
3099
2486
        socketfd = server_settings["socket"]
3111
2498
            pidfilename = "/var/run/mandos.pid"
3112
2499
        pidfile = None
3113
2500
        try:
3114
 
            pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
 
2501
            pidfile = open(pidfilename, "w")
3115
2502
        except IOError as e:
3116
2503
            logger.error("Could not open file %r", pidfilename,
3117
2504
                         exc_info=e)
3118
 
 
3119
 
    for name, group in (("_mandos", "_mandos"),
3120
 
                        ("mandos", "mandos"),
3121
 
                        ("nobody", "nogroup")):
 
2505
    
 
2506
    for name in ("_mandos", "mandos", "nobody"):
3122
2507
        try:
3123
2508
            uid = pwd.getpwnam(name).pw_uid
3124
 
            gid = pwd.getpwnam(group).pw_gid
 
2509
            gid = pwd.getpwnam(name).pw_gid
3125
2510
            break
3126
2511
        except KeyError:
3127
2512
            continue
3131
2516
    try:
3132
2517
        os.setgid(gid)
3133
2518
        os.setuid(uid)
3134
 
        if debug:
3135
 
            logger.debug("Did setuid/setgid to {}:{}".format(uid,
3136
 
                                                             gid))
3137
2519
    except OSError as error:
3138
 
        logger.warning("Failed to setuid/setgid to {}:{}: {}"
3139
 
                       .format(uid, gid, os.strerror(error.errno)))
3140
2520
        if error.errno != errno.EPERM:
3141
2521
            raise
3142
 
 
 
2522
    
3143
2523
    if debug:
3144
2524
        # Enable all possible GnuTLS debugging
3145
 
 
 
2525
        
3146
2526
        # "Use a log level over 10 to enable all debugging options."
3147
2527
        # - GnuTLS manual
3148
 
        gnutls.global_set_log_level(11)
3149
 
 
3150
 
        @gnutls.log_func
 
2528
        gnutls.library.functions.gnutls_global_set_log_level(11)
 
2529
        
 
2530
        @gnutls.library.types.gnutls_log_func
3151
2531
        def debug_gnutls(level, string):
3152
2532
            logger.debug("GnuTLS: %s", string[:-1])
3153
 
 
3154
 
        gnutls.global_set_log_function(debug_gnutls)
3155
 
 
 
2533
        
 
2534
        gnutls.library.functions.gnutls_global_set_log_function(
 
2535
            debug_gnutls)
 
2536
        
3156
2537
        # Redirect stdin so all checkers get /dev/null
3157
2538
        null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
3158
2539
        os.dup2(null, sys.stdin.fileno())
3159
2540
        if null > 2:
3160
2541
            os.close(null)
3161
 
 
 
2542
    
3162
2543
    # Need to fork before connecting to D-Bus
3163
2544
    if not foreground:
3164
2545
        # Close all input and output, do double fork, etc.
3165
2546
        daemon()
3166
 
 
3167
 
    if gi.version_info < (3, 10, 2):
3168
 
        # multiprocessing will use threads, so before we use GLib we
3169
 
        # need to inform GLib that threads will be used.
3170
 
        GLib.threads_init()
3171
 
 
 
2547
    
 
2548
    # multiprocessing will use threads, so before we use gobject we
 
2549
    # need to inform gobject that threads will be used.
 
2550
    gobject.threads_init()
 
2551
    
3172
2552
    global main_loop
3173
2553
    # From the Avahi example code
3174
2554
    DBusGMainLoop(set_as_default=True)
3175
 
    main_loop = GLib.MainLoop()
 
2555
    main_loop = gobject.MainLoop()
3176
2556
    bus = dbus.SystemBus()
3177
2557
    # End of Avahi example code
3178
2558
    if use_dbus:
3183
2563
            old_bus_name = dbus.service.BusName(
3184
2564
                "se.bsnet.fukt.Mandos", bus,
3185
2565
                do_not_queue=True)
3186
 
        except dbus.exceptions.DBusException as e:
 
2566
        except dbus.exceptions.NameExistsException as e:
3187
2567
            logger.error("Disabling D-Bus:", exc_info=e)
3188
2568
            use_dbus = False
3189
2569
            server_settings["use_dbus"] = False
3191
2571
    if zeroconf:
3192
2572
        protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
3193
2573
        service = AvahiServiceToSyslog(
3194
 
            name=server_settings["servicename"],
3195
 
            servicetype="_mandos._tcp",
3196
 
            protocol=protocol,
3197
 
            bus=bus)
 
2574
            name = server_settings["servicename"],
 
2575
            servicetype = "_mandos._tcp",
 
2576
            protocol = protocol,
 
2577
            bus = bus)
3198
2578
        if server_settings["interface"]:
3199
2579
            service.interface = if_nametoindex(
3200
2580
                server_settings["interface"].encode("utf-8"))
3201
 
 
 
2581
    
3202
2582
    global multiprocessing_manager
3203
2583
    multiprocessing_manager = multiprocessing.Manager()
3204
 
 
 
2584
    
3205
2585
    client_class = Client
3206
2586
    if use_dbus:
3207
 
        client_class = functools.partial(ClientDBus, bus=bus)
3208
 
 
 
2587
        client_class = functools.partial(ClientDBus, bus = bus)
 
2588
    
3209
2589
    client_settings = Client.config_parser(client_config)
3210
2590
    old_client_settings = {}
3211
2591
    clients_data = {}
3212
 
 
 
2592
    
3213
2593
    # This is used to redirect stdout and stderr for checker processes
3214
2594
    global wnull
3215
 
    wnull = open(os.devnull, "w")  # A writable /dev/null
 
2595
    wnull = open(os.devnull, "w") # A writable /dev/null
3216
2596
    # Only used if server is running in foreground but not in debug
3217
2597
    # mode
3218
2598
    if debug or not foreground:
3219
2599
        wnull.close()
3220
 
 
 
2600
    
3221
2601
    # Get client data and settings from last running state.
3222
2602
    if server_settings["restore"]:
3223
2603
        try:
3224
2604
            with open(stored_state_path, "rb") as stored_state:
3225
 
                if sys.version_info.major == 2:
3226
 
                    clients_data, old_client_settings = pickle.load(
3227
 
                        stored_state)
3228
 
                else:
3229
 
                    bytes_clients_data, bytes_old_client_settings = (
3230
 
                        pickle.load(stored_state, encoding="bytes"))
3231
 
                    #   Fix bytes to strings
3232
 
                    #  clients_data
3233
 
                    # .keys()
3234
 
                    clients_data = {(key.decode("utf-8")
3235
 
                                     if isinstance(key, bytes)
3236
 
                                     else key): value
3237
 
                                    for key, value in
3238
 
                                    bytes_clients_data.items()}
3239
 
                    del bytes_clients_data
3240
 
                    for key in clients_data:
3241
 
                        value = {(k.decode("utf-8")
3242
 
                                  if isinstance(k, bytes) else k): v
3243
 
                                 for k, v in
3244
 
                                 clients_data[key].items()}
3245
 
                        clients_data[key] = value
3246
 
                        # .client_structure
3247
 
                        value["client_structure"] = [
3248
 
                            (s.decode("utf-8")
3249
 
                             if isinstance(s, bytes)
3250
 
                             else s) for s in
3251
 
                            value["client_structure"]]
3252
 
                        # .name, .host, and .checker_command
3253
 
                        for k in ("name", "host", "checker_command"):
3254
 
                            if isinstance(value[k], bytes):
3255
 
                                value[k] = value[k].decode("utf-8")
3256
 
                        if "key_id" not in value:
3257
 
                            value["key_id"] = ""
3258
 
                        elif "fingerprint" not in value:
3259
 
                            value["fingerprint"] = ""
3260
 
                    #  old_client_settings
3261
 
                    # .keys()
3262
 
                    old_client_settings = {
3263
 
                        (key.decode("utf-8")
3264
 
                         if isinstance(key, bytes)
3265
 
                         else key): value
3266
 
                        for key, value in
3267
 
                        bytes_old_client_settings.items()}
3268
 
                    del bytes_old_client_settings
3269
 
                    # .host and .checker_command
3270
 
                    for value in old_client_settings.values():
3271
 
                        for attribute in ("host", "checker_command"):
3272
 
                            if isinstance(value[attribute], bytes):
3273
 
                                value[attribute] = (value[attribute]
3274
 
                                                    .decode("utf-8"))
 
2605
                clients_data, old_client_settings = pickle.load(
 
2606
                    stored_state)
3275
2607
            os.remove(stored_state_path)
3276
2608
        except IOError as e:
3277
2609
            if e.errno == errno.ENOENT:
3285
2617
            logger.warning("Could not load persistent state: "
3286
2618
                           "EOFError:",
3287
2619
                           exc_info=e)
3288
 
 
 
2620
    
3289
2621
    with PGPEngine() as pgp:
3290
2622
        for client_name, client in clients_data.items():
3291
2623
            # Skip removed clients
3292
2624
            if client_name not in client_settings:
3293
2625
                continue
3294
 
 
 
2626
            
3295
2627
            # Decide which value to use after restoring saved state.
3296
2628
            # We have three different values: Old config file,
3297
2629
            # new config file, and saved state.
3308
2640
                        client[name] = value
3309
2641
                except KeyError:
3310
2642
                    pass
3311
 
 
 
2643
            
3312
2644
            # Clients who has passed its expire date can still be
3313
 
            # enabled if its last checker was successful.  A Client
 
2645
            # enabled if its last checker was successful.  Clients
3314
2646
            # whose checker succeeded before we stored its state is
3315
2647
            # assumed to have successfully run all checkers during
3316
2648
            # downtime.
3347
2679
                    client_name))
3348
2680
                client["secret"] = (client_settings[client_name]
3349
2681
                                    ["secret"])
3350
 
 
 
2682
    
3351
2683
    # Add/remove clients based on new changes made to config
3352
2684
    for client_name in (set(old_client_settings)
3353
2685
                        - set(client_settings)):
3355
2687
    for client_name in (set(client_settings)
3356
2688
                        - set(old_client_settings)):
3357
2689
        clients_data[client_name] = client_settings[client_name]
3358
 
 
 
2690
    
3359
2691
    # Create all client objects
3360
2692
    for client_name, client in clients_data.items():
3361
2693
        tcp_server.clients[client_name] = client_class(
3362
 
            name=client_name,
3363
 
            settings=client,
3364
 
            server_settings=server_settings)
3365
 
 
 
2694
            name = client_name,
 
2695
            settings = client,
 
2696
            server_settings = server_settings)
 
2697
    
3366
2698
    if not tcp_server.clients:
3367
2699
        logger.warning("No clients defined")
3368
 
 
 
2700
    
3369
2701
    if not foreground:
3370
2702
        if pidfile is not None:
3371
 
            pid = os.getpid()
3372
2703
            try:
3373
2704
                with pidfile:
3374
 
                    print(pid, file=pidfile)
 
2705
                    pid = os.getpid()
 
2706
                    pidfile.write("{}\n".format(pid).encode("utf-8"))
3375
2707
            except IOError:
3376
2708
                logger.error("Could not write to file %r with PID %d",
3377
2709
                             pidfilename, pid)
3378
2710
        del pidfile
3379
2711
        del pidfilename
3380
 
 
3381
 
    for termsig in (signal.SIGHUP, signal.SIGTERM):
3382
 
        GLib.unix_signal_add(GLib.PRIORITY_HIGH, termsig,
3383
 
                             lambda: main_loop.quit() and False)
3384
 
 
 
2712
    
 
2713
    signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
 
2714
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
 
2715
    
3385
2716
    if use_dbus:
3386
 
 
 
2717
        
3387
2718
        @alternate_dbus_interfaces(
3388
 
            {"se.recompile.Mandos": "se.bsnet.fukt.Mandos"})
3389
 
        class MandosDBusService(DBusObjectWithObjectManager):
 
2719
            { "se.recompile.Mandos": "se.bsnet.fukt.Mandos" })
 
2720
        class MandosDBusService(DBusObjectWithProperties):
3390
2721
            """A D-Bus proxy object"""
3391
 
 
 
2722
            
3392
2723
            def __init__(self):
3393
2724
                dbus.service.Object.__init__(self, bus, "/")
3394
 
 
 
2725
            
3395
2726
            _interface = "se.recompile.Mandos"
3396
 
 
 
2727
            
 
2728
            @dbus_interface_annotations(_interface)
 
2729
            def _foo(self):
 
2730
                return {
 
2731
                    "org.freedesktop.DBus.Property.EmitsChangedSignal":
 
2732
                    "false" }
 
2733
            
3397
2734
            @dbus.service.signal(_interface, signature="o")
3398
2735
            def ClientAdded(self, objpath):
3399
2736
                "D-Bus signal"
3400
2737
                pass
3401
 
 
 
2738
            
3402
2739
            @dbus.service.signal(_interface, signature="ss")
3403
 
            def ClientNotFound(self, key_id, address):
 
2740
            def ClientNotFound(self, fingerprint, address):
3404
2741
                "D-Bus signal"
3405
2742
                pass
3406
 
 
3407
 
            @dbus_annotations({"org.freedesktop.DBus.Deprecated":
3408
 
                               "true"})
 
2743
            
3409
2744
            @dbus.service.signal(_interface, signature="os")
3410
2745
            def ClientRemoved(self, objpath, name):
3411
2746
                "D-Bus signal"
3412
2747
                pass
3413
 
 
3414
 
            @dbus_annotations({"org.freedesktop.DBus.Deprecated":
3415
 
                               "true"})
 
2748
            
3416
2749
            @dbus.service.method(_interface, out_signature="ao")
3417
2750
            def GetAllClients(self):
3418
2751
                "D-Bus method"
3419
2752
                return dbus.Array(c.dbus_object_path for c in
3420
 
                                  tcp_server.clients.values())
3421
 
 
3422
 
            @dbus_annotations({"org.freedesktop.DBus.Deprecated":
3423
 
                               "true"})
 
2753
                                  tcp_server.clients.itervalues())
 
2754
            
3424
2755
            @dbus.service.method(_interface,
3425
2756
                                 out_signature="a{oa{sv}}")
3426
2757
            def GetAllClientsWithProperties(self):
3427
2758
                "D-Bus method"
3428
2759
                return dbus.Dictionary(
3429
 
                    {c.dbus_object_path: c.GetAll(
3430
 
                        "se.recompile.Mandos.Client")
3431
 
                     for c in tcp_server.clients.values()},
 
2760
                    { c.dbus_object_path: c.GetAll("")
 
2761
                      for c in tcp_server.clients.itervalues() },
3432
2762
                    signature="oa{sv}")
3433
 
 
 
2763
            
3434
2764
            @dbus.service.method(_interface, in_signature="o")
3435
2765
            def RemoveClient(self, object_path):
3436
2766
                "D-Bus method"
3437
 
                for c in tcp_server.clients.values():
 
2767
                for c in tcp_server.clients.itervalues():
3438
2768
                    if c.dbus_object_path == object_path:
3439
2769
                        del tcp_server.clients[c.name]
3440
2770
                        c.remove_from_connection()
3441
 
                        # Don't signal the disabling
 
2771
                        # Don't signal anything except ClientRemoved
3442
2772
                        c.disable(quiet=True)
3443
 
                        # Emit D-Bus signal for removal
3444
 
                        self.client_removed_signal(c)
 
2773
                        # Emit D-Bus signal
 
2774
                        self.ClientRemoved(object_path, c.name)
3445
2775
                        return
3446
2776
                raise KeyError(object_path)
3447
 
 
 
2777
            
3448
2778
            del _interface
3449
 
 
3450
 
            @dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
3451
 
                                 out_signature="a{oa{sa{sv}}}")
3452
 
            def GetManagedObjects(self):
3453
 
                """D-Bus method"""
3454
 
                return dbus.Dictionary(
3455
 
                    {client.dbus_object_path:
3456
 
                     dbus.Dictionary(
3457
 
                         {interface: client.GetAll(interface)
3458
 
                          for interface in
3459
 
                          client._get_all_interface_names()})
3460
 
                     for client in tcp_server.clients.values()})
3461
 
 
3462
 
            def client_added_signal(self, client):
3463
 
                """Send the new standard signal and the old signal"""
3464
 
                if use_dbus:
3465
 
                    # New standard signal
3466
 
                    self.InterfacesAdded(
3467
 
                        client.dbus_object_path,
3468
 
                        dbus.Dictionary(
3469
 
                            {interface: client.GetAll(interface)
3470
 
                             for interface in
3471
 
                             client._get_all_interface_names()}))
3472
 
                    # Old signal
3473
 
                    self.ClientAdded(client.dbus_object_path)
3474
 
 
3475
 
            def client_removed_signal(self, client):
3476
 
                """Send the new standard signal and the old signal"""
3477
 
                if use_dbus:
3478
 
                    # New standard signal
3479
 
                    self.InterfacesRemoved(
3480
 
                        client.dbus_object_path,
3481
 
                        client._get_all_interface_names())
3482
 
                    # Old signal
3483
 
                    self.ClientRemoved(client.dbus_object_path,
3484
 
                                       client.name)
3485
 
 
 
2779
        
3486
2780
        mandos_dbus_service = MandosDBusService()
3487
 
 
3488
 
    # Save modules to variables to exempt the modules from being
3489
 
    # unloaded before the function registered with atexit() is run.
3490
 
    mp = multiprocessing
3491
 
    wn = wnull
3492
 
 
 
2781
    
3493
2782
    def cleanup():
3494
2783
        "Cleanup function; run on exit"
3495
2784
        if zeroconf:
3496
2785
            service.cleanup()
3497
 
 
3498
 
        mp.active_children()
3499
 
        wn.close()
 
2786
        
 
2787
        multiprocessing.active_children()
 
2788
        wnull.close()
3500
2789
        if not (tcp_server.clients or client_settings):
3501
2790
            return
3502
 
 
 
2791
        
3503
2792
        # Store client before exiting. Secrets are encrypted with key
3504
2793
        # based on what config file has. If config file is
3505
2794
        # removed/edited, old secret will thus be unrecovable.
3506
2795
        clients = {}
3507
2796
        with PGPEngine() as pgp:
3508
 
            for client in tcp_server.clients.values():
 
2797
            for client in tcp_server.clients.itervalues():
3509
2798
                key = client_settings[client.name]["secret"]
3510
2799
                client.encrypted_secret = pgp.encrypt(client.secret,
3511
2800
                                                      key)
3512
2801
                client_dict = {}
3513
 
 
 
2802
                
3514
2803
                # A list of attributes that can not be pickled
3515
2804
                # + secret.
3516
 
                exclude = {"bus", "changedstate", "secret",
3517
 
                           "checker", "server_settings"}
 
2805
                exclude = { "bus", "changedstate", "secret",
 
2806
                            "checker", "server_settings" }
3518
2807
                for name, typ in inspect.getmembers(dbus.service
3519
2808
                                                    .Object):
3520
2809
                    exclude.add(name)
3521
 
 
 
2810
                
3522
2811
                client_dict["encrypted_secret"] = (client
3523
2812
                                                   .encrypted_secret)
3524
2813
                for attr in client.client_structure:
3525
2814
                    if attr not in exclude:
3526
2815
                        client_dict[attr] = getattr(client, attr)
3527
 
 
 
2816
                
3528
2817
                clients[client.name] = client_dict
3529
2818
                del client_settings[client.name]["secret"]
3530
 
 
 
2819
        
3531
2820
        try:
3532
2821
            with tempfile.NamedTemporaryFile(
3533
2822
                    mode='wb',
3535
2824
                    prefix='clients-',
3536
2825
                    dir=os.path.dirname(stored_state_path),
3537
2826
                    delete=False) as stored_state:
3538
 
                pickle.dump((clients, client_settings), stored_state,
3539
 
                            protocol=2)
 
2827
                pickle.dump((clients, client_settings), stored_state)
3540
2828
                tempname = stored_state.name
3541
2829
            os.rename(tempname, stored_state_path)
3542
2830
        except (IOError, OSError) as e:
3552
2840
                logger.warning("Could not save persistent state:",
3553
2841
                               exc_info=e)
3554
2842
                raise
3555
 
 
 
2843
        
3556
2844
        # Delete all clients, and settings from config
3557
2845
        while tcp_server.clients:
3558
2846
            name, client = tcp_server.clients.popitem()
3559
2847
            if use_dbus:
3560
2848
                client.remove_from_connection()
3561
 
            # Don't signal the disabling
 
2849
            # Don't signal anything except ClientRemoved
3562
2850
            client.disable(quiet=True)
3563
 
            # Emit D-Bus signal for removal
3564
2851
            if use_dbus:
3565
 
                mandos_dbus_service.client_removed_signal(client)
 
2852
                # Emit D-Bus signal
 
2853
                mandos_dbus_service.ClientRemoved(
 
2854
                    client.dbus_object_path, client.name)
3566
2855
        client_settings.clear()
3567
 
 
 
2856
    
3568
2857
    atexit.register(cleanup)
3569
 
 
3570
 
    for client in tcp_server.clients.values():
 
2858
    
 
2859
    for client in tcp_server.clients.itervalues():
3571
2860
        if use_dbus:
3572
 
            # Emit D-Bus signal for adding
3573
 
            mandos_dbus_service.client_added_signal(client)
 
2861
            # Emit D-Bus signal
 
2862
            mandos_dbus_service.ClientAdded(client.dbus_object_path)
3574
2863
        # Need to initiate checking of clients
3575
2864
        if client.enabled:
3576
2865
            client.init_checker()
3577
 
 
 
2866
    
3578
2867
    tcp_server.enable()
3579
2868
    tcp_server.server_activate()
3580
 
 
 
2869
    
3581
2870
    # Find out what port we got
3582
2871
    if zeroconf:
3583
2872
        service.port = tcp_server.socket.getsockname()[1]
3588
2877
    else:                       # IPv4
3589
2878
        logger.info("Now listening on address %r, port %d",
3590
2879
                    *tcp_server.socket.getsockname())
3591
 
 
3592
 
    # service.interface = tcp_server.socket.getsockname()[3]
3593
 
 
 
2880
    
 
2881
    #service.interface = tcp_server.socket.getsockname()[3]
 
2882
    
3594
2883
    try:
3595
2884
        if zeroconf:
3596
2885
            # From the Avahi example code
3601
2890
                cleanup()
3602
2891
                sys.exit(1)
3603
2892
            # End of Avahi example code
3604
 
 
3605
 
        GLib.io_add_watch(
3606
 
            GLib.IOChannel.unix_new(tcp_server.fileno()),
3607
 
            GLib.PRIORITY_DEFAULT, GLib.IO_IN,
3608
 
            lambda *args, **kwargs: (tcp_server.handle_request
3609
 
                                     (*args[2:], **kwargs) or True))
3610
 
 
 
2893
        
 
2894
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
 
2895
                             lambda *args, **kwargs:
 
2896
                             (tcp_server.handle_request
 
2897
                              (*args[2:], **kwargs) or True))
 
2898
        
3611
2899
        logger.debug("Starting main loop")
3612
2900
        main_loop.run()
3613
2901
    except AvahiError as error:
3622
2910
    # Must run before the D-Bus bus name gets deregistered
3623
2911
    cleanup()
3624
2912
 
3625
 
 
3626
 
def should_only_run_tests():
3627
 
    parser = argparse.ArgumentParser(add_help=False)
3628
 
    parser.add_argument("--check", action='store_true')
3629
 
    args, unknown_args = parser.parse_known_args()
3630
 
    run_tests = args.check
3631
 
    if run_tests:
3632
 
        # Remove --check argument from sys.argv
3633
 
        sys.argv[1:] = unknown_args
3634
 
    return run_tests
3635
 
 
3636
 
# Add all tests from doctest strings
3637
 
def load_tests(loader, tests, none):
3638
 
    import doctest
3639
 
    tests.addTests(doctest.DocTestSuite())
3640
 
    return tests
3641
2913
 
3642
2914
if __name__ == '__main__':
3643
 
    try:
3644
 
        if should_only_run_tests():
3645
 
            # Call using ./mandos --check [--verbose]
3646
 
            unittest.main()
3647
 
        else:
3648
 
            main()
3649
 
    finally:
3650
 
        logging.shutdown()
 
2915
    main()