# Clusev reverse proxy — the panel domain is changeable FROM THE DASHBOARD.
#
# TLS is issued ON DEMAND: on the first HTTPS handshake for a host, Caddy asks the app
# (/_caddy/ask) whether to issue a certificate, and the app approves ONLY the domain
# currently configured in the dashboard (DB) / .env. So the operator changes the domain
# in the UI, points DNS at this host, restarts the stack, and Caddy gets the cert for the
# new domain automatically — no Caddyfile edit, no SITE_ADDRESS.
#
# HTTP is ALWAYS served (bare-IP access + the operator's recovery path); the app issues
# the HTTPS redirect for the configured domain (PanelScheme), and never for the raw IP.
# Reverb (Pusher protocol) rides the same address over /app/* (WS) and /apps/* (event
# API), proxied to reverb:8080 — no extra exposed port, no mixed content.
{
	# ACME contact for Let's Encrypt (set by install.sh; defaults to admin@localhost).
	email {$ACME_EMAIL}

	# Only mint a certificate for the host the app approves — prevents cert-issuance
	# abuse from arbitrary domains pointed at this IP.
	on_demand_tls {
		ask http://app:80/_caddy/ask
	}

	# Keep serving plain HTTP (still issue certs on demand): the APP decides the
	# HTTP->HTTPS redirect — only for the configured domain, never for the raw IP —
	# so bare-IP HTTP always stays reachable as the operator's recovery path.
	auto_https disable_redirects

	# Honor X-Forwarded-* from a trusted upstream proxy (external-TLS mode). Defaults to
	# 127.0.0.1/32 (trust nothing external); set TRUSTED_PROXY_CIDR to the upstream proxy's
	# EXACT address (a /32, or its tightest CIDR). SECURITY: never a broad range and never
	# 0.0.0.0/0 — a too-wide value lets any client forge X-Forwarded-For, which then drives
	# request()->ip() in the app and defeats the IP-keyed login/2FA throttles + the honeypot
	# brute-force ban (an attacker can spoof a victim's IP to get it banned, or a whitelisted
	# IP to evade the ban entirely).
	servers {
		trusted_proxies static {$TRUSTED_PROXY_CIDR:127.0.0.1/32}
	}
}

# Snippet: the shared proxy + hardening, reused by both the HTTP and HTTPS sites.
(clusev_proxy) {
	@reverb path /app/* /apps/*
	reverse_proxy @reverb reverb:8080

	# Terminal sidecar websocket (xterm.js ↔ PTY); Caddy handles the WS upgrade automatically.
	@terminal path /terminal/ws
	reverse_proxy @terminal terminal:3000

	# The internal terminal-resolve endpoint hands out DECRYPTED SSH credentials. It is for the sidecar
	# only, which reaches it over the private network (app:80, bypassing Caddy). Refuse it at the public
	# edge so a leaked sidecar secret can't be replayed from the internet (defence in depth).
	@internal path /_internal/*
	respond @internal 404

	reverse_proxy app:80

	encode zstd gzip

	header {
		# conservative hardening; HSTS is asserted by the app only over HTTPS
		X-Content-Type-Options nosniff
		X-Frame-Options DENY
		Referrer-Policy strict-origin-when-cross-origin
		-Server
	}
}

# HTTPS for any host the app approves (the configured panel domain); cert obtained
# on demand on the first handshake.
https:// {
	tls {
		on_demand
	}
	import clusev_proxy
}

# Plain HTTP: always served — bare-IP access and the recovery path. Also carries the
# ACME HTTP-01 challenge. The app redirects regular requests to HTTPS for the configured
# domain (PanelScheme); the raw IP stays on HTTP.
http:// {
	# Reverb (/app WS, /apps events) bypasses the app, so enforce transport here: on a
	# HOSTNAME force the WS/event paths onto HTTPS, so broadcasts (incl. the public metrics
	# channel) are never carried in cleartext and a stale/wrong host is refused by on-demand
	# TLS. On a bare IP there is no TLS, so plaintext there is expected and left alone.
	@reverb_cleartext {
		path /app/* /apps/*
		not header_regexp Host ^(\d{1,3}\.){3}\d{1,3}(:\d+)?$
		# External-TLS mode: an upstream proxy already terminated TLS and forwards over HTTP with
		# X-Forwarded-Proto: https. The connection is secure end-to-end, so do NOT bounce the
		# WS/event paths to https here — this Caddy serves no cert for the domain in that mode, so
		# the redirect would just break the WebSocket. Only genuine cleartext is redirected.
		not header X-Forwarded-Proto https
	}
	handle @reverb_cleartext {
		redir https://{host}{uri}
	}

	handle {
		import clusev_proxy
	}
}
