diff --git a/tdrive/backend/node/src/services/documents/entities/drive-file.ts b/tdrive/backend/node/src/services/documents/entities/drive-file.ts index f5bfe9d6..e4e029a5 100644 --- a/tdrive/backend/node/src/services/documents/entities/drive-file.ts +++ b/tdrive/backend/node/src/services/documents/entities/drive-file.ts @@ -135,6 +135,14 @@ const OnlyOfficeSafeDocKeyBase64 = { }, }; +function checkFieldValue(field: string, value: string) { + if (!/^[0-9a-zA-Z_-]+$/m.test(value)) + throw new Error( + `Invalid ${field} value (${JSON.stringify( + value, + )}). Must be short and only alpha numeric or '_' and '-'`, + ); +} /** Reference implementation for generating then parsing the {@link DriveFile.editing_session_key} field */ export const EditingSessionKeyFormat = { // OnlyOffice key limits: 128 chars, [0-9a-zA-Z.=_-] @@ -145,13 +153,15 @@ export const EditingSessionKeyFormat = { // common denominator to all plugin/interop systems. Plugins that // require something even stricter have the option of maintaining // a look up table to an acceptable value. - generate(applicationId: string, companyId: string, userId: string, overrideTimeStamp?: Date) { - if (!/^[0-9a-zA-Z_-]+$/m.test(applicationId)) - throw new Error( - `Invalid applicationId string (${JSON.stringify( - applicationId, - )}). Must be short and only alpha numeric`, - ); + generate( + applicationId: string, + instanceId: string, + companyId: string, + userId: string, + overrideTimeStamp?: Date, + ) { + checkFieldValue("applicationId", applicationId); + checkFieldValue("instanceId", instanceId); const isoUTCDateNoSpecialCharsNoMS = (overrideTimeStamp ?? new Date()) .toISOString() .replace(/\..+$/, "") @@ -162,7 +172,7 @@ export const EditingSessionKeyFormat = { const idsString = OnlyOfficeSafeDocKeyBase64.fromBuffer( Buffer.concat([companyIdBuffer, userIdBuffer, entropyBuffer]), ); - const newKey = [isoUTCDateNoSpecialCharsNoMS, applicationId, idsString].join("="); + const newKey = [isoUTCDateNoSpecialCharsNoMS, applicationId, instanceId, idsString].join("="); if (newKey.length > 128 || !/^[0-9a-zA-Z=_-]+$/m.test(newKey)) throw new Error( `Invalid generated editingSessionKey (${JSON.stringify( @@ -174,14 +184,14 @@ export const EditingSessionKeyFormat = { parse(editingSessionKey: string) { const parts = editingSessionKey.split("="); - const expectedParts = 3; + const expectedParts = 4; if (parts.length !== expectedParts) throw new Error( `Invalid editingSessionKey (${JSON.stringify( editingSessionKey, )}). Expected ${expectedParts} parts`, ); - const [timestampStr, appId, idsOOBase64String] = parts; + const [timestampStr, applicationId, instanceId, idsOOBase64String] = parts; const timestampMatch = timestampStr.match( /^(?\d{4})(?\d\d)(?\d\d)(?\d\d)(?\d\d)(?\d\d)$/, ); @@ -199,7 +209,8 @@ export const EditingSessionKeyFormat = { timestamp: new Date( Date.parse(`${[year, month, day].join("-")}T${[hour, minute, second].join(":")}Z`), ), - applicationId: appId, + applicationId, + instanceId, companyId, userId, }; diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index dabc3778..61e720d8 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -967,6 +967,7 @@ export class DocumentsService { beginEditing = async ( id: string, editorApplicationId: string, + appInstanceId: string, context: DriveExecutionContext, ) => { if (!context) { @@ -978,6 +979,7 @@ export class DocumentsService { try { newKey = EditingSessionKeyFormat.generate( editorApplicationId, + appInstanceId, context.company.id, context.user.id, ); diff --git a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts index 0084350e..e794117c 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -351,7 +351,7 @@ export class DocumentsController { request: FastifyRequest<{ Params: ItemRequestParams; //TODO application id should be received from the token that we have during the login - Body: { editorApplicationId: string }; + Body: { editorApplicationId: string; instanceId: string }; }>, ) => { try { @@ -366,6 +366,7 @@ export class DocumentsController { return await globalResolver.services.documents.documents.beginEditing( id, request.body.editorApplicationId, + request.body.instanceId || "", context, ); } catch (error) { diff --git a/tdrive/backend/node/test/unit/core/services/documents/drive-file-editing-sessions-key.test.ts b/tdrive/backend/node/test/unit/core/services/documents/drive-file-editing-sessions-key.test.ts index 89498de0..c5e10cac 100644 --- a/tdrive/backend/node/test/unit/core/services/documents/drive-file-editing-sessions-key.test.ts +++ b/tdrive/backend/node/test/unit/core/services/documents/drive-file-editing-sessions-key.test.ts @@ -7,6 +7,7 @@ describe('DriveFile EditingSessionKeyFormat', () => { const mockAppId = 'tdrive_random_application_id'; const mockCompanyId = randomUUID(); const mockUserId = randomUUID(); + const mockInstanceId = "super-instance-id"; const mockTimestamp = new Date(); const mockTimestampWith0MS = mockTimestamp.getTime() - (mockTimestamp.getTime() % 1000); @@ -23,31 +24,32 @@ describe('DriveFile EditingSessionKeyFormat', () => { test('generates a valid value that can be parsed', async () => { checkIsUUID(mockUserId); checkIsUUID(mockCompanyId); - const key = EditingSessionKeyFormat.generate(mockAppId, mockCompanyId, mockUserId, mockTimestamp); + const key = EditingSessionKeyFormat.generate(mockAppId, mockInstanceId, mockCompanyId, mockUserId, mockTimestamp); checkKeyIsOOCompatible(key); const parsed = EditingSessionKeyFormat.parse(key); expect(parsed.applicationId).toBe(mockAppId); + expect(parsed.instanceId).toBe(mockInstanceId); expect(parsed.userId).toBe(mockUserId); expect(parsed.companyId).toBe(mockCompanyId); expect(parsed.timestamp.getTime()).toBe(mockTimestampWith0MS); }); test('generates unique values', async () => { - const key = EditingSessionKeyFormat.generate(mockAppId, mockCompanyId, mockUserId, mockTimestamp); - const key2 = EditingSessionKeyFormat.generate(mockAppId, mockCompanyId, mockUserId, mockTimestamp); + const key = EditingSessionKeyFormat.generate(mockAppId, mockInstanceId, mockCompanyId, mockUserId, mockTimestamp); + const key2 = EditingSessionKeyFormat.generate(mockAppId, mockInstanceId, mockCompanyId, mockUserId, mockTimestamp); expect(key).not.toBe(key2); }); test('checks the appId', async () => { expect(() => { - EditingSessionKeyFormat.generate('invalid app id !', mockCompanyId, mockUserId); - }).toThrow('Invalid applicationId string'); + EditingSessionKeyFormat.generate('invalid app id !', mockInstanceId, mockCompanyId, mockUserId); + }).toThrow('Invalid applicationId value'); }); test('checks final length', async () => { expect(() => { const tooLongAppID = new Array(100).join('x'); - EditingSessionKeyFormat.generate(tooLongAppID, mockCompanyId, mockUserId); + EditingSessionKeyFormat.generate(tooLongAppID, mockInstanceId, mockCompanyId, mockUserId); }).toThrow('Must be <128 chars,'); }); }); \ No newline at end of file