Merge pull request 'fix/bugfix-9' (#88) from fix/bugfix-9 into release/v9.2.0

Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/server/pulls/88
This commit is contained in:
Oleg Korshul
2025-11-10 17:07:55 +00:00
2 changed files with 53 additions and 19 deletions

View File

@ -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;
}

View File

@ -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;
@ -2186,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;
@ -2196,6 +2191,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<void>}
*/
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 +3272,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<void>} 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 +3291,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 +3308,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);