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.error(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, config={}):
225
"""Note: the 'checker' argument sets the 'checker_command'
226
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()
228
logger.debug(u"Creating client %r", self.name)
229
# Uppercase and remove spaces from fingerprint
230
# for later comparison purposes with return value of
231
# the fingerprint() function
232
self.fingerprint = config["fingerprint"].upper()\
234
logger.debug(u" Fingerprint: %s", self.fingerprint)
235
if "secret" in config:
236
self.secret = config["secret"].decode(u"base64")
237
elif "secfile" in config:
238
sf = open(config["secfile"])
239
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
242
raise TypeError(u"No secret or secfile for client %s"
244
self.fqdn = config.get("fqdn", "")
245
self.created = datetime.datetime.now()
246
self.last_checked_ok = None
247
self.timeout = string_to_delta(config["timeout"])
248
self.interval = string_to_delta(config["interval"])
249
self.stop_hook = stop_hook
547
250
self.checker = None
548
251
self.checker_initiator_tag = None
549
self.disable_initiator_tag = None
252
self.stop_initiator_tag = None
550
253
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()
254
self.check_command = config["checker"]
575
256
"""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
257
# Schedule a new checker to be started an 'interval' from now,
610
258
# 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(),
259
self.checker_initiator_tag = gobject.timeout_add\
260
(self._interval_milliseconds,
622
262
# Also start a new checker *right now*.
623
263
self.start_checker()
625
def checker_callback(self, pid, condition, command):
264
# Schedule a stop() when 'timeout' has passed
265
self.stop_initiator_tag = gobject.timeout_add\
266
(self._timeout_milliseconds,
270
The possibility that a client might be restarted is left open,
271
but not currently used."""
272
# If this client doesn't have a secret, it is already stopped.
274
logger.info(u"Stopping client %s", self.name)
278
if getattr(self, "stop_initiator_tag", False):
279
gobject.source_remove(self.stop_initiator_tag)
280
self.stop_initiator_tag = None
281
if getattr(self, "checker_initiator_tag", False):
282
gobject.source_remove(self.checker_initiator_tag)
283
self.checker_initiator_tag = None
287
# Do not run this again if called by a gobject.timeout_add
290
self.stop_hook = None
292
def checker_callback(self, pid, condition):
626
293
"""The checker has completed, so take appropriate actions."""
294
now = datetime.datetime.now()
627
295
self.checker_callback_tag = None
628
296
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?",
297
if os.WIFEXITED(condition) \
298
and (os.WEXITSTATUS(condition) == 0):
299
logger.info(u"Checker for %(name)s succeeded",
301
self.last_checked_ok = now
302
gobject.source_remove(self.stop_initiator_tag)
303
self.stop_initiator_tag = gobject.timeout_add\
304
(self._timeout_milliseconds,
306
elif not os.WIFEXITED(condition):
307
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()
310
logger.info(u"Checker for %(name)s failed",
665
312
def start_checker(self):
666
313
"""Start a new checker subprocess if one is not running.
668
314
If a checker already exists, leave it running and do
670
316
# 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
317
# did that, then if a checker (for some reason) started
318
# running slowly and taking more than 'interval' time, the
319
# client would inevitably timeout, since no checker would get
320
# a chance to run to completion. If we instead leave running
675
321
# 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
322
# than 'timeout' for the client to be declared invalid, which
323
# is as it should be.
693
324
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",
326
# In case check_command has exactly one % operator
327
command = self.check_command % self.fqdn
329
# Escape attributes for the shell
330
escaped_attrs = dict((key, re.escape(str(val)))
332
vars(self).iteritems())
334
command = self.check_command % escaped_attrs
335
except TypeError, error:
336
logger.error(u'Could not format string "%s":'
337
u' %s', self.check_command, error)
338
return True # Try again later
340
logger.info(u"Starting checker %r for %s",
708
341
command, self.name)
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.)
713
342
self.checker = subprocess.Popen(command,
715
344
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)
345
self.checker_callback_tag = gobject.child_watch_add\
347
self.checker_callback)
348
except subprocess.OSError, error:
349
logger.error(u"Failed to start subprocess: %s",
729
351
# Re-run this periodically if run by gobject.timeout_add
732
353
def stop_checker(self):
733
354
"""Force the checker process, if any, to stop."""
734
355
if self.checker_callback_tag:
739
360
logger.debug("Stopping checker for %(name)s", vars(self))
741
self.checker.terminate()
362
os.kill(self.checker.pid, signal.SIGTERM)
743
364
#if self.checker.poll() is None:
744
# self.checker.kill()
745
except OSError as error:
365
# os.kill(self.checker.pid, signal.SIGKILL)
366
except OSError, error:
746
367
if error.errno != errno.ESRCH: # No such process
748
369
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.
370
def still_valid(self):
371
"""Has the timeout not yet passed for this client?"""
372
now = datetime.datetime.now()
373
if self.last_checked_ok is None:
374
return now < (self.created + self.timeout)
376
return now < (self.last_checked_ok + self.timeout)
379
def peer_certificate(session):
380
"Return the peer's OpenPGP certificate as a bytestring"
381
# If not an OpenPGP certificate...
382
if gnutls.library.functions.gnutls_certificate_type_get\
383
(session._c_object) \
384
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
385
# ...do the normal thing
386
return session.peer_certificate
387
list_size = ctypes.c_uint()
388
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
389
(session._c_object, ctypes.byref(list_size))
390
if list_size.value == 0:
393
return ctypes.string_at(cert.data, cert.size)
396
def fingerprint(openpgp):
397
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
398
# New empty GnuTLS certificate
399
crt = gnutls.library.types.gnutls_openpgp_crt_t()
400
gnutls.library.functions.gnutls_openpgp_crt_init\
402
# New GnuTLS "datum" with the OpenPGP public key
403
datum = gnutls.library.types.gnutls_datum_t\
404
(ctypes.cast(ctypes.c_char_p(openpgp),
405
ctypes.POINTER(ctypes.c_ubyte)),
406
ctypes.c_uint(len(openpgp)))
407
# Import the OpenPGP public key into the certificate
408
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
411
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
412
# New buffer for the fingerprint
413
buffer = ctypes.create_string_buffer(20)
414
buffer_length = ctypes.c_size_t()
415
# Get the fingerprint from the certificate into the buffer
416
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
417
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
418
# Deinit the certificate
419
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
420
# Convert the buffer to a Python bytestring
421
fpr = ctypes.string_at(buffer, buffer_length.value)
422
# Convert the bytestring to hexadecimal notation
423
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
427
class tcp_handler(SocketServer.BaseRequestHandler, object):
428
"""A TCP request handler class.
429
Instantiated by IPv6_TCPServer for each request to handle it.
1639
430
Note: This will run in its own forked process."""
1641
432
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
433
logger.info(u"TCP connection from: %s",
434
unicode(self.client_address))
435
session = gnutls.connection.ClientSession\
436
(self.request, gnutls.connection.X509Credentials())
438
line = self.request.makefile().readline()
439
logger.debug(u"Protocol version: %r", line)
441
if int(line.strip().split()[0]) > 1:
443
except (ValueError, IndexError, RuntimeError), error:
444
logger.error(u"Unknown protocol version: %s", error)
447
# Note: gnutls.connection.X509Credentials is really a generic
448
# GnuTLS certificate credentials object so long as no X.509
449
# keys are added to it. Therefore, we can use it here despite
450
# using OpenPGP certificates.
452
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
453
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
455
priority = "NORMAL" # Fallback default, since this
457
if self.server.settings["priority"]:
458
priority = self.server.settings["priority"]
459
gnutls.library.functions.gnutls_priority_set_direct\
460
(session._c_object, priority, None);
464
except gnutls.errors.GNUTLSError, error:
465
logger.warning(u"Handshake failed: %s", error)
466
# Do not run session.bye() here: the session is not
467
# established. Just abandon the request.
470
fpr = fingerprint(peer_certificate(session))
471
except (TypeError, gnutls.errors.GNUTLSError), error:
472
logger.warning(u"Bad certificate: %s", error)
475
logger.debug(u"Fingerprint: %s", fpr)
477
for c in self.server.clients:
478
if c.fingerprint == fpr:
482
logger.warning(u"Client not found for fingerprint: %s",
486
# Have to check if client.still_valid(), since it is possible
487
# that the client timed out while establishing the GnuTLS
489
if not client.still_valid():
490
logger.warning(u"Client %(name)s is invalid",
495
while sent_size < len(client.secret):
496
sent = session.send(client.secret[sent_size:])
497
logger.debug(u"Sent: %d, remaining: %d",
498
sent, len(client.secret)
499
- (sent_size + sent))
504
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
505
"""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
507
settings: Server settings
508
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)
510
address_family = socket.AF_INET6
511
def __init__(self, *args, **kwargs):
512
if "settings" in kwargs:
513
self.settings = kwargs["settings"]
514
del kwargs["settings"]
515
if "clients" in kwargs:
516
self.clients = kwargs["clients"]
517
del kwargs["clients"]
518
return super(type(self), self).__init__(*args, **kwargs)
1938
519
def server_bind(self):
1939
520
"""This overrides the normal server_bind() function
1940
521
to bind to an interface if one was specified, and also NOT to
1941
522
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",
523
if self.settings["interface"]:
524
# 25 is from /usr/include/asm-i486/socket.h
525
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
527
self.socket.setsockopt(socket.SOL_SOCKET,
529
self.settings["interface"])
530
except socket.error, error:
531
if error[0] == errno.EPERM:
532
logger.error(u"No permission to"
533
u" bind to interface %s",
534
self.settings["interface"])
1968
537
# Only bind(2) the socket if we really need to.
1969
538
if self.server_address[0] or self.server_address[1]:
1970
539
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,
541
self.server_address = (in6addr_any,
1976
542
self.server_address[1])
1977
elif not self.server_address[1]:
543
elif self.server_address[1] is None:
1978
544
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)
546
return super(type(self), self).server_bind()
2096
549
def string_to_delta(interval):
2097
550
"""Parse a string and return a datetime.timedelta
2099
552
>>> string_to_delta('7d')
2100
553
datetime.timedelta(7)
2101
554
>>> string_to_delta('60s')
2218
689
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2219
690
"servicename": "Mandos",
2225
"statedir": "/var/lib/mandos"
2228
693
# Parse config file for server-global settings
2229
server_config = configparser.SafeConfigParser(server_defaults)
694
server_config = ConfigParser.SafeConfigParser(server_defaults)
2230
695
del server_defaults
2231
server_config.read(os.path.join(options.configdir,
696
server_config.read(os.path.join(options.configdir, "server.conf"))
697
server_section = "server"
2233
698
# 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
699
server_settings = dict(server_config.items(server_section))
700
# Use getboolean on the boolean config option
701
server_settings["debug"] = server_config.getboolean\
702
(server_section, "debug")
2251
703
del server_config
2253
705
# Override the settings from the config file with command line
2254
706
# options, if set.
2255
707
for option in ("interface", "address", "port", "debug",
2256
"priority", "servicename", "configdir",
2257
"use_dbus", "use_ipv6", "debuglevel", "restore",
2258
"statedir", "socket"):
708
"priority", "servicename", "configdir"):
2259
709
value = getattr(options, option)
2260
710
if value is not None:
2261
711
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
713
# 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
715
# Parse config file with clients
2296
client_config = configparser.SafeConfigParser(Client
716
client_defaults = { "timeout": "1h",
718
"checker": "fping -q -- %%(fqdn)s",
720
client_config = ConfigParser.SafeConfigParser(client_defaults)
2298
721
client_config.read(os.path.join(server_settings["configdir"],
2299
722
"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
# multiprocessing will use threads, so before we use gobject we
2366
# need to inform gobject that threads will be used.
2367
gobject.threads_init()
725
service = AvahiService(name = server_settings["servicename"],
726
type = "_mandos._tcp", );
727
if server_settings["interface"]:
728
service.interface = if_nametoindex(server_settings["interface"])
2369
730
global main_loop
2370
733
# From the Avahi example code
2371
DBusGMainLoop(set_as_default=True)
734
DBusGMainLoop(set_as_default=True )
2372
735
main_loop = gobject.MainLoop()
2373
736
bus = dbus.SystemBus()
737
server = dbus.Interface(
738
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
739
avahi.DBUS_INTERFACE_SERVER )
2374
740
# End of Avahi example code
2377
bus_name = dbus.service.BusName("se.recompile.Mandos",
2378
bus, do_not_queue=True)
2379
old_bus_name = (dbus.service.BusName
2380
("se.bsnet.fukt.Mandos", bus,
2382
except dbus.exceptions.NameExistsException as e:
2383
logger.error("Disabling D-Bus:", exc_info=e)
2385
server_settings["use_dbus"] = False
2386
tcp_server.use_dbus = False
2387
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2388
service = AvahiServiceToSyslog(name =
2389
server_settings["servicename"],
2390
servicetype = "_mandos._tcp",
2391
protocol = protocol, bus = bus)
2392
if server_settings["interface"]:
2393
service.interface = (if_nametoindex
2394
(str(server_settings["interface"])))
2396
global multiprocessing_manager
2397
multiprocessing_manager = multiprocessing.Manager()
2399
client_class = Client
2401
client_class = functools.partial(ClientDBus, bus = bus)
2403
client_settings = Client.config_parser(client_config)
2404
old_client_settings = {}
2407
# Get client data and settings from last running state.
2408
if server_settings["restore"]:
2410
with open(stored_state_path, "rb") as stored_state:
2411
clients_data, old_client_settings = (pickle.load
2413
os.remove(stored_state_path)
2414
except IOError as e:
2415
if e.errno == errno.ENOENT:
2416
logger.warning("Could not load persistent state: {0}"
2417
.format(os.strerror(e.errno)))
2419
logger.critical("Could not load persistent state:",
2422
except EOFError as e:
2423
logger.warning("Could not load persistent state: "
2424
"EOFError:", exc_info=e)
2426
with PGPEngine() as pgp:
2427
for client_name, client in clients_data.iteritems():
2428
# Decide which value to use after restoring saved state.
2429
# We have three different values: Old config file,
2430
# new config file, and saved state.
2431
# New config value takes precedence if it differs from old
2432
# config value, otherwise use saved state.
2433
for name, value in client_settings[client_name].items():
2435
# For each value in new config, check if it
2436
# differs from the old config value (Except for
2437
# the "secret" attribute)
2438
if (name != "secret" and
2439
value != old_client_settings[client_name]
2441
client[name] = value
2445
# Clients who has passed its expire date can still be
2446
# enabled if its last checker was successful. Clients
2447
# whose checker succeeded before we stored its state is
2448
# assumed to have successfully run all checkers during
2450
if client["enabled"]:
2451
if datetime.datetime.utcnow() >= client["expires"]:
2452
if not client["last_checked_ok"]:
2454
"disabling client {0} - Client never "
2455
"performed a successful checker"
2456
.format(client_name))
2457
client["enabled"] = False
2458
elif client["last_checker_status"] != 0:
2460
"disabling client {0} - Client "
2461
"last checker failed with error code {1}"
2462
.format(client_name,
2463
client["last_checker_status"]))
2464
client["enabled"] = False
2466
client["expires"] = (datetime.datetime
2468
+ client["timeout"])
2469
logger.debug("Last checker succeeded,"
2470
" keeping {0} enabled"
2471
.format(client_name))
2473
client["secret"] = (
2474
pgp.decrypt(client["encrypted_secret"],
2475
client_settings[client_name]
2478
# If decryption fails, we use secret from new settings
2479
logger.debug("Failed to decrypt {0} old secret"
2480
.format(client_name))
2481
client["secret"] = (
2482
client_settings[client_name]["secret"])
2484
# Add/remove clients based on new changes made to config
2485
for client_name in (set(old_client_settings)
2486
- set(client_settings)):
2487
del clients_data[client_name]
2488
for client_name in (set(client_settings)
2489
- set(old_client_settings)):
2490
clients_data[client_name] = client_settings[client_name]
2492
# Create all client objects
2493
for client_name, client in clients_data.iteritems():
2494
tcp_server.clients[client_name] = client_class(
2495
name = client_name, settings = client)
2497
if not tcp_server.clients:
2498
logger.warning("No clients defined")
742
debug = server_settings["debug"]
745
console = logging.StreamHandler()
746
# console.setLevel(logging.DEBUG)
747
console.setFormatter(logging.Formatter\
748
('%(levelname)s: %(message)s'))
749
logger.addHandler(console)
753
def remove_from_clients(client):
754
clients.remove(client)
756
logger.critical(u"No clients left, exiting")
759
clients.update(Set(Client(name = section,
760
stop_hook = remove_from_clients,
762
= dict(client_config.items(section)))
763
for section in client_config.sections()))
2504
pidfile.write(str(pid) + "\n".encode("utf-8"))
2507
logger.error("Could not write to file %r with PID %d",
2510
# "pidfile" was never created
2514
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2515
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2518
@alternate_dbus_interfaces({"se.recompile.Mandos":
2519
"se.bsnet.fukt.Mandos"})
2520
class MandosDBusService(DBusObjectWithProperties):
2521
"""A D-Bus proxy object"""
2523
dbus.service.Object.__init__(self, bus, "/")
2524
_interface = "se.recompile.Mandos"
2526
@dbus_interface_annotations(_interface)
2528
return { "org.freedesktop.DBus.Property"
2529
".EmitsChangedSignal":
2532
@dbus.service.signal(_interface, signature="o")
2533
def ClientAdded(self, objpath):
2537
@dbus.service.signal(_interface, signature="ss")
2538
def ClientNotFound(self, fingerprint, address):
2542
@dbus.service.signal(_interface, signature="os")
2543
def ClientRemoved(self, objpath, name):
2547
@dbus.service.method(_interface, out_signature="ao")
2548
def GetAllClients(self):
2550
return dbus.Array(c.dbus_object_path
2552
tcp_server.clients.itervalues())
2554
@dbus.service.method(_interface,
2555
out_signature="a{oa{sv}}")
2556
def GetAllClientsWithProperties(self):
2558
return dbus.Dictionary(
2559
((c.dbus_object_path, c.GetAll(""))
2560
for c in tcp_server.clients.itervalues()),
2563
@dbus.service.method(_interface, in_signature="o")
2564
def RemoveClient(self, object_path):
2566
for c in tcp_server.clients.itervalues():
2567
if c.dbus_object_path == object_path:
2568
del tcp_server.clients[c.name]
2569
c.remove_from_connection()
2570
# Don't signal anything except ClientRemoved
2571
c.disable(quiet=True)
2573
self.ClientRemoved(object_path, c.name)
2575
raise KeyError(object_path)
2579
mandos_dbus_service = MandosDBusService()
2582
769
"Cleanup function; run on exit"
2585
multiprocessing.active_children()
2586
if not (tcp_server.clients or client_settings):
2589
# Store client before exiting. Secrets are encrypted with key
2590
# based on what config file has. If config file is
2591
# removed/edited, old secret will thus be unrecovable.
2593
with PGPEngine() as pgp:
2594
for client in tcp_server.clients.itervalues():
2595
key = client_settings[client.name]["secret"]
2596
client.encrypted_secret = pgp.encrypt(client.secret,
2600
# A list of attributes that can not be pickled
2602
exclude = set(("bus", "changedstate", "secret",
2604
for name, typ in (inspect.getmembers
2605
(dbus.service.Object)):
2608
client_dict["encrypted_secret"] = (client
2610
for attr in client.client_structure:
2611
if attr not in exclude:
2612
client_dict[attr] = getattr(client, attr)
2614
clients[client.name] = client_dict
2615
del client_settings[client.name]["secret"]
2618
with (tempfile.NamedTemporaryFile
2619
(mode='wb', suffix=".pickle", prefix='clients-',
2620
dir=os.path.dirname(stored_state_path),
2621
delete=False)) as stored_state:
2622
pickle.dump((clients, client_settings), stored_state)
2623
tempname=stored_state.name
2624
os.rename(tempname, stored_state_path)
2625
except (IOError, OSError) as e:
2631
if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
2632
logger.warning("Could not save persistent state: {0}"
2633
.format(os.strerror(e.errno)))
2635
logger.warning("Could not save persistent state:",
2639
# Delete all clients, and settings from config
2640
while tcp_server.clients:
2641
name, client = tcp_server.clients.popitem()
2643
client.remove_from_connection()
2644
# Don't signal anything except ClientRemoved
2645
client.disable(quiet=True)
2648
mandos_dbus_service.ClientRemoved(client
2651
client_settings.clear()
771
# From the Avahi example code
772
if not group is None:
775
# End of Avahi example code
778
client = clients.pop()
779
client.stop_hook = None
2653
782
atexit.register(cleanup)
2655
for client in tcp_server.clients.itervalues():
2658
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2659
# Need to initiate checking of clients
2661
client.init_checker()
2664
tcp_server.server_activate()
785
signal.signal(signal.SIGINT, signal.SIG_IGN)
786
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
787
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
789
for client in clients:
792
tcp_server = IPv6_TCPServer((server_settings["address"],
793
server_settings["port"]),
795
settings=server_settings,
2666
797
# Find out what port we got
2667
798
service.port = tcp_server.socket.getsockname()[1]
2669
logger.info("Now listening on address %r, port %d,"
2670
" flowinfo %d, scope_id %d",
2671
*tcp_server.socket.getsockname())
2673
logger.info("Now listening on address %r, port %d",
2674
*tcp_server.socket.getsockname())
799
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
800
u" scope_id %d" % tcp_server.socket.getsockname())
2676
802
#service.interface = tcp_server.socket.getsockname()[3]
2679
805
# From the Avahi example code
806
server.connect_to_signal("StateChanged", server_state_changed)
2682
except dbus.exceptions.DBusException as error:
2683
logger.critical("D-Bus Exception", exc_info=error)
808
server_state_changed(server.GetState())
809
except dbus.exceptions.DBusException, error:
810
logger.critical(u"DBusException: %s", error)
2686
812
# End of Avahi example code
2688
814
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2689
815
lambda *args, **kwargs:
2690
(tcp_server.handle_request
2691
(*args[2:], **kwargs) or True))
816
tcp_server.handle_request\
817
(*args[2:], **kwargs) or True)
2693
819
logger.debug("Starting main loop")
820
main_loop_started = True
2695
except AvahiError as error:
2696
logger.critical("Avahi Error", exc_info=error)
822
except AvahiError, error:
823
logger.critical(u"AvahiError: %s" + unicode(error))
2699
825
except KeyboardInterrupt:
2701
print("", file=sys.stderr)
2702
logger.debug("Server received KeyboardInterrupt")
2703
logger.debug("Server exiting")
2704
# Must run before the D-Bus bus name gets deregistered
2707
829
if __name__ == '__main__':