mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
Add a Windows proxy authentication tunnel helper (#55994)
## What changed Expose `connect_windows_proxy` for callers handling a CONNECT `407` response. Use libcurl with the signed-in Windows user's Negotiate or NTLM credentials, then return the authenticated connection with buffered data preserved. Support HTTP and HTTPS proxies, with default or caller-provided TLS configuration. HTTPS proxies requiring authentication tokens bound to their TLS connection are unsupported. Enable libcurl's NTLM support in Cargo and Bazel builds. ## Testing Add a Windows-only test asserting that libcurl provides NTLM, SPNEGO, and SSPI support. GitOrigin-RevId: 4ed7d2208766404b6125ce2f4b5f91de5616a37f
This commit is contained in:
@@ -385,6 +385,11 @@ crate.from_cargo(
|
||||
|
||||
bazel_dep(name = "zstd", version = "1.5.7")
|
||||
|
||||
crate.annotation(
|
||||
# Keep Windows proxy authentication aligned with codex-rs/.cargo/config.toml.
|
||||
build_script_env = {"CFLAGS": "-DCURL_ENABLE_NTLM"},
|
||||
crate = "curl-sys",
|
||||
)
|
||||
crate.annotation(
|
||||
# Daemon tests repeatedly hash large CLI binaries, even in fastbuild mode.
|
||||
# Optimize the native backends as well as the Rust implementation.
|
||||
|
||||
Generated
+3
@@ -1205,6 +1205,8 @@
|
||||
"ctor_1.0.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"libc-print\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"link-section\",\"optional\":true,\"req\":\"^0.17.0\"},{\"features\":[\"ctor\"],\"name\":\"linktime-proc-macro\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"macrotest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2\"}],\"features\":{\"default\":[\"std\",\"proc_macro\",\"priority\"],\"priority\":[\"dep:link-section\"],\"proc_macro\":[\"dep:linktime-proc-macro\"],\"std\":[]}}",
|
||||
"ctr_0.9.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"aes\",\"req\":\"^0.8\"},{\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3.3\"},{\"kind\":\"dev\",\"name\":\"kuznyechik\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"magma\",\"req\":\"^0.8\"}],\"features\":{\"alloc\":[\"cipher/alloc\"],\"block-padding\":[\"cipher/block-padding\"],\"std\":[\"cipher/std\",\"alloc\"],\"zeroize\":[\"cipher/zeroize\"]}}",
|
||||
"ctutils_0.4.2": "{\"dependencies\":[{\"name\":\"cmov\",\"req\":\"^0.5.3\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.11\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"}],\"features\":{\"alloc\":[],\"subtle\":[\"dep:subtle\"]}}",
|
||||
"curl-sys_0.4.90+curl-8.21.0": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"name\":\"libc\",\"req\":\"^0.2.2\"},{\"name\":\"libnghttp2-sys\",\"optional\":true,\"req\":\"^0.1.3\"},{\"default_features\":false,\"features\":[\"libc\"],\"name\":\"libz-sys\",\"req\":\"^1.0.18\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.64\",\"target\":\"cfg(all(unix))\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.3\"},{\"features\":[\"no_log_capture\"],\"name\":\"rustls-ffi\",\"optional\":true,\"req\":\"^0.15\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2\",\"target\":\"cfg(target_env = \\\"msvc\\\")\"},{\"features\":[\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.59.0, <0.62.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"ssl\"],\"force-system-lib-on-osx\":[],\"http2\":[\"libnghttp2-sys\"],\"mesalink\":[],\"ntlm\":[],\"poll_7_68_0\":[],\"protocol-ftp\":[],\"rustls\":[\"rustls-ffi\"],\"spnego\":[],\"ssl\":[\"openssl-sys\"],\"static-curl\":[],\"static-ssl\":[\"openssl-sys/vendored\"],\"upkeep_7_62_0\":[],\"windows-static-ssl\":[],\"zlib-ng-compat\":[\"libz-sys/zlib-ng\",\"static-curl\"]}}",
|
||||
"curl_0.4.50": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.31\"},{\"default_features\":false,\"name\":\"curl-sys\",\"req\":\"^0.4.89\"},{\"name\":\"libc\",\"req\":\"^0.2.42\"},{\"kind\":\"dev\",\"name\":\"mio\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"mio-extras\",\"req\":\"^2.0.3\"},{\"name\":\"openssl-probe\",\"optional\":true,\"req\":\"^0.1.2\",\"target\":\"cfg(all(unix))\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.64\",\"target\":\"cfg(all(unix))\"},{\"name\":\"schannel\",\"req\":\"^0.1.13\",\"target\":\"cfg(target_env = \\\"msvc\\\")\"},{\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_LibraryLoader\",\"Win32_Security_Cryptography\"],\"name\":\"windows-sys\",\"req\":\">=0.59.0, <0.62.0\",\"target\":\"cfg(target_env = \\\"msvc\\\")\"}],\"features\":{\"default\":[\"ssl\"],\"force-system-lib-on-osx\":[\"curl-sys/force-system-lib-on-osx\"],\"http2\":[\"curl-sys/http2\"],\"mesalink\":[\"curl-sys/mesalink\"],\"ntlm\":[\"curl-sys/ntlm\"],\"poll_7_68_0\":[\"curl-sys/poll_7_68_0\"],\"protocol-ftp\":[\"curl-sys/protocol-ftp\"],\"rustls\":[\"curl-sys/rustls\"],\"spnego\":[\"curl-sys/spnego\"],\"ssl\":[\"openssl-sys\",\"openssl-probe\",\"curl-sys/ssl\"],\"static-curl\":[\"curl-sys/static-curl\"],\"static-ssl\":[\"curl-sys/static-ssl\"],\"upkeep_7_62_0\":[\"curl-sys/upkeep_7_62_0\"],\"windows-static-ssl\":[\"static-curl\",\"curl-sys/windows-static-ssl\"],\"zlib-ng-compat\":[\"curl-sys/zlib-ng-compat\",\"static-curl\"]}}",
|
||||
"curve25519-dalek-derive_0.1.1": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.66\"},{\"name\":\"quote\",\"req\":\"^1.0.31\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.27\"}],\"features\":{}}",
|
||||
"curve25519-dalek_4.1.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2.6\",\"target\":\"cfg(target_arch = \\\"x86_64\\\")\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"curve25519-dalek-derive\",\"req\":\"^0.1\",\"target\":\"cfg(all(not(curve25519_dalek_backend = \\\"fiat\\\"), not(curve25519_dalek_backend = \\\"serial\\\"), target_arch = \\\"x86_64\\\"))\"},{\"default_features\":false,\"name\":\"digest\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"ff\",\"optional\":true,\"req\":\"^0.13\"},{\"default_features\":false,\"name\":\"fiat-crypto\",\"req\":\"^0.2.1\",\"target\":\"cfg(curve25519_dalek_backend = \\\"fiat\\\")\"},{\"default_features\":false,\"name\":\"group\",\"optional\":true,\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"default_features\":false,\"features\":[\"getrandom\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"zeroize?/alloc\"],\"default\":[\"alloc\",\"precomputed-tables\",\"zeroize\"],\"group\":[\"dep:group\",\"rand_core\"],\"group-bits\":[\"group\",\"ff/bits\"],\"legacy_compatibility\":[],\"precomputed-tables\":[]}}",
|
||||
"darling_0.20.11": "{\"dependencies\":[{\"name\":\"darling_core\",\"req\":\"=0.20.11\"},{\"name\":\"darling_macro\",\"req\":\"=0.20.11\"},{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.86\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0.18\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.9\",\"target\":\"cfg(compiletests)\"},{\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.15\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.89\",\"target\":\"cfg(compiletests)\"}],\"features\":{\"default\":[\"suggestions\"],\"diagnostics\":[\"darling_core/diagnostics\"],\"suggestions\":[\"darling_core/suggestions\"]}}",
|
||||
@@ -1597,6 +1599,7 @@
|
||||
"libredox_0.1.14": "{\"dependencies\":[{\"name\":\"bitflags\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"ioslice\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"plain\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"redox_syscall\",\"optional\":true,\"req\":\"^0.7\"}],\"features\":{\"base\":[\"libc\"],\"call\":[\"base\"],\"default\":[\"base\",\"call\",\"std\",\"redox_syscall\",\"protocol\"],\"mkns\":[\"ioslice\"],\"protocol\":[\"plain\",\"bitflags\",\"redox_syscall\"],\"std\":[\"base\"]}}",
|
||||
"libsqlite3-sys_0.37.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72\"},{\"kind\":\"build\",\"name\":\"cc\",\"optional\":true,\"req\":\"^1.2.27\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.103\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"optional\":true,\"req\":\"^0.3.19\"},{\"kind\":\"build\",\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2.20\"},{\"default_features\":false,\"kind\":\"build\",\"name\":\"quote\",\"optional\":true,\"req\":\"^1.0.36\"},{\"features\":[\"full\",\"extra-traits\",\"visit-mut\"],\"kind\":\"build\",\"name\":\"syn\",\"optional\":true,\"req\":\"^2.0.89\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"optional\":true,\"req\":\"^0.2.15\"}],\"features\":{\"buildtime_bindgen\":[\"bindgen\",\"pkg-config\",\"vcpkg\"],\"bundled\":[\"cc\",\"bundled_bindings\"],\"bundled-sqlcipher\":[\"bundled\"],\"bundled-sqlcipher-vendored-openssl\":[\"bundled-sqlcipher\",\"openssl-sys/vendored\"],\"bundled-windows\":[\"cc\",\"bundled_bindings\"],\"bundled_bindings\":[],\"column_metadata\":[],\"default\":[\"min_sqlite_version_3_34_1\"],\"in_gecko\":[],\"loadable_extension\":[\"prettyplease\",\"quote\",\"syn\"],\"min_sqlite_version_3_34_1\":[\"pkg-config\",\"vcpkg\"],\"preupdate_hook\":[\"buildtime_bindgen\"],\"session\":[\"preupdate_hook\",\"buildtime_bindgen\"],\"sqlcipher\":[],\"unlock_notify\":[],\"wasm32-wasi-vfs\":[],\"with-asan\":[]}}",
|
||||
"libz-sys_1.1.25": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.98\"},{\"kind\":\"build\",\"name\":\"cmake\",\"optional\":true,\"req\":\"^0.1.50\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.43\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.11\"}],\"features\":{\"asm\":[],\"default\":[\"libc\",\"stock-zlib\"],\"static\":[],\"stock-zlib\":[],\"zlib-ng\":[\"libc\",\"cmake\"],\"zlib-ng-no-cmake-experimental-community-maintained\":[\"libc\"]}}",
|
||||
"libz-sys_1.1.29": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.98\"},{\"kind\":\"build\",\"name\":\"cmake\",\"optional\":true,\"req\":\"^0.1.50\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.43\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.11\"}],\"features\":{\"asm\":[],\"default\":[\"libc\",\"stock-zlib\"],\"static\":[],\"stock-zlib\":[],\"zlib-ng\":[\"libc\",\"cmake\"],\"zlib-ng-no-cmake-experimental-community-maintained\":[\"libc\"]}}",
|
||||
"line-clipping_0.3.7": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.11\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.26\"}],\"features\":{}}",
|
||||
"link-section_0.17.2": "{\"dependencies\":[{\"features\":[\"link_section\"],\"name\":\"linktime-proc-macro\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"macrotest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2\"}],\"features\":{\"default\":[\"proc_macro\",\"std\"],\"proc_macro\":[\"dep:linktime-proc-macro\"],\"std\":[]}}",
|
||||
"linkify_0.11.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.3\"},{\"name\":\"memchr\",\"req\":\"^2.0.1\"}],\"features\":{}}",
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
[env]
|
||||
# curl-sys enables its NTLM sources but does not set the flag required by libcurl 8.21.
|
||||
CFLAGS_aarch64_pc_windows_gnullvm = "-DCURL_ENABLE_NTLM"
|
||||
CFLAGS_aarch64_pc_windows_msvc = "-DCURL_ENABLE_NTLM"
|
||||
CFLAGS_x86_64_pc_windows_gnullvm = "-DCURL_ENABLE_NTLM"
|
||||
CFLAGS_x86_64_pc_windows_msvc = "-DCURL_ENABLE_NTLM"
|
||||
|
||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||
rustflags = ["-C", "link-arg=/STACK:8388608", "-C", "target-feature=+crt-static"]
|
||||
|
||||
|
||||
Generated
+46
@@ -3728,6 +3728,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-rustls-provider",
|
||||
"curl",
|
||||
"futures",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
@@ -3746,13 +3747,16 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sha2 0.10.9",
|
||||
"socket2 0.6.3",
|
||||
"system-configuration",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tracing",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"windows-sys 0.61.2",
|
||||
"zstd",
|
||||
]
|
||||
@@ -6123,6 +6127,36 @@ dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curl"
|
||||
version = "0.4.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a45ee8994e5307cb4c60cfc1c20bf7263ffb771ddc135c9f768a14bcbc15b09"
|
||||
dependencies = [
|
||||
"curl-sys",
|
||||
"libc",
|
||||
"openssl-probe 0.1.6",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"socket2 0.6.3",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curl-sys"
|
||||
version = "0.4.90+curl-8.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97799a0d220bfb3361e0fe4936966ff8c4b24d65c3f06dfc70d7b680b44e7897"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
@@ -9915,6 +9949,18 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "line-clipping"
|
||||
version = "0.3.7"
|
||||
|
||||
@@ -31,7 +31,12 @@ zstd = { workspace = true }
|
||||
system-configuration = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
curl = { version = "0.4.50", features = ["static-curl", "ntlm"] }
|
||||
rustls-platform-verifier = "0.7.0"
|
||||
socket2 = "0.6"
|
||||
tokio = { workspace = true, features = ["io-util", "net"] }
|
||||
tokio-rustls = { workspace = true }
|
||||
url = { workspace = true }
|
||||
windows-sys = { workspace = true, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Networking_WinHttp",
|
||||
|
||||
@@ -72,8 +72,14 @@ pub use crate::transport::HttpTransport;
|
||||
pub use crate::transport::ReqwestTransport;
|
||||
pub use crate::transport::StreamResponse;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_proxy_auth;
|
||||
#[cfg(windows)]
|
||||
mod windows_tls;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub use crate::windows_proxy_auth::WindowsProxyTls;
|
||||
#[cfg(windows)]
|
||||
pub use crate::windows_proxy_auth::connect_windows_proxy;
|
||||
#[cfg(windows)]
|
||||
pub use crate::windows_tls::build_windows_platform_tls_config;
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
//! Authenticates a connection to a Windows proxy after a CONNECT 407.
|
||||
//! Codex owns the connection. Curl reaches it through a local socket used only for
|
||||
//! authentication. The caller receives the authenticated connection and buffered data.
|
||||
//! HTTPS proxies that require authentication tokens bound to their TLS connection
|
||||
//! are not supported because curl does not own that connection.
|
||||
|
||||
use std::io;
|
||||
use std::io::Cursor;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::SocketAddr;
|
||||
use std::os::windows::io::IntoRawSocket;
|
||||
use std::os::windows::io::RawSocket;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use curl::easy::Auth;
|
||||
use curl::easy::Easy2;
|
||||
use curl::easy::Handler;
|
||||
use curl::easy::List;
|
||||
use reqwest::Url;
|
||||
use rustls::ClientConfig;
|
||||
use rustls::pki_types::ServerName;
|
||||
use socket2::Socket;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::io::BufReader;
|
||||
use tokio::io::Interest;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::watch;
|
||||
use tokio::task::JoinSet;
|
||||
use tokio_rustls::TlsConnector;
|
||||
use url::Host;
|
||||
|
||||
use crate::build_rustls_client_config_with_custom_ca;
|
||||
|
||||
/// Reads and writes a TCP or TLS connection to the proxy.
|
||||
trait ProxyIo: AsyncRead + AsyncWrite + Unpin + Send {}
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin + Send> ProxyIo for T {}
|
||||
|
||||
struct CurlSocket {
|
||||
// After CONNECT, this duplicate lets Tokio wait for curl's socket without polling.
|
||||
// All reads still go through curl so bytes in curl's own buffers are preserved.
|
||||
readiness: Option<std::net::TcpStream>,
|
||||
expected_peer: Arc<Mutex<Option<SocketAddr>>>,
|
||||
cancellation: watch::Sender<()>,
|
||||
}
|
||||
|
||||
impl Handler for CurlSocket {
|
||||
fn open_socket(&mut self, family: i32, kind: i32, protocol: i32) -> Option<RawSocket> {
|
||||
let socket = Socket::new(family.into(), kind.into(), Some(protocol.into())).ok()?;
|
||||
socket
|
||||
.bind(&SocketAddr::from((Ipv4Addr::LOCALHOST, 0)).into())
|
||||
.ok()?;
|
||||
self.readiness = Some(socket.try_clone().ok()?.into());
|
||||
*self.expected_peer.lock().ok()? = Some(socket.local_addr().ok()?.as_socket()?);
|
||||
Some(socket.into_raw_socket())
|
||||
}
|
||||
|
||||
fn progress(&mut self, _dltotal: f64, _dlnow: f64, _ultotal: f64, _ulnow: f64) -> bool {
|
||||
!self.cancellation.is_closed()
|
||||
}
|
||||
}
|
||||
|
||||
/// TLS configuration for the connection to an HTTPS proxy.
|
||||
#[derive(Clone)]
|
||||
pub enum WindowsProxyTls {
|
||||
/// Use the shared default roots and custom CA configuration.
|
||||
Default,
|
||||
/// Reuse the caller's TLS configuration.
|
||||
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<>> {
|
||||
let proxy = Url::parse(proxy_url).map_err(|_| invalid_proxy())?;
|
||||
if !matches!(proxy.scheme(), "http" | "https")
|
||||
|| !proxy.username().is_empty()
|
||||
|| proxy.password().is_some()
|
||||
{
|
||||
return Err(invalid_proxy());
|
||||
}
|
||||
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)?;
|
||||
let listener = Arc::new(TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?);
|
||||
let local_port = listener.local_addr()?.port();
|
||||
|
||||
// Keep the real hostname for Windows' HTTP/<proxy host> identity. Resolve it to our
|
||||
// local socket for this handle only; Codex connects to the original proxy URL below.
|
||||
let mut dns = List::new();
|
||||
dns.append(&format!("{proxy_host}:{local_port}:127.0.0.1"))
|
||||
.map_err(io::Error::other)?;
|
||||
let mut auth = Auth::new();
|
||||
// Curl uses the signed-in user for Negotiate and separately advertised NTLM.
|
||||
// libcurl 8.22 removes NTLM from Windows Negotiate; check this behavior on upgrades.
|
||||
auth.gssnegotiate(true).ntlm(true);
|
||||
let expected_peer = Arc::new(Mutex::new(None));
|
||||
let (cancellation, _authentication_alive) = watch::channel(());
|
||||
let mut curl = Easy2::new(CurlSocket {
|
||||
readiness: None,
|
||||
expected_peer: Arc::clone(&expected_peer),
|
||||
cancellation,
|
||||
});
|
||||
curl.proxy(&format!("http://{proxy_host}:{local_port}"))
|
||||
.map_err(io::Error::other)?;
|
||||
curl.resolve(dns).map_err(io::Error::other)?;
|
||||
curl.noproxy("").map_err(io::Error::other)?;
|
||||
curl.url(&format!("http://{target_host}:{target_port}/"))
|
||||
.map_err(io::Error::other)?;
|
||||
curl.http_proxy_tunnel(true).map_err(io::Error::other)?;
|
||||
curl.connect_only(true).map_err(io::Error::other)?;
|
||||
curl.proxy_username("").map_err(io::Error::other)?;
|
||||
curl.proxy_password("").map_err(io::Error::other)?;
|
||||
curl.proxy_auth(&auth).map_err(io::Error::other)?;
|
||||
curl.progress(true).map_err(io::Error::other)?;
|
||||
|
||||
let listener_guard = Arc::clone(&listener);
|
||||
let mut authentication = tokio::task::spawn_blocking(move || {
|
||||
// Keep the port reserved until curl stops, even if the caller cancels.
|
||||
let _listener_guard = listener_guard;
|
||||
curl.perform().map_err(io::Error::other)?;
|
||||
Ok::<_, io::Error>(curl)
|
||||
});
|
||||
// Curl can reconnect. Keep the proxy connection that completed authentication.
|
||||
let (authenticated, _) = watch::channel(None);
|
||||
// Owning the jobs here also closes the real and local sockets if the caller cancels.
|
||||
let mut connections = JoinSet::new();
|
||||
let mut curl = loop {
|
||||
tokio::select! {
|
||||
result = &mut authentication => break result.map_err(io::Error::other)??,
|
||||
accepted = listener.accept() => {
|
||||
let (local, peer) = accepted?;
|
||||
{
|
||||
let mut expected = expected_peer
|
||||
.lock()
|
||||
.map_err(|_| io::Error::other("proxy socket state is poisoned"))?;
|
||||
if *expected != Some(peer) {
|
||||
continue;
|
||||
}
|
||||
*expected = None;
|
||||
}
|
||||
// Curl has moved to this socket, so earlier attempts cannot authenticate.
|
||||
connections.shutdown().await;
|
||||
let proxy = proxy.clone();
|
||||
let proxy_tls = proxy_tls.clone();
|
||||
let mut done = authenticated.subscribe();
|
||||
connections.spawn(async move {
|
||||
let upstream = connect_to_proxy(&proxy, proxy_tls).await?;
|
||||
let (mut from_curl, mut to_curl) = local.into_split();
|
||||
let (from_proxy, mut to_proxy) = tokio::io::split(upstream);
|
||||
let mut from_proxy = BufReader::new(from_proxy);
|
||||
tokio::select! {
|
||||
changed = done.changed() => {
|
||||
changed.map_err(io::Error::other)?;
|
||||
let selected = *done.borrow_and_update() == Some(peer.port());
|
||||
if selected {
|
||||
// Do not shut down the real writer. EOF on this side lets
|
||||
// curl return all bytes already sent to its local socket.
|
||||
to_curl.shutdown().await?;
|
||||
return Ok(Some((from_proxy, to_proxy)));
|
||||
}
|
||||
}
|
||||
result = async {
|
||||
tokio::try_join!(
|
||||
// copy flushes before waiting for more input, so Rustls
|
||||
// sends CONNECT before curl waits for the proxy response.
|
||||
tokio::io::copy(&mut from_curl, &mut to_proxy),
|
||||
async {
|
||||
tokio::io::copy_buf(&mut from_proxy, &mut to_curl).await?;
|
||||
to_curl.shutdown().await
|
||||
},
|
||||
)
|
||||
} => { result?; }
|
||||
}
|
||||
Ok::<_, io::Error>(None)
|
||||
});
|
||||
}
|
||||
_ = connections.join_next(), if !connections.is_empty() => {}
|
||||
}
|
||||
};
|
||||
authenticated.send_replace(Some(curl.local_port().map_err(io::Error::other)?));
|
||||
let (from_proxy, to_proxy) = loop {
|
||||
match connections.join_next().await {
|
||||
Some(Ok(Ok(Some(halves)))) => break halves,
|
||||
Some(_) => {}
|
||||
None => return Err(io::Error::other("authenticated proxy connection ended")),
|
||||
}
|
||||
};
|
||||
let readiness = curl
|
||||
.get_mut()
|
||||
.readiness
|
||||
.take()
|
||||
.ok_or_else(|| io::Error::other("curl did not open a proxy connection"))?;
|
||||
let readiness = TcpStream::from_std(readiness)?;
|
||||
let mut prefix = Vec::new();
|
||||
let mut buffer = [0; 8192];
|
||||
loop {
|
||||
// curl can hold bytes even while the socket isn't readable.
|
||||
let read = match curl.recv(&mut buffer) {
|
||||
Ok(read) => read,
|
||||
Err(error) if error.is_again() => {
|
||||
readiness
|
||||
.async_io(Interest::READABLE, || {
|
||||
curl.recv(&mut buffer).map_err(|error| {
|
||||
if error.is_again() {
|
||||
io::ErrorKind::WouldBlock.into()
|
||||
} else {
|
||||
io::Error::other(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
.await?
|
||||
}
|
||||
Err(error) => return Err(io::Error::other(error)),
|
||||
};
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
prefix.extend_from_slice(&buffer[..read]);
|
||||
}
|
||||
Ok(tokio::io::join(
|
||||
Cursor::new(prefix).chain(from_proxy),
|
||||
to_proxy,
|
||||
))
|
||||
}
|
||||
|
||||
async fn connect_to_proxy(proxy: &Url, proxy_tls: WindowsProxyTls) -> io::Result<Box<dyn ProxyIo>> {
|
||||
let host = match proxy.host().ok_or_else(invalid_proxy)? {
|
||||
Host::Domain(domain) => domain.to_owned(),
|
||||
Host::Ipv4(address) => address.to_string(),
|
||||
Host::Ipv6(address) => address.to_string(),
|
||||
};
|
||||
let port = proxy.port_or_known_default().ok_or_else(invalid_proxy)?;
|
||||
let tcp = TcpStream::connect((host.as_str(), port)).await?;
|
||||
tcp.set_nodelay(true)?;
|
||||
if proxy.scheme() == "https" {
|
||||
let config = match proxy_tls {
|
||||
WindowsProxyTls::Default => {
|
||||
build_rustls_client_config_with_custom_ca().map_err(io::Error::other)?
|
||||
}
|
||||
WindowsProxyTls::Custom(config) => config,
|
||||
};
|
||||
let server_name = ServerName::try_from(host).map_err(|_| invalid_proxy())?;
|
||||
Ok(Box::new(
|
||||
TlsConnector::from(config).connect(server_name, tcp).await?,
|
||||
))
|
||||
} else {
|
||||
Ok(Box::new(tcp))
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_proxy() -> io::Error {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"invalid Windows authentication proxy route",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#![cfg(windows)]
|
||||
|
||||
#[test]
|
||||
fn curl_supports_windows_proxy_authentication() {
|
||||
let version = curl::Version::get();
|
||||
assert!(
|
||||
version.feature_ntlm() && version.feature_spnego() && version.feature_sspi(),
|
||||
"{version:?}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user