mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Share ChatGPT cookies between HTTP and WebSocket transports (#46506)
## Why WebSocket handshakes did not reuse the HTTP cookie store or retain response cookies, so routing cookies such as `__oailb` were unavailable to subsequent connections. ## What changed - Reuse the HTTP factory's ChatGPT cookies for secure WebSocket handshakes, preserving explicit `Cookie` headers and marking generated headers sensitive. - Retain allowlisted infrastructure cookies from both successful and rejected upgrades. Keep configured cookies scoped to their factory and exclude account and session cookies from the shared store. - Apply HTTPS cookie scope to `wss` requests, preserving host and path restrictions and excluding insecure `ws` requests and non-ChatGPT hosts. ## Testing Add HTTP/WebSocket cookie-sharing coverage and local TLS handshake tests for routing-cookie reuse across connectors, rejected-upgrade refreshes, explicit header precedence, cookie scope, session-cookie exclusion, and deletion. GitOrigin-RevId: 6bd6afe16e97cf9758ca7ba207a4e88c969497a4
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
//! ChatGPT cookies shared by HTTP and WebSocket transports. Only infrastructure cookies may be
|
||||
//! stored globally; configured cookies remain scoped to their factory.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use http::HeaderMap;
|
||||
use http::HeaderValue;
|
||||
use http::Uri;
|
||||
use http::header::SET_COOKIE;
|
||||
use reqwest::cookie::CookieStore;
|
||||
use reqwest::cookie::Jar;
|
||||
use reqwest::header::HeaderValue;
|
||||
|
||||
use crate::HttpClientFactory;
|
||||
use crate::chatgpt_hosts::is_allowed_chatgpt_host;
|
||||
|
||||
// WARNING: this HTTP cookie store is process-global and may be shared across auth contexts.
|
||||
@@ -96,6 +103,38 @@ impl CookieStore for ChatGptCookieStore {
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClientFactory {
|
||||
/// Returns cookies for a ChatGPT HTTPS or WSS request, using the same store as HTTP clients.
|
||||
/// Explicit request Cookie headers should take precedence over this value.
|
||||
pub fn chatgpt_cookie_header(&self, uri: &Uri) -> Option<HeaderValue> {
|
||||
let url = chatgpt_cookie_url(uri)?;
|
||||
let mut cookies = match self.chatgpt_cookie_store() {
|
||||
Some(store) => store.cookies(&url),
|
||||
None => SHARED_CHATGPT_CLOUDFLARE_COOKIE_STORE.cookies(&url),
|
||||
}?;
|
||||
cookies.set_sensitive(true);
|
||||
Some(cookies)
|
||||
}
|
||||
|
||||
/// Retains only allowlisted infrastructure cookies from a ChatGPT HTTPS or WSS response.
|
||||
/// Account and session cookies are never added to the shared store.
|
||||
pub fn store_chatgpt_response_cookies(&self, uri: &Uri, headers: &HeaderMap) {
|
||||
if let Some(url) = chatgpt_cookie_url(uri) {
|
||||
SHARED_CHATGPT_CLOUDFLARE_COOKIE_STORE
|
||||
.set_cookies(&mut headers.get_all(SET_COOKIE).iter(), &url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn chatgpt_cookie_url(uri: &Uri) -> Option<reqwest::Url> {
|
||||
let mut url = reqwest::Url::parse(&uri.to_string()).ok()?;
|
||||
// A secure WebSocket handshake has the same cookie scope as HTTPS.
|
||||
if url.scheme() == "wss" {
|
||||
url.set_scheme("https").ok()?;
|
||||
}
|
||||
is_chatgpt_cookie_url(&url).then_some(url)
|
||||
}
|
||||
|
||||
/// Adds the process-local ChatGPT infrastructure cookie jar used by Codex HTTP clients.
|
||||
///
|
||||
/// WARNING: this jar is global within the process. It is only acceptable because it hardcodes a
|
||||
@@ -177,9 +216,52 @@ fn is_allowed_cloudflare_cookie_name(name: &str) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::OutboundProxyPolicy;
|
||||
use pretty_assertions::assert_eq;
|
||||
use reqwest::cookie::CookieStore;
|
||||
|
||||
#[test]
|
||||
fn http_and_websocket_cookies_share_the_factory_store() {
|
||||
let factory = HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault)
|
||||
.with_chatgpt_cookies([HeaderValue::from_static("configured=owner")]);
|
||||
let store = factory.chatgpt_cookie_store().unwrap();
|
||||
let https = reqwest::Url::parse("https://http-websocket-cookies.chatgpt.com/api").unwrap();
|
||||
let wss: Uri = "wss://http-websocket-cookies.chatgpt.com/api"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let cookie = HeaderValue::from_static("__oailb=from-http; Path=/; Secure");
|
||||
store.set_cookies(&mut std::iter::once(&cookie), &https);
|
||||
|
||||
let header = factory.chatgpt_cookie_header(&wss).unwrap();
|
||||
assert_eq!(header, "__oailb=from-http; configured=owner");
|
||||
assert!(header.is_sensitive());
|
||||
|
||||
let mut response_headers = HeaderMap::new();
|
||||
response_headers.insert(
|
||||
SET_COOKIE,
|
||||
HeaderValue::from_static("__oailb=from-wss; Path=/; Secure"),
|
||||
);
|
||||
factory.store_chatgpt_response_cookies(&wss, &response_headers);
|
||||
assert_eq!(
|
||||
store.cookies(&https),
|
||||
Some(HeaderValue::from_static(
|
||||
"__oailb=from-wss; configured=owner"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
factory.chatgpt_cookie_header(
|
||||
&"ws://http-websocket-cookies.chatgpt.com/api"
|
||||
.parse()
|
||||
.unwrap()
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
factory.chatgpt_cookie_header(&"wss://api.openai.com/api".parse().unwrap()),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn additional_cookies_use_current_path_scoped_cloudflare_cookies() {
|
||||
let cloudflare = Arc::new(ChatGptCloudflareCookieStore::default());
|
||||
|
||||
Reference in New Issue
Block a user