44
44
import gnutls.library.functions
45
45
import gnutls.library.constants
46
46
import gnutls.library.types
47
import ConfigParser as configparser
56
57
import logging.handlers
62
import cPickle as pickle
68
62
from dbus.mainloop.glib import DBusGMainLoop
71
import xml.dom.minidom
75
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
76
except AttributeError:
78
from IN import SO_BINDTODEVICE
80
SO_BINDTODEVICE = None
85
logger = logging.Logger(u'mandos')
86
syslogger = (logging.handlers.SysLogHandler
87
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
88
address = "/dev/log"))
89
syslogger.setFormatter(logging.Formatter
90
(u'Mandos [%(process)d]: %(levelname)s:'
65
# Brief description of the operation of this program:
67
# This server announces itself as a Zeroconf service. Connecting
68
# clients use the TLS protocol, with the unusual quirk that this
69
# server program acts as a TLS "client" while the connecting clients
70
# acts as a TLS "server". The clients (acting as a TLS "server") must
71
# supply an OpenPGP certificate, and the fingerprint of this
72
# certificate is used by this server to look up (in a list read from a
73
# file at start time) which binary blob to give the client. No other
74
# authentication or authorization is done by this server.
77
logger = logging.Logger('mandos')
78
syslogger = logging.handlers.SysLogHandler\
79
(facility = logging.handlers.SysLogHandler.LOG_DAEMON)
80
syslogger.setFormatter(logging.Formatter\
81
('%(levelname)s: %(message)s'))
92
82
logger.addHandler(syslogger)
94
console = logging.StreamHandler()
95
console.setFormatter(logging.Formatter(u'%(name)s [%(process)d]:'
98
logger.addHandler(console)
100
class AvahiError(Exception):
101
def __init__(self, value, *args, **kwargs):
103
super(AvahiError, self).__init__(value, *args, **kwargs)
104
def __unicode__(self):
105
return unicode(repr(self.value))
107
class AvahiServiceError(AvahiError):
110
class AvahiGroupError(AvahiError):
114
class AvahiService(object):
115
"""An Avahi (Zeroconf) service.
118
interface: integer; avahi.IF_UNSPEC or an interface index.
119
Used to optionally bind to the specified interface.
120
name: string; Example: u'Mandos'
121
type: string; Example: u'_mandos._tcp'.
122
See <http://www.dns-sd.org/ServiceTypes.html>
123
port: integer; what port to announce
124
TXT: list of strings; TXT record for the service
125
domain: string; Domain to publish on, default to .local if empty.
126
host: string; Host to publish records for, default is localhost
127
max_renames: integer; maximum number of renames
128
rename_count: integer; counter so we only rename after collisions
129
a sensible number of times
130
group: D-Bus Entry Group
132
bus: dbus.SystemBus()
134
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
135
servicetype = None, port = None, TXT = None,
136
domain = u"", host = u"", max_renames = 32768,
137
protocol = avahi.PROTO_UNSPEC, bus = None):
138
self.interface = interface
140
self.type = servicetype
142
self.TXT = TXT if TXT is not None else []
145
self.rename_count = 0
146
self.max_renames = max_renames
147
self.protocol = protocol
148
self.group = None # our entry group
152
"""Derived from the Avahi example code"""
153
if self.rename_count >= self.max_renames:
154
logger.critical(u"No suitable Zeroconf service name found"
155
u" after %i retries, exiting.",
157
raise AvahiServiceError(u"Too many renames")
158
self.name = self.server.GetAlternativeServiceName(self.name)
159
logger.info(u"Changing Zeroconf service name to %r ...",
161
syslogger.setFormatter(logging.Formatter
162
(u'Mandos (%s) [%%(process)d]:'
163
u' %%(levelname)s: %%(message)s'
167
self.rename_count += 1
169
"""Derived from the Avahi example code"""
170
if self.group is not None:
173
"""Derived from the Avahi example code"""
174
if self.group is None:
175
self.group = dbus.Interface(
176
self.bus.get_object(avahi.DBUS_NAME,
177
self.server.EntryGroupNew()),
178
avahi.DBUS_INTERFACE_ENTRY_GROUP)
179
self.group.connect_to_signal('StateChanged',
181
.entry_group_state_changed)
182
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
183
self.name, self.type)
184
self.group.AddService(
187
dbus.UInt32(0), # flags
188
self.name, self.type,
189
self.domain, self.host,
190
dbus.UInt16(self.port),
191
avahi.string_array_to_txt_array(self.TXT))
193
def entry_group_state_changed(self, state, error):
194
"""Derived from the Avahi example code"""
195
logger.debug(u"Avahi state change: %i", state)
197
if state == avahi.ENTRY_GROUP_ESTABLISHED:
198
logger.debug(u"Zeroconf service established.")
199
elif state == avahi.ENTRY_GROUP_COLLISION:
200
logger.warning(u"Zeroconf service name collision.")
202
elif state == avahi.ENTRY_GROUP_FAILURE:
203
logger.critical(u"Avahi: Error in group state changed %s",
205
raise AvahiGroupError(u"State changed: %s"
208
"""Derived from the Avahi example code"""
209
if self.group is not None:
212
def server_state_changed(self, state):
213
"""Derived from the Avahi example code"""
214
if state == avahi.SERVER_COLLISION:
215
logger.error(u"Zeroconf server name collision")
217
elif state == avahi.SERVER_RUNNING:
220
"""Derived from the Avahi example code"""
221
if self.server is None:
222
self.server = dbus.Interface(
223
self.bus.get_object(avahi.DBUS_NAME,
224
avahi.DBUS_PATH_SERVER),
225
avahi.DBUS_INTERFACE_SERVER)
226
self.server.connect_to_signal(u"StateChanged",
227
self.server_state_changed)
228
self.server_state_changed(self.server.GetState())
85
# This variable is used to optionally bind to a specified interface.
86
# It is a global variable to fit in with the other variables from the
88
serviceInterface = avahi.IF_UNSPEC
89
# From the Avahi example code:
90
serviceName = "Mandos"
91
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
92
servicePort = None # Not known at startup
93
serviceTXT = [] # TXT record for the service
94
domain = "" # Domain to publish on, default to .local
95
host = "" # Host to publish records for, default to localhost
96
group = None #our entry group
97
rename_count = 12 # Counter so we only rename after collisions a
98
# sensible number of times
99
# End of Avahi example code
231
102
class Client(object):
232
103
"""A representation of a client host served by this server.
235
name: string; from the config file, used in log messages and
105
name: string; from the config file, used in log messages
237
106
fingerprint: string (40 or 32 hexadecimal digits); used to
238
107
uniquely identify the client
239
secret: bytestring; sent verbatim (over TLS) to client
240
host: string; available for use by the checker command
241
created: datetime.datetime(); (UTC) object creation
242
last_enabled: datetime.datetime(); (UTC)
244
last_checked_ok: datetime.datetime(); (UTC) or None
245
timeout: datetime.timedelta(); How long from last_checked_ok
246
until this client is disabled
247
interval: datetime.timedelta(); How often to start a new checker
248
disable_hook: If set, called by disable() as disable_hook(self)
249
checker: subprocess.Popen(); a running checker process used
250
to see if the client lives.
251
'None' if no process is running.
108
secret: bytestring; sent verbatim (over TLS) to client
109
fqdn: string (FQDN); available for use by the checker command
110
created: datetime.datetime()
111
last_seen: datetime.datetime() or None if not yet seen
112
timeout: datetime.timedelta(); How long from last_seen until
113
this client is invalid
114
interval: datetime.timedelta(); How often to start a new checker
115
stop_hook: If set, called by stop() as stop_hook(self)
116
checker: subprocess.Popen(); a running checker process used
117
to see if the client lives.
118
Is None if no process is running.
252
119
checker_initiator_tag: a gobject event source tag, or None
253
disable_initiator_tag: - '' -
120
stop_initiator_tag: - '' -
254
121
checker_callback_tag: - '' -
255
122
checker_command: string; External command which is run to check if
256
client lives. %() expansions are done at
123
client lives. %()s expansions are done at
257
124
runtime with vars(self) as dict, so that for
258
125
instance %(name)s can be used in the command.
259
current_checker_command: string; current running checker_command
127
_timeout: Real variable for 'timeout'
128
_interval: Real variable for 'interval'
129
_timeout_milliseconds: Used by gobject.timeout_add()
130
_interval_milliseconds: - '' -
263
def _timedelta_to_milliseconds(td):
264
"Convert a datetime.timedelta() to milliseconds"
265
return ((td.days * 24 * 60 * 60 * 1000)
266
+ (td.seconds * 1000)
267
+ (td.microseconds // 1000))
269
def timeout_milliseconds(self):
270
"Return the 'timeout' attribute in milliseconds"
271
return self._timedelta_to_milliseconds(self.timeout)
273
def interval_milliseconds(self):
274
"Return the 'interval' attribute in milliseconds"
275
return self._timedelta_to_milliseconds(self.interval)
277
def __init__(self, name = None, disable_hook=None, config=None):
278
"""Note: the 'checker' key in 'config' sets the
279
'checker_command' attribute and *not* the 'checker'
132
def _set_timeout(self, timeout):
133
"Setter function for 'timeout' attribute"
134
self._timeout = timeout
135
self._timeout_milliseconds = ((self.timeout.days
136
* 24 * 60 * 60 * 1000)
137
+ (self.timeout.seconds * 1000)
138
+ (self.timeout.microseconds
140
timeout = property(lambda self: self._timeout,
143
def _set_interval(self, interval):
144
"Setter function for 'interval' attribute"
145
self._interval = interval
146
self._interval_milliseconds = ((self.interval.days
147
* 24 * 60 * 60 * 1000)
148
+ (self.interval.seconds
150
+ (self.interval.microseconds
152
interval = property(lambda self: self._interval,
155
def __init__(self, name=None, options=None, stop_hook=None,
156
fingerprint=None, secret=None, secfile=None,
157
fqdn=None, timeout=None, interval=-1, checker=None):
158
"""Note: the 'checker' argument sets the 'checker_command'
159
attribute and not the 'checker' attribute.."""
284
logger.debug(u"Creating client %r", self.name)
285
# Uppercase and remove spaces from fingerprint for later
286
# comparison purposes with return value from the fingerprint()
288
self.fingerprint = (config[u"fingerprint"].upper()
290
logger.debug(u" Fingerprint: %s", self.fingerprint)
291
if u"secret" in config:
292
self.secret = config[u"secret"].decode(u"base64")
293
elif u"secfile" in config:
294
with open(os.path.expanduser(os.path.expandvars
295
(config[u"secfile"])),
297
self.secret = secfile.read()
299
raise TypeError(u"No secret or secfile for client %s"
301
self.host = config.get(u"host", u"")
302
self.created = datetime.datetime.utcnow()
304
self.last_enabled = None
305
self.last_checked_ok = None
306
self.timeout = string_to_delta(config[u"timeout"])
307
self.interval = string_to_delta(config[u"interval"])
308
self.disable_hook = disable_hook
161
# Uppercase and remove spaces from fingerprint
162
# for later comparison purposes with return value of
163
# the fingerprint() function
164
self.fingerprint = fingerprint.upper().replace(u" ", u"")
166
self.secret = secret.decode(u"base64")
169
self.secret = sf.read()
172
raise RuntimeError(u"No secret or secfile for client %s"
174
self.fqdn = fqdn # string
175
self.created = datetime.datetime.now()
176
self.last_seen = None
178
self.timeout = options.timeout
180
self.timeout = string_to_delta(timeout)
182
self.interval = options.interval
184
self.interval = string_to_delta(interval)
185
self.stop_hook = stop_hook
309
186
self.checker = None
310
187
self.checker_initiator_tag = None
311
self.disable_initiator_tag = None
188
self.stop_initiator_tag = None
312
189
self.checker_callback_tag = None
313
self.checker_command = config[u"checker"]
314
self.current_checker_command = None
315
self.last_connect = None
190
self.check_command = checker
318
192
"""Start this client's checker and timeout hooks"""
319
if getattr(self, u"enabled", False):
322
self.last_enabled = datetime.datetime.utcnow()
323
193
# Schedule a new checker to be started an 'interval' from now,
324
194
# and every interval from then on.
325
self.checker_initiator_tag = (gobject.timeout_add
326
(self.interval_milliseconds(),
328
# Schedule a disable() when 'timeout' has passed
329
self.disable_initiator_tag = (gobject.timeout_add
330
(self.timeout_milliseconds(),
195
self.checker_initiator_tag = gobject.timeout_add\
196
(self._interval_milliseconds,
333
198
# Also start a new checker *right now*.
334
199
self.start_checker()
336
def disable(self, quiet=True):
337
"""Disable this client."""
338
if not getattr(self, "enabled", False):
200
# Schedule a stop() when 'timeout' has passed
201
self.stop_initiator_tag = gobject.timeout_add\
202
(self._timeout_milliseconds,
206
The possibility that this client might be restarted is left
207
open, but not currently used."""
208
# If this client doesn't have a secret, it is already stopped.
210
logger.debug(u"Stopping client %s", self.name)
341
logger.info(u"Disabling client %s", self.name)
342
if getattr(self, u"disable_initiator_tag", False):
343
gobject.source_remove(self.disable_initiator_tag)
344
self.disable_initiator_tag = None
345
if getattr(self, u"checker_initiator_tag", False):
214
if hasattr(self, "stop_initiator_tag") \
215
and self.stop_initiator_tag:
216
gobject.source_remove(self.stop_initiator_tag)
217
self.stop_initiator_tag = None
218
if hasattr(self, "checker_initiator_tag") \
219
and self.checker_initiator_tag:
346
220
gobject.source_remove(self.checker_initiator_tag)
347
221
self.checker_initiator_tag = None
348
222
self.stop_checker()
349
if self.disable_hook:
350
self.disable_hook(self)
352
225
# Do not run this again if called by a gobject.timeout_add
355
227
def __del__(self):
356
self.disable_hook = None
359
def checker_callback(self, pid, condition, command):
228
self.stop_hook = None
230
def checker_callback(self, pid, condition):
360
231
"""The checker has completed, so take appropriate actions."""
232
now = datetime.datetime.now()
361
233
self.checker_callback_tag = None
362
234
self.checker = None
363
if os.WIFEXITED(condition):
364
exitstatus = os.WEXITSTATUS(condition)
366
logger.info(u"Checker for %(name)s succeeded",
370
logger.info(u"Checker for %(name)s failed",
235
if os.WIFEXITED(condition) \
236
and (os.WEXITSTATUS(condition) == 0):
237
logger.debug(u"Checker for %(name)s succeeded",
240
gobject.source_remove(self.stop_initiator_tag)
241
self.stop_initiator_tag = gobject.timeout_add\
242
(self._timeout_milliseconds,
244
elif not os.WIFEXITED(condition):
373
245
logger.warning(u"Checker for %(name)s crashed?",
376
def checked_ok(self):
377
"""Bump up the timeout for this client.
379
This should only be called when the client has been seen,
382
self.last_checked_ok = datetime.datetime.utcnow()
383
gobject.source_remove(self.disable_initiator_tag)
384
self.disable_initiator_tag = (gobject.timeout_add
385
(self.timeout_milliseconds(),
248
logger.debug(u"Checker for %(name)s failed",
388
250
def start_checker(self):
389
251
"""Start a new checker subprocess if one is not running.
391
252
If a checker already exists, leave it running and do
393
254
# The reason for not killing a running checker is that if we
396
257
# client would inevitably timeout, since no checker would get
397
258
# a chance to run to completion. If we instead leave running
398
259
# checkers alone, the checker would have to take more time
399
# than 'timeout' for the client to be disabled, which is as it
402
# If a checker exists, make sure it is not a zombie
404
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
405
except (AttributeError, OSError), error:
406
if (isinstance(error, OSError)
407
and error.errno != errno.ECHILD):
411
logger.warning(u"Checker was a zombie")
412
gobject.source_remove(self.checker_callback_tag)
413
self.checker_callback(pid, status,
414
self.current_checker_command)
415
# Start a new checker if needed
260
# than 'timeout' for the client to be declared invalid, which
261
# is as it should be.
416
262
if self.checker is None:
418
# In case checker_command has exactly one % operator
419
command = self.checker_command % self.host
264
command = self.check_command % self.fqdn
420
265
except TypeError:
421
# Escape attributes for the shell
422
escaped_attrs = dict((key,
423
re.escape(unicode(str(val),
266
escaped_attrs = dict((key, re.escape(str(val)))
427
268
vars(self).iteritems())
429
command = self.checker_command % escaped_attrs
270
command = self.check_command % escaped_attrs
430
271
except TypeError, error:
431
logger.error(u'Could not format string "%s":'
432
u' %s', self.checker_command, error)
272
logger.critical(u'Could not format string "%s":'
273
u' %s', self.check_command, error)
433
274
return True # Try again later
434
self.current_checker_command = command
436
logger.info(u"Starting checker %r for %s",
438
# We don't need to redirect stdout and stderr, since
439
# in normal mode, that is already done by daemon(),
440
# and in debug mode we don't want to. (Stdin is
441
# always replaced by /dev/null.)
442
self.checker = subprocess.Popen(command,
444
shell=True, cwd=u"/")
445
self.checker_callback_tag = (gobject.child_watch_add
447
self.checker_callback,
449
# The checker may have completed before the gobject
450
# watch was added. Check for this.
451
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
453
gobject.source_remove(self.checker_callback_tag)
454
self.checker_callback(pid, status, command)
455
except OSError, error:
276
logger.debug(u"Starting checker %r for %s",
278
self.checker = subprocess.\
280
close_fds=True, shell=True,
282
self.checker_callback_tag = gobject.child_watch_add\
284
self.checker_callback)
285
except subprocess.OSError, error:
456
286
logger.error(u"Failed to start subprocess: %s",
458
288
# Re-run this periodically if run by gobject.timeout_add
461
290
def stop_checker(self):
462
291
"""Force the checker process, if any, to stop."""
463
292
if self.checker_callback_tag:
464
293
gobject.source_remove(self.checker_callback_tag)
465
294
self.checker_callback_tag = None
466
if getattr(self, u"checker", None) is None:
295
if not hasattr(self, "checker") or self.checker is None:
468
logger.debug(u"Stopping checker for %(name)s", vars(self))
297
logger.debug("Stopping checker for %(name)s", vars(self))
470
299
os.kill(self.checker.pid, signal.SIGTERM)
472
301
#if self.checker.poll() is None:
473
302
# os.kill(self.checker.pid, signal.SIGKILL)
474
303
except OSError, error:
475
if error.errno != errno.ESRCH: # No such process
304
if error.errno != errno.ESRCH:
477
306
self.checker = None
480
def dbus_service_property(dbus_interface, signature=u"v",
481
access=u"readwrite", byte_arrays=False):
482
"""Decorators for marking methods of a DBusObjectWithProperties to
483
become properties on the D-Bus.
485
The decorated method will be called with no arguments by "Get"
486
and with one argument by "Set".
488
The parameters, where they are supported, are the same as
489
dbus.service.method, except there is only "signature", since the
490
type from Get() and the type sent to Set() is the same.
492
# Encoding deeply encoded byte arrays is not supported yet by the
493
# "Set" method, so we fail early here:
494
if byte_arrays and signature != u"ay":
495
raise ValueError(u"Byte arrays not supported for non-'ay'"
496
u" signature %r" % signature)
498
func._dbus_is_property = True
499
func._dbus_interface = dbus_interface
500
func._dbus_signature = signature
501
func._dbus_access = access
502
func._dbus_name = func.__name__
503
if func._dbus_name.endswith(u"_dbus_property"):
504
func._dbus_name = func._dbus_name[:-14]
505
func._dbus_get_args_options = {u'byte_arrays': byte_arrays }
510
class DBusPropertyException(dbus.exceptions.DBusException):
511
"""A base class for D-Bus property-related exceptions
513
def __unicode__(self):
514
return unicode(str(self))
517
class DBusPropertyAccessException(DBusPropertyException):
518
"""A property's access permissions disallows an operation.
523
class DBusPropertyNotFound(DBusPropertyException):
524
"""An attempt was made to access a non-existing property.
529
class DBusObjectWithProperties(dbus.service.Object):
530
"""A D-Bus object with properties.
532
Classes inheriting from this can use the dbus_service_property
533
decorator to expose methods as D-Bus properties. It exposes the
534
standard Get(), Set(), and GetAll() methods on the D-Bus.
538
def _is_dbus_property(obj):
539
return getattr(obj, u"_dbus_is_property", False)
541
def _get_all_dbus_properties(self):
542
"""Returns a generator of (name, attribute) pairs
544
return ((prop._dbus_name, prop)
546
inspect.getmembers(self, self._is_dbus_property))
548
def _get_dbus_property(self, interface_name, property_name):
549
"""Returns a bound method if one exists which is a D-Bus
550
property with the specified name and interface.
552
for name in (property_name,
553
property_name + u"_dbus_property"):
554
prop = getattr(self, name, None)
556
or not self._is_dbus_property(prop)
557
or prop._dbus_name != property_name
558
or (interface_name and prop._dbus_interface
559
and interface_name != prop._dbus_interface)):
563
raise DBusPropertyNotFound(self.dbus_object_path + u":"
564
+ interface_name + u"."
567
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ss",
569
def Get(self, interface_name, property_name):
570
"""Standard D-Bus property Get() method, see D-Bus standard.
572
prop = self._get_dbus_property(interface_name, property_name)
573
if prop._dbus_access == u"write":
574
raise DBusPropertyAccessException(property_name)
576
if not hasattr(value, u"variant_level"):
578
return type(value)(value, variant_level=value.variant_level+1)
580
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ssv")
581
def Set(self, interface_name, property_name, value):
582
"""Standard D-Bus property Set() method, see D-Bus standard.
584
prop = self._get_dbus_property(interface_name, property_name)
585
if prop._dbus_access == u"read":
586
raise DBusPropertyAccessException(property_name)
587
if prop._dbus_get_args_options[u"byte_arrays"]:
588
# The byte_arrays option is not supported yet on
589
# signatures other than "ay".
590
if prop._dbus_signature != u"ay":
592
value = dbus.ByteArray(''.join(unichr(byte)
596
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"s",
597
out_signature=u"a{sv}")
598
def GetAll(self, interface_name):
599
"""Standard D-Bus property GetAll() method, see D-Bus
602
Note: Will not include properties with access="write".
605
for name, prop in self._get_all_dbus_properties():
607
and interface_name != prop._dbus_interface):
608
# Interface non-empty but did not match
610
# Ignore write-only properties
611
if prop._dbus_access == u"write":
614
if not hasattr(value, u"variant_level"):
617
all[name] = type(value)(value, variant_level=
618
value.variant_level+1)
619
return dbus.Dictionary(all, signature=u"sv")
621
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
623
path_keyword='object_path',
624
connection_keyword='connection')
625
def Introspect(self, object_path, connection):
626
"""Standard D-Bus method, overloaded to insert property tags.
628
xmlstring = dbus.service.Object.Introspect(self, object_path,
631
document = xml.dom.minidom.parseString(xmlstring)
632
def make_tag(document, name, prop):
633
e = document.createElement(u"property")
634
e.setAttribute(u"name", name)
635
e.setAttribute(u"type", prop._dbus_signature)
636
e.setAttribute(u"access", prop._dbus_access)
638
for if_tag in document.getElementsByTagName(u"interface"):
639
for tag in (make_tag(document, name, prop)
641
in self._get_all_dbus_properties()
642
if prop._dbus_interface
643
== if_tag.getAttribute(u"name")):
644
if_tag.appendChild(tag)
645
# Add the names to the return values for the
646
# "org.freedesktop.DBus.Properties" methods
647
if (if_tag.getAttribute(u"name")
648
== u"org.freedesktop.DBus.Properties"):
649
for cn in if_tag.getElementsByTagName(u"method"):
650
if cn.getAttribute(u"name") == u"Get":
651
for arg in cn.getElementsByTagName(u"arg"):
652
if (arg.getAttribute(u"direction")
654
arg.setAttribute(u"name", u"value")
655
elif cn.getAttribute(u"name") == u"GetAll":
656
for arg in cn.getElementsByTagName(u"arg"):
657
if (arg.getAttribute(u"direction")
659
arg.setAttribute(u"name", u"props")
660
xmlstring = document.toxml(u"utf-8")
662
except (AttributeError, xml.dom.DOMException,
663
xml.parsers.expat.ExpatError), error:
664
logger.error(u"Failed to override Introspection method",
669
class ClientDBus(Client, DBusObjectWithProperties):
670
"""A Client class using D-Bus
673
dbus_object_path: dbus.ObjectPath
674
bus: dbus.SystemBus()
676
# dbus.service.Object doesn't use super(), so we can't either.
678
def __init__(self, bus = None, *args, **kwargs):
680
Client.__init__(self, *args, **kwargs)
681
# Only now, when this client is initialized, can it show up on
683
self.dbus_object_path = (dbus.ObjectPath
685
+ self.name.replace(u".", u"_")))
686
DBusObjectWithProperties.__init__(self, self.bus,
687
self.dbus_object_path)
690
def _datetime_to_dbus(dt, variant_level=0):
691
"""Convert a UTC datetime.datetime() to a D-Bus type."""
692
return dbus.String(dt.isoformat(),
693
variant_level=variant_level)
696
oldstate = getattr(self, u"enabled", False)
697
r = Client.enable(self)
698
if oldstate != self.enabled:
700
self.PropertyChanged(dbus.String(u"enabled"),
701
dbus.Boolean(True, variant_level=1))
702
self.PropertyChanged(
703
dbus.String(u"last_enabled"),
704
self._datetime_to_dbus(self.last_enabled,
708
def disable(self, quiet = False):
709
oldstate = getattr(self, u"enabled", False)
710
r = Client.disable(self, quiet=quiet)
711
if not quiet and oldstate != self.enabled:
713
self.PropertyChanged(dbus.String(u"enabled"),
714
dbus.Boolean(False, variant_level=1))
717
def __del__(self, *args, **kwargs):
719
self.remove_from_connection()
722
if hasattr(DBusObjectWithProperties, u"__del__"):
723
DBusObjectWithProperties.__del__(self, *args, **kwargs)
724
Client.__del__(self, *args, **kwargs)
726
def checker_callback(self, pid, condition, command,
728
self.checker_callback_tag = None
731
self.PropertyChanged(dbus.String(u"checker_running"),
732
dbus.Boolean(False, variant_level=1))
733
if os.WIFEXITED(condition):
734
exitstatus = os.WEXITSTATUS(condition)
736
self.CheckerCompleted(dbus.Int16(exitstatus),
737
dbus.Int64(condition),
738
dbus.String(command))
741
self.CheckerCompleted(dbus.Int16(-1),
742
dbus.Int64(condition),
743
dbus.String(command))
745
return Client.checker_callback(self, pid, condition, command,
748
def checked_ok(self, *args, **kwargs):
749
r = Client.checked_ok(self, *args, **kwargs)
751
self.PropertyChanged(
752
dbus.String(u"last_checked_ok"),
753
(self._datetime_to_dbus(self.last_checked_ok,
757
def start_checker(self, *args, **kwargs):
758
old_checker = self.checker
759
if self.checker is not None:
760
old_checker_pid = self.checker.pid
762
old_checker_pid = None
763
r = Client.start_checker(self, *args, **kwargs)
764
# Only if new checker process was started
765
if (self.checker is not None
766
and old_checker_pid != self.checker.pid):
768
self.CheckerStarted(self.current_checker_command)
769
self.PropertyChanged(
770
dbus.String(u"checker_running"),
771
dbus.Boolean(True, variant_level=1))
774
def stop_checker(self, *args, **kwargs):
775
old_checker = getattr(self, u"checker", None)
776
r = Client.stop_checker(self, *args, **kwargs)
777
if (old_checker is not None
778
and getattr(self, u"checker", None) is None):
779
self.PropertyChanged(dbus.String(u"checker_running"),
780
dbus.Boolean(False, variant_level=1))
783
## D-Bus methods, signals & properties
784
_interface = u"se.bsnet.fukt.Mandos.Client"
788
# CheckerCompleted - signal
789
@dbus.service.signal(_interface, signature=u"nxs")
790
def CheckerCompleted(self, exitcode, waitstatus, command):
794
# CheckerStarted - signal
795
@dbus.service.signal(_interface, signature=u"s")
796
def CheckerStarted(self, command):
800
# PropertyChanged - signal
801
@dbus.service.signal(_interface, signature=u"sv")
802
def PropertyChanged(self, property, value):
807
@dbus.service.signal(_interface)
813
@dbus.service.signal(_interface)
821
@dbus.service.method(_interface)
823
return self.checked_ok()
826
@dbus.service.method(_interface)
831
# StartChecker - method
832
@dbus.service.method(_interface)
833
def StartChecker(self):
838
@dbus.service.method(_interface)
843
# StopChecker - method
844
@dbus.service.method(_interface)
845
def StopChecker(self):
851
@dbus_service_property(_interface, signature=u"s", access=u"read")
852
def name_dbus_property(self):
853
return dbus.String(self.name)
855
# fingerprint - property
856
@dbus_service_property(_interface, signature=u"s", access=u"read")
857
def fingerprint_dbus_property(self):
858
return dbus.String(self.fingerprint)
861
@dbus_service_property(_interface, signature=u"s",
863
def host_dbus_property(self, value=None):
864
if value is None: # get
865
return dbus.String(self.host)
868
self.PropertyChanged(dbus.String(u"host"),
869
dbus.String(value, variant_level=1))
872
@dbus_service_property(_interface, signature=u"s", access=u"read")
873
def created_dbus_property(self):
874
return dbus.String(self._datetime_to_dbus(self.created))
876
# last_enabled - property
877
@dbus_service_property(_interface, signature=u"s", access=u"read")
878
def last_enabled_dbus_property(self):
879
if self.last_enabled is None:
880
return dbus.String(u"")
881
return dbus.String(self._datetime_to_dbus(self.last_enabled))
884
@dbus_service_property(_interface, signature=u"b",
886
def enabled_dbus_property(self, value=None):
887
if value is None: # get
888
return dbus.Boolean(self.enabled)
894
# last_checked_ok - property
895
@dbus_service_property(_interface, signature=u"s",
897
def last_checked_ok_dbus_property(self, value=None):
898
if value is not None:
901
if self.last_checked_ok is None:
902
return dbus.String(u"")
903
return dbus.String(self._datetime_to_dbus(self
907
@dbus_service_property(_interface, signature=u"t",
909
def timeout_dbus_property(self, value=None):
910
if value is None: # get
911
return dbus.UInt64(self.timeout_milliseconds())
912
self.timeout = datetime.timedelta(0, 0, 0, value)
914
self.PropertyChanged(dbus.String(u"timeout"),
915
dbus.UInt64(value, variant_level=1))
916
if getattr(self, u"disable_initiator_tag", None) is None:
919
gobject.source_remove(self.disable_initiator_tag)
920
self.disable_initiator_tag = None
922
_timedelta_to_milliseconds((self
928
# The timeout has passed
931
self.disable_initiator_tag = (gobject.timeout_add
932
(time_to_die, self.disable))
934
# interval - property
935
@dbus_service_property(_interface, signature=u"t",
937
def interval_dbus_property(self, value=None):
938
if value is None: # get
939
return dbus.UInt64(self.interval_milliseconds())
940
self.interval = datetime.timedelta(0, 0, 0, value)
942
self.PropertyChanged(dbus.String(u"interval"),
943
dbus.UInt64(value, variant_level=1))
944
if getattr(self, u"checker_initiator_tag", None) is None:
946
# Reschedule checker run
947
gobject.source_remove(self.checker_initiator_tag)
948
self.checker_initiator_tag = (gobject.timeout_add
949
(value, self.start_checker))
950
self.start_checker() # Start one now, too
953
@dbus_service_property(_interface, signature=u"s",
955
def checker_dbus_property(self, value=None):
956
if value is None: # get
957
return dbus.String(self.checker_command)
958
self.checker_command = value
960
self.PropertyChanged(dbus.String(u"checker"),
961
dbus.String(self.checker_command,
964
# checker_running - property
965
@dbus_service_property(_interface, signature=u"b",
967
def checker_running_dbus_property(self, value=None):
968
if value is None: # get
969
return dbus.Boolean(self.checker is not None)
975
# object_path - property
976
@dbus_service_property(_interface, signature=u"o", access=u"read")
977
def object_path_dbus_property(self):
978
return self.dbus_object_path # is already a dbus.ObjectPath
981
@dbus_service_property(_interface, signature=u"ay",
982
access=u"write", byte_arrays=True)
983
def secret_dbus_property(self, value):
984
self.secret = str(value)
989
class ClientHandler(socketserver.BaseRequestHandler, object):
990
"""A class to handle client connections.
992
Instantiated once for each connection to handle it.
307
def still_valid(self, now=None):
308
"""Has the timeout not yet passed for this client?"""
310
now = datetime.datetime.now()
311
if self.last_seen is None:
312
return now < (self.created + self.timeout)
314
return now < (self.last_seen + self.timeout)
317
def peer_certificate(session):
318
"Return the peer's OpenPGP certificate as a bytestring"
319
# If not an OpenPGP certificate...
320
if gnutls.library.functions.gnutls_certificate_type_get\
321
(session._c_object) \
322
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
323
# ...do the normal thing
324
return session.peer_certificate
325
list_size = ctypes.c_uint()
326
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
327
(session._c_object, ctypes.byref(list_size))
328
if list_size.value == 0:
331
return ctypes.string_at(cert.data, cert.size)
334
def fingerprint(openpgp):
335
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
336
# New empty GnuTLS certificate
337
crt = gnutls.library.types.gnutls_openpgp_crt_t()
338
gnutls.library.functions.gnutls_openpgp_crt_init\
340
# New GnuTLS "datum" with the OpenPGP public key
341
datum = gnutls.library.types.gnutls_datum_t\
342
(ctypes.cast(ctypes.c_char_p(openpgp),
343
ctypes.POINTER(ctypes.c_ubyte)),
344
ctypes.c_uint(len(openpgp)))
345
# Import the OpenPGP public key into the certificate
346
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
349
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
350
# New buffer for the fingerprint
351
buffer = ctypes.create_string_buffer(20)
352
buffer_length = ctypes.c_size_t()
353
# Get the fingerprint from the certificate into the buffer
354
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
355
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
356
# Deinit the certificate
357
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
358
# Convert the buffer to a Python bytestring
359
fpr = ctypes.string_at(buffer, buffer_length.value)
360
# Convert the bytestring to hexadecimal notation
361
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
365
class tcp_handler(SocketServer.BaseRequestHandler, object):
366
"""A TCP request handler class.
367
Instantiated by IPv6_TCPServer for each request to handle it.
993
368
Note: This will run in its own forked process."""
995
370
def handle(self):
996
logger.info(u"TCP connection from: %s",
997
unicode(self.client_address))
998
logger.debug(u"IPC Pipe FD: %d",
999
self.server.child_pipe[1].fileno())
1000
# Open IPC pipe to parent process
1001
with contextlib.nested(self.server.child_pipe[1],
1002
self.server.parent_pipe[0]
1003
) as (ipc, ipc_return):
1004
session = (gnutls.connection
1005
.ClientSession(self.request,
1007
.X509Credentials()))
1009
line = self.request.makefile().readline()
1010
logger.debug(u"Protocol version: %r", line)
1012
if int(line.strip().split()[0]) > 1:
1014
except (ValueError, IndexError, RuntimeError), error:
1015
logger.error(u"Unknown protocol version: %s", error)
1018
# Note: gnutls.connection.X509Credentials is really a
1019
# generic GnuTLS certificate credentials object so long as
1020
# no X.509 keys are added to it. Therefore, we can use it
1021
# here despite using OpenPGP certificates.
1023
#priority = u':'.join((u"NONE", u"+VERS-TLS1.1",
1024
# u"+AES-256-CBC", u"+SHA1",
1025
# u"+COMP-NULL", u"+CTYPE-OPENPGP",
1027
# Use a fallback default, since this MUST be set.
1028
priority = self.server.gnutls_priority
1029
if priority is None:
1030
priority = u"NORMAL"
1031
(gnutls.library.functions
1032
.gnutls_priority_set_direct(session._c_object,
1037
except gnutls.errors.GNUTLSError, error:
1038
logger.warning(u"Handshake failed: %s", error)
1039
# Do not run session.bye() here: the session is not
1040
# established. Just abandon the request.
1042
logger.debug(u"Handshake succeeded")
1045
fpr = self.fingerprint(self.peer_certificate
1047
except (TypeError, gnutls.errors.GNUTLSError), error:
1048
logger.warning(u"Bad certificate: %s", error)
1050
logger.debug(u"Fingerprint: %s", fpr)
1052
for c in self.server.clients:
1053
if c.fingerprint == fpr:
1057
ipc.write(u"NOTFOUND %s %s\n"
1058
% (fpr, unicode(self.client_address)))
1060
# Have to check if client.enabled, since it is
1061
# possible that the client was disabled since the
1062
# GnuTLS session was established.
1063
ipc.write(u"GETATTR enabled %s\n" % fpr)
1064
enabled = pickle.load(ipc_return)
1066
ipc.write(u"DISABLED %s\n" % client.name)
1068
ipc.write(u"SENDING %s\n" % client.name)
1070
while sent_size < len(client.secret):
1071
sent = session.send(client.secret[sent_size:])
1072
logger.debug(u"Sent: %d, remaining: %d",
1073
sent, len(client.secret)
1074
- (sent_size + sent))
1080
def peer_certificate(session):
1081
"Return the peer's OpenPGP certificate as a bytestring"
1082
# If not an OpenPGP certificate...
1083
if (gnutls.library.functions
1084
.gnutls_certificate_type_get(session._c_object)
1085
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1086
# ...do the normal thing
1087
return session.peer_certificate
1088
list_size = ctypes.c_uint(1)
1089
cert_list = (gnutls.library.functions
1090
.gnutls_certificate_get_peers
1091
(session._c_object, ctypes.byref(list_size)))
1092
if not bool(cert_list) and list_size.value != 0:
1093
raise gnutls.errors.GNUTLSError(u"error getting peer"
1095
if list_size.value == 0:
1098
return ctypes.string_at(cert.data, cert.size)
1101
def fingerprint(openpgp):
1102
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1103
# New GnuTLS "datum" with the OpenPGP public key
1104
datum = (gnutls.library.types
1105
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1108
ctypes.c_uint(len(openpgp))))
1109
# New empty GnuTLS certificate
1110
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1111
(gnutls.library.functions
1112
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1113
# Import the OpenPGP public key into the certificate
1114
(gnutls.library.functions
1115
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1116
gnutls.library.constants
1117
.GNUTLS_OPENPGP_FMT_RAW))
1118
# Verify the self signature in the key
1119
crtverify = ctypes.c_uint()
1120
(gnutls.library.functions
1121
.gnutls_openpgp_crt_verify_self(crt, 0,
1122
ctypes.byref(crtverify)))
1123
if crtverify.value != 0:
1124
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1125
raise (gnutls.errors.CertificateSecurityError
1127
# New buffer for the fingerprint
1128
buf = ctypes.create_string_buffer(20)
1129
buf_len = ctypes.c_size_t()
1130
# Get the fingerprint from the certificate into the buffer
1131
(gnutls.library.functions
1132
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1133
ctypes.byref(buf_len)))
1134
# Deinit the certificate
1135
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1136
# Convert the buffer to a Python bytestring
1137
fpr = ctypes.string_at(buf, buf_len.value)
1138
# Convert the bytestring to hexadecimal notation
1139
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
1143
class ForkingMixInWithPipes(socketserver.ForkingMixIn, object):
1144
"""Like socketserver.ForkingMixIn, but also pass a pipe pair."""
1145
def process_request(self, request, client_address):
1146
"""Overrides and wraps the original process_request().
1148
This function creates a new pipe in self.pipe
1150
# Child writes to child_pipe
1151
self.child_pipe = map(os.fdopen, os.pipe(), u"rw", (1, 0))
1152
# Parent writes to parent_pipe
1153
self.parent_pipe = map(os.fdopen, os.pipe(), u"rw", (1, 0))
1154
super(ForkingMixInWithPipes,
1155
self).process_request(request, client_address)
1156
# Close unused ends for parent
1157
self.parent_pipe[0].close() # close read end
1158
self.child_pipe[1].close() # close write end
1159
self.add_pipe_fds(self.child_pipe[0], self.parent_pipe[1])
1160
def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1161
"""Dummy function; override as necessary"""
1162
child_pipe_fd.close()
1163
parent_pipe_fd.close()
1166
class IPv6_TCPServer(ForkingMixInWithPipes,
1167
socketserver.TCPServer, object):
1168
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
371
logger.debug(u"TCP connection from: %s",
372
unicode(self.client_address))
373
session = gnutls.connection.ClientSession(self.request,
377
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
378
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
380
priority = "SECURE256"
382
gnutls.library.functions.gnutls_priority_set_direct\
383
(session._c_object, priority, None);
387
except gnutls.errors.GNUTLSError, error:
388
logger.debug(u"Handshake failed: %s", error)
389
# Do not run session.bye() here: the session is not
390
# established. Just abandon the request.
393
fpr = fingerprint(peer_certificate(session))
394
except (TypeError, gnutls.errors.GNUTLSError), error:
395
logger.debug(u"Bad certificate: %s", error)
398
logger.debug(u"Fingerprint: %s", fpr)
400
for c in self.server.clients:
401
if c.fingerprint == fpr:
404
# Have to check if client.still_valid(), since it is possible
405
# that the client timed out while establishing the GnuTLS
407
if (not client) or (not client.still_valid()):
409
logger.debug(u"Client %(name)s is invalid",
412
logger.debug(u"Client not found for fingerprint: %s",
417
while sent_size < len(client.secret):
418
sent = session.send(client.secret[sent_size:])
419
logger.debug(u"Sent: %d, remaining: %d",
420
sent, len(client.secret)
421
- (sent_size + sent))
426
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
427
"""IPv6 TCP server. Accepts 'None' as address and/or port.
1171
enabled: Boolean; whether this server is activated yet
1172
interface: None or a network interface name (string)
1173
use_ipv6: Boolean; to use IPv6 or not
429
options: Command line options
430
clients: Set() of Client objects
1175
def __init__(self, server_address, RequestHandlerClass,
1176
interface=None, use_ipv6=True):
1177
self.interface = interface
1179
self.address_family = socket.AF_INET6
1180
socketserver.TCPServer.__init__(self, server_address,
1181
RequestHandlerClass)
432
address_family = socket.AF_INET6
433
def __init__(self, *args, **kwargs):
434
if "options" in kwargs:
435
self.options = kwargs["options"]
436
del kwargs["options"]
437
if "clients" in kwargs:
438
self.clients = kwargs["clients"]
439
del kwargs["clients"]
440
return super(type(self), self).__init__(*args, **kwargs)
1182
441
def server_bind(self):
1183
442
"""This overrides the normal server_bind() function
1184
443
to bind to an interface if one was specified, and also NOT to
1185
444
bind to an address or port if they were not specified."""
1186
if self.interface is not None:
1187
if SO_BINDTODEVICE is None:
1188
logger.error(u"SO_BINDTODEVICE does not exist;"
1189
u" cannot bind to interface %s",
1193
self.socket.setsockopt(socket.SOL_SOCKET,
1197
except socket.error, error:
1198
if error[0] == errno.EPERM:
1199
logger.error(u"No permission to"
1200
u" bind to interface %s",
1202
elif error[0] == errno.ENOPROTOOPT:
1203
logger.error(u"SO_BINDTODEVICE not available;"
1204
u" cannot bind to interface %s",
445
if self.options.interface:
446
if not hasattr(socket, "SO_BINDTODEVICE"):
447
# From /usr/include/asm-i486/socket.h
448
socket.SO_BINDTODEVICE = 25
450
self.socket.setsockopt(socket.SOL_SOCKET,
451
socket.SO_BINDTODEVICE,
452
self.options.interface)
453
except socket.error, error:
454
if error[0] == errno.EPERM:
455
logger.warning(u"No permission to"
456
u" bind to interface %s",
457
self.options.interface)
1208
460
# Only bind(2) the socket if we really need to.
1209
461
if self.server_address[0] or self.server_address[1]:
1210
462
if not self.server_address[0]:
1211
if self.address_family == socket.AF_INET6:
1212
any_address = u"::" # in6addr_any
1214
any_address = socket.INADDR_ANY
1215
self.server_address = (any_address,
464
self.server_address = (in6addr_any,
1216
465
self.server_address[1])
1217
elif not self.server_address[1]:
466
elif self.server_address[1] is None:
1218
467
self.server_address = (self.server_address[0],
1220
# if self.interface:
1221
# self.server_address = (self.server_address[0],
1226
return socketserver.TCPServer.server_bind(self)
1229
class MandosServer(IPv6_TCPServer):
1233
clients: set of Client objects
1234
gnutls_priority GnuTLS priority string
1235
use_dbus: Boolean; to emit D-Bus signals or not
1237
Assumes a gobject.MainLoop event loop.
1239
def __init__(self, server_address, RequestHandlerClass,
1240
interface=None, use_ipv6=True, clients=None,
1241
gnutls_priority=None, use_dbus=True):
1242
self.enabled = False
1243
self.clients = clients
1244
if self.clients is None:
1245
self.clients = set()
1246
self.use_dbus = use_dbus
1247
self.gnutls_priority = gnutls_priority
1248
IPv6_TCPServer.__init__(self, server_address,
1249
RequestHandlerClass,
1250
interface = interface,
1251
use_ipv6 = use_ipv6)
1252
def server_activate(self):
1254
return socketserver.TCPServer.server_activate(self)
1257
def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1258
# Call "handle_ipc" for both data and EOF events
1259
gobject.io_add_watch(child_pipe_fd.fileno(),
1260
gobject.IO_IN | gobject.IO_HUP,
1261
functools.partial(self.handle_ipc,
1262
reply = parent_pipe_fd,
1263
sender= child_pipe_fd))
1264
def handle_ipc(self, source, condition, reply=None, sender=None):
1266
gobject.IO_IN: u"IN", # There is data to read.
1267
gobject.IO_OUT: u"OUT", # Data can be written (without
1269
gobject.IO_PRI: u"PRI", # There is urgent data to read.
1270
gobject.IO_ERR: u"ERR", # Error condition.
1271
gobject.IO_HUP: u"HUP" # Hung up (the connection has been
1272
# broken, usually for pipes and
1275
conditions_string = ' | '.join(name
1277
condition_names.iteritems()
1278
if cond & condition)
1279
logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1282
# Read a line from the file object
1283
cmdline = sender.readline()
1284
if not cmdline: # Empty line means end of file
1285
# close the IPC pipes
1289
# Stop calling this function
1292
logger.debug(u"IPC command: %r", cmdline)
1294
# Parse and act on command
1295
cmd, args = cmdline.rstrip(u"\r\n").split(None, 1)
1297
if cmd == u"NOTFOUND":
1298
fpr, address = args.split(None, 1)
1299
logger.warning(u"Client not found for fingerprint: %s, ad"
1300
u"dress: %s", fpr, address)
1303
mandos_dbus_service.ClientNotFound(fpr, address)
1304
elif cmd == u"DISABLED":
1305
for client in self.clients:
1306
if client.name == args:
1307
logger.warning(u"Client %s is disabled", args)
1313
logger.error(u"Unknown client %s is disabled", args)
1314
elif cmd == u"SENDING":
1315
for client in self.clients:
1316
if client.name == args:
1317
logger.info(u"Sending secret to %s", client.name)
1324
logger.error(u"Sending secret to unknown client %s",
1326
elif cmd == u"GETATTR":
1327
attr_name, fpr = args.split(None, 1)
1328
for client in self.clients:
1329
if client.fingerprint == fpr:
1330
attr_value = getattr(client, attr_name, None)
1331
logger.debug("IPC reply: %r", attr_value)
1332
pickle.dump(attr_value, reply)
1335
logger.error(u"Client %s on address %s requesting "
1336
u"attribute %s not found", fpr, address,
1338
pickle.dump(None, reply)
1340
logger.error(u"Unknown IPC command: %r", cmdline)
1342
# Keep calling this function
469
return super(type(self), self).server_bind()
1346
472
def string_to_delta(interval):
1347
473
"""Parse a string and return a datetime.timedelta
1349
>>> string_to_delta(u'7d')
475
>>> string_to_delta('7d')
1350
476
datetime.timedelta(7)
1351
>>> string_to_delta(u'60s')
477
>>> string_to_delta('60s')
1352
478
datetime.timedelta(0, 60)
1353
>>> string_to_delta(u'60m')
479
>>> string_to_delta('60m')
1354
480
datetime.timedelta(0, 3600)
1355
>>> string_to_delta(u'24h')
481
>>> string_to_delta('24h')
1356
482
datetime.timedelta(1)
1357
483
>>> string_to_delta(u'1w')
1358
484
datetime.timedelta(7)
1359
>>> string_to_delta(u'5m 30s')
1360
datetime.timedelta(0, 330)
1362
timevalue = datetime.timedelta(0)
1363
for s in interval.split():
1365
suffix = unicode(s[-1])
1368
delta = datetime.timedelta(value)
1369
elif suffix == u"s":
1370
delta = datetime.timedelta(0, value)
1371
elif suffix == u"m":
1372
delta = datetime.timedelta(0, 0, 0, 0, value)
1373
elif suffix == u"h":
1374
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
1375
elif suffix == u"w":
1376
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1378
raise ValueError(u"Unknown suffix %r" % suffix)
1379
except (ValueError, IndexError), e:
1380
raise ValueError(e.message)
487
suffix=unicode(interval[-1])
488
value=int(interval[:-1])
490
delta = datetime.timedelta(value)
492
delta = datetime.timedelta(0, value)
494
delta = datetime.timedelta(0, 0, 0, 0, value)
496
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
498
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
501
except (ValueError, IndexError):
507
"""Derived from the Avahi example code"""
508
global group, serviceName, serviceType, servicePort, serviceTXT, \
511
group = dbus.Interface(
512
bus.get_object( avahi.DBUS_NAME,
513
server.EntryGroupNew()),
514
avahi.DBUS_INTERFACE_ENTRY_GROUP)
515
group.connect_to_signal('StateChanged',
516
entry_group_state_changed)
517
logger.debug(u"Adding service '%s' of type '%s' ...",
518
serviceName, serviceType)
521
serviceInterface, # interface
522
avahi.PROTO_INET6, # protocol
523
dbus.UInt32(0), # flags
524
serviceName, serviceType,
526
dbus.UInt16(servicePort),
527
avahi.string_array_to_txt_array(serviceTXT))
531
def remove_service():
532
"""From the Avahi example code"""
535
if not group is None:
539
def server_state_changed(state):
540
"""Derived from the Avahi example code"""
541
if state == avahi.SERVER_COLLISION:
542
logger.warning(u"Server name collision")
544
elif state == avahi.SERVER_RUNNING:
548
def entry_group_state_changed(state, error):
549
"""Derived from the Avahi example code"""
550
global serviceName, server, rename_count
552
logger.debug(u"state change: %i", state)
554
if state == avahi.ENTRY_GROUP_ESTABLISHED:
555
logger.debug(u"Service established.")
556
elif state == avahi.ENTRY_GROUP_COLLISION:
558
rename_count = rename_count - 1
560
name = server.GetAlternativeServiceName(name)
561
logger.warning(u"Service name collision, "
562
u"changing name to '%s' ...", name)
567
logger.error(u"No suitable service name found after %i"
568
u" retries, exiting.", n_rename)
570
elif state == avahi.ENTRY_GROUP_FAILURE:
571
logger.error(u"Error in group state changed %s",
1385
576
def if_nametoindex(interface):
1386
"""Call the C function if_nametoindex(), or equivalent
1388
Note: This function cannot accept a unicode string."""
1389
global if_nametoindex
577
"""Call the C function if_nametoindex()"""
1391
if_nametoindex = (ctypes.cdll.LoadLibrary
1392
(ctypes.util.find_library(u"c"))
579
libc = ctypes.cdll.LoadLibrary("libc.so.6")
580
return libc.if_nametoindex(interface)
1394
581
except (OSError, AttributeError):
1395
logger.warning(u"Doing if_nametoindex the hard way")
1396
def if_nametoindex(interface):
1397
"Get an interface index the hard way, i.e. using fcntl()"
1398
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
1399
with contextlib.closing(socket.socket()) as s:
1400
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1401
struct.pack(str(u"16s16x"),
1403
interface_index = struct.unpack(str(u"I"),
1405
return interface_index
1406
return if_nametoindex(interface)
1409
def daemon(nochdir = False, noclose = False):
582
if "struct" not in sys.modules:
584
if "fcntl" not in sys.modules:
586
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
588
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
589
struct.pack("16s16x", interface))
591
interface_index = struct.unpack("I", ifreq[16:20])[0]
592
return interface_index
595
def daemon(nochdir, noclose):
1410
596
"""See daemon(3). Standard BSD Unix function.
1412
597
This should really exist as os.daemon, but it doesn't (yet)."""
1421
604
# Close all standard open file descriptors
1422
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
605
null = os.open("/dev/null", os.O_NOCTTY | os.O_RDWR)
1423
606
if not stat.S_ISCHR(os.fstat(null).st_mode):
1424
607
raise OSError(errno.ENODEV,
1425
u"%s not a character device"
608
"/dev/null not a character device")
1427
609
os.dup2(null, sys.stdin.fileno())
1428
610
os.dup2(null, sys.stdout.fileno())
1429
611
os.dup2(null, sys.stderr.fileno())
616
def killme(status = 0):
617
logger.debug("Stopping server with exit status %d", status)
619
if main_loop_started:
1436
##################################################################
1437
# Parsing of options, both command line and config file
1439
parser = optparse.OptionParser(version = "%%prog %s" % version)
1440
parser.add_option("-i", u"--interface", type=u"string",
1441
metavar="IF", help=u"Bind to interface IF")
1442
parser.add_option("-a", u"--address", type=u"string",
1443
help=u"Address to listen for requests on")
1444
parser.add_option("-p", u"--port", type=u"int",
1445
help=u"Port number to receive requests on")
1446
parser.add_option("--check", action=u"store_true",
1447
help=u"Run self-test")
1448
parser.add_option("--debug", action=u"store_true",
1449
help=u"Debug mode; run in foreground and log to"
1451
parser.add_option("--priority", type=u"string", help=u"GnuTLS"
1452
u" priority string (see GnuTLS documentation)")
1453
parser.add_option("--servicename", type=u"string",
1454
metavar=u"NAME", help=u"Zeroconf service name")
1455
parser.add_option("--configdir", type=u"string",
1456
default=u"/etc/mandos", metavar=u"DIR",
1457
help=u"Directory to search for configuration"
1459
parser.add_option("--no-dbus", action=u"store_false",
1460
dest=u"use_dbus", help=u"Do not provide D-Bus"
1461
u" system bus interface")
1462
parser.add_option("--no-ipv6", action=u"store_false",
1463
dest=u"use_ipv6", help=u"Do not use IPv6")
1464
options = parser.parse_args()[0]
628
global main_loop_started
629
main_loop_started = False
631
parser = OptionParser()
632
parser.add_option("-i", "--interface", type="string",
633
default=None, metavar="IF",
634
help="Bind to interface IF")
635
parser.add_option("-a", "--address", type="string", default=None,
636
help="Address to listen for requests on")
637
parser.add_option("-p", "--port", type="int", default=None,
638
help="Port number to receive requests on")
639
parser.add_option("--timeout", type="string", # Parsed later
641
help="Amount of downtime allowed for clients")
642
parser.add_option("--interval", type="string", # Parsed later
644
help="How often to check that a client is up")
645
parser.add_option("--check", action="store_true", default=False,
646
help="Run self-test")
647
parser.add_option("--debug", action="store_true", default=False,
649
(options, args) = parser.parse_args()
1466
651
if options.check:
1468
653
doctest.testmod()
1471
# Default values for config file for server-global settings
1472
server_defaults = { u"interface": u"",
1477
u"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1478
u"servicename": u"Mandos",
1479
u"use_dbus": u"True",
1480
u"use_ipv6": u"True",
1483
# Parse config file for server-global settings
1484
server_config = configparser.SafeConfigParser(server_defaults)
1486
server_config.read(os.path.join(options.configdir,
1488
# Convert the SafeConfigParser object to a dict
1489
server_settings = server_config.defaults()
1490
# Use the appropriate methods on the non-string config options
1491
for option in (u"debug", u"use_dbus", u"use_ipv6"):
1492
server_settings[option] = server_config.getboolean(u"DEFAULT",
1494
if server_settings["port"]:
1495
server_settings["port"] = server_config.getint(u"DEFAULT",
1499
# Override the settings from the config file with command line
1501
for option in (u"interface", u"address", u"port", u"debug",
1502
u"priority", u"servicename", u"configdir",
1503
u"use_dbus", u"use_ipv6"):
1504
value = getattr(options, option)
1505
if value is not None:
1506
server_settings[option] = value
1508
# Force all strings to be unicode
1509
for option in server_settings.keys():
1510
if type(server_settings[option]) is str:
1511
server_settings[option] = unicode(server_settings[option])
1512
# Now we have our good server settings in "server_settings"
1514
##################################################################
1517
debug = server_settings[u"debug"]
1518
use_dbus = server_settings[u"use_dbus"]
1519
use_ipv6 = server_settings[u"use_ipv6"]
1522
syslogger.setLevel(logging.WARNING)
1523
console.setLevel(logging.WARNING)
1525
if server_settings[u"servicename"] != u"Mandos":
1526
syslogger.setFormatter(logging.Formatter
1527
(u'Mandos (%s) [%%(process)d]:'
1528
u' %%(levelname)s: %%(message)s'
1529
% server_settings[u"servicename"]))
1531
# Parse config file with clients
1532
client_defaults = { u"timeout": u"1h",
1534
u"checker": u"fping -q -- %%(host)s",
1537
client_config = configparser.SafeConfigParser(client_defaults)
1538
client_config.read(os.path.join(server_settings[u"configdir"],
1541
global mandos_dbus_service
1542
mandos_dbus_service = None
1544
tcp_server = MandosServer((server_settings[u"address"],
1545
server_settings[u"port"]),
1547
interface=server_settings[u"interface"],
1550
server_settings[u"priority"],
1552
pidfilename = u"/var/run/mandos.pid"
1554
pidfile = open(pidfilename, u"w")
1556
logger.error(u"Could not open file %r", pidfilename)
1559
uid = pwd.getpwnam(u"_mandos").pw_uid
1560
gid = pwd.getpwnam(u"_mandos").pw_gid
1563
uid = pwd.getpwnam(u"mandos").pw_uid
1564
gid = pwd.getpwnam(u"mandos").pw_gid
1567
uid = pwd.getpwnam(u"nobody").pw_uid
1568
gid = pwd.getpwnam(u"nobody").pw_gid
1575
except OSError, error:
1576
if error[0] != errno.EPERM:
1579
# Enable all possible GnuTLS debugging
1581
# "Use a log level over 10 to enable all debugging options."
1583
gnutls.library.functions.gnutls_global_set_log_level(11)
1585
@gnutls.library.types.gnutls_log_func
1586
def debug_gnutls(level, string):
1587
logger.debug(u"GnuTLS: %s", string[:-1])
1589
(gnutls.library.functions
1590
.gnutls_global_set_log_function(debug_gnutls))
656
# Parse the time arguments
658
options.timeout = string_to_delta(options.timeout)
660
parser.error("option --timeout: Unparseable time")
662
options.interval = string_to_delta(options.interval)
664
parser.error("option --interval: Unparseable time")
667
defaults = { "checker": "fping -q -- %%(fqdn)s" }
668
client_config = ConfigParser.SafeConfigParser(defaults)
669
#client_config.readfp(open("global.conf"), "global.conf")
670
client_config.read("mandos-clients.conf")
1592
672
global main_loop
1593
675
# From the Avahi example code
1594
676
DBusGMainLoop(set_as_default=True )
1595
677
main_loop = gobject.MainLoop()
1596
678
bus = dbus.SystemBus()
679
server = dbus.Interface(
680
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
681
avahi.DBUS_INTERFACE_SERVER )
1597
682
# End of Avahi example code
1600
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos",
1601
bus, do_not_queue=True)
1602
except dbus.exceptions.NameExistsException, e:
1603
logger.error(unicode(e) + u", disabling D-Bus")
1605
server_settings[u"use_dbus"] = False
1606
tcp_server.use_dbus = False
1607
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1608
service = AvahiService(name = server_settings[u"servicename"],
1609
servicetype = u"_mandos._tcp",
1610
protocol = protocol, bus = bus)
1611
if server_settings["interface"]:
1612
service.interface = (if_nametoindex
1613
(str(server_settings[u"interface"])))
1615
client_class = Client
1617
client_class = functools.partial(ClientDBus, bus = bus)
1618
tcp_server.clients.update(set(
1619
client_class(name = section,
1620
config= dict(client_config.items(section)))
1621
for section in client_config.sections()))
1622
if not tcp_server.clients:
1623
logger.warning(u"No clients defined")
684
debug = options.debug
1626
# Redirect stdin so all checkers get /dev/null
1627
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1628
os.dup2(null, sys.stdin.fileno())
1632
# No console logging
1633
logger.removeHandler(console)
1634
# Close all input and output, do double fork, etc.
1640
pidfile.write(str(pid) + "\n")
1643
logger.error(u"Could not write to file %r with PID %d",
1646
# "pidfile" was never created
687
console = logging.StreamHandler()
688
# console.setLevel(logging.DEBUG)
689
console.setFormatter(logging.Formatter\
690
('%(levelname)s: %(message)s'))
691
logger.addHandler(console)
695
def remove_from_clients(client):
696
clients.remove(client)
698
logger.debug(u"No clients left, exiting")
701
clients.update(Set(Client(name=section, options=options,
702
stop_hook = remove_from_clients,
703
**(dict(client_config\
705
for section in client_config.sections()))
1651
signal.signal(signal.SIGINT, signal.SIG_IGN)
1652
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1653
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1656
class MandosDBusService(dbus.service.Object):
1657
"""A D-Bus proxy object"""
1659
dbus.service.Object.__init__(self, bus, u"/")
1660
_interface = u"se.bsnet.fukt.Mandos"
1662
@dbus.service.signal(_interface, signature=u"o")
1663
def ClientAdded(self, objpath):
1667
@dbus.service.signal(_interface, signature=u"ss")
1668
def ClientNotFound(self, fingerprint, address):
1672
@dbus.service.signal(_interface, signature=u"os")
1673
def ClientRemoved(self, objpath, name):
1677
@dbus.service.method(_interface, out_signature=u"ao")
1678
def GetAllClients(self):
1680
return dbus.Array(c.dbus_object_path
1681
for c in tcp_server.clients)
1683
@dbus.service.method(_interface,
1684
out_signature=u"a{oa{sv}}")
1685
def GetAllClientsWithProperties(self):
1687
return dbus.Dictionary(
1688
((c.dbus_object_path, c.GetAll(u""))
1689
for c in tcp_server.clients),
1690
signature=u"oa{sv}")
1692
@dbus.service.method(_interface, in_signature=u"o")
1693
def RemoveClient(self, object_path):
1695
for c in tcp_server.clients:
1696
if c.dbus_object_path == object_path:
1697
tcp_server.clients.remove(c)
1698
c.remove_from_connection()
1699
# Don't signal anything except ClientRemoved
1700
c.disable(quiet=True)
1702
self.ClientRemoved(object_path, c.name)
1704
raise KeyError(object_path)
1708
mandos_dbus_service = MandosDBusService()
1711
711
"Cleanup function; run on exit"
1714
while tcp_server.clients:
1715
client = tcp_server.clients.pop()
1717
client.remove_from_connection()
1718
client.disable_hook = None
1719
# Don't signal anything except ClientRemoved
1720
client.disable(quiet=True)
1723
mandos_dbus_service.ClientRemoved(client.dbus_object_path,
1726
atexit.register(cleanup)
1728
for client in tcp_server.clients:
1731
mandos_dbus_service.ClientAdded(client.dbus_object_path)
1735
tcp_server.server_activate()
1737
# Find out what port we got
1738
service.port = tcp_server.socket.getsockname()[1]
1740
logger.info(u"Now listening on address %r, port %d,"
1741
" flowinfo %d, scope_id %d"
1742
% tcp_server.socket.getsockname())
1744
logger.info(u"Now listening on address %r, port %d"
1745
% tcp_server.socket.getsockname())
1747
#service.interface = tcp_server.socket.getsockname()[3]
1750
713
# From the Avahi example code
1753
except dbus.exceptions.DBusException, error:
1754
logger.critical(u"DBusException: %s", error)
714
if not group is None:
1757
717
# End of Avahi example code
1759
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1760
lambda *args, **kwargs:
1761
(tcp_server.handle_request
1762
(*args[2:], **kwargs) or True))
1764
logger.debug(u"Starting main loop")
720
client = clients.pop()
721
client.stop_hook = None
724
atexit.register(cleanup)
727
signal.signal(signal.SIGINT, signal.SIG_IGN)
728
signal.signal(signal.SIGHUP, lambda signum, frame: killme())
729
signal.signal(signal.SIGTERM, lambda signum, frame: killme())
731
for client in clients:
734
tcp_server = IPv6_TCPServer((options.address, options.port),
738
# Find out what random port we got
740
servicePort = tcp_server.socket.getsockname()[1]
741
logger.debug(u"Now listening on port %d", servicePort)
743
if options.interface is not None:
744
global serviceInterface
745
serviceInterface = if_nametoindex(options.interface)
747
# From the Avahi example code
748
server.connect_to_signal("StateChanged", server_state_changed)
750
server_state_changed(server.GetState())
751
except dbus.exceptions.DBusException, error:
752
logger.critical(u"DBusException: %s", error)
754
# End of Avahi example code
756
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
757
lambda *args, **kwargs:
758
tcp_server.handle_request(*args[2:],
761
logger.debug("Starting main loop")
762
main_loop_started = True
1766
except AvahiError, error:
1767
logger.critical(u"AvahiError: %s", error)
1770
764
except KeyboardInterrupt:
1773
logger.debug(u"Server received KeyboardInterrupt")
1774
logger.debug(u"Server exiting")
1775
# Must run before the D-Bus bus name gets deregistered
1778
770
if __name__ == '__main__':