44
14
import gnutls.library.functions
45
15
import gnutls.library.constants
46
16
import gnutls.library.types
47
import ConfigParser as configparser
56
import logging.handlers
62
import cPickle as pickle
69
28
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:'
93
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())
32
import logging.handlers
34
# logghandler.setFormatter(logging.Formatter('%(levelname)s %(message)s')
36
logger = logging.Logger('mandos')
37
logger.addHandler(logging.handlers.SysLogHandler(facility = logging.handlers.SysLogHandler.LOG_DAEMON))
39
# This variable is used to optionally bind to a specified interface.
40
# It is a global variable to fit in with the other variables from the
41
# Avahi server example code.
42
serviceInterface = avahi.IF_UNSPEC
43
# From the Avahi server example code:
44
serviceName = "Mandos"
45
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
46
servicePort = None # Not known at startup
47
serviceTXT = [] # TXT record for the service
48
domain = "" # Domain to publish on, default to .local
49
host = "" # Host to publish records for, default to localhost
50
group = None #our entry group
51
rename_count = 12 # Counter so we only rename after collisions a
52
# sensible number of times
53
# End of Avahi example code
233
56
class Client(object):
234
57
"""A representation of a client host served by this server.
237
name: string; from the config file, used in log messages and
59
name: string; from the config file, used in log messages
239
60
fingerprint: string (40 or 32 hexadecimal digits); used to
240
61
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.
62
secret: bytestring; sent verbatim (over TLS) to client
63
fqdn: string (FQDN); available for use by the checker command
64
created: datetime.datetime()
65
last_seen: datetime.datetime() or None if not yet seen
66
timeout: datetime.timedelta(); How long from last_seen until
67
this client is invalid
68
interval: datetime.timedelta(); How often to start a new checker
69
stop_hook: If set, called by stop() as stop_hook(self)
70
checker: subprocess.Popen(); a running checker process used
71
to see if the client lives.
72
Is None if no process is running.
254
73
checker_initiator_tag: a gobject event source tag, or None
255
disable_initiator_tag: - '' -
74
stop_initiator_tag: - '' -
256
75
checker_callback_tag: - '' -
257
76
checker_command: string; External command which is run to check if
258
client lives. %() expansions are done at
77
client lives. %()s expansions are done at
259
78
runtime with vars(self) as dict, so that for
260
79
instance %(name)s can be used in the command.
261
current_checker_command: string; current running checker_command
81
_timeout: Real variable for 'timeout'
82
_interval: Real variable for 'interval'
83
_timeout_milliseconds: Used by gobject.timeout_add()
84
_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'
86
def _set_timeout(self, timeout):
87
"Setter function for 'timeout' attribute"
88
self._timeout = timeout
89
self._timeout_milliseconds = ((self.timeout.days
90
* 24 * 60 * 60 * 1000)
91
+ (self.timeout.seconds * 1000)
92
+ (self.timeout.microseconds
94
timeout = property(lambda self: self._timeout,
97
def _set_interval(self, interval):
98
"Setter function for 'interval' attribute"
99
self._interval = interval
100
self._interval_milliseconds = ((self.interval.days
101
* 24 * 60 * 60 * 1000)
102
+ (self.interval.seconds
104
+ (self.interval.microseconds
106
interval = property(lambda self: self._interval,
109
def __init__(self, name=None, options=None, stop_hook=None,
110
fingerprint=None, secret=None, secfile=None, fqdn=None,
111
timeout=None, interval=-1, checker=None):
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
113
# Uppercase and remove spaces from fingerprint
114
# for later comparison purposes with return value of
115
# the fingerprint() function
116
self.fingerprint = fingerprint.upper().replace(u" ", u"")
118
self.secret = secret.decode(u"base64")
121
self.secret = sf.read()
124
raise RuntimeError(u"No secret or secfile for client %s"
126
self.fqdn = fqdn # string
127
self.created = datetime.datetime.now()
128
self.last_seen = None
130
timeout = options.timeout
131
self.timeout = timeout
133
interval = options.interval
135
interval = string_to_delta(interval)
136
self.interval = interval
137
self.stop_hook = stop_hook
311
138
self.checker = None
312
139
self.checker_initiator_tag = None
313
self.disable_initiator_tag = None
140
self.stop_initiator_tag = None
314
141
self.checker_callback_tag = None
315
self.checker_command = config[u"checker"]
316
self.current_checker_command = None
317
self.last_connect = None
320
"""Start this client's checker and timeout hooks"""
321
if getattr(self, u"enabled", False):
324
self.last_enabled = datetime.datetime.utcnow()
142
self.check_command = checker
144
"""Start this clients checker and timeout hooks"""
325
145
# Schedule a new checker to be started an 'interval' from now,
326
146
# 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(),
147
self.checker_initiator_tag = gobject.timeout_add\
148
(self._interval_milliseconds,
335
150
# Also start a new checker *right now*.
336
151
self.start_checker()
338
def disable(self, quiet=True):
339
"""Disable this client."""
340
if not getattr(self, "enabled", False):
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):
152
# Schedule a stop() when 'timeout' has passed
153
self.stop_initiator_tag = gobject.timeout_add\
154
(self._timeout_milliseconds,
158
The possibility that this client might be restarted is left
159
open, but not currently used."""
160
logger.debug(u"Stopping client %s", self.name)
162
if self.stop_initiator_tag:
163
gobject.source_remove(self.stop_initiator_tag)
164
self.stop_initiator_tag = None
165
if self.checker_initiator_tag:
348
166
gobject.source_remove(self.checker_initiator_tag)
349
167
self.checker_initiator_tag = None
350
168
self.stop_checker()
351
if self.disable_hook:
352
self.disable_hook(self)
354
171
# Do not run this again if called by a gobject.timeout_add
357
173
def __del__(self):
358
self.disable_hook = None
361
def checker_callback(self, pid, condition, command):
174
# Some code duplication here and in stop()
175
if hasattr(self, "stop_initiator_tag") \
176
and self.stop_initiator_tag:
177
gobject.source_remove(self.stop_initiator_tag)
178
self.stop_initiator_tag = None
179
if hasattr(self, "checker_initiator_tag") \
180
and self.checker_initiator_tag:
181
gobject.source_remove(self.checker_initiator_tag)
182
self.checker_initiator_tag = None
184
def checker_callback(self, pid, condition):
362
185
"""The checker has completed, so take appropriate actions."""
363
self.checker_callback_tag = 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",
186
now = datetime.datetime.now()
187
if os.WIFEXITED(condition) \
188
and (os.WEXITSTATUS(condition) == 0):
189
logger.debug(u"Checker for %(name)s succeeded",
192
gobject.source_remove(self.stop_initiator_tag)
193
self.stop_initiator_tag = gobject.timeout_add\
194
(self._timeout_milliseconds,
196
if not os.WIFEXITED(condition):
375
197
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(),
200
logger.debug(u"Checker for %(name)s failed",
203
self.checker_callback_tag = None
390
204
def start_checker(self):
391
205
"""Start a new checker subprocess if one is not running.
393
206
If a checker already exists, leave it running and do
395
# The reason for not killing a running checker is that if we
396
# did that, then if a checker (for some reason) started
397
# running slowly and taking more than 'interval' time, the
398
# client would inevitably timeout, since no checker would get
399
# a chance to run to completion. If we instead leave running
400
# 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
418
208
if self.checker is None:
209
logger.debug(u"Starting checker for %s",
420
# In case checker_command has exactly one % operator
421
command = self.checker_command % self.host
212
command = self.check_command % self.fqdn
422
213
except TypeError:
423
# Escape attributes for the shell
424
escaped_attrs = dict((key,
425
re.escape(unicode(str(val),
214
escaped_attrs = dict((key, re.escape(str(val)))
429
216
vars(self).iteritems())
431
command = self.checker_command % escaped_attrs
218
command = self.check_command % escaped_attrs
432
219
except TypeError, error:
433
logger.error(u'Could not format string "%s":'
434
u' %s', self.checker_command, error)
220
logger.critical(u'Could not format string "%s": %s',
221
self.check_command, error)
435
222
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:
224
self.checker = subprocess.\
226
stdout=subprocess.PIPE,
227
close_fds=True, shell=True,
229
self.checker_callback_tag = gobject.\
230
child_watch_add(self.checker.pid,
233
except subprocess.OSError, error:
458
234
logger.error(u"Failed to start subprocess: %s",
460
236
# Re-run this periodically if run by gobject.timeout_add
463
238
def stop_checker(self):
464
239
"""Force the checker process, if any, to stop."""
465
if self.checker_callback_tag:
466
gobject.source_remove(self.checker_callback_tag)
467
self.checker_callback_tag = None
468
if getattr(self, u"checker", None) is None:
240
if not hasattr(self, "checker") or self.checker is None:
470
logger.debug(u"Stopping checker for %(name)s", vars(self))
472
os.kill(self.checker.pid, signal.SIGTERM)
474
#if self.checker.poll() is None:
475
# os.kill(self.checker.pid, signal.SIGKILL)
476
except OSError, error:
477
if error.errno != errno.ESRCH: # No such process
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,
242
gobject.source_remove(self.checker_callback_tag)
730
243
self.checker_callback_tag = None
244
os.kill(self.checker.pid, signal.SIGTERM)
245
if self.checker.poll() is None:
246
os.kill(self.checker.pid, signal.SIGKILL)
731
247
self.checker = 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.
248
def still_valid(self, now=None):
249
"""Has the timeout not yet passed for this client?"""
251
now = datetime.datetime.now()
252
if self.last_seen is None:
253
return now < (self.created + self.timeout)
255
return now < (self.last_seen + self.timeout)
258
def peer_certificate(session):
259
# If not an OpenPGP certificate...
260
if gnutls.library.functions.gnutls_certificate_type_get\
261
(session._c_object) \
262
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
263
# ...do the normal thing
264
return session.peer_certificate
265
list_size = ctypes.c_uint()
266
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
267
(session._c_object, ctypes.byref(list_size))
268
if list_size.value == 0:
271
return ctypes.string_at(cert.data, cert.size)
274
def fingerprint(openpgp):
275
# New empty GnuTLS certificate
276
crt = gnutls.library.types.gnutls_openpgp_crt_t()
277
gnutls.library.functions.gnutls_openpgp_crt_init\
279
# New GnuTLS "datum" with the OpenPGP public key
280
datum = gnutls.library.types.gnutls_datum_t\
281
(ctypes.cast(ctypes.c_char_p(openpgp),
282
ctypes.POINTER(ctypes.c_ubyte)),
283
ctypes.c_uint(len(openpgp)))
284
# Import the OpenPGP public key into the certificate
285
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
288
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
289
# New buffer for the fingerprint
290
buffer = ctypes.create_string_buffer(20)
291
buffer_length = ctypes.c_size_t()
292
# Get the fingerprint from the certificate into the buffer
293
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
294
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
295
# Deinit the certificate
296
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
297
# Convert the buffer to a Python bytestring
298
fpr = ctypes.string_at(buffer, buffer_length.value)
299
# Convert the bytestring to hexadecimal notation
300
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
304
class tcp_handler(SocketServer.BaseRequestHandler, object):
305
"""A TCP request handler class.
306
Instantiated by IPv6_TCPServer for each request to handle it.
995
307
Note: This will run in its own forked process."""
997
309
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
310
logger.debug(u"TCP connection from: %s",
311
unicode(self.client_address))
312
session = gnutls.connection.ClientSession(self.request,
316
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
317
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
319
priority = "SECURE256"
321
gnutls.library.functions.gnutls_priority_set_direct\
322
(session._c_object, priority, None);
326
except gnutls.errors.GNUTLSError, error:
327
logger.debug(u"Handshake failed: %s", error)
328
# Do not run session.bye() here: the session is not
329
# established. Just abandon the request.
332
fpr = fingerprint(peer_certificate(session))
333
except (TypeError, gnutls.errors.GNUTLSError), error:
334
logger.debug(u"Bad certificate: %s", error)
337
logger.debug(u"Fingerprint: %s", fpr)
340
if c.fingerprint == fpr:
343
# Have to check if client.still_valid(), since it is possible
344
# that the client timed out while establishing the GnuTLS
346
if (not client) or (not client.still_valid()):
348
logger.debug(u"Client %(name)s is invalid",
351
logger.debug(u"Client not found for fingerprint: %s",
356
while sent_size < len(client.secret):
357
sent = session.send(client.secret[sent_size:])
358
logger.debug(u"Sent: %d, remaining: %d",
359
sent, len(client.secret)
360
- (sent_size + sent))
365
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
366
"""IPv6 TCP server. Accepts 'None' as address and/or port.
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
368
options: Command line options
369
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)
371
address_family = socket.AF_INET6
372
def __init__(self, *args, **kwargs):
373
if "options" in kwargs:
374
self.options = kwargs["options"]
375
del kwargs["options"]
376
if "clients" in kwargs:
377
self.clients = kwargs["clients"]
378
del kwargs["clients"]
379
return super(type(self), self).__init__(*args, **kwargs)
1203
380
def server_bind(self):
1204
381
"""This overrides the normal server_bind() function
1205
382
to bind to an interface if one was specified, and also NOT to
1206
383
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",
384
if self.options.interface:
385
if not hasattr(socket, "SO_BINDTODEVICE"):
386
# From /usr/include/asm-i486/socket.h
387
socket.SO_BINDTODEVICE = 25
389
self.socket.setsockopt(socket.SOL_SOCKET,
390
socket.SO_BINDTODEVICE,
391
self.options.interface)
392
except socket.error, error:
393
if error[0] == errno.EPERM:
394
logger.warning(u"No permission to"
395
u" bind to interface %s",
396
self.options.interface)
1229
399
# Only bind(2) the socket if we really need to.
1230
400
if self.server_address[0] or self.server_address[1]:
1231
401
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,
403
self.server_address = (in6addr_any,
1237
404
self.server_address[1])
1238
elif not self.server_address[1]:
405
elif self.server_address[1] is None:
1239
406
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
408
return super(type(self), self).server_bind()
1367
411
def string_to_delta(interval):
1368
412
"""Parse a string and return a datetime.timedelta
1370
>>> string_to_delta(u'7d')
414
>>> string_to_delta('7d')
1371
415
datetime.timedelta(7)
1372
>>> string_to_delta(u'60s')
416
>>> string_to_delta('60s')
1373
417
datetime.timedelta(0, 60)
1374
>>> string_to_delta(u'60m')
418
>>> string_to_delta('60m')
1375
419
datetime.timedelta(0, 3600)
1376
>>> string_to_delta(u'24h')
420
>>> string_to_delta('24h')
1377
421
datetime.timedelta(1)
1378
422
>>> string_to_delta(u'1w')
1379
423
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)
426
suffix=unicode(interval[-1])
427
value=int(interval[:-1])
429
delta = datetime.timedelta(value)
431
delta = datetime.timedelta(0, value)
433
delta = datetime.timedelta(0, 0, 0, 0, value)
435
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
437
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
440
except (ValueError, IndexError):
446
"""From the Avahi server example code"""
447
global group, serviceName, serviceType, servicePort, serviceTXT, \
450
group = dbus.Interface(
451
bus.get_object( avahi.DBUS_NAME,
452
server.EntryGroupNew()),
453
avahi.DBUS_INTERFACE_ENTRY_GROUP)
454
group.connect_to_signal('StateChanged',
455
entry_group_state_changed)
456
logger.debug(u"Adding service '%s' of type '%s' ...",
457
serviceName, serviceType)
460
serviceInterface, # interface
461
avahi.PROTO_INET6, # protocol
462
dbus.UInt32(0), # flags
463
serviceName, serviceType,
465
dbus.UInt16(servicePort),
466
avahi.string_array_to_txt_array(serviceTXT))
470
def remove_service():
471
"""From the Avahi server example code"""
474
if not group is None:
478
def server_state_changed(state):
479
"""From the Avahi server example code"""
480
if state == avahi.SERVER_COLLISION:
481
logger.warning(u"Server name collision")
483
elif state == avahi.SERVER_RUNNING:
487
def entry_group_state_changed(state, error):
488
"""From the Avahi server example code"""
489
global serviceName, server, rename_count
491
logger.debug(u"state change: %i", state)
493
if state == avahi.ENTRY_GROUP_ESTABLISHED:
494
logger.debug(u"Service established.")
495
elif state == avahi.ENTRY_GROUP_COLLISION:
497
rename_count = rename_count - 1
499
name = server.GetAlternativeServiceName(name)
500
logger.warning(u"Service name collision, "
501
u"changing name to '%s' ...", name)
506
logger.error(u"No suitable service name found "
507
u"after %i retries, exiting.",
510
elif state == avahi.ENTRY_GROUP_FAILURE:
511
logger.error(u"Error in group state changed %s",
1406
517
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
518
"""Call the C function if_nametoindex()"""
1412
if_nametoindex = (ctypes.cdll.LoadLibrary
1413
(ctypes.util.find_library(u"c"))
520
libc = ctypes.cdll.LoadLibrary("libc.so.6")
521
return libc.if_nametoindex(interface)
1415
522
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):
1431
"""See daemon(3). Standard BSD Unix function.
1433
This should really exist as os.daemon, but it doesn't (yet)."""
1442
# Close all standard open file descriptors
1443
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1444
if not stat.S_ISCHR(os.fstat(null).st_mode):
1445
raise OSError(errno.ENODEV,
1446
u"%s not a character device"
1448
os.dup2(null, sys.stdin.fileno())
1449
os.dup2(null, sys.stdout.fileno())
1450
os.dup2(null, sys.stderr.fileno())
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]
523
if "struct" not in sys.modules:
525
if "fcntl" not in sys.modules:
527
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
529
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
530
struct.pack("16s16x", interface))
532
interface_index = struct.unpack("I", ifreq[16:20])[0]
533
return interface_index
536
if __name__ == '__main__':
537
parser = OptionParser()
538
parser.add_option("-i", "--interface", type="string",
539
default=None, metavar="IF",
540
help="Bind to interface IF")
541
parser.add_option("--cert", type="string", default="cert.pem",
543
help="Public key certificate PEM file to use")
544
parser.add_option("--key", type="string", default="key.pem",
546
help="Private key PEM file to use")
547
parser.add_option("--ca", type="string", default="ca.pem",
549
help="Certificate Authority certificate PEM file to use")
550
parser.add_option("--crl", type="string", default="crl.pem",
552
help="Certificate Revokation List PEM file to use")
553
parser.add_option("-p", "--port", type="int", default=None,
554
help="Port number to receive requests on")
555
parser.add_option("--timeout", type="string", # Parsed later
557
help="Amount of downtime allowed for clients")
558
parser.add_option("--interval", type="string", # Parsed later
560
help="How often to check that a client is up")
561
parser.add_option("--check", action="store_true", default=False,
562
help="Run self-test")
563
parser.add_option("--debug", action="store_true", default=False,
565
(options, args) = parser.parse_args()
1487
567
if options.check:
1489
569
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))
1614
# From the Avahi example code
572
# Parse the time arguments
574
options.timeout = string_to_delta(options.timeout)
576
parser.error("option --timeout: Unparseable time")
578
options.interval = string_to_delta(options.interval)
580
parser.error("option --interval: Unparseable time")
583
defaults = { "checker": "sleep 1; fping -q -- %%(fqdn)s" }
584
client_config = ConfigParser.SafeConfigParser(defaults)
585
#client_config.readfp(open("secrets.conf"), "secrets.conf")
586
client_config.read("mandos-clients.conf")
588
# From the Avahi server example code
1615
589
DBusGMainLoop(set_as_default=True )
1616
590
main_loop = gobject.MainLoop()
1617
591
bus = dbus.SystemBus()
1618
# 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")
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
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
"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
# From the Avahi example code
1774
except dbus.exceptions.DBusException, error:
1775
logger.critical(u"DBusException: %s", error)
1778
# 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")
592
server = dbus.Interface(
593
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
594
avahi.DBUS_INTERFACE_SERVER )
595
# End of Avahi example code
597
debug = options.debug
600
def remove_from_clients(client):
601
clients.remove(client)
603
logger.debug(u"No clients left, exiting")
606
clients.update(Set(Client(name=section, options=options,
607
stop_hook = remove_from_clients,
608
**(dict(client_config\
610
for section in client_config.sections()))
611
for client in clients:
614
tcp_server = IPv6_TCPServer((None, options.port),
618
# Find out what random port we got
619
servicePort = tcp_server.socket.getsockname()[1]
620
logger.debug(u"Now listening on port %d", servicePort)
622
if options.interface is not None:
623
serviceInterface = if_nametoindex(options.interface)
625
# From the Avahi server example code
626
server.connect_to_signal("StateChanged", server_state_changed)
627
server_state_changed(server.GetState())
628
# End of Avahi example code
630
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
631
lambda *args, **kwargs:
632
tcp_server.handle_request(*args[2:],
1787
except AvahiError, error:
1788
logger.critical(u"AvahiError: %s", error)
1791
636
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
if __name__ == '__main__':
641
# From the Avahi server example code
642
if not group is None:
644
# End of Avahi example code
646
for client in clients:
647
client.stop_hook = None