From 0ea65497505aa56fed46e4aeaea21c7f28464427 Mon Sep 17 00:00:00 2001 From: Carlos Juega Date: Tue, 21 Jun 2022 22:48:14 +0200 Subject: [PATCH 1/4] feat(#503): support for mutual TLS authentication added --- README.md | 2 + src/aws/api-gateway-wrapper.ts | 17 ++++++- src/domain-config.ts | 10 ++++ src/types.ts | 2 + test/unit-tests/index.test.ts | 89 ++++++++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 95a87cfd..3c037b61 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,8 @@ custom: | route53Region | `(none)` | Region to send Route53 services requests to (only applicable if also using route53Profile option) | | endpointType | edge | Defines the endpoint type, accepts `regional` or `edge`. | | apiType | rest | Defines the api type, accepts `rest`, `http` or `websocket`. | +| tlsTruststoreUri | `undefined` | An Amazon S3 url that specifies the truststore for mutual TLS authentication, for example `s3://bucket-name/key-name`. The truststore can contain certificates from public or private certificate authorities. Be aware mutual TLS is only available for `regional` APIs. | +| tlsTruststoreVersion | `undefined` | The version of the S3 object that contains your truststore. To specify a version, you must have versioning enabled for the S3 bucket. | | hostedZoneId | | If hostedZoneId is set the route53 record set will be created in the matching zone, otherwise the hosted zone will be figured out from the domainName (hosted zone with matching domain). | | hostedZonePrivate | | If hostedZonePrivate is set to `true` then only private hosted zones will be used for route 53 records. If it is set to `false` then only public hosted zones will be used for route53 records. Setting this parameter is specially useful if you have multiple hosted zones with the same domain name (e.g. a public and a private one) | | enabled | true | Sometimes there are stages for which is not desired to have custom domain names. This flag allows the developer to disable the plugin for such cases. Accepts either `boolean` or `string` values and defaults to `true` for backwards compatibility. | diff --git a/src/aws/api-gateway-wrapper.ts b/src/aws/api-gateway-wrapper.ts index 5f6eaaa0..c3e95dd4 100644 --- a/src/aws/api-gateway-wrapper.ts +++ b/src/aws/api-gateway-wrapper.ts @@ -29,6 +29,7 @@ class APIGatewayWrapper { // For EDGE domain name or TLS 1.0, create with APIGateway (v1) const isEdgeType = domain.endpointType === Globals.endpointTypes.edge; + const hasMutualTls = !!domain.tlsTruststoreUri; if (isEdgeType || domain.securityPolicy === "TLS_1_0") { // Set up parameters const params = { @@ -41,6 +42,13 @@ class APIGatewayWrapper { tags: providerTags, }; + if (!isEdgeType && hasMutualTls) { + params.mutualTlsAuthentication = { + truststoreUri: domain.tlsTruststoreUri, + ...(domain.tlsTruststoreVersion ? {truststoreVersion: domain.tlsTruststoreVersion} : undefined) + }; + } + // Make API call to create domain try { // Creating EDGE domain so use APIGateway (v1) service @@ -51,7 +59,7 @@ class APIGatewayWrapper { } } else { // For Regional domain name create with ApiGatewayV2 - const params = { + const params: any = { DomainName: domain.givenDomainName, DomainNameConfigurations: [{ CertificateArn: domain.certificateArn, @@ -61,6 +69,13 @@ class APIGatewayWrapper { Tags: providerTags }; + if (!isEdgeType && hasMutualTls) { + params.MutualTlsAuthentication = { + TruststoreUri: domain.tlsTruststoreUri, + ...(domain.tlsTruststoreVersion ? {TruststoreVersion: domain.tlsTruststoreVersion} : undefined) + }; + } + // Make API call to create domain try { // Creating Regional domain so use ApiGatewayV2 diff --git a/src/domain-config.ts b/src/domain-config.ts index de5d6bf3..bcd08f0f 100644 --- a/src/domain-config.ts +++ b/src/domain-config.ts @@ -21,6 +21,8 @@ class DomainConfig { public route53Region: string | undefined; public endpointType: string | undefined; public apiType: string | undefined; + public tlsTruststoreUri: string | undefined; + public tlsTruststoreVersion: string | undefined; public hostedZoneId: string | undefined; public hostedZonePrivate: boolean | undefined; public enabled: boolean | string | undefined; @@ -78,6 +80,14 @@ class DomainConfig { } this.apiType = apiTypeToUse; + const isEdgeType = this.endpointType === Globals.endpointTypes.edge; + const hasMutualTls = !!config.tlsTruststoreUri; + if (isEdgeType && hasMutualTls) { + throw new Error(`${this.endpointType} APIs do not support mutual TLS, remove tlsTruststoreUri or change to a regional API.`); + } + this.tlsTruststoreUri = config.tlsTruststoreUri; + this.tlsTruststoreVersion = config.tlsTruststoreVersion; + const securityPolicyDefault = config.securityPolicy || Globals.tlsVersions.tls_1_2; const tlsVersionToUse = Globals.tlsVersions[securityPolicyDefault.toLowerCase()]; if (!tlsVersionToUse) { diff --git a/src/types.ts b/src/types.ts index 2230186c..2dfcad65 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,6 +12,8 @@ export interface CustomDomain { // tslint:disable-line route53Region: string | undefined; endpointType: string | undefined; apiType: string | undefined; + tlsTruststoreUri: string | undefined; + tlsTruststoreVersion: string | undefined; hostedZoneId: string | undefined; hostedZonePrivate: boolean | undefined; enabled: boolean | string | undefined; diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts index 55973d3c..2d1cb96b 100644 --- a/test/unit-tests/index.test.ts +++ b/test/unit-tests/index.test.ts @@ -58,6 +58,8 @@ const constructPlugin = (customDomainOptions, multiple: boolean = false) => { domainName: customDomainOptions.domainName, enabled: customDomainOptions.enabled, endpointType: customDomainOptions.endpointType, + tlsTruststoreUri: customDomainOptions.tlsTruststoreUri, + tlsTruststoreVersion: customDomainOptions.tlsTruststoreVersion, hostedZoneId: customDomainOptions.hostedZoneId, hostedZonePrivate: customDomainOptions.hostedZonePrivate, route53Profile: customDomainOptions.route53Profile, @@ -709,6 +711,80 @@ describe("Custom Domain Plugin", () => { expect(spy).to.have.been.called.with(expectedParams); }); + it("Create a domain name with mutual TLS authentication", async () => { + AWS.mock("APIGateway", "createDomainName", (params, callback) => { + callback(null, {distributionDomainName: "foo", securityPolicy: "TLS_1_0"}); + }); + + const plugin = constructPlugin({ + domainName: "test_domain", + endpointType: "regional", + securityPolicy: "tls_1_0", + tlsTruststoreUri: "s3://bucket-name/key-name" + }); + plugin.initializeVariables(); + plugin.initAWSResources(); + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + dc.certificateArn = "fake_cert"; + + const spy = chai.spy.on(plugin.apiGatewayWrapper.apiGateway, "createDomainName"); + await plugin.apiGatewayWrapper.createCustomDomain(dc); + const expectedParams = { + domainName: dc.givenDomainName, + endpointConfiguration: { + types: [dc.endpointType], + }, + mutualTlsAuthentication: { + truststoreUri: dc.tlsTruststoreUri + }, + securityPolicy: dc.securityPolicy, + tags: { + ...plugin.serverless.service.provider.stackTags, + ...plugin.serverless.service.provider.tags, + }, + regionalCertificateArn: dc.certificateArn + } + expect(spy).to.have.been.called.with(expectedParams); + }); + + it("Create an HTTP domain name with mutual TLS authentication", async () => { + AWS.mock("ApiGatewayV2", "createDomainName", (params, callback) => { + callback(null, params); + }); + + const plugin = constructPlugin({ + domainName: "test_domain", + endpointType: "regional", + apiType: "http", + tlsTruststoreUri: "s3://bucket-name/key-name" + }); + plugin.initializeVariables(); + plugin.initAWSResources(); + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + dc.certificateArn = "fake_cert"; + + const spy = chai.spy.on(plugin.apiGatewayWrapper.apiGatewayV2, "createDomainName"); + await plugin.apiGatewayWrapper.createCustomDomain(dc); + const expectedParams = { + DomainName: dc.givenDomainName, + DomainNameConfigurations: [{ + CertificateArn: dc.certificateArn, + EndpointType: dc.endpointType, + SecurityPolicy: dc.securityPolicy + }], + MutualTlsAuthentication: { + TruststoreUri: dc.tlsTruststoreUri + }, + Tags: { + ...plugin.serverless.service.provider.stackTags, + ...plugin.serverless.service.provider.tags, + } + } + expect(spy).to.have.been.called.with(expectedParams); + }); + it("Create new A and AAAA Alias Records", async () => { AWS.mock("Route53", "listHostedZones", (params, callback) => { // @ts-ignore @@ -1780,6 +1856,19 @@ describe("Custom Domain Plugin", () => { expect(errored).to.equal(true); }); + it("Should throw an Error when mutual TLS is enabled for edge APIs", async () => { + const plugin = constructPlugin({endpointType: "edge", tlsTruststoreUri: "s3://bucket-name/key-name"}); + + let errored = false; + try { + await plugin.hookWrapper(null); + } catch (err) { + errored = true; + expect(err.message).to.equal(`EDGE APIs do not support mutual TLS, remove tlsTruststoreUri or change to a regional API.`); + } + expect(errored).to.equal(true); + }); + afterEach(() => { consoleOutput = []; }); From 6c2f60c5c90baed29452d920fea2a601a68ff08c Mon Sep 17 00:00:00 2001 From: Carlos Juega Date: Wed, 22 Jun 2022 14:08:24 +0200 Subject: [PATCH 2/4] feat(#503): assert the mTLS certificate exists in S3 --- README.md | 2 + src/aws/s3-wrapper.ts | 45 +++++++++++ src/domain-config.ts | 4 + src/index.ts | 7 ++ src/types.ts | 1 + test/unit-tests/index.test.ts | 139 ++++++++++++++++++++++++++++++++++ 6 files changed, 198 insertions(+) create mode 100644 src/aws/s3-wrapper.ts diff --git a/README.md b/README.md index 3c037b61..99ab68bf 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ route53:ChangeResourceRecordSets hostedzone/{HostedZoneId} route53:GetHostedZone * route53:ListResourceRecordSets * iam:CreateServiceLinkedRole arn:aws:iam::${AWS::AccountId}: role/aws-service-role/ops.apigateway.amazonaws.com/AWSServiceRoleForAPIGateway +s3:ListBucket * +s3:GetObject * ``` ### CloudFormation Alternatively you can generate an least privileged IAM Managed Policy for deployment with this: diff --git a/src/aws/s3-wrapper.ts b/src/aws/s3-wrapper.ts new file mode 100644 index 00000000..745d1314 --- /dev/null +++ b/src/aws/s3-wrapper.ts @@ -0,0 +1,45 @@ +import {S3} from "aws-sdk"; +import {throttledCall} from "../utils"; +import DomainConfig = require("../domain-config"); +import Globals from "../globals"; + +class S3Wrapper { + public s3: S3; + + constructor(credentials: any) { + this.s3 = new S3(credentials); + } + + /** + * * Checks whether the Mutual TLS certificate exists in S3 or not + */ + public async assertTlsCertObjectExists(domain: DomainConfig): Promise { + try { + const {Bucket, Key} = this.extractBucketAndKey(domain.tlsTruststoreUri); + const params: S3.Types.HeadObjectRequest = {Bucket, Key}; + + if (domain.tlsTruststoreVersion) { + params.VersionId = domain.tlsTruststoreVersion; + } + + await throttledCall(this.s3, "headObject", params); + } catch (err) { + if (err.code !== "AccessDenied") { + throw Error(`Could not head S3 object at ${domain.tlsTruststoreUri}.\n${err.message}`); + } + + Globals.logWarning(`Unable to check existance of S3 object at ${domain.tlsTruststoreUri} due to\n${err.message}`); + } + } + + /** + * * Extracts Bucket and Key from the given s3 uri + */ + private extractBucketAndKey(uri: string): { Bucket: string; Key: string } { + const { hostname, pathname } = new URL(uri); + + return { Bucket: hostname, Key: pathname.substring(1) }; + } +} + +export = S3Wrapper; diff --git a/src/domain-config.ts b/src/domain-config.ts index bcd08f0f..bf29442e 100644 --- a/src/domain-config.ts +++ b/src/domain-config.ts @@ -87,6 +87,10 @@ class DomainConfig { } this.tlsTruststoreUri = config.tlsTruststoreUri; this.tlsTruststoreVersion = config.tlsTruststoreVersion; + const isS3UriRegExp = /^s3:\/\/[\w-_.]+(\/[\w-_.]+)+$/; + if (this.tlsTruststoreUri && !isS3UriRegExp.test(this.tlsTruststoreUri)) { + throw new Error(`${this.tlsTruststoreUri} is not a valid s3 uri, try something like s3://bucket-name/key-name.`); + } const securityPolicyDefault = config.securityPolicy || Globals.tlsVersions.tls_1_2; const tlsVersionToUse = Globals.tlsVersions[securityPolicyDefault.toLowerCase()]; diff --git a/src/index.ts b/src/index.ts index 56933a23..30fd13cb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import ACMWrapper = require("./aws/acm-wrapper"); import APIGatewayWrapper = require("./aws/api-gateway-wrapper"); import CloudFormationWrapper = require("./aws/cloud-formation-wrapper"); import Route53Wrapper = require("./aws/route53-wrapper"); +import S3Wrapper = require("./aws/s3-wrapper"); import DomainConfig = require("./domain-config"); import Globals from "./globals"; import {CustomDomain, ServerlessInstance, ServerlessOptions, ServerlessUtils} from "./types"; @@ -14,6 +15,7 @@ class ServerlessCustomDomain { // AWS SDK resources public apiGatewayWrapper: APIGatewayWrapper; public cloudFormationWrapper: CloudFormationWrapper; + public s3Wrapper: S3Wrapper; // Serverless specific properties public serverless: ServerlessInstance; @@ -168,6 +170,7 @@ class ServerlessCustomDomain { this.apiGatewayWrapper = new APIGatewayWrapper(credentials); this.cloudFormationWrapper = new CloudFormationWrapper(credentials); + this.s3Wrapper = new S3Wrapper(credentials); } /** @@ -190,6 +193,10 @@ class ServerlessCustomDomain { const route53 = new Route53Wrapper(domain.route53Profile, domain.route53Region); const acm = new ACMWrapper(domain.endpointType); try { + if (domain.tlsTruststoreUri) { + await this.s3Wrapper.assertTlsCertObjectExists(domain); + } + if (!domain.domainInfo) { if (!domain.certificateArn) { const searchName = domain.certificateName || domain.givenDomainName; diff --git a/src/types.ts b/src/types.ts index 2dfcad65..a1f6e35a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,6 +54,7 @@ export interface ServerlessInstance { // tslint:disable-line Route53: any, CloudFormation: any, ACM: any, + S3: any, config: { httpOptions: HTTPOptions, update(toUpdate: object): void, diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts index 2d1cb96b..d194e972 100644 --- a/test/unit-tests/index.test.ts +++ b/test/unit-tests/index.test.ts @@ -10,6 +10,7 @@ import ServerlessCustomDomain = require("../../src/index"); import {getAWSPagedResults} from "../../src/utils"; import Route53Wrapper = require("../../src/aws/route53-wrapper"); import ACMWrapper = require("../../src/aws/acm-wrapper"); +import S3Wrapper = require("../../src/aws/s3-wrapper"); const expect = chai.expect; chai.use(spies); @@ -90,6 +91,7 @@ const constructPlugin = (customDomainOptions, multiple: boolean = false) => { CloudFormation: aws.CloudFormation, Route53: aws.Route53, SharedIniFileCredentials: aws.SharedIniFileCredentials, + S3: aws.S3, config: { httpOptions: { timeout: 5000, @@ -570,6 +572,107 @@ describe("Custom Domain Plugin", () => { }); }); + describe("Check Mutual TLS certificate existance in S3", () => { + it("Should check existance of certificate in S3", async () => { + AWS.mock("S3", "headObject", (params, callback) => { + callback(null, params); + }); + const options = { + domainName: "test_domain", + endpointType: "regional", + tlsTruststoreUri: 's3://test_bucket/test_key' + }; + const plugin = constructPlugin(options); + plugin.initializeVariables(); + + const s3Wrapper = new S3Wrapper({}); + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + const spy = chai.spy.on(s3Wrapper.s3, "headObject"); + await s3Wrapper.assertTlsCertObjectExists(dc); + const expectedParams = { + Bucket: 'test_bucket', + Key: 'test_key' + } + expect(spy).to.have.been.called.with(expectedParams); + }); + + it("Should check existance of a concrete certificate version in S3", async () => { + AWS.mock("S3", "headObject", (params, callback) => { + callback(null, params); + }); + const options = { + domainName: "test_domain", + endpointType: "regional", + tlsTruststoreUri: 's3://test_bucket/test_key', + tlsTruststoreVersion: 'test_version' + }; + const plugin = constructPlugin(options); + plugin.initializeVariables(); + + const s3Wrapper = new S3Wrapper({}); + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + const spy = chai.spy.on(s3Wrapper.s3, "headObject"); + await s3Wrapper.assertTlsCertObjectExists(dc); + const expectedParams = { + Bucket: 'test_bucket', + Key: 'test_key', + VersionId: 'test_version' + } + expect(spy).to.have.been.called.with(expectedParams); + }); + + it('should fail when the mutual TLS certificate is not stored in S3', async () => { + AWS.mock("S3", "headObject", (params, callback) => { + // @ts-ignore + callback({code: 'NotFound'}, null); + }); + const options = { + domainName: "test_domain", + endpointType: "regional", + tlsTruststoreUri: 's3://test_bucket/test_key' + }; + const plugin = constructPlugin(options); + plugin.initializeVariables(); + + const s3Wrapper = new S3Wrapper({}); + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + try { + await s3Wrapper.assertTlsCertObjectExists(dc); + } catch(e) { + expect(e.message).to.contain('Could not head S3 object'); + } + }); + + it("Should not fail due to lack of S3 permissions", async () => { + AWS.mock("S3", "headObject", (params, callback) => { + // @ts-ignore + callback({code: 'AccessDenied'}, null); + }); + const options = { + domainName: "test_domain", + endpointType: "regional", + tlsTruststoreUri: 's3://test_bucket/test_key' + }; + const plugin = constructPlugin(options); + plugin.initializeVariables(); + + const s3Wrapper = new S3Wrapper({}); + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + let err; + try { + await s3Wrapper.assertTlsCertObjectExists(dc); + } catch(e) { + err = e; + } finally { + expect(err).to.be.undefined; + } + }); + }); + describe("Create a New Domain Name", () => { it("Get a given certificate by given domain name ", async () => { AWS.mock("ACM", "listCertificates", certTestData); @@ -1699,6 +1802,29 @@ describe("Custom Domain Plugin", () => { expect(consoleOutput[0]).to.contain("test message"); }); + it('should fail when the mutual TLS certificate is not stored in S3', async () => { + AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => { + callback(null, {DomainName: "test_domain", DomainNameConfigurations: [{HostedZoneId: "test_id"}]}); + }); + AWS.mock("S3", "headObject", (params, callback) => { + // @ts-ignore + callback({code: 'NotFound'}, null); + }); + const plugin = constructPlugin({ + domainName: "test_domain", + endpointType: "regional", + tlsTruststoreUri: 's3://test_bucket/test_key' + }); + plugin.initializeVariables(); + plugin.initAWSResources(); + + try { + await plugin.createDomains(); + } catch(e) { + expect(e.message).to.contain('Could not head S3 object'); + } + }); + afterEach(() => { AWS.restore(); consoleOutput = []; @@ -1869,6 +1995,19 @@ describe("Custom Domain Plugin", () => { expect(errored).to.equal(true); }); + it("Should throw an Error when mutual TLS uri is not an S3 uri", async () => { + const plugin = constructPlugin({endpointType: "regional", tlsTruststoreUri: "http://example.com"}); + + let errored = false; + try { + await plugin.hookWrapper(null); + } catch (err) { + errored = true; + expect(err.message).to.equal(`http://example.com is not a valid s3 uri, try something like s3://bucket-name/key-name.`); + } + expect(errored).to.equal(true); + }); + afterEach(() => { consoleOutput = []; }); From cddd823b6f8c99738d3ac00bf7dc9e5d402f568b Mon Sep 17 00:00:00 2001 From: Carlos Juega Date: Fri, 24 Jun 2022 08:42:11 +0200 Subject: [PATCH 3/4] refactor(#503): codacy issues fixed --- src/aws/api-gateway-wrapper.ts | 14 ++++++++++---- test/unit-tests/index.test.ts | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/aws/api-gateway-wrapper.ts b/src/aws/api-gateway-wrapper.ts index c3e95dd4..7766114f 100644 --- a/src/aws/api-gateway-wrapper.ts +++ b/src/aws/api-gateway-wrapper.ts @@ -44,9 +44,12 @@ class APIGatewayWrapper { if (!isEdgeType && hasMutualTls) { params.mutualTlsAuthentication = { - truststoreUri: domain.tlsTruststoreUri, - ...(domain.tlsTruststoreVersion ? {truststoreVersion: domain.tlsTruststoreVersion} : undefined) + truststoreUri: domain.tlsTruststoreUri }; + + if (domain.tlsTruststoreVersion) { + params.truststoreVersion = domain.tlsTruststoreVersion; + } } // Make API call to create domain @@ -71,9 +74,12 @@ class APIGatewayWrapper { if (!isEdgeType && hasMutualTls) { params.MutualTlsAuthentication = { - TruststoreUri: domain.tlsTruststoreUri, - ...(domain.tlsTruststoreVersion ? {TruststoreVersion: domain.tlsTruststoreVersion} : undefined) + TruststoreUri: domain.tlsTruststoreUri }; + + if (domain.tlsTruststoreVersion) { + params.TruststoreVersion = domain.tlsTruststoreVersion; + } } // Make API call to create domain diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts index d194e972..4bc7b69e 100644 --- a/test/unit-tests/index.test.ts +++ b/test/unit-tests/index.test.ts @@ -668,7 +668,7 @@ describe("Custom Domain Plugin", () => { } catch(e) { err = e; } finally { - expect(err).to.be.undefined; + expect(err).to.equal(undefined); } }); }); From c95cfbb3deeaf16b629d323697426d4425770325 Mon Sep 17 00:00:00 2001 From: Carlos Juega Date: Fri, 24 Jun 2022 09:10:55 +0200 Subject: [PATCH 4/4] refactor(#503): regex replaced in s3 uri validation to fix codacy issue --- src/domain-config.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/domain-config.ts b/src/domain-config.ts index bf29442e..d5da9a89 100644 --- a/src/domain-config.ts +++ b/src/domain-config.ts @@ -85,12 +85,11 @@ class DomainConfig { if (isEdgeType && hasMutualTls) { throw new Error(`${this.endpointType} APIs do not support mutual TLS, remove tlsTruststoreUri or change to a regional API.`); } + if (config.tlsTruststoreUri) { + this.validateS3Uri(config.tlsTruststoreUri); + } this.tlsTruststoreUri = config.tlsTruststoreUri; this.tlsTruststoreVersion = config.tlsTruststoreVersion; - const isS3UriRegExp = /^s3:\/\/[\w-_.]+(\/[\w-_.]+)+$/; - if (this.tlsTruststoreUri && !isS3UriRegExp.test(this.tlsTruststoreUri)) { - throw new Error(`${this.tlsTruststoreUri} is not a valid s3 uri, try something like s3://bucket-name/key-name.`); - } const securityPolicyDefault = config.securityPolicy || Globals.tlsVersions.tls_1_2; const tlsVersionToUse = Globals.tlsVersions[securityPolicyDefault.toLowerCase()]; @@ -120,6 +119,14 @@ class DomainConfig { healthCheckId: config.route53Params?.healthCheckId } } + + private validateS3Uri(uri: string): void { + const { protocol, pathname } = new URL(uri); + + if (protocol !== "s3:" && !pathname.substring(1).includes("/")) { + throw new Error(`${uri} is not a valid s3 uri, try something like s3://bucket-name/key-name.`); + } + } } export = DomainConfig;