120 lines
7.1 KiB
JavaScript
120 lines
7.1 KiB
JavaScript
// node --test relay/test/clients.test.mjs Structural checks of the clients, the playbooks and the Mac tools (no pwsh
|
|
// on the Mac: the PowerShell 5.1 parse is windows.yml's job; these catch the shapes the ledger names: X24, X25, X26,
|
|
// X27, X28, X29).
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
const read = p => readFileSync(join(ROOT, p), 'utf8');
|
|
const CLIENTS = ['relay/clients/igneum-agent.ps1', 'relay/clients/send.ps1', 'relay/clients/agent.sh', 'relay/clients/send.sh'];
|
|
const TOOLS = ['tools/relay.mjs', 'tools/console.mjs', 'tools/build-job.mjs'];
|
|
|
|
test('X24: every client and Mac tool sends x-relay-token as a header and never builds a tokened API path', () => {
|
|
for (const f of [...CLIENTS, ...TOOLS]) {
|
|
const s = read(f);
|
|
assert.match(s, /x-relay-token/, `${f} sends no x-relay-token header`);
|
|
assert.doesNotMatch(s, /\/r\/\$RelayToken\/api|\/r\/\$RELAY_TOKEN\/api|\/r\/\$\{token\b|\/r\/\$\{TOKEN\}\/(api|c)\//, `${f} still builds an API path with the token in it`);
|
|
}
|
|
// the one tokened path left is the phone's page, printed by `url`
|
|
assert.match(read('tools/relay.mjs'), /const WEB = `\$\{BASE\}\/r\/\$\{TOKEN\}`/);
|
|
assert.match(read('tools/relay.mjs'), /const API = `\$\{BASE\}\/api\/relay\?fn=`/);
|
|
});
|
|
|
|
test('X29: the shell clients hand curl its secret headers through a config file, never on the command line', () => {
|
|
for (const f of ['relay/clients/agent.sh', 'relay/clients/send.sh']) {
|
|
const s = read(f);
|
|
assert.match(s, /-K "\$HDR"/, `${f} does not use curl -K`);
|
|
assert.doesNotMatch(s, /curl[^\n]*-H "x-(igneum-key|relay-token|machine-secret):/, `${f} puts a secret header on the curl command line`);
|
|
}
|
|
});
|
|
|
|
test('X25: the agent arms the logon task only on the reboot paths and disarms on start and in finally', () => {
|
|
const s = read('relay/clients/igneum-agent.ps1');
|
|
const lines = s.split('\n');
|
|
const arms = lines.map((l, i) => [l, i]).filter(([l]) => /^\s*Arm-Restart\s*$/.test(l));
|
|
assert.equal(arms.length, 2, 'Arm-Restart is called exactly twice (the two reboot branches)');
|
|
for (const [, i] of arms) {
|
|
assert.ok(lines.slice(i, i + 4).some(l => /shutdown\.exe \/r/.test(l)), `Arm-Restart at line ${i + 1} is not followed by the restart`);
|
|
assert.ok(/^\s+/.test(lines[i]), 'Arm-Restart is never a top-level statement');
|
|
}
|
|
assert.match(s, /^Disarm-Restart$/m, 'the agent disarms at start');
|
|
assert.match(s, /finally \{[\s\S]*Disarm-Restart[\s\S]*\}/, 'the agent disarms in finally');
|
|
assert.match(s, /schtasks\.exe \/Delete \/F \/TN 'IgneumRelayAgent'/);
|
|
assert.match(s, /Remove-ItemProperty -Path \$k -Name 'IgneumRelayAgent'/);
|
|
});
|
|
|
|
test('X23: the agent checks the machine tag and the nonce before Start-Process, and refuses with exit 77', () => {
|
|
const s = read('relay/clients/igneum-agent.ps1');
|
|
const run = s.slice(s.indexOf('function Run-Task'), s.indexOf('Log ("igneum relay agent on'));
|
|
const check = run.indexOf('$why = Check-Task $task');
|
|
const start = run.indexOf('Start-Process powershell.exe');
|
|
assert.ok(check > 0 && start > check, 'Check-Task runs before Start-Process');
|
|
assert.match(run, /Post-Result \$task 77 \$log \("refused: " \+ \$why\)/);
|
|
assert.match(s, /HMACSHA256/);
|
|
assert.match(s, /igneum-relay-run\/1`nto=/);
|
|
const sh = read('relay/clients/agent.sh');
|
|
assert.ok(sh.indexOf('check_task "$it"') < sh.indexOf('bash "$STATE/tasks/task-$id.sh"'), 'agent.sh checks before it runs');
|
|
assert.match(sh, /hmac\.compare_digest/);
|
|
});
|
|
|
|
test('X28: the reboot marker must stand on its own line and the task must be queued with a reboot flag; GET inbox is never acked', () => {
|
|
const ps = read('relay/clients/igneum-agent.ps1');
|
|
assert.match(ps, /\(\?m\)\^RELAY-REBOOT\\r\?\$/);
|
|
assert.match(ps, /\$reboot = \$asked -and \$rebootAllowed/);
|
|
assert.match(ps, /Api-Post 'inbox' @\{ machine = \$script:Machine; kind = 'run'; ack = \$true \}/);
|
|
assert.doesNotMatch(ps, /inbox\?machine=[^\n]*ack=1/);
|
|
const sh = read('relay/clients/agent.sh');
|
|
assert.match(sh, /grep -qx 'RELAY-REBOOT' "\$logf" && \[ -n "\$rebootok" \]/);
|
|
assert.doesNotMatch(sh, /inbox\?machine=[^\n]*ack=1/);
|
|
for (const f of ['relay/clients/send.ps1', 'relay/clients/send.sh']) assert.doesNotMatch(read(f), /inbox\?machine=[^\n]*ack=1/, `${f} acks through a GET`);
|
|
});
|
|
|
|
test('X28: the registration carries no username and no folder', () => {
|
|
const ps = read('relay/clients/igneum-agent.ps1');
|
|
const info = ps.slice(ps.indexOf('function Collect-Info'), ps.indexOf('function Register-Machine'));
|
|
assert.doesNotMatch(info, /user = |dir = /);
|
|
const sh = read('relay/clients/agent.sh');
|
|
assert.doesNotMatch(sh, /"user":|"dir":/);
|
|
});
|
|
|
|
test('X26: no playbook carries __DL_BASE__ or prints the download URL; the agents hand the base over as RELAY_DL_BASE', () => {
|
|
for (const f of readdirSync(join(ROOT, 'relay/playbooks'))) {
|
|
const s = read(`relay/playbooks/${f}`);
|
|
if (/substituted at publish time/.test(s)) continue; // a jobs-channel body (packaging/ota/publish-jobs.sh fills it), never a relay run
|
|
assert.doesNotMatch(s, /__DL_BASE__/, `${f} still uses __DL_BASE__`);
|
|
assert.doesNotMatch(s, /Write-Host "downloading \$zipUrl"/, `${f} prints the tokened URL`);
|
|
}
|
|
assert.match(read('relay/clients/igneum-agent.ps1'), /\$env:RELAY_DL_BASE = '\$DlBase'/);
|
|
assert.match(read('relay/clients/agent.sh'), /export RELAY_DL_BASE=/);
|
|
const mjs = read('tools/relay.mjs');
|
|
assert.doesNotMatch(mjs, /replace\(\/__DL_BASE__\/g/, 'tools/relay.mjs still substitutes the dl base into bodies');
|
|
assert.match(mjs, /carries the dl token; it must never be in a task body/);
|
|
});
|
|
|
|
test('X27: results and registration carry the machine secret header; make-clients.sh bakes it per machine', () => {
|
|
for (const f of CLIENTS) assert.match(read(f), /x-machine-secret/, `${f} never presents the machine secret`);
|
|
const mk = read('relay/clients/make-clients.sh');
|
|
assert.match(mk, /--machine\) MACHINE=/);
|
|
assert.match(mk, /machine-secret\.txt/);
|
|
assert.match(mk, /__DL_BASE__#\$DL_BASE#g/);
|
|
});
|
|
|
|
test('X28: the WSL playbook grants sudo for apt-get and dpkg only, and no password travels on a command line', () => {
|
|
const w = read('relay/playbooks/wsl-setup.ps1');
|
|
assert.doesNotMatch(w, /NOPASSWD:ALL/);
|
|
assert.match(w, /NOPASSWD:SETENV: \/usr\/bin\/apt-get, \/usr\/bin\/dpkg/);
|
|
assert.doesNotMatch(read('relay/playbooks/prover-setup.ps1'), /echo igneum \| sudo -S/);
|
|
});
|
|
|
|
test('PowerShell shape: balanced braces and here-strings in the two .ps1 clients (the 5.1 parse runs in windows.yml)', () => {
|
|
for (const f of ['relay/clients/igneum-agent.ps1', 'relay/clients/send.ps1']) {
|
|
const s = read(f);
|
|
const open = (s.match(/\{/g) || []).length; const close = (s.match(/\}/g) || []).length;
|
|
assert.equal(open, close, `${f}: ${open} { against ${close} }`);
|
|
assert.equal((s.match(/@"\s*$/gm) || []).length, (s.match(/^"@\s*$/gm) || []).length, `${f}: here-string markers`);
|
|
assert.doesNotMatch(s, /\$[A-Za-z_]+:\s[a-z]/, `${f}: a "$name: text" drive-qualified reference (the 5.1 class of 4 October)`);
|
|
}
|
|
});
|