From 22ab7500bd65dcbca9a30820bc75cbd7fab195a6 Mon Sep 17 00:00:00 2001 From: Sergey Konovalov Date: Sun, 9 Nov 2025 11:08:29 +0300 Subject: [PATCH 1/3] [bug] Fix crash in getJwtHsKey; Uses validation approach from jsonwebtoken library --- Common/sources/utils.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Common/sources/utils.js b/Common/sources/utils.js index 4e82c529..b0ba5b6a 100644 --- a/Common/sources/utils.js +++ b/Common/sources/utils.js @@ -1108,13 +1108,18 @@ const jwtKeyCache = Object.create(null); /** * Gets or creates a cached symmetric key for JWT verification (HS256/HS384/HS512). * Caches crypto.KeyObject to avoid expensive key creation on every request. - * @param {string} secret - JWT symmetric secret - * @returns {crypto.KeyObject} Cached secret key object + * Uses the same validation approach as jsonwebtoken library. + * @param {string|Buffer} secret - JWT symmetric secret + * @returns {crypto.KeyObject|undefined} Cached secret key object, or undefined when secret is missing/invalid */ function getJwtHsKey(secret) { let res = jwtKeyCache[secret]; - if (!res) { - res = jwtKeyCache[secret] = crypto.createSecretKey(Buffer.from(secret, 'utf8')); + if (!res && secret != null) { + try { + res = jwtKeyCache[secret] = crypto.createSecretKey(typeof secret === 'string' ? Buffer.from(secret, 'utf8') : secret); + } catch { + return undefined; + } } return res; } From 28ebde22e0bf939c68928b74590bb6584231fb9d Mon Sep 17 00:00:00 2001 From: Sergey Konovalov Date: Sun, 9 Nov 2025 12:11:59 +0300 Subject: [PATCH 2/3] [bug] Prevent I/O overload from repeated clientLog messages --- DocService/sources/DocsCoServer.js | 55 ++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/DocService/sources/DocsCoServer.js b/DocService/sources/DocsCoServer.js index edd65950..4700fdc9 100644 --- a/DocService/sources/DocsCoServer.js +++ b/DocService/sources/DocsCoServer.js @@ -1951,18 +1951,9 @@ exports.install = function (server, app, callbackFunction) { yield canvasService.openDocument(ctx, conn, cmd); break; } - case 'clientLog': { - const level = data.level?.toLowerCase(); - if ('trace' === level || 'debug' === level || 'info' === level || 'warn' === level || 'error' === level || 'fatal' === level) { - ctx.logger[level]('clientLog: %s', data.msg); - } - if ('error' === level && tenErrorFiles && docId) { - const destDir = 'browser/' + docId; - yield storage.copyPath(ctx, docId, destDir, undefined, tenErrorFiles); - yield* saveErrorChanges(ctx, docId, destDir); - } + case 'clientLog': + yield handleClientLog(ctx, conn, docId, data, tenErrorFiles); break; - } case 'extendSession': ctx.logger.debug('extendSession idletime: %d', data.idletime); conn.sessionIsSendWarning = false; @@ -2196,6 +2187,31 @@ exports.install = function (server, app, callbackFunction) { } } + /** + * Handle client log message and create error files once per connection on first error. + * @param {object} ctx - Operation context + * @param {object} conn - Socket connection + * @param {string} docId - Document identifier + * @param {{level?: string, msg?: string}} data - Client log data + * @param {object} tenErrorFiles - Error files storage configuration + * @returns {Promise} + */ + async function handleClientLog(ctx, conn, docId, data, tenErrorFiles) { + const level = data.level?.toLowerCase(); + if ('trace' === level || 'debug' === level || 'info' === level || 'warn' === level || 'error' === level || 'fatal' === level) { + ctx.logger[level]('clientLog: %s', data.msg); + } + if ('error' === level && tenErrorFiles && docId && !conn.clientError) { + conn.clientError = true; + const destDir = 'browser/' + docId; + const list = await storage.listObjects(ctx, destDir, tenErrorFiles); + if (list.length === 0) { + await storage.copyPath(ctx, docId, destDir, undefined, tenErrorFiles); + await saveErrorChanges(ctx, docId, destDir); + } + } + } + // Getting changes for the document (either from the cache or accessing the database, but only if there were saves) function* getDocumentChanges(ctx, docId, optStartIndex, optEndIndex) { // If during that moment, while we were waiting for a response from the database, everyone left, then nothing needs to be sent @@ -3252,7 +3268,16 @@ exports.install = function (server, app, callbackFunction) { return res; } - function* saveErrorChanges(ctx, docId, destDir) { + /** + * Save document changes to error files storage for debugging purposes. + * Retrieves changes from database and creates JSON chunks stored as separate files. + * + * @param {object} ctx - Operation context with configuration and logger + * @param {string} docId - Document identifier to retrieve changes for + * @param {string} destDir - Destination directory path in storage for error files + * @returns {Promise} Resolves when all changes are saved to storage + */ + async function saveErrorChanges(ctx, docId, destDir) { const tenEditor = getEditorConfig(ctx); const tenMaxRequestChanges = ctx.getCfg('services.CoAuthoring.server.maxRequestChanges', cfgMaxRequestChanges); const tenErrorFiles = ctx.getCfg('FileConverter.converter.errorfiles', cfgErrorFiles); @@ -3262,12 +3287,12 @@ exports.install = function (server, app, callbackFunction) { let changes; const changesPrefix = destDir + '/' + constants.CHANGES_NAME + '/' + constants.CHANGES_NAME + '.json.'; do { - changes = yield sqlBase.getChangesPromise(ctx, docId, index, index + tenMaxRequestChanges); + changes = await sqlBase.getChangesPromise(ctx, docId, index, index + tenMaxRequestChanges); if (changes.length > 0) { let buffer; if (tenEditor['binaryChanges']) { const buffers = changes.map(elem => elem.change_data); - buffers.unshift(Buffer.from(utils.getChangesFileHeader(), 'utf-8')); + buffers.unshift(Buffer.from(utils.getChangesFileHeader(), 'utf8')); buffer = Buffer.concat(buffers); } else { let changesJSON = indexChunk > 1 ? ',[' : '['; @@ -3279,7 +3304,7 @@ exports.install = function (server, app, callbackFunction) { changesJSON += ']\r\n'; buffer = Buffer.from(changesJSON, 'utf8'); } - yield storage.putObject(ctx, changesPrefix + (indexChunk++).toString().padStart(3, '0'), buffer, buffer.length, tenErrorFiles); + await storage.putObject(ctx, changesPrefix + (indexChunk++).toString().padStart(3, '0'), buffer, buffer.length, tenErrorFiles); } index += tenMaxRequestChanges; } while (changes && tenMaxRequestChanges === changes.length); From fb1bb7ae7785b8dec9916dfc066fc81672092659 Mon Sep 17 00:00:00 2001 From: Sergey Konovalov Date: Mon, 10 Nov 2025 20:03:07 +0300 Subject: [PATCH 3/3] [bug] Delete redis proxy key when all viewer quit; For bug 77217 --- DocService/sources/DocsCoServer.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DocService/sources/DocsCoServer.js b/DocService/sources/DocsCoServer.js index 4700fdc9..af9e9bf2 100644 --- a/DocService/sources/DocsCoServer.js +++ b/DocService/sources/DocsCoServer.js @@ -2177,6 +2177,10 @@ exports.install = function (server, app, callbackFunction) { userIndex ); } + } else { + if (hvals?.length <= 0 && editorStatProxy?.deleteKey) { + yield editorStatProxy.deleteKey(docId); + } } const sessionType = isView ? 'view' : 'edit'; const sessionTimeMs = new Date().getTime() - conn.sessionTimeConnect;