From 0371f6df4a09be0de28d8af8a6a656be987b8477 Mon Sep 17 00:00:00 2001 From: 18041 <180419202@qq.com> Date: Fri, 8 May 2026 20:18:25 +0800 Subject: [PATCH] Preserve custom ports in signed local file URLs Local file links could be returned without the configured deployment port, which breaks uploads and document flows on self-hosted instances that expose OpenSign on a custom port. This normalizes local /files URLs against SERVER_URL before signing them, and adds regression coverage for both fresh and already-signed local file URLs. Constraint: Must preserve existing signed URL flows for non-local storage backends Rejected: Rebuild file URLs in each frontend caller | too many call sites and it would leave backend-generated links inconsistent Confidence: medium Scope-risk: narrow Reversibility: clean Directive: Keep local file URL normalization tied to SERVER_URL so deployment origin changes stay centralized Tested: Direct Node assertions for normalizeLocalFileUrl, getSignedLocalUrl, and presignedlocalUrl with a custom-port SERVER_URL Not-tested: Full OpenSignServer jasmine suite in this environment (npm test runner hits local Windows EPERM restrictions) --- .../cloud/parsefunction/getSignedUrl.js | 31 +++++++++-- apps/OpenSignServer/spec/Tests.spec.js | 53 +++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/apps/OpenSignServer/cloud/parsefunction/getSignedUrl.js b/apps/OpenSignServer/cloud/parsefunction/getSignedUrl.js index 61d08430b4..289a0c1317 100644 --- a/apps/OpenSignServer/cloud/parsefunction/getSignedUrl.js +++ b/apps/OpenSignServer/cloud/parsefunction/getSignedUrl.js @@ -125,21 +125,46 @@ export async function getSignedUrl(request) { } } +export function normalizeLocalFileUrl(fileUrl) { + if (!fileUrl) return fileUrl; + + try { + const parsedFileUrl = new URL(fileUrl); + if (!parsedFileUrl.pathname.includes('/files/')) { + return fileUrl; + } + + const configuredServerUrl = process.env.SERVER_URL; + if (!configuredServerUrl) { + return fileUrl; + } + + const parsedServerUrl = new URL(configuredServerUrl); + parsedFileUrl.protocol = parsedServerUrl.protocol; + parsedFileUrl.host = parsedServerUrl.host; + + return parsedFileUrl.toString(); + } catch (err) { + return fileUrl; + } +} + // Function to generate a signed URL with JWT export function getSignedLocalUrl(fileUrl, expirationTimeInSeconds) { const secretKey = process.env.MASTER_KEY; const exp = expirationTimeInSeconds || 200; try { + const normalizedFileUrl = normalizeLocalFileUrl(fileUrl); // Create the payload with the file URL and expiration time const payload = { - fileUrl, + fileUrl: normalizedFileUrl, exp: Math.floor(Date.now() / 1000) + exp, // Expiry time in seconds }; // Generate the JWT token const token = jwt.sign(payload, secretKey); // Return the signed URL containing the token - return `${fileUrl}?token=${token}`; + return `${normalizedFileUrl}?token=${token}`; } catch (err) { console.log('Err while siging local url', err); throw new Error('Invalid or expired token.'); @@ -148,7 +173,7 @@ export function getSignedLocalUrl(fileUrl, expirationTimeInSeconds) { export function presignedlocalUrl(signedUrl, expirationTimeInSeconds) { if (signedUrl?.includes('files')) { - const fileUrl = signedUrl.split('?')?.[0]; + const fileUrl = normalizeLocalFileUrl(signedUrl.split('?')?.[0]); const secretKey = process.env.MASTER_KEY; const exp = expirationTimeInSeconds || 200; try { diff --git a/apps/OpenSignServer/spec/Tests.spec.js b/apps/OpenSignServer/spec/Tests.spec.js index dcd01f33ec..1c3e013cab 100644 --- a/apps/OpenSignServer/spec/Tests.spec.js +++ b/apps/OpenSignServer/spec/Tests.spec.js @@ -1,4 +1,10 @@ import axios from 'axios'; +import { + getSignedLocalUrl, + normalizeLocalFileUrl, + presignedlocalUrl, +} from '../cloud/parsefunction/getSignedUrl.js'; + describe('Parse Server example', () => { Parse.User.enableUnsafeCurrentUser(); it('call function', async () => { @@ -31,3 +37,50 @@ describe('Parse Server example', () => { expect(data).toContain('Parse Server Example'); }); }); + +describe('local file URL signing', () => { + const originalServerUrl = process.env.SERVER_URL; + const originalMasterKey = process.env.MASTER_KEY; + + beforeEach(() => { + process.env.SERVER_URL = 'https://opensign.example.com:8443/api/app'; + process.env.MASTER_KEY = 'test-master-key'; + }); + + afterAll(() => { + process.env.SERVER_URL = originalServerUrl; + process.env.MASTER_KEY = originalMasterKey; + }); + + it('normalizes local file URLs to the configured server origin', () => { + const normalizedUrl = normalizeLocalFileUrl( + 'https://opensign.example.com/api/app/files/opensign/sample.pdf' + ); + + expect(normalizedUrl).toBe( + 'https://opensign.example.com:8443/api/app/files/opensign/sample.pdf' + ); + }); + + it('preserves the configured port when signing local file URLs', () => { + const signedUrl = getSignedLocalUrl( + 'https://opensign.example.com/api/app/files/opensign/sample.pdf', + 200 + ); + + expect(signedUrl).toContain( + 'https://opensign.example.com:8443/api/app/files/opensign/sample.pdf?token=' + ); + }); + + it('re-signs existing local URLs with the configured port', () => { + const signedUrl = presignedlocalUrl( + 'https://opensign.example.com/api/app/files/opensign/sample.pdf?token=old-token', + 200 + ); + + expect(signedUrl).toContain( + 'https://opensign.example.com:8443/api/app/files/opensign/sample.pdf?token=' + ); + }); +});