Commit f375fe85 authored by segfault's avatar segfault

Initial commit

parents
__pycache__/
build/
dist/
*.egg-info/
- Return success status for Start() and Stop()
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.boum.tails.OnionKit"/>
</policy>
<policy context="default">
<allow send_destination="org.boum.tails.OnionKit"/>
<allow receive_sender="org.boum.tails.OnionKit"/>
</policy>
</busconfig>
# Translation stuff
from os import path
import gettext
if path.exists('po/locale'):
translation = gettext.translation('onionservices', 'po/locale', fallback=True)
else:
translation = gettext.translation('onionservices', '/usr/share/locale', fallback=True)
_ = translation.gettext
# Services class to import and access and service modules
import os
from collections import OrderedDict
import importlib.machinery
from onionkit.config import SERVICES_DIR
# Only required for type hints
from typing import TYPE_CHECKING, List, Dict
if TYPE_CHECKING:
from onionkit.service import OnionService
class DuplicateServiceError(Exception):
pass
# We define this only for type hints, service modules are actually just Python modules
class ServiceModule(object):
service_class = None
class Services(object):
_names = list()
_objects = list()
_objects_dict = OrderedDict()
_module_paths_dict = OrderedDict()
_modules_dict = OrderedDict()
@property
def names(self) -> List[str]:
return list(self.objects_dict.keys())
@property
def objects(self) -> List["OnionService"]:
return list(self.objects_dict.values())
@property
def objects_dict(self) -> Dict[str, "OnionService"]:
if not self._objects_dict:
self._objects_dict = self._get_objects_dict()
return self._objects_dict
def _get_objects_dict(self):
return {name: module.service_class() for name, module in self._get_modules_dict().items()}
def _get_modules_dict(self):
"""Import the modules in SERVICES_DIR"""
service_modules = OrderedDict()
for name, module_path in self._get_module_paths_dict().items():
source_file_loader = importlib.machinery.SourceFileLoader(name, module_path)
service_modules[name] = source_file_loader.load_module()
return service_modules
def _get_module_paths_dict(self):
module_paths = OrderedDict()
filenames = os.listdir(SERVICES_DIR)
filenames.sort()
for filename in filenames:
root = os.path.splitext(filename)[0]
name = os.path.basename(root)
if name.startswith("__"):
continue
if name in module_paths:
raise DuplicateServiceError("Multiple files for service %r" % root)
module_paths[name] = os.path.join(SERVICES_DIR, filename)
return module_paths
def get_service(self, name: str) -> "OnionService":
return self.objects_dict[name]
services = Services()
import argparse
import sys
from os import path
from collections import OrderedDict
from pydbus import SystemBus
from onionkit.config import BUS_NAME, SERVICES_PATH
class HelpfulParser(argparse.ArgumentParser):
"""Argument parser that prints the help message on error"""
def error(self, message):
self.print_help()
sys.stderr.write('\nerror: %s\n' % message)
sys.exit(2)
class ServiceAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
"""Verify that the specified service is exported by OnionKit"""
try:
SystemBus().get(BUS_NAME, path.join(SERVICES_PATH, values))
except KeyError:
parser.error("Could not find service %r" % values)
namespace.service = values
class OptionAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
"""Verify that the specified option is exported by OnionKit"""
def verify_option_name(name):
try:
SystemBus().get(BUS_NAME, path.join(SERVICES_PATH, namespace.service, name))
except KeyError:
parser.error("Could not find option %r for service %r" % (name, namespace.service))
if self.dest == "OPTION":
namespace.options = list()
for option_name in values:
verify_option_name(option_name)
namespace.options.append(option_name)
if self.dest == "ASSIGNMENT":
namespace.assignments = list()
for assignment in values:
try:
option_name, value = assignment.split("=")
verify_option_name(option_name)
namespace.assignments.append((option_name, value))
except ValueError:
parser.error("Invalid assignment %r" % assignment)
class CommandParser(argparse.ArgumentParser):
formatter_class = argparse.RawTextHelpFormatter
def add_service_command(self, command_name, *args, **kwargs):
subparser = self.subparsers.add_parser(command_name, *args, **kwargs)
subparser.add_argument(dest="SERVICE", action=ServiceAction, help="Name of the service")
return subparser
def __init__(self, *args, **kwargs):
kwargs["formatter_class"] = self.formatter_class
super().__init__(*args, **kwargs)
self.add_argument("--verbose", "-v", action="count")
self.add_argument("--log-file")
self.subparsers = self.add_subparsers(dest="command", parser_class=HelpfulParser)
# Add general commands
self.subparsers.add_parser("list", help="Print list of available services")
self.subparsers.add_parser("list-installed", help="Print list of installed services")
self.subparsers.add_parser("list-running", help="Print list of running services")
self.subparsers.add_parser("list-published", help="Print list of published services")
self.subparsers.add_parser("restore", help="Install packages and restore files of services which have the 'persistence' option set to True")
self.subparsers.add_parser("autostart", help="Enable the services which have the 'autostart' option set to True")
# Add service commands
self.add_service_command("info", help="Print information about the service")
self.add_service_command("status", help="Print whether the service is installed and running")
self.add_service_command("install", help="Install the service")
self.add_service_command("uninstall", help="Uninstall the service")
self.add_service_command("start", help="Starts the service")
self.add_service_command("stop", help="Stops the service")
# Add option commands
subparser = self.add_service_command("get-option", help="Print the current value of an option.\nExample: onionkitctl get-option mumble AllowLocalhost")
subparser.add_argument(dest="OPTION", nargs="+", action=OptionAction, help="Option name. Example: VirtualPort")
subparser = self.add_service_command("set-option", help="Set an option. If the service is running, the option will be applied immediately, and, if necessary, the service will be restarted.\nExample: onionkitctl set-option mumble ServerPassword=\"foo\"")
subparser.add_argument(dest="ASSIGNMENT", nargs="+", action=OptionAction, help="Option name and value. Example: VirtualPort=12345")
def parse_args(self, **kwargs):
args = super().parse_args(**kwargs)
if not any(vars(args).values()):
self.print_help()
self.exit()
return args
from os import path
APP_NAME = "onionkit"
BUS_NAME = "org.boum.tails.OnionKit"
BASE_PATH = "/org/boum/tails/OnionKit"
SERVICES_PATH = BASE_PATH
DATA_DIR = path.join("/usr/share", APP_NAME)
STATE_DIR = path.join("/var/lib/", APP_NAME)
SERVICES_DIR = path.join(DATA_DIR, "services")
TORRC = "/etc/tor/torrc"
TOR_SERVICE = "[email protected]"
TOR_CONTROL_PORT = 9051
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from hashlib import sha1
from base64 import b32encode, b64encode, b64decode
# The exponent is hard coded to 65537 in Tor (see tor/common/crypto.c)
PUBLIC_EXPONENT = 65537
# The key size is hard coded to 1024 in Tor (see tor/common/crypto.c)
KEY_SIZE = 1024
def generate_hs_private_key() -> str:
private_key = rsa.generate_private_key(
public_exponent=PUBLIC_EXPONENT,
key_size=KEY_SIZE,
backend=default_backend()
)
der = private_key.private_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
return b64encode(der).decode()
def derive_onion_address(hs_private_key: str) -> str:
"""Derive the onion address from a hidden service private key according to the torspec
See https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt#n526"""
der_private_key = b64decode(hs_private_key.encode())
private_key = serialization.load_der_private_key(
der_private_key,
backend=default_backend(),
password=None
)
public_key = private_key.public_key()
der_public_key = public_key.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.PKCS1
)
# Calculate SHA1 of the private key
m = sha1()
m.update(der_public_key)
digest = m.digest()
# Use only the first 80 bits (= 10 bytes)
truncated_digest = digest[:10]
# Generate base32 encoding
base32 = b32encode(truncated_digest)
address = base32.decode()[:16].lower() + ".onion"
return address
from logging import getLogger
from gi.repository import Gio
from onionkit.config import BUS_NAME
logger = getLogger(__name__)
class BusNameLostError(Exception):
pass
class Bus(object):
def __init__(self, name):
self.name = name
self.connection = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
self.connection.autoclose = True
self.owner_id = Gio.bus_own_name_on_connection(self.connection,
self.name,
Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT |
Gio.BusNameOwnerFlags.REPLACE,
self.on_name_acquired,
self.on_name_lost)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.teardown()
def teardown(self):
Gio.bus_unown_name(self.owner_id)
def on_name_acquired(self, connection: Gio.DBusConnection, name: str) -> None:
logger.info("Acquired bus name %r", name)
def on_name_lost(self, connection: Gio.DBusConnection, name: str) -> None:
raise BusNameLostError("Lost bus name %r" % name)
bus = Bus(BUS_NAME)
from logging import getLogger
from typing import Union
from threading import Thread
from abc import abstractmethod
import os
from gi.repository import Gio, GLib
from onionkit.dbus.util import option_to_variant
from onionkit.dbus.bus import bus
logger = getLogger(__name__)
class RegistrationFailedError(Exception):
pass
class DBusObject(object):
@property
@abstractmethod
def dbus_info(self):
pass
@property
@abstractmethod
def dbus_path(self):
pass
def __init__(self):
self.node_info = Gio.DBusNodeInfo.new_for_xml(self.dbus_info)
self.registered = False
def register(self):
logger.debug("Registering %r", self.dbus_path)
for interface in self.node_info.interfaces:
reg_id = bus.connection.register_object(self.dbus_path,
interface,
self.handle_method_call,
self.handle_get_property,
self.handle_set_property)
if not reg_id:
raise RegistrationFailedError("Failed to register interface %r of object %r" % (interface, self))
self.registered = True
def emit_signal(self, interface_name, signal_name, parameters=None, signature=None):
if not self.registered:
logger.debug("Could not emit signal %r: Object %r not registered with DBus", signal_name, self.__class__.__name__)
return
if parameters:
parameters = GLib.Variant("(%s)" % signature, (parameters,))
bus.connection.emit_signal(None, self.dbus_path, interface_name, signal_name, parameters)
def handle_method_call(self, *args, **kwargs) -> None:
thread = Thread(target=self.do_handle_method_call, args=args, kwargs=kwargs)
thread.start()
def do_handle_method_call(self,
connection: Gio.DBusConnection,
sender: str,
object_path: str,
interface_name: str,
method_name: str,
parameters: GLib.Variant,
invocation: Gio.DBusMethodInvocation,
user_data: Union[object, None] = None) -> None:
try:
logger.debug("Handling method call %s.%s%s", self.__class__.__name__, method_name, parameters)
method_info = self.node_info.lookup_interface(interface_name).lookup_method(method_name)
func = getattr(self, method_name)
result = func(*parameters)
if not method_info.out_args:
invocation.return_value(None)
return
if len(method_info.out_args) == 1:
result = (result,)
return_signature = "(%s)" % "".join(arg.signature for arg in method_info.out_args)
invocation.return_value(GLib.Variant(return_signature, result))
except Exception as e:
logger.exception(e)
invocation.return_dbus_error("python." + type(e).__name__, str(e))
def handle_get_property(self,
connection: Gio.DBusConnection,
sender: str,
object_path: str,
interface_name: str,
property_name: str,
user_data: Union[object, None] = None) -> GLib.Variant:
logger.debug("Handling property read of %s.%s", object_path, property_name)
try:
property_info = self.node_info.lookup_interface(interface_name).lookup_property(property_name)
attribute = getattr(self, property_name)
if isinstance(attribute, property):
value = attribute.fget(attribute)
else:
value = attribute
if interface_name == "org.boum.tails.OnionKit.Option" and property_name == "Value":
value = option_to_variant(value)
logger.debug("Converting value %r to Variant type %r", value, property_info.signature)
return GLib.Variant(property_info.signature, value)
except Exception as e:
logger.exception(e)
def handle_set_property(self,
connection: Gio.DBusConnection,
sender: str,
object_path: str,
interface_name: str,
property_name: str,
value: GLib.Variant,
# error: GLib.Error,
user_data: Union[object, None] = None) -> bool:
logger.debug("Handling property write of %s.%s", object_path, property_name)
setattr(self, property_name, value.unpack())
return True
class Signal(object):
def __init__(self, obj, name):
self.object = obj
self.name = name
self.dbus_signal = None
self.dbus_connection = None
def __call__(self, *args, **kwargs):
self.dbus_connection.emit_signal(None, path, interface_name, self.dbus_signal.name, parameters)
from typing import Any
from gi.repository import Gio, GLib
class InvalidOptionTypeError(Exception):
pass
def option_to_variant(value: Any) -> GLib.Variant:
if isinstance(value, str):
return GLib.Variant("s", value)
# It is important that we test bool before int, because isinstance(bool(), int) is True
if isinstance(value, bool):
return GLib.Variant("b", value)
if isinstance(value, int):
return GLib.Variant("i", value)
raise InvalidOptionTypeError("Unsupported option type %r" % type(value))
class UnknownOptionError(Exception):
pass
class ServiceAlreadyStartedError(Exception):
pass
class ServiceAlreadyStoppedError(Exception):
pass
class ServiceAlreadyInstalledError(Exception):
pass
class ServiceNotInstalledError(Exception):
pass
class TorIsNotRunningError(Exception):
pass
class InvalidStatusError(Exception):
pass
class ReadOnlyOptionError(Exception):
pass
class OptionNotInitializedError(Exception):
def __init__(self, option=None):
msg = "Option %r accessed before it was initialized" % option if option else None
super().__init__(msg)
import logging
import fcntl
from contextlib import contextmanager
@contextmanager
def open_locked(path, *args, **kwargs):
with open(path, *args, **kwargs) as fd:
try:
logging.log(5, "Acquiring file lock on %r", path)
fcntl.flock(fd, fcntl.LOCK_EX)
logging.log(5, "Acquired file lock on %r", path)
yield fd
finally:
logging.log(5, "Releasing file lock on %r", path)
fcntl.flock(fd, fcntl.LOCK_UN)
# XXX: Use an existing solution to modify config files, e.g. Ansible
def append_to_file(file_path, s):
with open_locked(file_path, 'a') as f:
f.write(s)
def prepend_to_file(file_path, s):
with open_locked(file_path, 'r') as original:
original_content = original.read()
with open_locked(file_path, 'w') as f:
f.write(s + original_content)
def append_line_if_not_present(file_path, line_):
with open_locked(file_path, 'r+') as f:
if line_ in f.readlines():
return False
f.write(line_)
return True
def remove_line_if_present(file_path, line_):
removed = False
with open_locked(file_path, 'r') as f:
lines = f.readlines()
for i, line in enumerate(lines):
if line == line_:
del lines[i]
removed = True
with open_locked(file_path, 'w+') as f:
f.writelines(lines)
return removed