~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
75 lines · 3.9 KB Raw History
// 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);