Wire CP-78xx probe discovery, relay phone-probe commands, and a chat follow-up message so store desk phones get registration, switch, and provisioning detail alongside DECT and WAN diagnostics. Co-authored-by: Cursor <cursoragent@cursor.com>
183 lines
6.2 KiB
JavaScript
183 lines
6.2 KiB
JavaScript
// integrations/cisco-mpp-phone/probes.js
|
|
//
|
|
// Read-only probes against Cisco MPP desk phones (Webex Calling).
|
|
// Current-release Webex MPP firmware uses JSON endpoints on the SPA
|
|
// web UI (not legacy /admin/status.xml).
|
|
|
|
import { tryRequest } from './client.js';
|
|
import { parseCfgXml } from './statusXml.js';
|
|
|
|
export const READ_PROBE_PATHS = [
|
|
{ path: '/', purpose: 'user web landing (SPA shell)' },
|
|
{ path: '/Status.json', purpose: 'primary status JSON (Info > Status tab)' },
|
|
{ path: '/Download%20Status.json', purpose: 'provisioning + firmware upgrade history' },
|
|
{ path: '/ns.json', purpose: 'network statistics JSON' },
|
|
{ path: '/basic/System.json', purpose: 'system configuration JSON (admin fields)' },
|
|
// Legacy fallbacks (older MPP loads / CUCM-style)
|
|
{ path: '/admin/status.xml', purpose: 'legacy admin status XML' },
|
|
{ path: '/status.xml', purpose: 'legacy user status XML' },
|
|
{ path: '/admin/cfg.xml', purpose: 'legacy admin config XML' },
|
|
];
|
|
|
|
export const EXPLORE_PROBE_PATHS = [
|
|
{ path: '/Debug%20Info.json', purpose: 'debug info JSON' },
|
|
{ path: '/basic/init.json', purpose: 'UI layout metadata' },
|
|
{ path: '/admin/advanced', purpose: 'legacy admin HTML landing' },
|
|
];
|
|
|
|
export const MUTATING_ACTION_PATHS = Object.freeze({
|
|
REBOOT: '/admin/reboot',
|
|
RESYNC: '/admin/resync',
|
|
UPGRADE: '/admin/upgrade',
|
|
});
|
|
|
|
export async function runProbeList(client, pathList) {
|
|
const results = [];
|
|
for (const { path, purpose } of pathList) {
|
|
const r = await tryRequest(client, { method: 'GET', path });
|
|
results.push({ ...r, purpose });
|
|
}
|
|
return results;
|
|
}
|
|
|
|
export async function runReadProbes(client) {
|
|
return runProbeList(client, READ_PROBE_PATHS);
|
|
}
|
|
|
|
export async function runExploreProbes(client) {
|
|
return runProbeList(client, [...READ_PROBE_PATHS, ...EXPLORE_PROBE_PATHS]);
|
|
}
|
|
|
|
export async function runPhoneProbe(client, { includeBodies = false } = {}) {
|
|
const started = Date.now();
|
|
const probes = await runReadProbes(client);
|
|
|
|
const statusJson = await fetchBodyIfOk(probes, client, '/Status.json');
|
|
const statusXml = !statusJson
|
|
? (await fetchBodyIfOk(probes, client, '/admin/status.xml')
|
|
|| await fetchBodyIfOk(probes, client, '/status.xml'))
|
|
: null;
|
|
|
|
const downloadStatusJson = await fetchBodyIfOk(probes, client, '/Download%20Status.json');
|
|
const nsJson = await fetchBodyIfOk(probes, client, '/ns.json');
|
|
const systemJson = await fetchBodyIfOk(probes, client, '/basic/System.json');
|
|
const cfgXml = await fetchBodyIfOk(probes, client, '/admin/cfg.xml');
|
|
const cfgParsed = cfgXml ? parseCfgXml(cfgXml) : null;
|
|
|
|
const summary = summarizeProbes(probes, { statusJson, statusXml });
|
|
|
|
return {
|
|
probes: includeBodies ? await attachBodies(client, probes) : probes,
|
|
summary,
|
|
statusJson,
|
|
downloadStatusJson: downloadStatusJson || null,
|
|
nsJson: nsJson || null,
|
|
systemJson: systemJson || null,
|
|
statusXml,
|
|
cfgXml: includeBodies ? cfgXml : (cfgXml ? truncate(cfgXml, 4000) : null),
|
|
cfgParsed: cfgParsed?.highlights || null,
|
|
byteLength: statusJson
|
|
? Buffer.byteLength(statusJson, 'utf8')
|
|
: (statusXml ? Buffer.byteLength(statusXml, 'utf8') : null),
|
|
elapsedMs: Date.now() - started,
|
|
};
|
|
}
|
|
|
|
export async function fetchStatusJson(client) {
|
|
const r = await tryRequest(client, { method: 'GET', path: '/Status.json' });
|
|
if (r.status === 200 && r.sizeBytes > 0) {
|
|
const body = await fetchBody(client, '/Status.json');
|
|
return {
|
|
rawJson: body,
|
|
rawXml: null,
|
|
byteLength: Buffer.byteLength(body || '', 'utf8'),
|
|
probe: r,
|
|
path: '/Status.json',
|
|
};
|
|
}
|
|
const xml = await fetchStatusXml(client);
|
|
return {
|
|
rawJson: null,
|
|
rawXml: xml.rawXml,
|
|
byteLength: xml.byteLength,
|
|
probe: xml.probe,
|
|
path: xml.path,
|
|
};
|
|
}
|
|
|
|
export async function fetchStatusXml(client) {
|
|
for (const path of ['/admin/status.xml', '/status.xml']) {
|
|
const r = await tryRequest(client, { method: 'GET', path });
|
|
if (r.status === 200 && r.sizeBytes > 0) {
|
|
const body = await fetchBody(client, path);
|
|
return { rawXml: body, byteLength: Buffer.byteLength(body || '', 'utf8'), probe: r, path };
|
|
}
|
|
}
|
|
const err = new Error('phone returned no Status.json or status.xml');
|
|
err.code = 'PHONE_BAD_STATUS';
|
|
throw err;
|
|
}
|
|
|
|
async function fetchBodyIfOk(probes, client, path) {
|
|
const p = probes.find((row) => row.path === path);
|
|
if (p?.status === 200 && p.sizeBytes > 0) {
|
|
return fetchBody(client, path);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function fetchBody(client, path) {
|
|
try {
|
|
const res = await client.get(path);
|
|
return typeof res.data === 'string' ? res.data : JSON.stringify(res.data ?? '');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function attachBodies(client, probes) {
|
|
const out = [];
|
|
for (const p of probes) {
|
|
if (p.status === 200 && p.sizeBytes > 0) {
|
|
const body = await fetchBody(client, p.path);
|
|
out.push({ ...p, body: body ?? null });
|
|
} else {
|
|
out.push({ ...p, body: null });
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function truncate(str, max) {
|
|
if (!str || str.length <= max) return str;
|
|
return `${str.slice(0, max)}\n<!-- truncated ${str.length - max} bytes -->`;
|
|
}
|
|
|
|
export function summarizeProbes(probes, { statusJson = null, statusXml = null } = {}) {
|
|
const okCount = probes.filter((p) => p.status === 200).length;
|
|
const authOk = probes.some((p) => p.status === 200);
|
|
const statusJsonFound = probes.some(
|
|
(p) => p.path === '/Status.json' && p.status === 200 && p.sizeBytes > 0,
|
|
) || !!statusJson;
|
|
const statusXmlFound = probes.some(
|
|
(p) => (p.path === '/admin/status.xml' || p.path === '/status.xml')
|
|
&& p.status === 200 && p.sizeBytes > 0,
|
|
) || !!statusXml;
|
|
return {
|
|
okCount,
|
|
total: probes.length,
|
|
authOk,
|
|
statusJsonFound,
|
|
statusXmlFound,
|
|
statusJsonBytes: statusJson ? Buffer.byteLength(statusJson, 'utf8') : 0,
|
|
statusXmlBytes: statusXml ? Buffer.byteLength(statusXml, 'utf8') : 0,
|
|
forbidden: probes.filter((p) => p.status === 403).map((p) => p.path),
|
|
unauthorized: probes.filter((p) => p.status === 401).map((p) => p.path),
|
|
notFound: probes.filter((p) => p.status === 404).map((p) => p.path),
|
|
okPaths: probes.filter((p) => p.status === 200).map((p) => p.path),
|
|
};
|
|
}
|
|
|
|
export async function getPath(client, path) {
|
|
return tryRequest(client, { method: 'GET', path });
|
|
}
|