312 lines
8.5 KiB
Bash
Executable File
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}"
|