Compare commits

...

1 Commits

Author SHA1 Message Date
Chris Bookholt
9176718eb7 cloud-tasks: tighten backend request setup
Co-authored-by: Codex <noreply@openai.com>
2026-05-11 20:37:43 +00:00
5 changed files with 100 additions and 28 deletions

View File

@@ -94,10 +94,12 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result<BackendContext>
std::process::exit(1);
}
let auth_provider = codex_model_provider::auth_provider_from_auth(&auth);
http = http.with_auth_provider(auth_provider);
if let Some(acc) = auth.get_account_id() {
append_error_log(format!("auth: set ChatGPT-Account-Id header: {acc}"));
if util::should_attach_chatgpt_auth(&base_url) {
let auth_provider = codex_model_provider::auth_provider_from_auth(&auth);
http = http.with_auth_provider(auth_provider);
if let Some(acc) = auth.get_account_id() {
append_error_log(format!("auth: set ChatGPT-Account-Id header: {acc}"));
}
}
Ok(BackendContext {
@@ -185,7 +187,7 @@ async fn resolve_environment_id(ctx: &BackendContext, requested: &str) -> anyhow
return Err(anyhow!("environment id must not be empty"));
}
let normalized = util::normalize_base_url(&ctx.base_url);
let headers = util::build_chatgpt_headers().await;
let headers = util::build_chatgpt_headers(&normalized).await;
let environments = crate::env_detect::list_environments(&normalized, &headers).await?;
if environments.is_empty() {
return Err(anyhow!(
@@ -841,7 +843,7 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
&std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()),
);
let headers = util::build_chatgpt_headers().await;
let headers = util::build_chatgpt_headers(&base_url).await;
let res = crate::env_detect::list_environments(&base_url, &headers).await;
let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res));
});
@@ -857,7 +859,7 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()),
);
// Build headers: UA + ChatGPT auth if available
let headers = util::build_chatgpt_headers().await;
let headers = util::build_chatgpt_headers(&base_url).await;
// Run autodetect. If it fails, we keep using "All".
let res = crate::env_detect::autodetect_environment_id(
@@ -1082,7 +1084,8 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
&std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()),
);
let headers = crate::util::build_chatgpt_headers().await;
let headers =
crate::util::build_chatgpt_headers(&base_url).await;
let res = crate::env_detect::list_environments(&base_url, &headers).await;
let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res));
});
@@ -1465,7 +1468,7 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
let tx = tx.clone();
tokio::spawn(async move {
let base_url = crate::util::normalize_base_url(&std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()));
let headers = crate::util::build_chatgpt_headers().await;
let headers = crate::util::build_chatgpt_headers(&base_url).await;
let res = crate::env_detect::list_environments(&base_url, &headers).await;
let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res));
});
@@ -1654,7 +1657,8 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
&std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()),
);
let headers = crate::util::build_chatgpt_headers().await;
let headers =
crate::util::build_chatgpt_headers(&base_url).await;
let res = crate::env_detect::list_environments(&base_url, &headers).await;
let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res));
});
@@ -1830,7 +1834,8 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
let tx = tx.clone();
tokio::spawn(async move {
let base_url = crate::util::normalize_base_url(&std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()));
let headers = crate::util::build_chatgpt_headers().await;
let headers =
crate::util::build_chatgpt_headers(&base_url).await;
let res = crate::env_detect::list_environments(&base_url, &headers).await;
let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res));
});

View File

