#![allow(clippy::unwrap_used)]
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use std::process::{Command, Output, Stdio};
use mkit_core::layout::RepoLayout;
use mkit_core::ops::reachable_objects;
use mkit_core::refs;
use mkit_core::store::ObjectStore;
fn mkit_bin() -> &'static str {
env!("CARGO_BIN_EXE_mkit")
}
fn run_in(cwd: &Path, xdg: &Path, args: &[&str], extra_env: &[(&str, &str)]) -> Output {
let mut cmd = Command::new(mkit_bin());
cmd.args(args)
.current_dir(cwd)
.env("XDG_CONFIG_HOME", xdg)
.env("HOME", xdg)
.env("EDITOR", "true")
.env("VISUAL", "true")
.stdin(Stdio::null());
for (k, v) in extra_env {
cmd.env(k, v);
}
cmd.output().expect("spawn mkit")
}
fn write_ssh_wrapper(dir: &Path) -> std::path::PathBuf {
let wrapper = dir.join("fake_ssh.sh");
let script = r#"#!/bin/sh
set -eu
# Record the full argv this wrapper was invoked with, one token per line,
# framed by a record separator so the test can scan a single invocation.
{
echo "=== ssh invocation ==="
for a in "$@"; do
printf '%s\n' "$a"
done
} >> "$MKIT_SSH_ARGV_LOG"
# Find the `mkit serve <path>` triple at the tail of the argv and exec it
# locally. We walk forward to the literal `mkit` token; everything after
# `mkit serve` is the path.
found=0
path=""
for a in "$@"; do
if [ "$found" = 2 ]; then
path="$a"
break
fi
if [ "$found" = 1 ] && [ "$a" = serve ]; then
found=2
continue
fi
if [ "$a" = mkit ]; then
found=1
fi
done
if [ -z "$path" ]; then
echo "fake_ssh: could not locate 'mkit serve <path>' in argv" >&2
exit 64
fi
exec "$MKIT_SSH_TARGET_BIN" serve "$path"
"#;
fs::write(&wrapper, script).expect("write wrapper");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&wrapper).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&wrapper, perms).unwrap();
}
wrapper
}
fn build_source(root: &Path, xdg: &Path) -> std::path::PathBuf {
let source = root.join("source");
fs::create_dir_all(&source).unwrap();
assert!(
run_in(&source, xdg, &["init"], &[]).status.success(),
"init source"
);
assert!(
run_in(&source, xdg, &["keygen"], &[]).status.success(),
"keygen source"
);
fs::write(source.join("README.md"), b"# e2e ssh project\n").unwrap();
fs::create_dir_all(source.join("src")).unwrap();
fs::write(
source.join("src/main.rs"),
b"fn main() { println!(\"hi\"); }\n",
)
.unwrap();
assert!(
run_in(&source, xdg, &["add", "."], &[]).status.success(),
"add source"
);
let commit = run_in(&source, xdg, &["commit", "-m", "e2e-1"], &[]);
assert!(
commit.status.success(),
"commit source: {}",
String::from_utf8_lossy(&commit.stderr)
);
source
}
fn build_and_push_source(root: &Path, xdg: &Path) -> std::path::PathBuf {
let source = build_source(root, xdg);
let remote = root.join("remote");
fs::create_dir_all(&remote).unwrap();
assert!(
run_in(&remote, xdg, &["init"], &[]).status.success(),
"init remote"
);
let remote_url = format!("mkit+file://{}", remote.display());
assert!(
run_in(&source, xdg, &["remote", "add", "origin", &remote_url], &[])
.status
.success(),
"remote add"
);
let push = run_in(&source, xdg, &["push", "origin"], &[]);
assert!(
push.status.success(),
"push source -> remote: {}",
String::from_utf8_lossy(&push.stderr)
);
remote
}
#[test]
fn ssh_clone_moves_data_and_delivers_pinned_options() {
if cfg!(not(unix)) {
eprintln!("ssh_e2e: skipped (requires a POSIX shell)");
return;
}
let work = tempfile::tempdir().expect("work tempdir");
let xdg = tempfile::tempdir().expect("xdg tempdir");
let root = work.path();
let remote = build_and_push_source(root, xdg.path());
let source = root.join("source");
let source_tip = refs::read_ref(&RepoLayout::single(&source), "main")
.unwrap()
.expect("source has refs/heads/main");
let known_hosts = root.join("project.known_hosts");
fs::write(&known_hosts, b"# pinned known hosts for e2e\n").unwrap();
let identity = root.join("id_ed25519_e2e");
fs::write(&identity, b"-----BEGIN OPENSSH PRIVATE KEY-----\nfake\n").unwrap();
let strict_value = "yes";
for (key, val) in [
("ssh.strict_host_key_checking", strict_value),
("ssh.user_known_hosts_file", known_hosts.to_str().unwrap()),
("ssh.identity_file", identity.to_str().unwrap()),
] {
let out = run_in(root, xdg.path(), &["config", key, val], &[]);
assert!(
out.status.success(),
"set {key}: {}",
String::from_utf8_lossy(&out.stderr)
);
}
let argv_log = root.join("ssh_argv.log");
let wrapper = write_ssh_wrapper(root);
let remote_path = remote.to_str().unwrap();
let url = format!("mkit+ssh://e2euser@localhost{remote_path}");
let dest = root.join("dest");
let dest_str = dest.to_str().unwrap();
let clone = run_in(
root,
xdg.path(),
&["clone", &url, dest_str],
&[
("MKIT_SSH_PROGRAM", wrapper.to_str().unwrap()),
("MKIT_SSH_ARGV_LOG", argv_log.to_str().unwrap()),
("MKIT_SSH_TARGET_BIN", mkit_bin()),
],
);
assert!(
clone.status.success(),
"clone over ssh must succeed; stderr:\n{}",
String::from_utf8_lossy(&clone.stderr)
);
let dest_tip = refs::read_ref(&RepoLayout::single(&dest), "main")
.unwrap()
.expect("cloned repo has refs/heads/main");
assert_eq!(
source_tip, dest_tip,
"cloned HEAD must equal source HEAD (real ref negotiation)"
);
let source_store = ObjectStore::open(&RepoLayout::single(&source)).unwrap();
let dest_store = ObjectStore::open(&RepoLayout::single(&dest)).unwrap();
let source_set: HashSet<_> = reachable_objects(&source_store, &source_tip)
.unwrap()
.into_iter()
.collect();
let dest_set: HashSet<_> = reachable_objects(&dest_store, &dest_tip)
.unwrap()
.into_iter()
.collect();
assert_eq!(
source_set, dest_set,
"object closures must match — proves real data moved through the transport"
);
assert!(
source_set.len() >= 4,
"closure must include >= commit + tree + 2 blobs"
);
let log = fs::read_to_string(&argv_log).expect("argv log written by wrapper");
let lines: Vec<&str> = log.lines().collect();
assert!(
has_pair(
&lines,
"-o",
&format!("StrictHostKeyChecking={strict_value}")
),
"ssh argv missing StrictHostKeyChecking; log:\n{log}"
);
assert!(
has_pair(
&lines,
"-o",
&format!("UserKnownHostsFile={}", known_hosts.display())
),
"ssh argv missing UserKnownHostsFile; log:\n{log}"
);
assert!(
has_pair(&lines, "-i", &identity.display().to_string()),
"ssh argv missing identity file; log:\n{log}"
);
}
fn has_pair(lines: &[&str], flag: &str, value: &str) -> bool {
lines.windows(2).any(|w| w[0] == flag && w[1] == value)
}
fn hex(byte: char) -> String {
std::iter::repeat_n(byte, 64).collect()
}
fn ns(byte: char) -> String {
format!("ed25519-{}", hex(byte))
}
fn write_root_ssh_wrapper(dir: &Path) -> std::path::PathBuf {
let wrapper = dir.join("fake_ssh_root.sh");
let script = r#"#!/bin/sh
set -eu
{ echo "=== ssh invocation ==="; for a in "$@"; do printf '%s\n' "$a"; done; } >> "$MKIT_SSH_ARGV_LOG"
found=0
path=""
for a in "$@"; do
if [ "$found" = 2 ]; then
path="$a"
break
fi
if [ "$found" = 1 ] && [ "$a" = serve ]; then
found=2
continue
fi
if [ "$a" = mkit ]; then
found=1
fi
done
if [ -z "$path" ]; then
echo "fake_ssh_root: could not locate 'mkit serve <path>' in argv" >&2
exit 64
fi
# `MKIT_SSH_SERVE_ENV=1`: hand the path over SSH_ORIGINAL_COMMAND the way
# sshd does, instead of as a positional argument.
serve_env=${MKIT_SSH_SERVE_ENV:-}
if [ -n "$serve_env" ]; then
SSH_ORIGINAL_COMMAND="mkit serve $path"
export SSH_ORIGINAL_COMMAND
path=""
fi
if [ -n "${MKIT_SSH_PRINCIPAL:-}" ]; then
if [ -n "$path" ]; then
exec "$MKIT_SSH_TARGET_BIN" serve --root "$MKIT_SSH_ROOT" --principal "$MKIT_SSH_PRINCIPAL" "$path"
fi
exec "$MKIT_SSH_TARGET_BIN" serve --root "$MKIT_SSH_ROOT" --principal "$MKIT_SSH_PRINCIPAL"
elif [ -n "$path" ]; then
exec "$MKIT_SSH_TARGET_BIN" serve --root "$MKIT_SSH_ROOT" "$path"
else
exec "$MKIT_SSH_TARGET_BIN" serve --root "$MKIT_SSH_ROOT"
fi
"#;
fs::write(&wrapper, script).expect("write root wrapper");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&wrapper).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&wrapper, perms).unwrap();
}
wrapper
}
fn serve_root_with_rooms(
work: &Path,
xdg: &Path,
owner_ns: &str,
rooms: &[&str],
) -> std::path::PathBuf {
let serve_root = work.join("serve-root");
for room in rooms {
let dir = serve_root.join(owner_ns).join(room);
fs::create_dir_all(&dir).unwrap();
let out = run_in(&dir, xdg, &["init"], &[]);
assert!(
out.status.success(),
"init {room}: {}",
String::from_utf8_lossy(&out.stderr)
);
}
serve_root
}
fn root_env(
wrapper: &Path,
serve_root: &Path,
principal: Option<&str>,
env_path: bool,
) -> Vec<(String, String)> {
let mut env = vec![
(
"MKIT_SSH_PROGRAM".to_owned(),
wrapper.to_str().unwrap().to_owned(),
),
(
"MKIT_SSH_ARGV_LOG".to_owned(),
wrapper
.parent()
.unwrap()
.join("ssh_argv.log")
.to_str()
.unwrap()
.to_owned(),
),
("MKIT_SSH_TARGET_BIN".to_owned(), mkit_bin().to_owned()),
(
"MKIT_SSH_ROOT".to_owned(),
serve_root.to_str().unwrap().to_owned(),
),
];
if let Some(key) = principal {
env.push(("MKIT_SSH_PRINCIPAL".to_owned(), key.to_owned()));
}
if env_path {
env.push(("MKIT_SSH_SERVE_ENV".to_owned(), "1".to_owned()));
}
env
}
fn push_over_ssh(source: &Path, xdg: &Path, url: &str, env: &[(String, String)]) -> Output {
let env: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
let add = run_in(source, xdg, &["remote", "add", "origin", url], &env);
assert!(
add.status.success(),
"remote add: {}",
String::from_utf8_lossy(&add.stderr)
);
run_in(source, xdg, &["push", "origin"], &env)
}
#[test]
fn ssh_root_mode_owner_pushes_and_clones() {
if cfg!(not(unix)) {
eprintln!("ssh_root_mode: skipped (requires a POSIX shell)");
return;
}
let work = tempfile::tempdir().expect("work tempdir");
let xdg = tempfile::tempdir().expect("xdg tempdir");
let root = work.path();
let owner_ns = ns('a');
let serve_root = serve_root_with_rooms(root, xdg.path(), &owner_ns, &["room-a"]);
let source = build_source(root, xdg.path());
let wrapper = write_root_ssh_wrapper(root);
let env = root_env(&wrapper, &serve_root, Some(&hex('a')), true);
let url = format!("mkit+ssh://e2euser@localhost/{owner_ns}/room-a");
let push = push_over_ssh(&source, xdg.path(), &url, &env);
assert!(
push.status.success(),
"owner push must succeed; stderr:\n{}",
String::from_utf8_lossy(&push.stderr)
);
let dest = root.join("dest");
let clone = run_in(
root,
xdg.path(),
&["clone", &url, dest.to_str().unwrap()],
&env.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect::<Vec<_>>(),
);
assert!(
clone.status.success(),
"clone over ssh must succeed; stderr:\n{}",
String::from_utf8_lossy(&clone.stderr)
);
let source_tip = refs::read_ref(&RepoLayout::single(&source), "main")
.unwrap()
.expect("source has refs/heads/main");
let dest_tip = refs::read_ref(&RepoLayout::single(&dest), "main")
.unwrap()
.expect("cloned repo has refs/heads/main");
assert_eq!(source_tip, dest_tip, "clone must reconstruct the push");
}
#[test]
fn ssh_root_mode_non_owner_push_is_refused() {
if cfg!(not(unix)) {
eprintln!("ssh_root_mode: skipped (requires a POSIX shell)");
return;
}
let work = tempfile::tempdir().expect("work tempdir");
let xdg = tempfile::tempdir().expect("xdg tempdir");
let root = work.path();
let owner_ns = ns('a');
let serve_root = serve_root_with_rooms(root, xdg.path(), &owner_ns, &["room-a"]);
let source = build_source(root, xdg.path());
let wrapper = write_root_ssh_wrapper(root);
let env = root_env(&wrapper, &serve_root, Some(&hex('b')), false);
let url = format!("mkit+ssh://e2euser@localhost/{owner_ns}/room-a");
let push = push_over_ssh(&source, xdg.path(), &url, &env);
assert!(!push.status.success(), "non-owner push must fail: {push:?}");
let stderr = String::from_utf8_lossy(&push.stderr);
assert!(
stderr.contains("write not permitted"),
"the pinned refusal reaches the client: {stderr}"
);
let dest = root.join("dest");
let clone = run_in(
root,
xdg.path(),
&["clone", &url, dest.to_str().unwrap()],
&env.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect::<Vec<_>>(),
);
assert!(clone.status.success(), "{clone:?}");
assert!(
refs::read_ref(&RepoLayout::single(&dest), "main")
.unwrap()
.is_none(),
"a denied push leaves the repo with no refs"
);
}
#[test]
fn ssh_root_mode_without_principal_reads_but_cannot_write() {
if cfg!(not(unix)) {
eprintln!("ssh_root_mode: skipped (requires a POSIX shell)");
return;
}
let work = tempfile::tempdir().expect("work tempdir");
let xdg = tempfile::tempdir().expect("xdg tempdir");
let root = work.path();
let owner_ns = ns('a');
let serve_root = serve_root_with_rooms(root, xdg.path(), &owner_ns, &["room-a"]);
let source = build_source(root, xdg.path());
let wrapper = write_root_ssh_wrapper(root);
let url = format!("mkit+ssh://e2euser@localhost/{owner_ns}/room-a");
let owner_env = root_env(&wrapper, &serve_root, Some(&hex('a')), false);
let push = push_over_ssh(&source, xdg.path(), &url, &owner_env);
assert!(
push.status.success(),
"owner push: {}",
String::from_utf8_lossy(&push.stderr)
);
let anon_env = root_env(&wrapper, &serve_root, None, false);
let anon: Vec<(&str, &str)> = anon_env
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let dest = root.join("dest");
let clone = run_in(
root,
xdg.path(),
&["clone", &url, dest.to_str().unwrap()],
&anon,
);
assert!(
clone.status.success(),
"a keyless principal may read: {}",
String::from_utf8_lossy(&clone.stderr)
);
let second = root.join("source2");
fs::create_dir_all(&second).unwrap();
assert!(run_in(&second, xdg.path(), &["init"], &[]).status.success());
assert!(
run_in(&second, xdg.path(), &["keygen"], &[])
.status
.success()
);
fs::write(second.join("f.txt"), b"nope\n").unwrap();
assert!(
run_in(&second, xdg.path(), &["add", "."], &[])
.status
.success()
);
assert!(
run_in(&second, xdg.path(), &["commit", "-m", "x"], &[])
.status
.success()
);
let push = push_over_ssh(&second, xdg.path(), &url, &anon_env);
assert!(!push.status.success(), "a keyless push must fail: {push:?}");
assert!(
String::from_utf8_lossy(&push.stderr).contains("write not permitted"),
"{}",
String::from_utf8_lossy(&push.stderr)
);
}
#[test]
fn ssh_root_mode_isolates_repositories() {
if cfg!(not(unix)) {
eprintln!("ssh_root_mode: skipped (requires a POSIX shell)");
return;
}
let work = tempfile::tempdir().expect("work tempdir");
let xdg = tempfile::tempdir().expect("xdg tempdir");
let root = work.path();
let owner_ns = ns('a');
let serve_root = serve_root_with_rooms(root, xdg.path(), &owner_ns, &["room-a", "room-b"]);
let source = build_source(root, xdg.path());
let wrapper = write_root_ssh_wrapper(root);
let env = root_env(&wrapper, &serve_root, Some(&hex('a')), false);
let url_a = format!("mkit+ssh://e2euser@localhost/{owner_ns}/room-a");
let url_b = format!("mkit+ssh://e2euser@localhost/{owner_ns}/room-b");
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
let push = push_over_ssh(&source, xdg.path(), &url_a, &env);
assert!(
push.status.success(),
"push to room-a: {}",
String::from_utf8_lossy(&push.stderr)
);
let dest_b = root.join("dest-b");
let clone = run_in(
root,
xdg.path(),
&["clone", &url_b, dest_b.to_str().unwrap()],
&env_refs,
);
assert!(
clone.status.success(),
"clone room-b: {}",
String::from_utf8_lossy(&clone.stderr)
);
assert!(
refs::read_ref(&RepoLayout::single(&dest_b), "main")
.unwrap()
.is_none(),
"room-b must not see room-a's refs/heads/main"
);
}