47
14
import gnutls.library.functions
48
15
import gnutls.library.constants
49
16
import gnutls.library.types
50
import ConfigParser as configparser
59
import logging.handlers
65
import cPickle as pickle
66
import multiprocessing
76
28
from dbus.mainloop.glib import DBusGMainLoop
79
import xml.dom.minidom
84
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
85
except AttributeError:
87
from IN import SO_BINDTODEVICE
89
SO_BINDTODEVICE = None
92
stored_state_file = "clients.pickle"
94
logger = logging.getLogger()
95
syslogger = (logging.handlers.SysLogHandler
96
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
97
address = str("/dev/log")))
100
if_nametoindex = (ctypes.cdll.LoadLibrary
101
(ctypes.util.find_library("c"))
103
except (OSError, AttributeError):
104
def if_nametoindex(interface):
105
"Get an interface index the hard way, i.e. using fcntl()"
106
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
107
with contextlib.closing(socket.socket()) as s:
108
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
109
struct.pack(str("16s16x"),
111
interface_index = struct.unpack(str("I"),
113
return interface_index
116
def initlogger(debug, level=logging.WARNING):
117
"""init logger and add loglevel"""
119
syslogger.setFormatter(logging.Formatter
120
('Mandos [%(process)d]: %(levelname)s:'
122
logger.addHandler(syslogger)
125
console = logging.StreamHandler()
126
console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
130
logger.addHandler(console)
131
logger.setLevel(level)
134
class PGPError(Exception):
135
"""Exception if encryption/decryption fails"""
139
class PGPEngine(object):
140
"""A simple class for OpenPGP symmetric encryption & decryption"""
142
self.gnupg = GnuPGInterface.GnuPG()
143
self.tempdir = tempfile.mkdtemp(prefix="mandos-")
144
self.gnupg = GnuPGInterface.GnuPG()
145
self.gnupg.options.meta_interactive = False
146
self.gnupg.options.homedir = self.tempdir
147
self.gnupg.options.extra_args.extend(['--force-mdc',
154
def __exit__ (self, exc_type, exc_value, traceback):
162
if self.tempdir is not None:
163
# Delete contents of tempdir
164
for root, dirs, files in os.walk(self.tempdir,
166
for filename in files:
167
os.remove(os.path.join(root, filename))
169
os.rmdir(os.path.join(root, dirname))
171
os.rmdir(self.tempdir)
174
def password_encode(self, password):
175
# Passphrase can not be empty and can not contain newlines or
176
# NUL bytes. So we prefix it and hex encode it.
177
return b"mandos" + binascii.hexlify(password)
179
def encrypt(self, data, password):
180
self.gnupg.passphrase = self.password_encode(password)
181
with open(os.devnull, "w") as devnull:
183
proc = self.gnupg.run(['--symmetric'],
184
create_fhs=['stdin', 'stdout'],
185
attach_fhs={'stderr': devnull})
186
with contextlib.closing(proc.handles['stdin']) as f:
188
with contextlib.closing(proc.handles['stdout']) as f:
189
ciphertext = f.read()
193
self.gnupg.passphrase = None
196
def decrypt(self, data, password):
197
self.gnupg.passphrase = self.password_encode(password)
198
with open(os.devnull, "w") as devnull:
200
proc = self.gnupg.run(['--decrypt'],
201
create_fhs=['stdin', 'stdout'],
202
attach_fhs={'stderr': devnull})
203
with contextlib.closing(proc.handles['stdin']) as f:
205
with contextlib.closing(proc.handles['stdout']) as f:
206
decrypted_plaintext = f.read()
210
self.gnupg.passphrase = None
211
return decrypted_plaintext
214
class AvahiError(Exception):
215
def __init__(self, value, *args, **kwargs):
217
super(AvahiError, self).__init__(value, *args, **kwargs)
218
def __unicode__(self):
219
return unicode(repr(self.value))
221
class AvahiServiceError(AvahiError):
224
class AvahiGroupError(AvahiError):
228
class AvahiService(object):
229
"""An Avahi (Zeroconf) service.
232
interface: integer; avahi.IF_UNSPEC or an interface index.
233
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()
249
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):
253
self.interface = interface
255
self.type = servicetype
257
self.TXT = TXT if TXT is not None else []
260
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
269
"""Derived from the Avahi example code"""
270
if self.rename_count >= self.max_renames:
271
logger.critical("No suitable Zeroconf service name found"
272
" after %i retries, exiting.",
274
raise AvahiServiceError("Too many renames")
275
self.name = unicode(self.server
276
.GetAlternativeServiceName(self.name))
277
logger.info("Changing Zeroconf service name to %r ...",
282
except dbus.exceptions.DBusException as error:
283
logger.critical("D-Bus Exception", exc_info=error)
286
self.rename_count += 1
289
"""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:
297
"""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))
32
import logging.handlers
34
# logghandler.setFormatter(logging.Formatter('%(levelname)s %(message)s')
36
logger = logging.Logger('mandos')
37
logger.addHandler(logging.handlers.SysLogHandler(facility = logging.handlers.SysLogHandler.LOG_DAEMON))
39
# This variable is used to optionally bind to a specified interface.
40
# It is a global variable to fit in with the other variables from the
41
# Avahi server example code.
42
serviceInterface = avahi.IF_UNSPEC
43
# From the Avahi server example code:
44
serviceName = "Mandos"
45
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
46
servicePort = None # Not known at startup
47
serviceTXT = [] # TXT record for the service
48
domain = "" # Domain to publish on, default to .local
49
host = "" # Host to publish records for, default to localhost
50
group = None #our entry group
51
rename_count = 12 # Counter so we only rename after collisions a
52
# sensible number of times
53
# End of Avahi example code
401
56
class Client(object):
402
57
"""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
59
name: string; from the config file, used in log messages
60
fingerprint: string (40 or 32 hexadecimal digits); used to
61
uniquely identify the client
62
secret: bytestring; sent verbatim (over TLS) to client
63
fqdn: string (FQDN); available for use by the checker command
64
created: datetime.datetime()
65
last_seen: datetime.datetime() or None if not yet seen
66
timeout: datetime.timedelta(); How long from last_seen until
67
this client is invalid
68
interval: datetime.timedelta(); How often to start a new checker
69
stop_hook: If set, called by stop() as stop_hook(self)
70
checker: subprocess.Popen(); a running checker process used
71
to see if the client lives.
72
Is None if no process is running.
73
checker_initiator_tag: a gobject event source tag, or None
74
stop_initiator_tag: - '' -
75
checker_callback_tag: - '' -
76
checker_command: string; External command which is run to check if
77
client lives. %()s expansions are done at
414
78
runtime with vars(self) as dict, so that for
415
79
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
81
_timeout: Real variable for 'timeout'
82
_interval: Real variable for 'interval'
83
_timeout_milliseconds: Used by gobject.timeout_add()
84
_interval_milliseconds: - '' -
444
runtime_expansions = ("approval_delay", "approval_duration",
445
"created", "enabled", "fingerprint",
446
"host", "interval", "last_checked_ok",
447
"last_enabled", "name", "timeout")
448
client_defaults = { "timeout": "5m",
449
"extended_timeout": "15m",
451
"checker": "fping -q -- %%(host)s",
453
"approval_delay": "0s",
454
"approval_duration": "1s",
455
"approved_by_default": "True",
459
def timeout_milliseconds(self):
460
"Return the 'timeout' attribute in milliseconds"
461
return timedelta_to_milliseconds(self.timeout)
463
def extended_timeout_milliseconds(self):
464
"Return the 'extended_timeout' attribute in milliseconds"
465
return timedelta_to_milliseconds(self.extended_timeout)
467
def interval_milliseconds(self):
468
"Return the 'interval' attribute in milliseconds"
469
return timedelta_to_milliseconds(self.interval)
471
def approval_delay_milliseconds(self):
472
return timedelta_to_milliseconds(self.approval_delay)
475
def config_parser(config):
476
"""Construct a new dict of client settings of this form:
477
{ client_name: {setting_name: value, ...}, ...}
478
with exceptions for any special settings as defined above.
479
NOTE: Must be a pure function. Must return the same result
480
value given the same arguments.
483
for client_name in config.sections():
484
section = dict(config.items(client_name))
485
client = settings[client_name] = {}
487
client["host"] = section["host"]
488
# Reformat values from string types to Python types
489
client["approved_by_default"] = config.getboolean(
490
client_name, "approved_by_default")
491
client["enabled"] = config.getboolean(client_name,
494
client["fingerprint"] = (section["fingerprint"].upper()
496
if "secret" in section:
497
client["secret"] = section["secret"].decode("base64")
498
elif "secfile" in section:
499
with open(os.path.expanduser(os.path.expandvars
500
(section["secfile"])),
502
client["secret"] = secfile.read()
504
raise TypeError("No secret or secfile for section {0}"
506
client["timeout"] = string_to_delta(section["timeout"])
507
client["extended_timeout"] = string_to_delta(
508
section["extended_timeout"])
509
client["interval"] = string_to_delta(section["interval"])
510
client["approval_delay"] = string_to_delta(
511
section["approval_delay"])
512
client["approval_duration"] = string_to_delta(
513
section["approval_duration"])
514
client["checker_command"] = section["checker"]
515
client["last_approval_request"] = None
516
client["last_checked_ok"] = None
517
client["last_checker_status"] = -2
521
def __init__(self, settings, name = None):
86
def _set_timeout(self, timeout):
87
"Setter function for 'timeout' attribute"
88
self._timeout = timeout
89
self._timeout_milliseconds = ((self.timeout.days
90
* 24 * 60 * 60 * 1000)
91
+ (self.timeout.seconds * 1000)
92
+ (self.timeout.microseconds
94
timeout = property(lambda self: self._timeout,
97
def _set_interval(self, interval):
98
"Setter function for 'interval' attribute"
99
self._interval = interval
100
self._interval_milliseconds = ((self.interval.days
101
* 24 * 60 * 60 * 1000)
102
+ (self.interval.seconds
104
+ (self.interval.microseconds
106
interval = property(lambda self: self._interval,
109
def __init__(self, name=None, options=None, stop_hook=None,
110
fingerprint=None, secret=None, secfile=None, fqdn=None,
111
timeout=None, interval=-1, checker=None):
523
# adding all client settings
524
for setting, value in settings.iteritems():
525
setattr(self, setting, value)
528
if not hasattr(self, "last_enabled"):
529
self.last_enabled = datetime.datetime.utcnow()
530
if not hasattr(self, "expires"):
531
self.expires = (datetime.datetime.utcnow()
534
self.last_enabled = None
537
logger.debug("Creating client %r", self.name)
538
# Uppercase and remove spaces from fingerprint for later
539
# comparison purposes with return value from the fingerprint()
541
logger.debug(" Fingerprint: %s", self.fingerprint)
542
self.created = settings.get("created",
543
datetime.datetime.utcnow())
545
# attributes specific for this server instance
113
# Uppercase and remove spaces from fingerprint
114
# for later comparison purposes with return value of
115
# the fingerprint() function
116
self.fingerprint = fingerprint.upper().replace(u" ", u"")
118
self.secret = secret.decode(u"base64")
121
self.secret = sf.read()
124
raise RuntimeError(u"No secret or secfile for client %s"
126
self.fqdn = fqdn # string
127
self.created = datetime.datetime.now()
128
self.last_seen = None
130
timeout = options.timeout
131
self.timeout = timeout
133
interval = options.interval
135
interval = string_to_delta(interval)
136
self.interval = interval
137
self.stop_hook = stop_hook
546
138
self.checker = None
547
139
self.checker_initiator_tag = None
548
self.disable_initiator_tag = None
140
self.stop_initiator_tag = None
549
141
self.checker_callback_tag = None
550
self.current_checker_command = None
552
self.approvals_pending = 0
553
self.changedstate = (multiprocessing_manager
554
.Condition(multiprocessing_manager
556
self.client_structure = [attr for attr in
557
self.__dict__.iterkeys()
558
if not attr.startswith("_")]
559
self.client_structure.append("client_structure")
561
for name, t in inspect.getmembers(type(self),
565
if not name.startswith("_"):
566
self.client_structure.append(name)
568
# Send notice to process children that client state has changed
569
def send_changedstate(self):
570
with self.changedstate:
571
self.changedstate.notify_all()
574
"""Start this client's checker and timeout hooks"""
575
if getattr(self, "enabled", False):
578
self.expires = datetime.datetime.utcnow() + self.timeout
580
self.last_enabled = datetime.datetime.utcnow()
582
self.send_changedstate()
584
def disable(self, quiet=True):
585
"""Disable this client."""
586
if not getattr(self, "enabled", False):
589
logger.info("Disabling client %s", self.name)
590
if getattr(self, "disable_initiator_tag", None) is not None:
591
gobject.source_remove(self.disable_initiator_tag)
592
self.disable_initiator_tag = None
594
if getattr(self, "checker_initiator_tag", None) is not None:
595
gobject.source_remove(self.checker_initiator_tag)
596
self.checker_initiator_tag = None
600
self.send_changedstate()
601
# Do not run this again if called by a gobject.timeout_add
607
def init_checker(self):
142
self.check_command = checker
144
"""Start this clients checker and timeout hooks"""
608
145
# Schedule a new checker to be started an 'interval' from now,
609
146
# and every interval from then on.
610
if self.checker_initiator_tag is not None:
611
gobject.source_remove(self.checker_initiator_tag)
612
self.checker_initiator_tag = (gobject.timeout_add
613
(self.interval_milliseconds(),
615
# Schedule a disable() when 'timeout' has passed
616
if self.disable_initiator_tag is not None:
617
gobject.source_remove(self.disable_initiator_tag)
618
self.disable_initiator_tag = (gobject.timeout_add
619
(self.timeout_milliseconds(),
147
self.checker_initiator_tag = gobject.timeout_add\
148
(self._interval_milliseconds,
621
150
# Also start a new checker *right now*.
622
151
self.start_checker()
624
def checker_callback(self, pid, condition, command):
152
# Schedule a stop() when 'timeout' has passed
153
self.stop_initiator_tag = gobject.timeout_add\
154
(self._timeout_milliseconds,
158
The possibility that this client might be restarted is left
159
open, but not currently used."""
160
logger.debug(u"Stopping client %s", self.name)
162
if self.stop_initiator_tag:
163
gobject.source_remove(self.stop_initiator_tag)
164
self.stop_initiator_tag = None
165
if self.checker_initiator_tag:
166
gobject.source_remove(self.checker_initiator_tag)
167
self.checker_initiator_tag = None
171
# Do not run this again if called by a gobject.timeout_add
174
# Some code duplication here and in stop()
175
if hasattr(self, "stop_initiator_tag") \
176
and self.stop_initiator_tag:
177
gobject.source_remove(self.stop_initiator_tag)
178
self.stop_initiator_tag = None
179
if hasattr(self, "checker_initiator_tag") \
180
and self.checker_initiator_tag:
181
gobject.source_remove(self.checker_initiator_tag)
182
self.checker_initiator_tag = None
184
def checker_callback(self, pid, condition):
625
185
"""The checker has completed, so take appropriate actions."""
626
self.checker_callback_tag = None
628
if os.WIFEXITED(condition):
629
self.last_checker_status = os.WEXITSTATUS(condition)
630
if self.last_checker_status == 0:
631
logger.info("Checker for %(name)s succeeded",
635
logger.info("Checker for %(name)s failed",
638
self.last_checker_status = -1
639
logger.warning("Checker for %(name)s crashed?",
186
now = datetime.datetime.now()
187
if os.WIFEXITED(condition) \
188
and (os.WEXITSTATUS(condition) == 0):
189
logger.debug(u"Checker for %(name)s succeeded",
192
gobject.source_remove(self.stop_initiator_tag)
193
self.stop_initiator_tag = gobject.timeout_add\
194
(self._timeout_milliseconds,
196
if not os.WIFEXITED(condition):
197
logger.warning(u"Checker for %(name)s crashed?",
642
def checked_ok(self):
643
"""Assert that the client has been seen, alive and well."""
644
self.last_checked_ok = datetime.datetime.utcnow()
645
self.last_checker_status = 0
648
def bump_timeout(self, timeout=None):
649
"""Bump up the timeout for this client."""
651
timeout = self.timeout
652
if self.disable_initiator_tag is not None:
653
gobject.source_remove(self.disable_initiator_tag)
654
self.disable_initiator_tag = None
655
if getattr(self, "enabled", False):
656
self.disable_initiator_tag = (gobject.timeout_add
657
(timedelta_to_milliseconds
658
(timeout), self.disable))
659
self.expires = datetime.datetime.utcnow() + timeout
661
def need_approval(self):
662
self.last_approval_request = datetime.datetime.utcnow()
200
logger.debug(u"Checker for %(name)s failed",
203
self.checker_callback_tag = None
664
204
def start_checker(self):
665
205
"""Start a new checker subprocess if one is not running.
667
206
If a checker already exists, leave it running and do
669
# The reason for not killing a running checker is that if we
670
# did that, and if a checker (for some reason) started running
671
# slowly and taking more than 'interval' time, then the client
672
# would inevitably timeout, since no checker would get a
673
# chance to run to completion. If we instead leave running
674
# checkers alone, the checker would have to take more time
675
# than 'timeout' for the client to be disabled, which is as it
678
# If a checker exists, make sure it is not a zombie
680
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
681
except (AttributeError, OSError) as error:
682
if (isinstance(error, OSError)
683
and error.errno != errno.ECHILD):
687
logger.warning("Checker was a zombie")
688
gobject.source_remove(self.checker_callback_tag)
689
self.checker_callback(pid, status,
690
self.current_checker_command)
691
# Start a new checker if needed
692
208
if self.checker is None:
209
logger.debug(u"Starting checker for %s",
694
# In case checker_command has exactly one % operator
695
command = self.checker_command % self.host
212
command = self.check_command % self.fqdn
696
213
except TypeError:
697
# Escape attributes for the shell
698
escaped_attrs = dict(
700
re.escape(unicode(str(getattr(self, attr, "")),
704
self.runtime_expansions)
214
escaped_attrs = dict((key, re.escape(str(val)))
216
vars(self).iteritems())
707
command = self.checker_command % escaped_attrs
708
except TypeError as error:
709
logger.error('Could not format string "%s"',
710
self.checker_command, exc_info=error)
218
command = self.check_command % escaped_attrs
219
except TypeError, error:
220
logger.critical(u'Could not format string "%s": %s',
221
self.check_command, error)
711
222
return True # Try again later
712
self.current_checker_command = command
714
logger.info("Starting checker %r for %s",
716
# We don't need to redirect stdout and stderr, since
717
# in normal mode, that is already done by daemon(),
718
# and in debug mode we don't want to. (Stdin is
719
# always replaced by /dev/null.)
720
self.checker = subprocess.Popen(command,
723
self.checker_callback_tag = (gobject.child_watch_add
725
self.checker_callback,
727
# The checker may have completed before the gobject
728
# watch was added. Check for this.
729
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
731
gobject.source_remove(self.checker_callback_tag)
732
self.checker_callback(pid, status, command)
733
except OSError as error:
734
logger.error("Failed to start subprocess",
224
self.checker = subprocess.\
226
stdout=subprocess.PIPE,
227
close_fds=True, shell=True,
229
self.checker_callback_tag = gobject.\
230
child_watch_add(self.checker.pid,
233
except subprocess.OSError, error:
234
logger.error(u"Failed to start subprocess: %s",
736
236
# Re-run this periodically if run by gobject.timeout_add
739
238
def stop_checker(self):
740
239
"""Force the checker process, if any, to stop."""
741
if self.checker_callback_tag:
742
gobject.source_remove(self.checker_callback_tag)
743
self.checker_callback_tag = None
744
if getattr(self, "checker", None) is None:
240
if not hasattr(self, "checker") or self.checker is None:
746
logger.debug("Stopping checker for %(name)s", vars(self))
748
self.checker.terminate()
750
#if self.checker.poll() is None:
751
# self.checker.kill()
752
except OSError as error:
753
if error.errno != errno.ESRCH: # No such process
758
def dbus_service_property(dbus_interface, signature="v",
759
access="readwrite", byte_arrays=False):
760
"""Decorators for marking methods of a DBusObjectWithProperties to
761
become properties on the D-Bus.
763
The decorated method will be called with no arguments by "Get"
764
and with one argument by "Set".
766
The parameters, where they are supported, are the same as
767
dbus.service.method, except there is only "signature", since the
768
type from Get() and the type sent to Set() is the same.
770
# Encoding deeply encoded byte arrays is not supported yet by the
771
# "Set" method, so we fail early here:
772
if byte_arrays and signature != "ay":
773
raise ValueError("Byte arrays not supported for non-'ay'"
774
" signature {0!r}".format(signature))
776
func._dbus_is_property = True
777
func._dbus_interface = dbus_interface
778
func._dbus_signature = signature
779
func._dbus_access = access
780
func._dbus_name = func.__name__
781
if func._dbus_name.endswith("_dbus_property"):
782
func._dbus_name = func._dbus_name[:-14]
783
func._dbus_get_args_options = {'byte_arrays': byte_arrays }
788
def dbus_interface_annotations(dbus_interface):
789
"""Decorator for marking functions returning interface annotations
793
@dbus_interface_annotations("org.example.Interface")
794
def _foo(self): # Function name does not matter
795
return {"org.freedesktop.DBus.Deprecated": "true",
796
"org.freedesktop.DBus.Property.EmitsChangedSignal":
800
func._dbus_is_interface = True
801
func._dbus_interface = dbus_interface
802
func._dbus_name = dbus_interface
807
def dbus_annotations(annotations):
808
"""Decorator to annotate D-Bus methods, signals or properties
811
@dbus_service_property("org.example.Interface", signature="b",
813
@dbus_annotations({{"org.freedesktop.DBus.Deprecated": "true",
814
"org.freedesktop.DBus.Property."
815
"EmitsChangedSignal": "false"})
816
def Property_dbus_property(self):
817
return dbus.Boolean(False)
820
func._dbus_annotations = annotations
825
class DBusPropertyException(dbus.exceptions.DBusException):
826
"""A base class for D-Bus property-related exceptions
828
def __unicode__(self):
829
return unicode(str(self))
832
class DBusPropertyAccessException(DBusPropertyException):
833
"""A property's access permissions disallows an operation.
838
class DBusPropertyNotFound(DBusPropertyException):
839
"""An attempt was made to access a non-existing property.
844
class DBusObjectWithProperties(dbus.service.Object):
845
"""A D-Bus object with properties.
847
Classes inheriting from this can use the dbus_service_property
848
decorator to expose methods as D-Bus properties. It exposes the
849
standard Get(), Set(), and GetAll() methods on the D-Bus.
853
def _is_dbus_thing(thing):
854
"""Returns a function testing if an attribute is a D-Bus thing
856
If called like _is_dbus_thing("method") it returns a function
857
suitable for use as predicate to inspect.getmembers().
859
return lambda obj: getattr(obj, "_dbus_is_{0}".format(thing),
862
def _get_all_dbus_things(self, thing):
863
"""Returns a generator of (name, attribute) pairs
865
return ((getattr(athing.__get__(self), "_dbus_name",
867
athing.__get__(self))
868
for cls in self.__class__.__mro__
870
inspect.getmembers(cls,
871
self._is_dbus_thing(thing)))
873
def _get_dbus_property(self, interface_name, property_name):
874
"""Returns a bound method if one exists which is a D-Bus
875
property with the specified name and interface.
877
for cls in self.__class__.__mro__:
878
for name, value in (inspect.getmembers
880
self._is_dbus_thing("property"))):
881
if (value._dbus_name == property_name
882
and value._dbus_interface == interface_name):
883
return value.__get__(self)
886
raise DBusPropertyNotFound(self.dbus_object_path + ":"
887
+ interface_name + "."
890
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ss",
892
def Get(self, interface_name, property_name):
893
"""Standard D-Bus property Get() method, see D-Bus standard.
895
prop = self._get_dbus_property(interface_name, property_name)
896
if prop._dbus_access == "write":
897
raise DBusPropertyAccessException(property_name)
899
if not hasattr(value, "variant_level"):
901
return type(value)(value, variant_level=value.variant_level+1)
903
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
904
def Set(self, interface_name, property_name, value):
905
"""Standard D-Bus property Set() method, see D-Bus standard.
907
prop = self._get_dbus_property(interface_name, property_name)
908
if prop._dbus_access == "read":
909
raise DBusPropertyAccessException(property_name)
910
if prop._dbus_get_args_options["byte_arrays"]:
911
# The byte_arrays option is not supported yet on
912
# signatures other than "ay".
913
if prop._dbus_signature != "ay":
915
value = dbus.ByteArray(b''.join(chr(byte)
919
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s",
920
out_signature="a{sv}")
921
def GetAll(self, interface_name):
922
"""Standard D-Bus property GetAll() method, see D-Bus
925
Note: Will not include properties with access="write".
928
for name, prop in self._get_all_dbus_things("property"):
930
and interface_name != prop._dbus_interface):
931
# Interface non-empty but did not match
933
# Ignore write-only properties
934
if prop._dbus_access == "write":
937
if not hasattr(value, "variant_level"):
938
properties[name] = value
940
properties[name] = type(value)(value, variant_level=
941
value.variant_level+1)
942
return dbus.Dictionary(properties, signature="sv")
944
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
946
path_keyword='object_path',
947
connection_keyword='connection')
948
def Introspect(self, object_path, connection):
949
"""Overloading of standard D-Bus method.
951
Inserts property tags and interface annotation tags.
953
xmlstring = dbus.service.Object.Introspect(self, object_path,
956
document = xml.dom.minidom.parseString(xmlstring)
957
def make_tag(document, name, prop):
958
e = document.createElement("property")
959
e.setAttribute("name", name)
960
e.setAttribute("type", prop._dbus_signature)
961
e.setAttribute("access", prop._dbus_access)
963
for if_tag in document.getElementsByTagName("interface"):
965
for tag in (make_tag(document, name, prop)
967
in self._get_all_dbus_things("property")
968
if prop._dbus_interface
969
== if_tag.getAttribute("name")):
970
if_tag.appendChild(tag)
971
# Add annotation tags
972
for typ in ("method", "signal", "property"):
973
for tag in if_tag.getElementsByTagName(typ):
975
for name, prop in (self.
976
_get_all_dbus_things(typ)):
977
if (name == tag.getAttribute("name")
978
and prop._dbus_interface
979
== if_tag.getAttribute("name")):
980
annots.update(getattr
984
for name, value in annots.iteritems():
985
ann_tag = document.createElement(
987
ann_tag.setAttribute("name", name)
988
ann_tag.setAttribute("value", value)
989
tag.appendChild(ann_tag)
990
# Add interface annotation tags
991
for annotation, value in dict(
992
itertools.chain.from_iterable(
993
annotations().iteritems()
994
for name, annotations in
995
self._get_all_dbus_things("interface")
996
if name == if_tag.getAttribute("name")
998
ann_tag = document.createElement("annotation")
999
ann_tag.setAttribute("name", annotation)
1000
ann_tag.setAttribute("value", value)
1001
if_tag.appendChild(ann_tag)
1002
# Add the names to the return values for the
1003
# "org.freedesktop.DBus.Properties" methods
1004
if (if_tag.getAttribute("name")
1005
== "org.freedesktop.DBus.Properties"):
1006
for cn in if_tag.getElementsByTagName("method"):
1007
if cn.getAttribute("name") == "Get":
1008
for arg in cn.getElementsByTagName("arg"):
1009
if (arg.getAttribute("direction")
1011
arg.setAttribute("name", "value")
1012
elif cn.getAttribute("name") == "GetAll":
1013
for arg in cn.getElementsByTagName("arg"):
1014
if (arg.getAttribute("direction")
1016
arg.setAttribute("name", "props")
1017
xmlstring = document.toxml("utf-8")
1019
except (AttributeError, xml.dom.DOMException,
1020
xml.parsers.expat.ExpatError) as error:
1021
logger.error("Failed to override Introspection method",
1026
def datetime_to_dbus (dt, variant_level=0):
1027
"""Convert a UTC datetime.datetime() to a D-Bus type."""
1029
return dbus.String("", variant_level = variant_level)
1030
return dbus.String(dt.isoformat(),
1031
variant_level=variant_level)
1034
def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
1035
"""A class decorator; applied to a subclass of
1036
dbus.service.Object, it will add alternate D-Bus attributes with
1037
interface names according to the "alt_interface_names" mapping.
1040
@alternate_dbus_names({"org.example.Interface":
1041
"net.example.AlternateInterface"})
1042
class SampleDBusObject(dbus.service.Object):
1043
@dbus.service.method("org.example.Interface")
1044
def SampleDBusMethod():
1047
The above "SampleDBusMethod" on "SampleDBusObject" will be
1048
reachable via two interfaces: "org.example.Interface" and
1049
"net.example.AlternateInterface", the latter of which will have
1050
its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
1051
"true", unless "deprecate" is passed with a False value.
1053
This works for methods and signals, and also for D-Bus properties
1054
(from DBusObjectWithProperties) and interfaces (from the
1055
dbus_interface_annotations decorator).
1058
for orig_interface_name, alt_interface_name in (
1059
alt_interface_names.iteritems()):
1061
interface_names = set()
1062
# Go though all attributes of the class
1063
for attrname, attribute in inspect.getmembers(cls):
1064
# Ignore non-D-Bus attributes, and D-Bus attributes
1065
# with the wrong interface name
1066
if (not hasattr(attribute, "_dbus_interface")
1067
or not attribute._dbus_interface
1068
.startswith(orig_interface_name)):
1070
# Create an alternate D-Bus interface name based on
1072
alt_interface = (attribute._dbus_interface
1073
.replace(orig_interface_name,
1074
alt_interface_name))
1075
interface_names.add(alt_interface)
1076
# Is this a D-Bus signal?
1077
if getattr(attribute, "_dbus_is_signal", False):
1078
# Extract the original non-method function by
1080
nonmethod_func = (dict(
1081
zip(attribute.func_code.co_freevars,
1082
attribute.__closure__))["func"]
1084
# Create a new, but exactly alike, function
1085
# object, and decorate it to be a new D-Bus signal
1086
# with the alternate D-Bus interface name
1087
new_function = (dbus.service.signal
1089
attribute._dbus_signature)
1090
(types.FunctionType(
1091
nonmethod_func.func_code,
1092
nonmethod_func.func_globals,
1093
nonmethod_func.func_name,
1094
nonmethod_func.func_defaults,
1095
nonmethod_func.func_closure)))
1096
# Copy annotations, if any
1098
new_function._dbus_annotations = (
1099
dict(attribute._dbus_annotations))
1100
except AttributeError:
1102
# Define a creator of a function to call both the
1103
# original and alternate functions, so both the
1104
# original and alternate signals gets sent when
1105
# the function is called
1106
def fixscope(func1, func2):
1107
"""This function is a scope container to pass
1108
func1 and func2 to the "call_both" function
1109
outside of its arguments"""
1110
def call_both(*args, **kwargs):
1111
"""This function will emit two D-Bus
1112
signals by calling func1 and func2"""
1113
func1(*args, **kwargs)
1114
func2(*args, **kwargs)
1116
# Create the "call_both" function and add it to
1118
attr[attrname] = fixscope(attribute, new_function)
1119
# Is this a D-Bus method?
1120
elif getattr(attribute, "_dbus_is_method", False):
1121
# Create a new, but exactly alike, function
1122
# object. Decorate it to be a new D-Bus method
1123
# with the alternate D-Bus interface name. Add it
1125
attr[attrname] = (dbus.service.method
1127
attribute._dbus_in_signature,
1128
attribute._dbus_out_signature)
1130
(attribute.func_code,
1131
attribute.func_globals,
1132
attribute.func_name,
1133
attribute.func_defaults,
1134
attribute.func_closure)))
1135
# Copy annotations, if any
1137
attr[attrname]._dbus_annotations = (
1138
dict(attribute._dbus_annotations))
1139
except AttributeError:
1141
# Is this a D-Bus property?
1142
elif getattr(attribute, "_dbus_is_property", False):
1143
# Create a new, but exactly alike, function
1144
# object, and decorate it to be a new D-Bus
1145
# property with the alternate D-Bus interface
1146
# name. Add it to the class.
1147
attr[attrname] = (dbus_service_property
1149
attribute._dbus_signature,
1150
attribute._dbus_access,
1152
._dbus_get_args_options
1155
(attribute.func_code,
1156
attribute.func_globals,
1157
attribute.func_name,
1158
attribute.func_defaults,
1159
attribute.func_closure)))
1160
# Copy annotations, if any
1162
attr[attrname]._dbus_annotations = (
1163
dict(attribute._dbus_annotations))
1164
except AttributeError:
1166
# Is this a D-Bus interface?
1167
elif getattr(attribute, "_dbus_is_interface", False):
1168
# Create a new, but exactly alike, function
1169
# object. Decorate it to be a new D-Bus interface
1170
# with the alternate D-Bus interface name. Add it
1172
attr[attrname] = (dbus_interface_annotations
1175
(attribute.func_code,
1176
attribute.func_globals,
1177
attribute.func_name,
1178
attribute.func_defaults,
1179
attribute.func_closure)))
1181
# Deprecate all alternate interfaces
1182
iname="_AlternateDBusNames_interface_annotation{0}"
1183
for interface_name in interface_names:
1184
@dbus_interface_annotations(interface_name)
1186
return { "org.freedesktop.DBus.Deprecated":
1188
# Find an unused name
1189
for aname in (iname.format(i)
1190
for i in itertools.count()):
1191
if aname not in attr:
1195
# Replace the class with a new subclass of it with
1196
# methods, signals, etc. as created above.
1197
cls = type(b"{0}Alternate".format(cls.__name__),
1203
@alternate_dbus_interfaces({"se.recompile.Mandos":
1204
"se.bsnet.fukt.Mandos"})
1205
class ClientDBus(Client, DBusObjectWithProperties):
1206
"""A Client class using D-Bus
1209
dbus_object_path: dbus.ObjectPath
1210
bus: dbus.SystemBus()
1213
runtime_expansions = (Client.runtime_expansions
1214
+ ("dbus_object_path",))
1216
# dbus.service.Object doesn't use super(), so we can't either.
1218
def __init__(self, bus = None, *args, **kwargs):
1220
Client.__init__(self, *args, **kwargs)
1221
# Only now, when this client is initialized, can it show up on
1223
client_object_name = unicode(self.name).translate(
1224
{ord("."): ord("_"),
1225
ord("-"): ord("_")})
1226
self.dbus_object_path = (dbus.ObjectPath
1227
("/clients/" + client_object_name))
1228
DBusObjectWithProperties.__init__(self, self.bus,
1229
self.dbus_object_path)
1231
def notifychangeproperty(transform_func,
1232
dbus_name, type_func=lambda x: x,
1234
""" Modify a variable so that it's a property which announces
1235
its changes to DBus.
1237
transform_fun: Function that takes a value and a variant_level
1238
and transforms it to a D-Bus type.
1239
dbus_name: D-Bus name of the variable
1240
type_func: Function that transform the value before sending it
1241
to the D-Bus. Default: no transform
1242
variant_level: D-Bus variant level. Default: 1
1244
attrname = "_{0}".format(dbus_name)
1245
def setter(self, value):
1246
if hasattr(self, "dbus_object_path"):
1247
if (not hasattr(self, attrname) or
1248
type_func(getattr(self, attrname, None))
1249
!= type_func(value)):
1250
dbus_value = transform_func(type_func(value),
1253
self.PropertyChanged(dbus.String(dbus_name),
1255
setattr(self, attrname, value)
1257
return property(lambda self: getattr(self, attrname), setter)
1259
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1260
approvals_pending = notifychangeproperty(dbus.Boolean,
1263
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1264
last_enabled = notifychangeproperty(datetime_to_dbus,
1266
checker = notifychangeproperty(dbus.Boolean, "CheckerRunning",
1267
type_func = lambda checker:
1268
checker is not None)
1269
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1271
last_checker_status = notifychangeproperty(dbus.Int16,
1272
"LastCheckerStatus")
1273
last_approval_request = notifychangeproperty(
1274
datetime_to_dbus, "LastApprovalRequest")
1275
approved_by_default = notifychangeproperty(dbus.Boolean,
1276
"ApprovedByDefault")
1277
approval_delay = notifychangeproperty(dbus.UInt64,
1280
timedelta_to_milliseconds)
1281
approval_duration = notifychangeproperty(
1282
dbus.UInt64, "ApprovalDuration",
1283
type_func = timedelta_to_milliseconds)
1284
host = notifychangeproperty(dbus.String, "Host")
1285
timeout = notifychangeproperty(dbus.UInt64, "Timeout",
1287
timedelta_to_milliseconds)
1288
extended_timeout = notifychangeproperty(
1289
dbus.UInt64, "ExtendedTimeout",
1290
type_func = timedelta_to_milliseconds)
1291
interval = notifychangeproperty(dbus.UInt64,
1294
timedelta_to_milliseconds)
1295
checker_command = notifychangeproperty(dbus.String, "Checker")
1297
del notifychangeproperty
1299
def __del__(self, *args, **kwargs):
1301
self.remove_from_connection()
1304
if hasattr(DBusObjectWithProperties, "__del__"):
1305
DBusObjectWithProperties.__del__(self, *args, **kwargs)
1306
Client.__del__(self, *args, **kwargs)
1308
def checker_callback(self, pid, condition, command,
242
gobject.source_remove(self.checker_callback_tag)
1310
243
self.checker_callback_tag = None
244
os.kill(self.checker.pid, signal.SIGTERM)
245
if self.checker.poll() is None:
246
os.kill(self.checker.pid, signal.SIGKILL)
1311
247
self.checker = None
1312
if os.WIFEXITED(condition):
1313
exitstatus = os.WEXITSTATUS(condition)
1315
self.CheckerCompleted(dbus.Int16(exitstatus),
1316
dbus.Int64(condition),
1317
dbus.String(command))
1320
self.CheckerCompleted(dbus.Int16(-1),
1321
dbus.Int64(condition),
1322
dbus.String(command))
1324
return Client.checker_callback(self, pid, condition, command,
1327
def start_checker(self, *args, **kwargs):
1328
old_checker = self.checker
1329
if self.checker is not None:
1330
old_checker_pid = self.checker.pid
1332
old_checker_pid = None
1333
r = Client.start_checker(self, *args, **kwargs)
1334
# Only if new checker process was started
1335
if (self.checker is not None
1336
and old_checker_pid != self.checker.pid):
1338
self.CheckerStarted(self.current_checker_command)
1341
def _reset_approved(self):
1342
self.approved = None
1345
def approve(self, value=True):
1346
self.approved = value
1347
gobject.timeout_add(timedelta_to_milliseconds
1348
(self.approval_duration),
1349
self._reset_approved)
1350
self.send_changedstate()
1352
## D-Bus methods, signals & properties
1353
_interface = "se.recompile.Mandos.Client"
1357
@dbus_interface_annotations(_interface)
1359
return { "org.freedesktop.DBus.Property.EmitsChangedSignal":
1364
# CheckerCompleted - signal
1365
@dbus.service.signal(_interface, signature="nxs")
1366
def CheckerCompleted(self, exitcode, waitstatus, command):
1370
# CheckerStarted - signal
1371
@dbus.service.signal(_interface, signature="s")
1372
def CheckerStarted(self, command):
1376
# PropertyChanged - signal
1377
@dbus.service.signal(_interface, signature="sv")
1378
def PropertyChanged(self, property, value):
1382
# GotSecret - signal
1383
@dbus.service.signal(_interface)
1384
def GotSecret(self):
1386
Is sent after a successful transfer of secret from the Mandos
1387
server to mandos-client
1392
@dbus.service.signal(_interface, signature="s")
1393
def Rejected(self, reason):
1397
# NeedApproval - signal
1398
@dbus.service.signal(_interface, signature="tb")
1399
def NeedApproval(self, timeout, default):
1401
return self.need_approval()
1406
@dbus.service.method(_interface, in_signature="b")
1407
def Approve(self, value):
1410
# CheckedOK - method
1411
@dbus.service.method(_interface)
1412
def CheckedOK(self):
1416
@dbus.service.method(_interface)
1421
# StartChecker - method
1422
@dbus.service.method(_interface)
1423
def StartChecker(self):
1425
self.start_checker()
1428
@dbus.service.method(_interface)
1433
# StopChecker - method
1434
@dbus.service.method(_interface)
1435
def StopChecker(self):
1440
# ApprovalPending - property
1441
@dbus_service_property(_interface, signature="b", access="read")
1442
def ApprovalPending_dbus_property(self):
1443
return dbus.Boolean(bool(self.approvals_pending))
1445
# ApprovedByDefault - property
1446
@dbus_service_property(_interface, signature="b",
1448
def ApprovedByDefault_dbus_property(self, value=None):
1449
if value is None: # get
1450
return dbus.Boolean(self.approved_by_default)
1451
self.approved_by_default = bool(value)
1453
# ApprovalDelay - property
1454
@dbus_service_property(_interface, signature="t",
1456
def ApprovalDelay_dbus_property(self, value=None):
1457
if value is None: # get
1458
return dbus.UInt64(self.approval_delay_milliseconds())
1459
self.approval_delay = datetime.timedelta(0, 0, 0, value)
1461
# ApprovalDuration - property
1462
@dbus_service_property(_interface, signature="t",
1464
def ApprovalDuration_dbus_property(self, value=None):
1465
if value is None: # get
1466
return dbus.UInt64(timedelta_to_milliseconds(
1467
self.approval_duration))
1468
self.approval_duration = datetime.timedelta(0, 0, 0, value)
1471
@dbus_service_property(_interface, signature="s", access="read")
1472
def Name_dbus_property(self):
1473
return dbus.String(self.name)
1475
# Fingerprint - property
1476
@dbus_service_property(_interface, signature="s", access="read")
1477
def Fingerprint_dbus_property(self):
1478
return dbus.String(self.fingerprint)
1481
@dbus_service_property(_interface, signature="s",
1483
def Host_dbus_property(self, value=None):
1484
if value is None: # get
1485
return dbus.String(self.host)
1486
self.host = unicode(value)
1488
# Created - property
1489
@dbus_service_property(_interface, signature="s", access="read")
1490
def Created_dbus_property(self):
1491
return datetime_to_dbus(self.created)
1493
# LastEnabled - property
1494
@dbus_service_property(_interface, signature="s", access="read")
1495
def LastEnabled_dbus_property(self):
1496
return datetime_to_dbus(self.last_enabled)
1498
# Enabled - property
1499
@dbus_service_property(_interface, signature="b",
1501
def Enabled_dbus_property(self, value=None):
1502
if value is None: # get
1503
return dbus.Boolean(self.enabled)
1509
# LastCheckedOK - property
1510
@dbus_service_property(_interface, signature="s",
1512
def LastCheckedOK_dbus_property(self, value=None):
1513
if value is not None:
1516
return datetime_to_dbus(self.last_checked_ok)
1518
# LastCheckerStatus - property
1519
@dbus_service_property(_interface, signature="n",
1521
def LastCheckerStatus_dbus_property(self):
1522
return dbus.Int16(self.last_checker_status)
1524
# Expires - property
1525
@dbus_service_property(_interface, signature="s", access="read")
1526
def Expires_dbus_property(self):
1527
return datetime_to_dbus(self.expires)
1529
# LastApprovalRequest - property
1530
@dbus_service_property(_interface, signature="s", access="read")
1531
def LastApprovalRequest_dbus_property(self):
1532
return datetime_to_dbus(self.last_approval_request)
1534
# Timeout - property
1535
@dbus_service_property(_interface, signature="t",
1537
def Timeout_dbus_property(self, value=None):
1538
if value is None: # get
1539
return dbus.UInt64(self.timeout_milliseconds())
1540
old_timeout = self.timeout
1541
self.timeout = datetime.timedelta(0, 0, 0, value)
1542
# Reschedule disabling
1544
now = datetime.datetime.utcnow()
1545
self.expires += self.timeout - old_timeout
1546
if self.expires <= now:
1547
# The timeout has passed
1550
if (getattr(self, "disable_initiator_tag", None)
1553
gobject.source_remove(self.disable_initiator_tag)
1554
self.disable_initiator_tag = (
1555
gobject.timeout_add(
1556
timedelta_to_milliseconds(self.expires - now),
1559
# ExtendedTimeout - property
1560
@dbus_service_property(_interface, signature="t",
1562
def ExtendedTimeout_dbus_property(self, value=None):
1563
if value is None: # get
1564
return dbus.UInt64(self.extended_timeout_milliseconds())
1565
self.extended_timeout = datetime.timedelta(0, 0, 0, value)
1567
# Interval - property
1568
@dbus_service_property(_interface, signature="t",
1570
def Interval_dbus_property(self, value=None):
1571
if value is None: # get
1572
return dbus.UInt64(self.interval_milliseconds())
1573
self.interval = datetime.timedelta(0, 0, 0, value)
1574
if getattr(self, "checker_initiator_tag", None) is None:
1577
# Reschedule checker run
1578
gobject.source_remove(self.checker_initiator_tag)
1579
self.checker_initiator_tag = (gobject.timeout_add
1580
(value, self.start_checker))
1581
self.start_checker() # Start one now, too
1583
# Checker - property
1584
@dbus_service_property(_interface, signature="s",
1586
def Checker_dbus_property(self, value=None):
1587
if value is None: # get
1588
return dbus.String(self.checker_command)
1589
self.checker_command = unicode(value)
1591
# CheckerRunning - property
1592
@dbus_service_property(_interface, signature="b",
1594
def CheckerRunning_dbus_property(self, value=None):
1595
if value is None: # get
1596
return dbus.Boolean(self.checker is not None)
1598
self.start_checker()
1602
# ObjectPath - property
1603
@dbus_service_property(_interface, signature="o", access="read")
1604
def ObjectPath_dbus_property(self):
1605
return self.dbus_object_path # is already a dbus.ObjectPath
1608
@dbus_service_property(_interface, signature="ay",
1609
access="write", byte_arrays=True)
1610
def Secret_dbus_property(self, value):
1611
self.secret = str(value)
1616
class ProxyClient(object):
1617
def __init__(self, child_pipe, fpr, address):
1618
self._pipe = child_pipe
1619
self._pipe.send(('init', fpr, address))
1620
if not self._pipe.recv():
1623
def __getattribute__(self, name):
1625
return super(ProxyClient, self).__getattribute__(name)
1626
self._pipe.send(('getattr', name))
1627
data = self._pipe.recv()
1628
if data[0] == 'data':
1630
if data[0] == 'function':
1631
def func(*args, **kwargs):
1632
self._pipe.send(('funcall', name, args, kwargs))
1633
return self._pipe.recv()[1]
1636
def __setattr__(self, name, value):
1638
return super(ProxyClient, self).__setattr__(name, value)
1639
self._pipe.send(('setattr', name, value))
1642
class ClientHandler(socketserver.BaseRequestHandler, object):
1643
"""A class to handle client connections.
1645
Instantiated once for each connection to handle it.
248
def still_valid(self, now=None):
249
"""Has the timeout not yet passed for this client?"""
251
now = datetime.datetime.now()
252
if self.last_seen is None:
253
return now < (self.created + self.timeout)
255
return now < (self.last_seen + self.timeout)
258
def peer_certificate(session):
259
# If not an OpenPGP certificate...
260
if gnutls.library.functions.gnutls_certificate_type_get\
261
(session._c_object) \
262
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
263
# ...do the normal thing
264
return session.peer_certificate
265
list_size = ctypes.c_uint()
266
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
267
(session._c_object, ctypes.byref(list_size))
268
if list_size.value == 0:
271
return ctypes.string_at(cert.data, cert.size)
274
def fingerprint(openpgp):
275
# New empty GnuTLS certificate
276
crt = gnutls.library.types.gnutls_openpgp_crt_t()
277
gnutls.library.functions.gnutls_openpgp_crt_init\
279
# New GnuTLS "datum" with the OpenPGP public key
280
datum = gnutls.library.types.gnutls_datum_t\
281
(ctypes.cast(ctypes.c_char_p(openpgp),
282
ctypes.POINTER(ctypes.c_ubyte)),
283
ctypes.c_uint(len(openpgp)))
284
# Import the OpenPGP public key into the certificate
285
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
288
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
289
# New buffer for the fingerprint
290
buffer = ctypes.create_string_buffer(20)
291
buffer_length = ctypes.c_size_t()
292
# Get the fingerprint from the certificate into the buffer
293
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
294
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
295
# Deinit the certificate
296
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
297
# Convert the buffer to a Python bytestring
298
fpr = ctypes.string_at(buffer, buffer_length.value)
299
# Convert the bytestring to hexadecimal notation
300
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
304
class tcp_handler(SocketServer.BaseRequestHandler, object):
305
"""A TCP request handler class.
306
Instantiated by IPv6_TCPServer for each request to handle it.
1646
307
Note: This will run in its own forked process."""
1648
309
def handle(self):
1649
with contextlib.closing(self.server.child_pipe) as child_pipe:
1650
logger.info("TCP connection from: %s",
1651
unicode(self.client_address))
1652
logger.debug("Pipe FD: %d",
1653
self.server.child_pipe.fileno())
1655
session = (gnutls.connection
1656
.ClientSession(self.request,
1658
.X509Credentials()))
1660
# Note: gnutls.connection.X509Credentials is really a
1661
# generic GnuTLS certificate credentials object so long as
1662
# no X.509 keys are added to it. Therefore, we can use it
1663
# here despite using OpenPGP certificates.
1665
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
1666
# "+AES-256-CBC", "+SHA1",
1667
# "+COMP-NULL", "+CTYPE-OPENPGP",
1669
# Use a fallback default, since this MUST be set.
1670
priority = self.server.gnutls_priority
1671
if priority is None:
1673
(gnutls.library.functions
1674
.gnutls_priority_set_direct(session._c_object,
1677
# Start communication using the Mandos protocol
1678
# Get protocol number
1679
line = self.request.makefile().readline()
1680
logger.debug("Protocol version: %r", line)
1682
if int(line.strip().split()[0]) > 1:
1684
except (ValueError, IndexError, RuntimeError) as error:
1685
logger.error("Unknown protocol version: %s", error)
1688
# Start GnuTLS connection
1691
except gnutls.errors.GNUTLSError as error:
1692
logger.warning("Handshake failed: %s", error)
1693
# Do not run session.bye() here: the session is not
1694
# established. Just abandon the request.
1696
logger.debug("Handshake succeeded")
1698
approval_required = False
1701
fpr = self.fingerprint(self.peer_certificate
1704
gnutls.errors.GNUTLSError) as error:
1705
logger.warning("Bad certificate: %s", error)
1707
logger.debug("Fingerprint: %s", fpr)
1710
client = ProxyClient(child_pipe, fpr,
1711
self.client_address)
1715
if client.approval_delay:
1716
delay = client.approval_delay
1717
client.approvals_pending += 1
1718
approval_required = True
1721
if not client.enabled:
1722
logger.info("Client %s is disabled",
1724
if self.server.use_dbus:
1726
client.Rejected("Disabled")
1729
if client.approved or not client.approval_delay:
1730
#We are approved or approval is disabled
1732
elif client.approved is None:
1733
logger.info("Client %s needs approval",
1735
if self.server.use_dbus:
1737
client.NeedApproval(
1738
client.approval_delay_milliseconds(),
1739
client.approved_by_default)
1741
logger.warning("Client %s was not approved",
1743
if self.server.use_dbus:
1745
client.Rejected("Denied")
1748
#wait until timeout or approved
1749
time = datetime.datetime.now()
1750
client.changedstate.acquire()
1751
client.changedstate.wait(
1752
float(timedelta_to_milliseconds(delay)
1754
client.changedstate.release()
1755
time2 = datetime.datetime.now()
1756
if (time2 - time) >= delay:
1757
if not client.approved_by_default:
1758
logger.warning("Client %s timed out while"
1759
" waiting for approval",
1761
if self.server.use_dbus:
1763
client.Rejected("Approval timed out")
1768
delay -= time2 - time
1771
while sent_size < len(client.secret):
1773
sent = session.send(client.secret[sent_size:])
1774
except gnutls.errors.GNUTLSError as error:
1775
logger.warning("gnutls send failed",
1778
logger.debug("Sent: %d, remaining: %d",
1779
sent, len(client.secret)
1780
- (sent_size + sent))
1783
logger.info("Sending secret to %s", client.name)
1784
# bump the timeout using extended_timeout
1785
client.bump_timeout(client.extended_timeout)
1786
if self.server.use_dbus:
1791
if approval_required:
1792
client.approvals_pending -= 1
1795
except gnutls.errors.GNUTLSError as error:
1796
logger.warning("GnuTLS bye failed",
1800
def peer_certificate(session):
1801
"Return the peer's OpenPGP certificate as a bytestring"
1802
# If not an OpenPGP certificate...
1803
if (gnutls.library.functions
1804
.gnutls_certificate_type_get(session._c_object)
1805
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1806
# ...do the normal thing
1807
return session.peer_certificate
1808
list_size = ctypes.c_uint(1)
1809
cert_list = (gnutls.library.functions
1810
.gnutls_certificate_get_peers
1811
(session._c_object, ctypes.byref(list_size)))
1812
if not bool(cert_list) and list_size.value != 0:
1813
raise gnutls.errors.GNUTLSError("error getting peer"
1815
if list_size.value == 0:
1818
return ctypes.string_at(cert.data, cert.size)
1821
def fingerprint(openpgp):
1822
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1823
# New GnuTLS "datum" with the OpenPGP public key
1824
datum = (gnutls.library.types
1825
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1828
ctypes.c_uint(len(openpgp))))
1829
# New empty GnuTLS certificate
1830
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1831
(gnutls.library.functions
1832
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1833
# Import the OpenPGP public key into the certificate
1834
(gnutls.library.functions
1835
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1836
gnutls.library.constants
1837
.GNUTLS_OPENPGP_FMT_RAW))
1838
# Verify the self signature in the key
1839
crtverify = ctypes.c_uint()
1840
(gnutls.library.functions
1841
.gnutls_openpgp_crt_verify_self(crt, 0,
1842
ctypes.byref(crtverify)))
1843
if crtverify.value != 0:
1844
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1845
raise (gnutls.errors.CertificateSecurityError
1847
# New buffer for the fingerprint
1848
buf = ctypes.create_string_buffer(20)
1849
buf_len = ctypes.c_size_t()
1850
# Get the fingerprint from the certificate into the buffer
1851
(gnutls.library.functions
1852
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1853
ctypes.byref(buf_len)))
1854
# Deinit the certificate
1855
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1856
# Convert the buffer to a Python bytestring
1857
fpr = ctypes.string_at(buf, buf_len.value)
1858
# Convert the bytestring to hexadecimal notation
1859
hex_fpr = binascii.hexlify(fpr).upper()
1863
class MultiprocessingMixIn(object):
1864
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
1865
def sub_process_main(self, request, address):
1867
self.finish_request(request, address)
1869
self.handle_error(request, address)
1870
self.close_request(request)
1872
def process_request(self, request, address):
1873
"""Start a new process to process the request."""
1874
proc = multiprocessing.Process(target = self.sub_process_main,
1875
args = (request, address))
1880
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
1881
""" adds a pipe to the MixIn """
1882
def process_request(self, request, client_address):
1883
"""Overrides and wraps the original process_request().
1885
This function creates a new pipe in self.pipe
1887
parent_pipe, self.child_pipe = multiprocessing.Pipe()
1889
proc = MultiprocessingMixIn.process_request(self, request,
1891
self.child_pipe.close()
1892
self.add_pipe(parent_pipe, proc)
1894
def add_pipe(self, parent_pipe, proc):
1895
"""Dummy function; override as necessary"""
1896
raise NotImplementedError
1899
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1900
socketserver.TCPServer, object):
1901
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
310
logger.debug(u"TCP connection from: %s",
311
unicode(self.client_address))
312
session = gnutls.connection.ClientSession(self.request,
316
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
317
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
319
priority = "SECURE256"
321
gnutls.library.functions.gnutls_priority_set_direct\
322
(session._c_object, priority, None);
326
except gnutls.errors.GNUTLSError, error:
327
logger.debug(u"Handshake failed: %s", error)
328
# Do not run session.bye() here: the session is not
329
# established. Just abandon the request.
332
fpr = fingerprint(peer_certificate(session))
333
except (TypeError, gnutls.errors.GNUTLSError), error:
334
logger.debug(u"Bad certificate: %s", error)
337
logger.debug(u"Fingerprint: %s", fpr)
340
if c.fingerprint == fpr:
343
# Have to check if client.still_valid(), since it is possible
344
# that the client timed out while establishing the GnuTLS
346
if (not client) or (not client.still_valid()):
348
logger.debug(u"Client %(name)s is invalid",
351
logger.debug(u"Client not found for fingerprint: %s",
356
while sent_size < len(client.secret):
357
sent = session.send(client.secret[sent_size:])
358
logger.debug(u"Sent: %d, remaining: %d",
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.
1904
enabled: Boolean; whether this server is activated yet
1905
interface: None or a network interface name (string)
1906
use_ipv6: Boolean; to use IPv6 or not
368
options: Command line options
369
clients: Set() of Client objects
1908
def __init__(self, server_address, RequestHandlerClass,
1909
interface=None, use_ipv6=True):
1910
self.interface = interface
1912
self.address_family = socket.AF_INET6
1913
socketserver.TCPServer.__init__(self, server_address,
1914
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)
1915
380
def server_bind(self):
1916
381
"""This overrides the normal server_bind() function
1917
382
to bind to an interface if one was specified, and also NOT to
1918
383
bind to an address or port if they were not specified."""
1919
if self.interface is not None:
1920
if SO_BINDTODEVICE is None:
1921
logger.error("SO_BINDTODEVICE does not exist;"
1922
" cannot bind to interface %s",
1926
self.socket.setsockopt(socket.SOL_SOCKET,
1930
except socket.error as error:
1931
if error.errno == errno.EPERM:
1932
logger.error("No permission to"
1933
" bind to interface %s",
1935
elif error.errno == errno.ENOPROTOOPT:
1936
logger.error("SO_BINDTODEVICE not available;"
1937
" cannot bind to interface %s",
1939
elif error.errno == errno.ENODEV:
1940
logger.error("Interface %s does not"
1941
" exist, cannot bind",
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
logger.warning(u"No permission to"
395
u" bind to interface %s",
396
self.options.interface)
1945
399
# Only bind(2) the socket if we really need to.
1946
400
if self.server_address[0] or self.server_address[1]:
1947
401
if not self.server_address[0]:
1948
if self.address_family == socket.AF_INET6:
1949
any_address = "::" # in6addr_any
1951
any_address = socket.INADDR_ANY
1952
self.server_address = (any_address,
403
self.server_address = (in6addr_any,
1953
404
self.server_address[1])
1954
elif not self.server_address[1]:
405
elif self.server_address[1] is None:
1955
406
self.server_address = (self.server_address[0],
1957
# if self.interface:
1958
# self.server_address = (self.server_address[0],
1963
return socketserver.TCPServer.server_bind(self)
1966
class MandosServer(IPv6_TCPServer):
1970
clients: set of Client objects
1971
gnutls_priority GnuTLS priority string
1972
use_dbus: Boolean; to emit D-Bus signals or not
1974
Assumes a gobject.MainLoop event loop.
1976
def __init__(self, server_address, RequestHandlerClass,
1977
interface=None, use_ipv6=True, clients=None,
1978
gnutls_priority=None, use_dbus=True):
1979
self.enabled = False
1980
self.clients = clients
1981
if self.clients is None:
1983
self.use_dbus = use_dbus
1984
self.gnutls_priority = gnutls_priority
1985
IPv6_TCPServer.__init__(self, server_address,
1986
RequestHandlerClass,
1987
interface = interface,
1988
use_ipv6 = use_ipv6)
1989
def server_activate(self):
1991
return socketserver.TCPServer.server_activate(self)
1996
def add_pipe(self, parent_pipe, proc):
1997
# Call "handle_ipc" for both data and EOF events
1998
gobject.io_add_watch(parent_pipe.fileno(),
1999
gobject.IO_IN | gobject.IO_HUP,
2000
functools.partial(self.handle_ipc,
2005
def handle_ipc(self, source, condition, parent_pipe=None,
2006
proc = None, client_object=None):
2007
# error, or the other end of multiprocessing.Pipe has closed
2008
if condition & (gobject.IO_ERR | gobject.IO_HUP):
2009
# Wait for other process to exit
2013
# Read a request from the child
2014
request = parent_pipe.recv()
2015
command = request[0]
2017
if command == 'init':
2019
address = request[2]
2021
for c in self.clients.itervalues():
2022
if c.fingerprint == fpr:
2026
logger.info("Client not found for fingerprint: %s, ad"
2027
"dress: %s", fpr, address)
2030
mandos_dbus_service.ClientNotFound(fpr,
2032
parent_pipe.send(False)
2035
gobject.io_add_watch(parent_pipe.fileno(),
2036
gobject.IO_IN | gobject.IO_HUP,
2037
functools.partial(self.handle_ipc,
2043
parent_pipe.send(True)
2044
# remove the old hook in favor of the new above hook on
2047
if command == 'funcall':
2048
funcname = request[1]
2052
parent_pipe.send(('data', getattr(client_object,
2056
if command == 'getattr':
2057
attrname = request[1]
2058
if callable(client_object.__getattribute__(attrname)):
2059
parent_pipe.send(('function',))
2061
parent_pipe.send(('data', client_object
2062
.__getattribute__(attrname)))
2064
if command == 'setattr':
2065
attrname = request[1]
2067
setattr(client_object, attrname, value)
408
return super(type(self), self).server_bind()
2072
411
def string_to_delta(interval):
2073
412
"""Parse a string and return a datetime.timedelta
2075
414
>>> string_to_delta('7d')
2076
415
datetime.timedelta(7)
2077
416
>>> string_to_delta('60s')
2080
419
datetime.timedelta(0, 3600)
2081
420
>>> string_to_delta('24h')
2082
421
datetime.timedelta(1)
2083
>>> string_to_delta('1w')
422
>>> string_to_delta(u'1w')
2084
423
datetime.timedelta(7)
2085
>>> string_to_delta('5m 30s')
2086
datetime.timedelta(0, 330)
2088
timevalue = datetime.timedelta(0)
2089
for s in interval.split():
2091
suffix = unicode(s[-1])
2094
delta = datetime.timedelta(value)
2096
delta = datetime.timedelta(0, value)
2098
delta = datetime.timedelta(0, 0, 0, 0, value)
2100
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
2102
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
2104
raise ValueError("Unknown suffix {0!r}"
2106
except (ValueError, IndexError) as e:
2107
raise ValueError(*(e.args))
2112
def daemon(nochdir = False, noclose = False):
2113
"""See daemon(3). Standard BSD Unix function.
2115
This should really exist as os.daemon, but it doesn't (yet)."""
2124
# Close all standard open file descriptors
2125
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2126
if not stat.S_ISCHR(os.fstat(null).st_mode):
2127
raise OSError(errno.ENODEV,
2128
"{0} not a character device"
2129
.format(os.devnull))
2130
os.dup2(null, sys.stdin.fileno())
2131
os.dup2(null, sys.stdout.fileno())
2132
os.dup2(null, sys.stderr.fileno())
2139
##################################################################
2140
# Parsing of options, both command line and config file
2142
parser = argparse.ArgumentParser()
2143
parser.add_argument("-v", "--version", action="version",
2144
version = "%(prog)s {0}".format(version),
2145
help="show version number and exit")
2146
parser.add_argument("-i", "--interface", metavar="IF",
2147
help="Bind to interface IF")
2148
parser.add_argument("-a", "--address",
2149
help="Address to listen for requests on")
2150
parser.add_argument("-p", "--port", type=int,
2151
help="Port number to receive requests on")
2152
parser.add_argument("--check", action="store_true",
2153
help="Run self-test")
2154
parser.add_argument("--debug", action="store_true",
2155
help="Debug mode; run in foreground and log"
2157
parser.add_argument("--debuglevel", metavar="LEVEL",
2158
help="Debug level for stdout output")
2159
parser.add_argument("--priority", help="GnuTLS"
2160
" priority string (see GnuTLS documentation)")
2161
parser.add_argument("--servicename",
2162
metavar="NAME", help="Zeroconf service name")
2163
parser.add_argument("--configdir",
2164
default="/etc/mandos", metavar="DIR",
2165
help="Directory to search for configuration"
2167
parser.add_argument("--no-dbus", action="store_false",
2168
dest="use_dbus", help="Do not provide D-Bus"
2169
" system bus interface")
2170
parser.add_argument("--no-ipv6", action="store_false",
2171
dest="use_ipv6", help="Do not use IPv6")
2172
parser.add_argument("--no-restore", action="store_false",
2173
dest="restore", help="Do not restore stored"
2175
parser.add_argument("--statedir", metavar="DIR",
2176
help="Directory to save/restore state in")
2178
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)
456
logger.debug(u"Adding service '%s' of type '%s' ...",
457
serviceName, serviceType)
460
serviceInterface, # interface
461
avahi.PROTO_INET6, # protocol
462
dbus.UInt32(0), # flags
463
serviceName, serviceType,
465
dbus.UInt16(servicePort),
466
avahi.string_array_to_txt_array(serviceTXT))
470
def remove_service():
471
"""From the Avahi server example code"""
474
if not group is None:
478
def server_state_changed(state):
479
"""From the Avahi server example code"""
480
if state == avahi.SERVER_COLLISION:
481
logger.warning(u"Server name collision")
483
elif state == avahi.SERVER_RUNNING:
487
def entry_group_state_changed(state, error):
488
"""From the Avahi server example code"""
489
global serviceName, server, rename_count
491
logger.debug(u"state change: %i", state)
493
if state == avahi.ENTRY_GROUP_ESTABLISHED:
494
logger.debug(u"Service established.")
495
elif state == avahi.ENTRY_GROUP_COLLISION:
497
rename_count = rename_count - 1
499
name = server.GetAlternativeServiceName(name)
500
logger.warning(u"Service name collision, "
501
u"changing name to '%s' ...", name)
506
logger.error(u"No suitable service name found "
507
u"after %i retries, exiting.",
510
elif state == avahi.ENTRY_GROUP_FAILURE:
511
logger.error(u"Error in group state changed %s",
517
def if_nametoindex(interface):
518
"""Call the C function if_nametoindex()"""
520
libc = ctypes.cdll.LoadLibrary("libc.so.6")
521
return libc.if_nametoindex(interface)
522
except (OSError, AttributeError):
523
if "struct" not in sys.modules:
525
if "fcntl" not in sys.modules:
527
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
529
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
530
struct.pack("16s16x", interface))
532
interface_index = struct.unpack("I", ifreq[16:20])[0]
533
return interface_index
536
if __name__ == '__main__':
537
parser = OptionParser()
538
parser.add_option("-i", "--interface", type="string",
539
default=None, metavar="IF",
540
help="Bind to interface IF")
541
parser.add_option("--cert", type="string", default="cert.pem",
543
help="Public key certificate PEM file to use")
544
parser.add_option("--key", type="string", default="key.pem",
546
help="Private key PEM file to use")
547
parser.add_option("--ca", type="string", default="ca.pem",
549
help="Certificate Authority certificate PEM file to use")
550
parser.add_option("--crl", type="string", default="crl.pem",
552
help="Certificate Revokation List PEM file to use")
553
parser.add_option("-p", "--port", type="int", default=None,
554
help="Port number to receive requests on")
555
parser.add_option("--timeout", type="string", # Parsed later
557
help="Amount of downtime allowed for clients")
558
parser.add_option("--interval", type="string", # Parsed later
560
help="How often to check that a client is up")
561
parser.add_option("--check", action="store_true", default=False,
562
help="Run self-test")
563
parser.add_option("--debug", action="store_true", default=False,
565
(options, args) = parser.parse_args()
2180
567
if options.check:
2182
569
doctest.testmod()
2185
# Default values for config file for server-global settings
2186
server_defaults = { "interface": "",
2191
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2192
"servicename": "Mandos",
2197
"statedir": "/var/lib/mandos"
2200
# Parse config file for server-global settings
2201
server_config = configparser.SafeConfigParser(server_defaults)
2203
server_config.read(os.path.join(options.configdir,
2205
# Convert the SafeConfigParser object to a dict
2206
server_settings = server_config.defaults()
2207
# Use the appropriate methods on the non-string config options
2208
for option in ("debug", "use_dbus", "use_ipv6"):
2209
server_settings[option] = server_config.getboolean("DEFAULT",
2211
if server_settings["port"]:
2212
server_settings["port"] = server_config.getint("DEFAULT",
2216
# Override the settings from the config file with command line
2218
for option in ("interface", "address", "port", "debug",
2219
"priority", "servicename", "configdir",
2220
"use_dbus", "use_ipv6", "debuglevel", "restore",
2222
value = getattr(options, option)
2223
if value is not None:
2224
server_settings[option] = value
2226
# Force all strings to be unicode
2227
for option in server_settings.keys():
2228
if type(server_settings[option]) is str:
2229
server_settings[option] = unicode(server_settings[option])
2230
# Now we have our good server settings in "server_settings"
2232
##################################################################
2235
debug = server_settings["debug"]
2236
debuglevel = server_settings["debuglevel"]
2237
use_dbus = server_settings["use_dbus"]
2238
use_ipv6 = server_settings["use_ipv6"]
2239
stored_state_path = os.path.join(server_settings["statedir"],
2243
initlogger(debug, logging.DEBUG)
2248
level = getattr(logging, debuglevel.upper())
2249
initlogger(debug, level)
2251
if server_settings["servicename"] != "Mandos":
2252
syslogger.setFormatter(logging.Formatter
2253
('Mandos ({0}) [%(process)d]:'
2254
' %(levelname)s: %(message)s'
2255
.format(server_settings
2258
# Parse config file with clients
2259
client_config = configparser.SafeConfigParser(Client
2261
client_config.read(os.path.join(server_settings["configdir"],
2264
global mandos_dbus_service
2265
mandos_dbus_service = None
2267
tcp_server = MandosServer((server_settings["address"],
2268
server_settings["port"]),
2270
interface=(server_settings["interface"]
2274
server_settings["priority"],
2277
pidfilename = "/var/run/mandos.pid"
2279
pidfile = open(pidfilename, "w")
2280
except IOError as e:
2281
logger.error("Could not open file %r", pidfilename,
2284
for name in ("_mandos", "mandos", "nobody"):
2286
uid = pwd.getpwnam(name).pw_uid
2287
gid = pwd.getpwnam(name).pw_gid
2297
except OSError as error:
2298
if error[0] != errno.EPERM:
2302
# Enable all possible GnuTLS debugging
2304
# "Use a log level over 10 to enable all debugging options."
2306
gnutls.library.functions.gnutls_global_set_log_level(11)
2308
@gnutls.library.types.gnutls_log_func
2309
def debug_gnutls(level, string):
2310
logger.debug("GnuTLS: %s", string[:-1])
2312
(gnutls.library.functions
2313
.gnutls_global_set_log_function(debug_gnutls))
2315
# Redirect stdin so all checkers get /dev/null
2316
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2317
os.dup2(null, sys.stdin.fileno())
2321
# Need to fork before connecting to D-Bus
2323
# Close all input and output, do double fork, etc.
2326
gobject.threads_init()
2329
# From the Avahi example code
2330
DBusGMainLoop(set_as_default=True)
572
# Parse the time arguments
574
options.timeout = string_to_delta(options.timeout)
576
parser.error("option --timeout: Unparseable time")
578
options.interval = string_to_delta(options.interval)
580
parser.error("option --interval: Unparseable time")
583
defaults = { "checker": "sleep 1; fping -q -- %%(fqdn)s" }
584
client_config = ConfigParser.SafeConfigParser(defaults)
585
#client_config.readfp(open("secrets.conf"), "secrets.conf")
586
client_config.read("mandos-clients.conf")
588
# From the Avahi server example code
589
DBusGMainLoop(set_as_default=True )
2331
590
main_loop = gobject.MainLoop()
2332
591
bus = dbus.SystemBus()
2333
# End of Avahi example code
2336
bus_name = dbus.service.BusName("se.recompile.Mandos",
2337
bus, do_not_queue=True)
2338
old_bus_name = (dbus.service.BusName
2339
("se.bsnet.fukt.Mandos", bus,
2341
except dbus.exceptions.NameExistsException as e:
2342
logger.error("Disabling D-Bus:", exc_info=e)
2344
server_settings["use_dbus"] = False
2345
tcp_server.use_dbus = False
2346
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2347
service = AvahiServiceToSyslog(name =
2348
server_settings["servicename"],
2349
servicetype = "_mandos._tcp",
2350
protocol = protocol, bus = bus)
2351
if server_settings["interface"]:
2352
service.interface = (if_nametoindex
2353
(str(server_settings["interface"])))
2355
global multiprocessing_manager
2356
multiprocessing_manager = multiprocessing.Manager()
2358
client_class = Client
2360
client_class = functools.partial(ClientDBus, bus = bus)
2362
client_settings = Client.config_parser(client_config)
2363
old_client_settings = {}
2366
# Get client data and settings from last running state.
2367
if server_settings["restore"]:
2369
with open(stored_state_path, "rb") as stored_state:
2370
clients_data, old_client_settings = (pickle.load
2372
os.remove(stored_state_path)
2373
except IOError as e:
2374
if e.errno == errno.ENOENT:
2375
logger.warning("Could not load persistent state: {0}"
2376
.format(os.strerror(e.errno)))
2378
logger.critical("Could not load persistent state:",
2381
except EOFError as e:
2382
logger.warning("Could not load persistent state: "
2383
"EOFError:", exc_info=e)
2385
with PGPEngine() as pgp:
2386
for client_name, client in clients_data.iteritems():
2387
# Decide which value to use after restoring saved state.
2388
# We have three different values: Old config file,
2389
# new config file, and saved state.
2390
# New config value takes precedence if it differs from old
2391
# config value, otherwise use saved state.
2392
for name, value in client_settings[client_name].items():
2394
# For each value in new config, check if it
2395
# differs from the old config value (Except for
2396
# the "secret" attribute)
2397
if (name != "secret" and
2398
value != old_client_settings[client_name]
2400
client[name] = value
2404
# Clients who has passed its expire date can still be
2405
# enabled if its last checker was successful. Clients
2406
# whose checker succeeded before we stored its state is
2407
# assumed to have successfully run all checkers during
2409
if client["enabled"]:
2410
if datetime.datetime.utcnow() >= client["expires"]:
2411
if not client["last_checked_ok"]:
2413
"disabling client {0} - Client never "
2414
"performed a successful checker"
2415
.format(client_name))
2416
client["enabled"] = False
2417
elif client["last_checker_status"] != 0:
2419
"disabling client {0} - Client "
2420
"last checker failed with error code {1}"
2421
.format(client_name,
2422
client["last_checker_status"]))
2423
client["enabled"] = False
2425
client["expires"] = (datetime.datetime
2427
+ client["timeout"])
2428
logger.debug("Last checker succeeded,"
2429
" keeping {0} enabled"
2430
.format(client_name))
2432
client["secret"] = (
2433
pgp.decrypt(client["encrypted_secret"],
2434
client_settings[client_name]
2437
# If decryption fails, we use secret from new settings
2438
logger.debug("Failed to decrypt {0} old secret"
2439
.format(client_name))
2440
client["secret"] = (
2441
client_settings[client_name]["secret"])
2443
# Add/remove clients based on new changes made to config
2444
for client_name in (set(old_client_settings)
2445
- set(client_settings)):
2446
del clients_data[client_name]
2447
for client_name in (set(client_settings)
2448
- set(old_client_settings)):
2449
clients_data[client_name] = client_settings[client_name]
2451
# Create all client objects
2452
for client_name, client in clients_data.iteritems():
2453
tcp_server.clients[client_name] = client_class(
2454
name = client_name, settings = client)
2456
if not tcp_server.clients:
2457
logger.warning("No clients defined")
2463
pidfile.write(str(pid) + "\n".encode("utf-8"))
2466
logger.error("Could not write to file %r with PID %d",
2469
# "pidfile" was never created
2472
signal.signal(signal.SIGINT, signal.SIG_IGN)
2474
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2475
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2478
@alternate_dbus_interfaces({"se.recompile.Mandos":
2479
"se.bsnet.fukt.Mandos"})
2480
class MandosDBusService(DBusObjectWithProperties):
2481
"""A D-Bus proxy object"""
2483
dbus.service.Object.__init__(self, bus, "/")
2484
_interface = "se.recompile.Mandos"
2486
@dbus_interface_annotations(_interface)
2488
return { "org.freedesktop.DBus.Property"
2489
".EmitsChangedSignal":
2492
@dbus.service.signal(_interface, signature="o")
2493
def ClientAdded(self, objpath):
2497
@dbus.service.signal(_interface, signature="ss")
2498
def ClientNotFound(self, fingerprint, address):
2502
@dbus.service.signal(_interface, signature="os")
2503
def ClientRemoved(self, objpath, name):
2507
@dbus.service.method(_interface, out_signature="ao")
2508
def GetAllClients(self):
2510
return dbus.Array(c.dbus_object_path
2512
tcp_server.clients.itervalues())
2514
@dbus.service.method(_interface,
2515
out_signature="a{oa{sv}}")
2516
def GetAllClientsWithProperties(self):
2518
return dbus.Dictionary(
2519
((c.dbus_object_path, c.GetAll(""))
2520
for c in tcp_server.clients.itervalues()),
2523
@dbus.service.method(_interface, in_signature="o")
2524
def RemoveClient(self, object_path):
2526
for c in tcp_server.clients.itervalues():
2527
if c.dbus_object_path == object_path:
2528
del tcp_server.clients[c.name]
2529
c.remove_from_connection()
2530
# Don't signal anything except ClientRemoved
2531
c.disable(quiet=True)
2533
self.ClientRemoved(object_path, c.name)
2535
raise KeyError(object_path)
2539
mandos_dbus_service = MandosDBusService()
2542
"Cleanup function; run on exit"
2545
multiprocessing.active_children()
2546
if not (tcp_server.clients or client_settings):
2549
# Store client before exiting. Secrets are encrypted with key
2550
# based on what config file has. If config file is
2551
# removed/edited, old secret will thus be unrecovable.
2553
with PGPEngine() as pgp:
2554
for client in tcp_server.clients.itervalues():
2555
key = client_settings[client.name]["secret"]
2556
client.encrypted_secret = pgp.encrypt(client.secret,
2560
# A list of attributes that can not be pickled
2562
exclude = set(("bus", "changedstate", "secret",
2564
for name, typ in (inspect.getmembers
2565
(dbus.service.Object)):
2568
client_dict["encrypted_secret"] = (client
2570
for attr in client.client_structure:
2571
if attr not in exclude:
2572
client_dict[attr] = getattr(client, attr)
2574
clients[client.name] = client_dict
2575
del client_settings[client.name]["secret"]
2578
with (tempfile.NamedTemporaryFile
2579
(mode='wb', suffix=".pickle", prefix='clients-',
2580
dir=os.path.dirname(stored_state_path),
2581
delete=False)) as stored_state:
2582
pickle.dump((clients, client_settings), stored_state)
2583
tempname=stored_state.name
2584
os.rename(tempname, stored_state_path)
2585
except (IOError, OSError) as e:
2591
if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
2592
logger.warning("Could not save persistent state: {0}"
2593
.format(os.strerror(e.errno)))
2595
logger.warning("Could not save persistent state:",
2599
# Delete all clients, and settings from config
2600
while tcp_server.clients:
2601
name, client = tcp_server.clients.popitem()
2603
client.remove_from_connection()
2604
# Don't signal anything except ClientRemoved
2605
client.disable(quiet=True)
2608
mandos_dbus_service.ClientRemoved(client
2611
client_settings.clear()
2613
atexit.register(cleanup)
2615
for client in tcp_server.clients.itervalues():
2618
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2619
# Need to initiate checking of clients
2621
client.init_checker()
2624
tcp_server.server_activate()
2626
# Find out what port we got
2627
service.port = tcp_server.socket.getsockname()[1]
2629
logger.info("Now listening on address %r, port %d,"
2630
" flowinfo %d, scope_id %d",
2631
*tcp_server.socket.getsockname())
2633
logger.info("Now listening on address %r, port %d",
2634
*tcp_server.socket.getsockname())
2636
#service.interface = tcp_server.socket.getsockname()[3]
592
server = dbus.Interface(
593
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
594
avahi.DBUS_INTERFACE_SERVER )
595
# End of Avahi example code
597
debug = options.debug
600
def remove_from_clients(client):
601
clients.remove(client)
603
logger.debug(u"No clients left, exiting")
606
clients.update(Set(Client(name=section, options=options,
607
stop_hook = remove_from_clients,
608
**(dict(client_config\
610
for section in client_config.sections()))
611
for client in clients:
614
tcp_server = IPv6_TCPServer((None, options.port),
618
# Find out what random port we got
619
servicePort = tcp_server.socket.getsockname()[1]
620
logger.debug(u"Now listening on port %d", servicePort)
622
if options.interface is not None:
623
serviceInterface = if_nametoindex(options.interface)
625
# From the Avahi server example code
626
server.connect_to_signal("StateChanged", server_state_changed)
627
server_state_changed(server.GetState())
628
# End of Avahi example code
630
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
631
lambda *args, **kwargs:
632
tcp_server.handle_request(*args[2:],
2639
# From the Avahi example code
2642
except dbus.exceptions.DBusException as error:
2643
logger.critical("D-Bus Exception", exc_info=error)
2646
# End of Avahi example code
2648
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2649
lambda *args, **kwargs:
2650
(tcp_server.handle_request
2651
(*args[2:], **kwargs) or True))
2653
logger.debug("Starting main loop")
2655
except AvahiError as error:
2656
logger.critical("Avahi Error", exc_info=error)
2659
636
except KeyboardInterrupt:
2661
print("", file=sys.stderr)
2662
logger.debug("Server received KeyboardInterrupt")
2663
logger.debug("Server exiting")
2664
# Must run before the D-Bus bus name gets deregistered
2667
if __name__ == '__main__':
641
# From the Avahi server example code
642
if not group is None:
644
# End of Avahi example code
646
for client in clients:
647
client.stop_hook = None