From 0ce5876de1b934e4fece0c2959cb9a7fb0fb40fe Mon Sep 17 00:00:00 2001 From: Douglas Naphas Date: Fri, 18 Mar 2022 20:21:09 -0400 Subject: [PATCH] Add a WebSockets API behind CloudFront gh-250 --- backend/connect.js | 41 +++++++++++++++++++ backend/default.js | 13 ++++++ backend/disconnect.js | 41 +++++++++++++++++++ backend/schema.js | 8 ++++ lib/madliberation-webapp.ts | 72 +++++++++++++++++++++++++++++++++ package-lock.json | 81 ++++++++++++++++++++++++++++++------- package.json | 6 ++- 7 files changed, 246 insertions(+), 16 deletions(-) create mode 100644 backend/connect.js create mode 100644 backend/default.js create mode 100644 backend/disconnect.js diff --git a/backend/connect.js b/backend/connect.js new file mode 100644 index 00000000..f438a895 --- /dev/null +++ b/backend/connect.js @@ -0,0 +1,41 @@ +const DynamoDB = require("aws-sdk/clients/dynamodb"); +const schema = require("../schema"); + +exports.handler = async function (event, context, callback) { + console.log("connect handler called"); + console.log("event:"); + console.log(event); + console.log("context:"); + console.log(context); + const db = new DynamoDB.DocumentClient(); + const now = new Date(); + var putParams = { + TableName: process.env.TABLE_NAME, + Item: { + [schema.PARTITION_KEY]: `CHECK?PARAMS`, // need to see if event has params + [schema.SORT_KEY]: + `${schema.CONNECT}` + + `${schema.SEPARATOR}` + + `${event.requestContext.connectionId}`, + [schema.CONNECTION_ID]: event.requestContext.connectionId, + [schema.DATE]: now.toISOString(), + [schema.MS]: now.getTime() + }, + }; + + try { + // Insert incoming connection id in the WebSocket + await db.put(putParams).promise(); + + return { + statusCode: 200, + body: "Connected", + }; + } catch (e) { + console.error("connect error!", e); + return { + statusCode: 501, + body: "Failed to connect: " + JSON.stringify(e), + }; + } +}; diff --git a/backend/default.js b/backend/default.js new file mode 100644 index 00000000..37007ef2 --- /dev/null +++ b/backend/default.js @@ -0,0 +1,13 @@ +const schema = require("../schema"); + +exports.handler = async function (event, context, callback) { + console.log("default handler called"); + console.log("event:"); + console.log(event); + console.log("context:"); + console.log(context); + return { + statusCode: 200, + body: "defaulted", + }; +}; diff --git a/backend/disconnect.js b/backend/disconnect.js new file mode 100644 index 00000000..d42f8c9c --- /dev/null +++ b/backend/disconnect.js @@ -0,0 +1,41 @@ +const DynamoDB = require("aws-sdk/clients/dynamodb"); +const schema = require("../schema"); + +exports.handler = async function (event, context, callback) { + console.log("disconnect handler called"); + console.log("event:"); + console.log(event); + console.log("context:"); + console.log(context); + const db = new DynamoDB.DocumentClient(); + const now = new Date(); + var putParams = { + TableName: process.env.TABLE_NAME, + Item: { + [schema.PARTITION_KEY]: `CHECK?PARAMS`, // need to see if event has params + [schema.SORT_KEY]: + `${schema.DISCONNECT}` + + `${schema.SEPARATOR}` + + `${event.requestContext.connectionId}`, + [schema.CONNECTION_ID]: event.requestContext.connectionId, + [schema.DATE]: now.toISOString(), + [schema.MS]: now.getTime() + }, + }; + + try { + // Insert incoming connection id in the WebSocket + await db.put(putParams).promise(); + + return { + statusCode: 200, + body: "Disconnected", + }; + } catch (e) { + console.error("disconnect error!", e); + return { + statusCode: 501, + body: "Failed to disconnect: " + JSON.stringify(e), + }; + } +}; diff --git a/backend/schema.js b/backend/schema.js index cef332d5..809f557e 100644 --- a/backend/schema.js +++ b/backend/schema.js @@ -54,6 +54,14 @@ const schema = { OPAQUE_COOKIE_EXPIRATION_MILLISECONDS: "cookie_expiration_ms", OPAQUE_COOKIE_ISSUED_DATE: "cookie_issued_date", OPAQUE_COOKIE_EXPIRATION_DATE: "cookie_expiration_date", + // WebSockets + CONNECTION: "connection", + EVENT: "event", // CONNECT or DISCONNECT + CONNECT: "connect", + DISCONNECT: "disconnect", + DATE: "date", + MS: "ms", + CONNECTION_ID: "connection_id" }; module.exports = schema; diff --git a/lib/madliberation-webapp.ts b/lib/madliberation-webapp.ts index dbec37e5..dac6f183 100644 --- a/lib/madliberation-webapp.ts +++ b/lib/madliberation-webapp.ts @@ -21,6 +21,8 @@ import { aws_iam as iam } from "aws-cdk-lib"; import { aws_certificatemanager as acm } from "aws-cdk-lib"; import { aws_route53 as route53 } from "aws-cdk-lib"; import { aws_route53_targets as targets } from "aws-cdk-lib"; +import * as apigwv2 from "@aws-cdk/aws-apigatewayv2-alpha"; +import * as apigwv2i from "@aws-cdk/aws-apigatewayv2-integrations-alpha"; const schema = require("../backend/schema"); export interface MadLiberationWebappProps extends StackProps { @@ -424,6 +426,73 @@ export class MadliberationWebapp extends Stack { cfnAliasWWWRecordSet.setIdentifier = "mlwebapp-www-cf-alias"; } + const makeWSHandler = (prefix: string) => + new lambda.Function(this, `${prefix}Handler`, { + runtime: lambda.Runtime.NODEJS_14_X, + handler: `${prefix.toLowerCase()}.handler`, + code: lambda.Code.fromAsset("backend"), + memorySize: 3000, + environment: { + NODE_ENV: "production", + TABLE_NAME: sedersTable.tableName, + }, + timeout: Duration.seconds(20), + }); + const connectHandler = makeWSHandler("Connect"); + const disconnectHandler = makeWSHandler("Disconnet"); + const defaultHandler = makeWSHandler("Default"); + const webSocketApi = new apigwv2.WebSocketApi(this, "WSAPI", { + connectRouteOptions: { + integration: new apigwv2i.WebSocketLambdaIntegration( + "ConnectIntegration", + connectHandler + ), + }, + disconnectRouteOptions: { + integration: new apigwv2i.WebSocketLambdaIntegration( + "DisconnectIntegration", + disconnectHandler + ), + }, + defaultRouteOptions: { + integration: new apigwv2i.WebSocketLambdaIntegration( + "DefaultIntegration", + defaultHandler + ), + }, + }); + const stageName = "ws"; + const wsStage = new apigwv2.WebSocketStage(this, "WSStage", { + stageName, + webSocketApi, + autoDeploy: true, + }); + distro.addBehavior( + `/${stageName}/*`, + new origins.HttpOrigin( + `${webSocketApi.apiId}.execute-api.${this.region}.${this.urlSuffix}`, + { + protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY, + } + ), + { + allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, + cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, + viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + originRequestPolicy: new cloudfront.OriginRequestPolicy( + this, + "WSOriginRequestPolicy", + { + headerBehavior: cloudfront.OriginRequestHeaderBehavior.allowList( + "Sec-WebSocket-Extensions", + "Sec-WebSocket-Key", + "Sec-WebSocket-Version" + ), + } + ), + } + ); + const scriptsBucket = new MadLiberationBucket(this, "ScriptsBucket", { versioned: true, }); @@ -464,5 +533,8 @@ export class MadliberationWebapp extends Stack { value: scriptsBucket.bucketName, }); new CfnOutput(this, "TableName", { value: sedersTable.tableName }); + new CfnOutput(this, "WSAPIEndpoint", { + value: webSocketApi.apiEndpoint, + }); } } diff --git a/package-lock.json b/package-lock.json index 5b1b7878..695fca22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { + "@aws-cdk/aws-apigatewayv2-alpha": "^2.17.0-alpha.0", + "@aws-cdk/aws-apigatewayv2-integrations-alpha": "^2.17.0-alpha.0", "@cdk-turnkey/stackname": "^1.2.0", "aws-cdk-lib": "^2.0.0", "aws-sdk": "^2.850.0", @@ -26,6 +28,31 @@ "typescript": "~3.9.7" } }, + "node_modules/@aws-cdk/aws-apigatewayv2-alpha": { + "version": "2.17.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-alpha/-/aws-apigatewayv2-alpha-2.17.0-alpha.0.tgz", + "integrity": "sha512-skOShUEi+npxbg+lgGRWsjts/z0AttGxWKIylIlgVoLsWB8dLsHJaiUefCByu87Gpe5w2HOMj4shrai2nKLwOA==", + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.17.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-cdk/aws-apigatewayv2-integrations-alpha": { + "version": "2.17.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-integrations-alpha/-/aws-apigatewayv2-integrations-alpha-2.17.0-alpha.0.tgz", + "integrity": "sha512-KicMeXovJcxWr6+wAB7u/9Ti5AMKFz2hljeJY8FiHFEy6w+vCRhQ0EVuJWxBfRdVmJoNK0AmBU4/Y+4drxnZVg==", + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@aws-cdk/aws-apigatewayv2-alpha": "2.17.0-alpha.0", + "aws-cdk-lib": "^2.17.0", + "constructs": "^10.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", @@ -1344,9 +1371,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.15.0.tgz", - "integrity": "sha512-QyxP4GM8bgwIdvE93V9WgE7y/yutFrY8pRxveCy6uY6E29Na2Lbes+4NT4xxcmLuj8h4bgcimbOreRw01OWcFA==", + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.17.0.tgz", + "integrity": "sha512-bga2HptbGx3rMdSkIKxBS13miogj/DHB2VPfQZAoKoCOAanOot+M3mHhYqe5aNdxhrppaRjG2eid2p1/MvRnvg==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -1666,7 +1693,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -1691,6 +1719,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1916,7 +1945,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "node_modules/constructs": { "version": "10.0.79", @@ -2408,7 +2438,8 @@ "node_modules/graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true }, "node_modules/has": { "version": "1.0.3", @@ -3630,6 +3661,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3930,6 +3962,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, "engines": { "node": ">=6" } @@ -4049,6 +4082,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -4738,6 +4772,18 @@ } }, "dependencies": { + "@aws-cdk/aws-apigatewayv2-alpha": { + "version": "2.17.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-alpha/-/aws-apigatewayv2-alpha-2.17.0-alpha.0.tgz", + "integrity": "sha512-skOShUEi+npxbg+lgGRWsjts/z0AttGxWKIylIlgVoLsWB8dLsHJaiUefCByu87Gpe5w2HOMj4shrai2nKLwOA==", + "requires": {} + }, + "@aws-cdk/aws-apigatewayv2-integrations-alpha": { + "version": "2.17.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-integrations-alpha/-/aws-apigatewayv2-integrations-alpha-2.17.0-alpha.0.tgz", + "integrity": "sha512-KicMeXovJcxWr6+wAB7u/9Ti5AMKFz2hljeJY8FiHFEy6w+vCRhQ0EVuJWxBfRdVmJoNK0AmBU4/Y+4drxnZVg==", + "requires": {} + }, "@babel/code-frame": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", @@ -5771,9 +5817,9 @@ } }, "aws-cdk-lib": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.15.0.tgz", - "integrity": "sha512-QyxP4GM8bgwIdvE93V9WgE7y/yutFrY8pRxveCy6uY6E29Na2Lbes+4NT4xxcmLuj8h4bgcimbOreRw01OWcFA==", + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.17.0.tgz", + "integrity": "sha512-bga2HptbGx3rMdSkIKxBS13miogj/DHB2VPfQZAoKoCOAanOot+M3mHhYqe5aNdxhrppaRjG2eid2p1/MvRnvg==", "requires": { "@balena/dockerignore": "^1.0.2", "case": "1.6.3", @@ -5995,7 +6041,8 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "base64-js": { "version": "1.5.1", @@ -6006,6 +6053,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6182,7 +6230,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "constructs": { "version": "10.0.79", @@ -6547,7 +6596,8 @@ "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true }, "has": { "version": "1.0.3", @@ -7481,6 +7531,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7708,7 +7759,8 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true }, "querystring": { "version": "0.2.0", @@ -7796,7 +7848,8 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true }, "shebang-command": { "version": "2.0.0", diff --git a/package.json b/package.json index 4c98c91b..9e228b3d 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,13 @@ "typescript": "~3.9.7" }, "dependencies": { - "aws-cdk-lib": "^2.0.0", - "constructs": "^10.0.0", + "@aws-cdk/aws-apigatewayv2-alpha": "^2.17.0-alpha.0", + "@aws-cdk/aws-apigatewayv2-integrations-alpha": "^2.17.0-alpha.0", "@cdk-turnkey/stackname": "^1.2.0", + "aws-cdk-lib": "^2.0.0", "aws-sdk": "^2.850.0", "bufferutil": "^4.0.3", + "constructs": "^10.0.0", "utf-8-validate": "^5.0.5" } }