rmux-server 0.10.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use super::*;
use crate::pane_io::AttachControl;

async fn register_read_only_attach(
    handler: &RequestHandler,
    requester_pid: u32,
    session: &rmux_proto::SessionName,
) -> mpsc::UnboundedReceiver<AttachControl> {
    let (control_tx, control_rx) = mpsc::unbounded_channel();
    handler
        .register_attach(requester_pid, session.clone(), control_tx)
        .await;
    let mut active_attach = handler.active_attach.lock().await;
    let active = active_attach
        .by_pid
        .get_mut(&requester_pid)
        .expect("read-only attach is active");
    active.can_write = false;
    active.flags = active.flags.with_read_only();
    control_rx
}

async fn bind_key(handler: &RequestHandler, key: &str, command: &[&str]) {
    let response = handler
        .handle(Request::BindKey(Box::new(BindKeyRequest {
            table_name: "root".to_owned(),
            key: key.to_owned(),
            note: Some("read-only security boundary test".to_owned()),
            repeat: false,
            command: Some(
                command
                    .iter()
                    .map(|argument| (*argument).to_owned())
                    .collect(),
            ),
        })))
        .await;
    assert!(matches!(response, Response::BindKey(_)), "{response:?}");
}

async fn active_session(handler: &RequestHandler, requester_pid: u32) -> rmux_proto::SessionName {
    handler
        .active_attach
        .lock()
        .await
        .by_pid
        .get(&requester_pid)
        .expect("read-only attach remains active")
        .session_name
        .clone()
}

fn assert_no_terminal_control(control_rx: &mut mpsc::UnboundedReceiver<AttachControl>) {
    while let Ok(control) = control_rx.try_recv() {
        assert!(
            !matches!(
                control,
                AttachControl::Detach
                    | AttachControl::DetachKill
                    | AttachControl::DetachExecShellCommand(_)
            ),
            "read-only unsafe form emitted terminal client control: {control:?}"
        );
    }
}

#[tokio::test]
async fn read_only_switch_allowlist_rejects_privilege_targeting_and_session_mutation() {
    let handler = RequestHandler::new();
    let alpha = session_name("read-only-switch-security-alpha");
    let beta = session_name("read-only-switch-security-beta");
    create_send_keys_test_session(&handler, &alpha).await;
    create_send_keys_test_session(&handler, &beta).await;
    let requester_pid = std::process::id();
    let witness_pid = requester_pid.wrapping_add(1);
    let mut requester_rx = register_read_only_attach(&handler, requester_pid, &alpha).await;
    let mut witness_rx = register_read_only_attach(&handler, witness_pid, &alpha).await;
    let witness_name = handler
        .active_attach
        .lock()
        .await
        .by_pid
        .get(&witness_pid)
        .expect("witness attach is active")
        .client_name
        .clone();
    let target_window = {
        let mut state = handler.state.lock().await;
        state
            .sessions
            .session_mut(&beta)
            .expect("beta exists")
            .create_window(TerminalSize { cols: 80, rows: 24 })
            .expect("inactive beta window is created")
            .0
    };

    bind_key(&handler, "r", &["switch-client", "-Er"]).await;
    bind_key(
        &handler,
        "c",
        &[
            "switch-client",
            "-c",
            witness_name.as_str(),
            "-E",
            "-t",
            beta.as_str(),
        ],
    )
    .await;
    let window_target = format!("{beta}:{target_window}");
    bind_key(
        &handler,
        "w",
        &["switch-client", "-t", window_target.as_str()],
    )
    .await;
    bind_key(&handler, "z", &["switch-client", "-Z", "-t", beta.as_str()]).await;

    for key in [b"r", b"c", b"w", b"z"] {
        handler
            .handle_attached_live_input_for_test(requester_pid, key)
            .await
            .expect("unsafe read-only switch input is consumed");
        assert_eq!(active_session(&handler, requester_pid).await, alpha);
        assert_eq!(active_session(&handler, witness_pid).await, alpha);
        assert!(
            handler
                .active_attach
                .lock()
                .await
                .by_pid
                .get(&requester_pid)
                .expect("requester remains active")
                .flags
                .contains(crate::client_flags::ClientFlags::READONLY),
            "switch-client -r must never clear read-only"
        );
        assert_eq!(
            handler
                .state
                .lock()
                .await
                .sessions
                .session(&beta)
                .expect("beta exists")
                .active_window_index(),
            0,
            "window or zoom target must not mutate the session"
        );
        assert_no_terminal_control(&mut requester_rx);
        assert_no_terminal_control(&mut witness_rx);
    }
}

