refactor: Setup node context API, and consolidate code between Webhook and Wait nodes (no-changelog) (#6464)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-07-04 16:17:50 +02:00 committed by GitHub
parent 29882a6f39
commit 4c854f4f23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 679 additions and 1094 deletions

View file

@ -106,13 +106,13 @@ export class WaitingWebhooks {
workflow, workflow,
workflow.getNode(lastNodeExecuted) as INode, workflow.getNode(lastNodeExecuted) as INode,
additionalData, additionalData,
).filter((webhook) => { ).find((webhook) => {
return ( return (
webhook.httpMethod === httpMethod && webhook.httpMethod === httpMethod &&
webhook.path === path && webhook.path === path &&
webhook.webhookDescription.restartWebhook === true webhook.webhookDescription.restartWebhook === true
); );
})[0]; });
if (webhookData === undefined) { if (webhookData === undefined) {
// If no data got found it means that the execution can not be started via a webhook. // If no data got found it means that the execution can not be started via a webhook.

View file

@ -518,7 +518,11 @@ export const N8N_CONTACT_EMAIL = 'contact@n8n.io';
export const EXPRESSION_EDITOR_PARSER_TIMEOUT = 15_000; // ms export const EXPRESSION_EDITOR_PARSER_TIMEOUT = 15_000; // ms
export const KEEP_AUTH_IN_NDV_FOR_NODES = [HTTP_REQUEST_NODE_TYPE, WEBHOOK_NODE_TYPE]; export const KEEP_AUTH_IN_NDV_FOR_NODES = [
HTTP_REQUEST_NODE_TYPE,
WEBHOOK_NODE_TYPE,
WAIT_NODE_TYPE,
];
export const MAIN_AUTH_FIELD_NAME = 'authentication'; export const MAIN_AUTH_FIELD_NAME = 'authentication';
export const NODE_RESOURCE_FIELD_NAME = 'resource'; export const NODE_RESOURCE_FIELD_NAME = 'resource';

View file

@ -1,44 +1,34 @@
import type { import type {
IExecuteFunctions, IExecuteFunctions,
ICredentialDataDecryptedObject,
IDataObject,
INodeExecutionData, INodeExecutionData,
INodeType,
INodeTypeDescription, INodeTypeDescription,
IWebhookFunctions, INodeProperties,
IWebhookResponseData, IDisplayOptions,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { BINARY_ENCODING, WAIT_TIME_UNLIMITED, NodeOperationError } from 'n8n-workflow'; import { WAIT_TIME_UNLIMITED } from 'n8n-workflow';
import fs from 'fs'; import {
import stream from 'stream'; authenticationProperty,
import { promisify } from 'util'; credentialsProperty,
import basicAuth from 'basic-auth'; defaultWebhookDescription,
import type { Response } from 'express'; httpMethodsProperty,
import formidable from 'formidable'; optionsProperty,
import isbot from 'isbot'; responseBinaryPropertyNameProperty,
import { file as tmpFile } from 'tmp-promise'; responseCodeProperty,
responseDataProperty,
responseModeProperty,
} from '../Webhook/description';
import { Webhook } from '../Webhook/Webhook.node';
const pipeline = promisify(stream.pipeline); const displayOnWebhook: IDisplayOptions = {
show: {
resume: ['webhook'],
},
};
function authorizationError(resp: Response, realm: string, responseCode: number, message?: string) { export class Wait extends Webhook {
if (message === undefined) { authPropertyName = 'incomingAuthentication';
message = 'Authorization problem!';
if (responseCode === 401) {
message = 'Authorization is required!';
} else if (responseCode === 403) {
message = 'Authorization data is wrong!';
}
}
resp.writeHead(responseCode, { 'WWW-Authenticate': `Basic realm="${realm}"` });
resp.end(message);
return {
noWebhookResponse: true,
};
}
export class Wait implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Wait', displayName: 'Wait',
name: 'wait', name: 'wait',
@ -52,70 +42,16 @@ export class Wait implements INodeType {
}, },
inputs: ['main'], inputs: ['main'],
outputs: ['main'], outputs: ['main'],
credentials: [ credentials: credentialsProperty(this.authPropertyName),
{
name: 'httpBasicAuth',
required: true,
displayOptions: {
show: {
incomingAuthentication: ['basicAuth'],
},
},
},
{
name: 'httpHeaderAuth',
required: true,
displayOptions: {
show: {
incomingAuthentication: ['headerAuth'],
},
},
},
],
webhooks: [ webhooks: [
{ {
name: 'default', ...defaultWebhookDescription,
httpMethod: '={{$parameter["httpMethod"]}}',
isFullPath: true,
responseCode: '={{$parameter["responseCode"]}}',
responseMode: '={{$parameter["responseMode"]}}',
responseData: '={{$parameter["responseData"]}}', responseData: '={{$parameter["responseData"]}}',
responseBinaryPropertyName: '={{$parameter["responseBinaryPropertyName"]}}',
responseContentType: '={{$parameter["options"]["responseContentType"]}}',
responsePropertyName: '={{$parameter["options"]["responsePropertyName"]}}',
responseHeaders: '={{$parameter["options"]["responseHeaders"]}}',
path: '={{$parameter["options"]["webhookSuffix"] || ""}}', path: '={{$parameter["options"]["webhookSuffix"] || ""}}',
restartWebhook: true, restartWebhook: true,
}, },
], ],
properties: [ properties: [
{
displayName: 'Webhook Authentication',
name: 'incomingAuthentication',
type: 'options',
displayOptions: {
show: {
resume: ['webhook'],
},
},
options: [
{
name: 'Basic Auth',
value: 'basicAuth',
},
{
name: 'Header Auth',
value: 'headerAuth',
},
{
name: 'None',
value: 'none',
},
],
default: 'none',
description:
'If and how incoming resume-webhook-requests to $execution.resumeUrl should be authenticated for additional security',
},
{ {
displayName: 'Resume', displayName: 'Resume',
name: 'resume', name: 'resume',
@ -140,6 +76,12 @@ export class Wait implements INodeType {
default: 'timeInterval', default: 'timeInterval',
description: 'Determines the waiting mode to use before the workflow continues', description: 'Determines the waiting mode to use before the workflow continues',
}, },
{
...authenticationProperty(this.authPropertyName),
description:
'If and how incoming resume-webhook-requests to $execution.resumeUrl should be authenticated for additional security',
displayOptions: displayOnWebhook,
},
// ---------------------------------- // ----------------------------------
// resume:specificTime // resume:specificTime
@ -215,142 +157,39 @@ export class Wait implements INodeType {
'The webhook URL will be generated at run time. It can be referenced with the <strong>$execution.resumeUrl</strong> variable. Send it somewhere before getting to this node. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=n8n-nodes-base.wait" target="_blank">More info</a>', 'The webhook URL will be generated at run time. It can be referenced with the <strong>$execution.resumeUrl</strong> variable. Send it somewhere before getting to this node. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=n8n-nodes-base.wait" target="_blank">More info</a>',
name: 'webhookNotice', name: 'webhookNotice',
type: 'notice', type: 'notice',
displayOptions: { displayOptions: displayOnWebhook,
show: {
resume: ['webhook'],
},
},
default: '', default: '',
}, },
{ {
displayName: 'HTTP Method', ...httpMethodsProperty,
name: 'httpMethod', displayOptions: displayOnWebhook,
type: 'options',
displayOptions: {
show: {
resume: ['webhook'],
},
},
options: [
{
name: 'DELETE',
value: 'DELETE',
},
{
name: 'GET',
value: 'GET',
},
{
name: 'HEAD',
value: 'HEAD',
},
{
name: 'PATCH',
value: 'PATCH',
},
{
name: 'POST',
value: 'POST',
},
{
name: 'PUT',
value: 'PUT',
},
],
default: 'GET',
description: 'The HTTP method of the Webhook call', description: 'The HTTP method of the Webhook call',
}, },
{ {
displayName: 'Response Code', ...responseCodeProperty,
name: 'responseCode', displayOptions: displayOnWebhook,
type: 'number',
displayOptions: {
show: {
resume: ['webhook'],
},
},
typeOptions: {
minValue: 100,
maxValue: 599,
},
default: 200,
description: 'The HTTP Response code to return',
}, },
{ {
displayName: 'Respond', ...responseModeProperty,
name: 'responseMode', displayOptions: displayOnWebhook,
type: 'options',
displayOptions: {
show: {
resume: ['webhook'],
},
},
options: [
{
name: 'Immediately',
value: 'onReceived',
description: 'As soon as this node executes',
},
{
name: 'When Last Node Finishes',
value: 'lastNode',
description: 'Returns data of the last-executed node',
},
{
name: "Using 'Respond to Webhook' Node",
value: 'responseNode',
description: 'Response defined in that node',
},
],
default: 'onReceived',
description: 'When and how to respond to the webhook',
}, },
{ {
displayName: 'Response Data', ...responseDataProperty,
name: 'responseData',
type: 'options',
displayOptions: { displayOptions: {
show: { show: {
resume: ['webhook'], ...responseDataProperty.displayOptions?.show,
responseMode: ['lastNode'], ...displayOnWebhook.show,
}, },
}, },
options: [
{
name: 'All Entries',
value: 'allEntries',
description: 'Returns all the entries of the last node. Always returns an array.',
},
{
name: 'First Entry JSON',
value: 'firstEntryJson',
description:
'Returns the JSON data of the first entry of the last node. Always returns a JSON object.',
},
{
name: 'First Entry Binary',
value: 'firstEntryBinary',
description:
'Returns the binary data of the first entry of the last node. Always returns a binary file.',
},
],
default: 'firstEntryJson',
description:
'What data should be returned. If it should return all the items as array or only the first item as object.',
}, },
{ {
displayName: 'Property Name', ...responseBinaryPropertyNameProperty,
name: 'responseBinaryPropertyName',
type: 'string',
required: true,
default: 'data',
displayOptions: { displayOptions: {
show: { show: {
resume: ['webhook'], ...responseBinaryPropertyNameProperty.displayOptions?.show,
responseData: ['firstEntryBinary'], ...displayOnWebhook.show,
}, },
}, },
description: 'Name of the binary property to return',
}, },
{ {
displayName: 'Limit Wait Time', displayName: 'Limit Wait Time',
@ -360,11 +199,7 @@ export class Wait implements INodeType {
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
description: description:
'If no webhook call is received, the workflow will automatically resume execution after the specified limit type', 'If no webhook call is received, the workflow will automatically resume execution after the specified limit type',
displayOptions: { displayOptions: displayOnWebhook,
show: {
resume: ['webhook'],
},
},
}, },
{ {
displayName: 'Limit Type', displayName: 'Limit Type',
@ -376,7 +211,7 @@ export class Wait implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
limitWaitTime: [true], limitWaitTime: [true],
resume: ['webhook'], ...displayOnWebhook.show,
}, },
}, },
options: [ options: [
@ -400,7 +235,7 @@ export class Wait implements INodeType {
show: { show: {
limitType: ['afterTimeInterval'], limitType: ['afterTimeInterval'],
limitWaitTime: [true], limitWaitTime: [true],
resume: ['webhook'], ...displayOnWebhook.show,
}, },
}, },
typeOptions: { typeOptions: {
@ -418,7 +253,7 @@ export class Wait implements INodeType {
show: { show: {
limitType: ['afterTimeInterval'], limitType: ['afterTimeInterval'],
limitWaitTime: [true], limitWaitTime: [true],
resume: ['webhook'], ...displayOnWebhook.show,
}, },
}, },
options: [ options: [
@ -450,132 +285,17 @@ export class Wait implements INodeType {
show: { show: {
limitType: ['atSpecifiedTime'], limitType: ['atSpecifiedTime'],
limitWaitTime: [true], limitWaitTime: [true],
resume: ['webhook'], ...displayOnWebhook.show,
}, },
}, },
default: '', default: '',
description: 'Continue execution after the specified date and time', description: 'Continue execution after the specified date and time',
}, },
{ {
displayName: 'Options', ...optionsProperty,
name: 'options', displayOptions: displayOnWebhook,
type: 'collection',
displayOptions: {
show: {
resume: ['webhook'],
},
},
placeholder: 'Add Option',
default: {},
options: [ options: [
{ ...(optionsProperty.options as INodeProperties[]),
displayName: 'Binary Data',
name: 'binaryData',
type: 'boolean',
displayOptions: {
show: {
'/httpMethod': ['PATCH', 'PUT', 'POST'],
},
},
default: false,
description: 'Whether the webhook will receive binary data',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
displayOptions: {
show: {
binaryData: [true],
},
},
description:
'Name of the binary property to which to write the data of the received file. If the data gets received via "Form-Data Multipart" it will be the prefix and a number starting with 0 will be attached to it.',
},
{
displayName: 'Ignore Bots',
name: 'ignoreBots',
type: 'boolean',
default: false,
description:
'Whether to ignore requests from bots like link previewers and web crawlers',
},
{
displayName: 'Response Data',
name: 'responseData',
type: 'string',
displayOptions: {
show: {
'/responseMode': ['onReceived'],
},
},
default: '',
placeholder: 'success',
description: 'Custom response data to send',
},
{
displayName: 'Response Content-Type',
name: 'responseContentType',
type: 'string',
displayOptions: {
show: {
'/responseData': ['firstEntryJson'],
'/responseMode': ['lastNode'],
},
},
default: '',
placeholder: 'application/xml',
// eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-json
description:
'Set a custom content-type to return if another one as the "application/json" should be returned',
},
{
displayName: 'Response Headers',
name: 'responseHeaders',
placeholder: 'Add Response Header',
description: 'Add headers to the webhook response',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'entries',
displayName: 'Entries',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the header',
},
],
},
],
},
{
displayName: 'Property Name',
name: 'responsePropertyName',
type: 'string',
displayOptions: {
show: {
'/responseData': ['firstEntryJson'],
'/responseMode': ['lastNode'],
},
},
default: 'data',
description: 'Name of the property to return the data of instead of the whole JSON',
},
{ {
displayName: 'Webhook Suffix', displayName: 'Webhook Suffix',
name: 'webhookSuffix', name: 'webhookSuffix',
@ -585,253 +305,23 @@ export class Wait implements INodeType {
description: description:
'This suffix path will be appended to the restart URL. Helpful when using multiple wait nodes. Note: Does not support expressions.', 'This suffix path will be appended to the restart URL. Helpful when using multiple wait nodes. Note: Does not support expressions.',
}, },
// {
// displayName: 'Raw Body',
// name: 'rawBody',
// type: 'boolean',
// displayOptions: {
// hide: {
// binaryData: [
// true,
// ],
// },
// },
// default: false,
// description: 'Raw body (binary)',
// },
], ],
}, },
], ],
}; };
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> { async execute(context: IExecuteFunctions): Promise<INodeExecutionData[][]> {
// INFO: Currently (20.06.2021) 100% identical with Webhook-Node const resume = context.getNodeParameter('resume', 0) as string;
const incomingAuthentication = this.getNodeParameter('incomingAuthentication') as string;
const options = this.getNodeParameter('options', {}) as IDataObject;
const req = this.getRequestObject();
const resp = this.getResponseObject();
const headers = this.getHeaderData();
const realm = 'Webhook';
const ignoreBots = options.ignoreBots as boolean;
if (ignoreBots && isbot((headers as IDataObject)['user-agent'] as string)) {
return authorizationError(resp, realm, 403);
}
if (incomingAuthentication === 'basicAuth') {
// Basic authorization is needed to call webhook
let httpBasicAuth: ICredentialDataDecryptedObject | undefined;
try {
httpBasicAuth = await this.getCredentials('httpBasicAuth');
} catch (error) {
// Do nothing
}
if (httpBasicAuth === undefined || !httpBasicAuth.user || !httpBasicAuth.password) {
// Data is not defined on node so can not authenticate
return authorizationError(resp, realm, 500, 'No authentication data defined on node!');
}
const basicAuthData = basicAuth(req);
if (basicAuthData === undefined) {
// Authorization data is missing
return authorizationError(resp, realm, 401);
}
if (
basicAuthData.name !== httpBasicAuth.user ||
basicAuthData.pass !== httpBasicAuth.password
) {
// Provided authentication data is wrong
return authorizationError(resp, realm, 403);
}
} else if (incomingAuthentication === 'headerAuth') {
// Special header with value is needed to call webhook
let httpHeaderAuth: ICredentialDataDecryptedObject | undefined;
try {
httpHeaderAuth = await this.getCredentials('httpHeaderAuth');
} catch (error) {
// Do nothing
}
if (httpHeaderAuth === undefined || !httpHeaderAuth.name || !httpHeaderAuth.value) {
// Data is not defined on node so can not authenticate
return authorizationError(resp, realm, 500, 'No authentication data defined on node!');
}
const headerName = (httpHeaderAuth.name as string).toLowerCase();
const headerValue = httpHeaderAuth.value as string;
if (
!headers.hasOwnProperty(headerName) ||
(headers as IDataObject)[headerName] !== headerValue
) {
// Provided authentication data is wrong
return authorizationError(resp, realm, 403);
}
}
const mimeType = headers['content-type'] ?? 'application/json';
if (mimeType.includes('multipart/form-data')) {
const form = new formidable.IncomingForm({ multiples: true });
return new Promise((resolve, _reject) => {
form.parse(req, async (err, data, files) => {
const returnItem: INodeExecutionData = {
binary: {},
json: {
headers,
params: this.getParamsData(),
query: this.getQueryData(),
body: data,
},
};
let count = 0;
for (const xfile of Object.keys(files)) {
const processFiles: formidable.File[] = [];
let multiFile = false;
if (Array.isArray(files[xfile])) {
processFiles.push(...(files[xfile] as formidable.File[]));
multiFile = true;
} else {
processFiles.push(files[xfile] as formidable.File);
}
let fileCount = 0;
for (const file of processFiles) {
let binaryPropertyName = xfile;
if (binaryPropertyName.endsWith('[]')) {
binaryPropertyName = binaryPropertyName.slice(0, -2);
}
if (multiFile) {
binaryPropertyName += fileCount++;
}
if (options.binaryPropertyName) {
binaryPropertyName = `${options.binaryPropertyName}${count}`;
}
const fileJson = file.toJSON();
returnItem.binary![binaryPropertyName] = await this.nodeHelpers.copyBinaryFile(
file.path,
fileJson.name || fileJson.filename,
fileJson.type as string,
);
count += 1;
}
}
resolve({
workflowData: [[returnItem]],
});
});
});
}
if (options.binaryData === true) {
const binaryFile = await tmpFile({ prefix: 'n8n-webhook-' });
try {
await pipeline(req, fs.createWriteStream(binaryFile.path));
const returnItem: INodeExecutionData = {
binary: {},
json: {
headers,
params: this.getParamsData(),
query: this.getQueryData(),
body: this.getBodyData(),
},
};
const binaryPropertyName = (options.binaryPropertyName || 'data') as string;
returnItem.binary![binaryPropertyName] = await this.nodeHelpers.copyBinaryFile(
binaryFile.path,
mimeType,
);
return {
workflowData: [[returnItem]],
};
} catch (error) {
throw new NodeOperationError(this.getNode(), error as Error);
} finally {
await binaryFile.cleanup();
}
}
const response: INodeExecutionData = {
json: {
headers,
params: this.getParamsData(),
query: this.getQueryData(),
body: this.getBodyData(),
},
};
if (options.rawBody) {
response.binary = {
data: {
data: req.rawBody.toString(BINARY_ENCODING),
mimeType,
},
};
}
let webhookResponse: string | undefined;
if (options.responseData) {
webhookResponse = options.responseData as string;
}
return {
webhookResponse,
workflowData: [[response]],
};
}
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const resume = this.getNodeParameter('resume', 0) as string;
if (resume === 'webhook') { if (resume === 'webhook') {
let waitTill = new Date(WAIT_TIME_UNLIMITED); return this.handleWebhookResume(context);
const limitWaitTime = this.getNodeParameter('limitWaitTime', 0);
if (limitWaitTime === true) {
const limitType = this.getNodeParameter('limitType', 0);
if (limitType === 'afterTimeInterval') {
let waitAmount = this.getNodeParameter('resumeAmount', 0) as number;
const resumeUnit = this.getNodeParameter('resumeUnit', 0);
if (resumeUnit === 'minutes') {
waitAmount *= 60;
}
if (resumeUnit === 'hours') {
waitAmount *= 60 * 60;
}
if (resumeUnit === 'days') {
waitAmount *= 60 * 60 * 24;
}
waitAmount *= 1000;
waitTill = new Date(new Date().getTime() + waitAmount);
} else {
waitTill = new Date(this.getNodeParameter('maxDateAndTime', 0) as string);
}
}
await this.putExecutionToWait(waitTill);
return [this.getInputData()];
} }
let waitTill: Date; let waitTill: Date;
if (resume === 'timeInterval') { if (resume === 'timeInterval') {
const unit = this.getNodeParameter('unit', 0) as string; const unit = context.getNodeParameter('unit', 0) as string;
let waitAmount = this.getNodeParameter('amount', 0) as number; let waitAmount = context.getNodeParameter('amount', 0) as number;
if (unit === 'minutes') { if (unit === 'minutes') {
waitAmount *= 60; waitAmount *= 60;
} }
@ -847,7 +337,7 @@ export class Wait implements INodeType {
waitTill = new Date(new Date().getTime() + waitAmount); waitTill = new Date(new Date().getTime() + waitAmount);
} else { } else {
// resume: dateTime // resume: dateTime
const dateTime = this.getNodeParameter('dateTime', 0) as string; const dateTime = context.getNodeParameter('dateTime', 0) as string;
waitTill = new Date(dateTime); waitTill = new Date(dateTime);
} }
@ -859,14 +349,48 @@ export class Wait implements INodeType {
// we just check the database every 60 seconds. // we just check the database every 60 seconds.
return new Promise((resolve, _reject) => { return new Promise((resolve, _reject) => {
setTimeout(() => { setTimeout(() => {
resolve([this.getInputData()]); resolve([context.getInputData()]);
}, waitValue); }, waitValue);
}); });
} }
// If longer than 60 seconds put execution to wait // If longer than 65 seconds put execution to wait
await this.putExecutionToWait(waitTill); return this.putToWait(context, waitTill);
}
return [this.getInputData()]; private async handleWebhookResume(context: IExecuteFunctions) {
let waitTill = new Date(WAIT_TIME_UNLIMITED);
const limitWaitTime = context.getNodeParameter('limitWaitTime', 0);
if (limitWaitTime === true) {
const limitType = context.getNodeParameter('limitType', 0);
if (limitType === 'afterTimeInterval') {
let waitAmount = context.getNodeParameter('resumeAmount', 0) as number;
const resumeUnit = context.getNodeParameter('resumeUnit', 0);
if (resumeUnit === 'minutes') {
waitAmount *= 60;
}
if (resumeUnit === 'hours') {
waitAmount *= 60 * 60;
}
if (resumeUnit === 'days') {
waitAmount *= 60 * 60 * 24;
}
waitAmount *= 1000;
waitTill = new Date(new Date().getTime() + waitAmount);
} else {
waitTill = new Date(context.getNodeParameter('maxDateAndTime', 0) as string);
}
}
return this.putToWait(context, waitTill);
}
private async putToWait(context: IExecuteFunctions, waitTill: Date) {
await context.putExecutionToWait(waitTill);
return [context.getInputData()];
} }
} }

