mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
fix(core): Add support for .cn Amazon regions (#19363)
This commit is contained in:
@@ -11,7 +11,17 @@ import type {
|
|||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { isObjectEmpty } from 'n8n-workflow';
|
import { isObjectEmpty } from 'n8n-workflow';
|
||||||
|
|
||||||
export const regions = [
|
type RegionData = {
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
location: string;
|
||||||
|
domain?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const chinaDomain = 'amazonaws.com.cn';
|
||||||
|
const globalDomain = 'amazonaws.com';
|
||||||
|
|
||||||
|
export const regions: RegionData[] = [
|
||||||
{
|
{
|
||||||
name: 'af-south-1',
|
name: 'af-south-1',
|
||||||
displayName: 'Africa',
|
displayName: 'Africa',
|
||||||
@@ -91,11 +101,13 @@ export const regions = [
|
|||||||
name: 'cn-north-1',
|
name: 'cn-north-1',
|
||||||
displayName: 'China',
|
displayName: 'China',
|
||||||
location: 'Beijing',
|
location: 'Beijing',
|
||||||
|
domain: chinaDomain,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'cn-northwest-1',
|
name: 'cn-northwest-1',
|
||||||
displayName: 'China',
|
displayName: 'China',
|
||||||
location: 'Ningxia',
|
location: 'Ningxia',
|
||||||
|
domain: chinaDomain,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'eu-central-1',
|
name: 'eu-central-1',
|
||||||
@@ -211,12 +223,18 @@ export type AwsCredentialsType = {
|
|||||||
ssmEndpoint?: string;
|
ssmEndpoint?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getAwsDomain(region: AWSRegion): string {
|
||||||
|
return regions.find((r) => r.name === region)?.domain ?? globalDomain;
|
||||||
|
}
|
||||||
|
|
||||||
// Some AWS services are global and don't have a region
|
// Some AWS services are global and don't have a region
|
||||||
// https://docs.aws.amazon.com/general/latest/gr/rande.html#global-endpoints
|
// https://docs.aws.amazon.com/general/latest/gr/rande.html#global-endpoints
|
||||||
// Example: iam.amazonaws.com (global), s3.us-east-1.amazonaws.com (regional)
|
// Example: iam.amazonaws.com (global), s3.us-east-1.amazonaws.com (regional)
|
||||||
function parseAwsUrl(url: URL): { region: AWSRegion | null; service: string } {
|
function parseAwsUrl(url: URL): { region: AWSRegion | null; service: string } {
|
||||||
const [service, region] = url.hostname.replace('amazonaws.com', '').split('.');
|
const hostname = url.hostname;
|
||||||
return { service, region: region as AWSRegion };
|
// Handle both .amazonaws.com and .amazonaws.com.cn domains
|
||||||
|
const [service, region] = hostname.replace(/\.amazonaws\.com.*$/, '').split('.');
|
||||||
|
return { service, region };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Aws implements ICredentialType {
|
export class Aws implements ICredentialType {
|
||||||
@@ -436,10 +454,11 @@ export class Aws implements ICredentialType {
|
|||||||
endpointString = credentials.sesEndpoint;
|
endpointString = credentials.sesEndpoint;
|
||||||
} else if (service === 'rekognition' && credentials.rekognitionEndpoint) {
|
} else if (service === 'rekognition' && credentials.rekognitionEndpoint) {
|
||||||
endpointString = credentials.rekognitionEndpoint;
|
endpointString = credentials.rekognitionEndpoint;
|
||||||
} else if (service) {
|
|
||||||
endpointString = `https://${service}.${region}.amazonaws.com`;
|
|
||||||
} else if (service === 'ssm' && credentials.ssmEndpoint) {
|
} else if (service === 'ssm' && credentials.ssmEndpoint) {
|
||||||
endpointString = credentials.ssmEndpoint;
|
endpointString = credentials.ssmEndpoint;
|
||||||
|
} else if (service) {
|
||||||
|
const domain = getAwsDomain(region);
|
||||||
|
endpointString = `https://${service}.${region}.${domain}`;
|
||||||
}
|
}
|
||||||
endpoint = new URL(endpointString!.replace('{region}', region) + path);
|
endpoint = new URL(endpointString!.replace('{region}', region) + path);
|
||||||
} else {
|
} else {
|
||||||
@@ -486,7 +505,6 @@ export class Aws implements ICredentialType {
|
|||||||
bodyContent = params.toString();
|
bodyContent = params.toString();
|
||||||
contentTypeHeader = 'application/x-www-form-urlencoded';
|
contentTypeHeader = 'application/x-www-form-urlencoded';
|
||||||
}
|
}
|
||||||
|
|
||||||
const signOpts = {
|
const signOpts = {
|
||||||
...requestOptions,
|
...requestOptions,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -526,7 +544,9 @@ export class Aws implements ICredentialType {
|
|||||||
|
|
||||||
test: ICredentialTestRequest = {
|
test: ICredentialTestRequest = {
|
||||||
request: {
|
request: {
|
||||||
baseURL: '=https://sts.{{$credentials.region}}.amazonaws.com',
|
baseURL:
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-interpolation-in-regular-string
|
||||||
|
'={{$credentials.region.startsWith("cn-") ? `https://sts.${$credentials.region}.amazonaws.com.cn` : `https://sts.${$credentials.region}.amazonaws.com`}}',
|
||||||
url: '?Action=GetCallerIdentity&Version=2011-06-15',
|
url: '?Action=GetCallerIdentity&Version=2011-06-15',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ describe('Aws Credential', () => {
|
|||||||
expect(aws.documentationUrl).toBe('aws');
|
expect(aws.documentationUrl).toBe('aws');
|
||||||
expect(aws.icon).toEqual({ light: 'file:icons/AWS.svg', dark: 'file:icons/AWS.dark.svg' });
|
expect(aws.icon).toEqual({ light: 'file:icons/AWS.svg', dark: 'file:icons/AWS.dark.svg' });
|
||||||
expect(aws.properties.length).toBeGreaterThan(0);
|
expect(aws.properties.length).toBeGreaterThan(0);
|
||||||
expect(aws.test.request.baseURL).toBe('=https://sts.{{$credentials.region}}.amazonaws.com');
|
expect(aws.test.request.baseURL).toBe(
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-interpolation-in-regular-string
|
||||||
|
'={{$credentials.region.startsWith("cn-") ? `https://sts.${$credentials.region}.amazonaws.com.cn` : `https://sts.${$credentials.region}.amazonaws.com`}}',
|
||||||
|
);
|
||||||
expect(aws.test.request.url).toBe('?Action=GetCallerIdentity&Version=2011-06-15');
|
expect(aws.test.request.url).toBe('?Action=GetCallerIdentity&Version=2011-06-15');
|
||||||
expect(aws.test.request.method).toBe('POST');
|
expect(aws.test.request.method).toBe('POST');
|
||||||
});
|
});
|
||||||
@@ -178,5 +181,103 @@ describe('Aws Credential', () => {
|
|||||||
expect(result.method).toBe('POST');
|
expect(result.method).toBe('POST');
|
||||||
expect(result.url).toBe('https://iam.amazonaws.com/');
|
expect(result.url).toBe('https://iam.amazonaws.com/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('China regions', () => {
|
||||||
|
const chinaCredentials: AwsCredentialsType = {
|
||||||
|
region: 'cn-north-1',
|
||||||
|
accessKeyId: 'hakuna',
|
||||||
|
secretAccessKey: 'matata',
|
||||||
|
customEndpoints: false,
|
||||||
|
temporaryCredentials: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should use amazonaws.com.cn domain for cn-north-1 region', async () => {
|
||||||
|
const result = await aws.authenticate(chinaCredentials, {
|
||||||
|
...requestOptions,
|
||||||
|
url: '',
|
||||||
|
baseURL: '',
|
||||||
|
qs: { service: 's3' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockSign).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
host: 's3.cn-north-1.amazonaws.com.cn',
|
||||||
|
region: 'cn-north-1',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
accessKeyId: 'hakuna',
|
||||||
|
secretAccessKey: 'matata',
|
||||||
|
sessionToken: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(result.url).toBe('https://s3.cn-north-1.amazonaws.com.cn/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle custom endpoints for China regions', async () => {
|
||||||
|
const customEndpoint = 'https://custom.china.endpoint.com.cn';
|
||||||
|
const result = await aws.authenticate(
|
||||||
|
{ ...chinaCredentials, customEndpoints: true, s3Endpoint: customEndpoint },
|
||||||
|
{ ...requestOptions, url: '', baseURL: '', qs: { service: 's3' } },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockSign).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
host: 'custom.china.endpoint.com.cn',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
accessKeyId: 'hakuna',
|
||||||
|
secretAccessKey: 'matata',
|
||||||
|
sessionToken: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(result.url).toBe(`${customEndpoint}/`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse China region URLs correctly', async () => {
|
||||||
|
const result = await aws.authenticate(chinaCredentials, {
|
||||||
|
...requestOptions,
|
||||||
|
url: 'https://s3.cn-north-1.amazonaws.com.cn/bucket/key',
|
||||||
|
baseURL: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockSign).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
host: 's3.cn-north-1.amazonaws.com.cn',
|
||||||
|
region: 'cn-north-1',
|
||||||
|
path: '/bucket/key',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
accessKeyId: 'hakuna',
|
||||||
|
secretAccessKey: 'matata',
|
||||||
|
sessionToken: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(result.url).toBe('https://s3.cn-north-1.amazonaws.com.cn/bucket/key');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Regular regions (non-China)', () => {
|
||||||
|
it('should use amazonaws.com domain for regular regions', async () => {
|
||||||
|
const result = await aws.authenticate(credentials, {
|
||||||
|
...requestOptions,
|
||||||
|
url: '',
|
||||||
|
baseURL: '',
|
||||||
|
qs: { service: 's3' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockSign).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
host: 's3.eu-central-1.amazonaws.com',
|
||||||
|
region: 'eu-central-1',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
accessKeyId: 'hakuna',
|
||||||
|
secretAccessKey: 'matata',
|
||||||
|
sessionToken: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(result.url).toBe('https://s3.eu-central-1.amazonaws.com/');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user