2
# -*- mode: python; coding: utf-8 -*-
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 -*-
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-2016 Teddy Hogeborn
15
# Copyright © 2008-2016 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-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
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,
81
83
import dbus.service
83
from gi.repository import GObject
85
import gobject as GObject
85
from gi.repository import GLib
86
86
from dbus.mainloop.glib import DBusGMainLoop
89
89
import xml.dom.minidom
92
if sys.version_info.major == 2:
96
# Show warnings by default
97
if not sys.warnoptions:
99
warnings.simplefilter("default")
101
# Try to find the value of SO_BINDTODEVICE:
103
# This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and
104
# newer, and it is also the most natural place for it:
93
105
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
94
106
except AttributeError:
108
# This is where SO_BINDTODEVICE was up to and including Python
96
110
from IN import SO_BINDTODEVICE
97
111
except ImportError:
98
SO_BINDTODEVICE = None
100
if sys.version_info.major == 2:
112
# In Python 2.7 it seems to have been removed entirely.
113
# Try running the C preprocessor:
115
cc = subprocess.Popen(["cc", "--language=c", "-E",
117
stdin=subprocess.PIPE,
118
stdout=subprocess.PIPE)
119
stdout = cc.communicate(
120
"#include <sys/socket.h>\nSO_BINDTODEVICE\n")[0]
121
SO_BINDTODEVICE = int(stdout.splitlines()[-1])
122
except (OSError, ValueError, IndexError):
124
SO_BINDTODEVICE = None
126
if sys.version_info < (3, 2):
127
configparser.Configparser = configparser.SafeConfigParser
104
130
stored_state_file = "clients.pickle"
106
132
logger = logging.getLogger()
133
logging.captureWarnings(True) # Show warnings via the logging system
110
137
if_nametoindex = ctypes.cdll.LoadLibrary(
111
138
ctypes.util.find_library("c")).if_nametoindex
112
139
except (OSError, AttributeError):
114
141
def if_nametoindex(interface):
115
142
"Get an interface index the hard way, i.e. using fcntl()"
116
143
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
230
259
'--passphrase-file',
232
261
+ self.gnupgargs,
233
stdin = subprocess.PIPE,
234
stdout = subprocess.PIPE,
235
stderr = subprocess.PIPE)
236
ciphertext, err = proc.communicate(input = data)
262
stdin=subprocess.PIPE,
263
stdout=subprocess.PIPE,
264
stderr=subprocess.PIPE)
265
ciphertext, err = proc.communicate(input=data)
237
266
if proc.returncode != 0:
238
267
raise PGPError(err)
239
268
return ciphertext
241
270
def decrypt(self, data, password):
242
271
passphrase = self.password_encode(password)
243
272
with tempfile.NamedTemporaryFile(
244
dir = self.tempdir) as passfile:
273
dir=self.tempdir) as passfile:
245
274
passfile.write(passphrase)
247
276
proc = subprocess.Popen([self.gpg, '--decrypt',
248
277
'--passphrase-file',
250
279
+ self.gnupgargs,
251
stdin = subprocess.PIPE,
252
stdout = subprocess.PIPE,
253
stderr = subprocess.PIPE)
254
decrypted_plaintext, err = proc.communicate(input = data)
280
stdin=subprocess.PIPE,
281
stdout=subprocess.PIPE,
282
stderr=subprocess.PIPE)
283
decrypted_plaintext, err = proc.communicate(input=data)
255
284
if proc.returncode != 0:
256
285
raise PGPError(err)
257
286
return decrypted_plaintext
259
289
# Pretend that we have an Avahi module
261
"""This isn't so much a class as it is a module-like namespace.
262
It is instantiated once, and simulates having an Avahi module."""
263
IF_UNSPEC = -1 # avahi-common/address.h
264
PROTO_UNSPEC = -1 # avahi-common/address.h
265
PROTO_INET = 0 # avahi-common/address.h
266
PROTO_INET6 = 1 # avahi-common/address.h
291
"""This isn't so much a class as it is a module-like namespace."""
292
IF_UNSPEC = -1 # avahi-common/address.h
293
PROTO_UNSPEC = -1 # avahi-common/address.h
294
PROTO_INET = 0 # avahi-common/address.h
295
PROTO_INET6 = 1 # avahi-common/address.h
267
296
DBUS_NAME = "org.freedesktop.Avahi"
268
297
DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup"
269
298
DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
270
299
DBUS_PATH_SERVER = "/"
271
def string_array_to_txt_array(self, t):
302
def string_array_to_txt_array(t):
272
303
return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
273
304
for s in t), signature="ay")
274
ENTRY_GROUP_ESTABLISHED = 2 # avahi-common/defs.h
275
ENTRY_GROUP_COLLISION = 3 # avahi-common/defs.h
276
ENTRY_GROUP_FAILURE = 4 # avahi-common/defs.h
277
SERVER_INVALID = 0 # avahi-common/defs.h
278
SERVER_REGISTERING = 1 # avahi-common/defs.h
279
SERVER_RUNNING = 2 # avahi-common/defs.h
280
SERVER_COLLISION = 3 # avahi-common/defs.h
281
SERVER_FAILURE = 4 # avahi-common/defs.h
305
ENTRY_GROUP_ESTABLISHED = 2 # avahi-common/defs.h
306
ENTRY_GROUP_COLLISION = 3 # avahi-common/defs.h
307
ENTRY_GROUP_FAILURE = 4 # avahi-common/defs.h
308
SERVER_INVALID = 0 # avahi-common/defs.h
309
SERVER_REGISTERING = 1 # avahi-common/defs.h
310
SERVER_RUNNING = 2 # avahi-common/defs.h
311
SERVER_COLLISION = 3 # avahi-common/defs.h
312
SERVER_FAILURE = 4 # avahi-common/defs.h
284
315
class AvahiError(Exception):
285
316
def __init__(self, value, *args, **kwargs):
476
507
class AvahiServiceToSyslog(AvahiService):
477
508
def rename(self, *args, **kwargs):
478
509
"""Add the new name to the syslog messages"""
479
ret = AvahiService.rename(self, *args, **kwargs)
510
ret = super(AvahiServiceToSyslog, self).rename(*args, **kwargs)
480
511
syslogger.setFormatter(logging.Formatter(
481
512
'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
482
513
.format(self.name)))
485
517
# Pretend that we have a GnuTLS module
486
class GnuTLS(object):
487
"""This isn't so much a class as it is a module-like namespace.
488
It is instantiated once, and simulates having a GnuTLS module."""
490
_library = ctypes.cdll.LoadLibrary(
491
ctypes.util.find_library("gnutls"))
492
_need_version = b"3.3.0"
494
# Need to use class name "GnuTLS" here, since this method is
495
# called before the assignment to the "gnutls" global variable
497
if GnuTLS.check_version(self._need_version) is None:
498
raise GnuTLS.Error("Needs GnuTLS {} or later"
499
.format(self._need_version))
519
"""This isn't so much a class as it is a module-like namespace."""
521
library = ctypes.util.find_library("gnutls")
523
library = ctypes.util.find_library("gnutls-deb0")
524
_library = ctypes.cdll.LoadLibrary(library)
501
527
# Unless otherwise indicated, the constants and types below are
502
528
# all from the gnutls/gnutls.h C header file.
506
532
E_INTERRUPTED = -52
511
538
CRD_CERTIFICATE = 1
512
539
E_NO_CERTIFICATE_FOUND = -49
544
KEYID_USE_SHA256 = 1 # gnutls/x509.h
513
545
OPENPGP_FMT_RAW = 0 # gnutls/openpgp.h
516
548
class session_int(ctypes.Structure):
518
550
session_t = ctypes.POINTER(session_int)
519
552
class certificate_credentials_st(ctypes.Structure):
521
554
certificate_credentials_t = ctypes.POINTER(
522
555
certificate_credentials_st)
523
556
certificate_type_t = ctypes.c_int
524
558
class datum_t(ctypes.Structure):
525
559
_fields_ = [('data', ctypes.POINTER(ctypes.c_ubyte)),
526
560
('size', ctypes.c_uint)]
527
562
class openpgp_crt_int(ctypes.Structure):
529
564
openpgp_crt_t = ctypes.POINTER(openpgp_crt_int)
530
openpgp_crt_fmt_t = ctypes.c_int # gnutls/openpgp.h
565
openpgp_crt_fmt_t = ctypes.c_int # gnutls/openpgp.h
531
566
log_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
532
567
credentials_type_t = ctypes.c_int
533
568
transport_ptr_t = ctypes.c_void_p
534
569
close_request_t = ctypes.c_int
537
572
class Error(Exception):
538
# We need to use the class name "GnuTLS" here, since this
539
# exception might be raised from within GnuTLS.__init__,
540
# which is called before the assignment to the "gnutls"
541
# global variable has happened.
542
def __init__(self, message = None, code = None, args=()):
573
def __init__(self, message=None, code=None, args=()):
543
574
# Default usage is by a message string, but if a return
544
575
# code is passed, convert it to a string with
545
576
# gnutls.strerror()
547
578
if message is None and code is not None:
548
message = GnuTLS.strerror(code)
549
return super(GnuTLS.Error, self).__init__(
579
message = gnutls.strerror(code)
580
return super(gnutls.Error, self).__init__(
552
583
class CertificateSecurityError(Error):
556
class Credentials(object):
557
588
def __init__(self):
558
589
self._c_object = gnutls.certificate_credentials_t()
559
590
gnutls.certificate_allocate_credentials(
560
591
ctypes.byref(self._c_object))
561
592
self.type = gnutls.CRD_CERTIFICATE
563
594
def __del__(self):
564
595
gnutls.certificate_free_credentials(self._c_object)
566
class ClientSession(object):
567
def __init__(self, socket, credentials = None):
598
def __init__(self, socket, credentials=None):
568
599
self._c_object = gnutls.session_t()
569
gnutls.init(ctypes.byref(self._c_object), gnutls.CLIENT)
600
gnutls_flags = gnutls.CLIENT
601
if gnutls.check_version(b"3.5.6"):
602
gnutls_flags |= gnutls.NO_TICKETS
604
gnutls_flags |= gnutls.ENABLE_RAWPK
605
gnutls.init(ctypes.byref(self._c_object), gnutls_flags)
570
607
gnutls.set_default_priority(self._c_object)
571
608
gnutls.transport_set_ptr(self._c_object, socket.fileno())
572
609
gnutls.handshake_set_private_extensions(self._c_object,
614
651
return _error_code(result)
615
652
result = func(*arguments)
618
655
# Unless otherwise indicated, the function declarations below are
619
656
# all from the gnutls/gnutls.h C header file.
622
659
priority_set_direct = _library.gnutls_priority_set_direct
623
660
priority_set_direct.argtypes = [session_t, ctypes.c_char_p,
624
661
ctypes.POINTER(ctypes.c_char_p)]
625
662
priority_set_direct.restype = _error_code
627
664
init = _library.gnutls_init
628
665
init.argtypes = [ctypes.POINTER(session_t), ctypes.c_int]
629
666
init.restype = _error_code
631
668
set_default_priority = _library.gnutls_set_default_priority
632
669
set_default_priority.argtypes = [session_t]
633
670
set_default_priority.restype = _error_code
635
672
record_send = _library.gnutls_record_send
636
673
record_send.argtypes = [session_t, ctypes.c_void_p,
638
675
record_send.restype = ctypes.c_ssize_t
639
676
record_send.errcheck = _retry_on_error
641
678
certificate_allocate_credentials = (
642
679
_library.gnutls_certificate_allocate_credentials)
643
680
certificate_allocate_credentials.argtypes = [
644
681
ctypes.POINTER(certificate_credentials_t)]
645
682
certificate_allocate_credentials.restype = _error_code
647
684
certificate_free_credentials = (
648
685
_library.gnutls_certificate_free_credentials)
649
certificate_free_credentials.argtypes = [certificate_credentials_t]
686
certificate_free_credentials.argtypes = [
687
certificate_credentials_t]
650
688
certificate_free_credentials.restype = None
652
690
handshake_set_private_extensions = (
653
691
_library.gnutls_handshake_set_private_extensions)
654
692
handshake_set_private_extensions.argtypes = [session_t,
656
694
handshake_set_private_extensions.restype = None
658
696
credentials_set = _library.gnutls_credentials_set
659
697
credentials_set.argtypes = [session_t, credentials_type_t,
661
699
credentials_set.restype = _error_code
663
701
strerror = _library.gnutls_strerror
664
702
strerror.argtypes = [ctypes.c_int]
665
703
strerror.restype = ctypes.c_char_p
667
705
certificate_type_get = _library.gnutls_certificate_type_get
668
706
certificate_type_get.argtypes = [session_t]
669
707
certificate_type_get.restype = _error_code
671
709
certificate_get_peers = _library.gnutls_certificate_get_peers
672
710
certificate_get_peers.argtypes = [session_t,
673
711
ctypes.POINTER(ctypes.c_uint)]
674
712
certificate_get_peers.restype = ctypes.POINTER(datum_t)
676
714
global_set_log_level = _library.gnutls_global_set_log_level
677
715
global_set_log_level.argtypes = [ctypes.c_int]
678
716
global_set_log_level.restype = None
680
718
global_set_log_function = _library.gnutls_global_set_log_function
681
719
global_set_log_function.argtypes = [log_func]
682
720
global_set_log_function.restype = None
684
722
deinit = _library.gnutls_deinit
685
723
deinit.argtypes = [session_t]
686
724
deinit.restype = None
688
726
handshake = _library.gnutls_handshake
689
727
handshake.argtypes = [session_t]
690
728
handshake.restype = _error_code
691
729
handshake.errcheck = _retry_on_error
693
731
transport_set_ptr = _library.gnutls_transport_set_ptr
694
732
transport_set_ptr.argtypes = [session_t, transport_ptr_t]
695
733
transport_set_ptr.restype = None
697
735
bye = _library.gnutls_bye
698
736
bye.argtypes = [session_t, close_request_t]
699
737
bye.restype = _error_code
700
738
bye.errcheck = _retry_on_error
702
740
check_version = _library.gnutls_check_version
703
741
check_version.argtypes = [ctypes.c_char_p]
704
742
check_version.restype = ctypes.c_char_p
706
# All the function declarations below are from gnutls/openpgp.h
708
openpgp_crt_init = _library.gnutls_openpgp_crt_init
709
openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
710
openpgp_crt_init.restype = _error_code
712
openpgp_crt_import = _library.gnutls_openpgp_crt_import
713
openpgp_crt_import.argtypes = [openpgp_crt_t,
714
ctypes.POINTER(datum_t),
716
openpgp_crt_import.restype = _error_code
718
openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
719
openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
720
ctypes.POINTER(ctypes.c_uint)]
721
openpgp_crt_verify_self.restype = _error_code
723
openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
724
openpgp_crt_deinit.argtypes = [openpgp_crt_t]
725
openpgp_crt_deinit.restype = None
727
openpgp_crt_get_fingerprint = (
728
_library.gnutls_openpgp_crt_get_fingerprint)
729
openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
733
openpgp_crt_get_fingerprint.restype = _error_code
744
_need_version = b"3.3.0"
745
if check_version(_need_version) is None:
746
raise self.Error("Needs GnuTLS {} or later"
747
.format(_need_version))
749
_tls_rawpk_version = b"3.6.6"
750
has_rawpk = bool(check_version(_tls_rawpk_version))
754
class pubkey_st(ctypes.Structure):
756
pubkey_t = ctypes.POINTER(pubkey_st)
758
x509_crt_fmt_t = ctypes.c_int
760
# All the function declarations below are from gnutls/abstract.h
761
pubkey_init = _library.gnutls_pubkey_init
762
pubkey_init.argtypes = [ctypes.POINTER(pubkey_t)]
763
pubkey_init.restype = _error_code
765
pubkey_import = _library.gnutls_pubkey_import
766
pubkey_import.argtypes = [pubkey_t, ctypes.POINTER(datum_t),
768
pubkey_import.restype = _error_code
770
pubkey_get_key_id = _library.gnutls_pubkey_get_key_id
771
pubkey_get_key_id.argtypes = [pubkey_t, ctypes.c_int,
772
ctypes.POINTER(ctypes.c_ubyte),
773
ctypes.POINTER(ctypes.c_size_t)]
774
pubkey_get_key_id.restype = _error_code
776
pubkey_deinit = _library.gnutls_pubkey_deinit
777
pubkey_deinit.argtypes = [pubkey_t]
778
pubkey_deinit.restype = None
780
# All the function declarations below are from gnutls/openpgp.h
782
openpgp_crt_init = _library.gnutls_openpgp_crt_init
783
openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
784
openpgp_crt_init.restype = _error_code
786
openpgp_crt_import = _library.gnutls_openpgp_crt_import
787
openpgp_crt_import.argtypes = [openpgp_crt_t,
788
ctypes.POINTER(datum_t),
790
openpgp_crt_import.restype = _error_code
792
openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
793
openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
794
ctypes.POINTER(ctypes.c_uint)]
795
openpgp_crt_verify_self.restype = _error_code
797
openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
798
openpgp_crt_deinit.argtypes = [openpgp_crt_t]
799
openpgp_crt_deinit.restype = None
801
openpgp_crt_get_fingerprint = (
802
_library.gnutls_openpgp_crt_get_fingerprint)
803
openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
807
openpgp_crt_get_fingerprint.restype = _error_code
809
if check_version(b"3.6.4"):
810
certificate_type_get2 = _library.gnutls_certificate_type_get2
811
certificate_type_get2.argtypes = [session_t, ctypes.c_int]
812
certificate_type_get2.restype = _error_code
735
814
# Remove non-public functions
736
815
del _error_code, _retry_on_error
737
# Create the global "gnutls" object, simulating a module
740
818
def call_pipe(connection, # : multiprocessing.Connection
741
819
func, *args, **kwargs):
742
820
"""This function is meant to be called by multiprocessing.Process
744
822
This function runs func(*args, **kwargs), and writes the resulting
745
823
return value on the provided multiprocessing.Connection.
747
825
connection.send(func(*args, **kwargs))
748
826
connection.close()
750
class Client(object):
751
830
"""A representation of a client host served by this server.
754
833
approved: bool(); 'None' if not yet approved/disapproved
755
834
approval_delay: datetime.timedelta(); Time to wait for approval
756
835
approval_duration: datetime.timedelta(); Duration of one approval
757
checker: subprocess.Popen(); a running checker process used
758
to see if the client lives.
759
'None' if no process is running.
760
checker_callback_tag: a GObject event source tag, or None
836
checker: multiprocessing.Process(); a running checker process used
837
to see if the client lives. 'None' if no process is
839
checker_callback_tag: a GLib event source tag, or None
761
840
checker_command: string; External command which is run to check
762
841
if client lives. %() expansions are done at
763
842
runtime with vars(self) as dict, so that for
764
843
instance %(name)s can be used in the command.
765
checker_initiator_tag: a GObject event source tag, or None
844
checker_initiator_tag: a GLib event source tag, or None
766
845
created: datetime.datetime(); (UTC) object creation
767
846
client_structure: Object describing what attributes a client has
768
847
and is used for storing the client at exit
769
848
current_checker_command: string; current running checker_command
770
disable_initiator_tag: a GObject event source tag, or None
849
disable_initiator_tag: a GLib event source tag, or None
772
851
fingerprint: string (40 or 32 hexadecimal digits); used to
773
uniquely identify the client
852
uniquely identify an OpenPGP client
853
key_id: string (64 hexadecimal digits); used to uniquely identify
854
a client using raw public keys
774
855
host: string; available for use by the checker command
775
856
interval: datetime.timedelta(); How often to start a new checker
776
857
last_approval_request: datetime.datetime(); (UTC) or None
930
1014
logger.info("Disabling client %s", self.name)
931
1015
if getattr(self, "disable_initiator_tag", None) is not None:
932
GObject.source_remove(self.disable_initiator_tag)
1016
GLib.source_remove(self.disable_initiator_tag)
933
1017
self.disable_initiator_tag = None
934
1018
self.expires = None
935
1019
if getattr(self, "checker_initiator_tag", None) is not None:
936
GObject.source_remove(self.checker_initiator_tag)
1020
GLib.source_remove(self.checker_initiator_tag)
937
1021
self.checker_initiator_tag = None
938
1022
self.stop_checker()
939
1023
self.enabled = False
941
1025
self.send_changedstate()
942
# Do not run this again if called by a GObject.timeout_add
1026
# Do not run this again if called by a GLib.timeout_add
945
1029
def __del__(self):
948
1032
def init_checker(self):
949
1033
# Schedule a new checker to be started an 'interval' from now,
950
1034
# and every interval from then on.
951
1035
if self.checker_initiator_tag is not None:
952
GObject.source_remove(self.checker_initiator_tag)
953
self.checker_initiator_tag = GObject.timeout_add(
1036
GLib.source_remove(self.checker_initiator_tag)
1037
self.checker_initiator_tag = GLib.timeout_add(
954
1038
int(self.interval.total_seconds() * 1000),
955
1039
self.start_checker)
956
1040
# Schedule a disable() when 'timeout' has passed
957
1041
if self.disable_initiator_tag is not None:
958
GObject.source_remove(self.disable_initiator_tag)
959
self.disable_initiator_tag = GObject.timeout_add(
1042
GLib.source_remove(self.disable_initiator_tag)
1043
self.disable_initiator_tag = GLib.timeout_add(
960
1044
int(self.timeout.total_seconds() * 1000), self.disable)
961
1045
# Also start a new checker *right now*.
962
1046
self.start_checker()
964
1048
def checker_callback(self, source, condition, connection,
966
1050
"""The checker has completed, so take appropriate actions."""
967
self.checker_callback_tag = None
969
1051
# Read return code from connection (see call_pipe)
970
1052
returncode = connection.recv()
971
1053
connection.close()
1054
if self.checker is not None:
1056
self.checker_callback_tag = None
973
1059
if returncode >= 0:
974
1060
self.last_checker_status = returncode
975
1061
self.last_checker_signal = None
1431
1517
exc_info=error)
1432
1518
return xmlstring
1435
1522
dbus.OBJECT_MANAGER_IFACE
1436
1523
except AttributeError:
1437
1524
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
1439
1527
class DBusObjectWithObjectManager(DBusObjectWithAnnotations):
1440
1528
"""A D-Bus object with an ObjectManager.
1442
1530
Classes inheriting from this exposes the standard
1443
1531
GetManagedObjects call and the InterfacesAdded and
1444
1532
InterfacesRemoved signals on the standard
1445
1533
"org.freedesktop.DBus.ObjectManager" interface.
1447
1535
Note: No signals are sent automatically; they must be sent
1450
1538
@dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
1451
out_signature = "a{oa{sa{sv}}}")
1539
out_signature="a{oa{sa{sv}}}")
1452
1540
def GetManagedObjects(self):
1453
1541
"""This function must be overridden"""
1454
1542
raise NotImplementedError()
1456
1544
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE,
1457
signature = "oa{sa{sv}}")
1545
signature="oa{sa{sv}}")
1458
1546
def InterfacesAdded(self, object_path, interfaces_and_properties):
1461
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature = "oas")
1549
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature="oas")
1462
1550
def InterfacesRemoved(self, object_path, interfaces):
1465
1553
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1466
out_signature = "s",
1467
path_keyword = 'object_path',
1468
connection_keyword = 'connection')
1555
path_keyword='object_path',
1556
connection_keyword='connection')
1469
1557
def Introspect(self, object_path, connection):
1470
1558
"""Overloading of standard D-Bus method.
1472
1560
Override return argument name of GetManagedObjects to be
1473
1561
"objpath_interfaces_and_properties"
2348
class MultiprocessingMixIn(object):
2504
class MultiprocessingMixIn:
2349
2505
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
2351
2507
def sub_process_main(self, request, address):
2353
2509
self.finish_request(request, address)
2354
2510
except Exception:
2355
2511
self.handle_error(request, address)
2356
2512
self.close_request(request)
2358
2514
def process_request(self, request, address):
2359
2515
"""Start a new process to process the request."""
2360
proc = multiprocessing.Process(target = self.sub_process_main,
2361
args = (request, address))
2516
proc = multiprocessing.Process(target=self.sub_process_main,
2517
args=(request, address))
2366
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
2522
class MultiprocessingMixInWithPipe(MultiprocessingMixIn):
2367
2523
""" adds a pipe to the MixIn """
2369
2525
def process_request(self, request, client_address):
2370
2526
"""Overrides and wraps the original process_request().
2372
2528
This function creates a new pipe in self.pipe
2374
2530
parent_pipe, self.child_pipe = multiprocessing.Pipe()
2376
2532
proc = MultiprocessingMixIn.process_request(self, request,
2377
2533
client_address)
2378
2534
self.child_pipe.close()
2379
2535
self.add_pipe(parent_pipe, proc)
2381
2537
def add_pipe(self, parent_pipe, proc):
2382
2538
"""Dummy function; override as necessary"""
2383
2539
raise NotImplementedError()
2386
2542
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
2387
socketserver.TCPServer, object):
2543
socketserver.TCPServer):
2388
2544
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
2391
2547
enabled: Boolean; whether this server is activated yet
2392
2548
interface: None or a network interface name (string)
2393
2549
use_ipv6: Boolean; to use IPv6 or not
2396
2552
def __init__(self, server_address, RequestHandlerClass,
2397
2553
interface=None,
2431
2588
# socket_wrapper(), if socketfd was set.
2432
2589
socketserver.TCPServer.__init__(self, server_address,
2433
2590
RequestHandlerClass)
2435
2592
def server_bind(self):
2436
2593
"""This overrides the normal server_bind() function
2437
2594
to bind to an interface if one was specified, and also NOT to
2438
2595
bind to an address or port if they were not specified."""
2596
global SO_BINDTODEVICE
2439
2597
if self.interface is not None:
2440
2598
if SO_BINDTODEVICE is None:
2441
logger.error("SO_BINDTODEVICE does not exist;"
2442
" cannot bind to interface %s",
2446
self.socket.setsockopt(
2447
socket.SOL_SOCKET, SO_BINDTODEVICE,
2448
(self.interface + "\0").encode("utf-8"))
2449
except socket.error as error:
2450
if error.errno == errno.EPERM:
2451
logger.error("No permission to bind to"
2452
" interface %s", self.interface)
2453
elif error.errno == errno.ENOPROTOOPT:
2454
logger.error("SO_BINDTODEVICE not available;"
2455
" cannot bind to interface %s",
2457
elif error.errno == errno.ENODEV:
2458
logger.error("Interface %s does not exist,"
2459
" cannot bind", self.interface)
2599
# Fall back to a hard-coded value which seems to be
2601
logger.warning("SO_BINDTODEVICE not found, trying 25")
2602
SO_BINDTODEVICE = 25
2604
self.socket.setsockopt(
2605
socket.SOL_SOCKET, SO_BINDTODEVICE,
2606
(self.interface + "\0").encode("utf-8"))
2607
except socket.error as error:
2608
if error.errno == errno.EPERM:
2609
logger.error("No permission to bind to"
2610
" interface %s", self.interface)
2611
elif error.errno == errno.ENOPROTOOPT:
2612
logger.error("SO_BINDTODEVICE not available;"
2613
" cannot bind to interface %s",
2615
elif error.errno == errno.ENODEV:
2616
logger.error("Interface %s does not exist,"
2617
" cannot bind", self.interface)
2462
2620
# Only bind(2) the socket if we really need to.
2463
2621
if self.server_address[0] or self.server_address[1]:
2622
if self.server_address[1]:
2623
self.allow_reuse_address = True
2464
2624
if not self.server_address[0]:
2465
2625
if self.address_family == socket.AF_INET6:
2466
any_address = "::" # in6addr_any
2626
any_address = "::" # in6addr_any
2468
any_address = "0.0.0.0" # INADDR_ANY
2628
any_address = "0.0.0.0" # INADDR_ANY
2469
2629
self.server_address = (any_address,
2470
2630
self.server_address[1])
2471
2631
elif not self.server_address[1]:
2505
2665
self.gnutls_priority = gnutls_priority
2506
2666
IPv6_TCPServer.__init__(self, server_address,
2507
2667
RequestHandlerClass,
2508
interface = interface,
2509
use_ipv6 = use_ipv6,
2510
socketfd = socketfd)
2668
interface=interface,
2512
2672
def server_activate(self):
2513
2673
if self.enabled:
2514
2674
return socketserver.TCPServer.server_activate(self)
2516
2676
def enable(self):
2517
2677
self.enabled = True
2519
2679
def add_pipe(self, parent_pipe, proc):
2520
2680
# Call "handle_ipc" for both data and EOF events
2521
GObject.io_add_watch(
2522
parent_pipe.fileno(),
2523
GObject.IO_IN | GObject.IO_HUP,
2682
GLib.IOChannel.unix_new(parent_pipe.fileno()),
2683
GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
2524
2684
functools.partial(self.handle_ipc,
2525
parent_pipe = parent_pipe,
2685
parent_pipe=parent_pipe,
2528
2688
def handle_ipc(self, source, condition,
2529
2689
parent_pipe=None,
2531
2691
client_object=None):
2532
2692
# error, or the other end of multiprocessing.Pipe has closed
2533
if condition & (GObject.IO_ERR | GObject.IO_HUP):
2693
if condition & (GLib.IO_ERR | GLib.IO_HUP):
2534
2694
# Wait for other process to exit
2538
2698
# Read a request from the child
2539
2699
request = parent_pipe.recv()
2540
2700
command = request[0]
2542
2702
if command == 'init':
2544
address = request[2]
2703
key_id = request[1].decode("ascii")
2704
fpr = request[2].decode("ascii")
2705
address = request[3]
2546
2707
for c in self.clients.values():
2547
if c.fingerprint == fpr:
2708
if key_id == "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855":
2710
if key_id and c.key_id == key_id:
2713
if fpr and c.fingerprint == fpr:
2551
logger.info("Client not found for fingerprint: %s, ad"
2552
"dress: %s", fpr, address)
2717
logger.info("Client not found for key ID: %s, address"
2718
": %s", key_id or fpr, address)
2553
2719
if self.use_dbus:
2554
2720
# Emit D-Bus signal
2555
mandos_dbus_service.ClientNotFound(fpr,
2721
mandos_dbus_service.ClientNotFound(key_id or fpr,
2557
2723
parent_pipe.send(False)
2560
GObject.io_add_watch(
2561
parent_pipe.fileno(),
2562
GObject.IO_IN | GObject.IO_HUP,
2727
GLib.IOChannel.unix_new(parent_pipe.fileno()),
2728
GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
2563
2729
functools.partial(self.handle_ipc,
2564
parent_pipe = parent_pipe,
2566
client_object = client))
2730
parent_pipe=parent_pipe,
2732
client_object=client))
2567
2733
parent_pipe.send(True)
2568
2734
# remove the old hook in favor of the new above hook on
2586
2752
parent_pipe.send((
2587
2753
'data', client_object.__getattribute__(attrname)))
2589
2755
if command == 'setattr':
2590
2756
attrname = request[1]
2591
2757
value = request[2]
2592
2758
setattr(client_object, attrname, value)
2597
2763
def rfc3339_duration_to_delta(duration):
2598
2764
"""Parse an RFC 3339 "duration" and return a datetime.timedelta
2600
>>> rfc3339_duration_to_delta("P7D")
2601
datetime.timedelta(7)
2602
>>> rfc3339_duration_to_delta("PT60S")
2603
datetime.timedelta(0, 60)
2604
>>> rfc3339_duration_to_delta("PT60M")
2605
datetime.timedelta(0, 3600)
2606
>>> rfc3339_duration_to_delta("PT24H")
2607
datetime.timedelta(1)
2608
>>> rfc3339_duration_to_delta("P1W")
2609
datetime.timedelta(7)
2610
>>> rfc3339_duration_to_delta("PT5M30S")
2611
datetime.timedelta(0, 330)
2612
>>> rfc3339_duration_to_delta("P1DT3M20S")
2613
datetime.timedelta(1, 200)
2766
>>> rfc3339_duration_to_delta("P7D") == datetime.timedelta(7)
2768
>>> rfc3339_duration_to_delta("PT60S") == datetime.timedelta(0, 60)
2770
>>> rfc3339_duration_to_delta("PT60M") == datetime.timedelta(0, 3600)
2772
>>> rfc3339_duration_to_delta("PT24H") == datetime.timedelta(1)
2774
>>> rfc3339_duration_to_delta("P1W") == datetime.timedelta(7)
2776
>>> rfc3339_duration_to_delta("PT5M30S") == datetime.timedelta(0, 330)
2778
>>> rfc3339_duration_to_delta("P1DT3M20S") == datetime.timedelta(1, 200)
2616
2782
# Parsing an RFC 3339 duration with regular expressions is not
2617
2783
# possible - there would have to be multiple places for the same
2618
2784
# values, like seconds. The current code, while more esoteric, is
2619
2785
# cleaner without depending on a parsing library. If Python had a
2620
2786
# built-in library for parsing we would use it, but we'd like to
2621
2787
# avoid excessive use of external libraries.
2623
2789
# New type for defining tokens, syntax, and semantics all-in-one
2624
2790
Token = collections.namedtuple("Token", (
2625
2791
"regexp", # To match token; if "value" is not None, must have
2809
2978
parser.add_argument("--no-zeroconf", action="store_false",
2810
2979
dest="zeroconf", help="Do not use Zeroconf",
2813
2982
options = parser.parse_args()
2817
fail_count, test_count = doctest.testmod()
2818
sys.exit(os.EX_OK if fail_count == 0 else 1)
2820
2984
# Default values for config file for server-global settings
2821
server_defaults = { "interface": "",
2826
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2827
":+SIGN-DSA-SHA256",
2828
"servicename": "Mandos",
2834
"statedir": "/var/lib/mandos",
2835
"foreground": "False",
2985
if gnutls.has_rawpk:
2986
priority = ("SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA"
2987
":!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA")
2989
priority = ("SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2990
":+SIGN-DSA-SHA256")
2991
server_defaults = {"interface": "",
2995
"priority": priority,
2996
"servicename": "Mandos",
3002
"statedir": "/var/lib/mandos",
3003
"foreground": "False",
2839
3008
# Parse config file for server-global settings
2840
server_config = configparser.SafeConfigParser(server_defaults)
3009
server_config = configparser.ConfigParser(server_defaults)
2841
3010
del server_defaults
2842
3011
server_config.read(os.path.join(options.configdir, "mandos.conf"))
2843
# Convert the SafeConfigParser object to a dict
3012
# Convert the ConfigParser object to a dict
2844
3013
server_settings = server_config.defaults()
2845
3014
# Use the appropriate methods on the non-string config options
2846
for option in ("debug", "use_dbus", "use_ipv6", "foreground"):
3015
for option in ("debug", "use_dbus", "use_ipv6", "restore",
3016
"foreground", "zeroconf"):
2847
3017
server_settings[option] = server_config.getboolean("DEFAULT",
2849
3019
if server_settings["port"]:
3022
3192
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
3023
3193
service = AvahiServiceToSyslog(
3024
name = server_settings["servicename"],
3025
servicetype = "_mandos._tcp",
3026
protocol = protocol,
3194
name=server_settings["servicename"],
3195
servicetype="_mandos._tcp",
3028
3198
if server_settings["interface"]:
3029
3199
service.interface = if_nametoindex(
3030
3200
server_settings["interface"].encode("utf-8"))
3032
3202
global multiprocessing_manager
3033
3203
multiprocessing_manager = multiprocessing.Manager()
3035
3205
client_class = Client
3037
client_class = functools.partial(ClientDBus, bus = bus)
3207
client_class = functools.partial(ClientDBus, bus=bus)
3039
3209
client_settings = Client.config_parser(client_config)
3040
3210
old_client_settings = {}
3041
3211
clients_data = {}
3043
3213
# This is used to redirect stdout and stderr for checker processes
3045
wnull = open(os.devnull, "w") # A writable /dev/null
3215
wnull = open(os.devnull, "w") # A writable /dev/null
3046
3216
# Only used if server is running in foreground but not in debug
3048
3218
if debug or not foreground:
3051
3221
# Get client data and settings from last running state.
3052
3222
if server_settings["restore"]:
3054
3224
with open(stored_state_path, "rb") as stored_state:
3055
if sys.version_info.major == 2:
3225
if sys.version_info.major == 2:
3056
3226
clients_data, old_client_settings = pickle.load(
3059
3229
bytes_clients_data, bytes_old_client_settings = (
3060
pickle.load(stored_state, encoding = "bytes"))
3061
### Fix bytes to strings
3230
pickle.load(stored_state, encoding="bytes"))
3231
# Fix bytes to strings
3064
clients_data = { (key.decode("utf-8")
3065
if isinstance(key, bytes)
3068
bytes_clients_data.items() }
3234
clients_data = {(key.decode("utf-8")
3235
if isinstance(key, bytes)
3238
bytes_clients_data.items()}
3239
del bytes_clients_data
3069
3240
for key in clients_data:
3070
value = { (k.decode("utf-8")
3071
if isinstance(k, bytes) else k): v
3073
clients_data[key].items() }
3241
value = {(k.decode("utf-8")
3242
if isinstance(k, bytes) else k): v
3244
clients_data[key].items()}
3074
3245
clients_data[key] = value
3075
3246
# .client_structure
3076
3247
value["client_structure"] = [
3077
3248
(s.decode("utf-8")
3078
3249
if isinstance(s, bytes)
3079
3250
else s) for s in
3080
value["client_structure"] ]
3082
for k in ("name", "host"):
3251
value["client_structure"]]
3252
# .name, .host, and .checker_command
3253
for k in ("name", "host", "checker_command"):
3083
3254
if isinstance(value[k], bytes):
3084
3255
value[k] = value[k].decode("utf-8")
3085
## old_client_settings
3256
if "key_id" not in value:
3257
value["key_id"] = ""
3258
elif "fingerprint" not in value:
3259
value["fingerprint"] = ""
3260
# old_client_settings
3087
3262
old_client_settings = {
3088
3263
(key.decode("utf-8")
3089
3264
if isinstance(key, bytes)
3090
3265
else key): value
3091
3266
for key, value in
3092
bytes_old_client_settings.items() }
3267
bytes_old_client_settings.items()}
3268
del bytes_old_client_settings
3269
# .host and .checker_command
3094
3270
for value in old_client_settings.values():
3095
value["host"] = value["host"].decode("utf-8")
3271
for attribute in ("host", "checker_command"):
3272
if isinstance(value[attribute], bytes):
3273
value[attribute] = (value[attribute]
3096
3275
os.remove(stored_state_path)
3097
3276
except IOError as e:
3098
3277
if e.errno == errno.ENOENT: