// src/integrations/cisco-dect/probes.js // // Individual probe / action functions against a DBS-210 base station. // Every function takes an axios client from client.js and returns a // ProbeResult or an object built from one, so the CLI runner has a // uniform envelope to print. // // URL map is derived from reverse-engineering the actual admin UI JS // (see .dect-samples/dbs210-*.{html,js} pulled from a live base, and // specifically dbs210-gen.js `LoadPage(...)` call sites). // // ⚠️ IMPORTANT SAFETY MODEL — READ BEFORE ADDING NEW PATHS ⚠️ // // The DBS-210 admin UI uses a Cisco SPA-family legacy pattern where // ACTIONS are triggered by simple GET navigation, not POST + form. // GETting `/reboot.html` reboots the base. GETting `/DefaultEeprom.html` // factory-resets it. There is no confirmation dialog on the server // side — the browser JS shows the confirm() prompt, but the server // happily executes on any authenticated GET. The legacy alias // `/admin/reboot.htm` doesn't even enforce the CSRF token. // // Our previous probe list included `/admin/reboot.htm` as a "guess" // and REBOOTED the user's lab base while probing. Never again. Any // URL that mutates state MUST live in MUTATING_ACTION_PATHS below, // which is NOT touched by runReadProbes() and is only reachable via // explicit triggerX() functions gated by the CLI's --execute flag. import { tryRequest } from './client.js'; // ─── Read-safe endpoints ──────────────────────────────────────────── // // SSR HTML pages (the whole admin UI's page set from main.html's left // nav) plus the two machine-readable XML endpoints referenced by // gen.js. All confirmed by the browser HAR + JS grep — no more // guessing. Every one of these is idempotent as far as we know. export const READ_PROBE_PATHS = [ // Home + machine-readable data endpoints first — most useful for // "am I connected and authenticated?" and for the eventual data // collector. { path: '/main.html', purpose: 'home/status page (SSR HTML)' }, { path: '/admin/status.xml', purpose: 'machine-readable status XML (called by GetStausXml() in gen.js)' }, { path: '/Settings.xml', purpose: 'machine-readable settings XML (called by GetSettingsXml() in gen.js)' }, // Left-nav pages — SSR HTML, useful for scraping specific data. { path: '/Ext.html', purpose: 'extensions page' }, { path: '/Servers.html', purpose: 'SIP servers page' }, { path: '/Network.html', purpose: 'network config page' }, { path: '/Management.html', purpose: 'management page (holds REBOOT_OPTION button)' }, { path: '/Fwu.html', purpose: 'firmware update page' }, { path: '/CountryTimeDate.html',purpose: 'country/time page' }, { path: '/Security.html', purpose: 'security page' }, { path: '/License.html', purpose: 'license info page' }, ]; // ─── Mutating action endpoints — QUARANTINED ──────────────────────── // // Every entry here triggers a real side-effect on the device with a // bare authenticated GET. NEVER include these in runReadProbes(). // They're exported only so the triggerX() functions below have a // single source of truth for the URL strings. export const MUTATING_ACTION_PATHS = Object.freeze({ REBOOT: '/reboot.html', FORCE_REBOOT: '/forcereboot.html', REBOOT_CHAIN: '/rebootchain.html', FORCE_REBOOT_CHAIN: '/forcerebootchain.html', FACTORY_RESET: '/DefaultEeprom.html', RECONFIGURE_TREE: '/reconfiguredecttree.html', }); // ─── Read-only helpers ────────────────────────────────────────────── /** * Fire every read-only probe and return an array of ProbeResults. * Sequential so output is readable and the DBS-210 (which is not * exactly a beefy web server) doesn't get stampeded. */ export async function runReadProbes(client) { const results = []; for (const { path, purpose } of READ_PROBE_PATHS) { const r = await tryRequest(client, { method: 'GET', path }); results.push({ ...r, purpose }); } return results; } /** * Fetch an arbitrary path with no CSRF token. Only intended for * safe reads — the CLI runner's `get` subcommand routes here. */ export async function getPath(client, path) { return tryRequest(client, { method: 'GET', path }); } /** * Pull `/main.html`, parse out the CSRF token from the meta tag, and * return it. Every mutating action needs to include this as * `?csrf_token=` — the JS on the real page does the same when * building any state-changing URL. * * Notable exception: the legacy `/admin/reboot.htm` alias does NOT * enforce CSRF (verified: our tokenless probe rebooted the base). * That alias is intentionally NOT exposed by triggerReboot() — * always take the modern `/reboot.html` path so future firmware * that tightens CSRF enforcement doesn't silently break us. * * @returns {Promise} The CSRF token, or null if the page * doesn't expose one (older firmware). */ export async function fetchCsrfToken(client) { const r = await tryRequest(client, { method: 'GET', path: '/main.html' }); if (!r.status || r.status >= 400) { throw new Error(`Cannot fetch /main.html for CSRF token (status: ${r.status ?? 'ERR'})`); } // The full body isn't in the ProbeResult (only a snippet), so // re-request for the raw HTML. Cheap on LAN, and keeps the pure // ProbeResult shape clean for the probe runner. const raw = await client.get('/main.html'); const body = raw.data || ''; // Meta tag shape (from real page): const m = body.match(/