mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
## Why Realtime WebSockets connected directly, bypassing the configured proxy policy for Voice's WebRTC sideband. ## What changed Pass the configured `HttpClientFactory` into `RealtimeWebsocketClient` and connect through the shared `WebSocketConnector`. Add support for supplying an existing Rustls configuration to preserve custom CA handling and Windows platform trust. ## Testing Add an integration test that enables `respect_system_proxy`, starts a WebRTC conversation, and verifies that the sideband establishes its session through an HTTP `CONNECT` proxy and closes successfully. GitOrigin-RevId: d974c9275d7fe29f388d56a9d5d8ce0c483eb8d0
173 lines
6.5 KiB
Rust
173 lines
6.5 KiB
Rust
#![allow(clippy::expect_used)]
|
|
//! Exercise realtime TLS selection without mutating the test process's trust environment.
|
|
|
|
use std::io;
|
|
use std::net::TcpListener;
|
|
use std::process::Command;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use std::time::Instant;
|
|
|
|
use codex_api::Provider;
|
|
use codex_api::RealtimeEventParser;
|
|
use codex_api::RealtimeOutputModality;
|
|
use codex_api::RealtimeSessionConfig;
|
|
use codex_api::RealtimeSessionMode;
|
|
use codex_api::RealtimeWebsocketClient;
|
|
use codex_api::RetryConfig;
|
|
use codex_http_client::HttpClientFactory;
|
|
use codex_http_client::OutboundProxyPolicy;
|
|
use codex_protocol::protocol::RealtimeVoice;
|
|
use http::HeaderMap;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
const ADDRESS_ENV: &str = "CODEX_TEST_REALTIME_TLS_ADDRESS";
|
|
const TRUST_ENV: &str = "CODEX_TEST_REALTIME_TLS_TRUST";
|
|
|
|
#[test]
|
|
fn realtime_tls_selects_system_and_custom_trust() {
|
|
if let Ok(address) = std::env::var(ADDRESS_ENV) {
|
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap();
|
|
runtime.block_on(check_connection(address));
|
|
return;
|
|
}
|
|
|
|
codex_utils_rustls_provider::ensure_rustls_crypto_provider();
|
|
let certificate = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
|
|
let temp = tempfile::TempDir::new().unwrap();
|
|
let ca = temp.path().join("ca.pem");
|
|
std::fs::write(&ca, certificate.cert.pem()).unwrap();
|
|
let config = Arc::new(
|
|
rustls::ServerConfig::builder()
|
|
.with_no_client_auth()
|
|
.with_single_cert(
|
|
vec![certificate.cert.der().clone()],
|
|
certificate.signing_key.into(),
|
|
)
|
|
.unwrap(),
|
|
);
|
|
|
|
for trust in ["system", "custom"] {
|
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
|
let address = listener.local_addr().unwrap();
|
|
listener.set_nonblocking(/*nonblocking*/ true).unwrap();
|
|
let config = config.clone();
|
|
let server = std::thread::spawn(move || {
|
|
let deadline = Instant::now() + Duration::from_secs(/*secs*/ 30);
|
|
let stream = loop {
|
|
match listener.accept() {
|
|
Ok((stream, _)) => break stream,
|
|
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
|
assert!(Instant::now() < deadline, "TLS client did not connect");
|
|
std::thread::sleep(Duration::from_millis(/*millis*/ 10));
|
|
}
|
|
Err(error) => panic!("TLS accept failed: {error}"),
|
|
}
|
|
};
|
|
stream.set_nonblocking(/*nonblocking*/ false).unwrap();
|
|
stream
|
|
.set_read_timeout(Some(Duration::from_secs(/*secs*/ 30)))
|
|
.unwrap();
|
|
stream
|
|
.set_write_timeout(Some(Duration::from_secs(/*secs*/ 30)))
|
|
.unwrap();
|
|
let tls =
|
|
rustls::StreamOwned::new(rustls::ServerConnection::new(config).unwrap(), stream);
|
|
match tungstenite::accept(tls) {
|
|
Ok(mut socket) => {
|
|
assert_eq!(trust, "custom", "system trust must reject the generated CA");
|
|
let message = socket.read().unwrap().into_text().unwrap();
|
|
let update: serde_json::Value = serde_json::from_str(&message).unwrap();
|
|
assert_eq!(update["type"], "session.update");
|
|
}
|
|
Err(_) => assert_eq!(trust, "system", "custom CA must allow WSS"),
|
|
}
|
|
});
|
|
// Re-execute this test so each connection uses its own CA environment, including on Windows.
|
|
let mut child = Command::new(std::env::current_exe().unwrap());
|
|
child
|
|
.args([
|
|
"--exact",
|
|
"realtime_tls_selects_system_and_custom_trust",
|
|
"--nocapture",
|
|
])
|
|
.env(ADDRESS_ENV, format!("localhost:{}", address.port()))
|
|
.env(TRUST_ENV, trust)
|
|
.env_remove("CODEX_CA_CERTIFICATE")
|
|
.env_remove("SSL_CERT_FILE");
|
|
if trust == "custom" {
|
|
child.env("CODEX_CA_CERTIFICATE", &ca).env(
|
|
"SSL_CERT_FILE",
|
|
temp.path().join("missing-lower-priority-ca.pem"),
|
|
);
|
|
}
|
|
let output = child.output().unwrap();
|
|
server.join().unwrap();
|
|
assert!(
|
|
output.status.success(),
|
|
"{trust}: {}\n{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
}
|
|
|
|
async fn check_connection(address: String) {
|
|
let client = RealtimeWebsocketClient::new(
|
|
Provider {
|
|
name: "local TLS test".into(),
|
|
base_url: format!("https://{address}"),
|
|
query_params: None,
|
|
headers: HeaderMap::new(),
|
|
retry: RetryConfig {
|
|
max_attempts: 1,
|
|
base_delay: Duration::from_millis(/*millis*/ 1),
|
|
retry_429: false,
|
|
retry_5xx: false,
|
|
retry_transport: false,
|
|
},
|
|
stream_idle_timeout: Duration::from_secs(/*secs*/ 5),
|
|
},
|
|
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
|
);
|
|
let result = tokio::time::timeout(
|
|
Duration::from_secs(/*secs*/ 20),
|
|
client.connect(
|
|
RealtimeSessionConfig {
|
|
instructions: "TLS test".into(),
|
|
initial_items: Vec::new(),
|
|
delegation_ack_filler: None,
|
|
model: Some("realtime-test-model".into()),
|
|
session_id: None,
|
|
event_parser: RealtimeEventParser::V1,
|
|
session_mode: RealtimeSessionMode::Conversational,
|
|
output_modality: RealtimeOutputModality::Audio,
|
|
voice: RealtimeVoice::Cove,
|
|
},
|
|
HeaderMap::new(),
|
|
HeaderMap::new(),
|
|
),
|
|
)
|
|
.await
|
|
.expect("TLS connection should finish");
|
|
match std::env::var(TRUST_ENV)
|
|
.expect("trust scenario should be set")
|
|
.as_str()
|
|
{
|
|
"custom" => {
|
|
result.expect("configured CA should permit realtime WSS");
|
|
}
|
|
"system" => {
|
|
let error = match result {
|
|
Ok(_) => panic!("system trust accepted an untrusted certificate"),
|
|
Err(error) => error.to_string(),
|
|
};
|
|
assert!(error.to_lowercase().contains("certificate"), "{error}");
|
|
}
|
|
other => panic!("unknown trust scenario: {other}"),
|
|
}
|
|
}
|