105
128
max_renames: integer; maximum number of renames
106
129
rename_count: integer; counter so we only rename after collisions
107
130
a sensible number of times
131
group: D-Bus Entry Group
133
bus: dbus.SystemBus()
109
135
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
110
type = None, port = None, TXT = None, domain = "",
111
host = "", max_renames = 32768):
136
servicetype = None, port = None, TXT = None,
137
domain = u"", host = u"", max_renames = 32768,
138
protocol = avahi.PROTO_UNSPEC, bus = None):
112
139
self.interface = interface
141
self.type = servicetype
143
self.TXT = TXT if TXT is not None else []
120
144
self.domain = domain
122
146
self.rename_count = 0
123
147
self.max_renames = max_renames
148
self.protocol = protocol
149
self.group = None # our entry group
124
152
def rename(self):
125
153
"""Derived from the Avahi example code"""
126
154
if self.rename_count >= self.max_renames:
127
155
logger.critical(u"No suitable Zeroconf service name found"
128
156
u" after %i retries, exiting.",
130
raise AvahiServiceError("Too many renames")
131
self.name = server.GetAlternativeServiceName(self.name)
158
raise AvahiServiceError(u"Too many renames")
159
self.name = self.server.GetAlternativeServiceName(self.name)
132
160
logger.info(u"Changing Zeroconf service name to %r ...",
134
syslogger.setFormatter(logging.Formatter\
135
('Mandos (%s): %%(levelname)s:'
136
' %%(message)s' % self.name))
162
syslogger.setFormatter(logging.Formatter
163
(u'Mandos (%s) [%%(process)d]:'
164
u' %%(levelname)s: %%(message)s'
139
168
self.rename_count += 1
140
169
def remove(self):
141
170
"""Derived from the Avahi example code"""
142
if group is not None:
171
if self.group is not None:
145
174
"""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)
175
if self.group is None:
176
self.group = dbus.Interface(
177
self.bus.get_object(avahi.DBUS_NAME,
178
self.server.EntryGroupNew()),
179
avahi.DBUS_INTERFACE_ENTRY_GROUP)
180
self.group.connect_to_signal('StateChanged',
182
.entry_group_state_changed)
154
183
logger.debug(u"Adding Zeroconf 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
184
self.name, self.type)
185
self.group.AddService(
188
dbus.UInt32(0), # flags
189
self.name, self.type,
190
self.domain, self.host,
191
dbus.UInt16(self.port),
192
avahi.string_array_to_txt_array(self.TXT))
194
def entry_group_state_changed(self, state, error):
195
"""Derived from the Avahi example code"""
196
logger.debug(u"Avahi entry group state change: %i", state)
198
if state == avahi.ENTRY_GROUP_ESTABLISHED:
199
logger.debug(u"Zeroconf service established.")
200
elif state == avahi.ENTRY_GROUP_COLLISION:
201
logger.warning(u"Zeroconf service name collision.")
203
elif state == avahi.ENTRY_GROUP_FAILURE:
204
logger.critical(u"Avahi: Error in group state changed %s",
206
raise AvahiGroupError(u"State changed: %s"
209
"""Derived from the Avahi example code"""
210
if self.group is not None:
213
def server_state_changed(self, state):
214
"""Derived from the Avahi example code"""
215
logger.debug(u"Avahi server state change: %i", state)
216
if state == avahi.SERVER_COLLISION:
217
logger.error(u"Zeroconf server name collision")
219
elif state == avahi.SERVER_RUNNING:
222
"""Derived from the Avahi example code"""
223
if self.server is None:
224
self.server = dbus.Interface(
225
self.bus.get_object(avahi.DBUS_NAME,
226
avahi.DBUS_PATH_SERVER),
227
avahi.DBUS_INTERFACE_SERVER)
228
self.server.connect_to_signal(u"StateChanged",
229
self.server_state_changed)
230
self.server_state_changed(self.server.GetState())
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
host: string; 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
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, config={}):
265
def _timedelta_to_milliseconds(td):
266
"Convert a datetime.timedelta() to milliseconds"
267
return ((td.days * 24 * 60 * 60 * 1000)
268
+ (td.seconds * 1000)
269
+ (td.microseconds // 1000))
271
def timeout_milliseconds(self):
272
"Return the 'timeout' attribute in milliseconds"
273
return self._timedelta_to_milliseconds(self.timeout)
275
def interval_milliseconds(self):
276
"Return the 'interval' attribute in milliseconds"
277
return self._timedelta_to_milliseconds(self.interval)
279
def __init__(self, name = None, disable_hook=None, config=None):
225
280
"""Note: the 'checker' key in 'config' sets the
226
281
'checker_command' attribute and *not* the 'checker'
229
286
logger.debug(u"Creating client %r", self.name)
230
287
# Uppercase and remove spaces from fingerprint for later
231
288
# comparison purposes with return value from the fingerprint()
233
self.fingerprint = config["fingerprint"].upper()\
290
self.fingerprint = (config[u"fingerprint"].upper()
235
292
logger.debug(u" Fingerprint: %s", self.fingerprint)
236
if "secret" in config:
237
self.secret = config["secret"].decode(u"base64")
238
elif "secfile" in config:
239
sf = open(config["secfile"])
240
self.secret = sf.read()
293
if u"secret" in config:
294
self.secret = config[u"secret"].decode(u"base64")
295
elif u"secfile" in config:
296
with open(os.path.expanduser(os.path.expandvars
297
(config[u"secfile"])),
299
self.secret = secfile.read()
243
301
raise TypeError(u"No secret or secfile for client %s"
245
self.host = config.get("host", "")
246
self.created = datetime.datetime.now()
303
self.host = config.get(u"host", u"")
304
self.created = datetime.datetime.utcnow()
306
self.last_enabled = None
247
307
self.last_checked_ok = None
248
self.timeout = string_to_delta(config["timeout"])
249
self.interval = string_to_delta(config["interval"])
250
self.stop_hook = stop_hook
308
self.timeout = string_to_delta(config[u"timeout"])
309
self.interval = string_to_delta(config[u"interval"])
310
self.disable_hook = disable_hook
251
311
self.checker = None
252
312
self.checker_initiator_tag = None
253
self.stop_initiator_tag = None
313
self.disable_initiator_tag = None
254
314
self.checker_callback_tag = None
255
self.check_command = config["checker"]
315
self.checker_command = config[u"checker"]
316
self.current_checker_command = None
317
self.last_connect = None
257
320
"""Start this client's checker and timeout hooks"""
321
if getattr(self, u"enabled", False):
324
self.last_enabled = datetime.datetime.utcnow()
258
325
# Schedule a new checker to be started an 'interval' from now,
259
326
# and every interval from then on.
260
self.checker_initiator_tag = gobject.timeout_add\
261
(self._interval_milliseconds,
327
self.checker_initiator_tag = (gobject.timeout_add
328
(self.interval_milliseconds(),
330
# Schedule a disable() when 'timeout' has passed
331
self.disable_initiator_tag = (gobject.timeout_add
332
(self.timeout_milliseconds(),
263
335
# Also start a new checker *right now*.
264
336
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.
274
if hasattr(self, "secret") and self.secret:
275
logger.info(u"Stopping client %s", self.name)
338
def disable(self, quiet=True):
339
"""Disable this client."""
340
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):
343
logger.info(u"Disabling client %s", self.name)
344
if getattr(self, u"disable_initiator_tag", False):
345
gobject.source_remove(self.disable_initiator_tag)
346
self.disable_initiator_tag = None
347
if getattr(self, u"checker_initiator_tag", False):
283
348
gobject.source_remove(self.checker_initiator_tag)
284
349
self.checker_initiator_tag = None
285
350
self.stop_checker()
351
if self.disable_hook:
352
self.disable_hook(self)
288
354
# Do not run this again if called by a gobject.timeout_add
290
357
def __del__(self):
291
self.stop_hook = None
293
def checker_callback(self, pid, condition):
358
self.disable_hook = None
361
def checker_callback(self, pid, condition, command):
294
362
"""The checker has completed, so take appropriate actions."""
295
now = datetime.datetime.now()
296
363
self.checker_callback_tag = None
297
364
self.checker = None
298
if os.WIFEXITED(condition) \
299
and (os.WEXITSTATUS(condition) == 0):
300
logger.info(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):
365
if os.WIFEXITED(condition):
366
exitstatus = os.WEXITSTATUS(condition)
368
logger.info(u"Checker for %(name)s succeeded",
372
logger.info(u"Checker for %(name)s failed",
308
375
logger.warning(u"Checker for %(name)s crashed?",
311
logger.info(u"Checker for %(name)s failed",
378
def checked_ok(self):
379
"""Bump up the timeout for this client.
381
This should only be called when the client has been seen,
384
self.last_checked_ok = datetime.datetime.utcnow()
385
gobject.source_remove(self.disable_initiator_tag)
386
self.disable_initiator_tag = (gobject.timeout_add
387
(self.timeout_milliseconds(),
313
390
def start_checker(self):
314
391
"""Start a new checker subprocess if one is not running.
315
393
If a checker already exists, leave it running and do
317
395
# The reason for not killing a running checker is that if we
346
443
# always replaced by /dev/null.)
347
444
self.checker = subprocess.Popen(command,
350
self.checker_callback_tag = gobject.child_watch_add\
352
self.checker_callback)
446
shell=True, cwd=u"/")
447
self.checker_callback_tag = (gobject.child_watch_add
449
self.checker_callback,
451
# The checker may have completed before the gobject
452
# watch was added. Check for this.
453
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
455
gobject.source_remove(self.checker_callback_tag)
456
self.checker_callback(pid, status, command)
353
457
except OSError, error:
354
458
logger.error(u"Failed to start subprocess: %s",
356
460
# Re-run this periodically if run by gobject.timeout_add
358
463
def stop_checker(self):
359
464
"""Force the checker process, if any, to stop."""
360
465
if self.checker_callback_tag:
361
466
gobject.source_remove(self.checker_callback_tag)
362
467
self.checker_callback_tag = None
363
if getattr(self, "checker", None) is None:
468
if getattr(self, u"checker", None) is None:
365
470
logger.debug(u"Stopping checker for %(name)s", vars(self))
367
472
os.kill(self.checker.pid, signal.SIGTERM)
369
474
#if self.checker.poll() is None:
370
475
# os.kill(self.checker.pid, signal.SIGKILL)
371
476
except OSError, error:
372
477
if error.errno != errno.ESRCH: # No such process
374
479
self.checker = None
375
def still_valid(self):
376
"""Has the timeout not yet passed for this client?"""
377
now = datetime.datetime.now()
482
def dbus_service_property(dbus_interface, signature=u"v",
483
access=u"readwrite", byte_arrays=False):
484
"""Decorators for marking methods of a DBusObjectWithProperties to
485
become properties on the D-Bus.
487
The decorated method will be called with no arguments by "Get"
488
and with one argument by "Set".
490
The parameters, where they are supported, are the same as
491
dbus.service.method, except there is only "signature", since the
492
type from Get() and the type sent to Set() is the same.
494
# Encoding deeply encoded byte arrays is not supported yet by the
495
# "Set" method, so we fail early here:
496
if byte_arrays and signature != u"ay":
497
raise ValueError(u"Byte arrays not supported for non-'ay'"
498
u" signature %r" % signature)
500
func._dbus_is_property = True
501
func._dbus_interface = dbus_interface
502
func._dbus_signature = signature
503
func._dbus_access = access
504
func._dbus_name = func.__name__
505
if func._dbus_name.endswith(u"_dbus_property"):
506
func._dbus_name = func._dbus_name[:-14]
507
func._dbus_get_args_options = {u'byte_arrays': byte_arrays }
512
class DBusPropertyException(dbus.exceptions.DBusException):
513
"""A base class for D-Bus property-related exceptions
515
def __unicode__(self):
516
return unicode(str(self))
519
class DBusPropertyAccessException(DBusPropertyException):
520
"""A property's access permissions disallows an operation.
525
class DBusPropertyNotFound(DBusPropertyException):
526
"""An attempt was made to access a non-existing property.
531
class DBusObjectWithProperties(dbus.service.Object):
532
"""A D-Bus object with properties.
534
Classes inheriting from this can use the dbus_service_property
535
decorator to expose methods as D-Bus properties. It exposes the
536
standard Get(), Set(), and GetAll() methods on the D-Bus.
540
def _is_dbus_property(obj):
541
return getattr(obj, u"_dbus_is_property", False)
543
def _get_all_dbus_properties(self):
544
"""Returns a generator of (name, attribute) pairs
546
return ((prop._dbus_name, prop)
548
inspect.getmembers(self, self._is_dbus_property))
550
def _get_dbus_property(self, interface_name, property_name):
551
"""Returns a bound method if one exists which is a D-Bus
552
property with the specified name and interface.
554
for name in (property_name,
555
property_name + u"_dbus_property"):
556
prop = getattr(self, name, None)
558
or not self._is_dbus_property(prop)
559
or prop._dbus_name != property_name
560
or (interface_name and prop._dbus_interface
561
and interface_name != prop._dbus_interface)):
565
raise DBusPropertyNotFound(self.dbus_object_path + u":"
566
+ interface_name + u"."
569
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ss",
571
def Get(self, interface_name, property_name):
572
"""Standard D-Bus property Get() method, see D-Bus standard.
574
prop = self._get_dbus_property(interface_name, property_name)
575
if prop._dbus_access == u"write":
576
raise DBusPropertyAccessException(property_name)
578
if not hasattr(value, u"variant_level"):
580
return type(value)(value, variant_level=value.variant_level+1)
582
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ssv")
583
def Set(self, interface_name, property_name, value):
584
"""Standard D-Bus property Set() method, see D-Bus standard.
586
prop = self._get_dbus_property(interface_name, property_name)
587
if prop._dbus_access == u"read":
588
raise DBusPropertyAccessException(property_name)
589
if prop._dbus_get_args_options[u"byte_arrays"]:
590
# The byte_arrays option is not supported yet on
591
# signatures other than "ay".
592
if prop._dbus_signature != u"ay":
594
value = dbus.ByteArray(''.join(unichr(byte)
598
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"s",
599
out_signature=u"a{sv}")
600
def GetAll(self, interface_name):
601
"""Standard D-Bus property GetAll() method, see D-Bus
604
Note: Will not include properties with access="write".
607
for name, prop in self._get_all_dbus_properties():
609
and interface_name != prop._dbus_interface):
610
# Interface non-empty but did not match
612
# Ignore write-only properties
613
if prop._dbus_access == u"write":
616
if not hasattr(value, u"variant_level"):
619
all[name] = type(value)(value, variant_level=
620
value.variant_level+1)
621
return dbus.Dictionary(all, signature=u"sv")
623
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
625
path_keyword='object_path',
626
connection_keyword='connection')
627
def Introspect(self, object_path, connection):
628
"""Standard D-Bus method, overloaded to insert property tags.
630
xmlstring = dbus.service.Object.Introspect(self, object_path,
633
document = xml.dom.minidom.parseString(xmlstring)
634
def make_tag(document, name, prop):
635
e = document.createElement(u"property")
636
e.setAttribute(u"name", name)
637
e.setAttribute(u"type", prop._dbus_signature)
638
e.setAttribute(u"access", prop._dbus_access)
640
for if_tag in document.getElementsByTagName(u"interface"):
641
for tag in (make_tag(document, name, prop)
643
in self._get_all_dbus_properties()
644
if prop._dbus_interface
645
== if_tag.getAttribute(u"name")):
646
if_tag.appendChild(tag)
647
# Add the names to the return values for the
648
# "org.freedesktop.DBus.Properties" methods
649
if (if_tag.getAttribute(u"name")
650
== u"org.freedesktop.DBus.Properties"):
651
for cn in if_tag.getElementsByTagName(u"method"):
652
if cn.getAttribute(u"name") == u"Get":
653
for arg in cn.getElementsByTagName(u"arg"):
654
if (arg.getAttribute(u"direction")
656
arg.setAttribute(u"name", u"value")
657
elif cn.getAttribute(u"name") == u"GetAll":
658
for arg in cn.getElementsByTagName(u"arg"):
659
if (arg.getAttribute(u"direction")
661
arg.setAttribute(u"name", u"props")
662
xmlstring = document.toxml(u"utf-8")
664
except (AttributeError, xml.dom.DOMException,
665
xml.parsers.expat.ExpatError), error:
666
logger.error(u"Failed to override Introspection method",
671
class ClientDBus(Client, DBusObjectWithProperties):
672
"""A Client class using D-Bus
675
dbus_object_path: dbus.ObjectPath
676
bus: dbus.SystemBus()
678
# dbus.service.Object doesn't use super(), so we can't either.
680
def __init__(self, bus = None, *args, **kwargs):
682
Client.__init__(self, *args, **kwargs)
683
# Only now, when this client is initialized, can it show up on
685
self.dbus_object_path = (dbus.ObjectPath
687
+ self.name.replace(u".", u"_")))
688
DBusObjectWithProperties.__init__(self, self.bus,
689
self.dbus_object_path)
692
def _datetime_to_dbus(dt, variant_level=0):
693
"""Convert a UTC datetime.datetime() to a D-Bus type."""
694
return dbus.String(dt.isoformat(),
695
variant_level=variant_level)
698
oldstate = getattr(self, u"enabled", False)
699
r = Client.enable(self)
700
if oldstate != self.enabled:
702
self.PropertyChanged(dbus.String(u"enabled"),
703
dbus.Boolean(True, variant_level=1))
704
self.PropertyChanged(
705
dbus.String(u"last_enabled"),
706
self._datetime_to_dbus(self.last_enabled,
710
def disable(self, quiet = False):
711
oldstate = getattr(self, u"enabled", False)
712
r = Client.disable(self, quiet=quiet)
713
if not quiet and oldstate != self.enabled:
715
self.PropertyChanged(dbus.String(u"enabled"),
716
dbus.Boolean(False, variant_level=1))
719
def __del__(self, *args, **kwargs):
721
self.remove_from_connection()
724
if hasattr(DBusObjectWithProperties, u"__del__"):
725
DBusObjectWithProperties.__del__(self, *args, **kwargs)
726
Client.__del__(self, *args, **kwargs)
728
def checker_callback(self, pid, condition, command,
730
self.checker_callback_tag = None
733
self.PropertyChanged(dbus.String(u"checker_running"),
734
dbus.Boolean(False, variant_level=1))
735
if os.WIFEXITED(condition):
736
exitstatus = os.WEXITSTATUS(condition)
738
self.CheckerCompleted(dbus.Int16(exitstatus),
739
dbus.Int64(condition),
740
dbus.String(command))
743
self.CheckerCompleted(dbus.Int16(-1),
744
dbus.Int64(condition),
745
dbus.String(command))
747
return Client.checker_callback(self, pid, condition, command,
750
def checked_ok(self, *args, **kwargs):
751
r = Client.checked_ok(self, *args, **kwargs)
753
self.PropertyChanged(
754
dbus.String(u"last_checked_ok"),
755
(self._datetime_to_dbus(self.last_checked_ok,
759
def start_checker(self, *args, **kwargs):
760
old_checker = self.checker
761
if self.checker is not None:
762
old_checker_pid = self.checker.pid
764
old_checker_pid = None
765
r = Client.start_checker(self, *args, **kwargs)
766
# Only if new checker process was started
767
if (self.checker is not None
768
and old_checker_pid != self.checker.pid):
770
self.CheckerStarted(self.current_checker_command)
771
self.PropertyChanged(
772
dbus.String(u"checker_running"),
773
dbus.Boolean(True, variant_level=1))
776
def stop_checker(self, *args, **kwargs):
777
old_checker = getattr(self, u"checker", None)
778
r = Client.stop_checker(self, *args, **kwargs)
779
if (old_checker is not None
780
and getattr(self, u"checker", None) is None):
781
self.PropertyChanged(dbus.String(u"checker_running"),
782
dbus.Boolean(False, variant_level=1))
785
## D-Bus methods, signals & properties
786
_interface = u"se.bsnet.fukt.Mandos.Client"
790
# CheckerCompleted - signal
791
@dbus.service.signal(_interface, signature=u"nxs")
792
def CheckerCompleted(self, exitcode, waitstatus, command):
796
# CheckerStarted - signal
797
@dbus.service.signal(_interface, signature=u"s")
798
def CheckerStarted(self, command):
802
# PropertyChanged - signal
803
@dbus.service.signal(_interface, signature=u"sv")
804
def PropertyChanged(self, property, value):
809
@dbus.service.signal(_interface)
815
@dbus.service.signal(_interface)
823
@dbus.service.method(_interface)
825
return self.checked_ok()
828
@dbus.service.method(_interface)
833
# StartChecker - method
834
@dbus.service.method(_interface)
835
def StartChecker(self):
840
@dbus.service.method(_interface)
845
# StopChecker - method
846
@dbus.service.method(_interface)
847
def StopChecker(self):
853
@dbus_service_property(_interface, signature=u"s", access=u"read")
854
def name_dbus_property(self):
855
return dbus.String(self.name)
857
# fingerprint - property
858
@dbus_service_property(_interface, signature=u"s", access=u"read")
859
def fingerprint_dbus_property(self):
860
return dbus.String(self.fingerprint)
863
@dbus_service_property(_interface, signature=u"s",
865
def host_dbus_property(self, value=None):
866
if value is None: # get
867
return dbus.String(self.host)
870
self.PropertyChanged(dbus.String(u"host"),
871
dbus.String(value, variant_level=1))
874
@dbus_service_property(_interface, signature=u"s", access=u"read")
875
def created_dbus_property(self):
876
return dbus.String(self._datetime_to_dbus(self.created))
878
# last_enabled - property
879
@dbus_service_property(_interface, signature=u"s", access=u"read")
880
def last_enabled_dbus_property(self):
881
if self.last_enabled is None:
882
return dbus.String(u"")
883
return dbus.String(self._datetime_to_dbus(self.last_enabled))
886
@dbus_service_property(_interface, signature=u"b",
888
def enabled_dbus_property(self, value=None):
889
if value is None: # get
890
return dbus.Boolean(self.enabled)
896
# last_checked_ok - property
897
@dbus_service_property(_interface, signature=u"s",
899
def last_checked_ok_dbus_property(self, value=None):
900
if value is not None:
378
903
if self.last_checked_ok is None:
379
return now < (self.created + self.timeout)
381
return now < (self.last_checked_ok + self.timeout)
384
def peer_certificate(session):
385
"Return the peer's OpenPGP certificate as a bytestring"
386
# If not an OpenPGP certificate...
387
if gnutls.library.functions.gnutls_certificate_type_get\
388
(session._c_object) \
389
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
390
# ...do the normal thing
391
return session.peer_certificate
392
list_size = ctypes.c_uint()
393
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
394
(session._c_object, ctypes.byref(list_size))
395
if list_size.value == 0:
398
return ctypes.string_at(cert.data, cert.size)
401
def fingerprint(openpgp):
402
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
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
# New empty GnuTLS certificate
409
crt = gnutls.library.types.gnutls_openpgp_crt_t()
410
gnutls.library.functions.gnutls_openpgp_crt_init\
412
# Import the OpenPGP public key into the certificate
413
gnutls.library.functions.gnutls_openpgp_crt_import\
414
(crt, ctypes.byref(datum),
415
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
416
# Verify the self signature in the key
417
crtverify = ctypes.c_uint();
418
gnutls.library.functions.gnutls_openpgp_crt_verify_self\
419
(crt, 0, ctypes.byref(crtverify))
420
if crtverify.value != 0:
421
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
422
raise gnutls.errors.CertificateSecurityError("Verify failed")
423
# New buffer for the fingerprint
424
buffer = ctypes.create_string_buffer(20)
425
buffer_length = ctypes.c_size_t()
426
# Get the fingerprint from the certificate into the buffer
427
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
428
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
429
# Deinit the certificate
430
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
431
# Convert the buffer to a Python bytestring
432
fpr = ctypes.string_at(buffer, buffer_length.value)
433
# Convert the bytestring to hexadecimal notation
434
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
438
class tcp_handler(SocketServer.BaseRequestHandler, object):
439
"""A TCP request handler class.
440
Instantiated by IPv6_TCPServer for each request to handle it.
904
return dbus.String(u"")
905
return dbus.String(self._datetime_to_dbus(self
909
@dbus_service_property(_interface, signature=u"t",
911
def timeout_dbus_property(self, value=None):
912
if value is None: # get
913
return dbus.UInt64(self.timeout_milliseconds())
914
self.timeout = datetime.timedelta(0, 0, 0, value)
916
self.PropertyChanged(dbus.String(u"timeout"),
917
dbus.UInt64(value, variant_level=1))
918
if getattr(self, u"disable_initiator_tag", None) is None:
921
gobject.source_remove(self.disable_initiator_tag)
922
self.disable_initiator_tag = None
924
_timedelta_to_milliseconds((self
930
# The timeout has passed
933
self.disable_initiator_tag = (gobject.timeout_add
934
(time_to_die, self.disable))
936
# interval - property
937
@dbus_service_property(_interface, signature=u"t",
939
def interval_dbus_property(self, value=None):
940
if value is None: # get
941
return dbus.UInt64(self.interval_milliseconds())
942
self.interval = datetime.timedelta(0, 0, 0, value)
944
self.PropertyChanged(dbus.String(u"interval"),
945
dbus.UInt64(value, variant_level=1))
946
if getattr(self, u"checker_initiator_tag", None) is None:
948
# Reschedule checker run
949
gobject.source_remove(self.checker_initiator_tag)
950
self.checker_initiator_tag = (gobject.timeout_add
951
(value, self.start_checker))
952
self.start_checker() # Start one now, too
955
@dbus_service_property(_interface, signature=u"s",
957
def checker_dbus_property(self, value=None):
958
if value is None: # get
959
return dbus.String(self.checker_command)
960
self.checker_command = value
962
self.PropertyChanged(dbus.String(u"checker"),
963
dbus.String(self.checker_command,
966
# checker_running - property
967
@dbus_service_property(_interface, signature=u"b",
969
def checker_running_dbus_property(self, value=None):
970
if value is None: # get
971
return dbus.Boolean(self.checker is not None)
977
# object_path - property
978
@dbus_service_property(_interface, signature=u"o", access=u"read")
979
def object_path_dbus_property(self):
980
return self.dbus_object_path # is already a dbus.ObjectPath
983
@dbus_service_property(_interface, signature=u"ay",
984
access=u"write", byte_arrays=True)
985
def secret_dbus_property(self, value):
986
self.secret = str(value)
991
class ClientHandler(socketserver.BaseRequestHandler, object):
992
"""A class to handle client connections.
994
Instantiated once for each connection to handle it.
441
995
Note: This will run in its own forked process."""
443
997
def handle(self):
444
998
logger.info(u"TCP connection from: %s",
445
unicode(self.client_address))
446
session = gnutls.connection.ClientSession\
447
(self.request, gnutls.connection.X509Credentials())
449
line = self.request.makefile().readline()
450
logger.debug(u"Protocol version: %r", line)
452
if int(line.strip().split()[0]) > 1:
454
except (ValueError, IndexError, RuntimeError), error:
455
logger.error(u"Unknown protocol version: %s", error)
458
# Note: gnutls.connection.X509Credentials is really a generic
459
# GnuTLS certificate credentials object so long as no X.509
460
# keys are added to it. Therefore, we can use it here despite
461
# using OpenPGP certificates.
463
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
464
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
466
priority = "NORMAL" # Fallback default, since this
468
if self.server.settings["priority"]:
469
priority = self.server.settings["priority"]
470
gnutls.library.functions.gnutls_priority_set_direct\
471
(session._c_object, priority, None);
475
except gnutls.errors.GNUTLSError, error:
476
logger.warning(u"Handshake failed: %s", error)
477
# Do not run session.bye() here: the session is not
478
# established. Just abandon the request.
481
fpr = fingerprint(peer_certificate(session))
482
except (TypeError, gnutls.errors.GNUTLSError), error:
483
logger.warning(u"Bad certificate: %s", error)
486
logger.debug(u"Fingerprint: %s", fpr)
488
for c in self.server.clients:
489
if c.fingerprint == fpr:
493
logger.warning(u"Client not found for fingerprint: %s",
497
# Have to check if client.still_valid(), since it is possible
498
# that the client timed out while establishing the GnuTLS
500
if not client.still_valid():
501
logger.warning(u"Client %(name)s is invalid",
506
while sent_size < len(client.secret):
507
sent = session.send(client.secret[sent_size:])
508
logger.debug(u"Sent: %d, remaining: %d",
509
sent, len(client.secret)
510
- (sent_size + sent))
515
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
516
"""IPv6 TCP server. Accepts 'None' as address and/or port.
999
unicode(self.client_address))
1000
logger.debug(u"IPC Pipe FD: %d",
1001
self.server.child_pipe[1].fileno())
1002
# Open IPC pipe to parent process
1003
with contextlib.nested(self.server.child_pipe[1],
1004
self.server.parent_pipe[0]
1005
) as (ipc, ipc_return):
1006
session = (gnutls.connection
1007
.ClientSession(self.request,
1009
.X509Credentials()))
1011
# Note: gnutls.connection.X509Credentials is really a
1012
# generic GnuTLS certificate credentials object so long as
1013
# no X.509 keys are added to it. Therefore, we can use it
1014
# here despite using OpenPGP certificates.
1016
#priority = u':'.join((u"NONE", u"+VERS-TLS1.1",
1017
# u"+AES-256-CBC", u"+SHA1",
1018
# u"+COMP-NULL", u"+CTYPE-OPENPGP",
1020
# Use a fallback default, since this MUST be set.
1021
priority = self.server.gnutls_priority
1022
if priority is None:
1023
priority = u"NORMAL"
1024
(gnutls.library.functions
1025
.gnutls_priority_set_direct(session._c_object,
1028
# Start communication using the Mandos protocol
1029
# Get protocol number
1030
line = self.request.makefile().readline()
1031
logger.debug(u"Protocol version: %r", line)
1033
if int(line.strip().split()[0]) > 1:
1035
except (ValueError, IndexError, RuntimeError), error:
1036
logger.error(u"Unknown protocol version: %s", error)
1039
# Start GnuTLS connection
1042
except gnutls.errors.GNUTLSError, error:
1043
logger.warning(u"Handshake failed: %s", error)
1044
# Do not run session.bye() here: the session is not
1045
# established. Just abandon the request.
1047
logger.debug(u"Handshake succeeded")
1050
fpr = self.fingerprint(self.peer_certificate
1052
except (TypeError, gnutls.errors.GNUTLSError), error:
1053
logger.warning(u"Bad certificate: %s", error)
1055
logger.debug(u"Fingerprint: %s", fpr)
1057
for c in self.server.clients:
1058
if c.fingerprint == fpr:
1062
ipc.write(u"NOTFOUND %s %s\n"
1063
% (fpr, unicode(self.client_address)))
1066
class ClientProxy(object):
1067
"""Client proxy object. Not for calling methods."""
1068
def __init__(self, client):
1069
self.client = client
1070
def __getattr__(self, name):
1071
if name.startswith("ipc_"):
1073
ipc.write("%s %s\n" % (name[4:].upper(),
1076
if not hasattr(self.client, name):
1077
raise AttributeError
1078
ipc.write(u"GETATTR %s %s\n"
1079
% (name, self.client.fingerprint))
1080
return pickle.load(ipc_return)
1081
clientproxy = ClientProxy(client)
1082
# Have to check if client.enabled, since it is
1083
# possible that the client was disabled since the
1084
# GnuTLS session was established.
1085
if not clientproxy.enabled:
1086
clientproxy.ipc_disabled()
1089
clientproxy.ipc_sending()
1091
while sent_size < len(client.secret):
1092
sent = session.send(client.secret[sent_size:])
1093
logger.debug(u"Sent: %d, remaining: %d",
1094
sent, len(client.secret)
1095
- (sent_size + sent))
1101
def peer_certificate(session):
1102
"Return the peer's OpenPGP certificate as a bytestring"
1103
# If not an OpenPGP certificate...
1104
if (gnutls.library.functions
1105
.gnutls_certificate_type_get(session._c_object)
1106
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1107
# ...do the normal thing
1108
return session.peer_certificate
1109
list_size = ctypes.c_uint(1)
1110
cert_list = (gnutls.library.functions
1111
.gnutls_certificate_get_peers
1112
(session._c_object, ctypes.byref(list_size)))
1113
if not bool(cert_list) and list_size.value != 0:
1114
raise gnutls.errors.GNUTLSError(u"error getting peer"
1116
if list_size.value == 0:
1119
return ctypes.string_at(cert.data, cert.size)
1122
def fingerprint(openpgp):
1123
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1124
# New GnuTLS "datum" with the OpenPGP public key
1125
datum = (gnutls.library.types
1126
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1129
ctypes.c_uint(len(openpgp))))
1130
# New empty GnuTLS certificate
1131
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1132
(gnutls.library.functions
1133
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1134
# Import the OpenPGP public key into the certificate
1135
(gnutls.library.functions
1136
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1137
gnutls.library.constants
1138
.GNUTLS_OPENPGP_FMT_RAW))
1139
# Verify the self signature in the key
1140
crtverify = ctypes.c_uint()
1141
(gnutls.library.functions
1142
.gnutls_openpgp_crt_verify_self(crt, 0,
1143
ctypes.byref(crtverify)))
1144
if crtverify.value != 0:
1145
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1146
raise (gnutls.errors.CertificateSecurityError
1148
# New buffer for the fingerprint
1149
buf = ctypes.create_string_buffer(20)
1150
buf_len = ctypes.c_size_t()
1151
# Get the fingerprint from the certificate into the buffer
1152
(gnutls.library.functions
1153
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1154
ctypes.byref(buf_len)))
1155
# Deinit the certificate
1156
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1157
# Convert the buffer to a Python bytestring
1158
fpr = ctypes.string_at(buf, buf_len.value)
1159
# Convert the bytestring to hexadecimal notation
1160
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
1164
class ForkingMixInWithPipes(socketserver.ForkingMixIn, object):
1165
"""Like socketserver.ForkingMixIn, but also pass a pipe pair."""
1166
def process_request(self, request, client_address):
1167
"""Overrides and wraps the original process_request().
1169
This function creates a new pipe in self.pipe
1171
# Child writes to child_pipe
1172
self.child_pipe = map(os.fdopen, os.pipe(), u"rw", (1, 0))
1173
# Parent writes to parent_pipe
1174
self.parent_pipe = map(os.fdopen, os.pipe(), u"rw", (1, 0))
1175
super(ForkingMixInWithPipes,
1176
self).process_request(request, client_address)
1177
# Close unused ends for parent
1178
self.parent_pipe[0].close() # close read end
1179
self.child_pipe[1].close() # close write end
1180
self.add_pipe_fds(self.child_pipe[0], self.parent_pipe[1])
1181
def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1182
"""Dummy function; override as necessary"""
1183
child_pipe_fd.close()
1184
parent_pipe_fd.close()
1187
class IPv6_TCPServer(ForkingMixInWithPipes,
1188
socketserver.TCPServer, object):
1189
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
518
settings: Server settings
519
clients: Set() of Client objects
1192
enabled: Boolean; whether this server is activated yet
1193
interface: None or a network interface name (string)
1194
use_ipv6: Boolean; to use IPv6 or not
521
address_family = socket.AF_INET6
522
def __init__(self, *args, **kwargs):
523
if "settings" in kwargs:
524
self.settings = kwargs["settings"]
525
del kwargs["settings"]
526
if "clients" in kwargs:
527
self.clients = kwargs["clients"]
528
del kwargs["clients"]
529
return super(type(self), self).__init__(*args, **kwargs)
1196
def __init__(self, server_address, RequestHandlerClass,
1197
interface=None, use_ipv6=True):
1198
self.interface = interface
1200
self.address_family = socket.AF_INET6
1201
socketserver.TCPServer.__init__(self, server_address,
1202
RequestHandlerClass)
530
1203
def server_bind(self):
531
1204
"""This overrides the normal server_bind() function
532
1205
to bind to an interface if one was specified, and also NOT to
533
1206
bind to an address or port if they were not specified."""
534
if self.settings["interface"]:
535
# 25 is from /usr/include/asm-i486/socket.h
536
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
538
self.socket.setsockopt(socket.SOL_SOCKET,
540
self.settings["interface"])
541
except socket.error, error:
542
if error[0] == errno.EPERM:
543
logger.error(u"No permission to"
544
u" bind to interface %s",
545
self.settings["interface"])
1207
if self.interface is not None:
1208
if SO_BINDTODEVICE is None:
1209
logger.error(u"SO_BINDTODEVICE does not exist;"
1210
u" cannot bind to interface %s",
1214
self.socket.setsockopt(socket.SOL_SOCKET,
1218
except socket.error, error:
1219
if error[0] == errno.EPERM:
1220
logger.error(u"No permission to"
1221
u" bind to interface %s",
1223
elif error[0] == errno.ENOPROTOOPT:
1224
logger.error(u"SO_BINDTODEVICE not available;"
1225
u" cannot bind to interface %s",
548
1229
# Only bind(2) the socket if we really need to.
549
1230
if self.server_address[0] or self.server_address[1]:
550
1231
if not self.server_address[0]:
552
self.server_address = (in6addr_any,
1232
if self.address_family == socket.AF_INET6:
1233
any_address = u"::" # in6addr_any
1235
any_address = socket.INADDR_ANY
1236
self.server_address = (any_address,
553
1237
self.server_address[1])
554
1238
elif not self.server_address[1]:
555
1239
self.server_address = (self.server_address[0],
557
# if self.settings["interface"]:
1241
# if self.interface:
558
1242
# self.server_address = (self.server_address[0],
561
1245
# if_nametoindex
564
return super(type(self), self).server_bind()
1247
return socketserver.TCPServer.server_bind(self)
1250
class MandosServer(IPv6_TCPServer):
1254
clients: set of Client objects
1255
gnutls_priority GnuTLS priority string
1256
use_dbus: Boolean; to emit D-Bus signals or not
1258
Assumes a gobject.MainLoop event loop.
1260
def __init__(self, server_address, RequestHandlerClass,
1261
interface=None, use_ipv6=True, clients=None,
1262
gnutls_priority=None, use_dbus=True):
1263
self.enabled = False
1264
self.clients = clients
1265
if self.clients is None:
1266
self.clients = set()
1267
self.use_dbus = use_dbus
1268
self.gnutls_priority = gnutls_priority
1269
IPv6_TCPServer.__init__(self, server_address,
1270
RequestHandlerClass,
1271
interface = interface,
1272
use_ipv6 = use_ipv6)
1273
def server_activate(self):
1275
return socketserver.TCPServer.server_activate(self)
1278
def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1279
# Call "handle_ipc" for both data and EOF events
1280
gobject.io_add_watch(child_pipe_fd.fileno(),
1281
gobject.IO_IN | gobject.IO_HUP,
1282
functools.partial(self.handle_ipc,
1283
reply = parent_pipe_fd,
1284
sender= child_pipe_fd))
1285
def handle_ipc(self, source, condition, reply=None, sender=None):
1287
gobject.IO_IN: u"IN", # There is data to read.
1288
gobject.IO_OUT: u"OUT", # Data can be written (without
1290
gobject.IO_PRI: u"PRI", # There is urgent data to read.
1291
gobject.IO_ERR: u"ERR", # Error condition.
1292
gobject.IO_HUP: u"HUP" # Hung up (the connection has been
1293
# broken, usually for pipes and
1296
conditions_string = ' | '.join(name
1298
condition_names.iteritems()
1299
if cond & condition)
1300
logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1303
# Read a line from the file object
1304
cmdline = sender.readline()
1305
if not cmdline: # Empty line means end of file
1306
# close the IPC pipes
1310
# Stop calling this function
1313
logger.debug(u"IPC command: %r", cmdline)
1315
# Parse and act on command
1316
cmd, args = cmdline.rstrip(u"\r\n").split(None, 1)
1318
if cmd == u"NOTFOUND":
1319
fpr, address = args.split(None, 1)
1320
logger.warning(u"Client not found for fingerprint: %s, ad"
1321
u"dress: %s", fpr, address)
1324
mandos_dbus_service.ClientNotFound(fpr, address)
1325
elif cmd == u"DISABLED":
1326
for client in self.clients:
1327
if client.name == args:
1328
logger.warning(u"Client %s is disabled", args)
1334
logger.error(u"Unknown client %s is disabled", args)
1335
elif cmd == u"SENDING":
1336
for client in self.clients:
1337
if client.name == args:
1338
logger.info(u"Sending secret to %s", client.name)
1345
logger.error(u"Sending secret to unknown client %s",
1347
elif cmd == u"GETATTR":
1348
attr_name, fpr = args.split(None, 1)
1349
for client in self.clients:
1350
if client.fingerprint == fpr:
1351
attr_value = getattr(client, attr_name, None)
1352
logger.debug("IPC reply: %r", attr_value)
1353
pickle.dump(attr_value, reply)
1356
logger.error(u"Client %s on address %s requesting "
1357
u"attribute %s not found", fpr, address,
1359
pickle.dump(None, reply)
1361
logger.error(u"Unknown IPC command: %r", cmdline)
1363
# Keep calling this function
567
1367
def string_to_delta(interval):
568
1368
"""Parse a string and return a datetime.timedelta
570
>>> string_to_delta('7d')
1370
>>> string_to_delta(u'7d')
571
1371
datetime.timedelta(7)
572
>>> string_to_delta('60s')
1372
>>> string_to_delta(u'60s')
573
1373
datetime.timedelta(0, 60)
574
>>> string_to_delta('60m')
1374
>>> string_to_delta(u'60m')
575
1375
datetime.timedelta(0, 3600)
576
>>> string_to_delta('24h')
1376
>>> string_to_delta(u'24h')
577
1377
datetime.timedelta(1)
578
1378
>>> string_to_delta(u'1w')
579
1379
datetime.timedelta(7)
580
>>> string_to_delta('5m 30s')
1380
>>> string_to_delta(u'5m 30s')
581
1381
datetime.timedelta(0, 330)
583
1383
timevalue = datetime.timedelta(0)
584
1384
for s in interval.split():
586
suffix=unicode(s[-1])
1386
suffix = unicode(s[-1])
588
1388
if suffix == u"d":
589
1389
delta = datetime.timedelta(value)
590
1390
elif suffix == u"s":