@@ -1,6 +1,7 @@
use chrono::DateTime;
use chrono::Local;
use chrono::Utc;
use codex_client::is_allowed_chatgpt_url;
use reqwest::header::HeaderMap;
use codex_core::config::Config;
@@ -32,15 +33,18 @@ pub fn normalize_base_url(input: &str) -> String {
while base_url.ends_with('/') {
base_url.pop();
}
if (base_url.starts_with("https://chatgpt.com")
|| base_url.starts_with("https://chat.openai.com"))
&& !base_url.contains("/backend-api")
{
if should_attach_chatgpt_auth(&base_url) && !base_url.contains("/backend-api") {
base_url = format!("{base_url}/backend-api");
}
base_url
}
pub(crate) fn should_attach_chatgpt_auth(base_url: &str) -> bool {
reqwest::Url::parse(base_url)
.ok()
.is_some_and(|url| is_allowed_chatgpt_url(&url))
}
pub async fn load_auth_manager(chatgpt_base_url: Option<String>) -> Option<AuthManager> {
// TODO: pass in cli overrides once cloud tasks properly support them.
let config = Config::load_with_cli_overrides(Vec::new()).await.ok()?;
@@ -57,7 +61,7 @@ pub async fn load_auth_manager(chatgpt_base_url: Option<String>) -> Option<AuthM
/// Build headers for ChatGPT-backed requests: `User-Agent`, optional `Authorization`,
/// and optional `ChatGPT-Account-Id`.
pub async fn build_chatgpt_headers() -> HeaderMap {
pub async fn build_chatgpt_headers(base_url: &str) -> HeaderMap {
use reqwest::header::HeaderValue;
use reqwest::header::USER_AGENT;
@@ -68,7 +72,8 @@ pub async fn build_chatgpt_headers() -> HeaderMap {
USER_AGENT,
HeaderValue::from_str(&ua).unwrap_or(HeaderValue::from_static("codex-cli")),
);
if let Some(am) = load_auth_manager(/*chatgpt_base_url*/ None).await
if should_attach_chatgpt_auth(base_url)
&& let Some(am) = load_auth_manager(/*chatgpt_base_url*/ None).await
&& let Some(auth) = am.auth().await
&& auth.uses_codex_backend()
{
@@ -115,3 +120,45 @@ pub fn format_relative_time(reference: DateTime<Utc>, ts: DateTime<Utc>) -> Stri
pub fn format_relative_time_now(ts: DateTime<Utc>) -> String {
format_relative_time(Utc::now(), ts)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn normalize_base_url_only_rewrites_allowed_chatgpt_origins() {
assert_eq!(
normalize_base_url("https://chatgpt.com"),
"https://chatgpt.com/backend-api"
);
assert_eq!(
normalize_base_url("https://chat.openai.com/"),
"https://chat.openai.com/backend-api"
);
assert_eq!(
normalize_base_url("https://chatgpt.com.fromspeech.ai/"),
"https://chatgpt.com.fromspeech.ai"
);
assert_eq!(
normalize_base_url("http://chatgpt.com/"),
"http://chatgpt.com"
);
}
#[test]
fn allowed_chatgpt_base_urls_require_https_and_exact_first_party_hosts() {
assert!(should_attach_chatgpt_auth(
"https://chatgpt.com/backend-api"
));
assert!(should_attach_chatgpt_auth(
"https://foo.chatgpt.com/backend-api"
));
assert!(!should_attach_chatgpt_auth(
"https://chatgpt.com.fromspeech.ai/backend-api"
));
assert!(!should_attach_chatgpt_auth(
"http://chatgpt.com/backend-api"
));
}
}

View File

@@ -5,7 +5,7 @@ use reqwest::cookie::CookieStore;
use reqwest::cookie::Jar;
use reqwest::header::HeaderValue;
use crate::chatgpt_hosts::is_allowed_chatgpt_host;
use crate::chatgpt_hosts::is_allowed_chatgpt_url;
// WARNING: this store is process-global and may be shared across auth contexts.
// It must only ever contain Cloudflare infrastructure cookies. Never extend this
@@ -56,16 +56,7 @@ pub fn with_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)
is_allowed_chatgpt_url(url)
}
fn is_allowed_cloudflare_set_cookie_header(header: &HeaderValue) -> bool {

View File

@@ -10,6 +10,11 @@ pub fn is_allowed_chatgpt_host(host: &str) -> bool {
.any(|suffix| host.ends_with(suffix))
}
/// Returns whether `url` is an HTTPS URL targeting a first-party ChatGPT host.
pub fn is_allowed_chatgpt_url(url: &reqwest::Url) -> bool {
url.scheme() == "https" && url.host_str().is_some_and(is_allowed_chatgpt_host)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -36,4 +41,27 @@ mod tests {
assert!(!is_allowed_chatgpt_host(host));
}
}
#[test]
fn recognizes_chatgpt_urls_without_origin_confusion() {
for url in [
"https://chatgpt.com/backend-api",
"https://foo.chatgpt.com/backend-api",
"https://chat.openai.com/backend-api",
"https://api.chatgpt-staging.com/backend-api",
] {
let parsed = reqwest::Url::parse(url).expect("test URL should parse");
assert!(is_allowed_chatgpt_url(&parsed));
}
for url in [
"http://chatgpt.com/backend-api",
"https://chatgpt.com.fromspeech.ai/backend-api",
"https://chat.openai.com.evil.example/backend-api",
"https://api.openai.com/v1/responses",
] {
let parsed = reqwest::Url::parse(url).expect("test URL should parse");
assert!(!is_allowed_chatgpt_url(&parsed));
}
}
}

View File

@@ -11,6 +11,7 @@ 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::chatgpt_hosts::is_allowed_chatgpt_url;
pub use crate::custom_ca::BuildCustomCaTransportError;
/// Test-only subprocess hook for custom CA coverage.
///