mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
Retry proxy CONNECT challenges with Windows authentication (#56000)
## Why HTTPS requests and WebSocket connections on Windows need to authenticate when a proxy rejects `CONNECT` with status `407`. ## What changed - Retry proxy challenges through the existing Windows authentication helper, using the signed-in user's Negotiate or NTLM credentials. Treat an origin `407` as a normal HTTP response. - Bridge authenticated tunnels to the HTTP client through destination-specific named pipes, retaining redirect checks and TLS backend fallback without caching those tunnels across destinations. - Carry explicit CA certificates and client identities into HTTPS proxy TLS configuration, and apply the configured connect timeout when opening authenticated HTTP tunnels. GitOrigin-RevId: ec7d9168972a7a640350e5348cfca40307390fbf
This commit is contained in:
@@ -38,12 +38,14 @@ pub struct HttpClientBuilder {
|
||||
pub(crate) default_headers: Option<HeaderMap>,
|
||||
follow_redirects: bool,
|
||||
pub(crate) redirect_observed: Option<Arc<AtomicBool>>,
|
||||
connect_timeout: Option<Duration>,
|
||||
pub(crate) connect_timeout: Option<Duration>,
|
||||
chatgpt_cloudflare_cookie_store: bool,
|
||||
chatgpt_cookie_store: Option<Arc<ChatGptCookieStore>>,
|
||||
pub(crate) request_logging: RequestLogging,
|
||||
tls_backend: TlsBackend,
|
||||
tls: HttpClientTlsConfig,
|
||||
pub(crate) tls: HttpClientTlsConfig,
|
||||
#[cfg(windows)]
|
||||
windows_proxy_pipe_path: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
@@ -184,9 +186,21 @@ impl HttpClientBuilder {
|
||||
)
|
||||
.into_client());
|
||||
}
|
||||
let route = http_client_factory.resolve_proxy_route(request_url);
|
||||
#[cfg(windows)]
|
||||
if crate::windows_proxy_auth::can_authenticate_route(request_url, &route) {
|
||||
return Ok(crate::RouteAwareClientPool::with_builder_and_initial_route(
|
||||
http_client_factory.clone(),
|
||||
route_class,
|
||||
self,
|
||||
route,
|
||||
)?
|
||||
.into_client());
|
||||
}
|
||||
self.chatgpt_cookie_store = http_client_factory.chatgpt_cookie_store();
|
||||
let (builder, request_logging, default_headers) = self.into_reqwest_parts();
|
||||
let inner = http_client_factory.build_reqwest_client(builder, request_url, route_class)?;
|
||||
let inner =
|
||||
crate::outbound_proxy::build_reqwest_client_for_route(builder, route_class, &route)?;
|
||||
Ok(HttpClient::from_parts(
|
||||
inner,
|
||||
request_logging,
|
||||
@@ -204,17 +218,16 @@ impl HttpClientBuilder {
|
||||
self.chatgpt_cookie_store = http_client_factory.chatgpt_cookie_store();
|
||||
let explicit_roots = self.tls.root_certificate.is_some();
|
||||
let (builder, request_logging, default_headers) = self.into_reqwest_parts();
|
||||
let builder = crate::outbound_proxy::configure_builder_for_resolved_route(
|
||||
builder,
|
||||
route_class,
|
||||
route,
|
||||
)?;
|
||||
let inner = if explicit_roots {
|
||||
builder
|
||||
.build()
|
||||
.map_err(BuildRouteAwareHttpClientError::ExplicitTls)?
|
||||
crate::outbound_proxy::configure_builder_for_resolved_route(
|
||||
builder,
|
||||
route_class,
|
||||
route,
|
||||
)?
|
||||
.build()
|
||||
.map_err(BuildRouteAwareHttpClientError::ExplicitTls)?
|
||||
} else {
|
||||
build_reqwest_client_with_custom_ca(builder)?
|
||||
crate::outbound_proxy::build_reqwest_client_for_route(builder, route_class, route)?
|
||||
};
|
||||
Ok(TransportClient::new(
|
||||
inner,
|
||||
@@ -223,6 +236,15 @@ impl HttpClientBuilder {
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn with_windows_proxy_pipe(
|
||||
mut self,
|
||||
pipe: &crate::windows_proxy_auth::ProxyPipe,
|
||||
) -> Self {
|
||||
self.windows_proxy_pipe_path = Some(pipe.path().to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds a client using the transport's default proxy behavior.
|
||||
///
|
||||
/// # Legacy compatibility only
|
||||
@@ -358,6 +380,10 @@ impl HttpClientBuilder {
|
||||
|
||||
fn base_reqwest_builder(self) -> reqwest::ClientBuilder {
|
||||
let mut builder = reqwest::Client::builder();
|
||||
#[cfg(windows)]
|
||||
if let Some(path) = self.windows_proxy_pipe_path {
|
||||
builder = builder.windows_named_pipe(path);
|
||||
}
|
||||
if self.http2_prior_knowledge {
|
||||
builder = builder.http2_prior_knowledge();
|
||||
}
|
||||
@@ -368,10 +394,10 @@ impl HttpClientBuilder {
|
||||
if let Some(certificate) = self.tls.root_certificate {
|
||||
builder = builder
|
||||
.tls_built_in_root_certs(false)
|
||||
.add_root_certificate(certificate);
|
||||
.add_root_certificate(certificate.reqwest);
|
||||
}
|
||||
if let Some(identity) = self.tls.client_identity {
|
||||
builder = builder.identity(identity).https_only(true);
|
||||
builder = builder.identity(identity.reqwest).https_only(true);
|
||||
}
|
||||
if !self.follow_redirects {
|
||||
builder = builder.redirect(reqwest::redirect::Policy::none());
|
||||
@@ -407,6 +433,8 @@ impl Default for HttpClientBuilder {
|
||||
request_logging: RequestLogging::Enabled,
|
||||
tls_backend: TlsBackend::TransportDefault,
|
||||
tls: HttpClientTlsConfig::default(),
|
||||
#[cfg(windows)]
|
||||
windows_proxy_pipe_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,55 @@
|
||||
//! Explicit roots replace shared environment roots; PEM identities require HTTPS and rustls.
|
||||
|
||||
use std::fmt;
|
||||
#[cfg(windows)]
|
||||
use std::io;
|
||||
#[cfg(windows)]
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(windows)]
|
||||
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
|
||||
#[cfg(windows)]
|
||||
use rustls::ClientConfig;
|
||||
#[cfg(windows)]
|
||||
use rustls::RootCertStore;
|
||||
#[cfg(windows)]
|
||||
use rustls_pki_types::CertificateDer;
|
||||
#[cfg(windows)]
|
||||
use rustls_pki_types::PrivateKeyDer;
|
||||
#[cfg(windows)]
|
||||
use rustls_pki_types::pem::PemObject;
|
||||
|
||||
use crate::HttpError;
|
||||
#[cfg(windows)]
|
||||
use crate::WindowsProxyTls;
|
||||
|
||||
/// TLS configuration consumed by [`crate::HttpClientBuilder::build_with_tls`].
|
||||
/// Keeping this separate from the builder prevents combining it with legacy fallback APIs.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct HttpClientTlsConfig {
|
||||
pub(crate) root_certificate: Option<reqwest::Certificate>,
|
||||
pub(crate) client_identity: Option<reqwest::Identity>,
|
||||
pub(crate) root_certificate: Option<TlsMaterial<reqwest::Certificate>>,
|
||||
pub(crate) client_identity: Option<TlsMaterial<reqwest::Identity>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TlsMaterial<T> {
|
||||
pub(crate) reqwest: T,
|
||||
// Reqwest does not expose its TLS material to the Windows proxy connector.
|
||||
#[cfg(windows)]
|
||||
pem: Arc<[u8]>,
|
||||
}
|
||||
|
||||
impl<T> TlsMaterial<T> {
|
||||
fn from_pem(
|
||||
pem: &[u8],
|
||||
parse: impl FnOnce(&[u8]) -> Result<T, reqwest::Error>,
|
||||
) -> Result<Self, HttpError> {
|
||||
Ok(Self {
|
||||
reqwest: parse(pem)?,
|
||||
#[cfg(windows)]
|
||||
pem: pem.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for HttpClientTlsConfig {
|
||||
@@ -25,13 +65,49 @@ impl fmt::Debug for HttpClientTlsConfig {
|
||||
impl HttpClientTlsConfig {
|
||||
/// Trusts this PEM certificate instead of system or environment-provided roots.
|
||||
pub fn with_root_certificate_pem(mut self, pem: &[u8]) -> Result<Self, HttpError> {
|
||||
self.root_certificate = Some(reqwest::Certificate::from_pem(pem)?);
|
||||
self.root_certificate = Some(TlsMaterial::from_pem(pem, reqwest::Certificate::from_pem)?);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Installs a PEM certificate chain and private key and requires HTTPS requests.
|
||||
pub fn with_client_identity_pem(mut self, pem: &[u8]) -> Result<Self, HttpError> {
|
||||
self.client_identity = Some(reqwest::Identity::from_pem(pem)?);
|
||||
self.client_identity = Some(TlsMaterial::from_pem(pem, reqwest::Identity::from_pem)?);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn windows_proxy_tls(&self) -> io::Result<WindowsProxyTls> {
|
||||
if self.root_certificate.is_none() && self.client_identity.is_none() {
|
||||
return Ok(WindowsProxyTls::Default);
|
||||
}
|
||||
let roots = if let Some(root) = &self.root_certificate {
|
||||
let mut roots = RootCertStore::empty();
|
||||
for certificate in CertificateDer::pem_slice_iter(&root.pem) {
|
||||
roots
|
||||
.add(certificate.map_err(io::Error::other)?)
|
||||
.map_err(io::Error::other)?;
|
||||
}
|
||||
roots
|
||||
} else {
|
||||
crate::custom_ca::build_rustls_root_store_with_custom_ca().map_err(io::Error::other)?
|
||||
};
|
||||
ensure_rustls_crypto_provider();
|
||||
let builder = ClientConfig::builder().with_root_certificates(roots);
|
||||
let config = if let Some(identity) = &self.client_identity {
|
||||
let certificates = CertificateDer::pem_slice_iter(&identity.pem)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(io::Error::other)?;
|
||||
let key = PrivateKeyDer::pem_slice_iter(&identity.pem)
|
||||
.last()
|
||||
.transpose()
|
||||
.map_err(io::Error::other)?
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing private key"))?;
|
||||
builder
|
||||
.with_client_auth_cert(certificates, key)
|
||||
.map_err(io::Error::other)?
|
||||
} else {
|
||||
builder.with_no_client_auth()
|
||||
};
|
||||
Ok(WindowsProxyTls::Custom(Arc::new(config)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ use std::sync::Arc;
|
||||
|
||||
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
|
||||
use rustls::ClientConfig;
|
||||
use rustls::RootCertStore;
|
||||
use rustls_pki_types::CertificateDer;
|
||||
use rustls_pki_types::pem::PemObject;
|
||||
use rustls_pki_types::pem::SectionKind;
|
||||
@@ -244,7 +245,22 @@ fn build_rustls_client_config(
|
||||
bundle: Option<&ConfiguredCaBundle>,
|
||||
) -> Result<Arc<ClientConfig>, BuildCustomCaTransportError> {
|
||||
ensure_rustls_crypto_provider();
|
||||
Ok(Arc::new(
|
||||
ClientConfig::builder()
|
||||
.with_root_certificates(build_rustls_root_store(bundle)?)
|
||||
.with_no_client_auth(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn build_rustls_root_store_with_custom_ca()
|
||||
-> Result<RootCertStore, BuildCustomCaTransportError> {
|
||||
build_rustls_root_store(ProcessEnv.configured_ca_bundle().as_ref())
|
||||
}
|
||||
|
||||
fn build_rustls_root_store(
|
||||
bundle: Option<&ConfiguredCaBundle>,
|
||||
) -> Result<RootCertStore, BuildCustomCaTransportError> {
|
||||
// Start from the platform roots so websocket callers keep the same baseline trust behavior
|
||||
// they would get from tungstenite's default rustls connector, then layer in the Codex custom
|
||||
// CA bundle on top when configured.
|
||||
@@ -271,11 +287,7 @@ fn build_rustls_client_config(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Arc::new(
|
||||
ClientConfig::builder()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth(),
|
||||
))
|
||||
Ok(root_store)
|
||||
}
|
||||
|
||||
/// Builds a reqwest client using an injected environment source and reqwest builder.
|
||||
|
||||
@@ -265,7 +265,8 @@ impl HttpClientFactory {
|
||||
/// WebSocket schemes are resolved through their HTTP equivalents so platform PAC and system
|
||||
/// proxy APIs apply the same policy to `ws`/`wss` and `http`/`https` destinations. When system
|
||||
/// resolution is unavailable, explicit environment settings are resolved before falling back
|
||||
/// to a direct route.
|
||||
/// to a direct route. Ordered PAC candidates are collapsed to one route on Windows and macOS;
|
||||
/// later proxy or `DIRECT` candidates are not retried after a connection failure.
|
||||
pub fn resolve_proxy_route(&self, request_url: &str) -> OutboundProxyRoute {
|
||||
resolve_proxy_route(
|
||||
&ProcessEnv,
|
||||
@@ -321,21 +322,6 @@ impl HttpClientFactory {
|
||||
cached_system_proxy_decision(&request_url)
|
||||
.map(|decision| route_from_system_decision(&ProcessEnv, env_proxy_kind, decision))
|
||||
}
|
||||
|
||||
/// Builds a reqwest client for a concrete outbound route.
|
||||
pub(crate) fn build_reqwest_client(
|
||||
&self,
|
||||
builder: reqwest::ClientBuilder,
|
||||
request_url: &str,
|
||||
route_class: ClientRouteClass,
|
||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||
build_reqwest_client_for_route(
|
||||
builder,
|
||||
request_url,
|
||||
route_class,
|
||||
self.outbound_proxy_policy,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_proxy_route(
|
||||
@@ -457,29 +443,17 @@ impl From<BuildRouteAwareHttpClientError> for io::Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a reqwest client with conservative route selection and shared CA handling.
|
||||
///
|
||||
/// Unavailable platform resolution falls back to environment proxies and then direct. Errors after
|
||||
/// a route is selected are returned without trying another route. Ordered PAC candidates are
|
||||
/// currently collapsed to one route on both Windows and macOS; later proxy or `DIRECT` candidates
|
||||
/// are not retried after a connection failure.
|
||||
fn build_reqwest_client_for_route(
|
||||
/// Builds a reqwest client with the resolved route and shared CA handling.
|
||||
pub(crate) fn build_reqwest_client_for_route(
|
||||
builder: reqwest::ClientBuilder,
|
||||
request_url: &str,
|
||||
route_class: ClientRouteClass,
|
||||
outbound_proxy_policy: OutboundProxyPolicy,
|
||||
route: &OutboundProxyRoute,
|
||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||
let builder = configure_proxy_for_route(
|
||||
&ProcessEnv,
|
||||
builder,
|
||||
request_url,
|
||||
route_class,
|
||||
outbound_proxy_policy,
|
||||
resolve_system_proxy,
|
||||
)?;
|
||||
let builder = configure_builder_for_resolved_route(builder, route_class, route)?;
|
||||
build_reqwest_client_with_custom_ca(builder).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn configure_proxy_for_route(
|
||||
env: &dyn EnvSource,
|
||||
builder: reqwest::ClientBuilder,
|
||||
|
||||
@@ -295,6 +295,24 @@ impl RouteAwareClientPool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the first transport now so fixed-destination callers still receive setup errors.
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn with_builder_and_initial_route(
|
||||
http_client_factory: HttpClientFactory,
|
||||
route_class: ClientRouteClass,
|
||||
client_builder: HttpClientBuilder,
|
||||
route: OutboundProxyRoute,
|
||||
) -> Result<Self, BuildRouteAwareHttpClientError> {
|
||||
let mut pool = Self::with_builder(http_client_factory, route_class, client_builder);
|
||||
let client = pool.client_builder_for_route().build_for_resolved_route(
|
||||
&pool.http_client_factory,
|
||||
pool.route_class,
|
||||
&route,
|
||||
)?;
|
||||
pool.clients = Arc::new(Mutex::new(HashMap::from([(route, client)])));
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
/// Starts a new account-scoped operation while sharing the existing connection cache.
|
||||
/// Capture this before resolving credentials or reading account-specific content.
|
||||
pub fn for_current_account(&self) -> Self {
|
||||
@@ -495,11 +513,7 @@ impl RouteAwareClientPool {
|
||||
return Ok((route, client.clone(), SelectedTlsBackend::TransportDefault));
|
||||
}
|
||||
}
|
||||
let client_builder = if self.follows_redirects_manually() {
|
||||
self.client_builder.clone().without_redirects()
|
||||
} else {
|
||||
self.client_builder.clone()
|
||||
};
|
||||
let client_builder = self.client_builder_for_route();
|
||||
let pool = self.clone();
|
||||
let build_route = route.clone();
|
||||
let client = tokio::task::spawn_blocking(move || {
|
||||
@@ -553,6 +567,14 @@ impl RouteAwareClientPool {
|
||||
|| self.rustls_clients.is_some())
|
||||
}
|
||||
|
||||
fn client_builder_for_route(&self) -> HttpClientBuilder {
|
||||
if self.follows_redirects_manually() {
|
||||
self.client_builder.clone().without_redirects()
|
||||
} else {
|
||||
self.client_builder.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn rustls_client_for_route(
|
||||
&self,
|
||||
route: &OutboundProxyRoute,
|
||||
|
||||
@@ -9,10 +9,16 @@ use futures::future::BoxFuture;
|
||||
use http::header::PROXY_AUTHORIZATION;
|
||||
|
||||
use super::RouteAwareClientPool;
|
||||
#[cfg(windows)]
|
||||
use super::RouteAwareClientPoolError;
|
||||
use super::RouteAwareRequestError;
|
||||
use super::SelectedTlsBackend;
|
||||
use crate::HttpResponse;
|
||||
use crate::OutboundProxyRoute;
|
||||
#[cfg(windows)]
|
||||
use crate::RouteFailureClass;
|
||||
#[cfg(windows)]
|
||||
use crate::client::TransportClient;
|
||||
use crate::client::apply_default_headers;
|
||||
use crate::request_draft::RequestDraft;
|
||||
use crate::route_aware_redirect::MAX_REDIRECTS;
|
||||
@@ -22,6 +28,8 @@ use crate::route_aware_redirect::redirect_request;
|
||||
use crate::route_aware_redirect::redirect_url;
|
||||
use crate::route_aware_redirect::remove_sensitive_headers;
|
||||
use crate::tls_backend_fallback::should_retry_with_rustls;
|
||||
#[cfg(windows)]
|
||||
use crate::windows_proxy_auth::ProxyPipe;
|
||||
|
||||
impl RouteAwareClientPool {
|
||||
/// Executes requests through shared routing, destination, and redirect checks.
|
||||
@@ -108,15 +116,7 @@ impl RouteAwareClientPool {
|
||||
request.headers_mut().remove(PROXY_AUTHORIZATION);
|
||||
}
|
||||
previous_route = Some(current_route.clone());
|
||||
if let Some(timeout_deadline) = timeout_deadline {
|
||||
let remaining = timeout_deadline
|
||||
.checked_duration_since(tokio::time::Instant::now())
|
||||
.ok_or(RouteAwareRequestError::Timeout)?;
|
||||
if remaining.is_zero() {
|
||||
return Err(RouteAwareRequestError::Timeout);
|
||||
}
|
||||
*request.timeout_mut() = Some(remaining);
|
||||
}
|
||||
set_remaining_timeout(&mut request, timeout_deadline)?;
|
||||
let method = request.method().clone();
|
||||
let headers = request.headers().clone();
|
||||
let version = request.version();
|
||||
@@ -126,28 +126,46 @@ impl RouteAwareClientPool {
|
||||
client.execute_without_request_logging(request).await
|
||||
} else {
|
||||
client.execute(request).await
|
||||
};
|
||||
let response = match result {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
let result = self
|
||||
.retry_with_rustls(
|
||||
¤t_url,
|
||||
¤t_route,
|
||||
selected_tls_backend,
|
||||
replay.as_ref(),
|
||||
error,
|
||||
timeout_deadline,
|
||||
)
|
||||
.await;
|
||||
if follows_redirects_manually
|
||||
&& let Err(RouteAwareRequestError::Request(error)) = &result
|
||||
{
|
||||
client.log_error_summary(&request_method, &request_url, error);
|
||||
}
|
||||
result?
|
||||
}
|
||||
.map_err(RouteAwareRequestError::from);
|
||||
let result = match result {
|
||||
// A failed CONNECT identifies a proxy challenge. An origin 407 is a response.
|
||||
#[cfg(windows)]
|
||||
Err(error)
|
||||
if error.is_connect()
|
||||
&& error.failure_class()
|
||||
== Some(RouteFailureClass::ProxyAuthenticationRequired) =>
|
||||
{
|
||||
self.retry_windows_proxy(
|
||||
¤t_route,
|
||||
selected_tls_backend,
|
||||
replay.as_ref(),
|
||||
error,
|
||||
timeout_deadline,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(RouteAwareRequestError::Request(error)) => {
|
||||
self.retry_with_rustls(
|
||||
¤t_route,
|
||||
selected_tls_backend,
|
||||
replay.as_ref(),
|
||||
#[cfg(windows)]
|
||||
/*proxy_pipe*/
|
||||
None,
|
||||
error,
|
||||
timeout_deadline,
|
||||
)
|
||||
.await
|
||||
}
|
||||
result => result,
|
||||
};
|
||||
if follows_redirects_manually
|
||||
&& let Err(RouteAwareRequestError::Request(error)) = &result
|
||||
{
|
||||
client.log_error_summary(&request_method, &request_url, error);
|
||||
}
|
||||
let response = result?;
|
||||
let next_request = if follows_redirects_manually && is_redirect(response.status()) {
|
||||
redirect_url(&response).and_then(|next_url| {
|
||||
redirect_request(
|
||||
@@ -202,16 +220,20 @@ impl RouteAwareClientPool {
|
||||
|
||||
pub(super) async fn retry_with_rustls(
|
||||
&self,
|
||||
current_url: &reqwest::Url,
|
||||
current_route: &OutboundProxyRoute,
|
||||
selected_tls_backend: SelectedTlsBackend,
|
||||
replay: Option<&reqwest::Request>,
|
||||
#[cfg(windows)] proxy_pipe: Option<&ProxyPipe>,
|
||||
error: reqwest::Error,
|
||||
timeout_deadline: Option<tokio::time::Instant>,
|
||||
) -> Result<reqwest::Response, RouteAwareRequestError> {
|
||||
let Some(rustls_clients) = self.rustls_clients.as_ref() else {
|
||||
return Err(error.into());
|
||||
};
|
||||
let Some(replay) = replay else {
|
||||
return Err(error.into());
|
||||
};
|
||||
let current_url = replay.url();
|
||||
|
||||
if current_url.scheme() != "https"
|
||||
|| selected_tls_backend == SelectedTlsBackend::RustlsFallback
|
||||
@@ -220,25 +242,27 @@ impl RouteAwareClientPool {
|
||||
return Err(error.into());
|
||||
}
|
||||
|
||||
let Some(mut retry_request) = replay.and_then(reqwest::Request::try_clone) else {
|
||||
let Some(mut retry_request) = replay.try_clone() else {
|
||||
return Err(error.into());
|
||||
};
|
||||
|
||||
let mut fallback_client = match rustls_clients.client_for_route(current_route) {
|
||||
let candidate = rustls_clients.client_for_route(current_route);
|
||||
#[cfg(windows)]
|
||||
let candidate = match proxy_pipe {
|
||||
Some(pipe) => Some(self.windows_proxy_client(
|
||||
current_route,
|
||||
SelectedTlsBackend::RustlsFallback,
|
||||
pipe,
|
||||
)?),
|
||||
None => candidate,
|
||||
};
|
||||
let mut fallback_client = match candidate {
|
||||
Some(client) => client,
|
||||
None => self.rustls_client_for_route(current_route)?,
|
||||
};
|
||||
fallback_client.request_logging = self.client_builder.request_logging;
|
||||
|
||||
if let Some(timeout_deadline) = timeout_deadline {
|
||||
let remaining = timeout_deadline
|
||||
.checked_duration_since(tokio::time::Instant::now())
|
||||
.ok_or(RouteAwareRequestError::Timeout)?;
|
||||
if remaining.is_zero() {
|
||||
return Err(RouteAwareRequestError::Timeout);
|
||||
}
|
||||
*retry_request.timeout_mut() = Some(remaining);
|
||||
}
|
||||
set_remaining_timeout(&mut retry_request, timeout_deadline)?;
|
||||
|
||||
let response = if self.follows_redirects_manually() {
|
||||
fallback_client
|
||||
@@ -248,12 +272,103 @@ impl RouteAwareClientPool {
|
||||
fallback_client.execute(retry_request).await
|
||||
}?;
|
||||
|
||||
rustls_clients.remember(current_url, current_route, fallback_client);
|
||||
tracing::info!(
|
||||
event.name = "codex.http_client.tls_backend_fallback",
|
||||
"HTTP client switched to rustls after a TLS protocol negotiation failure"
|
||||
);
|
||||
|
||||
// A Windows auth pipe serves this destination alone, so it cannot enter the route cache.
|
||||
#[cfg(windows)]
|
||||
if proxy_pipe.is_some() {
|
||||
return Ok(response);
|
||||
}
|
||||
rustls_clients.remember(current_url, current_route, fallback_client);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn retry_windows_proxy(
|
||||
&self,
|
||||
current_route: &OutboundProxyRoute,
|
||||
selected_tls_backend: SelectedTlsBackend,
|
||||
replay: Option<&reqwest::Request>,
|
||||
error: RouteAwareRequestError,
|
||||
timeout_deadline: Option<tokio::time::Instant>,
|
||||
) -> Result<reqwest::Response, RouteAwareRequestError> {
|
||||
let OutboundProxyRoute::Proxy { url, .. } = current_route else {
|
||||
return Err(error);
|
||||
};
|
||||
let Some(mut request) = replay.and_then(reqwest::Request::try_clone) else {
|
||||
return Err(error);
|
||||
};
|
||||
if request.url().scheme() != "https" {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let pipe = match crate::windows_proxy_auth::pipe_for(
|
||||
url,
|
||||
request.url(),
|
||||
&self.client_builder.tls,
|
||||
self.client_builder.connect_timeout,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(pipe) => pipe,
|
||||
Err(auth_error) if auth_error.kind() == io::ErrorKind::TimedOut => {
|
||||
return Err(RouteAwareRequestError::Timeout);
|
||||
}
|
||||
Err(_) => return Err(error),
|
||||
};
|
||||
let client = self.windows_proxy_client(current_route, selected_tls_backend, &pipe)?;
|
||||
set_remaining_timeout(&mut request, timeout_deadline)?;
|
||||
// Keep the listener alive through a possible TLS retry.
|
||||
match client.execute_without_request_logging(request).await {
|
||||
Ok(response) => Ok(response),
|
||||
Err(error) => {
|
||||
self.retry_with_rustls(
|
||||
current_route,
|
||||
selected_tls_backend,
|
||||
replay,
|
||||
Some(&pipe),
|
||||
error,
|
||||
timeout_deadline,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn windows_proxy_client(
|
||||
&self,
|
||||
route: &OutboundProxyRoute,
|
||||
backend: SelectedTlsBackend,
|
||||
pipe: &ProxyPipe,
|
||||
) -> Result<TransportClient, RouteAwareClientPoolError> {
|
||||
let mut builder = self
|
||||
.client_builder
|
||||
.clone()
|
||||
.without_redirects()
|
||||
.with_windows_proxy_pipe(pipe);
|
||||
if backend == SelectedTlsBackend::RustlsFallback {
|
||||
builder = builder.with_rustls_tls();
|
||||
}
|
||||
builder
|
||||
.build_for_resolved_route(&self.http_client_factory, self.route_class, route)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_remaining_timeout(
|
||||
request: &mut reqwest::Request,
|
||||
deadline: Option<tokio::time::Instant>,
|
||||
) -> Result<(), RouteAwareRequestError> {
|
||||
if let Some(deadline) = deadline {
|
||||
let remaining = deadline
|
||||
.checked_duration_since(tokio::time::Instant::now())
|
||||
.filter(|remaining| !remaining.is_zero())
|
||||
.ok_or(RouteAwareRequestError::Timeout)?;
|
||||
*request.timeout_mut() = Some(remaining);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -141,10 +141,12 @@ async fn retries_a_native_tls_failure_after_another_request_caches_rustls() {
|
||||
|
||||
let error = pool
|
||||
.retry_with_rustls(
|
||||
&destination,
|
||||
&route,
|
||||
selected_tls_backend,
|
||||
Some(&replay),
|
||||
#[cfg(windows)]
|
||||
/*proxy_pipe*/
|
||||
None,
|
||||
error,
|
||||
/*timeout_deadline*/ None,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
//! HTTPS proxies that require authentication tokens bound to their TLS connection
|
||||
//! are not supported because curl does not own that connection.
|
||||
|
||||
mod pipe;
|
||||
|
||||
pub(crate) use pipe::ProxyPipe;
|
||||
pub(crate) use pipe::pipe_for;
|
||||
|
||||
use std::io;
|
||||
use std::io::Cursor;
|
||||
use std::net::Ipv4Addr;
|
||||
@@ -34,6 +39,7 @@ use tokio::task::JoinSet;
|
||||
use tokio_rustls::TlsConnector;
|
||||
use url::Host;
|
||||
|
||||
use crate::OutboundProxyRoute;
|
||||
use crate::build_rustls_client_config_with_custom_ca;
|
||||
|
||||
/// Reads and writes a TCP or TLS connection to the proxy.
|
||||
@@ -73,13 +79,13 @@ pub enum WindowsProxyTls {
|
||||
Custom(Arc<ClientConfig>),
|
||||
}
|
||||
|
||||
/// Opens a new connection to the selected proxy and authenticates a CONNECT tunnel.
|
||||
/// Call only after the proxy rejects a CONNECT request with 407.
|
||||
pub async fn connect_windows_proxy(
|
||||
proxy_url: &str,
|
||||
target: &Url,
|
||||
proxy_tls: WindowsProxyTls,
|
||||
) -> io::Result<impl AsyncRead + AsyncWrite + Unpin + Send + use<>> {
|
||||
pub(crate) fn can_authenticate_route(target: &str, route: &OutboundProxyRoute) -> bool {
|
||||
matches!(route, OutboundProxyRoute::Proxy { url, .. }
|
||||
if Url::parse(target).is_ok_and(|target| target.scheme() == "https")
|
||||
&& parse_proxy_url(url).is_ok())
|
||||
}
|
||||
|
||||
fn parse_proxy_url(proxy_url: &str) -> io::Result<Url> {
|
||||
let proxy = Url::parse(proxy_url).map_err(|_| invalid_proxy())?;
|
||||
if !matches!(proxy.scheme(), "http" | "https")
|
||||
|| !proxy.username().is_empty()
|
||||
@@ -87,6 +93,17 @@ pub async fn connect_windows_proxy(
|
||||
{
|
||||
return Err(invalid_proxy());
|
||||
}
|
||||
Ok(proxy)
|
||||
}
|
||||
|
||||
/// Opens a new connection to the selected proxy and authenticates a CONNECT tunnel.
|
||||
/// Call only after the proxy rejects a CONNECT request with 407.
|
||||
pub async fn connect_windows_proxy(
|
||||
proxy_url: &str,
|
||||
target: &Url,
|
||||
proxy_tls: WindowsProxyTls,
|
||||
) -> io::Result<impl AsyncRead + AsyncWrite + Unpin + Send + use<>> {
|
||||
let proxy = parse_proxy_url(proxy_url)?;
|
||||
let proxy_host = proxy.host_str().ok_or_else(invalid_proxy)?;
|
||||
let target_host = target.host_str().ok_or_else(invalid_proxy)?;
|
||||
let target_port = target.port_or_known_default().ok_or_else(invalid_proxy)?;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Adapts an authenticated tunnel to reqwest's Windows named-pipe connector.
|
||||
//! Each listener serves one proxy and destination; only an accepted connection can outlive it.
|
||||
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use reqwest::Url;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::net::windows::named_pipe::ServerOptions;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use super::WindowsProxyTls;
|
||||
use super::connect_windows_proxy;
|
||||
use crate::HttpClientTlsConfig;
|
||||
|
||||
static NEXT_PIPE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub(crate) struct ProxyPipe {
|
||||
path: PathBuf,
|
||||
listener: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl ProxyPipe {
|
||||
pub(crate) fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProxyPipe {
|
||||
fn drop(&mut self) {
|
||||
self.listener.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called only on a confirmed proxy CONNECT 407. A pipe cannot route an arbitrary URL:
|
||||
/// reqwest sends TLS first, so the pipe must already know its proxy and destination.
|
||||
pub(crate) async fn pipe_for(
|
||||
proxy_url: &str,
|
||||
target: &Url,
|
||||
tls: &HttpClientTlsConfig,
|
||||
connect_timeout: Option<Duration>,
|
||||
) -> io::Result<ProxyPipe> {
|
||||
let proxy_tls = if Url::parse(proxy_url).map_err(io::Error::other)?.scheme() == "https" {
|
||||
tls.windows_proxy_tls()?
|
||||
} else {
|
||||
WindowsProxyTls::Default
|
||||
};
|
||||
// Surface auth failure before reqwest starts TLS on the pipe and obscures the error.
|
||||
let upstream =
|
||||
connect_with_timeout(proxy_url, target, proxy_tls.clone(), connect_timeout).await?;
|
||||
let id = NEXT_PIPE.fetch_add(1, Ordering::Relaxed);
|
||||
let path = PathBuf::from(format!(
|
||||
r"\\.\pipe\codex-proxy-auth-{}-{id}",
|
||||
std::process::id(),
|
||||
));
|
||||
let first = ServerOptions::new()
|
||||
.first_pipe_instance(true)
|
||||
.reject_remote_clients(true)
|
||||
.create(&path)?;
|
||||
let proxy_url = proxy_url.to_owned();
|
||||
let target = target.clone();
|
||||
let listen_path = path.clone();
|
||||
let listener = tokio::spawn(async move {
|
||||
let mut next = first;
|
||||
let mut first_upstream = Some(upstream);
|
||||
while next.connect().await.is_ok() {
|
||||
// Always prepare the next instance before serving this connection.
|
||||
// Reqwest opens pipes without retrying ERROR_PIPE_BUSY.
|
||||
let mut accepted = next;
|
||||
let Ok(server) = ServerOptions::new()
|
||||
.reject_remote_clients(true)
|
||||
.create(&listen_path)
|
||||
else {
|
||||
break;
|
||||
};
|
||||
next = server;
|
||||
let proxy_url = proxy_url.clone();
|
||||
let target = target.clone();
|
||||
let proxy_tls = proxy_tls.clone();
|
||||
let connected = first_upstream.take();
|
||||
tokio::spawn(async move {
|
||||
let result = async {
|
||||
let mut upstream = match connected {
|
||||
Some(upstream) => upstream,
|
||||
None => {
|
||||
connect_with_timeout(&proxy_url, &target, proxy_tls, connect_timeout)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
tokio::io::copy_bidirectional(&mut accepted, &mut upstream).await?;
|
||||
Ok::<(), io::Error>(())
|
||||
}
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
tracing::debug!(kind = ?error.kind(), "Windows proxy pipe ended");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Ok(ProxyPipe { path, listener })
|
||||
}
|
||||
|
||||
async fn connect_with_timeout(
|
||||
proxy_url: &str,
|
||||
target: &Url,
|
||||
proxy_tls: WindowsProxyTls,
|
||||
connect_timeout: Option<Duration>,
|
||||
) -> io::Result<impl AsyncRead + AsyncWrite + Unpin + Send + use<>> {
|
||||
let connect = connect_windows_proxy(proxy_url, target, proxy_tls);
|
||||
match connect_timeout {
|
||||
Some(timeout) => tokio::time::timeout(timeout, connect).await.map_err(|_| {
|
||||
io::Error::new(io::ErrorKind::TimedOut, "proxy authentication timed out")
|
||||
})?,
|
||||
None => connect.await,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user