45
14
import gnutls.library.functions
46
15
import gnutls.library.constants
47
16
import gnutls.library.types
48
import ConfigParser as configparser
57
import logging.handlers
63
import cPickle as pickle
64
import multiprocessing
74
28
from dbus.mainloop.glib import DBusGMainLoop
77
import xml.dom.minidom
82
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
83
except AttributeError:
85
from IN import SO_BINDTODEVICE
87
SO_BINDTODEVICE = None
90
stored_state_file = "clients.pickle"
92
logger = logging.getLogger()
93
syslogger = (logging.handlers.SysLogHandler
94
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
95
address = str("/dev/log")))
98
if_nametoindex = (ctypes.cdll.LoadLibrary
99
(ctypes.util.find_library("c"))
101
except (OSError, AttributeError):
102
def if_nametoindex(interface):
103
"Get an interface index the hard way, i.e. using fcntl()"
104
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
105
with contextlib.closing(socket.socket()) as s:
106
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
107
struct.pack(str("16s16x"),
109
interface_index = struct.unpack(str("I"),
111
return interface_index
114
def initlogger(debug, level=logging.WARNING):
115
"""init logger and add loglevel"""
117
syslogger.setFormatter(logging.Formatter
118
('Mandos [%(process)d]: %(levelname)s:'
120
logger.addHandler(syslogger)
123
console = logging.StreamHandler()
124
console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
128
logger.addHandler(console)
129
logger.setLevel(level)
132
class PGPError(Exception):
133
"""Exception if encryption/decryption fails"""
137
class PGPEngine(object):
138
"""A simple class for OpenPGP symmetric encryption & decryption"""
140
self.gnupg = GnuPGInterface.GnuPG()
141
self.tempdir = tempfile.mkdtemp(prefix="mandos-")
142
self.gnupg = GnuPGInterface.GnuPG()
143
self.gnupg.options.meta_interactive = False
144
self.gnupg.options.homedir = self.tempdir
145
self.gnupg.options.extra_args.extend(['--force-mdc',
152
def __exit__ (self, exc_type, exc_value, traceback):
160
if self.tempdir is not None:
161
# Delete contents of tempdir
162
for root, dirs, files in os.walk(self.tempdir,
164
for filename in files:
165
os.remove(os.path.join(root, filename))
167
os.rmdir(os.path.join(root, dirname))
169
os.rmdir(self.tempdir)
172
def password_encode(self, password):
173
# Passphrase can not be empty and can not contain newlines or
174
# NUL bytes. So we prefix it and hex encode it.
175
return b"mandos" + binascii.hexlify(password)
177
def encrypt(self, data, password):
178
self.gnupg.passphrase = self.password_encode(password)
179
with open(os.devnull, "w") as devnull:
181
proc = self.gnupg.run(['--symmetric'],
182
create_fhs=['stdin', 'stdout'],
183
attach_fhs={'stderr': devnull})
184
with contextlib.closing(proc.handles['stdin']) as f:
186
with contextlib.closing(proc.handles['stdout']) as f:
187
ciphertext = f.read()
191
self.gnupg.passphrase = None
194
def decrypt(self, data, password):
195
self.gnupg.passphrase = self.password_encode(password)
196
with open(os.devnull, "w") as devnull:
198
proc = self.gnupg.run(['--decrypt'],
199
create_fhs=['stdin', 'stdout'],
200
attach_fhs={'stderr': devnull})
201
with contextlib.closing(proc.handles['stdin']) as f:
203
with contextlib.closing(proc.handles['stdout']) as f:
204
decrypted_plaintext = f.read()
208
self.gnupg.passphrase = None
209
return decrypted_plaintext
213
class AvahiError(Exception):
214
def __init__(self, value, *args, **kwargs):
216
super(AvahiError, self).__init__(value, *args, **kwargs)
217
def __unicode__(self):
218
return unicode(repr(self.value))
220
class AvahiServiceError(AvahiError):
223
class AvahiGroupError(AvahiError):
227
class AvahiService(object):
228
"""An Avahi (Zeroconf) service.
231
interface: integer; avahi.IF_UNSPEC or an interface index.
232
Used to optionally bind to the specified interface.
233
name: string; Example: 'Mandos'
234
type: string; Example: '_mandos._tcp'.
235
See <http://www.dns-sd.org/ServiceTypes.html>
236
port: integer; what port to announce
237
TXT: list of strings; TXT record for the service
238
domain: string; Domain to publish on, default to .local if empty.
239
host: string; Host to publish records for, default is localhost
240
max_renames: integer; maximum number of renames
241
rename_count: integer; counter so we only rename after collisions
242
a sensible number of times
243
group: D-Bus Entry Group
245
bus: dbus.SystemBus()
247
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
248
servicetype = None, port = None, TXT = None,
249
domain = "", host = "", max_renames = 32768,
250
protocol = avahi.PROTO_UNSPEC, bus = None):
251
self.interface = interface
253
self.type = servicetype
255
self.TXT = TXT if TXT is not None else []
258
self.rename_count = 0
259
self.max_renames = max_renames
260
self.protocol = protocol
261
self.group = None # our entry group
264
self.entry_group_state_changed_match = None
266
"""Derived from the Avahi example code"""
267
if self.rename_count >= self.max_renames:
268
logger.critical("No suitable Zeroconf service name found"
269
" after %i retries, exiting.",
271
raise AvahiServiceError("Too many renames")
272
self.name = unicode(self.server
273
.GetAlternativeServiceName(self.name))
274
logger.info("Changing Zeroconf service name to %r ...",
279
except dbus.exceptions.DBusException as error:
280
logger.critical("D-Bus Exception", exc_info=error)
283
self.rename_count += 1
285
"""Derived from the Avahi example code"""
286
if self.entry_group_state_changed_match is not None:
287
self.entry_group_state_changed_match.remove()
288
self.entry_group_state_changed_match = None
289
if self.group is not None:
292
"""Derived from the Avahi example code"""
294
if self.group is None:
295
self.group = dbus.Interface(
296
self.bus.get_object(avahi.DBUS_NAME,
297
self.server.EntryGroupNew()),
298
avahi.DBUS_INTERFACE_ENTRY_GROUP)
299
self.entry_group_state_changed_match = (
300
self.group.connect_to_signal(
301
'StateChanged', self.entry_group_state_changed))
302
logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
303
self.name, self.type)
304
self.group.AddService(
307
dbus.UInt32(0), # flags
308
self.name, self.type,
309
self.domain, self.host,
310
dbus.UInt16(self.port),
311
avahi.string_array_to_txt_array(self.TXT))
313
def entry_group_state_changed(self, state, error):
314
"""Derived from the Avahi example code"""
315
logger.debug("Avahi entry group state change: %i", state)
317
if state == avahi.ENTRY_GROUP_ESTABLISHED:
318
logger.debug("Zeroconf service established.")
319
elif state == avahi.ENTRY_GROUP_COLLISION:
320
logger.info("Zeroconf service name collision.")
322
elif state == avahi.ENTRY_GROUP_FAILURE:
323
logger.critical("Avahi: Error in group state changed %s",
325
raise AvahiGroupError("State changed: %s"
328
"""Derived from the Avahi example code"""
329
if self.group is not None:
332
except (dbus.exceptions.UnknownMethodException,
333
dbus.exceptions.DBusException):
337
def server_state_changed(self, state, error=None):
338
"""Derived from the Avahi example code"""
339
logger.debug("Avahi server state change: %i", state)
340
bad_states = { avahi.SERVER_INVALID:
341
"Zeroconf server invalid",
342
avahi.SERVER_REGISTERING: None,
343
avahi.SERVER_COLLISION:
344
"Zeroconf server name collision",
345
avahi.SERVER_FAILURE:
346
"Zeroconf server failure" }
347
if state in bad_states:
348
if bad_states[state] is not None:
350
logger.error(bad_states[state])
352
logger.error(bad_states[state] + ": %r", error)
354
elif state == avahi.SERVER_RUNNING:
358
logger.debug("Unknown state: %r", state)
360
logger.debug("Unknown state: %r: %r", state, error)
362
"""Derived from the Avahi example code"""
363
if self.server is None:
364
self.server = dbus.Interface(
365
self.bus.get_object(avahi.DBUS_NAME,
366
avahi.DBUS_PATH_SERVER,
367
follow_name_owner_changes=True),
368
avahi.DBUS_INTERFACE_SERVER)
369
self.server.connect_to_signal("StateChanged",
370
self.server_state_changed)
371
self.server_state_changed(self.server.GetState())
373
class AvahiServiceToSyslog(AvahiService):
375
"""Add the new name to the syslog messages"""
376
ret = AvahiService.rename(self)
377
syslogger.setFormatter(logging.Formatter
378
('Mandos (%s) [%%(process)d]:'
379
' %%(levelname)s: %%(message)s'
383
def timedelta_to_milliseconds(td):
384
"Convert a datetime.timedelta() to milliseconds"
385
return ((td.days * 24 * 60 * 60 * 1000)
386
+ (td.seconds * 1000)
387
+ (td.microseconds // 1000))
31
# This variable is used to optionally bind to a specified interface.
32
# It is a global variable to fit in with the other variables from the
33
# Avahi server example code.
34
serviceInterface = avahi.IF_UNSPEC
35
# From the Avahi server example code:
36
serviceName = "Mandos"
37
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
38
servicePort = None # Not known at startup
39
serviceTXT = [] # TXT record for the service
40
domain = "" # Domain to publish on, default to .local
41
host = "" # Host to publish records for, default to localhost
42
group = None #our entry group
43
rename_count = 12 # Counter so we only rename after collisions a
44
# sensible number of times
45
# End of Avahi example code
389
48
class Client(object):
390
49
"""A representation of a client host served by this server.
393
approved: bool(); 'None' if not yet approved/disapproved
394
approval_delay: datetime.timedelta(); Time to wait for approval
395
approval_duration: datetime.timedelta(); Duration of one approval
396
checker: subprocess.Popen(); a running checker process used
397
to see if the client lives.
398
'None' if no process is running.
399
checker_callback_tag: a gobject event source tag, or None
400
checker_command: string; External command which is run to check
401
if client lives. %() expansions are done at
51
name: string; from the config file, used in log messages
52
fingerprint: string (40 or 32 hexadecimal digits); used to
53
uniquely identify the client
54
secret: bytestring; sent verbatim (over TLS) to client
55
fqdn: string (FQDN); available for use by the checker command
56
created: datetime.datetime()
57
last_seen: datetime.datetime() or None if not yet seen
58
timeout: datetime.timedelta(); How long from last_seen until
59
this client is invalid
60
interval: datetime.timedelta(); How often to start a new checker
61
stop_hook: If set, called by stop() as stop_hook(self)
62
checker: subprocess.Popen(); a running checker process used
63
to see if the client lives.
64
Is None if no process is running.
65
checker_initiator_tag: a gobject event source tag, or None
66
stop_initiator_tag: - '' -
67
checker_callback_tag: - '' -
68
checker_command: string; External command which is run to check if
69
client lives. %()s expansions are done at
402
70
runtime with vars(self) as dict, so that for
403
71
instance %(name)s can be used in the command.
404
checker_initiator_tag: a gobject event source tag, or None
405
created: datetime.datetime(); (UTC) object creation
406
client_structure: Object describing what attributes a client has
407
and is used for storing the client at exit
408
current_checker_command: string; current running checker_command
409
disable_initiator_tag: a gobject event source tag, or None
411
fingerprint: string (40 or 32 hexadecimal digits); used to
412
uniquely identify the client
413
host: string; available for use by the checker command
414
interval: datetime.timedelta(); How often to start a new checker
415
last_approval_request: datetime.datetime(); (UTC) or None
416
last_checked_ok: datetime.datetime(); (UTC) or None
417
last_checker_status: integer between 0 and 255 reflecting exit
418
status of last checker. -1 reflects crashed
419
checker, -2 means no checker completed yet.
420
last_enabled: datetime.datetime(); (UTC) or None
421
name: string; from the config file, used in log messages and
423
secret: bytestring; sent verbatim (over TLS) to client
424
timeout: datetime.timedelta(); How long from last_checked_ok
425
until this client is disabled
426
extended_timeout: extra long timeout when secret has been sent
427
runtime_expansions: Allowed attributes for runtime expansion.
428
expires: datetime.datetime(); time (UTC) when a client will be
73
_timeout: Real variable for 'timeout'
74
_interval: Real variable for 'interval'
75
_timeout_milliseconds: Used by gobject.timeout_add()
76
_interval_milliseconds: - '' -
432
runtime_expansions = ("approval_delay", "approval_duration",
433
"created", "enabled", "fingerprint",
434
"host", "interval", "last_checked_ok",
435
"last_enabled", "name", "timeout")
436
client_defaults = { "timeout": "5m",
437
"extended_timeout": "15m",
439
"checker": "fping -q -- %%(host)s",
441
"approval_delay": "0s",
442
"approval_duration": "1s",
443
"approved_by_default": "True",
447
def timeout_milliseconds(self):
448
"Return the 'timeout' attribute in milliseconds"
449
return timedelta_to_milliseconds(self.timeout)
451
def extended_timeout_milliseconds(self):
452
"Return the 'extended_timeout' attribute in milliseconds"
453
return timedelta_to_milliseconds(self.extended_timeout)
455
def interval_milliseconds(self):
456
"Return the 'interval' attribute in milliseconds"
457
return timedelta_to_milliseconds(self.interval)
459
def approval_delay_milliseconds(self):
460
return timedelta_to_milliseconds(self.approval_delay)
463
def config_parser(config):
464
"""Construct a new dict of client settings of this form:
465
{ client_name: {setting_name: value, ...}, ...}
466
with exceptions for any special settings as defined above.
467
NOTE: Must be a pure function. Must return the same result
468
value given the same arguments.
471
for client_name in config.sections():
472
section = dict(config.items(client_name))
473
client = settings[client_name] = {}
475
client["host"] = section["host"]
476
# Reformat values from string types to Python types
477
client["approved_by_default"] = config.getboolean(
478
client_name, "approved_by_default")
479
client["enabled"] = config.getboolean(client_name,
482
client["fingerprint"] = (section["fingerprint"].upper()
484
if "secret" in section:
485
client["secret"] = section["secret"].decode("base64")
486
elif "secfile" in section:
487
with open(os.path.expanduser(os.path.expandvars
488
(section["secfile"])),
490
client["secret"] = secfile.read()
492
raise TypeError("No secret or secfile for section %s"
494
client["timeout"] = string_to_delta(section["timeout"])
495
client["extended_timeout"] = string_to_delta(
496
section["extended_timeout"])
497
client["interval"] = string_to_delta(section["interval"])
498
client["approval_delay"] = string_to_delta(
499
section["approval_delay"])
500
client["approval_duration"] = string_to_delta(
501
section["approval_duration"])
502
client["checker_command"] = section["checker"]
503
client["last_approval_request"] = None
504
client["last_checked_ok"] = None
505
client["last_checker_status"] = -2
510
def __init__(self, settings, name = None):
511
"""Note: the 'checker' key in 'config' sets the
512
'checker_command' attribute and *not* the 'checker'
78
def _set_timeout(self, timeout):
79
"Setter function for 'timeout' attribute"
80
self._timeout = timeout
81
self._timeout_milliseconds = ((self.timeout.days
82
* 24 * 60 * 60 * 1000)
83
+ (self.timeout.seconds * 1000)
84
+ (self.timeout.microseconds
86
timeout = property(lambda self: self._timeout,
89
def _set_interval(self, interval):
90
"Setter function for 'interval' attribute"
91
self._interval = interval
92
self._interval_milliseconds = ((self.interval.days
93
* 24 * 60 * 60 * 1000)
94
+ (self.interval.seconds
96
+ (self.interval.microseconds
98
interval = property(lambda self: self._interval,
101
def __init__(self, name=None, options=None, stop_hook=None,
102
fingerprint=None, secret=None, secfile=None, fqdn=None,
103
timeout=None, interval=-1, checker=None):
515
# adding all client settings
516
for setting, value in settings.iteritems():
517
setattr(self, setting, value)
520
if not hasattr(self, "last_enabled"):
521
self.last_enabled = datetime.datetime.utcnow()
522
if not hasattr(self, "expires"):
523
self.expires = (datetime.datetime.utcnow()
526
self.last_enabled = None
529
logger.debug("Creating client %r", self.name)
530
# Uppercase and remove spaces from fingerprint for later
531
# comparison purposes with return value from the fingerprint()
533
logger.debug(" Fingerprint: %s", self.fingerprint)
534
self.created = settings.get("created",
535
datetime.datetime.utcnow())
537
# attributes specific for this server instance
105
# Uppercase and remove spaces from fingerprint
106
# for later comparison purposes with return value of
107
# the fingerprint() function
108
self.fingerprint = fingerprint.upper().replace(u" ", u"")
110
self.secret = secret.decode(u"base64")
113
self.secret = sf.read()
116
raise RuntimeError(u"No secret or secfile for client %s"
118
self.fqdn = fqdn # string
119
self.created = datetime.datetime.now()
120
self.last_seen = None
122
timeout = options.timeout
123
self.timeout = timeout
125
interval = options.interval
127
interval = string_to_delta(interval)
128
self.interval = interval
129
self.stop_hook = stop_hook
538
130
self.checker = None
539
131
self.checker_initiator_tag = None
540
self.disable_initiator_tag = None
132
self.stop_initiator_tag = None
541
133
self.checker_callback_tag = None
542
self.current_checker_command = None
544
self.approvals_pending = 0
545
self.changedstate = (multiprocessing_manager
546
.Condition(multiprocessing_manager
548
self.client_structure = [attr for attr in
549
self.__dict__.iterkeys()
550
if not attr.startswith("_")]
551
self.client_structure.append("client_structure")
553
for name, t in inspect.getmembers(type(self),
557
if not name.startswith("_"):
558
self.client_structure.append(name)
560
# Send notice to process children that client state has changed
561
def send_changedstate(self):
562
with self.changedstate:
563
self.changedstate.notify_all()
566
"""Start this client's checker and timeout hooks"""
567
if getattr(self, "enabled", False):
570
self.send_changedstate()
571
self.expires = datetime.datetime.utcnow() + self.timeout
573
self.last_enabled = datetime.datetime.utcnow()
576
def disable(self, quiet=True):
577
"""Disable this client."""
578
if not getattr(self, "enabled", False):
581
self.send_changedstate()
583
logger.info("Disabling client %s", self.name)
584
if getattr(self, "disable_initiator_tag", False):
585
gobject.source_remove(self.disable_initiator_tag)
586
self.disable_initiator_tag = None
588
if getattr(self, "checker_initiator_tag", False):
589
gobject.source_remove(self.checker_initiator_tag)
590
self.checker_initiator_tag = None
593
# Do not run this again if called by a gobject.timeout_add
599
def init_checker(self):
134
self.check_command = checker
136
"""Start this clients checker and timeout hooks"""
600
137
# Schedule a new checker to be started an 'interval' from now,
601
138
# and every interval from then on.
602
self.checker_initiator_tag = (gobject.timeout_add
603
(self.interval_milliseconds(),
605
# Schedule a disable() when 'timeout' has passed
606
self.disable_initiator_tag = (gobject.timeout_add
607
(self.timeout_milliseconds(),
139
self.checker_initiator_tag = gobject.timeout_add\
140
(self._interval_milliseconds,
609
142
# Also start a new checker *right now*.
610
143
self.start_checker()
612
def checker_callback(self, pid, condition, command):
144
# Schedule a stop() when 'timeout' has passed
145
self.stop_initiator_tag = gobject.timeout_add\
146
(self._timeout_milliseconds,
150
The possibility that this client might be restarted is left
151
open, but not currently used."""
153
sys.stderr.write(u"Stopping client %s\n" % self.name)
155
if self.stop_initiator_tag:
156
gobject.source_remove(self.stop_initiator_tag)
157
self.stop_initiator_tag = None
158
if self.checker_initiator_tag:
159
gobject.source_remove(self.checker_initiator_tag)
160
self.checker_initiator_tag = None
164
# Do not run this again if called by a gobject.timeout_add
167
# Some code duplication here and in stop()
168
if hasattr(self, "stop_initiator_tag") \
169
and self.stop_initiator_tag:
170
gobject.source_remove(self.stop_initiator_tag)
171
self.stop_initiator_tag = None
172
if hasattr(self, "checker_initiator_tag") \
173
and self.checker_initiator_tag:
174
gobject.source_remove(self.checker_initiator_tag)
175
self.checker_initiator_tag = None
177
def checker_callback(self, pid, condition):
613
178
"""The checker has completed, so take appropriate actions."""
179
now = datetime.datetime.now()
180
if os.WIFEXITED(condition) \
181
and (os.WEXITSTATUS(condition) == 0):
183
sys.stderr.write(u"Checker for %(name)s succeeded\n"
186
gobject.source_remove(self.stop_initiator_tag)
187
self.stop_initiator_tag = gobject.timeout_add\
188
(self._timeout_milliseconds,
191
if not os.WIFEXITED(condition):
192
sys.stderr.write(u"Checker for %(name)s crashed?\n"
195
sys.stderr.write(u"Checker for %(name)s failed\n"
614
198
self.checker_callback_tag = None
616
if os.WIFEXITED(condition):
617
self.last_checker_status = os.WEXITSTATUS(condition)
618
if self.last_checker_status == 0:
619
logger.info("Checker for %(name)s succeeded",
623
logger.info("Checker for %(name)s failed",
626
self.last_checker_status = -1
627
logger.warning("Checker for %(name)s crashed?",
630
def checked_ok(self):
631
"""Assert that the client has been seen, alive and well."""
632
self.last_checked_ok = datetime.datetime.utcnow()
633
self.last_checker_status = 0
636
def bump_timeout(self, timeout=None):
637
"""Bump up the timeout for this client."""
639
timeout = self.timeout
640
if self.disable_initiator_tag is not None:
641
gobject.source_remove(self.disable_initiator_tag)
642
if getattr(self, "enabled", False):
643
self.disable_initiator_tag = (gobject.timeout_add
644
(timedelta_to_milliseconds
645
(timeout), self.disable))
646
self.expires = datetime.datetime.utcnow() + timeout
648
def need_approval(self):
649
self.last_approval_request = datetime.datetime.utcnow()
651
199
def start_checker(self):
652
200
"""Start a new checker subprocess if one is not running.
654
201
If a checker already exists, leave it running and do
656
# The reason for not killing a running checker is that if we
657
# did that, then if a checker (for some reason) started
658
# running slowly and taking more than 'interval' time, the
659
# client would inevitably timeout, since no checker would get
660
# a chance to run to completion. If we instead leave running
661
# checkers alone, the checker would have to take more time
662
# than 'timeout' for the client to be disabled, which is as it
665
# If a checker exists, make sure it is not a zombie
667
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
668
except (AttributeError, OSError) as error:
669
if (isinstance(error, OSError)
670
and error.errno != errno.ECHILD):
674
logger.warning("Checker was a zombie")
675
gobject.source_remove(self.checker_callback_tag)
676
self.checker_callback(pid, status,
677
self.current_checker_command)
678
# Start a new checker if needed
679
203
if self.checker is None:
205
sys.stderr.write(u"Starting checker for %s\n"
681
# In case checker_command has exactly one % operator
682
command = self.checker_command % self.host
208
command = self.check_command % self.fqdn
683
209
except TypeError:
684
# Escape attributes for the shell
685
escaped_attrs = dict(
687
re.escape(unicode(str(getattr(self, attr, "")),
691
self.runtime_expansions)
694
command = self.checker_command % escaped_attrs
695
except TypeError as error:
696
logger.error('Could not format string "%s"',
697
self.checker_command, exc_info=error)
698
return True # Try again later
699
self.current_checker_command = command
210
escaped_attrs = dict((key, re.escape(str(val)))
212
vars(self).iteritems())
213
command = self.check_command % escaped_attrs
701
logger.info("Starting checker %r for %s",
703
# We don't need to redirect stdout and stderr, since
704
# in normal mode, that is already done by daemon(),
705
# and in debug mode we don't want to. (Stdin is
706
# always replaced by /dev/null.)
707
self.checker = subprocess.Popen(command,
710
self.checker_callback_tag = (gobject.child_watch_add
712
self.checker_callback,
714
# The checker may have completed before the gobject
715
# watch was added. Check for this.
716
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
718
gobject.source_remove(self.checker_callback_tag)
719
self.checker_callback(pid, status, command)
720
except OSError as error:
721
logger.error("Failed to start subprocess",
215
self.checker = subprocess.\
217
stdout=subprocess.PIPE,
218
close_fds=True, shell=True,
220
self.checker_callback_tag = gobject.\
221
child_watch_add(self.checker.pid,
224
except subprocess.OSError, error:
225
sys.stderr.write(u"Failed to start subprocess: %s\n"
723
227
# Re-run this periodically if run by gobject.timeout_add
726
229
def stop_checker(self):
727
230
"""Force the checker process, if any, to stop."""
728
if self.checker_callback_tag:
729
gobject.source_remove(self.checker_callback_tag)
730
self.checker_callback_tag = None
731
if getattr(self, "checker", None) is None:
231
if not hasattr(self, "checker") or self.checker is None:
733
logger.debug("Stopping checker for %(name)s", vars(self))
735
self.checker.terminate()
737
#if self.checker.poll() is None:
738
# self.checker.kill()
739
except OSError as error:
740
if error.errno != errno.ESRCH: # No such process
745
def dbus_service_property(dbus_interface, signature="v",
746
access="readwrite", byte_arrays=False):
747
"""Decorators for marking methods of a DBusObjectWithProperties to
748
become properties on the D-Bus.
750
The decorated method will be called with no arguments by "Get"
751
and with one argument by "Set".
753
The parameters, where they are supported, are the same as
754
dbus.service.method, except there is only "signature", since the
755
type from Get() and the type sent to Set() is the same.
757
# Encoding deeply encoded byte arrays is not supported yet by the
758
# "Set" method, so we fail early here:
759
if byte_arrays and signature != "ay":
760
raise ValueError("Byte arrays not supported for non-'ay'"
761
" signature %r" % signature)
763
func._dbus_is_property = True
764
func._dbus_interface = dbus_interface
765
func._dbus_signature = signature
766
func._dbus_access = access
767
func._dbus_name = func.__name__
768
if func._dbus_name.endswith("_dbus_property"):
769
func._dbus_name = func._dbus_name[:-14]
770
func._dbus_get_args_options = {'byte_arrays': byte_arrays }
775
def dbus_interface_annotations(dbus_interface):
776
"""Decorator for marking functions returning interface annotations.
780
@dbus_interface_annotations("org.example.Interface")
781
def _foo(self): # Function name does not matter
782
return {"org.freedesktop.DBus.Deprecated": "true",
783
"org.freedesktop.DBus.Property.EmitsChangedSignal":
787
func._dbus_is_interface = True
788
func._dbus_interface = dbus_interface
789
func._dbus_name = dbus_interface
794
def dbus_annotations(annotations):
795
"""Decorator to annotate D-Bus methods, signals or properties
798
@dbus_service_property("org.example.Interface", signature="b",
800
@dbus_annotations({{"org.freedesktop.DBus.Deprecated": "true",
801
"org.freedesktop.DBus.Property."
802
"EmitsChangedSignal": "false"})
803
def Property_dbus_property(self):
804
return dbus.Boolean(False)
807
func._dbus_annotations = annotations
812
class DBusPropertyException(dbus.exceptions.DBusException):
813
"""A base class for D-Bus property-related exceptions
815
def __unicode__(self):
816
return unicode(str(self))
819
class DBusPropertyAccessException(DBusPropertyException):
820
"""A property's access permissions disallows an operation.
825
class DBusPropertyNotFound(DBusPropertyException):
826
"""An attempt was made to access a non-existing property.
831
class DBusObjectWithProperties(dbus.service.Object):
832
"""A D-Bus object with properties.
834
Classes inheriting from this can use the dbus_service_property
835
decorator to expose methods as D-Bus properties. It exposes the
836
standard Get(), Set(), and GetAll() methods on the D-Bus.
840
def _is_dbus_thing(thing):
841
"""Returns a function testing if an attribute is a D-Bus thing
843
If called like _is_dbus_thing("method") it returns a function
844
suitable for use as predicate to inspect.getmembers().
846
return lambda obj: getattr(obj, "_dbus_is_{0}".format(thing),
849
def _get_all_dbus_things(self, thing):
850
"""Returns a generator of (name, attribute) pairs
852
return ((getattr(athing.__get__(self), "_dbus_name",
854
athing.__get__(self))
855
for cls in self.__class__.__mro__
857
inspect.getmembers(cls,
858
self._is_dbus_thing(thing)))
860
def _get_dbus_property(self, interface_name, property_name):
861
"""Returns a bound method if one exists which is a D-Bus
862
property with the specified name and interface.
864
for cls in self.__class__.__mro__:
865
for name, value in (inspect.getmembers
867
self._is_dbus_thing("property"))):
868
if (value._dbus_name == property_name
869
and value._dbus_interface == interface_name):
870
return value.__get__(self)
873
raise DBusPropertyNotFound(self.dbus_object_path + ":"
874
+ interface_name + "."
877
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ss",
879
def Get(self, interface_name, property_name):
880
"""Standard D-Bus property Get() method, see D-Bus standard.
882
prop = self._get_dbus_property(interface_name, property_name)
883
if prop._dbus_access == "write":
884
raise DBusPropertyAccessException(property_name)
886
if not hasattr(value, "variant_level"):
888
return type(value)(value, variant_level=value.variant_level+1)
890
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
891
def Set(self, interface_name, property_name, value):
892
"""Standard D-Bus property Set() method, see D-Bus standard.
894
prop = self._get_dbus_property(interface_name, property_name)
895
if prop._dbus_access == "read":
896
raise DBusPropertyAccessException(property_name)
897
if prop._dbus_get_args_options["byte_arrays"]:
898
# The byte_arrays option is not supported yet on
899
# signatures other than "ay".
900
if prop._dbus_signature != "ay":
902
value = dbus.ByteArray(b''.join(chr(byte)
906
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s",
907
out_signature="a{sv}")
908
def GetAll(self, interface_name):
909
"""Standard D-Bus property GetAll() method, see D-Bus
912
Note: Will not include properties with access="write".
915
for name, prop in self._get_all_dbus_things("property"):
917
and interface_name != prop._dbus_interface):
918
# Interface non-empty but did not match
920
# Ignore write-only properties
921
if prop._dbus_access == "write":
924
if not hasattr(value, "variant_level"):
925
properties[name] = value
927
properties[name] = type(value)(value, variant_level=
928
value.variant_level+1)
929
return dbus.Dictionary(properties, signature="sv")
931
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
933
path_keyword='object_path',
934
connection_keyword='connection')
935
def Introspect(self, object_path, connection):
936
"""Overloading of standard D-Bus method.
938
Inserts property tags and interface annotation tags.
940
xmlstring = dbus.service.Object.Introspect(self, object_path,
943
document = xml.dom.minidom.parseString(xmlstring)
944
def make_tag(document, name, prop):
945
e = document.createElement("property")
946
e.setAttribute("name", name)
947
e.setAttribute("type", prop._dbus_signature)
948
e.setAttribute("access", prop._dbus_access)
950
for if_tag in document.getElementsByTagName("interface"):
952
for tag in (make_tag(document, name, prop)
954
in self._get_all_dbus_things("property")
955
if prop._dbus_interface
956
== if_tag.getAttribute("name")):
957
if_tag.appendChild(tag)
958
# Add annotation tags
959
for typ in ("method", "signal", "property"):
960
for tag in if_tag.getElementsByTagName(typ):
962
for name, prop in (self.
963
_get_all_dbus_things(typ)):
964
if (name == tag.getAttribute("name")
965
and prop._dbus_interface
966
== if_tag.getAttribute("name")):
967
annots.update(getattr
971
for name, value in annots.iteritems():
972
ann_tag = document.createElement(
974
ann_tag.setAttribute("name", name)
975
ann_tag.setAttribute("value", value)
976
tag.appendChild(ann_tag)
977
# Add interface annotation tags
978
for annotation, value in dict(
980
*(annotations().iteritems()
981
for name, annotations in
982
self._get_all_dbus_things("interface")
983
if name == if_tag.getAttribute("name")
985
ann_tag = document.createElement("annotation")
986
ann_tag.setAttribute("name", annotation)
987
ann_tag.setAttribute("value", value)
988
if_tag.appendChild(ann_tag)
989
# Add the names to the return values for the
990
# "org.freedesktop.DBus.Properties" methods
991
if (if_tag.getAttribute("name")
992
== "org.freedesktop.DBus.Properties"):
993
for cn in if_tag.getElementsByTagName("method"):
994
if cn.getAttribute("name") == "Get":
995
for arg in cn.getElementsByTagName("arg"):
996
if (arg.getAttribute("direction")
998
arg.setAttribute("name", "value")
999
elif cn.getAttribute("name") == "GetAll":
1000
for arg in cn.getElementsByTagName("arg"):
1001
if (arg.getAttribute("direction")
1003
arg.setAttribute("name", "props")
1004
xmlstring = document.toxml("utf-8")
1006
except (AttributeError, xml.dom.DOMException,
1007
xml.parsers.expat.ExpatError) as error:
1008
logger.error("Failed to override Introspection method",
1013
def datetime_to_dbus (dt, variant_level=0):
1014
"""Convert a UTC datetime.datetime() to a D-Bus type."""
1016
return dbus.String("", variant_level = variant_level)
1017
return dbus.String(dt.isoformat(),
1018
variant_level=variant_level)
1021
class AlternateDBusNamesMetaclass(DBusObjectWithProperties
1023
"""Applied to an empty subclass of a D-Bus object, this metaclass
1024
will add additional D-Bus attributes matching a certain pattern.
1026
def __new__(mcs, name, bases, attr):
1027
# Go through all the base classes which could have D-Bus
1028
# methods, signals, or properties in them
1029
old_interface_names = []
1030
for base in (b for b in bases
1031
if issubclass(b, dbus.service.Object)):
1032
# Go though all attributes of the base class
1033
for attrname, attribute in inspect.getmembers(base):
1034
# Ignore non-D-Bus attributes, and D-Bus attributes
1035
# with the wrong interface name
1036
if (not hasattr(attribute, "_dbus_interface")
1037
or not attribute._dbus_interface
1038
.startswith("se.recompile.Mandos")):
1040
# Create an alternate D-Bus interface name based on
1042
alt_interface = (attribute._dbus_interface
1043
.replace("se.recompile.Mandos",
1044
"se.bsnet.fukt.Mandos"))
1045
if alt_interface != attribute._dbus_interface:
1046
old_interface_names.append(alt_interface)
1047
# Is this a D-Bus signal?
1048
if getattr(attribute, "_dbus_is_signal", False):
1049
# Extract the original non-method function by
1051
nonmethod_func = (dict(
1052
zip(attribute.func_code.co_freevars,
1053
attribute.__closure__))["func"]
1055
# Create a new, but exactly alike, function
1056
# object, and decorate it to be a new D-Bus signal
1057
# with the alternate D-Bus interface name
1058
new_function = (dbus.service.signal
1060
attribute._dbus_signature)
1061
(types.FunctionType(
1062
nonmethod_func.func_code,
1063
nonmethod_func.func_globals,
1064
nonmethod_func.func_name,
1065
nonmethod_func.func_defaults,
1066
nonmethod_func.func_closure)))
1067
# Copy annotations, if any
1069
new_function._dbus_annotations = (
1070
dict(attribute._dbus_annotations))
1071
except AttributeError:
1073
# Define a creator of a function to call both the
1074
# old and new functions, so both the old and new
1075
# signals gets sent when the function is called
1076
def fixscope(func1, func2):
1077
"""This function is a scope container to pass
1078
func1 and func2 to the "call_both" function
1079
outside of its arguments"""
1080
def call_both(*args, **kwargs):
1081
"""This function will emit two D-Bus
1082
signals by calling func1 and func2"""
1083
func1(*args, **kwargs)
1084
func2(*args, **kwargs)
1086
# Create the "call_both" function and add it to
1088
attr[attrname] = fixscope(attribute,
1090
# Is this a D-Bus method?
1091
elif getattr(attribute, "_dbus_is_method", False):
1092
# Create a new, but exactly alike, function
1093
# object. Decorate it to be a new D-Bus method
1094
# with the alternate D-Bus interface name. Add it
1096
attr[attrname] = (dbus.service.method
1098
attribute._dbus_in_signature,
1099
attribute._dbus_out_signature)
1101
(attribute.func_code,
1102
attribute.func_globals,
1103
attribute.func_name,
1104
attribute.func_defaults,
1105
attribute.func_closure)))
1106
# Copy annotations, if any
1108
attr[attrname]._dbus_annotations = (
1109
dict(attribute._dbus_annotations))
1110
except AttributeError:
1112
# Is this a D-Bus property?
1113
elif getattr(attribute, "_dbus_is_property", False):
1114
# Create a new, but exactly alike, function
1115
# object, and decorate it to be a new D-Bus
1116
# property with the alternate D-Bus interface
1117
# name. Add it to the class.
1118
attr[attrname] = (dbus_service_property
1120
attribute._dbus_signature,
1121
attribute._dbus_access,
1123
._dbus_get_args_options
1126
(attribute.func_code,
1127
attribute.func_globals,
1128
attribute.func_name,
1129
attribute.func_defaults,
1130
attribute.func_closure)))
1131
# Copy annotations, if any
1133
attr[attrname]._dbus_annotations = (
1134
dict(attribute._dbus_annotations))
1135
except AttributeError:
1137
# Is this a D-Bus interface?
1138
elif getattr(attribute, "_dbus_is_interface", False):
1139
# Create a new, but exactly alike, function
1140
# object. Decorate it to be a new D-Bus interface
1141
# with the alternate D-Bus interface name. Add it
1143
attr[attrname] = (dbus_interface_annotations
1146
(attribute.func_code,
1147
attribute.func_globals,
1148
attribute.func_name,
1149
attribute.func_defaults,
1150
attribute.func_closure)))
1151
# Deprecate all old interfaces
1152
basename="_AlternateDBusNamesMetaclass_interface_annotation{0}"
1153
for old_interface_name in old_interface_names:
1154
@dbus_interface_annotations(old_interface_name)
1156
return { "org.freedesktop.DBus.Deprecated": "true" }
1157
# Find an unused name
1158
for aname in (basename.format(i) for i in
1160
if aname not in attr:
1163
return type.__new__(mcs, name, bases, attr)
1166
class ClientDBus(Client, DBusObjectWithProperties):
1167
"""A Client class using D-Bus
1170
dbus_object_path: dbus.ObjectPath
1171
bus: dbus.SystemBus()
1174
runtime_expansions = (Client.runtime_expansions
1175
+ ("dbus_object_path",))
1177
# dbus.service.Object doesn't use super(), so we can't either.
1179
def __init__(self, bus = None, *args, **kwargs):
1181
Client.__init__(self, *args, **kwargs)
1182
# Only now, when this client is initialized, can it show up on
1184
client_object_name = unicode(self.name).translate(
1185
{ord("."): ord("_"),
1186
ord("-"): ord("_")})
1187
self.dbus_object_path = (dbus.ObjectPath
1188
("/clients/" + client_object_name))
1189
DBusObjectWithProperties.__init__(self, self.bus,
1190
self.dbus_object_path)
1192
def notifychangeproperty(transform_func,
1193
dbus_name, type_func=lambda x: x,
1195
""" Modify a variable so that it's a property which announces
1196
its changes to DBus.
1198
transform_fun: Function that takes a value and a variant_level
1199
and transforms it to a D-Bus type.
1200
dbus_name: D-Bus name of the variable
1201
type_func: Function that transform the value before sending it
1202
to the D-Bus. Default: no transform
1203
variant_level: D-Bus variant level. Default: 1
1205
attrname = "_{0}".format(dbus_name)
1206
def setter(self, value):
1207
if hasattr(self, "dbus_object_path"):
1208
if (not hasattr(self, attrname) or
1209
type_func(getattr(self, attrname, None))
1210
!= type_func(value)):
1211
dbus_value = transform_func(type_func(value),
1214
self.PropertyChanged(dbus.String(dbus_name),
1216
setattr(self, attrname, value)
1218
return property(lambda self: getattr(self, attrname), setter)
1221
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1222
approvals_pending = notifychangeproperty(dbus.Boolean,
1225
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1226
last_enabled = notifychangeproperty(datetime_to_dbus,
1228
checker = notifychangeproperty(dbus.Boolean, "CheckerRunning",
1229
type_func = lambda checker:
1230
checker is not None)
1231
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1233
last_checker_status = notifychangeproperty(dbus.Int16,
1234
"LastCheckerStatus")
1235
last_approval_request = notifychangeproperty(
1236
datetime_to_dbus, "LastApprovalRequest")
1237
approved_by_default = notifychangeproperty(dbus.Boolean,
1238
"ApprovedByDefault")
1239
approval_delay = notifychangeproperty(dbus.UInt64,
1242
timedelta_to_milliseconds)
1243
approval_duration = notifychangeproperty(
1244
dbus.UInt64, "ApprovalDuration",
1245
type_func = timedelta_to_milliseconds)
1246
host = notifychangeproperty(dbus.String, "Host")
1247
timeout = notifychangeproperty(dbus.UInt64, "Timeout",
1249
timedelta_to_milliseconds)
1250
extended_timeout = notifychangeproperty(
1251
dbus.UInt64, "ExtendedTimeout",
1252
type_func = timedelta_to_milliseconds)
1253
interval = notifychangeproperty(dbus.UInt64,
1256
timedelta_to_milliseconds)
1257
checker_command = notifychangeproperty(dbus.String, "Checker")
1259
del notifychangeproperty
1261
def __del__(self, *args, **kwargs):
1263
self.remove_from_connection()
1266
if hasattr(DBusObjectWithProperties, "__del__"):
1267
DBusObjectWithProperties.__del__(self, *args, **kwargs)
1268
Client.__del__(self, *args, **kwargs)
1270
def checker_callback(self, pid, condition, command,
233
gobject.source_remove(self.checker_callback_tag)
1272
234
self.checker_callback_tag = None
235
os.kill(self.checker.pid, signal.SIGTERM)
236
if self.checker.poll() is None:
237
os.kill(self.checker.pid, signal.SIGKILL)
1273
238
self.checker = None
1274
if os.WIFEXITED(condition):
1275
exitstatus = os.WEXITSTATUS(condition)
1277
self.CheckerCompleted(dbus.Int16(exitstatus),
1278
dbus.Int64(condition),
1279
dbus.String(command))
1282
self.CheckerCompleted(dbus.Int16(-1),
1283
dbus.Int64(condition),
1284
dbus.String(command))
1286
return Client.checker_callback(self, pid, condition, command,
1289
def start_checker(self, *args, **kwargs):
1290
old_checker = self.checker
1291
if self.checker is not None:
1292
old_checker_pid = self.checker.pid
1294
old_checker_pid = None
1295
r = Client.start_checker(self, *args, **kwargs)
1296
# Only if new checker process was started
1297
if (self.checker is not None
1298
and old_checker_pid != self.checker.pid):
1300
self.CheckerStarted(self.current_checker_command)
1303
def _reset_approved(self):
1304
self.approved = None
1307
def approve(self, value=True):
1308
self.send_changedstate()
1309
self.approved = value
1310
gobject.timeout_add(timedelta_to_milliseconds
1311
(self.approval_duration),
1312
self._reset_approved)
1315
## D-Bus methods, signals & properties
1316
_interface = "se.recompile.Mandos.Client"
1320
@dbus_interface_annotations(_interface)
1322
return { "org.freedesktop.DBus.Property.EmitsChangedSignal":
1327
# CheckerCompleted - signal
1328
@dbus.service.signal(_interface, signature="nxs")
1329
def CheckerCompleted(self, exitcode, waitstatus, command):
1333
# CheckerStarted - signal
1334
@dbus.service.signal(_interface, signature="s")
1335
def CheckerStarted(self, command):
1339
# PropertyChanged - signal
1340
@dbus.service.signal(_interface, signature="sv")
1341
def PropertyChanged(self, property, value):
1345
# GotSecret - signal
1346
@dbus.service.signal(_interface)
1347
def GotSecret(self):
1349
Is sent after a successful transfer of secret from the Mandos
1350
server to mandos-client
1355
@dbus.service.signal(_interface, signature="s")
1356
def Rejected(self, reason):
1360
# NeedApproval - signal
1361
@dbus.service.signal(_interface, signature="tb")
1362
def NeedApproval(self, timeout, default):
1364
return self.need_approval()
1369
@dbus.service.method(_interface, in_signature="b")
1370
def Approve(self, value):
1373
# CheckedOK - method
1374
@dbus.service.method(_interface)
1375
def CheckedOK(self):
1379
@dbus.service.method(_interface)
1384
# StartChecker - method
1385
@dbus.service.method(_interface)
1386
def StartChecker(self):
1388
self.start_checker()
1391
@dbus.service.method(_interface)
1396
# StopChecker - method
1397
@dbus.service.method(_interface)
1398
def StopChecker(self):
1403
# ApprovalPending - property
1404
@dbus_service_property(_interface, signature="b", access="read")
1405
def ApprovalPending_dbus_property(self):
1406
return dbus.Boolean(bool(self.approvals_pending))
1408
# ApprovedByDefault - property
1409
@dbus_service_property(_interface, signature="b",
1411
def ApprovedByDefault_dbus_property(self, value=None):
1412
if value is None: # get
1413
return dbus.Boolean(self.approved_by_default)
1414
self.approved_by_default = bool(value)
1416
# ApprovalDelay - property
1417
@dbus_service_property(_interface, signature="t",
1419
def ApprovalDelay_dbus_property(self, value=None):
1420
if value is None: # get
1421
return dbus.UInt64(self.approval_delay_milliseconds())
1422
self.approval_delay = datetime.timedelta(0, 0, 0, value)
1424
# ApprovalDuration - property
1425
@dbus_service_property(_interface, signature="t",
1427
def ApprovalDuration_dbus_property(self, value=None):
1428
if value is None: # get
1429
return dbus.UInt64(timedelta_to_milliseconds(
1430
self.approval_duration))
1431
self.approval_duration = datetime.timedelta(0, 0, 0, value)
1434
@dbus_service_property(_interface, signature="s", access="read")
1435
def Name_dbus_property(self):
1436
return dbus.String(self.name)
1438
# Fingerprint - property
1439
@dbus_service_property(_interface, signature="s", access="read")
1440
def Fingerprint_dbus_property(self):
1441
return dbus.String(self.fingerprint)
1444
@dbus_service_property(_interface, signature="s",
1446
def Host_dbus_property(self, value=None):
1447
if value is None: # get
1448
return dbus.String(self.host)
1449
self.host = unicode(value)
1451
# Created - property
1452
@dbus_service_property(_interface, signature="s", access="read")
1453
def Created_dbus_property(self):
1454
return datetime_to_dbus(self.created)
1456
# LastEnabled - property
1457
@dbus_service_property(_interface, signature="s", access="read")
1458
def LastEnabled_dbus_property(self):
1459
return datetime_to_dbus(self.last_enabled)
1461
# Enabled - property
1462
@dbus_service_property(_interface, signature="b",
1464
def Enabled_dbus_property(self, value=None):
1465
if value is None: # get
1466
return dbus.Boolean(self.enabled)
1472
# LastCheckedOK - property
1473
@dbus_service_property(_interface, signature="s",
1475
def LastCheckedOK_dbus_property(self, value=None):
1476
if value is not None:
1479
return datetime_to_dbus(self.last_checked_ok)
1481
# LastCheckerStatus - property
1482
@dbus_service_property(_interface, signature="n",
1484
def LastCheckerStatus_dbus_property(self):
1485
return dbus.Int16(self.last_checker_status)
1487
# Expires - property
1488
@dbus_service_property(_interface, signature="s", access="read")
1489
def Expires_dbus_property(self):
1490
return datetime_to_dbus(self.expires)
1492
# LastApprovalRequest - property
1493
@dbus_service_property(_interface, signature="s", access="read")
1494
def LastApprovalRequest_dbus_property(self):
1495
return datetime_to_dbus(self.last_approval_request)
1497
# Timeout - property
1498
@dbus_service_property(_interface, signature="t",
1500
def Timeout_dbus_property(self, value=None):
1501
if value is None: # get
1502
return dbus.UInt64(self.timeout_milliseconds())
1503
self.timeout = datetime.timedelta(0, 0, 0, value)
1504
# Reschedule timeout
1506
now = datetime.datetime.utcnow()
1507
time_to_die = timedelta_to_milliseconds(
1508
(self.last_checked_ok + self.timeout) - now)
1509
if time_to_die <= 0:
1510
# The timeout has passed
1513
self.expires = (now +
1514
datetime.timedelta(milliseconds =
1516
if (getattr(self, "disable_initiator_tag", None)
1519
gobject.source_remove(self.disable_initiator_tag)
1520
self.disable_initiator_tag = (gobject.timeout_add
1524
# ExtendedTimeout - property
1525
@dbus_service_property(_interface, signature="t",
1527
def ExtendedTimeout_dbus_property(self, value=None):
1528
if value is None: # get
1529
return dbus.UInt64(self.extended_timeout_milliseconds())
1530
self.extended_timeout = datetime.timedelta(0, 0, 0, value)
1532
# Interval - property
1533
@dbus_service_property(_interface, signature="t",
1535
def Interval_dbus_property(self, value=None):
1536
if value is None: # get
1537
return dbus.UInt64(self.interval_milliseconds())
1538
self.interval = datetime.timedelta(0, 0, 0, value)
1539
if getattr(self, "checker_initiator_tag", None) is None:
1542
# Reschedule checker run
1543
gobject.source_remove(self.checker_initiator_tag)
1544
self.checker_initiator_tag = (gobject.timeout_add
1545
(value, self.start_checker))
1546
self.start_checker() # Start one now, too
1548
# Checker - property
1549
@dbus_service_property(_interface, signature="s",
1551
def Checker_dbus_property(self, value=None):
1552
if value is None: # get
1553
return dbus.String(self.checker_command)
1554
self.checker_command = unicode(value)
1556
# CheckerRunning - property
1557
@dbus_service_property(_interface, signature="b",
1559
def CheckerRunning_dbus_property(self, value=None):
1560
if value is None: # get
1561
return dbus.Boolean(self.checker is not None)
1563
self.start_checker()
1567
# ObjectPath - property
1568
@dbus_service_property(_interface, signature="o", access="read")
1569
def ObjectPath_dbus_property(self):
1570
return self.dbus_object_path # is already a dbus.ObjectPath
1573
@dbus_service_property(_interface, signature="ay",
1574
access="write", byte_arrays=True)
1575
def Secret_dbus_property(self, value):
1576
self.secret = str(value)
1581
class ProxyClient(object):
1582
def __init__(self, child_pipe, fpr, address):
1583
self._pipe = child_pipe
1584
self._pipe.send(('init', fpr, address))
1585
if not self._pipe.recv():
1588
def __getattribute__(self, name):
1590
return super(ProxyClient, self).__getattribute__(name)
1591
self._pipe.send(('getattr', name))
1592
data = self._pipe.recv()
1593
if data[0] == 'data':
1595
if data[0] == 'function':
1596
def func(*args, **kwargs):
1597
self._pipe.send(('funcall', name, args, kwargs))
1598
return self._pipe.recv()[1]
1601
def __setattr__(self, name, value):
1603
return super(ProxyClient, self).__setattr__(name, value)
1604
self._pipe.send(('setattr', name, value))
1607
class ClientDBusTransitional(ClientDBus):
1608
__metaclass__ = AlternateDBusNamesMetaclass
1611
class ClientHandler(socketserver.BaseRequestHandler, object):
1612
"""A class to handle client connections.
1614
Instantiated once for each connection to handle it.
239
def still_valid(self, now=None):
240
"""Has the timeout not yet passed for this client?"""
242
now = datetime.datetime.now()
243
if self.last_seen is None:
244
return now < (self.created + self.timeout)
246
return now < (self.last_seen + self.timeout)
249
def peer_certificate(session):
250
# If not an OpenPGP certificate...
251
if gnutls.library.functions.gnutls_certificate_type_get\
252
(session._c_object) \
253
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
254
# ...do the normal thing
255
return session.peer_certificate
256
list_size = ctypes.c_uint()
257
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
258
(session._c_object, ctypes.byref(list_size))
259
if list_size.value == 0:
262
return ctypes.string_at(cert.data, cert.size)
265
def fingerprint(openpgp):
266
# New empty GnuTLS certificate
267
crt = gnutls.library.types.gnutls_openpgp_crt_t()
268
gnutls.library.functions.gnutls_openpgp_crt_init\
270
# New GnuTLS "datum" with the OpenPGP public key
271
datum = gnutls.library.types.gnutls_datum_t\
272
(ctypes.cast(ctypes.c_char_p(openpgp),
273
ctypes.POINTER(ctypes.c_ubyte)),
274
ctypes.c_uint(len(openpgp)))
275
# Import the OpenPGP public key into the certificate
276
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
279
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
280
# New buffer for the fingerprint
281
buffer = ctypes.create_string_buffer(20)
282
buffer_length = ctypes.c_size_t()
283
# Get the fingerprint from the certificate into the buffer
284
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
285
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
286
# Deinit the certificate
287
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
288
# Convert the buffer to a Python bytestring
289
fpr = ctypes.string_at(buffer, buffer_length.value)
290
# Convert the bytestring to hexadecimal notation
291
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
295
class tcp_handler(SocketServer.BaseRequestHandler, object):
296
"""A TCP request handler class.
297
Instantiated by IPv6_TCPServer for each request to handle it.
1615
298
Note: This will run in its own forked process."""
1617
300
def handle(self):
1618
with contextlib.closing(self.server.child_pipe) as child_pipe:
1619
logger.info("TCP connection from: %s",
1620
unicode(self.client_address))
1621
logger.debug("Pipe FD: %d",
1622
self.server.child_pipe.fileno())
1624
session = (gnutls.connection
1625
.ClientSession(self.request,
1627
.X509Credentials()))
1629
# Note: gnutls.connection.X509Credentials is really a
1630
# generic GnuTLS certificate credentials object so long as
1631
# no X.509 keys are added to it. Therefore, we can use it
1632
# here despite using OpenPGP certificates.
1634
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
1635
# "+AES-256-CBC", "+SHA1",
1636
# "+COMP-NULL", "+CTYPE-OPENPGP",
1638
# Use a fallback default, since this MUST be set.
1639
priority = self.server.gnutls_priority
1640
if priority is None:
1642
(gnutls.library.functions
1643
.gnutls_priority_set_direct(session._c_object,
1646
# Start communication using the Mandos protocol
1647
# Get protocol number
1648
line = self.request.makefile().readline()
1649
logger.debug("Protocol version: %r", line)
1651
if int(line.strip().split()[0]) > 1:
1653
except (ValueError, IndexError, RuntimeError) as error:
1654
logger.error("Unknown protocol version: %s", error)
1657
# Start GnuTLS connection
1660
except gnutls.errors.GNUTLSError as error:
1661
logger.warning("Handshake failed: %s", error)
1662
# Do not run session.bye() here: the session is not
1663
# established. Just abandon the request.
1665
logger.debug("Handshake succeeded")
1667
approval_required = False
1670
fpr = self.fingerprint(self.peer_certificate
1673
gnutls.errors.GNUTLSError) as error:
1674
logger.warning("Bad certificate: %s", error)
1676
logger.debug("Fingerprint: %s", fpr)
1679
client = ProxyClient(child_pipe, fpr,
1680
self.client_address)
1684
if client.approval_delay:
1685
delay = client.approval_delay
1686
client.approvals_pending += 1
1687
approval_required = True
1690
if not client.enabled:
1691
logger.info("Client %s is disabled",
1693
if self.server.use_dbus:
1695
client.Rejected("Disabled")
1698
if client.approved or not client.approval_delay:
1699
#We are approved or approval is disabled
1701
elif client.approved is None:
1702
logger.info("Client %s needs approval",
1704
if self.server.use_dbus:
1706
client.NeedApproval(
1707
client.approval_delay_milliseconds(),
1708
client.approved_by_default)
1710
logger.warning("Client %s was not approved",
1712
if self.server.use_dbus:
1714
client.Rejected("Denied")
1717
#wait until timeout or approved
1718
time = datetime.datetime.now()
1719
client.changedstate.acquire()
1720
(client.changedstate.wait
1721
(float(client.timedelta_to_milliseconds(delay)
1723
client.changedstate.release()
1724
time2 = datetime.datetime.now()
1725
if (time2 - time) >= delay:
1726
if not client.approved_by_default:
1727
logger.warning("Client %s timed out while"
1728
" waiting for approval",
1730
if self.server.use_dbus:
1732
client.Rejected("Approval timed out")
1737
delay -= time2 - time
1740
while sent_size < len(client.secret):
1742
sent = session.send(client.secret[sent_size:])
1743
except gnutls.errors.GNUTLSError as error:
1744
logger.warning("gnutls send failed")
1746
logger.debug("Sent: %d, remaining: %d",
1747
sent, len(client.secret)
1748
- (sent_size + sent))
1751
logger.info("Sending secret to %s", client.name)
1752
# bump the timeout using extended_timeout
1753
client.bump_timeout(client.extended_timeout)
1754
if self.server.use_dbus:
1759
if approval_required:
1760
client.approvals_pending -= 1
1763
except gnutls.errors.GNUTLSError as error:
1764
logger.warning("GnuTLS bye failed")
1767
def peer_certificate(session):
1768
"Return the peer's OpenPGP certificate as a bytestring"
1769
# If not an OpenPGP certificate...
1770
if (gnutls.library.functions
1771
.gnutls_certificate_type_get(session._c_object)
1772
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1773
# ...do the normal thing
1774
return session.peer_certificate
1775
list_size = ctypes.c_uint(1)
1776
cert_list = (gnutls.library.functions
1777
.gnutls_certificate_get_peers
1778
(session._c_object, ctypes.byref(list_size)))
1779
if not bool(cert_list) and list_size.value != 0:
1780
raise gnutls.errors.GNUTLSError("error getting peer"
1782
if list_size.value == 0:
1785
return ctypes.string_at(cert.data, cert.size)
1788
def fingerprint(openpgp):
1789
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1790
# New GnuTLS "datum" with the OpenPGP public key
1791
datum = (gnutls.library.types
1792
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1795
ctypes.c_uint(len(openpgp))))
1796
# New empty GnuTLS certificate
1797
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1798
(gnutls.library.functions
1799
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1800
# Import the OpenPGP public key into the certificate
1801
(gnutls.library.functions
1802
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1803
gnutls.library.constants
1804
.GNUTLS_OPENPGP_FMT_RAW))
1805
# Verify the self signature in the key
1806
crtverify = ctypes.c_uint()
1807
(gnutls.library.functions
1808
.gnutls_openpgp_crt_verify_self(crt, 0,
1809
ctypes.byref(crtverify)))
1810
if crtverify.value != 0:
1811
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1812
raise (gnutls.errors.CertificateSecurityError
1814
# New buffer for the fingerprint
1815
buf = ctypes.create_string_buffer(20)
1816
buf_len = ctypes.c_size_t()
1817
# Get the fingerprint from the certificate into the buffer
1818
(gnutls.library.functions
1819
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1820
ctypes.byref(buf_len)))
1821
# Deinit the certificate
1822
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1823
# Convert the buffer to a Python bytestring
1824
fpr = ctypes.string_at(buf, buf_len.value)
1825
# Convert the bytestring to hexadecimal notation
1826
hex_fpr = binascii.hexlify(fpr).upper()
1830
class MultiprocessingMixIn(object):
1831
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
1832
def sub_process_main(self, request, address):
1834
self.finish_request(request, address)
1836
self.handle_error(request, address)
1837
self.close_request(request)
1839
def process_request(self, request, address):
1840
"""Start a new process to process the request."""
1841
proc = multiprocessing.Process(target = self.sub_process_main,
1848
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
1849
""" adds a pipe to the MixIn """
1850
def process_request(self, request, client_address):
1851
"""Overrides and wraps the original process_request().
1853
This function creates a new pipe in self.pipe
1855
parent_pipe, self.child_pipe = multiprocessing.Pipe()
1857
proc = MultiprocessingMixIn.process_request(self, request,
1859
self.child_pipe.close()
1860
self.add_pipe(parent_pipe, proc)
1862
def add_pipe(self, parent_pipe, proc):
1863
"""Dummy function; override as necessary"""
1864
raise NotImplementedError
1867
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1868
socketserver.TCPServer, object):
1869
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
302
sys.stderr.write(u"TCP request came\n")
303
sys.stderr.write(u"Request: %s\n" % self.request)
304
sys.stderr.write(u"Client Address: %s\n"
305
% unicode(self.client_address))
306
sys.stderr.write(u"Server: %s\n" % self.server)
307
session = gnutls.connection.ClientSession(self.request,
311
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
312
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
314
priority = "SECURE256"
316
gnutls.library.functions.gnutls_priority_set_direct\
317
(session._c_object, priority, None);
321
except gnutls.errors.GNUTLSError, error:
323
sys.stderr.write(u"Handshake failed: %s\n" % error)
324
# Do not run session.bye() here: the session is not
325
# established. Just abandon the request.
328
fpr = fingerprint(peer_certificate(session))
329
except (TypeError, gnutls.errors.GNUTLSError), error:
331
sys.stderr.write(u"Bad certificate: %s\n" % error)
335
sys.stderr.write(u"Fingerprint: %s\n" % fpr)
338
if c.fingerprint == fpr:
341
# Have to check if client.still_valid(), since it is possible
342
# that the client timed out while establishing the GnuTLS
344
if (not client) or (not client.still_valid()):
347
sys.stderr.write(u"Client %(name)s is invalid\n"
350
sys.stderr.write(u"Client not found for "
351
u"fingerprint: %s\n" % fpr)
355
while sent_size < len(client.secret):
356
sent = session.send(client.secret[sent_size:])
358
sys.stderr.write(u"Sent: %d, remaining: %d\n"
359
% (sent, len(client.secret)
360
- (sent_size + sent)))
365
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
366
"""IPv6 TCP server. Accepts 'None' as address and/or port.
1872
enabled: Boolean; whether this server is activated yet
1873
interface: None or a network interface name (string)
1874
use_ipv6: Boolean; to use IPv6 or not
368
options: Command line options
369
clients: Set() of Client objects
1876
def __init__(self, server_address, RequestHandlerClass,
1877
interface=None, use_ipv6=True):
1878
self.interface = interface
1880
self.address_family = socket.AF_INET6
1881
socketserver.TCPServer.__init__(self, server_address,
1882
RequestHandlerClass)
371
address_family = socket.AF_INET6
372
def __init__(self, *args, **kwargs):
373
if "options" in kwargs:
374
self.options = kwargs["options"]
375
del kwargs["options"]
376
if "clients" in kwargs:
377
self.clients = kwargs["clients"]
378
del kwargs["clients"]
379
return super(type(self), self).__init__(*args, **kwargs)
1883
380
def server_bind(self):
1884
381
"""This overrides the normal server_bind() function
1885
382
to bind to an interface if one was specified, and also NOT to
1886
383
bind to an address or port if they were not specified."""
1887
if self.interface is not None:
1888
if SO_BINDTODEVICE is None:
1889
logger.error("SO_BINDTODEVICE does not exist;"
1890
" cannot bind to interface %s",
1894
self.socket.setsockopt(socket.SOL_SOCKET,
1898
except socket.error as error:
1899
if error[0] == errno.EPERM:
1900
logger.error("No permission to"
1901
" bind to interface %s",
1903
elif error[0] == errno.ENOPROTOOPT:
1904
logger.error("SO_BINDTODEVICE not available;"
1905
" cannot bind to interface %s",
384
if self.options.interface:
385
if not hasattr(socket, "SO_BINDTODEVICE"):
386
# From /usr/include/asm-i486/socket.h
387
socket.SO_BINDTODEVICE = 25
389
self.socket.setsockopt(socket.SOL_SOCKET,
390
socket.SO_BINDTODEVICE,
391
self.options.interface)
392
except socket.error, error:
393
if error[0] == errno.EPERM:
394
sys.stderr.write(u"Warning: No permission to" \
395
u" bind to interface %s\n"
396
% self.options.interface)
1909
399
# Only bind(2) the socket if we really need to.
1910
400
if self.server_address[0] or self.server_address[1]:
1911
401
if not self.server_address[0]:
1912
if self.address_family == socket.AF_INET6:
1913
any_address = "::" # in6addr_any
1915
any_address = socket.INADDR_ANY
1916
self.server_address = (any_address,
403
self.server_address = (in6addr_any,
1917
404
self.server_address[1])
1918
elif not self.server_address[1]:
405
elif self.server_address[1] is None:
1919
406
self.server_address = (self.server_address[0],
1921
# if self.interface:
1922
# self.server_address = (self.server_address[0],
1927
return socketserver.TCPServer.server_bind(self)
1930
class MandosServer(IPv6_TCPServer):
1934
clients: set of Client objects
1935
gnutls_priority GnuTLS priority string
1936
use_dbus: Boolean; to emit D-Bus signals or not
1938
Assumes a gobject.MainLoop event loop.
1940
def __init__(self, server_address, RequestHandlerClass,
1941
interface=None, use_ipv6=True, clients=None,
1942
gnutls_priority=None, use_dbus=True):
1943
self.enabled = False
1944
self.clients = clients
1945
if self.clients is None:
1947
self.use_dbus = use_dbus
1948
self.gnutls_priority = gnutls_priority
1949
IPv6_TCPServer.__init__(self, server_address,
1950
RequestHandlerClass,
1951
interface = interface,
1952
use_ipv6 = use_ipv6)
1953
def server_activate(self):
1955
return socketserver.TCPServer.server_activate(self)
1960
def add_pipe(self, parent_pipe, proc):
1961
# Call "handle_ipc" for both data and EOF events
1962
gobject.io_add_watch(parent_pipe.fileno(),
1963
gobject.IO_IN | gobject.IO_HUP,
1964
functools.partial(self.handle_ipc,
1969
def handle_ipc(self, source, condition, parent_pipe=None,
1970
proc = None, client_object=None):
1972
gobject.IO_IN: "IN", # There is data to read.
1973
gobject.IO_OUT: "OUT", # Data can be written (without
1975
gobject.IO_PRI: "PRI", # There is urgent data to read.
1976
gobject.IO_ERR: "ERR", # Error condition.
1977
gobject.IO_HUP: "HUP" # Hung up (the connection has been
1978
# broken, usually for pipes and
1981
conditions_string = ' | '.join(name
1983
condition_names.iteritems()
1984
if cond & condition)
1985
# error, or the other end of multiprocessing.Pipe has closed
1986
if condition & (gobject.IO_ERR | condition & gobject.IO_HUP):
1987
# Wait for other process to exit
1991
# Read a request from the child
1992
request = parent_pipe.recv()
1993
command = request[0]
1995
if command == 'init':
1997
address = request[2]
1999
for c in self.clients.itervalues():
2000
if c.fingerprint == fpr:
2004
logger.info("Client not found for fingerprint: %s, ad"
2005
"dress: %s", fpr, address)
2008
mandos_dbus_service.ClientNotFound(fpr,
2010
parent_pipe.send(False)
2013
gobject.io_add_watch(parent_pipe.fileno(),
2014
gobject.IO_IN | gobject.IO_HUP,
2015
functools.partial(self.handle_ipc,
2021
parent_pipe.send(True)
2022
# remove the old hook in favor of the new above hook on
2025
if command == 'funcall':
2026
funcname = request[1]
2030
parent_pipe.send(('data', getattr(client_object,
2034
if command == 'getattr':
2035
attrname = request[1]
2036
if callable(client_object.__getattribute__(attrname)):
2037
parent_pipe.send(('function',))
2039
parent_pipe.send(('data', client_object
2040
.__getattribute__(attrname)))
2042
if command == 'setattr':
2043
attrname = request[1]
2045
setattr(client_object, attrname, value)
408
return super(type(self), self).server_bind()
2050
411
def string_to_delta(interval):
2051
412
"""Parse a string and return a datetime.timedelta
2053
414
>>> string_to_delta('7d')
2054
415
datetime.timedelta(7)
2055
416
>>> string_to_delta('60s')
2058
419
datetime.timedelta(0, 3600)
2059
420
>>> string_to_delta('24h')
2060
421
datetime.timedelta(1)
2061
>>> string_to_delta('1w')
422
>>> string_to_delta(u'1w')
2062
423
datetime.timedelta(7)
2063
>>> string_to_delta('5m 30s')
2064
datetime.timedelta(0, 330)
2066
timevalue = datetime.timedelta(0)
2067
for s in interval.split():
2069
suffix = unicode(s[-1])
2072
delta = datetime.timedelta(value)
2074
delta = datetime.timedelta(0, value)
2076
delta = datetime.timedelta(0, 0, 0, 0, value)
2078
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
2080
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
2082
raise ValueError("Unknown suffix %r" % suffix)
2083
except (ValueError, IndexError) as e:
2084
raise ValueError(*(e.args))
2089
def daemon(nochdir = False, noclose = False):
2090
"""See daemon(3). Standard BSD Unix function.
2092
This should really exist as os.daemon, but it doesn't (yet)."""
2101
# Close all standard open file descriptors
2102
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2103
if not stat.S_ISCHR(os.fstat(null).st_mode):
2104
raise OSError(errno.ENODEV,
2105
"%s not a character device"
2107
os.dup2(null, sys.stdin.fileno())
2108
os.dup2(null, sys.stdout.fileno())
2109
os.dup2(null, sys.stderr.fileno())
2116
##################################################################
2117
# Parsing of options, both command line and config file
2119
parser = argparse.ArgumentParser()
2120
parser.add_argument("-v", "--version", action="version",
2121
version = "%%(prog)s %s" % version,
2122
help="show version number and exit")
2123
parser.add_argument("-i", "--interface", metavar="IF",
2124
help="Bind to interface IF")
2125
parser.add_argument("-a", "--address",
2126
help="Address to listen for requests on")
2127
parser.add_argument("-p", "--port", type=int,
2128
help="Port number to receive requests on")
2129
parser.add_argument("--check", action="store_true",
2130
help="Run self-test")
2131
parser.add_argument("--debug", action="store_true",
2132
help="Debug mode; run in foreground and log"
2134
parser.add_argument("--debuglevel", metavar="LEVEL",
2135
help="Debug level for stdout output")
2136
parser.add_argument("--priority", help="GnuTLS"
2137
" priority string (see GnuTLS documentation)")
2138
parser.add_argument("--servicename",
2139
metavar="NAME", help="Zeroconf service name")
2140
parser.add_argument("--configdir",
2141
default="/etc/mandos", metavar="DIR",
2142
help="Directory to search for configuration"
2144
parser.add_argument("--no-dbus", action="store_false",
2145
dest="use_dbus", help="Do not provide D-Bus"
2146
" system bus interface")
2147
parser.add_argument("--no-ipv6", action="store_false",
2148
dest="use_ipv6", help="Do not use IPv6")
2149
parser.add_argument("--no-restore", action="store_false",
2150
dest="restore", help="Do not restore stored"
2152
parser.add_argument("--statedir", metavar="DIR",
2153
help="Directory to save/restore state in")
2155
options = parser.parse_args()
426
suffix=unicode(interval[-1])
427
value=int(interval[:-1])
429
delta = datetime.timedelta(value)
431
delta = datetime.timedelta(0, value)
433
delta = datetime.timedelta(0, 0, 0, 0, value)
435
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
437
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
440
except (ValueError, IndexError):
446
"""From the Avahi server example code"""
447
global group, serviceName, serviceType, servicePort, serviceTXT, \
450
group = dbus.Interface(
451
bus.get_object( avahi.DBUS_NAME,
452
server.EntryGroupNew()),
453
avahi.DBUS_INTERFACE_ENTRY_GROUP)
454
group.connect_to_signal('StateChanged',
455
entry_group_state_changed)
457
sys.stderr.write(u"Adding service '%s' of type '%s' ...\n"
458
% (serviceName, serviceType))
461
serviceInterface, # interface
462
avahi.PROTO_INET6, # protocol
463
dbus.UInt32(0), # flags
464
serviceName, serviceType,
466
dbus.UInt16(servicePort),
467
avahi.string_array_to_txt_array(serviceTXT))
471
def remove_service():
472
"""From the Avahi server example code"""
475
if not group is None:
479
def server_state_changed(state):
480
"""From the Avahi server example code"""
481
if state == avahi.SERVER_COLLISION:
482
sys.stderr.write(u"WARNING: Server name collision\n")
484
elif state == avahi.SERVER_RUNNING:
488
def entry_group_state_changed(state, error):
489
"""From the Avahi server example code"""
490
global serviceName, server, rename_count
493
sys.stderr.write(u"state change: %i\n" % state)
495
if state == avahi.ENTRY_GROUP_ESTABLISHED:
497
sys.stderr.write(u"Service established.\n")
498
elif state == avahi.ENTRY_GROUP_COLLISION:
500
rename_count = rename_count - 1
502
name = server.GetAlternativeServiceName(name)
503
sys.stderr.write(u"WARNING: Service name collision, "
504
u"changing name to '%s' ...\n" % name)
509
sys.stderr.write(u"ERROR: No suitable service name found "
510
u"after %i retries, exiting.\n"
513
elif state == avahi.ENTRY_GROUP_FAILURE:
514
sys.stderr.write(u"Error in group state changed %s\n"
520
def if_nametoindex(interface):
521
"""Call the C function if_nametoindex()"""
523
libc = ctypes.cdll.LoadLibrary("libc.so.6")
524
return libc.if_nametoindex(interface)
525
except (OSError, AttributeError):
526
if "struct" not in sys.modules:
528
if "fcntl" not in sys.modules:
530
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
532
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
533
struct.pack("16s16x", interface))
535
interface_index = struct.unpack("I", ifreq[16:20])[0]
536
return interface_index
539
if __name__ == '__main__':
540
parser = OptionParser()
541
parser.add_option("-i", "--interface", type="string",
542
default=None, metavar="IF",
543
help="Bind to interface IF")
544
parser.add_option("--cert", type="string", default="cert.pem",
546
help="Public key certificate PEM file to use")
547
parser.add_option("--key", type="string", default="key.pem",
549
help="Private key PEM file to use")
550
parser.add_option("--ca", type="string", default="ca.pem",
552
help="Certificate Authority certificate PEM file to use")
553
parser.add_option("--crl", type="string", default="crl.pem",
555
help="Certificate Revokation List PEM file to use")
556
parser.add_option("-p", "--port", type="int", default=None,
557
help="Port number to receive requests on")
558
parser.add_option("--timeout", type="string", # Parsed later
560
help="Amount of downtime allowed for clients")
561
parser.add_option("--interval", type="string", # Parsed later
563
help="How often to check that a client is up")
564
parser.add_option("--check", action="store_true", default=False,
565
help="Run self-test")
566
parser.add_option("--debug", action="store_true", default=False,
568
(options, args) = parser.parse_args()
2157
570
if options.check:
2159
572
doctest.testmod()
2162
# Default values for config file for server-global settings
2163
server_defaults = { "interface": "",
2168
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2169
"servicename": "Mandos",
2174
"statedir": "/var/lib/mandos"
2177
# Parse config file for server-global settings
2178
server_config = configparser.SafeConfigParser(server_defaults)
2180
server_config.read(os.path.join(options.configdir,
2182
# Convert the SafeConfigParser object to a dict
2183
server_settings = server_config.defaults()
2184
# Use the appropriate methods on the non-string config options
2185
for option in ("debug", "use_dbus", "use_ipv6"):
2186
server_settings[option] = server_config.getboolean("DEFAULT",
2188
if server_settings["port"]:
2189
server_settings["port"] = server_config.getint("DEFAULT",
2193
# Override the settings from the config file with command line
2195
for option in ("interface", "address", "port", "debug",
2196
"priority", "servicename", "configdir",
2197
"use_dbus", "use_ipv6", "debuglevel", "restore",
2199
value = getattr(options, option)
2200
if value is not None:
2201
server_settings[option] = value
2203
# Force all strings to be unicode
2204
for option in server_settings.keys():
2205
if type(server_settings[option]) is str:
2206
server_settings[option] = unicode(server_settings[option])
2207
# Now we have our good server settings in "server_settings"
2209
##################################################################
2212
debug = server_settings["debug"]
2213
debuglevel = server_settings["debuglevel"]
2214
use_dbus = server_settings["use_dbus"]
2215
use_ipv6 = server_settings["use_ipv6"]
2216
stored_state_path = os.path.join(server_settings["statedir"],
2220
initlogger(debug, logging.DEBUG)
2225
level = getattr(logging, debuglevel.upper())
2226
initlogger(debug, level)
2228
if server_settings["servicename"] != "Mandos":
2229
syslogger.setFormatter(logging.Formatter
2230
('Mandos (%s) [%%(process)d]:'
2231
' %%(levelname)s: %%(message)s'
2232
% server_settings["servicename"]))
2234
# Parse config file with clients
2235
client_config = configparser.SafeConfigParser(Client
2237
client_config.read(os.path.join(server_settings["configdir"],
2240
global mandos_dbus_service
2241
mandos_dbus_service = None
2243
tcp_server = MandosServer((server_settings["address"],
2244
server_settings["port"]),
2246
interface=(server_settings["interface"]
2250
server_settings["priority"],
2253
pidfilename = "/var/run/mandos.pid"
2255
pidfile = open(pidfilename, "w")
2257
logger.error("Could not open file %r", pidfilename)
2259
for name in ("_mandos", "mandos", "nobody"):
2261
uid = pwd.getpwnam(name).pw_uid
2262
gid = pwd.getpwnam(name).pw_gid
2272
except OSError as error:
2273
if error[0] != errno.EPERM:
2277
# Enable all possible GnuTLS debugging
2279
# "Use a log level over 10 to enable all debugging options."
2281
gnutls.library.functions.gnutls_global_set_log_level(11)
2283
@gnutls.library.types.gnutls_log_func
2284
def debug_gnutls(level, string):
2285
logger.debug("GnuTLS: %s", string[:-1])
2287
(gnutls.library.functions
2288
.gnutls_global_set_log_function(debug_gnutls))
2290
# Redirect stdin so all checkers get /dev/null
2291
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2292
os.dup2(null, sys.stdin.fileno())
2296
# Need to fork before connecting to D-Bus
2298
# Close all input and output, do double fork, etc.
2301
gobject.threads_init()
2304
# From the Avahi example code
2305
DBusGMainLoop(set_as_default=True)
575
# Parse the time arguments
577
options.timeout = string_to_delta(options.timeout)
579
parser.error("option --timeout: Unparseable time")
581
options.interval = string_to_delta(options.interval)
583
parser.error("option --interval: Unparseable time")
586
defaults = { "checker": "sleep 1; fping -q -- %%(fqdn)s" }
587
client_config = ConfigParser.SafeConfigParser(defaults)
588
#client_config.readfp(open("secrets.conf"), "secrets.conf")
589
client_config.read("mandos-clients.conf")
591
# From the Avahi server example code
592
DBusGMainLoop(set_as_default=True )
2306
593
main_loop = gobject.MainLoop()
2307
594
bus = dbus.SystemBus()
2308
# End of Avahi example code
2311
bus_name = dbus.service.BusName("se.recompile.Mandos",
2312
bus, do_not_queue=True)
2313
old_bus_name = (dbus.service.BusName
2314
("se.bsnet.fukt.Mandos", bus,
2316
except dbus.exceptions.NameExistsException as e:
2317
logger.error(unicode(e) + ", disabling D-Bus")
2319
server_settings["use_dbus"] = False
2320
tcp_server.use_dbus = False
2321
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2322
service = AvahiServiceToSyslog(name =
2323
server_settings["servicename"],
2324
servicetype = "_mandos._tcp",
2325
protocol = protocol, bus = bus)
2326
if server_settings["interface"]:
2327
service.interface = (if_nametoindex
2328
(str(server_settings["interface"])))
2330
global multiprocessing_manager
2331
multiprocessing_manager = multiprocessing.Manager()
2333
client_class = Client
2335
client_class = functools.partial(ClientDBusTransitional,
2338
client_settings = Client.config_parser(client_config)
2339
old_client_settings = {}
2342
# Get client data and settings from last running state.
2343
if server_settings["restore"]:
2345
with open(stored_state_path, "rb") as stored_state:
2346
clients_data, old_client_settings = (pickle.load
2348
os.remove(stored_state_path)
2349
except IOError as e:
2350
logger.warning("Could not load persistent state: {0}"
2352
if e.errno != errno.ENOENT:
2354
except EOFError as e:
2355
logger.warning("Could not load persistent state: "
2356
"EOFError: {0}".format(e))
2358
with PGPEngine() as pgp:
2359
for client_name, client in clients_data.iteritems():
2360
# Decide which value to use after restoring saved state.
2361
# We have three different values: Old config file,
2362
# new config file, and saved state.
2363
# New config value takes precedence if it differs from old
2364
# config value, otherwise use saved state.
2365
for name, value in client_settings[client_name].items():
2367
# For each value in new config, check if it
2368
# differs from the old config value (Except for
2369
# the "secret" attribute)
2370
if (name != "secret" and
2371
value != old_client_settings[client_name]
2373
client[name] = value
2377
# Clients who has passed its expire date can still be
2378
# enabled if its last checker was successful. Clients
2379
# whose checker succeeded before we stored its state is
2380
# assumed to have successfully run all checkers during
2382
if client["enabled"]:
2383
if datetime.datetime.utcnow() >= client["expires"]:
2384
if not client["last_checked_ok"]:
2386
"disabling client {0} - Client never "
2387
"performed a successful checker"
2388
.format(client_name))
2389
client["enabled"] = False
2390
elif client["last_checker_status"] != 0:
2392
"disabling client {0} - Client "
2393
"last checker failed with error code {1}"
2394
.format(client_name,
2395
client["last_checker_status"]))
2396
client["enabled"] = False
2398
client["expires"] = (datetime.datetime
2400
+ client["timeout"])
2401
logger.debug("Last checker succeeded,"
2402
" keeping {0} enabled"
2403
.format(client_name))
2405
client["secret"] = (
2406
pgp.decrypt(client["encrypted_secret"],
2407
client_settings[client_name]
2410
# If decryption fails, we use secret from new settings
2411
logger.debug("Failed to decrypt {0} old secret"
2412
.format(client_name))
2413
client["secret"] = (
2414
client_settings[client_name]["secret"])
2417
# Add/remove clients based on new changes made to config
2418
for client_name in (set(old_client_settings)
2419
- set(client_settings)):
2420
del clients_data[client_name]
2421
for client_name in (set(client_settings)
2422
- set(old_client_settings)):
2423
clients_data[client_name] = client_settings[client_name]
2425
# Create all client objects
2426
for client_name, client in clients_data.iteritems():
2427
tcp_server.clients[client_name] = client_class(
2428
name = client_name, settings = client)
2430
if not tcp_server.clients:
2431
logger.warning("No clients defined")
2437
pidfile.write(str(pid) + "\n".encode("utf-8"))
2440
logger.error("Could not write to file %r with PID %d",
2443
# "pidfile" was never created
2446
signal.signal(signal.SIGINT, signal.SIG_IGN)
2448
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2449
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2452
class MandosDBusService(DBusObjectWithProperties):
2453
"""A D-Bus proxy object"""
2455
dbus.service.Object.__init__(self, bus, "/")
2456
_interface = "se.recompile.Mandos"
2458
@dbus_interface_annotations(_interface)
2460
return { "org.freedesktop.DBus.Property"
2461
".EmitsChangedSignal":
2464
@dbus.service.signal(_interface, signature="o")
2465
def ClientAdded(self, objpath):
2469
@dbus.service.signal(_interface, signature="ss")
2470
def ClientNotFound(self, fingerprint, address):
2474
@dbus.service.signal(_interface, signature="os")
2475
def ClientRemoved(self, objpath, name):
2479
@dbus.service.method(_interface, out_signature="ao")
2480
def GetAllClients(self):
2482
return dbus.Array(c.dbus_object_path
2484
tcp_server.clients.itervalues())
2486
@dbus.service.method(_interface,
2487
out_signature="a{oa{sv}}")
2488
def GetAllClientsWithProperties(self):
2490
return dbus.Dictionary(
2491
((c.dbus_object_path, c.GetAll(""))
2492
for c in tcp_server.clients.itervalues()),
2495
@dbus.service.method(_interface, in_signature="o")
2496
def RemoveClient(self, object_path):
2498
for c in tcp_server.clients.itervalues():
2499
if c.dbus_object_path == object_path:
2500
del tcp_server.clients[c.name]
2501
c.remove_from_connection()
2502
# Don't signal anything except ClientRemoved
2503
c.disable(quiet=True)
2505
self.ClientRemoved(object_path, c.name)
2507
raise KeyError(object_path)
2511
class MandosDBusServiceTransitional(MandosDBusService):
2512
__metaclass__ = AlternateDBusNamesMetaclass
2513
mandos_dbus_service = MandosDBusServiceTransitional()
2516
"Cleanup function; run on exit"
2519
multiprocessing.active_children()
2520
if not (tcp_server.clients or client_settings):
2523
# Store client before exiting. Secrets are encrypted with key
2524
# based on what config file has. If config file is
2525
# removed/edited, old secret will thus be unrecovable.
2527
with PGPEngine() as pgp:
2528
for client in tcp_server.clients.itervalues():
2529
key = client_settings[client.name]["secret"]
2530
client.encrypted_secret = pgp.encrypt(client.secret,
2534
# A list of attributes that can not be pickled
2536
exclude = set(("bus", "changedstate", "secret",
2538
for name, typ in (inspect.getmembers
2539
(dbus.service.Object)):
2542
client_dict["encrypted_secret"] = (client
2544
for attr in client.client_structure:
2545
if attr not in exclude:
2546
client_dict[attr] = getattr(client, attr)
2548
clients[client.name] = client_dict
2549
del client_settings[client.name]["secret"]
2552
tempfd, tempname = tempfile.mkstemp(suffix=".pickle",
2555
(stored_state_path))
2556
with os.fdopen(tempfd, "wb") as stored_state:
2557
pickle.dump((clients, client_settings), stored_state)
2558
os.rename(tempname, stored_state_path)
2559
except (IOError, OSError) as e:
2560
logger.warning("Could not save persistent state: {0}"
2567
if e.errno not in set((errno.ENOENT, errno.EACCES,
2571
# Delete all clients, and settings from config
2572
while tcp_server.clients:
2573
name, client = tcp_server.clients.popitem()
2575
client.remove_from_connection()
2576
# Don't signal anything except ClientRemoved
2577
client.disable(quiet=True)
2580
mandos_dbus_service.ClientRemoved(client
2583
client_settings.clear()
2585
atexit.register(cleanup)
2587
for client in tcp_server.clients.itervalues():
2590
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2591
# Need to initiate checking of clients
2593
client.init_checker()
2596
tcp_server.server_activate()
2598
# Find out what port we got
2599
service.port = tcp_server.socket.getsockname()[1]
2601
logger.info("Now listening on address %r, port %d,"
2602
" flowinfo %d, scope_id %d"
2603
% tcp_server.socket.getsockname())
2605
logger.info("Now listening on address %r, port %d"
2606
% tcp_server.socket.getsockname())
2608
#service.interface = tcp_server.socket.getsockname()[3]
595
server = dbus.Interface(
596
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
597
avahi.DBUS_INTERFACE_SERVER )
598
# End of Avahi example code
600
debug = options.debug
603
def remove_from_clients(client):
604
clients.remove(client)
607
sys.stderr.write(u"No clients left, exiting\n")
610
clients.update(Set(Client(name=section, options=options,
611
stop_hook = remove_from_clients,
612
**(dict(client_config\
614
for section in client_config.sections()))
615
for client in clients:
618
tcp_server = IPv6_TCPServer((None, options.port),
622
# Find out what random port we got
623
servicePort = tcp_server.socket.getsockname()[1]
625
sys.stderr.write(u"Now listening on port %d\n" % servicePort)
627
if options.interface is not None:
628
serviceInterface = if_nametoindex(options.interface)
630
# From the Avahi server example code
631
server.connect_to_signal("StateChanged", server_state_changed)
632
server_state_changed(server.GetState())
633
# End of Avahi example code
635
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
636
lambda *args, **kwargs:
637
tcp_server.handle_request(*args[2:],
2611
# From the Avahi example code
2614
except dbus.exceptions.DBusException as error:
2615
logger.critical("D-Bus Exception", exc_info=error)
2618
# End of Avahi example code
2620
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2621
lambda *args, **kwargs:
2622
(tcp_server.handle_request
2623
(*args[2:], **kwargs) or True))
2625
logger.debug("Starting main loop")
2627
except AvahiError as error:
2628
logger.critical("Avahi Error", exc_info=error)
2631
641
except KeyboardInterrupt:
2633
print("", file=sys.stderr)
2634
logger.debug("Server received KeyboardInterrupt")
2635
logger.debug("Server exiting")
2636
# Must run before the D-Bus bus name gets deregistered
2639
if __name__ == '__main__':
646
# From the Avahi server example code
647
if not group is None:
649
# End of Avahi example code
651
for client in clients:
652
client.stop_hook = None