mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-20 10:34:30 +00:00
Compare commits
15 Commits
4220511eb7
...
cf3bc4aae1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf3bc4aae1 | ||
|
|
2f53c4dd39 | ||
|
|
e26a4b71eb | ||
|
|
3f19c91007 | ||
|
|
80705b85af | ||
|
|
a70611a2e9 | ||
|
|
213d0330fd | ||
|
|
99c28b583f | ||
|
|
847de4d570 | ||
|
|
71a50e6d19 | ||
|
|
a6089664eb | ||
|
|
c47a856e3e | ||
|
|
72bcb93f56 | ||
|
|
f2edc6ab2b | ||
|
|
e100c89ecc |
18
Makefile
18
Makefile
@ -98,6 +98,7 @@ install:
|
||||
rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/COPYING.md && \
|
||||
rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/direct_url.json && \
|
||||
$(INSTALL) -D -t $(BIN_DIR) bin/plinth
|
||||
$(INSTALL) -D -t $(BIN_DIR) bin/freedombox-privileged
|
||||
|
||||
# Actions
|
||||
$(INSTALL) -D -t $(DESTDIR)/usr/share/plinth/actions actions/actions
|
||||
@ -150,6 +151,13 @@ clean:
|
||||
rm -rf Plinth.egg-info
|
||||
find plinth/locale -name *.mo -delete
|
||||
|
||||
define DEVELOP_SERVICE_CONF
|
||||
[Service]
|
||||
Environment=FREEDOMBOX_DEVELOP=1
|
||||
Environment=PYTHONPATH=/freedombox/
|
||||
endef
|
||||
export DEVELOP_SERVICE_CONF
|
||||
|
||||
# Run basic setup for a developer environment (VM or container)
|
||||
provision-dev:
|
||||
# Install newer build dependencies if any
|
||||
@ -159,9 +167,19 @@ provision-dev:
|
||||
# Install latest code over .deb
|
||||
$(MAKE) build install
|
||||
|
||||
# Configure privileged daemon for development setup
|
||||
mkdir -p /etc/systemd/system/freedombox-privileged.service.d/
|
||||
echo "$$DEVELOP_SERVICE_CONF" > /etc/systemd/system/freedombox-privileged.service.d/develop.conf
|
||||
|
||||
# Reload newer systemd units, ignore failure
|
||||
-systemctl daemon-reload
|
||||
|
||||
# Enable privileged daemon
|
||||
-systemctl stop freedombox-privileged.service
|
||||
|
||||
-test -d /run/systemd/system && \
|
||||
systemctl enable --now freedombox-privileged.socket
|
||||
|
||||
# Stop any ongoing upgrade, ignore failure
|
||||
-killall -9 unattended-upgr
|
||||
|
||||
|
||||
6
bin/freedombox-privileged
Executable file
6
bin/freedombox-privileged
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/python3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import plinth.privileged_daemon
|
||||
|
||||
plinth.privileged_daemon.main()
|
||||
21
data/usr/lib/systemd/system/freedombox-privileged.service
Normal file
21
data/usr/lib/systemd/system/freedombox-privileged.service
Normal file
@ -0,0 +1,21 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
[Unit]
|
||||
Description=FreedomBox Privileged Service
|
||||
Documentation=https://wiki.debian.org/FreedomBox/
|
||||
# Don't hit the start rate limiting.
|
||||
StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/freedombox-privileged
|
||||
TimeoutSec=300s
|
||||
User=root
|
||||
Group=root
|
||||
NotifyAccess=main
|
||||
PrivateTmp=yes
|
||||
Restart=on-failure
|
||||
# Don't restart too fast
|
||||
RestartSec=1
|
||||
RestartSteps=3
|
||||
RestartMaxDelaySec=5
|
||||
16
data/usr/lib/systemd/system/freedombox-privileged.socket
Normal file
16
data/usr/lib/systemd/system/freedombox-privileged.socket
Normal file
@ -0,0 +1,16 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
[Unit]
|
||||
Description=FreedomBox Privileged Service Socket
|
||||
Documentation=https://wiki.debian.org/FreedomBox/
|
||||
|
||||
[Socket]
|
||||
Accept=no
|
||||
ListenStream=/run/freedombox/privileged.socket
|
||||
SocketUser=root
|
||||
SocketGroup=root
|
||||
SocketMode=0666
|
||||
DirectoryMode=755
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
1
debian/control
vendored
1
debian/control
vendored
@ -50,7 +50,6 @@ Build-Depends:
|
||||
python3-requests,
|
||||
python3-ruamel.yaml,
|
||||
python3-setuptools,
|
||||
python3-setuptools-git,
|
||||
python3-systemd,
|
||||
python3-typeshed,
|
||||
python3-yaml,
|
||||
|
||||
3
debian/rules
vendored
3
debian/rules
vendored
@ -34,4 +34,5 @@ override_dh_installsystemd:
|
||||
# (as of debhelper 13.5.2) that still has hardcoded search path of
|
||||
# /lib/systemd/system for searching systemd services. See #987989 and
|
||||
# reversion of its changes.
|
||||
dh_installsystemd --tmpdir=debian/tmp/usr --package=freedombox plinth.service
|
||||
dh_installsystemd --tmpdir=debian/tmp/usr --package=freedombox \
|
||||
plinth.service freedombox-privileged.socket
|
||||
|
||||
@ -9,6 +9,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
@ -23,6 +24,8 @@ EXIT_PERM = 20
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
socket_path = '/run/freedombox/privileged.socket'
|
||||
|
||||
|
||||
# An alias for 'str' to mark some strings as sensitive. Sensitive strings are
|
||||
# not logged. Use 'type secret_str = str' when Python 3.11 support is no longer
|
||||
@ -70,10 +73,133 @@ def privileged(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
module_name = _get_privileged_action_module_name(func)
|
||||
action_name = func.__name__
|
||||
return _run_privileged_method(func, module_name, action_name, args,
|
||||
kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _run_privileged_method(func, module_name, action_name, args, kwargs):
|
||||
"""Execute a privileged method either using a server or sudo."""
|
||||
try:
|
||||
return _run_privileged_method_on_server(func, module_name, action_name,
|
||||
list(args), dict(kwargs))
|
||||
except (
|
||||
NotImplementedError, # For raw_output and run_as_user flags
|
||||
FileNotFoundError, # When the .socket file is not present
|
||||
ConnectionRefusedError, # When is daemon not running
|
||||
ConnectionResetError # When daemon fails permission check
|
||||
):
|
||||
return _run_privileged_method_as_process(func, module_name,
|
||||
action_name, args, kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
def _read_from_server(client_socket: socket.socket) -> bytes:
|
||||
"""Read everything from a socket and return the data."""
|
||||
response = b''
|
||||
while True:
|
||||
chunk = client_socket.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
response += chunk
|
||||
|
||||
return json.loads(response)
|
||||
|
||||
|
||||
def _request_to_server(request: dict) -> socket.socket:
|
||||
"""Connect to the server and make a request."""
|
||||
request_string = json.dumps(request)
|
||||
client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
client_socket.connect(socket_path)
|
||||
client_socket.sendall(request_string.encode('utf-8'))
|
||||
# Close the write end of the socket signaling an EOF and no more data
|
||||
# will be sent.
|
||||
client_socket.shutdown(socket.SHUT_WR)
|
||||
except Exception:
|
||||
client_socket.close()
|
||||
raise
|
||||
|
||||
return client_socket
|
||||
|
||||
|
||||
def _run_privileged_method_on_server(func, module_name, action_name, args,
|
||||
kwargs):
|
||||
"""Execute a privileged method using a server."""
|
||||
run_as_user = kwargs.pop('_run_as_user', None)
|
||||
run_in_background = kwargs.pop('_run_in_background', False)
|
||||
raw_output = kwargs.pop('_raw_output', False)
|
||||
log_error = kwargs.pop('_log_error', True)
|
||||
|
||||
if raw_output or run_as_user:
|
||||
raise NotImplementedError('Not yet implemented')
|
||||
|
||||
_log_action(func, module_name, action_name, args, kwargs, run_as_user,
|
||||
run_in_background, is_server=True)
|
||||
|
||||
request = {
|
||||
'module': module_name,
|
||||
'action': action_name,
|
||||
'args': args,
|
||||
'kwargs': kwargs
|
||||
}
|
||||
client_socket = _request_to_server(request)
|
||||
|
||||
args = (func, module_name, action_name, args, kwargs, log_error,
|
||||
client_socket)
|
||||
if not run_in_background:
|
||||
return _wait_for_server_response(*args)
|
||||
|
||||
read_thread = threading.Thread(target=_wait_for_server_response, args=args)
|
||||
read_thread.start()
|
||||
|
||||
|
||||
def _wait_for_server_response(func, module_name, action_name, args, kwargs,
|
||||
log_error, client_socket):
|
||||
"""Wait for the server to respond and process the response."""
|
||||
try:
|
||||
return_value = _read_from_server(client_socket)
|
||||
except json.JSONDecodeError:
|
||||
logger.error('Error decoding action return value %s..%s(*%s, **%s)',
|
||||
module_name, action_name, args, kwargs)
|
||||
raise
|
||||
finally:
|
||||
client_socket.close()
|
||||
|
||||
if return_value['result'] == 'success':
|
||||
return return_value['return']
|
||||
|
||||
module = importlib.import_module(return_value['exception']['module'])
|
||||
exception_class = getattr(module, return_value['exception']['name'])
|
||||
exception = exception_class(*return_value['exception']['args'])
|
||||
exception.stdout = b''
|
||||
exception.stderr = b''
|
||||
|
||||
def _get_html_message():
|
||||
"""Return an HTML format error that can be shown in messages."""
|
||||
from django.utils.html import format_html
|
||||
|
||||
formatted_args = _format_args(func, args, kwargs)
|
||||
exception_args, stdout, stderr, traceback = _format_error(
|
||||
exception, return_value)
|
||||
return format_html('Error running action: {}..{}({}): {}({})\n{}{}{}',
|
||||
module_name, action_name, formatted_args,
|
||||
return_value['exception']['name'], exception_args,
|
||||
stdout, stderr, traceback)
|
||||
|
||||
exception.get_html_message = _get_html_message
|
||||
|
||||
if log_error:
|
||||
formatted_args = _format_args(func, args, kwargs)
|
||||
exception_args, stdout, stderr, traceback = _format_error(
|
||||
exception, return_value)
|
||||
logger.error('Error running action %s..%s(%s): %s(%s)\n'
|
||||
'%s%s%s', module_name, action_name, formatted_args,
|
||||
return_value['exception']['name'], exception_args, stdout,
|
||||
stderr, traceback)
|
||||
|
||||
raise exception
|
||||
|
||||
|
||||
def _run_privileged_method_as_process(func, module_name, action_name, args,
|
||||
@ -113,7 +239,7 @@ def _run_privileged_method_as_process(func, module_name, action_name, args,
|
||||
proc_kwargs['env'] = {'PYTHONPATH': cfg.file_root}
|
||||
|
||||
_log_action(func, module_name, action_name, args, kwargs, run_as_user,
|
||||
run_in_background)
|
||||
run_in_background, is_server=False)
|
||||
|
||||
proc = subprocess.Popen(command, **proc_kwargs)
|
||||
os.close(write_fd)
|
||||
@ -297,9 +423,13 @@ def _get_privileged_action_module_name(func):
|
||||
|
||||
|
||||
def _log_action(func, module_name, action_name, args, kwargs, run_as_user,
|
||||
run_in_background):
|
||||
run_in_background, is_server):
|
||||
"""Log an action in a compact format."""
|
||||
if is_server:
|
||||
prompt = '»'
|
||||
else:
|
||||
prompt = f'({run_as_user})$' if run_as_user else '#'
|
||||
|
||||
suffix = '&' if run_in_background else ''
|
||||
formatted_args = _format_args(func, args, kwargs)
|
||||
logger.info('%s %s..%s(%s) %s', prompt, module_name, action_name,
|
||||
@ -359,6 +489,54 @@ def privileged_main():
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def privileged_handle_json_request(request_string: str) -> str:
|
||||
"""Parse arguments for the program spawned as a privileged action."""
|
||||
|
||||
def _parse_request() -> dict:
|
||||
"""Return a JSON parsed and validated request."""
|
||||
try:
|
||||
request = json.loads(request_string)
|
||||
except json.JSONDecodeError:
|
||||
raise SyntaxError('Invalid JSON in request')
|
||||
|
||||
required_parameters = [('module', str), ('action', str),
|
||||
('args', list), ('kwargs', dict)]
|
||||
|
||||
for parameter, expected_type in required_parameters:
|
||||
if parameter not in request:
|
||||
raise TypeError(f'Missing required parameter "{parameter}"')
|
||||
if not isinstance(request[parameter], expected_type):
|
||||
raise TypeError(f'Parameter "{parameter}" must be of type'
|
||||
f'{expected_type.__name__}')
|
||||
|
||||
return request
|
||||
|
||||
try:
|
||||
request = _parse_request()
|
||||
logger.info('Received request for %s..%s(..)', request['module'],
|
||||
request['action'])
|
||||
arguments = {'args': request['args'], 'kwargs': request['kwargs']}
|
||||
return_value = _privileged_call(request['module'], request['action'],
|
||||
arguments)
|
||||
except (PermissionError, SyntaxError, TypeError, Exception) as exception:
|
||||
if isinstance(exception, (PermissionError, SyntaxError, TypeError)):
|
||||
logger.error(exception.args[0])
|
||||
else:
|
||||
logger.exception(exception)
|
||||
|
||||
return_value = {
|
||||
'result': 'exception',
|
||||
'exception': {
|
||||
'module': type(exception).__module__,
|
||||
'name': type(exception).__name__,
|
||||
'args': exception.args,
|
||||
'traceback': traceback.format_tb(exception.__traceback__)
|
||||
}
|
||||
}
|
||||
|
||||
return json.dumps(return_value, cls=JSONEncoder)
|
||||
|
||||
|
||||
def _privileged_call(module_name, action_name, arguments):
|
||||
"""Import the module and run action as superuser"""
|
||||
if '.' in module_name:
|
||||
@ -368,7 +546,10 @@ def _privileged_call(module_name, action_name, arguments):
|
||||
if module_name == 'plinth':
|
||||
import_path = 'plinth'
|
||||
else:
|
||||
try:
|
||||
import_path = module_loader.get_module_import_path(module_name)
|
||||
except FileNotFoundError as exception:
|
||||
raise SyntaxError('Specified module not found') from exception
|
||||
|
||||
try:
|
||||
module = importlib.import_module(import_path + '.privileged')
|
||||
|
||||
@ -596,6 +596,9 @@ class EnableState(LeaderComponent):
|
||||
|
||||
def apps_init():
|
||||
"""Create apps by constructing them with components."""
|
||||
if App.list():
|
||||
return # Apps have already been initialized
|
||||
|
||||
from . import module_loader # noqa # Avoid circular import
|
||||
for module_name, module in module_loader.loaded_modules.items():
|
||||
_initialize_module(module_name, module)
|
||||
|
||||
@ -6,6 +6,7 @@ pytest configuration for all tests.
|
||||
import importlib
|
||||
import os
|
||||
import pathlib
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
@ -109,6 +110,17 @@ def fixture_needs_sudo():
|
||||
pytest.skip('Needs sudo command installed.')
|
||||
|
||||
|
||||
@pytest.fixture(name='no_privileged_server', scope='module')
|
||||
def fixture_no_privileged__server():
|
||||
"""Don't setup for and run privileged methods on server.
|
||||
|
||||
Tests on using privileged daemon are not yet implemented.
|
||||
"""
|
||||
with patch('plinth.actions._run_privileged_method_on_server') as mock:
|
||||
mock.side_effect = NotImplementedError
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def splinter_selenium_implicit_wait():
|
||||
"""Disable implicit waiting."""
|
||||
|
||||
@ -7,6 +7,7 @@ import importlib
|
||||
import logging
|
||||
import pathlib
|
||||
import re
|
||||
import types
|
||||
|
||||
import django
|
||||
|
||||
@ -15,7 +16,7 @@ from plinth.signals import pre_module_loading
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
loaded_modules = dict()
|
||||
loaded_modules: dict[str, types.ModuleType] = dict()
|
||||
_modules_to_load = None
|
||||
|
||||
|
||||
@ -31,6 +32,9 @@ def load_modules():
|
||||
Read names of enabled modules in modules/enabled directory and
|
||||
import them from modules directory.
|
||||
"""
|
||||
if loaded_modules:
|
||||
return # Modules have already been loaded
|
||||
|
||||
pre_module_loading.send_robust(sender="module_loader")
|
||||
for module_import_path in get_modules_to_load():
|
||||
module_name = module_import_path.split('.')[-1]
|
||||
|
||||
@ -15,7 +15,8 @@ from plinth.modules.backups import privileged
|
||||
from plinth.modules.backups.repository import BorgRepository, SshBorgRepository
|
||||
from plinth.tests import config as test_config
|
||||
|
||||
pytestmark = pytest.mark.usefixtures('needs_root', 'needs_borg', 'load_cfg')
|
||||
pytestmark = pytest.mark.usefixtures('needs_root', 'needs_borg', 'load_cfg',
|
||||
'no_privileged_server')
|
||||
|
||||
# try to access a non-existing url and a URL that exists but does not
|
||||
# grant access
|
||||
|
||||
@ -194,8 +194,11 @@ class Packages(app_module.FollowerComponent):
|
||||
# Ensure package list is update-to-date before looking at dependencies.
|
||||
refresh_package_lists()
|
||||
|
||||
# List of packages to purge from the system
|
||||
packages = self.get_actual_packages()
|
||||
# List of packages to purge from the system. Be resilient to a package
|
||||
# not being found in apt's cache by not using get_actual_packages(). If
|
||||
# a package expression is listed as (package1 | package2) then try to
|
||||
# remove both packages.
|
||||
packages = self.possible_packages
|
||||
logger.info('App\'s list of packages to remove: %s', packages)
|
||||
|
||||
packages = self._filter_packages_to_keep(packages)
|
||||
@ -304,7 +307,7 @@ class Packages(app_module.FollowerComponent):
|
||||
|
||||
# Remove packages used by other installed apps
|
||||
for component in app.get_components_of_type(Packages):
|
||||
keep_packages |= set(component.get_actual_packages())
|
||||
keep_packages |= set(component.possible_packages)
|
||||
|
||||
# Get list of all the dependencies of packages to keep.
|
||||
keep_packages_with_deps: set[str] = set()
|
||||
|
||||
248
plinth/privileged_daemon.py
Normal file
248
plinth/privileged_daemon.py
Normal file
@ -0,0 +1,248 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""The main method for a daemon that runs privileged methods."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import pwd
|
||||
import socket
|
||||
import socketserver
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import systemd.daemon
|
||||
|
||||
from . import __version__, actions
|
||||
from . import app as app_module
|
||||
from . import log, module_loader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
address = pathlib.Path('/run/freedombox/privileged.socket')
|
||||
|
||||
FREEDOMBOX_PROCESS_USER = 'plinth'
|
||||
|
||||
MAX_REQUEST_LENGTH = 1_000_000
|
||||
|
||||
idle_shutdown_time: int | None = 5 * 60 # 5 minutes
|
||||
|
||||
freedombox_develop = False
|
||||
|
||||
|
||||
class RequestHandler(socketserver.StreamRequestHandler):
|
||||
"""Handle a single streaming request.
|
||||
|
||||
It is instantiated once per connection to the server. The overridden
|
||||
handle() method implements communication with the newly connected client.
|
||||
"""
|
||||
|
||||
def _read_request(self) -> str:
|
||||
"""Return a single request read from the client."""
|
||||
request_data = self.rfile.read(MAX_REQUEST_LENGTH + 1)
|
||||
if len(request_data) > MAX_REQUEST_LENGTH:
|
||||
raise ValueError('Request too large')
|
||||
|
||||
try:
|
||||
request = request_data.decode('utf-8')
|
||||
except UnicodeError:
|
||||
raise ValueError('Invalid Unicode in request')
|
||||
|
||||
return request
|
||||
|
||||
def _write_response(self, response: str):
|
||||
"""Write a single response to the client."""
|
||||
self.wfile.write(response.encode('utf-8'))
|
||||
|
||||
def handle(self) -> None:
|
||||
"""Handle a new connection from a client."""
|
||||
try:
|
||||
request = self._read_request()
|
||||
response_string = actions.privileged_handle_json_request(request)
|
||||
except Exception as exception:
|
||||
logger.exception('Error running privileged request: %s', exception)
|
||||
response = {
|
||||
'result': 'exception',
|
||||
'exception': {
|
||||
'module': type(exception).__module__,
|
||||
'name': type(exception).__name__,
|
||||
'args': exception.args,
|
||||
'traceback': traceback.format_tb(exception.__traceback__)
|
||||
}
|
||||
}
|
||||
response_string = json.dumps(response)
|
||||
|
||||
self._write_response(response_string)
|
||||
|
||||
|
||||
class Server(socketserver.ThreadingUnixStreamServer):
|
||||
"""Server to handle privileged request.
|
||||
|
||||
Requests from any process with UID other than from FreedomBox daemon user
|
||||
or root will be denied.
|
||||
|
||||
If the server does not receive a request for idle_shutdown_time seconds and
|
||||
no requests are being processed, then serve_forever() will raise
|
||||
TimeoutError (so that the program can catch it and exit).
|
||||
|
||||
If the daemon is spawned by systemd socket activation, then systemd
|
||||
provided socket is re-used (no bind() and listen() calls are made on it)
|
||||
and it will not be closed after the server is shutdown. If a daemon is
|
||||
spawned without socket activation, unix socket file is created with
|
||||
appropriate permissions after the parent directory is created. socket
|
||||
object is created, bind() and listen() calls will be made on the socket.
|
||||
When server is shutdown, close() is called on the socket.
|
||||
"""
|
||||
|
||||
def __init__(self, address, *args, **kwargs):
|
||||
"""Initialize the server."""
|
||||
# Retrieve FreedomBox process UID
|
||||
user_info = pwd.getpwnam(FREEDOMBOX_PROCESS_USER)
|
||||
self.allowed_peer_uids = [0, user_info.pw_uid]
|
||||
|
||||
# Used to auto-shutdown service
|
||||
self.last_request_time = time.time()
|
||||
|
||||
self.listen_fd = self._get_listen_fd()
|
||||
if self.listen_fd:
|
||||
logger.info('systemd socket activated.')
|
||||
self.socket = socket.fromfd(self.listen_fd, socket.AF_UNIX,
|
||||
socket.SOCK_STREAM)
|
||||
self.server_address = self.socket.getsockname()
|
||||
super(socketserver.TCPServer,
|
||||
self).__init__(address, *args, **kwargs)
|
||||
else:
|
||||
logger.info('systemd service activated.')
|
||||
address_path = pathlib.Path(address)
|
||||
address_path.parent.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
address_path.unlink(missing_ok=True)
|
||||
super().__init__(address, *args, **kwargs)
|
||||
address_path.chmod(0o666) # All users are allowed to connect
|
||||
|
||||
def server_bind(self):
|
||||
"""Called by constructor to activate the server.
|
||||
|
||||
Do nothing if socket-activated by systemd.
|
||||
"""
|
||||
if not self.listen_fd:
|
||||
super().server_bind()
|
||||
|
||||
def server_activate(self):
|
||||
"""Called by constructor to activate the server.
|
||||
|
||||
Do nothing if socket-activated by systemd.
|
||||
"""
|
||||
if not self.listen_fd:
|
||||
super().server_activate()
|
||||
|
||||
def server_close(self):
|
||||
"""Called to clean-up the server.
|
||||
|
||||
Don't close the socket if socket-activated by systemd.
|
||||
"""
|
||||
if not self.listen_fd:
|
||||
# Not called with systemd socket activation. We are responsible for
|
||||
# cleaning up the socket file we created.
|
||||
super().server_close()
|
||||
pathlib.Path(str(self.server_address)).unlink(missing_ok=True)
|
||||
else:
|
||||
# Don't close the socket, systemd is still using it for next
|
||||
# invocation.
|
||||
pass
|
||||
|
||||
def service_actions(self):
|
||||
"""Called from serve_forever() loop. Shutdown service if unused."""
|
||||
super().service_actions()
|
||||
|
||||
if idle_shutdown_time is None:
|
||||
return
|
||||
|
||||
if time.time() - self.last_request_time < idle_shutdown_time:
|
||||
return
|
||||
|
||||
if not isinstance(self._threads, list):
|
||||
return
|
||||
|
||||
if any(thread.is_alive() for thread in self._threads):
|
||||
return
|
||||
|
||||
# Raise an exception in the serve_forever() loop.
|
||||
raise TimeoutError()
|
||||
|
||||
def _get_listen_fd(self):
|
||||
"""Return the listening socket from systemd socket activation."""
|
||||
listen_fds = systemd.daemon.listen_fds(unset_environment=True)
|
||||
if len(listen_fds) == 0:
|
||||
# Activated without socket activation.
|
||||
return None
|
||||
|
||||
if len(listen_fds) > 1:
|
||||
# Activated with multiple listening sockets. We didn't configure
|
||||
# our .socket unit like this. This is unexpected.
|
||||
return None
|
||||
|
||||
listen_fd = listen_fds[0]
|
||||
if not systemd.daemon.is_socket_unix(listen_fd, socket.SOCK_STREAM,
|
||||
listening=1):
|
||||
# Socket is not a AF_UNIX socket, it is not SOCK_STREAM socket, or
|
||||
# listen() has not been called on it. This is unexpected.
|
||||
return None
|
||||
|
||||
return listen_fd
|
||||
|
||||
def verify_request(self, request, client_address) -> bool:
|
||||
"""Return False if the request must be denied."""
|
||||
creds = request.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED,
|
||||
struct.calcsize('3i'))
|
||||
_, uid, _ = struct.unpack('3i', creds)
|
||||
if uid not in self.allowed_peer_uids:
|
||||
return False
|
||||
|
||||
self.last_request_time = time.time()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Start the server, listen on socket, and serve forever."""
|
||||
global freedombox_develop, idle_shutdown_time
|
||||
|
||||
log.action_init()
|
||||
|
||||
logger.info('FreedomBox privileged daemon: %s', __version__)
|
||||
|
||||
if os.getenv('FREEDOMBOX_DEVELOP', '') == '1':
|
||||
freedombox_develop = True
|
||||
idle_shutdown_time = 5
|
||||
logger.info('Running development mode, idle shutdown time = %ss',
|
||||
idle_shutdown_time)
|
||||
|
||||
# When invoked as a systemd service, don't perform automatic idle shutdown.
|
||||
if not systemd.daemon.listen_fds(unset_environment=False):
|
||||
idle_shutdown_time = None
|
||||
|
||||
module_loader.load_modules()
|
||||
app_module.apps_init()
|
||||
|
||||
with Server(str(address), RequestHandler) as server:
|
||||
# systemd will wait until notification to proceed with other processes.
|
||||
# We have service Type=notify.
|
||||
systemd.daemon.notify('READY=1')
|
||||
|
||||
try:
|
||||
server.serve_forever()
|
||||
except TimeoutError:
|
||||
logger.info('FreedomBox privileged daemon exiting on idle.')
|
||||
except Exception as exception:
|
||||
logger.exception(
|
||||
'FreedomBox privileged daemon exiting on error - %s',
|
||||
exception)
|
||||
sys.exit(-1)
|
||||
else:
|
||||
logger.info('FreedomBox privileged daemon exiting.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -20,6 +20,8 @@ from plinth.actions import privileged, secret_str
|
||||
|
||||
actions_name = 'actions'
|
||||
|
||||
pytestmark = pytest.mark.usefixtures('no_privileged_server')
|
||||
|
||||
|
||||
@pytest.fixture(name='popen')
|
||||
def fixture_popen():
|
||||
|
||||
@ -31,7 +31,7 @@ from plinth.modules.firewall.components import get_port_forwarding_info
|
||||
from plinth.package import Packages
|
||||
from plinth.translation import get_language_from_request, set_language
|
||||
|
||||
from . import forms, frontpage, operation, package, setup
|
||||
from . import forms, frontpage, operation, setup
|
||||
|
||||
REDIRECT_FIELD_NAME = 'next'
|
||||
|
||||
@ -537,11 +537,6 @@ class SetupView(TemplateView):
|
||||
response.status_code = 303
|
||||
return response
|
||||
|
||||
if 'refresh-packages' in request.POST:
|
||||
# Refresh apt package lists
|
||||
package.refresh_package_lists()
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@ -103,6 +103,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project.scripts]
|
||||
plinth = "plinth.__main__:main"
|
||||
freedombox-privileged = "plinth.privileged_daemon:main"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "plinth.__version__"}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user