diff --git a/packages/nodes-base/credentials/Aws.credentials.ts b/packages/nodes-base/credentials/Aws.credentials.ts index 1af3b764c3..69fe9f1a47 100644 --- a/packages/nodes-base/credentials/Aws.credentials.ts +++ b/packages/nodes-base/credentials/Aws.credentials.ts @@ -30,5 +30,86 @@ export class Aws implements ICredentialType { password: true, }, }, + { + displayName: 'Custom Endpoints', + name: 'customEndpoints', + type: 'boolean' as NodePropertyTypes, + default: false, + }, + { + displayName: 'Rekognition Endpoint', + name: 'rekognitionEndpoint', + description: 'If you use Amazon VPC to host n8n, you can establish a connection between your VPC and Rekognition using a VPC endpoint. Leave blank to use the default endpoint.', + type: 'string' as NodePropertyTypes, + displayOptions: { + show: { + customEndpoints: [ + true, + ], + }, + }, + default: '', + placeholder: 'https://rekognition.{region}.amazonaws.com', + }, + { + displayName: 'Lambda Endpoint', + name: 'lambdaEndpoint', + description: 'If you use Amazon VPC to host n8n, you can establish a connection between your VPC and Lambda using a VPC endpoint. Leave blank to use the default endpoint.', + type: 'string' as NodePropertyTypes, + displayOptions: { + show: { + customEndpoints: [ + true, + ], + }, + }, + default: '', + placeholder: 'https://lambda.{region}.amazonaws.com', + }, + { + displayName: 'SNS Endpoint', + name: 'snsEndpoint', + description: 'If you use Amazon VPC to host n8n, you can establish a connection between your VPC and SNS using a VPC endpoint. Leave blank to use the default endpoint.', + type: 'string' as NodePropertyTypes, + displayOptions: { + show: { + customEndpoints: [ + true, + ], + }, + }, + default: '', + placeholder: 'https://sns.{region}.amazonaws.com', + }, + { + displayName: 'SES Endpoint', + name: 'sesEndpoint', + description: 'If you use Amazon VPC to host n8n, you can establish a connection between your VPC and SES using a VPC endpoint. Leave blank to use the default endpoint.', + type: 'string' as NodePropertyTypes, + displayOptions: { + show: { + customEndpoints: [ + true, + ], + }, + }, + default: '', + placeholder: 'https://email.{region}.amazonaws.com', + }, + { + displayName: 'S3 Endpoint', + name: 's3Endpoint', + description: 'If you use Amazon VPC to host n8n, you can establish a connection between your VPC and S3 using a VPC endpoint. Leave blank to use the default endpoint.', + type: 'string' as NodePropertyTypes, + displayOptions: { + show: { + customEndpoints: [ + true, + ], + }, + }, + default: '', + placeholder: 'https://s3.{region}.amazonaws.com', + }, ]; } diff --git a/packages/nodes-base/nodes/Aws/GenericFunctions.ts b/packages/nodes-base/nodes/Aws/GenericFunctions.ts index 880c0a02bb..6b3d7dab14 100644 --- a/packages/nodes-base/nodes/Aws/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Aws/GenericFunctions.ts @@ -1,3 +1,4 @@ +import { URL } from 'url'; import { sign } from 'aws4'; import { OptionsWithUri } from 'request'; import { parseString } from 'xml2js'; @@ -9,6 +10,21 @@ import { IWebhookFunctions, } from 'n8n-core'; +import { + ICredentialDataDecryptedObject, +} from 'n8n-workflow'; + +function getEndpointForService(service: string, credentials: ICredentialDataDecryptedObject): string { + let endpoint; + if (service === "lambda" && credentials.lambdaEndpoint) { + endpoint = credentials.lambdaEndpoint; + } else if (service === "sns" && credentials.snsEndpoint) { + endpoint = credentials.snsEndpoint; + } else { + endpoint = `https://${service}.${credentials.region}.amazonaws.com`; + } + return (endpoint as string).replace('{region}', credentials.region as string); +} 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'); @@ -16,23 +32,25 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I throw new Error('No credentials got returned!'); } - const endpoint = `${service}.${credentials.region}.amazonaws.com`; + // Concatenate path and instantiate URL object so it parses correctly query strings + const endpoint = new URL(getEndpointForService(service, credentials) + path); // Sign AWS API request with the user credentials - const signOpts = { headers: headers || {}, host: endpoint, method, path, body }; + const signOpts = { headers: headers || {}, host: endpoint.host, method, path: endpoint.pathname, body }; sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() }); + const options: OptionsWithUri = { headers: signOpts.headers, method, - uri: `https://${endpoint}${signOpts.path}`, + uri: endpoint.href, body: signOpts.body, }; try { return await this.helpers.request!(options); } catch (error) { - const errorMessage = error.response.body.message || error.response.body.Message || error.message; + const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message; if (error.statusCode === 403) { if (errorMessage === 'The security token included in the request is invalid.') { diff --git a/packages/nodes-base/nodes/Aws/Rekognition/GenericFunctions.ts b/packages/nodes-base/nodes/Aws/Rekognition/GenericFunctions.ts index 9b8d1782ba..779f6d1f3d 100644 --- a/packages/nodes-base/nodes/Aws/Rekognition/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Aws/Rekognition/GenericFunctions.ts @@ -1,3 +1,7 @@ +import { + URL, +} from 'url'; + import { sign, } from 'aws4'; @@ -31,17 +35,17 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I throw new Error('No credentials got returned!'); } - const endpoint = `${service}.${region || credentials.region}.amazonaws.com`; + const endpoint = new URL(((credentials.rekognitionEndpoint as string || '').replace('{region}', credentials.region as string) || `https://${service}.${credentials.region}.amazonaws.com`) + path); // Sign AWS API request with the user credentials - const signOpts = {headers: headers || {}, host: endpoint, method, path, body}; + const signOpts = {headers: headers || {}, host: endpoint.host, method, path, body}; sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim()}); const options: OptionsWithUri = { headers: signOpts.headers, method, - uri: `https://${endpoint}${signOpts.path}`, + uri: endpoint.href, body: signOpts.body, }; @@ -51,7 +55,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I try { return await this.helpers.request!(options); } catch (error) { - const errorMessage = error.response.body.message || error.response.body.Message || error.message; + const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message; if (error.statusCode === 403) { if (errorMessage === 'The security token included in the request is invalid.') { diff --git a/packages/nodes-base/nodes/Aws/S3/GenericFunctions.ts b/packages/nodes-base/nodes/Aws/S3/GenericFunctions.ts index fd7fe838f6..0c5e3b83aa 100644 --- a/packages/nodes-base/nodes/Aws/S3/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Aws/S3/GenericFunctions.ts @@ -1,3 +1,7 @@ +import { + URL, +} from 'url'; + import { sign, } from 'aws4'; @@ -31,10 +35,11 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I throw new Error('No credentials got returned!'); } - const endpoint = `${service}.${region || credentials.region}.amazonaws.com`; + const endpoint = new URL(((credentials.s3Endpoint as string || '').replace('{region}', credentials.region as string) || `https://${service}.${credentials.region}.amazonaws.com`) + path); // Sign AWS API request with the user credentials - const signOpts = {headers: headers || {}, host: endpoint, method, path: `${path}?${queryToString(query).replace(/\+/g, '%2B')}`, body}; + const signOpts = {headers: headers || {}, host: endpoint.host, method, path: `${endpoint.pathname}?${queryToString(query).replace(/\+/g, '%2B')}`, body}; + sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim()}); @@ -42,7 +47,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I headers: signOpts.headers, method, qs: query, - uri: `https://${endpoint}${signOpts.path}`, + uri: endpoint.href, body: signOpts.body, }; @@ -52,7 +57,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I try { return await this.helpers.request!(options); } catch (error) { - const errorMessage = error.response.body.message || error.response.body.Message || error.message; + const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message; if (error.statusCode === 403) { if (errorMessage === 'The security token included in the request is invalid.') { diff --git a/packages/nodes-base/nodes/Aws/SES/GenericFunctions.ts b/packages/nodes-base/nodes/Aws/SES/GenericFunctions.ts index b22ee7ca63..62323a447f 100644 --- a/packages/nodes-base/nodes/Aws/SES/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Aws/SES/GenericFunctions.ts @@ -1,3 +1,7 @@ +import { + URL, +} from 'url'; + import { sign, } from 'aws4'; @@ -31,23 +35,24 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I throw new Error('No credentials got returned!'); } - const endpoint = `${service}.${credentials.region}.amazonaws.com`; + const endpoint = new URL(((credentials.sesEndpoint as string || '').replace('{region}', credentials.region as string) || `https://${service}.${credentials.region}.amazonaws.com`) + path); // Sign AWS API request with the user credentials - const signOpts = { headers: headers || {}, host: endpoint, method, path, body }; + + const signOpts = { headers: headers || {}, host: endpoint.host, method, path, body }; sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() }); const options: OptionsWithUri = { headers: signOpts.headers, method, - uri: `https://${endpoint}${signOpts.path}`, + uri: endpoint.href, body: signOpts.body, }; try { return await this.helpers.request!(options); } catch (error) { - const errorMessage = error.response.body.message || error.response.body.Message || error.message; + const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message; if (error.statusCode === 403) { if (errorMessage === 'The security token included in the request is invalid.') {