From b4e23486d18545dc403afff98544c0d95663eac6 Mon Sep 17 00:00:00 2001 From: Abolfazl Date: Thu, 7 May 2026 21:49:34 +0330 Subject: [PATCH] fixed youtube on relay & chatgpt login on VPS exit node --- apps_script/vps_exit_node.py | 37 +++++++++++++++++++++++++++--------- src/relay/domain_fronter.py | 8 ++------ src/relay/http_reader.py | 5 ++++- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/apps_script/vps_exit_node.py b/apps_script/vps_exit_node.py index 1a83bbb..027bbab 100644 --- a/apps_script/vps_exit_node.py +++ b/apps_script/vps_exit_node.py @@ -158,6 +158,32 @@ def _safe_url(url: str) -> bool: return True +def _collect_headers(raw_headers) -> dict: + """Collect HTTP response headers, preserving all values for duplicate names. + + Python's http.client.HTTPMessage yields duplicate header names (e.g. multiple + Set-Cookie lines) as separate items when iterated. A plain dict assignment + silently overwrites earlier values, so sites like auth.openai.com that set + several Set-Cookie headers in one response would lose all but the last one. + Accumulate duplicates into a list so every value reaches the browser. + """ + out: dict = {} + key_map: dict[str, str] = {} # lowercase name → first-seen canonical case + for k, v in raw_headers.items(): + kl = k.lower() + if kl not in key_map: + key_map[kl] = k + out[k] = v + else: + canonical = key_map[kl] + cur = out[canonical] + if isinstance(cur, list): + cur.append(v) + else: + out[canonical] = [cur, v] + return out + + def _relay_request( url: str, method: str, headers: dict[str, str], body: bytes ) -> dict: @@ -169,23 +195,16 @@ def _relay_request( try: with _NO_REDIRECT_OPENER.open(request, timeout=_OUTBOUND_TIMEOUT) as resp: data = resp.read(_MAX_RESPONSE_BODY) - resp_headers: dict[str, str] = {} - for k, v in resp.headers.items(): - resp_headers[k] = v return { "s": resp.status, - "h": resp_headers, + "h": _collect_headers(resp.headers), "b": base64.b64encode(data).decode(), } except urllib.error.HTTPError as exc: data = exc.read(_MAX_RESPONSE_BODY) if exc.fp else b"" - resp_headers = {} - if exc.headers: - for k, v in exc.headers.items(): - resp_headers[k] = v return { "s": exc.code, - "h": resp_headers, + "h": _collect_headers(exc.headers) if exc.headers else {}, "b": base64.b64encode(data).decode(), } diff --git a/src/relay/domain_fronter.py b/src/relay/domain_fronter.py index a9d22fd..92c41f7 100644 --- a/src/relay/domain_fronter.py +++ b/src/relay/domain_fronter.py @@ -1392,12 +1392,8 @@ class DomainFronter: # Script quota usage. _relay_with_retry bypasses batching entirely. raw = await self._batch_submit(outer) - # raw is now the response from the exit node (inner relay JSON) - # _parse_relay_response will decode it into the final HTTP response. - # But we need to unwrap one level: Apps Script gives us exit node HTTP - # response body (which is itself a relay JSON), so parse twice. - _, _, apps_script_body = split_raw_response(raw) - result = parse_relay_response(apps_script_body, self._max_response_body_bytes) + _, _, vps_relay_bytes = split_raw_response(raw) + result = parse_relay_response(vps_relay_bytes, self._max_response_body_bytes) log.debug("Exit node relay OK: %s", payload.get("u", "")[:80]) return result diff --git a/src/relay/http_reader.py b/src/relay/http_reader.py index 6fc9974..68d2999 100644 --- a/src/relay/http_reader.py +++ b/src/relay/http_reader.py @@ -43,7 +43,10 @@ async def read_http_response( while b"\r\n\r\n" not in raw: if len(raw) > 65536: # 64 KB header size limit return 0, {}, b"" - chunk = await asyncio.wait_for(reader.read(8192), timeout=8) + # 30s per-read: exit-node chain (Apps Script → VPS → target) needs + # time to fetch + process large responses before sending headers. + # The outer asyncio.wait_for in _relay_single caps total time. + chunk = await asyncio.wait_for(reader.read(8192), timeout=30) if not chunk: break raw += chunk