diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 859946525..b91734495 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -16,6 +16,7 @@ jobs: run: | /bin/bash -n deploy/apple-container.sh /bin/bash deploy/tests/apple-container-test.sh + /bin/sh deploy/test-caddyfile-cache.sh test: runs-on: ubuntu-latest diff --git a/deploy/Caddyfile b/deploy/Caddyfile index 079b5f07e..77e1bf4c5 100644 --- a/deploy/Caddyfile +++ b/deploy/Caddyfile @@ -67,7 +67,14 @@ api.sub2api.com { gzip 6 minimum_length 256 match { - header Content-Type text/* + # 不使用 text/*,否则 text/event-stream 会被压缩并缓冲到流结束 + header Content-Type text/css* + header Content-Type text/csv* + header Content-Type text/html* + header Content-Type text/javascript* + header Content-Type text/markdown* + header Content-Type text/plain* + header Content-Type text/xml* header Content-Type application/json* header Content-Type application/javascript* header Content-Type application/xml* diff --git a/deploy/EDGE_SECURITY.md b/deploy/EDGE_SECURITY.md index 956507c4f..5e9ab10fb 100644 --- a/deploy/EDGE_SECURITY.md +++ b/deploy/EDGE_SECURITY.md @@ -128,6 +128,20 @@ server { } ``` +If Nginx gzip is enabled in the `http` block, keep `text/event-stream` out of +`gzip_types` and do not use `gzip_types *` for Sub2API. The +`proxy_buffering off` setting above prevents proxy buffering, but it does not +disable the gzip response filter. Use an explicit list for ordinary responses: + +```nginx +gzip on; +gzip_types text/plain text/css application/json application/javascript application/xml image/svg+xml; +``` + +If a shared global configuration cannot exclude SSE by content type, set +`gzip off;` in the locations serving streaming API routes. This leaves gzip +available for the web UI and static assets. + Do not use an incoming `$http_x_forwarded_for` value unless Nginx real-IP processing is restricted to explicit trusted proxy CIDRs. @@ -140,6 +154,17 @@ the TCP peer. It is therefore a direct-to-Caddy baseline. Do not use its attributed to a CDN egress address, collapsing rejection aggregation and the invalid-auth limiter onto unrelated users. +The bundled Caddy configuration leaves `flush_interval` unset so Caddy can +automatically flush `text/event-stream` responses while still propagating +client cancellation upstream. Do not set it globally: positive values can add +streaming latency, while Caddy 2.6.2's special `-1` mode also causes +reverse-proxied requests to continue after clients disconnect. The +configuration uses an explicit response content-type list for compression. Do +not replace that list with `text/*` or the shorthand `encode gzip zstd`: both +match `text/event-stream` and can buffer SSE until the response ends. Keep +streaming responses uncompressed while retaining compression for the web UI, +JSON, and static assets. + For a CDN deployment, first firewall the origin so only current CDN egress CIDRs can connect. Then configure those exact ranges as Caddy trusted proxies and derive upstream headers from Caddy's parsed `{client_ip}`. For example: diff --git a/deploy/test-caddyfile-cache.sh b/deploy/test-caddyfile-cache.sh index 789e48ee2..803231f4b 100755 --- a/deploy/test-caddyfile-cache.sh +++ b/deploy/test-caddyfile-cache.sh @@ -4,15 +4,82 @@ set -eu repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) caddyfile="$repo_root/deploy/Caddyfile" active_config=$(sed 's/[[:space:]]*#.*$//' "$caddyfile") +normalized_config=$(printf '%s\n' "$active_config" | awk ' + NF > 0 { + for (field = 1; field <= NF; field++) { + token = $field + sub(/^["`]/, "", token) + sub(/["`]$/, "", token) + printf "%s%s", field == 1 ? "" : " ", token + } + print "" + } +') -if printf '%s\n' "$active_config" | grep -Eiq 'Cache-Control.*immutable'; then +# Keep one canonical encode block instead of reimplementing Caddy matcher semantics. +expected_encode_block=$(cat <<'EOF' +encode { +zstd +gzip 6 +minimum_length 256 +match { +header Content-Type text/css* +header Content-Type text/csv* +header Content-Type text/html* +header Content-Type text/javascript* +header Content-Type text/markdown* +header Content-Type text/plain* +header Content-Type text/xml* +header Content-Type application/json* +header Content-Type application/javascript* +header Content-Type application/xml* +header Content-Type application/rss+xml* +header Content-Type image/svg+xml* +} +} +EOF +) + +if printf '%s\n' "$normalized_config" | grep -Eiq 'cache-control.*immutable'; then echo "Caddyfile must not force immutable caching; the backend owns asset cache policy" >&2 exit 1 fi -if ! printf '%s\n' "$active_config" | grep -Eq '^[[:space:]]*reverse_proxy[[:space:]]+localhost:8080'; then +if ! printf '%s\n' "$normalized_config" | grep -Eq '^reverse_proxy localhost:8080([[:space:]]|$)'; then echo "Caddyfile must continue proxying all application routes to localhost:8080" >&2 exit 1 fi -echo "Caddyfile preserves backend Cache-Control policy and reverse_proxy routing" +if printf '%s\n' "$normalized_config" | grep -Eq '^import([[:space:]]|$)'; then + echo "Caddyfile must not import configuration outside this canonical policy check" >&2 + exit 1 +fi + +if printf '%s\n' "$normalized_config" | grep -Eiq '^flush_interval([[:space:]]|$)'; then + echo "Caddyfile must leave flush_interval unset so SSE auto-flushing and client cancellation remain intact" >&2 + exit 1 +fi + +encode_directive_count=$(printf '%s\n' "$normalized_config" | awk '$1 == "encode" { count++ } END { print count + 0 }') +if [ "$encode_directive_count" -ne 1 ]; then + echo "Caddyfile must contain exactly one explicit encode block" >&2 + exit 1 +fi + +actual_encode_block=$(printf '%s\n' "$normalized_config" | awk ' + $1 == "encode" { in_block = 1 } + in_block { + print + for (field = 1; field <= NF; field++) { + if ($field == "{") depth++ + if ($field == "}") depth-- + } + if (depth == 0) exit + } +') +if [ "$actual_encode_block" != "$expected_encode_block" ]; then + echo "Caddyfile encode block must keep the canonical non-SSE compression policy" >&2 + exit 1 +fi + +echo "Caddyfile preserves backend cache policy, SSE streaming, and non-SSE compression"