diff --git a/Cargo.lock b/Cargo.lock index f693acd66..6caa1f2a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3821,6 +3821,7 @@ version = "0.0.0" dependencies = [ "base64 0.22.1", "crossterm 0.28.1", + "futures", "miette", "openshell-bootstrap", "openshell-core", @@ -3831,6 +3832,7 @@ dependencies = [ "serde", "terminal-colorsaurus", "tokio", + "tokio-tungstenite 0.26.2", "tonic", "tracing", "url", diff --git a/crates/openshell-tui/Cargo.toml b/crates/openshell-tui/Cargo.toml index 238166136..aaeacf2f2 100644 --- a/crates/openshell-tui/Cargo.toml +++ b/crates/openshell-tui/Cargo.toml @@ -27,6 +27,8 @@ owo-colors = { workspace = true } serde = { workspace = true } tracing = { workspace = true } url = { workspace = true } +tokio-tungstenite = { workspace = true } +futures = { workspace = true } [lints] workspace = true diff --git a/crates/openshell-tui/src/edge_tunnel.rs b/crates/openshell-tui/src/edge_tunnel.rs new file mode 100644 index 000000000..4ebc67e0a --- /dev/null +++ b/crates/openshell-tui/src/edge_tunnel.rs @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Edge-authenticated WebSocket tunnel proxy for TUI gateway switching. + +use futures::stream::{SplitSink, SplitStream}; +use futures::{SinkExt, StreamExt}; +use miette::{IntoDiagnostic, Result}; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio_tungstenite::tungstenite::Message; +use tokio_tungstenite::tungstenite::client::IntoClientRequest; +use tokio_tungstenite::tungstenite::http::HeaderValue; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; +use tracing::{debug, error, warn}; + +pub struct EdgeTunnelProxy { + pub local_addr: SocketAddr, +} + +#[derive(Clone)] +struct TunnelConfig { + ws_url: String, + edge_token: String, +} + +pub async fn start_tunnel_proxy( + gateway_endpoint: &str, + edge_token: &str, +) -> Result { + let listener = TcpListener::bind("127.0.0.1:0").await.into_diagnostic()?; + let local_addr = listener.local_addr().into_diagnostic()?; + let ws_url = format!( + "{}/_ws_tunnel", + gateway_endpoint + .replacen("https://", "wss://", 1) + .replacen("http://", "ws://", 1) + .trim_end_matches('/') + ); + let config = Arc::new(TunnelConfig { + ws_url, + edge_token: edge_token.to_string(), + }); + + debug!( + local_addr = %local_addr, + gateway = %gateway_endpoint, + "starting TUI edge tunnel proxy" + ); + tokio::spawn(accept_loop(listener, config)); + Ok(EdgeTunnelProxy { local_addr }) +} + +async fn accept_loop(listener: TcpListener, config: Arc) { + loop { + match listener.accept().await { + Ok((stream, peer)) => { + let config = Arc::clone(&config); + tokio::spawn(async move { + if let Err(err) = handle_connection(stream, &config).await { + warn!(peer = %peer, error = %err, "TUI edge tunnel connection failed"); + } + }); + } + Err(err) => { + error!(error = %err, "failed to accept TUI edge tunnel connection"); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + } + } +} + +async fn handle_connection(tcp_stream: TcpStream, config: &TunnelConfig) -> Result<()> { + let ws_stream = open_ws(config).await?; + let (ws_sink, ws_source) = ws_stream.split(); + let (tcp_read, tcp_write) = tokio::io::split(tcp_stream); + + let mut tcp_to_ws = tokio::spawn(copy_tcp_to_ws(tcp_read, ws_sink)); + let mut ws_to_tcp = tokio::spawn(copy_ws_to_tcp(ws_source, tcp_write)); + + tokio::select! { + res = &mut tcp_to_ws => { + if let Err(err) = res { + debug!(error = %err, "TUI tcp->ws task panicked"); + } + ws_to_tcp.abort(); + } + res = &mut ws_to_tcp => { + if let Err(err) = res { + debug!(error = %err, "TUI ws->tcp task panicked"); + } + tcp_to_ws.abort(); + } + } + + Ok(()) +} + +async fn open_ws(config: &TunnelConfig) -> Result>> { + let mut request = (&config.ws_url).into_client_request().into_diagnostic()?; + let token_val = HeaderValue::from_str(&config.edge_token) + .map_err(|err| miette::miette!("invalid edge token header value: {err}"))?; + request + .headers_mut() + .insert("Cf-Access-Token", token_val.clone()); + request + .headers_mut() + .insert("Cf-Access-Jwt-Assertion", token_val); + request.headers_mut().insert( + "Cookie", + HeaderValue::from_str(&format!("CF_Authorization={}", config.edge_token)) + .map_err(|err| miette::miette!("invalid edge token cookie value: {err}"))?, + ); + + let (ws_stream, response) = tokio_tungstenite::connect_async(request) + .await + .map_err(|err| miette::miette!("WebSocket connect failed: {err}"))?; + debug!(status = %response.status(), "TUI edge WebSocket connected"); + Ok(ws_stream) +} + +async fn copy_tcp_to_ws( + mut tcp_read: tokio::io::ReadHalf, + mut ws_sink: SplitSink>, Message>, +) { + let mut buf = vec![0_u8; 32 * 1024]; + loop { + match tcp_read.read(&mut buf).await { + Ok(0) => { + let _ = ws_sink.close().await; + break; + } + Ok(n) => { + if ws_sink + .send(Message::Binary(buf[..n].to_vec().into())) + .await + .is_err() + { + break; + } + } + Err(err) => { + debug!(error = %err, "TUI tcp read error"); + let _ = ws_sink.close().await; + break; + } + } + } +} + +async fn copy_ws_to_tcp( + mut ws_source: SplitStream>>, + mut tcp_write: tokio::io::WriteHalf, +) { + while let Some(msg) = ws_source.next().await { + match msg { + Ok(Message::Binary(data)) => { + if tcp_write.write_all(&data).await.is_err() { + break; + } + } + Ok(Message::Close(_)) => break, + Ok(Message::Ping(_) | Message::Pong(_) | Message::Text(_) | Message::Frame(_)) => {} + Err(err) => { + debug!(error = %err, "TUI WebSocket read error"); + break; + } + } + } + let _ = tcp_write.shutdown().await; +} diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 7992666d3..83b679d86 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -3,6 +3,7 @@ mod app; mod clipboard; +mod edge_tunnel; mod event; pub mod theme; mod ui; @@ -18,7 +19,9 @@ use crossterm::terminal::{ EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, }; use miette::{IntoDiagnostic, Result}; -use openshell_bootstrap::list_gateways_with_source; +use openshell_bootstrap::{ + GatewayMetadata, edge_token::load_edge_token, list_gateways_with_source, +}; use openshell_core::auth::EdgeAuthInterceptor; use openshell_core::metadata::{ObjectId, ObjectLabels, ObjectName}; use openshell_core::proto::SandboxPhase; @@ -494,6 +497,24 @@ async fn handle_gateway_switch(app: &mut App) { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum GatewayChannelMode { + Oidc, + Edge, + Plaintext, + Mtls, +} + +fn gateway_channel_mode(meta: Option<&GatewayMetadata>, endpoint: &str) -> GatewayChannelMode { + match meta.and_then(|m| m.auth_mode.as_deref()) { + Some("oidc") => GatewayChannelMode::Oidc, + Some("cloudflare_jwt") => GatewayChannelMode::Edge, + Some("plaintext") => GatewayChannelMode::Plaintext, + _ if endpoint.starts_with("http://") => GatewayChannelMode::Plaintext, + _ => GatewayChannelMode::Mtls, + } +} + /// Build a gRPC channel and auth interceptor for a gateway. /// /// Checks gateway metadata for the auth mode and loads the appropriate @@ -501,26 +522,64 @@ async fn handle_gateway_switch(app: &mut App) { async fn connect_to_gateway(name: &str, endpoint: &str) -> Result<(Channel, EdgeAuthInterceptor)> { let meta = openshell_bootstrap::get_gateway_metadata(name); - if meta.as_ref().and_then(|m| m.auth_mode.as_deref()) == Some("oidc") { - let bundle = openshell_bootstrap::oidc_token::load_oidc_token(name).ok_or_else(|| { - miette::miette!( - "No OIDC token for gateway '{name}'.\n\ + match gateway_channel_mode(meta.as_ref(), endpoint) { + GatewayChannelMode::Oidc => { + let bundle = + openshell_bootstrap::oidc_token::load_oidc_token(name).ok_or_else(|| { + miette::miette!( + "No OIDC token for gateway '{name}'.\n\ Authenticate with: openshell gateway login" - ) - })?; - if openshell_bootstrap::oidc_token::is_token_expired(&bundle) { - miette::bail!( - "OIDC token for gateway '{name}' has expired.\n\ - Re-authenticate with: openshell gateway login" - ); - } - let interceptor = EdgeAuthInterceptor::new(Some(&bundle.access_token), None)?; - let channel = build_oidc_channel(name, endpoint).await?; - Ok((channel, interceptor)) - } else { - let channel = build_mtls_channel(name, endpoint).await?; - Ok((channel, EdgeAuthInterceptor::noop())) + ) + })?; + if openshell_bootstrap::oidc_token::is_token_expired(&bundle) { + miette::bail!( + "OIDC token for gateway '{name}' has expired.\n\ + Re-authenticate with: openshell gateway login" + ); + } + let interceptor = EdgeAuthInterceptor::new(Some(&bundle.access_token), None)?; + let channel = build_oidc_channel(name, endpoint).await?; + Ok((channel, interceptor)) + } + GatewayChannelMode::Edge => { + let token = load_edge_token(name).ok_or_else(|| { + miette::miette!( + "No edge token for gateway '{name}'.\n\ + Authenticate with: openshell gateway login" + ) + })?; + let interceptor = EdgeAuthInterceptor::new(None, Some(&token))?; + let channel = build_edge_channel(endpoint, &token).await?; + Ok((channel, interceptor)) + } + GatewayChannelMode::Plaintext => { + let channel = build_plaintext_channel(endpoint).await?; + Ok((channel, EdgeAuthInterceptor::noop())) + } + GatewayChannelMode::Mtls => { + let channel = build_mtls_channel(name, endpoint).await?; + Ok((channel, EdgeAuthInterceptor::noop())) + } + } +} + +async fn build_edge_channel(endpoint: &str, token: &str) -> Result { + if endpoint.starts_with("https://") { + let proxy = edge_tunnel::start_tunnel_proxy(endpoint, token).await?; + return build_plaintext_channel(&format!("http://{}", proxy.local_addr)).await; } + build_plaintext_channel(endpoint).await +} + +async fn build_plaintext_channel(endpoint: &str) -> Result { + Endpoint::from_shared(endpoint.to_string()) + .into_diagnostic()? + .connect_timeout(Duration::from_secs(10)) + .http2_keep_alive_interval(Duration::from_secs(10)) + .keep_alive_while_idle(true) + .connect() + .await + .into_diagnostic() } /// Build an HTTPS channel for OIDC-authenticated gateways. @@ -2510,3 +2569,53 @@ fn days_to_ymd(days: i64) -> (i64, i64, i64) { let y = if m <= 2 { y + 1 } else { y }; (y, m, d) } + +#[cfg(test)] +mod tests { + use super::{GatewayChannelMode, gateway_channel_mode}; + use openshell_bootstrap::GatewayMetadata; + + #[test] + fn gateway_channel_mode_uses_plaintext_auth_mode() { + let meta = GatewayMetadata { + auth_mode: Some("plaintext".to_string()), + ..Default::default() + }; + assert_eq!( + gateway_channel_mode(Some(&meta), "https://gateway.example.com"), + GatewayChannelMode::Plaintext + ); + } + + #[test] + fn gateway_channel_mode_prefers_edge_metadata() { + let meta = GatewayMetadata { + auth_mode: Some("cloudflare_jwt".to_string()), + ..Default::default() + }; + assert_eq!( + gateway_channel_mode(Some(&meta), "https://gateway.example.com"), + GatewayChannelMode::Edge + ); + } + + #[test] + fn gateway_channel_mode_uses_http_endpoint_fallback() { + assert_eq!( + gateway_channel_mode(None, "http://127.0.0.1:17670"), + GatewayChannelMode::Plaintext + ); + } + + #[test] + fn gateway_channel_mode_prefers_oidc_metadata() { + let meta = GatewayMetadata { + auth_mode: Some("oidc".to_string()), + ..Default::default() + }; + assert_eq!( + gateway_channel_mode(Some(&meta), "https://gateway.example.com"), + GatewayChannelMode::Oidc + ); + } +} diff --git a/e2e/rust/Cargo.lock b/e2e/rust/Cargo.lock index 953449c57..1869e0a08 100644 --- a/e2e/rust/Cargo.lock +++ b/e2e/rust/Cargo.lock @@ -2,18 +2,166 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "autotools" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941527c41b0fc0dd48511a8154cd5fc7e29200a0ff8b7203c5d777dbc795cf" +dependencies = [ + "cc", +] + +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + [[package]] name = "base64" version = "0.22.1" @@ -78,18 +226,78 @@ dependencies = [ "serde_repr", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -109,6 +317,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "digest" version = "0.10.7" @@ -130,6 +344,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.16.0" @@ -158,6 +378,24 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -173,6 +411,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.32" @@ -180,6 +424,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -188,6 +433,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + [[package]] name = "futures-macro" version = "0.3.32" @@ -218,8 +469,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -234,6 +488,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -241,9 +508,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -259,6 +528,31 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -341,6 +635,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -367,19 +662,52 @@ dependencies = [ "winapi", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -402,6 +730,30 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -523,6 +875,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "itertools" version = "0.14.0" @@ -538,6 +902,27 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -577,6 +962,18 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.0" @@ -584,27 +981,125 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "mio" -version = "1.1.1" +name = "miette" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", ] [[package]] -name = "once_cell" -version = "1.21.3" +name = "miette-derive" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "openshell-e2e" -version = "0.1.0" -dependencies = [ - "base64", +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openshell-core" +version = "0.0.0" +dependencies = [ + "base64", + "chrono", + "ipnet", + "miette", + "prost 0.14.4", + "prost-types", + "protobuf-src", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tracing", + "url", +] + +[[package]] +name = "openshell-e2e" +version = "0.1.0" +dependencies = [ + "base64", "bollard", "bytes", "futures-util", @@ -612,15 +1107,32 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "prost", + "openshell-core", + "prost 0.13.5", "rand", + "rcgen", + "rustls", "serde_json", "sha1", "sha2", "tempfile", "tokio", + "tokio-stream", + "tonic", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + [[package]] name = "parking_lot" version = "0.12.5" @@ -644,12 +1156,53 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -665,6 +1218,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -700,7 +1259,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive 0.14.4", +] + +[[package]] +name = "prost-build" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost 0.14.4", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn", + "tempfile", ] [[package]] @@ -716,6 +1306,112 @@ dependencies = [ "syn", ] +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost 0.14.4", +] + +[[package]] +name = "protobuf-src" +version = "1.1.0+21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7ac8852baeb3cc6fb83b93646fb93c0ffe5d14bf138c945ceb4b9948ee0e3c1" +dependencies = [ + "autotools", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" +dependencies = [ + "pulldown-cmark", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -766,6 +1462,19 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -775,6 +1484,102 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustix" version = "1.1.4" @@ -788,18 +1593,106 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -894,6 +1787,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -932,6 +1831,33 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + [[package]] name = "syn" version = "2.0.117" @@ -943,6 +1869,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -967,6 +1902,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -987,6 +1942,25 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + [[package]] name = "tinystr" version = "0.8.3" @@ -997,6 +1971,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.50.0" @@ -1025,6 +2014,27 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -1038,6 +2048,119 @@ dependencies = [ "tokio", ] +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "rustls-native-certs", + "socket2", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost 0.14.4", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", + "tempfile", + "tonic-build", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -1051,9 +2174,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -1075,18 +2210,48 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -1144,6 +2309,61 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -1178,6 +2398,35 @@ dependencies = [ "semver", ] +[[package]] +name = "web-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1200,19 +2449,81 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -1224,6 +2535,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -1231,58 +2558,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" @@ -1383,6 +2758,15 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.3" @@ -1447,6 +2831,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/e2e/rust/Cargo.toml b/e2e/rust/Cargo.toml index 083c622df..c115b2fa6 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -111,13 +111,18 @@ http-body-util = "0.1" hyper = { version = "1", features = ["client", "http1"] } hyper-util = { version = "0.1", features = ["tokio"] } prost = "0.13" +rcgen = "0.13" +rustls = { version = "0.23", features = ["ring"] } tokio = { version = "1.43", features = ["full"] } +tokio-stream = { version = "0.1", features = ["net"] } tempfile = "3" sha1 = "0.10" sha2 = "0.10" hex = "0.4" rand = "0.9" serde_json = "1" +tonic = { version = "0.14", features = ["transport"] } +openshell-core = { path = "../../crates/openshell-core" } [lints.rust] unsafe_code = "warn" diff --git a/e2e/rust/src/harness/certs.rs b/e2e/rust/src/harness/certs.rs new file mode 100644 index 000000000..f9c0150ac --- /dev/null +++ b/e2e/rust/src/harness/certs.rs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! TLS certificate generation helpers for e2e tests. +//! +//! Wraps rcgen to produce a CA + server cert (valid for localhost/127.0.0.1) +//! + client cert, all as PEM strings, in a single call. + +pub use rcgen::{Certificate, KeyPair}; +use rcgen::{BasicConstraints, CertificateParams, ExtendedKeyUsagePurpose, IsCa}; + +/// PEM-encoded certificate material generated for one test run. +pub struct TestCerts { + pub ca_cert: Certificate, + pub ca_key: KeyPair, + pub ca_pem: String, + pub server_cert_pem: String, + pub server_key_pem: String, + pub client_cert_pem: String, + pub client_key_pem: String, +} + +/// Generate a fresh CA, server cert, and client cert. +/// +/// Server cert is valid for both `localhost` and `127.0.0.1` so the TUI +/// can connect to `https://127.0.0.1:` without a hostname mismatch. +pub fn generate_test_certs() -> TestCerts { + let ca_key = KeyPair::generate().unwrap(); + let mut ca_params = CertificateParams::new(Vec::::new()).unwrap(); + ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + let ca_cert = ca_params.self_signed(&ca_key).unwrap(); + + let server_key = KeyPair::generate().unwrap(); + let mut server_params = + CertificateParams::new(vec!["localhost".to_string(), "127.0.0.1".to_string()]).unwrap(); + server_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; + let server_cert = server_params.signed_by(&server_key, &ca_cert, &ca_key).unwrap(); + + let client_key = KeyPair::generate().unwrap(); + let mut client_params = CertificateParams::new(Vec::::new()).unwrap(); + client_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth]; + let client_cert = client_params.signed_by(&client_key, &ca_cert, &ca_key).unwrap(); + + TestCerts { + ca_pem: ca_cert.pem(), + server_cert_pem: server_cert.pem(), + server_key_pem: server_key.serialize_pem(), + client_cert_pem: client_cert.pem(), + client_key_pem: client_key.serialize_pem(), + ca_cert, + ca_key, + } +} + +/// Install the rustls ring crypto provider as the process default. +/// +/// Must be called before any in-process TLS. Safe to call multiple times. +pub fn install_rustls_provider() { + let _ = rustls::crypto::ring::default_provider().install_default(); +} diff --git a/e2e/rust/src/harness/mock_gateway.rs b/e2e/rust/src/harness/mock_gateway.rs new file mode 100644 index 000000000..b54e83a5b --- /dev/null +++ b/e2e/rust/src/harness/mock_gateway.rs @@ -0,0 +1,580 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Minimal mock gRPC gateway for TUI tests. +//! +//! The TUI's gateway-switch flow needs a reachable gRPC endpoint to prove it +//! connected to the right server — but it doesn't need any real business logic. +//! [`MockGateway`] implements the full [`OpenShell`] trait with stubs so that +//! tonic is satisfied, while only [`list_providers`](OpenShell::list_providers) +//! does meaningful work (counting calls so tests can assert connectivity). +//! +//! Use [`start_gateway`] to bind a random loopback port and get a +//! [`RunningGateway`] handle whose `provider_calls` counter you can check. + +use std::pin::Pin; +use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, +}; + +use openshell_core::proto::{ + self, + open_shell_server::{OpenShell, OpenShellServer}, +}; +use tokio::net::TcpListener; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; +use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream}; +use tonic::transport::{Certificate as TonicCert, Identity, Server, ServerTlsConfig}; +use tonic::{Response, Status}; + +#[derive(Clone)] +pub struct MockGateway { + provider_name: String, + provider_calls: Arc, + required_edge_token: Option, + required_bearer_token: Option, +} + +impl MockGateway { + fn provider(&self) -> proto::Provider { + proto::Provider { + metadata: Some(proto::datamodel::v1::ObjectMeta { + id: format!("id-{}", self.provider_name), + name: self.provider_name.clone(), + ..Default::default() + }), + r#type: "mock".to_string(), + ..Default::default() + } + } + + fn check_auth(&self, request: &tonic::Request) -> Result<(), Status> { + if let Some(ref expected) = self.required_edge_token { + match request.metadata().get("cf-access-jwt-assertion") { + Some(v) if v.to_str().unwrap_or("") == expected => {} + _ => return Err(Status::unauthenticated("missing or invalid edge token")), + } + } + if let Some(ref expected) = self.required_bearer_token { + let expected_value = format!("Bearer {expected}"); + match request.metadata().get("authorization") { + Some(v) if v.to_str().unwrap_or("") == expected_value => {} + _ => return Err(Status::unauthenticated("missing or invalid bearer token")), + } + } + Ok(()) + } +} + +fn empty_stream() -> ReceiverStream> { + let (_tx, rx) = mpsc::channel(1); + ReceiverStream::new(rx) +} + +fn empty_box_stream() -> Pin> + Send>> +where + T: Send + 'static, +{ + Box::pin(tokio_stream::empty()) +} + +#[tonic::async_trait] +impl OpenShell for MockGateway { + type WatchSandboxStream = ReceiverStream>; + type ExecSandboxStream = ReceiverStream>; + type ExecSandboxInteractiveStream = ReceiverStream>; + type ConnectSupervisorStream = ReceiverStream>; + type RelayStreamStream = ReceiverStream>; + type ForwardTcpStream = + Pin> + Send>>; + + async fn health( + &self, + request: tonic::Request, + ) -> Result, Status> { + self.check_auth(&request)?; + Ok(Response::new(proto::HealthResponse { + status: proto::ServiceStatus::Healthy.into(), + version: "test".to_string(), + })) + } + + async fn create_sandbox( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(proto::SandboxResponse::default())) + } + + async fn get_sandbox( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(proto::SandboxResponse::default())) + } + + async fn list_sandboxes( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(proto::ListSandboxesResponse::default())) + } + + async fn list_sandbox_providers( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(proto::ListSandboxProvidersResponse::default())) + } + + async fn attach_sandbox_provider( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new( + proto::AttachSandboxProviderResponse::default(), + )) + } + + async fn detach_sandbox_provider( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new( + proto::DetachSandboxProviderResponse::default(), + )) + } + + async fn delete_sandbox( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(proto::DeleteSandboxResponse { + deleted: true, + })) + } + + async fn get_sandbox_config( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(proto::GetSandboxConfigResponse::default())) + } + + async fn get_gateway_config( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(proto::GetGatewayConfigResponse { + settings: std::collections::HashMap::default(), + settings_revision: 1, + })) + } + + async fn get_sandbox_provider_environment( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new( + proto::GetSandboxProviderEnvironmentResponse::default(), + )) + } + + async fn create_ssh_session( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(proto::CreateSshSessionResponse::default())) + } + + async fn expose_service( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(proto::ServiceEndpointResponse::default())) + } + + async fn get_service( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn list_services( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn delete_service( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn revoke_ssh_session( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(proto::RevokeSshSessionResponse::default())) + } + + async fn create_provider( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn get_provider( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn list_providers( + &self, + request: tonic::Request, + ) -> Result, Status> { + self.check_auth(&request)?; + self.provider_calls.fetch_add(1, Ordering::SeqCst); + Ok(Response::new(proto::ListProvidersResponse { + providers: vec![self.provider()], + })) + } + + async fn list_provider_profiles( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn get_provider_profile( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn import_provider_profiles( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn lint_provider_profiles( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn delete_provider_profile( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn update_provider( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn get_provider_refresh_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn configure_provider_refresh( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn rotate_provider_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn delete_provider_refresh( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn delete_provider( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn watch_sandbox( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(empty_stream())) + } + + async fn exec_sandbox( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(empty_stream())) + } + + async fn exec_sandbox_interactive( + &self, + _request: tonic::Request>, + ) -> Result, Status> { + Ok(Response::new(empty_stream())) + } + + async fn update_config( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn get_sandbox_policy_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn list_sandbox_policies( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(proto::ListSandboxPoliciesResponse::default())) + } + + async fn report_policy_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn get_sandbox_logs( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn push_sandbox_logs( + &self, + _request: tonic::Request>, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn submit_policy_analysis( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn get_draft_policy( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(proto::GetDraftPolicyResponse::default())) + } + + async fn approve_draft_chunk( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn reject_draft_chunk( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn approve_all_draft_chunks( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn edit_draft_chunk( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn undo_draft_chunk( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn clear_draft_chunks( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn get_draft_history( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn issue_sandbox_token( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn refresh_sandbox_token( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn connect_supervisor( + &self, + _request: tonic::Request>, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn relay_stream( + &self, + _request: tonic::Request>, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn forward_tcp( + &self, + _request: tonic::Request>, + ) -> Result, Status> { + Ok(Response::new(empty_box_stream())) + } +} + +pub struct RunningGateway { + pub endpoint: String, + pub provider_calls: Arc, + pub task: JoinHandle<()>, +} + +pub async fn start_gateway(provider_name: &str) -> RunningGateway { + spawn_gateway(provider_name, None, None, None).await +} + +pub async fn start_gateway_with_edge_token( + provider_name: &str, + required_token: &str, +) -> RunningGateway { + spawn_gateway( + provider_name, + Some(required_token.to_string()), + None, + None, + ) + .await +} + +/// Start a TLS-only mock gateway (no client cert required) that validates +/// a Bearer token in every request. Used for OIDC gateway tests. +pub async fn start_gateway_with_tls_and_bearer( + provider_name: &str, + server_cert_pem: &str, + server_key_pem: &str, + required_bearer_token: &str, +) -> RunningGateway { + let tls = ServerTlsConfig::new().identity(Identity::from_pem(server_cert_pem, server_key_pem)); + spawn_gateway( + provider_name, + None, + Some(required_bearer_token.to_string()), + Some(tls), + ) + .await +} + +/// Start a mutual-TLS mock gateway that requires a client certificate signed +/// by the given CA. Used for mTLS gateway tests. +pub async fn start_gateway_with_mtls( + provider_name: &str, + ca_pem: &str, + server_cert_pem: &str, + server_key_pem: &str, +) -> RunningGateway { + let tls = ServerTlsConfig::new() + .identity(Identity::from_pem(server_cert_pem, server_key_pem)) + .client_ca_root(TonicCert::from_pem(ca_pem)); + spawn_gateway(provider_name, None, None, Some(tls)).await +} + +async fn spawn_gateway( + provider_name: &str, + required_edge_token: Option, + required_bearer_token: Option, + tls: Option, +) -> RunningGateway { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind test gateway"); + let addr = listener.local_addr().expect("read test gateway addr"); + let provider_calls = Arc::new(AtomicUsize::new(0)); + let service = MockGateway { + provider_name: provider_name.to_string(), + provider_calls: Arc::clone(&provider_calls), + required_edge_token, + required_bearer_token, + }; + let use_tls = tls.is_some(); + let task = tokio::spawn(async move { + let mut builder = Server::builder(); + if let Some(tls_config) = tls { + builder = builder.tls_config(tls_config).unwrap(); + } + builder + .add_service(OpenShellServer::new(service)) + .serve_with_incoming(TcpListenerStream::new(listener)) + .await + .expect("serve test gateway"); + }); + RunningGateway { + endpoint: format!("{}://{addr}", if use_tls { "https" } else { "http" }), + provider_calls, + task, + } +} diff --git a/e2e/rust/src/harness/mod.rs b/e2e/rust/src/harness/mod.rs index f2dfd5ec9..53b88695d 100644 --- a/e2e/rust/src/harness/mod.rs +++ b/e2e/rust/src/harness/mod.rs @@ -10,3 +10,6 @@ pub mod gateway; pub mod output; pub mod port; pub mod sandbox; + +pub mod mock_gateway; +pub mod certs; \ No newline at end of file diff --git a/e2e/rust/tests/tui_gateway_auth.rs b/e2e/rust/tests/tui_gateway_auth.rs new file mode 100644 index 000000000..8c97ff4f9 --- /dev/null +++ b/e2e/rust/tests/tui_gateway_auth.rs @@ -0,0 +1,303 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(feature = "e2e")] + +use std::path::Path; +use std::process::Stdio; +use std::sync::atomic::Ordering; +use std::time::Duration; + +use openshell_e2e::harness::binary::openshell_tty_cmd; +use openshell_e2e::harness::certs::{generate_test_certs, install_rustls_provider}; +use openshell_e2e::harness::mock_gateway::{ + start_gateway, start_gateway_with_edge_token, start_gateway_with_mtls, + start_gateway_with_tls_and_bearer, +}; +use openshell_e2e::harness::output::strip_ansi; +use tokio::time::{sleep, timeout}; + +fn normalize_output(output: &str) -> String { + let stripped = strip_ansi(output).replace('\r', ""); + let mut cleaned = String::with_capacity(stripped.len()); + + for ch in stripped.chars() { + match ch { + '\u{8}' => { + cleaned.pop(); + } + '\u{4}' => {} + _ => cleaned.push(ch), + } + } + + cleaned +} + +#[derive(Clone)] +struct GatewayConfig<'a> { + name: &'a str, + endpoint: &'a str, + auth_mode: Option<&'a str>, + edge_token: Option<&'a str>, +} + +fn seed_gateway_config(config_dir: &Path, active_gateway: &str, gateways: &[GatewayConfig<'_>]) { + let openshell_dir = config_dir.join("openshell"); + let gateways_root = openshell_dir.join("gateways"); + std::fs::create_dir_all(&gateways_root).expect("create gateways dir"); + std::fs::write(openshell_dir.join("active_gateway"), active_gateway) + .expect("write active gateway"); + + for gateway in gateways { + let gateway_path = gateways_root.join(gateway.name); + std::fs::create_dir_all(&gateway_path).expect("create gateway dir"); + let mut metadata = serde_json::json!({ + "name": gateway.name, + "gateway_endpoint": gateway.endpoint, + "is_remote": false, + "gateway_port": 0, + }); + if let Some(auth_mode) = gateway.auth_mode { + metadata["auth_mode"] = serde_json::Value::from(auth_mode); + } + std::fs::write( + gateway_path.join("metadata.json"), + serde_json::to_vec_pretty(&metadata).expect("serialize metadata"), + ) + .expect("write metadata"); + + if let Some(token) = gateway.edge_token { + std::fs::write(gateway_path.join("edge_token"), token).expect("write edge token"); + } + } +} +/// Write mTLS material into the gateway's mtls/ subdirectory. +/// `client_cert_pem`/`client_key_pem` are `None` for OIDC (server-TLS only). +fn write_gateway_certs( + config_dir: &Path, + gateway_name: &str, + ca_pem: &str, + client_cert_pem: Option<&str>, + client_key_pem: Option<&str>, +) { + let mtls_dir = config_dir + .join("openshell") + .join("gateways") + .join(gateway_name) + .join("mtls"); + std::fs::create_dir_all(&mtls_dir).unwrap(); + std::fs::write(mtls_dir.join("ca.crt"), ca_pem).unwrap(); + if let (Some(cert), Some(key)) = (client_cert_pem, client_key_pem) { + std::fs::write(mtls_dir.join("tls.crt"), cert).unwrap(); + std::fs::write(mtls_dir.join("tls.key"), key).unwrap(); + } +} + +/// Write an OIDC token JSON file for the gateway. +fn write_oidc_token(config_dir: &Path, gateway_name: &str, access_token: &str) { + let gateway_dir = config_dir.join("openshell").join("gateways").join(gateway_name); + std::fs::create_dir_all(&gateway_dir).unwrap(); + let token = serde_json::json!({ + "access_token": access_token, + "issuer": "https://test.example.com", + "client_id": "test-client", + "expires_at": 9_999_999_999u64, + }); + std::fs::write( + gateway_dir.join("oidc_token.json"), + serde_json::to_vec_pretty(&token).unwrap(), + ) + .unwrap(); +} + +async fn run_tui_connect(config_dir: &Path, gateway_name: &str) -> String { + let mut cmd = openshell_tty_cmd(&["--gateway", gateway_name, "term", "--theme", "dark"]); + cmd.env("XDG_CONFIG_HOME", config_dir) + .env("HOME", config_dir) + .env("TERM", "xterm-256color") + .env("COLUMNS", "140") + .env("LINES", "40") + .env_remove("OPENSHELL_GATEWAY") + .env_remove("OPENSHELL_GATEWAY_ENDPOINT") + // stdin kept piped so script does not see EOF and close the PTY + // master prematurely before the TUI has connected. + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let mut child = cmd.spawn().expect("spawn openshell term"); + + // The TUI calls refresh_data (health + providers + sandboxes) on startup + // before entering the event loop. A few seconds is ample for a loopback + // gRPC connection to complete that sequence. + sleep(Duration::from_secs(3)).await; + + // Kill rather than sending `q`: crossterm's event::poll is a blocking + // call that starves other tasks on a single-thread runtime, so the event + // loop may never process the keystroke. We only need proof of connection + // (provider_calls > 0), not graceful exit. + child.kill().await.ok(); + + let output = timeout(Duration::from_secs(5), child.wait_with_output()) + .await + .expect("openshell term should exit after kill") + .expect("collect openshell term output"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let combined = format!("{stdout}{stderr}"); + normalize_output(&combined) +} + +#[tokio::test] +async fn tui_connects_to_plaintext_gateway() { + let gw = start_gateway("provider-plaintext").await; + + let tmpdir = tempfile::tempdir().expect("create temp config dir"); + seed_gateway_config( + tmpdir.path(), + "my-gw", + &[GatewayConfig { + name: "my-gw", + endpoint: &gw.endpoint, + auth_mode: Some("plaintext"), + edge_token: None, + }], + ); + + let output = run_tui_connect(tmpdir.path(), "my-gw").await; + + gw.task.abort(); + + assert!( + gw.provider_calls.load(Ordering::SeqCst) > 0, + "TUI should query providers from the plaintext gateway:\n{output}" + ); +} + +#[tokio::test] +async fn tui_connects_to_edge_gateway_with_token() { + let gw = start_gateway_with_edge_token("provider-edge", "dummy-edge-token").await; + + let tmpdir = tempfile::tempdir().expect("create temp config dir"); + seed_gateway_config( + tmpdir.path(), + "my-gw", + &[GatewayConfig { + name: "my-gw", + endpoint: &gw.endpoint, + auth_mode: Some("cloudflare_jwt"), + edge_token: Some("dummy-edge-token"), + }], + ); + + let output = run_tui_connect(tmpdir.path(), "my-gw").await; + + gw.task.abort(); + + assert!( + gw.provider_calls.load(Ordering::SeqCst) > 0, + "TUI should query providers from the edge-auth gateway:\n{output}" + ); +} + +#[tokio::test] +async fn tui_connects_to_http_endpoint_without_auth_mode() { + let gw = start_gateway("provider-http").await; + + let tmpdir = tempfile::tempdir().expect("create temp config dir"); + seed_gateway_config( + tmpdir.path(), + "my-gw", + &[GatewayConfig { + name: "my-gw", + endpoint: &gw.endpoint, + auth_mode: None, + edge_token: None, + }], + ); + + let output = run_tui_connect(tmpdir.path(), "my-gw").await; + + gw.task.abort(); + + assert!( + gw.provider_calls.load(Ordering::SeqCst) > 0, + "TUI should query providers from the http-endpoint gateway:\n{output}" + ); +} + +#[tokio::test] +async fn tui_connects_to_oidc_gateway() { + install_rustls_provider(); + let certs = generate_test_certs(); + let gw = start_gateway_with_tls_and_bearer( + "provider-oidc", + &certs.server_cert_pem, + &certs.server_key_pem, + "dummy-oidc-token", + ) + .await; + + let tmpdir = tempfile::tempdir().expect("create temp config dir"); + seed_gateway_config( + tmpdir.path(), + "my-gw", + &[GatewayConfig { + name: "my-gw", + endpoint: &gw.endpoint, + auth_mode: Some("oidc"), + edge_token: None, + }], + ); + write_gateway_certs(tmpdir.path(), "my-gw", &certs.ca_pem, None, None); + write_oidc_token(tmpdir.path(), "my-gw", "dummy-oidc-token"); + + let output = run_tui_connect(tmpdir.path(), "my-gw").await; + gw.task.abort(); + + assert!( + gw.provider_calls.load(Ordering::SeqCst) > 0, + "TUI should authenticate and query providers from the OIDC gateway:\n{output}" + ); +} + +#[tokio::test] +async fn tui_connects_to_mtls_gateway() { + install_rustls_provider(); + let certs = generate_test_certs(); + let gw = start_gateway_with_mtls( + "provider-mtls", + &certs.ca_pem, + &certs.server_cert_pem, + &certs.server_key_pem, + ) + .await; + + let tmpdir = tempfile::tempdir().expect("create temp config dir"); + seed_gateway_config( + tmpdir.path(), + "my-gw", + &[GatewayConfig { + name: "my-gw", + endpoint: &gw.endpoint, + auth_mode: None, + edge_token: None, + }], + ); + write_gateway_certs( + tmpdir.path(), + "my-gw", + &certs.ca_pem, + Some(&certs.client_cert_pem), + Some(&certs.client_key_pem), + ); + + let output = run_tui_connect(tmpdir.path(), "my-gw").await; + gw.task.abort(); + + assert!( + gw.provider_calls.load(Ordering::SeqCst) > 0, + "TUI should complete mTLS handshake and query providers:\n{output}" + ); +}