http-sh 0.1.0

An HTTP server for executing shell scripts.
Documentation
use std::io::BufRead;
use std::io::Read;
use std::str::FromStr;

use std::process::Command;

use sysinfo::SystemExt;

// Taken from:
// https://github.com/assert-rs/assert_cmd/blob/e71a9f7b15596dd2aeea911bedbbd1859d84fa67/src/cargo.rs#L183-L208
fn target_dir() -> std::path::PathBuf {
    std::env::current_exe()
        .ok()
        .map(|mut path| {
            path.pop();
            if path.ends_with("deps") {
                path.pop();
            }
            path
        })
        .unwrap()
}

fn cargo_bin(name: &str) -> std::path::PathBuf {
    let env_var = format!("CARGO_BIN_EXE_{}", name);
    std::env::var_os(&env_var)
        .map(|p| p.into())
        .unwrap_or_else(|| target_dir().join(format!("{}{}", name, std::env::consts::EXE_SUFFIX)))
}

fn run_curl(args: Vec<&str>) -> std::process::Output {
    let mut command = Command::new("curl");
    command.arg("-s");
    for arg in args {
        command.arg(arg);
    }
    command.output().unwrap()
}

#[test]
fn connection_cleanup() {
    let temp_dir = tempfile::tempdir().unwrap();
    let path = temp_dir.path().join("test.sock");
    let path = path.to_str().unwrap();

    let http_sh = cargo_bin("http-sh");

    let serve = Command::new(http_sh)
        .arg(path)
        .arg("--")
        .arg("bash")
        .arg("-c")
        .arg(
            r#"
            exec 4>&-
            sleep 3600 &
            SLEEPID=$!
            echo $SLEEPID
            trap 'kill $SLEEPID' SIGTERM
            wait
            "#,
        )
        .stdout(std::process::Stdio::piped())
        .spawn()
        .unwrap();

    let mut serve = scopeguard::guard(serve, |mut serve| {
        let _ = serve.kill();
    });

    // read startup log line to ensure serve is ready
    let stdout = serve.stdout.take().unwrap();
    let stdout = std::io::BufReader::new(stdout);
    let mut loglines = stdout.lines();
    println!("logline: {:?}", loglines.next().unwrap());

    let mut curl = Command::new("curl")
        .arg("-s")
        .arg("--no-buffer")
        .arg("--unix-socket")
        .arg(path)
        .arg("http://localhost/")
        .stdout(std::process::Stdio::piped())
        .spawn()
        .unwrap();

    let curl_out = curl.stdout.take().unwrap();
    let curl_out = std::io::BufReader::new(curl_out);
    let mut curl_lines = curl_out.lines();

    let pid = curl_lines.next().unwrap().unwrap();
    let pid = sysinfo::Pid::from_str(&pid).unwrap();

    let mut sys = sysinfo::System::new_all();
    sys.refresh_all();
    assert!(sys.process(pid).is_some());

    let _ = curl.kill().unwrap();
    let _ = curl.wait().unwrap();

    sys.refresh_all();
    assert!(sys.process(pid).is_none());
}

#[test]
fn serve_unix() {
    let temp_dir = tempfile::tempdir().unwrap();
    let path = temp_dir.path().join("test.sock");
    let path = path.to_str().unwrap();

    let http_sh = cargo_bin("http-sh");
    let want = "Hello from server!";

    let mut serve = Command::new(http_sh)
        .arg(path)
        .arg("--")
        .arg("printf")
        .arg(want)
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .unwrap();

    // read startup log line to ensure serve is ready
    let stdout = serve.stdout.take().unwrap();
    let stdout = std::io::BufReader::new(stdout);
    let mut loglines = stdout.lines();

    let _logline = loglines.next().unwrap().unwrap();
    // todo: assert start logline looks OK

    let got = run_curl(vec![
        "--http1.1",
        "--unix-socket",
        path,
        "http://localhost:5555/",
    ]);
    assert_eq!(want.as_bytes(), got.stdout);

    // next , parse got.stdout to a Response and assert HOST header
    let logline = loglines.next().unwrap().unwrap();
    let log: http_sh::Request = serde_json::from_str(&logline).unwrap();
    assert_eq!(log.proto, "HTTP/1.1");
    assert_eq!(log.authority, Some("localhost:5555".to_string()));

    let got = run_curl(vec![
        "--http2-prior-knowledge",
        "--unix-socket",
        path,
        "http://localhost:5555/",
    ]);
    assert_eq!(want.as_bytes(), got.stdout);
    let logline = loglines.next().unwrap().unwrap();
    assert_eq!(log.authority, Some("localhost:5555".to_string()));

    serve.kill().unwrap();
    // todo: graceful shutdown
    let _exit_status = serve.wait().unwrap();
    // println!("exit_status: {:?}", exit_status);

    let log: http_sh::Request = serde_json::from_str(&logline).unwrap();
    assert_eq!(log.proto, "HTTP/2.0");

    for line in loglines {
        println!("remaining stdout: {}", line.unwrap());
    }

    let mut stderr = String::new();
    let n = serve
        .stderr
        .as_mut()
        .unwrap()
        .read_to_string(&mut stderr)
        .unwrap();
    assert_eq!(0, n, "stderr is not empty: \n---\n{}---\n", stderr);
}

#[test]
fn serve_tcp() {
    let http_sh = cargo_bin("http-sh");
    let want = "Hello from server!";

    let mut serve = Command::new(http_sh)
        .arg(":0")
        .arg("--")
        .arg("printf")
        .arg(want)
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .unwrap();

    // read startup log line to ensure serve is ready
    let stdout = serve.stdout.take().unwrap();
    let mut stdout = std::io::BufReader::new(stdout);
    let mut read = String::new();
    stdout.read_line(&mut read).unwrap();
    let log: serde_json::Value = serde_json::from_str(&read).unwrap();

    let got = Command::new("curl")
        .arg("-s")
        .arg(log["address"].as_str().unwrap())
        .output()
        .unwrap();
    assert_eq!(want.as_bytes(), got.stdout);

    serve.kill().unwrap();
    // todo: graceful shutdown
    let _exit_status = serve.wait().unwrap();
    // println!("exit_status: {:?}", exit_status);

    // read remaining logs
    stdout.read_to_string(&mut read).unwrap();
    // println!("remaining logs: {}", read);

    let mut stderr = String::new();
    let n = serve
        .stderr
        .as_mut()
        .unwrap()
        .read_to_string(&mut stderr)
        .unwrap();
    assert_eq!(0, n, "stderr is not empty: \n---\n{}---\n", stderr);
}