Files
Qortal-Nextcloud-Integration/scripts/ensure-nextcloud-url-config.sh

315 lines
9.4 KiB
Bash
Executable File

#!/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 <<USAGE
Usage: ./scripts/ensure-nextcloud-url-config.sh --compose-file <file> --env-file <file> [options]
Ensure Nextcloud URL-related system config is aligned with environment:
- trusted_domains
- overwrite.cli.url
- overwritehost
- overwriteprotocol
Options:
--compose-file <path> docker compose file to use (required)
--env-file <path> env file to use (required)
--app-service <name> Nextcloud app service name (default: app)
--public-url <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:-<no-public-url>}."