2019-06-23 03:35:23 -07:00
import {
2020-03-20 14:47:47 -07:00
BINARY_ENCODING ,
2019-06-23 03:35:23 -07:00
IWebhookFunctions ,
} from 'n8n-core' ;
import {
IDataObject ,
2019-12-21 17:03:24 -08:00
INodeExecutionData ,
2019-06-23 03:35:23 -07:00
INodeType ,
2020-10-01 05:01:39 -07:00
INodeTypeDescription ,
2019-10-11 04:02:44 -07:00
IWebhookResponseData ,
2021-04-16 09:33:36 -07:00
NodeOperationError ,
2019-06-23 03:35:23 -07:00
} from 'n8n-workflow' ;
import * as basicAuth from 'basic-auth' ;
import { Response } from 'express' ;
2020-02-06 08:21:25 -08:00
import * as fs from 'fs' ;
import * as formidable from 'formidable' ;
2021-12-09 05:28:14 -08:00
import * as isbot from 'isbot' ;
2019-06-23 03:35:23 -07:00
function authorizationError ( resp : Response , realm : string , 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!' ;
}
}
resp . writeHead ( responseCode , { 'WWW-Authenticate' : ` Basic realm=" ${ realm } " ` } ) ;
resp . end ( message ) ;
return {
noWebhookResponse : true ,
} ;
}
export class Webhook implements INodeType {
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' ] ,
version : 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' ,
} ,
inputs : [ ] ,
outputs : [ 'main' ] ,
credentials : [
{
name : 'httpBasicAuth' ,
required : true ,
displayOptions : {
show : {
authentication : [
'basicAuth' ,
] ,
} ,
} ,
} ,
{
name : 'httpHeaderAuth' ,
required : true ,
displayOptions : {
show : {
authentication : [
'headerAuth' ,
] ,
} ,
} ,
} ,
] ,
webhooks : [
{
name : 'default' ,
httpMethod : '={{$parameter["httpMethod"]}}' ,
2020-06-10 06:39:15 -07:00
isFullPath : true ,
2019-08-28 08:03:35 -07:00
responseCode : '={{$parameter["responseCode"]}}' ,
2019-08-28 08:16:09 -07:00
responseMode : '={{$parameter["responseMode"]}}' ,
2022-02-19 03:37:41 -08:00
responseData : '={{$parameter["responseData"] || ($parameter.options.noResponseBody ? "noData" : undefined) }}' ,
2019-06-23 03:35:23 -07:00
responseBinaryPropertyName : '={{$parameter["responseBinaryPropertyName"]}}' ,
2019-10-16 05:01:39 -07:00
responseContentType : '={{$parameter["options"]["responseContentType"]}}' ,
responsePropertyName : '={{$parameter["options"]["responsePropertyName"]}}' ,
2020-04-26 02:01:20 -07:00
responseHeaders : '={{$parameter["options"]["responseHeaders"]}}' ,
2019-06-23 03:35:23 -07:00
path : '={{$parameter["path"]}}' ,
} ,
] ,
properties : [
{
displayName : 'Authentication' ,
name : 'authentication' ,
type : 'options' ,
options : [
{
name : 'Basic Auth' ,
2020-10-22 06:46:03 -07:00
value : 'basicAuth' ,
2019-06-23 03:35:23 -07:00
} ,
{
name : 'Header Auth' ,
2020-10-22 06:46:03 -07:00
value : 'headerAuth' ,
2019-06-23 03:35:23 -07:00
} ,
{
name : 'None' ,
2020-10-22 06:46:03 -07:00
value : 'none' ,
2019-06-23 03:35:23 -07:00
} ,
] ,
default : 'none' ,
description : 'The way to authenticate.' ,
} ,
{
displayName : 'HTTP Method' ,
name : 'httpMethod' ,
type : 'options' ,
options : [
2022-02-20 01:30:01 -08:00
{
name : 'DELETE' ,
value : 'DELETE' ,
} ,
2019-06-23 03:35:23 -07:00
{
name : 'GET' ,
value : 'GET' ,
} ,
2020-12-30 02:00:06 -08:00
{
name : 'HEAD' ,
value : 'HEAD' ,
} ,
2022-02-20 01:30:01 -08:00
{
name : 'PATCH' ,
value : 'PATCH' ,
} ,
2019-06-23 03:35:23 -07:00
{
name : 'POST' ,
value : 'POST' ,
} ,
2022-02-20 01:30:01 -08:00
{
name : 'PUT' ,
value : 'PUT' ,
} ,
2019-06-23 03:35:23 -07:00
] ,
default : 'GET' ,
2021-09-28 12:25:54 -07:00
description : 'The HTTP method to listen to.' ,
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 ,
2020-05-31 12:13:45 -07:00
description : 'The path to listen to.' ,
2019-06-23 03:35:23 -07:00
} ,
2019-08-28 08:03:35 -07:00
{
2021-11-05 09:45:51 -07:00
displayName : 'Respond' ,
2019-08-28 08:16:09 -07:00
name : 'responseMode' ,
2019-06-23 03:35:23 -07:00
type : 'options' ,
options : [
{
2021-11-05 09:45:51 -07:00
name : 'Immediately' ,
2019-06-23 03:35:23 -07:00
value : 'onReceived' ,
2021-11-05 09:45:51 -07:00
description : 'As soon as this node executes' ,
2019-06-23 03:35:23 -07:00
} ,
{
2021-11-05 09:45:51 -07:00
name : 'When last node finishes' ,
2019-06-23 03:35:23 -07:00
value : 'lastNode' ,
2021-11-05 09:45:51 -07:00
description : 'Returns data of the last-executed node' ,
} ,
{
name : 'Using \'Respond to Webhook\' node' ,
value : 'responseNode' ,
description : 'Response defined in that node' ,
2019-06-23 03:35:23 -07:00
} ,
] ,
default : 'onReceived' ,
description : 'When and how to respond to the webhook.' ,
} ,
2021-11-05 09:45:51 -07:00
{
displayName : 'Insert a \'Respond to Webhook\' node to control when and how you respond. <a href="https://docs.n8n.io/nodes/n8n-nodes-base.respondToWebhook" target="_blank">More details</a>' ,
name : 'webhookNotice' ,
type : 'notice' ,
displayOptions : {
show : {
responseMode : [
'responseNode' ,
] ,
} ,
} ,
default : '' ,
} ,
{
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' ,
} ,
2019-06-23 03:35:23 -07:00
{
2019-08-28 08:16:09 -07:00
displayName : 'Response Data' ,
name : 'responseData' ,
2019-06-23 03:35:23 -07:00
type : 'options' ,
displayOptions : {
show : {
2019-08-28 08:16:09 -07:00
responseMode : [
2019-06-23 03:35:23 -07:00
'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.' ,
} ,
2022-02-19 03:37:41 -08:00
{
name : 'No Response Body' ,
value : 'noData' ,
description : 'Returns without a body.' ,
} ,
2019-06-23 03:35:23 -07:00
] ,
default : 'firstEntryJson' ,
2021-10-27 13:00:13 -07:00
description : 'What data should be returned. If it should return all items as an array or only the first item as object.' ,
2019-06-23 03:35:23 -07:00
} ,
{
displayName : 'Property Name' ,
name : 'responseBinaryPropertyName' ,
type : 'string' ,
required : true ,
default : 'data' ,
displayOptions : {
show : {
2019-08-28 08:16:09 -07:00
responseData : [
2020-10-22 06:46:03 -07:00
'firstEntryBinary' ,
2019-06-23 03:35:23 -07:00
] ,
} ,
} ,
description : 'Name of the binary property to return' ,
} ,
2019-10-16 05:01:39 -07:00
{
displayName : 'Options' ,
name : 'options' ,
type : 'collection' ,
placeholder : 'Add Option' ,
default : { } ,
options : [
2020-03-21 15:39:40 -07:00
{
displayName : 'Binary Data' ,
name : 'binaryData' ,
type : 'boolean' ,
displayOptions : {
show : {
'/httpMethod' : [
2022-02-20 01:30:01 -08:00
'PATCH' ,
'PUT' ,
2020-03-21 15:39:40 -07:00
'POST' ,
] ,
} ,
} ,
default : false ,
description : 'Set to true if webhook will receive binary data.' ,
} ,
{
displayName : 'Binary Property' ,
name : 'binaryPropertyName' ,
type : 'string' ,
default : 'data' ,
required : true ,
displayOptions : {
show : {
binaryData : [
true ,
] ,
} ,
} ,
2021-10-27 13:00:13 -07:00
description : ` Name of the binary property to write the data of
the received file to . If the data gets received via "Form-Data Multipart"
2020-03-30 05:53:42 -07:00
it will be the prefix and a number starting with 0 will be attached to it . ` ,
2020-03-21 15:39:40 -07:00
} ,
2022-01-02 01:20:12 -08:00
{
displayName : 'Ignore Bots' ,
name : 'ignoreBots' ,
type : 'boolean' ,
default : false ,
description : 'Set to true to ignore requests from bots like link previewers and web crawlers' ,
} ,
2022-02-19 03:37:41 -08:00
{
displayName : 'No Response Body' ,
name : 'noResponseBody' ,
type : 'boolean' ,
default : false ,
description : 'Do not 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 ,
description : 'Raw body (binary)' ,
} ,
2020-12-14 08:19:20 -08:00
{
displayName : 'Response Data' ,
name : 'responseData' ,
type : 'string' ,
displayOptions : {
show : {
'/responseMode' : [
'onReceived' ,
] ,
} ,
2022-02-19 03:37:41 -08:00
hide : {
'noResponseBody' : [
true ,
] ,
} ,
2020-12-14 08:19:20 -08:00
} ,
default : '' ,
placeholder : 'success' ,
description : 'Custom response data to send.' ,
} ,
2019-10-16 05:01:39 -07:00
{
displayName : 'Response Content-Type' ,
name : 'responseContentType' ,
type : 'string' ,
2019-12-21 17:03:24 -08:00
displayOptions : {
show : {
'/responseData' : [
'firstEntryJson' ,
] ,
'/responseMode' : [
'lastNode' ,
] ,
} ,
} ,
2019-10-16 05:01:39 -07:00
default : '' ,
placeholder : 'application/xml' ,
description : 'Set a custom content-type to return if another one as the "application/json" should be returned.' ,
} ,
2020-04-26 02:01:20 -07:00
{
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.' ,
} ,
2020-10-22 06:46:03 -07:00
] ,
2020-04-26 02:01:20 -07:00
} ,
] ,
} ,
2019-10-16 05:01:39 -07:00
{
displayName : 'Property Name' ,
name : 'responsePropertyName' ,
type : 'string' ,
2019-12-21 17:03:24 -08:00
displayOptions : {
show : {
'/responseData' : [
'firstEntryJson' ,
] ,
'/responseMode' : [
'lastNode' ,
] ,
} ,
} ,
2019-10-16 05:01:39 -07:00
default : 'data' ,
description : 'Name of the property to return the data of instead of the whole JSON.' ,
} ,
2019-12-21 12:36:08 -08:00
] ,
} ,
2019-06-23 03:35:23 -07:00
] ,
} ;
2019-10-11 04:02:44 -07:00
async webhook ( this : IWebhookFunctions ) : Promise < IWebhookResponseData > {
2020-03-21 15:39:40 -07:00
const authentication = this . getNodeParameter ( 'authentication' ) as string ;
const options = this . getNodeParameter ( 'options' , { } ) as IDataObject ;
2019-06-23 03:35:23 -07:00
const req = this . getRequestObject ( ) ;
const resp = this . getResponseObject ( ) ;
const headers = this . getHeaderData ( ) ;
const realm = 'Webhook' ;
2021-12-09 05:28:14 -08:00
const ignoreBots = options . ignoreBots as boolean ;
if ( ignoreBots && isbot ( ( headers as IDataObject ) [ 'user-agent' ] as string ) ) {
return authorizationError ( resp , realm , 403 ) ;
}
2019-06-23 03:35:23 -07:00
if ( authentication === 'basicAuth' ) {
// Basic authorization is needed to call webhook
2021-08-20 09:57:30 -07:00
const httpBasicAuth = await this . getCredentials ( 'httpBasicAuth' ) ;
2019-06-23 03:35:23 -07:00
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
2021-08-20 09:57:30 -07:00
const httpHeaderAuth = await this . getCredentials ( 'httpHeaderAuth' ) ;
2019-06-23 03:35:23 -07:00
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 ) ;
}
}
2019-12-21 17:03:24 -08:00
// @ts-ignore
const mimeType = headers [ 'content-type' ] || 'application/json' ;
2020-02-06 08:21:25 -08:00
if ( mimeType . includes ( 'multipart/form-data' ) ) {
2021-10-13 15:21:00 -07:00
// @ts-ignore
2021-08-29 11:56:19 -07:00
const form = new formidable . IncomingForm ( { multiples : true } ) ;
2020-02-06 08:21:25 -08:00
return new Promise ( ( resolve , reject ) = > {
form . parse ( req , async ( err , data , files ) = > {
2020-03-30 05:53:42 -07:00
const returnItem : INodeExecutionData = {
binary : { } ,
json : {
headers ,
2021-01-23 11:00:32 -08:00
params : this.getParamsData ( ) ,
2020-03-30 05:53:42 -07:00
query : this.getQueryData ( ) ,
2021-01-23 11:00:32 -08:00
body : data ,
2020-03-30 05:53:42 -07:00
} ,
} ;
let count = 0 ;
2021-08-29 11:56:19 -07:00
for ( const xfile of Object . keys ( files ) ) {
2021-08-29 14:08:56 -07:00
const processFiles : formidable.File [ ] = [ ] ;
2021-08-29 11:56:19 -07:00
let multiFile = false ;
if ( Array . isArray ( files [ xfile ] ) ) {
processFiles . push ( . . . files [ xfile ] as formidable . File [ ] ) ;
multiFile = true ;
} else {
2021-08-29 14:08:56 -07:00
processFiles . push ( files [ xfile ] as formidable . File ) ;
2020-03-30 05:53:42 -07:00
}
2021-08-29 11:56:19 -07:00
let fileCount = 0 ;
2021-08-29 14:08:56 -07:00
for ( const file of processFiles ) {
2021-08-29 11:56:19 -07:00
let binaryPropertyName = xfile ;
if ( binaryPropertyName . endsWith ( '[]' ) ) {
binaryPropertyName = binaryPropertyName . slice ( 0 , - 2 ) ;
}
if ( multiFile === true ) {
binaryPropertyName += fileCount ++ ;
}
if ( options . binaryPropertyName ) {
binaryPropertyName = ` ${ options . binaryPropertyName } ${ count } ` ;
}
2021-08-29 14:08:56 -07:00
const fileJson = file . toJSON ( ) as unknown as IDataObject ;
const fileContent = await fs . promises . readFile ( file . path ) ;
2020-03-30 05:53:42 -07:00
2021-08-29 11:56:19 -07:00
returnItem . binary ! [ binaryPropertyName ] = await this . helpers . prepareBinaryData ( Buffer . from ( fileContent ) , fileJson . name as string , fileJson . type as string ) ;
2020-03-30 05:53:42 -07:00
2021-08-29 11:56:19 -07:00
count += 1 ;
}
2020-02-06 08:21:25 -08:00
}
resolve ( {
workflowData : [
2020-03-30 05:53:42 -07:00
[
returnItem ,
2020-10-22 06:46:03 -07:00
] ,
2020-02-06 08:21:25 -08:00
] ,
} ) ;
} ) ;
} ) ;
}
2019-12-21 17:03:24 -08:00
2020-03-21 15:39:40 -07:00
if ( options . binaryData === true ) {
2020-03-20 11:53:51 -07:00
return new Promise ( ( resolve , reject ) = > {
2020-03-21 15:39:40 -07:00
const binaryPropertyName = options . binaryPropertyName || 'data' ;
2020-03-20 11:53:51 -07:00
const data : Buffer [ ] = [ ] ;
req . on ( 'data' , ( chunk ) = > {
data . push ( chunk ) ;
} ) ;
2020-03-21 15:39:40 -07:00
req . on ( 'end' , async ( ) = > {
const returnItem : INodeExecutionData = {
binary : { } ,
2020-03-24 16:13:40 -07:00
json : {
headers ,
2021-01-23 11:00:32 -08:00
params : this.getParamsData ( ) ,
2020-03-24 16:13:40 -07:00
query : this.getQueryData ( ) ,
2021-01-23 11:00:32 -08:00
body : this.getBodyData ( ) ,
2020-03-24 16:13:40 -07:00
} ,
2020-03-21 15:39:40 -07:00
} ;
returnItem . binary ! [ binaryPropertyName as string ] = await this . helpers . prepareBinaryData ( Buffer . concat ( data ) ) ;
2020-03-20 11:53:51 -07:00
return resolve ( {
workflowData : [
2020-03-21 15:39:40 -07:00
[
2020-10-22 06:46:03 -07:00
returnItem ,
] ,
2020-03-20 11:53:51 -07:00
] ,
} ) ;
} ) ;
2021-04-16 09:33:36 -07:00
req . on ( 'error' , ( error ) = > {
throw new NodeOperationError ( this . getNode ( ) , error ) ;
2020-03-20 11:53:51 -07:00
} ) ;
} ) ;
}
2019-12-21 17:03:24 -08:00
const response : INodeExecutionData = {
2019-12-21 12:36:08 -08:00
json : {
2019-06-23 03:35:23 -07:00
headers ,
2021-01-23 11:00:32 -08:00
params : this.getParamsData ( ) ,
2019-06-23 03:35:23 -07:00
query : this.getQueryData ( ) ,
2021-01-23 11:00:32 -08:00
body : this.getBodyData ( ) ,
2019-12-21 12:36:08 -08:00
} ,
} ;
if ( options . rawBody ) {
response . binary = {
2019-12-21 17:03:24 -08:00
data : {
// @ts-ignore
2020-03-20 14:47:47 -07:00
data : req.rawBody.toString ( BINARY_ENCODING ) ,
2019-12-21 17:03:24 -08:00
mimeType ,
2020-10-22 06:46:03 -07:00
} ,
2019-12-21 12:36:08 -08:00
} ;
}
2020-12-14 08:19:20 -08:00
let webhookResponse : string | undefined ;
if ( options . responseData ) {
webhookResponse = options . responseData as string ;
}
2019-06-23 03:35:23 -07:00
return {
2020-12-14 08:19:20 -08:00
webhookResponse ,
2019-06-23 03:35:23 -07:00
workflowData : [
2019-12-21 12:36:08 -08:00
[
response ,
] ,
2019-06-23 03:35:23 -07:00
] ,
} ;
}
}