Skip to content

Commit

Permalink
Add IP based rate limit middleware, powered by upstash
Browse files Browse the repository at this point in the history
The minimum rate limitting limits in AWS WAF are too high for our
purposes of trying to avoid cost spikes caused by to many openai API
requests.
  • Loading branch information
unstubbable committed Mar 14, 2024
1 parent 78ba515 commit f94c2ce
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 7 deletions.
1 change: 1 addition & 0 deletions apps/aws-app/cdk/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as cdk from 'aws-cdk-lib';
import './env.js';
import {MainStack} from './main-stack.js';
import {WafStack} from './waf-stack.js';

Expand Down
20 changes: 20 additions & 0 deletions apps/aws-app/cdk/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {z} from 'zod';

declare global {
namespace NodeJS {
interface ProcessEnv extends z.infer<typeof envVariables> {}
}
}

const envVariables = z.object({
AWS_ACCESS_KEY_ID: z.string(),
AWS_HANDLER_VERIFY_HEADER: z.string(),
AWS_REGION: z.string(),
AWS_SECRET_ACCESS_KEY: z.string(),
GOOGLE_SEARCH_API_KEY: z.string(),
GOOGLE_SEARCH_SEARCH_ENGINE_ID: z.string(),
UPSTASH_REDIS_REST_TOKEN: z.string(),
UPSTASH_REDIS_REST_URL: z.string(),
});

envVariables.parse(process.env);
18 changes: 11 additions & 7 deletions apps/aws-app/cdk/main-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import path from 'path';
import * as cdk from 'aws-cdk-lib';
import type {Construct} from 'constructs';

const verifyHeader = process.env.AWS_HANDLER_VERIFY_HEADER;
const distDirname = path.join(import.meta.dirname, `../dist/`);

export interface MainStackProps extends cdk.StackProps {
Expand All @@ -26,9 +25,14 @@ export class MainStack extends cdk.Stack {
runtime: cdk.aws_lambda.Runtime.NODEJS_20_X,
bundling: {format: cdk.aws_lambda_nodejs.OutputFormat.ESM},
timeout: cdk.Duration.minutes(1),
environment: verifyHeader
? {AWS_HANDLER_VERIFY_HEADER: verifyHeader}
: undefined,
environment: {
AWS_HANDLER_VERIFY_HEADER: process.env.AWS_HANDLER_VERIFY_HEADER,
GOOGLE_SEARCH_API_KEY: process.env.GOOGLE_SEARCH_API_KEY,
GOOGLE_SEARCH_SEARCH_ENGINE_ID:
process.env.GOOGLE_SEARCH_SEARCH_ENGINE_ID,
UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN,
UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL,
},
},
);

Expand All @@ -47,9 +51,9 @@ export class MainStack extends cdk.Stack {
const distribution = new cdk.aws_cloudfront.Distribution(this, `cdn`, {
defaultBehavior: {
origin: new cdk.aws_cloudfront_origins.FunctionUrlOrigin(functionUrl, {
customHeaders: verifyHeader
? {'X-Origin-Verify': verifyHeader}
: undefined,
customHeaders: {
'X-Origin-Verify': process.env.AWS_HANDLER_VERIFY_HEADER,
},
}),
allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_ALL,
cachePolicy: new cdk.aws_cloudfront.CachePolicy(this, `cache-policy`, {
Expand Down
2 changes: 2 additions & 0 deletions apps/aws-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"dependencies": {
"@mfng/core": "*",
"@mfng/shared-app": "*",
"@upstash/ratelimit": "^1.0.1",
"@upstash/redis": "^1.28.4",
"ai": "^3.0.11",
"clsx": "^1.2.1",
"openai": "^4.29.0",
Expand Down
2 changes: 2 additions & 0 deletions apps/aws-app/src/handler/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import {
reactServerManifest,
reactSsrManifest,
} from './manifests.js';
import {ratelimitMiddleware} from './ratelimit-middleware.js';

export const app = new Hono();

app.use(authMiddleware);
app.use(ratelimitMiddleware);
app.use(loggerMiddleware);
app.get(`/*`, async (context) => handleGet(context.req.raw));
app.post(`/*`, async (context) => handlePost(context.req.raw));
Expand Down
38 changes: 38 additions & 0 deletions apps/aws-app/src/handler/ratelimit-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {Ratelimit} from '@upstash/ratelimit';
import {Redis} from '@upstash/redis';
import type {MiddlewareHandler} from 'hono';

let ratelimit: Ratelimit | undefined;

try {
ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, `10 m`),
analytics: true,
ephemeralCache: new Map(),
});
} catch {
console.warn(`Unable to create Redis instance, no rate limits are applied.`);
}

export const ratelimitMiddleware: MiddlewareHandler = async (context, next) => {
if (ratelimit && context.req.method === `POST`) {
const ip = context.req.header(`cloudfront-viewer-address`);

if (ip) {
const {success, reset} = await ratelimit.limit(ip);

if (!success) {
console.debug(`Rate limit reached for ${ip}`);

return context.text(`Too Many Requests`, 429, {
'Retry-After': ((reset - Date.now()) / 1000).toFixed(),
});
}
} else {
console.warn(`cloudfront-viewer-address not provided`);
}
}

return next();
};
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f94c2ce

Please sign in to comment.