View file

@ -1,43 +1,40 @@
/* eslint-disable n8n-nodes-base/node-execute-block-wrong-error-thrown */
import type { import type {
IWebhookFunctions, IWebhookFunctions,
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject,
IDataObject, IDataObject,
INodeExecutionData, INodeExecutionData,
INodeType,
INodeTypeDescription, INodeTypeDescription,
IWebhookResponseData, IWebhookResponseData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { BINARY_ENCODING, NodeOperationError } from 'n8n-workflow'; import { BINARY_ENCODING, NodeOperationError, Node } from 'n8n-workflow';
import fs from 'fs'; import fs from 'fs';
import stream from 'stream'; import stream from 'stream';
import { promisify } from 'util'; import { promisify } from 'util';
import basicAuth from 'basic-auth'; import basicAuth from 'basic-auth';
import type { Response } from 'express';
import formidable from 'formidable'; import formidable from 'formidable';
import isbot from 'isbot'; import isbot from 'isbot';
import { file as tmpFile } from 'tmp-promise'; import { file as tmpFile } from 'tmp-promise';
import {
authenticationProperty,
credentialsProperty,
defaultWebhookDescription,
httpMethodsProperty,
optionsProperty,
responseBinaryPropertyNameProperty,
responseCodeProperty,
responseDataProperty,
responseModeProperty,
} from './description';
import { WebhookAuthorizationError } from './error';
const pipeline = promisify(stream.pipeline); const pipeline = promisify(stream.pipeline);
function authorizationError(resp: Response, realm: string, responseCode: number, message?: string) { export class Webhook extends Node {
if (message === undefined) { authPropertyName = 'authentication';
message = 'Authorization problem!';
if (responseCode === 401) {
message = 'Authorization is required!';
} else if (responseCode === 403) {
message = 'Authorization data is wrong!';
}
}
resp.writeHead(responseCode, { 'WWW-Authenticate': `Basic realm="${realm}"` });
resp.end(message);
return {
noWebhookResponse: true,
};
}
export class Webhook implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Webhook', displayName: 'Webhook',
icon: 'file:webhook.svg', icon: 'file:webhook.svg',
@ -64,97 +61,11 @@ export class Webhook implements INodeType {
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
inputs: [], inputs: [],
outputs: ['main'], outputs: ['main'],
credentials: [ credentials: credentialsProperty(this.authPropertyName),
{ webhooks: [defaultWebhookDescription],
name: 'httpBasicAuth',
required: true,
displayOptions: {
show: {
authentication: ['basicAuth'],
},
},
},
{
name: 'httpHeaderAuth',
required: true,
displayOptions: {
show: {
authentication: ['headerAuth'],
},
},
},
],
webhooks: [
{
name: 'default',
httpMethod: '={{$parameter["httpMethod"]}}',
isFullPath: true,
responseCode: '={{$parameter["responseCode"]}}',
responseMode: '={{$parameter["responseMode"]}}',
responseData:
'={{$parameter["responseData"] || ($parameter.options.noResponseBody ? "noData" : undefined) }}',
responseBinaryPropertyName: '={{$parameter["responseBinaryPropertyName"]}}',
responseContentType: '={{$parameter["options"]["responseContentType"]}}',
responsePropertyName: '={{$parameter["options"]["responsePropertyName"]}}',
responseHeaders: '={{$parameter["options"]["responseHeaders"]}}',
path: '={{$parameter["path"]}}',
},
],
properties: [ properties: [
{ authenticationProperty(this.authPropertyName),
displayName: 'Authentication', httpMethodsProperty,
name: 'authentication',
type: 'options',
options: [
{
name: 'Basic Auth',
value: 'basicAuth',
},
{
name: 'Header Auth',
value: 'headerAuth',
},
{
name: 'None',
value: 'none',
},
],
default: 'none',
description: 'The way to authenticate',
},
{
displayName: 'HTTP Method',
name: 'httpMethod',
type: 'options',
options: [
{
name: 'DELETE',
value: 'DELETE',
},
{
name: 'GET',
value: 'GET',
},
{
name: 'HEAD',
value: 'HEAD',
},
{
name: 'PATCH',
value: 'PATCH',
},
{
name: 'POST',
value: 'POST',
},
{
name: 'PUT',
value: 'PUT',
},
],
default: 'GET',
description: 'The HTTP method to listen to',
},
{ {
displayName: 'Path', displayName: 'Path',
name: 'path', name: 'path',
@ -164,30 +75,7 @@ export class Webhook implements INodeType {
required: true, required: true,
description: 'The path to listen to', description: 'The path to listen to',
}, },
{ responseModeProperty,
displayName: 'Respond',
name: 'responseMode',
type: 'options',
options: [
{
name: 'Immediately',
value: 'onReceived',
description: 'As soon as this node executes',
},
{
name: 'When Last Node Finishes',
value: 'lastNode',
description: 'Returns data of the last-executed node',
},
{
name: "Using 'Respond to Webhook' Node",
value: 'responseNode',
description: 'Response defined in that node',
},
],
default: 'onReceived',
description: 'When and how to respond to the webhook',
},
{ {
displayName: 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>', '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>',
@ -200,406 +88,206 @@ export class Webhook implements INodeType {
}, },
default: '', default: '',
}, },
{ responseCodeProperty,
displayName: 'Response Code', responseDataProperty,
name: 'responseCode', responseBinaryPropertyNameProperty,
type: 'number', optionsProperty,
displayOptions: {
hide: {
responseMode: ['responseNode'],
},
},
typeOptions: {
minValue: 100,
maxValue: 599,
},
default: 200,
description: 'The HTTP Response code to return',
},
{
displayName: 'Response Data',
name: 'responseData',
type: 'options',
displayOptions: {
show: {
responseMode: ['lastNode'],
},
},
options: [
{
name: 'All Entries',
value: 'allEntries',
description: 'Returns all the entries of the last node. Always returns an array.',
},
{
name: 'First Entry JSON',
value: 'firstEntryJson',
description:
'Returns the JSON data of the first entry of the last node. Always returns a JSON object.',
},
{
name: 'First Entry Binary',
value: 'firstEntryBinary',
description:
'Returns the binary data of the first entry of the last node. Always returns a binary file.',
},
{
name: 'No Response Body',
value: 'noData',
description: 'Returns without a body',
},
],
default: 'firstEntryJson',
description:
'What data should be returned. If it should return all items as an array or only the first item as object.',
},
{
displayName: 'Property Name',
name: 'responseBinaryPropertyName',
type: 'string',
required: true,
default: 'data',
displayOptions: {
show: {
responseData: ['firstEntryBinary'],
},
},
description: 'Name of the binary property to return',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Binary Data',
name: 'binaryData',
type: 'boolean',
displayOptions: {
show: {
'/httpMethod': ['PATCH', 'PUT', 'POST'],
},
},
default: false,
description: 'Whether the webhook will receive binary data',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
binaryData: [true],
},
},
description:
'Name of the binary property to write the data of the received file to. If the data gets received via "Form-Data Multipart" it will be the prefix and a number starting with 0 will be attached to it.',
},
{
displayName: 'Ignore Bots',
name: 'ignoreBots',
type: 'boolean',
default: false,
description:
'Whether to ignore requests from bots like link previewers and web crawlers',
},
{
displayName: 'No Response Body',
name: 'noResponseBody',
type: 'boolean',
default: false,
description: 'Whether to send any body in the response',
displayOptions: {
hide: {
rawBody: [true],
},
show: {
'/responseMode': ['onReceived'],
},
},
},
{
displayName: 'Raw Body',
name: 'rawBody',
type: 'boolean',
displayOptions: {
hide: {
binaryData: [true],
noResponseBody: [true],
},
},
default: false,
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
description: 'Raw body (binary)',
},
{
displayName: 'Response Data',
name: 'responseData',
type: 'string',
displayOptions: {
show: {
'/responseMode': ['onReceived'],
},
hide: {
noResponseBody: [true],
},
},
default: '',
placeholder: 'success',
description: 'Custom response data to send',
},
{
displayName: 'Response Content-Type',
name: 'responseContentType',
type: 'string',
displayOptions: {
show: {
'/responseData': ['firstEntryJson'],
'/responseMode': ['lastNode'],
},
},
default: '',
placeholder: 'application/xml',
// eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-json
description:
'Set a custom content-type to return if another one as the "application/json" should be returned',
},
{
displayName: 'Response Headers',
name: 'responseHeaders',
placeholder: 'Add Response Header',
description: 'Add headers to the webhook response',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'entries',
displayName: 'Entries',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the header',
},
],
},
],
},
{
displayName: 'Property Name',
name: 'responsePropertyName',
type: 'string',
displayOptions: {
show: {
'/responseData': ['firstEntryJson'],
'/responseMode': ['lastNode'],
},
},
default: 'data',
description: 'Name of the property to return the data of instead of the whole JSON',
},
],
},
], ],
}; };
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> { async webhook(context: IWebhookFunctions): Promise<IWebhookResponseData> {
const authentication = this.getNodeParameter('authentication') as string; const options = context.getNodeParameter('options', {}) as {
const options = this.getNodeParameter('options', {}) as IDataObject; binaryData: boolean;
const req = this.getRequestObject(); ignoreBots: boolean;
const resp = this.getResponseObject(); rawBody: Buffer;
const headers = this.getHeaderData(); responseData?: string;
const realm = 'Webhook'; };
const req = context.getRequestObject();
const resp = context.getResponseObject();
const ignoreBots = options.ignoreBots as boolean; try {
if (ignoreBots && isbot((headers as IDataObject)['user-agent'] as string)) { if (options.ignoreBots && isbot(req.headers['user-agent']))
return authorizationError(resp, realm, 403); throw new WebhookAuthorizationError(403);
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;
} }
if (authentication === 'basicAuth') { const mimeType = req.headers['content-type'] ?? 'application/json';
// Basic authorization is needed to call webhook
let httpBasicAuth: ICredentialDataDecryptedObject | undefined;
try {
httpBasicAuth = await this.getCredentials('httpBasicAuth');
} catch (error) {
// Do nothing
}
if (httpBasicAuth === undefined || !httpBasicAuth.user || !httpBasicAuth.password) {
// Data is not defined on node so can not authenticate
return authorizationError(resp, realm, 500, 'No authentication data defined on node!');
}
const basicAuthData = basicAuth(req);
if (basicAuthData === undefined) {
// Authorization data is missing
return authorizationError(resp, realm, 401);
}
if (
basicAuthData.name !== httpBasicAuth.user ||
basicAuthData.pass !== httpBasicAuth.password
) {
// Provided authentication data is wrong
return authorizationError(resp, realm, 403);
}
} else if (authentication === 'headerAuth') {
// Special header with value is needed to call webhook
let httpHeaderAuth: ICredentialDataDecryptedObject | undefined;
try {
httpHeaderAuth = await this.getCredentials('httpHeaderAuth');
} catch (error) {
// Do nothing
}
if (httpHeaderAuth === undefined || !httpHeaderAuth.name || !httpHeaderAuth.value) {
// Data is not defined on node so can not authenticate
return authorizationError(resp, realm, 500, 'No authentication data defined on node!');
}
const headerName = (httpHeaderAuth.name as string).toLowerCase();
const headerValue = httpHeaderAuth.value as string;
if (
!headers.hasOwnProperty(headerName) ||
(headers as IDataObject)[headerName] !== headerValue
) {
// Provided authentication data is wrong
return authorizationError(resp, realm, 403);
}
}
const mimeType = headers['content-type'] ?? 'application/json';
if (mimeType.includes('multipart/form-data')) { if (mimeType.includes('multipart/form-data')) {
const form = new formidable.IncomingForm({ multiples: true }); return this.handleFormData(context);
return new Promise((resolve, _reject) => {
form.parse(req, async (err, data, files) => {
const returnItem: INodeExecutionData = {
binary: {},
json: {
headers,
params: this.getParamsData(),
query: this.getQueryData(),
body: data,
},
};
let count = 0;
for (const xfile of Object.keys(files)) {
const processFiles: formidable.File[] = [];
let multiFile = false;
if (Array.isArray(files[xfile])) {
processFiles.push(...(files[xfile] as formidable.File[]));
multiFile = true;
} else {
processFiles.push(files[xfile] as formidable.File);
}
let fileCount = 0;
for (const file of processFiles) {
let binaryPropertyName = xfile;
if (binaryPropertyName.endsWith('[]')) {
binaryPropertyName = binaryPropertyName.slice(0, -2);
}
if (multiFile) {
binaryPropertyName += fileCount++;
}
if (options.binaryPropertyName) {
binaryPropertyName = `${options.binaryPropertyName}${count}`;
}
const fileJson = file.toJSON();
returnItem.binary![binaryPropertyName] = await this.nodeHelpers.copyBinaryFile(
file.path,
fileJson.name || fileJson.filename,
fileJson.type as string,
);
count += 1;
}
}
resolve({
workflowData: [[returnItem]],
});
});
});
} }
if (options.binaryData === true) { if (options.binaryData) {
const binaryFile = await tmpFile({ prefix: 'n8n-webhook-' }); return this.handleBinaryData(context);
try {
await pipeline(req, fs.createWriteStream(binaryFile.path));
const returnItem: INodeExecutionData = {
binary: {},
json: {
headers,
params: this.getParamsData(),
query: this.getQueryData(),
body: this.getBodyData(),
},
};
const binaryPropertyName = (options.binaryPropertyName || 'data') as string;
returnItem.binary![binaryPropertyName] = await this.nodeHelpers.copyBinaryFile(
binaryFile.path,
mimeType,
);
return {
workflowData: [[returnItem]],
};
} catch (error) {
throw new NodeOperationError(this.getNode(), error as Error);
} finally {
await binaryFile.cleanup();
}
} }
const response: INodeExecutionData = { const response: INodeExecutionData = {
json: { json: {
headers, headers: req.headers,
params: this.getParamsData(), params: req.params,
query: this.getQueryData(), query: req.query,
body: this.getBodyData(), body: req.body,
}, },
binary: options.rawBody
? {
data: {
data: req.rawBody.toString(BINARY_ENCODING),
mimeType,
},
}
: undefined,
}; };
if (options.rawBody) {
response.binary = {
data: {
data: req.rawBody.toString(BINARY_ENCODING),
mimeType,
},
};
}
let webhookResponse: string | undefined;
if (options.responseData) {
webhookResponse = options.responseData as string;
}
return { return {
webhookResponse, webhookResponse: options.responseData,
workflowData: [[response]], workflowData: [[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();
if (authentication === 'basicAuth') {
// Basic authorization is needed to call webhook
let expectedAuth: ICredentialDataDecryptedObject | undefined;
try {
expectedAuth = await context.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 context.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);
}
}
}
private async handleFormData(context: IWebhookFunctions) {
const req = context.getRequestObject();
const options = context.getNodeParameter('options', {}) as IDataObject;
const form = new formidable.IncomingForm({ multiples: true });
return new Promise<IWebhookResponseData>((resolve, _reject) => {
form.parse(req, async (err, data, files) => {
const returnItem: INodeExecutionData = {
binary: {},
json: {
headers: req.headers,
params: req.params,
query: req.query,
body: data,
},
};
let count = 0;
for (const xfile of Object.keys(files)) {
const processFiles: formidable.File[] = [];
let multiFile = false;
if (Array.isArray(files[xfile])) {
processFiles.push(...(files[xfile] as formidable.File[]));
multiFile = true;
} else {
processFiles.push(files[xfile] as formidable.File);
}
let fileCount = 0;
for (const file of processFiles) {
let binaryPropertyName = xfile;
if (binaryPropertyName.endsWith('[]')) {
binaryPropertyName = binaryPropertyName.slice(0, -2);
}
if (multiFile) {
binaryPropertyName += fileCount++;
}
if (options.binaryPropertyName) {
binaryPropertyName = `${options.binaryPropertyName}${count}`;
}
const fileJson = file.toJSON();
returnItem.binary![binaryPropertyName] = await context.nodeHelpers.copyBinaryFile(
file.path,
fileJson.name || fileJson.filename,
fileJson.type as string,
);
count += 1;
}
}
resolve({ workflowData: [[returnItem]] });
});
});
}
private async handleBinaryData(context: IWebhookFunctions): Promise<IWebhookResponseData> {
const req = context.getRequestObject();
const options = context.getNodeParameter('options', {}) as IDataObject;
const binaryFile = await tmpFile({ prefix: 'n8n-webhook-' });
try {
await pipeline(req, fs.createWriteStream(binaryFile.path));
const returnItem: INodeExecutionData = {
binary: {},
json: {
headers: req.headers,
params: req.params,
query: req.query,
body: req.body,
},
};
const binaryPropertyName = (options.binaryPropertyName || 'data') as string;
returnItem.binary![binaryPropertyName] = await context.nodeHelpers.copyBinaryFile(
binaryFile.path,
req.headers['content-type'] ?? 'application/octet-stream',
);
return { workflowData: [[returnItem]] };
} catch (error) {
throw new NodeOperationError(context.getNode(), error as Error);
} finally {
await binaryFile.cleanup();
}
}
} }

View file

@ -0,0 +1,341 @@
import type { INodeProperties, INodeTypeDescription, IWebhookDescription } from 'n8n-workflow';
export const defaultWebhookDescription: IWebhookDescription = {
name: 'default',
httpMethod: '={{$parameter["httpMethod"]}}',
isFullPath: true,
responseCode: '={{$parameter["responseCode"]}}',
responseMode: '={{$parameter["responseMode"]}}',
responseData:
'={{$parameter["responseData"] || ($parameter.options.noResponseBody ? "noData" : undefined) }}',
responseBinaryPropertyName: '={{$parameter["responseBinaryPropertyName"]}}',
responseContentType: '={{$parameter["options"]["responseContentType"]}}',
responsePropertyName: '={{$parameter["options"]["responsePropertyName"]}}',
responseHeaders: '={{$parameter["options"]["responseHeaders"]}}',
path: '={{$parameter["path"]}}',
};
export const credentialsProperty = (
propertyName: string = 'authentication',
): INodeTypeDescription['credentials'] => [
{
name: 'httpBasicAuth',
required: true,
displayOptions: {
show: {
[propertyName]: ['basicAuth'],
},
},
},
{
name: 'httpHeaderAuth',
required: true,
displayOptions: {
show: {
[propertyName]: ['headerAuth'],
},
},
},
];
export const authenticationProperty = (
propertyName: string = 'authentication',
): INodeProperties => ({
displayName: 'Authentication',
name: propertyName,
type: 'options',
options: [
{
name: 'Basic Auth',
value: 'basicAuth',
},
{
name: 'Header Auth',
value: 'headerAuth',
},
{
name: 'None',
value: 'none',
},
],
default: 'none',
description: 'The way to authenticate',
});
export const httpMethodsProperty: INodeProperties = {
displayName: 'HTTP Method',
name: 'httpMethod',
type: 'options',
options: [
{
name: 'DELETE',
value: 'DELETE',
},
{
name: 'GET',
value: 'GET',
},
{
name: 'HEAD',
value: 'HEAD',
},
{
name: 'PATCH',
value: 'PATCH',
},
{
name: 'POST',
value: 'POST',
},
{
name: 'PUT',
value: 'PUT',
},
],
default: 'GET',
description: 'The HTTP method to listen to',
};
export const responseCodeProperty: INodeProperties = {
displayName: 'Response Code',
name: 'responseCode',
type: 'number',
displayOptions: {
hide: {
responseMode: ['responseNode'],
},
},
typeOptions: {
minValue: 100,
maxValue: 599,
},
default: 200,
description: 'The HTTP Response code to return',
};
export const responseModeProperty: INodeProperties = {
displayName: 'Respond',
name: 'responseMode',
type: 'options',
options: [
{
name: 'Immediately',
value: 'onReceived',
description: 'As soon as this node executes',
},
{
name: 'When Last Node Finishes',
value: 'lastNode',
description: 'Returns data of the last-executed node',
},
{
name: "Using 'Respond to Webhook' Node",
value: 'responseNode',
description: 'Response defined in that node',
},
],
default: 'onReceived',
description: 'When and how to respond to the webhook',
};
export const responseDataProperty: INodeProperties = {
displayName: 'Response Data',
name: 'responseData',
type: 'options',
displayOptions: {
show: {
responseMode: ['lastNode'],
},
},
options: [
{
name: 'All Entries',
value: 'allEntries',
description: 'Returns all the entries of the last node. Always returns an array.',
},
{
name: 'First Entry JSON',
value: 'firstEntryJson',
description:
'Returns the JSON data of the first entry of the last node. Always returns a JSON object.',
},
{
name: 'First Entry Binary',
value: 'firstEntryBinary',
description:
'Returns the binary data of the first entry of the last node. Always returns a binary file.',
},
{
name: 'No Response Body',
value: 'noData',
description: 'Returns without a body',
},
],
default: 'firstEntryJson',
description:
'What data should be returned. If it should return all items as an array or only the first item as object.',
};
export const responseBinaryPropertyNameProperty: INodeProperties = {
displayName: 'Property Name',
name: 'responseBinaryPropertyName',
type: 'string',
required: true,
default: 'data',
displayOptions: {
show: {
responseData: ['firstEntryBinary'],
},
},
description: 'Name of the binary property to return',
};
export const optionsProperty: INodeProperties = {
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Binary Data',
name: 'binaryData',
type: 'boolean',
displayOptions: {
show: {
'/httpMethod': ['PATCH', 'PUT', 'POST'],
},
},
default: false,
description: 'Whether the webhook will receive binary data',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
binaryData: [true],
},
},
description:
'Name of the binary property to write the data of the received file to. If the data gets received via "Form-Data Multipart" it will be the prefix and a number starting with 0 will be attached to it.',
},
{
displayName: 'Ignore Bots',
name: 'ignoreBots',
type: 'boolean',
default: false,
description: 'Whether to ignore requests from bots like link previewers and web crawlers',
},
{
displayName: 'No Response Body',
name: 'noResponseBody',
type: 'boolean',
default: false,
description: 'Whether to send any body in the response',
displayOptions: {
hide: {
rawBody: [true],
},
show: {
'/responseMode': ['onReceived'],
},
},
},
{
displayName: 'Raw Body',
name: 'rawBody',
type: 'boolean',
displayOptions: {
hide: {
binaryData: [true],
noResponseBody: [true],
},
},
default: false,
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
description: 'Raw body (binary)',
},
{
displayName: 'Response Data',
name: 'responseData',
type: 'string',
displayOptions: {
show: {
'/responseMode': ['onReceived'],
},
hide: {
noResponseBody: [true],
},
},
default: '',
placeholder: 'success',
description: 'Custom response data to send',
},
{
displayName: 'Response Content-Type',
name: 'responseContentType',
type: 'string',
displayOptions: {
show: {
'/responseData': ['firstEntryJson'],
'/responseMode': ['lastNode'],
},
},
default: '',
placeholder: 'application/xml',
// eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-json
description:
'Set a custom content-type to return if another one as the "application/json" should be returned',
},
{
displayName: 'Response Headers',
name: 'responseHeaders',
placeholder: 'Add Response Header',
description: 'Add headers to the webhook response',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'entries',
displayName: 'Entries',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the header',
},
],
},
],
},
{
displayName: 'Property Name',
name: 'responsePropertyName',
type: 'string',
displayOptions: {
show: {
'/responseData': ['firstEntryJson'],
'/responseMode': ['lastNode'],
},
},
default: 'data',
description: 'Name of the property to return the data of instead of the whole JSON',
},
],
};

