1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Helpers for the transports' loopback checks (atlibs-iroh's and atlibs-webrtc's tests/loopback.test.mjs):
// long-lived helpers, free ports, a throwaway sshd. A test file calls
// after(cleanup) once; what these start is stopped then.
import { execFile, spawn } from "node:child_process";
import { readFile, rm, writeFile } from "node:fs/promises";
import { createServer, connect } from "node:net";
import { join } from "node:path";
import { promisify } from "node:util";
export const ROOT = new URL("../../", import.meta.url).pathname;
export const exec = promisify(execFile);
export const which = (name) => exec("sh", ["-c", `command -v ${name}`]).then(({ stdout }) => stdout.trim(), () => null);
export const missing = async (...names) => (await Promise.all(names.map(async (n) => ((await which(n)) ? null : n)))).filter(Boolean);
// What to stop at the end: child processes, temp dirs, servers.
export const children = [];
export const temps = [];
export const servers = [];
export async function cleanup() {
for (const child of children) child.kill();
for (const server of servers) server.close();
await Promise.all(temps.map((dir) => rm(dir, { recursive: true, force: true })));
}
// Spawns a long-lived helper; resolves with the first match of `pattern` on its stdout or stderr.
export function spawnUntil(cmd, args, pattern, options = {}, ms = 15000) {
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], ...options });
children.push(child);
return new Promise((resolve, reject) => {
let seen = "";
const timer = setTimeout(() => reject(new Error(`${cmd}: no ${pattern} in ${ms} ms: ${seen}`)), ms);
const on = (chunk) => {
seen += chunk;
const match = seen.match(pattern);
if (match) clearTimeout(timer), resolve({ child, match });
};
child.stdout.on("data", on);
child.stderr.on("data", on);
child.on("exit", (code) => (clearTimeout(timer), reject(new Error(`${cmd} exited ${code}: ${seen}`))));
});
}
export const freePort = () =>
new Promise((resolve) => {
const s = createServer().listen(0, "127.0.0.1", () => {
const { port } = s.address();
s.close(() => resolve(port));
});
});
export const listening = async (port, ms = 5000) => {
for (const end = Date.now() + ms; Date.now() < end; await new Promise((r) => setTimeout(r, 50))) {
const ok = await new Promise((resolve) => connect(port, "127.0.0.1").on("connect", function () { this.end(); resolve(true); }).on("error", () => resolve(false)));
if (ok) return;
}
throw new Error(`nothing listening on 127.0.0.1:${port}`);
};
// A throwaway sshd on a random 127.0.0.1 port: a temp host key of `type`, authorized_keys in dir (written
// by the caller, read per login). An unprivileged sshd logs in only the user running it (it cannot setuid),
// so StrictModes is off for the temp files and the clients log in as this user, not production's root.
export async function sshd(dir, type) {
await exec("ssh-keygen", ["-q", "-t", type, "-N", "", "-C", "loopback-host", "-f", join(dir, "host_key")]);
const port = await freePort();
await writeFile(join(dir, "sshd_config"), [`Port ${port}`, "ListenAddress 127.0.0.1", `HostKey ${dir}/host_key`, `AuthorizedKeysFile ${dir}/authorized_keys`,
"StrictModes no", "PasswordAuthentication no", "KbdInteractiveAuthentication no", "UsePAM no", `PidFile ${dir}/sshd.pid`, ""].join("\n"));
children.push(spawn(await which("sshd"), ["-D", "-e", "-f", join(dir, "sshd_config")], { stdio: "ignore" }));
await listening(port);
return { port, hostKey: await readFile(join(dir, "host_key.pub"), "utf8") };
}
// rust/host/target/loopback/<triple>/release/<name> (`cargo loopback`, its own target dir): `name` is atlibs
// (built without iroh). ATLIBS_LOOPBACK_DIR overrides the directory.
export const loopbackBin = (hostBin, name) =>
join(process.env.ATLIBS_LOOPBACK_DIR ?? hostBin.replace("/rust/host/target/", "/rust/host/target/loopback/").replace(/\/(debug|release)\/atlibs$/, "/release"), name);