import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs'; import { after as laneAfter } from 'node:test'; import { testerRequest } from '../../cli/src/tester-http.mjs'; import test from 'node:test'; import assert from 'node:assert/strict'; import { spawn, spawnSync } from 'node:child_process'; import { once } from 'node:events'; import { createServer } from 'node:net'; import { request } from 'node:http'; import { testerFixture } from './tester-fixture.mjs'; import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, chmodSync, symlinkSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const CLI = new URL('../src/cli.mjs', import.meta.url).pathname; const VALUE = 'fixture_proxy_proof_0123456789abcdef_0123456789'; const PREVIEW = ['--public-host', 'console.example', '--auth-header', 'x-user', '--allow-user', 'jason']; function fixture(t) { const root = mkdtempSync(join(tmpdir(), 'proof-cli-')); t.after(() => rmSync(root, { recursive: true, force: true })); const file = join(root, 'fixture.token'); writeFileSync(file, VALUE + '\n', { mode: 0o600 }); return { root, file, env: { ...process.env, MOSAIC_CONFIG: join(root, 'missing-config.json') } }; } test('proof startup refuses partial flags non-preview and invalid fixture file metadata or bytes', t => { const f = fixture(t), proof = ['--proof-header', 'x-console-proof', '--proof-file', f.file]; const sets = [[...PREVIEW, '--proof-header', 'x-console-proof'], [...PREVIEW, '--proof-file', f.file], proof]; const missing = join(f.root, 'missing'), dir = join(f.root, 'directory'), link = join(f.root, 'link'); mkdirSync(dir); symlinkSync(f.file, link); for (const file of [missing, dir, link]) sets.push([...PREVIEW, '--proof-header', 'x-console-proof', '--proof-file', file]); const cases = [ ...sets.map(flags => ({ flags })), ...[['mode', VALUE, 0o644], ['short', 'short_fixture', 0o600], ['newline', VALUE.slice(0, 20) + '\n' + VALUE.slice(20), 0o600], ['bad', VALUE + '!', 0o600], ['two-final-lines', VALUE + '\n\n', 0o600]].map(([name, value, mode]) => { const file = join(f.root, name); writeFileSync(file, value, { mode }); chmodSync(file, mode); // umask-proof (row 243) return { flags: [...PREVIEW, '--proof-header', 'x-console-proof', '--proof-file', file], value }; }), ]; // Simulate another uid via the fixture child's fstat; never chown a host file. const hook = join(f.root, 'other-owner.mjs'); writeFileSync(hook, `import fs from 'node:fs';import {syncBuiltinESMExports} from 'node:module';const original=fs.fstatSync;fs.fstatSync=(...args)=>{const s=original(...args);s.uid=process.getuid()+1;return s;};syncBuiltinESMExports();`); cases.push({ flags: [...PREVIEW, ...proof], env: { ...f.env, NODE_OPTIONS: `--import=${hook}` } }); for (const c of cases) { const r = spawnSync(process.execPath, [CLI, 'serve', '--port', '0', '--board', 'http://127.0.0.1:9', ...c.flags], { env: c.env ?? f.env, encoding: 'utf8', timeout: 5000 }); assert.equal(r.status, 2, 'invalid proof startup must refuse'); assert.match(r.stderr, /proof/i); assert.ok(!r.stderr.includes(VALUE)); assert.ok(!r.stdout.includes(VALUE)); if (c.value) assert.ok(!r.stderr.includes(c.value)); } }); test('serve argv carries only fixture proof path and header; startup explicitly reports proof on or off', async t => { const f = fixture(t); for (const on of [true, false]) { const child = spawn(process.execPath, [CLI, 'serve', '--port', '0', '--board', 'http://127.0.0.1:9', ...PREVIEW, ...(on ? ['--proof-header', 'x-console-proof', '--proof-file', f.file] : [])], { env: f.env, stdio: ['ignore', 'pipe', 'pipe'] }); let out = '', err = ''; child.stdout.on('data', c => out += c); child.stderr.on('data', c => err += c); t.after(async () => { if (child.exitCode === null && child.signalCode === null) { child.kill('SIGTERM'); await once(child, 'close'); } }); await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(Error('fixture CLI startup timeout')), 5000); const check = () => { if (out.includes('Ctrl-C')) { clearTimeout(timer); resolve(); } }; child.stdout.on('data', check); child.once('exit', () => { clearTimeout(timer); reject(Error('fixture CLI exited before startup')); }); check(); }); assert.match(out, new RegExp(`proxy proof ${on ? 'on' : 'off'}`)); const cmdline = readFileSync(`/proc/${child.pid}/cmdline`, 'utf8'); assert.ok(!cmdline.includes(VALUE)); assert.ok(!out.includes(VALUE)); assert.ok(!err.includes(VALUE)); if (on) { assert.ok(cmdline.includes(f.file)); assert.ok(cmdline.includes('--proof-header')); } child.kill('SIGTERM'); await once(child, 'close'); } }); test('CLI applies proof on the main listener and rejects that header on its tester listener', async t => { const f = await testerFixture(t), file = join(f.root, 'proof.fixture'); writeFileSync(file, VALUE, { mode: 0o600 }); await new Promise(r=>f.web.close(r)); const child = spawn(process.execPath, [CLI, 'serve', '--port', '0', '--board', 'http://127.0.0.1:9', '--tester-socket', '--business', 'acme', ...PREVIEW, '--proof-header', 'x-console-proof', '--proof-file', file], { env: f.f.env, stdio: ['ignore', 'pipe', 'pipe'] }); let output = ''; child.stdout.on('data', c => output += c); child.stderr.resume(); t.after(async () => { if (child.exitCode === null && child.signalCode === null) { child.kill('SIGTERM'); await once(child, 'close'); } }); await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(Error('fixture tester startup timeout')), 5000); const check = () => { if (output.includes('Ctrl-C')) { clearTimeout(timer); resolve(); } }; child.stdout.on('data', check); child.once('exit', () => { clearTimeout(timer); reject(Error('fixture tester startup refused')); }); check(); }); const port = Number(output.match(/Mosaic Console: http:\/\/127\.0\.0\.1:(\d+)/)[1]); const get = (port, headers) => new Promise((resolve, reject) => { const q = request({ host: '127.0.0.1', port, path: '/api/config', headers }, r => { let text = ''; r.on('data', c => text += c); r.on('end', () => resolve({ status: r.statusCode, text })); }); q.on('error', reject); q.end(); }); assert.equal((await get(port, { host: 'console.example', 'x-user': 'jason' })).status, 401); const good = await get(port, { host: 'console.example', 'x-user': 'jason', 'x-console-proof': VALUE }); assert.equal(good.status, 200); assert.ok(!good.text.includes(VALUE)); assert.equal((await testerRequest({dataRoot:f.f.dataRoot,path:'/api/config',headers:{'x-console-proof':VALUE}})).status, 403); });