diff --git a/Common/sources/utils.js b/Common/sources/utils.js
index 942087c3..12e3d0d4 100644
--- a/Common/sources/utils.js
+++ b/Common/sources/utils.js
@@ -1,1335 +1,1324 @@
-/*
- * (c) Copyright Ascensio System SIA 2010-2024
- *
- * This program is a free software product. You can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License (AGPL)
- * version 3 as published by the Free Software Foundation. In accordance with
- * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
- * that Ascensio System SIA expressly excludes the warranty of non-infringement
- * of any third-party rights.
- *
- * This program is distributed WITHOUT ANY WARRANTY; without even the implied
- * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
- * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
- *
- * You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
- * street, Riga, Latvia, EU, LV-1050.
- *
- * The interactive user interfaces in modified source and object code versions
- * of the Program must display Appropriate Legal Notices, as required under
- * Section 5 of the GNU AGPL version 3.
- *
- * Pursuant to Section 7(b) of the License you must retain the original Product
- * logo when distributing the program. Pursuant to Section 7(e) we decline to
- * grant you any rights under trademark law for use of our trademarks.
- *
- * All the Product's GUI elements, including illustrations and icon sets, as
- * well as technical writing content are licensed under the terms of the
- * Creative Commons Attribution-ShareAlike 4.0 International. See the License
- * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
- *
- */
-
-'use strict';
-
-//Fix EPROTO error in node 8.x at some web sites(https://github.com/nodejs/node/issues/21513)
-require("tls").DEFAULT_ECDH_CURVE = "auto";
-
-const { pipeline } = require('node:stream/promises');
-const { buffer } = require('node:stream/consumers');
-const { Transform } = require('stream');
-var config = require('config');
-var fs = require('fs');
-var path = require('path');
-const crypto = require('crypto');
-var url = require('url');
-var axios = require('axios');
-var co = require('co');
-var URI = require("uri-js");
-const escapeStringRegexp = require('escape-string-regexp');
-const ipaddr = require('ipaddr.js');
-const getDnsCache = require('dnscache');
-const jwt = require('jsonwebtoken');
-const NodeCache = require( "node-cache" );
-const ms = require('ms');
-const constants = require('./constants');
-const commonDefines = require('./commondefines');
-const forwarded = require('forwarded');
-const { RequestFilteringHttpAgent, RequestFilteringHttpsAgent } = require("request-filtering-agent");
-const https = require('https');
-const http = require('http');
-const ca = require('win-ca/api');
-const util = require('util');
-
-const contentDisposition = require('content-disposition');
-const operationContext = require("./operationContext");
-
-//Clone sealed config objects before passing to external libraries using config.util.cloneDeep
-const cfgDnsCache = config.util.cloneDeep(config.get('dnscache'));
-const cfgIpFilterRules = config.get('services.CoAuthoring.ipfilter.rules');
-const cfgIpFilterErrorCode = config.get('services.CoAuthoring.ipfilter.errorcode');
-const cfgIpFilterUseForRequest = config.get('services.CoAuthoring.ipfilter.useforrequest');
-const cfgExpPemStdTtl = config.get('services.CoAuthoring.expire.pemStdTTL');
-const cfgExpPemCheckPeriod = config.get('services.CoAuthoring.expire.pemCheckPeriod');
-const cfgTokenOutboxHeader = config.get('services.CoAuthoring.token.outbox.header');
-const cfgTokenOutboxPrefix = config.get('services.CoAuthoring.token.outbox.prefix');
-const cfgTokenOutboxAlgorithm = config.get('services.CoAuthoring.token.outbox.algorithm');
-const cfgTokenOutboxExpires = config.get('services.CoAuthoring.token.outbox.expires');
-const cfgVisibilityTimeout = config.get('queue.visibilityTimeout');
-const cfgQueueRetentionPeriod = config.get('queue.retentionPeriod');
-const cfgRequestDefaults = config.util.cloneDeep(config.get('services.CoAuthoring.requestDefaults'));
-const cfgTokenEnableRequestOutbox = config.get('services.CoAuthoring.token.enable.request.outbox');
-const cfgTokenOutboxUrlExclusionRegex = config.get('services.CoAuthoring.token.outbox.urlExclusionRegex');
-const cfgSecret = config.get('aesEncrypt.secret');
-const cfgAESConfig = config.util.cloneDeep(config.get('aesEncrypt.config'));
-const cfgRequesFilteringAgent = config.get('services.CoAuthoring.request-filtering-agent');
-const cfgStorageExternalHost = config.get('storage.externalHost');
-const cfgExternalRequestDirectIfIn = config.get('externalRequest.directIfIn');
-const cfgExternalRequestAction = config.get('externalRequest.action');
-const cfgWinCa = config.util.cloneDeep(config.get('win-ca'));
-
-ca(cfgWinCa);
-
-const minimumIterationsByteLength = 4;
-const dnscache = getDnsCache(cfgDnsCache);
-
-var ANDROID_SAFE_FILENAME = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-+,@£$€!½§~\'=()[]{}0123456789';
-
-//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json
-BigInt.prototype.toJSON = function() { return this.toString() };
-
-var g_oIpFilterRules = new Map();
-function getIpFilterRule(address) {
- let exp = g_oIpFilterRules.get(address);
- if (!exp) {
- let regExpStr = address.split('*').map(escapeStringRegexp).join('.*');
- exp = new RegExp('^' + regExpStr + '$', 'i');
- g_oIpFilterRules.set(address, exp);
- }
- return exp;
-}
-const pemfileCache = new NodeCache({stdTTL: ms(cfgExpPemStdTtl) / 1000, checkperiod: ms(cfgExpPemCheckPeriod) / 1000, errorOnMissing: false, useClones: true});
-
-exports.getConvertionTimeout = function(opt_ctx) {
- if (opt_ctx) {
- const tenVisibilityTimeout = opt_ctx.getCfg('queue.visibilityTimeout', cfgVisibilityTimeout);
- const tenQueueRetentionPeriod = opt_ctx.getCfg('queue.retentionPeriod', cfgQueueRetentionPeriod);
- return 1.5 * (tenVisibilityTimeout + tenQueueRetentionPeriod) * 1000;
- } else {
- return 1.5 * (cfgVisibilityTimeout + cfgQueueRetentionPeriod) * 1000;
- }
-}
-
-exports.addSeconds = function(date, sec) {
- date.setSeconds(date.getSeconds() + sec);
-};
-exports.getMillisecondsOfHour = function(date) {
- return (date.getUTCMinutes() * 60 + date.getUTCSeconds()) * 1000 + date.getUTCMilliseconds();
-};
-exports.encodeXml = function(value) {
- return value.replace(/[<>&'"\r\n\t\xA0]/g, function (c) {
- switch (c) {
- case '<': return '<';
- case '>': return '>';
- case '&': return '&';
- case '\'': return ''';
- case '"': return '"';
- case '\r': return '
';
- case '\n': return '
';
- case '\t': return ' ';
- case '\xA0': return ' ';
- }
- });
-};
-function fsStat(fsPath) {
- return new Promise(function(resolve, reject) {
- fs.stat(fsPath, function(err, stats) {
- if (err) {
- reject(err);
- } else {
- resolve(stats);
- }
- });
- });
-}
-exports.fsStat = fsStat;
-function fsReadDir(fsPath) {
- return new Promise(function(resolve, reject) {
- fs.readdir(fsPath, function(err, list) {
- if (err) {
- return reject(err);
- } else {
- resolve(list);
- }
- });
- });
-}
-function* walkDir(fsPath, results, optNoSubDir, optOnlyFolders) {
- const list = yield fsReadDir(fsPath);
- for (let i = 0; i < list.length; ++i) {
- const file = path.join(fsPath, list[i]);
- let stats;
- try {
- stats = yield fsStat(file);
- } catch (e) {
- //exception if fsPath not exist
- stats = null;
- }
- if (!stats) {
- continue;
- }
- if (stats.isDirectory()) {
- if (optNoSubDir) {
- optOnlyFolders && results.push(file);
- } else {
- yield* walkDir(file, results, optNoSubDir, optOnlyFolders);
- }
- } else {
- !optOnlyFolders && results.push(file);
- }
- }
-}
-exports.listFolders = function(fsPath, optNoSubDir) {
- return co(function* () {
- let stats, list = [];
- try {
- stats = yield fsStat(fsPath);
- } catch (e) {
- //exception if fsPath not exist
- stats = null;
- }
- if (stats && stats.isDirectory()) {
- yield* walkDir(fsPath, list, optNoSubDir, true);
- }
- return list;
- });
-};
-exports.listObjects = function(fsPath, optNoSubDir) {
- return co(function* () {
- let stats, list = [];
- try {
- stats = yield fsStat(fsPath);
- } catch (e) {
- //exception if fsPath not exist
- stats = null;
- }
- if (stats) {
- if (stats.isDirectory()) {
- yield* walkDir(fsPath, list, optNoSubDir, false);
- } else {
- list.push(fsPath);
- }
- }
- return list;
- });
-};
-exports.sleep = function(ms) {
- return new Promise(function(resolve) {
- setTimeout(resolve, ms);
- });
-};
-exports.readFile = function(file) {
- return new Promise(function(resolve, reject) {
- fs.readFile(file, function(err, data) {
- if (err) {
- reject(err);
- } else {
- resolve(data);
- }
- });
- });
-};
-function makeAndroidSafeFileName(str) {
- for (var i = 0; i < str.length; i++) {
- if (-1 == ANDROID_SAFE_FILENAME.indexOf(str[i])) {
- str[i] = '_';
- }
- }
- return str;
-}
-function encodeRFC5987ValueChars(str) {
- return encodeURIComponent(str).
- // Note that although RFC3986 reserves "!", RFC5987 does not,
- // so we do not need to escape it
- replace(/['()]/g, escape). // i.e., %27 %28 %29
- replace(/\*/g, '%2A').
- // The following are not required for percent-encoding per RFC5987,
- // so we can allow for a little better readability over the wire: |`^
- replace(/%(?:7C|60|5E)/g, unescape);
-}
-function getContentDisposition (opt_filename, opt_useragent, opt_type) {
- let type = opt_type || constants.CONTENT_DISPOSITION_ATTACHMENT;
- return contentDisposition(opt_filename, {type: type});
-}
-exports.getContentDisposition = getContentDisposition;
-function raiseError(ro, code, msg) {
- ro.abort();
- let error = new Error(msg);
- error.code = code;
- ro.emit('error', error);
-}
-function raiseErrorObj(ro, error) {
- ro.abort();
- ro.emit('error', error);
-}
-function isRedirectResponse(response) {
- //All header names are lower cased and can be accessed using the bracket notation.
- return response && response.status >= 300 && response.status < 400 && !!response.headers['location'];
-}
-
-function isAllowDirectRequest(ctx, uri, isInJwtToken) {
- let res = false;
- const tenExternalRequestDirectIfIn = ctx.getCfg('externalRequest.directIfIn', cfgExternalRequestDirectIfIn);
- let allowList = tenExternalRequestDirectIfIn.allowList;
- if (allowList.length > 0) {
- let allowIndex = allowList.findIndex((allowPrefix) => {
- return uri.startsWith(allowPrefix);
- }, uri);
- res = -1 !== allowIndex;
- ctx.logger.debug("isAllowDirectRequest check allow list res=%s", res);
- } else if (tenExternalRequestDirectIfIn.jwtToken) {
- res = isInJwtToken;
- ctx.logger.debug("isAllowDirectRequest url in jwt token res=%s", res);
- }
- return res;
-}
-function addExternalRequestOptions(ctx, uri, isInJwtToken, options, httpAgentOptions, httpsAgentOptions) {
- let res = false;
- const tenExternalRequestAction = ctx.getCfg('externalRequest.action', cfgExternalRequestAction);
- const tenRequestFilteringAgent = ctx.getCfg('services.CoAuthoring.request-filtering-agent', cfgRequesFilteringAgent);
- if (isAllowDirectRequest(ctx, uri, isInJwtToken)) {
- res = true;
- } else if (tenExternalRequestAction.allow) {
- res = true;
- if (tenExternalRequestAction.blockPrivateIP) {
- options.httpsAgent = new RequestFilteringHttpsAgent({
- ...httpsAgentOptions,
- ...tenRequestFilteringAgent
- });
- options.httpAgent = new RequestFilteringHttpAgent({
- ...httpAgentOptions,
- ...tenRequestFilteringAgent
- });
- }
- if (tenExternalRequestAction.proxyUrl) {
- const proxyUrl = tenExternalRequestAction.proxyUrl;
- const parsedProxyUrl = url.parse(proxyUrl);
-
- options.proxy = {
- host: parsedProxyUrl.hostname,
- port: parsedProxyUrl.port,
- protocol: parsedProxyUrl.protocol
- };
- }
-
- if (tenExternalRequestAction.proxyUser?.username) {
- //This will set an `Proxy-Authorization` header, overwriting any existing
- //`Proxy-Authorization` custom headers you have set using `headers`.
- options.proxy.auth = tenExternalRequestAction.proxyUser;
- }
- if (tenExternalRequestAction.proxyHeaders) {
- options.headers = {
- ...options.headers,
- ...tenExternalRequestAction.proxyHeaders
- };
- }
- }
- return res;
-}
-/*
- * @param {object} options - The options object to modify.
- */
-function changeOptionsForCompatibilityWithRequest(options, httpAgentOptions, httpsAgentOptions) {
- if (false === options.followRedirect) {
- options.maxRedirects = 0;
- }
- if (false === options.gzip) {
- options.headers = { ...options.headers, 'Accept-Encoding': 'identity' };
- delete options.gzip;
- }
- if (options.forever !== undefined) {
- httpAgentOptions.keepAlive = !!options.forever;
- httpsAgentOptions.keepAlive = !!options.forever;
- }
-}
-/*
- * Download a URL and return the response.
- * @param {operationContext.Context} ctx - The operation context.
- * @param {string} uri - The URL to download.
- * @param {object} optTimeout - Optional timeout configuration.
- * @param {number} optLimit - Optional limit on the size of the response.
- * @param {string} opt_Authorization - Optional authorization header.
- * @param {boolean} opt_filterPrivate - Optional flag to filter private requests.
- * @param {object} opt_headers - Optional headers to include in the request.
- * @param {boolean} opt_returnStream - Optional flag to return stream.
- * @returns {Promise<{response: axios.AxiosResponse, sha256: string|null, body: Buffer|null, stream: NodeJS.ReadableStream|null}>} - A promise that resolves to object containing response, sha256 hash, and body (null if opt_streamWriter is provided).
- */
-async function downloadUrlPromise(ctx, uri, optTimeout, optLimit, opt_Authorization, opt_filterPrivate, opt_headers, opt_returnStream) {
- const tenTenantRequestDefaults = ctx.getCfg('services.CoAuthoring.requestDefaults', cfgRequestDefaults);
- const tenTokenOutboxHeader = ctx.getCfg('services.CoAuthoring.token.outbox.header', cfgTokenOutboxHeader);
- const tenTokenOutboxPrefix = ctx.getCfg('services.CoAuthoring.token.outbox.prefix', cfgTokenOutboxPrefix);
- let sizeLimit = optLimit || Number.MAX_VALUE;
- uri = URI.serialize(URI.parse(uri));
- const options = config.util.cloneDeep(tenTenantRequestDefaults);
-
- //baseRequest creates new agent(win-ca injects in globalAgent)
- const httpsAgentOptions = { ...https.globalAgent.options, ...options};
- const httpAgentOptions = { ...http.globalAgent.options, ...options};
- changeOptionsForCompatibilityWithRequest(options, httpAgentOptions, httpsAgentOptions);
-
- // if (optTimeout.connectionAndInactivity) {
- // httpAgentOptions.timeout = ms(optTimeout.connectionAndInactivity);
- // httpsAgentOptions.timeout = ms(optTimeout.connectionAndInactivity);
- // }
-
- if (!addExternalRequestOptions(ctx, uri, opt_filterPrivate, options, httpAgentOptions, httpsAgentOptions)) {
- throw new Error('Block external request. See externalRequest config options');
- }
-
- if (!options.httpsAgent || !options.httpAgent) {
- options.httpsAgent = new https.Agent(httpsAgentOptions);
- options.httpAgent = new http.Agent(httpAgentOptions);
- }
-
- const headers = { ...options.headers };
- if (opt_Authorization) {
- headers[tenTokenOutboxHeader] = tenTokenOutboxPrefix + opt_Authorization;
- }
- if (opt_headers) {
- Object.assign(headers, opt_headers);
- }
-
- const axiosConfig = {
- ...options,
- url: uri,
- method: 'GET',
- responseType: 'stream',
- headers,
- validateStatus: (status) => status >= 200 && status < 300,
- signal: optTimeout.wholeCycle && AbortSignal.timeout ? AbortSignal.timeout(ms(optTimeout.wholeCycle)) : undefined,
- timeout: optTimeout.connectionAndInactivity ? ms(optTimeout.connectionAndInactivity) : undefined,
- // cancelToken: new axios.CancelToken(cancel => {
- // if (optTimeout?.wholeCycle) {
- // setTimeout(() => {
- // cancel(`ETIMEDOUT: ${optTimeout.wholeCycle}`);
- // }, ms(optTimeout.wholeCycle));
- // }
- // }),
- };
- try {
- const response = await axios(axiosConfig);
- const { status, headers } = response;
- if (![200, 206].includes(status)) {
- const error = new Error(`Error response: statusCode:${status}; headers:${JSON.stringify(headers)};`);
- error.statusCode = status;
- error.response = response;
- throw error;
- }
-
- const contentLength = headers['content-length'];
- if (contentLength && parseInt(contentLength) > sizeLimit) {
- // Close the stream to prevent downloading
- const error = new Error('EMSGSIZE: Error response: content-length:' + contentLength);
- error.code = 'EMSGSIZE';
- response.data.destroy(error);
- throw error;
- }
- const limitedStream = new SizeLimitStream(optLimit);
- if (opt_returnStream) {
- // When returning a stream, we'll return the response for the caller to handle streaming
- // The content-length check is already done above
- return { response, sha256: null, body: null, stream: response.data.pipe(limitedStream) };
- }
-
- const body = await pipeline(response.data, limitedStream, buffer);
- const sha256 = crypto.createHash('sha256').update(body).digest('hex');
- return { response, sha256, body, stream: null };
- } catch (err) {
- if('ERR_CANCELED' === err.code) {
- err.code = 'ETIMEDOUT';
- } else if(['ECONNABORTED', 'ECONNRESET'].includes(err.code)) {
- err.code = 'ESOCKETTIMEDOUT';
- }
- if (err.status){
- err.statusCode = err.status;
- }
- throw err;
- }
-}
-
-async function postRequestPromise(ctx, uri, postData, postDataStream, postDataSize, optTimeout, opt_Authorization, opt_isInJwtToken, opt_headers) {
- const tenTenantRequestDefaults = ctx.getCfg('services.CoAuthoring.requestDefaults', cfgRequestDefaults);
- const tenTokenOutboxHeader = ctx.getCfg('services.CoAuthoring.token.outbox.header', cfgTokenOutboxHeader);
- const tenTokenOutboxPrefix = ctx.getCfg('services.CoAuthoring.token.outbox.prefix', cfgTokenOutboxPrefix);
- let connectionAndInactivity = optTimeout && optTimeout.connectionAndInactivity && ms(optTimeout.connectionAndInactivity);
- const wholeCycleTimeout = optTimeout?.wholeCycle ? ms(optTimeout.wholeCycle) : undefined;
- uri = URI.serialize(URI.parse(uri));
- let options = { ...tenTenantRequestDefaults };
- Object.assign(options, {
- method: 'post',
- url: uri,
- timeout: connectionAndInactivity,
- validateStatus: (status) => status === 200 || status === 204
- });
-
- if (options.gzip !== undefined && !options.gzip) {
- options.headers = { ...options.headers, 'Accept-Encoding': 'identity' };
- delete options.gzip;
- }
-
- if (!addExternalRequestOptions(ctx, uri, opt_isInJwtToken, options, http.globalAgent.options, https.globalAgent.options)) {
- throw new Error('Block external request. See externalRequest config options');
- }
- const protocol = new URL(uri).protocol;
- if (!options.httpsAgent && !options.httpAgent) {
- const agentOptions = {
- ...https.globalAgent.options,
- rejectUnauthorized: tenTenantRequestDefaults.rejectUnauthorized === false ? false : true
- };
-
- if (tenTenantRequestDefaults.forever !== undefined) {
- agentOptions.keepAlive = !!tenTenantRequestDefaults.forever;
- }
-
- if (protocol === 'https:') {
- options.httpsAgent = new https.Agent(agentOptions);
- } else if (protocol === 'http:') {
- options.httpAgent = new http.Agent(agentOptions);
- }
- }
- if (postData) {
- options.data = postData;
- } else if (postDataStream) {
- options.data = postDataStream;
- }
- options.headers = options.headers || {};
- if (opt_Authorization) {
- //todo ctx.getCfg
- options.headers[tenTokenOutboxHeader] = `${tenTokenOutboxPrefix}${opt_Authorization}`;
- }
- if (opt_headers) {
- Object.assign(options.headers, opt_headers);
- }
- if (undefined !== postDataSize) {
- //If no Content-Length is set, data will automatically be encoded in HTTP Chunked transfer encoding,
- //so that server knows when the data ends. The Transfer-Encoding: chunked header is added.
- //https://nodejs.org/api/http.html#requestwritechunk-encoding-callback
- //issue with Transfer-Encoding: chunked wopi and sharepoint 2019
- //https://community.alteryx.com/t5/Dev-Space/Download-Tool-amp-Microsoft-SharePoint-Chunked-Request-Error/td-p/735824
- options.headers['Content-Length'] = postDataSize;
- }
- const cancelTokenSource = axios.CancelToken.source();
- if (wholeCycleTimeout) {
- setTimeout(() => {
- cancelTokenSource.cancel(`Whole request cycle timeout: ${optTimeout.wholeCycle}`);
- }, wholeCycleTimeout);
- }
- options.cancelToken = cancelTokenSource.token;
- try {
- const response = await axios(options);
- return {
- response: {
- statusCode: response.status,
- headers: response.headers,
- body: response.data
- },
- body: JSON.stringify(response.data)
- }
- } catch (error) {
- if (axios.isCancel(error)) {
- const err = new Error(error.message);
- err.code = 'ETIMEDOUT';
- throw err;
- }
- if (error.response) {
- const { status, headers, data } = error.response;
- const err = new Error(`Error response: statusCode:${status}; headers:${JSON.stringify(headers)}; body:\r\n${data}`);
- err.statusCode = status;
- err.response = error.response;
- throw err;
- }
- throw error;
- }
-}
-exports.postRequestPromise = postRequestPromise;
-exports.downloadUrlPromise = downloadUrlPromise;
-exports.mapAscServerErrorToOldError = function(error) {
- var res = -1;
- switch (error) {
- case constants.NO_ERROR :
- case constants.CONVERT_CELLLIMITS :
- res = 0;
- break;
- case constants.TASK_QUEUE :
- case constants.TASK_RESULT :
- res = -6;
- break;
- case constants.CONVERT_PASSWORD :
- case constants.CONVERT_DRM :
- case constants.CONVERT_DRM_UNSUPPORTED :
- res = -5;
- break;
- case constants.CONVERT_DOWNLOAD :
- res = -4;
- break;
- case constants.CONVERT_TIMEOUT :
- case constants.CONVERT_DEAD_LETTER :
- res = -2;
- break;
- case constants.CONVERT_PARAMS :
- res = -7;
- break;
- case constants.CONVERT_LIMITS :
- res = -10;
- break;
- case constants.CONVERT_NEED_PARAMS :
- case constants.CONVERT_LIBREOFFICE :
- case constants.CONVERT_CORRUPTED :
- case constants.CONVERT_UNKNOWN_FORMAT :
- case constants.CONVERT_READ_FILE :
- case constants.CONVERT_TEMPORARY :
- case constants.CONVERT :
- res = -3;
- break;
- case constants.CONVERT_DETECT :
- res = -9;
- break;
- case constants.VKEY :
- case constants.VKEY_ENCRYPT :
- case constants.VKEY_KEY_EXPIRE :
- case constants.VKEY_USER_COUNT_EXCEED :
- res = -8;
- break;
- case constants.STORAGE :
- case constants.STORAGE_FILE_NO_FOUND :
- case constants.STORAGE_READ :
- case constants.STORAGE_WRITE :
- case constants.STORAGE_REMOVE_DIR :
- case constants.STORAGE_CREATE_DIR :
- case constants.STORAGE_GET_INFO :
- case constants.UPLOAD :
- case constants.READ_REQUEST_STREAM :
- case constants.UNKNOWN :
- res = -1;
- break;
- }
- return res;
-};
-function fillXmlResponse(val) {
- var xml = '';
- if (undefined != val.error) {
- xml += '' + exports.encodeXml(val.error.toString()) + '';
- } else {
- if (val.fileUrl) {
- xml += '' + exports.encodeXml(val.fileUrl) + '';
- } else {
- xml += '';
- }
- if (val.fileType) {
- xml += '' + exports.encodeXml(val.fileType) + '';
- } else {
- xml += '';
- }
- xml += '' + val.percent + '';
- xml += '' + (val.endConvert ? 'True' : 'False') + '';
- }
- xml += '';
- return xml;
-}
-
-function fillResponseSimple(res, str, contentType) {
- let body = Buffer.from(str, 'utf-8');
- res.setHeader('Content-Type', contentType + '; charset=UTF-8');
- res.setHeader('Content-Length', body.length);
- res.send(body);
-}
-function _fillResponse(res, output, isJSON) {
- let data;
- let contentType;
- if (isJSON) {
- data = JSON.stringify(output);
- contentType = 'application/json';
- } else {
- data = fillXmlResponse(output);
- contentType = 'text/xml';
- }
- fillResponseSimple(res, data, contentType);
-}
-
-function fillResponse(req, res, convertStatus, isJSON) {
- let output;
- if (constants.NO_ERROR != convertStatus.err) {
- output = {error: exports.mapAscServerErrorToOldError(convertStatus.err)};
- } else {
- output = {fileUrl: convertStatus.url, fileType: convertStatus.filetype, percent: (convertStatus.end ? 100 : 0), endConvert: convertStatus.end};
- }
- const accepts = isJSON ? ['json', 'xml'] : ['xml', 'json'];
- switch (req.accepts(accepts)) {
- case 'json':
- isJSON = true;
- break;
- case 'xml':
- isJSON = false;
- break;
- }
- _fillResponse(res, output, isJSON);
-}
-
-exports.fillResponseSimple = fillResponseSimple;
-exports.fillResponse = fillResponse;
-
-function fillResponseBuilder(res, key, urls, end, error) {
- let output;
- if (constants.NO_ERROR != error) {
- output = {error: exports.mapAscServerErrorToOldError(error)};
- } else {
- output = {key: key, urls: urls, end: end};
- }
- _fillResponse(res, output, true);
-}
-
-exports.fillResponseBuilder = fillResponseBuilder;
-
-function promiseCreateWriteStream(strPath, optOptions) {
- return new Promise(function(resolve, reject) {
- var file = fs.createWriteStream(strPath, optOptions);
- var errorCallback = function(e) {
- reject(e);
- };
- file.on('error', errorCallback);
- file.on('open', function() {
- file.removeListener('error', errorCallback);
- resolve(file);
- });
- });
-};
-exports.promiseCreateWriteStream = promiseCreateWriteStream;
-
-function promiseWaitDrain(stream) {
- return new Promise(function(resolve, reject) {
- stream.once('drain', resolve);
- });
-}
-exports.promiseWaitDrain = promiseWaitDrain;
-
-function promiseWaitClose(stream) {
- return new Promise(function(resolve, reject) {
- stream.once('close', resolve);
- });
-}
-exports.promiseWaitClose = promiseWaitClose;
-
-function promiseCreateReadStream(strPath) {
- return new Promise(function(resolve, reject) {
- var file = fs.createReadStream(strPath);
- var errorCallback = function(e) {
- reject(e);
- };
- file.on('error', errorCallback);
- file.on('open', function() {
- file.removeListener('error', errorCallback);
- resolve(file);
- });
- });
-};
-exports.promiseCreateReadStream = promiseCreateReadStream;
-exports.compareStringByLength = function(x, y) {
- if (x && y) {
- if (x.length == y.length) {
- return x.localeCompare(y);
- } else {
- return x.length - y.length;
- }
- } else {
- if (null != x) {
- return 1;
- } else if (null != y) {
- return -1;
- }
- }
- return 0;
-};
-exports.promiseRedis = function(client, func) {
- var newArguments = Array.prototype.slice.call(arguments, 2);
- return new Promise(function(resolve, reject) {
- newArguments.push(function(err, data) {
- if (err) {
- reject(err);
- } else {
- resolve(data);
- }
- });
- func.apply(client, newArguments);
- });
-};
-exports.containsAllAscii = function(str) {
- return /^[\000-\177]*$/.test(str);
-};
-function containsAllAsciiNP(str) {
- return /^[\040-\176]*$/.test(str);//non-printing characters
-}
-exports.containsAllAsciiNP = containsAllAsciiNP;
-function getDomain(hostHeader, forwardedHostHeader) {
- return forwardedHostHeader || hostHeader || 'localhost';
-};
-function getBaseUrl(protocol, hostHeader, forwardedProtoHeader, forwardedHostHeader, forwardedPrefixHeader) {
- var url = '';
- if (forwardedProtoHeader && constants.ALLOWED_PROTO.test(forwardedProtoHeader)) {
- url += forwardedProtoHeader;
- } else if (protocol && constants.ALLOWED_PROTO.test(protocol)) {
- url += protocol;
- } else {
- url += 'http';
- }
- url += '://';
- url += getDomain(hostHeader, forwardedHostHeader);
- if (forwardedPrefixHeader) {
- url += forwardedPrefixHeader;
- }
- return url;
-}
-function getBaseUrlByConnection(ctx, conn) {
- conn = conn.request;
- //Header names are lower-cased. https://nodejs.org/api/http.html#messageheaders
- let cloudfrontForwardedProto = conn.headers['cloudfront-forwarded-proto'];
- let forwardedProto = conn.headers['x-forwarded-proto'];
- let forwardedHost = conn.headers['x-forwarded-host'];
- let forwardedPrefix = conn.headers['x-forwarded-prefix'];
- let host = conn.headers['host'];
- let proto = cloudfrontForwardedProto || forwardedProto;
- ctx.logger.debug(`getBaseUrlByConnection host=%s x-forwarded-host=%s x-forwarded-proto=%s x-forwarded-prefix=%s cloudfront-forwarded-proto=%s `,
- host, forwardedHost, forwardedProto, forwardedPrefix, cloudfrontForwardedProto);
- return getBaseUrl('', host, proto, forwardedHost, forwardedPrefix);
-}
-function getBaseUrlByRequest(ctx, req) {
- //case-insensitive match. https://expressjs.com/en/api.html#req.get
- let cloudfrontForwardedProto = req.get('cloudfront-forwarded-proto');
- let forwardedProto = req.get('x-forwarded-proto');
- let forwardedHost = req.get('x-forwarded-host');
- let forwardedPrefix = req.get('x-forwarded-prefix');
- let host = req.get('host');
- let protocol = req.protocol;
- let proto = cloudfrontForwardedProto || forwardedProto;
- ctx.logger.debug(`getBaseUrlByRequest protocol=%s host=%s x-forwarded-host=%s x-forwarded-proto=%s x-forwarded-prefix=%s cloudfront-forwarded-proto=%s `,
- protocol, host, forwardedHost, forwardedProto, forwardedPrefix, cloudfrontForwardedProto);
- return getBaseUrl(protocol, host, proto, forwardedHost, forwardedPrefix);
-}
-exports.getBaseUrlByConnection = getBaseUrlByConnection;
-exports.getBaseUrlByRequest = getBaseUrlByRequest;
-function getDomainByConnection(ctx, conn) {
- let incomingMessage = conn.request;
- let host = incomingMessage.headers['host'];
- let forwardedHost = incomingMessage.headers['x-forwarded-host'];
- ctx.logger.debug("getDomainByConnection headers['host']=%s headers['x-forwarded-host']=%s", host, forwardedHost);
- return getDomain(host, forwardedHost);
-}
-function getDomainByRequest(ctx, req) {
- let host = req.get('host');
- let forwardedHost = req.get('x-forwarded-host');
- ctx.logger.debug("getDomainByRequest headers['host']=%s headers['x-forwarded-host']=%s", host, forwardedHost);
- return getDomain(req.get('host'), req.get('x-forwarded-host'));
-}
-exports.getDomainByConnection = getDomainByConnection;
-exports.getDomainByRequest = getDomainByRequest;
-function getShardKeyByConnection(ctx, conn) {
- return conn?.handshake?.query?.[constants.SHARD_KEY_API_NAME];
-}
-function getWopiSrcByConnection(ctx, conn) {
- return conn?.handshake?.query?.[constants.SHARD_KEY_WOPI_NAME];
-}
-function getShardKeyByRequest(ctx, req) {
- return req.query?.[constants.SHARD_KEY_API_NAME];
-}
-function getWopiSrcByRequest(ctx, req) {
- return req.query?.[constants.SHARD_KEY_WOPI_NAME];
-}
-exports.getShardKeyByConnection = getShardKeyByConnection;
-exports.getWopiSrcByConnection = getWopiSrcByConnection;
-exports.getShardKeyByRequest = getShardKeyByRequest;
-exports.getWopiSrcByRequest = getWopiSrcByRequest;
-function stream2Buffer(stream) {
- return new Promise(function(resolve, reject) {
- if (!stream.readable) {
- resolve(Buffer.alloc(0));
- }
- var bufs = [];
- stream.on('data', function(data) {
- bufs.push(data);
- });
- function onEnd(err) {
- if (err) {
- reject(err);
- } else {
- resolve(Buffer.concat(bufs));
- }
- }
- stream.on('end', onEnd);
- stream.on('error', onEnd);
- });
-}
-exports.stream2Buffer = stream2Buffer;
-function changeOnlyOfficeUrl(inputUrl, strPath, optFilename) {
- //onlyoffice file server expects url end with file extension
- if (-1 == inputUrl.indexOf('?')) {
- inputUrl += '?';
- } else {
- inputUrl += '&';
- }
- return inputUrl + constants.ONLY_OFFICE_URL_PARAM + '=' + constants.OUTPUT_NAME + path.extname(optFilename || strPath);
-}
-exports.changeOnlyOfficeUrl = changeOnlyOfficeUrl;
-function pipeStreams(from, to, isEnd) {
- return new Promise(function(resolve, reject) {
- from.pipe(to, {end: isEnd});
- from.on('end', function() {
- resolve();
- });
- from.on('error', function(e) {
- reject(e);
- });
- });
-}
-exports.pipeStreams = pipeStreams;
-function* pipeFiles(from, to) {
- var fromStream = yield promiseCreateReadStream(from);
- var toStream = yield promiseCreateWriteStream(to);
- yield pipeStreams(fromStream, toStream, true);
-}
-exports.pipeFiles = co.wrap(pipeFiles);
-function checkIpFilter(ctx, ipString, opt_hostname) {
- const tenIpFilterRules = ctx.getCfg('services.CoAuthoring.ipfilter.rules', cfgIpFilterRules);
-
- var status = 0;
- var ip4;
- var ip6;
- if (ipaddr.isValid(ipString)) {
- var ip = ipaddr.parse(ipString);
- if ('ipv6' === ip.kind()) {
- if (ip.isIPv4MappedAddress()) {
- ip4 = ip.toIPv4Address().toString();
- }
- ip6 = ip.toNormalizedString();
- } else {
- ip4 = ip.toString();
- ip6 = ip.toIPv4MappedAddress().toNormalizedString();
- }
- }
-
- for (let i = 0; i < tenIpFilterRules.length; ++i) {
- let rule = tenIpFilterRules[i];
- let exp = getIpFilterRule(rule.address);
- if ((opt_hostname && exp.test(opt_hostname)) || (ip4 && exp.test(ip4)) || (ip6 && exp.test(ip6))) {
- if (!rule.allowed) {
- const tenIpFilterErrorCode = ctx.getCfg('services.CoAuthoring.ipfilter.errorcode', cfgIpFilterErrorCode);
- status = tenIpFilterErrorCode;
- }
- break;
- }
- }
- return status;
-}
-exports.checkIpFilter = checkIpFilter;
-function* checkHostFilter(ctx, hostname) {
- let status = 0;
- let hostIp;
- try {
- hostIp = yield dnsLookup(hostname);
- } catch (e) {
- const tenIpFilterErrorCode = ctx.getCfg('services.CoAuthoring.ipfilter.errorcode', cfgIpFilterErrorCode);
- status = tenIpFilterErrorCode;
- ctx.logger.error('dnsLookup error: hostname = %s %s', hostname, e.stack);
- }
- if (0 === status) {
- status = checkIpFilter(ctx, hostIp, hostname);
- }
- return status;
-}
-exports.checkHostFilter = checkHostFilter;
-function checkClientIp(req, res, next) {
- let ctx = new operationContext.Context();
- ctx.initFromRequest(req);
- const tenIpFilterUseForRequest = ctx.getCfg('services.CoAuthoring.ipfilter.useforrequest', cfgIpFilterUseForRequest);
- let status = 0;
- if (tenIpFilterUseForRequest) {
- const addresses = forwarded(req);
- const ipString = addresses[addresses.length - 1];
- status = checkIpFilter(ctx, ipString);
- }
- if (status > 0) {
- res.sendStatus(status);
- } else {
- next();
- }
-}
-exports.checkClientIp = checkClientIp;
-function lowercaseQueryString(req, res, next) {
- for (var key in req.query) {
- if (req.query.hasOwnProperty(key) && key.toLowerCase() !== key) {
- req.query[key.toLowerCase()] = req.query[key];
- delete req.query[key];
- }
- }
- next();
-}
-exports.lowercaseQueryString = lowercaseQueryString;
-function dnsLookup(hostname, options) {
- return new Promise(function(resolve, reject) {
- dnscache.lookup(hostname, options, function(err, addresses){
- if (err) {
- reject(err);
- } else {
- resolve(addresses);
- }
- });
- });
-}
-exports.dnsLookup = dnsLookup;
-function isEmptyObject(val) {
- return !(val && Object.keys(val).length);
-}
-exports.isEmptyObject = isEmptyObject;
-function getSecretByElem(secretElem) {
- let secret;
- if (secretElem) {
- if (secretElem.string) {
- secret = secretElem.string;
- } else if (secretElem.file) {
- secret = pemfileCache.get(secretElem.file);
- if (!secret) {
- secret = fs.readFileSync(secretElem.file);
- pemfileCache.set(secretElem.file, secret);
- }
- }
- }
- return secret;
-}
-exports.getSecretByElem = getSecretByElem;
-function fillJwtForRequest(ctx, payload, secret, opt_inBody) {
- const tenTokenOutboxAlgorithm = ctx.getCfg('services.CoAuthoring.token.outbox.algorithm', cfgTokenOutboxAlgorithm);
- const tenTokenOutboxExpires = ctx.getCfg('services.CoAuthoring.token.outbox.expires', cfgTokenOutboxExpires);
- //todo refuse prototypes in payload(they are simple getter/setter).
- //JSON.parse/stringify is more universal but Object.assign is enough for our inputs
- payload = Object.assign(Object.create(null), payload);
- let data;
- if (opt_inBody) {
- data = payload;
- } else {
- data = {payload: payload};
- }
-
- let options = {algorithm: tenTokenOutboxAlgorithm, expiresIn: tenTokenOutboxExpires};
- return jwt.sign(data, secret, options);
-}
-exports.fillJwtForRequest = fillJwtForRequest;
-exports.forwarded = forwarded;
-exports.getIndexFromUserId = function(userId, userIdOriginal){
- return parseInt(userId.substring(userIdOriginal.length));
-};
-exports.checkPathTraversal = function(ctx, docId, rootDirectory, filename) {
- if (filename.indexOf('\0') !== -1) {
- ctx.logger.warn('checkPathTraversal Poison Null Bytes filename=%s', filename);
- return false;
- }
- if (!filename.startsWith(rootDirectory)) {
- ctx.logger.warn('checkPathTraversal Path Traversal filename=%s', filename);
- return false;
- }
- return true;
-};
-exports.getConnectionInfo = function(conn){
- var user = conn.user;
- var data = {
- id: user.id,
- idOriginal: user.idOriginal,
- username: user.username,
- indexUser: user.indexUser,
- view: user.view,
- connectionId: conn.id,
- isCloseCoAuthoring: conn.isCloseCoAuthoring,
- isLiveViewer: exports.isLiveViewer(conn),
- encrypted: conn.encrypted
- };
- return data;
-};
-exports.getConnectionInfoStr = function(conn){
- return JSON.stringify(exports.getConnectionInfo(conn));
-};
-exports.isLiveViewer = function(conn){
- return conn.user?.view && "fast" === conn.coEditingMode;
-};
-exports.isLiveViewerSupport = function(licenseInfo){
- return licenseInfo.connectionsView > 0 || licenseInfo.usersViewCount > 0;
-};
-exports.canIncludeOutboxAuthorization = function (ctx, url) {
- const tenTokenEnableRequestOutbox = ctx.getCfg('services.CoAuthoring.token.enable.request.outbox', cfgTokenEnableRequestOutbox);
- const tenTokenOutboxUrlExclusionRegex = ctx.getCfg('services.CoAuthoring.token.outbox.urlExclusionRegex', cfgTokenOutboxUrlExclusionRegex);
- if (tenTokenEnableRequestOutbox) {
- if (!tenTokenOutboxUrlExclusionRegex) {
- return true;
- } else if (!new RegExp(escapeStringRegexp(tenTokenOutboxUrlExclusionRegex)).test(url)) {
- return true;
- } else {
- ctx.logger.debug('canIncludeOutboxAuthorization excluded by token.outbox.urlExclusionRegex url=%s', url);
- }
- }
- return false;
-};
-/*
- Code samples taken from here: https://gist.github.com/btxtiger/e8eaee70d6e46729d127f1e384e755d6
- */
-exports.encryptPassword = async function (ctx, password) {
- const pbkdf2Promise = util.promisify(crypto.pbkdf2);
- const tenSecret = ctx.getCfg('aesEncrypt.secret', cfgSecret);
- const tenAESConfig = ctx.getCfg('aesEncrypt.config', cfgAESConfig) ?? {};
- const {
- keyByteLength = 32,
- saltByteLength = 64,
- initializationVectorByteLength = 16,
- iterationsByteLength = 5
- } = tenAESConfig;
-
- const salt = crypto.randomBytes(saltByteLength);
- const initializationVector = crypto.randomBytes(initializationVectorByteLength);
-
- const iterationsLength = iterationsByteLength < minimumIterationsByteLength ? minimumIterationsByteLength : iterationsByteLength;
- // Generate random count of iterations; 10.000 - 99.999 -> 5 bytes
- const lowerNumber = Math.pow(10, iterationsLength - 1);
- const greaterNumber = Math.pow(10, iterationsLength) - 1;
- const iterations = Math.floor(Math.random() * (greaterNumber - lowerNumber)) + lowerNumber;
-
- const encryptionKey = await pbkdf2Promise(tenSecret, salt, iterations, keyByteLength, 'sha512');
- //todo chacha20-poly1305 (clean db)
- const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey, initializationVector, {authTagLength:16});
- const encryptedData = Buffer.concat([cipher.update(password, 'utf8'), cipher.final()]);
- const authTag = cipher.getAuthTag();
- const predicate = iterations.toString(16);
- const data = Buffer.concat([salt, initializationVector, authTag, encryptedData]).toString('hex');
-
- return `${predicate}:${data}`;
-};
-exports.decryptPassword = async function (ctx, password) {
- const pbkdf2Promise = util.promisify(crypto.pbkdf2);
- const tenSecret = ctx.getCfg('aesEncrypt.secret', cfgSecret);
- const tenAESConfig = ctx.getCfg('aesEncrypt.config', cfgAESConfig) ?? {};
- const {
- keyByteLength = 32,
- saltByteLength = 64,
- initializationVectorByteLength = 16,
- } = tenAESConfig;
-
- const [iterations, dataHex] = password.split(':');
- const data = Buffer.from(dataHex, 'hex');
- // authTag in node.js equals 16 bytes(128 bits), see https://stackoverflow.com/questions/33976117/does-node-js-crypto-use-fixed-tag-size-with-gcm-mode
- const delta = [saltByteLength, initializationVectorByteLength, 16];
- const pointerArray = [];
-
- for (let byte = 0, i = 0; i < delta.length; i++) {
- const deltaValue = delta[i];
- pointerArray.push(data.subarray(byte, byte + deltaValue));
- byte += deltaValue;
-
- if (i === delta.length - 1) {
- pointerArray.push(data.subarray(byte));
- }
- }
-
- const [
- salt,
- initializationVector,
- authTag,
- encryptedData
- ] = pointerArray;
-
- const decryptionKey = await pbkdf2Promise(tenSecret, salt, parseInt(iterations, 16), keyByteLength, 'sha512');
- const decipher = crypto.createDecipheriv('aes-256-gcm', decryptionKey, initializationVector, {authTagLength:16});
- decipher.setAuthTag(authTag);
-
- return Buffer.concat([decipher.update(encryptedData, 'binary'), decipher.final()]).toString();
-};
-exports.getDateTimeTicks = function(date) {
- return BigInt(date.getTime() * 10000) + 621355968000000000n;
-};
-exports.convertLicenseInfoToFileParams = function(licenseInfo) {
- // todo
- // {
- // user_quota = 0;
- // portal_count = 0;
- // process = 2;
- // ssbranding = false;
- // whiteLabel = false;
- // }
- let license = {};
- license.start_date = licenseInfo.startDate && licenseInfo.startDate.toJSON();
- license.end_date = licenseInfo.endDate && licenseInfo.endDate.toJSON();
- license.timelimited = 0 !== (constants.LICENSE_MODE.Limited & licenseInfo.mode);
- license.trial = 0 !== (constants.LICENSE_MODE.Trial & licenseInfo.mode);
- license.developer = 0 !== (constants.LICENSE_MODE.Developer & licenseInfo.mode);
- license.branding = licenseInfo.branding;
- license.customization = licenseInfo.customization;
- license.advanced_api = licenseInfo.advancedApi;
- license.connections = licenseInfo.connections;
- license.connections_view = licenseInfo.connectionsView;
- license.users_count = licenseInfo.usersCount;
- license.users_view_count = licenseInfo.usersViewCount;
- license.users_expire = licenseInfo.usersExpire / constants.LICENSE_EXPIRE_USERS_ONE_DAY;
- license.customer_id = licenseInfo.customerId;
- license.alias = licenseInfo.alias;
- license.multitenancy = licenseInfo.multitenancy;
- return license;
-};
-exports.convertLicenseInfoToServerParams = function(licenseInfo) {
- let license = {};
- license.workersCount = licenseInfo.count;
- license.resultType = licenseInfo.type;
- license.packageType = licenseInfo.packageType;
- license.buildDate = licenseInfo.buildDate && licenseInfo.buildDate.toJSON();
- license.buildVersion = commonDefines.buildVersion;
- license.buildNumber = commonDefines.buildNumber;
- return license;
-};
-exports.checkBaseUrl = function(ctx, baseUrl, opt_storageCfg) {
- let storageExternalHost = opt_storageCfg ? opt_storageCfg.externalHost : cfgStorageExternalHost
- const tenStorageExternalHost = ctx.getCfg('storage.externalHost', storageExternalHost);
- return tenStorageExternalHost ? tenStorageExternalHost : baseUrl;
-};
-exports.resolvePath = function(object, path, defaultValue) {
- return path.split('.').reduce((o, p) => o ? o[p] : defaultValue, object);
-};
-Date.isLeapYear = function (year) {
- return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
-};
-
-Date.getDaysInMonth = function (year, month) {
- return [31, (Date.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
-};
-
-Date.prototype.isLeapYear = function () {
- return Date.isLeapYear(this.getUTCFullYear());
-};
-
-Date.prototype.getDaysInMonth = function () {
- return Date.getDaysInMonth(this.getUTCFullYear(), this.getUTCMonth());
-};
-
-Date.prototype.addMonths = function (value) {
- var n = this.getUTCDate();
- this.setUTCDate(1);
- this.setUTCMonth(this.getUTCMonth() + value);
- this.setUTCDate(Math.min(n, this.getDaysInMonth()));
- return this;
-};
-function getMonthDiff(d1, d2) {
- var months;
- months = (d2.getUTCFullYear() - d1.getUTCFullYear()) * 12;
- months -= d1.getUTCMonth();
- months += d2.getUTCMonth();
- return months;
-}
-exports.getMonthDiff = getMonthDiff;
-
-/**
- * A Transform stream that limits the size of data passing through it.
- * It will throw an EMSGSIZE error if the size exceeds the limit.
- *
- * @class SizeLimitStream
- * @extends {Transform}
- */
-class SizeLimitStream extends Transform {
- /**
- * Creates an instance of SizeLimitStream.
- * @param {number} sizeLimit - Maximum size in bytes that can pass through the stream
- * @memberof SizeLimitStream
- */
- constructor(sizeLimit) {
- super();
- this.sizeLimit = sizeLimit;
- this.bytesReceived = 0;
- }
-
- /**
- * Transform implementation that tracks the bytes received and enforces the size limit
- *
- * @param {Buffer|string} chunk - The chunk of data to process
- * @param {string} encoding - The encoding of the chunk if it's a string
- * @param {Function} callback - Called when processing is complete
- * @memberof SizeLimitStream
- */
- _transform(chunk, encoding, callback) {
- this.bytesReceived += chunk.length;
-
- if (this.sizeLimit && this.bytesReceived > this.sizeLimit) {
- const error = new Error(`EMSGSIZE: Response too large: ${this.bytesReceived} bytes (limit: ${this.sizeLimit} bytes)`);
- error.code = 'EMSGSIZE';
- callback(error);
- return;
- }
-
- callback(null, chunk);
- }
-}
-exports.getLicensePeriod = function(startDate, now) {
- startDate = new Date(startDate.getTime());//clone
- startDate.addMonths(getMonthDiff(startDate, now));
- if (startDate > now) {
- startDate.addMonths(-1);
- }
- startDate.setUTCHours(0,0,0,0);
- return startDate.getTime();
-};
-
-exports.removeIllegalCharacters = function(filename) {
- return filename?.replace(/[/\\?%*:|"<>]/g, '-') || filename;
-}
-exports.getFunctionArguments = function(func) {
- return func.toString().
- replace(/[\r\n\s]+/g, ' ').
- match(/(?:function\s*\w*)?\s*(?:\((.*?)\)|([^\s]+))/).
- slice(1, 3).
- join('').
- split(/\s*,\s*/);
-};
-exports.isUselesSfc = function(row, cmd) {
- return !(row && commonDefines.FileStatus.SaveVersion === row.status && cmd.getStatusInfoIn() === row.status_info);
-};
-exports.getChangesFileHeader = function() {
- return `CHANGES\t${commonDefines.buildVersion}\n`;
-};
-exports.checksumFile = function(hashName, path) {
- //https://stackoverflow.com/a/44643479
- return new Promise((resolve, reject) => {
- const hash = crypto.createHash(hashName);
- const stream = fs.createReadStream(path);
- stream.on('error', err => reject(err));
- stream.on('data', chunk => hash.update(chunk));
- stream.on('end', () => resolve(hash.digest('hex')));
- });
-};
-
-function isObject(item) {
- return (item && typeof item === 'object' && !Array.isArray(item));
-}
-
-function deepMergeObjects(target, ...sources) {
- if (!sources.length) {
- return target;
- }
-
- const source = sources.shift();
- if (isObject(target) && isObject(source)) {
- for (const key in source) {
- if (isObject(source[key])) {
- if (!target[key]) {
- Object.assign(target, { [key]: {} });
- }
-
- deepMergeObjects(target[key], source[key]);
- } else {
- Object.assign(target, { [key]: source[key] });
- }
- }
- }
-
- return deepMergeObjects(target, ...sources);
-}
-exports.isObject = isObject;
-exports.deepMergeObjects = deepMergeObjects;
+/*
+ * (c) Copyright Ascensio System SIA 2010-2024
+ *
+ * This program is a free software product. You can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License (AGPL)
+ * version 3 as published by the Free Software Foundation. In accordance with
+ * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
+ * that Ascensio System SIA expressly excludes the warranty of non-infringement
+ * of any third-party rights.
+ *
+ * This program is distributed WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
+ * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
+ *
+ * You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
+ * street, Riga, Latvia, EU, LV-1050.
+ *
+ * The interactive user interfaces in modified source and object code versions
+ * of the Program must display Appropriate Legal Notices, as required under
+ * Section 5 of the GNU AGPL version 3.
+ *
+ * Pursuant to Section 7(b) of the License you must retain the original Product
+ * logo when distributing the program. Pursuant to Section 7(e) we decline to
+ * grant you any rights under trademark law for use of our trademarks.
+ *
+ * All the Product's GUI elements, including illustrations and icon sets, as
+ * well as technical writing content are licensed under the terms of the
+ * Creative Commons Attribution-ShareAlike 4.0 International. See the License
+ * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
+ *
+ */
+
+'use strict';
+
+//Fix EPROTO error in node 8.x at some web sites(https://github.com/nodejs/node/issues/21513)
+require("tls").DEFAULT_ECDH_CURVE = "auto";
+
+const { pipeline } = require('node:stream/promises');
+const { buffer } = require('node:stream/consumers');
+const { Transform } = require('stream');
+var config = require('config');
+var fs = require('fs');
+var path = require('path');
+const crypto = require('crypto');
+var url = require('url');
+var axios = require('axios');
+var co = require('co');
+var URI = require("uri-js");
+const escapeStringRegexp = require('escape-string-regexp');
+const ipaddr = require('ipaddr.js');
+const getDnsCache = require('dnscache');
+const jwt = require('jsonwebtoken');
+const NodeCache = require( "node-cache" );
+const ms = require('ms');
+const constants = require('./constants');
+const commonDefines = require('./commondefines');
+const forwarded = require('forwarded');
+const { RequestFilteringHttpAgent, RequestFilteringHttpsAgent } = require("request-filtering-agent");
+const https = require('https');
+const http = require('http');
+const ca = require('win-ca/api');
+const util = require('util');
+
+const contentDisposition = require('content-disposition');
+const operationContext = require("./operationContext");
+
+//Clone sealed config objects before passing to external libraries using config.util.cloneDeep
+const cfgDnsCache = config.util.cloneDeep(config.get('dnscache'));
+const cfgIpFilterRules = config.get('services.CoAuthoring.ipfilter.rules');
+const cfgIpFilterErrorCode = config.get('services.CoAuthoring.ipfilter.errorcode');
+const cfgIpFilterUseForRequest = config.get('services.CoAuthoring.ipfilter.useforrequest');
+const cfgExpPemStdTtl = config.get('services.CoAuthoring.expire.pemStdTTL');
+const cfgExpPemCheckPeriod = config.get('services.CoAuthoring.expire.pemCheckPeriod');
+const cfgTokenOutboxHeader = config.get('services.CoAuthoring.token.outbox.header');
+const cfgTokenOutboxPrefix = config.get('services.CoAuthoring.token.outbox.prefix');
+const cfgTokenOutboxAlgorithm = config.get('services.CoAuthoring.token.outbox.algorithm');
+const cfgTokenOutboxExpires = config.get('services.CoAuthoring.token.outbox.expires');
+const cfgVisibilityTimeout = config.get('queue.visibilityTimeout');
+const cfgQueueRetentionPeriod = config.get('queue.retentionPeriod');
+const cfgRequestDefaults = config.util.cloneDeep(config.get('services.CoAuthoring.requestDefaults'));
+const cfgTokenEnableRequestOutbox = config.get('services.CoAuthoring.token.enable.request.outbox');
+const cfgTokenOutboxUrlExclusionRegex = config.get('services.CoAuthoring.token.outbox.urlExclusionRegex');
+const cfgSecret = config.get('aesEncrypt.secret');
+const cfgAESConfig = config.util.cloneDeep(config.get('aesEncrypt.config'));
+const cfgRequesFilteringAgent = config.get('services.CoAuthoring.request-filtering-agent');
+const cfgStorageExternalHost = config.get('storage.externalHost');
+const cfgExternalRequestDirectIfIn = config.get('externalRequest.directIfIn');
+const cfgExternalRequestAction = config.get('externalRequest.action');
+const cfgWinCa = config.util.cloneDeep(config.get('win-ca'));
+
+ca(cfgWinCa);
+
+const minimumIterationsByteLength = 4;
+const dnscache = getDnsCache(cfgDnsCache);
+
+var ANDROID_SAFE_FILENAME = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-+,@£$€!½§~\'=()[]{}0123456789';
+
+//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json
+BigInt.prototype.toJSON = function() { return this.toString() };
+
+var g_oIpFilterRules = new Map();
+function getIpFilterRule(address) {
+ let exp = g_oIpFilterRules.get(address);
+ if (!exp) {
+ let regExpStr = address.split('*').map(escapeStringRegexp).join('.*');
+ exp = new RegExp('^' + regExpStr + '$', 'i');
+ g_oIpFilterRules.set(address, exp);
+ }
+ return exp;
+}
+const pemfileCache = new NodeCache({stdTTL: ms(cfgExpPemStdTtl) / 1000, checkperiod: ms(cfgExpPemCheckPeriod) / 1000, errorOnMissing: false, useClones: true});
+
+exports.getConvertionTimeout = function(opt_ctx) {
+ if (opt_ctx) {
+ const tenVisibilityTimeout = opt_ctx.getCfg('queue.visibilityTimeout', cfgVisibilityTimeout);
+ const tenQueueRetentionPeriod = opt_ctx.getCfg('queue.retentionPeriod', cfgQueueRetentionPeriod);
+ return 1.5 * (tenVisibilityTimeout + tenQueueRetentionPeriod) * 1000;
+ } else {
+ return 1.5 * (cfgVisibilityTimeout + cfgQueueRetentionPeriod) * 1000;
+ }
+}
+
+exports.addSeconds = function(date, sec) {
+ date.setSeconds(date.getSeconds() + sec);
+};
+exports.getMillisecondsOfHour = function(date) {
+ return (date.getUTCMinutes() * 60 + date.getUTCSeconds()) * 1000 + date.getUTCMilliseconds();
+};
+exports.encodeXml = function(value) {
+ return value.replace(/[<>&'"\r\n\t\xA0]/g, function (c) {
+ switch (c) {
+ case '<': return '<';
+ case '>': return '>';
+ case '&': return '&';
+ case '\'': return ''';
+ case '"': return '"';
+ case '\r': return '
';
+ case '\n': return '
';
+ case '\t': return ' ';
+ case '\xA0': return ' ';
+ }
+ });
+};
+function fsStat(fsPath) {
+ return new Promise(function(resolve, reject) {
+ fs.stat(fsPath, function(err, stats) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(stats);
+ }
+ });
+ });
+}
+exports.fsStat = fsStat;
+function fsReadDir(fsPath) {
+ return new Promise(function(resolve, reject) {
+ fs.readdir(fsPath, function(err, list) {
+ if (err) {
+ return reject(err);
+ } else {
+ resolve(list);
+ }
+ });
+ });
+}
+function* walkDir(fsPath, results, optNoSubDir, optOnlyFolders) {
+ const list = yield fsReadDir(fsPath);
+ for (let i = 0; i < list.length; ++i) {
+ const file = path.join(fsPath, list[i]);
+ let stats;
+ try {
+ stats = yield fsStat(file);
+ } catch (e) {
+ //exception if fsPath not exist
+ stats = null;
+ }
+ if (!stats) {
+ continue;
+ }
+ if (stats.isDirectory()) {
+ if (optNoSubDir) {
+ optOnlyFolders && results.push(file);
+ } else {
+ yield* walkDir(file, results, optNoSubDir, optOnlyFolders);
+ }
+ } else {
+ !optOnlyFolders && results.push(file);
+ }
+ }
+}
+exports.listFolders = function(fsPath, optNoSubDir) {
+ return co(function* () {
+ let stats, list = [];
+ try {
+ stats = yield fsStat(fsPath);
+ } catch (e) {
+ //exception if fsPath not exist
+ stats = null;
+ }
+ if (stats && stats.isDirectory()) {
+ yield* walkDir(fsPath, list, optNoSubDir, true);
+ }
+ return list;
+ });
+};
+exports.listObjects = function(fsPath, optNoSubDir) {
+ return co(function* () {
+ let stats, list = [];
+ try {
+ stats = yield fsStat(fsPath);
+ } catch (e) {
+ //exception if fsPath not exist
+ stats = null;
+ }
+ if (stats) {
+ if (stats.isDirectory()) {
+ yield* walkDir(fsPath, list, optNoSubDir, false);
+ } else {
+ list.push(fsPath);
+ }
+ }
+ return list;
+ });
+};
+exports.sleep = function(ms) {
+ return new Promise(function(resolve) {
+ setTimeout(resolve, ms);
+ });
+};
+exports.readFile = function(file) {
+ return new Promise(function(resolve, reject) {
+ fs.readFile(file, function(err, data) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(data);
+ }
+ });
+ });
+};
+function makeAndroidSafeFileName(str) {
+ for (var i = 0; i < str.length; i++) {
+ if (-1 == ANDROID_SAFE_FILENAME.indexOf(str[i])) {
+ str[i] = '_';
+ }
+ }
+ return str;
+}
+function encodeRFC5987ValueChars(str) {
+ return encodeURIComponent(str).
+ // Note that although RFC3986 reserves "!", RFC5987 does not,
+ // so we do not need to escape it
+ replace(/['()]/g, escape). // i.e., %27 %28 %29
+ replace(/\*/g, '%2A').
+ // The following are not required for percent-encoding per RFC5987,
+ // so we can allow for a little better readability over the wire: |`^
+ replace(/%(?:7C|60|5E)/g, unescape);
+}
+function getContentDisposition (opt_filename, opt_useragent, opt_type) {
+ let type = opt_type || constants.CONTENT_DISPOSITION_ATTACHMENT;
+ return contentDisposition(opt_filename, {type: type});
+}
+exports.getContentDisposition = getContentDisposition;
+function raiseError(ro, code, msg) {
+ ro.abort();
+ let error = new Error(msg);
+ error.code = code;
+ ro.emit('error', error);
+}
+function raiseErrorObj(ro, error) {
+ ro.abort();
+ ro.emit('error', error);
+}
+function isRedirectResponse(response) {
+ //All header names are lower cased and can be accessed using the bracket notation.
+ return response && response.status >= 300 && response.status < 400 && !!response.headers['location'];
+}
+
+function isAllowDirectRequest(ctx, uri, isInJwtToken) {
+ let res = false;
+ const tenExternalRequestDirectIfIn = ctx.getCfg('externalRequest.directIfIn', cfgExternalRequestDirectIfIn);
+ let allowList = tenExternalRequestDirectIfIn.allowList;
+ if (allowList.length > 0) {
+ let allowIndex = allowList.findIndex((allowPrefix) => {
+ return uri.startsWith(allowPrefix);
+ }, uri);
+ res = -1 !== allowIndex;
+ ctx.logger.debug("isAllowDirectRequest check allow list res=%s", res);
+ } else if (tenExternalRequestDirectIfIn.jwtToken) {
+ res = isInJwtToken;
+ ctx.logger.debug("isAllowDirectRequest url in jwt token res=%s", res);
+ }
+ return res;
+}
+function addExternalRequestOptions(ctx, uri, isInJwtToken, options, httpAgentOptions, httpsAgentOptions) {
+ let res = false;
+ const tenExternalRequestAction = ctx.getCfg('externalRequest.action', cfgExternalRequestAction);
+ const tenRequestFilteringAgent = ctx.getCfg('services.CoAuthoring.request-filtering-agent', cfgRequesFilteringAgent);
+ if (isAllowDirectRequest(ctx, uri, isInJwtToken)) {
+ res = true;
+ } else if (tenExternalRequestAction.allow) {
+ res = true;
+ if (tenExternalRequestAction.blockPrivateIP) {
+ options.httpsAgent = new RequestFilteringHttpsAgent({
+ ...httpsAgentOptions,
+ ...tenRequestFilteringAgent
+ });
+ options.httpAgent = new RequestFilteringHttpAgent({
+ ...httpAgentOptions,
+ ...tenRequestFilteringAgent
+ });
+ }
+ if (tenExternalRequestAction.proxyUrl) {
+ const proxyUrl = tenExternalRequestAction.proxyUrl;
+ const parsedProxyUrl = url.parse(proxyUrl);
+
+ options.proxy = {
+ host: parsedProxyUrl.hostname,
+ port: parsedProxyUrl.port,
+ protocol: parsedProxyUrl.protocol
+ };
+ }
+
+ if (tenExternalRequestAction.proxyUser?.username) {
+ //This will set an `Proxy-Authorization` header, overwriting any existing
+ //`Proxy-Authorization` custom headers you have set using `headers`.
+ options.proxy.auth = tenExternalRequestAction.proxyUser;
+ }
+ if (tenExternalRequestAction.proxyHeaders) {
+ options.headers = {
+ ...options.headers,
+ ...tenExternalRequestAction.proxyHeaders
+ };
+ }
+ }
+ return res;
+}
+/*
+ * @param {object} options - The options object to modify.
+ */
+function changeOptionsForCompatibilityWithRequest(options, httpAgentOptions, httpsAgentOptions) {
+ if (false === options.followRedirect) {
+ options.maxRedirects = 0;
+ }
+ if (false === options.gzip) {
+ options.headers = { ...options.headers, 'Accept-Encoding': 'identity' };
+ delete options.gzip;
+ }
+ if (options.forever !== undefined) {
+ httpAgentOptions.keepAlive = !!options.forever;
+ httpsAgentOptions.keepAlive = !!options.forever;
+ }
+}
+/*
+ * Download a URL and return the response.
+ * @param {operationContext.Context} ctx - The operation context.
+ * @param {string} uri - The URL to download.
+ * @param {object} optTimeout - Optional timeout configuration.
+ * @param {number} optLimit - Optional limit on the size of the response.
+ * @param {string} opt_Authorization - Optional authorization header.
+ * @param {boolean} opt_filterPrivate - Optional flag to filter private requests.
+ * @param {object} opt_headers - Optional headers to include in the request.
+ * @param {boolean} opt_returnStream - Optional flag to return stream.
+ * @returns {Promise<{response: axios.AxiosResponse, sha256: string|null, body: Buffer|null, stream: NodeJS.ReadableStream|null}>} - A promise that resolves to object containing response, sha256 hash, and body (null if opt_streamWriter is provided).
+ */
+async function downloadUrlPromise(ctx, uri, optTimeout, optLimit, opt_Authorization, opt_filterPrivate, opt_headers, opt_returnStream) {
+ const tenTenantRequestDefaults = ctx.getCfg('services.CoAuthoring.requestDefaults', cfgRequestDefaults);
+ const tenTokenOutboxHeader = ctx.getCfg('services.CoAuthoring.token.outbox.header', cfgTokenOutboxHeader);
+ const tenTokenOutboxPrefix = ctx.getCfg('services.CoAuthoring.token.outbox.prefix', cfgTokenOutboxPrefix);
+ let sizeLimit = optLimit || Number.MAX_VALUE;
+ uri = URI.serialize(URI.parse(uri));
+ const options = config.util.cloneDeep(tenTenantRequestDefaults);
+
+ //baseRequest creates new agent(win-ca injects in globalAgent)
+ const httpsAgentOptions = { ...https.globalAgent.options, ...options};
+ const httpAgentOptions = { ...http.globalAgent.options, ...options};
+ changeOptionsForCompatibilityWithRequest(options, httpAgentOptions, httpsAgentOptions);
+
+ // if (optTimeout.connectionAndInactivity) {
+ // httpAgentOptions.timeout = ms(optTimeout.connectionAndInactivity);
+ // httpsAgentOptions.timeout = ms(optTimeout.connectionAndInactivity);
+ // }
+
+ if (!addExternalRequestOptions(ctx, uri, opt_filterPrivate, options, httpAgentOptions, httpsAgentOptions)) {
+ throw new Error('Block external request. See externalRequest config options');
+ }
+
+ if (!options.httpsAgent || !options.httpAgent) {
+ options.httpsAgent = new https.Agent(httpsAgentOptions);
+ options.httpAgent = new http.Agent(httpAgentOptions);
+ }
+
+ const headers = { ...options.headers };
+ if (opt_Authorization) {
+ headers[tenTokenOutboxHeader] = tenTokenOutboxPrefix + opt_Authorization;
+ }
+ if (opt_headers) {
+ Object.assign(headers, opt_headers);
+ }
+
+ const axiosConfig = {
+ ...options,
+ url: uri,
+ method: 'GET',
+ responseType: 'stream',
+ headers,
+ validateStatus: (status) => status >= 200 && status < 300,
+ signal: optTimeout.wholeCycle && AbortSignal.timeout ? AbortSignal.timeout(ms(optTimeout.wholeCycle)) : undefined,
+ timeout: optTimeout.connectionAndInactivity ? ms(optTimeout.connectionAndInactivity) : undefined,
+ // cancelToken: new axios.CancelToken(cancel => {
+ // if (optTimeout?.wholeCycle) {
+ // setTimeout(() => {
+ // cancel(`ETIMEDOUT: ${optTimeout.wholeCycle}`);
+ // }, ms(optTimeout.wholeCycle));
+ // }
+ // }),
+ };
+ try {
+ const response = await axios(axiosConfig);
+ const { status, headers } = response;
+ if (![200, 206].includes(status)) {
+ const error = new Error(`Error response: statusCode:${status}; headers:${JSON.stringify(headers)};`);
+ error.statusCode = status;
+ error.response = response;
+ throw error;
+ }
+
+ const contentLength = headers['content-length'];
+ if (contentLength && parseInt(contentLength) > sizeLimit) {
+ // Close the stream to prevent downloading
+ const error = new Error('EMSGSIZE: Error response: content-length:' + contentLength);
+ error.code = 'EMSGSIZE';
+ response.data.destroy(error);
+ throw error;
+ }
+ const limitedStream = new SizeLimitStream(optLimit);
+ if (opt_returnStream) {
+ // When returning a stream, we'll return the response for the caller to handle streaming
+ // The content-length check is already done above
+ return { response, sha256: null, body: null, stream: response.data.pipe(limitedStream) };
+ }
+
+ const body = await pipeline(response.data, limitedStream, buffer);
+ const sha256 = crypto.createHash('sha256').update(body).digest('hex');
+ return { response, sha256, body, stream: null };
+ } catch (err) {
+ if('ERR_CANCELED' === err.code) {
+ err.code = 'ETIMEDOUT';
+ } else if(['ECONNABORTED', 'ECONNRESET'].includes(err.code)) {
+ err.code = 'ESOCKETTIMEDOUT';
+ }
+ if (err.status){
+ err.statusCode = err.status;
+ }
+ throw err;
+ }
+}
+
+async function postRequestPromise(ctx, uri, postData, postDataStream, postDataSize, optTimeout, opt_Authorization, opt_isInJwtToken, opt_headers) {
+ const tenTenantRequestDefaults = ctx.getCfg('services.CoAuthoring.requestDefaults', cfgRequestDefaults);
+ const tenTokenOutboxHeader = ctx.getCfg('services.CoAuthoring.token.outbox.header', cfgTokenOutboxHeader);
+ const tenTokenOutboxPrefix = ctx.getCfg('services.CoAuthoring.token.outbox.prefix', cfgTokenOutboxPrefix);
+ uri = URI.serialize(URI.parse(uri));
+ const options = config.util.cloneDeep(tenTenantRequestDefaults);
+
+ const httpsAgentOptions = { ...https.globalAgent.options, ...options};
+ const httpAgentOptions = { ...http.globalAgent.options, ...options};
+ changeOptionsForCompatibilityWithRequest(options, httpAgentOptions, httpsAgentOptions);
+
+ if (!addExternalRequestOptions(ctx, uri, opt_isInJwtToken, options, httpAgentOptions, httpsAgentOptions)) {
+ throw new Error('Block external request. See externalRequest config options');
+ }
+
+ if (!options.httpsAgent || !options.httpAgent) {
+ options.httpsAgent = new https.Agent(httpsAgentOptions);
+ options.httpAgent = new http.Agent(httpAgentOptions);
+ }
+
+ const headers = { ...options.headers };
+ if (opt_Authorization) {
+ headers[tenTokenOutboxHeader] = tenTokenOutboxPrefix + opt_Authorization;
+ }
+ if (opt_headers) {
+ Object.assign(headers, opt_headers);
+ }
+ if (undefined !== postDataSize) {
+ //If no Content-Length is set, data will automatically be encoded in HTTP Chunked transfer encoding,
+ //so that server knows when the data ends. The Transfer-Encoding: chunked header is added.
+ //https://nodejs.org/api/http.html#requestwritechunk-encoding-callback
+ //issue with Transfer-Encoding: chunked wopi and sharepoint 2019
+ //https://community.alteryx.com/t5/Dev-Space/Download-Tool-amp-Microsoft-SharePoint-Chunked-Request-Error/td-p/735824
+ headers['Content-Length'] = postDataSize;
+ }
+
+ const axiosConfig = {
+ ...options,
+ url: uri,
+ method: 'POST',
+ headers,
+ validateStatus: (status) => status === 200 || status === 204,
+ signal: optTimeout?.wholeCycle && AbortSignal.timeout ? AbortSignal.timeout(ms(optTimeout.wholeCycle)) : undefined,
+ timeout: optTimeout?.connectionAndInactivity ? ms(optTimeout.connectionAndInactivity) : undefined,
+ };
+
+ if (postData) {
+ axiosConfig.data = postData;
+ } else if (postDataStream) {
+ axiosConfig.data = postDataStream;
+ }
+
+ try {
+ const response = await axios(axiosConfig);
+ const { status, headers, data } = response;
+
+ return {
+ response: {
+ statusCode: status,
+ headers: headers,
+ body: data
+ },
+ body: JSON.stringify(data)
+ };
+ } catch (err) {
+ if ('ERR_CANCELED' === err.code) {
+ err.code = 'ETIMEDOUT';
+ } else if (['ECONNABORTED', 'ECONNRESET'].includes(err.code)) {
+ err.code = 'ESOCKETTIMEDOUT';
+ }
+ if (err.status) {
+ err.statusCode = err.status;
+ }
+ if (err.response) {
+ const { status, headers, data } = err.response;
+ const error = new Error(`Error response: statusCode:${status}; headers:${JSON.stringify(headers)}; body:\r\n${data}`);
+ error.statusCode = status;
+ error.response = err.response;
+ throw error;
+ }
+ throw err;
+ }
+}
+exports.postRequestPromise = postRequestPromise;
+exports.downloadUrlPromise = downloadUrlPromise;
+exports.mapAscServerErrorToOldError = function(error) {
+ var res = -1;
+ switch (error) {
+ case constants.NO_ERROR :
+ case constants.CONVERT_CELLLIMITS :
+ res = 0;
+ break;
+ case constants.TASK_QUEUE :
+ case constants.TASK_RESULT :
+ res = -6;
+ break;
+ case constants.CONVERT_PASSWORD :
+ case constants.CONVERT_DRM :
+ case constants.CONVERT_DRM_UNSUPPORTED :
+ res = -5;
+ break;
+ case constants.CONVERT_DOWNLOAD :
+ res = -4;
+ break;
+ case constants.CONVERT_TIMEOUT :
+ case constants.CONVERT_DEAD_LETTER :
+ res = -2;
+ break;
+ case constants.CONVERT_PARAMS :
+ res = -7;
+ break;
+ case constants.CONVERT_LIMITS :
+ res = -10;
+ break;
+ case constants.CONVERT_NEED_PARAMS :
+ case constants.CONVERT_LIBREOFFICE :
+ case constants.CONVERT_CORRUPTED :
+ case constants.CONVERT_UNKNOWN_FORMAT :
+ case constants.CONVERT_READ_FILE :
+ case constants.CONVERT_TEMPORARY :
+ case constants.CONVERT :
+ res = -3;
+ break;
+ case constants.CONVERT_DETECT :
+ res = -9;
+ break;
+ case constants.VKEY :
+ case constants.VKEY_ENCRYPT :
+ case constants.VKEY_KEY_EXPIRE :
+ case constants.VKEY_USER_COUNT_EXCEED :
+ res = -8;
+ break;
+ case constants.STORAGE :
+ case constants.STORAGE_FILE_NO_FOUND :
+ case constants.STORAGE_READ :
+ case constants.STORAGE_WRITE :
+ case constants.STORAGE_REMOVE_DIR :
+ case constants.STORAGE_CREATE_DIR :
+ case constants.STORAGE_GET_INFO :
+ case constants.UPLOAD :
+ case constants.READ_REQUEST_STREAM :
+ case constants.UNKNOWN :
+ res = -1;
+ break;
+ }
+ return res;
+};
+function fillXmlResponse(val) {
+ var xml = '';
+ if (undefined != val.error) {
+ xml += '' + exports.encodeXml(val.error.toString()) + '';
+ } else {
+ if (val.fileUrl) {
+ xml += '' + exports.encodeXml(val.fileUrl) + '';
+ } else {
+ xml += '';
+ }
+ if (val.fileType) {
+ xml += '' + exports.encodeXml(val.fileType) + '';
+ } else {
+ xml += '';
+ }
+ xml += '' + val.percent + '';
+ xml += '' + (val.endConvert ? 'True' : 'False') + '';
+ }
+ xml += '';
+ return xml;
+}
+
+function fillResponseSimple(res, str, contentType) {
+ let body = Buffer.from(str, 'utf-8');
+ res.setHeader('Content-Type', contentType + '; charset=UTF-8');
+ res.setHeader('Content-Length', body.length);
+ res.send(body);
+}
+function _fillResponse(res, output, isJSON) {
+ let data;
+ let contentType;
+ if (isJSON) {
+ data = JSON.stringify(output);
+ contentType = 'application/json';
+ } else {
+ data = fillXmlResponse(output);
+ contentType = 'text/xml';
+ }
+ fillResponseSimple(res, data, contentType);
+}
+
+function fillResponse(req, res, convertStatus, isJSON) {
+ let output;
+ if (constants.NO_ERROR != convertStatus.err) {
+ output = {error: exports.mapAscServerErrorToOldError(convertStatus.err)};
+ } else {
+ output = {fileUrl: convertStatus.url, fileType: convertStatus.filetype, percent: (convertStatus.end ? 100 : 0), endConvert: convertStatus.end};
+ }
+ const accepts = isJSON ? ['json', 'xml'] : ['xml', 'json'];
+ switch (req.accepts(accepts)) {
+ case 'json':
+ isJSON = true;
+ break;
+ case 'xml':
+ isJSON = false;
+ break;
+ }
+ _fillResponse(res, output, isJSON);
+}
+
+exports.fillResponseSimple = fillResponseSimple;
+exports.fillResponse = fillResponse;
+
+function fillResponseBuilder(res, key, urls, end, error) {
+ let output;
+ if (constants.NO_ERROR != error) {
+ output = {error: exports.mapAscServerErrorToOldError(error)};
+ } else {
+ output = {key: key, urls: urls, end: end};
+ }
+ _fillResponse(res, output, true);
+}
+
+exports.fillResponseBuilder = fillResponseBuilder;
+
+function promiseCreateWriteStream(strPath, optOptions) {
+ return new Promise(function(resolve, reject) {
+ var file = fs.createWriteStream(strPath, optOptions);
+ var errorCallback = function(e) {
+ reject(e);
+ };
+ file.on('error', errorCallback);
+ file.on('open', function() {
+ file.removeListener('error', errorCallback);
+ resolve(file);
+ });
+ });
+};
+exports.promiseCreateWriteStream = promiseCreateWriteStream;
+
+function promiseWaitDrain(stream) {
+ return new Promise(function(resolve, reject) {
+ stream.once('drain', resolve);
+ });
+}
+exports.promiseWaitDrain = promiseWaitDrain;
+
+function promiseWaitClose(stream) {
+ return new Promise(function(resolve, reject) {
+ stream.once('close', resolve);
+ });
+}
+exports.promiseWaitClose = promiseWaitClose;
+
+function promiseCreateReadStream(strPath) {
+ return new Promise(function(resolve, reject) {
+ var file = fs.createReadStream(strPath);
+ var errorCallback = function(e) {
+ reject(e);
+ };
+ file.on('error', errorCallback);
+ file.on('open', function() {
+ file.removeListener('error', errorCallback);
+ resolve(file);
+ });
+ });
+};
+exports.promiseCreateReadStream = promiseCreateReadStream;
+exports.compareStringByLength = function(x, y) {
+ if (x && y) {
+ if (x.length == y.length) {
+ return x.localeCompare(y);
+ } else {
+ return x.length - y.length;
+ }
+ } else {
+ if (null != x) {
+ return 1;
+ } else if (null != y) {
+ return -1;
+ }
+ }
+ return 0;
+};
+exports.promiseRedis = function(client, func) {
+ var newArguments = Array.prototype.slice.call(arguments, 2);
+ return new Promise(function(resolve, reject) {
+ newArguments.push(function(err, data) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(data);
+ }
+ });
+ func.apply(client, newArguments);
+ });
+};
+exports.containsAllAscii = function(str) {
+ return /^[\000-\177]*$/.test(str);
+};
+function containsAllAsciiNP(str) {
+ return /^[\040-\176]*$/.test(str);//non-printing characters
+}
+exports.containsAllAsciiNP = containsAllAsciiNP;
+function getDomain(hostHeader, forwardedHostHeader) {
+ return forwardedHostHeader || hostHeader || 'localhost';
+};
+function getBaseUrl(protocol, hostHeader, forwardedProtoHeader, forwardedHostHeader, forwardedPrefixHeader) {
+ var url = '';
+ if (forwardedProtoHeader && constants.ALLOWED_PROTO.test(forwardedProtoHeader)) {
+ url += forwardedProtoHeader;
+ } else if (protocol && constants.ALLOWED_PROTO.test(protocol)) {
+ url += protocol;
+ } else {
+ url += 'http';
+ }
+ url += '://';
+ url += getDomain(hostHeader, forwardedHostHeader);
+ if (forwardedPrefixHeader) {
+ url += forwardedPrefixHeader;
+ }
+ return url;
+}
+function getBaseUrlByConnection(ctx, conn) {
+ conn = conn.request;
+ //Header names are lower-cased. https://nodejs.org/api/http.html#messageheaders
+ let cloudfrontForwardedProto = conn.headers['cloudfront-forwarded-proto'];
+ let forwardedProto = conn.headers['x-forwarded-proto'];
+ let forwardedHost = conn.headers['x-forwarded-host'];
+ let forwardedPrefix = conn.headers['x-forwarded-prefix'];
+ let host = conn.headers['host'];
+ let proto = cloudfrontForwardedProto || forwardedProto;
+ ctx.logger.debug(`getBaseUrlByConnection host=%s x-forwarded-host=%s x-forwarded-proto=%s x-forwarded-prefix=%s cloudfront-forwarded-proto=%s `,
+ host, forwardedHost, forwardedProto, forwardedPrefix, cloudfrontForwardedProto);
+ return getBaseUrl('', host, proto, forwardedHost, forwardedPrefix);
+}
+function getBaseUrlByRequest(ctx, req) {
+ //case-insensitive match. https://expressjs.com/en/api.html#req.get
+ let cloudfrontForwardedProto = req.get('cloudfront-forwarded-proto');
+ let forwardedProto = req.get('x-forwarded-proto');
+ let forwardedHost = req.get('x-forwarded-host');
+ let forwardedPrefix = req.get('x-forwarded-prefix');
+ let host = req.get('host');
+ let protocol = req.protocol;
+ let proto = cloudfrontForwardedProto || forwardedProto;
+ ctx.logger.debug(`getBaseUrlByRequest protocol=%s host=%s x-forwarded-host=%s x-forwarded-proto=%s x-forwarded-prefix=%s cloudfront-forwarded-proto=%s `,
+ protocol, host, forwardedHost, forwardedProto, forwardedPrefix, cloudfrontForwardedProto);
+ return getBaseUrl(protocol, host, proto, forwardedHost, forwardedPrefix);
+}
+exports.getBaseUrlByConnection = getBaseUrlByConnection;
+exports.getBaseUrlByRequest = getBaseUrlByRequest;
+function getDomainByConnection(ctx, conn) {
+ let incomingMessage = conn.request;
+ let host = incomingMessage.headers['host'];
+ let forwardedHost = incomingMessage.headers['x-forwarded-host'];
+ ctx.logger.debug("getDomainByConnection headers['host']=%s headers['x-forwarded-host']=%s", host, forwardedHost);
+ return getDomain(host, forwardedHost);
+}
+function getDomainByRequest(ctx, req) {
+ let host = req.get('host');
+ let forwardedHost = req.get('x-forwarded-host');
+ ctx.logger.debug("getDomainByRequest headers['host']=%s headers['x-forwarded-host']=%s", host, forwardedHost);
+ return getDomain(req.get('host'), req.get('x-forwarded-host'));
+}
+exports.getDomainByConnection = getDomainByConnection;
+exports.getDomainByRequest = getDomainByRequest;
+function getShardKeyByConnection(ctx, conn) {
+ return conn?.handshake?.query?.[constants.SHARD_KEY_API_NAME];
+}
+function getWopiSrcByConnection(ctx, conn) {
+ return conn?.handshake?.query?.[constants.SHARD_KEY_WOPI_NAME];
+}
+function getShardKeyByRequest(ctx, req) {
+ return req.query?.[constants.SHARD_KEY_API_NAME];
+}
+function getWopiSrcByRequest(ctx, req) {
+ return req.query?.[constants.SHARD_KEY_WOPI_NAME];
+}
+exports.getShardKeyByConnection = getShardKeyByConnection;
+exports.getWopiSrcByConnection = getWopiSrcByConnection;
+exports.getShardKeyByRequest = getShardKeyByRequest;
+exports.getWopiSrcByRequest = getWopiSrcByRequest;
+function stream2Buffer(stream) {
+ return new Promise(function(resolve, reject) {
+ if (!stream.readable) {
+ resolve(Buffer.alloc(0));
+ }
+ var bufs = [];
+ stream.on('data', function(data) {
+ bufs.push(data);
+ });
+ function onEnd(err) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(Buffer.concat(bufs));
+ }
+ }
+ stream.on('end', onEnd);
+ stream.on('error', onEnd);
+ });
+}
+exports.stream2Buffer = stream2Buffer;
+function changeOnlyOfficeUrl(inputUrl, strPath, optFilename) {
+ //onlyoffice file server expects url end with file extension
+ if (-1 == inputUrl.indexOf('?')) {
+ inputUrl += '?';
+ } else {
+ inputUrl += '&';
+ }
+ return inputUrl + constants.ONLY_OFFICE_URL_PARAM + '=' + constants.OUTPUT_NAME + path.extname(optFilename || strPath);
+}
+exports.changeOnlyOfficeUrl = changeOnlyOfficeUrl;
+function pipeStreams(from, to, isEnd) {
+ return new Promise(function(resolve, reject) {
+ from.pipe(to, {end: isEnd});
+ from.on('end', function() {
+ resolve();
+ });
+ from.on('error', function(e) {
+ reject(e);
+ });
+ });
+}
+exports.pipeStreams = pipeStreams;
+function* pipeFiles(from, to) {
+ var fromStream = yield promiseCreateReadStream(from);
+ var toStream = yield promiseCreateWriteStream(to);
+ yield pipeStreams(fromStream, toStream, true);
+}
+exports.pipeFiles = co.wrap(pipeFiles);
+function checkIpFilter(ctx, ipString, opt_hostname) {
+ const tenIpFilterRules = ctx.getCfg('services.CoAuthoring.ipfilter.rules', cfgIpFilterRules);
+
+ var status = 0;
+ var ip4;
+ var ip6;
+ if (ipaddr.isValid(ipString)) {
+ var ip = ipaddr.parse(ipString);
+ if ('ipv6' === ip.kind()) {
+ if (ip.isIPv4MappedAddress()) {
+ ip4 = ip.toIPv4Address().toString();
+ }
+ ip6 = ip.toNormalizedString();
+ } else {
+ ip4 = ip.toString();
+ ip6 = ip.toIPv4MappedAddress().toNormalizedString();
+ }
+ }
+
+ for (let i = 0; i < tenIpFilterRules.length; ++i) {
+ let rule = tenIpFilterRules[i];
+ let exp = getIpFilterRule(rule.address);
+ if ((opt_hostname && exp.test(opt_hostname)) || (ip4 && exp.test(ip4)) || (ip6 && exp.test(ip6))) {
+ if (!rule.allowed) {
+ const tenIpFilterErrorCode = ctx.getCfg('services.CoAuthoring.ipfilter.errorcode', cfgIpFilterErrorCode);
+ status = tenIpFilterErrorCode;
+ }
+ break;
+ }
+ }
+ return status;
+}
+exports.checkIpFilter = checkIpFilter;
+function* checkHostFilter(ctx, hostname) {
+ let status = 0;
+ let hostIp;
+ try {
+ hostIp = yield dnsLookup(hostname);
+ } catch (e) {
+ const tenIpFilterErrorCode = ctx.getCfg('services.CoAuthoring.ipfilter.errorcode', cfgIpFilterErrorCode);
+ status = tenIpFilterErrorCode;
+ ctx.logger.error('dnsLookup error: hostname = %s %s', hostname, e.stack);
+ }
+ if (0 === status) {
+ status = checkIpFilter(ctx, hostIp, hostname);
+ }
+ return status;
+}
+exports.checkHostFilter = checkHostFilter;
+function checkClientIp(req, res, next) {
+ let ctx = new operationContext.Context();
+ ctx.initFromRequest(req);
+ const tenIpFilterUseForRequest = ctx.getCfg('services.CoAuthoring.ipfilter.useforrequest', cfgIpFilterUseForRequest);
+ let status = 0;
+ if (tenIpFilterUseForRequest) {
+ const addresses = forwarded(req);
+ const ipString = addresses[addresses.length - 1];
+ status = checkIpFilter(ctx, ipString);
+ }
+ if (status > 0) {
+ res.sendStatus(status);
+ } else {
+ next();
+ }
+}
+exports.checkClientIp = checkClientIp;
+function lowercaseQueryString(req, res, next) {
+ for (var key in req.query) {
+ if (req.query.hasOwnProperty(key) && key.toLowerCase() !== key) {
+ req.query[key.toLowerCase()] = req.query[key];
+ delete req.query[key];
+ }
+ }
+ next();
+}
+exports.lowercaseQueryString = lowercaseQueryString;
+function dnsLookup(hostname, options) {
+ return new Promise(function(resolve, reject) {
+ dnscache.lookup(hostname, options, function(err, addresses){
+ if (err) {
+ reject(err);
+ } else {
+ resolve(addresses);
+ }
+ });
+ });
+}
+exports.dnsLookup = dnsLookup;
+function isEmptyObject(val) {
+ return !(val && Object.keys(val).length);
+}
+exports.isEmptyObject = isEmptyObject;
+function getSecretByElem(secretElem) {
+ let secret;
+ if (secretElem) {
+ if (secretElem.string) {
+ secret = secretElem.string;
+ } else if (secretElem.file) {
+ secret = pemfileCache.get(secretElem.file);
+ if (!secret) {
+ secret = fs.readFileSync(secretElem.file);
+ pemfileCache.set(secretElem.file, secret);
+ }
+ }
+ }
+ return secret;
+}
+exports.getSecretByElem = getSecretByElem;
+function fillJwtForRequest(ctx, payload, secret, opt_inBody) {
+ const tenTokenOutboxAlgorithm = ctx.getCfg('services.CoAuthoring.token.outbox.algorithm', cfgTokenOutboxAlgorithm);
+ const tenTokenOutboxExpires = ctx.getCfg('services.CoAuthoring.token.outbox.expires', cfgTokenOutboxExpires);
+ //todo refuse prototypes in payload(they are simple getter/setter).
+ //JSON.parse/stringify is more universal but Object.assign is enough for our inputs
+ payload = Object.assign(Object.create(null), payload);
+ let data;
+ if (opt_inBody) {
+ data = payload;
+ } else {
+ data = {payload: payload};
+ }
+
+ let options = {algorithm: tenTokenOutboxAlgorithm, expiresIn: tenTokenOutboxExpires};
+ return jwt.sign(data, secret, options);
+}
+exports.fillJwtForRequest = fillJwtForRequest;
+exports.forwarded = forwarded;
+exports.getIndexFromUserId = function(userId, userIdOriginal){
+ return parseInt(userId.substring(userIdOriginal.length));
+};
+exports.checkPathTraversal = function(ctx, docId, rootDirectory, filename) {
+ if (filename.indexOf('\0') !== -1) {
+ ctx.logger.warn('checkPathTraversal Poison Null Bytes filename=%s', filename);
+ return false;
+ }
+ if (!filename.startsWith(rootDirectory)) {
+ ctx.logger.warn('checkPathTraversal Path Traversal filename=%s', filename);
+ return false;
+ }
+ return true;
+};
+exports.getConnectionInfo = function(conn){
+ var user = conn.user;
+ var data = {
+ id: user.id,
+ idOriginal: user.idOriginal,
+ username: user.username,
+ indexUser: user.indexUser,
+ view: user.view,
+ connectionId: conn.id,
+ isCloseCoAuthoring: conn.isCloseCoAuthoring,
+ isLiveViewer: exports.isLiveViewer(conn),
+ encrypted: conn.encrypted
+ };
+ return data;
+};
+exports.getConnectionInfoStr = function(conn){
+ return JSON.stringify(exports.getConnectionInfo(conn));
+};
+exports.isLiveViewer = function(conn){
+ return conn.user?.view && "fast" === conn.coEditingMode;
+};
+exports.isLiveViewerSupport = function(licenseInfo){
+ return licenseInfo.connectionsView > 0 || licenseInfo.usersViewCount > 0;
+};
+exports.canIncludeOutboxAuthorization = function (ctx, url) {
+ const tenTokenEnableRequestOutbox = ctx.getCfg('services.CoAuthoring.token.enable.request.outbox', cfgTokenEnableRequestOutbox);
+ const tenTokenOutboxUrlExclusionRegex = ctx.getCfg('services.CoAuthoring.token.outbox.urlExclusionRegex', cfgTokenOutboxUrlExclusionRegex);
+ if (tenTokenEnableRequestOutbox) {
+ if (!tenTokenOutboxUrlExclusionRegex) {
+ return true;
+ } else if (!new RegExp(escapeStringRegexp(tenTokenOutboxUrlExclusionRegex)).test(url)) {
+ return true;
+ } else {
+ ctx.logger.debug('canIncludeOutboxAuthorization excluded by token.outbox.urlExclusionRegex url=%s', url);
+ }
+ }
+ return false;
+};
+/*
+ Code samples taken from here: https://gist.github.com/btxtiger/e8eaee70d6e46729d127f1e384e755d6
+ */
+exports.encryptPassword = async function (ctx, password) {
+ const pbkdf2Promise = util.promisify(crypto.pbkdf2);
+ const tenSecret = ctx.getCfg('aesEncrypt.secret', cfgSecret);
+ const tenAESConfig = ctx.getCfg('aesEncrypt.config', cfgAESConfig) ?? {};
+ const {
+ keyByteLength = 32,
+ saltByteLength = 64,
+ initializationVectorByteLength = 16,
+ iterationsByteLength = 5
+ } = tenAESConfig;
+
+ const salt = crypto.randomBytes(saltByteLength);
+ const initializationVector = crypto.randomBytes(initializationVectorByteLength);
+
+ const iterationsLength = iterationsByteLength < minimumIterationsByteLength ? minimumIterationsByteLength : iterationsByteLength;
+ // Generate random count of iterations; 10.000 - 99.999 -> 5 bytes
+ const lowerNumber = Math.pow(10, iterationsLength - 1);
+ const greaterNumber = Math.pow(10, iterationsLength) - 1;
+ const iterations = Math.floor(Math.random() * (greaterNumber - lowerNumber)) + lowerNumber;
+
+ const encryptionKey = await pbkdf2Promise(tenSecret, salt, iterations, keyByteLength, 'sha512');
+ //todo chacha20-poly1305 (clean db)
+ const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey, initializationVector, {authTagLength:16});
+ const encryptedData = Buffer.concat([cipher.update(password, 'utf8'), cipher.final()]);
+ const authTag = cipher.getAuthTag();
+ const predicate = iterations.toString(16);
+ const data = Buffer.concat([salt, initializationVector, authTag, encryptedData]).toString('hex');
+
+ return `${predicate}:${data}`;
+};
+exports.decryptPassword = async function (ctx, password) {
+ const pbkdf2Promise = util.promisify(crypto.pbkdf2);
+ const tenSecret = ctx.getCfg('aesEncrypt.secret', cfgSecret);
+ const tenAESConfig = ctx.getCfg('aesEncrypt.config', cfgAESConfig) ?? {};
+ const {
+ keyByteLength = 32,
+ saltByteLength = 64,
+ initializationVectorByteLength = 16,
+ } = tenAESConfig;
+
+ const [iterations, dataHex] = password.split(':');
+ const data = Buffer.from(dataHex, 'hex');
+ // authTag in node.js equals 16 bytes(128 bits), see https://stackoverflow.com/questions/33976117/does-node-js-crypto-use-fixed-tag-size-with-gcm-mode
+ const delta = [saltByteLength, initializationVectorByteLength, 16];
+ const pointerArray = [];
+
+ for (let byte = 0, i = 0; i < delta.length; i++) {
+ const deltaValue = delta[i];
+ pointerArray.push(data.subarray(byte, byte + deltaValue));
+ byte += deltaValue;
+
+ if (i === delta.length - 1) {
+ pointerArray.push(data.subarray(byte));
+ }
+ }
+
+ const [
+ salt,
+ initializationVector,
+ authTag,
+ encryptedData
+ ] = pointerArray;
+
+ const decryptionKey = await pbkdf2Promise(tenSecret, salt, parseInt(iterations, 16), keyByteLength, 'sha512');
+ const decipher = crypto.createDecipheriv('aes-256-gcm', decryptionKey, initializationVector, {authTagLength:16});
+ decipher.setAuthTag(authTag);
+
+ return Buffer.concat([decipher.update(encryptedData, 'binary'), decipher.final()]).toString();
+};
+exports.getDateTimeTicks = function(date) {
+ return BigInt(date.getTime() * 10000) + 621355968000000000n;
+};
+exports.convertLicenseInfoToFileParams = function(licenseInfo) {
+ // todo
+ // {
+ // user_quota = 0;
+ // portal_count = 0;
+ // process = 2;
+ // ssbranding = false;
+ // whiteLabel = false;
+ // }
+ let license = {};
+ license.start_date = licenseInfo.startDate && licenseInfo.startDate.toJSON();
+ license.end_date = licenseInfo.endDate && licenseInfo.endDate.toJSON();
+ license.timelimited = 0 !== (constants.LICENSE_MODE.Limited & licenseInfo.mode);
+ license.trial = 0 !== (constants.LICENSE_MODE.Trial & licenseInfo.mode);
+ license.developer = 0 !== (constants.LICENSE_MODE.Developer & licenseInfo.mode);
+ license.branding = licenseInfo.branding;
+ license.customization = licenseInfo.customization;
+ license.advanced_api = licenseInfo.advancedApi;
+ license.connections = licenseInfo.connections;
+ license.connections_view = licenseInfo.connectionsView;
+ license.users_count = licenseInfo.usersCount;
+ license.users_view_count = licenseInfo.usersViewCount;
+ license.users_expire = licenseInfo.usersExpire / constants.LICENSE_EXPIRE_USERS_ONE_DAY;
+ license.customer_id = licenseInfo.customerId;
+ license.alias = licenseInfo.alias;
+ license.multitenancy = licenseInfo.multitenancy;
+ return license;
+};
+exports.convertLicenseInfoToServerParams = function(licenseInfo) {
+ let license = {};
+ license.workersCount = licenseInfo.count;
+ license.resultType = licenseInfo.type;
+ license.packageType = licenseInfo.packageType;
+ license.buildDate = licenseInfo.buildDate && licenseInfo.buildDate.toJSON();
+ license.buildVersion = commonDefines.buildVersion;
+ license.buildNumber = commonDefines.buildNumber;
+ return license;
+};
+exports.checkBaseUrl = function(ctx, baseUrl, opt_storageCfg) {
+ let storageExternalHost = opt_storageCfg ? opt_storageCfg.externalHost : cfgStorageExternalHost
+ const tenStorageExternalHost = ctx.getCfg('storage.externalHost', storageExternalHost);
+ return tenStorageExternalHost ? tenStorageExternalHost : baseUrl;
+};
+exports.resolvePath = function(object, path, defaultValue) {
+ return path.split('.').reduce((o, p) => o ? o[p] : defaultValue, object);
+};
+Date.isLeapYear = function (year) {
+ return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
+};
+
+Date.getDaysInMonth = function (year, month) {
+ return [31, (Date.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
+};
+
+Date.prototype.isLeapYear = function () {
+ return Date.isLeapYear(this.getUTCFullYear());
+};
+
+Date.prototype.getDaysInMonth = function () {
+ return Date.getDaysInMonth(this.getUTCFullYear(), this.getUTCMonth());
+};
+
+Date.prototype.addMonths = function (value) {
+ var n = this.getUTCDate();
+ this.setUTCDate(1);
+ this.setUTCMonth(this.getUTCMonth() + value);
+ this.setUTCDate(Math.min(n, this.getDaysInMonth()));
+ return this;
+};
+function getMonthDiff(d1, d2) {
+ var months;
+ months = (d2.getUTCFullYear() - d1.getUTCFullYear()) * 12;
+ months -= d1.getUTCMonth();
+ months += d2.getUTCMonth();
+ return months;
+}
+exports.getMonthDiff = getMonthDiff;
+
+/**
+ * A Transform stream that limits the size of data passing through it.
+ * It will throw an EMSGSIZE error if the size exceeds the limit.
+ *
+ * @class SizeLimitStream
+ * @extends {Transform}
+ */
+class SizeLimitStream extends Transform {
+ /**
+ * Creates an instance of SizeLimitStream.
+ * @param {number} sizeLimit - Maximum size in bytes that can pass through the stream
+ * @memberof SizeLimitStream
+ */
+ constructor(sizeLimit) {
+ super();
+ this.sizeLimit = sizeLimit;
+ this.bytesReceived = 0;
+ }
+
+ /**
+ * Transform implementation that tracks the bytes received and enforces the size limit
+ *
+ * @param {Buffer|string} chunk - The chunk of data to process
+ * @param {string} encoding - The encoding of the chunk if it's a string
+ * @param {Function} callback - Called when processing is complete
+ * @memberof SizeLimitStream
+ */
+ _transform(chunk, encoding, callback) {
+ this.bytesReceived += chunk.length;
+
+ if (this.sizeLimit && this.bytesReceived > this.sizeLimit) {
+ const error = new Error(`EMSGSIZE: Response too large: ${this.bytesReceived} bytes (limit: ${this.sizeLimit} bytes)`);
+ error.code = 'EMSGSIZE';
+ callback(error);
+ return;
+ }
+
+ callback(null, chunk);
+ }
+}
+exports.getLicensePeriod = function(startDate, now) {
+ startDate = new Date(startDate.getTime());//clone
+ startDate.addMonths(getMonthDiff(startDate, now));
+ if (startDate > now) {
+ startDate.addMonths(-1);
+ }
+ startDate.setUTCHours(0,0,0,0);
+ return startDate.getTime();
+};
+
+exports.removeIllegalCharacters = function(filename) {
+ return filename?.replace(/[/\\?%*:|"<>]/g, '-') || filename;
+}
+exports.getFunctionArguments = function(func) {
+ return func.toString().
+ replace(/[\r\n\s]+/g, ' ').
+ match(/(?:function\s*\w*)?\s*(?:\((.*?)\)|([^\s]+))/).
+ slice(1, 3).
+ join('').
+ split(/\s*,\s*/);
+};
+exports.isUselesSfc = function(row, cmd) {
+ return !(row && commonDefines.FileStatus.SaveVersion === row.status && cmd.getStatusInfoIn() === row.status_info);
+};
+exports.getChangesFileHeader = function() {
+ return `CHANGES\t${commonDefines.buildVersion}\n`;
+};
+exports.checksumFile = function(hashName, path) {
+ //https://stackoverflow.com/a/44643479
+ return new Promise((resolve, reject) => {
+ const hash = crypto.createHash(hashName);
+ const stream = fs.createReadStream(path);
+ stream.on('error', err => reject(err));
+ stream.on('data', chunk => hash.update(chunk));
+ stream.on('end', () => resolve(hash.digest('hex')));
+ });
+};
+
+function isObject(item) {
+ return (item && typeof item === 'object' && !Array.isArray(item));
+}
+
+function deepMergeObjects(target, ...sources) {
+ if (!sources.length) {
+ return target;
+ }
+
+ const source = sources.shift();
+ if (isObject(target) && isObject(source)) {
+ for (const key in source) {
+ if (isObject(source[key])) {
+ if (!target[key]) {
+ Object.assign(target, { [key]: {} });
+ }
+
+ deepMergeObjects(target[key], source[key]);
+ } else {
+ Object.assign(target, { [key]: source[key] });
+ }
+ }
+ }
+
+ return deepMergeObjects(target, ...sources);
+}
+exports.isObject = isObject;
+exports.deepMergeObjects = deepMergeObjects;