Files
upstream-mirror 30810eecd7
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 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

158 lines
5.0 KiB
TypeScript

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');
}