From 42d8e64e407c8c6e35f59bd39714c20a58da93f2 Mon Sep 17 00:00:00 2001 From: sercebr Date: Sun, 20 Sep 2026 03:03:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20`*`=20=E4=B8=8D=E5=BE=97=E8=A6=86?= =?UTF-8?q?=E7=9B=96=20Accept-Encoding=20=E9=87=8C=E6=98=BE=E5=BC=8F?= =?UTF-8?q?=E7=BB=99=E5=87=BA=E7=9A=84=20q=3D0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `parse_accept_encoding` 对 `q=0` 只是 `continue` 跳过,没有记下"这个编码被 显式拒了",于是 `*` 那一支又把它选回来,而且 `*` 默认 q=1.0、优先级还最高。 RFC 9110 §12.5.3 里 `*` 只匹配**没有显式列出**的编码,所以客户端发 `*, gzip;q=0` 会收到 gzip —— 一个它刚声明过收不了的编码。 修法: - 记下被显式拒掉的编码,`*` 选择时避开; - 收尾再加一道,挡住 `gzip;q=0, gzip;q=0.5` 这种自相矛盾输入把 gzip 选回来。 仓库里既有的 `"gzip;q=0, deflate"` 用例抓不到这条:它靠的是 deflate 显式出现 且 q 更高;只有两侧都仅由 `*` 覆盖时才露出来。已补 4 条用例。 变异验证(同一条断言、只换 DLL): 未修正 -> 3 条 FAIL(第 4 条 `*, deflate;q=0` 两边都过,它不区分,作用是 挡住"把 deflate 也一起拒掉"这种过度修正) 修正后 -> 10/10 PASS,5 条既有断言两边都绿(未碰既有行为) --- src/web/uvcpp_http_compress.cpp | 37 +++++++++++++++++++++-- tests/functional/web_http_client_func.cpp | 19 ++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/web/uvcpp_http_compress.cpp b/src/web/uvcpp_http_compress.cpp index e00e04f7..6927c37c 100644 --- a/src/web/uvcpp_http_compress.cpp +++ b/src/web/uvcpp_http_compress.cpp @@ -28,6 +28,10 @@ http_compress_method http_compress::parse_accept_encoding( struct candidate { http_compress_method method; double q; }; candidate best = { http_compress_method::NONE, 0.0 }; + // `q=0` 是"明确不可接受"。这两个标志记住哪些编码被显式拒过 —— 因为它们 + // 必须能挡住 `*`(见下面 wildcard 那一支的注释)。 + bool gzip_ok = true, deflate_ok = true; + std::string tok; std::istringstream ss(header); while (std::getline(ss, tok, ',')) { @@ -50,15 +54,26 @@ http_compress_method http_compress::parse_accept_encoding( while (!enc.empty() && enc.back() == ' ') enc.pop_back(); } - if (q == 0.0) continue; // "not acceptable" + if (q == 0.0) { + // "not acceptable" —— 记下来,别只是跳过这一项。 + if (enc == "gzip" || enc == "x-gzip") gzip_ok = false; + else if (enc == "deflate" || enc == "x-deflate") deflate_ok = false; + continue; + } http_compress_method method = http_compress_method::NONE; if (enc == "gzip" || enc == "x-gzip") method = http_compress_method::GZIP; else if (enc == "deflate" || enc == "x-deflate") method = http_compress_method::DEFLATE; - else if (enc == "*") - method = http_compress_method::GZIP; // wildcard → prefer gzip + else if (enc == "*") { + // RFC 9110 §12.5.3:`*` 只匹配**没有显式列出**的编码。所以选择时要把 + // 被 `q=0` 显式拒掉的排掉 —— 否则 `*, gzip;q=0` 会选回 gzip,服务端 + // 于是发了一个客户端刚刚声明过收不了的编码。 + method = gzip_ok ? http_compress_method::GZIP + : (deflate_ok ? http_compress_method::DEFLATE + : http_compress_method::NONE); + } if (method != http_compress_method::NONE) { if (q > best.q || @@ -69,6 +84,22 @@ http_compress_method http_compress::parse_accept_encoding( } } + // 收尾一道:上面按 q 值取最大值,而 `q=0` 那支是"直接跳过",所以一条 + // `gzip;q=0, gzip;q=0.5` 这种自相矛盾的输入仍会把 gzip 选回来。显式拒绝 + // 优先 —— 少发一层编码只是没优化,发错一层编码是客户端解不开。 + if ((best.method == http_compress_method::GZIP && !gzip_ok) || + (best.method == http_compress_method::DEFLATE && !deflate_ok)) { + const bool was_gzip = (best.method == http_compress_method::GZIP); + if (was_gzip && deflate_ok) { + best.method = http_compress_method::DEFLATE; + } else if (!was_gzip && gzip_ok) { + best.method = http_compress_method::GZIP; + } else { + // 两条路都被拒(或另一条 q 更低但可用)—— 退回未压缩。 + best.method = http_compress_method::NONE; + } + } + return best.method; } diff --git a/tests/functional/web_http_client_func.cpp b/tests/functional/web_http_client_func.cpp index 3c7b8a4e..f21367d2 100644 --- a/tests/functional/web_http_client_func.cpp +++ b/tests/functional/web_http_client_func.cpp @@ -199,6 +199,25 @@ static bool test_accept_encoding_parse() { // q=0 should skip if (http_compress::parse_accept_encoding("gzip;q=0, deflate") != http_compress_method::DEFLATE) return false; + + // ── `*` 不得把被显式拒掉的编码选回来 ──────────────────────────────── + // RFC 9110 §12.5.3:`*` 只匹配**没有显式列出**的编码。上面那条 + // `gzip;q=0, deflate` 之所以通过,靠的是"deflate 显式出现且 q 更高", + // 一旦两侧都只由 `*` 覆盖,原来的实现就会把 gzip 选回来 —— 也就是发一个 + // 客户端刚声明过收不了的编码。下面四条钉住这一点。 + if (http_compress::parse_accept_encoding("*, gzip;q=0") != http_compress_method::DEFLATE) + return false; + if (http_compress::parse_accept_encoding("gzip;q=0, *") != http_compress_method::DEFLATE) + return false; + if (http_compress::parse_accept_encoding("*, deflate;q=0") != http_compress_method::GZIP) + return false; + if (http_compress::parse_accept_encoding("*, gzip;q=0, deflate;q=0") != + http_compress_method::NONE) + return false; + // 注:这里**没有**钉"显式项比 `*` 的 q 更高时该听谁的"(例如 + // `*, gzip;q=0.5` 里 gzip 到底是 1.0 还是 0.5)。按 RFC 该由显式项说了算, + // 但当前实现不区分这两者 —— 那是一条独立的、尚未处理的偏差,不在这里 + // 顺手固化下来。 return true; }