feat: strip accept-encoding header to ensure uncompressed responses in relay

This commit is contained in:
Abolfazl
2026-05-05 07:39:32 +03:30
parent 64363ed531
commit f42497aa82
5 changed files with 55 additions and 1 deletions
+4
View File
@@ -26,6 +26,10 @@ const SKIP_HEADERS = {
"x-forwarded-port": 1, "x-real-ip": 1, "forwarded": 1, "via": 1, "x-forwarded-port": 1, "x-real-ip": 1, "forwarded": 1, "via": 1,
// Internal relay hop-count header — must not be forwarded to target sites. // Internal relay hop-count header — must not be forwarded to target sites.
"x-mhr-hop": 1, "x-mhr-hop": 1,
// UrlFetchApp does not decompress gzip/br/deflate responses — stripping
// accept-encoding forces targets to reply with plain (uncompressed) bodies
// so the relay never has to handle compressed content it cannot decode.
"accept-encoding": 1,
}; };
// Pattern that matches any Google Apps Script execution endpoint. // Pattern that matches any Google Apps Script execution endpoint.
+3
View File
@@ -19,6 +19,9 @@ const STRIP_HEADERS = new Set([
"via", "via",
// Internal relay hop header — must not propagate to the final target. // Internal relay hop header — must not propagate to the final target.
"x-mhr-hop", "x-mhr-hop",
// Workers cannot decompress gzip/br/deflate — stripping accept-encoding
// forces targets to reply with plain bodies the Worker can forward as-is.
"accept-encoding",
]); ]);
function decodeBase64ToBytes(input) { function decodeBase64ToBytes(input) {
+3
View File
@@ -77,6 +77,9 @@ _STRIP_HEADERS = frozenset(
"x-real-ip", "x-real-ip",
"forwarded", "forwarded",
"via", "via",
# urllib.request cannot decompress gzip/br/deflate — stripping this
# forces targets to reply with plain bodies the server can forward.
"accept-encoding",
] ]
) )
+10 -1
View File
@@ -1202,9 +1202,18 @@ class DomainFronter:
body of the outer Apps Script relay call, so Apps Script POSTs it to body of the outer Apps Script relay call, so Apps Script POSTs it to
the exit node URL on our behalf. the exit node URL on our behalf.
""" """
# Build inner payload: what the exit node will execute # Build inner payload: what the exit node will execute.
# Strip accept-encoding from the inner headers so the target site
# returns an uncompressed body. Exit nodes (CF Worker, VPS) make
# plain Python/JS fetch() calls that don't auto-decompress, so a
# compressed response body would be forwarded as garbled bytes.
inner = dict(payload) inner = dict(payload)
inner["k"] = self._exit_node_psk inner["k"] = self._exit_node_psk
if isinstance(inner.get("h"), dict):
inner["h"] = {
k: v for k, v in inner["h"].items()
if k.lower() != "accept-encoding"
}
inner_json = json.dumps(inner).encode() inner_json = json.dumps(inner).encode()
# Build outer payload: what Apps Script will fetch # Build outer payload: what Apps Script will fetch
+35
View File
@@ -22,9 +22,11 @@ classify_relay_error(raw) -> str
import base64 import base64
import codecs import codecs
import gzip
import json import json
import logging import logging
import re import re
import zlib
log = logging.getLogger("Fronter") log = logging.getLogger("Fronter")
@@ -230,6 +232,39 @@ def parse_relay_json(data: dict, max_body_bytes: int) -> bytes:
status = data.get("s", 200) status = data.get("s", 200)
resp_headers = data.get("h", {}) resp_headers = data.get("h", {})
resp_body = base64.b64decode(data.get("b", "")) resp_body = base64.b64decode(data.get("b", ""))
# ── Decompress if the target sent a compressed body ─────────────────────────
# UrlFetchApp does NOT auto-decompress gzip/deflate responses, so if the
# client's Accept-Encoding header was forwarded and the server compressed
# its reply, we receive raw compressed bytes. We decompress here so the
# browser always gets plain content (and we can safely drop the header).
_ce = ""
for _k, _v in resp_headers.items():
if _k.lower() == "content-encoding":
_ce = str(_v).lower().strip()
break
if _ce == "gzip":
try:
resp_body = gzip.decompress(resp_body)
except Exception as _exc:
log.debug("gzip decompress skipped (%s) — body may already be plain", _exc)
elif _ce in ("deflate", "zlib"):
try:
# Try zlib wrapper first, then raw deflate
resp_body = zlib.decompress(resp_body)
except Exception:
try:
resp_body = zlib.decompress(resp_body, -15)
except Exception as _exc:
log.debug("deflate decompress skipped (%s)", _exc)
elif _ce == "br":
# Brotli is uncommon in this relay path but log if seen so it is visible
log.debug("brotli-encoded response from target — install 'brotli' package for support")
try:
import brotli # type: ignore
resp_body = brotli.decompress(resp_body)
except Exception:
pass # leave body as-is; browser will likely fail gracefully
if len(resp_body) > max_body_bytes: if len(resp_body) > max_body_bytes:
return error_response( return error_response(
502, 502,