44
15
import gnutls.library.functions
45
16
import gnutls.library.constants
46
17
import gnutls.library.types
47
import ConfigParser as configparser
56
import logging.handlers
58
from contextlib import closing
67
31
from dbus.mainloop.glib import DBusGMainLoop
70
import xml.dom.minidom
74
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
75
except AttributeError:
77
from IN import SO_BINDTODEVICE
79
SO_BINDTODEVICE = None
84
logger = logging.Logger(u'mandos')
85
syslogger = (logging.handlers.SysLogHandler
86
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
87
address = "/dev/log"))
88
syslogger.setFormatter(logging.Formatter
89
(u'Mandos [%(process)d]: %(levelname)s:'
35
import logging.handlers
37
logger = logging.Logger('mandos')
38
syslogger = logging.handlers.SysLogHandler\
39
(facility = logging.handlers.SysLogHandler.LOG_DAEMON)
40
syslogger.setFormatter(logging.Formatter\
41
('%(levelname)s: %(message)s'))
91
42
logger.addHandler(syslogger)
93
console = logging.StreamHandler()
94
console.setFormatter(logging.Formatter(u'%(name)s [%(process)d]:'
97
logger.addHandler(console)
99
class AvahiError(Exception):
100
def __init__(self, value, *args, **kwargs):
102
super(AvahiError, self).__init__(value, *args, **kwargs)
103
def __unicode__(self):
104
return unicode(repr(self.value))
106
class AvahiServiceError(AvahiError):
109
class AvahiGroupError(AvahiError):
113
class AvahiService(object):
114
"""An Avahi (Zeroconf) service.
117
interface: integer; avahi.IF_UNSPEC or an interface index.
118
Used to optionally bind to the specified interface.
119
name: string; Example: u'Mandos'
120
type: string; Example: u'_mandos._tcp'.
121
See <http://www.dns-sd.org/ServiceTypes.html>
122
port: integer; what port to announce
123
TXT: list of strings; TXT record for the service
124
domain: string; Domain to publish on, default to .local if empty.
125
host: string; Host to publish records for, default is localhost
126
max_renames: integer; maximum number of renames
127
rename_count: integer; counter so we only rename after collisions
128
a sensible number of times
129
group: D-Bus Entry Group
131
bus: dbus.SystemBus()
133
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
134
servicetype = None, port = None, TXT = None,
135
domain = u"", host = u"", max_renames = 32768,
136
protocol = avahi.PROTO_UNSPEC, bus = None):
137
self.interface = interface
139
self.type = servicetype
141
self.TXT = TXT if TXT is not None else []
144
self.rename_count = 0
145
self.max_renames = max_renames
146
self.protocol = protocol
147
self.group = None # our entry group
151
"""Derived from the Avahi example code"""
152
if self.rename_count >= self.max_renames:
153
logger.critical(u"No suitable Zeroconf service name found"
154
u" after %i retries, exiting.",
156
raise AvahiServiceError(u"Too many renames")
157
self.name = self.server.GetAlternativeServiceName(self.name)
158
logger.info(u"Changing Zeroconf service name to %r ...",
160
syslogger.setFormatter(logging.Formatter
161
(u'Mandos (%s) [%%(process)d]:'
162
u' %%(levelname)s: %%(message)s'
166
self.rename_count += 1
168
"""Derived from the Avahi example code"""
169
if self.group is not None:
172
"""Derived from the Avahi example code"""
173
if self.group is None:
174
self.group = dbus.Interface(
175
self.bus.get_object(avahi.DBUS_NAME,
176
self.server.EntryGroupNew()),
177
avahi.DBUS_INTERFACE_ENTRY_GROUP)
178
self.group.connect_to_signal('StateChanged',
180
.entry_group_state_changed)
181
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
182
self.name, self.type)
183
self.group.AddService(
186
dbus.UInt32(0), # flags
187
self.name, self.type,
188
self.domain, self.host,
189
dbus.UInt16(self.port),
190
avahi.string_array_to_txt_array(self.TXT))
192
def entry_group_state_changed(self, state, error):
193
"""Derived from the Avahi example code"""
194
logger.debug(u"Avahi state change: %i", state)
196
if state == avahi.ENTRY_GROUP_ESTABLISHED:
197
logger.debug(u"Zeroconf service established.")
198
elif state == avahi.ENTRY_GROUP_COLLISION:
199
logger.warning(u"Zeroconf service name collision.")
201
elif state == avahi.ENTRY_GROUP_FAILURE:
202
logger.critical(u"Avahi: Error in group state changed %s",
204
raise AvahiGroupError(u"State changed: %s"
207
"""Derived from the Avahi example code"""
208
if self.group is not None:
211
def server_state_changed(self, state):
212
"""Derived from the Avahi example code"""
213
if state == avahi.SERVER_COLLISION:
214
logger.error(u"Zeroconf server name collision")
216
elif state == avahi.SERVER_RUNNING:
219
"""Derived from the Avahi example code"""
220
if self.server is None:
221
self.server = dbus.Interface(
222
self.bus.get_object(avahi.DBUS_NAME,
223
avahi.DBUS_PATH_SERVER),
224
avahi.DBUS_INTERFACE_SERVER)
225
self.server.connect_to_signal(u"StateChanged",
226
self.server_state_changed)
227
self.server_state_changed(self.server.GetState())
45
# This variable is used to optionally bind to a specified interface.
46
# It is a global variable to fit in with the other variables from the
47
# Avahi server example code.
48
serviceInterface = avahi.IF_UNSPEC
49
# From the Avahi server example code:
50
serviceName = "Mandos"
51
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
52
servicePort = None # Not known at startup
53
serviceTXT = [] # TXT record for the service
54
domain = "" # Domain to publish on, default to .local
55
host = "" # Host to publish records for, default to localhost
56
group = None #our entry group
57
rename_count = 12 # Counter so we only rename after collisions a
58
# sensible number of times
59
# End of Avahi example code
230
62
class Client(object):
231
63
"""A representation of a client host served by this server.
234
name: string; from the config file, used in log messages and
65
name: string; from the config file, used in log messages
236
66
fingerprint: string (40 or 32 hexadecimal digits); used to
237
67
uniquely identify the client
238
secret: bytestring; sent verbatim (over TLS) to client
239
host: string; available for use by the checker command
240
created: datetime.datetime(); (UTC) object creation
241
last_enabled: datetime.datetime(); (UTC)
243
last_checked_ok: datetime.datetime(); (UTC) or None
244
timeout: datetime.timedelta(); How long from last_checked_ok
245
until this client is invalid
246
interval: datetime.timedelta(); How often to start a new checker
247
disable_hook: If set, called by disable() as disable_hook(self)
248
checker: subprocess.Popen(); a running checker process used
249
to see if the client lives.
250
'None' if no process is running.
68
secret: bytestring; sent verbatim (over TLS) to client
69
fqdn: string (FQDN); available for use by the checker command
70
created: datetime.datetime()
71
last_seen: datetime.datetime() or None if not yet seen
72
timeout: datetime.timedelta(); How long from last_seen until
73
this client is invalid
74
interval: datetime.timedelta(); How often to start a new checker
75
stop_hook: If set, called by stop() as stop_hook(self)
76
checker: subprocess.Popen(); a running checker process used
77
to see if the client lives.
78
Is None if no process is running.
251
79
checker_initiator_tag: a gobject event source tag, or None
252
disable_initiator_tag: - '' -
80
stop_initiator_tag: - '' -
253
81
checker_callback_tag: - '' -
254
82
checker_command: string; External command which is run to check if
255
client lives. %() expansions are done at
83
client lives. %()s expansions are done at
256
84
runtime with vars(self) as dict, so that for
257
85
instance %(name)s can be used in the command.
258
current_checker_command: string; current running checker_command
87
_timeout: Real variable for 'timeout'
88
_interval: Real variable for 'interval'
89
_timeout_milliseconds: Used by gobject.timeout_add()
90
_interval_milliseconds: - '' -
262
def _timedelta_to_milliseconds(td):
263
"Convert a datetime.timedelta() to milliseconds"
264
return ((td.days * 24 * 60 * 60 * 1000)
265
+ (td.seconds * 1000)
266
+ (td.microseconds // 1000))
268
def timeout_milliseconds(self):
269
"Return the 'timeout' attribute in milliseconds"
270
return self._timedelta_to_milliseconds(self.timeout)
272
def interval_milliseconds(self):
273
"Return the 'interval' attribute in milliseconds"
274
return self._timedelta_to_milliseconds(self.interval)
276
def __init__(self, name = None, disable_hook=None, config=None):
277
"""Note: the 'checker' key in 'config' sets the
278
'checker_command' attribute and *not* the 'checker'
92
def _set_timeout(self, timeout):
93
"Setter function for 'timeout' attribute"
94
self._timeout = timeout
95
self._timeout_milliseconds = ((self.timeout.days
96
* 24 * 60 * 60 * 1000)
97
+ (self.timeout.seconds * 1000)
98
+ (self.timeout.microseconds
100
timeout = property(lambda self: self._timeout,
103
def _set_interval(self, interval):
104
"Setter function for 'interval' attribute"
105
self._interval = interval
106
self._interval_milliseconds = ((self.interval.days
107
* 24 * 60 * 60 * 1000)
108
+ (self.interval.seconds
110
+ (self.interval.microseconds
112
interval = property(lambda self: self._interval,
115
def __init__(self, name=None, options=None, stop_hook=None,
116
fingerprint=None, secret=None, secfile=None,
117
fqdn=None, timeout=None, interval=-1, checker=None):
283
logger.debug(u"Creating client %r", self.name)
284
# Uppercase and remove spaces from fingerprint for later
285
# comparison purposes with return value from the fingerprint()
287
self.fingerprint = (config[u"fingerprint"].upper()
289
logger.debug(u" Fingerprint: %s", self.fingerprint)
290
if u"secret" in config:
291
self.secret = config[u"secret"].decode(u"base64")
292
elif u"secfile" in config:
293
with closing(open(os.path.expanduser
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
119
# Uppercase and remove spaces from fingerprint
120
# for later comparison purposes with return value of
121
# the fingerprint() function
122
self.fingerprint = fingerprint.upper().replace(u" ", u"")
124
self.secret = secret.decode(u"base64")
127
self.secret = sf.read()
130
raise RuntimeError(u"No secret or secfile for client %s"
132
self.fqdn = fqdn # string
133
self.created = datetime.datetime.now()
134
self.last_seen = None
136
self.timeout = options.timeout
138
self.timeout = string_to_delta(timeout)
140
self.interval = options.interval
142
self.interval = string_to_delta(interval)
143
self.stop_hook = stop_hook
309
144
self.checker = None
310
145
self.checker_initiator_tag = None
311
self.disable_initiator_tag = None
146
self.stop_initiator_tag = None
312
147
self.checker_callback_tag = None
313
self.checker_command = config[u"checker"]
314
self.current_checker_command = None
315
self.last_connect = None
318
"""Start this client's checker and timeout hooks"""
319
if getattr(self, u"enabled", False):
322
self.last_enabled = datetime.datetime.utcnow()
148
self.check_command = checker
150
"""Start this clients checker and timeout hooks"""
323
151
# Schedule a new checker to be started an 'interval' from now,
324
152
# 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(),
153
self.checker_initiator_tag = gobject.timeout_add\
154
(self._interval_milliseconds,
333
156
# Also start a new checker *right now*.
334
157
self.start_checker()
336
def disable(self, quiet=True):
337
"""Disable this client."""
338
if not getattr(self, "enabled", False):
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):
158
# Schedule a stop() when 'timeout' has passed
159
self.stop_initiator_tag = gobject.timeout_add\
160
(self._timeout_milliseconds,
164
The possibility that this client might be restarted is left
165
open, but not currently used."""
166
logger.debug(u"Stopping client %s", self.name)
168
if self.stop_initiator_tag:
169
gobject.source_remove(self.stop_initiator_tag)
170
self.stop_initiator_tag = None
171
if self.checker_initiator_tag:
346
172
gobject.source_remove(self.checker_initiator_tag)
347
173
self.checker_initiator_tag = None
348
174
self.stop_checker()
349
if self.disable_hook:
350
self.disable_hook(self)
352
177
# Do not run this again if called by a gobject.timeout_add
355
179
def __del__(self):
356
self.disable_hook = None
359
def checker_callback(self, pid, condition, command):
180
# Some code duplication here and in stop()
181
if hasattr(self, "stop_initiator_tag") \
182
and self.stop_initiator_tag:
183
gobject.source_remove(self.stop_initiator_tag)
184
self.stop_initiator_tag = None
185
if hasattr(self, "checker_initiator_tag") \
186
and self.checker_initiator_tag:
187
gobject.source_remove(self.checker_initiator_tag)
188
self.checker_initiator_tag = None
190
def checker_callback(self, pid, condition):
360
191
"""The checker has completed, so take appropriate actions."""
361
self.checker_callback_tag = 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",
192
now = datetime.datetime.now()
193
if os.WIFEXITED(condition) \
194
and (os.WEXITSTATUS(condition) == 0):
195
logger.debug(u"Checker for %(name)s succeeded",
198
gobject.source_remove(self.stop_initiator_tag)
199
self.stop_initiator_tag = gobject.timeout_add\
200
(self._timeout_milliseconds,
202
elif not os.WIFEXITED(condition):
373
203
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(),
206
logger.debug(u"Checker for %(name)s failed",
209
self.checker_callback_tag = None
388
210
def start_checker(self):
389
211
"""Start a new checker subprocess if one is not running.
391
212
If a checker already exists, leave it running and do
393
# The reason for not killing a running checker is that if we
394
# did that, then if a checker (for some reason) started
395
# running slowly and taking more than 'interval' time, the
396
# client would inevitably timeout, since no checker would get
397
# a chance to run to completion. If we instead leave running
398
# checkers alone, the checker would have to take more time
399
# than 'timeout' for the client to be declared invalid, which
400
# is as it should be.
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
416
214
if self.checker is None:
418
# In case checker_command has exactly one % operator
419
command = self.checker_command % self.host
216
command = self.check_command % self.fqdn
420
217
except TypeError:
421
# Escape attributes for the shell
422
escaped_attrs = dict((key,
423
re.escape(unicode(str(val),
218
escaped_attrs = dict((key, re.escape(str(val)))
427
220
vars(self).iteritems())
429
command = self.checker_command % escaped_attrs
222
command = self.check_command % escaped_attrs
430
223
except TypeError, error:
431
logger.error(u'Could not format string "%s":'
432
u' %s', self.checker_command, error)
224
logger.critical(u'Could not format string "%s":'
225
u' %s', self.check_command, error)
433
226
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:
228
logger.debug(u"Starting checker %r for %s",
230
self.checker = subprocess.\
232
close_fds=True, shell=True,
234
self.checker_callback_tag = gobject.child_watch_add\
236
self.checker_callback)
237
except subprocess.OSError, error:
456
238
logger.error(u"Failed to start subprocess: %s",
458
240
# Re-run this periodically if run by gobject.timeout_add
461
242
def stop_checker(self):
462
243
"""Force the checker process, if any, to stop."""
463
if self.checker_callback_tag:
464
gobject.source_remove(self.checker_callback_tag)
465
self.checker_callback_tag = None
466
if getattr(self, u"checker", None) is None:
244
if not hasattr(self, "checker") or self.checker is None:
468
logger.debug(u"Stopping checker for %(name)s", vars(self))
470
os.kill(self.checker.pid, signal.SIGTERM)
472
#if self.checker.poll() is None:
473
# os.kill(self.checker.pid, signal.SIGKILL)
474
except OSError, error:
475
if error.errno != errno.ESRCH: # No such process
246
gobject.source_remove(self.checker_callback_tag)
247
self.checker_callback_tag = None
248
os.kill(self.checker.pid, signal.SIGTERM)
249
if self.checker.poll() is None:
250
os.kill(self.checker.pid, signal.SIGKILL)
477
251
self.checker = None
479
def still_valid(self):
252
def still_valid(self, now=None):
480
253
"""Has the timeout not yet passed for this client?"""
481
if not getattr(self, u"enabled", False):
483
now = datetime.datetime.utcnow()
484
if self.last_checked_ok is None:
255
now = datetime.datetime.now()
256
if self.last_seen is None:
485
257
return now < (self.created + self.timeout)
487
return now < (self.last_checked_ok + self.timeout)
490
def dbus_service_property(dbus_interface, signature=u"v",
491
access=u"readwrite", byte_arrays=False):
492
"""Decorators for marking methods of a DBusObjectWithProperties to
493
become properties on the D-Bus.
495
The decorated method will be called with no arguments by "Get"
496
and with one argument by "Set".
498
The parameters, where they are supported, are the same as
499
dbus.service.method, except there is only "signature", since the
500
type from Get() and the type sent to Set() is the same.
503
func._dbus_is_property = True
504
func._dbus_interface = dbus_interface
505
func._dbus_signature = signature
506
func._dbus_access = access
507
func._dbus_name = func.__name__
508
if func._dbus_name.endswith(u"_dbus_property"):
509
func._dbus_name = func._dbus_name[:-14]
510
func._dbus_get_args_options = {u'byte_arrays': byte_arrays }
515
class DBusPropertyException(dbus.exceptions.DBusException):
516
"""A base class for D-Bus property-related exceptions
518
def __unicode__(self):
519
return unicode(str(self))
522
class DBusPropertyAccessException(DBusPropertyException):
523
"""A property's access permissions disallows an operation.
528
class DBusPropertyNotFound(DBusPropertyException):
529
"""An attempt was made to access a non-existing property.
534
class DBusObjectWithProperties(dbus.service.Object):
535
"""A D-Bus object with properties.
537
Classes inheriting from this can use the dbus_service_property
538
decorator to expose methods as D-Bus properties. It exposes the
539
standard Get(), Set(), and GetAll() methods on the D-Bus.
543
def _is_dbus_property(obj):
544
return getattr(obj, u"_dbus_is_property", False)
546
def _get_all_dbus_properties(self):
547
"""Returns a generator of (name, attribute) pairs
549
return ((prop._dbus_name, prop)
551
inspect.getmembers(self, self._is_dbus_property))
553
def _get_dbus_property(self, interface_name, property_name):
554
"""Returns a bound method if one exists which is a D-Bus
555
property with the specified name and interface.
557
for name in (property_name,
558
property_name + u"_dbus_property"):
559
prop = getattr(self, name, None)
561
or not self._is_dbus_property(prop)
562
or prop._dbus_name != property_name
563
or (interface_name and prop._dbus_interface
564
and interface_name != prop._dbus_interface)):
568
raise DBusPropertyNotFound(self.dbus_object_path + u":"
569
+ interface_name + u"."
572
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ss",
574
def Get(self, interface_name, property_name):
575
"""Standard D-Bus property Get() method, see D-Bus standard.
577
prop = self._get_dbus_property(interface_name, property_name)
578
if prop._dbus_access == u"write":
579
raise DBusPropertyAccessException(property_name)
581
if not hasattr(value, u"variant_level"):
583
return type(value)(value, variant_level=value.variant_level+1)
585
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ssv")
586
def Set(self, interface_name, property_name, value):
587
"""Standard D-Bus property Set() method, see D-Bus standard.
589
prop = self._get_dbus_property(interface_name, property_name)
590
if prop._dbus_access == u"read":
591
raise DBusPropertyAccessException(property_name)
592
if prop._dbus_get_args_options[u"byte_arrays"]:
593
value = dbus.ByteArray(''.join(unichr(byte)
597
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"s",
598
out_signature=u"a{sv}")
599
def GetAll(self, interface_name):
600
"""Standard D-Bus property GetAll() method, see D-Bus
603
Note: Will not include properties with access="write".
606
for name, prop in self._get_all_dbus_properties():
608
and interface_name != prop._dbus_interface):
609
# Interface non-empty but did not match
611
# Ignore write-only properties
612
if prop._dbus_access == u"write":
615
if not hasattr(value, u"variant_level"):
618
all[name] = type(value)(value, variant_level=
619
value.variant_level+1)
620
return dbus.Dictionary(all, signature=u"sv")
622
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
624
path_keyword='object_path',
625
connection_keyword='connection')
626
def Introspect(self, object_path, connection):
627
"""Standard D-Bus method, overloaded to insert property tags.
629
xmlstring = dbus.service.Object.Introspect(self, object_path,
632
document = xml.dom.minidom.parseString(xmlstring)
633
def make_tag(document, name, prop):
634
e = document.createElement(u"property")
635
e.setAttribute(u"name", name)
636
e.setAttribute(u"type", prop._dbus_signature)
637
e.setAttribute(u"access", prop._dbus_access)
639
for if_tag in document.getElementsByTagName(u"interface"):
640
for tag in (make_tag(document, name, prop)
642
in self._get_all_dbus_properties()
643
if prop._dbus_interface
644
== if_tag.getAttribute(u"name")):
645
if_tag.appendChild(tag)
646
# Add the names to the return values for the
647
# "org.freedesktop.DBus.Properties" methods
648
if (if_tag.getAttribute(u"name")
649
== u"org.freedesktop.DBus.Properties"):
650
for cn in if_tag.getElementsByTagName(u"method"):
651
if cn.getAttribute(u"name") == u"Get":
652
for arg in cn.getElementsByTagName(u"arg"):
653
if (arg.getAttribute(u"direction")
655
arg.setAttribute(u"name", u"value")
656
elif cn.getAttribute(u"name") == u"GetAll":
657
for arg in cn.getElementsByTagName(u"arg"):
658
if (arg.getAttribute(u"direction")
660
arg.setAttribute(u"name", u"props")
661
xmlstring = document.toxml(u"utf-8")
663
except (AttributeError, xml.dom.DOMException,
664
xml.parsers.expat.ExpatError), error:
665
logger.error(u"Failed to override Introspection method",
670
class ClientDBus(Client, DBusObjectWithProperties):
671
"""A Client class using D-Bus
674
dbus_object_path: dbus.ObjectPath
675
bus: dbus.SystemBus()
677
# dbus.service.Object doesn't use super(), so we can't either.
679
def __init__(self, bus = None, *args, **kwargs):
681
Client.__init__(self, *args, **kwargs)
682
# Only now, when this client is initialized, can it show up on
684
self.dbus_object_path = (dbus.ObjectPath
686
+ self.name.replace(u".", u"_")))
687
DBusObjectWithProperties.__init__(self, self.bus,
688
self.dbus_object_path)
691
def _datetime_to_dbus(dt, variant_level=0):
692
"""Convert a UTC datetime.datetime() to a D-Bus type."""
693
return dbus.String(dt.isoformat(),
694
variant_level=variant_level)
697
oldstate = getattr(self, u"enabled", False)
698
r = Client.enable(self)
699
if oldstate != self.enabled:
701
self.PropertyChanged(dbus.String(u"enabled"),
702
dbus.Boolean(True, variant_level=1))
703
self.PropertyChanged(
704
dbus.String(u"last_enabled"),
705
self._datetime_to_dbus(self.last_enabled,
709
def disable(self, quiet = False):
710
oldstate = getattr(self, u"enabled", False)
711
r = Client.disable(self, quiet=quiet)
712
if not quiet and oldstate != self.enabled:
714
self.PropertyChanged(dbus.String(u"enabled"),
715
dbus.Boolean(False, variant_level=1))
718
def __del__(self, *args, **kwargs):
720
self.remove_from_connection()
723
if hasattr(DBusObjectWithProperties, u"__del__"):
724
DBusObjectWithProperties.__del__(self, *args, **kwargs)
725
Client.__del__(self, *args, **kwargs)
727
def checker_callback(self, pid, condition, command,
729
self.checker_callback_tag = None
732
self.PropertyChanged(dbus.String(u"checker_running"),
733
dbus.Boolean(False, variant_level=1))
734
if os.WIFEXITED(condition):
735
exitstatus = os.WEXITSTATUS(condition)
737
self.CheckerCompleted(dbus.Int16(exitstatus),
738
dbus.Int64(condition),
739
dbus.String(command))
742
self.CheckerCompleted(dbus.Int16(-1),
743
dbus.Int64(condition),
744
dbus.String(command))
746
return Client.checker_callback(self, pid, condition, command,
749
def checked_ok(self, *args, **kwargs):
750
r = Client.checked_ok(self, *args, **kwargs)
752
self.PropertyChanged(
753
dbus.String(u"last_checked_ok"),
754
(self._datetime_to_dbus(self.last_checked_ok,
758
def start_checker(self, *args, **kwargs):
759
old_checker = self.checker
760
if self.checker is not None:
761
old_checker_pid = self.checker.pid
763
old_checker_pid = None
764
r = Client.start_checker(self, *args, **kwargs)
765
# Only if new checker process was started
766
if (self.checker is not None
767
and old_checker_pid != self.checker.pid):
769
self.CheckerStarted(self.current_checker_command)
770
self.PropertyChanged(
771
dbus.String(u"checker_running"),
772
dbus.Boolean(True, variant_level=1))
775
def stop_checker(self, *args, **kwargs):
776
old_checker = getattr(self, u"checker", None)
777
r = Client.stop_checker(self, *args, **kwargs)
778
if (old_checker is not None
779
and getattr(self, u"checker", None) is None):
780
self.PropertyChanged(dbus.String(u"checker_running"),
781
dbus.Boolean(False, variant_level=1))
784
## D-Bus methods & signals
785
_interface = u"se.bsnet.fukt.Mandos.Client"
788
@dbus.service.method(_interface)
790
return self.checked_ok()
792
# CheckerCompleted - signal
793
@dbus.service.signal(_interface, signature=u"nxs")
794
def CheckerCompleted(self, exitcode, waitstatus, command):
798
# CheckerStarted - signal
799
@dbus.service.signal(_interface, signature=u"s")
800
def CheckerStarted(self, command):
804
# PropertyChanged - signal
805
@dbus.service.signal(_interface, signature=u"sv")
806
def PropertyChanged(self, property, value):
811
@dbus.service.signal(_interface)
817
@dbus.service.signal(_interface)
823
@dbus.service.method(_interface)
828
# StartChecker - method
829
@dbus.service.method(_interface)
830
def StartChecker(self):
835
@dbus.service.method(_interface)
840
# StopChecker - method
841
@dbus.service.method(_interface)
842
def StopChecker(self):
846
@dbus_service_property(_interface, signature=u"s", access=u"read")
847
def name_dbus_property(self):
848
return dbus.String(self.name)
850
# fingerprint - property
851
@dbus_service_property(_interface, signature=u"s", access=u"read")
852
def fingerprint_dbus_property(self):
853
return dbus.String(self.fingerprint)
856
@dbus_service_property(_interface, signature=u"s",
858
def host_dbus_property(self, value=None):
859
if value is None: # get
860
return dbus.String(self.host)
863
self.PropertyChanged(dbus.String(u"host"),
864
dbus.String(value, variant_level=1))
867
@dbus_service_property(_interface, signature=u"s", access=u"read")
868
def created_dbus_property(self):
869
return dbus.String(self._datetime_to_dbus(self.created))
871
# last_enabled - property
872
@dbus_service_property(_interface, signature=u"s", access=u"read")
873
def last_enabled_dbus_property(self):
874
if self.last_enabled is None:
875
return dbus.String(u"")
876
return dbus.String(self._datetime_to_dbus(self.last_enabled))
879
@dbus_service_property(_interface, signature=u"b",
881
def enabled_dbus_property(self, value=None):
882
if value is None: # get
883
return dbus.Boolean(self.enabled)
889
# last_checked_ok - property
890
@dbus_service_property(_interface, signature=u"s",
892
def last_checked_ok_dbus_property(self, value=None):
893
if value is not None:
896
if self.last_checked_ok is None:
897
return dbus.String(u"")
898
return dbus.String(self._datetime_to_dbus(self
902
@dbus_service_property(_interface, signature=u"t",
904
def timeout_dbus_property(self, value=None):
905
if value is None: # get
906
return dbus.UInt64(self.timeout_milliseconds())
907
self.timeout = datetime.timedelta(0, 0, 0, value)
909
self.PropertyChanged(dbus.String(u"timeout"),
910
dbus.UInt64(value, variant_level=1))
911
if getattr(self, u"disable_initiator_tag", None) is None:
914
gobject.source_remove(self.disable_initiator_tag)
915
self.disable_initiator_tag = None
917
_timedelta_to_milliseconds((self
923
# The timeout has passed
926
self.disable_initiator_tag = (gobject.timeout_add
927
(time_to_die, self.disable))
929
# interval - property
930
@dbus_service_property(_interface, signature=u"t",
932
def interval_dbus_property(self, value=None):
933
if value is None: # get
934
return dbus.UInt64(self.interval_milliseconds())
935
self.interval = datetime.timedelta(0, 0, 0, value)
937
self.PropertyChanged(dbus.String(u"interval"),
938
dbus.UInt64(value, variant_level=1))
939
if getattr(self, u"checker_initiator_tag", None) is None:
941
# Reschedule checker run
942
gobject.source_remove(self.checker_initiator_tag)
943
self.checker_initiator_tag = (gobject.timeout_add
944
(value, self.start_checker))
945
self.start_checker() # Start one now, too
948
@dbus_service_property(_interface, signature=u"s",
950
def checker_dbus_property(self, value=None):
951
if value is None: # get
952
return dbus.String(self.checker_command)
953
self.checker_command = value
955
self.PropertyChanged(dbus.String(u"checker"),
956
dbus.String(self.checker_command,
959
# checker_running - property
960
@dbus_service_property(_interface, signature=u"b",
962
def checker_running_dbus_property(self, value=None):
963
if value is None: # get
964
return dbus.Boolean(self.checker is not None)
970
# object_path - property
971
@dbus_service_property(_interface, signature=u"o", access=u"read")
972
def object_path_dbus_property(self):
973
return self.dbus_object_path # is already a dbus.ObjectPath
976
@dbus_service_property(_interface, signature=u"ay",
977
access=u"write", byte_arrays=True)
978
def secret_dbus_property(self, value):
979
self.secret = str(value)
984
class ClientHandler(socketserver.BaseRequestHandler, object):
985
"""A class to handle client connections.
987
Instantiated once for each connection to handle it.
259
return now < (self.last_seen + self.timeout)
262
def peer_certificate(session):
263
"Return an OpenPGP data packet string for the peer's certificate"
264
# If not an OpenPGP certificate...
265
if gnutls.library.functions.gnutls_certificate_type_get\
266
(session._c_object) \
267
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
268
# ...do the normal thing
269
return session.peer_certificate
270
list_size = ctypes.c_uint()
271
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
272
(session._c_object, ctypes.byref(list_size))
273
if list_size.value == 0:
276
return ctypes.string_at(cert.data, cert.size)
279
def fingerprint(openpgp):
280
"Convert an OpenPGP data string to a hexdigit fingerprint string"
281
# New empty GnuTLS certificate
282
crt = gnutls.library.types.gnutls_openpgp_crt_t()
283
gnutls.library.functions.gnutls_openpgp_crt_init\
285
# New GnuTLS "datum" with the OpenPGP public key
286
datum = gnutls.library.types.gnutls_datum_t\
287
(ctypes.cast(ctypes.c_char_p(openpgp),
288
ctypes.POINTER(ctypes.c_ubyte)),
289
ctypes.c_uint(len(openpgp)))
290
# Import the OpenPGP public key into the certificate
291
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
294
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
295
# New buffer for the fingerprint
296
buffer = ctypes.create_string_buffer(20)
297
buffer_length = ctypes.c_size_t()
298
# Get the fingerprint from the certificate into the buffer
299
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
300
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
301
# Deinit the certificate
302
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
303
# Convert the buffer to a Python bytestring
304
fpr = ctypes.string_at(buffer, buffer_length.value)
305
# Convert the bytestring to hexadecimal notation
306
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
310
class tcp_handler(SocketServer.BaseRequestHandler, object):
311
"""A TCP request handler class.
312
Instantiated by IPv6_TCPServer for each request to handle it.
988
313
Note: This will run in its own forked process."""
990
315
def handle(self):
991
logger.info(u"TCP connection from: %s",
992
unicode(self.client_address))
993
logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
994
# Open IPC pipe to parent process
995
with closing(os.fdopen(self.server.pipe[1], u"w", 1)) as ipc:
996
session = (gnutls.connection
997
.ClientSession(self.request,
1001
line = self.request.makefile().readline()
1002
logger.debug(u"Protocol version: %r", line)
1004
if int(line.strip().split()[0]) > 1:
1006
except (ValueError, IndexError, RuntimeError), error:
1007
logger.error(u"Unknown protocol version: %s", error)
1010
# Note: gnutls.connection.X509Credentials is really a
1011
# generic GnuTLS certificate credentials object so long as
1012
# no X.509 keys are added to it. Therefore, we can use it
1013
# here despite using OpenPGP certificates.
1015
#priority = u':'.join((u"NONE", u"+VERS-TLS1.1",
1016
# u"+AES-256-CBC", u"+SHA1",
1017
# u"+COMP-NULL", u"+CTYPE-OPENPGP",
1019
# Use a fallback default, since this MUST be set.
1020
priority = self.server.gnutls_priority
1021
if priority is None:
1022
priority = u"NORMAL"
1023
(gnutls.library.functions
1024
.gnutls_priority_set_direct(session._c_object,
1029
except gnutls.errors.GNUTLSError, error:
1030
logger.warning(u"Handshake failed: %s", error)
1031
# Do not run session.bye() here: the session is not
1032
# established. Just abandon the request.
1034
logger.debug(u"Handshake succeeded")
1036
fpr = self.fingerprint(self.peer_certificate(session))
1037
except (TypeError, gnutls.errors.GNUTLSError), error:
1038
logger.warning(u"Bad certificate: %s", error)
1041
logger.debug(u"Fingerprint: %s", fpr)
1043
for c in self.server.clients:
1044
if c.fingerprint == fpr:
316
logger.debug(u"TCP connection from: %s",
317
unicode(self.client_address))
318
session = gnutls.connection.ClientSession(self.request,
322
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
323
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
325
priority = "SECURE256"
327
gnutls.library.functions.gnutls_priority_set_direct\
328
(session._c_object, priority, None);
332
except gnutls.errors.GNUTLSError, error:
333
logger.debug(u"Handshake failed: %s", error)
334
# Do not run session.bye() here: the session is not
335
# established. Just abandon the request.
338
fpr = fingerprint(peer_certificate(session))
339
except (TypeError, gnutls.errors.GNUTLSError), error:
340
logger.debug(u"Bad certificate: %s", error)
343
logger.debug(u"Fingerprint: %s", fpr)
346
if c.fingerprint == fpr:
349
# Have to check if client.still_valid(), since it is possible
350
# that the client timed out while establishing the GnuTLS
352
if (not client) or (not client.still_valid()):
354
logger.debug(u"Client %(name)s is invalid",
1048
ipc.write(u"NOTFOUND %s %s\n"
1049
% (fpr, unicode(self.client_address)))
1052
# Have to check if client.still_valid(), since it is
1053
# possible that the client timed out while establishing
1054
# the GnuTLS session.
1055
if not client.still_valid():
1056
ipc.write(u"INVALID %s\n" % client.name)
1059
ipc.write(u"SENDING %s\n" % client.name)
1061
while sent_size < len(client.secret):
1062
sent = session.send(client.secret[sent_size:])
1063
logger.debug(u"Sent: %d, remaining: %d",
1064
sent, len(client.secret)
1065
- (sent_size + sent))
357
logger.debug(u"Client not found for fingerprint: %s",
1070
def peer_certificate(session):
1071
"Return the peer's OpenPGP certificate as a bytestring"
1072
# If not an OpenPGP certificate...
1073
if (gnutls.library.functions
1074
.gnutls_certificate_type_get(session._c_object)
1075
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1076
# ...do the normal thing
1077
return session.peer_certificate
1078
list_size = ctypes.c_uint(1)
1079
cert_list = (gnutls.library.functions
1080
.gnutls_certificate_get_peers
1081
(session._c_object, ctypes.byref(list_size)))
1082
if not bool(cert_list) and list_size.value != 0:
1083
raise gnutls.errors.GNUTLSError(u"error getting peer"
1085
if list_size.value == 0:
1088
return ctypes.string_at(cert.data, cert.size)
1091
def fingerprint(openpgp):
1092
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1093
# New GnuTLS "datum" with the OpenPGP public key
1094
datum = (gnutls.library.types
1095
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1098
ctypes.c_uint(len(openpgp))))
1099
# New empty GnuTLS certificate
1100
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1101
(gnutls.library.functions
1102
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1103
# Import the OpenPGP public key into the certificate
1104
(gnutls.library.functions
1105
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1106
gnutls.library.constants
1107
.GNUTLS_OPENPGP_FMT_RAW))
1108
# Verify the self signature in the key
1109
crtverify = ctypes.c_uint()
1110
(gnutls.library.functions
1111
.gnutls_openpgp_crt_verify_self(crt, 0,
1112
ctypes.byref(crtverify)))
1113
if crtverify.value != 0:
1114
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1115
raise (gnutls.errors.CertificateSecurityError
1117
# New buffer for the fingerprint
1118
buf = ctypes.create_string_buffer(20)
1119
buf_len = ctypes.c_size_t()
1120
# Get the fingerprint from the certificate into the buffer
1121
(gnutls.library.functions
1122
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1123
ctypes.byref(buf_len)))
1124
# Deinit the certificate
1125
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1126
# Convert the buffer to a Python bytestring
1127
fpr = ctypes.string_at(buf, buf_len.value)
1128
# Convert the bytestring to hexadecimal notation
1129
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
1133
class ForkingMixInWithPipe(socketserver.ForkingMixIn, object):
1134
"""Like socketserver.ForkingMixIn, but also pass a pipe."""
1135
def process_request(self, request, client_address):
1136
"""Overrides and wraps the original process_request().
1138
This function creates a new pipe in self.pipe
1140
self.pipe = os.pipe()
1141
super(ForkingMixInWithPipe,
1142
self).process_request(request, client_address)
1143
os.close(self.pipe[1]) # close write end
1144
self.add_pipe(self.pipe[0])
1145
def add_pipe(self, pipe):
1146
"""Dummy function; override as necessary"""
1150
class IPv6_TCPServer(ForkingMixInWithPipe,
1151
socketserver.TCPServer, object):
1152
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
362
while sent_size < len(client.secret):
363
sent = session.send(client.secret[sent_size:])
364
logger.debug(u"Sent: %d, remaining: %d",
365
sent, len(client.secret)
366
- (sent_size + sent))
371
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
372
"""IPv6 TCP server. Accepts 'None' as address and/or port.
1155
enabled: Boolean; whether this server is activated yet
1156
interface: None or a network interface name (string)
1157
use_ipv6: Boolean; to use IPv6 or not
374
options: Command line options
375
clients: Set() of Client objects
1159
def __init__(self, server_address, RequestHandlerClass,
1160
interface=None, use_ipv6=True):
1161
self.interface = interface
1163
self.address_family = socket.AF_INET6
1164
socketserver.TCPServer.__init__(self, server_address,
1165
RequestHandlerClass)
377
address_family = socket.AF_INET6
378
def __init__(self, *args, **kwargs):
379
if "options" in kwargs:
380
self.options = kwargs["options"]
381
del kwargs["options"]
382
if "clients" in kwargs:
383
self.clients = kwargs["clients"]
384
del kwargs["clients"]
385
return super(type(self), self).__init__(*args, **kwargs)
1166
386
def server_bind(self):
1167
387
"""This overrides the normal server_bind() function
1168
388
to bind to an interface if one was specified, and also NOT to
1169
389
bind to an address or port if they were not specified."""
1170
if self.interface is not None:
1171
if SO_BINDTODEVICE is None:
1172
logger.error(u"SO_BINDTODEVICE does not exist;"
1173
u" cannot bind to interface %s",
1177
self.socket.setsockopt(socket.SOL_SOCKET,
1181
except socket.error, error:
1182
if error[0] == errno.EPERM:
1183
logger.error(u"No permission to"
1184
u" bind to interface %s",
1186
elif error[0] == errno.ENOPROTOOPT:
1187
logger.error(u"SO_BINDTODEVICE not available;"
1188
u" cannot bind to interface %s",
390
if self.options.interface:
391
if not hasattr(socket, "SO_BINDTODEVICE"):
392
# From /usr/include/asm-i486/socket.h
393
socket.SO_BINDTODEVICE = 25
395
self.socket.setsockopt(socket.SOL_SOCKET,
396
socket.SO_BINDTODEVICE,
397
self.options.interface)
398
except socket.error, error:
399
if error[0] == errno.EPERM:
400
logger.warning(u"No permission to"
401
u" bind to interface %s",
402
self.options.interface)
1192
405
# Only bind(2) the socket if we really need to.
1193
406
if self.server_address[0] or self.server_address[1]:
1194
407
if not self.server_address[0]:
1195
if self.address_family == socket.AF_INET6:
1196
any_address = u"::" # in6addr_any
1198
any_address = socket.INADDR_ANY
1199
self.server_address = (any_address,
409
self.server_address = (in6addr_any,
1200
410
self.server_address[1])
1201
elif not self.server_address[1]:
411
elif self.server_address[1] is None:
1202
412
self.server_address = (self.server_address[0],
1204
# if self.interface:
1205
# self.server_address = (self.server_address[0],
1210
return socketserver.TCPServer.server_bind(self)
1213
class MandosServer(IPv6_TCPServer):
1217
clients: set of Client objects
1218
gnutls_priority GnuTLS priority string
1219
use_dbus: Boolean; to emit D-Bus signals or not
1221
Assumes a gobject.MainLoop event loop.
1223
def __init__(self, server_address, RequestHandlerClass,
1224
interface=None, use_ipv6=True, clients=None,
1225
gnutls_priority=None, use_dbus=True):
1226
self.enabled = False
1227
self.clients = clients
1228
if self.clients is None:
1229
self.clients = set()
1230
self.use_dbus = use_dbus
1231
self.gnutls_priority = gnutls_priority
1232
IPv6_TCPServer.__init__(self, server_address,
1233
RequestHandlerClass,
1234
interface = interface,
1235
use_ipv6 = use_ipv6)
1236
def server_activate(self):
1238
return socketserver.TCPServer.server_activate(self)
1241
def add_pipe(self, pipe):
1242
# Call "handle_ipc" for both data and EOF events
1243
gobject.io_add_watch(pipe, gobject.IO_IN | gobject.IO_HUP,
1245
def handle_ipc(self, source, condition, file_objects={}):
1247
gobject.IO_IN: u"IN", # There is data to read.
1248
gobject.IO_OUT: u"OUT", # Data can be written (without
1250
gobject.IO_PRI: u"PRI", # There is urgent data to read.
1251
gobject.IO_ERR: u"ERR", # Error condition.
1252
gobject.IO_HUP: u"HUP" # Hung up (the connection has been
1253
# broken, usually for pipes and
1256
conditions_string = ' | '.join(name
1258
condition_names.iteritems()
1259
if cond & condition)
1260
logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1263
# Turn the pipe file descriptor into a Python file object
1264
if source not in file_objects:
1265
file_objects[source] = os.fdopen(source, u"r", 1)
1267
# Read a line from the file object
1268
cmdline = file_objects[source].readline()
1269
if not cmdline: # Empty line means end of file
1270
# close the IPC pipe
1271
file_objects[source].close()
1272
del file_objects[source]
1274
# Stop calling this function
1277
logger.debug(u"IPC command: %r", cmdline)
1279
# Parse and act on command
1280
cmd, args = cmdline.rstrip(u"\r\n").split(None, 1)
1282
if cmd == u"NOTFOUND":
1283
logger.warning(u"Client not found for fingerprint: %s",
1287
mandos_dbus_service.ClientNotFound(args)
1288
elif cmd == u"INVALID":
1289
for client in self.clients:
1290
if client.name == args:
1291
logger.warning(u"Client %s is invalid", args)
1297
logger.error(u"Unknown client %s is invalid", args)
1298
elif cmd == u"SENDING":
1299
for client in self.clients:
1300
if client.name == args:
1301
logger.info(u"Sending secret to %s", client.name)
1308
logger.error(u"Sending secret to unknown client %s",
1311
logger.error(u"Unknown IPC command: %r", cmdline)
1313
# Keep calling this function
414
return super(type(self), self).server_bind()
1317
417
def string_to_delta(interval):
1318
418
"""Parse a string and return a datetime.timedelta
1320
>>> string_to_delta(u'7d')
420
>>> string_to_delta('7d')
1321
421
datetime.timedelta(7)
1322
>>> string_to_delta(u'60s')
422
>>> string_to_delta('60s')
1323
423
datetime.timedelta(0, 60)
1324
>>> string_to_delta(u'60m')
424
>>> string_to_delta('60m')
1325
425
datetime.timedelta(0, 3600)
1326
>>> string_to_delta(u'24h')
426
>>> string_to_delta('24h')
1327
427
datetime.timedelta(1)
1328
428
>>> string_to_delta(u'1w')
1329
429
datetime.timedelta(7)
1330
>>> string_to_delta(u'5m 30s')
1331
datetime.timedelta(0, 330)
1333
timevalue = datetime.timedelta(0)
1334
for s in interval.split():
1336
suffix = unicode(s[-1])
1339
delta = datetime.timedelta(value)
1340
elif suffix == u"s":
1341
delta = datetime.timedelta(0, value)
1342
elif suffix == u"m":
1343
delta = datetime.timedelta(0, 0, 0, 0, value)
1344
elif suffix == u"h":
1345
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
1346
elif suffix == u"w":
1347
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1349
raise ValueError(u"Unknown suffix %r" % suffix)
1350
except (ValueError, IndexError), e:
1351
raise ValueError(e.message)
432
suffix=unicode(interval[-1])
433
value=int(interval[:-1])
435
delta = datetime.timedelta(value)
437
delta = datetime.timedelta(0, value)
439
delta = datetime.timedelta(0, 0, 0, 0, value)
441
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
443
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
446
except (ValueError, IndexError):
452
"""From the Avahi server example code"""
453
global group, serviceName, serviceType, servicePort, serviceTXT, \
456
group = dbus.Interface(
457
bus.get_object( avahi.DBUS_NAME,
458
server.EntryGroupNew()),
459
avahi.DBUS_INTERFACE_ENTRY_GROUP)
460
group.connect_to_signal('StateChanged',
461
entry_group_state_changed)
462
logger.debug(u"Adding service '%s' of type '%s' ...",
463
serviceName, serviceType)
466
serviceInterface, # interface
467
avahi.PROTO_INET6, # protocol
468
dbus.UInt32(0), # flags
469
serviceName, serviceType,
471
dbus.UInt16(servicePort),
472
avahi.string_array_to_txt_array(serviceTXT))
476
def remove_service():
477
"""From the Avahi server example code"""
480
if not group is None:
484
def server_state_changed(state):
485
"""From the Avahi server example code"""
486
if state == avahi.SERVER_COLLISION:
487
logger.warning(u"Server name collision")
489
elif state == avahi.SERVER_RUNNING:
493
def entry_group_state_changed(state, error):
494
"""From the Avahi server example code"""
495
global serviceName, server, rename_count
497
logger.debug(u"state change: %i", state)
499
if state == avahi.ENTRY_GROUP_ESTABLISHED:
500
logger.debug(u"Service established.")
501
elif state == avahi.ENTRY_GROUP_COLLISION:
503
rename_count = rename_count - 1
505
name = server.GetAlternativeServiceName(name)
506
logger.warning(u"Service name collision, "
507
u"changing name to '%s' ...", name)
512
logger.error(u"No suitable service name found after %i"
513
u" retries, exiting.", n_rename)
515
elif state == avahi.ENTRY_GROUP_FAILURE:
516
logger.error(u"Error in group state changed %s",
1356
521
def if_nametoindex(interface):
1357
"""Call the C function if_nametoindex(), or equivalent
1359
Note: This function cannot accept a unicode string."""
1360
global if_nametoindex
522
"""Call the C function if_nametoindex()"""
1362
if_nametoindex = (ctypes.cdll.LoadLibrary
1363
(ctypes.util.find_library(u"c"))
524
libc = ctypes.cdll.LoadLibrary("libc.so.6")
525
return libc.if_nametoindex(interface)
1365
526
except (OSError, AttributeError):
1366
logger.warning(u"Doing if_nametoindex the hard way")
1367
def if_nametoindex(interface):
1368
"Get an interface index the hard way, i.e. using fcntl()"
1369
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
1370
with closing(socket.socket()) as s:
1371
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1372
struct.pack(str(u"16s16x"),
1374
interface_index = struct.unpack(str(u"I"),
1376
return interface_index
1377
return if_nametoindex(interface)
1380
def daemon(nochdir = False, noclose = False):
527
if "struct" not in sys.modules:
529
if "fcntl" not in sys.modules:
531
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
533
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
534
struct.pack("16s16x", interface))
536
interface_index = struct.unpack("I", ifreq[16:20])[0]
537
return interface_index
540
def daemon(nochdir, noclose):
1381
541
"""See daemon(3). Standard BSD Unix function.
1383
542
This should really exist as os.daemon, but it doesn't (yet)."""
1392
549
# Close all standard open file descriptors
1393
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
550
null = os.open("/dev/null", os.O_NOCTTY | os.O_RDWR)
1394
551
if not stat.S_ISCHR(os.fstat(null).st_mode):
1395
552
raise OSError(errno.ENODEV,
1396
u"%s not a character device"
553
"/dev/null not a character device")
1398
554
os.dup2(null, sys.stdin.fileno())
1399
555
os.dup2(null, sys.stdout.fileno())
1400
556
os.dup2(null, sys.stderr.fileno())
1407
##################################################################
1408
# Parsing of options, both command line and config file
1410
parser = optparse.OptionParser(version = "%%prog %s" % version)
1411
parser.add_option("-i", u"--interface", type=u"string",
1412
metavar="IF", help=u"Bind to interface IF")
1413
parser.add_option("-a", u"--address", type=u"string",
1414
help=u"Address to listen for requests on")
1415
parser.add_option("-p", u"--port", type=u"int",
1416
help=u"Port number to receive requests on")
1417
parser.add_option("--check", action=u"store_true",
1418
help=u"Run self-test")
1419
parser.add_option("--debug", action=u"store_true",
1420
help=u"Debug mode; run in foreground and log to"
1422
parser.add_option("--priority", type=u"string", help=u"GnuTLS"
1423
u" priority string (see GnuTLS documentation)")
1424
parser.add_option("--servicename", type=u"string",
1425
metavar=u"NAME", help=u"Zeroconf service name")
1426
parser.add_option("--configdir", type=u"string",
1427
default=u"/etc/mandos", metavar=u"DIR",
1428
help=u"Directory to search for configuration"
1430
parser.add_option("--no-dbus", action=u"store_false",
1431
dest=u"use_dbus", help=u"Do not provide D-Bus"
1432
u" system bus interface")
1433
parser.add_option("--no-ipv6", action=u"store_false",
1434
dest=u"use_ipv6", help=u"Do not use IPv6")
1435
options = parser.parse_args()[0]
561
def killme(status = 0):
562
logger.debug("Stopping server with exit status %d", status)
564
if main_loop_started:
570
if __name__ == '__main__':
572
main_loop_started = False
573
parser = OptionParser()
574
parser.add_option("-i", "--interface", type="string",
575
default=None, metavar="IF",
576
help="Bind to interface IF")
577
parser.add_option("-p", "--port", type="int", default=None,
578
help="Port number to receive requests on")
579
parser.add_option("--timeout", type="string", # Parsed later
581
help="Amount of downtime allowed for clients")
582
parser.add_option("--interval", type="string", # Parsed later
584
help="How often to check that a client is up")
585
parser.add_option("--check", action="store_true", default=False,
586
help="Run self-test")
587
parser.add_option("--debug", action="store_true", default=False,
589
(options, args) = parser.parse_args()
1437
591
if options.check:
1439
593
doctest.testmod()
1442
# Default values for config file for server-global settings
1443
server_defaults = { u"interface": u"",
1448
u"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1449
u"servicename": u"Mandos",
1450
u"use_dbus": u"True",
1451
u"use_ipv6": u"True",
1454
# Parse config file for server-global settings
1455
server_config = configparser.SafeConfigParser(server_defaults)
1457
server_config.read(os.path.join(options.configdir,
1459
# Convert the SafeConfigParser object to a dict
1460
server_settings = server_config.defaults()
1461
# Use the appropriate methods on the non-string config options
1462
for option in (u"debug", u"use_dbus", u"use_ipv6"):
1463
server_settings[option] = server_config.getboolean(u"DEFAULT",
1465
if server_settings["port"]:
1466
server_settings["port"] = server_config.getint(u"DEFAULT",
1470
# Override the settings from the config file with command line
1472
for option in (u"interface", u"address", u"port", u"debug",
1473
u"priority", u"servicename", u"configdir",
1474
u"use_dbus", u"use_ipv6"):
1475
value = getattr(options, option)
1476
if value is not None:
1477
server_settings[option] = value
1479
# Force all strings to be unicode
1480
for option in server_settings.keys():
1481
if type(server_settings[option]) is str:
1482
server_settings[option] = unicode(server_settings[option])
1483
# Now we have our good server settings in "server_settings"
1485
##################################################################
1488
debug = server_settings[u"debug"]
1489
use_dbus = server_settings[u"use_dbus"]
1490
use_ipv6 = server_settings[u"use_ipv6"]
1493
syslogger.setLevel(logging.WARNING)
1494
console.setLevel(logging.WARNING)
1496
if server_settings[u"servicename"] != u"Mandos":
1497
syslogger.setFormatter(logging.Formatter
1498
(u'Mandos (%s) [%%(process)d]:'
1499
u' %%(levelname)s: %%(message)s'
1500
% server_settings[u"servicename"]))
1502
# Parse config file with clients
1503
client_defaults = { u"timeout": u"1h",
1505
u"checker": u"fping -q -- %%(host)s",
1508
client_config = configparser.SafeConfigParser(client_defaults)
1509
client_config.read(os.path.join(server_settings[u"configdir"],
1512
global mandos_dbus_service
1513
mandos_dbus_service = None
1515
tcp_server = MandosServer((server_settings[u"address"],
1516
server_settings[u"port"]),
1518
interface=server_settings[u"interface"],
1521
server_settings[u"priority"],
1523
pidfilename = u"/var/run/mandos.pid"
1525
pidfile = open(pidfilename, u"w")
1527
logger.error(u"Could not open file %r", pidfilename)
1530
uid = pwd.getpwnam(u"_mandos").pw_uid
1531
gid = pwd.getpwnam(u"_mandos").pw_gid
1534
uid = pwd.getpwnam(u"mandos").pw_uid
1535
gid = pwd.getpwnam(u"mandos").pw_gid
1538
uid = pwd.getpwnam(u"nobody").pw_uid
1539
gid = pwd.getpwnam(u"nobody").pw_gid
1546
except OSError, error:
1547
if error[0] != errno.EPERM:
1550
# Enable all possible GnuTLS debugging
1552
# "Use a log level over 10 to enable all debugging options."
1554
gnutls.library.functions.gnutls_global_set_log_level(11)
1556
@gnutls.library.types.gnutls_log_func
1557
def debug_gnutls(level, string):
1558
logger.debug(u"GnuTLS: %s", string[:-1])
1560
(gnutls.library.functions
1561
.gnutls_global_set_log_function(debug_gnutls))
1564
# From the Avahi example code
596
# Parse the time arguments
598
options.timeout = string_to_delta(options.timeout)
600
parser.error("option --timeout: Unparseable time")
602
options.interval = string_to_delta(options.interval)
604
parser.error("option --interval: Unparseable time")
607
defaults = { "checker": "fping -q -- %%(fqdn)s" }
608
client_config = ConfigParser.SafeConfigParser(defaults)
609
#client_config.readfp(open("secrets.conf"), "secrets.conf")
610
client_config.read("mandos-clients.conf")
612
# From the Avahi server example code
1565
613
DBusGMainLoop(set_as_default=True )
1566
614
main_loop = gobject.MainLoop()
1567
615
bus = dbus.SystemBus()
616
server = dbus.Interface(
617
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
618
avahi.DBUS_INTERFACE_SERVER )
1568
619
# End of Avahi example code
1571
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos",
1572
bus, do_not_queue=True)
1573
except dbus.exceptions.NameExistsException, e:
1574
logger.error(unicode(e) + u", disabling D-Bus")
1576
server_settings[u"use_dbus"] = False
1577
tcp_server.use_dbus = False
1578
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1579
service = AvahiService(name = server_settings[u"servicename"],
1580
servicetype = u"_mandos._tcp",
1581
protocol = protocol, bus = bus)
1582
if server_settings["interface"]:
1583
service.interface = (if_nametoindex
1584
(str(server_settings[u"interface"])))
1586
client_class = Client
1588
client_class = functools.partial(ClientDBus, bus = bus)
1589
tcp_server.clients.update(set(
1590
client_class(name = section,
1591
config= dict(client_config.items(section)))
1592
for section in client_config.sections()))
1593
if not tcp_server.clients:
1594
logger.warning(u"No clients defined")
621
debug = options.debug
1597
# Redirect stdin so all checkers get /dev/null
1598
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1599
os.dup2(null, sys.stdin.fileno())
1603
# No console logging
1604
logger.removeHandler(console)
1605
# Close all input and output, do double fork, etc.
1609
with closing(pidfile):
1611
pidfile.write(str(pid) + "\n")
1614
logger.error(u"Could not write to file %r with PID %d",
1617
# "pidfile" was never created
624
console = logging.StreamHandler()
625
# console.setLevel(logging.DEBUG)
626
console.setFormatter(logging.Formatter\
627
('%(levelname)s: %(message)s'))
628
logger.addHandler(console)
632
def remove_from_clients(client):
633
clients.remove(client)
635
logger.debug(u"No clients left, exiting")
638
clients.update(Set(Client(name=section, options=options,
639
stop_hook = remove_from_clients,
640
**(dict(client_config\
642
for section in client_config.sections()))
1622
signal.signal(signal.SIGINT, signal.SIG_IGN)
1623
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1624
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1627
class MandosDBusService(dbus.service.Object):
1628
"""A D-Bus proxy object"""
1630
dbus.service.Object.__init__(self, bus, u"/")
1631
_interface = u"se.bsnet.fukt.Mandos"
1633
@dbus.service.signal(_interface, signature=u"oa{sv}")
1634
def ClientAdded(self, objpath, properties):
1638
@dbus.service.signal(_interface, signature=u"s")
1639
def ClientNotFound(self, fingerprint):
1643
@dbus.service.signal(_interface, signature=u"os")
1644
def ClientRemoved(self, objpath, name):
1648
@dbus.service.method(_interface, out_signature=u"ao")
1649
def GetAllClients(self):
1651
return dbus.Array(c.dbus_object_path
1652
for c in tcp_server.clients)
1654
@dbus.service.method(_interface,
1655
out_signature=u"a{oa{sv}}")
1656
def GetAllClientsWithProperties(self):
1658
return dbus.Dictionary(
1659
((c.dbus_object_path, c.GetAll(u""))
1660
for c in tcp_server.clients),
1661
signature=u"oa{sv}")
1663
@dbus.service.method(_interface, in_signature=u"o")
1664
def RemoveClient(self, object_path):
1666
for c in tcp_server.clients:
1667
if c.dbus_object_path == object_path:
1668
tcp_server.clients.remove(c)
1669
c.remove_from_connection()
1670
# Don't signal anything except ClientRemoved
1671
c.disable(quiet=True)
1673
self.ClientRemoved(object_path, c.name)
1675
raise KeyError(object_path)
1679
mandos_dbus_service = MandosDBusService()
1682
648
"Cleanup function; run on exit"
650
# From the Avahi server example code
651
if not group is None:
654
# End of Avahi example code
1685
while tcp_server.clients:
1686
client = tcp_server.clients.pop()
1688
client.remove_from_connection()
1689
client.disable_hook = None
1690
# Don't signal anything except ClientRemoved
1691
client.disable(quiet=True)
1694
mandos_dbus_service.ClientRemoved(client.dbus_object_path,
656
for client in clients:
657
client.stop_hook = None
1697
660
atexit.register(cleanup)
1699
for client in tcp_server.clients:
1702
mandos_dbus_service.ClientAdded(client.dbus_object_path,
1707
tcp_server.server_activate()
1709
# Find out what port we got
1710
service.port = tcp_server.socket.getsockname()[1]
1712
logger.info(u"Now listening on address %r, port %d,"
1713
" flowinfo %d, scope_id %d"
1714
% tcp_server.socket.getsockname())
1716
logger.info(u"Now listening on address %r, port %d"
1717
% tcp_server.socket.getsockname())
1719
#service.interface = tcp_server.socket.getsockname()[3]
1722
# From the Avahi example code
1725
except dbus.exceptions.DBusException, error:
1726
logger.critical(u"DBusException: %s", error)
1729
# End of Avahi example code
1731
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1732
lambda *args, **kwargs:
1733
(tcp_server.handle_request
1734
(*args[2:], **kwargs) or True))
1736
logger.debug(u"Starting main loop")
663
signal.signal(signal.SIGINT, signal.SIG_IGN)
664
signal.signal(signal.SIGHUP, lambda signum, frame: killme())
665
signal.signal(signal.SIGTERM, lambda signum, frame: killme())
667
for client in clients:
670
tcp_server = IPv6_TCPServer((None, options.port),
674
# Find out what random port we got
675
servicePort = tcp_server.socket.getsockname()[1]
676
logger.debug(u"Now listening on port %d", servicePort)
678
if options.interface is not None:
679
serviceInterface = if_nametoindex(options.interface)
681
# From the Avahi server example code
682
server.connect_to_signal("StateChanged", server_state_changed)
684
server_state_changed(server.GetState())
685
except dbus.exceptions.DBusException, error:
686
logger.critical(u"DBusException: %s", error)
688
# End of Avahi example code
690
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
691
lambda *args, **kwargs:
692
tcp_server.handle_request(*args[2:],
695
main_loop_started = True
1738
except AvahiError, error:
1739
logger.critical(u"AvahiError: %s", error)
1742
697
except KeyboardInterrupt:
1745
logger.debug(u"Server received KeyboardInterrupt")
1746
logger.debug(u"Server exiting")
1747
# Must run before the D-Bus bus name gets deregistered
1750
if __name__ == '__main__':