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-12-12 19:13:48 -08:00
import { stat } from 'fs/promises' ;
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 ,
2024-03-28 01:46:39 -07:00
INodeProperties ,
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' ;
2024-03-28 01:46:39 -07:00
import jwt from 'jsonwebtoken' ;
2022-11-24 07:54:43 -08:00
2023-07-04 07:17:50 -07:00
import {
authenticationProperty ,
credentialsProperty ,
defaultWebhookDescription ,
httpMethodsProperty ,
optionsProperty ,
responseBinaryPropertyNameProperty ,
2024-03-28 01:46:39 -07:00
responseCodeOption ,
2023-07-04 07:17:50 -07:00
responseCodeProperty ,
responseDataProperty ,
responseModeProperty ,
} from './description' ;
import { WebhookAuthorizationError } from './error' ;
2024-03-28 01:46:39 -07:00
import {
checkResponseModeConfiguration ,
configuredOutputs ,
isIpWhitelisted ,
setupOutputConnection ,
} from './utils' ;
import { formatPrivateKey } from '../../utils/utilities' ;
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' ] ,
2024-03-28 01:46:39 -07:00
version : [ 1 , 1.1 , 2 ] ,
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 :
2024-03-28 01:46:39 -07:00
"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 : [ ] ,
2024-03-28 01:46:39 -07:00
outputs : ` ={{( ${ configuredOutputs } )( $ parameter)}} ` ,
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
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 ,
2024-03-28 01:46:39 -07:00
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
} ,
2024-03-28 01:46:39 -07:00
authenticationProperty ( this . authPropertyName ) ,
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 : '' ,
} ,
2024-03-28 01:46:39 -07:00
{
. . . responseCodeProperty ,
displayOptions : {
show : {
'@version' : [ 1 , 1.1 ] ,
} ,
hide : {
responseMode : [ 'responseNode' ] ,
} ,
} ,
} ,
2023-07-04 07:17:50 -07:00
responseDataProperty ,
responseBinaryPropertyNameProperty ,
2024-03-28 01:46:39 -07:00
{
. . . 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
] ,
} ;
2023-07-04 07:17:50 -07:00
async webhook ( context : IWebhookFunctions ) : Promise < IWebhookResponseData > {
2024-03-28 01:46:39 -07:00
const { typeVersion : nodeVersion , type : nodeType } = context . getNode ( ) ;
if ( nodeVersion >= 2 && nodeType === 'n8n-nodes-base.webhook' ) {
checkResponseModeConfiguration ( context ) ;
}
2023-07-04 07:17:50 -07:00
const options = context . getNodeParameter ( 'options' , { } ) as {
binaryData : boolean ;
ignoreBots : boolean ;
2024-01-08 05:33:14 -08:00
rawBody : boolean ;
2023-07-04 07:17:50 -07:00
responseData? : string ;
2024-03-28 01:46:39 -07:00
ipWhitelist? : string ;
2023-07-04 07:17:50 -07:00
} ;
const req = context . getRequestObject ( ) ;
const resp = context . getResponseObject ( ) ;
2024-03-28 01:46:39 -07:00
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 ;
2023-07-04 07:17:50 -07:00
try {
if ( options . ignoreBots && isbot ( req . headers [ 'user-agent' ] ) )
throw new WebhookAuthorizationError ( 403 ) ;
2024-03-28 01:46:39 -07:00
validationData = await this . validateAuth ( context ) ;
2023-07-04 07:17:50 -07:00
} 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
}
2024-03-28 01:46:39 -07:00
const prepareOutput = setupOutputConnection ( context , {
jwtPayload : validationData ,
} ) ;
2023-07-04 07:17:50 -07:00
if ( options . binaryData ) {
2024-03-28 01:46:39 -07:00
return await this . handleBinaryData ( context , prepareOutput ) ;
2023-07-04 07:17:50 -07:00
}
2023-08-01 08:32:30 -07:00
if ( req . contentType === 'multipart/form-data' ) {
2024-03-28 01:46:39 -07:00
return await this . handleFormData ( context , prepareOutput ) ;
2023-08-01 08:32:30 -07:00
}
2023-12-06 07:46:40 -08:00
if ( nodeVersion > 1 && ! req . body && ! options . rawBody ) {
try {
2024-03-28 01:46:39 -07:00
return await this . handleBinaryData ( context , prepareOutput ) ;
2023-12-06 07:46:40 -08:00
} 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
} ,
2024-03-26 06:22:57 -07:00
}
2023-07-04 07:17:50 -07:00
: undefined ,
} ;
return {
webhookResponse : options.responseData ,
2024-03-28 01:46:39 -07:00
workflowData : prepareOutput ( response ) ,
2023-07-04 07:17:50 -07:00
} ;
}
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
}
2024-03-28 01:46:39 -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 ) ;
}
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
}
2023-07-04 07:17:50 -07:00
}
2019-12-21 17:03:24 -08:00
2024-03-28 01:46:39 -07:00
private async handleFormData (
context : IWebhookFunctions ,
prepareOutput : ( data : INodeExecutionData ) = > INodeExecutionData [ ] [ ] ,
) {
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 = {
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
2024-01-08 05:33:14 -08:00
if ( files && Object . keys ( files ) . length ) {
2023-12-12 19:13:48 -08:00
returnItem . binary = { } ;
}
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
2024-03-28 01:46:39 -07:00
return { workflowData : prepareOutput ( returnItem ) } ;
2023-07-04 07:17:50 -07:00
}
2019-12-21 12:36:08 -08:00
2024-03-28 01:46:39 -07:00
private async handleBinaryData (
context : IWebhookFunctions ,
prepareOutput : ( data : INodeExecutionData ) = > INodeExecutionData [ ] [ ] ,
) : Promise < IWebhookResponseData > {
2023-07-04 07:17:50 -07:00
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 = {
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-12-12 19:13:48 -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 } ;
2023-12-06 07:46:40 -08:00
}
2024-03-28 01:46:39 -07:00
return { workflowData : prepareOutput ( returnItem ) } ;
2023-07-04 07:17:50 -07:00
} catch ( error ) {
throw new NodeOperationError ( context . getNode ( ) , error as Error ) ;
} finally {
await binaryFile . cleanup ( ) ;
}
2019-06-23 03:35:23 -07:00
}
}