diff --git a/packages/geoip-redirect/README.md b/packages/geoip-redirect/README.md index 40b83a8d..2bf87dce 100644 --- a/packages/geoip-redirect/README.md +++ b/packages/geoip-redirect/README.md @@ -1,4 +1,57 @@ -# Geo-IP Redirect -This library provides a construct which creates a Lambda@Edge functions to perform GeoIP redirects. +# Geo-IP Redirect -These functions are intended to be added to an existing Cloudfront distribution +![TypeScript version](https://img.shields.io/github/package-json/dependency-version/aligent/cdk-constructs/dev/typescript?filename=packages/geoip-redirect/package.json&color=red) ![AWS CDK version](https://img.shields.io/github/package-json/dependency-version/aligent/cdk-constructs/dev/aws-cdk?filename=packages/geoip-redirect/package.json) ![NPM version](https://img.shields.io/npm/v/%40aligent%2Fcdk-geoip-redirect?color=green) + +## Overview + +This library provides a construct which creates a Lambda@Edge which is intended to be attached to the Origin Request in a CloudFront distribution. The construct allows a CloudFront website to perform GeoIP redirects to redirect users to a version of the website related to their location such as `.com` `.com.au` or `.co.nz` etc. + +The Lambda@Edge function will check if the viewer's country code matches any supported regions. The user's country code for each request is pulled from the `cloudfront-viewer-country`. The construct will match the code to the record with the corresponding regex lookup. + +## Digram +![geoip lambda@edge diagram](docs/geoip-redirect.drawio.png) + +## Usage and Default Geo-IP Redirect options + +### `redirectHost` (string) + +``` +interface RedirectFunctionOptions { + supportedRegions?: Record; + defaultRegionCode: string; + defaultDomain: string; +} +``` + +| Property | Definition | +| -------- | ---------- | +| `supportedRegions` | A record with domain codes as a key (regex) and a domain to redirect to as a value | +| `defaultRegionCode` | The default region code(s) as regex. These are the regions supported by `defaultDomain`. When multiple codes are used the default will be the first code the default site eg. `AU,NZ` will treat `AU` as the default | +| `defaultDomain` | The website's main domain. This will act as a fallback for any unsupported regions | +| `enablePathRedirect` | Will toggle adding a path suffix for a region such as `.com/au` or whether it should just be `.com` | + +### Using this package + +The two main ways you can use this package are as follows: +First off your website has a basic domain let's say `www.aligent.com.au` and you serve all content for all regions of the world here such as `www.aligent.com.au/au` or `www.aligent.com.au/nz`. For this approach you should use the below method + +``` +redirectBehaviourOptions: { + defaultDomain: "www.iamawesome.com/au", + defaultRegionCode: "AU,NZ", +} +``` + +Any region codes that are regexed to be `XX,YY` (note the comma `,`) will automatically add the matching region as a path suffix to the url. + +However in order to redirect to a website that is different from the base domain such as `www.aligent.co.nz` you can "hardcode" a domain for a region to use by using the `supportedRegions` value. + +``` +redirectBehaviourOptions: { + defaultDomain: "www.example.com", + defaultRegionCode: "AU,US", + supportedRegions: { "NZ": "www.example.co.nz" } +} +``` + +_this package has not been tested with interplanetary domains_ diff --git a/packages/geoip-redirect/docs/geoip-redirect.drawio.png b/packages/geoip-redirect/docs/geoip-redirect.drawio.png new file mode 100644 index 00000000..1c43c045 Binary files /dev/null and b/packages/geoip-redirect/docs/geoip-redirect.drawio.png differ diff --git a/packages/geoip-redirect/index.ts b/packages/geoip-redirect/index.ts index 807b044b..6b054fc4 100644 --- a/packages/geoip-redirect/index.ts +++ b/packages/geoip-redirect/index.ts @@ -1,3 +1,6 @@ -import { RedirectFunction } from "./lib/redirect-construct"; +import { + RedirectFunction, + RedirectFunctionOptions, +} from "./lib/redirect-construct"; -export { RedirectFunction }; +export { RedirectFunction, RedirectFunctionOptions }; diff --git a/packages/geoip-redirect/lib/handlers/redirect.ts b/packages/geoip-redirect/lib/handlers/redirect.ts index 7a762f24..2db256f7 100644 --- a/packages/geoip-redirect/lib/handlers/redirect.ts +++ b/packages/geoip-redirect/lib/handlers/redirect.ts @@ -5,26 +5,63 @@ import { 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; +const options = { + defaultDomain: process.env.DEFAULT_DOMAIN ?? "", + defaultRegionCode: process.env.DEFAULT_REGION_CODE ?? "", + supportedRegions: { "": "" } as Record, + enablePathRedirect: + process.env.ENABLE_PATH_REDIRECT === "true" ? true : false, +}; + +options.supportedRegions = { + ...(JSON.parse( + JSON.stringify(process.env.SUPPORTED_REGIONS ?? "{}") + ) as Record), + ...{ [options.defaultRegionCode]: options.defaultDomain }, +}; + +const defaultRegion = options.defaultRegionCode.split(",")[0].toLowerCase(); export const handler = async ( event: CloudFrontRequestEvent ): Promise => { const request = event.Records[0].cf.request; - let redirectURL = `https://${REDIRECT_HOST}/`; + // this block takes the records in supportedRegions and converts the keys to lowercase. + // doesn't change the functionality but makes it easier for users to not worry about being case sensitive + if (options.supportedRegions) { + options.supportedRegions = Object.keys(options.supportedRegions).reduce( + (newRecord, key) => { + newRecord[key.toLowerCase()] = options.supportedRegions + ? options.supportedRegions[key] + : ""; + return newRecord; + }, + {} // keeps the value for the corresponding key + ); + } + + let redirectURL = `https://${options.defaultDomain}/`; 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}`; + const countryCode = + request.headers["cloudfront-viewer-country"][0].value.toLowerCase(); + // Check if any key in supportedSubRegions matches the countryCode using regex + const recordKey = Object.keys(options.supportedRegions || {}) + .find(recordRegionCode => + recordRegionCode.toLowerCase().includes(countryCode) + ) + ?.toLowerCase(); + // if theres a record key it means a redirect domain was hardcoded in the value we can get the value of a record using record[key] + if (recordKey) { + redirectURL = `https://${options.supportedRegions[recordKey]}/`; + // If the key includes multiple domains, we additionally want to redirect to the country path of the user + if (recordKey.includes(",")) redirectURL += countryCode.toLowerCase(); } else { - redirectURL = `${redirectURL}${DEFAULT_REGION.toLowerCase()}${ - request.uri - }`; + // otherwise direct to the default domain + redirectURL = `${redirectURL}${request.uri}`; + if (options.enablePathRedirect) + redirectURL = `${redirectURL}${defaultRegion}${request.uri}`; } - return { status: "302", statusDescription: "Found", @@ -32,7 +69,7 @@ export const handler = async ( location: [ { key: "Location", - value: redirectURL, + value: redirectURL.replace("/index.html", ""), }, ], }, diff --git a/packages/geoip-redirect/lib/redirect-construct.ts b/packages/geoip-redirect/lib/redirect-construct.ts index 49a8d5b9..6f618bdb 100644 --- a/packages/geoip-redirect/lib/redirect-construct.ts +++ b/packages/geoip-redirect/lib/redirect-construct.ts @@ -5,12 +5,28 @@ import { Construct } from "constructs"; import { join } from "path"; import { Esbuild } from "@aligent/cdk-esbuild"; +/** + * The default region, domain, and other supported regions for a website to redirect to. + */ export interface RedirectFunctionOptions { - redirectHost: string; - // Case-sensitive regular expression matching cloudfront-viewer-country - supportedRegionsExpression: string; - // default region code to use when not matched - defaultRegion: string; + /** + * Regex formatted string to match region codes and redirect to the DomainOverwrite destination. + * @default undefined + */ + supportedRegions?: Record; + /** + * Regex for supported domain paths on the default domain eg .com/au + */ + defaultRegionCode: string; + /** + * Default domain to redirect to unless otherwise specified. + */ + defaultDomain: string; + /** + * Toggle whether to use a path suffix for a region such as `.com/au` or just `.com` . + * @default false + */ + enablePathRedirect?: boolean; } export class RedirectFunction extends Construct { @@ -35,12 +51,23 @@ export class RedirectFunction extends Construct { command, image: DockerImage.fromRegistry("busybox"), local: new Esbuild({ + minify: false, + minifySyntax: false, + minifyWhitespace: false, entryPoints: [join(__dirname, "handlers/redirect.ts")], define: { - "process.env.REDIRECT_HOST": options.redirectHost, - "process.env.SUPPORTED_REGIONS": - options.supportedRegionsExpression, - "process.env.DEFAULT_REGION": options.defaultRegion, + "process.env.DEFAULT_DOMAIN": JSON.stringify( + options.defaultDomain + ), + "process.env.DEFAULT_REGION_CODE": JSON.stringify( + options.defaultRegionCode + ).toLowerCase(), + "process.env.SUPPORTED_REGIONS": JSON.stringify( + options.supportedRegions + )?.toLowerCase(), + "process.env.ENABLE_PATH_REDIRECT": JSON.stringify( + options.enablePathRedirect + )?.toLowerCase(), }, }), },