From ea5f84d5e1286e9a07138f32eb56f14a326e34a7 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 8 Feb 2020 17:19:00 -0500 Subject: [PATCH 1/2] :sparkles: AWS SNS trigger --- .../nodes/Aws/AwsSnsTrigger.node.ts | 179 ++++++++++++++++++ .../nodes-base/nodes/Aws/GenericFunctions.ts | 5 +- packages/nodes-base/package.json | 1 + 3 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 packages/nodes-base/nodes/Aws/AwsSnsTrigger.node.ts 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 000000000..da501d7fb --- /dev/null +++ b/packages/nodes-base/nodes/Aws/AwsSnsTrigger.node.ts @@ -0,0 +1,179 @@ +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'); + const topic = this.getNodeParameter('topic') as string; + const params = [ + `TopicArn=${topic}`, + `Endpoint=${webhookUrl}`, + `Protocol=${webhookUrl?.split(':')[0]}`, + 'ReturnSubscriptionArn=true', + 'Version=2010-03-31', + ]; + try { + const { SubscribeResponse } = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=Subscribe&' + params.join('&')); + webhookData.webhookId = SubscribeResponse.SubscribeResult.SubscriptionArn; + } catch (err) { + throw new Error(err); + } + 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; + if (req.body.Type === 'SubscriptionConfirmation' && + req.body.TopicArn === topic) { + const { Token } = req.body; + const params = [ + `TopicArn=${topic}`, + `Token=${Token}`, + 'Version=2010-03-31', + ]; + await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ConfirmSubscription&' + params.join('&')); + return {}; + } + if (req.body.Type === 'UnsubscribeConfirmation') { + return {}; + } + //TODO verify message signature + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Aws/GenericFunctions.ts b/packages/nodes-base/nodes/Aws/GenericFunctions.ts index 303a02707..31e25dce3 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!'); @@ -59,7 +60,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 4407952c9..a582385d1 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", From 3fe236b9e63700937ea2a5055b82b0f741a1842e Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 9 Feb 2020 18:27:06 -0800 Subject: [PATCH 2/2] :bug: Fix SNS-Trigger-Node --- packages/cli/src/Server.ts | 21 ++++++--- .../nodes/Aws/AwsSnsTrigger.node.ts | 46 ++++++++++++------- .../nodes-base/nodes/Aws/GenericFunctions.ts | 5 +- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index de561725e..971aeb23f 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 index da501d7fb..3d4b63b30 100644 --- a/packages/nodes-base/nodes/Aws/AwsSnsTrigger.node.ts +++ b/packages/nodes-base/nodes/Aws/AwsSnsTrigger.node.ts @@ -21,13 +21,13 @@ export class AwsSnsTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'AWS SNS Trigger', subtitle: `={{$parameter["topic"].split(':')[5]}}`, - name: 'AwsSnsTrigger', + name: 'awsSnsTrigger', icon: 'file:sns.png', group: ['trigger'], version: 1, description: 'Handle AWS SNS events via webhooks', defaults: { - name: 'AWS SNS Trigger', + name: 'AWS-SNS-Trigger', color: '#FF9900', }, inputs: [], @@ -108,7 +108,7 @@ export class AwsSnsTrigger implements INodeType { 'Version=2010-03-31', ]; const data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListSubscriptionsByTopic&' + params.join('&')); - const subscriptions = get(data, 'ListSubscriptionsByTopicResponse.ListSubscriptionsByTopicResult.Subscriptions.member') + const subscriptions = get(data, 'ListSubscriptionsByTopicResponse.ListSubscriptionsByTopicResult.Subscriptions.member'); for (const subscription of subscriptions) { if (webhookData.webhookId === subscription.SubscriptionArn) { return true; @@ -118,8 +118,13 @@ export class AwsSnsTrigger implements INodeType { }, async create(this: IHookFunctions): Promise { const webhookData = this.getWorkflowStaticData('node'); - const webhookUrl = this.getNodeWebhookUrl('default'); + 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}`, @@ -127,12 +132,10 @@ export class AwsSnsTrigger implements INodeType { 'ReturnSubscriptionArn=true', 'Version=2010-03-31', ]; - try { - const { SubscribeResponse } = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=Subscribe&' + params.join('&')); - webhookData.webhookId = SubscribeResponse.SubscribeResult.SubscriptionArn; - } catch (err) { - throw new Error(err); - } + + const { SubscribeResponse } = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=Subscribe&' + params.join('&')); + webhookData.webhookId = SubscribeResponse.SubscribeResult.SubscriptionArn; + return true; }, async delete(this: IHookFunctions): Promise { @@ -155,24 +158,33 @@ export class AwsSnsTrigger implements INodeType { async webhook(this: IWebhookFunctions): Promise { const req = this.getRequestObject(); const topic = this.getNodeParameter('topic') as string; - if (req.body.Type === 'SubscriptionConfirmation' && - req.body.TopicArn === topic) { - const { Token } = req.body; + + // @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 {}; } - if (req.body.Type === 'UnsubscribeConfirmation') { - return {}; - } + //TODO verify message signature return { workflowData: [ - this.helpers.returnJsonArray(req.body), + this.helpers.returnJsonArray(body), ], }; } diff --git a/packages/nodes-base/nodes/Aws/GenericFunctions.ts b/packages/nodes-base/nodes/Aws/GenericFunctions.ts index 31e25dce3..ff1a6a5fd 100644 --- a/packages/nodes-base/nodes/Aws/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Aws/GenericFunctions.ts @@ -42,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}`); } }