265
104
max_renames: integer; maximum number of renames
266
105
rename_count: integer; counter so we only rename after collisions
267
106
a sensible number of times
268
group: D-Bus Entry Group
270
bus: dbus.SystemBus()
274
interface = avahi.IF_UNSPEC,
282
protocol = avahi.PROTO_UNSPEC,
108
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
109
type = None, port = None, TXT = None, domain = "",
110
host = "", max_renames = 32768):
284
111
self.interface = interface
286
self.type = servicetype
288
self.TXT = TXT if TXT is not None else []
289
119
self.domain = domain
291
121
self.rename_count = 0
292
122
self.max_renames = max_renames
293
self.protocol = protocol
294
self.group = None # our entry group
297
self.entry_group_state_changed_match = None
299
def rename(self, remove=True):
300
124
"""Derived from the Avahi example code"""
301
125
if self.rename_count >= self.max_renames:
302
logger.critical("No suitable Zeroconf service name found"
303
" after %i retries, exiting.",
126
logger.critical(u"No suitable service name found after %i"
127
u" retries, exiting.", rename_count)
305
128
raise AvahiServiceError("Too many renames")
307
self.server.GetAlternativeServiceName(self.name))
129
self.name = server.GetAlternativeServiceName(self.name)
130
logger.info(u"Changing name to %r ...", str(self.name))
131
syslogger.setFormatter(logging.Formatter\
132
('Mandos (%s): %%(levelname)s:'
133
' %%(message)s' % self.name))
308
136
self.rename_count += 1
309
logger.info("Changing Zeroconf service name to %r ...",
315
except dbus.exceptions.DBusException as error:
316
if (error.get_dbus_name()
317
== "org.freedesktop.Avahi.CollisionError"):
318
logger.info("Local Zeroconf service name collision.")
319
return self.rename(remove=False)
321
logger.critical("D-Bus Exception", exc_info=error)
325
137
def remove(self):
326
138
"""Derived from the Avahi example code"""
327
if self.entry_group_state_changed_match is not None:
328
self.entry_group_state_changed_match.remove()
329
self.entry_group_state_changed_match = None
330
if self.group is not None:
139
if group is not None:
334
142
"""Derived from the Avahi example code"""
336
if self.group is None:
337
self.group = dbus.Interface(
338
self.bus.get_object(avahi.DBUS_NAME,
339
self.server.EntryGroupNew()),
340
avahi.DBUS_INTERFACE_ENTRY_GROUP)
341
self.entry_group_state_changed_match = (
342
self.group.connect_to_signal(
343
'StateChanged', self.entry_group_state_changed))
344
logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
345
self.name, self.type)
346
self.group.AddService(
349
dbus.UInt32(0), # flags
350
self.name, self.type,
351
self.domain, self.host,
352
dbus.UInt16(self.port),
353
avahi.string_array_to_txt_array(self.TXT))
356
def entry_group_state_changed(self, state, error):
357
"""Derived from the Avahi example code"""
358
logger.debug("Avahi entry group state change: %i", state)
360
if state == avahi.ENTRY_GROUP_ESTABLISHED:
361
logger.debug("Zeroconf service established.")
362
elif state == avahi.ENTRY_GROUP_COLLISION:
363
logger.info("Zeroconf service name collision.")
365
elif state == avahi.ENTRY_GROUP_FAILURE:
366
logger.critical("Avahi: Error in group state changed %s",
368
raise AvahiGroupError("State changed: {!s}".format(error))
371
"""Derived from the Avahi example code"""
372
if self.group is not None:
375
except (dbus.exceptions.UnknownMethodException,
376
dbus.exceptions.DBusException):
381
def server_state_changed(self, state, error=None):
382
"""Derived from the Avahi example code"""
383
logger.debug("Avahi server state change: %i", state)
385
avahi.SERVER_INVALID: "Zeroconf server invalid",
386
avahi.SERVER_REGISTERING: None,
387
avahi.SERVER_COLLISION: "Zeroconf server name collision",
388
avahi.SERVER_FAILURE: "Zeroconf server failure",
390
if state in bad_states:
391
if bad_states[state] is not None:
393
logger.error(bad_states[state])
395
logger.error(bad_states[state] + ": %r", error)
397
elif state == avahi.SERVER_RUNNING:
401
logger.debug("Unknown state: %r", state)
403
logger.debug("Unknown state: %r: %r", state, error)
406
"""Derived from the Avahi example code"""
407
if self.server is None:
408
self.server = dbus.Interface(
409
self.bus.get_object(avahi.DBUS_NAME,
410
avahi.DBUS_PATH_SERVER,
411
follow_name_owner_changes=True),
412
avahi.DBUS_INTERFACE_SERVER)
413
self.server.connect_to_signal("StateChanged",
414
self.server_state_changed)
415
self.server_state_changed(self.server.GetState())
418
class AvahiServiceToSyslog(AvahiService):
419
def rename(self, *args, **kwargs):
420
"""Add the new name to the syslog messages"""
421
ret = AvahiService.rename(self, *args, **kwargs)
422
syslogger.setFormatter(logging.Formatter(
423
'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
145
group = dbus.Interface\
146
(bus.get_object(avahi.DBUS_NAME,
147
server.EntryGroupNew()),
148
avahi.DBUS_INTERFACE_ENTRY_GROUP)
149
group.connect_to_signal('StateChanged',
150
entry_group_state_changed)
151
logger.debug(u"Adding service '%s' of type '%s' ...",
152
service.name, service.type)
154
self.interface, # interface
155
avahi.PROTO_INET6, # protocol
156
dbus.UInt32(0), # flags
157
self.name, self.type,
158
self.domain, self.host,
159
dbus.UInt16(self.port),
160
avahi.string_array_to_txt_array(self.TXT))
163
# From the Avahi example code:
164
group = None # our entry group
165
# End of Avahi example code
428
168
class Client(object):
429
169
"""A representation of a client host served by this server.
432
approved: bool(); 'None' if not yet approved/disapproved
433
approval_delay: datetime.timedelta(); Time to wait for approval
434
approval_duration: datetime.timedelta(); Duration of one approval
435
checker: subprocess.Popen(); a running checker process used
436
to see if the client lives.
437
'None' if no process is running.
438
checker_callback_tag: a gobject event source tag, or None
439
checker_command: string; External command which is run to check
440
if client lives. %() expansions are done at
171
name: string; from the config file, used in log messages
172
fingerprint: string (40 or 32 hexadecimal digits); used to
173
uniquely identify the client
174
secret: bytestring; sent verbatim (over TLS) to client
175
host: string; available for use by the checker command
176
created: datetime.datetime(); object creation, not client host
177
last_checked_ok: datetime.datetime() or None if not yet checked OK
178
timeout: datetime.timedelta(); How long from last_checked_ok
179
until this client is invalid
180
interval: datetime.timedelta(); How often to start a new checker
181
stop_hook: If set, called by stop() as stop_hook(self)
182
checker: subprocess.Popen(); a running checker process used
183
to see if the client lives.
184
'None' if no process is running.
185
checker_initiator_tag: a gobject event source tag, or None
186
stop_initiator_tag: - '' -
187
checker_callback_tag: - '' -
188
checker_command: string; External command which is run to check if
189
client lives. %() expansions are done at
441
190
runtime with vars(self) as dict, so that for
442
191
instance %(name)s can be used in the command.
443
checker_initiator_tag: a gobject event source tag, or None
444
created: datetime.datetime(); (UTC) object creation
445
client_structure: Object describing what attributes a client has
446
and is used for storing the client at exit
447
current_checker_command: string; current running checker_command
448
disable_initiator_tag: a gobject event source tag, or None
450
fingerprint: string (40 or 32 hexadecimal digits); used to
451
uniquely identify the client
452
host: string; available for use by the checker command
453
interval: datetime.timedelta(); How often to start a new checker
454
last_approval_request: datetime.datetime(); (UTC) or None
455
last_checked_ok: datetime.datetime(); (UTC) or None
456
last_checker_status: integer between 0 and 255 reflecting exit
457
status of last checker. -1 reflects crashed
458
checker, -2 means no checker completed yet.
459
last_enabled: datetime.datetime(); (UTC) or None
460
name: string; from the config file, used in log messages and
462
secret: bytestring; sent verbatim (over TLS) to client
463
timeout: datetime.timedelta(); How long from last_checked_ok
464
until this client is disabled
465
extended_timeout: extra long timeout when secret has been sent
466
runtime_expansions: Allowed attributes for runtime expansion.
467
expires: datetime.datetime(); time (UTC) when a client will be
469
server_settings: The server_settings dict from main()
193
_timeout: Real variable for 'timeout'
194
_interval: Real variable for 'interval'
195
_timeout_milliseconds: Used when calling gobject.timeout_add()
196
_interval_milliseconds: - '' -
472
runtime_expansions = ("approval_delay", "approval_duration",
473
"created", "enabled", "expires",
474
"fingerprint", "host", "interval",
475
"last_approval_request", "last_checked_ok",
476
"last_enabled", "name", "timeout")
479
"extended_timeout": "PT15M",
481
"checker": "fping -q -- %%(host)s",
483
"approval_delay": "PT0S",
484
"approval_duration": "PT1S",
485
"approved_by_default": "True",
490
def config_parser(config):
491
"""Construct a new dict of client settings of this form:
492
{ client_name: {setting_name: value, ...}, ...}
493
with exceptions for any special settings as defined above.
494
NOTE: Must be a pure function. Must return the same result
495
value given the same arguments.
498
for client_name in config.sections():
499
section = dict(config.items(client_name))
500
client = settings[client_name] = {}
502
client["host"] = section["host"]
503
# Reformat values from string types to Python types
504
client["approved_by_default"] = config.getboolean(
505
client_name, "approved_by_default")
506
client["enabled"] = config.getboolean(client_name,
509
# Uppercase and remove spaces from fingerprint for later
510
# comparison purposes with return value from the
511
# fingerprint() function
512
client["fingerprint"] = (section["fingerprint"].upper()
514
if "secret" in section:
515
client["secret"] = section["secret"].decode("base64")
516
elif "secfile" in section:
517
with open(os.path.expanduser(os.path.expandvars
518
(section["secfile"])),
520
client["secret"] = secfile.read()
522
raise TypeError("No secret or secfile for section {}"
524
client["timeout"] = string_to_delta(section["timeout"])
525
client["extended_timeout"] = string_to_delta(
526
section["extended_timeout"])
527
client["interval"] = string_to_delta(section["interval"])
528
client["approval_delay"] = string_to_delta(
529
section["approval_delay"])
530
client["approval_duration"] = string_to_delta(
531
section["approval_duration"])
532
client["checker_command"] = section["checker"]
533
client["last_approval_request"] = None
534
client["last_checked_ok"] = None
535
client["last_checker_status"] = -2
539
def __init__(self, settings, name = None, server_settings=None):
198
def _set_timeout(self, timeout):
199
"Setter function for 'timeout' attribute"
200
self._timeout = timeout
201
self._timeout_milliseconds = ((self.timeout.days
202
* 24 * 60 * 60 * 1000)
203
+ (self.timeout.seconds * 1000)
204
+ (self.timeout.microseconds
206
timeout = property(lambda self: self._timeout,
209
def _set_interval(self, interval):
210
"Setter function for 'interval' attribute"
211
self._interval = interval
212
self._interval_milliseconds = ((self.interval.days
213
* 24 * 60 * 60 * 1000)
214
+ (self.interval.seconds
216
+ (self.interval.microseconds
218
interval = property(lambda self: self._interval,
221
def __init__(self, name = None, stop_hook=None, config={}):
222
"""Note: the 'checker' key in 'config' sets the
223
'checker_command' attribute and *not* the 'checker'
541
if server_settings is None:
543
self.server_settings = server_settings
544
# adding all client settings
545
for setting, value in settings.items():
546
setattr(self, setting, value)
549
if not hasattr(self, "last_enabled"):
550
self.last_enabled = datetime.datetime.utcnow()
551
if not hasattr(self, "expires"):
552
self.expires = (datetime.datetime.utcnow()
226
logger.debug(u"Creating client %r", self.name)
227
# Uppercase and remove spaces from fingerprint for later
228
# comparison purposes with return value from the fingerprint()
230
self.fingerprint = config["fingerprint"].upper()\
232
logger.debug(u" Fingerprint: %s", self.fingerprint)
233
if "secret" in config:
234
self.secret = config["secret"].decode(u"base64")
235
elif "secfile" in config:
236
sf = open(config["secfile"])
237
self.secret = sf.read()
555
self.last_enabled = None
558
logger.debug("Creating client %r", self.name)
559
logger.debug(" Fingerprint: %s", self.fingerprint)
560
self.created = settings.get("created",
561
datetime.datetime.utcnow())
563
# attributes specific for this server instance
240
raise TypeError(u"No secret or secfile for client %s"
242
self.host = config.get("host", "")
243
self.created = datetime.datetime.now()
244
self.last_checked_ok = None
245
self.timeout = string_to_delta(config["timeout"])
246
self.interval = string_to_delta(config["interval"])
247
self.stop_hook = stop_hook
564
248
self.checker = None
565
249
self.checker_initiator_tag = None
566
self.disable_initiator_tag = None
250
self.stop_initiator_tag = None
567
251
self.checker_callback_tag = None
568
self.current_checker_command = None
570
self.approvals_pending = 0
571
self.changedstate = multiprocessing_manager.Condition(
572
multiprocessing_manager.Lock())
573
self.client_structure = [attr
574
for attr in self.__dict__.iterkeys()
575
if not attr.startswith("_")]
576
self.client_structure.append("client_structure")
578
for name, t in inspect.getmembers(
579
type(self), lambda obj: isinstance(obj, property)):
580
if not name.startswith("_"):
581
self.client_structure.append(name)
583
# Send notice to process children that client state has changed
584
def send_changedstate(self):
585
with self.changedstate:
586
self.changedstate.notify_all()
252
self.check_command = config["checker"]
589
254
"""Start this client's checker and timeout hooks"""
590
if getattr(self, "enabled", False):
593
self.expires = datetime.datetime.utcnow() + self.timeout
595
self.last_enabled = datetime.datetime.utcnow()
597
self.send_changedstate()
599
def disable(self, quiet=True):
600
"""Disable this client."""
601
if not getattr(self, "enabled", False):
604
logger.info("Disabling client %s", self.name)
605
if getattr(self, "disable_initiator_tag", None) is not None:
606
gobject.source_remove(self.disable_initiator_tag)
607
self.disable_initiator_tag = None
609
if getattr(self, "checker_initiator_tag", None) is not None:
610
gobject.source_remove(self.checker_initiator_tag)
611
self.checker_initiator_tag = None
615
self.send_changedstate()
616
# Do not run this again if called by a gobject.timeout_add
622
def init_checker(self):
623
255
# Schedule a new checker to be started an 'interval' from now,
624
256
# and every interval from then on.
625
if self.checker_initiator_tag is not None:
626
gobject.source_remove(self.checker_initiator_tag)
627
self.checker_initiator_tag = gobject.timeout_add(
628
int(self.interval.total_seconds() * 1000),
630
# Schedule a disable() when 'timeout' has passed
631
if self.disable_initiator_tag is not None:
632
gobject.source_remove(self.disable_initiator_tag)
633
self.disable_initiator_tag = gobject.timeout_add(
634
int(self.timeout.total_seconds() * 1000), self.disable)
257
self.checker_initiator_tag = gobject.timeout_add\
258
(self._interval_milliseconds,
635
260
# Also start a new checker *right now*.
636
261
self.start_checker()
638
def checker_callback(self, pid, condition, command):
262
# Schedule a stop() when 'timeout' has passed
263
self.stop_initiator_tag = gobject.timeout_add\
264
(self._timeout_milliseconds,
268
The possibility that a client might be restarted is left open,
269
but not currently used."""
270
# If this client doesn't have a secret, it is already stopped.
271
if hasattr(self, "secret") and self.secret:
272
logger.info(u"Stopping client %s", self.name)
276
if getattr(self, "stop_initiator_tag", False):
277
gobject.source_remove(self.stop_initiator_tag)
278
self.stop_initiator_tag = None
279
if getattr(self, "checker_initiator_tag", False):
280
gobject.source_remove(self.checker_initiator_tag)
281
self.checker_initiator_tag = None
285
# Do not run this again if called by a gobject.timeout_add
288
self.stop_hook = None
290
def checker_callback(self, pid, condition):
639
291
"""The checker has completed, so take appropriate actions."""
292
now = datetime.datetime.now()
640
293
self.checker_callback_tag = None
641
294
self.checker = None
642
if os.WIFEXITED(condition):
643
self.last_checker_status = os.WEXITSTATUS(condition)
644
if self.last_checker_status == 0:
645
logger.info("Checker for %(name)s succeeded",
649
logger.info("Checker for %(name)s failed", vars(self))
651
self.last_checker_status = -1
652
logger.warning("Checker for %(name)s crashed?",
295
if os.WIFEXITED(condition) \
296
and (os.WEXITSTATUS(condition) == 0):
297
logger.info(u"Checker for %(name)s succeeded",
299
self.last_checked_ok = now
300
gobject.source_remove(self.stop_initiator_tag)
301
self.stop_initiator_tag = gobject.timeout_add\
302
(self._timeout_milliseconds,
304
elif not os.WIFEXITED(condition):
305
logger.warning(u"Checker for %(name)s crashed?",
655
def checked_ok(self):
656
"""Assert that the client has been seen, alive and well."""
657
self.last_checked_ok = datetime.datetime.utcnow()
658
self.last_checker_status = 0
661
def bump_timeout(self, timeout=None):
662
"""Bump up the timeout for this client."""
664
timeout = self.timeout
665
if self.disable_initiator_tag is not None:
666
gobject.source_remove(self.disable_initiator_tag)
667
self.disable_initiator_tag = None
668
if getattr(self, "enabled", False):
669
self.disable_initiator_tag = gobject.timeout_add(
670
int(timeout.total_seconds() * 1000), self.disable)
671
self.expires = datetime.datetime.utcnow() + timeout
673
def need_approval(self):
674
self.last_approval_request = datetime.datetime.utcnow()
308
logger.info(u"Checker for %(name)s failed",
676
310
def start_checker(self):
677
311
"""Start a new checker subprocess if one is not running.
679
312
If a checker already exists, leave it running and do
681
314
# The reason for not killing a running checker is that if we
682
# did that, and if a checker (for some reason) started running
683
# slowly and taking more than 'interval' time, then the client
684
# would inevitably timeout, since no checker would get a
685
# chance to run to completion. If we instead leave running
315
# did that, then if a checker (for some reason) started
316
# running slowly and taking more than 'interval' time, the
317
# client would inevitably timeout, since no checker would get
318
# a chance to run to completion. If we instead leave running
686
319
# checkers alone, the checker would have to take more time
687
# than 'timeout' for the client to be disabled, which is as it
690
# If a checker exists, make sure it is not a zombie
692
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
693
except AttributeError:
695
except OSError as error:
696
if error.errno != errno.ECHILD:
700
logger.warning("Checker was a zombie")
701
gobject.source_remove(self.checker_callback_tag)
702
self.checker_callback(pid, status,
703
self.current_checker_command)
704
# Start a new checker if needed
320
# than 'timeout' for the client to be declared invalid, which
321
# is as it should be.
705
322
if self.checker is None:
706
# Escape attributes for the shell
708
attr: re.escape(str(getattr(self, attr)))
709
for attr in self.runtime_expansions }
711
command = self.checker_command % escaped_attrs
712
except TypeError as error:
713
logger.error('Could not format string "%s"',
714
self.checker_command,
716
return True # Try again later
717
self.current_checker_command = command
719
logger.info("Starting checker %r for %s", command,
721
# We don't need to redirect stdout and stderr, since
722
# in normal mode, that is already done by daemon(),
723
# and in debug mode we don't want to. (Stdin is
724
# always replaced by /dev/null.)
725
# The exception is when not debugging but nevertheless
726
# running in the foreground; use the previously
729
if (not self.server_settings["debug"]
730
and self.server_settings["foreground"]):
731
popen_args.update({"stdout": wnull,
324
# In case check_command has exactly one % operator
325
command = self.check_command % self.host
327
# Escape attributes for the shell
328
escaped_attrs = dict((key, re.escape(str(val)))
330
vars(self).iteritems())
332
command = self.check_command % escaped_attrs
333
except TypeError, error:
334
logger.error(u'Could not format string "%s":'
335
u' %s', self.check_command, error)
336
return True # Try again later
338
logger.info(u"Starting checker %r for %s",
733
340
self.checker = subprocess.Popen(command,
738
except OSError as error:
739
logger.error("Failed to start subprocess",
742
self.checker_callback_tag = gobject.child_watch_add(
743
self.checker.pid, self.checker_callback, data=command)
744
# The checker may have completed before the gobject
745
# watch was added. Check for this.
747
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
748
except OSError as error:
749
if error.errno == errno.ECHILD:
750
# This should never happen
751
logger.error("Child process vanished",
756
gobject.source_remove(self.checker_callback_tag)
757
self.checker_callback(pid, status, command)
343
self.checker_callback_tag = gobject.child_watch_add\
345
self.checker_callback)
346
except subprocess.OSError, error:
347
logger.error(u"Failed to start subprocess: %s",
758
349
# Re-run this periodically if run by gobject.timeout_add
761
351
def stop_checker(self):
762
352
"""Force the checker process, if any, to stop."""
763
353
if self.checker_callback_tag:
765
355
self.checker_callback_tag = None
766
356
if getattr(self, "checker", None) is None:
768
logger.debug("Stopping checker for %(name)s", vars(self))
358
logger.debug(u"Stopping checker for %(name)s", vars(self))
770
self.checker.terminate()
360
os.kill(self.checker.pid, signal.SIGTERM)
772
362
#if self.checker.poll() is None:
773
# self.checker.kill()
774
except OSError as error:
363
# os.kill(self.checker.pid, signal.SIGKILL)
364
except OSError, error:
775
365
if error.errno != errno.ESRCH: # No such process
777
367
self.checker = None
780
def dbus_service_property(dbus_interface,
784
"""Decorators for marking methods of a DBusObjectWithProperties to
785
become properties on the D-Bus.
787
The decorated method will be called with no arguments by "Get"
788
and with one argument by "Set".
790
The parameters, where they are supported, are the same as
791
dbus.service.method, except there is only "signature", since the
792
type from Get() and the type sent to Set() is the same.
794
# Encoding deeply encoded byte arrays is not supported yet by the
795
# "Set" method, so we fail early here:
796
if byte_arrays and signature != "ay":
797
raise ValueError("Byte arrays not supported for non-'ay'"
798
" signature {!r}".format(signature))
801
func._dbus_is_property = True
802
func._dbus_interface = dbus_interface
803
func._dbus_signature = signature
804
func._dbus_access = access
805
func._dbus_name = func.__name__
806
if func._dbus_name.endswith("_dbus_property"):
807
func._dbus_name = func._dbus_name[:-14]
808
func._dbus_get_args_options = {'byte_arrays': byte_arrays }
814
def dbus_interface_annotations(dbus_interface):
815
"""Decorator for marking functions returning interface annotations
819
@dbus_interface_annotations("org.example.Interface")
820
def _foo(self): # Function name does not matter
821
return {"org.freedesktop.DBus.Deprecated": "true",
822
"org.freedesktop.DBus.Property.EmitsChangedSignal":
827
func._dbus_is_interface = True
828
func._dbus_interface = dbus_interface
829
func._dbus_name = dbus_interface
835
def dbus_annotations(annotations):
836
"""Decorator to annotate D-Bus methods, signals or properties
839
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true",
840
"org.freedesktop.DBus.Property."
841
"EmitsChangedSignal": "false"})
842
@dbus_service_property("org.example.Interface", signature="b",
844
def Property_dbus_property(self):
845
return dbus.Boolean(False)
849
func._dbus_annotations = annotations
855
class DBusPropertyException(dbus.exceptions.DBusException):
856
"""A base class for D-Bus property-related exceptions
861
class DBusPropertyAccessException(DBusPropertyException):
862
"""A property's access permissions disallows an operation.
867
class DBusPropertyNotFound(DBusPropertyException):
868
"""An attempt was made to access a non-existing property.
873
class DBusObjectWithProperties(dbus.service.Object):
874
"""A D-Bus object with properties.
876
Classes inheriting from this can use the dbus_service_property
877
decorator to expose methods as D-Bus properties. It exposes the
878
standard Get(), Set(), and GetAll() methods on the D-Bus.
882
def _is_dbus_thing(thing):
883
"""Returns a function testing if an attribute is a D-Bus thing
885
If called like _is_dbus_thing("method") it returns a function
886
suitable for use as predicate to inspect.getmembers().
888
return lambda obj: getattr(obj, "_dbus_is_{}".format(thing),
891
def _get_all_dbus_things(self, thing):
892
"""Returns a generator of (name, attribute) pairs
894
return ((getattr(athing.__get__(self), "_dbus_name", name),
895
athing.__get__(self))
896
for cls in self.__class__.__mro__
898
inspect.getmembers(cls, self._is_dbus_thing(thing)))
900
def _get_dbus_property(self, interface_name, property_name):
901
"""Returns a bound method if one exists which is a D-Bus
902
property with the specified name and interface.
904
for cls in self.__class__.__mro__:
905
for name, value in inspect.getmembers(
906
cls, self._is_dbus_thing("property")):
907
if (value._dbus_name == property_name
908
and value._dbus_interface == interface_name):
909
return value.__get__(self)
912
raise DBusPropertyNotFound("{}:{}.{}".format(
913
self.dbus_object_path, interface_name, property_name))
915
@dbus.service.method(dbus.PROPERTIES_IFACE,
918
def Get(self, interface_name, property_name):
919
"""Standard D-Bus property Get() method, see D-Bus standard.
921
prop = self._get_dbus_property(interface_name, property_name)
922
if prop._dbus_access == "write":
923
raise DBusPropertyAccessException(property_name)
925
if not hasattr(value, "variant_level"):
927
return type(value)(value, variant_level=value.variant_level+1)
929
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
930
def Set(self, interface_name, property_name, value):
931
"""Standard D-Bus property Set() method, see D-Bus standard.
933
prop = self._get_dbus_property(interface_name, property_name)
934
if prop._dbus_access == "read":
935
raise DBusPropertyAccessException(property_name)
936
if prop._dbus_get_args_options["byte_arrays"]:
937
# The byte_arrays option is not supported yet on
938
# signatures other than "ay".
939
if prop._dbus_signature != "ay":
940
raise ValueError("Byte arrays not supported for non-"
941
"'ay' signature {!r}"
942
.format(prop._dbus_signature))
943
value = dbus.ByteArray(b''.join(chr(byte)
947
@dbus.service.method(dbus.PROPERTIES_IFACE,
949
out_signature="a{sv}")
950
def GetAll(self, interface_name):
951
"""Standard D-Bus property GetAll() method, see D-Bus
954
Note: Will not include properties with access="write".
957
for name, prop in self._get_all_dbus_things("property"):
959
and interface_name != prop._dbus_interface):
960
# Interface non-empty but did not match
962
# Ignore write-only properties
963
if prop._dbus_access == "write":
966
if not hasattr(value, "variant_level"):
967
properties[name] = value
969
properties[name] = type(value)(
970
value, variant_level = value.variant_level + 1)
971
return dbus.Dictionary(properties, signature="sv")
973
@dbus.service.signal(dbus.PROPERTIES_IFACE, signature="sa{sv}as")
974
def PropertiesChanged(self, interface_name, changed_properties,
975
invalidated_properties):
976
"""Standard D-Bus PropertiesChanged() signal, see D-Bus
981
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
983
path_keyword='object_path',
984
connection_keyword='connection')
985
def Introspect(self, object_path, connection):
986
"""Overloading of standard D-Bus method.
988
Inserts property tags and interface annotation tags.
990
xmlstring = dbus.service.Object.Introspect(self, object_path,
993
document = xml.dom.minidom.parseString(xmlstring)
995
def make_tag(document, name, prop):
996
e = document.createElement("property")
997
e.setAttribute("name", name)
998
e.setAttribute("type", prop._dbus_signature)
999
e.setAttribute("access", prop._dbus_access)
1002
for if_tag in document.getElementsByTagName("interface"):
1004
for tag in (make_tag(document, name, prop)
1006
in self._get_all_dbus_things("property")
1007
if prop._dbus_interface
1008
== if_tag.getAttribute("name")):
1009
if_tag.appendChild(tag)
1010
# Add annotation tags
1011
for typ in ("method", "signal", "property"):
1012
for tag in if_tag.getElementsByTagName(typ):
1014
for name, prop in (self.
1015
_get_all_dbus_things(typ)):
1016
if (name == tag.getAttribute("name")
1017
and prop._dbus_interface
1018
== if_tag.getAttribute("name")):
1019
annots.update(getattr(
1020
prop, "_dbus_annotations", {}))
1021
for name, value in annots.items():
1022
ann_tag = document.createElement(
1024
ann_tag.setAttribute("name", name)
1025
ann_tag.setAttribute("value", value)
1026
tag.appendChild(ann_tag)
1027
# Add interface annotation tags
1028
for annotation, value in dict(
1029
itertools.chain.from_iterable(
1030
annotations().items()
1031
for name, annotations
1032
in self._get_all_dbus_things("interface")
1033
if name == if_tag.getAttribute("name")
1035
ann_tag = document.createElement("annotation")
1036
ann_tag.setAttribute("name", annotation)
1037
ann_tag.setAttribute("value", value)
1038
if_tag.appendChild(ann_tag)
1039
# Add the names to the return values for the
1040
# "org.freedesktop.DBus.Properties" methods
1041
if (if_tag.getAttribute("name")
1042
== "org.freedesktop.DBus.Properties"):
1043
for cn in if_tag.getElementsByTagName("method"):
1044
if cn.getAttribute("name") == "Get":
1045
for arg in cn.getElementsByTagName("arg"):
1046
if (arg.getAttribute("direction")
1048
arg.setAttribute("name", "value")
1049
elif cn.getAttribute("name") == "GetAll":
1050
for arg in cn.getElementsByTagName("arg"):
1051
if (arg.getAttribute("direction")
1053
arg.setAttribute("name", "props")
1054
xmlstring = document.toxml("utf-8")
1056
except (AttributeError, xml.dom.DOMException,
1057
xml.parsers.expat.ExpatError) as error:
1058
logger.error("Failed to override Introspection method",
1063
def datetime_to_dbus(dt, variant_level=0):
1064
"""Convert a UTC datetime.datetime() to a D-Bus type."""
1066
return dbus.String("", variant_level = variant_level)
1067
return dbus.String(dt.isoformat(), variant_level=variant_level)
1070
def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
1071
"""A class decorator; applied to a subclass of
1072
dbus.service.Object, it will add alternate D-Bus attributes with
1073
interface names according to the "alt_interface_names" mapping.
1076
@alternate_dbus_interfaces({"org.example.Interface":
1077
"net.example.AlternateInterface"})
1078
class SampleDBusObject(dbus.service.Object):
1079
@dbus.service.method("org.example.Interface")
1080
def SampleDBusMethod():
1083
The above "SampleDBusMethod" on "SampleDBusObject" will be
1084
reachable via two interfaces: "org.example.Interface" and
1085
"net.example.AlternateInterface", the latter of which will have
1086
its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
1087
"true", unless "deprecate" is passed with a False value.
1089
This works for methods and signals, and also for D-Bus properties
1090
(from DBusObjectWithProperties) and interfaces (from the
1091
dbus_interface_annotations decorator).
1095
for orig_interface_name, alt_interface_name in (
1096
alt_interface_names.items()):
1098
interface_names = set()
1099
# Go though all attributes of the class
1100
for attrname, attribute in inspect.getmembers(cls):
1101
# Ignore non-D-Bus attributes, and D-Bus attributes
1102
# with the wrong interface name
1103
if (not hasattr(attribute, "_dbus_interface")
1104
or not attribute._dbus_interface.startswith(
1105
orig_interface_name)):
1107
# Create an alternate D-Bus interface name based on
1109
alt_interface = attribute._dbus_interface.replace(
1110
orig_interface_name, alt_interface_name)
1111
interface_names.add(alt_interface)
1112
# Is this a D-Bus signal?
1113
if getattr(attribute, "_dbus_is_signal", False):
1114
if sys.version == 2:
1115
# Extract the original non-method undecorated
1116
# function by black magic
1117
nonmethod_func = (dict(
1118
zip(attribute.func_code.co_freevars,
1119
attribute.__closure__))
1120
["func"].cell_contents)
1122
nonmethod_func = attribute
1123
# Create a new, but exactly alike, function
1124
# object, and decorate it to be a new D-Bus signal
1125
# with the alternate D-Bus interface name
1126
if sys.version == 2:
1127
new_function = types.FunctionType(
1128
nonmethod_func.func_code,
1129
nonmethod_func.func_globals,
1130
nonmethod_func.func_name,
1131
nonmethod_func.func_defaults,
1132
nonmethod_func.func_closure)
1134
new_function = types.FunctionType(
1135
nonmethod_func.__code__,
1136
nonmethod_func.__globals__,
1137
nonmethod_func.__name__,
1138
nonmethod_func.__defaults__,
1139
nonmethod_func.__closure__)
1140
new_function = (dbus.service.signal(
1142
attribute._dbus_signature)(new_function))
1143
# Copy annotations, if any
1145
new_function._dbus_annotations = dict(
1146
attribute._dbus_annotations)
1147
except AttributeError:
1149
# Define a creator of a function to call both the
1150
# original and alternate functions, so both the
1151
# original and alternate signals gets sent when
1152
# the function is called
1153
def fixscope(func1, func2):
1154
"""This function is a scope container to pass
1155
func1 and func2 to the "call_both" function
1156
outside of its arguments"""
1158
def call_both(*args, **kwargs):
1159
"""This function will emit two D-Bus
1160
signals by calling func1 and func2"""
1161
func1(*args, **kwargs)
1162
func2(*args, **kwargs)
1165
# Create the "call_both" function and add it to
1167
attr[attrname] = fixscope(attribute, new_function)
1168
# Is this a D-Bus method?
1169
elif getattr(attribute, "_dbus_is_method", False):
1170
# Create a new, but exactly alike, function
1171
# object. Decorate it to be a new D-Bus method
1172
# with the alternate D-Bus interface name. Add it
1175
dbus.service.method(
1177
attribute._dbus_in_signature,
1178
attribute._dbus_out_signature)
1179
(types.FunctionType(attribute.func_code,
1180
attribute.func_globals,
1181
attribute.func_name,
1182
attribute.func_defaults,
1183
attribute.func_closure)))
1184
# Copy annotations, if any
1186
attr[attrname]._dbus_annotations = dict(
1187
attribute._dbus_annotations)
1188
except AttributeError:
1190
# Is this a D-Bus property?
1191
elif getattr(attribute, "_dbus_is_property", False):
1192
# Create a new, but exactly alike, function
1193
# object, and decorate it to be a new D-Bus
1194
# property with the alternate D-Bus interface
1195
# name. Add it to the class.
1196
attr[attrname] = (dbus_service_property(
1197
alt_interface, attribute._dbus_signature,
1198
attribute._dbus_access,
1199
attribute._dbus_get_args_options
1201
(types.FunctionType(
1202
attribute.func_code,
1203
attribute.func_globals,
1204
attribute.func_name,
1205
attribute.func_defaults,
1206
attribute.func_closure)))
1207
# Copy annotations, if any
1209
attr[attrname]._dbus_annotations = dict(
1210
attribute._dbus_annotations)
1211
except AttributeError:
1213
# Is this a D-Bus interface?
1214
elif getattr(attribute, "_dbus_is_interface", False):
1215
# Create a new, but exactly alike, function
1216
# object. Decorate it to be a new D-Bus interface
1217
# with the alternate D-Bus interface name. Add it
1220
dbus_interface_annotations(alt_interface)
1221
(types.FunctionType(attribute.func_code,
1222
attribute.func_globals,
1223
attribute.func_name,
1224
attribute.func_defaults,
1225
attribute.func_closure)))
1227
# Deprecate all alternate interfaces
1228
iname="_AlternateDBusNames_interface_annotation{}"
1229
for interface_name in interface_names:
1231
@dbus_interface_annotations(interface_name)
1233
return { "org.freedesktop.DBus.Deprecated":
1235
# Find an unused name
1236
for aname in (iname.format(i)
1237
for i in itertools.count()):
1238
if aname not in attr:
1242
# Replace the class with a new subclass of it with
1243
# methods, signals, etc. as created above.
1244
cls = type(b"{}Alternate".format(cls.__name__),
1251
@alternate_dbus_interfaces({"se.recompile.Mandos":
1252
"se.bsnet.fukt.Mandos"})
1253
class ClientDBus(Client, DBusObjectWithProperties):
1254
"""A Client class using D-Bus
1257
dbus_object_path: dbus.ObjectPath
1258
bus: dbus.SystemBus()
1261
runtime_expansions = (Client.runtime_expansions
1262
+ ("dbus_object_path", ))
1264
_interface = "se.recompile.Mandos.Client"
1266
# dbus.service.Object doesn't use super(), so we can't either.
1268
def __init__(self, bus = None, *args, **kwargs):
1270
Client.__init__(self, *args, **kwargs)
1271
# Only now, when this client is initialized, can it show up on
1273
client_object_name = str(self.name).translate(
1274
{ord("."): ord("_"),
1275
ord("-"): ord("_")})
1276
self.dbus_object_path = dbus.ObjectPath(
1277
"/clients/" + client_object_name)
1278
DBusObjectWithProperties.__init__(self, self.bus,
1279
self.dbus_object_path)
1281
def notifychangeproperty(transform_func, dbus_name,
1282
type_func=lambda x: x,
1284
invalidate_only=False,
1285
_interface=_interface):
1286
""" Modify a variable so that it's a property which announces
1287
its changes to DBus.
1289
transform_fun: Function that takes a value and a variant_level
1290
and transforms it to a D-Bus type.
1291
dbus_name: D-Bus name of the variable
1292
type_func: Function that transform the value before sending it
1293
to the D-Bus. Default: no transform
1294
variant_level: D-Bus variant level. Default: 1
1296
attrname = "_{}".format(dbus_name)
1298
def setter(self, value):
1299
if hasattr(self, "dbus_object_path"):
1300
if (not hasattr(self, attrname) or
1301
type_func(getattr(self, attrname, None))
1302
!= type_func(value)):
1304
self.PropertiesChanged(
1305
_interface, dbus.Dictionary(),
1306
dbus.Array((dbus_name, )))
1308
dbus_value = transform_func(
1310
variant_level = variant_level)
1311
self.PropertyChanged(dbus.String(dbus_name),
1313
self.PropertiesChanged(
1315
dbus.Dictionary({ dbus.String(dbus_name):
1318
setattr(self, attrname, value)
1320
return property(lambda self: getattr(self, attrname), setter)
1322
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1323
approvals_pending = notifychangeproperty(dbus.Boolean,
1326
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1327
last_enabled = notifychangeproperty(datetime_to_dbus,
1329
checker = notifychangeproperty(
1330
dbus.Boolean, "CheckerRunning",
1331
type_func = lambda checker: checker is not None)
1332
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1334
last_checker_status = notifychangeproperty(dbus.Int16,
1335
"LastCheckerStatus")
1336
last_approval_request = notifychangeproperty(
1337
datetime_to_dbus, "LastApprovalRequest")
1338
approved_by_default = notifychangeproperty(dbus.Boolean,
1339
"ApprovedByDefault")
1340
approval_delay = notifychangeproperty(
1341
dbus.UInt64, "ApprovalDelay",
1342
type_func = lambda td: td.total_seconds() * 1000)
1343
approval_duration = notifychangeproperty(
1344
dbus.UInt64, "ApprovalDuration",
1345
type_func = lambda td: td.total_seconds() * 1000)
1346
host = notifychangeproperty(dbus.String, "Host")
1347
timeout = notifychangeproperty(
1348
dbus.UInt64, "Timeout",
1349
type_func = lambda td: td.total_seconds() * 1000)
1350
extended_timeout = notifychangeproperty(
1351
dbus.UInt64, "ExtendedTimeout",
1352
type_func = lambda td: td.total_seconds() * 1000)
1353
interval = notifychangeproperty(
1354
dbus.UInt64, "Interval",
1355
type_func = lambda td: td.total_seconds() * 1000)
1356
checker_command = notifychangeproperty(dbus.String, "Checker")
1357
secret = notifychangeproperty(dbus.ByteArray, "Secret",
1358
invalidate_only=True)
1360
del notifychangeproperty
1362
def __del__(self, *args, **kwargs):
1364
self.remove_from_connection()
1367
if hasattr(DBusObjectWithProperties, "__del__"):
1368
DBusObjectWithProperties.__del__(self, *args, **kwargs)
1369
Client.__del__(self, *args, **kwargs)
1371
def checker_callback(self, pid, condition, command,
1373
self.checker_callback_tag = None
1375
if os.WIFEXITED(condition):
1376
exitstatus = os.WEXITSTATUS(condition)
1378
self.CheckerCompleted(dbus.Int16(exitstatus),
1379
dbus.Int64(condition),
1380
dbus.String(command))
1383
self.CheckerCompleted(dbus.Int16(-1),
1384
dbus.Int64(condition),
1385
dbus.String(command))
1387
return Client.checker_callback(self, pid, condition, command,
1390
def start_checker(self, *args, **kwargs):
1391
old_checker_pid = getattr(self.checker, "pid", None)
1392
r = Client.start_checker(self, *args, **kwargs)
1393
# Only if new checker process was started
1394
if (self.checker is not None
1395
and old_checker_pid != self.checker.pid):
1397
self.CheckerStarted(self.current_checker_command)
1400
def _reset_approved(self):
1401
self.approved = None
1404
def approve(self, value=True):
1405
self.approved = value
1406
gobject.timeout_add(int(self.approval_duration.total_seconds()
1407
* 1000), self._reset_approved)
1408
self.send_changedstate()
1410
## D-Bus methods, signals & properties
1416
# CheckerCompleted - signal
1417
@dbus.service.signal(_interface, signature="nxs")
1418
def CheckerCompleted(self, exitcode, waitstatus, command):
1422
# CheckerStarted - signal
1423
@dbus.service.signal(_interface, signature="s")
1424
def CheckerStarted(self, command):
1428
# PropertyChanged - signal
1429
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1430
@dbus.service.signal(_interface, signature="sv")
1431
def PropertyChanged(self, property, value):
1435
# GotSecret - signal
1436
@dbus.service.signal(_interface)
1437
def GotSecret(self):
1439
Is sent after a successful transfer of secret from the Mandos
1440
server to mandos-client
1445
@dbus.service.signal(_interface, signature="s")
1446
def Rejected(self, reason):
1450
# NeedApproval - signal
1451
@dbus.service.signal(_interface, signature="tb")
1452
def NeedApproval(self, timeout, default):
1454
return self.need_approval()
1459
@dbus.service.method(_interface, in_signature="b")
1460
def Approve(self, value):
1463
# CheckedOK - method
1464
@dbus.service.method(_interface)
1465
def CheckedOK(self):
1469
@dbus.service.method(_interface)
1474
# StartChecker - method
1475
@dbus.service.method(_interface)
1476
def StartChecker(self):
1478
self.start_checker()
1481
@dbus.service.method(_interface)
1486
# StopChecker - method
1487
@dbus.service.method(_interface)
1488
def StopChecker(self):
1493
# ApprovalPending - property
1494
@dbus_service_property(_interface, signature="b", access="read")
1495
def ApprovalPending_dbus_property(self):
1496
return dbus.Boolean(bool(self.approvals_pending))
1498
# ApprovedByDefault - property
1499
@dbus_service_property(_interface,
1502
def ApprovedByDefault_dbus_property(self, value=None):
1503
if value is None: # get
1504
return dbus.Boolean(self.approved_by_default)
1505
self.approved_by_default = bool(value)
1507
# ApprovalDelay - property
1508
@dbus_service_property(_interface,
1511
def ApprovalDelay_dbus_property(self, value=None):
1512
if value is None: # get
1513
return dbus.UInt64(self.approval_delay.total_seconds()
1515
self.approval_delay = datetime.timedelta(0, 0, 0, value)
1517
# ApprovalDuration - property
1518
@dbus_service_property(_interface,
1521
def ApprovalDuration_dbus_property(self, value=None):
1522
if value is None: # get
1523
return dbus.UInt64(self.approval_duration.total_seconds()
1525
self.approval_duration = datetime.timedelta(0, 0, 0, value)
1528
@dbus_service_property(_interface, signature="s", access="read")
1529
def Name_dbus_property(self):
1530
return dbus.String(self.name)
1532
# Fingerprint - property
1533
@dbus_service_property(_interface, signature="s", access="read")
1534
def Fingerprint_dbus_property(self):
1535
return dbus.String(self.fingerprint)
1538
@dbus_service_property(_interface,
1541
def Host_dbus_property(self, value=None):
1542
if value is None: # get
1543
return dbus.String(self.host)
1544
self.host = str(value)
1546
# Created - property
1547
@dbus_service_property(_interface, signature="s", access="read")
1548
def Created_dbus_property(self):
1549
return datetime_to_dbus(self.created)
1551
# LastEnabled - property
1552
@dbus_service_property(_interface, signature="s", access="read")
1553
def LastEnabled_dbus_property(self):
1554
return datetime_to_dbus(self.last_enabled)
1556
# Enabled - property
1557
@dbus_service_property(_interface,
1560
def Enabled_dbus_property(self, value=None):
1561
if value is None: # get
1562
return dbus.Boolean(self.enabled)
1568
# LastCheckedOK - property
1569
@dbus_service_property(_interface,
1572
def LastCheckedOK_dbus_property(self, value=None):
1573
if value is not None:
1576
return datetime_to_dbus(self.last_checked_ok)
1578
# LastCheckerStatus - property
1579
@dbus_service_property(_interface, signature="n", access="read")
1580
def LastCheckerStatus_dbus_property(self):
1581
return dbus.Int16(self.last_checker_status)
1583
# Expires - property
1584
@dbus_service_property(_interface, signature="s", access="read")
1585
def Expires_dbus_property(self):
1586
return datetime_to_dbus(self.expires)
1588
# LastApprovalRequest - property
1589
@dbus_service_property(_interface, signature="s", access="read")
1590
def LastApprovalRequest_dbus_property(self):
1591
return datetime_to_dbus(self.last_approval_request)
1593
# Timeout - property
1594
@dbus_service_property(_interface,
1597
def Timeout_dbus_property(self, value=None):
1598
if value is None: # get
1599
return dbus.UInt64(self.timeout.total_seconds() * 1000)
1600
old_timeout = self.timeout
1601
self.timeout = datetime.timedelta(0, 0, 0, value)
1602
# Reschedule disabling
1604
now = datetime.datetime.utcnow()
1605
self.expires += self.timeout - old_timeout
1606
if self.expires <= now:
1607
# The timeout has passed
1610
if (getattr(self, "disable_initiator_tag", None)
1613
gobject.source_remove(self.disable_initiator_tag)
1614
self.disable_initiator_tag = gobject.timeout_add(
1615
int((self.expires - now).total_seconds() * 1000),
1618
# ExtendedTimeout - property
1619
@dbus_service_property(_interface,
1622
def ExtendedTimeout_dbus_property(self, value=None):
1623
if value is None: # get
1624
return dbus.UInt64(self.extended_timeout.total_seconds()
1626
self.extended_timeout = datetime.timedelta(0, 0, 0, value)
1628
# Interval - property
1629
@dbus_service_property(_interface,
1632
def Interval_dbus_property(self, value=None):
1633
if value is None: # get
1634
return dbus.UInt64(self.interval.total_seconds() * 1000)
1635
self.interval = datetime.timedelta(0, 0, 0, value)
1636
if getattr(self, "checker_initiator_tag", None) is None:
1639
# Reschedule checker run
1640
gobject.source_remove(self.checker_initiator_tag)
1641
self.checker_initiator_tag = gobject.timeout_add(
1642
value, self.start_checker)
1643
self.start_checker() # Start one now, too
1645
# Checker - property
1646
@dbus_service_property(_interface,
1649
def Checker_dbus_property(self, value=None):
1650
if value is None: # get
1651
return dbus.String(self.checker_command)
1652
self.checker_command = str(value)
1654
# CheckerRunning - property
1655
@dbus_service_property(_interface,
1658
def CheckerRunning_dbus_property(self, value=None):
1659
if value is None: # get
1660
return dbus.Boolean(self.checker is not None)
1662
self.start_checker()
1666
# ObjectPath - property
1667
@dbus_service_property(_interface, signature="o", access="read")
1668
def ObjectPath_dbus_property(self):
1669
return self.dbus_object_path # is already a dbus.ObjectPath
1672
@dbus_service_property(_interface,
1676
def Secret_dbus_property(self, value):
1677
self.secret = bytes(value)
1682
class ProxyClient(object):
1683
def __init__(self, child_pipe, fpr, address):
1684
self._pipe = child_pipe
1685
self._pipe.send(('init', fpr, address))
1686
if not self._pipe.recv():
1689
def __getattribute__(self, name):
1691
return super(ProxyClient, self).__getattribute__(name)
1692
self._pipe.send(('getattr', name))
1693
data = self._pipe.recv()
1694
if data[0] == 'data':
1696
if data[0] == 'function':
1698
def func(*args, **kwargs):
1699
self._pipe.send(('funcall', name, args, kwargs))
1700
return self._pipe.recv()[1]
1704
def __setattr__(self, name, value):
1706
return super(ProxyClient, self).__setattr__(name, value)
1707
self._pipe.send(('setattr', name, value))
1710
class ClientHandler(socketserver.BaseRequestHandler, object):
1711
"""A class to handle client connections.
1713
Instantiated once for each connection to handle it.
368
def still_valid(self):
369
"""Has the timeout not yet passed for this client?"""
370
now = datetime.datetime.now()
371
if self.last_checked_ok is None:
372
return now < (self.created + self.timeout)
374
return now < (self.last_checked_ok + self.timeout)
377
def peer_certificate(session):
378
"Return the peer's OpenPGP certificate as a bytestring"
379
# If not an OpenPGP certificate...
380
if gnutls.library.functions.gnutls_certificate_type_get\
381
(session._c_object) \
382
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
383
# ...do the normal thing
384
return session.peer_certificate
385
list_size = ctypes.c_uint()
386
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
387
(session._c_object, ctypes.byref(list_size))
388
if list_size.value == 0:
391
return ctypes.string_at(cert.data, cert.size)
394
def fingerprint(openpgp):
395
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
396
# New GnuTLS "datum" with the OpenPGP public key
397
datum = gnutls.library.types.gnutls_datum_t\
398
(ctypes.cast(ctypes.c_char_p(openpgp),
399
ctypes.POINTER(ctypes.c_ubyte)),
400
ctypes.c_uint(len(openpgp)))
401
# New empty GnuTLS certificate
402
crt = gnutls.library.types.gnutls_openpgp_crt_t()
403
gnutls.library.functions.gnutls_openpgp_crt_init\
405
# Import the OpenPGP public key into the certificate
406
gnutls.library.functions.gnutls_openpgp_crt_import\
407
(crt, ctypes.byref(datum),
408
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
409
# New buffer for the fingerprint
410
buffer = ctypes.create_string_buffer(20)
411
buffer_length = ctypes.c_size_t()
412
# Get the fingerprint from the certificate into the buffer
413
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
414
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
415
# Deinit the certificate
416
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
417
# Convert the buffer to a Python bytestring
418
fpr = ctypes.string_at(buffer, buffer_length.value)
419
# Convert the bytestring to hexadecimal notation
420
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
424
class tcp_handler(SocketServer.BaseRequestHandler, object):
425
"""A TCP request handler class.
426
Instantiated by IPv6_TCPServer for each request to handle it.
1714
427
Note: This will run in its own forked process."""
1716
429
def handle(self):
1717
with contextlib.closing(self.server.child_pipe) as child_pipe:
1718
logger.info("TCP connection from: %s",
1719
str(self.client_address))
1720
logger.debug("Pipe FD: %d",
1721
self.server.child_pipe.fileno())
1723
session = gnutls.connection.ClientSession(
1724
self.request, gnutls.connection .X509Credentials())
1726
# Note: gnutls.connection.X509Credentials is really a
1727
# generic GnuTLS certificate credentials object so long as
1728
# no X.509 keys are added to it. Therefore, we can use it
1729
# here despite using OpenPGP certificates.
1731
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
1732
# "+AES-256-CBC", "+SHA1",
1733
# "+COMP-NULL", "+CTYPE-OPENPGP",
1735
# Use a fallback default, since this MUST be set.
1736
priority = self.server.gnutls_priority
1737
if priority is None:
1739
gnutls.library.functions.gnutls_priority_set_direct(
1740
session._c_object, priority, None)
1742
# Start communication using the Mandos protocol
1743
# Get protocol number
1744
line = self.request.makefile().readline()
1745
logger.debug("Protocol version: %r", line)
1747
if int(line.strip().split()[0]) > 1:
1748
raise RuntimeError(line)
1749
except (ValueError, IndexError, RuntimeError) as error:
1750
logger.error("Unknown protocol version: %s", error)
1753
# Start GnuTLS connection
1756
except gnutls.errors.GNUTLSError as error:
1757
logger.warning("Handshake failed: %s", error)
1758
# Do not run session.bye() here: the session is not
1759
# established. Just abandon the request.
1761
logger.debug("Handshake succeeded")
1763
approval_required = False
1766
fpr = self.fingerprint(
1767
self.peer_certificate(session))
1769
gnutls.errors.GNUTLSError) as error:
1770
logger.warning("Bad certificate: %s", error)
1772
logger.debug("Fingerprint: %s", fpr)
1775
client = ProxyClient(child_pipe, fpr,
1776
self.client_address)
1780
if client.approval_delay:
1781
delay = client.approval_delay
1782
client.approvals_pending += 1
1783
approval_required = True
1786
if not client.enabled:
1787
logger.info("Client %s is disabled",
1789
if self.server.use_dbus:
1791
client.Rejected("Disabled")
1794
if client.approved or not client.approval_delay:
1795
#We are approved or approval is disabled
1797
elif client.approved is None:
1798
logger.info("Client %s needs approval",
1800
if self.server.use_dbus:
1802
client.NeedApproval(
1803
client.approval_delay.total_seconds()
1804
* 1000, client.approved_by_default)
1806
logger.warning("Client %s was not approved",
1808
if self.server.use_dbus:
1810
client.Rejected("Denied")
1813
#wait until timeout or approved
1814
time = datetime.datetime.now()
1815
client.changedstate.acquire()
1816
client.changedstate.wait(delay.total_seconds())
1817
client.changedstate.release()
1818
time2 = datetime.datetime.now()
1819
if (time2 - time) >= delay:
1820
if not client.approved_by_default:
1821
logger.warning("Client %s timed out while"
1822
" waiting for approval",
1824
if self.server.use_dbus:
1826
client.Rejected("Approval timed out")
1831
delay -= time2 - time
1834
while sent_size < len(client.secret):
1836
sent = session.send(client.secret[sent_size:])
1837
except gnutls.errors.GNUTLSError as error:
1838
logger.warning("gnutls send failed",
1841
logger.debug("Sent: %d, remaining: %d", sent,
1842
len(client.secret) - (sent_size
1846
logger.info("Sending secret to %s", client.name)
1847
# bump the timeout using extended_timeout
1848
client.bump_timeout(client.extended_timeout)
1849
if self.server.use_dbus:
1854
if approval_required:
1855
client.approvals_pending -= 1
1858
except gnutls.errors.GNUTLSError as error:
1859
logger.warning("GnuTLS bye failed",
1863
def peer_certificate(session):
1864
"Return the peer's OpenPGP certificate as a bytestring"
1865
# If not an OpenPGP certificate...
1866
if (gnutls.library.functions.gnutls_certificate_type_get(
1868
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1869
# ...do the normal thing
1870
return session.peer_certificate
1871
list_size = ctypes.c_uint(1)
1872
cert_list = (gnutls.library.functions
1873
.gnutls_certificate_get_peers
1874
(session._c_object, ctypes.byref(list_size)))
1875
if not bool(cert_list) and list_size.value != 0:
1876
raise gnutls.errors.GNUTLSError("error getting peer"
1878
if list_size.value == 0:
1881
return ctypes.string_at(cert.data, cert.size)
1884
def fingerprint(openpgp):
1885
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1886
# New GnuTLS "datum" with the OpenPGP public key
1887
datum = gnutls.library.types.gnutls_datum_t(
1888
ctypes.cast(ctypes.c_char_p(openpgp),
1889
ctypes.POINTER(ctypes.c_ubyte)),
1890
ctypes.c_uint(len(openpgp)))
1891
# New empty GnuTLS certificate
1892
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1893
gnutls.library.functions.gnutls_openpgp_crt_init(
1895
# Import the OpenPGP public key into the certificate
1896
gnutls.library.functions.gnutls_openpgp_crt_import(
1897
crt, ctypes.byref(datum),
1898
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
1899
# Verify the self signature in the key
1900
crtverify = ctypes.c_uint()
1901
gnutls.library.functions.gnutls_openpgp_crt_verify_self(
1902
crt, 0, ctypes.byref(crtverify))
1903
if crtverify.value != 0:
1904
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1905
raise gnutls.errors.CertificateSecurityError(
1907
# New buffer for the fingerprint
1908
buf = ctypes.create_string_buffer(20)
1909
buf_len = ctypes.c_size_t()
1910
# Get the fingerprint from the certificate into the buffer
1911
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint(
1912
crt, ctypes.byref(buf), ctypes.byref(buf_len))
1913
# Deinit the certificate
1914
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1915
# Convert the buffer to a Python bytestring
1916
fpr = ctypes.string_at(buf, buf_len.value)
1917
# Convert the bytestring to hexadecimal notation
1918
hex_fpr = binascii.hexlify(fpr).upper()
1922
class MultiprocessingMixIn(object):
1923
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
1925
def sub_process_main(self, request, address):
1927
self.finish_request(request, address)
1929
self.handle_error(request, address)
1930
self.close_request(request)
1932
def process_request(self, request, address):
1933
"""Start a new process to process the request."""
1934
proc = multiprocessing.Process(target = self.sub_process_main,
1935
args = (request, address))
1940
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
1941
""" adds a pipe to the MixIn """
1943
def process_request(self, request, client_address):
1944
"""Overrides and wraps the original process_request().
1946
This function creates a new pipe in self.pipe
1948
parent_pipe, self.child_pipe = multiprocessing.Pipe()
1950
proc = MultiprocessingMixIn.process_request(self, request,
1952
self.child_pipe.close()
1953
self.add_pipe(parent_pipe, proc)
1955
def add_pipe(self, parent_pipe, proc):
1956
"""Dummy function; override as necessary"""
1957
raise NotImplementedError()
1960
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1961
socketserver.TCPServer, object):
1962
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
430
logger.info(u"TCP connection from: %s",
431
unicode(self.client_address))
432
session = gnutls.connection.ClientSession\
433
(self.request, gnutls.connection.X509Credentials())
435
line = self.request.makefile().readline()
436
logger.debug(u"Protocol version: %r", line)
438
if int(line.strip().split()[0]) > 1:
440
except (ValueError, IndexError, RuntimeError), error:
441
logger.error(u"Unknown protocol version: %s", error)
444
# Note: gnutls.connection.X509Credentials is really a generic
445
# GnuTLS certificate credentials object so long as no X.509
446
# keys are added to it. Therefore, we can use it here despite
447
# using OpenPGP certificates.
449
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
450
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
452
priority = "NORMAL" # Fallback default, since this
454
if self.server.settings["priority"]:
455
priority = self.server.settings["priority"]
456
gnutls.library.functions.gnutls_priority_set_direct\
457
(session._c_object, priority, None);
461
except gnutls.errors.GNUTLSError, error:
462
logger.warning(u"Handshake failed: %s", error)
463
# Do not run session.bye() here: the session is not
464
# established. Just abandon the request.
467
fpr = fingerprint(peer_certificate(session))
468
except (TypeError, gnutls.errors.GNUTLSError), error:
469
logger.warning(u"Bad certificate: %s", error)
472
logger.debug(u"Fingerprint: %s", fpr)
474
for c in self.server.clients:
475
if c.fingerprint == fpr:
479
logger.warning(u"Client not found for fingerprint: %s",
483
# Have to check if client.still_valid(), since it is possible
484
# that the client timed out while establishing the GnuTLS
486
if not client.still_valid():
487
logger.warning(u"Client %(name)s is invalid",
492
while sent_size < len(client.secret):
493
sent = session.send(client.secret[sent_size:])
494
logger.debug(u"Sent: %d, remaining: %d",
495
sent, len(client.secret)
496
- (sent_size + sent))
501
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
502
"""IPv6 TCP server. Accepts 'None' as address and/or port.
1965
enabled: Boolean; whether this server is activated yet
1966
interface: None or a network interface name (string)
1967
use_ipv6: Boolean; to use IPv6 or not
504
settings: Server settings
505
clients: Set() of Client objects
1970
def __init__(self, server_address, RequestHandlerClass,
1974
"""If socketfd is set, use that file descriptor instead of
1975
creating a new one with socket.socket().
1977
self.interface = interface
1979
self.address_family = socket.AF_INET6
1980
if socketfd is not None:
1981
# Save the file descriptor
1982
self.socketfd = socketfd
1983
# Save the original socket.socket() function
1984
self.socket_socket = socket.socket
1985
# To implement --socket, we monkey patch socket.socket.
1987
# (When socketserver.TCPServer is a new-style class, we
1988
# could make self.socket into a property instead of monkey
1989
# patching socket.socket.)
1991
# Create a one-time-only replacement for socket.socket()
1992
@functools.wraps(socket.socket)
1993
def socket_wrapper(*args, **kwargs):
1994
# Restore original function so subsequent calls are
1996
socket.socket = self.socket_socket
1997
del self.socket_socket
1998
# This time only, return a new socket object from the
1999
# saved file descriptor.
2000
return socket.fromfd(self.socketfd, *args, **kwargs)
2001
# Replace socket.socket() function with wrapper
2002
socket.socket = socket_wrapper
2003
# The socketserver.TCPServer.__init__ will call
2004
# socket.socket(), which might be our replacement,
2005
# socket_wrapper(), if socketfd was set.
2006
socketserver.TCPServer.__init__(self, server_address,
2007
RequestHandlerClass)
507
address_family = socket.AF_INET6
508
def __init__(self, *args, **kwargs):
509
if "settings" in kwargs:
510
self.settings = kwargs["settings"]
511
del kwargs["settings"]
512
if "clients" in kwargs:
513
self.clients = kwargs["clients"]
514
del kwargs["clients"]
515
return super(type(self), self).__init__(*args, **kwargs)
2009
516
def server_bind(self):
2010
517
"""This overrides the normal server_bind() function
2011
518
to bind to an interface if one was specified, and also NOT to
2012
519
bind to an address or port if they were not specified."""
2013
if self.interface is not None:
2014
if SO_BINDTODEVICE is None:
2015
logger.error("SO_BINDTODEVICE does not exist;"
2016
" cannot bind to interface %s",
2020
self.socket.setsockopt(
2021
socket.SOL_SOCKET, SO_BINDTODEVICE,
2022
(self.interface + "\0").encode("utf-8"))
2023
except socket.error as error:
2024
if error.errno == errno.EPERM:
2025
logger.error("No permission to bind to"
2026
" interface %s", self.interface)
2027
elif error.errno == errno.ENOPROTOOPT:
2028
logger.error("SO_BINDTODEVICE not available;"
2029
" cannot bind to interface %s",
2031
elif error.errno == errno.ENODEV:
2032
logger.error("Interface %s does not exist,"
2033
" cannot bind", self.interface)
520
if self.settings["interface"]:
521
# 25 is from /usr/include/asm-i486/socket.h
522
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
524
self.socket.setsockopt(socket.SOL_SOCKET,
526
self.settings["interface"])
527
except socket.error, error:
528
if error[0] == errno.EPERM:
529
logger.error(u"No permission to"
530
u" bind to interface %s",
531
self.settings["interface"])
2036
534
# Only bind(2) the socket if we really need to.
2037
535
if self.server_address[0] or self.server_address[1]:
2038
536
if not self.server_address[0]:
2039
if self.address_family == socket.AF_INET6:
2040
any_address = "::" # in6addr_any
2042
any_address = "0.0.0.0" # INADDR_ANY
2043
self.server_address = (any_address,
538
self.server_address = (in6addr_any,
2044
539
self.server_address[1])
2045
540
elif not self.server_address[1]:
2046
self.server_address = (self.server_address[0], 0)
2047
# if self.interface:
541
self.server_address = (self.server_address[0],
543
# if self.settings["interface"]:
2048
544
# self.server_address = (self.server_address[0],
2051
547
# if_nametoindex
2053
return socketserver.TCPServer.server_bind(self)
2056
class MandosServer(IPv6_TCPServer):
2060
clients: set of Client objects
2061
gnutls_priority GnuTLS priority string
2062
use_dbus: Boolean; to emit D-Bus signals or not
2064
Assumes a gobject.MainLoop event loop.
2067
def __init__(self, server_address, RequestHandlerClass,
2071
gnutls_priority=None,
2074
self.enabled = False
2075
self.clients = clients
2076
if self.clients is None:
2078
self.use_dbus = use_dbus
2079
self.gnutls_priority = gnutls_priority
2080
IPv6_TCPServer.__init__(self, server_address,
2081
RequestHandlerClass,
2082
interface = interface,
2083
use_ipv6 = use_ipv6,
2084
socketfd = socketfd)
2086
def server_activate(self):
2088
return socketserver.TCPServer.server_activate(self)
2093
def add_pipe(self, parent_pipe, proc):
2094
# Call "handle_ipc" for both data and EOF events
2095
gobject.io_add_watch(
2096
parent_pipe.fileno(),
2097
gobject.IO_IN | gobject.IO_HUP,
2098
functools.partial(self.handle_ipc,
2099
parent_pipe = parent_pipe,
2102
def handle_ipc(self, source, condition,
2105
client_object=None):
2106
# error, or the other end of multiprocessing.Pipe has closed
2107
if condition & (gobject.IO_ERR | gobject.IO_HUP):
2108
# Wait for other process to exit
2112
# Read a request from the child
2113
request = parent_pipe.recv()
2114
command = request[0]
2116
if command == 'init':
2118
address = request[2]
2120
for c in self.clients.itervalues():
2121
if c.fingerprint == fpr:
2125
logger.info("Client not found for fingerprint: %s, ad"
2126
"dress: %s", fpr, address)
2129
mandos_dbus_service.ClientNotFound(fpr,
2131
parent_pipe.send(False)
2134
gobject.io_add_watch(
2135
parent_pipe.fileno(),
2136
gobject.IO_IN | gobject.IO_HUP,
2137
functools.partial(self.handle_ipc,
2138
parent_pipe = parent_pipe,
2140
client_object = client))
2141
parent_pipe.send(True)
2142
# remove the old hook in favor of the new above hook on
2145
if command == 'funcall':
2146
funcname = request[1]
2150
parent_pipe.send(('data', getattr(client_object,
2154
if command == 'getattr':
2155
attrname = request[1]
2156
if callable(client_object.__getattribute__(attrname)):
2157
parent_pipe.send(('function', ))
2160
'data', client_object.__getattribute__(attrname)))
2162
if command == 'setattr':
2163
attrname = request[1]
2165
setattr(client_object, attrname, value)
2170
def rfc3339_duration_to_delta(duration):
2171
"""Parse an RFC 3339 "duration" and return a datetime.timedelta
2173
>>> rfc3339_duration_to_delta("P7D")
2174
datetime.timedelta(7)
2175
>>> rfc3339_duration_to_delta("PT60S")
2176
datetime.timedelta(0, 60)
2177
>>> rfc3339_duration_to_delta("PT60M")
2178
datetime.timedelta(0, 3600)
2179
>>> rfc3339_duration_to_delta("PT24H")
2180
datetime.timedelta(1)
2181
>>> rfc3339_duration_to_delta("P1W")
2182
datetime.timedelta(7)
2183
>>> rfc3339_duration_to_delta("PT5M30S")
2184
datetime.timedelta(0, 330)
2185
>>> rfc3339_duration_to_delta("P1DT3M20S")
2186
datetime.timedelta(1, 200)
2189
# Parsing an RFC 3339 duration with regular expressions is not
2190
# possible - there would have to be multiple places for the same
2191
# values, like seconds. The current code, while more esoteric, is
2192
# cleaner without depending on a parsing library. If Python had a
2193
# built-in library for parsing we would use it, but we'd like to
2194
# avoid excessive use of external libraries.
2196
# New type for defining tokens, syntax, and semantics all-in-one
2197
Token = collections.namedtuple("Token", (
2198
"regexp", # To match token; if "value" is not None, must have
2199
# a "group" containing digits
2200
"value", # datetime.timedelta or None
2201
"followers")) # Tokens valid after this token
2202
# RFC 3339 "duration" tokens, syntax, and semantics; taken from
2203
# the "duration" ABNF definition in RFC 3339, Appendix A.
2204
token_end = Token(re.compile(r"$"), None, frozenset())
2205
token_second = Token(re.compile(r"(\d+)S"),
2206
datetime.timedelta(seconds=1),
2207
frozenset((token_end, )))
2208
token_minute = Token(re.compile(r"(\d+)M"),
2209
datetime.timedelta(minutes=1),
2210
frozenset((token_second, token_end)))
2211
token_hour = Token(re.compile(r"(\d+)H"),
2212
datetime.timedelta(hours=1),
2213
frozenset((token_minute, token_end)))
2214
token_time = Token(re.compile(r"T"),
2216
frozenset((token_hour, token_minute,
2218
token_day = Token(re.compile(r"(\d+)D"),
2219
datetime.timedelta(days=1),
2220
frozenset((token_time, token_end)))
2221
token_month = Token(re.compile(r"(\d+)M"),
2222
datetime.timedelta(weeks=4),
2223
frozenset((token_day, token_end)))
2224
token_year = Token(re.compile(r"(\d+)Y"),
2225
datetime.timedelta(weeks=52),
2226
frozenset((token_month, token_end)))
2227
token_week = Token(re.compile(r"(\d+)W"),
2228
datetime.timedelta(weeks=1),
2229
frozenset((token_end, )))
2230
token_duration = Token(re.compile(r"P"), None,
2231
frozenset((token_year, token_month,
2232
token_day, token_time,
2234
# Define starting values
2235
value = datetime.timedelta() # Value so far
2237
followers = frozenset((token_duration, )) # Following valid tokens
2238
s = duration # String left to parse
2239
# Loop until end token is found
2240
while found_token is not token_end:
2241
# Search for any currently valid tokens
2242
for token in followers:
2243
match = token.regexp.match(s)
2244
if match is not None:
2246
if token.value is not None:
2247
# Value found, parse digits
2248
factor = int(match.group(1), 10)
2249
# Add to value so far
2250
value += factor * token.value
2251
# Strip token from string
2252
s = token.regexp.sub("", s, 1)
2255
# Set valid next tokens
2256
followers = found_token.followers
2259
# No currently valid tokens were found
2260
raise ValueError("Invalid RFC 3339 duration: {!r}"
550
return super(type(self), self).server_bind()
2266
553
def string_to_delta(interval):
2267
554
"""Parse a string and return a datetime.timedelta
2269
556
>>> string_to_delta('7d')
2270
557
datetime.timedelta(7)
2271
558
>>> string_to_delta('60s')
2397
693
"debug": "False",
2399
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2400
":+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
695
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2401
696
"servicename": "Mandos",
2407
"statedir": "/var/lib/mandos",
2408
"foreground": "False",
2412
699
# Parse config file for server-global settings
2413
server_config = configparser.SafeConfigParser(server_defaults)
700
server_config = ConfigParser.SafeConfigParser(server_defaults)
2414
701
del server_defaults
2415
702
server_config.read(os.path.join(options.configdir, "mandos.conf"))
703
server_section = "server"
2416
704
# Convert the SafeConfigParser object to a dict
2417
server_settings = server_config.defaults()
2418
# Use the appropriate methods on the non-string config options
2419
for option in ("debug", "use_dbus", "use_ipv6", "foreground"):
2420
server_settings[option] = server_config.getboolean("DEFAULT",
2422
if server_settings["port"]:
2423
server_settings["port"] = server_config.getint("DEFAULT",
2425
if server_settings["socket"]:
2426
server_settings["socket"] = server_config.getint("DEFAULT",
2428
# Later, stdin will, and stdout and stderr might, be dup'ed
2429
# over with an opened os.devnull. But we don't want this to
2430
# happen with a supplied network socket.
2431
if 0 <= server_settings["socket"] <= 2:
2432
server_settings["socket"] = os.dup(server_settings
705
server_settings = dict(server_config.items(server_section))
706
# Use getboolean on the boolean config option
707
server_settings["debug"] = server_config.getboolean\
708
(server_section, "debug")
2434
709
del server_config
2436
711
# Override the settings from the config file with command line
2437
712
# options, if set.
2438
713
for option in ("interface", "address", "port", "debug",
2439
"priority", "servicename", "configdir", "use_dbus",
2440
"use_ipv6", "debuglevel", "restore", "statedir",
2441
"socket", "foreground", "zeroconf"):
714
"priority", "servicename", "configdir"):
2442
715
value = getattr(options, option)
2443
716
if value is not None:
2444
717
server_settings[option] = value
2446
# Force all strings to be unicode
2447
for option in server_settings.keys():
2448
if isinstance(server_settings[option], bytes):
2449
server_settings[option] = (server_settings[option]
2451
# Force all boolean options to be boolean
2452
for option in ("debug", "use_dbus", "use_ipv6", "restore",
2453
"foreground", "zeroconf"):
2454
server_settings[option] = bool(server_settings[option])
2455
# Debug implies foreground
2456
if server_settings["debug"]:
2457
server_settings["foreground"] = True
2458
719
# Now we have our good server settings in "server_settings"
2460
##################################################################
2462
if (not server_settings["zeroconf"]
2463
and not (server_settings["port"]
2464
or server_settings["socket"] != "")):
2465
parser.error("Needs port or socket to work without Zeroconf")
2468
721
debug = server_settings["debug"]
2469
debuglevel = server_settings["debuglevel"]
2470
use_dbus = server_settings["use_dbus"]
2471
use_ipv6 = server_settings["use_ipv6"]
2472
stored_state_path = os.path.join(server_settings["statedir"],
2474
foreground = server_settings["foreground"]
2475
zeroconf = server_settings["zeroconf"]
2478
initlogger(debug, logging.DEBUG)
2483
level = getattr(logging, debuglevel.upper())
2484
initlogger(debug, level)
724
syslogger.setLevel(logging.WARNING)
725
console.setLevel(logging.WARNING)
2486
727
if server_settings["servicename"] != "Mandos":
2487
syslogger.setFormatter(
2488
logging.Formatter('Mandos ({}) [%(process)d]:'
2489
' %(levelname)s: %(message)s'.format(
2490
server_settings["servicename"])))
728
syslogger.setFormatter(logging.Formatter\
729
('Mandos (%s): %%(levelname)s:'
731
% server_settings["servicename"]))
2492
733
# Parse config file with clients
2493
client_config = configparser.SafeConfigParser(Client
734
client_defaults = { "timeout": "1h",
736
"checker": "fping -q -- %%(host)s",
738
client_config = ConfigParser.SafeConfigParser(client_defaults)
2495
739
client_config.read(os.path.join(server_settings["configdir"],
2496
740
"clients.conf"))
2498
global mandos_dbus_service
2499
mandos_dbus_service = None
2502
if server_settings["socket"] != "":
2503
socketfd = server_settings["socket"]
2504
tcp_server = MandosServer(
2505
(server_settings["address"], server_settings["port"]),
2507
interface=(server_settings["interface"] or None),
2509
gnutls_priority=server_settings["priority"],
2513
pidfilename = "/run/mandos.pid"
2514
if not os.path.isdir("/run/."):
2515
pidfilename = "/var/run/mandos.pid"
2518
pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
2519
except IOError as e:
2520
logger.error("Could not open file %r", pidfilename,
2523
for name in ("_mandos", "mandos", "nobody"):
2525
uid = pwd.getpwnam(name).pw_uid
2526
gid = pwd.getpwnam(name).pw_gid
2536
except OSError as error:
2537
if error.errno != errno.EPERM:
2541
# Enable all possible GnuTLS debugging
2543
# "Use a log level over 10 to enable all debugging options."
2545
gnutls.library.functions.gnutls_global_set_log_level(11)
2547
@gnutls.library.types.gnutls_log_func
2548
def debug_gnutls(level, string):
2549
logger.debug("GnuTLS: %s", string[:-1])
2551
gnutls.library.functions.gnutls_global_set_log_function(
2554
# Redirect stdin so all checkers get /dev/null
2555
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2556
os.dup2(null, sys.stdin.fileno())
2560
# Need to fork before connecting to D-Bus
2562
# Close all input and output, do double fork, etc.
2565
# multiprocessing will use threads, so before we use gobject we
2566
# need to inform gobject that threads will be used.
2567
gobject.threads_init()
743
service = AvahiService(name = server_settings["servicename"],
744
type = "_mandos._tcp", );
745
if server_settings["interface"]:
746
service.interface = if_nametoindex(server_settings["interface"])
2569
748
global main_loop
2570
751
# From the Avahi example code
2571
DBusGMainLoop(set_as_default=True)
752
DBusGMainLoop(set_as_default=True )
2572
753
main_loop = gobject.MainLoop()
2573
754
bus = dbus.SystemBus()
755
server = dbus.Interface(
756
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
757
avahi.DBUS_INTERFACE_SERVER )
2574
758
# End of Avahi example code
2577
bus_name = dbus.service.BusName("se.recompile.Mandos",
2580
old_bus_name = dbus.service.BusName(
2581
"se.bsnet.fukt.Mandos", bus,
2583
except dbus.exceptions.DBusException as e:
2584
logger.error("Disabling D-Bus:", exc_info=e)
2586
server_settings["use_dbus"] = False
2587
tcp_server.use_dbus = False
2589
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2590
service = AvahiServiceToSyslog(
2591
name = server_settings["servicename"],
2592
servicetype = "_mandos._tcp",
2593
protocol = protocol,
2595
if server_settings["interface"]:
2596
service.interface = if_nametoindex(
2597
server_settings["interface"].encode("utf-8"))
2599
global multiprocessing_manager
2600
multiprocessing_manager = multiprocessing.Manager()
2602
client_class = Client
2604
client_class = functools.partial(ClientDBus, bus = bus)
2606
client_settings = Client.config_parser(client_config)
2607
old_client_settings = {}
2610
# This is used to redirect stdout and stderr for checker processes
2612
wnull = open(os.devnull, "w") # A writable /dev/null
2613
# Only used if server is running in foreground but not in debug
2615
if debug or not foreground:
2618
# Get client data and settings from last running state.
2619
if server_settings["restore"]:
2621
with open(stored_state_path, "rb") as stored_state:
2622
clients_data, old_client_settings = pickle.load(
2624
os.remove(stored_state_path)
2625
except IOError as e:
2626
if e.errno == errno.ENOENT:
2627
logger.warning("Could not load persistent state:"
2628
" {}".format(os.strerror(e.errno)))
2630
logger.critical("Could not load persistent state:",
2633
except EOFError as e:
2634
logger.warning("Could not load persistent state: "
2638
with PGPEngine() as pgp:
2639
for client_name, client in clients_data.items():
2640
# Skip removed clients
2641
if client_name not in client_settings:
2644
# Decide which value to use after restoring saved state.
2645
# We have three different values: Old config file,
2646
# new config file, and saved state.
2647
# New config value takes precedence if it differs from old
2648
# config value, otherwise use saved state.
2649
for name, value in client_settings[client_name].items():
2651
# For each value in new config, check if it
2652
# differs from the old config value (Except for
2653
# the "secret" attribute)
2654
if (name != "secret"
2656
old_client_settings[client_name][name])):
2657
client[name] = value
2661
# Clients who has passed its expire date can still be
2662
# enabled if its last checker was successful. Clients
2663
# whose checker succeeded before we stored its state is
2664
# assumed to have successfully run all checkers during
2666
if client["enabled"]:
2667
if datetime.datetime.utcnow() >= client["expires"]:
2668
if not client["last_checked_ok"]:
2670
"disabling client {} - Client never "
2671
"performed a successful checker".format(
2673
client["enabled"] = False
2674
elif client["last_checker_status"] != 0:
2676
"disabling client {} - Client last"
2677
" checker failed with error code"
2680
client["last_checker_status"]))
2681
client["enabled"] = False
2683
client["expires"] = (
2684
datetime.datetime.utcnow()
2685
+ client["timeout"])
2686
logger.debug("Last checker succeeded,"
2687
" keeping {} enabled".format(
2690
client["secret"] = pgp.decrypt(
2691
client["encrypted_secret"],
2692
client_settings[client_name]["secret"])
2694
# If decryption fails, we use secret from new settings
2695
logger.debug("Failed to decrypt {} old secret".format(
2697
client["secret"] = (client_settings[client_name]
2700
# Add/remove clients based on new changes made to config
2701
for client_name in (set(old_client_settings)
2702
- set(client_settings)):
2703
del clients_data[client_name]
2704
for client_name in (set(client_settings)
2705
- set(old_client_settings)):
2706
clients_data[client_name] = client_settings[client_name]
2708
# Create all client objects
2709
for client_name, client in clients_data.items():
2710
tcp_server.clients[client_name] = client_class(
2713
server_settings = server_settings)
2715
if not tcp_server.clients:
2716
logger.warning("No clients defined")
2719
if pidfile is not None:
2723
print(pid, file=pidfile)
2725
logger.error("Could not write to file %r with PID %d",
761
def remove_from_clients(client):
762
clients.remove(client)
764
logger.critical(u"No clients left, exiting")
767
clients.update(Set(Client(name = section,
768
stop_hook = remove_from_clients,
770
= dict(client_config.items(section)))
771
for section in client_config.sections()))
773
logger.critical(u"No clients defined")
777
logger.removeHandler(console)
780
pidfilename = "/var/run/mandos/mandos.pid"
783
pidfile = open(pidfilename, "w")
784
pidfile.write(str(pid) + "\n")
2730
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2731
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2735
@alternate_dbus_interfaces(
2736
{ "se.recompile.Mandos": "se.bsnet.fukt.Mandos" })
2737
class MandosDBusService(DBusObjectWithProperties):
2738
"""A D-Bus proxy object"""
2741
dbus.service.Object.__init__(self, bus, "/")
2743
_interface = "se.recompile.Mandos"
2745
@dbus_interface_annotations(_interface)
2748
"org.freedesktop.DBus.Property.EmitsChangedSignal":
2751
@dbus.service.signal(_interface, signature="o")
2752
def ClientAdded(self, objpath):
2756
@dbus.service.signal(_interface, signature="ss")
2757
def ClientNotFound(self, fingerprint, address):
2761
@dbus.service.signal(_interface, signature="os")
2762
def ClientRemoved(self, objpath, name):
2766
@dbus.service.method(_interface, out_signature="ao")
2767
def GetAllClients(self):
2769
return dbus.Array(c.dbus_object_path for c in
2770
tcp_server.clients.itervalues())
2772
@dbus.service.method(_interface,
2773
out_signature="a{oa{sv}}")
2774
def GetAllClientsWithProperties(self):
2776
return dbus.Dictionary(
2777
{ c.dbus_object_path: c.GetAll("")
2778
for c in tcp_server.clients.itervalues() },
2781
@dbus.service.method(_interface, in_signature="o")
2782
def RemoveClient(self, object_path):
2784
for c in tcp_server.clients.itervalues():
2785
if c.dbus_object_path == object_path:
2786
del tcp_server.clients[c.name]
2787
c.remove_from_connection()
2788
# Don't signal anything except ClientRemoved
2789
c.disable(quiet=True)
2791
self.ClientRemoved(object_path, c.name)
2793
raise KeyError(object_path)
2797
mandos_dbus_service = MandosDBusService()
788
logger.error(u"Could not write %s file with PID %d",
789
pidfilename, os.getpid())
2800
792
"Cleanup function; run on exit"
2804
multiprocessing.active_children()
2806
if not (tcp_server.clients or client_settings):
2809
# Store client before exiting. Secrets are encrypted with key
2810
# based on what config file has. If config file is
2811
# removed/edited, old secret will thus be unrecovable.
2813
with PGPEngine() as pgp:
2814
for client in tcp_server.clients.itervalues():
2815
key = client_settings[client.name]["secret"]
2816
client.encrypted_secret = pgp.encrypt(client.secret,
2820
# A list of attributes that can not be pickled
2822
exclude = { "bus", "changedstate", "secret",
2823
"checker", "server_settings" }
2824
for name, typ in inspect.getmembers(dbus.service
2828
client_dict["encrypted_secret"] = (client
2830
for attr in client.client_structure:
2831
if attr not in exclude:
2832
client_dict[attr] = getattr(client, attr)
2834
clients[client.name] = client_dict
2835
del client_settings[client.name]["secret"]
2838
with tempfile.NamedTemporaryFile(
2842
dir=os.path.dirname(stored_state_path),
2843
delete=False) as stored_state:
2844
pickle.dump((clients, client_settings), stored_state)
2845
tempname = stored_state.name
2846
os.rename(tempname, stored_state_path)
2847
except (IOError, OSError) as e:
2853
if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
2854
logger.warning("Could not save persistent state: {}"
2855
.format(os.strerror(e.errno)))
2857
logger.warning("Could not save persistent state:",
2861
# Delete all clients, and settings from config
2862
while tcp_server.clients:
2863
name, client = tcp_server.clients.popitem()
2865
client.remove_from_connection()
2866
# Don't signal anything except ClientRemoved
2867
client.disable(quiet=True)
2870
mandos_dbus_service.ClientRemoved(
2871
client.dbus_object_path, client.name)
2872
client_settings.clear()
794
# From the Avahi example code
795
if not group is None:
798
# End of Avahi example code
801
client = clients.pop()
802
client.stop_hook = None
2874
805
atexit.register(cleanup)
2876
for client in tcp_server.clients.itervalues():
2879
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2880
# Need to initiate checking of clients
2882
client.init_checker()
2885
tcp_server.server_activate()
808
signal.signal(signal.SIGINT, signal.SIG_IGN)
809
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
810
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
812
for client in clients:
815
tcp_server = IPv6_TCPServer((server_settings["address"],
816
server_settings["port"]),
818
settings=server_settings,
2887
820
# Find out what port we got
2889
service.port = tcp_server.socket.getsockname()[1]
2891
logger.info("Now listening on address %r, port %d,"
2892
" flowinfo %d, scope_id %d",
2893
*tcp_server.socket.getsockname())
2895
logger.info("Now listening on address %r, port %d",
2896
*tcp_server.socket.getsockname())
821
service.port = tcp_server.socket.getsockname()[1]
822
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
823
u" scope_id %d" % tcp_server.socket.getsockname())
2898
825
#service.interface = tcp_server.socket.getsockname()[3]
2902
# From the Avahi example code
2905
except dbus.exceptions.DBusException as error:
2906
logger.critical("D-Bus Exception", exc_info=error)
2909
# End of Avahi example code
828
# From the Avahi example code
829
server.connect_to_signal("StateChanged", server_state_changed)
831
server_state_changed(server.GetState())
832
except dbus.exceptions.DBusException, error:
833
logger.critical(u"DBusException: %s", error)
835
# End of Avahi example code
2911
837
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2912
838
lambda *args, **kwargs:
2913
(tcp_server.handle_request
2914
(*args[2:], **kwargs) or True))
839
tcp_server.handle_request\
840
(*args[2:], **kwargs) or True)
2916
logger.debug("Starting main loop")
842
logger.debug(u"Starting main loop")
843
main_loop_started = True
2918
except AvahiError as error:
2919
logger.critical("Avahi Error", exc_info=error)
845
except AvahiError, error:
846
logger.critical(u"AvahiError: %s" + unicode(error))
2922
848
except KeyboardInterrupt:
2924
print("", file=sys.stderr)
2925
logger.debug("Server received KeyboardInterrupt")
2926
logger.debug("Server exiting")
2927
# Must run before the D-Bus bus name gets deregistered
2931
852
if __name__ == '__main__':