188
171
def password_encode(self, password):
189
172
# Passphrase can not be empty and can not contain newlines or
190
173
# NUL bytes. So we prefix it and hex encode it.
191
encoded = b"mandos" + binascii.hexlify(password)
192
if len(encoded) > 2048:
193
# GnuPG can't handle long passwords, so encode differently
194
encoded = (b"mandos" + password.replace(b"\\", b"\\\\")
195
.replace(b"\n", b"\\n")
196
.replace(b"\0", b"\\x00"))
174
return b"mandos" + binascii.hexlify(password)
199
176
def encrypt(self, data, password):
200
passphrase = self.password_encode(password)
201
with tempfile.NamedTemporaryFile(
202
dir=self.tempdir) as passfile:
203
passfile.write(passphrase)
205
proc = subprocess.Popen(['gpg', '--symmetric',
209
stdin = subprocess.PIPE,
210
stdout = subprocess.PIPE,
211
stderr = subprocess.PIPE)
212
ciphertext, err = proc.communicate(input = data)
213
if proc.returncode != 0:
177
self.gnupg.passphrase = self.password_encode(password)
178
with open(os.devnull) as devnull:
180
proc = self.gnupg.run(['--symmetric'],
181
create_fhs=['stdin', 'stdout'],
182
attach_fhs={'stderr': devnull})
183
with contextlib.closing(proc.handles['stdin']) as f:
185
with contextlib.closing(proc.handles['stdout']) as f:
186
ciphertext = f.read()
190
self.gnupg.passphrase = None
215
191
return ciphertext
217
193
def decrypt(self, data, password):
218
passphrase = self.password_encode(password)
219
with tempfile.NamedTemporaryFile(
220
dir = self.tempdir) as passfile:
221
passfile.write(passphrase)
223
proc = subprocess.Popen(['gpg', '--decrypt',
227
stdin = subprocess.PIPE,
228
stdout = subprocess.PIPE,
229
stderr = subprocess.PIPE)
230
decrypted_plaintext, err = proc.communicate(input = data)
231
if proc.returncode != 0:
194
self.gnupg.passphrase = self.password_encode(password)
195
with open(os.devnull) as devnull:
197
proc = self.gnupg.run(['--decrypt'],
198
create_fhs=['stdin', 'stdout'],
199
attach_fhs={'stderr': devnull})
200
with contextlib.closing(proc.handles['stdin'] ) as f:
202
with contextlib.closing(proc.handles['stdout']) as f:
203
decrypted_plaintext = f.read()
207
self.gnupg.passphrase = None
233
208
return decrypted_plaintext
236
212
class AvahiError(Exception):
237
213
def __init__(self, value, *args, **kwargs):
238
214
self.value = value
239
return super(AvahiError, self).__init__(value, *args,
215
super(AvahiError, self).__init__(value, *args, **kwargs)
216
def __unicode__(self):
217
return unicode(repr(self.value))
243
219
class AvahiServiceError(AvahiError):
247
222
class AvahiGroupError(AvahiError):
643
598
def init_checker(self):
644
599
# Schedule a new checker to be started an 'interval' from now,
645
600
# and every interval from then on.
646
if self.checker_initiator_tag is not None:
647
gobject.source_remove(self.checker_initiator_tag)
648
self.checker_initiator_tag = gobject.timeout_add(
649
int(self.interval.total_seconds() * 1000),
601
self.checker_initiator_tag = (gobject.timeout_add
602
(self.interval_milliseconds(),
651
604
# Schedule a disable() when 'timeout' has passed
652
if self.disable_initiator_tag is not None:
653
gobject.source_remove(self.disable_initiator_tag)
654
self.disable_initiator_tag = gobject.timeout_add(
655
int(self.timeout.total_seconds() * 1000), self.disable)
605
self.disable_initiator_tag = (gobject.timeout_add
606
(self.timeout_milliseconds(),
656
608
# Also start a new checker *right now*.
657
609
self.start_checker()
659
def checker_callback(self, source, condition, connection,
611
def checker_callback(self, pid, condition, command):
661
612
"""The checker has completed, so take appropriate actions."""
662
613
self.checker_callback_tag = None
663
614
self.checker = None
664
# Read return code from connection (see call_pipe)
665
returncode = connection.recv()
669
self.last_checker_status = returncode
670
self.last_checker_signal = None
615
if os.WIFEXITED(condition):
616
self.last_checker_status = os.WEXITSTATUS(condition)
671
617
if self.last_checker_status == 0:
672
618
logger.info("Checker for %(name)s succeeded",
674
620
self.checked_ok()
676
logger.info("Checker for %(name)s failed", vars(self))
622
logger.info("Checker for %(name)s failed",
678
625
self.last_checker_status = -1
679
self.last_checker_signal = -returncode
680
626
logger.warning("Checker for %(name)s crashed?",
684
def checked_ok(self):
685
"""Assert that the client has been seen, alive and well."""
686
self.last_checked_ok = datetime.datetime.utcnow()
687
self.last_checker_status = 0
688
self.last_checker_signal = None
691
def bump_timeout(self, timeout=None):
692
"""Bump up the timeout for this client."""
629
def checked_ok(self, timeout=None):
630
"""Bump up the timeout for this client.
632
This should only be called when the client has been seen,
693
635
if timeout is None:
694
636
timeout = self.timeout
637
self.last_checked_ok = datetime.datetime.utcnow()
695
638
if self.disable_initiator_tag is not None:
696
639
gobject.source_remove(self.disable_initiator_tag)
697
self.disable_initiator_tag = None
698
640
if getattr(self, "enabled", False):
699
self.disable_initiator_tag = gobject.timeout_add(
700
int(timeout.total_seconds() * 1000), self.disable)
641
self.disable_initiator_tag = (gobject.timeout_add
642
(timedelta_to_milliseconds
643
(timeout), self.disable))
701
644
self.expires = datetime.datetime.utcnow() + timeout
703
646
def need_approval(self):
709
652
If a checker already exists, leave it running and do
711
654
# The reason for not killing a running checker is that if we
712
# did that, and if a checker (for some reason) started running
713
# slowly and taking more than 'interval' time, then the client
714
# would inevitably timeout, since no checker would get a
715
# chance to run to completion. If we instead leave running
655
# did that, then if a checker (for some reason) started
656
# running slowly and taking more than 'interval' time, the
657
# client would inevitably timeout, since no checker would get
658
# a chance to run to completion. If we instead leave running
716
659
# checkers alone, the checker would have to take more time
717
660
# than 'timeout' for the client to be disabled, which is as it
720
if self.checker is not None and not self.checker.is_alive():
721
logger.warning("Checker was not alive; joining")
663
# If a checker exists, make sure it is not a zombie
665
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
666
except (AttributeError, OSError) as error:
667
if (isinstance(error, OSError)
668
and error.errno != errno.ECHILD):
672
logger.warning("Checker was a zombie")
673
gobject.source_remove(self.checker_callback_tag)
674
self.checker_callback(pid, status,
675
self.current_checker_command)
724
676
# Start a new checker if needed
725
677
if self.checker is None:
726
# Escape attributes for the shell
728
attr: re.escape(str(getattr(self, attr)))
729
for attr in self.runtime_expansions }
731
command = self.checker_command % escaped_attrs
732
except TypeError as error:
733
logger.error('Could not format string "%s"',
734
self.checker_command,
736
return True # Try again later
679
# In case checker_command has exactly one % operator
680
command = self.checker_command % self.host
682
# Escape attributes for the shell
683
escaped_attrs = dict(
685
re.escape(unicode(str(getattr(self, attr, "")),
689
self.runtime_expansions)
692
command = self.checker_command % escaped_attrs
693
except TypeError as error:
694
logger.error('Could not format string "%s":'
695
' %s', self.checker_command, error)
696
return True # Try again later
737
697
self.current_checker_command = command
738
logger.info("Starting checker %r for %s", command,
740
# We don't need to redirect stdout and stderr, since
741
# in normal mode, that is already done by daemon(),
742
# and in debug mode we don't want to. (Stdin is
743
# always replaced by /dev/null.)
744
# The exception is when not debugging but nevertheless
745
# running in the foreground; use the previously
747
popen_args = { "close_fds": True,
750
if (not self.server_settings["debug"]
751
and self.server_settings["foreground"]):
752
popen_args.update({"stdout": wnull,
754
pipe = multiprocessing.Pipe(duplex = False)
755
self.checker = multiprocessing.Process(
757
args = (pipe[1], subprocess.call, command),
760
self.checker_callback_tag = gobject.io_add_watch(
761
pipe[0].fileno(), gobject.IO_IN,
762
self.checker_callback, pipe[0], command)
699
logger.info("Starting checker %r for %s",
701
# We don't need to redirect stdout and stderr, since
702
# in normal mode, that is already done by daemon(),
703
# and in debug mode we don't want to. (Stdin is
704
# always replaced by /dev/null.)
705
self.checker = subprocess.Popen(command,
708
self.checker_callback_tag = (gobject.child_watch_add
710
self.checker_callback,
712
# The checker may have completed before the gobject
713
# watch was added. Check for this.
714
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
716
gobject.source_remove(self.checker_callback_tag)
717
self.checker_callback(pid, status, command)
718
except OSError as error:
719
logger.error("Failed to start subprocess: %s",
763
721
# Re-run this periodically if run by gobject.timeout_add
873
class DBusObjectWithAnnotations(dbus.service.Object):
874
"""A D-Bus object with annotations.
876
Classes inheriting from this can use the dbus_annotations
877
decorator to add annotations to methods or signals.
881
def _is_dbus_thing(thing):
882
"""Returns a function testing if an attribute is a D-Bus thing
884
If called like _is_dbus_thing("method") it returns a function
885
suitable for use as predicate to inspect.getmembers().
887
return lambda obj: getattr(obj, "_dbus_is_{}".format(thing),
890
def _get_all_dbus_things(self, thing):
891
"""Returns a generator of (name, attribute) pairs
893
return ((getattr(athing.__get__(self), "_dbus_name", name),
894
athing.__get__(self))
895
for cls in self.__class__.__mro__
897
inspect.getmembers(cls, self._is_dbus_thing(thing)))
899
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
901
path_keyword = 'object_path',
902
connection_keyword = 'connection')
903
def Introspect(self, object_path, connection):
904
"""Overloading of standard D-Bus method.
906
Inserts annotation tags on methods and signals.
908
xmlstring = dbus.service.Object.Introspect(self, object_path,
911
document = xml.dom.minidom.parseString(xmlstring)
913
for if_tag in document.getElementsByTagName("interface"):
914
# Add annotation tags
915
for typ in ("method", "signal"):
916
for tag in if_tag.getElementsByTagName(typ):
918
for name, prop in (self.
919
_get_all_dbus_things(typ)):
920
if (name == tag.getAttribute("name")
921
and prop._dbus_interface
922
== if_tag.getAttribute("name")):
923
annots.update(getattr(
924
prop, "_dbus_annotations", {}))
925
for name, value in annots.items():
926
ann_tag = document.createElement(
928
ann_tag.setAttribute("name", name)
929
ann_tag.setAttribute("value", value)
930
tag.appendChild(ann_tag)
931
# Add interface annotation tags
932
for annotation, value in dict(
933
itertools.chain.from_iterable(
934
annotations().items()
935
for name, annotations
936
in self._get_all_dbus_things("interface")
937
if name == if_tag.getAttribute("name")
939
ann_tag = document.createElement("annotation")
940
ann_tag.setAttribute("name", annotation)
941
ann_tag.setAttribute("value", value)
942
if_tag.appendChild(ann_tag)
943
# Fix argument name for the Introspect method itself
944
if (if_tag.getAttribute("name")
945
== dbus.INTROSPECTABLE_IFACE):
946
for cn in if_tag.getElementsByTagName("method"):
947
if cn.getAttribute("name") == "Introspect":
948
for arg in cn.getElementsByTagName("arg"):
949
if (arg.getAttribute("direction")
951
arg.setAttribute("name",
953
xmlstring = document.toxml("utf-8")
955
except (AttributeError, xml.dom.DOMException,
956
xml.parsers.expat.ExpatError) as error:
957
logger.error("Failed to override Introspection method",
962
class DBusObjectWithProperties(DBusObjectWithAnnotations):
792
class DBusObjectWithProperties(dbus.service.Object):
963
793
"""A D-Bus object with properties.
965
795
Classes inheriting from this can use the dbus_service_property
1045
875
if not hasattr(value, "variant_level"):
1046
876
properties[name] = value
1048
properties[name] = type(value)(
1049
value, variant_level = value.variant_level + 1)
878
properties[name] = type(value)(value, variant_level=
879
value.variant_level+1)
1050
880
return dbus.Dictionary(properties, signature="sv")
1052
@dbus.service.signal(dbus.PROPERTIES_IFACE, signature="sa{sv}as")
1053
def PropertiesChanged(self, interface_name, changed_properties,
1054
invalidated_properties):
1055
"""Standard D-Bus PropertiesChanged() signal, see D-Bus
1060
882
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1061
883
out_signature="s",
1062
884
path_keyword='object_path',
1063
885
connection_keyword='connection')
1064
886
def Introspect(self, object_path, connection):
1065
"""Overloading of standard D-Bus method.
1067
Inserts property tags and interface annotation tags.
887
"""Standard D-Bus method, overloaded to insert property tags.
1069
xmlstring = DBusObjectWithAnnotations.Introspect(self,
889
xmlstring = dbus.service.Object.Introspect(self, object_path,
1073
892
document = xml.dom.minidom.parseString(xmlstring)
1075
893
def make_tag(document, name, prop):
1076
894
e = document.createElement("property")
1077
895
e.setAttribute("name", name)
1078
896
e.setAttribute("type", prop._dbus_signature)
1079
897
e.setAttribute("access", prop._dbus_access)
1082
899
for if_tag in document.getElementsByTagName("interface"):
1084
900
for tag in (make_tag(document, name, prop)
1086
in self._get_all_dbus_things("property")
902
in self._get_all_dbus_properties()
1087
903
if prop._dbus_interface
1088
904
== if_tag.getAttribute("name")):
1089
905
if_tag.appendChild(tag)
1090
# Add annotation tags for properties
1091
for tag in if_tag.getElementsByTagName("property"):
1093
for name, prop in self._get_all_dbus_things(
1095
if (name == tag.getAttribute("name")
1096
and prop._dbus_interface
1097
== if_tag.getAttribute("name")):
1098
annots.update(getattr(
1099
prop, "_dbus_annotations", {}))
1100
for name, value in annots.items():
1101
ann_tag = document.createElement(
1103
ann_tag.setAttribute("name", name)
1104
ann_tag.setAttribute("value", value)
1105
tag.appendChild(ann_tag)
1106
906
# Add the names to the return values for the
1107
907
# "org.freedesktop.DBus.Properties" methods
1108
908
if (if_tag.getAttribute("name")
1123
923
except (AttributeError, xml.dom.DOMException,
1124
924
xml.parsers.expat.ExpatError) as error:
1125
925
logger.error("Failed to override Introspection method",
1130
dbus.OBJECT_MANAGER_IFACE
1131
except AttributeError:
1132
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
1134
class DBusObjectWithObjectManager(DBusObjectWithAnnotations):
1135
"""A D-Bus object with an ObjectManager.
1137
Classes inheriting from this exposes the standard
1138
GetManagedObjects call and the InterfacesAdded and
1139
InterfacesRemoved signals on the standard
1140
"org.freedesktop.DBus.ObjectManager" interface.
1142
Note: No signals are sent automatically; they must be sent
1145
@dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
1146
out_signature = "a{oa{sa{sv}}}")
1147
def GetManagedObjects(self):
1148
"""This function must be overridden"""
1149
raise NotImplementedError()
1151
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE,
1152
signature = "oa{sa{sv}}")
1153
def InterfacesAdded(self, object_path, interfaces_and_properties):
1156
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature = "oas")
1157
def InterfacesRemoved(self, object_path, interfaces):
1160
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1161
out_signature = "s",
1162
path_keyword = 'object_path',
1163
connection_keyword = 'connection')
1164
def Introspect(self, object_path, connection):
1165
"""Overloading of standard D-Bus method.
1167
Override return argument name of GetManagedObjects to be
1168
"objpath_interfaces_and_properties"
1170
xmlstring = DBusObjectWithAnnotations.Introspect(self,
1174
document = xml.dom.minidom.parseString(xmlstring)
1176
for if_tag in document.getElementsByTagName("interface"):
1177
# Fix argument name for the GetManagedObjects method
1178
if (if_tag.getAttribute("name")
1179
== dbus.OBJECT_MANAGER_IFACE):
1180
for cn in if_tag.getElementsByTagName("method"):
1181
if (cn.getAttribute("name")
1182
== "GetManagedObjects"):
1183
for arg in cn.getElementsByTagName("arg"):
1184
if (arg.getAttribute("direction")
1188
"objpath_interfaces"
1190
xmlstring = document.toxml("utf-8")
1192
except (AttributeError, xml.dom.DOMException,
1193
xml.parsers.expat.ExpatError) as error:
1194
logger.error("Failed to override Introspection method",
1198
def datetime_to_dbus(dt, variant_level=0):
930
def datetime_to_dbus (dt, variant_level=0):
1199
931
"""Convert a UTC datetime.datetime() to a D-Bus type."""
1201
933
return dbus.String("", variant_level = variant_level)
1202
return dbus.String(dt.isoformat(), variant_level=variant_level)
1205
def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
1206
"""A class decorator; applied to a subclass of
1207
dbus.service.Object, it will add alternate D-Bus attributes with
1208
interface names according to the "alt_interface_names" mapping.
1211
@alternate_dbus_interfaces({"org.example.Interface":
1212
"net.example.AlternateInterface"})
1213
class SampleDBusObject(dbus.service.Object):
1214
@dbus.service.method("org.example.Interface")
1215
def SampleDBusMethod():
1218
The above "SampleDBusMethod" on "SampleDBusObject" will be
1219
reachable via two interfaces: "org.example.Interface" and
1220
"net.example.AlternateInterface", the latter of which will have
1221
its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
1222
"true", unless "deprecate" is passed with a False value.
1224
This works for methods and signals, and also for D-Bus properties
1225
(from DBusObjectWithProperties) and interfaces (from the
1226
dbus_interface_annotations decorator).
934
return dbus.String(dt.isoformat(),
935
variant_level=variant_level)
938
class AlternateDBusNamesMetaclass(DBusObjectWithProperties
940
"""Applied to an empty subclass of a D-Bus object, this metaclass
941
will add additional D-Bus attributes matching a certain pattern.
1230
for orig_interface_name, alt_interface_name in (
1231
alt_interface_names.items()):
1233
interface_names = set()
1234
# Go though all attributes of the class
1235
for attrname, attribute in inspect.getmembers(cls):
943
def __new__(mcs, name, bases, attr):
944
# Go through all the base classes which could have D-Bus
945
# methods, signals, or properties in them
946
for base in (b for b in bases
947
if issubclass(b, dbus.service.Object)):
948
# Go though all attributes of the base class
949
for attrname, attribute in inspect.getmembers(base):
1236
950
# Ignore non-D-Bus attributes, and D-Bus attributes
1237
951
# with the wrong interface name
1238
952
if (not hasattr(attribute, "_dbus_interface")
1239
or not attribute._dbus_interface.startswith(
1240
orig_interface_name)):
953
or not attribute._dbus_interface
954
.startswith("se.recompile.Mandos")):
1242
956
# Create an alternate D-Bus interface name based on
1243
957
# the current name
1244
alt_interface = attribute._dbus_interface.replace(
1245
orig_interface_name, alt_interface_name)
1246
interface_names.add(alt_interface)
958
alt_interface = (attribute._dbus_interface
959
.replace("se.recompile.Mandos",
960
"se.bsnet.fukt.Mandos"))
1247
961
# Is this a D-Bus signal?
1248
962
if getattr(attribute, "_dbus_is_signal", False):
1249
if sys.version_info.major == 2:
1250
# Extract the original non-method undecorated
1251
# function by black magic
1252
nonmethod_func = (dict(
963
# Extract the original non-method function by
965
nonmethod_func = (dict(
1253
966
zip(attribute.func_code.co_freevars,
1254
attribute.__closure__))
1255
["func"].cell_contents)
1257
nonmethod_func = attribute
967
attribute.__closure__))["func"]
1258
969
# Create a new, but exactly alike, function
1259
970
# object, and decorate it to be a new D-Bus signal
1260
971
# with the alternate D-Bus interface name
1261
if sys.version_info.major == 2:
1262
new_function = types.FunctionType(
1263
nonmethod_func.func_code,
1264
nonmethod_func.func_globals,
1265
nonmethod_func.func_name,
1266
nonmethod_func.func_defaults,
1267
nonmethod_func.func_closure)
1269
new_function = types.FunctionType(
1270
nonmethod_func.__code__,
1271
nonmethod_func.__globals__,
1272
nonmethod_func.__name__,
1273
nonmethod_func.__defaults__,
1274
nonmethod_func.__closure__)
1275
new_function = (dbus.service.signal(
1277
attribute._dbus_signature)(new_function))
1278
# Copy annotations, if any
1280
new_function._dbus_annotations = dict(
1281
attribute._dbus_annotations)
1282
except AttributeError:
972
new_function = (dbus.service.signal
974
attribute._dbus_signature)
976
nonmethod_func.func_code,
977
nonmethod_func.func_globals,
978
nonmethod_func.func_name,
979
nonmethod_func.func_defaults,
980
nonmethod_func.func_closure)))
1284
981
# Define a creator of a function to call both the
1285
# original and alternate functions, so both the
1286
# original and alternate signals gets sent when
1287
# the function is called
982
# old and new functions, so both the old and new
983
# signals gets sent when the function is called
1288
984
def fixscope(func1, func2):
1289
985
"""This function is a scope container to pass
1290
986
func1 and func2 to the "call_both" function
1291
987
outside of its arguments"""
1293
@functools.wraps(func2)
1294
988
def call_both(*args, **kwargs):
1295
989
"""This function will emit two D-Bus
1296
990
signals by calling func1 and func2"""
1297
991
func1(*args, **kwargs)
1298
992
func2(*args, **kwargs)
1299
# Make wrapper function look like a D-Bus signal
1300
for name, attr in inspect.getmembers(func2):
1301
if name.startswith("_dbus_"):
1302
setattr(call_both, name, attr)
1304
993
return call_both
1305
994
# Create the "call_both" function and add it to
1307
attr[attrname] = fixscope(attribute, new_function)
996
attr[attrname] = fixscope(attribute,
1308
998
# Is this a D-Bus method?
1309
999
elif getattr(attribute, "_dbus_is_method", False):
1310
1000
# Create a new, but exactly alike, function
1311
1001
# object. Decorate it to be a new D-Bus method
1312
1002
# with the alternate D-Bus interface name. Add it
1313
1003
# to the class.
1315
dbus.service.method(
1317
attribute._dbus_in_signature,
1318
attribute._dbus_out_signature)
1319
(types.FunctionType(attribute.func_code,
1320
attribute.func_globals,
1321
attribute.func_name,
1322
attribute.func_defaults,
1323
attribute.func_closure)))
1324
# Copy annotations, if any
1326
attr[attrname]._dbus_annotations = dict(
1327
attribute._dbus_annotations)
1328
except AttributeError:
1004
attr[attrname] = (dbus.service.method
1006
attribute._dbus_in_signature,
1007
attribute._dbus_out_signature)
1009
(attribute.func_code,
1010
attribute.func_globals,
1011
attribute.func_name,
1012
attribute.func_defaults,
1013
attribute.func_closure)))
1330
1014
# Is this a D-Bus property?
1331
1015
elif getattr(attribute, "_dbus_is_property", False):
1332
1016
# Create a new, but exactly alike, function
1333
1017
# object, and decorate it to be a new D-Bus
1334
1018
# property with the alternate D-Bus interface
1335
1019
# name. Add it to the class.
1336
attr[attrname] = (dbus_service_property(
1337
alt_interface, attribute._dbus_signature,
1338
attribute._dbus_access,
1339
attribute._dbus_get_args_options
1341
(types.FunctionType(
1342
attribute.func_code,
1343
attribute.func_globals,
1344
attribute.func_name,
1345
attribute.func_defaults,
1346
attribute.func_closure)))
1347
# Copy annotations, if any
1349
attr[attrname]._dbus_annotations = dict(
1350
attribute._dbus_annotations)
1351
except AttributeError:
1353
# Is this a D-Bus interface?
1354
elif getattr(attribute, "_dbus_is_interface", False):
1355
# Create a new, but exactly alike, function
1356
# object. Decorate it to be a new D-Bus interface
1357
# with the alternate D-Bus interface name. Add it
1360
dbus_interface_annotations(alt_interface)
1361
(types.FunctionType(attribute.func_code,
1362
attribute.func_globals,
1363
attribute.func_name,
1364
attribute.func_defaults,
1365
attribute.func_closure)))
1367
# Deprecate all alternate interfaces
1368
iname="_AlternateDBusNames_interface_annotation{}"
1369
for interface_name in interface_names:
1371
@dbus_interface_annotations(interface_name)
1373
return { "org.freedesktop.DBus.Deprecated":
1375
# Find an unused name
1376
for aname in (iname.format(i)
1377
for i in itertools.count()):
1378
if aname not in attr:
1382
# Replace the class with a new subclass of it with
1383
# methods, signals, etc. as created above.
1384
cls = type(b"{}Alternate".format(cls.__name__),
1391
@alternate_dbus_interfaces({"se.recompile.Mandos":
1392
"se.bsnet.fukt.Mandos"})
1020
attr[attrname] = (dbus_service_property
1022
attribute._dbus_signature,
1023
attribute._dbus_access,
1025
._dbus_get_args_options
1028
(attribute.func_code,
1029
attribute.func_globals,
1030
attribute.func_name,
1031
attribute.func_defaults,
1032
attribute.func_closure)))
1033
return type.__new__(mcs, name, bases, attr)
1393
1036
class ClientDBus(Client, DBusObjectWithProperties):
1394
1037
"""A Client class using D-Bus
1466
1095
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1467
1096
last_enabled = notifychangeproperty(datetime_to_dbus,
1469
checker = notifychangeproperty(
1470
dbus.Boolean, "CheckerRunning",
1471
type_func = lambda checker: checker is not None)
1098
checker = notifychangeproperty(dbus.Boolean, "CheckerRunning",
1099
type_func = lambda checker:
1100
checker is not None)
1472
1101
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1473
1102
"LastCheckedOK")
1474
last_checker_status = notifychangeproperty(dbus.Int16,
1475
"LastCheckerStatus")
1476
1103
last_approval_request = notifychangeproperty(
1477
1104
datetime_to_dbus, "LastApprovalRequest")
1478
1105
approved_by_default = notifychangeproperty(dbus.Boolean,
1479
1106
"ApprovedByDefault")
1480
approval_delay = notifychangeproperty(
1481
dbus.UInt64, "ApprovalDelay",
1482
type_func = lambda td: td.total_seconds() * 1000)
1107
approval_delay = notifychangeproperty(dbus.UInt64,
1110
timedelta_to_milliseconds)
1483
1111
approval_duration = notifychangeproperty(
1484
1112
dbus.UInt64, "ApprovalDuration",
1485
type_func = lambda td: td.total_seconds() * 1000)
1113
type_func = timedelta_to_milliseconds)
1486
1114
host = notifychangeproperty(dbus.String, "Host")
1487
timeout = notifychangeproperty(
1488
dbus.UInt64, "Timeout",
1489
type_func = lambda td: td.total_seconds() * 1000)
1115
timeout = notifychangeproperty(dbus.UInt64, "Timeout",
1117
timedelta_to_milliseconds)
1490
1118
extended_timeout = notifychangeproperty(
1491
1119
dbus.UInt64, "ExtendedTimeout",
1492
type_func = lambda td: td.total_seconds() * 1000)
1493
interval = notifychangeproperty(
1494
dbus.UInt64, "Interval",
1495
type_func = lambda td: td.total_seconds() * 1000)
1120
type_func = timedelta_to_milliseconds)
1121
interval = notifychangeproperty(dbus.UInt64,
1124
timedelta_to_milliseconds)
1496
1125
checker_command = notifychangeproperty(dbus.String, "Checker")
1497
secret = notifychangeproperty(dbus.ByteArray, "Secret",
1498
invalidate_only=True)
1500
1127
del notifychangeproperty
1744
1350
return datetime_to_dbus(self.last_approval_request)
1746
1352
# Timeout - property
1747
@dbus_service_property(_interface,
1353
@dbus_service_property(_interface, signature="t",
1749
1354
access="readwrite")
1750
1355
def Timeout_dbus_property(self, value=None):
1751
1356
if value is None: # get
1752
return dbus.UInt64(self.timeout.total_seconds() * 1000)
1753
old_timeout = self.timeout
1357
return dbus.UInt64(self.timeout_milliseconds())
1754
1358
self.timeout = datetime.timedelta(0, 0, 0, value)
1755
# Reschedule disabling
1359
# Reschedule timeout
1756
1360
if self.enabled:
1757
1361
now = datetime.datetime.utcnow()
1758
self.expires += self.timeout - old_timeout
1759
if self.expires <= now:
1362
time_to_die = timedelta_to_milliseconds(
1363
(self.last_checked_ok + self.timeout) - now)
1364
if time_to_die <= 0:
1760
1365
# The timeout has passed
1368
self.expires = (now +
1369
datetime.timedelta(milliseconds =
1763
1371
if (getattr(self, "disable_initiator_tag", None)
1766
1374
gobject.source_remove(self.disable_initiator_tag)
1767
self.disable_initiator_tag = gobject.timeout_add(
1768
int((self.expires - now).total_seconds() * 1000),
1375
self.disable_initiator_tag = (gobject.timeout_add
1771
1379
# ExtendedTimeout - property
1772
@dbus_service_property(_interface,
1380
@dbus_service_property(_interface, signature="t",
1774
1381
access="readwrite")
1775
1382
def ExtendedTimeout_dbus_property(self, value=None):
1776
1383
if value is None: # get
1777
return dbus.UInt64(self.extended_timeout.total_seconds()
1384
return dbus.UInt64(self.extended_timeout_milliseconds())
1779
1385
self.extended_timeout = datetime.timedelta(0, 0, 0, value)
1781
1387
# Interval - property
1782
@dbus_service_property(_interface,
1388
@dbus_service_property(_interface, signature="t",
1784
1389
access="readwrite")
1785
1390
def Interval_dbus_property(self, value=None):
1786
1391
if value is None: # get
1787
return dbus.UInt64(self.interval.total_seconds() * 1000)
1392
return dbus.UInt64(self.interval_milliseconds())
1788
1393
self.interval = datetime.timedelta(0, 0, 0, value)
1789
1394
if getattr(self, "checker_initiator_tag", None) is None:
1791
1396
if self.enabled:
1792
1397
# Reschedule checker run
1793
1398
gobject.source_remove(self.checker_initiator_tag)
1794
self.checker_initiator_tag = gobject.timeout_add(
1795
value, self.start_checker)
1796
self.start_checker() # Start one now, too
1399
self.checker_initiator_tag = (gobject.timeout_add
1400
(value, self.start_checker))
1401
self.start_checker() # Start one now, too
1798
1403
# Checker - property
1799
@dbus_service_property(_interface,
1404
@dbus_service_property(_interface, signature="s",
1801
1405
access="readwrite")
1802
1406
def Checker_dbus_property(self, value=None):
1803
1407
if value is None: # get
1804
1408
return dbus.String(self.checker_command)
1805
self.checker_command = str(value)
1409
self.checker_command = unicode(value)
1807
1411
# CheckerRunning - property
1808
@dbus_service_property(_interface,
1412
@dbus_service_property(_interface, signature="b",
1810
1413
access="readwrite")
1811
1414
def CheckerRunning_dbus_property(self, value=None):
1812
1415
if value is None: # get
2043
1643
def fingerprint(openpgp):
2044
1644
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
2045
1645
# New GnuTLS "datum" with the OpenPGP public key
2046
datum = gnutls.library.types.gnutls_datum_t(
2047
ctypes.cast(ctypes.c_char_p(openpgp),
2048
ctypes.POINTER(ctypes.c_ubyte)),
2049
ctypes.c_uint(len(openpgp)))
1646
datum = (gnutls.library.types
1647
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1650
ctypes.c_uint(len(openpgp))))
2050
1651
# New empty GnuTLS certificate
2051
1652
crt = gnutls.library.types.gnutls_openpgp_crt_t()
2052
gnutls.library.functions.gnutls_openpgp_crt_init(
1653
(gnutls.library.functions
1654
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
2054
1655
# Import the OpenPGP public key into the certificate
2055
gnutls.library.functions.gnutls_openpgp_crt_import(
2056
crt, ctypes.byref(datum),
2057
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
1656
(gnutls.library.functions
1657
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1658
gnutls.library.constants
1659
.GNUTLS_OPENPGP_FMT_RAW))
2058
1660
# Verify the self signature in the key
2059
1661
crtverify = ctypes.c_uint()
2060
gnutls.library.functions.gnutls_openpgp_crt_verify_self(
2061
crt, 0, ctypes.byref(crtverify))
1662
(gnutls.library.functions
1663
.gnutls_openpgp_crt_verify_self(crt, 0,
1664
ctypes.byref(crtverify)))
2062
1665
if crtverify.value != 0:
2063
1666
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
2064
raise gnutls.errors.CertificateSecurityError(
1667
raise (gnutls.errors.CertificateSecurityError
2066
1669
# New buffer for the fingerprint
2067
1670
buf = ctypes.create_string_buffer(20)
2068
1671
buf_len = ctypes.c_size_t()
2069
1672
# Get the fingerprint from the certificate into the buffer
2070
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint(
2071
crt, ctypes.byref(buf), ctypes.byref(buf_len))
1673
(gnutls.library.functions
1674
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1675
ctypes.byref(buf_len)))
2072
1676
# Deinit the certificate
2073
1677
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
2074
1678
# Convert the buffer to a Python bytestring
2330
def rfc3339_duration_to_delta(duration):
2331
"""Parse an RFC 3339 "duration" and return a datetime.timedelta
2333
>>> rfc3339_duration_to_delta("P7D")
2334
datetime.timedelta(7)
2335
>>> rfc3339_duration_to_delta("PT60S")
2336
datetime.timedelta(0, 60)
2337
>>> rfc3339_duration_to_delta("PT60M")
2338
datetime.timedelta(0, 3600)
2339
>>> rfc3339_duration_to_delta("PT24H")
2340
datetime.timedelta(1)
2341
>>> rfc3339_duration_to_delta("P1W")
2342
datetime.timedelta(7)
2343
>>> rfc3339_duration_to_delta("PT5M30S")
2344
datetime.timedelta(0, 330)
2345
>>> rfc3339_duration_to_delta("P1DT3M20S")
2346
datetime.timedelta(1, 200)
2349
# Parsing an RFC 3339 duration with regular expressions is not
2350
# possible - there would have to be multiple places for the same
2351
# values, like seconds. The current code, while more esoteric, is
2352
# cleaner without depending on a parsing library. If Python had a
2353
# built-in library for parsing we would use it, but we'd like to
2354
# avoid excessive use of external libraries.
2356
# New type for defining tokens, syntax, and semantics all-in-one
2357
Token = collections.namedtuple("Token", (
2358
"regexp", # To match token; if "value" is not None, must have
2359
# a "group" containing digits
2360
"value", # datetime.timedelta or None
2361
"followers")) # Tokens valid after this token
2362
# RFC 3339 "duration" tokens, syntax, and semantics; taken from
2363
# the "duration" ABNF definition in RFC 3339, Appendix A.
2364
token_end = Token(re.compile(r"$"), None, frozenset())
2365
token_second = Token(re.compile(r"(\d+)S"),
2366
datetime.timedelta(seconds=1),
2367
frozenset((token_end, )))
2368
token_minute = Token(re.compile(r"(\d+)M"),
2369
datetime.timedelta(minutes=1),
2370
frozenset((token_second, token_end)))
2371
token_hour = Token(re.compile(r"(\d+)H"),
2372
datetime.timedelta(hours=1),
2373
frozenset((token_minute, token_end)))
2374
token_time = Token(re.compile(r"T"),
2376
frozenset((token_hour, token_minute,
2378
token_day = Token(re.compile(r"(\d+)D"),
2379
datetime.timedelta(days=1),
2380
frozenset((token_time, token_end)))
2381
token_month = Token(re.compile(r"(\d+)M"),
2382
datetime.timedelta(weeks=4),
2383
frozenset((token_day, token_end)))
2384
token_year = Token(re.compile(r"(\d+)Y"),
2385
datetime.timedelta(weeks=52),
2386
frozenset((token_month, token_end)))
2387
token_week = Token(re.compile(r"(\d+)W"),
2388
datetime.timedelta(weeks=1),
2389
frozenset((token_end, )))
2390
token_duration = Token(re.compile(r"P"), None,
2391
frozenset((token_year, token_month,
2392
token_day, token_time,
2394
# Define starting values
2395
value = datetime.timedelta() # Value so far
2397
followers = frozenset((token_duration, )) # Following valid tokens
2398
s = duration # String left to parse
2399
# Loop until end token is found
2400
while found_token is not token_end:
2401
# Search for any currently valid tokens
2402
for token in followers:
2403
match = token.regexp.match(s)
2404
if match is not None:
2406
if token.value is not None:
2407
# Value found, parse digits
2408
factor = int(match.group(1), 10)
2409
# Add to value so far
2410
value += factor * token.value
2411
# Strip token from string
2412
s = token.regexp.sub("", s, 1)
2415
# Set valid next tokens
2416
followers = found_token.followers
2419
# No currently valid tokens were found
2420
raise ValueError("Invalid RFC 3339 duration: {!r}"
2426
1905
def string_to_delta(interval):
2427
1906
"""Parse a string and return a datetime.timedelta
2557
2021
"debug": "False",
2559
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2560
":+SIGN-DSA-SHA256",
2023
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2561
2024
"servicename": "Mandos",
2562
2025
"use_dbus": "True",
2563
2026
"use_ipv6": "True",
2564
2027
"debuglevel": "",
2565
2028
"restore": "True",
2567
"statedir": "/var/lib/mandos",
2568
"foreground": "False",
2029
"statedir": "/var/lib/mandos"
2572
2032
# Parse config file for server-global settings
2573
2033
server_config = configparser.SafeConfigParser(server_defaults)
2574
2034
del server_defaults
2575
server_config.read(os.path.join(options.configdir, "mandos.conf"))
2035
server_config.read(os.path.join(options.configdir,
2576
2037
# Convert the SafeConfigParser object to a dict
2577
2038
server_settings = server_config.defaults()
2578
2039
# Use the appropriate methods on the non-string config options
2579
for option in ("debug", "use_dbus", "use_ipv6", "foreground"):
2040
for option in ("debug", "use_dbus", "use_ipv6"):
2580
2041
server_settings[option] = server_config.getboolean("DEFAULT",
2582
2043
if server_settings["port"]:
2583
2044
server_settings["port"] = server_config.getint("DEFAULT",
2585
if server_settings["socket"]:
2586
server_settings["socket"] = server_config.getint("DEFAULT",
2588
# Later, stdin will, and stdout and stderr might, be dup'ed
2589
# over with an opened os.devnull. But we don't want this to
2590
# happen with a supplied network socket.
2591
if 0 <= server_settings["socket"] <= 2:
2592
server_settings["socket"] = os.dup(server_settings
2594
2046
del server_config
2596
2048
# Override the settings from the config file with command line
2597
2049
# options, if set.
2598
2050
for option in ("interface", "address", "port", "debug",
2599
"priority", "servicename", "configdir", "use_dbus",
2600
"use_ipv6", "debuglevel", "restore", "statedir",
2601
"socket", "foreground", "zeroconf"):
2051
"priority", "servicename", "configdir",
2052
"use_dbus", "use_ipv6", "debuglevel", "restore",
2602
2054
value = getattr(options, option)
2603
2055
if value is not None:
2604
2056
server_settings[option] = value
2606
2058
# Force all strings to be unicode
2607
2059
for option in server_settings.keys():
2608
if isinstance(server_settings[option], bytes):
2609
server_settings[option] = (server_settings[option]
2611
# Force all boolean options to be boolean
2612
for option in ("debug", "use_dbus", "use_ipv6", "restore",
2613
"foreground", "zeroconf"):
2614
server_settings[option] = bool(server_settings[option])
2615
# Debug implies foreground
2616
if server_settings["debug"]:
2617
server_settings["foreground"] = True
2060
if type(server_settings[option]) is str:
2061
server_settings[option] = unicode(server_settings[option])
2618
2062
# Now we have our good server settings in "server_settings"
2620
2064
##################################################################
2622
if (not server_settings["zeroconf"]
2623
and not (server_settings["port"]
2624
or server_settings["socket"] != "")):
2625
parser.error("Needs port or socket to work without Zeroconf")
2627
2066
# For convenience
2628
2067
debug = server_settings["debug"]
2629
2068
debuglevel = server_settings["debuglevel"]
2708
2143
def debug_gnutls(level, string):
2709
2144
logger.debug("GnuTLS: %s", string[:-1])
2711
gnutls.library.functions.gnutls_global_set_log_function(
2146
(gnutls.library.functions
2147
.gnutls_global_set_log_function(debug_gnutls))
2714
2149
# Redirect stdin so all checkers get /dev/null
2715
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2150
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
2716
2151
os.dup2(null, sys.stdin.fileno())
2720
2155
# Need to fork before connecting to D-Bus
2722
2157
# Close all input and output, do double fork, etc.
2725
# multiprocessing will use threads, so before we use gobject we
2726
# need to inform gobject that threads will be used.
2727
2160
gobject.threads_init()
2729
2162
global main_loop
2730
2163
# From the Avahi example code
2731
DBusGMainLoop(set_as_default=True)
2164
DBusGMainLoop(set_as_default=True )
2732
2165
main_loop = gobject.MainLoop()
2733
2166
bus = dbus.SystemBus()
2734
2167
# End of Avahi example code
2737
2170
bus_name = dbus.service.BusName("se.recompile.Mandos",
2740
old_bus_name = dbus.service.BusName(
2741
"se.bsnet.fukt.Mandos", bus,
2743
except dbus.exceptions.DBusException as e:
2744
logger.error("Disabling D-Bus:", exc_info=e)
2171
bus, do_not_queue=True)
2172
old_bus_name = (dbus.service.BusName
2173
("se.bsnet.fukt.Mandos", bus,
2175
except dbus.exceptions.NameExistsException as e:
2176
logger.error(unicode(e) + ", disabling D-Bus")
2745
2177
use_dbus = False
2746
2178
server_settings["use_dbus"] = False
2747
2179
tcp_server.use_dbus = False
2749
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2750
service = AvahiServiceToSyslog(
2751
name = server_settings["servicename"],
2752
servicetype = "_mandos._tcp",
2753
protocol = protocol,
2755
if server_settings["interface"]:
2756
service.interface = if_nametoindex(
2757
server_settings["interface"].encode("utf-8"))
2180
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2181
service = AvahiServiceToSyslog(name =
2182
server_settings["servicename"],
2183
servicetype = "_mandos._tcp",
2184
protocol = protocol, bus = bus)
2185
if server_settings["interface"]:
2186
service.interface = (if_nametoindex
2187
(str(server_settings["interface"])))
2759
2189
global multiprocessing_manager
2760
2190
multiprocessing_manager = multiprocessing.Manager()
2762
2192
client_class = Client
2764
client_class = functools.partial(ClientDBus, bus = bus)
2194
client_class = functools.partial(ClientDBusTransitional,
2766
2197
client_settings = Client.config_parser(client_config)
2767
2198
old_client_settings = {}
2768
2199
clients_data = {}
2770
# This is used to redirect stdout and stderr for checker processes
2772
wnull = open(os.devnull, "w") # A writable /dev/null
2773
# Only used if server is running in foreground but not in debug
2775
if debug or not foreground:
2778
2201
# Get client data and settings from last running state.
2779
2202
if server_settings["restore"]:
2781
2204
with open(stored_state_path, "rb") as stored_state:
2782
clients_data, old_client_settings = pickle.load(
2205
clients_data, old_client_settings = (pickle.load
2784
2207
os.remove(stored_state_path)
2785
2208
except IOError as e:
2786
if e.errno == errno.ENOENT:
2787
logger.warning("Could not load persistent state:"
2788
" {}".format(os.strerror(e.errno)))
2790
logger.critical("Could not load persistent state:",
2209
logger.warning("Could not load persistent state: {0}"
2211
if e.errno != errno.ENOENT:
2793
2213
except EOFError as e:
2794
2214
logger.warning("Could not load persistent state: "
2215
"EOFError: {0}".format(e))
2798
2217
with PGPEngine() as pgp:
2799
for client_name, client in clients_data.items():
2800
# Skip removed clients
2801
if client_name not in client_settings:
2218
for client_name, client in clients_data.iteritems():
2804
2219
# Decide which value to use after restoring saved state.
2805
2220
# We have three different values: Old config file,
2806
2221
# new config file, and saved state.