Expand native auth and benchmark gates
This commit is contained in:
+334
-1
@@ -4,6 +4,7 @@ use std::net::{TcpListener, TcpStream, UdpSocket};
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -234,6 +235,37 @@ fn start_echo_server() -> u16 {
|
||||
port
|
||||
}
|
||||
|
||||
fn start_slow_sink_server() -> u16 {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
thread::spawn(move || {
|
||||
if let Ok((mut stream, _)) = listener.accept() {
|
||||
let mut buf = [0u8; 8192];
|
||||
loop {
|
||||
match stream.read(&mut buf) {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(_) => thread::sleep(Duration::from_millis(20)),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
port
|
||||
}
|
||||
|
||||
fn connect_with_retry_no_child(port: u16, timeout: Duration) -> TcpStream {
|
||||
let deadline = std::time::Instant::now() + timeout;
|
||||
loop {
|
||||
match TcpStream::connect(("127.0.0.1", port)) {
|
||||
Ok(stream) => return stream,
|
||||
Err(err) if std::time::Instant::now() < deadline => {
|
||||
let _ = err;
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err(err) => panic!("connect to local forward 127.0.0.1:{port}: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_native_client_auth(dir: &tempfile::TempDir, config_path: &std::path::Path) {
|
||||
write_native_client_auth_with_options(dir, config_path, "");
|
||||
}
|
||||
@@ -414,6 +446,15 @@ fn direct_attach(
|
||||
config: &std::path::Path,
|
||||
port: u16,
|
||||
mode: &str,
|
||||
) -> (std::net::UdpSocket, dosh::auth::BootstrapResponse, AttachOk) {
|
||||
direct_attach_session(config, port, mode, "default")
|
||||
}
|
||||
|
||||
fn direct_attach_session(
|
||||
config: &std::path::Path,
|
||||
port: u16,
|
||||
mode: &str,
|
||||
session: &str,
|
||||
) -> (std::net::UdpSocket, dosh::auth::BootstrapResponse, AttachOk) {
|
||||
let config = load_server_config(Some(config.to_path_buf())).unwrap();
|
||||
let secret = load_or_create_server_secret(&config).unwrap();
|
||||
@@ -421,7 +462,7 @@ fn direct_attach(
|
||||
&config,
|
||||
&secret,
|
||||
"tester".to_string(),
|
||||
"default".to_string(),
|
||||
session.to_string(),
|
||||
mode.to_string(),
|
||||
(80, 24),
|
||||
crypto::random_12(),
|
||||
@@ -782,6 +823,106 @@ fn native_local_forward_background_smoke() {
|
||||
assert_eq!(&buf[..n], b"dosh-background-forward-ping");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_local_forward_bulk_load_does_not_delay_interactive_terminal() {
|
||||
use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem};
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let port = free_udp_port();
|
||||
let config = write_server_config(&dir, port);
|
||||
write_native_client_auth(&dir, &config);
|
||||
let sink_port = start_slow_sink_server();
|
||||
let local_port = free_tcp_port();
|
||||
let mut server = start_server(&dir, &config);
|
||||
|
||||
let pty = NativePtySystem::default();
|
||||
let pair = pty
|
||||
.openpty(PtySize {
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
})
|
||||
.unwrap();
|
||||
let mut reader = pair.master.try_clone_reader().unwrap();
|
||||
let mut writer = pair.master.take_writer().unwrap();
|
||||
|
||||
let client_bin = env!("CARGO_BIN_EXE_dosh-client");
|
||||
let mut cmd = CommandBuilder::new(client_bin);
|
||||
let forward_arg = format!("{local_port}:127.0.0.1:{sink_port}");
|
||||
let dosh_port = port.to_string();
|
||||
cmd.args([
|
||||
"--auth",
|
||||
"native",
|
||||
"--no-cache",
|
||||
"-L",
|
||||
&forward_arg,
|
||||
"--session",
|
||||
"load-session",
|
||||
"--dosh-host",
|
||||
"127.0.0.1",
|
||||
"--dosh-port",
|
||||
&dosh_port,
|
||||
"local",
|
||||
]);
|
||||
cmd.env("HOME", dir.path().to_string_lossy().to_string());
|
||||
let mut child = pair.slave.spawn_command(cmd).unwrap();
|
||||
drop(pair.slave);
|
||||
|
||||
let mut forward_stream = connect_with_retry_no_child(local_port, Duration::from_secs(5));
|
||||
let bulk_writer = thread::spawn(move || {
|
||||
let chunk = vec![b'x'; 8192];
|
||||
for _ in 0..128 {
|
||||
if forward_stream.write_all(&chunk).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let (seen_tx, seen_rx) = mpsc::channel();
|
||||
let reader_handle = thread::spawn(move || {
|
||||
let mut buf = [0u8; 4096];
|
||||
let mut output = Vec::new();
|
||||
loop {
|
||||
match reader.read(&mut buf) {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => {
|
||||
output.extend_from_slice(&buf[..n]);
|
||||
if output
|
||||
.windows(b"DOSH_LOAD_TERMINAL_OK".len())
|
||||
.any(|w| w == b"DOSH_LOAD_TERMINAL_OK")
|
||||
{
|
||||
let _ = seen_tx.send(String::from_utf8_lossy(&output).to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
writer
|
||||
.write_all(b"printf DOSH_LOAD_TERMINAL_OK\\n\r")
|
||||
.unwrap();
|
||||
writer.flush().unwrap();
|
||||
|
||||
let seen = seen_rx.recv_timeout(Duration::from_secs(3));
|
||||
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
drop(writer);
|
||||
drop(pair.master);
|
||||
let _ = reader_handle.join();
|
||||
let _ = bulk_writer.join();
|
||||
let _ = server.kill();
|
||||
let _ = server.wait();
|
||||
|
||||
assert!(
|
||||
seen.is_ok(),
|
||||
"interactive terminal output was delayed behind local-forward bulk load"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_remote_forward_echo_smoke() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@@ -1180,6 +1321,67 @@ fn live_output_forwards_terminal_control_sequences() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_control_sequences_survive_transport_verbatim() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let port = free_udp_port();
|
||||
let config = write_server_config(&dir, port);
|
||||
let mut server = start_server(&dir, &config);
|
||||
let (socket, bootstrap, ok) = direct_attach(&config, port, "read-write");
|
||||
|
||||
let sequences = concat!(
|
||||
"\x1b[?1049h", // alternate screen on
|
||||
"\x1b[?2004h", // bracketed paste on
|
||||
"\x1b[?1000h", // mouse tracking on
|
||||
"\x1b[?1006h", // SGR mouse encoding on
|
||||
"\x1b[?25l", // cursor hidden
|
||||
"\x1b[12;34H", // absolute cursor movement
|
||||
"DOSH_TUI_VERBATIM",
|
||||
"\x1b[?25h",
|
||||
"\x1b[?1006l",
|
||||
"\x1b[?1000l",
|
||||
"\x1b[?2004l",
|
||||
"\x1b[?1049l"
|
||||
);
|
||||
let input = Input {
|
||||
bytes: format!("printf '{sequences}'\n").into_bytes(),
|
||||
};
|
||||
send_encrypted(
|
||||
&socket,
|
||||
port,
|
||||
PacketKind::Input,
|
||||
ok.client_id,
|
||||
2,
|
||||
0,
|
||||
&bootstrap.session_key,
|
||||
&protocol::to_body(&input).unwrap(),
|
||||
);
|
||||
let text = collect_frame_text(&socket, &bootstrap.session_key, 2000);
|
||||
|
||||
let _ = server.kill();
|
||||
let _ = server.wait();
|
||||
|
||||
for needle in [
|
||||
"\x1b[?1049h",
|
||||
"\x1b[?2004h",
|
||||
"\x1b[?1000h",
|
||||
"\x1b[?1006h",
|
||||
"\x1b[?25l",
|
||||
"\x1b[12;34H",
|
||||
"DOSH_TUI_VERBATIM",
|
||||
"\x1b[?25h",
|
||||
"\x1b[?1006l",
|
||||
"\x1b[?1000l",
|
||||
"\x1b[?2004l",
|
||||
"\x1b[?1049l",
|
||||
] {
|
||||
assert!(
|
||||
text.contains(needle),
|
||||
"expected TUI control sequence {needle:?} to survive verbatim, got {text:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_snapshot_preserves_alternate_screen_mode() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@@ -1983,3 +2185,134 @@ fn session_survives_server_restart_same_shell_and_screen() {
|
||||
"restored attach snapshot must repaint the pre-restart screen, got {snapshot:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_persistent_named_sessions_survive_restart_independently() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let sessions_dir = dir.path().join("sessions");
|
||||
let port = free_udp_port();
|
||||
let config = write_persistent_server_config(&dir, port);
|
||||
|
||||
let mut server = start_server(&dir, &config);
|
||||
|
||||
let (work_socket, work_bootstrap, work_ok) =
|
||||
direct_attach_session(&config, port, "read-write", "work");
|
||||
let (logs_socket, logs_bootstrap, logs_ok) =
|
||||
direct_attach_session(&config, port, "read-write", "logs");
|
||||
let work_key = work_bootstrap.session_key;
|
||||
let logs_key = logs_bootstrap.session_key;
|
||||
|
||||
type_line(
|
||||
&work_socket,
|
||||
port,
|
||||
&work_ok,
|
||||
&work_key,
|
||||
2,
|
||||
"export DOSH_SLOT=work_slot\n",
|
||||
);
|
||||
type_line(
|
||||
&logs_socket,
|
||||
port,
|
||||
&logs_ok,
|
||||
&logs_key,
|
||||
2,
|
||||
"export DOSH_SLOT=logs_slot\n",
|
||||
);
|
||||
type_line(
|
||||
&work_socket,
|
||||
port,
|
||||
&work_ok,
|
||||
&work_key,
|
||||
3,
|
||||
"printf 'WORK_SCREEN_MARKER\\n'\n",
|
||||
);
|
||||
type_line(
|
||||
&logs_socket,
|
||||
port,
|
||||
&logs_ok,
|
||||
&logs_key,
|
||||
3,
|
||||
"printf 'LOGS_SCREEN_MARKER\\n'\n",
|
||||
);
|
||||
let work_pre = collect_frame_text(&work_socket, &work_key, 1500);
|
||||
let logs_pre = collect_frame_text(&logs_socket, &logs_key, 1500);
|
||||
assert!(
|
||||
work_pre.contains("WORK_SCREEN_MARKER"),
|
||||
"work marker missing before restart: {work_pre:?}"
|
||||
);
|
||||
assert!(
|
||||
logs_pre.contains("LOGS_SCREEN_MARKER"),
|
||||
"logs marker missing before restart: {logs_pre:?}"
|
||||
);
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
|
||||
let work_pid_before = holder_shell_pid(&sessions_dir, "work");
|
||||
let logs_pid_before = holder_shell_pid(&sessions_dir, "logs");
|
||||
assert!(work_pid_before.is_some(), "work holder pid missing");
|
||||
assert!(logs_pid_before.is_some(), "logs holder pid missing");
|
||||
assert_ne!(
|
||||
work_pid_before, logs_pid_before,
|
||||
"named sessions must have independent holder shells"
|
||||
);
|
||||
|
||||
let _ = server.kill();
|
||||
let _ = server.wait();
|
||||
thread::sleep(Duration::from_millis(300));
|
||||
|
||||
let mut server = start_server(&dir, &config);
|
||||
let (work_socket2, work_bootstrap2, work_ok2) =
|
||||
direct_attach_session(&config, port, "read-write", "work");
|
||||
let (logs_socket2, logs_bootstrap2, logs_ok2) =
|
||||
direct_attach_session(&config, port, "read-write", "logs");
|
||||
let work_key2 = work_bootstrap2.session_key;
|
||||
let logs_key2 = logs_bootstrap2.session_key;
|
||||
let work_snapshot = String::from_utf8_lossy(&work_ok2.snapshot).to_string();
|
||||
let logs_snapshot = String::from_utf8_lossy(&logs_ok2.snapshot).to_string();
|
||||
|
||||
type_line(
|
||||
&work_socket2,
|
||||
port,
|
||||
&work_ok2,
|
||||
&work_key2,
|
||||
2,
|
||||
"printf 'WORK_SLOT=%s\\n' \"$DOSH_SLOT\"\n",
|
||||
);
|
||||
type_line(
|
||||
&logs_socket2,
|
||||
port,
|
||||
&logs_ok2,
|
||||
&logs_key2,
|
||||
2,
|
||||
"printf 'LOGS_SLOT=%s\\n' \"$DOSH_SLOT\"\n",
|
||||
);
|
||||
let work_post = collect_frame_text(&work_socket2, &work_key2, 2000);
|
||||
let logs_post = collect_frame_text(&logs_socket2, &logs_key2, 2000);
|
||||
let work_pid_after = holder_shell_pid(&sessions_dir, "work");
|
||||
let logs_pid_after = holder_shell_pid(&sessions_dir, "logs");
|
||||
|
||||
let _ = server.kill();
|
||||
let _ = server.wait();
|
||||
kill_holder(&sessions_dir, "work");
|
||||
kill_holder(&sessions_dir, "logs");
|
||||
|
||||
assert_eq!(work_pid_after, work_pid_before);
|
||||
assert_eq!(logs_pid_after, logs_pid_before);
|
||||
assert!(
|
||||
work_snapshot.contains("WORK_SCREEN_MARKER")
|
||||
&& !work_snapshot.contains("LOGS_SCREEN_MARKER"),
|
||||
"work snapshot should restore only work screen, got {work_snapshot:?}"
|
||||
);
|
||||
assert!(
|
||||
logs_snapshot.contains("LOGS_SCREEN_MARKER")
|
||||
&& !logs_snapshot.contains("WORK_SCREEN_MARKER"),
|
||||
"logs snapshot should restore only logs screen, got {logs_snapshot:?}"
|
||||
);
|
||||
assert!(
|
||||
work_post.contains("WORK_SLOT=work_slot") && !work_post.contains("logs_slot"),
|
||||
"work state crossed sessions or disappeared: {work_post:?}"
|
||||
);
|
||||
assert!(
|
||||
logs_post.contains("LOGS_SLOT=logs_slot") && !logs_post.contains("work_slot"),
|
||||
"logs state crossed sessions or disappeared: {logs_post:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user