/* Сервис интеграции ПП Парус 8 с WEB API Модуль ядра: вспомогательные функции */ //---------------------- // Подключение библиотек //---------------------- const fs = require("fs"); //Работа с файлами const os = require("os"); //Средства операционной системы const xml2js = require("xml2js"); //Конвертация XML в JSON const Schema = require("validate"); //Схемы валидации const nodemailer = require("nodemailer"); //Отправка E-Mail сообщений const { SERR_UNEXPECTED, SMODULES_PATH_MODULES, SERR_OBJECT_BAD_INTERFACE, SERR_MODULES_NO_MODULE_SPECIFIED, SERR_MODULES_BAD_INTERFACE, SERR_MAIL_FAILED } = require("./constants"); //Глобавльные константы системы const { ServerError } = require("./server_errors"); //Ошибка сервера const prmsUtilsSchema = require("../models/prms_utils"); //Схемы валидации параметров функций const { SPROTOCOL_HTTP, SPROTOCOL_KAFKA } = require("../models/obj_service"); //Схемы валидации сервиса //------------ // Тело модуля //------------ //Глубокое клонирование const deepClone = value => { //Клонируем объект в зависимости от его типа, сохраняя прототипы const seen = new WeakMap(); const clone = val => { //Примитивы и функции возвращаем как есть if (val === null || typeof val !== "object") return val; if (typeof val === "function") return val; //Защита от циклических ссылок if (seen.has(val)) return seen.get(val); //Специализированные типы if (val instanceof Date) return new Date(val); if (Buffer.isBuffer(val)) return Buffer.from(val); if (ArrayBuffer.isView(val)) return new val.constructor(val); if (val instanceof RegExp) return new RegExp(val); //Ошибки: сохраняем тип, сообщение, стек и пользовательские поля if (val instanceof Error) { const clonedError = new val.constructor(val.message); seen.set(val, clonedError); clonedError.name = val.name; clonedError.stack = val.stack; Reflect.ownKeys(val).forEach(key => { if (key === "name" || key === "message" || key === "stack") return; const desc = Object.getOwnPropertyDescriptor(val, key); if (!desc) return; if ("value" in desc) desc.value = clone(desc.value); try { Object.defineProperty(clonedError, key, desc); } catch (e) {} }); return clonedError; } //Коллекции if (val instanceof Map) { const m = new Map(); seen.set(val, m); val.forEach((v, k) => m.set(clone(k), clone(v))); return m; } if (val instanceof Set) { const s = new Set(); seen.set(val, s); val.forEach(v => s.add(clone(v))); return s; } //Коллекции, которые невозможно корректно клонировать - возвращаем по ссылке if (val instanceof WeakMap || val instanceof WeakSet || val instanceof Promise) { return val; } //Массивы if (Array.isArray(val)) { const arr = new Array(val.length); seen.set(val, arr); for (let i = 0; i < val.length; i++) { if (Object.prototype.hasOwnProperty.call(val, i)) { arr[i] = clone(val[i]); } } return arr; } //Общие объекты и пользовательские классы: сохраняем прототип и дескрипторы свойств const proto = Object.getPrototypeOf(val); const obj = Object.create(proto); seen.set(val, obj); Reflect.ownKeys(val).forEach(key => { const desc = Object.getOwnPropertyDescriptor(val, key); if (!desc) return; if ("value" in desc) { desc.value = clone(desc.value); } try { Object.defineProperty(obj, key, desc); } catch (e) {} }); return obj; }; try { return clone(value); } catch (e) { //В случае непредвиденной ошибки формируем информативное исключение const err = new Error("Ошибка глубокого копирования объекта"); err.originalError = e; throw err; } }; //Валидация объекта const validateObject = (obj, schema, sObjName) => { //Объявим результат let sRes = ""; //Если пришла верная схема if (schema instanceof Schema) { //И есть что проверять if (obj) { //Сделаем это const objTmp = deepClone(obj); const errors = schema.validate(objTmp, { strip: false }); //Если есть ошибки if (errors && Array.isArray(errors)) { if (errors.length > 0) { //Сформируем из них сообщение об ошибке валидации let a = errors.map(e => { return e.message; }); sRes = `Объект${sObjName ? ` "${sObjName}" ` : " "}имеет некорректный формат: ${Array.from(new Set(a)).join("; ")}`; } } else { //Валидатор вернул не то, что мы ожидали sRes = "Неожиданный ответ валидатора"; } } else { //Нам не передали объект на проверку sRes = `Объект${sObjName ? ` "${sObjName}" ` : " "}не указан`; } } else { //Пришла не схема валидации а непонятно что sRes = "Ошибочный формат схемы валидации"; } //Вернем результат return sRes; }; //Формирование полного пути к подключаемому модулю const makeModuleFullPath = sModuleName => { //Если имя модуля передано if (sModuleName) { //Объединим его с шаблоном пути до библиотеки модулей return `${SMODULES_PATH_MODULES}/${sModuleName}`; } else { //Нет имени модуля - нет полного пути return ""; } }; //Формирование текста ошибки const makeErrorText = e => { //Сообщение об ошибке по умолчанию let sErr = `${SERR_UNEXPECTED}: ${e.message}`; //Если это простая строка if (e instanceof String || typeof e === "string") sErr = `${SERR_UNEXPECTED}: ${e}`; //Если это наше внутреннее сообщение, с кодом, то сделаем ошибку более информативной if (e instanceof ServerError) sErr = `${e.sCode}: ${e.sMessage}`; //Вернем ответ return sErr; }; //Считывание наименования модуля-обработчика сервера приложений (ожидаемый формат - <МОДУЛЬ>.js/<ФУНКЦИЯ>) const getAppSrvModuleName = sAppSrv => { //Если есть что разбирать if (sAppSrv) { //И если это строка if (sAppSrv instanceof String || typeof sAppSrv === "string") { //Проверим наличие разделителя между именем модуля и функции if (sAppSrv.indexOf("/") === -1) { //Нет разделителя - нечего вернуть return null; } else { //Вернём всё, что левее разделителя return sAppSrv.substring(0, sAppSrv.indexOf("/")); } } else { //Пришла не строка return null; } } else { //Ничего не пришло return null; } }; //Считывание наименования функции модуля-обработчика сервера приложений (ожидаемый формат - <МОДУЛЬ>.js/<ФУНКЦИЯ>) const getAppSrvFunctionName = sAppSrv => { //Если есть что разбирать if (sAppSrv) { //И если это строка if (sAppSrv instanceof String || typeof sAppSrv === "string") { //Проверим наличие разделителя между именем модуля и функции if (sAppSrv.indexOf("/") === -1) { //Нет разделителя - нечего вернуть return null; } else { //Вернём всё, что правее разделителя return sAppSrv.substring(sAppSrv.indexOf("/") + 1, sAppSrv.length); } } else { //Пришла не строка return null; } } else { //Ничего не пришло return null; } }; //Получение функции обработчика const getAppSrvFunction = sAppSrv => { //Объявим формат (для сообщений об ошибках) const sFormat = "(ожидаемый формат: <МОДУЛЬ>/<ФУНКЦИЯ>)"; //Проверим, что есть что разбирать if (!sAppSrv) throw new ServerError(SERR_MODULES_NO_MODULE_SPECIFIED, `Не указаны модуль и функция обработчика ${sFormat}`); //Разбираем try { //Разбираем на модуль и функцию let moduleName = getAppSrvModuleName(sAppSrv); let funcName = getAppSrvFunctionName(sAppSrv); //Проверим, что есть и то и другое if (!moduleName) throw Error(`Обработчик ${sAppSrv} не указывает на модуль ${sFormat}`); if (!funcName) throw Error(`Обработчик ${sAppSrv} не указывает на функцию ${sFormat}`); //Подключаем модуль let mdl = null; try { mdl = require(makeModuleFullPath(moduleName)); } catch (e) { throw Error( `Не удалось подключить модуль ${moduleName}, проверье что он существует и не имеет синтаксических ошибок. Ошибка подключения: ${e.message}` ); } //Проверяем, что в нём есть эта функция if (!mdl[funcName]) throw Error(`Функция ${funcName} не определена в модуле ${moduleName}`); //Проверяем, что функция асинхронна и если это так - возвращаем её if ({}.toString.call(mdl[funcName]) === "[object AsyncFunction]") return mdl[funcName]; else throw Error(`Функция ${funcName} модуля ${moduleName} должна быть асинхронной`); } catch (e) { throw new ServerError(SERR_MODULES_BAD_INTERFACE, e.message); } }; //Отправка E-Mail уведомления const sendMail = prms => { return new Promise((resolve, reject) => { //Проверяем структуру переданного объекта для отправки E-Mail уведомления let sCheckResult = validateObject(prms, prmsUtilsSchema.sendMail, "Параметры функции отправки E-Mail уведомления"); //Если структура объекта в норме if (!sCheckResult) { //Формируем параметры для подключения к SMTP let transpOptions = { host: prms.mail.sHost, port: prms.mail.nPort, secure: prms.mail.bSecure, tls: { rejectUnauthorized: prms.mail.bRejectUnauthorized } }; //Если есть информация о пользователе - добавляем if (prms.mail.sUser) { transpOptions.auth = { user: prms.mail.sUser, pass: prms.mail.sPass }; } //Настраиваем подключение к SMTP-серверу let transporter = nodemailer.createTransport(transpOptions); //Параметры отправляемого сообщения let mailOptions = { from: prms.mail.sFrom, to: prms.sTo, subject: prms.sSubject, text: prms.sMessage, html: prms.sHTML, attachments: prms.attachments }; //Отправляем сообщение transporter.sendMail(mailOptions, (error, info) => { if (error) { reject(new ServerError(SERR_MAIL_FAILED, `${error.code}: ${error}`)); } else { if (info.rejected && Array.isArray(info.rejected) && info.rejected.length > 0) { reject(new ServerError(SERR_MAIL_FAILED, `Сообщение не доствлено адресатам: ${info.rejected.join(", ")}`)); } else { resolve(info); } } }); } else { reject(new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult)); } }); }; //Сборка URL по адресу сервиса и функции сервиса const buildURL = prms => { //Проверяем структуру переданного объекта для сборки URL let sCheckResult = validateObject(prms, prmsUtilsSchema.buildURL, "Параметры функции формирования URL"); //Если структура объекта в норме if (!sCheckResult) { //Формируем URL с учетом лишних "/" return `${prms.sSrvRoot.replace(/\/+$/, "")}/${prms.sFnURL.replace(/^\/+/, "")}${prms.sQuery ? `?${prms.sQuery}` : ""}`; } else { throw new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult); } }; //Получение списка IP-адресов хоста сервера const getIPs = () => { let ips = []; //получим список сетевых интерфейсов const ifaces = os.networkInterfaces(); //обходим сетевые интерфейсы Object.keys(ifaces).forEach(ifname => { ifaces[ifname].forEach(iface => { //пропускаем локальный адрес и не IPv4 адреса if ("IPv4" !== iface.family || iface.internal !== false) return; //добавим адрес к резульату ips.push(iface.address); }); }); //вернем ответ return ips; }; //Разбор XML (обёртка для async/await) const parseXML = prms => { return new Promise((resolve, reject) => { //Проверяем структуру переданного объекта для парсинша let sCheckResult = validateObject(prms, prmsUtilsSchema.parseXML, "Параметры функции разбора XML"); //Если структура объекта в норме if (!sCheckResult) { xml2js.parseString(prms.sXML, prms.options, (err, result) => { if (err) reject(err); else resolve(result); }); } else { reject(new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult)); } }); }; //Разбор параметров сообщения/ответа (XML > JSON) const parseOptionsXML = async prms => { //Проверяем структуру переданных параметров let sCheckResult = validateObject(prms, prmsUtilsSchema.parseOptionsXML, "Параметры функции разбора XML параметров сообщения/ответа"); //Если структура объекта в норме if (!sCheckResult) { try { parseRes = await parseXML({ sXML: prms.sOptions, options: { explicitArray: false, mergeAttrs: true, valueProcessors: [xml2js.processors.parseNumbers, xml2js.processors.parseBooleans] } }); return parseRes.root; } catch (e) { throw new Error("Ошибка рабора XML с параметрами сообщения/ответа: " + e); } } else { throw new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult); } }; //Сборка параметров сообщения/ответа (JSON > XML) const buildOptionsXML = prms => { //Проверяем структуру переданных параметров let sCheckResult = validateObject(prms, prmsUtilsSchema.buildOptionsXML, "Параметры функции сборки XML параметров сообщения/ответа"); //Если структура объекта в норме if (!sCheckResult) { try { let builder = new xml2js.Builder(); return builder.buildObject({ root: prms.options }); } catch (e) { throw new Error("Ошибка сборки XML с параметрами сообщения/ответа: " + e); } } else { throw new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult); } }; //Получение текущего времени в строковом формате const getNowString = () => { //Создадим объект даты const dNow = new Date(); //Возьмём его строковое представление const sNow = dNow.toLocaleString(); //Вернем результат return sNow; }; //Глубокое слияние объектов function deepMerge(...sources) { const isPlainObject = value => Object.prototype.toString.call(value) === "[object Object]"; const cloneValue = value => { return deepClone(value); }; const target = {}; for (const source of sources) { if (!isPlainObject(source)) continue; for (const [key, value] of Object.entries(source)) { if (isPlainObject(value)) { if (!isPlainObject(target[key])) target[key] = {}; target[key] = deepMerge(target[key], value); } else { target[key] = cloneValue(value); } } } return target; } //Глубокое копирование объекта const deepCopyObject = obj => JSON.parse(JSON.stringify(obj)); //Проверка на undefined const isUndefined = value => value === undefined; //Считывание параметров подключения для сервиса обмена (при service === "" считывание подключения "По умолчанию", settingsArray - массив объектов [{sService: "", ...},...]) const getConnectionSettings = (service, settingsArray) => { //Считываем параметры и возвращаем return settingsArray.find(connection => { return connection.sService === service; }); }; //Считывание параметров подключения к Kafka для сервиса обмена (kafka - массив объектов [{sService: "", ...},...]) const getKafkaConnectionSettings = (service, kafka) => { //Считываем подключение с указанным сервисом обмена let kafkaConnection = getConnectionSettings(service, kafka); //Если нет подключения с указанным сервисом обмена if (!kafkaConnection) { //Считываем "По умолчанию" kafkaConnection = getConnectionSettings("", kafka); } //Вернем результат return kafkaConnection; }; //Считывание параметров подключения к MQTT для сервиса обмена (mqtt - массив объектов [{sService: "", ...},...]) const getMQTTConnectionSettings = (service, mqtt) => { //Считываем подключение с указанным сервисом обмена let mqttConnection = getConnectionSettings(service, mqtt); //Если нет подключения с указанным сервисом обмена if (!mqttConnection) { //Считываем "По умолчанию" mqttConnection = getConnectionSettings("", mqtt); } //Вернем результат return mqttConnection; }; //Получение брокера Kafka по адресу сервиса обмена (прим. kafka://server.ru -> server.ru, https://server.ru => undefined) const getKafkaBroker = sURL => { //Если протокол URL - Kafka if (getURLProtocol(sURL) === SPROTOCOL_KAFKA) { //Возвращаем брокера return sURL.slice(8); } //Возвращаем undefined return; }; //Получение авторизации для Kafka const getKafkaAuth = (sUser, sPass, kafka) => { //Если аутентификация по SSL-сертификату if (kafka.bAuthSSL) { //Возвращаем авторизацию в формате SSL return { ssl: { rejectUnauthorized: kafka.ssl.bRejectUnauthorized, ca: kafka.ssl.sPathCa ? [fs.readFileSync(kafka.ssl.sPathCa, "utf-8")] : [], key: kafka.ssl.sPathKey ? fs.readFileSync(kafka.ssl.sPathKey, "utf-8") : "", cert: kafka.ssl.sPathCert ? fs.readFileSync(kafka.ssl.sPathCert, "utf-8") : "" } }; } //Возвращаем авторизацию по пользователю, если необходимо return sUser ? { ssl: true, sasl: { mechanism: "plain", username: sUser, password: sPass } } : null; }; //Получение протокола адреса (прим. mqtt://server.ru -> mqtt, https://server.ru => https, ...) const getURLProtocol = sURL => { //Если начинается с "/" - HTTP, иначе получаем из URL return sURL.substring(0, 1) === "/" ? SPROTOCOL_HTTP : new URL(sURL).protocol.slice(0, -1); }; //Wraps async task with timeout and abort support const wrapPromiseTimeout = (timeout, executor) => { if (!timeout || typeof executor !== "function") { return executor ? executor() : Promise.resolve(); } const controller = new AbortController(); const sMessage = `Истёк интервал ожидания (${timeout} мс) завершения асинхронного процесса.`; const timeoutError = new Error(sMessage); timeoutError.error = sMessage; let timeoutPid; const timeoutPromise = new Promise((_, reject) => { timeoutPid = setTimeout(() => { controller.abort(timeoutError); reject(timeoutError); }, timeout); }); const taskPromise = Promise.resolve(executor(controller.signal)); return Promise.race([taskPromise, timeoutPromise]).finally(() => { if (timeoutPid) clearTimeout(timeoutPid); }); }; //----------------- // Интерфейс модуля //----------------- exports.validateObject = validateObject; exports.makeModuleFullPath = makeModuleFullPath; exports.makeErrorText = makeErrorText; exports.getAppSrvModuleName = getAppSrvModuleName; exports.getAppSrvFunctionName = getAppSrvFunctionName; exports.getAppSrvFunction = getAppSrvFunction; exports.sendMail = sendMail; exports.buildURL = buildURL; exports.getIPs = getIPs; exports.parseXML = parseXML; exports.parseOptionsXML = parseOptionsXML; exports.buildOptionsXML = buildOptionsXML; exports.getNowString = getNowString; exports.deepMerge = deepMerge; exports.deepClone = deepClone; exports.deepCopyObject = deepCopyObject; exports.isUndefined = isUndefined; exports.getKafkaConnectionSettings = getKafkaConnectionSettings; exports.getMQTTConnectionSettings = getMQTTConnectionSettings; exports.getKafkaBroker = getKafkaBroker; exports.getKafkaAuth = getKafkaAuth; exports.getURLProtocol = getURLProtocol; exports.wrapPromiseTimeout = wrapPromiseTimeout;