├── images
├── sample.jpg
└── architecture.png
├── .gitignore
├── CODE_OF_CONDUCT.md
├── cdk.json
├── src
├── main
│ └── java
│ │ └── com
│ │ └── amazonaws
│ │ └── services
│ │ └── sample
│ │ └── apigateway
│ │ └── websocketratelimit
│ │ ├── RateLimitApp.java
│ │ └── RateLimitStack.java
└── test
│ └── java
│ └── com
│ └── amazonaws
│ └── services
│ └── sample
│ └── apigateway
│ └── websocketratelimit
│ └── RateLimitTest.java
├── LICENSE
├── lambda
├── Tenant.js
├── SampleClientGet.js
├── SessionTTL.js
├── WebsocketDisconnect.js
├── SampleClient.html
├── Authorizer.js
├── Session.js
├── WebsocketEcho.js
├── WebsocketConnect.js
├── SQSEcho.js
├── Common.js
└── SampleClient.js
├── pom.xml
├── CONTRIBUTING.md
└── README.md
/images/sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/api-gateway-websocket-saas-rate-limiting-using-aws-lambda-authorizer/HEAD/images/sample.jpg
--------------------------------------------------------------------------------
/images/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/api-gateway-websocket-saas-rate-limiting-using-aws-lambda-authorizer/HEAD/images/architecture.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .classpath.txt
2 | target
3 | .classpath
4 | .project
5 | .idea
6 | .settings
7 | .vscode
8 | *.iml
9 | *.DS_Store
10 | dependency-reduced-pom.xml
11 | *.jar
12 |
13 | # CDK asset staging directory
14 | .cdk.staging
15 | cdk.out
16 |
17 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "mvn -e -q compile exec:java",
3 | "context": {
4 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
5 | "@aws-cdk/core:stackRelativeExports": "true",
6 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true,
7 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
8 | "@aws-cdk/aws-lambda:recognizeVersionProps": true,
9 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/com/amazonaws/services/sample/apigateway/websocketratelimit/RateLimitApp.java:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: MIT-0
3 |
4 | package com.amazonaws.services.sample.apigateway.websocketratelimit;
5 |
6 | import software.amazon.awscdk.App;
7 | import software.amazon.awscdk.StackProps;
8 |
9 | public class RateLimitApp {
10 | public static void main(final String[] args) {
11 | App app = new App();
12 |
13 | new RateLimitStack(app, "APIGatewayWebSocketRateLimitStack", StackProps.builder()
14 | .stackName("APIGatewayWebSocketRateLimit")
15 | .build());
16 |
17 | app.synth();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15 |
16 |
--------------------------------------------------------------------------------
/src/test/java/com/amazonaws/services/sample/apigateway/websocketratelimit/RateLimitTest.java:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: MIT-0
3 |
4 | package com.amazonaws.services.sample.apigateway.websocketratelimit;
5 |
6 | import org.junit.Assert;
7 | import org.junit.Test;
8 | import software.amazon.awscdk.App;
9 | import com.fasterxml.jackson.databind.JsonNode;
10 | import com.fasterxml.jackson.databind.ObjectMapper;
11 | import com.fasterxml.jackson.databind.SerializationFeature;
12 |
13 | import java.io.IOException;
14 |
15 |
16 | public class RateLimitTest {
17 | private final static ObjectMapper JSON =
18 | new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true);
19 |
20 | @Test
21 | public void testStack() throws IOException {
22 | App app = new App();
23 | RateLimitStack stack = new RateLimitStack(app, "test");
24 |
25 | // synthesize the stack to a CloudFormation template
26 | JsonNode actual = JSON.valueToTree(app.synth().getStackArtifact(stack.getArtifactId()).getTemplate());
27 | Assert.assertNotNull(actual);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lambda/Tenant.js:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: MIT-0
3 |
4 | const common = require("./Common.js");
5 |
6 | // This handler is just a sample helper to fetch the current tenant ids from the database.
7 | // In a production system the tenant id would typically be known to the user and a list would not be
8 | // available as a public endpoint.
9 | exports.handler = async(event, context) => {
10 | //console.log('Received event:', JSON.stringify(event, null, 2));
11 |
12 | try {
13 | if (event.requestContext.http.method == "GET") {
14 | event.queryStringParameters = {
15 | tenantId: "none"
16 | };
17 | let dynamo = common.createDynamoDBClient(event);
18 | let body = await dynamo.scan({ "TableName": process.env.TenantTableName }).promise();
19 | return {statusCode: 200, headers: {"Content-Type": "application/json"}, body: JSON.stringify(body)};
20 | }
21 | }
22 | catch (err) {
23 | console.error(err);
24 | return { statusCode: 400, headers: { "Content-Type": "application/json" }, body: JSON.stringify(err.message) };
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lambda/SampleClientGet.js:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: MIT-0
3 |
4 | const fs = require('fs')
5 |
6 | // This is a simple handler to return back the sample webpage with url values created by the cloudformation stack
7 | exports.handler = async(event) => {
8 | //console.log("Sample: " + JSON.stringify(event, null, 2));
9 |
10 | try {
11 | let filename = event.queryStringParameters && event.queryStringParameters.page ? event.queryStringParameters.page : "SampleClient.html";
12 | const data = fs.readFileSync("./" + filename, 'utf8').replace("{{WssUrl}}", process.env.WssUrl).replace("{{sessionUrl}}", process.env.SessionUrl).replace("{{tenantUrl}}", process.env.TenantUrl);
13 | let contentType = filename == "SampleClient.js" ? "text/javascript" : "text/html";
14 | return {
15 | statusCode: 200,
16 | body: data,
17 | headers: { "Content-Type": contentType }
18 | };
19 | }
20 | catch (err) {
21 | console.error(err);
22 | return {
23 | statusCode: 500,
24 | body: JSON.stringify('Error: ' + JSON.stringify(err))
25 | };
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/lambda/SessionTTL.js:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: MIT-0
3 |
4 | const AWS = require("aws-sdk");
5 | const apig = new AWS.ApiGatewayManagementApi({ endpoint: process.env.ApiGatewayEndpoint });
6 |
7 | // This handler is used to disconnect any remaining websocket connections for a given session when the time to live (TTL) expires
8 | exports.handler = async function(event, context) {
9 | //console.log(JSON.stringify(event));
10 | for (let x = 0; x < event.Records.length; x++) {
11 | const record = event.Records[x];
12 | if (record.userIdentity && record.userIdentity.principalId && record.userIdentity.type && record.userIdentity.principalId == "dynamodb.amazonaws.com" && record.userIdentity.type == "Service") {
13 | if (record.eventName == 'REMOVE' && record.dynamodb && record.dynamodb.OldImage && record.dynamodb.OldImage.connectionIds) {
14 | let connectionIds = record.dynamodb.OldImage.connectionIds.SS;
15 | for (let y = 0; y < connectionIds.length; y++) {
16 | const connectionId = connectionIds[y];
17 | //console.log("SessionTTL Removing ConnectionId: " + connectionId);
18 | try {
19 | await apig.deleteConnection({ ConnectionId: connectionId }).promise();
20 | }
21 | catch (err) {
22 | console.error(err);
23 | }
24 | };
25 | }
26 | }
27 | }
28 | return { statusCode: 200 };
29 | };
30 |
--------------------------------------------------------------------------------
/lambda/WebsocketDisconnect.js:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: MIT-0
3 |
4 | const common = require("./Common.js");
5 |
6 | // This handler will remove the current connection from the sessions connectionId set
7 | // and decrement the total number of connections for this tenant
8 | exports.handler = async function(event, context) {
9 | //console.log('Received event:', JSON.stringify(event, null, 2));
10 |
11 | if (event.requestContext.routeKey == '$disconnect') {
12 | try {
13 | let dynamo = common.createDynamoDBClient(event);
14 | let tenantId = common.getTenantId(event);
15 | let sessionId = common.getSessionId(event);
16 | let deleteConnectIdParams = {
17 | "TableName": process.env.SessionTableName,
18 | "Key": {tenantId: tenantId, sessionId: sessionId},
19 | "UpdateExpression": "DELETE connectionIds :c",
20 | "ExpressionAttributeValues": {
21 | ":c": dynamo.createSet([event.requestContext.connectionId])
22 | },
23 | "ReturnValues": "NONE"
24 | };
25 | let updateConnectCountParams = {
26 | "TableName": process.env.LimitTableName,
27 | "Key": { tenantId: tenantId, key: tenantId },
28 | "UpdateExpression": "set itemCount = if_not_exists(itemCount, :zero) - :dec",
29 | "ExpressionAttributeValues": {":dec": 1, ":zero": 0},
30 | "ReturnValues": "NONE"
31 | };
32 | await dynamo.transactWrite({TransactItems: [{Update: deleteConnectIdParams}, {Update: updateConnectCountParams}]}).promise();
33 | } catch (err) {
34 | console.error(err);
35 | return {statusCode: 1011}; // return server error code
36 | }
37 | }
38 | return { statusCode: 200 };
39 | }
40 |
--------------------------------------------------------------------------------
/lambda/SampleClient.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Demo Client
6 |
7 |
8 |
9 |
10 |
34 |
35 |