mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-27 10:44:33 +00:00
Compare commits
No commits in common. "cf3bc4aae15cea1fc9d053c79afa4b30404c6957" and "4220511eb76762864a74f805721f14869b368a13" have entirely different histories.
cf3bc4aae1
...
4220511eb7
18
Makefile
18
Makefile
@ -98,7 +98,6 @@ install:
|
|||||||
rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/COPYING.md && \
|
rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/COPYING.md && \
|
||||||
rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/direct_url.json && \
|
rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/direct_url.json && \
|
||||||
$(INSTALL) -D -t $(BIN_DIR) bin/plinth
|
$(INSTALL) -D -t $(BIN_DIR) bin/plinth
|
||||||
$(INSTALL) -D -t $(BIN_DIR) bin/freedombox-privileged
|
|
||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
$(INSTALL) -D -t $(DESTDIR)/usr/share/plinth/actions actions/actions
|
$(INSTALL) -D -t $(DESTDIR)/usr/share/plinth/actions actions/actions
|
||||||
@ -151,13 +150,6 @@ clean:
|
|||||||
rm -rf Plinth.egg-info
|
rm -rf Plinth.egg-info
|
||||||
find plinth/locale -name *.mo -delete
|
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)
|
# Run basic setup for a developer environment (VM or container)
|
||||||
provision-dev:
|
provision-dev:
|
||||||
# Install newer build dependencies if any
|
# Install newer build dependencies if any
|
||||||
@ -167,19 +159,9 @@ provision-dev:
|
|||||||
# Install latest code over .deb
|
# Install latest code over .deb
|
||||||
$(MAKE) build install
|
$(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
|
# Reload newer systemd units, ignore failure
|
||||||
-systemctl daemon-reload
|
-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
|
# Stop any ongoing upgrade, ignore failure
|
||||||
-killall -9 unattended-upgr
|
-killall -9 unattended-upgr
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import plinth.privileged_daemon
|
|
||||||
|
|
||||||
plinth.privileged_daemon.main()
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
# 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,6 +50,7 @@ Build-Depends:
|
|||||||
python3-requests,
|
python3-requests,
|
||||||
python3-ruamel.yaml,
|
python3-ruamel.yaml,
|
||||||
python3-setuptools,
|
python3-setuptools,
|
||||||
|
python3-setuptools-git,
|
||||||
python3-systemd,
|
python3-systemd,
|
||||||
python3-typeshed,
|
python3-typeshed,
|
||||||
python3-yaml,
|
python3-yaml,
|
||||||
|
|||||||
3
debian/rules
vendored
3
debian/rules
vendored
@ -34,5 +34,4 @@ override_dh_installsystemd:
|
|||||||
# (as of debhelper 13.5.2) that still has hardcoded search path of
|
# (as of debhelper 13.5.2) that still has hardcoded search path of
|
||||||
# /lib/systemd/system for searching systemd services. See #987989 and
|
# /lib/systemd/system for searching systemd services. See #987989 and
|
||||||
# reversion of its changes.
|
# reversion of its changes.
|
||||||
dh_installsystemd --tmpdir=debian/tmp/usr --package=freedombox \
|
dh_installsystemd --tmpdir=debian/tmp/usr --package=freedombox plinth.service
|
||||||
plinth.service freedombox-privileged.socket
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import socket
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@ -24,8 +23,6 @@ EXIT_PERM = 20
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
socket_path = '/run/freedombox/privileged.socket'
|
|
||||||
|
|
||||||
|
|
||||||
# An alias for 'str' to mark some strings as sensitive. Sensitive strings are
|
# 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
|
# not logged. Use 'type secret_str = str' when Python 3.11 support is no longer
|
||||||
@ -73,133 +70,10 @@ def privileged(func):
|
|||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
module_name = _get_privileged_action_module_name(func)
|
module_name = _get_privileged_action_module_name(func)
|
||||||
action_name = func.__name__
|
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,
|
return _run_privileged_method_as_process(func, module_name,
|
||||||
action_name, args, kwargs)
|
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,
|
def _run_privileged_method_as_process(func, module_name, action_name, args,
|
||||||
@ -239,7 +113,7 @@ def _run_privileged_method_as_process(func, module_name, action_name, args,
|
|||||||
proc_kwargs['env'] = {'PYTHONPATH': cfg.file_root}
|
proc_kwargs['env'] = {'PYTHONPATH': cfg.file_root}
|
||||||
|
|
||||||
_log_action(func, module_name, action_name, args, kwargs, run_as_user,
|
_log_action(func, module_name, action_name, args, kwargs, run_as_user,
|
||||||
run_in_background, is_server=False)
|
run_in_background)
|
||||||
|
|
||||||
proc = subprocess.Popen(command, **proc_kwargs)
|
proc = subprocess.Popen(command, **proc_kwargs)
|
||||||
os.close(write_fd)
|
os.close(write_fd)
|
||||||
@ -423,13 +297,9 @@ def _get_privileged_action_module_name(func):
|
|||||||
|
|
||||||
|
|
||||||
def _log_action(func, module_name, action_name, args, kwargs, run_as_user,
|
def _log_action(func, module_name, action_name, args, kwargs, run_as_user,
|
||||||
run_in_background, is_server):
|
run_in_background):
|
||||||
"""Log an action in a compact format."""
|
"""Log an action in a compact format."""
|
||||||
if is_server:
|
prompt = f'({run_as_user})$' if run_as_user else '#'
|
||||||
prompt = '»'
|
|
||||||
else:
|
|
||||||
prompt = f'({run_as_user})$' if run_as_user else '#'
|
|
||||||
|
|
||||||
suffix = '&' if run_in_background else ''
|
suffix = '&' if run_in_background else ''
|
||||||
formatted_args = _format_args(func, args, kwargs)
|
formatted_args = _format_args(func, args, kwargs)
|
||||||
logger.info('%s %s..%s(%s) %s', prompt, module_name, action_name,
|
logger.info('%s %s..%s(%s) %s', prompt, module_name, action_name,
|
||||||
@ -489,54 +359,6 @@ def privileged_main():
|
|||||||
sys.exit(1)
|
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):
|
def _privileged_call(module_name, action_name, arguments):
|
||||||
"""Import the module and run action as superuser"""
|
"""Import the module and run action as superuser"""
|
||||||
if '.' in module_name:
|
if '.' in module_name:
|
||||||
@ -546,10 +368,7 @@ def _privileged_call(module_name, action_name, arguments):
|
|||||||
if module_name == 'plinth':
|
if module_name == 'plinth':
|
||||||
import_path = 'plinth'
|
import_path = 'plinth'
|
||||||
else:
|
else:
|
||||||
try:
|
import_path = module_loader.get_module_import_path(module_name)
|
||||||
import_path = module_loader.get_module_import_path(module_name)
|
|
||||||
except FileNotFoundError as exception:
|
|
||||||
raise SyntaxError('Specified module not found') from exception
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module(import_path + '.privileged')
|
module = importlib.import_module(import_path + '.privileged')
|
||||||
|
|||||||
@ -596,9 +596,6 @@ class EnableState(LeaderComponent):
|
|||||||
|
|
||||||
def apps_init():
|
def apps_init():
|
||||||
"""Create apps by constructing them with components."""
|
"""Create apps by constructing them with components."""
|
||||||
if App.list():
|
|
||||||
return # Apps have already been initialized
|
|
||||||
|
|
||||||
from . import module_loader # noqa # Avoid circular import
|
from . import module_loader # noqa # Avoid circular import
|
||||||
for module_name, module in module_loader.loaded_modules.items():
|
for module_name, module in module_loader.loaded_modules.items():
|
||||||
_initialize_module(module_name, module)
|
_initialize_module(module_name, module)
|
||||||
|
|||||||
@ -6,7 +6,6 @@ pytest configuration for all tests.
|
|||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -110,17 +109,6 @@ def fixture_needs_sudo():
|
|||||||
pytest.skip('Needs sudo command installed.')
|
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')
|
@pytest.fixture(scope='session')
|
||||||
def splinter_selenium_implicit_wait():
|
def splinter_selenium_implicit_wait():
|
||||||
"""Disable implicit waiting."""
|
"""Disable implicit waiting."""
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import importlib
|
|||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import types
|
|
||||||
|
|
||||||
import django
|
import django
|
||||||
|
|
||||||
@ -16,7 +15,7 @@ from plinth.signals import pre_module_loading
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
loaded_modules: dict[str, types.ModuleType] = dict()
|
loaded_modules = dict()
|
||||||
_modules_to_load = None
|
_modules_to_load = None
|
||||||
|
|
||||||
|
|
||||||
@ -32,9 +31,6 @@ def load_modules():
|
|||||||
Read names of enabled modules in modules/enabled directory and
|
Read names of enabled modules in modules/enabled directory and
|
||||||
import them from modules directory.
|
import them from modules directory.
|
||||||
"""
|
"""
|
||||||
if loaded_modules:
|
|
||||||
return # Modules have already been loaded
|
|
||||||
|
|
||||||
pre_module_loading.send_robust(sender="module_loader")
|
pre_module_loading.send_robust(sender="module_loader")
|
||||||
for module_import_path in get_modules_to_load():
|
for module_import_path in get_modules_to_load():
|
||||||
module_name = module_import_path.split('.')[-1]
|
module_name = module_import_path.split('.')[-1]
|
||||||
|
|||||||
@ -15,8 +15,7 @@ from plinth.modules.backups import privileged
|
|||||||
from plinth.modules.backups.repository import BorgRepository, SshBorgRepository
|
from plinth.modules.backups.repository import BorgRepository, SshBorgRepository
|
||||||
from plinth.tests import config as test_config
|
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
|
# try to access a non-existing url and a URL that exists but does not
|
||||||
# grant access
|
# grant access
|
||||||
|
|||||||
@ -194,11 +194,8 @@ class Packages(app_module.FollowerComponent):
|
|||||||
# Ensure package list is update-to-date before looking at dependencies.
|
# Ensure package list is update-to-date before looking at dependencies.
|
||||||
refresh_package_lists()
|
refresh_package_lists()
|
||||||
|
|
||||||
# List of packages to purge from the system. Be resilient to a package
|
# List of packages to purge from the system
|
||||||
# not being found in apt's cache by not using get_actual_packages(). If
|
packages = self.get_actual_packages()
|
||||||
# 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)
|
logger.info('App\'s list of packages to remove: %s', packages)
|
||||||
|
|
||||||
packages = self._filter_packages_to_keep(packages)
|
packages = self._filter_packages_to_keep(packages)
|
||||||
@ -307,7 +304,7 @@ class Packages(app_module.FollowerComponent):
|
|||||||
|
|
||||||
# Remove packages used by other installed apps
|
# Remove packages used by other installed apps
|
||||||
for component in app.get_components_of_type(Packages):
|
for component in app.get_components_of_type(Packages):
|
||||||
keep_packages |= set(component.possible_packages)
|
keep_packages |= set(component.get_actual_packages())
|
||||||
|
|
||||||
# Get list of all the dependencies of packages to keep.
|
# Get list of all the dependencies of packages to keep.
|
||||||
keep_packages_with_deps: set[str] = set()
|
keep_packages_with_deps: set[str] = set()
|
||||||
|
|||||||
@ -1,248 +0,0 @@
|
|||||||
# 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,8 +20,6 @@ from plinth.actions import privileged, secret_str
|
|||||||
|
|
||||||
actions_name = 'actions'
|
actions_name = 'actions'
|
||||||
|
|
||||||
pytestmark = pytest.mark.usefixtures('no_privileged_server')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name='popen')
|
@pytest.fixture(name='popen')
|
||||||
def fixture_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.package import Packages
|
||||||
from plinth.translation import get_language_from_request, set_language
|
from plinth.translation import get_language_from_request, set_language
|
||||||
|
|
||||||
from . import forms, frontpage, operation, setup
|
from . import forms, frontpage, operation, package, setup
|
||||||
|
|
||||||
REDIRECT_FIELD_NAME = 'next'
|
REDIRECT_FIELD_NAME = 'next'
|
||||||
|
|
||||||
@ -537,6 +537,11 @@ class SetupView(TemplateView):
|
|||||||
response.status_code = 303
|
response.status_code = 303
|
||||||
return response
|
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)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -103,7 +103,6 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
plinth = "plinth.__main__:main"
|
plinth = "plinth.__main__:main"
|
||||||
freedombox-privileged = "plinth.privileged_daemon:main"
|
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
version = {attr = "plinth.__version__"}
|
version = {attr = "plinth.__version__"}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user