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
69
62
from dbus.mainloop.glib import DBusGMainLoop
72
import xml.dom.minidom
76
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
77
except AttributeError:
79
from IN import SO_BINDTODEVICE
81
SO_BINDTODEVICE = None
86
logger = logging.Logger(u'mandos')
87
syslogger = (logging.handlers.SysLogHandler
88
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
89
address = "/dev/log"))
90
syslogger.setFormatter(logging.Formatter
91
(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'))
93
82
logger.addHandler(syslogger)
95
console = logging.StreamHandler()
96
console.setFormatter(logging.Formatter(u'%(name)s [%(process)d]:'
99
logger.addHandler(console)
101
class AvahiError(Exception):
102
def __init__(self, value, *args, **kwargs):
104
super(AvahiError, self).__init__(value, *args, **kwargs)
105
def __unicode__(self):
106
return unicode(repr(self.value))
108
class AvahiServiceError(AvahiError):
111
class AvahiGroupError(AvahiError):
115
class AvahiService(object):
116
"""An Avahi (Zeroconf) service.
119
interface: integer; avahi.IF_UNSPEC or an interface index.
120
Used to optionally bind to the specified interface.
121
name: string; Example: u'Mandos'
122
type: string; Example: u'_mandos._tcp'.
123
See <http://www.dns-sd.org/ServiceTypes.html>
124
port: integer; what port to announce
125
TXT: list of strings; TXT record for the service
126
domain: string; Domain to publish on, default to .local if empty.
127
host: string; Host to publish records for, default is localhost
128
max_renames: integer; maximum number of renames
129
rename_count: integer; counter so we only rename after collisions
130
a sensible number of times
131
group: D-Bus Entry Group
133
bus: dbus.SystemBus()
135
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
136
servicetype = None, port = None, TXT = None,
137
domain = u"", host = u"", max_renames = 32768,
138
protocol = avahi.PROTO_UNSPEC, bus = None):
139
self.interface = interface
141
self.type = servicetype
143
self.TXT = TXT if TXT is not None else []
146
self.rename_count = 0
147
self.max_renames = max_renames
148
self.protocol = protocol
149
self.group = None # our entry group
153
"""Derived from the Avahi example code"""
154
if self.rename_count >= self.max_renames:
155
logger.critical(u"No suitable Zeroconf service name found"
156
u" after %i retries, exiting.",
158
raise AvahiServiceError(u"Too many renames")
159
self.name = self.server.GetAlternativeServiceName(self.name)
160
logger.info(u"Changing Zeroconf service name to %r ...",
162
syslogger.setFormatter(logging.Formatter
163
(u'Mandos (%s) [%%(process)d]:'
164
u' %%(levelname)s: %%(message)s'
168
self.rename_count += 1
170
"""Derived from the Avahi example code"""
171
if self.group is not None:
174
"""Derived from the Avahi example code"""
175
if self.group is None:
176
self.group = dbus.Interface(
177
self.bus.get_object(avahi.DBUS_NAME,
178
self.server.EntryGroupNew()),
179
avahi.DBUS_INTERFACE_ENTRY_GROUP)
180
self.group.connect_to_signal('StateChanged',
182
.entry_group_state_changed)
183
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
184
self.name, self.type)
185
self.group.AddService(
188
dbus.UInt32(0), # flags
189
self.name, self.type,
190
self.domain, self.host,
191
dbus.UInt16(self.port),
192
avahi.string_array_to_txt_array(self.TXT))
194
def entry_group_state_changed(self, state, error):
195
"""Derived from the Avahi example code"""
196
logger.debug(u"Avahi entry group state change: %i", state)
198
if state == avahi.ENTRY_GROUP_ESTABLISHED:
199
logger.debug(u"Zeroconf service established.")
200
elif state == avahi.ENTRY_GROUP_COLLISION:
201
logger.warning(u"Zeroconf service name collision.")
203
elif state == avahi.ENTRY_GROUP_FAILURE:
204
logger.critical(u"Avahi: Error in group state changed %s",
206
raise AvahiGroupError(u"State changed: %s"
209
"""Derived from the Avahi example code"""
210
if self.group is not None:
213
def server_state_changed(self, state):
214
"""Derived from the Avahi example code"""
215
logger.debug(u"Avahi server state change: %i", state)
216
if state == avahi.SERVER_COLLISION:
217
logger.error(u"Zeroconf server name collision")
219
elif state == avahi.SERVER_RUNNING:
222
"""Derived from the Avahi example code"""
223
if self.server is None:
224
self.server = dbus.Interface(
225
self.bus.get_object(avahi.DBUS_NAME,
226
avahi.DBUS_PATH_SERVER),
227
avahi.DBUS_INTERFACE_SERVER)
228
self.server.connect_to_signal(u"StateChanged",
229
self.server_state_changed)
230
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
233
102
class Client(object):
234
103
"""A representation of a client host served by this server.
237
name: string; from the config file, used in log messages and
105
name: string; from the config file, used in log messages
239
106
fingerprint: string (40 or 32 hexadecimal digits); used to
240
107
uniquely identify the client
241
secret: bytestring; sent verbatim (over TLS) to client
242
host: string; available for use by the checker command
243
created: datetime.datetime(); (UTC) object creation
244
last_enabled: datetime.datetime(); (UTC)
246
last_checked_ok: datetime.datetime(); (UTC) or None
247
timeout: datetime.timedelta(); How long from last_checked_ok
248
until this client is disabled
249
interval: datetime.timedelta(); How often to start a new checker
250
disable_hook: If set, called by disable() as disable_hook(self)
251
checker: subprocess.Popen(); a running checker process used
252
to see if the client lives.
253
'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.
254
119
checker_initiator_tag: a gobject event source tag, or None
255
disable_initiator_tag: - '' -
120
stop_initiator_tag: - '' -
256
121
checker_callback_tag: - '' -
257
122
checker_command: string; External command which is run to check if
258
client lives. %() expansions are done at
123
client lives. %()s expansions are done at
259
124
runtime with vars(self) as dict, so that for
260
125
instance %(name)s can be used in the command.
261
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: - '' -
265
def _timedelta_to_milliseconds(td):
266
"Convert a datetime.timedelta() to milliseconds"
267
return ((td.days * 24 * 60 * 60 * 1000)
268
+ (td.seconds * 1000)
269
+ (td.microseconds // 1000))
271
def timeout_milliseconds(self):
272
"Return the 'timeout' attribute in milliseconds"
273
return self._timedelta_to_milliseconds(self.timeout)
275
def interval_milliseconds(self):
276
"Return the 'interval' attribute in milliseconds"
277
return self._timedelta_to_milliseconds(self.interval)
279
def __init__(self, name = None, disable_hook=None, config=None):
280
"""Note: the 'checker' key in 'config' sets the
281
'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.."""
286
logger.debug(u"Creating client %r", self.name)
287
# Uppercase and remove spaces from fingerprint for later
288
# comparison purposes with return value from the fingerprint()
290
self.fingerprint = (config[u"fingerprint"].upper()
292
logger.debug(u" Fingerprint: %s", self.fingerprint)
293
if u"secret" in config:
294
self.secret = config[u"secret"].decode(u"base64")
295
elif u"secfile" in config:
296
with open(os.path.expanduser(os.path.expandvars
297
(config[u"secfile"])),
299
self.secret = secfile.read()
301
raise TypeError(u"No secret or secfile for client %s"
303
self.host = config.get(u"host", u"")
304
self.created = datetime.datetime.utcnow()
306
self.last_enabled = None
307
self.last_checked_ok = None
308
self.timeout = string_to_delta(config[u"timeout"])
309
self.interval = string_to_delta(config[u"interval"])
310
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
311
186
self.checker = None
312
187
self.checker_initiator_tag = None
313
self.disable_initiator_tag = None
188
self.stop_initiator_tag = None
314
189
self.checker_callback_tag = None
315
self.checker_command = config[u"checker"]
316
self.current_checker_command = None
317
self.last_connect = None
190
self.check_command = checker
320
192
"""Start this client's checker and timeout hooks"""
321
if getattr(self, u"enabled", False):
324
self.last_enabled = datetime.datetime.utcnow()
325
193
# Schedule a new checker to be started an 'interval' from now,
326
194
# and every interval from then on.
327
self.checker_initiator_tag = (gobject.timeout_add
328
(self.interval_milliseconds(),
330
# Schedule a disable() when 'timeout' has passed
331
self.disable_initiator_tag = (gobject.timeout_add
332
(self.timeout_milliseconds(),
195
self.checker_initiator_tag = gobject.timeout_add\
196
(self._interval_milliseconds,
335
198
# Also start a new checker *right now*.
336
199
self.start_checker()
338
def disable(self, quiet=True):
339
"""Disable this client."""
340
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)
343
logger.info(u"Disabling client %s", self.name)
344
if getattr(self, u"disable_initiator_tag", False):
345
gobject.source_remove(self.disable_initiator_tag)
346
self.disable_initiator_tag = None
347
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:
348
220
gobject.source_remove(self.checker_initiator_tag)
349
221
self.checker_initiator_tag = None
350
222
self.stop_checker()
351
if self.disable_hook:
352
self.disable_hook(self)
354
225
# Do not run this again if called by a gobject.timeout_add
357
227
def __del__(self):
358
self.disable_hook = None
361
def checker_callback(self, pid, condition, command):
228
self.stop_hook = None
230
def checker_callback(self, pid, condition):
362
231
"""The checker has completed, so take appropriate actions."""
232
now = datetime.datetime.now()
363
233
self.checker_callback_tag = None
364
234
self.checker = None
365
if os.WIFEXITED(condition):
366
exitstatus = os.WEXITSTATUS(condition)
368
logger.info(u"Checker for %(name)s succeeded",
372
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):
375
245
logger.warning(u"Checker for %(name)s crashed?",
378
def checked_ok(self):
379
"""Bump up the timeout for this client.
381
This should only be called when the client has been seen,
384
self.last_checked_ok = datetime.datetime.utcnow()
385
gobject.source_remove(self.disable_initiator_tag)
386
self.disable_initiator_tag = (gobject.timeout_add
387
(self.timeout_milliseconds(),
248
logger.debug(u"Checker for %(name)s failed",
390
250
def start_checker(self):
391
251
"""Start a new checker subprocess if one is not running.
393
252
If a checker already exists, leave it running and do
395
254
# The reason for not killing a running checker is that if we
398
257
# client would inevitably timeout, since no checker would get
399
258
# a chance to run to completion. If we instead leave running
400
259
# checkers alone, the checker would have to take more time
401
# than 'timeout' for the client to be disabled, which is as it
404
# If a checker exists, make sure it is not a zombie
406
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
407
except (AttributeError, OSError), error:
408
if (isinstance(error, OSError)
409
and error.errno != errno.ECHILD):
413
logger.warning(u"Checker was a zombie")
414
gobject.source_remove(self.checker_callback_tag)
415
self.checker_callback(pid, status,
416
self.current_checker_command)
417
# Start a new checker if needed
260
# than 'timeout' for the client to be declared invalid, which
261
# is as it should be.
418
262
if self.checker is None:
420
# In case checker_command has exactly one % operator
421
command = self.checker_command % self.host
264
command = self.check_command % self.fqdn
422
265
except TypeError:
423
# Escape attributes for the shell
424
escaped_attrs = dict((key,
425
re.escape(unicode(str(val),
266
escaped_attrs = dict((key, re.escape(str(val)))
429
268
vars(self).iteritems())
431
command = self.checker_command % escaped_attrs
270
command = self.check_command % escaped_attrs
432
271
except TypeError, error:
433
logger.error(u'Could not format string "%s":'
434
u' %s', self.checker_command, error)
272
logger.critical(u'Could not format string "%s":'
273
u' %s', self.check_command, error)
435
274
return True # Try again later
436
self.current_checker_command = command
438
logger.info(u"Starting checker %r for %s",
440
# We don't need to redirect stdout and stderr, since
441
# in normal mode, that is already done by daemon(),
442
# and in debug mode we don't want to. (Stdin is
443
# always replaced by /dev/null.)
444
self.checker = subprocess.Popen(command,
446
shell=True, cwd=u"/")
447
self.checker_callback_tag = (gobject.child_watch_add
449
self.checker_callback,
451
# The checker may have completed before the gobject
452
# watch was added. Check for this.
453
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
455
gobject.source_remove(self.checker_callback_tag)
456
self.checker_callback(pid, status, command)
457
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:
458
286
logger.error(u"Failed to start subprocess: %s",
460
288
# Re-run this periodically if run by gobject.timeout_add
463
290
def stop_checker(self):
464
291
"""Force the checker process, if any, to stop."""
465
292
if self.checker_callback_tag:
466
293
gobject.source_remove(self.checker_callback_tag)
467
294
self.checker_callback_tag = None
468
if getattr(self, u"checker", None) is None:
295
if not hasattr(self, "checker") or self.checker is None:
470
logger.debug(u"Stopping checker for %(name)s", vars(self))
297
logger.debug("Stopping checker for %(name)s", vars(self))
472
299
os.kill(self.checker.pid, signal.SIGTERM)
474
301
#if self.checker.poll() is None:
475
302
# os.kill(self.checker.pid, signal.SIGKILL)
476
303
except OSError, error:
477
if error.errno != errno.ESRCH: # No such process
304
if error.errno != errno.ESRCH:
479
306
self.checker = None
482
def dbus_service_property(dbus_interface, signature=u"v",
483
access=u"readwrite", byte_arrays=False):
484
"""Decorators for marking methods of a DBusObjectWithProperties to
485
become properties on the D-Bus.
487
The decorated method will be called with no arguments by "Get"
488
and with one argument by "Set".
490
The parameters, where they are supported, are the same as
491
dbus.service.method, except there is only "signature", since the
492
type from Get() and the type sent to Set() is the same.
494
# Encoding deeply encoded byte arrays is not supported yet by the
495
# "Set" method, so we fail early here:
496
if byte_arrays and signature != u"ay":
497
raise ValueError(u"Byte arrays not supported for non-'ay'"
498
u" signature %r" % signature)
500
func._dbus_is_property = True
501
func._dbus_interface = dbus_interface
502
func._dbus_signature = signature
503
func._dbus_access = access
504
func._dbus_name = func.__name__
505
if func._dbus_name.endswith(u"_dbus_property"):
506
func._dbus_name = func._dbus_name[:-14]
507
func._dbus_get_args_options = {u'byte_arrays': byte_arrays }
512
class DBusPropertyException(dbus.exceptions.DBusException):
513
"""A base class for D-Bus property-related exceptions
515
def __unicode__(self):
516
return unicode(str(self))
519
class DBusPropertyAccessException(DBusPropertyException):
520
"""A property's access permissions disallows an operation.
525
class DBusPropertyNotFound(DBusPropertyException):
526
"""An attempt was made to access a non-existing property.
531
class DBusObjectWithProperties(dbus.service.Object):
532
"""A D-Bus object with properties.
534
Classes inheriting from this can use the dbus_service_property
535
decorator to expose methods as D-Bus properties. It exposes the
536
standard Get(), Set(), and GetAll() methods on the D-Bus.
540
def _is_dbus_property(obj):
541
return getattr(obj, u"_dbus_is_property", False)
543
def _get_all_dbus_properties(self):
544
"""Returns a generator of (name, attribute) pairs
546
return ((prop._dbus_name, prop)
548
inspect.getmembers(self, self._is_dbus_property))
550
def _get_dbus_property(self, interface_name, property_name):
551
"""Returns a bound method if one exists which is a D-Bus
552
property with the specified name and interface.
554
for name in (property_name,
555
property_name + u"_dbus_property"):
556
prop = getattr(self, name, None)
558
or not self._is_dbus_property(prop)
559
or prop._dbus_name != property_name
560
or (interface_name and prop._dbus_interface
561
and interface_name != prop._dbus_interface)):
565
raise DBusPropertyNotFound(self.dbus_object_path + u":"
566
+ interface_name + u"."
569
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ss",
571
def Get(self, interface_name, property_name):
572
"""Standard D-Bus property Get() method, see D-Bus standard.
574
prop = self._get_dbus_property(interface_name, property_name)
575
if prop._dbus_access == u"write":
576
raise DBusPropertyAccessException(property_name)
578
if not hasattr(value, u"variant_level"):
580
return type(value)(value, variant_level=value.variant_level+1)
582
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ssv")
583
def Set(self, interface_name, property_name, value):
584
"""Standard D-Bus property Set() method, see D-Bus standard.
586
prop = self._get_dbus_property(interface_name, property_name)
587
if prop._dbus_access == u"read":
588
raise DBusPropertyAccessException(property_name)
589
if prop._dbus_get_args_options[u"byte_arrays"]:
590
# The byte_arrays option is not supported yet on
591
# signatures other than "ay".
592
if prop._dbus_signature != u"ay":
594
value = dbus.ByteArray(''.join(unichr(byte)
598
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"s",
599
out_signature=u"a{sv}")
600
def GetAll(self, interface_name):
601
"""Standard D-Bus property GetAll() method, see D-Bus
604
Note: Will not include properties with access="write".
607
for name, prop in self._get_all_dbus_properties():
609
and interface_name != prop._dbus_interface):
610
# Interface non-empty but did not match
612
# Ignore write-only properties
613
if prop._dbus_access == u"write":
616
if not hasattr(value, u"variant_level"):
619
all[name] = type(value)(value, variant_level=
620
value.variant_level+1)
621
return dbus.Dictionary(all, signature=u"sv")
623
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
625
path_keyword='object_path',
626
connection_keyword='connection')
627
def Introspect(self, object_path, connection):
628
"""Standard D-Bus method, overloaded to insert property tags.
630
xmlstring = dbus.service.Object.Introspect(self, object_path,
633
document = xml.dom.minidom.parseString(xmlstring)
634
def make_tag(document, name, prop):
635
e = document.createElement(u"property")
636
e.setAttribute(u"name", name)
637
e.setAttribute(u"type", prop._dbus_signature)
638
e.setAttribute(u"access", prop._dbus_access)
640
for if_tag in document.getElementsByTagName(u"interface"):
641
for tag in (make_tag(document, name, prop)
643
in self._get_all_dbus_properties()
644
if prop._dbus_interface
645
== if_tag.getAttribute(u"name")):
646
if_tag.appendChild(tag)
647
# Add the names to the return values for the
648
# "org.freedesktop.DBus.Properties" methods
649
if (if_tag.getAttribute(u"name")
650
== u"org.freedesktop.DBus.Properties"):
651
for cn in if_tag.getElementsByTagName(u"method"):
652
if cn.getAttribute(u"name") == u"Get":
653
for arg in cn.getElementsByTagName(u"arg"):
654
if (arg.getAttribute(u"direction")
656
arg.setAttribute(u"name", u"value")
657
elif cn.getAttribute(u"name") == u"GetAll":
658
for arg in cn.getElementsByTagName(u"arg"):
659
if (arg.getAttribute(u"direction")
661
arg.setAttribute(u"name", u"props")
662
xmlstring = document.toxml(u"utf-8")
664
except (AttributeError, xml.dom.DOMException,
665
xml.parsers.expat.ExpatError), error:
666
logger.error(u"Failed to override Introspection method",
671
class ClientDBus(Client, DBusObjectWithProperties):
672
"""A Client class using D-Bus
675
dbus_object_path: dbus.ObjectPath
676
bus: dbus.SystemBus()
678
# dbus.service.Object doesn't use super(), so we can't either.
680
def __init__(self, bus = None, *args, **kwargs):
682
Client.__init__(self, *args, **kwargs)
683
# Only now, when this client is initialized, can it show up on
685
self.dbus_object_path = (dbus.ObjectPath
687
+ self.name.replace(u".", u"_")))
688
DBusObjectWithProperties.__init__(self, self.bus,
689
self.dbus_object_path)
692
def _datetime_to_dbus(dt, variant_level=0):
693
"""Convert a UTC datetime.datetime() to a D-Bus type."""
694
return dbus.String(dt.isoformat(),
695
variant_level=variant_level)
698
oldstate = getattr(self, u"enabled", False)
699
r = Client.enable(self)
700
if oldstate != self.enabled:
702
self.PropertyChanged(dbus.String(u"enabled"),
703
dbus.Boolean(True, variant_level=1))
704
self.PropertyChanged(
705
dbus.String(u"last_enabled"),
706
self._datetime_to_dbus(self.last_enabled,
710
def disable(self, quiet = False):
711
oldstate = getattr(self, u"enabled", False)
712
r = Client.disable(self, quiet=quiet)
713
if not quiet and oldstate != self.enabled:
715
self.PropertyChanged(dbus.String(u"enabled"),
716
dbus.Boolean(False, variant_level=1))
719
def __del__(self, *args, **kwargs):
721
self.remove_from_connection()
724
if hasattr(DBusObjectWithProperties, u"__del__"):
725
DBusObjectWithProperties.__del__(self, *args, **kwargs)
726
Client.__del__(self, *args, **kwargs)
728
def checker_callback(self, pid, condition, command,
730
self.checker_callback_tag = None
733
self.PropertyChanged(dbus.String(u"checker_running"),
734
dbus.Boolean(False, variant_level=1))
735
if os.WIFEXITED(condition):
736
exitstatus = os.WEXITSTATUS(condition)
738
self.CheckerCompleted(dbus.Int16(exitstatus),
739
dbus.Int64(condition),
740
dbus.String(command))
743
self.CheckerCompleted(dbus.Int16(-1),
744
dbus.Int64(condition),
745
dbus.String(command))
747
return Client.checker_callback(self, pid, condition, command,
750
def checked_ok(self, *args, **kwargs):
751
r = Client.checked_ok(self, *args, **kwargs)
753
self.PropertyChanged(
754
dbus.String(u"last_checked_ok"),
755
(self._datetime_to_dbus(self.last_checked_ok,
759
def start_checker(self, *args, **kwargs):
760
old_checker = self.checker
761
if self.checker is not None:
762
old_checker_pid = self.checker.pid
764
old_checker_pid = None
765
r = Client.start_checker(self, *args, **kwargs)
766
# Only if new checker process was started
767
if (self.checker is not None
768
and old_checker_pid != self.checker.pid):
770
self.CheckerStarted(self.current_checker_command)
771
self.PropertyChanged(
772
dbus.String(u"checker_running"),
773
dbus.Boolean(True, variant_level=1))
776
def stop_checker(self, *args, **kwargs):
777
old_checker = getattr(self, u"checker", None)
778
r = Client.stop_checker(self, *args, **kwargs)
779
if (old_checker is not None
780
and getattr(self, u"checker", None) is None):
781
self.PropertyChanged(dbus.String(u"checker_running"),
782
dbus.Boolean(False, variant_level=1))
785
## D-Bus methods, signals & properties
786
_interface = u"se.bsnet.fukt.Mandos.Client"
790
# CheckerCompleted - signal
791
@dbus.service.signal(_interface, signature=u"nxs")
792
def CheckerCompleted(self, exitcode, waitstatus, command):
796
# CheckerStarted - signal
797
@dbus.service.signal(_interface, signature=u"s")
798
def CheckerStarted(self, command):
802
# PropertyChanged - signal
803
@dbus.service.signal(_interface, signature=u"sv")
804
def PropertyChanged(self, property, value):
809
@dbus.service.signal(_interface)
815
@dbus.service.signal(_interface)
823
@dbus.service.method(_interface)
825
return self.checked_ok()
828
@dbus.service.method(_interface)
833
# StartChecker - method
834
@dbus.service.method(_interface)
835
def StartChecker(self):
840
@dbus.service.method(_interface)
845
# StopChecker - method
846
@dbus.service.method(_interface)
847
def StopChecker(self):
853
@dbus_service_property(_interface, signature=u"s", access=u"read")
854
def name_dbus_property(self):
855
return dbus.String(self.name)
857
# fingerprint - property
858
@dbus_service_property(_interface, signature=u"s", access=u"read")
859
def fingerprint_dbus_property(self):
860
return dbus.String(self.fingerprint)
863
@dbus_service_property(_interface, signature=u"s",
865
def host_dbus_property(self, value=None):
866
if value is None: # get
867
return dbus.String(self.host)
870
self.PropertyChanged(dbus.String(u"host"),
871
dbus.String(value, variant_level=1))
874
@dbus_service_property(_interface, signature=u"s", access=u"read")
875
def created_dbus_property(self):
876
return dbus.String(self._datetime_to_dbus(self.created))
878
# last_enabled - property
879
@dbus_service_property(_interface, signature=u"s", access=u"read")
880
def last_enabled_dbus_property(self):
881
if self.last_enabled is None:
882
return dbus.String(u"")
883
return dbus.String(self._datetime_to_dbus(self.last_enabled))
886
@dbus_service_property(_interface, signature=u"b",
888
def enabled_dbus_property(self, value=None):
889
if value is None: # get
890
return dbus.Boolean(self.enabled)
896
# last_checked_ok - property
897
@dbus_service_property(_interface, signature=u"s",
899
def last_checked_ok_dbus_property(self, value=None):
900
if value is not None:
903
if self.last_checked_ok is None:
904
return dbus.String(u"")
905
return dbus.String(self._datetime_to_dbus(self
909
@dbus_service_property(_interface, signature=u"t",
911
def timeout_dbus_property(self, value=None):
912
if value is None: # get
913
return dbus.UInt64(self.timeout_milliseconds())
914
self.timeout = datetime.timedelta(0, 0, 0, value)
916
self.PropertyChanged(dbus.String(u"timeout"),
917
dbus.UInt64(value, variant_level=1))
918
if getattr(self, u"disable_initiator_tag", None) is None:
921
gobject.source_remove(self.disable_initiator_tag)
922
self.disable_initiator_tag = None
924
_timedelta_to_milliseconds((self
930
# The timeout has passed
933
self.disable_initiator_tag = (gobject.timeout_add
934
(time_to_die, self.disable))
936
# interval - property
937
@dbus_service_property(_interface, signature=u"t",
939
def interval_dbus_property(self, value=None):
940
if value is None: # get
941
return dbus.UInt64(self.interval_milliseconds())
942
self.interval = datetime.timedelta(0, 0, 0, value)
944
self.PropertyChanged(dbus.String(u"interval"),
945
dbus.UInt64(value, variant_level=1))
946
if getattr(self, u"checker_initiator_tag", None) is None:
948
# Reschedule checker run
949
gobject.source_remove(self.checker_initiator_tag)
950
self.checker_initiator_tag = (gobject.timeout_add
951
(value, self.start_checker))
952
self.start_checker() # Start one now, too
955
@dbus_service_property(_interface, signature=u"s",
957
def checker_dbus_property(self, value=None):
958
if value is None: # get
959
return dbus.String(self.checker_command)
960
self.checker_command = value
962
self.PropertyChanged(dbus.String(u"checker"),
963
dbus.String(self.checker_command,
966
# checker_running - property
967
@dbus_service_property(_interface, signature=u"b",
969
def checker_running_dbus_property(self, value=None):
970
if value is None: # get
971
return dbus.Boolean(self.checker is not None)
977
# object_path - property
978
@dbus_service_property(_interface, signature=u"o", access=u"read")
979
def object_path_dbus_property(self):
980
return self.dbus_object_path # is already a dbus.ObjectPath
983
@dbus_service_property(_interface, signature=u"ay",
984
access=u"write", byte_arrays=True)
985
def secret_dbus_property(self, value):
986
self.secret = str(value)
991
class ClientHandler(socketserver.BaseRequestHandler, object):
992
"""A class to handle client connections.
994
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.
995
368
Note: This will run in its own forked process."""
997
370
def handle(self):
998
logger.info(u"TCP connection from: %s",
999
unicode(self.client_address))
1000
logger.debug(u"IPC Pipe FD: %d",
1001
self.server.child_pipe[1].fileno())
1002
# Open IPC pipe to parent process
1003
with contextlib.nested(self.server.child_pipe[1],
1004
self.server.parent_pipe[0]
1005
) as (ipc, ipc_return):
1006
session = (gnutls.connection
1007
.ClientSession(self.request,
1009
.X509Credentials()))
1011
# Note: gnutls.connection.X509Credentials is really a
1012
# generic GnuTLS certificate credentials object so long as
1013
# no X.509 keys are added to it. Therefore, we can use it
1014
# here despite using OpenPGP certificates.
1016
#priority = u':'.join((u"NONE", u"+VERS-TLS1.1",
1017
# u"+AES-256-CBC", u"+SHA1",
1018
# u"+COMP-NULL", u"+CTYPE-OPENPGP",
1020
# Use a fallback default, since this MUST be set.
1021
priority = self.server.gnutls_priority
1022
if priority is None:
1023
priority = u"NORMAL"
1024
(gnutls.library.functions
1025
.gnutls_priority_set_direct(session._c_object,
1028
# Start communication using the Mandos protocol
1029
# Get protocol number
1030
line = self.request.makefile().readline()
1031
logger.debug(u"Protocol version: %r", line)
1033
if int(line.strip().split()[0]) > 1:
1035
except (ValueError, IndexError, RuntimeError), error:
1036
logger.error(u"Unknown protocol version: %s", error)
1039
# Start GnuTLS connection
1042
except gnutls.errors.GNUTLSError, error:
1043
logger.warning(u"Handshake failed: %s", error)
1044
# Do not run session.bye() here: the session is not
1045
# established. Just abandon the request.
1047
logger.debug(u"Handshake succeeded")
1050
fpr = self.fingerprint(self.peer_certificate
1052
except (TypeError, gnutls.errors.GNUTLSError), error:
1053
logger.warning(u"Bad certificate: %s", error)
1055
logger.debug(u"Fingerprint: %s", fpr)
1057
for c in self.server.clients:
1058
if c.fingerprint == fpr:
1062
ipc.write(u"NOTFOUND %s %s\n"
1063
% (fpr, unicode(self.client_address)))
1066
class ClientProxy(object):
1067
"""Client proxy object. Not for calling methods."""
1068
def __init__(self, client):
1069
self.client = client
1070
def __getattr__(self, name):
1071
if name.startswith("ipc_"):
1073
ipc.write("%s %s\n" % (name[4:].upper(),
1076
if not hasattr(self.client, name):
1077
raise AttributeError
1078
ipc.write(u"GETATTR %s %s\n"
1079
% (name, self.client.fingerprint))
1080
return pickle.load(ipc_return)
1081
clientproxy = ClientProxy(client)
1082
# Have to check if client.enabled, since it is
1083
# possible that the client was disabled since the
1084
# GnuTLS session was established.
1085
if not clientproxy.enabled:
1086
clientproxy.ipc_disabled()
1089
clientproxy.ipc_sending()
1091
while sent_size < len(client.secret):
1092
sent = session.send(client.secret[sent_size:])
1093
logger.debug(u"Sent: %d, remaining: %d",
1094
sent, len(client.secret)
1095
- (sent_size + sent))
1101
def peer_certificate(session):
1102
"Return the peer's OpenPGP certificate as a bytestring"
1103
# If not an OpenPGP certificate...
1104
if (gnutls.library.functions
1105
.gnutls_certificate_type_get(session._c_object)
1106
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1107
# ...do the normal thing
1108
return session.peer_certificate
1109
list_size = ctypes.c_uint(1)
1110
cert_list = (gnutls.library.functions
1111
.gnutls_certificate_get_peers
1112
(session._c_object, ctypes.byref(list_size)))
1113
if not bool(cert_list) and list_size.value != 0:
1114
raise gnutls.errors.GNUTLSError(u"error getting peer"
1116
if list_size.value == 0:
1119
return ctypes.string_at(cert.data, cert.size)
1122
def fingerprint(openpgp):
1123
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1124
# New GnuTLS "datum" with the OpenPGP public key
1125
datum = (gnutls.library.types
1126
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1129
ctypes.c_uint(len(openpgp))))
1130
# New empty GnuTLS certificate
1131
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1132
(gnutls.library.functions
1133
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1134
# Import the OpenPGP public key into the certificate
1135
(gnutls.library.functions
1136
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1137
gnutls.library.constants
1138
.GNUTLS_OPENPGP_FMT_RAW))
1139
# Verify the self signature in the key
1140
crtverify = ctypes.c_uint()
1141
(gnutls.library.functions
1142
.gnutls_openpgp_crt_verify_self(crt, 0,
1143
ctypes.byref(crtverify)))
1144
if crtverify.value != 0:
1145
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1146
raise (gnutls.errors.CertificateSecurityError
1148
# New buffer for the fingerprint
1149
buf = ctypes.create_string_buffer(20)
1150
buf_len = ctypes.c_size_t()
1151
# Get the fingerprint from the certificate into the buffer
1152
(gnutls.library.functions
1153
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1154
ctypes.byref(buf_len)))
1155
# Deinit the certificate
1156
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1157
# Convert the buffer to a Python bytestring
1158
fpr = ctypes.string_at(buf, buf_len.value)
1159
# Convert the bytestring to hexadecimal notation
1160
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
1164
class ForkingMixInWithPipes(socketserver.ForkingMixIn, object):
1165
"""Like socketserver.ForkingMixIn, but also pass a pipe pair."""
1166
def process_request(self, request, client_address):
1167
"""Overrides and wraps the original process_request().
1169
This function creates a new pipe in self.pipe
1171
# Child writes to child_pipe
1172
self.child_pipe = map(os.fdopen, os.pipe(), u"rw", (1, 0))
1173
# Parent writes to parent_pipe
1174
self.parent_pipe = map(os.fdopen, os.pipe(), u"rw", (1, 0))
1175
super(ForkingMixInWithPipes,
1176
self).process_request(request, client_address)
1177
# Close unused ends for parent
1178
self.parent_pipe[0].close() # close read end
1179
self.child_pipe[1].close() # close write end
1180
self.add_pipe_fds(self.child_pipe[0], self.parent_pipe[1])
1181
def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1182
"""Dummy function; override as necessary"""
1183
child_pipe_fd.close()
1184
parent_pipe_fd.close()
1187
class IPv6_TCPServer(ForkingMixInWithPipes,
1188
socketserver.TCPServer, object):
1189
"""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.
1192
enabled: Boolean; whether this server is activated yet
1193
interface: None or a network interface name (string)
1194
use_ipv6: Boolean; to use IPv6 or not
429
options: Command line options
430
clients: Set() of Client objects
1196
def __init__(self, server_address, RequestHandlerClass,
1197
interface=None, use_ipv6=True):
1198
self.interface = interface
1200
self.address_family = socket.AF_INET6
1201
socketserver.TCPServer.__init__(self, server_address,
1202
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)
1203
441
def server_bind(self):
1204
442
"""This overrides the normal server_bind() function
1205
443
to bind to an interface if one was specified, and also NOT to
1206
444
bind to an address or port if they were not specified."""
1207
if self.interface is not None:
1208
if SO_BINDTODEVICE is None:
1209
logger.error(u"SO_BINDTODEVICE does not exist;"
1210
u" cannot bind to interface %s",
1214
self.socket.setsockopt(socket.SOL_SOCKET,
1218
except socket.error, error:
1219
if error[0] == errno.EPERM:
1220
logger.error(u"No permission to"
1221
u" bind to interface %s",
1223
elif error[0] == errno.ENOPROTOOPT:
1224
logger.error(u"SO_BINDTODEVICE not available;"
1225
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)
1229
460
# Only bind(2) the socket if we really need to.
1230
461
if self.server_address[0] or self.server_address[1]:
1231
462
if not self.server_address[0]:
1232
if self.address_family == socket.AF_INET6:
1233
any_address = u"::" # in6addr_any
1235
any_address = socket.INADDR_ANY
1236
self.server_address = (any_address,
464
self.server_address = (in6addr_any,
1237
465
self.server_address[1])
1238
elif not self.server_address[1]:
466
elif self.server_address[1] is None:
1239
467
self.server_address = (self.server_address[0],
1241
# if self.interface:
1242
# self.server_address = (self.server_address[0],
1247
return socketserver.TCPServer.server_bind(self)
1250
class MandosServer(IPv6_TCPServer):
1254
clients: set of Client objects
1255
gnutls_priority GnuTLS priority string
1256
use_dbus: Boolean; to emit D-Bus signals or not
1258
Assumes a gobject.MainLoop event loop.
1260
def __init__(self, server_address, RequestHandlerClass,
1261
interface=None, use_ipv6=True, clients=None,
1262
gnutls_priority=None, use_dbus=True):
1263
self.enabled = False
1264
self.clients = clients
1265
if self.clients is None:
1266
self.clients = set()
1267
self.use_dbus = use_dbus
1268
self.gnutls_priority = gnutls_priority
1269
IPv6_TCPServer.__init__(self, server_address,
1270
RequestHandlerClass,
1271
interface = interface,
1272
use_ipv6 = use_ipv6)
1273
def server_activate(self):
1275
return socketserver.TCPServer.server_activate(self)
1278
def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1279
# Call "handle_ipc" for both data and EOF events
1280
gobject.io_add_watch(child_pipe_fd.fileno(),
1281
gobject.IO_IN | gobject.IO_HUP,
1282
functools.partial(self.handle_ipc,
1283
reply = parent_pipe_fd,
1284
sender= child_pipe_fd))
1285
def handle_ipc(self, source, condition, reply=None, sender=None):
1287
gobject.IO_IN: u"IN", # There is data to read.
1288
gobject.IO_OUT: u"OUT", # Data can be written (without
1290
gobject.IO_PRI: u"PRI", # There is urgent data to read.
1291
gobject.IO_ERR: u"ERR", # Error condition.
1292
gobject.IO_HUP: u"HUP" # Hung up (the connection has been
1293
# broken, usually for pipes and
1296
conditions_string = ' | '.join(name
1298
condition_names.iteritems()
1299
if cond & condition)
1300
logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1303
# Read a line from the file object
1304
cmdline = sender.readline()
1305
if not cmdline: # Empty line means end of file
1306
# close the IPC pipes
1310
# Stop calling this function
1313
logger.debug(u"IPC command: %r", cmdline)
1315
# Parse and act on command
1316
cmd, args = cmdline.rstrip(u"\r\n").split(None, 1)
1318
if cmd == u"NOTFOUND":
1319
fpr, address = args.split(None, 1)
1320
logger.warning(u"Client not found for fingerprint: %s, ad"
1321
u"dress: %s", fpr, address)
1324
mandos_dbus_service.ClientNotFound(fpr, address)
1325
elif cmd == u"DISABLED":
1326
for client in self.clients:
1327
if client.name == args:
1328
logger.warning(u"Client %s is disabled", args)
1334
logger.error(u"Unknown client %s is disabled", args)
1335
elif cmd == u"SENDING":
1336
for client in self.clients:
1337
if client.name == args:
1338
logger.info(u"Sending secret to %s", client.name)
1345
logger.error(u"Sending secret to unknown client %s",
1347
elif cmd == u"GETATTR":
1348
attr_name, fpr = args.split(None, 1)
1349
for client in self.clients:
1350
if client.fingerprint == fpr:
1351
attr_value = getattr(client, attr_name, None)
1352
logger.debug("IPC reply: %r", attr_value)
1353
pickle.dump(attr_value, reply)
1356
logger.error(u"Client %s on address %s requesting "
1357
u"attribute %s not found", fpr, address,
1359
pickle.dump(None, reply)
1361
logger.error(u"Unknown IPC command: %r", cmdline)
1363
# Keep calling this function
469
return super(type(self), self).server_bind()
1367
472
def string_to_delta(interval):
1368
473
"""Parse a string and return a datetime.timedelta
1370
>>> string_to_delta(u'7d')
475
>>> string_to_delta('7d')
1371
476
datetime.timedelta(7)
1372
>>> string_to_delta(u'60s')
477
>>> string_to_delta('60s')
1373
478
datetime.timedelta(0, 60)
1374
>>> string_to_delta(u'60m')
479
>>> string_to_delta('60m')
1375
480
datetime.timedelta(0, 3600)
1376
>>> string_to_delta(u'24h')
481
>>> string_to_delta('24h')
1377
482
datetime.timedelta(1)
1378
483
>>> string_to_delta(u'1w')
1379
484
datetime.timedelta(7)
1380
>>> string_to_delta(u'5m 30s')
1381
datetime.timedelta(0, 330)
1383
timevalue = datetime.timedelta(0)
1384
for s in interval.split():
1386
suffix = unicode(s[-1])
1389
delta = datetime.timedelta(value)
1390
elif suffix == u"s":
1391
delta = datetime.timedelta(0, value)
1392
elif suffix == u"m":
1393
delta = datetime.timedelta(0, 0, 0, 0, value)
1394
elif suffix == u"h":
1395
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
1396
elif suffix == u"w":
1397
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1399
raise ValueError(u"Unknown suffix %r" % suffix)
1400
except (ValueError, IndexError), e:
1401
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",
1406
576
def if_nametoindex(interface):
1407
"""Call the C function if_nametoindex(), or equivalent
1409
Note: This function cannot accept a unicode string."""
1410
global if_nametoindex
577
"""Call the C function if_nametoindex()"""
1412
if_nametoindex = (ctypes.cdll.LoadLibrary
1413
(ctypes.util.find_library(u"c"))
579
libc = ctypes.cdll.LoadLibrary("libc.so.6")
580
return libc.if_nametoindex(interface)
1415
581
except (OSError, AttributeError):
1416
logger.warning(u"Doing if_nametoindex the hard way")
1417
def if_nametoindex(interface):
1418
"Get an interface index the hard way, i.e. using fcntl()"
1419
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
1420
with contextlib.closing(socket.socket()) as s:
1421
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1422
struct.pack(str(u"16s16x"),
1424
interface_index = struct.unpack(str(u"I"),
1426
return interface_index
1427
return if_nametoindex(interface)
1430
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):
1431
596
"""See daemon(3). Standard BSD Unix function.
1433
597
This should really exist as os.daemon, but it doesn't (yet)."""
1442
604
# Close all standard open file descriptors
1443
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
605
null = os.open("/dev/null", os.O_NOCTTY | os.O_RDWR)
1444
606
if not stat.S_ISCHR(os.fstat(null).st_mode):
1445
607
raise OSError(errno.ENODEV,
1446
u"%s not a character device"
608
"/dev/null not a character device")
1448
609
os.dup2(null, sys.stdin.fileno())
1449
610
os.dup2(null, sys.stdout.fileno())
1450
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:
1457
##################################################################
1458
# Parsing of options, both command line and config file
1460
parser = optparse.OptionParser(version = "%%prog %s" % version)
1461
parser.add_option("-i", u"--interface", type=u"string",
1462
metavar="IF", help=u"Bind to interface IF")
1463
parser.add_option("-a", u"--address", type=u"string",
1464
help=u"Address to listen for requests on")
1465
parser.add_option("-p", u"--port", type=u"int",
1466
help=u"Port number to receive requests on")
1467
parser.add_option("--check", action=u"store_true",
1468
help=u"Run self-test")
1469
parser.add_option("--debug", action=u"store_true",
1470
help=u"Debug mode; run in foreground and log to"
1472
parser.add_option("--priority", type=u"string", help=u"GnuTLS"
1473
u" priority string (see GnuTLS documentation)")
1474
parser.add_option("--servicename", type=u"string",
1475
metavar=u"NAME", help=u"Zeroconf service name")
1476
parser.add_option("--configdir", type=u"string",
1477
default=u"/etc/mandos", metavar=u"DIR",
1478
help=u"Directory to search for configuration"
1480
parser.add_option("--no-dbus", action=u"store_false",
1481
dest=u"use_dbus", help=u"Do not provide D-Bus"
1482
u" system bus interface")
1483
parser.add_option("--no-ipv6", action=u"store_false",
1484
dest=u"use_ipv6", help=u"Do not use IPv6")
1485
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()
1487
651
if options.check:
1489
653
doctest.testmod()
1492
# Default values for config file for server-global settings
1493
server_defaults = { u"interface": u"",
1498
u"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1499
u"servicename": u"Mandos",
1500
u"use_dbus": u"True",
1501
u"use_ipv6": u"True",
1504
# Parse config file for server-global settings
1505
server_config = configparser.SafeConfigParser(server_defaults)
1507
server_config.read(os.path.join(options.configdir,
1509
# Convert the SafeConfigParser object to a dict
1510
server_settings = server_config.defaults()
1511
# Use the appropriate methods on the non-string config options
1512
for option in (u"debug", u"use_dbus", u"use_ipv6"):
1513
server_settings[option] = server_config.getboolean(u"DEFAULT",
1515
if server_settings["port"]:
1516
server_settings["port"] = server_config.getint(u"DEFAULT",
1520
# Override the settings from the config file with command line
1522
for option in (u"interface", u"address", u"port", u"debug",
1523
u"priority", u"servicename", u"configdir",
1524
u"use_dbus", u"use_ipv6"):
1525
value = getattr(options, option)
1526
if value is not None:
1527
server_settings[option] = value
1529
# Force all strings to be unicode
1530
for option in server_settings.keys():
1531
if type(server_settings[option]) is str:
1532
server_settings[option] = unicode(server_settings[option])
1533
# Now we have our good server settings in "server_settings"
1535
##################################################################
1538
debug = server_settings[u"debug"]
1539
use_dbus = server_settings[u"use_dbus"]
1540
use_ipv6 = server_settings[u"use_ipv6"]
1543
syslogger.setLevel(logging.WARNING)
1544
console.setLevel(logging.WARNING)
1546
if server_settings[u"servicename"] != u"Mandos":
1547
syslogger.setFormatter(logging.Formatter
1548
(u'Mandos (%s) [%%(process)d]:'
1549
u' %%(levelname)s: %%(message)s'
1550
% server_settings[u"servicename"]))
1552
# Parse config file with clients
1553
client_defaults = { u"timeout": u"1h",
1555
u"checker": u"fping -q -- %%(host)s",
1558
client_config = configparser.SafeConfigParser(client_defaults)
1559
client_config.read(os.path.join(server_settings[u"configdir"],
1562
global mandos_dbus_service
1563
mandos_dbus_service = None
1565
tcp_server = MandosServer((server_settings[u"address"],
1566
server_settings[u"port"]),
1568
interface=server_settings[u"interface"],
1571
server_settings[u"priority"],
1573
pidfilename = u"/var/run/mandos.pid"
1575
pidfile = open(pidfilename, u"w")
1577
logger.error(u"Could not open file %r", pidfilename)
1580
uid = pwd.getpwnam(u"_mandos").pw_uid
1581
gid = pwd.getpwnam(u"_mandos").pw_gid
1584
uid = pwd.getpwnam(u"mandos").pw_uid
1585
gid = pwd.getpwnam(u"mandos").pw_gid
1588
uid = pwd.getpwnam(u"nobody").pw_uid
1589
gid = pwd.getpwnam(u"nobody").pw_gid
1596
except OSError, error:
1597
if error[0] != errno.EPERM:
1600
# Enable all possible GnuTLS debugging
1602
# "Use a log level over 10 to enable all debugging options."
1604
gnutls.library.functions.gnutls_global_set_log_level(11)
1606
@gnutls.library.types.gnutls_log_func
1607
def debug_gnutls(level, string):
1608
logger.debug(u"GnuTLS: %s", string[:-1])
1610
(gnutls.library.functions
1611
.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")
1613
672
global main_loop
1614
675
# From the Avahi example code
1615
676
DBusGMainLoop(set_as_default=True )
1616
677
main_loop = gobject.MainLoop()
1617
678
bus = dbus.SystemBus()
679
server = dbus.Interface(
680
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
681
avahi.DBUS_INTERFACE_SERVER )
1618
682
# End of Avahi example code
1621
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos",
1622
bus, do_not_queue=True)
1623
except dbus.exceptions.NameExistsException, e:
1624
logger.error(unicode(e) + u", disabling D-Bus")
1626
server_settings[u"use_dbus"] = False
1627
tcp_server.use_dbus = False
1628
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1629
service = AvahiService(name = server_settings[u"servicename"],
1630
servicetype = u"_mandos._tcp",
1631
protocol = protocol, bus = bus)
1632
if server_settings["interface"]:
1633
service.interface = (if_nametoindex
1634
(str(server_settings[u"interface"])))
1636
client_class = Client
1638
client_class = functools.partial(ClientDBus, bus = bus)
1639
tcp_server.clients.update(set(
1640
client_class(name = section,
1641
config= dict(client_config.items(section)))
1642
for section in client_config.sections()))
1643
if not tcp_server.clients:
1644
logger.warning(u"No clients defined")
684
debug = options.debug
1647
# Redirect stdin so all checkers get /dev/null
1648
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1649
os.dup2(null, sys.stdin.fileno())
1653
# No console logging
1654
logger.removeHandler(console)
1655
# Close all input and output, do double fork, etc.
1661
pidfile.write(str(pid) + "\n")
1664
logger.error(u"Could not write to file %r with PID %d",
1667
# "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()))
1672
signal.signal(signal.SIGINT, signal.SIG_IGN)
1673
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1674
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1677
class MandosDBusService(dbus.service.Object):
1678
"""A D-Bus proxy object"""
1680
dbus.service.Object.__init__(self, bus, u"/")
1681
_interface = u"se.bsnet.fukt.Mandos"
1683
@dbus.service.signal(_interface, signature=u"o")
1684
def ClientAdded(self, objpath):
1688
@dbus.service.signal(_interface, signature=u"ss")
1689
def ClientNotFound(self, fingerprint, address):
1693
@dbus.service.signal(_interface, signature=u"os")
1694
def ClientRemoved(self, objpath, name):
1698
@dbus.service.method(_interface, out_signature=u"ao")
1699
def GetAllClients(self):
1701
return dbus.Array(c.dbus_object_path
1702
for c in tcp_server.clients)
1704
@dbus.service.method(_interface,
1705
out_signature=u"a{oa{sv}}")
1706
def GetAllClientsWithProperties(self):
1708
return dbus.Dictionary(
1709
((c.dbus_object_path, c.GetAll(u""))
1710
for c in tcp_server.clients),
1711
signature=u"oa{sv}")
1713
@dbus.service.method(_interface, in_signature=u"o")
1714
def RemoveClient(self, object_path):
1716
for c in tcp_server.clients:
1717
if c.dbus_object_path == object_path:
1718
tcp_server.clients.remove(c)
1719
c.remove_from_connection()
1720
# Don't signal anything except ClientRemoved
1721
c.disable(quiet=True)
1723
self.ClientRemoved(object_path, c.name)
1725
raise KeyError(object_path)
1729
mandos_dbus_service = MandosDBusService()
1732
711
"Cleanup function; run on exit"
1735
while tcp_server.clients:
1736
client = tcp_server.clients.pop()
1738
client.remove_from_connection()
1739
client.disable_hook = None
1740
# Don't signal anything except ClientRemoved
1741
client.disable(quiet=True)
1744
mandos_dbus_service.ClientRemoved(client.dbus_object_path,
1747
atexit.register(cleanup)
1749
for client in tcp_server.clients:
1752
mandos_dbus_service.ClientAdded(client.dbus_object_path)
1756
tcp_server.server_activate()
1758
# Find out what port we got
1759
service.port = tcp_server.socket.getsockname()[1]
1761
logger.info(u"Now listening on address %r, port %d,"
1762
" flowinfo %d, scope_id %d"
1763
% tcp_server.socket.getsockname())
1765
logger.info(u"Now listening on address %r, port %d"
1766
% tcp_server.socket.getsockname())
1768
#service.interface = tcp_server.socket.getsockname()[3]
1771
713
# From the Avahi example code
1774
except dbus.exceptions.DBusException, error:
1775
logger.critical(u"DBusException: %s", error)
714
if not group is None:
1778
717
# End of Avahi example code
1780
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1781
lambda *args, **kwargs:
1782
(tcp_server.handle_request
1783
(*args[2:], **kwargs) or True))
1785
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
1787
except AvahiError, error:
1788
logger.critical(u"AvahiError: %s", error)
1791
764
except KeyboardInterrupt:
1794
logger.debug(u"Server received KeyboardInterrupt")
1795
logger.debug(u"Server exiting")
1796
# Must run before the D-Bus bus name gets deregistered
1799
770
if __name__ == '__main__':