102 lines
3.6 KiB
TypeScript
102 lines
3.6 KiB
TypeScript
/**
|
|
* Regression test for MITM cert install in Docker `USER node` (non-root, no sudo).
|
|
*
|
|
* OmniRoute's runtime Docker image (`Dockerfile`) runs as `USER node` (UID 1000)
|
|
* and the slim base (`node:24-trixie-slim`) does NOT ship `sudo`. The previous
|
|
* `execFileWithPassword("sudo", ["-S", ...])` would spawn `sudo` unconditionally
|
|
* when not root, producing `spawn sudo ENOENT` and breaking `installCert` /
|
|
* `addDNSEntries` for any MITM operation triggered from inside the container.
|
|
*
|
|
* The fix:
|
|
* - `isSudoAvailable()` probes `PATH` for `sudo`.
|
|
* - `execFileWithPassword` when invoked with `command === "sudo"` and sudo is
|
|
* NOT available and the process is NOT root, strips the leading `sudo -S`
|
|
* flags and runs the underlying command directly (same user, no elevation).
|
|
* - Callers that absolutely require elevation (e.g. installing a CA to the
|
|
* system trust store) can pre-check with `isSudoAvailable()` and skip with
|
|
* a clear log instead of throwing.
|
|
*
|
|
* This mirrors upstream behavior (decolua/9router) extended to OmniRoute's TS
|
|
* fork and OmniRoute's existing root-detection (`isRoot()`).
|
|
*/
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
execFileWithPassword,
|
|
isRoot,
|
|
isSudoAvailable,
|
|
} from "../../src/mitm/systemCommands.ts";
|
|
|
|
test("isSudoAvailable: returns a boolean (probe shape)", () => {
|
|
const result = isSudoAvailable();
|
|
assert.equal(typeof result, "boolean");
|
|
});
|
|
|
|
test(
|
|
"execFileWithPassword: when command is 'sudo' but sudo is missing, falls back to direct exec (no ENOENT)",
|
|
async () => {
|
|
if (isRoot()) {
|
|
// Root path is already handled by an earlier branch — skip.
|
|
return;
|
|
}
|
|
|
|
if (isSudoAvailable()) {
|
|
// We cannot exercise the no-sudo path on a host that has sudo on PATH.
|
|
// The dedicated unit test below covers it by isolating PATH.
|
|
return;
|
|
}
|
|
|
|
// Use a no-op binary that succeeds without requiring privileges.
|
|
// After stripping `sudo -S`, the underlying command must be `true` (or any
|
|
// executable in PATH that exits 0).
|
|
const result = await execFileWithPassword(
|
|
"sudo",
|
|
["-S", "true"],
|
|
"fake-password"
|
|
);
|
|
assert.equal(typeof result, "string");
|
|
}
|
|
);
|
|
|
|
test(
|
|
"execFileWithPassword: with empty PATH so sudo is undiscoverable, falls back gracefully",
|
|
async () => {
|
|
if (isRoot()) {
|
|
// Root branch strips sudo upstream — covered separately.
|
|
return;
|
|
}
|
|
|
|
// Force the discovery probe to find no sudo by clearing PATH temporarily.
|
|
// We do this only around the probe call inside `execFileWithPassword`,
|
|
// by stubbing the env. Restore on test exit.
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = "/__no_such_dir__";
|
|
try {
|
|
// After fallback, the bare command must be a valid executable. We use
|
|
// the current Node binary with `process.exit(0)` so it is portable
|
|
// regardless of which utilities (`true`, `:`) exist in the path.
|
|
const result = await execFileWithPassword(
|
|
"sudo",
|
|
["-S", process.execPath, "-e", "process.exit(0)"],
|
|
"fake-password"
|
|
);
|
|
assert.equal(typeof result, "string");
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"execFileWithPassword: a non-sudo command is unaffected by the fallback path",
|
|
async () => {
|
|
// Direct invocation of a known-good binary must still work end-to-end.
|
|
const result = await execFileWithPassword(
|
|
process.execPath,
|
|
["-e", "process.exit(0)"],
|
|
"" // no password needed
|
|
);
|
|
assert.equal(typeof result, "string");
|
|
}
|
|
);
|