From 9a06d0fffcae355a8c3f2b5c303443150d711cee Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 20 Feb 2022 10:30:01 +0100 Subject: [PATCH] :sparkles: Add DELETE, PATCH and PUT request support to Webhooks --- packages/cli/src/Server.ts | 127 ++++-------- packages/cli/src/WebhookServer.ts | 195 ++++-------------- packages/nodes-base/nodes/Wait/Wait.node.ts | 14 ++ .../nodes-base/nodes/Webhook/Webhook.node.ts | 14 ++ packages/workflow/src/Interfaces.ts | 2 +- 5 files changed, 103 insertions(+), 249 deletions(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 7ee1334a22..fa2399dd62 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -72,6 +72,7 @@ import { IWorkflowBase, LoggerProxy, NodeHelpers, + WebhookHttpMethod, Workflow, WorkflowExecuteMode, } from 'n8n-workflow'; @@ -202,6 +203,8 @@ class App { presetCredentialsLoaded: boolean; + webhookMethods: WebhookHttpMethod[]; + constructor() { this.app = express(); @@ -237,6 +240,8 @@ class App { this.presetCredentialsLoaded = false; this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string; + this.webhookMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']; + const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl(); const telemetrySettings: ITelemetrySettings = { @@ -2704,8 +2709,8 @@ class App { WebhookServer.registerProductionWebhooks.apply(this); } - // HEAD webhook requests (test for UI) - this.app.head( + // Register all webhook requests (test for UI) + this.app.all( `/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => { // Cut away the "/webhook-test/" to get the registred part of the url @@ -2713,98 +2718,36 @@ class App { this.endpointWebhookTest.length + 2, ); + const method = req.method.toUpperCase() as WebhookHttpMethod; + + if (method === 'OPTIONS') { + let allowedMethods: string[]; + try { + allowedMethods = await this.testWebhooks.getWebhookMethods(requestUrl); + allowedMethods.push('OPTIONS'); + + // Add custom "Allow" header to satisfy OPTIONS response. + res.append('Allow', allowedMethods); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } + + ResponseHelper.sendSuccessResponse(res, {}, true, 204); + return; + } + + if (!this.webhookMethods.includes(method)) { + ResponseHelper.sendErrorResponse( + res, + new Error(`The method ${method} is not supported.`), + ); + return; + } + let response; try { - response = await this.testWebhooks.callTestWebhook('HEAD', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } - - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } - - ResponseHelper.sendSuccessResponse( - res, - response.data, - true, - response.responseCode, - response.headers, - ); - }, - ); - - // HEAD webhook requests (test for UI) - this.app.options( - `/${this.endpointWebhookTest}/*`, - async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook-test/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( - this.endpointWebhookTest.length + 2, - ); - - let allowedMethods: string[]; - try { - allowedMethods = await this.testWebhooks.getWebhookMethods(requestUrl); - allowedMethods.push('OPTIONS'); - - // Add custom "Allow" header to satisfy OPTIONS response. - res.append('Allow', allowedMethods); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } - - ResponseHelper.sendSuccessResponse(res, {}, true, 204); - }, - ); - - // GET webhook requests (test for UI) - this.app.get( - `/${this.endpointWebhookTest}/*`, - async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook-test/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( - this.endpointWebhookTest.length + 2, - ); - - let response; - try { - response = await this.testWebhooks.callTestWebhook('GET', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } - - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } - - ResponseHelper.sendSuccessResponse( - res, - response.data, - true, - response.responseCode, - response.headers, - ); - }, - ); - - // POST webhook requests (test for UI) - this.app.post( - `/${this.endpointWebhookTest}/*`, - async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook-test/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( - this.endpointWebhookTest.length + 2, - ); - - let response; - try { - response = await this.testWebhooks.callTestWebhook('POST', requestUrl, req, res); + response = await this.testWebhooks.callTestWebhook(method, requestUrl, req, res); } catch (error) { ResponseHelper.sendErrorResponse(res, error); return; diff --git a/packages/cli/src/WebhookServer.ts b/packages/cli/src/WebhookServer.ts index c63526bfcc..45bf8c16e1 100644 --- a/packages/cli/src/WebhookServer.ts +++ b/packages/cli/src/WebhookServer.ts @@ -16,6 +16,7 @@ import * as _ from 'lodash'; import * as compression from 'compression'; // eslint-disable-next-line import/no-extraneous-dependencies import * as parseUrl from 'parseurl'; +import { WebhookHttpMethod } from 'n8n-workflow'; // eslint-disable-next-line import/no-cycle import { ActiveExecutions, @@ -41,8 +42,8 @@ export function registerProductionWebhooks() { // Regular Webhooks // ---------------------------------------- - // HEAD webhook requests - this.app.head( + // Register all webhook requests + this.app.all( `/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { // Cut away the "/webhook/" to get the registred part of the url @@ -50,99 +51,34 @@ export function registerProductionWebhooks() { this.endpointWebhook.length + 2, ); + const method = req.method.toUpperCase() as WebhookHttpMethod; + + if (method === 'OPTIONS') { + let allowedMethods: string[]; + try { + allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl); + allowedMethods.push('OPTIONS'); + + // Add custom "Allow" header to satisfy OPTIONS response. + res.append('Allow', allowedMethods); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } + + ResponseHelper.sendSuccessResponse(res, {}, true, 204); + return; + } + + if (!this.webhookMethods.includes(method)) { + ResponseHelper.sendErrorResponse(res, new Error(`The method ${method} is not supported.`)); + return; + } + let response; try { // eslint-disable-next-line @typescript-eslint/no-unsafe-call - response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } - - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } - - ResponseHelper.sendSuccessResponse( - res, - response.data, - true, - response.responseCode, - response.headers, - ); - }, - ); - - // OPTIONS webhook requests - this.app.options( - `/${this.endpointWebhook}/*`, - async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( - this.endpointWebhook.length + 2, - ); - - let allowedMethods: string[]; - try { - allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl); - allowedMethods.push('OPTIONS'); - - // Add custom "Allow" header to satisfy OPTIONS response. - res.append('Allow', allowedMethods); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } - - ResponseHelper.sendSuccessResponse(res, {}, true, 204); - }, - ); - - // GET webhook requests - this.app.get( - `/${this.endpointWebhook}/*`, - async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( - this.endpointWebhook.length + 2, - ); - - let response; - try { - response = await this.activeWorkflowRunner.executeWebhook('GET', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } - - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } - - ResponseHelper.sendSuccessResponse( - res, - response.data, - true, - response.responseCode, - response.headers, - ); - }, - ); - - // POST webhook requests - this.app.post( - `/${this.endpointWebhook}/*`, - async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( - this.endpointWebhook.length + 2, - ); - - let response; - try { - response = await this.activeWorkflowRunner.executeWebhook('POST', requestUrl, req, res); + response = await this.activeWorkflowRunner.executeWebhook(method, requestUrl, req, res); } catch (error) { ResponseHelper.sendErrorResponse(res, error); return; @@ -169,8 +105,8 @@ export function registerProductionWebhooks() { const waitingWebhooks = new WaitingWebhooks(); - // HEAD webhook-waiting requests - this.app.head( + // Register all webhook-waiting requests + this.app.all( `/${this.endpointWebhookWaiting}/*`, async (req: express.Request, res: express.Response) => { // Cut away the "/webhook-waiting/" to get the registred part of the url @@ -178,73 +114,20 @@ export function registerProductionWebhooks() { this.endpointWebhookWaiting.length + 2, ); - let response; - try { - response = await waitingWebhooks.executeWebhook('HEAD', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); + const method = req.method.toUpperCase() as WebhookHttpMethod; + + // TOOD: Add support for OPTIONS in the future + // if (method === 'OPTIONS') { + // } + + if (!this.webhookMethods.includes(method)) { + ResponseHelper.sendErrorResponse(res, new Error(`The method ${method} is not supported.`)); return; } - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } - - ResponseHelper.sendSuccessResponse( - res, - response.data, - true, - response.responseCode, - response.headers, - ); - }, - ); - - // GET webhook-waiting requests - this.app.get( - `/${this.endpointWebhookWaiting}/*`, - async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook-waiting/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( - this.endpointWebhookWaiting.length + 2, - ); - let response; try { - response = await waitingWebhooks.executeWebhook('GET', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } - - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } - - ResponseHelper.sendSuccessResponse( - res, - response.data, - true, - response.responseCode, - response.headers, - ); - }, - ); - - // POST webhook-waiting requests - this.app.post( - `/${this.endpointWebhookWaiting}/*`, - async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook-waiting/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( - this.endpointWebhookWaiting.length + 2, - ); - - let response; - try { - response = await waitingWebhooks.executeWebhook('POST', requestUrl, req, res); + response = await waitingWebhooks.executeWebhook(method, requestUrl, req, res); } catch (error) { ResponseHelper.sendErrorResponse(res, error); return; diff --git a/packages/nodes-base/nodes/Wait/Wait.node.ts b/packages/nodes-base/nodes/Wait/Wait.node.ts index 20280377bd..5ca7a012e3 100644 --- a/packages/nodes-base/nodes/Wait/Wait.node.ts +++ b/packages/nodes-base/nodes/Wait/Wait.node.ts @@ -250,6 +250,10 @@ export class Wait implements INodeType { }, }, options: [ + { + name: 'DELETE', + value: 'DELETE', + }, { name: 'GET', value: 'GET', @@ -258,10 +262,18 @@ export class Wait implements INodeType { 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', @@ -514,6 +526,8 @@ export class Wait implements INodeType { displayOptions: { show: { '/httpMethod': [ + 'PATCH', + 'PUT', 'POST', ], }, diff --git a/packages/nodes-base/nodes/Webhook/Webhook.node.ts b/packages/nodes-base/nodes/Webhook/Webhook.node.ts index 352c3d2308..16256249bf 100644 --- a/packages/nodes-base/nodes/Webhook/Webhook.node.ts +++ b/packages/nodes-base/nodes/Webhook/Webhook.node.ts @@ -120,6 +120,10 @@ export class Webhook implements INodeType { name: 'httpMethod', type: 'options', options: [ + { + name: 'DELETE', + value: 'DELETE', + }, { name: 'GET', value: 'GET', @@ -128,10 +132,18 @@ export class Webhook implements INodeType { 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.', @@ -265,6 +277,8 @@ export class Webhook implements INodeType { displayOptions: { show: { '/httpMethod': [ + 'PATCH', + 'PUT', 'POST', ], }, diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 5bd066c17f..fa0c687265 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1178,7 +1178,7 @@ export interface IWorkflowMetadata { active: boolean; } -export type WebhookHttpMethod = 'GET' | 'POST' | 'HEAD' | 'OPTIONS'; +export type WebhookHttpMethod = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'OPTIONS'; export interface IWebhookResponseData { workflowData?: INodeExecutionData[][];