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:
efrazer-oai
2026-10-21 22:26:16 +00:00
committed by copyberry
parent d5bc4161b6
commit f496b09bd1
10 changed files with 524 additions and 112 deletions
+42 -14
View File
@@ -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,
}
}
}
+80 -4
View File
@@ -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)))
}
}
+17 -5
View File
@@ -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.
+7 -33
View File
@@ -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(
&current_url,
&current_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(
&current_route,
selected_tls_backend,
replay.as_ref(),
error,
timeout_deadline,
)
.await
}
Err(RouteAwareRequestError::Request(error)) => {
self.retry_with_rustls(
&current_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,
)
+24 -7
View File
@@ -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,
}
}