[wopi] Log userSessionId; Allow usid in discovery query param; Send WOPI X-WOPI-CorrelationId/X-WOPI-SessionId headers;

This commit is contained in:
Sergey Konovalov
2025-08-20 10:54:45 +03:00
parent 3ec445b909
commit 3cd46b8cb7
9 changed files with 68 additions and 30 deletions

View File

@ -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"
}
}
},

View File

@ -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"
}
}
},

View File

@ -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,

View File

@ -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');

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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