diff --git a/Common/config/default.json b/Common/config/default.json index 0b588e01..8a01d2b9 100644 --- a/Common/config/default.json +++ b/Common/config/default.json @@ -27,6 +27,7 @@ "endpoint": "http://localhost/s3", "bucketName": "cache", "storageFolderName": "files", + "cacheFolderName": "data", "urlExpires": 604800, "accessKeyId": "AKID", "secretAccessKey": "SECRET", @@ -103,7 +104,12 @@ "baseDomain": "", "filenameSecret": "secret.key", "filenameLicense": "license.lic", - "defaultTetant": "tetant" + "defaultTenant": "localhost", + "cache" : { + "stdTTL": 300, + "checkperiod": 60, + "useClones": false + } }, "services": { "CoAuthoring": { @@ -258,6 +264,7 @@ }, "sockjs": { "sockjs_url": "", + "disable_cors": true, "websocket": true }, "callbackBackoffOptions": { diff --git a/Common/sources/commondefines.js b/Common/sources/commondefines.js index 693b1dd7..99b09290 100644 --- a/Common/sources/commondefines.js +++ b/Common/sources/commondefines.js @@ -904,7 +904,7 @@ function OutputMailMerge(mailMergeSendData) { this['title'] = mailMergeSendData.getFileName(); const mailFormat = mailMergeSendData.getMailFormat(); switch (mailFormat) { - case constants.AVS_OFFICESTUDIO_FILE_OTHER_HTMLZIP : + case constants.AVS_OFFICESTUDIO_FILE_DOCUMENT_HTML : this['type'] = 0; break; case constants.AVS_OFFICESTUDIO_FILE_DOCUMENT_DOCX : diff --git a/Common/sources/storage-base.js b/Common/sources/storage-base.js index 14f2efb3..8d40b308 100644 --- a/Common/sources/storage-base.js +++ b/Common/sources/storage-base.js @@ -37,42 +37,45 @@ var utils = require('./utils'); var storage = require('./' + config.get('storage.name')); var tenantManager = require('./tenantManager'); -function getStoragePath(ctx, strPath) { - return tenantManager.getTenantPathPrefix(ctx) + strPath.replace(/\\/g, '/') +const cfgCacheFolderName = config.get('storage.cacheFolderName'); + +function getStoragePath(ctx, strPath, opt_specialDir) { + opt_specialDir = opt_specialDir || cfgCacheFolderName; + return opt_specialDir + '/' + tenantManager.getTenantPathPrefix(ctx) + strPath.replace(/\\/g, '/') } -exports.headObject = function(ctx, strPath) { - return storage.headObject(getStoragePath(ctx, strPath)); +exports.headObject = function(ctx, strPath, opt_specialDir) { + return storage.headObject(getStoragePath(ctx, strPath, opt_specialDir)); }; -exports.getObject = function(ctx, strPath) { - return storage.getObject(getStoragePath(ctx, strPath)); +exports.getObject = function(ctx, strPath, opt_specialDir) { + return storage.getObject(getStoragePath(ctx, strPath, opt_specialDir)); }; -exports.createReadStream = function(ctx, strPath) { - return storage.createReadStream(getStoragePath(ctx, strPath)); +exports.createReadStream = function(ctx, strPath, opt_specialDir) { + return storage.createReadStream(getStoragePath(ctx, strPath, opt_specialDir)); }; -exports.putObject = function(ctx, strPath, buffer, contentLength) { - return storage.putObject(getStoragePath(ctx, strPath), buffer, contentLength); +exports.putObject = function(ctx, strPath, buffer, contentLength, opt_specialDir) { + return storage.putObject(getStoragePath(ctx, strPath, opt_specialDir), buffer, contentLength); }; -exports.uploadObject = function(ctx, strPath, filePath) { - return storage.uploadObject(getStoragePath(ctx, strPath), filePath); +exports.uploadObject = function(ctx, strPath, filePath, opt_specialDir) { + return storage.uploadObject(getStoragePath(ctx, strPath, opt_specialDir), filePath); }; -exports.copyObject = function(ctx, sourceKey, destinationKey) { - let storageSrc = getStoragePath(ctx, sourceKey); - let storageDst = getStoragePath(ctx, destinationKey); +exports.copyObject = function(ctx, sourceKey, destinationKey, opt_specialDirSrc, opt_specialDirDst) { + let storageSrc = getStoragePath(ctx, sourceKey, opt_specialDirSrc); + let storageDst = getStoragePath(ctx, destinationKey, opt_specialDirDst); return storage.copyObject(storageSrc, storageDst); }; -exports.copyPath = function(ctx, sourcePath, destinationPath) { - let storageSrc = getStoragePath(ctx, sourcePath); - let storageDst = getStoragePath(ctx, destinationPath); +exports.copyPath = function(ctx, sourcePath, destinationPath, opt_specialDirSrc, opt_specialDirDst) { + let storageSrc = getStoragePath(ctx, sourcePath, opt_specialDirSrc); + let storageDst = getStoragePath(ctx, destinationPath, opt_specialDirDst); return storage.listObjects(storageSrc).then(function(list) { return Promise.all(list.map(function(curValue) { return storage.copyObject(curValue, storageDst + '/' + exports.getRelativePath(storageSrc, curValue)); })); }); }; -exports.listObjects = function(ctx, strPath) { - let prefix = getStoragePath(ctx, ""); - return storage.listObjects(getStoragePath(ctx, strPath)).then(function(list) { +exports.listObjects = function(ctx, strPath, opt_specialDir) { + let prefix = getStoragePath(ctx, "", opt_specialDir); + return storage.listObjects(getStoragePath(ctx, strPath, opt_specialDir)).then(function(list) { return list.map((currentValue) => { return currentValue.substring(prefix.length); }); @@ -81,26 +84,26 @@ exports.listObjects = function(ctx, strPath) { return []; }); }; -exports.deleteObject = function(ctx, strPath) { - return storage.deleteObject(getStoragePath(ctx, strPath)); +exports.deleteObject = function(ctx, strPath, opt_specialDir) { + return storage.deleteObject(getStoragePath(ctx, strPath, opt_specialDir)); }; -exports.deleteObjects = function(ctx, strPaths) { +exports.deleteObjects = function(ctx, strPaths, opt_specialDir) { var StoragePaths = strPaths.map(function(curValue) { - return getStoragePath(ctx, curValue); + return getStoragePath(ctx, curValue, opt_specialDir); }); return storage.deleteObjects(StoragePaths); }; -exports.deletePath = function(ctx, strPath) { - let storageSrc = getStoragePath(ctx, strPath); +exports.deletePath = function(ctx, strPath, opt_specialDir) { + let storageSrc = getStoragePath(ctx, strPath, opt_specialDir); return storage.listObjects(storageSrc).then(function(list) { return storage.deleteObjects(list); }); }; -exports.getSignedUrl = function(ctx, baseUrl, strPath, urlType, optFilename, opt_creationDate) { - return storage.getSignedUrl(baseUrl, getStoragePath(ctx, strPath), urlType, optFilename, opt_creationDate); +exports.getSignedUrl = function(ctx, baseUrl, strPath, urlType, optFilename, opt_creationDate, opt_specialDir) { + return storage.getSignedUrl(baseUrl, getStoragePath(ctx, strPath, opt_specialDir), urlType, optFilename, opt_creationDate); }; -exports.getSignedUrls = function(ctx, baseUrl, strPath, urlType, opt_creationDate) { - let storageSrc = getStoragePath(ctx, strPath); +exports.getSignedUrls = function(ctx, baseUrl, strPath, urlType, opt_creationDate, opt_specialDir) { + let storageSrc = getStoragePath(ctx, strPath, opt_specialDir); return storage.listObjects(storageSrc).then(function(list) { return Promise.all(list.map(function(curValue) { return storage.getSignedUrl(baseUrl, curValue, urlType, undefined, opt_creationDate); @@ -113,18 +116,18 @@ exports.getSignedUrls = function(ctx, baseUrl, strPath, urlType, opt_creationDat }); }); }; -exports.getSignedUrlsArrayByArray = function(ctx, baseUrl, list, urlType) { +exports.getSignedUrlsArrayByArray = function(ctx, baseUrl, list, urlType, opt_specialDir) { return Promise.all(list.map(function(curValue) { - let storageSrc = getStoragePath(ctx, curValue); + let storageSrc = getStoragePath(ctx, curValue, opt_specialDir); return storage.getSignedUrl(baseUrl, storageSrc, urlType, undefined); })); }; -exports.getSignedUrlsByArray = function(ctx, baseUrl, list, optPath, urlType) { - return exports.getSignedUrlsArrayByArray(ctx, baseUrl, list, urlType).then(function(urls) { +exports.getSignedUrlsByArray = function(ctx, baseUrl, list, optPath, urlType, opt_specialDir) { + return exports.getSignedUrlsArrayByArray(ctx, baseUrl, list, urlType, opt_specialDir).then(function(urls) { var outputMap = {}; for (var i = 0; i < list.length && i < urls.length; ++i) { if (optPath) { - let storageSrc = getStoragePath(ctx, optPath); + let storageSrc = getStoragePath(ctx, optPath, opt_specialDir); outputMap[exports.getRelativePath(storageSrc, list[i])] = urls[i]; } else { outputMap[list[i]] = urls[i]; diff --git a/Common/sources/tenantManager.js b/Common/sources/tenantManager.js index aec9166b..51f31e0e 100644 --- a/Common/sources/tenantManager.js +++ b/Common/sources/tenantManager.js @@ -34,6 +34,7 @@ const config = require('config'); const co = require('co'); +const NodeCache = require( "node-cache" ); const license = require('./../../Common/sources/license'); const constants = require('./../../Common/sources/constants'); const commonDefines = require('./../../Common/sources/commondefines'); @@ -46,7 +47,8 @@ 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 cfgTenantsDefaultTetant = config.get('tenants.defaultTetant'); +const cfgTenantsDefaultTenant = config.get('tenants.defaultTenant'); +const cfgTenantsCache = 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'); @@ -54,19 +56,22 @@ const cfgSecretSession = config.get('services.CoAuthoring.secret.session'); let licenseInfo; let licenseOriginal; +const nodeCache = new NodeCache(cfgTenantsCache); + function getDefautTenant() { - return cfgTenantsDefaultTetant; + return cfgTenantsDefaultTenant; } function getTenant(ctx, domain) { let tenant = getDefautTenant(); if (domain) { //remove port - domain = domain.substring(0, domain.indexOf(':')); - let index = domain.indexOf('.' + cfgTenantsBaseDomain); - if (-1 !== index) { - tenant = domain.substring(0, index); - } else { - ctx.logger.warn('getTenant invalid domain=%s', domain); + domain = domain.replace(/\:.*$/, ''); + tenant = domain; + if (cfgTenantsBaseDomain) { + let index = domain.indexOf('.' + cfgTenantsBaseDomain); + if (-1 !== index) { + tenant = domain.substring(0, index); + } } } return tenant; @@ -86,8 +91,14 @@ function getTenantSecret(ctx, type) { if (isMultitenantMode()) { let tenantPath = utils.removeIllegalCharacters(ctx.tenant); let secretPath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameSecret); - ctx.logger.debug('getTenantSecret path=%s', secretPath); - res = yield readFile(secretPath, {encoding: 'utf8'}); + res = nodeCache.get(secretPath); + if (res) { + ctx.logger.debug('getTenantSecret from cache'); + } else { + res = yield readFile(secretPath, {encoding: 'utf8'}); + nodeCache.set(secretPath, res); + ctx.logger.debug('getTenantSecret from %s', secretPath); + } } else { switch (type) { case commonDefines.c_oAscSecretType.Browser: @@ -116,8 +127,14 @@ function getTenantLicense(ctx) { if (isMultitenantMode()) { let tenantPath = utils.removeIllegalCharacters(ctx.tenant); let licensePath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameLicense); - ctx.logger.debug('getTenantLicense path=%s', licensePath); - [res] = yield* license.readLicense(licensePath); + res = nodeCache.get(licensePath); + if (res) { + ctx.logger.debug('getTenantLicense from cache'); + } else { + [res] = yield* license.readLicense(licensePath); + nodeCache.set(licensePath, res); + ctx.logger.debug('getTenantLicense from %s', licensePath); + } } else { res = licenseInfo; } @@ -125,7 +142,7 @@ function getTenantLicense(ctx) { }); } function isMultitenantMode() { - return !!cfgTenantsBaseDir && !!cfgTenantsBaseDomain; + return !!cfgTenantsBaseDir; } exports.getDefautTenant = getDefautTenant; diff --git a/Common/sources/utils.js b/Common/sources/utils.js index 9bdaff76..940d36eb 100644 --- a/Common/sources/utils.js +++ b/Common/sources/utils.js @@ -945,6 +945,7 @@ exports.convertLicenseInfoToFileParams = function(licenseInfo) { license.light = licenseInfo.light; license.branding = licenseInfo.branding; license.customization = licenseInfo.customization; + license.advanced_api = licenseInfo.advancedApi; license.plugins = licenseInfo.plugins; license.connections = licenseInfo.connections; license.connections_view = licenseInfo.connectionsView; diff --git a/DocService/sources/DocsCoServer.js b/DocService/sources/DocsCoServer.js index 6ce04971..f01d51fa 100644 --- a/DocService/sources/DocsCoServer.js +++ b/DocService/sources/DocsCoServer.js @@ -137,7 +137,6 @@ const cfgTokenSessionExpires = ms(config.get('token.session.expires')); const cfgTokenInboxHeader = config.get('token.inbox.header'); const cfgTokenInboxPrefix = config.get('token.inbox.prefix'); const cfgTokenVerifyOptions = config.get('token.verifyOptions'); -const cfgSecretSession = config.get('secret.session'); const cfgForceSaveEnable = config.get('autoAssembly.enable'); const cfgForceSaveInterval = ms(config.get('autoAssembly.interval')); const cfgForceSaveStep = ms(config.get('autoAssembly.step')); @@ -465,41 +464,45 @@ let changeConnectionInfo = co.wrap(function*(ctx, conn, cmd) { } return false; }); -function signToken(payload, algorithm, expiresIn, secretElem) { - var options = {algorithm: algorithm, expiresIn: expiresIn}; - var secret = utils.getSecretByElem(secretElem); - return jwt.sign(payload, secret, options); +function signToken(ctx, payload, algorithm, expiresIn, secretElem) { + return co(function*() { + var options = {algorithm: algorithm, expiresIn: expiresIn}; + let secret = yield tenantManager.getTenantSecret(ctx, secretElem); + return jwt.sign(payload, secret, options); + }); } function needSendChanges (conn){ return !conn.user?.view || utils.isLiveViewer(conn); } -function fillJwtByConnection(conn) { - var payload = {document: {}, editorConfig: {user: {}}}; - var doc = payload.document; - doc.key = conn.docId; - doc.permissions = conn.permissions; - doc.ds_encrypted = conn.encrypted; - var edit = payload.editorConfig; - //todo - //edit.callbackUrl = callbackUrl; - //edit.lang = conn.lang; - //edit.mode = conn.mode; - var user = edit.user; - user.id = conn.user.idOriginal; - user.name = conn.user.username; - user.index = conn.user.indexUser; - if (conn.coEditingMode) { - edit.coEditing = {mode: conn.coEditingMode}; - } - //no standart - edit.ds_view = conn.user.view; - edit.ds_isCloseCoAuthoring = conn.isCloseCoAuthoring; - edit.ds_isEnterCorrectPassword = conn.isEnterCorrectPassword; - // presenter viewer opens with same session jwt. do not put sessionId to jwt - // edit.ds_sessionId = conn.sessionId; - edit.ds_sessionTimeConnect = conn.sessionTimeConnect; +function fillJwtByConnection(ctx, conn) { + return co(function*() { + var payload = {document: {}, editorConfig: {user: {}}}; + var doc = payload.document; + doc.key = conn.docId; + doc.permissions = conn.permissions; + doc.ds_encrypted = conn.encrypted; + var edit = payload.editorConfig; + //todo + //edit.callbackUrl = callbackUrl; + //edit.lang = conn.lang; + //edit.mode = conn.mode; + var user = edit.user; + user.id = conn.user.idOriginal; + user.name = conn.user.username; + user.index = conn.user.indexUser; + if (conn.coEditingMode) { + edit.coEditing = {mode: conn.coEditingMode}; + } + //no standart + edit.ds_view = conn.user.view; + edit.ds_isCloseCoAuthoring = conn.isCloseCoAuthoring; + edit.ds_isEnterCorrectPassword = conn.isEnterCorrectPassword; + // presenter viewer opens with same session jwt. do not put sessionId to jwt + // edit.ds_sessionId = conn.sessionId; + edit.ds_sessionTimeConnect = conn.sessionTimeConnect; - return signToken(payload, cfgTokenSessionAlgorithm, cfgTokenSessionExpires / 1000, cfgSecretSession); + return yield signToken(ctx, payload, cfgTokenSessionAlgorithm, cfgTokenSessionExpires / 1000, commonDefines.c_oAscSecretType.Session); + }); } function sendData(ctx, conn, data) { @@ -526,8 +529,8 @@ function sendDataMeta(ctx, conn, msg) { function sendDataSession(ctx, conn, msg) { sendData(ctx, conn, {type: "session", messages: msg}); } -function sendDataRefreshToken(ctx, conn) { - sendData(ctx, conn, {type: "refreshToken", messages: fillJwtByConnection(conn)}); +function sendDataRefreshToken(ctx, conn, msg) { + sendData(ctx, conn, {type: "refreshToken", messages: msg}); } function sendDataRpc(ctx, conn, responseKey, data) { sendData(ctx, conn, {type: "rpc", responseKey: responseKey, data: data}); @@ -543,12 +546,15 @@ function sendReleaseLock(ctx, conn, userLocks) { })}); } function modifyConnectionForPassword(ctx, conn, isEnterCorrectPassword) { - if (isEnterCorrectPassword) { - conn.isEnterCorrectPassword = true; - if (cfgTokenEnableBrowser) { - sendDataRefreshToken(ctx, conn); + return co(function*() { + if (isEnterCorrectPassword) { + conn.isEnterCorrectPassword = true; + if (cfgTokenEnableBrowser) { + let sessionToken = yield fillJwtByConnection(ctx, conn); + sendDataRefreshToken(ctx, conn, sessionToken); + } } - } + }); } function getParticipants(docId, excludeClosed, excludeUserId, excludeViewer) { return _.filter(connections, function(el) { @@ -1098,7 +1104,7 @@ function* cleanDocumentOnExit(ctx, docId, deleteChanges, opt_userIndex) { yield taskResult.restoreInitialPassword(ctx.tenant, docId); sqlBase.deleteChanges(ctx, docId, null); //delete forgotten after successful send on callbackUrl - yield storage.deletePath(ctx, cfgForgottenFiles + '/' + docId); + yield storage.deletePath(ctx, docId, cfgForgottenFiles); } yield unlockWopiDoc(ctx, docId, opt_userIndex); } @@ -1300,12 +1306,12 @@ exports.install = function(server, callbackFunction) { var sockjs_echo = sockjs.createServer(cfgSockjs); sockjs_echo.on('connection', function(conn) { - let ctx = new operationContext.Context(); - ctx.initFromConnection(conn); if (!conn) { operationContext.global.logger.error("null == conn"); return; } + let ctx = new operationContext.Context(); + ctx.initFromConnection(conn); if (getIsShutdown()) { sendFileError(ctx, conn, 'Server shutdow'); return; @@ -1391,8 +1397,8 @@ exports.install = function(server, callbackFunction) { case 'changesError': ctx.logger.error("changesError: %s", data.stack); if (cfgErrorFiles && docId) { - let destDir = cfgErrorFiles + '/browser/' + docId; - yield storage.copyPath(ctx, docId, destDir); + let destDir = 'browser/' + docId; + yield storage.copyPath(ctx, docId, destDir, undefined, cfgErrorFiles); yield* saveErrorChanges(ctx, docId, destDir); } break; @@ -1485,7 +1491,8 @@ exports.install = function(server, callbackFunction) { conn.isCloseCoAuthoring = true; yield addPresence(ctx, conn, true); if (cfgTokenEnableBrowser) { - sendDataRefreshToken(ctx, conn); + let sessionToken = yield fillJwtByConnection(ctx, conn); + sendDataRefreshToken(ctx, conn, sessionToken); } } } @@ -1546,7 +1553,7 @@ exports.install = function(server, callbackFunction) { if (!needSaveChanges) { //start save changes if forgotten file exists. //more effective to send file without sfc, but this method is simpler by code - let forgotten = yield storage.listObjects(ctx, cfgForgottenFiles + '/' + docId); + let forgotten = yield storage.listObjects(ctx, docId, cfgForgottenFiles); needSaveChanges = forgotten.length > 0; ctx.logger.debug('closeDocument hasForgotten %s', needSaveChanges); } @@ -1592,7 +1599,8 @@ exports.install = function(server, callbackFunction) { conn.docId = docIdNew; yield addPresence(ctx, conn, true); if (cfgTokenEnableBrowser) { - sendDataRefreshToken(ctx, conn); + let sessionToken = yield fillJwtByConnection(ctx, conn); + sendDataRefreshToken(ctx, conn, sessionToken); } } //open @@ -2124,6 +2132,7 @@ exports.install = function(server, callbackFunction) { function* auth(ctx, conn, data) { //TODO: Do authorization etc. check md5 or query db if (data.token && data.user) { + ctx.setUserId(data.user.id); let licenseInfo = yield tenantManager.getTenantLicense(ctx); //check jwt if (cfgTokenEnableBrowser) { @@ -2162,6 +2171,7 @@ exports.install = function(server, callbackFunction) { return; } } + ctx.setUserId(data.user.id); let docId = data.docid; const user = data.user; @@ -2433,7 +2443,7 @@ exports.install = function(server, callbackFunction) { let callback = yield* sendStatusDocument(ctx, docId, c_oAscChangeBase.No, userAction, userIndex, documentCallback, conn.baseUrl); if (!callback && !bIsRestore) { //check forgotten file - let forgotten = yield storage.listObjects(ctx, cfgForgottenFiles + '/' + docId); + let forgotten = yield storage.listObjects(ctx, docId, cfgForgottenFiles); hasForgotten = forgotten.length > 0; ctx.logger.debug('endAuth hasForgotten %s', hasForgotten); } @@ -2507,7 +2517,7 @@ exports.install = function(server, callbackFunction) { } changesJSON += ']\r\n'; let buffer = Buffer.from(changesJSON, 'utf8'); - yield storage.putObject(ctx, changesPrefix + (indexChunk++).toString().padStart(3, '0'), buffer, buffer.length); + yield storage.putObject(ctx, changesPrefix + (indexChunk++).toString().padStart(3, '0'), buffer, buffer.length, cfgErrorFiles); } index += cfgMaxRequestChanges; } while (changes && cfgMaxRequestChanges === changes.length); @@ -2560,6 +2570,10 @@ exports.install = function(server, callbackFunction) { } let allMessages = yield editorData.getMessages(ctx, docId); allMessages = allMessages.length > 0 ? allMessages : undefined;//todo client side + let sessionToken; + if (cfgTokenEnableBrowser && !bIsRestore) { + sessionToken = yield fillJwtByConnection(ctx, conn); + } const sendObject = { type: 'auth', result: 1, @@ -2570,7 +2584,7 @@ exports.install = function(server, callbackFunction) { locks: docLock, indexUser: conn.user.indexUser, hasForgotten: opt_hasForgotten, - jwt: (!bIsRestore && cfgTokenEnableBrowser) ? fillJwtByConnection(conn) : undefined, + jwt: sessionToken, g_cAscSpellCheckUrl: cfgEditor["spellcheckerUrl"], buildVersion: commonDefines.buildVersion, buildNumber: commonDefines.buildNumber, @@ -2975,6 +2989,7 @@ exports.install = function(server, callbackFunction) { function _checkLicense(ctx, conn) { return co(function* () { try { + ctx.logger.info('_checkLicense start'); let rights = constants.RIGHTS.Edit; if (config.get('server.edit_singleton')) { // ToDo docId from url ? @@ -3005,9 +3020,11 @@ exports.install = function(server, callbackFunction) { liveViewerSupport: (licenseInfo.connectionsView > 0 || licenseInfo.usersViewCount > 0 ), branding: licenseInfo.branding, customization: licenseInfo.customization, + advancedApi: licenseInfo.advancedApi, plugins: licenseInfo.plugins } }); + ctx.logger.info('_checkLicense end'); } catch (err) { ctx.logger.error('_checkLicense error: %s', err.stack); } @@ -3213,7 +3230,7 @@ exports.install = function(server, callbackFunction) { } else { let url; if (cmd.getInline()) { - url = canvasService.getPrintFileUrl(data.needUrlKey, participant.baseUrl, cmd.getTitle()); + url = yield canvasService.getPrintFileUrl(ctx, data.needUrlKey, participant.baseUrl, cmd.getTitle()); outputData.setExtName('.pdf'); } else { url = yield storage.getSignedUrl(ctx, participant.baseUrl, data.needUrlKey, data.needUrlType, cmd.getTitle(), data.creationDate); @@ -3224,7 +3241,7 @@ exports.install = function(server, callbackFunction) { if (undefined !== data.openedAt) { outputData.setOpenedAt(data.openedAt); } - modifyConnectionForPassword(ctx, participant, data.needUrlIsCorrectPassword); + yield modifyConnectionForPassword(ctx, participant, data.needUrlIsCorrectPassword); } sendData(ctx, participant, output); } @@ -3283,7 +3300,8 @@ exports.install = function(server, callbackFunction) { participant.user.username = cmd.getUserName(); yield addPresence(ctx, participant, false); if (cfgTokenEnableBrowser) { - sendDataRefreshToken(ctx, participant); + let sessionToken = yield fillJwtByConnection(ctx, participant); + sendDataRefreshToken(ctx, participant, sessionToken); } } } @@ -3316,6 +3334,7 @@ exports.install = function(server, callbackFunction) { return co(function* () { let ctx = new operationContext.Context(); try { + let tenants = {}; let countEditByShard = 0; let countLiveViewByShard = 0; let countViewByShard = 0; @@ -3325,6 +3344,10 @@ exports.install = function(server, callbackFunction) { for (var i = 0; i < connections.length; ++i) { var conn = connections[i]; ctx.initFromConnection(conn); + let tenant = tenants[ctx.tenant]; + if (!tenant) { + tenant = tenants[ctx.tenant] = {countEditByShard: 0, countLiveViewByShard: 0, countViewByShard: 0}; + } //wopi access_token_ttl; if (cfgExpSessionAbsolute > 0 || conn.access_token_ttl) { if ((cfgExpSessionAbsolute > 0 && maxMs - conn.sessionTimeConnect > cfgExpSessionAbsolute || @@ -3339,7 +3362,7 @@ exports.install = function(server, callbackFunction) { continue; } } - if (cfgExpSessionIdle > 0) { + if (cfgExpSessionIdle > 0 && !conn.user?.view) { if (maxMs - conn.sessionTimeLastAction > cfgExpSessionIdle && !conn.sessionIsSendWarning) { conn.sessionIsSendWarning = true; sendDataSession(ctx, conn, { @@ -3357,26 +3380,33 @@ exports.install = function(server, callbackFunction) { } yield addPresence(ctx, conn, false); if (utils.isLiveViewer(conn)) { - countLiveViewByShard++; + tenant.countLiveViewByShard++; } else if(conn.isCloseCoAuthoring || (conn.user && conn.user.view)) { - countViewByShard++; + tenant.countViewByShard++; } else { - countEditByShard++; + tenant.countEditByShard++; + } + } + for (let tenantId in tenants) { + if(tenants.hasOwnProperty(tenantId)) { + ctx.setTenant(tenantId); + let tenant = tenants[tenantId]; + yield* collectStats(ctx, tenant.countEditByShard, tenant.countLiveViewByShard, tenant.countViewByShard); + yield editorData.setEditorConnectionsCountByShard(ctx, SHARD_ID, tenant.countEditByShard); + yield editorData.setLiveViewerConnectionsCountByShard(ctx, SHARD_ID, tenant.countLiveViewByShard); + yield editorData.setViewerConnectionsCountByShard(ctx, SHARD_ID, tenant.countViewByShard); + if (clientStatsD) { + //todo with multitenant + let countEdit = yield editorData.getEditorConnectionsCount(ctx, connections); + clientStatsD.gauge('expireDoc.connections.edit', countEdit); + let countLiveView = yield editorData.getLiveViewerConnectionsCount(ctx, connections); + clientStatsD.gauge('expireDoc.connections.liveview', countLiveView); + let countView = yield editorData.getViewerConnectionsCount(ctx, connections); + clientStatsD.gauge('expireDoc.connections.view', countView); + } } } ctx.initDefault(); - yield* collectStats(ctx, countEditByShard, countLiveViewByShard, countViewByShard); - yield editorData.setEditorConnectionsCountByShard(ctx, SHARD_ID, countEditByShard); - yield editorData.setLiveViewerConnectionsCountByShard(ctx, SHARD_ID, countLiveViewByShard); - yield editorData.setViewerConnectionsCountByShard(ctx, SHARD_ID, countViewByShard); - if (clientStatsD) { - let countEdit = yield editorData.getEditorConnectionsCount(ctx, connections); - clientStatsD.gauge('expireDoc.connections.edit', countEdit); - let countLiveView = yield editorData.getLiveViewerConnectionsCount(ctx, connections); - clientStatsD.gauge('expireDoc.connections.liveview', countLiveView); - let countView = yield editorData.getViewerConnectionsCount(ctx, connections); - clientStatsD.gauge('expireDoc.connections.view', countView); - } } catch (err) { ctx.logger.error('expireDoc error: %s', err.stack); } finally { diff --git a/DocService/sources/canvasservice.js b/DocService/sources/canvasservice.js index ee9b9cdc..2dd46660 100644 --- a/DocService/sources/canvasservice.js +++ b/DocService/sources/canvasservice.js @@ -63,7 +63,6 @@ var cfgRedisPrefix = config.get('services.CoAuthoring.redis.prefix'); var cfgTokenEnableBrowser = config.get('services.CoAuthoring.token.enable.browser'); const cfgTokenSessionAlgorithm = config.get('services.CoAuthoring.token.session.algorithm'); const cfgTokenSessionExpires = ms(config.get('services.CoAuthoring.token.session.expires')); -const cfgSecretSession = config.get('services.CoAuthoring.secret.session'); const cfgForgottenFiles = config_server.get('forgottenfiles'); const cfgForgottenFilesName = config_server.get('forgottenfilesname'); const cfgOpenProtectedFile = config_server.get('openProtectedFile'); @@ -221,7 +220,7 @@ var getOutputData = co.wrap(function* (ctx, cmd, outputData, key, optConn, optAd if (optConn) { let url; if(cmd.getInline()) { - url = getPrintFileUrl(key, optConn.baseUrl, cmd.getTitle()); + url = yield getPrintFileUrl(ctx, key, optConn.baseUrl, cmd.getTitle()); } else { url = yield storage.getSignedUrl(ctx, optConn.baseUrl, strPath, commonDefines.c_oAscUrlTypes.Temporary, cmd.getTitle()); @@ -487,13 +486,12 @@ function* commandOpen(ctx, conn, cmd, outputData, opt_upsertRes, opt_bIsRestore) let updateIfRes = yield taskResult.updateIf(ctx, task, updateMask); if (updateIfRes.affectedRows > 0) { - let forgottenId = cfgForgottenFiles + '/' + cmd.getDocId(); - let forgotten = yield storage.listObjects(ctx, forgottenId); + let forgotten = yield storage.listObjects(ctx, cmd.getDocId(), cfgForgottenFiles); //replace url with forgotten file because it absorbed all lost changes if (forgotten.length > 0) { ctx.logger.debug("commandOpen from forgotten"); cmd.setUrl(undefined); - cmd.setForgotten(forgottenId); + cmd.setForgotten(cmd.getDocId()); } //add task cmd.setOutputFormat(constants.AVS_OFFICESTUDIO_FILE_CANVAS); @@ -530,7 +528,7 @@ function* commandReopen(ctx, conn, cmd, outputData) { if (sqlBase.DocumentPassword.prototype.getCurPassword(ctx, row.password)) { ctx.logger.debug('commandReopen has password'); yield* commandOpenFillOutput(ctx, conn, cmd, outputData, false); - docsCoServer.modifyConnectionForPassword(ctx, conn, constants.FILE_STATUS_OK === outputData.getStatus()); + yield docsCoServer.modifyConnectionForPassword(ctx, conn, constants.FILE_STATUS_OK === outputData.getStatus()); return res; } } @@ -848,7 +846,7 @@ function* commandSetPassword(ctx, conn, cmd, outputData) { if (upsertRes.affectedRows > 0) { outputData.setStatus('ok'); if (!conn.isEnterCorrectPassword) { - docsCoServer.modifyConnectionForPassword(ctx, conn, true); + yield docsCoServer.modifyConnectionForPassword(ctx, conn, true); } yield docsCoServer.resetForceSaveAfterChanges(ctx, cmd.getDocId(), newChangesLastDate.getTime(), 0, utils.getBaseUrlByConnection(conn), changeInfo); } else { @@ -979,8 +977,7 @@ function* commandSfcCallback(ctx, cmd, isSfcm, isEncrypted) { outputSfc.setUserData(cmd.getUserData()); if (!isError || isErrorCorrupted) { try { - let forgottenId = cfgForgottenFiles + '/' + docId; - let forgotten = yield storage.listObjects(ctx, forgottenId); + let forgotten = yield storage.listObjects(ctx, docId, cfgForgottenFiles); let isSendHistory = 0 === forgotten.length; if (!isSendHistory) { //check indicator file to determine if opening was from the forgotten file @@ -1120,7 +1117,7 @@ function* commandSfcCallback(ctx, cmd, isSfcm, isEncrypted) { try { ctx.logger.warn("storeForgotten"); let forgottenName = cfgForgottenFilesName + pathModule.extname(cmd.getOutputPath()); - yield storage.copyObject(ctx, savePathDoc, cfgForgottenFiles + '/' + docId + '/' + forgottenName); + yield storage.copyObject(ctx, savePathDoc, docId + '/' + forgottenName, undefined, cfgForgottenFiles); } catch (err) { ctx.logger.error('Error storeForgotten: %s', err.stack); } @@ -1453,17 +1450,19 @@ exports.saveFile = function(req, res) { } }); }; -function getPrintFileUrl(docId, baseUrl, filename) { - baseUrl = utils.checkBaseUrl(baseUrl); - let token = ''; - if (cfgTokenEnableBrowser) { - let payload = {document: {key: docId}}; - token = docsCoServer.signToken(payload, cfgTokenSessionAlgorithm, cfgTokenSessionExpires / 1000, cfgSecretSession); - } - //while save printed file Chrome's extension seems to rely on the resource name set in the URI https://stackoverflow.com/a/53593453 - //replace '/' with %2f before encodeURIComponent becase nginx determine %2f as '/' and get wrong system path - var userFriendlyName = encodeURIComponent(filename.replace(/\//g, "%2f")); - return `${baseUrl}/printfile/${encodeURIComponent(docId)}/${userFriendlyName}?token=${encodeURIComponent(token)}&filename=${userFriendlyName}`; +function getPrintFileUrl(ctx, docId, baseUrl, filename) { + return co(function*() { + baseUrl = utils.checkBaseUrl(baseUrl); + let token = ''; + if (cfgTokenEnableBrowser) { + let payload = {document: {key: docId}}; + token = yield docsCoServer.signToken(ctx, payload, cfgTokenSessionAlgorithm, cfgTokenSessionExpires / 1000, commonDefines.c_oAscSecretType.Session); + } + //while save printed file Chrome's extension seems to rely on the resource name set in the URI https://stackoverflow.com/a/53593453 + //replace '/' with %2f before encodeURIComponent becase nginx determine %2f as '/' and get wrong system path + var userFriendlyName = encodeURIComponent(filename.replace(/\//g, "%2f")); + return `${baseUrl}/printfile/${encodeURIComponent(docId)}/${userFriendlyName}?token=${encodeURIComponent(token)}&filename=${userFriendlyName}`; + }); } exports.getPrintFileUrl = getPrintFileUrl; exports.printFile = function(req, res) { diff --git a/DocService/sources/fileuploaderservice.js b/DocService/sources/fileuploaderservice.js index 49b2cb51..886212cb 100644 --- a/DocService/sources/fileuploaderservice.js +++ b/DocService/sources/fileuploaderservice.js @@ -122,22 +122,20 @@ exports.uploadImageFileOld = function(req, res) { let ctx = new operationContext.Context(); ctx.initFromRequest(req); var docId = req.params.docid; - var userid = req.params.userid; - ctx.init(ctx.tenant, docId, userid); + ctx.setDocId(docId); ctx.logger.debug('Start uploadImageFileOld'); if (cfgTokenEnableBrowser) { var checkJwtRes = yield* checkJwtUpload(ctx, 'uploadImageFileOld', req.query['token']); if(!checkJwtRes.err){ docId = checkJwtRes.docId || docId; - userid = checkJwtRes.userid || userid; + ctx.setDocId(docId); + ctx.setUserId(checkJwtRes.userid); } else { res.sendStatus(403); return; } } - ctx.init(ctx.tenant, docId, userid); var listImages = []; - //todo userid if (docId) { var isError = false; var form = new multiparty.Form(); @@ -176,7 +174,7 @@ exports.uploadImageFileOld = function(req, res) { ctx.logger.error('Error parsing form part:%s', err.toString()); }); }); - form.on('close', function() { + form.once('close', function() { if (isError) { res.sendStatus(400); } else { @@ -193,8 +191,8 @@ exports.uploadImageFileOld = function(req, res) { ctx.logger.debug('End uploadImageFileOld:%s', output); } ).catch(function(err) { + ctx.logger.error('error getSignedUrlsByArray:%s', err.stack); res.sendStatus(400); - ctx.logger.error('upload getSignedUrlsByArray:%s', err.stack); }); } }); @@ -229,11 +227,12 @@ exports.uploadImageFile = function(req, res) { if (!transformedRes.err) { docId = transformedRes.docId || docId; encrypted = transformedRes.encrypted; + ctx.setDocId(docId); + ctx.setUserId(transformedRes.userid); } else { isValidJwt = false; } } - ctx.setDocId(docId); if (isValidJwt && docId && req.body && Buffer.isBuffer(req.body)) { let buffer = req.body; diff --git a/DocService/sources/server.js b/DocService/sources/server.js index 92fba29a..6ae7da9b 100644 --- a/DocService/sources/server.js +++ b/DocService/sources/server.js @@ -218,8 +218,9 @@ docsCoServer.install(server, () => { res.sendStatus(403); } }); - app.post('/uploadold/:docid/:userid/:index', fileUploaderService.uploadImageFileOld); - app.post('/upload/:docid/:userid/:index', rawFileParser, fileUploaderService.uploadImageFile); + //'*' for backward compatible + app.post('/uploadold/:docid*', fileUploaderService.uploadImageFileOld); + app.post('/upload/:docid*', rawFileParser, fileUploaderService.uploadImageFile); app.post('/downloadas/:docid', rawFileParser, canvasService.downloadAs); app.post('/savefile/:docid', rawFileParser, canvasService.saveFile); diff --git a/FileConverter/sources/converter.js b/FileConverter/sources/converter.js index 8abc168d..51900df8 100644 --- a/FileConverter/sources/converter.js +++ b/FileConverter/sources/converter.js @@ -74,6 +74,7 @@ var cfgInputLimits = configConverter.get('inputLimits'); const cfgStreamWriterBufferSize = configConverter.get('streamWriterBufferSize'); //cfgMaxRequestChanges was obtained as a result of the test: 84408 changes - 5,16 MB const cfgMaxRequestChanges = config.get('services.CoAuthoring.server.maxRequestChanges'); +const cfgForgottenFiles = config.get('services.CoAuthoring.server.forgottenfiles'); const cfgForgottenFilesName = config.get('services.CoAuthoring.server.forgottenfilesname'); const cfgNewFileTemplate = config.get('services.CoAuthoring.server.newFileTemplate'); @@ -346,8 +347,8 @@ function* downloadFile(ctx, uri, fileFrom, withAuthorization, filterPrivate, opt } return res; } -function* downloadFileFromStorage(ctx, strPath, dir) { - var list = yield storage.listObjects(ctx, strPath); +function* downloadFileFromStorage(ctx, strPath, dir, opt_specialDir) { + var list = yield storage.listObjects(ctx, strPath, opt_specialDir); ctx.logger.debug('downloadFileFromStorage list %s', list.toString()); //create dirs var dirsToCreate = []; @@ -374,7 +375,7 @@ function* downloadFileFromStorage(ctx, strPath, dir) { for (var i = 0; i < list.length; ++i) { var file = list[i]; var fileRel = storage.getRelativePath(strPath, file); - var data = yield storage.getObject(ctx, file); + var data = yield storage.getObject(ctx, file, opt_specialDir); fs.writeFileSync(path.join(dir, fileRel), data); } } @@ -617,7 +618,7 @@ function* postProcess(ctx, cmd, dataConvert, tempDirs, childRes, error, isTimeou writeProcessOutputToLog(ctx, childRes, false); ctx.logger.error('ExitCode (code=%d;signal=%s;error:%d)', exitCode, exitSignal, error); if (cfgErrorFiles) { - yield* processUploadToStorage(ctx, tempDirs.temp, cfgErrorFiles + '/' + dataConvert.key); + yield* processUploadToStorage(ctx, tempDirs.temp, dataConvert.key, cfgErrorFiles); ctx.logger.debug('processUploadToStorage error complete(id=%s)', dataConvert.key); } } @@ -791,7 +792,7 @@ function* ExecuteTask(ctx, task) { } error = yield* processDownloadFromStorage(ctx, dataConvert, cmd, task, tempDirs, authorProps); } else if (cmd.getForgotten()) { - yield* downloadFileFromStorage(ctx, cmd.getForgotten(), tempDirs.source); + yield* downloadFileFromStorage(ctx, cmd.getForgotten(), tempDirs.source, cfgForgottenFiles); ctx.logger.debug('downloadFileFromStorage complete'); let list = yield utils.listObjects(tempDirs.source, false); if (list.length > 0) { @@ -816,21 +817,16 @@ function* ExecuteTask(ctx, task) { let childRes = null; let isTimeout = false; if (constants.NO_ERROR === error) { - if(constants.AVS_OFFICESTUDIO_FILE_OTHER_HTMLZIP === dataConvert.formatTo && cmd.getSaveKey() && !dataConvert.mailMergeSend) { - //todo заглушка.вся конвертация на клиенте, но нет простого механизма сохранения на клиенте - yield utils.pipeFiles(dataConvert.fileFrom, dataConvert.fileTo); - } else { + ({childRes, isTimeout} = yield* spawnProcess(ctx, isBuilder, tempDirs, dataConvert, authorProps, getTaskTime, task)); + if (childRes && 0 !== childRes.status && !isTimeout && task.getFromChanges() + && constants.AVS_OFFICESTUDIO_FILE_OTHER_OOXML !== dataConvert.formatTo + && !formatChecker.isOOXFormat(dataConvert.formatTo) && !cmd.getWopiParams()) { + ctx.logger.warn('rollback to save changes to ooxml. See assemblyFormatAsOrigin param. formatTo=%s', formatChecker.getStringFromFormat(dataConvert.formatTo)); + let extOld = path.extname(dataConvert.fileTo); + let extNew = '.' + formatChecker.getStringFromFormat(constants.AVS_OFFICESTUDIO_FILE_OTHER_OOXML); + dataConvert.formatTo = constants.AVS_OFFICESTUDIO_FILE_OTHER_OOXML; + dataConvert.fileTo = dataConvert.fileTo.slice(0, -extOld.length) + extNew; ({childRes, isTimeout} = yield* spawnProcess(ctx, isBuilder, tempDirs, dataConvert, authorProps, getTaskTime, task)); - if (childRes && 0 !== childRes.status && !isTimeout && task.getFromChanges() - && constants.AVS_OFFICESTUDIO_FILE_OTHER_OOXML !== dataConvert.formatTo - && !formatChecker.isOOXFormat(dataConvert.formatTo) && !cmd.getWopiParams()) { - ctx.logger.warn('rollback to save changes to ooxml. See assemblyFormatAsOrigin param. formatTo=%s', formatChecker.getStringFromFormat(dataConvert.formatTo)); - let extOld = path.extname(dataConvert.fileTo); - let extNew = '.' + formatChecker.getStringFromFormat(constants.AVS_OFFICESTUDIO_FILE_OTHER_OOXML); - dataConvert.formatTo = constants.AVS_OFFICESTUDIO_FILE_OTHER_OOXML; - dataConvert.fileTo = dataConvert.fileTo.slice(0, -extOld.length) + extNew; - ({childRes, isTimeout} = yield* spawnProcess(ctx, isBuilder, tempDirs, dataConvert, authorProps, getTaskTime, task)); - } } if(clientStatsD) { clientStatsD.timing('conv.spawnSync', new Date() - curDate); diff --git a/schema/postgresql/createdb.sql b/schema/postgresql/createdb.sql index b1a0b0d9..e1fc6e83 100644 --- a/schema/postgresql/createdb.sql +++ b/schema/postgresql/createdb.sql @@ -40,7 +40,7 @@ PRIMARY KEY ("tenant", "id") ) WITH (OIDS=FALSE); -CREATE OR REPLACE FUNCTION merge_db(_tetant varchar(255), _id varchar(255), _status int2, _status_info int4, _last_open_date timestamp without time zone, _user_index int4, _change_id int4, _callback text, _baseurl text, OUT isupdate char(5), OUT userindex int4) AS +CREATE OR REPLACE FUNCTION merge_db(_tenant varchar(255), _id varchar(255), _status int2, _status_info int4, _last_open_date timestamp without time zone, _user_index int4, _change_id int4, _callback text, _baseurl text, OUT isupdate char(5), OUT userindex int4) AS $$ DECLARE t_var "public"."task_result"."user_index"%TYPE; @@ -49,9 +49,9 @@ BEGIN -- first try to update the key -- note that "a" must be unique IF ((_callback <> '') IS TRUE) AND ((_baseurl <> '') IS TRUE) THEN - UPDATE "public"."task_result" SET last_open_date=_last_open_date, user_index=user_index+1,callback=_callback,baseurl=_baseurl WHERE tenant = _tetant AND id = _id RETURNING user_index into userindex; + UPDATE "public"."task_result" SET last_open_date=_last_open_date, user_index=user_index+1,callback=_callback,baseurl=_baseurl WHERE tenant = _tenant AND id = _id RETURNING user_index into userindex; ELSE - UPDATE "public"."task_result" SET last_open_date=_last_open_date, user_index=user_index+1 WHERE tenant = _tetant AND id = _id RETURNING user_index into userindex; + UPDATE "public"."task_result" SET last_open_date=_last_open_date, user_index=user_index+1 WHERE tenant = _tenant AND id = _id RETURNING user_index into userindex; END IF; IF found THEN isupdate := 'true'; @@ -61,7 +61,7 @@ BEGIN -- if someone else inserts the same key concurrently, -- we could get a unique-key failure BEGIN - INSERT INTO "public"."task_result"(id, status, status_info, last_open_date, user_index, change_id, callback, baseurl) VALUES(_tetant, _id, _status, _status_info, _last_open_date, _user_index, _change_id, _callback, _baseurl) RETURNING user_index into userindex; + INSERT INTO "public"."task_result"(id, status, status_info, last_open_date, user_index, change_id, callback, baseurl) VALUES(_tenant, _id, _status, _status_info, _last_open_date, _user_index, _change_id, _callback, _baseurl) RETURNING user_index into userindex; isupdate := 'false'; RETURN; EXCEPTION WHEN unique_violation THEN