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
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (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
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
This commit is contained in:
554
apps/meteor/server/api/webhooks.ts
Normal file
554
apps/meteor/server/api/webhooks.ts
Normal file
@@ -0,0 +1,554 @@
|
||||
import type { IIncomingIntegration, IIntegration, IOutgoingIntegration, IUser, RequiredField } from '@rocket.chat/core-typings';
|
||||
import { Integrations, Users } from '@rocket.chat/models';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import { isIntegrationsHooksAddSchema, isIntegrationsHooksRemoveSchema } from '@rocket.chat/rest-typings';
|
||||
import type express from 'express';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import type { RateLimiterOptionsToCheck } from 'meteor/rate-limit';
|
||||
import { WebApp } from 'meteor/webapp';
|
||||
import _ from 'underscore';
|
||||
|
||||
import { APIClass } from './ApiClass';
|
||||
import type { RateLimiterOptions } from './api';
|
||||
import { API, defaultRateLimiterOptions } from './api';
|
||||
import type { FailureResult, GenericRouteExecutionContext, SuccessResult, UnavailableResult } from './definition';
|
||||
import type { APIActionContext } from './router';
|
||||
import { isPlainObject } from '../../lib/utils/isPlainObject';
|
||||
import { hasPermissionAsync } from '../lib/authorization/hasPermission';
|
||||
import { IsolatedVMScriptEngine } from '../lib/integrations/lib/isolated-vm/isolated-vm';
|
||||
import { metrics } from '../lib/metrics';
|
||||
import { settings } from '../settings';
|
||||
import { loggerMiddleware } from './v1/middlewares/logger';
|
||||
import { metricsMiddleware } from './v1/middlewares/metrics';
|
||||
import { tracerSpanMiddleware } from './v1/middlewares/tracer';
|
||||
import { incomingLogger, integrationLogger } from '../lib/integrations/logger';
|
||||
import type { WebhookResponseItem } from '../lib/messages/processWebhookMessage';
|
||||
import { processWebhookMessage } from '../lib/messages/processWebhookMessage';
|
||||
import { addOutgoingIntegration } from '../meteor-methods/integrations/outgoing/addOutgoingIntegration';
|
||||
import { deleteOutgoingIntegration } from '../meteor-methods/integrations/outgoing/deleteOutgoingIntegration';
|
||||
|
||||
const ivmEngine = new IsolatedVMScriptEngine(true);
|
||||
|
||||
function getEngine(_integration: IIntegration): IsolatedVMScriptEngine<true> {
|
||||
return ivmEngine;
|
||||
}
|
||||
|
||||
type IntegrationOptions = {
|
||||
event: string;
|
||||
name: string;
|
||||
target_url: string;
|
||||
data?: {
|
||||
channel_name?: string;
|
||||
trigger_words?: string[];
|
||||
username?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type IntegrationThis = GenericRouteExecutionContext & {
|
||||
request: Request & {
|
||||
integration: IIncomingIntegration;
|
||||
};
|
||||
user: RequiredField<IUser, 'username'>;
|
||||
};
|
||||
|
||||
async function createIntegration(options: IntegrationOptions, user: IUser): Promise<IOutgoingIntegration | undefined> {
|
||||
incomingLogger.info({ msg: 'Add integration', integration: options.name });
|
||||
incomingLogger.debug({ options });
|
||||
|
||||
switch (options.event) {
|
||||
case 'newMessageOnChannel':
|
||||
if (options.data == null) {
|
||||
options.data = {};
|
||||
}
|
||||
if (options.data.channel_name?.indexOf('#') === -1) {
|
||||
options.data.channel_name = `#${options.data.channel_name}`;
|
||||
}
|
||||
return addOutgoingIntegration(user._id, {
|
||||
username: 'rocket.cat',
|
||||
urls: [options.target_url],
|
||||
name: options.name,
|
||||
channel: options.data.channel_name,
|
||||
triggerWords: options.data.trigger_words,
|
||||
type: 'webhook-outgoing',
|
||||
event: 'sendMessage',
|
||||
token: Random.id(24),
|
||||
scriptEnabled: false,
|
||||
script: '',
|
||||
enabled: true,
|
||||
_id: Random.id(),
|
||||
_updatedAt: new Date(),
|
||||
});
|
||||
case 'newMessageToUser':
|
||||
if (options.data?.username?.indexOf('@') === -1) {
|
||||
options.data.username = `@${options.data.username}`;
|
||||
}
|
||||
if (!options.data?.username) {
|
||||
throw new Error('username-required');
|
||||
}
|
||||
|
||||
return addOutgoingIntegration(user._id, {
|
||||
username: 'rocket.cat',
|
||||
urls: [options.target_url],
|
||||
name: options.name,
|
||||
channel: options.data.username,
|
||||
triggerWords: options.data.trigger_words,
|
||||
_id: '',
|
||||
type: 'webhook-outgoing',
|
||||
token: '',
|
||||
scriptEnabled: false,
|
||||
script: '',
|
||||
enabled: false,
|
||||
_updatedAt: new Date(),
|
||||
event: 'sendMessage',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function removeIntegration(options: { target_url: string }, user: IUser): Promise<SuccessResult<void> | FailureResult<string>> {
|
||||
incomingLogger.info('Remove integration');
|
||||
incomingLogger.debug({ options });
|
||||
|
||||
const integrationToRemove = await Integrations.findOneByUrl(options.target_url);
|
||||
if (!integrationToRemove) {
|
||||
return API.v1.failure('integration-not-found');
|
||||
}
|
||||
|
||||
await deleteOutgoingIntegration(integrationToRemove._id, user._id);
|
||||
|
||||
return API.v1.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Slack/GitHub-style webhooks send JSON wrapped in a `payload` field
|
||||
* with Content-Type: application/x-www-form-urlencoded (e.g. `payload={"text":"hello"}`).
|
||||
* This function unwraps it so integrations receive the parsed JSON directly.
|
||||
*/
|
||||
function getBodyParams(bodyParams: unknown, request: Request): Record<string, unknown> {
|
||||
if (!isPlainObject(bodyParams)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (
|
||||
request.headers.get('content-type')?.startsWith('application/x-www-form-urlencoded') &&
|
||||
Object.keys(bodyParams).length === 1 &&
|
||||
typeof bodyParams.payload === 'string'
|
||||
) {
|
||||
try {
|
||||
const parsed = JSON.parse(bodyParams.payload);
|
||||
|
||||
// Valid JSON must be an object, not an array or primitive
|
||||
if (!isPlainObject(parsed)) {
|
||||
throw new Error('Integration payload must be a JSON object, not an array or primitive');
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (err) {
|
||||
// Invalid JSON -> return original bodyParams (backward compatibility)
|
||||
if (err instanceof SyntaxError) {
|
||||
return bodyParams;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return bodyParams;
|
||||
}
|
||||
|
||||
async function executeIntegrationRest(
|
||||
this: IntegrationThis,
|
||||
): Promise<
|
||||
| SuccessResult<Record<string, string> | { responses: WebhookResponseItem[] } | undefined | void>
|
||||
| FailureResult<string>
|
||||
| FailureResult<{ responses: WebhookResponseItem[] }>
|
||||
| UnavailableResult<string>
|
||||
> {
|
||||
incomingLogger.info({ msg: 'Post integration:', integration: this.request.integration.name });
|
||||
incomingLogger.debug({ urlParams: this.urlParams, bodyParams: this.bodyParams });
|
||||
|
||||
if (this.request.integration.enabled !== true) {
|
||||
return API.v1.unavailable('Service Unavailable');
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(this.user, 'message-impersonate'))) {
|
||||
incomingLogger.error({
|
||||
msg: 'Error trying to execute integration',
|
||||
integration: this.request.integration.name,
|
||||
error: 'The assigned user from the integration must have the message-impersonate permission.',
|
||||
});
|
||||
return API.v1.failure('error-user-lacks-message-impersonate-permission');
|
||||
}
|
||||
|
||||
const defaultValues = {
|
||||
channel: this.request.integration.channel,
|
||||
alias: this.request.integration.alias || '',
|
||||
avatar: this.request.integration.avatar || '',
|
||||
emoji: this.request.integration.emoji || '',
|
||||
};
|
||||
|
||||
const scriptEngine = getEngine(this.request.integration);
|
||||
|
||||
let bodyParams: Record<string, unknown>;
|
||||
try {
|
||||
bodyParams = getBodyParams(this.bodyParams, this.request);
|
||||
} catch (err) {
|
||||
return API.v1.failure(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
|
||||
const separateResponse = bodyParams.separateResponse === true;
|
||||
let scriptResponse: Record<string, any> | undefined;
|
||||
|
||||
if (scriptEngine.integrationHasValidScript(this.request.integration) && this.request.body) {
|
||||
const buffers = [];
|
||||
const reader = this.request.body.getReader();
|
||||
for (let result = await reader.read(); !result.done; result = await reader.read()) {
|
||||
buffers.push(result.value);
|
||||
}
|
||||
const contentRaw = Buffer.concat(buffers).toString('utf8');
|
||||
const protocol = `${this.request.headers.get('x-forwarded-proto')}:` || 'http:';
|
||||
const url = new URL(this.request.url, `${protocol}//${this.request.headers.get('host')}`);
|
||||
const query = isPlainObject(this.queryParams) ? this.queryParams : {};
|
||||
|
||||
const request = {
|
||||
url: {
|
||||
query,
|
||||
hash: url.hash,
|
||||
search: url.search,
|
||||
pathname: url.pathname,
|
||||
path: this.request.url,
|
||||
},
|
||||
url_raw: this.request.url,
|
||||
url_params: this.urlParams,
|
||||
content: bodyParams,
|
||||
content_raw: contentRaw,
|
||||
headers: Object.fromEntries(this.request.headers.entries()),
|
||||
body: bodyParams,
|
||||
user: {
|
||||
_id: this.user._id,
|
||||
name: this.user.name || '',
|
||||
username: this.user.username,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await scriptEngine.processIncomingRequest({
|
||||
integration: this.request.integration,
|
||||
request,
|
||||
});
|
||||
|
||||
try {
|
||||
if (!result) {
|
||||
incomingLogger.debug({
|
||||
msg: 'Process Incoming Request result of Trigger has no data',
|
||||
integration: this.request.integration.name,
|
||||
});
|
||||
return API.v1.success();
|
||||
}
|
||||
if (result?.error) {
|
||||
return API.v1.failure(result.error);
|
||||
}
|
||||
|
||||
bodyParams = result?.content;
|
||||
|
||||
if (!('separateResponse' in bodyParams)) {
|
||||
bodyParams.separateResponse = separateResponse;
|
||||
}
|
||||
|
||||
scriptResponse = result.response;
|
||||
if (result.user) {
|
||||
this.user = result.user;
|
||||
}
|
||||
|
||||
incomingLogger.debug({
|
||||
msg: 'Process Incoming Request result of Trigger',
|
||||
integration: this.request.integration.name,
|
||||
result: bodyParams,
|
||||
});
|
||||
} catch (err) {
|
||||
incomingLogger.error({
|
||||
msg: 'Error running Script in Trigger',
|
||||
integration: this.request.integration.name,
|
||||
script: this.request.integration.scriptCompiled,
|
||||
err,
|
||||
});
|
||||
return API.v1.failure('error-running-script');
|
||||
}
|
||||
}
|
||||
|
||||
if (!bodyParams || (_.isEmpty(bodyParams) && !this.request.integration.scriptEnabled)) {
|
||||
return API.v1.success();
|
||||
}
|
||||
|
||||
if ((bodyParams.channel || bodyParams.roomId) && !this.request.integration.overrideDestinationChannelEnabled) {
|
||||
return API.v1.failure('overriding destination channel is disabled for this integration');
|
||||
}
|
||||
|
||||
bodyParams.bot = { i: this.request.integration._id };
|
||||
|
||||
try {
|
||||
const messageResponse = await processWebhookMessage(bodyParams, this.user, defaultValues);
|
||||
if (_.isEmpty(messageResponse)) {
|
||||
return API.v1.failure('unknown-error');
|
||||
}
|
||||
|
||||
if (scriptResponse) {
|
||||
incomingLogger.debug({ msg: 'response', response: scriptResponse });
|
||||
return API.v1.success(scriptResponse);
|
||||
}
|
||||
if (bodyParams.separateResponse) {
|
||||
const allFailed = messageResponse.every((response) => 'error' in response && response.error);
|
||||
if (allFailed) {
|
||||
return API.v1.failure({ responses: messageResponse });
|
||||
}
|
||||
return API.v1.success({ responses: messageResponse });
|
||||
}
|
||||
return API.v1.success();
|
||||
} catch (err: any) {
|
||||
incomingLogger.error({ msg: 'Error processing webhook message', err });
|
||||
return API.v1.failure(err?.error || err?.message || 'Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
type IntegrationSampleBody = {
|
||||
token: string;
|
||||
channel_id: string;
|
||||
channel_name: string;
|
||||
timestamp: Date;
|
||||
user_id: string;
|
||||
user_name: string;
|
||||
text: string;
|
||||
trigger_word: string;
|
||||
};
|
||||
|
||||
function integrationSampleRest(): { statusCode: number; body: IntegrationSampleBody[] } {
|
||||
incomingLogger.info('Sample Integration');
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: [
|
||||
{
|
||||
token: Random.id(24),
|
||||
channel_id: Random.id(),
|
||||
channel_name: 'general',
|
||||
timestamp: new Date(),
|
||||
user_id: Random.id(),
|
||||
user_name: 'rocket.cat',
|
||||
text: 'Sample text 1',
|
||||
trigger_word: 'Sample',
|
||||
},
|
||||
{
|
||||
token: Random.id(24),
|
||||
channel_id: Random.id(),
|
||||
channel_name: 'general',
|
||||
timestamp: new Date(),
|
||||
user_id: Random.id(),
|
||||
user_name: 'rocket.cat',
|
||||
text: 'Sample text 2',
|
||||
trigger_word: 'Sample',
|
||||
},
|
||||
{
|
||||
token: Random.id(24),
|
||||
channel_id: Random.id(),
|
||||
channel_name: 'general',
|
||||
timestamp: new Date(),
|
||||
user_id: Random.id(),
|
||||
user_name: 'rocket.cat',
|
||||
text: 'Sample text 3',
|
||||
trigger_word: 'Sample',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function integrationInfoRest(): { statusCode: number; body: { success: boolean } } {
|
||||
incomingLogger.info('Info integration');
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
success: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class WebHookAPI extends APIClass<'/hooks'> {
|
||||
override async authenticatedRoute(routeContext: APIActionContext): Promise<IUser | null> {
|
||||
const { integrationId, token } = routeContext.urlParams;
|
||||
const integration = await Integrations.findOneByIdAndToken<IIncomingIntegration>(integrationId, decodeURIComponent(token));
|
||||
|
||||
if (!integration) {
|
||||
incomingLogger.info({ msg: 'Invalid integration id or token', integrationId, token });
|
||||
|
||||
throw new Error('Invalid integration id or token provided.');
|
||||
}
|
||||
|
||||
routeContext.request.headers.set('x-auth-token', token);
|
||||
|
||||
const req = routeContext.request as Request & { integration?: IIncomingIntegration };
|
||||
req.integration = integration;
|
||||
|
||||
return Users.findOneById(req.integration.userId);
|
||||
}
|
||||
|
||||
override shouldAddRateLimitToRoute(options: { rateLimiterOptions?: RateLimiterOptions | boolean }): boolean {
|
||||
const { rateLimiterOptions } = options;
|
||||
return (
|
||||
(typeof rateLimiterOptions === 'object' || rateLimiterOptions === undefined) &&
|
||||
!process.env.TEST_MODE &&
|
||||
Boolean(defaultRateLimiterOptions.numRequestsAllowed && defaultRateLimiterOptions.intervalTimeInMS)
|
||||
);
|
||||
}
|
||||
|
||||
override async shouldVerifyRateLimit(): Promise<boolean> {
|
||||
return (
|
||||
settings.get('API_Enable_Rate_Limiter') === true &&
|
||||
(process.env.NODE_ENV !== 'development' || settings.get('API_Enable_Rate_Limiter_Dev') === true)
|
||||
);
|
||||
}
|
||||
|
||||
override async enforceRateLimit(
|
||||
objectForRateLimitMatch: RateLimiterOptionsToCheck,
|
||||
request: Request,
|
||||
response: Response,
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
const { method, url } = request;
|
||||
const route = url.replace(`/${this.apiPath}`, '');
|
||||
const nameRoute = this.getFullRouteName(route, method.toLowerCase());
|
||||
if (!this.getRateLimiter(nameRoute)) {
|
||||
this.addRateLimiterRuleForRoutes({
|
||||
routes: [route],
|
||||
rateLimiterOptions: defaultRateLimiterOptions,
|
||||
endpoints: {
|
||||
post: 'executeIntegrationRest',
|
||||
get: 'executeIntegrationRest',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const integrationForRateLimitMatch = objectForRateLimitMatch;
|
||||
integrationForRateLimitMatch.route = nameRoute;
|
||||
|
||||
await super.enforceRateLimit(integrationForRateLimitMatch, request, response, userId);
|
||||
}
|
||||
}
|
||||
|
||||
const Api = new WebHookAPI({
|
||||
enableCors: true,
|
||||
apiPath: 'hooks/',
|
||||
useDefaultAuth: false,
|
||||
prettyJson: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
Api.router
|
||||
.use(
|
||||
metricsMiddleware({
|
||||
basePathRegex: new RegExp(/^\/hooks\//),
|
||||
api: Api,
|
||||
settings,
|
||||
endpointTimeSummary: metrics.rocketchatRestApi,
|
||||
endpointTimeHistogram: metrics.rocketchatRestApiSeconds,
|
||||
responseSizeHistogram: metrics.rocketchatRestApiResponseSizeBytes,
|
||||
activeRequestsGauge: metrics.rocketchatRestApiActiveRequests,
|
||||
}),
|
||||
)
|
||||
.use(tracerSpanMiddleware)
|
||||
.use(loggerMiddleware(integrationLogger));
|
||||
|
||||
Api.addRoute(
|
||||
':integrationId/:userId/:token',
|
||||
{ authRequired: true },
|
||||
{
|
||||
post: executeIntegrationRest,
|
||||
get: executeIntegrationRest,
|
||||
},
|
||||
);
|
||||
|
||||
Api.addRoute(
|
||||
':integrationId/:token',
|
||||
{ authRequired: true },
|
||||
{
|
||||
post: executeIntegrationRest,
|
||||
get: executeIntegrationRest,
|
||||
},
|
||||
);
|
||||
|
||||
Api.addRoute(
|
||||
'sample/:integrationId/:userId/:token',
|
||||
{ authRequired: true },
|
||||
{
|
||||
get: integrationSampleRest,
|
||||
},
|
||||
);
|
||||
|
||||
Api.addRoute(
|
||||
'sample/:integrationId/:token',
|
||||
{ authRequired: true },
|
||||
{
|
||||
get: integrationSampleRest,
|
||||
},
|
||||
);
|
||||
|
||||
Api.addRoute(
|
||||
'info/:integrationId/:userId/:token',
|
||||
{ authRequired: true },
|
||||
{
|
||||
get: integrationInfoRest,
|
||||
},
|
||||
);
|
||||
|
||||
Api.addRoute(
|
||||
'info/:integrationId/:token',
|
||||
{ authRequired: true },
|
||||
{
|
||||
get: integrationInfoRest,
|
||||
},
|
||||
);
|
||||
|
||||
Api.addRoute(
|
||||
'add/:integrationId/:userId/:token',
|
||||
{ authRequired: true, validateParams: isIntegrationsHooksAddSchema },
|
||||
{
|
||||
async post() {
|
||||
const result = await createIntegration(this.bodyParams, this.user);
|
||||
|
||||
return API.v1.success(result || {});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Api.addRoute(
|
||||
'add/:integrationId/:token',
|
||||
{ authRequired: true, validateParams: isIntegrationsHooksAddSchema },
|
||||
{
|
||||
async post() {
|
||||
const result = await createIntegration(this.bodyParams, this.user);
|
||||
|
||||
return API.v1.success(result || {});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Api.addRoute(
|
||||
'remove/:integrationId/:userId/:token',
|
||||
{ authRequired: true, validateParams: isIntegrationsHooksRemoveSchema },
|
||||
{
|
||||
async post() {
|
||||
const result = await removeIntegration(this.bodyParams, this.user);
|
||||
|
||||
return API.v1.success(result || {});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Api.addRoute(
|
||||
'remove/:integrationId/:token',
|
||||
{ authRequired: true, validateParams: isIntegrationsHooksRemoveSchema },
|
||||
{
|
||||
async post() {
|
||||
const result = await removeIntegration(this.bodyParams, this.user);
|
||||
|
||||
return API.v1.success(result || {});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Meteor.startup(() => {
|
||||
(WebApp.rawConnectHandlers as unknown as ReturnType<typeof express>).use(Api.router.router);
|
||||
});
|
||||
Reference in New Issue
Block a user