2
# -*- mode: python; coding: utf-8 -*-
4
# Mandos server - give out binary blobs to connecting clients.
6
# This program is partly derived from an example program for an Avahi
7
# service publisher, downloaded from
8
# <http://avahi.org/wiki/PythonPublishExample>. This includes the
9
# methods "add", "remove", "server_state_changed",
10
# "entry_group_state_changed", "cleanup", and "activate" in the
11
# "AvahiService" class, and some lines in "main".
14
# Copyright © 2008-2011 Teddy Hogeborn
15
# Copyright © 2008-2011 Björn Påhlsson
17
# This program is free software: you can redistribute it and/or modify
18
# it under the terms of the GNU General Public License as published by
19
# the Free Software Foundation, either version 3 of the License, or
20
# (at your option) any later version.
22
# This program is distributed in the hope that it will be useful,
23
# but WITHOUT ANY WARRANTY; without even the implied warranty of
24
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
# GNU General Public License for more details.
27
# You should have received a copy of the GNU General Public License
28
# along with this program. If not, see
29
# <http://www.gnu.org/licenses/>.
31
# Contact the authors at <mandos@fukt.bsnet.se>.
34
from __future__ import (division, absolute_import, print_function,
37
import SocketServer as socketserver
6
from optparse import OptionParser
42
9
import gnutls.crypto
43
10
import gnutls.connection
44
11
import gnutls.errors
45
import gnutls.library.functions
46
import gnutls.library.constants
47
import gnutls.library.types
48
import ConfigParser as configparser
57
import logging.handlers
63
import cPickle as pickle
64
import multiprocessing
71
from dbus.mainloop.glib import DBusGMainLoop
74
import xml.dom.minidom
78
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
79
except AttributeError:
81
from IN import SO_BINDTODEVICE
83
SO_BINDTODEVICE = None
88
#logger = logging.getLogger('mandos')
89
logger = logging.Logger('mandos')
90
syslogger = (logging.handlers.SysLogHandler
91
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
92
address = str("/dev/log")))
93
syslogger.setFormatter(logging.Formatter
94
('Mandos [%(process)d]: %(levelname)s:'
96
logger.addHandler(syslogger)
98
console = logging.StreamHandler()
99
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
102
logger.addHandler(console)
104
class AvahiError(Exception):
105
def __init__(self, value, *args, **kwargs):
107
super(AvahiError, self).__init__(value, *args, **kwargs)
108
def __unicode__(self):
109
return unicode(repr(self.value))
111
class AvahiServiceError(AvahiError):
114
class AvahiGroupError(AvahiError):
118
class AvahiService(object):
119
"""An Avahi (Zeroconf) service.
122
interface: integer; avahi.IF_UNSPEC or an interface index.
123
Used to optionally bind to the specified interface.
124
name: string; Example: 'Mandos'
125
type: string; Example: '_mandos._tcp'.
126
See <http://www.dns-sd.org/ServiceTypes.html>
127
port: integer; what port to announce
128
TXT: list of strings; TXT record for the service
129
domain: string; Domain to publish on, default to .local if empty.
130
host: string; Host to publish records for, default is localhost
131
max_renames: integer; maximum number of renames
132
rename_count: integer; counter so we only rename after collisions
133
a sensible number of times
134
group: D-Bus Entry Group
136
bus: dbus.SystemBus()
138
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
139
servicetype = None, port = None, TXT = None,
140
domain = "", host = "", max_renames = 32768,
141
protocol = avahi.PROTO_UNSPEC, bus = None):
142
self.interface = interface
144
self.type = servicetype
146
self.TXT = TXT if TXT is not None else []
149
self.rename_count = 0
150
self.max_renames = max_renames
151
self.protocol = protocol
152
self.group = None # our entry group
155
self.entry_group_state_changed_match = None
157
"""Derived from the Avahi example code"""
158
if self.rename_count >= self.max_renames:
159
logger.critical("No suitable Zeroconf service name found"
160
" after %i retries, exiting.",
162
raise AvahiServiceError("Too many renames")
163
self.name = unicode(self.server.GetAlternativeServiceName(self.name))
164
logger.info("Changing Zeroconf service name to %r ...",
166
syslogger.setFormatter(logging.Formatter
167
('Mandos (%s) [%%(process)d]:'
168
' %%(levelname)s: %%(message)s'
173
except dbus.exceptions.DBusException as error:
174
logger.critical("DBusException: %s", error)
177
self.rename_count += 1
179
"""Derived from the Avahi example code"""
180
if self.entry_group_state_changed_match is not None:
181
self.entry_group_state_changed_match.remove()
182
self.entry_group_state_changed_match = None
183
if self.group is not None:
186
"""Derived from the Avahi example code"""
188
if self.group is None:
189
self.group = dbus.Interface(
190
self.bus.get_object(avahi.DBUS_NAME,
191
self.server.EntryGroupNew()),
192
avahi.DBUS_INTERFACE_ENTRY_GROUP)
193
self.entry_group_state_changed_match = (
194
self.group.connect_to_signal(
195
'StateChanged', self .entry_group_state_changed))
196
logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
197
self.name, self.type)
198
self.group.AddService(
201
dbus.UInt32(0), # flags
202
self.name, self.type,
203
self.domain, self.host,
204
dbus.UInt16(self.port),
205
avahi.string_array_to_txt_array(self.TXT))
207
def entry_group_state_changed(self, state, error):
208
"""Derived from the Avahi example code"""
209
logger.debug("Avahi entry group state change: %i", state)
211
if state == avahi.ENTRY_GROUP_ESTABLISHED:
212
logger.debug("Zeroconf service established.")
213
elif state == avahi.ENTRY_GROUP_COLLISION:
214
logger.info("Zeroconf service name collision.")
216
elif state == avahi.ENTRY_GROUP_FAILURE:
217
logger.critical("Avahi: Error in group state changed %s",
219
raise AvahiGroupError("State changed: %s"
222
"""Derived from the Avahi example code"""
223
if self.group is not None:
226
except (dbus.exceptions.UnknownMethodException,
227
dbus.exceptions.DBusException) as e:
231
def server_state_changed(self, state, error=None):
232
"""Derived from the Avahi example code"""
233
logger.debug("Avahi server state change: %i", state)
234
bad_states = { avahi.SERVER_INVALID:
235
"Zeroconf server invalid",
236
avahi.SERVER_REGISTERING: None,
237
avahi.SERVER_COLLISION:
238
"Zeroconf server name collision",
239
avahi.SERVER_FAILURE:
240
"Zeroconf server failure" }
241
if state in bad_states:
242
if bad_states[state] is not None:
244
logger.error(bad_states[state])
246
logger.error(bad_states[state] + ": %r", error)
248
elif state == avahi.SERVER_RUNNING:
252
logger.debug("Unknown state: %r", state)
254
logger.debug("Unknown state: %r: %r", state, error)
256
"""Derived from the Avahi example code"""
257
if self.server is None:
258
self.server = dbus.Interface(
259
self.bus.get_object(avahi.DBUS_NAME,
260
avahi.DBUS_PATH_SERVER,
261
follow_name_owner_changes=True),
262
avahi.DBUS_INTERFACE_SERVER)
263
self.server.connect_to_signal("StateChanged",
264
self.server_state_changed)
265
self.server_state_changed(self.server.GetState())
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
14
class Client(object):
275
"""A representation of a client host served by this server.
278
_approved: bool(); 'None' if not yet approved/disapproved
279
approval_delay: datetime.timedelta(); Time to wait for approval
280
approval_duration: datetime.timedelta(); Duration of one approval
281
checker: subprocess.Popen(); a running checker process used
282
to see if the client lives.
283
'None' if no process is running.
284
checker_callback_tag: a gobject event source tag, or None
285
checker_command: string; External command which is run to check
286
if client lives. %() expansions are done at
287
runtime with vars(self) as dict, so that for
288
instance %(name)s can be used in the command.
289
checker_initiator_tag: a gobject event source tag, or None
290
created: datetime.datetime(); (UTC) object creation
291
current_checker_command: string; current running checker_command
292
disable_hook: If set, called by disable() as disable_hook(self)
293
disable_initiator_tag: a gobject event source tag, or None
295
fingerprint: string (40 or 32 hexadecimal digits); used to
296
uniquely identify the client
297
host: string; available for use by the checker command
298
interval: datetime.timedelta(); How often to start a new checker
299
last_approval_request: datetime.datetime(); (UTC) or None
300
last_checked_ok: datetime.datetime(); (UTC) or None
301
last_enabled: datetime.datetime(); (UTC)
302
name: string; from the config file, used in log messages and
304
secret: bytestring; sent verbatim (over TLS) to client
305
timeout: datetime.timedelta(); How long from last_checked_ok
306
until this client is disabled
307
extended_timeout: extra long timeout when password has been sent
308
runtime_expansions: Allowed attributes for runtime expansion.
309
expires: datetime.datetime(); time (UTC) when a client will be
313
runtime_expansions = ("approval_delay", "approval_duration",
314
"created", "enabled", "fingerprint",
315
"host", "interval", "last_checked_ok",
316
"last_enabled", "name", "timeout")
318
def timeout_milliseconds(self):
319
"Return the 'timeout' attribute in milliseconds"
320
return _timedelta_to_milliseconds(self.timeout)
322
def extended_timeout_milliseconds(self):
323
"Return the 'extended_timeout' attribute in milliseconds"
324
return _timedelta_to_milliseconds(self.extended_timeout)
326
def interval_milliseconds(self):
327
"Return the 'interval' attribute in milliseconds"
328
return _timedelta_to_milliseconds(self.interval)
330
def approval_delay_milliseconds(self):
331
return _timedelta_to_milliseconds(self.approval_delay)
333
def __init__(self, name = None, disable_hook=None, config=None):
334
"""Note: the 'checker' key in 'config' sets the
335
'checker_command' attribute and *not* the 'checker'
15
def __init__(self, name=None, dn=None, password=None,
16
passfile=None, fqdn=None, timeout=None,
340
logger.debug("Creating client %r", self.name)
341
# Uppercase and remove spaces from fingerprint for later
342
# comparison purposes with return value from the fingerprint()
344
self.fingerprint = (config["fingerprint"].upper()
346
logger.debug(" Fingerprint: %s", self.fingerprint)
347
if "secret" in config:
348
self.secret = config["secret"].decode("base64")
349
elif "secfile" in config:
350
with open(os.path.expanduser(os.path.expandvars
351
(config["secfile"])),
353
self.secret = secfile.read()
355
raise TypeError("No secret or secfile for client %s"
357
self.host = config.get("host", "")
358
self.created = datetime.datetime.utcnow()
360
self.last_approval_request = None
361
self.last_enabled = None
362
self.last_checked_ok = None
363
self.timeout = string_to_delta(config["timeout"])
364
self.extended_timeout = string_to_delta(config["extended_timeout"])
365
self.interval = string_to_delta(config["interval"])
366
self.disable_hook = disable_hook
368
self.checker_initiator_tag = None
369
self.disable_initiator_tag = None
371
self.checker_callback_tag = None
372
self.checker_command = config["checker"]
373
self.current_checker_command = None
374
self.last_connect = None
375
self._approved = None
376
self.approved_by_default = config.get("approved_by_default",
378
self.approvals_pending = 0
379
self.approval_delay = string_to_delta(
380
config["approval_delay"])
381
self.approval_duration = string_to_delta(
382
config["approval_duration"])
383
self.changedstate = multiprocessing_manager.Condition(multiprocessing_manager.Lock())
385
def send_changedstate(self):
386
self.changedstate.acquire()
387
self.changedstate.notify_all()
388
self.changedstate.release()
391
"""Start this client's checker and timeout hooks"""
392
if getattr(self, "enabled", False):
395
self.send_changedstate()
396
# Schedule a new checker to be started an 'interval' from now,
397
# and every interval from then on.
398
self.checker_initiator_tag = (gobject.timeout_add
399
(self.interval_milliseconds(),
401
# Schedule a disable() when 'timeout' has passed
402
self.expires = datetime.datetime.utcnow() + self.timeout
403
self.disable_initiator_tag = (gobject.timeout_add
404
(self.timeout_milliseconds(),
407
self.last_enabled = datetime.datetime.utcnow()
408
# Also start a new checker *right now*.
411
def disable(self, quiet=True):
412
"""Disable this client."""
413
if not getattr(self, "enabled", False):
416
self.send_changedstate()
418
logger.info("Disabling client %s", self.name)
419
if getattr(self, "disable_initiator_tag", False):
420
gobject.source_remove(self.disable_initiator_tag)
421
self.disable_initiator_tag = None
423
if getattr(self, "checker_initiator_tag", False):
424
gobject.source_remove(self.checker_initiator_tag)
425
self.checker_initiator_tag = None
427
if self.disable_hook:
428
self.disable_hook(self)
430
# Do not run this again if called by a gobject.timeout_add
434
self.disable_hook = None
437
def checker_callback(self, pid, condition, command):
438
"""The checker has completed, so take appropriate actions."""
439
self.checker_callback_tag = None
441
if os.WIFEXITED(condition):
442
exitstatus = os.WEXITSTATUS(condition)
444
logger.info("Checker for %(name)s succeeded",
448
logger.info("Checker for %(name)s failed",
451
logger.warning("Checker for %(name)s crashed?",
454
def checked_ok(self, timeout=None):
455
"""Bump up the timeout for this client.
457
This should only be called when the client has been seen,
21
self.password = password
23
self.password = open(passfile).readall()
25
print "No Password or Passfile in client config file"
26
# raise RuntimeError XXX
27
self.password = "gazonk"
460
31
if timeout is None:
461
timeout = self.timeout
462
self.last_checked_ok = datetime.datetime.utcnow()
463
gobject.source_remove(self.disable_initiator_tag)
464
self.expires = datetime.datetime.utcnow() + timeout
465
self.disable_initiator_tag = (gobject.timeout_add
466
(_timedelta_to_milliseconds(timeout),
469
def need_approval(self):
470
self.last_approval_request = datetime.datetime.utcnow()
472
def start_checker(self):
473
"""Start a new checker subprocess if one is not running.
475
If a checker already exists, leave it running and do
477
# The reason for not killing a running checker is that if we
478
# did that, then if a checker (for some reason) started
479
# running slowly and taking more than 'interval' time, the
480
# client would inevitably timeout, since no checker would get
481
# a chance to run to completion. If we instead leave running
482
# checkers alone, the checker would have to take more time
483
# than 'timeout' for the client to be disabled, which is as it
486
# If a checker exists, make sure it is not a zombie
32
timeout = self.server.options.timeout
33
self.timeout = timeout
35
interval = self.server.options.interval
36
self.interval = interval
38
def server_bind(self):
39
if self.options.interface:
40
if not hasattr(socket, "SO_BINDTODEVICE"):
41
# From /usr/include/asm-i486/socket.h
42
socket.SO_BINDTODEVICE = 25
488
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
489
except (AttributeError, OSError) as error:
490
if (isinstance(error, OSError)
491
and error.errno != errno.ECHILD):
44
self.socket.setsockopt(socket.SOL_SOCKET,
45
socket.SO_BINDTODEVICE,
46
self.options.interface)
47
except socket.error, error:
48
if error[0] == errno.EPERM:
49
print "Warning: Denied permission to bind to interface", \
50
self.options.interface
495
logger.warning("Checker was a zombie")
496
gobject.source_remove(self.checker_callback_tag)
497
self.checker_callback(pid, status,
498
self.current_checker_command)
499
# Start a new checker if needed
500
if self.checker is None:
502
# In case checker_command has exactly one % operator
503
command = self.checker_command % self.host
505
# Escape attributes for the shell
506
escaped_attrs = dict(
508
re.escape(unicode(str(getattr(self, attr, "")),
512
self.runtime_expansions)
515
command = self.checker_command % escaped_attrs
516
except TypeError as error:
517
logger.error('Could not format string "%s":'
518
' %s', self.checker_command, error)
519
return True # Try again later
520
self.current_checker_command = command
522
logger.info("Starting checker %r for %s",
524
# We don't need to redirect stdout and stderr, since
525
# in normal mode, that is already done by daemon(),
526
# and in debug mode we don't want to. (Stdin is
527
# always replaced by /dev/null.)
528
self.checker = subprocess.Popen(command,
531
self.checker_callback_tag = (gobject.child_watch_add
533
self.checker_callback,
535
# The checker may have completed before the gobject
536
# watch was added. Check for this.
537
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
539
gobject.source_remove(self.checker_callback_tag)
540
self.checker_callback(pid, status, command)
541
except OSError as error:
542
logger.error("Failed to start subprocess: %s",
544
# Re-run this periodically if run by gobject.timeout_add
547
def stop_checker(self):
548
"""Force the checker process, if any, to stop."""
549
if self.checker_callback_tag:
550
gobject.source_remove(self.checker_callback_tag)
551
self.checker_callback_tag = None
552
if getattr(self, "checker", None) is None:
554
logger.debug("Stopping checker for %(name)s", vars(self))
556
os.kill(self.checker.pid, signal.SIGTERM)
558
#if self.checker.poll() is None:
559
# os.kill(self.checker.pid, signal.SIGKILL)
560
except OSError as error:
561
if error.errno != errno.ESRCH: # No such process
566
def dbus_service_property(dbus_interface, signature="v",
567
access="readwrite", byte_arrays=False):
568
"""Decorators for marking methods of a DBusObjectWithProperties to
569
become properties on the D-Bus.
571
The decorated method will be called with no arguments by "Get"
572
and with one argument by "Set".
574
The parameters, where they are supported, are the same as
575
dbus.service.method, except there is only "signature", since the
576
type from Get() and the type sent to Set() is the same.
578
# Encoding deeply encoded byte arrays is not supported yet by the
579
# "Set" method, so we fail early here:
580
if byte_arrays and signature != "ay":
581
raise ValueError("Byte arrays not supported for non-'ay'"
582
" signature %r" % signature)
584
func._dbus_is_property = True
585
func._dbus_interface = dbus_interface
586
func._dbus_signature = signature
587
func._dbus_access = access
588
func._dbus_name = func.__name__
589
if func._dbus_name.endswith("_dbus_property"):
590
func._dbus_name = func._dbus_name[:-14]
591
func._dbus_get_args_options = {'byte_arrays': byte_arrays }
596
class DBusPropertyException(dbus.exceptions.DBusException):
597
"""A base class for D-Bus property-related exceptions
599
def __unicode__(self):
600
return unicode(str(self))
603
class DBusPropertyAccessException(DBusPropertyException):
604
"""A property's access permissions disallows an operation.
609
class DBusPropertyNotFound(DBusPropertyException):
610
"""An attempt was made to access a non-existing property.
615
class DBusObjectWithProperties(dbus.service.Object):
616
"""A D-Bus object with properties.
618
Classes inheriting from this can use the dbus_service_property
619
decorator to expose methods as D-Bus properties. It exposes the
620
standard Get(), Set(), and GetAll() methods on the D-Bus.
624
def _is_dbus_property(obj):
625
return getattr(obj, "_dbus_is_property", False)
627
def _get_all_dbus_properties(self):
628
"""Returns a generator of (name, attribute) pairs
630
return ((prop.__get__(self)._dbus_name, prop.__get__(self))
631
for cls in self.__class__.__mro__
632
for name, prop in inspect.getmembers(cls, self._is_dbus_property))
634
def _get_dbus_property(self, interface_name, property_name):
635
"""Returns a bound method if one exists which is a D-Bus
636
property with the specified name and interface.
638
for cls in self.__class__.__mro__:
639
for name, value in inspect.getmembers(cls, self._is_dbus_property):
640
if value._dbus_name == property_name and value._dbus_interface == interface_name:
641
return value.__get__(self)
644
raise DBusPropertyNotFound(self.dbus_object_path + ":"
645
+ interface_name + "."
648
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ss",
650
def Get(self, interface_name, property_name):
651
"""Standard D-Bus property Get() method, see D-Bus standard.
653
prop = self._get_dbus_property(interface_name, property_name)
654
if prop._dbus_access == "write":
655
raise DBusPropertyAccessException(property_name)
657
if not hasattr(value, "variant_level"):
659
return type(value)(value, variant_level=value.variant_level+1)
661
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
662
def Set(self, interface_name, property_name, value):
663
"""Standard D-Bus property Set() method, see D-Bus standard.
665
prop = self._get_dbus_property(interface_name, property_name)
666
if prop._dbus_access == "read":
667
raise DBusPropertyAccessException(property_name)
668
if prop._dbus_get_args_options["byte_arrays"]:
669
# The byte_arrays option is not supported yet on
670
# signatures other than "ay".
671
if prop._dbus_signature != "ay":
673
value = dbus.ByteArray(''.join(unichr(byte)
677
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s",
678
out_signature="a{sv}")
679
def GetAll(self, interface_name):
680
"""Standard D-Bus property GetAll() method, see D-Bus
683
Note: Will not include properties with access="write".
686
for name, prop in self._get_all_dbus_properties():
688
and interface_name != prop._dbus_interface):
689
# Interface non-empty but did not match
691
# Ignore write-only properties
692
if prop._dbus_access == "write":
695
if not hasattr(value, "variant_level"):
698
all[name] = type(value)(value, variant_level=
699
value.variant_level+1)
700
return dbus.Dictionary(all, signature="sv")
702
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
704
path_keyword='object_path',
705
connection_keyword='connection')
706
def Introspect(self, object_path, connection):
707
"""Standard D-Bus method, overloaded to insert property tags.
709
xmlstring = dbus.service.Object.Introspect(self, object_path,
712
document = xml.dom.minidom.parseString(xmlstring)
713
def make_tag(document, name, prop):
714
e = document.createElement("property")
715
e.setAttribute("name", name)
716
e.setAttribute("type", prop._dbus_signature)
717
e.setAttribute("access", prop._dbus_access)
719
for if_tag in document.getElementsByTagName("interface"):
720
for tag in (make_tag(document, name, prop)
722
in self._get_all_dbus_properties()
723
if prop._dbus_interface
724
== if_tag.getAttribute("name")):
725
if_tag.appendChild(tag)
726
# Add the names to the return values for the
727
# "org.freedesktop.DBus.Properties" methods
728
if (if_tag.getAttribute("name")
729
== "org.freedesktop.DBus.Properties"):
730
for cn in if_tag.getElementsByTagName("method"):
731
if cn.getAttribute("name") == "Get":
732
for arg in cn.getElementsByTagName("arg"):
733
if (arg.getAttribute("direction")
735
arg.setAttribute("name", "value")
736
elif cn.getAttribute("name") == "GetAll":
737
for arg in cn.getElementsByTagName("arg"):
738
if (arg.getAttribute("direction")
740
arg.setAttribute("name", "props")
741
xmlstring = document.toxml("utf-8")
743
except (AttributeError, xml.dom.DOMException,
744
xml.parsers.expat.ExpatError) as error:
745
logger.error("Failed to override Introspection method",
750
def datetime_to_dbus (dt, variant_level=0):
751
"""Convert a UTC datetime.datetime() to a D-Bus type."""
753
return dbus.String("", variant_level = variant_level)
754
return dbus.String(dt.isoformat(),
755
variant_level=variant_level)
757
class AlternateDBusNamesMetaclass(DBusObjectWithProperties.__metaclass__):
758
"""Applied to an empty subclass of a D-Bus object, this metaclass
759
will add additional D-Bus attributes matching a certain pattern.
761
def __new__(mcs, name, bases, attr):
762
# Go through all the base classes which could have D-Bus
763
# methods, signals, or properties in them
764
for base in (b for b in bases
765
if issubclass(b, dbus.service.Object)):
766
# Go though all attributes of the base class
767
for attrname, attribute in inspect.getmembers(base):
768
# Ignore non-D-Bus attributes, and D-Bus attributes
769
# with the wrong interface name
770
if (not hasattr(attribute, "_dbus_interface")
771
or not attribute._dbus_interface
772
.startswith("se.recompile.Mandos")):
774
# Create an alternate D-Bus interface name based on
776
alt_interface = (attribute._dbus_interface
777
.replace("se.recompile.Mandos",
778
"se.bsnet.fukt.Mandos"))
779
# Is this a D-Bus signal?
780
if getattr(attribute, "_dbus_is_signal", False):
781
# Extract the original non-method function by
783
nonmethod_func = (dict(
784
zip(attribute.func_code.co_freevars,
785
attribute.__closure__))["func"]
787
# Create a new, but exactly alike, function
788
# object, and decorate it to be a new D-Bus signal
789
# with the alternate D-Bus interface name
790
new_function = (dbus.service.signal
792
attribute._dbus_signature)
794
nonmethod_func.func_code,
795
nonmethod_func.func_globals,
796
nonmethod_func.func_name,
797
nonmethod_func.func_defaults,
798
nonmethod_func.func_closure)))
799
# Define a creator of a function to call both the
800
# old and new functions, so both the old and new
801
# signals gets sent when the function is called
802
def fixscope(func1, func2):
803
"""This function is a scope container to pass
804
func1 and func2 to the "call_both" function
805
outside of its arguments"""
806
def call_both(*args, **kwargs):
807
"""This function will emit two D-Bus
808
signals by calling func1 and func2"""
809
func1(*args, **kwargs)
810
func2(*args, **kwargs)
812
# Create the "call_both" function and add it to
814
attr[attrname] = fixscope(attribute,
816
# Is this a D-Bus method?
817
elif getattr(attribute, "_dbus_is_method", False):
818
# Create a new, but exactly alike, function
819
# object. Decorate it to be a new D-Bus method
820
# with the alternate D-Bus interface name. Add it
822
attr[attrname] = (dbus.service.method
824
attribute._dbus_in_signature,
825
attribute._dbus_out_signature)
827
(attribute.func_code,
828
attribute.func_globals,
830
attribute.func_defaults,
831
attribute.func_closure)))
832
# Is this a D-Bus property?
833
elif getattr(attribute, "_dbus_is_property", False):
834
# Create a new, but exactly alike, function
835
# object, and decorate it to be a new D-Bus
836
# property with the alternate D-Bus interface
837
# name. Add it to the class.
838
attr[attrname] = (dbus_service_property
840
attribute._dbus_signature,
841
attribute._dbus_access,
843
._dbus_get_args_options
846
(attribute.func_code,
847
attribute.func_globals,
849
attribute.func_defaults,
850
attribute.func_closure)))
851
return type.__new__(mcs, name, bases, attr)
853
class ClientDBus(Client, DBusObjectWithProperties):
854
"""A Client class using D-Bus
857
dbus_object_path: dbus.ObjectPath
858
bus: dbus.SystemBus()
861
runtime_expansions = (Client.runtime_expansions
862
+ ("dbus_object_path",))
864
# dbus.service.Object doesn't use super(), so we can't either.
866
def __init__(self, bus = None, *args, **kwargs):
867
self._approvals_pending = 0
869
Client.__init__(self, *args, **kwargs)
870
# Only now, when this client is initialized, can it show up on
872
client_object_name = unicode(self.name).translate(
875
self.dbus_object_path = (dbus.ObjectPath
876
("/clients/" + client_object_name))
877
DBusObjectWithProperties.__init__(self, self.bus,
878
self.dbus_object_path)
880
def notifychangeproperty(transform_func,
881
dbus_name, type_func=lambda x: x,
883
""" Modify a variable so that it's a property which announces
886
transform_fun: Function that takes a value and transforms it
888
dbus_name: D-Bus name of the variable
889
type_func: Function that transform the value before sending it
890
to the D-Bus. Default: no transform
891
variant_level: D-Bus variant level. Default: 1
894
def setter(self, value):
895
old_value = real_value[0]
896
real_value[0] = value
897
if hasattr(self, "dbus_object_path"):
898
if type_func(old_value) != type_func(real_value[0]):
899
dbus_value = transform_func(type_func(real_value[0]),
901
self.PropertyChanged(dbus.String(dbus_name),
904
return property(lambda self: real_value[0], setter)
907
expires = notifychangeproperty(datetime_to_dbus, "Expires")
908
approvals_pending = notifychangeproperty(dbus.Boolean,
911
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
912
last_enabled = notifychangeproperty(datetime_to_dbus,
914
checker = notifychangeproperty(dbus.Boolean, "CheckerRunning",
915
type_func = lambda checker: checker is not None)
916
last_checked_ok = notifychangeproperty(datetime_to_dbus,
918
last_approval_request = notifychangeproperty(datetime_to_dbus,
919
"LastApprovalRequest")
920
approved_by_default = notifychangeproperty(dbus.Boolean,
922
approval_delay = notifychangeproperty(dbus.UInt16, "ApprovalDelay",
923
type_func = _timedelta_to_milliseconds)
924
approval_duration = notifychangeproperty(dbus.UInt16, "ApprovalDuration",
925
type_func = _timedelta_to_milliseconds)
926
host = notifychangeproperty(dbus.String, "Host")
927
timeout = notifychangeproperty(dbus.UInt16, "Timeout",
928
type_func = _timedelta_to_milliseconds)
929
extended_timeout = notifychangeproperty(dbus.UInt16, "ExtendedTimeout",
930
type_func = _timedelta_to_milliseconds)
931
interval = notifychangeproperty(dbus.UInt16, "Interval",
932
type_func = _timedelta_to_milliseconds)
933
checker_command = notifychangeproperty(dbus.String, "Checker")
935
del notifychangeproperty
937
def __del__(self, *args, **kwargs):
939
self.remove_from_connection()
942
if hasattr(DBusObjectWithProperties, "__del__"):
943
DBusObjectWithProperties.__del__(self, *args, **kwargs)
944
Client.__del__(self, *args, **kwargs)
946
def checker_callback(self, pid, condition, command,
948
self.checker_callback_tag = None
950
if os.WIFEXITED(condition):
951
exitstatus = os.WEXITSTATUS(condition)
953
self.CheckerCompleted(dbus.Int16(exitstatus),
954
dbus.Int64(condition),
955
dbus.String(command))
958
self.CheckerCompleted(dbus.Int16(-1),
959
dbus.Int64(condition),
960
dbus.String(command))
962
return Client.checker_callback(self, pid, condition, command,
965
def start_checker(self, *args, **kwargs):
966
old_checker = self.checker
967
if self.checker is not None:
968
old_checker_pid = self.checker.pid
970
old_checker_pid = None
971
r = Client.start_checker(self, *args, **kwargs)
972
# Only if new checker process was started
973
if (self.checker is not None
974
and old_checker_pid != self.checker.pid):
976
self.CheckerStarted(self.current_checker_command)
979
def _reset_approved(self):
980
self._approved = None
983
def approve(self, value=True):
984
self.send_changedstate()
985
self._approved = value
986
gobject.timeout_add(_timedelta_to_milliseconds
987
(self.approval_duration),
988
self._reset_approved)
991
## D-Bus methods, signals & properties
992
_interface = "se.recompile.Mandos.Client"
996
# CheckerCompleted - signal
997
@dbus.service.signal(_interface, signature="nxs")
998
def CheckerCompleted(self, exitcode, waitstatus, command):
1002
# CheckerStarted - signal
1003
@dbus.service.signal(_interface, signature="s")
1004
def CheckerStarted(self, command):
1008
# PropertyChanged - signal
1009
@dbus.service.signal(_interface, signature="sv")
1010
def PropertyChanged(self, property, value):
1014
# GotSecret - signal
1015
@dbus.service.signal(_interface)
1016
def GotSecret(self):
1018
Is sent after a successful transfer of secret from the Mandos
1019
server to mandos-client
1024
@dbus.service.signal(_interface, signature="s")
1025
def Rejected(self, reason):
1029
# NeedApproval - signal
1030
@dbus.service.signal(_interface, signature="tb")
1031
def NeedApproval(self, timeout, default):
1033
return self.need_approval()
1038
@dbus.service.method(_interface, in_signature="b")
1039
def Approve(self, value):
1042
# CheckedOK - method
1043
@dbus.service.method(_interface)
1044
def CheckedOK(self):
1048
@dbus.service.method(_interface)
1053
# StartChecker - method
1054
@dbus.service.method(_interface)
1055
def StartChecker(self):
1057
self.start_checker()
1060
@dbus.service.method(_interface)
1065
# StopChecker - method
1066
@dbus.service.method(_interface)
1067
def StopChecker(self):
1072
# ApprovalPending - property
1073
@dbus_service_property(_interface, signature="b", access="read")
1074
def ApprovalPending_dbus_property(self):
1075
return dbus.Boolean(bool(self.approvals_pending))
1077
# ApprovedByDefault - property
1078
@dbus_service_property(_interface, signature="b",
1080
def ApprovedByDefault_dbus_property(self, value=None):
1081
if value is None: # get
1082
return dbus.Boolean(self.approved_by_default)
1083
self.approved_by_default = bool(value)
1085
# ApprovalDelay - property
1086
@dbus_service_property(_interface, signature="t",
1088
def ApprovalDelay_dbus_property(self, value=None):
1089
if value is None: # get
1090
return dbus.UInt64(self.approval_delay_milliseconds())
1091
self.approval_delay = datetime.timedelta(0, 0, 0, value)
1093
# ApprovalDuration - property
1094
@dbus_service_property(_interface, signature="t",
1096
def ApprovalDuration_dbus_property(self, value=None):
1097
if value is None: # get
1098
return dbus.UInt64(_timedelta_to_milliseconds(
1099
self.approval_duration))
1100
self.approval_duration = datetime.timedelta(0, 0, 0, value)
1103
@dbus_service_property(_interface, signature="s", access="read")
1104
def Name_dbus_property(self):
1105
return dbus.String(self.name)
1107
# Fingerprint - property
1108
@dbus_service_property(_interface, signature="s", access="read")
1109
def Fingerprint_dbus_property(self):
1110
return dbus.String(self.fingerprint)
1113
@dbus_service_property(_interface, signature="s",
1115
def Host_dbus_property(self, value=None):
1116
if value is None: # get
1117
return dbus.String(self.host)
1120
# Created - property
1121
@dbus_service_property(_interface, signature="s", access="read")
1122
def Created_dbus_property(self):
1123
return dbus.String(datetime_to_dbus(self.created))
1125
# LastEnabled - property
1126
@dbus_service_property(_interface, signature="s", access="read")
1127
def LastEnabled_dbus_property(self):
1128
return datetime_to_dbus(self.last_enabled)
1130
# Enabled - property
1131
@dbus_service_property(_interface, signature="b",
1133
def Enabled_dbus_property(self, value=None):
1134
if value is None: # get
1135
return dbus.Boolean(self.enabled)
1141
# LastCheckedOK - property
1142
@dbus_service_property(_interface, signature="s",
1144
def LastCheckedOK_dbus_property(self, value=None):
1145
if value is not None:
1148
return datetime_to_dbus(self.last_checked_ok)
1150
# Expires - property
1151
@dbus_service_property(_interface, signature="s", access="read")
1152
def Expires_dbus_property(self):
1153
return datetime_to_dbus(self.expires)
1155
# LastApprovalRequest - property
1156
@dbus_service_property(_interface, signature="s", access="read")
1157
def LastApprovalRequest_dbus_property(self):
1158
return datetime_to_dbus(self.last_approval_request)
1160
# Timeout - property
1161
@dbus_service_property(_interface, signature="t",
1163
def Timeout_dbus_property(self, value=None):
1164
if value is None: # get
1165
return dbus.UInt64(self.timeout_milliseconds())
1166
self.timeout = datetime.timedelta(0, 0, 0, value)
1167
if getattr(self, "disable_initiator_tag", None) is None:
1169
# Reschedule timeout
1170
gobject.source_remove(self.disable_initiator_tag)
1171
self.disable_initiator_tag = None
1173
time_to_die = (self.
1174
_timedelta_to_milliseconds((self
1179
if time_to_die <= 0:
1180
# The timeout has passed
1183
self.expires = (datetime.datetime.utcnow()
1184
+ datetime.timedelta(milliseconds = time_to_die))
1185
self.disable_initiator_tag = (gobject.timeout_add
1186
(time_to_die, self.disable))
1188
# ExtendedTimeout - property
1189
@dbus_service_property(_interface, signature="t",
1191
def ExtendedTimeout_dbus_property(self, value=None):
1192
if value is None: # get
1193
return dbus.UInt64(self.extended_timeout_milliseconds())
1194
self.extended_timeout = datetime.timedelta(0, 0, 0, value)
1196
# Interval - property
1197
@dbus_service_property(_interface, signature="t",
1199
def Interval_dbus_property(self, value=None):
1200
if value is None: # get
1201
return dbus.UInt64(self.interval_milliseconds())
1202
self.interval = datetime.timedelta(0, 0, 0, value)
1203
if getattr(self, "checker_initiator_tag", None) is None:
1205
# Reschedule checker run
1206
gobject.source_remove(self.checker_initiator_tag)
1207
self.checker_initiator_tag = (gobject.timeout_add
1208
(value, self.start_checker))
1209
self.start_checker() # Start one now, too
1211
# Checker - property
1212
@dbus_service_property(_interface, signature="s",
1214
def Checker_dbus_property(self, value=None):
1215
if value is None: # get
1216
return dbus.String(self.checker_command)
1217
self.checker_command = value
1219
# CheckerRunning - property
1220
@dbus_service_property(_interface, signature="b",
1222
def CheckerRunning_dbus_property(self, value=None):
1223
if value is None: # get
1224
return dbus.Boolean(self.checker is not None)
1226
self.start_checker()
1230
# ObjectPath - property
1231
@dbus_service_property(_interface, signature="o", access="read")
1232
def ObjectPath_dbus_property(self):
1233
return self.dbus_object_path # is already a dbus.ObjectPath
1236
@dbus_service_property(_interface, signature="ay",
1237
access="write", byte_arrays=True)
1238
def Secret_dbus_property(self, value):
1239
self.secret = str(value)
1244
class ProxyClient(object):
1245
def __init__(self, child_pipe, fpr, address):
1246
self._pipe = child_pipe
1247
self._pipe.send(('init', fpr, address))
1248
if not self._pipe.recv():
1251
def __getattribute__(self, name):
1252
if(name == '_pipe'):
1253
return super(ProxyClient, self).__getattribute__(name)
1254
self._pipe.send(('getattr', name))
1255
data = self._pipe.recv()
1256
if data[0] == 'data':
1258
if data[0] == 'function':
1259
def func(*args, **kwargs):
1260
self._pipe.send(('funcall', name, args, kwargs))
1261
return self._pipe.recv()[1]
1264
def __setattr__(self, name, value):
1265
if(name == '_pipe'):
1266
return super(ProxyClient, self).__setattr__(name, value)
1267
self._pipe.send(('setattr', name, value))
1269
class ClientDBusTransitional(ClientDBus):
1270
__metaclass__ = AlternateDBusNamesMetaclass
1272
class ClientHandler(socketserver.BaseRequestHandler, object):
1273
"""A class to handle client connections.
1275
Instantiated once for each connection to handle it.
1276
Note: This will run in its own forked process."""
1279
with contextlib.closing(self.server.child_pipe) as child_pipe:
1280
logger.info("TCP connection from: %s",
1281
unicode(self.client_address))
1282
logger.debug("Pipe FD: %d",
1283
self.server.child_pipe.fileno())
1285
session = (gnutls.connection
1286
.ClientSession(self.request,
1288
.X509Credentials()))
1290
# Note: gnutls.connection.X509Credentials is really a
1291
# generic GnuTLS certificate credentials object so long as
1292
# no X.509 keys are added to it. Therefore, we can use it
1293
# here despite using OpenPGP certificates.
1295
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
1296
# "+AES-256-CBC", "+SHA1",
1297
# "+COMP-NULL", "+CTYPE-OPENPGP",
1299
# Use a fallback default, since this MUST be set.
1300
priority = self.server.gnutls_priority
1301
if priority is None:
1303
(gnutls.library.functions
1304
.gnutls_priority_set_direct(session._c_object,
1307
# Start communication using the Mandos protocol
1308
# Get protocol number
1309
line = self.request.makefile().readline()
1310
logger.debug("Protocol version: %r", line)
1312
if int(line.strip().split()[0]) > 1:
1314
except (ValueError, IndexError, RuntimeError) as error:
1315
logger.error("Unknown protocol version: %s", error)
1318
# Start GnuTLS connection
1321
except gnutls.errors.GNUTLSError as error:
1322
logger.warning("Handshake failed: %s", error)
1323
# Do not run session.bye() here: the session is not
1324
# established. Just abandon the request.
1326
logger.debug("Handshake succeeded")
1328
approval_required = False
1331
fpr = self.fingerprint(self.peer_certificate
1334
gnutls.errors.GNUTLSError) as error:
1335
logger.warning("Bad certificate: %s", error)
1337
logger.debug("Fingerprint: %s", fpr)
1340
client = ProxyClient(child_pipe, fpr,
1341
self.client_address)
1345
if client.approval_delay:
1346
delay = client.approval_delay
1347
client.approvals_pending += 1
1348
approval_required = True
1351
if not client.enabled:
1352
logger.info("Client %s is disabled",
1354
if self.server.use_dbus:
1356
client.Rejected("Disabled")
1359
if client._approved or not client.approval_delay:
1360
#We are approved or approval is disabled
1362
elif client._approved is None:
1363
logger.info("Client %s needs approval",
1365
if self.server.use_dbus:
1367
client.NeedApproval(
1368
client.approval_delay_milliseconds(),
1369
client.approved_by_default)
1371
logger.warning("Client %s was not approved",
1373
if self.server.use_dbus:
1375
client.Rejected("Denied")
1378
#wait until timeout or approved
1379
#x = float(client._timedelta_to_milliseconds(delay))
1380
time = datetime.datetime.now()
1381
client.changedstate.acquire()
1382
client.changedstate.wait(float(client._timedelta_to_milliseconds(delay) / 1000))
1383
client.changedstate.release()
1384
time2 = datetime.datetime.now()
1385
if (time2 - time) >= delay:
1386
if not client.approved_by_default:
1387
logger.warning("Client %s timed out while"
1388
" waiting for approval",
1390
if self.server.use_dbus:
1392
client.Rejected("Approval timed out")
1397
delay -= time2 - time
1400
while sent_size < len(client.secret):
1402
sent = session.send(client.secret[sent_size:])
1403
except gnutls.errors.GNUTLSError as error:
1404
logger.warning("gnutls send failed")
1406
logger.debug("Sent: %d, remaining: %d",
1407
sent, len(client.secret)
1408
- (sent_size + sent))
1411
logger.info("Sending secret to %s", client.name)
1412
# bump the timeout as if seen
1413
client.checked_ok(client.extended_timeout)
1414
if self.server.use_dbus:
1419
if approval_required:
1420
client.approvals_pending -= 1
1423
except gnutls.errors.GNUTLSError as error:
1424
logger.warning("GnuTLS bye failed")
1427
def peer_certificate(session):
1428
"Return the peer's OpenPGP certificate as a bytestring"
1429
# If not an OpenPGP certificate...
1430
if (gnutls.library.functions
1431
.gnutls_certificate_type_get(session._c_object)
1432
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1433
# ...do the normal thing
1434
return session.peer_certificate
1435
list_size = ctypes.c_uint(1)
1436
cert_list = (gnutls.library.functions
1437
.gnutls_certificate_get_peers
1438
(session._c_object, ctypes.byref(list_size)))
1439
if not bool(cert_list) and list_size.value != 0:
1440
raise gnutls.errors.GNUTLSError("error getting peer"
1442
if list_size.value == 0:
1445
return ctypes.string_at(cert.data, cert.size)
1448
def fingerprint(openpgp):
1449
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1450
# New GnuTLS "datum" with the OpenPGP public key
1451
datum = (gnutls.library.types
1452
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1455
ctypes.c_uint(len(openpgp))))
1456
# New empty GnuTLS certificate
1457
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1458
(gnutls.library.functions
1459
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1460
# Import the OpenPGP public key into the certificate
1461
(gnutls.library.functions
1462
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1463
gnutls.library.constants
1464
.GNUTLS_OPENPGP_FMT_RAW))
1465
# Verify the self signature in the key
1466
crtverify = ctypes.c_uint()
1467
(gnutls.library.functions
1468
.gnutls_openpgp_crt_verify_self(crt, 0,
1469
ctypes.byref(crtverify)))
1470
if crtverify.value != 0:
1471
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1472
raise (gnutls.errors.CertificateSecurityError
1474
# New buffer for the fingerprint
1475
buf = ctypes.create_string_buffer(20)
1476
buf_len = ctypes.c_size_t()
1477
# Get the fingerprint from the certificate into the buffer
1478
(gnutls.library.functions
1479
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1480
ctypes.byref(buf_len)))
1481
# Deinit the certificate
1482
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1483
# Convert the buffer to a Python bytestring
1484
fpr = ctypes.string_at(buf, buf_len.value)
1485
# Convert the bytestring to hexadecimal notation
1486
hex_fpr = ''.join("%02X" % ord(char) for char in fpr)
1490
class MultiprocessingMixIn(object):
1491
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
1492
def sub_process_main(self, request, address):
1494
self.finish_request(request, address)
1496
self.handle_error(request, address)
1497
self.close_request(request)
1499
def process_request(self, request, address):
1500
"""Start a new process to process the request."""
1501
multiprocessing.Process(target = self.sub_process_main,
1502
args = (request, address)).start()
1505
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
1506
""" adds a pipe to the MixIn """
1507
def process_request(self, request, client_address):
1508
"""Overrides and wraps the original process_request().
1510
This function creates a new pipe in self.pipe
1512
parent_pipe, self.child_pipe = multiprocessing.Pipe()
1514
super(MultiprocessingMixInWithPipe,
1515
self).process_request(request, client_address)
1516
self.child_pipe.close()
1517
self.add_pipe(parent_pipe)
1519
def add_pipe(self, parent_pipe):
1520
"""Dummy function; override as necessary"""
1521
raise NotImplementedError
1524
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1525
socketserver.TCPServer, object):
1526
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
1529
enabled: Boolean; whether this server is activated yet
1530
interface: None or a network interface name (string)
1531
use_ipv6: Boolean; to use IPv6 or not
1533
def __init__(self, server_address, RequestHandlerClass,
1534
interface=None, use_ipv6=True):
1535
self.interface = interface
1537
self.address_family = socket.AF_INET6
1538
socketserver.TCPServer.__init__(self, server_address,
1539
RequestHandlerClass)
1540
def server_bind(self):
1541
"""This overrides the normal server_bind() function
1542
to bind to an interface if one was specified, and also NOT to
1543
bind to an address or port if they were not specified."""
1544
if self.interface is not None:
1545
if SO_BINDTODEVICE is None:
1546
logger.error("SO_BINDTODEVICE does not exist;"
1547
" cannot bind to interface %s",
1551
self.socket.setsockopt(socket.SOL_SOCKET,
1555
except socket.error as error:
1556
if error[0] == errno.EPERM:
1557
logger.error("No permission to"
1558
" bind to interface %s",
1560
elif error[0] == errno.ENOPROTOOPT:
1561
logger.error("SO_BINDTODEVICE not available;"
1562
" cannot bind to interface %s",
1566
# Only bind(2) the socket if we really need to.
1567
if self.server_address[0] or self.server_address[1]:
1568
if not self.server_address[0]:
1569
if self.address_family == socket.AF_INET6:
1570
any_address = "::" # in6addr_any
1572
any_address = socket.INADDR_ANY
1573
self.server_address = (any_address,
1574
self.server_address[1])
1575
elif not self.server_address[1]:
1576
self.server_address = (self.server_address[0],
1578
# if self.interface:
1579
# self.server_address = (self.server_address[0],
1584
return socketserver.TCPServer.server_bind(self)
1587
class MandosServer(IPv6_TCPServer):
1591
clients: set of Client objects
1592
gnutls_priority GnuTLS priority string
1593
use_dbus: Boolean; to emit D-Bus signals or not
1595
Assumes a gobject.MainLoop event loop.
1597
def __init__(self, server_address, RequestHandlerClass,
1598
interface=None, use_ipv6=True, clients=None,
1599
gnutls_priority=None, use_dbus=True):
1600
self.enabled = False
1601
self.clients = clients
1602
if self.clients is None:
1603
self.clients = set()
1604
self.use_dbus = use_dbus
1605
self.gnutls_priority = gnutls_priority
1606
IPv6_TCPServer.__init__(self, server_address,
1607
RequestHandlerClass,
1608
interface = interface,
1609
use_ipv6 = use_ipv6)
1610
def server_activate(self):
1612
return socketserver.TCPServer.server_activate(self)
1615
def add_pipe(self, parent_pipe):
1616
# Call "handle_ipc" for both data and EOF events
1617
gobject.io_add_watch(parent_pipe.fileno(),
1618
gobject.IO_IN | gobject.IO_HUP,
1619
functools.partial(self.handle_ipc,
1620
parent_pipe = parent_pipe))
1622
def handle_ipc(self, source, condition, parent_pipe=None,
1623
client_object=None):
1625
gobject.IO_IN: "IN", # There is data to read.
1626
gobject.IO_OUT: "OUT", # Data can be written (without
1628
gobject.IO_PRI: "PRI", # There is urgent data to read.
1629
gobject.IO_ERR: "ERR", # Error condition.
1630
gobject.IO_HUP: "HUP" # Hung up (the connection has been
1631
# broken, usually for pipes and
1634
conditions_string = ' | '.join(name
1636
condition_names.iteritems()
1637
if cond & condition)
1638
# error or the other end of multiprocessing.Pipe has closed
1639
if condition & (gobject.IO_ERR | condition & gobject.IO_HUP):
1642
# Read a request from the child
1643
request = parent_pipe.recv()
1644
command = request[0]
1646
if command == 'init':
1648
address = request[2]
1650
for c in self.clients:
1651
if c.fingerprint == fpr:
1655
logger.info("Client not found for fingerprint: %s, ad"
1656
"dress: %s", fpr, address)
1659
mandos_dbus_service.ClientNotFound(fpr, address[0])
1660
parent_pipe.send(False)
1663
gobject.io_add_watch(parent_pipe.fileno(),
1664
gobject.IO_IN | gobject.IO_HUP,
1665
functools.partial(self.handle_ipc,
1666
parent_pipe = parent_pipe,
1667
client_object = client))
1668
parent_pipe.send(True)
1669
# remove the old hook in favor of the new above hook on same fileno
1671
if command == 'funcall':
1672
funcname = request[1]
1676
parent_pipe.send(('data', getattr(client_object, funcname)(*args, **kwargs)))
1678
if command == 'getattr':
1679
attrname = request[1]
1680
if callable(client_object.__getattribute__(attrname)):
1681
parent_pipe.send(('function',))
1683
parent_pipe.send(('data', client_object.__getattribute__(attrname)))
1685
if command == 'setattr':
1686
attrname = request[1]
1688
setattr(client_object, attrname, value)
1693
def string_to_delta(interval):
1694
"""Parse a string and return a datetime.timedelta
1696
>>> string_to_delta('7d')
1697
datetime.timedelta(7)
1698
>>> string_to_delta('60s')
1699
datetime.timedelta(0, 60)
1700
>>> string_to_delta('60m')
1701
datetime.timedelta(0, 3600)
1702
>>> string_to_delta('24h')
1703
datetime.timedelta(1)
1704
>>> string_to_delta('1w')
1705
datetime.timedelta(7)
1706
>>> string_to_delta('5m 30s')
1707
datetime.timedelta(0, 330)
1709
timevalue = datetime.timedelta(0)
1710
for s in interval.split():
1712
suffix = unicode(s[-1])
1715
delta = datetime.timedelta(value)
1717
delta = datetime.timedelta(0, value)
1719
delta = datetime.timedelta(0, 0, 0, 0, value)
1721
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
1723
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1725
raise ValueError("Unknown suffix %r" % suffix)
1726
except (ValueError, IndexError) as e:
1727
raise ValueError(*(e.args))
1732
def if_nametoindex(interface):
1733
"""Call the C function if_nametoindex(), or equivalent
1735
Note: This function cannot accept a unicode string."""
1736
global if_nametoindex
1738
if_nametoindex = (ctypes.cdll.LoadLibrary
1739
(ctypes.util.find_library("c"))
1741
except (OSError, AttributeError):
1742
logger.warning("Doing if_nametoindex the hard way")
1743
def if_nametoindex(interface):
1744
"Get an interface index the hard way, i.e. using fcntl()"
1745
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
1746
with contextlib.closing(socket.socket()) as s:
1747
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1748
struct.pack(str("16s16x"),
1750
interface_index = struct.unpack(str("I"),
1752
return interface_index
1753
return if_nametoindex(interface)
1756
def daemon(nochdir = False, noclose = False):
1757
"""See daemon(3). Standard BSD Unix function.
1759
This should really exist as os.daemon, but it doesn't (yet)."""
1768
# Close all standard open file descriptors
1769
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1770
if not stat.S_ISCHR(os.fstat(null).st_mode):
1771
raise OSError(errno.ENODEV,
1772
"%s not a character device"
1774
os.dup2(null, sys.stdin.fileno())
1775
os.dup2(null, sys.stdout.fileno())
1776
os.dup2(null, sys.stderr.fileno())
53
return super(type(self), self).server_bind()
56
def init_with_options(self, *args, **kwargs):
57
if "options" in kwargs:
58
self.options = kwargs["options"]
60
if "clients" in kwargs:
61
self.clients = kwargs["clients"]
63
if "credentials" in kwargs:
64
self.credentials = kwargs["credentials"]
65
del kwargs["credentials"]
66
return super(type(self), self).__init__(*args, **kwargs)
69
class udp_handler(SocketServer.DatagramRequestHandler, object):
71
self.wfile.write("Polo")
72
print "UDP request answered"
75
class IPv6_UDPServer(SocketServer.UDPServer, object):
76
__init__ = init_with_options
77
address_family = socket.AF_INET6
78
allow_reuse_address = True
79
server_bind = server_bind
80
def verify_request(self, request, client_address):
81
print "UDP request came"
82
return request[0] == "Marco"
85
class tcp_handler(SocketServer.BaseRequestHandler, object):
87
print "TCP request came"
88
print "Request:", self.request
89
print "Client Address:", self.client_address
90
print "Server:", self.server
91
session = gnutls.connection.ServerSession(self.request,
92
self.server.credentials)
94
if session.peer_certificate:
95
print "DN:", session.peer_certificate.subject
98
except gnutls.errors.CertificateError, error:
99
print "Verify failed", error
103
session.send(dict((client.dn, client.password)
104
for client in self.server.clients)
105
[session.peer_certificate.subject])
107
session.send("gazonk")
111
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
112
__init__ = init_with_options
113
address_family = socket.AF_INET6
114
allow_reuse_address = True
115
request_queue_size = 1024
116
server_bind = server_bind
1783
##################################################################
1784
# Parsing of options, both command line and config file
1786
parser = argparse.ArgumentParser()
1787
parser.add_argument("-v", "--version", action="version",
1788
version = "%%(prog)s %s" % version,
1789
help="show version number and exit")
1790
parser.add_argument("-i", "--interface", metavar="IF",
1791
help="Bind to interface IF")
1792
parser.add_argument("-a", "--address",
1793
help="Address to listen for requests on")
1794
parser.add_argument("-p", "--port", type=int,
1795
help="Port number to receive requests on")
1796
parser.add_argument("--check", action="store_true",
1797
help="Run self-test")
1798
parser.add_argument("--debug", action="store_true",
1799
help="Debug mode; run in foreground and log"
1801
parser.add_argument("--debuglevel", metavar="LEVEL",
1802
help="Debug level for stdout output")
1803
parser.add_argument("--priority", help="GnuTLS"
1804
" priority string (see GnuTLS documentation)")
1805
parser.add_argument("--servicename",
1806
metavar="NAME", help="Zeroconf service name")
1807
parser.add_argument("--configdir",
1808
default="/etc/mandos", metavar="DIR",
1809
help="Directory to search for configuration"
1811
parser.add_argument("--no-dbus", action="store_false",
1812
dest="use_dbus", help="Do not provide D-Bus"
1813
" system bus interface")
1814
parser.add_argument("--no-ipv6", action="store_false",
1815
dest="use_ipv6", help="Do not use IPv6")
1816
options = parser.parse_args()
1823
# Default values for config file for server-global settings
1824
server_defaults = { "interface": "",
1829
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1830
"servicename": "Mandos",
1836
# Parse config file for server-global settings
1837
server_config = configparser.SafeConfigParser(server_defaults)
1839
server_config.read(os.path.join(options.configdir,
1841
# Convert the SafeConfigParser object to a dict
1842
server_settings = server_config.defaults()
1843
# Use the appropriate methods on the non-string config options
1844
for option in ("debug", "use_dbus", "use_ipv6"):
1845
server_settings[option] = server_config.getboolean("DEFAULT",
1847
if server_settings["port"]:
1848
server_settings["port"] = server_config.getint("DEFAULT",
1852
# Override the settings from the config file with command line
1854
for option in ("interface", "address", "port", "debug",
1855
"priority", "servicename", "configdir",
1856
"use_dbus", "use_ipv6", "debuglevel"):
1857
value = getattr(options, option)
1858
if value is not None:
1859
server_settings[option] = value
1861
# Force all strings to be unicode
1862
for option in server_settings.keys():
1863
if type(server_settings[option]) is str:
1864
server_settings[option] = unicode(server_settings[option])
1865
# Now we have our good server settings in "server_settings"
1867
##################################################################
1870
debug = server_settings["debug"]
1871
debuglevel = server_settings["debuglevel"]
1872
use_dbus = server_settings["use_dbus"]
1873
use_ipv6 = server_settings["use_ipv6"]
1875
if server_settings["servicename"] != "Mandos":
1876
syslogger.setFormatter(logging.Formatter
1877
('Mandos (%s) [%%(process)d]:'
1878
' %%(levelname)s: %%(message)s'
1879
% server_settings["servicename"]))
1881
# Parse config file with clients
1882
client_defaults = { "timeout": "5m",
1883
"extended_timeout": "15m",
1885
"checker": "fping -q -- %%(host)s",
1887
"approval_delay": "0s",
1888
"approval_duration": "1s",
1890
client_config = configparser.SafeConfigParser(client_defaults)
1891
client_config.read(os.path.join(server_settings["configdir"],
1894
global mandos_dbus_service
1895
mandos_dbus_service = None
1897
tcp_server = MandosServer((server_settings["address"],
1898
server_settings["port"]),
1900
interface=(server_settings["interface"]
1904
server_settings["priority"],
1907
pidfilename = "/var/run/mandos.pid"
1909
pidfile = open(pidfilename, "w")
1911
logger.error("Could not open file %r", pidfilename)
1914
uid = pwd.getpwnam("_mandos").pw_uid
1915
gid = pwd.getpwnam("_mandos").pw_gid
1918
uid = pwd.getpwnam("mandos").pw_uid
1919
gid = pwd.getpwnam("mandos").pw_gid
1922
uid = pwd.getpwnam("nobody").pw_uid
1923
gid = pwd.getpwnam("nobody").pw_gid
1930
except OSError as error:
1931
if error[0] != errno.EPERM:
1934
if not debug and not debuglevel:
1935
syslogger.setLevel(logging.WARNING)
1936
console.setLevel(logging.WARNING)
1938
level = getattr(logging, debuglevel.upper())
1939
syslogger.setLevel(level)
1940
console.setLevel(level)
1943
# Enable all possible GnuTLS debugging
1945
# "Use a log level over 10 to enable all debugging options."
1947
gnutls.library.functions.gnutls_global_set_log_level(11)
1949
@gnutls.library.types.gnutls_log_func
1950
def debug_gnutls(level, string):
1951
logger.debug("GnuTLS: %s", string[:-1])
1953
(gnutls.library.functions
1954
.gnutls_global_set_log_function(debug_gnutls))
1956
# Redirect stdin so all checkers get /dev/null
1957
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1958
os.dup2(null, sys.stdin.fileno())
1962
# No console logging
1963
logger.removeHandler(console)
1965
# Need to fork before connecting to D-Bus
1967
# Close all input and output, do double fork, etc.
1971
# From the Avahi example code
1972
DBusGMainLoop(set_as_default=True )
1973
main_loop = gobject.MainLoop()
1974
bus = dbus.SystemBus()
1975
# End of Avahi example code
1978
bus_name = dbus.service.BusName("se.recompile.Mandos",
1979
bus, do_not_queue=True)
1980
old_bus_name = dbus.service.BusName("se.bsnet.fukt.Mandos",
1981
bus, do_not_queue=True)
1982
except dbus.exceptions.NameExistsException as e:
1983
logger.error(unicode(e) + ", disabling D-Bus")
1985
server_settings["use_dbus"] = False
1986
tcp_server.use_dbus = False
1987
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1988
service = AvahiService(name = server_settings["servicename"],
1989
servicetype = "_mandos._tcp",
1990
protocol = protocol, bus = bus)
1991
if server_settings["interface"]:
1992
service.interface = (if_nametoindex
1993
(str(server_settings["interface"])))
1995
global multiprocessing_manager
1996
multiprocessing_manager = multiprocessing.Manager()
1998
client_class = Client
2000
client_class = functools.partial(ClientDBusTransitional, bus = bus)
2001
def client_config_items(config, section):
2002
special_settings = {
2003
"approved_by_default":
2004
lambda: config.getboolean(section,
2005
"approved_by_default"),
2007
for name, value in config.items(section):
2009
yield (name, special_settings[name]())
2013
tcp_server.clients.update(set(
2014
client_class(name = section,
2015
config= dict(client_config_items(
2016
client_config, section)))
2017
for section in client_config.sections()))
2018
if not tcp_server.clients:
2019
logger.warning("No clients defined")
2025
pidfile.write(str(pid) + "\n".encode("utf-8"))
2028
logger.error("Could not write to file %r with PID %d",
2031
# "pidfile" was never created
2035
signal.signal(signal.SIGINT, signal.SIG_IGN)
2037
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2038
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2041
class MandosDBusService(dbus.service.Object):
2042
"""A D-Bus proxy object"""
2044
dbus.service.Object.__init__(self, bus, "/")
2045
_interface = "se.recompile.Mandos"
2047
@dbus.service.signal(_interface, signature="o")
2048
def ClientAdded(self, objpath):
2052
@dbus.service.signal(_interface, signature="ss")
2053
def ClientNotFound(self, fingerprint, address):
2057
@dbus.service.signal(_interface, signature="os")
2058
def ClientRemoved(self, objpath, name):
2062
@dbus.service.method(_interface, out_signature="ao")
2063
def GetAllClients(self):
2065
return dbus.Array(c.dbus_object_path
2066
for c in tcp_server.clients)
2068
@dbus.service.method(_interface,
2069
out_signature="a{oa{sv}}")
2070
def GetAllClientsWithProperties(self):
2072
return dbus.Dictionary(
2073
((c.dbus_object_path, c.GetAll(""))
2074
for c in tcp_server.clients),
2077
@dbus.service.method(_interface, in_signature="o")
2078
def RemoveClient(self, object_path):
2080
for c in tcp_server.clients:
2081
if c.dbus_object_path == object_path:
2082
tcp_server.clients.remove(c)
2083
c.remove_from_connection()
2084
# Don't signal anything except ClientRemoved
2085
c.disable(quiet=True)
2087
self.ClientRemoved(object_path, c.name)
2089
raise KeyError(object_path)
2093
class MandosDBusServiceTransitional(MandosDBusService):
2094
__metaclass__ = AlternateDBusNamesMetaclass
2095
mandos_dbus_service = MandosDBusServiceTransitional()
2098
"Cleanup function; run on exit"
2101
while tcp_server.clients:
2102
client = tcp_server.clients.pop()
2104
client.remove_from_connection()
2105
client.disable_hook = None
2106
# Don't signal anything except ClientRemoved
2107
client.disable(quiet=True)
2110
mandos_dbus_service.ClientRemoved(client.dbus_object_path,
2113
atexit.register(cleanup)
2115
for client in tcp_server.clients:
2118
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2122
tcp_server.server_activate()
2124
# Find out what port we got
2125
service.port = tcp_server.socket.getsockname()[1]
2127
logger.info("Now listening on address %r, port %d,"
2128
" flowinfo %d, scope_id %d"
2129
% tcp_server.socket.getsockname())
2131
logger.info("Now listening on address %r, port %d"
2132
% tcp_server.socket.getsockname())
2134
#service.interface = tcp_server.socket.getsockname()[3]
2137
# From the Avahi example code
2140
except dbus.exceptions.DBusException as error:
2141
logger.critical("DBusException: %s", error)
2144
# End of Avahi example code
2146
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2147
lambda *args, **kwargs:
2148
(tcp_server.handle_request
2149
(*args[2:], **kwargs) or True))
2151
logger.debug("Starting main loop")
2153
except AvahiError as error:
2154
logger.critical("AvahiError: %s", error)
2157
except KeyboardInterrupt:
2159
print("", file=sys.stderr)
2160
logger.debug("Server received KeyboardInterrupt")
2161
logger.debug("Server exiting")
2162
# Must run before the D-Bus bus name gets deregistered
2166
if __name__ == '__main__':
124
parser = OptionParser()
125
parser.add_option("-i", "--interface", type="string",
126
default="eth0", metavar="IF",
127
help="Interface to bind to")
128
parser.add_option("--cert", type="string", default="cert.pem",
130
help="Public key certificate to use")
131
parser.add_option("--key", type="string", default="key.pem",
133
help="Private key to use")
134
parser.add_option("--ca", type="string", default="ca.pem",
136
help="Certificate Authority certificate to use")
137
parser.add_option("--crl", type="string", default="crl.pem",
139
help="Certificate Revokation List to use")
140
parser.add_option("-p", "--port", type="int", default=49001,
141
help="Port number to receive requests on")
142
parser.add_option("--dh", type="int", metavar="BITS",
143
help="DH group to use")
144
parser.add_option("-t", "--timeout", type="string", # Parsed later
146
help="Amount of downtime allowed for clients")
147
(options, args) = parser.parse_args()
149
# Parse the time argument
151
suffix=options.timeout[-1]
152
value=int(options.timeout[:-1])
154
options.timeout = datetime.timedelta(value)
156
options.timeout = datetime.timedelta(0, value)
158
options.timeout = datetime.timedelta(0, 0, 0, 0, value)
160
options.timeout = datetime.timedelta(0, 0, 0, 0, 0, value)
162
options.timeout = datetime.timedelta(0, 0, 0, 0, 0, 0,
166
except (ValueError, IndexError):
167
parser.error("option --timeout: Unparseable time")
169
cert = gnutls.crypto.X509Certificate(open(options.cert).read())
170
key = gnutls.crypto.X509PrivateKey(open(options.key).read())
171
ca = gnutls.crypto.X509Certificate(open(options.ca).read())
172
crl = gnutls.crypto.X509CRL(open(options.crl).read())
173
cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
177
client_config_object = ConfigParser.SafeConfigParser(defaults)
178
client_config_object.read("mandos-clients.conf")
179
clients = [Client(name=section,
180
**(dict(client_config_object.items(section))))
181
for section in client_config_object.sections()]
183
udp_server = IPv6_UDPServer((in6addr_any, options.port),
187
tcp_server = IPv6_TCPServer((in6addr_any, options.port),
194
in_, out, err = select.select((udp_server,
197
server.handle_request()
200
if __name__ == "__main__":