Move from native-tls to rustls in the remote client (mirrors the web server) (#4972)

* native-tls -> rustls

* add/update tests

* rustfmt

* add pr
This commit is contained in:
Aram Drevekenin
2026-03-31 20:19:50 +02:00
committed by GitHub
parent 50c029afaf
commit 7c68623580
9 changed files with 685 additions and 154 deletions
+1
View File
@@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* feat: add --layout-string to allow for in-line layouts, add --tab-id to all new-pane CLI commands (https://github.com/zellij-org/zellij/pull/4956)
* fix: proper error on socket paths that are too long (https://github.com/zellij-org/zellij/pull/4957)
* fix: scrollback with partial scroll region (eg. Codex bug) (https://github.com/zellij-org/zellij/pull/4941)
* fix: remote attach issues on macOS (https://github.com/zellij-org/zellij/pull/4972)
## [0.44.0] - 2026-03-23
* fix: meta key handling in web client (https://github.com/zellij-org/zellij/pull/4376)
Generated
+84 -98
View File
@@ -331,9 +331,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.5.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block-buffer"
@@ -623,9 +623,9 @@ dependencies = [
[[package]]
name = "core-foundation"
version = "0.9.3"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
@@ -720,7 +720,7 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
"crossterm_winapi",
"mio 1.0.3",
"parking_lot 0.12.1",
@@ -767,7 +767,7 @@ checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
dependencies = [
"curl-sys",
"libc",
"openssl-probe",
"openssl-probe 0.1.5",
"openssl-sys",
"schannel",
"socket2 0.4.9",
@@ -1084,21 +1084,6 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@@ -1974,7 +1959,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
"libc",
"redox_syscall 0.5.13",
]
@@ -2223,23 +2208,6 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nix"
version = "0.23.1"
@@ -2269,7 +2237,7 @@ version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
"crossbeam-channel",
"filetime",
"fsevent-sys",
@@ -2344,38 +2312,18 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl"
version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
"bitflags 2.5.0",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-src"
version = "300.5.4+3.5.4"
@@ -2470,6 +2418,16 @@ dependencies = [
"windows-targets 0.48.0",
]
[[package]]
name = "pem"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
dependencies = [
"base64 0.22.1",
"serde_core",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -2743,6 +2701,19 @@ dependencies = [
"zerocopy 0.8.17",
]
[[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 = "recvmsg"
version = "1.0.0"
@@ -2773,7 +2744,7 @@ version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
]
[[package]]
@@ -2824,7 +2795,7 @@ version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
@@ -2867,7 +2838,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys 0.4.15",
@@ -2880,7 +2851,7 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys 0.11.0",
@@ -2904,6 +2875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"rustls-pki-types",
"rustls-webpki",
@@ -2911,6 +2883,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
"openssl-probe 0.2.1",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
@@ -2994,11 +2978,11 @@ checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21"
[[package]]
name = "security-framework"
version = "2.10.0"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.11.0",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -3007,9 +2991,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.11.0"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
@@ -3529,7 +3513,7 @@ version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b858526d22750088a9b3cf2e3c2aacebd5377f13adeec02860c30d09113010a6"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
"cap-fs-ext",
"cap-std",
"fd-lock",
@@ -3722,16 +3706,6 @@ dependencies = [
"syn 2.0.96",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.2"
@@ -3761,9 +3735,7 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
dependencies = [
"futures-util",
"log",
"native-tls",
"tokio",
"tokio-native-tls",
"tungstenite 0.20.1",
]
@@ -3863,7 +3835,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
"bytes",
"http 1.3.1",
"pin-project-lite",
@@ -3937,7 +3909,6 @@ dependencies = [
"http 0.2.9",
"httparse",
"log",
"native-tls",
"rand 0.8.5",
"sha1",
"thiserror 1.0.61",
@@ -4187,7 +4158,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82735a180c58e86727965ca39c0390d21c86a354f03cf7a3fc73f1de4c01ae1c"
dependencies = [
"anyhow",
"bitflags 2.5.0",
"bitflags 2.11.0",
"cap-fs-ext",
"cap-rand",
"cap-std",
@@ -4326,7 +4297,7 @@ version = "0.228.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
"indexmap 2.7.1",
]
@@ -4336,7 +4307,7 @@ version = "0.239.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
"indexmap 2.7.1",
]
@@ -4405,7 +4376,7 @@ checksum = "b61d3bcb6a981233e8b774738f4495ad2c724a0d73ac626f373944e9ba1e3101"
dependencies = [
"anyhow",
"async-trait",
"bitflags 2.5.0",
"bitflags 2.11.0",
"thiserror 2.0.12",
"tracing",
"wiggle-macro",
@@ -4700,7 +4671,7 @@ version = "0.36.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
"windows-sys 0.52.0",
]
@@ -4710,7 +4681,7 @@ version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.11.0",
]
[[package]]
@@ -4783,6 +4754,15 @@ dependencies = [
"linked-hash-map",
]
[[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.1"
@@ -4853,18 +4833,24 @@ dependencies = [
"isahc",
"lazy_static",
"log",
"native-tls",
"nix",
"notify",
"num_cpus",
"rcgen",
"regex",
"rustls",
"rustls-native-certs",
"rustls-pemfile",
"rustls-pki-types",
"serde",
"serde_json",
"serde_yaml",
"serial_test",
"signal-hook",
"tempfile",
"time",
"tokio",
"tokio-rustls",
"tokio-tungstenite 0.20.1",
"tokio-util",
"tower-http",
@@ -4951,7 +4937,7 @@ version = "0.44.0"
dependencies = [
"anyhow",
"backtrace",
"bitflags 2.5.0",
"bitflags 2.11.0",
"clap",
"clap_complete",
"colored",
+8 -2
View File
@@ -39,8 +39,12 @@ zellij-utils = { workspace = true }
tokio = { workspace = true }
tokio-util = { workspace = true }
isahc = { workspace = true }
tokio-tungstenite = { version = "0.20", features = ["native-tls"] } #TODO: see about this native-tls...
native-tls = "0.2"
tokio-tungstenite = "0.20"
rustls = { version = "0.23", default-features = false, features = ["std"] }
tokio-rustls = "0.26"
rustls-pemfile = "2"
rustls-native-certs = "0.8"
rustls-pki-types = "1"
futures-util = "0.3"
urlencoding = "2.1"
@@ -61,8 +65,10 @@ windows-sys = { workspace = true, features = [
[dev-dependencies]
insta = "1.6.0"
rcgen = "0.13"
serde_json = "1.0"
serial_test = "3.0"
tempfile = "3"
[features]
unstable = []
+2 -2
View File
@@ -1,7 +1,7 @@
mod auth;
mod config;
mod http_client;
mod websockets;
pub mod http_client;
pub mod websockets;
#[cfg(test)]
mod unit;
@@ -59,11 +59,11 @@ mod mock_server {
}
#[derive(Deserialize)]
struct LoginRequest {
auth_token: String,
pub struct LoginRequest {
pub auth_token: String,
}
async fn handle_login(
pub async fn handle_login(
State(state): State<MockRemoteServerState>,
jar: CookieJar,
Json(payload): Json<LoginRequest>,
@@ -101,7 +101,7 @@ mod mock_server {
))
}
async fn handle_session(
pub async fn handle_session(
State(state): State<MockRemoteServerState>,
jar: CookieJar,
) -> Result<Json<serde_json::Value>, StatusCode> {
@@ -124,7 +124,7 @@ mod mock_server {
})))
}
async fn handle_ws_terminal(
pub async fn handle_ws_terminal(
ws: axum::extract::ws::WebSocketUpgrade,
State(state): State<MockRemoteServerState>,
jar: CookieJar,
@@ -157,7 +157,7 @@ mod mock_server {
}))
}
async fn handle_ws_control(
pub async fn handle_ws_control(
ws: axum::extract::ws::WebSocketUpgrade,
State(state): State<MockRemoteServerState>,
jar: CookieJar,
@@ -215,6 +215,112 @@ mod mock_server {
}
}
#[cfg(feature = "web_server_capability")]
mod tls_mock_server {
use super::mock_server::MockRemoteServerState;
use axum::routing::{get, post};
use axum::Router;
use axum_server::tls_rustls::RustlsConfig;
use axum_server::Handle;
use std::path::PathBuf;
use std::time::Duration;
pub struct TlsTestCerts {
pub ca_cert_path: PathBuf,
_ca_cert_file: tempfile::NamedTempFile,
_server_cert_file: tempfile::NamedTempFile,
_server_key_file: tempfile::NamedTempFile,
server_cert_path: PathBuf,
server_key_path: PathBuf,
}
pub fn generate_test_certs() -> TlsTestCerts {
// Create a CA with proper key usage
let ca_key = rcgen::KeyPair::generate().unwrap();
let mut ca_params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
ca_params.key_usages = vec![
rcgen::KeyUsagePurpose::KeyCertSign,
rcgen::KeyUsagePurpose::CrlSign,
];
let ca = ca_params.self_signed(&ca_key).unwrap();
// Create server cert with IP SAN only (no DNS name for IP addresses)
let mut server_params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
server_params.subject_alt_names = vec![rcgen::SanType::IpAddress(std::net::IpAddr::V4(
std::net::Ipv4Addr::LOCALHOST,
))];
server_params.extended_key_usages = vec![rcgen::ExtendedKeyUsagePurpose::ServerAuth];
let server_key = rcgen::KeyPair::generate().unwrap();
let server_cert = server_params.signed_by(&server_key, &ca, &ca_key).unwrap();
// Write to temp files
let ca_cert_file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(ca_cert_file.path(), ca.pem()).unwrap();
let server_cert_file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(server_cert_file.path(), server_cert.pem()).unwrap();
let server_key_file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(server_key_file.path(), server_key.serialize_pem()).unwrap();
TlsTestCerts {
ca_cert_path: ca_cert_file.path().to_path_buf(),
server_cert_path: server_cert_file.path().to_path_buf(),
server_key_path: server_key_file.path().to_path_buf(),
_ca_cert_file: ca_cert_file,
_server_cert_file: server_cert_file,
_server_key_file: server_key_file,
}
}
pub async fn start_tls_mock_server(
state: MockRemoteServerState,
certs: &TlsTestCerts,
) -> (u16, Handle, tokio::task::JoinHandle<()>) {
let app = Router::new()
.route("/command/login", post(super::mock_server::handle_login))
.route("/session", post(super::mock_server::handle_session))
.route("/ws/terminal", get(super::mock_server::handle_ws_terminal))
.route(
"/ws/terminal/{session_name}",
get(super::mock_server::handle_ws_terminal),
)
.route("/ws/control", get(super::mock_server::handle_ws_control))
.with_state(state);
let rustls_config =
RustlsConfig::from_pem_file(&certs.server_cert_path, &certs.server_key_path)
.await
.expect("Failed to load test TLS config");
let listener =
std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind test TLS server");
let port = listener.local_addr().unwrap().port();
let handle = Handle::new();
let server_handle = handle.clone();
let server_task = tokio::spawn(async move {
axum_server::from_tcp_rustls(listener, rustls_config)
.handle(server_handle)
.serve(app.into_make_service())
.await
.unwrap();
});
// Wait for the server to be listening (deterministic, no sleep)
handle.listening().await;
(port, handle, server_task)
}
pub async fn shutdown_server(handle: Handle, server_task: tokio::task::JoinHandle<()>) {
handle.graceful_shutdown(Some(Duration::from_secs(1)));
let _ = server_task.await;
}
}
// Database test helpers
fn setup_test_db(server_url: &str) {
let _ = remote_session_tokens::delete_session_token(server_url);
@@ -658,6 +764,172 @@ mod tests {
RemoteClientError::UrlParseError(_)
));
}
// -- TLS tests ------------------------------------------------------------
//
// These tests exercise the rustls WebSocket TLS code paths added by the
// native-tls → rustls migration. They call establish_websocket_connections
// directly rather than going through attach_to_remote_session, because:
//
// 1. The HTTP auth step (isahc/curl) uses a separate TLS stack that was
// not changed by this migration — it is tested by the non-TLS tests above.
// 2. attach_to_remote_session opens the SQLite session-token database,
// which can cause I/O contention with web_client tests that use a
// different SQLite database in the same directory.
//
// Each test seeds a session directly in the mock server state and
// pre-populates the HTTP client cookie, then connects over wss://.
/// Helper: create an HTTP client with a pre-seeded session cookie and
/// register the session in the mock server state. Returns (web_client_id,
/// http_client).
fn seed_mock_session(
server_state: &MockRemoteServerState,
) -> (
String,
crate::remote_attach::http_client::HttpClientWithCookies,
) {
let session_token = uuid::Uuid::new_v4().to_string();
let web_client_id = uuid::Uuid::new_v4().to_string();
server_state
.session_tokens
.lock()
.unwrap()
.insert(session_token.clone(), web_client_id.clone());
// The HTTP client is only used for its cookie jar (WebSocket upgrade
// sends the session cookie). TLS for this client is irrelevant since
// it never makes HTTP requests in these tests.
let http_client =
crate::remote_attach::http_client::HttpClientWithCookies::new(None, true).unwrap();
http_client.set_cookie("session_token".to_string(), session_token);
(web_client_id, http_client)
}
#[tokio::test]
#[serial]
async fn test_tls_insecure_mode() {
use crate::remote_attach::websockets;
let certs = tls_mock_server::generate_test_certs();
let server_state = MockRemoteServerState::new();
let (port, handle, server_task) =
tls_mock_server::start_tls_mock_server(server_state.clone(), &certs).await;
let (web_client_id, http_client) = seed_mock_session(&server_state);
let server_base_url = format!("https://127.0.0.1:{}", port);
let result = tokio::time::timeout(
Duration::from_secs(10),
websockets::establish_websocket_connections(
&web_client_id,
&http_client,
&server_base_url,
"test-session",
None,
true, // insecure — exercises NoVerifier
),
)
.await
.expect("Test timed out");
assert!(
result.is_ok(),
"TLS insecure mode should connect successfully: {:?}",
result.err()
);
let connections = result.unwrap();
assert!(!connections.web_client_id.is_empty());
let endpoints = server_state.get_endpoints_called();
assert!(endpoints.contains(&"/ws/terminal".to_string()));
assert!(endpoints.contains(&"/ws/control".to_string()));
tls_mock_server::shutdown_server(handle, server_task).await;
}
#[tokio::test]
#[serial]
async fn test_tls_ca_cert_mode() {
use crate::remote_attach::websockets;
let certs = tls_mock_server::generate_test_certs();
let server_state = MockRemoteServerState::new();
let (port, handle, server_task) =
tls_mock_server::start_tls_mock_server(server_state.clone(), &certs).await;
let (web_client_id, http_client) = seed_mock_session(&server_state);
let server_base_url = format!("https://127.0.0.1:{}", port);
let result = tokio::time::timeout(
Duration::from_secs(10),
websockets::establish_websocket_connections(
&web_client_id,
&http_client,
&server_base_url,
"test-session",
Some(certs.ca_cert_path.as_path()),
false, // not insecure — verify against CA cert
),
)
.await
.expect("Test timed out");
assert!(
result.is_ok(),
"WebSocket TLS with CA cert should connect successfully: {:?}",
result.err()
);
let connections = result.unwrap();
assert!(!connections.web_client_id.is_empty());
let endpoints = server_state.get_endpoints_called();
assert!(endpoints.contains(&"/ws/terminal".to_string()));
assert!(endpoints.contains(&"/ws/control".to_string()));
tls_mock_server::shutdown_server(handle, server_task).await;
}
#[tokio::test]
#[serial]
async fn test_tls_rejects_untrusted_cert() {
use crate::remote_attach::websockets;
let certs = tls_mock_server::generate_test_certs();
let server_state = MockRemoteServerState::new();
let (port, handle, server_task) =
tls_mock_server::start_tls_mock_server(server_state.clone(), &certs).await;
let (web_client_id, http_client) = seed_mock_session(&server_state);
let server_base_url = format!("https://127.0.0.1:{}", port);
let result = tokio::time::timeout(
Duration::from_secs(10),
websockets::establish_websocket_connections(
&web_client_id,
&http_client,
&server_base_url,
"test-session",
None, // no CA cert
false, // not insecure — should reject self-signed
),
)
.await
.expect("Test timed out");
assert!(
result.is_err(),
"TLS without CA cert should reject self-signed server"
);
tls_mock_server::shutdown_server(handle, server_task).await;
}
}
// Tests that don't require the web_server_capability feature
+188 -33
View File
@@ -1,14 +1,178 @@
use super::config::{WS_CONTROL_ENDPOINT, WS_TERMINAL_ENDPOINT};
use super::http_client::HttpClientWithCookies;
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::TcpStream;
use tokio_tungstenite::{
connect_async_tls_with_config, Connector, MaybeTlsStream, WebSocketStream,
};
use tokio_tungstenite::WebSocketStream;
// -- MaybeTls stream enum -------------------------------------------------
/// A TCP stream that may or may not be wrapped in TLS.
pub enum MaybeTls {
Plain(TcpStream),
Tls(tokio_rustls::client::TlsStream<TcpStream>),
}
impl AsyncRead for MaybeTls {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
match self.get_mut() {
MaybeTls::Plain(s) => Pin::new(s).poll_read(cx, buf),
MaybeTls::Tls(s) => Pin::new(s).poll_read(cx, buf),
}
}
}
impl AsyncWrite for MaybeTls {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
match self.get_mut() {
MaybeTls::Plain(s) => Pin::new(s).poll_write(cx, buf),
MaybeTls::Tls(s) => Pin::new(s).poll_write(cx, buf),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
match self.get_mut() {
MaybeTls::Plain(s) => Pin::new(s).poll_flush(cx),
MaybeTls::Tls(s) => Pin::new(s).poll_flush(cx),
}
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
match self.get_mut() {
MaybeTls::Plain(s) => Pin::new(s).poll_shutdown(cx),
MaybeTls::Tls(s) => Pin::new(s).poll_shutdown(cx),
}
}
}
// -- NoVerifier (for --insecure mode) --------------------------------------
#[derive(Debug)]
struct NoVerifier;
impl rustls::client::danger::ServerCertVerifier for NoVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls_pki_types::CertificateDer<'_>,
_intermediates: &[rustls_pki_types::CertificateDer<'_>],
_server_name: &rustls_pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls_pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls_pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls_pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
rustls::crypto::aws_lc_rs::default_provider()
.signature_verification_algorithms
.supported_schemes()
}
}
// -- TLS config builder ----------------------------------------------------
fn build_tls_config(
ca_cert: Option<&Path>,
insecure: bool,
) -> Result<Arc<rustls::ClientConfig>, Box<dyn std::error::Error>> {
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
if insecure {
let config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()?
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoVerifier))
.with_no_client_auth();
Ok(Arc::new(config))
} else if let Some(ca_path) = ca_cert {
let ca_pem = std::fs::read(ca_path)?;
let mut cursor = std::io::Cursor::new(ca_pem);
let certs: Vec<rustls_pki_types::CertificateDer<'static>> =
rustls_pemfile::certs(&mut cursor)
.filter_map(|r| r.ok())
.collect();
let mut root_store = rustls::RootCertStore::empty();
for cert in certs {
root_store.add(cert)?;
}
let config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()?
.with_root_certificates(root_store)
.with_no_client_auth();
Ok(Arc::new(config))
} else {
let native_certs = rustls_native_certs::load_native_certs();
for err in &native_certs.errors {
log::warn!("Error loading native certificate: {}", err);
}
let mut root_store = rustls::RootCertStore::empty();
root_store.add_parsable_certificates(native_certs.certs);
let config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()?
.with_root_certificates(root_store)
.with_no_client_auth();
Ok(Arc::new(config))
}
}
// -- WebSocket connection helpers ------------------------------------------
async fn connect_ws(
request: tokio_tungstenite::tungstenite::http::Request<()>,
host: &str,
port: u16,
tls_config: Option<Arc<rustls::ClientConfig>>,
) -> Result<WebSocketStream<MaybeTls>, Box<dyn std::error::Error>> {
let tcp_stream = TcpStream::connect((host, port)).await?;
let stream = if let Some(config) = tls_config {
let connector = tokio_rustls::TlsConnector::from(config);
let server_name = rustls_pki_types::ServerName::try_from(host.to_string())?;
let tls_stream = connector.connect(server_name, tcp_stream).await?;
MaybeTls::Tls(tls_stream)
} else {
MaybeTls::Plain(tcp_stream)
};
let (ws_stream, _response) =
tokio_tungstenite::client_async_with_config(request, stream, None).await?;
Ok(ws_stream)
}
// -- Public API ------------------------------------------------------------
pub struct WebSocketConnections {
pub terminal_ws: WebSocketStream<MaybeTlsStream<TcpStream>>,
pub control_ws: WebSocketStream<MaybeTlsStream<TcpStream>>,
pub terminal_ws: WebSocketStream<MaybeTls>,
pub control_ws: WebSocketStream<MaybeTls>,
pub web_client_id: String,
}
@@ -28,14 +192,18 @@ pub async fn establish_websocket_connections(
ca_cert: Option<&Path>,
insecure: bool,
) -> Result<WebSocketConnections, Box<dyn std::error::Error>> {
let ws_protocol = if server_base_url.starts_with("https") {
"wss"
} else {
"ws"
};
let base_host = server_base_url
.replace("https://", "")
.replace("http://", "");
let parsed_url = url::Url::parse(server_base_url)?;
let host = parsed_url
.host_str()
.ok_or("no host in server URL")?
.to_string();
let port = parsed_url
.port_or_known_default()
.ok_or("no port in server URL")?;
let is_tls = parsed_url.scheme() == "https";
let ws_protocol = if is_tls { "wss" } else { "ws" };
let base_host = format!("{}:{}", host, port);
let terminal_url = if session_name.is_empty() {
format!(
@@ -59,7 +227,7 @@ pub async fn establish_websocket_connections(
log::info!("Connecting to terminal WebSocket: {}", terminal_url);
log::info!("Connecting to control WebSocket: {}", control_url);
// Create WebSocket request with cookies
// Build WebSocket requests with cookies
let mut terminal_request = tokio_tungstenite::tungstenite::http::Request::builder()
.uri(&terminal_url)
.header("Host", &base_host)
@@ -91,29 +259,16 @@ pub async fn establish_websocket_connections(
let terminal_request = terminal_request.body(())?;
let control_request = control_request.body(())?;
// Build TLS connector based on ca_cert/insecure settings
let connector = if insecure {
let tls_connector = native_tls::TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()?;
Some(Connector::NativeTls(tls_connector))
} else if let Some(ca_path) = ca_cert {
let ca_pem = std::fs::read(ca_path)?;
let ca_cert = native_tls::Certificate::from_pem(&ca_pem)?;
let tls_connector = native_tls::TlsConnector::builder()
.add_root_certificate(ca_cert)
.build()?;
Some(Connector::NativeTls(tls_connector))
// Build TLS config (only for wss://)
let tls_config = if is_tls {
Some(build_tls_config(ca_cert, insecure)?)
} else {
None
};
// Connect to both WebSockets concurrently
let (terminal_ws, _) =
connect_async_tls_with_config(terminal_request, None, false, connector.clone()).await?;
let (control_ws, _) =
connect_async_tls_with_config(control_request, None, false, connector).await?;
// Connect to both WebSockets
let terminal_ws = connect_ws(terminal_request, &host, port, tls_config.clone()).await?;
let control_ws = connect_ws(control_request, &host, port, tls_config).await?;
Ok(WebSocketConnections {
terminal_ws,
+122 -13
View File
@@ -1,4 +1,5 @@
use crate::os_input_output::{AsyncSignals, AsyncStdin, ClientOsApi, SignalEvent};
use crate::remote_attach::websockets::MaybeTls;
use crate::remote_attach::WebSocketConnections;
use crate::run_remote_client_terminal_loop;
use crate::web_client::control_message::{
@@ -11,8 +12,8 @@ use serial_test::serial;
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;
use zellij_utils::data::Palette;
use zellij_utils::errors::ErrorContext;
@@ -325,8 +326,26 @@ async fn test_stdin_forwarded_to_terminal_websocket() {
let terminal_url = format!("ws://127.0.0.1:{}/ws/terminal", port);
let control_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (terminal_ws, _) = connect_async(&terminal_url).await.unwrap();
let (control_ws, _) = connect_async(&control_url).await.unwrap();
let terminal_tcp = TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.unwrap();
let (terminal_ws, _) = tokio_tungstenite::client_async_with_config(
&terminal_url,
MaybeTls::Plain(terminal_tcp),
None,
)
.await
.unwrap();
let control_tcp = TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.unwrap();
let (control_ws, _) = tokio_tungstenite::client_async_with_config(
&control_url,
MaybeTls::Plain(control_tcp),
None,
)
.await
.unwrap();
let connections = WebSocketConnections {
terminal_ws,
@@ -379,8 +398,26 @@ async fn test_terminal_output_written_to_stdout() {
let terminal_url = format!("ws://127.0.0.1:{}/ws/terminal", port);
let control_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (terminal_ws, _) = connect_async(&terminal_url).await.unwrap();
let (control_ws, _) = connect_async(&control_url).await.unwrap();
let terminal_tcp = TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.unwrap();
let (terminal_ws, _) = tokio_tungstenite::client_async_with_config(
&terminal_url,
MaybeTls::Plain(terminal_tcp),
None,
)
.await
.unwrap();
let control_tcp = TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.unwrap();
let (control_ws, _) = tokio_tungstenite::client_async_with_config(
&control_url,
MaybeTls::Plain(control_tcp),
None,
)
.await
.unwrap();
let connections = WebSocketConnections {
terminal_ws,
@@ -436,8 +473,26 @@ async fn test_resize_signal_sends_control_message() {
let terminal_url = format!("ws://127.0.0.1:{}/ws/terminal", port);
let control_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (terminal_ws, _) = connect_async(&terminal_url).await.unwrap();
let (control_ws, _) = connect_async(&control_url).await.unwrap();
let terminal_tcp = TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.unwrap();
let (terminal_ws, _) = tokio_tungstenite::client_async_with_config(
&terminal_url,
MaybeTls::Plain(terminal_tcp),
None,
)
.await
.unwrap();
let control_tcp = TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.unwrap();
let (control_ws, _) = tokio_tungstenite::client_async_with_config(
&control_url,
MaybeTls::Plain(control_tcp),
None,
)
.await
.unwrap();
let connections = WebSocketConnections {
terminal_ws,
@@ -509,8 +564,26 @@ async fn test_quit_signal_exits_loop() {
let terminal_url = format!("ws://127.0.0.1:{}/ws/terminal", port);
let control_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (terminal_ws, _) = connect_async(&terminal_url).await.unwrap();
let (control_ws, _) = connect_async(&control_url).await.unwrap();
let terminal_tcp = TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.unwrap();
let (terminal_ws, _) = tokio_tungstenite::client_async_with_config(
&terminal_url,
MaybeTls::Plain(terminal_tcp),
None,
)
.await
.unwrap();
let control_tcp = TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.unwrap();
let (control_ws, _) = tokio_tungstenite::client_async_with_config(
&control_url,
MaybeTls::Plain(control_tcp),
None,
)
.await
.unwrap();
let connections = WebSocketConnections {
terminal_ws,
@@ -546,8 +619,26 @@ async fn test_websocket_close_exits_loop() {
let terminal_url = format!("ws://127.0.0.1:{}/ws/terminal", port);
let control_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (terminal_ws, _) = connect_async(&terminal_url).await.unwrap();
let (control_ws, _) = connect_async(&control_url).await.unwrap();
let terminal_tcp = TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.unwrap();
let (terminal_ws, _) = tokio_tungstenite::client_async_with_config(
&terminal_url,
MaybeTls::Plain(terminal_tcp),
None,
)
.await
.unwrap();
let control_tcp = TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.unwrap();
let (control_ws, _) = tokio_tungstenite::client_async_with_config(
&control_url,
MaybeTls::Plain(control_tcp),
None,
)
.await
.unwrap();
let connections = WebSocketConnections {
terminal_ws,
@@ -586,8 +677,26 @@ async fn test_control_message_handling() {
let terminal_url = format!("ws://127.0.0.1:{}/ws/terminal", port);
let control_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (terminal_ws, _) = connect_async(&terminal_url).await.unwrap();
let (control_ws, _) = connect_async(&control_url).await.unwrap();
let terminal_tcp = TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.unwrap();
let (terminal_ws, _) = tokio_tungstenite::client_async_with_config(
&terminal_url,
MaybeTls::Plain(terminal_tcp),
None,
)
.await
.unwrap();
let control_tcp = TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.unwrap();
let (control_ws, _) = tokio_tungstenite::client_async_with_config(
&control_url,
MaybeTls::Plain(control_tcp),
None,
)
.await
.unwrap();
let connections = WebSocketConnections {
terminal_ws,
@@ -44,6 +44,7 @@ fn get_db_path() -> Result<PathBuf> {
}
fn init_db(conn: &Connection) -> Result<()> {
conn.execute_batch("PRAGMA busy_timeout = 5000")?;
conn.execute(
"CREATE TABLE IF NOT EXISTS remote_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -89,6 +89,7 @@ fn open_db() -> Result<Connection> {
}
fn init_db(conn: &Connection) -> Result<()> {
conn.execute_batch("PRAGMA busy_timeout = 5000")?;
conn.execute_batch("PRAGMA foreign_keys = ON")?;
conn.execute(