fix(version): support lightweight backend probes

This commit is contained in:
Aethersailor
2026-06-06 13:11:48 +08:00
parent a09aadb40f
commit 60bbadde5b
2 changed files with 122 additions and 3 deletions

View File

@@ -12,6 +12,7 @@ from __future__ import annotations
import argparse
import difflib
import json
import re
import sys
import urllib.error
import urllib.parse
@@ -29,12 +30,22 @@ def build_url(base_url: str, path: str, params: dict[str, str] | None = None) ->
return f"{base}{path}" + (f"?{query}" if query else "")
def fetch(base_url: str, path: str, params: dict[str, str] | None, timeout: int) -> str:
def fetch_response(
base_url: str,
path: str,
params: dict[str, str] | None,
timeout: int,
headers: dict[str, str] | None = None,
) -> tuple[str, dict[str, str]]:
url = build_url(base_url, path, params)
request = urllib.request.Request(url, headers=headers or {})
try:
with urllib.request.urlopen(url, timeout=timeout) as response:
with urllib.request.urlopen(request, timeout=timeout) as response:
status = response.status
body = response.read().decode("utf-8", errors="replace")
response_headers = {
key.lower(): value for key, value in response.headers.items()
}
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
raise AssertionError(f"{url} returned HTTP {exc.code}\n{body}") from exc
@@ -43,6 +54,11 @@ def fetch(base_url: str, path: str, params: dict[str, str] | None, timeout: int)
if status < 200 or status >= 300:
raise AssertionError(f"{url} returned HTTP {status}\n{body}")
return body, response_headers
def fetch(base_url: str, path: str, params: dict[str, str] | None, timeout: int) -> str:
body, _ = fetch_response(base_url, path, params, timeout)
return body
@@ -76,6 +92,75 @@ def run_checks(base_url: str, timeout: int, snapshot_dir: Path | None, update: b
if health.strip() != "ok":
raise AssertionError(f"/healthz returned unexpected body: {health!r}")
version_page, version_headers = fetch_response(
base_url, "/version", None, timeout
)
if (
"<!DOCTYPE html>" not in version_page
or "SubConverter-Extended" not in version_page
):
raise AssertionError("/version did not return the HTML version page")
if not version_headers.get("content-type", "").lower().startswith("text/html"):
raise AssertionError("/version HTML response has an unexpected content type")
navigation_page, navigation_headers = fetch_response(
base_url,
"/version",
None,
timeout,
{
"Origin": "https://edgetunnel.example",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Dest": "document",
},
)
if "<!DOCTYPE html>" not in navigation_page:
raise AssertionError("/version navigation request did not return HTML")
if not navigation_headers.get("content-type", "").lower().startswith(
"text/html"
):
raise AssertionError("/version navigation response has an unexpected content type")
probe_headers = {
"Origin": "https://edgetunnel.example",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
}
version_probe, version_probe_headers = fetch_response(
base_url, "/version", None, timeout, probe_headers
)
version_probe_line = version_probe.strip()
if not re.fullmatch(
r"SubConverter-Extended \S+ backend", version_probe_line
):
raise AssertionError(
f"/version probe returned an unexpected body: {version_probe!r}"
)
if "subconverter" not in version_probe_line.lower() or "<" in version_probe_line:
raise AssertionError("/version probe is not compatible with backend detection")
if not version_probe_headers.get("content-type", "").lower().startswith(
"text/plain"
):
raise AssertionError("/version probe response has an unexpected content type")
if version_probe_headers.get("access-control-allow-origin") != "*":
raise AssertionError("/version probe response is missing the CORS header")
if "no-store" not in version_probe_headers.get("cache-control", "").lower():
raise AssertionError("/version probe response is missing no-store caching")
vary = version_probe_headers.get("vary", "").lower()
for header in ("sec-fetch-mode", "sec-fetch-dest", "origin"):
if header not in vary:
raise AssertionError(f"/version probe Vary header is missing {header}")
legacy_probe, _ = fetch_response(
base_url,
"/version",
None,
timeout,
{"Origin": "https://edgetunnel.example"},
)
if legacy_probe != version_probe:
raise AssertionError("/version legacy browser probe response is inconsistent")
inspect_page = fetch(base_url, "/inspect", None, timeout)
if (
"Request Inspector" not in inspect_page

View File

@@ -3,6 +3,7 @@
#include <string>
#include "handler/settings.h"
#include "utils/string.h"
#include "version.h"
namespace {
@@ -88,6 +89,32 @@ std::string buildCommitLink(const std::string &build_id) {
build_id + "</a>";
}
std::string headerValue(const Request &request, const std::string &name) {
auto iter = request.headers.find(name);
if (iter == request.headers.end())
return "";
return trimWhitespace(iter->second, true, true);
}
bool isScriptVersionProbe(const Request &request) {
std::string fetch_mode = toLower(headerValue(request, "Sec-Fetch-Mode"));
std::string fetch_dest = toLower(headerValue(request, "Sec-Fetch-Dest"));
if (fetch_mode == "cors" && fetch_dest == "empty")
return true;
return fetch_mode.empty() && fetch_dest.empty() &&
!headerValue(request, "Origin").empty();
}
std::string buildPlainVersion() {
std::string version = VERSION;
std::string build_id = BUILD_ID;
if (!build_id.empty())
version += "-" + build_id;
return "SubConverter-Extended " + version + " backend\n";
}
} // namespace
namespace version_page {
@@ -102,9 +129,16 @@ std::string faviconLight(Request &, Response &response) {
return VERSION_FAVICON_LIGHT;
}
std::string page(Request &, Response &response) {
std::string page(Request &request, Response &response) {
response.headers["X-Robots-Tag"] =
"noindex, nofollow, noarchive, nosnippet, noimageindex";
response.headers["Vary"] = "Sec-Fetch-Mode, Sec-Fetch-Dest, Origin";
if (isScriptVersionProbe(request)) {
response.content_type = "text/plain; charset=utf-8";
response.headers["Cache-Control"] = "no-store";
return buildPlainVersion();
}
std::string build_id = BUILD_ID;
std::string build_date = BUILD_DATE;
std::string build_date_display = formatBuildDate(build_date);