snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
157
apps/meteor/server/routes/avatar/utils.ts
Normal file
157
apps/meteor/server/routes/avatar/utils.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import type { ServerResponse } from 'node:http';
|
||||
|
||||
import { hashLoginToken } from '@rocket.chat/account-utils';
|
||||
import type { IIncomingMessage, IUpload } from '@rocket.chat/core-typings';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
import type { NextFunction } from 'connect';
|
||||
import { Cookies } from 'meteor/ostrio:cookies';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import sharp from 'sharp';
|
||||
import { throttle } from 'underscore';
|
||||
|
||||
import { FileUpload } from '../../lib/media/file-upload';
|
||||
import { getAvatarColor } from '../../lib/utils/lib/getAvatarColor';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
const FALLBACK_LAST_MODIFIED = 'Thu, 01 Jan 2015 00:00:00 GMT';
|
||||
|
||||
const cookie = new Cookies();
|
||||
|
||||
export const MAX_SVG_AVATAR_SIZE = 1024;
|
||||
export const MIN_SVG_AVATAR_SIZE = 16;
|
||||
const MAX_SVG_AVATAR_INITIALS = 3;
|
||||
|
||||
export const serveAvatarFile = (file: IUpload, req: IIncomingMessage, res: ServerResponse, next: NextFunction) => {
|
||||
res.setHeader('Content-Security-Policy', "default-src 'none'");
|
||||
const reqModifiedHeader = req.headers['if-modified-since'];
|
||||
|
||||
if (reqModifiedHeader && reqModifiedHeader === file.uploadedAt?.toUTCString()) {
|
||||
res.setHeader('Last-Modified', reqModifiedHeader);
|
||||
res.writeHead(304);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.uploadedAt) {
|
||||
res.setHeader('Last-Modified', file.uploadedAt?.toUTCString());
|
||||
}
|
||||
|
||||
if (file.type) {
|
||||
res.setHeader('Content-Type', file.type);
|
||||
}
|
||||
|
||||
if (file.size) {
|
||||
res.setHeader('Content-Length', file.size);
|
||||
}
|
||||
|
||||
return FileUpload.get(file, req, res, next);
|
||||
};
|
||||
|
||||
export const getAvatarSizeFromRequest = (req: IIncomingMessage) => {
|
||||
const requestSize = req.query.size && parseInt(req.query.size);
|
||||
if (!requestSize) {
|
||||
return;
|
||||
}
|
||||
return Math.min(Math.max(requestSize, MIN_SVG_AVATAR_SIZE), MAX_SVG_AVATAR_SIZE);
|
||||
};
|
||||
export const serveSvgAvatarInRequestedFormat = ({
|
||||
nameOrUsername,
|
||||
req,
|
||||
res,
|
||||
useAllInitials = false,
|
||||
}: {
|
||||
nameOrUsername: string;
|
||||
req: IIncomingMessage;
|
||||
res: ServerResponse;
|
||||
useAllInitials?: boolean;
|
||||
}) => {
|
||||
const size = getAvatarSizeFromRequest(req);
|
||||
const avatar = renderSVGLetters(nameOrUsername, size, useAllInitials);
|
||||
res.setHeader('Last-Modified', FALLBACK_LAST_MODIFIED);
|
||||
|
||||
const { format } = req.query;
|
||||
|
||||
if (['png', 'jpg', 'jpeg'].includes(format)) {
|
||||
res.setHeader('Content-Type', `image/${format}`);
|
||||
sharp(Buffer.from(avatar)).toFormat(format).pipe(res);
|
||||
return;
|
||||
}
|
||||
res.setHeader('Content-Type', 'image/svg+xml');
|
||||
|
||||
res.write(avatar);
|
||||
res.end();
|
||||
};
|
||||
|
||||
export const wasFallbackModified = (reqModifiedHeader?: string) => {
|
||||
if (!reqModifiedHeader || reqModifiedHeader !== FALLBACK_LAST_MODIFIED) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
async function isUserAuthenticated({ headers, query }: Pick<IIncomingMessage, 'headers' | 'query'>) {
|
||||
let { rc_uid, rc_token } = query;
|
||||
|
||||
if (!rc_uid && headers.cookie) {
|
||||
rc_uid = cookie.get('rc_uid', headers.cookie);
|
||||
rc_token = cookie.get('rc_token', headers.cookie);
|
||||
}
|
||||
|
||||
if (rc_uid == null || rc_token == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const userFound = await Users.findOneByIdAndLoginToken(rc_uid, hashLoginToken(rc_token), { projection: { _id: 1 } }); // TODO memoize find
|
||||
|
||||
return !!userFound;
|
||||
}
|
||||
|
||||
const warnUnauthenticatedAccess = throttle(() => {
|
||||
console.warn('The server detected an unauthenticated access to an user avatar. This type of request will soon be blocked by default.');
|
||||
}, 60000 * 30); // 30 minutes
|
||||
|
||||
export async function userCanAccessAvatar({ headers = {}, query = {} }: IIncomingMessage) {
|
||||
if (!settings.get('Accounts_AvatarBlockUnauthenticatedAccess')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isAuthenticated = await isUserAuthenticated({ headers, query });
|
||||
if (!isAuthenticated) {
|
||||
warnUnauthenticatedAccess();
|
||||
}
|
||||
|
||||
return isAuthenticated;
|
||||
}
|
||||
|
||||
const getFirstLetter = (name: string) => {
|
||||
const sanitizedName = sanitizeHtml(name);
|
||||
return sanitizedName.substring(0, 1).toUpperCase();
|
||||
};
|
||||
|
||||
const getInitials = (name: string) => name.split(' ').slice(0, MAX_SVG_AVATAR_INITIALS).map(getFirstLetter).join('');
|
||||
|
||||
export const renderSVGLetters = (name: string, viewSize = 200, useAllInitials = false) => {
|
||||
let color = '';
|
||||
let initials = '';
|
||||
|
||||
if (name === '?') {
|
||||
color = '#000';
|
||||
initials = name;
|
||||
} else {
|
||||
color = getAvatarColor(name);
|
||||
initials = !useAllInitials ? getFirstLetter(name) : getInitials(name);
|
||||
}
|
||||
|
||||
const reductionFactor = initials.length > 1 ? Math.pow(initials.length, 2) / 10 : 0;
|
||||
const fontSize = viewSize / (1.6 + reductionFactor);
|
||||
|
||||
return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${viewSize} ${viewSize}\">\n<rect width=\"100%\" height=\"100%\" fill=\"${color}\"/>\n<text x=\"50%\" y=\"50%\" dy=\"0.36em\" text-anchor=\"middle\" pointer-events=\"none\" fill=\"#ffffff\" font-family=\"'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif'\" font-size="${fontSize}">\n${initials}\n</text>\n</svg>`;
|
||||
};
|
||||
|
||||
const getCacheTime = (cacheTime: number) => cacheTime || settings.get('Accounts_AvatarCacheTime');
|
||||
|
||||
export function setCacheAndDispositionHeaders(req: IIncomingMessage, res: ServerResponse) {
|
||||
const cacheTime = getCacheTime(req.query.cacheTime);
|
||||
res.setHeader('Cache-Control', `public, max-age=${cacheTime}`);
|
||||
res.setHeader('Content-Disposition', 'inline');
|
||||
}
|
||||
Reference in New Issue
Block a user