#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "${script_dir}/.." && pwd)" source "${script_dir}/lib-compose-project.sh" compose_file="" env_file="" app_service="app" public_url_override="" usage() { cat < --env-file [options] Ensure Nextcloud URL-related system config is aligned with environment: - trusted_domains - overwrite.cli.url - overwritehost - overwriteprotocol Options: --compose-file docker compose file to use (required) --env-file env file to use (required) --app-service Nextcloud app service name (default: app) --public-url Override NEXTCLOUD_PUBLIC_URL from env -h, --help Show this help USAGE } read_kv() { local key="$1" local line line="$(grep -m1 -E "^${key}=" "${env_file}" || true)" if [[ -z "${line}" ]]; then return 1 fi echo "${line#*=}" } trim() { local value="$1" value="$(echo "${value}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" echo "${value}" } extract_url_parts() { local raw="$1" local url rest authority host_port host scheme url="$(trim "${raw}")" url="${url%/}" if [[ "${url}" != *"://"* ]]; then return 1 fi scheme="${url%%://*}" rest="${url#*://}" authority="${rest%%/*}" authority="${authority%%\?*}" authority="${authority%%\#*}" host_port="${authority##*@}" if [[ -z "${host_port}" ]]; then return 1 fi if [[ "${host_port}" == \[*\]* ]]; then host="${host_port%%]*}" host="${host#[}" else host="${host_port%%:*}" fi if [[ -z "${scheme}" || -z "${host}" ]]; then return 1 fi echo "${scheme}|${host_port}|${host}" } declare -A seen_domains=() domains=() declare -A seen_proxies=() trusted_proxies=() forwarded_for_headers=() add_domain() { local candidate candidate="$(trim "${1:-}")" candidate="${candidate%/}" if [[ -z "${candidate}" ]]; then return 0 fi if [[ -n "${seen_domains[$candidate]+x}" ]]; then return 0 fi seen_domains["$candidate"]=1 domains+=("$candidate") } add_proxy() { local candidate candidate="$(trim "${1:-}")" if [[ -z "${candidate}" ]]; then return 0 fi if [[ -n "${seen_proxies[$candidate]+x}" ]]; then return 0 fi seen_proxies["$candidate"]=1 trusted_proxies+=("$candidate") } add_url_hosts() { local parsed parsed="$(extract_url_parts "$1" || true)" if [[ -z "${parsed}" ]]; then return 0 fi local _scheme host_port host IFS='|' read -r _scheme host_port host <<< "${parsed}" add_domain "${host_port}" add_domain "${host}" } while [[ $# -gt 0 ]]; do case "$1" in --compose-file) compose_file="${2:-}" shift 2 ;; --compose-file=*) compose_file="${1#*=}" shift ;; --env-file) env_file="${2:-}" shift 2 ;; --env-file=*) env_file="${1#*=}" shift ;; --app-service) app_service="${2:-}" shift 2 ;; --app-service=*) app_service="${1#*=}" shift ;; --public-url) public_url_override="${2:-}" shift 2 ;; --public-url=*) public_url_override="${1#*=}" shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" usage exit 1 ;; esac done if [[ -z "${compose_file}" || -z "${env_file}" ]]; then usage exit 1 fi if [[ "${compose_file}" != /* ]]; then compose_file="${repo_root}/${compose_file}" fi if [[ "${env_file}" != /* ]]; then env_file="${repo_root}/${env_file}" fi if [[ ! -f "${compose_file}" ]]; then echo "Missing compose file: ${compose_file}" exit 1 fi if [[ ! -f "${env_file}" ]]; then echo "Missing env file: ${env_file}" exit 1 fi export_compose_project_name_from_env "${env_file}" || true nextcloud_domain="$(read_kv "NEXTCLOUD_DOMAIN" || true)" trusted_domains_raw="$(read_kv "NEXTCLOUD_TRUSTED_DOMAINS" || true)" trusted_proxies_raw="$(read_kv "NEXTCLOUD_TRUSTED_PROXIES" || true)" forwarded_for_headers_raw="$(read_kv "NEXTCLOUD_FORWARDED_FOR_HEADERS" || true)" redirect_allowlist_raw="$(read_kv "OIDC_REDIRECT_URI_ALLOWLIST" || true)" public_url="${public_url_override}" if [[ -z "${public_url}" ]]; then public_url="$(read_kv "NEXTCLOUD_PUBLIC_URL" || true)" fi public_url="$(trim "${public_url}")" public_url="${public_url%/}" if [[ -z "${public_url}" && -n "${nextcloud_domain}" ]]; then public_url="https://${nextcloud_domain}" fi if [[ -n "${trusted_domains_raw}" ]]; then while IFS= read -r token; do add_domain "${token}" done < <(echo "${trusted_domains_raw}" | tr ', ' '\n\n' | sed '/^$/d') fi add_domain "${nextcloud_domain}" add_domain "localhost" add_domain "127.0.0.1" add_domain "app" add_url_hosts "${public_url}" if [[ -n "${redirect_allowlist_raw}" ]]; then IFS=',' read -r -a allowlist_parts <<< "${redirect_allowlist_raw}" for part in "${allowlist_parts[@]}"; do add_url_hosts "${part}" done fi if [[ -n "${trusted_proxies_raw}" ]]; then while IFS= read -r token; do add_proxy "${token}" done < <(echo "${trusted_proxies_raw}" | tr ', ' '\n\n' | sed '/^$/d') fi # In SSL mode the bundled Caddy proxy terminates TLS and forwards traffic to app:80. # Trust that proxy IP so Nextcloud can honor forwarded client IP/proto and avoid brute-force false positives. caddy_proxy_ip="$( docker compose -f "${compose_file}" --env-file "${env_file}" exec -T "${app_service}" sh -lc \ "getent ahostsv4 caddy 2>/dev/null | awk '{print \$1; exit}'" 2>/dev/null | tr -d '\r' || true )" add_proxy "${caddy_proxy_ip}" # When the reverse proxy runs on the Docker host (common in no-SSL mode), traffic often # enters the app container from the container's default gateway (for example 172.23.0.1). # Prefer Docker metadata for gateway detection (works even if container lacks `ip` tool). host_gateway_proxy_ip="" app_container_id="$( docker compose -f "${compose_file}" --env-file "${env_file}" ps -q "${app_service}" 2>/dev/null | head -n1 | tr -d '\r' || true )" if [[ -n "${app_container_id}" ]]; then host_gateway_proxy_ip="$( docker inspect -f '{{range .NetworkSettings.Networks}}{{if .Gateway}}{{.Gateway}}{{end}}{{end}}' "${app_container_id}" 2>/dev/null | awk '{print $1; exit}' | tr -d '\r' || true )" fi if [[ -z "${host_gateway_proxy_ip}" ]]; then host_gateway_proxy_ip="$( docker compose -f "${compose_file}" --env-file "${env_file}" exec -T "${app_service}" sh -lc \ "ip -4 route show default 2>/dev/null | awk '{print \$3; exit}'" 2>/dev/null | tr -d '\r' || true )" fi add_proxy "${host_gateway_proxy_ip}" if [[ -n "${forwarded_for_headers_raw}" ]]; then while IFS= read -r token; do token="$(trim "${token}")" if [[ -n "${token}" ]]; then forwarded_for_headers+=("${token}") fi done < <(echo "${forwarded_for_headers_raw}" | tr ', ' '\n\n' | sed '/^$/d') fi if [[ "${#forwarded_for_headers[@]}" -eq 0 ]]; then forwarded_for_headers=("HTTP_X_FORWARDED_FOR") fi if [[ "${#domains[@]}" -eq 0 ]]; then echo "No trusted domains resolved; refusing to overwrite trusted_domains." exit 1 fi if ! docker compose -f "${compose_file}" --env-file "${env_file}" exec -T "${app_service}" php occ status >/dev/null 2>&1; then echo "Nextcloud OCC is not ready in service '${app_service}'." exit 1 fi docker compose -f "${compose_file}" --env-file "${env_file}" exec -T "${app_service}" php occ config:system:delete trusted_domains >/dev/null 2>&1 || true index=0 for domain in "${domains[@]}"; do docker compose -f "${compose_file}" --env-file "${env_file}" exec -T "${app_service}" php occ config:system:set trusted_domains "${index}" --value="${domain}" >/dev/null index=$((index + 1)) done docker compose -f "${compose_file}" --env-file "${env_file}" exec -T "${app_service}" php occ config:system:delete trusted_proxies >/dev/null 2>&1 || true if [[ "${#trusted_proxies[@]}" -gt 0 ]]; then index=0 for proxy in "${trusted_proxies[@]}"; do docker compose -f "${compose_file}" --env-file "${env_file}" exec -T "${app_service}" php occ config:system:set trusted_proxies "${index}" --value="${proxy}" >/dev/null index=$((index + 1)) done fi docker compose -f "${compose_file}" --env-file "${env_file}" exec -T "${app_service}" php occ config:system:delete forwarded_for_headers >/dev/null 2>&1 || true index=0 for header_name in "${forwarded_for_headers[@]}"; do docker compose -f "${compose_file}" --env-file "${env_file}" exec -T "${app_service}" php occ config:system:set forwarded_for_headers "${index}" --value="${header_name}" >/dev/null index=$((index + 1)) done if parsed_public="$(extract_url_parts "${public_url}" || true)"; [[ -n "${parsed_public}" ]]; then IFS='|' read -r public_scheme public_host_port _public_host <<< "${parsed_public}" docker compose -f "${compose_file}" --env-file "${env_file}" exec -T "${app_service}" php occ config:system:set overwrite.cli.url --value="${public_url}" >/dev/null docker compose -f "${compose_file}" --env-file "${env_file}" exec -T "${app_service}" php occ config:system:set overwritehost --value="${public_host_port}" >/dev/null docker compose -f "${compose_file}" --env-file "${env_file}" exec -T "${app_service}" php occ config:system:set overwriteprotocol --value="${public_scheme}" >/dev/null fi echo "Aligned Nextcloud URL config (trusted_domains + trusted_proxies + forwarded_for_headers + overwrite*) for ${public_url:-}."