diff --git a/.changeset/wicked-humans-hang.md b/.changeset/wicked-humans-hang.md new file mode 100644 index 0000000000000..e793bc978902d --- /dev/null +++ b/.changeset/wicked-humans-hang.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Improve cache of static files diff --git a/apps/meteor/app/cors/server/cors.ts b/apps/meteor/app/cors/server/cors.ts index 03a42e45a17bb..cb6fa94273a28 100644 --- a/apps/meteor/app/cors/server/cors.ts +++ b/apps/meteor/app/cors/server/cors.ts @@ -1,4 +1,6 @@ +import { createHash } from 'crypto'; import type http from 'http'; +import type { UrlWithParsedQuery } from 'url'; import url from 'url'; import { Logger } from '@rocket.chat/logger'; @@ -77,6 +79,19 @@ WebApp.rawConnectHandlers.use((_req: http.IncomingMessage, res: http.ServerRespo }); const _staticFilesMiddleware = WebAppInternals.staticFilesMiddleware; +declare module 'meteor/webapp' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace WebApp { + function categorizeRequest( + req: http.IncomingMessage, + ): { arch: string; path: string; url: UrlWithParsedQuery } & Record; + } +} + +// These routes already handle cache control on their own +const cacheControlledRoutes: Array = ['/assets', '/custom-sounds', '/emoji-custom', '/avatar', '/file-upload'].map( + (route) => new RegExp(`^${route}`, 'i'), +); // @ts-expect-error - accessing internal property of webapp WebAppInternals.staticFilesMiddleware = function ( @@ -86,6 +101,32 @@ WebAppInternals.staticFilesMiddleware = function ( next: NextFunction, ) { res.setHeader('Access-Control-Allow-Origin', '*'); + const { arch, path, url } = WebApp.categorizeRequest(req); + + if (Meteor.isProduction && !cacheControlledRoutes.some((regexp) => regexp.test(path))) { + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } + + // Prevent meteor_runtime_config.js to load from a different expected hash possibly causing + // 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'); + } + + if (program.meteorRuntimeConfigHash !== url.query.hash) { + res.writeHead(404); + return res.end(); + } + } return _staticFilesMiddleware(staticFiles, req, res, next); }; diff --git a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts index a4f59136a1f9f..f881c15f98867 100644 --- a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts +++ b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts @@ -7,21 +7,22 @@ import { getURL } from '../../../utils/client'; import { sdk } from '../../../utils/client/lib/SDKClient'; const getCustomSoundId = (soundId: ICustomSound['_id']) => `custom-sound-${soundId}`; +const getAssetUrl = (asset: string, params?: Record) => getURL(asset, params, undefined, true); const defaultSounds = [ - { _id: 'chime', name: 'Chime', extension: 'mp3', src: getURL('sounds/chime.mp3') }, - { _id: 'door', name: 'Door', extension: 'mp3', src: getURL('sounds/door.mp3') }, - { _id: 'beep', name: 'Beep', extension: 'mp3', src: getURL('sounds/beep.mp3') }, - { _id: 'chelle', name: 'Chelle', extension: 'mp3', src: getURL('sounds/chelle.mp3') }, - { _id: 'ding', name: 'Ding', extension: 'mp3', src: getURL('sounds/ding.mp3') }, - { _id: 'droplet', name: 'Droplet', extension: 'mp3', src: getURL('sounds/droplet.mp3') }, - { _id: 'highbell', name: 'Highbell', extension: 'mp3', src: getURL('sounds/highbell.mp3') }, - { _id: 'seasons', name: 'Seasons', extension: 'mp3', src: getURL('sounds/seasons.mp3') }, - { _id: 'telephone', name: 'Telephone', extension: 'mp3', src: getURL('sounds/telephone.mp3') }, - { _id: 'outbound-call-ringing', name: 'Outbound Call Ringing', extension: 'mp3', src: getURL('sounds/outbound-call-ringing.mp3') }, - { _id: 'call-ended', name: 'Call Ended', extension: 'mp3', src: getURL('sounds/call-ended.mp3') }, - { _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getURL('sounds/dialtone.mp3') }, - { _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getURL('sounds/ringtone.mp3') }, + { _id: 'chime', name: 'Chime', extension: 'mp3', src: getAssetUrl('sounds/chime.mp3') }, + { _id: 'door', name: 'Door', extension: 'mp3', src: getAssetUrl('sounds/door.mp3') }, + { _id: 'beep', name: 'Beep', extension: 'mp3', src: getAssetUrl('sounds/beep.mp3') }, + { _id: 'chelle', name: 'Chelle', extension: 'mp3', src: getAssetUrl('sounds/chelle.mp3') }, + { _id: 'ding', name: 'Ding', extension: 'mp3', src: getAssetUrl('sounds/ding.mp3') }, + { _id: 'droplet', name: 'Droplet', extension: 'mp3', src: getAssetUrl('sounds/droplet.mp3') }, + { _id: 'highbell', name: 'Highbell', extension: 'mp3', src: getAssetUrl('sounds/highbell.mp3') }, + { _id: 'seasons', name: 'Seasons', extension: 'mp3', src: getAssetUrl('sounds/seasons.mp3') }, + { _id: 'telephone', name: 'Telephone', extension: 'mp3', src: getAssetUrl('sounds/telephone.mp3') }, + { _id: 'outbound-call-ringing', name: 'Outbound Call Ringing', extension: 'mp3', src: getAssetUrl('sounds/outbound-call-ringing.mp3') }, + { _id: 'call-ended', name: 'Call Ended', extension: 'mp3', src: getAssetUrl('sounds/call-ended.mp3') }, + { _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getAssetUrl('sounds/dialtone.mp3') }, + { _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getAssetUrl('sounds/ringtone.mp3') }, ]; class CustomSoundsClass { @@ -85,7 +86,7 @@ class CustomSoundsClass { } getURL(sound: ICustomSound) { - return getURL(`/custom-sounds/${sound._id}.${sound.extension}?_dc=${sound.random || 0}`); + return getAssetUrl(`/custom-sounds/${sound._id}.${sound.extension}`, { _dc: sound.random || 0 }); } getList() { diff --git a/apps/meteor/app/ui-master/server/inject.ts b/apps/meteor/app/ui-master/server/inject.ts index 78112bcee343f..1e00a0e47433f 100644 --- a/apps/meteor/app/ui-master/server/inject.ts +++ b/apps/meteor/app/ui-master/server/inject.ts @@ -32,7 +32,7 @@ const callback: NextHandleFunction = (req, res, next) => { return; } - const injection = headInjections.get(pathname.replace(/^\//, '')) as Injection | undefined; + const injection = headInjections.get(pathname.replace(/^\//, '').split('_')[0]) as Injection | undefined; if (!injection || typeof injection === 'string') { next(); @@ -76,27 +76,37 @@ export const injectIntoHead = (key: string, value: Injection): void => { }; export const addScript = (key: string, content: string): void => { + if (/_/.test(key)) { + throw new Error('inject.js > addScript - key cannot contain "_" (underscore)'); + } + if (!content.trim()) { - injectIntoHead(`${key}.js`, ''); + injectIntoHead(key, ''); return; } const currentHash = crypto.createHash('sha1').update(content).digest('hex'); - injectIntoHead(`${key}.js`, { + + injectIntoHead(key, { type: 'JS', - tag: ``, + tag: ``, content, }); }; export const addStyle = (key: string, content: string): void => { + if (/_/.test(key)) { + throw new Error('inject.js > addStyle - key cannot contain "_" (underscore)'); + } + if (!content.trim()) { - injectIntoHead(`${key}.css`, ''); + injectIntoHead(key, ''); return; } const currentHash = crypto.createHash('sha1').update(content).digest('hex'); - injectIntoHead(`${key}.css`, { + + injectIntoHead(key, { type: 'CSS', - tag: ``, + tag: ``, content, }); }; diff --git a/apps/meteor/app/utils/client/getURL.ts b/apps/meteor/app/utils/client/getURL.ts index 91ef0989bd190..040b6dfa9dc28 100644 --- a/apps/meteor/app/utils/client/getURL.ts +++ b/apps/meteor/app/utils/client/getURL.ts @@ -1,13 +1,19 @@ import { settings } from '../../settings/client'; import { getURLWithoutSettings } from '../lib/getURL'; +import { Info } from '../rocketchat.info'; export const getURL = function ( path: string, // eslint-disable-next-line @typescript-eslint/naming-convention params: Record = {}, cloudDeepLinkUrl?: string, + cacheKey?: boolean, ): string { const cdnPrefix = settings.get('CDN_PREFIX') || ''; const siteUrl = settings.get('Site_Url') || ''; + if (cacheKey) { + path += `${path.includes('?') ? '&' : '?'}cacheKey=${Info.version}`; + } + return getURLWithoutSettings(path, params, cdnPrefix, siteUrl, cloudDeepLinkUrl); };