#[tokio::test]
async fn read_only_detach_allowlist_rejects_every_nonlocal_form() {
    let handler = RequestHandler::new();
    let alpha = session_name("read-only-detach-security");
    create_send_keys_test_session(&handler, &alpha).await;
    let requester_pid = std::process::id();
    let witness_pid = requester_pid.wrapping_add(1);
    let mut requester_rx = register_read_only_attach(&handler, requester_pid, &alpha).await;
    let mut witness_rx = register_read_only_attach(&handler, witness_pid, &alpha).await;
    let witness_name = handler
        .active_attach
        .lock()
        .await
        .by_pid
        .get(&witness_pid)
        .expect("witness attach is active")
        .client_name
        .clone();

    bind_key(&handler, "p", &["detach-client", "-P"]).await;
    bind_key(&handler, "e", &["detach-client", "-E", "printf unsafe"]).await;
    bind_key(&handler, "a", &["detach-client", "-aP"]).await;
    bind_key(
        &handler,
        "t",
        &["detach-client", "-t", witness_name.as_str()],
    )
    .await;
    bind_key(&handler, "s", &["detach-client", "-s", alpha.as_str()]).await;

    for key in [b"p", b"e", b"a", b"t", b"s"] {
        handler
            .handle_attached_live_input_for_test(requester_pid, key)
            .await
            .expect("unsafe read-only detach input is consumed");
        assert_eq!(active_session(&handler, requester_pid).await, alpha);
        assert_eq!(active_session(&handler, witness_pid).await, alpha);
        assert_no_terminal_control(&mut requester_rx);
        assert_no_terminal_control(&mut witness_rx);
    }
}

#[tokio::test]
async fn read_only_safe_switch_cannot_lead_a_composed_mutation() {
    let handler = RequestHandler::new();
    let alpha = session_name("read-only-switch-chain-alpha");
    let beta = session_name("read-only-switch-chain-beta");
    create_send_keys_test_session(&handler, &alpha).await;
    create_send_keys_test_session(&handler, &beta).await;
    let requester_pid = std::process::id();
    let mut control_rx = register_read_only_attach(&handler, requester_pid, &alpha).await;
    bind_key(
        &handler,
        "x",
        &["switch-client", "-t", beta.as_str(), ";", "new-window"],
    )
    .await;

    handler
        .handle_attached_live_input_for_test(requester_pid, b"x")
        .await
        .expect("composed read-only switch input is consumed");

    assert_eq!(active_session(&handler, requester_pid).await, alpha);
    assert_eq!(
        handler
            .state
            .lock()
            .await
            .sessions
            .session(&beta)
            .expect("beta exists")
            .windows()
            .len(),
        1,
        "the mutation after a safe-looking first command must not execute"
    );
    assert_no_terminal_control(&mut control_rx);
}

#[tokio::test]
async fn read_only_key_table_switch_cannot_lead_a_composed_mutation() {
    let handler = RequestHandler::new();
    let alpha = session_name("read-only-key-table-chain");
    create_send_keys_test_session(&handler, &alpha).await;
    let requester_pid = std::process::id();
    let mut control_rx = register_read_only_attach(&handler, requester_pid, &alpha).await;
    bind_key(
        &handler,
        "x",
        &["switch-client", "-T", "custom", ";", "new-window"],
    )
    .await;

    handler
        .handle_attached_live_input_for_test(requester_pid, b"x")
        .await
        .expect("composed read-only key-table input is consumed");

    let active_attach = handler.active_attach.lock().await;
    let active = active_attach
        .by_pid
        .get(&requester_pid)
        .expect("requester remains active");
    assert_eq!(active.key_table_name, None);
    drop(active_attach);
    assert_eq!(
        handler
            .state
            .lock()
            .await
            .sessions
            .session(&alpha)
            .expect("alpha exists")
            .windows()
            .len(),
        1
    );
    assert_no_terminal_control(&mut control_rx);
}