Merge pull request #903 from CyberMind-FR/fix/nextcloud-connection-urls
Some checks are pending
License Headers / check (push) Waiting to run

fix(nextcloud): connection URLs use real public domain not localhost:8080
This commit is contained in:
CyberMind 2026-07-25 12:55:01 +02:00 committed by GitHub
commit aacbb39fb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 90 additions and 9 deletions

View File

@ -16,6 +16,24 @@ from secubox_core.config import get_config
app = FastAPI(title="SecuBox Nextcloud") app = FastAPI(title="SecuBox Nextcloud")
config = get_config("nextcloud") config = get_config("nextcloud")
def _public_base_url(ssl_domain: str, domain: str, http_port: int) -> str:
"""Return the URL a client/device uses to reach Nextcloud.
HAProxy terminates TLS 1.3 in front of every SecuBox vhost, so a real
public domain is always reachable over https. Precedence: an explicit
ssl_domain wins; otherwise the configured public ``domain`` (skipping the
``cloud.local`` placeholder); only a bare host with no real domain falls
back to the container port. Never emit ``localhost`` when a domain exists
that is unreachable from a phone/desktop client (the bug this fixes).
"""
if ssl_domain:
return f"https://{ssl_domain}"
if "." in domain and domain != "cloud.local":
return f"https://{domain}"
return f"http://localhost:{http_port}"
LXC_NAME = config.get("container_name", "nextcloud") LXC_NAME = config.get("container_name", "nextcloud")
LXC_PATH = Path(config.get("lxc_path", "/data/lxc")) LXC_PATH = Path(config.get("lxc_path", "/data/lxc"))
DATA_PATH = Path(config.get("data_path", "/data/volumes/nextcloud")) DATA_PATH = Path(config.get("data_path", "/data/volumes/nextcloud"))
@ -158,8 +176,7 @@ async def status():
"disk_used": disk_used, "disk_used": disk_used,
# Public URL from the real domain (not localhost) so the dashboard links # Public URL from the real domain (not localhost) so the dashboard links
# somewhere reachable; falls back to the container port for a bare host. # somewhere reachable; falls back to the container port for a bare host.
"web_url": f"https://{domain}" if "." in domain and domain != "cloud.local" "web_url": _public_base_url(config.get("ssl_domain", ""), domain, http_port),
else f"http://localhost:{http_port}",
"ssl_enabled": config.get("ssl_enabled", False), "ssl_enabled": config.get("ssl_enabled", False),
"container_name": LXC_NAME, "container_name": LXC_NAME,
} }
@ -443,12 +460,10 @@ def restore_backup(name: str):
async def get_connections(): async def get_connections():
"""Get connection URLs (CalDAV, CardDAV, WebDAV)""" """Get connection URLs (CalDAV, CardDAV, WebDAV)"""
http_port = config.get("http_port", 8080) http_port = config.get("http_port", 8080)
ssl_enabled = config.get("ssl_enabled", False) domain = config.get("domain", "cloud.local")
ssl_domain = config.get("ssl_domain", "") ssl_domain = config.get("ssl_domain", "")
base_url = f"http://localhost:{http_port}" base_url = _public_base_url(ssl_domain, domain, http_port)
if ssl_enabled and ssl_domain:
base_url = f"https://{ssl_domain}"
return { return {
"base_url": base_url, "base_url": base_url,

View File

@ -1,3 +1,15 @@
secubox-nextcloud (1.5.1-1~bookworm1) bookworm; urgency=medium
* Fix connection URLs showing http://localhost:8080 instead of the real
public domain. /connections only checked ssl_domain (unset in
secubox.conf [nextcloud], which carries `domain`) and fell back to
localhost; the panel also hardcoded localhost, ignoring status.web_url.
New pure _public_base_url() helper (ssl_domain > domain > localhost),
used by both /connections and /status; panel now renders status.web_url.
Regression tests added (tests/test_public_base_url.py).
-- Gerald KERMA <devel@cybermind.fr> Fri, 25 Jul 2026 12:00:00 +0200
secubox-nextcloud (1.5.0-1~bookworm2) bookworm; urgency=medium secubox-nextcloud (1.5.0-1~bookworm2) bookworm; urgency=medium
* Phase 2: Requires=secubox-core.service -> Wants= on this module's unit * Phase 2: Requires=secubox-core.service -> Wants= on this module's unit

View File

@ -0,0 +1,52 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
"""
SecuBox-Deb :: Nextcloud _public_base_url unit tests
Regression guard: /connections must never hand a client localhost when a
real public domain is configured (clients reach Nextcloud through HAProxy
TLS 1.3). The helper is pure so it is tested without importing the FastAPI
app (which starts a cache thread + LXC probes at import time).
"""
import ast
import textwrap
from pathlib import Path
MAIN = Path(__file__).resolve().parent.parent / "api" / "main.py"
def _load_helper():
"""Extract and exec only the _public_base_url function from api/main.py."""
tree = ast.parse(MAIN.read_text())
for node in tree.body:
if isinstance(node, ast.FunctionDef) and node.name == "_public_base_url":
src = textwrap.dedent(ast.get_source_segment(MAIN.read_text(), node))
ns: dict = {}
exec(src, ns)
return ns["_public_base_url"]
raise AssertionError("_public_base_url not found in api/main.py")
_public_base_url = _load_helper()
def test_configured_domain_yields_https_not_localhost():
# The bug: secubox.conf [nextcloud] has domain but no ssl_domain.
assert _public_base_url("", "nc.gk2.secubox.in", 8080) == "https://nc.gk2.secubox.in"
def test_ssl_domain_takes_precedence():
assert _public_base_url("secure.example.com", "nc.gk2.secubox.in", 8080) == \
"https://secure.example.com"
def test_placeholder_domain_falls_back_to_localhost():
assert _public_base_url("", "cloud.local", 8080) == "http://localhost:8080"
def test_bare_host_without_dot_falls_back_to_localhost():
assert _public_base_url("", "nextcloud", 9000) == "http://localhost:9000"

View File

@ -237,9 +237,11 @@
document.getElementById('data-size').textContent = status.disk_used || '-'; document.getElementById('data-size').textContent = status.disk_used || '-';
document.getElementById('version').textContent = status.version || '-'; document.getElementById('version').textContent = status.version || '-';
const port = status.http_port || 8080; // Use the public URL the API resolves (real domain behind HAProxy
document.getElementById('web-url').textContent = `http://localhost:${port}`; // TLS), not a hardcoded localhost the client can't reach.
document.getElementById('web-url').href = `http://localhost:${port}`; const webUrl = status.web_url || `http://localhost:${status.http_port || 8080}`;
document.getElementById('web-url').textContent = webUrl;
document.getElementById('web-url').href = webUrl;
} }
async function loadStorage() { async function loadStorage() {