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

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

404 lines
12 KiB
TypeScript
Raw Normal View History

/* eslint-disable n8n-nodes-base/node-execute-block-wrong-error-thrown */
import { pipeline } from 'stream/promises';
import { createWriteStream } from 'fs';
import { stat } from 'fs/promises';
import type {
IWebhookFunctions,
ICredentialDataDecryptedObject,
2019-06-23 03:35:23 -07:00
IDataObject,
2019-12-21 17:03:24 -08:00
INodeExecutionData,
INodeTypeDescription,
IWebhookResponseData,
MultiPartFormData,
INodeProperties,
2019-06-23 03:35:23 -07:00
} from 'n8n-workflow';
import { BINARY_ENCODING, NodeOperationError, Node } from 'n8n-workflow';
2019-06-23 03:35:23 -07:00
import { v4 as uuid } from 'uuid';
import basicAuth from 'basic-auth';
import isbot from 'isbot';
import { file as tmpFile } from 'tmp-promise';
import jwt from 'jsonwebtoken';
import {
authenticationProperty,
credentialsProperty,
defaultWebhookDescription,
httpMethodsProperty,
optionsProperty,
responseBinaryPropertyNameProperty,
responseCodeOption,
responseCodeProperty,
responseDataProperty,
responseModeProperty,
} from './description';
import { WebhookAuthorizationError } from './error';
import {
checkResponseModeConfiguration,
configuredOutputs,
isIpWhitelisted,
setupOutputConnection,
} from './utils';
import { formatPrivateKey } from '../../utils/utilities';
export class Webhook extends Node {
authPropertyName = 'authentication';
2019-06-23 03:35:23 -07:00
description: INodeTypeDescription = {
displayName: 'Webhook',
2021-06-18 14:48:38 -07:00
icon: 'file:webhook.svg',
2019-06-23 03:35:23 -07:00
name: 'webhook',
group: ['trigger'],
version: [1, 1.1, 2],
description: 'Starts the workflow when a webhook is called',
eventTriggerDescription: 'Waiting for you to call the Test URL',
:sparkles: Improve workflow activation (#2692) * feat: activator disabled based on thiggers * feat: tooltip over inactive switch * feat: message for trigger types * feat: deactivate on save if trigger is removed * chore: refactor executions modal * feat: calculate service name if possible * feat: alert on activation * chore: fix linting * feat: always enable activator when active * fix: adjust the alert * feat: take disabled state into account * feat: automatically save on activation * feat: rely on nodes name and edit messages * feat: isolate state for each activator instance * feat: create activation modal component * feat: activationModal checkbox and trigger message * feat: add activation messages to node config * chore: style activation modal * chore: style fixes * feat: refactor disabled state * chore: refactor modal * chore: refactor modal * chore: tidy the node config * chore: refactor and styling tweaks * chore: minor fixes * fix: check webhooks from ui nodes * chore: remove saving prompt * chore: explicit current workflow evaluation * feat: add settings link to activation modal * fix: immediately load executions on render * feat: exclude error trigger from trigger nodes * chore: add i18n keys * fix: check localstorage more strictly * fix: handle refresh in execution list * remove unnessary event * remove comment * fix closing executions modal bugs * update closing * update translation key * fix translation keys * fix modal closing * fix closing * fix drawer closing * close all modals when opening executions * update key * close all modals when opening workflow or new page * delete unnessary comment * clean up import * clean up unnessary initial data * clean up activator impl * rewrite * fix open modal bug * simply remove error * refactor activation logic * fix i18n and such * remove changes * revert saving changes * Revert "revert saving changes" 25c29d10553ebcc11939ff29938e8a5ac6b3ffae * add translation * fix new workflows saving * clean up modal impl * clean up impl * refactor common code out * remove active changes from saving * refactor differently * revert unnessary change * set dirty false * fix i18n bug * avoid opening two modals * fix tooltips * add comment * address other comments * address comments Co-authored-by: saintsebastian <tilitidam@gmail.com>
2022-01-21 09:00:00 -08:00
activationMessage: 'You can now make calls to your production webhook URL.',
2019-06-23 03:35:23 -07:00
defaults: {
name: 'Webhook',
},
supportsCORS: true,
feat(editor): Improve trigger panel (#3509) * add panel * add workflow activation hints * support service trigger nodes * update polling state * support more views * update when trigger panel shows * update start/error nodes * add cron/interval info box * clean up start node * fix up webhook views * remove console log * add listening state * clean up loading state * update loading state * fix up animation * update views * add executions hint * update views * update accordian styling * address more issues * disable execute button if issues * disable if it has issues * add stop waiting button * can activate workflow when dsiabled * update el * fix has issues * add margin bttm * update views * close ndv * add shake * update copies * add error when polling node is missing one * update package lock * hide switch * hide binary data that's missing keys * hide main bar if ndv is open * remove waiting to execute * change accordion bg color * capitalize text * disable trigger panel in read only views * remove webhook title * update webhook desc * update component * update webhook executions note * update header * update webhook url * update exec help * bring back waiting to execute for non triggers * add transition fade * set shake * add helpful tooltip * add nonactive text * add inactive text * hide trigger panel by default * remove unused import * update pulse animation * handle empty values for options * update text * add flag for mock manual executions * add overrides * Add overrides * update check * update package lock; show button for others * hide more info * update other core nodes * update service name * remove panel from nodes * update panel * last tweaks * add telemetry event * add telemetry; address issues * address feedback * address feedback * address feedback * fix previous * fix previous * fix bug * fix bug with webhookbased * add extra break * update telemetry * update telemetry * add telemetry req * add info icon story; use icon component * clean css; en.json * clean en.json * rename key * add key * sort keys alpha * handle activation if active + add previous state to telemetry * stop activation if active * remove unnessary tracking * remove unused import * remove unused * remove unnessary flag * rewrite in ts * move pulse to design system * clean up * clean up * clean up * disable tslint check * disable tslint check
2022-06-20 12:39:24 -07:00
triggerPanel: {
header: '',
executionsHelp: {
inactive:
'Webhooks have two modes: test and production. <br /> <br /> <b>Use test mode while you build your workflow</b>. Click the \'listen\' button, then make a request to the test URL. The executions will show up in the editor.<br /> <br /> <b>Use production mode to run your workflow automatically</b>. <a data-key="activate">Activate</a> the workflow, then make requests to the production URL. These executions will show up in the executions list, but not in the editor.',
active:
'Webhooks have two modes: test and production. <br /> <br /> <b>Use test mode while you build your workflow</b>. Click the \'listen\' button, then make a request to the test URL. The executions will show up in the editor.<br /> <br /> <b>Use production mode to run your workflow automatically</b>. Since the workflow is activated, you can make requests to the production URL. These executions will show up in the <a data-key="executions">executions list</a>, but not in the editor.',
feat(editor): Improve trigger panel (#3509) * add panel * add workflow activation hints * support service trigger nodes * update polling state * support more views * update when trigger panel shows * update start/error nodes * add cron/interval info box * clean up start node * fix up webhook views * remove console log * add listening state * clean up loading state * update loading state * fix up animation * update views * add executions hint * update views * update accordian styling * address more issues * disable execute button if issues * disable if it has issues * add stop waiting button * can activate workflow when dsiabled * update el * fix has issues * add margin bttm * update views * close ndv * add shake * update copies * add error when polling node is missing one * update package lock * hide switch * hide binary data that's missing keys * hide main bar if ndv is open * remove waiting to execute * change accordion bg color * capitalize text * disable trigger panel in read only views * remove webhook title * update webhook desc * update component * update webhook executions note * update header * update webhook url * update exec help * bring back waiting to execute for non triggers * add transition fade * set shake * add helpful tooltip * add nonactive text * add inactive text * hide trigger panel by default * remove unused import * update pulse animation * handle empty values for options * update text * add flag for mock manual executions * add overrides * Add overrides * update check * update package lock; show button for others * hide more info * update other core nodes * update service name * remove panel from nodes * update panel * last tweaks * add telemetry event * add telemetry; address issues * address feedback * address feedback * address feedback * fix previous * fix previous * fix bug * fix bug with webhookbased * add extra break * update telemetry * update telemetry * add telemetry req * add info icon story; use icon component * clean css; en.json * clean en.json * rename key * add key * sort keys alpha * handle activation if active + add previous state to telemetry * stop activation if active * remove unnessary tracking * remove unused import * remove unused * remove unnessary flag * rewrite in ts * move pulse to design system * clean up * clean up * clean up * disable tslint check * disable tslint check
2022-06-20 12:39:24 -07:00
},
activationHint:
"Once you've finished building your workflow, run it without having to click this button by using the production webhook URL.",
feat(editor): Improve trigger panel (#3509) * add panel * add workflow activation hints * support service trigger nodes * update polling state * support more views * update when trigger panel shows * update start/error nodes * add cron/interval info box * clean up start node * fix up webhook views * remove console log * add listening state * clean up loading state * update loading state * fix up animation * update views * add executions hint * update views * update accordian styling * address more issues * disable execute button if issues * disable if it has issues * add stop waiting button * can activate workflow when dsiabled * update el * fix has issues * add margin bttm * update views * close ndv * add shake * update copies * add error when polling node is missing one * update package lock * hide switch * hide binary data that's missing keys * hide main bar if ndv is open * remove waiting to execute * change accordion bg color * capitalize text * disable trigger panel in read only views * remove webhook title * update webhook desc * update component * update webhook executions note * update header * update webhook url * update exec help * bring back waiting to execute for non triggers * add transition fade * set shake * add helpful tooltip * add nonactive text * add inactive text * hide trigger panel by default * remove unused import * update pulse animation * handle empty values for options * update text * add flag for mock manual executions * add overrides * Add overrides * update check * update package lock; show button for others * hide more info * update other core nodes * update service name * remove panel from nodes * update panel * last tweaks * add telemetry event * add telemetry; address issues * address feedback * address feedback * address feedback * fix previous * fix previous * fix bug * fix bug with webhookbased * add extra break * update telemetry * update telemetry * add telemetry req * add info icon story; use icon component * clean css; en.json * clean en.json * rename key * add key * sort keys alpha * handle activation if active + add previous state to telemetry * stop activation if active * remove unnessary tracking * remove unused import * remove unused * remove unnessary flag * rewrite in ts * move pulse to design system * clean up * clean up * clean up * disable tslint check * disable tslint check
2022-06-20 12:39:24 -07:00
},
refactor: Apply `eslint-plugin-n8n-nodes-base` autofixable rules (#3174) * :zap: Initial setup * :shirt: Update `.eslintignore` * :shirt: Autofix node-param-default-missing (#3173) * :fire: Remove duplicate key * :shirt: Add exceptions * :package: Update package-lock.json * :shirt: Apply `node-class-description-inputs-wrong-trigger-node` (#3176) * :shirt: Apply `node-class-description-inputs-wrong-regular-node` (#3177) * :shirt: Apply `node-class-description-outputs-wrong` (#3178) * :shirt: Apply `node-execute-block-double-assertion-for-items` (#3179) * :shirt: Apply `node-param-default-wrong-for-collection` (#3180) * :shirt: Apply node-param-default-wrong-for-boolean (#3181) * Autofixed default missing * Autofixed booleans, worked well * :zap: Fix params * :rewind: Undo exempted autofixes * :package: Update package-lock.json * :shirt: Apply node-class-description-missing-subtitle (#3182) * :zap: Fix missing comma * :shirt: Apply `node-param-default-wrong-for-fixed-collection` (#3184) * :shirt: Add exception for `node-class-description-missing-subtitle` * :shirt: Apply `node-param-default-wrong-for-multi-options` (#3185) * :shirt: Apply `node-param-collection-type-unsorted-items` (#3186) * Missing coma * :shirt: Apply `node-param-default-wrong-for-simplify` (#3187) * :shirt: Apply `node-param-description-comma-separated-hyphen` (#3190) * :shirt: Apply `node-param-description-empty-string` (#3189) * :shirt: Apply `node-param-description-excess-inner-whitespace` (#3191) * Rule looks good * Add whitespace rule in eslint config * :zao: fix * :shirt: Apply `node-param-description-identical-to-display-name` (#3193) * :shirt: Apply `node-param-description-missing-for-ignore-ssl-issues` (#3195) * :rewind: Revert ":zao: fix" This reverts commit ef8a76f3dfedffd1bdccf3178af8a8d90cf5a55c. * :shirt: Apply `node-param-description-missing-for-simplify` (#3196) * :shirt: Apply `node-param-description-missing-final-period` (#3194) * Rule working as intended * Add rule to eslint * :shirt: Apply node-param-description-missing-for-return-all (#3197) * :zap: Restore `lintfix` command Co-authored-by: agobrech <45268029+agobrech@users.noreply.github.com> Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: agobrech <ael.gobrecht@gmail.com> Co-authored-by: Michael Kret <michael.k@radency.com>
2022-04-22 09:29:51 -07:00
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
2019-06-23 03:35:23 -07:00
inputs: [],
outputs: `={{(${configuredOutputs})($parameter)}}`,
credentials: credentialsProperty(this.authPropertyName),
webhooks: [defaultWebhookDescription],
2019-06-23 03:35:23 -07:00
properties: [
httpMethodsProperty,
2019-06-23 03:35:23 -07:00
{
displayName: 'Path',
name: 'path',
type: 'string',
default: '',
placeholder: 'webhook',
2019-06-23 03:35:23 -07:00
required: true,
description:
"The path to listen to, dynamic values could be specified by using ':', e.g. 'your-path/:dynamic-value'. If dynamic values are set 'webhookId' would be prepended to path.",
2019-06-23 03:35:23 -07:00
},
authenticationProperty(this.authPropertyName),
responseModeProperty,
{
displayName:
'Insert a \'Respond to Webhook\' node to control when and how you respond. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.respondtowebhook/" target="_blank">More details</a>',
name: 'webhookNotice',
type: 'notice',
displayOptions: {
show: {
responseMode: ['responseNode'],
},
},
default: '',
},
{
...responseCodeProperty,
displayOptions: {
show: {
'@version': [1, 1.1],
},
hide: {
responseMode: ['responseNode'],
},
},
},
responseDataProperty,
responseBinaryPropertyNameProperty,
{
...optionsProperty,
options: [...(optionsProperty.options as INodeProperties[]), responseCodeOption].sort(
(a, b) => {
const nameA = a.displayName.toUpperCase();
const nameB = b.displayName.toUpperCase();
if (nameA < nameB) return -1;
if (nameA > nameB) return 1;
return 0;
},
),
},
2019-06-23 03:35:23 -07:00
],
};
async webhook(context: IWebhookFunctions): Promise<IWebhookResponseData> {
const { typeVersion: nodeVersion, type: nodeType } = context.getNode();
if (nodeVersion >= 2 && nodeType === 'n8n-nodes-base.webhook') {
checkResponseModeConfiguration(context);
}
const options = context.getNodeParameter('options', {}) as {
binaryData: boolean;
ignoreBots: boolean;
rawBody: boolean;
responseData?: string;
ipWhitelist?: string;
};
const req = context.getRequestObject();
const resp = context.getResponseObject();
if (!isIpWhitelisted(options.ipWhitelist, req.ips, req.ip)) {
resp.writeHead(403);
resp.end('IP is not whitelisted to access the webhook!');
return { noWebhookResponse: true };
}
let validationData: IDataObject | undefined;
try {
if (options.ignoreBots && isbot(req.headers['user-agent']))
throw new WebhookAuthorizationError(403);
validationData = await this.validateAuth(context);
} catch (error) {
if (error instanceof WebhookAuthorizationError) {
resp.writeHead(error.responseCode, { 'WWW-Authenticate': 'Basic realm="Webhook"' });
resp.end(error.message);
return { noWebhookResponse: true };
}
throw error;
}
const prepareOutput = setupOutputConnection(context, {
jwtPayload: validationData,
});
if (options.binaryData) {
return await this.handleBinaryData(context, prepareOutput);
}
if (req.contentType === 'multipart/form-data') {
return await this.handleFormData(context, prepareOutput);
}
if (nodeVersion > 1 && !req.body && !options.rawBody) {
try {
return await this.handleBinaryData(context, prepareOutput);
} catch (error) {}
}
if (options.rawBody && !req.rawBody) {
await req.readRawBody();
}
const response: INodeExecutionData = {
json: {
headers: req.headers,
params: req.params,
query: req.query,
body: req.body,
},
binary: options.rawBody
? {
data: {
data: (req.rawBody ?? '').toString(BINARY_ENCODING),
mimeType: req.contentType ?? 'application/json',
},
}
: undefined,
};
return {
webhookResponse: options.responseData,
workflowData: prepareOutput(response),
};
}
private async validateAuth(context: IWebhookFunctions) {
const authentication = context.getNodeParameter(this.authPropertyName) as string;
if (authentication === 'none') return;
const req = context.getRequestObject();
const headers = context.getHeaderData();
2019-06-23 03:35:23 -07:00
if (authentication === 'basicAuth') {
// Basic authorization is needed to call webhook
let expectedAuth: ICredentialDataDecryptedObject | undefined;
try {
expectedAuth = await context.getCredentials('httpBasicAuth');
} catch {}
2019-06-23 03:35:23 -07:00
if (expectedAuth === undefined || !expectedAuth.user || !expectedAuth.password) {
2019-06-23 03:35:23 -07:00
// Data is not defined on node so can not authenticate
throw new WebhookAuthorizationError(500, 'No authentication data defined on node!');
2019-06-23 03:35:23 -07:00
}
const providedAuth = basicAuth(req);
// Authorization data is missing
if (!providedAuth) throw new WebhookAuthorizationError(401);
2019-06-23 03:35:23 -07:00
if (providedAuth.name !== expectedAuth.user || providedAuth.pass !== expectedAuth.password) {
2019-06-23 03:35:23 -07:00
// Provided authentication data is wrong
throw new WebhookAuthorizationError(403);
2019-06-23 03:35:23 -07:00
}
} else if (authentication === 'headerAuth') {
// Special header with value is needed to call webhook
let expectedAuth: ICredentialDataDecryptedObject | undefined;
try {
expectedAuth = await context.getCredentials('httpHeaderAuth');
} catch {}
if (expectedAuth === undefined || !expectedAuth.name || !expectedAuth.value) {
2019-06-23 03:35:23 -07:00
// Data is not defined on node so can not authenticate
throw new WebhookAuthorizationError(500, 'No authentication data defined on node!');
2019-06-23 03:35:23 -07:00
}
const headerName = (expectedAuth.name as string).toLowerCase();
const expectedValue = expectedAuth.value as string;
2019-06-23 03:35:23 -07:00
if (
!headers.hasOwnProperty(headerName) ||
(headers as IDataObject)[headerName] !== expectedValue
) {
2019-06-23 03:35:23 -07:00
// Provided authentication data is wrong
throw new WebhookAuthorizationError(403);
2019-06-23 03:35:23 -07:00
}
} else if (authentication === 'jwtAuth') {
let expectedAuth;
try {
expectedAuth = (await context.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 && 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);
}
2019-06-23 03:35:23 -07:00
}
}
2019-12-21 17:03:24 -08:00
private async handleFormData(
context: IWebhookFunctions,
prepareOutput: (data: INodeExecutionData) => INodeExecutionData[][],
) {
const req = context.getRequestObject() as MultiPartFormData.Request;
const options = context.getNodeParameter('options', {}) as IDataObject;
const { data, files } = req.body;
const returnItem: INodeExecutionData = {
json: {
headers: req.headers,
params: req.params,
query: req.query,
body: data,
},
};
if (files && Object.keys(files).length) {
returnItem.binary = {};
}
let count = 0;
for (const key of Object.keys(files)) {
const processFiles: MultiPartFormData.File[] = [];
let multiFile = false;
if (Array.isArray(files[key])) {
processFiles.push(...(files[key] as MultiPartFormData.File[]));
multiFile = true;
} else {
processFiles.push(files[key] as MultiPartFormData.File);
}
2020-03-20 11:53:51 -07:00
let fileCount = 0;
for (const file of processFiles) {
let binaryPropertyName = key;
if (binaryPropertyName.endsWith('[]')) {
binaryPropertyName = binaryPropertyName.slice(0, -2);
}
if (multiFile) {
binaryPropertyName += fileCount++;
}
if (options.binaryPropertyName) {
binaryPropertyName = `${options.binaryPropertyName}${count}`;
}
2020-03-20 11:53:51 -07:00
returnItem.binary![binaryPropertyName] = await context.nodeHelpers.copyBinaryFile(
file.filepath,
file.originalFilename ?? file.newFilename,
file.mimetype,
);
count += 1;
}
}
return { workflowData: prepareOutput(returnItem) };
}
2019-12-21 12:36:08 -08:00
private async handleBinaryData(
context: IWebhookFunctions,
prepareOutput: (data: INodeExecutionData) => INodeExecutionData[][],
): Promise<IWebhookResponseData> {
const req = context.getRequestObject();
const options = context.getNodeParameter('options', {}) as IDataObject;
// TODO: create empty binaryData placeholder, stream into that path, and then finalize the binaryData
const binaryFile = await tmpFile({ prefix: 'n8n-webhook-' });
try {
await pipeline(req, createWriteStream(binaryFile.path));
const returnItem: INodeExecutionData = {
json: {
headers: req.headers,
params: req.params,
query: req.query,
body: {},
2020-10-22 06:46:03 -07:00
},
2019-12-21 12:36:08 -08:00
};
const stats = await stat(binaryFile.path);
if (stats.size) {
const binaryPropertyName = (options.binaryPropertyName ?? 'data') as string;
const fileName = req.contentDisposition?.filename ?? uuid();
const binaryData = await context.nodeHelpers.copyBinaryFile(
binaryFile.path,
fileName,
req.contentType ?? 'application/octet-stream',
);
returnItem.binary = { [binaryPropertyName]: binaryData };
}
return { workflowData: prepareOutput(returnItem) };
} catch (error) {
throw new NodeOperationError(context.getNode(), error as Error);
} finally {
await binaryFile.cleanup();
}
2019-06-23 03:35:23 -07:00
}
}