Files

312 lines
8.5 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"
usage() {
cat <<'USAGE'
Usage: ./scripts/validate-proxy-alignment.sh --compose-file <file> --env-file <file> [options]
Validate proxy/header alignment for a cloud instance, with additional checks
based on EXTERNAL_PROXY_MODE.
Options:
--compose-file <path> Compose file to use (required)
--env-file <path> Env file to use (required)
--app-service <name> Nextcloud app service name (default: app)
-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:-}"
echo "${value}" | xargs
}
trim_csv() {
local raw="${1:-}"
local cleaned
cleaned="$(echo "${raw}" | tr -d '[:space:]')"
cleaned="${cleaned#,}"
cleaned="${cleaned%,}"
echo "${cleaned}"
}
profile_enabled() {
local profiles="$1"
local name="$2"
[[ ",${profiles}," == *",${name},"* ]]
}
ensure_docker_access() {
local docker_path=""
docker_path="$(type -P docker || true)"
if [[ -z "${docker_path}" ]]; then
echo "docker is required"
exit 1
fi
if "${docker_path}" info >/dev/null 2>&1; then
DOCKER_CMD=("${docker_path}")
return 0
fi
if ! command -v sudo >/dev/null 2>&1; then
echo "docker is installed but this user cannot access it, and sudo is unavailable."
exit 1
fi
if sudo -n "${docker_path}" info >/dev/null 2>&1; then
DOCKER_CMD=(sudo "${docker_path}")
return 0
fi
echo "docker requires elevated privileges on this machine."
echo "You may be prompted for your sudo password."
if sudo "${docker_path}" info >/dev/null 2>&1; then
DOCKER_CMD=(sudo "${docker_path}")
return 0
fi
echo "Unable to access docker, even with sudo."
exit 1
}
docker() {
"${DOCKER_CMD[@]}" "$@"
}
compose_cmd() {
docker compose -f "${compose_file}" --env-file "${env_file}" "$@"
}
occ_exec() {
compose_cmd exec -T --user www-data "${app_service}" php occ "$@"
}
failures=0
report_ok() {
printf '[ok] %s\n' "$1"
}
report_warn() {
printf '[warn] %s\n' "$1"
}
report_fail() {
printf '[fail] %s\n' "$1"
failures=$((failures + 1))
}
expect_equal() {
local label="$1"
local expected="$2"
local actual="$3"
if [[ "${actual}" == "${expected}" ]]; then
report_ok "${label}: ${actual}"
else
report_fail "${label}: expected '${expected}', got '${actual:-<empty>}'"
fi
}
expect_nonempty() {
local label="$1"
local actual="$2"
if [[ -n "${actual}" ]]; then
report_ok "${label}: ${actual}"
else
report_fail "${label}: missing"
fi
}
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
;;
-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
ensure_docker_access
if ! occ_exec status >/dev/null 2>&1; then
echo "Nextcloud OCC is not ready in service '${app_service}'."
exit 1
fi
compose_name="$(basename "${compose_file}")"
stack_mode="manual"
if [[ "${compose_name}" == *".nossl.yml" ]]; then
stack_mode="nossl"
elif [[ "${compose_name}" == *".yml" ]]; then
stack_mode="ssl"
fi
profiles="$(trim_csv "$(read_kv "COMPOSE_PROFILES" || true)")"
external_proxy_mode="$(trim "$(read_kv "EXTERNAL_PROXY_MODE" || true)")"
external_proxy_mode="${external_proxy_mode:-none}"
nextcloud_public_url="$(trim "$(read_kv "NEXTCLOUD_PUBLIC_URL" || true)")"
devprod_http_bind_host="$(trim "$(read_kv "DEVPROD_HTTP_BIND_HOST" || true)")"
broker_bind_host="$(trim "$(read_kv "DEVPROD_BROKER_BIND_HOST" || true)")"
collabora_bind_host="$(trim "$(read_kv "COLLABORA_BIND_HOST" || true)")"
talk_recording_bind_host="$(trim "$(read_kv "TALK_RECORDING_BIND_HOST" || true)")"
talk_signaling_bind_host="$(trim "$(read_kv "TALK_SIGNALING_BIND_HOST" || true)")"
talk_signaling_port="$(trim "$(read_kv "TALK_SIGNALING_PORT" || true)")"
talk_signaling_container_port="$(trim "$(read_kv "TALK_SIGNALING_CONTAINER_PORT" || true)")"
talk_signaling_upstream_host="$(trim "$(read_kv "TALK_SIGNALING_UPSTREAM_HOST" || true)")"
talk_signaling_upstream_port="$(trim "$(read_kv "TALK_SIGNALING_UPSTREAM_PORT" || true)")"
talk_signaling_bind_host="${talk_signaling_bind_host:-0.0.0.0}"
talk_signaling_port="${talk_signaling_port:-8081}"
talk_signaling_container_port="${talk_signaling_container_port:-8081}"
trusted_proxies="$(occ_exec config:system:get trusted_proxies 2>/dev/null | tr -d '\r' || true)"
forwarded_for_headers="$(occ_exec config:system:get forwarded_for_headers 2>/dev/null | tr -d '\r' || true)"
overwrite_protocol="$(occ_exec config:system:get overwriteprotocol 2>/dev/null | tr -d '\r' || true)"
apache_remoteip_header="$(
compose_cmd exec -T "${app_service}" sh -lc \
"awk 'toupper(\$1)==\"REMOTEIPHEADER\" {print \$2; exit}' /etc/apache2/conf-enabled/remoteip.conf 2>/dev/null" \
| tr -d '\r' || true
)"
echo "Proxy alignment validation"
echo
echo "Runtime:"
echo " compose_file=${compose_file}"
echo " env_file=${env_file}"
echo " stack_mode=${stack_mode}"
echo " external_proxy_mode=${external_proxy_mode}"
echo " profiles=${profiles:-<none>}"
echo
case "${external_proxy_mode}" in
none|manual|global-caddy)
report_ok "EXTERNAL_PROXY_MODE accepted: ${external_proxy_mode}"
;;
*)
report_fail "EXTERNAL_PROXY_MODE must be one of: none, manual, global-caddy (got '${external_proxy_mode}')"
;;
esac
expect_nonempty "NEXTCLOUD_PUBLIC_URL" "${nextcloud_public_url}"
expect_nonempty "Nextcloud trusted_proxies" "${trusted_proxies}"
if [[ "${forwarded_for_headers}" == *"HTTP_X_FORWARDED_FOR"* ]]; then
report_ok "Nextcloud forwarded_for_headers includes HTTP_X_FORWARDED_FOR"
else
report_fail "Nextcloud forwarded_for_headers must include HTTP_X_FORWARDED_FOR"
fi
expect_equal "Apache RemoteIPHeader" "X-Forwarded-For" "${apache_remoteip_header}"
expect_equal "Nextcloud overwriteprotocol" "https" "${overwrite_protocol}"
if [[ "${stack_mode}" == "ssl" ]]; then
if [[ "${external_proxy_mode}" != "none" ]]; then
report_fail "SSL stack should use EXTERNAL_PROXY_MODE=none"
else
report_ok "SSL stack proxy mode matches bundled TLS expectations"
fi
fi
if [[ "${stack_mode}" == "nossl" ]]; then
if [[ "${external_proxy_mode}" == "none" ]]; then
report_warn "No-SSL stack is using EXTERNAL_PROXY_MODE=none; manual proxy assumptions may be incomplete"
fi
if [[ "${external_proxy_mode}" == "global-caddy" ]]; then
expect_equal "DEVPROD_HTTP_BIND_HOST" "0.0.0.0" "${devprod_http_bind_host}"
expect_equal "DEVPROD_BROKER_BIND_HOST" "0.0.0.0" "${broker_bind_host}"
expect_equal "TALK_SIGNALING_BIND_HOST" "0.0.0.0" "${talk_signaling_bind_host}"
expect_equal "TALK_SIGNALING_UPSTREAM_HOST" "host.docker.internal" "${talk_signaling_upstream_host}"
if profile_enabled "${profiles}" "talk" || profile_enabled "${profiles}" "turn" || profile_enabled "${profiles}" "janus"; then
expect_equal "TALK_SIGNALING_CONTAINER_PORT" "8081" "${talk_signaling_container_port}"
expect_equal "TALK_SIGNALING_UPSTREAM_PORT" "${talk_signaling_port}" "${talk_signaling_upstream_port}"
fi
if profile_enabled "${profiles}" "office"; then
expect_equal "COLLABORA_BIND_HOST" "0.0.0.0" "${collabora_bind_host}"
fi
if profile_enabled "${profiles}" "recording"; then
expect_equal "TALK_RECORDING_BIND_HOST" "0.0.0.0" "${talk_recording_bind_host}"
fi
fi
fi
echo
if [[ "${failures}" -eq 0 ]]; then
echo "Proxy alignment verified."
else
echo "Proxy alignment validation failed with ${failures} issue(s)."
fi
exit "${failures}"