import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import { existsSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; import { OUTPUT_MAX, exchange, irohCall, irohHost, irohSession, sshArgs } from "./index.js"; // fake-bin/dumbpipe and fake-bin/ssh stand in for the real binaries (no network). const FAKE = new URL("./fake-bin/", import.meta.url).pathname; const T = "endpointabex5k47heqqkdykv2b27ydg75jjaxeucm2ej74nt34a7up6wqlkyaiaenuhi5dqom5c6l3von3tcljrfzzgk3dbpexg4mbonfzg62bonruw42zof4"; const hostKeys = [{ type: "ssh-ed25519", publicKey: "AAAAC3Bare" }]; const spawned = []; const session = irohSession({ key: "/nonexistent/key", spawn: (cmd, args, options) => (spawned.push(args), spawn(join(FAKE, cmd), args, options)) }); const all = async (stream) => { const got = []; for await (const m of stream) got.push(m); return got; }; test("a raw alpn session streams stdout, takes sends on stdin, and ends with the exit code", async () => { const s = await session({ ticket: `iroh://${T}`, alpn: "echo/1" }); const it = s[Symbol.asyncIterator](); assert.deepEqual((await it.next()).value, { stdout: "ready\n" }); await s.send("hello\n"); const rest = []; for (let n = await it.next(); !n.done; n = await it.next()) rest.push(n.value); assert.deepEqual(rest, [{ stdout: "got hello\n" }, { exitCode: 0 }]); await assert.rejects(s.send("late"), /session is closed/); }); test("ssh sessions: output as it arrives, then the exit code; ssh's own failure fails the stream", async () => { assert.deepEqual(await all(await session({ ticket: T, hostKeys, command: "uname -a" })), [{ stdout: "line 1\n" }, { stdout: "line 2\n" }, { stderr: "oops\n" }, { exitCode: 3 }]); await assert.rejects(all(await session({ ticket: T, hostKeys, command: "fail" })), /ssh failed: Host key verification failed\./); await assert.rejects(all(await session({ ticket: T, alpn: "fail/1" })), /dumbpipe exited 1: no route to endpoint/); const files = spawned.flat().filter((a) => a.startsWith("UserKnownHostsFile=")).map((a) => a.slice("UserKnownHostsFile=".length)); assert.ok(files.length >= 2 && files.every((f) => !existsSync(f)), "each session's known_hosts dir is removed when it ends"); }); test("a shell session (no command) runs until closed; bad input is refused before anything runs", async () => { const s = await session({ ticket: T, hostKeys }); const it = s[Symbol.asyncIterator](); assert.deepEqual((await it.next()).value, { stdout: "$ " }); await s.send("hello\n"); assert.deepEqual((await it.next()).value, { stdout: "hello\n$ " }); s.close(); assert.equal((await it.next()).done, true, "killed: no exitCode, a clean end"); const stop = new AbortController(); const aborted = await session({ ticket: T, hostKeys }, { signal: stop.signal }); stop.abort(); assert.deepEqual(await all(aborted), []); await assert.rejects(session({ ticket: T }), /no host keys/); await assert.rejects(session({ ticket: T, hostKeys, command: "" }), /command must be a non-empty string/); await assert.rejects(session({ ticket: "not a ticket!" }), /not an iroh ticket/); await assert.rejects(session({ ticket: T, alpn: "bad alpn" }), /alpn must be/); }); test("dumbpipe's 'using secret key' line never reaches the module; stderr is capped; a spawn failure has no exitCode", async () => { const messages = await all(await session({ ticket: T, alpn: "secret/1" })); assert.deepEqual(messages, [{ stdout: "ok\n" }, { exitCode: 0 }]); process.env.PATH = `${FAKE}:${process.env.PATH}`; // the one-shot call spawns dumbpipe from PATH const oneShot = irohHost({ key: "/nonexistent/key" }); assert.deepEqual(await oneShot({ ticket: T, alpn: "secret/1" }), { output: "ok\n" }); for (const run of [async () => all(await session({ ticket: T, alpn: "noisy/1" })), () => oneShot({ ticket: T, alpn: "noisy/1" })]) { const error = await run().then(() => assert.fail("noisy/1 should fail"), (e) => e); assert.match(error.message, /^dumbpipe exited 1: x+\nlast line$/); assert.ok(error.message.length < 16500, `stderr kept: ${error.message.length} bytes`); } const missing = irohSession({ key: "/nonexistent/key", spawn: (cmd, args, options) => spawn(join(FAKE, "no-such-binary"), args, options) }); const got = []; const failed = await (async () => { for await (const m of await missing({ ticket: T, alpn: "echo/1" })) got.push(m); })().then(() => null, (e) => e); assert.match(failed?.message ?? "", /spawn dumbpipe .*ENOENT/); assert.deepEqual(got, [], "no spurious exitCode -2 after the spawn error"); }); test("irohCall: com.fedfork.exec.iroh.connect with the host key; ssh needs the key, a raw alpn does not", async () => { const stateDir = () => mkdtempSync(join(tmpdir(), "atlibs-iroh-")); const key = join(stateDir(), "iroh_ed25519"); const ran = []; const run = async (cmd, args, input) => (ran.push({ cmd, args, input }), { stdout: "fc-1\n", stderr: "", code: 0 }); const iroh = irohCall({ key, run }); const ssh = { input: { ticket: `iroh://${T}`, hostKeys, command: "hostname" } }; await assert.rejects(iroh(ssh), /set ATLIBS_IROH_KEY or put it at .*iroh_ed25519/); assert.equal(ran.length, 0); writeFileSync(key, "not a real key\n", { mode: 0o600 }); assert.deepEqual(await iroh(ssh), { output: "fc-1\n", exitCode: 0 }); assert.equal(ran[0].cmd, "ssh"); assert.equal(ran[0].args[ran[0].args.indexOf("-i") + 1], key); // A raw ALPN exchange needs no ssh key. const raw = irohCall({ key: join(stateDir(), "missing"), run }); assert.deepEqual(await raw({ input: { ticket: T, alpn: "atirc/irc/1", send: "PING\r\n" } }), { output: "fc-1\n" }); // review 3b #7: a peer that never closes is killed at the timeout (code null); what it sent comes back, flagged. const slow = irohCall({ key: join(stateDir(), "missing"), run: async () => ({ stdout: "partial", stderr: "", code: null }) }); assert.deepEqual(await slow({ input: { ticket: T, alpn: "atirc/irc/1" } }), { output: "partial", timedOut: true }); // review 4a M3: one killed at OUTPUT_MAX says truncated (ssh and alpn), not timed out or failed const flood = irohCall({ key, run: async () => ({ stdout: "x", stderr: "", code: null, truncated: true }) }); assert.deepEqual(await flood(ssh), { output: "x", truncated: true }); assert.deepEqual(await flood({ input: { ticket: T, alpn: "atirc/irc/1" } }), { output: "x", truncated: true }); }); // review 4a M2/M3: no user ssh config, no host env (ATPROTO_*, SSH_AUTH_SOCK) in dumbpipe or ssh, stdout capped. test("children: ssh -F /dev/null, a cleared env (PATH, HOME, LANG, IROH_SECRET), at most OUTPUT_MAX of stdout", async () => { assert.deepEqual(sshArgs("/k", T, "/kh", ["true"]).slice(0, 2), ["-F", "/dev/null"]); process.env.ATPROTO_PASSWORD_TEST = "never-in-a-child"; try { const keys = (await exchange("/usr/bin/env", [])).stdout.split("\n").filter(Boolean).map((l) => l.split("=")[0]); assert.ok(keys.includes("IROH_SECRET") && keys.every((k) => ["PATH", "HOME", "LANG", "IROH_SECRET"].includes(k)), keys.join(" ")); const seen = []; const spy = irohSession({ key: "/nonexistent/key", spawn: (cmd, args, options) => (seen.push(options.env), spawn(join(FAKE, cmd), args, options)) }); await all(await spy({ ticket: T, alpn: "secret/1" })); assert.ok(!Object.keys(seen[0]).some((k) => k.startsWith("ATPROTO_")), "a session's child env"); } finally { delete process.env.ATPROTO_PASSWORD_TEST; } const flood = await exchange("sh", ["-c", "head -c 10000000 /dev/zero; sleep 5"]); assert.deepEqual([flood.stdout.length, flood.truncated, flood.code], [OUTPUT_MAX, true, null]); const exact = await exchange("sh", ["-c", `head -c ${OUTPUT_MAX} /dev/zero`]); assert.deepEqual([exact.stdout.length, exact.truncated, exact.code], [OUTPUT_MAX, undefined, 0], "exactly the cap is all of it"); });