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 -*-
2
# -*- mode: python; coding: utf-8 -*-
4
4
# Mandos server - give out binary blobs to connecting clients.
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
# methods "add", "remove", "server_state_changed",
10
# "entry_group_state_changed", "cleanup", and "activate" in the
11
# "AvahiService" class, and some lines in "main".
9
# following functions: "AvahiService.add", "AvahiService.remove",
10
# "server_state_changed", "entry_group_state_changed", and some lines
13
13
# Everything else is
14
# Copyright © 2008-2019 Teddy Hogeborn
15
# Copyright © 2008-2019 Björn Påhlsson
17
# This file is part of Mandos.
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 © 2007-2008 Teddy Hogeborn & Björn Påhlsson
16
# This program is free software: you can redistribute it and/or modify
17
# it under the terms of the GNU General Public License as published by
21
18
# the Free Software Foundation, either version 3 of the License, or
22
19
# (at your option) any later version.
24
# Mandos is distributed in the hope that it will be useful, but
25
# WITHOUT ANY WARRANTY; without even the implied warranty of
21
# This program is distributed in the hope that it will be useful,
22
# but WITHOUT ANY WARRANTY; without even the implied warranty of
26
23
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
24
# GNU General Public License for more details.
29
26
# You should have received a copy of the GNU General Public License
30
# along with Mandos. If not, see <http://www.gnu.org/licenses/>.
32
# Contact the authors at <mandos@recompile.se>.
35
from __future__ import (division, absolute_import, print_function,
39
from future_builtins import *
44
import SocketServer as socketserver
27
# along with this program. If not, see <http://www.gnu.org/licenses/>.
29
# Contact the authors at <mandos@fukt.bsnet.se>.
32
from __future__ import division
37
from optparse import OptionParser
52
import ConfigParser as configparser
41
import gnutls.connection
43
import gnutls.library.functions
44
import gnutls.library.constants
45
import gnutls.library.types
63
56
import logging.handlers
70
import cPickle as pickle
73
import multiprocessing
85
from gi.repository import GLib
86
61
from dbus.mainloop.glib import DBusGMainLoop
89
import xml.dom.minidom
92
if sys.version_info.major == 2:
95
# Try to find the value of SO_BINDTODEVICE:
97
# This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and
98
# newer, and it is also the most natural place for it:
99
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
100
except AttributeError:
102
# This is where SO_BINDTODEVICE was up to and including Python
104
from IN import SO_BINDTODEVICE
106
# In Python 2.7 it seems to have been removed entirely.
107
# Try running the C preprocessor:
109
cc = subprocess.Popen(["cc", "--language=c", "-E",
111
stdin=subprocess.PIPE,
112
stdout=subprocess.PIPE)
113
stdout = cc.communicate(
114
"#include <sys/socket.h>\nSO_BINDTODEVICE\n")[0]
115
SO_BINDTODEVICE = int(stdout.splitlines()[-1])
116
except (OSError, ValueError, IndexError):
118
SO_BINDTODEVICE = None
120
if sys.version_info.major == 2:
123
if sys.version_info < (3, 2):
124
configparser.Configparser = configparser.SafeConfigParser
127
stored_state_file = "clients.pickle"
129
logger = logging.getLogger()
133
if_nametoindex = ctypes.cdll.LoadLibrary(
134
ctypes.util.find_library("c")).if_nametoindex
135
except (OSError, AttributeError):
137
def if_nametoindex(interface):
138
"Get an interface index the hard way, i.e. using fcntl()"
139
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
140
with contextlib.closing(socket.socket()) as s:
141
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
142
struct.pack(b"16s16x", interface))
143
interface_index = struct.unpack("I", ifreq[16:20])[0]
144
return interface_index
147
def copy_function(func):
148
"""Make a copy of a function"""
149
if sys.version_info.major == 2:
150
return types.FunctionType(func.func_code,
156
return types.FunctionType(func.__code__,
163
def initlogger(debug, level=logging.WARNING):
164
"""init logger and add loglevel"""
167
syslogger = (logging.handlers.SysLogHandler(
168
facility=logging.handlers.SysLogHandler.LOG_DAEMON,
170
syslogger.setFormatter(logging.Formatter
171
('Mandos [%(process)d]: %(levelname)s:'
173
logger.addHandler(syslogger)
176
console = logging.StreamHandler()
177
console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
181
logger.addHandler(console)
182
logger.setLevel(level)
185
class PGPError(Exception):
186
"""Exception if encryption/decryption fails"""
191
"""A simple class for OpenPGP symmetric encryption & decryption"""
194
self.tempdir = tempfile.mkdtemp(prefix="mandos-")
197
output = subprocess.check_output(["gpgconf"])
198
for line in output.splitlines():
199
name, text, path = line.split(b":")
204
if e.errno != errno.ENOENT:
206
self.gnupgargs = ['--batch',
207
'--homedir', self.tempdir,
210
# Only GPG version 1 has the --no-use-agent option.
211
if self.gpg == "gpg" or self.gpg.endswith("/gpg"):
212
self.gnupgargs.append("--no-use-agent")
217
def __exit__(self, exc_type, exc_value, traceback):
225
if self.tempdir is not None:
226
# Delete contents of tempdir
227
for root, dirs, files in os.walk(self.tempdir,
229
for filename in files:
230
os.remove(os.path.join(root, filename))
232
os.rmdir(os.path.join(root, dirname))
234
os.rmdir(self.tempdir)
237
def password_encode(self, password):
238
# Passphrase can not be empty and can not contain newlines or
239
# NUL bytes. So we prefix it and hex encode it.
240
encoded = b"mandos" + binascii.hexlify(password)
241
if len(encoded) > 2048:
242
# GnuPG can't handle long passwords, so encode differently
243
encoded = (b"mandos" + password.replace(b"\\", b"\\\\")
244
.replace(b"\n", b"\\n")
245
.replace(b"\0", b"\\x00"))
248
def encrypt(self, data, password):
249
passphrase = self.password_encode(password)
250
with tempfile.NamedTemporaryFile(
251
dir=self.tempdir) as passfile:
252
passfile.write(passphrase)
254
proc = subprocess.Popen([self.gpg, '--symmetric',
258
stdin=subprocess.PIPE,
259
stdout=subprocess.PIPE,
260
stderr=subprocess.PIPE)
261
ciphertext, err = proc.communicate(input=data)
262
if proc.returncode != 0:
266
def decrypt(self, data, password):
267
passphrase = self.password_encode(password)
268
with tempfile.NamedTemporaryFile(
269
dir=self.tempdir) as passfile:
270
passfile.write(passphrase)
272
proc = subprocess.Popen([self.gpg, '--decrypt',
276
stdin=subprocess.PIPE,
277
stdout=subprocess.PIPE,
278
stderr=subprocess.PIPE)
279
decrypted_plaintext, err = proc.communicate(input=data)
280
if proc.returncode != 0:
282
return decrypted_plaintext
285
# Pretend that we have an Avahi module
287
"""This isn't so much a class as it is a module-like namespace."""
288
IF_UNSPEC = -1 # avahi-common/address.h
289
PROTO_UNSPEC = -1 # avahi-common/address.h
290
PROTO_INET = 0 # avahi-common/address.h
291
PROTO_INET6 = 1 # avahi-common/address.h
292
DBUS_NAME = "org.freedesktop.Avahi"
293
DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup"
294
DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
295
DBUS_PATH_SERVER = "/"
298
def string_array_to_txt_array(t):
299
return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
300
for s in t), signature="ay")
301
ENTRY_GROUP_ESTABLISHED = 2 # avahi-common/defs.h
302
ENTRY_GROUP_COLLISION = 3 # avahi-common/defs.h
303
ENTRY_GROUP_FAILURE = 4 # avahi-common/defs.h
304
SERVER_INVALID = 0 # avahi-common/defs.h
305
SERVER_REGISTERING = 1 # avahi-common/defs.h
306
SERVER_RUNNING = 2 # avahi-common/defs.h
307
SERVER_COLLISION = 3 # avahi-common/defs.h
308
SERVER_FAILURE = 4 # avahi-common/defs.h
64
# Brief description of the operation of this program:
66
# This server announces itself as a Zeroconf service. Connecting
67
# clients use the TLS protocol, with the unusual quirk that this
68
# server program acts as a TLS "client" while a connecting client acts
69
# as a TLS "server". The client (acting as a TLS "server") must
70
# supply an OpenPGP certificate, and the fingerprint of this
71
# certificate is used by this server to look up (in a list read from a
72
# file at start time) which binary blob to give the client. No other
73
# authentication or authorization is done by this server.
76
logger = logging.Logger('mandos')
77
syslogger = logging.handlers.SysLogHandler\
78
(facility = logging.handlers.SysLogHandler.LOG_DAEMON)
79
syslogger.setFormatter(logging.Formatter\
80
('%(levelname)s: %(message)s'))
81
logger.addHandler(syslogger)
311
85
class AvahiError(Exception):
312
def __init__(self, value, *args, **kwargs):
86
def __init__(self, value):
313
87
self.value = value
314
return super(AvahiError, self).__init__(value, *args,
89
return repr(self.value)
318
91
class AvahiServiceError(AvahiError):
322
94
class AvahiGroupError(AvahiError):
327
"""An Avahi (Zeroconf) service.
98
class AvahiService(object):
330
100
interface: integer; avahi.IF_UNSPEC or an interface index.
331
101
Used to optionally bind to the specified interface.
332
name: string; Example: 'Mandos'
333
type: string; Example: '_mandos._tcp'.
334
See <https://www.iana.org/assignments/service-names-port-numbers>
335
port: integer; what port to announce
336
TXT: list of strings; TXT record for the service
337
domain: string; Domain to publish on, default to .local if empty.
338
host: string; Host to publish records for, default is localhost
339
max_renames: integer; maximum number of renames
340
rename_count: integer; counter so we only rename after collisions
341
a sensible number of times
342
group: D-Bus Entry Group
344
bus: dbus.SystemBus()
102
name = string; Example: "Mandos"
103
type = string; Example: "_mandos._tcp".
104
See <http://www.dns-sd.org/ServiceTypes.html>
105
port = integer; what port to announce
106
TXT = list of strings; TXT record for the service
107
domain = string; Domain to publish on, default to .local if empty.
108
host = string; Host to publish records for, default to localhost
110
max_renames = integer; maximum number of renames
111
rename_count = integer; counter so we only rename after collisions
112
a sensible number of times
348
interface=avahi.IF_UNSPEC,
356
protocol=avahi.PROTO_UNSPEC,
114
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
115
type = None, port = None, TXT = None, domain = "",
116
host = "", max_renames = 12):
117
"""An Avahi (Zeroconf) service. """
358
118
self.interface = interface
360
self.type = servicetype
362
self.TXT = TXT if TXT is not None else []
363
126
self.domain = domain
365
128
self.rename_count = 0
366
self.max_renames = max_renames
367
self.protocol = protocol
368
self.group = None # our entry group
371
self.entry_group_state_changed_match = None
373
def rename(self, remove=True):
374
130
"""Derived from the Avahi example code"""
375
131
if self.rename_count >= self.max_renames:
376
logger.critical("No suitable Zeroconf service name found"
377
" after %i retries, exiting.",
132
logger.critical(u"No suitable service name found after %i"
133
u" retries, exiting.", rename_count)
379
134
raise AvahiServiceError("Too many renames")
381
self.server.GetAlternativeServiceName(self.name))
135
name = server.GetAlternativeServiceName(name)
136
logger.notice(u"Changing name to %r ...", name)
382
139
self.rename_count += 1
383
logger.info("Changing Zeroconf service name to %r ...",
389
except dbus.exceptions.DBusException as error:
390
if (error.get_dbus_name()
391
== "org.freedesktop.Avahi.CollisionError"):
392
logger.info("Local Zeroconf service name collision.")
393
return self.rename(remove=False)
395
logger.critical("D-Bus Exception", exc_info=error)
399
140
def remove(self):
400
141
"""Derived from the Avahi example code"""
401
if self.entry_group_state_changed_match is not None:
402
self.entry_group_state_changed_match.remove()
403
self.entry_group_state_changed_match = None
404
if self.group is not None:
142
if group is not None:
408
145
"""Derived from the Avahi example code"""
410
if self.group is None:
411
self.group = dbus.Interface(
412
self.bus.get_object(avahi.DBUS_NAME,
413
self.server.EntryGroupNew()),
414
avahi.DBUS_INTERFACE_ENTRY_GROUP)
415
self.entry_group_state_changed_match = (
416
self.group.connect_to_signal(
417
'StateChanged', self.entry_group_state_changed))
418
logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
419
self.name, self.type)
420
self.group.AddService(
423
dbus.UInt32(0), # flags
424
self.name, self.type,
425
self.domain, self.host,
426
dbus.UInt16(self.port),
427
avahi.string_array_to_txt_array(self.TXT))
430
def entry_group_state_changed(self, state, error):
431
"""Derived from the Avahi example code"""
432
logger.debug("Avahi entry group state change: %i", state)
434
if state == avahi.ENTRY_GROUP_ESTABLISHED:
435
logger.debug("Zeroconf service established.")
436
elif state == avahi.ENTRY_GROUP_COLLISION:
437
logger.info("Zeroconf service name collision.")
439
elif state == avahi.ENTRY_GROUP_FAILURE:
440
logger.critical("Avahi: Error in group state changed %s",
442
raise AvahiGroupError("State changed: {!s}".format(error))
445
"""Derived from the Avahi example code"""
446
if self.group is not None:
449
except (dbus.exceptions.UnknownMethodException,
450
dbus.exceptions.DBusException):
455
def server_state_changed(self, state, error=None):
456
"""Derived from the Avahi example code"""
457
logger.debug("Avahi server state change: %i", state)
459
avahi.SERVER_INVALID: "Zeroconf server invalid",
460
avahi.SERVER_REGISTERING: None,
461
avahi.SERVER_COLLISION: "Zeroconf server name collision",
462
avahi.SERVER_FAILURE: "Zeroconf server failure",
464
if state in bad_states:
465
if bad_states[state] is not None:
467
logger.error(bad_states[state])
469
logger.error(bad_states[state] + ": %r", error)
471
elif state == avahi.SERVER_RUNNING:
474
except dbus.exceptions.DBusException as error:
475
if (error.get_dbus_name()
476
== "org.freedesktop.Avahi.CollisionError"):
477
logger.info("Local Zeroconf service name"
479
return self.rename(remove=False)
481
logger.critical("D-Bus Exception", exc_info=error)
486
logger.debug("Unknown state: %r", state)
488
logger.debug("Unknown state: %r: %r", state, error)
491
"""Derived from the Avahi example code"""
492
if self.server is None:
493
self.server = dbus.Interface(
494
self.bus.get_object(avahi.DBUS_NAME,
495
avahi.DBUS_PATH_SERVER,
496
follow_name_owner_changes=True),
497
avahi.DBUS_INTERFACE_SERVER)
498
self.server.connect_to_signal("StateChanged",
499
self.server_state_changed)
500
self.server_state_changed(self.server.GetState())
503
class AvahiServiceToSyslog(AvahiService):
504
def rename(self, *args, **kwargs):
505
"""Add the new name to the syslog messages"""
506
ret = super(AvahiServiceToSyslog, self).rename(*args, **kwargs)
507
syslogger.setFormatter(logging.Formatter(
508
'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
513
# Pretend that we have a GnuTLS module
515
"""This isn't so much a class as it is a module-like namespace."""
517
library = ctypes.util.find_library("gnutls")
519
library = ctypes.util.find_library("gnutls-deb0")
520
_library = ctypes.cdll.LoadLibrary(library)
523
# Unless otherwise indicated, the constants and types below are
524
# all from the gnutls/gnutls.h C header file.
535
E_NO_CERTIFICATE_FOUND = -49
540
KEYID_USE_SHA256 = 1 # gnutls/x509.h
541
OPENPGP_FMT_RAW = 0 # gnutls/openpgp.h
544
class session_int(ctypes.Structure):
546
session_t = ctypes.POINTER(session_int)
548
class certificate_credentials_st(ctypes.Structure):
550
certificate_credentials_t = ctypes.POINTER(
551
certificate_credentials_st)
552
certificate_type_t = ctypes.c_int
554
class datum_t(ctypes.Structure):
555
_fields_ = [('data', ctypes.POINTER(ctypes.c_ubyte)),
556
('size', ctypes.c_uint)]
558
class openpgp_crt_int(ctypes.Structure):
560
openpgp_crt_t = ctypes.POINTER(openpgp_crt_int)
561
openpgp_crt_fmt_t = ctypes.c_int # gnutls/openpgp.h
562
log_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
563
credentials_type_t = ctypes.c_int
564
transport_ptr_t = ctypes.c_void_p
565
close_request_t = ctypes.c_int
568
class Error(Exception):
569
def __init__(self, message=None, code=None, args=()):
570
# Default usage is by a message string, but if a return
571
# code is passed, convert it to a string with
574
if message is None and code is not None:
575
message = gnutls.strerror(code)
576
return super(gnutls.Error, self).__init__(
579
class CertificateSecurityError(Error):
585
self._c_object = gnutls.certificate_credentials_t()
586
gnutls.certificate_allocate_credentials(
587
ctypes.byref(self._c_object))
588
self.type = gnutls.CRD_CERTIFICATE
591
gnutls.certificate_free_credentials(self._c_object)
594
def __init__(self, socket, credentials=None):
595
self._c_object = gnutls.session_t()
596
gnutls_flags = gnutls.CLIENT
597
if gnutls.check_version(b"3.5.6"):
598
gnutls_flags |= gnutls.NO_TICKETS
600
gnutls_flags |= gnutls.ENABLE_RAWPK
601
gnutls.init(ctypes.byref(self._c_object), gnutls_flags)
603
gnutls.set_default_priority(self._c_object)
604
gnutls.transport_set_ptr(self._c_object, socket.fileno())
605
gnutls.handshake_set_private_extensions(self._c_object,
608
if credentials is None:
609
credentials = gnutls.Credentials()
610
gnutls.credentials_set(self._c_object, credentials.type,
611
ctypes.cast(credentials._c_object,
613
self.credentials = credentials
616
gnutls.deinit(self._c_object)
619
return gnutls.handshake(self._c_object)
621
def send(self, data):
625
data_len -= gnutls.record_send(self._c_object,
630
return gnutls.bye(self._c_object, gnutls.SHUT_RDWR)
632
# Error handling functions
633
def _error_code(result):
634
"""A function to raise exceptions on errors, suitable
635
for the 'restype' attribute on ctypes functions"""
638
if result == gnutls.E_NO_CERTIFICATE_FOUND:
639
raise gnutls.CertificateSecurityError(code=result)
640
raise gnutls.Error(code=result)
642
def _retry_on_error(result, func, arguments):
643
"""A function to retry on some errors, suitable
644
for the 'errcheck' attribute on ctypes functions"""
646
if result not in (gnutls.E_INTERRUPTED, gnutls.E_AGAIN):
647
return _error_code(result)
648
result = func(*arguments)
651
# Unless otherwise indicated, the function declarations below are
652
# all from the gnutls/gnutls.h C header file.
655
priority_set_direct = _library.gnutls_priority_set_direct
656
priority_set_direct.argtypes = [session_t, ctypes.c_char_p,
657
ctypes.POINTER(ctypes.c_char_p)]
658
priority_set_direct.restype = _error_code
660
init = _library.gnutls_init
661
init.argtypes = [ctypes.POINTER(session_t), ctypes.c_int]
662
init.restype = _error_code
664
set_default_priority = _library.gnutls_set_default_priority
665
set_default_priority.argtypes = [session_t]
666
set_default_priority.restype = _error_code
668
record_send = _library.gnutls_record_send
669
record_send.argtypes = [session_t, ctypes.c_void_p,
671
record_send.restype = ctypes.c_ssize_t
672
record_send.errcheck = _retry_on_error
674
certificate_allocate_credentials = (
675
_library.gnutls_certificate_allocate_credentials)
676
certificate_allocate_credentials.argtypes = [
677
ctypes.POINTER(certificate_credentials_t)]
678
certificate_allocate_credentials.restype = _error_code
680
certificate_free_credentials = (
681
_library.gnutls_certificate_free_credentials)
682
certificate_free_credentials.argtypes = [
683
certificate_credentials_t]
684
certificate_free_credentials.restype = None
686
handshake_set_private_extensions = (
687
_library.gnutls_handshake_set_private_extensions)
688
handshake_set_private_extensions.argtypes = [session_t,
690
handshake_set_private_extensions.restype = None
692
credentials_set = _library.gnutls_credentials_set
693
credentials_set.argtypes = [session_t, credentials_type_t,
695
credentials_set.restype = _error_code
697
strerror = _library.gnutls_strerror
698
strerror.argtypes = [ctypes.c_int]
699
strerror.restype = ctypes.c_char_p
701
certificate_type_get = _library.gnutls_certificate_type_get
702
certificate_type_get.argtypes = [session_t]
703
certificate_type_get.restype = _error_code
705
certificate_get_peers = _library.gnutls_certificate_get_peers
706
certificate_get_peers.argtypes = [session_t,
707
ctypes.POINTER(ctypes.c_uint)]
708
certificate_get_peers.restype = ctypes.POINTER(datum_t)
710
global_set_log_level = _library.gnutls_global_set_log_level
711
global_set_log_level.argtypes = [ctypes.c_int]
712
global_set_log_level.restype = None
714
global_set_log_function = _library.gnutls_global_set_log_function
715
global_set_log_function.argtypes = [log_func]
716
global_set_log_function.restype = None
718
deinit = _library.gnutls_deinit
719
deinit.argtypes = [session_t]
720
deinit.restype = None
722
handshake = _library.gnutls_handshake
723
handshake.argtypes = [session_t]
724
handshake.restype = _error_code
725
handshake.errcheck = _retry_on_error
727
transport_set_ptr = _library.gnutls_transport_set_ptr
728
transport_set_ptr.argtypes = [session_t, transport_ptr_t]
729
transport_set_ptr.restype = None
731
bye = _library.gnutls_bye
732
bye.argtypes = [session_t, close_request_t]
733
bye.restype = _error_code
734
bye.errcheck = _retry_on_error
736
check_version = _library.gnutls_check_version
737
check_version.argtypes = [ctypes.c_char_p]
738
check_version.restype = ctypes.c_char_p
740
_need_version = b"3.3.0"
741
if check_version(_need_version) is None:
742
raise self.Error("Needs GnuTLS {} or later"
743
.format(_need_version))
745
_tls_rawpk_version = b"3.6.6"
746
has_rawpk = bool(check_version(_tls_rawpk_version))
750
class pubkey_st(ctypes.Structure):
752
pubkey_t = ctypes.POINTER(pubkey_st)
754
x509_crt_fmt_t = ctypes.c_int
756
# All the function declarations below are from gnutls/abstract.h
757
pubkey_init = _library.gnutls_pubkey_init
758
pubkey_init.argtypes = [ctypes.POINTER(pubkey_t)]
759
pubkey_init.restype = _error_code
761
pubkey_import = _library.gnutls_pubkey_import
762
pubkey_import.argtypes = [pubkey_t, ctypes.POINTER(datum_t),
764
pubkey_import.restype = _error_code
766
pubkey_get_key_id = _library.gnutls_pubkey_get_key_id
767
pubkey_get_key_id.argtypes = [pubkey_t, ctypes.c_int,
768
ctypes.POINTER(ctypes.c_ubyte),
769
ctypes.POINTER(ctypes.c_size_t)]
770
pubkey_get_key_id.restype = _error_code
772
pubkey_deinit = _library.gnutls_pubkey_deinit
773
pubkey_deinit.argtypes = [pubkey_t]
774
pubkey_deinit.restype = None
776
# All the function declarations below are from gnutls/openpgp.h
778
openpgp_crt_init = _library.gnutls_openpgp_crt_init
779
openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
780
openpgp_crt_init.restype = _error_code
782
openpgp_crt_import = _library.gnutls_openpgp_crt_import
783
openpgp_crt_import.argtypes = [openpgp_crt_t,
784
ctypes.POINTER(datum_t),
786
openpgp_crt_import.restype = _error_code
788
openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
789
openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
790
ctypes.POINTER(ctypes.c_uint)]
791
openpgp_crt_verify_self.restype = _error_code
793
openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
794
openpgp_crt_deinit.argtypes = [openpgp_crt_t]
795
openpgp_crt_deinit.restype = None
797
openpgp_crt_get_fingerprint = (
798
_library.gnutls_openpgp_crt_get_fingerprint)
799
openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
803
openpgp_crt_get_fingerprint.restype = _error_code
805
if check_version(b"3.6.4"):
806
certificate_type_get2 = _library.gnutls_certificate_type_get2
807
certificate_type_get2.argtypes = [session_t, ctypes.c_int]
808
certificate_type_get2.restype = _error_code
810
# Remove non-public functions
811
del _error_code, _retry_on_error
814
def call_pipe(connection, # : multiprocessing.Connection
815
func, *args, **kwargs):
816
"""This function is meant to be called by multiprocessing.Process
818
This function runs func(*args, **kwargs), and writes the resulting
819
return value on the provided multiprocessing.Connection.
821
connection.send(func(*args, **kwargs))
148
group = dbus.Interface\
149
(bus.get_object(avahi.DBUS_NAME,
150
server.EntryGroupNew()),
151
avahi.DBUS_INTERFACE_ENTRY_GROUP)
152
group.connect_to_signal('StateChanged',
153
entry_group_state_changed)
154
logger.debug(u"Adding service '%s' of type '%s' ...",
155
service.name, service.type)
157
self.interface, # interface
158
avahi.PROTO_INET6, # protocol
159
dbus.UInt32(0), # flags
160
self.name, self.type,
161
self.domain, self.host,
162
dbus.UInt16(self.port),
163
avahi.string_array_to_txt_array(self.TXT))
166
# From the Avahi example code:
167
group = None # our entry group
168
# End of Avahi example code
171
class Client(object):
826
172
"""A representation of a client host served by this server.
829
approved: bool(); 'None' if not yet approved/disapproved
830
approval_delay: datetime.timedelta(); Time to wait for approval
831
approval_duration: datetime.timedelta(); Duration of one approval
832
checker: multiprocessing.Process(); a running checker process used
833
to see if the client lives. 'None' if no process is
835
checker_callback_tag: a GLib event source tag, or None
836
checker_command: string; External command which is run to check
837
if client lives. %() expansions are done at
174
name: string; from the config file, used in log messages
175
fingerprint: string (40 or 32 hexadecimal digits); used to
176
uniquely identify the client
177
secret: bytestring; sent verbatim (over TLS) to client
178
fqdn: string (FQDN); available for use by the checker command
179
created: datetime.datetime(); object creation, not client host
180
last_checked_ok: datetime.datetime() or None if not yet checked OK
181
timeout: datetime.timedelta(); How long from last_checked_ok
182
until this client is invalid
183
interval: datetime.timedelta(); How often to start a new checker
184
stop_hook: If set, called by stop() as stop_hook(self)
185
checker: subprocess.Popen(); a running checker process used
186
to see if the client lives.
187
'None' if no process is running.
188
checker_initiator_tag: a gobject event source tag, or None
189
stop_initiator_tag: - '' -
190
checker_callback_tag: - '' -
191
checker_command: string; External command which is run to check if
192
client lives. %() expansions are done at
838
193
runtime with vars(self) as dict, so that for
839
194
instance %(name)s can be used in the command.
840
checker_initiator_tag: a GLib event source tag, or None
841
created: datetime.datetime(); (UTC) object creation
842
client_structure: Object describing what attributes a client has
843
and is used for storing the client at exit
844
current_checker_command: string; current running checker_command
845
disable_initiator_tag: a GLib event source tag, or None
847
fingerprint: string (40 or 32 hexadecimal digits); used to
848
uniquely identify an OpenPGP client
849
key_id: string (64 hexadecimal digits); used to uniquely identify
850
a client using raw public keys
851
host: string; available for use by the checker command
852
interval: datetime.timedelta(); How often to start a new checker
853
last_approval_request: datetime.datetime(); (UTC) or None
854
last_checked_ok: datetime.datetime(); (UTC) or None
855
last_checker_status: integer between 0 and 255 reflecting exit
856
status of last checker. -1 reflects crashed
857
checker, -2 means no checker completed yet.
858
last_checker_signal: The signal which killed the last checker, if
859
last_checker_status is -1
860
last_enabled: datetime.datetime(); (UTC) or None
861
name: string; from the config file, used in log messages and
863
secret: bytestring; sent verbatim (over TLS) to client
864
timeout: datetime.timedelta(); How long from last_checked_ok
865
until this client is disabled
866
extended_timeout: extra long timeout when secret has been sent
867
runtime_expansions: Allowed attributes for runtime expansion.
868
expires: datetime.datetime(); time (UTC) when a client will be
870
server_settings: The server_settings dict from main()
196
_timeout: Real variable for 'timeout'
197
_interval: Real variable for 'interval'
198
_timeout_milliseconds: Used when calling gobject.timeout_add()
199
_interval_milliseconds: - '' -
873
runtime_expansions = ("approval_delay", "approval_duration",
874
"created", "enabled", "expires", "key_id",
875
"fingerprint", "host", "interval",
876
"last_approval_request", "last_checked_ok",
877
"last_enabled", "name", "timeout")
880
"extended_timeout": "PT15M",
882
"checker": "fping -q -- %%(host)s",
884
"approval_delay": "PT0S",
885
"approval_duration": "PT1S",
886
"approved_by_default": "True",
891
def config_parser(config):
892
"""Construct a new dict of client settings of this form:
893
{ client_name: {setting_name: value, ...}, ...}
894
with exceptions for any special settings as defined above.
895
NOTE: Must be a pure function. Must return the same result
896
value given the same arguments.
899
for client_name in config.sections():
900
section = dict(config.items(client_name))
901
client = settings[client_name] = {}
903
client["host"] = section["host"]
904
# Reformat values from string types to Python types
905
client["approved_by_default"] = config.getboolean(
906
client_name, "approved_by_default")
907
client["enabled"] = config.getboolean(client_name,
910
# Uppercase and remove spaces from key_id and fingerprint
911
# for later comparison purposes with return value from the
912
# key_id() and fingerprint() functions
913
client["key_id"] = (section.get("key_id", "").upper()
915
client["fingerprint"] = (section["fingerprint"].upper()
917
if "secret" in section:
918
client["secret"] = codecs.decode(section["secret"]
921
elif "secfile" in section:
922
with open(os.path.expanduser(os.path.expandvars
923
(section["secfile"])),
925
client["secret"] = secfile.read()
927
raise TypeError("No secret or secfile for section {}"
929
client["timeout"] = string_to_delta(section["timeout"])
930
client["extended_timeout"] = string_to_delta(
931
section["extended_timeout"])
932
client["interval"] = string_to_delta(section["interval"])
933
client["approval_delay"] = string_to_delta(
934
section["approval_delay"])
935
client["approval_duration"] = string_to_delta(
936
section["approval_duration"])
937
client["checker_command"] = section["checker"]
938
client["last_approval_request"] = None
939
client["last_checked_ok"] = None
940
client["last_checker_status"] = -2
944
def __init__(self, settings, name=None, server_settings=None):
201
def _set_timeout(self, timeout):
202
"Setter function for 'timeout' attribute"
203
self._timeout = timeout
204
self._timeout_milliseconds = ((self.timeout.days
205
* 24 * 60 * 60 * 1000)
206
+ (self.timeout.seconds * 1000)
207
+ (self.timeout.microseconds
209
timeout = property(lambda self: self._timeout,
212
def _set_interval(self, interval):
213
"Setter function for 'interval' attribute"
214
self._interval = interval
215
self._interval_milliseconds = ((self.interval.days
216
* 24 * 60 * 60 * 1000)
217
+ (self.interval.seconds
219
+ (self.interval.microseconds
221
interval = property(lambda self: self._interval,
224
def __init__(self, name=None, stop_hook=None, fingerprint=None,
225
secret=None, secfile=None, fqdn=None, timeout=None,
226
interval=-1, checker=None):
227
"""Note: the 'checker' argument sets the 'checker_command'
228
attribute and not the 'checker' attribute.."""
946
if server_settings is None:
948
self.server_settings = server_settings
949
# adding all client settings
950
for setting, value in settings.items():
951
setattr(self, setting, value)
954
if not hasattr(self, "last_enabled"):
955
self.last_enabled = datetime.datetime.utcnow()
956
if not hasattr(self, "expires"):
957
self.expires = (datetime.datetime.utcnow()
230
logger.debug(u"Creating client %r", self.name)
231
# Uppercase and remove spaces from fingerprint
232
# for later comparison purposes with return value of
233
# the fingerprint() function
234
self.fingerprint = fingerprint.upper().replace(u" ", u"")
235
logger.debug(u" Fingerprint: %s", self.fingerprint)
237
self.secret = secret.decode(u"base64")
240
self.secret = sf.read()
960
self.last_enabled = None
963
logger.debug("Creating client %r", self.name)
964
logger.debug(" Key ID: %s", self.key_id)
965
logger.debug(" Fingerprint: %s", self.fingerprint)
966
self.created = settings.get("created",
967
datetime.datetime.utcnow())
969
# attributes specific for this server instance
243
raise TypeError(u"No secret or secfile for client %s"
246
self.created = datetime.datetime.now()
247
self.last_checked_ok = None
248
self.timeout = string_to_delta(timeout)
249
self.interval = string_to_delta(interval)
250
self.stop_hook = stop_hook
970
251
self.checker = None
971
252
self.checker_initiator_tag = None
972
self.disable_initiator_tag = None
253
self.stop_initiator_tag = None
973
254
self.checker_callback_tag = None
974
self.current_checker_command = None
976
self.approvals_pending = 0
977
self.changedstate = multiprocessing_manager.Condition(
978
multiprocessing_manager.Lock())
979
self.client_structure = [attr
980
for attr in self.__dict__.keys()
981
if not attr.startswith("_")]
982
self.client_structure.append("client_structure")
984
for name, t in inspect.getmembers(
985
type(self), lambda obj: isinstance(obj, property)):
986
if not name.startswith("_"):
987
self.client_structure.append(name)
989
# Send notice to process children that client state has changed
990
def send_changedstate(self):
991
with self.changedstate:
992
self.changedstate.notify_all()
255
self.check_command = checker
995
257
"""Start this client's checker and timeout hooks"""
996
if getattr(self, "enabled", False):
999
self.expires = datetime.datetime.utcnow() + self.timeout
1001
self.last_enabled = datetime.datetime.utcnow()
1003
self.send_changedstate()
1005
def disable(self, quiet=True):
1006
"""Disable this client."""
1007
if not getattr(self, "enabled", False):
1010
logger.info("Disabling client %s", self.name)
1011
if getattr(self, "disable_initiator_tag", None) is not None:
1012
GLib.source_remove(self.disable_initiator_tag)
1013
self.disable_initiator_tag = None
1015
if getattr(self, "checker_initiator_tag", None) is not None:
1016
GLib.source_remove(self.checker_initiator_tag)
1017
self.checker_initiator_tag = None
1019
self.enabled = False
1021
self.send_changedstate()
1022
# Do not run this again if called by a GLib.timeout_add
1028
def init_checker(self):
1029
258
# Schedule a new checker to be started an 'interval' from now,
1030
259
# and every interval from then on.
1031
if self.checker_initiator_tag is not None:
1032
GLib.source_remove(self.checker_initiator_tag)
1033
self.checker_initiator_tag = GLib.timeout_add(
1034
int(self.interval.total_seconds() * 1000),
1036
# Schedule a disable() when 'timeout' has passed
1037
if self.disable_initiator_tag is not None:
1038
GLib.source_remove(self.disable_initiator_tag)
1039
self.disable_initiator_tag = GLib.timeout_add(
1040
int(self.timeout.total_seconds() * 1000), self.disable)
260
self.checker_initiator_tag = gobject.timeout_add\
261
(self._interval_milliseconds,
1041
263
# Also start a new checker *right now*.
1042
264
self.start_checker()
1044
def checker_callback(self, source, condition, connection,
265
# Schedule a stop() when 'timeout' has passed
266
self.stop_initiator_tag = gobject.timeout_add\
267
(self._timeout_milliseconds,
271
The possibility that a client might be restarted is left open,
272
but not currently used."""
273
# If this client doesn't have a secret, it is already stopped.
275
logger.debug(u"Stopping client %s", self.name)
279
if getattr(self, "stop_initiator_tag", False):
280
gobject.source_remove(self.stop_initiator_tag)
281
self.stop_initiator_tag = None
282
if getattr(self, "checker_initiator_tag", False):
283
gobject.source_remove(self.checker_initiator_tag)
284
self.checker_initiator_tag = None
288
# Do not run this again if called by a gobject.timeout_add
291
self.stop_hook = None
293
def checker_callback(self, pid, condition):
1046
294
"""The checker has completed, so take appropriate actions."""
1047
# Read return code from connection (see call_pipe)
1048
returncode = connection.recv()
295
now = datetime.datetime.now()
1051
296
self.checker_callback_tag = None
1052
297
self.checker = None
1055
self.last_checker_status = returncode
1056
self.last_checker_signal = None
1057
if self.last_checker_status == 0:
1058
logger.info("Checker for %(name)s succeeded",
1062
logger.info("Checker for %(name)s failed", vars(self))
1064
self.last_checker_status = -1
1065
self.last_checker_signal = -returncode
1066
logger.warning("Checker for %(name)s crashed?",
298
if os.WIFEXITED(condition) \
299
and (os.WEXITSTATUS(condition) == 0):
300
logger.debug(u"Checker for %(name)s succeeded",
302
self.last_checked_ok = now
303
gobject.source_remove(self.stop_initiator_tag)
304
self.stop_initiator_tag = gobject.timeout_add\
305
(self._timeout_milliseconds,
307
elif not os.WIFEXITED(condition):
308
logger.warning(u"Checker for %(name)s crashed?",
1070
def checked_ok(self):
1071
"""Assert that the client has been seen, alive and well."""
1072
self.last_checked_ok = datetime.datetime.utcnow()
1073
self.last_checker_status = 0
1074
self.last_checker_signal = None
1077
def bump_timeout(self, timeout=None):
1078
"""Bump up the timeout for this client."""
1080
timeout = self.timeout
1081
if self.disable_initiator_tag is not None:
1082
GLib.source_remove(self.disable_initiator_tag)
1083
self.disable_initiator_tag = None
1084
if getattr(self, "enabled", False):
1085
self.disable_initiator_tag = GLib.timeout_add(
1086
int(timeout.total_seconds() * 1000), self.disable)
1087
self.expires = datetime.datetime.utcnow() + timeout
1089
def need_approval(self):
1090
self.last_approval_request = datetime.datetime.utcnow()
311
logger.debug(u"Checker for %(name)s failed",
1092
313
def start_checker(self):
1093
314
"""Start a new checker subprocess if one is not running.
1095
315
If a checker already exists, leave it running and do
1097
317
# The reason for not killing a running checker is that if we
1098
# did that, and if a checker (for some reason) started running
1099
# slowly and taking more than 'interval' time, then the client
1100
# would inevitably timeout, since no checker would get a
1101
# chance to run to completion. If we instead leave running
318
# did that, then if a checker (for some reason) started
319
# running slowly and taking more than 'interval' time, the
320
# client would inevitably timeout, since no checker would get
321
# a chance to run to completion. If we instead leave running
1102
322
# checkers alone, the checker would have to take more time
1103
# than 'timeout' for the client to be disabled, which is as it
1106
if self.checker is not None and not self.checker.is_alive():
1107
logger.warning("Checker was not alive; joining")
1110
# Start a new checker if needed
323
# than 'timeout' for the client to be declared invalid, which
324
# is as it should be.
1111
325
if self.checker is None:
1112
# Escape attributes for the shell
1114
attr: re.escape(str(getattr(self, attr)))
1115
for attr in self.runtime_expansions}
1117
command = self.checker_command % escaped_attrs
1118
except TypeError as error:
1119
logger.error('Could not format string "%s"',
1120
self.checker_command,
1122
return True # Try again later
1123
self.current_checker_command = command
1124
logger.info("Starting checker %r for %s", command,
1126
# We don't need to redirect stdout and stderr, since
1127
# in normal mode, that is already done by daemon(),
1128
# and in debug mode we don't want to. (Stdin is
1129
# always replaced by /dev/null.)
1130
# The exception is when not debugging but nevertheless
1131
# running in the foreground; use the previously
1133
popen_args = {"close_fds": True,
1136
if (not self.server_settings["debug"]
1137
and self.server_settings["foreground"]):
1138
popen_args.update({"stdout": wnull,
1140
pipe = multiprocessing.Pipe(duplex=False)
1141
self.checker = multiprocessing.Process(
1143
args=(pipe[1], subprocess.call, command),
1145
self.checker.start()
1146
self.checker_callback_tag = GLib.io_add_watch(
1147
pipe[0].fileno(), GLib.IO_IN,
1148
self.checker_callback, pipe[0], command)
1149
# Re-run this periodically if run by GLib.timeout_add
327
# In case check_command has exactly one % operator
328
command = self.check_command % self.fqdn
330
# Escape attributes for the shell
331
escaped_attrs = dict((key, re.escape(str(val)))
333
vars(self).iteritems())
335
command = self.check_command % escaped_attrs
336
except TypeError, error:
337
logger.error(u'Could not format string "%s":'
338
u' %s', self.check_command, error)
339
return True # Try again later
341
logger.debug(u"Starting checker %r for %s",
343
self.checker = subprocess.Popen(command,
346
self.checker_callback_tag = gobject.child_watch_add\
348
self.checker_callback)
349
except subprocess.OSError, error:
350
logger.error(u"Failed to start subprocess: %s",
352
# Re-run this periodically if run by gobject.timeout_add
1152
354
def stop_checker(self):
1153
355
"""Force the checker process, if any, to stop."""
1154
356
if self.checker_callback_tag:
1155
GLib.source_remove(self.checker_callback_tag)
357
gobject.source_remove(self.checker_callback_tag)
1156
358
self.checker_callback_tag = None
1157
359
if getattr(self, "checker", None) is None:
1159
361
logger.debug("Stopping checker for %(name)s", vars(self))
1160
self.checker.terminate()
363
os.kill(self.checker.pid, signal.SIGTERM)
365
#if self.checker.poll() is None:
366
# os.kill(self.checker.pid, signal.SIGKILL)
367
except OSError, error:
368
if error.errno != errno.ESRCH: # No such process
1161
370
self.checker = None
1164
def dbus_service_property(dbus_interface,
1168
"""Decorators for marking methods of a DBusObjectWithProperties to
1169
become properties on the D-Bus.
1171
The decorated method will be called with no arguments by "Get"
1172
and with one argument by "Set".
1174
The parameters, where they are supported, are the same as
1175
dbus.service.method, except there is only "signature", since the
1176
type from Get() and the type sent to Set() is the same.
1178
# Encoding deeply encoded byte arrays is not supported yet by the
1179
# "Set" method, so we fail early here:
1180
if byte_arrays and signature != "ay":
1181
raise ValueError("Byte arrays not supported for non-'ay'"
1182
" signature {!r}".format(signature))
1184
def decorator(func):
1185
func._dbus_is_property = True
1186
func._dbus_interface = dbus_interface
1187
func._dbus_signature = signature
1188
func._dbus_access = access
1189
func._dbus_name = func.__name__
1190
if func._dbus_name.endswith("_dbus_property"):
1191
func._dbus_name = func._dbus_name[:-14]
1192
func._dbus_get_args_options = {'byte_arrays': byte_arrays}
1198
def dbus_interface_annotations(dbus_interface):
1199
"""Decorator for marking functions returning interface annotations
1203
@dbus_interface_annotations("org.example.Interface")
1204
def _foo(self): # Function name does not matter
1205
return {"org.freedesktop.DBus.Deprecated": "true",
1206
"org.freedesktop.DBus.Property.EmitsChangedSignal":
1210
def decorator(func):
1211
func._dbus_is_interface = True
1212
func._dbus_interface = dbus_interface
1213
func._dbus_name = dbus_interface
1219
def dbus_annotations(annotations):
1220
"""Decorator to annotate D-Bus methods, signals or properties
1223
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true",
1224
"org.freedesktop.DBus.Property."
1225
"EmitsChangedSignal": "false"})
1226
@dbus_service_property("org.example.Interface", signature="b",
1228
def Property_dbus_property(self):
1229
return dbus.Boolean(False)
1231
See also the DBusObjectWithAnnotations class.
1234
def decorator(func):
1235
func._dbus_annotations = annotations
1241
class DBusPropertyException(dbus.exceptions.DBusException):
1242
"""A base class for D-Bus property-related exceptions
1247
class DBusPropertyAccessException(DBusPropertyException):
1248
"""A property's access permissions disallows an operation.
1253
class DBusPropertyNotFound(DBusPropertyException):
1254
"""An attempt was made to access a non-existing property.
1259
class DBusObjectWithAnnotations(dbus.service.Object):
1260
"""A D-Bus object with annotations.
1262
Classes inheriting from this can use the dbus_annotations
1263
decorator to add annotations to methods or signals.
1267
def _is_dbus_thing(thing):
1268
"""Returns a function testing if an attribute is a D-Bus thing
1270
If called like _is_dbus_thing("method") it returns a function
1271
suitable for use as predicate to inspect.getmembers().
1273
return lambda obj: getattr(obj, "_dbus_is_{}".format(thing),
1276
def _get_all_dbus_things(self, thing):
1277
"""Returns a generator of (name, attribute) pairs
1279
return ((getattr(athing.__get__(self), "_dbus_name", name),
1280
athing.__get__(self))
1281
for cls in self.__class__.__mro__
1283
inspect.getmembers(cls, self._is_dbus_thing(thing)))
1285
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1287
path_keyword='object_path',
1288
connection_keyword='connection')
1289
def Introspect(self, object_path, connection):
1290
"""Overloading of standard D-Bus method.
1292
Inserts annotation tags on methods and signals.
1294
xmlstring = dbus.service.Object.Introspect(self, object_path,
1297
document = xml.dom.minidom.parseString(xmlstring)
1299
for if_tag in document.getElementsByTagName("interface"):
1300
# Add annotation tags
1301
for typ in ("method", "signal"):
1302
for tag in if_tag.getElementsByTagName(typ):
1304
for name, prop in (self.
1305
_get_all_dbus_things(typ)):
1306
if (name == tag.getAttribute("name")
1307
and prop._dbus_interface
1308
== if_tag.getAttribute("name")):
1309
annots.update(getattr(
1310
prop, "_dbus_annotations", {}))
1311
for name, value in annots.items():
1312
ann_tag = document.createElement(
1314
ann_tag.setAttribute("name", name)
1315
ann_tag.setAttribute("value", value)
1316
tag.appendChild(ann_tag)
1317
# Add interface annotation tags
1318
for annotation, value in dict(
1319
itertools.chain.from_iterable(
1320
annotations().items()
1321
for name, annotations
1322
in self._get_all_dbus_things("interface")
1323
if name == if_tag.getAttribute("name")
1325
ann_tag = document.createElement("annotation")
1326
ann_tag.setAttribute("name", annotation)
1327
ann_tag.setAttribute("value", value)
1328
if_tag.appendChild(ann_tag)
1329
# Fix argument name for the Introspect method itself
1330
if (if_tag.getAttribute("name")
1331
== dbus.INTROSPECTABLE_IFACE):
1332
for cn in if_tag.getElementsByTagName("method"):
1333
if cn.getAttribute("name") == "Introspect":
1334
for arg in cn.getElementsByTagName("arg"):
1335
if (arg.getAttribute("direction")
1337
arg.setAttribute("name",
1339
xmlstring = document.toxml("utf-8")
1341
except (AttributeError, xml.dom.DOMException,
1342
xml.parsers.expat.ExpatError) as error:
1343
logger.error("Failed to override Introspection method",
1348
class DBusObjectWithProperties(DBusObjectWithAnnotations):
1349
"""A D-Bus object with properties.
1351
Classes inheriting from this can use the dbus_service_property
1352
decorator to expose methods as D-Bus properties. It exposes the
1353
standard Get(), Set(), and GetAll() methods on the D-Bus.
1356
def _get_dbus_property(self, interface_name, property_name):
1357
"""Returns a bound method if one exists which is a D-Bus
1358
property with the specified name and interface.
1360
for cls in self.__class__.__mro__:
1361
for name, value in inspect.getmembers(
1362
cls, self._is_dbus_thing("property")):
1363
if (value._dbus_name == property_name
1364
and value._dbus_interface == interface_name):
1365
return value.__get__(self)
1368
raise DBusPropertyNotFound("{}:{}.{}".format(
1369
self.dbus_object_path, interface_name, property_name))
1372
def _get_all_interface_names(cls):
1373
"""Get a sequence of all interfaces supported by an object"""
1374
return (name for name in set(getattr(getattr(x, attr),
1375
"_dbus_interface", None)
1376
for x in (inspect.getmro(cls))
1378
if name is not None)
1380
@dbus.service.method(dbus.PROPERTIES_IFACE,
1383
def Get(self, interface_name, property_name):
1384
"""Standard D-Bus property Get() method, see D-Bus standard.
1386
prop = self._get_dbus_property(interface_name, property_name)
1387
if prop._dbus_access == "write":
1388
raise DBusPropertyAccessException(property_name)
1390
if not hasattr(value, "variant_level"):
1392
return type(value)(value, variant_level=value.variant_level+1)
1394
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
1395
def Set(self, interface_name, property_name, value):
1396
"""Standard D-Bus property Set() method, see D-Bus standard.
1398
prop = self._get_dbus_property(interface_name, property_name)
1399
if prop._dbus_access == "read":
1400
raise DBusPropertyAccessException(property_name)
1401
if prop._dbus_get_args_options["byte_arrays"]:
1402
# The byte_arrays option is not supported yet on
1403
# signatures other than "ay".
1404
if prop._dbus_signature != "ay":
1405
raise ValueError("Byte arrays not supported for non-"
1406
"'ay' signature {!r}"
1407
.format(prop._dbus_signature))
1408
value = dbus.ByteArray(b''.join(chr(byte)
1412
@dbus.service.method(dbus.PROPERTIES_IFACE,
1414
out_signature="a{sv}")
1415
def GetAll(self, interface_name):
1416
"""Standard D-Bus property GetAll() method, see D-Bus
1419
Note: Will not include properties with access="write".
1422
for name, prop in self._get_all_dbus_things("property"):
1424
and interface_name != prop._dbus_interface):
1425
# Interface non-empty but did not match
1427
# Ignore write-only properties
1428
if prop._dbus_access == "write":
1431
if not hasattr(value, "variant_level"):
1432
properties[name] = value
1434
properties[name] = type(value)(
1435
value, variant_level=value.variant_level + 1)
1436
return dbus.Dictionary(properties, signature="sv")
1438
@dbus.service.signal(dbus.PROPERTIES_IFACE, signature="sa{sv}as")
1439
def PropertiesChanged(self, interface_name, changed_properties,
1440
invalidated_properties):
1441
"""Standard D-Bus PropertiesChanged() signal, see D-Bus
1446
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1448
path_keyword='object_path',
1449
connection_keyword='connection')
1450
def Introspect(self, object_path, connection):
1451
"""Overloading of standard D-Bus method.
1453
Inserts property tags and interface annotation tags.
1455
xmlstring = DBusObjectWithAnnotations.Introspect(self,
1459
document = xml.dom.minidom.parseString(xmlstring)
1461
def make_tag(document, name, prop):
1462
e = document.createElement("property")
1463
e.setAttribute("name", name)
1464
e.setAttribute("type", prop._dbus_signature)
1465
e.setAttribute("access", prop._dbus_access)
1468
for if_tag in document.getElementsByTagName("interface"):
1470
for tag in (make_tag(document, name, prop)
1472
in self._get_all_dbus_things("property")
1473
if prop._dbus_interface
1474
== if_tag.getAttribute("name")):
1475
if_tag.appendChild(tag)
1476
# Add annotation tags for properties
1477
for tag in if_tag.getElementsByTagName("property"):
1479
for name, prop in self._get_all_dbus_things(
1481
if (name == tag.getAttribute("name")
1482
and prop._dbus_interface
1483
== if_tag.getAttribute("name")):
1484
annots.update(getattr(
1485
prop, "_dbus_annotations", {}))
1486
for name, value in annots.items():
1487
ann_tag = document.createElement(
1489
ann_tag.setAttribute("name", name)
1490
ann_tag.setAttribute("value", value)
1491
tag.appendChild(ann_tag)
1492
# Add the names to the return values for the
1493
# "org.freedesktop.DBus.Properties" methods
1494
if (if_tag.getAttribute("name")
1495
== "org.freedesktop.DBus.Properties"):
1496
for cn in if_tag.getElementsByTagName("method"):
1497
if cn.getAttribute("name") == "Get":
1498
for arg in cn.getElementsByTagName("arg"):
1499
if (arg.getAttribute("direction")
1501
arg.setAttribute("name", "value")
1502
elif cn.getAttribute("name") == "GetAll":
1503
for arg in cn.getElementsByTagName("arg"):
1504
if (arg.getAttribute("direction")
1506
arg.setAttribute("name", "props")
1507
xmlstring = document.toxml("utf-8")
1509
except (AttributeError, xml.dom.DOMException,
1510
xml.parsers.expat.ExpatError) as error:
1511
logger.error("Failed to override Introspection method",
1517
dbus.OBJECT_MANAGER_IFACE
1518
except AttributeError:
1519
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
1522
class DBusObjectWithObjectManager(DBusObjectWithAnnotations):
1523
"""A D-Bus object with an ObjectManager.
1525
Classes inheriting from this exposes the standard
1526
GetManagedObjects call and the InterfacesAdded and
1527
InterfacesRemoved signals on the standard
1528
"org.freedesktop.DBus.ObjectManager" interface.
1530
Note: No signals are sent automatically; they must be sent
1533
@dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
1534
out_signature="a{oa{sa{sv}}}")
1535
def GetManagedObjects(self):
1536
"""This function must be overridden"""
1537
raise NotImplementedError()
1539
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE,
1540
signature="oa{sa{sv}}")
1541
def InterfacesAdded(self, object_path, interfaces_and_properties):
1544
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature="oas")
1545
def InterfacesRemoved(self, object_path, interfaces):
1548
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1550
path_keyword='object_path',
1551
connection_keyword='connection')
1552
def Introspect(self, object_path, connection):
1553
"""Overloading of standard D-Bus method.
1555
Override return argument name of GetManagedObjects to be
1556
"objpath_interfaces_and_properties"
1558
xmlstring = DBusObjectWithAnnotations.Introspect(self,
1562
document = xml.dom.minidom.parseString(xmlstring)
1564
for if_tag in document.getElementsByTagName("interface"):
1565
# Fix argument name for the GetManagedObjects method
1566
if (if_tag.getAttribute("name")
1567
== dbus.OBJECT_MANAGER_IFACE):
1568
for cn in if_tag.getElementsByTagName("method"):
1569
if (cn.getAttribute("name")
1570
== "GetManagedObjects"):
1571
for arg in cn.getElementsByTagName("arg"):
1572
if (arg.getAttribute("direction")
1576
"objpath_interfaces"
1578
xmlstring = document.toxml("utf-8")
1580
except (AttributeError, xml.dom.DOMException,
1581
xml.parsers.expat.ExpatError) as error:
1582
logger.error("Failed to override Introspection method",
1587
def datetime_to_dbus(dt, variant_level=0):
1588
"""Convert a UTC datetime.datetime() to a D-Bus type."""
1590
return dbus.String("", variant_level=variant_level)
1591
return dbus.String(dt.isoformat(), variant_level=variant_level)
1594
def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
1595
"""A class decorator; applied to a subclass of
1596
dbus.service.Object, it will add alternate D-Bus attributes with
1597
interface names according to the "alt_interface_names" mapping.
1600
@alternate_dbus_interfaces({"org.example.Interface":
1601
"net.example.AlternateInterface"})
1602
class SampleDBusObject(dbus.service.Object):
1603
@dbus.service.method("org.example.Interface")
1604
def SampleDBusMethod():
1607
The above "SampleDBusMethod" on "SampleDBusObject" will be
1608
reachable via two interfaces: "org.example.Interface" and
1609
"net.example.AlternateInterface", the latter of which will have
1610
its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
1611
"true", unless "deprecate" is passed with a False value.
1613
This works for methods and signals, and also for D-Bus properties
1614
(from DBusObjectWithProperties) and interfaces (from the
1615
dbus_interface_annotations decorator).
1619
for orig_interface_name, alt_interface_name in (
1620
alt_interface_names.items()):
1622
interface_names = set()
1623
# Go though all attributes of the class
1624
for attrname, attribute in inspect.getmembers(cls):
1625
# Ignore non-D-Bus attributes, and D-Bus attributes
1626
# with the wrong interface name
1627
if (not hasattr(attribute, "_dbus_interface")
1628
or not attribute._dbus_interface.startswith(
1629
orig_interface_name)):
1631
# Create an alternate D-Bus interface name based on
1633
alt_interface = attribute._dbus_interface.replace(
1634
orig_interface_name, alt_interface_name)
1635
interface_names.add(alt_interface)
1636
# Is this a D-Bus signal?
1637
if getattr(attribute, "_dbus_is_signal", False):
1638
# Extract the original non-method undecorated
1639
# function by black magic
1640
if sys.version_info.major == 2:
1641
nonmethod_func = (dict(
1642
zip(attribute.func_code.co_freevars,
1643
attribute.__closure__))
1644
["func"].cell_contents)
1646
nonmethod_func = (dict(
1647
zip(attribute.__code__.co_freevars,
1648
attribute.__closure__))
1649
["func"].cell_contents)
1650
# Create a new, but exactly alike, function
1651
# object, and decorate it to be a new D-Bus signal
1652
# with the alternate D-Bus interface name
1653
new_function = copy_function(nonmethod_func)
1654
new_function = (dbus.service.signal(
1656
attribute._dbus_signature)(new_function))
1657
# Copy annotations, if any
1659
new_function._dbus_annotations = dict(
1660
attribute._dbus_annotations)
1661
except AttributeError:
1664
# Define a creator of a function to call both the
1665
# original and alternate functions, so both the
1666
# original and alternate signals gets sent when
1667
# the function is called
1668
def fixscope(func1, func2):
1669
"""This function is a scope container to pass
1670
func1 and func2 to the "call_both" function
1671
outside of its arguments"""
1673
@functools.wraps(func2)
1674
def call_both(*args, **kwargs):
1675
"""This function will emit two D-Bus
1676
signals by calling func1 and func2"""
1677
func1(*args, **kwargs)
1678
func2(*args, **kwargs)
1679
# Make wrapper function look like a D-Bus
1681
for name, attr in inspect.getmembers(func2):
1682
if name.startswith("_dbus_"):
1683
setattr(call_both, name, attr)
1686
# Create the "call_both" function and add it to
1688
attr[attrname] = fixscope(attribute, new_function)
1689
# Is this a D-Bus method?
1690
elif getattr(attribute, "_dbus_is_method", False):
1691
# Create a new, but exactly alike, function
1692
# object. Decorate it to be a new D-Bus method
1693
# with the alternate D-Bus interface name. Add it
1696
dbus.service.method(
1698
attribute._dbus_in_signature,
1699
attribute._dbus_out_signature)
1700
(copy_function(attribute)))
1701
# Copy annotations, if any
1703
attr[attrname]._dbus_annotations = dict(
1704
attribute._dbus_annotations)
1705
except AttributeError:
1707
# Is this a D-Bus property?
1708
elif getattr(attribute, "_dbus_is_property", False):
1709
# Create a new, but exactly alike, function
1710
# object, and decorate it to be a new D-Bus
1711
# property with the alternate D-Bus interface
1712
# name. Add it to the class.
1713
attr[attrname] = (dbus_service_property(
1714
alt_interface, attribute._dbus_signature,
1715
attribute._dbus_access,
1716
attribute._dbus_get_args_options
1718
(copy_function(attribute)))
1719
# Copy annotations, if any
1721
attr[attrname]._dbus_annotations = dict(
1722
attribute._dbus_annotations)
1723
except AttributeError:
1725
# Is this a D-Bus interface?
1726
elif getattr(attribute, "_dbus_is_interface", False):
1727
# Create a new, but exactly alike, function
1728
# object. Decorate it to be a new D-Bus interface
1729
# with the alternate D-Bus interface name. Add it
1732
dbus_interface_annotations(alt_interface)
1733
(copy_function(attribute)))
1735
# Deprecate all alternate interfaces
1736
iname = "_AlternateDBusNames_interface_annotation{}"
1737
for interface_name in interface_names:
1739
@dbus_interface_annotations(interface_name)
1741
return {"org.freedesktop.DBus.Deprecated":
1743
# Find an unused name
1744
for aname in (iname.format(i)
1745
for i in itertools.count()):
1746
if aname not in attr:
1750
# Replace the class with a new subclass of it with
1751
# methods, signals, etc. as created above.
1752
if sys.version_info.major == 2:
1753
cls = type(b"{}Alternate".format(cls.__name__),
1756
cls = type("{}Alternate".format(cls.__name__),
1763
@alternate_dbus_interfaces({"se.recompile.Mandos":
1764
"se.bsnet.fukt.Mandos"})
1765
class ClientDBus(Client, DBusObjectWithProperties):
1766
"""A Client class using D-Bus
1769
dbus_object_path: dbus.ObjectPath
1770
bus: dbus.SystemBus()
1773
runtime_expansions = (Client.runtime_expansions
1774
+ ("dbus_object_path", ))
1776
_interface = "se.recompile.Mandos.Client"
1778
# dbus.service.Object doesn't use super(), so we can't either.
1780
def __init__(self, bus=None, *args, **kwargs):
1782
Client.__init__(self, *args, **kwargs)
1783
# Only now, when this client is initialized, can it show up on
1785
client_object_name = str(self.name).translate(
1786
{ord("."): ord("_"),
1787
ord("-"): ord("_")})
1788
self.dbus_object_path = dbus.ObjectPath(
1789
"/clients/" + client_object_name)
1790
DBusObjectWithProperties.__init__(self, self.bus,
1791
self.dbus_object_path)
1793
def notifychangeproperty(transform_func, dbus_name,
1794
type_func=lambda x: x,
1796
invalidate_only=False,
1797
_interface=_interface):
1798
""" Modify a variable so that it's a property which announces
1799
its changes to DBus.
1801
transform_fun: Function that takes a value and a variant_level
1802
and transforms it to a D-Bus type.
1803
dbus_name: D-Bus name of the variable
1804
type_func: Function that transform the value before sending it
1805
to the D-Bus. Default: no transform
1806
variant_level: D-Bus variant level. Default: 1
1808
attrname = "_{}".format(dbus_name)
1810
def setter(self, value):
1811
if hasattr(self, "dbus_object_path"):
1812
if (not hasattr(self, attrname) or
1813
type_func(getattr(self, attrname, None))
1814
!= type_func(value)):
1816
self.PropertiesChanged(
1817
_interface, dbus.Dictionary(),
1818
dbus.Array((dbus_name, )))
1820
dbus_value = transform_func(
1822
variant_level=variant_level)
1823
self.PropertyChanged(dbus.String(dbus_name),
1825
self.PropertiesChanged(
1827
dbus.Dictionary({dbus.String(dbus_name):
1830
setattr(self, attrname, value)
1832
return property(lambda self: getattr(self, attrname), setter)
1834
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1835
approvals_pending = notifychangeproperty(dbus.Boolean,
1838
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1839
last_enabled = notifychangeproperty(datetime_to_dbus,
1841
checker = notifychangeproperty(
1842
dbus.Boolean, "CheckerRunning",
1843
type_func=lambda checker: checker is not None)
1844
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1846
last_checker_status = notifychangeproperty(dbus.Int16,
1847
"LastCheckerStatus")
1848
last_approval_request = notifychangeproperty(
1849
datetime_to_dbus, "LastApprovalRequest")
1850
approved_by_default = notifychangeproperty(dbus.Boolean,
1851
"ApprovedByDefault")
1852
approval_delay = notifychangeproperty(
1853
dbus.UInt64, "ApprovalDelay",
1854
type_func=lambda td: td.total_seconds() * 1000)
1855
approval_duration = notifychangeproperty(
1856
dbus.UInt64, "ApprovalDuration",
1857
type_func=lambda td: td.total_seconds() * 1000)
1858
host = notifychangeproperty(dbus.String, "Host")
1859
timeout = notifychangeproperty(
1860
dbus.UInt64, "Timeout",
1861
type_func=lambda td: td.total_seconds() * 1000)
1862
extended_timeout = notifychangeproperty(
1863
dbus.UInt64, "ExtendedTimeout",
1864
type_func=lambda td: td.total_seconds() * 1000)
1865
interval = notifychangeproperty(
1866
dbus.UInt64, "Interval",
1867
type_func=lambda td: td.total_seconds() * 1000)
1868
checker_command = notifychangeproperty(dbus.String, "Checker")
1869
secret = notifychangeproperty(dbus.ByteArray, "Secret",
1870
invalidate_only=True)
1872
del notifychangeproperty
1874
def __del__(self, *args, **kwargs):
1876
self.remove_from_connection()
1879
if hasattr(DBusObjectWithProperties, "__del__"):
1880
DBusObjectWithProperties.__del__(self, *args, **kwargs)
1881
Client.__del__(self, *args, **kwargs)
1883
def checker_callback(self, source, condition,
1884
connection, command, *args, **kwargs):
1885
ret = Client.checker_callback(self, source, condition,
1886
connection, command, *args,
1888
exitstatus = self.last_checker_status
1891
self.CheckerCompleted(dbus.Int16(exitstatus),
1892
# This is specific to GNU libC
1893
dbus.Int64(exitstatus << 8),
1894
dbus.String(command))
1897
self.CheckerCompleted(dbus.Int16(-1),
1899
# This is specific to GNU libC
1901
| self.last_checker_signal),
1902
dbus.String(command))
1905
def start_checker(self, *args, **kwargs):
1906
old_checker_pid = getattr(self.checker, "pid", None)
1907
r = Client.start_checker(self, *args, **kwargs)
1908
# Only if new checker process was started
1909
if (self.checker is not None
1910
and old_checker_pid != self.checker.pid):
1912
self.CheckerStarted(self.current_checker_command)
1915
def _reset_approved(self):
1916
self.approved = None
1919
def approve(self, value=True):
1920
self.approved = value
1921
GLib.timeout_add(int(self.approval_duration.total_seconds()
1922
* 1000), self._reset_approved)
1923
self.send_changedstate()
1925
# D-Bus methods, signals & properties
1931
# CheckerCompleted - signal
1932
@dbus.service.signal(_interface, signature="nxs")
1933
def CheckerCompleted(self, exitcode, waitstatus, command):
1937
# CheckerStarted - signal
1938
@dbus.service.signal(_interface, signature="s")
1939
def CheckerStarted(self, command):
1943
# PropertyChanged - signal
1944
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1945
@dbus.service.signal(_interface, signature="sv")
1946
def PropertyChanged(self, property, value):
1950
# GotSecret - signal
1951
@dbus.service.signal(_interface)
1952
def GotSecret(self):
1954
Is sent after a successful transfer of secret from the Mandos
1955
server to mandos-client
1960
@dbus.service.signal(_interface, signature="s")
1961
def Rejected(self, reason):
1965
# NeedApproval - signal
1966
@dbus.service.signal(_interface, signature="tb")
1967
def NeedApproval(self, timeout, default):
1969
return self.need_approval()
1974
@dbus.service.method(_interface, in_signature="b")
1975
def Approve(self, value):
1978
# CheckedOK - method
1979
@dbus.service.method(_interface)
1980
def CheckedOK(self):
1984
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1985
@dbus.service.method(_interface)
1990
# StartChecker - method
1991
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1992
@dbus.service.method(_interface)
1993
def StartChecker(self):
1995
self.start_checker()
1998
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1999
@dbus.service.method(_interface)
2004
# StopChecker - method
2005
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
2006
@dbus.service.method(_interface)
2007
def StopChecker(self):
2012
# ApprovalPending - property
2013
@dbus_service_property(_interface, signature="b", access="read")
2014
def ApprovalPending_dbus_property(self):
2015
return dbus.Boolean(bool(self.approvals_pending))
2017
# ApprovedByDefault - property
2018
@dbus_service_property(_interface,
2021
def ApprovedByDefault_dbus_property(self, value=None):
2022
if value is None: # get
2023
return dbus.Boolean(self.approved_by_default)
2024
self.approved_by_default = bool(value)
2026
# ApprovalDelay - property
2027
@dbus_service_property(_interface,
2030
def ApprovalDelay_dbus_property(self, value=None):
2031
if value is None: # get
2032
return dbus.UInt64(self.approval_delay.total_seconds()
2034
self.approval_delay = datetime.timedelta(0, 0, 0, value)
2036
# ApprovalDuration - property
2037
@dbus_service_property(_interface,
2040
def ApprovalDuration_dbus_property(self, value=None):
2041
if value is None: # get
2042
return dbus.UInt64(self.approval_duration.total_seconds()
2044
self.approval_duration = datetime.timedelta(0, 0, 0, value)
2048
{"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2049
@dbus_service_property(_interface, signature="s", access="read")
2050
def Name_dbus_property(self):
2051
return dbus.String(self.name)
2055
{"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2056
@dbus_service_property(_interface, signature="s", access="read")
2057
def KeyID_dbus_property(self):
2058
return dbus.String(self.key_id)
2060
# Fingerprint - property
2062
{"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2063
@dbus_service_property(_interface, signature="s", access="read")
2064
def Fingerprint_dbus_property(self):
2065
return dbus.String(self.fingerprint)
2068
@dbus_service_property(_interface,
2071
def Host_dbus_property(self, value=None):
2072
if value is None: # get
2073
return dbus.String(self.host)
2074
self.host = str(value)
2076
# Created - property
2078
{"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2079
@dbus_service_property(_interface, signature="s", access="read")
2080
def Created_dbus_property(self):
2081
return datetime_to_dbus(self.created)
2083
# LastEnabled - property
2084
@dbus_service_property(_interface, signature="s", access="read")
2085
def LastEnabled_dbus_property(self):
2086
return datetime_to_dbus(self.last_enabled)
2088
# Enabled - property
2089
@dbus_service_property(_interface,
2092
def Enabled_dbus_property(self, value=None):
2093
if value is None: # get
2094
return dbus.Boolean(self.enabled)
2100
# LastCheckedOK - property
2101
@dbus_service_property(_interface,
2104
def LastCheckedOK_dbus_property(self, value=None):
2105
if value is not None:
2108
return datetime_to_dbus(self.last_checked_ok)
2110
# LastCheckerStatus - property
2111
@dbus_service_property(_interface, signature="n", access="read")
2112
def LastCheckerStatus_dbus_property(self):
2113
return dbus.Int16(self.last_checker_status)
2115
# Expires - property
2116
@dbus_service_property(_interface, signature="s", access="read")
2117
def Expires_dbus_property(self):
2118
return datetime_to_dbus(self.expires)
2120
# LastApprovalRequest - property
2121
@dbus_service_property(_interface, signature="s", access="read")
2122
def LastApprovalRequest_dbus_property(self):
2123
return datetime_to_dbus(self.last_approval_request)
2125
# Timeout - property
2126
@dbus_service_property(_interface,
2129
def Timeout_dbus_property(self, value=None):
2130
if value is None: # get
2131
return dbus.UInt64(self.timeout.total_seconds() * 1000)
2132
old_timeout = self.timeout
2133
self.timeout = datetime.timedelta(0, 0, 0, value)
2134
# Reschedule disabling
2136
now = datetime.datetime.utcnow()
2137
self.expires += self.timeout - old_timeout
2138
if self.expires <= now:
2139
# The timeout has passed
2142
if (getattr(self, "disable_initiator_tag", None)
2145
GLib.source_remove(self.disable_initiator_tag)
2146
self.disable_initiator_tag = GLib.timeout_add(
2147
int((self.expires - now).total_seconds() * 1000),
2150
# ExtendedTimeout - property
2151
@dbus_service_property(_interface,
2154
def ExtendedTimeout_dbus_property(self, value=None):
2155
if value is None: # get
2156
return dbus.UInt64(self.extended_timeout.total_seconds()
2158
self.extended_timeout = datetime.timedelta(0, 0, 0, value)
2160
# Interval - property
2161
@dbus_service_property(_interface,
2164
def Interval_dbus_property(self, value=None):
2165
if value is None: # get
2166
return dbus.UInt64(self.interval.total_seconds() * 1000)
2167
self.interval = datetime.timedelta(0, 0, 0, value)
2168
if getattr(self, "checker_initiator_tag", None) is None:
2171
# Reschedule checker run
2172
GLib.source_remove(self.checker_initiator_tag)
2173
self.checker_initiator_tag = GLib.timeout_add(
2174
value, self.start_checker)
2175
self.start_checker() # Start one now, too
2177
# Checker - property
2178
@dbus_service_property(_interface,
2181
def Checker_dbus_property(self, value=None):
2182
if value is None: # get
2183
return dbus.String(self.checker_command)
2184
self.checker_command = str(value)
2186
# CheckerRunning - property
2187
@dbus_service_property(_interface,
2190
def CheckerRunning_dbus_property(self, value=None):
2191
if value is None: # get
2192
return dbus.Boolean(self.checker is not None)
2194
self.start_checker()
2198
# ObjectPath - property
2200
{"org.freedesktop.DBus.Property.EmitsChangedSignal": "const",
2201
"org.freedesktop.DBus.Deprecated": "true"})
2202
@dbus_service_property(_interface, signature="o", access="read")
2203
def ObjectPath_dbus_property(self):
2204
return self.dbus_object_path # is already a dbus.ObjectPath
2208
{"org.freedesktop.DBus.Property.EmitsChangedSignal":
2210
@dbus_service_property(_interface,
2214
def Secret_dbus_property(self, value):
2215
self.secret = bytes(value)
2221
def __init__(self, child_pipe, key_id, fpr, address):
2222
self._pipe = child_pipe
2223
self._pipe.send(('init', key_id, fpr, address))
2224
if not self._pipe.recv():
2225
raise KeyError(key_id or fpr)
2227
def __getattribute__(self, name):
2229
return super(ProxyClient, self).__getattribute__(name)
2230
self._pipe.send(('getattr', name))
2231
data = self._pipe.recv()
2232
if data[0] == 'data':
2234
if data[0] == 'function':
2236
def func(*args, **kwargs):
2237
self._pipe.send(('funcall', name, args, kwargs))
2238
return self._pipe.recv()[1]
2242
def __setattr__(self, name, value):
2244
return super(ProxyClient, self).__setattr__(name, value)
2245
self._pipe.send(('setattr', name, value))
2248
class ClientHandler(socketserver.BaseRequestHandler, object):
2249
"""A class to handle client connections.
2251
Instantiated once for each connection to handle it.
371
def still_valid(self):
372
"""Has the timeout not yet passed for this client?"""
373
now = datetime.datetime.now()
374
if self.last_checked_ok is None:
375
return now < (self.created + self.timeout)
377
return now < (self.last_checked_ok + self.timeout)
380
def peer_certificate(session):
381
"Return the peer's OpenPGP certificate as a bytestring"
382
# If not an OpenPGP certificate...
383
if gnutls.library.functions.gnutls_certificate_type_get\
384
(session._c_object) \
385
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
386
# ...do the normal thing
387
return session.peer_certificate
388
list_size = ctypes.c_uint()
389
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
390
(session._c_object, ctypes.byref(list_size))
391
if list_size.value == 0:
394
return ctypes.string_at(cert.data, cert.size)
397
def fingerprint(openpgp):
398
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
399
# New empty GnuTLS certificate
400
crt = gnutls.library.types.gnutls_openpgp_crt_t()
401
gnutls.library.functions.gnutls_openpgp_crt_init\
403
# New GnuTLS "datum" with the OpenPGP public key
404
datum = gnutls.library.types.gnutls_datum_t\
405
(ctypes.cast(ctypes.c_char_p(openpgp),
406
ctypes.POINTER(ctypes.c_ubyte)),
407
ctypes.c_uint(len(openpgp)))
408
# Import the OpenPGP public key into the certificate
409
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
412
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
413
# New buffer for the fingerprint
414
buffer = ctypes.create_string_buffer(20)
415
buffer_length = ctypes.c_size_t()
416
# Get the fingerprint from the certificate into the buffer
417
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
418
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
419
# Deinit the certificate
420
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
421
# Convert the buffer to a Python bytestring
422
fpr = ctypes.string_at(buffer, buffer_length.value)
423
# Convert the bytestring to hexadecimal notation
424
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
428
class tcp_handler(SocketServer.BaseRequestHandler, object):
429
"""A TCP request handler class.
430
Instantiated by IPv6_TCPServer for each request to handle it.
2252
431
Note: This will run in its own forked process."""
2254
433
def handle(self):
2255
with contextlib.closing(self.server.child_pipe) as child_pipe:
2256
logger.info("TCP connection from: %s",
2257
str(self.client_address))
2258
logger.debug("Pipe FD: %d",
2259
self.server.child_pipe.fileno())
2261
session = gnutls.ClientSession(self.request)
2263
# priority = ':'.join(("NONE", "+VERS-TLS1.1",
2264
# "+AES-256-CBC", "+SHA1",
2265
# "+COMP-NULL", "+CTYPE-OPENPGP",
2267
# Use a fallback default, since this MUST be set.
2268
priority = self.server.gnutls_priority
2269
if priority is None:
2271
gnutls.priority_set_direct(session._c_object,
2272
priority.encode("utf-8"),
2275
# Start communication using the Mandos protocol
2276
# Get protocol number
2277
line = self.request.makefile().readline()
2278
logger.debug("Protocol version: %r", line)
2280
if int(line.strip().split()[0]) > 1:
2281
raise RuntimeError(line)
2282
except (ValueError, IndexError, RuntimeError) as error:
2283
logger.error("Unknown protocol version: %s", error)
2286
# Start GnuTLS connection
2289
except gnutls.Error as error:
2290
logger.warning("Handshake failed: %s", error)
2291
# Do not run session.bye() here: the session is not
2292
# established. Just abandon the request.
2294
logger.debug("Handshake succeeded")
2296
approval_required = False
2298
if gnutls.has_rawpk:
2301
key_id = self.key_id(
2302
self.peer_certificate(session))
2303
except (TypeError, gnutls.Error) as error:
2304
logger.warning("Bad certificate: %s", error)
2306
logger.debug("Key ID: %s", key_id)
2311
fpr = self.fingerprint(
2312
self.peer_certificate(session))
2313
except (TypeError, gnutls.Error) as error:
2314
logger.warning("Bad certificate: %s", error)
2316
logger.debug("Fingerprint: %s", fpr)
2319
client = ProxyClient(child_pipe, key_id, fpr,
2320
self.client_address)
2324
if client.approval_delay:
2325
delay = client.approval_delay
2326
client.approvals_pending += 1
2327
approval_required = True
2330
if not client.enabled:
2331
logger.info("Client %s is disabled",
2333
if self.server.use_dbus:
2335
client.Rejected("Disabled")
2338
if client.approved or not client.approval_delay:
2339
# We are approved or approval is disabled
2341
elif client.approved is None:
2342
logger.info("Client %s needs approval",
2344
if self.server.use_dbus:
2346
client.NeedApproval(
2347
client.approval_delay.total_seconds()
2348
* 1000, client.approved_by_default)
2350
logger.warning("Client %s was not approved",
2352
if self.server.use_dbus:
2354
client.Rejected("Denied")
2357
# wait until timeout or approved
2358
time = datetime.datetime.now()
2359
client.changedstate.acquire()
2360
client.changedstate.wait(delay.total_seconds())
2361
client.changedstate.release()
2362
time2 = datetime.datetime.now()
2363
if (time2 - time) >= delay:
2364
if not client.approved_by_default:
2365
logger.warning("Client %s timed out while"
2366
" waiting for approval",
2368
if self.server.use_dbus:
2370
client.Rejected("Approval timed out")
2375
delay -= time2 - time
2378
session.send(client.secret)
2379
except gnutls.Error as error:
2380
logger.warning("gnutls send failed",
2384
logger.info("Sending secret to %s", client.name)
2385
# bump the timeout using extended_timeout
2386
client.bump_timeout(client.extended_timeout)
2387
if self.server.use_dbus:
2392
if approval_required:
2393
client.approvals_pending -= 1
2396
except gnutls.Error as error:
2397
logger.warning("GnuTLS bye failed",
2401
def peer_certificate(session):
2402
"Return the peer's certificate as a bytestring"
2404
cert_type = gnutls.certificate_type_get2(session._c_object,
2406
except AttributeError:
2407
cert_type = gnutls.certificate_type_get(session._c_object)
2408
if gnutls.has_rawpk:
2409
valid_cert_types = frozenset((gnutls.CRT_RAWPK,))
2411
valid_cert_types = frozenset((gnutls.CRT_OPENPGP,))
2412
# If not a valid certificate type...
2413
if cert_type not in valid_cert_types:
2414
logger.info("Cert type %r not in %r", cert_type,
2416
# ...return invalid data
2418
list_size = ctypes.c_uint(1)
2419
cert_list = (gnutls.certificate_get_peers
2420
(session._c_object, ctypes.byref(list_size)))
2421
if not bool(cert_list) and list_size.value != 0:
2422
raise gnutls.Error("error getting peer certificate")
2423
if list_size.value == 0:
2426
return ctypes.string_at(cert.data, cert.size)
2429
def key_id(certificate):
2430
"Convert a certificate bytestring to a hexdigit key ID"
2431
# New GnuTLS "datum" with the public key
2432
datum = gnutls.datum_t(
2433
ctypes.cast(ctypes.c_char_p(certificate),
2434
ctypes.POINTER(ctypes.c_ubyte)),
2435
ctypes.c_uint(len(certificate)))
2436
# XXX all these need to be created in the gnutls "module"
2437
# New empty GnuTLS certificate
2438
pubkey = gnutls.pubkey_t()
2439
gnutls.pubkey_init(ctypes.byref(pubkey))
2440
# Import the raw public key into the certificate
2441
gnutls.pubkey_import(pubkey,
2442
ctypes.byref(datum),
2443
gnutls.X509_FMT_DER)
2444
# New buffer for the key ID
2445
buf = ctypes.create_string_buffer(32)
2446
buf_len = ctypes.c_size_t(len(buf))
2447
# Get the key ID from the raw public key into the buffer
2448
gnutls.pubkey_get_key_id(pubkey,
2449
gnutls.KEYID_USE_SHA256,
2450
ctypes.cast(ctypes.byref(buf),
2451
ctypes.POINTER(ctypes.c_ubyte)),
2452
ctypes.byref(buf_len))
2453
# Deinit the certificate
2454
gnutls.pubkey_deinit(pubkey)
2456
# Convert the buffer to a Python bytestring
2457
key_id = ctypes.string_at(buf, buf_len.value)
2458
# Convert the bytestring to hexadecimal notation
2459
hex_key_id = binascii.hexlify(key_id).upper()
2463
def fingerprint(openpgp):
2464
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
2465
# New GnuTLS "datum" with the OpenPGP public key
2466
datum = gnutls.datum_t(
2467
ctypes.cast(ctypes.c_char_p(openpgp),
2468
ctypes.POINTER(ctypes.c_ubyte)),
2469
ctypes.c_uint(len(openpgp)))
2470
# New empty GnuTLS certificate
2471
crt = gnutls.openpgp_crt_t()
2472
gnutls.openpgp_crt_init(ctypes.byref(crt))
2473
# Import the OpenPGP public key into the certificate
2474
gnutls.openpgp_crt_import(crt, ctypes.byref(datum),
2475
gnutls.OPENPGP_FMT_RAW)
2476
# Verify the self signature in the key
2477
crtverify = ctypes.c_uint()
2478
gnutls.openpgp_crt_verify_self(crt, 0,
2479
ctypes.byref(crtverify))
2480
if crtverify.value != 0:
2481
gnutls.openpgp_crt_deinit(crt)
2482
raise gnutls.CertificateSecurityError(code
2484
# New buffer for the fingerprint
2485
buf = ctypes.create_string_buffer(20)
2486
buf_len = ctypes.c_size_t()
2487
# Get the fingerprint from the certificate into the buffer
2488
gnutls.openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
2489
ctypes.byref(buf_len))
2490
# Deinit the certificate
2491
gnutls.openpgp_crt_deinit(crt)
2492
# Convert the buffer to a Python bytestring
2493
fpr = ctypes.string_at(buf, buf_len.value)
2494
# Convert the bytestring to hexadecimal notation
2495
hex_fpr = binascii.hexlify(fpr).upper()
2499
class MultiprocessingMixIn:
2500
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
2502
def sub_process_main(self, request, address):
2504
self.finish_request(request, address)
2506
self.handle_error(request, address)
2507
self.close_request(request)
2509
def process_request(self, request, address):
2510
"""Start a new process to process the request."""
2511
proc = multiprocessing.Process(target=self.sub_process_main,
2512
args=(request, address))
2517
class MultiprocessingMixInWithPipe(MultiprocessingMixIn):
2518
""" adds a pipe to the MixIn """
2520
def process_request(self, request, client_address):
2521
"""Overrides and wraps the original process_request().
2523
This function creates a new pipe in self.pipe
2525
parent_pipe, self.child_pipe = multiprocessing.Pipe()
2527
proc = MultiprocessingMixIn.process_request(self, request,
2529
self.child_pipe.close()
2530
self.add_pipe(parent_pipe, proc)
2532
def add_pipe(self, parent_pipe, proc):
2533
"""Dummy function; override as necessary"""
2534
raise NotImplementedError()
2537
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
2538
socketserver.TCPServer):
2539
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
434
logger.debug(u"TCP connection from: %s",
435
unicode(self.client_address))
436
session = gnutls.connection.ClientSession\
437
(self.request, gnutls.connection.X509Credentials())
438
# Note: gnutls.connection.X509Credentials is really a generic
439
# GnuTLS certificate credentials object so long as no X.509
440
# keys are added to it. Therefore, we can use it here despite
441
# using OpenPGP certificates.
443
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
444
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
446
priority = "NORMAL" # Fallback default, since this
448
if self.server.settings["priority"]:
449
priority = self.server.settings["priority"]
450
gnutls.library.functions.gnutls_priority_set_direct\
451
(session._c_object, priority, None);
455
except gnutls.errors.GNUTLSError, error:
456
logger.debug(u"Handshake failed: %s", error)
457
# Do not run session.bye() here: the session is not
458
# established. Just abandon the request.
461
fpr = fingerprint(peer_certificate(session))
462
except (TypeError, gnutls.errors.GNUTLSError), error:
463
logger.debug(u"Bad certificate: %s", error)
466
logger.debug(u"Fingerprint: %s", fpr)
468
for c in self.server.clients:
469
if c.fingerprint == fpr:
473
logger.debug(u"Client not found for fingerprint: %s", fpr)
476
# Have to check if client.still_valid(), since it is possible
477
# that the client timed out while establishing the GnuTLS
479
if not client.still_valid():
480
logger.debug(u"Client %(name)s is invalid", vars(client))
484
while sent_size < len(client.secret):
485
sent = session.send(client.secret[sent_size:])
486
logger.debug(u"Sent: %d, remaining: %d",
487
sent, len(client.secret)
488
- (sent_size + sent))
493
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
494
"""IPv6 TCP server. Accepts 'None' as address and/or port.
2542
enabled: Boolean; whether this server is activated yet
2543
interface: None or a network interface name (string)
2544
use_ipv6: Boolean; to use IPv6 or not
496
settings: Server settings
497
clients: Set() of Client objects
2547
def __init__(self, server_address, RequestHandlerClass,
2551
"""If socketfd is set, use that file descriptor instead of
2552
creating a new one with socket.socket().
2554
self.interface = interface
2556
self.address_family = socket.AF_INET6
2557
if socketfd is not None:
2558
# Save the file descriptor
2559
self.socketfd = socketfd
2560
# Save the original socket.socket() function
2561
self.socket_socket = socket.socket
2563
# To implement --socket, we monkey patch socket.socket.
2565
# (When socketserver.TCPServer is a new-style class, we
2566
# could make self.socket into a property instead of monkey
2567
# patching socket.socket.)
2569
# Create a one-time-only replacement for socket.socket()
2570
@functools.wraps(socket.socket)
2571
def socket_wrapper(*args, **kwargs):
2572
# Restore original function so subsequent calls are
2574
socket.socket = self.socket_socket
2575
del self.socket_socket
2576
# This time only, return a new socket object from the
2577
# saved file descriptor.
2578
return socket.fromfd(self.socketfd, *args, **kwargs)
2579
# Replace socket.socket() function with wrapper
2580
socket.socket = socket_wrapper
2581
# The socketserver.TCPServer.__init__ will call
2582
# socket.socket(), which might be our replacement,
2583
# socket_wrapper(), if socketfd was set.
2584
socketserver.TCPServer.__init__(self, server_address,
2585
RequestHandlerClass)
499
address_family = socket.AF_INET6
500
def __init__(self, *args, **kwargs):
501
if "settings" in kwargs:
502
self.settings = kwargs["settings"]
503
del kwargs["settings"]
504
if "clients" in kwargs:
505
self.clients = kwargs["clients"]
506
del kwargs["clients"]
507
return super(type(self), self).__init__(*args, **kwargs)
2587
508
def server_bind(self):
2588
509
"""This overrides the normal server_bind() function
2589
510
to bind to an interface if one was specified, and also NOT to
2590
511
bind to an address or port if they were not specified."""
2591
global SO_BINDTODEVICE
2592
if self.interface is not None:
2593
if SO_BINDTODEVICE is None:
2594
# Fall back to a hard-coded value which seems to be
2596
logger.warning("SO_BINDTODEVICE not found, trying 25")
2597
SO_BINDTODEVICE = 25
512
if self.settings["interface"] != avahi.IF_UNSPEC:
513
# 25 is from /usr/include/asm-i486/socket.h
514
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
2599
self.socket.setsockopt(
2600
socket.SOL_SOCKET, SO_BINDTODEVICE,
2601
(self.interface + "\0").encode("utf-8"))
2602
except socket.error as error:
2603
if error.errno == errno.EPERM:
2604
logger.error("No permission to bind to"
2605
" interface %s", self.interface)
2606
elif error.errno == errno.ENOPROTOOPT:
2607
logger.error("SO_BINDTODEVICE not available;"
2608
" cannot bind to interface %s",
2610
elif error.errno == errno.ENODEV:
2611
logger.error("Interface %s does not exist,"
2612
" cannot bind", self.interface)
516
self.socket.setsockopt(socket.SOL_SOCKET,
518
self.settings["interface"])
519
except socket.error, error:
520
if error[0] == errno.EPERM:
521
logger.warning(u"No permission to"
522
u" bind to interface %s",
523
self.settings["interface"])
2615
526
# Only bind(2) the socket if we really need to.
2616
527
if self.server_address[0] or self.server_address[1]:
2617
if self.server_address[1]:
2618
self.allow_reuse_address = True
2619
528
if not self.server_address[0]:
2620
if self.address_family == socket.AF_INET6:
2621
any_address = "::" # in6addr_any
2623
any_address = "0.0.0.0" # INADDR_ANY
2624
self.server_address = (any_address,
530
self.server_address = (in6addr_any,
2625
531
self.server_address[1])
2626
elif not self.server_address[1]:
2627
self.server_address = (self.server_address[0], 0)
2628
# if self.interface:
2629
# self.server_address = (self.server_address[0],
2634
return socketserver.TCPServer.server_bind(self)
2637
class MandosServer(IPv6_TCPServer):
2641
clients: set of Client objects
2642
gnutls_priority GnuTLS priority string
2643
use_dbus: Boolean; to emit D-Bus signals or not
2645
Assumes a GLib.MainLoop event loop.
2648
def __init__(self, server_address, RequestHandlerClass,
2652
gnutls_priority=None,
2655
self.enabled = False
2656
self.clients = clients
2657
if self.clients is None:
2659
self.use_dbus = use_dbus
2660
self.gnutls_priority = gnutls_priority
2661
IPv6_TCPServer.__init__(self, server_address,
2662
RequestHandlerClass,
2663
interface=interface,
2667
def server_activate(self):
2669
return socketserver.TCPServer.server_activate(self)
2674
def add_pipe(self, parent_pipe, proc):
2675
# Call "handle_ipc" for both data and EOF events
2677
parent_pipe.fileno(),
2678
GLib.IO_IN | GLib.IO_HUP,
2679
functools.partial(self.handle_ipc,
2680
parent_pipe=parent_pipe,
2683
def handle_ipc(self, source, condition,
2686
client_object=None):
2687
# error, or the other end of multiprocessing.Pipe has closed
2688
if condition & (GLib.IO_ERR | GLib.IO_HUP):
2689
# Wait for other process to exit
2693
# Read a request from the child
2694
request = parent_pipe.recv()
2695
command = request[0]
2697
if command == 'init':
2698
key_id = request[1].decode("ascii")
2699
fpr = request[2].decode("ascii")
2700
address = request[3]
2702
for c in self.clients.values():
2703
if key_id == "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855":
2705
if key_id and c.key_id == key_id:
2708
if fpr and c.fingerprint == fpr:
2712
logger.info("Client not found for key ID: %s, address"
2713
": %s", key_id or fpr, address)
2716
mandos_dbus_service.ClientNotFound(key_id or fpr,
2718
parent_pipe.send(False)
2722
parent_pipe.fileno(),
2723
GLib.IO_IN | GLib.IO_HUP,
2724
functools.partial(self.handle_ipc,
2725
parent_pipe=parent_pipe,
2727
client_object=client))
2728
parent_pipe.send(True)
2729
# remove the old hook in favor of the new above hook on
2732
if command == 'funcall':
2733
funcname = request[1]
2737
parent_pipe.send(('data', getattr(client_object,
2741
if command == 'getattr':
2742
attrname = request[1]
2743
if isinstance(client_object.__getattribute__(attrname),
2744
collections.Callable):
2745
parent_pipe.send(('function', ))
2748
'data', client_object.__getattribute__(attrname)))
2750
if command == 'setattr':
2751
attrname = request[1]
2753
setattr(client_object, attrname, value)
2758
def rfc3339_duration_to_delta(duration):
2759
"""Parse an RFC 3339 "duration" and return a datetime.timedelta
2761
>>> rfc3339_duration_to_delta("P7D")
2762
datetime.timedelta(7)
2763
>>> rfc3339_duration_to_delta("PT60S")
2764
datetime.timedelta(0, 60)
2765
>>> rfc3339_duration_to_delta("PT60M")
2766
datetime.timedelta(0, 3600)
2767
>>> rfc3339_duration_to_delta("PT24H")
2768
datetime.timedelta(1)
2769
>>> rfc3339_duration_to_delta("P1W")
2770
datetime.timedelta(7)
2771
>>> rfc3339_duration_to_delta("PT5M30S")
2772
datetime.timedelta(0, 330)
2773
>>> rfc3339_duration_to_delta("P1DT3M20S")
2774
datetime.timedelta(1, 200)
2777
# Parsing an RFC 3339 duration with regular expressions is not
2778
# possible - there would have to be multiple places for the same
2779
# values, like seconds. The current code, while more esoteric, is
2780
# cleaner without depending on a parsing library. If Python had a
2781
# built-in library for parsing we would use it, but we'd like to
2782
# avoid excessive use of external libraries.
2784
# New type for defining tokens, syntax, and semantics all-in-one
2785
Token = collections.namedtuple("Token", (
2786
"regexp", # To match token; if "value" is not None, must have
2787
# a "group" containing digits
2788
"value", # datetime.timedelta or None
2789
"followers")) # Tokens valid after this token
2790
# RFC 3339 "duration" tokens, syntax, and semantics; taken from
2791
# the "duration" ABNF definition in RFC 3339, Appendix A.
2792
token_end = Token(re.compile(r"$"), None, frozenset())
2793
token_second = Token(re.compile(r"(\d+)S"),
2794
datetime.timedelta(seconds=1),
2795
frozenset((token_end, )))
2796
token_minute = Token(re.compile(r"(\d+)M"),
2797
datetime.timedelta(minutes=1),
2798
frozenset((token_second, token_end)))
2799
token_hour = Token(re.compile(r"(\d+)H"),
2800
datetime.timedelta(hours=1),
2801
frozenset((token_minute, token_end)))
2802
token_time = Token(re.compile(r"T"),
2804
frozenset((token_hour, token_minute,
2806
token_day = Token(re.compile(r"(\d+)D"),
2807
datetime.timedelta(days=1),
2808
frozenset((token_time, token_end)))
2809
token_month = Token(re.compile(r"(\d+)M"),
2810
datetime.timedelta(weeks=4),
2811
frozenset((token_day, token_end)))
2812
token_year = Token(re.compile(r"(\d+)Y"),
2813
datetime.timedelta(weeks=52),
2814
frozenset((token_month, token_end)))
2815
token_week = Token(re.compile(r"(\d+)W"),
2816
datetime.timedelta(weeks=1),
2817
frozenset((token_end, )))
2818
token_duration = Token(re.compile(r"P"), None,
2819
frozenset((token_year, token_month,
2820
token_day, token_time,
2822
# Define starting values:
2824
value = datetime.timedelta()
2826
# Following valid tokens
2827
followers = frozenset((token_duration, ))
2828
# String left to parse
2830
# Loop until end token is found
2831
while found_token is not token_end:
2832
# Search for any currently valid tokens
2833
for token in followers:
2834
match = token.regexp.match(s)
2835
if match is not None:
2837
if token.value is not None:
2838
# Value found, parse digits
2839
factor = int(match.group(1), 10)
2840
# Add to value so far
2841
value += factor * token.value
2842
# Strip token from string
2843
s = token.regexp.sub("", s, 1)
2846
# Set valid next tokens
2847
followers = found_token.followers
2850
# No currently valid tokens were found
2851
raise ValueError("Invalid RFC 3339 duration: {!r}"
532
elif self.server_address[1] is None:
533
self.server_address = (self.server_address[0],
535
return super(type(self), self).server_bind()
2857
538
def string_to_delta(interval):
2929
##################################################################
2930
# Parsing of options, both command line and config file
2932
parser = argparse.ArgumentParser()
2933
parser.add_argument("-v", "--version", action="version",
2934
version="%(prog)s {}".format(version),
2935
help="show version number and exit")
2936
parser.add_argument("-i", "--interface", metavar="IF",
2937
help="Bind to interface IF")
2938
parser.add_argument("-a", "--address",
2939
help="Address to listen for requests on")
2940
parser.add_argument("-p", "--port", type=int,
2941
help="Port number to receive requests on")
2942
parser.add_argument("--check", action="store_true",
2943
help="Run self-test")
2944
parser.add_argument("--debug", action="store_true",
2945
help="Debug mode; run in foreground and log"
2946
" to terminal", default=None)
2947
parser.add_argument("--debuglevel", metavar="LEVEL",
2948
help="Debug level for stdout output")
2949
parser.add_argument("--priority", help="GnuTLS"
2950
" priority string (see GnuTLS documentation)")
2951
parser.add_argument("--servicename",
2952
metavar="NAME", help="Zeroconf service name")
2953
parser.add_argument("--configdir",
2954
default="/etc/mandos", metavar="DIR",
2955
help="Directory to search for configuration"
2957
parser.add_argument("--no-dbus", action="store_false",
2958
dest="use_dbus", help="Do not provide D-Bus"
2959
" system bus interface", default=None)
2960
parser.add_argument("--no-ipv6", action="store_false",
2961
dest="use_ipv6", help="Do not use IPv6",
2963
parser.add_argument("--no-restore", action="store_false",
2964
dest="restore", help="Do not restore stored"
2965
" state", default=None)
2966
parser.add_argument("--socket", type=int,
2967
help="Specify a file descriptor to a network"
2968
" socket to use instead of creating one")
2969
parser.add_argument("--statedir", metavar="DIR",
2970
help="Directory to save/restore state in")
2971
parser.add_argument("--foreground", action="store_true",
2972
help="Run in foreground", default=None)
2973
parser.add_argument("--no-zeroconf", action="store_false",
2974
dest="zeroconf", help="Do not use Zeroconf",
2977
options = parser.parse_args()
651
global main_loop_started
652
main_loop_started = False
654
parser = OptionParser()
655
parser.add_option("-i", "--interface", type="string",
656
metavar="IF", help="Bind to interface IF")
657
parser.add_option("-a", "--address", type="string",
658
help="Address to listen for requests on")
659
parser.add_option("-p", "--port", type="int",
660
help="Port number to receive requests on")
661
parser.add_option("--check", action="store_true", default=False,
662
help="Run self-test")
663
parser.add_option("--debug", action="store_true", default=False,
664
help="Debug mode; run in foreground and log to"
666
parser.add_option("--priority", type="string", help="GnuTLS"
667
" priority string (see GnuTLS documentation)")
668
parser.add_option("--servicename", type="string", metavar="NAME",
669
help="Zeroconf service name")
670
parser.add_option("--configdir", type="string",
671
default="/etc/mandos", metavar="DIR",
672
help="Directory to search for configuration"
674
(options, args) = parser.parse_args()
2979
681
# Default values for config file for server-global settings
2980
if gnutls.has_rawpk:
2981
priority = ("SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA"
2982
":!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA")
2984
priority = ("SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2985
":+SIGN-DSA-SHA256")
2986
server_defaults = {"interface": "",
2990
"priority": priority,
2991
"servicename": "Mandos",
2997
"statedir": "/var/lib/mandos",
2998
"foreground": "False",
682
server_defaults = { "interface": "",
687
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
688
"servicename": "Mandos",
3003
691
# Parse config file for server-global settings
3004
server_config = configparser.ConfigParser(server_defaults)
692
server_config = ConfigParser.SafeConfigParser(server_defaults)
3005
693
del server_defaults
3006
server_config.read(os.path.join(options.configdir, "mandos.conf"))
3007
# Convert the ConfigParser object to a dict
3008
server_settings = server_config.defaults()
3009
# Use the appropriate methods on the non-string config options
3010
for option in ("debug", "use_dbus", "use_ipv6", "restore",
3011
"foreground", "zeroconf"):
3012
server_settings[option] = server_config.getboolean("DEFAULT",
3014
if server_settings["port"]:
3015
server_settings["port"] = server_config.getint("DEFAULT",
3017
if server_settings["socket"]:
3018
server_settings["socket"] = server_config.getint("DEFAULT",
3020
# Later, stdin will, and stdout and stderr might, be dup'ed
3021
# over with an opened os.devnull. But we don't want this to
3022
# happen with a supplied network socket.
3023
if 0 <= server_settings["socket"] <= 2:
3024
server_settings["socket"] = os.dup(server_settings
694
server_config.read(os.path.join(options.configdir, "server.conf"))
695
server_section = "server"
696
# Convert the SafeConfigParser object to a dict
697
server_settings = dict(server_config.items(server_section))
698
# Use getboolean on the boolean config option
699
server_settings["debug"] = server_config.getboolean\
700
(server_section, "debug")
3026
701
del server_config
702
if not server_settings["interface"]:
703
server_settings["interface"] = avahi.IF_UNSPEC
3028
705
# Override the settings from the config file with command line
3029
706
# options, if set.
3030
707
for option in ("interface", "address", "port", "debug",
3031
"priority", "servicename", "configdir", "use_dbus",
3032
"use_ipv6", "debuglevel", "restore", "statedir",
3033
"socket", "foreground", "zeroconf"):
708
"priority", "servicename", "configdir"):
3034
709
value = getattr(options, option)
3035
710
if value is not None:
3036
711
server_settings[option] = value
3038
# Force all strings to be unicode
3039
for option in server_settings.keys():
3040
if isinstance(server_settings[option], bytes):
3041
server_settings[option] = (server_settings[option]
3043
# Force all boolean options to be boolean
3044
for option in ("debug", "use_dbus", "use_ipv6", "restore",
3045
"foreground", "zeroconf"):
3046
server_settings[option] = bool(server_settings[option])
3047
# Debug implies foreground
3048
if server_settings["debug"]:
3049
server_settings["foreground"] = True
3050
713
# Now we have our good server settings in "server_settings"
3052
##################################################################
3054
if (not server_settings["zeroconf"]
3055
and not (server_settings["port"]
3056
or server_settings["socket"] != "")):
3057
parser.error("Needs port or socket to work without Zeroconf")
3060
debug = server_settings["debug"]
3061
debuglevel = server_settings["debuglevel"]
3062
use_dbus = server_settings["use_dbus"]
3063
use_ipv6 = server_settings["use_ipv6"]
3064
stored_state_path = os.path.join(server_settings["statedir"],
3066
foreground = server_settings["foreground"]
3067
zeroconf = server_settings["zeroconf"]
3070
initlogger(debug, logging.DEBUG)
3075
level = getattr(logging, debuglevel.upper())
3076
initlogger(debug, level)
3078
if server_settings["servicename"] != "Mandos":
3079
syslogger.setFormatter(
3080
logging.Formatter('Mandos ({}) [%(process)d]:'
3081
' %(levelname)s: %(message)s'.format(
3082
server_settings["servicename"])))
3084
715
# Parse config file with clients
3085
client_config = configparser.ConfigParser(Client.client_defaults)
716
client_defaults = { "timeout": "1h",
718
"checker": "fping -q -- %%(fqdn)s",
720
client_config = ConfigParser.SafeConfigParser(client_defaults)
3086
721
client_config.read(os.path.join(server_settings["configdir"],
3087
722
"clients.conf"))
3089
global mandos_dbus_service
3090
mandos_dbus_service = None
3093
if server_settings["socket"] != "":
3094
socketfd = server_settings["socket"]
3095
tcp_server = MandosServer(
3096
(server_settings["address"], server_settings["port"]),
3098
interface=(server_settings["interface"] or None),
3100
gnutls_priority=server_settings["priority"],
3104
pidfilename = "/run/mandos.pid"
3105
if not os.path.isdir("/run/."):
3106
pidfilename = "/var/run/mandos.pid"
3109
pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
3110
except IOError as e:
3111
logger.error("Could not open file %r", pidfilename,
3114
for name, group in (("_mandos", "_mandos"),
3115
("mandos", "mandos"),
3116
("nobody", "nogroup")):
3118
uid = pwd.getpwnam(name).pw_uid
3119
gid = pwd.getpwnam(group).pw_gid
3130
logger.debug("Did setuid/setgid to {}:{}".format(uid,
3132
except OSError as error:
3133
logger.warning("Failed to setuid/setgid to {}:{}: {}"
3134
.format(uid, gid, os.strerror(error.errno)))
3135
if error.errno != errno.EPERM:
3139
# Enable all possible GnuTLS debugging
3141
# "Use a log level over 10 to enable all debugging options."
3143
gnutls.global_set_log_level(11)
3146
def debug_gnutls(level, string):
3147
logger.debug("GnuTLS: %s", string[:-1])
3149
gnutls.global_set_log_function(debug_gnutls)
3151
# Redirect stdin so all checkers get /dev/null
3152
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
3153
os.dup2(null, sys.stdin.fileno())
3157
# Need to fork before connecting to D-Bus
3159
# Close all input and output, do double fork, etc.
3162
if gi.version_info < (3, 10, 2):
3163
# multiprocessing will use threads, so before we use GLib we
3164
# need to inform GLib that threads will be used.
725
service = AvahiService(name = server_settings["servicename"],
726
type = "_mandos._tcp", );
3167
728
global main_loop
3168
731
# From the Avahi example code
3169
DBusGMainLoop(set_as_default=True)
3170
main_loop = GLib.MainLoop()
732
DBusGMainLoop(set_as_default=True )
733
main_loop = gobject.MainLoop()
3171
734
bus = dbus.SystemBus()
735
server = dbus.Interface(
736
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
737
avahi.DBUS_INTERFACE_SERVER )
3172
738
# End of Avahi example code
3175
bus_name = dbus.service.BusName("se.recompile.Mandos",
3178
old_bus_name = dbus.service.BusName(
3179
"se.bsnet.fukt.Mandos", bus,
3181
except dbus.exceptions.DBusException as e:
3182
logger.error("Disabling D-Bus:", exc_info=e)
3184
server_settings["use_dbus"] = False
3185
tcp_server.use_dbus = False
3187
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
3188
service = AvahiServiceToSyslog(
3189
name=server_settings["servicename"],
3190
servicetype="_mandos._tcp",
3193
if server_settings["interface"]:
3194
service.interface = if_nametoindex(
3195
server_settings["interface"].encode("utf-8"))
3197
global multiprocessing_manager
3198
multiprocessing_manager = multiprocessing.Manager()
3200
client_class = Client
3202
client_class = functools.partial(ClientDBus, bus=bus)
3204
client_settings = Client.config_parser(client_config)
3205
old_client_settings = {}
3208
# This is used to redirect stdout and stderr for checker processes
3210
wnull = open(os.devnull, "w") # A writable /dev/null
3211
# Only used if server is running in foreground but not in debug
3213
if debug or not foreground:
3216
# Get client data and settings from last running state.
3217
if server_settings["restore"]:
3219
with open(stored_state_path, "rb") as stored_state:
3220
if sys.version_info.major == 2:
3221
clients_data, old_client_settings = pickle.load(
3224
bytes_clients_data, bytes_old_client_settings = (
3225
pickle.load(stored_state, encoding="bytes"))
3226
# Fix bytes to strings
3229
clients_data = {(key.decode("utf-8")
3230
if isinstance(key, bytes)
3233
bytes_clients_data.items()}
3234
del bytes_clients_data
3235
for key in clients_data:
3236
value = {(k.decode("utf-8")
3237
if isinstance(k, bytes) else k): v
3239
clients_data[key].items()}
3240
clients_data[key] = value
3242
value["client_structure"] = [
3244
if isinstance(s, bytes)
3246
value["client_structure"]]
3248
for k in ("name", "host"):
3249
if isinstance(value[k], bytes):
3250
value[k] = value[k].decode("utf-8")
3251
if "key_id" not in value:
3252
value["key_id"] = ""
3253
elif "fingerprint" not in value:
3254
value["fingerprint"] = ""
3255
# old_client_settings
3257
old_client_settings = {
3258
(key.decode("utf-8")
3259
if isinstance(key, bytes)
3262
bytes_old_client_settings.items()}
3263
del bytes_old_client_settings
3265
for value in old_client_settings.values():
3266
if isinstance(value["host"], bytes):
3267
value["host"] = (value["host"]
3269
os.remove(stored_state_path)
3270
except IOError as e:
3271
if e.errno == errno.ENOENT:
3272
logger.warning("Could not load persistent state:"
3273
" {}".format(os.strerror(e.errno)))
3275
logger.critical("Could not load persistent state:",
3278
except EOFError as e:
3279
logger.warning("Could not load persistent state: "
3283
with PGPEngine() as pgp:
3284
for client_name, client in clients_data.items():
3285
# Skip removed clients
3286
if client_name not in client_settings:
3289
# Decide which value to use after restoring saved state.
3290
# We have three different values: Old config file,
3291
# new config file, and saved state.
3292
# New config value takes precedence if it differs from old
3293
# config value, otherwise use saved state.
3294
for name, value in client_settings[client_name].items():
3296
# For each value in new config, check if it
3297
# differs from the old config value (Except for
3298
# the "secret" attribute)
3299
if (name != "secret"
3301
old_client_settings[client_name][name])):
3302
client[name] = value
3306
# Clients who has passed its expire date can still be
3307
# enabled if its last checker was successful. A Client
3308
# whose checker succeeded before we stored its state is
3309
# assumed to have successfully run all checkers during
3311
if client["enabled"]:
3312
if datetime.datetime.utcnow() >= client["expires"]:
3313
if not client["last_checked_ok"]:
3315
"disabling client {} - Client never "
3316
"performed a successful checker".format(
3318
client["enabled"] = False
3319
elif client["last_checker_status"] != 0:
3321
"disabling client {} - Client last"
3322
" checker failed with error code"
3325
client["last_checker_status"]))
3326
client["enabled"] = False
3328
client["expires"] = (
3329
datetime.datetime.utcnow()
3330
+ client["timeout"])
3331
logger.debug("Last checker succeeded,"
3332
" keeping {} enabled".format(
3335
client["secret"] = pgp.decrypt(
3336
client["encrypted_secret"],
3337
client_settings[client_name]["secret"])
3339
# If decryption fails, we use secret from new settings
3340
logger.debug("Failed to decrypt {} old secret".format(
3342
client["secret"] = (client_settings[client_name]
3345
# Add/remove clients based on new changes made to config
3346
for client_name in (set(old_client_settings)
3347
- set(client_settings)):
3348
del clients_data[client_name]
3349
for client_name in (set(client_settings)
3350
- set(old_client_settings)):
3351
clients_data[client_name] = client_settings[client_name]
3353
# Create all client objects
3354
for client_name, client in clients_data.items():
3355
tcp_server.clients[client_name] = client_class(
3358
server_settings=server_settings)
3360
if not tcp_server.clients:
3361
logger.warning("No clients defined")
3364
if pidfile is not None:
3368
print(pid, file=pidfile)
3370
logger.error("Could not write to file %r with PID %d",
3375
for termsig in (signal.SIGHUP, signal.SIGTERM):
3376
GLib.unix_signal_add(GLib.PRIORITY_HIGH, termsig,
3377
lambda: main_loop.quit() and False)
3381
@alternate_dbus_interfaces(
3382
{"se.recompile.Mandos": "se.bsnet.fukt.Mandos"})
3383
class MandosDBusService(DBusObjectWithObjectManager):
3384
"""A D-Bus proxy object"""
3387
dbus.service.Object.__init__(self, bus, "/")
3389
_interface = "se.recompile.Mandos"
3391
@dbus.service.signal(_interface, signature="o")
3392
def ClientAdded(self, objpath):
3396
@dbus.service.signal(_interface, signature="ss")
3397
def ClientNotFound(self, key_id, address):
3401
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3403
@dbus.service.signal(_interface, signature="os")
3404
def ClientRemoved(self, objpath, name):
3408
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3410
@dbus.service.method(_interface, out_signature="ao")
3411
def GetAllClients(self):
3413
return dbus.Array(c.dbus_object_path for c in
3414
tcp_server.clients.values())
3416
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3418
@dbus.service.method(_interface,
3419
out_signature="a{oa{sv}}")
3420
def GetAllClientsWithProperties(self):
3422
return dbus.Dictionary(
3423
{c.dbus_object_path: c.GetAll(
3424
"se.recompile.Mandos.Client")
3425
for c in tcp_server.clients.values()},
3428
@dbus.service.method(_interface, in_signature="o")
3429
def RemoveClient(self, object_path):
3431
for c in tcp_server.clients.values():
3432
if c.dbus_object_path == object_path:
3433
del tcp_server.clients[c.name]
3434
c.remove_from_connection()
3435
# Don't signal the disabling
3436
c.disable(quiet=True)
3437
# Emit D-Bus signal for removal
3438
self.client_removed_signal(c)
3440
raise KeyError(object_path)
3444
@dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
3445
out_signature="a{oa{sa{sv}}}")
3446
def GetManagedObjects(self):
3448
return dbus.Dictionary(
3449
{client.dbus_object_path:
3451
{interface: client.GetAll(interface)
3453
client._get_all_interface_names()})
3454
for client in tcp_server.clients.values()})
3456
def client_added_signal(self, client):
3457
"""Send the new standard signal and the old signal"""
3459
# New standard signal
3460
self.InterfacesAdded(
3461
client.dbus_object_path,
3463
{interface: client.GetAll(interface)
3465
client._get_all_interface_names()}))
3467
self.ClientAdded(client.dbus_object_path)
3469
def client_removed_signal(self, client):
3470
"""Send the new standard signal and the old signal"""
3472
# New standard signal
3473
self.InterfacesRemoved(
3474
client.dbus_object_path,
3475
client._get_all_interface_names())
3477
self.ClientRemoved(client.dbus_object_path,
3480
mandos_dbus_service = MandosDBusService()
3482
# Save modules to variables to exempt the modules from being
3483
# unloaded before the function registered with atexit() is run.
3484
mp = multiprocessing
740
debug = server_settings["debug"]
743
console = logging.StreamHandler()
744
# console.setLevel(logging.DEBUG)
745
console.setFormatter(logging.Formatter\
746
('%(levelname)s: %(message)s'))
747
logger.addHandler(console)
751
def remove_from_clients(client):
752
clients.remove(client)
754
logger.debug(u"No clients left, exiting")
757
clients.update(Set(Client(name=section,
758
stop_hook = remove_from_clients,
759
**(dict(client_config\
761
for section in client_config.sections()))
3488
767
"Cleanup function; run on exit"
3492
mp.active_children()
3494
if not (tcp_server.clients or client_settings):
3497
# Store client before exiting. Secrets are encrypted with key
3498
# based on what config file has. If config file is
3499
# removed/edited, old secret will thus be unrecovable.
3501
with PGPEngine() as pgp:
3502
for client in tcp_server.clients.values():
3503
key = client_settings[client.name]["secret"]
3504
client.encrypted_secret = pgp.encrypt(client.secret,
3508
# A list of attributes that can not be pickled
3510
exclude = {"bus", "changedstate", "secret",
3511
"checker", "server_settings"}
3512
for name, typ in inspect.getmembers(dbus.service
3516
client_dict["encrypted_secret"] = (client
3518
for attr in client.client_structure:
3519
if attr not in exclude:
3520
client_dict[attr] = getattr(client, attr)
3522
clients[client.name] = client_dict
3523
del client_settings[client.name]["secret"]
3526
with tempfile.NamedTemporaryFile(
3530
dir=os.path.dirname(stored_state_path),
3531
delete=False) as stored_state:
3532
pickle.dump((clients, client_settings), stored_state,
3534
tempname = stored_state.name
3535
os.rename(tempname, stored_state_path)
3536
except (IOError, OSError) as e:
3542
if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
3543
logger.warning("Could not save persistent state: {}"
3544
.format(os.strerror(e.errno)))
3546
logger.warning("Could not save persistent state:",
3550
# Delete all clients, and settings from config
3551
while tcp_server.clients:
3552
name, client = tcp_server.clients.popitem()
3554
client.remove_from_connection()
3555
# Don't signal the disabling
3556
client.disable(quiet=True)
3557
# Emit D-Bus signal for removal
3559
mandos_dbus_service.client_removed_signal(client)
3560
client_settings.clear()
769
# From the Avahi example code
770
if not group is None:
773
# End of Avahi example code
776
client = clients.pop()
777
client.stop_hook = None
3562
780
atexit.register(cleanup)
3564
for client in tcp_server.clients.values():
3566
# Emit D-Bus signal for adding
3567
mandos_dbus_service.client_added_signal(client)
3568
# Need to initiate checking of clients
3570
client.init_checker()
3573
tcp_server.server_activate()
783
signal.signal(signal.SIGINT, signal.SIG_IGN)
784
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
785
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
787
for client in clients:
790
tcp_server = IPv6_TCPServer((server_settings["address"],
791
server_settings["port"]),
793
settings=server_settings,
3575
795
# Find out what port we got
3577
service.port = tcp_server.socket.getsockname()[1]
3579
logger.info("Now listening on address %r, port %d,"
3580
" flowinfo %d, scope_id %d",
3581
*tcp_server.socket.getsockname())
3583
logger.info("Now listening on address %r, port %d",
3584
*tcp_server.socket.getsockname())
3586
# service.interface = tcp_server.socket.getsockname()[3]
796
service.port = tcp_server.socket.getsockname()[1]
797
logger.debug(u"Now listening on port %d", service.port)
799
if not server_settings["interface"]:
800
service.interface = if_nametoindex\
801
(server_settings["interface"])
3590
# From the Avahi example code
3593
except dbus.exceptions.DBusException as error:
3594
logger.critical("D-Bus Exception", exc_info=error)
3597
# End of Avahi example code
3599
GLib.io_add_watch(tcp_server.fileno(), GLib.IO_IN,
3600
lambda *args, **kwargs:
3601
(tcp_server.handle_request
3602
(*args[2:], **kwargs) or True))
804
# From the Avahi example code
805
server.connect_to_signal("StateChanged", server_state_changed)
807
server_state_changed(server.GetState())
808
except dbus.exceptions.DBusException, error:
809
logger.critical(u"DBusException: %s", error)
811
# End of Avahi example code
813
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
814
lambda *args, **kwargs:
815
tcp_server.handle_request\
816
(*args[2:], **kwargs) or True)
3604
818
logger.debug("Starting main loop")
819
main_loop_started = True
3606
except AvahiError as error:
3607
logger.critical("Avahi Error", exc_info=error)
821
except AvahiError, error:
822
logger.critical(u"AvahiError: %s" + unicode(error))
3610
824
except KeyboardInterrupt:
3612
print("", file=sys.stderr)
3613
logger.debug("Server received KeyboardInterrupt")
3614
logger.debug("Server exiting")
3615
# Must run before the D-Bus bus name gets deregistered
3619
def should_only_run_tests():
3620
parser = argparse.ArgumentParser(add_help=False)
3621
parser.add_argument("--check", action='store_true')
3622
args, unknown_args = parser.parse_known_args()
3623
run_tests = args.check
3625
# Remove --check argument from sys.argv
3626
sys.argv[1:] = unknown_args
3629
# Add all tests from doctest strings
3630
def load_tests(loader, tests, none):
3632
tests.addTests(doctest.DocTestSuite())
3635
828
if __name__ == '__main__':
3637
if should_only_run_tests():
3638
# Call using ./mandos --check [--verbose]