315 lines
9.4 KiB
Bash
Executable File
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>}."
|