Preserve Cloudfare HTTP cookies in codex (#17783)

## Summary
- Adds a process-local, in-memory cookie store for ChatGPT HTTP clients.
- Limits cookie storage and replay to a shared ChatGPT host allowlist.
- Wires the shared store into the default Codex reqwest client and
backend client.
- Shares the ChatGPT host allowlist with remote-control URL validation
to avoid drift.
- Enables reqwest cookie support and updates lockfiles.
This commit is contained in:
Shijie Rao
2026-04-21 17:40:15 -04:00
committed by GitHub
Unverified
parent be75785504
commit c5e9c6f71f
9 changed files with 387 additions and 5 deletions
+5
View File
@@ -755,6 +755,8 @@
"convert_case_0.10.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.9.0\"}],\"features\":{}}",
"convert_case_0.6.0": "{\"dependencies\":[{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.18.0\"},{\"kind\":\"dev\",\"name\":\"strum_macros\",\"req\":\"^0.18.0\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.9.0\"}],\"features\":{\"random\":[\"rand\"]}}",
"cookie-factory_0.3.3": "{\"dependencies\":[{\"features\":[\"attributes\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.9.0\"},{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3.16\"},{\"kind\":\"dev\",\"name\":\"maplit\",\"req\":\"^1.0\"}],\"features\":{\"async\":[\"futures\"],\"default\":[\"std\",\"async\"],\"std\":[]}}",
"cookie_0.18.1": "{\"dependencies\":[{\"name\":\"aes-gcm\",\"optional\":true,\"req\":\"^0.10.0\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"hkdf\",\"optional\":true,\"req\":\"^0.12.0\"},{\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12.0\"},{\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.0\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.0\"},{\"name\":\"subtle\",\"optional\":true,\"req\":\"^2.3\"},{\"default_features\":false,\"features\":[\"std\",\"parsing\",\"formatting\",\"macros\"],\"name\":\"time\",\"req\":\"^0.3\"},{\"kind\":\"build\",\"name\":\"version_check\",\"req\":\"^0.9.4\"}],\"features\":{\"key-expansion\":[\"sha2\",\"hkdf\"],\"percent-encode\":[\"percent-encoding\"],\"private\":[\"aes-gcm\",\"base64\",\"rand\",\"subtle\"],\"secure\":[\"private\",\"signed\",\"key-expansion\"],\"signed\":[\"hmac\",\"sha2\",\"base64\",\"rand\",\"subtle\"]}}",
"cookie_store_0.22.1": "{\"dependencies\":[{\"features\":[\"percent-encode\"],\"name\":\"cookie\",\"req\":\"^0.18.0\"},{\"name\":\"document-features\",\"req\":\"^0.2.10\"},{\"name\":\"idna\",\"req\":\"^1.0\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.6.0\"},{\"name\":\"log\",\"req\":\"^0.4.17\"},{\"name\":\"publicsuffix\",\"optional\":true,\"req\":\"^2.2.3\"},{\"name\":\"ron\",\"optional\":true,\"req\":\"^0.10.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.147\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0.147\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.87\"},{\"name\":\"time\",\"req\":\"^0.3.47\"},{\"name\":\"url\",\"req\":\"^2.3.1\"}],\"features\":{\"default\":[\"public_suffix\",\"serde_json\"],\"log_secure_cookie_values\":[],\"preserve_order\":[\"dep:indexmap\"],\"public_suffix\":[\"dep:publicsuffix\"],\"serde\":[\"dep:serde\",\"dep:serde_derive\"],\"serde_json\":[\"serde\",\"dep:serde_json\"],\"serde_ron\":[\"serde\",\"dep:ron\"],\"wasm-bindgen\":[\"time/wasm-bindgen\"]}}",
"core-foundation-sys_0.8.7": "{\"dependencies\":[],\"features\":{\"default\":[\"link\"],\"link\":[],\"mac_os_10_7_support\":[],\"mac_os_10_8_features\":[]}}",
"core-foundation_0.10.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"core-foundation-sys\",\"req\":\"^0.8\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[\"link\"],\"link\":[\"core-foundation-sys/link\"],\"mac_os_10_7_support\":[\"core-foundation-sys/mac_os_10_7_support\"],\"mac_os_10_8_features\":[\"core-foundation-sys/mac_os_10_8_features\"],\"with-uuid\":[\"dep:uuid\"]}}",
"core-foundation_0.9.4": "{\"dependencies\":[{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"core-foundation-sys\",\"req\":\"^0.8.6\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^0.5\"}],\"features\":{\"default\":[\"link\"],\"link\":[\"core-foundation-sys/link\"],\"mac_os_10_7_support\":[\"core-foundation-sys/mac_os_10_7_support\"],\"mac_os_10_8_features\":[\"core-foundation-sys/mac_os_10_8_features\"],\"with-chrono\":[\"chrono\"],\"with-uuid\":[\"uuid\"]}}",
@@ -833,6 +835,7 @@
"display_container_0.9.0": "{\"dependencies\":[{\"name\":\"either\",\"req\":\"^1.8\"},{\"name\":\"indenter\",\"req\":\"^0.3.3\"}],\"features\":{}}",
"displaydoc_0.2.5": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^0.6.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^1.0.24\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
"dns-lookup_3.0.1": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"features\":[\"Win32_Networking_WinSock\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\"^0.60\",\"target\":\"cfg(windows)\"}],\"features\":{}}",
"document-features_0.2.12": "{\"dependencies\":[{\"name\":\"litrs\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[],\"self-test\":[]}}",
"dotenvy_0.15.7": "{\"dependencies\":[{\"name\":\"clap\",\"optional\":true,\"req\":\"^3.2\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.16.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.3.0\"}],\"features\":{\"cli\":[\"clap\"]}}",
"downcast-rs_1.2.1": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
"dtor-proc-macro_0.0.6": "{\"dependencies\":[],\"features\":{\"default\":[]}}",
@@ -1140,6 +1143,7 @@
"linux-raw-sys_0.12.1": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.100\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"auxvec\":[],\"bootparam\":[],\"btrfs\":[],\"default\":[\"std\",\"general\",\"errno\"],\"elf\":[],\"elf_uapi\":[],\"errno\":[],\"general\":[],\"if_arp\":[],\"if_ether\":[],\"if_packet\":[],\"if_tun\":[],\"image\":[],\"io_uring\":[],\"ioctl\":[],\"landlock\":[],\"loop_device\":[],\"mempolicy\":[],\"net\":[],\"netlink\":[],\"no_std\":[],\"prctl\":[],\"ptrace\":[],\"rustc-dep-of-std\":[\"core\",\"no_std\"],\"std\":[],\"system\":[],\"vm_sockets\":[],\"xdp\":[]}}",
"linux-raw-sys_0.4.15": "{\"dependencies\":[{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.49\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.100\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"bootparam\":[],\"btrfs\":[],\"default\":[\"std\",\"general\",\"errno\"],\"elf\":[],\"elf_uapi\":[],\"errno\":[],\"general\":[],\"if_arp\":[],\"if_ether\":[],\"if_packet\":[],\"io_uring\":[],\"ioctl\":[],\"landlock\":[],\"loop_device\":[],\"mempolicy\":[],\"net\":[],\"netlink\":[],\"no_std\":[],\"prctl\":[],\"ptrace\":[],\"rustc-dep-of-std\":[\"core\",\"compiler_builtins\",\"no_std\"],\"std\":[],\"system\":[],\"xdp\":[]}}",
"litemap_0.8.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"use-std\"],\"kind\":\"dev\",\"name\":\"postcard\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"validation\"],\"kind\":\"dev\",\"name\":\"rkyv\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"serde_core\",\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"yoke\",\"optional\":true,\"req\":\"^0.8.0\"}],\"features\":{\"alloc\":[],\"databake\":[\"dep:databake\"],\"default\":[\"alloc\"],\"serde\":[\"dep:serde_core\",\"alloc\"],\"testing\":[\"alloc\"],\"yoke\":[\"dep:yoke\"]}}",
"litrs_1.0.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"optional\":true,\"req\":\"^1.0.63\"},{\"name\":\"unicode-xid\",\"optional\":true,\"req\":\"^0.2.4\"}],\"features\":{\"check_suffix\":[\"unicode-xid\"]}}",
"local-waker_0.1.4": "{\"dependencies\":[],\"features\":{}}",
"lock_api_0.4.14": "{\"dependencies\":[{\"name\":\"owning_ref\",\"optional\":true,\"req\":\"^0.4.1\"},{\"default_features\":false,\"name\":\"scopeguard\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.126\"}],\"features\":{\"arc_lock\":[],\"atomic_usize\":[],\"default\":[\"atomic_usize\"],\"nightly\":[]}}",
"log_0.4.29": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.63\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"sval\",\"optional\":true,\"req\":\"^2.16\"},{\"kind\":\"dev\",\"name\":\"sval\",\"req\":\"^2.16\"},{\"kind\":\"dev\",\"name\":\"sval_derive\",\"req\":\"^2.16\"},{\"default_features\":false,\"name\":\"sval_ref\",\"optional\":true,\"req\":\"^2.16\"},{\"default_features\":false,\"features\":[\"inline-i128\"],\"name\":\"value-bag\",\"optional\":true,\"req\":\"^1.12\"},{\"features\":[\"test\"],\"kind\":\"dev\",\"name\":\"value-bag\",\"req\":\"^1.12\"}],\"features\":{\"kv\":[],\"kv_serde\":[\"kv_std\",\"value-bag/serde\",\"serde\"],\"kv_std\":[\"std\",\"kv\",\"value-bag/error\"],\"kv_sval\":[\"kv\",\"value-bag/sval\",\"sval\",\"sval_ref\"],\"kv_unstable\":[\"kv\",\"value-bag\"],\"kv_unstable_serde\":[\"kv_serde\",\"kv_unstable_std\"],\"kv_unstable_std\":[\"kv_std\",\"kv_unstable\"],\"kv_unstable_sval\":[\"kv_sval\",\"kv_unstable\"],\"max_level_debug\":[],\"max_level_error\":[],\"max_level_info\":[],\"max_level_off\":[],\"max_level_trace\":[],\"max_level_warn\":[],\"release_max_level_debug\":[],\"release_max_level_error\":[],\"release_max_level_info\":[],\"release_max_level_off\":[],\"release_max_level_trace\":[],\"release_max_level_warn\":[],\"serde\":[\"serde_core\"],\"std\":[]}}",
@@ -1324,6 +1328,7 @@
"protoc-gen-tonic_0.4.1": "{\"dependencies\":[{\"name\":\"heck\",\"req\":\"^0.5.0\"},{\"name\":\"prettyplease\",\"req\":\"^0.2.9\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"prost\",\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"prost-build\",\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"prost-types\",\"req\":\"^0.13.1\"},{\"name\":\"protoc-gen-prost\",\"req\":\"^0.4.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"regex\",\"req\":\"^1.5.5\"},{\"features\":[\"parsing\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0.22\"},{\"name\":\"tonic-build\",\"req\":\"^0.12.0\"}],\"features\":{}}",
"psl-types_2.0.11": "{\"dependencies\":[],\"features\":{}}",
"psl_2.1.184": "{\"dependencies\":[{\"name\":\"psl-types\",\"req\":\"^2.0.11\"},{\"kind\":\"dev\",\"name\":\"rspec\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"helpers\"],\"helpers\":[]}}",
"publicsuffix_2.3.0": "{\"dependencies\":[{\"features\":[\"inline-more\"],\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15.1\"},{\"name\":\"idna\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"psl-types\",\"req\":\"^2.0.11\"},{\"kind\":\"dev\",\"name\":\"rspec\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"unicase\",\"optional\":true,\"req\":\"^2.6.0\"}],\"features\":{\"anycase\":[\"unicase\"],\"default\":[\"punycode\"],\"punycode\":[\"idna\"],\"std\":[]}}",
"pulldown-cmark-escape_0.10.1": "{\"dependencies\":[],\"features\":{\"simd\":[]}}",
"pulldown-cmark_0.10.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"name\":\"bitflags\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"getopts\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"memchr\",\"req\":\"^2.5\"},{\"name\":\"pulldown-cmark-escape\",\"optional\":true,\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.6\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.61\"},{\"name\":\"unicase\",\"req\":\"^2.6\"}],\"features\":{\"default\":[\"getopts\",\"html\"],\"gen-tests\":[],\"html\":[\"pulldown-cmark-escape\"],\"simd\":[\"pulldown-cmark-escape?/simd\"]}}",
"pxfm_0.1.27": "{\"dependencies\":[{\"name\":\"num-traits\",\"req\":\"^0.2.3\"}],\"features\":{}}",
+56
View File
@@ -3481,6 +3481,17 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "cookie-factory"
version = "0.3.3"
@@ -3490,6 +3501,24 @@ dependencies = [
"futures",
]
[[package]]
name = "cookie_store"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
dependencies = [
"cookie",
"document-features",
"idna",
"log",
"publicsuffix",
"serde",
"serde_derive",
"serde_json",
"time",
"url",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -4344,6 +4373,15 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "document-features"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
dependencies = [
"litrs",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
@@ -7432,6 +7470,12 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "litrs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "livekit-protocol"
version = "0.7.1"
@@ -9237,6 +9281,16 @@ version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
[[package]]
name = "publicsuffix"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
dependencies = [
"idna",
"psl-types",
]
[[package]]
name = "pulldown-cmark"
version = "0.10.3"
@@ -9921,6 +9975,8 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
"cookie",
"cookie_store",
"encoding_rs",
"futures-channel",
"futures-core",
+1 -1
View File
@@ -294,7 +294,7 @@ ratatui = "0.29.0"
ratatui-macros = "0.6.0"
regex = "1.12.3"
regex-lite = "0.1.8"
reqwest = "0.12"
reqwest = { version = "0.12", features = ["cookies"] }
rmcp = { version = "0.15.0", default-features = false }
runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" }
rustls = { version = "0.23", default-features = false, features = [
@@ -105,7 +105,7 @@ pub(crate) struct ServerEnvelope {
pub(crate) seq_id: u64,
}
fn is_allowed_chatgpt_host(host: &Option<Host<&str>>) -> bool {
fn is_allowed_remote_control_chatgpt_host(host: &Option<Host<&str>>) -> bool {
let Some(Host::Domain(host)) = *host else {
return false;
};
@@ -156,7 +156,7 @@ pub(super) fn normalize_remote_control_url(
.map_err(map_url_parse_error)?;
let host = enroll_url.host();
match enroll_url.scheme() {
"https" if is_localhost(&host) || is_allowed_chatgpt_host(&host) => {
"https" if is_localhost(&host) || is_allowed_remote_control_chatgpt_host(&host) => {
websocket_url.set_scheme("wss").map_err(map_scheme_error)?;
}
"http" if is_localhost(&host) => {
@@ -232,6 +232,7 @@ mod tests {
"http://chatgpt.com/backend-api",
"http://example.com/backend-api",
"https://example.com/backend-api",
"https://chat.openai.com/backend-api",
"https://chatgpt.com.evil.com/backend-api",
"https://evilchatgpt.com/backend-api",
"https://foo.localhost/backend-api",
+4 -1
View File
@@ -6,6 +6,7 @@ use crate::types::RateLimitStatusPayload;
use crate::types::TurnAttemptsSiblingTurnsResponse;
use anyhow::Result;
use codex_client::build_reqwest_client_with_custom_ca;
use codex_client::with_chatgpt_cloudflare_cookie_store;
use codex_login::CodexAuth;
use codex_login::default_client::get_codex_user_agent;
use codex_protocol::account::PlanType as AccountPlanType;
@@ -137,7 +138,9 @@ impl Client {
{
base_url = format!("{base_url}/backend-api");
}
let http = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?;
let http = build_reqwest_client_with_custom_ca(with_chatgpt_cloudflare_cookie_store(
reqwest::Client::builder(),
))?;
let path_style = PathStyle::from_base_url(&base_url);
Ok(Self {
base_url,
@@ -0,0 +1,264 @@
use std::sync::Arc;
use std::sync::LazyLock;
use reqwest::cookie::CookieStore;
use reqwest::cookie::Jar;
use reqwest::header::HeaderValue;
use crate::chatgpt_hosts::is_allowed_chatgpt_host;
// WARNING: this store is process-global and may be shared across auth contexts.
// It must only ever contain Cloudflare infrastructure cookies. Never extend this
// store to persist ChatGPT account, session, auth, or other user-specific cookie
// data.
static SHARED_CHATGPT_CLOUDFLARE_COOKIE_STORE: LazyLock<Arc<ChatGptCloudflareCookieStore>> =
LazyLock::new(|| Arc::new(ChatGptCloudflareCookieStore::default()));
#[derive(Debug, Default)]
struct ChatGptCloudflareCookieStore {
jar: Jar,
}
impl CookieStore for ChatGptCloudflareCookieStore {
fn set_cookies(
&self,
cookie_headers: &mut dyn Iterator<Item = &HeaderValue>,
url: &reqwest::Url,
) {
if !is_chatgpt_cookie_url(url) {
return;
}
let mut cloudflare_cookie_headers =
cookie_headers.filter(|header| is_allowed_cloudflare_set_cookie_header(header));
self.jar.set_cookies(&mut cloudflare_cookie_headers, url);
}
fn cookies(&self, url: &reqwest::Url) -> Option<HeaderValue> {
if is_chatgpt_cookie_url(url) {
self.jar.cookies(url).and_then(only_cloudflare_cookies)
} else {
None
}
}
}
/// Adds the process-local ChatGPT Cloudflare cookie jar used by Codex HTTP clients.
///
/// WARNING: this jar is global within the process. It is only acceptable because it hardcodes a
/// small allowlist of Cloudflare cookie names and refuses all other ChatGPT cookies. Do not store
/// ChatGPT account, session, auth, or other user-specific cookies here. If a future caller needs
/// those cookies, the store must be scoped to the auth/session owner instead of shared globally.
pub fn with_chatgpt_cloudflare_cookie_store(
builder: reqwest::ClientBuilder,
) -> reqwest::ClientBuilder {
builder.cookie_provider(Arc::clone(&SHARED_CHATGPT_CLOUDFLARE_COOKIE_STORE))
}
fn is_chatgpt_cookie_url(url: &reqwest::Url) -> bool {
match url.scheme() {
"https" => {}
_ => return false,
}
let Some(host) = url.host_str() else {
return false;
};
is_allowed_chatgpt_host(host)
}
fn is_allowed_cloudflare_set_cookie_header(header: &HeaderValue) -> bool {
header
.to_str()
.ok()
.and_then(set_cookie_name)
.is_some_and(is_allowed_cloudflare_cookie_name)
}
fn set_cookie_name(header: &str) -> Option<&str> {
let (name, _) = header.split_once('=')?;
let name = name.trim();
(!name.is_empty()).then_some(name)
}
fn only_cloudflare_cookies(header: HeaderValue) -> Option<HeaderValue> {
let header = header.to_str().ok()?;
let cookies = header
.split(';')
.filter_map(|cookie| {
let cookie = cookie.trim();
let name = cookie.split_once('=')?.0.trim();
is_allowed_cloudflare_cookie_name(name).then_some(cookie)
})
.collect::<Vec<_>>()
.join("; ");
if cookies.is_empty() {
None
} else {
HeaderValue::from_str(&cookies).ok()
}
}
fn is_allowed_cloudflare_cookie_name(name: &str) -> bool {
// Keep this allowlist aligned with Cloudflare's documented service cookies:
// https://developers.cloudflare.com/fundamentals/reference/policies-compliances/cloudflare-cookies/
matches!(
name,
"__cf_bm"
| "__cflb"
| "__cfruid"
| "__cfseq"
| "__cfwaitingroom"
| "_cfuvid"
| "cf_clearance"
| "cf_ob_info"
| "cf_use_ob"
) || name.starts_with("cf_chl_")
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use reqwest::cookie::CookieStore;
#[test]
fn stores_and_returns_cloudflare_cookies_for_chatgpt_hosts() {
let store = ChatGptCloudflareCookieStore::default();
let url = reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses").unwrap();
let cfuvid = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly");
let clearance =
HeaderValue::from_static("cf_clearance=clearance; Path=/; Secure; HttpOnly");
store.set_cookies(&mut [&cfuvid, &clearance].into_iter(), &url);
let mut cookies = store
.cookies(&url)
.and_then(|value| value.to_str().ok().map(str::to_string))
.map(|header| {
header
.split("; ")
.map(str::to_string)
.collect::<Vec<String>>()
})
.unwrap_or_default();
cookies.sort();
assert_eq!(
cookies,
vec![
"_cfuvid=visitor".to_string(),
"cf_clearance=clearance".to_string()
]
);
}
#[test]
fn ignores_non_chatgpt_cookies() {
let store = ChatGptCloudflareCookieStore::default();
let url = reqwest::Url::parse("https://api.openai.com/v1/responses").unwrap();
let set_cookie = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly");
store.set_cookies(&mut std::iter::once(&set_cookie), &url);
assert_eq!(store.cookies(&url), None);
}
#[test]
fn ignores_non_cloudflare_cookies_for_chatgpt_hosts() {
let store = ChatGptCloudflareCookieStore::default();
let url = reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses").unwrap();
let set_cookie = HeaderValue::from_static(
"__Secure-next-auth.session-token=secret; Path=/; Secure; HttpOnly",
);
store.set_cookies(&mut std::iter::once(&set_cookie), &url);
assert_eq!(store.cookies(&url), None);
}
#[test]
fn ignores_mixed_non_cloudflare_cookies_for_chatgpt_hosts() {
let store = ChatGptCloudflareCookieStore::default();
let url = reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses").unwrap();
let cfuvid = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly");
let account_cookie =
HeaderValue::from_static("chatgpt_session=secret; Path=/; Secure; HttpOnly");
store.set_cookies(&mut [&cfuvid, &account_cookie].into_iter(), &url);
assert_eq!(
store
.cookies(&url)
.and_then(|value| value.to_str().ok().map(str::to_string)),
Some("_cfuvid=visitor".to_string())
);
}
#[test]
fn does_not_return_chatgpt_cloudflare_cookies_for_other_hosts() {
let store = ChatGptCloudflareCookieStore::default();
let chatgpt_url =
reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses").unwrap();
let api_url = reqwest::Url::parse("https://api.openai.com/v1/responses").unwrap();
let set_cookie = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly");
store.set_cookies(&mut std::iter::once(&set_cookie), &chatgpt_url);
assert_eq!(store.cookies(&api_url), None);
}
#[test]
fn rejects_plain_http_chatgpt_cookie_urls() {
let store = ChatGptCloudflareCookieStore::default();
let http_url = reqwest::Url::parse("http://chatgpt.com/backend-api/codex/responses")
.expect("URL should parse");
let https_url = reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses")
.expect("URL should parse");
let set_cookie = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly");
store.set_cookies(&mut std::iter::once(&set_cookie), &http_url);
assert_eq!(store.cookies(&http_url), None);
assert_eq!(store.cookies(&https_url), None);
}
#[test]
fn only_allows_https_urls() {
let url = reqwest::Url::parse("http://chatgpt.com/backend-api/codex/responses").unwrap();
assert!(!is_chatgpt_cookie_url(&url));
let url = reqwest::Url::parse("wss://chatgpt.com/backend-api/codex/responses").unwrap();
assert!(!is_chatgpt_cookie_url(&url));
}
#[test]
fn allows_only_known_cloudflare_cookie_names() {
for name in [
"__cf_bm",
"__cflb",
"__cfruid",
"__cfseq",
"__cfwaitingroom",
"_cfuvid",
"cf_clearance",
"cf_ob_info",
"cf_use_ob",
"cf_chl_rc_i",
] {
assert!(is_allowed_cloudflare_cookie_name(name));
}
for name in [
"__Secure-next-auth.session-token",
"chatgpt_session",
"oai-auth-token",
"not_cf_clearance",
] {
assert!(!is_allowed_cloudflare_cookie_name(name));
}
}
}
@@ -0,0 +1,39 @@
/// Returns whether `host` is one of the ChatGPT hosts Codex is allowed to treat
/// as first-party ChatGPT traffic.
pub fn is_allowed_chatgpt_host(host: &str) -> bool {
const EXACT_HOSTS: &[&str] = &["chatgpt.com", "chat.openai.com", "chatgpt-staging.com"];
const SUBDOMAIN_SUFFIXES: &[&str] = &[".chatgpt.com", ".chatgpt-staging.com"];
EXACT_HOSTS.contains(&host)
|| SUBDOMAIN_SUFFIXES
.iter()
.any(|suffix| host.ends_with(suffix))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recognizes_chatgpt_hosts_without_suffix_tricks() {
for host in [
"chatgpt.com",
"foo.chatgpt.com",
"staging.chatgpt.com",
"chat.openai.com",
"chatgpt-staging.com",
"api.chatgpt-staging.com",
] {
assert!(is_allowed_chatgpt_host(host));
}
for host in [
"evilchatgpt.com",
"chatgpt.com.evil.example",
"api.openai.com",
"foo.chat.openai.com",
] {
assert!(!is_allowed_chatgpt_host(host));
}
}
}
+4
View File
@@ -1,3 +1,5 @@
mod chatgpt_cloudflare_cookies;
mod chatgpt_hosts;
mod custom_ca;
mod default_client;
mod error;
@@ -7,6 +9,8 @@ mod sse;
mod telemetry;
mod transport;
pub use crate::chatgpt_cloudflare_cookies::with_chatgpt_cloudflare_cookie_store;
pub use crate::chatgpt_hosts::is_allowed_chatgpt_host;
pub use crate::custom_ca::BuildCustomCaTransportError;
/// Test-only subprocess hook for custom CA coverage.
///
+11 -1
View File
@@ -8,6 +8,7 @@ use codex_client::BuildCustomCaTransportError;
use codex_client::CodexHttpClient;
pub use codex_client::CodexRequestBuilder;
use codex_client::build_reqwest_client_with_custom_ca;
use codex_client::with_chatgpt_cloudflare_cookie_store;
use codex_terminal_detection::user_agent;
use reqwest::header::HeaderMap;
use reqwest::header::HeaderValue;
@@ -201,7 +202,15 @@ pub fn create_client() -> CodexHttpClient {
pub fn build_reqwest_client() -> reqwest::Client {
try_build_reqwest_client().unwrap_or_else(|error| {
tracing::warn!(error = %error, "failed to build default reqwest client");
reqwest::Client::new()
with_chatgpt_cloudflare_cookie_store(reqwest::Client::builder())
.build()
.unwrap_or_else(|fallback_error| {
tracing::warn!(
error = %fallback_error,
"failed to build fallback reqwest client with ChatGPT Cloudflare cookie store"
);
reqwest::Client::new()
})
})
}
@@ -219,6 +228,7 @@ pub fn try_build_reqwest_client() -> Result<reqwest::Client, BuildCustomCaTransp
if is_sandboxed() {
builder = builder.no_proxy();
}
builder = with_chatgpt_cloudflare_cookie_store(builder);
build_reqwest_client_with_custom_ca(builder)
}