fix(core): Add support for .cn Amazon regions (#19363)

This commit is contained in:
yehorkardash
2025-09-11 08:55:52 +00:00
committed by GitHub
parent 12f12da288
commit fea0a62f8e
2 changed files with 129 additions and 8 deletions

View File

@@ -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',
}, },

View File

@@ -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/');
});
});
}); });
}); });