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
73
28
from dbus.mainloop.glib import DBusGMainLoop
76
import xml.dom.minidom
81
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
82
except AttributeError:
84
from IN import SO_BINDTODEVICE
86
SO_BINDTODEVICE = None
89
stored_state_file = "clients.pickle"
91
logger = logging.getLogger()
92
syslogger = (logging.handlers.SysLogHandler
93
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
94
address = str("/dev/log")))
97
if_nametoindex = (ctypes.cdll.LoadLibrary
98
(ctypes.util.find_library("c"))
100
except (OSError, AttributeError):
101
def if_nametoindex(interface):
102
"Get an interface index the hard way, i.e. using fcntl()"
103
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
104
with contextlib.closing(socket.socket()) as s:
105
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
106
struct.pack(str("16s16x"),
108
interface_index = struct.unpack(str("I"),
110
return interface_index
113
def initlogger(debug, level=logging.WARNING):
114
"""init logger and add loglevel"""
116
syslogger.setFormatter(logging.Formatter
117
('Mandos [%(process)d]: %(levelname)s:'
119
logger.addHandler(syslogger)
122
console = logging.StreamHandler()
123
console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
127
logger.addHandler(console)
128
logger.setLevel(level)
131
class PGPError(Exception):
132
"""Exception if encryption/decryption fails"""
136
class PGPEngine(object):
137
"""A simple class for OpenPGP symmetric encryption & decryption"""
139
self.gnupg = GnuPGInterface.GnuPG()
140
self.tempdir = tempfile.mkdtemp(prefix="mandos-")
141
self.gnupg = GnuPGInterface.GnuPG()
142
self.gnupg.options.meta_interactive = False
143
self.gnupg.options.homedir = self.tempdir
144
self.gnupg.options.extra_args.extend(['--force-mdc',
151
def __exit__ (self, exc_type, exc_value, traceback):
159
if self.tempdir is not None:
160
# Delete contents of tempdir
161
for root, dirs, files in os.walk(self.tempdir,
163
for filename in files:
164
os.remove(os.path.join(root, filename))
166
os.rmdir(os.path.join(root, dirname))
168
os.rmdir(self.tempdir)
171
def password_encode(self, password):
172
# Passphrase can not be empty and can not contain newlines or
173
# NUL bytes. So we prefix it and hex encode it.
174
return b"mandos" + binascii.hexlify(password)
176
def encrypt(self, data, password):
177
self.gnupg.passphrase = self.password_encode(password)
178
with open(os.devnull) as devnull:
180
proc = self.gnupg.run(['--symmetric'],
181
create_fhs=['stdin', 'stdout'],
182
attach_fhs={'stderr': devnull})
183
with contextlib.closing(proc.handles['stdin']) as f:
185
with contextlib.closing(proc.handles['stdout']) as f:
186
ciphertext = f.read()
190
self.gnupg.passphrase = None
193
def decrypt(self, data, password):
194
self.gnupg.passphrase = self.password_encode(password)
195
with open(os.devnull) as devnull:
197
proc = self.gnupg.run(['--decrypt'],
198
create_fhs=['stdin', 'stdout'],
199
attach_fhs={'stderr': devnull})
200
with contextlib.closing(proc.handles['stdin'] ) as f:
202
with contextlib.closing(proc.handles['stdout']) as f:
203
decrypted_plaintext = f.read()
207
self.gnupg.passphrase = None
208
return decrypted_plaintext
212
class AvahiError(Exception):
213
def __init__(self, value, *args, **kwargs):
215
super(AvahiError, self).__init__(value, *args, **kwargs)
216
def __unicode__(self):
217
return unicode(repr(self.value))
219
class AvahiServiceError(AvahiError):
222
class AvahiGroupError(AvahiError):
226
class AvahiService(object):
227
"""An Avahi (Zeroconf) service.
230
interface: integer; avahi.IF_UNSPEC or an interface index.
231
Used to optionally bind to the specified interface.
232
name: string; Example: 'Mandos'
233
type: string; Example: '_mandos._tcp'.
234
See <http://www.dns-sd.org/ServiceTypes.html>
235
port: integer; what port to announce
236
TXT: list of strings; TXT record for the service
237
domain: string; Domain to publish on, default to .local if empty.
238
host: string; Host to publish records for, default is localhost
239
max_renames: integer; maximum number of renames
240
rename_count: integer; counter so we only rename after collisions
241
a sensible number of times
242
group: D-Bus Entry Group
244
bus: dbus.SystemBus()
246
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
247
servicetype = None, port = None, TXT = None,
248
domain = "", host = "", max_renames = 32768,
249
protocol = avahi.PROTO_UNSPEC, bus = None):
250
self.interface = interface
252
self.type = servicetype
254
self.TXT = TXT if TXT is not None else []
257
self.rename_count = 0
258
self.max_renames = max_renames
259
self.protocol = protocol
260
self.group = None # our entry group
263
self.entry_group_state_changed_match = None
265
"""Derived from the Avahi example code"""
266
if self.rename_count >= self.max_renames:
267
logger.critical("No suitable Zeroconf service name found"
268
" after %i retries, exiting.",
270
raise AvahiServiceError("Too many renames")
271
self.name = unicode(self.server
272
.GetAlternativeServiceName(self.name))
273
logger.info("Changing Zeroconf service name to %r ...",
278
except dbus.exceptions.DBusException as error:
279
logger.critical("DBusException: %s", error)
282
self.rename_count += 1
284
"""Derived from the Avahi example code"""
285
if self.entry_group_state_changed_match is not None:
286
self.entry_group_state_changed_match.remove()
287
self.entry_group_state_changed_match = None
288
if self.group is not None:
291
"""Derived from the Avahi example code"""
293
if self.group is None:
294
self.group = dbus.Interface(
295
self.bus.get_object(avahi.DBUS_NAME,
296
self.server.EntryGroupNew()),
297
avahi.DBUS_INTERFACE_ENTRY_GROUP)
298
self.entry_group_state_changed_match = (
299
self.group.connect_to_signal(
300
'StateChanged', self.entry_group_state_changed))
301
logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
302
self.name, self.type)
303
self.group.AddService(
306
dbus.UInt32(0), # flags
307
self.name, self.type,
308
self.domain, self.host,
309
dbus.UInt16(self.port),
310
avahi.string_array_to_txt_array(self.TXT))
312
def entry_group_state_changed(self, state, error):
313
"""Derived from the Avahi example code"""
314
logger.debug("Avahi entry group state change: %i", state)
316
if state == avahi.ENTRY_GROUP_ESTABLISHED:
317
logger.debug("Zeroconf service established.")
318
elif state == avahi.ENTRY_GROUP_COLLISION:
319
logger.info("Zeroconf service name collision.")
321
elif state == avahi.ENTRY_GROUP_FAILURE:
322
logger.critical("Avahi: Error in group state changed %s",
324
raise AvahiGroupError("State changed: %s"
327
"""Derived from the Avahi example code"""
328
if self.group is not None:
331
except (dbus.exceptions.UnknownMethodException,
332
dbus.exceptions.DBusException):
336
def server_state_changed(self, state, error=None):
337
"""Derived from the Avahi example code"""
338
logger.debug("Avahi server state change: %i", state)
339
bad_states = { avahi.SERVER_INVALID:
340
"Zeroconf server invalid",
341
avahi.SERVER_REGISTERING: None,
342
avahi.SERVER_COLLISION:
343
"Zeroconf server name collision",
344
avahi.SERVER_FAILURE:
345
"Zeroconf server failure" }
346
if state in bad_states:
347
if bad_states[state] is not None:
349
logger.error(bad_states[state])
351
logger.error(bad_states[state] + ": %r", error)
353
elif state == avahi.SERVER_RUNNING:
357
logger.debug("Unknown state: %r", state)
359
logger.debug("Unknown state: %r: %r", state, error)
361
"""Derived from the Avahi example code"""
362
if self.server is None:
363
self.server = dbus.Interface(
364
self.bus.get_object(avahi.DBUS_NAME,
365
avahi.DBUS_PATH_SERVER,
366
follow_name_owner_changes=True),
367
avahi.DBUS_INTERFACE_SERVER)
368
self.server.connect_to_signal("StateChanged",
369
self.server_state_changed)
370
self.server_state_changed(self.server.GetState())
372
class AvahiServiceToSyslog(AvahiService):
374
"""Add the new name to the syslog messages"""
375
ret = AvahiService.rename(self)
376
syslogger.setFormatter(logging.Formatter
377
('Mandos (%s) [%%(process)d]:'
378
' %%(levelname)s: %%(message)s'
382
def timedelta_to_milliseconds(td):
383
"Convert a datetime.timedelta() to milliseconds"
384
return ((td.days * 24 * 60 * 60 * 1000)
385
+ (td.seconds * 1000)
386
+ (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
388
56
class Client(object):
389
57
"""A representation of a client host served by this server.
392
approved: bool(); 'None' if not yet approved/disapproved
393
approval_delay: datetime.timedelta(); Time to wait for approval
394
approval_duration: datetime.timedelta(); Duration of one approval
395
checker: subprocess.Popen(); a running checker process used
396
to see if the client lives.
397
'None' if no process is running.
398
checker_callback_tag: a gobject event source tag, or None
399
checker_command: string; External command which is run to check
400
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
401
78
runtime with vars(self) as dict, so that for
402
79
instance %(name)s can be used in the command.
403
checker_initiator_tag: a gobject event source tag, or None
404
created: datetime.datetime(); (UTC) object creation
405
client_structure: Object describing what attributes a client has
406
and is used for storing the client at exit
407
current_checker_command: string; current running checker_command
408
disable_initiator_tag: a gobject event source tag, or None
410
fingerprint: string (40 or 32 hexadecimal digits); used to
411
uniquely identify the client
412
host: string; available for use by the checker command
413
interval: datetime.timedelta(); How often to start a new checker
414
last_approval_request: datetime.datetime(); (UTC) or None
415
last_checked_ok: datetime.datetime(); (UTC) or None
416
last_checker_status: integer between 0 and 255 reflecting exit
417
status of last checker. -1 reflects crashed
419
last_enabled: datetime.datetime(); (UTC) or None
420
name: string; from the config file, used in log messages and
422
secret: bytestring; sent verbatim (over TLS) to client
423
timeout: datetime.timedelta(); How long from last_checked_ok
424
until this client is disabled
425
extended_timeout: extra long timeout when password has been sent
426
runtime_expansions: Allowed attributes for runtime expansion.
427
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: - '' -
431
runtime_expansions = ("approval_delay", "approval_duration",
432
"created", "enabled", "fingerprint",
433
"host", "interval", "last_checked_ok",
434
"last_enabled", "name", "timeout")
435
client_defaults = { "timeout": "5m",
436
"extended_timeout": "15m",
438
"checker": "fping -q -- %%(host)s",
440
"approval_delay": "0s",
441
"approval_duration": "1s",
442
"approved_by_default": "True",
446
def timeout_milliseconds(self):
447
"Return the 'timeout' attribute in milliseconds"
448
return timedelta_to_milliseconds(self.timeout)
450
def extended_timeout_milliseconds(self):
451
"Return the 'extended_timeout' attribute in milliseconds"
452
return timedelta_to_milliseconds(self.extended_timeout)
454
def interval_milliseconds(self):
455
"Return the 'interval' attribute in milliseconds"
456
return timedelta_to_milliseconds(self.interval)
458
def approval_delay_milliseconds(self):
459
return timedelta_to_milliseconds(self.approval_delay)
462
def config_parser(config):
463
""" Construct a new dict of client settings of this form:
464
{ client_name: {setting_name: value, ...}, ...}
465
with exceptions for any special settings as defined above"""
467
for client_name in config.sections():
468
section = dict(config.items(client_name))
469
client = settings[client_name] = {}
471
client["host"] = section["host"]
472
# Reformat values from string types to Python types
473
client["approved_by_default"] = config.getboolean(
474
client_name, "approved_by_default")
475
client["enabled"] = config.getboolean(client_name, "enabled")
477
client["fingerprint"] = (section["fingerprint"].upper()
479
if "secret" in section:
480
client["secret"] = section["secret"].decode("base64")
481
elif "secfile" in section:
482
with open(os.path.expanduser(os.path.expandvars
483
(section["secfile"])),
485
client["secret"] = secfile.read()
487
raise TypeError("No secret or secfile for section %s"
489
client["timeout"] = string_to_delta(section["timeout"])
490
client["extended_timeout"] = string_to_delta(
491
section["extended_timeout"])
492
client["interval"] = string_to_delta(section["interval"])
493
client["approval_delay"] = string_to_delta(
494
section["approval_delay"])
495
client["approval_duration"] = string_to_delta(
496
section["approval_duration"])
497
client["checker_command"] = section["checker"]
498
client["last_approval_request"] = None
499
client["last_checked_ok"] = None
500
client["last_checker_status"] = None
501
if client["enabled"]:
502
client["last_enabled"] = datetime.datetime.utcnow()
503
client["expires"] = (datetime.datetime.utcnow()
506
client["last_enabled"] = None
507
client["expires"] = None
512
def __init__(self, settings, name = None):
513
"""Note: the 'checker' key in 'config' sets the
514
'checker_command' attribute and *not* the 'checker'
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):
517
# adding all client settings
518
for setting, value in settings.iteritems():
519
setattr(self, setting, value)
521
logger.debug("Creating client %r", self.name)
522
# Uppercase and remove spaces from fingerprint for later
523
# comparison purposes with return value from the fingerprint()
525
logger.debug(" Fingerprint: %s", self.fingerprint)
526
self.created = settings.get("created", datetime.datetime.utcnow())
528
# 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
529
138
self.checker = None
530
139
self.checker_initiator_tag = None
531
self.disable_initiator_tag = None
140
self.stop_initiator_tag = None
532
141
self.checker_callback_tag = None
533
self.current_checker_command = None
535
self.approvals_pending = 0
536
self.changedstate = (multiprocessing_manager
537
.Condition(multiprocessing_manager
539
self.client_structure = [attr for attr in
540
self.__dict__.iterkeys()
541
if not attr.startswith("_")]
542
self.client_structure.append("client_structure")
544
for name, t in inspect.getmembers(type(self),
548
if not name.startswith("_"):
549
self.client_structure.append(name)
551
# Send notice to process children that client state has changed
552
def send_changedstate(self):
553
with self.changedstate:
554
self.changedstate.notify_all()
557
"""Start this client's checker and timeout hooks"""
558
if getattr(self, "enabled", False):
561
self.send_changedstate()
562
self.expires = datetime.datetime.utcnow() + self.timeout
564
self.last_enabled = datetime.datetime.utcnow()
567
def disable(self, quiet=True):
568
"""Disable this client."""
569
if not getattr(self, "enabled", False):
572
self.send_changedstate()
574
logger.info("Disabling client %s", self.name)
575
if getattr(self, "disable_initiator_tag", False):
576
gobject.source_remove(self.disable_initiator_tag)
577
self.disable_initiator_tag = None
579
if getattr(self, "checker_initiator_tag", False):
580
gobject.source_remove(self.checker_initiator_tag)
581
self.checker_initiator_tag = None
584
# Do not run this again if called by a gobject.timeout_add
590
def init_checker(self):
142
self.check_command = checker
144
"""Start this clients checker and timeout hooks"""
591
145
# Schedule a new checker to be started an 'interval' from now,
592
146
# and every interval from then on.
593
self.checker_initiator_tag = (gobject.timeout_add
594
(self.interval_milliseconds(),
596
# Schedule a disable() when 'timeout' has passed
597
self.disable_initiator_tag = (gobject.timeout_add
598
(self.timeout_milliseconds(),
147
self.checker_initiator_tag = gobject.timeout_add\
148
(self._interval_milliseconds,
600
150
# Also start a new checker *right now*.
601
151
self.start_checker()
603
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):
604
185
"""The checker has completed, so take appropriate actions."""
605
self.checker_callback_tag = None
607
if os.WIFEXITED(condition):
608
self.last_checker_status = os.WEXITSTATUS(condition)
609
if self.last_checker_status == 0:
610
logger.info("Checker for %(name)s succeeded",
614
logger.info("Checker for %(name)s failed",
617
self.last_checker_status = -1
618
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?",
621
def checked_ok(self, timeout=None):
622
"""Bump up the timeout for this client.
624
This should only be called when the client has been seen,
628
timeout = self.timeout
629
self.last_checked_ok = datetime.datetime.utcnow()
630
if self.disable_initiator_tag is not None:
631
gobject.source_remove(self.disable_initiator_tag)
632
if getattr(self, "enabled", False):
633
self.disable_initiator_tag = (gobject.timeout_add
634
(timedelta_to_milliseconds
635
(timeout), self.disable))
636
self.expires = datetime.datetime.utcnow() + timeout
638
def need_approval(self):
639
self.last_approval_request = datetime.datetime.utcnow()
200
logger.debug(u"Checker for %(name)s failed",
203
self.checker_callback_tag = None
641
204
def start_checker(self):
642
205
"""Start a new checker subprocess if one is not running.
644
206
If a checker already exists, leave it running and do
646
# The reason for not killing a running checker is that if we
647
# did that, then if a checker (for some reason) started
648
# running slowly and taking more than 'interval' time, the
649
# client would inevitably timeout, since no checker would get
650
# a chance to run to completion. If we instead leave running
651
# checkers alone, the checker would have to take more time
652
# than 'timeout' for the client to be disabled, which is as it
655
# If a checker exists, make sure it is not a zombie
657
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
658
except (AttributeError, OSError) as error:
659
if (isinstance(error, OSError)
660
and error.errno != errno.ECHILD):
664
logger.warning("Checker was a zombie")
665
gobject.source_remove(self.checker_callback_tag)
666
self.checker_callback(pid, status,
667
self.current_checker_command)
668
# Start a new checker if needed
669
208
if self.checker is None:
209
logger.debug(u"Starting checker for %s",
671
# In case checker_command has exactly one % operator
672
command = self.checker_command % self.host
212
command = self.check_command % self.fqdn
673
213
except TypeError:
674
# Escape attributes for the shell
675
escaped_attrs = dict(
677
re.escape(unicode(str(getattr(self, attr, "")),
681
self.runtime_expansions)
214
escaped_attrs = dict((key, re.escape(str(val)))
216
vars(self).iteritems())
684
command = self.checker_command % escaped_attrs
685
except TypeError as error:
686
logger.error('Could not format string "%s":'
687
' %s', self.checker_command, 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)
688
222
return True # Try again later
689
self.current_checker_command = command
691
logger.info("Starting checker %r for %s",
693
# We don't need to redirect stdout and stderr, since
694
# in normal mode, that is already done by daemon(),
695
# and in debug mode we don't want to. (Stdin is
696
# always replaced by /dev/null.)
697
self.checker = subprocess.Popen(command,
700
self.checker_callback_tag = (gobject.child_watch_add
702
self.checker_callback,
704
# The checker may have completed before the gobject
705
# watch was added. Check for this.
706
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
708
gobject.source_remove(self.checker_callback_tag)
709
self.checker_callback(pid, status, command)
710
except OSError as error:
711
logger.error("Failed to start subprocess: %s",
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",
713
236
# Re-run this periodically if run by gobject.timeout_add
716
238
def stop_checker(self):
717
239
"""Force the checker process, if any, to stop."""
718
if self.checker_callback_tag:
719
gobject.source_remove(self.checker_callback_tag)
720
self.checker_callback_tag = None
721
if getattr(self, "checker", None) is None:
240
if not hasattr(self, "checker") or self.checker is None:
723
logger.debug("Stopping checker for %(name)s", vars(self))
725
os.kill(self.checker.pid, signal.SIGTERM)
727
#if self.checker.poll() is None:
728
# os.kill(self.checker.pid, signal.SIGKILL)
729
except OSError as error:
730
if error.errno != errno.ESRCH: # No such process
735
def dbus_service_property(dbus_interface, signature="v",
736
access="readwrite", byte_arrays=False):
737
"""Decorators for marking methods of a DBusObjectWithProperties to
738
become properties on the D-Bus.
740
The decorated method will be called with no arguments by "Get"
741
and with one argument by "Set".
743
The parameters, where they are supported, are the same as
744
dbus.service.method, except there is only "signature", since the
745
type from Get() and the type sent to Set() is the same.
747
# Encoding deeply encoded byte arrays is not supported yet by the
748
# "Set" method, so we fail early here:
749
if byte_arrays and signature != "ay":
750
raise ValueError("Byte arrays not supported for non-'ay'"
751
" signature %r" % signature)
753
func._dbus_is_property = True
754
func._dbus_interface = dbus_interface
755
func._dbus_signature = signature
756
func._dbus_access = access
757
func._dbus_name = func.__name__
758
if func._dbus_name.endswith("_dbus_property"):
759
func._dbus_name = func._dbus_name[:-14]
760
func._dbus_get_args_options = {'byte_arrays': byte_arrays }
765
class DBusPropertyException(dbus.exceptions.DBusException):
766
"""A base class for D-Bus property-related exceptions
768
def __unicode__(self):
769
return unicode(str(self))
772
class DBusPropertyAccessException(DBusPropertyException):
773
"""A property's access permissions disallows an operation.
778
class DBusPropertyNotFound(DBusPropertyException):
779
"""An attempt was made to access a non-existing property.
784
class DBusObjectWithProperties(dbus.service.Object):
785
"""A D-Bus object with properties.
787
Classes inheriting from this can use the dbus_service_property
788
decorator to expose methods as D-Bus properties. It exposes the
789
standard Get(), Set(), and GetAll() methods on the D-Bus.
793
def _is_dbus_property(obj):
794
return getattr(obj, "_dbus_is_property", False)
796
def _get_all_dbus_properties(self):
797
"""Returns a generator of (name, attribute) pairs
799
return ((prop.__get__(self)._dbus_name, prop.__get__(self))
800
for cls in self.__class__.__mro__
802
inspect.getmembers(cls, self._is_dbus_property))
804
def _get_dbus_property(self, interface_name, property_name):
805
"""Returns a bound method if one exists which is a D-Bus
806
property with the specified name and interface.
808
for cls in self.__class__.__mro__:
809
for name, value in (inspect.getmembers
810
(cls, self._is_dbus_property)):
811
if (value._dbus_name == property_name
812
and value._dbus_interface == interface_name):
813
return value.__get__(self)
816
raise DBusPropertyNotFound(self.dbus_object_path + ":"
817
+ interface_name + "."
820
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ss",
822
def Get(self, interface_name, property_name):
823
"""Standard D-Bus property Get() method, see D-Bus standard.
825
prop = self._get_dbus_property(interface_name, property_name)
826
if prop._dbus_access == "write":
827
raise DBusPropertyAccessException(property_name)
829
if not hasattr(value, "variant_level"):
831
return type(value)(value, variant_level=value.variant_level+1)
833
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
834
def Set(self, interface_name, property_name, value):
835
"""Standard D-Bus property Set() method, see D-Bus standard.
837
prop = self._get_dbus_property(interface_name, property_name)
838
if prop._dbus_access == "read":
839
raise DBusPropertyAccessException(property_name)
840
if prop._dbus_get_args_options["byte_arrays"]:
841
# The byte_arrays option is not supported yet on
842
# signatures other than "ay".
843
if prop._dbus_signature != "ay":
845
value = dbus.ByteArray(''.join(unichr(byte)
849
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s",
850
out_signature="a{sv}")
851
def GetAll(self, interface_name):
852
"""Standard D-Bus property GetAll() method, see D-Bus
855
Note: Will not include properties with access="write".
858
for name, prop in self._get_all_dbus_properties():
860
and interface_name != prop._dbus_interface):
861
# Interface non-empty but did not match
863
# Ignore write-only properties
864
if prop._dbus_access == "write":
867
if not hasattr(value, "variant_level"):
868
properties[name] = value
870
properties[name] = type(value)(value, variant_level=
871
value.variant_level+1)
872
return dbus.Dictionary(properties, signature="sv")
874
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
876
path_keyword='object_path',
877
connection_keyword='connection')
878
def Introspect(self, object_path, connection):
879
"""Standard D-Bus method, overloaded to insert property tags.
881
xmlstring = dbus.service.Object.Introspect(self, object_path,
884
document = xml.dom.minidom.parseString(xmlstring)
885
def make_tag(document, name, prop):
886
e = document.createElement("property")
887
e.setAttribute("name", name)
888
e.setAttribute("type", prop._dbus_signature)
889
e.setAttribute("access", prop._dbus_access)
891
for if_tag in document.getElementsByTagName("interface"):
892
for tag in (make_tag(document, name, prop)
894
in self._get_all_dbus_properties()
895
if prop._dbus_interface
896
== if_tag.getAttribute("name")):
897
if_tag.appendChild(tag)
898
# Add the names to the return values for the
899
# "org.freedesktop.DBus.Properties" methods
900
if (if_tag.getAttribute("name")
901
== "org.freedesktop.DBus.Properties"):
902
for cn in if_tag.getElementsByTagName("method"):
903
if cn.getAttribute("name") == "Get":
904
for arg in cn.getElementsByTagName("arg"):
905
if (arg.getAttribute("direction")
907
arg.setAttribute("name", "value")
908
elif cn.getAttribute("name") == "GetAll":
909
for arg in cn.getElementsByTagName("arg"):
910
if (arg.getAttribute("direction")
912
arg.setAttribute("name", "props")
913
xmlstring = document.toxml("utf-8")
915
except (AttributeError, xml.dom.DOMException,
916
xml.parsers.expat.ExpatError) as error:
917
logger.error("Failed to override Introspection method",
922
def datetime_to_dbus (dt, variant_level=0):
923
"""Convert a UTC datetime.datetime() to a D-Bus type."""
925
return dbus.String("", variant_level = variant_level)
926
return dbus.String(dt.isoformat(),
927
variant_level=variant_level)
930
class AlternateDBusNamesMetaclass(DBusObjectWithProperties
932
"""Applied to an empty subclass of a D-Bus object, this metaclass
933
will add additional D-Bus attributes matching a certain pattern.
935
def __new__(mcs, name, bases, attr):
936
# Go through all the base classes which could have D-Bus
937
# methods, signals, or properties in them
938
for base in (b for b in bases
939
if issubclass(b, dbus.service.Object)):
940
# Go though all attributes of the base class
941
for attrname, attribute in inspect.getmembers(base):
942
# Ignore non-D-Bus attributes, and D-Bus attributes
943
# with the wrong interface name
944
if (not hasattr(attribute, "_dbus_interface")
945
or not attribute._dbus_interface
946
.startswith("se.recompile.Mandos")):
948
# Create an alternate D-Bus interface name based on
950
alt_interface = (attribute._dbus_interface
951
.replace("se.recompile.Mandos",
952
"se.bsnet.fukt.Mandos"))
953
# Is this a D-Bus signal?
954
if getattr(attribute, "_dbus_is_signal", False):
955
# Extract the original non-method function by
957
nonmethod_func = (dict(
958
zip(attribute.func_code.co_freevars,
959
attribute.__closure__))["func"]
961
# Create a new, but exactly alike, function
962
# object, and decorate it to be a new D-Bus signal
963
# with the alternate D-Bus interface name
964
new_function = (dbus.service.signal
966
attribute._dbus_signature)
968
nonmethod_func.func_code,
969
nonmethod_func.func_globals,
970
nonmethod_func.func_name,
971
nonmethod_func.func_defaults,
972
nonmethod_func.func_closure)))
973
# Define a creator of a function to call both the
974
# old and new functions, so both the old and new
975
# signals gets sent when the function is called
976
def fixscope(func1, func2):
977
"""This function is a scope container to pass
978
func1 and func2 to the "call_both" function
979
outside of its arguments"""
980
def call_both(*args, **kwargs):
981
"""This function will emit two D-Bus
982
signals by calling func1 and func2"""
983
func1(*args, **kwargs)
984
func2(*args, **kwargs)
986
# Create the "call_both" function and add it to
988
attr[attrname] = fixscope(attribute,
990
# Is this a D-Bus method?
991
elif getattr(attribute, "_dbus_is_method", False):
992
# Create a new, but exactly alike, function
993
# object. Decorate it to be a new D-Bus method
994
# with the alternate D-Bus interface name. Add it
996
attr[attrname] = (dbus.service.method
998
attribute._dbus_in_signature,
999
attribute._dbus_out_signature)
1001
(attribute.func_code,
1002
attribute.func_globals,
1003
attribute.func_name,
1004
attribute.func_defaults,
1005
attribute.func_closure)))
1006
# Is this a D-Bus property?
1007
elif getattr(attribute, "_dbus_is_property", False):
1008
# Create a new, but exactly alike, function
1009
# object, and decorate it to be a new D-Bus
1010
# property with the alternate D-Bus interface
1011
# name. Add it to the class.
1012
attr[attrname] = (dbus_service_property
1014
attribute._dbus_signature,
1015
attribute._dbus_access,
1017
._dbus_get_args_options
1020
(attribute.func_code,
1021
attribute.func_globals,
1022
attribute.func_name,
1023
attribute.func_defaults,
1024
attribute.func_closure)))
1025
return type.__new__(mcs, name, bases, attr)
1028
class ClientDBus(Client, DBusObjectWithProperties):
1029
"""A Client class using D-Bus
1032
dbus_object_path: dbus.ObjectPath
1033
bus: dbus.SystemBus()
1036
runtime_expansions = (Client.runtime_expansions
1037
+ ("dbus_object_path",))
1039
# dbus.service.Object doesn't use super(), so we can't either.
1041
def __init__(self, bus = None, *args, **kwargs):
1043
Client.__init__(self, *args, **kwargs)
1044
self._approvals_pending = 0
1046
self._approvals_pending = 0
1047
# Only now, when this client is initialized, can it show up on
1049
client_object_name = unicode(self.name).translate(
1050
{ord("."): ord("_"),
1051
ord("-"): ord("_")})
1052
self.dbus_object_path = (dbus.ObjectPath
1053
("/clients/" + client_object_name))
1054
DBusObjectWithProperties.__init__(self, self.bus,
1055
self.dbus_object_path)
1057
def notifychangeproperty(transform_func,
1058
dbus_name, type_func=lambda x: x,
1060
""" Modify a variable so that it's a property which announces
1061
its changes to DBus.
1063
transform_fun: Function that takes a value and a variant_level
1064
and transforms it to a D-Bus type.
1065
dbus_name: D-Bus name of the variable
1066
type_func: Function that transform the value before sending it
1067
to the D-Bus. Default: no transform
1068
variant_level: D-Bus variant level. Default: 1
1070
attrname = "_{0}".format(dbus_name)
1071
def setter(self, value):
1072
if hasattr(self, "dbus_object_path"):
1073
if (not hasattr(self, attrname) or
1074
type_func(getattr(self, attrname, None))
1075
!= type_func(value)):
1076
dbus_value = transform_func(type_func(value),
1079
self.PropertyChanged(dbus.String(dbus_name),
1081
setattr(self, attrname, value)
1083
return property(lambda self: getattr(self, attrname), setter)
1086
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1087
approvals_pending = notifychangeproperty(dbus.Boolean,
1090
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1091
last_enabled = notifychangeproperty(datetime_to_dbus,
1093
checker = notifychangeproperty(dbus.Boolean, "CheckerRunning",
1094
type_func = lambda checker:
1095
checker is not None)
1096
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1098
last_approval_request = notifychangeproperty(
1099
datetime_to_dbus, "LastApprovalRequest")
1100
approved_by_default = notifychangeproperty(dbus.Boolean,
1101
"ApprovedByDefault")
1102
approval_delay = notifychangeproperty(dbus.UInt64,
1105
timedelta_to_milliseconds)
1106
approval_duration = notifychangeproperty(
1107
dbus.UInt64, "ApprovalDuration",
1108
type_func = timedelta_to_milliseconds)
1109
host = notifychangeproperty(dbus.String, "Host")
1110
timeout = notifychangeproperty(dbus.UInt64, "Timeout",
1112
timedelta_to_milliseconds)
1113
extended_timeout = notifychangeproperty(
1114
dbus.UInt64, "ExtendedTimeout",
1115
type_func = timedelta_to_milliseconds)
1116
interval = notifychangeproperty(dbus.UInt64,
1119
timedelta_to_milliseconds)
1120
checker_command = notifychangeproperty(dbus.String, "Checker")
1122
del notifychangeproperty
1124
def __del__(self, *args, **kwargs):
1126
self.remove_from_connection()
1129
if hasattr(DBusObjectWithProperties, "__del__"):
1130
DBusObjectWithProperties.__del__(self, *args, **kwargs)
1131
Client.__del__(self, *args, **kwargs)
1133
def checker_callback(self, pid, condition, command,
242
gobject.source_remove(self.checker_callback_tag)
1135
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)
1136
247
self.checker = None
1137
if os.WIFEXITED(condition):
1138
exitstatus = os.WEXITSTATUS(condition)
1140
self.CheckerCompleted(dbus.Int16(exitstatus),
1141
dbus.Int64(condition),
1142
dbus.String(command))
1145
self.CheckerCompleted(dbus.Int16(-1),
1146
dbus.Int64(condition),
1147
dbus.String(command))
1149
return Client.checker_callback(self, pid, condition, command,
1152
def start_checker(self, *args, **kwargs):
1153
old_checker = self.checker
1154
if self.checker is not None:
1155
old_checker_pid = self.checker.pid
1157
old_checker_pid = None
1158
r = Client.start_checker(self, *args, **kwargs)
1159
# Only if new checker process was started
1160
if (self.checker is not None
1161
and old_checker_pid != self.checker.pid):
1163
self.CheckerStarted(self.current_checker_command)
1166
def _reset_approved(self):
1167
self.approved = None
1170
def approve(self, value=True):
1171
self.send_changedstate()
1172
self.approved = value
1173
gobject.timeout_add(timedelta_to_milliseconds
1174
(self.approval_duration),
1175
self._reset_approved)
1178
## D-Bus methods, signals & properties
1179
_interface = "se.recompile.Mandos.Client"
1183
# CheckerCompleted - signal
1184
@dbus.service.signal(_interface, signature="nxs")
1185
def CheckerCompleted(self, exitcode, waitstatus, command):
1189
# CheckerStarted - signal
1190
@dbus.service.signal(_interface, signature="s")
1191
def CheckerStarted(self, command):
1195
# PropertyChanged - signal
1196
@dbus.service.signal(_interface, signature="sv")
1197
def PropertyChanged(self, property, value):
1201
# GotSecret - signal
1202
@dbus.service.signal(_interface)
1203
def GotSecret(self):
1205
Is sent after a successful transfer of secret from the Mandos
1206
server to mandos-client
1211
@dbus.service.signal(_interface, signature="s")
1212
def Rejected(self, reason):
1216
# NeedApproval - signal
1217
@dbus.service.signal(_interface, signature="tb")
1218
def NeedApproval(self, timeout, default):
1220
return self.need_approval()
1222
# NeRwequest - signal
1223
@dbus.service.signal(_interface, signature="s")
1224
def NewRequest(self, ip):
1226
Is sent after a client request a password.
1233
@dbus.service.method(_interface, in_signature="b")
1234
def Approve(self, value):
1237
# CheckedOK - method
1238
@dbus.service.method(_interface)
1239
def CheckedOK(self):
1243
@dbus.service.method(_interface)
1248
# StartChecker - method
1249
@dbus.service.method(_interface)
1250
def StartChecker(self):
1252
self.start_checker()
1255
@dbus.service.method(_interface)
1260
# StopChecker - method
1261
@dbus.service.method(_interface)
1262
def StopChecker(self):
1267
# ApprovalPending - property
1268
@dbus_service_property(_interface, signature="b", access="read")
1269
def ApprovalPending_dbus_property(self):
1270
return dbus.Boolean(bool(self.approvals_pending))
1272
# ApprovedByDefault - property
1273
@dbus_service_property(_interface, signature="b",
1275
def ApprovedByDefault_dbus_property(self, value=None):
1276
if value is None: # get
1277
return dbus.Boolean(self.approved_by_default)
1278
self.approved_by_default = bool(value)
1280
# ApprovalDelay - property
1281
@dbus_service_property(_interface, signature="t",
1283
def ApprovalDelay_dbus_property(self, value=None):
1284
if value is None: # get
1285
return dbus.UInt64(self.approval_delay_milliseconds())
1286
self.approval_delay = datetime.timedelta(0, 0, 0, value)
1288
# ApprovalDuration - property
1289
@dbus_service_property(_interface, signature="t",
1291
def ApprovalDuration_dbus_property(self, value=None):
1292
if value is None: # get
1293
return dbus.UInt64(timedelta_to_milliseconds(
1294
self.approval_duration))
1295
self.approval_duration = datetime.timedelta(0, 0, 0, value)
1298
@dbus_service_property(_interface, signature="s", access="read")
1299
def Name_dbus_property(self):
1300
return dbus.String(self.name)
1302
# Fingerprint - property
1303
@dbus_service_property(_interface, signature="s", access="read")
1304
def Fingerprint_dbus_property(self):
1305
return dbus.String(self.fingerprint)
1308
@dbus_service_property(_interface, signature="s",
1310
def Host_dbus_property(self, value=None):
1311
if value is None: # get
1312
return dbus.String(self.host)
1313
self.host = unicode(value)
1315
# Created - property
1316
@dbus_service_property(_interface, signature="s", access="read")
1317
def Created_dbus_property(self):
1318
return datetime_to_dbus(self.created)
1320
# LastEnabled - property
1321
@dbus_service_property(_interface, signature="s", access="read")
1322
def LastEnabled_dbus_property(self):
1323
return datetime_to_dbus(self.last_enabled)
1325
# Enabled - property
1326
@dbus_service_property(_interface, signature="b",
1328
def Enabled_dbus_property(self, value=None):
1329
if value is None: # get
1330
return dbus.Boolean(self.enabled)
1336
# LastCheckedOK - property
1337
@dbus_service_property(_interface, signature="s",
1339
def LastCheckedOK_dbus_property(self, value=None):
1340
if value is not None:
1343
return datetime_to_dbus(self.last_checked_ok)
1345
# Expires - property
1346
@dbus_service_property(_interface, signature="s", access="read")
1347
def Expires_dbus_property(self):
1348
return datetime_to_dbus(self.expires)
1350
# LastApprovalRequest - property
1351
@dbus_service_property(_interface, signature="s", access="read")
1352
def LastApprovalRequest_dbus_property(self):
1353
return datetime_to_dbus(self.last_approval_request)
1355
# Timeout - property
1356
@dbus_service_property(_interface, signature="t",
1358
def Timeout_dbus_property(self, value=None):
1359
if value is None: # get
1360
return dbus.UInt64(self.timeout_milliseconds())
1361
self.timeout = datetime.timedelta(0, 0, 0, value)
1362
if getattr(self, "disable_initiator_tag", None) is None:
1364
# Reschedule timeout
1365
gobject.source_remove(self.disable_initiator_tag)
1366
self.disable_initiator_tag = None
1368
time_to_die = timedelta_to_milliseconds((self
1373
if time_to_die <= 0:
1374
# The timeout has passed
1377
self.expires = (datetime.datetime.utcnow()
1378
+ datetime.timedelta(milliseconds =
1380
self.disable_initiator_tag = (gobject.timeout_add
1381
(time_to_die, self.disable))
1383
# ExtendedTimeout - property
1384
@dbus_service_property(_interface, signature="t",
1386
def ExtendedTimeout_dbus_property(self, value=None):
1387
if value is None: # get
1388
return dbus.UInt64(self.extended_timeout_milliseconds())
1389
self.extended_timeout = datetime.timedelta(0, 0, 0, value)
1391
# Interval - property
1392
@dbus_service_property(_interface, signature="t",
1394
def Interval_dbus_property(self, value=None):
1395
if value is None: # get
1396
return dbus.UInt64(self.interval_milliseconds())
1397
self.interval = datetime.timedelta(0, 0, 0, value)
1398
if getattr(self, "checker_initiator_tag", None) is None:
1401
# Reschedule checker run
1402
gobject.source_remove(self.checker_initiator_tag)
1403
self.checker_initiator_tag = (gobject.timeout_add
1404
(value, self.start_checker))
1405
self.start_checker() # Start one now, too
1407
# Checker - property
1408
@dbus_service_property(_interface, signature="s",
1410
def Checker_dbus_property(self, value=None):
1411
if value is None: # get
1412
return dbus.String(self.checker_command)
1413
self.checker_command = unicode(value)
1415
# CheckerRunning - property
1416
@dbus_service_property(_interface, signature="b",
1418
def CheckerRunning_dbus_property(self, value=None):
1419
if value is None: # get
1420
return dbus.Boolean(self.checker is not None)
1422
self.start_checker()
1426
# ObjectPath - property
1427
@dbus_service_property(_interface, signature="o", access="read")
1428
def ObjectPath_dbus_property(self):
1429
return self.dbus_object_path # is already a dbus.ObjectPath
1432
@dbus_service_property(_interface, signature="ay",
1433
access="write", byte_arrays=True)
1434
def Secret_dbus_property(self, value):
1435
self.secret = str(value)
1440
class ProxyClient(object):
1441
def __init__(self, child_pipe, fpr, address):
1442
self._pipe = child_pipe
1443
self._pipe.send(('init', fpr, address))
1444
if not self._pipe.recv():
1447
def __getattribute__(self, name):
1449
return super(ProxyClient, self).__getattribute__(name)
1450
self._pipe.send(('getattr', name))
1451
data = self._pipe.recv()
1452
if data[0] == 'data':
1454
if data[0] == 'function':
1455
def func(*args, **kwargs):
1456
self._pipe.send(('funcall', name, args, kwargs))
1457
return self._pipe.recv()[1]
1460
def __setattr__(self, name, value):
1462
return super(ProxyClient, self).__setattr__(name, value)
1463
self._pipe.send(('setattr', name, value))
1466
class ClientDBusTransitional(ClientDBus):
1467
__metaclass__ = AlternateDBusNamesMetaclass
1470
class ClientHandler(socketserver.BaseRequestHandler, object):
1471
"""A class to handle client connections.
1473
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.
1474
307
Note: This will run in its own forked process."""
1476
309
def handle(self):
1477
with contextlib.closing(self.server.child_pipe) as child_pipe:
1478
logger.info("TCP connection from: %s",
1479
unicode(self.client_address))
1480
logger.debug("Pipe FD: %d",
1481
self.server.child_pipe.fileno())
1483
session = (gnutls.connection
1484
.ClientSession(self.request,
1486
.X509Credentials()))
1488
# Note: gnutls.connection.X509Credentials is really a
1489
# generic GnuTLS certificate credentials object so long as
1490
# no X.509 keys are added to it. Therefore, we can use it
1491
# here despite using OpenPGP certificates.
1493
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
1494
# "+AES-256-CBC", "+SHA1",
1495
# "+COMP-NULL", "+CTYPE-OPENPGP",
1497
# Use a fallback default, since this MUST be set.
1498
priority = self.server.gnutls_priority
1499
if priority is None:
1501
(gnutls.library.functions
1502
.gnutls_priority_set_direct(session._c_object,
1505
# Start communication using the Mandos protocol
1506
# Get protocol number
1507
line = self.request.makefile().readline()
1508
logger.debug("Protocol version: %r", line)
1510
if int(line.strip().split()[0]) > 1:
1512
except (ValueError, IndexError, RuntimeError) as error:
1513
logger.error("Unknown protocol version: %s", error)
1516
# Start GnuTLS connection
1519
except gnutls.errors.GNUTLSError as error:
1520
logger.warning("Handshake failed: %s", error)
1521
# Do not run session.bye() here: the session is not
1522
# established. Just abandon the request.
1524
logger.debug("Handshake succeeded")
1526
approval_required = False
1529
fpr = self.fingerprint(self.peer_certificate
1532
gnutls.errors.GNUTLSError) as error:
1533
logger.warning("Bad certificate: %s", error)
1535
logger.debug("Fingerprint: %s", fpr)
1538
client = ProxyClient(child_pipe, fpr,
1539
self.client_address)
1543
if self.server.use_dbus:
1545
client.NewRequest(str(self.client_address))
1547
if client.approval_delay:
1548
delay = client.approval_delay
1549
client.approvals_pending += 1
1550
approval_required = True
1553
if not client.enabled:
1554
logger.info("Client %s is disabled",
1556
if self.server.use_dbus:
1558
client.Rejected("Disabled")
1561
if client.approved or not client.approval_delay:
1562
#We are approved or approval is disabled
1564
elif client.approved is None:
1565
logger.info("Client %s needs approval",
1567
if self.server.use_dbus:
1569
client.NeedApproval(
1570
client.approval_delay_milliseconds(),
1571
client.approved_by_default)
1573
logger.warning("Client %s was not approved",
1575
if self.server.use_dbus:
1577
client.Rejected("Denied")
1580
#wait until timeout or approved
1581
time = datetime.datetime.now()
1582
client.changedstate.acquire()
1583
(client.changedstate.wait
1584
(float(client.timedelta_to_milliseconds(delay)
1586
client.changedstate.release()
1587
time2 = datetime.datetime.now()
1588
if (time2 - time) >= delay:
1589
if not client.approved_by_default:
1590
logger.warning("Client %s timed out while"
1591
" waiting for approval",
1593
if self.server.use_dbus:
1595
client.Rejected("Approval timed out")
1600
delay -= time2 - time
1603
while sent_size < len(client.secret):
1605
sent = session.send(client.secret[sent_size:])
1606
except gnutls.errors.GNUTLSError as error:
1607
logger.warning("gnutls send failed")
1609
logger.debug("Sent: %d, remaining: %d",
1610
sent, len(client.secret)
1611
- (sent_size + sent))
1614
logger.info("Sending secret to %s", client.name)
1615
# bump the timeout using extended_timeout
1616
client.checked_ok(client.extended_timeout)
1617
if self.server.use_dbus:
1622
if approval_required:
1623
client.approvals_pending -= 1
1626
except gnutls.errors.GNUTLSError as error:
1627
logger.warning("GnuTLS bye failed")
1630
def peer_certificate(session):
1631
"Return the peer's OpenPGP certificate as a bytestring"
1632
# If not an OpenPGP certificate...
1633
if (gnutls.library.functions
1634
.gnutls_certificate_type_get(session._c_object)
1635
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1636
# ...do the normal thing
1637
return session.peer_certificate
1638
list_size = ctypes.c_uint(1)
1639
cert_list = (gnutls.library.functions
1640
.gnutls_certificate_get_peers
1641
(session._c_object, ctypes.byref(list_size)))
1642
if not bool(cert_list) and list_size.value != 0:
1643
raise gnutls.errors.GNUTLSError("error getting peer"
1645
if list_size.value == 0:
1648
return ctypes.string_at(cert.data, cert.size)
1651
def fingerprint(openpgp):
1652
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1653
# New GnuTLS "datum" with the OpenPGP public key
1654
datum = (gnutls.library.types
1655
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1658
ctypes.c_uint(len(openpgp))))
1659
# New empty GnuTLS certificate
1660
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1661
(gnutls.library.functions
1662
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1663
# Import the OpenPGP public key into the certificate
1664
(gnutls.library.functions
1665
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1666
gnutls.library.constants
1667
.GNUTLS_OPENPGP_FMT_RAW))
1668
# Verify the self signature in the key
1669
crtverify = ctypes.c_uint()
1670
(gnutls.library.functions
1671
.gnutls_openpgp_crt_verify_self(crt, 0,
1672
ctypes.byref(crtverify)))
1673
if crtverify.value != 0:
1674
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1675
raise (gnutls.errors.CertificateSecurityError
1677
# New buffer for the fingerprint
1678
buf = ctypes.create_string_buffer(20)
1679
buf_len = ctypes.c_size_t()
1680
# Get the fingerprint from the certificate into the buffer
1681
(gnutls.library.functions
1682
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1683
ctypes.byref(buf_len)))
1684
# Deinit the certificate
1685
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1686
# Convert the buffer to a Python bytestring
1687
fpr = ctypes.string_at(buf, buf_len.value)
1688
# Convert the bytestring to hexadecimal notation
1689
hex_fpr = binascii.hexlify(fpr).upper()
1693
class MultiprocessingMixIn(object):
1694
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
1695
def sub_process_main(self, request, address):
1697
self.finish_request(request, address)
1699
self.handle_error(request, address)
1700
self.close_request(request)
1702
def process_request(self, request, address):
1703
"""Start a new process to process the request."""
1704
proc = multiprocessing.Process(target = self.sub_process_main,
1711
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
1712
""" adds a pipe to the MixIn """
1713
def process_request(self, request, client_address):
1714
"""Overrides and wraps the original process_request().
1716
This function creates a new pipe in self.pipe
1718
parent_pipe, self.child_pipe = multiprocessing.Pipe()
1720
proc = MultiprocessingMixIn.process_request(self, request,
1722
self.child_pipe.close()
1723
self.add_pipe(parent_pipe, proc)
1725
def add_pipe(self, parent_pipe, proc):
1726
"""Dummy function; override as necessary"""
1727
raise NotImplementedError
1730
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1731
socketserver.TCPServer, object):
1732
"""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.
1735
enabled: Boolean; whether this server is activated yet
1736
interface: None or a network interface name (string)
1737
use_ipv6: Boolean; to use IPv6 or not
368
options: Command line options
369
clients: Set() of Client objects
1739
def __init__(self, server_address, RequestHandlerClass,
1740
interface=None, use_ipv6=True):
1741
self.interface = interface
1743
self.address_family = socket.AF_INET6
1744
socketserver.TCPServer.__init__(self, server_address,
1745
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)
1746
380
def server_bind(self):
1747
381
"""This overrides the normal server_bind() function
1748
382
to bind to an interface if one was specified, and also NOT to
1749
383
bind to an address or port if they were not specified."""
1750
if self.interface is not None:
1751
if SO_BINDTODEVICE is None:
1752
logger.error("SO_BINDTODEVICE does not exist;"
1753
" cannot bind to interface %s",
1757
self.socket.setsockopt(socket.SOL_SOCKET,
1761
except socket.error as error:
1762
if error[0] == errno.EPERM:
1763
logger.error("No permission to"
1764
" bind to interface %s",
1766
elif error[0] == errno.ENOPROTOOPT:
1767
logger.error("SO_BINDTODEVICE not available;"
1768
" 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
logger.warning(u"No permission to"
395
u" bind to interface %s",
396
self.options.interface)
1772
399
# Only bind(2) the socket if we really need to.
1773
400
if self.server_address[0] or self.server_address[1]:
1774
401
if not self.server_address[0]:
1775
if self.address_family == socket.AF_INET6:
1776
any_address = "::" # in6addr_any
1778
any_address = socket.INADDR_ANY
1779
self.server_address = (any_address,
403
self.server_address = (in6addr_any,
1780
404
self.server_address[1])
1781
elif not self.server_address[1]:
405
elif self.server_address[1] is None:
1782
406
self.server_address = (self.server_address[0],
1784
# if self.interface:
1785
# self.server_address = (self.server_address[0],
1790
return socketserver.TCPServer.server_bind(self)
1793
class MandosServer(IPv6_TCPServer):
1797
clients: set of Client objects
1798
gnutls_priority GnuTLS priority string
1799
use_dbus: Boolean; to emit D-Bus signals or not
1801
Assumes a gobject.MainLoop event loop.
1803
def __init__(self, server_address, RequestHandlerClass,
1804
interface=None, use_ipv6=True, clients=None,
1805
gnutls_priority=None, use_dbus=True):
1806
self.enabled = False
1807
self.clients = clients
1808
if self.clients is None:
1810
self.use_dbus = use_dbus
1811
self.gnutls_priority = gnutls_priority
1812
IPv6_TCPServer.__init__(self, server_address,
1813
RequestHandlerClass,
1814
interface = interface,
1815
use_ipv6 = use_ipv6)
1816
def server_activate(self):
1818
return socketserver.TCPServer.server_activate(self)
1823
def add_pipe(self, parent_pipe, proc):
1824
# Call "handle_ipc" for both data and EOF events
1825
gobject.io_add_watch(parent_pipe.fileno(),
1826
gobject.IO_IN | gobject.IO_HUP,
1827
functools.partial(self.handle_ipc,
1832
def handle_ipc(self, source, condition, parent_pipe=None,
1833
proc = None, client_object=None):
1835
gobject.IO_IN: "IN", # There is data to read.
1836
gobject.IO_OUT: "OUT", # Data can be written (without
1838
gobject.IO_PRI: "PRI", # There is urgent data to read.
1839
gobject.IO_ERR: "ERR", # Error condition.
1840
gobject.IO_HUP: "HUP" # Hung up (the connection has been
1841
# broken, usually for pipes and
1844
conditions_string = ' | '.join(name
1846
condition_names.iteritems()
1847
if cond & condition)
1848
# error, or the other end of multiprocessing.Pipe has closed
1849
if condition & (gobject.IO_ERR | condition & gobject.IO_HUP):
1850
# Wait for other process to exit
1854
# Read a request from the child
1855
request = parent_pipe.recv()
1856
command = request[0]
1858
if command == 'init':
1860
address = request[2]
1862
for c in self.clients.itervalues():
1863
if c.fingerprint == fpr:
1867
logger.info("Client not found for fingerprint: %s, ad"
1868
"dress: %s", fpr, address)
1871
mandos_dbus_service.ClientNotFound(fpr,
1873
parent_pipe.send(False)
1876
gobject.io_add_watch(parent_pipe.fileno(),
1877
gobject.IO_IN | gobject.IO_HUP,
1878
functools.partial(self.handle_ipc,
1884
parent_pipe.send(True)
1885
# remove the old hook in favor of the new above hook on
1888
if command == 'funcall':
1889
funcname = request[1]
1893
parent_pipe.send(('data', getattr(client_object,
1897
if command == 'getattr':
1898
attrname = request[1]
1899
if callable(client_object.__getattribute__(attrname)):
1900
parent_pipe.send(('function',))
1902
parent_pipe.send(('data', client_object
1903
.__getattribute__(attrname)))
1905
if command == 'setattr':
1906
attrname = request[1]
1908
setattr(client_object, attrname, value)
408
return super(type(self), self).server_bind()
1913
411
def string_to_delta(interval):
1914
412
"""Parse a string and return a datetime.timedelta
1916
414
>>> string_to_delta('7d')
1917
415
datetime.timedelta(7)
1918
416
>>> string_to_delta('60s')
1921
419
datetime.timedelta(0, 3600)
1922
420
>>> string_to_delta('24h')
1923
421
datetime.timedelta(1)
1924
>>> string_to_delta('1w')
422
>>> string_to_delta(u'1w')
1925
423
datetime.timedelta(7)
1926
>>> string_to_delta('5m 30s')
1927
datetime.timedelta(0, 330)
1929
timevalue = datetime.timedelta(0)
1930
for s in interval.split():
1932
suffix = unicode(s[-1])
1935
delta = datetime.timedelta(value)
1937
delta = datetime.timedelta(0, value)
1939
delta = datetime.timedelta(0, 0, 0, 0, value)
1941
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
1943
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1945
raise ValueError("Unknown suffix %r" % suffix)
1946
except (ValueError, IndexError) as e:
1947
raise ValueError(*(e.args))
1952
def daemon(nochdir = False, noclose = False):
1953
"""See daemon(3). Standard BSD Unix function.
1955
This should really exist as os.daemon, but it doesn't (yet)."""
1964
# Close all standard open file descriptors
1965
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1966
if not stat.S_ISCHR(os.fstat(null).st_mode):
1967
raise OSError(errno.ENODEV,
1968
"%s not a character device"
1970
os.dup2(null, sys.stdin.fileno())
1971
os.dup2(null, sys.stdout.fileno())
1972
os.dup2(null, sys.stderr.fileno())
1979
##################################################################
1980
# Parsing of options, both command line and config file
1982
parser = argparse.ArgumentParser()
1983
parser.add_argument("-v", "--version", action="version",
1984
version = "%%(prog)s %s" % version,
1985
help="show version number and exit")
1986
parser.add_argument("-i", "--interface", metavar="IF",
1987
help="Bind to interface IF")
1988
parser.add_argument("-a", "--address",
1989
help="Address to listen for requests on")
1990
parser.add_argument("-p", "--port", type=int,
1991
help="Port number to receive requests on")
1992
parser.add_argument("--check", action="store_true",
1993
help="Run self-test")
1994
parser.add_argument("--debug", action="store_true",
1995
help="Debug mode; run in foreground and log"
1997
parser.add_argument("--debuglevel", metavar="LEVEL",
1998
help="Debug level for stdout output")
1999
parser.add_argument("--priority", help="GnuTLS"
2000
" priority string (see GnuTLS documentation)")
2001
parser.add_argument("--servicename",
2002
metavar="NAME", help="Zeroconf service name")
2003
parser.add_argument("--configdir",
2004
default="/etc/mandos", metavar="DIR",
2005
help="Directory to search for configuration"
2007
parser.add_argument("--no-dbus", action="store_false",
2008
dest="use_dbus", help="Do not provide D-Bus"
2009
" system bus interface")
2010
parser.add_argument("--no-ipv6", action="store_false",
2011
dest="use_ipv6", help="Do not use IPv6")
2012
parser.add_argument("--no-restore", action="store_false",
2013
dest="restore", help="Do not restore stored"
2015
parser.add_argument("--statedir", metavar="DIR",
2016
help="Directory to save/restore state in")
2018
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()
2020
567
if options.check:
2022
569
doctest.testmod()
2025
# Default values for config file for server-global settings
2026
server_defaults = { "interface": "",
2031
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2032
"servicename": "Mandos",
2037
"statedir": "/var/lib/mandos"
2040
# Parse config file for server-global settings
2041
server_config = configparser.SafeConfigParser(server_defaults)
2043
server_config.read(os.path.join(options.configdir,
2045
# Convert the SafeConfigParser object to a dict
2046
server_settings = server_config.defaults()
2047
# Use the appropriate methods on the non-string config options
2048
for option in ("debug", "use_dbus", "use_ipv6"):
2049
server_settings[option] = server_config.getboolean("DEFAULT",
2051
if server_settings["port"]:
2052
server_settings["port"] = server_config.getint("DEFAULT",
2056
# Override the settings from the config file with command line
2058
for option in ("interface", "address", "port", "debug",
2059
"priority", "servicename", "configdir",
2060
"use_dbus", "use_ipv6", "debuglevel", "restore",
2062
value = getattr(options, option)
2063
if value is not None:
2064
server_settings[option] = value
2066
# Force all strings to be unicode
2067
for option in server_settings.keys():
2068
if type(server_settings[option]) is str:
2069
server_settings[option] = unicode(server_settings[option])
2070
# Now we have our good server settings in "server_settings"
2072
##################################################################
2075
debug = server_settings["debug"]
2076
debuglevel = server_settings["debuglevel"]
2077
use_dbus = server_settings["use_dbus"]
2078
use_ipv6 = server_settings["use_ipv6"]
2079
stored_state_path = os.path.join(server_settings["statedir"],
2083
initlogger(debug, logging.DEBUG)
2088
level = getattr(logging, debuglevel.upper())
2089
initlogger(debug, level)
2091
if server_settings["servicename"] != "Mandos":
2092
syslogger.setFormatter(logging.Formatter
2093
('Mandos (%s) [%%(process)d]:'
2094
' %%(levelname)s: %%(message)s'
2095
% server_settings["servicename"]))
2097
# Parse config file with clients
2098
client_config = configparser.SafeConfigParser(Client.client_defaults)
2099
client_config.read(os.path.join(server_settings["configdir"],
2102
global mandos_dbus_service
2103
mandos_dbus_service = None
2105
tcp_server = MandosServer((server_settings["address"],
2106
server_settings["port"]),
2108
interface=(server_settings["interface"]
2112
server_settings["priority"],
2115
pidfilename = "/var/run/mandos.pid"
2117
pidfile = open(pidfilename, "w")
2119
logger.error("Could not open file %r", pidfilename)
2122
uid = pwd.getpwnam("_mandos").pw_uid
2123
gid = pwd.getpwnam("_mandos").pw_gid
2126
uid = pwd.getpwnam("mandos").pw_uid
2127
gid = pwd.getpwnam("mandos").pw_gid
2130
uid = pwd.getpwnam("nobody").pw_uid
2131
gid = pwd.getpwnam("nobody").pw_gid
2138
except OSError as error:
2139
if error[0] != errno.EPERM:
2143
# Enable all possible GnuTLS debugging
2145
# "Use a log level over 10 to enable all debugging options."
2147
gnutls.library.functions.gnutls_global_set_log_level(11)
2149
@gnutls.library.types.gnutls_log_func
2150
def debug_gnutls(level, string):
2151
logger.debug("GnuTLS: %s", string[:-1])
2153
(gnutls.library.functions
2154
.gnutls_global_set_log_function(debug_gnutls))
2156
# Redirect stdin so all checkers get /dev/null
2157
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
2158
os.dup2(null, sys.stdin.fileno())
2162
# Need to fork before connecting to D-Bus
2164
# Close all input and output, do double fork, etc.
2167
gobject.threads_init()
2170
# From the Avahi example code
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
2171
589
DBusGMainLoop(set_as_default=True )
2172
590
main_loop = gobject.MainLoop()
2173
591
bus = dbus.SystemBus()
2174
# End of Avahi example code
2177
bus_name = dbus.service.BusName("se.recompile.Mandos",
2178
bus, do_not_queue=True)
2179
old_bus_name = (dbus.service.BusName
2180
("se.bsnet.fukt.Mandos", bus,
2182
except dbus.exceptions.NameExistsException as e:
2183
logger.error(unicode(e) + ", disabling D-Bus")
2185
server_settings["use_dbus"] = False
2186
tcp_server.use_dbus = False
2187
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2188
service = AvahiServiceToSyslog(name =
2189
server_settings["servicename"],
2190
servicetype = "_mandos._tcp",
2191
protocol = protocol, bus = bus)
2192
if server_settings["interface"]:
2193
service.interface = (if_nametoindex
2194
(str(server_settings["interface"])))
2196
global multiprocessing_manager
2197
multiprocessing_manager = multiprocessing.Manager()
2199
client_class = Client
2201
client_class = functools.partial(ClientDBusTransitional,
2204
client_settings = Client.config_parser(client_config)
2205
old_client_settings = {}
2208
# Get client data and settings from last running state.
2209
if server_settings["restore"]:
2211
with open(stored_state_path, "rb") as stored_state:
2212
clients_data, old_client_settings = (pickle.load
2214
os.remove(stored_state_path)
2215
except IOError as e:
2216
logger.warning("Could not load persistent state: {0}"
2218
if e.errno != errno.ENOENT:
2220
except EOFError as e:
2221
logger.warning("Could not load persistent state: "
2222
"EOFError: {0}".format(e))
2224
with PGPEngine() as pgp:
2225
for client_name, client in clients_data.iteritems():
2226
# Decide which value to use after restoring saved state.
2227
# We have three different values: Old config file,
2228
# new config file, and saved state.
2229
# New config value takes precedence if it differs from old
2230
# config value, otherwise use saved state.
2231
for name, value in client_settings[client_name].items():
2233
# For each value in new config, check if it
2234
# differs from the old config value (Except for
2235
# the "secret" attribute)
2236
if (name != "secret" and
2237
value != old_client_settings[client_name]
2239
client[name] = value
2243
# Clients who has passed its expire date can still be
2244
# enabled if its last checker was successful. Clients
2245
# whose checker failed before we stored its state is
2246
# assumed to have failed all checkers during downtime.
2247
if client["enabled"]:
2248
if datetime.datetime.utcnow() >= client["expires"]:
2249
if not client["last_checked_ok"]:
2251
"disabling client {0} - Client never "
2252
"performed a successfull checker"
2253
.format(client["name"]))
2254
client["enabled"] = False
2255
elif client["last_checker_status"] != 0:
2257
"disabling client {0} - Client "
2258
"last checker failed with error code {1}"
2259
.format(client["name"],
2260
client["last_checker_status"]))
2261
client["enabled"] = False
2263
client["expires"] = (datetime.datetime
2265
+ client["timeout"])
2268
client["secret"] = (
2269
pgp.decrypt(client["encrypted_secret"],
2270
client_settings[client_name]
2273
# If decryption fails, we use secret from new settings
2274
logger.debug("Failed to decrypt {0} old secret"
2275
.format(client_name))
2276
client["secret"] = (
2277
client_settings[client_name]["secret"])
2280
# Add/remove clients based on new changes made to config
2281
for client_name in set(old_client_settings) - set(client_settings):
2282
del clients_data[client_name]
2283
for client_name in set(client_settings) - set(old_client_settings):
2284
clients_data[client_name] = client_settings[client_name]
2286
# Create clients all clients
2287
for client_name, client in clients_data.iteritems():
2288
tcp_server.clients[client_name] = client_class(
2289
name = client_name, settings = client)
2291
if not tcp_server.clients:
2292
logger.warning("No clients defined")
2298
pidfile.write(str(pid) + "\n".encode("utf-8"))
2301
logger.error("Could not write to file %r with PID %d",
2304
# "pidfile" was never created
2307
signal.signal(signal.SIGINT, signal.SIG_IGN)
2309
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2310
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2313
class MandosDBusService(dbus.service.Object):
2314
"""A D-Bus proxy object"""
2316
dbus.service.Object.__init__(self, bus, "/")
2317
_interface = "se.recompile.Mandos"
2319
@dbus.service.signal(_interface, signature="o")
2320
def ClientAdded(self, objpath):
2324
@dbus.service.signal(_interface, signature="ss")
2325
def ClientNotFound(self, fingerprint, address):
2329
@dbus.service.signal(_interface, signature="os")
2330
def ClientRemoved(self, objpath, name):
2334
@dbus.service.method(_interface, out_signature="ao")
2335
def GetAllClients(self):
2337
return dbus.Array(c.dbus_object_path
2339
tcp_server.clients.itervalues())
2341
@dbus.service.method(_interface,
2342
out_signature="a{oa{sv}}")
2343
def GetAllClientsWithProperties(self):
2345
return dbus.Dictionary(
2346
((c.dbus_object_path, c.GetAll(""))
2347
for c in tcp_server.clients.itervalues()),
2350
@dbus.service.method(_interface, in_signature="o")
2351
def RemoveClient(self, object_path):
2353
for c in tcp_server.clients.itervalues():
2354
if c.dbus_object_path == object_path:
2355
del tcp_server.clients[c.name]
2356
c.remove_from_connection()
2357
# Don't signal anything except ClientRemoved
2358
c.disable(quiet=True)
2360
self.ClientRemoved(object_path, c.name)
2362
raise KeyError(object_path)
2366
class MandosDBusServiceTransitional(MandosDBusService):
2367
__metaclass__ = AlternateDBusNamesMetaclass
2368
mandos_dbus_service = MandosDBusServiceTransitional()
2371
"Cleanup function; run on exit"
2374
multiprocessing.active_children()
2375
if not (tcp_server.clients or client_settings):
2378
# Store client before exiting. Secrets are encrypted with key
2379
# based on what config file has. If config file is
2380
# removed/edited, old secret will thus be unrecovable.
2382
with PGPEngine() as pgp:
2383
for client in tcp_server.clients.itervalues():
2384
key = client_settings[client.name]["secret"]
2385
client.encrypted_secret = pgp.encrypt(client.secret,
2389
# A list of attributes that can not be pickled
2391
exclude = set(("bus", "changedstate", "secret",
2393
for name, typ in (inspect.getmembers
2394
(dbus.service.Object)):
2397
client_dict["encrypted_secret"] = (client
2399
for attr in client.client_structure:
2400
if attr not in exclude:
2401
client_dict[attr] = getattr(client, attr)
2403
clients[client.name] = client_dict
2404
del client_settings[client.name]["secret"]
2407
tempfd, tempname = tempfile.mkstemp(suffix=".pickle",
2410
(stored_state_path))
2411
with os.fdopen(tempfd, "wb") as stored_state:
2412
pickle.dump((clients, client_settings), stored_state)
2413
os.rename(tempname, stored_state_path)
2414
except (IOError, OSError) as e:
2415
logger.warning("Could not save persistent state: {0}"
2422
if e.errno not in set((errno.ENOENT, errno.EACCES,
2426
# Delete all clients, and settings from config
2427
while tcp_server.clients:
2428
name, client = tcp_server.clients.popitem()
2430
client.remove_from_connection()
2431
# Don't signal anything except ClientRemoved
2432
client.disable(quiet=True)
2435
mandos_dbus_service.ClientRemoved(client
2438
client_settings.clear()
2440
atexit.register(cleanup)
2442
for client in tcp_server.clients.itervalues():
2445
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2446
# Need to initiate checking of clients
2448
client.init_checker()
2451
tcp_server.server_activate()
2453
# Find out what port we got
2454
service.port = tcp_server.socket.getsockname()[1]
2456
logger.info("Now listening on address %r, port %d,"
2457
" flowinfo %d, scope_id %d"
2458
% tcp_server.socket.getsockname())
2460
logger.info("Now listening on address %r, port %d"
2461
% tcp_server.socket.getsockname())
2463
#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:],
2466
# From the Avahi example code
2469
except dbus.exceptions.DBusException as error:
2470
logger.critical("DBusException: %s", error)
2473
# End of Avahi example code
2475
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2476
lambda *args, **kwargs:
2477
(tcp_server.handle_request
2478
(*args[2:], **kwargs) or True))
2480
logger.debug("Starting main loop")
2482
except AvahiError as error:
2483
logger.critical("AvahiError: %s", error)
2486
636
except KeyboardInterrupt:
2488
print("", file=sys.stderr)
2489
logger.debug("Server received KeyboardInterrupt")
2490
logger.debug("Server exiting")
2491
# Must run before the D-Bus bus name gets deregistered
2494
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