mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
## What changed - Use the shared runtime and effective configuration for account API clients and remote plugin requests, including standalone plugin commands. - Support dual-header and assertion-only authentication for remote plugins and MCP file uploads when runtime agent instances are enabled. - Keep plugin credentials and caches scoped to the thread's account when resuming with different credentials. Bind request pools and multi-step plugin storage operations to the selected account so account replacement revokes stale operations. - Protect runtime credentials on cross-origin redirects while preserving configured routing policies and legacy authentication for custom backends. - Apply server-provided `upload_headers` to file storage uploads. ## Testing Add coverage for shared runtime registration, both authentication modes, configuration reloads, resumed-thread account selection, account replacement during plugin uploads and downloads, and credential stripping on redirects. Extend file upload tests to verify runtime assertions and storage-only upload headers. GitOrigin-RevId: 68aef81051a6f9bf919fffda52f372b108fce805
133 lines
5.6 KiB
Rust
133 lines
5.6 KiB
Rust
//! Route isolated first-party auth tests to loopback without relaxing destination checks.
|
|
|
|
use anyhow::Result;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::io::AsyncReadExt;
|
|
use tokio::io::AsyncWriteExt;
|
|
use tokio::net::TcpListener;
|
|
use tokio::net::TcpStream;
|
|
use tokio::process::Command;
|
|
use tokio::task::JoinSet;
|
|
use tokio_rustls::TlsAcceptor;
|
|
use tokio_rustls::rustls;
|
|
use wiremock::MockServer;
|
|
|
|
pub const PROBE_SERVER: &str = "CODEX_RUNTIME_AUTH_TEST_SERVER";
|
|
|
|
/// Run one test in an isolated subprocess with first-party HTTP routed to the mock server.
|
|
/// An optional Realtime target receives chatgpt.com Realtime WebSocket upgrades.
|
|
pub async fn run_harness_through_proxy(
|
|
server: &MockServer,
|
|
test_name: &str,
|
|
realtime_target: Option<std::net::SocketAddr>,
|
|
) -> Result<()> {
|
|
// The production destination check stays intact. A test-only CA and CONNECT proxy route
|
|
// the real host names to wiremock, only inside this isolated test subprocess.
|
|
codex_utils_rustls_provider::ensure_rustls_crypto_provider();
|
|
let rcgen::CertifiedKey { cert, signing_key } = rcgen::generate_simple_self_signed(vec![
|
|
"api.openai.com".into(),
|
|
"auth.openai.com".into(),
|
|
"chatgpt.com".into(),
|
|
"us.chatgpt.com".into(),
|
|
])?;
|
|
let ca_dir = tempfile::tempdir()?;
|
|
let ca_path = ca_dir.path().join("ca.pem");
|
|
std::fs::write(&ca_path, cert.pem())?;
|
|
let acceptor = TlsAcceptor::from(Arc::new(
|
|
rustls::ServerConfig::builder()
|
|
.with_no_client_auth()
|
|
.with_single_cert(vec![cert.der().clone()], signing_key.into())?,
|
|
));
|
|
let proxy = TcpListener::bind("127.0.0.1:0").await?;
|
|
let proxy_url = format!("http://{}", proxy.local_addr()?);
|
|
let target = *server.address();
|
|
let proxy_task = tokio::spawn(async move {
|
|
let mut connections = JoinSet::new();
|
|
loop {
|
|
let (mut stream, _) = proxy.accept().await?;
|
|
let acceptor = acceptor.clone();
|
|
connections.spawn(async move {
|
|
let mut headers = Vec::new();
|
|
while !headers.ends_with(b"\r\n\r\n") && headers.len() < 8192 {
|
|
headers.push(stream.read_u8().await?);
|
|
}
|
|
let headers = std::str::from_utf8(&headers)?;
|
|
anyhow::ensure!(
|
|
matches!(
|
|
headers.lines().next(),
|
|
Some(
|
|
"CONNECT api.openai.com:443 HTTP/1.1"
|
|
| "CONNECT auth.openai.com:443 HTTP/1.1"
|
|
| "CONNECT chatgpt.com:443 HTTP/1.1"
|
|
| "CONNECT us.chatgpt.com:443 HTTP/1.1"
|
|
)
|
|
),
|
|
"unexpected proxy destination"
|
|
);
|
|
stream
|
|
.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
|
|
.await?;
|
|
let realtime_host = headers.starts_with("CONNECT chatgpt.com:443 ");
|
|
let mut stream = acceptor.accept(stream).await?;
|
|
let mut prefix = Vec::new();
|
|
let mut destination = target;
|
|
if let Some(realtime_target) = realtime_target.filter(|_| realtime_host) {
|
|
// Background HTTP requests share this host with Realtime. Only upgrades
|
|
// belong on the WebSocket fixture; ordinary HTTP still goes to wiremock.
|
|
while !prefix.ends_with(b"\r\n\r\n") && prefix.len() < 65536 {
|
|
prefix.push(stream.read_u8().await?);
|
|
}
|
|
anyhow::ensure!(
|
|
prefix.ends_with(b"\r\n\r\n"),
|
|
"oversized test request headers"
|
|
);
|
|
let request_headers = std::str::from_utf8(&prefix)?;
|
|
let realtime_path = request_headers.lines().next().is_some_and(|line| {
|
|
line.starts_with("GET /v1/realtime") || line.starts_with("GET /v1/live")
|
|
});
|
|
if realtime_path
|
|
&& request_headers.lines().any(|line| {
|
|
line.split_once(':').is_some_and(|(name, value)| {
|
|
name.eq_ignore_ascii_case("upgrade")
|
|
&& value.trim().eq_ignore_ascii_case("websocket")
|
|
})
|
|
})
|
|
{
|
|
destination = realtime_target;
|
|
}
|
|
}
|
|
let mut target = TcpStream::connect(destination).await?;
|
|
target.write_all(&prefix).await?;
|
|
tokio::io::copy_bidirectional(&mut stream, &mut target).await?;
|
|
Ok::<_, anyhow::Error>(())
|
|
});
|
|
}
|
|
#[allow(unreachable_code)]
|
|
Ok::<_, anyhow::Error>(())
|
|
});
|
|
let mut command = Command::new(std::env::current_exe()?);
|
|
command
|
|
.args(["--exact", test_name, "--nocapture"])
|
|
.kill_on_drop(true);
|
|
for &key in codex_network_proxy::PROXY_ENV_KEYS {
|
|
command.env_remove(key);
|
|
}
|
|
command
|
|
.env(PROBE_SERVER, server.uri())
|
|
.env("HTTPS_PROXY", proxy_url)
|
|
.env("NO_PROXY", codex_network_proxy::DEFAULT_NO_PROXY_VALUE)
|
|
.env("CODEX_CA_CERTIFICATE", ca_path);
|
|
let output = tokio::time::timeout(Duration::from_secs(90), command.output()).await;
|
|
proxy_task.abort();
|
|
let output = output??;
|
|
assert!(
|
|
output.status.success(),
|
|
"harness subprocess failed\n{}\n{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
|
|
Ok(())
|
|
}
|