feat: add codex tls client and headers

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
zyycn
2026-06-11 21:17:58 +08:00
co-authored by Codex
parent eb0341e884
commit 1bdd08c2f8
17 changed files with 281 additions and 7 deletions
Generated
+153
View File
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aead"
version = "0.5.2"
@@ -46,6 +52,21 @@ 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.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
@@ -95,6 +116,18 @@ 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-trait"
version = "0.1.89"
@@ -227,6 +260,27 @@ dependencies = [
"hybrid-array",
]
[[package]]
name = "brotli"
version = "8.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
@@ -252,6 +306,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
@@ -347,6 +403,26 @@ dependencies = [
"zeroize",
]
[[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 = "concurrent-queue"
version = "2.5.0"
@@ -500,6 +576,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
@@ -743,6 +828,16 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.12.0"
@@ -1330,6 +1425,16 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.100"
@@ -1457,6 +1562,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "mio"
version = "1.2.1"
@@ -2257,6 +2372,12 @@ dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "similar"
version = "2.7.0"
@@ -2759,13 +2880,17 @@ 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",
"http-body",
"http-body-util",
"pin-project-lite",
"tokio",
"tokio-util",
"tower",
"tower-layer",
"tower-service",
@@ -3575,3 +3700,31 @@ 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",
]
+1 -1
View File
@@ -21,7 +21,7 @@ dotenvy = "0.15.7"
futures = "0.3.32"
hmac = "0.13.0"
rand = "0.10.1"
reqwest = { version = "=0.12.28", default-features = false, features = ["json", "stream", "cookies", "rustls-tls-native-roots"] }
reqwest = { version = "=0.12.28", default-features = false, features = ["json", "stream", "cookies", "rustls-tls-native-roots", "gzip", "brotli", "zstd", "deflate"] }
rustls = { version = "=0.23.36", default-features = false, features = ["ring", "std", "tls12"] }
secrecy = "0.10.3"
serde = { version = "1.0.228", features = ["derive"] }
+1
View File
@@ -4,6 +4,7 @@
- Commit `Cargo.lock` because this is an application.
- Run `cargo update` in dedicated dependency-refresh changes.
- Keep `reqwest = 0.12.28` and `rustls = 0.23.36` pinned until a TLS fingerprint review proves a newer pair matches real Codex Desktop behavior.
- `reqwest` is built with `rustls-tls-native-roots`, `json`, `stream`, `cookies`, and response compression features used by the Codex client builder. Do not switch back to default native-tls.
- Do not add unmaintained crates when a maintained community-standard crate exists.
- Every dependency change must run:
+1 -1
View File
@@ -11,7 +11,7 @@ Update this file before each feature commit.
| Admin auth and client API keys | Completed | pending auth commit | `cargo test client_api_key_has_proxy_prefix_and_verifies_against_hash`; `cargo test admin_password_hash_is_not_a_client_api_key`; full `fmt/test/clippy` before commit | Admin passwords use Argon2id, client API keys use `cpr_` prefix plus HMAC-SHA256 with server-side pepper. |
| Secret encryption | Completed | pending crypto commit | `cargo test secret_box_encrypts_and_decrypts_without_storing_plaintext`; full `fmt/test/clippy` before commit | AES-256-GCM `SecretBox` stores upstream secrets as `v1:<nonce>:<ciphertext>` without plaintext. |
| Logging and pagination | Completed | pending logs commit | `cargo test event_logs_are_cursor_paginated`; full `fmt/test/clippy` before commit | Adds cursor pagination, `Page<T>` camelCase serialization, event log repository, and rotation config shell. |
| TLS headers and fingerprint | Planned | | | |
| TLS headers and fingerprint | Completed | pending codex headers commit | `cargo test codex_headers_include_desktop_identity_and_turn_state`; `cargo tree \| rg 'reqwest\|rustls'`; full `fmt/test/clippy` before commit | Adds Codex Desktop fingerprint model, exact identity/request headers, and pinned reqwest/rustls client builder. |
| Cookie persistence | Planned | | | |
| Account pool and refresh | Planned | | | |
| Translation | Planned | | | |
@@ -1745,7 +1745,7 @@ git commit -m "feat: add paginated event logs"
- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`
- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/codex_headers_test.rs`
- [ ] **Step 1: Write failing header test**
- [x] **Step 1: Write failing header test**
Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/codex_headers_test.rs`:
@@ -1769,7 +1769,7 @@ fn codex_headers_include_desktop_identity_and_turn_state() {
}
```
- [ ] **Step 2: Run test and verify RED**
- [x] **Step 2: Run test and verify RED**
Run:
@@ -1779,7 +1779,7 @@ cargo test codex_headers_include_desktop_identity_and_turn_state
Expected: compile failure because `codex` and `fingerprint` modules do not exist.
- [ ] **Step 3: Implement fingerprint and headers**
- [x] **Step 3: Implement fingerprint and headers**
Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`:
@@ -1908,7 +1908,7 @@ Create empty module files:
touch src/fingerprint/updater.rs src/codex/sse.rs src/codex/types.rs src/codex/usage.rs src/codex/websocket.rs
```
- [ ] **Step 4: Verify headers**
- [x] **Step 4: Verify headers**
Run:
@@ -1919,7 +1919,7 @@ cargo tree | rg 'reqwest|rustls'
Expected: test passes; `cargo tree` shows `reqwest v0.12.28` and `rustls v0.23.36`.
- [ ] **Step 5: Commit TLS and headers foundation**
- [x] **Step 5: Commit TLS and headers foundation**
```bash
git add Cargo.toml src/codex src/fingerprint src/lib.rs tests/codex_headers_test.rs
+18
View File
@@ -0,0 +1,18 @@
use reqwest::Client;
pub fn build_reqwest_client(force_http11: bool) -> Result<Client, reqwest::Error> {
// 中文注释:Codex Desktop 指纹依赖 reqwest/rustls 组合,升级前必须重新验证 TLS 行为。
let builder = Client::builder()
.use_rustls_tls()
.no_proxy()
.gzip(true)
.brotli(true)
.zstd(true)
.deflate(true);
let builder = if force_http11 {
builder.http1_only()
} else {
builder
};
builder.build()
}
+32
View File
@@ -0,0 +1,32 @@
use std::collections::BTreeMap;
use crate::fingerprint::model::Fingerprint;
pub fn build_codex_headers(
fp: &Fingerprint,
access_token: &str,
account_id: Option<&str>,
turn_state: Option<&str>,
request_id: &str,
) -> BTreeMap<String, String> {
let mut headers = BTreeMap::new();
headers.insert("accept".to_string(), "text/event-stream".to_string());
headers.insert(
"authorization".to_string(),
format!("Bearer {access_token}"),
);
headers.insert("originator".to_string(), fp.originator.clone());
headers.insert("user-agent".to_string(), fp.user_agent());
headers.insert("x-client-request-id".to_string(), request_id.to_string());
headers.insert(
"x-openai-internal-codex-residency".to_string(),
"global".to_string(),
);
if let Some(account_id) = account_id {
headers.insert("chatgpt-account-id".to_string(), account_id.to_string());
}
if let Some(turn_state) = turn_state {
headers.insert("x-codex-turn-state".to_string(), turn_state.to_string());
}
headers
}
+6
View File
@@ -0,0 +1,6 @@
pub mod client;
pub mod headers;
pub mod sse;
pub mod types;
pub mod usage;
pub mod websocket;
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+2
View File
@@ -0,0 +1,2 @@
pub mod model;
pub mod updater;
+35
View File
@@ -0,0 +1,35 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Fingerprint {
pub originator: String,
pub app_version: String,
pub build_number: String,
pub platform: String,
pub arch: String,
pub chromium_version: String,
pub user_agent_template: String,
}
impl Fingerprint {
pub fn default_for_tests() -> Self {
Self {
originator: "Codex Desktop".to_string(),
app_version: "26.519.81530".to_string(),
build_number: "3178".to_string(),
platform: "darwin".to_string(),
arch: "arm64".to_string(),
chromium_version: "146".to_string(),
user_agent_template:
"Codex/{app_version} ({platform}; {arch}) Chromium/{chromium_version}".to_string(),
}
}
pub fn user_agent(&self) -> String {
self.user_agent_template
.replace("{app_version}", &self.app_version)
.replace("{platform}", &self.platform)
.replace("{arch}", &self.arch)
.replace("{chromium_version}", &self.chromium_version)
}
}
+1
View File
@@ -0,0 +1 @@
+2
View File
@@ -1,7 +1,9 @@
pub mod auth;
pub mod codex;
pub mod config;
pub mod crypto;
pub mod error;
pub mod fingerprint;
pub mod logs;
pub mod pagination;
pub mod state;
+20
View File
@@ -0,0 +1,20 @@
use codex_proxy_rs::{codex::headers::build_codex_headers, fingerprint::model::Fingerprint};
#[test]
fn codex_headers_include_desktop_identity_and_turn_state() {
let fp = Fingerprint::default_for_tests();
let headers = build_codex_headers(
&fp,
"access-token",
Some("acct_123"),
Some("turn-state"),
"rid_1",
);
assert_eq!(headers.get("originator").unwrap(), "Codex Desktop");
assert!(headers.get("user-agent").unwrap().contains("Codex"));
assert_eq!(headers.get("authorization").unwrap(), "Bearer access-token");
assert_eq!(headers.get("chatgpt-account-id").unwrap(), "acct_123");
assert_eq!(headers.get("x-codex-turn-state").unwrap(), "turn-state");
assert_eq!(headers.get("x-client-request-id").unwrap(), "rid_1");
}