diff --git a/.eslintrc.json b/.eslintrc.json index 39c80868..387a939f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -160,7 +160,6 @@ "src/ssl-server-test/analyze.lambda.ts", "src/toolkit-cleaner/clean.lambda.ts", "src/url-shortener/shortener.lambda.ts", - "src/static-website/origin-request.edge-lambda.ts", "src/url-shortener/redirect.edge-lambda.ts", ".projenrc.ts", "projenrc/**/*.ts" diff --git a/.gitattributes b/.gitattributes index ef902dda..06749854 100644 --- a/.gitattributes +++ b/.gitattributes @@ -24,7 +24,6 @@ /src/slack-events/events-function.ts linguist-generated /src/slack-textract/detect-function.ts linguist-generated /src/ssl-server-test/analyze-function.ts linguist-generated -/src/static-website/origin-request-function.ts linguist-generated /src/toolkit-cleaner/clean-function.ts linguist-generated /src/url-shortener/redirect-function.ts linguist-generated /src/url-shortener/shortener-function.ts linguist-generated diff --git a/.gitignore b/.gitignore index ac8f17a9..51d6b9e8 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,6 @@ tsconfig.json !/src/ssl-server-test/analyze-function.ts !/src/toolkit-cleaner/clean-function.ts !/src/url-shortener/shortener-function.ts -!/src/static-website/origin-request-function.ts !/src/url-shortener/redirect-function.ts test/mjml-template/.tmp test/mjml-template/mjml-template.integ.snapshot/asset.* diff --git a/.projen/files.json b/.projen/files.json index 95d8d3ea..29f4e388 100644 --- a/.projen/files.json +++ b/.projen/files.json @@ -18,7 +18,6 @@ "src/slack-events/events-function.ts", "src/slack-textract/detect-function.ts", "src/ssl-server-test/analyze-function.ts", - "src/static-website/origin-request-function.ts", "src/toolkit-cleaner/clean-function.ts", "src/url-shortener/redirect-function.ts", "src/url-shortener/shortener-function.ts", diff --git a/.projen/tasks.json b/.projen/tasks.json index f4f0b638..6a8b3577 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -67,9 +67,6 @@ { "spawn": "bundle:url-shortener/shortener.lambda" }, - { - "spawn": "bundle:static-website/origin-request.edge-lambda" - }, { "spawn": "bundle:url-shortener/redirect.edge-lambda" } @@ -165,24 +162,6 @@ } ] }, - "bundle:static-website/origin-request.edge-lambda": { - "name": "bundle:static-website/origin-request.edge-lambda", - "description": "Create a JavaScript bundle from src/static-website/origin-request.edge-lambda.ts", - "steps": [ - { - "exec": "esbuild --bundle src/static-website/origin-request.edge-lambda.ts --target=\"node24\" --platform=\"node\" --outfile=\"assets/static-website/origin-request.edge-lambda/index.js\" --tsconfig=\"tsconfig.dev.json\" --external:@aws-sdk/*" - } - ] - }, - "bundle:static-website/origin-request.edge-lambda:watch": { - "name": "bundle:static-website/origin-request.edge-lambda:watch", - "description": "Continuously update the JavaScript bundle from src/static-website/origin-request.edge-lambda.ts", - "steps": [ - { - "exec": "esbuild --bundle src/static-website/origin-request.edge-lambda.ts --target=\"node24\" --platform=\"node\" --outfile=\"assets/static-website/origin-request.edge-lambda/index.js\" --tsconfig=\"tsconfig.dev.json\" --external:@aws-sdk/* --watch" - } - ] - }, "bundle:toolkit-cleaner/clean.lambda": { "name": "bundle:toolkit-cleaner/clean.lambda", "description": "Create a JavaScript bundle from src/toolkit-cleaner/clean.lambda.ts", diff --git a/API.md b/API.md index 89c8807e..3a4b1a6b 100644 --- a/API.md +++ b/API.md @@ -3787,7 +3787,7 @@ public readonly edgeLambdas: EdgeLambda[]; ``` - *Type:* aws-cdk-lib.aws_cloudfront.EdgeLambda[] -- *Default:* an origin request function that redirects all requests for a path to /index.html +- *Default:* no edge Lambdas The Lambda@Edge functions to invoke before serving the contents. diff --git a/package.json b/package.json index 39f3bf3c..9e9f6cf3 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,6 @@ "bundle:slack-textract/detect.lambda:watch": "npx projen bundle:slack-textract/detect.lambda:watch", "bundle:ssl-server-test/analyze.lambda": "npx projen bundle:ssl-server-test/analyze.lambda", "bundle:ssl-server-test/analyze.lambda:watch": "npx projen bundle:ssl-server-test/analyze.lambda:watch", - "bundle:static-website/origin-request.edge-lambda": "npx projen bundle:static-website/origin-request.edge-lambda", - "bundle:static-website/origin-request.edge-lambda:watch": "npx projen bundle:static-website/origin-request.edge-lambda:watch", "bundle:toolkit-cleaner/clean.lambda": "npx projen bundle:toolkit-cleaner/clean.lambda", "bundle:toolkit-cleaner/clean.lambda:watch": "npx projen bundle:toolkit-cleaner/clean.lambda:watch", "bundle:url-shortener/redirect.edge-lambda": "npx projen bundle:url-shortener/redirect.edge-lambda", diff --git a/src/static-website/index.ts b/src/static-website/index.ts index 883d1667..d24d9b6d 100644 --- a/src/static-website/index.ts +++ b/src/static-website/index.ts @@ -4,12 +4,10 @@ import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as route53 from 'aws-cdk-lib/aws-route53'; -import * as patterns from 'aws-cdk-lib/aws-route53-patterns'; import * as targets from 'aws-cdk-lib/aws-route53-targets'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as cr from 'aws-cdk-lib/custom-resources'; import { Construct } from 'constructs'; -import { OriginRequestFunction } from './origin-request-function'; /** * Properties for a StaticWebsite @@ -67,7 +65,7 @@ export interface StaticWebsiteProps { /** * The Lambda@Edge functions to invoke before serving the contents. * - * @default - an origin request function that redirects all requests for a path to /index.html + * @default - no edge Lambdas */ readonly edgeLambdas?: cloudfront.EdgeLambda[]; } @@ -125,12 +123,14 @@ export class StaticWebsite extends Construct { defaultBehavior: { origin: origins.S3BucketOrigin.withOriginAccessControl(this.bucket), viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - edgeLambdas: props.edgeLambdas ?? [ - { - eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, - functionVersion: new OriginRequestFunction(this, 'OriginRequest'), - }, - ], + edgeLambdas: props.edgeLambdas, + functionAssociations: [{ + eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, + function: new cloudfront.Function(this, 'RewriteFunction', { + code: cloudfront.FunctionCode.fromInline(rewriteFunctionCode()), + runtime: cloudfront.FunctionRuntime.JS_2_0, + }), + }], responseHeadersPolicy: props.responseHeadersPolicy ?? new cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', { securityHeadersBehavior: StaticWebsite.defaultSecurityHeadersBehavior, }), @@ -156,7 +156,7 @@ export class StaticWebsite extends Construct { }); new route53.RecordSet(this, 'HttpsRecord', { - recordType: 'HTTPS' as route53.RecordType, + recordType: route53.RecordType.HTTPS, recordName: props.domainName, zone: props.hostedZone, target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.distribution)), @@ -187,17 +187,35 @@ export class StaticWebsite extends Construct { } if (shouldAddRedirect(props)) { - const httpsRedirect = new patterns.HttpsRedirect(this, 'HttpsRedirect', { - targetDomain: props.domainName, - zone: props.hostedZone, - recordNames: props.redirects, + const redirects = props.redirects ?? [props.hostedZone.zoneName]; + const redirectDistribution = new cloudfront.Distribution(this, 'RedirectDistribution', { + defaultBehavior: { + origin: new origins.HttpOrigin(props.domainName), + viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + functionAssociations: [{ + eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, + function: new cloudfront.Function(this, 'RedirectFunction', { + code: cloudfront.FunctionCode.fromInline(redirectFunctionCode(props.domainName)), + runtime: cloudfront.FunctionRuntime.JS_2_0, + }), + }], + }, + defaultRootObject: '', + domainNames: redirects, + certificate: props.certificate, + httpVersion: cloudfront.HttpVersion.HTTP2_AND_3, + comment: `Redirect to ${props.domainName} from ${redirects.join(', ')}`, }); - // Force minimum protocol version - const redirectDistribution = httpsRedirect.node.tryFindChild('RedirectDistribution') as cloudfront.CloudFrontWebDistribution; - const cfnDistribution = redirectDistribution.node.tryFindChild('CFDistribution') as cloudfront.CfnDistribution; - if (cfnDistribution) { - cfnDistribution.addPropertyOverride('DistributionConfig.ViewerCertificate.MinimumProtocolVersion', 'TLSv1.2_2021'); - cfnDistribution.addPropertyOverride('DistributionConfig.HttpVersion', 'http2and3'); + for (const redirect of redirects) { + const safeRedirectId = redirect.replace(/\./g, '-'); + const aliasProps = { + recordName: redirect, + zone: props.hostedZone, + target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(redirectDistribution)), + }; + new route53.ARecord(this, `RedirectARecord${safeRedirectId}`, aliasProps); + new route53.AaaaRecord(this, `RedirectAaaaRecord${safeRedirectId}`, aliasProps); + new route53.HttpsRecord(this, `RedirectHttpsRecord${safeRedirectId}`, aliasProps); } } } @@ -216,3 +234,28 @@ function shouldAddRedirect(props: StaticWebsiteProps): boolean { return true; } + +function rewriteFunctionCode(): string { + return `function handler(event) { + const request = event.request; + const uri = request.uri; + const hasExtension = /\.[a-zA-Z0-9]+$/.test(uri); + if (!hasExtension) { + request.uri = '/index.html'; + } + return request; + }`; +} + +function redirectFunctionCode(domainName: string): string { + return `function handler(event) { + return { + statusCode: 301, + statusDescription: 'Moved permanently', + headers: { + location: { + value: 'https://${domainName}', + }, + }, + };`; +} diff --git a/src/static-website/origin-request-function.ts b/src/static-website/origin-request-function.ts deleted file mode 100644 index 13412846..00000000 --- a/src/static-website/origin-request-function.ts +++ /dev/null @@ -1,26 +0,0 @@ -// ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". -import * as path from 'path'; -import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; -import * as lambda from 'aws-cdk-lib/aws-lambda'; -import { Construct } from 'constructs'; - -/** - * Props for OriginRequestFunction - */ -export interface OriginRequestFunctionProps extends cloudfront.experimental.EdgeFunctionProps { -} - -/** - * An AWS Lambda function which executes src/static-website/origin-request. - */ -export class OriginRequestFunction extends cloudfront.experimental.EdgeFunction { - constructor(scope: Construct, id: string, props?: OriginRequestFunctionProps) { - super(scope, id, { - description: 'src/static-website/origin-request.edge-lambda.ts', - ...props, - runtime: new lambda.Runtime('nodejs24.x', lambda.RuntimeFamily.NODEJS), - handler: 'index.handler', - code: lambda.Code.fromAsset(path.join(__dirname, '../../assets/static-website/origin-request.edge-lambda')), - }); - } -} \ No newline at end of file diff --git a/src/static-website/origin-request.edge-lambda.ts b/src/static-website/origin-request.edge-lambda.ts deleted file mode 100644 index d8d7805e..00000000 --- a/src/static-website/origin-request.edge-lambda.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as path from 'path'; - -export async function handler(event: AWSLambda.CloudFrontRequestEvent): Promise { - const request = event.Records[0].cf.request; - - if (!path.extname(request.uri)) request.uri = '/index.html'; - - return request; -} diff --git a/src/url-shortener/index.ts b/src/url-shortener/index.ts index b2d8b875..e5bdb0de 100644 --- a/src/url-shortener/index.ts +++ b/src/url-shortener/index.ts @@ -138,7 +138,7 @@ export class UrlShortener extends Construct { }, certificate: props.certificate, domainNames: [domainName], - httpVersion: 'http2and3' as cloudfront.HttpVersion, + httpVersion: cloudfront.HttpVersion.HTTP2_AND_3, }); // Route53 records diff --git a/test/static-website/__snapshots__/static-website.test.ts.snap b/test/static-website/__snapshots__/static-website.test.ts.snap index 3f755b08..8a103bc4 100644 --- a/test/static-website/__snapshots__/static-website.test.ts.snap +++ b/test/static-website/__snapshots__/static-website.test.ts.snap @@ -1,129 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`StaticWebsite 1`] = ` -{ - "Parameters": { - "BootstrapVersion": { - "Default": "/cdk-bootstrap/hnb659fds/version", - "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", - "Type": "AWS::SSM::Parameter::Value", - }, - }, - "Resources": { - "OriginRequest0052232D": { - "DependsOn": [ - "OriginRequestServiceRole04760C2E", - ], - "Properties": { - "Code": { - "S3Bucket": { - "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-us-east-1", - }, - "S3Key": "26f41d5aecce2a35015d531a35df7ba8056a4dd03df5aedc8b706fb6d5d7a0d8.zip", - }, - "Description": "src/static-website/origin-request.edge-lambda.ts", - "Handler": "index.handler", - "Role": { - "Fn::GetAtt": [ - "OriginRequestServiceRole04760C2E", - "Arn", - ], - }, - "Runtime": "nodejs24.x", - }, - "Type": "AWS::Lambda::Function", - }, - "OriginRequestCurrentVersionD6F77E9F1aa1d0332e3d0c3ddd4e96ca4a785f29": { - "Metadata": { - "aws:cdk:do-not-refactor": true, - }, - "Properties": { - "FunctionName": { - "Ref": "OriginRequest0052232D", - }, - }, - "Type": "AWS::Lambda::Version", - }, - "OriginRequestParameter759D9B4C": { - "Properties": { - "Name": "/cdk/EdgeFunctionArn/eu-west-1/Stack/StaticWebsite/OriginRequest", - "Type": "String", - "Value": { - "Ref": "OriginRequestCurrentVersionD6F77E9F1aa1d0332e3d0c3ddd4e96ca4a785f29", - }, - }, - "Type": "AWS::SSM::Parameter", - }, - "OriginRequestServiceRole04760C2E": { - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com", - }, - }, - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "edgelambda.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition", - }, - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", - ], - ], - }, - ], - }, - "Type": "AWS::IAM::Role", - }, - }, - "Rules": { - "CheckBootstrapVersion": { - "Assertions": [ - { - "Assert": { - "Fn::Not": [ - { - "Fn::Contains": [ - [ - "1", - "2", - "3", - "4", - "5", - ], - { - "Ref": "BootstrapVersion", - }, - ], - }, - ], - }, - "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", - }, - ], - }, - }, -} -`; - -exports[`StaticWebsite 2`] = ` { "Mappings": { "AWSCloudFrontPartitionHostedZoneIdMap": { @@ -284,84 +161,6 @@ exports[`StaticWebsite 2`] = ` }, "Type": "AWS::ApiGatewayV2::Api", }, - "CustomCrossRegionStringParameterReaderCustomResourceProviderHandler65B5F33A": { - "DependsOn": [ - "CustomCrossRegionStringParameterReaderCustomResourceProviderRole71CD6825", - ], - "Properties": { - "Code": { - "S3Bucket": { - "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-eu-west-1", - }, - "S3Key": "934f26e3be418ca933eeffc0c28bc2546c67037b7ea94c28b93ad93e1952a30e.zip", - }, - "Handler": "__entrypoint__.handler", - "MemorySize": 128, - "Role": { - "Fn::GetAtt": [ - "CustomCrossRegionStringParameterReaderCustomResourceProviderRole71CD6825", - "Arn", - ], - }, - "Runtime": "nodejs22.x", - "Timeout": 900, - }, - "Type": "AWS::Lambda::Function", - }, - "CustomCrossRegionStringParameterReaderCustomResourceProviderRole71CD6825": { - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "ManagedPolicyArns": [ - { - "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", - }, - ], - "Policies": [ - { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "ssm:GetParameter", - ], - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition", - }, - ":ssm:us-east-1:", - { - "Ref": "AWS::AccountId", - }, - ":parameter/cdk/EdgeFunctionArn/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "Inline", - }, - ], - }, - "Type": "AWS::IAM::Role", - }, "Fn9270CBC0": { "DependsOn": [ "FnServiceRoleB9001A96", @@ -551,13 +350,13 @@ exports[`StaticWebsite 2`] = ` "DefaultCacheBehavior": { "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", "Compress": true, - "LambdaFunctionAssociations": [ + "FunctionAssociations": [ { - "EventType": "origin-request", - "LambdaFunctionARN": { + "EventType": "viewer-request", + "FunctionARN": { "Fn::GetAtt": [ - "StaticWebsiteOriginRequestArnReaderEA9004DC", - "FunctionArn", + "StaticWebsiteRewriteFunction32E38FCF", + "FunctionARN", ], }, }, @@ -639,12 +438,103 @@ exports[`StaticWebsite 2`] = ` }, "Type": "AWS::Route53::RecordSet", }, - "StaticWebsiteHttpsRedirectRedirectAliasRecord2591f3E6FEEF4C": { + "StaticWebsitePutConfig8F8DD69A": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "StaticWebsitePutConfigCustomResourcePolicy54D08151", + ], + "Properties": { + "Create": { + "Fn::Join": [ + "", + [ + "{"service":"S3","action":"putObject","parameters":{"Bucket":"", + { + "Ref": "StaticWebsiteBucket0E92E0FC", + }, + "","Key":"config.json","Body":"{\\"key1\\":\\"value1\\",\\"key2\\":\\"value2\\",\\"apiUrl\\":\\"https://", + { + "Ref": "ApiF70053CD", + }, + ".execute-api.eu-west-1.", + { + "Ref": "AWS::URLSuffix", + }, + "/\\"}","ContentType":"application/json","CacheControl":"max-age=0, no-cache, no-store, must-revalidate"},"physicalResourceId":{"id":"config"}}", + ], + ], + }, + "InstallLatestAwsSdk": true, + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn", + ], + }, + "Update": { + "Fn::Join": [ + "", + [ + "{"service":"S3","action":"putObject","parameters":{"Bucket":"", + { + "Ref": "StaticWebsiteBucket0E92E0FC", + }, + "","Key":"config.json","Body":"{\\"key1\\":\\"value1\\",\\"key2\\":\\"value2\\",\\"apiUrl\\":\\"https://", + { + "Ref": "ApiF70053CD", + }, + ".execute-api.eu-west-1.", + { + "Ref": "AWS::URLSuffix", + }, + "/\\"}","ContentType":"application/json","CacheControl":"max-age=0, no-cache, no-store, must-revalidate"},"physicalResourceId":{"id":"config"}}", + ], + ], + }, + }, + "Type": "Custom::AWS", + "UpdateReplacePolicy": "Delete", + }, + "StaticWebsitePutConfigCustomResourcePolicy54D08151": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "StaticWebsiteBucket0E92E0FC", + "Arn", + ], + }, + "/config.json", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "StaticWebsitePutConfigCustomResourcePolicy54D08151", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "StaticWebsiteRedirectARecordmysitecom45D4EFA8": { "Properties": { "AliasTarget": { "DNSName": { "Fn::GetAtt": [ - "StaticWebsiteHttpsRedirectRedirectDistributionCFDistributionF7ADE06F", + "StaticWebsiteRedirectDistributionA10E29FF", "DomainName", ], }, @@ -666,12 +556,12 @@ exports[`StaticWebsite 2`] = ` }, "Type": "AWS::Route53::RecordSet", }, - "StaticWebsiteHttpsRedirectRedirectAliasRecordSix2591f309F9321D": { + "StaticWebsiteRedirectAaaaRecordmysitecom9E9936CA": { "Properties": { "AliasTarget": { "DNSName": { "Fn::GetAtt": [ - "StaticWebsiteHttpsRedirectRedirectDistributionCFDistributionF7ADE06F", + "StaticWebsiteRedirectDistributionA10E29FF", "DomainName", ], }, @@ -693,168 +583,7 @@ exports[`StaticWebsite 2`] = ` }, "Type": "AWS::Route53::RecordSet", }, - "StaticWebsiteHttpsRedirectRedirectBucket45BA11C9": { - "DeletionPolicy": "Delete", - "Properties": { - "PublicAccessBlockConfiguration": { - "BlockPublicAcls": true, - "BlockPublicPolicy": true, - "IgnorePublicAcls": true, - "RestrictPublicBuckets": true, - }, - "WebsiteConfiguration": { - "RedirectAllRequestsTo": { - "HostName": "www.my-site.com", - "Protocol": "https", - }, - }, - }, - "Type": "AWS::S3::Bucket", - "UpdateReplacePolicy": "Delete", - }, - "StaticWebsiteHttpsRedirectRedirectCertificateCertificateRequestorFunctionEE7DB513": { - "DependsOn": [ - "StaticWebsiteHttpsRedirectRedirectCertificateCertificateRequestorFunctionServiceRoleDefaultPolicy81FC4515", - "StaticWebsiteHttpsRedirectRedirectCertificateCertificateRequestorFunctionServiceRole08DAF404", - ], - "Properties": { - "Code": { - "S3Bucket": { - "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-eu-west-1", - }, - "S3Key": "b073cebcf4d61fb152a30f5a5e57a94df7f980a549fdf1a79a0b18c5750522d8.zip", - }, - "Handler": "index.certificateRequestHandler", - "Role": { - "Fn::GetAtt": [ - "StaticWebsiteHttpsRedirectRedirectCertificateCertificateRequestorFunctionServiceRole08DAF404", - "Arn", - ], - }, - "Runtime": "nodejs22.x", - "Timeout": 900, - }, - "Type": "AWS::Lambda::Function", - }, - "StaticWebsiteHttpsRedirectRedirectCertificateCertificateRequestorFunctionServiceRole08DAF404": { - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition", - }, - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", - ], - ], - }, - ], - }, - "Type": "AWS::IAM::Role", - }, - "StaticWebsiteHttpsRedirectRedirectCertificateCertificateRequestorFunctionServiceRoleDefaultPolicy81FC4515": { - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "acm:RequestCertificate", - "acm:DescribeCertificate", - "acm:DeleteCertificate", - "acm:AddTagsToCertificate", - ], - "Effect": "Allow", - "Resource": "*", - }, - { - "Action": "route53:GetChange", - "Effect": "Allow", - "Resource": "*", - }, - { - "Action": "route53:changeResourceRecordSets", - "Condition": { - "ForAllValues:StringEquals": { - "route53:ChangeResourceRecordSetsActions": [ - "UPSERT", - ], - "route53:ChangeResourceRecordSetsRecordTypes": [ - "CNAME", - ], - }, - "ForAllValues:StringLike": { - "route53:ChangeResourceRecordSetsNormalizedRecordNames": [ - "*.my-site.com", - "*.my-site.com", - ], - }, - }, - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition", - }, - ":route53:::hostedzone/", - { - "Ref": "HostedZoneDB99F866", - }, - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "StaticWebsiteHttpsRedirectRedirectCertificateCertificateRequestorFunctionServiceRoleDefaultPolicy81FC4515", - "Roles": [ - { - "Ref": "StaticWebsiteHttpsRedirectRedirectCertificateCertificateRequestorFunctionServiceRole08DAF404", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "StaticWebsiteHttpsRedirectRedirectCertificateCertificateRequestorResource692E38D9": { - "DeletionPolicy": "Delete", - "Properties": { - "DomainName": "my-site.com", - "HostedZoneId": { - "Ref": "HostedZoneDB99F866", - }, - "Region": "us-east-1", - "ServiceToken": { - "Fn::GetAtt": [ - "StaticWebsiteHttpsRedirectRedirectCertificateCertificateRequestorFunctionEE7DB513", - "Arn", - ], - }, - "SubjectAlternativeNames": [ - "my-site.com", - ], - }, - "Type": "AWS::CloudFormation::CustomResource", - "UpdateReplacePolicy": "Delete", - }, - "StaticWebsiteHttpsRedirectRedirectDistributionCFDistributionF7ADE06F": { + "StaticWebsiteRedirectDistributionA10E29FF": { "Properties": { "DistributionConfig": { "Aliases": [ @@ -862,22 +591,20 @@ exports[`StaticWebsite 2`] = ` ], "Comment": "Redirect to www.my-site.com from my-site.com", "DefaultCacheBehavior": { - "AllowedMethods": [ - "GET", - "HEAD", - ], - "CachedMethods": [ - "GET", - "HEAD", - ], + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", "Compress": true, - "ForwardedValues": { - "Cookies": { - "Forward": "none", + "FunctionAssociations": [ + { + "EventType": "viewer-request", + "FunctionARN": { + "Fn::GetAtt": [ + "StaticWebsiteRedirectFunctionA4B8165C", + "FunctionARN", + ], + }, }, - "QueryString": false, - }, - "TargetOriginId": "origin1", + ], + "TargetOriginId": "StackStaticWebsiteRedirectDistributionOrigin157F27077", "ViewerProtocolPolicy": "redirect-to-https", }, "DefaultRootObject": "", @@ -886,45 +613,18 @@ exports[`StaticWebsite 2`] = ` "IPV6Enabled": true, "Origins": [ { - "ConnectionAttempts": 3, - "ConnectionTimeout": 10, "CustomOriginConfig": { - "HTTPPort": 80, - "HTTPSPort": 443, - "OriginKeepaliveTimeout": 5, - "OriginProtocolPolicy": "http-only", - "OriginReadTimeout": 30, + "OriginProtocolPolicy": "https-only", "OriginSSLProtocols": [ "TLSv1.2", ], }, - "DomainName": { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Fn::GetAtt": [ - "StaticWebsiteHttpsRedirectRedirectBucket45BA11C9", - "WebsiteURL", - ], - }, - ], - }, - ], - }, - "Id": "origin1", + "DomainName": "www.my-site.com", + "Id": "StackStaticWebsiteRedirectDistributionOrigin157F27077", }, ], - "PriceClass": "PriceClass_All", "ViewerCertificate": { - "AcmCertificateArn": { - "Fn::GetAtt": [ - "StaticWebsiteHttpsRedirectRedirectCertificateCertificateRequestorResource692E38D9", - "Arn", - ], - }, + "AcmCertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/abcd-efgh-ijkl-mnop", "MinimumProtocolVersion": "TLSv1.2_2021", "SslSupportMethod": "sni-only", }, @@ -932,112 +632,53 @@ exports[`StaticWebsite 2`] = ` }, "Type": "AWS::CloudFront::Distribution", }, - "StaticWebsiteOriginRequestArnReaderEA9004DC": { - "DeletionPolicy": "Delete", + "StaticWebsiteRedirectFunctionA4B8165C": { "Properties": { - "ParameterName": "/cdk/EdgeFunctionArn/eu-west-1/Stack/StaticWebsite/OriginRequest", - "RefreshToken": "OriginRequestCurrentVersionD6F77E9F1aa1d0332e3d0c3ddd4e96ca4a785f29", - "Region": "us-east-1", - "ServiceToken": { - "Fn::GetAtt": [ - "CustomCrossRegionStringParameterReaderCustomResourceProviderHandler65B5F33A", - "Arn", - ], + "AutoPublish": true, + "FunctionCode": "function handler(event) { + return { + statusCode: 301, + statusDescription: 'Moved permanently', + headers: { + location: { + value: 'https://www.my-site.com', + }, + }, + };", + "FunctionConfig": { + "Comment": "eu-west-1StackStaticWebsiRedirectFunction172BEC3F", + "Runtime": "cloudfront-js-2.0", }, + "Name": "eu-west-1StackStaticWebsiRedirectFunction172BEC3F", }, - "Type": "Custom::CrossRegionStringParameterReader", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::CloudFront::Function", }, - "StaticWebsitePutConfig8F8DD69A": { - "DeletionPolicy": "Delete", - "DependsOn": [ - "StaticWebsitePutConfigCustomResourcePolicy54D08151", - ], + "StaticWebsiteRedirectHttpsRecordmysitecomB25F25A5": { "Properties": { - "Create": { - "Fn::Join": [ - "", - [ - "{"service":"S3","action":"putObject","parameters":{"Bucket":"", - { - "Ref": "StaticWebsiteBucket0E92E0FC", - }, - "","Key":"config.json","Body":"{\\"key1\\":\\"value1\\",\\"key2\\":\\"value2\\",\\"apiUrl\\":\\"https://", - { - "Ref": "ApiF70053CD", - }, - ".execute-api.eu-west-1.", - { - "Ref": "AWS::URLSuffix", - }, - "/\\"}","ContentType":"application/json","CacheControl":"max-age=0, no-cache, no-store, must-revalidate"},"physicalResourceId":{"id":"config"}}", + "AliasTarget": { + "DNSName": { + "Fn::GetAtt": [ + "StaticWebsiteRedirectDistributionA10E29FF", + "DomainName", ], - ], - }, - "InstallLatestAwsSdk": true, - "ServiceToken": { - "Fn::GetAtt": [ - "AWS679f53fac002430cb0da5b7982bd22872D164C4C", - "Arn", - ], - }, - "Update": { - "Fn::Join": [ - "", - [ - "{"service":"S3","action":"putObject","parameters":{"Bucket":"", - { - "Ref": "StaticWebsiteBucket0E92E0FC", - }, - "","Key":"config.json","Body":"{\\"key1\\":\\"value1\\",\\"key2\\":\\"value2\\",\\"apiUrl\\":\\"https://", - { - "Ref": "ApiF70053CD", - }, - ".execute-api.eu-west-1.", + }, + "HostedZoneId": { + "Fn::FindInMap": [ + "AWSCloudFrontPartitionHostedZoneIdMap", { - "Ref": "AWS::URLSuffix", + "Ref": "AWS::Partition", }, - "/\\"}","ContentType":"application/json","CacheControl":"max-age=0, no-cache, no-store, must-revalidate"},"physicalResourceId":{"id":"config"}}", + "zoneId", ], - ], + }, }, - }, - "Type": "Custom::AWS", - "UpdateReplacePolicy": "Delete", - }, - "StaticWebsitePutConfigCustomResourcePolicy54D08151": { - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "s3:PutObject", - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - { - "Fn::GetAtt": [ - "StaticWebsiteBucket0E92E0FC", - "Arn", - ], - }, - "/config.json", - ], - ], - }, - }, - ], - "Version": "2012-10-17", + "HostedZoneId": { + "Ref": "HostedZoneDB99F866", }, - "PolicyName": "StaticWebsitePutConfigCustomResourcePolicy54D08151", - "Roles": [ - { - "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", - }, - ], + "Name": "my-site.com.", + "Type": "HTTPS", }, - "Type": "AWS::IAM::Policy", + "Type": "AWS::Route53::RecordSet", }, "StaticWebsiteResponseHeadersPolicyF3EBE566": { "Properties": { @@ -1071,6 +712,26 @@ exports[`StaticWebsite 2`] = ` }, "Type": "AWS::CloudFront::ResponseHeadersPolicy", }, + "StaticWebsiteRewriteFunction32E38FCF": { + "Properties": { + "AutoPublish": true, + "FunctionCode": "function handler(event) { + const request = event.request; + const uri = request.uri; + const hasExtension = /.[a-zA-Z0-9]+$/.test(uri); + if (!hasExtension) { + request.uri = '/index.html'; + } + return request; + }", + "FunctionConfig": { + "Comment": "eu-west-1StackStaticWebsiteRewriteFunction4EBA86F3", + "Runtime": "cloudfront-js-2.0", + }, + "Name": "eu-west-1StackStaticWebsiteRewriteFunction4EBA86F3", + }, + "Type": "AWS::CloudFront::Function", + }, }, "Rules": { "CheckBootstrapVersion": { diff --git a/test/static-website/origin-request-handler.test.ts b/test/static-website/origin-request-handler.test.ts deleted file mode 100644 index 033addbd..00000000 --- a/test/static-website/origin-request-handler.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { handler } from '../../src/static-website/origin-request.edge-lambda'; - -test('without extension', async () => { - const request = await handler({ - Records: [ - { - cf: { - request: { - uri: '/my-uri', - }, - }, - }, - ], - } as AWSLambda.CloudFrontRequestEvent); - - expect(request.uri).toBe('/index.html'); -}); - -test('with extension', async () => { - const request = await handler({ - Records: [ - { - cf: { - request: { - uri: '/style/cool.css', - }, - }, - }, - ], - } as AWSLambda.CloudFrontRequestEvent); - - expect(request.uri).toBe('/style/cool.css'); -});