diff --git a/Common/config/log4js/development.json b/Common/config/log4js/development.json index 67134f6a..fea3d9a1 100644 --- a/Common/config/log4js/development.json +++ b/Common/config/log4js/development.json @@ -3,8 +3,8 @@ "default": { "type": "console", "layout": { - "type": "pattern", - "pattern": "%[[%d] [%p] [%X{TENANT}] [%X{DOCID}] [%X{USERID}] %c -%] %.10000m" + "type": "patternWithTokens", + "pattern": "%[[%d] [%p] [%X{TENANT}] [%X{DOCID}] [%X{USERID}]%x{usid} %c -%] %.10000m" } } }, diff --git a/Common/config/log4js/production.json b/Common/config/log4js/production.json index 84943039..77d7e110 100644 --- a/Common/config/log4js/production.json +++ b/Common/config/log4js/production.json @@ -3,8 +3,8 @@ "default": { "type": "console", "layout": { - "type": "pattern", - "pattern": "[%d] [%p] [%X{TENANT}] [%X{DOCID}] [%X{USERID}] %c - %.10000m" + "type": "patternWithTokens", + "pattern": "[%d] [%p] [%X{TENANT}] [%X{DOCID}] [%X{USERID}]%x{usid} %c - %.10000m" } } }, diff --git a/Common/sources/constants.js b/Common/sources/constants.js index 30b57572..8e3c7c76 100644 --- a/Common/sources/constants.js +++ b/Common/sources/constants.js @@ -52,6 +52,7 @@ exports.DEFAULT_USER_ID = 'userId'; exports.ALLOWED_PROTO = /^https?$/i; exports.SHARD_KEY_WOPI_NAME = 'WOPISrc'; exports.SHARD_KEY_API_NAME = 'shardkey'; +exports.USER_SESSION_ID_NAME = 'usid'; exports.RIGHTS = { None : 0, diff --git a/Common/sources/logger.js b/Common/sources/logger.js index 5e71c95b..7f189504 100644 --- a/Common/sources/logger.js +++ b/Common/sources/logger.js @@ -36,6 +36,7 @@ var config = require('config'); var util = require('util'); var log4js = require('log4js'); +const layouts = require('log4js/lib/layouts'); // https://stackoverflow.com/a/36643588 var dateToJSONWithTZ = function (d) { @@ -62,6 +63,23 @@ log4js.addLayout('json', function(config) { } }); +/** + * Custom pattern layout that supports %x{usid} using USERSESSIONID from context. + * @param {object} cfg + * @returns {function} + */ +log4js.addLayout('patternWithTokens', function(cfg) { + const pattern = (cfg && cfg.pattern) ? cfg.pattern : '%m'; + const baseTokens = (cfg && cfg.tokens) ? cfg.tokens : {}; + const tokens = Object.assign({}, baseTokens, { + usid: function(ev) { + const id = ev && ev.context && ev.context.USERSESSIONID; + return id ? ` [${id}]` : ''; + } + }); + return layouts.patternLayout(pattern, tokens); +}); + log4js.configure(config.get('log.filePath')); var logger = log4js.getLogger('nodeJS'); diff --git a/Common/sources/operationContext.js b/Common/sources/operationContext.js index a87d56ad..e2d1c736 100644 --- a/Common/sources/operationContext.js +++ b/Common/sources/operationContext.js @@ -43,12 +43,13 @@ function Context(){ this.logger = logger.getLogger('nodeJS'); this.initDefault(); } -Context.prototype.init = function(tenant, docId, userId, opt_shardKey, opt_WopiSrc) { +Context.prototype.init = function(tenant, docId, userId, opt_shardKey, opt_WopiSrc, opt_userSessionId) { this.setTenant(tenant); this.setDocId(docId); this.setUserId(userId); this.setShardKey(opt_shardKey); this.setWopiSrc(opt_WopiSrc); + this.setUserSessionId(opt_userSessionId); this.config = null; this.secret = null; @@ -72,21 +73,23 @@ Context.prototype.initFromConnection = function(conn) { let userId = conn.user?.id; let shardKey = utils.getShardKeyByConnection(this, conn); let wopiSrc = utils.getWopiSrcByConnection(this, conn); - this.init(tenant, docId || this.docId, userId || this.userId, shardKey, wopiSrc); + let userSessionId = utils.getSessionIdByConnection(this, conn); + this.init(tenant, docId || this.docId, userId || this.userId, shardKey, wopiSrc, userSessionId); }; Context.prototype.initFromRequest = function(req) { let tenant = tenantManager.getTenantByRequest(this, req); let shardKey = utils.getShardKeyByRequest(this, req); let wopiSrc = utils.getWopiSrcByRequest(this, req); - this.init(tenant, this.docId, this.userId, shardKey, wopiSrc); + let userSessionId = utils.getSessionIdByRequest(this, req); + this.init(tenant, this.docId, this.userId, shardKey, wopiSrc, userSessionId); }; Context.prototype.initFromTaskQueueData = function(task) { let ctx = task.getCtx(); - this.init(ctx.tenant, ctx.docId, ctx.userId, ctx.shardKey, ctx.wopiSrc); + this.init(ctx.tenant, ctx.docId, ctx.userId, ctx.shardKey, ctx.wopiSrc, ctx.userSessionId); }; Context.prototype.initFromPubSub = function(data) { let ctx = data.ctx; - this.init(ctx.tenant, ctx.docId, ctx.userId, ctx.shardKey, ctx.wopiSrc); + this.init(ctx.tenant, ctx.docId, ctx.userId, ctx.shardKey, ctx.wopiSrc, ctx.userSessionId); }; Context.prototype.initTenantCache = async function() { const runtimeConfig = await runtimeConfigManager.getConfig(this); @@ -114,11 +117,18 @@ Context.prototype.setShardKey = function(shardKey) { Context.prototype.setWopiSrc = function(wopiSrc) { this.wopiSrc = wopiSrc; }; +Context.prototype.setUserSessionId = function(userSessionId) { + if (userSessionId) { + this.userSessionId = userSessionId; + this.logger.addContext('USERSESSIONID', userSessionId); + } +}; Context.prototype.toJSON = function() { return { tenant: this.tenant, docId: this.docId, userId: this.userId, + userSessionId: this.userSessionId, shardKey: this.shardKey, wopiSrc: this.wopiSrc } diff --git a/Common/sources/utils.js b/Common/sources/utils.js index 1d1dfc67..a1dc97c8 100644 --- a/Common/sources/utils.js +++ b/Common/sources/utils.js @@ -877,16 +877,24 @@ function getShardKeyByConnection(ctx, conn) { function getWopiSrcByConnection(ctx, conn) { return conn?.handshake?.query?.[constants.SHARD_KEY_WOPI_NAME]; } +function getSessionIdByConnection(ctx, conn) { + return conn?.handshake?.query?.[constants.USER_SESSION_ID_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]; } +function getSessionIdByRequest(ctx, req) { + return req.query?.[constants.USER_SESSION_ID_NAME]; +} exports.getShardKeyByConnection = getShardKeyByConnection; exports.getWopiSrcByConnection = getWopiSrcByConnection; +exports.getSessionIdByConnection = getSessionIdByConnection; exports.getShardKeyByRequest = getShardKeyByRequest; exports.getWopiSrcByRequest = getWopiSrcByRequest; +exports.getSessionIdByRequest = getSessionIdByRequest; function stream2Buffer(stream) { return new Promise(function(resolve, reject) { if (!stream.readable) { diff --git a/DocService/sources/canvasservice.js b/DocService/sources/canvasservice.js index fa410c29..6f41f479 100644 --- a/DocService/sources/canvasservice.js +++ b/DocService/sources/canvasservice.js @@ -1602,6 +1602,9 @@ function getPrintFileUrl(ctx, docId, baseUrl, filename) { if (ctx.wopiSrc) { res += `&${constants.SHARD_KEY_WOPI_NAME}=${encodeURIComponent(ctx.wopiSrc)}`; } + if (ctx.userSessionId) { + res += `&${constants.USER_SESSION_ID_NAME}=${encodeURIComponent(ctx.userSessionId)}`; + } res += `&filename=${userFriendlyName}`; return res; }); diff --git a/DocService/sources/wopiClient.js b/DocService/sources/wopiClient.js index 1b2287a4..ff091bfb 100644 --- a/DocService/sources/wopiClient.js +++ b/DocService/sources/wopiClient.js @@ -32,6 +32,7 @@ 'use strict'; +const crypto = require('crypto'); const path = require('path'); const { pipeline } = require('node:stream/promises'); const {URL} = require('url'); @@ -576,7 +577,7 @@ async function prepareDocumentForEditing(ctx, wopiSrc, fileInfo, userAuth, fileT if (!shutdownFlag) { const preOpenRes = await preOpen(ctx, checkRes.lockId, docId, fileInfo, userAuth, baseUrl, fileType); if (!preOpenRes && userAuth.mode !== 'view') { - ctx.logger.error('prepareDocumentForEditing error: lock failed, fallback to view mode'); + ctx.logger.warn('prepareDocumentForEditing error: lock failed, fallback to view mode'); userAuth.mode = 'view'; userAuth.forcedViewMode = true; return await prepareDocumentForEditing(ctx, wopiSrc, fileInfo, userAuth, fileType, baseUrl, params); @@ -601,6 +602,8 @@ function getEditorHtml(req, res) { let wopiSrc = req.query['wopisrc']; let fileId = wopiSrc.substring(wopiSrc.lastIndexOf('/') + 1); ctx.setDocId(fileId); + let usid = req.query['usid'] || crypto.randomUUID(); + ctx.setUserSessionId(usid); ctx.logger.info('wopiEditor start'); ctx.logger.debug(`wopiEditor req.url:%s`, req.url); @@ -610,7 +613,6 @@ function getEditorHtml(req, res) { params.documentType = req.params.documentType; let mode = req.params.mode; let sc = req.query['sc']; - let hostSessionId = req.query['hid']; let lang = req.query['lang']; let ui = req.query['ui']; let access_token = req.body['access_token'] || ""; @@ -619,7 +621,15 @@ function getEditorHtml(req, res) { if (docs_api_config) { params.docs_api_config = JSON.parse(docs_api_config); } - + // Create user authentication object + const userAuth = params.userAuth = { + wopiSrc: wopiSrc, + access_token: access_token, + access_token_ttl: access_token_ttl, + userSessionId: usid, + mode: mode, + forcedViewMode: false + }; let fileInfo = params.fileInfo = yield checkFileInfo(ctx, wopiSrc, access_token, sc); if (!fileInfo) { @@ -633,19 +643,10 @@ function getEditorHtml(req, res) { const canEdit = (fileInfo.UserCanOnlyComment || fileInfo.UserCanWrite || fileInfo.UserCanReview); if (!canEdit) { - ctx.logger.error('wopiEditor error: edit mode is not allowed'); - mode = 'view'; + ctx.logger.warn('wopiEditor: edit mode is not allowed, fallback to view mode'); + userAuth.mode = 'view'; + userAuth.forcedViewMode = true; } - // Create user authentication object - const userAuth = params.userAuth = { - wopiSrc: wopiSrc, - access_token: access_token, - access_token_ttl: access_token_ttl, - hostSessionId: hostSessionId, - userSessionId: undefined, // Will be set after prepareDocumentForEditing - mode: mode, - forcedViewMode: false - }; // Prepare document for editing (docId, cache validation) const prepareResult = yield prepareDocumentForEditing(ctx, wopiSrc, fileInfo, userAuth, fileType, utils.getBaseUrlByRequest(ctx, req), params); @@ -654,8 +655,6 @@ function getEditorHtml(req, res) { return; } - // Update userSessionId with the document ID - userAuth.userSessionId = params.key; mode = userAuth.mode; ctx.setDocId(params.key); @@ -1054,7 +1053,7 @@ function getWopiParams(lockId, fileInfo, wopiSrc, access_token, access_token_ttl let commonInfo = {lockId: lockId, fileInfo: fileInfo}; let userAuth = { wopiSrc: wopiSrc, access_token: access_token, access_token_ttl: access_token_ttl, - hostSessionId: null, userSessionId: null, mode: null + userSessionId: null, mode: null }; return {commonInfo: commonInfo, userAuth: userAuth, LastModifiedTime: null}; } diff --git a/DocService/sources/wopiUtils.js b/DocService/sources/wopiUtils.js index 9a3e57b6..dfd38d46 100644 --- a/DocService/sources/wopiUtils.js +++ b/DocService/sources/wopiUtils.js @@ -106,9 +106,8 @@ async function fillStandardHeaders(ctx, headers, url, access_token) { } headers['X-WOPI-TimeStamp'] = timeStamp; headers['X-WOPI-ClientVersion'] = commonDefines.buildVersion + '.' + commonDefines.buildNumber; - // todo - // headers['X-WOPI-CorrelationId '] = ""; - // headers['X-WOPI-SessionId'] = ""; + headers['X-WOPI-CorrelationId'] = crypto.randomUUID(); + headers['X-WOPI-SessionId'] = ctx.userSessionId; //remove redundant header https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/common-headers#request-headers // headers['Authorization'] = `Bearer ${access_token}`; }