codex_login/auth/
default_client.rs1use codex_http_client::BuildRouteAwareHttpClientError;
8use codex_http_client::ClientRouteClass;
9use codex_http_client::HttpClient;
10use codex_http_client::HttpClientBuilder;
11use codex_http_client::HttpClientFactory;
12use codex_http_client::OutboundProxyPolicy;
13pub use codex_http_client::RequestBuilder as CodexRequestBuilder;
14use codex_terminal_detection::user_agent;
15use http::HeaderMap;
16use http::HeaderValue;
17use http::header::USER_AGENT;
18use std::sync::LazyLock;
19use std::sync::Mutex;
20use std::sync::RwLock;
21
22use crate::outbound_proxy::AuthRouteConfig;
23
24pub static USER_AGENT_SUFFIX: LazyLock<Mutex<Option<String>>> = LazyLock::new(|| Mutex::new(None));
40pub const DEFAULT_ORIGINATOR: &str = "codex_cli_rs";
41pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
42pub const RESIDENCY_HEADER_NAME: &str = "x-openai-internal-codex-residency";
43
44pub use codex_config::ResidencyRequirement;
45
46#[derive(Debug, Clone)]
47pub struct Originator {
48 pub value: String,
49 pub header_value: HeaderValue,
50}
51static ORIGINATOR: LazyLock<RwLock<Option<Originator>>> = LazyLock::new(|| RwLock::new(None));
52static REQUIREMENTS_RESIDENCY: LazyLock<RwLock<Option<ResidencyRequirement>>> =
53 LazyLock::new(|| RwLock::new(None));
54static ROUTE_AWARE_CLIENT_BUILD_PERMIT: tokio::sync::Semaphore =
55 tokio::sync::Semaphore::const_new(1);
56
57#[derive(Debug)]
58pub enum SetOriginatorError {
59 InvalidHeaderValue,
60 AlreadyInitialized,
61}
62
63fn get_originator_value(provided: Option<String>) -> Originator {
64 let value = std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR)
65 .ok()
66 .or(provided)
67 .unwrap_or(DEFAULT_ORIGINATOR.to_string());
68
69 match HeaderValue::from_str(&value) {
70 Ok(header_value) => Originator {
71 value,
72 header_value,
73 },
74 Err(e) => {
75 tracing::error!("Unable to turn originator override {value} into header value: {e}");
76 Originator {
77 value: DEFAULT_ORIGINATOR.to_string(),
78 header_value: HeaderValue::from_static(DEFAULT_ORIGINATOR),
79 }
80 }
81 }
82}
83
84pub fn set_default_originator(value: String) -> Result<(), SetOriginatorError> {
85 if HeaderValue::from_str(&value).is_err() {
86 return Err(SetOriginatorError::InvalidHeaderValue);
87 }
88 let originator = get_originator_value(Some(value));
89 let Ok(mut guard) = ORIGINATOR.write() else {
90 return Err(SetOriginatorError::AlreadyInitialized);
91 };
92 if guard.is_some() {
93 return Err(SetOriginatorError::AlreadyInitialized);
94 }
95 *guard = Some(originator);
96 Ok(())
97}
98
99pub fn set_default_client_residency_requirement(enforce_residency: Option<ResidencyRequirement>) {
100 let Ok(mut guard) = REQUIREMENTS_RESIDENCY.write() else {
101 tracing::warn!("Failed to acquire requirements residency lock");
102 return;
103 };
104 *guard = enforce_residency;
105}
106
107pub fn originator() -> Originator {
108 if let Ok(guard) = ORIGINATOR.read()
109 && let Some(originator) = guard.as_ref()
110 {
111 return originator.clone();
112 }
113
114 if std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR).is_ok() {
115 let originator = get_originator_value(None);
116 if let Ok(mut guard) = ORIGINATOR.write() {
117 match guard.as_ref() {
118 Some(originator) => return originator.clone(),
119 None => *guard = Some(originator.clone()),
120 }
121 }
122 return originator;
123 }
124
125 get_originator_value(None)
126}
127
128pub fn add_originator_header(headers: &mut HeaderMap, originator_value: &str) {
133 let default_originator = originator();
134 if originator_value == default_originator.value.as_str() {
135 return;
136 }
137
138 match HeaderValue::from_str(originator_value) {
139 Ok(header_value) => {
140 headers.insert("originator", header_value);
141 }
142 Err(err) => {
143 tracing::warn!("ignoring invalid thread originator header value: {err}");
144 }
145 }
146}
147
148pub fn is_first_party_originator(originator_value: &str) -> bool {
149 originator_value == DEFAULT_ORIGINATOR
150 || originator_value == "codex-tui"
151 || originator_value == "codex_vscode"
152 || originator_value.starts_with("Codex ")
153}
154
155pub fn is_first_party_chat_originator(originator_value: &str) -> bool {
156 originator_value == "codex_atlas" || originator_value == "codex_chatgpt_desktop"
157}
158
159pub fn get_codex_user_agent() -> String {
160 let build_version = env!("CARGO_PKG_VERSION");
161 let os_info = os_info::get();
162 let originator = originator();
163 let prefix = format!(
164 "{}/{build_version} ({} {}; {}) {}",
165 originator.value.as_str(),
166 os_info.os_type(),
167 os_info.version(),
168 os_info.architecture().unwrap_or("unknown"),
169 user_agent()
170 );
171 let suffix = USER_AGENT_SUFFIX
172 .lock()
173 .ok()
174 .and_then(|guard| guard.clone());
175 let suffix = suffix
176 .as_deref()
177 .map(str::trim)
178 .filter(|value| !value.is_empty())
179 .map_or_else(String::new, |value| format!(" ({value})"));
180
181 let candidate = format!("{prefix}{suffix}");
182 sanitize_user_agent(candidate, &prefix)
183}
184
185fn sanitize_user_agent(candidate: String, fallback: &str) -> String {
191 if HeaderValue::from_str(candidate.as_str()).is_ok() {
192 return candidate;
193 }
194
195 let sanitized: String = candidate
196 .chars()
197 .map(|ch| if matches!(ch, ' '..='~') { ch } else { '_' })
198 .collect();
199 if !sanitized.is_empty() && HeaderValue::from_str(sanitized.as_str()).is_ok() {
200 tracing::warn!(
201 "Sanitized Codex user agent because provided suffix contained invalid header characters"
202 );
203 sanitized
204 } else if HeaderValue::from_str(fallback).is_ok() {
205 tracing::warn!(
206 "Falling back to base Codex user agent because provided suffix could not be sanitized"
207 );
208 fallback.to_string()
209 } else {
210 tracing::warn!(
211 "Falling back to default Codex originator because base user agent string is invalid"
212 );
213 originator().value
214 }
215}
216
217pub fn create_client() -> HttpClient {
222 build_default_client(default_http_client_builder())
223}
224
225pub fn create_client_without_request_logging() -> HttpClient {
230 build_default_client(default_http_client_builder().without_request_logging())
231}
232
233pub fn create_client_for_route(
239 http_client_factory: &HttpClientFactory,
240 request_url: &str,
241 route_class: ClientRouteClass,
242) -> Result<HttpClient, BuildRouteAwareHttpClientError> {
243 if matches!(
244 http_client_factory.outbound_proxy_policy(),
245 OutboundProxyPolicy::ReqwestDefault
246 ) {
247 return Ok(create_client());
248 }
249 if is_sandboxed() {
250 return Ok(create_client());
253 }
254
255 default_http_client_builder().build_respecting_outbound_proxy_policy(
256 http_client_factory,
257 request_url,
258 route_class,
259 )
260}
261
262pub async fn create_client_for_route_async(
265 http_client_factory: HttpClientFactory,
266 request_url: String,
267 route_class: ClientRouteClass,
268) -> std::io::Result<HttpClient> {
269 let permit = ROUTE_AWARE_CLIENT_BUILD_PERMIT
270 .acquire()
271 .await
272 .map_err(std::io::Error::other)?;
273 tokio::task::spawn_blocking(move || {
274 let _permit = permit;
275 create_client_for_route(&http_client_factory, &request_url, route_class)
276 .map_err(std::io::Error::from)
277 })
278 .await
279 .map_err(std::io::Error::other)?
280}
281
282fn default_http_client_builder() -> HttpClientBuilder {
283 HttpClientBuilder::new()
284 .default_headers(default_headers())
285 .with_chatgpt_cloudflare_cookie_store()
286}
287
288#[allow(deprecated)]
291fn build_default_client(builder: HttpClientBuilder) -> HttpClient {
292 if is_sandboxed() {
293 builder.build_direct_with_custom_ca_fallback()
294 } else {
295 builder.build_with_transport_default_proxy_and_custom_ca_fallback()
296 }
297}
298
299pub(crate) fn create_raw_auth_client(
301 endpoint: &str,
302 auth_route_config: Option<&AuthRouteConfig>,
303) -> Result<HttpClient, BuildRouteAwareHttpClientError> {
304 auth_http_client_factory(auth_route_config)
305 .build_client_without_request_logging(endpoint, ClientRouteClass::Auth)
306}
307
308pub(crate) fn create_default_auth_client(
310 endpoint: &str,
311 auth_route_config: Option<&AuthRouteConfig>,
312) -> Result<HttpClient, BuildRouteAwareHttpClientError> {
313 create_client_for_route(
314 &auth_http_client_factory(auth_route_config),
315 endpoint,
316 ClientRouteClass::Auth,
317 )
318}
319
320fn auth_http_client_factory(auth_route_config: Option<&AuthRouteConfig>) -> HttpClientFactory {
321 auth_route_config.map_or_else(
322 || HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
323 |config| config.http_client_factory().clone(),
324 )
325}
326
327pub fn default_headers() -> HeaderMap {
328 let mut headers = HeaderMap::new();
329 headers.insert("originator", originator().header_value);
330 if let Ok(user_agent) = HeaderValue::from_str(&get_codex_user_agent()) {
331 headers.insert(USER_AGENT, user_agent);
332 }
333 if let Ok(guard) = REQUIREMENTS_RESIDENCY.read()
334 && let Some(requirement) = guard.as_ref()
335 && !headers.contains_key(RESIDENCY_HEADER_NAME)
336 {
337 let value = match requirement {
338 ResidencyRequirement::Us => HeaderValue::from_static("us"),
339 };
340 headers.insert(RESIDENCY_HEADER_NAME, value);
341 }
342 headers
343}
344
345fn is_sandboxed() -> bool {
346 std::env::var("CODEX_SANDBOX").as_deref() == Ok("seatbelt")
347}
348
349#[cfg(test)]
350#[path = "default_client_tests.rs"]
351mod tests;