44
44
import gnutls.library.functions
45
45
import gnutls.library.constants
46
46
import gnutls.library.types
47
import ConfigParser as configparser
56
57
import logging.handlers
58
from contextlib import closing
67
62
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:'
65
# Brief description of the operation of this program:
67
# This server announces itself as a Zeroconf service. Connecting
68
# clients use the TLS protocol, with the unusual quirk that this
69
# server program acts as a TLS "client" while the connecting clients
70
# acts as a TLS "server". The clients (acting as a TLS "server") must
71
# supply an OpenPGP certificate, and the fingerprint of this
72
# certificate is used by this server to look up (in a list read from a
73
# file at start time) which binary blob to give the client. No other
74
# authentication or authorization is done by this server.
77
logger = logging.Logger('mandos')
78
syslogger = logging.handlers.SysLogHandler\
79
(facility = logging.handlers.SysLogHandler.LOG_DAEMON)
80
syslogger.setFormatter(logging.Formatter\
81
('%(levelname)s: %(message)s'))
91
82
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())
85
# This variable is used to optionally bind to a specified interface.
86
# It is a global variable to fit in with the other variables from the
88
serviceInterface = avahi.IF_UNSPEC
89
# From the Avahi example code:
91
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
92
servicePort = None # Not known at startup
93
serviceTXT = [] # TXT record for the service
94
domain = "" # Domain to publish on, default to .local
95
host = "" # Host to publish records for, default to localhost
96
group = None #our entry group
97
rename_count = 12 # Counter so we only rename after collisions a
98
# sensible number of times
99
# End of Avahi example code
230
102
class Client(object):
231
103
"""A representation of a client host served by this server.
234
name: string; from the config file, used in log messages and
105
name: string; from the config file, used in log messages
236
106
fingerprint: string (40 or 32 hexadecimal digits); used to
237
107
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.
108
secret: bytestring; sent verbatim (over TLS) to client
109
fqdn: string (FQDN); available for use by the checker command
110
created: datetime.datetime()
111
last_seen: datetime.datetime() or None if not yet seen
112
timeout: datetime.timedelta(); How long from last_seen until
113
this client is invalid
114
interval: datetime.timedelta(); How often to start a new checker
115
stop_hook: If set, called by stop() as stop_hook(self)
116
checker: subprocess.Popen(); a running checker process used
117
to see if the client lives.
118
Is None if no process is running.
251
119
checker_initiator_tag: a gobject event source tag, or None
252
disable_initiator_tag: - '' -
120
stop_initiator_tag: - '' -
253
121
checker_callback_tag: - '' -
254
122
checker_command: string; External command which is run to check if
255
client lives. %() expansions are done at
123
client lives. %()s expansions are done at
256
124
runtime with vars(self) as dict, so that for
257
125
instance %(name)s can be used in the command.
258
current_checker_command: string; current running checker_command
127
_timeout: Real variable for 'timeout'
128
_interval: Real variable for 'interval'
129
_timeout_milliseconds: Used by gobject.timeout_add()
130
_interval_milliseconds: - '' -
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'
132
def _set_timeout(self, timeout):
133
"Setter function for 'timeout' attribute"
134
self._timeout = timeout
135
self._timeout_milliseconds = ((self.timeout.days
136
* 24 * 60 * 60 * 1000)
137
+ (self.timeout.seconds * 1000)
138
+ (self.timeout.microseconds
140
timeout = property(lambda self: self._timeout,
143
def _set_interval(self, interval):
144
"Setter function for 'interval' attribute"
145
self._interval = interval
146
self._interval_milliseconds = ((self.interval.days
147
* 24 * 60 * 60 * 1000)
148
+ (self.interval.seconds
150
+ (self.interval.microseconds
152
interval = property(lambda self: self._interval,
155
def __init__(self, name=None, stop_hook=None, fingerprint=None,
156
secret=None, secfile=None, fqdn=None, timeout=None,
157
interval=-1, checker=None):
158
"""Note: the 'checker' argument sets the 'checker_command'
159
attribute and not the 'checker' attribute.."""
283
161
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()
162
# Uppercase and remove spaces from fingerprint
163
# for later comparison purposes with return value of
164
# the fingerprint() function
165
self.fingerprint = fingerprint.upper().replace(u" ", u"")
289
166
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()
168
self.secret = secret.decode(u"base64")
171
self.secret = sf.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
174
raise RuntimeError(u"No secret or secfile for client %s"
176
self.fqdn = fqdn # string
177
self.created = datetime.datetime.now()
178
self.last_seen = None
179
self.timeout = string_to_delta(timeout)
180
self.interval = string_to_delta(interval)
181
self.stop_hook = stop_hook
309
182
self.checker = None
310
183
self.checker_initiator_tag = None
311
self.disable_initiator_tag = None
184
self.stop_initiator_tag = None
312
185
self.checker_callback_tag = None
313
self.checker_command = config[u"checker"]
314
self.current_checker_command = None
315
self.last_connect = None
186
self.check_command = checker
318
188
"""Start this client's checker and timeout hooks"""
319
if getattr(self, u"enabled", False):
322
self.last_enabled = datetime.datetime.utcnow()
323
189
# Schedule a new checker to be started an 'interval' from now,
324
190
# and every interval from then on.
325
self.checker_initiator_tag = (gobject.timeout_add
326
(self.interval_milliseconds(),
191
self.checker_initiator_tag = gobject.timeout_add\
192
(self._interval_milliseconds,
328
194
# Also start a new checker *right now*.
329
195
self.start_checker()
330
# Schedule a disable() when 'timeout' has passed
331
self.disable_initiator_tag = (gobject.timeout_add
332
(self.timeout_milliseconds(),
337
"""Disable this client."""
338
if not getattr(self, "enabled", False):
196
# Schedule a stop() when 'timeout' has passed
197
self.stop_initiator_tag = gobject.timeout_add\
198
(self._timeout_milliseconds,
202
The possibility that this client might be restarted is left
203
open, but not currently used."""
204
# If this client doesn't have a secret, it is already stopped.
206
logger.debug(u"Stopping client %s", self.name)
340
logger.info(u"Disabling client %s", self.name)
341
if getattr(self, u"disable_initiator_tag", False):
342
gobject.source_remove(self.disable_initiator_tag)
343
self.disable_initiator_tag = None
344
if getattr(self, u"checker_initiator_tag", False):
210
if hasattr(self, "stop_initiator_tag") \
211
and self.stop_initiator_tag:
212
gobject.source_remove(self.stop_initiator_tag)
213
self.stop_initiator_tag = None
214
if hasattr(self, "checker_initiator_tag") \
215
and self.checker_initiator_tag:
345
216
gobject.source_remove(self.checker_initiator_tag)
346
217
self.checker_initiator_tag = None
347
218
self.stop_checker()
348
if self.disable_hook:
349
self.disable_hook(self)
351
221
# Do not run this again if called by a gobject.timeout_add
354
223
def __del__(self):
355
self.disable_hook = None
358
def checker_callback(self, pid, condition, command):
224
self.stop_hook = None
226
def checker_callback(self, pid, condition):
359
227
"""The checker has completed, so take appropriate actions."""
228
now = datetime.datetime.now()
360
229
self.checker_callback_tag = None
361
230
self.checker = None
362
if os.WIFEXITED(condition):
363
exitstatus = os.WEXITSTATUS(condition)
365
logger.info(u"Checker for %(name)s succeeded",
369
logger.info(u"Checker for %(name)s failed",
231
if os.WIFEXITED(condition) \
232
and (os.WEXITSTATUS(condition) == 0):
233
logger.debug(u"Checker for %(name)s succeeded",
236
gobject.source_remove(self.stop_initiator_tag)
237
self.stop_initiator_tag = gobject.timeout_add\
238
(self._timeout_milliseconds,
240
elif not os.WIFEXITED(condition):
372
241
logger.warning(u"Checker for %(name)s crashed?",
375
def checked_ok(self):
376
"""Bump up the timeout for this client.
378
This should only be called when the client has been seen,
381
self.last_checked_ok = datetime.datetime.utcnow()
382
gobject.source_remove(self.disable_initiator_tag)
383
self.disable_initiator_tag = (gobject.timeout_add
384
(self.timeout_milliseconds(),
244
logger.debug(u"Checker for %(name)s failed",
387
246
def start_checker(self):
388
247
"""Start a new checker subprocess if one is not running.
390
248
If a checker already exists, leave it running and do
392
250
# The reason for not killing a running checker is that if we
397
255
# checkers alone, the checker would have to take more time
398
256
# than 'timeout' for the client to be declared invalid, which
399
257
# is as it should be.
401
# If a checker exists, make sure it is not a zombie
403
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
404
except (AttributeError, OSError), error:
405
if (isinstance(error, OSError)
406
and error.errno != errno.ECHILD):
410
logger.warning(u"Checker was a zombie")
411
gobject.source_remove(self.checker_callback_tag)
412
self.checker_callback(pid, status,
413
self.current_checker_command)
414
# Start a new checker if needed
415
258
if self.checker is None:
417
# In case checker_command has exactly one % operator
418
command = self.checker_command % self.host
260
command = self.check_command % self.fqdn
419
261
except TypeError:
420
# Escape attributes for the shell
421
escaped_attrs = dict((key,
422
re.escape(unicode(str(val),
262
escaped_attrs = dict((key, re.escape(str(val)))
426
264
vars(self).iteritems())
428
command = self.checker_command % escaped_attrs
266
command = self.check_command % escaped_attrs
429
267
except TypeError, error:
430
logger.error(u'Could not format string "%s":'
431
u' %s', self.checker_command, error)
268
logger.critical(u'Could not format string "%s":'
269
u' %s', self.check_command, error)
432
270
return True # Try again later
433
self.current_checker_command = command
435
logger.info(u"Starting checker %r for %s",
437
# We don't need to redirect stdout and stderr, since
438
# in normal mode, that is already done by daemon(),
439
# and in debug mode we don't want to. (Stdin is
440
# always replaced by /dev/null.)
441
self.checker = subprocess.Popen(command,
443
shell=True, cwd=u"/")
444
self.checker_callback_tag = (gobject.child_watch_add
446
self.checker_callback,
448
# The checker may have completed before the gobject
449
# watch was added. Check for this.
450
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
452
gobject.source_remove(self.checker_callback_tag)
453
self.checker_callback(pid, status, command)
454
except OSError, error:
272
logger.debug(u"Starting checker %r for %s",
274
self.checker = subprocess.\
276
close_fds=True, shell=True,
278
self.checker_callback_tag = gobject.child_watch_add\
280
self.checker_callback)
281
except subprocess.OSError, error:
455
282
logger.error(u"Failed to start subprocess: %s",
457
284
# Re-run this periodically if run by gobject.timeout_add
460
286
def stop_checker(self):
461
287
"""Force the checker process, if any, to stop."""
462
288
if self.checker_callback_tag:
463
289
gobject.source_remove(self.checker_callback_tag)
464
290
self.checker_callback_tag = None
465
if getattr(self, u"checker", None) is None:
291
if not hasattr(self, "checker") or self.checker is None:
467
logger.debug(u"Stopping checker for %(name)s", vars(self))
293
logger.debug("Stopping checker for %(name)s", vars(self))
469
295
os.kill(self.checker.pid, signal.SIGTERM)
471
297
#if self.checker.poll() is None:
472
298
# os.kill(self.checker.pid, signal.SIGKILL)
473
299
except OSError, error:
474
if error.errno != errno.ESRCH: # No such process
300
if error.errno != errno.ESRCH:
476
302
self.checker = None
478
def still_valid(self):
303
def still_valid(self, now=None):
479
304
"""Has the timeout not yet passed for this client?"""
480
if not getattr(self, u"enabled", False):
482
now = datetime.datetime.utcnow()
483
if self.last_checked_ok is None:
306
now = datetime.datetime.now()
307
if self.last_seen is None:
484
308
return now < (self.created + self.timeout)
486
return now < (self.last_checked_ok + self.timeout)
489
def dbus_service_property(dbus_interface, signature=u"v",
490
access=u"readwrite", byte_arrays=False):
491
"""Decorators for marking methods of a DBusObjectWithProperties to
492
become properties on the D-Bus.
494
The decorated method will be called with no arguments by "Get"
495
and with one argument by "Set".
497
The parameters, where they are supported, are the same as
498
dbus.service.method, except there is only "signature", since the
499
type from Get() and the type sent to Set() is the same.
502
func._dbus_is_property = True
503
func._dbus_interface = dbus_interface
504
func._dbus_signature = signature
505
func._dbus_access = access
506
func._dbus_name = func.__name__
507
if func._dbus_name.endswith(u"_dbus_property"):
508
func._dbus_name = func._dbus_name[:-14]
509
func._dbus_get_args_options = {u'byte_arrays': byte_arrays }
514
class DBusPropertyException(dbus.exceptions.DBusException):
515
"""A base class for D-Bus property-related exceptions
517
def __unicode__(self):
518
return unicode(str(self))
521
class DBusPropertyAccessException(DBusPropertyException):
522
"""A property's access permissions disallows an operation.
527
class DBusPropertyNotFound(DBusPropertyException):
528
"""An attempt was made to access a non-existing property.
533
class DBusObjectWithProperties(dbus.service.Object):
534
"""A D-Bus object with properties.
536
Classes inheriting from this can use the dbus_service_property
537
decorator to expose methods as D-Bus properties. It exposes the
538
standard Get(), Set(), and GetAll() methods on the D-Bus.
542
def _is_dbus_property(obj):
543
return getattr(obj, u"_dbus_is_property", False)
545
def _get_all_dbus_properties(self):
546
"""Returns a generator of (name, attribute) pairs
548
return ((prop._dbus_name, prop)
550
inspect.getmembers(self, self._is_dbus_property))
552
def _get_dbus_property(self, interface_name, property_name):
553
"""Returns a bound method if one exists which is a D-Bus
554
property with the specified name and interface.
556
for name in (property_name,
557
property_name + u"_dbus_property"):
558
prop = getattr(self, name, None)
560
or not self._is_dbus_property(prop)
561
or prop._dbus_name != property_name
562
or (interface_name and prop._dbus_interface
563
and interface_name != prop._dbus_interface)):
567
raise DBusPropertyNotFound(self.dbus_object_path + u":"
568
+ interface_name + u"."
571
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ss",
573
def Get(self, interface_name, property_name):
574
"""Standard D-Bus property Get() method, see D-Bus standard.
576
prop = self._get_dbus_property(interface_name, property_name)
577
if prop._dbus_access == u"write":
578
raise DBusPropertyAccessException(property_name)
580
if not hasattr(value, u"variant_level"):
582
return type(value)(value, variant_level=value.variant_level+1)
584
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ssv")
585
def Set(self, interface_name, property_name, value):
586
"""Standard D-Bus property Set() method, see D-Bus standard.
588
prop = self._get_dbus_property(interface_name, property_name)
589
if prop._dbus_access == u"read":
590
raise DBusPropertyAccessException(property_name)
591
if prop._dbus_get_args_options[u"byte_arrays"]:
592
value = dbus.ByteArray(''.join(unichr(byte)
596
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"s",
597
out_signature=u"a{sv}")
598
def GetAll(self, interface_name):
599
"""Standard D-Bus property GetAll() method, see D-Bus
602
Note: Will not include properties with access="write".
605
for name, prop in self._get_all_dbus_properties():
607
and interface_name != prop._dbus_interface):
608
# Interface non-empty but did not match
610
# Ignore write-only properties
611
if prop._dbus_access == u"write":
614
if not hasattr(value, u"variant_level"):
617
all[name] = type(value)(value, variant_level=
618
value.variant_level+1)
619
return dbus.Dictionary(all, signature=u"sv")
621
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
623
path_keyword='object_path',
624
connection_keyword='connection')
625
def Introspect(self, object_path, connection):
626
"""Standard D-Bus method, overloaded to insert property tags.
628
xmlstring = dbus.service.Object.Introspect(self, object_path,
631
document = xml.dom.minidom.parseString(xmlstring)
632
def make_tag(document, name, prop):
633
e = document.createElement(u"property")
634
e.setAttribute(u"name", name)
635
e.setAttribute(u"type", prop._dbus_signature)
636
e.setAttribute(u"access", prop._dbus_access)
638
for if_tag in document.getElementsByTagName(u"interface"):
639
for tag in (make_tag(document, name, prop)
641
in self._get_all_dbus_properties()
642
if prop._dbus_interface
643
== if_tag.getAttribute(u"name")):
644
if_tag.appendChild(tag)
645
# Add the names to the return values for the
646
# "org.freedesktop.DBus.Properties" methods
647
if (if_tag.getAttribute(u"name")
648
== u"org.freedesktop.DBus.Properties"):
649
for cn in if_tag.getElementsByTagName(u"method"):
650
if cn.getAttribute(u"name") == u"Get":
651
for arg in cn.getElementsByTagName(u"arg"):
652
if (arg.getAttribute(u"direction")
654
arg.setAttribute(u"name", u"value")
655
elif cn.getAttribute(u"name") == u"GetAll":
656
for arg in cn.getElementsByTagName(u"arg"):
657
if (arg.getAttribute(u"direction")
659
arg.setAttribute(u"name", u"props")
660
xmlstring = document.toxml(u"utf-8")
662
except (AttributeError, xml.dom.DOMException,
663
xml.parsers.expat.ExpatError), error:
664
logger.error(u"Failed to override Introspection method",
669
class ClientDBus(Client, DBusObjectWithProperties):
670
"""A Client class using D-Bus
673
dbus_object_path: dbus.ObjectPath
674
bus: dbus.SystemBus()
676
# dbus.service.Object doesn't use super(), so we can't either.
678
def __init__(self, bus = None, *args, **kwargs):
680
Client.__init__(self, *args, **kwargs)
681
# Only now, when this client is initialized, can it show up on
683
self.dbus_object_path = (dbus.ObjectPath
685
+ self.name.replace(u".", u"_")))
686
DBusObjectWithProperties.__init__(self, self.bus,
687
self.dbus_object_path)
690
def _datetime_to_dbus(dt, variant_level=0):
691
"""Convert a UTC datetime.datetime() to a D-Bus type."""
692
return dbus.String(dt.isoformat(),
693
variant_level=variant_level)
696
oldstate = getattr(self, u"enabled", False)
697
r = Client.enable(self)
698
if oldstate != self.enabled:
700
self.PropertyChanged(dbus.String(u"enabled"),
701
dbus.Boolean(True, variant_level=1))
702
self.PropertyChanged(
703
dbus.String(u"last_enabled"),
704
self._datetime_to_dbus(self.last_enabled,
708
def disable(self, signal = True):
709
oldstate = getattr(self, u"enabled", False)
710
r = Client.disable(self)
711
if signal and oldstate != self.enabled:
713
self.PropertyChanged(dbus.String(u"enabled"),
714
dbus.Boolean(False, variant_level=1))
717
def __del__(self, *args, **kwargs):
719
self.remove_from_connection()
722
if hasattr(DBusObjectWithProperties, u"__del__"):
723
DBusObjectWithProperties.__del__(self, *args, **kwargs)
724
Client.__del__(self, *args, **kwargs)
726
def checker_callback(self, pid, condition, command,
728
self.checker_callback_tag = None
731
self.PropertyChanged(dbus.String(u"checker_running"),
732
dbus.Boolean(False, variant_level=1))
733
if os.WIFEXITED(condition):
734
exitstatus = os.WEXITSTATUS(condition)
736
self.CheckerCompleted(dbus.Int16(exitstatus),
737
dbus.Int64(condition),
738
dbus.String(command))
741
self.CheckerCompleted(dbus.Int16(-1),
742
dbus.Int64(condition),
743
dbus.String(command))
745
return Client.checker_callback(self, pid, condition, command,
748
def checked_ok(self, *args, **kwargs):
749
r = Client.checked_ok(self, *args, **kwargs)
751
self.PropertyChanged(
752
dbus.String(u"last_checked_ok"),
753
(self._datetime_to_dbus(self.last_checked_ok,
757
def start_checker(self, *args, **kwargs):
758
old_checker = self.checker
759
if self.checker is not None:
760
old_checker_pid = self.checker.pid
762
old_checker_pid = None
763
r = Client.start_checker(self, *args, **kwargs)
764
# Only if new checker process was started
765
if (self.checker is not None
766
and old_checker_pid != self.checker.pid):
768
self.CheckerStarted(self.current_checker_command)
769
self.PropertyChanged(
770
dbus.String(u"checker_running"),
771
dbus.Boolean(True, variant_level=1))
774
def stop_checker(self, *args, **kwargs):
775
old_checker = getattr(self, u"checker", None)
776
r = Client.stop_checker(self, *args, **kwargs)
777
if (old_checker is not None
778
and getattr(self, u"checker", None) is None):
779
self.PropertyChanged(dbus.String(u"checker_running"),
780
dbus.Boolean(False, variant_level=1))
783
## D-Bus methods & signals
784
_interface = u"se.bsnet.fukt.Mandos.Client"
787
@dbus.service.method(_interface)
789
return self.checked_ok()
791
# CheckerCompleted - signal
792
@dbus.service.signal(_interface, signature=u"nxs")
793
def CheckerCompleted(self, exitcode, waitstatus, command):
797
# CheckerStarted - signal
798
@dbus.service.signal(_interface, signature=u"s")
799
def CheckerStarted(self, command):
803
# PropertyChanged - signal
804
@dbus.service.signal(_interface, signature=u"sv")
805
def PropertyChanged(self, property, value):
810
@dbus.service.signal(_interface)
816
@dbus.service.signal(_interface)
822
@dbus.service.method(_interface)
827
# StartChecker - method
828
@dbus.service.method(_interface)
829
def StartChecker(self):
834
@dbus.service.method(_interface)
839
# StopChecker - method
840
@dbus.service.method(_interface)
841
def StopChecker(self):
845
@dbus_service_property(_interface, signature=u"s", access=u"read")
846
def name_dbus_property(self):
847
return dbus.String(self.name)
849
# fingerprint - property
850
@dbus_service_property(_interface, signature=u"s", access=u"read")
851
def fingerprint_dbus_property(self):
852
return dbus.String(self.fingerprint)
855
@dbus_service_property(_interface, signature=u"s",
857
def host_dbus_property(self, value=None):
858
if value is None: # get
859
return dbus.String(self.host)
862
self.PropertyChanged(dbus.String(u"host"),
863
dbus.String(value, variant_level=1))
866
@dbus_service_property(_interface, signature=u"s", access=u"read")
867
def created_dbus_property(self):
868
return dbus.String(self._datetime_to_dbus(self.created))
870
# last_enabled - property
871
@dbus_service_property(_interface, signature=u"s", access=u"read")
872
def last_enabled_dbus_property(self):
873
if self.last_enabled is None:
874
return dbus.String(u"")
875
return dbus.String(self._datetime_to_dbus(self.last_enabled))
878
@dbus_service_property(_interface, signature=u"b",
880
def enabled_dbus_property(self, value=None):
881
if value is None: # get
882
return dbus.Boolean(self.enabled)
888
# last_checked_ok - property
889
@dbus_service_property(_interface, signature=u"s",
891
def last_checked_ok_dbus_property(self, value=None):
892
if value is not None:
895
if self.last_checked_ok is None:
896
return dbus.String(u"")
897
return dbus.String(self._datetime_to_dbus(self
901
@dbus_service_property(_interface, signature=u"t",
903
def timeout_dbus_property(self, value=None):
904
if value is None: # get
905
return dbus.UInt64(self.timeout_milliseconds())
906
self.timeout = datetime.timedelta(0, 0, 0, value)
908
self.PropertyChanged(dbus.String(u"timeout"),
909
dbus.UInt64(value, variant_level=1))
910
if getattr(self, u"disable_initiator_tag", None) is None:
913
gobject.source_remove(self.disable_initiator_tag)
914
self.disable_initiator_tag = None
916
_timedelta_to_milliseconds((self
922
# The timeout has passed
925
self.disable_initiator_tag = (gobject.timeout_add
926
(time_to_die, self.disable))
928
# interval - property
929
@dbus_service_property(_interface, signature=u"t",
931
def interval_dbus_property(self, value=None):
932
if value is None: # get
933
return dbus.UInt64(self.interval_milliseconds())
934
self.interval = datetime.timedelta(0, 0, 0, value)
936
self.PropertyChanged(dbus.String(u"interval"),
937
dbus.UInt64(value, variant_level=1))
938
if getattr(self, u"checker_initiator_tag", None) is None:
940
# Reschedule checker run
941
gobject.source_remove(self.checker_initiator_tag)
942
self.checker_initiator_tag = (gobject.timeout_add
943
(value, self.start_checker))
944
self.start_checker() # Start one now, too
947
@dbus_service_property(_interface, signature=u"s",
949
def checker_dbus_property(self, value=None):
950
if value is None: # get
951
return dbus.String(self.checker_command)
952
self.checker_command = value
954
self.PropertyChanged(dbus.String(u"checker"),
955
dbus.String(self.checker_command,
958
# checker_running - property
959
@dbus_service_property(_interface, signature=u"b",
961
def checker_running_dbus_property(self, value=None):
962
if value is None: # get
963
return dbus.Boolean(self.checker is not None)
969
# object_path - property
970
@dbus_service_property(_interface, signature=u"o", access=u"read")
971
def object_path_dbus_property(self):
972
return self.dbus_object_path # is already a dbus.ObjectPath
975
@dbus_service_property(_interface, signature=u"ay",
976
access=u"write", byte_arrays=True)
977
def secret_dbus_property(self, value):
978
self.secret = str(value)
983
class ClientHandler(socketserver.BaseRequestHandler, object):
984
"""A class to handle client connections.
986
Instantiated once for each connection to handle it.
310
return now < (self.last_seen + self.timeout)
313
def peer_certificate(session):
314
"Return the peer's OpenPGP certificate as a bytestring"
315
# If not an OpenPGP certificate...
316
if gnutls.library.functions.gnutls_certificate_type_get\
317
(session._c_object) \
318
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
319
# ...do the normal thing
320
return session.peer_certificate
321
list_size = ctypes.c_uint()
322
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
323
(session._c_object, ctypes.byref(list_size))
324
if list_size.value == 0:
327
return ctypes.string_at(cert.data, cert.size)
330
def fingerprint(openpgp):
331
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
332
# New empty GnuTLS certificate
333
crt = gnutls.library.types.gnutls_openpgp_crt_t()
334
gnutls.library.functions.gnutls_openpgp_crt_init\
336
# New GnuTLS "datum" with the OpenPGP public key
337
datum = gnutls.library.types.gnutls_datum_t\
338
(ctypes.cast(ctypes.c_char_p(openpgp),
339
ctypes.POINTER(ctypes.c_ubyte)),
340
ctypes.c_uint(len(openpgp)))
341
# Import the OpenPGP public key into the certificate
342
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
345
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
346
# New buffer for the fingerprint
347
buffer = ctypes.create_string_buffer(20)
348
buffer_length = ctypes.c_size_t()
349
# Get the fingerprint from the certificate into the buffer
350
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
351
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
352
# Deinit the certificate
353
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
354
# Convert the buffer to a Python bytestring
355
fpr = ctypes.string_at(buffer, buffer_length.value)
356
# Convert the bytestring to hexadecimal notation
357
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
361
class tcp_handler(SocketServer.BaseRequestHandler, object):
362
"""A TCP request handler class.
363
Instantiated by IPv6_TCPServer for each request to handle it.
987
364
Note: This will run in its own forked process."""
989
366
def handle(self):
990
logger.info(u"TCP connection from: %s",
991
unicode(self.client_address))
992
logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
993
# Open IPC pipe to parent process
994
with closing(os.fdopen(self.server.pipe[1], u"w", 1)) as ipc:
995
session = (gnutls.connection
996
.ClientSession(self.request,
1000
line = self.request.makefile().readline()
1001
logger.debug(u"Protocol version: %r", line)
1003
if int(line.strip().split()[0]) > 1:
1005
except (ValueError, IndexError, RuntimeError), error:
1006
logger.error(u"Unknown protocol version: %s", error)
1009
# Note: gnutls.connection.X509Credentials is really a
1010
# generic GnuTLS certificate credentials object so long as
1011
# no X.509 keys are added to it. Therefore, we can use it
1012
# here despite using OpenPGP certificates.
1014
#priority = u':'.join((u"NONE", u"+VERS-TLS1.1",
1015
# u"+AES-256-CBC", u"+SHA1",
1016
# u"+COMP-NULL", u"+CTYPE-OPENPGP",
1018
# Use a fallback default, since this MUST be set.
1019
priority = self.server.gnutls_priority
1020
if priority is None:
1021
priority = u"NORMAL"
1022
(gnutls.library.functions
1023
.gnutls_priority_set_direct(session._c_object,
1028
except gnutls.errors.GNUTLSError, error:
1029
logger.warning(u"Handshake failed: %s", error)
1030
# Do not run session.bye() here: the session is not
1031
# established. Just abandon the request.
1033
logger.debug(u"Handshake succeeded")
1035
fpr = self.fingerprint(self.peer_certificate(session))
1036
except (TypeError, gnutls.errors.GNUTLSError), error:
1037
logger.warning(u"Bad certificate: %s", error)
1040
logger.debug(u"Fingerprint: %s", fpr)
1042
for c in self.server.clients:
1043
if c.fingerprint == fpr:
367
logger.debug(u"TCP connection from: %s",
368
unicode(self.client_address))
369
session = gnutls.connection.ClientSession(self.request,
373
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
374
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
377
if self.server.options.priority:
378
priority = self.server.options.priority
379
gnutls.library.functions.gnutls_priority_set_direct\
380
(session._c_object, priority, None);
384
except gnutls.errors.GNUTLSError, error:
385
logger.debug(u"Handshake failed: %s", error)
386
# Do not run session.bye() here: the session is not
387
# established. Just abandon the request.
390
fpr = fingerprint(peer_certificate(session))
391
except (TypeError, gnutls.errors.GNUTLSError), error:
392
logger.debug(u"Bad certificate: %s", error)
395
logger.debug(u"Fingerprint: %s", fpr)
397
for c in self.server.clients:
398
if c.fingerprint == fpr:
401
# Have to check if client.still_valid(), since it is possible
402
# that the client timed out while establishing the GnuTLS
404
if (not client) or (not client.still_valid()):
406
logger.debug(u"Client %(name)s is invalid",
1047
ipc.write(u"NOTFOUND %s %s\n"
1048
% (fpr, unicode(self.client_address)))
1051
# Have to check if client.still_valid(), since it is
1052
# possible that the client timed out while establishing
1053
# the GnuTLS session.
1054
if not client.still_valid():
1055
ipc.write(u"INVALID %s\n" % client.name)
1058
ipc.write(u"SENDING %s\n" % client.name)
1060
while sent_size < len(client.secret):
1061
sent = session.send(client.secret[sent_size:])
1062
logger.debug(u"Sent: %d, remaining: %d",
1063
sent, len(client.secret)
1064
- (sent_size + sent))
409
logger.debug(u"Client not found for fingerprint: %s",
1069
def peer_certificate(session):
1070
"Return the peer's OpenPGP certificate as a bytestring"
1071
# If not an OpenPGP certificate...
1072
if (gnutls.library.functions
1073
.gnutls_certificate_type_get(session._c_object)
1074
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1075
# ...do the normal thing
1076
return session.peer_certificate
1077
list_size = ctypes.c_uint(1)
1078
cert_list = (gnutls.library.functions
1079
.gnutls_certificate_get_peers
1080
(session._c_object, ctypes.byref(list_size)))
1081
if not bool(cert_list) and list_size.value != 0:
1082
raise gnutls.errors.GNUTLSError(u"error getting peer"
1084
if list_size.value == 0:
1087
return ctypes.string_at(cert.data, cert.size)
1090
def fingerprint(openpgp):
1091
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1092
# New GnuTLS "datum" with the OpenPGP public key
1093
datum = (gnutls.library.types
1094
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1097
ctypes.c_uint(len(openpgp))))
1098
# New empty GnuTLS certificate
1099
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1100
(gnutls.library.functions
1101
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1102
# Import the OpenPGP public key into the certificate
1103
(gnutls.library.functions
1104
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1105
gnutls.library.constants
1106
.GNUTLS_OPENPGP_FMT_RAW))
1107
# Verify the self signature in the key
1108
crtverify = ctypes.c_uint()
1109
(gnutls.library.functions
1110
.gnutls_openpgp_crt_verify_self(crt, 0,
1111
ctypes.byref(crtverify)))
1112
if crtverify.value != 0:
1113
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1114
raise (gnutls.errors.CertificateSecurityError
1116
# New buffer for the fingerprint
1117
buf = ctypes.create_string_buffer(20)
1118
buf_len = ctypes.c_size_t()
1119
# Get the fingerprint from the certificate into the buffer
1120
(gnutls.library.functions
1121
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1122
ctypes.byref(buf_len)))
1123
# Deinit the certificate
1124
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1125
# Convert the buffer to a Python bytestring
1126
fpr = ctypes.string_at(buf, buf_len.value)
1127
# Convert the bytestring to hexadecimal notation
1128
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
1132
class ForkingMixInWithPipe(socketserver.ForkingMixIn, object):
1133
"""Like socketserver.ForkingMixIn, but also pass a pipe."""
1134
def process_request(self, request, client_address):
1135
"""Overrides and wraps the original process_request().
1137
This function creates a new pipe in self.pipe
1139
self.pipe = os.pipe()
1140
super(ForkingMixInWithPipe,
1141
self).process_request(request, client_address)
1142
os.close(self.pipe[1]) # close write end
1143
self.add_pipe(self.pipe[0])
1144
def add_pipe(self, pipe):
1145
"""Dummy function; override as necessary"""
1149
class IPv6_TCPServer(ForkingMixInWithPipe,
1150
socketserver.TCPServer, object):
1151
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
414
while sent_size < len(client.secret):
415
sent = session.send(client.secret[sent_size:])
416
logger.debug(u"Sent: %d, remaining: %d",
417
sent, len(client.secret)
418
- (sent_size + sent))
423
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
424
"""IPv6 TCP server. Accepts 'None' as address and/or port.
1154
enabled: Boolean; whether this server is activated yet
1155
interface: None or a network interface name (string)
1156
use_ipv6: Boolean; to use IPv6 or not
426
options: Command line options
427
clients: Set() of Client objects
1158
def __init__(self, server_address, RequestHandlerClass,
1159
interface=None, use_ipv6=True):
1160
self.interface = interface
1162
self.address_family = socket.AF_INET6
1163
socketserver.TCPServer.__init__(self, server_address,
1164
RequestHandlerClass)
429
address_family = socket.AF_INET6
430
def __init__(self, *args, **kwargs):
431
if "options" in kwargs:
432
self.options = kwargs["options"]
433
del kwargs["options"]
434
if "clients" in kwargs:
435
self.clients = kwargs["clients"]
436
del kwargs["clients"]
437
return super(type(self), self).__init__(*args, **kwargs)
1165
438
def server_bind(self):
1166
439
"""This overrides the normal server_bind() function
1167
440
to bind to an interface if one was specified, and also NOT to
1168
441
bind to an address or port if they were not specified."""
1169
if self.interface is not None:
1170
if SO_BINDTODEVICE is None:
1171
logger.error(u"SO_BINDTODEVICE does not exist;"
1172
u" cannot bind to interface %s",
1176
self.socket.setsockopt(socket.SOL_SOCKET,
1180
except socket.error, error:
1181
if error[0] == errno.EPERM:
1182
logger.error(u"No permission to"
1183
u" bind to interface %s",
1185
elif error[0] == errno.ENOPROTOOPT:
1186
logger.error(u"SO_BINDTODEVICE not available;"
1187
u" cannot bind to interface %s",
442
if self.options.interface:
443
if not hasattr(socket, "SO_BINDTODEVICE"):
444
# From /usr/include/asm-i486/socket.h
445
socket.SO_BINDTODEVICE = 25
447
self.socket.setsockopt(socket.SOL_SOCKET,
448
socket.SO_BINDTODEVICE,
449
self.options.interface)
450
except socket.error, error:
451
if error[0] == errno.EPERM:
452
logger.warning(u"No permission to"
453
u" bind to interface %s",
454
self.options.interface)
1191
457
# Only bind(2) the socket if we really need to.
1192
458
if self.server_address[0] or self.server_address[1]:
1193
459
if not self.server_address[0]:
1194
if self.address_family == socket.AF_INET6:
1195
any_address = u"::" # in6addr_any
1197
any_address = socket.INADDR_ANY
1198
self.server_address = (any_address,
461
self.server_address = (in6addr_any,
1199
462
self.server_address[1])
1200
elif not self.server_address[1]:
463
elif self.server_address[1] is None:
1201
464
self.server_address = (self.server_address[0],
1203
# if self.interface:
1204
# self.server_address = (self.server_address[0],
1209
return socketserver.TCPServer.server_bind(self)
1212
class MandosServer(IPv6_TCPServer):
1216
clients: set of Client objects
1217
gnutls_priority GnuTLS priority string
1218
use_dbus: Boolean; to emit D-Bus signals or not
1220
Assumes a gobject.MainLoop event loop.
1222
def __init__(self, server_address, RequestHandlerClass,
1223
interface=None, use_ipv6=True, clients=None,
1224
gnutls_priority=None, use_dbus=True):
1225
self.enabled = False
1226
self.clients = clients
1227
if self.clients is None:
1228
self.clients = set()
1229
self.use_dbus = use_dbus
1230
self.gnutls_priority = gnutls_priority
1231
IPv6_TCPServer.__init__(self, server_address,
1232
RequestHandlerClass,
1233
interface = interface,
1234
use_ipv6 = use_ipv6)
1235
def server_activate(self):
1237
return socketserver.TCPServer.server_activate(self)
1240
def add_pipe(self, pipe):
1241
# Call "handle_ipc" for both data and EOF events
1242
gobject.io_add_watch(pipe, gobject.IO_IN | gobject.IO_HUP,
1244
def handle_ipc(self, source, condition, file_objects={}):
1246
gobject.IO_IN: u"IN", # There is data to read.
1247
gobject.IO_OUT: u"OUT", # Data can be written (without
1249
gobject.IO_PRI: u"PRI", # There is urgent data to read.
1250
gobject.IO_ERR: u"ERR", # Error condition.
1251
gobject.IO_HUP: u"HUP" # Hung up (the connection has been
1252
# broken, usually for pipes and
1255
conditions_string = ' | '.join(name
1257
condition_names.iteritems()
1258
if cond & condition)
1259
logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1262
# Turn the pipe file descriptor into a Python file object
1263
if source not in file_objects:
1264
file_objects[source] = os.fdopen(source, u"r", 1)
1266
# Read a line from the file object
1267
cmdline = file_objects[source].readline()
1268
if not cmdline: # Empty line means end of file
1269
# close the IPC pipe
1270
file_objects[source].close()
1271
del file_objects[source]
1273
# Stop calling this function
1276
logger.debug(u"IPC command: %r", cmdline)
1278
# Parse and act on command
1279
cmd, args = cmdline.rstrip(u"\r\n").split(None, 1)
1281
if cmd == u"NOTFOUND":
1282
logger.warning(u"Client not found for fingerprint: %s",
1286
mandos_dbus_service.ClientNotFound(args)
1287
elif cmd == u"INVALID":
1288
for client in self.clients:
1289
if client.name == args:
1290
logger.warning(u"Client %s is invalid", args)
1296
logger.error(u"Unknown client %s is invalid", args)
1297
elif cmd == u"SENDING":
1298
for client in self.clients:
1299
if client.name == args:
1300
logger.info(u"Sending secret to %s", client.name)
1307
logger.error(u"Sending secret to unknown client %s",
1310
logger.error(u"Unknown IPC command: %r", cmdline)
1312
# Keep calling this function
466
return super(type(self), self).server_bind()
1316
469
def string_to_delta(interval):
1317
470
"""Parse a string and return a datetime.timedelta
1319
>>> string_to_delta(u'7d')
472
>>> string_to_delta('7d')
1320
473
datetime.timedelta(7)
1321
>>> string_to_delta(u'60s')
474
>>> string_to_delta('60s')
1322
475
datetime.timedelta(0, 60)
1323
>>> string_to_delta(u'60m')
476
>>> string_to_delta('60m')
1324
477
datetime.timedelta(0, 3600)
1325
>>> string_to_delta(u'24h')
478
>>> string_to_delta('24h')
1326
479
datetime.timedelta(1)
1327
480
>>> string_to_delta(u'1w')
1328
481
datetime.timedelta(7)
1329
>>> string_to_delta(u'5m 30s')
1330
datetime.timedelta(0, 330)
1332
timevalue = datetime.timedelta(0)
1333
for s in interval.split():
1335
suffix = unicode(s[-1])
1338
delta = datetime.timedelta(value)
1339
elif suffix == u"s":
1340
delta = datetime.timedelta(0, value)
1341
elif suffix == u"m":
1342
delta = datetime.timedelta(0, 0, 0, 0, value)
1343
elif suffix == u"h":
1344
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
1345
elif suffix == u"w":
1346
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1349
except (ValueError, IndexError):
484
suffix=unicode(interval[-1])
485
value=int(interval[:-1])
487
delta = datetime.timedelta(value)
489
delta = datetime.timedelta(0, value)
491
delta = datetime.timedelta(0, 0, 0, 0, value)
493
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
495
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1350
497
raise ValueError
498
except (ValueError, IndexError):
504
"""Derived from the Avahi example code"""
505
global group, serviceName, serviceType, servicePort, serviceTXT, \
508
group = dbus.Interface(
509
bus.get_object( avahi.DBUS_NAME,
510
server.EntryGroupNew()),
511
avahi.DBUS_INTERFACE_ENTRY_GROUP)
512
group.connect_to_signal('StateChanged',
513
entry_group_state_changed)
514
logger.debug(u"Adding service '%s' of type '%s' ...",
515
serviceName, serviceType)
518
serviceInterface, # interface
519
avahi.PROTO_INET6, # protocol
520
dbus.UInt32(0), # flags
521
serviceName, serviceType,
523
dbus.UInt16(servicePort),
524
avahi.string_array_to_txt_array(serviceTXT))
528
def remove_service():
529
"""From the Avahi example code"""
532
if not group is None:
536
def server_state_changed(state):
537
"""Derived from the Avahi example code"""
538
if state == avahi.SERVER_COLLISION:
539
logger.warning(u"Server name collision")
541
elif state == avahi.SERVER_RUNNING:
545
def entry_group_state_changed(state, error):
546
"""Derived from the Avahi example code"""
547
global serviceName, server, rename_count
549
logger.debug(u"state change: %i", state)
551
if state == avahi.ENTRY_GROUP_ESTABLISHED:
552
logger.debug(u"Service established.")
553
elif state == avahi.ENTRY_GROUP_COLLISION:
555
rename_count = rename_count - 1
557
name = server.GetAlternativeServiceName(name)
558
logger.warning(u"Service name collision, "
559
u"changing name to '%s' ...", name)
564
logger.error(u"No suitable service name found after %i"
565
u" retries, exiting.", n_rename)
567
elif state == avahi.ENTRY_GROUP_FAILURE:
568
logger.error(u"Error in group state changed %s",
1355
573
def if_nametoindex(interface):
1356
"""Call the C function if_nametoindex(), or equivalent
1358
Note: This function cannot accept a unicode string."""
1359
global if_nametoindex
574
"""Call the C function if_nametoindex()"""
1361
if_nametoindex = (ctypes.cdll.LoadLibrary
1362
(ctypes.util.find_library(u"c"))
576
libc = ctypes.cdll.LoadLibrary("libc.so.6")
577
return libc.if_nametoindex(interface)
1364
578
except (OSError, AttributeError):
1365
logger.warning(u"Doing if_nametoindex the hard way")
1366
def if_nametoindex(interface):
1367
"Get an interface index the hard way, i.e. using fcntl()"
1368
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
1369
with closing(socket.socket()) as s:
1370
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1371
struct.pack(str(u"16s16x"),
1373
interface_index = struct.unpack(str(u"I"),
1375
return interface_index
1376
return if_nametoindex(interface)
1379
def daemon(nochdir = False, noclose = False):
579
if "struct" not in sys.modules:
581
if "fcntl" not in sys.modules:
583
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
585
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
586
struct.pack("16s16x", interface))
588
interface_index = struct.unpack("I", ifreq[16:20])[0]
589
return interface_index
592
def daemon(nochdir, noclose):
1380
593
"""See daemon(3). Standard BSD Unix function.
1382
594
This should really exist as os.daemon, but it doesn't (yet)."""
1391
601
# Close all standard open file descriptors
1392
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
602
null = os.open("/dev/null", os.O_NOCTTY | os.O_RDWR)
1393
603
if not stat.S_ISCHR(os.fstat(null).st_mode):
1394
604
raise OSError(errno.ENODEV,
1395
u"/dev/null not a character device")
605
"/dev/null not a character device")
1396
606
os.dup2(null, sys.stdin.fileno())
1397
607
os.dup2(null, sys.stdout.fileno())
1398
608
os.dup2(null, sys.stderr.fileno())
613
def killme(status = 0):
614
logger.debug("Stopping server with exit status %d", status)
616
if main_loop_started:
1405
##################################################################
1406
# Parsing of options, both command line and config file
1408
parser = optparse.OptionParser(version = "%%prog %s" % version)
1409
parser.add_option("-i", u"--interface", type=u"string",
1410
metavar="IF", help=u"Bind to interface IF")
1411
parser.add_option("-a", u"--address", type=u"string",
1412
help=u"Address to listen for requests on")
1413
parser.add_option("-p", u"--port", type=u"int",
1414
help=u"Port number to receive requests on")
1415
parser.add_option("--check", action=u"store_true",
1416
help=u"Run self-test")
1417
parser.add_option("--debug", action=u"store_true",
1418
help=u"Debug mode; run in foreground and log to"
1420
parser.add_option("--priority", type=u"string", help=u"GnuTLS"
1421
u" priority string (see GnuTLS documentation)")
1422
parser.add_option("--servicename", type=u"string",
1423
metavar=u"NAME", help=u"Zeroconf service name")
1424
parser.add_option("--configdir", type=u"string",
1425
default=u"/etc/mandos", metavar=u"DIR",
1426
help=u"Directory to search for configuration"
1428
parser.add_option("--no-dbus", action=u"store_false",
1429
dest=u"use_dbus", help=u"Do not provide D-Bus"
1430
u" system bus interface")
1431
parser.add_option("--no-ipv6", action=u"store_false",
1432
dest=u"use_ipv6", help=u"Do not use IPv6")
1433
options = parser.parse_args()[0]
625
global main_loop_started
626
main_loop_started = False
628
parser = OptionParser()
629
parser.add_option("-i", "--interface", type="string",
630
default=None, metavar="IF",
631
help="Bind to interface IF")
632
parser.add_option("-a", "--address", type="string", default=None,
633
help="Address to listen for requests on")
634
parser.add_option("-p", "--port", type="int", default=None,
635
help="Port number to receive requests on")
636
parser.add_option("--check", action="store_true", default=False,
637
help="Run self-test")
638
parser.add_option("--debug", action="store_true", default=False,
640
parser.add_option("--priority", type="string",
642
help="GnuTLS priority string"
643
" (see GnuTLS documentation)")
644
parser.add_option("--servicename", type="string",
645
default="Mandos", help="Zeroconf service name")
646
(options, args) = parser.parse_args()
1435
648
if options.check:
1437
650
doctest.testmod()
1440
# Default values for config file for server-global settings
1441
server_defaults = { u"interface": u"",
1446
u"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1447
u"servicename": u"Mandos",
1448
u"use_dbus": u"True",
1449
u"use_ipv6": u"True",
1452
# Parse config file for server-global settings
1453
server_config = configparser.SafeConfigParser(server_defaults)
1455
server_config.read(os.path.join(options.configdir,
1457
# Convert the SafeConfigParser object to a dict
1458
server_settings = server_config.defaults()
1459
# Use the appropriate methods on the non-string config options
1460
for option in (u"debug", u"use_dbus", u"use_ipv6"):
1461
server_settings[option] = server_config.getboolean(u"DEFAULT",
1463
if server_settings["port"]:
1464
server_settings["port"] = server_config.getint(u"DEFAULT",
1468
# Override the settings from the config file with command line
1470
for option in (u"interface", u"address", u"port", u"debug",
1471
u"priority", u"servicename", u"configdir",
1472
u"use_dbus", u"use_ipv6"):
1473
value = getattr(options, option)
1474
if value is not None:
1475
server_settings[option] = value
1477
# Force all strings to be unicode
1478
for option in server_settings.keys():
1479
if type(server_settings[option]) is str:
1480
server_settings[option] = unicode(server_settings[option])
1481
# Now we have our good server settings in "server_settings"
1483
##################################################################
1486
debug = server_settings[u"debug"]
1487
use_dbus = server_settings[u"use_dbus"]
1488
use_ipv6 = server_settings[u"use_ipv6"]
1491
syslogger.setLevel(logging.WARNING)
1492
console.setLevel(logging.WARNING)
1494
if server_settings[u"servicename"] != u"Mandos":
1495
syslogger.setFormatter(logging.Formatter
1496
(u'Mandos (%s) [%%(process)d]:'
1497
u' %%(levelname)s: %%(message)s'
1498
% server_settings[u"servicename"]))
1500
# Parse config file with clients
1501
client_defaults = { u"timeout": u"1h",
1503
u"checker": u"fping -q -- %%(host)s",
1506
client_config = configparser.SafeConfigParser(client_defaults)
1507
client_config.read(os.path.join(server_settings[u"configdir"],
1510
global mandos_dbus_service
1511
mandos_dbus_service = None
1513
tcp_server = MandosServer((server_settings[u"address"],
1514
server_settings[u"port"]),
1516
interface=server_settings[u"interface"],
1519
server_settings[u"priority"],
1521
pidfilename = u"/var/run/mandos.pid"
1523
pidfile = open(pidfilename, u"w")
1525
logger.error(u"Could not open file %r", pidfilename)
1528
uid = pwd.getpwnam(u"_mandos").pw_uid
1529
gid = pwd.getpwnam(u"_mandos").pw_gid
1532
uid = pwd.getpwnam(u"mandos").pw_uid
1533
gid = pwd.getpwnam(u"mandos").pw_gid
1536
uid = pwd.getpwnam(u"nobody").pw_uid
1537
gid = pwd.getpwnam(u"nobody").pw_gid
1544
except OSError, error:
1545
if error[0] != errno.EPERM:
1548
# Enable all possible GnuTLS debugging
1550
# "Use a log level over 10 to enable all debugging options."
1552
gnutls.library.functions.gnutls_global_set_log_level(11)
1554
@gnutls.library.types.gnutls_log_func
1555
def debug_gnutls(level, string):
1556
logger.debug(u"GnuTLS: %s", string[:-1])
1558
(gnutls.library.functions
1559
.gnutls_global_set_log_function(debug_gnutls))
654
defaults = { "timeout": "1h",
656
"checker": "fping -q -- %%(fqdn)s",
658
client_config = ConfigParser.SafeConfigParser(defaults)
659
#client_config.readfp(open("global.conf"), "global.conf")
660
client_config.read("mandos-clients.conf")
663
serviceName = options.servicename;
1561
665
global main_loop
1562
668
# From the Avahi example code
1563
669
DBusGMainLoop(set_as_default=True )
1564
670
main_loop = gobject.MainLoop()
1565
671
bus = dbus.SystemBus()
672
server = dbus.Interface(
673
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
674
avahi.DBUS_INTERFACE_SERVER )
1566
675
# End of Avahi example code
1568
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1569
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1570
service = AvahiService(name = server_settings[u"servicename"],
1571
servicetype = u"_mandos._tcp",
1572
protocol = protocol, bus = bus)
1573
if server_settings["interface"]:
1574
service.interface = (if_nametoindex
1575
(str(server_settings[u"interface"])))
1577
client_class = Client
1579
client_class = functools.partial(ClientDBus, bus = bus)
1580
tcp_server.clients.update(set(
1581
client_class(name = section,
1582
config= dict(client_config.items(section)))
1583
for section in client_config.sections()))
1584
if not tcp_server.clients:
1585
logger.warning(u"No clients defined")
677
debug = options.debug
1588
# Redirect stdin so all checkers get /dev/null
1589
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1590
os.dup2(null, sys.stdin.fileno())
1594
# No console logging
1595
logger.removeHandler(console)
1596
# Close all input and output, do double fork, etc.
1600
with closing(pidfile):
1602
pidfile.write(str(pid) + "\n")
1605
logger.error(u"Could not write to file %r with PID %d",
1608
# "pidfile" was never created
680
console = logging.StreamHandler()
681
# console.setLevel(logging.DEBUG)
682
console.setFormatter(logging.Formatter\
683
('%(levelname)s: %(message)s'))
684
logger.addHandler(console)
688
def remove_from_clients(client):
689
clients.remove(client)
691
logger.debug(u"No clients left, exiting")
694
clients.update(Set(Client(name=section,
695
stop_hook = remove_from_clients,
696
**(dict(client_config\
698
for section in client_config.sections()))
1613
704
"Cleanup function; run on exit"
706
# From the Avahi example code
707
if not group is None:
710
# End of Avahi example code
1616
while tcp_server.clients:
1617
client = tcp_server.clients.pop()
1618
client.disable_hook = None
713
client = clients.pop()
714
client.stop_hook = None
1621
717
atexit.register(cleanup)
1624
720
signal.signal(signal.SIGINT, signal.SIG_IGN)
1625
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1626
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1629
class MandosDBusService(dbus.service.Object):
1630
"""A D-Bus proxy object"""
1632
dbus.service.Object.__init__(self, bus, u"/")
1633
_interface = u"se.bsnet.fukt.Mandos"
1635
@dbus.service.signal(_interface, signature=u"oa{sv}")
1636
def ClientAdded(self, objpath, properties):
1640
@dbus.service.signal(_interface, signature=u"s")
1641
def ClientNotFound(self, fingerprint):
1645
@dbus.service.signal(_interface, signature=u"os")
1646
def ClientRemoved(self, objpath, name):
1650
@dbus.service.method(_interface, out_signature=u"ao")
1651
def GetAllClients(self):
1653
return dbus.Array(c.dbus_object_path
1654
for c in tcp_server.clients)
1656
@dbus.service.method(_interface,
1657
out_signature=u"a{oa{sv}}")
1658
def GetAllClientsWithProperties(self):
1660
return dbus.Dictionary(
1661
((c.dbus_object_path, c.GetAll(u""))
1662
for c in tcp_server.clients),
1663
signature=u"oa{sv}")
1665
@dbus.service.method(_interface, in_signature=u"o")
1666
def RemoveClient(self, object_path):
1668
for c in tcp_server.clients:
1669
if c.dbus_object_path == object_path:
1670
tcp_server.clients.remove(c)
1671
c.remove_from_connection()
1672
# Don't signal anything except ClientRemoved
1673
c.disable(signal=False)
1675
self.ClientRemoved(object_path, c.name)
1681
mandos_dbus_service = MandosDBusService()
1683
for client in tcp_server.clients:
1686
mandos_dbus_service.ClientAdded(client.dbus_object_path,
1691
tcp_server.server_activate()
1693
# Find out what port we got
1694
service.port = tcp_server.socket.getsockname()[1]
1696
logger.info(u"Now listening on address %r, port %d,"
1697
" flowinfo %d, scope_id %d"
1698
% tcp_server.socket.getsockname())
1700
logger.info(u"Now listening on address %r, port %d"
1701
% tcp_server.socket.getsockname())
1703
#service.interface = tcp_server.socket.getsockname()[3]
1706
# From the Avahi example code
1709
except dbus.exceptions.DBusException, error:
1710
logger.critical(u"DBusException: %s", error)
1712
# End of Avahi example code
1714
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1715
lambda *args, **kwargs:
1716
(tcp_server.handle_request
1717
(*args[2:], **kwargs) or True))
1719
logger.debug(u"Starting main loop")
721
signal.signal(signal.SIGHUP, lambda signum, frame: killme())
722
signal.signal(signal.SIGTERM, lambda signum, frame: killme())
724
for client in clients:
727
tcp_server = IPv6_TCPServer((options.address, options.port),
731
# Find out what random port we got
733
servicePort = tcp_server.socket.getsockname()[1]
734
logger.debug(u"Now listening on port %d", servicePort)
736
if options.interface is not None:
737
global serviceInterface
738
serviceInterface = if_nametoindex(options.interface)
740
# From the Avahi example code
741
server.connect_to_signal("StateChanged", server_state_changed)
743
server_state_changed(server.GetState())
744
except dbus.exceptions.DBusException, error:
745
logger.critical(u"DBusException: %s", error)
747
# End of Avahi example code
749
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
750
lambda *args, **kwargs:
751
tcp_server.handle_request(*args[2:],
754
logger.debug("Starting main loop")
755
main_loop_started = True
1721
except AvahiError, error:
1722
logger.critical(u"AvahiError: %s", error)
1724
757
except KeyboardInterrupt:
1727
logger.debug(u"Server received KeyboardInterrupt")
1728
logger.debug(u"Server exiting")
1730
763
if __name__ == '__main__':