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
72
28
from dbus.mainloop.glib import DBusGMainLoop
75
import xml.dom.minidom
77
import Crypto.Cipher.AES
80
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
81
except AttributeError:
83
from IN import SO_BINDTODEVICE
85
SO_BINDTODEVICE = None
90
logger = logging.getLogger()
91
stored_state_path = "/var/lib/mandos/clients.pickle"
93
syslogger = (logging.handlers.SysLogHandler
94
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
95
address = str("/dev/log")))
96
syslogger.setFormatter(logging.Formatter
97
('Mandos [%(process)d]: %(levelname)s:'
99
logger.addHandler(syslogger)
101
console = logging.StreamHandler()
102
console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
106
logger.addHandler(console)
109
class AvahiError(Exception):
110
def __init__(self, value, *args, **kwargs):
112
super(AvahiError, self).__init__(value, *args, **kwargs)
113
def __unicode__(self):
114
return unicode(repr(self.value))
116
class AvahiServiceError(AvahiError):
119
class AvahiGroupError(AvahiError):
123
class AvahiService(object):
124
"""An Avahi (Zeroconf) service.
127
interface: integer; avahi.IF_UNSPEC or an interface index.
128
Used to optionally bind to the specified interface.
129
name: string; Example: 'Mandos'
130
type: string; Example: '_mandos._tcp'.
131
See <http://www.dns-sd.org/ServiceTypes.html>
132
port: integer; what port to announce
133
TXT: list of strings; TXT record for the service
134
domain: string; Domain to publish on, default to .local if empty.
135
host: string; Host to publish records for, default is localhost
136
max_renames: integer; maximum number of renames
137
rename_count: integer; counter so we only rename after collisions
138
a sensible number of times
139
group: D-Bus Entry Group
141
bus: dbus.SystemBus()
143
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
144
servicetype = None, port = None, TXT = None,
145
domain = "", host = "", max_renames = 32768,
146
protocol = avahi.PROTO_UNSPEC, bus = None):
147
self.interface = interface
149
self.type = servicetype
151
self.TXT = TXT if TXT is not None else []
154
self.rename_count = 0
155
self.max_renames = max_renames
156
self.protocol = protocol
157
self.group = None # our entry group
160
self.entry_group_state_changed_match = None
162
"""Derived from the Avahi example code"""
163
if self.rename_count >= self.max_renames:
164
logger.critical("No suitable Zeroconf service name found"
165
" after %i retries, exiting.",
167
raise AvahiServiceError("Too many renames")
168
self.name = unicode(self.server
169
.GetAlternativeServiceName(self.name))
170
logger.info("Changing Zeroconf service name to %r ...",
175
except dbus.exceptions.DBusException as error:
176
logger.critical("DBusException: %s", error)
179
self.rename_count += 1
181
"""Derived from the Avahi example code"""
182
if self.entry_group_state_changed_match is not None:
183
self.entry_group_state_changed_match.remove()
184
self.entry_group_state_changed_match = None
185
if self.group is not None:
188
"""Derived from the Avahi example code"""
190
if self.group is None:
191
self.group = dbus.Interface(
192
self.bus.get_object(avahi.DBUS_NAME,
193
self.server.EntryGroupNew()),
194
avahi.DBUS_INTERFACE_ENTRY_GROUP)
195
self.entry_group_state_changed_match = (
196
self.group.connect_to_signal(
197
'StateChanged', self.entry_group_state_changed))
198
logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
199
self.name, self.type)
200
self.group.AddService(
203
dbus.UInt32(0), # flags
204
self.name, self.type,
205
self.domain, self.host,
206
dbus.UInt16(self.port),
207
avahi.string_array_to_txt_array(self.TXT))
209
def entry_group_state_changed(self, state, error):
210
"""Derived from the Avahi example code"""
211
logger.debug("Avahi entry group state change: %i", state)
213
if state == avahi.ENTRY_GROUP_ESTABLISHED:
214
logger.debug("Zeroconf service established.")
215
elif state == avahi.ENTRY_GROUP_COLLISION:
216
logger.info("Zeroconf service name collision.")
218
elif state == avahi.ENTRY_GROUP_FAILURE:
219
logger.critical("Avahi: Error in group state changed %s",
221
raise AvahiGroupError("State changed: %s"
224
"""Derived from the Avahi example code"""
225
if self.group is not None:
228
except (dbus.exceptions.UnknownMethodException,
229
dbus.exceptions.DBusException) as e:
233
def server_state_changed(self, state, error=None):
234
"""Derived from the Avahi example code"""
235
logger.debug("Avahi server state change: %i", state)
236
bad_states = { avahi.SERVER_INVALID:
237
"Zeroconf server invalid",
238
avahi.SERVER_REGISTERING: None,
239
avahi.SERVER_COLLISION:
240
"Zeroconf server name collision",
241
avahi.SERVER_FAILURE:
242
"Zeroconf server failure" }
243
if state in bad_states:
244
if bad_states[state] is not None:
246
logger.error(bad_states[state])
248
logger.error(bad_states[state] + ": %r", error)
250
elif state == avahi.SERVER_RUNNING:
254
logger.debug("Unknown state: %r", state)
256
logger.debug("Unknown state: %r: %r", state, error)
258
"""Derived from the Avahi example code"""
259
if self.server is None:
260
self.server = dbus.Interface(
261
self.bus.get_object(avahi.DBUS_NAME,
262
avahi.DBUS_PATH_SERVER,
263
follow_name_owner_changes=True),
264
avahi.DBUS_INTERFACE_SERVER)
265
self.server.connect_to_signal("StateChanged",
266
self.server_state_changed)
267
self.server_state_changed(self.server.GetState())
269
class AvahiServiceToSyslog(AvahiService):
271
"""Add the new name to the syslog messages"""
272
ret = AvahiService.rename(self)
273
syslogger.setFormatter(logging.Formatter
274
('Mandos (%s) [%%(process)d]:'
275
' %%(levelname)s: %%(message)s'
279
def _timedelta_to_milliseconds(td):
280
"Convert a datetime.timedelta() to milliseconds"
281
return ((td.days * 24 * 60 * 60 * 1000)
282
+ (td.seconds * 1000)
283
+ (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
285
56
class Client(object):
286
57
"""A representation of a client host served by this server.
289
_approved: bool(); 'None' if not yet approved/disapproved
290
approval_delay: datetime.timedelta(); Time to wait for approval
291
approval_duration: datetime.timedelta(); Duration of one approval
292
checker: subprocess.Popen(); a running checker process used
293
to see if the client lives.
294
'None' if no process is running.
295
checker_callback_tag: a gobject event source tag, or None
296
checker_command: string; External command which is run to check
297
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
298
78
runtime with vars(self) as dict, so that for
299
79
instance %(name)s can be used in the command.
300
checker_initiator_tag: a gobject event source tag, or None
301
created: datetime.datetime(); (UTC) object creation
302
client_structure: Object describing what attributes a client has
303
and is used for storing the client at exit
304
current_checker_command: string; current running checker_command
305
disable_initiator_tag: a gobject event source tag, or None
307
fingerprint: string (40 or 32 hexadecimal digits); used to
308
uniquely identify the client
309
host: string; available for use by the checker command
310
interval: datetime.timedelta(); How often to start a new checker
311
last_approval_request: datetime.datetime(); (UTC) or None
312
last_checked_ok: datetime.datetime(); (UTC) or None
313
last_checker_status: integer between 0 and 255 reflecting exit
314
status of last checker. -1 reflect crashed
316
last_enabled: datetime.datetime(); (UTC)
317
name: string; from the config file, used in log messages and
319
secret: bytestring; sent verbatim (over TLS) to client
320
timeout: datetime.timedelta(); How long from last_checked_ok
321
until this client is disabled
322
extended_timeout: extra long timeout when password has been sent
323
runtime_expansions: Allowed attributes for runtime expansion.
324
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: - '' -
328
runtime_expansions = ("approval_delay", "approval_duration",
329
"created", "enabled", "fingerprint",
330
"host", "interval", "last_checked_ok",
331
"last_enabled", "name", "timeout")
333
def timeout_milliseconds(self):
334
"Return the 'timeout' attribute in milliseconds"
335
return _timedelta_to_milliseconds(self.timeout)
337
def extended_timeout_milliseconds(self):
338
"Return the 'extended_timeout' attribute in milliseconds"
339
return _timedelta_to_milliseconds(self.extended_timeout)
341
def interval_milliseconds(self):
342
"Return the 'interval' attribute in milliseconds"
343
return _timedelta_to_milliseconds(self.interval)
345
def approval_delay_milliseconds(self):
346
return _timedelta_to_milliseconds(self.approval_delay)
348
def __init__(self, name = None, config=None):
349
"""Note: the 'checker' key in 'config' sets the
350
'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):
355
logger.debug("Creating client %r", self.name)
356
# Uppercase and remove spaces from fingerprint for later
357
# comparison purposes with return value from the fingerprint()
359
self.fingerprint = (config["fingerprint"].upper()
361
logger.debug(" Fingerprint: %s", self.fingerprint)
362
if "secret" in config:
363
self.secret = config["secret"].decode("base64")
364
elif "secfile" in config:
365
with open(os.path.expanduser(os.path.expandvars
366
(config["secfile"])),
368
self.secret = secfile.read()
370
raise TypeError("No secret or secfile for client %s"
372
self.host = config.get("host", "")
373
self.created = datetime.datetime.utcnow()
375
self.last_approval_request = None
376
self.last_enabled = datetime.datetime.utcnow()
377
self.last_checked_ok = None
378
self.last_checker_status = None
379
self.timeout = string_to_delta(config["timeout"])
380
self.extended_timeout = string_to_delta(config
381
["extended_timeout"])
382
self.interval = string_to_delta(config["interval"])
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
383
138
self.checker = None
384
139
self.checker_initiator_tag = None
385
self.disable_initiator_tag = None
386
self.expires = datetime.datetime.utcnow() + self.timeout
140
self.stop_initiator_tag = None
387
141
self.checker_callback_tag = None
388
self.checker_command = config["checker"]
389
self.current_checker_command = None
390
self._approved = None
391
self.approved_by_default = config.get("approved_by_default",
393
self.approvals_pending = 0
394
self.approval_delay = string_to_delta(
395
config["approval_delay"])
396
self.approval_duration = string_to_delta(
397
config["approval_duration"])
398
self.changedstate = (multiprocessing_manager
399
.Condition(multiprocessing_manager
401
self.client_structure = [attr for attr
402
in self.__dict__.iterkeys()
403
if not attr.startswith("_")]
404
self.client_structure.append("client_structure")
407
for name, t in inspect.getmembers(type(self),
411
if not name.startswith("_"):
412
self.client_structure.append(name)
414
# Send notice to process children that client state has changed
415
def send_changedstate(self):
416
with self.changedstate:
417
self.changedstate.notify_all()
420
"""Start this client's checker and timeout hooks"""
421
if getattr(self, "enabled", False):
424
self.send_changedstate()
425
self.expires = datetime.datetime.utcnow() + self.timeout
427
self.last_enabled = datetime.datetime.utcnow()
430
def disable(self, quiet=True):
431
"""Disable this client."""
432
if not getattr(self, "enabled", False):
435
self.send_changedstate()
437
logger.info("Disabling client %s", self.name)
438
if getattr(self, "disable_initiator_tag", False):
439
gobject.source_remove(self.disable_initiator_tag)
440
self.disable_initiator_tag = None
442
if getattr(self, "checker_initiator_tag", False):
443
gobject.source_remove(self.checker_initiator_tag)
444
self.checker_initiator_tag = None
447
# Do not run this again if called by a gobject.timeout_add
453
def init_checker(self):
142
self.check_command = checker
144
"""Start this clients checker and timeout hooks"""
454
145
# Schedule a new checker to be started an 'interval' from now,
455
146
# and every interval from then on.
456
self.checker_initiator_tag = (gobject.timeout_add
457
(self.interval_milliseconds(),
459
# Schedule a disable() when 'timeout' has passed
460
self.disable_initiator_tag = (gobject.timeout_add
461
(self.timeout_milliseconds(),
147
self.checker_initiator_tag = gobject.timeout_add\
148
(self._interval_milliseconds,
463
150
# Also start a new checker *right now*.
464
151
self.start_checker()
467
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):
468
185
"""The checker has completed, so take appropriate actions."""
469
self.checker_callback_tag = None
471
if os.WIFEXITED(condition):
472
self.last_checker_status = os.WEXITSTATUS(condition)
473
if self.last_checker_status == 0:
474
logger.info("Checker for %(name)s succeeded",
478
logger.info("Checker for %(name)s failed",
481
self.last_checker_status = -1
482
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?",
485
def checked_ok(self, timeout=None):
486
"""Bump up the timeout for this client.
488
This should only be called when the client has been seen,
492
timeout = self.timeout
493
self.last_checked_ok = datetime.datetime.utcnow()
494
if self.disable_initiator_tag is not None:
495
gobject.source_remove(self.disable_initiator_tag)
496
if getattr(self, "enabled", False):
497
self.disable_initiator_tag = (gobject.timeout_add
498
(_timedelta_to_milliseconds
499
(timeout), self.disable))
500
self.expires = datetime.datetime.utcnow() + timeout
502
def need_approval(self):
503
self.last_approval_request = datetime.datetime.utcnow()
200
logger.debug(u"Checker for %(name)s failed",
203
self.checker_callback_tag = None
505
204
def start_checker(self):
506
205
"""Start a new checker subprocess if one is not running.
508
206
If a checker already exists, leave it running and do
510
# The reason for not killing a running checker is that if we
511
# did that, then if a checker (for some reason) started
512
# running slowly and taking more than 'interval' time, the
513
# client would inevitably timeout, since no checker would get
514
# a chance to run to completion. If we instead leave running
515
# checkers alone, the checker would have to take more time
516
# than 'timeout' for the client to be disabled, which is as it
519
# If a checker exists, make sure it is not a zombie
521
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
522
except (AttributeError, OSError) as error:
523
if (isinstance(error, OSError)
524
and error.errno != errno.ECHILD):
528
logger.warning("Checker was a zombie")
529
gobject.source_remove(self.checker_callback_tag)
530
self.checker_callback(pid, status,
531
self.current_checker_command)
532
# Start a new checker if needed
533
208
if self.checker is None:
209
logger.debug(u"Starting checker for %s",
535
# In case checker_command has exactly one % operator
536
command = self.checker_command % self.host
212
command = self.check_command % self.fqdn
537
213
except TypeError:
538
# Escape attributes for the shell
539
escaped_attrs = dict(
541
re.escape(unicode(str(getattr(self, attr, "")),
545
self.runtime_expansions)
214
escaped_attrs = dict((key, re.escape(str(val)))
216
vars(self).iteritems())
548
command = self.checker_command % escaped_attrs
549
except TypeError as error:
550
logger.error('Could not format string "%s":'
551
' %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)
552
222
return True # Try again later
553
self.current_checker_command = command
555
logger.info("Starting checker %r for %s",
557
# We don't need to redirect stdout and stderr, since
558
# in normal mode, that is already done by daemon(),
559
# and in debug mode we don't want to. (Stdin is
560
# always replaced by /dev/null.)
561
self.checker = subprocess.Popen(command,
564
self.checker_callback_tag = (gobject.child_watch_add
566
self.checker_callback,
568
# The checker may have completed before the gobject
569
# watch was added. Check for this.
570
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
572
gobject.source_remove(self.checker_callback_tag)
573
self.checker_callback(pid, status, command)
574
except OSError as error:
575
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",
577
236
# Re-run this periodically if run by gobject.timeout_add
580
238
def stop_checker(self):
581
239
"""Force the checker process, if any, to stop."""
582
if self.checker_callback_tag:
583
gobject.source_remove(self.checker_callback_tag)
584
self.checker_callback_tag = None
585
if getattr(self, "checker", None) is None:
240
if not hasattr(self, "checker") or self.checker is None:
587
logger.debug("Stopping checker for %(name)s", vars(self))
589
os.kill(self.checker.pid, signal.SIGTERM)
591
#if self.checker.poll() is None:
592
# os.kill(self.checker.pid, signal.SIGKILL)
593
except OSError as error:
594
if error.errno != errno.ESRCH: # No such process
598
# Encrypts a client secret and stores it in a varible
600
def encrypt_secret(self, key):
601
# Encryption-key need to be of a specific size, so we hash
603
hasheng = hashlib.sha256()
605
encryptionkey = hasheng.digest()
607
# Create validation hash so we know at decryption if it was
609
hasheng = hashlib.sha256()
610
hasheng.update(self.secret)
611
validationhash = hasheng.digest()
614
iv = os.urandom(Crypto.Cipher.AES.block_size)
615
ciphereng = Crypto.Cipher.AES.new(encryptionkey,
616
Crypto.Cipher.AES.MODE_CFB, iv)
617
ciphertext = ciphereng.encrypt(validationhash+self.secret)
618
self.encrypted_secret = (ciphertext, iv)
620
# Decrypt a encrypted client secret
621
def decrypt_secret(self, key):
622
# Decryption-key need to be of a specific size, so we hash
624
hasheng = hashlib.sha256()
626
encryptionkey = hasheng.digest()
628
# Decrypt encrypted secret
629
ciphertext, iv = self.encrypted_secret
630
ciphereng = Crypto.Cipher.AES.new(encryptionkey,
631
Crypto.Cipher.AES.MODE_CFB, iv)
632
plain = ciphereng.decrypt(ciphertext)
634
# Validate decrypted secret to know if it was succesful
635
hasheng = hashlib.sha256()
636
validationhash = plain[:hasheng.digest_size]
637
secret = plain[hasheng.digest_size:]
638
hasheng.update(secret)
640
# If validation fails, we use key as new secret. Otherwise, we
641
# use the decrypted secret
642
if hasheng.digest() == validationhash:
646
del self.encrypted_secret
649
def dbus_service_property(dbus_interface, signature="v",
650
access="readwrite", byte_arrays=False):
651
"""Decorators for marking methods of a DBusObjectWithProperties to
652
become properties on the D-Bus.
654
The decorated method will be called with no arguments by "Get"
655
and with one argument by "Set".
657
The parameters, where they are supported, are the same as
658
dbus.service.method, except there is only "signature", since the
659
type from Get() and the type sent to Set() is the same.
661
# Encoding deeply encoded byte arrays is not supported yet by the
662
# "Set" method, so we fail early here:
663
if byte_arrays and signature != "ay":
664
raise ValueError("Byte arrays not supported for non-'ay'"
665
" signature %r" % signature)
667
func._dbus_is_property = True
668
func._dbus_interface = dbus_interface
669
func._dbus_signature = signature
670
func._dbus_access = access
671
func._dbus_name = func.__name__
672
if func._dbus_name.endswith("_dbus_property"):
673
func._dbus_name = func._dbus_name[:-14]
674
func._dbus_get_args_options = {'byte_arrays': byte_arrays }
679
class DBusPropertyException(dbus.exceptions.DBusException):
680
"""A base class for D-Bus property-related exceptions
682
def __unicode__(self):
683
return unicode(str(self))
686
class DBusPropertyAccessException(DBusPropertyException):
687
"""A property's access permissions disallows an operation.
692
class DBusPropertyNotFound(DBusPropertyException):
693
"""An attempt was made to access a non-existing property.
698
class DBusObjectWithProperties(dbus.service.Object):
699
"""A D-Bus object with properties.
701
Classes inheriting from this can use the dbus_service_property
702
decorator to expose methods as D-Bus properties. It exposes the
703
standard Get(), Set(), and GetAll() methods on the D-Bus.
707
def _is_dbus_property(obj):
708
return getattr(obj, "_dbus_is_property", False)
710
def _get_all_dbus_properties(self):
711
"""Returns a generator of (name, attribute) pairs
713
return ((prop.__get__(self)._dbus_name, prop.__get__(self))
714
for cls in self.__class__.__mro__
716
inspect.getmembers(cls, self._is_dbus_property))
718
def _get_dbus_property(self, interface_name, property_name):
719
"""Returns a bound method if one exists which is a D-Bus
720
property with the specified name and interface.
722
for cls in self.__class__.__mro__:
723
for name, value in (inspect.getmembers
724
(cls, self._is_dbus_property)):
725
if (value._dbus_name == property_name
726
and value._dbus_interface == interface_name):
727
return value.__get__(self)
730
raise DBusPropertyNotFound(self.dbus_object_path + ":"
731
+ interface_name + "."
734
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ss",
736
def Get(self, interface_name, property_name):
737
"""Standard D-Bus property Get() method, see D-Bus standard.
739
prop = self._get_dbus_property(interface_name, property_name)
740
if prop._dbus_access == "write":
741
raise DBusPropertyAccessException(property_name)
743
if not hasattr(value, "variant_level"):
745
return type(value)(value, variant_level=value.variant_level+1)
747
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
748
def Set(self, interface_name, property_name, value):
749
"""Standard D-Bus property Set() method, see D-Bus standard.
751
prop = self._get_dbus_property(interface_name, property_name)
752
if prop._dbus_access == "read":
753
raise DBusPropertyAccessException(property_name)
754
if prop._dbus_get_args_options["byte_arrays"]:
755
# The byte_arrays option is not supported yet on
756
# signatures other than "ay".
757
if prop._dbus_signature != "ay":
759
value = dbus.ByteArray(''.join(unichr(byte)
763
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s",
764
out_signature="a{sv}")
765
def GetAll(self, interface_name):
766
"""Standard D-Bus property GetAll() method, see D-Bus
769
Note: Will not include properties with access="write".
772
for name, prop in self._get_all_dbus_properties():
774
and interface_name != prop._dbus_interface):
775
# Interface non-empty but did not match
777
# Ignore write-only properties
778
if prop._dbus_access == "write":
781
if not hasattr(value, "variant_level"):
784
all[name] = type(value)(value, variant_level=
785
value.variant_level+1)
786
return dbus.Dictionary(all, signature="sv")
788
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
790
path_keyword='object_path',
791
connection_keyword='connection')
792
def Introspect(self, object_path, connection):
793
"""Standard D-Bus method, overloaded to insert property tags.
795
xmlstring = dbus.service.Object.Introspect(self, object_path,
798
document = xml.dom.minidom.parseString(xmlstring)
799
def make_tag(document, name, prop):
800
e = document.createElement("property")
801
e.setAttribute("name", name)
802
e.setAttribute("type", prop._dbus_signature)
803
e.setAttribute("access", prop._dbus_access)
805
for if_tag in document.getElementsByTagName("interface"):
806
for tag in (make_tag(document, name, prop)
808
in self._get_all_dbus_properties()
809
if prop._dbus_interface
810
== if_tag.getAttribute("name")):
811
if_tag.appendChild(tag)
812
# Add the names to the return values for the
813
# "org.freedesktop.DBus.Properties" methods
814
if (if_tag.getAttribute("name")
815
== "org.freedesktop.DBus.Properties"):
816
for cn in if_tag.getElementsByTagName("method"):
817
if cn.getAttribute("name") == "Get":
818
for arg in cn.getElementsByTagName("arg"):
819
if (arg.getAttribute("direction")
821
arg.setAttribute("name", "value")
822
elif cn.getAttribute("name") == "GetAll":
823
for arg in cn.getElementsByTagName("arg"):
824
if (arg.getAttribute("direction")
826
arg.setAttribute("name", "props")
827
xmlstring = document.toxml("utf-8")
829
except (AttributeError, xml.dom.DOMException,
830
xml.parsers.expat.ExpatError) as error:
831
logger.error("Failed to override Introspection method",
836
def datetime_to_dbus (dt, variant_level=0):
837
"""Convert a UTC datetime.datetime() to a D-Bus type."""
839
return dbus.String("", variant_level = variant_level)
840
return dbus.String(dt.isoformat(),
841
variant_level=variant_level)
843
class AlternateDBusNamesMetaclass(DBusObjectWithProperties
845
"""Applied to an empty subclass of a D-Bus object, this metaclass
846
will add additional D-Bus attributes matching a certain pattern.
848
def __new__(mcs, name, bases, attr):
849
# Go through all the base classes which could have D-Bus
850
# methods, signals, or properties in them
851
for base in (b for b in bases
852
if issubclass(b, dbus.service.Object)):
853
# Go though all attributes of the base class
854
for attrname, attribute in inspect.getmembers(base):
855
# Ignore non-D-Bus attributes, and D-Bus attributes
856
# with the wrong interface name
857
if (not hasattr(attribute, "_dbus_interface")
858
or not attribute._dbus_interface
859
.startswith("se.recompile.Mandos")):
861
# Create an alternate D-Bus interface name based on
863
alt_interface = (attribute._dbus_interface
864
.replace("se.recompile.Mandos",
865
"se.bsnet.fukt.Mandos"))
866
# Is this a D-Bus signal?
867
if getattr(attribute, "_dbus_is_signal", False):
868
# Extract the original non-method function by
870
nonmethod_func = (dict(
871
zip(attribute.func_code.co_freevars,
872
attribute.__closure__))["func"]
874
# Create a new, but exactly alike, function
875
# object, and decorate it to be a new D-Bus signal
876
# with the alternate D-Bus interface name
877
new_function = (dbus.service.signal
879
attribute._dbus_signature)
881
nonmethod_func.func_code,
882
nonmethod_func.func_globals,
883
nonmethod_func.func_name,
884
nonmethod_func.func_defaults,
885
nonmethod_func.func_closure)))
886
# Define a creator of a function to call both the
887
# old and new functions, so both the old and new
888
# signals gets sent when the function is called
889
def fixscope(func1, func2):
890
"""This function is a scope container to pass
891
func1 and func2 to the "call_both" function
892
outside of its arguments"""
893
def call_both(*args, **kwargs):
894
"""This function will emit two D-Bus
895
signals by calling func1 and func2"""
896
func1(*args, **kwargs)
897
func2(*args, **kwargs)
899
# Create the "call_both" function and add it to
901
attr[attrname] = fixscope(attribute,
903
# Is this a D-Bus method?
904
elif getattr(attribute, "_dbus_is_method", False):
905
# Create a new, but exactly alike, function
906
# object. Decorate it to be a new D-Bus method
907
# with the alternate D-Bus interface name. Add it
909
attr[attrname] = (dbus.service.method
911
attribute._dbus_in_signature,
912
attribute._dbus_out_signature)
914
(attribute.func_code,
915
attribute.func_globals,
917
attribute.func_defaults,
918
attribute.func_closure)))
919
# Is this a D-Bus property?
920
elif getattr(attribute, "_dbus_is_property", False):
921
# Create a new, but exactly alike, function
922
# object, and decorate it to be a new D-Bus
923
# property with the alternate D-Bus interface
924
# name. Add it to the class.
925
attr[attrname] = (dbus_service_property
927
attribute._dbus_signature,
928
attribute._dbus_access,
930
._dbus_get_args_options
933
(attribute.func_code,
934
attribute.func_globals,
936
attribute.func_defaults,
937
attribute.func_closure)))
938
return type.__new__(mcs, name, bases, attr)
940
class ClientDBus(Client, DBusObjectWithProperties):
941
"""A Client class using D-Bus
944
dbus_object_path: dbus.ObjectPath
945
bus: dbus.SystemBus()
948
runtime_expansions = (Client.runtime_expansions
949
+ ("dbus_object_path",))
951
# dbus.service.Object doesn't use super(), so we can't either.
953
def __init__(self, bus = None, *args, **kwargs):
955
self._approvals_pending = 0
956
Client.__init__(self, *args, **kwargs)
959
def add_to_dbus(self):
960
# Only now, when this client is initialized, can it show up on
962
client_object_name = unicode(self.name).translate(
965
self.dbus_object_path = (dbus.ObjectPath
966
("/clients/" + client_object_name))
967
DBusObjectWithProperties.__init__(self, self.bus,
968
self.dbus_object_path)
970
def notifychangeproperty(transform_func,
971
dbus_name, type_func=lambda x: x,
973
""" Modify a variable so that it's a property which announces
976
transform_fun: Function that takes a value and a variant_level
977
and transforms it to a D-Bus type.
978
dbus_name: D-Bus name of the variable
979
type_func: Function that transform the value before sending it
980
to the D-Bus. Default: no transform
981
variant_level: D-Bus variant level. Default: 1
983
attrname = "_{0}".format(dbus_name)
984
def setter(self, value):
985
if hasattr(self, "dbus_object_path"):
986
if (not hasattr(self, attrname) or
987
type_func(getattr(self, attrname, None))
988
!= type_func(value)):
989
dbus_value = transform_func(type_func(value),
992
self.PropertyChanged(dbus.String(dbus_name),
994
setattr(self, attrname, value)
996
return property(lambda self: getattr(self, attrname), setter)
999
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1000
approvals_pending = notifychangeproperty(dbus.Boolean,
1003
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1004
last_enabled = notifychangeproperty(datetime_to_dbus,
1006
checker = notifychangeproperty(dbus.Boolean, "CheckerRunning",
1007
type_func = lambda checker:
1008
checker is not None)
1009
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1011
last_approval_request = notifychangeproperty(
1012
datetime_to_dbus, "LastApprovalRequest")
1013
approved_by_default = notifychangeproperty(dbus.Boolean,
1014
"ApprovedByDefault")
1015
approval_delay = notifychangeproperty(dbus.UInt16,
1018
_timedelta_to_milliseconds)
1019
approval_duration = notifychangeproperty(
1020
dbus.UInt16, "ApprovalDuration",
1021
type_func = _timedelta_to_milliseconds)
1022
host = notifychangeproperty(dbus.String, "Host")
1023
timeout = notifychangeproperty(dbus.UInt16, "Timeout",
1025
_timedelta_to_milliseconds)
1026
extended_timeout = notifychangeproperty(
1027
dbus.UInt16, "ExtendedTimeout",
1028
type_func = _timedelta_to_milliseconds)
1029
interval = notifychangeproperty(dbus.UInt16,
1032
_timedelta_to_milliseconds)
1033
checker_command = notifychangeproperty(dbus.String, "Checker")
1035
del notifychangeproperty
1037
def __del__(self, *args, **kwargs):
1039
self.remove_from_connection()
1042
if hasattr(DBusObjectWithProperties, "__del__"):
1043
DBusObjectWithProperties.__del__(self, *args, **kwargs)
1044
Client.__del__(self, *args, **kwargs)
1046
def checker_callback(self, pid, condition, command,
242
gobject.source_remove(self.checker_callback_tag)
1048
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)
1049
247
self.checker = None
1050
if os.WIFEXITED(condition):
1051
exitstatus = os.WEXITSTATUS(condition)
1053
self.CheckerCompleted(dbus.Int16(exitstatus),
1054
dbus.Int64(condition),
1055
dbus.String(command))
1058
self.CheckerCompleted(dbus.Int16(-1),
1059
dbus.Int64(condition),
1060
dbus.String(command))
1062
return Client.checker_callback(self, pid, condition, command,
1065
def start_checker(self, *args, **kwargs):
1066
old_checker = self.checker
1067
if self.checker is not None:
1068
old_checker_pid = self.checker.pid
1070
old_checker_pid = None
1071
r = Client.start_checker(self, *args, **kwargs)
1072
# Only if new checker process was started
1073
if (self.checker is not None
1074
and old_checker_pid != self.checker.pid):
1076
self.CheckerStarted(self.current_checker_command)
1079
def _reset_approved(self):
1080
self._approved = None
1083
def approve(self, value=True):
1084
self.send_changedstate()
1085
self._approved = value
1086
gobject.timeout_add(_timedelta_to_milliseconds
1087
(self.approval_duration),
1088
self._reset_approved)
1091
## D-Bus methods, signals & properties
1092
_interface = "se.recompile.Mandos.Client"
1096
# CheckerCompleted - signal
1097
@dbus.service.signal(_interface, signature="nxs")
1098
def CheckerCompleted(self, exitcode, waitstatus, command):
1102
# CheckerStarted - signal
1103
@dbus.service.signal(_interface, signature="s")
1104
def CheckerStarted(self, command):
1108
# PropertyChanged - signal
1109
@dbus.service.signal(_interface, signature="sv")
1110
def PropertyChanged(self, property, value):
1114
# GotSecret - signal
1115
@dbus.service.signal(_interface)
1116
def GotSecret(self):
1118
Is sent after a successful transfer of secret from the Mandos
1119
server to mandos-client
1124
@dbus.service.signal(_interface, signature="s")
1125
def Rejected(self, reason):
1129
# NeedApproval - signal
1130
@dbus.service.signal(_interface, signature="tb")
1131
def NeedApproval(self, timeout, default):
1133
return self.need_approval()
1138
@dbus.service.method(_interface, in_signature="b")
1139
def Approve(self, value):
1142
# CheckedOK - method
1143
@dbus.service.method(_interface)
1144
def CheckedOK(self):
1148
@dbus.service.method(_interface)
1153
# StartChecker - method
1154
@dbus.service.method(_interface)
1155
def StartChecker(self):
1157
self.start_checker()
1160
@dbus.service.method(_interface)
1165
# StopChecker - method
1166
@dbus.service.method(_interface)
1167
def StopChecker(self):
1172
# ApprovalPending - property
1173
@dbus_service_property(_interface, signature="b", access="read")
1174
def ApprovalPending_dbus_property(self):
1175
return dbus.Boolean(bool(self.approvals_pending))
1177
# ApprovedByDefault - property
1178
@dbus_service_property(_interface, signature="b",
1180
def ApprovedByDefault_dbus_property(self, value=None):
1181
if value is None: # get
1182
return dbus.Boolean(self.approved_by_default)
1183
self.approved_by_default = bool(value)
1185
# ApprovalDelay - property
1186
@dbus_service_property(_interface, signature="t",
1188
def ApprovalDelay_dbus_property(self, value=None):
1189
if value is None: # get
1190
return dbus.UInt64(self.approval_delay_milliseconds())
1191
self.approval_delay = datetime.timedelta(0, 0, 0, value)
1193
# ApprovalDuration - property
1194
@dbus_service_property(_interface, signature="t",
1196
def ApprovalDuration_dbus_property(self, value=None):
1197
if value is None: # get
1198
return dbus.UInt64(_timedelta_to_milliseconds(
1199
self.approval_duration))
1200
self.approval_duration = datetime.timedelta(0, 0, 0, value)
1203
@dbus_service_property(_interface, signature="s", access="read")
1204
def Name_dbus_property(self):
1205
return dbus.String(self.name)
1207
# Fingerprint - property
1208
@dbus_service_property(_interface, signature="s", access="read")
1209
def Fingerprint_dbus_property(self):
1210
return dbus.String(self.fingerprint)
1213
@dbus_service_property(_interface, signature="s",
1215
def Host_dbus_property(self, value=None):
1216
if value is None: # get
1217
return dbus.String(self.host)
1220
# Created - property
1221
@dbus_service_property(_interface, signature="s", access="read")
1222
def Created_dbus_property(self):
1223
return dbus.String(datetime_to_dbus(self.created))
1225
# LastEnabled - property
1226
@dbus_service_property(_interface, signature="s", access="read")
1227
def LastEnabled_dbus_property(self):
1228
return datetime_to_dbus(self.last_enabled)
1230
# Enabled - property
1231
@dbus_service_property(_interface, signature="b",
1233
def Enabled_dbus_property(self, value=None):
1234
if value is None: # get
1235
return dbus.Boolean(self.enabled)
1241
# LastCheckedOK - property
1242
@dbus_service_property(_interface, signature="s",
1244
def LastCheckedOK_dbus_property(self, value=None):
1245
if value is not None:
1248
return datetime_to_dbus(self.last_checked_ok)
1250
# Expires - property
1251
@dbus_service_property(_interface, signature="s", access="read")
1252
def Expires_dbus_property(self):
1253
return datetime_to_dbus(self.expires)
1255
# LastApprovalRequest - property
1256
@dbus_service_property(_interface, signature="s", access="read")
1257
def LastApprovalRequest_dbus_property(self):
1258
return datetime_to_dbus(self.last_approval_request)
1260
# Timeout - property
1261
@dbus_service_property(_interface, signature="t",
1263
def Timeout_dbus_property(self, value=None):
1264
if value is None: # get
1265
return dbus.UInt64(self.timeout_milliseconds())
1266
self.timeout = datetime.timedelta(0, 0, 0, value)
1267
if getattr(self, "disable_initiator_tag", None) is None:
1269
# Reschedule timeout
1270
gobject.source_remove(self.disable_initiator_tag)
1271
self.disable_initiator_tag = None
1273
time_to_die = _timedelta_to_milliseconds((self
1278
if time_to_die <= 0:
1279
# The timeout has passed
1282
self.expires = (datetime.datetime.utcnow()
1283
+ datetime.timedelta(milliseconds =
1285
self.disable_initiator_tag = (gobject.timeout_add
1286
(time_to_die, self.disable))
1288
# ExtendedTimeout - property
1289
@dbus_service_property(_interface, signature="t",
1291
def ExtendedTimeout_dbus_property(self, value=None):
1292
if value is None: # get
1293
return dbus.UInt64(self.extended_timeout_milliseconds())
1294
self.extended_timeout = datetime.timedelta(0, 0, 0, value)
1296
# Interval - property
1297
@dbus_service_property(_interface, signature="t",
1299
def Interval_dbus_property(self, value=None):
1300
if value is None: # get
1301
return dbus.UInt64(self.interval_milliseconds())
1302
self.interval = datetime.timedelta(0, 0, 0, value)
1303
if getattr(self, "checker_initiator_tag", None) is None:
1305
# Reschedule checker run
1306
gobject.source_remove(self.checker_initiator_tag)
1307
self.checker_initiator_tag = (gobject.timeout_add
1308
(value, self.start_checker))
1309
self.start_checker() # Start one now, too
1311
# Checker - property
1312
@dbus_service_property(_interface, signature="s",
1314
def Checker_dbus_property(self, value=None):
1315
if value is None: # get
1316
return dbus.String(self.checker_command)
1317
self.checker_command = value
1319
# CheckerRunning - property
1320
@dbus_service_property(_interface, signature="b",
1322
def CheckerRunning_dbus_property(self, value=None):
1323
if value is None: # get
1324
return dbus.Boolean(self.checker is not None)
1326
self.start_checker()
1330
# ObjectPath - property
1331
@dbus_service_property(_interface, signature="o", access="read")
1332
def ObjectPath_dbus_property(self):
1333
return self.dbus_object_path # is already a dbus.ObjectPath
1336
@dbus_service_property(_interface, signature="ay",
1337
access="write", byte_arrays=True)
1338
def Secret_dbus_property(self, value):
1339
self.secret = str(value)
1344
class ProxyClient(object):
1345
def __init__(self, child_pipe, fpr, address):
1346
self._pipe = child_pipe
1347
self._pipe.send(('init', fpr, address))
1348
if not self._pipe.recv():
1351
def __getattribute__(self, name):
1352
if(name == '_pipe'):
1353
return super(ProxyClient, self).__getattribute__(name)
1354
self._pipe.send(('getattr', name))
1355
data = self._pipe.recv()
1356
if data[0] == 'data':
1358
if data[0] == 'function':
1359
def func(*args, **kwargs):
1360
self._pipe.send(('funcall', name, args, kwargs))
1361
return self._pipe.recv()[1]
1364
def __setattr__(self, name, value):
1365
if(name == '_pipe'):
1366
return super(ProxyClient, self).__setattr__(name, value)
1367
self._pipe.send(('setattr', name, value))
1369
class ClientDBusTransitional(ClientDBus):
1370
__metaclass__ = AlternateDBusNamesMetaclass
1372
class ClientHandler(socketserver.BaseRequestHandler, object):
1373
"""A class to handle client connections.
1375
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.
1376
307
Note: This will run in its own forked process."""
1378
309
def handle(self):
1379
with contextlib.closing(self.server.child_pipe) as child_pipe:
1380
logger.info("TCP connection from: %s",
1381
unicode(self.client_address))
1382
logger.debug("Pipe FD: %d",
1383
self.server.child_pipe.fileno())
1385
session = (gnutls.connection
1386
.ClientSession(self.request,
1388
.X509Credentials()))
1390
# Note: gnutls.connection.X509Credentials is really a
1391
# generic GnuTLS certificate credentials object so long as
1392
# no X.509 keys are added to it. Therefore, we can use it
1393
# here despite using OpenPGP certificates.
1395
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
1396
# "+AES-256-CBC", "+SHA1",
1397
# "+COMP-NULL", "+CTYPE-OPENPGP",
1399
# Use a fallback default, since this MUST be set.
1400
priority = self.server.gnutls_priority
1401
if priority is None:
1403
(gnutls.library.functions
1404
.gnutls_priority_set_direct(session._c_object,
1407
# Start communication using the Mandos protocol
1408
# Get protocol number
1409
line = self.request.makefile().readline()
1410
logger.debug("Protocol version: %r", line)
1412
if int(line.strip().split()[0]) > 1:
1414
except (ValueError, IndexError, RuntimeError) as error:
1415
logger.error("Unknown protocol version: %s", error)
1418
# Start GnuTLS connection
1421
except gnutls.errors.GNUTLSError as error:
1422
logger.warning("Handshake failed: %s", error)
1423
# Do not run session.bye() here: the session is not
1424
# established. Just abandon the request.
1426
logger.debug("Handshake succeeded")
1428
approval_required = False
1431
fpr = self.fingerprint(self.peer_certificate
1434
gnutls.errors.GNUTLSError) as error:
1435
logger.warning("Bad certificate: %s", error)
1437
logger.debug("Fingerprint: %s", fpr)
1440
client = ProxyClient(child_pipe, fpr,
1441
self.client_address)
1445
if client.approval_delay:
1446
delay = client.approval_delay
1447
client.approvals_pending += 1
1448
approval_required = True
1451
if not client.enabled:
1452
logger.info("Client %s is disabled",
1454
if self.server.use_dbus:
1456
client.Rejected("Disabled")
1459
if client._approved or not client.approval_delay:
1460
#We are approved or approval is disabled
1462
elif client._approved is None:
1463
logger.info("Client %s needs approval",
1465
if self.server.use_dbus:
1467
client.NeedApproval(
1468
client.approval_delay_milliseconds(),
1469
client.approved_by_default)
1471
logger.warning("Client %s was not approved",
1473
if self.server.use_dbus:
1475
client.Rejected("Denied")
1478
#wait until timeout or approved
1479
time = datetime.datetime.now()
1480
client.changedstate.acquire()
1481
(client.changedstate.wait
1482
(float(client._timedelta_to_milliseconds(delay)
1484
client.changedstate.release()
1485
time2 = datetime.datetime.now()
1486
if (time2 - time) >= delay:
1487
if not client.approved_by_default:
1488
logger.warning("Client %s timed out while"
1489
" waiting for approval",
1491
if self.server.use_dbus:
1493
client.Rejected("Approval timed out")
1498
delay -= time2 - time
1501
while sent_size < len(client.secret):
1503
sent = session.send(client.secret[sent_size:])
1504
except gnutls.errors.GNUTLSError as error:
1505
logger.warning("gnutls send failed")
1507
logger.debug("Sent: %d, remaining: %d",
1508
sent, len(client.secret)
1509
- (sent_size + sent))
1512
logger.info("Sending secret to %s", client.name)
1513
# bump the timeout using extended_timeout
1514
client.checked_ok(client.extended_timeout)
1515
if self.server.use_dbus:
1520
if approval_required:
1521
client.approvals_pending -= 1
1524
except gnutls.errors.GNUTLSError as error:
1525
logger.warning("GnuTLS bye failed")
1528
def peer_certificate(session):
1529
"Return the peer's OpenPGP certificate as a bytestring"
1530
# If not an OpenPGP certificate...
1531
if (gnutls.library.functions
1532
.gnutls_certificate_type_get(session._c_object)
1533
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1534
# ...do the normal thing
1535
return session.peer_certificate
1536
list_size = ctypes.c_uint(1)
1537
cert_list = (gnutls.library.functions
1538
.gnutls_certificate_get_peers
1539
(session._c_object, ctypes.byref(list_size)))
1540
if not bool(cert_list) and list_size.value != 0:
1541
raise gnutls.errors.GNUTLSError("error getting peer"
1543
if list_size.value == 0:
1546
return ctypes.string_at(cert.data, cert.size)
1549
def fingerprint(openpgp):
1550
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1551
# New GnuTLS "datum" with the OpenPGP public key
1552
datum = (gnutls.library.types
1553
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1556
ctypes.c_uint(len(openpgp))))
1557
# New empty GnuTLS certificate
1558
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1559
(gnutls.library.functions
1560
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1561
# Import the OpenPGP public key into the certificate
1562
(gnutls.library.functions
1563
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1564
gnutls.library.constants
1565
.GNUTLS_OPENPGP_FMT_RAW))
1566
# Verify the self signature in the key
1567
crtverify = ctypes.c_uint()
1568
(gnutls.library.functions
1569
.gnutls_openpgp_crt_verify_self(crt, 0,
1570
ctypes.byref(crtverify)))
1571
if crtverify.value != 0:
1572
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1573
raise (gnutls.errors.CertificateSecurityError
1575
# New buffer for the fingerprint
1576
buf = ctypes.create_string_buffer(20)
1577
buf_len = ctypes.c_size_t()
1578
# Get the fingerprint from the certificate into the buffer
1579
(gnutls.library.functions
1580
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1581
ctypes.byref(buf_len)))
1582
# Deinit the certificate
1583
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1584
# Convert the buffer to a Python bytestring
1585
fpr = ctypes.string_at(buf, buf_len.value)
1586
# Convert the bytestring to hexadecimal notation
1587
hex_fpr = ''.join("%02X" % ord(char) for char in fpr)
1591
class MultiprocessingMixIn(object):
1592
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
1593
def sub_process_main(self, request, address):
1595
self.finish_request(request, address)
1597
self.handle_error(request, address)
1598
self.close_request(request)
1600
def process_request(self, request, address):
1601
"""Start a new process to process the request."""
1602
proc = multiprocessing.Process(target = self.sub_process_main,
1609
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
1610
""" adds a pipe to the MixIn """
1611
def process_request(self, request, client_address):
1612
"""Overrides and wraps the original process_request().
1614
This function creates a new pipe in self.pipe
1616
parent_pipe, self.child_pipe = multiprocessing.Pipe()
1618
proc = MultiprocessingMixIn.process_request(self, request,
1620
self.child_pipe.close()
1621
self.add_pipe(parent_pipe, proc)
1623
def add_pipe(self, parent_pipe, proc):
1624
"""Dummy function; override as necessary"""
1625
raise NotImplementedError
1628
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1629
socketserver.TCPServer, object):
1630
"""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.
1633
enabled: Boolean; whether this server is activated yet
1634
interface: None or a network interface name (string)
1635
use_ipv6: Boolean; to use IPv6 or not
368
options: Command line options
369
clients: Set() of Client objects
1637
def __init__(self, server_address, RequestHandlerClass,
1638
interface=None, use_ipv6=True):
1639
self.interface = interface
1641
self.address_family = socket.AF_INET6
1642
socketserver.TCPServer.__init__(self, server_address,
1643
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)
1644
380
def server_bind(self):
1645
381
"""This overrides the normal server_bind() function
1646
382
to bind to an interface if one was specified, and also NOT to
1647
383
bind to an address or port if they were not specified."""
1648
if self.interface is not None:
1649
if SO_BINDTODEVICE is None:
1650
logger.error("SO_BINDTODEVICE does not exist;"
1651
" cannot bind to interface %s",
1655
self.socket.setsockopt(socket.SOL_SOCKET,
1659
except socket.error as error:
1660
if error[0] == errno.EPERM:
1661
logger.error("No permission to"
1662
" bind to interface %s",
1664
elif error[0] == errno.ENOPROTOOPT:
1665
logger.error("SO_BINDTODEVICE not available;"
1666
" 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)
1670
399
# Only bind(2) the socket if we really need to.
1671
400
if self.server_address[0] or self.server_address[1]:
1672
401
if not self.server_address[0]:
1673
if self.address_family == socket.AF_INET6:
1674
any_address = "::" # in6addr_any
1676
any_address = socket.INADDR_ANY
1677
self.server_address = (any_address,
403
self.server_address = (in6addr_any,
1678
404
self.server_address[1])
1679
elif not self.server_address[1]:
405
elif self.server_address[1] is None:
1680
406
self.server_address = (self.server_address[0],
1682
# if self.interface:
1683
# self.server_address = (self.server_address[0],
1688
return socketserver.TCPServer.server_bind(self)
1691
class MandosServer(IPv6_TCPServer):
1695
clients: set of Client objects
1696
gnutls_priority GnuTLS priority string
1697
use_dbus: Boolean; to emit D-Bus signals or not
1699
Assumes a gobject.MainLoop event loop.
1701
def __init__(self, server_address, RequestHandlerClass,
1702
interface=None, use_ipv6=True, clients=None,
1703
gnutls_priority=None, use_dbus=True):
1704
self.enabled = False
1705
self.clients = clients
1706
if self.clients is None:
1708
self.use_dbus = use_dbus
1709
self.gnutls_priority = gnutls_priority
1710
IPv6_TCPServer.__init__(self, server_address,
1711
RequestHandlerClass,
1712
interface = interface,
1713
use_ipv6 = use_ipv6)
1714
def server_activate(self):
1716
return socketserver.TCPServer.server_activate(self)
1721
def add_pipe(self, parent_pipe, proc):
1722
# Call "handle_ipc" for both data and EOF events
1723
gobject.io_add_watch(parent_pipe.fileno(),
1724
gobject.IO_IN | gobject.IO_HUP,
1725
functools.partial(self.handle_ipc,
1730
def handle_ipc(self, source, condition, parent_pipe=None,
1731
proc = None, client_object=None):
1733
gobject.IO_IN: "IN", # There is data to read.
1734
gobject.IO_OUT: "OUT", # Data can be written (without
1736
gobject.IO_PRI: "PRI", # There is urgent data to read.
1737
gobject.IO_ERR: "ERR", # Error condition.
1738
gobject.IO_HUP: "HUP" # Hung up (the connection has been
1739
# broken, usually for pipes and
1742
conditions_string = ' | '.join(name
1744
condition_names.iteritems()
1745
if cond & condition)
1746
# error, or the other end of multiprocessing.Pipe has closed
1747
if condition & (gobject.IO_ERR | condition & gobject.IO_HUP):
1748
# Wait for other process to exit
1752
# Read a request from the child
1753
request = parent_pipe.recv()
1754
command = request[0]
1756
if command == 'init':
1758
address = request[2]
1760
for c in self.clients.itervalues():
1761
if c.fingerprint == fpr:
1765
logger.info("Client not found for fingerprint: %s, ad"
1766
"dress: %s", fpr, address)
1769
mandos_dbus_service.ClientNotFound(fpr,
1771
parent_pipe.send(False)
1774
gobject.io_add_watch(parent_pipe.fileno(),
1775
gobject.IO_IN | gobject.IO_HUP,
1776
functools.partial(self.handle_ipc,
1782
parent_pipe.send(True)
1783
# remove the old hook in favor of the new above hook on
1786
if command == 'funcall':
1787
funcname = request[1]
1791
parent_pipe.send(('data', getattr(client_object,
1795
if command == 'getattr':
1796
attrname = request[1]
1797
if callable(client_object.__getattribute__(attrname)):
1798
parent_pipe.send(('function',))
1800
parent_pipe.send(('data', client_object
1801
.__getattribute__(attrname)))
1803
if command == 'setattr':
1804
attrname = request[1]
1806
setattr(client_object, attrname, value)
408
return super(type(self), self).server_bind()
1811
411
def string_to_delta(interval):
1812
412
"""Parse a string and return a datetime.timedelta
1814
414
>>> string_to_delta('7d')
1815
415
datetime.timedelta(7)
1816
416
>>> string_to_delta('60s')
1819
419
datetime.timedelta(0, 3600)
1820
420
>>> string_to_delta('24h')
1821
421
datetime.timedelta(1)
1822
>>> string_to_delta('1w')
422
>>> string_to_delta(u'1w')
1823
423
datetime.timedelta(7)
1824
>>> string_to_delta('5m 30s')
1825
datetime.timedelta(0, 330)
1827
timevalue = datetime.timedelta(0)
1828
for s in interval.split():
1830
suffix = unicode(s[-1])
1833
delta = datetime.timedelta(value)
1835
delta = datetime.timedelta(0, value)
1837
delta = datetime.timedelta(0, 0, 0, 0, value)
1839
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
1841
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1843
raise ValueError("Unknown suffix %r" % suffix)
1844
except (ValueError, IndexError) as e:
1845
raise ValueError(*(e.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",
1850
517
def if_nametoindex(interface):
1851
"""Call the C function if_nametoindex(), or equivalent
1853
Note: This function cannot accept a unicode string."""
1854
global if_nametoindex
518
"""Call the C function if_nametoindex()"""
1856
if_nametoindex = (ctypes.cdll.LoadLibrary
1857
(ctypes.util.find_library("c"))
520
libc = ctypes.cdll.LoadLibrary("libc.so.6")
521
return libc.if_nametoindex(interface)
1859
522
except (OSError, AttributeError):
1860
logger.warning("Doing if_nametoindex the hard way")
1861
def if_nametoindex(interface):
1862
"Get an interface index the hard way, i.e. using fcntl()"
1863
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
1864
with contextlib.closing(socket.socket()) as s:
1865
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1866
struct.pack(str("16s16x"),
1868
interface_index = struct.unpack(str("I"),
1870
return interface_index
1871
return if_nametoindex(interface)
1874
def daemon(nochdir = False, noclose = False):
1875
"""See daemon(3). Standard BSD Unix function.
1877
This should really exist as os.daemon, but it doesn't (yet)."""
1886
# Close all standard open file descriptors
1887
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1888
if not stat.S_ISCHR(os.fstat(null).st_mode):
1889
raise OSError(errno.ENODEV,
1890
"%s not a character device"
1892
os.dup2(null, sys.stdin.fileno())
1893
os.dup2(null, sys.stdout.fileno())
1894
os.dup2(null, sys.stderr.fileno())
1901
##################################################################
1902
# Parsing of options, both command line and config file
1904
parser = argparse.ArgumentParser()
1905
parser.add_argument("-v", "--version", action="version",
1906
version = "%%(prog)s %s" % version,
1907
help="show version number and exit")
1908
parser.add_argument("-i", "--interface", metavar="IF",
1909
help="Bind to interface IF")
1910
parser.add_argument("-a", "--address",
1911
help="Address to listen for requests on")
1912
parser.add_argument("-p", "--port", type=int,
1913
help="Port number to receive requests on")
1914
parser.add_argument("--check", action="store_true",
1915
help="Run self-test")
1916
parser.add_argument("--debug", action="store_true",
1917
help="Debug mode; run in foreground and log"
1919
parser.add_argument("--debuglevel", metavar="LEVEL",
1920
help="Debug level for stdout output")
1921
parser.add_argument("--priority", help="GnuTLS"
1922
" priority string (see GnuTLS documentation)")
1923
parser.add_argument("--servicename",
1924
metavar="NAME", help="Zeroconf service name")
1925
parser.add_argument("--configdir",
1926
default="/etc/mandos", metavar="DIR",
1927
help="Directory to search for configuration"
1929
parser.add_argument("--no-dbus", action="store_false",
1930
dest="use_dbus", help="Do not provide D-Bus"
1931
" system bus interface")
1932
parser.add_argument("--no-ipv6", action="store_false",
1933
dest="use_ipv6", help="Do not use IPv6")
1934
parser.add_argument("--no-restore", action="store_false",
1936
help="Do not restore stored state",
1939
options = parser.parse_args()
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()
1941
567
if options.check:
1943
569
doctest.testmod()
1946
# Default values for config file for server-global settings
1947
server_defaults = { "interface": "",
1952
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1953
"servicename": "Mandos",
1959
# Parse config file for server-global settings
1960
server_config = configparser.SafeConfigParser(server_defaults)
1962
server_config.read(os.path.join(options.configdir,
1964
# Convert the SafeConfigParser object to a dict
1965
server_settings = server_config.defaults()
1966
# Use the appropriate methods on the non-string config options
1967
for option in ("debug", "use_dbus", "use_ipv6"):
1968
server_settings[option] = server_config.getboolean("DEFAULT",
1970
if server_settings["port"]:
1971
server_settings["port"] = server_config.getint("DEFAULT",
1975
# Override the settings from the config file with command line
1977
for option in ("interface", "address", "port", "debug",
1978
"priority", "servicename", "configdir",
1979
"use_dbus", "use_ipv6", "debuglevel", "restore"):
1980
value = getattr(options, option)
1981
if value is not None:
1982
server_settings[option] = value
1984
# Force all strings to be unicode
1985
for option in server_settings.keys():
1986
if type(server_settings[option]) is str:
1987
server_settings[option] = unicode(server_settings[option])
1988
# Now we have our good server settings in "server_settings"
1990
##################################################################
1993
debug = server_settings["debug"]
1994
debuglevel = server_settings["debuglevel"]
1995
use_dbus = server_settings["use_dbus"]
1996
use_ipv6 = server_settings["use_ipv6"]
1998
if server_settings["servicename"] != "Mandos":
1999
syslogger.setFormatter(logging.Formatter
2000
('Mandos (%s) [%%(process)d]:'
2001
' %%(levelname)s: %%(message)s'
2002
% server_settings["servicename"]))
2004
# Parse config file with clients
2005
client_defaults = { "timeout": "5m",
2006
"extended_timeout": "15m",
2008
"checker": "fping -q -- %%(host)s",
2010
"approval_delay": "0s",
2011
"approval_duration": "1s",
2013
client_config = configparser.SafeConfigParser(client_defaults)
2014
client_config.read(os.path.join(server_settings["configdir"],
2017
global mandos_dbus_service
2018
mandos_dbus_service = None
2020
tcp_server = MandosServer((server_settings["address"],
2021
server_settings["port"]),
2023
interface=(server_settings["interface"]
2027
server_settings["priority"],
2030
pidfilename = "/var/run/mandos.pid"
2032
pidfile = open(pidfilename, "w")
2034
logger.error("Could not open file %r", pidfilename)
2037
uid = pwd.getpwnam("_mandos").pw_uid
2038
gid = pwd.getpwnam("_mandos").pw_gid
2041
uid = pwd.getpwnam("mandos").pw_uid
2042
gid = pwd.getpwnam("mandos").pw_gid
2045
uid = pwd.getpwnam("nobody").pw_uid
2046
gid = pwd.getpwnam("nobody").pw_gid
2053
except OSError as error:
2054
if error[0] != errno.EPERM:
2057
if not debug and not debuglevel:
2058
logger.setLevel(logging.WARNING)
2060
level = getattr(logging, debuglevel.upper())
2061
logger.setLevel(level)
2064
logger.setLevel(logging.DEBUG)
2065
# Enable all possible GnuTLS debugging
2067
# "Use a log level over 10 to enable all debugging options."
2069
gnutls.library.functions.gnutls_global_set_log_level(11)
2071
@gnutls.library.types.gnutls_log_func
2072
def debug_gnutls(level, string):
2073
logger.debug("GnuTLS: %s", string[:-1])
2075
(gnutls.library.functions
2076
.gnutls_global_set_log_function(debug_gnutls))
2078
# Redirect stdin so all checkers get /dev/null
2079
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
2080
os.dup2(null, sys.stdin.fileno())
2084
# No console logging
2085
logger.removeHandler(console)
2087
# Need to fork before connecting to D-Bus
2089
# Close all input and output, do double fork, etc.
2093
# 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
2094
589
DBusGMainLoop(set_as_default=True )
2095
590
main_loop = gobject.MainLoop()
2096
591
bus = dbus.SystemBus()
2097
# End of Avahi example code
2100
bus_name = dbus.service.BusName("se.recompile.Mandos",
2101
bus, do_not_queue=True)
2102
old_bus_name = (dbus.service.BusName
2103
("se.bsnet.fukt.Mandos", bus,
2105
except dbus.exceptions.NameExistsException as e:
2106
logger.error(unicode(e) + ", disabling D-Bus")
2108
server_settings["use_dbus"] = False
2109
tcp_server.use_dbus = False
2110
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2111
service = AvahiServiceToSyslog(name =
2112
server_settings["servicename"],
2113
servicetype = "_mandos._tcp",
2114
protocol = protocol, bus = bus)
2115
if server_settings["interface"]:
2116
service.interface = (if_nametoindex
2117
(str(server_settings["interface"])))
2119
global multiprocessing_manager
2120
multiprocessing_manager = multiprocessing.Manager()
2122
client_class = Client
2124
client_class = functools.partial(ClientDBusTransitional,
2127
special_settings = {
2128
# Some settings need to be accessd by special methods;
2129
# booleans need .getboolean(), etc. Here is a list of them:
2130
"approved_by_default":
2132
client_config.getboolean(section, "approved_by_default"),
2134
# Construct a new dict of client settings of this form:
2135
# { client_name: {setting_name: value, ...}, ...}
2136
# with exceptions for any special settings as defined above
2137
client_settings = dict((clientname,
2140
setting not in special_settings
2141
else special_settings[setting]
2144
in client_config.items(clientname)))
2145
for clientname in client_config.sections())
2147
old_client_settings = {}
2150
# Get client data and settings from last running state.
2151
if server_settings["restore"]:
2153
with open(stored_state_path, "rb") as stored_state:
2154
clients_data, old_client_settings = (
2155
pickle.load(stored_state))
2156
os.remove(stored_state_path)
2157
except IOError as e:
2158
logger.warning("Could not load persistant state: {0}"
2160
if e.errno != errno.ENOENT:
2163
for client in clients_data:
2164
client_name = client["name"]
2166
# Decide which value to use after restoring saved state.
2167
# We have three different values: Old config file,
2168
# new config file, and saved state.
2169
# New config value takes precedence if it differs from old
2170
# config value, otherwise use saved state.
2171
for name, value in client_settings[client_name].items():
2173
# For each value in new config, check if it differs
2174
# from the old config value (Except for the "secret"
2176
if (name != "secret" and
2177
value != old_client_settings[client_name][name]):
2178
setattr(client, name, value)
2182
# Clients who has passed its expire date, can still be enabled
2183
# if its last checker was sucessful. Clients who checkers
2184
# failed before we stored it state is asumed to had failed
2185
# checker during downtime.
2186
if client["enabled"] and client["last_checked_ok"]:
2187
if ((datetime.datetime.utcnow()
2188
- client["last_checked_ok"]) > client["interval"]):
2189
if client["last_checker_status"] != 0:
2190
client["enabled"] = False
2192
client["expires"] = (datetime.datetime.utcnow()
2193
+ client["timeout"])
2195
client["changedstate"] = (multiprocessing_manager
2196
.Condition(multiprocessing_manager
2199
new_client = ClientDBusTransitional.__new__(
2200
ClientDBusTransitional)
2201
tcp_server.clients[client_name] = new_client
2202
new_client.bus = bus
2203
for name, value in client.iteritems():
2204
setattr(new_client, name, value)
2205
new_client._approvals_pending = 0
2206
new_client.add_to_dbus()
2208
tcp_server.clients[client_name] = Client.__new__(Client)
2209
for name, value in client.iteritems():
2210
setattr(tcp_server.clients[client_name], name, value)
2212
tcp_server.clients[client_name].decrypt_secret(
2213
client_settings[client_name]["secret"])
2215
# Create/remove clients based on new changes made to config
2216
for clientname in set(old_client_settings) - set(client_settings):
2217
del tcp_server.clients[clientname]
2218
for clientname in set(client_settings) - set(old_client_settings):
2219
tcp_server.clients[clientname] = client_class(name
2225
if not tcp_server.clients:
2226
logger.warning("No clients defined")
2232
pidfile.write(str(pid) + "\n".encode("utf-8"))
2235
logger.error("Could not write to file %r with PID %d",
2238
# "pidfile" was never created
2242
signal.signal(signal.SIGINT, signal.SIG_IGN)
2244
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2245
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2248
class MandosDBusService(dbus.service.Object):
2249
"""A D-Bus proxy object"""
2251
dbus.service.Object.__init__(self, bus, "/")
2252
_interface = "se.recompile.Mandos"
2254
@dbus.service.signal(_interface, signature="o")
2255
def ClientAdded(self, objpath):
2259
@dbus.service.signal(_interface, signature="ss")
2260
def ClientNotFound(self, fingerprint, address):
2264
@dbus.service.signal(_interface, signature="os")
2265
def ClientRemoved(self, objpath, name):
2269
@dbus.service.method(_interface, out_signature="ao")
2270
def GetAllClients(self):
2272
return dbus.Array(c.dbus_object_path
2274
tcp_server.clients.itervalues())
2276
@dbus.service.method(_interface,
2277
out_signature="a{oa{sv}}")
2278
def GetAllClientsWithProperties(self):
2280
return dbus.Dictionary(
2281
((c.dbus_object_path, c.GetAll(""))
2282
for c in tcp_server.clients.itervalues()),
2285
@dbus.service.method(_interface, in_signature="o")
2286
def RemoveClient(self, object_path):
2288
for c in tcp_server.clients.itervalues():
2289
if c.dbus_object_path == object_path:
2290
del tcp_server.clients[c.name]
2291
c.remove_from_connection()
2292
# Don't signal anything except ClientRemoved
2293
c.disable(quiet=True)
2295
self.ClientRemoved(object_path, c.name)
2297
raise KeyError(object_path)
2301
class MandosDBusServiceTransitional(MandosDBusService):
2302
__metaclass__ = AlternateDBusNamesMetaclass
2303
mandos_dbus_service = MandosDBusServiceTransitional()
2306
"Cleanup function; run on exit"
2309
multiprocessing.active_children()
2310
if not (tcp_server.clients or client_settings):
2313
# Store client before exiting. Secrets are encrypted with key
2314
# based on what config file has. If config file is
2315
# removed/edited, old secret will thus be unrecovable.
2317
for client in tcp_server.clients.itervalues():
2318
client.encrypt_secret(
2319
client_settings[client.name]["secret"])
2323
# A list of attributes that will not be stored when
2325
exclude = set(("bus", "changedstate", "secret"))
2326
for name, typ in inspect.getmembers(dbus.service.Object):
2329
client_dict["encrypted_secret"] = client.encrypted_secret
2330
for attr in client.client_structure:
2331
if attr not in exclude:
2332
client_dict[attr] = getattr(client, attr)
2334
clients.append(client_dict)
2335
del client_settings[client.name]["secret"]
2338
with os.fdopen(os.open(stored_state_path,
2339
os.O_CREAT|os.O_WRONLY|os.O_TRUNC,
2340
stat.S_IRUSR | stat.S_IWUSR),
2341
"wb") as stored_state:
2342
pickle.dump((clients, client_settings), stored_state)
2343
except IOError as e:
2344
logger.warning("Could not save persistant state: {0}"
2346
if e.errno != errno.ENOENT:
2349
# Delete all clients, and settings from config
2350
while tcp_server.clients:
2351
name, client = tcp_server.clients.popitem()
2353
client.remove_from_connection()
2354
# Don't signal anything except ClientRemoved
2355
client.disable(quiet=True)
2358
mandos_dbus_service.ClientRemoved(client
2361
client_settings.clear()
2363
atexit.register(cleanup)
2365
for client in tcp_server.clients.itervalues():
2368
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2369
# Need to initiate checking of clients
2371
client.init_checker()
2375
tcp_server.server_activate()
2377
# Find out what port we got
2378
service.port = tcp_server.socket.getsockname()[1]
2380
logger.info("Now listening on address %r, port %d,"
2381
" flowinfo %d, scope_id %d"
2382
% tcp_server.socket.getsockname())
2384
logger.info("Now listening on address %r, port %d"
2385
% tcp_server.socket.getsockname())
2387
#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:],
2390
# From the Avahi example code
2393
except dbus.exceptions.DBusException as error:
2394
logger.critical("DBusException: %s", error)
2397
# End of Avahi example code
2399
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2400
lambda *args, **kwargs:
2401
(tcp_server.handle_request
2402
(*args[2:], **kwargs) or True))
2404
logger.debug("Starting main loop")
2406
except AvahiError as error:
2407
logger.critical("AvahiError: %s", error)
2410
636
except KeyboardInterrupt:
2412
print("", file=sys.stderr)
2413
logger.debug("Server received KeyboardInterrupt")
2414
logger.debug("Server exiting")
2415
# Must run before the D-Bus bus name gets deregistered
2419
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