| use axum::Router; |
| use axum::body::Body; |
| use axum::extract::ConnectInfo; |
| use axum::extract::State; |
| use axum::extract::ws::WebSocketUpgrade; |
| use axum::http::Request; |
| use axum::http::StatusCode; |
| use axum::http::header::ORIGIN; |
| use axum::middleware; |
| use axum::middleware::Next; |
| use axum::response::IntoResponse; |
| use axum::response::Response; |
| use axum::routing::any; |
| use axum::routing::get; |
| use codex_http_client::HttpClientFactory; |
| use std::io::Write as _; |
| use std::net::SocketAddr; |
| use tokio::io; |
| use tokio::io::AsyncRead; |
| use tokio::io::AsyncWrite; |
| use tokio::net::TcpListener; |
| use tracing::info; |
| use tracing::warn; |
|
|
| use crate::ExecServerRuntimePaths; |
| use crate::ExecServerTelemetry; |
| use crate::connection::JsonRpcConnection; |
| use crate::server::RequestDispatchMode; |
| use crate::server::processor::ConnectionProcessor; |
| use crate::telemetry::ConnectionTransport; |
|
|
| pub const DEFAULT_LISTEN_URL: &str = "ws://127.0.0.1:0"; |
|
|
| #[derive(Debug, Clone, Eq, PartialEq)] |
| pub(crate) enum ExecServerListenTransport { |
| WebSocket(SocketAddr), |
| Stdio, |
| } |
|
|
| #[derive(Debug, Clone, Eq, PartialEq)] |
| pub enum ExecServerListenUrlParseError { |
| UnsupportedListenUrl(String), |
| InvalidWebSocketListenUrl(String), |
| } |
|
|
| impl std::fmt::Display for ExecServerListenUrlParseError { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| match self { |
| ExecServerListenUrlParseError::UnsupportedListenUrl(listen_url) => write!( |
| f, |
| "unsupported --listen URL `{listen_url}`; expected `ws://IP:PORT` or `stdio`" |
| ), |
| ExecServerListenUrlParseError::InvalidWebSocketListenUrl(listen_url) => write!( |
| f, |
| "invalid websocket --listen URL `{listen_url}`; expected `ws://IP:PORT`" |
| ), |
| } |
| } |
| } |
|
|
| impl std::error::Error for ExecServerListenUrlParseError {} |
|
|
| pub(crate) fn parse_listen_url( |
| listen_url: &str, |
| ) -> Result<ExecServerListenTransport, ExecServerListenUrlParseError> { |
| if matches!(listen_url, "stdio" | "stdio://") { |
| return Ok(ExecServerListenTransport::Stdio); |
| } |
|
|
| if let Some(socket_addr) = listen_url.strip_prefix("ws://") { |
| return socket_addr |
| .parse::<SocketAddr>() |
| .map(ExecServerListenTransport::WebSocket) |
| .map_err(|_| { |
| ExecServerListenUrlParseError::InvalidWebSocketListenUrl(listen_url.to_string()) |
| }); |
| } |
|
|
| Err(ExecServerListenUrlParseError::UnsupportedListenUrl( |
| listen_url.to_string(), |
| )) |
| } |
|
|
| pub(crate) async fn run_transport( |
| listen_url: &str, |
| runtime_paths: ExecServerRuntimePaths, |
| telemetry: ExecServerTelemetry, |
| http_client_factory: HttpClientFactory, |
| request_dispatch_mode: RequestDispatchMode, |
| ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
| match parse_listen_url(listen_url)? { |
| ExecServerListenTransport::WebSocket(bind_address) => { |
| run_websocket_listener( |
| bind_address, |
| runtime_paths, |
| telemetry, |
| http_client_factory, |
| request_dispatch_mode, |
| ) |
| .await |
| } |
| ExecServerListenTransport::Stdio => { |
| run_stdio_connection( |
| runtime_paths, |
| telemetry, |
| http_client_factory, |
| request_dispatch_mode, |
| ) |
| .await |
| } |
| } |
| } |
|
|
| async fn run_stdio_connection( |
| runtime_paths: ExecServerRuntimePaths, |
| telemetry: ExecServerTelemetry, |
| http_client_factory: HttpClientFactory, |
| request_dispatch_mode: RequestDispatchMode, |
| ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
| run_stdio_connection_with_io( |
| io::stdin(), |
| io::stdout(), |
| runtime_paths, |
| telemetry, |
| http_client_factory, |
| request_dispatch_mode, |
| ) |
| .await |
| } |
|
|
| async fn run_stdio_connection_with_io<R, W>( |
| reader: R, |
| writer: W, |
| runtime_paths: ExecServerRuntimePaths, |
| telemetry: ExecServerTelemetry, |
| http_client_factory: HttpClientFactory, |
| request_dispatch_mode: RequestDispatchMode, |
| ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> |
| where |
| R: AsyncRead + Unpin + Send + 'static, |
| W: AsyncWrite + Unpin + Send + 'static, |
| { |
| let processor = ConnectionProcessor::new_with_telemetry( |
| runtime_paths, |
| telemetry, |
| http_client_factory, |
| request_dispatch_mode, |
| ); |
| tracing::info!("codex-exec-server listening on stdio"); |
| processor |
| .run_connection( |
| JsonRpcConnection::from_stdio(reader, writer, "exec-server stdio".to_string()), |
| ConnectionTransport::Stdio, |
| ) |
| .await; |
| |
| processor.shutdown().await; |
| Ok(()) |
| } |
|
|
| async fn run_websocket_listener( |
| bind_address: SocketAddr, |
| runtime_paths: ExecServerRuntimePaths, |
| telemetry: ExecServerTelemetry, |
| http_client_factory: HttpClientFactory, |
| request_dispatch_mode: RequestDispatchMode, |
| ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
| let listener = TcpListener::bind(bind_address).await?; |
| let local_addr = listener.local_addr()?; |
| let processor = ConnectionProcessor::new_with_telemetry( |
| runtime_paths, |
| telemetry, |
| http_client_factory, |
| request_dispatch_mode, |
| ); |
| info!("codex-exec-server listening on ws://{local_addr}"); |
| println!("ws://{local_addr}"); |
| std::io::stdout().flush()?; |
|
|
| let router = Router::new() |
| .route("/", any(websocket_upgrade_handler)) |
| .route("/readyz", get(readiness_handler)) |
| .layer(middleware::from_fn(reject_requests_with_origin_header)) |
| .with_state(ExecServerWebSocketState { processor }); |
| axum::serve( |
| listener, |
| router.into_make_service_with_connect_info::<SocketAddr>(), |
| ) |
| .await?; |
| Ok(()) |
| } |
|
|
| #[derive(Clone)] |
| struct ExecServerWebSocketState { |
| processor: ConnectionProcessor, |
| } |
|
|
| async fn readiness_handler() -> StatusCode { |
| StatusCode::OK |
| } |
|
|
| async fn reject_requests_with_origin_header( |
| request: Request<Body>, |
| next: Next, |
| ) -> Result<Response, StatusCode> { |
| if request.headers().contains_key(ORIGIN) { |
| warn!( |
| method = %request.method(), |
| uri = %request.uri(), |
| "rejecting exec-server websocket listener request with Origin header" |
| ); |
| Err(StatusCode::FORBIDDEN) |
| } else { |
| Ok(next.run(request).await) |
| } |
| } |
|
|
| async fn websocket_upgrade_handler( |
| websocket: WebSocketUpgrade, |
| ConnectInfo(peer_addr): ConnectInfo<SocketAddr>, |
| State(state): State<ExecServerWebSocketState>, |
| ) -> impl IntoResponse { |
| info!(%peer_addr, "exec-server websocket client connected"); |
| websocket.on_upgrade(move |stream| async move { |
| state |
| .processor |
| .run_connection( |
| JsonRpcConnection::from_axum_websocket( |
| stream, |
| format!("exec-server websocket {peer_addr}"), |
| ), |
| ConnectionTransport::WebSocket, |
| ) |
| .await; |
| }) |
| } |
|
|
| #[cfg(test)] |
| #[path = "transport_tests.rs"] |
| mod transport_tests; |
|
|