228
98
class AvahiService(object):
229
"""An Avahi (Zeroconf) service.
232
100
interface: integer; avahi.IF_UNSPEC or an interface index.
233
101
Used to optionally bind to the specified interface.
234
name: string; Example: 'Mandos'
235
type: string; Example: '_mandos._tcp'.
236
See <http://www.dns-sd.org/ServiceTypes.html>
237
port: integer; what port to announce
238
TXT: list of strings; TXT record for the service
239
domain: string; Domain to publish on, default to .local if empty.
240
host: string; Host to publish records for, default is localhost
241
max_renames: integer; maximum number of renames
242
rename_count: integer; counter so we only rename after collisions
243
a sensible number of times
244
group: D-Bus Entry Group
246
bus: dbus.SystemBus()
102
name = string; Example: "Mandos"
103
type = string; Example: "_mandos._tcp".
104
See <http://www.dns-sd.org/ServiceTypes.html>
105
port = integer; what port to announce
106
TXT = list of strings; TXT record for the service
107
domain = string; Domain to publish on, default to .local if empty.
108
host = string; Host to publish records for, default to localhost
110
max_renames = integer; maximum number of renames
111
rename_count = integer; counter so we only rename after collisions
112
a sensible number of times
249
114
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
250
servicetype = None, port = None, TXT = None,
251
domain = "", host = "", max_renames = 32768,
252
protocol = avahi.PROTO_UNSPEC, bus = None):
115
type = None, port = None, TXT = None, domain = "",
116
host = "", max_renames = 12):
117
"""An Avahi (Zeroconf) service. """
253
118
self.interface = interface
255
self.type = servicetype
257
self.TXT = TXT if TXT is not None else []
258
126
self.domain = domain
260
128
self.rename_count = 0
261
self.max_renames = max_renames
262
self.protocol = protocol
263
self.group = None # our entry group
266
self.entry_group_state_changed_match = None
268
129
def rename(self):
269
130
"""Derived from the Avahi example code"""
270
131
if self.rename_count >= self.max_renames:
271
logger.critical("No suitable Zeroconf service name found"
272
" after %i retries, exiting.",
132
logger.critical(u"No suitable service name found after %i"
133
u" retries, exiting.", rename_count)
274
134
raise AvahiServiceError("Too many renames")
275
self.name = unicode(self.server
276
.GetAlternativeServiceName(self.name))
277
logger.info("Changing Zeroconf service name to %r ...",
135
name = server.GetAlternativeServiceName(name)
136
logger.notice(u"Changing name to %r ...", name)
282
except dbus.exceptions.DBusException as error:
283
logger.critical("D-Bus Exception", exc_info=error)
286
139
self.rename_count += 1
288
140
def remove(self):
289
141
"""Derived from the Avahi example code"""
290
if self.entry_group_state_changed_match is not None:
291
self.entry_group_state_changed_match.remove()
292
self.entry_group_state_changed_match = None
293
if self.group is not None:
142
if group is not None:
297
145
"""Derived from the Avahi example code"""
299
if self.group is None:
300
self.group = dbus.Interface(
301
self.bus.get_object(avahi.DBUS_NAME,
302
self.server.EntryGroupNew()),
303
avahi.DBUS_INTERFACE_ENTRY_GROUP)
304
self.entry_group_state_changed_match = (
305
self.group.connect_to_signal(
306
'StateChanged', self.entry_group_state_changed))
307
logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
308
self.name, self.type)
309
self.group.AddService(
312
dbus.UInt32(0), # flags
313
self.name, self.type,
314
self.domain, self.host,
315
dbus.UInt16(self.port),
316
avahi.string_array_to_txt_array(self.TXT))
319
def entry_group_state_changed(self, state, error):
320
"""Derived from the Avahi example code"""
321
logger.debug("Avahi entry group state change: %i", state)
323
if state == avahi.ENTRY_GROUP_ESTABLISHED:
324
logger.debug("Zeroconf service established.")
325
elif state == avahi.ENTRY_GROUP_COLLISION:
326
logger.info("Zeroconf service name collision.")
328
elif state == avahi.ENTRY_GROUP_FAILURE:
329
logger.critical("Avahi: Error in group state changed %s",
331
raise AvahiGroupError("State changed: {0!s}"
335
"""Derived from the Avahi example code"""
336
if self.group is not None:
339
except (dbus.exceptions.UnknownMethodException,
340
dbus.exceptions.DBusException):
345
def server_state_changed(self, state, error=None):
346
"""Derived from the Avahi example code"""
347
logger.debug("Avahi server state change: %i", state)
348
bad_states = { avahi.SERVER_INVALID:
349
"Zeroconf server invalid",
350
avahi.SERVER_REGISTERING: None,
351
avahi.SERVER_COLLISION:
352
"Zeroconf server name collision",
353
avahi.SERVER_FAILURE:
354
"Zeroconf server failure" }
355
if state in bad_states:
356
if bad_states[state] is not None:
358
logger.error(bad_states[state])
360
logger.error(bad_states[state] + ": %r", error)
362
elif state == avahi.SERVER_RUNNING:
366
logger.debug("Unknown state: %r", state)
368
logger.debug("Unknown state: %r: %r", state, error)
371
"""Derived from the Avahi example code"""
372
if self.server is None:
373
self.server = dbus.Interface(
374
self.bus.get_object(avahi.DBUS_NAME,
375
avahi.DBUS_PATH_SERVER,
376
follow_name_owner_changes=True),
377
avahi.DBUS_INTERFACE_SERVER)
378
self.server.connect_to_signal("StateChanged",
379
self.server_state_changed)
380
self.server_state_changed(self.server.GetState())
383
class AvahiServiceToSyslog(AvahiService):
385
"""Add the new name to the syslog messages"""
386
ret = AvahiService.rename(self)
387
syslogger.setFormatter(logging.Formatter
388
('Mandos ({0}) [%(process)d]:'
389
' %(levelname)s: %(message)s'
394
def timedelta_to_milliseconds(td):
395
"Convert a datetime.timedelta() to milliseconds"
396
return ((td.days * 24 * 60 * 60 * 1000)
397
+ (td.seconds * 1000)
398
+ (td.microseconds // 1000))
148
group = dbus.Interface\
149
(bus.get_object(avahi.DBUS_NAME,
150
server.EntryGroupNew()),
151
avahi.DBUS_INTERFACE_ENTRY_GROUP)
152
group.connect_to_signal('StateChanged',
153
entry_group_state_changed)
154
logger.debug(u"Adding service '%s' of type '%s' ...",
155
service.name, service.type)
157
self.interface, # interface
158
avahi.PROTO_INET6, # protocol
159
dbus.UInt32(0), # flags
160
self.name, self.type,
161
self.domain, self.host,
162
dbus.UInt16(self.port),
163
avahi.string_array_to_txt_array(self.TXT))
166
# From the Avahi example code:
167
group = None # our entry group
168
# End of Avahi example code
401
171
class Client(object):
402
172
"""A representation of a client host served by this server.
405
approved: bool(); 'None' if not yet approved/disapproved
406
approval_delay: datetime.timedelta(); Time to wait for approval
407
approval_duration: datetime.timedelta(); Duration of one approval
408
checker: subprocess.Popen(); a running checker process used
409
to see if the client lives.
410
'None' if no process is running.
411
checker_callback_tag: a gobject event source tag, or None
412
checker_command: string; External command which is run to check
413
if client lives. %() expansions are done at
174
name: string; from the config file, used in log messages
175
fingerprint: string (40 or 32 hexadecimal digits); used to
176
uniquely identify the client
177
secret: bytestring; sent verbatim (over TLS) to client
178
fqdn: string (FQDN); available for use by the checker command
179
created: datetime.datetime(); object creation, not client host
180
last_checked_ok: datetime.datetime() or None if not yet checked OK
181
timeout: datetime.timedelta(); How long from last_checked_ok
182
until this client is invalid
183
interval: datetime.timedelta(); How often to start a new checker
184
stop_hook: If set, called by stop() as stop_hook(self)
185
checker: subprocess.Popen(); a running checker process used
186
to see if the client lives.
187
'None' if no process is running.
188
checker_initiator_tag: a gobject event source tag, or None
189
stop_initiator_tag: - '' -
190
checker_callback_tag: - '' -
191
checker_command: string; External command which is run to check if
192
client lives. %() expansions are done at
414
193
runtime with vars(self) as dict, so that for
415
194
instance %(name)s can be used in the command.
416
checker_initiator_tag: a gobject event source tag, or None
417
created: datetime.datetime(); (UTC) object creation
418
client_structure: Object describing what attributes a client has
419
and is used for storing the client at exit
420
current_checker_command: string; current running checker_command
421
disable_initiator_tag: a gobject event source tag, or None
423
fingerprint: string (40 or 32 hexadecimal digits); used to
424
uniquely identify the client
425
host: string; available for use by the checker command
426
interval: datetime.timedelta(); How often to start a new checker
427
last_approval_request: datetime.datetime(); (UTC) or None
428
last_checked_ok: datetime.datetime(); (UTC) or None
429
last_checker_status: integer between 0 and 255 reflecting exit
430
status of last checker. -1 reflects crashed
431
checker, -2 means no checker completed yet.
432
last_enabled: datetime.datetime(); (UTC) or None
433
name: string; from the config file, used in log messages and
435
secret: bytestring; sent verbatim (over TLS) to client
436
timeout: datetime.timedelta(); How long from last_checked_ok
437
until this client is disabled
438
extended_timeout: extra long timeout when secret has been sent
439
runtime_expansions: Allowed attributes for runtime expansion.
440
expires: datetime.datetime(); time (UTC) when a client will be
196
_timeout: Real variable for 'timeout'
197
_interval: Real variable for 'interval'
198
_timeout_milliseconds: Used when calling gobject.timeout_add()
199
_interval_milliseconds: - '' -
444
runtime_expansions = ("approval_delay", "approval_duration",
445
"created", "enabled", "expires",
446
"fingerprint", "host", "interval",
447
"last_approval_request", "last_checked_ok",
448
"last_enabled", "name", "timeout")
449
client_defaults = { "timeout": "5m",
450
"extended_timeout": "15m",
452
"checker": "fping -q -- %%(host)s",
454
"approval_delay": "0s",
455
"approval_duration": "1s",
456
"approved_by_default": "True",
460
def timeout_milliseconds(self):
461
"Return the 'timeout' attribute in milliseconds"
462
return timedelta_to_milliseconds(self.timeout)
464
def extended_timeout_milliseconds(self):
465
"Return the 'extended_timeout' attribute in milliseconds"
466
return timedelta_to_milliseconds(self.extended_timeout)
468
def interval_milliseconds(self):
469
"Return the 'interval' attribute in milliseconds"
470
return timedelta_to_milliseconds(self.interval)
472
def approval_delay_milliseconds(self):
473
return timedelta_to_milliseconds(self.approval_delay)
476
def config_parser(config):
477
"""Construct a new dict of client settings of this form:
478
{ client_name: {setting_name: value, ...}, ...}
479
with exceptions for any special settings as defined above.
480
NOTE: Must be a pure function. Must return the same result
481
value given the same arguments.
484
for client_name in config.sections():
485
section = dict(config.items(client_name))
486
client = settings[client_name] = {}
488
client["host"] = section["host"]
489
# Reformat values from string types to Python types
490
client["approved_by_default"] = config.getboolean(
491
client_name, "approved_by_default")
492
client["enabled"] = config.getboolean(client_name,
495
client["fingerprint"] = (section["fingerprint"].upper()
497
if "secret" in section:
498
client["secret"] = section["secret"].decode("base64")
499
elif "secfile" in section:
500
with open(os.path.expanduser(os.path.expandvars
501
(section["secfile"])),
503
client["secret"] = secfile.read()
505
raise TypeError("No secret or secfile for section {0}"
507
client["timeout"] = string_to_delta(section["timeout"])
508
client["extended_timeout"] = string_to_delta(
509
section["extended_timeout"])
510
client["interval"] = string_to_delta(section["interval"])
511
client["approval_delay"] = string_to_delta(
512
section["approval_delay"])
513
client["approval_duration"] = string_to_delta(
514
section["approval_duration"])
515
client["checker_command"] = section["checker"]
516
client["last_approval_request"] = None
517
client["last_checked_ok"] = None
518
client["last_checker_status"] = -2
522
def __init__(self, settings, name = None):
201
def _set_timeout(self, timeout):
202
"Setter function for 'timeout' attribute"
203
self._timeout = timeout
204
self._timeout_milliseconds = ((self.timeout.days
205
* 24 * 60 * 60 * 1000)
206
+ (self.timeout.seconds * 1000)
207
+ (self.timeout.microseconds
209
timeout = property(lambda self: self._timeout,
212
def _set_interval(self, interval):
213
"Setter function for 'interval' attribute"
214
self._interval = interval
215
self._interval_milliseconds = ((self.interval.days
216
* 24 * 60 * 60 * 1000)
217
+ (self.interval.seconds
219
+ (self.interval.microseconds
221
interval = property(lambda self: self._interval,
224
def __init__(self, name=None, stop_hook=None, fingerprint=None,
225
secret=None, secfile=None, fqdn=None, timeout=None,
226
interval=-1, checker=None):
227
"""Note: the 'checker' argument sets the 'checker_command'
228
attribute and not the 'checker' attribute.."""
524
# adding all client settings
525
for setting, value in settings.iteritems():
526
setattr(self, setting, value)
529
if not hasattr(self, "last_enabled"):
530
self.last_enabled = datetime.datetime.utcnow()
531
if not hasattr(self, "expires"):
532
self.expires = (datetime.datetime.utcnow()
230
logger.debug(u"Creating client %r", self.name)
231
# Uppercase and remove spaces from fingerprint
232
# for later comparison purposes with return value of
233
# the fingerprint() function
234
self.fingerprint = fingerprint.upper().replace(u" ", u"")
235
logger.debug(u" Fingerprint: %s", self.fingerprint)
237
self.secret = secret.decode(u"base64")
240
self.secret = sf.read()
535
self.last_enabled = None
538
logger.debug("Creating client %r", self.name)
539
# Uppercase and remove spaces from fingerprint for later
540
# comparison purposes with return value from the fingerprint()
542
logger.debug(" Fingerprint: %s", self.fingerprint)
543
self.created = settings.get("created",
544
datetime.datetime.utcnow())
546
# attributes specific for this server instance
243
raise TypeError(u"No secret or secfile for client %s"
246
self.created = datetime.datetime.now()
247
self.last_checked_ok = None
248
self.timeout = string_to_delta(timeout)
249
self.interval = string_to_delta(interval)
250
self.stop_hook = stop_hook
547
251
self.checker = None
548
252
self.checker_initiator_tag = None
549
self.disable_initiator_tag = None
253
self.stop_initiator_tag = None
550
254
self.checker_callback_tag = None
551
self.current_checker_command = None
553
self.approvals_pending = 0
554
self.changedstate = (multiprocessing_manager
555
.Condition(multiprocessing_manager
557
self.client_structure = [attr for attr in
558
self.__dict__.iterkeys()
559
if not attr.startswith("_")]
560
self.client_structure.append("client_structure")
562
for name, t in inspect.getmembers(type(self),
566
if not name.startswith("_"):
567
self.client_structure.append(name)
569
# Send notice to process children that client state has changed
570
def send_changedstate(self):
571
with self.changedstate:
572
self.changedstate.notify_all()
255
self.check_command = checker
575
257
"""Start this client's checker and timeout hooks"""
576
if getattr(self, "enabled", False):
579
self.expires = datetime.datetime.utcnow() + self.timeout
581
self.last_enabled = datetime.datetime.utcnow()
583
self.send_changedstate()
585
def disable(self, quiet=True):
586
"""Disable this client."""
587
if not getattr(self, "enabled", False):
590
logger.info("Disabling client %s", self.name)
591
if getattr(self, "disable_initiator_tag", None) is not None:
592
gobject.source_remove(self.disable_initiator_tag)
593
self.disable_initiator_tag = None
595
if getattr(self, "checker_initiator_tag", None) is not None:
596
gobject.source_remove(self.checker_initiator_tag)
597
self.checker_initiator_tag = None
601
self.send_changedstate()
602
# Do not run this again if called by a gobject.timeout_add
608
def init_checker(self):
609
258
# Schedule a new checker to be started an 'interval' from now,
610
259
# and every interval from then on.
611
if self.checker_initiator_tag is not None:
612
gobject.source_remove(self.checker_initiator_tag)
613
self.checker_initiator_tag = (gobject.timeout_add
614
(self.interval_milliseconds(),
616
# Schedule a disable() when 'timeout' has passed
617
if self.disable_initiator_tag is not None:
618
gobject.source_remove(self.disable_initiator_tag)
619
self.disable_initiator_tag = (gobject.timeout_add
620
(self.timeout_milliseconds(),
260
self.checker_initiator_tag = gobject.timeout_add\
261
(self._interval_milliseconds,
622
263
# Also start a new checker *right now*.
623
264
self.start_checker()
625
def checker_callback(self, pid, condition, command):
265
# Schedule a stop() when 'timeout' has passed
266
self.stop_initiator_tag = gobject.timeout_add\
267
(self._timeout_milliseconds,
271
The possibility that a client might be restarted is left open,
272
but not currently used."""
273
# If this client doesn't have a secret, it is already stopped.
275
logger.debug(u"Stopping client %s", self.name)
279
if getattr(self, "stop_initiator_tag", False):
280
gobject.source_remove(self.stop_initiator_tag)
281
self.stop_initiator_tag = None
282
if getattr(self, "checker_initiator_tag", False):
283
gobject.source_remove(self.checker_initiator_tag)
284
self.checker_initiator_tag = None
288
# Do not run this again if called by a gobject.timeout_add
291
self.stop_hook = None
293
def checker_callback(self, pid, condition):
626
294
"""The checker has completed, so take appropriate actions."""
295
now = datetime.datetime.now()
627
296
self.checker_callback_tag = None
628
297
self.checker = None
629
if os.WIFEXITED(condition):
630
self.last_checker_status = os.WEXITSTATUS(condition)
631
if self.last_checker_status == 0:
632
logger.info("Checker for %(name)s succeeded",
636
logger.info("Checker for %(name)s failed",
639
self.last_checker_status = -1
640
logger.warning("Checker for %(name)s crashed?",
298
if os.WIFEXITED(condition) \
299
and (os.WEXITSTATUS(condition) == 0):
300
logger.debug(u"Checker for %(name)s succeeded",
302
self.last_checked_ok = now
303
gobject.source_remove(self.stop_initiator_tag)
304
self.stop_initiator_tag = gobject.timeout_add\
305
(self._timeout_milliseconds,
307
elif not os.WIFEXITED(condition):
308
logger.warning(u"Checker for %(name)s crashed?",
643
def checked_ok(self):
644
"""Assert that the client has been seen, alive and well."""
645
self.last_checked_ok = datetime.datetime.utcnow()
646
self.last_checker_status = 0
649
def bump_timeout(self, timeout=None):
650
"""Bump up the timeout for this client."""
652
timeout = self.timeout
653
if self.disable_initiator_tag is not None:
654
gobject.source_remove(self.disable_initiator_tag)
655
self.disable_initiator_tag = None
656
if getattr(self, "enabled", False):
657
self.disable_initiator_tag = (gobject.timeout_add
658
(timedelta_to_milliseconds
659
(timeout), self.disable))
660
self.expires = datetime.datetime.utcnow() + timeout
662
def need_approval(self):
663
self.last_approval_request = datetime.datetime.utcnow()
311
logger.debug(u"Checker for %(name)s failed",
665
313
def start_checker(self):
666
314
"""Start a new checker subprocess if one is not running.
668
315
If a checker already exists, leave it running and do
670
317
# The reason for not killing a running checker is that if we
671
# did that, and if a checker (for some reason) started running
672
# slowly and taking more than 'interval' time, then the client
673
# would inevitably timeout, since no checker would get a
674
# chance to run to completion. If we instead leave running
318
# did that, then if a checker (for some reason) started
319
# running slowly and taking more than 'interval' time, the
320
# client would inevitably timeout, since no checker would get
321
# a chance to run to completion. If we instead leave running
675
322
# checkers alone, the checker would have to take more time
676
# than 'timeout' for the client to be disabled, which is as it
679
# If a checker exists, make sure it is not a zombie
681
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
682
except (AttributeError, OSError) as error:
683
if (isinstance(error, OSError)
684
and error.errno != errno.ECHILD):
688
logger.warning("Checker was a zombie")
689
gobject.source_remove(self.checker_callback_tag)
690
self.checker_callback(pid, status,
691
self.current_checker_command)
692
# Start a new checker if needed
323
# than 'timeout' for the client to be declared invalid, which
324
# is as it should be.
693
325
if self.checker is None:
694
# Escape attributes for the shell
695
escaped_attrs = dict(
696
(attr, re.escape(unicode(getattr(self, attr))))
698
self.runtime_expansions)
700
command = self.checker_command % escaped_attrs
701
except TypeError as error:
702
logger.error('Could not format string "%s"',
703
self.checker_command, exc_info=error)
704
return True # Try again later
705
self.current_checker_command = command
707
logger.info("Starting checker %r for %s",
709
# We don't need to redirect stdout and stderr, since
710
# in normal mode, that is already done by daemon(),
711
# and in debug mode we don't want to. (Stdin is
712
# always replaced by /dev/null.)
327
# In case check_command has exactly one % operator
328
command = self.check_command % self.fqdn
330
# Escape attributes for the shell
331
escaped_attrs = dict((key, re.escape(str(val)))
333
vars(self).iteritems())
335
command = self.check_command % escaped_attrs
336
except TypeError, error:
337
logger.error(u'Could not format string "%s":'
338
u' %s', self.check_command, error)
339
return True # Try again later
341
logger.debug(u"Starting checker %r for %s",
713
343
self.checker = subprocess.Popen(command,
715
345
shell=True, cwd="/")
716
except OSError as error:
717
logger.error("Failed to start subprocess",
719
self.checker_callback_tag = (gobject.child_watch_add
721
self.checker_callback,
723
# The checker may have completed before the gobject
724
# watch was added. Check for this.
725
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
727
gobject.source_remove(self.checker_callback_tag)
728
self.checker_callback(pid, status, command)
346
self.checker_callback_tag = gobject.child_watch_add\
348
self.checker_callback)
349
except subprocess.OSError, error:
350
logger.error(u"Failed to start subprocess: %s",
729
352
# Re-run this periodically if run by gobject.timeout_add
732
354
def stop_checker(self):
733
355
"""Force the checker process, if any, to stop."""
734
356
if self.checker_callback_tag:
739
361
logger.debug("Stopping checker for %(name)s", vars(self))
741
self.checker.terminate()
363
os.kill(self.checker.pid, signal.SIGTERM)
743
365
#if self.checker.poll() is None:
744
# self.checker.kill()
745
except OSError as error:
366
# os.kill(self.checker.pid, signal.SIGKILL)
367
except OSError, error:
746
368
if error.errno != errno.ESRCH: # No such process
748
370
self.checker = None
751
def dbus_service_property(dbus_interface, signature="v",
752
access="readwrite", byte_arrays=False):
753
"""Decorators for marking methods of a DBusObjectWithProperties to
754
become properties on the D-Bus.
756
The decorated method will be called with no arguments by "Get"
757
and with one argument by "Set".
759
The parameters, where they are supported, are the same as
760
dbus.service.method, except there is only "signature", since the
761
type from Get() and the type sent to Set() is the same.
763
# Encoding deeply encoded byte arrays is not supported yet by the
764
# "Set" method, so we fail early here:
765
if byte_arrays and signature != "ay":
766
raise ValueError("Byte arrays not supported for non-'ay'"
767
" signature {0!r}".format(signature))
769
func._dbus_is_property = True
770
func._dbus_interface = dbus_interface
771
func._dbus_signature = signature
772
func._dbus_access = access
773
func._dbus_name = func.__name__
774
if func._dbus_name.endswith("_dbus_property"):
775
func._dbus_name = func._dbus_name[:-14]
776
func._dbus_get_args_options = {'byte_arrays': byte_arrays }
781
def dbus_interface_annotations(dbus_interface):
782
"""Decorator for marking functions returning interface annotations
786
@dbus_interface_annotations("org.example.Interface")
787
def _foo(self): # Function name does not matter
788
return {"org.freedesktop.DBus.Deprecated": "true",
789
"org.freedesktop.DBus.Property.EmitsChangedSignal":
793
func._dbus_is_interface = True
794
func._dbus_interface = dbus_interface
795
func._dbus_name = dbus_interface
800
def dbus_annotations(annotations):
801
"""Decorator to annotate D-Bus methods, signals or properties
804
@dbus_service_property("org.example.Interface", signature="b",
806
@dbus_annotations({{"org.freedesktop.DBus.Deprecated": "true",
807
"org.freedesktop.DBus.Property."
808
"EmitsChangedSignal": "false"})
809
def Property_dbus_property(self):
810
return dbus.Boolean(False)
813
func._dbus_annotations = annotations
818
class DBusPropertyException(dbus.exceptions.DBusException):
819
"""A base class for D-Bus property-related exceptions
821
def __unicode__(self):
822
return unicode(str(self))
825
class DBusPropertyAccessException(DBusPropertyException):
826
"""A property's access permissions disallows an operation.
831
class DBusPropertyNotFound(DBusPropertyException):
832
"""An attempt was made to access a non-existing property.
837
class DBusObjectWithProperties(dbus.service.Object):
838
"""A D-Bus object with properties.
840
Classes inheriting from this can use the dbus_service_property
841
decorator to expose methods as D-Bus properties. It exposes the
842
standard Get(), Set(), and GetAll() methods on the D-Bus.
846
def _is_dbus_thing(thing):
847
"""Returns a function testing if an attribute is a D-Bus thing
849
If called like _is_dbus_thing("method") it returns a function
850
suitable for use as predicate to inspect.getmembers().
852
return lambda obj: getattr(obj, "_dbus_is_{0}".format(thing),
855
def _get_all_dbus_things(self, thing):
856
"""Returns a generator of (name, attribute) pairs
858
return ((getattr(athing.__get__(self), "_dbus_name",
860
athing.__get__(self))
861
for cls in self.__class__.__mro__
863
inspect.getmembers(cls,
864
self._is_dbus_thing(thing)))
866
def _get_dbus_property(self, interface_name, property_name):
867
"""Returns a bound method if one exists which is a D-Bus
868
property with the specified name and interface.
870
for cls in self.__class__.__mro__:
871
for name, value in (inspect.getmembers
873
self._is_dbus_thing("property"))):
874
if (value._dbus_name == property_name
875
and value._dbus_interface == interface_name):
876
return value.__get__(self)
879
raise DBusPropertyNotFound(self.dbus_object_path + ":"
880
+ interface_name + "."
883
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ss",
885
def Get(self, interface_name, property_name):
886
"""Standard D-Bus property Get() method, see D-Bus standard.
888
prop = self._get_dbus_property(interface_name, property_name)
889
if prop._dbus_access == "write":
890
raise DBusPropertyAccessException(property_name)
892
if not hasattr(value, "variant_level"):
894
return type(value)(value, variant_level=value.variant_level+1)
896
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
897
def Set(self, interface_name, property_name, value):
898
"""Standard D-Bus property Set() method, see D-Bus standard.
900
prop = self._get_dbus_property(interface_name, property_name)
901
if prop._dbus_access == "read":
902
raise DBusPropertyAccessException(property_name)
903
if prop._dbus_get_args_options["byte_arrays"]:
904
# The byte_arrays option is not supported yet on
905
# signatures other than "ay".
906
if prop._dbus_signature != "ay":
908
value = dbus.ByteArray(b''.join(chr(byte)
912
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s",
913
out_signature="a{sv}")
914
def GetAll(self, interface_name):
915
"""Standard D-Bus property GetAll() method, see D-Bus
918
Note: Will not include properties with access="write".
921
for name, prop in self._get_all_dbus_things("property"):
923
and interface_name != prop._dbus_interface):
924
# Interface non-empty but did not match
926
# Ignore write-only properties
927
if prop._dbus_access == "write":
930
if not hasattr(value, "variant_level"):
931
properties[name] = value
933
properties[name] = type(value)(value, variant_level=
934
value.variant_level+1)
935
return dbus.Dictionary(properties, signature="sv")
937
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
939
path_keyword='object_path',
940
connection_keyword='connection')
941
def Introspect(self, object_path, connection):
942
"""Overloading of standard D-Bus method.
944
Inserts property tags and interface annotation tags.
946
xmlstring = dbus.service.Object.Introspect(self, object_path,
949
document = xml.dom.minidom.parseString(xmlstring)
950
def make_tag(document, name, prop):
951
e = document.createElement("property")
952
e.setAttribute("name", name)
953
e.setAttribute("type", prop._dbus_signature)
954
e.setAttribute("access", prop._dbus_access)
956
for if_tag in document.getElementsByTagName("interface"):
958
for tag in (make_tag(document, name, prop)
960
in self._get_all_dbus_things("property")
961
if prop._dbus_interface
962
== if_tag.getAttribute("name")):
963
if_tag.appendChild(tag)
964
# Add annotation tags
965
for typ in ("method", "signal", "property"):
966
for tag in if_tag.getElementsByTagName(typ):
968
for name, prop in (self.
969
_get_all_dbus_things(typ)):
970
if (name == tag.getAttribute("name")
971
and prop._dbus_interface
972
== if_tag.getAttribute("name")):
973
annots.update(getattr
977
for name, value in annots.iteritems():
978
ann_tag = document.createElement(
980
ann_tag.setAttribute("name", name)
981
ann_tag.setAttribute("value", value)
982
tag.appendChild(ann_tag)
983
# Add interface annotation tags
984
for annotation, value in dict(
985
itertools.chain.from_iterable(
986
annotations().iteritems()
987
for name, annotations in
988
self._get_all_dbus_things("interface")
989
if name == if_tag.getAttribute("name")
991
ann_tag = document.createElement("annotation")
992
ann_tag.setAttribute("name", annotation)
993
ann_tag.setAttribute("value", value)
994
if_tag.appendChild(ann_tag)
995
# Add the names to the return values for the
996
# "org.freedesktop.DBus.Properties" methods
997
if (if_tag.getAttribute("name")
998
== "org.freedesktop.DBus.Properties"):
999
for cn in if_tag.getElementsByTagName("method"):
1000
if cn.getAttribute("name") == "Get":
1001
for arg in cn.getElementsByTagName("arg"):
1002
if (arg.getAttribute("direction")
1004
arg.setAttribute("name", "value")
1005
elif cn.getAttribute("name") == "GetAll":
1006
for arg in cn.getElementsByTagName("arg"):
1007
if (arg.getAttribute("direction")
1009
arg.setAttribute("name", "props")
1010
xmlstring = document.toxml("utf-8")
1012
except (AttributeError, xml.dom.DOMException,
1013
xml.parsers.expat.ExpatError) as error:
1014
logger.error("Failed to override Introspection method",
1019
def datetime_to_dbus(dt, variant_level=0):
1020
"""Convert a UTC datetime.datetime() to a D-Bus type."""
1022
return dbus.String("", variant_level = variant_level)
1023
return dbus.String(dt.isoformat(),
1024
variant_level=variant_level)
1027
def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
1028
"""A class decorator; applied to a subclass of
1029
dbus.service.Object, it will add alternate D-Bus attributes with
1030
interface names according to the "alt_interface_names" mapping.
1033
@alternate_dbus_interfaces({"org.example.Interface":
1034
"net.example.AlternateInterface"})
1035
class SampleDBusObject(dbus.service.Object):
1036
@dbus.service.method("org.example.Interface")
1037
def SampleDBusMethod():
1040
The above "SampleDBusMethod" on "SampleDBusObject" will be
1041
reachable via two interfaces: "org.example.Interface" and
1042
"net.example.AlternateInterface", the latter of which will have
1043
its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
1044
"true", unless "deprecate" is passed with a False value.
1046
This works for methods and signals, and also for D-Bus properties
1047
(from DBusObjectWithProperties) and interfaces (from the
1048
dbus_interface_annotations decorator).
1051
for orig_interface_name, alt_interface_name in (
1052
alt_interface_names.iteritems()):
1054
interface_names = set()
1055
# Go though all attributes of the class
1056
for attrname, attribute in inspect.getmembers(cls):
1057
# Ignore non-D-Bus attributes, and D-Bus attributes
1058
# with the wrong interface name
1059
if (not hasattr(attribute, "_dbus_interface")
1060
or not attribute._dbus_interface
1061
.startswith(orig_interface_name)):
1063
# Create an alternate D-Bus interface name based on
1065
alt_interface = (attribute._dbus_interface
1066
.replace(orig_interface_name,
1067
alt_interface_name))
1068
interface_names.add(alt_interface)
1069
# Is this a D-Bus signal?
1070
if getattr(attribute, "_dbus_is_signal", False):
1071
# Extract the original non-method function by
1073
nonmethod_func = (dict(
1074
zip(attribute.func_code.co_freevars,
1075
attribute.__closure__))["func"]
1077
# Create a new, but exactly alike, function
1078
# object, and decorate it to be a new D-Bus signal
1079
# with the alternate D-Bus interface name
1080
new_function = (dbus.service.signal
1082
attribute._dbus_signature)
1083
(types.FunctionType(
1084
nonmethod_func.func_code,
1085
nonmethod_func.func_globals,
1086
nonmethod_func.func_name,
1087
nonmethod_func.func_defaults,
1088
nonmethod_func.func_closure)))
1089
# Copy annotations, if any
1091
new_function._dbus_annotations = (
1092
dict(attribute._dbus_annotations))
1093
except AttributeError:
1095
# Define a creator of a function to call both the
1096
# original and alternate functions, so both the
1097
# original and alternate signals gets sent when
1098
# the function is called
1099
def fixscope(func1, func2):
1100
"""This function is a scope container to pass
1101
func1 and func2 to the "call_both" function
1102
outside of its arguments"""
1103
def call_both(*args, **kwargs):
1104
"""This function will emit two D-Bus
1105
signals by calling func1 and func2"""
1106
func1(*args, **kwargs)
1107
func2(*args, **kwargs)
1109
# Create the "call_both" function and add it to
1111
attr[attrname] = fixscope(attribute, new_function)
1112
# Is this a D-Bus method?
1113
elif getattr(attribute, "_dbus_is_method", False):
1114
# Create a new, but exactly alike, function
1115
# object. Decorate it to be a new D-Bus method
1116
# with the alternate D-Bus interface name. Add it
1118
attr[attrname] = (dbus.service.method
1120
attribute._dbus_in_signature,
1121
attribute._dbus_out_signature)
1123
(attribute.func_code,
1124
attribute.func_globals,
1125
attribute.func_name,
1126
attribute.func_defaults,
1127
attribute.func_closure)))
1128
# Copy annotations, if any
1130
attr[attrname]._dbus_annotations = (
1131
dict(attribute._dbus_annotations))
1132
except AttributeError:
1134
# Is this a D-Bus property?
1135
elif getattr(attribute, "_dbus_is_property", False):
1136
# Create a new, but exactly alike, function
1137
# object, and decorate it to be a new D-Bus
1138
# property with the alternate D-Bus interface
1139
# name. Add it to the class.
1140
attr[attrname] = (dbus_service_property
1142
attribute._dbus_signature,
1143
attribute._dbus_access,
1145
._dbus_get_args_options
1148
(attribute.func_code,
1149
attribute.func_globals,
1150
attribute.func_name,
1151
attribute.func_defaults,
1152
attribute.func_closure)))
1153
# Copy annotations, if any
1155
attr[attrname]._dbus_annotations = (
1156
dict(attribute._dbus_annotations))
1157
except AttributeError:
1159
# Is this a D-Bus interface?
1160
elif getattr(attribute, "_dbus_is_interface", False):
1161
# Create a new, but exactly alike, function
1162
# object. Decorate it to be a new D-Bus interface
1163
# with the alternate D-Bus interface name. Add it
1165
attr[attrname] = (dbus_interface_annotations
1168
(attribute.func_code,
1169
attribute.func_globals,
1170
attribute.func_name,
1171
attribute.func_defaults,
1172
attribute.func_closure)))
1174
# Deprecate all alternate interfaces
1175
iname="_AlternateDBusNames_interface_annotation{0}"
1176
for interface_name in interface_names:
1177
@dbus_interface_annotations(interface_name)
1179
return { "org.freedesktop.DBus.Deprecated":
1181
# Find an unused name
1182
for aname in (iname.format(i)
1183
for i in itertools.count()):
1184
if aname not in attr:
1188
# Replace the class with a new subclass of it with
1189
# methods, signals, etc. as created above.
1190
cls = type(b"{0}Alternate".format(cls.__name__),
1196
@alternate_dbus_interfaces({"se.recompile.Mandos":
1197
"se.bsnet.fukt.Mandos"})
1198
class ClientDBus(Client, DBusObjectWithProperties):
1199
"""A Client class using D-Bus
1202
dbus_object_path: dbus.ObjectPath
1203
bus: dbus.SystemBus()
1206
runtime_expansions = (Client.runtime_expansions
1207
+ ("dbus_object_path",))
1209
# dbus.service.Object doesn't use super(), so we can't either.
1211
def __init__(self, bus = None, *args, **kwargs):
1213
Client.__init__(self, *args, **kwargs)
1214
# Only now, when this client is initialized, can it show up on
1216
client_object_name = unicode(self.name).translate(
1217
{ord("."): ord("_"),
1218
ord("-"): ord("_")})
1219
self.dbus_object_path = (dbus.ObjectPath
1220
("/clients/" + client_object_name))
1221
DBusObjectWithProperties.__init__(self, self.bus,
1222
self.dbus_object_path)
1224
def notifychangeproperty(transform_func,
1225
dbus_name, type_func=lambda x: x,
1227
""" Modify a variable so that it's a property which announces
1228
its changes to DBus.
1230
transform_fun: Function that takes a value and a variant_level
1231
and transforms it to a D-Bus type.
1232
dbus_name: D-Bus name of the variable
1233
type_func: Function that transform the value before sending it
1234
to the D-Bus. Default: no transform
1235
variant_level: D-Bus variant level. Default: 1
1237
attrname = "_{0}".format(dbus_name)
1238
def setter(self, value):
1239
if hasattr(self, "dbus_object_path"):
1240
if (not hasattr(self, attrname) or
1241
type_func(getattr(self, attrname, None))
1242
!= type_func(value)):
1243
dbus_value = transform_func(type_func(value),
1246
self.PropertyChanged(dbus.String(dbus_name),
1248
setattr(self, attrname, value)
1250
return property(lambda self: getattr(self, attrname), setter)
1252
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1253
approvals_pending = notifychangeproperty(dbus.Boolean,
1256
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1257
last_enabled = notifychangeproperty(datetime_to_dbus,
1259
checker = notifychangeproperty(dbus.Boolean, "CheckerRunning",
1260
type_func = lambda checker:
1261
checker is not None)
1262
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1264
last_checker_status = notifychangeproperty(dbus.Int16,
1265
"LastCheckerStatus")
1266
last_approval_request = notifychangeproperty(
1267
datetime_to_dbus, "LastApprovalRequest")
1268
approved_by_default = notifychangeproperty(dbus.Boolean,
1269
"ApprovedByDefault")
1270
approval_delay = notifychangeproperty(dbus.UInt64,
1273
timedelta_to_milliseconds)
1274
approval_duration = notifychangeproperty(
1275
dbus.UInt64, "ApprovalDuration",
1276
type_func = timedelta_to_milliseconds)
1277
host = notifychangeproperty(dbus.String, "Host")
1278
timeout = notifychangeproperty(dbus.UInt64, "Timeout",
1280
timedelta_to_milliseconds)
1281
extended_timeout = notifychangeproperty(
1282
dbus.UInt64, "ExtendedTimeout",
1283
type_func = timedelta_to_milliseconds)
1284
interval = notifychangeproperty(dbus.UInt64,
1287
timedelta_to_milliseconds)
1288
checker_command = notifychangeproperty(dbus.String, "Checker")
1290
del notifychangeproperty
1292
def __del__(self, *args, **kwargs):
1294
self.remove_from_connection()
1297
if hasattr(DBusObjectWithProperties, "__del__"):
1298
DBusObjectWithProperties.__del__(self, *args, **kwargs)
1299
Client.__del__(self, *args, **kwargs)
1301
def checker_callback(self, pid, condition, command,
1303
self.checker_callback_tag = None
1305
if os.WIFEXITED(condition):
1306
exitstatus = os.WEXITSTATUS(condition)
1308
self.CheckerCompleted(dbus.Int16(exitstatus),
1309
dbus.Int64(condition),
1310
dbus.String(command))
1313
self.CheckerCompleted(dbus.Int16(-1),
1314
dbus.Int64(condition),
1315
dbus.String(command))
1317
return Client.checker_callback(self, pid, condition, command,
1320
def start_checker(self, *args, **kwargs):
1321
old_checker = self.checker
1322
if self.checker is not None:
1323
old_checker_pid = self.checker.pid
1325
old_checker_pid = None
1326
r = Client.start_checker(self, *args, **kwargs)
1327
# Only if new checker process was started
1328
if (self.checker is not None
1329
and old_checker_pid != self.checker.pid):
1331
self.CheckerStarted(self.current_checker_command)
1334
def _reset_approved(self):
1335
self.approved = None
1338
def approve(self, value=True):
1339
self.approved = value
1340
gobject.timeout_add(timedelta_to_milliseconds
1341
(self.approval_duration),
1342
self._reset_approved)
1343
self.send_changedstate()
1345
## D-Bus methods, signals & properties
1346
_interface = "se.recompile.Mandos.Client"
1350
@dbus_interface_annotations(_interface)
1352
return { "org.freedesktop.DBus.Property.EmitsChangedSignal":
1357
# CheckerCompleted - signal
1358
@dbus.service.signal(_interface, signature="nxs")
1359
def CheckerCompleted(self, exitcode, waitstatus, command):
1363
# CheckerStarted - signal
1364
@dbus.service.signal(_interface, signature="s")
1365
def CheckerStarted(self, command):
1369
# PropertyChanged - signal
1370
@dbus.service.signal(_interface, signature="sv")
1371
def PropertyChanged(self, property, value):
1375
# GotSecret - signal
1376
@dbus.service.signal(_interface)
1377
def GotSecret(self):
1379
Is sent after a successful transfer of secret from the Mandos
1380
server to mandos-client
1385
@dbus.service.signal(_interface, signature="s")
1386
def Rejected(self, reason):
1390
# NeedApproval - signal
1391
@dbus.service.signal(_interface, signature="tb")
1392
def NeedApproval(self, timeout, default):
1394
return self.need_approval()
1399
@dbus.service.method(_interface, in_signature="b")
1400
def Approve(self, value):
1403
# CheckedOK - method
1404
@dbus.service.method(_interface)
1405
def CheckedOK(self):
1409
@dbus.service.method(_interface)
1414
# StartChecker - method
1415
@dbus.service.method(_interface)
1416
def StartChecker(self):
1418
self.start_checker()
1421
@dbus.service.method(_interface)
1426
# StopChecker - method
1427
@dbus.service.method(_interface)
1428
def StopChecker(self):
1433
# ApprovalPending - property
1434
@dbus_service_property(_interface, signature="b", access="read")
1435
def ApprovalPending_dbus_property(self):
1436
return dbus.Boolean(bool(self.approvals_pending))
1438
# ApprovedByDefault - property
1439
@dbus_service_property(_interface, signature="b",
1441
def ApprovedByDefault_dbus_property(self, value=None):
1442
if value is None: # get
1443
return dbus.Boolean(self.approved_by_default)
1444
self.approved_by_default = bool(value)
1446
# ApprovalDelay - property
1447
@dbus_service_property(_interface, signature="t",
1449
def ApprovalDelay_dbus_property(self, value=None):
1450
if value is None: # get
1451
return dbus.UInt64(self.approval_delay_milliseconds())
1452
self.approval_delay = datetime.timedelta(0, 0, 0, value)
1454
# ApprovalDuration - property
1455
@dbus_service_property(_interface, signature="t",
1457
def ApprovalDuration_dbus_property(self, value=None):
1458
if value is None: # get
1459
return dbus.UInt64(timedelta_to_milliseconds(
1460
self.approval_duration))
1461
self.approval_duration = datetime.timedelta(0, 0, 0, value)
1464
@dbus_service_property(_interface, signature="s", access="read")
1465
def Name_dbus_property(self):
1466
return dbus.String(self.name)
1468
# Fingerprint - property
1469
@dbus_service_property(_interface, signature="s", access="read")
1470
def Fingerprint_dbus_property(self):
1471
return dbus.String(self.fingerprint)
1474
@dbus_service_property(_interface, signature="s",
1476
def Host_dbus_property(self, value=None):
1477
if value is None: # get
1478
return dbus.String(self.host)
1479
self.host = unicode(value)
1481
# Created - property
1482
@dbus_service_property(_interface, signature="s", access="read")
1483
def Created_dbus_property(self):
1484
return datetime_to_dbus(self.created)
1486
# LastEnabled - property
1487
@dbus_service_property(_interface, signature="s", access="read")
1488
def LastEnabled_dbus_property(self):
1489
return datetime_to_dbus(self.last_enabled)
1491
# Enabled - property
1492
@dbus_service_property(_interface, signature="b",
1494
def Enabled_dbus_property(self, value=None):
1495
if value is None: # get
1496
return dbus.Boolean(self.enabled)
1502
# LastCheckedOK - property
1503
@dbus_service_property(_interface, signature="s",
1505
def LastCheckedOK_dbus_property(self, value=None):
1506
if value is not None:
1509
return datetime_to_dbus(self.last_checked_ok)
1511
# LastCheckerStatus - property
1512
@dbus_service_property(_interface, signature="n",
1514
def LastCheckerStatus_dbus_property(self):
1515
return dbus.Int16(self.last_checker_status)
1517
# Expires - property
1518
@dbus_service_property(_interface, signature="s", access="read")
1519
def Expires_dbus_property(self):
1520
return datetime_to_dbus(self.expires)
1522
# LastApprovalRequest - property
1523
@dbus_service_property(_interface, signature="s", access="read")
1524
def LastApprovalRequest_dbus_property(self):
1525
return datetime_to_dbus(self.last_approval_request)
1527
# Timeout - property
1528
@dbus_service_property(_interface, signature="t",
1530
def Timeout_dbus_property(self, value=None):
1531
if value is None: # get
1532
return dbus.UInt64(self.timeout_milliseconds())
1533
old_timeout = self.timeout
1534
self.timeout = datetime.timedelta(0, 0, 0, value)
1535
# Reschedule disabling
1537
now = datetime.datetime.utcnow()
1538
self.expires += self.timeout - old_timeout
1539
if self.expires <= now:
1540
# The timeout has passed
1543
if (getattr(self, "disable_initiator_tag", None)
1546
gobject.source_remove(self.disable_initiator_tag)
1547
self.disable_initiator_tag = (
1548
gobject.timeout_add(
1549
timedelta_to_milliseconds(self.expires - now),
1552
# ExtendedTimeout - property
1553
@dbus_service_property(_interface, signature="t",
1555
def ExtendedTimeout_dbus_property(self, value=None):
1556
if value is None: # get
1557
return dbus.UInt64(self.extended_timeout_milliseconds())
1558
self.extended_timeout = datetime.timedelta(0, 0, 0, value)
1560
# Interval - property
1561
@dbus_service_property(_interface, signature="t",
1563
def Interval_dbus_property(self, value=None):
1564
if value is None: # get
1565
return dbus.UInt64(self.interval_milliseconds())
1566
self.interval = datetime.timedelta(0, 0, 0, value)
1567
if getattr(self, "checker_initiator_tag", None) is None:
1570
# Reschedule checker run
1571
gobject.source_remove(self.checker_initiator_tag)
1572
self.checker_initiator_tag = (gobject.timeout_add
1573
(value, self.start_checker))
1574
self.start_checker() # Start one now, too
1576
# Checker - property
1577
@dbus_service_property(_interface, signature="s",
1579
def Checker_dbus_property(self, value=None):
1580
if value is None: # get
1581
return dbus.String(self.checker_command)
1582
self.checker_command = unicode(value)
1584
# CheckerRunning - property
1585
@dbus_service_property(_interface, signature="b",
1587
def CheckerRunning_dbus_property(self, value=None):
1588
if value is None: # get
1589
return dbus.Boolean(self.checker is not None)
1591
self.start_checker()
1595
# ObjectPath - property
1596
@dbus_service_property(_interface, signature="o", access="read")
1597
def ObjectPath_dbus_property(self):
1598
return self.dbus_object_path # is already a dbus.ObjectPath
1601
@dbus_service_property(_interface, signature="ay",
1602
access="write", byte_arrays=True)
1603
def Secret_dbus_property(self, value):
1604
self.secret = str(value)
1609
class ProxyClient(object):
1610
def __init__(self, child_pipe, fpr, address):
1611
self._pipe = child_pipe
1612
self._pipe.send(('init', fpr, address))
1613
if not self._pipe.recv():
1616
def __getattribute__(self, name):
1618
return super(ProxyClient, self).__getattribute__(name)
1619
self._pipe.send(('getattr', name))
1620
data = self._pipe.recv()
1621
if data[0] == 'data':
1623
if data[0] == 'function':
1624
def func(*args, **kwargs):
1625
self._pipe.send(('funcall', name, args, kwargs))
1626
return self._pipe.recv()[1]
1629
def __setattr__(self, name, value):
1631
return super(ProxyClient, self).__setattr__(name, value)
1632
self._pipe.send(('setattr', name, value))
1635
class ClientHandler(socketserver.BaseRequestHandler, object):
1636
"""A class to handle client connections.
1638
Instantiated once for each connection to handle it.
371
def still_valid(self):
372
"""Has the timeout not yet passed for this client?"""
373
now = datetime.datetime.now()
374
if self.last_checked_ok is None:
375
return now < (self.created + self.timeout)
377
return now < (self.last_checked_ok + self.timeout)
380
def peer_certificate(session):
381
"Return the peer's OpenPGP certificate as a bytestring"
382
# If not an OpenPGP certificate...
383
if gnutls.library.functions.gnutls_certificate_type_get\
384
(session._c_object) \
385
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
386
# ...do the normal thing
387
return session.peer_certificate
388
list_size = ctypes.c_uint()
389
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
390
(session._c_object, ctypes.byref(list_size))
391
if list_size.value == 0:
394
return ctypes.string_at(cert.data, cert.size)
397
def fingerprint(openpgp):
398
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
399
# New empty GnuTLS certificate
400
crt = gnutls.library.types.gnutls_openpgp_crt_t()
401
gnutls.library.functions.gnutls_openpgp_crt_init\
403
# New GnuTLS "datum" with the OpenPGP public key
404
datum = gnutls.library.types.gnutls_datum_t\
405
(ctypes.cast(ctypes.c_char_p(openpgp),
406
ctypes.POINTER(ctypes.c_ubyte)),
407
ctypes.c_uint(len(openpgp)))
408
# Import the OpenPGP public key into the certificate
409
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
412
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
413
# New buffer for the fingerprint
414
buffer = ctypes.create_string_buffer(20)
415
buffer_length = ctypes.c_size_t()
416
# Get the fingerprint from the certificate into the buffer
417
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
418
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
419
# Deinit the certificate
420
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
421
# Convert the buffer to a Python bytestring
422
fpr = ctypes.string_at(buffer, buffer_length.value)
423
# Convert the bytestring to hexadecimal notation
424
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
428
class tcp_handler(SocketServer.BaseRequestHandler, object):
429
"""A TCP request handler class.
430
Instantiated by IPv6_TCPServer for each request to handle it.
1639
431
Note: This will run in its own forked process."""
1641
433
def handle(self):
1642
with contextlib.closing(self.server.child_pipe) as child_pipe:
1643
logger.info("TCP connection from: %s",
1644
unicode(self.client_address))
1645
logger.debug("Pipe FD: %d",
1646
self.server.child_pipe.fileno())
1648
session = (gnutls.connection
1649
.ClientSession(self.request,
1651
.X509Credentials()))
1653
# Note: gnutls.connection.X509Credentials is really a
1654
# generic GnuTLS certificate credentials object so long as
1655
# no X.509 keys are added to it. Therefore, we can use it
1656
# here despite using OpenPGP certificates.
1658
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
1659
# "+AES-256-CBC", "+SHA1",
1660
# "+COMP-NULL", "+CTYPE-OPENPGP",
1662
# Use a fallback default, since this MUST be set.
1663
priority = self.server.gnutls_priority
1664
if priority is None:
1666
(gnutls.library.functions
1667
.gnutls_priority_set_direct(session._c_object,
1670
# Start communication using the Mandos protocol
1671
# Get protocol number
1672
line = self.request.makefile().readline()
1673
logger.debug("Protocol version: %r", line)
1675
if int(line.strip().split()[0]) > 1:
1677
except (ValueError, IndexError, RuntimeError) as error:
1678
logger.error("Unknown protocol version: %s", error)
1681
# Start GnuTLS connection
1684
except gnutls.errors.GNUTLSError as error:
1685
logger.warning("Handshake failed: %s", error)
1686
# Do not run session.bye() here: the session is not
1687
# established. Just abandon the request.
1689
logger.debug("Handshake succeeded")
1691
approval_required = False
1694
fpr = self.fingerprint(self.peer_certificate
1697
gnutls.errors.GNUTLSError) as error:
1698
logger.warning("Bad certificate: %s", error)
1700
logger.debug("Fingerprint: %s", fpr)
1703
client = ProxyClient(child_pipe, fpr,
1704
self.client_address)
1708
if client.approval_delay:
1709
delay = client.approval_delay
1710
client.approvals_pending += 1
1711
approval_required = True
1714
if not client.enabled:
1715
logger.info("Client %s is disabled",
1717
if self.server.use_dbus:
1719
client.Rejected("Disabled")
1722
if client.approved or not client.approval_delay:
1723
#We are approved or approval is disabled
1725
elif client.approved is None:
1726
logger.info("Client %s needs approval",
1728
if self.server.use_dbus:
1730
client.NeedApproval(
1731
client.approval_delay_milliseconds(),
1732
client.approved_by_default)
1734
logger.warning("Client %s was not approved",
1736
if self.server.use_dbus:
1738
client.Rejected("Denied")
1741
#wait until timeout or approved
1742
time = datetime.datetime.now()
1743
client.changedstate.acquire()
1744
client.changedstate.wait(
1745
float(timedelta_to_milliseconds(delay)
1747
client.changedstate.release()
1748
time2 = datetime.datetime.now()
1749
if (time2 - time) >= delay:
1750
if not client.approved_by_default:
1751
logger.warning("Client %s timed out while"
1752
" waiting for approval",
1754
if self.server.use_dbus:
1756
client.Rejected("Approval timed out")
1761
delay -= time2 - time
1764
while sent_size < len(client.secret):
1766
sent = session.send(client.secret[sent_size:])
1767
except gnutls.errors.GNUTLSError as error:
1768
logger.warning("gnutls send failed",
1771
logger.debug("Sent: %d, remaining: %d",
1772
sent, len(client.secret)
1773
- (sent_size + sent))
1776
logger.info("Sending secret to %s", client.name)
1777
# bump the timeout using extended_timeout
1778
client.bump_timeout(client.extended_timeout)
1779
if self.server.use_dbus:
1784
if approval_required:
1785
client.approvals_pending -= 1
1788
except gnutls.errors.GNUTLSError as error:
1789
logger.warning("GnuTLS bye failed",
1793
def peer_certificate(session):
1794
"Return the peer's OpenPGP certificate as a bytestring"
1795
# If not an OpenPGP certificate...
1796
if (gnutls.library.functions
1797
.gnutls_certificate_type_get(session._c_object)
1798
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1799
# ...do the normal thing
1800
return session.peer_certificate
1801
list_size = ctypes.c_uint(1)
1802
cert_list = (gnutls.library.functions
1803
.gnutls_certificate_get_peers
1804
(session._c_object, ctypes.byref(list_size)))
1805
if not bool(cert_list) and list_size.value != 0:
1806
raise gnutls.errors.GNUTLSError("error getting peer"
1808
if list_size.value == 0:
1811
return ctypes.string_at(cert.data, cert.size)
1814
def fingerprint(openpgp):
1815
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1816
# New GnuTLS "datum" with the OpenPGP public key
1817
datum = (gnutls.library.types
1818
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1821
ctypes.c_uint(len(openpgp))))
1822
# New empty GnuTLS certificate
1823
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1824
(gnutls.library.functions
1825
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1826
# Import the OpenPGP public key into the certificate
1827
(gnutls.library.functions
1828
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1829
gnutls.library.constants
1830
.GNUTLS_OPENPGP_FMT_RAW))
1831
# Verify the self signature in the key
1832
crtverify = ctypes.c_uint()
1833
(gnutls.library.functions
1834
.gnutls_openpgp_crt_verify_self(crt, 0,
1835
ctypes.byref(crtverify)))
1836
if crtverify.value != 0:
1837
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1838
raise (gnutls.errors.CertificateSecurityError
1840
# New buffer for the fingerprint
1841
buf = ctypes.create_string_buffer(20)
1842
buf_len = ctypes.c_size_t()
1843
# Get the fingerprint from the certificate into the buffer
1844
(gnutls.library.functions
1845
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1846
ctypes.byref(buf_len)))
1847
# Deinit the certificate
1848
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1849
# Convert the buffer to a Python bytestring
1850
fpr = ctypes.string_at(buf, buf_len.value)
1851
# Convert the bytestring to hexadecimal notation
1852
hex_fpr = binascii.hexlify(fpr).upper()
1856
class MultiprocessingMixIn(object):
1857
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
1858
def sub_process_main(self, request, address):
1860
self.finish_request(request, address)
1862
self.handle_error(request, address)
1863
self.close_request(request)
1865
def process_request(self, request, address):
1866
"""Start a new process to process the request."""
1867
proc = multiprocessing.Process(target = self.sub_process_main,
1868
args = (request, address))
1873
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
1874
""" adds a pipe to the MixIn """
1875
def process_request(self, request, client_address):
1876
"""Overrides and wraps the original process_request().
1878
This function creates a new pipe in self.pipe
1880
parent_pipe, self.child_pipe = multiprocessing.Pipe()
1882
proc = MultiprocessingMixIn.process_request(self, request,
1884
self.child_pipe.close()
1885
self.add_pipe(parent_pipe, proc)
1887
def add_pipe(self, parent_pipe, proc):
1888
"""Dummy function; override as necessary"""
1889
raise NotImplementedError
1892
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1893
socketserver.TCPServer, object):
1894
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
434
logger.debug(u"TCP connection from: %s",
435
unicode(self.client_address))
436
session = gnutls.connection.ClientSession\
437
(self.request, gnutls.connection.X509Credentials())
438
# Note: gnutls.connection.X509Credentials is really a generic
439
# GnuTLS certificate credentials object so long as no X.509
440
# keys are added to it. Therefore, we can use it here despite
441
# using OpenPGP certificates.
443
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
444
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
446
priority = "NORMAL" # Fallback default, since this
448
if self.server.settings["priority"]:
449
priority = self.server.settings["priority"]
450
gnutls.library.functions.gnutls_priority_set_direct\
451
(session._c_object, priority, None);
455
except gnutls.errors.GNUTLSError, error:
456
logger.debug(u"Handshake failed: %s", error)
457
# Do not run session.bye() here: the session is not
458
# established. Just abandon the request.
461
fpr = fingerprint(peer_certificate(session))
462
except (TypeError, gnutls.errors.GNUTLSError), error:
463
logger.debug(u"Bad certificate: %s", error)
466
logger.debug(u"Fingerprint: %s", fpr)
468
for c in self.server.clients:
469
if c.fingerprint == fpr:
473
logger.debug(u"Client not found for fingerprint: %s", fpr)
476
# Have to check if client.still_valid(), since it is possible
477
# that the client timed out while establishing the GnuTLS
479
if not client.still_valid():
480
logger.debug(u"Client %(name)s is invalid", vars(client))
484
while sent_size < len(client.secret):
485
sent = session.send(client.secret[sent_size:])
486
logger.debug(u"Sent: %d, remaining: %d",
487
sent, len(client.secret)
488
- (sent_size + sent))
493
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
494
"""IPv6 TCP server. Accepts 'None' as address and/or port.
1897
enabled: Boolean; whether this server is activated yet
1898
interface: None or a network interface name (string)
1899
use_ipv6: Boolean; to use IPv6 or not
496
settings: Server settings
497
clients: Set() of Client objects
1901
def __init__(self, server_address, RequestHandlerClass,
1902
interface=None, use_ipv6=True, socketfd=None):
1903
"""If socketfd is set, use that file descriptor instead of
1904
creating a new one with socket.socket().
1906
self.interface = interface
1908
self.address_family = socket.AF_INET6
1909
if socketfd is not None:
1910
# Save the file descriptor
1911
self.socketfd = socketfd
1912
# Save the original socket.socket() function
1913
self.socket_socket = socket.socket
1914
# To implement --socket, we monkey patch socket.socket.
1916
# (When socketserver.TCPServer is a new-style class, we
1917
# could make self.socket into a property instead of monkey
1918
# patching socket.socket.)
1920
# Create a one-time-only replacement for socket.socket()
1921
@functools.wraps(socket.socket)
1922
def socket_wrapper(*args, **kwargs):
1923
# Restore original function so subsequent calls are
1925
socket.socket = self.socket_socket
1926
del self.socket_socket
1927
# This time only, return a new socket object from the
1928
# saved file descriptor.
1929
return socket.fromfd(self.socketfd, *args, **kwargs)
1930
# Replace socket.socket() function with wrapper
1931
socket.socket = socket_wrapper
1932
# The socketserver.TCPServer.__init__ will call
1933
# socket.socket(), which might be our replacement,
1934
# socket_wrapper(), if socketfd was set.
1935
socketserver.TCPServer.__init__(self, server_address,
1936
RequestHandlerClass)
499
address_family = socket.AF_INET6
500
def __init__(self, *args, **kwargs):
501
if "settings" in kwargs:
502
self.settings = kwargs["settings"]
503
del kwargs["settings"]
504
if "clients" in kwargs:
505
self.clients = kwargs["clients"]
506
del kwargs["clients"]
507
return super(type(self), self).__init__(*args, **kwargs)
1938
508
def server_bind(self):
1939
509
"""This overrides the normal server_bind() function
1940
510
to bind to an interface if one was specified, and also NOT to
1941
511
bind to an address or port if they were not specified."""
1942
if self.interface is not None:
1943
if SO_BINDTODEVICE is None:
1944
logger.error("SO_BINDTODEVICE does not exist;"
1945
" cannot bind to interface %s",
1949
self.socket.setsockopt(socket.SOL_SOCKET,
1953
except socket.error as error:
1954
if error.errno == errno.EPERM:
1955
logger.error("No permission to"
1956
" bind to interface %s",
1958
elif error.errno == errno.ENOPROTOOPT:
1959
logger.error("SO_BINDTODEVICE not available;"
1960
" cannot bind to interface %s",
1962
elif error.errno == errno.ENODEV:
1963
logger.error("Interface %s does not"
1964
" exist, cannot bind",
512
if self.settings["interface"]:
513
# 25 is from /usr/include/asm-i486/socket.h
514
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
516
self.socket.setsockopt(socket.SOL_SOCKET,
518
self.settings["interface"])
519
except socket.error, error:
520
if error[0] == errno.EPERM:
521
logger.warning(u"No permission to"
522
u" bind to interface %s",
523
self.settings["interface"])
1968
526
# Only bind(2) the socket if we really need to.
1969
527
if self.server_address[0] or self.server_address[1]:
1970
528
if not self.server_address[0]:
1971
if self.address_family == socket.AF_INET6:
1972
any_address = "::" # in6addr_any
1974
any_address = socket.INADDR_ANY
1975
self.server_address = (any_address,
530
self.server_address = (in6addr_any,
1976
531
self.server_address[1])
1977
elif not self.server_address[1]:
532
elif self.server_address[1] is None:
1978
533
self.server_address = (self.server_address[0],
1980
# if self.interface:
1981
# self.server_address = (self.server_address[0],
1986
return socketserver.TCPServer.server_bind(self)
1989
class MandosServer(IPv6_TCPServer):
1993
clients: set of Client objects
1994
gnutls_priority GnuTLS priority string
1995
use_dbus: Boolean; to emit D-Bus signals or not
1997
Assumes a gobject.MainLoop event loop.
1999
def __init__(self, server_address, RequestHandlerClass,
2000
interface=None, use_ipv6=True, clients=None,
2001
gnutls_priority=None, use_dbus=True, socketfd=None):
2002
self.enabled = False
2003
self.clients = clients
2004
if self.clients is None:
2006
self.use_dbus = use_dbus
2007
self.gnutls_priority = gnutls_priority
2008
IPv6_TCPServer.__init__(self, server_address,
2009
RequestHandlerClass,
2010
interface = interface,
2011
use_ipv6 = use_ipv6,
2012
socketfd = socketfd)
2013
def server_activate(self):
2015
return socketserver.TCPServer.server_activate(self)
2020
def add_pipe(self, parent_pipe, proc):
2021
# Call "handle_ipc" for both data and EOF events
2022
gobject.io_add_watch(parent_pipe.fileno(),
2023
gobject.IO_IN | gobject.IO_HUP,
2024
functools.partial(self.handle_ipc,
2029
def handle_ipc(self, source, condition, parent_pipe=None,
2030
proc = None, client_object=None):
2031
# error, or the other end of multiprocessing.Pipe has closed
2032
if condition & (gobject.IO_ERR | gobject.IO_HUP):
2033
# Wait for other process to exit
2037
# Read a request from the child
2038
request = parent_pipe.recv()
2039
command = request[0]
2041
if command == 'init':
2043
address = request[2]
2045
for c in self.clients.itervalues():
2046
if c.fingerprint == fpr:
2050
logger.info("Client not found for fingerprint: %s, ad"
2051
"dress: %s", fpr, address)
2054
mandos_dbus_service.ClientNotFound(fpr,
2056
parent_pipe.send(False)
2059
gobject.io_add_watch(parent_pipe.fileno(),
2060
gobject.IO_IN | gobject.IO_HUP,
2061
functools.partial(self.handle_ipc,
2067
parent_pipe.send(True)
2068
# remove the old hook in favor of the new above hook on
2071
if command == 'funcall':
2072
funcname = request[1]
2076
parent_pipe.send(('data', getattr(client_object,
2080
if command == 'getattr':
2081
attrname = request[1]
2082
if callable(client_object.__getattribute__(attrname)):
2083
parent_pipe.send(('function',))
2085
parent_pipe.send(('data', client_object
2086
.__getattribute__(attrname)))
2088
if command == 'setattr':
2089
attrname = request[1]
2091
setattr(client_object, attrname, value)
535
return super(type(self), self).server_bind()
2096
538
def string_to_delta(interval):
2097
539
"""Parse a string and return a datetime.timedelta
2099
541
>>> string_to_delta('7d')
2100
542
datetime.timedelta(7)
2101
543
>>> string_to_delta('60s')
2218
687
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2219
688
"servicename": "Mandos",
2225
"statedir": "/var/lib/mandos"
2228
691
# Parse config file for server-global settings
2229
server_config = configparser.SafeConfigParser(server_defaults)
692
server_config = ConfigParser.SafeConfigParser(server_defaults)
2230
693
del server_defaults
2231
server_config.read(os.path.join(options.configdir,
694
server_config.read(os.path.join(options.configdir, "server.conf"))
695
server_section = "server"
2233
696
# Convert the SafeConfigParser object to a dict
2234
server_settings = server_config.defaults()
2235
# Use the appropriate methods on the non-string config options
2236
for option in ("debug", "use_dbus", "use_ipv6"):
2237
server_settings[option] = server_config.getboolean("DEFAULT",
2239
if server_settings["port"]:
2240
server_settings["port"] = server_config.getint("DEFAULT",
2242
if server_settings["socket"]:
2243
server_settings["socket"] = server_config.getint("DEFAULT",
2245
# Later, stdin will, and stdout, and stderr might, be dup'ed
2246
# over with an opened os.devnull. But we don't want this to
2247
# happen with a supplied network socket.
2248
if 0 <= server_settings["socket"] <= 2:
2249
server_settings["socket"] = os.dup(server_settings
697
server_settings = dict(server_config.items(server_section))
698
# Use getboolean on the boolean config option
699
server_settings["debug"] = server_config.getboolean\
700
(server_section, "debug")
2251
701
del server_config
2253
703
# Override the settings from the config file with command line
2254
704
# options, if set.
2255
705
for option in ("interface", "address", "port", "debug",
2256
"priority", "servicename", "configdir",
2257
"use_dbus", "use_ipv6", "debuglevel", "restore",
2258
"statedir", "socket"):
706
"priority", "servicename", "configdir"):
2259
707
value = getattr(options, option)
2260
708
if value is not None:
2261
709
server_settings[option] = value
2263
# Force all strings to be unicode
2264
for option in server_settings.keys():
2265
if type(server_settings[option]) is str:
2266
server_settings[option] = unicode(server_settings[option])
2267
711
# Now we have our good server settings in "server_settings"
2269
##################################################################
2272
debug = server_settings["debug"]
2273
debuglevel = server_settings["debuglevel"]
2274
use_dbus = server_settings["use_dbus"]
2275
use_ipv6 = server_settings["use_ipv6"]
2276
stored_state_path = os.path.join(server_settings["statedir"],
2280
initlogger(debug, logging.DEBUG)
2285
level = getattr(logging, debuglevel.upper())
2286
initlogger(debug, level)
2288
if server_settings["servicename"] != "Mandos":
2289
syslogger.setFormatter(logging.Formatter
2290
('Mandos ({0}) [%(process)d]:'
2291
' %(levelname)s: %(message)s'
2292
.format(server_settings
2295
713
# Parse config file with clients
2296
client_config = configparser.SafeConfigParser(Client
714
client_defaults = { "timeout": "1h",
716
"checker": "fping -q -- %%(fqdn)s",
718
client_config = ConfigParser.SafeConfigParser(client_defaults)
2298
719
client_config.read(os.path.join(server_settings["configdir"],
2299
720
"clients.conf"))
2301
global mandos_dbus_service
2302
mandos_dbus_service = None
2304
tcp_server = MandosServer((server_settings["address"],
2305
server_settings["port"]),
2307
interface=(server_settings["interface"]
2311
server_settings["priority"],
2313
socketfd=(server_settings["socket"]
2316
pidfilename = "/var/run/mandos.pid"
2318
pidfile = open(pidfilename, "w")
2319
except IOError as e:
2320
logger.error("Could not open file %r", pidfilename,
2323
for name in ("_mandos", "mandos", "nobody"):
2325
uid = pwd.getpwnam(name).pw_uid
2326
gid = pwd.getpwnam(name).pw_gid
2336
except OSError as error:
2337
if error.errno != errno.EPERM:
2341
# Enable all possible GnuTLS debugging
2343
# "Use a log level over 10 to enable all debugging options."
2345
gnutls.library.functions.gnutls_global_set_log_level(11)
2347
@gnutls.library.types.gnutls_log_func
2348
def debug_gnutls(level, string):
2349
logger.debug("GnuTLS: %s", string[:-1])
2351
(gnutls.library.functions
2352
.gnutls_global_set_log_function(debug_gnutls))
2354
# Redirect stdin so all checkers get /dev/null
2355
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2356
os.dup2(null, sys.stdin.fileno())
2360
# Need to fork before connecting to D-Bus
2362
# Close all input and output, do double fork, etc.
2365
gobject.threads_init()
723
service = AvahiService(name = server_settings["servicename"],
724
type = "_mandos._tcp", );
725
if server_settings["interface"]:
726
service.interface = if_nametoindex(server_settings["interface"])
2367
728
global main_loop
2368
731
# From the Avahi example code
2369
DBusGMainLoop(set_as_default=True)
732
DBusGMainLoop(set_as_default=True )
2370
733
main_loop = gobject.MainLoop()
2371
734
bus = dbus.SystemBus()
735
server = dbus.Interface(
736
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
737
avahi.DBUS_INTERFACE_SERVER )
2372
738
# End of Avahi example code
2375
bus_name = dbus.service.BusName("se.recompile.Mandos",
2376
bus, do_not_queue=True)
2377
old_bus_name = (dbus.service.BusName
2378
("se.bsnet.fukt.Mandos", bus,
2380
except dbus.exceptions.NameExistsException as e:
2381
logger.error("Disabling D-Bus:", exc_info=e)
2383
server_settings["use_dbus"] = False
2384
tcp_server.use_dbus = False
2385
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2386
service = AvahiServiceToSyslog(name =
2387
server_settings["servicename"],
2388
servicetype = "_mandos._tcp",
2389
protocol = protocol, bus = bus)
2390
if server_settings["interface"]:
2391
service.interface = (if_nametoindex
2392
(str(server_settings["interface"])))
2394
global multiprocessing_manager
2395
multiprocessing_manager = multiprocessing.Manager()
2397
client_class = Client
2399
client_class = functools.partial(ClientDBus, bus = bus)
2401
client_settings = Client.config_parser(client_config)
2402
old_client_settings = {}
2405
# Get client data and settings from last running state.
2406
if server_settings["restore"]:
2408
with open(stored_state_path, "rb") as stored_state:
2409
clients_data, old_client_settings = (pickle.load
2411
os.remove(stored_state_path)
2412
except IOError as e:
2413
if e.errno == errno.ENOENT:
2414
logger.warning("Could not load persistent state: {0}"
2415
.format(os.strerror(e.errno)))
2417
logger.critical("Could not load persistent state:",
2420
except EOFError as e:
2421
logger.warning("Could not load persistent state: "
2422
"EOFError:", exc_info=e)
2424
with PGPEngine() as pgp:
2425
for client_name, client in clients_data.iteritems():
2426
# Decide which value to use after restoring saved state.
2427
# We have three different values: Old config file,
2428
# new config file, and saved state.
2429
# New config value takes precedence if it differs from old
2430
# config value, otherwise use saved state.
2431
for name, value in client_settings[client_name].items():
2433
# For each value in new config, check if it
2434
# differs from the old config value (Except for
2435
# the "secret" attribute)
2436
if (name != "secret" and
2437
value != old_client_settings[client_name]
2439
client[name] = value
2443
# Clients who has passed its expire date can still be
2444
# enabled if its last checker was successful. Clients
2445
# whose checker succeeded before we stored its state is
2446
# assumed to have successfully run all checkers during
2448
if client["enabled"]:
2449
if datetime.datetime.utcnow() >= client["expires"]:
2450
if not client["last_checked_ok"]:
2452
"disabling client {0} - Client never "
2453
"performed a successful checker"
2454
.format(client_name))
2455
client["enabled"] = False
2456
elif client["last_checker_status"] != 0:
2458
"disabling client {0} - Client "
2459
"last checker failed with error code {1}"
2460
.format(client_name,
2461
client["last_checker_status"]))
2462
client["enabled"] = False
2464
client["expires"] = (datetime.datetime
2466
+ client["timeout"])
2467
logger.debug("Last checker succeeded,"
2468
" keeping {0} enabled"
2469
.format(client_name))
2471
client["secret"] = (
2472
pgp.decrypt(client["encrypted_secret"],
2473
client_settings[client_name]
2476
# If decryption fails, we use secret from new settings
2477
logger.debug("Failed to decrypt {0} old secret"
2478
.format(client_name))
2479
client["secret"] = (
2480
client_settings[client_name]["secret"])
2482
# Add/remove clients based on new changes made to config
2483
for client_name in (set(old_client_settings)
2484
- set(client_settings)):
2485
del clients_data[client_name]
2486
for client_name in (set(client_settings)
2487
- set(old_client_settings)):
2488
clients_data[client_name] = client_settings[client_name]
2490
# Create all client objects
2491
for client_name, client in clients_data.iteritems():
2492
tcp_server.clients[client_name] = client_class(
2493
name = client_name, settings = client)
2495
if not tcp_server.clients:
2496
logger.warning("No clients defined")
740
debug = server_settings["debug"]
743
console = logging.StreamHandler()
744
# console.setLevel(logging.DEBUG)
745
console.setFormatter(logging.Formatter\
746
('%(levelname)s: %(message)s'))
747
logger.addHandler(console)
751
def remove_from_clients(client):
752
clients.remove(client)
754
logger.debug(u"No clients left, exiting")
757
clients.update(Set(Client(name=section,
758
stop_hook = remove_from_clients,
759
**(dict(client_config\
761
for section in client_config.sections()))
2502
pidfile.write(str(pid) + "\n".encode("utf-8"))
2505
logger.error("Could not write to file %r with PID %d",
2508
# "pidfile" was never created
2512
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2513
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2516
@alternate_dbus_interfaces({"se.recompile.Mandos":
2517
"se.bsnet.fukt.Mandos"})
2518
class MandosDBusService(DBusObjectWithProperties):
2519
"""A D-Bus proxy object"""
2521
dbus.service.Object.__init__(self, bus, "/")
2522
_interface = "se.recompile.Mandos"
2524
@dbus_interface_annotations(_interface)
2526
return { "org.freedesktop.DBus.Property"
2527
".EmitsChangedSignal":
2530
@dbus.service.signal(_interface, signature="o")
2531
def ClientAdded(self, objpath):
2535
@dbus.service.signal(_interface, signature="ss")
2536
def ClientNotFound(self, fingerprint, address):
2540
@dbus.service.signal(_interface, signature="os")
2541
def ClientRemoved(self, objpath, name):
2545
@dbus.service.method(_interface, out_signature="ao")
2546
def GetAllClients(self):
2548
return dbus.Array(c.dbus_object_path
2550
tcp_server.clients.itervalues())
2552
@dbus.service.method(_interface,
2553
out_signature="a{oa{sv}}")
2554
def GetAllClientsWithProperties(self):
2556
return dbus.Dictionary(
2557
((c.dbus_object_path, c.GetAll(""))
2558
for c in tcp_server.clients.itervalues()),
2561
@dbus.service.method(_interface, in_signature="o")
2562
def RemoveClient(self, object_path):
2564
for c in tcp_server.clients.itervalues():
2565
if c.dbus_object_path == object_path:
2566
del tcp_server.clients[c.name]
2567
c.remove_from_connection()
2568
# Don't signal anything except ClientRemoved
2569
c.disable(quiet=True)
2571
self.ClientRemoved(object_path, c.name)
2573
raise KeyError(object_path)
2577
mandos_dbus_service = MandosDBusService()
2580
767
"Cleanup function; run on exit"
2583
multiprocessing.active_children()
2584
if not (tcp_server.clients or client_settings):
2587
# Store client before exiting. Secrets are encrypted with key
2588
# based on what config file has. If config file is
2589
# removed/edited, old secret will thus be unrecovable.
2591
with PGPEngine() as pgp:
2592
for client in tcp_server.clients.itervalues():
2593
key = client_settings[client.name]["secret"]
2594
client.encrypted_secret = pgp.encrypt(client.secret,
2598
# A list of attributes that can not be pickled
2600
exclude = set(("bus", "changedstate", "secret",
2602
for name, typ in (inspect.getmembers
2603
(dbus.service.Object)):
2606
client_dict["encrypted_secret"] = (client
2608
for attr in client.client_structure:
2609
if attr not in exclude:
2610
client_dict[attr] = getattr(client, attr)
2612
clients[client.name] = client_dict
2613
del client_settings[client.name]["secret"]
2616
with (tempfile.NamedTemporaryFile
2617
(mode='wb', suffix=".pickle", prefix='clients-',
2618
dir=os.path.dirname(stored_state_path),
2619
delete=False)) as stored_state:
2620
pickle.dump((clients, client_settings), stored_state)
2621
tempname=stored_state.name
2622
os.rename(tempname, stored_state_path)
2623
except (IOError, OSError) as e:
2629
if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
2630
logger.warning("Could not save persistent state: {0}"
2631
.format(os.strerror(e.errno)))
2633
logger.warning("Could not save persistent state:",
2637
# Delete all clients, and settings from config
2638
while tcp_server.clients:
2639
name, client = tcp_server.clients.popitem()
2641
client.remove_from_connection()
2642
# Don't signal anything except ClientRemoved
2643
client.disable(quiet=True)
2646
mandos_dbus_service.ClientRemoved(client
2649
client_settings.clear()
769
# From the Avahi example code
770
if not group is None:
773
# End of Avahi example code
776
client = clients.pop()
777
client.stop_hook = None
2651
780
atexit.register(cleanup)
2653
for client in tcp_server.clients.itervalues():
2656
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2657
# Need to initiate checking of clients
2659
client.init_checker()
2662
tcp_server.server_activate()
783
signal.signal(signal.SIGINT, signal.SIG_IGN)
784
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
785
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
787
for client in clients:
790
tcp_server = IPv6_TCPServer((server_settings["address"],
791
server_settings["port"]),
793
settings=server_settings,
2664
795
# Find out what port we got
2665
796
service.port = tcp_server.socket.getsockname()[1]
2667
logger.info("Now listening on address %r, port %d,"
2668
" flowinfo %d, scope_id %d",
2669
*tcp_server.socket.getsockname())
2671
logger.info("Now listening on address %r, port %d",
2672
*tcp_server.socket.getsockname())
797
logger.debug(u"Now listening on address %r, port %d, flowinfo %d,"
798
u" scope_id %d" % tcp_server.socket.getsockname())
2674
800
#service.interface = tcp_server.socket.getsockname()[3]
2677
803
# From the Avahi example code
804
server.connect_to_signal("StateChanged", server_state_changed)
2680
except dbus.exceptions.DBusException as error:
2681
logger.critical("D-Bus Exception", exc_info=error)
806
server_state_changed(server.GetState())
807
except dbus.exceptions.DBusException, error:
808
logger.critical(u"DBusException: %s", error)
2684
810
# End of Avahi example code
2686
812
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2687
813
lambda *args, **kwargs:
2688
(tcp_server.handle_request
2689
(*args[2:], **kwargs) or True))
814
tcp_server.handle_request\
815
(*args[2:], **kwargs) or True)
2691
817
logger.debug("Starting main loop")
818
main_loop_started = True
2693
except AvahiError as error:
2694
logger.critical("Avahi Error", exc_info=error)
820
except AvahiError, error:
821
logger.critical(u"AvahiError: %s" + unicode(error))
2697
823
except KeyboardInterrupt:
2699
print("", file=sys.stderr)
2700
logger.debug("Server received KeyboardInterrupt")
2701
logger.debug("Server exiting")
2702
# Must run before the D-Bus bus name gets deregistered
2705
827
if __name__ == '__main__':