n8n/packages/nodes-base/nodes/Webhook/utils.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

272 lines
7.5 KiB
TypeScript
Raw Normal View History

import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import type {
IWebhookFunctions,
INodeExecutionData,
IDataObject,
ICredentialDataDecryptedObject,
} from 'n8n-workflow';
import { WebhookAuthorizationError } from './error';
import basicAuth from 'basic-auth';
import jwt from 'jsonwebtoken';
import { formatPrivateKey } from '../../utils/utilities';
type WebhookParameters = {
httpMethod: string;
responseMode: string;
responseData: string;
responseCode?: number; //typeVersion <= 1.1
options?: {
responseData?: string;
responseCode?: {
values?: {
responseCode: number;
customCode?: number;
};
};
noResponseBody?: boolean;
};
};
export const getResponseCode = (parameters: WebhookParameters) => {
if (parameters.responseCode) {
return parameters.responseCode;
}
const responseCodeOptions = parameters.options;
if (responseCodeOptions?.responseCode?.values) {
const { responseCode, customCode } = responseCodeOptions.responseCode.values;
if (customCode) {
return customCode;
}
return responseCode;
}
return 200;
};
export const getResponseData = (parameters: WebhookParameters) => {
const { responseData, responseMode, options } = parameters;
if (responseData) return responseData;
if (responseMode === 'onReceived') {
const data = options?.responseData;
if (data) return data;
}
if (options?.noResponseBody) return 'noData';
return undefined;
};
export const configuredOutputs = (parameters: WebhookParameters) => {
const httpMethod = parameters.httpMethod as string | string[];
if (!Array.isArray(httpMethod))
return [
{
type: `${NodeConnectionType.Main}`,
displayName: httpMethod,
},
];
const outputs = httpMethod.map((method) => {
return {
type: `${NodeConnectionType.Main}`,
displayName: method,
};
});
return outputs;
};
export const setupOutputConnection = (
ctx: IWebhookFunctions,
method: string,
additionalData: {
jwtPayload?: IDataObject;
},
) => {
const httpMethod = ctx.getNodeParameter('httpMethod', []) as string[] | string;
let webhookUrl = ctx.getNodeWebhookUrl('default') as string;
const executionMode = ctx.getMode() === 'manual' ? 'test' : 'production';
if (executionMode === 'test') {
webhookUrl = webhookUrl.replace('/webhook/', '/webhook-test/');
}
// multi methods could be set in settings of node, so we need to check if it's an array
if (!Array.isArray(httpMethod)) {
return (outputData: INodeExecutionData): INodeExecutionData[][] => {
outputData.json.webhookUrl = webhookUrl;
outputData.json.executionMode = executionMode;
if (additionalData?.jwtPayload) {
outputData.json.jwtPayload = additionalData.jwtPayload;
}
return [[outputData]];
};
}
const outputIndex = httpMethod.indexOf(method.toUpperCase());
const outputs: INodeExecutionData[][] = httpMethod.map(() => []);
return (outputData: INodeExecutionData): INodeExecutionData[][] => {
outputData.json.webhookUrl = webhookUrl;
outputData.json.executionMode = executionMode;
if (additionalData?.jwtPayload) {
outputData.json.jwtPayload = additionalData.jwtPayload;
}
outputs[outputIndex] = [outputData];
return outputs;
};
};
export const isIpWhitelisted = (
whitelist: string | string[] | undefined,
ips: string[],
ip?: string,
) => {
if (whitelist === undefined || whitelist === '') {
return true;
}
if (!Array.isArray(whitelist)) {
whitelist = whitelist.split(',').map((entry) => entry.trim());
}
for (const address of whitelist) {
if (ip && ip.includes(address)) {
return true;
}
if (ips.some((entry) => entry.includes(address))) {
return true;
}
}
return false;
};
export const checkResponseModeConfiguration = (context: IWebhookFunctions) => {
const responseMode = context.getNodeParameter('responseMode', 'onReceived') as string;
const connectedNodes = context.getChildNodes(context.getNode().name);
const isRespondToWebhookConnected = connectedNodes.some(
(node) => node.type === 'n8n-nodes-base.respondToWebhook',
);
if (!isRespondToWebhookConnected && responseMode === 'responseNode') {
throw new NodeOperationError(
context.getNode(),
new Error('No Respond to Webhook node found in the workflow'),
{
description:
'Insert a Respond to Webhook node to your workflow to respond to the webhook or choose another option for the “Respond” parameter',
},
);
}
if (isRespondToWebhookConnected && responseMode !== 'responseNode') {
throw new NodeOperationError(
context.getNode(),
new Error('Webhook node not correctly configured'),
{
description:
'Set the “Respond” parameter to “Using Respond to Webhook Node” or remove the Respond to Webhook node',
},
);
}
};
export async function validateWebhookAuthentication(
ctx: IWebhookFunctions,
authPropertyName: string,
) {
const authentication = ctx.getNodeParameter(authPropertyName) as string;
if (authentication === 'none') return;
const req = ctx.getRequestObject();
const headers = ctx.getHeaderData();
if (authentication === 'basicAuth') {
// Basic authorization is needed to call webhook
let expectedAuth: ICredentialDataDecryptedObject | undefined;
try {
expectedAuth = await ctx.getCredentials('httpBasicAuth');
} catch {}
if (expectedAuth === undefined || !expectedAuth.user || !expectedAuth.password) {
// Data is not defined on node so can not authenticate
throw new WebhookAuthorizationError(500, 'No authentication data defined on node!');
}
const providedAuth = basicAuth(req);
// Authorization data is missing
if (!providedAuth) throw new WebhookAuthorizationError(401);
if (providedAuth.name !== expectedAuth.user || providedAuth.pass !== expectedAuth.password) {
// Provided authentication data is wrong
throw new WebhookAuthorizationError(403);
}
} else if (authentication === 'headerAuth') {
// Special header with value is needed to call webhook
let expectedAuth: ICredentialDataDecryptedObject | undefined;
try {
expectedAuth = await ctx.getCredentials('httpHeaderAuth');
} catch {}
if (expectedAuth === undefined || !expectedAuth.name || !expectedAuth.value) {
// Data is not defined on node so can not authenticate
throw new WebhookAuthorizationError(500, 'No authentication data defined on node!');
}
const headerName = (expectedAuth.name as string).toLowerCase();
const expectedValue = expectedAuth.value as string;
if (
!headers.hasOwnProperty(headerName) ||
(headers as IDataObject)[headerName] !== expectedValue
) {
// Provided authentication data is wrong
throw new WebhookAuthorizationError(403);
}
} else if (authentication === 'jwtAuth') {
let expectedAuth;
try {
expectedAuth = (await ctx.getCredentials('jwtAuth')) as {
keyType: 'passphrase' | 'pemKey';
publicKey: string;
secret: string;
algorithm: jwt.Algorithm;
};
} catch {}
if (expectedAuth === undefined) {
// Data is not defined on node so can not authenticate
throw new WebhookAuthorizationError(500, 'No authentication data defined on node!');
}
const authHeader = req.headers.authorization;
const token = authHeader?.split(' ')[1];
if (!token) {
throw new WebhookAuthorizationError(401, 'No token provided');
}
let secretOrPublicKey;
if (expectedAuth.keyType === 'passphrase') {
secretOrPublicKey = expectedAuth.secret;
} else {
secretOrPublicKey = formatPrivateKey(expectedAuth.publicKey, true);
}
try {
return jwt.verify(token, secretOrPublicKey, {
algorithms: [expectedAuth.algorithm],
}) as IDataObject;
} catch (error) {
throw new WebhookAuthorizationError(403, error.message);
}
}
}