From dabddc25cb1df9e7ae0590bb85afea09a64ed922 Mon Sep 17 00:00:00 2001 From: Gowri Date: Wed, 27 Sep 2023 11:31:51 +0930 Subject: [PATCH 01/10] DO-1534: upgrade rabbitmq construct --- package-lock.json | 22 +++++ packages/rabbitmq/.gitignore | 8 ++ packages/rabbitmq/.npmignore | 11 +++ packages/rabbitmq/.npmrc | 1 + packages/rabbitmq/.nvmrc | 1 + packages/rabbitmq/CDKPipeline-RabbitMQ.jpeg | Bin 0 -> 46055 bytes packages/rabbitmq/README.md | 88 ++++++++++++++++++++ packages/rabbitmq/index.ts | 3 + packages/rabbitmq/lib/rabbitmq-construct.ts | 68 +++++++++++++++ packages/rabbitmq/package.json | 30 +++++++ packages/rabbitmq/tsconfig.json | 3 + 11 files changed, 235 insertions(+) create mode 100644 packages/rabbitmq/.gitignore create mode 100644 packages/rabbitmq/.npmignore create mode 100644 packages/rabbitmq/.npmrc create mode 100644 packages/rabbitmq/.nvmrc create mode 100644 packages/rabbitmq/CDKPipeline-RabbitMQ.jpeg create mode 100644 packages/rabbitmq/README.md create mode 100644 packages/rabbitmq/index.ts create mode 100644 packages/rabbitmq/lib/rabbitmq-construct.ts create mode 100644 packages/rabbitmq/package.json create mode 100644 packages/rabbitmq/tsconfig.json diff --git a/package-lock.json b/package-lock.json index c176627a..cb26bcac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,10 @@ "node": ">=0.10.0" } }, + "node_modules/@aligent/cdk-rabbitmq": { + "resolved": "packages/rabbitmq", + "link": true + }, "node_modules/@aligent/cdk-static-hosting": { "resolved": "packages/static-hosting", "link": true @@ -5779,6 +5783,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/rabbitmq": { + "version": "2.0.0", + "license": "GPL-3.0-only", + "dependencies": { + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + } + }, "packages/static-hosting": { "name": "@aligent/cdk-static-hosting", "version": "2.0.0", diff --git a/packages/rabbitmq/.gitignore b/packages/rabbitmq/.gitignore new file mode 100644 index 00000000..4bdca62e --- /dev/null +++ b/packages/rabbitmq/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out \ No newline at end of file diff --git a/packages/rabbitmq/.npmignore b/packages/rabbitmq/.npmignore new file mode 100644 index 00000000..1464bb5c --- /dev/null +++ b/packages/rabbitmq/.npmignore @@ -0,0 +1,11 @@ +*.ts +!lib/handlers/*.ts +!*.d.ts +!*.js + +# CDK asset staging directory +.cdk.staging +cdk.out + +# Samples +sample/ \ No newline at end of file diff --git a/packages/rabbitmq/.npmrc b/packages/rabbitmq/.npmrc new file mode 100644 index 00000000..3b9bddfc --- /dev/null +++ b/packages/rabbitmq/.npmrc @@ -0,0 +1 @@ +10.1.0 \ No newline at end of file diff --git a/packages/rabbitmq/.nvmrc b/packages/rabbitmq/.nvmrc new file mode 100644 index 00000000..ef1520fc --- /dev/null +++ b/packages/rabbitmq/.nvmrc @@ -0,0 +1 @@ +20.7.0 \ No newline at end of file diff --git a/packages/rabbitmq/CDKPipeline-RabbitMQ.jpeg b/packages/rabbitmq/CDKPipeline-RabbitMQ.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b248b94fe68e98e3bccc05df8f893dd51a25b114 GIT binary patch literal 46055 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 ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'" + IdentityFile /path/to/ssh_key/for/backend_instance + +2. Find RabbitMQ endpoint hostname and the application BE instance ID to run this command that will open a ssh connection (not an interactive shell with the `-N` flag): + + ssh -N -L ::443 + + for example, + + ssh -L 56710:b-abcd-ef12-3456-7890-abcdef123456.mq.ap-southeast-2.amazonaws.com:443 i-abcdef1234567890 + +3. Open your web browser to access to the GUI (get id/pw from environments.ts) + + https://localhost: + + for example, + + https://localhost:56710 + + +## Local development +[NPM link](https://docs.npmjs.com/cli/v7/commands/npm-link) can be used to develop the module locally. +1. Pull this repository locally +2. `cd` into this repository +3. run `npm link` +4. `cd` into the downstream repo (target project, etc) and run `npm link 'aws-rabbitmq-stack'` +The downstream repository should now include a symlink to this module. Allowing local changes to be tested before pushing. \ No newline at end of file diff --git a/packages/rabbitmq/index.ts b/packages/rabbitmq/index.ts new file mode 100644 index 00000000..835a9f0c --- /dev/null +++ b/packages/rabbitmq/index.ts @@ -0,0 +1,3 @@ +import { RabbitMQ } from "./lib/rabbitmq-construct"; + +export { RabbitMQ }; \ No newline at end of file diff --git a/packages/rabbitmq/lib/rabbitmq-construct.ts b/packages/rabbitmq/lib/rabbitmq-construct.ts new file mode 100644 index 00000000..33b77b4f --- /dev/null +++ b/packages/rabbitmq/lib/rabbitmq-construct.ts @@ -0,0 +1,68 @@ +import { Construct } from "constructs"; +import { CfnOutput } from "aws-cdk-lib"; +import { SecurityGroup, Vpc, Port } from "aws-cdk-lib/aws-ec2"; +import { CfnBrokerProps, CfnBroker } from "aws-cdk-lib/aws-amazonmq"; + +export interface RabbitMQProps { + rabbitMQProps: CfnBrokerProps; + applicationVpcId: string; + applicationSecurityGroupId: string; +} + +export class RabbitMQ extends Construct { + constructor(scope: Construct, id: string, props: RabbitMQProps) { + super(scope, id); + + const sourceSecurityGroup = SecurityGroup.fromLookupById( + this, + id + "-sourceSecurityGroup", + props.applicationSecurityGroupId + ); + + const applicationVpc = Vpc.fromLookup(this, id + "-applicationVpc", { + vpcId: props.applicationVpcId, + }); + + const securityGroup = new SecurityGroup(this, id + "-securityGroup", { + vpc: applicationVpc, + allowAllOutbound: false, + }); + + securityGroup.addIngressRule(sourceSecurityGroup, Port.tcp(5671)); + securityGroup.addIngressRule(sourceSecurityGroup, Port.tcp(443)); + + // Choose only one or two subnets out of all the available private ones + const rabbitMqSubnets: string[] = []; + if (props.rabbitMQProps.deploymentMode == "SINGLE_INSTANCE") { + rabbitMqSubnets.push(applicationVpc.privateSubnets[0].subnetId); + } else { + rabbitMqSubnets.push(applicationVpc.privateSubnets[0].subnetId); + rabbitMqSubnets.push(applicationVpc.privateSubnets[1].subnetId); + } + + const rabbitMQ = new CfnBroker(this, id + "-rabbitMQBroker", { + autoMinorVersionUpgrade: props.rabbitMQProps.autoMinorVersionUpgrade, + brokerName: props.rabbitMQProps.brokerName, + deploymentMode: props.rabbitMQProps.deploymentMode, + engineType: props.rabbitMQProps.engineType, + engineVersion: props.rabbitMQProps.engineVersion, + hostInstanceType: props.rabbitMQProps.hostInstanceType, + publiclyAccessible: props.rabbitMQProps.publiclyAccessible, + users: props.rabbitMQProps.users, + logs: props.rabbitMQProps.logs, + maintenanceWindowStartTime: + props.rabbitMQProps.maintenanceWindowStartTime, + securityGroups: [securityGroup.securityGroupId], + subnetIds: rabbitMqSubnets, + }); + + // Cfn does not respect .split(). We will get by with Arn for now. + // const arn = rabbitmq.attrArn + // const endpoint = arn.split(":", 7) + '.mq.' + this.region + '.amazonaws.com' + new CfnOutput(this, rabbitMQ.brokerName + "Arn", { + // value: rendpoint, + value: rabbitMQ.attrArn, + exportName: rabbitMQ.brokerName + "Arn", + }); + } +} diff --git a/packages/rabbitmq/package.json b/packages/rabbitmq/package.json new file mode 100644 index 00000000..0b47cdd5 --- /dev/null +++ b/packages/rabbitmq/package.json @@ -0,0 +1,30 @@ +{ + "name": "@aligent/cdk-rabbitmq", + "version": "2.0.0", + "main": "index.js", + "license": "GPL-3.0-only", + "homepage": "https://github.com/aligent/aws-rabbitmq-waf-stack#readme", + "repository": { + "type": "git", + "url": "https://github.com/aligent/aws-rabbitmq-waf-stack" + }, + "types": "index.d.ts", + "scripts": { + "build": "tsc", + "prepublish": "tsc" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + }, + "dependencies": { + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } +} diff --git a/packages/rabbitmq/tsconfig.json b/packages/rabbitmq/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/rabbitmq/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} From ff3c5a3a38065af94936284026737f147c828934 Mon Sep 17 00:00:00 2001 From: Gowri Date: Wed, 27 Sep 2023 12:36:32 +0930 Subject: [PATCH 02/10] DO-1530: move esbuild to own package * upgrade basic-auth construct * upgrade cloudfront-security-headers construct --- package-lock.json | 73 +++++++++++++++++++ packages/basic-auth/.gitignore | 58 +++++++++++++++ packages/basic-auth/.npmignore | 11 +++ packages/basic-auth/.npmrc | 1 + packages/basic-auth/.nvmrc | 1 + packages/basic-auth/README.md | 4 + packages/basic-auth/index.ts | 3 + .../basic-auth/lib/basic-auth-construct.ts | 52 +++++++++++++ .../basic-auth/lib/handlers/basic-auth.ts | 38 ++++++++++ packages/basic-auth/package.json | 35 +++++++++ packages/basic-auth/tsconfig.json | 3 + .../cloudfront-security-headers/.gitignore | 8 ++ .../cloudfront-security-headers/.npmignore | 11 +++ packages/cloudfront-security-headers/.npmrc | 1 + packages/cloudfront-security-headers/.nvmrc | 1 + .../cloudfront-security-headers/README.md | 8 ++ .../lib/handlers/security-header.ts | 25 +++++++ .../cloudfront-security-headers/lib/index.ts | 60 +++++++++++++++ .../cloudfront-security-headers/package.json | 28 +++++++ .../cdk-cloudfront-security-headers.test.ts | 17 +++++ .../cloudfront-security-headers/tsconfig.json | 3 + .../lib/utils => esbuild}/esbuild.ts | 6 +- packages/esbuild/index.ts | 3 + packages/esbuild/package.json | 26 +++++++ packages/esbuild/tsconfig.json | 3 + packages/rabbitmq/index.ts | 2 +- packages/static-hosting/.npmrc | 2 +- packages/static-hosting/.nvmrc | 2 +- packages/static-hosting/lib/path-remap.ts | 2 +- 29 files changed, 480 insertions(+), 7 deletions(-) create mode 100644 packages/basic-auth/.gitignore create mode 100644 packages/basic-auth/.npmignore create mode 100644 packages/basic-auth/.npmrc create mode 100644 packages/basic-auth/.nvmrc create mode 100644 packages/basic-auth/README.md create mode 100644 packages/basic-auth/index.ts create mode 100644 packages/basic-auth/lib/basic-auth-construct.ts create mode 100644 packages/basic-auth/lib/handlers/basic-auth.ts create mode 100644 packages/basic-auth/package.json create mode 100644 packages/basic-auth/tsconfig.json create mode 100644 packages/cloudfront-security-headers/.gitignore create mode 100644 packages/cloudfront-security-headers/.npmignore create mode 100644 packages/cloudfront-security-headers/.npmrc create mode 100644 packages/cloudfront-security-headers/.nvmrc create mode 100644 packages/cloudfront-security-headers/README.md create mode 100644 packages/cloudfront-security-headers/lib/handlers/security-header.ts create mode 100644 packages/cloudfront-security-headers/lib/index.ts create mode 100644 packages/cloudfront-security-headers/package.json create mode 100644 packages/cloudfront-security-headers/test/cdk-cloudfront-security-headers.test.ts create mode 100644 packages/cloudfront-security-headers/tsconfig.json rename packages/{static-hosting/lib/utils => esbuild}/esbuild.ts (91%) create mode 100644 packages/esbuild/index.ts create mode 100644 packages/esbuild/package.json create mode 100644 packages/esbuild/tsconfig.json diff --git a/package-lock.json b/package-lock.json index cb26bcac..b8d3bc82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,14 @@ "node": ">=0.10.0" } }, + "node_modules/@aligent/cdk-basic-auth": { + "resolved": "packages/basic-auth", + "link": true + }, + "node_modules/@aligent/cdk-cloudfront-security-headers": { + "resolved": "packages/cloudfront-security-headers", + "link": true + }, "node_modules/@aligent/cdk-rabbitmq": { "resolved": "packages/rabbitmq", "link": true @@ -38,6 +46,10 @@ "resolved": "packages/static-hosting", "link": true }, + "node_modules/@aligent/esbuild": { + "resolved": "packages/esbuild", + "link": true + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -5783,7 +5795,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/basic-auth": { + "name": "@aligent/cdk-basic-auth", + "version": "2.0.0", + "license": "GPL-3.0-only", + "dependencies": { + "@types/aws-lambda": "^8.10.122", + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "esbuild": "^0.17.0", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + } + }, + "packages/cloudfront-security-headers": { + "name": "@aligent/cdk-cloudfront-security-headers", + "version": "2.0.0", + "dependencies": { + "@types/aws-lambda": "^8.10.122", + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + } + }, + "packages/esbuild": { + "name": "@aligent/esbuild", + "version": "2.0.0", + "license": "GPL-3.0-only", + "dependencies": { + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "esbuild": "^0.17.0", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + } + }, "packages/rabbitmq": { + "name": "@aligent/cdk-rabbitmq", "version": "2.0.0", "license": "GPL-3.0-only", "dependencies": { diff --git a/packages/basic-auth/.gitignore b/packages/basic-auth/.gitignore new file mode 100644 index 00000000..75ecda95 --- /dev/null +++ b/packages/basic-auth/.gitignore @@ -0,0 +1,58 @@ +# These are some examples of commonly ignored file patterns. +# You should customize this list as applicable to your project. +# Learn more about .gitignore: +# https://www.atlassian.com/git/tutorials/saving-changes/gitignore + +# Node artifact files +node_modules/ +dist/ + +# Compiled Java class files +*.class + +# Compiled Python bytecode +*.py[cod] + +# Log files +*.log + +# Package files +*.jar + +# Maven +target/ +dist/ + +# JetBrains IDE +.idea/ + +# Unit test reports +TEST*.xml + +# Generated by MacOS +.DS_Store + +# Generated by Windows +Thumbs.db + +# Applications +*.app +*.exe +*.war + +# Large media files +*.mp4 +*.tiff +*.avi +*.flv +*.mov +*.wmv + +!jest.config.js + +# CDK asset staging directory +.cdk.staging +cdk.out + +*.d.ts +*.js \ No newline at end of file diff --git a/packages/basic-auth/.npmignore b/packages/basic-auth/.npmignore new file mode 100644 index 00000000..1464bb5c --- /dev/null +++ b/packages/basic-auth/.npmignore @@ -0,0 +1,11 @@ +*.ts +!lib/handlers/*.ts +!*.d.ts +!*.js + +# CDK asset staging directory +.cdk.staging +cdk.out + +# Samples +sample/ \ No newline at end of file diff --git a/packages/basic-auth/.npmrc b/packages/basic-auth/.npmrc new file mode 100644 index 00000000..3b9bddfc --- /dev/null +++ b/packages/basic-auth/.npmrc @@ -0,0 +1 @@ +10.1.0 \ No newline at end of file diff --git a/packages/basic-auth/.nvmrc b/packages/basic-auth/.nvmrc new file mode 100644 index 00000000..ef1520fc --- /dev/null +++ b/packages/basic-auth/.nvmrc @@ -0,0 +1 @@ +20.7.0 \ No newline at end of file diff --git a/packages/basic-auth/README.md b/packages/basic-auth/README.md new file mode 100644 index 00000000..c16f668b --- /dev/null +++ b/packages/basic-auth/README.md @@ -0,0 +1,4 @@ +# Basic Auth +This library provides a construct which creates a Lambda@Edge functions to perform basic auth validation. + +These functions are intended to be added to an existing Cloudfront distribution diff --git a/packages/basic-auth/index.ts b/packages/basic-auth/index.ts new file mode 100644 index 00000000..0cf5631f --- /dev/null +++ b/packages/basic-auth/index.ts @@ -0,0 +1,3 @@ +import { BasicAuthFunction } from "./lib/basic-auth-construct"; + +export { BasicAuthFunction }; diff --git a/packages/basic-auth/lib/basic-auth-construct.ts b/packages/basic-auth/lib/basic-auth-construct.ts new file mode 100644 index 00000000..2c954f55 --- /dev/null +++ b/packages/basic-auth/lib/basic-auth-construct.ts @@ -0,0 +1,52 @@ +import { Construct } from "constructs"; +import { EdgeFunction } from "aws-cdk-lib/aws-cloudfront/lib/experimental"; +import { Esbuild } from "@aligent/esbuild"; +import { AssetHashType, DockerImage } from "aws-cdk-lib"; +import { Code, IVersion, Runtime, Version } from "aws-cdk-lib/aws-lambda"; +import { join } from "path"; + +export interface BasicAuthFunctionOptions { + username: string; + password: string; +} + +export class BasicAuthFunction extends Construct { + readonly edgeFunction: EdgeFunction; + + constructor(scope: Construct, id: string, options: BasicAuthFunctionOptions) { + super(scope, id); + + const command = [ + "sh", + "-c", + 'echo "Docker build not supported. Please install esbuild."', + ]; + + this.edgeFunction = new EdgeFunction(this, `${id}-basic-auth-fn`, { + code: Code.fromAsset(join(__dirname, "handlers"), { + assetHashType: AssetHashType.OUTPUT, + bundling: { + command, + image: DockerImage.fromRegistry("busybox"), + local: new Esbuild({ + entryPoints: [join(__dirname, "handlers/basic-auth.ts")], + define: { + "process.env.AUTH_USERNAME": options.username, + "process.env.AUTH_PASSWORD": options.password, + }, + }), + }, + }), + runtime: Runtime.NODEJS_18_X, + handler: "basic-auth.handler", + }); + } + + public getFunctionVersion(): IVersion { + return Version.fromVersionArn( + this, + "basic-auth-function-version", + this.edgeFunction.currentVersion.edgeArn + ); + } +} diff --git a/packages/basic-auth/lib/handlers/basic-auth.ts b/packages/basic-auth/lib/handlers/basic-auth.ts new file mode 100644 index 00000000..262387a0 --- /dev/null +++ b/packages/basic-auth/lib/handlers/basic-auth.ts @@ -0,0 +1,38 @@ +import "source-map-support/register"; +import { + CloudFrontRequestEvent, + CloudFrontResponse, + CloudFrontRequest, +} from "aws-lambda"; + +const AUTH_USERNAME = process.env.AUTH_USERNAME; +const AUTH_PASSWORD = process.env.AUTH_PASSWORD; +const authString = + "Basic " + + Buffer.from(AUTH_USERNAME + ":" + AUTH_PASSWORD, "binary").toString("base64"); + +export const handler = async ( + event: CloudFrontRequestEvent +): Promise => { + const request = event.Records[0].cf.request; + const headers = request.headers; + + // Require Basic authentication + if ( + typeof headers.authorization == "undefined" || + headers.authorization[0].value != authString + ) { + const body = "Unauthorized"; + const response = { + status: "401", + statusDescription: "Unauthorized", + body: body, + headers: { + "www-authenticate": [{ key: "WWW-Authenticate", value: "Basic" }], + }, + }; + return response; + } + + return request; +}; diff --git a/packages/basic-auth/package.json b/packages/basic-auth/package.json new file mode 100644 index 00000000..acfc50da --- /dev/null +++ b/packages/basic-auth/package.json @@ -0,0 +1,35 @@ +{ + "name": "@aligent/cdk-basic-auth", + "version": "2.0.0", + "description": "A Cloudfront Lambda@Edge stack for performing basic auth protection", + "main": "index.js", + "scripts": { + "build": "tsc && cd ./lib/handlers && npm ci", + "prepublish": "tsc && cd ./lib/handlers && npm ci" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/aligent/aws-cdk-prerender-proxy-stack.git" + }, + "license": "GPL-3.0-only", + "bugs": { + "url": "https://github.com/aligent/aws-cdk-prerender-proxy-stack/issues" + }, + "homepage": "https://github.com/aligent/aws-cdk-prerender-proxy-stack#readme", + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + }, + "dependencies": { + "@types/aws-lambda": "^8.10.122", + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "esbuild": "^0.17.0", + "source-map-support": "^0.5.21" + } +} diff --git a/packages/basic-auth/tsconfig.json b/packages/basic-auth/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/basic-auth/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/packages/cloudfront-security-headers/.gitignore b/packages/cloudfront-security-headers/.gitignore new file mode 100644 index 00000000..f60797b6 --- /dev/null +++ b/packages/cloudfront-security-headers/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/packages/cloudfront-security-headers/.npmignore b/packages/cloudfront-security-headers/.npmignore new file mode 100644 index 00000000..bfd115ba --- /dev/null +++ b/packages/cloudfront-security-headers/.npmignore @@ -0,0 +1,11 @@ +*.ts +!lib/handlers/*.ts +!*.d.ts +!*.js + +# CDK asset staging directory +.cdk.staging +cdk.out + +# Samples +sample/ diff --git a/packages/cloudfront-security-headers/.npmrc b/packages/cloudfront-security-headers/.npmrc new file mode 100644 index 00000000..3b9bddfc --- /dev/null +++ b/packages/cloudfront-security-headers/.npmrc @@ -0,0 +1 @@ +10.1.0 \ No newline at end of file diff --git a/packages/cloudfront-security-headers/.nvmrc b/packages/cloudfront-security-headers/.nvmrc new file mode 100644 index 00000000..ef1520fc --- /dev/null +++ b/packages/cloudfront-security-headers/.nvmrc @@ -0,0 +1 @@ +20.7.0 \ No newline at end of file diff --git a/packages/cloudfront-security-headers/README.md b/packages/cloudfront-security-headers/README.md new file mode 100644 index 00000000..42a12717 --- /dev/null +++ b/packages/cloudfront-security-headers/README.md @@ -0,0 +1,8 @@ +# AWS CDK CloudFront Security Headers +This package contains a Lambda@Edge function for cloudfront to add security headers to the origin response of all requests. + +## Useful commands + + * `npm run build` compile typescript to js + * `npm run watch` watch for changes and compile + * `npm run test` perform the jest unit tests \ No newline at end of file diff --git a/packages/cloudfront-security-headers/lib/handlers/security-header.ts b/packages/cloudfront-security-headers/lib/handlers/security-header.ts new file mode 100644 index 00000000..009e6f56 --- /dev/null +++ b/packages/cloudfront-security-headers/lib/handlers/security-header.ts @@ -0,0 +1,25 @@ +import { CloudFrontResponse, CloudFrontResponseEvent } from "aws-lambda"; + +export const handler = async ( + event: CloudFrontResponseEvent +): Promise => { + const response = event.Records[0].cf.response; + const headers = response.headers; + + // Add in security headers + headers["strict-transport-security"] = [ + { + key: "Strict-Transport-Security", + value: "max-age=108000; includeSubdomains; preload", + }, + ]; + headers["content-security-policy"] = [ + { key: "Content-Security-Policy", value: __CONTENT_SECURITY_POLICY__ }, + ]; + headers["x-content-type-options"] = [ + { key: "X-Content-Type-Options", value: "nosniff" }, + ]; + headers["x-frame-options"] = [{ key: "X-Frame-Options", value: "DENY" }]; + + return response; +}; diff --git a/packages/cloudfront-security-headers/lib/index.ts b/packages/cloudfront-security-headers/lib/index.ts new file mode 100644 index 00000000..3df24046 --- /dev/null +++ b/packages/cloudfront-security-headers/lib/index.ts @@ -0,0 +1,60 @@ +import { AssetHashType, DockerImage } from "aws-cdk-lib"; +import { EdgeFunction } from "aws-cdk-lib/aws-cloudfront/lib/experimental"; +import { Code, IVersion, Runtime, Version } from "aws-cdk-lib/aws-lambda"; +import { Construct } from "constructs"; +import { join } from "path"; +import { Esbuild } from "@aligent/esbuild"; + +export interface SecurityHeaderFunctionProps { + contentSecurityPolicy?: Array; +} + +export class SecurityHeaderFunction extends Construct { + readonly edgeFunction: EdgeFunction; + + constructor( + scope: Construct, + id: string, + props?: SecurityHeaderFunctionProps + ) { + super(scope, id); + + const defineOptions: any = {}; + + if (props?.contentSecurityPolicy) { + defineOptions.__CONTENT_SECURITY_POLICY__ = JSON.stringify( + props.contentSecurityPolicy.join("; ") + ); + } + + const command = [ + "sh", + "-c", + 'echo "Docker build not supported. Please install esbuild."', + ]; + + this.edgeFunction = new EdgeFunction(this, `${id}-security-header-fn`, { + code: Code.fromAsset(join(__dirname, "handlers"), { + assetHashType: AssetHashType.OUTPUT, + bundling: { + command, + image: DockerImage.fromRegistry("busybox"), + local: new Esbuild({ + entryPoints: [join(__dirname, "handlers/security-header.ts")], + define: defineOptions, + }), + }, + }), + runtime: Runtime.NODEJS_18_X, + handler: "security-header.handler", + }); + } + + public getFunctionVersion(): IVersion { + return Version.fromVersionArn( + this, + "security-header-fn-version", + this.edgeFunction.currentVersion.edgeArn + ); + } +} diff --git a/packages/cloudfront-security-headers/package.json b/packages/cloudfront-security-headers/package.json new file mode 100644 index 00000000..31d24bc7 --- /dev/null +++ b/packages/cloudfront-security-headers/package.json @@ -0,0 +1,28 @@ +{ + "name": "@aligent/cdk-cloudfront-security-headers", + "version": "2.0.0", + "description": "A Cloudfront Lambda@Edge function for adding security headers.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "tsc && cd ./lib/handlers && npm ci", + "prepublish": "tsc && cd ./lib/handlers && npm ci", + "watch": "tsc -w", + "test": "jest" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + }, + "dependencies": { + "@types/aws-lambda": "^8.10.122", + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } +} diff --git a/packages/cloudfront-security-headers/test/cdk-cloudfront-security-headers.test.ts b/packages/cloudfront-security-headers/test/cdk-cloudfront-security-headers.test.ts new file mode 100644 index 00000000..f862c662 --- /dev/null +++ b/packages/cloudfront-security-headers/test/cdk-cloudfront-security-headers.test.ts @@ -0,0 +1,17 @@ +// import { expect as expectCDK, countResources } from "@aws-cdk/assert"; +// import * as cdk from "aws-cdk-lib"; +// import { SecurityHeaderFunction } from "../lib/index"; + +/* + * Example test + */ +// test("Lambda Function Created", () => { +// const app = new cdk.App(); +// const stack = new cdk.Stack(app, "TestStack", { +// env: { region: "us-east-1" }, +// }); +// // WHEN +// new SecurityHeaderFunction(stack, "MyTestConstruct"); +// // THEN +// expectCDK(stack).to(countResources("AWS::Lambda::Function", 1)); +// }); diff --git a/packages/cloudfront-security-headers/tsconfig.json b/packages/cloudfront-security-headers/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/cloudfront-security-headers/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/packages/static-hosting/lib/utils/esbuild.ts b/packages/esbuild/esbuild.ts similarity index 91% rename from packages/static-hosting/lib/utils/esbuild.ts rename to packages/esbuild/esbuild.ts index 3385918f..f372e7d8 100644 --- a/packages/static-hosting/lib/utils/esbuild.ts +++ b/packages/esbuild/esbuild.ts @@ -12,13 +12,13 @@ export class Esbuild implements ILocalBundling { logLevel: "info", sourcemap: false, bundle: true, - minify: false, + minify: true, platform: "node", // Do not minify identifiers, otherwise the exported `handler` function name gets minified failing to start // the lambda minifyIdentifiers: false, - minifyWhitespace: false, - minifySyntax: false, + minifyWhitespace: true, + minifySyntax: true, }); } diff --git a/packages/esbuild/index.ts b/packages/esbuild/index.ts new file mode 100644 index 00000000..d482acd3 --- /dev/null +++ b/packages/esbuild/index.ts @@ -0,0 +1,3 @@ +import { Esbuild } from "./esbuild"; + +export { Esbuild }; diff --git a/packages/esbuild/package.json b/packages/esbuild/package.json new file mode 100644 index 00000000..913393a4 --- /dev/null +++ b/packages/esbuild/package.json @@ -0,0 +1,26 @@ +{ + "name": "@aligent/esbuild", + "version": "2.0.0", + "description": "Esbuild implementation for CDK", + "main": "index.js", + "license": "GPL-3.0-only", + "scripts": { + "build": "tsc", + "prepublish": "tsc" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + }, + "dependencies": { + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "esbuild": "^0.17.0", + "source-map-support": "^0.5.21" + } +} diff --git a/packages/esbuild/tsconfig.json b/packages/esbuild/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/esbuild/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/packages/rabbitmq/index.ts b/packages/rabbitmq/index.ts index 835a9f0c..07ca6ec9 100644 --- a/packages/rabbitmq/index.ts +++ b/packages/rabbitmq/index.ts @@ -1,3 +1,3 @@ import { RabbitMQ } from "./lib/rabbitmq-construct"; -export { RabbitMQ }; \ No newline at end of file +export { RabbitMQ }; diff --git a/packages/static-hosting/.npmrc b/packages/static-hosting/.npmrc index eb1dc6a5..3b9bddfc 100644 --- a/packages/static-hosting/.npmrc +++ b/packages/static-hosting/.npmrc @@ -1 +1 @@ -7.13.0 +10.1.0 \ No newline at end of file diff --git a/packages/static-hosting/.nvmrc b/packages/static-hosting/.nvmrc index fd1bd70b..ef1520fc 100644 --- a/packages/static-hosting/.nvmrc +++ b/packages/static-hosting/.nvmrc @@ -1 +1 @@ -16.2.0 +20.7.0 \ No newline at end of file diff --git a/packages/static-hosting/lib/path-remap.ts b/packages/static-hosting/lib/path-remap.ts index b82a211f..bce4bea1 100644 --- a/packages/static-hosting/lib/path-remap.ts +++ b/packages/static-hosting/lib/path-remap.ts @@ -3,7 +3,7 @@ import { Code, IVersion, Runtime, Version } from "aws-cdk-lib/aws-lambda"; import { Construct } from "constructs"; import { join } from "path"; import * as cf from "aws-cdk-lib/aws-cloudfront"; -import { Esbuild } from "./utils/esbuild"; +import { Esbuild } from "@aligent/esbuild"; export interface RemapOptions { path: string; From 80cb9d5bd2ee33580e7510f7eecc1ea1c87e72be Mon Sep 17 00:00:00 2001 From: Gowri Date: Wed, 27 Sep 2023 13:28:01 +0930 Subject: [PATCH 03/10] DO-1535: upgrade geoip redirect construct --- package-lock.json | 23 ++++++++ packages/geoip-redirect/.gitignore | 58 +++++++++++++++++++ packages/geoip-redirect/.npmignore | 11 ++++ packages/geoip-redirect/README.md | 4 ++ packages/geoip-redirect/index.ts | 3 + .../geoip-redirect/lib/handlers/redirect.ts | 43 ++++++++++++++ .../geoip-redirect/lib/redirect-construct.ts | 57 ++++++++++++++++++ packages/geoip-redirect/package.json | 34 +++++++++++ packages/geoip-redirect/tsconfig.json | 3 + 9 files changed, 236 insertions(+) create mode 100644 packages/geoip-redirect/.gitignore create mode 100644 packages/geoip-redirect/.npmignore create mode 100644 packages/geoip-redirect/README.md create mode 100644 packages/geoip-redirect/index.ts create mode 100644 packages/geoip-redirect/lib/handlers/redirect.ts create mode 100644 packages/geoip-redirect/lib/redirect-construct.ts create mode 100644 packages/geoip-redirect/package.json create mode 100644 packages/geoip-redirect/tsconfig.json diff --git a/package-lock.json b/package-lock.json index b8d3bc82..d00d4897 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,10 @@ "resolved": "packages/cloudfront-security-headers", "link": true }, + "node_modules/@aligent/cdk-geoip-redirect": { + "resolved": "packages/geoip-redirect", + "link": true + }, "node_modules/@aligent/cdk-rabbitmq": { "resolved": "packages/rabbitmq", "link": true @@ -5855,6 +5859,25 @@ "typescript": "~5.2.2" } }, + "packages/geoip-redirect": { + "version": "0.1.0", + "license": "GPL-3.0-only", + "dependencies": { + "@types/aws-lambda": "^8.10.122", + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + } + }, "packages/rabbitmq": { "name": "@aligent/cdk-rabbitmq", "version": "2.0.0", diff --git a/packages/geoip-redirect/.gitignore b/packages/geoip-redirect/.gitignore new file mode 100644 index 00000000..8f77f768 --- /dev/null +++ b/packages/geoip-redirect/.gitignore @@ -0,0 +1,58 @@ +# These are some examples of commonly ignored file patterns. +# You should customize this list as applicable to your project. +# Learn more about .gitignore: +# https://www.atlassian.com/git/tutorials/saving-changes/gitignore + +# Node artifact files +node_modules/ +dist/ + +# Compiled Java class files +*.class + +# Compiled Python bytecode +*.py[cod] + +# Log files +*.log + +# Package files +*.jar + +# Maven +target/ +dist/ + +# JetBrains IDE +.idea/ + +# Unit test reports +TEST*.xml + +# Generated by MacOS +.DS_Store + +# Generated by Windows +Thumbs.db + +# Applications +*.app +*.exe +*.war + +# Large media files +*.mp4 +*.tiff +*.avi +*.flv +*.mov +*.wmv + +!jest.config.js + +# CDK asset staging directory +.cdk.staging +cdk.out + +*.d.ts +*.js diff --git a/packages/geoip-redirect/.npmignore b/packages/geoip-redirect/.npmignore new file mode 100644 index 00000000..bfd115ba --- /dev/null +++ b/packages/geoip-redirect/.npmignore @@ -0,0 +1,11 @@ +*.ts +!lib/handlers/*.ts +!*.d.ts +!*.js + +# CDK asset staging directory +.cdk.staging +cdk.out + +# Samples +sample/ diff --git a/packages/geoip-redirect/README.md b/packages/geoip-redirect/README.md new file mode 100644 index 00000000..40b83a8d --- /dev/null +++ b/packages/geoip-redirect/README.md @@ -0,0 +1,4 @@ +# Geo-IP Redirect +This library provides a construct which creates a Lambda@Edge functions to perform GeoIP redirects. + +These functions are intended to be added to an existing Cloudfront distribution diff --git a/packages/geoip-redirect/index.ts b/packages/geoip-redirect/index.ts new file mode 100644 index 00000000..807b044b --- /dev/null +++ b/packages/geoip-redirect/index.ts @@ -0,0 +1,3 @@ +import { RedirectFunction } from "./lib/redirect-construct"; + +export { RedirectFunction }; diff --git a/packages/geoip-redirect/lib/handlers/redirect.ts b/packages/geoip-redirect/lib/handlers/redirect.ts new file mode 100644 index 00000000..7a762f24 --- /dev/null +++ b/packages/geoip-redirect/lib/handlers/redirect.ts @@ -0,0 +1,43 @@ +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 => { + const 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/geoip-redirect/lib/redirect-construct.ts b/packages/geoip-redirect/lib/redirect-construct.ts new file mode 100644 index 00000000..0cf65aea --- /dev/null +++ b/packages/geoip-redirect/lib/redirect-construct.ts @@ -0,0 +1,57 @@ +import { AssetHashType, DockerImage } from "aws-cdk-lib"; +import { EdgeFunction } from "aws-cdk-lib/aws-cloudfront/lib/experimental"; +import { Code, IVersion, Runtime, Version } from "aws-cdk-lib/aws-lambda"; +import { Construct } from "constructs"; +import { join } from "path"; +import { Esbuild } from "@aligent/esbuild"; + +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; +} + +export class RedirectFunction extends Construct { + readonly edgeFunction: EdgeFunction; + + constructor(scope: Construct, id: string, options: RedirectFunctionOptions) { + super(scope, id); + + const command = [ + "sh", + "-c", + 'echo "Docker build not supported. Please install esbuild."', + ]; + + this.edgeFunction = new EdgeFunction(this, `${id}-redirect-fn`, { + code: Code.fromAsset(join(__dirname, "handlers"), { + assetHashType: AssetHashType.OUTPUT, + bundling: { + command, + image: DockerImage.fromRegistry("busybox"), + local: new Esbuild({ + 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, + }, + }), + }, + }), + runtime: Runtime.NODEJS_18_X, + handler: "redirect.handler", + }); + } + + public getFunctionVersion(): IVersion { + return Version.fromVersionArn( + this, + "redirect-fn-version", + this.edgeFunction.currentVersion.edgeArn + ); + } +} diff --git a/packages/geoip-redirect/package.json b/packages/geoip-redirect/package.json new file mode 100644 index 00000000..532c5543 --- /dev/null +++ b/packages/geoip-redirect/package.json @@ -0,0 +1,34 @@ +{ + "name": "@aligent/cdk-geoip-redirect", + "version": "0.1.0", + "description": "A Cloudfront Lambda@Edge stack for performing redirection based on CloudFront-Viewer-Country", + "main": "index.js", + "scripts": { + "build": "tsc && cd ./lib/handlers && npm ci", + "prepublish": "tsc && cd ./lib/handlers && npm ci" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/aligent/aws-cdk-prerender-proxy-stack.git" + }, + "license": "GPL-3.0-only", + "bugs": { + "url": "https://github.com/aligent/aws-cdk-prerender-proxy-stack/issues" + }, + "homepage": "https://github.com/aligent/aws-cdk-prerender-proxy-stack#readme", + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + }, + "dependencies": { + "@types/aws-lambda": "^8.10.122", + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } +} diff --git a/packages/geoip-redirect/tsconfig.json b/packages/geoip-redirect/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/geoip-redirect/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} From 023308b8b0d6c38a215975384cfb34b00f32114b Mon Sep 17 00:00:00 2001 From: Gowri Date: Wed, 27 Sep 2023 13:37:53 +0930 Subject: [PATCH 04/10] DO-1533: upgrade shared-vpc construct --- package-lock.json | 22 +++++ packages/shared-vpc/.gitignore | 8 ++ packages/shared-vpc/.npmignore | 11 +++ packages/shared-vpc/README.md | 36 +++++++ packages/shared-vpc/index.ts | 3 + packages/shared-vpc/lib/shared-vpc.ts | 134 ++++++++++++++++++++++++++ packages/shared-vpc/package.json | 24 +++++ packages/shared-vpc/tsconfig.json | 3 + 8 files changed, 241 insertions(+) create mode 100644 packages/shared-vpc/.gitignore create mode 100644 packages/shared-vpc/.npmignore create mode 100644 packages/shared-vpc/README.md create mode 100644 packages/shared-vpc/index.ts create mode 100644 packages/shared-vpc/lib/shared-vpc.ts create mode 100644 packages/shared-vpc/package.json create mode 100644 packages/shared-vpc/tsconfig.json diff --git a/package-lock.json b/package-lock.json index d00d4897..f53b5eab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,10 @@ "resolved": "packages/rabbitmq", "link": true }, + "node_modules/@aligent/cdk-shared-vpc": { + "resolved": "packages/shared-vpc", + "link": true + }, "node_modules/@aligent/cdk-static-hosting": { "resolved": "packages/static-hosting", "link": true @@ -5860,6 +5864,7 @@ } }, "packages/geoip-redirect": { + "name": "@aligent/cdk-geoip-redirect", "version": "0.1.0", "license": "GPL-3.0-only", "dependencies": { @@ -5897,6 +5902,23 @@ "typescript": "~5.2.2" } }, + "packages/shared-vpc": { + "version": "2.0.0", + "dependencies": { + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + } + }, "packages/static-hosting": { "name": "@aligent/cdk-static-hosting", "version": "2.0.0", diff --git a/packages/shared-vpc/.gitignore b/packages/shared-vpc/.gitignore new file mode 100644 index 00000000..f60797b6 --- /dev/null +++ b/packages/shared-vpc/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/packages/shared-vpc/.npmignore b/packages/shared-vpc/.npmignore new file mode 100644 index 00000000..1464bb5c --- /dev/null +++ b/packages/shared-vpc/.npmignore @@ -0,0 +1,11 @@ +*.ts +!lib/handlers/*.ts +!*.d.ts +!*.js + +# CDK asset staging directory +.cdk.staging +cdk.out + +# Samples +sample/ \ No newline at end of file diff --git a/packages/shared-vpc/README.md b/packages/shared-vpc/README.md new file mode 100644 index 00000000..5b2e8336 --- /dev/null +++ b/packages/shared-vpc/README.md @@ -0,0 +1,36 @@ +# AligentShared VPC + +## Overview + +This repository creates a stack that provides a stable elastic IP for micro-services. Micro-services are then deployed into this VPC rather than creating their own. A hosted zone is also created to allow for private DNS configuration. + +## Example + +The following can be used to provision a shared VPC. + +``` +import 'source-map-support/register'; +import * as cdk from '@aws-cdk/core'; +import { Construct, Stack, StackProps } from '@aws-cdk/core'; +import { SharedVpc, SharedVpcProps, Zone } from '@aligent/cdk-shared-vpc'; + +const hostedZones: Zone[] = [ + { type: "A", target: "10.6.0.12", record: "subdomain" } +] +const sharedVpcProps : SharedVpcProps = { + vpcName: 'my-vpc-name', + cidr: '10.0.0.0/16', + hostedZoneDomain: 'example.com', + hostedZoneRecords: hostedZones +}; + +class MyStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + new SharedVpc(this, 'my-shared-vpc', sharedVpcProps); + } +} + +const app = new cdk.App(); +new MyStack(app, 'my-stack'); +``` \ No newline at end of file diff --git a/packages/shared-vpc/index.ts b/packages/shared-vpc/index.ts new file mode 100644 index 00000000..52445ff4 --- /dev/null +++ b/packages/shared-vpc/index.ts @@ -0,0 +1,3 @@ +import { SharedVpc, SharedVpcProps } from "./lib/shared-vpc"; + +export { SharedVpc, SharedVpcProps }; diff --git a/packages/shared-vpc/lib/shared-vpc.ts b/packages/shared-vpc/lib/shared-vpc.ts new file mode 100644 index 00000000..ea0f7320 --- /dev/null +++ b/packages/shared-vpc/lib/shared-vpc.ts @@ -0,0 +1,134 @@ +import { CfnOutput } from "aws-cdk-lib"; +import { SubnetType, Vpc } from "aws-cdk-lib/aws-ec2"; +import { + ARecord, + CnameRecord, + PrivateHostedZone, + RecordTarget, +} from "aws-cdk-lib/aws-route53"; +import { Construct } from "constructs"; + +const DEFAULT_ZONE_RECORD_SUFFIX = "root"; + +export type Zone = { + type: string; + target: string; + record?: string; +}; + +export interface SharedVpcProps { + /** + * The name we should use to create the VPC and prefix it's resources with + */ + vpcName: string; + /** + * The optional CIDR address + */ + cidr?: string; + /** + * The optional domain to use for the hosted-zone + */ + hostedZoneDomain?: string; + /** + * Optional zone records + */ + hostedZoneRecords?: Zone[]; +} + +export class SharedVpc extends Construct { + public readonly vpc: Vpc; + public readonly privateHostedZone: PrivateHostedZone; + + constructor(scope: Construct, id: string, props: SharedVpcProps) { + super(scope, id); + const { vpcName, cidr, hostedZoneDomain, hostedZoneRecords } = props; + + this.vpc = new Vpc(this, vpcName, { + maxAzs: 2, + cidr: cidr || Vpc.DEFAULT_CIDR_RANGE, + enableDnsHostnames: true, + enableDnsSupport: true, + natGateways: 1, + subnetConfiguration: [ + { + cidrMask: 22, + name: `${vpcName}-subnet-private`, + subnetType: SubnetType.PRIVATE_WITH_NAT, + }, + { + cidrMask: 22, + name: `${vpcName}-subnet-public`, + subnetType: SubnetType.PUBLIC, + }, + ], + }); + + // Export the VPC ID + new CfnOutput(this, "vpc", { + value: this.vpc.vpcId, + exportName: `${vpcName}-vpc`, + }); + + // Export each subnet from this VPC + this.vpc.privateSubnets.forEach((subnet, index) => { + const id = index + 1; + new CfnOutput(this, `private-subnet-${id}`, { + value: subnet.subnetId, + exportName: `${vpcName}-private-subnet-${id}`, + }); + }); + this.vpc.publicSubnets.forEach((subnet, index) => { + const id = index + 1; + new CfnOutput(this, `public-subnet-${id}`, { + value: subnet.subnetId, + exportName: `${vpcName}-public-subnet-${id}`, + }); + }); + + // Generate DNS records for each hosted zone + if (hostedZoneDomain) { + this.privateHostedZone = new PrivateHostedZone( + this, + `${vpcName}-hosted-zone`, + { + zoneName: hostedZoneDomain, + vpc: this.vpc, + } + ); + + if (hostedZoneRecords?.length) { + for (const zone of hostedZoneRecords) { + const recordId = `${vpcName}-hosted-zone-record-${ + zone.record || DEFAULT_ZONE_RECORD_SUFFIX + }`; + switch (zone.type) { + case "A": { + new ARecord(this, recordId, { + zone: this.privateHostedZone, + target: RecordTarget.fromIpAddresses(zone.target), + }); + break; + } + case "CNAME": { + new CnameRecord(this, recordId, { + zone: this.privateHostedZone, + domainName: zone.target, + recordName: zone.record, + }); + break; + } + default: { + throw Error(`${zone.type} is not supported`); + } + } + } + } + + // Export the hosted zone + new CfnOutput(this, "hosted-zone", { + value: this.privateHostedZone.hostedZoneId, + exportName: `${vpcName}-hosted-zone`, + }); + } + } +} diff --git a/packages/shared-vpc/package.json b/packages/shared-vpc/package.json new file mode 100644 index 00000000..59c88514 --- /dev/null +++ b/packages/shared-vpc/package.json @@ -0,0 +1,24 @@ +{ + "name": "@aligent/cdk-shared-vpc", + "version": "2.0.0", + "main": "index.js", + "scripts": { + "build": "tsc", + "prepublish": "tsc" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + }, + "dependencies": { + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } + } + \ No newline at end of file diff --git a/packages/shared-vpc/tsconfig.json b/packages/shared-vpc/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/shared-vpc/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} From 08b25664a35bdd9bf246c119f42874a7e864b968 Mon Sep 17 00:00:00 2001 From: Gowri Date: Wed, 27 Sep 2023 13:47:30 +0930 Subject: [PATCH 05/10] DO-1532: upgrade waf construct to cdk 2 --- package-lock.json | 24 ++ packages/waf/.gitignore | 58 +++ packages/waf/.npmignore | 11 + packages/waf/.npmrc | 1 + packages/waf/.nvmrc | 1 + .../waf/CdkPipelineCrossAccountDeploy.jpeg | Bin 0 -> 50251 bytes packages/waf/README.md | 67 ++++ packages/waf/index.ts | 3 + packages/waf/lib/waf.ts | 372 ++++++++++++++++++ packages/waf/package.json | 31 ++ packages/waf/tsconfig.json | 3 + 11 files changed, 571 insertions(+) create mode 100644 packages/waf/.gitignore create mode 100644 packages/waf/.npmignore create mode 100644 packages/waf/.npmrc create mode 100644 packages/waf/.nvmrc create mode 100644 packages/waf/CdkPipelineCrossAccountDeploy.jpeg create mode 100644 packages/waf/README.md create mode 100644 packages/waf/index.ts create mode 100644 packages/waf/lib/waf.ts create mode 100644 packages/waf/package.json create mode 100644 packages/waf/tsconfig.json diff --git a/package-lock.json b/package-lock.json index f53b5eab..e7f40d92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,10 @@ "resolved": "packages/static-hosting", "link": true }, + "node_modules/@aligent/cdk-waf": { + "resolved": "packages/waf", + "link": true + }, "node_modules/@aligent/esbuild": { "resolved": "packages/esbuild", "link": true @@ -5903,6 +5907,7 @@ } }, "packages/shared-vpc": { + "name": "@aligent/cdk-shared-vpc", "version": "2.0.0", "dependencies": { "aws-cdk-lib": "2.97.0", @@ -5939,6 +5944,25 @@ "ts-node": "^10.9.1", "typescript": "~5.2.2" } + }, + "packages/waf": { + "name": "@aligent/cdk-waf", + "version": "2.0.0", + "license": "GPL-3.0-only", + "dependencies": { + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + } } } } diff --git a/packages/waf/.gitignore b/packages/waf/.gitignore new file mode 100644 index 00000000..8f77f768 --- /dev/null +++ b/packages/waf/.gitignore @@ -0,0 +1,58 @@ +# These are some examples of commonly ignored file patterns. +# You should customize this list as applicable to your project. +# Learn more about .gitignore: +# https://www.atlassian.com/git/tutorials/saving-changes/gitignore + +# Node artifact files +node_modules/ +dist/ + +# Compiled Java class files +*.class + +# Compiled Python bytecode +*.py[cod] + +# Log files +*.log + +# Package files +*.jar + +# Maven +target/ +dist/ + +# JetBrains IDE +.idea/ + +# Unit test reports +TEST*.xml + +# Generated by MacOS +.DS_Store + +# Generated by Windows +Thumbs.db + +# Applications +*.app +*.exe +*.war + +# Large media files +*.mp4 +*.tiff +*.avi +*.flv +*.mov +*.wmv + +!jest.config.js + +# CDK asset staging directory +.cdk.staging +cdk.out + +*.d.ts +*.js diff --git a/packages/waf/.npmignore b/packages/waf/.npmignore new file mode 100644 index 00000000..bfd115ba --- /dev/null +++ b/packages/waf/.npmignore @@ -0,0 +1,11 @@ +*.ts +!lib/handlers/*.ts +!*.d.ts +!*.js + +# CDK asset staging directory +.cdk.staging +cdk.out + +# Samples +sample/ diff --git a/packages/waf/.npmrc b/packages/waf/.npmrc new file mode 100644 index 00000000..3b9bddfc --- /dev/null +++ b/packages/waf/.npmrc @@ -0,0 +1 @@ +10.1.0 \ No newline at end of file diff --git a/packages/waf/.nvmrc b/packages/waf/.nvmrc new file mode 100644 index 00000000..ef1520fc --- /dev/null +++ b/packages/waf/.nvmrc @@ -0,0 +1 @@ +20.7.0 \ No newline at end of file diff --git a/packages/waf/CdkPipelineCrossAccountDeploy.jpeg b/packages/waf/CdkPipelineCrossAccountDeploy.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..30550aab07b0ed3be0cc5fd13b9656000eb1198d GIT binary patch literal 50251 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 literal 0 HcmV?d00001 diff --git a/packages/waf/README.md b/packages/waf/README.md new file mode 100644 index 00000000..83442075 --- /dev/null +++ b/packages/waf/README.md @@ -0,0 +1,67 @@ +# Aligent AWS WAF + +## Overview + +This repository defines a CDK construct for provisioning an AWS Web Application Firewall (WAF) stack. It can be imported and used within CDK application. +##Example +The following CDK snippet can be used to provision the an AWS WAF stack. + +``` +import 'source-map-support/register'; +const cdk = require('@aws-cdk/core'); +import { WebApplicationFirewall } from '@aligent/cdk-waf'; +import { Stack } from '@aws-cdk/core'; + + +import { Environment } from '@aws-cdk/core' +import { env } from 'node:process'; + +const preprodEnv: Environment = {account: '', region: ''}; + +const target = ''; +const appName = 'WAF'; + +const defaultAllowedIPv4s = [ + 'a.a.a.a/32', 'b.b.b.b/32', // Offices + 'c.c.c.c/32', 'd.d.d.d/32', // Payment Gateways +] + +const defaultAllowedIPv6s = [ + '1234:abcd:5678:ef01::/56', // Offices + '1234:ef01:5678:abcd::/56', // Security Scanner +] + +export const preProductionWafStackProps = { +env: preprodEnv, + activate: true, // Update this line with either true or false, defining Block mode or Count-only mode, respectively. + allowedIPs: defaultAllowedIPs.concat([ + 'y.y.y.y/32' // AWS NAT GW of preprod vpc + // environment-specific comma-separated allow-list comes here + ]), + allowedUserAgents: [], // Allowed User-Agent list that would have been blocked by AWS BadBot rule. Case-sensitive. Optional. + excludedAwsRules: [], // The rule to exclude (override) from AWS-managed RuleSet. Optional. + associatedLoadBalancerArn: '', + wafName: +} + +class WAFStack extends Stack { + constructor(scope: Construct, id: string, props: preprodEnv) { + super(scope, id, props); + + new WebApplicationFirewall(scope, 'waf-stack', prod); + } +} + +new WAFStack(scope, envName, preProductionWafStackProps); +``` + +## Monitor and activate +By default, WebACL this stack creates will work in COUNT mode to begin with.After a certain period of monitoring under real traffic and load, apply necessary changes, e.g. IP allow_list or rate limit, to avoid service interruptions before switching to BLOCK mode. + +## Local development +[NPM link](https://docs.npmjs.com/cli/v7/commands/npm-link) can be used to develop the module locally. +1. Pull this repository locally +2. `cd` into this repository +3. run `npm link` +4. `cd` into the downstream repo (target project, etc) and run `npm link '@aligent/cdk-waf'` +The downstream repository should now include a symlink to this module. Allowing local changes to be tested before pushing. You may want to update the version notation of the package in the downstream repository's `package.json`. \ No newline at end of file diff --git a/packages/waf/index.ts b/packages/waf/index.ts new file mode 100644 index 00000000..a28fd36e --- /dev/null +++ b/packages/waf/index.ts @@ -0,0 +1,3 @@ +import { WebApplicationFirewall, WebApplicationFirewallProps } from "./lib/waf"; + +export { WebApplicationFirewall, WebApplicationFirewallProps }; diff --git a/packages/waf/lib/waf.ts b/packages/waf/lib/waf.ts new file mode 100644 index 00000000..c4d92bf1 --- /dev/null +++ b/packages/waf/lib/waf.ts @@ -0,0 +1,372 @@ +import { aws_wafv2 } from "aws-cdk-lib"; +import { Construct } from "constructs"; + +export const REGIONAL = "REGIONAL"; +export type REGIONAL = typeof REGIONAL; + +export const CLOUDFRONT = "CLOUDFRONT"; +export type CLOUDFRONT = typeof CLOUDFRONT; + +export interface WebApplicationFirewallProps { + /** + * Whether this WAF is global or regional + */ + scope?: REGIONAL | CLOUDFRONT; + + /** + * true for blocking mode, false for Count-only mode + */ + activate?: boolean; + + /** + * List of Allowed IPv4 addresses, if neither allowedIPs nor allowedIPsIPv6 are set allow_xff_ip_rule and allow_src_ip_rule rules + * are not added + */ + allowedIPs?: string[]; + + /** + * List of Allowed IPv6 addresses, if neither allowedIPs nor allowedIPsIPv6 are set allow_xff_ip_rule and allow_src_ip_rule rules + * are not added + */ + allowedIPv6s?: string[]; + + /** + * Explicit paths to allow through the waf + */ + allowedPaths?: string[]; + + /** + * Default Rate limit count, if not set the rate limit rule will not be added + */ + rateLimit?: number; + + /** + * Explicit allow of user agents, if not set rule will not be added + */ + allowedUserAgents?: string[]; + + /** + * A list of AWS Rules to ignore + */ + excludedAwsRules?: string[]; + + /** + * A list of ARNs to associate with the WAF + */ + associations?: string[]; + + /** + * Name of the WAF + */ + wafName: string; + + /** + * Whether to block by default + */ + blockByDefault?: boolean; +} + +export class WebApplicationFirewall extends Construct { + readonly web_acl: aws_wafv2.CfnWebACL; + + constructor( + scope: Construct, + id: string, + props: WebApplicationFirewallProps + ) { + super(scope, id); + + const finalRules: aws_wafv2.CfnWebACL.RuleProperty[] = []; + const wafScope = props.scope ?? REGIONAL; + + if (props.allowedIPs) { + // IPv4 Allowlist + const allowed_ips = new aws_wafv2.CfnIPSet(this, "IPSet-IPv4", { + addresses: props.allowedIPs, + ipAddressVersion: "IPV4", + scope: wafScope, + description: props.wafName, + }); + + finalRules.push({ + name: "allow_xff_ip_rule", + priority: 2, + statement: { + ipSetReferenceStatement: { + arn: allowed_ips.attrArn, + ipSetForwardedIpConfig: { + fallbackBehavior: "NO_MATCH", + headerName: "X-Forwarded-For", + position: "ANY", + }, + }, + }, + action: { allow: {} }, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: "AllowXFFIPRule", + sampledRequestsEnabled: true, + }, + }); + + finalRules.push({ + name: "allow_src_ip_rule", + priority: 3, + statement: { + ipSetReferenceStatement: { + arn: allowed_ips.attrArn, + }, + }, + action: { allow: {} }, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: "allow_src_ip_rule", + sampledRequestsEnabled: true, + }, + }); + } + + if (props.allowedIPv6s) { + // IPv6 Allowlist + const allowed_ips = new aws_wafv2.CfnIPSet(this, "IPSet-IPv6", { + addresses: props.allowedIPv6s, + ipAddressVersion: "IPV6", + scope: wafScope, + description: props.wafName, + }); + + finalRules.push({ + name: "allow_xff_ip_rule_ipv6", + priority: 4, + statement: { + ipSetReferenceStatement: { + arn: allowed_ips.attrArn, + ipSetForwardedIpConfig: { + fallbackBehavior: "NO_MATCH", + headerName: "X-Forwarded-For", + position: "ANY", + }, + }, + }, + action: { allow: {} }, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: "AllowXFFIPRule", + sampledRequestsEnabled: true, + }, + }); + + finalRules.push({ + name: "allow_src_ip_rule_ipv6", + priority: 5, + statement: { + ipSetReferenceStatement: { + arn: allowed_ips.attrArn, + }, + }, + action: { allow: {} }, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: "allow_src_ip_rule", + sampledRequestsEnabled: true, + }, + }); + } + + // Implement AWSManagedRulesKnownBadInputsRuleSet + finalRules.push({ + name: "bad_actors_rule", + priority: 0, + overrideAction: { none: {} }, + statement: { + managedRuleGroupStatement: { + name: "AWSManagedRulesKnownBadInputsRuleSet", + vendorName: "AWS", + excludedRules: [ + { name: "Host_localhost_HEADER" }, + { name: "PROPFIND_METHOD" }, + { name: "ExploitablePaths_URIPATH" }, + ], + }, + }, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: "bad_actors_rule", + sampledRequestsEnabled: true, + }, + }); + + if (props.allowedPaths) { + // Path Allowlist + const allowed_paths = new aws_wafv2.CfnRegexPatternSet(this, "PathSet", { + regularExpressionList: props.allowedPaths, + scope: wafScope, + }); + + finalRules.push({ + name: "allow_path_rule", + priority: 1, + statement: { + regexPatternSetReferenceStatement: { + arn: allowed_paths.attrArn, + fieldToMatch: { + uriPath: {}, + }, + textTransformations: [ + { + priority: 0, + type: "NONE", + }, + ], + }, + }, + action: { allow: {} }, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: "AllowPathRule", + sampledRequestsEnabled: true, + }, + }); + } + + // UserAgent Allowlist - only when the parameter is present + if (props.allowedUserAgents) { + const allowed_user_agent = new aws_wafv2.CfnRegexPatternSet( + this, + "UserAgent", + { + regularExpressionList: props.allowedUserAgents, + scope: wafScope, + } + ); + + finalRules.push({ + name: "allow_user_agent_rule", + priority: 6, + statement: { + regexPatternSetReferenceStatement: { + arn: allowed_user_agent.attrArn, + fieldToMatch: { singleHeader: { Name: "User-Agent" } }, + textTransformations: [ + { + priority: 0, + type: "NONE", + }, + ], + }, + }, + action: { allow: {} }, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: "allow_user_agent_rule", + sampledRequestsEnabled: true, + }, + }); + } + + // Activate the rules or not + let overrideAction: object = { count: {} }; + let action: object = { count: {} }; + if (props.activate == true) { + overrideAction = { none: {} }; + action = { block: {} }; + } + + // Exclude specific rules from AWS Core Rule Group - only when the parameter is present + const excludedAwsRules: aws_wafv2.CfnWebACL.ExcludedRuleProperty[] = []; + if (props.excludedAwsRules) { + props.excludedAwsRules.forEach(ruleName => { + excludedAwsRules.push({ + name: ruleName, + }); + }); + } + + // Implement AWSManagedRulesCommonRuleSet + finalRules.push({ + name: "common_rule_set", + priority: 10, + statement: { + managedRuleGroupStatement: { + name: "AWSManagedRulesCommonRuleSet", + vendorName: "AWS", + excludedRules: excludedAwsRules, + }, + }, + overrideAction: overrideAction, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: "common_rule_set", + sampledRequestsEnabled: true, + }, + }); + + // Implement AWSManagedRulesPHPRuleSet + finalRules.push({ + name: "php_rule_set", + priority: 11, + statement: { + managedRuleGroupStatement: { + name: "AWSManagedRulesPHPRuleSet", + vendorName: "AWS", + }, + }, + overrideAction: overrideAction, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: "php_rule_set", + sampledRequestsEnabled: true, + }, + }); + + // Implement rate-based limit + if (props.rateLimit) { + finalRules.push({ + name: "rate_limit_rule", + priority: 20, + statement: { + rateBasedStatement: { + aggregateKeyType: "FORWARDED_IP", + forwardedIpConfig: { + fallbackBehavior: "MATCH", + headerName: "X-Forwarded-For", + }, + limit: props.rateLimit, + }, + }, + action: action, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: "rate_limit_rule", + sampledRequestsEnabled: true, + }, + }); + } + + const defaultAction = props.blockByDefault ? { block: {} } : { allow: {} }; + + this.web_acl = new aws_wafv2.CfnWebACL(this, "WebAcl", { + name: props.wafName, + defaultAction: defaultAction, + scope: wafScope, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: "WebAcl", + sampledRequestsEnabled: true, + }, + rules: finalRules, + }); + + // If any resources associations have been passed loop through them and add an association with WebACL + if (props.associations) { + props.associations.forEach((association, index) => { + new aws_wafv2.CfnWebACLAssociation(this, "WebACLAssociation" + index, { + // If the application stack has had the ARN exported, importValue could be used as below: + // resourceArn: cdk.Fn.importValue("WAFTestALB"), + resourceArn: association, + webAclArn: this.web_acl.attrArn, + }); + }); + } + } +} diff --git a/packages/waf/package.json b/packages/waf/package.json new file mode 100644 index 00000000..60f54db3 --- /dev/null +++ b/packages/waf/package.json @@ -0,0 +1,31 @@ +{ + "name": "@aligent/cdk-waf", + "version": "2.0.0", + "main": "index.js", + "license": "GPL-3.0-only", + "homepage": "https://github.com/aligent/aws-cdk-waf-stack#readme", + "repository": { + "type": "git", + "url": "https://github.com/aligent/aws-cdk-waf-stack" + }, + "types": "index.d.ts", + "scripts": { + "build": "tsc", + "prepublish": "tsc" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + }, + "dependencies": { + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } + } + \ No newline at end of file diff --git a/packages/waf/tsconfig.json b/packages/waf/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/waf/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} From bdb63fc7d794ac44dcae2525e160c489ad34ccb1 Mon Sep 17 00:00:00 2001 From: Gowri Date: Wed, 27 Sep 2023 14:09:56 +0930 Subject: [PATCH 06/10] DO-1541: upgrade prerender proxy construct --- package-lock.json | 116 ++++++++++++++++++ packages/prerender-proxy/README.md | 26 ++++ packages/prerender-proxy/index.ts | 29 +++++ .../lib/error-response-construct.ts | 54 ++++++++ .../lib/handlers/cache-control.ts | 20 +++ .../lib/handlers/error-response.ts | 61 +++++++++ .../lib/handlers/prerender-check.ts | 33 +++++ .../prerender-proxy/lib/handlers/prerender.ts | 48 ++++++++ .../prerender-cf-cache-control-construct.ts | 58 +++++++++ .../lib/prerender-check-construct.ts | 43 +++++++ .../lib/prerender-construct.ts | 55 +++++++++ .../lib/prerender-lambda-construct.ts | 48 ++++++++ packages/prerender-proxy/package.json | 36 ++++++ packages/prerender-proxy/tsconfig.json | 3 + 14 files changed, 630 insertions(+) create mode 100644 packages/prerender-proxy/README.md create mode 100644 packages/prerender-proxy/index.ts create mode 100644 packages/prerender-proxy/lib/error-response-construct.ts create mode 100644 packages/prerender-proxy/lib/handlers/cache-control.ts create mode 100644 packages/prerender-proxy/lib/handlers/error-response.ts create mode 100644 packages/prerender-proxy/lib/handlers/prerender-check.ts create mode 100644 packages/prerender-proxy/lib/handlers/prerender.ts create mode 100644 packages/prerender-proxy/lib/prerender-cf-cache-control-construct.ts create mode 100644 packages/prerender-proxy/lib/prerender-check-construct.ts create mode 100644 packages/prerender-proxy/lib/prerender-construct.ts create mode 100644 packages/prerender-proxy/lib/prerender-lambda-construct.ts create mode 100644 packages/prerender-proxy/package.json create mode 100644 packages/prerender-proxy/tsconfig.json diff --git a/package-lock.json b/package-lock.json index e7f40d92..f8379a32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,10 @@ "resolved": "packages/geoip-redirect", "link": true }, + "node_modules/@aligent/cdk-prerender-proxy": { + "resolved": "packages/prerender-proxy", + "link": true + }, "node_modules/@aligent/cdk-rabbitmq": { "resolved": "packages/rabbitmq", "link": true @@ -2159,6 +2163,11 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/aws-cdk": { "version": "2.97.0", "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.97.0.tgz", @@ -2514,6 +2523,16 @@ "node": ">= 6" } }, + "node_modules/axios": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2848,6 +2867,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2955,6 +2985,14 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3489,6 +3527,38 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4671,6 +4741,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -5061,6 +5150,11 @@ "node": ">= 6" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -5887,6 +5981,28 @@ "typescript": "~5.2.2" } }, + "packages/prerender-proxy": { + "name": "@aligent/cdk-prerender-proxy", + "version": "2.0.0", + "license": "GPL-3.0-only", + "dependencies": { + "@types/aws-lambda": "^8.10.122", + "aws-cdk-lib": "2.97.0", + "axios": "^1.5.1", + "constructs": "^10.0.0", + "esbuild": "^0.17.0", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + } + }, "packages/rabbitmq": { "name": "@aligent/cdk-rabbitmq", "version": "2.0.0", diff --git a/packages/prerender-proxy/README.md b/packages/prerender-proxy/README.md new file mode 100644 index 00000000..7f8503e2 --- /dev/null +++ b/packages/prerender-proxy/README.md @@ -0,0 +1,26 @@ +# Prerender Proxy + +This library provides two function constructs and a construct that creates two Lambda@Edge functions to use prerender.io as a Cloudfront Origin for site indexers (Google, Bing, etc). + +The `prerender-check` is a `viewer-request` function that will check if a requester is from a indexer and if it is adds a header so that the second function `prerender` (`origin-request`) will alter the origin to prerender. + +The `prerender` will function also make a HEAD request to a nominated backend to detect 301 and 302 redirects and if so forward them on to the frontend. This ensures that your SEO rankings are not penalized by having multiple pages at the same URL. + +These functions are intended to be added to an existing Cloudfront + +## Cache Control + +The intention of `cache-control` function is to avoid/control CloudFront Caches for `prerender` bot. +This function to be associated with CloudFront's `origin response` + +## Props + +`redirectBackendOrigin`: The backend origin to make the HEAD request to +`redirectFrontendHost`: This hostname is used to replace the backend host for any redirects that contain the backend host +`prerenderToken`: Your prerender.io authentication token +`prerenderUrl`: The URL of your Prerender service (optional: defaults to prerender.io) +`pathPrefix`: A prefix path (optional) +`cacheControlProps.cacheKey`: An optional parameter, which is to set `PRERENDER_CACHE_KEY` to be used in *[prerender CloudFront cache control function]* +`cacheControlProps.maxAge`: An optional parameter, which is to set `PRERENDER_CACHE_MAX_AGE` to be used in *[prerender CloudFront cache control function]* + +[prerender CloudFront cache control function]:lib/handlers/cache-control.ts \ No newline at end of file diff --git a/packages/prerender-proxy/index.ts b/packages/prerender-proxy/index.ts new file mode 100644 index 00000000..fc41368b --- /dev/null +++ b/packages/prerender-proxy/index.ts @@ -0,0 +1,29 @@ +import { + PrerenderLambda, + PrerenderLambdaProps, +} from "./lib/prerender-lambda-construct"; +import { + PrerenderFunction, + PrerenderFunctionOptions, +} from "./lib/prerender-construct"; +import { PrerenderCheckFunction } from "./lib/prerender-check-construct"; +import { + ErrorResponseFunction, + ErrorResponseFunctionOptions, +} from "./lib/error-response-construct"; +import { + CloudFrontCacheControl, + CloudFrontCacheControlOptions, +} from "./lib/prerender-cf-cache-control-construct"; + +export { + PrerenderLambda, + PrerenderFunction, + PrerenderCheckFunction, + ErrorResponseFunction, + CloudFrontCacheControl, + CloudFrontCacheControlOptions, + ErrorResponseFunctionOptions, + PrerenderFunctionOptions, + PrerenderLambdaProps, +}; diff --git a/packages/prerender-proxy/lib/error-response-construct.ts b/packages/prerender-proxy/lib/error-response-construct.ts new file mode 100644 index 00000000..c84bb68c --- /dev/null +++ b/packages/prerender-proxy/lib/error-response-construct.ts @@ -0,0 +1,54 @@ +import { AssetHashType, DockerImage } from "aws-cdk-lib"; +import { EdgeFunction } from "aws-cdk-lib/aws-cloudfront/lib/experimental"; +import { Code, IVersion, Runtime, Version } from "aws-cdk-lib/aws-lambda"; +import { Construct } from "constructs"; +import { join } from "path"; +import { Esbuild } from "@aligent/esbuild"; + +export interface ErrorResponseFunctionOptions { + pathPrefix?: string; +} + +export class ErrorResponseFunction extends Construct { + readonly edgeFunction: EdgeFunction; + + constructor( + scope: Construct, + id: string, + options: ErrorResponseFunctionOptions + ) { + super(scope, id); + + const command = [ + "sh", + "-c", + 'echo "Docker build not supported. Please install esbuild."', + ]; + + this.edgeFunction = new EdgeFunction(this, `${id}-error-response-fn`, { + code: Code.fromAsset(join(__dirname, "handlers"), { + assetHashType: AssetHashType.OUTPUT, + bundling: { + command, + image: DockerImage.fromRegistry("busybox"), + local: new Esbuild({ + entryPoints: [join(__dirname, "handlers/error-response.ts")], + define: { + "process.env.PATH_PREFIX": options.pathPrefix ?? "", + }, + }), + }, + }), + runtime: Runtime.NODEJS_18_X, + handler: "error-response.handler", + }); + } + + public getFunctionVersion(): IVersion { + return Version.fromVersionArn( + this, + "error-response-fn-version", + this.edgeFunction.currentVersion.edgeArn + ); + } +} diff --git a/packages/prerender-proxy/lib/handlers/cache-control.ts b/packages/prerender-proxy/lib/handlers/cache-control.ts new file mode 100644 index 00000000..8543a6ef --- /dev/null +++ b/packages/prerender-proxy/lib/handlers/cache-control.ts @@ -0,0 +1,20 @@ +import "source-map-support/register"; +import { CloudFrontResponseEvent, CloudFrontResponse } from "aws-lambda"; + +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"; + const 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/prerender-proxy/lib/handlers/error-response.ts b/packages/prerender-proxy/lib/handlers/error-response.ts new file mode 100644 index 00000000..7c95b77a --- /dev/null +++ b/packages/prerender-proxy/lib/handlers/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 => { + const response = event.Records[0].cf.response; + const 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/prerender-proxy/lib/handlers/prerender-check.ts b/packages/prerender-proxy/lib/handlers/prerender-check.ts new file mode 100644 index 00000000..879c1d25 --- /dev/null +++ b/packages/prerender-proxy/lib/handlers/prerender-check.ts @@ -0,0 +1,33 @@ +import "source-map-support/register"; +import { CloudFrontRequest, CloudFrontRequestEvent } from "aws-lambda"; + +const IS_BOT = + /googlebot|Google-InspectionTool|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 => { + const 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_BOT.test(request.headers["user-agent"][0].value)) { + if (!IS_FILE.test(request.uri) && !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/prerender-proxy/lib/handlers/prerender.ts b/packages/prerender-proxy/lib/handlers/prerender.ts new file mode 100644 index 00000000..675fb048 --- /dev/null +++ b/packages/prerender-proxy/lib/handlers/prerender.ts @@ -0,0 +1,48 @@ +import { CloudFrontRequest, CloudFrontRequestEvent, CloudFrontResponse } from "aws-lambda"; +import "source-map-support/register"; + + +const PRERENDER_TOKEN = process.env.PRERENDER_TOKEN; +const PATH_PREFIX = process.env.PATH_PREFIX; +const PRERENDER_URL = process.env.PRERENDER_URL; + +export const handler = async ( + event: CloudFrontRequestEvent +): Promise => { + const request = event.Records[0].cf.request; + + // viewer-request function will determine whether we prerender or not + // if we should we add prerender as our custom origin + if (request.headers["x-request-prerender"]) { + // Cloudfront will alter the request for / to /index.html + // since it is defined as the default root object + // we do not want to do this when prerendering the homepage + if (request.uri === `${PATH_PREFIX}/index.html`) { + request.uri = `${PATH_PREFIX}/`; + } + + request.origin = { + custom: { + domainName: PRERENDER_URL, + port: 443, + protocol: "https", + readTimeout: 60, + keepaliveTimeout: 5, + sslProtocols: ["TLSv1", "TLSv1.1", "TLSv1.2"], + path: "/https%3A%2F%2F" + request.headers["x-prerender-host"][0].value, + customHeaders: { + "x-prerender-token": [ + { + key: "x-prerender-token", + value: PRERENDER_TOKEN, + }, + ], + }, + }, + }; + } else { + request.uri = `${PATH_PREFIX}/index.html`; + } + + return request; +}; diff --git a/packages/prerender-proxy/lib/prerender-cf-cache-control-construct.ts b/packages/prerender-proxy/lib/prerender-cf-cache-control-construct.ts new file mode 100644 index 00000000..fbc52713 --- /dev/null +++ b/packages/prerender-proxy/lib/prerender-cf-cache-control-construct.ts @@ -0,0 +1,58 @@ +import { AssetHashType, DockerImage } from "aws-cdk-lib"; +import { EdgeFunction } from "aws-cdk-lib/aws-cloudfront/lib/experimental"; +import { Code, IVersion, Runtime, Version } from "aws-cdk-lib/aws-lambda"; +import { Construct } from "constructs"; +import { join } from "path"; +import { Esbuild } from "@aligent/esbuild"; + +export interface CloudFrontCacheControlOptions { + cacheKey?: string; + maxAge?: number; +} + +export class CloudFrontCacheControl extends Construct { + readonly edgeFunction: EdgeFunction; + + constructor( + scope: Construct, + id: string, + options?: CloudFrontCacheControlOptions + ) { + super(scope, id); + + const command = [ + "sh", + "-c", + 'echo "Docker build not supported. Please install esbuild."', + ]; + + this.edgeFunction = new EdgeFunction(this, `${id}-cache-control-fn`, { + code: Code.fromAsset(join(__dirname, "handlers"), { + assetHashType: AssetHashType.OUTPUT, + bundling: { + command, + image: DockerImage.fromRegistry("busybox"), + local: new Esbuild({ + entryPoints: [join(__dirname, "handlers/cache-control.ts")], + define: { + "process.env.PRERENDER_CACHE_KEY": + options?.cacheKey ?? "x-prerender-requestid", + "process.env.PRERENDER_CACHE_MAX_AGE": + String(options?.maxAge) ?? "0", + }, + }), + }, + }), + runtime: Runtime.NODEJS_18_X, + handler: "cache-control.handler", + }); + } + + public getFunctionVersion(): IVersion { + return Version.fromVersionArn( + this, + "cache-control-fn-version", + this.edgeFunction.currentVersion.edgeArn + ); + } +} diff --git a/packages/prerender-proxy/lib/prerender-check-construct.ts b/packages/prerender-proxy/lib/prerender-check-construct.ts new file mode 100644 index 00000000..9d127088 --- /dev/null +++ b/packages/prerender-proxy/lib/prerender-check-construct.ts @@ -0,0 +1,43 @@ +import { AssetHashType, DockerImage } from "aws-cdk-lib"; +import { EdgeFunction } from "aws-cdk-lib/aws-cloudfront/lib/experimental"; +import { Code, IVersion, Runtime, Version } from "aws-cdk-lib/aws-lambda"; +import { Construct } from "constructs"; +import { join } from "path"; +import { Esbuild } from "@aligent/esbuild"; + +export class PrerenderCheckFunction extends Construct { + readonly edgeFunction: EdgeFunction; + + constructor(scope: Construct, id: string) { + super(scope, id); + + const command = [ + "sh", + "-c", + 'echo "Docker build not supported. Please install esbuild."', + ]; + + this.edgeFunction = new EdgeFunction(this, `${id}-prerender-check-fn`, { + code: Code.fromAsset(join(__dirname, "handlers"), { + assetHashType: AssetHashType.OUTPUT, + bundling: { + command, + image: DockerImage.fromRegistry("busybox"), + local: new Esbuild({ + entryPoints: [join(__dirname, "handlers/prerender-check.ts")], + }), + }, + }), + runtime: Runtime.NODEJS_18_X, + handler: "prerender-check.handler", + }); + } + + public getFunctionVersion(): IVersion { + return Version.fromVersionArn( + this, + "prerender-check-fn-version", + this.edgeFunction.currentVersion.edgeArn + ); + } +} diff --git a/packages/prerender-proxy/lib/prerender-construct.ts b/packages/prerender-proxy/lib/prerender-construct.ts new file mode 100644 index 00000000..2707384a --- /dev/null +++ b/packages/prerender-proxy/lib/prerender-construct.ts @@ -0,0 +1,55 @@ +import { AssetHashType, DockerImage } from "aws-cdk-lib"; +import { EdgeFunction } from "aws-cdk-lib/aws-cloudfront/lib/experimental"; +import { Code, IVersion, Runtime, Version } from "aws-cdk-lib/aws-lambda"; +import { Construct } from "constructs"; +import { join } from "path"; +import { Esbuild } from "@aligent/esbuild"; + +export interface PrerenderFunctionOptions { + prerenderToken: string; + prerenderUrl?: string; + pathPrefix?: string; +} + +export class PrerenderFunction extends Construct { + readonly edgeFunction: EdgeFunction; + + constructor(scope: Construct, id: string, options: PrerenderFunctionOptions) { + super(scope, id); + + const command = [ + "sh", + "-c", + 'echo "Docker build not supported. Please install esbuild."', + ]; + + this.edgeFunction = new EdgeFunction(this, `${id}-prerender-fn`, { + code: Code.fromAsset(join(__dirname, "handlers"), { + assetHashType: AssetHashType.OUTPUT, + bundling: { + command, + image: DockerImage.fromRegistry("busybox"), + local: new Esbuild({ + entryPoints: [join(__dirname, "handlers/prerender.ts")], + define: { + "process.env.PRERENDER_TOKEN": options.prerenderToken, + "process.env.PATH_PREFIX": options.pathPrefix ?? "", + "process.env.PRERENDER_URL": + options.prerenderUrl ?? "service.prerender.io", + }, + }), + }, + }), + runtime: Runtime.NODEJS_18_X, + handler: "prerender.handler", + }); + } + + public getFunctionVersion(): IVersion { + return Version.fromVersionArn( + this, + "prerender-fn-version", + this.edgeFunction.currentVersion.edgeArn + ); + } +} diff --git a/packages/prerender-proxy/lib/prerender-lambda-construct.ts b/packages/prerender-proxy/lib/prerender-lambda-construct.ts new file mode 100644 index 00000000..4f0959fe --- /dev/null +++ b/packages/prerender-proxy/lib/prerender-lambda-construct.ts @@ -0,0 +1,48 @@ +import { Construct } from "constructs"; +import { + CloudFrontCacheControl, + CloudFrontCacheControlOptions, +} from "./prerender-cf-cache-control-construct"; +import { PrerenderCheckFunction } from "./prerender-check-construct"; +import { PrerenderFunction } from "./prerender-construct"; +import { ErrorResponseFunction } from "./error-response-construct"; + +export interface PrerenderLambdaProps { + prerenderToken: string; + exclusionExpression?: string; + cacheControlProps?: CloudFrontCacheControlOptions; +} + +export class PrerenderLambda extends Construct { + readonly prerenderCheckFunction: PrerenderCheckFunction; + readonly prerenderFunction: PrerenderFunction; + readonly errorResponseFunction: ErrorResponseFunction; + readonly cacheControlFunction: CloudFrontCacheControl; + + constructor(scope: Construct, id: string, props: PrerenderLambdaProps) { + super(scope, id); + + this.prerenderCheckFunction = new PrerenderCheckFunction( + this, + "PrerenderViewerRequest" + ); + + this.prerenderFunction = new PrerenderFunction( + this, + "PrerenderOriginRequest", + props + ); + + this.errorResponseFunction = new ErrorResponseFunction( + this, + "ErrorResponse", + {} + ); + + this.cacheControlFunction = new CloudFrontCacheControl( + this, + "PrerenderCloudFrontCacheControl", + props.cacheControlProps + ); + } +} diff --git a/packages/prerender-proxy/package.json b/packages/prerender-proxy/package.json new file mode 100644 index 00000000..0d5698cd --- /dev/null +++ b/packages/prerender-proxy/package.json @@ -0,0 +1,36 @@ +{ + "name": "@aligent/cdk-prerender-proxy", + "version": "2.0.0", + "description": "Cloudfront Lambda@Edge constructs for integrating with prerender.io", + "main": "index.js", + "scripts": { + "build": "tsc && cd ./lib/handlers && npm ci", + "prepublish": "tsc && cd ./lib/handlers && npm ci" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/aligent/aws-cdk-prerender-proxy-stack.git" + }, + "license": "GPL-3.0-only", + "bugs": { + "url": "https://github.com/aligent/aws-cdk-prerender-proxy-stack/issues" + }, + "homepage": "https://github.com/aligent/aws-cdk-prerender-proxy-stack#readme", + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + }, + "dependencies": { + "@types/aws-lambda": "^8.10.122", + "aws-cdk-lib": "2.97.0", + "axios": "^1.5.1", + "constructs": "^10.0.0", + "esbuild": "^0.17.0", + "source-map-support": "^0.5.21" + } +} diff --git a/packages/prerender-proxy/tsconfig.json b/packages/prerender-proxy/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/prerender-proxy/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} From 1607818636e11a03014fc357f53f347eee398299 Mon Sep 17 00:00:00 2001 From: Gowri Date: Wed, 27 Sep 2023 14:18:36 +0930 Subject: [PATCH 07/10] DO-1542:upgrade lambda handlers package --- .nvmrc | 2 +- package-lock.json | 13 +++ packages/lambda-at-edge-handlers/index.ts | 13 +++ .../lib/cache-control.ts | 23 +++++ .../lib/error-response.ts | 62 ++++++++++++++ .../lib/prerender-check.ts | 35 ++++++++ .../lambda-at-edge-handlers/lib/prerender.ts | 55 ++++++++++++ .../lambda-at-edge-handlers/lib/redirect.ts | 43 ++++++++++ .../lambda-at-edge-handlers/package-lock.json | 85 +++++++++++++++++++ packages/lambda-at-edge-handlers/package.json | 26 ++++++ .../lambda-at-edge-handlers/tsconfig.json | 3 + 11 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 packages/lambda-at-edge-handlers/index.ts create mode 100644 packages/lambda-at-edge-handlers/lib/cache-control.ts create mode 100644 packages/lambda-at-edge-handlers/lib/error-response.ts create mode 100644 packages/lambda-at-edge-handlers/lib/prerender-check.ts create mode 100644 packages/lambda-at-edge-handlers/lib/prerender.ts create mode 100644 packages/lambda-at-edge-handlers/lib/redirect.ts create mode 100644 packages/lambda-at-edge-handlers/package-lock.json create mode 100644 packages/lambda-at-edge-handlers/package.json create mode 100644 packages/lambda-at-edge-handlers/tsconfig.json diff --git a/.nvmrc b/.nvmrc index 0c19c7b4..ef1520fc 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16.18.1 +20.7.0 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f8379a32..ba543ed6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,10 @@ "resolved": "packages/geoip-redirect", "link": true }, + "node_modules/@aligent/cdk-lambda-at-edge-handlers": { + "resolved": "packages/lambda-at-edge-handlers", + "link": true + }, "node_modules/@aligent/cdk-prerender-proxy": { "resolved": "packages/prerender-proxy", "link": true @@ -5981,6 +5985,15 @@ "typescript": "~5.2.2" } }, + "packages/lambda-at-edge-handlers": { + "version": "0.1.0", + "license": "GPL-3.0-only", + "dependencies": { + "@types/aws-lambda": "^8.10.122", + "axios": "^1.5.1", + "source-map-support": "^0.5.21" + } + }, "packages/prerender-proxy": { "name": "@aligent/cdk-prerender-proxy", "version": "2.0.0", diff --git a/packages/lambda-at-edge-handlers/index.ts b/packages/lambda-at-edge-handlers/index.ts new file mode 100644 index 00000000..3bade8fe --- /dev/null +++ b/packages/lambda-at-edge-handlers/index.ts @@ -0,0 +1,13 @@ +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, + 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..66236483 --- /dev/null +++ b/packages/lambda-at-edge-handlers/lib/cache-control.ts @@ -0,0 +1,23 @@ +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"; + const 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..2a62d79f --- /dev/null +++ b/packages/lambda-at-edge-handlers/lib/error-response.ts @@ -0,0 +1,62 @@ +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 => { + const response = event.Records[0].cf.response; + const 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 => { + // Commenting this as there is body is not defined in the CloudFrontResponse type + // 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..93a2a6d5 --- /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|Google-InspectionTool|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 => { + const 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.ts b/packages/lambda-at-edge-handlers/lib/prerender.ts new file mode 100644 index 00000000..14166866 --- /dev/null +++ b/packages/lambda-at-edge-handlers/lib/prerender.ts @@ -0,0 +1,55 @@ +import "source-map-support/register"; +import { + CloudFrontRequest, + CloudFrontRequestEvent, + CloudFrontResponse, +} from "aws-lambda"; + +const PRERENDER_TOKEN = process.env.PRERENDER_TOKEN + ? process.env.PRERENDER_TOKEN + : "undefined"; +const PATH_PREFIX = process.env.PATH_PREFIX; +const PRERENDER_URL = process.env.PRERENDER_URL + ? process.env.PRERENDER_URL + : "service.prerender.io"; + +export const handler = async ( + event: CloudFrontRequestEvent +): Promise => { + const request = event.Records[0].cf.request; + + // viewer-request function will determine whether we prerender or not + // if we should we add prerender as our custom origin + if (request.headers["x-request-prerender"]) { + // Cloudfront will alter the request for / to /index.html + // since it is defined as the default root object + // we do not want to do this when prerendering the homepage + if (request.uri === `${PATH_PREFIX}/index.html`) { + request.uri = `${PATH_PREFIX}/`; + } + + request.origin = { + custom: { + domainName: PRERENDER_URL, + port: 443, + protocol: "https", + readTimeout: 20, + keepaliveTimeout: 5, + sslProtocols: ["TLSv1", "TLSv1.1", "TLSv1.2"], + path: "/https%3A%2F%2F" + request.headers["x-prerender-host"][0].value, + customHeaders: { + "x-prerender-token": [ + { + key: "x-prerender-token", + value: PRERENDER_TOKEN, + }, + ], + }, + }, + }; + } else { + request.uri = `${PATH_PREFIX}/index.html`; + } + + return 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..400e22f9 --- /dev/null +++ b/packages/lambda-at-edge-handlers/lib/redirect.ts @@ -0,0 +1,43 @@ +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 => { + const 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 new file mode 100644 index 00000000..6d78e486 --- /dev/null +++ b/packages/lambda-at-edge-handlers/package-lock.json @@ -0,0 +1,85 @@ +{ + "name": "@aligent/cdk-lambda-at-edge-handlers", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@aligent/cdk-lambda-at-edge-handlers", + "version": "0.1.0", + "license": "GPL-3.0-only", + "dependencies": { + "@types/aws-lambda": "^8.10.77", + "esbuild": "0.12.15", + "source-map-support": "^0.5.16" + } + }, + "node_modules/@types/aws-lambda": { + "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", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/esbuild": { + "version": "0.12.15", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.15.tgz", + "integrity": "sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + }, + "dependencies": { + "@types/aws-lambda": { + "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", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "esbuild": { + "version": "0.12.15", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.15.tgz", + "integrity": "sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } +} diff --git a/packages/lambda-at-edge-handlers/package.json b/packages/lambda-at-edge-handlers/package.json new file mode 100644 index 00000000..75d65928 --- /dev/null +++ b/packages/lambda-at-edge-handlers/package.json @@ -0,0 +1,26 @@ +{ + "name": "@aligent/cdk-lambda-at-edge-handlers", + "version": "0.1.0", + "description": "A Cloudfront Lambda@Edge handlers powered by Middy", + "main": "index.js", + "scripts": { + "build": "tsc", + "prepublish": "tsc", + "watch": "tsc -w", + "test": "echo \"No test specified\" && exit 0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/aligent/aws-cdk-constructs.git" + }, + "license": "GPL-3.0-only", + "bugs": { + "url": "https://github.com/aligent/cdk-constructs/issues" + }, + "homepage": "https://github.com/aligent/cdk-constructs#readme", + "dependencies": { + "@types/aws-lambda": "^8.10.122", + "source-map-support": "^0.5.21", + "axios": "^1.5.1" + } +} diff --git a/packages/lambda-at-edge-handlers/tsconfig.json b/packages/lambda-at-edge-handlers/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/lambda-at-edge-handlers/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} From 899cac2c2ed9ad4fd4a05644ca9827098a755ef7 Mon Sep 17 00:00:00 2001 From: Gowri Date: Wed, 27 Sep 2023 14:20:50 +0930 Subject: [PATCH 08/10] DO-1530: move @types/aws-lambda to dev dependencies --- packages/basic-auth/package.json | 2 +- packages/cloudfront-security-headers/package.json | 2 +- packages/geoip-redirect/package.json | 2 +- packages/lambda-at-edge-handlers/package.json | 4 +++- packages/prerender-proxy/package.json | 2 +- packages/static-hosting/package.json | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/basic-auth/package.json b/packages/basic-auth/package.json index acfc50da..66d6a454 100644 --- a/packages/basic-auth/package.json +++ b/packages/basic-auth/package.json @@ -19,6 +19,7 @@ "devDependencies": { "@types/jest": "^29.5.5", "@types/node": "20.6.3", + "@types/aws-lambda": "^8.10.122", "aws-cdk": "2.97.0", "jest": "^29.7.0", "ts-jest": "^29.1.1", @@ -26,7 +27,6 @@ "typescript": "~5.2.2" }, "dependencies": { - "@types/aws-lambda": "^8.10.122", "aws-cdk-lib": "2.97.0", "constructs": "^10.0.0", "esbuild": "^0.17.0", diff --git a/packages/cloudfront-security-headers/package.json b/packages/cloudfront-security-headers/package.json index 31d24bc7..21b3d112 100644 --- a/packages/cloudfront-security-headers/package.json +++ b/packages/cloudfront-security-headers/package.json @@ -13,6 +13,7 @@ "devDependencies": { "@types/jest": "^29.5.5", "@types/node": "20.6.3", + "@types/aws-lambda": "^8.10.122", "aws-cdk": "2.97.0", "jest": "^29.7.0", "ts-jest": "^29.1.1", @@ -20,7 +21,6 @@ "typescript": "~5.2.2" }, "dependencies": { - "@types/aws-lambda": "^8.10.122", "aws-cdk-lib": "2.97.0", "constructs": "^10.0.0", "source-map-support": "^0.5.21" diff --git a/packages/geoip-redirect/package.json b/packages/geoip-redirect/package.json index 532c5543..2da8c69f 100644 --- a/packages/geoip-redirect/package.json +++ b/packages/geoip-redirect/package.json @@ -19,6 +19,7 @@ "devDependencies": { "@types/jest": "^29.5.5", "@types/node": "20.6.3", + "@types/aws-lambda": "^8.10.122", "aws-cdk": "2.97.0", "jest": "^29.7.0", "ts-jest": "^29.1.1", @@ -26,7 +27,6 @@ "typescript": "~5.2.2" }, "dependencies": { - "@types/aws-lambda": "^8.10.122", "aws-cdk-lib": "2.97.0", "constructs": "^10.0.0", "source-map-support": "^0.5.21" diff --git a/packages/lambda-at-edge-handlers/package.json b/packages/lambda-at-edge-handlers/package.json index 75d65928..aba08db8 100644 --- a/packages/lambda-at-edge-handlers/package.json +++ b/packages/lambda-at-edge-handlers/package.json @@ -18,8 +18,10 @@ "url": "https://github.com/aligent/cdk-constructs/issues" }, "homepage": "https://github.com/aligent/cdk-constructs#readme", + "devDependencies": { + "@types/aws-lambda": "^8.10.122" + }, "dependencies": { - "@types/aws-lambda": "^8.10.122", "source-map-support": "^0.5.21", "axios": "^1.5.1" } diff --git a/packages/prerender-proxy/package.json b/packages/prerender-proxy/package.json index 0d5698cd..4725ac32 100644 --- a/packages/prerender-proxy/package.json +++ b/packages/prerender-proxy/package.json @@ -19,6 +19,7 @@ "devDependencies": { "@types/jest": "^29.5.5", "@types/node": "20.6.3", + "@types/aws-lambda": "^8.10.122", "aws-cdk": "2.97.0", "jest": "^29.7.0", "ts-jest": "^29.1.1", @@ -26,7 +27,6 @@ "typescript": "~5.2.2" }, "dependencies": { - "@types/aws-lambda": "^8.10.122", "aws-cdk-lib": "2.97.0", "axios": "^1.5.1", "constructs": "^10.0.0", diff --git a/packages/static-hosting/package.json b/packages/static-hosting/package.json index 389cba56..1420065d 100644 --- a/packages/static-hosting/package.json +++ b/packages/static-hosting/package.json @@ -16,6 +16,7 @@ "devDependencies": { "@types/jest": "^29.5.5", "@types/node": "20.6.3", + "@types/aws-lambda": "^8.10.122", "aws-cdk": "2.97.0", "jest": "^29.7.0", "ts-jest": "^29.1.1", @@ -23,7 +24,6 @@ "typescript": "~5.2.2" }, "dependencies": { - "@types/aws-lambda": "^8.10.122", "aws-cdk-lib": "2.97.0", "constructs": "^10.0.0", "esbuild": "^0.17.0", From ae7f0d6cdb44e7bb65aafa227b54e6f9ac1e1c15 Mon Sep 17 00:00:00 2001 From: Gowri Date: Wed, 27 Sep 2023 14:25:02 +0930 Subject: [PATCH 09/10] DO-1530: update identifiers to be consistent --- packages/basic-auth/lib/basic-auth-construct.ts | 2 +- packages/static-hosting/lib/path-remap.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/basic-auth/lib/basic-auth-construct.ts b/packages/basic-auth/lib/basic-auth-construct.ts index 2c954f55..e27afd3c 100644 --- a/packages/basic-auth/lib/basic-auth-construct.ts +++ b/packages/basic-auth/lib/basic-auth-construct.ts @@ -45,7 +45,7 @@ export class BasicAuthFunction extends Construct { public getFunctionVersion(): IVersion { return Version.fromVersionArn( this, - "basic-auth-function-version", + "basic-auth-fn-version", this.edgeFunction.currentVersion.edgeArn ); } diff --git a/packages/static-hosting/lib/path-remap.ts b/packages/static-hosting/lib/path-remap.ts index bce4bea1..86ebe59f 100644 --- a/packages/static-hosting/lib/path-remap.ts +++ b/packages/static-hosting/lib/path-remap.ts @@ -23,7 +23,7 @@ export class PathRemapFunction extends Construct { this.edgeFunction = new cf.experimental.EdgeFunction( this, - `${id}-edge-function`, + `${id}-remap-fn`, { code: Code.fromAsset(join(__dirname, "handlers"), { assetHashType: AssetHashType.OUTPUT, @@ -47,7 +47,7 @@ export class PathRemapFunction extends Construct { public getFunctionVersion(): IVersion { return Version.fromVersionArn( this, - "remap-function-version", + "remap-fn-version", this.edgeFunction.currentVersion.edgeArn ); } From fc7f46a99310f0c047e2642b2f3c0d5743122fac Mon Sep 17 00:00:00 2001 From: Gowri Date: Wed, 27 Sep 2023 14:52:34 +0930 Subject: [PATCH 10/10] DO-1530: run formatter and readd build command --- package.json | 4 +- .../prerender-proxy/lib/handlers/prerender.ts | 7 ++- packages/shared-vpc/package.json | 43 +++++++------- packages/waf/package.json | 57 +++++++++---------- tsconfig.json | 14 ++--- 5 files changed, 63 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index e53091eb..c0870056 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ }, "homepage": "https://github.com/aligent/aws-cdk-constructs#readme", "scripts": { - "build": "", - "prepublish": "", + "build": "tsc", + "prepublish": "tsc", "lint": "eslint --ignore-path .eslintignore --ext .ts .", "lint:check": "npm run lint", "lint:fix": "npm run lint --fix", diff --git a/packages/prerender-proxy/lib/handlers/prerender.ts b/packages/prerender-proxy/lib/handlers/prerender.ts index 675fb048..b28580ea 100644 --- a/packages/prerender-proxy/lib/handlers/prerender.ts +++ b/packages/prerender-proxy/lib/handlers/prerender.ts @@ -1,7 +1,10 @@ -import { CloudFrontRequest, CloudFrontRequestEvent, CloudFrontResponse } from "aws-lambda"; +import { + CloudFrontRequest, + CloudFrontRequestEvent, + CloudFrontResponse, +} from "aws-lambda"; import "source-map-support/register"; - const PRERENDER_TOKEN = process.env.PRERENDER_TOKEN; const PATH_PREFIX = process.env.PATH_PREFIX; const PRERENDER_URL = process.env.PRERENDER_URL; diff --git a/packages/shared-vpc/package.json b/packages/shared-vpc/package.json index 59c88514..36738100 100644 --- a/packages/shared-vpc/package.json +++ b/packages/shared-vpc/package.json @@ -1,24 +1,23 @@ { - "name": "@aligent/cdk-shared-vpc", - "version": "2.0.0", - "main": "index.js", - "scripts": { - "build": "tsc", - "prepublish": "tsc" - }, - "devDependencies": { - "@types/jest": "^29.5.5", - "@types/node": "20.6.3", - "aws-cdk": "2.97.0", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.1", - "typescript": "~5.2.2" - }, - "dependencies": { - "aws-cdk-lib": "2.97.0", - "constructs": "^10.0.0", - "source-map-support": "^0.5.21" - } + "name": "@aligent/cdk-shared-vpc", + "version": "2.0.0", + "main": "index.js", + "scripts": { + "build": "tsc", + "prepublish": "tsc" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + }, + "dependencies": { + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" } - \ No newline at end of file +} diff --git a/packages/waf/package.json b/packages/waf/package.json index 60f54db3..fe511827 100644 --- a/packages/waf/package.json +++ b/packages/waf/package.json @@ -1,31 +1,30 @@ { - "name": "@aligent/cdk-waf", - "version": "2.0.0", - "main": "index.js", - "license": "GPL-3.0-only", - "homepage": "https://github.com/aligent/aws-cdk-waf-stack#readme", - "repository": { - "type": "git", - "url": "https://github.com/aligent/aws-cdk-waf-stack" - }, - "types": "index.d.ts", - "scripts": { - "build": "tsc", - "prepublish": "tsc" - }, - "devDependencies": { - "@types/jest": "^29.5.5", - "@types/node": "20.6.3", - "aws-cdk": "2.97.0", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.1", - "typescript": "~5.2.2" - }, - "dependencies": { - "aws-cdk-lib": "2.97.0", - "constructs": "^10.0.0", - "source-map-support": "^0.5.21" - } + "name": "@aligent/cdk-waf", + "version": "2.0.0", + "main": "index.js", + "license": "GPL-3.0-only", + "homepage": "https://github.com/aligent/aws-cdk-waf-stack#readme", + "repository": { + "type": "git", + "url": "https://github.com/aligent/aws-cdk-waf-stack" + }, + "types": "index.d.ts", + "scripts": { + "build": "tsc", + "prepublish": "tsc" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + }, + "dependencies": { + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" } - \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index f1904fed..4119ec45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,12 +18,12 @@ "experimentalDecorators": true, "strictPropertyInitialization": false, "typeRoots": ["./node_modules/@types"], - "types": ["node", "jest"] + "types": ["node", "jest"], + + // https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/826 + // Ideally this should only be specified in packages/esbuild/tsconfig.json, however it doesn't seem to be + // loading that config file during build. + "skipLibCheck": true }, - "exclude": [ - "**/node_modules", - "cdk.out", - "**/handlers/**", - "packages/geoip-redirect/**" - ] + "exclude": ["**/node_modules", "cdk.out", "**/handlers/**"] }