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 <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2024-04-19 11:00:20 -07:00 committed by James Valleroy
parent 1272be0ad6
commit fb0dd323ff
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
4 changed files with 111 additions and 34 deletions

View File

@ -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

View File

@ -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=_(

View File

@ -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

View File

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