diff --git a/.changeset/curly-teachers-sort.md b/.changeset/curly-teachers-sort.md new file mode 100644 index 00000000000..73c935a98bc --- /dev/null +++ b/.changeset/curly-teachers-sort.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes a bug where changing site url breaks `meteor_runtime_config` path until we reset the server diff --git a/apps/meteor/app/cors/server/cors.ts b/apps/meteor/app/cors/server/cors.ts index a9d50ee4297..12ec52d0ab0 100644 --- a/apps/meteor/app/cors/server/cors.ts +++ b/apps/meteor/app/cors/server/cors.ts @@ -1,4 +1,3 @@ -import { createHash } from 'crypto'; import type http from 'http'; import type { UrlWithParsedQuery } from 'url'; import url from 'url'; @@ -9,6 +8,7 @@ import { Meteor } from 'meteor/meteor'; import type { StaticFiles } from 'meteor/webapp'; import { WebApp, WebAppInternals } from 'meteor/webapp'; +import { getWebAppHash } from '../../../server/configuration/configureBoilerplate'; import { settings } from '../../settings/server'; // Taken from 'connect' types @@ -128,18 +128,9 @@ WebAppInternals.staticFilesMiddleware = function ( // a cache of the file for the wrong hash and start a client loop due to the mismatch // of the hashes of ui versions which would be checked against a websocket response if (path === '/meteor_runtime_config.js') { - const program = WebApp.clientPrograms[arch] as (typeof WebApp.clientPrograms)[string] & { - meteorRuntimeConfigHash?: string; - meteorRuntimeConfig: string; - }; - - if (!program?.meteorRuntimeConfigHash) { - program.meteorRuntimeConfigHash = createHash('sha1') - .update(JSON.stringify(encodeURIComponent(program.meteorRuntimeConfig))) - .digest('hex'); - } + const hash = getWebAppHash(arch); - if (program.meteorRuntimeConfigHash !== url.query.hash) { + if (!hash || hash !== url.query.hash) { res.writeHead(404); return res.end(); } diff --git a/apps/meteor/server/configuration/configureBoilerplate.ts b/apps/meteor/server/configuration/configureBoilerplate.ts index dbc5bd5e2ba..ca5c949c699 100644 --- a/apps/meteor/server/configuration/configureBoilerplate.ts +++ b/apps/meteor/server/configuration/configureBoilerplate.ts @@ -1,13 +1,39 @@ +import { createHash } from 'crypto'; + import { Meteor } from 'meteor/meteor'; -import { WebAppInternals } from 'meteor/webapp'; +import { WebApp, WebAppInternals } from 'meteor/webapp'; import type { ICachedSettings } from '../../app/settings/server/CachedSettings'; +const webAppHashes: Record = {}; + +export function getWebAppHash(arch: string): string | undefined { + if (!webAppHashes[arch]) { + const program = WebApp.clientPrograms[arch] as (typeof WebApp.clientPrograms)[string] & { + meteorRuntimeConfig: string; + }; + webAppHashes[arch] = createHash('sha1') + .update(JSON.stringify(encodeURIComponent(program.meteorRuntimeConfig))) + .digest('hex'); + } + + return webAppHashes[arch]; +} + +const { generateBoilerplate } = WebAppInternals; + +WebAppInternals.generateBoilerplate = function (...args: Parameters) { + for (const arch of Object.keys(WebApp.clientPrograms)) { + delete webAppHashes[arch]; + } + return generateBoilerplate.apply(this, args); +}; + export function configureBoilerplate(settings: ICachedSettings): void { settings.watch( 'Site_Url', // Needed as WebAppInternals.generateBoilerplate needs to be called in a fiber - Meteor.bindEnvironment((value) => { + Meteor.bindEnvironment(async (value) => { if (value == null || value.trim() === '') { return; } @@ -28,7 +54,7 @@ export function configureBoilerplate(settings: ICachedSettings): void { process.env.MOBILE_ROOT_URL = host; process.env.MOBILE_DDP_URL = host; if (typeof WebAppInternals !== 'undefined' && WebAppInternals.generateBoilerplate) { - return WebAppInternals.generateBoilerplate(); + await WebAppInternals.generateBoilerplate(); } }), ); diff --git a/apps/meteor/tests/end-to-end/api/cors.ts b/apps/meteor/tests/end-to-end/api/cors.ts new file mode 100644 index 00000000000..089c7c78066 --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/cors.ts @@ -0,0 +1,72 @@ +import { expect } from 'chai'; +import { after, before, describe, it } from 'mocha'; + +import { getCredentials, request } from '../../data/api-data'; +import { updateSetting } from '../../data/permissions.helper'; + +const getHash = () => + request + .get('/') + .expect(200) + .expect('Content-Type', 'text/html; charset=utf-8') + .then((res) => { + const hashMatch = res.text.match(/meteor_runtime_config\.js\?hash=([^"']+)/); + expect(hashMatch).to.not.be.null; + const hash = hashMatch![1]; + return hash; + }); + +describe('[CORS]', () => { + before((done) => getCredentials(done)); + after(async () => { + await updateSetting('Site_Url', 'http://localhost:3000'); + }); + + describe('[/meteor_runtime_config.js]', () => { + it('should return 404 when hash is missing', (done) => { + void request.get('/meteor_runtime_config.js').expect(404).end(done); + }); + + it('should return 404 when hash is invalid', (done) => { + void request.get('/meteor_runtime_config.js').query({ hash: 'invalid_hash' }).expect(404).end(done); + }); + + it('should return runtime config when hash matches', async () => { + // First get the root page to extract the hash + const hash = await getHash(); + + // Now request with the extracted hash + await request + .get('/meteor_runtime_config.js') + .query({ hash }) + .expect(200) + .expect('Content-Type', 'application/javascript; charset=UTF-8') + .expect((res) => { + expect(res.text).to.include('__meteor_runtime_config__'); + expect(res.text).to.include('http://localhost:3000'); + }); + }); + + it('should return 404 when hash does not match', (done) => { + // First get the root page to extract the hash + void request.get('/meteor_runtime_config.js').query({ hash: 'invalidHash' }).expect(404).end(done); + }); + + it('should update hash when ROOT_URL changes', async () => { + const originalHash = getHash(); + await updateSetting('Site_Url', 'http://new-url:3000'); + const newHash = await getHash(); + expect(newHash).to.not.equal(originalHash); + // it should return if the new hash is valid + await request + .get('/meteor_runtime_config.js') + .query({ hash: newHash }) + .expect(200) + .expect('Content-Type', 'application/javascript; charset=UTF-8') + .expect((res) => { + expect(res.text).to.include('__meteor_runtime_config__'); + expect(res.text).to.include('http://new-url:3000'); + }); + }); + }); +});