Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .eslintrc.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .gitattributes

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .projen/files.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 0 additions & 21 deletions .projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 63 additions & 20 deletions src/static-website/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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[];
}
Expand Down Expand Up @@ -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,
}),
Expand All @@ -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)),
Expand Down Expand Up @@ -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);
}
}
}
Expand All @@ -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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The regex /\.[a-zA-Z0-9]+$/ to detect a file extension is a bit restrictive. It won't match extensions containing characters other than letters and numbers. A more robust approach would be to allow any character except a slash in the extension part.

  const hasExtension = /\.[^/]+$/.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}',
},
},
};`;
}
26 changes: 0 additions & 26 deletions src/static-website/origin-request-function.ts

This file was deleted.

9 changes: 0 additions & 9 deletions src/static-website/origin-request.edge-lambda.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/url-shortener/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading