270
124
self.rename_count = 0
271
125
self.max_renames = max_renames
272
self.protocol = protocol
273
self.group = None # our entry group
276
self.entry_group_state_changed_match = None
278
126
def rename(self):
279
127
"""Derived from the Avahi example code"""
280
128
if self.rename_count >= self.max_renames:
281
logger.critical("No suitable Zeroconf service name found"
282
" after %i retries, exiting.",
129
logger.critical(u"No suitable Zeroconf service name found"
130
u" after %i retries, exiting.",
283
131
self.rename_count)
284
raise AvahiServiceError("Too many renames")
285
self.name = unicode(self.server
286
.GetAlternativeServiceName(self.name))
287
logger.info("Changing Zeroconf service name to %r ...",
132
raise AvahiServiceError(u"Too many renames")
133
self.name = server.GetAlternativeServiceName(self.name)
134
logger.info(u"Changing Zeroconf service name to %r ...",
136
syslogger.setFormatter(logging.Formatter
137
('Mandos (%s): %%(levelname)s:'
138
' %%(message)s' % self.name))
292
except dbus.exceptions.DBusException as error:
293
logger.critical("D-Bus Exception", exc_info=error)
296
141
self.rename_count += 1
298
142
def remove(self):
299
143
"""Derived from the Avahi example code"""
300
if self.entry_group_state_changed_match is not None:
301
self.entry_group_state_changed_match.remove()
302
self.entry_group_state_changed_match = None
303
if self.group is not None:
144
if group is not None:
307
147
"""Derived from the Avahi example code"""
309
if self.group is None:
310
self.group = dbus.Interface(
311
self.bus.get_object(avahi.DBUS_NAME,
312
self.server.EntryGroupNew()),
313
avahi.DBUS_INTERFACE_ENTRY_GROUP)
314
self.entry_group_state_changed_match = (
315
self.group.connect_to_signal(
316
'StateChanged', self.entry_group_state_changed))
317
logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
318
self.name, self.type)
319
self.group.AddService(
322
dbus.UInt32(0), # flags
323
self.name, self.type,
324
self.domain, self.host,
325
dbus.UInt16(self.port),
326
avahi.string_array_to_txt_array(self.TXT))
329
def entry_group_state_changed(self, state, error):
330
"""Derived from the Avahi example code"""
331
logger.debug("Avahi entry group state change: %i", state)
333
if state == avahi.ENTRY_GROUP_ESTABLISHED:
334
logger.debug("Zeroconf service established.")
335
elif state == avahi.ENTRY_GROUP_COLLISION:
336
logger.info("Zeroconf service name collision.")
338
elif state == avahi.ENTRY_GROUP_FAILURE:
339
logger.critical("Avahi: Error in group state changed %s",
341
raise AvahiGroupError("State changed: {!s}"
345
"""Derived from the Avahi example code"""
346
if self.group is not None:
349
except (dbus.exceptions.UnknownMethodException,
350
dbus.exceptions.DBusException):
355
def server_state_changed(self, state, error=None):
356
"""Derived from the Avahi example code"""
357
logger.debug("Avahi server state change: %i", state)
358
bad_states = { avahi.SERVER_INVALID:
359
"Zeroconf server invalid",
360
avahi.SERVER_REGISTERING: None,
361
avahi.SERVER_COLLISION:
362
"Zeroconf server name collision",
363
avahi.SERVER_FAILURE:
364
"Zeroconf server failure" }
365
if state in bad_states:
366
if bad_states[state] is not None:
368
logger.error(bad_states[state])
370
logger.error(bad_states[state] + ": %r", error)
372
elif state == avahi.SERVER_RUNNING:
376
logger.debug("Unknown state: %r", state)
378
logger.debug("Unknown state: %r: %r", state, error)
381
"""Derived from the Avahi example code"""
382
if self.server is None:
383
self.server = dbus.Interface(
384
self.bus.get_object(avahi.DBUS_NAME,
385
avahi.DBUS_PATH_SERVER,
386
follow_name_owner_changes=True),
387
avahi.DBUS_INTERFACE_SERVER)
388
self.server.connect_to_signal("StateChanged",
389
self.server_state_changed)
390
self.server_state_changed(self.server.GetState())
393
class AvahiServiceToSyslog(AvahiService):
395
"""Add the new name to the syslog messages"""
396
ret = AvahiService.rename(self)
397
syslogger.setFormatter(logging.Formatter
398
('Mandos ({}) [%(process)d]:'
399
' %(levelname)s: %(message)s'
404
class Client(object):
150
group = dbus.Interface(bus.get_object
152
server.EntryGroupNew()),
153
avahi.DBUS_INTERFACE_ENTRY_GROUP)
154
group.connect_to_signal('StateChanged',
155
entry_group_state_changed)
156
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
157
service.name, service.type)
159
self.interface, # interface
160
avahi.PROTO_INET6, # protocol
161
dbus.UInt32(0), # flags
162
self.name, self.type,
163
self.domain, self.host,
164
dbus.UInt16(self.port),
165
avahi.string_array_to_txt_array(self.TXT))
168
# From the Avahi example code:
169
group = None # our entry group
170
# End of Avahi example code
173
def _datetime_to_dbus(dt, variant_level=0):
174
"""Convert a UTC datetime.datetime() to a D-Bus type."""
175
return dbus.String(dt.isoformat(), variant_level=variant_level)
178
class Client(dbus.service.Object):
405
179
"""A representation of a client host served by this server.
408
approved: bool(); 'None' if not yet approved/disapproved
409
approval_delay: datetime.timedelta(); Time to wait for approval
410
approval_duration: datetime.timedelta(); Duration of one approval
181
name: string; from the config file, used in log messages
182
fingerprint: string (40 or 32 hexadecimal digits); used to
183
uniquely identify the client
184
secret: bytestring; sent verbatim (over TLS) to client
185
host: string; available for use by the checker command
186
created: datetime.datetime(); (UTC) object creation
187
last_enabled: datetime.datetime(); (UTC)
189
last_checked_ok: datetime.datetime(); (UTC) or None
190
timeout: datetime.timedelta(); How long from last_checked_ok
191
until this client is invalid
192
interval: datetime.timedelta(); How often to start a new checker
193
disable_hook: If set, called by disable() as disable_hook(self)
411
194
checker: subprocess.Popen(); a running checker process used
412
195
to see if the client lives.
413
196
'None' if no process is running.
414
checker_callback_tag: a gobject event source tag, or None
415
checker_command: string; External command which is run to check
416
if client lives. %() expansions are done at
197
checker_initiator_tag: a gobject event source tag, or None
198
disable_initiator_tag: - '' -
199
checker_callback_tag: - '' -
200
checker_command: string; External command which is run to check if
201
client lives. %() expansions are done at
417
202
runtime with vars(self) as dict, so that for
418
203
instance %(name)s can be used in the command.
419
checker_initiator_tag: a gobject event source tag, or None
420
created: datetime.datetime(); (UTC) object creation
421
client_structure: Object describing what attributes a client has
422
and is used for storing the client at exit
423
current_checker_command: string; current running checker_command
424
disable_initiator_tag: a gobject event source tag, or None
426
fingerprint: string (40 or 32 hexadecimal digits); used to
427
uniquely identify the client
428
host: string; available for use by the checker command
429
interval: datetime.timedelta(); How often to start a new checker
430
last_approval_request: datetime.datetime(); (UTC) or None
431
last_checked_ok: datetime.datetime(); (UTC) or None
432
last_checker_status: integer between 0 and 255 reflecting exit
433
status of last checker. -1 reflects crashed
434
checker, -2 means no checker completed yet.
435
last_enabled: datetime.datetime(); (UTC) or None
436
name: string; from the config file, used in log messages and
438
secret: bytestring; sent verbatim (over TLS) to client
439
timeout: datetime.timedelta(); How long from last_checked_ok
440
until this client is disabled
441
extended_timeout: extra long timeout when secret has been sent
442
runtime_expansions: Allowed attributes for runtime expansion.
443
expires: datetime.datetime(); time (UTC) when a client will be
445
server_settings: The server_settings dict from main()
204
use_dbus: bool(); Whether to provide D-Bus interface and signals
205
dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
448
runtime_expansions = ("approval_delay", "approval_duration",
449
"created", "enabled", "expires",
450
"fingerprint", "host", "interval",
451
"last_approval_request", "last_checked_ok",
452
"last_enabled", "name", "timeout")
453
client_defaults = { "timeout": "PT5M",
454
"extended_timeout": "PT15M",
456
"checker": "fping -q -- %%(host)s",
458
"approval_delay": "PT0S",
459
"approval_duration": "PT1S",
460
"approved_by_default": "True",
465
def config_parser(config):
466
"""Construct a new dict of client settings of this form:
467
{ client_name: {setting_name: value, ...}, ...}
468
with exceptions for any special settings as defined above.
469
NOTE: Must be a pure function. Must return the same result
470
value given the same arguments.
473
for client_name in config.sections():
474
section = dict(config.items(client_name))
475
client = settings[client_name] = {}
477
client["host"] = section["host"]
478
# Reformat values from string types to Python types
479
client["approved_by_default"] = config.getboolean(
480
client_name, "approved_by_default")
481
client["enabled"] = config.getboolean(client_name,
484
client["fingerprint"] = (section["fingerprint"].upper()
486
if "secret" in section:
487
client["secret"] = section["secret"].decode("base64")
488
elif "secfile" in section:
489
with open(os.path.expanduser(os.path.expandvars
490
(section["secfile"])),
492
client["secret"] = secfile.read()
494
raise TypeError("No secret or secfile for section {}"
496
client["timeout"] = string_to_delta(section["timeout"])
497
client["extended_timeout"] = string_to_delta(
498
section["extended_timeout"])
499
client["interval"] = string_to_delta(section["interval"])
500
client["approval_delay"] = string_to_delta(
501
section["approval_delay"])
502
client["approval_duration"] = string_to_delta(
503
section["approval_duration"])
504
client["checker_command"] = section["checker"]
505
client["last_approval_request"] = None
506
client["last_checked_ok"] = None
507
client["last_checker_status"] = -2
511
def __init__(self, settings, name = None, server_settings=None):
207
def timeout_milliseconds(self):
208
"Return the 'timeout' attribute in milliseconds"
209
return ((self.timeout.days * 24 * 60 * 60 * 1000)
210
+ (self.timeout.seconds * 1000)
211
+ (self.timeout.microseconds // 1000))
213
def interval_milliseconds(self):
214
"Return the 'interval' attribute in milliseconds"
215
return ((self.interval.days * 24 * 60 * 60 * 1000)
216
+ (self.interval.seconds * 1000)
217
+ (self.interval.microseconds // 1000))
219
def __init__(self, name = None, disable_hook=None, config=None,
221
"""Note: the 'checker' key in 'config' sets the
222
'checker_command' attribute and *not* the 'checker'
513
if server_settings is None:
515
self.server_settings = server_settings
516
# adding all client settings
517
for setting, value in settings.items():
518
setattr(self, setting, value)
521
if not hasattr(self, "last_enabled"):
522
self.last_enabled = datetime.datetime.utcnow()
523
if not hasattr(self, "expires"):
524
self.expires = (datetime.datetime.utcnow()
527
self.last_enabled = None
530
logger.debug("Creating client %r", self.name)
227
logger.debug(u"Creating client %r", self.name)
228
self.use_dbus = use_dbus
230
self.dbus_object_path = (dbus.ObjectPath
232
+ self.name.replace(".", "_")))
233
dbus.service.Object.__init__(self, bus,
234
self.dbus_object_path)
531
235
# Uppercase and remove spaces from fingerprint for later
532
236
# comparison purposes with return value from the fingerprint()
534
logger.debug(" Fingerprint: %s", self.fingerprint)
535
self.created = settings.get("created",
536
datetime.datetime.utcnow())
538
# attributes specific for this server instance
238
self.fingerprint = (config["fingerprint"].upper()
240
logger.debug(u" Fingerprint: %s", self.fingerprint)
241
if "secret" in config:
242
self.secret = config["secret"].decode(u"base64")
243
elif "secfile" in config:
244
with closing(open(os.path.expanduser
246
(config["secfile"])))) as secfile:
247
self.secret = secfile.read()
249
raise TypeError(u"No secret or secfile for client %s"
251
self.host = config.get("host", "")
252
self.created = datetime.datetime.utcnow()
254
self.last_enabled = None
255
self.last_checked_ok = None
256
self.timeout = string_to_delta(config["timeout"])
257
self.interval = string_to_delta(config["interval"])
258
self.disable_hook = disable_hook
539
259
self.checker = None
540
260
self.checker_initiator_tag = None
541
261
self.disable_initiator_tag = None
542
262
self.checker_callback_tag = None
543
self.current_checker_command = None
545
self.approvals_pending = 0
546
self.changedstate = (multiprocessing_manager
547
.Condition(multiprocessing_manager
549
self.client_structure = [attr for attr in
550
self.__dict__.iterkeys()
551
if not attr.startswith("_")]
552
self.client_structure.append("client_structure")
554
for name, t in inspect.getmembers(type(self),
558
if not name.startswith("_"):
559
self.client_structure.append(name)
561
# Send notice to process children that client state has changed
562
def send_changedstate(self):
563
with self.changedstate:
564
self.changedstate.notify_all()
263
self.checker_command = config["checker"]
566
265
def enable(self):
567
266
"""Start this client's checker and timeout hooks"""
568
if getattr(self, "enabled", False):
571
self.expires = datetime.datetime.utcnow() + self.timeout
573
267
self.last_enabled = datetime.datetime.utcnow()
575
self.send_changedstate()
577
def disable(self, quiet=True):
578
"""Disable this client."""
579
if not getattr(self, "enabled", False):
582
logger.info("Disabling client %s", self.name)
583
if getattr(self, "disable_initiator_tag", None) is not None:
584
gobject.source_remove(self.disable_initiator_tag)
585
self.disable_initiator_tag = None
587
if getattr(self, "checker_initiator_tag", None) is not None:
588
gobject.source_remove(self.checker_initiator_tag)
589
self.checker_initiator_tag = None
593
self.send_changedstate()
594
# Do not run this again if called by a gobject.timeout_add
600
def init_checker(self):
601
268
# Schedule a new checker to be started an 'interval' from now,
602
269
# and every interval from then on.
603
if self.checker_initiator_tag is not None:
604
gobject.source_remove(self.checker_initiator_tag)
605
270
self.checker_initiator_tag = (gobject.timeout_add
607
.total_seconds() * 1000),
271
(self.interval_milliseconds(),
608
272
self.start_checker))
273
# Also start a new checker *right now*.
609
275
# Schedule a disable() when 'timeout' has passed
610
if self.disable_initiator_tag is not None:
276
self.disable_initiator_tag = (gobject.timeout_add
277
(self.timeout_milliseconds(),
282
self.PropertyChanged(dbus.String(u"enabled"),
283
dbus.Boolean(True, variant_level=1))
284
self.PropertyChanged(dbus.String(u"last_enabled"),
285
(_datetime_to_dbus(self.last_enabled,
289
"""Disable this client."""
290
if not getattr(self, "enabled", False):
292
logger.info(u"Disabling client %s", self.name)
293
if getattr(self, "disable_initiator_tag", False):
611
294
gobject.source_remove(self.disable_initiator_tag)
612
self.disable_initiator_tag = (gobject.timeout_add
614
.total_seconds() * 1000),
616
# Also start a new checker *right now*.
295
self.disable_initiator_tag = None
296
if getattr(self, "checker_initiator_tag", False):
297
gobject.source_remove(self.checker_initiator_tag)
298
self.checker_initiator_tag = None
300
if self.disable_hook:
301
self.disable_hook(self)
305
self.PropertyChanged(dbus.String(u"enabled"),
306
dbus.Boolean(False, variant_level=1))
307
# Do not run this again if called by a gobject.timeout_add
311
self.disable_hook = None
619
314
def checker_callback(self, pid, condition, command):
620
315
"""The checker has completed, so take appropriate actions."""
621
316
self.checker_callback_tag = None
622
317
self.checker = None
623
if os.WIFEXITED(condition):
624
self.last_checker_status = os.WEXITSTATUS(condition)
625
if self.last_checker_status == 0:
626
logger.info("Checker for %(name)s succeeded",
630
logger.info("Checker for %(name)s failed",
633
self.last_checker_status = -1
634
logger.warning("Checker for %(name)s crashed?",
320
self.PropertyChanged(dbus.String(u"checker_running"),
321
dbus.Boolean(False, variant_level=1))
322
if (os.WIFEXITED(condition)
323
and (os.WEXITSTATUS(condition) == 0)):
324
logger.info(u"Checker for %(name)s succeeded",
328
self.CheckerCompleted(dbus.Boolean(True),
329
dbus.UInt16(condition),
330
dbus.String(command))
332
elif not os.WIFEXITED(condition):
333
logger.warning(u"Checker for %(name)s crashed?",
337
self.CheckerCompleted(dbus.Boolean(False),
338
dbus.UInt16(condition),
339
dbus.String(command))
341
logger.info(u"Checker for %(name)s failed",
345
self.CheckerCompleted(dbus.Boolean(False),
346
dbus.UInt16(condition),
347
dbus.String(command))
637
def checked_ok(self):
638
"""Assert that the client has been seen, alive and well."""
349
def bump_timeout(self):
350
"""Bump up the timeout for this client.
351
This should only be called when the client has been seen,
639
354
self.last_checked_ok = datetime.datetime.utcnow()
640
self.last_checker_status = 0
643
def bump_timeout(self, timeout=None):
644
"""Bump up the timeout for this client."""
646
timeout = self.timeout
647
if self.disable_initiator_tag is not None:
648
gobject.source_remove(self.disable_initiator_tag)
649
self.disable_initiator_tag = None
650
if getattr(self, "enabled", False):
651
self.disable_initiator_tag = (gobject.timeout_add
652
(int(timeout.total_seconds()
653
* 1000), self.disable))
654
self.expires = datetime.datetime.utcnow() + timeout
656
def need_approval(self):
657
self.last_approval_request = datetime.datetime.utcnow()
355
gobject.source_remove(self.disable_initiator_tag)
356
self.disable_initiator_tag = (gobject.timeout_add
357
(self.timeout_milliseconds(),
361
self.PropertyChanged(
362
dbus.String(u"last_checked_ok"),
363
(_datetime_to_dbus(self.last_checked_ok,
659
366
def start_checker(self):
660
367
"""Start a new checker subprocess if one is not running.
662
368
If a checker already exists, leave it running and do
664
370
# The reason for not killing a running checker is that if we
665
# did that, and if a checker (for some reason) started running
666
# slowly and taking more than 'interval' time, then the client
667
# would inevitably timeout, since no checker would get a
668
# chance to run to completion. If we instead leave running
371
# did that, then if a checker (for some reason) started
372
# running slowly and taking more than 'interval' time, the
373
# client would inevitably timeout, since no checker would get
374
# a chance to run to completion. If we instead leave running
669
375
# checkers alone, the checker would have to take more time
670
# than 'timeout' for the client to be disabled, which is as it
673
# If a checker exists, make sure it is not a zombie
675
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
676
except AttributeError:
678
except OSError as error:
679
if error.errno != errno.ECHILD:
683
logger.warning("Checker was a zombie")
684
gobject.source_remove(self.checker_callback_tag)
685
self.checker_callback(pid, status,
686
self.current_checker_command)
687
# Start a new checker if needed
376
# than 'timeout' for the client to be declared invalid, which
377
# is as it should be.
688
378
if self.checker is None:
689
# Escape attributes for the shell
690
escaped_attrs = { attr:
691
re.escape(unicode(getattr(self,
693
for attr in self.runtime_expansions }
695
command = self.checker_command % escaped_attrs
696
except TypeError as error:
697
logger.error('Could not format string "%s"',
698
self.checker_command, exc_info=error)
699
return True # Try again later
700
self.current_checker_command = command
702
logger.info("Starting checker %r for %s",
380
# In case checker_command has exactly one % operator
381
command = self.checker_command % self.host
383
# Escape attributes for the shell
384
escaped_attrs = dict((key, re.escape(str(val)))
386
vars(self).iteritems())
388
command = self.checker_command % escaped_attrs
389
except TypeError, error:
390
logger.error(u'Could not format string "%s":'
391
u' %s', self.checker_command, error)
392
return True # Try again later
394
logger.info(u"Starting checker %r for %s",
703
395
command, self.name)
704
396
# We don't need to redirect stdout and stderr, since
705
397
# in normal mode, that is already done by daemon(),
706
398
# and in debug mode we don't want to. (Stdin is
707
399
# always replaced by /dev/null.)
708
# The exception is when not debugging but nevertheless
709
# running in the foreground; use the previously
712
if (not self.server_settings["debug"]
713
and self.server_settings["foreground"]):
714
popen_args.update({"stdout": wnull,
716
400
self.checker = subprocess.Popen(command,
720
except OSError as error:
721
logger.error("Failed to start subprocess",
724
self.checker_callback_tag = (gobject.child_watch_add
726
self.checker_callback,
728
# The checker may have completed before the gobject
729
# watch was added. Check for this.
731
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
732
except OSError as error:
733
if error.errno == errno.ECHILD:
734
# This should never happen
735
logger.error("Child process vanished",
740
gobject.source_remove(self.checker_callback_tag)
741
self.checker_callback(pid, status, command)
405
self.CheckerStarted(command)
406
self.PropertyChanged(
407
dbus.String("checker_running"),
408
dbus.Boolean(True, variant_level=1))
409
self.checker_callback_tag = (gobject.child_watch_add
411
self.checker_callback,
413
except OSError, error:
414
logger.error(u"Failed to start subprocess: %s",
742
416
# Re-run this periodically if run by gobject.timeout_add
749
423
self.checker_callback_tag = None
750
424
if getattr(self, "checker", None) is None:
752
logger.debug("Stopping checker for %(name)s", vars(self))
426
logger.debug(u"Stopping checker for %(name)s", vars(self))
754
self.checker.terminate()
428
os.kill(self.checker.pid, signal.SIGTERM)
756
430
#if self.checker.poll() is None:
757
# self.checker.kill()
758
except OSError as error:
431
# os.kill(self.checker.pid, signal.SIGKILL)
432
except OSError, error:
759
433
if error.errno != errno.ESRCH: # No such process
761
435
self.checker = None
764
def dbus_service_property(dbus_interface, signature="v",
765
access="readwrite", byte_arrays=False):
766
"""Decorators for marking methods of a DBusObjectWithProperties to
767
become properties on the D-Bus.
769
The decorated method will be called with no arguments by "Get"
770
and with one argument by "Set".
772
The parameters, where they are supported, are the same as
773
dbus.service.method, except there is only "signature", since the
774
type from Get() and the type sent to Set() is the same.
776
# Encoding deeply encoded byte arrays is not supported yet by the
777
# "Set" method, so we fail early here:
778
if byte_arrays and signature != "ay":
779
raise ValueError("Byte arrays not supported for non-'ay'"
780
" signature {!r}".format(signature))
782
func._dbus_is_property = True
783
func._dbus_interface = dbus_interface
784
func._dbus_signature = signature
785
func._dbus_access = access
786
func._dbus_name = func.__name__
787
if func._dbus_name.endswith("_dbus_property"):
788
func._dbus_name = func._dbus_name[:-14]
789
func._dbus_get_args_options = {'byte_arrays': byte_arrays }
794
def dbus_interface_annotations(dbus_interface):
795
"""Decorator for marking functions returning interface annotations
799
@dbus_interface_annotations("org.example.Interface")
800
def _foo(self): # Function name does not matter
801
return {"org.freedesktop.DBus.Deprecated": "true",
802
"org.freedesktop.DBus.Property.EmitsChangedSignal":
806
func._dbus_is_interface = True
807
func._dbus_interface = dbus_interface
808
func._dbus_name = dbus_interface
813
def dbus_annotations(annotations):
814
"""Decorator to annotate D-Bus methods, signals or properties
817
@dbus_service_property("org.example.Interface", signature="b",
819
@dbus_annotations({{"org.freedesktop.DBus.Deprecated": "true",
820
"org.freedesktop.DBus.Property."
821
"EmitsChangedSignal": "false"})
822
def Property_dbus_property(self):
823
return dbus.Boolean(False)
826
func._dbus_annotations = annotations
831
class DBusPropertyException(dbus.exceptions.DBusException):
832
"""A base class for D-Bus property-related exceptions
834
def __unicode__(self):
835
return unicode(str(self))
838
class DBusPropertyAccessException(DBusPropertyException):
839
"""A property's access permissions disallows an operation.
844
class DBusPropertyNotFound(DBusPropertyException):
845
"""An attempt was made to access a non-existing property.
850
class DBusObjectWithProperties(dbus.service.Object):
851
"""A D-Bus object with properties.
853
Classes inheriting from this can use the dbus_service_property
854
decorator to expose methods as D-Bus properties. It exposes the
855
standard Get(), Set(), and GetAll() methods on the D-Bus.
859
def _is_dbus_thing(thing):
860
"""Returns a function testing if an attribute is a D-Bus thing
862
If called like _is_dbus_thing("method") it returns a function
863
suitable for use as predicate to inspect.getmembers().
865
return lambda obj: getattr(obj, "_dbus_is_{}".format(thing),
868
def _get_all_dbus_things(self, thing):
869
"""Returns a generator of (name, attribute) pairs
871
return ((getattr(athing.__get__(self), "_dbus_name",
873
athing.__get__(self))
874
for cls in self.__class__.__mro__
876
inspect.getmembers(cls,
877
self._is_dbus_thing(thing)))
879
def _get_dbus_property(self, interface_name, property_name):
880
"""Returns a bound method if one exists which is a D-Bus
881
property with the specified name and interface.
883
for cls in self.__class__.__mro__:
884
for name, value in (inspect.getmembers
886
self._is_dbus_thing("property"))):
887
if (value._dbus_name == property_name
888
and value._dbus_interface == interface_name):
889
return value.__get__(self)
892
raise DBusPropertyNotFound(self.dbus_object_path + ":"
893
+ interface_name + "."
896
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ss",
898
def Get(self, interface_name, property_name):
899
"""Standard D-Bus property Get() method, see D-Bus standard.
901
prop = self._get_dbus_property(interface_name, property_name)
902
if prop._dbus_access == "write":
903
raise DBusPropertyAccessException(property_name)
905
if not hasattr(value, "variant_level"):
907
return type(value)(value, variant_level=value.variant_level+1)
909
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
910
def Set(self, interface_name, property_name, value):
911
"""Standard D-Bus property Set() method, see D-Bus standard.
913
prop = self._get_dbus_property(interface_name, property_name)
914
if prop._dbus_access == "read":
915
raise DBusPropertyAccessException(property_name)
916
if prop._dbus_get_args_options["byte_arrays"]:
917
# The byte_arrays option is not supported yet on
918
# signatures other than "ay".
919
if prop._dbus_signature != "ay":
920
raise ValueError("Byte arrays not supported for non-"
921
"'ay' signature {!r}"
922
.format(prop._dbus_signature))
923
value = dbus.ByteArray(b''.join(chr(byte)
927
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s",
928
out_signature="a{sv}")
929
def GetAll(self, interface_name):
930
"""Standard D-Bus property GetAll() method, see D-Bus
933
Note: Will not include properties with access="write".
936
for name, prop in self._get_all_dbus_things("property"):
938
and interface_name != prop._dbus_interface):
939
# Interface non-empty but did not match
941
# Ignore write-only properties
942
if prop._dbus_access == "write":
945
if not hasattr(value, "variant_level"):
946
properties[name] = value
948
properties[name] = type(value)(value, variant_level=
949
value.variant_level+1)
950
return dbus.Dictionary(properties, signature="sv")
952
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
954
path_keyword='object_path',
955
connection_keyword='connection')
956
def Introspect(self, object_path, connection):
957
"""Overloading of standard D-Bus method.
959
Inserts property tags and interface annotation tags.
961
xmlstring = dbus.service.Object.Introspect(self, object_path,
964
document = xml.dom.minidom.parseString(xmlstring)
965
def make_tag(document, name, prop):
966
e = document.createElement("property")
967
e.setAttribute("name", name)
968
e.setAttribute("type", prop._dbus_signature)
969
e.setAttribute("access", prop._dbus_access)
971
for if_tag in document.getElementsByTagName("interface"):
973
for tag in (make_tag(document, name, prop)
975
in self._get_all_dbus_things("property")
976
if prop._dbus_interface
977
== if_tag.getAttribute("name")):
978
if_tag.appendChild(tag)
979
# Add annotation tags
980
for typ in ("method", "signal", "property"):
981
for tag in if_tag.getElementsByTagName(typ):
983
for name, prop in (self.
984
_get_all_dbus_things(typ)):
985
if (name == tag.getAttribute("name")
986
and prop._dbus_interface
987
== if_tag.getAttribute("name")):
988
annots.update(getattr
992
for name, value in annots.items():
993
ann_tag = document.createElement(
995
ann_tag.setAttribute("name", name)
996
ann_tag.setAttribute("value", value)
997
tag.appendChild(ann_tag)
998
# Add interface annotation tags
999
for annotation, value in dict(
1000
itertools.chain.from_iterable(
1001
annotations().items()
1002
for name, annotations in
1003
self._get_all_dbus_things("interface")
1004
if name == if_tag.getAttribute("name")
1006
ann_tag = document.createElement("annotation")
1007
ann_tag.setAttribute("name", annotation)
1008
ann_tag.setAttribute("value", value)
1009
if_tag.appendChild(ann_tag)
1010
# Add the names to the return values for the
1011
# "org.freedesktop.DBus.Properties" methods
1012
if (if_tag.getAttribute("name")
1013
== "org.freedesktop.DBus.Properties"):
1014
for cn in if_tag.getElementsByTagName("method"):
1015
if cn.getAttribute("name") == "Get":
1016
for arg in cn.getElementsByTagName("arg"):
1017
if (arg.getAttribute("direction")
1019
arg.setAttribute("name", "value")
1020
elif cn.getAttribute("name") == "GetAll":
1021
for arg in cn.getElementsByTagName("arg"):
1022
if (arg.getAttribute("direction")
1024
arg.setAttribute("name", "props")
1025
xmlstring = document.toxml("utf-8")
1027
except (AttributeError, xml.dom.DOMException,
1028
xml.parsers.expat.ExpatError) as error:
1029
logger.error("Failed to override Introspection method",
1034
def datetime_to_dbus(dt, variant_level=0):
1035
"""Convert a UTC datetime.datetime() to a D-Bus type."""
1037
return dbus.String("", variant_level = variant_level)
1038
return dbus.String(dt.isoformat(),
1039
variant_level=variant_level)
1042
def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
1043
"""A class decorator; applied to a subclass of
1044
dbus.service.Object, it will add alternate D-Bus attributes with
1045
interface names according to the "alt_interface_names" mapping.
1048
@alternate_dbus_interfaces({"org.example.Interface":
1049
"net.example.AlternateInterface"})
1050
class SampleDBusObject(dbus.service.Object):
1051
@dbus.service.method("org.example.Interface")
1052
def SampleDBusMethod():
1055
The above "SampleDBusMethod" on "SampleDBusObject" will be
1056
reachable via two interfaces: "org.example.Interface" and
1057
"net.example.AlternateInterface", the latter of which will have
1058
its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
1059
"true", unless "deprecate" is passed with a False value.
1061
This works for methods and signals, and also for D-Bus properties
1062
(from DBusObjectWithProperties) and interfaces (from the
1063
dbus_interface_annotations decorator).
1066
for orig_interface_name, alt_interface_name in (
1067
alt_interface_names.items()):
1069
interface_names = set()
1070
# Go though all attributes of the class
1071
for attrname, attribute in inspect.getmembers(cls):
1072
# Ignore non-D-Bus attributes, and D-Bus attributes
1073
# with the wrong interface name
1074
if (not hasattr(attribute, "_dbus_interface")
1075
or not attribute._dbus_interface
1076
.startswith(orig_interface_name)):
1078
# Create an alternate D-Bus interface name based on
1080
alt_interface = (attribute._dbus_interface
1081
.replace(orig_interface_name,
1082
alt_interface_name))
1083
interface_names.add(alt_interface)
1084
# Is this a D-Bus signal?
1085
if getattr(attribute, "_dbus_is_signal", False):
1086
# Extract the original non-method undecorated
1087
# function by black magic
1088
nonmethod_func = (dict(
1089
zip(attribute.func_code.co_freevars,
1090
attribute.__closure__))["func"]
1092
# Create a new, but exactly alike, function
1093
# object, and decorate it to be a new D-Bus signal
1094
# with the alternate D-Bus interface name
1095
new_function = (dbus.service.signal
1097
attribute._dbus_signature)
1098
(types.FunctionType(
1099
nonmethod_func.func_code,
1100
nonmethod_func.func_globals,
1101
nonmethod_func.func_name,
1102
nonmethod_func.func_defaults,
1103
nonmethod_func.func_closure)))
1104
# Copy annotations, if any
1106
new_function._dbus_annotations = (
1107
dict(attribute._dbus_annotations))
1108
except AttributeError:
1110
# Define a creator of a function to call both the
1111
# original and alternate functions, so both the
1112
# original and alternate signals gets sent when
1113
# the function is called
1114
def fixscope(func1, func2):
1115
"""This function is a scope container to pass
1116
func1 and func2 to the "call_both" function
1117
outside of its arguments"""
1118
def call_both(*args, **kwargs):
1119
"""This function will emit two D-Bus
1120
signals by calling func1 and func2"""
1121
func1(*args, **kwargs)
1122
func2(*args, **kwargs)
1124
# Create the "call_both" function and add it to
1126
attr[attrname] = fixscope(attribute, new_function)
1127
# Is this a D-Bus method?
1128
elif getattr(attribute, "_dbus_is_method", False):
1129
# Create a new, but exactly alike, function
1130
# object. Decorate it to be a new D-Bus method
1131
# with the alternate D-Bus interface name. Add it
1133
attr[attrname] = (dbus.service.method
1135
attribute._dbus_in_signature,
1136
attribute._dbus_out_signature)
1138
(attribute.func_code,
1139
attribute.func_globals,
1140
attribute.func_name,
1141
attribute.func_defaults,
1142
attribute.func_closure)))
1143
# Copy annotations, if any
1145
attr[attrname]._dbus_annotations = (
1146
dict(attribute._dbus_annotations))
1147
except AttributeError:
1149
# Is this a D-Bus property?
1150
elif getattr(attribute, "_dbus_is_property", False):
1151
# Create a new, but exactly alike, function
1152
# object, and decorate it to be a new D-Bus
1153
# property with the alternate D-Bus interface
1154
# name. Add it to the class.
1155
attr[attrname] = (dbus_service_property
1157
attribute._dbus_signature,
1158
attribute._dbus_access,
1160
._dbus_get_args_options
1163
(attribute.func_code,
1164
attribute.func_globals,
1165
attribute.func_name,
1166
attribute.func_defaults,
1167
attribute.func_closure)))
1168
# Copy annotations, if any
1170
attr[attrname]._dbus_annotations = (
1171
dict(attribute._dbus_annotations))
1172
except AttributeError:
1174
# Is this a D-Bus interface?
1175
elif getattr(attribute, "_dbus_is_interface", False):
1176
# Create a new, but exactly alike, function
1177
# object. Decorate it to be a new D-Bus interface
1178
# with the alternate D-Bus interface name. Add it
1180
attr[attrname] = (dbus_interface_annotations
1183
(attribute.func_code,
1184
attribute.func_globals,
1185
attribute.func_name,
1186
attribute.func_defaults,
1187
attribute.func_closure)))
1189
# Deprecate all alternate interfaces
1190
iname="_AlternateDBusNames_interface_annotation{}"
1191
for interface_name in interface_names:
1192
@dbus_interface_annotations(interface_name)
1194
return { "org.freedesktop.DBus.Deprecated":
1196
# Find an unused name
1197
for aname in (iname.format(i)
1198
for i in itertools.count()):
1199
if aname not in attr:
1203
# Replace the class with a new subclass of it with
1204
# methods, signals, etc. as created above.
1205
cls = type(b"{}Alternate".format(cls.__name__),
1211
@alternate_dbus_interfaces({"se.recompile.Mandos":
1212
"se.bsnet.fukt.Mandos"})
1213
class ClientDBus(Client, DBusObjectWithProperties):
1214
"""A Client class using D-Bus
1217
dbus_object_path: dbus.ObjectPath
1218
bus: dbus.SystemBus()
1221
runtime_expansions = (Client.runtime_expansions
1222
+ ("dbus_object_path",))
1224
# dbus.service.Object doesn't use super(), so we can't either.
1226
def __init__(self, bus = None, *args, **kwargs):
1228
Client.__init__(self, *args, **kwargs)
1229
# Only now, when this client is initialized, can it show up on
1231
client_object_name = unicode(self.name).translate(
1232
{ord("."): ord("_"),
1233
ord("-"): ord("_")})
1234
self.dbus_object_path = (dbus.ObjectPath
1235
("/clients/" + client_object_name))
1236
DBusObjectWithProperties.__init__(self, self.bus,
1237
self.dbus_object_path)
1239
def notifychangeproperty(transform_func,
1240
dbus_name, type_func=lambda x: x,
1242
""" Modify a variable so that it's a property which announces
1243
its changes to DBus.
1245
transform_fun: Function that takes a value and a variant_level
1246
and transforms it to a D-Bus type.
1247
dbus_name: D-Bus name of the variable
1248
type_func: Function that transform the value before sending it
1249
to the D-Bus. Default: no transform
1250
variant_level: D-Bus variant level. Default: 1
1252
attrname = "_{}".format(dbus_name)
1253
def setter(self, value):
1254
if hasattr(self, "dbus_object_path"):
1255
if (not hasattr(self, attrname) or
1256
type_func(getattr(self, attrname, None))
1257
!= type_func(value)):
1258
dbus_value = transform_func(type_func(value),
1261
self.PropertyChanged(dbus.String(dbus_name),
1263
setattr(self, attrname, value)
1265
return property(lambda self: getattr(self, attrname), setter)
1267
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1268
approvals_pending = notifychangeproperty(dbus.Boolean,
1271
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1272
last_enabled = notifychangeproperty(datetime_to_dbus,
1274
checker = notifychangeproperty(dbus.Boolean, "CheckerRunning",
1275
type_func = lambda checker:
1276
checker is not None)
1277
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1279
last_checker_status = notifychangeproperty(dbus.Int16,
1280
"LastCheckerStatus")
1281
last_approval_request = notifychangeproperty(
1282
datetime_to_dbus, "LastApprovalRequest")
1283
approved_by_default = notifychangeproperty(dbus.Boolean,
1284
"ApprovedByDefault")
1285
approval_delay = notifychangeproperty(dbus.UInt64,
1288
lambda td: td.total_seconds()
1290
approval_duration = notifychangeproperty(
1291
dbus.UInt64, "ApprovalDuration",
1292
type_func = lambda td: td.total_seconds() * 1000)
1293
host = notifychangeproperty(dbus.String, "Host")
1294
timeout = notifychangeproperty(dbus.UInt64, "Timeout",
1295
type_func = lambda td:
1296
td.total_seconds() * 1000)
1297
extended_timeout = notifychangeproperty(
1298
dbus.UInt64, "ExtendedTimeout",
1299
type_func = lambda td: td.total_seconds() * 1000)
1300
interval = notifychangeproperty(dbus.UInt64,
1303
lambda td: td.total_seconds()
1305
checker_command = notifychangeproperty(dbus.String, "Checker")
1307
del notifychangeproperty
1309
def __del__(self, *args, **kwargs):
1311
self.remove_from_connection()
1314
if hasattr(DBusObjectWithProperties, "__del__"):
1315
DBusObjectWithProperties.__del__(self, *args, **kwargs)
1316
Client.__del__(self, *args, **kwargs)
1318
def checker_callback(self, pid, condition, command,
1320
self.checker_callback_tag = None
1322
if os.WIFEXITED(condition):
1323
exitstatus = os.WEXITSTATUS(condition)
1325
self.CheckerCompleted(dbus.Int16(exitstatus),
1326
dbus.Int64(condition),
1327
dbus.String(command))
437
self.PropertyChanged(dbus.String(u"checker_running"),
438
dbus.Boolean(False, variant_level=1))
440
def still_valid(self):
441
"""Has the timeout not yet passed for this client?"""
442
if not getattr(self, "enabled", False):
444
now = datetime.datetime.utcnow()
445
if self.last_checked_ok is None:
446
return now < (self.created + self.timeout)
1330
self.CheckerCompleted(dbus.Int16(-1),
1331
dbus.Int64(condition),
1332
dbus.String(command))
1334
return Client.checker_callback(self, pid, condition, command,
1337
def start_checker(self, *args, **kwargs):
1338
old_checker_pid = getattr(self.checker, "pid", None)
1339
r = Client.start_checker(self, *args, **kwargs)
1340
# Only if new checker process was started
1341
if (self.checker is not None
1342
and old_checker_pid != self.checker.pid):
1344
self.CheckerStarted(self.current_checker_command)
1347
def _reset_approved(self):
1348
self.approved = None
1351
def approve(self, value=True):
1352
self.approved = value
1353
gobject.timeout_add(int(self.approval_duration.total_seconds()
1354
* 1000), self._reset_approved)
1355
self.send_changedstate()
1357
## D-Bus methods, signals & properties
1358
_interface = "se.recompile.Mandos.Client"
1362
@dbus_interface_annotations(_interface)
1364
return { "org.freedesktop.DBus.Property.EmitsChangedSignal":
448
return now < (self.last_checked_ok + self.timeout)
450
## D-Bus methods & signals
451
_interface = u"org.mandos_system.Mandos.Client"
453
# BumpTimeout - method
454
BumpTimeout = dbus.service.method(_interface)(bump_timeout)
455
BumpTimeout.__name__ = "BumpTimeout"
1369
457
# CheckerCompleted - signal
1370
@dbus.service.signal(_interface, signature="nxs")
1371
def CheckerCompleted(self, exitcode, waitstatus, command):
458
@dbus.service.signal(_interface, signature="bqs")
459
def CheckerCompleted(self, success, condition, command):
1438
579
# StopChecker - method
1439
@dbus.service.method(_interface)
1440
def StopChecker(self):
1445
# ApprovalPending - property
1446
@dbus_service_property(_interface, signature="b", access="read")
1447
def ApprovalPending_dbus_property(self):
1448
return dbus.Boolean(bool(self.approvals_pending))
1450
# ApprovedByDefault - property
1451
@dbus_service_property(_interface, signature="b",
1453
def ApprovedByDefault_dbus_property(self, value=None):
1454
if value is None: # get
1455
return dbus.Boolean(self.approved_by_default)
1456
self.approved_by_default = bool(value)
1458
# ApprovalDelay - property
1459
@dbus_service_property(_interface, signature="t",
1461
def ApprovalDelay_dbus_property(self, value=None):
1462
if value is None: # get
1463
return dbus.UInt64(self.approval_delay.total_seconds()
1465
self.approval_delay = datetime.timedelta(0, 0, 0, value)
1467
# ApprovalDuration - property
1468
@dbus_service_property(_interface, signature="t",
1470
def ApprovalDuration_dbus_property(self, value=None):
1471
if value is None: # get
1472
return dbus.UInt64(self.approval_duration.total_seconds()
1474
self.approval_duration = datetime.timedelta(0, 0, 0, value)
1477
@dbus_service_property(_interface, signature="s", access="read")
1478
def Name_dbus_property(self):
1479
return dbus.String(self.name)
1481
# Fingerprint - property
1482
@dbus_service_property(_interface, signature="s", access="read")
1483
def Fingerprint_dbus_property(self):
1484
return dbus.String(self.fingerprint)
1487
@dbus_service_property(_interface, signature="s",
1489
def Host_dbus_property(self, value=None):
1490
if value is None: # get
1491
return dbus.String(self.host)
1492
self.host = unicode(value)
1494
# Created - property
1495
@dbus_service_property(_interface, signature="s", access="read")
1496
def Created_dbus_property(self):
1497
return datetime_to_dbus(self.created)
1499
# LastEnabled - property
1500
@dbus_service_property(_interface, signature="s", access="read")
1501
def LastEnabled_dbus_property(self):
1502
return datetime_to_dbus(self.last_enabled)
1504
# Enabled - property
1505
@dbus_service_property(_interface, signature="b",
1507
def Enabled_dbus_property(self, value=None):
1508
if value is None: # get
1509
return dbus.Boolean(self.enabled)
1515
# LastCheckedOK - property
1516
@dbus_service_property(_interface, signature="s",
1518
def LastCheckedOK_dbus_property(self, value=None):
1519
if value is not None:
1522
return datetime_to_dbus(self.last_checked_ok)
1524
# LastCheckerStatus - property
1525
@dbus_service_property(_interface, signature="n",
1527
def LastCheckerStatus_dbus_property(self):
1528
return dbus.Int16(self.last_checker_status)
1530
# Expires - property
1531
@dbus_service_property(_interface, signature="s", access="read")
1532
def Expires_dbus_property(self):
1533
return datetime_to_dbus(self.expires)
1535
# LastApprovalRequest - property
1536
@dbus_service_property(_interface, signature="s", access="read")
1537
def LastApprovalRequest_dbus_property(self):
1538
return datetime_to_dbus(self.last_approval_request)
1540
# Timeout - property
1541
@dbus_service_property(_interface, signature="t",
1543
def Timeout_dbus_property(self, value=None):
1544
if value is None: # get
1545
return dbus.UInt64(self.timeout.total_seconds() * 1000)
1546
old_timeout = self.timeout
1547
self.timeout = datetime.timedelta(0, 0, 0, value)
1548
# Reschedule disabling
1550
now = datetime.datetime.utcnow()
1551
self.expires += self.timeout - old_timeout
1552
if self.expires <= now:
1553
# The timeout has passed
1556
if (getattr(self, "disable_initiator_tag", None)
1559
gobject.source_remove(self.disable_initiator_tag)
1560
self.disable_initiator_tag = (
1561
gobject.timeout_add(
1562
int((self.expires - now).total_seconds()
1563
* 1000), self.disable))
1565
# ExtendedTimeout - property
1566
@dbus_service_property(_interface, signature="t",
1568
def ExtendedTimeout_dbus_property(self, value=None):
1569
if value is None: # get
1570
return dbus.UInt64(self.extended_timeout.total_seconds()
1572
self.extended_timeout = datetime.timedelta(0, 0, 0, value)
1574
# Interval - property
1575
@dbus_service_property(_interface, signature="t",
1577
def Interval_dbus_property(self, value=None):
1578
if value is None: # get
1579
return dbus.UInt64(self.interval.total_seconds() * 1000)
1580
self.interval = datetime.timedelta(0, 0, 0, value)
1581
if getattr(self, "checker_initiator_tag", None) is None:
1584
# Reschedule checker run
1585
gobject.source_remove(self.checker_initiator_tag)
1586
self.checker_initiator_tag = (gobject.timeout_add
1587
(value, self.start_checker))
1588
self.start_checker() # Start one now, too
1590
# Checker - property
1591
@dbus_service_property(_interface, signature="s",
1593
def Checker_dbus_property(self, value=None):
1594
if value is None: # get
1595
return dbus.String(self.checker_command)
1596
self.checker_command = unicode(value)
1598
# CheckerRunning - property
1599
@dbus_service_property(_interface, signature="b",
1601
def CheckerRunning_dbus_property(self, value=None):
1602
if value is None: # get
1603
return dbus.Boolean(self.checker is not None)
1605
self.start_checker()
1609
# ObjectPath - property
1610
@dbus_service_property(_interface, signature="o", access="read")
1611
def ObjectPath_dbus_property(self):
1612
return self.dbus_object_path # is already a dbus.ObjectPath
1615
@dbus_service_property(_interface, signature="ay",
1616
access="write", byte_arrays=True)
1617
def Secret_dbus_property(self, value):
1618
self.secret = str(value)
580
StopChecker = dbus.service.method(_interface)(stop_checker)
581
StopChecker.__name__ = "StopChecker"
1623
class ProxyClient(object):
1624
def __init__(self, child_pipe, fpr, address):
1625
self._pipe = child_pipe
1626
self._pipe.send(('init', fpr, address))
1627
if not self._pipe.recv():
1630
def __getattribute__(self, name):
1632
return super(ProxyClient, self).__getattribute__(name)
1633
self._pipe.send(('getattr', name))
1634
data = self._pipe.recv()
1635
if data[0] == 'data':
1637
if data[0] == 'function':
1638
def func(*args, **kwargs):
1639
self._pipe.send(('funcall', name, args, kwargs))
1640
return self._pipe.recv()[1]
1643
def __setattr__(self, name, value):
1645
return super(ProxyClient, self).__setattr__(name, value)
1646
self._pipe.send(('setattr', name, value))
1649
class ClientHandler(socketserver.BaseRequestHandler, object):
1650
"""A class to handle client connections.
1652
Instantiated once for each connection to handle it.
586
def peer_certificate(session):
587
"Return the peer's OpenPGP certificate as a bytestring"
588
# If not an OpenPGP certificate...
589
if (gnutls.library.functions
590
.gnutls_certificate_type_get(session._c_object)
591
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
592
# ...do the normal thing
593
return session.peer_certificate
594
list_size = ctypes.c_uint()
595
cert_list = (gnutls.library.functions
596
.gnutls_certificate_get_peers
597
(session._c_object, ctypes.byref(list_size)))
598
if list_size.value == 0:
601
return ctypes.string_at(cert.data, cert.size)
604
def fingerprint(openpgp):
605
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
606
# New GnuTLS "datum" with the OpenPGP public key
607
datum = (gnutls.library.types
608
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
611
ctypes.c_uint(len(openpgp))))
612
# New empty GnuTLS certificate
613
crt = gnutls.library.types.gnutls_openpgp_crt_t()
614
(gnutls.library.functions
615
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
616
# Import the OpenPGP public key into the certificate
617
(gnutls.library.functions
618
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
619
gnutls.library.constants
620
.GNUTLS_OPENPGP_FMT_RAW))
621
# Verify the self signature in the key
622
crtverify = ctypes.c_uint()
623
(gnutls.library.functions
624
.gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
625
if crtverify.value != 0:
626
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
627
raise gnutls.errors.CertificateSecurityError("Verify failed")
628
# New buffer for the fingerprint
629
buf = ctypes.create_string_buffer(20)
630
buf_len = ctypes.c_size_t()
631
# Get the fingerprint from the certificate into the buffer
632
(gnutls.library.functions
633
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
634
ctypes.byref(buf_len)))
635
# Deinit the certificate
636
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
637
# Convert the buffer to a Python bytestring
638
fpr = ctypes.string_at(buf, buf_len.value)
639
# Convert the bytestring to hexadecimal notation
640
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
644
class TCP_handler(SocketServer.BaseRequestHandler, object):
645
"""A TCP request handler class.
646
Instantiated by IPv6_TCPServer for each request to handle it.
1653
647
Note: This will run in its own forked process."""
1655
649
def handle(self):
1656
with contextlib.closing(self.server.child_pipe) as child_pipe:
1657
logger.info("TCP connection from: %s",
1658
unicode(self.client_address))
1659
logger.debug("Pipe FD: %d",
1660
self.server.child_pipe.fileno())
1662
session = (gnutls.connection
1663
.ClientSession(self.request,
1665
.X509Credentials()))
1667
# Note: gnutls.connection.X509Credentials is really a
1668
# generic GnuTLS certificate credentials object so long as
1669
# no X.509 keys are added to it. Therefore, we can use it
1670
# here despite using OpenPGP certificates.
1672
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
1673
# "+AES-256-CBC", "+SHA1",
1674
# "+COMP-NULL", "+CTYPE-OPENPGP",
1676
# Use a fallback default, since this MUST be set.
1677
priority = self.server.gnutls_priority
1678
if priority is None:
1680
(gnutls.library.functions
1681
.gnutls_priority_set_direct(session._c_object,
1684
# Start communication using the Mandos protocol
1685
# Get protocol number
1686
line = self.request.makefile().readline()
1687
logger.debug("Protocol version: %r", line)
1689
if int(line.strip().split()[0]) > 1:
1690
raise RuntimeError(line)
1691
except (ValueError, IndexError, RuntimeError) as error:
1692
logger.error("Unknown protocol version: %s", error)
1695
# Start GnuTLS connection
1698
except gnutls.errors.GNUTLSError as error:
1699
logger.warning("Handshake failed: %s", error)
1700
# Do not run session.bye() here: the session is not
1701
# established. Just abandon the request.
1703
logger.debug("Handshake succeeded")
1705
approval_required = False
1708
fpr = self.fingerprint(self.peer_certificate
1711
gnutls.errors.GNUTLSError) as error:
1712
logger.warning("Bad certificate: %s", error)
1714
logger.debug("Fingerprint: %s", fpr)
1717
client = ProxyClient(child_pipe, fpr,
1718
self.client_address)
1722
if client.approval_delay:
1723
delay = client.approval_delay
1724
client.approvals_pending += 1
1725
approval_required = True
1728
if not client.enabled:
1729
logger.info("Client %s is disabled",
1731
if self.server.use_dbus:
1733
client.Rejected("Disabled")
1736
if client.approved or not client.approval_delay:
1737
#We are approved or approval is disabled
1739
elif client.approved is None:
1740
logger.info("Client %s needs approval",
1742
if self.server.use_dbus:
1744
client.NeedApproval(
1745
client.approval_delay.total_seconds()
1746
* 1000, client.approved_by_default)
1748
logger.warning("Client %s was not approved",
1750
if self.server.use_dbus:
1752
client.Rejected("Denied")
1755
#wait until timeout or approved
1756
time = datetime.datetime.now()
1757
client.changedstate.acquire()
1758
client.changedstate.wait(delay.total_seconds())
1759
client.changedstate.release()
1760
time2 = datetime.datetime.now()
1761
if (time2 - time) >= delay:
1762
if not client.approved_by_default:
1763
logger.warning("Client %s timed out while"
1764
" waiting for approval",
1766
if self.server.use_dbus:
1768
client.Rejected("Approval timed out")
1773
delay -= time2 - time
1776
while sent_size < len(client.secret):
1778
sent = session.send(client.secret[sent_size:])
1779
except gnutls.errors.GNUTLSError as error:
1780
logger.warning("gnutls send failed",
1783
logger.debug("Sent: %d, remaining: %d",
1784
sent, len(client.secret)
1785
- (sent_size + sent))
1788
logger.info("Sending secret to %s", client.name)
1789
# bump the timeout using extended_timeout
1790
client.bump_timeout(client.extended_timeout)
1791
if self.server.use_dbus:
1796
if approval_required:
1797
client.approvals_pending -= 1
1800
except gnutls.errors.GNUTLSError as error:
1801
logger.warning("GnuTLS bye failed",
1805
def peer_certificate(session):
1806
"Return the peer's OpenPGP certificate as a bytestring"
1807
# If not an OpenPGP certificate...
1808
if (gnutls.library.functions
1809
.gnutls_certificate_type_get(session._c_object)
1810
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1811
# ...do the normal thing
1812
return session.peer_certificate
1813
list_size = ctypes.c_uint(1)
1814
cert_list = (gnutls.library.functions
1815
.gnutls_certificate_get_peers
1816
(session._c_object, ctypes.byref(list_size)))
1817
if not bool(cert_list) and list_size.value != 0:
1818
raise gnutls.errors.GNUTLSError("error getting peer"
1820
if list_size.value == 0:
1823
return ctypes.string_at(cert.data, cert.size)
1826
def fingerprint(openpgp):
1827
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1828
# New GnuTLS "datum" with the OpenPGP public key
1829
datum = (gnutls.library.types
1830
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1833
ctypes.c_uint(len(openpgp))))
1834
# New empty GnuTLS certificate
1835
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1836
(gnutls.library.functions
1837
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1838
# Import the OpenPGP public key into the certificate
1839
(gnutls.library.functions
1840
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1841
gnutls.library.constants
1842
.GNUTLS_OPENPGP_FMT_RAW))
1843
# Verify the self signature in the key
1844
crtverify = ctypes.c_uint()
1845
(gnutls.library.functions
1846
.gnutls_openpgp_crt_verify_self(crt, 0,
1847
ctypes.byref(crtverify)))
1848
if crtverify.value != 0:
1849
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1850
raise (gnutls.errors.CertificateSecurityError
1852
# New buffer for the fingerprint
1853
buf = ctypes.create_string_buffer(20)
1854
buf_len = ctypes.c_size_t()
1855
# Get the fingerprint from the certificate into the buffer
1856
(gnutls.library.functions
1857
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1858
ctypes.byref(buf_len)))
1859
# Deinit the certificate
1860
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1861
# Convert the buffer to a Python bytestring
1862
fpr = ctypes.string_at(buf, buf_len.value)
1863
# Convert the bytestring to hexadecimal notation
1864
hex_fpr = binascii.hexlify(fpr).upper()
1868
class MultiprocessingMixIn(object):
1869
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
1870
def sub_process_main(self, request, address):
1872
self.finish_request(request, address)
1874
self.handle_error(request, address)
1875
self.close_request(request)
1877
def process_request(self, request, address):
1878
"""Start a new process to process the request."""
1879
proc = multiprocessing.Process(target = self.sub_process_main,
1880
args = (request, address))
1885
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
1886
""" adds a pipe to the MixIn """
1887
def process_request(self, request, client_address):
1888
"""Overrides and wraps the original process_request().
1890
This function creates a new pipe in self.pipe
1892
parent_pipe, self.child_pipe = multiprocessing.Pipe()
1894
proc = MultiprocessingMixIn.process_request(self, request,
1896
self.child_pipe.close()
1897
self.add_pipe(parent_pipe, proc)
1899
def add_pipe(self, parent_pipe, proc):
1900
"""Dummy function; override as necessary"""
1901
raise NotImplementedError()
1904
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1905
socketserver.TCPServer, object):
1906
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
650
logger.info(u"TCP connection from: %s",
651
unicode(self.client_address))
652
session = (gnutls.connection
653
.ClientSession(self.request,
657
line = self.request.makefile().readline()
658
logger.debug(u"Protocol version: %r", line)
660
if int(line.strip().split()[0]) > 1:
662
except (ValueError, IndexError, RuntimeError), error:
663
logger.error(u"Unknown protocol version: %s", error)
666
# Note: gnutls.connection.X509Credentials is really a generic
667
# GnuTLS certificate credentials object so long as no X.509
668
# keys are added to it. Therefore, we can use it here despite
669
# using OpenPGP certificates.
671
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
672
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
674
# Use a fallback default, since this MUST be set.
675
priority = self.server.settings.get("priority", "NORMAL")
676
(gnutls.library.functions
677
.gnutls_priority_set_direct(session._c_object,
682
except gnutls.errors.GNUTLSError, error:
683
logger.warning(u"Handshake failed: %s", error)
684
# Do not run session.bye() here: the session is not
685
# established. Just abandon the request.
688
fpr = fingerprint(peer_certificate(session))
689
except (TypeError, gnutls.errors.GNUTLSError), error:
690
logger.warning(u"Bad certificate: %s", error)
693
logger.debug(u"Fingerprint: %s", fpr)
694
for c in self.server.clients:
695
if c.fingerprint == fpr:
699
logger.warning(u"Client not found for fingerprint: %s",
703
# Have to check if client.still_valid(), since it is possible
704
# that the client timed out while establishing the GnuTLS
706
if not client.still_valid():
707
logger.warning(u"Client %(name)s is invalid",
711
## This won't work here, since we're in a fork.
712
# client.bump_timeout()
714
while sent_size < len(client.secret):
715
sent = session.send(client.secret[sent_size:])
716
logger.debug(u"Sent: %d, remaining: %d",
717
sent, len(client.secret)
718
- (sent_size + sent))
723
class IPv6_TCPServer(SocketServer.ForkingMixIn,
724
SocketServer.TCPServer, object):
725
"""IPv6 TCP server. Accepts 'None' as address and/or port.
727
settings: Server settings
728
clients: Set() of Client objects
1909
729
enabled: Boolean; whether this server is activated yet
1910
interface: None or a network interface name (string)
1911
use_ipv6: Boolean; to use IPv6 or not
1913
def __init__(self, server_address, RequestHandlerClass,
1914
interface=None, use_ipv6=True, socketfd=None):
1915
"""If socketfd is set, use that file descriptor instead of
1916
creating a new one with socket.socket().
1918
self.interface = interface
1920
self.address_family = socket.AF_INET6
1921
if socketfd is not None:
1922
# Save the file descriptor
1923
self.socketfd = socketfd
1924
# Save the original socket.socket() function
1925
self.socket_socket = socket.socket
1926
# To implement --socket, we monkey patch socket.socket.
1928
# (When socketserver.TCPServer is a new-style class, we
1929
# could make self.socket into a property instead of monkey
1930
# patching socket.socket.)
1932
# Create a one-time-only replacement for socket.socket()
1933
@functools.wraps(socket.socket)
1934
def socket_wrapper(*args, **kwargs):
1935
# Restore original function so subsequent calls are
1937
socket.socket = self.socket_socket
1938
del self.socket_socket
1939
# This time only, return a new socket object from the
1940
# saved file descriptor.
1941
return socket.fromfd(self.socketfd, *args, **kwargs)
1942
# Replace socket.socket() function with wrapper
1943
socket.socket = socket_wrapper
1944
# The socketserver.TCPServer.__init__ will call
1945
# socket.socket(), which might be our replacement,
1946
# socket_wrapper(), if socketfd was set.
1947
socketserver.TCPServer.__init__(self, server_address,
1948
RequestHandlerClass)
731
address_family = socket.AF_INET6
732
def __init__(self, *args, **kwargs):
733
if "settings" in kwargs:
734
self.settings = kwargs["settings"]
735
del kwargs["settings"]
736
if "clients" in kwargs:
737
self.clients = kwargs["clients"]
738
del kwargs["clients"]
740
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
1950
741
def server_bind(self):
1951
742
"""This overrides the normal server_bind() function
1952
743
to bind to an interface if one was specified, and also NOT to
1953
744
bind to an address or port if they were not specified."""
1954
if self.interface is not None:
1955
if SO_BINDTODEVICE is None:
1956
logger.error("SO_BINDTODEVICE does not exist;"
1957
" cannot bind to interface %s",
1961
self.socket.setsockopt(socket.SOL_SOCKET,
1963
str(self.interface + '\0'))
1964
except socket.error as error:
1965
if error.errno == errno.EPERM:
1966
logger.error("No permission to bind to"
1967
" interface %s", self.interface)
1968
elif error.errno == errno.ENOPROTOOPT:
1969
logger.error("SO_BINDTODEVICE not available;"
1970
" cannot bind to interface %s",
1972
elif error.errno == errno.ENODEV:
1973
logger.error("Interface %s does not exist,"
1974
" cannot bind", self.interface)
745
if self.settings["interface"]:
746
# 25 is from /usr/include/asm-i486/socket.h
747
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
749
self.socket.setsockopt(socket.SOL_SOCKET,
751
self.settings["interface"])
752
except socket.error, error:
753
if error[0] == errno.EPERM:
754
logger.error(u"No permission to"
755
u" bind to interface %s",
756
self.settings["interface"])
1977
759
# Only bind(2) the socket if we really need to.
1978
760
if self.server_address[0] or self.server_address[1]:
1979
761
if not self.server_address[0]:
1980
if self.address_family == socket.AF_INET6:
1981
any_address = "::" # in6addr_any
1983
any_address = "0.0.0.0" # INADDR_ANY
1984
self.server_address = (any_address,
763
self.server_address = (in6addr_any,
1985
764
self.server_address[1])
1986
765
elif not self.server_address[1]:
1987
766
self.server_address = (self.server_address[0],
1989
# if self.interface:
768
# if self.settings["interface"]:
1990
769
# self.server_address = (self.server_address[0],
1993
772
# if_nametoindex
1995
return socketserver.TCPServer.server_bind(self)
1998
class MandosServer(IPv6_TCPServer):
2002
clients: set of Client objects
2003
gnutls_priority GnuTLS priority string
2004
use_dbus: Boolean; to emit D-Bus signals or not
2006
Assumes a gobject.MainLoop event loop.
2008
def __init__(self, server_address, RequestHandlerClass,
2009
interface=None, use_ipv6=True, clients=None,
2010
gnutls_priority=None, use_dbus=True, socketfd=None):
2011
self.enabled = False
2012
self.clients = clients
2013
if self.clients is None:
2015
self.use_dbus = use_dbus
2016
self.gnutls_priority = gnutls_priority
2017
IPv6_TCPServer.__init__(self, server_address,
2018
RequestHandlerClass,
2019
interface = interface,
2020
use_ipv6 = use_ipv6,
2021
socketfd = socketfd)
775
return super(IPv6_TCPServer, self).server_bind()
2022
776
def server_activate(self):
2023
777
if self.enabled:
2024
return socketserver.TCPServer.server_activate(self)
778
return super(IPv6_TCPServer, self).server_activate()
2026
779
def enable(self):
2027
780
self.enabled = True
2029
def add_pipe(self, parent_pipe, proc):
2030
# Call "handle_ipc" for both data and EOF events
2031
gobject.io_add_watch(parent_pipe.fileno(),
2032
gobject.IO_IN | gobject.IO_HUP,
2033
functools.partial(self.handle_ipc,
2038
def handle_ipc(self, source, condition, parent_pipe=None,
2039
proc = None, client_object=None):
2040
# error, or the other end of multiprocessing.Pipe has closed
2041
if condition & (gobject.IO_ERR | gobject.IO_HUP):
2042
# Wait for other process to exit
2046
# Read a request from the child
2047
request = parent_pipe.recv()
2048
command = request[0]
2050
if command == 'init':
2052
address = request[2]
2054
for c in self.clients.itervalues():
2055
if c.fingerprint == fpr:
2059
logger.info("Client not found for fingerprint: %s, ad"
2060
"dress: %s", fpr, address)
2063
mandos_dbus_service.ClientNotFound(fpr,
2065
parent_pipe.send(False)
2068
gobject.io_add_watch(parent_pipe.fileno(),
2069
gobject.IO_IN | gobject.IO_HUP,
2070
functools.partial(self.handle_ipc,
2076
parent_pipe.send(True)
2077
# remove the old hook in favor of the new above hook on
2080
if command == 'funcall':
2081
funcname = request[1]
2085
parent_pipe.send(('data', getattr(client_object,
2089
if command == 'getattr':
2090
attrname = request[1]
2091
if callable(client_object.__getattribute__(attrname)):
2092
parent_pipe.send(('function',))
2094
parent_pipe.send(('data', client_object
2095
.__getattribute__(attrname)))
2097
if command == 'setattr':
2098
attrname = request[1]
2100
setattr(client_object, attrname, value)
2105
def rfc3339_duration_to_delta(duration):
2106
"""Parse an RFC 3339 "duration" and return a datetime.timedelta
2108
>>> rfc3339_duration_to_delta("P7D")
2109
datetime.timedelta(7)
2110
>>> rfc3339_duration_to_delta("PT60S")
2111
datetime.timedelta(0, 60)
2112
>>> rfc3339_duration_to_delta("PT60M")
2113
datetime.timedelta(0, 3600)
2114
>>> rfc3339_duration_to_delta("PT24H")
2115
datetime.timedelta(1)
2116
>>> rfc3339_duration_to_delta("P1W")
2117
datetime.timedelta(7)
2118
>>> rfc3339_duration_to_delta("PT5M30S")
2119
datetime.timedelta(0, 330)
2120
>>> rfc3339_duration_to_delta("P1DT3M20S")
2121
datetime.timedelta(1, 200)
2124
# Parsing an RFC 3339 duration with regular expressions is not
2125
# possible - there would have to be multiple places for the same
2126
# values, like seconds. The current code, while more esoteric, is
2127
# cleaner without depending on a parsing library. If Python had a
2128
# built-in library for parsing we would use it, but we'd like to
2129
# avoid excessive use of external libraries.
2131
# New type for defining tokens, syntax, and semantics all-in-one
2132
Token = collections.namedtuple("Token",
2133
("regexp", # To match token; if
2134
# "value" is not None,
2135
# must have a "group"
2137
"value", # datetime.timedelta or
2139
"followers")) # Tokens valid after
2141
# RFC 3339 "duration" tokens, syntax, and semantics; taken from
2142
# the "duration" ABNF definition in RFC 3339, Appendix A.
2143
token_end = Token(re.compile(r"$"), None, frozenset())
2144
token_second = Token(re.compile(r"(\d+)S"),
2145
datetime.timedelta(seconds=1),
2146
frozenset((token_end,)))
2147
token_minute = Token(re.compile(r"(\d+)M"),
2148
datetime.timedelta(minutes=1),
2149
frozenset((token_second, token_end)))
2150
token_hour = Token(re.compile(r"(\d+)H"),
2151
datetime.timedelta(hours=1),
2152
frozenset((token_minute, token_end)))
2153
token_time = Token(re.compile(r"T"),
2155
frozenset((token_hour, token_minute,
2157
token_day = Token(re.compile(r"(\d+)D"),
2158
datetime.timedelta(days=1),
2159
frozenset((token_time, token_end)))
2160
token_month = Token(re.compile(r"(\d+)M"),
2161
datetime.timedelta(weeks=4),
2162
frozenset((token_day, token_end)))
2163
token_year = Token(re.compile(r"(\d+)Y"),
2164
datetime.timedelta(weeks=52),
2165
frozenset((token_month, token_end)))
2166
token_week = Token(re.compile(r"(\d+)W"),
2167
datetime.timedelta(weeks=1),
2168
frozenset((token_end,)))
2169
token_duration = Token(re.compile(r"P"), None,
2170
frozenset((token_year, token_month,
2171
token_day, token_time,
2173
# Define starting values
2174
value = datetime.timedelta() # Value so far
2176
followers = frozenset((token_duration,)) # Following valid tokens
2177
s = duration # String left to parse
2178
# Loop until end token is found
2179
while found_token is not token_end:
2180
# Search for any currently valid tokens
2181
for token in followers:
2182
match = token.regexp.match(s)
2183
if match is not None:
2185
if token.value is not None:
2186
# Value found, parse digits
2187
factor = int(match.group(1), 10)
2188
# Add to value so far
2189
value += factor * token.value
2190
# Strip token from string
2191
s = token.regexp.sub("", s, 1)
2194
# Set valid next tokens
2195
followers = found_token.followers
2198
# No currently valid tokens were found
2199
raise ValueError("Invalid RFC 3339 duration")
2204
783
def string_to_delta(interval):
2205
784
"""Parse a string and return a datetime.timedelta
2207
786
>>> string_to_delta('7d')
2208
787
datetime.timedelta(7)
2209
788
>>> string_to_delta('60s')
2335
927
"debug": "False",
2337
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
929
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2338
930
"servicename": "Mandos",
2339
931
"use_dbus": "True",
2344
"statedir": "/var/lib/mandos",
2345
"foreground": "False",
2349
934
# Parse config file for server-global settings
2350
server_config = configparser.SafeConfigParser(server_defaults)
935
server_config = ConfigParser.SafeConfigParser(server_defaults)
2351
936
del server_defaults
2352
server_config.read(os.path.join(options.configdir,
937
server_config.read(os.path.join(options.configdir, "mandos.conf"))
2354
938
# Convert the SafeConfigParser object to a dict
2355
939
server_settings = server_config.defaults()
2356
# Use the appropriate methods on the non-string config options
2357
for option in ("debug", "use_dbus", "use_ipv6", "foreground"):
2358
server_settings[option] = server_config.getboolean("DEFAULT",
2360
if server_settings["port"]:
2361
server_settings["port"] = server_config.getint("DEFAULT",
2363
if server_settings["socket"]:
2364
server_settings["socket"] = server_config.getint("DEFAULT",
2366
# Later, stdin will, and stdout and stderr might, be dup'ed
2367
# over with an opened os.devnull. But we don't want this to
2368
# happen with a supplied network socket.
2369
if 0 <= server_settings["socket"] <= 2:
2370
server_settings["socket"] = os.dup(server_settings
940
# Use getboolean on the boolean config options
941
server_settings["debug"] = (server_config.getboolean
942
("DEFAULT", "debug"))
943
server_settings["use_dbus"] = (server_config.getboolean
944
("DEFAULT", "use_dbus"))
2372
945
del server_config
2374
947
# Override the settings from the config file with command line
2375
948
# options, if set.
2376
949
for option in ("interface", "address", "port", "debug",
2377
950
"priority", "servicename", "configdir",
2378
"use_dbus", "use_ipv6", "debuglevel", "restore",
2379
"statedir", "socket", "foreground", "zeroconf"):
2380
952
value = getattr(options, option)
2381
953
if value is not None:
2382
954
server_settings[option] = value
2384
# Force all strings to be unicode
2385
for option in server_settings.keys():
2386
if type(server_settings[option]) is str:
2387
server_settings[option] = unicode(server_settings[option])
2388
# Force all boolean options to be boolean
2389
for option in ("debug", "use_dbus", "use_ipv6", "restore",
2390
"foreground", "zeroconf"):
2391
server_settings[option] = bool(server_settings[option])
2392
# Debug implies foreground
2393
if server_settings["debug"]:
2394
server_settings["foreground"] = True
2395
956
# Now we have our good server settings in "server_settings"
2397
##################################################################
2399
if (not server_settings["zeroconf"] and
2400
not (server_settings["port"]
2401
or server_settings["socket"] != "")):
2402
parser.error("Needs port or socket to work without"
2405
958
# For convenience
2406
959
debug = server_settings["debug"]
2407
debuglevel = server_settings["debuglevel"]
2408
960
use_dbus = server_settings["use_dbus"]
2409
use_ipv6 = server_settings["use_ipv6"]
2410
stored_state_path = os.path.join(server_settings["statedir"],
2412
foreground = server_settings["foreground"]
2413
zeroconf = server_settings["zeroconf"]
2416
initlogger(debug, logging.DEBUG)
2421
level = getattr(logging, debuglevel.upper())
2422
initlogger(debug, level)
963
syslogger.setLevel(logging.WARNING)
964
console.setLevel(logging.WARNING)
2424
966
if server_settings["servicename"] != "Mandos":
2425
967
syslogger.setFormatter(logging.Formatter
2426
('Mandos ({}) [%(process)d]:'
2427
' %(levelname)s: %(message)s'
2428
.format(server_settings
968
('Mandos (%s): %%(levelname)s:'
970
% server_settings["servicename"]))
2431
972
# Parse config file with clients
2432
client_config = configparser.SafeConfigParser(Client
973
client_defaults = { "timeout": "1h",
975
"checker": "fping -q -- %%(host)s",
978
client_config = ConfigParser.SafeConfigParser(client_defaults)
2434
979
client_config.read(os.path.join(server_settings["configdir"],
2435
980
"clients.conf"))
2437
global mandos_dbus_service
2438
mandos_dbus_service = None
2441
if server_settings["socket"] != "":
2442
socketfd = server_settings["socket"]
2443
tcp_server = MandosServer((server_settings["address"],
2444
server_settings["port"]),
2446
interface=(server_settings["interface"]
2450
server_settings["priority"],
2454
pidfilename = "/run/mandos.pid"
2455
if not os.path.isdir("/run/."):
2456
pidfilename = "/var/run/mandos.pid"
2459
pidfile = open(pidfilename, "w")
2460
except IOError as e:
2461
logger.error("Could not open file %r", pidfilename,
2464
for name in ("_mandos", "mandos", "nobody"):
2466
uid = pwd.getpwnam(name).pw_uid
2467
gid = pwd.getpwnam(name).pw_gid
983
tcp_server = IPv6_TCPServer((server_settings["address"],
984
server_settings["port"]),
986
settings=server_settings,
988
pidfilename = "/var/run/mandos.pid"
990
pidfile = open(pidfilename, "w")
991
except IOError, error:
992
logger.error("Could not open file %r", pidfilename)
995
uid = pwd.getpwnam("_mandos").pw_uid
996
gid = pwd.getpwnam("_mandos").pw_gid
999
uid = pwd.getpwnam("mandos").pw_uid
1000
gid = pwd.getpwnam("mandos").pw_gid
2469
1001
except KeyError:
1003
uid = pwd.getpwnam("nobody").pw_uid
1004
gid = pwd.getpwnam("nogroup").pw_gid
2477
except OSError as error:
2478
if error.errno != errno.EPERM:
2482
# Enable all possible GnuTLS debugging
2484
# "Use a log level over 10 to enable all debugging options."
2486
gnutls.library.functions.gnutls_global_set_log_level(11)
2488
@gnutls.library.types.gnutls_log_func
2489
def debug_gnutls(level, string):
2490
logger.debug("GnuTLS: %s", string[:-1])
2492
(gnutls.library.functions
2493
.gnutls_global_set_log_function(debug_gnutls))
2495
# Redirect stdin so all checkers get /dev/null
2496
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2497
os.dup2(null, sys.stdin.fileno())
2501
# Need to fork before connecting to D-Bus
2503
# Close all input and output, do double fork, etc.
2506
# multiprocessing will use threads, so before we use gobject we
2507
# need to inform gobject that threads will be used.
2508
gobject.threads_init()
1011
except OSError, error:
1012
if error[0] != errno.EPERM:
1016
service = AvahiService(name = server_settings["servicename"],
1017
servicetype = "_mandos._tcp", )
1018
if server_settings["interface"]:
1019
service.interface = (if_nametoindex
1020
(server_settings["interface"]))
2510
1022
global main_loop
2511
1025
# From the Avahi example code
2512
DBusGMainLoop(set_as_default=True)
1026
DBusGMainLoop(set_as_default=True )
2513
1027
main_loop = gobject.MainLoop()
2514
1028
bus = dbus.SystemBus()
1029
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1030
avahi.DBUS_PATH_SERVER),
1031
avahi.DBUS_INTERFACE_SERVER)
2515
1032
# End of Avahi example code
2518
bus_name = dbus.service.BusName("se.recompile.Mandos",
2519
bus, do_not_queue=True)
2520
old_bus_name = (dbus.service.BusName
2521
("se.bsnet.fukt.Mandos", bus,
2523
except dbus.exceptions.NameExistsException as e:
2524
logger.error("Disabling D-Bus:", exc_info=e)
2526
server_settings["use_dbus"] = False
2527
tcp_server.use_dbus = False
2529
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2530
service = AvahiServiceToSyslog(name =
2531
server_settings["servicename"],
2532
servicetype = "_mandos._tcp",
2533
protocol = protocol, bus = bus)
2534
if server_settings["interface"]:
2535
service.interface = (if_nametoindex
2536
(str(server_settings["interface"])))
2538
global multiprocessing_manager
2539
multiprocessing_manager = multiprocessing.Manager()
2541
client_class = Client
2543
client_class = functools.partial(ClientDBus, bus = bus)
2545
client_settings = Client.config_parser(client_config)
2546
old_client_settings = {}
2549
# This is used to redirect stdout and stderr for checker processes
2551
wnull = open(os.devnull, "w") # A writable /dev/null
2552
# Only used if server is running in foreground but not in debug
2554
if debug or not foreground:
2557
# Get client data and settings from last running state.
2558
if server_settings["restore"]:
2560
with open(stored_state_path, "rb") as stored_state:
2561
clients_data, old_client_settings = (pickle.load
2563
os.remove(stored_state_path)
2564
except IOError as e:
2565
if e.errno == errno.ENOENT:
2566
logger.warning("Could not load persistent state: {}"
2567
.format(os.strerror(e.errno)))
2569
logger.critical("Could not load persistent state:",
2572
except EOFError as e:
2573
logger.warning("Could not load persistent state: "
2574
"EOFError:", exc_info=e)
2576
with PGPEngine() as pgp:
2577
for client_name, client in clients_data.items():
2578
# Skip removed clients
2579
if client_name not in client_settings:
2582
# Decide which value to use after restoring saved state.
2583
# We have three different values: Old config file,
2584
# new config file, and saved state.
2585
# New config value takes precedence if it differs from old
2586
# config value, otherwise use saved state.
2587
for name, value in client_settings[client_name].items():
2589
# For each value in new config, check if it
2590
# differs from the old config value (Except for
2591
# the "secret" attribute)
2592
if (name != "secret" and
2593
value != old_client_settings[client_name]
2595
client[name] = value
2599
# Clients who has passed its expire date can still be
2600
# enabled if its last checker was successful. Clients
2601
# whose checker succeeded before we stored its state is
2602
# assumed to have successfully run all checkers during
2604
if client["enabled"]:
2605
if datetime.datetime.utcnow() >= client["expires"]:
2606
if not client["last_checked_ok"]:
2608
"disabling client {} - Client never "
2609
"performed a successful checker"
2610
.format(client_name))
2611
client["enabled"] = False
2612
elif client["last_checker_status"] != 0:
2614
"disabling client {} - Client last"
2615
" checker failed with error code {}"
2616
.format(client_name,
2617
client["last_checker_status"]))
2618
client["enabled"] = False
2620
client["expires"] = (datetime.datetime
2622
+ client["timeout"])
2623
logger.debug("Last checker succeeded,"
2624
" keeping {} enabled"
2625
.format(client_name))
2627
client["secret"] = (
2628
pgp.decrypt(client["encrypted_secret"],
2629
client_settings[client_name]
2632
# If decryption fails, we use secret from new settings
2633
logger.debug("Failed to decrypt {} old secret"
2634
.format(client_name))
2635
client["secret"] = (
2636
client_settings[client_name]["secret"])
2638
# Add/remove clients based on new changes made to config
2639
for client_name in (set(old_client_settings)
2640
- set(client_settings)):
2641
del clients_data[client_name]
2642
for client_name in (set(client_settings)
2643
- set(old_client_settings)):
2644
clients_data[client_name] = client_settings[client_name]
2646
# Create all client objects
2647
for client_name, client in clients_data.items():
2648
tcp_server.clients[client_name] = client_class(
2649
name = client_name, settings = client,
2650
server_settings = server_settings)
2652
if not tcp_server.clients:
2653
logger.warning("No clients defined")
2656
if pidfile is not None:
2660
pidfile.write(str(pid) + "\n".encode("utf-8"))
2662
logger.error("Could not write to file %r with PID %d",
1034
bus_name = dbus.service.BusName(u"org.mandos-system.Mandos",
1037
clients.update(Set(Client(name = section,
1039
= dict(client_config.items(section)),
1040
use_dbus = use_dbus)
1041
for section in client_config.sections()))
1043
logger.warning(u"No clients defined")
1046
# Redirect stdin so all checkers get /dev/null
1047
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1048
os.dup2(null, sys.stdin.fileno())
1052
# No console logging
1053
logger.removeHandler(console)
1054
# Close all input and output, do double fork, etc.
1059
pidfile.write(str(pid) + "\n")
1063
logger.error(u"Could not write to file %r with PID %d",
1066
# "pidfile" was never created
1071
"Cleanup function; run on exit"
1073
# From the Avahi example code
1074
if not group is None:
1077
# End of Avahi example code
1080
client = clients.pop()
1081
client.disable_hook = None
1084
atexit.register(cleanup)
1087
signal.signal(signal.SIGINT, signal.SIG_IGN)
2667
1088
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2668
1089
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2671
@alternate_dbus_interfaces({"se.recompile.Mandos":
2672
"se.bsnet.fukt.Mandos"})
2673
class MandosDBusService(DBusObjectWithProperties):
1092
class MandosServer(dbus.service.Object):
2674
1093
"""A D-Bus proxy object"""
2675
1094
def __init__(self):
2676
dbus.service.Object.__init__(self, bus, "/")
2677
_interface = "se.recompile.Mandos"
2679
@dbus_interface_annotations(_interface)
2681
return { "org.freedesktop.DBus.Property"
2682
".EmitsChangedSignal":
1095
dbus.service.Object.__init__(self, bus,
1097
_interface = u"org.mandos_system.Mandos"
1099
@dbus.service.signal(_interface, signature="oa{sv}")
1100
def ClientAdded(self, objpath, properties):
2685
1104
@dbus.service.signal(_interface, signature="o")
2686
def ClientAdded(self, objpath):
2690
@dbus.service.signal(_interface, signature="ss")
2691
def ClientNotFound(self, fingerprint, address):
2695
@dbus.service.signal(_interface, signature="os")
2696
def ClientRemoved(self, objpath, name):
1105
def ClientRemoved(self, objpath):
2700
1109
@dbus.service.method(_interface, out_signature="ao")
2701
1110
def GetAllClients(self):
2703
return dbus.Array(c.dbus_object_path
2705
tcp_server.clients.itervalues())
2707
@dbus.service.method(_interface,
2708
out_signature="a{oa{sv}}")
1111
return dbus.Array(c.dbus_object_path for c in clients)
1113
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
2709
1114
def GetAllClientsWithProperties(self):
2711
1115
return dbus.Dictionary(
2712
((c.dbus_object_path, c.GetAll(""))
2713
for c in tcp_server.clients.itervalues()),
1116
((c.dbus_object_path, c.GetAllProperties())
2714
1118
signature="oa{sv}")
2716
1120
@dbus.service.method(_interface, in_signature="o")
2717
1121
def RemoveClient(self, object_path):
2719
for c in tcp_server.clients.itervalues():
2720
1123
if c.dbus_object_path == object_path:
2721
del tcp_server.clients[c.name]
2722
c.remove_from_connection()
2723
1125
# Don't signal anything except ClientRemoved
2724
c.disable(quiet=True)
2725
1128
# Emit D-Bus signal
2726
self.ClientRemoved(object_path, c.name)
1129
self.ClientRemoved(object_path)
2728
raise KeyError(object_path)
1132
@dbus.service.method(_interface)
2732
mandos_dbus_service = MandosDBusService()
2735
"Cleanup function; run on exit"
2739
multiprocessing.active_children()
2741
if not (tcp_server.clients or client_settings):
2744
# Store client before exiting. Secrets are encrypted with key
2745
# based on what config file has. If config file is
2746
# removed/edited, old secret will thus be unrecovable.
2748
with PGPEngine() as pgp:
2749
for client in tcp_server.clients.itervalues():
2750
key = client_settings[client.name]["secret"]
2751
client.encrypted_secret = pgp.encrypt(client.secret,
2755
# A list of attributes that can not be pickled
2757
exclude = { "bus", "changedstate", "secret",
2758
"checker", "server_settings" }
2759
for name, typ in (inspect.getmembers
2760
(dbus.service.Object)):
2763
client_dict["encrypted_secret"] = (client
2765
for attr in client.client_structure:
2766
if attr not in exclude:
2767
client_dict[attr] = getattr(client, attr)
2769
clients[client.name] = client_dict
2770
del client_settings[client.name]["secret"]
2773
with (tempfile.NamedTemporaryFile
2774
(mode='wb', suffix=".pickle", prefix='clients-',
2775
dir=os.path.dirname(stored_state_path),
2776
delete=False)) as stored_state:
2777
pickle.dump((clients, client_settings), stored_state)
2778
tempname=stored_state.name
2779
os.rename(tempname, stored_state_path)
2780
except (IOError, OSError) as e:
2786
if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
2787
logger.warning("Could not save persistent state: {}"
2788
.format(os.strerror(e.errno)))
2790
logger.warning("Could not save persistent state:",
2794
# Delete all clients, and settings from config
2795
while tcp_server.clients:
2796
name, client = tcp_server.clients.popitem()
2798
client.remove_from_connection()
2799
# Don't signal anything except ClientRemoved
2800
client.disable(quiet=True)
2803
mandos_dbus_service.ClientRemoved(client
2806
client_settings.clear()
2808
atexit.register(cleanup)
2810
for client in tcp_server.clients.itervalues():
1138
mandos_server = MandosServer()
1140
for client in clients:
2812
1142
# Emit D-Bus signal
2813
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2814
# Need to initiate checking of clients
2816
client.init_checker()
1143
mandos_server.ClientAdded(client.dbus_object_path,
1144
client.GetAllProperties())
2818
1147
tcp_server.enable()
2819
1148
tcp_server.server_activate()
2821
1150
# Find out what port we got
2823
service.port = tcp_server.socket.getsockname()[1]
2825
logger.info("Now listening on address %r, port %d,"
2826
" flowinfo %d, scope_id %d",
2827
*tcp_server.socket.getsockname())
2829
logger.info("Now listening on address %r, port %d",
2830
*tcp_server.socket.getsockname())
1151
service.port = tcp_server.socket.getsockname()[1]
1152
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1153
u" scope_id %d" % tcp_server.socket.getsockname())
2832
1155
#service.interface = tcp_server.socket.getsockname()[3]
2836
# From the Avahi example code
2839
except dbus.exceptions.DBusException as error:
2840
logger.critical("D-Bus Exception", exc_info=error)
2843
# End of Avahi example code
1158
# From the Avahi example code
1159
server.connect_to_signal("StateChanged", server_state_changed)
1161
server_state_changed(server.GetState())
1162
except dbus.exceptions.DBusException, error:
1163
logger.critical(u"DBusException: %s", error)
1165
# End of Avahi example code
2845
1167
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2846
1168
lambda *args, **kwargs:
2847
1169
(tcp_server.handle_request
2848
1170
(*args[2:], **kwargs) or True))
2850
logger.debug("Starting main loop")
1172
logger.debug(u"Starting main loop")
2851
1173
main_loop.run()
2852
except AvahiError as error:
2853
logger.critical("Avahi Error", exc_info=error)
1174
except AvahiError, error:
1175
logger.critical(u"AvahiError: %s", error)
2856
1177
except KeyboardInterrupt:
2858
print("", file=sys.stderr)
2859
logger.debug("Server received KeyboardInterrupt")
2860
logger.debug("Server exiting")
2861
# Must run before the D-Bus bus name gets deregistered
2864
1181
if __name__ == '__main__':