2023-07-04 07:17:50 -07:00
/* eslint-disable n8n-nodes-base/node-execute-block-wrong-error-thrown */
2023-08-01 08:32:30 -07:00
import { pipeline } from 'stream/promises' ;
import { createWriteStream } from 'fs' ;
2023-01-27 03:22:44 -08:00
import type {
2023-03-09 09:13:15 -08:00
IWebhookFunctions ,
2022-04-14 23:00:47 -07:00
ICredentialDataDecryptedObject ,
2019-06-23 03:35:23 -07:00
IDataObject ,
2019-12-21 17:03:24 -08:00
INodeExecutionData ,
2020-10-01 05:01:39 -07:00
INodeTypeDescription ,
2019-10-11 04:02:44 -07:00
IWebhookResponseData ,
2023-08-01 08:32:30 -07:00
MultiPartFormData ,
2019-06-23 03:35:23 -07:00
} from 'n8n-workflow' ;
2023-07-04 07:17:50 -07:00
import { BINARY_ENCODING , NodeOperationError , Node } from 'n8n-workflow' ;
2019-06-23 03:35:23 -07:00
2023-08-01 08:32:30 -07:00
import { v4 as uuid } from 'uuid' ;
2022-11-24 07:54:43 -08:00
import basicAuth from 'basic-auth' ;
2022-04-08 14:32:08 -07:00
import isbot from 'isbot' ;
2022-11-24 07:54:43 -08:00
import { file as tmpFile } from 'tmp-promise' ;
2023-07-04 07:17:50 -07:00
import {
authenticationProperty ,
credentialsProperty ,
defaultWebhookDescription ,
httpMethodsProperty ,
optionsProperty ,
responseBinaryPropertyNameProperty ,
responseCodeProperty ,
responseDataProperty ,
responseModeProperty ,
} from './description' ;
import { WebhookAuthorizationError } from './error' ;
2021-12-09 05:28:14 -08:00
2023-07-04 07:17:50 -07:00
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' ] ,
2023-12-06 07:46:40 -08:00
version : [ 1 , 1.1 ] ,
2021-07-03 05:40:16 -07:00
description : 'Starts the workflow when a webhook is called' ,
2021-11-26 03:42:08 -08:00
eventTriggerDescription : 'Waiting for you to call the Test URL' ,
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' ,
} ,
2023-11-22 08:49:56 -08:00
supportsCORS : true ,
2022-06-20 12:39:24 -07:00
triggerPanel : {
header : '' ,
executionsHelp : {
2022-08-17 08:50:24 -07:00
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.' ,
2022-06-20 12:39:24 -07:00
} ,
2022-08-17 08:50: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.' ,
2022-06-20 12:39:24 -07:00
} ,
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 : [ 'main' ] ,
2023-07-04 07:17:50 -07:00
credentials : credentialsProperty ( this . authPropertyName ) ,
webhooks : [ defaultWebhookDescription ] ,
2019-06-23 03:35:23 -07:00
properties : [
2023-07-04 07:17:50 -07:00
authenticationProperty ( this . authPropertyName ) ,
httpMethodsProperty ,
2019-06-23 03:35:23 -07:00
{
displayName : 'Path' ,
name : 'path' ,
type : 'string' ,
default : '' ,
2019-08-28 08:03:35 -07:00
placeholder : 'webhook' ,
2019-06-23 03:35:23 -07:00
required : true ,
2022-05-06 14:01:25 -07:00
description : 'The path to listen to' ,
2019-06-23 03:35:23 -07:00
} ,
2023-07-04 07:17:50 -07:00
responseModeProperty ,
2021-11-05 09:45:51 -07:00
{
2022-08-17 08:50:24 -07:00
displayName :
2022-09-29 03:33:16 -07:00
'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>' ,
2021-11-05 09:45:51 -07:00
name : 'webhookNotice' ,
type : 'notice' ,
displayOptions : {
show : {
2022-08-17 08:50:24 -07:00
responseMode : [ 'responseNode' ] ,
2021-11-05 09:45:51 -07:00
} ,
} ,
default : '' ,
} ,
2023-07-04 07:17:50 -07:00
responseCodeProperty ,
responseDataProperty ,
responseBinaryPropertyNameProperty ,
optionsProperty ,
2019-06-23 03:35:23 -07:00
] ,
} ;
2023-07-04 07:17:50 -07:00
async webhook ( context : IWebhookFunctions ) : Promise < IWebhookResponseData > {
const options = context . getNodeParameter ( 'options' , { } ) as {
binaryData : boolean ;
ignoreBots : boolean ;
rawBody : Buffer ;
responseData? : string ;
} ;
const req = context . getRequestObject ( ) ;
const resp = context . getResponseObject ( ) ;
try {
if ( options . ignoreBots && isbot ( req . headers [ 'user-agent' ] ) )
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 ;
2021-12-09 05:28:14 -08:00
}
2023-07-04 07:17:50 -07:00
if ( options . binaryData ) {
return this . handleBinaryData ( context ) ;
}
2023-08-01 08:32:30 -07:00
if ( req . contentType === 'multipart/form-data' ) {
return this . handleFormData ( context ) ;
}
2023-12-06 07:46:40 -08:00
const nodeVersion = context . getNode ( ) . typeVersion ;
if ( nodeVersion > 1 && ! req . body && ! options . rawBody ) {
try {
return await this . handleBinaryData ( context ) ;
} catch ( error ) { }
}
if ( options . rawBody && ! req . rawBody ) {
await req . readRawBody ( ) ;
}
2023-07-04 07:17:50 -07:00
const response : INodeExecutionData = {
json : {
headers : req.headers ,
2023-08-25 04:28:32 -07:00
params : req.params ,
2023-07-04 07:17:50 -07:00
query : req.query ,
body : req.body ,
} ,
binary : options.rawBody
? {
data : {
2023-12-06 07:46:40 -08:00
data : ( req . rawBody ? ? '' ) . toString ( BINARY_ENCODING ) ,
2023-08-01 08:32:30 -07:00
mimeType : req.contentType ? ? 'application/json' ,
2023-07-04 07:17:50 -07:00
} ,
}
: undefined ,
} ;
return {
webhookResponse : options.responseData ,
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 ( ) ;
2019-06-23 03:35:23 -07:00
if ( authentication === 'basicAuth' ) {
// Basic authorization is needed to call webhook
2023-07-04 07:17:50 -07:00
let expectedAuth : ICredentialDataDecryptedObject | undefined ;
2022-04-14 23:00:47 -07:00
try {
2023-07-04 07:17:50 -07:00
expectedAuth = await context . getCredentials ( 'httpBasicAuth' ) ;
} catch { }
2019-06-23 03:35:23 -07:00
2023-07-04 07:17:50 -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
2023-07-04 07:17:50 -07:00
throw new WebhookAuthorizationError ( 500 , 'No authentication data defined on node!' ) ;
2019-06-23 03:35:23 -07:00
}
2023-07-04 07:17:50 -07:00
const providedAuth = basicAuth ( req ) ;
// Authorization data is missing
if ( ! providedAuth ) throw new WebhookAuthorizationError ( 401 ) ;
2019-06-23 03:35:23 -07:00
2023-07-04 07:17:50 -07:00
if ( providedAuth . name !== expectedAuth . user || providedAuth . pass !== expectedAuth . password ) {
2019-06-23 03:35:23 -07:00
// Provided authentication data is wrong
2023-07-04 07:17:50 -07:00
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
2023-07-04 07:17:50 -07:00
let expectedAuth : ICredentialDataDecryptedObject | undefined ;
2022-04-14 23:00:47 -07:00
try {
2023-07-04 07:17:50 -07:00
expectedAuth = await context . getCredentials ( 'httpHeaderAuth' ) ;
} catch { }
2022-04-14 23:00:47 -07:00
2023-07-04 07:17:50 -07:00
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
2023-07-04 07:17:50 -07:00
throw new WebhookAuthorizationError ( 500 , 'No authentication data defined on node!' ) ;
2019-06-23 03:35:23 -07:00
}
2023-07-04 07:17:50 -07:00
const headerName = ( expectedAuth . name as string ) . toLowerCase ( ) ;
const expectedValue = expectedAuth . value as string ;
2019-06-23 03:35:23 -07:00
2022-08-17 08:50:24 -07:00
if (
! headers . hasOwnProperty ( headerName ) ||
2023-07-04 07:17:50 -07:00
( headers as IDataObject ) [ headerName ] !== expectedValue
2022-08-17 08:50:24 -07:00
) {
2019-06-23 03:35:23 -07:00
// Provided authentication data is wrong
2023-07-04 07:17:50 -07:00
throw new WebhookAuthorizationError ( 403 ) ;
2019-06-23 03:35:23 -07:00
}
}
2023-07-04 07:17:50 -07:00
}
2019-12-21 17:03:24 -08:00
2023-07-04 07:17:50 -07:00
private async handleFormData ( context : IWebhookFunctions ) {
2023-08-01 08:32:30 -07:00
const req = context . getRequestObject ( ) as MultiPartFormData . Request ;
2023-07-04 07:17:50 -07:00
const options = context . getNodeParameter ( 'options' , { } ) as IDataObject ;
2023-08-01 08:32:30 -07:00
const { data , files } = req . body ;
2020-03-21 15:39:40 -07:00
2023-08-01 08:32:30 -07:00
const returnItem : INodeExecutionData = {
binary : { } ,
json : {
headers : req.headers ,
2023-08-25 04:28:32 -07:00
params : req.params ,
2023-08-01 08:32:30 -07:00
query : req.query ,
body : data ,
} ,
} ;
2020-03-21 15:39:40 -07:00
2023-08-01 08:32:30 -07:00
let count = 0 ;
2023-12-06 07:46:40 -08:00
2023-08-01 08:32:30 -07:00
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
2023-08-01 08:32:30 -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
2023-08-01 08:32:30 -07:00
returnItem . binary ! [ binaryPropertyName ] = await context . nodeHelpers . copyBinaryFile (
file . filepath ,
file . originalFilename ? ? file . newFilename ,
file . mimetype ,
) ;
2023-07-04 07:17:50 -07:00
2023-08-01 08:32:30 -07:00
count += 1 ;
}
}
2023-12-06 07:46:40 -08:00
2023-08-01 08:32:30 -07:00
return { workflowData : [ [ returnItem ] ] } ;
2023-07-04 07:17:50 -07:00
}
2019-12-21 12:36:08 -08:00
2023-07-04 07:17:50 -07:00
private async handleBinaryData ( context : IWebhookFunctions ) : Promise < IWebhookResponseData > {
const req = context . getRequestObject ( ) ;
const options = context . getNodeParameter ( 'options' , { } ) as IDataObject ;
2023-08-01 08:32:30 -07:00
// TODO: create empty binaryData placeholder, stream into that path, and then finalize the binaryData
2023-07-04 07:17:50 -07:00
const binaryFile = await tmpFile ( { prefix : 'n8n-webhook-' } ) ;
try {
2023-08-01 08:32:30 -07:00
await pipeline ( req , createWriteStream ( binaryFile . path ) ) ;
2023-07-04 07:17:50 -07:00
const returnItem : INodeExecutionData = {
binary : { } ,
json : {
headers : req.headers ,
2023-08-25 04:28:32 -07:00
params : req.params ,
2023-07-04 07:17:50 -07:00
query : req.query ,
2023-08-01 08:32:30 -07:00
body : { } ,
2020-10-22 06:46:03 -07:00
} ,
2019-12-21 12:36:08 -08:00
} ;
2023-07-04 07:17:50 -07:00
const binaryPropertyName = ( options . binaryPropertyName || 'data' ) as string ;
2023-08-01 08:32:30 -07:00
const fileName = req . contentDisposition ? . filename ? ? uuid ( ) ;
2023-12-06 07:46:40 -08:00
const binaryData = await context . nodeHelpers . copyBinaryFile (
2023-07-04 07:17:50 -07:00
binaryFile . path ,
2023-08-01 08:32:30 -07:00
fileName ,
req . contentType ? ? 'application/octet-stream' ,
2023-07-04 07:17:50 -07:00
) ;
2020-12-14 08:19:20 -08:00
2023-12-06 07:46:40 -08:00
if ( ! binaryData . data ) {
return { workflowData : [ [ returnItem ] ] } ;
}
returnItem . binary ! [ binaryPropertyName ] = binaryData ;
2023-07-04 07:17:50 -07:00
return { workflowData : [ [ returnItem ] ] } ;
} catch ( error ) {
throw new NodeOperationError ( context . getNode ( ) , error as Error ) ;
} finally {
await binaryFile . cleanup ( ) ;
}
2019-06-23 03:35:23 -07:00
}
}