# Duga Остров — единственная дверь наружу.
#
# ПОЧЕМУ БЕЗ TLS НЕЛЬЗЯ ВООБЩЕ. Два независимых запрета, оба не обходятся:
#   1) crypto.subtle (сквозное шифрование переписки, ключи, резервная копия)
#      существует только в защищённом контексте. По http:// на чужом адресе
#      его в браузере просто НЕТ — приложение не сможет создать ключи;
#   2) Android-WebView не даёт странице, открытой по https, подключиться к
#      ws:// — правило MIXED_CONTENT_NEVER_ALLOW. Обратное сочетание тоже
#      бесполезно: по http приложение упрётся в пункт 1.
# Поэтому TLS здесь не «желательно», а условие работы.

{
	# Сюда установщик подставляет строку «email pochta@primer» — но только
	# если почта задана. Пустой директивы email Caddy не прощает: он
	# отказывается разбирать конфиг целиком, и наружу не встаёт НИЧЕГО.
	# Проверено на стенде 05.09.2026: caddy перезапускался по кругу.
	{$DUGA_ACME_STROKA}

	# Установка на голый IP без домена. В SNI IP-адрес не кладут — это
	# запрещено самим TLS. Без этой строки Caddy не может выбрать
	# сертификат и рвёт рукопожатие: снаружи выглядит как «не открывается».
	# Для установки на домен строка безвредна.
	default_sni {$DUGA_HOST}
}

# ── проверка здоровья ────────────────────────────────────────────────
# Только внутри контейнера: порт 8081 наружу не публикуется. По https
# проверить нельзя — busybox-wget кладёт в SNI IP-адрес, сертификата на
# него нет, и рукопожатие рвётся: здоровая служба выглядела бы больной.
# Здесь проверяется главное: конфиг загружен и файлы клиента читаются.
:8081 {
	root * /srv/klient
	file_server
}

https://{$DUGA_HOST} {
	# Строка режима TLS подставляется установщиком:
	#   пусто        — Caddy сам берёт сертификат Let's Encrypt (свой домен
	#                  либо адрес вида 1-2-3-4.sslip.io);
	#   tls internal — свой корень, для сети без интернета. Отпечаток
	#                  печатает ustanovit.sh, чтобы приложение доверяло по нему.
	{$DUGA_TLS}

	encode gzip

	# ── веб-сокет шлюза ──────────────────────────────────────────────
	handle /ws {
		reverse_proxy shlyuz:8765
	}

	# ── сигнализация звонков ─────────────────────────────────────────
	# LiveKit-клиент идёт на wss://<адрес>/rtc — тот же адрес и сертификат,
	# второго домена не заводим. Сама сигнализация (7880) наружу не открыта:
	# её видит только Caddy во внутренней сети. Наружу идут лишь медиапорты
	# 7881/7882, и они минуют Caddy. Если звонки выключены, службы livekit
	# нет — сюда попросту никто не стучится (кнопка звонка у клиента скрыта).
	handle /rtc* {
		reverse_proxy livekit:7880
	}

	# ── приём вложений ───────────────────────────────────────────────
	handle /upload {
		# Служба вложений — встроенный http-сервер Python, он представляется
		# своей версией. Знать её посторонним незачем: это подсказка, какие
		# уязвимости пробовать.
		header -Server
		# Потолок на тело запроса. Без него единственной защитой осталась бы
		# проверка Content-Length внутри Python, то есть ЗАЯВЛЕННОЙ длины.
		request_body {
			max_size 340MB
		}
		reverse_proxy zagruzka:8770
	}

	# ── раздача вложений ─────────────────────────────────────────────
	# Файлы раздаёт СЛУЖБА, а не Caddy: она отдаёт файл лишь тому, кто
	# предъявил ключ входа и числится участником разговора. file_server
	# здесь означал бы «у кого ссылка — у того и файл».
	# handle, а не handle_path: службе нужен полный путь /files/<имя>.
	handle /files/* {
		header Content-Disposition attachment
		header X-Content-Type-Options nosniff
		header Content-Security-Policy "sandbox"
		header Strict-Transport-Security "max-age=31536000"
		header X-Frame-Options "DENY"
		header Referrer-Policy "no-referrer"
		header -Server
		request_body {
			max_size 128KB
		}
		reverse_proxy zagruzka:8770
	}

	# ── служебные и запасные файлы не отдаём ─────────────────────────
	# Заглушка try_files ниже отдаёт index.html на любой неизвестный путь,
	# поэтому даже удалённый файл не давал бы честного 404. Отвечаем явно
	# и ДО заглушки.
	@zapasy path_regexp zapasy "(?i)(\.do_|\.bak|\.old$|\.orig$|\.save$|\.swp$|~$|\.tgz$|\.tar$|\.gz$|\.zip$|\.7z$|\.rar$|\.sql$|\.dump$|\.log$|\.env|\.pem$|\.key$|\.p12$|\.jks$|\.keystore$|\.py$|\.sh$|\.ini$|/\.git)"
	handle @zapasy {
		respond "Not Found" 404
	}

	# ── сам клиент ───────────────────────────────────────────────────
	# Переключатель клиента для разбора неполадок: при DUGA_KLIENT_SUFFIX=.polnyj
	# по адресу /app.js отдаётся неминифицированный app.polnyj.js. Имя в адресе
	# не меняется, поэтому служебный работник кэширует его как обычно.
	@appjs path /app.js
	rewrite @appjs /app{$DUGA_KLIENT_SUFFIX}.js

	handle {
		header {
			Strict-Transport-Security "max-age=31536000"
			X-Content-Type-Options "nosniff"
			X-Frame-Options "DENY"
			Referrer-Policy "no-referrer"
			# connect-src: только к себе и по wss. Внешних адресов у этой
			# сборки нет вовсе. wss: оставлен — человек вправе указать
			# другой сервер Duga в настройках.
			# img-src https: нужен для превью ссылок. Плата за него —
			# чужой сервер картинки видит IP получателя; убрать значит
			# отнять превью у всех.
			Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; media-src 'self' data: blob:; connect-src 'self' wss:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
			Permissions-Policy "camera=(self), microphone=(self), geolocation=(self), payment=(), usb=(), serial=(), midi=(), interest-cohort=()"
			-Server
		}
		root * /srv/klient
		@manifest path /manifest.webmanifest
		header @manifest Content-Type application/manifest+json
		@svezhee path /app.js /style.css /index.html / /sw.js /i18n.js /duga-motion.js /duga-tokens.css /duga-components.css /fonts.css
		header @svezhee Cache-Control "no-cache, must-revalidate"

		# ПОЧЕМУ ДВА file_server, А НЕ ОДИН.
		# try_files отдаёт index.html на любой неизвестный путь — так
		# работает одностраничное приложение. Но для файла с расширением
		# это ложь: отсутствующий скрипт вернулся бы с кодом 200 и HTML
		# внутри, а браузер упал бы на нём с синтаксической ошибкой.
		# «Файла нет» выглядело бы как «приложение сломалось».
		@statika path *.js *.css *.png *.jpg *.jpeg *.webp *.svg *.ico *.json *.map *.txt *.woff *.woff2 *.webmanifest *.html
		route {
			file_server @statika
			try_files {path} /index.html
			file_server
		}
	}
}
