From af0d6b2fe542ffd7d6781dcf87e574f38007f975 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 3 Oct 2026 23:39:01 -0700 Subject: [PATCH] P7 live probes: real-HTTP transcripts for forward_auth (inactive 503; active: allow/deny/unknown-device/step-up) via seeded real stores + registry stub provider --- dashcaddy-api/scripts/p7-live-curl2.js | 111 +++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 dashcaddy-api/scripts/p7-live-curl2.js diff --git a/dashcaddy-api/scripts/p7-live-curl2.js b/dashcaddy-api/scripts/p7-live-curl2.js new file mode 100644 index 0000000..8d12a26 --- /dev/null +++ b/dashcaddy-api/scripts/p7-live-curl2.js @@ -0,0 +1,111 @@ +/** + * Live transcript, phase 2: ACTIVE-provider outcomes over a real socket. + * Strategy: seed the REAL data files (/etc/dashcaddy/network-*.json — none + * exist on this fresh install) in the exact on-disk format the stores read, + * register a stub provider through the REAL registry (setActiveProvider), + * boot the REAL app, and hit the REAL mounted route over HTTP. Only the + * network edge (whois) is stubbed. Leaves no state behind that matters + * (fresh-install files; removed on exit). + */ +'use strict'; + +const http = require('http'); +const fs = require('fs'); +const { createApp } = require('../src/app'); +const registry = require('../src/network/registry'); +const DATA = '/etc/dashcaddy'; + +function req(port, method, reqPath, headers = {}) { + return new Promise((resolve, reject) => { + const r = http.request({ host: '127.0.0.1', port, method, path: reqPath, headers }, (res) => { + let body = ''; + res.on('data', (c) => { body += c; }); + res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body })); + }); + r.on('error', reject); + r.end(); + }); +} + +const CREATED = []; +function writeIfAbsent(name, obj) { + const p = `${DATA}/${name}`; + if (!fs.existsSync(p)) { + fs.writeFileSync(p, JSON.stringify(obj, null, 2)); + CREATED.push(p); + } +} + +async function main() { + // ── seed the real store files (fresh install: none exist) ── + const now = new Date().toISOString(); + writeIfAbsent('network-people.json', { + people: { + 'p-alice': { id: 'p-alice', displayName: 'Alice', providerUserId: '1001', groups: ['Admin'], status: 'active', devices: [], createdAt: now, updatedAt: now }, + 'p-bob': { id: 'p-bob', displayName: 'Bob', providerUserId: '1002', groups: ['Family'], status: 'active', devices: [], createdAt: now, updatedAt: now }, + }, + }); + writeIfAbsent('network-resources.json', { + resources: [ + { id: 'r-plex', name: 'plex', host: 'plex.internal', ports: ['*'], sensitive: false, createdAt: now, updatedAt: now }, + { id: 'r-vault', name: 'vault', host: 'vault.internal', ports: ['*'], sensitive: true, createdAt: now, updatedAt: now }, + ], + }); + writeIfAbsent('network-access.json', { + grants: [ + { principal: { type: 'person', id: 'p-alice' }, resource: 'plex', allowed: true, updatedAt: now }, + { principal: { type: 'person', id: 'p-alice' }, resource: 'vault', allowed: true, updatedAt: now }, + ], + updatedAt: now, + }); + + // ── register + activate a stub provider through the REAL registry ── + const DEVICE_IP = '10.53.0.7'; + const stub = { + id: () => 'stub-live', + whois: async (ip) => (ip === DEVICE_IP + ? { personId: 'p-alice', deviceId: 'dev-1' } + : (ip === '10.53.0.9' ? { personId: 'p-bob', deviceId: 'dev-2' } : { personId: null, deviceId: null })), + listDevices: async () => [], + listPeople: async () => [], + capabilities: () => ({ id: 'stub-live' }), + getStatus: async () => ({ ok: true }), + createInvite: async () => { throw new Error('not needed for probe'); }, + revokeInvite: async () => { throw new Error('not needed for probe'); }, + blockPerson: async () => { throw new Error('not needed for probe'); }, + unblockPerson: async () => { throw new Error('not needed for probe'); }, + removePerson: async () => { throw new Error('not needed for probe'); }, + revokeDevice: async () => { throw new Error('not needed for probe'); }, + syncAccess: async () => { throw new Error('not needed for probe'); }, + configureDns: async () => { throw new Error('not needed for probe'); }, + }; + registry.registerProvider('stub-live', () => stub); + registry.setActiveProvider('stub-live'); + + const { app } = await createApp(); + const server = app.listen(0, '127.0.0.1'); + await new Promise((r) => server.on('listening', r)); + const port = server.address().port; + const FA = '/api/v1/network/forward-auth/'; + + const show = (label, res) => { + console.log(`### ${label}`); + console.log(`< HTTP ${res.status}`); + if (res.headers['www-authenticate']) console.log(`< www-authenticate: ${res.headers['www-authenticate']}`); + if (res.headers['x-forwarded-user']) console.log(`< x-forwarded-user: ${res.headers['x-forwarded-user']}`); + console.log(`< body: ${res.body.slice(0, 240)}`); + console.log(''); + }; + + // loopback is in the default trustedProxies → forwarded headers honored + show('ALLOWED non-sensitive (alice device → plex.internal)', await req(port, 'GET', FA, { 'X-Forwarded-For': DEVICE_IP, 'X-Forwarded-Host': 'plex.internal' })); + show('DENIED (bob device → plex.internal, no grant)', await req(port, 'GET', FA, { 'X-Forwarded-For': '10.53.0.9', 'X-Forwarded-Host': 'plex.internal' })); + show('UNKNOWN-DEVICE (unrecognized tailnet ip)', await req(port, 'GET', FA, { 'X-Forwarded-For': '10.99.99.99', 'X-Forwarded-Host': 'plex.internal' })); + show('SENSITIVE step-up REQUIRED (alice → vault.internal, no session)', await req(port, 'GET', FA, { 'X-Forwarded-For': DEVICE_IP, 'X-Forwarded-Host': 'vault.internal' })); + + server.close(); + console.log('LIVE-PROBE-PHASE2-DONE'); + process.exit(0); +} + +main().catch((e) => { console.error('FATAL', e && e.stack || e); process.exit(1); });