diff --git a/packages/nodes-base/credentials/Aws.credentials.ts b/packages/nodes-base/credentials/Aws.credentials.ts new file mode 100644 index 0000000000..6a0f294b88 --- /dev/null +++ b/packages/nodes-base/credentials/Aws.credentials.ts @@ -0,0 +1,33 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class Aws implements ICredentialType { + name = 'aws'; + displayName = 'AWS'; + properties = [ + { + displayName: 'Region', + name: 'region', + type: 'string' as NodePropertyTypes, + default: 'us-east-1', + }, + { + displayName: 'Access Key Id', + name: 'accessKeyId', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Secret Access Key', + name: 'secretAccessKey', + type: 'string' as NodePropertyTypes, + default: '', + typeOptions: { + password: true, + }, + }, + ]; +} diff --git a/packages/nodes-base/nodes/Aws/AwsLambda.node.ts b/packages/nodes-base/nodes/Aws/AwsLambda.node.ts new file mode 100644 index 0000000000..589f310433 --- /dev/null +++ b/packages/nodes-base/nodes/Aws/AwsLambda.node.ts @@ -0,0 +1,199 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + INodeTypeDescription, + INodeExecutionData, + INodeType, + INodePropertyOptions, + ILoadOptionsFunctions, + IDataObject +} from 'n8n-workflow'; + +import { awsApiRequestREST } from './GenericFunctions'; + +export class AwsLambda implements INodeType { + description: INodeTypeDescription = { + displayName: 'AWS Lambda', + name: 'awsLambda', + icon: 'file:lambda.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["function"]}}', + description: 'Invoke functions on AWS Lambda', + defaults: { + name: 'AWS Lambda', + color: '#FF9900', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'aws', + required: true, + } + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Invoke', + value: 'invoke', + description: 'Invoke a function', + }, + ], + default: 'invoke', + description: 'The operation to perform.', + }, + { + displayName: 'Function', + name: 'function', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getFunctions', + }, + displayOptions: { + show: { + operation: [ + 'invoke', + ], + }, + }, + options: [], + default: '', + required: true, + description: 'The function you want to invoke', + }, + { + displayName: 'Qualifier', + name: 'qualifier', + type: 'string', + displayOptions: { + show: { + operation: [ + 'invoke', + ], + }, + }, + required: true, + default: '$LATEST', + description: 'Specify a version or alias to invoke a published version of the function', + }, + { + displayName: 'Invocation Type', + name: 'invocationType', + type: 'options', + options: [ + { + name: 'Wait for results', + value: 'RequestResponse', + description: 'Invoke the function synchronously and wait for the response', + }, + { + name: 'Continue workflow', + value: 'Event', + description: 'Invoke the function and immediately continue the workflow', + }, + ], + displayOptions: { + show: { + operation: [ + 'invoke', + ], + }, + }, + default: 'RequestResponse', + description: 'Specify if the workflow should wait for the function to return the results', + }, + { + displayName: 'JSON Input', + name: 'payload', + type: 'string', + displayOptions: { + show: { + operation: [ + 'invoke', + ], + }, + }, + default: '', + description: 'The JSON that you want to provide to your Lambda function as input', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + }; + + methods = { + loadOptions: { + async getFunctions(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + try { + var data = await awsApiRequestREST.call(this, 'lambda', 'GET', '/2015-03-31/functions/'); + } catch (err) { + throw new Error(`AWS Error: ${err}`); + } + + for (let func of data.Functions!) { + returnData.push({ + name: func.FunctionName as string, + value: func.FunctionArn as string, + }); + } + return returnData; + } + }, + }; + + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + for (let i = 0; i < items.length; i++) { + const params = { + FunctionName: this.getNodeParameter('function', i) as string, + InvocationType: this.getNodeParameter('invocationType', i) as string, + Payload: this.getNodeParameter('payload', i) as string, + Qualifier: this.getNodeParameter('qualifier', i) as string, + }; + + try { + var responseData = await awsApiRequestREST.call( + this, + 'lambda', + 'POST', + `/2015-03-31/functions/${params.FunctionName}/invocations?Qualifier=${params.Qualifier}`, + params.Payload, + { + 'X-Amz-Invocation-Type': params.InvocationType, + 'Content-Type': 'application/x-amz-json-1.0', + }, + ); + } catch (err) { + throw new Error(`AWS Error: ${err}`); + } + + if (responseData.errorMessage === undefined) { + returnData.push({ + FunctionName: params.FunctionName, + FunctionQualifier: params.Qualifier, + Result: responseData, + } as IDataObject); + } else { + returnData.push({ + FunctionName: params.FunctionName, + FunctionQualifier: params.Qualifier, + ErrorMessage: responseData.errorMessage, + ErrorType: responseData.errorType, + ErrorStackTrace: responseData.stackTrace, + } as IDataObject); + } + + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Aws/AwsSns.node.ts b/packages/nodes-base/nodes/Aws/AwsSns.node.ts new file mode 100644 index 0000000000..2cbdf90472 --- /dev/null +++ b/packages/nodes-base/nodes/Aws/AwsSns.node.ts @@ -0,0 +1,153 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + INodeTypeDescription, + INodeExecutionData, + INodeType, + INodePropertyOptions, + ILoadOptionsFunctions, + IDataObject +} from 'n8n-workflow'; + +import { awsApiRequestSOAP } from './GenericFunctions'; + +export class AwsSns implements INodeType { + description: INodeTypeDescription = { + displayName: 'AWS SNS', + name: 'awsSns', + icon: 'file:sns.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["topic"]}}', + description: 'Sends data to AWS SNS', + defaults: { + name: 'AWS SNS', + color: '#FF9900', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'aws', + required: true, + } + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Publish', + value: 'publish', + description: 'Publish a message to a topic', + }, + ], + default: 'invoke', + description: 'The operation to perform.', + }, + { + displayName: 'Topic', + name: 'topic', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTopics', + }, + displayOptions: { + show: { + operation: [ + 'publish', + ], + }, + }, + options: [], + default: '', + required: true, + description: 'The topic you want to publish to', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + displayOptions: { + show: { + operation: [ + 'publish', + ], + }, + }, + default: '', + required: true, + description: 'Subject when the message is delivered to email endpoints', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + displayOptions: { + show: { + operation: [ + 'publish', + ], + }, + }, + required: true, + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'The message you want to send', + }, + ], + }; + + 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[] = []; + try { + var data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListTopics'); + } catch (err) { + throw new Error(`AWS Error: ${err}`); + } + + const topics = data.ListTopicsResponse.ListTopicsResult.Topics.member; + for (let topic of topics) { + let topicArn = topic.TopicArn as string; + let topicName = topicArn.split(':')[5]; + + returnData.push({ + name: topicName, + value: topicArn, + }); + } + return returnData; + } + }, + }; + + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + for (let i = 0; i < items.length; i++) { + const params = [ + 'TopicArn=' + this.getNodeParameter('topic', i) as string, + 'Subject=' + this.getNodeParameter('subject', i) as string, + 'Message=' + this.getNodeParameter('message', i) as string, + ]; + + try { + var responseData = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=Publish&' + params.join('&')); + } catch (err) { + throw new Error(`AWS Error: ${err}`); + } + returnData.push({MessageId: responseData.PublishResponse.PublishResult.MessageId} as IDataObject); + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Aws/GenericFunctions.ts b/packages/nodes-base/nodes/Aws/GenericFunctions.ts new file mode 100644 index 0000000000..cf5f7e0af2 --- /dev/null +++ b/packages/nodes-base/nodes/Aws/GenericFunctions.ts @@ -0,0 +1,75 @@ +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { OptionsWithUri } from 'request'; +import { sign } from 'aws4'; +import { parseString } from 'xml2js'; + +export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, 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!'); + } + + const endpoint = `${service}.${credentials.region}.amazonaws.com` + + // Sign AWS API request with the user credentials + const signOpts = {headers: headers || {}, host: endpoint, method: method, path: path, body: body} + sign(signOpts, {accessKeyId: `${credentials.accessKeyId}`, secretAccessKey: `${credentials.secretAccessKey}`}) + + const options: OptionsWithUri = { + headers: signOpts.headers, + method: method, + uri: `https://${endpoint}${signOpts.path}`, + body: signOpts.body, + }; + + try { + return await this.helpers.request!(options); + } catch (error) { + console.error(error); + + const errorMessage = error.response.body.message || error.response.body.Message; + if (error.statusCode === 403) { + if (errorMessage == 'The security token included in the request is invalid.') { + throw new Error('The AWS credentials are not valid!'); + } else if (errorMessage.startsWith('The request signature we calculated does not match the signature you provided')) { + throw new Error('The AWS credentials are not valid!'); + } + } + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} + + +export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, 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 JSON.parse(response); + } catch (e) { + return response + } +} + +export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, 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) => { + parseString(response, { explicitArray: false }, (err, data) => { + if (err) { + return reject(err); + } + resolve(data); + }); + }); + } catch (e) { + return response + } +} diff --git a/packages/nodes-base/nodes/Aws/lambda.png b/packages/nodes-base/nodes/Aws/lambda.png new file mode 100644 index 0000000000..d98d8f3cdc Binary files /dev/null and b/packages/nodes-base/nodes/Aws/lambda.png differ diff --git a/packages/nodes-base/nodes/Aws/sns.png b/packages/nodes-base/nodes/Aws/sns.png new file mode 100644 index 0000000000..1ebc137c32 Binary files /dev/null and b/packages/nodes-base/nodes/Aws/sns.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 3a649b7ab1..b0f5e63673 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -29,6 +29,7 @@ "dist/credentials/ActiveCampaignApi.credentials.js", "dist/credentials/AirtableApi.credentials.js", "dist/credentials/AsanaApi.credentials.js", + "dist/credentials/Aws.credentials.js", "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/DropboxApi.credentials.js", "dist/credentials/GithubApi.credentials.js", @@ -56,6 +57,8 @@ "dist/nodes/Airtable/Airtable.node.js", "dist/nodes/Asana/Asana.node.js", "dist/nodes/Asana/AsanaTrigger.node.js", + "dist/nodes/Aws/AwsLambda.node.js", + "dist/nodes/Aws/AwsSns.node.js", "dist/nodes/Chargebee/Chargebee.node.js", "dist/nodes/Chargebee/ChargebeeTrigger.node.js", "dist/nodes/Cron.node.js", @@ -105,6 +108,7 @@ ] }, "devDependencies": { + "@types/aws4": "^1.5.1", "@types/basic-auth": "^1.1.2", "@types/cron": "^1.6.1", "@types/express": "^4.16.1", @@ -125,6 +129,7 @@ "typescript": "~3.5.2" }, "dependencies": { + "aws4": "^1.8.0", "basic-auth": "^2.0.1", "cron": "^1.6.0", "glob-promise": "^3.4.0",