diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index de561725ed..971aeb23f0 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -231,19 +231,28 @@ class App { }); // Support application/json type post data - this.app.use(bodyParser.json({ limit: "16mb", verify: (req, res, buf) => { - // @ts-ignore - req.rawBody = buf; - }})); + this.app.use(bodyParser.json({ + limit: '16mb', verify: (req, res, buf) => { + // @ts-ignore + req.rawBody = buf; + } + })); // Support application/xml type post data // @ts-ignore - this.app.use(bodyParser.xml({ limit: "16mb", xmlParseOptions: { + this.app.use(bodyParser.xml({ limit: '16mb', xmlParseOptions: { normalize: true, // Trim whitespace inside text nodes normalizeTags: true, // Transform tags to lowercase - explicitArray: false // Only put properties in array if length > 1 + explicitArray: false, // Only put properties in array if length > 1 } })); + this.app.use(bodyParser.text({ + limit: '16mb', verify: (req, res, buf) => { + // @ts-ignore + req.rawBody = buf; + } + })); + // Make sure that Vue history mode works properly this.app.use(history({ rewrites: [ diff --git a/packages/nodes-base/nodes/Aws/AwsSnsTrigger.node.ts b/packages/nodes-base/nodes/Aws/AwsSnsTrigger.node.ts new file mode 100644 index 0000000000..3d4b63b30b --- /dev/null +++ b/packages/nodes-base/nodes/Aws/AwsSnsTrigger.node.ts @@ -0,0 +1,191 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + INodeTypeDescription, + INodeType, + IWebhookResponseData, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + awsApiRequestSOAP, +} from './GenericFunctions'; + +import { get } from 'lodash'; + +export class AwsSnsTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'AWS SNS Trigger', + subtitle: `={{$parameter["topic"].split(':')[5]}}`, + name: 'awsSnsTrigger', + icon: 'file:sns.png', + group: ['trigger'], + version: 1, + description: 'Handle AWS SNS events via webhooks', + defaults: { + name: 'AWS-SNS-Trigger', + color: '#FF9900', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'aws', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Topic', + name: 'topic', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getTopics', + }, + default: '', + }, + ], + }; + + methods = { + loadOptions: { + // Get all the available topics to display them to user so that he can + // select them easily + async getTopics(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let data; + try { + data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListTopics'); + } catch (err) { + throw new Error(`AWS Error: ${err}`); + } + + let topics = data.ListTopicsResponse.ListTopicsResult.Topics.member; + + if (!Array.isArray(topics)) { + // If user has only a single topic no array get returned so we make + // one manually to be able to process everything identically + topics = [topics]; + } + + for (const topic of topics) { + const topicArn = topic.TopicArn as string; + const topicName = topicArn.split(':')[5]; + + returnData.push({ + name: topicName, + value: topicArn, + }); + } + return returnData; + } + }, + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const topic = this.getNodeParameter('topic') as string; + if (webhookData.webhookId === undefined) { + return false; + } + const params = [ + `TopicArn=${topic}`, + 'Version=2010-03-31', + ]; + const data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListSubscriptionsByTopic&' + params.join('&')); + const subscriptions = get(data, 'ListSubscriptionsByTopicResponse.ListSubscriptionsByTopicResult.Subscriptions.member'); + for (const subscription of subscriptions) { + if (webhookData.webhookId === subscription.SubscriptionArn) { + return true; + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default') as string; + const topic = this.getNodeParameter('topic') as string; + + if (webhookUrl.includes('%20')) { + throw new Error('The name of the SNS Trigger Node is not allowed to contain any spaces!'); + } + + const params = [ + `TopicArn=${topic}`, + `Endpoint=${webhookUrl}`, + `Protocol=${webhookUrl?.split(':')[0]}`, + 'ReturnSubscriptionArn=true', + 'Version=2010-03-31', + ]; + + const { SubscribeResponse } = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=Subscribe&' + params.join('&')); + webhookData.webhookId = SubscribeResponse.SubscribeResult.SubscriptionArn; + + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const params = [ + `SubscriptionArn=${webhookData.webhookId}`, + 'Version=2010-03-31', + ]; + try { + await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=Unsubscribe&' + params.join('&')); + } catch(error) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + const topic = this.getNodeParameter('topic') as string; + + // @ts-ignore + const body = JSON.parse((req.rawBody).toString()); + + if (body.Type === 'SubscriptionConfirmation' && + body.TopicArn === topic) { + const { Token } = body; + const params = [ + `TopicArn=${topic}`, + `Token=${Token}`, + 'Version=2010-03-31', + ]; + await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ConfirmSubscription&' + params.join('&')); + + return { + noWebhookResponse: true, + }; + } + + if (body.Type === 'UnsubscribeConfirmation') { + return {}; + } + + //TODO verify message signature + return { + workflowData: [ + this.helpers.returnJsonArray(body), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Aws/GenericFunctions.ts b/packages/nodes-base/nodes/Aws/GenericFunctions.ts index 303a027078..ff1a6a5fd2 100644 --- a/packages/nodes-base/nodes/Aws/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Aws/GenericFunctions.ts @@ -6,10 +6,11 @@ import { IExecuteFunctions, IHookFunctions, ILoadOptionsFunctions, + IWebhookFunctions, } from 'n8n-core'; -export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise { // tslint:disable-line:no-any +export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('aws'); if (credentials === undefined) { throw new Error('No credentials got returned!'); @@ -41,10 +42,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I } } - if (errorMessage !== undefined) { - throw errorMessage; - } - throw error.response.body; + throw new Error(`AWS error response [${error.statusCode}]: ${errorMessage}`); } } @@ -59,7 +57,7 @@ export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions } -export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise { // tslint:disable-line:no-any +export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise { // tslint:disable-line:no-any const response = await awsApiRequest.call(this, service, method, path, body, headers); try { return await new Promise((resolve, reject) => { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 4407952c96..a582385d1d 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -107,6 +107,7 @@ "dist/nodes/Aws/AwsLambda.node.js", "dist/nodes/Aws/AwsSes.node.js", "dist/nodes/Aws/AwsSns.node.js", + "dist/nodes/Aws/AwsSnsTrigger.node.js", "dist/nodes/Bitbucket/BitbucketTrigger.node.js", "dist/nodes/Bitly/Bitly.node.js", "dist/nodes/Chargebee/Chargebee.node.js",