rmux-server 0.1.1

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use super::{
    default_socket_path, remove_stale_socket_if_needed, socket_root_from_env, DaemonConfig,
    ServerDaemon,
};
use std::ffi::OsStr;
use std::fs;
use std::os::unix::fs::{FileTypeExt, PermissionsExt};
use std::os::unix::net::{UnixListener as StdUnixListener, UnixStream as StdUnixStream};
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;

static UNIQUE_ID: AtomicUsize = AtomicUsize::new(0);

#[test]
fn default_socket_path_uses_the_spec_layout() {
    let path = default_socket_path().expect("default socket path");
    let path_string = path.to_string_lossy();

    assert!(path_string.ends_with("/default"));
    assert!(path_string.contains("/rmux-"));
}

#[test]
fn unresolved_rmux_tmpdir_falls_back_to_tmp() {
    assert_eq!(
        socket_root_from_env(Some(OsStr::new(
            "relative-rmux-test-path-that-does-not-exist"
        )))
        .expect("socket root"),
        fs::canonicalize("/tmp").expect("canonical /tmp")
    );
}

#[test]
fn real_user_id_matches_process_identity() {
    assert_eq!(
        super::real_user_id().expect("real uid"),
        rmux_os::identity::real_user_id()
    );
}

#[test]
fn daemon_config_returns_the_configured_path() {
    let path = PathBuf::from("/tmp/rmux-test/default");
    let config = DaemonConfig::new(path.clone());

    assert_eq!(config.socket_path(), path.as_path());
}

#[test]
fn stale_socket_probe_removes_unreachable_socket_files() {
    let socket_path = unique_socket_path("stale-socket");
    let parent = socket_path.parent().expect("socket parent");
    let _ = fs::remove_file(&socket_path);
    let _ = fs::remove_dir_all(parent);
    fs::create_dir_all(parent).expect("create socket parent");
    let listener = StdUnixListener::bind(&socket_path).expect("bind stale socket");
    drop(listener);
    wait_until_socket_is_stale(&socket_path).expect("dropped listener becomes unreachable");

    remove_stale_socket_if_needed(&socket_path).expect("remove stale socket");

    assert!(!socket_path.exists());
    let _ = fs::remove_dir_all(parent);
}

#[test]
fn unsafe_managed_rmux_socket_directory_is_rejected() {
    let user_id = super::real_user_id().expect("real uid");
    let root = unique_socket_path("permissions")
        .parent()
        .expect("socket parent")
        .to_path_buf();
    let socket_path = root.join(format!("rmux-{user_id}")).join("default");
    let parent = socket_path.parent().expect("socket parent");
    fs::create_dir_all(parent).expect("create managed socket parent");
    fs::set_permissions(parent, fs::Permissions::from_mode(0o755)).expect("set perms");

    let error = super::ensure_parent_directory(parent).expect_err("unsafe parent should fail");

    assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
    let _ = fs::remove_dir_all(root);
}

#[test]
fn group_accessible_managed_rmux_socket_directory_is_rejected() {
    let user_id = super::real_user_id().expect("real uid");
    let root = unique_socket_path("group-permissions")
        .parent()
        .expect("socket parent")
        .to_path_buf();
    let socket_path = root.join(format!("rmux-{user_id}")).join("default");
    let parent = socket_path.parent().expect("socket parent");
    fs::create_dir_all(parent).expect("create managed socket parent");
    fs::set_permissions(parent, fs::Permissions::from_mode(0o750)).expect("set perms");

    let error = super::ensure_parent_directory(parent).expect_err("unsafe parent should fail");

    assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
    let _ = fs::set_permissions(parent, fs::Permissions::from_mode(0o700));
    let _ = fs::remove_dir_all(root);
}

#[test]
fn unsafe_managed_rmux_socket_directory_ancestor_is_rejected() {
    let user_id = super::real_user_id().expect("real uid");
    let root = unique_socket_path("nested-permissions")
        .parent()
        .expect("socket parent")
        .to_path_buf();
    let managed = root.join(format!("rmux-{user_id}"));
    let nested_parent = managed.join("nested");
    fs::create_dir_all(&nested_parent).expect("create nested socket parent");
    fs::set_permissions(&managed, fs::Permissions::from_mode(0o755)).expect("set perms");

    let error = super::ensure_parent_directory(&nested_parent)
        .expect_err("unsafe managed ancestor should fail");

    assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
    let _ = fs::set_permissions(&managed, fs::Permissions::from_mode(0o700));
    let _ = fs::remove_dir_all(root);
}

#[tokio::test]
async fn bound_socket_permissions_are_owner_only() {
    let socket_path = unique_socket_path("bound-permissions");
    let parent = socket_path.parent().expect("socket parent");
    let _ = fs::remove_file(&socket_path);
    let _ = fs::remove_dir_all(parent);

    let handle = ServerDaemon::new(DaemonConfig::new(socket_path.clone()))
        .bind()
        .await
        .expect("bind daemon");

    let metadata = fs::symlink_metadata(&socket_path).expect("bound socket metadata");
    assert!(metadata.file_type().is_socket());
    assert_eq!(
        metadata.permissions().mode() & 0o077,
        0,
        "bound daemon socket must not expose group/other permissions"
    );

    handle.shutdown().await.expect("shutdown daemon");
    let _ = fs::remove_dir_all(parent);
}

#[test]
fn stale_socket_removal_leaves_non_socket_paths_untouched() {
    let socket_path = unique_socket_path("not-a-socket");
    let parent = socket_path.parent().expect("socket parent");
    fs::create_dir_all(parent).expect("create socket parent");
    fs::write(&socket_path, "not a socket").expect("write regular file");

    let error = remove_stale_socket_if_needed(&socket_path).expect_err("must fail");

    assert_eq!(error.kind(), std::io::ErrorKind::AlreadyExists);
    assert!(fs::symlink_metadata(&socket_path)
        .expect("metadata")
        .file_type()
        .is_file());
    let _ = fs::remove_file(&socket_path);
    let _ = fs::remove_dir_all(parent);
}

fn unique_socket_path(label: &str) -> PathBuf {
    let unique_id = UNIQUE_ID.fetch_add(1, Ordering::Relaxed);
    let process_id = std::process::id();
    std::env::temp_dir()
        .join(format!("rmux-server-{label}-{process_id}-{unique_id}"))
        .join("default.sock")
}

fn wait_until_socket_is_stale(socket_path: &std::path::Path) -> std::io::Result<()> {
    for _ in 0..100 {
        match StdUnixStream::connect(socket_path) {
            Err(error) if super::indicates_stale_socket(&error) => return Ok(()),
            Err(error) => return Err(error),
            Ok(stream) => drop(stream),
        }

        std::thread::sleep(Duration::from_millis(1));
    }

    Err(std::io::Error::new(
        std::io::ErrorKind::TimedOut,
        format!("socket '{}' stayed reachable", socket_path.display()),
    ))
}