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
429
502
.format(self.name)))
432
506
# Pretend that we have a GnuTLS module
433
507
class GnuTLS(object):
434
508
"""This isn't so much a class as it is a module-like namespace.
435
509
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"
511
library = ctypes.util.find_library("gnutls")
513
library = ctypes.util.find_library("gnutls-deb0")
514
_library = ctypes.cdll.LoadLibrary(library)
516
_need_version = b"3.3.0"
440
518
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))
519
# Need to use "self" here, since this method is called before
520
# the assignment to the "gnutls" global variable happens.
521
if self.check_version(self._need_version) is None:
522
raise self.Error("Needs GnuTLS {} or later"
523
.format(self._need_version))
448
525
# Unless otherwise indicated, the constants and types below are
449
526
# all from the gnutls/gnutls.h C header file.
456
535
CRD_CERTIFICATE = 1
457
536
E_NO_CERTIFICATE_FOUND = -49
458
537
OPENPGP_FMT_RAW = 0 # gnutls/openpgp.h
461
540
class session_int(ctypes.Structure):
463
542
session_t = ctypes.POINTER(session_int)
464
544
class certificate_credentials_st(ctypes.Structure):
466
546
certificate_credentials_t = ctypes.POINTER(
467
547
certificate_credentials_st)
468
548
certificate_type_t = ctypes.c_int
469
550
class datum_t(ctypes.Structure):
470
551
_fields_ = [('data', ctypes.POINTER(ctypes.c_ubyte)),
471
552
('size', ctypes.c_uint)]
472
554
class openpgp_crt_int(ctypes.Structure):
474
556
openpgp_crt_t = ctypes.POINTER(openpgp_crt_int)
475
openpgp_crt_fmt_t = ctypes.c_int # gnutls/openpgp.h
557
openpgp_crt_fmt_t = ctypes.c_int # gnutls/openpgp.h
476
558
log_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
477
credentials_type_t = ctypes.c_int #
559
credentials_type_t = ctypes.c_int
478
560
transport_ptr_t = ctypes.c_void_p
479
561
close_request_t = ctypes.c_int
482
564
class Error(Exception):
483
565
# We need to use the class name "GnuTLS" here, since this
484
566
# exception might be raised from within GnuTLS.__init__,
485
567
# which is called before the assignment to the "gnutls"
486
# global variable happens.
487
def __init__(self, message = None, code = None, args=()):
568
# global variable has happened.
569
def __init__(self, message=None, code=None, args=()):
488
570
# Default usage is by a message string, but if a return
489
571
# code is passed, convert it to a string with
490
572
# gnutls.strerror()
491
574
if message is None and code is not None:
492
575
message = GnuTLS.strerror(code)
493
576
return super(GnuTLS.Error, self).__init__(
496
579
class CertificateSecurityError(Error):
500
583
class Credentials(object):
501
584
def __init__(self):
522
605
ctypes.cast(credentials._c_object,
523
606
ctypes.c_void_p))
524
607
self.credentials = credentials
526
609
def __del__(self):
527
610
gnutls.deinit(self._c_object)
529
612
def handshake(self):
530
613
return gnutls.handshake(self._c_object)
532
615
def send(self, data):
533
616
data = bytes(data)
536
return gnutls.record_send(self._c_object, data, len(data))
619
data_len -= gnutls.record_send(self._c_object,
539
624
return gnutls.bye(self._c_object, gnutls.SHUT_RDWR)
541
# Error handling function
626
# Error handling functions
542
627
def _error_code(result):
543
628
"""A function to raise exceptions on errors, suitable
544
629
for the 'restype' attribute on ctypes functions"""
547
632
if result == gnutls.E_NO_CERTIFICATE_FOUND:
548
raise gnutls.CertificateSecurityError(code = result)
549
raise gnutls.Error(code = result)
633
raise gnutls.CertificateSecurityError(code=result)
634
raise gnutls.Error(code=result)
636
def _retry_on_error(result, func, arguments):
637
"""A function to retry on some errors, suitable
638
for the 'errcheck' attribute on ctypes functions"""
640
if result not in (gnutls.E_INTERRUPTED, gnutls.E_AGAIN):
641
return _error_code(result)
642
result = func(*arguments)
551
645
# Unless otherwise indicated, the function declarations below are
552
646
# all from the gnutls/gnutls.h C header file.
555
649
priority_set_direct = _library.gnutls_priority_set_direct
556
650
priority_set_direct.argtypes = [session_t, ctypes.c_char_p,
557
651
ctypes.POINTER(ctypes.c_char_p)]
558
652
priority_set_direct.restype = _error_code
560
654
init = _library.gnutls_init
561
655
init.argtypes = [ctypes.POINTER(session_t), ctypes.c_int]
562
656
init.restype = _error_code
564
658
set_default_priority = _library.gnutls_set_default_priority
565
659
set_default_priority.argtypes = [session_t]
566
660
set_default_priority.restype = _error_code
568
662
record_send = _library.gnutls_record_send
569
663
record_send.argtypes = [session_t, ctypes.c_void_p,
571
665
record_send.restype = ctypes.c_ssize_t
666
record_send.errcheck = _retry_on_error
573
668
certificate_allocate_credentials = (
574
669
_library.gnutls_certificate_allocate_credentials)
575
670
certificate_allocate_credentials.argtypes = [
576
671
ctypes.POINTER(certificate_credentials_t)]
577
672
certificate_allocate_credentials.restype = _error_code
579
674
certificate_free_credentials = (
580
675
_library.gnutls_certificate_free_credentials)
581
certificate_free_credentials.argtypes = [certificate_credentials_t]
676
certificate_free_credentials.argtypes = [
677
certificate_credentials_t]
582
678
certificate_free_credentials.restype = None
584
680
handshake_set_private_extensions = (
585
681
_library.gnutls_handshake_set_private_extensions)
586
682
handshake_set_private_extensions.argtypes = [session_t,
588
684
handshake_set_private_extensions.restype = None
590
686
credentials_set = _library.gnutls_credentials_set
591
687
credentials_set.argtypes = [session_t, credentials_type_t,
593
689
credentials_set.restype = _error_code
595
691
strerror = _library.gnutls_strerror
596
692
strerror.argtypes = [ctypes.c_int]
597
693
strerror.restype = ctypes.c_char_p
599
695
certificate_type_get = _library.gnutls_certificate_type_get
600
696
certificate_type_get.argtypes = [session_t]
601
697
certificate_type_get.restype = _error_code
603
699
certificate_get_peers = _library.gnutls_certificate_get_peers
604
700
certificate_get_peers.argtypes = [session_t,
605
701
ctypes.POINTER(ctypes.c_uint)]
606
702
certificate_get_peers.restype = ctypes.POINTER(datum_t)
608
704
global_set_log_level = _library.gnutls_global_set_log_level
609
705
global_set_log_level.argtypes = [ctypes.c_int]
610
706
global_set_log_level.restype = None
612
708
global_set_log_function = _library.gnutls_global_set_log_function
613
709
global_set_log_function.argtypes = [log_func]
614
710
global_set_log_function.restype = None
616
712
deinit = _library.gnutls_deinit
617
713
deinit.argtypes = [session_t]
618
714
deinit.restype = None
620
716
handshake = _library.gnutls_handshake
621
717
handshake.argtypes = [session_t]
622
718
handshake.restype = _error_code
719
handshake.errcheck = _retry_on_error
624
721
transport_set_ptr = _library.gnutls_transport_set_ptr
625
722
transport_set_ptr.argtypes = [session_t, transport_ptr_t]
626
723
transport_set_ptr.restype = None
628
725
bye = _library.gnutls_bye
629
726
bye.argtypes = [session_t, close_request_t]
630
727
bye.restype = _error_code
728
bye.errcheck = _retry_on_error
632
730
check_version = _library.gnutls_check_version
633
731
check_version.argtypes = [ctypes.c_char_p]
634
732
check_version.restype = ctypes.c_char_p
636
734
# All the function declarations below are from gnutls/openpgp.h
638
736
openpgp_crt_init = _library.gnutls_openpgp_crt_init
639
737
openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
640
738
openpgp_crt_init.restype = _error_code
642
740
openpgp_crt_import = _library.gnutls_openpgp_crt_import
643
741
openpgp_crt_import.argtypes = [openpgp_crt_t,
644
742
ctypes.POINTER(datum_t),
645
743
openpgp_crt_fmt_t]
646
744
openpgp_crt_import.restype = _error_code
648
746
openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
649
747
openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
650
748
ctypes.POINTER(ctypes.c_uint)]
651
749
openpgp_crt_verify_self.restype = _error_code
653
751
openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
654
752
openpgp_crt_deinit.argtypes = [openpgp_crt_t]
655
753
openpgp_crt_deinit.restype = None
657
755
openpgp_crt_get_fingerprint = (
658
756
_library.gnutls_openpgp_crt_get_fingerprint)
659
757
openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
687
787
checker: subprocess.Popen(); a running checker process used
688
788
to see if the client lives.
689
789
'None' if no process is running.
690
checker_callback_tag: a gobject event source tag, or None
790
checker_callback_tag: a GLib event source tag, or None
691
791
checker_command: string; External command which is run to check
692
792
if client lives. %() expansions are done at
693
793
runtime with vars(self) as dict, so that for
694
794
instance %(name)s can be used in the command.
695
checker_initiator_tag: a gobject event source tag, or None
795
checker_initiator_tag: a GLib event source tag, or None
696
796
created: datetime.datetime(); (UTC) object creation
697
797
client_structure: Object describing what attributes a client has
698
798
and is used for storing the client at exit
699
799
current_checker_command: string; current running checker_command
700
disable_initiator_tag: a gobject event source tag, or None
800
disable_initiator_tag: a GLib event source tag, or None
702
802
fingerprint: string (40 or 32 hexadecimal digits); used to
703
803
uniquely identify the client
858
960
logger.info("Disabling client %s", self.name)
859
961
if getattr(self, "disable_initiator_tag", None) is not None:
860
gobject.source_remove(self.disable_initiator_tag)
962
GLib.source_remove(self.disable_initiator_tag)
861
963
self.disable_initiator_tag = None
862
964
self.expires = None
863
965
if getattr(self, "checker_initiator_tag", None) is not None:
864
gobject.source_remove(self.checker_initiator_tag)
966
GLib.source_remove(self.checker_initiator_tag)
865
967
self.checker_initiator_tag = None
866
968
self.stop_checker()
867
969
self.enabled = False
869
971
self.send_changedstate()
870
# Do not run this again if called by a gobject.timeout_add
972
# Do not run this again if called by a GLib.timeout_add
873
975
def __del__(self):
876
978
def init_checker(self):
877
979
# Schedule a new checker to be started an 'interval' from now,
878
980
# and every interval from then on.
879
981
if self.checker_initiator_tag is not None:
880
gobject.source_remove(self.checker_initiator_tag)
881
self.checker_initiator_tag = gobject.timeout_add(
982
GLib.source_remove(self.checker_initiator_tag)
983
self.checker_initiator_tag = GLib.timeout_add(
882
984
int(self.interval.total_seconds() * 1000),
883
985
self.start_checker)
884
986
# Schedule a disable() when 'timeout' has passed
885
987
if self.disable_initiator_tag is not None:
886
gobject.source_remove(self.disable_initiator_tag)
887
self.disable_initiator_tag = gobject.timeout_add(
988
GLib.source_remove(self.disable_initiator_tag)
989
self.disable_initiator_tag = GLib.timeout_add(
888
990
int(self.timeout.total_seconds() * 1000), self.disable)
889
991
# Also start a new checker *right now*.
890
992
self.start_checker()
892
994
def checker_callback(self, source, condition, connection,
894
996
"""The checker has completed, so take appropriate actions."""
913
1015
logger.warning("Checker for %(name)s crashed?",
917
1019
def checked_ok(self):
918
1020
"""Assert that the client has been seen, alive and well."""
919
1021
self.last_checked_ok = datetime.datetime.utcnow()
920
1022
self.last_checker_status = 0
921
1023
self.last_checker_signal = None
922
1024
self.bump_timeout()
924
1026
def bump_timeout(self, timeout=None):
925
1027
"""Bump up the timeout for this client."""
926
1028
if timeout is None:
927
1029
timeout = self.timeout
928
1030
if self.disable_initiator_tag is not None:
929
gobject.source_remove(self.disable_initiator_tag)
1031
GLib.source_remove(self.disable_initiator_tag)
930
1032
self.disable_initiator_tag = None
931
1033
if getattr(self, "enabled", False):
932
self.disable_initiator_tag = gobject.timeout_add(
1034
self.disable_initiator_tag = GLib.timeout_add(
933
1035
int(timeout.total_seconds() * 1000), self.disable)
934
1036
self.expires = datetime.datetime.utcnow() + timeout
936
1038
def need_approval(self):
937
1039
self.last_approval_request = datetime.datetime.utcnow()
939
1041
def start_checker(self):
940
1042
"""Start a new checker subprocess if one is not running.
942
1044
If a checker already exists, leave it running and do
944
1046
# The reason for not killing a running checker is that if we
977
1079
# The exception is when not debugging but nevertheless
978
1080
# running in the foreground; use the previously
979
1081
# created wnull.
980
popen_args = { "close_fds": True,
1082
popen_args = {"close_fds": True,
983
1085
if (not self.server_settings["debug"]
984
1086
and self.server_settings["foreground"]):
985
1087
popen_args.update({"stdout": wnull,
987
pipe = multiprocessing.Pipe(duplex = False)
1089
pipe = multiprocessing.Pipe(duplex=False)
988
1090
self.checker = multiprocessing.Process(
990
args = (pipe[1], subprocess.call, command),
1092
args=(pipe[1], subprocess.call, command),
992
1094
self.checker.start()
993
self.checker_callback_tag = gobject.io_add_watch(
994
pipe[0].fileno(), gobject.IO_IN,
1095
self.checker_callback_tag = GLib.io_add_watch(
1096
pipe[0].fileno(), GLib.IO_IN,
995
1097
self.checker_callback, pipe[0], command)
996
# Re-run this periodically if run by gobject.timeout_add
1098
# Re-run this periodically if run by GLib.timeout_add
999
1101
def stop_checker(self):
1000
1102
"""Force the checker process, if any, to stop."""
1001
1103
if self.checker_callback_tag:
1002
gobject.source_remove(self.checker_callback_tag)
1104
GLib.source_remove(self.checker_callback_tag)
1003
1105
self.checker_callback_tag = None
1004
1106
if getattr(self, "checker", None) is None:
1036
1138
func._dbus_name = func.__name__
1037
1139
if func._dbus_name.endswith("_dbus_property"):
1038
1140
func._dbus_name = func._dbus_name[:-14]
1039
func._dbus_get_args_options = {'byte_arrays': byte_arrays }
1141
func._dbus_get_args_options = {'byte_arrays': byte_arrays}
1042
1144
return decorator
1045
1147
def dbus_interface_annotations(dbus_interface):
1046
1148
"""Decorator for marking functions returning interface annotations
1050
1152
@dbus_interface_annotations("org.example.Interface")
1051
1153
def _foo(self): # Function name does not matter
1052
1154
return {"org.freedesktop.DBus.Deprecated": "true",
1053
1155
"org.freedesktop.DBus.Property.EmitsChangedSignal":
1057
1159
def decorator(func):
1058
1160
func._dbus_is_interface = True
1059
1161
func._dbus_interface = dbus_interface
1060
1162
func._dbus_name = dbus_interface
1063
1165
return decorator
1066
1168
def dbus_annotations(annotations):
1067
1169
"""Decorator to annotate D-Bus methods, signals or properties
1070
1172
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true",
1071
1173
"org.freedesktop.DBus.Property."
1072
1174
"EmitsChangedSignal": "false"})
1359
1461
exc_info=error)
1360
1462
return xmlstring
1363
1466
dbus.OBJECT_MANAGER_IFACE
1364
1467
except AttributeError:
1365
1468
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
1367
1471
class DBusObjectWithObjectManager(DBusObjectWithAnnotations):
1368
1472
"""A D-Bus object with an ObjectManager.
1370
1474
Classes inheriting from this exposes the standard
1371
1475
GetManagedObjects call and the InterfacesAdded and
1372
1476
InterfacesRemoved signals on the standard
1373
1477
"org.freedesktop.DBus.ObjectManager" interface.
1375
1479
Note: No signals are sent automatically; they must be sent
1378
1482
@dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
1379
out_signature = "a{oa{sa{sv}}}")
1483
out_signature="a{oa{sa{sv}}}")
1380
1484
def GetManagedObjects(self):
1381
1485
"""This function must be overridden"""
1382
1486
raise NotImplementedError()
1384
1488
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE,
1385
signature = "oa{sa{sv}}")
1489
signature="oa{sa{sv}}")
1386
1490
def InterfacesAdded(self, object_path, interfaces_and_properties):
1389
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature = "oas")
1493
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature="oas")
1390
1494
def InterfacesRemoved(self, object_path, interfaces):
1393
1497
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1394
out_signature = "s",
1395
path_keyword = 'object_path',
1396
connection_keyword = 'connection')
1499
path_keyword='object_path',
1500
connection_keyword='connection')
1397
1501
def Introspect(self, object_path, connection):
1398
1502
"""Overloading of standard D-Bus method.
1400
1504
Override return argument name of GetManagedObjects to be
1401
1505
"objpath_interfaces_and_properties"
1479
1584
interface_names.add(alt_interface)
1480
1585
# Is this a D-Bus signal?
1481
1586
if getattr(attribute, "_dbus_is_signal", False):
1587
# Extract the original non-method undecorated
1588
# function by black magic
1482
1589
if sys.version_info.major == 2:
1483
# Extract the original non-method undecorated
1484
# function by black magic
1485
1590
nonmethod_func = (dict(
1486
1591
zip(attribute.func_code.co_freevars,
1487
1592
attribute.__closure__))
1488
1593
["func"].cell_contents)
1490
nonmethod_func = attribute
1595
nonmethod_func = (dict(
1596
zip(attribute.__code__.co_freevars,
1597
attribute.__closure__))
1598
["func"].cell_contents)
1491
1599
# Create a new, but exactly alike, function
1492
1600
# object, and decorate it to be a new D-Bus signal
1493
1601
# 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__)
1602
new_function = copy_function(nonmethod_func)
1508
1603
new_function = (dbus.service.signal(
1510
1605
attribute._dbus_signature)(new_function))
1681
1769
dbus_value = transform_func(
1682
1770
type_func(value),
1683
variant_level = variant_level)
1771
variant_level=variant_level)
1684
1772
self.PropertyChanged(dbus.String(dbus_name),
1686
1774
self.PropertiesChanged(
1688
dbus.Dictionary({ dbus.String(dbus_name):
1776
dbus.Dictionary({dbus.String(dbus_name):
1691
1779
setattr(self, attrname, value)
1693
1781
return property(lambda self: getattr(self, attrname), setter)
1695
1783
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1696
1784
approvals_pending = notifychangeproperty(dbus.Boolean,
1697
1785
"ApprovalPending",
1699
1787
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1700
1788
last_enabled = notifychangeproperty(datetime_to_dbus,
1702
1790
checker = notifychangeproperty(
1703
1791
dbus.Boolean, "CheckerRunning",
1704
type_func = lambda checker: checker is not None)
1792
type_func=lambda checker: checker is not None)
1705
1793
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1706
1794
"LastCheckedOK")
1707
1795
last_checker_status = notifychangeproperty(dbus.Int16,
1712
1800
"ApprovedByDefault")
1713
1801
approval_delay = notifychangeproperty(
1714
1802
dbus.UInt64, "ApprovalDelay",
1715
type_func = lambda td: td.total_seconds() * 1000)
1803
type_func=lambda td: td.total_seconds() * 1000)
1716
1804
approval_duration = notifychangeproperty(
1717
1805
dbus.UInt64, "ApprovalDuration",
1718
type_func = lambda td: td.total_seconds() * 1000)
1806
type_func=lambda td: td.total_seconds() * 1000)
1719
1807
host = notifychangeproperty(dbus.String, "Host")
1720
1808
timeout = notifychangeproperty(
1721
1809
dbus.UInt64, "Timeout",
1722
type_func = lambda td: td.total_seconds() * 1000)
1810
type_func=lambda td: td.total_seconds() * 1000)
1723
1811
extended_timeout = notifychangeproperty(
1724
1812
dbus.UInt64, "ExtendedTimeout",
1725
type_func = lambda td: td.total_seconds() * 1000)
1813
type_func=lambda td: td.total_seconds() * 1000)
1726
1814
interval = notifychangeproperty(
1727
1815
dbus.UInt64, "Interval",
1728
type_func = lambda td: td.total_seconds() * 1000)
1816
type_func=lambda td: td.total_seconds() * 1000)
1729
1817
checker_command = notifychangeproperty(dbus.String, "Checker")
1730
1818
secret = notifychangeproperty(dbus.ByteArray, "Secret",
1731
1819
invalidate_only=True)
1733
1821
del notifychangeproperty
1735
1823
def __del__(self, *args, **kwargs):
1737
1825
self.remove_from_connection()
1772
1860
# Emit D-Bus signal
1773
1861
self.CheckerStarted(self.current_checker_command)
1776
1864
def _reset_approved(self):
1777
1865
self.approved = None
1780
1868
def approve(self, value=True):
1781
1869
self.approved = value
1782
gobject.timeout_add(int(self.approval_duration.total_seconds()
1783
* 1000), self._reset_approved)
1870
GLib.timeout_add(int(self.approval_duration.total_seconds()
1871
* 1000), self._reset_approved)
1784
1872
self.send_changedstate()
1786
## D-Bus methods, signals & properties
1874
# D-Bus methods, signals & properties
1792
1880
# CheckerCompleted - signal
1793
1881
@dbus.service.signal(_interface, signature="nxs")
1794
1882
def CheckerCompleted(self, exitcode, waitstatus, command):
1798
1886
# CheckerStarted - signal
1799
1887
@dbus.service.signal(_interface, signature="s")
1800
1888
def CheckerStarted(self, command):
1804
1892
# PropertyChanged - signal
1805
1893
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1806
1894
@dbus.service.signal(_interface, signature="sv")
1807
1895
def PropertyChanged(self, property, value):
1811
1899
# GotSecret - signal
1812
1900
@dbus.service.signal(_interface)
1813
1901
def GotSecret(self):
1816
1904
server to mandos-client
1820
1908
# Rejected - signal
1821
1909
@dbus.service.signal(_interface, signature="s")
1822
1910
def Rejected(self, reason):
1826
1914
# NeedApproval - signal
1827
1915
@dbus.service.signal(_interface, signature="tb")
1828
1916
def NeedApproval(self, timeout, default):
1830
1918
return self.need_approval()
1834
1922
# Approve - method
1835
1923
@dbus.service.method(_interface, in_signature="b")
1836
1924
def Approve(self, value):
1837
1925
self.approve(value)
1839
1927
# CheckedOK - method
1840
1928
@dbus.service.method(_interface)
1841
1929
def CheckedOK(self):
1842
1930
self.checked_ok()
1844
1932
# Enable - method
1845
1933
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1846
1934
@dbus.service.method(_interface)
1847
1935
def Enable(self):
1851
1939
# StartChecker - method
1852
1940
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1853
1941
@dbus.service.method(_interface)
1854
1942
def StartChecker(self):
1856
1944
self.start_checker()
1858
1946
# Disable - method
1859
1947
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1860
1948
@dbus.service.method(_interface)
1861
1949
def Disable(self):
1865
1953
# StopChecker - method
1866
1954
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1867
1955
@dbus.service.method(_interface)
1868
1956
def StopChecker(self):
1869
1957
self.stop_checker()
1873
1961
# ApprovalPending - property
1874
1962
@dbus_service_property(_interface, signature="b", access="read")
1875
1963
def ApprovalPending_dbus_property(self):
1876
1964
return dbus.Boolean(bool(self.approvals_pending))
1878
1966
# ApprovedByDefault - property
1879
1967
@dbus_service_property(_interface,
2102
2190
class ClientHandler(socketserver.BaseRequestHandler, object):
2103
2191
"""A class to handle client connections.
2105
2193
Instantiated once for each connection to handle it.
2106
2194
Note: This will run in its own forked process."""
2108
2196
def handle(self):
2109
2197
with contextlib.closing(self.server.child_pipe) as child_pipe:
2110
2198
logger.info("TCP connection from: %s",
2111
2199
str(self.client_address))
2112
2200
logger.debug("Pipe FD: %d",
2113
2201
self.server.child_pipe.fileno())
2115
2203
session = gnutls.ClientSession(self.request)
2117
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
2118
# "+AES-256-CBC", "+SHA1",
2119
# "+COMP-NULL", "+CTYPE-OPENPGP",
2205
# priority = ':'.join(("NONE", "+VERS-TLS1.1",
2206
# "+AES-256-CBC", "+SHA1",
2207
# "+COMP-NULL", "+CTYPE-OPENPGP",
2121
2209
# Use a fallback default, since this MUST be set.
2122
2210
priority = self.server.gnutls_priority
2123
2211
if priority is None:
2124
2212
priority = "NORMAL"
2125
gnutls.priority_set_direct(session._c_object, priority,
2213
gnutls.priority_set_direct(session._c_object,
2214
priority.encode("utf-8"),
2128
2217
# Start communication using the Mandos protocol
2129
2218
# Get protocol number
2130
2219
line = self.request.makefile().readline()
2301
2384
class MultiprocessingMixIn(object):
2302
2385
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
2304
2387
def sub_process_main(self, request, address):
2306
2389
self.finish_request(request, address)
2307
2390
except Exception:
2308
2391
self.handle_error(request, address)
2309
2392
self.close_request(request)
2311
2394
def process_request(self, request, address):
2312
2395
"""Start a new process to process the request."""
2313
proc = multiprocessing.Process(target = self.sub_process_main,
2314
args = (request, address))
2396
proc = multiprocessing.Process(target=self.sub_process_main,
2397
args=(request, address))
2319
2402
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
2320
2403
""" adds a pipe to the MixIn """
2322
2405
def process_request(self, request, client_address):
2323
2406
"""Overrides and wraps the original process_request().
2325
2408
This function creates a new pipe in self.pipe
2327
2410
parent_pipe, self.child_pipe = multiprocessing.Pipe()
2329
2412
proc = MultiprocessingMixIn.process_request(self, request,
2330
2413
client_address)
2331
2414
self.child_pipe.close()
2332
2415
self.add_pipe(parent_pipe, proc)
2334
2417
def add_pipe(self, parent_pipe, proc):
2335
2418
"""Dummy function; override as necessary"""
2336
2419
raise NotImplementedError()
2384
2468
# socket_wrapper(), if socketfd was set.
2385
2469
socketserver.TCPServer.__init__(self, server_address,
2386
2470
RequestHandlerClass)
2388
2472
def server_bind(self):
2389
2473
"""This overrides the normal server_bind() function
2390
2474
to bind to an interface if one was specified, and also NOT to
2391
2475
bind to an address or port if they were not specified."""
2476
global SO_BINDTODEVICE
2392
2477
if self.interface is not None:
2393
2478
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)
2479
# Fall back to a hard-coded value which seems to be
2481
logger.warning("SO_BINDTODEVICE not found, trying 25")
2482
SO_BINDTODEVICE = 25
2484
self.socket.setsockopt(
2485
socket.SOL_SOCKET, SO_BINDTODEVICE,
2486
(self.interface + "\0").encode("utf-8"))
2487
except socket.error as error:
2488
if error.errno == errno.EPERM:
2489
logger.error("No permission to bind to"
2490
" interface %s", self.interface)
2491
elif error.errno == errno.ENOPROTOOPT:
2492
logger.error("SO_BINDTODEVICE not available;"
2493
" cannot bind to interface %s",
2495
elif error.errno == errno.ENODEV:
2496
logger.error("Interface %s does not exist,"
2497
" cannot bind", self.interface)
2415
2500
# Only bind(2) the socket if we really need to.
2416
2501
if self.server_address[0] or self.server_address[1]:
2417
2502
if not self.server_address[0]:
2418
2503
if self.address_family == socket.AF_INET6:
2419
any_address = "::" # in6addr_any
2504
any_address = "::" # in6addr_any
2421
any_address = "0.0.0.0" # INADDR_ANY
2506
any_address = "0.0.0.0" # INADDR_ANY
2422
2507
self.server_address = (any_address,
2423
2508
self.server_address[1])
2424
2509
elif not self.server_address[1]:
2458
2543
self.gnutls_priority = gnutls_priority
2459
2544
IPv6_TCPServer.__init__(self, server_address,
2460
2545
RequestHandlerClass,
2461
interface = interface,
2462
use_ipv6 = use_ipv6,
2463
socketfd = socketfd)
2546
interface=interface,
2465
2550
def server_activate(self):
2466
2551
if self.enabled:
2467
2552
return socketserver.TCPServer.server_activate(self)
2469
2554
def enable(self):
2470
2555
self.enabled = True
2472
2557
def add_pipe(self, parent_pipe, proc):
2473
2558
# Call "handle_ipc" for both data and EOF events
2474
gobject.io_add_watch(
2475
2560
parent_pipe.fileno(),
2476
gobject.IO_IN | gobject.IO_HUP,
2561
GLib.IO_IN | GLib.IO_HUP,
2477
2562
functools.partial(self.handle_ipc,
2478
parent_pipe = parent_pipe,
2563
parent_pipe=parent_pipe,
2481
2566
def handle_ipc(self, source, condition,
2482
2567
parent_pipe=None,
2484
2569
client_object=None):
2485
2570
# error, or the other end of multiprocessing.Pipe has closed
2486
if condition & (gobject.IO_ERR | gobject.IO_HUP):
2571
if condition & (GLib.IO_ERR | GLib.IO_HUP):
2487
2572
# Wait for other process to exit
2491
2576
# Read a request from the child
2492
2577
request = parent_pipe.recv()
2493
2578
command = request[0]
2495
2580
if command == 'init':
2496
2581
fpr = request[1]
2497
2582
address = request[2]
2499
for c in self.clients.itervalues():
2584
for c in self.clients.values():
2500
2585
if c.fingerprint == fpr:
3008
logger.debug("Did setuid/setgid to {}:{}".format(uid,
2916
3010
except OSError as error:
3011
logger.warning("Failed to setuid/setgid to {}:{}: {}"
3012
.format(uid, gid, os.strerror(error.errno)))
2917
3013
if error.errno != errno.EPERM:
2921
3017
# Enable all possible GnuTLS debugging
2923
3019
# "Use a log level over 10 to enable all debugging options."
2924
3020
# - GnuTLS manual
2925
3021
gnutls.global_set_log_level(11)
2927
3023
@gnutls.log_func
2928
3024
def debug_gnutls(level, string):
2929
3025
logger.debug("GnuTLS: %s", string[:-1])
2931
3027
gnutls.global_set_log_function(debug_gnutls)
2933
3029
# Redirect stdin so all checkers get /dev/null
2934
3030
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2935
3031
os.dup2(null, sys.stdin.fileno())
2939
3035
# Need to fork before connecting to D-Bus
2940
3036
if not foreground:
2941
3037
# 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()
3040
# multiprocessing will use threads, so before we use GLib we need
3041
# to inform GLib that threads will be used.
2948
3044
global main_loop
2949
3045
# From the Avahi example code
2950
3046
DBusGMainLoop(set_as_default=True)
2951
main_loop = gobject.MainLoop()
3047
main_loop = GLib.MainLoop()
2952
3048
bus = dbus.SystemBus()
2953
3049
# End of Avahi example code
2968
3064
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2969
3065
service = AvahiServiceToSyslog(
2970
name = server_settings["servicename"],
2971
servicetype = "_mandos._tcp",
2972
protocol = protocol,
3066
name=server_settings["servicename"],
3067
servicetype="_mandos._tcp",
2974
3070
if server_settings["interface"]:
2975
3071
service.interface = if_nametoindex(
2976
3072
server_settings["interface"].encode("utf-8"))
2978
3074
global multiprocessing_manager
2979
3075
multiprocessing_manager = multiprocessing.Manager()
2981
3077
client_class = Client
2983
client_class = functools.partial(ClientDBus, bus = bus)
3079
client_class = functools.partial(ClientDBus, bus=bus)
2985
3081
client_settings = Client.config_parser(client_config)
2986
3082
old_client_settings = {}
2987
3083
clients_data = {}
2989
3085
# This is used to redirect stdout and stderr for checker processes
2991
wnull = open(os.devnull, "w") # A writable /dev/null
3087
wnull = open(os.devnull, "w") # A writable /dev/null
2992
3088
# Only used if server is running in foreground but not in debug
2994
3090
if debug or not foreground:
2997
3093
# Get client data and settings from last running state.
2998
3094
if server_settings["restore"]:
3000
3096
with open(stored_state_path, "rb") as stored_state:
3001
clients_data, old_client_settings = pickle.load(
3097
if sys.version_info.major == 2:
3098
clients_data, old_client_settings = pickle.load(
3101
bytes_clients_data, bytes_old_client_settings = (
3102
pickle.load(stored_state, encoding="bytes"))
3103
# Fix bytes to strings
3106
clients_data = {(key.decode("utf-8")
3107
if isinstance(key, bytes)
3110
bytes_clients_data.items()}
3111
del bytes_clients_data
3112
for key in clients_data:
3113
value = {(k.decode("utf-8")
3114
if isinstance(k, bytes) else k): v
3116
clients_data[key].items()}
3117
clients_data[key] = value
3119
value["client_structure"] = [
3121
if isinstance(s, bytes)
3123
value["client_structure"]]
3125
for k in ("name", "host"):
3126
if isinstance(value[k], bytes):
3127
value[k] = value[k].decode("utf-8")
3128
# old_client_settings
3130
old_client_settings = {
3131
(key.decode("utf-8")
3132
if isinstance(key, bytes)
3135
bytes_old_client_settings.items()}
3136
del bytes_old_client_settings
3138
for value in old_client_settings.values():
3139
if isinstance(value["host"], bytes):
3140
value["host"] = (value["host"]
3003
3142
os.remove(stored_state_path)
3004
3143
except IOError as e:
3005
3144
if e.errno == errno.ENOENT:
3105
3244
pidfilename, pid)
3107
3246
del pidfilename
3109
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
3110
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
3248
for termsig in (signal.SIGHUP, signal.SIGTERM):
3249
GLib.unix_signal_add(GLib.PRIORITY_HIGH, termsig,
3250
lambda: main_loop.quit() and False)
3114
3254
@alternate_dbus_interfaces(
3115
{ "se.recompile.Mandos": "se.bsnet.fukt.Mandos" })
3255
{"se.recompile.Mandos": "se.bsnet.fukt.Mandos"})
3116
3256
class MandosDBusService(DBusObjectWithObjectManager):
3117
3257
"""A D-Bus proxy object"""
3119
3259
def __init__(self):
3120
3260
dbus.service.Object.__init__(self, bus, "/")
3122
3262
_interface = "se.recompile.Mandos"
3124
3264
@dbus.service.signal(_interface, signature="o")
3125
3265
def ClientAdded(self, objpath):
3129
3269
@dbus.service.signal(_interface, signature="ss")
3130
3270
def ClientNotFound(self, fingerprint, address):
3134
3274
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3136
3276
@dbus.service.signal(_interface, signature="os")
3137
3277
def ClientRemoved(self, objpath, name):
3141
3281
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3143
3283
@dbus.service.method(_interface, out_signature="ao")
3144
3284
def GetAllClients(self):
3146
3286
return dbus.Array(c.dbus_object_path for c in
3147
tcp_server.clients.itervalues())
3287
tcp_server.clients.values())
3149
3289
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3151
3291
@dbus.service.method(_interface,
3210
3350
self.ClientRemoved(client.dbus_object_path,
3213
3353
mandos_dbus_service = MandosDBusService()
3355
# Save modules to variables to exempt the modules from being
3356
# unloaded before the function registered with atexit() is run.
3357
mp = multiprocessing
3216
3361
"Cleanup function; run on exit"
3218
3363
service.cleanup()
3220
multiprocessing.active_children()
3365
mp.active_children()
3222
3367
if not (tcp_server.clients or client_settings):
3225
3370
# Store client before exiting. Secrets are encrypted with key
3226
3371
# based on what config file has. If config file is
3227
3372
# removed/edited, old secret will thus be unrecovable.
3229
3374
with PGPEngine() as pgp:
3230
for client in tcp_server.clients.itervalues():
3375
for client in tcp_server.clients.values():
3231
3376
key = client_settings[client.name]["secret"]
3232
3377
client.encrypted_secret = pgp.encrypt(client.secret,
3234
3379
client_dict = {}
3236
3381
# A list of attributes that can not be pickled
3238
exclude = { "bus", "changedstate", "secret",
3239
"checker", "server_settings" }
3383
exclude = {"bus", "changedstate", "secret",
3384
"checker", "server_settings"}
3240
3385
for name, typ in inspect.getmembers(dbus.service
3242
3387
exclude.add(name)
3244
3389
client_dict["encrypted_secret"] = (client
3245
3390
.encrypted_secret)
3246
3391
for attr in client.client_structure:
3247
3392
if attr not in exclude:
3248
3393
client_dict[attr] = getattr(client, attr)
3250
3395
clients[client.name] = client_dict
3251
3396
del client_settings[client.name]["secret"]
3254
3399
with tempfile.NamedTemporaryFile(