564 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Сервис интеграции ПП Парус 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;