/* Сервис интеграции ПП Парус 8 с WEB API Модуль ядра: вспомогательные функции */ //---------------------- // Подключение библиотек //---------------------- const fs = require("fs"); //Работа с файлами const _ = require("lodash"); //Работа с массивами и объектами 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 validateObject = (obj, schema, sObjName) => { //Объявим результат let sRes = ""; //Если пришла верная схема if (schema instanceof Schema) { //И есть что проверять if (obj) { //Сделаем это const objTmp = _.cloneDeep(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}" ` : " "}имеет некорректный формат: ${_.uniq(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; }; //Глубокое слияние объектов const deepMerge = (...args) => { let res = {}; for (let i = 0; i < args.length; i++) _.merge(res, args[i]); return res; }; //Глубокое копирование объекта 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); }; //Обёртывание промиса в таймаут исполнения const wrapPromiseTimeout = (timeout, promise) => { if (!timeout) return promise; let timeoutPid; const timeoutPromise = new Promise((resolve, reject) => { const sMessage = `Истёк интервал ожидания (${timeout} мс) завершения асинхронного процесса.`; let e = new Error(sMessage); e.error = sMessage; timeoutPid = setTimeout(() => reject(e), timeout); }); return Promise.race([promise, timeoutPromise]).finally(() => { if (promise.promise().isPending()) promise.cancel(); 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.deepCopyObject = deepCopyObject; exports.isUndefined = isUndefined; exports.getKafkaConnectionSettings = getKafkaConnectionSettings; exports.getMQTTConnectionSettings = getMQTTConnectionSettings; exports.getKafkaBroker = getKafkaBroker; exports.getKafkaAuth = getKafkaAuth; exports.getURLProtocol = getURLProtocol; exports.wrapPromiseTimeout = wrapPromiseTimeout;