diff --git a/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.spec.ts b/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.spec.ts index d841c52fff7c2..53bf929a5ce7a 100644 --- a/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.spec.ts +++ b/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.spec.ts @@ -18,6 +18,7 @@ import { bucketEndpointMiddleware } from "./bucketEndpointMiddleware"; describe("bucketEndpointMiddleware", () => { const input = { Bucket: "bucket" }; + const mockRegion = "us-foo-1"; const requestInput = { method: "GET", headers: {}, @@ -27,10 +28,11 @@ describe("bucketEndpointMiddleware", () => { }; const next = jest.fn(); const previouslyResolvedConfig = { - region: jest.fn().mockResolvedValue("us-foo-1"), + isCustomEndpoint: false, + region: jest.fn().mockResolvedValue(mockRegion), regionInfoProvider: jest .fn() - .mockResolvedValue({ hostname: "foo.us-foo-2.amazonaws.com", partition: "aws-foo", signingRegion: "us-foo-1" }), + .mockResolvedValue({ hostname: "foo.us-foo-2.amazonaws.com", partition: "aws-foo", signingRegion: mockRegion }), useArnRegion: jest.fn().mockResolvedValue(false), }; @@ -61,10 +63,12 @@ describe("bucketEndpointMiddleware", () => { expect(param).toEqual({ bucketName: input.Bucket, baseHostname: requestInput.hostname, + clientRegion: mockRegion, accelerateEndpoint: false, dualstackEndpoint: false, pathStyleEndpoint: false, tlsCompatible: true, + isCustomEndpoint: false, }); }); @@ -77,6 +81,7 @@ describe("bucketEndpointMiddleware", () => { useAccelerateEndpoint: true, useDualstackEndpoint: true, forcePathStyle: true, + isCustomEndpoint: true, }) )(next, {} as any); await handler({ input, request }); @@ -85,10 +90,12 @@ describe("bucketEndpointMiddleware", () => { expect(param).toEqual({ bucketName: input.Bucket, baseHostname: requestInput.hostname, + clientRegion: mockRegion, accelerateEndpoint: true, dualstackEndpoint: true, pathStyleEndpoint: true, tlsCompatible: false, + isCustomEndpoint: true, }); }); }); @@ -118,13 +125,15 @@ describe("bucketEndpointMiddleware", () => { expect(param).toEqual({ bucketName: mockBucketArn, baseHostname: requestInput.hostname, + clientRegion: mockRegion, accelerateEndpoint: false, dualstackEndpoint: false, pathStyleEndpoint: false, tlsCompatible: true, clientPartition: "aws-foo", - clientSigningRegion: "us-foo-1", + clientSigningRegion: mockRegion, useArnRegion: false, + isCustomEndpoint: false, }); expect(previouslyResolvedConfig.region).toBeCalled(); expect(previouslyResolvedConfig.regionInfoProvider).toBeCalled(); @@ -144,7 +153,7 @@ describe("bucketEndpointMiddleware", () => { request, }); expect(previouslyResolvedConfig.regionInfoProvider).toBeCalled(); - expect(previouslyResolvedConfig.regionInfoProvider.mock.calls[0][0]).toBe("us-foo-1"); + expect(previouslyResolvedConfig.regionInfoProvider.mock.calls[0][0]).toBe(mockRegion); }); it("should supply bucketHostname in ARN object if bucket name string is a valid ARN", async () => { diff --git a/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.ts b/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.ts index 7e1062a044327..01323c2467f70 100644 --- a/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.ts +++ b/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.ts @@ -42,6 +42,8 @@ export const bucketEndpointMiddleware = (options: BucketEndpointResolvedConfig): useArnRegion, clientPartition: partition, clientSigningRegion: signingRegion, + clientRegion: clientRegion, + isCustomEndpoint: options.isCustomEndpoint, }); // If the request needs to use a region or service name inferred from ARN that different from client region, we @@ -56,13 +58,16 @@ export const bucketEndpointMiddleware = (options: BucketEndpointResolvedConfig): request.hostname = hostname; replaceBucketInPath = bucketEndpoint; } else { + const clientRegion = getPseudoRegion(await options.region()); const { hostname, bucketEndpoint } = bucketHostname({ bucketName, + clientRegion, baseHostname: request.hostname, accelerateEndpoint: options.useAccelerateEndpoint, dualstackEndpoint: options.useDualstackEndpoint, pathStyleEndpoint: options.forcePathStyle, tlsCompatible: request.protocol === "https:", + isCustomEndpoint: options.isCustomEndpoint, }); request.hostname = hostname; diff --git a/packages/middleware-bucket-endpoint/src/bucketHostname.spec.ts b/packages/middleware-bucket-endpoint/src/bucketHostname.spec.ts index 3315da1d79fbd..673555c517c84 100644 --- a/packages/middleware-bucket-endpoint/src/bucketHostname.spec.ts +++ b/packages/middleware-bucket-endpoint/src/bucketHostname.spec.ts @@ -3,28 +3,93 @@ import { parse as parseArn } from "@aws-sdk/util-arn-parser"; import { bucketHostname } from "./bucketHostname"; describe("bucketHostname", () => { + const region = "us-west-2"; describe("from bucket name", () => { - it("should use a virtual-hosted-style endpoint by default", () => { - const baseHostname = "s3.us-west-2.amazonaws.com"; - const { bucketEndpoint, hostname } = bucketHostname({ - bucketName: "foo", - baseHostname, - }); + [ + { baseHostname: "s3.us-west-2.amazonaws.com", isCustomEndpoint: false }, + { baseHostname: "beta.example.com", isCustomEndpoint: true }, + ].forEach(({ baseHostname, isCustomEndpoint }) => { + describe(`baseHostname: ${baseHostname}`, () => { + it("should use a virtual-hosted-style endpoint by default", () => { + const { bucketEndpoint, hostname } = bucketHostname({ + bucketName: "foo", + baseHostname, + isCustomEndpoint, + clientRegion: region, + }); - expect(bucketEndpoint).toBe(true); - expect(hostname).toBe(`foo.${baseHostname}`); - }); + expect(bucketEndpoint).toBe(true); + expect(hostname).toBe(`foo.${baseHostname}`); + }); - it("should use a path-style endpoint when requested", () => { - const baseHostname = "s3.us-west-2.amazonaws.com"; - const { bucketEndpoint, hostname } = bucketHostname({ - bucketName: "foo", - baseHostname, - pathStyleEndpoint: true, - }); + it("should use a path-style endpoint when requested", () => { + const { bucketEndpoint, hostname } = bucketHostname({ + bucketName: "foo", + baseHostname, + isCustomEndpoint, + clientRegion: region, + pathStyleEndpoint: true, + }); - expect(bucketEndpoint).toBe(false); - expect(hostname).toBe(baseHostname); + expect(bucketEndpoint).toBe(false); + expect(hostname).toBe(baseHostname); + }); + + it("should use a path-style endpoint when the bucket name contains a dot", () => { + const { bucketEndpoint, hostname } = bucketHostname({ + bucketName: "foo.bar", + baseHostname, + isCustomEndpoint, + clientRegion: region, + }); + + expect(bucketEndpoint).toBe(false); + expect(hostname).toBe(baseHostname); + }); + + it("should use a virtual-hosted-style endpoint when SSL compatibility is not requested and the bucket name contains a dot", () => { + const { bucketEndpoint, hostname } = bucketHostname({ + bucketName: "foo.bar", + baseHostname, + isCustomEndpoint, + clientRegion: region, + tlsCompatible: false, + }); + + expect(bucketEndpoint).toBe(true); + expect(hostname).toBe(`foo.bar.${baseHostname}`); + }); + + for (const nonDnsCompliantBucketName of [ + // too short + "fo", + // too long + // eslint-disable-next-line @typescript-eslint/no-unused-vars + new Array(64).map((_) => "a").join(""), + // leading period + ".myawsbucket", + // trailing period + "myawsbucket.", + // sequential periods + "my..examplebucket", + // capital letters + "MyAWSBucket", + // IP address + "192.168.5.4", + ]) { + it(`should use a path-style endpoint for the non-DNS-compliant bucket name of ${nonDnsCompliantBucketName}`, () => { + const { bucketEndpoint, hostname } = bucketHostname({ + bucketName: nonDnsCompliantBucketName, + baseHostname, + isCustomEndpoint, + clientRegion: region, + }); + + expect(bucketEndpoint).toBe(false); + expect(hostname).toBe(baseHostname); + }); + } + }); }); it("should ignore transfer acceleration when a path-style endpoint is requested", () => { @@ -32,6 +97,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: "foo", baseHostname, + isCustomEndpoint: false, + clientRegion: region, pathStyleEndpoint: true, accelerateEndpoint: true, }); @@ -40,29 +107,6 @@ describe("bucketHostname", () => { expect(hostname).toBe(baseHostname); }); - it("should use a path-style endpoint when the bucket name contains a dot", () => { - const baseHostname = "s3.us-west-2.amazonaws.com"; - const { bucketEndpoint, hostname } = bucketHostname({ - bucketName: "foo.bar", - baseHostname, - }); - - expect(bucketEndpoint).toBe(false); - expect(hostname).toBe(baseHostname); - }); - - it("should use a virtual-hosted-style endpoint when SSL compatibility is not requested and the bucket name contains a dot", () => { - const baseHostname = "s3.us-west-2.amazonaws.com"; - const { bucketEndpoint, hostname } = bucketHostname({ - bucketName: "foo.bar", - baseHostname, - tlsCompatible: false, - }); - - expect(bucketEndpoint).toBe(true); - expect(hostname).toBe(`foo.bar.${baseHostname}`); - }); - for (const [baseHostname, dualstackHostname] of [ ["s3.amazonaws.com", "s3.dualstack.us-east-1.amazonaws.com"], ["s3-external-1.amazonaws.com", "s3.dualstack.us-east-1.amazonaws.com"], @@ -73,6 +117,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: "foo", baseHostname, + isCustomEndpoint: false, + clientRegion: region, accelerateEndpoint: true, }); @@ -84,6 +130,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: "foo", baseHostname, + isCustomEndpoint: false, + clientRegion: region, accelerateEndpoint: true, dualstackEndpoint: true, }); @@ -96,6 +144,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: "foo", baseHostname, + isCustomEndpoint: false, + clientRegion: region, dualstackEndpoint: true, }); @@ -107,6 +157,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: "foo", baseHostname, + isCustomEndpoint: false, + clientRegion: region, dualstackEndpoint: true, pathStyleEndpoint: true, }); @@ -116,69 +168,69 @@ describe("bucketHostname", () => { }); } - for (const nonDnsCompliantBucketName of [ - // too short - "fo", - // too long - // eslint-disable-next-line @typescript-eslint/no-unused-vars - new Array(64).map((_) => "a").join(""), - // leading period - ".myawsbucket", - // trailing period - "myawsbucket.", - // sequential periods - "my..examplebucket", - // capital letters - "MyAWSBucket", - // IP address - "192.168.5.4", - ]) { - it(`should use a path-style endpoint for the non-DNS-compliant bucket name of ${nonDnsCompliantBucketName}`, () => { - const baseHostname = "s3.us-west-2.amazonaws.com"; - const { bucketEndpoint, hostname } = bucketHostname({ - bucketName: nonDnsCompliantBucketName, - baseHostname, + describe("should throw when provided a non-S3 hostname with", () => { + ["dualstackEndpoint", "accelerateEndpoint"].forEach((option) => { + it(`${option} enabled`, () => { + expect(() => { + bucketHostname({ + bucketName: "foo", + baseHostname: "example.com", + isCustomEndpoint: true, + clientRegion: region, + [option]: true, + }); + }).toThrow("endpoint is not supported with custom endpoint"); }); - - expect(bucketEndpoint).toBe(false); - expect(hostname).toBe(baseHostname); - }); - } - - it("should perform no transformations when provided a non-S3 hostname", () => { - expect( - bucketHostname({ - bucketName: "foo", - baseHostname: "example.com", - }) - ).toEqual({ - bucketEndpoint: false, - hostname: "example.com", }); }); }); describe("from Access Point ARN", () => { describe("populates access point endpoint from ARN", () => { - it("should use client region", () => { - const baseHostname = "s3.us-west-2.amazonaws.com"; - const { bucketEndpoint, hostname } = bucketHostname({ - bucketName: parseArn("arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint"), - baseHostname, + const s3Hostname = "s3.us-west-2.amazonaws.com"; + const customHostname = "example.com"; + + describe(`baseHostname: ${s3Hostname}`, () => { + const baseHostname = s3Hostname; + it("should use client region", () => { + const { bucketEndpoint, hostname } = bucketHostname({ + bucketName: parseArn("arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint"), + baseHostname, + isCustomEndpoint: false, + clientRegion: region, + }); + expect(bucketEndpoint).toBe(true); + expect(hostname).toBe("myendpoint-123456789012.s3-accesspoint.us-west-2.amazonaws.com"); + }); + + it("should use ARN region", () => { + const { bucketEndpoint, hostname } = bucketHostname({ + bucketName: parseArn("arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint"), + baseHostname, + isCustomEndpoint: false, + clientRegion: region, + useArnRegion: true, + }); + expect(bucketEndpoint).toBe(true); + expect(hostname).toBe("myendpoint-123456789012.s3-accesspoint.us-east-1.amazonaws.com"); }); - expect(bucketEndpoint).toBe(true); - expect(hostname).toBe("myendpoint-123456789012.s3-accesspoint.us-west-2.amazonaws.com"); }); - it("should use ARN region", () => { - const baseHostname = "s3.us-west-2.amazonaws.com"; - const { bucketEndpoint, hostname } = bucketHostname({ - bucketName: parseArn("arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint"), - baseHostname, - useArnRegion: true, + describe(`baseHostname: ${customHostname}`, () => { + const baseHostname = customHostname; + [true, false].forEach((useArnRegion) => { + it(`should ignore useArnRegion=${useArnRegion}`, () => { + const { bucketEndpoint, hostname } = bucketHostname({ + bucketName: parseArn("arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint"), + baseHostname, + isCustomEndpoint: true, + clientRegion: "us-east-1", + useArnRegion, + }); + expect(bucketEndpoint).toBe(true); + expect(hostname).toBe(`myendpoint-123456789012.${baseHostname}`); + }); }); - expect(bucketEndpoint).toBe(true); - expect(hostname).toBe("myendpoint-123456789012.s3-accesspoint.us-east-1.amazonaws.com"); }); }); @@ -189,6 +241,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: parseArn("arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint"), baseHostname, + isCustomEndpoint: false, + clientRegion: region, clientSigningRegion: "us-east-1", }); expect(bucketEndpoint).toBe(true); @@ -202,6 +256,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: parseArn("arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint"), baseHostname, + isCustomEndpoint: false, + clientRegion: region, clientSigningRegion: "us-east-1", useArnRegion: true, }); @@ -216,6 +272,8 @@ describe("bucketHostname", () => { bucketHostname({ bucketName: parseArn("arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint"), baseHostname: "s3.us-west-2.amazonaws.com", + isCustomEndpoint: false, + clientRegion: region, }); }).toThrow("Region in ARN is incompatible, got us-east-1 but expected us-west-2"); }); @@ -224,6 +282,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: parseArn("arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint"), baseHostname: "s3.us-west-2.amazonaws.com", + isCustomEndpoint: false, + clientRegion: region, useArnRegion: true, dualstackEndpoint: true, }); @@ -238,6 +298,8 @@ describe("bucketHostname", () => { bucketHostname({ bucketName: bucketArn, baseHostname: "s3.us-west-2.amazonaws.com", + isCustomEndpoint: false, + clientRegion: region, useArnRegion: true, }); }).toThrow(`Partition in ARN is incompatible, got "aws-cn" but expected "aws"`); @@ -247,6 +309,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: parseArn("arn:aws-cn:s3:cn-northwest-1:123456789012:accesspoint:myendpoint"), baseHostname: "s3.cn-north-1.amazonaws.com.cn", + isCustomEndpoint: false, + clientRegion: "cn-north-1", clientPartition: "aws-cn", useArnRegion: true, }); @@ -258,6 +322,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: bucketArn, baseHostname: "s3.cn-north-1.amazonaws.com.cn", + isCustomEndpoint: false, + clientRegion: "cn-north-1", clientPartition: "aws-cn", }); expect(bucketEndpoint).toBe(true); @@ -271,6 +337,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: bucketArn, baseHostname: "s3.fips-us-gov-east-1.amazonaws.com", + isCustomEndpoint: false, + clientRegion: "us-gov-east-1", clientPartition: "aws-us-gov", }); expect(bucketEndpoint).toBe(true); @@ -281,6 +349,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: bucketArn, baseHostname: "s3.fips-us-gov-east-1.amazonaws.com", + isCustomEndpoint: false, + clientRegion: "us-gov-east-1", clientPartition: "aws-us-gov", useArnRegion: true, }); @@ -292,6 +362,8 @@ describe("bucketHostname", () => { const { bucketEndpoint, hostname } = bucketHostname({ bucketName: bucketArn, baseHostname: "s3.fips-us-gov-east-1.amazonaws.com", + isCustomEndpoint: false, + clientRegion: "us-gov-east-1", clientPartition: "aws-us-gov", useArnRegion: true, dualstackEndpoint: true, @@ -306,57 +378,66 @@ describe("bucketHostname", () => { bucketHostname({ bucketName: parseArn("arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint"), baseHostname: "s3.us-west-2.amazonaws.com", + isCustomEndpoint: false, + clientRegion: region, accelerateEndpoint: true, }); }).toThrow("Accelerate endpoint is not supported when bucket is an ARN"); }); - describe("should validate Access Point ARN", () => { - [ - { - bucketArn: "arn:aws:sqs:us-west-2:123456789012:someresource", - message: "Expect 's3' or 's3-outposts' in ARN service component", - }, - { - bucketArn: "arn:aws:s3:us-west-2:123456789012:bucket_name:mybucket", - message: "ARN resource should begin with 'accesspoint:' or 'outpost:'", - }, - { - bucketArn: "arn:aws:s3::123456789012:accesspoint:myendpoint", - message: "ARN region is empty", - }, - { - bucketArn: "arn:aws:s3:us-west-2::accesspoint:myendpoint", - message: "Access point ARN accountID does not match regex '[0-9]{12}'", - }, - { - bucketArn: "arn:aws:s3:us-west-2:123.45678.9012:accesspoint:mybucket", - message: "Access point ARN accountID does not match regex '[0-9]{12}'", - }, - { - bucketArn: "arn:aws:s3:us-west-2:123456789012:accesspoint:", - message: "Access Point ARN should have one resource accesspoint:{accesspointname}", - }, - { - bucketArn: "arn:aws:s3:us-west-2:123456789012:accesspoint:*", - message: "Invalid DNS label *-123456789012", - }, - { - bucketArn: "arn:aws:s3:us-west-2:123456789012:accesspoint:my.bucket", - message: "Invalid DNS label my.bucket-123456789012", - }, - { - bucketArn: "arn:aws:s3:us-west-2:123456789012:accesspoint:mybucket:object:foo ", - message: "Access Point ARN should have one resource accesspoint:{accesspointname}", - }, - ].forEach(({ bucketArn, message }) => { - it(`should throw "${message}"`, () => { - expect(() => { - bucketHostname({ - bucketName: parseArn(bucketArn), - baseHostname: "s3.us-west-2.amazonaws.com", - }); - }).toThrow(message); + [ + { baseHostname: "s3.us-west-2.amazonaws.com", isCustomEndpoint: false }, + { baseHostname: "beta.example.com", isCustomEndpoint: true }, + ].forEach(({ baseHostname, isCustomEndpoint }) => { + describe(`should validate Access Point ARN with baseHostname: ${baseHostname}`, () => { + [ + { + bucketArn: "arn:aws:sqs:us-west-2:123456789012:someresource", + message: "Expect 's3' or 's3-outposts' in ARN service component", + }, + { + bucketArn: "arn:aws:s3:us-west-2:123456789012:bucket_name:mybucket", + message: "ARN resource should begin with 'accesspoint:' or 'outpost:'", + }, + { + bucketArn: "arn:aws:s3::123456789012:accesspoint:myendpoint", + message: "ARN region is empty", + }, + { + bucketArn: "arn:aws:s3:us-west-2::accesspoint:myendpoint", + message: "Access point ARN accountID does not match regex '[0-9]{12}'", + }, + { + bucketArn: "arn:aws:s3:us-west-2:123.45678.9012:accesspoint:mybucket", + message: "Access point ARN accountID does not match regex '[0-9]{12}'", + }, + { + bucketArn: "arn:aws:s3:us-west-2:123456789012:accesspoint:", + message: "Access Point ARN should have one resource accesspoint:{accesspointname}", + }, + { + bucketArn: "arn:aws:s3:us-west-2:123456789012:accesspoint:*", + message: "Invalid DNS label *-123456789012", + }, + { + bucketArn: "arn:aws:s3:us-west-2:123456789012:accesspoint:my.bucket", + message: "Invalid DNS label my.bucket-123456789012", + }, + { + bucketArn: "arn:aws:s3:us-west-2:123456789012:accesspoint:mybucket:object:foo ", + message: "Access Point ARN should have one resource accesspoint:{accesspointname}", + }, + ].forEach(({ bucketArn, message }) => { + it(`should throw "${message}"`, () => { + expect(() => { + bucketHostname({ + bucketName: parseArn(bucketArn), + baseHostname, + isCustomEndpoint, + clientRegion: region, + }); + }).toThrow(message); + }); }); }); }); @@ -367,6 +448,8 @@ describe("bucketHostname", () => { bucketHostname({ bucketName: bucketArn, baseHostname: "s3.us-east-1.amazonaws.com", + isCustomEndpoint: false, + clientRegion: "us-east-1", useArnRegion: true, }).signingRegion ).toBe("us-west-2"); @@ -375,36 +458,71 @@ describe("bucketHostname", () => { describe("from Outpost ARN", () => { describe("populates access point endpoint from ARN", () => { - it("should use client region", () => { - const baseHostname = "s3.us-west-2.amazonaws.com"; - const expectedEndpoint = "myaccesspoint-123456789012.op-01234567890123456.s3-outposts.us-west-2.amazonaws.com"; - [ - "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint", - "arn:aws:s3-outposts:us-west-2:123456789012:outpost/op-01234567890123456/accesspoint/myaccesspoint", - ].forEach((outpostArn) => { - const { bucketEndpoint, hostname } = bucketHostname({ - bucketName: parseArn(outpostArn), - baseHostname, + const s3Hostname = "s3.us-west-2.amazonaws.com"; + const customHostname = "example.com"; + + describe(`baseHostname: ${s3Hostname}`, () => { + const baseHostname = s3Hostname; + it("should use client region", () => { + const region = "us-west-2"; + const expectedEndpoint = + "myaccesspoint-123456789012.op-01234567890123456.s3-outposts.us-west-2.amazonaws.com"; + [ + "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint", + "arn:aws:s3-outposts:us-west-2:123456789012:outpost/op-01234567890123456/accesspoint/myaccesspoint", + ].forEach((outpostArn) => { + const { bucketEndpoint, hostname } = bucketHostname({ + bucketName: parseArn(outpostArn), + baseHostname, + isCustomEndpoint: false, + clientRegion: region, + }); + expect(bucketEndpoint).toBe(true); + expect(hostname).toBe(expectedEndpoint); + }); + }); + + it("should use ARN region", () => { + const region = "us-west-2"; + const expectedEndpoint = + "myaccesspoint-123456789012.op-01234567890123456.s3-outposts.us-east-1.amazonaws.com"; + [ + "arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint", + "arn:aws:s3-outposts:us-east-1:123456789012:outpost/op-01234567890123456/accesspoint/myaccesspoint", + ].forEach((outpostArn) => { + const { bucketEndpoint, hostname } = bucketHostname({ + bucketName: parseArn(outpostArn), + baseHostname, + isCustomEndpoint: false, + clientRegion: region, + useArnRegion: true, + }); + expect(bucketEndpoint).toBe(true); + expect(hostname).toBe(expectedEndpoint); }); - expect(bucketEndpoint).toBe(true); - expect(hostname).toBe(expectedEndpoint); }); }); - it("should use ARN region", () => { - const baseHostname = "s3.us-west-2.amazonaws.com"; - const expectedEndpoint = "myaccesspoint-123456789012.op-01234567890123456.s3-outposts.us-east-1.amazonaws.com"; - [ - "arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint", - "arn:aws:s3-outposts:us-east-1:123456789012:outpost/op-01234567890123456/accesspoint/myaccesspoint", - ].forEach((outpostArn) => { - const { bucketEndpoint, hostname } = bucketHostname({ - bucketName: parseArn(outpostArn), - baseHostname, - useArnRegion: true, + describe(`baseHostname: ${customHostname}`, () => { + const baseHostname = customHostname; + [true, false].forEach((useArnRegion) => { + [ + "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint", + "arn:aws:s3-outposts:us-west-2:123456789012:outpost/op-01234567890123456/accesspoint/myaccesspoint", + ].forEach((outpostArn) => { + it(`should ignore useArnRegion=${useArnRegion}`, () => { + const region = "us-west-2"; + const { bucketEndpoint, hostname } = bucketHostname({ + bucketName: parseArn(outpostArn), + baseHostname, + isCustomEndpoint: true, + clientRegion: region, + useArnRegion, + }); + expect(bucketEndpoint).toBe(true); + expect(hostname).toBe(`myaccesspoint-123456789012.op-01234567890123456.${baseHostname}`); + }); }); - expect(bucketEndpoint).toBe(true); - expect(hostname).toBe(expectedEndpoint); }); }); }); @@ -416,6 +534,8 @@ describe("bucketHostname", () => { "arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint" ), baseHostname: "s3.us-west-2.amazonaws.com", + isCustomEndpoint: false, + clientRegion: region, }); }).toThrow("Region in ARN is incompatible, got us-east-1 but expected us-west-2"); }); @@ -427,6 +547,8 @@ describe("bucketHostname", () => { "arn:aws-cn:s3-outposts:cn-north-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint" ), baseHostname: "s3.us-west-2.amazonaws.com", + isCustomEndpoint: false, + clientRegion: region, useArnRegion: true, }); }).toThrow(`Partition in ARN is incompatible, got "aws-cn" but expected "aws"`); @@ -441,6 +563,8 @@ describe("bucketHostname", () => { "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint" ), baseHostname: "s3.fips-us-gov-east-1.amazonaws.com", + isCustomEndpoint: false, + clientRegion: "us-gov-east-1", clientPartition: "aws-us-gov", }); }).toThrow("FIPS region is not supported with Outpost, got fips-us-gov-east-1"); @@ -451,6 +575,8 @@ describe("bucketHostname", () => { "arn:aws-us-gov:s3-outposts:fips-us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint" ), baseHostname: "s3.fips-us-gov-east-1.amazonaws.com", + isCustomEndpoint: false, + clientRegion: "us-gov-east-1", clientPartition: "aws-us-gov", useArnRegion: true, }); @@ -463,6 +589,8 @@ describe("bucketHostname", () => { "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint" ), baseHostname: "s3.fips-us-gov-east-1.amazonaws.com", + isCustomEndpoint: false, + clientRegion: "us-gov-east-1", clientPartition: "aws-us-gov", useArnRegion: true, }); @@ -473,74 +601,99 @@ describe("bucketHostname", () => { }); }); - it("should throw if dualstack is set", () => { - expect(() => { - bucketHostname({ - bucketName: parseArn( - "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint" - ), - baseHostname: "s3.us-west-2.amazonaws.com", - dualstackEndpoint: true, - }); - }).toThrow("Dualstack endpoint is not supported with Outpost"); - }); - - it("should throw if accelerate endpoint is set", () => { - expect(() => { - bucketHostname({ - bucketName: parseArn( - "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint" - ), - baseHostname: "s3.us-west-2.amazonaws.com", - accelerateEndpoint: true, + describe("should throw if dualstack is set", () => { + [ + { baseHostname: "s3.us-west-2.amazonaws.com", isCustomEndpoint: false }, + { baseHostname: "beta.example.com", isCustomEndpoint: true }, + ].forEach(({ baseHostname, isCustomEndpoint }) => { + it(`with baseHostname: ${baseHostname}`, () => { + expect(() => { + bucketHostname({ + bucketName: parseArn( + "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint" + ), + baseHostname, + isCustomEndpoint, + clientRegion: region, + dualstackEndpoint: true, + }); + }).toThrow("Dualstack endpoint is not supported"); }); - }).toThrow("Accelerate endpoint is not supported when bucket is an ARN"); + }); }); - describe("should validate Access Point ARN", () => { + describe("should throw if accelerate endpoint is set", () => { [ - { - outpostArn: "arn:aws:s3-outposts:us-west-2:123456789012:outpost", - message: "Outpost ARN should have resource outpost/{outpostId}/accesspoint/{accesspointName}", - }, - { - outpostArn: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456", - message: "Outpost ARN should have resource outpost:{outpostId}:accesspoint:{accesspointName}", - }, - { - outpostArn: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:myaccesspoint", - message: "Outpost ARN should have resource outpost:{outpostId}:accesspoint:{accesspointName}", - }, - { - outpostArn: "arn:aws:s3-outposts:us-west-2:123456789012:outpost::accesspoint:myaccesspoint", - message: "Outpost ARN should have resource outpost:{outpostId}:accesspoint:{accesspointName}", - }, - { - outpostArn: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint", - message: "Outpost ARN should have resource outpost:{outpostId}:accesspoint:{accesspointName}", - }, - { - outpostArn: - "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:mybucket:object:foo", - message: "Outpost ARN should have resource outpost:{outpostId}:accesspoint:{accesspointName}", - }, - { - outpostArn: - "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-0123456.890123456:accesspoint:myaccesspoint", - message: "Invalid DNS label op-0123456.890123456", - }, - { - outpostArn: "arn:aws:s3:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint", - message: "Expect 's3-posts' in Outpost ARN service component", - }, - ].forEach(({ outpostArn, message }) => { - it(`should throw "${message}"`, () => { + { baseHostname: "s3.us-west-2.amazonaws.com", isCustomEndpoint: false }, + { baseHostname: "beta.example.com", isCustomEndpoint: true }, + ].forEach(({ baseHostname, isCustomEndpoint }) => { + it(`with baseHostname: ${baseHostname}`, () => { expect(() => { bucketHostname({ - bucketName: parseArn(outpostArn), - baseHostname: "s3.us-west-2.amazonaws.com", + bucketName: parseArn( + "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint" + ), + baseHostname, + isCustomEndpoint, + clientRegion: region, + accelerateEndpoint: true, }); - }).toThrow(message); + }).toThrow("Accelerate endpoint is not supported"); + }); + }); + }); + + [ + { baseHostname: "s3.us-west-2.amazonaws.com", isCustomEndpoint: false }, + { baseHostname: "beta.example.com", isCustomEndpoint: true }, + ].forEach(({ baseHostname, isCustomEndpoint }) => { + describe(`should validate Outpost ARN with baseHostname: ${baseHostname}`, () => { + [ + { + outpostArn: "arn:aws:s3-outposts:us-west-2:123456789012:outpost", + message: "Outpost ARN should have resource outpost/{outpostId}/accesspoint/{accesspointName}", + }, + { + outpostArn: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456", + message: "Outpost ARN should have resource outpost:{outpostId}:accesspoint:{accesspointName}", + }, + { + outpostArn: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:myaccesspoint", + message: "Outpost ARN should have resource outpost:{outpostId}:accesspoint:{accesspointName}", + }, + { + outpostArn: "arn:aws:s3-outposts:us-west-2:123456789012:outpost::accesspoint:myaccesspoint", + message: "Outpost ARN should have resource outpost:{outpostId}:accesspoint:{accesspointName}", + }, + { + outpostArn: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint", + message: "Outpost ARN should have resource outpost:{outpostId}:accesspoint:{accesspointName}", + }, + { + outpostArn: + "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:mybucket:object:foo", + message: "Outpost ARN should have resource outpost:{outpostId}:accesspoint:{accesspointName}", + }, + { + outpostArn: + "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-0123456.890123456:accesspoint:myaccesspoint", + message: "Invalid DNS label op-0123456.890123456", + }, + { + outpostArn: "arn:aws:s3:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint", + message: "Expect 's3-posts' in Outpost ARN service component", + }, + ].forEach(({ outpostArn, message }) => { + it(`should throw "${message}"`, () => { + expect(() => { + bucketHostname({ + bucketName: parseArn(outpostArn), + baseHostname, + isCustomEndpoint, + clientRegion: region, + }); + }).toThrow(message); + }); }); }); }); @@ -552,6 +705,8 @@ describe("bucketHostname", () => { const { signingRegion, signingService } = bucketHostname({ bucketName: bucketArn, baseHostname: "s3.us-east-1.amazonaws.com", + isCustomEndpoint: false, + clientRegion: "us-east-1", useArnRegion: true, }); expect(signingRegion).toBe("us-west-2"); diff --git a/packages/middleware-bucket-endpoint/src/bucketHostname.ts b/packages/middleware-bucket-endpoint/src/bucketHostname.ts index 442b1904537c1..23d8cfbfcf75e 100644 --- a/packages/middleware-bucket-endpoint/src/bucketHostname.ts +++ b/packages/middleware-bucket-endpoint/src/bucketHostname.ts @@ -7,7 +7,6 @@ import { getSuffixForArnEndpoint, isBucketNameOptions, isDnsCompatibleBucketName, - S3_HOSTNAME_PATTERN, validateAccountId, validateArnEndpointOptions, validateDNSHostLabel, @@ -28,23 +27,27 @@ export interface BucketHostname { } export const bucketHostname = (options: BucketHostnameParams | ArnHostnameParams): BucketHostname => { - const { baseHostname } = options; - if (!S3_HOSTNAME_PATTERN.test(baseHostname)) { - return { - bucketEndpoint: false, - hostname: baseHostname, - }; + const { isCustomEndpoint, baseHostname, dualstackEndpoint, accelerateEndpoint } = options; + + if (isCustomEndpoint) { + if (dualstackEndpoint) throw new Error("Dualstack endpoint is not supported with custom endpoint"); + if (accelerateEndpoint) throw new Error("Accelerate endpoint is not supported with custom endpoint"); } + return isBucketNameOptions(options) ? // Construct endpoint when bucketName is a string referring to a bucket name - getEndpointFromBucketName(options) + getEndpointFromBucketName({ ...options, isCustomEndpoint }) : // Construct endpoint when bucketName is an ARN referring to an S3 resource like Access Point - getEndpointFromArn(options); + getEndpointFromArn({ ...options, isCustomEndpoint }); }; -const getEndpointFromArn = (options: ArnHostnameParams): BucketHostname => { - // Infer client region and hostname suffix from hostname from endpoints.json, like `s3.us-west-2.amazonaws.com` - const [clientRegion, hostnameSuffix] = getSuffixForArnEndpoint(options.baseHostname); +const getEndpointFromArn = (options: ArnHostnameParams & { isCustomEndpoint: boolean }): BucketHostname => { + const { isCustomEndpoint, baseHostname } = options; + const [clientRegion, hostnameSuffix] = isCustomEndpoint + ? [options.clientRegion, baseHostname] + : // Infer client region and hostname suffix from hostname from endpoints.json, like `s3.us-west-2.amazonaws.com` + getSuffixForArnEndpoint(baseHostname); + const { pathStyleEndpoint, dualstackEndpoint = false, @@ -75,33 +78,37 @@ const getEndpointFromArn = (options: ArnHostnameParams): BucketHostname => { validateDNSHostLabel(outpostId, { tlsCompatible }); validateNoDualstack(dualstackEndpoint); validateNoFIPS(endpointRegion); + const hostnamePrefix = `${accesspointName}-${accountId}.${outpostId}`; return { bucketEndpoint: true, - hostname: `${accesspointName}-${accountId}.${outpostId}.s3-outposts.${endpointRegion}.${hostnameSuffix}`, + hostname: `${hostnamePrefix}${isCustomEndpoint ? "" : `.s3-outposts.${endpointRegion}`}.${hostnameSuffix}`, signingRegion, signingService: "s3-outposts", }; } // construct endpoint from Accesspoint ARN validateS3Service(service); + const hostnamePrefix = `${accesspointName}-${accountId}`; return { bucketEndpoint: true, - hostname: `${accesspointName}-${accountId}.s3-accesspoint${ - dualstackEndpoint ? ".dualstack" : "" - }.${endpointRegion}.${hostnameSuffix}`, + hostname: `${hostnamePrefix}${ + isCustomEndpoint ? "" : `.s3-accesspoint${dualstackEndpoint ? ".dualstack" : ""}.${endpointRegion}` + }.${hostnameSuffix}`, signingRegion, }; }; const getEndpointFromBucketName = ({ accelerateEndpoint = false, + clientRegion: region, baseHostname, bucketName, dualstackEndpoint = false, pathStyleEndpoint = false, tlsCompatible = true, -}: BucketHostnameParams): BucketHostname => { - const [clientRegion, hostnameSuffix] = getSuffix(baseHostname); + isCustomEndpoint = false, +}: BucketHostnameParams & { isCustomEndpoint: boolean }): BucketHostname => { + const [clientRegion, hostnameSuffix] = isCustomEndpoint ? [region, baseHostname] : getSuffix(baseHostname); if (pathStyleEndpoint || !isDnsCompatibleBucketName(bucketName) || (tlsCompatible && DOT_PATTERN.test(bucketName))) { return { bucketEndpoint: false, diff --git a/packages/middleware-bucket-endpoint/src/bucketHostnameUtils.ts b/packages/middleware-bucket-endpoint/src/bucketHostnameUtils.ts index 974093e173f9a..4a78e0b28eff5 100644 --- a/packages/middleware-bucket-endpoint/src/bucketHostnameUtils.ts +++ b/packages/middleware-bucket-endpoint/src/bucketHostnameUtils.ts @@ -13,8 +13,10 @@ export interface AccessPointArn extends ARN { } export interface BucketHostnameParams { + isCustomEndpoint: boolean; baseHostname: string; bucketName: string; + clientRegion: string; accelerateEndpoint?: boolean; dualstackEndpoint?: boolean; pathStyleEndpoint?: boolean; diff --git a/packages/middleware-bucket-endpoint/src/configurations.ts b/packages/middleware-bucket-endpoint/src/configurations.ts index a067ac17f04e6..29b95c861c719 100644 --- a/packages/middleware-bucket-endpoint/src/configurations.ts +++ b/packages/middleware-bucket-endpoint/src/configurations.ts @@ -25,11 +25,13 @@ export interface BucketEndpointInputConfig { } interface PreviouslyResolved { + isCustomEndpoint: boolean; region: Provider; regionInfoProvider: RegionInfoProvider; } export interface BucketEndpointResolvedConfig { + isCustomEndpoint: boolean; bucketEndpoint: boolean; forcePathStyle: boolean; useAccelerateEndpoint: boolean; diff --git a/packages/middleware-sdk-s3-control/src/configurations.ts b/packages/middleware-sdk-s3-control/src/configurations.ts index 774326040f4ee..14f1405483e2e 100644 --- a/packages/middleware-sdk-s3-control/src/configurations.ts +++ b/packages/middleware-sdk-s3-control/src/configurations.ts @@ -13,11 +13,13 @@ export interface S3ControlInputConfig { } interface PreviouslyResolved { + isCustomEndpoint: boolean; region: Provider; regionInfoProvider: RegionInfoProvider; } export interface S3ControlResolvedConfig { + isCustomEndpoint: boolean; useDualstackEndpoint: boolean; useArnRegion: Provider; region: Provider; diff --git a/packages/middleware-sdk-s3-control/src/process-arnables-plugin/plugin.spec.ts b/packages/middleware-sdk-s3-control/src/process-arnables-plugin/plugin.spec.ts index 668fe60e12851..8e14d905f73a5 100644 --- a/packages/middleware-sdk-s3-control/src/process-arnables-plugin/plugin.spec.ts +++ b/packages/middleware-sdk-s3-control/src/process-arnables-plugin/plugin.spec.ts @@ -20,6 +20,7 @@ describe("getProcessArnablesMiddleware", () => { regionInfoProvider: options.regionInfoProvider ?? jest.fn().mockResolvedValue({ partition: "aws" }), region: jest.fn().mockResolvedValue(options.region), useArnRegion: jest.fn().mockResolvedValue(options.useArnRegion ?? false), + isCustomEndpoint: false, }; }; diff --git a/packages/middleware-sdk-s3-control/src/process-arnables-plugin/plugin.ts b/packages/middleware-sdk-s3-control/src/process-arnables-plugin/plugin.ts index e6602872fa91e..ea2e8f7002240 100644 --- a/packages/middleware-sdk-s3-control/src/process-arnables-plugin/plugin.ts +++ b/packages/middleware-sdk-s3-control/src/process-arnables-plugin/plugin.ts @@ -7,6 +7,6 @@ import { updateArnablesRequestMiddleware, updateArnablesRequestMiddlewareOptions export const getProcessArnablesPlugin = (options: S3ControlResolvedConfig): Pluggable => ({ applyToStack: (clientStack) => { clientStack.add(parseOutpostArnablesMiddleaware(options), parseOutpostArnablesMiddleawareOptions); - clientStack.add(updateArnablesRequestMiddleware, updateArnablesRequestMiddlewareOptions); + clientStack.add(updateArnablesRequestMiddleware(options), updateArnablesRequestMiddlewareOptions); }, }); diff --git a/packages/middleware-sdk-s3-control/src/process-arnables-plugin/update-arnables-request.ts b/packages/middleware-sdk-s3-control/src/process-arnables-plugin/update-arnables-request.ts index da2f415e34de3..3db0d00d00a33 100644 --- a/packages/middleware-sdk-s3-control/src/process-arnables-plugin/update-arnables-request.ts +++ b/packages/middleware-sdk-s3-control/src/process-arnables-plugin/update-arnables-request.ts @@ -1,6 +1,7 @@ import { HttpRequest } from "@aws-sdk/protocol-http"; import { BuildHandlerOptions, BuildMiddleware } from "@aws-sdk/types"; +import { S3ControlResolvedConfig } from "../configurations"; import { CONTEXT_ACCOUNT_ID, CONTEXT_ARN_REGION, CONTEXT_OUTPOST_ID } from "../constants"; const ACCOUNT_ID_HEADER = "x-amz-account-id"; @@ -11,23 +12,35 @@ const REGEX_S3CONTROL_HOSTNAME = /^(.+\.)?s3-control[.-]([a-z0-9-]+)\./; * After outpost request is constructed, redirect request to outpost endpoint and set `x-amz-account-id` and * `x-amz-outpost-id` headers. */ -export const updateArnablesRequestMiddleware: BuildMiddleware = (next, context) => (args) => { +export const updateArnablesRequestMiddleware = ({ + isCustomEndpoint, +}: { + isCustomEndpoint: boolean; +}): BuildMiddleware => (next, context) => (args) => { const { request } = args; if (!HttpRequest.isInstance(request)) return next(args); if (context[CONTEXT_ACCOUNT_ID]) request.headers[ACCOUNT_ID_HEADER] = context[CONTEXT_ACCOUNT_ID]; if (context[CONTEXT_OUTPOST_ID]) { request.headers[OUTPOST_ID_HEADER] = context[CONTEXT_OUTPOST_ID]; - request.hostname = getOutpostEndpoint(request.hostname, context[CONTEXT_ARN_REGION]); + request.hostname = getOutpostEndpoint(request.hostname, { + isCustomEndpoint, + regionOverride: context[CONTEXT_ARN_REGION], + }); } return next(args); }; -export const getOutpostEndpoint = (hostname: string, regionOverride?: string): string => { +export const getOutpostEndpoint = ( + hostname: string, + { isCustomEndpoint, regionOverride }: { isCustomEndpoint?: boolean; regionOverride?: string } = {} +): string => { const [matched, prefix, region] = hostname.match(REGEX_S3CONTROL_HOSTNAME)!; // hostname prefix will be ignored even if presents - return ["s3-outposts", regionOverride || region, hostname.replace(new RegExp(`^${matched}`), "")] - .filter((part) => part !== undefined) - .join("."); + return isCustomEndpoint + ? hostname + : ["s3-outposts", regionOverride || region, hostname.replace(new RegExp(`^${matched}`), "")] + .filter((part) => part !== undefined) + .join("."); }; export const updateArnablesRequestMiddlewareOptions: BuildHandlerOptions = { diff --git a/packages/middleware-sdk-s3-control/src/redirect-from-postid.spec.ts b/packages/middleware-sdk-s3-control/src/redirect-from-postid.spec.ts index 20165770bdd2e..590e5795cb3f9 100644 --- a/packages/middleware-sdk-s3-control/src/redirect-from-postid.spec.ts +++ b/packages/middleware-sdk-s3-control/src/redirect-from-postid.spec.ts @@ -6,10 +6,7 @@ describe("redirectFromPostIdMiddleware", () => { it("should redirect request if Bucket is a valid ARN", async () => { const next: any = (args: any) => ({ output: args.request }); const context: any = {}; - const { output } = await redirectFromPostIdMiddleware( - next, - context - )({ + const { output } = await redirectFromPostIdMiddleware({ isCustomEndpoint: false })(next, context)({ input: { OutpostId: "op-123" }, request: new HttpRequest({ hostname: "123456789012.s3-control.us-west-2.amazonaws.com" }), }); diff --git a/packages/middleware-sdk-s3-control/src/redirect-from-postid.ts b/packages/middleware-sdk-s3-control/src/redirect-from-postid.ts index e7fd16916affa..0cbd387117564 100644 --- a/packages/middleware-sdk-s3-control/src/redirect-from-postid.ts +++ b/packages/middleware-sdk-s3-control/src/redirect-from-postid.ts @@ -13,11 +13,15 @@ type InputType = { * If OutpostId is set, redirect hostname to Outpost one, and change signing service to `s3-outposts`. * Applied to S3Control.CreateBucket and S3Control.ListRegionalBuckets */ -export const redirectFromPostIdMiddleware: BuildMiddleware = (next, context) => (args) => { +export const redirectFromPostIdMiddleware = ({ + isCustomEndpoint, +}: { + isCustomEndpoint: boolean; +}): BuildMiddleware => (next, context) => (args) => { const { input, request } = args; if (!HttpRequest.isInstance(request)) return next(args); if (input.OutpostId) { - request.hostname = getOutpostEndpoint(request.hostname); + request.hostname = getOutpostEndpoint(request.hostname, { isCustomEndpoint }); context[CONTEXT_SIGNING_SERVICE] = "s3-outposts"; } return next(args); @@ -32,6 +36,6 @@ export const redirectFromPostIdMiddlewareOptions: BuildHandlerOptions = { export const getRedirectFromPostIdPlugin = (options: S3ControlResolvedConfig): Pluggable => ({ applyToStack: (clientStack) => { - clientStack.add(redirectFromPostIdMiddleware, redirectFromPostIdMiddlewareOptions); + clientStack.add(redirectFromPostIdMiddleware(options), redirectFromPostIdMiddlewareOptions); }, }); diff --git a/packages/protocol-http/src/isValidHostname.spec.ts b/packages/protocol-http/src/isValidHostname.spec.ts index ee073934c214c..aa280d55cf282 100644 --- a/packages/protocol-http/src/isValidHostname.spec.ts +++ b/packages/protocol-http/src/isValidHostname.spec.ts @@ -9,7 +9,7 @@ describe("implementation selection", () => { }); it("should return false for invalid hostnames", () => { - const invalidHostnames = ["foo.com/?bar", ".foo", `${new Array(64).fill("a").join("")}`]; + const invalidHostnames = ["foo.com/?bar", ".foo"]; for (const hostname of invalidHostnames) { expect(isValidHostname(hostname)).toBe(false); } diff --git a/packages/protocol-http/src/isValidHostname.ts b/packages/protocol-http/src/isValidHostname.ts index e4c673942d38d..2f9fdfb1f5912 100644 --- a/packages/protocol-http/src/isValidHostname.ts +++ b/packages/protocol-http/src/isValidHostname.ts @@ -1,4 +1,4 @@ export function isValidHostname(hostname: string): boolean { - const hostPattern = /^[a-z0-9][a-z0-9\.\-]{1,61}[a-z0-9]$/; + const hostPattern = /^[a-z0-9][a-z0-9\.\-]*[a-z0-9]$/; return hostPattern.test(hostname); }