UNPKG

cisco-axl

Version:

A library to make Cisco AXL a lot easier

580 lines (511 loc) 22.9 kB
const soap = require("strong-soap").soap; const WSDL = soap.WSDL; const path = require("path"); const https = require("https"); const { URL } = require("url"); const wsdlOptions = { attributesKey: "attributes", valueKey: "value", ns1: "ns", }; /** * Helper function to log debug messages only when DEBUG environment variable is set * @param {string} message - The message to log * @param {any} [data] - Optional data to log */ const debugLog = (message, data) => { // Get the DEBUG value, handling case-insensitivity const debug = process.env.DEBUG; // Check if DEBUG is set and is a truthy value (not 'false', 'no', '0', etc.) const isDebugEnabled = debug && !["false", "no", "0", "off", "n"].includes(debug.toLowerCase()); if (isDebugEnabled) { if (data) { console.log(`[AXL DEBUG] ${message}`, data); } else { console.log(`[AXL DEBUG] ${message}`); } } }; /** * Cisco axlService Service * This is a service class that uses fetch and promises to pull AXL data from Cisco CUCM * * @class axlService */ class axlService { constructor(host, username, password, version) { if (!host || !username || !password || !version) throw new TypeError("missing parameters"); this._OPTIONS = { username: username, password: password, url: path.join(__dirname, `/schema/${version}/AXLAPI.wsdl`), endpoint: `https://${host}:8443/axl/`, version: version, }; debugLog(`Initializing AXL service for host: ${host}, version: ${version}`); } /** * Test authentication credentials against the AXL endpoint * @returns {Promise<boolean>} - Resolves to true if authentication is successful */ async testAuthentication() { try { const authSuccess = await this._testAuthenticationDirectly(); if (!authSuccess) { throw new Error("Authentication failed. Check username and password."); } return true; } catch (error) { throw new Error(`Authentication test failed: ${error.message}`); } } /** * Private method to test authentication using a simple GET request to the AXL endpoint * @returns {Promise<boolean>} - Resolves with true if authentication successful, false otherwise * @private */ async _testAuthenticationDirectly() { const options = this._OPTIONS; const url = new URL(options.endpoint); return new Promise((resolve) => { const authHeader = "Basic " + Buffer.from(`${options.username}:${options.password}`).toString("base64"); const reqOptions = { hostname: url.hostname, port: url.port || 8443, path: url.pathname, method: "GET", // Simply use GET instead of POST headers: { Authorization: authHeader, Connection: "keep-alive", }, rejectUnauthorized: false, // For self-signed certificates }; debugLog(`Testing authentication to ${url.hostname}:${url.port || 8443}${url.pathname}`); const req = https.request(reqOptions, (res) => { debugLog(`Authentication test response status: ${res.statusCode}`); // Check status code for authentication failures if (res.statusCode === 401 || res.statusCode === 403) { debugLog("Authentication failed: Unauthorized status code"); resolve(false); // Authentication failed return; } let responseData = ""; res.on("data", (chunk) => { responseData += chunk; }); res.on("end", () => { // Check for the expected success message const successIndicator = "Cisco CallManager: AXL Web Service"; if (responseData.includes(successIndicator)) { debugLog("Authentication succeeded: Found success message"); resolve(true); // Authentication succeeded } else if (responseData.includes("Authentication failed") || responseData.includes("401 Unauthorized") || responseData.includes("403 Forbidden")) { debugLog("Authentication failed: Found failure message in response"); resolve(false); // Authentication failed } else { debugLog("Authentication status uncertain, response did not contain expected messages"); // If we're not sure, assume it failed to be safe resolve(false); } }); }); req.on("error", (error) => { console.error("Authentication test error:", error.message); resolve(false); }); // Since it's a GET request, we just end it without writing any data req.end(); }); } returnOperations(filter) { debugLog(`Getting available operations${filter ? ` with filter: ${filter}` : ''}`); var options = this._OPTIONS; return new Promise((resolve, reject) => { debugLog(`Creating SOAP client for ${options.url}`); soap.createClient(options.url, wsdlOptions, function (err, client) { if (err) { debugLog(`SOAP error occurred: ${err.message || 'Unknown error'}`, err); reject(err); return; } client.setSecurity(new soap.BasicAuthSecurity(options.username, options.password)); client.setEndpoint(options.endpoint); var description = client.describe(); var outputArr = []; for (const [key, value] of Object.entries(description.AXLAPIService.AXLPort)) { outputArr.push(value.name); } const sortAlphaNum = (a, b) => a.localeCompare(b, "en", { numeric: true }); const matches = (substring, array) => array.filter((element) => { if (element.toLowerCase().includes(substring.toLowerCase())) { return true; } }); if (filter) { resolve(matches(filter, outputArr).sort(sortAlphaNum)); } else { resolve(outputArr.sort(sortAlphaNum)); } client.on("soapError", function (err) { reject(err.root.Envelope.Body.Fault); }); }); }); } getOperationTags(operation) { debugLog(`Getting tags for operation: ${operation}`); var options = this._OPTIONS; return new Promise((resolve, reject) => { const wsdlPath = path.join(__dirname, `/schema/${options.version}/AXLAPI.wsdl`); debugLog(`Opening WSDL file: ${wsdlPath}`); WSDL.open(wsdlPath, wsdlOptions, function (err, wsdl) { if (err) { debugLog(`WSDL error occurred: ${err.message || 'Unknown error'}`, err); reject(err); } var operationDef = wsdl.definitions.bindings.AXLAPIBinding.operations[operation]; var operName = operationDef.$name; var operationDesc = operationDef.describe(wsdl); var envelopeBody = {}; operationDesc.input.body.elements.map((object) => { var operMatch = new RegExp(object.qname.name, "i"); envelopeBody[object.qname.name] = ""; if (object.qname.name === "searchCriteria") { let output = nestedObj(object); envelopeBody.searchCriteria = output; } if (object.qname.name === "returnedTags") { let output = nestedObj(object); envelopeBody.returnedTags = output; } if (operName.match(operMatch)) { let output = nestedObj(object); envelopeBody[object.qname.name] = output; } }); resolve(envelopeBody); }); }); } /** * Executes an AXL operation against the CUCM * @param {string} operation - The AXL operation to execute * @param {Object} tags - The tags required for the operation * @param {Object} [opts] - Optional parameters for customizing the operation * @returns {Promise<any>} - Result of the operation */ async executeOperation(operation, tags, opts) { debugLog(`Preparing to execute operation: ${operation}`); const options = this._OPTIONS; // First test authentication debugLog(`Testing authentication before executing operation: ${operation}`); const authSuccess = await this._testAuthenticationDirectly(); if (!authSuccess) { debugLog(`Authentication failed for operation: ${operation}`); throw new Error("Authentication failed. Check username and password."); } debugLog("Authentication successful, proceeding with operation"); const clean = opts?.clean ?? false; const dataContainerIdentifierTails = opts?.dataContainerIdentifierTails ?? "_data"; const removeAttributes = opts?.removeAttributes ?? false; // Let's remove empty top level strings. Also filter out json-variables debugLog("Cleaning input tags from empty values and json-variables"); Object.keys(tags).forEach((k) => { if (tags[k] == "" || k.includes(dataContainerIdentifierTails)) { debugLog(`Removing tag: ${k}`); delete tags[k]; } }); return new Promise((resolve, reject) => { debugLog(`Creating SOAP client for operation: ${operation}`); soap.createClient(options.url, wsdlOptions, function (err, client) { if (err) { debugLog(`SOAP client creation error: ${err.message || 'Unknown error'}`, err); reject(err); return; } // Get the properly versioned namespace URL const namespaceUrl = `http://www.cisco.com/AXL/API/${options.version}`; debugLog(`Using AXL namespace: ${namespaceUrl}`); // 1. Set envelope key debugLog("Setting envelope key to 'soapenv'"); client.wsdl.options = { ...client.wsdl.options, envelopeKey: "soapenv", // Change default 'soap' to 'soapenv' }; // 2. Define namespaces with the correct version debugLog(`Setting namespace 'ns' to: ${namespaceUrl}`); client.wsdl.definitions.xmlns.ns = namespaceUrl; // Remove ns1 if it exists if (client.wsdl.definitions.xmlns.ns1) { debugLog("Removing 'ns1' namespace"); delete client.wsdl.definitions.xmlns.ns1; } var customRequestHeader = { connection: "keep-alive", SOAPAction: `"CUCM:DB ver=${options.version} ${operation}"`, }; client.setSecurity(new soap.BasicAuthSecurity(options.username, options.password)); client.setEndpoint(options.endpoint); client.on("soapError", function (err) { debugLog("SOAP error event triggered"); // Check if this is an authentication error if (err.root?.Envelope?.Body?.Fault) { const fault = err.root.Envelope.Body.Fault; const faultString = fault.faultstring || fault.faultString || ""; debugLog(`SOAP fault detected: ${faultString}`, fault); if (typeof faultString === "string" && (faultString.includes("Authentication failed") || faultString.includes("credentials") || faultString.includes("authorize"))) { debugLog("Authentication error detected in SOAP fault"); reject(new Error("Authentication failed. Check username and password.")); } else { debugLog("Non-authentication SOAP fault"); reject(fault); } } else { debugLog("Unstructured SOAP error", err); reject(err); } }); // Check if the operation function exists if (!client.AXLAPIService || !client.AXLAPIService.AXLPort || typeof client.AXLAPIService.AXLPort[operation] !== "function") { debugLog(`Operation '${operation}' not found in AXL API, attempting alternative approach`); // For operations that aren't found, try a manual approach if (operation.startsWith("apply") || operation.startsWith("reset")) { debugLog(`Using manual XML approach for ${operation} operation`); // Determine which parameter to use (name or uuid) const operationObj = tags[operation] || tags; // Check if uuid or name is provided let paramTag, paramValue; if (operationObj.uuid) { paramTag = "uuid"; paramValue = operationObj.uuid; debugLog(`Using uuid parameter: ${paramValue}`); } else { paramTag = "name"; paramValue = operationObj.name; debugLog(`Using name parameter: ${paramValue}`); } const rawXml = `<?xml version="1.0" encoding="UTF-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns="${namespaceUrl}"> <soapenv:Header/> <soapenv:Body> <ns:${operation}> <${paramTag}>${paramValue}</${paramTag}> </ns:${operation}> </soapenv:Body> </soapenv:Envelope>`; debugLog(`Executing manual XML request for operation: ${operation}`); // Use client.request for direct XML request debugLog(`Sending manual XML request to ${options.endpoint}`, { operation, paramTag, paramValue }); client._request( options.endpoint, rawXml, function (err, body, response) { if (err) { debugLog(`Error in manual XML request: ${err.message || 'Unknown error'}`, err); reject(err); return; } // Check for authentication failures in the response if (response && (response.statusCode === 401 || response.statusCode === 403)) { debugLog(`Authentication failed in manual request. Status code: ${response.statusCode}`); reject(new Error("Authentication failed. Check username and password.")); return; } if (body && typeof body === "string" && (body.includes("Authentication failed") || body.includes("401 Unauthorized") || body.includes("403 Forbidden"))) { debugLog(`Authentication failed in manual request. Found auth failure text in body.`); reject(new Error("Authentication failed. Check username and password.")); return; } debugLog(`Manual XML request response received. Size: ${body ? body.length : 0} bytes`); // Parse the response try { // Don't automatically assume success if (body && body.includes("Fault")) { debugLog("Fault detected in manual XML response"); // Try to extract the fault message const faultMatch = /<faultstring>(.*?)<\/faultstring>/; const match = body.match(faultMatch); if (match && match[1]) { const faultString = match[1]; debugLog(`Extracted fault string: ${faultString}`); if (faultString.includes("Authentication failed") || faultString.includes("credentials") || faultString.includes("authorize")) { debugLog("Authentication failure detected in fault string"); reject(new Error("Authentication failed. Check username and password.")); } else { debugLog(`Operation failed with fault: ${faultString}`); reject(new Error(faultString)); } } else { debugLog("Unknown SOAP fault format, couldn't extract fault string"); reject(new Error("Unknown SOAP fault occurred")); } } else { debugLog(`Operation ${operation} completed successfully via manual XML`); const result = { return: "Success" }; // Only report success if no errors found resolve(result); } } catch (parseError) { debugLog(`Error parsing manual XML response: ${parseError.message || 'Unknown error'}`, parseError); reject(parseError); } }, customRequestHeader ); return; } else { debugLog(`Operation "${operation}" not found and cannot be handled via manual XML`); reject(new Error(`Operation "${operation}" not found`)); return; } } // Get the operation function - confirmed to exist at this point var axlFunc = client.AXLAPIService.AXLPort[operation]; debugLog(`Found operation function: ${operation}`); // Define namespace context with the correct version const nsContext = { ns: namespaceUrl, }; // Prepare message for specific operations let message = tags; // Handle operations that start with "apply" or "reset" if (operation.startsWith("apply") || operation.startsWith("reset")) { debugLog(`Special message handling for ${operation} operation`); const operationKey = operation; // If there's a nested structure, flatten it if (tags[operationKey]) { debugLog(`Found nested structure for ${operationKey}`); // Check if uuid or name is provided in the nested structure if (tags[operationKey].uuid) { debugLog(`Using uuid from nested structure: ${tags[operationKey].uuid}`); message = { uuid: tags[operationKey].uuid }; } else if (tags[operationKey].name) { debugLog(`Using name from nested structure: ${tags[operationKey].name}`); message = { name: tags[operationKey].name }; } // If neither uuid nor name is provided, try to use any available else { // Try to use uuid or name from the top level as fallback if (tags.uuid) { debugLog(`Using uuid from top level: ${tags.uuid}`); message = { uuid: tags.uuid }; } else { debugLog(`Using name from top level: ${tags.name}`); message = { name: tags.name }; } } } else { debugLog(`No nested structure found for ${operationKey}, using tags directly`); } } debugLog(`Executing operation: ${operation}`); // Create a sanitized copy of the message for logging let logMessage = JSON.parse(JSON.stringify(message)); // Remove any sensitive data if present if (logMessage.password) logMessage.password = '********'; debugLog(`Preparing message for operation ${operation}:`, logMessage); // Execute the operation axlFunc( message, function (err, result, rawResponse, soapHeader, rawRequest) { if (err) { debugLog(`Error in operation ${operation}: ${err.message || 'Unknown error'}`); // Check if this is an authentication error if (err.message && (err.message.includes("Authentication failed") || err.message.includes("401 Unauthorized") || err.message.includes("403 Forbidden") || err.message.includes("credentials"))) { debugLog(`Authentication failure detected in operation error message`); reject(new Error("Authentication failed. Check username and password.")); return; } // Check if the error response indicates authentication failure if (err.response && (err.response.statusCode === 401 || err.response.statusCode === 403)) { debugLog(`Authentication failure detected in response status code: ${err.response.statusCode}`); reject(new Error("Authentication failed. Check username and password.")); return; } debugLog(`Operation ${operation} failed with error`, err); reject(err); return; } debugLog(`Operation ${operation} executed successfully`); // Check the raw response for auth failures (belt and suspenders approach) if (rawResponse && typeof rawResponse === "string" && (rawResponse.includes("Authentication failed") || rawResponse.includes("401 Unauthorized") || rawResponse.includes("403 Forbidden"))) { debugLog(`Authentication failure detected in raw response`); reject(new Error("Authentication failed. Check username and password.")); return; } if (result?.hasOwnProperty("return")) { var output = result.return; debugLog(`Operation returned data with 'return' property`); if (clean) { debugLog(`Cleaning empty/null values from output`); cleanObj(output); } if (removeAttributes) { debugLog(`Removing attribute fields from output`); cleanAttributes(output); } debugLog(`Operation ${operation} completed successfully with return data`); resolve(output); } else { debugLog(`Operation ${operation} completed successfully without return data`); resolve(result || { return: "Success" }); } }, nsContext, customRequestHeader ); }); }); } } const nestedObj = (object) => { var operObj = {}; object.elements.map((object) => { operObj[object.qname.name] = ""; if (Array.isArray(object.elements) && object.elements.length > 0) { var nestName = object.qname.name; operObj[nestName] = {}; var nestObj = nestedObj(object); operObj[nestName] = nestObj; } }); const isEmpty = Object.keys(operObj).length === 0; if (isEmpty) { operObj = ""; return operObj; } else { return operObj; } }; const cleanObj = (object) => { Object.entries(object).forEach(([k, v]) => { if (v && typeof v === "object") { cleanObj(v); } if ((v && typeof v === "object" && !Object.keys(v).length) || v === null || v === undefined) { if (Array.isArray(object)) { object.splice(k, 1); } else { delete object[k]; } } }); return object; }; const cleanAttributes = (object) => { Object.entries(object).forEach(([k, v]) => { if (v && typeof v === "object") { cleanAttributes(v); } if (v && typeof v === "object" && "attributes" in object) { if (Array.isArray(object)) { object.splice(k, 1); } else { delete object[k]; } } }); return object; }; module.exports = axlService;