2
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
9
# methods "add", "remove", "server_state_changed",
10
10
# "entry_group_state_changed", "cleanup", and "activate" in the
11
11
# "AvahiService" class, and some lines in "main".
13
13
# Everything else is
14
# Copyright © 2008-2015 Teddy Hogeborn
15
# Copyright © 2008-2015 Björn Påhlsson
17
# This program is free software: you can redistribute it and/or modify
18
# it under the terms of the GNU General Public License as published by
14
# Copyright © 2008-2017 Teddy Hogeborn
15
# Copyright © 2008-2017 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
19
21
# the Free Software Foundation, either version 3 of the License, or
20
22
# (at your option) any later version.
22
# This program is distributed in the hope that it will be useful,
23
# but WITHOUT ANY WARRANTY; without even the implied warranty of
24
# Mandos is distributed in the hope that it will be useful, but
25
# WITHOUT ANY WARRANTY; without even the implied warranty of
24
26
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
27
# GNU General Public License for more details.
27
29
# You should have received a copy of the GNU General Public License
28
# along with this program. If not, see
29
# <http://www.gnu.org/licenses/>.
30
# along with Mandos. If not, see <http://www.gnu.org/licenses/>.
31
32
# Contact the authors at <mandos@recompile.se>.
34
35
from __future__ import (division, absolute_import, print_function,
37
from future_builtins import *
39
from future_builtins import *
40
44
import SocketServer as socketserver
78
82
import dbus.service
82
from gi.repository import GObject as gobject
83
from gi.repository import GLib
84
84
from dbus.mainloop.glib import DBusGMainLoop
87
87
import xml.dom.minidom
90
# Try to find the value of SO_BINDTODEVICE:
92
# This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and
93
# newer, and it is also the most natural place for it:
91
94
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
92
95
except AttributeError:
97
# This is where SO_BINDTODEVICE was up to and including Python
94
99
from IN import SO_BINDTODEVICE
95
100
except ImportError:
96
SO_BINDTODEVICE = None
101
# In Python 2.7 it seems to have been removed entirely.
102
# Try running the C preprocessor:
104
cc = subprocess.Popen(["cc", "--language=c", "-E",
106
stdin=subprocess.PIPE,
107
stdout=subprocess.PIPE)
108
stdout = cc.communicate(
109
"#include <sys/socket.h>\nSO_BINDTODEVICE\n")[0]
110
SO_BINDTODEVICE = int(stdout.splitlines()[-1])
111
except (OSError, ValueError, IndexError):
113
SO_BINDTODEVICE = None
98
115
if sys.version_info.major == 2:
102
119
stored_state_file = "clients.pickle"
104
121
logger = logging.getLogger()
149
182
class PGPEngine(object):
150
183
"""A simple class for OpenPGP symmetric encryption & decryption"""
152
185
def __init__(self):
153
186
self.tempdir = tempfile.mkdtemp(prefix="mandos-")
189
output = subprocess.check_output(["gpgconf"])
190
for line in output.splitlines():
191
name, text, path = line.split(b":")
196
if e.errno != errno.ENOENT:
154
198
self.gnupgargs = ['--batch',
155
'--home', self.tempdir,
199
'--homedir', self.tempdir,
202
# Only GPG version 1 has the --no-use-agent option.
203
if self.gpg == "gpg" or self.gpg.endswith("/gpg"):
204
self.gnupgargs.append("--no-use-agent")
160
206
def __enter__(self):
163
209
def __exit__(self, exc_type, exc_value, traceback):
167
213
def __del__(self):
170
216
def _cleanup(self):
171
217
if self.tempdir is not None:
172
218
# Delete contents of tempdir
173
219
for root, dirs, files in os.walk(self.tempdir,
175
221
for filename in files:
176
222
os.remove(os.path.join(root, filename))
177
223
for dirname in dirs:
190
236
.replace(b"\n", b"\\n")
191
237
.replace(b"\0", b"\\x00"))
194
240
def encrypt(self, data, password):
195
241
passphrase = self.password_encode(password)
196
242
with tempfile.NamedTemporaryFile(
197
243
dir=self.tempdir) as passfile:
198
244
passfile.write(passphrase)
200
proc = subprocess.Popen(['gpg', '--symmetric',
246
proc = subprocess.Popen([self.gpg, '--symmetric',
201
247
'--passphrase-file',
203
249
+ self.gnupgargs,
204
stdin = subprocess.PIPE,
205
stdout = subprocess.PIPE,
206
stderr = subprocess.PIPE)
207
ciphertext, err = proc.communicate(input = data)
250
stdin=subprocess.PIPE,
251
stdout=subprocess.PIPE,
252
stderr=subprocess.PIPE)
253
ciphertext, err = proc.communicate(input=data)
208
254
if proc.returncode != 0:
209
255
raise PGPError(err)
210
256
return ciphertext
212
258
def decrypt(self, data, password):
213
259
passphrase = self.password_encode(password)
214
260
with tempfile.NamedTemporaryFile(
215
dir = self.tempdir) as passfile:
261
dir=self.tempdir) as passfile:
216
262
passfile.write(passphrase)
218
proc = subprocess.Popen(['gpg', '--decrypt',
264
proc = subprocess.Popen([self.gpg, '--decrypt',
219
265
'--passphrase-file',
221
267
+ self.gnupgargs,
222
stdin = subprocess.PIPE,
223
stdout = subprocess.PIPE,
224
stderr = subprocess.PIPE)
225
decrypted_plaintext, err = proc.communicate(input = data)
268
stdin=subprocess.PIPE,
269
stdout=subprocess.PIPE,
270
stderr=subprocess.PIPE)
271
decrypted_plaintext, err = proc.communicate(input=data)
226
272
if proc.returncode != 0:
227
273
raise PGPError(err)
228
274
return decrypted_plaintext
277
# Pretend that we have an Avahi module
279
"""This isn't so much a class as it is a module-like namespace.
280
It is instantiated once, and simulates having an Avahi module."""
281
IF_UNSPEC = -1 # avahi-common/address.h
282
PROTO_UNSPEC = -1 # avahi-common/address.h
283
PROTO_INET = 0 # avahi-common/address.h
284
PROTO_INET6 = 1 # avahi-common/address.h
285
DBUS_NAME = "org.freedesktop.Avahi"
286
DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup"
287
DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
288
DBUS_PATH_SERVER = "/"
290
def string_array_to_txt_array(self, t):
291
return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
292
for s in t), signature="ay")
293
ENTRY_GROUP_ESTABLISHED = 2 # avahi-common/defs.h
294
ENTRY_GROUP_COLLISION = 3 # avahi-common/defs.h
295
ENTRY_GROUP_FAILURE = 4 # avahi-common/defs.h
296
SERVER_INVALID = 0 # avahi-common/defs.h
297
SERVER_REGISTERING = 1 # avahi-common/defs.h
298
SERVER_RUNNING = 2 # avahi-common/defs.h
299
SERVER_COLLISION = 3 # avahi-common/defs.h
300
SERVER_FAILURE = 4 # avahi-common/defs.h
231
304
class AvahiError(Exception):
232
305
def __init__(self, value, *args, **kwargs):
233
306
self.value = value
423
496
class AvahiServiceToSyslog(AvahiService):
424
497
def rename(self, *args, **kwargs):
425
498
"""Add the new name to the syslog messages"""
426
ret = AvahiService.rename(self, *args, **kwargs)
499
ret = super(AvahiServiceToSyslog, self).rename(self, *args,
427
501
syslogger.setFormatter(logging.Formatter(
428
502
'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
429
503
.format(self.name)))
432
507
# Pretend that we have a GnuTLS module
433
508
class GnuTLS(object):
434
509
"""This isn't so much a class as it is a module-like namespace.
435
510
It is instantiated once, and simulates having a GnuTLS module."""
437
_library = ctypes.cdll.LoadLibrary(
438
ctypes.util.find_library("gnutls"))
439
_need_version = "3.3.0"
512
library = ctypes.util.find_library("gnutls")
514
library = ctypes.util.find_library("gnutls-deb0")
515
_library = ctypes.cdll.LoadLibrary(library)
517
_need_version = b"3.3.0"
440
519
def __init__(self):
441
# Need to use class name "GnuTLS" here, since this method is
442
# called before the assignment to the "gnutls" global variable
444
if GnuTLS.check_version(self._need_version) is None:
445
raise GnuTLS.Error("Needs GnuTLS {} or later"
446
.format(self._need_version))
520
# Need to use "self" here, since this method is called before
521
# the assignment to the "gnutls" global variable happens.
522
if self.check_version(self._need_version) is None:
523
raise self.Error("Needs GnuTLS {} or later"
524
.format(self._need_version))
448
526
# Unless otherwise indicated, the constants and types below are
449
527
# all from the gnutls/gnutls.h C header file.
456
536
CRD_CERTIFICATE = 1
457
537
E_NO_CERTIFICATE_FOUND = -49
458
538
OPENPGP_FMT_RAW = 0 # gnutls/openpgp.h
461
541
class session_int(ctypes.Structure):
463
543
session_t = ctypes.POINTER(session_int)
464
545
class certificate_credentials_st(ctypes.Structure):
466
547
certificate_credentials_t = ctypes.POINTER(
467
548
certificate_credentials_st)
468
549
certificate_type_t = ctypes.c_int
469
551
class datum_t(ctypes.Structure):
470
552
_fields_ = [('data', ctypes.POINTER(ctypes.c_ubyte)),
471
553
('size', ctypes.c_uint)]
472
555
class openpgp_crt_int(ctypes.Structure):
474
557
openpgp_crt_t = ctypes.POINTER(openpgp_crt_int)
475
openpgp_crt_fmt_t = ctypes.c_int # gnutls/openpgp.h
558
openpgp_crt_fmt_t = ctypes.c_int # gnutls/openpgp.h
476
559
log_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
477
credentials_type_t = ctypes.c_int #
560
credentials_type_t = ctypes.c_int
478
561
transport_ptr_t = ctypes.c_void_p
479
562
close_request_t = ctypes.c_int
482
565
class Error(Exception):
483
566
# We need to use the class name "GnuTLS" here, since this
484
567
# exception might be raised from within GnuTLS.__init__,
485
568
# which is called before the assignment to the "gnutls"
486
# global variable happens.
487
def __init__(self, message = None, code = None, args=()):
569
# global variable has happened.
570
def __init__(self, message=None, code=None, args=()):
488
571
# Default usage is by a message string, but if a return
489
572
# code is passed, convert it to a string with
490
573
# gnutls.strerror()
491
575
if message is None and code is not None:
492
576
message = GnuTLS.strerror(code)
493
577
return super(GnuTLS.Error, self).__init__(
496
580
class CertificateSecurityError(Error):
500
584
class Credentials(object):
501
585
def __init__(self):
522
606
ctypes.cast(credentials._c_object,
523
607
ctypes.c_void_p))
524
608
self.credentials = credentials
526
610
def __del__(self):
527
611
gnutls.deinit(self._c_object)
529
613
def handshake(self):
530
614
return gnutls.handshake(self._c_object)
532
616
def send(self, data):
533
617
data = bytes(data)
536
return gnutls.record_send(self._c_object, data, len(data))
620
data_len -= gnutls.record_send(self._c_object,
539
625
return gnutls.bye(self._c_object, gnutls.SHUT_RDWR)
541
# Error handling function
627
# Error handling functions
542
628
def _error_code(result):
543
629
"""A function to raise exceptions on errors, suitable
544
630
for the 'restype' attribute on ctypes functions"""
547
633
if result == gnutls.E_NO_CERTIFICATE_FOUND:
548
raise gnutls.CertificateSecurityError(code = result)
549
raise gnutls.Error(code = result)
634
raise gnutls.CertificateSecurityError(code=result)
635
raise gnutls.Error(code=result)
637
def _retry_on_error(result, func, arguments):
638
"""A function to retry on some errors, suitable
639
for the 'errcheck' attribute on ctypes functions"""
641
if result not in (gnutls.E_INTERRUPTED, gnutls.E_AGAIN):
642
return _error_code(result)
643
result = func(*arguments)
551
646
# Unless otherwise indicated, the function declarations below are
552
647
# all from the gnutls/gnutls.h C header file.
555
650
priority_set_direct = _library.gnutls_priority_set_direct
556
651
priority_set_direct.argtypes = [session_t, ctypes.c_char_p,
557
652
ctypes.POINTER(ctypes.c_char_p)]
558
653
priority_set_direct.restype = _error_code
560
655
init = _library.gnutls_init
561
656
init.argtypes = [ctypes.POINTER(session_t), ctypes.c_int]
562
657
init.restype = _error_code
564
659
set_default_priority = _library.gnutls_set_default_priority
565
660
set_default_priority.argtypes = [session_t]
566
661
set_default_priority.restype = _error_code
568
663
record_send = _library.gnutls_record_send
569
664
record_send.argtypes = [session_t, ctypes.c_void_p,
571
666
record_send.restype = ctypes.c_ssize_t
667
record_send.errcheck = _retry_on_error
573
669
certificate_allocate_credentials = (
574
670
_library.gnutls_certificate_allocate_credentials)
575
671
certificate_allocate_credentials.argtypes = [
576
672
ctypes.POINTER(certificate_credentials_t)]
577
673
certificate_allocate_credentials.restype = _error_code
579
675
certificate_free_credentials = (
580
676
_library.gnutls_certificate_free_credentials)
581
certificate_free_credentials.argtypes = [certificate_credentials_t]
677
certificate_free_credentials.argtypes = [
678
certificate_credentials_t]
582
679
certificate_free_credentials.restype = None
584
681
handshake_set_private_extensions = (
585
682
_library.gnutls_handshake_set_private_extensions)
586
683
handshake_set_private_extensions.argtypes = [session_t,
588
685
handshake_set_private_extensions.restype = None
590
687
credentials_set = _library.gnutls_credentials_set
591
688
credentials_set.argtypes = [session_t, credentials_type_t,
593
690
credentials_set.restype = _error_code
595
692
strerror = _library.gnutls_strerror
596
693
strerror.argtypes = [ctypes.c_int]
597
694
strerror.restype = ctypes.c_char_p
599
696
certificate_type_get = _library.gnutls_certificate_type_get
600
697
certificate_type_get.argtypes = [session_t]
601
698
certificate_type_get.restype = _error_code
603
700
certificate_get_peers = _library.gnutls_certificate_get_peers
604
701
certificate_get_peers.argtypes = [session_t,
605
702
ctypes.POINTER(ctypes.c_uint)]
606
703
certificate_get_peers.restype = ctypes.POINTER(datum_t)
608
705
global_set_log_level = _library.gnutls_global_set_log_level
609
706
global_set_log_level.argtypes = [ctypes.c_int]
610
707
global_set_log_level.restype = None
612
709
global_set_log_function = _library.gnutls_global_set_log_function
613
710
global_set_log_function.argtypes = [log_func]
614
711
global_set_log_function.restype = None
616
713
deinit = _library.gnutls_deinit
617
714
deinit.argtypes = [session_t]
618
715
deinit.restype = None
620
717
handshake = _library.gnutls_handshake
621
718
handshake.argtypes = [session_t]
622
719
handshake.restype = _error_code
720
handshake.errcheck = _retry_on_error
624
722
transport_set_ptr = _library.gnutls_transport_set_ptr
625
723
transport_set_ptr.argtypes = [session_t, transport_ptr_t]
626
724
transport_set_ptr.restype = None
628
726
bye = _library.gnutls_bye
629
727
bye.argtypes = [session_t, close_request_t]
630
728
bye.restype = _error_code
729
bye.errcheck = _retry_on_error
632
731
check_version = _library.gnutls_check_version
633
732
check_version.argtypes = [ctypes.c_char_p]
634
733
check_version.restype = ctypes.c_char_p
636
735
# All the function declarations below are from gnutls/openpgp.h
638
737
openpgp_crt_init = _library.gnutls_openpgp_crt_init
639
738
openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
640
739
openpgp_crt_init.restype = _error_code
642
741
openpgp_crt_import = _library.gnutls_openpgp_crt_import
643
742
openpgp_crt_import.argtypes = [openpgp_crt_t,
644
743
ctypes.POINTER(datum_t),
645
744
openpgp_crt_fmt_t]
646
745
openpgp_crt_import.restype = _error_code
648
747
openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
649
748
openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
650
749
ctypes.POINTER(ctypes.c_uint)]
651
750
openpgp_crt_verify_self.restype = _error_code
653
752
openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
654
753
openpgp_crt_deinit.argtypes = [openpgp_crt_t]
655
754
openpgp_crt_deinit.restype = None
657
756
openpgp_crt_get_fingerprint = (
658
757
_library.gnutls_openpgp_crt_get_fingerprint)
659
758
openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
687
788
checker: subprocess.Popen(); a running checker process used
688
789
to see if the client lives.
689
790
'None' if no process is running.
690
checker_callback_tag: a gobject event source tag, or None
791
checker_callback_tag: a GLib event source tag, or None
691
792
checker_command: string; External command which is run to check
692
793
if client lives. %() expansions are done at
693
794
runtime with vars(self) as dict, so that for
694
795
instance %(name)s can be used in the command.
695
checker_initiator_tag: a gobject event source tag, or None
796
checker_initiator_tag: a GLib event source tag, or None
696
797
created: datetime.datetime(); (UTC) object creation
697
798
client_structure: Object describing what attributes a client has
698
799
and is used for storing the client at exit
699
800
current_checker_command: string; current running checker_command
700
disable_initiator_tag: a gobject event source tag, or None
801
disable_initiator_tag: a GLib event source tag, or None
702
803
fingerprint: string (40 or 32 hexadecimal digits); used to
703
804
uniquely identify the client
858
961
logger.info("Disabling client %s", self.name)
859
962
if getattr(self, "disable_initiator_tag", None) is not None:
860
gobject.source_remove(self.disable_initiator_tag)
963
GLib.source_remove(self.disable_initiator_tag)
861
964
self.disable_initiator_tag = None
862
965
self.expires = None
863
966
if getattr(self, "checker_initiator_tag", None) is not None:
864
gobject.source_remove(self.checker_initiator_tag)
967
GLib.source_remove(self.checker_initiator_tag)
865
968
self.checker_initiator_tag = None
866
969
self.stop_checker()
867
970
self.enabled = False
869
972
self.send_changedstate()
870
# Do not run this again if called by a gobject.timeout_add
973
# Do not run this again if called by a GLib.timeout_add
873
976
def __del__(self):
876
979
def init_checker(self):
877
980
# Schedule a new checker to be started an 'interval' from now,
878
981
# and every interval from then on.
879
982
if self.checker_initiator_tag is not None:
880
gobject.source_remove(self.checker_initiator_tag)
881
self.checker_initiator_tag = gobject.timeout_add(
983
GLib.source_remove(self.checker_initiator_tag)
984
self.checker_initiator_tag = GLib.timeout_add(
882
985
int(self.interval.total_seconds() * 1000),
883
986
self.start_checker)
884
987
# Schedule a disable() when 'timeout' has passed
885
988
if self.disable_initiator_tag is not None:
886
gobject.source_remove(self.disable_initiator_tag)
887
self.disable_initiator_tag = gobject.timeout_add(
989
GLib.source_remove(self.disable_initiator_tag)
990
self.disable_initiator_tag = GLib.timeout_add(
888
991
int(self.timeout.total_seconds() * 1000), self.disable)
889
992
# Also start a new checker *right now*.
890
993
self.start_checker()
892
995
def checker_callback(self, source, condition, connection,
894
997
"""The checker has completed, so take appropriate actions."""
913
1016
logger.warning("Checker for %(name)s crashed?",
917
1020
def checked_ok(self):
918
1021
"""Assert that the client has been seen, alive and well."""
919
1022
self.last_checked_ok = datetime.datetime.utcnow()
920
1023
self.last_checker_status = 0
921
1024
self.last_checker_signal = None
922
1025
self.bump_timeout()
924
1027
def bump_timeout(self, timeout=None):
925
1028
"""Bump up the timeout for this client."""
926
1029
if timeout is None:
927
1030
timeout = self.timeout
928
1031
if self.disable_initiator_tag is not None:
929
gobject.source_remove(self.disable_initiator_tag)
1032
GLib.source_remove(self.disable_initiator_tag)
930
1033
self.disable_initiator_tag = None
931
1034
if getattr(self, "enabled", False):
932
self.disable_initiator_tag = gobject.timeout_add(
1035
self.disable_initiator_tag = GLib.timeout_add(
933
1036
int(timeout.total_seconds() * 1000), self.disable)
934
1037
self.expires = datetime.datetime.utcnow() + timeout
936
1039
def need_approval(self):
937
1040
self.last_approval_request = datetime.datetime.utcnow()
939
1042
def start_checker(self):
940
1043
"""Start a new checker subprocess if one is not running.
942
1045
If a checker already exists, leave it running and do
944
1047
# The reason for not killing a running checker is that if we
977
1080
# The exception is when not debugging but nevertheless
978
1081
# running in the foreground; use the previously
979
1082
# created wnull.
980
popen_args = { "close_fds": True,
1083
popen_args = {"close_fds": True,
983
1086
if (not self.server_settings["debug"]
984
1087
and self.server_settings["foreground"]):
985
1088
popen_args.update({"stdout": wnull,
987
pipe = multiprocessing.Pipe(duplex = False)
1090
pipe = multiprocessing.Pipe(duplex=False)
988
1091
self.checker = multiprocessing.Process(
990
args = (pipe[1], subprocess.call, command),
1093
args=(pipe[1], subprocess.call, command),
992
1095
self.checker.start()
993
self.checker_callback_tag = gobject.io_add_watch(
994
pipe[0].fileno(), gobject.IO_IN,
1096
self.checker_callback_tag = GLib.io_add_watch(
1097
pipe[0].fileno(), GLib.IO_IN,
995
1098
self.checker_callback, pipe[0], command)
996
# Re-run this periodically if run by gobject.timeout_add
1099
# Re-run this periodically if run by GLib.timeout_add
999
1102
def stop_checker(self):
1000
1103
"""Force the checker process, if any, to stop."""
1001
1104
if self.checker_callback_tag:
1002
gobject.source_remove(self.checker_callback_tag)
1105
GLib.source_remove(self.checker_callback_tag)
1003
1106
self.checker_callback_tag = None
1004
1107
if getattr(self, "checker", None) is None:
1036
1139
func._dbus_name = func.__name__
1037
1140
if func._dbus_name.endswith("_dbus_property"):
1038
1141
func._dbus_name = func._dbus_name[:-14]
1039
func._dbus_get_args_options = {'byte_arrays': byte_arrays }
1142
func._dbus_get_args_options = {'byte_arrays': byte_arrays}
1042
1145
return decorator
1045
1148
def dbus_interface_annotations(dbus_interface):
1046
1149
"""Decorator for marking functions returning interface annotations
1050
1153
@dbus_interface_annotations("org.example.Interface")
1051
1154
def _foo(self): # Function name does not matter
1052
1155
return {"org.freedesktop.DBus.Deprecated": "true",
1053
1156
"org.freedesktop.DBus.Property.EmitsChangedSignal":
1057
1160
def decorator(func):
1058
1161
func._dbus_is_interface = True
1059
1162
func._dbus_interface = dbus_interface
1060
1163
func._dbus_name = dbus_interface
1063
1166
return decorator
1066
1169
def dbus_annotations(annotations):
1067
1170
"""Decorator to annotate D-Bus methods, signals or properties
1070
1173
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true",
1071
1174
"org.freedesktop.DBus.Property."
1072
1175
"EmitsChangedSignal": "false"})
1359
1462
exc_info=error)
1360
1463
return xmlstring
1363
1467
dbus.OBJECT_MANAGER_IFACE
1364
1468
except AttributeError:
1365
1469
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
1367
1472
class DBusObjectWithObjectManager(DBusObjectWithAnnotations):
1368
1473
"""A D-Bus object with an ObjectManager.
1370
1475
Classes inheriting from this exposes the standard
1371
1476
GetManagedObjects call and the InterfacesAdded and
1372
1477
InterfacesRemoved signals on the standard
1373
1478
"org.freedesktop.DBus.ObjectManager" interface.
1375
1480
Note: No signals are sent automatically; they must be sent
1378
1483
@dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
1379
out_signature = "a{oa{sa{sv}}}")
1484
out_signature="a{oa{sa{sv}}}")
1380
1485
def GetManagedObjects(self):
1381
1486
"""This function must be overridden"""
1382
1487
raise NotImplementedError()
1384
1489
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE,
1385
signature = "oa{sa{sv}}")
1490
signature="oa{sa{sv}}")
1386
1491
def InterfacesAdded(self, object_path, interfaces_and_properties):
1389
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature = "oas")
1494
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature="oas")
1390
1495
def InterfacesRemoved(self, object_path, interfaces):
1393
1498
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1394
out_signature = "s",
1395
path_keyword = 'object_path',
1396
connection_keyword = 'connection')
1500
path_keyword='object_path',
1501
connection_keyword='connection')
1397
1502
def Introspect(self, object_path, connection):
1398
1503
"""Overloading of standard D-Bus method.
1400
1505
Override return argument name of GetManagedObjects to be
1401
1506
"objpath_interfaces_and_properties"
1479
1585
interface_names.add(alt_interface)
1480
1586
# Is this a D-Bus signal?
1481
1587
if getattr(attribute, "_dbus_is_signal", False):
1588
# Extract the original non-method undecorated
1589
# function by black magic
1482
1590
if sys.version_info.major == 2:
1483
# Extract the original non-method undecorated
1484
# function by black magic
1485
1591
nonmethod_func = (dict(
1486
1592
zip(attribute.func_code.co_freevars,
1487
1593
attribute.__closure__))
1488
1594
["func"].cell_contents)
1490
nonmethod_func = attribute
1596
nonmethod_func = (dict(
1597
zip(attribute.__code__.co_freevars,
1598
attribute.__closure__))
1599
["func"].cell_contents)
1491
1600
# Create a new, but exactly alike, function
1492
1601
# object, and decorate it to be a new D-Bus signal
1493
1602
# with the alternate D-Bus interface name
1494
if sys.version_info.major == 2:
1495
new_function = types.FunctionType(
1496
nonmethod_func.func_code,
1497
nonmethod_func.func_globals,
1498
nonmethod_func.func_name,
1499
nonmethod_func.func_defaults,
1500
nonmethod_func.func_closure)
1502
new_function = types.FunctionType(
1503
nonmethod_func.__code__,
1504
nonmethod_func.__globals__,
1505
nonmethod_func.__name__,
1506
nonmethod_func.__defaults__,
1507
nonmethod_func.__closure__)
1603
new_function = copy_function(nonmethod_func)
1508
1604
new_function = (dbus.service.signal(
1510
1606
attribute._dbus_signature)(new_function))
1681
1770
dbus_value = transform_func(
1682
1771
type_func(value),
1683
variant_level = variant_level)
1772
variant_level=variant_level)
1684
1773
self.PropertyChanged(dbus.String(dbus_name),
1686
1775
self.PropertiesChanged(
1688
dbus.Dictionary({ dbus.String(dbus_name):
1777
dbus.Dictionary({dbus.String(dbus_name):
1691
1780
setattr(self, attrname, value)
1693
1782
return property(lambda self: getattr(self, attrname), setter)
1695
1784
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1696
1785
approvals_pending = notifychangeproperty(dbus.Boolean,
1697
1786
"ApprovalPending",
1699
1788
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1700
1789
last_enabled = notifychangeproperty(datetime_to_dbus,
1702
1791
checker = notifychangeproperty(
1703
1792
dbus.Boolean, "CheckerRunning",
1704
type_func = lambda checker: checker is not None)
1793
type_func=lambda checker: checker is not None)
1705
1794
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1706
1795
"LastCheckedOK")
1707
1796
last_checker_status = notifychangeproperty(dbus.Int16,
1712
1801
"ApprovedByDefault")
1713
1802
approval_delay = notifychangeproperty(
1714
1803
dbus.UInt64, "ApprovalDelay",
1715
type_func = lambda td: td.total_seconds() * 1000)
1804
type_func=lambda td: td.total_seconds() * 1000)
1716
1805
approval_duration = notifychangeproperty(
1717
1806
dbus.UInt64, "ApprovalDuration",
1718
type_func = lambda td: td.total_seconds() * 1000)
1807
type_func=lambda td: td.total_seconds() * 1000)
1719
1808
host = notifychangeproperty(dbus.String, "Host")
1720
1809
timeout = notifychangeproperty(
1721
1810
dbus.UInt64, "Timeout",
1722
type_func = lambda td: td.total_seconds() * 1000)
1811
type_func=lambda td: td.total_seconds() * 1000)
1723
1812
extended_timeout = notifychangeproperty(
1724
1813
dbus.UInt64, "ExtendedTimeout",
1725
type_func = lambda td: td.total_seconds() * 1000)
1814
type_func=lambda td: td.total_seconds() * 1000)
1726
1815
interval = notifychangeproperty(
1727
1816
dbus.UInt64, "Interval",
1728
type_func = lambda td: td.total_seconds() * 1000)
1817
type_func=lambda td: td.total_seconds() * 1000)
1729
1818
checker_command = notifychangeproperty(dbus.String, "Checker")
1730
1819
secret = notifychangeproperty(dbus.ByteArray, "Secret",
1731
1820
invalidate_only=True)
1733
1822
del notifychangeproperty
1735
1824
def __del__(self, *args, **kwargs):
1737
1826
self.remove_from_connection()
1772
1861
# Emit D-Bus signal
1773
1862
self.CheckerStarted(self.current_checker_command)
1776
1865
def _reset_approved(self):
1777
1866
self.approved = None
1780
1869
def approve(self, value=True):
1781
1870
self.approved = value
1782
gobject.timeout_add(int(self.approval_duration.total_seconds()
1783
* 1000), self._reset_approved)
1871
GLib.timeout_add(int(self.approval_duration.total_seconds()
1872
* 1000), self._reset_approved)
1784
1873
self.send_changedstate()
1786
## D-Bus methods, signals & properties
1875
# D-Bus methods, signals & properties
1792
1881
# CheckerCompleted - signal
1793
1882
@dbus.service.signal(_interface, signature="nxs")
1794
1883
def CheckerCompleted(self, exitcode, waitstatus, command):
1798
1887
# CheckerStarted - signal
1799
1888
@dbus.service.signal(_interface, signature="s")
1800
1889
def CheckerStarted(self, command):
1804
1893
# PropertyChanged - signal
1805
1894
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1806
1895
@dbus.service.signal(_interface, signature="sv")
1807
1896
def PropertyChanged(self, property, value):
1811
1900
# GotSecret - signal
1812
1901
@dbus.service.signal(_interface)
1813
1902
def GotSecret(self):
1816
1905
server to mandos-client
1820
1909
# Rejected - signal
1821
1910
@dbus.service.signal(_interface, signature="s")
1822
1911
def Rejected(self, reason):
1826
1915
# NeedApproval - signal
1827
1916
@dbus.service.signal(_interface, signature="tb")
1828
1917
def NeedApproval(self, timeout, default):
1830
1919
return self.need_approval()
1834
1923
# Approve - method
1835
1924
@dbus.service.method(_interface, in_signature="b")
1836
1925
def Approve(self, value):
1837
1926
self.approve(value)
1839
1928
# CheckedOK - method
1840
1929
@dbus.service.method(_interface)
1841
1930
def CheckedOK(self):
1842
1931
self.checked_ok()
1844
1933
# Enable - method
1845
1934
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1846
1935
@dbus.service.method(_interface)
1847
1936
def Enable(self):
1851
1940
# StartChecker - method
1852
1941
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1853
1942
@dbus.service.method(_interface)
1854
1943
def StartChecker(self):
1856
1945
self.start_checker()
1858
1947
# Disable - method
1859
1948
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1860
1949
@dbus.service.method(_interface)
1861
1950
def Disable(self):
1865
1954
# StopChecker - method
1866
1955
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1867
1956
@dbus.service.method(_interface)
1868
1957
def StopChecker(self):
1869
1958
self.stop_checker()
1873
1962
# ApprovalPending - property
1874
1963
@dbus_service_property(_interface, signature="b", access="read")
1875
1964
def ApprovalPending_dbus_property(self):
1876
1965
return dbus.Boolean(bool(self.approvals_pending))
1878
1967
# ApprovedByDefault - property
1879
1968
@dbus_service_property(_interface,
2102
2191
class ClientHandler(socketserver.BaseRequestHandler, object):
2103
2192
"""A class to handle client connections.
2105
2194
Instantiated once for each connection to handle it.
2106
2195
Note: This will run in its own forked process."""
2108
2197
def handle(self):
2109
2198
with contextlib.closing(self.server.child_pipe) as child_pipe:
2110
2199
logger.info("TCP connection from: %s",
2111
2200
str(self.client_address))
2112
2201
logger.debug("Pipe FD: %d",
2113
2202
self.server.child_pipe.fileno())
2115
2204
session = gnutls.ClientSession(self.request)
2117
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
2118
# "+AES-256-CBC", "+SHA1",
2119
# "+COMP-NULL", "+CTYPE-OPENPGP",
2206
# priority = ':'.join(("NONE", "+VERS-TLS1.1",
2207
# "+AES-256-CBC", "+SHA1",
2208
# "+COMP-NULL", "+CTYPE-OPENPGP",
2121
2210
# Use a fallback default, since this MUST be set.
2122
2211
priority = self.server.gnutls_priority
2123
2212
if priority is None:
2124
2213
priority = "NORMAL"
2125
gnutls.priority_set_direct(session._c_object, priority,
2214
gnutls.priority_set_direct(session._c_object,
2215
priority.encode("utf-8"),
2128
2218
# Start communication using the Mandos protocol
2129
2219
# Get protocol number
2130
2220
line = self.request.makefile().readline()
2301
2385
class MultiprocessingMixIn(object):
2302
2386
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
2304
2388
def sub_process_main(self, request, address):
2306
2390
self.finish_request(request, address)
2307
2391
except Exception:
2308
2392
self.handle_error(request, address)
2309
2393
self.close_request(request)
2311
2395
def process_request(self, request, address):
2312
2396
"""Start a new process to process the request."""
2313
proc = multiprocessing.Process(target = self.sub_process_main,
2314
args = (request, address))
2397
proc = multiprocessing.Process(target=self.sub_process_main,
2398
args=(request, address))
2319
2403
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
2320
2404
""" adds a pipe to the MixIn """
2322
2406
def process_request(self, request, client_address):
2323
2407
"""Overrides and wraps the original process_request().
2325
2409
This function creates a new pipe in self.pipe
2327
2411
parent_pipe, self.child_pipe = multiprocessing.Pipe()
2329
2413
proc = MultiprocessingMixIn.process_request(self, request,
2330
2414
client_address)
2331
2415
self.child_pipe.close()
2332
2416
self.add_pipe(parent_pipe, proc)
2334
2418
def add_pipe(self, parent_pipe, proc):
2335
2419
"""Dummy function; override as necessary"""
2336
2420
raise NotImplementedError()
2384
2469
# socket_wrapper(), if socketfd was set.
2385
2470
socketserver.TCPServer.__init__(self, server_address,
2386
2471
RequestHandlerClass)
2388
2473
def server_bind(self):
2389
2474
"""This overrides the normal server_bind() function
2390
2475
to bind to an interface if one was specified, and also NOT to
2391
2476
bind to an address or port if they were not specified."""
2477
global SO_BINDTODEVICE
2392
2478
if self.interface is not None:
2393
2479
if SO_BINDTODEVICE is None:
2394
logger.error("SO_BINDTODEVICE does not exist;"
2395
" cannot bind to interface %s",
2399
self.socket.setsockopt(
2400
socket.SOL_SOCKET, SO_BINDTODEVICE,
2401
(self.interface + "\0").encode("utf-8"))
2402
except socket.error as error:
2403
if error.errno == errno.EPERM:
2404
logger.error("No permission to bind to"
2405
" interface %s", self.interface)
2406
elif error.errno == errno.ENOPROTOOPT:
2407
logger.error("SO_BINDTODEVICE not available;"
2408
" cannot bind to interface %s",
2410
elif error.errno == errno.ENODEV:
2411
logger.error("Interface %s does not exist,"
2412
" cannot bind", self.interface)
2480
# Fall back to a hard-coded value which seems to be
2482
logger.warning("SO_BINDTODEVICE not found, trying 25")
2483
SO_BINDTODEVICE = 25
2485
self.socket.setsockopt(
2486
socket.SOL_SOCKET, SO_BINDTODEVICE,
2487
(self.interface + "\0").encode("utf-8"))
2488
except socket.error as error:
2489
if error.errno == errno.EPERM:
2490
logger.error("No permission to bind to"
2491
" interface %s", self.interface)
2492
elif error.errno == errno.ENOPROTOOPT:
2493
logger.error("SO_BINDTODEVICE not available;"
2494
" cannot bind to interface %s",
2496
elif error.errno == errno.ENODEV:
2497
logger.error("Interface %s does not exist,"
2498
" cannot bind", self.interface)
2415
2501
# Only bind(2) the socket if we really need to.
2416
2502
if self.server_address[0] or self.server_address[1]:
2417
2503
if not self.server_address[0]:
2418
2504
if self.address_family == socket.AF_INET6:
2419
any_address = "::" # in6addr_any
2505
any_address = "::" # in6addr_any
2421
any_address = "0.0.0.0" # INADDR_ANY
2507
any_address = "0.0.0.0" # INADDR_ANY
2422
2508
self.server_address = (any_address,
2423
2509
self.server_address[1])
2424
2510
elif not self.server_address[1]:
2458
2544
self.gnutls_priority = gnutls_priority
2459
2545
IPv6_TCPServer.__init__(self, server_address,
2460
2546
RequestHandlerClass,
2461
interface = interface,
2462
use_ipv6 = use_ipv6,
2463
socketfd = socketfd)
2547
interface=interface,
2465
2551
def server_activate(self):
2466
2552
if self.enabled:
2467
2553
return socketserver.TCPServer.server_activate(self)
2469
2555
def enable(self):
2470
2556
self.enabled = True
2472
2558
def add_pipe(self, parent_pipe, proc):
2473
2559
# Call "handle_ipc" for both data and EOF events
2474
gobject.io_add_watch(
2475
2561
parent_pipe.fileno(),
2476
gobject.IO_IN | gobject.IO_HUP,
2562
GLib.IO_IN | GLib.IO_HUP,
2477
2563
functools.partial(self.handle_ipc,
2478
parent_pipe = parent_pipe,
2564
parent_pipe=parent_pipe,
2481
2567
def handle_ipc(self, source, condition,
2482
2568
parent_pipe=None,
2484
2570
client_object=None):
2485
2571
# error, or the other end of multiprocessing.Pipe has closed
2486
if condition & (gobject.IO_ERR | gobject.IO_HUP):
2572
if condition & (GLib.IO_ERR | GLib.IO_HUP):
2487
2573
# Wait for other process to exit
2491
2577
# Read a request from the child
2492
2578
request = parent_pipe.recv()
2493
2579
command = request[0]
2495
2581
if command == 'init':
2582
fpr = request[1].decode("ascii")
2497
2583
address = request[2]
2499
for c in self.clients.itervalues():
2585
for c in self.clients.values():
2500
2586
if c.fingerprint == fpr:
3009
logger.debug("Did setuid/setgid to {}:{}".format(uid,
2916
3011
except OSError as error:
3012
logger.warning("Failed to setuid/setgid to {}:{}: {}"
3013
.format(uid, gid, os.strerror(error.errno)))
2917
3014
if error.errno != errno.EPERM:
2921
3018
# Enable all possible GnuTLS debugging
2923
3020
# "Use a log level over 10 to enable all debugging options."
2924
3021
# - GnuTLS manual
2925
3022
gnutls.global_set_log_level(11)
2927
3024
@gnutls.log_func
2928
3025
def debug_gnutls(level, string):
2929
3026
logger.debug("GnuTLS: %s", string[:-1])
2931
3028
gnutls.global_set_log_function(debug_gnutls)
2933
3030
# Redirect stdin so all checkers get /dev/null
2934
3031
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2935
3032
os.dup2(null, sys.stdin.fileno())
2939
3036
# Need to fork before connecting to D-Bus
2940
3037
if not foreground:
2941
3038
# Close all input and output, do double fork, etc.
2944
# multiprocessing will use threads, so before we use gobject we
2945
# need to inform gobject that threads will be used.
2946
gobject.threads_init()
3041
# multiprocessing will use threads, so before we use GLib we need
3042
# to inform GLib that threads will be used.
2948
3045
global main_loop
2949
3046
# From the Avahi example code
2950
3047
DBusGMainLoop(set_as_default=True)
2951
main_loop = gobject.MainLoop()
3048
main_loop = GLib.MainLoop()
2952
3049
bus = dbus.SystemBus()
2953
3050
# End of Avahi example code
2968
3065
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2969
3066
service = AvahiServiceToSyslog(
2970
name = server_settings["servicename"],
2971
servicetype = "_mandos._tcp",
2972
protocol = protocol,
3067
name=server_settings["servicename"],
3068
servicetype="_mandos._tcp",
2974
3071
if server_settings["interface"]:
2975
3072
service.interface = if_nametoindex(
2976
3073
server_settings["interface"].encode("utf-8"))
2978
3075
global multiprocessing_manager
2979
3076
multiprocessing_manager = multiprocessing.Manager()
2981
3078
client_class = Client
2983
client_class = functools.partial(ClientDBus, bus = bus)
3080
client_class = functools.partial(ClientDBus, bus=bus)
2985
3082
client_settings = Client.config_parser(client_config)
2986
3083
old_client_settings = {}
2987
3084
clients_data = {}
2989
3086
# This is used to redirect stdout and stderr for checker processes
2991
wnull = open(os.devnull, "w") # A writable /dev/null
3088
wnull = open(os.devnull, "w") # A writable /dev/null
2992
3089
# Only used if server is running in foreground but not in debug
2994
3091
if debug or not foreground:
2997
3094
# Get client data and settings from last running state.
2998
3095
if server_settings["restore"]:
3000
3097
with open(stored_state_path, "rb") as stored_state:
3001
clients_data, old_client_settings = pickle.load(
3098
if sys.version_info.major == 2:
3099
clients_data, old_client_settings = pickle.load(
3102
bytes_clients_data, bytes_old_client_settings = (
3103
pickle.load(stored_state, encoding="bytes"))
3104
# Fix bytes to strings
3107
clients_data = {(key.decode("utf-8")
3108
if isinstance(key, bytes)
3111
bytes_clients_data.items()}
3112
del bytes_clients_data
3113
for key in clients_data:
3114
value = {(k.decode("utf-8")
3115
if isinstance(k, bytes) else k): v
3117
clients_data[key].items()}
3118
clients_data[key] = value
3120
value["client_structure"] = [
3122
if isinstance(s, bytes)
3124
value["client_structure"]]
3126
for k in ("name", "host"):
3127
if isinstance(value[k], bytes):
3128
value[k] = value[k].decode("utf-8")
3129
# old_client_settings
3131
old_client_settings = {
3132
(key.decode("utf-8")
3133
if isinstance(key, bytes)
3136
bytes_old_client_settings.items()}
3137
del bytes_old_client_settings
3139
for value in old_client_settings.values():
3140
if isinstance(value["host"], bytes):
3141
value["host"] = (value["host"]
3003
3143
os.remove(stored_state_path)
3004
3144
except IOError as e:
3005
3145
if e.errno == errno.ENOENT:
3105
3245
pidfilename, pid)
3107
3247
del pidfilename
3109
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
3110
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
3249
for termsig in (signal.SIGHUP, signal.SIGTERM):
3250
GLib.unix_signal_add(GLib.PRIORITY_HIGH, termsig,
3251
lambda: main_loop.quit() and False)
3114
3255
@alternate_dbus_interfaces(
3115
{ "se.recompile.Mandos": "se.bsnet.fukt.Mandos" })
3256
{"se.recompile.Mandos": "se.bsnet.fukt.Mandos"})
3116
3257
class MandosDBusService(DBusObjectWithObjectManager):
3117
3258
"""A D-Bus proxy object"""
3119
3260
def __init__(self):
3120
3261
dbus.service.Object.__init__(self, bus, "/")
3122
3263
_interface = "se.recompile.Mandos"
3124
3265
@dbus.service.signal(_interface, signature="o")
3125
3266
def ClientAdded(self, objpath):
3129
3270
@dbus.service.signal(_interface, signature="ss")
3130
3271
def ClientNotFound(self, fingerprint, address):
3134
3275
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3136
3277
@dbus.service.signal(_interface, signature="os")
3137
3278
def ClientRemoved(self, objpath, name):
3141
3282
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3143
3284
@dbus.service.method(_interface, out_signature="ao")
3144
3285
def GetAllClients(self):
3146
3287
return dbus.Array(c.dbus_object_path for c in
3147
tcp_server.clients.itervalues())
3288
tcp_server.clients.values())
3149
3290
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3151
3292
@dbus.service.method(_interface,
3210
3351
self.ClientRemoved(client.dbus_object_path,
3213
3354
mandos_dbus_service = MandosDBusService()
3356
# Save modules to variables to exempt the modules from being
3357
# unloaded before the function registered with atexit() is run.
3358
mp = multiprocessing
3216
3362
"Cleanup function; run on exit"
3218
3364
service.cleanup()
3220
multiprocessing.active_children()
3366
mp.active_children()
3222
3368
if not (tcp_server.clients or client_settings):
3225
3371
# Store client before exiting. Secrets are encrypted with key
3226
3372
# based on what config file has. If config file is
3227
3373
# removed/edited, old secret will thus be unrecovable.
3229
3375
with PGPEngine() as pgp:
3230
for client in tcp_server.clients.itervalues():
3376
for client in tcp_server.clients.values():
3231
3377
key = client_settings[client.name]["secret"]
3232
3378
client.encrypted_secret = pgp.encrypt(client.secret,
3234
3380
client_dict = {}
3236
3382
# A list of attributes that can not be pickled
3238
exclude = { "bus", "changedstate", "secret",
3239
"checker", "server_settings" }
3384
exclude = {"bus", "changedstate", "secret",
3385
"checker", "server_settings"}
3240
3386
for name, typ in inspect.getmembers(dbus.service
3242
3388
exclude.add(name)
3244
3390
client_dict["encrypted_secret"] = (client
3245
3391
.encrypted_secret)
3246
3392
for attr in client.client_structure:
3247
3393
if attr not in exclude:
3248
3394
client_dict[attr] = getattr(client, attr)
3250
3396
clients[client.name] = client_dict
3251
3397
del client_settings[client.name]["secret"]
3254
3400
with tempfile.NamedTemporaryFile(