// Row 178: fixture proof values only. No installed proof, proxy or unit is read. import test from 'node:test'; import assert from 'node:assert/strict'; import { createServer, request } from 'node:http'; import { mkdtempSync, writeFileSync, chmodSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import * as serve from '../src/serve.mjs'; import { close } from './fixture.mjs'; import { startTesterSocket } from '../src/tester-socket.mjs'; import { testerRequest } from '../../cli/src/tester-http.mjs'; const HOST = 'console.example', HEADER = 'x-console-proof', VALUE = 'fixture_proxy_proof_0123456789abcdef_0123456789'; const preview = { host: HOST, header: 'x-user', user: 'jason' }; function ask(port, { host = HOST, headers = {}, path = '/api/bus/tasks', method = 'GET', body } = {}) { return new Promise((resolve, reject) => { const q = request({ host: '127.0.0.1', port, path, method, headers: Object.fromEntries(Object.entries({ host, 'x-user': 'jason', ...headers }).filter(([, value]) => value !== undefined)) }, r => { let text = ''; r.on('data', c => text += c); r.on('end', () => resolve({ status: r.statusCode, text, body: JSON.parse(text) })); }); q.on('error', reject); q.end(body); }); } async function setup(t) { const root = mkdtempSync(join(tmpdir(), 'proof-http-')), file = join(root, 'fixture.token'); t.after(() => rmSync(root, { recursive: true, force: true })); writeFileSync(file, VALUE + '\n', { mode: 0o600 }); const proof = serve.proofOptions({ proofHeader: HEADER, proofFile: file }, preview); const calls = [], logs = []; const board = createServer((q, r) => { calls.push('board'); r.setHeader('content-type', 'application/json'); r.end('{"sessions":[]}'); }); await new Promise(r => board.listen(0, '127.0.0.1', r)); t.after(() => close(board)); const server = await serve.startServer({ port: 0, preview, proof, board: `http://127.0.0.1:${board.address().port}`, bus: { tasks: async () => { calls.push('tasks'); return []; } }, log: line => logs.push(line) }); t.after(() => close(server)); return { port: server.address().port, calls, logs, proof, file }; } test('proxy proof admits named reads but preview remains read-only, reads its fixture once, and never exposes the value', async t => { const f = await setup(t), headers = { [HEADER]: VALUE }; writeFileSync(f.file, 'changed_fixture_value_012345678901234567890'); assert.equal((await ask(f.port, { headers })).status, 200); assert.equal((await ask(f.port, { headers, path: '/api/board' })).status, 200); const sent = await ask(f.port, { headers: { ...headers, origin: `https://${HOST}`, 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' }, method: 'POST', path: '/api/bus/message', body: JSON.stringify({ to: 'coder', body: 'fixture' }) }); assert.equal(sent.status, 403); assert.equal(sent.body.error, 'read-only'); assert.deepEqual(f.calls, ['tasks', 'board']); const config = await ask(f.port, { headers, path: '/api/config' }); assert.equal(config.status, 200); assert.ok(!config.text.includes(VALUE)); assert.ok(!f.logs.join('\n').includes(VALUE)); }); test('proxy proof refuses missing wrong short or repeated proof before auth and before any read', async t => { const f = await setup(t); for (const headers of [{}, { [HEADER]: 'x'.repeat(VALUE.length) }, { [HEADER]: VALUE.slice(1) }, { [HEADER]: [VALUE, VALUE] }]) { for (const path of ['/api/bus/tasks', '/api/board']) { const result = await ask(f.port, { headers, path }); assert.equal(result.status, 401); assert.deepEqual(result.body, { error: 'proxy proof required' }); assert.ok(!result.text.includes(VALUE)); } } // Writes are fenced by the same proof check, before the human send callback. for (const headers of [{}, { [HEADER]: 'x'.repeat(VALUE.length) }, { [HEADER]: VALUE.slice(1) }, { [HEADER]: [VALUE, VALUE] }]) { const result = await ask(f.port, { headers: { ...headers, origin: `https://${HOST}`, 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' }, path: '/api/bus/message', method: 'POST', body: JSON.stringify({ to: 'coder', body: 'fixture' }) }); assert.equal(result.status, 401); assert.deepEqual(result.body, { error: 'proxy proof required' }); assert.ok(!result.text.includes(VALUE)); } const noIdentity = await ask(f.port, { headers: { 'x-user': 'wrong' } }); assert.equal(noIdentity.status, 401); assert.deepEqual(noIdentity.body, { error: 'proxy proof required' }); assert.deepEqual(f.calls, []); assert.ok(!f.logs.join('\n').includes(VALUE)); }); test('proxy proof header is refused on other Hosts and on the tester listener', async t => { const f = await setup(t); for (const host of [`127.0.0.1:${f.port}`, 'other.example']) { assert.equal((await ask(f.port, { host, headers: { 'x-user': undefined, [HEADER]: VALUE } })).status, 403); } assert.equal((await ask(f.port, { host: `127.0.0.1:${f.port}`, headers: { 'x-user': undefined, [HEADER]: '' } })).status, 403); const testerRoot = mkdtempSync(join(tmpdir(),'tester-proof-')); t.after(()=>rmSync(testerRoot,{recursive:true,force:true})); const server = await startTesterSocket({ dataRoot: testerRoot, testerProofHeader: HEADER, board: 'http://127.0.0.1:9' }); t.after(() => close(server)); const result = await testerRequest({dataRoot:testerRoot,headers:{[HEADER]:VALUE},path:'/api/config'}); assert.equal(result.status, 403); assert.deepEqual(f.calls, []); }); test('proof options reject unsafe or colliding header names without disclosing file contents', t => { const f = mkdtempSync(join(tmpdir(), 'proof-options-')); t.after(() => rmSync(f, { recursive: true, force: true })); const file = join(f, 'fixture.token'); writeFileSync(file, VALUE, { mode: 0o600 }); for (const proofHeader of ['host', 'origin', 'x-user', 'x-mosaic-tester', 'bad header', '']) { assert.throws(() => serve.proofOptions({ proofHeader, proofFile: file }, preview), /proof/); } chmodSync(file, 0o400); assert.equal(serve.proofOptions({ proofHeader: HEADER, proofFile: file }, preview).header, HEADER); });