diff --git a/README.md b/README.md index c3bef40..5ba522a 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ CloudFront functions are ideal for lightweight computation tasks on web requests |[Verify JSON Web Tokens](verify-jwt/)| This function performs a lightweight security token validation using JSON Web Tokens. You can use this type of tokenization to give a user of your site a URL that is time-bound. Once the predetermined expiration time has occurred, the user can no longer access the content at that URL.| |[Add CORS headers if missing](add-cors-header/)| This function adds an `Access-Control-Allow-Origin` response header if it is not present on the outgoing response from CloudFront.| |[Add a `Cache-Control` header](add-cache-control-header/)| This function adds a `Cache-Control` response header to the outgoing response from CloudFront for browser caching.| +|[URL redirect to enforce a domain name](enforce-domain-redirect/)| This function will redirect any request for a domain name other than the enforced domain to the enforced domain. The most common use is redirecting the apex or naked domain to the `www` subdomain.| ## Deploying a CloudFront function using the AWS CLI We will use the example that adds cache control headers to responses as our function, but the same process can be used for all the functions with only minor changes. diff --git a/enforce-domain-redirect/README.md b/enforce-domain-redirect/README.md new file mode 100644 index 0000000..9774577 --- /dev/null +++ b/enforce-domain-redirect/README.md @@ -0,0 +1,85 @@ +## Redirect any host that is not the enforced domain + +**CloudFront Functions event type: viewer request** + +This function redirects all users to an enforced domain name. This ensures that all users are using your preferred domain name when accessing your site. The most common example of this is to automatically redirect the *apex* or *naked* domain to the *www* domain. When accessing through the enforced domain, this function does not perform any action. + +**Important: Set the `enforceDomainName` constant to an appropriate value for your specific needs.** + +**Testing the function** + +To validate that the function is working as expected, you can use the JSON test objects in the `test-objects` directory. To test, use the `test-function` CLI command as shown in the following examples: + +**Apex or naked domain redirect** + +This test validates that a request to the `example.com` domain will be redirected to the `www.example.com` domain. + +```shell +# Get the current ETag value +$ aws cloudfront describe-function --name enforce-domain-redirect +# Run the test +$ aws cloudfront test-function --if-match EXXXXXXXXXXXX --name enforce-domain-redirect --event-object fileb://enforce-domain-redirect/test-objects/apex-domain.json +``` + +If the function has been set up correctly, you should see a result similar to the following with the redirect being issued (`location` header being returned) in the `FunctionOutput` JSON object. Notice that the `value` is the `enforceDomainName` value: + +```json +{ + "TestResult": { + "FunctionSummary": { + "Name": "enforce-domain-redirect", + "Status": "UNASSOCIATED", + "FunctionConfig": { + "Comment": "", + "Runtime": "cloudfront-js-2.0" + }, + "FunctionMetadata": { + "FunctionARN": "arn:aws:cloudfront::060232822672:function/enforce-domain-redirect", + "Stage": "DEVELOPMENT", + "CreatedTime": "2024-10-01T14:36:47.121000+00:00", + "LastModifiedTime": "2024-10-01T14:37:08.199000+00:00" + } + }, + "ComputeUtilization": "6", + "FunctionExecutionLogs": [], + "FunctionErrorMessage": "", + "FunctionOutput": "{\"response\":{\"headers\":{\"location\":{\"value\":\"https://www.example.com\"}},\"statusDescription\":\"Moved Permanently\",\"cookies\":{},\"statusCode\":301}}" + } +} +``` + +**No redirect when using enforced domain** + +```shell +# Get the current ETag value +$ aws cloudfront describe-function --name enforce-domain-redirect +# Run the test +$ aws cloudfront test-function --if-match EXXXXXXXXXXXX --name enforce-domain-redirect --event-object fileb://enforce-domain-redirect/test-objects/apex-domain.json +``` + +If the function has been set up correctly, you should see the `request` being returned as part of the `FunctionOutput` JSON object meaning that no action was taken.: + +```json +{ + "TestResult": { + "FunctionSummary": { + "Name": "enforce-domain-redirect", + "Status": "UNASSOCIATED", + "FunctionConfig": { + "Comment": "", + "Runtime": "cloudfront-js-2.0" + }, + "FunctionMetadata": { + "FunctionARN": "arn:aws:cloudfront::060232822672:function/enforce-domain-redirect", + "Stage": "DEVELOPMENT", + "CreatedTime": "2024-10-01T14:36:47.121000+00:00", + "LastModifiedTime": "2024-10-01T14:37:08.199000+00:00" + } + }, + "ComputeUtilization": "6", + "FunctionExecutionLogs": [], + "FunctionErrorMessage": "", + "FunctionOutput": "{\"request\":{\"headers\":{\"host\":{\"value\":\"www.example.com\"},\"accept\":{\"value\":\"text/html\"}},\"method\":\"GET\",\"querystring\":{},\"uri\":\"/index.html\",\"cookies\":{}}}" + } +} +``` diff --git a/enforce-domain-redirect/index.js b/enforce-domain-redirect/index.js new file mode 100644 index 0000000..c9893b0 --- /dev/null +++ b/enforce-domain-redirect/index.js @@ -0,0 +1,20 @@ +function handler(event) { + const request = event.request; + const host = request.headers.host.value; + const enforceDomainName = "www.example.com"; + + if (host !== enforceDomainName) { + const response = { + statusCode: 301, + statusDescription: 'Moved Permanently', + headers: + { + location: { value: `https://${enforceDomainName}` }, + } + } + + return response; + } + + return request; +} \ No newline at end of file diff --git a/enforce-domain-redirect/test-objects/apex-domain.json b/enforce-domain-redirect/test-objects/apex-domain.json new file mode 100644 index 0000000..f078d14 --- /dev/null +++ b/enforce-domain-redirect/test-objects/apex-domain.json @@ -0,0 +1,17 @@ +{ + "version": "1.0", + "context": { + "eventType": "viewer-request" + }, + "viewer": { + "ip": "0.0.0.0" + }, + "request": { + "method": "GET", + "uri": "/index.html", + "headers": { + "host": { "value": "example.com" }, + "accept": { "value": "text/html", "multivalue": [ { "value": "text/html" }, { "value": "application/xhtml+xml" } ] } + } + } +} \ No newline at end of file diff --git a/enforce-domain-redirect/test-objects/enforced-domain.json b/enforce-domain-redirect/test-objects/enforced-domain.json new file mode 100644 index 0000000..b2b4a73 --- /dev/null +++ b/enforce-domain-redirect/test-objects/enforced-domain.json @@ -0,0 +1,17 @@ +{ + "version": "1.0", + "context": { + "eventType": "viewer-request" + }, + "viewer": { + "ip": "0.0.0.0" + }, + "request": { + "method": "GET", + "uri": "/index.html", + "headers": { + "host": { "value": "www.example.com" }, + "accept": { "value": "text/html", "multivalue": [ { "value": "text/html" }, { "value": "application/xhtml+xml" } ] } + } + } +} \ No newline at end of file