cisco-axl
Version:
A library to make Cisco AXL a lot easier
596 lines • 30.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
const soap = __importStar(require("strong-soap"));
const path = __importStar(require("path"));
const https = __importStar(require("https"));
const url_1 = require("url");
const WSDL = soap.soap.WSDL;
const wsdlOptions = {
attributesKey: "attributes",
valueKey: "value",
};
/**
* 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 {
/**
* Creates an instance of axlService.
* @param {string} host - CUCM hostname or IP address
* @param {string} username - CUCM username with AXL permissions
* @param {string} password - CUCM password
* @param {string} version - CUCM version (e.g. "14.0")
* @memberof 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
* @memberof axlService
*/
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_1.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();
});
}
/**
* Returns a list of available AXL operations
* @param {string} [filter] - Optional filter to narrow down operations
* @returns {Promise<string[]>} - Array of operation names
* @memberof axlService
*/
returnOperations(filter) {
debugLog(`Getting available operations${filter ? ` with filter: ${filter}` : ""}`);
const options = this._OPTIONS;
return new Promise((resolve, reject) => {
debugLog(`Creating SOAP client for ${options.url}`);
soap.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.soap.BasicAuthSecurity(options.username, options.password));
client.setEndpoint(options.endpoint);
const description = client.describe();
const outputArr = [];
for (const [, 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;
}
return false;
});
if (filter) {
resolve(matches(filter, outputArr).sort(sortAlphaNum));
}
else {
resolve(outputArr.sort(sortAlphaNum));
}
client.on("soapError", function (err) {
reject(err.root.Envelope.Body.Fault);
});
});
});
}
/**
* Gets the tags required for a specific AXL operation
* @param {string} operation - The AXL operation name
* @returns {Promise<any>} - Object containing the required tags
* @memberof axlService
*/
getOperationTags(operation) {
debugLog(`Getting tags for operation: ${operation}`);
const 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);
return;
}
const operationDef = wsdl.definitions.bindings.AXLAPIBinding.operations[operation];
const operName = operationDef.$name;
const operationDesc = operationDef.describe(wsdl);
const envelopeBody = {};
operationDesc.input.body.elements.map((object) => {
const operMatch = new RegExp(object.qname.name, "i");
envelopeBody[object.qname.name] = "";
if (object.qname.name === "searchCriteria") {
const output = nestedObj(object);
envelopeBody.searchCriteria = output;
}
if (object.qname.name === "returnedTags") {
const output = nestedObj(object);
envelopeBody.returnedTags = output;
}
if (operName.match(operMatch)) {
const 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 {any} tags - The tags required for the operation
* @param {OperationOptions} [opts] - Optional parameters for customizing the operation
* @returns {Promise<any>} - Result of the operation
* @memberof axlService
*/
async executeOperation(operation, tags, opts) {
var _a, _b, _c;
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 = (_a = opts === null || opts === void 0 ? void 0 : opts.clean) !== null && _a !== void 0 ? _a : false;
const dataContainerIdentifierTails = (_b = opts === null || opts === void 0 ? void 0 : opts.dataContainerIdentifierTails) !== null && _b !== void 0 ? _b : "_data";
const removeAttributes = (_c = opts === null || opts === void 0 ? void 0 : opts.removeAttributes) !== null && _c !== void 0 ? _c : 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.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;
}
const customRequestHeader = {
connection: "keep-alive",
SOAPAction: `"CUCM:DB ver=${options.version} ${operation}"`,
};
client.setSecurity(new soap.soap.BasicAuthSecurity(options.username, options.password));
client.setEndpoint(options.endpoint);
client.on("soapError", function (err) {
var _a, _b, _c;
debugLog("SOAP error event triggered");
// Check if this is an authentication error
if ((_c = (_b = (_a = err.root) === null || _a === void 0 ? void 0 : _a.Envelope) === null || _b === void 0 ? void 0 : _b.Body) === null || _c === void 0 ? void 0 : _c.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 instanceof Error ? 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
const 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) {
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 === null || result === void 0 ? void 0 : result.hasOwnProperty("return")) {
const 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);
});
});
}
}
/**
* Creates a nested object from WSDL elements
* @param {any} object - The WSDL object to process
* @returns {any} - Processed object
*/
const nestedObj = (object) => {
const operObj = {};
object.elements.map((object) => {
operObj[object.qname.name] = "";
if (Array.isArray(object.elements) && object.elements.length > 0) {
const nestName = object.qname.name;
operObj[nestName] = {};
const nestObj = nestedObj(object);
operObj[nestName] = nestObj;
}
});
const isEmpty = Object.keys(operObj).length === 0;
if (isEmpty) {
return "";
}
else {
return operObj;
}
};
/**
* Cleans an object by removing null, undefined, or empty values
* @param {any} object - The object to clean
* @returns {any} - Cleaned object
*/
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(parseInt(k), 1);
}
else {
delete object[k];
}
}
});
return object;
};
/**
* Removes attribute fields from an object
* @param {any} object - The object to clean
* @returns {any} - Cleaned 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(parseInt(k), 1);
}
else {
delete object[k];
}
}
});
return object;
};
module.exports = axlService;
//# sourceMappingURL=index.js.map