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']: