diff --git a/packages/lambda-at-edge-handlers/.gitignore b/packages/lambda-at-edge-handlers/.gitignore index 8f77f768..75ecda95 100644 --- a/packages/lambda-at-edge-handlers/.gitignore +++ b/packages/lambda-at-edge-handlers/.gitignore @@ -55,4 +55,4 @@ Thumbs.db cdk.out *.d.ts -*.js +*.js \ No newline at end of file diff --git a/packages/lambda-at-edge-handlers/index.ts b/packages/lambda-at-edge-handlers/index.ts index a2790fd8..3bade8fe 100644 --- a/packages/lambda-at-edge-handlers/index.ts +++ b/packages/lambda-at-edge-handlers/index.ts @@ -1,3 +1,13 @@ -import { prerenderHandler } from "./lib/prerender-handler"; +import { handler as PrerenderHandler } from "./lib/prerender"; +import { handler as PrerenderCacheControlHandler } from "./lib/cache-control"; +import { handler as PrerenderErrorResponseHandler } from "./lib/error-response"; +import { handler as PrerenderCheckHandler } from "./lib/prerender-check"; +import { handler as GeoIpRedirectHandler } from "./lib/redirect"; -export { prerenderHandler }; +export { + PrerenderHandler, + PrerenderCacheControlHandler, + PrerenderErrorResponseHandler, + PrerenderCheckHandler, + GeoIpRedirectHandler, +}; diff --git a/packages/lambda-at-edge-handlers/lib/cache-control.ts b/packages/lambda-at-edge-handlers/lib/cache-control.ts new file mode 100644 index 00000000..cb15455f --- /dev/null +++ b/packages/lambda-at-edge-handlers/lib/cache-control.ts @@ -0,0 +1,21 @@ +import 'source-map-support/register'; +import { CloudFrontResponseEvent, CloudFrontResponse } from 'aws-lambda'; +/* + Prerender cache control function + Consider associate this function as *origin response* function +*/ +export const handler = async (event: CloudFrontResponseEvent): Promise => { + const cacheKey = process.env.PRERENDER_CACHE_KEY || 'x-prerender-requestid'; + const cacheMaxAge = process.env.PRERENDER_CACHE_MAX_AGE || '0'; + let response = event.Records[0].cf.response; + + if (response.headers[`${cacheKey}`]) { + response.headers['Cache-Control'] = [ + { + key: 'Cache-Control', + value: `max-age=${cacheMaxAge}` + } + ] + } + return response; +} diff --git a/packages/lambda-at-edge-handlers/lib/error-response.ts b/packages/lambda-at-edge-handlers/lib/error-response.ts new file mode 100644 index 00000000..c4e44903 --- /dev/null +++ b/packages/lambda-at-edge-handlers/lib/error-response.ts @@ -0,0 +1,61 @@ +import "source-map-support/register"; +import { + CloudFrontRequest, + CloudFrontResponseEvent, + CloudFrontResponse, +} from "aws-lambda"; +import axios from "axios"; +import * as https from "https"; + +const FRONTEND_HOST = process.env.FRONTEND_HOST; +const PATH_PREFIX = process.env.PATH_PREFIX; + +// Create axios client outside of lambda function for re-use between calls +const instance = axios.create({ + timeout: 1000, + // Don't follow redirects + maxRedirects: 0, + // Only valid response codes are 200 + validateStatus: function (status) { + return status == 200; + }, + // keep connection alive so we don't constantly do SSL negotiation + httpsAgent: new https.Agent({ keepAlive: true }), +}); + +export const handler = ( + event: CloudFrontResponseEvent +): Promise => { + let response = event.Records[0].cf.response; + let request = event.Records[0].cf.request; + + if ( + response.status != "200" && + !request.headers["x-request-prerender"] && + request.uri != `${PATH_PREFIX}/index.html` + ) { + // Fetch default page and return body + return instance + .get(`https://${FRONTEND_HOST}${PATH_PREFIX}/index.html`) + .then((res) => { + response.body = res.data; + + response.headers["content-type"] = [ + { + key: "Content-Type", + value: "text/html", + }, + ]; + + // Remove content-length if set as this may be the value from the origin. + delete response.headers["content-length"]; + + return response; + }) + .catch((err) => { + return response; + }); + } + + return Promise.resolve(response); +}; diff --git a/packages/lambda-at-edge-handlers/lib/prerender-check.ts b/packages/lambda-at-edge-handlers/lib/prerender-check.ts new file mode 100644 index 00000000..6b4ec090 --- /dev/null +++ b/packages/lambda-at-edge-handlers/lib/prerender-check.ts @@ -0,0 +1,35 @@ +import "source-map-support/register"; +import { CloudFrontRequest, CloudFrontRequestEvent } from "aws-lambda"; + +const IS_BOT = + /googlebot|chrome-lighthouse|lighthouse|adsbot\-google|Feedfetcher\-Google|bingbot|yandex|baiduspider|Facebot|facebookexternalhit|twitterbot|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator/i; +const IS_FILE = + /\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot)$/i; + +export const handler = async ( + event: CloudFrontRequestEvent +): Promise => { + let request = event.Records[0].cf.request; + + // If the request is from a bot, is not a file and is not from prerender + // then set the x-request-prerender header so the origin-request lambda function + // alters the origin to prerender.io + if ( + !IS_FILE.test(request.uri) && + IS_BOT.test(request.headers["user-agent"][0].value) && + !request.headers["x-prerender"] + ) { + request.headers["x-request-prerender"] = [ + { + key: "x-request-prerender", + value: "true", + }, + ]; + + request.headers["x-prerender-host"] = [ + { key: "X-Prerender-Host", value: request.headers.host[0].value }, + ]; + } + + return request; +}; diff --git a/packages/lambda-at-edge-handlers/lib/prerender-handler.ts b/packages/lambda-at-edge-handlers/lib/prerender.ts similarity index 96% rename from packages/lambda-at-edge-handlers/lib/prerender-handler.ts rename to packages/lambda-at-edge-handlers/lib/prerender.ts index 7e4b069b..d898fa06 100644 --- a/packages/lambda-at-edge-handlers/lib/prerender-handler.ts +++ b/packages/lambda-at-edge-handlers/lib/prerender.ts @@ -4,15 +4,16 @@ import { CloudFrontRequestEvent, CloudFrontResponse, } from "aws-lambda"; + const PRERENDER_TOKEN = process.env.PRERENDER_TOKEN ? process.env.PRERENDER_TOKEN - : "undef"; + : "undefined"; const PATH_PREFIX = process.env.PATH_PREFIX; const PRERENDER_URL = process.env.PRERENDER_URL ? process.env.PRERENDER_URL : "service.prerender.io"; -export const prerenderHandler = async ( +export const handler = async ( event: CloudFrontRequestEvent ): Promise => { let request = event.Records[0].cf.request; diff --git a/packages/lambda-at-edge-handlers/lib/redirect.ts b/packages/lambda-at-edge-handlers/lib/redirect.ts new file mode 100644 index 00000000..fe260ccc --- /dev/null +++ b/packages/lambda-at-edge-handlers/lib/redirect.ts @@ -0,0 +1,34 @@ +import 'source-map-support/register'; +import { CloudFrontRequestEvent, CloudFrontResponse, CloudFrontRequest } from 'aws-lambda'; + +const REDIRECT_HOST = process.env.REDIRECT_HOST; +const SUPPORTED_REGIONS = new RegExp(process.env.SUPPORTED_REGIONS); +const DEFAULT_REGION = process.env.DEFAULT_REGION; + +export const handler = async (event: CloudFrontRequestEvent): Promise => { + let request = event.Records[0].cf.request; + + let redirectURL = `https://${REDIRECT_HOST}/`; + if (request.headers['cloudfront-viewer-country']) { + const countryCode = request.headers['cloudfront-viewer-country'][0].value; + if (SUPPORTED_REGIONS.test(countryCode)) { + redirectURL = `${redirectURL}${countryCode.toLowerCase()}${request.uri}`; + } else { + redirectURL = `${redirectURL}${DEFAULT_REGION.toLowerCase()}${request.uri}`; + } + + return { + status: '302', + statusDescription: 'Found', + headers: { + location: [{ + key: 'Location', + value: redirectURL, + }], + }, + }; + } + + return request; + +} diff --git a/packages/lambda-at-edge-handlers/package-lock.json b/packages/lambda-at-edge-handlers/package-lock.json index 3ab5d49f..6d78e486 100644 --- a/packages/lambda-at-edge-handlers/package-lock.json +++ b/packages/lambda-at-edge-handlers/package-lock.json @@ -15,9 +15,9 @@ } }, "node_modules/@types/aws-lambda": { - "version": "8.10.108", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.108.tgz", - "integrity": "sha512-1yh1W1WoqK3lGHy+V/Fi55zobxrDHUUsluCWdMlOXkCvtsCmHPXOG+CQ2STIL4B1g6xi6I6XzxaF8V9+zeIFLA==" + "version": "8.10.109", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.109.tgz", + "integrity": "sha512-/ME92FneNyXQzrAfcnQQlW1XkCZGPDlpi2ao1MJwecN+6SbeonKeggU8eybv1DfKli90FAVT1MlIZVXfwVuCyg==" }, "node_modules/buffer-from": { "version": "1.1.2", @@ -53,9 +53,9 @@ }, "dependencies": { "@types/aws-lambda": { - "version": "8.10.108", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.108.tgz", - "integrity": "sha512-1yh1W1WoqK3lGHy+V/Fi55zobxrDHUUsluCWdMlOXkCvtsCmHPXOG+CQ2STIL4B1g6xi6I6XzxaF8V9+zeIFLA==" + "version": "8.10.109", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.109.tgz", + "integrity": "sha512-/ME92FneNyXQzrAfcnQQlW1XkCZGPDlpi2ao1MJwecN+6SbeonKeggU8eybv1DfKli90FAVT1MlIZVXfwVuCyg==" }, "buffer-from": { "version": "1.1.2", diff --git a/packages/lambda-at-edge-handlers/package.json b/packages/lambda-at-edge-handlers/package.json index 250092e4..d7dac0fd 100644 --- a/packages/lambda-at-edge-handlers/package.json +++ b/packages/lambda-at-edge-handlers/package.json @@ -21,6 +21,7 @@ "dependencies": { "@types/aws-lambda": "^8.10.77", "esbuild": "0.12.15", - "source-map-support": "^0.5.16" + "source-map-support": "^0.5.16", + "axios": "^0.21.1" } }