Skip to content

Updated README for geoip-redirect package #1418

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
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
59 changes: 56 additions & 3 deletions packages/geoip-redirect/README.md
Original file line number Diff line number Diff line change
@@ -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<string, DomainOverwrite>;
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_
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions packages/geoip-redirect/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { RedirectFunction } from "./lib/redirect-construct";
import {
RedirectFunction,
RedirectFunctionOptions,
} from "./lib/redirect-construct";

export { RedirectFunction };
export { RedirectFunction, RedirectFunctionOptions };
61 changes: 49 additions & 12 deletions packages/geoip-redirect/lib/handlers/redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,71 @@ 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<string, string>,
enablePathRedirect:
process.env.ENABLE_PATH_REDIRECT === "true" ? true : false,
};

options.supportedRegions = {
...(JSON.parse(
JSON.stringify(process.env.SUPPORTED_REGIONS ?? "{}")
) as Record<string, string>),
...{ [options.defaultRegionCode]: options.defaultDomain },
};

const defaultRegion = options.defaultRegionCode.split(",")[0].toLowerCase();

export const handler = async (
event: CloudFrontRequestEvent
): Promise<CloudFrontResponse | CloudFrontRequest> => {
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",
headers: {
location: [
{
key: "Location",
value: redirectURL,
value: redirectURL.replace("/index.html", ""),
},
],
},
Expand Down
45 changes: 36 additions & 9 deletions packages/geoip-redirect/lib/redirect-construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
/**
* 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 {
Expand All @@ -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(),
},
}),
},
Expand Down