fix(openai,xai): match official accept-encoding and block attestation passthrough

Official Codex Desktop app-server and Grok CLI build reqwest without
compression features, so upstream requests carry no Accept-Encoding.
Drop the gzip/brotli/zstd/deflate features and the explicit
'accept-encoding: identity' Grok header so the wire matches official
clients. Verified: reqwest only inserts Accept-Encoding through the
tower-http Decompression layer, which is compiled solely when a
compression feature is enabled; the official binary's 'gzip, deflate'
string is the h2 HPACK static table entry, not a request header.

Also stop downstream clients from injecting device attestation/integrity
headers (x-oai-attestation, x-oai-is, x-oai-is-update) at both the API
passthrough boundary and the provider header builder; only the official
client or the gateway itself may supply those.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
zyycn
2026-08-02 02:13:08 +08:00
co-authored by Codex
parent 33381fc472
commit 1ff520659f
10 changed files with 41 additions and 112 deletions
-101
View File
@@ -27,21 +27,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
@@ -100,18 +85,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "async-compression"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac"
dependencies = [
"compression-codecs",
"compression-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "async-lock"
version = "3.4.2"
@@ -661,27 +634,6 @@ dependencies = [
"hybrid-array",
]
[[package]]
name = "brotli"
version = "8.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "5.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
@@ -829,26 +781,6 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "compression-codecs"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
dependencies = [
"brotli",
"compression-core",
"flate2",
"memchr",
"zstd",
"zstd-safe",
]
[[package]]
name = "compression-core"
version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
[[package]]
name = "config"
version = "0.15.25"
@@ -3922,17 +3854,12 @@ version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
"async-compression",
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
"pin-project-lite",
"tokio",
"tokio-util",
"tower",
"tower-layer",
"tower-service",
@@ -4732,31 +4659,3 @@ name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",
]
@@ -474,6 +474,11 @@ fn passthrough_header_name(name: &str, connection_headers: &[String]) -> bool {
// 不能透传覆盖,避免不同下游客户端暴露不一致的设备指纹。
| "originator"
| "user-agent"
// 设备 attestation/integrity 头只能由官方客户端或网关自身生成,
// 客户端注入的 x-oai-attestation / X-OAI-IS 不得透传上游。
| "x-oai-attestation"
| "x-oai-is"
| "x-oai-is-update"
)
}
@@ -288,6 +288,15 @@ fn decoder_should_preserve_ordinary_request_headers_as_opaque_multivalues() {
"x-codex-installation-id",
HeaderValue::from_static("client-installation"),
);
headers.insert(
"x-oai-attestation",
HeaderValue::from_static("client-attestation"),
);
headers.insert("x-oai-is", HeaderValue::from_static("client-is"));
headers.insert(
"x-oai-is-update",
HeaderValue::from_static("client-is-update"),
);
let decoded =
decode_request_with_headers(br#"{"model":"smart-code","input":"hello"}"#, &headers)
@@ -340,6 +349,9 @@ fn decoder_should_preserve_ordinary_request_headers_as_opaque_multivalues() {
// 上游指纹由运行时画像统一生成,客户端不得覆盖。
"user-agent",
"originator",
"x-oai-attestation",
"x-oai-is",
"x-oai-is-update",
] {
assert!(values(excluded).is_empty(), "unexpected {excluded}");
}
+2 -1
View File
@@ -28,7 +28,8 @@ hex = "0.4.3"
# 会偏离官方指纹;升级前必须重新抓握手对比扩展顺序。
hyper-rustls = "=0.27.7"
jsonwebtoken = { version = "=11.0.0", default-features = false, features = ["aws_lc_rs", "use_pem"] }
reqwest = { version = "=0.12.28", default-features = false, features = ["json", "stream", "cookies", "rustls-tls-native-roots", "gzip", "brotli", "zstd", "deflate", "http2"] }
# 与官方 Codex Desktop 一致:reqwest 不启用压缩特性,上游请求不带 accept-encoding。
reqwest = { version = "=0.12.28", default-features = false, features = ["json", "stream", "cookies", "rustls-tls-native-roots", "http2"] }
roxmltree = "0.21.1"
semver = "1.0.27"
# 保持 ring provider:reqwest rustls-tls-native-roots 即 __rustls-ring,与官方一致;
@@ -47,6 +47,7 @@ type ReqwestClientCache = Mutex<HashMap<ReqwestClientCacheKey, Client>>;
/// 构建带缓存、自动协商 HTTP/2 的 reqwest Client。
pub fn build_reqwest_client() -> Result<Client, CustomCaError> {
super::tls::ensure_rustls_provider();
let cache_key = custom_ca_env_cache_key();
static CLIENTS: OnceLock<ReqwestClientCache> = OnceLock::new();
let cache = CLIENTS.get_or_init(|| Mutex::new(HashMap::new()));
@@ -68,11 +69,7 @@ pub fn build_reqwest_client() -> Result<Client, CustomCaError> {
.tcp_keepalive(Duration::from_secs(30))
.http2_keep_alive_interval(Duration::from_secs(30))
.http2_keep_alive_timeout(Duration::from_secs(5))
.http2_keep_alive_while_idle(true)
.gzip(true)
.brotli(true)
.zstd(true)
.deflate(true);
.http2_keep_alive_while_idle(true);
let client = build_reqwest_client_with_custom_ca(builder)?;
let mut clients = cache
.lock()
@@ -522,7 +522,10 @@ impl CodexBackendClient {
for name in request.passthrough_headers.keys() {
// 上游指纹必须由运行时画像统一生成:originator/User-Agent 即使绕过
// API 透传黑名单也不能覆盖画像,避免下游客户端暴露不一致指纹。
if matches!(name.as_str(), "originator" | "user-agent") {
if matches!(
name.as_str(),
"originator" | "user-agent" | "x-oai-attestation" | "x-oai-is" | "x-oai-is-update"
) {
continue;
}
headers.remove(name);
@@ -48,7 +48,10 @@ fn request_with_opaque_headers(use_websocket: bool) -> CodexResponsesRequest {
[
"x-codex-installation-id",
STANDARD.encode(b"client-installation")
]
],
["x-oai-attestation", STANDARD.encode(b"client-attestation")],
["x-oai-is", STANDARD.encode(b"client-is")],
["x-oai-is-update", STANDARD.encode(b"client-is-update")]
]),
),
("turn_state".to_owned(), json!("typed-turn-state")),
@@ -413,6 +416,12 @@ async fn backend_http_should_restore_opaque_multivalue_header_bytes_and_lease_id
raw_header_values(&raw, "originator"),
vec![b"codex_cli_rs".to_vec()]
);
for dropped in ["x-oai-attestation", "x-oai-is", "x-oai-is-update"] {
assert!(
raw_header_values(&raw, dropped).is_empty(),
"unexpected {dropped}"
);
}
for secret in [
b"client-secret".as_slice(),
b"client-account",
@@ -515,6 +524,9 @@ async fn backend_websocket_should_drop_only_unrepresentable_opaque_header_values
vec![test_wire_profile().snapshot().user_agent().into_bytes()]
);
assert_eq!(values("originator"), vec![b"codex_cli_rs".to_vec()]);
for dropped in ["x-oai-attestation", "x-oai-is", "x-oai-is-update"] {
assert!(values(dropped).is_empty(), "unexpected {dropped}");
}
assert_eq!(
values("authorization"),
vec![b"Bearer lease-token".to_vec()]
+2 -1
View File
@@ -19,7 +19,8 @@ gateway-core = { path = "../../gateway-core" }
gateway-protocol = { path = "../../gateway-protocol" }
getrandom = "0.4.3"
jsonwebtoken = { version = "=11.0.0", default-features = false, features = ["aws_lc_rs"] }
reqwest = { version = "=0.12.28", default-features = false, features = ["stream", "rustls-tls-native-roots", "gzip", "brotli", "zstd", "deflate", "http2"] }
# 与官方 Grok CLI 一致:reqwest 不启用压缩特性,上游请求不带 accept-encoding。
reqwest = { version = "=0.12.28", default-features = false, features = ["stream", "rustls-tls-native-roots", "http2"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.150", features = ["arbitrary_precision", "preserve_order"] }
semver = "1.0.27"
@@ -127,7 +127,6 @@ pub fn build_grok_headers(
GrokHeader::public("user-agent", profile.user_agent()),
GrokHeader::public("content-type", "application/json"),
GrokHeader::public("accept", "text/event-stream"),
GrokHeader::public("accept-encoding", "identity"),
GrokHeader::sensitive("x-grok-req-id", SecretValue::new(upstream_request_id)),
GrokHeader::sensitive(
"idempotency-key",
@@ -71,7 +71,7 @@ fn headers_should_bind_identity_to_the_selected_oauth_account() {
assert_eq!(value("x-grok-client-version"), Some("0.2.106"));
assert_eq!(value("x-grok-client-identifier"), Some("grok-shell"));
assert_eq!(value("x-grok-client-mode"), Some("headless"));
assert_eq!(value("accept-encoding"), Some("identity"));
assert_eq!(value("accept-encoding"), None);
assert_eq!(value("x-grok-turn-idx"), Some("7"));
assert_eq!(
value("user-agent"),