/* * (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'; const config = require('config'); const co = require('co'); const NodeCache = require('node-cache'); const constants = require('./../../Common/sources/constants'); const commonDefines = require('./../../Common/sources/commondefines'); const utils = require('./../../Common/sources/utils'); const {readFile, readdir, writeFile} = require('fs/promises'); const path = require('path'); const cfgTenantsBaseDomain = config.get('tenants.baseDomain'); const cfgTenantsBaseDir = config.get('tenants.baseDir'); const cfgTenantsFilenameSecret = config.get('tenants.filenameSecret'); const cfgTenantsFilenameLicense = config.get('tenants.filenameLicense'); const cfgTenantsFilenameConfig = config.get('tenants.filenameConfig'); const cfgTenantsDefaultTenant = config.get('tenants.defaultTenant'); const cfgTenantsCache = config.util.cloneDeep(config.get('tenants.cache')); const cfgSecretInbox = config.get('services.CoAuthoring.secret.inbox'); const cfgSecretOutbox = config.get('services.CoAuthoring.secret.outbox'); const cfgSecretSession = config.get('services.CoAuthoring.secret.session'); let licenseInfo; let licenseOriginal; let licenseTuple; //to avoid array creating in getTenantLicense const c_LM = constants.LICENSE_MODE; const nodeCache = new NodeCache(cfgTenantsCache); function getDefautTenant() { return cfgTenantsDefaultTenant; } function getTenant(ctx, domain) { let tenant = getDefautTenant(); if (domain) { //remove port domain = domain.replace(/:.*$/, ''); if (cfgTenantsBaseDomain && domain.endsWith('.' + cfgTenantsBaseDomain)) { tenant = domain.substring(0, domain.length - cfgTenantsBaseDomain.length - 1); } else if (cfgTenantsBaseDomain === domain) { tenant = getDefautTenant(); } else { tenant = domain; } } return tenant; } async function getAllTenants(ctx) { let dirList = []; try { if (isMultitenantMode(ctx)) { const entitiesList = await readdir(cfgTenantsBaseDir, {withFileTypes: true}); dirList = entitiesList.filter(direntObj => direntObj.isDirectory()).map(directory => directory.name); } } catch (error) { ctx.logger.error('getAllTenants error: ', error.stack); } return dirList; } function getTenantByConnection(ctx, conn) { return isMultitenantMode(ctx) ? getTenant(ctx, utils.getDomainByConnection(ctx, conn)) : getDefautTenant(); } function getTenantByRequest(ctx, req) { return isMultitenantMode(ctx) ? getTenant(ctx, utils.getDomainByRequest(ctx, req)) : getDefautTenant(); } function getTenantPathPrefix(ctx) { return isMultitenantMode(ctx) ? utils.removeIllegalCharacters(ctx.tenant) + '/' : ''; } async function getTenantConfig(ctx) { let res = null; if (isMultitenantMode(ctx) && !isDefaultTenant(ctx)) { const tenantPath = utils.removeIllegalCharacters(ctx.tenant); const configPath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameConfig); res = nodeCache.get(configPath); if (res) { ctx.logger.debug('getTenantConfig from cache'); } else { try { const cfgString = await readFile(configPath, {encoding: 'utf8'}); res = config.util.parseString(cfgString, path.extname(configPath).substring(1)); ctx.logger.debug('getTenantConfig from %s', configPath); } catch (e) { ctx.logger.debug('getTenantConfig error: %s', e.stack); } finally { ctx.cleanTenantConfigCache(ctx.tenant); nodeCache.set(configPath, res); } } } return res; } /** * Set tenant configuration for the current context * @param {operationContext} ctx - Operation context * @param {Object} config - Configuration data to save * @returns {Object} Saved configuration object */ async function setTenantConfig(ctx, config) { let newConfig = await getTenantConfig(ctx); if (isMultitenantMode(ctx) && !isDefaultTenant(ctx)) { newConfig = utils.deepMergeObjects(newConfig || {}, config); const tenantPath = utils.removeIllegalCharacters(ctx.tenant); const configPath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameConfig); await writeFile(configPath, JSON.stringify(newConfig, null, 2), 'utf8'); ctx.cleanTenantConfigCache(ctx.tenant); nodeCache.set(configPath, newConfig); } return newConfig; } /** * Replace tenant configuration completely (no merging) * @param {operationContext} ctx - Operation context * @param {Object} config - Configuration data to replace with * @returns {Object} Replaced configuration object */ async function replaceTenantConfig(ctx, config) { if (isMultitenantMode(ctx) && !isDefaultTenant(ctx)) { const tenantPath = utils.removeIllegalCharacters(ctx.tenant); const configPath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameConfig); await writeFile(configPath, JSON.stringify(config, null, 2), 'utf8'); ctx.cleanTenantConfigCache(ctx.tenant); nodeCache.set(configPath, config); return config; } return config; } function getTenantSecret(ctx, type) { return co(function* () { let cfgTenant; //check config const tenantConfig = yield getTenantConfig(ctx); if (tenantConfig) { switch (type) { case commonDefines.c_oAscSecretType.Browser: case commonDefines.c_oAscSecretType.Inbox: cfgTenant = tenantConfig?.services?.CoAuthoring?.secret?.inbox; break; case commonDefines.c_oAscSecretType.Outbox: cfgTenant = tenantConfig?.services?.CoAuthoring?.secret?.outbox; break; case commonDefines.c_oAscSecretType.Session: cfgTenant = tenantConfig?.services?.CoAuthoring?.secret?.session; break; } } if (cfgTenant) { return utils.getSecretByElem(cfgTenant); } let res = undefined; //read secret file if (isMultitenantMode(ctx) && !isDefaultTenant(ctx)) { const tenantPath = utils.removeIllegalCharacters(ctx.tenant); const secretPath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameSecret); res = nodeCache.get(secretPath); if (res) { ctx.logger.debug('getTenantSecret from cache'); } else { try { const secret = yield readFile(secretPath, {encoding: 'utf8'}); //trim whitespace plus line terminators from string (newline is common on Posix systems) res = secret.trim(); if (res.length !== secret.length) { ctx.logger.warn('getTenantSecret secret in %s contains a leading or trailing whitespace that has been trimmed', secretPath); } ctx.logger.debug('getTenantSecret from %s', secretPath); } catch (e) { res = undefined; ctx.logger.warn('getTenantConfig error: %s', e.stack); } finally { nodeCache.set(secretPath, res); } } } else { switch (type) { case commonDefines.c_oAscSecretType.Browser: case commonDefines.c_oAscSecretType.Inbox: res = utils.getSecretByElem(cfgSecretInbox); break; case commonDefines.c_oAscSecretType.Outbox: res = utils.getSecretByElem(cfgSecretOutbox); break; case commonDefines.c_oAscSecretType.Session: res = utils.getSecretByElem(cfgSecretSession); break; } } return res; }); } function setDefLicense(data, original) { licenseInfo = data; licenseOriginal = original; licenseTuple = [licenseInfo, licenseOriginal]; } //todo move to license file? function fixTenantLicense(ctx, licenseInfo, licenseInfoTenant) { const errors = []; //bitwise if (0 !== (licenseInfo.mode & c_LM.Limited) && 0 === (licenseInfoTenant.mode & c_LM.Limited)) { licenseInfoTenant.mode |= c_LM.Limited; errors.push('timelimited'); } if (0 !== (licenseInfo.mode & c_LM.Trial) && 0 === (licenseInfoTenant.mode & c_LM.Trial)) { licenseInfoTenant.mode |= c_LM.Trial; errors.push('trial'); } if (0 !== (licenseInfo.mode & c_LM.Developer) && 0 === (licenseInfoTenant.mode & c_LM.Developer)) { licenseInfoTenant.mode |= c_LM.Developer; errors.push('developer'); } //can not turn on const flags = ['branding', 'customization']; flags.forEach(flag => { if (!licenseInfo[flag] && licenseInfoTenant[flag]) { licenseInfoTenant[flag] = licenseInfo[flag]; errors.push(flag); } }); if (!licenseInfo.advancedApi && licenseInfoTenant.advancedApi) { licenseInfoTenant.advancedApi = licenseInfo.advancedApi; errors.push('advanced_api'); } //can not up limits // if (licenseInfo.connections < licenseInfoTenant.connections) { // licenseInfoTenant.connections = licenseInfo.connections; // errors.push('connections'); // } // if (licenseInfo.connectionsView < licenseInfoTenant.connectionsView) { // licenseInfoTenant.connectionsView = licenseInfo.connectionsView; // errors.push('connections_view'); // } // if (licenseInfo.usersCount < licenseInfoTenant.usersCount) { // licenseInfoTenant.usersCount = licenseInfo.usersCount; // errors.push('users_count'); // } // if (licenseInfo.usersViewCount < licenseInfoTenant.usersViewCount) { // licenseInfoTenant.usersViewCount = licenseInfo.usersViewCount; // errors.push('users_view_count'); // } if (licenseInfo.endDate && licenseInfoTenant.endDate && licenseInfo.endDate < licenseInfoTenant.endDate) { licenseInfoTenant.endDate = licenseInfo.endDate; errors.push('end_date'); } if (errors.length > 0) { ctx.logger.warn('fixTenantLicense not allowed to improve these license fields: %s', errors.join(', ')); } } async function getTenantLicense(ctx) { let res = licenseTuple; if (isMultitenantMode(ctx) && !isDefaultTenant(ctx)) { //todo alias is deprecated. remove one year after 8.3 if (licenseInfo.multitenancy || licenseInfo.alias) { const tenantPath = utils.removeIllegalCharacters(ctx.tenant); const licensePath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameLicense); let licenseTupleTenant = nodeCache.get(licensePath); if (licenseTupleTenant) { ctx.logger.debug('getTenantLicense from cache'); } else { licenseTupleTenant = await readLicenseTenant(ctx, licensePath, licenseInfo); fixTenantLicense(ctx, licenseInfo, licenseTupleTenant[0]); nodeCache.set(licensePath, licenseTupleTenant); ctx.logger.debug('getTenantLicense from %s', licensePath); } res = licenseTupleTenant; } else { res = [...res]; res[0] = {...res[0]}; res.type = constants.LICENSE_RESULT.Error; ctx.logger.error('getTenantLicense error: missing "multitenancy" or "alias" field'); } } return res; } function getServerLicense(_ctx) { return licenseInfo; } let hasBaseDir = !!cfgTenantsBaseDir; function isMultitenantMode(_ctx) { return hasBaseDir; } function setMultitenantMode(val) { //for tests only!! return (hasBaseDir = val); } function isDefaultTenant(ctx) { return ctx.tenant === cfgTenantsDefaultTenant; } //todo move to license file? async function readLicenseTenant(ctx, licenseFile, baseVerifiedLicense) { const c_LR = constants.LICENSE_RESULT; const c_LM = constants.LICENSE_MODE; const res = {...baseVerifiedLicense}; let oLicense = null; try { const oFile = (await readFile(licenseFile)).toString(); res.hasLicense = true; oLicense = JSON.parse(oFile); //do not verify tenant signature. verify main lic signature. //delete from object to keep signature secret delete oLicense['signature']; if (oLicense['start_date']) { res.startDate = new Date(oLicense['start_date']); } const startDate = res.startDate; if (oLicense['end_date']) { res.endDate = new Date(oLicense['end_date']); } else { //spread copy do not copy date res.endDate = new Date(res.endDate); } if (oLicense['customer_id']) { res.customerId = oLicense['customer_id']; } if (oLicense['alias']) { res.alias = oLicense['alias']; } if (oLicense['multitenancy']) { res.multitenancy = oLicense['multitenancy']; } if (true === oLicense['timelimited']) { res.mode |= c_LM.Limited; } if (Object.hasOwn(oLicense, 'trial')) { res.mode |= true === oLicense['trial'] || 'true' === oLicense['trial'] || 'True' === oLicense['trial'] ? c_LM.Trial : c_LM.None; // Someone who likes to put json string instead of bool } if (true === oLicense['developer']) { res.mode |= c_LM.Developer; } if (Object.hasOwn(oLicense, 'branding')) { res.branding = true === oLicense['branding'] || 'true' === oLicense['branding'] || 'True' === oLicense['branding']; // Someone who likes to put json string instead of bool } if (Object.hasOwn(oLicense, 'customization')) { res.customization = !!oLicense['customization']; } if (Object.hasOwn(oLicense, 'advanced_api')) { res.advancedApi = !!oLicense['advanced_api']; } if (Object.hasOwn(oLicense, 'connections')) { res.connections = oLicense['connections'] >> 0; } if (Object.hasOwn(oLicense, 'connections_view')) { res.connectionsView = oLicense['connections_view'] >> 0; } if (Object.hasOwn(oLicense, 'users_count')) { res.usersCount = oLicense['users_count'] >> 0; } if (Object.hasOwn(oLicense, 'users_view_count')) { res.usersViewCount = oLicense['users_view_count'] >> 0; } if (Object.hasOwn(oLicense, 'users_expire')) { res.usersExpire = Math.max(constants.LICENSE_EXPIRE_USERS_ONE_DAY, (oLicense['users_expire'] >> 0) * constants.LICENSE_EXPIRE_USERS_ONE_DAY); } // Read grace_days setting from license file if available if (Object.hasOwn(oLicense, 'grace_days')) { res.graceDays = Math.max(0, oLicense['grace_days'] >> 0); } const timeLimited = 0 !== (res.mode & c_LM.Limited); const checkDate = res.mode & c_LM.Trial || timeLimited ? new Date() : licenseInfo.buildDate; //Calendar check of start_date allows to issue a license for old versions const checkStartDate = new Date(); if (startDate <= checkStartDate && checkDate <= res.endDate) { res.type = c_LR.Success; } else if (startDate > checkStartDate) { res.type = c_LR.NotBefore; ctx.logger.warn('License: License not active before start_date:%s.', startDate.toISOString()); } else if (timeLimited) { // Grace period after end license = limited mode with limited connections if (res.endDate.setUTCDate(res.endDate.getUTCDate() + res.graceDays) >= checkDate) { res.type = c_LR.SuccessLimit; res.connections = Math.min(res.connections, constants.LICENSE_CONNECTIONS); res.connectionsView = Math.min(res.connectionsView, constants.LICENSE_CONNECTIONS); res.usersCount = Math.min(res.usersCount, constants.LICENSE_USERS); res.usersViewCount = Math.min(res.usersViewCount, constants.LICENSE_USERS); const errStr = res.usersCount ? `${res.usersCount} unique users` : `${res.connections} concurrent connections`; ctx.logger.error( `License: License needs to be renewed.\nYour users have only ${errStr} ` + `available for document editing for the next ${res.graceDays} days.\nPlease renew the ` + 'license to restore the full access' ); } else { res.type = c_LR.ExpiredLimited; } } else if (0 !== (res.mode & c_LM.Trial)) { res.type = c_LR.ExpiredTrial; } else { res.type = c_LR.Expired; } } catch (e) { ctx.logger.warn(e); res.count = 1; res.connections = 0; res.connectionsView = 0; res.usersCount = 0; res.usersViewCount = 0; res.type = c_LR.Error; } if (res.type === c_LR.Expired || res.type === c_LR.ExpiredLimited || res.type === c_LR.ExpiredTrial) { res.count = 1; let errorMessage; if (res.type === c_LR.Expired) { errorMessage = 'Your access to updates and support has expired.\n' + 'Your license key can not be applied to new versions.\n' + 'Please extend the license to get updates and support.'; } else if (res.type === c_LR.ExpiredLimited) { errorMessage = 'License expired.\nYour users can not edit or view document anymore.\n' + 'Please renew the license.'; } else { errorMessage = 'License Expired!!!'; } ctx.logger.warn('License: ' + errorMessage); } return [res, oLicense]; } exports.getAllTenants = getAllTenants; exports.getDefautTenant = getDefautTenant; exports.getTenantByConnection = getTenantByConnection; exports.getTenantByRequest = getTenantByRequest; exports.getTenantPathPrefix = getTenantPathPrefix; exports.getTenantConfig = getTenantConfig; exports.getTenantSecret = getTenantSecret; exports.getTenantLicense = getTenantLicense; exports.getServerLicense = getServerLicense; exports.setDefLicense = setDefLicense; exports.setTenantConfig = setTenantConfig; exports.replaceTenantConfig = replaceTenantConfig; exports.isMultitenantMode = isMultitenantMode; exports.setMultitenantMode = setMultitenantMode; exports.isDefaultTenant = isDefaultTenant;