Skip to content

Commit

Permalink
refactor: Convert cors and invites to ts (#29076)
Browse files Browse the repository at this point in the history
  • Loading branch information
KevLehman authored May 2, 2023
1 parent cfe84fa commit e34e05b
Show file tree
Hide file tree
Showing 15 changed files with 96 additions and 54 deletions.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
import url from 'url';
import type http from 'http';

import { Meteor } from 'meteor/meteor';
import type { StaticFiles } from 'meteor/webapp';
import { WebApp, WebAppInternals } from 'meteor/webapp';
import _ from 'underscore';

import { settings } from '../../settings/server';
import { Logger } from '../../logger/server';

// Taken from 'connect' types
type NextFunction = (err?: any) => void;

const logger = new Logger('CORS');

settings.watch(
settings.watch<boolean>(
'Enable_CSP',
Meteor.bindEnvironment((enabled) => {
WebAppInternals.setInlineScriptsAllowed(!enabled);
}),
);

WebApp.rawConnectHandlers.use(function (req, res, next) {
WebApp.rawConnectHandlers.use(function (_req: http.IncomingMessage, res: http.ServerResponse, next: NextFunction) {
// XSS Protection for old browsers (IE)
res.setHeader('X-XSS-Protection', '1');

// X-Content-Type-Options header to prevent MIME Sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');

if (settings.get('Iframe_Restrict_Access')) {
res.setHeader('X-Frame-Options', settings.get('Iframe_X_Frame_Options'));
res.setHeader('X-Frame-Options', settings.get<string>('Iframe_X_Frame_Options'));
}

if (settings.get('Enable_CSP')) {
const cdn_prefixes = [settings.get('CDN_PREFIX'), settings.get('CDN_PREFIX_ALL') ? null : settings.get('CDN_JSCSS_PREFIX')]
if (settings.get<boolean>('Enable_CSP')) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const cdn_prefixes = [
settings.get<string>('CDN_PREFIX'),
settings.get<string>('CDN_PREFIX_ALL') ? null : settings.get<string>('CDN_JSCSS_PREFIX'),
]
.filter(Boolean)
.join(' ');

Expand All @@ -39,11 +48,11 @@ WebApp.rawConnectHandlers.use(function (req, res, next) {
.filter(Boolean)
.join(' ');
const external = [
settings.get('Accounts_OAuth_Apple') && 'https://appleid.cdn-apple.com',
settings.get('PiwikAnalytics_enabled') && settings.get('PiwikAnalytics_url'),
settings.get('GoogleAnalytics_enabled' && 'https://www.google-analytics.com'),
settings.get<boolean>('Accounts_OAuth_Apple') && 'https://appleid.cdn-apple.com',
settings.get<boolean>('PiwikAnalytics_enabled') && settings.get('PiwikAnalytics_url'),
settings.get<boolean>('GoogleAnalytics_enabled') && 'https://www.google-analytics.com',
...settings
.get('Extra_CSP_Domains')
.get<string>('Extra_CSP_Domains')
.split(/[ \n\,]/gim)
.filter((e) => Boolean(e.trim())),
]
Expand All @@ -69,7 +78,13 @@ WebApp.rawConnectHandlers.use(function (req, res, next) {

const _staticFilesMiddleware = WebAppInternals.staticFilesMiddleware;

WebAppInternals._staticFilesMiddleware = function (staticFiles, req, res, next) {
// @ts-expect-error - accessing internal property of webapp
WebAppInternals._staticFilesMiddleware = function (
staticFiles: StaticFiles,
req: http.IncomingMessage,
res: http.ServerResponse,
next: NextFunction,
) {
res.setHeader('Access-Control-Allow-Origin', '*');
return _staticFilesMiddleware(staticFiles, req, res, next);
};
Expand All @@ -90,15 +105,16 @@ WebApp.httpServer.addListener('request', function (req, res, ...args) {
return;
}

const remoteAddress = req.connection.remoteAddress || req.socket.remoteAddress;
const remoteAddress = req.connection.remoteAddress || req.socket.remoteAddress || '';
const localhostRegexp = /^\s*(127\.0\.0\.1|::1)\s*$/;
const localhostTest = function (x) {
const localhostTest = function (x: string) {
return localhostRegexp.test(x);
};

const isLocal =
localhostRegexp.test(remoteAddress) &&
(!req.headers['x-forwarded-for'] || _.all(req.headers['x-forwarded-for'].split(','), localhostTest));
(!req.headers['x-forwarded-for'] || _.all((req.headers['x-forwarded-for'] as string).split(','), localhostTest));
// @ts-expect-error - `pair` is valid, but doesnt exists on types
const isSsl = req.connection.pair || (req.headers['x-forwarded-proto'] && req.headers['x-forwarded-proto'].indexOf('https') !== -1);

logger.debug('req.url', req.url);
Expand All @@ -108,7 +124,7 @@ WebApp.httpServer.addListener('request', function (req, res, ...args) {
logger.debug('req.headers', req.headers);

if (!isLocal && !isSsl) {
let host = req.headers.host || url.parse(Meteor.absoluteUrl()).hostname;
let host = req.headers.host || url.parse(Meteor.absoluteUrl()).hostname || '';
host = host.replace(/:\d+$/, '');
res.writeHead(302, {
Location: `https://${host}${req.url}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor';
import { settings } from '../../settings/server';

Meteor.startup(function () {
settings.watch('Force_SSL', (value) => {
settings.watch<boolean>('Force_SSL', (value) => {
Meteor.absoluteUrl.defaultOptions.secure = Boolean(value);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { settings } from '../../../settings/server';
import { sendMessage } from '../../../lib/server';

class ErrorHandler {
reporting: boolean;

rid: string | null;

lastError: string | null;

constructor() {
this.reporting = false;
this.rid = null;
Expand All @@ -13,11 +19,14 @@ class ErrorHandler {
Meteor.startup(async () => {
await this.registerHandlers();

settings.watch('Log_Exceptions_to_Channel', async (value) => {
settings.watch<string>('Log_Exceptions_to_Channel', async (value) => {
this.rid = null;
const roomName = value.trim();
if (roomName) {
this.rid = await this.getRoomId(roomName);
const rid = await this.getRoomId(roomName);
if (rid) {
this.rid = rid;
}
}

if (this.rid) {
Expand All @@ -38,27 +47,28 @@ class ErrorHandler {
await this.trackError(error.message, error.stack);
});

// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const originalMeteorDebug = Meteor._debug;
Meteor._debug = function (message, stack, ...args) {
if (!self.reporting) {
return originalMeteorDebug.call(this, message, stack);
}
self.trackError(message, stack);
void self.trackError(message, stack);
return originalMeteorDebug.apply(this, [message, stack, ...args]);
};
}

async getRoomId(roomName) {
roomName = roomName.replace('#');
async getRoomId(roomName: string): Promise<string | undefined> {
roomName = roomName.replace('#', '');
const room = await Rooms.findOneByName(roomName, { projection: { _id: 1, t: 1 } });
if (!room || (room.t !== 'c' && room.t !== 'p')) {
return;
}
return room._id;
}

async trackError(message, stack) {
async trackError(message: string, stack?: string): Promise<void> {
if (!this.reporting || !this.rid || this.lastError === message) {
return;
}
Expand Down
1 change: 0 additions & 1 deletion apps/meteor/app/file-upload/server/lib/FileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,6 @@ export class FileUploadClass {
async deleteById(fileId: string) {
const file = await this.model.findOneById(fileId);

// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (!file) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { escapeRegExp } from '@rocket.chat/string-helpers';

const checkHighlightedWordsInUrls = (msg, urlRegex) => msg.match(urlRegex);
const checkHighlightedWordsInUrls = (msg: string, urlRegex: RegExp) => msg.match(urlRegex);

const removeHighlightedUrls = (msg, highlight, urlMatches) => {
const removeHighlightedUrls = (msg: string, highlight: string, urlMatches: string[]) => {
const highlightRegex = new RegExp(highlight, 'gmi');

return urlMatches.reduce((msg, match) => {
Expand All @@ -14,22 +14,22 @@ const removeHighlightedUrls = (msg, highlight, urlMatches) => {

const highlightTemplate = '$1<mark class="highlight-text">$2</mark>$3';

export const getRegexHighlight = (highlight) =>
export const getRegexHighlight = (highlight: string) =>
new RegExp(
`(^|\\b|[\\s\\n\\r\\t.,،'\\\"\\+!?:-])(${escapeRegExp(highlight)})($|\\b|[\\s\\n\\r\\t.,،'\\\"\\+!?:-])(?![^<]*>|[^<>]*<\\/)`,
'gmi',
);

export const getRegexHighlightUrl = (highlight) =>
export const getRegexHighlightUrl = (highlight: string) =>
new RegExp(
`https?:\/\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)(${escapeRegExp(
highlight,
)})\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)`,
'gmi',
);

export const highlightWords = (msg, highlights) =>
highlights.reduce((msg, { highlight, regex, urlRegex }) => {
export const highlightWords = (msg: string, highlights: { highlight: string; regex: RegExp; urlRegex: RegExp }[]) =>
highlights.reduce((msg: string, { highlight, regex, urlRegex }: { highlight: string; regex: RegExp; urlRegex: RegExp }) => {
const urlMatches = checkHighlightedWordsInUrls(msg, urlRegex);
if (!urlMatches) {
return msg.replace(regex, highlightTemplate);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { Meteor } from 'meteor/meteor';
import type { DeepWritable } from '@rocket.chat/core-typings';
import type { Filter } from 'mongodb';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';

export const mountIntegrationQueryBasedOnPermissions = async (userId) => {
export const mountIntegrationQueryBasedOnPermissions = async (userId: string) => {
if (!userId) {
throw new Meteor.Error('You must provide the userId to the "mountIntegrationQueryBasedOnPermissions" fucntion.');
throw new Meteor.Error('You must provide the userId to the "mountIntegrationQueryBasedOnPermissions" function.');
}
const canViewAllOutgoingIntegrations = await hasPermissionAsync(userId, 'manage-outgoing-integrations');
const canViewAllIncomingIntegrations = await hasPermissionAsync(userId, 'manage-incoming-integrations');
const canViewOnlyOwnOutgoingIntegrations = await hasPermissionAsync(userId, 'manage-own-outgoing-integrations');
const canViewOnlyOwnIncomingIntegrations = await hasPermissionAsync(userId, 'manage-own-incoming-integrations');

const query = {};
const query: DeepWritable<Filter<any>> = {};

if (canViewAllOutgoingIntegrations && canViewAllIncomingIntegrations) {
return query;
Expand All @@ -36,7 +38,7 @@ export const mountIntegrationQueryBasedOnPermissions = async (userId) => {
return query;
};

export const mountIntegrationHistoryQueryBasedOnPermissions = async (userId, integrationId) => {
export const mountIntegrationHistoryQueryBasedOnPermissions = async (userId: string, integrationId: string) => {
if (!userId) {
throw new Meteor.Error('You must provide the userId to the "mountIntegrationHistoryQueryBasedOnPermissions" fucntion.');
}
Expand All @@ -45,7 +47,7 @@ export const mountIntegrationHistoryQueryBasedOnPermissions = async (userId, int
}

const canViewOnlyOwnOutgoingIntegrations = await hasPermissionAsync(userId, 'manage-own-outgoing-integrations');
const canViewAllOutgoingIntegrations = await (userId, 'manage-outgoing-integrations');
const canViewAllOutgoingIntegrations = await hasPermissionAsync(userId, 'manage-outgoing-integrations');
if (!canViewAllOutgoingIntegrations && canViewOnlyOwnOutgoingIntegrations) {
return { 'integration._id': integrationId, 'integration._createdBy._id': userId };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ import { Meteor } from 'meteor/meteor';
import { Random } from '@rocket.chat/random';
import { Invites, Subscriptions, Rooms } from '@rocket.chat/models';
import { api } from '@rocket.chat/core-services';
import type { IInvite } from '@rocket.chat/core-typings';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { settings } from '../../../settings/server';
import { getURL } from '../../../utils/server/getURL';
import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator';
import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig';

function getInviteUrl(invite) {
function getInviteUrl(invite: Omit<IInvite, '_updatedAt'>) {
const { _id } = invite;

const useDirectLink = settings.get('Accounts_Registration_InviteUrlType') === 'direct';
const useDirectLink = settings.get<string>('Accounts_Registration_InviteUrlType') === 'direct';

return getURL(
`invite/${_id}`,
Expand All @@ -21,14 +22,14 @@ function getInviteUrl(invite) {
cloud: !useDirectLink,
cloud_route: 'invite',
},
settings.get('DeepLink_Url'),
settings.get<string>('DeepLink_Url'),
);
}

const possibleDays = [0, 1, 7, 15, 30];
const possibleUses = [0, 1, 5, 10, 25, 50, 100];

export const findOrCreateInvite = async (userId, invite) => {
export const findOrCreateInvite = async (userId: string, invite: Pick<IInvite, 'rid' | 'days' | 'maxUses'>) => {
if (!userId || !invite) {
return false;
}
Expand Down Expand Up @@ -97,14 +98,15 @@ export const findOrCreateInvite = async (userId, invite) => {
expires.setDate(expires.getDate() + days);
}

const createInvite = {
const createInvite: Omit<IInvite, '_updatedAt'> = {
_id,
days,
maxUses,
rid: invite.rid,
userId,
createdAt,
expires,
url: '',
uses: 0,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Invites } from '@rocket.chat/models';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';

export const listInvites = async (userId) => {
export const listInvites = async (userId: string) => {
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'listInvites' });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Meteor } from 'meteor/meteor';
import { Invites } from '@rocket.chat/models';
import type { IInvite } from '@rocket.chat/core-typings';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';

export const removeInvite = async (userId, invite) => {
export const removeInvite = async (userId: string, invite: Pick<IInvite, '_id'>) => {
if (!userId || !invite) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom';
import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator';
import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig';

export const useInviteToken = async (userId, token) => {
export const useInviteToken = async (userId: string, token: string) => {
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'The user is invalid', {
method: 'useInviteToken',
Expand All @@ -31,13 +31,19 @@ export const useInviteToken = async (userId, token) => {
}

const user = await Users.findOneById(userId);
if (!user) {
throw new Meteor.Error('error-invalid-user', 'The user is invalid', {
method: 'useInviteToken',
field: 'userId',
});
}
await Users.updateInviteToken(user._id, token);

const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, {
projection: { _id: 1 },
});
if (!subscription) {
await Invites.increaseUsageById(inviteData._id);
await Invites.increaseUsageById(inviteData._id, 1);
}

// If the user already has an username, then join the invite room,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Invites, Rooms } from '@rocket.chat/models';

export const validateInviteToken = async (token) => {
export const validateInviteToken = async (token: string) => {
if (!token || typeof token !== 'string') {
throw new Meteor.Error('error-invalid-token', 'The invite token is invalid.', {
method: 'validateInviteToken',
Expand All @@ -25,7 +25,7 @@ export const validateInviteToken = async (token) => {
});
}

if (inviteData.expires && inviteData.expires <= Date.now()) {
if (inviteData.expires && new Date(inviteData.expires).getTime() <= Date.now()) {
throw new Meteor.Error('error-invite-expired', 'The invite token has expired.', {
method: 'validateInviteToken',
field: 'expires',
Expand Down
Loading

0 comments on commit e34e05b

Please sign in to comment.