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-2015 Teddy Hogeborn
15
# Copyright © 2008-2015 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@recompile.se>.
34
from __future__ import (division, absolute_import, print_function,
37
from future_builtins import *
40
import SocketServer as socketserver
6
from optparse import OptionParser
47
9
import gnutls.crypto
48
10
import gnutls.connection
49
11
import gnutls.errors
50
import gnutls.library.functions
51
import gnutls.library.constants
52
import gnutls.library.types
54
import ConfigParser as configparser
65
import logging.handlers
72
import cPickle as pickle
75
import multiprocessing
88
from gi.repository import GObject as gobject
90
from dbus.mainloop.glib import DBusGMainLoop
93
import xml.dom.minidom
97
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
98
except AttributeError:
100
from IN import SO_BINDTODEVICE
102
SO_BINDTODEVICE = None
104
if sys.version_info.major == 2:
108
stored_state_file = "clients.pickle"
110
logger = logging.getLogger()
114
if_nametoindex = ctypes.cdll.LoadLibrary(
115
ctypes.util.find_library("c")).if_nametoindex
116
except (OSError, AttributeError):
118
def if_nametoindex(interface):
119
"Get an interface index the hard way, i.e. using fcntl()"
120
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
121
with contextlib.closing(socket.socket()) as s:
122
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
123
struct.pack(b"16s16x", interface))
124
interface_index = struct.unpack("I", ifreq[16:20])[0]
125
return interface_index
128
def initlogger(debug, level=logging.WARNING):
129
"""init logger and add loglevel"""
132
syslogger = (logging.handlers.SysLogHandler(
133
facility = logging.handlers.SysLogHandler.LOG_DAEMON,
134
address = "/dev/log"))
135
syslogger.setFormatter(logging.Formatter
136
('Mandos [%(process)d]: %(levelname)s:'
138
logger.addHandler(syslogger)
141
console = logging.StreamHandler()
142
console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
146
logger.addHandler(console)
147
logger.setLevel(level)
150
class PGPError(Exception):
151
"""Exception if encryption/decryption fails"""
155
class PGPEngine(object):
156
"""A simple class for OpenPGP symmetric encryption & decryption"""
159
self.tempdir = tempfile.mkdtemp(prefix="mandos-")
160
self.gnupgargs = ['--batch',
161
'--home', self.tempdir,
169
def __exit__(self, exc_type, exc_value, traceback):
177
if self.tempdir is not None:
178
# Delete contents of tempdir
179
for root, dirs, files in os.walk(self.tempdir,
181
for filename in files:
182
os.remove(os.path.join(root, filename))
184
os.rmdir(os.path.join(root, dirname))
186
os.rmdir(self.tempdir)
189
def password_encode(self, password):
190
# Passphrase can not be empty and can not contain newlines or
191
# NUL bytes. So we prefix it and hex encode it.
192
encoded = b"mandos" + binascii.hexlify(password)
193
if len(encoded) > 2048:
194
# GnuPG can't handle long passwords, so encode differently
195
encoded = (b"mandos" + password.replace(b"\\", b"\\\\")
196
.replace(b"\n", b"\\n")
197
.replace(b"\0", b"\\x00"))
200
def encrypt(self, data, password):
201
passphrase = self.password_encode(password)
202
with tempfile.NamedTemporaryFile(
203
dir=self.tempdir) as passfile:
204
passfile.write(passphrase)
206
proc = subprocess.Popen(['gpg', '--symmetric',
210
stdin = subprocess.PIPE,
211
stdout = subprocess.PIPE,
212
stderr = subprocess.PIPE)
213
ciphertext, err = proc.communicate(input = data)
214
if proc.returncode != 0:
218
def decrypt(self, data, password):
219
passphrase = self.password_encode(password)
220
with tempfile.NamedTemporaryFile(
221
dir = self.tempdir) as passfile:
222
passfile.write(passphrase)
224
proc = subprocess.Popen(['gpg', '--decrypt',
228
stdin = subprocess.PIPE,
229
stdout = subprocess.PIPE,
230
stderr = subprocess.PIPE)
231
decrypted_plaintext, err = proc.communicate(input = data)
232
if proc.returncode != 0:
234
return decrypted_plaintext
237
class AvahiError(Exception):
238
def __init__(self, value, *args, **kwargs):
240
return super(AvahiError, self).__init__(value, *args,
244
class AvahiServiceError(AvahiError):
248
class AvahiGroupError(AvahiError):
252
class AvahiService(object):
253
"""An Avahi (Zeroconf) service.
256
interface: integer; avahi.IF_UNSPEC or an interface index.
257
Used to optionally bind to the specified interface.
258
name: string; Example: 'Mandos'
259
type: string; Example: '_mandos._tcp'.
260
See <https://www.iana.org/assignments/service-names-port-numbers>
261
port: integer; what port to announce
262
TXT: list of strings; TXT record for the service
263
domain: string; Domain to publish on, default to .local if empty.
264
host: string; Host to publish records for, default is localhost
265
max_renames: integer; maximum number of renames
266
rename_count: integer; counter so we only rename after collisions
267
a sensible number of times
268
group: D-Bus Entry Group
270
bus: dbus.SystemBus()
274
interface = avahi.IF_UNSPEC,
282
protocol = avahi.PROTO_UNSPEC,
284
self.interface = interface
286
self.type = servicetype
288
self.TXT = TXT if TXT is not None else []
291
self.rename_count = 0
292
self.max_renames = max_renames
293
self.protocol = protocol
294
self.group = None # our entry group
297
self.entry_group_state_changed_match = None
299
def rename(self, remove=True):
300
"""Derived from the Avahi example code"""
301
if self.rename_count >= self.max_renames:
302
logger.critical("No suitable Zeroconf service name found"
303
" after %i retries, exiting.",
305
raise AvahiServiceError("Too many renames")
307
self.server.GetAlternativeServiceName(self.name))
308
self.rename_count += 1
309
logger.info("Changing Zeroconf service name to %r ...",
315
except dbus.exceptions.DBusException as error:
316
if (error.get_dbus_name()
317
== "org.freedesktop.Avahi.CollisionError"):
318
logger.info("Local Zeroconf service name collision.")
319
return self.rename(remove=False)
321
logger.critical("D-Bus Exception", exc_info=error)
326
"""Derived from the Avahi example code"""
327
if self.entry_group_state_changed_match is not None:
328
self.entry_group_state_changed_match.remove()
329
self.entry_group_state_changed_match = None
330
if self.group is not None:
334
"""Derived from the Avahi example code"""
336
if self.group is None:
337
self.group = dbus.Interface(
338
self.bus.get_object(avahi.DBUS_NAME,
339
self.server.EntryGroupNew()),
340
avahi.DBUS_INTERFACE_ENTRY_GROUP)
341
self.entry_group_state_changed_match = (
342
self.group.connect_to_signal(
343
'StateChanged', self.entry_group_state_changed))
344
logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
345
self.name, self.type)
346
self.group.AddService(
349
dbus.UInt32(0), # flags
350
self.name, self.type,
351
self.domain, self.host,
352
dbus.UInt16(self.port),
353
avahi.string_array_to_txt_array(self.TXT))
356
def entry_group_state_changed(self, state, error):
357
"""Derived from the Avahi example code"""
358
logger.debug("Avahi entry group state change: %i", state)
360
if state == avahi.ENTRY_GROUP_ESTABLISHED:
361
logger.debug("Zeroconf service established.")
362
elif state == avahi.ENTRY_GROUP_COLLISION:
363
logger.info("Zeroconf service name collision.")
365
elif state == avahi.ENTRY_GROUP_FAILURE:
366
logger.critical("Avahi: Error in group state changed %s",
368
raise AvahiGroupError("State changed: {!s}".format(error))
371
"""Derived from the Avahi example code"""
372
if self.group is not None:
375
except (dbus.exceptions.UnknownMethodException,
376
dbus.exceptions.DBusException):
381
def server_state_changed(self, state, error=None):
382
"""Derived from the Avahi example code"""
383
logger.debug("Avahi server state change: %i", state)
385
avahi.SERVER_INVALID: "Zeroconf server invalid",
386
avahi.SERVER_REGISTERING: None,
387
avahi.SERVER_COLLISION: "Zeroconf server name collision",
388
avahi.SERVER_FAILURE: "Zeroconf server failure",
390
if state in bad_states:
391
if bad_states[state] is not None:
393
logger.error(bad_states[state])
395
logger.error(bad_states[state] + ": %r", error)
397
elif state == avahi.SERVER_RUNNING:
401
logger.debug("Unknown state: %r", state)
403
logger.debug("Unknown state: %r: %r", state, error)
406
"""Derived from the Avahi example code"""
407
if self.server is None:
408
self.server = dbus.Interface(
409
self.bus.get_object(avahi.DBUS_NAME,
410
avahi.DBUS_PATH_SERVER,
411
follow_name_owner_changes=True),
412
avahi.DBUS_INTERFACE_SERVER)
413
self.server.connect_to_signal("StateChanged",
414
self.server_state_changed)
415
self.server_state_changed(self.server.GetState())
418
class AvahiServiceToSyslog(AvahiService):
419
def rename(self, *args, **kwargs):
420
"""Add the new name to the syslog messages"""
421
ret = AvahiService.rename(self, *args, **kwargs)
422
syslogger.setFormatter(logging.Formatter(
423
'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
428
16
class Client(object):
429
"""A representation of a client host served by this server.
432
approved: bool(); 'None' if not yet approved/disapproved
433
approval_delay: datetime.timedelta(); Time to wait for approval
434
approval_duration: datetime.timedelta(); Duration of one approval
435
checker: subprocess.Popen(); a running checker process used
436
to see if the client lives.
437
'None' if no process is running.
438
checker_callback_tag: a gobject event source tag, or None
439
checker_command: string; External command which is run to check
440
if client lives. %() expansions are done at
441
runtime with vars(self) as dict, so that for
442
instance %(name)s can be used in the command.
443
checker_initiator_tag: a gobject event source tag, or None
444
created: datetime.datetime(); (UTC) object creation
445
client_structure: Object describing what attributes a client has
446
and is used for storing the client at exit
447
current_checker_command: string; current running checker_command
448
disable_initiator_tag: a gobject event source tag, or None
450
fingerprint: string (40 or 32 hexadecimal digits); used to
451
uniquely identify the client
452
host: string; available for use by the checker command
453
interval: datetime.timedelta(); How often to start a new checker
454
last_approval_request: datetime.datetime(); (UTC) or None
455
last_checked_ok: datetime.datetime(); (UTC) or None
456
last_checker_status: integer between 0 and 255 reflecting exit
457
status of last checker. -1 reflects crashed
458
checker, -2 means no checker completed yet.
459
last_enabled: datetime.datetime(); (UTC) or None
460
name: string; from the config file, used in log messages and
462
secret: bytestring; sent verbatim (over TLS) to client
463
timeout: datetime.timedelta(); How long from last_checked_ok
464
until this client is disabled
465
extended_timeout: extra long timeout when secret has been sent
466
runtime_expansions: Allowed attributes for runtime expansion.
467
expires: datetime.datetime(); time (UTC) when a client will be
469
server_settings: The server_settings dict from main()
472
runtime_expansions = ("approval_delay", "approval_duration",
473
"created", "enabled", "expires",
474
"fingerprint", "host", "interval",
475
"last_approval_request", "last_checked_ok",
476
"last_enabled", "name", "timeout")
479
"extended_timeout": "PT15M",
481
"checker": "fping -q -- %%(host)s",
483
"approval_delay": "PT0S",
484
"approval_duration": "PT1S",
485
"approved_by_default": "True",
490
def config_parser(config):
491
"""Construct a new dict of client settings of this form:
492
{ client_name: {setting_name: value, ...}, ...}
493
with exceptions for any special settings as defined above.
494
NOTE: Must be a pure function. Must return the same result
495
value given the same arguments.
498
for client_name in config.sections():
499
section = dict(config.items(client_name))
500
client = settings[client_name] = {}
502
client["host"] = section["host"]
503
# Reformat values from string types to Python types
504
client["approved_by_default"] = config.getboolean(
505
client_name, "approved_by_default")
506
client["enabled"] = config.getboolean(client_name,
509
# Uppercase and remove spaces from fingerprint for later
510
# comparison purposes with return value from the
511
# fingerprint() function
512
client["fingerprint"] = (section["fingerprint"].upper()
514
if "secret" in section:
515
client["secret"] = section["secret"].decode("base64")
516
elif "secfile" in section:
517
with open(os.path.expanduser(os.path.expandvars
518
(section["secfile"])),
520
client["secret"] = secfile.read()
522
raise TypeError("No secret or secfile for section {}"
524
client["timeout"] = string_to_delta(section["timeout"])
525
client["extended_timeout"] = string_to_delta(
526
section["extended_timeout"])
527
client["interval"] = string_to_delta(section["interval"])
528
client["approval_delay"] = string_to_delta(
529
section["approval_delay"])
530
client["approval_duration"] = string_to_delta(
531
section["approval_duration"])
532
client["checker_command"] = section["checker"]
533
client["last_approval_request"] = None
534
client["last_checked_ok"] = None
535
client["last_checker_status"] = -2
539
def __init__(self, settings, name = None, server_settings=None):
17
def __init__(self, name=None, options=None, dn=None,
18
password=None, passfile=None, fqdn=None,
19
timeout=None, interval=-1):
541
if server_settings is None:
543
self.server_settings = server_settings
544
# adding all client settings
545
for setting, value in settings.items():
546
setattr(self, setting, value)
549
if not hasattr(self, "last_enabled"):
550
self.last_enabled = datetime.datetime.utcnow()
551
if not hasattr(self, "expires"):
552
self.expires = (datetime.datetime.utcnow()
555
self.last_enabled = None
558
logger.debug("Creating client %r", self.name)
559
logger.debug(" Fingerprint: %s", self.fingerprint)
560
self.created = settings.get("created",
561
datetime.datetime.utcnow())
563
# attributes specific for this server instance
565
self.checker_initiator_tag = None
566
self.disable_initiator_tag = None
567
self.checker_callback_tag = None
568
self.current_checker_command = None
570
self.approvals_pending = 0
571
self.changedstate = multiprocessing_manager.Condition(
572
multiprocessing_manager.Lock())
573
self.client_structure = [attr
574
for attr in self.__dict__.iterkeys()
575
if not attr.startswith("_")]
576
self.client_structure.append("client_structure")
578
for name, t in inspect.getmembers(
579
type(self), lambda obj: isinstance(obj, property)):
580
if not name.startswith("_"):
581
self.client_structure.append(name)
583
# Send notice to process children that client state has changed
584
def send_changedstate(self):
585
with self.changedstate:
586
self.changedstate.notify_all()
589
"""Start this client's checker and timeout hooks"""
590
if getattr(self, "enabled", False):
593
self.expires = datetime.datetime.utcnow() + self.timeout
595
self.last_enabled = datetime.datetime.utcnow()
597
self.send_changedstate()
599
def disable(self, quiet=True):
600
"""Disable this client."""
601
if not getattr(self, "enabled", False):
604
logger.info("Disabling client %s", self.name)
605
if getattr(self, "disable_initiator_tag", None) is not None:
606
gobject.source_remove(self.disable_initiator_tag)
607
self.disable_initiator_tag = None
609
if getattr(self, "checker_initiator_tag", None) is not None:
610
gobject.source_remove(self.checker_initiator_tag)
611
self.checker_initiator_tag = None
615
self.send_changedstate()
616
# Do not run this again if called by a gobject.timeout_add
622
def init_checker(self):
623
# Schedule a new checker to be started an 'interval' from now,
624
# and every interval from then on.
625
if self.checker_initiator_tag is not None:
626
gobject.source_remove(self.checker_initiator_tag)
627
self.checker_initiator_tag = gobject.timeout_add(
628
int(self.interval.total_seconds() * 1000),
630
# Schedule a disable() when 'timeout' has passed
631
if self.disable_initiator_tag is not None:
632
gobject.source_remove(self.disable_initiator_tag)
633
self.disable_initiator_tag = gobject.timeout_add(
634
int(self.timeout.total_seconds() * 1000), self.disable)
635
# Also start a new checker *right now*.
638
def checker_callback(self, pid, condition, command):
639
"""The checker has completed, so take appropriate actions."""
640
self.checker_callback_tag = None
642
if os.WIFEXITED(condition):
643
self.last_checker_status = os.WEXITSTATUS(condition)
644
if self.last_checker_status == 0:
645
logger.info("Checker for %(name)s succeeded",
649
logger.info("Checker for %(name)s failed", vars(self))
651
self.last_checker_status = -1
652
logger.warning("Checker for %(name)s crashed?",
655
def checked_ok(self):
656
"""Assert that the client has been seen, alive and well."""
657
self.last_checked_ok = datetime.datetime.utcnow()
658
self.last_checker_status = 0
661
def bump_timeout(self, timeout=None):
662
"""Bump up the timeout for this client."""
23
self.password = password
25
self.password = open(passfile).readall()
27
print "No Password or Passfile in client config file"
28
# raise RuntimeError XXX
29
self.password = "gazonk"
31
self.created = datetime.datetime.now()
663
33
if timeout is None:
664
timeout = self.timeout
665
if self.disable_initiator_tag is not None:
666
gobject.source_remove(self.disable_initiator_tag)
667
self.disable_initiator_tag = None
668
if getattr(self, "enabled", False):
669
self.disable_initiator_tag = gobject.timeout_add(
670
int(timeout.total_seconds() * 1000), self.disable)
671
self.expires = datetime.datetime.utcnow() + timeout
673
def need_approval(self):
674
self.last_approval_request = datetime.datetime.utcnow()
676
def start_checker(self):
677
"""Start a new checker subprocess if one is not running.
679
If a checker already exists, leave it running and do
681
# The reason for not killing a running checker is that if we
682
# did that, and if a checker (for some reason) started running
683
# slowly and taking more than 'interval' time, then the client
684
# would inevitably timeout, since no checker would get a
685
# chance to run to completion. If we instead leave running
686
# checkers alone, the checker would have to take more time
687
# than 'timeout' for the client to be disabled, which is as it
690
# If a checker exists, make sure it is not a zombie
692
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
693
except AttributeError:
695
except OSError as error:
696
if error.errno != errno.ECHILD:
700
logger.warning("Checker was a zombie")
701
gobject.source_remove(self.checker_callback_tag)
702
self.checker_callback(pid, status,
703
self.current_checker_command)
704
# Start a new checker if needed
705
if self.checker is None:
706
# Escape attributes for the shell
708
attr: re.escape(str(getattr(self, attr)))
709
for attr in self.runtime_expansions }
711
command = self.checker_command % escaped_attrs
712
except TypeError as error:
713
logger.error('Could not format string "%s"',
714
self.checker_command,
716
return True # Try again later
717
self.current_checker_command = command
719
logger.info("Starting checker %r for %s", command,
721
# We don't need to redirect stdout and stderr, since
722
# in normal mode, that is already done by daemon(),
723
# and in debug mode we don't want to. (Stdin is
724
# always replaced by /dev/null.)
725
# The exception is when not debugging but nevertheless
726
# running in the foreground; use the previously
729
if (not self.server_settings["debug"]
730
and self.server_settings["foreground"]):
731
popen_args.update({"stdout": wnull,
733
self.checker = subprocess.Popen(command,
738
except OSError as error:
739
logger.error("Failed to start subprocess",
742
self.checker_callback_tag = gobject.child_watch_add(
743
self.checker.pid, self.checker_callback, data=command)
744
# The checker may have completed before the gobject
745
# watch was added. Check for this.
747
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
748
except OSError as error:
749
if error.errno == errno.ECHILD:
750
# This should never happen
751
logger.error("Child process vanished",
756
gobject.source_remove(self.checker_callback_tag)
757
self.checker_callback(pid, status, command)
758
# Re-run this periodically if run by gobject.timeout_add
761
def stop_checker(self):
762
"""Force the checker process, if any, to stop."""
763
if self.checker_callback_tag:
764
gobject.source_remove(self.checker_callback_tag)
765
self.checker_callback_tag = None
766
if getattr(self, "checker", None) is None:
768
logger.debug("Stopping checker for %(name)s", vars(self))
770
self.checker.terminate()
772
#if self.checker.poll() is None:
773
# self.checker.kill()
774
except OSError as error:
775
if error.errno != errno.ESRCH: # No such process
780
def dbus_service_property(dbus_interface,
784
"""Decorators for marking methods of a DBusObjectWithProperties to
785
become properties on the D-Bus.
787
The decorated method will be called with no arguments by "Get"
788
and with one argument by "Set".
790
The parameters, where they are supported, are the same as
791
dbus.service.method, except there is only "signature", since the
792
type from Get() and the type sent to Set() is the same.
794
# Encoding deeply encoded byte arrays is not supported yet by the
795
# "Set" method, so we fail early here:
796
if byte_arrays and signature != "ay":
797
raise ValueError("Byte arrays not supported for non-'ay'"
798
" signature {!r}".format(signature))
801
func._dbus_is_property = True
802
func._dbus_interface = dbus_interface
803
func._dbus_signature = signature
804
func._dbus_access = access
805
func._dbus_name = func.__name__
806
if func._dbus_name.endswith("_dbus_property"):
807
func._dbus_name = func._dbus_name[:-14]
808
func._dbus_get_args_options = {'byte_arrays': byte_arrays }
814
def dbus_interface_annotations(dbus_interface):
815
"""Decorator for marking functions returning interface annotations
819
@dbus_interface_annotations("org.example.Interface")
820
def _foo(self): # Function name does not matter
821
return {"org.freedesktop.DBus.Deprecated": "true",
822
"org.freedesktop.DBus.Property.EmitsChangedSignal":
827
func._dbus_is_interface = True
828
func._dbus_interface = dbus_interface
829
func._dbus_name = dbus_interface
835
def dbus_annotations(annotations):
836
"""Decorator to annotate D-Bus methods, signals or properties
839
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true",
840
"org.freedesktop.DBus.Property."
841
"EmitsChangedSignal": "false"})
842
@dbus_service_property("org.example.Interface", signature="b",
844
def Property_dbus_property(self):
845
return dbus.Boolean(False)
849
func._dbus_annotations = annotations
855
class DBusPropertyException(dbus.exceptions.DBusException):
856
"""A base class for D-Bus property-related exceptions
861
class DBusPropertyAccessException(DBusPropertyException):
862
"""A property's access permissions disallows an operation.
867
class DBusPropertyNotFound(DBusPropertyException):
868
"""An attempt was made to access a non-existing property.
873
class DBusObjectWithProperties(dbus.service.Object):
874
"""A D-Bus object with properties.
876
Classes inheriting from this can use the dbus_service_property
877
decorator to expose methods as D-Bus properties. It exposes the
878
standard Get(), Set(), and GetAll() methods on the D-Bus.
882
def _is_dbus_thing(thing):
883
"""Returns a function testing if an attribute is a D-Bus thing
885
If called like _is_dbus_thing("method") it returns a function
886
suitable for use as predicate to inspect.getmembers().
888
return lambda obj: getattr(obj, "_dbus_is_{}".format(thing),
891
def _get_all_dbus_things(self, thing):
892
"""Returns a generator of (name, attribute) pairs
894
return ((getattr(athing.__get__(self), "_dbus_name", name),
895
athing.__get__(self))
896
for cls in self.__class__.__mro__
898
inspect.getmembers(cls, self._is_dbus_thing(thing)))
900
def _get_dbus_property(self, interface_name, property_name):
901
"""Returns a bound method if one exists which is a D-Bus
902
property with the specified name and interface.
904
for cls in self.__class__.__mro__:
905
for name, value in inspect.getmembers(
906
cls, self._is_dbus_thing("property")):
907
if (value._dbus_name == property_name
908
and value._dbus_interface == interface_name):
909
return value.__get__(self)
912
raise DBusPropertyNotFound("{}:{}.{}".format(
913
self.dbus_object_path, interface_name, property_name))
915
@dbus.service.method(dbus.PROPERTIES_IFACE,
918
def Get(self, interface_name, property_name):
919
"""Standard D-Bus property Get() method, see D-Bus standard.
921
prop = self._get_dbus_property(interface_name, property_name)
922
if prop._dbus_access == "write":
923
raise DBusPropertyAccessException(property_name)
925
if not hasattr(value, "variant_level"):
927
return type(value)(value, variant_level=value.variant_level+1)
929
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
930
def Set(self, interface_name, property_name, value):
931
"""Standard D-Bus property Set() method, see D-Bus standard.
933
prop = self._get_dbus_property(interface_name, property_name)
934
if prop._dbus_access == "read":
935
raise DBusPropertyAccessException(property_name)
936
if prop._dbus_get_args_options["byte_arrays"]:
937
# The byte_arrays option is not supported yet on
938
# signatures other than "ay".
939
if prop._dbus_signature != "ay":
940
raise ValueError("Byte arrays not supported for non-"
941
"'ay' signature {!r}"
942
.format(prop._dbus_signature))
943
value = dbus.ByteArray(b''.join(chr(byte)
947
@dbus.service.method(dbus.PROPERTIES_IFACE,
949
out_signature="a{sv}")
950
def GetAll(self, interface_name):
951
"""Standard D-Bus property GetAll() method, see D-Bus
954
Note: Will not include properties with access="write".
957
for name, prop in self._get_all_dbus_things("property"):
959
and interface_name != prop._dbus_interface):
960
# Interface non-empty but did not match
962
# Ignore write-only properties
963
if prop._dbus_access == "write":
966
if not hasattr(value, "variant_level"):
967
properties[name] = value
969
properties[name] = type(value)(
970
value, variant_level = value.variant_level + 1)
971
return dbus.Dictionary(properties, signature="sv")
973
@dbus.service.signal(dbus.PROPERTIES_IFACE, signature="sa{sv}as")
974
def PropertiesChanged(self, interface_name, changed_properties,
975
invalidated_properties):
976
"""Standard D-Bus PropertiesChanged() signal, see D-Bus
981
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
983
path_keyword='object_path',
984
connection_keyword='connection')
985
def Introspect(self, object_path, connection):
986
"""Overloading of standard D-Bus method.
988
Inserts property tags and interface annotation tags.
990
xmlstring = dbus.service.Object.Introspect(self, object_path,
993
document = xml.dom.minidom.parseString(xmlstring)
995
def make_tag(document, name, prop):
996
e = document.createElement("property")
997
e.setAttribute("name", name)
998
e.setAttribute("type", prop._dbus_signature)
999
e.setAttribute("access", prop._dbus_access)
1002
for if_tag in document.getElementsByTagName("interface"):
1004
for tag in (make_tag(document, name, prop)
1006
in self._get_all_dbus_things("property")
1007
if prop._dbus_interface
1008
== if_tag.getAttribute("name")):
1009
if_tag.appendChild(tag)
1010
# Add annotation tags
1011
for typ in ("method", "signal", "property"):
1012
for tag in if_tag.getElementsByTagName(typ):
1014
for name, prop in (self.
1015
_get_all_dbus_things(typ)):
1016
if (name == tag.getAttribute("name")
1017
and prop._dbus_interface
1018
== if_tag.getAttribute("name")):
1019
annots.update(getattr(
1020
prop, "_dbus_annotations", {}))
1021
for name, value in annots.items():
1022
ann_tag = document.createElement(
1024
ann_tag.setAttribute("name", name)
1025
ann_tag.setAttribute("value", value)
1026
tag.appendChild(ann_tag)
1027
# Add interface annotation tags
1028
for annotation, value in dict(
1029
itertools.chain.from_iterable(
1030
annotations().items()
1031
for name, annotations
1032
in self._get_all_dbus_things("interface")
1033
if name == if_tag.getAttribute("name")
1035
ann_tag = document.createElement("annotation")
1036
ann_tag.setAttribute("name", annotation)
1037
ann_tag.setAttribute("value", value)
1038
if_tag.appendChild(ann_tag)
1039
# Add the names to the return values for the
1040
# "org.freedesktop.DBus.Properties" methods
1041
if (if_tag.getAttribute("name")
1042
== "org.freedesktop.DBus.Properties"):
1043
for cn in if_tag.getElementsByTagName("method"):
1044
if cn.getAttribute("name") == "Get":
1045
for arg in cn.getElementsByTagName("arg"):
1046
if (arg.getAttribute("direction")
1048
arg.setAttribute("name", "value")
1049
elif cn.getAttribute("name") == "GetAll":
1050
for arg in cn.getElementsByTagName("arg"):
1051
if (arg.getAttribute("direction")
1053
arg.setAttribute("name", "props")
1054
xmlstring = document.toxml("utf-8")
1056
except (AttributeError, xml.dom.DOMException,
1057
xml.parsers.expat.ExpatError) as error:
1058
logger.error("Failed to override Introspection method",
1063
def datetime_to_dbus(dt, variant_level=0):
1064
"""Convert a UTC datetime.datetime() to a D-Bus type."""
1066
return dbus.String("", variant_level = variant_level)
1067
return dbus.String(dt.isoformat(), variant_level=variant_level)
1070
def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
1071
"""A class decorator; applied to a subclass of
1072
dbus.service.Object, it will add alternate D-Bus attributes with
1073
interface names according to the "alt_interface_names" mapping.
1076
@alternate_dbus_interfaces({"org.example.Interface":
1077
"net.example.AlternateInterface"})
1078
class SampleDBusObject(dbus.service.Object):
1079
@dbus.service.method("org.example.Interface")
1080
def SampleDBusMethod():
1083
The above "SampleDBusMethod" on "SampleDBusObject" will be
1084
reachable via two interfaces: "org.example.Interface" and
1085
"net.example.AlternateInterface", the latter of which will have
1086
its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
1087
"true", unless "deprecate" is passed with a False value.
1089
This works for methods and signals, and also for D-Bus properties
1090
(from DBusObjectWithProperties) and interfaces (from the
1091
dbus_interface_annotations decorator).
1095
for orig_interface_name, alt_interface_name in (
1096
alt_interface_names.items()):
1098
interface_names = set()
1099
# Go though all attributes of the class
1100
for attrname, attribute in inspect.getmembers(cls):
1101
# Ignore non-D-Bus attributes, and D-Bus attributes
1102
# with the wrong interface name
1103
if (not hasattr(attribute, "_dbus_interface")
1104
or not attribute._dbus_interface.startswith(
1105
orig_interface_name)):
1107
# Create an alternate D-Bus interface name based on
1109
alt_interface = attribute._dbus_interface.replace(
1110
orig_interface_name, alt_interface_name)
1111
interface_names.add(alt_interface)
1112
# Is this a D-Bus signal?
1113
if getattr(attribute, "_dbus_is_signal", False):
1114
if sys.version == 2:
1115
# Extract the original non-method undecorated
1116
# function by black magic
1117
nonmethod_func = (dict(
1118
zip(attribute.func_code.co_freevars,
1119
attribute.__closure__))
1120
["func"].cell_contents)
1122
nonmethod_func = attribute
1123
# Create a new, but exactly alike, function
1124
# object, and decorate it to be a new D-Bus signal
1125
# with the alternate D-Bus interface name
1126
if sys.version == 2:
1127
new_function = types.FunctionType(
1128
nonmethod_func.func_code,
1129
nonmethod_func.func_globals,
1130
nonmethod_func.func_name,
1131
nonmethod_func.func_defaults,
1132
nonmethod_func.func_closure)
1134
new_function = types.FunctionType(
1135
nonmethod_func.__code__,
1136
nonmethod_func.__globals__,
1137
nonmethod_func.__name__,
1138
nonmethod_func.__defaults__,
1139
nonmethod_func.__closure__)
1140
new_function = (dbus.service.signal(
1142
attribute._dbus_signature)(new_function))
1143
# Copy annotations, if any
1145
new_function._dbus_annotations = dict(
1146
attribute._dbus_annotations)
1147
except AttributeError:
1149
# Define a creator of a function to call both the
1150
# original and alternate functions, so both the
1151
# original and alternate signals gets sent when
1152
# the function is called
1153
def fixscope(func1, func2):
1154
"""This function is a scope container to pass
1155
func1 and func2 to the "call_both" function
1156
outside of its arguments"""
1158
def call_both(*args, **kwargs):
1159
"""This function will emit two D-Bus
1160
signals by calling func1 and func2"""
1161
func1(*args, **kwargs)
1162
func2(*args, **kwargs)
1165
# Create the "call_both" function and add it to
1167
attr[attrname] = fixscope(attribute, new_function)
1168
# Is this a D-Bus method?
1169
elif getattr(attribute, "_dbus_is_method", False):
1170
# Create a new, but exactly alike, function
1171
# object. Decorate it to be a new D-Bus method
1172
# with the alternate D-Bus interface name. Add it
1175
dbus.service.method(
1177
attribute._dbus_in_signature,
1178
attribute._dbus_out_signature)
1179
(types.FunctionType(attribute.func_code,
1180
attribute.func_globals,
1181
attribute.func_name,
1182
attribute.func_defaults,
1183
attribute.func_closure)))
1184
# Copy annotations, if any
1186
attr[attrname]._dbus_annotations = dict(
1187
attribute._dbus_annotations)
1188
except AttributeError:
1190
# Is this a D-Bus property?
1191
elif getattr(attribute, "_dbus_is_property", False):
1192
# Create a new, but exactly alike, function
1193
# object, and decorate it to be a new D-Bus
1194
# property with the alternate D-Bus interface
1195
# name. Add it to the class.
1196
attr[attrname] = (dbus_service_property(
1197
alt_interface, attribute._dbus_signature,
1198
attribute._dbus_access,
1199
attribute._dbus_get_args_options
1201
(types.FunctionType(
1202
attribute.func_code,
1203
attribute.func_globals,
1204
attribute.func_name,
1205
attribute.func_defaults,
1206
attribute.func_closure)))
1207
# Copy annotations, if any
1209
attr[attrname]._dbus_annotations = dict(
1210
attribute._dbus_annotations)
1211
except AttributeError:
1213
# Is this a D-Bus interface?
1214
elif getattr(attribute, "_dbus_is_interface", False):
1215
# Create a new, but exactly alike, function
1216
# object. Decorate it to be a new D-Bus interface
1217
# with the alternate D-Bus interface name. Add it
1220
dbus_interface_annotations(alt_interface)
1221
(types.FunctionType(attribute.func_code,
1222
attribute.func_globals,
1223
attribute.func_name,
1224
attribute.func_defaults,
1225
attribute.func_closure)))
1227
# Deprecate all alternate interfaces
1228
iname="_AlternateDBusNames_interface_annotation{}"
1229
for interface_name in interface_names:
1231
@dbus_interface_annotations(interface_name)
1233
return { "org.freedesktop.DBus.Deprecated":
1235
# Find an unused name
1236
for aname in (iname.format(i)
1237
for i in itertools.count()):
1238
if aname not in attr:
1242
# Replace the class with a new subclass of it with
1243
# methods, signals, etc. as created above.
1244
cls = type(b"{}Alternate".format(cls.__name__),
1251
@alternate_dbus_interfaces({"se.recompile.Mandos":
1252
"se.bsnet.fukt.Mandos"})
1253
class ClientDBus(Client, DBusObjectWithProperties):
1254
"""A Client class using D-Bus
1257
dbus_object_path: dbus.ObjectPath
1258
bus: dbus.SystemBus()
1261
runtime_expansions = (Client.runtime_expansions
1262
+ ("dbus_object_path", ))
1264
_interface = "se.recompile.Mandos.Client"
1266
# dbus.service.Object doesn't use super(), so we can't either.
1268
def __init__(self, bus = None, *args, **kwargs):
1270
Client.__init__(self, *args, **kwargs)
1271
# Only now, when this client is initialized, can it show up on
1273
client_object_name = str(self.name).translate(
1274
{ord("."): ord("_"),
1275
ord("-"): ord("_")})
1276
self.dbus_object_path = dbus.ObjectPath(
1277
"/clients/" + client_object_name)
1278
DBusObjectWithProperties.__init__(self, self.bus,
1279
self.dbus_object_path)
1281
def notifychangeproperty(transform_func, dbus_name,
1282
type_func=lambda x: x,
1284
invalidate_only=False,
1285
_interface=_interface):
1286
""" Modify a variable so that it's a property which announces
1287
its changes to DBus.
1289
transform_fun: Function that takes a value and a variant_level
1290
and transforms it to a D-Bus type.
1291
dbus_name: D-Bus name of the variable
1292
type_func: Function that transform the value before sending it
1293
to the D-Bus. Default: no transform
1294
variant_level: D-Bus variant level. Default: 1
1296
attrname = "_{}".format(dbus_name)
1298
def setter(self, value):
1299
if hasattr(self, "dbus_object_path"):
1300
if (not hasattr(self, attrname) or
1301
type_func(getattr(self, attrname, None))
1302
!= type_func(value)):
1304
self.PropertiesChanged(
1305
_interface, dbus.Dictionary(),
1306
dbus.Array((dbus_name, )))
1308
dbus_value = transform_func(
1310
variant_level = variant_level)
1311
self.PropertyChanged(dbus.String(dbus_name),
1313
self.PropertiesChanged(
1315
dbus.Dictionary({ dbus.String(dbus_name):
1318
setattr(self, attrname, value)
1320
return property(lambda self: getattr(self, attrname), setter)
1322
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1323
approvals_pending = notifychangeproperty(dbus.Boolean,
1326
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1327
last_enabled = notifychangeproperty(datetime_to_dbus,
1329
checker = notifychangeproperty(
1330
dbus.Boolean, "CheckerRunning",
1331
type_func = lambda checker: checker is not None)
1332
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1334
last_checker_status = notifychangeproperty(dbus.Int16,
1335
"LastCheckerStatus")
1336
last_approval_request = notifychangeproperty(
1337
datetime_to_dbus, "LastApprovalRequest")
1338
approved_by_default = notifychangeproperty(dbus.Boolean,
1339
"ApprovedByDefault")
1340
approval_delay = notifychangeproperty(
1341
dbus.UInt64, "ApprovalDelay",
1342
type_func = lambda td: td.total_seconds() * 1000)
1343
approval_duration = notifychangeproperty(
1344
dbus.UInt64, "ApprovalDuration",
1345
type_func = lambda td: td.total_seconds() * 1000)
1346
host = notifychangeproperty(dbus.String, "Host")
1347
timeout = notifychangeproperty(
1348
dbus.UInt64, "Timeout",
1349
type_func = lambda td: td.total_seconds() * 1000)
1350
extended_timeout = notifychangeproperty(
1351
dbus.UInt64, "ExtendedTimeout",
1352
type_func = lambda td: td.total_seconds() * 1000)
1353
interval = notifychangeproperty(
1354
dbus.UInt64, "Interval",
1355
type_func = lambda td: td.total_seconds() * 1000)
1356
checker_command = notifychangeproperty(dbus.String, "Checker")
1357
secret = notifychangeproperty(dbus.ByteArray, "Secret",
1358
invalidate_only=True)
1360
del notifychangeproperty
1362
def __del__(self, *args, **kwargs):
1364
self.remove_from_connection()
1367
if hasattr(DBusObjectWithProperties, "__del__"):
1368
DBusObjectWithProperties.__del__(self, *args, **kwargs)
1369
Client.__del__(self, *args, **kwargs)
1371
def checker_callback(self, pid, condition, command,
1373
self.checker_callback_tag = None
1375
if os.WIFEXITED(condition):
1376
exitstatus = os.WEXITSTATUS(condition)
1378
self.CheckerCompleted(dbus.Int16(exitstatus),
1379
dbus.Int64(condition),
1380
dbus.String(command))
1383
self.CheckerCompleted(dbus.Int16(-1),
1384
dbus.Int64(condition),
1385
dbus.String(command))
1387
return Client.checker_callback(self, pid, condition, command,
1390
def start_checker(self, *args, **kwargs):
1391
old_checker_pid = getattr(self.checker, "pid", None)
1392
r = Client.start_checker(self, *args, **kwargs)
1393
# Only if new checker process was started
1394
if (self.checker is not None
1395
and old_checker_pid != self.checker.pid):
1397
self.CheckerStarted(self.current_checker_command)
1400
def _reset_approved(self):
1401
self.approved = None
1404
def approve(self, value=True):
1405
self.approved = value
1406
gobject.timeout_add(int(self.approval_duration.total_seconds()
1407
* 1000), self._reset_approved)
1408
self.send_changedstate()
1410
## D-Bus methods, signals & properties
1416
# CheckerCompleted - signal
1417
@dbus.service.signal(_interface, signature="nxs")
1418
def CheckerCompleted(self, exitcode, waitstatus, command):
1422
# CheckerStarted - signal
1423
@dbus.service.signal(_interface, signature="s")
1424
def CheckerStarted(self, command):
1428
# PropertyChanged - signal
1429
@dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
1430
@dbus.service.signal(_interface, signature="sv")
1431
def PropertyChanged(self, property, value):
1435
# GotSecret - signal
1436
@dbus.service.signal(_interface)
1437
def GotSecret(self):
1439
Is sent after a successful transfer of secret from the Mandos
1440
server to mandos-client
1445
@dbus.service.signal(_interface, signature="s")
1446
def Rejected(self, reason):
1450
# NeedApproval - signal
1451
@dbus.service.signal(_interface, signature="tb")
1452
def NeedApproval(self, timeout, default):
1454
return self.need_approval()
1459
@dbus.service.method(_interface, in_signature="b")
1460
def Approve(self, value):
1463
# CheckedOK - method
1464
@dbus.service.method(_interface)
1465
def CheckedOK(self):
1469
@dbus.service.method(_interface)
1474
# StartChecker - method
1475
@dbus.service.method(_interface)
1476
def StartChecker(self):
1478
self.start_checker()
1481
@dbus.service.method(_interface)
1486
# StopChecker - method
1487
@dbus.service.method(_interface)
1488
def StopChecker(self):
1493
# ApprovalPending - property
1494
@dbus_service_property(_interface, signature="b", access="read")
1495
def ApprovalPending_dbus_property(self):
1496
return dbus.Boolean(bool(self.approvals_pending))
1498
# ApprovedByDefault - property
1499
@dbus_service_property(_interface,
1502
def ApprovedByDefault_dbus_property(self, value=None):
1503
if value is None: # get
1504
return dbus.Boolean(self.approved_by_default)
1505
self.approved_by_default = bool(value)
1507
# ApprovalDelay - property
1508
@dbus_service_property(_interface,
1511
def ApprovalDelay_dbus_property(self, value=None):
1512
if value is None: # get
1513
return dbus.UInt64(self.approval_delay.total_seconds()
1515
self.approval_delay = datetime.timedelta(0, 0, 0, value)
1517
# ApprovalDuration - property
1518
@dbus_service_property(_interface,
1521
def ApprovalDuration_dbus_property(self, value=None):
1522
if value is None: # get
1523
return dbus.UInt64(self.approval_duration.total_seconds()
1525
self.approval_duration = datetime.timedelta(0, 0, 0, value)
1528
@dbus_service_property(_interface, signature="s", access="read")
1529
def Name_dbus_property(self):
1530
return dbus.String(self.name)
1532
# Fingerprint - property
1533
@dbus_service_property(_interface, signature="s", access="read")
1534
def Fingerprint_dbus_property(self):
1535
return dbus.String(self.fingerprint)
1538
@dbus_service_property(_interface,
1541
def Host_dbus_property(self, value=None):
1542
if value is None: # get
1543
return dbus.String(self.host)
1544
self.host = str(value)
1546
# Created - property
1547
@dbus_service_property(_interface, signature="s", access="read")
1548
def Created_dbus_property(self):
1549
return datetime_to_dbus(self.created)
1551
# LastEnabled - property
1552
@dbus_service_property(_interface, signature="s", access="read")
1553
def LastEnabled_dbus_property(self):
1554
return datetime_to_dbus(self.last_enabled)
1556
# Enabled - property
1557
@dbus_service_property(_interface,
1560
def Enabled_dbus_property(self, value=None):
1561
if value is None: # get
1562
return dbus.Boolean(self.enabled)
1568
# LastCheckedOK - property
1569
@dbus_service_property(_interface,
1572
def LastCheckedOK_dbus_property(self, value=None):
1573
if value is not None:
1576
return datetime_to_dbus(self.last_checked_ok)
1578
# LastCheckerStatus - property
1579
@dbus_service_property(_interface, signature="n", access="read")
1580
def LastCheckerStatus_dbus_property(self):
1581
return dbus.Int16(self.last_checker_status)
1583
# Expires - property
1584
@dbus_service_property(_interface, signature="s", access="read")
1585
def Expires_dbus_property(self):
1586
return datetime_to_dbus(self.expires)
1588
# LastApprovalRequest - property
1589
@dbus_service_property(_interface, signature="s", access="read")
1590
def LastApprovalRequest_dbus_property(self):
1591
return datetime_to_dbus(self.last_approval_request)
1593
# Timeout - property
1594
@dbus_service_property(_interface,
1597
def Timeout_dbus_property(self, value=None):
1598
if value is None: # get
1599
return dbus.UInt64(self.timeout.total_seconds() * 1000)
1600
old_timeout = self.timeout
1601
self.timeout = datetime.timedelta(0, 0, 0, value)
1602
# Reschedule disabling
1604
now = datetime.datetime.utcnow()
1605
self.expires += self.timeout - old_timeout
1606
if self.expires <= now:
1607
# The timeout has passed
1610
if (getattr(self, "disable_initiator_tag", None)
1613
gobject.source_remove(self.disable_initiator_tag)
1614
self.disable_initiator_tag = gobject.timeout_add(
1615
int((self.expires - now).total_seconds() * 1000),
1618
# ExtendedTimeout - property
1619
@dbus_service_property(_interface,
1622
def ExtendedTimeout_dbus_property(self, value=None):
1623
if value is None: # get
1624
return dbus.UInt64(self.extended_timeout.total_seconds()
1626
self.extended_timeout = datetime.timedelta(0, 0, 0, value)
1628
# Interval - property
1629
@dbus_service_property(_interface,
1632
def Interval_dbus_property(self, value=None):
1633
if value is None: # get
1634
return dbus.UInt64(self.interval.total_seconds() * 1000)
1635
self.interval = datetime.timedelta(0, 0, 0, value)
1636
if getattr(self, "checker_initiator_tag", None) is None:
1639
# Reschedule checker run
1640
gobject.source_remove(self.checker_initiator_tag)
1641
self.checker_initiator_tag = gobject.timeout_add(
1642
value, self.start_checker)
1643
self.start_checker() # Start one now, too
1645
# Checker - property
1646
@dbus_service_property(_interface,
1649
def Checker_dbus_property(self, value=None):
1650
if value is None: # get
1651
return dbus.String(self.checker_command)
1652
self.checker_command = str(value)
1654
# CheckerRunning - property
1655
@dbus_service_property(_interface,
1658
def CheckerRunning_dbus_property(self, value=None):
1659
if value is None: # get
1660
return dbus.Boolean(self.checker is not None)
1662
self.start_checker()
1666
# ObjectPath - property
1667
@dbus_service_property(_interface, signature="o", access="read")
1668
def ObjectPath_dbus_property(self):
1669
return self.dbus_object_path # is already a dbus.ObjectPath
1672
@dbus_service_property(_interface,
1676
def Secret_dbus_property(self, value):
1677
self.secret = bytes(value)
1682
class ProxyClient(object):
1683
def __init__(self, child_pipe, fpr, address):
1684
self._pipe = child_pipe
1685
self._pipe.send(('init', fpr, address))
1686
if not self._pipe.recv():
1689
def __getattribute__(self, name):
1691
return super(ProxyClient, self).__getattribute__(name)
1692
self._pipe.send(('getattr', name))
1693
data = self._pipe.recv()
1694
if data[0] == 'data':
1696
if data[0] == 'function':
1698
def func(*args, **kwargs):
1699
self._pipe.send(('funcall', name, args, kwargs))
1700
return self._pipe.recv()[1]
1704
def __setattr__(self, name, value):
1706
return super(ProxyClient, self).__setattr__(name, value)
1707
self._pipe.send(('setattr', name, value))
1710
class ClientHandler(socketserver.BaseRequestHandler, object):
1711
"""A class to handle client connections.
1713
Instantiated once for each connection to handle it.
1714
Note: This will run in its own forked process."""
1717
with contextlib.closing(self.server.child_pipe) as child_pipe:
1718
logger.info("TCP connection from: %s",
1719
str(self.client_address))
1720
logger.debug("Pipe FD: %d",
1721
self.server.child_pipe.fileno())
1723
session = gnutls.connection.ClientSession(
1724
self.request, gnutls.connection .X509Credentials())
1726
# Note: gnutls.connection.X509Credentials is really a
1727
# generic GnuTLS certificate credentials object so long as
1728
# no X.509 keys are added to it. Therefore, we can use it
1729
# here despite using OpenPGP certificates.
1731
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
1732
# "+AES-256-CBC", "+SHA1",
1733
# "+COMP-NULL", "+CTYPE-OPENPGP",
1735
# Use a fallback default, since this MUST be set.
1736
priority = self.server.gnutls_priority
1737
if priority is None:
1739
gnutls.library.functions.gnutls_priority_set_direct(
1740
session._c_object, priority, None)
1742
# Start communication using the Mandos protocol
1743
# Get protocol number
1744
line = self.request.makefile().readline()
1745
logger.debug("Protocol version: %r", line)
1747
if int(line.strip().split()[0]) > 1:
1748
raise RuntimeError(line)
1749
except (ValueError, IndexError, RuntimeError) as error:
1750
logger.error("Unknown protocol version: %s", error)
1753
# Start GnuTLS connection
1756
except gnutls.errors.GNUTLSError as error:
1757
logger.warning("Handshake failed: %s", error)
1758
# Do not run session.bye() here: the session is not
1759
# established. Just abandon the request.
1761
logger.debug("Handshake succeeded")
1763
approval_required = False
1766
fpr = self.fingerprint(
1767
self.peer_certificate(session))
1769
gnutls.errors.GNUTLSError) as error:
1770
logger.warning("Bad certificate: %s", error)
1772
logger.debug("Fingerprint: %s", fpr)
1775
client = ProxyClient(child_pipe, fpr,
1776
self.client_address)
1780
if client.approval_delay:
1781
delay = client.approval_delay
1782
client.approvals_pending += 1
1783
approval_required = True
1786
if not client.enabled:
1787
logger.info("Client %s is disabled",
1789
if self.server.use_dbus:
1791
client.Rejected("Disabled")
1794
if client.approved or not client.approval_delay:
1795
#We are approved or approval is disabled
1797
elif client.approved is None:
1798
logger.info("Client %s needs approval",
1800
if self.server.use_dbus:
1802
client.NeedApproval(
1803
client.approval_delay.total_seconds()
1804
* 1000, client.approved_by_default)
1806
logger.warning("Client %s was not approved",
1808
if self.server.use_dbus:
1810
client.Rejected("Denied")
1813
#wait until timeout or approved
1814
time = datetime.datetime.now()
1815
client.changedstate.acquire()
1816
client.changedstate.wait(delay.total_seconds())
1817
client.changedstate.release()
1818
time2 = datetime.datetime.now()
1819
if (time2 - time) >= delay:
1820
if not client.approved_by_default:
1821
logger.warning("Client %s timed out while"
1822
" waiting for approval",
1824
if self.server.use_dbus:
1826
client.Rejected("Approval timed out")
1831
delay -= time2 - time
1834
while sent_size < len(client.secret):
1836
sent = session.send(client.secret[sent_size:])
1837
except gnutls.errors.GNUTLSError as error:
1838
logger.warning("gnutls send failed",
1841
logger.debug("Sent: %d, remaining: %d", sent,
1842
len(client.secret) - (sent_size
1846
logger.info("Sending secret to %s", client.name)
1847
# bump the timeout using extended_timeout
1848
client.bump_timeout(client.extended_timeout)
1849
if self.server.use_dbus:
1854
if approval_required:
1855
client.approvals_pending -= 1
1858
except gnutls.errors.GNUTLSError as error:
1859
logger.warning("GnuTLS bye failed",
1863
def peer_certificate(session):
1864
"Return the peer's OpenPGP certificate as a bytestring"
1865
# If not an OpenPGP certificate...
1866
if (gnutls.library.functions.gnutls_certificate_type_get(
1868
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1869
# ...do the normal thing
1870
return session.peer_certificate
1871
list_size = ctypes.c_uint(1)
1872
cert_list = (gnutls.library.functions
1873
.gnutls_certificate_get_peers
1874
(session._c_object, ctypes.byref(list_size)))
1875
if not bool(cert_list) and list_size.value != 0:
1876
raise gnutls.errors.GNUTLSError("error getting peer"
1878
if list_size.value == 0:
1881
return ctypes.string_at(cert.data, cert.size)
1884
def fingerprint(openpgp):
1885
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1886
# New GnuTLS "datum" with the OpenPGP public key
1887
datum = gnutls.library.types.gnutls_datum_t(
1888
ctypes.cast(ctypes.c_char_p(openpgp),
1889
ctypes.POINTER(ctypes.c_ubyte)),
1890
ctypes.c_uint(len(openpgp)))
1891
# New empty GnuTLS certificate
1892
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1893
gnutls.library.functions.gnutls_openpgp_crt_init(
1895
# Import the OpenPGP public key into the certificate
1896
gnutls.library.functions.gnutls_openpgp_crt_import(
1897
crt, ctypes.byref(datum),
1898
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
1899
# Verify the self signature in the key
1900
crtverify = ctypes.c_uint()
1901
gnutls.library.functions.gnutls_openpgp_crt_verify_self(
1902
crt, 0, ctypes.byref(crtverify))
1903
if crtverify.value != 0:
1904
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1905
raise gnutls.errors.CertificateSecurityError(
1907
# New buffer for the fingerprint
1908
buf = ctypes.create_string_buffer(20)
1909
buf_len = ctypes.c_size_t()
1910
# Get the fingerprint from the certificate into the buffer
1911
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint(
1912
crt, ctypes.byref(buf), ctypes.byref(buf_len))
1913
# Deinit the certificate
1914
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1915
# Convert the buffer to a Python bytestring
1916
fpr = ctypes.string_at(buf, buf_len.value)
1917
# Convert the bytestring to hexadecimal notation
1918
hex_fpr = binascii.hexlify(fpr).upper()
1922
class MultiprocessingMixIn(object):
1923
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
1925
def sub_process_main(self, request, address):
1927
self.finish_request(request, address)
1929
self.handle_error(request, address)
1930
self.close_request(request)
1932
def process_request(self, request, address):
1933
"""Start a new process to process the request."""
1934
proc = multiprocessing.Process(target = self.sub_process_main,
1935
args = (request, address))
1940
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
1941
""" adds a pipe to the MixIn """
1943
def process_request(self, request, client_address):
1944
"""Overrides and wraps the original process_request().
1946
This function creates a new pipe in self.pipe
1948
parent_pipe, self.child_pipe = multiprocessing.Pipe()
1950
proc = MultiprocessingMixIn.process_request(self, request,
1952
self.child_pipe.close()
1953
self.add_pipe(parent_pipe, proc)
1955
def add_pipe(self, parent_pipe, proc):
1956
"""Dummy function; override as necessary"""
1957
raise NotImplementedError()
1960
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1961
socketserver.TCPServer, object):
1962
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
1965
enabled: Boolean; whether this server is activated yet
1966
interface: None or a network interface name (string)
1967
use_ipv6: Boolean; to use IPv6 or not
1970
def __init__(self, server_address, RequestHandlerClass,
1974
"""If socketfd is set, use that file descriptor instead of
1975
creating a new one with socket.socket().
1977
self.interface = interface
1979
self.address_family = socket.AF_INET6
1980
if socketfd is not None:
1981
# Save the file descriptor
1982
self.socketfd = socketfd
1983
# Save the original socket.socket() function
1984
self.socket_socket = socket.socket
1985
# To implement --socket, we monkey patch socket.socket.
1987
# (When socketserver.TCPServer is a new-style class, we
1988
# could make self.socket into a property instead of monkey
1989
# patching socket.socket.)
1991
# Create a one-time-only replacement for socket.socket()
1992
@functools.wraps(socket.socket)
1993
def socket_wrapper(*args, **kwargs):
1994
# Restore original function so subsequent calls are
1996
socket.socket = self.socket_socket
1997
del self.socket_socket
1998
# This time only, return a new socket object from the
1999
# saved file descriptor.
2000
return socket.fromfd(self.socketfd, *args, **kwargs)
2001
# Replace socket.socket() function with wrapper
2002
socket.socket = socket_wrapper
2003
# The socketserver.TCPServer.__init__ will call
2004
# socket.socket(), which might be our replacement,
2005
# socket_wrapper(), if socketfd was set.
2006
socketserver.TCPServer.__init__(self, server_address,
2007
RequestHandlerClass)
2009
def server_bind(self):
2010
"""This overrides the normal server_bind() function
2011
to bind to an interface if one was specified, and also NOT to
2012
bind to an address or port if they were not specified."""
2013
if self.interface is not None:
2014
if SO_BINDTODEVICE is None:
2015
logger.error("SO_BINDTODEVICE does not exist;"
2016
" cannot bind to interface %s",
2020
self.socket.setsockopt(
2021
socket.SOL_SOCKET, SO_BINDTODEVICE,
2022
(self.interface + "\0").encode("utf-8"))
2023
except socket.error as error:
2024
if error.errno == errno.EPERM:
2025
logger.error("No permission to bind to"
2026
" interface %s", self.interface)
2027
elif error.errno == errno.ENOPROTOOPT:
2028
logger.error("SO_BINDTODEVICE not available;"
2029
" cannot bind to interface %s",
2031
elif error.errno == errno.ENODEV:
2032
logger.error("Interface %s does not exist,"
2033
" cannot bind", self.interface)
2036
# Only bind(2) the socket if we really need to.
2037
if self.server_address[0] or self.server_address[1]:
2038
if not self.server_address[0]:
2039
if self.address_family == socket.AF_INET6:
2040
any_address = "::" # in6addr_any
2042
any_address = "0.0.0.0" # INADDR_ANY
2043
self.server_address = (any_address,
2044
self.server_address[1])
2045
elif not self.server_address[1]:
2046
self.server_address = (self.server_address[0], 0)
2047
# if self.interface:
2048
# self.server_address = (self.server_address[0],
2053
return socketserver.TCPServer.server_bind(self)
2056
class MandosServer(IPv6_TCPServer):
2060
clients: set of Client objects
2061
gnutls_priority GnuTLS priority string
2062
use_dbus: Boolean; to emit D-Bus signals or not
2064
Assumes a gobject.MainLoop event loop.
2067
def __init__(self, server_address, RequestHandlerClass,
2071
gnutls_priority=None,
2074
self.enabled = False
2075
self.clients = clients
2076
if self.clients is None:
2078
self.use_dbus = use_dbus
2079
self.gnutls_priority = gnutls_priority
2080
IPv6_TCPServer.__init__(self, server_address,
2081
RequestHandlerClass,
2082
interface = interface,
2083
use_ipv6 = use_ipv6,
2084
socketfd = socketfd)
2086
def server_activate(self):
2088
return socketserver.TCPServer.server_activate(self)
2093
def add_pipe(self, parent_pipe, proc):
2094
# Call "handle_ipc" for both data and EOF events
2095
gobject.io_add_watch(
2096
parent_pipe.fileno(),
2097
gobject.IO_IN | gobject.IO_HUP,
2098
functools.partial(self.handle_ipc,
2099
parent_pipe = parent_pipe,
2102
def handle_ipc(self, source, condition,
2105
client_object=None):
2106
# error, or the other end of multiprocessing.Pipe has closed
2107
if condition & (gobject.IO_ERR | gobject.IO_HUP):
2108
# Wait for other process to exit
2112
# Read a request from the child
2113
request = parent_pipe.recv()
2114
command = request[0]
2116
if command == 'init':
2118
address = request[2]
2120
for c in self.clients.itervalues():
2121
if c.fingerprint == fpr:
2125
logger.info("Client not found for fingerprint: %s, ad"
2126
"dress: %s", fpr, address)
2129
mandos_dbus_service.ClientNotFound(fpr,
2131
parent_pipe.send(False)
2134
gobject.io_add_watch(
2135
parent_pipe.fileno(),
2136
gobject.IO_IN | gobject.IO_HUP,
2137
functools.partial(self.handle_ipc,
2138
parent_pipe = parent_pipe,
2140
client_object = client))
2141
parent_pipe.send(True)
2142
# remove the old hook in favor of the new above hook on
2145
if command == 'funcall':
2146
funcname = request[1]
2150
parent_pipe.send(('data', getattr(client_object,
2154
if command == 'getattr':
2155
attrname = request[1]
2156
if isinstance(client_object.__getattribute__(attrname),
2157
collections.Callable):
2158
parent_pipe.send(('function', ))
2161
'data', client_object.__getattribute__(attrname)))
2163
if command == 'setattr':
2164
attrname = request[1]
2166
setattr(client_object, attrname, value)
2171
def rfc3339_duration_to_delta(duration):
2172
"""Parse an RFC 3339 "duration" and return a datetime.timedelta
2174
>>> rfc3339_duration_to_delta("P7D")
2175
datetime.timedelta(7)
2176
>>> rfc3339_duration_to_delta("PT60S")
2177
datetime.timedelta(0, 60)
2178
>>> rfc3339_duration_to_delta("PT60M")
2179
datetime.timedelta(0, 3600)
2180
>>> rfc3339_duration_to_delta("PT24H")
2181
datetime.timedelta(1)
2182
>>> rfc3339_duration_to_delta("P1W")
2183
datetime.timedelta(7)
2184
>>> rfc3339_duration_to_delta("PT5M30S")
2185
datetime.timedelta(0, 330)
2186
>>> rfc3339_duration_to_delta("P1DT3M20S")
2187
datetime.timedelta(1, 200)
2190
# Parsing an RFC 3339 duration with regular expressions is not
2191
# possible - there would have to be multiple places for the same
2192
# values, like seconds. The current code, while more esoteric, is
2193
# cleaner without depending on a parsing library. If Python had a
2194
# built-in library for parsing we would use it, but we'd like to
2195
# avoid excessive use of external libraries.
2197
# New type for defining tokens, syntax, and semantics all-in-one
2198
Token = collections.namedtuple("Token", (
2199
"regexp", # To match token; if "value" is not None, must have
2200
# a "group" containing digits
2201
"value", # datetime.timedelta or None
2202
"followers")) # Tokens valid after this token
2203
# RFC 3339 "duration" tokens, syntax, and semantics; taken from
2204
# the "duration" ABNF definition in RFC 3339, Appendix A.
2205
token_end = Token(re.compile(r"$"), None, frozenset())
2206
token_second = Token(re.compile(r"(\d+)S"),
2207
datetime.timedelta(seconds=1),
2208
frozenset((token_end, )))
2209
token_minute = Token(re.compile(r"(\d+)M"),
2210
datetime.timedelta(minutes=1),
2211
frozenset((token_second, token_end)))
2212
token_hour = Token(re.compile(r"(\d+)H"),
2213
datetime.timedelta(hours=1),
2214
frozenset((token_minute, token_end)))
2215
token_time = Token(re.compile(r"T"),
2217
frozenset((token_hour, token_minute,
2219
token_day = Token(re.compile(r"(\d+)D"),
2220
datetime.timedelta(days=1),
2221
frozenset((token_time, token_end)))
2222
token_month = Token(re.compile(r"(\d+)M"),
2223
datetime.timedelta(weeks=4),
2224
frozenset((token_day, token_end)))
2225
token_year = Token(re.compile(r"(\d+)Y"),
2226
datetime.timedelta(weeks=52),
2227
frozenset((token_month, token_end)))
2228
token_week = Token(re.compile(r"(\d+)W"),
2229
datetime.timedelta(weeks=1),
2230
frozenset((token_end, )))
2231
token_duration = Token(re.compile(r"P"), None,
2232
frozenset((token_year, token_month,
2233
token_day, token_time,
2235
# Define starting values
2236
value = datetime.timedelta() # Value so far
2238
followers = frozenset((token_duration, )) # Following valid tokens
2239
s = duration # String left to parse
2240
# Loop until end token is found
2241
while found_token is not token_end:
2242
# Search for any currently valid tokens
2243
for token in followers:
2244
match = token.regexp.match(s)
2245
if match is not None:
2247
if token.value is not None:
2248
# Value found, parse digits
2249
factor = int(match.group(1), 10)
2250
# Add to value so far
2251
value += factor * token.value
2252
# Strip token from string
2253
s = token.regexp.sub("", s, 1)
2256
# Set valid next tokens
2257
followers = found_token.followers
2260
# No currently valid tokens were found
2261
raise ValueError("Invalid RFC 3339 duration: {!r}"
34
timeout = options.timeout
35
self.timeout = timeout
37
interval = options.interval
38
self.interval = interval
39
self.next_check = datetime.datetime.now()
42
class server_metaclass(type):
43
"Common behavior for the UDP and TCP server classes"
44
def __new__(cls, name, bases, attrs):
45
attrs["address_family"] = socket.AF_INET6
46
attrs["allow_reuse_address"] = True
47
def server_bind(self):
48
if self.options.interface:
49
if not hasattr(socket, "SO_BINDTODEVICE"):
50
# From /usr/include/asm-i486/socket.h
51
socket.SO_BINDTODEVICE = 25
53
self.socket.setsockopt(socket.SOL_SOCKET,
54
socket.SO_BINDTODEVICE,
55
self.options.interface)
56
except socket.error, error:
57
if error[0] == errno.EPERM:
58
print "Warning: No permission to bind to interface", \
59
self.options.interface
62
return super(type(self), self).server_bind()
63
attrs["server_bind"] = server_bind
64
def init(self, *args, **kwargs):
65
if "options" in kwargs:
66
self.options = kwargs["options"]
68
if "clients" in kwargs:
69
self.clients = kwargs["clients"]
71
if "credentials" in kwargs:
72
self.credentials = kwargs["credentials"]
73
del kwargs["credentials"]
74
return super(type(self), self).__init__(*args, **kwargs)
75
attrs["__init__"] = init
76
return type.__new__(cls, name, bases, attrs)
79
class udp_handler(SocketServer.DatagramRequestHandler, object):
81
self.wfile.write("Polo")
82
print "UDP request answered"
85
class IPv6_UDPServer(SocketServer.UDPServer, object):
86
__metaclass__ = server_metaclass
87
def verify_request(self, request, client_address):
88
print "UDP request came"
89
return request[0] == "Marco"
92
class tcp_handler(SocketServer.BaseRequestHandler, object):
94
print "TCP request came"
95
print "Request:", self.request
96
print "Client Address:", self.client_address
97
print "Server:", self.server
98
session = gnutls.connection.ServerSession(self.request,
99
self.server.credentials)
101
if session.peer_certificate:
102
print "DN:", session.peer_certificate.subject
104
session.verify_peer()
105
except gnutls.errors.CertificateError, error:
106
print "Verify failed", error
110
session.send(dict((client.dn, client.password)
111
for client in self.server.clients)
112
[session.peer_certificate.subject])
114
session.send("gazonk")
119
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
120
__metaclass__ = server_metaclass
121
request_queue_size = 1024
2267
128
def string_to_delta(interval):
2268
129
"""Parse a string and return a datetime.timedelta
2270
131
>>> string_to_delta('7d')
2271
132
datetime.timedelta(7)
2272
133
>>> string_to_delta('60s')
2275
136
datetime.timedelta(0, 3600)
2276
137
>>> string_to_delta('24h')
2277
138
datetime.timedelta(1)
2278
>>> string_to_delta('1w')
139
>>> string_to_delta(u'1w')
2279
140
datetime.timedelta(7)
2280
>>> string_to_delta('5m 30s')
2281
datetime.timedelta(0, 330)
2285
return rfc3339_duration_to_delta(interval)
2289
timevalue = datetime.timedelta(0)
2290
for s in interval.split():
2295
delta = datetime.timedelta(value)
2297
delta = datetime.timedelta(0, value)
2299
delta = datetime.timedelta(0, 0, 0, 0, value)
2301
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
2303
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
2305
raise ValueError("Unknown suffix {!r}".format(suffix))
2306
except IndexError as e:
2307
raise ValueError(*(e.args))
2312
def daemon(nochdir = False, noclose = False):
2313
"""See daemon(3). Standard BSD Unix function.
2315
This should really exist as os.daemon, but it doesn't (yet)."""
2324
# Close all standard open file descriptors
2325
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2326
if not stat.S_ISCHR(os.fstat(null).st_mode):
2327
raise OSError(errno.ENODEV,
2328
"{} not a character device"
2329
.format(os.devnull))
2330
os.dup2(null, sys.stdin.fileno())
2331
os.dup2(null, sys.stdout.fileno())
2332
os.dup2(null, sys.stderr.fileno())
143
suffix=unicode(interval[-1])
144
value=int(interval[:-1])
146
delta = datetime.timedelta(value)
148
delta = datetime.timedelta(0, value)
150
delta = datetime.timedelta(0, 0, 0, 0, value)
152
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
154
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
157
except (ValueError, IndexError):
2339
##################################################################
2340
# Parsing of options, both command line and config file
2342
parser = argparse.ArgumentParser()
2343
parser.add_argument("-v", "--version", action="version",
2344
version = "%(prog)s {}".format(version),
2345
help="show version number and exit")
2346
parser.add_argument("-i", "--interface", metavar="IF",
2347
help="Bind to interface IF")
2348
parser.add_argument("-a", "--address",
2349
help="Address to listen for requests on")
2350
parser.add_argument("-p", "--port", type=int,
2351
help="Port number to receive requests on")
2352
parser.add_argument("--check", action="store_true",
2353
help="Run self-test")
2354
parser.add_argument("--debug", action="store_true",
2355
help="Debug mode; run in foreground and log"
2356
" to terminal", default=None)
2357
parser.add_argument("--debuglevel", metavar="LEVEL",
2358
help="Debug level for stdout output")
2359
parser.add_argument("--priority", help="GnuTLS"
2360
" priority string (see GnuTLS documentation)")
2361
parser.add_argument("--servicename",
2362
metavar="NAME", help="Zeroconf service name")
2363
parser.add_argument("--configdir",
2364
default="/etc/mandos", metavar="DIR",
2365
help="Directory to search for configuration"
2367
parser.add_argument("--no-dbus", action="store_false",
2368
dest="use_dbus", help="Do not provide D-Bus"
2369
" system bus interface", default=None)
2370
parser.add_argument("--no-ipv6", action="store_false",
2371
dest="use_ipv6", help="Do not use IPv6",
2373
parser.add_argument("--no-restore", action="store_false",
2374
dest="restore", help="Do not restore stored"
2375
" state", default=None)
2376
parser.add_argument("--socket", type=int,
2377
help="Specify a file descriptor to a network"
2378
" socket to use instead of creating one")
2379
parser.add_argument("--statedir", metavar="DIR",
2380
help="Directory to save/restore state in")
2381
parser.add_argument("--foreground", action="store_true",
2382
help="Run in foreground", default=None)
2383
parser.add_argument("--no-zeroconf", action="store_false",
2384
dest="zeroconf", help="Do not use Zeroconf",
2387
options = parser.parse_args()
162
parser = OptionParser()
163
parser.add_option("-i", "--interface", type="string",
164
default="eth0", metavar="IF",
165
help="Interface to bind to")
166
parser.add_option("--cert", type="string", default="cert.pem",
168
help="Public key certificate to use")
169
parser.add_option("--key", type="string", default="key.pem",
171
help="Private key to use")
172
parser.add_option("--ca", type="string", default="ca.pem",
174
help="Certificate Authority certificate to use")
175
parser.add_option("--crl", type="string", default="crl.pem",
177
help="Certificate Revokation List to use")
178
parser.add_option("-p", "--port", type="int", default=49001,
179
help="Port number to receive requests on")
180
parser.add_option("--dh", type="int", metavar="BITS",
181
help="DH group to use")
182
parser.add_option("-t", "--timeout", type="string", # Parsed later
184
help="Amount of downtime allowed for clients")
185
parser.add_option("--interval", type="string", # Parsed later
187
help="How often to check that a client is up")
188
parser.add_option("--check", action="store_true", default=False,
189
help="Run self-test")
190
(options, args) = parser.parse_args()
2389
192
if options.check:
2391
fail_count, test_count = doctest.testmod()
2392
sys.exit(os.EX_OK if fail_count == 0 else 1)
2394
# Default values for config file for server-global settings
2395
server_defaults = { "interface": "",
2400
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2401
":+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
2402
"servicename": "Mandos",
2408
"statedir": "/var/lib/mandos",
2409
"foreground": "False",
2413
# Parse config file for server-global settings
2414
server_config = configparser.SafeConfigParser(server_defaults)
2416
server_config.read(os.path.join(options.configdir, "mandos.conf"))
2417
# Convert the SafeConfigParser object to a dict
2418
server_settings = server_config.defaults()
2419
# Use the appropriate methods on the non-string config options
2420
for option in ("debug", "use_dbus", "use_ipv6", "foreground"):
2421
server_settings[option] = server_config.getboolean("DEFAULT",
2423
if server_settings["port"]:
2424
server_settings["port"] = server_config.getint("DEFAULT",
2426
if server_settings["socket"]:
2427
server_settings["socket"] = server_config.getint("DEFAULT",
2429
# Later, stdin will, and stdout and stderr might, be dup'ed
2430
# over with an opened os.devnull. But we don't want this to
2431
# happen with a supplied network socket.
2432
if 0 <= server_settings["socket"] <= 2:
2433
server_settings["socket"] = os.dup(server_settings
2437
# Override the settings from the config file with command line
2439
for option in ("interface", "address", "port", "debug",
2440
"priority", "servicename", "configdir", "use_dbus",
2441
"use_ipv6", "debuglevel", "restore", "statedir",
2442
"socket", "foreground", "zeroconf"):
2443
value = getattr(options, option)
2444
if value is not None:
2445
server_settings[option] = value
2447
# Force all strings to be unicode
2448
for option in server_settings.keys():
2449
if isinstance(server_settings[option], bytes):
2450
server_settings[option] = (server_settings[option]
2452
# Force all boolean options to be boolean
2453
for option in ("debug", "use_dbus", "use_ipv6", "restore",
2454
"foreground", "zeroconf"):
2455
server_settings[option] = bool(server_settings[option])
2456
# Debug implies foreground
2457
if server_settings["debug"]:
2458
server_settings["foreground"] = True
2459
# Now we have our good server settings in "server_settings"
2461
##################################################################
2463
if (not server_settings["zeroconf"]
2464
and not (server_settings["port"]
2465
or server_settings["socket"] != "")):
2466
parser.error("Needs port or socket to work without Zeroconf")
2469
debug = server_settings["debug"]
2470
debuglevel = server_settings["debuglevel"]
2471
use_dbus = server_settings["use_dbus"]
2472
use_ipv6 = server_settings["use_ipv6"]
2473
stored_state_path = os.path.join(server_settings["statedir"],
2475
foreground = server_settings["foreground"]
2476
zeroconf = server_settings["zeroconf"]
2479
initlogger(debug, logging.DEBUG)
2484
level = getattr(logging, debuglevel.upper())
2485
initlogger(debug, level)
2487
if server_settings["servicename"] != "Mandos":
2488
syslogger.setFormatter(
2489
logging.Formatter('Mandos ({}) [%(process)d]:'
2490
' %(levelname)s: %(message)s'.format(
2491
server_settings["servicename"])))
2493
# Parse config file with clients
2494
client_config = configparser.SafeConfigParser(Client
2496
client_config.read(os.path.join(server_settings["configdir"],
2499
global mandos_dbus_service
2500
mandos_dbus_service = None
2503
if server_settings["socket"] != "":
2504
socketfd = server_settings["socket"]
2505
tcp_server = MandosServer(
2506
(server_settings["address"], server_settings["port"]),
2508
interface=(server_settings["interface"] or None),
2510
gnutls_priority=server_settings["priority"],
2514
pidfilename = "/run/mandos.pid"
2515
if not os.path.isdir("/run/."):
2516
pidfilename = "/var/run/mandos.pid"
2519
pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
2520
except IOError as e:
2521
logger.error("Could not open file %r", pidfilename,
2524
for name in ("_mandos", "mandos", "nobody"):
2526
uid = pwd.getpwnam(name).pw_uid
2527
gid = pwd.getpwnam(name).pw_gid
2537
except OSError as error:
2538
if error.errno != errno.EPERM:
2542
# Enable all possible GnuTLS debugging
2544
# "Use a log level over 10 to enable all debugging options."
2546
gnutls.library.functions.gnutls_global_set_log_level(11)
2548
@gnutls.library.types.gnutls_log_func
2549
def debug_gnutls(level, string):
2550
logger.debug("GnuTLS: %s", string[:-1])
2552
gnutls.library.functions.gnutls_global_set_log_function(
2555
# Redirect stdin so all checkers get /dev/null
2556
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
2557
os.dup2(null, sys.stdin.fileno())
2561
# Need to fork before connecting to D-Bus
2563
# Close all input and output, do double fork, etc.
2566
# multiprocessing will use threads, so before we use gobject we
2567
# need to inform gobject that threads will be used.
2568
gobject.threads_init()
2571
# From the Avahi example code
2572
DBusGMainLoop(set_as_default=True)
2573
main_loop = gobject.MainLoop()
2574
bus = dbus.SystemBus()
2575
# End of Avahi example code
2578
bus_name = dbus.service.BusName("se.recompile.Mandos",
2581
old_bus_name = dbus.service.BusName(
2582
"se.bsnet.fukt.Mandos", bus,
2584
except dbus.exceptions.DBusException as e:
2585
logger.error("Disabling D-Bus:", exc_info=e)
2587
server_settings["use_dbus"] = False
2588
tcp_server.use_dbus = False
2590
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2591
service = AvahiServiceToSyslog(
2592
name = server_settings["servicename"],
2593
servicetype = "_mandos._tcp",
2594
protocol = protocol,
2596
if server_settings["interface"]:
2597
service.interface = if_nametoindex(
2598
server_settings["interface"].encode("utf-8"))
2600
global multiprocessing_manager
2601
multiprocessing_manager = multiprocessing.Manager()
2603
client_class = Client
2605
client_class = functools.partial(ClientDBus, bus = bus)
2607
client_settings = Client.config_parser(client_config)
2608
old_client_settings = {}
2611
# This is used to redirect stdout and stderr for checker processes
2613
wnull = open(os.devnull, "w") # A writable /dev/null
2614
# Only used if server is running in foreground but not in debug
2616
if debug or not foreground:
2619
# Get client data and settings from last running state.
2620
if server_settings["restore"]:
2622
with open(stored_state_path, "rb") as stored_state:
2623
clients_data, old_client_settings = pickle.load(
2625
os.remove(stored_state_path)
2626
except IOError as e:
2627
if e.errno == errno.ENOENT:
2628
logger.warning("Could not load persistent state:"
2629
" {}".format(os.strerror(e.errno)))
2631
logger.critical("Could not load persistent state:",
2634
except EOFError as e:
2635
logger.warning("Could not load persistent state: "
2639
with PGPEngine() as pgp:
2640
for client_name, client in clients_data.items():
2641
# Skip removed clients
2642
if client_name not in client_settings:
2645
# Decide which value to use after restoring saved state.
2646
# We have three different values: Old config file,
2647
# new config file, and saved state.
2648
# New config value takes precedence if it differs from old
2649
# config value, otherwise use saved state.
2650
for name, value in client_settings[client_name].items():
2652
# For each value in new config, check if it
2653
# differs from the old config value (Except for
2654
# the "secret" attribute)
2655
if (name != "secret"
2657
old_client_settings[client_name][name])):
2658
client[name] = value
2662
# Clients who has passed its expire date can still be
2663
# enabled if its last checker was successful. Clients
2664
# whose checker succeeded before we stored its state is
2665
# assumed to have successfully run all checkers during
2667
if client["enabled"]:
2668
if datetime.datetime.utcnow() >= client["expires"]:
2669
if not client["last_checked_ok"]:
2671
"disabling client {} - Client never "
2672
"performed a successful checker".format(
2674
client["enabled"] = False
2675
elif client["last_checker_status"] != 0:
2677
"disabling client {} - Client last"
2678
" checker failed with error code"
2681
client["last_checker_status"]))
2682
client["enabled"] = False
2684
client["expires"] = (
2685
datetime.datetime.utcnow()
2686
+ client["timeout"])
2687
logger.debug("Last checker succeeded,"
2688
" keeping {} enabled".format(
2691
client["secret"] = pgp.decrypt(
2692
client["encrypted_secret"],
2693
client_settings[client_name]["secret"])
2695
# If decryption fails, we use secret from new settings
2696
logger.debug("Failed to decrypt {} old secret".format(
2698
client["secret"] = (client_settings[client_name]
2701
# Add/remove clients based on new changes made to config
2702
for client_name in (set(old_client_settings)
2703
- set(client_settings)):
2704
del clients_data[client_name]
2705
for client_name in (set(client_settings)
2706
- set(old_client_settings)):
2707
clients_data[client_name] = client_settings[client_name]
2709
# Create all client objects
2710
for client_name, client in clients_data.items():
2711
tcp_server.clients[client_name] = client_class(
2714
server_settings = server_settings)
2716
if not tcp_server.clients:
2717
logger.warning("No clients defined")
2720
if pidfile is not None:
2724
print(pid, file=pidfile)
2726
logger.error("Could not write to file %r with PID %d",
2731
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2732
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
2736
@alternate_dbus_interfaces(
2737
{ "se.recompile.Mandos": "se.bsnet.fukt.Mandos" })
2738
class MandosDBusService(DBusObjectWithProperties):
2739
"""A D-Bus proxy object"""
2742
dbus.service.Object.__init__(self, bus, "/")
2744
_interface = "se.recompile.Mandos"
2746
@dbus_interface_annotations(_interface)
2749
"org.freedesktop.DBus.Property.EmitsChangedSignal":
2752
@dbus.service.signal(_interface, signature="o")
2753
def ClientAdded(self, objpath):
2757
@dbus.service.signal(_interface, signature="ss")
2758
def ClientNotFound(self, fingerprint, address):
2762
@dbus.service.signal(_interface, signature="os")
2763
def ClientRemoved(self, objpath, name):
2767
@dbus.service.method(_interface, out_signature="ao")
2768
def GetAllClients(self):
2770
return dbus.Array(c.dbus_object_path for c in
2771
tcp_server.clients.itervalues())
2773
@dbus.service.method(_interface,
2774
out_signature="a{oa{sv}}")
2775
def GetAllClientsWithProperties(self):
2777
return dbus.Dictionary(
2778
{ c.dbus_object_path: c.GetAll("")
2779
for c in tcp_server.clients.itervalues() },
2782
@dbus.service.method(_interface, in_signature="o")
2783
def RemoveClient(self, object_path):
2785
for c in tcp_server.clients.itervalues():
2786
if c.dbus_object_path == object_path:
2787
del tcp_server.clients[c.name]
2788
c.remove_from_connection()
2789
# Don't signal anything except ClientRemoved
2790
c.disable(quiet=True)
2792
self.ClientRemoved(object_path, c.name)
2794
raise KeyError(object_path)
2798
mandos_dbus_service = MandosDBusService()
2801
"Cleanup function; run on exit"
2805
multiprocessing.active_children()
2807
if not (tcp_server.clients or client_settings):
2810
# Store client before exiting. Secrets are encrypted with key
2811
# based on what config file has. If config file is
2812
# removed/edited, old secret will thus be unrecovable.
2814
with PGPEngine() as pgp:
2815
for client in tcp_server.clients.itervalues():
2816
key = client_settings[client.name]["secret"]
2817
client.encrypted_secret = pgp.encrypt(client.secret,
2821
# A list of attributes that can not be pickled
2823
exclude = { "bus", "changedstate", "secret",
2824
"checker", "server_settings" }
2825
for name, typ in inspect.getmembers(dbus.service
2829
client_dict["encrypted_secret"] = (client
2831
for attr in client.client_structure:
2832
if attr not in exclude:
2833
client_dict[attr] = getattr(client, attr)
2835
clients[client.name] = client_dict
2836
del client_settings[client.name]["secret"]
2839
with tempfile.NamedTemporaryFile(
2843
dir=os.path.dirname(stored_state_path),
2844
delete=False) as stored_state:
2845
pickle.dump((clients, client_settings), stored_state)
2846
tempname = stored_state.name
2847
os.rename(tempname, stored_state_path)
2848
except (IOError, OSError) as e:
2854
if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
2855
logger.warning("Could not save persistent state: {}"
2856
.format(os.strerror(e.errno)))
2858
logger.warning("Could not save persistent state:",
2862
# Delete all clients, and settings from config
2863
while tcp_server.clients:
2864
name, client = tcp_server.clients.popitem()
2866
client.remove_from_connection()
2867
# Don't signal anything except ClientRemoved
2868
client.disable(quiet=True)
2871
mandos_dbus_service.ClientRemoved(
2872
client.dbus_object_path, client.name)
2873
client_settings.clear()
2875
atexit.register(cleanup)
2877
for client in tcp_server.clients.itervalues():
2880
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2881
# Need to initiate checking of clients
2883
client.init_checker()
2886
tcp_server.server_activate()
2888
# Find out what port we got
2890
service.port = tcp_server.socket.getsockname()[1]
2892
logger.info("Now listening on address %r, port %d,"
2893
" flowinfo %d, scope_id %d",
2894
*tcp_server.socket.getsockname())
2896
logger.info("Now listening on address %r, port %d",
2897
*tcp_server.socket.getsockname())
2899
#service.interface = tcp_server.socket.getsockname()[3]
2903
# From the Avahi example code
2906
except dbus.exceptions.DBusException as error:
2907
logger.critical("D-Bus Exception", exc_info=error)
2910
# End of Avahi example code
2912
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2913
lambda *args, **kwargs:
2914
(tcp_server.handle_request
2915
(*args[2:], **kwargs) or True))
2917
logger.debug("Starting main loop")
2919
except AvahiError as error:
2920
logger.critical("Avahi Error", exc_info=error)
2923
except KeyboardInterrupt:
2925
print("", file=sys.stderr)
2926
logger.debug("Server received KeyboardInterrupt")
2927
logger.debug("Server exiting")
2928
# Must run before the D-Bus bus name gets deregistered
2932
if __name__ == '__main__':
197
# Parse the time arguments
199
options.timeout = string_to_delta(options.timeout)
201
parser.error("option --timeout: Unparseable time")
204
options.interval = string_to_delta(options.interval)
206
parser.error("option --interval: Unparseable time")
208
cert = gnutls.crypto.X509Certificate(open(options.cert).read())
209
key = gnutls.crypto.X509PrivateKey(open(options.key).read())
210
ca = gnutls.crypto.X509Certificate(open(options.ca).read())
211
crl = gnutls.crypto.X509CRL(open(options.crl).read())
212
cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
216
client_config_object = ConfigParser.SafeConfigParser(defaults)
217
client_config_object.read("mandos-clients.conf")
218
clients = [Client(name=section, options=options,
219
**(dict(client_config_object.items(section))))
220
for section in client_config_object.sections()]
222
udp_server = IPv6_UDPServer((in6addr_any, options.port),
226
tcp_server = IPv6_TCPServer((in6addr_any, options.port),
233
in_, out, err = select.select((udp_server,
236
server.handle_request()
239
if __name__ == "__main__":