mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
Add opt-in sanitized proxy diagnostics
This commit is contained in:
@@ -6,6 +6,7 @@ mod error;
|
||||
mod outbound_proxy;
|
||||
mod request;
|
||||
mod retry;
|
||||
mod route_diagnostics;
|
||||
mod sse;
|
||||
mod telemetry;
|
||||
mod transport;
|
||||
@@ -43,6 +44,9 @@ pub use crate::retry::RetryOn;
|
||||
pub use crate::retry::RetryPolicy;
|
||||
pub use crate::retry::backoff;
|
||||
pub use crate::retry::run_with_retry;
|
||||
pub use crate::route_diagnostics::emit_auth_http_status;
|
||||
pub use crate::route_diagnostics::emit_auth_network_environment_snapshot;
|
||||
pub use crate::route_diagnostics::emit_auth_transport_failure;
|
||||
pub use crate::sse::sse_stream;
|
||||
pub use crate::telemetry::RequestTelemetry;
|
||||
pub use crate::transport::ByteStream;
|
||||
|
||||
@@ -15,6 +15,8 @@ use std::time::Instant;
|
||||
|
||||
use crate::custom_ca::BuildCustomCaTransportError;
|
||||
use crate::custom_ca::build_reqwest_client_with_custom_ca;
|
||||
use crate::route_diagnostics::RouteDecisionSource;
|
||||
use crate::route_diagnostics::RouteDiagnostic;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use sha2::Digest;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
@@ -209,6 +211,7 @@ fn configure_proxy_for_route(
|
||||
let origin = RequestOrigin::parse(request_url);
|
||||
|
||||
if config.mode == OutboundProxyMode::Direct {
|
||||
RouteDiagnostic::direct(route_class, RouteDecisionSource::ConfigDisabled).emit_opt_in();
|
||||
return Ok(builder.no_proxy());
|
||||
}
|
||||
|
||||
@@ -217,6 +220,7 @@ fn configure_proxy_for_route(
|
||||
// `NO_PROXY` is an env-derived direct decision; reqwest remains the
|
||||
// authority for applying the env proxy contract to its own env proxies.
|
||||
if origin.as_ref().is_some_and(no_proxy_env_matches_origin) {
|
||||
RouteDiagnostic::direct(route_class, RouteDecisionSource::Env).emit_opt_in();
|
||||
if config.mode == OutboundProxyMode::Env {
|
||||
return Ok(builder.no_proxy());
|
||||
}
|
||||
@@ -233,6 +237,12 @@ fn configure_proxy_for_route(
|
||||
}
|
||||
|
||||
if !SystemProxyEnvOverride::from_env().system_discovery_enabled() {
|
||||
RouteDiagnostic::unavailable(
|
||||
route_class,
|
||||
RouteDecisionSource::ConfigDisabled,
|
||||
RouteFailureClass::ProxyResolutionUnavailable,
|
||||
)
|
||||
.emit_opt_in();
|
||||
if config.mode == OutboundProxyMode::System {
|
||||
return Err(BuildRouteAwareHttpClientError::SystemProxyUnavailable {
|
||||
route_class,
|
||||
@@ -248,6 +258,12 @@ fn configure_proxy_for_route(
|
||||
}
|
||||
|
||||
if !system_proxy_supported() {
|
||||
RouteDiagnostic::unavailable(
|
||||
route_class,
|
||||
RouteDecisionSource::UnsupportedPlatform,
|
||||
RouteFailureClass::ProxyResolutionUnavailable,
|
||||
)
|
||||
.emit_opt_in();
|
||||
if config.mode == OutboundProxyMode::System {
|
||||
return Err(BuildRouteAwareHttpClientError::SystemProxyUnavailable {
|
||||
route_class,
|
||||
@@ -263,6 +279,12 @@ fn configure_proxy_for_route(
|
||||
}
|
||||
|
||||
let Some(origin) = origin.as_ref() else {
|
||||
RouteDiagnostic::unavailable(
|
||||
route_class,
|
||||
RouteDecisionSource::ResolutionError,
|
||||
RouteFailureClass::InvalidProxyConfig,
|
||||
)
|
||||
.emit_opt_in();
|
||||
return if config.mode == OutboundProxyMode::System {
|
||||
Err(BuildRouteAwareHttpClientError::SystemProxyUnavailable {
|
||||
route_class,
|
||||
@@ -283,9 +305,15 @@ fn configure_proxy_for_route(
|
||||
OutboundProxyMode::Auto | OutboundProxyMode::System
|
||||
);
|
||||
match resolve_system_proxy(request_url, origin, include_auto_detect) {
|
||||
SystemProxyDecision::Direct => Ok(builder.no_proxy()),
|
||||
SystemProxyDecision::Proxy { url } => configure_concrete_proxy(builder, route_class, &url),
|
||||
SystemProxyDecision::Unavailable { failure } => {
|
||||
SystemProxyDecision::Direct { source } => {
|
||||
RouteDiagnostic::direct(route_class, source).emit_opt_in();
|
||||
Ok(builder.no_proxy())
|
||||
}
|
||||
SystemProxyDecision::Proxy { source, url } => {
|
||||
configure_concrete_proxy(builder, route_class, source, &url)
|
||||
}
|
||||
SystemProxyDecision::Unavailable { source, failure } => {
|
||||
RouteDiagnostic::unavailable(route_class, source, failure).emit_opt_in();
|
||||
if config.mode == OutboundProxyMode::System {
|
||||
Err(BuildRouteAwareHttpClientError::SystemProxyUnavailable {
|
||||
route_class,
|
||||
@@ -310,15 +338,23 @@ const fn system_proxy_supported() -> bool {
|
||||
fn configure_concrete_proxy(
|
||||
builder: reqwest::ClientBuilder,
|
||||
route_class: ClientRouteClass,
|
||||
source: RouteDecisionSource,
|
||||
proxy_url: &str,
|
||||
) -> Result<reqwest::ClientBuilder, BuildRouteAwareHttpClientError> {
|
||||
let proxy = match reqwest::Proxy::all(proxy_url) {
|
||||
Ok(proxy) => proxy,
|
||||
Err(_source) => {
|
||||
RouteDiagnostic::unavailable(
|
||||
route_class,
|
||||
source,
|
||||
RouteFailureClass::InvalidProxyConfig,
|
||||
)
|
||||
.emit_opt_in();
|
||||
return Err(BuildRouteAwareHttpClientError::InvalidProxyConfig { route_class });
|
||||
}
|
||||
};
|
||||
let proxy = proxy.no_proxy(reqwest::NoProxy::from_env());
|
||||
RouteDiagnostic::proxy(route_class, source, proxy_url).emit_opt_in();
|
||||
Ok(builder.proxy(proxy))
|
||||
}
|
||||
|
||||
@@ -338,18 +374,26 @@ fn configure_env_proxy_handling(
|
||||
&& let Some(proxy_url) = env_proxy_for_origin(origin)
|
||||
{
|
||||
if handling == EnvProxyHandling::ResolveConcreteOrNoProxy {
|
||||
return configure_concrete_proxy(builder, route_class, &proxy_url);
|
||||
return configure_concrete_proxy(
|
||||
builder,
|
||||
route_class,
|
||||
RouteDecisionSource::Env,
|
||||
&proxy_url,
|
||||
);
|
||||
}
|
||||
RouteDiagnostic::proxy(route_class, RouteDecisionSource::Env, &proxy_url).emit_opt_in();
|
||||
return Ok(builder);
|
||||
}
|
||||
|
||||
if conventional_proxy_env_present() {
|
||||
RouteDiagnostic::direct(route_class, RouteDecisionSource::Env).emit_opt_in();
|
||||
return match handling {
|
||||
EnvProxyHandling::ResolveConcreteOrNoProxy => Ok(builder.no_proxy()),
|
||||
EnvProxyHandling::DelegateToReqwest => Ok(builder),
|
||||
};
|
||||
}
|
||||
|
||||
RouteDiagnostic::direct(route_class, RouteDecisionSource::Direct).emit_opt_in();
|
||||
match handling {
|
||||
EnvProxyHandling::ResolveConcreteOrNoProxy => Ok(builder.no_proxy()),
|
||||
EnvProxyHandling::DelegateToReqwest => Ok(builder),
|
||||
@@ -386,17 +430,25 @@ fn default_port_for_scheme(scheme: &str) -> Option<u16> {
|
||||
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum SystemProxyDecision {
|
||||
Direct,
|
||||
Proxy { url: String },
|
||||
Unavailable { failure: RouteFailureClass },
|
||||
Direct {
|
||||
source: RouteDecisionSource,
|
||||
},
|
||||
Proxy {
|
||||
source: RouteDecisionSource,
|
||||
url: String,
|
||||
},
|
||||
Unavailable {
|
||||
source: RouteDecisionSource,
|
||||
failure: RouteFailureClass,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SystemProxyDecision> for SystemProxyRouteDecision {
|
||||
fn from(decision: SystemProxyDecision) -> Self {
|
||||
match decision {
|
||||
SystemProxyDecision::Direct => Self::Direct,
|
||||
SystemProxyDecision::Proxy { url } => Self::Proxy { url },
|
||||
SystemProxyDecision::Unavailable { failure } => Self::Unavailable { failure },
|
||||
SystemProxyDecision::Direct { .. } => Self::Direct,
|
||||
SystemProxyDecision::Proxy { url, .. } => Self::Proxy { url },
|
||||
SystemProxyDecision::Unavailable { failure, .. } => Self::Unavailable { failure },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -406,19 +458,37 @@ pub fn resolve_system_proxy_for_url(
|
||||
request_url: &str,
|
||||
include_auto_detect: bool,
|
||||
) -> SystemProxyRouteDecision {
|
||||
if !SystemProxyEnvOverride::from_env().system_discovery_enabled() || !system_proxy_supported() {
|
||||
return SystemProxyRouteDecision::Unavailable {
|
||||
let decision = if !SystemProxyEnvOverride::from_env().system_discovery_enabled() {
|
||||
SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::ConfigDisabled,
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
};
|
||||
}
|
||||
|
||||
let Some(origin) = RequestOrigin::parse(request_url) else {
|
||||
return SystemProxyRouteDecision::Unavailable {
|
||||
}
|
||||
} else if !system_proxy_supported() {
|
||||
SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::UnsupportedPlatform,
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
}
|
||||
} else if let Some(origin) = RequestOrigin::parse(request_url) {
|
||||
resolve_system_proxy(request_url, &origin, include_auto_detect)
|
||||
} else {
|
||||
SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::ResolutionError,
|
||||
failure: RouteFailureClass::InvalidProxyConfig,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
resolve_system_proxy(request_url, &origin, include_auto_detect).into()
|
||||
match &decision {
|
||||
SystemProxyDecision::Direct { source } => {
|
||||
RouteDiagnostic::direct(ClientRouteClass::Other, *source).emit_opt_in();
|
||||
}
|
||||
SystemProxyDecision::Proxy { source, url } => {
|
||||
RouteDiagnostic::proxy(ClientRouteClass::Other, *source, url).emit_opt_in();
|
||||
}
|
||||
SystemProxyDecision::Unavailable { source, failure } => {
|
||||
RouteDiagnostic::unavailable(ClientRouteClass::Other, *source, *failure).emit_opt_in();
|
||||
}
|
||||
}
|
||||
decision.into()
|
||||
}
|
||||
|
||||
fn resolve_system_proxy(
|
||||
@@ -460,6 +530,7 @@ fn resolve_platform_system_proxy(
|
||||
_include_auto_detect: bool,
|
||||
) -> SystemProxyDecision {
|
||||
SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::UnsupportedPlatform,
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::time::Instant;
|
||||
use super::RequestOrigin;
|
||||
use super::RouteFailureClass;
|
||||
use super::SystemProxyDecision;
|
||||
use crate::route_diagnostics::RouteDecisionSource;
|
||||
use system_configuration::core_foundation::array::CFArray;
|
||||
use system_configuration::core_foundation::array::CFArrayRef;
|
||||
use system_configuration::core_foundation::base::CFEqual;
|
||||
@@ -87,23 +88,31 @@ pub(super) fn resolve(
|
||||
) -> SystemProxyDecision {
|
||||
let Some(target_url) = cf_url(request_url) else {
|
||||
return SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::ResolutionError,
|
||||
failure: RouteFailureClass::InvalidProxyConfig,
|
||||
};
|
||||
};
|
||||
|
||||
let Some(settings) = system_proxy_settings(include_auto_detect) else {
|
||||
return SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::ResolutionError,
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
};
|
||||
};
|
||||
|
||||
let Some(proxies) = copy_proxies_for_url(&target_url, &settings) else {
|
||||
return SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::ResolutionError,
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
};
|
||||
};
|
||||
|
||||
proxy_array_decision(&proxies, &target_url, origin)
|
||||
proxy_array_decision(
|
||||
&proxies,
|
||||
&target_url,
|
||||
origin,
|
||||
RouteDecisionSource::MacOsSystem,
|
||||
)
|
||||
}
|
||||
|
||||
fn system_proxy_settings(include_auto_detect: bool) -> Option<CFDictionary<CFString, CFType>> {
|
||||
@@ -146,14 +155,19 @@ fn proxy_array_decision(
|
||||
proxies: &ProxyArray,
|
||||
target_url: &CFURL,
|
||||
origin: &RequestOrigin,
|
||||
source: RouteDecisionSource,
|
||||
) -> SystemProxyDecision {
|
||||
let mut saw_unsupported = false;
|
||||
let mut saw_unavailable = false;
|
||||
|
||||
for proxy in proxies {
|
||||
match proxy_entry_decision(&proxy, target_url, origin) {
|
||||
ProxyEntryDecision::Direct => return SystemProxyDecision::Direct,
|
||||
ProxyEntryDecision::Proxy { url } => return SystemProxyDecision::Proxy { url },
|
||||
match proxy_entry_decision(&proxy, target_url, origin, source) {
|
||||
ProxyEntryDecision::Direct { source } => {
|
||||
return SystemProxyDecision::Direct { source };
|
||||
}
|
||||
ProxyEntryDecision::Proxy { source, url } => {
|
||||
return SystemProxyDecision::Proxy { source, url };
|
||||
}
|
||||
ProxyEntryDecision::UnsupportedScheme => saw_unsupported = true,
|
||||
ProxyEntryDecision::Unavailable => saw_unavailable = true,
|
||||
}
|
||||
@@ -161,14 +175,16 @@ fn proxy_array_decision(
|
||||
|
||||
if saw_unsupported {
|
||||
SystemProxyDecision::Unavailable {
|
||||
source,
|
||||
failure: RouteFailureClass::UnsupportedProxyScheme,
|
||||
}
|
||||
} else if saw_unavailable {
|
||||
SystemProxyDecision::Unavailable {
|
||||
source,
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
}
|
||||
} else {
|
||||
SystemProxyDecision::Direct
|
||||
SystemProxyDecision::Direct { source }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,23 +192,24 @@ fn proxy_entry_decision(
|
||||
proxy: &ProxyDictionary,
|
||||
target_url: &CFURL,
|
||||
origin: &RequestOrigin,
|
||||
source: RouteDecisionSource,
|
||||
) -> ProxyEntryDecision {
|
||||
let Some(proxy_type) = cf_string_value(proxy, unsafe { kCFProxyTypeKey }) else {
|
||||
return ProxyEntryDecision::Unavailable;
|
||||
};
|
||||
|
||||
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeNone }) {
|
||||
return ProxyEntryDecision::Direct;
|
||||
return ProxyEntryDecision::Direct { source };
|
||||
}
|
||||
|
||||
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeHTTP }) {
|
||||
return concrete_proxy_entry(proxy, "http");
|
||||
return concrete_proxy_entry(proxy, "http", source);
|
||||
}
|
||||
|
||||
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeHTTPS }) {
|
||||
// CFNetwork's HTTPS proxy type is a tunneling proxy for HTTPS destinations; it does not
|
||||
// preserve an explicit TLS-to-proxy transport. See https://developer.apple.com/documentation/cfnetwork/kcfproxytypehttps.
|
||||
return concrete_proxy_entry(proxy, "http");
|
||||
return concrete_proxy_entry(proxy, "http", source);
|
||||
}
|
||||
|
||||
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeSOCKS }) {
|
||||
@@ -233,13 +250,19 @@ fn pac_decision(
|
||||
Err(_) => return ProxyEntryDecision::Unavailable,
|
||||
};
|
||||
|
||||
match proxy_array_decision(&proxies, target_url, origin) {
|
||||
SystemProxyDecision::Direct => ProxyEntryDecision::Direct,
|
||||
SystemProxyDecision::Proxy { url } => ProxyEntryDecision::Proxy { url },
|
||||
match proxy_array_decision(
|
||||
&proxies,
|
||||
target_url,
|
||||
origin,
|
||||
RouteDecisionSource::MacOsCfNetworkPac,
|
||||
) {
|
||||
SystemProxyDecision::Direct { source } => ProxyEntryDecision::Direct { source },
|
||||
SystemProxyDecision::Proxy { source, url } => ProxyEntryDecision::Proxy { source, url },
|
||||
SystemProxyDecision::Unavailable {
|
||||
failure: RouteFailureClass::UnsupportedProxyScheme,
|
||||
..
|
||||
} => ProxyEntryDecision::UnsupportedScheme,
|
||||
SystemProxyDecision::Unavailable { failure: _ } => ProxyEntryDecision::Unavailable,
|
||||
SystemProxyDecision::Unavailable { .. } => ProxyEntryDecision::Unavailable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +348,11 @@ struct PacRunLoopState {
|
||||
result: Option<Result<ProxyArray, RouteFailureClass>>,
|
||||
}
|
||||
|
||||
fn concrete_proxy_entry(proxy: &ProxyDictionary, proxy_scheme: &str) -> ProxyEntryDecision {
|
||||
fn concrete_proxy_entry(
|
||||
proxy: &ProxyDictionary,
|
||||
proxy_scheme: &str,
|
||||
source: RouteDecisionSource,
|
||||
) -> ProxyEntryDecision {
|
||||
let Some(host) = cf_string_value(proxy, unsafe { kCFProxyHostNameKey })
|
||||
.map(|host| host.to_string())
|
||||
.filter(|host| !host.is_empty())
|
||||
@@ -338,7 +365,7 @@ fn concrete_proxy_entry(proxy: &ProxyDictionary, proxy_scheme: &str) -> ProxyEnt
|
||||
Some(port) if port > 0 => format!("{proxy_scheme}://{host}:{port}"),
|
||||
_ => format!("{proxy_scheme}://{host}"),
|
||||
};
|
||||
ProxyEntryDecision::Proxy { url }
|
||||
ProxyEntryDecision::Proxy { source, url }
|
||||
}
|
||||
|
||||
fn bracket_ipv6_host(host: &str) -> String {
|
||||
@@ -395,8 +422,13 @@ fn cf_url(value: &str) -> Option<CFURL> {
|
||||
}
|
||||
|
||||
enum ProxyEntryDecision {
|
||||
Direct,
|
||||
Proxy { url: String },
|
||||
Direct {
|
||||
source: RouteDecisionSource,
|
||||
},
|
||||
Proxy {
|
||||
source: RouteDecisionSource,
|
||||
url: String,
|
||||
},
|
||||
UnsupportedScheme,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use super::RouteFailureClass;
|
||||
use super::SystemProxyDecision;
|
||||
use super::no_proxy_matches_origin;
|
||||
use super::parse_proxy_list;
|
||||
use crate::route_diagnostics::RouteDecisionSource;
|
||||
use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND;
|
||||
use windows_sys::Win32::Foundation::FALSE;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
@@ -56,7 +57,10 @@ pub(super) fn resolve(
|
||||
let ie_config = match current_user_ie_proxy_config() {
|
||||
Ok(config) => config,
|
||||
Err(failure) => {
|
||||
return SystemProxyDecision::Unavailable { failure };
|
||||
return SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::ResolutionError,
|
||||
failure,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,17 +84,22 @@ pub(super) fn resolve(
|
||||
.as_deref()
|
||||
.is_some_and(|bypass| proxy_bypass_matches_origin(bypass, origin))
|
||||
{
|
||||
return SystemProxyDecision::Direct;
|
||||
return SystemProxyDecision::Direct {
|
||||
source: RouteDecisionSource::WindowsStatic,
|
||||
};
|
||||
}
|
||||
return proxy_list_decision(proxy, origin);
|
||||
return proxy_list_decision(proxy, origin, RouteDecisionSource::WindowsStatic);
|
||||
}
|
||||
|
||||
if ie_config.auto_config_url.is_some() || (include_auto_detect && ie_config.auto_detect) {
|
||||
SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::WindowsWinHttpPac,
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
}
|
||||
} else {
|
||||
SystemProxyDecision::Direct
|
||||
SystemProxyDecision::Direct {
|
||||
source: RouteDecisionSource::Direct,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +139,7 @@ fn resolve_with_winhttp_options(
|
||||
) -> SystemProxyDecision {
|
||||
let Some(session) = WinHttpSession::open() else {
|
||||
return SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::WindowsWinHttpPac,
|
||||
failure: classify_winhttp_error(last_error()),
|
||||
};
|
||||
};
|
||||
@@ -150,35 +160,46 @@ fn resolve_with_winhttp_options(
|
||||
};
|
||||
if ok == FALSE {
|
||||
return SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::WindowsWinHttpPac,
|
||||
failure: classify_winhttp_error(last_error()),
|
||||
};
|
||||
}
|
||||
|
||||
let proxy_info = ProxyInfo::from_raw(proxy_info);
|
||||
if proxy_info.access_type == WINHTTP_ACCESS_TYPE_NO_PROXY {
|
||||
return SystemProxyDecision::Direct;
|
||||
return SystemProxyDecision::Direct {
|
||||
source: RouteDecisionSource::WindowsWinHttpPac,
|
||||
};
|
||||
}
|
||||
if proxy_info.access_type != WINHTTP_ACCESS_TYPE_NAMED_PROXY {
|
||||
return SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::WindowsWinHttpPac,
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
};
|
||||
}
|
||||
let Some(proxy) = proxy_info.proxy.as_deref() else {
|
||||
return SystemProxyDecision::Unavailable {
|
||||
source: RouteDecisionSource::WindowsWinHttpPac,
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
};
|
||||
};
|
||||
proxy_list_decision(proxy, origin)
|
||||
proxy_list_decision(proxy, origin, RouteDecisionSource::WindowsWinHttpPac)
|
||||
}
|
||||
|
||||
fn proxy_list_decision(proxy_list: &str, origin: &RequestOrigin) -> SystemProxyDecision {
|
||||
fn proxy_list_decision(
|
||||
proxy_list: &str,
|
||||
origin: &RequestOrigin,
|
||||
source: RouteDecisionSource,
|
||||
) -> SystemProxyDecision {
|
||||
match parse_proxy_list(proxy_list, &origin.scheme) {
|
||||
ParsedProxyListDecision::Direct => SystemProxyDecision::Direct,
|
||||
ParsedProxyListDecision::Proxy(url) => SystemProxyDecision::Proxy { url },
|
||||
ParsedProxyListDecision::Direct => SystemProxyDecision::Direct { source },
|
||||
ParsedProxyListDecision::Proxy(url) => SystemProxyDecision::Proxy { source, url },
|
||||
ParsedProxyListDecision::UnsupportedScheme => SystemProxyDecision::Unavailable {
|
||||
source,
|
||||
failure: RouteFailureClass::UnsupportedProxyScheme,
|
||||
},
|
||||
ParsedProxyListDecision::Unavailable => SystemProxyDecision::Unavailable {
|
||||
source,
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
},
|
||||
}
|
||||
|
||||
321
codex-rs/codex-client/src/route_diagnostics.rs
Normal file
321
codex-rs/codex-client/src/route_diagnostics.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
//! Sanitized, opt-in diagnostics for resolver-aware HTTP clients.
|
||||
//!
|
||||
//! Values emitted from this module must not contain request URLs, PAC URLs,
|
||||
//! proxy hostnames, credentials, headers, tokens, or certificate paths.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::outbound_proxy::ClientRouteClass;
|
||||
use crate::outbound_proxy::RouteFailureClass;
|
||||
|
||||
/// Opt-in switch for sanitized network diagnostics.
|
||||
///
|
||||
/// Set to `1`, `true`, `on`, or `yes` to emit diagnostic events. The configured
|
||||
/// value itself is never logged.
|
||||
const CODEX_NETWORK_DIAGNOSTICS_ENV: &str = "CODEX_NETWORK_DIAGNOSTICS";
|
||||
|
||||
const CODEX_SYSTEM_PROXY_ENV: &str = "CODEX_SYSTEM_PROXY";
|
||||
|
||||
fn env_flag_enabled(name: &str) -> bool {
|
||||
std::env::var(name).ok().as_deref().is_some_and(|value| {
|
||||
matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
"1" | "true" | "on" | "yes"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether sanitized network diagnostics are enabled for this process.
|
||||
fn network_diagnostics_enabled() -> bool {
|
||||
env_flag_enabled(CODEX_NETWORK_DIAGNOSTICS_ENV)
|
||||
}
|
||||
|
||||
fn env_present(name: &str) -> bool {
|
||||
std::env::var_os(name).is_some_and(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn proxy_env_present(upper: &str, lower: &str) -> bool {
|
||||
env_present(upper) || env_present(lower)
|
||||
}
|
||||
|
||||
fn system_proxy_override_state() -> &'static str {
|
||||
let disabled = std::env::var(CODEX_SYSTEM_PROXY_ENV)
|
||||
.ok()
|
||||
.as_deref()
|
||||
.is_some_and(|value| {
|
||||
matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
"off" | "false" | "0" | "no" | "disabled"
|
||||
)
|
||||
});
|
||||
if disabled { "disabled" } else { "default" }
|
||||
}
|
||||
|
||||
/// Emits environment presence bits for an auth operation.
|
||||
///
|
||||
/// Proxy values, CA paths, URLs, headers, and tokens are intentionally omitted.
|
||||
pub fn emit_auth_network_environment_snapshot(operation: &'static str) {
|
||||
if !network_diagnostics_enabled() {
|
||||
return;
|
||||
}
|
||||
tracing::info!(
|
||||
target_class = "auth",
|
||||
operation = operation,
|
||||
http_proxy_present = proxy_env_present("HTTP_PROXY", "http_proxy"),
|
||||
https_proxy_present = proxy_env_present("HTTPS_PROXY", "https_proxy"),
|
||||
all_proxy_present = proxy_env_present("ALL_PROXY", "all_proxy"),
|
||||
no_proxy_present = proxy_env_present("NO_PROXY", "no_proxy"),
|
||||
codex_system_proxy = system_proxy_override_state(),
|
||||
custom_ca_present = env_present("CODEX_CA_CERTIFICATE") || env_present("SSL_CERT_FILE"),
|
||||
"opt-in auth network diagnostic snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
fn classify_reqwest_error(error: &reqwest::Error) -> Option<RouteFailureClass> {
|
||||
if error.is_timeout() {
|
||||
return Some(RouteFailureClass::ConnectTimeout);
|
||||
}
|
||||
if error.status().is_some_and(|status| status.as_u16() == 407) {
|
||||
return Some(RouteFailureClass::ProxyAuthenticationRequired);
|
||||
}
|
||||
let rendered = error.to_string().to_ascii_lowercase();
|
||||
if rendered.contains("tls") || rendered.contains("certificate") || rendered.contains("cert") {
|
||||
return Some(RouteFailureClass::TlsError);
|
||||
}
|
||||
if error.is_connect() {
|
||||
return Some(RouteFailureClass::ResolverError);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Emits a coarse auth transport failure without the error text or URL.
|
||||
pub fn emit_auth_transport_failure(operation: &'static str, error: &reqwest::Error) {
|
||||
if !network_diagnostics_enabled() {
|
||||
return;
|
||||
}
|
||||
let failure = classify_reqwest_error(error)
|
||||
.map(|failure| failure.to_string())
|
||||
.unwrap_or_else(|| "other".to_string());
|
||||
tracing::info!(
|
||||
target_class = "auth",
|
||||
operation = operation,
|
||||
failure = %failure,
|
||||
is_timeout = error.is_timeout(),
|
||||
is_connect = error.is_connect(),
|
||||
status_present = error.status().is_some(),
|
||||
status = error
|
||||
.status()
|
||||
.map(|status| status.as_u16())
|
||||
.unwrap_or(/*default*/ 0),
|
||||
"opt-in auth network transport diagnostic"
|
||||
);
|
||||
}
|
||||
|
||||
/// Emits an auth HTTP status without response content or endpoint details.
|
||||
pub fn emit_auth_http_status(operation: &'static str, status: reqwest::StatusCode) {
|
||||
if !network_diagnostics_enabled() {
|
||||
return;
|
||||
}
|
||||
let failure = if status.as_u16() == 407 {
|
||||
RouteFailureClass::ProxyAuthenticationRequired.to_string()
|
||||
} else {
|
||||
"other".to_string()
|
||||
};
|
||||
tracing::info!(
|
||||
target_class = "auth",
|
||||
operation = operation,
|
||||
status = status.as_u16(),
|
||||
failure = %failure,
|
||||
"opt-in auth network HTTP status diagnostic"
|
||||
);
|
||||
}
|
||||
|
||||
/// Source that produced a route decision.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum RouteDecisionSource {
|
||||
Env,
|
||||
ConfigDisabled,
|
||||
UnsupportedPlatform,
|
||||
ResolutionError,
|
||||
#[cfg(target_os = "macos")]
|
||||
MacOsCfNetworkPac,
|
||||
#[cfg(target_os = "macos")]
|
||||
MacOsSystem,
|
||||
#[cfg(target_os = "windows")]
|
||||
WindowsWinHttpPac,
|
||||
#[cfg(target_os = "windows")]
|
||||
WindowsStatic,
|
||||
Direct,
|
||||
}
|
||||
|
||||
impl fmt::Display for RouteDecisionSource {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Env => "env",
|
||||
Self::ConfigDisabled => "config_disabled",
|
||||
Self::UnsupportedPlatform => "unsupported_platform",
|
||||
Self::ResolutionError => "resolution_error",
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::MacOsCfNetworkPac => "macos_cfnetwork_pac",
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::MacOsSystem => "macos_system",
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::WindowsWinHttpPac => "windows_winhttp_pac",
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::WindowsStatic => "windows_static",
|
||||
Self::Direct => "direct",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A proxy endpoint rendered without credentials, hostname, path, or query.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct RedactedProxyEndpoint(String);
|
||||
|
||||
impl RedactedProxyEndpoint {
|
||||
fn parse(input: &str) -> Self {
|
||||
let Some((scheme, rest)) = input.split_once("://") else {
|
||||
return Self("<invalid-proxy-url>".to_string());
|
||||
};
|
||||
if scheme.is_empty()
|
||||
|| !scheme
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
|
||||
{
|
||||
return Self("<invalid-proxy-url>".to_string());
|
||||
}
|
||||
|
||||
let Some(authority) = rest
|
||||
.split(['/', '?', '#'])
|
||||
.next()
|
||||
.filter(|authority| !authority.is_empty())
|
||||
else {
|
||||
return Self("<invalid-proxy-url>".to_string());
|
||||
};
|
||||
let host_port = authority
|
||||
.rsplit_once('@')
|
||||
.map_or(authority, |(_, tail)| tail);
|
||||
let port = redacted_port_suffix(host_port).unwrap_or_default();
|
||||
Self(format!(
|
||||
"{}://<redacted-host>{port}",
|
||||
scheme.to_ascii_lowercase()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn redacted_port_suffix(host_port: &str) -> Option<String> {
|
||||
if host_port.starts_with('[') {
|
||||
let end = host_port.find(']')?;
|
||||
let port = host_port[end + 1..].strip_prefix(':')?;
|
||||
return (!port.is_empty() && port.bytes().all(|byte| byte.is_ascii_digit()))
|
||||
.then(|| format!(":{port}"));
|
||||
}
|
||||
|
||||
let (host, port) = host_port.rsplit_once(':')?;
|
||||
if host.is_empty() || host.contains(':') || port.is_empty() {
|
||||
return None;
|
||||
}
|
||||
port.bytes()
|
||||
.all(|byte| byte.is_ascii_digit())
|
||||
.then(|| format!(":{port}"))
|
||||
}
|
||||
|
||||
impl fmt::Debug for RedactedProxyEndpoint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RedactedProxyEndpoint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum RouteDecision {
|
||||
Direct,
|
||||
Proxy(RedactedProxyEndpoint),
|
||||
Unavailable(RouteFailureClass),
|
||||
}
|
||||
|
||||
impl fmt::Display for RouteDecision {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Direct => f.write_str("direct"),
|
||||
Self::Proxy(endpoint) => write!(f, "proxy({endpoint})"),
|
||||
Self::Unavailable(failure) => write!(f, "unavailable({failure})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One safe diagnostic event for a resolver/client decision.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct RouteDiagnostic {
|
||||
route_class: ClientRouteClass,
|
||||
source: RouteDecisionSource,
|
||||
decision: RouteDecision,
|
||||
}
|
||||
|
||||
impl RouteDiagnostic {
|
||||
pub(crate) fn direct(route_class: ClientRouteClass, source: RouteDecisionSource) -> Self {
|
||||
Self::new(route_class, source, RouteDecision::Direct)
|
||||
}
|
||||
|
||||
pub(crate) fn proxy(
|
||||
route_class: ClientRouteClass,
|
||||
source: RouteDecisionSource,
|
||||
proxy_url: &str,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
route_class,
|
||||
source,
|
||||
RouteDecision::Proxy(RedactedProxyEndpoint::parse(proxy_url)),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn unavailable(
|
||||
route_class: ClientRouteClass,
|
||||
source: RouteDecisionSource,
|
||||
failure: RouteFailureClass,
|
||||
) -> Self {
|
||||
Self::new(route_class, source, RouteDecision::Unavailable(failure))
|
||||
}
|
||||
|
||||
fn new(
|
||||
route_class: ClientRouteClass,
|
||||
source: RouteDecisionSource,
|
||||
decision: RouteDecision,
|
||||
) -> Self {
|
||||
Self {
|
||||
route_class,
|
||||
source,
|
||||
decision,
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits a sanitized structured event when diagnostics are explicitly enabled.
|
||||
pub(crate) fn emit_opt_in(&self) {
|
||||
if !network_diagnostics_enabled() {
|
||||
return;
|
||||
}
|
||||
let failure = match &self.decision {
|
||||
RouteDecision::Unavailable(failure) => failure.to_string(),
|
||||
RouteDecision::Direct | RouteDecision::Proxy(_) => "none".to_string(),
|
||||
};
|
||||
let custom_ca_configured =
|
||||
env_present("CODEX_CA_CERTIFICATE") || env_present("SSL_CERT_FILE");
|
||||
tracing::info!(
|
||||
route_class = %self.route_class,
|
||||
source = %self.source,
|
||||
decision = %self.decision,
|
||||
failure = %failure,
|
||||
custom_ca_configured = custom_ca_configured,
|
||||
"opt-in outbound route diagnostic"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "route_diagnostics_tests.rs"]
|
||||
mod tests;
|
||||
20
codex-rs/codex-client/src/route_diagnostics_tests.rs
Normal file
20
codex-rs/codex-client/src/route_diagnostics_tests.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn proxy_endpoint_redacts_credentials_host_path_and_query() {
|
||||
let endpoint = RedactedProxyEndpoint::parse(
|
||||
"http://user:secret@proxy.internal.example:8080/pac?token=secret",
|
||||
);
|
||||
|
||||
assert_eq!(endpoint.to_string(), "http://<redacted-host>:8080");
|
||||
assert!(!format!("{endpoint:?}").contains("secret"));
|
||||
assert!(!format!("{endpoint}").contains("proxy.internal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_proxy_url_is_not_echoed() {
|
||||
let endpoint = RedactedProxyEndpoint::parse("not a url with password=secret");
|
||||
|
||||
assert_eq!(endpoint.to_string(), "<invalid-proxy-url>");
|
||||
}
|
||||
@@ -39,6 +39,9 @@ use crate::token_data::parse_chatgpt_jwt_claims;
|
||||
use base64::Engine;
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_client::emit_auth_http_status;
|
||||
use codex_client::emit_auth_network_environment_snapshot;
|
||||
use codex_client::emit_auth_transport_failure;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_utils_template::Template;
|
||||
use rand::RngCore;
|
||||
@@ -748,6 +751,7 @@ pub(crate) async fn exchange_code_for_tokens(
|
||||
}
|
||||
|
||||
let token_endpoint = format!("{}/oauth/token", issuer.trim_end_matches('/'));
|
||||
emit_auth_network_environment_snapshot("oauth_token_exchange");
|
||||
let client =
|
||||
build_auth_reqwest_client_with_auth_route_config(&token_endpoint, auth_route_config)?;
|
||||
info!(
|
||||
@@ -771,6 +775,7 @@ pub(crate) async fn exchange_code_for_tokens(
|
||||
let resp = match resp {
|
||||
Ok(resp) => resp,
|
||||
Err(error) => {
|
||||
emit_auth_transport_failure("oauth_token_exchange", &error);
|
||||
let error = redact_sensitive_error_url(error);
|
||||
error!(
|
||||
is_timeout = error.is_timeout(),
|
||||
@@ -785,6 +790,7 @@ pub(crate) async fn exchange_code_for_tokens(
|
||||
|
||||
let status = resp.status();
|
||||
if !status.is_success() {
|
||||
emit_auth_http_status("oauth_token_exchange", status);
|
||||
let body = resp.text().await.map_err(io::Error::other)?;
|
||||
let detail = parse_token_endpoint_error(&body);
|
||||
warn!(
|
||||
@@ -1172,6 +1178,7 @@ pub(crate) async fn obtain_api_key(
|
||||
access_token: String,
|
||||
}
|
||||
let token_endpoint = format!("{}/oauth/token", issuer.trim_end_matches('/'));
|
||||
emit_auth_network_environment_snapshot("api_key_exchange");
|
||||
let client =
|
||||
build_auth_reqwest_client_with_auth_route_config(&token_endpoint, auth_route_config)?;
|
||||
let resp = client
|
||||
@@ -1189,9 +1196,13 @@ pub(crate) async fn obtain_api_key(
|
||||
.await;
|
||||
let resp = match resp {
|
||||
Ok(resp) => resp,
|
||||
Err(error) => return Err(io::Error::other(error)),
|
||||
Err(error) => {
|
||||
emit_auth_transport_failure("api_key_exchange", &error);
|
||||
return Err(io::Error::other(error));
|
||||
}
|
||||
};
|
||||
if !resp.status().is_success() {
|
||||
emit_auth_http_status("api_key_exchange", resp.status());
|
||||
return Err(io::Error::other(format!(
|
||||
"api key exchange failed with status {}",
|
||||
resp.status()
|
||||
|
||||
Reference in New Issue
Block a user