-
Notifications
You must be signed in to change notification settings - Fork 4k
/
Copy pathwebsite-redirect.ts
171 lines (160 loc) · 7.13 KB
/
website-redirect.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import { Construct } from 'constructs';
import { DnsValidatedCertificate, ICertificate, Certificate, CertificateValidation } from '../../aws-certificatemanager';
import { CloudFrontWebDistribution, OriginProtocolPolicy, PriceClass, ViewerCertificate, ViewerProtocolPolicy } from '../../aws-cloudfront';
import { ARecord, AaaaRecord, IHostedZone, RecordTarget } from '../../aws-route53';
import { CloudFrontTarget } from '../../aws-route53-targets';
import { BlockPublicAccess, Bucket, RedirectProtocol } from '../../aws-s3';
import { ArnFormat, RemovalPolicy, Stack, Token, FeatureFlags } from '../../core';
import { md5hash } from '../../core/lib/helpers-internal';
import { ROUTE53_PATTERNS_USE_CERTIFICATE } from '../../cx-api';
/**
* Properties to configure an HTTPS Redirect
*/
export interface HttpsRedirectProps {
/**
* Hosted zone of the domain which will be used to create alias record(s) from
* domain names in the hosted zone to the target domain. The hosted zone must
* contain entries for the domain name(s) supplied through `recordNames` that
* will redirect to the target domain.
*
* Domain names in the hosted zone can include a specific domain (example.com)
* and its subdomains (acme.example.com, zenith.example.com).
*
*/
readonly zone: IHostedZone;
/**
* The redirect target fully qualified domain name (FQDN). An alias record
* will be created that points to your CloudFront distribution. Root domain
* or sub-domain can be supplied.
*/
readonly targetDomain: string;
/**
* The domain names that will redirect to `targetDomain`
*
* @default - the domain name of the hosted zone
*/
readonly recordNames?: string[];
/**
* The AWS Certificate Manager (ACM) certificate that will be associated with
* the CloudFront distribution that will be created. If provided, the certificate must be
* stored in us-east-1 (N. Virginia)
*
* @default - A new certificate is created in us-east-1 (N. Virginia)
*/
readonly certificate?: ICertificate;
}
/**
* Allows creating a domainA -> domainB redirect using CloudFront and S3.
* You can specify multiple domains to be redirected.
*/
export class HttpsRedirect extends Construct {
constructor(scope: Construct, id: string, props: HttpsRedirectProps) {
super(scope, id);
const domainNames = props.recordNames ?? [props.zone.zoneName];
if (props.certificate) {
const certificateRegion = Stack.of(this).splitArn(props.certificate.certificateArn, ArnFormat.SLASH_RESOURCE_NAME).region;
if (!Token.isUnresolved(certificateRegion) && certificateRegion !== 'us-east-1') {
throw new Error(`The certificate must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`);
}
}
const redirectCert = props.certificate ?? this.createCertificate(domainNames, props.zone);
const redirectBucket = new Bucket(this, 'RedirectBucket', {
websiteRedirect: {
hostName: props.targetDomain,
protocol: RedirectProtocol.HTTPS,
},
removalPolicy: RemovalPolicy.DESTROY,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
});
const redirectDist = new CloudFrontWebDistribution(this, 'RedirectDistribution', {
defaultRootObject: '',
originConfigs: [{
behaviors: [{ isDefaultBehavior: true }],
customOriginSource: {
domainName: redirectBucket.bucketWebsiteDomainName,
originProtocolPolicy: OriginProtocolPolicy.HTTP_ONLY,
},
}],
viewerCertificate: ViewerCertificate.fromAcmCertificate(redirectCert, {
aliases: domainNames,
}),
comment: `Redirect to ${props.targetDomain} from ${domainNames.join(', ')}`,
priceClass: PriceClass.PRICE_CLASS_ALL,
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
});
domainNames.forEach((domainName) => {
const hash = md5hash(domainName).slice(0, 6);
const aliasProps = {
recordName: domainName,
zone: props.zone,
target: RecordTarget.fromAlias(new CloudFrontTarget(redirectDist)),
};
new ARecord(this, `RedirectAliasRecord${hash}`, aliasProps);
new AaaaRecord(this, `RedirectAliasRecordSix${hash}`, aliasProps);
});
}
/**
* Gets the stack to use for creating the Certificate
* If the current stack is not in `us-east-1` then this
* will create a new `us-east-1` stack.
*
* CloudFront is a global resource which you can create (via CloudFormation) from
* _any_ region. So I could create a CloudFront distribution in `us-east-2` if I wanted
* to (maybe the rest of my application lives there). The problem is that some supporting resources
* that CloudFront uses (i.e. ACM Certificates) are required to exist in `us-east-1`. This means
* that if I want to create a CloudFront distribution in `us-east-2` I still need to create a ACM certificate in
* `us-east-1`.
*
* In order to do this correctly we need to know which region the CloudFront distribution is being created in.
* We have two options, either require the user to specify the region or make an assumption if they do not.
* This implementation requires the user specify the region.
*/
private certificateScope(): Construct {
const stack = Stack.of(this);
const parent = stack.node.scope;
if (!parent) {
throw new Error(`Stack ${stack.stackId} must be created in the scope of an App or Stage`);
}
if (Token.isUnresolved(stack.region)) {
throw new Error(`When ${ROUTE53_PATTERNS_USE_CERTIFICATE} is enabled, a region must be defined on the Stack`);
}
if (stack.region !== 'us-east-1') {
const stackId = `certificate-redirect-stack-${stack.node.addr}`;
const certStack = parent.node.tryFindChild(stackId) as Stack;
return certStack ?? new Stack(parent, stackId, {
env: { region: 'us-east-1', account: stack.account },
});
}
return this;
}
/**
* Creates a certificate.
*
* If the `ROUTE53_PATTERNS_USE_CERTIFICATE` feature flag is set then
* this will use the `Certificate` class otherwise it will use the
* `DnsValidatedCertificate` class
*
* This is also safe to upgrade since the new certificate will be created and updated
* on the CloudFront distribution before the old one is deleted.
*/
private createCertificate(domainNames: string[], zone: IHostedZone): ICertificate {
const useCertificate = FeatureFlags.of(this).isEnabled(ROUTE53_PATTERNS_USE_CERTIFICATE);
if (useCertificate) {
// this preserves backwards compatibility. Previously the certificate was always created in `this` scope
// so we need to keep the name the same
const id = (this.certificateScope() === this) ? 'RedirectCertificate' : 'RedirectCertificate'+this.node.addr;
return new Certificate(this.certificateScope(), id, {
domainName: domainNames[0],
subjectAlternativeNames: domainNames,
validation: CertificateValidation.fromDns(zone),
});
} else {
return new DnsValidatedCertificate(this, 'RedirectCertificate', {
domainName: domainNames[0],
subjectAlternativeNames: domainNames,
hostedZone: zone,
region: 'us-east-1',
});
}
}
}