From fb0dd323ff953e7acea36e862f4c016c1352e4b5 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 19 Apr 2024 11:00:20 -0700 Subject: [PATCH] nextcloud: Populated and maintain a list of trusted domains - Rename 'domain' to 'override domain'. See below. - If override domain is not set and trusted domains list is properly maintained, then Nextcloud can be accessed using a domain from list of trusted domains. This is ideal as accessing from .onion domain and a regular domain will simultaneously without forcing a single domain. However, non-localhost IP addresses will not work with this approach and 'override domain' will be needed. - When override domain is set to an IP address or a domain, then that domain will forced. Also hostname are accepted on a request but after the first page load, access will be forcefully redirected to the configured override domain. Multiple domains, even trusted domains, will thus not work. This option should be used as a last resort. - All un-setting the override domain to an empty value so that trusted domains can be used again. - Update diagnostic checks to ensure that above logic is used with checking domains. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/nextcloud/__init__.py | 103 ++++++++++++++++++++----- plinth/modules/nextcloud/forms.py | 9 ++- plinth/modules/nextcloud/privileged.py | 27 +++++-- plinth/modules/nextcloud/views.py | 6 +- 4 files changed, 111 insertions(+), 34 deletions(-) diff --git a/plinth/modules/nextcloud/__init__.py b/plinth/modules/nextcloud/__init__.py index 38a45e4ed..e1e42c13f 100644 --- a/plinth/modules/nextcloud/__init__.py +++ b/plinth/modules/nextcloud/__init__.py @@ -1,17 +1,22 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """FreedomBox app to configure Nextcloud.""" +import contextlib + from django.utils.translation import gettext_lazy as _ from plinth import app as app_module from plinth import cfg, frontpage, menu from plinth.config import DropinConfigs from plinth.daemon import Daemon, SharedDaemon -from plinth.modules.apache.components import Webserver, diagnose_url +from plinth.modules.apache.components import (Webserver, diagnose_url, + diagnose_url_on_all) from plinth.modules.backups.components import BackupRestore from plinth.modules.firewall.components import (Firewall, FirewallLocalProtection) +from plinth.modules.names.components import DomainName from plinth.package import Packages +from plinth.signals import domain_added, domain_removed from plinth.utils import format_lazy from . import manifest, privileged @@ -90,8 +95,7 @@ class NextcloudApp(app_module.App): 'firewall-local-protection-nextcloud', ['9000']) self.add(firewall_local_protection) - webserver = Webserver('webserver-nextcloud', 'nextcloud-freedombox', - urls=['https://{host}/nextcloud/login']) + webserver = Webserver('webserver-nextcloud', 'nextcloud-freedombox') self.add(webserver) daemon = SharedDaemon('shared-daemon-podman-auto-update', @@ -116,6 +120,12 @@ class NextcloudApp(app_module.App): **manifest.backup) self.add(backup_restore) + @staticmethod + def post_init(): + """Perform post initialization operations.""" + domain_added.connect(_on_domain_added) + domain_removed.connect(_on_domain_removed) + def setup(self, old_version): """Install and configure the app.""" super().setup(old_version) @@ -133,6 +143,7 @@ class NextcloudApp(app_module.App): # Database needs to be running for successful initialization or # upgrade of Nextcloud database. privileged.setup() + _set_trusted_domains() if should_disable: self.disable() @@ -146,9 +157,33 @@ class NextcloudApp(app_module.App): super().uninstall() def diagnose(self): - """Run diagnostics and return the results.""" + """Run diagnostics and return the results. + + When an override domain is set, that domain and all other addresses are + expected to work. This is because Nextcloud will accept any Host: HTTP + header and then override it with the provided domain name. When + override domain is not set, only the configured trusted domains along + with local IP addresses are allowed. Others are rejected with an error. + """ results = super().diagnose() + + kwargs = {'check_certificate': False} + url = 'https://{domain}/nextcloud/login' + domain = privileged.get_override_domain() + if domain: + results.append(diagnose_url(url.format(domain=domain), **kwargs)) + results += diagnose_url_on_all(url.format(domain='{host}'), + **kwargs) + else: + local_addresses = [('localhost', '4'), ('localhost', '6'), + ('127.0.0.1', '4'), ('[::1]', '6')] + for address, kind in local_addresses: + results.append( + diagnose_url(url.format(domain=address), kind=kind, + **kwargs)) + results.append(diagnose_url('docker.com')) + return results @@ -176,23 +211,51 @@ class NextcloudBackupRestore(BackupRestore): def backup_pre(self, packet): """Save database contents.""" super().backup_pre(packet) - self.app.get_component('dropin-configs-nextcloud').enable() - mysql = self.app.get_component('shared-daemon-nextcloud-mysql') - redis = self.app.get_component('shared-daemon-nextcloud-redis') - container = self.app.get_component('daemon-nextcloud') - with mysql.ensure_running(): - with redis.ensure_running(): - with container.ensure_running(): - privileged.dump_database() + with _ensure_nextcloud_running(): + privileged.dump_database() def restore_post(self, packet): """Restore database contents.""" super().restore_post(packet) - self.app.get_component('dropin-configs-nextcloud').enable() - mysql = self.app.get_component('shared-daemon-nextcloud-mysql') - redis = self.app.get_component('shared-daemon-nextcloud-redis') - container = self.app.get_component('daemon-nextcloud') - with mysql.ensure_running(): - with redis.ensure_running(): - with container.ensure_running(): - privileged.restore_database() + with _ensure_nextcloud_running(): + privileged.restore_database() + + +def _on_domain_added(sender, domain_type, name='', description='', + services=None, **kwargs): + """Add domain to list of trusted domains.""" + app = app_module.App.get('nextcloud') + if app.needs_setup(): + return + + _set_trusted_domains() + + +def _on_domain_removed(sender, domain_type, name='', **kwargs): + """Update the list of trusted domains.""" + app = app_module.App.get('nextcloud') + if app.needs_setup(): + return + + _set_trusted_domains() + + +def _set_trusted_domains(): + """Set the list of trusted domains.""" + all_domains = DomainName.list_names() + with _ensure_nextcloud_running(): + privileged.set_trusted_domains(list(all_domains)) + + +@contextlib.contextmanager +def _ensure_nextcloud_running(): + """Ensure the nextcloud is running and returns to original state.""" + app = app_module.App.get('nextcloud') + app.get_component('dropin-configs-nextcloud').enable() + mysql = app.get_component('shared-daemon-nextcloud-mysql') + redis = app.get_component('shared-daemon-nextcloud-redis') + container = app.get_component('daemon-nextcloud') + with mysql.ensure_running(): + with redis.ensure_running(): + with container.ensure_running(): + yield diff --git a/plinth/modules/nextcloud/forms.py b/plinth/modules/nextcloud/forms.py index 731ac25ec..b5f6d1cb5 100644 --- a/plinth/modules/nextcloud/forms.py +++ b/plinth/modules/nextcloud/forms.py @@ -22,9 +22,12 @@ def _get_phone_regions(): class NextcloudForm(forms.Form): """Nextcloud configuration form.""" - domain = forms.CharField( - label=_('Domain'), required=False, help_text=_( - 'Examples: "myfreedombox.example.org" or "example.onion".')) + override_domain = forms.CharField( + label=_('Override domain'), required=False, help_text=_( + 'Set to the domain or IP address that Nextcloud should be forced ' + 'to generate URLs with. Should not be needed if a valid domain is ' + 'used to access Nextcloud. Examples: "myfreedombox.example.org" ' + 'or "example.onion".')) admin_password = forms.CharField( label=_('Administrator password'), help_text=_( diff --git a/plinth/modules/nextcloud/privileged.py b/plinth/modules/nextcloud/privileged.py index d45cfe8f1..5770b0c56 100644 --- a/plinth/modules/nextcloud/privileged.py +++ b/plinth/modules/nextcloud/privileged.py @@ -105,8 +105,8 @@ def disable(): @privileged -def get_domain(): - """Return domain name set in Nextcloud.""" +def get_override_domain(): + """Return the domain name that Nextcloud is configured to override with.""" try: domain = _run_occ('config:system:get', 'overwritehost', capture_output=True) @@ -116,22 +116,33 @@ def get_domain(): @privileged -def set_domain(domain_name: str): - """Set Nextcloud domain name.""" +def set_override_domain(domain_name: str): + """Set the domain name that Nextcloud will use to override all domains.""" protocol = 'https' if domain_name.endswith('.onion'): protocol = 'http' if domain_name: _run_occ('config:system:set', 'overwritehost', '--value', domain_name) - + _run_occ('config:system:set', 'overwriteprotocol', '--value', protocol) _run_occ('config:system:set', 'overwrite.cli.url', '--value', f'{protocol}://{domain_name}/nextcloud') + else: + _run_occ('config:system:delete', 'overwritehost') + _run_occ('config:system:delete', 'overwriteprotocol') + _run_occ('config:system:delete', 'overwrite.cli.url') - _run_occ('config:system:set', 'overwriteprotocol', '--value', protocol) + # Restart to apply changes immediately + action_utils.service_restart('nextcloud-freedombox') - # Restart to apply changes immediately - action_utils.service_restart('nextcloud-freedombox') + +@privileged +def set_trusted_domains(domains: list[str]): + """Set the list of trusted domains.""" + _run_occ('config:system:delete', 'trusted_domains') + for index, domain in enumerate(domains): + _run_occ('config:system:set', 'trusted_domains', str(index), '--value', + domain) @privileged diff --git a/plinth/modules/nextcloud/views.py b/plinth/modules/nextcloud/views.py index 92a1e21f6..eac1fc9cb 100644 --- a/plinth/modules/nextcloud/views.py +++ b/plinth/modules/nextcloud/views.py @@ -24,7 +24,7 @@ class NextcloudAppView(AppView): """Return the values to fill in the form.""" initial = super().get_initial() initial.update({ - 'domain': privileged.get_domain(), + 'override_domain': privileged.get_override_domain(), 'default_phone_region': privileged.get_default_phone_region() or '' }) return initial @@ -39,8 +39,8 @@ class NextcloudAppView(AppView): def _value_changed(key): return old_config.get(key) != new_config.get(key) - if _value_changed('domain'): - privileged.set_domain(new_config['domain']) + if _value_changed('override_domain'): + privileged.set_override_domain(new_config['override_domain']) is_changed = True if new_config['admin_password']: