// integrations/cisco-mpp-phone/client.js // // Axios wrapper for Cisco MPP desk phones (CP-78xx, Webex Calling). // HTTPS on 443 with a self-signed cert; Basic auth is typical, with // Digest retry on 401 (same interceptor pattern as DECT). import axios from 'axios'; import https from 'node:https'; import { parseDigestChallenge, buildDigestAuthHeader, } from '../../utils/httpDigestAuth.js'; /** * @param {object} opts * @param {string} opts.host * @param {string} [opts.user] default `admin` when password is set * @param {string} [opts.password] omit for unauthenticated probes (Webex MPP JSON) * @param {number} [opts.timeoutMs] * @returns {import('axios').AxiosInstance} */ export function createMppPhoneClient({ host, user, password, timeoutMs = 15_000 }) { if (!host) throw new Error('createMppPhoneClient: host is required'); const useAuth = !!(password && String(password).length > 0); const authUser = user || 'admin'; const client = axios.create({ baseURL: `https://${host}`, timeout: timeoutMs, httpsAgent: new https.Agent({ rejectUnauthorized: false }), validateStatus: () => true, responseType: 'text', transformResponse: [(data) => data], ...(useAuth ? { auth: { username: authUser, password } } : {}), headers: { 'User-Agent': 'collabSupport-mpp-phone/0.1' }, }); if (useAuth) { client.defaults.__mppAuth = { user: authUser, password }; } client.interceptors.response.use(async (response) => { if (response.status !== 401) return response; if (!client.defaults.__mppAuth) return response; const originalConfig = response.config; if (originalConfig.__digestRetried) return response; const wwwAuth = response.headers?.['www-authenticate']; const challenge = parseDigestChallenge(wwwAuth); if (!challenge) return response; const { user: username, password: pass } = client.defaults.__mppAuth; const method = (originalConfig.method || 'get').toUpperCase(); const uri = originalConfig.url || '/'; const authHeader = buildDigestAuthHeader({ username, password: pass, method, uri, challenge, }); return client.request({ ...originalConfig, auth: undefined, headers: { ...(originalConfig.headers || {}), Authorization: authHeader }, __digestRetried: true, }); }); return client; } /** * @typedef {object} ProbeResult * @property {string} path * @property {string} method * @property {number|null} status * @property {string|null} contentType * @property {number} sizeBytes * @property {string|null} snippet * @property {string|null} error * @property {number} elapsedMs */ export async function tryRequest(client, { method = 'GET', path, data, headers } = {}) { const start = Date.now(); try { const res = await client.request({ method, url: path, data, headers }); return normalize({ path, method, status: res.status, contentType: res.headers?.['content-type'] || null, body: res.data, elapsedMs: Date.now() - start, }); } catch (err) { return { path, method, status: null, contentType: null, sizeBytes: 0, snippet: null, error: err.code ? `${err.code}: ${err.message}` : err.message, elapsedMs: Date.now() - start, }; } } function normalize({ path, method, status, contentType, body, elapsedMs, error = null }) { const bodyStr = typeof body === 'string' ? body : (body == null ? '' : String(body)); const flat = bodyStr.replace(/\s+/g, ' ').trim(); return { path, method, status, contentType, sizeBytes: Buffer.byteLength(bodyStr, 'utf8'), snippet: flat ? flat.slice(0, 200) : null, error, elapsedMs, }; }