mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
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:
Generated
-101
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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()]
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user