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
14
# Copyright © 2008-2016 Teddy Hogeborn
15
# Copyright © 2008-2016 Björn Påhlsson
17
17
# This program is free software: you can redistribute it and/or modify
18
18
# it under the terms of the GNU General Public License as published by
34
34
from __future__ import (division, absolute_import, print_function,
37
from future_builtins import *
38
from future_builtins import *
40
43
import SocketServer as socketserver
78
81
import dbus.service
82
from gi.repository import GObject as gobject
82
from gi.repository import GLib
84
83
from dbus.mainloop.glib import DBusGMainLoop
87
86
import xml.dom.minidom
89
# Try to find the value of SO_BINDTODEVICE:
91
# This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and
92
# newer, and it is also the most natural place for it:
91
93
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
92
94
except AttributeError:
96
# This is where SO_BINDTODEVICE was up to and including Python
94
98
from IN import SO_BINDTODEVICE
95
99
except ImportError:
96
SO_BINDTODEVICE = None
100
# In Python 2.7 it seems to have been removed entirely.
101
# Try running the C preprocessor:
103
cc = subprocess.Popen(["cc", "--language=c", "-E",
105
stdin=subprocess.PIPE,
106
stdout=subprocess.PIPE)
107
stdout = cc.communicate(
108
"#include <sys/socket.h>\nSO_BINDTODEVICE\n")[0]
109
SO_BINDTODEVICE = int(stdout.splitlines()[-1])
110
except (OSError, ValueError, IndexError):
112
SO_BINDTODEVICE = None
98
114
if sys.version_info.major == 2:
102
118
stored_state_file = "clients.pickle"
104
120
logger = logging.getLogger()
119
135
return interface_index
138
def copy_function(func):
139
"""Make a copy of a function"""
140
if sys.version_info.major == 2:
141
return types.FunctionType(func.func_code,
147
return types.FunctionType(func.__code__,
122
154
def initlogger(debug, level=logging.WARNING):
123
155
"""init logger and add loglevel"""
152
184
def __init__(self):
153
185
self.tempdir = tempfile.mkdtemp(prefix="mandos-")
188
output = subprocess.check_output(["gpgconf"])
189
for line in output.splitlines():
190
name, text, path = line.split(b":")
195
if e.errno != errno.ENOENT:
154
197
self.gnupgargs = ['--batch',
155
'--home', self.tempdir,
198
'--homedir', self.tempdir,
201
# Only GPG version 1 has the --no-use-agent option.
202
if self.gpg == "gpg" or self.gpg.endswith("/gpg"):
203
self.gnupgargs.append("--no-use-agent")
160
205
def __enter__(self):
197
242
dir=self.tempdir) as passfile:
198
243
passfile.write(passphrase)
200
proc = subprocess.Popen(['gpg', '--symmetric',
245
proc = subprocess.Popen([self.gpg, '--symmetric',
201
246
'--passphrase-file',
203
248
+ self.gnupgargs,
215
260
dir = self.tempdir) as passfile:
216
261
passfile.write(passphrase)
218
proc = subprocess.Popen(['gpg', '--decrypt',
263
proc = subprocess.Popen([self.gpg, '--decrypt',
219
264
'--passphrase-file',
221
266
+ self.gnupgargs,
227
272
raise PGPError(err)
228
273
return decrypted_plaintext
275
# Pretend that we have an Avahi module
277
"""This isn't so much a class as it is a module-like namespace.
278
It is instantiated once, and simulates having an Avahi module."""
279
IF_UNSPEC = -1 # avahi-common/address.h
280
PROTO_UNSPEC = -1 # avahi-common/address.h
281
PROTO_INET = 0 # avahi-common/address.h
282
PROTO_INET6 = 1 # avahi-common/address.h
283
DBUS_NAME = "org.freedesktop.Avahi"
284
DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup"
285
DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
286
DBUS_PATH_SERVER = "/"
287
def string_array_to_txt_array(self, t):
288
return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
289
for s in t), signature="ay")
290
ENTRY_GROUP_ESTABLISHED = 2 # avahi-common/defs.h
291
ENTRY_GROUP_COLLISION = 3 # avahi-common/defs.h
292
ENTRY_GROUP_FAILURE = 4 # avahi-common/defs.h
293
SERVER_INVALID = 0 # avahi-common/defs.h
294
SERVER_REGISTERING = 1 # avahi-common/defs.h
295
SERVER_RUNNING = 2 # avahi-common/defs.h
296
SERVER_COLLISION = 3 # avahi-common/defs.h
297
SERVER_FAILURE = 4 # avahi-common/defs.h
231
300
class AvahiError(Exception):
232
301
def __init__(self, value, *args, **kwargs):
476
545
openpgp_crt_t = ctypes.POINTER(openpgp_crt_int)
477
546
openpgp_crt_fmt_t = ctypes.c_int # gnutls/openpgp.h
478
547
log_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
479
credentials_type_t = ctypes.c_int #
548
credentials_type_t = ctypes.c_int
480
549
transport_ptr_t = ctypes.c_void_p
481
550
close_request_t = ctypes.c_int
704
773
checker: subprocess.Popen(); a running checker process used
705
774
to see if the client lives.
706
775
'None' if no process is running.
707
checker_callback_tag: a gobject event source tag, or None
776
checker_callback_tag: a GLib event source tag, or None
708
777
checker_command: string; External command which is run to check
709
778
if client lives. %() expansions are done at
710
779
runtime with vars(self) as dict, so that for
711
780
instance %(name)s can be used in the command.
712
checker_initiator_tag: a gobject event source tag, or None
781
checker_initiator_tag: a GLib event source tag, or None
713
782
created: datetime.datetime(); (UTC) object creation
714
783
client_structure: Object describing what attributes a client has
715
784
and is used for storing the client at exit
716
785
current_checker_command: string; current running checker_command
717
disable_initiator_tag: a gobject event source tag, or None
786
disable_initiator_tag: a GLib event source tag, or None
719
788
fingerprint: string (40 or 32 hexadecimal digits); used to
720
789
uniquely identify the client
783
852
client["fingerprint"] = (section["fingerprint"].upper()
784
853
.replace(" ", ""))
785
854
if "secret" in section:
786
client["secret"] = section["secret"].decode("base64")
855
client["secret"] = codecs.decode(section["secret"]
787
858
elif "secfile" in section:
788
859
with open(os.path.expanduser(os.path.expandvars
789
860
(section["secfile"])),
842
913
self.changedstate = multiprocessing_manager.Condition(
843
914
multiprocessing_manager.Lock())
844
915
self.client_structure = [attr
845
for attr in self.__dict__.iterkeys()
916
for attr in self.__dict__.keys()
846
917
if not attr.startswith("_")]
847
918
self.client_structure.append("client_structure")
875
946
logger.info("Disabling client %s", self.name)
876
947
if getattr(self, "disable_initiator_tag", None) is not None:
877
gobject.source_remove(self.disable_initiator_tag)
948
GLib.source_remove(self.disable_initiator_tag)
878
949
self.disable_initiator_tag = None
879
950
self.expires = None
880
951
if getattr(self, "checker_initiator_tag", None) is not None:
881
gobject.source_remove(self.checker_initiator_tag)
952
GLib.source_remove(self.checker_initiator_tag)
882
953
self.checker_initiator_tag = None
883
954
self.stop_checker()
884
955
self.enabled = False
886
957
self.send_changedstate()
887
# Do not run this again if called by a gobject.timeout_add
958
# Do not run this again if called by a GLib.timeout_add
890
961
def __del__(self):
894
965
# Schedule a new checker to be started an 'interval' from now,
895
966
# and every interval from then on.
896
967
if self.checker_initiator_tag is not None:
897
gobject.source_remove(self.checker_initiator_tag)
898
self.checker_initiator_tag = gobject.timeout_add(
968
GLib.source_remove(self.checker_initiator_tag)
969
self.checker_initiator_tag = GLib.timeout_add(
899
970
int(self.interval.total_seconds() * 1000),
900
971
self.start_checker)
901
972
# Schedule a disable() when 'timeout' has passed
902
973
if self.disable_initiator_tag is not None:
903
gobject.source_remove(self.disable_initiator_tag)
904
self.disable_initiator_tag = gobject.timeout_add(
974
GLib.source_remove(self.disable_initiator_tag)
975
self.disable_initiator_tag = GLib.timeout_add(
905
976
int(self.timeout.total_seconds() * 1000), self.disable)
906
977
# Also start a new checker *right now*.
907
978
self.start_checker()
943
1014
if timeout is None:
944
1015
timeout = self.timeout
945
1016
if self.disable_initiator_tag is not None:
946
gobject.source_remove(self.disable_initiator_tag)
1017
GLib.source_remove(self.disable_initiator_tag)
947
1018
self.disable_initiator_tag = None
948
1019
if getattr(self, "enabled", False):
949
self.disable_initiator_tag = gobject.timeout_add(
1020
self.disable_initiator_tag = GLib.timeout_add(
950
1021
int(timeout.total_seconds() * 1000), self.disable)
951
1022
self.expires = datetime.datetime.utcnow() + timeout
1007
1078
args = (pipe[1], subprocess.call, command),
1008
1079
kwargs = popen_args)
1009
1080
self.checker.start()
1010
self.checker_callback_tag = gobject.io_add_watch(
1011
pipe[0].fileno(), gobject.IO_IN,
1081
self.checker_callback_tag = GLib.io_add_watch(
1082
pipe[0].fileno(), GLib.IO_IN,
1012
1083
self.checker_callback, pipe[0], command)
1013
# Re-run this periodically if run by gobject.timeout_add
1084
# Re-run this periodically if run by GLib.timeout_add
1016
1087
def stop_checker(self):
1017
1088
"""Force the checker process, if any, to stop."""
1018
1089
if self.checker_callback_tag:
1019
gobject.source_remove(self.checker_callback_tag)
1090
GLib.source_remove(self.checker_callback_tag)
1020
1091
self.checker_callback_tag = None
1021
1092
if getattr(self, "checker", None) is None:
1496
1567
interface_names.add(alt_interface)
1497
1568
# Is this a D-Bus signal?
1498
1569
if getattr(attribute, "_dbus_is_signal", False):
1570
# Extract the original non-method undecorated
1571
# function by black magic
1499
1572
if sys.version_info.major == 2:
1500
# Extract the original non-method undecorated
1501
# function by black magic
1502
1573
nonmethod_func = (dict(
1503
1574
zip(attribute.func_code.co_freevars,
1504
1575
attribute.__closure__))
1505
1576
["func"].cell_contents)
1507
nonmethod_func = attribute
1578
nonmethod_func = (dict(
1579
zip(attribute.__code__.co_freevars,
1580
attribute.__closure__))
1581
["func"].cell_contents)
1508
1582
# Create a new, but exactly alike, function
1509
1583
# object, and decorate it to be a new D-Bus signal
1510
1584
# with the alternate D-Bus interface name
1511
if sys.version_info.major == 2:
1512
new_function = types.FunctionType(
1513
nonmethod_func.func_code,
1514
nonmethod_func.func_globals,
1515
nonmethod_func.func_name,
1516
nonmethod_func.func_defaults,
1517
nonmethod_func.func_closure)
1519
new_function = types.FunctionType(
1520
nonmethod_func.__code__,
1521
nonmethod_func.__globals__,
1522
nonmethod_func.__name__,
1523
nonmethod_func.__defaults__,
1524
nonmethod_func.__closure__)
1585
new_function = copy_function(nonmethod_func)
1525
1586
new_function = (dbus.service.signal(
1527
1588
attribute._dbus_signature)(new_function))
1567
1628
attribute._dbus_in_signature,
1568
1629
attribute._dbus_out_signature)
1569
(types.FunctionType(attribute.func_code,
1570
attribute.func_globals,
1571
attribute.func_name,
1572
attribute.func_defaults,
1573
attribute.func_closure)))
1630
(copy_function(attribute)))
1574
1631
# Copy annotations, if any
1576
1633
attr[attrname]._dbus_annotations = dict(
1588
1645
attribute._dbus_access,
1589
1646
attribute._dbus_get_args_options
1590
1647
["byte_arrays"])
1591
(types.FunctionType(
1592
attribute.func_code,
1593
attribute.func_globals,
1594
attribute.func_name,
1595
attribute.func_defaults,
1596
attribute.func_closure)))
1648
(copy_function(attribute)))
1597
1649
# Copy annotations, if any
1599
1651
attr[attrname]._dbus_annotations = dict(
1608
1660
# to the class.
1609
1661
attr[attrname] = (
1610
1662
dbus_interface_annotations(alt_interface)
1611
(types.FunctionType(attribute.func_code,
1612
attribute.func_globals,
1613
attribute.func_name,
1614
attribute.func_defaults,
1615
attribute.func_closure)))
1663
(copy_function(attribute)))
1617
1665
# Deprecate all alternate interfaces
1618
1666
iname="_AlternateDBusNames_interface_annotation{}"
1631
1679
if interface_names:
1632
1680
# Replace the class with a new subclass of it with
1633
1681
# methods, signals, etc. as created above.
1634
cls = type(b"{}Alternate".format(cls.__name__),
1682
if sys.version_info.major == 2:
1683
cls = type(b"{}Alternate".format(cls.__name__),
1686
cls = type("{}Alternate".format(cls.__name__),
1797
1849
def approve(self, value=True):
1798
1850
self.approved = value
1799
gobject.timeout_add(int(self.approval_duration.total_seconds()
1800
* 1000), self._reset_approved)
1851
GLib.timeout_add(int(self.approval_duration.total_seconds()
1852
* 1000), self._reset_approved)
1801
1853
self.send_changedstate()
1803
1855
## D-Bus methods, signals & properties
2013
2065
if (getattr(self, "disable_initiator_tag", None)
2016
gobject.source_remove(self.disable_initiator_tag)
2017
self.disable_initiator_tag = gobject.timeout_add(
2068
GLib.source_remove(self.disable_initiator_tag)
2069
self.disable_initiator_tag = GLib.timeout_add(
2018
2070
int((self.expires - now).total_seconds() * 1000),
2041
2093
if self.enabled:
2042
2094
# Reschedule checker run
2043
gobject.source_remove(self.checker_initiator_tag)
2044
self.checker_initiator_tag = gobject.timeout_add(
2095
GLib.source_remove(self.checker_initiator_tag)
2096
self.checker_initiator_tag = GLib.timeout_add(
2045
2097
value, self.start_checker)
2046
2098
self.start_checker() # Start one now, too
2139
2191
priority = self.server.gnutls_priority
2140
2192
if priority is None:
2141
2193
priority = "NORMAL"
2142
gnutls.priority_set_direct(session._c_object, priority,
2194
gnutls.priority_set_direct(session._c_object,
2195
priority.encode("utf-8"),
2145
2198
# Start communication using the Mandos protocol
2400
2453
"""This overrides the normal server_bind() function
2401
2454
to bind to an interface if one was specified, and also NOT to
2402
2455
bind to an address or port if they were not specified."""
2456
global SO_BINDTODEVICE
2403
2457
if self.interface is not None:
2404
2458
if SO_BINDTODEVICE is None:
2405
logger.error("SO_BINDTODEVICE does not exist;"
2406
" cannot bind to interface %s",
2410
self.socket.setsockopt(
2411
socket.SOL_SOCKET, SO_BINDTODEVICE,
2412
(self.interface + "\0").encode("utf-8"))
2413
except socket.error as error:
2414
if error.errno == errno.EPERM:
2415
logger.error("No permission to bind to"
2416
" interface %s", self.interface)
2417
elif error.errno == errno.ENOPROTOOPT:
2418
logger.error("SO_BINDTODEVICE not available;"
2419
" cannot bind to interface %s",
2421
elif error.errno == errno.ENODEV:
2422
logger.error("Interface %s does not exist,"
2423
" cannot bind", self.interface)
2459
# Fall back to a hard-coded value which seems to be
2461
logger.warning("SO_BINDTODEVICE not found, trying 25")
2462
SO_BINDTODEVICE = 25
2464
self.socket.setsockopt(
2465
socket.SOL_SOCKET, SO_BINDTODEVICE,
2466
(self.interface + "\0").encode("utf-8"))
2467
except socket.error as error:
2468
if error.errno == errno.EPERM:
2469
logger.error("No permission to bind to"
2470
" interface %s", self.interface)
2471
elif error.errno == errno.ENOPROTOOPT:
2472
logger.error("SO_BINDTODEVICE not available;"
2473
" cannot bind to interface %s",
2475
elif error.errno == errno.ENODEV:
2476
logger.error("Interface %s does not exist,"
2477
" cannot bind", self.interface)
2426
2480
# Only bind(2) the socket if we really need to.
2427
2481
if self.server_address[0] or self.server_address[1]:
2428
2482
if not self.server_address[0]:
2451
2505
gnutls_priority GnuTLS priority string
2452
2506
use_dbus: Boolean; to emit D-Bus signals or not
2454
Assumes a gobject.MainLoop event loop.
2508
Assumes a GLib.MainLoop event loop.
2457
2511
def __init__(self, server_address, RequestHandlerClass,
2483
2537
def add_pipe(self, parent_pipe, proc):
2484
2538
# Call "handle_ipc" for both data and EOF events
2485
gobject.io_add_watch(
2486
2540
parent_pipe.fileno(),
2487
gobject.IO_IN | gobject.IO_HUP,
2541
GLib.IO_IN | GLib.IO_HUP,
2488
2542
functools.partial(self.handle_ipc,
2489
2543
parent_pipe = parent_pipe,
2521
2575
parent_pipe.send(False)
2524
gobject.io_add_watch(
2525
2579
parent_pipe.fileno(),
2526
gobject.IO_IN | gobject.IO_HUP,
2580
GLib.IO_IN | GLib.IO_HUP,
2527
2581
functools.partial(self.handle_ipc,
2528
2582
parent_pipe = parent_pipe,
2911
2965
logger.error("Could not open file %r", pidfilename,
2914
for name in ("_mandos", "mandos", "nobody"):
2968
for name, group in (("_mandos", "_mandos"),
2969
("mandos", "mandos"),
2970
("nobody", "nogroup")):
2916
2972
uid = pwd.getpwnam(name).pw_uid
2917
gid = pwd.getpwnam(name).pw_gid
2973
gid = pwd.getpwnam(group).pw_gid
2919
2975
except KeyError:
2984
logger.debug("Did setuid/setgid to {}:{}".format(uid,
2927
2986
except OSError as error:
2987
logger.warning("Failed to setuid/setgid to {}:{}: {}"
2988
.format(uid, gid, os.strerror(error.errno)))
2928
2989
if error.errno != errno.EPERM:
2952
3013
# Close all input and output, do double fork, etc.
2955
# multiprocessing will use threads, so before we use gobject we
2956
# need to inform gobject that threads will be used.
2957
gobject.threads_init()
3016
# multiprocessing will use threads, so before we use GLib we need
3017
# to inform GLib that threads will be used.
2959
3020
global main_loop
2960
3021
# From the Avahi example code
2961
3022
DBusGMainLoop(set_as_default=True)
2962
main_loop = gobject.MainLoop()
3023
main_loop = GLib.MainLoop()
2963
3024
bus = dbus.SystemBus()
2964
3025
# End of Avahi example code
3009
3070
if server_settings["restore"]:
3011
3072
with open(stored_state_path, "rb") as stored_state:
3012
clients_data, old_client_settings = pickle.load(
3073
if sys.version_info.major == 2:
3074
clients_data, old_client_settings = pickle.load(
3077
bytes_clients_data, bytes_old_client_settings = (
3078
pickle.load(stored_state, encoding = "bytes"))
3079
### Fix bytes to strings
3082
clients_data = { (key.decode("utf-8")
3083
if isinstance(key, bytes)
3086
bytes_clients_data.items() }
3087
del bytes_clients_data
3088
for key in clients_data:
3089
value = { (k.decode("utf-8")
3090
if isinstance(k, bytes) else k): v
3092
clients_data[key].items() }
3093
clients_data[key] = value
3095
value["client_structure"] = [
3097
if isinstance(s, bytes)
3099
value["client_structure"] ]
3101
for k in ("name", "host"):
3102
if isinstance(value[k], bytes):
3103
value[k] = value[k].decode("utf-8")
3104
## old_client_settings
3106
old_client_settings = {
3107
(key.decode("utf-8")
3108
if isinstance(key, bytes)
3111
bytes_old_client_settings.items() }
3112
del bytes_old_client_settings
3114
for value in old_client_settings.values():
3115
if isinstance(value["host"], bytes):
3116
value["host"] = (value["host"]
3014
3118
os.remove(stored_state_path)
3015
3119
except IOError as e:
3016
3120
if e.errno == errno.ENOENT:
3118
3222
del pidfilename
3120
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
3121
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
3224
for termsig in (signal.SIGHUP, signal.SIGTERM):
3225
GLib.unix_signal_add(GLib.PRIORITY_HIGH, termsig,
3226
lambda: main_loop.quit() and False)
3166
3271
return dbus.Dictionary(
3167
3272
{ c.dbus_object_path: c.GetAll(
3168
3273
"se.recompile.Mandos.Client")
3169
for c in tcp_server.clients.itervalues() },
3274
for c in tcp_server.clients.values() },
3170
3275
signature="oa{sv}")
3172
3277
@dbus.service.method(_interface, in_signature="o")
3173
3278
def RemoveClient(self, object_path):
3175
for c in tcp_server.clients.itervalues():
3280
for c in tcp_server.clients.values():
3176
3281
if c.dbus_object_path == object_path:
3177
3282
del tcp_server.clients[c.name]
3178
3283
c.remove_from_connection()
3224
3329
mandos_dbus_service = MandosDBusService()
3331
# Save modules to variables to exempt the modules from being
3332
# unloaded before the function registered with atexit() is run.
3333
mp = multiprocessing
3227
3336
"Cleanup function; run on exit"
3229
3338
service.cleanup()
3231
multiprocessing.active_children()
3340
mp.active_children()
3233
3342
if not (tcp_server.clients or client_settings):
3238
3347
# removed/edited, old secret will thus be unrecovable.
3240
3349
with PGPEngine() as pgp:
3241
for client in tcp_server.clients.itervalues():
3350
for client in tcp_server.clients.values():
3242
3351
key = client_settings[client.name]["secret"]
3243
3352
client.encrypted_secret = pgp.encrypt(client.secret,
3268
3377
prefix='clients-',
3269
3378
dir=os.path.dirname(stored_state_path),
3270
3379
delete=False) as stored_state:
3271
pickle.dump((clients, client_settings), stored_state)
3380
pickle.dump((clients, client_settings), stored_state,
3272
3382
tempname = stored_state.name
3273
3383
os.rename(tempname, stored_state_path)
3274
3384
except (IOError, OSError) as e:
3335
3445
# End of Avahi example code
3337
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
3338
lambda *args, **kwargs:
3339
(tcp_server.handle_request
3340
(*args[2:], **kwargs) or True))
3447
GLib.io_add_watch(tcp_server.fileno(), GLib.IO_IN,
3448
lambda *args, **kwargs:
3449
(tcp_server.handle_request
3450
(*args[2:], **kwargs) or True))
3342
3452
logger.debug("Starting main loop")
3343
3453
main_loop.run()