mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
Also, 1. Consistent CORS support ~on all three webhook types~ waiting webhooks never supported CORS. I'll fix that in another PR 2. [Fixes binary-data handling when request body is text, json, or xml](https://linear.app/n8n/issue/NODE-505/webhook-binary-data-handling-fails-for-textplain-files). 3. Reduced number of middleware that each request has to go through. 4. Removed the need to maintain webhook endpoints in the auth-exception list. 5. Skip all middlewares (apart from `compression`) on Webhook routes. 6. move `multipart/form-data` support out of individual nodes 7. upgrade `formidable` 8. fix the filenames on binary-data in webhooks nodes 9. add unit tests and integration tests for webhook request handling, and increase test coverage
293 lines
7.9 KiB
TypeScript
293 lines
7.9 KiB
TypeScript
import type express from 'express';
|
|
import { Service } from 'typedi';
|
|
|
|
import type {
|
|
IWebhookData,
|
|
IWorkflowExecuteAdditionalData,
|
|
IHttpRequestMethods,
|
|
Workflow,
|
|
WorkflowActivateMode,
|
|
WorkflowExecuteMode,
|
|
} from 'n8n-workflow';
|
|
|
|
import { ActiveWebhooks } from '@/ActiveWebhooks';
|
|
import type {
|
|
IResponseCallbackData,
|
|
IWebhookManager,
|
|
IWorkflowDb,
|
|
WebhookRequest,
|
|
} from '@/Interfaces';
|
|
import { Push } from '@/push';
|
|
import * as ResponseHelper from '@/ResponseHelper';
|
|
import * as WebhookHelpers from '@/WebhookHelpers';
|
|
import { webhookNotFoundErrorMessage } from './utils';
|
|
|
|
const WEBHOOK_TEST_UNREGISTERED_HINT =
|
|
"Click the 'Execute workflow' button on the canvas, then try again. (In test mode, the webhook only works for one call after you click this button)";
|
|
|
|
@Service()
|
|
export class TestWebhooks implements IWebhookManager {
|
|
private testWebhookData: {
|
|
[key: string]: {
|
|
sessionId?: string;
|
|
timeout: NodeJS.Timeout;
|
|
workflowData: IWorkflowDb;
|
|
workflow: Workflow;
|
|
destinationNode?: string;
|
|
};
|
|
} = {};
|
|
|
|
constructor(
|
|
private activeWebhooks: ActiveWebhooks,
|
|
private push: Push,
|
|
) {
|
|
activeWebhooks.testWebhooks = true;
|
|
}
|
|
|
|
/**
|
|
* Executes a test-webhook and returns the data. It also makes sure that the
|
|
* data gets additionally send to the UI. After the request got handled it
|
|
* automatically remove the test-webhook.
|
|
*/
|
|
async executeWebhook(
|
|
request: WebhookRequest,
|
|
response: express.Response,
|
|
): Promise<IResponseCallbackData> {
|
|
const httpMethod = request.method;
|
|
let path = request.params.path;
|
|
|
|
// Remove trailing slash
|
|
if (path.endsWith('/')) {
|
|
path = path.slice(0, -1);
|
|
}
|
|
|
|
const { activeWebhooks, push, testWebhookData } = this;
|
|
|
|
let webhookData: IWebhookData | undefined = activeWebhooks.get(httpMethod, path);
|
|
|
|
// check if path is dynamic
|
|
if (webhookData === undefined) {
|
|
const pathElements = path.split('/');
|
|
const webhookId = pathElements.shift();
|
|
|
|
webhookData = activeWebhooks.get(httpMethod, pathElements.join('/'), webhookId);
|
|
if (webhookData === undefined) {
|
|
// The requested webhook is not registered
|
|
const methods = await this.getWebhookMethods(path);
|
|
throw new ResponseHelper.NotFoundError(
|
|
webhookNotFoundErrorMessage(path, httpMethod, methods),
|
|
WEBHOOK_TEST_UNREGISTERED_HINT,
|
|
);
|
|
}
|
|
|
|
path = webhookData.path;
|
|
// extracting params from path
|
|
path.split('/').forEach((ele, index) => {
|
|
if (ele.startsWith(':')) {
|
|
// write params to req.params
|
|
// @ts-ignore
|
|
request.params[ele.slice(1)] = pathElements[index];
|
|
}
|
|
});
|
|
}
|
|
|
|
const { workflowId } = webhookData;
|
|
const webhookKey = `${activeWebhooks.getWebhookKey(
|
|
webhookData.httpMethod,
|
|
webhookData.path,
|
|
webhookData.webhookId,
|
|
)}|${workflowId}`;
|
|
|
|
// TODO: Clean that duplication up one day and improve code generally
|
|
if (testWebhookData[webhookKey] === undefined) {
|
|
// The requested webhook is not registered
|
|
const methods = await this.getWebhookMethods(path);
|
|
throw new ResponseHelper.NotFoundError(
|
|
webhookNotFoundErrorMessage(path, httpMethod, methods),
|
|
WEBHOOK_TEST_UNREGISTERED_HINT,
|
|
);
|
|
}
|
|
|
|
const { destinationNode, sessionId, workflow, workflowData, timeout } =
|
|
testWebhookData[webhookKey];
|
|
|
|
// Get the node which has the webhook defined to know where to start from and to
|
|
// get additional data
|
|
const workflowStartNode = workflow.getNode(webhookData.node);
|
|
if (workflowStartNode === null) {
|
|
throw new ResponseHelper.NotFoundError('Could not find node to process webhook.');
|
|
}
|
|
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
const executionMode = 'manual';
|
|
const executionId = await WebhookHelpers.executeWebhook(
|
|
workflow,
|
|
webhookData!,
|
|
workflowData,
|
|
workflowStartNode,
|
|
executionMode,
|
|
sessionId,
|
|
undefined,
|
|
undefined,
|
|
request,
|
|
response,
|
|
(error: Error | null, data: IResponseCallbackData) => {
|
|
if (error !== null) reject(error);
|
|
else resolve(data);
|
|
},
|
|
destinationNode,
|
|
);
|
|
|
|
// The workflow did not run as the request was probably setup related
|
|
// or a ping so do not resolve the promise and wait for the real webhook
|
|
// request instead.
|
|
if (executionId === undefined) return;
|
|
|
|
// Inform editor-ui that webhook got received
|
|
if (sessionId !== undefined) {
|
|
push.send('testWebhookReceived', { workflowId, executionId }, sessionId);
|
|
}
|
|
} catch {}
|
|
|
|
// Delete webhook also if an error is thrown
|
|
if (timeout) clearTimeout(timeout);
|
|
delete testWebhookData[webhookKey];
|
|
|
|
await activeWebhooks.removeWorkflow(workflow);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets all request methods associated with a single test webhook
|
|
*/
|
|
async getWebhookMethods(path: string): Promise<IHttpRequestMethods[]> {
|
|
const webhookMethods = this.activeWebhooks.getWebhookMethods(path);
|
|
if (!webhookMethods.length) {
|
|
// The requested webhook is not registered
|
|
throw new ResponseHelper.NotFoundError(
|
|
webhookNotFoundErrorMessage(path),
|
|
WEBHOOK_TEST_UNREGISTERED_HINT,
|
|
);
|
|
}
|
|
|
|
return webhookMethods;
|
|
}
|
|
|
|
/**
|
|
* Checks if it has to wait for webhook data to execute the workflow.
|
|
* If yes it waits for it and resolves with the result of the workflow if not it simply resolves with undefined
|
|
*/
|
|
async needsWebhookData(
|
|
workflowData: IWorkflowDb,
|
|
workflow: Workflow,
|
|
additionalData: IWorkflowExecuteAdditionalData,
|
|
mode: WorkflowExecuteMode,
|
|
activation: WorkflowActivateMode,
|
|
sessionId?: string,
|
|
destinationNode?: string,
|
|
): Promise<boolean> {
|
|
const webhooks = WebhookHelpers.getWorkflowWebhooks(
|
|
workflow,
|
|
additionalData,
|
|
destinationNode,
|
|
true,
|
|
);
|
|
if (!webhooks.find((webhook) => webhook.webhookDescription.restartWebhook !== true)) {
|
|
// No webhooks found to start a workflow
|
|
return false;
|
|
}
|
|
|
|
if (workflow.id === undefined) {
|
|
throw new Error('Webhooks can only be added for saved workflows as an id is needed!');
|
|
}
|
|
|
|
// Remove test-webhooks automatically if they do not get called (after 120 seconds)
|
|
const timeout = setTimeout(() => {
|
|
this.cancelTestWebhook(workflowData.id);
|
|
}, 120000);
|
|
|
|
const { activeWebhooks, testWebhookData } = this;
|
|
|
|
let key: string;
|
|
const activatedKey: string[] = [];
|
|
|
|
for (const webhookData of webhooks) {
|
|
key = `${activeWebhooks.getWebhookKey(
|
|
webhookData.httpMethod,
|
|
webhookData.path,
|
|
webhookData.webhookId,
|
|
)}|${workflowData.id}`;
|
|
|
|
activatedKey.push(key);
|
|
|
|
testWebhookData[key] = {
|
|
sessionId,
|
|
timeout,
|
|
workflow,
|
|
workflowData,
|
|
destinationNode,
|
|
};
|
|
|
|
try {
|
|
await activeWebhooks.add(workflow, webhookData, mode, activation);
|
|
} catch (error) {
|
|
activatedKey.forEach((deleteKey) => delete testWebhookData[deleteKey]);
|
|
|
|
await activeWebhooks.removeWorkflow(workflow);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Removes a test webhook of the workflow with the given id
|
|
*
|
|
*/
|
|
cancelTestWebhook(workflowId: string): boolean {
|
|
let foundWebhook = false;
|
|
const { activeWebhooks, push, testWebhookData } = this;
|
|
|
|
for (const webhookKey of Object.keys(testWebhookData)) {
|
|
const { sessionId, timeout, workflow, workflowData } = testWebhookData[webhookKey];
|
|
|
|
if (workflowData.id !== workflowId) {
|
|
continue;
|
|
}
|
|
|
|
clearTimeout(timeout);
|
|
|
|
// Inform editor-ui that webhook got received
|
|
if (sessionId !== undefined) {
|
|
try {
|
|
push.send('testWebhookDeleted', { workflowId }, sessionId);
|
|
} catch {
|
|
// Could not inform editor, probably is not connected anymore. So simply go on.
|
|
}
|
|
}
|
|
|
|
// Remove the webhook
|
|
delete testWebhookData[webhookKey];
|
|
|
|
if (!foundWebhook) {
|
|
// As it removes all webhooks of the workflow execute only once
|
|
void activeWebhooks.removeWorkflow(workflow);
|
|
}
|
|
|
|
foundWebhook = true;
|
|
}
|
|
|
|
return foundWebhook;
|
|
}
|
|
|
|
/**
|
|
* Removes all the currently active test webhooks
|
|
*/
|
|
async removeAll(): Promise<void> {
|
|
const workflows = Object.values(this.testWebhookData).map(({ workflow }) => workflow);
|
|
return this.activeWebhooks.removeAll(workflows);
|
|
}
|
|
}
|