actions, backup: Implement raw output for privileged daemon

- Regression: downloading does not work with sudo based action anymore. However,
sudo based actions are to be removed in later patches.

Tests:

- Downloading tar backup archive works. Untar works. Downloading gives upto
10MiB/s speed.

- If API is not called with _raw_output=True, then special exception is raised.

- Downloading tar file from command line using nc also works.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Joseph Nuthalapati <njoseph@riseup.net>
This commit is contained in:
Sunil Mohan Adapa 2025-08-12 16:22:33 -07:00 committed by Joseph Nuthalapati
parent 0aa3ee5a70
commit 143e4a00bd
No known key found for this signature in database
GPG Key ID: 5398F00A2FA43C35
3 changed files with 47 additions and 11 deletions

View File

@ -86,7 +86,6 @@ def _run_privileged_method(func, module_name, action_name, args, kwargs):
return _run_privileged_method_on_server(func, module_name, action_name, return _run_privileged_method_on_server(func, module_name, action_name,
list(args), dict(kwargs)) list(args), dict(kwargs))
except ( except (
NotImplementedError, # For raw_output flag
FileNotFoundError, # When the .socket file is not present FileNotFoundError, # When the .socket file is not present
ConnectionRefusedError, # When is daemon not running ConnectionRefusedError, # When is daemon not running
ConnectionResetError # When daemon fails permission check ConnectionResetError # When daemon fails permission check
@ -132,9 +131,6 @@ def _run_privileged_method_on_server(func, module_name, action_name, args,
raw_output = kwargs.pop('_raw_output', False) raw_output = kwargs.pop('_raw_output', False)
log_error = kwargs.pop('_log_error', True) log_error = kwargs.pop('_log_error', True)
if raw_output:
raise NotImplementedError('Not yet implemented')
_log_action(func, module_name, action_name, args, kwargs, _log_action(func, module_name, action_name, args, kwargs,
run_in_background, is_server=True) run_in_background, is_server=True)
@ -142,10 +138,27 @@ def _run_privileged_method_on_server(func, module_name, action_name, args,
'module': module_name, 'module': module_name,
'action': action_name, 'action': action_name,
'args': args, 'args': args,
'kwargs': kwargs 'kwargs': kwargs,
} }
if raw_output:
request['raw_output'] = raw_output
client_socket = _request_to_server(request) client_socket = _request_to_server(request)
if raw_output:
def _reader_func():
while True:
chunk = client_socket.recv(4096)
if chunk:
yield chunk
else:
break
client_socket.close()
return _reader_func()
args = (func, module_name, action_name, args, kwargs, log_error, args = (func, module_name, action_name, args, kwargs, log_error,
client_socket) client_socket)
if not run_in_background: if not run_in_background:
@ -535,7 +548,8 @@ def privileged_main():
sys.exit(1) sys.exit(1)
def privileged_handle_json_request(request_string: str) -> str: def privileged_handle_json_request(
request_string: str) -> str | io.BufferedReader:
"""Parse arguments for the program spawned as a privileged action.""" """Parse arguments for the program spawned as a privileged action."""
def _parse_request() -> dict: def _parse_request() -> dict:
@ -555,6 +569,10 @@ def privileged_handle_json_request(request_string: str) -> str:
raise TypeError(f'Parameter "{parameter}" must be of type' raise TypeError(f'Parameter "{parameter}" must be of type'
f'{expected_type.__name__}') f'{expected_type.__name__}')
if 'raw_output' in request and not isinstance(request['raw_output'],
bool):
raise TypeError('Incorrect "raw_output" parameter')
return request return request
try: try:
@ -564,6 +582,13 @@ def privileged_handle_json_request(request_string: str) -> str:
arguments = {'args': request['args'], 'kwargs': request['kwargs']} arguments = {'args': request['args'], 'kwargs': request['kwargs']}
return_value = _privileged_call(request['module'], request['action'], return_value = _privileged_call(request['module'], request['action'],
arguments) arguments)
if isinstance(return_value, io.BufferedReader):
raw_output = request.get('raw_output', False)
if not raw_output:
raise TypeError('Invalid call to raw output API.')
return return_value
except (PermissionError, SyntaxError, TypeError, Exception) as exception: except (PermissionError, SyntaxError, TypeError, Exception) as exception:
if isinstance(exception, (PermissionError, SyntaxError, TypeError)): if isinstance(exception, (PermissionError, SyntaxError, TypeError)):
logger.error(exception.args[0]) logger.error(exception.args[0])
@ -623,6 +648,9 @@ def _privileged_call(module_name, action_name, arguments):
try: try:
return_values = func(*arguments['args'], **arguments['kwargs']) return_values = func(*arguments['args'], **arguments['kwargs'])
if isinstance(return_values, io.BufferedReader):
return return_values
return_value = {'result': 'success', 'return': return_values} return_value = {'result': 'success', 'return': return_values}
except Exception as exception: except Exception as exception:
return_value = { return_value = {

View File

@ -12,7 +12,7 @@ import tarfile
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from plinth import action_utils from plinth import action_utils, actions
from plinth import app as app_module from plinth import app as app_module
from plinth import module_loader from plinth import module_loader
from plinth.actions import privileged, secret_str from plinth.actions import privileged, secret_str
@ -340,8 +340,11 @@ def _extract(archive_path, destination, encryption_passphrase, locations=None):
@privileged @privileged
def export_tar(path: str, encryption_passphrase: secret_str | None = None): def export_tar(path: str, encryption_passphrase: secret_str | None = None):
"""Export archive contents as tar stream on stdout.""" """Export archive contents as tar stream on stdout."""
_run(['borg', 'export-tar', path, '-', '--tar-filter=gzip'], env = _get_env(encryption_passphrase)
encryption_passphrase) process = subprocess.Popen(
['borg', 'export-tar', path, '-', '--tar-filter=gzip'], env=env,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
return actions.ProcessBufferedReader(process)
def _read_archive_file(archive, filepath, encryption_passphrase): def _read_archive_file(archive, filepath, encryption_passphrase):

View File

@ -1,6 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""The main method for a daemon that runs privileged methods.""" """The main method for a daemon that runs privileged methods."""
import io
import json import json
import logging import logging
import os import os
@ -52,9 +53,13 @@ class RequestHandler(socketserver.StreamRequestHandler):
return request return request
def _write_response(self, response: str): def _write_response(self, response: str | io.BufferedReader):
"""Write a single response to the client.""" """Write a single response to the client."""
self.wfile.write(response.encode('utf-8')) if isinstance(response, str):
self.wfile.write(response.encode('utf-8'))
else:
for chunk in response:
self.wfile.write(chunk)
def handle(self) -> None: def handle(self) -> None:
"""Handle a new connection from a client.""" """Handle a new connection from a client."""