View file

@ -0,0 +1,13 @@
export class WebhookAuthorizationError extends Error {
constructor(readonly responseCode: number, message?: string) {
if (message === undefined) {
message = 'Authorization problem!';
if (responseCode === 401) {
message = 'Authorization is required!';
} else if (responseCode === 403) {
message = 'Authorization data is wrong!';
}
}
super(message);
}
}

View file

@ -1258,6 +1258,16 @@ export interface INodeType {
}; };
} }
/**
* This class serves as the base for all nodes using the new context API
* having this as a class enables us to identify these instances at runtime
*/
export abstract class Node {
abstract description: INodeTypeDescription;
execute?(context: IExecuteFunctions): Promise<INodeExecutionData[][]>;
webhook?(context: IWebhookFunctions): Promise<IWebhookResponseData>;
}
export interface IVersionedNodeType { export interface IVersionedNodeType {
nodeVersions: { nodeVersions: {
[key: number]: INodeType; [key: number]: INodeType;

View file

@ -49,6 +49,7 @@ import type {
IRunNodeResponse, IRunNodeResponse,
NodeParameterValueType, NodeParameterValueType,
} from './Interfaces'; } from './Interfaces';
import { Node } from './Interfaces';
import type { IDeferredPromise } from './DeferredPromise'; import type { IDeferredPromise } from './DeferredPromise';
import * as NodeHelpers from './NodeHelpers'; import * as NodeHelpers from './NodeHelpers';
@ -1137,14 +1138,14 @@ export class Workflow {
throw new Error(`The node "${node.name}" does not have any webhooks defined.`); throw new Error(`The node "${node.name}" does not have any webhooks defined.`);
} }
const thisArgs = nodeExecuteFunctions.getExecuteWebhookFunctions( const context = nodeExecuteFunctions.getExecuteWebhookFunctions(
this, this,
node, node,
additionalData, additionalData,
mode, mode,
webhookData, webhookData,
); );
return nodeType.webhook.call(thisArgs); return nodeType instanceof Node ? nodeType.webhook(context) : nodeType.webhook.call(context);
} }
/** /**
@ -1255,7 +1256,7 @@ export class Workflow {
return { data: [promiseResults] }; return { data: [promiseResults] };
} }
} else if (nodeType.execute) { } else if (nodeType.execute) {
const thisArgs = nodeExecuteFunctions.getExecuteFunctions( const context = nodeExecuteFunctions.getExecuteFunctions(
this, this,
runExecutionData, runExecutionData,
runIndex, runIndex,
@ -1266,7 +1267,11 @@ export class Workflow {
executionData, executionData,
mode, mode,
); );
return { data: await nodeType.execute.call(thisArgs) }; const data =
nodeType instanceof Node
? await nodeType.execute(context)
: await nodeType.execute.call(context);
return { data };
} else if (nodeType.poll) { } else if (nodeType.poll) {
if (mode === 'manual') { if (mode === 'manual') {
// In manual mode run the poll function // In manual mode run the poll function