98
116
class AvahiService(object):
117
"""An Avahi (Zeroconf) service.
100
120
interface: integer; avahi.IF_UNSPEC or an interface index.
101
121
Used to optionally bind to the specified interface.
102
name = string; Example: "Mandos"
103
type = string; Example: "_mandos._tcp".
104
See <http://www.dns-sd.org/ServiceTypes.html>
105
port = integer; what port to announce
106
TXT = list of strings; TXT record for the service
107
domain = string; Domain to publish on, default to .local if empty.
108
host = string; Host to publish records for, default to localhost
110
max_renames = integer; maximum number of renames
111
rename_count = integer; counter so we only rename after collisions
112
a sensible number of times
122
name: string; Example: u'Mandos'
123
type: string; Example: u'_mandos._tcp'.
124
See <http://www.dns-sd.org/ServiceTypes.html>
125
port: integer; what port to announce
126
TXT: list of strings; TXT record for the service
127
domain: string; Domain to publish on, default to .local if empty.
128
host: string; Host to publish records for, default is localhost
129
max_renames: integer; maximum number of renames
130
rename_count: integer; counter so we only rename after collisions
131
a sensible number of times
132
group: D-Bus Entry Group
134
bus: dbus.SystemBus()
114
136
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
115
type = None, port = None, TXT = None, domain = "",
116
host = "", max_renames = 12):
117
"""An Avahi (Zeroconf) service. """
137
servicetype = None, port = None, TXT = None,
138
domain = u"", host = u"", max_renames = 32768,
139
protocol = avahi.PROTO_UNSPEC, bus = None):
118
140
self.interface = interface
142
self.type = servicetype
144
self.TXT = TXT if TXT is not None else []
126
145
self.domain = domain
128
147
self.rename_count = 0
148
self.max_renames = max_renames
149
self.protocol = protocol
150
self.group = None # our entry group
129
153
def rename(self):
130
154
"""Derived from the Avahi example code"""
131
155
if self.rename_count >= self.max_renames:
132
logger.critical(u"No suitable service name found after %i"
133
u" retries, exiting.", rename_count)
134
raise AvahiServiceError("Too many renames")
135
name = server.GetAlternativeServiceName(name)
136
logger.notice(u"Changing name to %r ...", name)
156
logger.critical(u"No suitable Zeroconf service name found"
157
u" after %i retries, exiting.",
159
raise AvahiServiceError(u"Too many renames")
160
self.name = self.server.GetAlternativeServiceName(self.name)
161
logger.info(u"Changing Zeroconf service name to %r ...",
163
syslogger.setFormatter(logging.Formatter
164
(u'Mandos (%s) [%%(process)d]:'
165
u' %%(levelname)s: %%(message)s'
139
169
self.rename_count += 1
140
170
def remove(self):
141
171
"""Derived from the Avahi example code"""
142
if group is not None:
172
if self.group is not None:
145
175
"""Derived from the Avahi example code"""
148
group = dbus.Interface\
149
(bus.get_object(avahi.DBUS_NAME,
150
server.EntryGroupNew()),
151
avahi.DBUS_INTERFACE_ENTRY_GROUP)
152
group.connect_to_signal('StateChanged',
153
entry_group_state_changed)
154
logger.debug(u"Adding service '%s' of type '%s' ...",
155
service.name, service.type)
157
self.interface, # interface
158
avahi.PROTO_INET6, # protocol
159
dbus.UInt32(0), # flags
160
self.name, self.type,
161
self.domain, self.host,
162
dbus.UInt16(self.port),
163
avahi.string_array_to_txt_array(self.TXT))
166
# From the Avahi example code:
167
group = None # our entry group
168
# End of Avahi example code
176
if self.group is None:
177
self.group = dbus.Interface(
178
self.bus.get_object(avahi.DBUS_NAME,
179
self.server.EntryGroupNew()),
180
avahi.DBUS_INTERFACE_ENTRY_GROUP)
181
self.group.connect_to_signal('StateChanged',
183
.entry_group_state_changed)
184
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
185
self.name, self.type)
186
self.group.AddService(
189
dbus.UInt32(0), # flags
190
self.name, self.type,
191
self.domain, self.host,
192
dbus.UInt16(self.port),
193
avahi.string_array_to_txt_array(self.TXT))
195
def entry_group_state_changed(self, state, error):
196
"""Derived from the Avahi example code"""
197
logger.debug(u"Avahi state change: %i", state)
199
if state == avahi.ENTRY_GROUP_ESTABLISHED:
200
logger.debug(u"Zeroconf service established.")
201
elif state == avahi.ENTRY_GROUP_COLLISION:
202
logger.warning(u"Zeroconf service name collision.")
204
elif state == avahi.ENTRY_GROUP_FAILURE:
205
logger.critical(u"Avahi: Error in group state changed %s",
207
raise AvahiGroupError(u"State changed: %s"
210
"""Derived from the Avahi example code"""
211
if self.group is not None:
214
def server_state_changed(self, state):
215
"""Derived from the Avahi example code"""
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())
171
233
class Client(object):
172
234
"""A representation of a client host served by this server.
174
name: string; from the config file, used in log messages
237
name: string; from the config file, used in log messages and
175
239
fingerprint: string (40 or 32 hexadecimal digits); used to
176
240
uniquely identify the client
177
secret: bytestring; sent verbatim (over TLS) to client
178
fqdn: string (FQDN); available for use by the checker command
179
created: datetime.datetime(); object creation, not client host
180
last_checked_ok: datetime.datetime() or None if not yet checked OK
181
timeout: datetime.timedelta(); How long from last_checked_ok
182
until this client is invalid
183
interval: datetime.timedelta(); How often to start a new checker
184
stop_hook: If set, called by stop() as stop_hook(self)
185
checker: subprocess.Popen(); a running checker process used
186
to see if the client lives.
187
'None' if no process is running.
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.
188
254
checker_initiator_tag: a gobject event source tag, or None
189
stop_initiator_tag: - '' -
255
disable_initiator_tag: - '' -
190
256
checker_callback_tag: - '' -
191
257
checker_command: string; External command which is run to check if
192
258
client lives. %() expansions are done at
193
259
runtime with vars(self) as dict, so that for
194
260
instance %(name)s can be used in the command.
196
_timeout: Real variable for 'timeout'
197
_interval: Real variable for 'interval'
198
_timeout_milliseconds: Used when calling gobject.timeout_add()
199
_interval_milliseconds: - '' -
261
current_checker_command: string; current running checker_command
262
approved_delay: datetime.timedelta(); Time to wait for approval
263
_approved: bool(); 'None' if not yet approved/disapproved
264
approved_duration: datetime.timedelta(); Duration of one approval
201
def _set_timeout(self, timeout):
202
"Setter function for 'timeout' attribute"
203
self._timeout = timeout
204
self._timeout_milliseconds = ((self.timeout.days
205
* 24 * 60 * 60 * 1000)
206
+ (self.timeout.seconds * 1000)
207
+ (self.timeout.microseconds
209
timeout = property(lambda self: self._timeout,
212
def _set_interval(self, interval):
213
"Setter function for 'interval' attribute"
214
self._interval = interval
215
self._interval_milliseconds = ((self.interval.days
216
* 24 * 60 * 60 * 1000)
217
+ (self.interval.seconds
219
+ (self.interval.microseconds
221
interval = property(lambda self: self._interval,
224
def __init__(self, name=None, stop_hook=None, fingerprint=None,
225
secret=None, secfile=None, fqdn=None, timeout=None,
226
interval=-1, checker=None):
227
"""Note: the 'checker' argument sets the 'checker_command'
228
attribute and not the 'checker' attribute.."""
268
def _timedelta_to_milliseconds(td):
269
"Convert a datetime.timedelta() to milliseconds"
270
return ((td.days * 24 * 60 * 60 * 1000)
271
+ (td.seconds * 1000)
272
+ (td.microseconds // 1000))
274
def timeout_milliseconds(self):
275
"Return the 'timeout' attribute in milliseconds"
276
return self._timedelta_to_milliseconds(self.timeout)
278
def interval_milliseconds(self):
279
"Return the 'interval' attribute in milliseconds"
280
return self._timedelta_to_milliseconds(self.interval)
282
def approved_delay_milliseconds(self):
283
return self._timedelta_to_milliseconds(self.approved_delay)
285
def __init__(self, name = None, disable_hook=None, config=None):
286
"""Note: the 'checker' key in 'config' sets the
287
'checker_command' attribute and *not* the 'checker'
230
292
logger.debug(u"Creating client %r", self.name)
231
# Uppercase and remove spaces from fingerprint
232
# for later comparison purposes with return value of
233
# the fingerprint() function
234
self.fingerprint = fingerprint.upper().replace(u" ", u"")
293
# Uppercase and remove spaces from fingerprint for later
294
# comparison purposes with return value from the fingerprint()
296
self.fingerprint = (config[u"fingerprint"].upper()
235
298
logger.debug(u" Fingerprint: %s", self.fingerprint)
237
self.secret = secret.decode(u"base64")
240
self.secret = sf.read()
299
if u"secret" in config:
300
self.secret = config[u"secret"].decode(u"base64")
301
elif u"secfile" in config:
302
with open(os.path.expanduser(os.path.expandvars
303
(config[u"secfile"])),
305
self.secret = secfile.read()
307
#XXX Need to allow secret on demand!
243
308
raise TypeError(u"No secret or secfile for client %s"
246
self.created = datetime.datetime.now()
310
self.host = config.get(u"host", u"")
311
self.created = datetime.datetime.utcnow()
313
self.last_enabled = None
247
314
self.last_checked_ok = None
248
self.timeout = string_to_delta(timeout)
249
self.interval = string_to_delta(interval)
250
self.stop_hook = stop_hook
315
self.timeout = string_to_delta(config[u"timeout"])
316
self.interval = string_to_delta(config[u"interval"])
317
self.disable_hook = disable_hook
251
318
self.checker = None
252
319
self.checker_initiator_tag = None
253
self.stop_initiator_tag = None
320
self.disable_initiator_tag = None
254
321
self.checker_callback_tag = None
255
self.check_command = checker
322
self.checker_command = config[u"checker"]
323
self.current_checker_command = None
324
self.last_connect = None
325
self._approved = None
326
self.approved_by_default = config.get(u"approved_by_default",
328
self.approvals_pending = 0
329
self.approved_delay = string_to_delta(
330
config[u"approved_delay"])
331
self.approved_duration = string_to_delta(
332
config[u"approved_duration"])
333
self.changedstate = multiprocessing_manager.Condition(multiprocessing_manager.Lock())
335
def send_changedstate(self):
336
self.changedstate.acquire()
337
self.changedstate.notify_all()
338
self.changedstate.release()
257
341
"""Start this client's checker and timeout hooks"""
342
if getattr(self, u"enabled", False):
345
self.send_changedstate()
346
self.last_enabled = datetime.datetime.utcnow()
258
347
# Schedule a new checker to be started an 'interval' from now,
259
348
# and every interval from then on.
260
self.checker_initiator_tag = gobject.timeout_add\
261
(self._interval_milliseconds,
349
self.checker_initiator_tag = (gobject.timeout_add
350
(self.interval_milliseconds(),
352
# Schedule a disable() when 'timeout' has passed
353
self.disable_initiator_tag = (gobject.timeout_add
354
(self.timeout_milliseconds(),
263
357
# Also start a new checker *right now*.
264
358
self.start_checker()
265
# Schedule a stop() when 'timeout' has passed
266
self.stop_initiator_tag = gobject.timeout_add\
267
(self._timeout_milliseconds,
271
The possibility that a client might be restarted is left open,
272
but not currently used."""
273
# If this client doesn't have a secret, it is already stopped.
275
logger.debug(u"Stopping client %s", self.name)
360
def disable(self, quiet=True):
361
"""Disable this client."""
362
if not getattr(self, "enabled", False):
279
if getattr(self, "stop_initiator_tag", False):
280
gobject.source_remove(self.stop_initiator_tag)
281
self.stop_initiator_tag = None
282
if getattr(self, "checker_initiator_tag", False):
365
self.send_changedstate()
367
logger.info(u"Disabling client %s", self.name)
368
if getattr(self, u"disable_initiator_tag", False):
369
gobject.source_remove(self.disable_initiator_tag)
370
self.disable_initiator_tag = None
371
if getattr(self, u"checker_initiator_tag", False):
283
372
gobject.source_remove(self.checker_initiator_tag)
284
373
self.checker_initiator_tag = None
285
374
self.stop_checker()
375
if self.disable_hook:
376
self.disable_hook(self)
288
378
# Do not run this again if called by a gobject.timeout_add
290
381
def __del__(self):
291
self.stop_hook = None
293
def checker_callback(self, pid, condition):
382
self.disable_hook = None
385
def checker_callback(self, pid, condition, command):
294
386
"""The checker has completed, so take appropriate actions."""
295
now = datetime.datetime.now()
296
387
self.checker_callback_tag = None
297
388
self.checker = None
298
if os.WIFEXITED(condition) \
299
and (os.WEXITSTATUS(condition) == 0):
300
logger.debug(u"Checker for %(name)s succeeded",
302
self.last_checked_ok = now
303
gobject.source_remove(self.stop_initiator_tag)
304
self.stop_initiator_tag = gobject.timeout_add\
305
(self._timeout_milliseconds,
307
elif not os.WIFEXITED(condition):
389
if os.WIFEXITED(condition):
390
exitstatus = os.WEXITSTATUS(condition)
392
logger.info(u"Checker for %(name)s succeeded",
396
logger.info(u"Checker for %(name)s failed",
308
399
logger.warning(u"Checker for %(name)s crashed?",
311
logger.debug(u"Checker for %(name)s failed",
402
def checked_ok(self):
403
"""Bump up the timeout for this client.
405
This should only be called when the client has been seen,
408
self.last_checked_ok = datetime.datetime.utcnow()
409
gobject.source_remove(self.disable_initiator_tag)
410
self.disable_initiator_tag = (gobject.timeout_add
411
(self.timeout_milliseconds(),
313
414
def start_checker(self):
314
415
"""Start a new checker subprocess if one is not running.
315
417
If a checker already exists, leave it running and do
317
419
# The reason for not killing a running checker is that if we
320
422
# client would inevitably timeout, since no checker would get
321
423
# a chance to run to completion. If we instead leave running
322
424
# checkers alone, the checker would have to take more time
323
# than 'timeout' for the client to be declared invalid, which
324
# is as it should be.
425
# than 'timeout' for the client to be disabled, which is as it
428
# If a checker exists, make sure it is not a zombie
430
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
431
except (AttributeError, OSError), error:
432
if (isinstance(error, OSError)
433
and error.errno != errno.ECHILD):
437
logger.warning(u"Checker was a zombie")
438
gobject.source_remove(self.checker_callback_tag)
439
self.checker_callback(pid, status,
440
self.current_checker_command)
441
# Start a new checker if needed
325
442
if self.checker is None:
327
# In case check_command has exactly one % operator
328
command = self.check_command % self.fqdn
444
# In case checker_command has exactly one % operator
445
command = self.checker_command % self.host
329
446
except TypeError:
330
447
# Escape attributes for the shell
331
escaped_attrs = dict((key, re.escape(str(val)))
448
escaped_attrs = dict((key,
449
re.escape(unicode(str(val),
333
453
vars(self).iteritems())
335
command = self.check_command % escaped_attrs
455
command = self.checker_command % escaped_attrs
336
456
except TypeError, error:
337
457
logger.error(u'Could not format string "%s":'
338
u' %s', self.check_command, error)
458
u' %s', self.checker_command, error)
339
459
return True # Try again later
460
self.current_checker_command = command
341
logger.debug(u"Starting checker %r for %s",
462
logger.info(u"Starting checker %r for %s",
464
# We don't need to redirect stdout and stderr, since
465
# in normal mode, that is already done by daemon(),
466
# and in debug mode we don't want to. (Stdin is
467
# always replaced by /dev/null.)
343
468
self.checker = subprocess.Popen(command,
346
self.checker_callback_tag = gobject.child_watch_add\
348
self.checker_callback)
349
except subprocess.OSError, error:
470
shell=True, cwd=u"/")
471
self.checker_callback_tag = (gobject.child_watch_add
473
self.checker_callback,
475
# The checker may have completed before the gobject
476
# watch was added. Check for this.
477
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
479
gobject.source_remove(self.checker_callback_tag)
480
self.checker_callback(pid, status, command)
481
except OSError, error:
350
482
logger.error(u"Failed to start subprocess: %s",
352
484
# Re-run this periodically if run by gobject.timeout_add
354
487
def stop_checker(self):
355
488
"""Force the checker process, if any, to stop."""
356
489
if self.checker_callback_tag:
357
490
gobject.source_remove(self.checker_callback_tag)
358
491
self.checker_callback_tag = None
359
if getattr(self, "checker", None) is None:
492
if getattr(self, u"checker", None) is None:
361
logger.debug("Stopping checker for %(name)s", vars(self))
494
logger.debug(u"Stopping checker for %(name)s", vars(self))
363
496
os.kill(self.checker.pid, signal.SIGTERM)
365
498
#if self.checker.poll() is None:
366
499
# os.kill(self.checker.pid, signal.SIGKILL)
367
500
except OSError, error:
368
501
if error.errno != errno.ESRCH: # No such process
370
503
self.checker = None
371
def still_valid(self):
372
"""Has the timeout not yet passed for this client?"""
373
now = datetime.datetime.now()
505
def dbus_service_property(dbus_interface, signature=u"v",
506
access=u"readwrite", byte_arrays=False):
507
"""Decorators for marking methods of a DBusObjectWithProperties to
508
become properties on the D-Bus.
510
The decorated method will be called with no arguments by "Get"
511
and with one argument by "Set".
513
The parameters, where they are supported, are the same as
514
dbus.service.method, except there is only "signature", since the
515
type from Get() and the type sent to Set() is the same.
517
# Encoding deeply encoded byte arrays is not supported yet by the
518
# "Set" method, so we fail early here:
519
if byte_arrays and signature != u"ay":
520
raise ValueError(u"Byte arrays not supported for non-'ay'"
521
u" signature %r" % signature)
523
func._dbus_is_property = True
524
func._dbus_interface = dbus_interface
525
func._dbus_signature = signature
526
func._dbus_access = access
527
func._dbus_name = func.__name__
528
if func._dbus_name.endswith(u"_dbus_property"):
529
func._dbus_name = func._dbus_name[:-14]
530
func._dbus_get_args_options = {u'byte_arrays': byte_arrays }
535
class DBusPropertyException(dbus.exceptions.DBusException):
536
"""A base class for D-Bus property-related exceptions
538
def __unicode__(self):
539
return unicode(str(self))
542
class DBusPropertyAccessException(DBusPropertyException):
543
"""A property's access permissions disallows an operation.
548
class DBusPropertyNotFound(DBusPropertyException):
549
"""An attempt was made to access a non-existing property.
554
class DBusObjectWithProperties(dbus.service.Object):
555
"""A D-Bus object with properties.
557
Classes inheriting from this can use the dbus_service_property
558
decorator to expose methods as D-Bus properties. It exposes the
559
standard Get(), Set(), and GetAll() methods on the D-Bus.
563
def _is_dbus_property(obj):
564
return getattr(obj, u"_dbus_is_property", False)
566
def _get_all_dbus_properties(self):
567
"""Returns a generator of (name, attribute) pairs
569
return ((prop._dbus_name, prop)
571
inspect.getmembers(self, self._is_dbus_property))
573
def _get_dbus_property(self, interface_name, property_name):
574
"""Returns a bound method if one exists which is a D-Bus
575
property with the specified name and interface.
577
for name in (property_name,
578
property_name + u"_dbus_property"):
579
prop = getattr(self, name, None)
581
or not self._is_dbus_property(prop)
582
or prop._dbus_name != property_name
583
or (interface_name and prop._dbus_interface
584
and interface_name != prop._dbus_interface)):
588
raise DBusPropertyNotFound(self.dbus_object_path + u":"
589
+ interface_name + u"."
592
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ss",
594
def Get(self, interface_name, property_name):
595
"""Standard D-Bus property Get() method, see D-Bus standard.
597
prop = self._get_dbus_property(interface_name, property_name)
598
if prop._dbus_access == u"write":
599
raise DBusPropertyAccessException(property_name)
601
if not hasattr(value, u"variant_level"):
603
return type(value)(value, variant_level=value.variant_level+1)
605
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ssv")
606
def Set(self, interface_name, property_name, value):
607
"""Standard D-Bus property Set() method, see D-Bus standard.
609
prop = self._get_dbus_property(interface_name, property_name)
610
if prop._dbus_access == u"read":
611
raise DBusPropertyAccessException(property_name)
612
if prop._dbus_get_args_options[u"byte_arrays"]:
613
# The byte_arrays option is not supported yet on
614
# signatures other than "ay".
615
if prop._dbus_signature != u"ay":
617
value = dbus.ByteArray(''.join(unichr(byte)
621
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"s",
622
out_signature=u"a{sv}")
623
def GetAll(self, interface_name):
624
"""Standard D-Bus property GetAll() method, see D-Bus
627
Note: Will not include properties with access="write".
630
for name, prop in self._get_all_dbus_properties():
632
and interface_name != prop._dbus_interface):
633
# Interface non-empty but did not match
635
# Ignore write-only properties
636
if prop._dbus_access == u"write":
639
if not hasattr(value, u"variant_level"):
642
all[name] = type(value)(value, variant_level=
643
value.variant_level+1)
644
return dbus.Dictionary(all, signature=u"sv")
646
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
648
path_keyword='object_path',
649
connection_keyword='connection')
650
def Introspect(self, object_path, connection):
651
"""Standard D-Bus method, overloaded to insert property tags.
653
xmlstring = dbus.service.Object.Introspect(self, object_path,
656
document = xml.dom.minidom.parseString(xmlstring)
657
def make_tag(document, name, prop):
658
e = document.createElement(u"property")
659
e.setAttribute(u"name", name)
660
e.setAttribute(u"type", prop._dbus_signature)
661
e.setAttribute(u"access", prop._dbus_access)
663
for if_tag in document.getElementsByTagName(u"interface"):
664
for tag in (make_tag(document, name, prop)
666
in self._get_all_dbus_properties()
667
if prop._dbus_interface
668
== if_tag.getAttribute(u"name")):
669
if_tag.appendChild(tag)
670
# Add the names to the return values for the
671
# "org.freedesktop.DBus.Properties" methods
672
if (if_tag.getAttribute(u"name")
673
== u"org.freedesktop.DBus.Properties"):
674
for cn in if_tag.getElementsByTagName(u"method"):
675
if cn.getAttribute(u"name") == u"Get":
676
for arg in cn.getElementsByTagName(u"arg"):
677
if (arg.getAttribute(u"direction")
679
arg.setAttribute(u"name", u"value")
680
elif cn.getAttribute(u"name") == u"GetAll":
681
for arg in cn.getElementsByTagName(u"arg"):
682
if (arg.getAttribute(u"direction")
684
arg.setAttribute(u"name", u"props")
685
xmlstring = document.toxml(u"utf-8")
687
except (AttributeError, xml.dom.DOMException,
688
xml.parsers.expat.ExpatError), error:
689
logger.error(u"Failed to override Introspection method",
694
class ClientDBus(Client, DBusObjectWithProperties):
695
"""A Client class using D-Bus
698
dbus_object_path: dbus.ObjectPath
699
bus: dbus.SystemBus()
701
# dbus.service.Object doesn't use super(), so we can't either.
703
def __init__(self, bus = None, *args, **kwargs):
704
self._approvals_pending = 0
706
Client.__init__(self, *args, **kwargs)
707
# Only now, when this client is initialized, can it show up on
709
self.dbus_object_path = (dbus.ObjectPath
711
+ self.name.replace(u".", u"_")))
712
DBusObjectWithProperties.__init__(self, self.bus,
713
self.dbus_object_path)
715
#Could possible return a bool(self._approvals_pending),
716
#but this could mess up approvals_pending += 1 XXX
717
def _get_approvals_pending(self):
718
return self._approvals_pending
719
def _set_approvals_pending(self, value):
720
old_value = self._approvals_pending
721
self._approvals_pending = value
723
if (hasattr(self, "dbus_object_path")
724
and bval is not bool(old_value)):
725
dbus_bool = dbus.Boolean(bval, variant_level=1)
726
self.PropertyChanged(dbus.String(u"approved_pending"),
729
approvals_pending = property(_get_approvals_pending,
730
_set_approvals_pending)
731
del _get_approvals_pending, _set_approvals_pending
734
def _datetime_to_dbus(dt, variant_level=0):
735
"""Convert a UTC datetime.datetime() to a D-Bus type."""
736
return dbus.String(dt.isoformat(),
737
variant_level=variant_level)
740
oldstate = getattr(self, u"enabled", False)
741
r = Client.enable(self)
742
if oldstate != self.enabled:
744
self.PropertyChanged(dbus.String(u"enabled"),
745
dbus.Boolean(True, variant_level=1))
746
self.PropertyChanged(
747
dbus.String(u"last_enabled"),
748
self._datetime_to_dbus(self.last_enabled,
752
def disable(self, quiet = False):
753
oldstate = getattr(self, u"enabled", False)
754
r = Client.disable(self, quiet=quiet)
755
if not quiet and oldstate != self.enabled:
757
self.PropertyChanged(dbus.String(u"enabled"),
758
dbus.Boolean(False, variant_level=1))
761
def __del__(self, *args, **kwargs):
763
self.remove_from_connection()
766
if hasattr(DBusObjectWithProperties, u"__del__"):
767
DBusObjectWithProperties.__del__(self, *args, **kwargs)
768
Client.__del__(self, *args, **kwargs)
770
def checker_callback(self, pid, condition, command,
772
self.checker_callback_tag = None
775
self.PropertyChanged(dbus.String(u"checker_running"),
776
dbus.Boolean(False, variant_level=1))
777
if os.WIFEXITED(condition):
778
exitstatus = os.WEXITSTATUS(condition)
780
self.CheckerCompleted(dbus.Int16(exitstatus),
781
dbus.Int64(condition),
782
dbus.String(command))
785
self.CheckerCompleted(dbus.Int16(-1),
786
dbus.Int64(condition),
787
dbus.String(command))
789
return Client.checker_callback(self, pid, condition, command,
792
def checked_ok(self, *args, **kwargs):
793
r = Client.checked_ok(self, *args, **kwargs)
795
self.PropertyChanged(
796
dbus.String(u"last_checked_ok"),
797
(self._datetime_to_dbus(self.last_checked_ok,
801
def start_checker(self, *args, **kwargs):
802
old_checker = self.checker
803
if self.checker is not None:
804
old_checker_pid = self.checker.pid
806
old_checker_pid = None
807
r = Client.start_checker(self, *args, **kwargs)
808
# Only if new checker process was started
809
if (self.checker is not None
810
and old_checker_pid != self.checker.pid):
812
self.CheckerStarted(self.current_checker_command)
813
self.PropertyChanged(
814
dbus.String(u"checker_running"),
815
dbus.Boolean(True, variant_level=1))
818
def stop_checker(self, *args, **kwargs):
819
old_checker = getattr(self, u"checker", None)
820
r = Client.stop_checker(self, *args, **kwargs)
821
if (old_checker is not None
822
and getattr(self, u"checker", None) is None):
823
self.PropertyChanged(dbus.String(u"checker_running"),
824
dbus.Boolean(False, variant_level=1))
827
def _reset_approved(self):
828
self._approved = None
831
def approve(self, value=True):
832
self.send_changedstate()
833
self._approved = value
834
gobject.timeout_add(self._timedelta_to_milliseconds(self.approved_duration),
835
self._reset_approved)
838
## D-Bus methods, signals & properties
839
_interface = u"se.bsnet.fukt.Mandos.Client"
843
# CheckerCompleted - signal
844
@dbus.service.signal(_interface, signature=u"nxs")
845
def CheckerCompleted(self, exitcode, waitstatus, command):
849
# CheckerStarted - signal
850
@dbus.service.signal(_interface, signature=u"s")
851
def CheckerStarted(self, command):
855
# PropertyChanged - signal
856
@dbus.service.signal(_interface, signature=u"sv")
857
def PropertyChanged(self, property, value):
862
# XXXTEDDY Is sent after succesfull transfer of secret from mandos-server to mandos-client
863
@dbus.service.signal(_interface)
869
@dbus.service.signal(_interface, signature=u"s")
870
def Rejected(self, reason):
874
# NeedApproval - signal
875
@dbus.service.signal(_interface, signature=u"db")
876
def NeedApproval(self, timeout, default):
883
@dbus.service.method(_interface, in_signature=u"b")
884
def Approve(self, value):
888
@dbus.service.method(_interface)
890
return self.checked_ok()
893
@dbus.service.method(_interface)
898
# StartChecker - method
899
@dbus.service.method(_interface)
900
def StartChecker(self):
905
@dbus.service.method(_interface)
910
# StopChecker - method
911
@dbus.service.method(_interface)
912
def StopChecker(self):
917
# approved_pending - property
918
@dbus_service_property(_interface, signature=u"b", access=u"read")
919
def approved_pending_dbus_property(self):
920
return dbus.Boolean(bool(self.approvals_pending))
922
# approved_by_default - property
923
@dbus_service_property(_interface, signature=u"b",
925
def approved_by_default_dbus_property(self):
926
return dbus.Boolean(self.approved_by_default)
928
# approved_delay - property
929
@dbus_service_property(_interface, signature=u"t",
931
def approved_delay_dbus_property(self):
932
return dbus.UInt64(self.approved_delay_milliseconds())
934
# approved_duration - property
935
@dbus_service_property(_interface, signature=u"t",
937
def approved_duration_dbus_property(self):
938
return dbus.UInt64(self._timedelta_to_milliseconds(
939
self.approved_duration))
942
@dbus_service_property(_interface, signature=u"s", access=u"read")
943
def name_dbus_property(self):
944
return dbus.String(self.name)
946
# fingerprint - property
947
@dbus_service_property(_interface, signature=u"s", access=u"read")
948
def fingerprint_dbus_property(self):
949
return dbus.String(self.fingerprint)
952
@dbus_service_property(_interface, signature=u"s",
954
def host_dbus_property(self, value=None):
955
if value is None: # get
956
return dbus.String(self.host)
959
self.PropertyChanged(dbus.String(u"host"),
960
dbus.String(value, variant_level=1))
963
@dbus_service_property(_interface, signature=u"s", access=u"read")
964
def created_dbus_property(self):
965
return dbus.String(self._datetime_to_dbus(self.created))
967
# last_enabled - property
968
@dbus_service_property(_interface, signature=u"s", access=u"read")
969
def last_enabled_dbus_property(self):
970
if self.last_enabled is None:
971
return dbus.String(u"")
972
return dbus.String(self._datetime_to_dbus(self.last_enabled))
975
@dbus_service_property(_interface, signature=u"b",
977
def enabled_dbus_property(self, value=None):
978
if value is None: # get
979
return dbus.Boolean(self.enabled)
985
# last_checked_ok - property
986
@dbus_service_property(_interface, signature=u"s",
988
def last_checked_ok_dbus_property(self, value=None):
989
if value is not None:
374
992
if self.last_checked_ok is None:
375
return now < (self.created + self.timeout)
377
return now < (self.last_checked_ok + self.timeout)
380
def peer_certificate(session):
381
"Return the peer's OpenPGP certificate as a bytestring"
382
# If not an OpenPGP certificate...
383
if gnutls.library.functions.gnutls_certificate_type_get\
384
(session._c_object) \
385
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
386
# ...do the normal thing
387
return session.peer_certificate
388
list_size = ctypes.c_uint()
389
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
390
(session._c_object, ctypes.byref(list_size))
391
if list_size.value == 0:
394
return ctypes.string_at(cert.data, cert.size)
397
def fingerprint(openpgp):
398
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
399
# New empty GnuTLS certificate
400
crt = gnutls.library.types.gnutls_openpgp_crt_t()
401
gnutls.library.functions.gnutls_openpgp_crt_init\
403
# New GnuTLS "datum" with the OpenPGP public key
404
datum = gnutls.library.types.gnutls_datum_t\
405
(ctypes.cast(ctypes.c_char_p(openpgp),
406
ctypes.POINTER(ctypes.c_ubyte)),
407
ctypes.c_uint(len(openpgp)))
408
# Import the OpenPGP public key into the certificate
409
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
412
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
413
# New buffer for the fingerprint
414
buffer = ctypes.create_string_buffer(20)
415
buffer_length = ctypes.c_size_t()
416
# Get the fingerprint from the certificate into the buffer
417
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
418
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
419
# Deinit the certificate
420
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
421
# Convert the buffer to a Python bytestring
422
fpr = ctypes.string_at(buffer, buffer_length.value)
423
# Convert the bytestring to hexadecimal notation
424
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
428
class tcp_handler(SocketServer.BaseRequestHandler, object):
429
"""A TCP request handler class.
430
Instantiated by IPv6_TCPServer for each request to handle it.
993
return dbus.String(u"")
994
return dbus.String(self._datetime_to_dbus(self
998
@dbus_service_property(_interface, signature=u"t",
1000
def timeout_dbus_property(self, value=None):
1001
if value is None: # get
1002
return dbus.UInt64(self.timeout_milliseconds())
1003
self.timeout = datetime.timedelta(0, 0, 0, value)
1005
self.PropertyChanged(dbus.String(u"timeout"),
1006
dbus.UInt64(value, variant_level=1))
1007
if getattr(self, u"disable_initiator_tag", None) is None:
1009
# Reschedule timeout
1010
gobject.source_remove(self.disable_initiator_tag)
1011
self.disable_initiator_tag = None
1012
time_to_die = (self.
1013
_timedelta_to_milliseconds((self
1018
if time_to_die <= 0:
1019
# The timeout has passed
1022
self.disable_initiator_tag = (gobject.timeout_add
1023
(time_to_die, self.disable))
1025
# interval - property
1026
@dbus_service_property(_interface, signature=u"t",
1027
access=u"readwrite")
1028
def interval_dbus_property(self, value=None):
1029
if value is None: # get
1030
return dbus.UInt64(self.interval_milliseconds())
1031
self.interval = datetime.timedelta(0, 0, 0, value)
1033
self.PropertyChanged(dbus.String(u"interval"),
1034
dbus.UInt64(value, variant_level=1))
1035
if getattr(self, u"checker_initiator_tag", None) is None:
1037
# Reschedule checker run
1038
gobject.source_remove(self.checker_initiator_tag)
1039
self.checker_initiator_tag = (gobject.timeout_add
1040
(value, self.start_checker))
1041
self.start_checker() # Start one now, too
1043
# checker - property
1044
@dbus_service_property(_interface, signature=u"s",
1045
access=u"readwrite")
1046
def checker_dbus_property(self, value=None):
1047
if value is None: # get
1048
return dbus.String(self.checker_command)
1049
self.checker_command = value
1051
self.PropertyChanged(dbus.String(u"checker"),
1052
dbus.String(self.checker_command,
1055
# checker_running - property
1056
@dbus_service_property(_interface, signature=u"b",
1057
access=u"readwrite")
1058
def checker_running_dbus_property(self, value=None):
1059
if value is None: # get
1060
return dbus.Boolean(self.checker is not None)
1062
self.start_checker()
1066
# object_path - property
1067
@dbus_service_property(_interface, signature=u"o", access=u"read")
1068
def object_path_dbus_property(self):
1069
return self.dbus_object_path # is already a dbus.ObjectPath
1072
@dbus_service_property(_interface, signature=u"ay",
1073
access=u"write", byte_arrays=True)
1074
def secret_dbus_property(self, value):
1075
self.secret = str(value)
1080
class ProxyClient(object):
1081
def __init__(self, child_pipe, fpr, address):
1082
self._pipe = child_pipe
1083
self._pipe.send(('init', fpr, address))
1084
if not self._pipe.recv():
1087
def __getattribute__(self, name):
1088
if(name == '_pipe'):
1089
return super(ProxyClient, self).__getattribute__(name)
1090
self._pipe.send(('getattr', name))
1091
data = self._pipe.recv()
1092
if data[0] == 'data':
1094
if data[0] == 'function':
1095
def func(*args, **kwargs):
1096
self._pipe.send(('funcall', name, args, kwargs))
1097
return self._pipe.recv()[1]
1100
def __setattr__(self, name, value):
1101
if(name == '_pipe'):
1102
return super(ProxyClient, self).__setattr__(name, value)
1103
self._pipe.send(('setattr', name, value))
1106
class ClientHandler(socketserver.BaseRequestHandler, object):
1107
"""A class to handle client connections.
1109
Instantiated once for each connection to handle it.
431
1110
Note: This will run in its own forked process."""
433
1112
def handle(self):
434
logger.debug(u"TCP connection from: %s",
435
unicode(self.client_address))
436
session = gnutls.connection.ClientSession\
437
(self.request, gnutls.connection.X509Credentials())
439
line = self.request.makefile().readline()
440
logger.debug(u"Protocol version: %r", line)
442
if int(line.strip().split()[0]) > 1:
444
except (ValueError, IndexError, RuntimeError), error:
445
logger.error(u"Unknown protocol version: %s", error)
448
# Note: gnutls.connection.X509Credentials is really a generic
449
# GnuTLS certificate credentials object so long as no X.509
450
# keys are added to it. Therefore, we can use it here despite
451
# using OpenPGP certificates.
453
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
454
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
456
priority = "NORMAL" # Fallback default, since this
458
if self.server.settings["priority"]:
459
priority = self.server.settings["priority"]
460
gnutls.library.functions.gnutls_priority_set_direct\
461
(session._c_object, priority, None);
465
except gnutls.errors.GNUTLSError, error:
466
logger.debug(u"Handshake failed: %s", error)
467
# Do not run session.bye() here: the session is not
468
# established. Just abandon the request.
471
fpr = fingerprint(peer_certificate(session))
472
except (TypeError, gnutls.errors.GNUTLSError), error:
473
logger.debug(u"Bad certificate: %s", error)
476
logger.debug(u"Fingerprint: %s", fpr)
478
for c in self.server.clients:
479
if c.fingerprint == fpr:
483
logger.debug(u"Client not found for fingerprint: %s", fpr)
486
# Have to check if client.still_valid(), since it is possible
487
# that the client timed out while establishing the GnuTLS
489
if not client.still_valid():
490
logger.debug(u"Client %(name)s is invalid", vars(client))
494
while sent_size < len(client.secret):
495
sent = session.send(client.secret[sent_size:])
496
logger.debug(u"Sent: %d, remaining: %d",
497
sent, len(client.secret)
498
- (sent_size + sent))
503
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
504
"""IPv6 TCP server. Accepts 'None' as address and/or port.
1113
with contextlib.closing(self.server.child_pipe) as child_pipe:
1114
logger.info(u"TCP connection from: %s",
1115
unicode(self.client_address))
1116
logger.debug(u"Pipe FD: %d",
1117
self.server.child_pipe.fileno())
1119
session = (gnutls.connection
1120
.ClientSession(self.request,
1122
.X509Credentials()))
1124
# Note: gnutls.connection.X509Credentials is really a
1125
# generic GnuTLS certificate credentials object so long as
1126
# no X.509 keys are added to it. Therefore, we can use it
1127
# here despite using OpenPGP certificates.
1129
#priority = u':'.join((u"NONE", u"+VERS-TLS1.1",
1130
# u"+AES-256-CBC", u"+SHA1",
1131
# u"+COMP-NULL", u"+CTYPE-OPENPGP",
1133
# Use a fallback default, since this MUST be set.
1134
priority = self.server.gnutls_priority
1135
if priority is None:
1136
priority = u"NORMAL"
1137
(gnutls.library.functions
1138
.gnutls_priority_set_direct(session._c_object,
1141
# Start communication using the Mandos protocol
1142
# Get protocol number
1143
line = self.request.makefile().readline()
1144
logger.debug(u"Protocol version: %r", line)
1146
if int(line.strip().split()[0]) > 1:
1148
except (ValueError, IndexError, RuntimeError), error:
1149
logger.error(u"Unknown protocol version: %s", error)
1152
# Start GnuTLS connection
1155
except gnutls.errors.GNUTLSError, error:
1156
logger.warning(u"Handshake failed: %s", error)
1157
# Do not run session.bye() here: the session is not
1158
# established. Just abandon the request.
1160
logger.debug(u"Handshake succeeded")
1162
approval_required = False
1165
fpr = self.fingerprint(self.peer_certificate
1167
except (TypeError, gnutls.errors.GNUTLSError), error:
1168
logger.warning(u"Bad certificate: %s", error)
1170
logger.debug(u"Fingerprint: %s", fpr)
1173
client = ProxyClient(child_pipe, fpr,
1174
self.client_address)
1178
if client.approved_delay:
1179
delay = client.approved_delay
1180
client.approvals_pending += 1
1181
approval_required = True
1184
if not client.enabled:
1185
logger.warning(u"Client %s is disabled",
1187
if self.server.use_dbus:
1189
client.Rejected("Disabled")
1192
if client._approved or not client.approved_delay:
1193
#We are approved or approval is disabled
1195
elif client._approved is None:
1196
logger.info(u"Client %s need approval",
1198
if self.server.use_dbus:
1200
client.NeedApproval(
1201
client.approved_delay_milliseconds(),
1202
client.approved_by_default)
1204
logger.warning(u"Client %s was not approved",
1206
if self.server.use_dbus:
1208
client.Rejected("Disapproved")
1211
#wait until timeout or approved
1212
#x = float(client._timedelta_to_milliseconds(delay))
1213
time = datetime.datetime.now()
1214
client.changedstate.acquire()
1215
client.changedstate.wait(float(client._timedelta_to_milliseconds(delay) / 1000))
1216
client.changedstate.release()
1217
time2 = datetime.datetime.now()
1218
if (time2 - time) >= delay:
1219
if not client.approved_by_default:
1220
logger.warning("Client %s timed out while"
1221
" waiting for approval",
1223
if self.server.use_dbus:
1225
client.Rejected("Time out")
1230
delay -= time2 - time
1233
while sent_size < len(client.secret):
1234
# XXX handle session exception
1235
sent = session.send(client.secret[sent_size:])
1236
logger.debug(u"Sent: %d, remaining: %d",
1237
sent, len(client.secret)
1238
- (sent_size + sent))
1241
logger.info(u"Sending secret to %s", client.name)
1242
# bump the timeout as if seen
1244
if self.server.use_dbus:
1249
if approval_required:
1250
client.approvals_pending -= 1
1254
def peer_certificate(session):
1255
"Return the peer's OpenPGP certificate as a bytestring"
1256
# If not an OpenPGP certificate...
1257
if (gnutls.library.functions
1258
.gnutls_certificate_type_get(session._c_object)
1259
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1260
# ...do the normal thing
1261
return session.peer_certificate
1262
list_size = ctypes.c_uint(1)
1263
cert_list = (gnutls.library.functions
1264
.gnutls_certificate_get_peers
1265
(session._c_object, ctypes.byref(list_size)))
1266
if not bool(cert_list) and list_size.value != 0:
1267
raise gnutls.errors.GNUTLSError(u"error getting peer"
1269
if list_size.value == 0:
1272
return ctypes.string_at(cert.data, cert.size)
1275
def fingerprint(openpgp):
1276
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1277
# New GnuTLS "datum" with the OpenPGP public key
1278
datum = (gnutls.library.types
1279
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1282
ctypes.c_uint(len(openpgp))))
1283
# New empty GnuTLS certificate
1284
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1285
(gnutls.library.functions
1286
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1287
# Import the OpenPGP public key into the certificate
1288
(gnutls.library.functions
1289
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1290
gnutls.library.constants
1291
.GNUTLS_OPENPGP_FMT_RAW))
1292
# Verify the self signature in the key
1293
crtverify = ctypes.c_uint()
1294
(gnutls.library.functions
1295
.gnutls_openpgp_crt_verify_self(crt, 0,
1296
ctypes.byref(crtverify)))
1297
if crtverify.value != 0:
1298
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1299
raise (gnutls.errors.CertificateSecurityError
1301
# New buffer for the fingerprint
1302
buf = ctypes.create_string_buffer(20)
1303
buf_len = ctypes.c_size_t()
1304
# Get the fingerprint from the certificate into the buffer
1305
(gnutls.library.functions
1306
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1307
ctypes.byref(buf_len)))
1308
# Deinit the certificate
1309
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1310
# Convert the buffer to a Python bytestring
1311
fpr = ctypes.string_at(buf, buf_len.value)
1312
# Convert the bytestring to hexadecimal notation
1313
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
1317
class MultiprocessingMixIn(object):
1318
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
1319
def sub_process_main(self, request, address):
1321
self.finish_request(request, address)
1323
self.handle_error(request, address)
1324
self.close_request(request)
1326
def process_request(self, request, address):
1327
"""Start a new process to process the request."""
1328
multiprocessing.Process(target = self.sub_process_main,
1329
args = (request, address)).start()
1331
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
1332
""" adds a pipe to the MixIn """
1333
def process_request(self, request, client_address):
1334
"""Overrides and wraps the original process_request().
1336
This function creates a new pipe in self.pipe
1338
parent_pipe, self.child_pipe = multiprocessing.Pipe()
1340
super(MultiprocessingMixInWithPipe,
1341
self).process_request(request, client_address)
1342
self.child_pipe.close()
1343
self.add_pipe(parent_pipe)
1345
def add_pipe(self, parent_pipe):
1346
"""Dummy function; override as necessary"""
1349
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1350
socketserver.TCPServer, object):
1351
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
506
settings: Server settings
507
clients: Set() of Client objects
1354
enabled: Boolean; whether this server is activated yet
1355
interface: None or a network interface name (string)
1356
use_ipv6: Boolean; to use IPv6 or not
509
address_family = socket.AF_INET6
510
def __init__(self, *args, **kwargs):
511
if "settings" in kwargs:
512
self.settings = kwargs["settings"]
513
del kwargs["settings"]
514
if "clients" in kwargs:
515
self.clients = kwargs["clients"]
516
del kwargs["clients"]
517
return super(type(self), self).__init__(*args, **kwargs)
1358
def __init__(self, server_address, RequestHandlerClass,
1359
interface=None, use_ipv6=True):
1360
self.interface = interface
1362
self.address_family = socket.AF_INET6
1363
socketserver.TCPServer.__init__(self, server_address,
1364
RequestHandlerClass)
518
1365
def server_bind(self):
519
1366
"""This overrides the normal server_bind() function
520
1367
to bind to an interface if one was specified, and also NOT to
521
1368
bind to an address or port if they were not specified."""
522
if self.settings["interface"]:
523
# 25 is from /usr/include/asm-i486/socket.h
524
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
526
self.socket.setsockopt(socket.SOL_SOCKET,
528
self.settings["interface"])
529
except socket.error, error:
530
if error[0] == errno.EPERM:
531
logger.warning(u"No permission to"
532
u" bind to interface %s",
533
self.settings["interface"])
1369
if self.interface is not None:
1370
if SO_BINDTODEVICE is None:
1371
logger.error(u"SO_BINDTODEVICE does not exist;"
1372
u" cannot bind to interface %s",
1376
self.socket.setsockopt(socket.SOL_SOCKET,
1380
except socket.error, error:
1381
if error[0] == errno.EPERM:
1382
logger.error(u"No permission to"
1383
u" bind to interface %s",
1385
elif error[0] == errno.ENOPROTOOPT:
1386
logger.error(u"SO_BINDTODEVICE not available;"
1387
u" cannot bind to interface %s",
536
1391
# Only bind(2) the socket if we really need to.
537
1392
if self.server_address[0] or self.server_address[1]:
538
1393
if not self.server_address[0]:
540
self.server_address = (in6addr_any,
1394
if self.address_family == socket.AF_INET6:
1395
any_address = u"::" # in6addr_any
1397
any_address = socket.INADDR_ANY
1398
self.server_address = (any_address,
541
1399
self.server_address[1])
542
elif self.server_address[1] is None:
1400
elif not self.server_address[1]:
543
1401
self.server_address = (self.server_address[0],
545
return super(type(self), self).server_bind()
1403
# if self.interface:
1404
# self.server_address = (self.server_address[0],
1409
return socketserver.TCPServer.server_bind(self)
1412
class MandosServer(IPv6_TCPServer):
1416
clients: set of Client objects
1417
gnutls_priority GnuTLS priority string
1418
use_dbus: Boolean; to emit D-Bus signals or not
1420
Assumes a gobject.MainLoop event loop.
1422
def __init__(self, server_address, RequestHandlerClass,
1423
interface=None, use_ipv6=True, clients=None,
1424
gnutls_priority=None, use_dbus=True):
1425
self.enabled = False
1426
self.clients = clients
1427
if self.clients is None:
1428
self.clients = set()
1429
self.use_dbus = use_dbus
1430
self.gnutls_priority = gnutls_priority
1431
IPv6_TCPServer.__init__(self, server_address,
1432
RequestHandlerClass,
1433
interface = interface,
1434
use_ipv6 = use_ipv6)
1435
def server_activate(self):
1437
return socketserver.TCPServer.server_activate(self)
1440
def add_pipe(self, parent_pipe):
1441
# Call "handle_ipc" for both data and EOF events
1442
gobject.io_add_watch(parent_pipe.fileno(),
1443
gobject.IO_IN | gobject.IO_HUP,
1444
functools.partial(self.handle_ipc,
1445
parent_pipe = parent_pipe))
1447
def handle_ipc(self, source, condition, parent_pipe=None,
1448
client_object=None):
1450
gobject.IO_IN: u"IN", # There is data to read.
1451
gobject.IO_OUT: u"OUT", # Data can be written (without
1453
gobject.IO_PRI: u"PRI", # There is urgent data to read.
1454
gobject.IO_ERR: u"ERR", # Error condition.
1455
gobject.IO_HUP: u"HUP" # Hung up (the connection has been
1456
# broken, usually for pipes and
1459
conditions_string = ' | '.join(name
1461
condition_names.iteritems()
1462
if cond & condition)
1463
logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1466
# XXXTEDDY error or the other end of multiprocessing.Pipe has closed
1467
if condition & gobject.IO_HUP or condition & gobject.IO_ERR:
1470
# Read a request from the child
1471
request = parent_pipe.recv()
1472
logger.debug(u"IPC request: %s", repr(request))
1473
command = request[0]
1475
if command == 'init':
1477
address = request[2]
1479
for c in self.clients:
1480
if c.fingerprint == fpr:
1484
logger.warning(u"Client not found for fingerprint: %s, ad"
1485
u"dress: %s", fpr, address)
1488
mandos_dbus_service.ClientNotFound(fpr, address)
1489
parent_pipe.send(False)
1492
gobject.io_add_watch(parent_pipe.fileno(),
1493
gobject.IO_IN | gobject.IO_HUP,
1494
functools.partial(self.handle_ipc,
1495
parent_pipe = parent_pipe,
1496
client_object = client))
1497
parent_pipe.send(True)
1498
# remove the old hook in favor of the new above hook on same fileno
1500
if command == 'funcall':
1501
funcname = request[1]
1505
parent_pipe.send(('data', getattr(client_object, funcname)(*args, **kwargs)))
1507
if command == 'getattr':
1508
attrname = request[1]
1509
if callable(client_object.__getattribute__(attrname)):
1510
parent_pipe.send(('function',))
1512
parent_pipe.send(('data', client_object.__getattribute__(attrname)))
1514
if command == 'setattr':
1515
attrname = request[1]
1517
setattr(client_object, attrname, value)
548
1522
def string_to_delta(interval):
549
1523
"""Parse a string and return a datetime.timedelta
551
>>> string_to_delta('7d')
1525
>>> string_to_delta(u'7d')
552
1526
datetime.timedelta(7)
553
>>> string_to_delta('60s')
1527
>>> string_to_delta(u'60s')
554
1528
datetime.timedelta(0, 60)
555
>>> string_to_delta('60m')
1529
>>> string_to_delta(u'60m')
556
1530
datetime.timedelta(0, 3600)
557
>>> string_to_delta('24h')
1531
>>> string_to_delta(u'24h')
558
1532
datetime.timedelta(1)
559
1533
>>> string_to_delta(u'1w')
560
1534
datetime.timedelta(7)
1535
>>> string_to_delta(u'5m 30s')
1536
datetime.timedelta(0, 330)
563
suffix=unicode(interval[-1])
564
value=int(interval[:-1])
566
delta = datetime.timedelta(value)
568
delta = datetime.timedelta(0, value)
570
delta = datetime.timedelta(0, 0, 0, 0, value)
572
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
574
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
577
except (ValueError, IndexError):
582
def server_state_changed(state):
583
"""Derived from the Avahi example code"""
584
if state == avahi.SERVER_COLLISION:
585
logger.warning(u"Server name collision")
587
elif state == avahi.SERVER_RUNNING:
591
def entry_group_state_changed(state, error):
592
"""Derived from the Avahi example code"""
593
logger.debug(u"state change: %i", state)
595
if state == avahi.ENTRY_GROUP_ESTABLISHED:
596
logger.debug(u"Service established.")
597
elif state == avahi.ENTRY_GROUP_COLLISION:
598
logger.warning(u"Service name collision.")
600
elif state == avahi.ENTRY_GROUP_FAILURE:
601
logger.critical(u"Error in group state changed %s",
603
raise AvahiGroupError("State changed: %s", str(error))
1538
timevalue = datetime.timedelta(0)
1539
for s in interval.split():
1541
suffix = unicode(s[-1])
1544
delta = datetime.timedelta(value)
1545
elif suffix == u"s":
1546
delta = datetime.timedelta(0, value)
1547
elif suffix == u"m":
1548
delta = datetime.timedelta(0, 0, 0, 0, value)
1549
elif suffix == u"h":
1550
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
1551
elif suffix == u"w":
1552
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1554
raise ValueError(u"Unknown suffix %r" % suffix)
1555
except (ValueError, IndexError), e:
1556
raise ValueError(e.message)
605
1561
def if_nametoindex(interface):
606
"""Call the C function if_nametoindex(), or equivalent"""
1562
"""Call the C function if_nametoindex(), or equivalent
1564
Note: This function cannot accept a unicode string."""
607
1565
global if_nametoindex
609
if "ctypes.util" not in sys.modules:
611
if_nametoindex = ctypes.cdll.LoadLibrary\
612
(ctypes.util.find_library("c")).if_nametoindex
1567
if_nametoindex = (ctypes.cdll.LoadLibrary
1568
(ctypes.util.find_library(u"c"))
613
1570
except (OSError, AttributeError):
614
if "struct" not in sys.modules:
616
if "fcntl" not in sys.modules:
1571
logger.warning(u"Doing if_nametoindex the hard way")
618
1572
def if_nametoindex(interface):
619
1573
"Get an interface index the hard way, i.e. using fcntl()"
620
1574
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
622
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
623
struct.pack("16s16x", interface))
625
interface_index = struct.unpack("I", ifreq[16:20])[0]
1575
with contextlib.closing(socket.socket()) as s:
1576
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1577
struct.pack(str(u"16s16x"),
1579
interface_index = struct.unpack(str(u"I"),
626
1581
return interface_index
627
1582
return if_nametoindex(interface)
630
def daemon(nochdir, noclose):
1585
def daemon(nochdir = False, noclose = False):
631
1586
"""See daemon(3). Standard BSD Unix function.
632
1588
This should really exist as os.daemon, but it doesn't (yet)."""
639
1597
# Close all standard open file descriptors
640
1598
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
641
1599
if not stat.S_ISCHR(os.fstat(null).st_mode):
642
1600
raise OSError(errno.ENODEV,
643
"/dev/null not a character device")
1601
u"%s not a character device"
644
1603
os.dup2(null, sys.stdin.fileno())
645
1604
os.dup2(null, sys.stdout.fileno())
646
1605
os.dup2(null, sys.stderr.fileno())
682
1647
# Default values for config file for server-global settings
683
server_defaults = { "interface": "",
688
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
689
"servicename": "Mandos",
1648
server_defaults = { u"interface": u"",
1653
u"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1654
u"servicename": u"Mandos",
1655
u"use_dbus": u"True",
1656
u"use_ipv6": u"True",
692
1659
# Parse config file for server-global settings
693
server_config = ConfigParser.SafeConfigParser(server_defaults)
1660
server_config = configparser.SafeConfigParser(server_defaults)
694
1661
del server_defaults
695
server_config.read(os.path.join(options.configdir, "server.conf"))
696
server_section = "server"
1662
server_config.read(os.path.join(options.configdir,
697
1664
# Convert the SafeConfigParser object to a dict
698
server_settings = dict(server_config.items(server_section))
699
# Use getboolean on the boolean config option
700
server_settings["debug"] = server_config.getboolean\
701
(server_section, "debug")
1665
server_settings = server_config.defaults()
1666
# Use the appropriate methods on the non-string config options
1667
for option in (u"debug", u"use_dbus", u"use_ipv6"):
1668
server_settings[option] = server_config.getboolean(u"DEFAULT",
1670
if server_settings["port"]:
1671
server_settings["port"] = server_config.getint(u"DEFAULT",
702
1673
del server_config
704
1675
# Override the settings from the config file with command line
705
1676
# options, if set.
706
for option in ("interface", "address", "port", "debug",
707
"priority", "servicename", "configdir"):
1677
for option in (u"interface", u"address", u"port", u"debug",
1678
u"priority", u"servicename", u"configdir",
1679
u"use_dbus", u"use_ipv6"):
708
1680
value = getattr(options, option)
709
1681
if value is not None:
710
1682
server_settings[option] = value
1684
# Force all strings to be unicode
1685
for option in server_settings.keys():
1686
if type(server_settings[option]) is str:
1687
server_settings[option] = unicode(server_settings[option])
712
1688
# Now we have our good server settings in "server_settings"
1690
##################################################################
1693
debug = server_settings[u"debug"]
1694
use_dbus = server_settings[u"use_dbus"]
1695
use_ipv6 = server_settings[u"use_ipv6"]
1698
syslogger.setLevel(logging.WARNING)
1699
console.setLevel(logging.WARNING)
1701
if server_settings[u"servicename"] != u"Mandos":
1702
syslogger.setFormatter(logging.Formatter
1703
(u'Mandos (%s) [%%(process)d]:'
1704
u' %%(levelname)s: %%(message)s'
1705
% server_settings[u"servicename"]))
714
1707
# Parse config file with clients
715
client_defaults = { "timeout": "1h",
717
"checker": "fping -q -- %%(fqdn)s",
1708
client_defaults = { u"timeout": u"1h",
1710
u"checker": u"fping -q -- %%(host)s",
1712
u"approved_delay": u"0s",
1713
u"approved_duration": u"1s",
719
client_config = ConfigParser.SafeConfigParser(client_defaults)
720
client_config.read(os.path.join(server_settings["configdir"],
724
service = AvahiService(name = server_settings["servicename"],
725
type = "_mandos._tcp", );
726
if server_settings["interface"]:
727
service.interface = if_nametoindex(server_settings["interface"])
1715
client_config = configparser.SafeConfigParser(client_defaults)
1716
client_config.read(os.path.join(server_settings[u"configdir"],
1719
global mandos_dbus_service
1720
mandos_dbus_service = None
1722
tcp_server = MandosServer((server_settings[u"address"],
1723
server_settings[u"port"]),
1725
interface=server_settings[u"interface"],
1728
server_settings[u"priority"],
1730
pidfilename = u"/var/run/mandos.pid"
1732
pidfile = open(pidfilename, u"w")
1734
logger.error(u"Could not open file %r", pidfilename)
1737
uid = pwd.getpwnam(u"_mandos").pw_uid
1738
gid = pwd.getpwnam(u"_mandos").pw_gid
1741
uid = pwd.getpwnam(u"mandos").pw_uid
1742
gid = pwd.getpwnam(u"mandos").pw_gid
1745
uid = pwd.getpwnam(u"nobody").pw_uid
1746
gid = pwd.getpwnam(u"nobody").pw_gid
1753
except OSError, error:
1754
if error[0] != errno.EPERM:
1757
# Enable all possible GnuTLS debugging
1759
# "Use a log level over 10 to enable all debugging options."
1761
gnutls.library.functions.gnutls_global_set_log_level(11)
1763
@gnutls.library.types.gnutls_log_func
1764
def debug_gnutls(level, string):
1765
logger.debug(u"GnuTLS: %s", string[:-1])
1767
(gnutls.library.functions
1768
.gnutls_global_set_log_function(debug_gnutls))
729
1770
global main_loop
732
1771
# From the Avahi example code
733
1772
DBusGMainLoop(set_as_default=True )
734
1773
main_loop = gobject.MainLoop()
735
1774
bus = dbus.SystemBus()
736
server = dbus.Interface(
737
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
738
avahi.DBUS_INTERFACE_SERVER )
739
1775
# End of Avahi example code
741
debug = server_settings["debug"]
1778
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos",
1779
bus, do_not_queue=True)
1780
except dbus.exceptions.NameExistsException, e:
1781
logger.error(unicode(e) + u", disabling D-Bus")
1783
server_settings[u"use_dbus"] = False
1784
tcp_server.use_dbus = False
1785
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1786
service = AvahiService(name = server_settings[u"servicename"],
1787
servicetype = u"_mandos._tcp",
1788
protocol = protocol, bus = bus)
1789
if server_settings["interface"]:
1790
service.interface = (if_nametoindex
1791
(str(server_settings[u"interface"])))
1793
global multiprocessing_manager
1794
multiprocessing_manager = multiprocessing.Manager()
1796
client_class = Client
1798
client_class = functools.partial(ClientDBus, bus = bus)
1799
def client_config_items(config, section):
1800
special_settings = {
1801
"approved_by_default":
1802
lambda: config.getboolean(section,
1803
"approved_by_default"),
1805
for name, value in config.items(section):
1807
yield (name, special_settings[name]())
1811
tcp_server.clients.update(set(
1812
client_class(name = section,
1813
config= dict(client_config_items(
1814
client_config, section)))
1815
for section in client_config.sections()))
1816
if not tcp_server.clients:
1817
logger.warning(u"No clients defined")
744
console = logging.StreamHandler()
745
# console.setLevel(logging.DEBUG)
746
console.setFormatter(logging.Formatter\
747
('%(levelname)s: %(message)s'))
748
logger.addHandler(console)
752
def remove_from_clients(client):
753
clients.remove(client)
755
logger.debug(u"No clients left, exiting")
758
clients.update(Set(Client(name=section,
759
stop_hook = remove_from_clients,
760
**(dict(client_config\
762
for section in client_config.sections()))
1820
# Redirect stdin so all checkers get /dev/null
1821
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1822
os.dup2(null, sys.stdin.fileno())
1826
# No console logging
1827
logger.removeHandler(console)
1828
# Close all input and output, do double fork, etc.
1834
pidfile.write(str(pid) + "\n")
1837
logger.error(u"Could not write to file %r with PID %d",
1840
# "pidfile" was never created
1845
signal.signal(signal.SIGINT, signal.SIG_IGN)
1846
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1847
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1850
class MandosDBusService(dbus.service.Object):
1851
"""A D-Bus proxy object"""
1853
dbus.service.Object.__init__(self, bus, u"/")
1854
_interface = u"se.bsnet.fukt.Mandos"
1856
@dbus.service.signal(_interface, signature=u"o")
1857
def ClientAdded(self, objpath):
1861
@dbus.service.signal(_interface, signature=u"ss")
1862
def ClientNotFound(self, fingerprint, address):
1866
@dbus.service.signal(_interface, signature=u"os")
1867
def ClientRemoved(self, objpath, name):
1871
@dbus.service.method(_interface, out_signature=u"ao")
1872
def GetAllClients(self):
1874
return dbus.Array(c.dbus_object_path
1875
for c in tcp_server.clients)
1877
@dbus.service.method(_interface,
1878
out_signature=u"a{oa{sv}}")
1879
def GetAllClientsWithProperties(self):
1881
return dbus.Dictionary(
1882
((c.dbus_object_path, c.GetAll(u""))
1883
for c in tcp_server.clients),
1884
signature=u"oa{sv}")
1886
@dbus.service.method(_interface, in_signature=u"o")
1887
def RemoveClient(self, object_path):
1889
for c in tcp_server.clients:
1890
if c.dbus_object_path == object_path:
1891
tcp_server.clients.remove(c)
1892
c.remove_from_connection()
1893
# Don't signal anything except ClientRemoved
1894
c.disable(quiet=True)
1896
self.ClientRemoved(object_path, c.name)
1898
raise KeyError(object_path)
1902
mandos_dbus_service = MandosDBusService()
768
1905
"Cleanup function; run on exit"
770
# From the Avahi example code
771
if not group is None:
774
# End of Avahi example code
777
client = clients.pop()
778
client.stop_hook = None
1908
while tcp_server.clients:
1909
client = tcp_server.clients.pop()
1911
client.remove_from_connection()
1912
client.disable_hook = None
1913
# Don't signal anything except ClientRemoved
1914
client.disable(quiet=True)
1917
mandos_dbus_service.ClientRemoved(client.dbus_object_path,
781
1920
atexit.register(cleanup)
784
signal.signal(signal.SIGINT, signal.SIG_IGN)
785
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
786
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
788
for client in clients:
791
tcp_server = IPv6_TCPServer((server_settings["address"],
792
server_settings["port"]),
794
settings=server_settings,
1922
for client in tcp_server.clients:
1925
mandos_dbus_service.ClientAdded(client.dbus_object_path)
1929
tcp_server.server_activate()
796
1931
# Find out what port we got
797
1932
service.port = tcp_server.socket.getsockname()[1]
798
logger.debug(u"Now listening on address %r, port %d, flowinfo %d,"
799
u" scope_id %d" % tcp_server.socket.getsockname())
1934
logger.info(u"Now listening on address %r, port %d,"
1935
" flowinfo %d, scope_id %d"
1936
% tcp_server.socket.getsockname())
1938
logger.info(u"Now listening on address %r, port %d"
1939
% tcp_server.socket.getsockname())
801
1941
#service.interface = tcp_server.socket.getsockname()[3]
804
1944
# From the Avahi example code
805
server.connect_to_signal("StateChanged", server_state_changed)
807
server_state_changed(server.GetState())
808
1947
except dbus.exceptions.DBusException, error:
809
1948
logger.critical(u"DBusException: %s", error)
811
1951
# End of Avahi example code
813
1953
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
814
1954
lambda *args, **kwargs:
815
tcp_server.handle_request\
816
(*args[2:], **kwargs) or True)
1955
(tcp_server.handle_request
1956
(*args[2:], **kwargs) or True))
818
logger.debug("Starting main loop")
819
main_loop_started = True
1958
logger.debug(u"Starting main loop")
821
1960
except AvahiError, error:
822
logger.critical(u"AvahiError: %s" + unicode(error))
1961
logger.critical(u"AvahiError: %s", error)
824
1964
except KeyboardInterrupt:
1967
logger.debug(u"Server received KeyboardInterrupt")
1968
logger.debug(u"Server exiting")
1969
# Must run before the D-Bus bus name gets deregistered
828
1972
if __name__ == '__main__':