Skip to content

Commit

Permalink
feat: use native rate-limiter (#139)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone authored Aug 31, 2024
1 parent e66e28e commit 3a869a4
Show file tree
Hide file tree
Showing 10 changed files with 62 additions and 183 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"upload": "yarn tsx src/maintenance/upload.ts"
},
"dependencies": {
"itty-durable": "^1.2.0",
"itty-router": "^2.6.1",
"itty-router-extras": "^0.4.2",
"json-logic-js": "^2.0.2",
Expand Down
99 changes: 0 additions & 99 deletions src/durable_objects/RateLimiter.ts

This file was deleted.

9 changes: 0 additions & 9 deletions src/durable_objects/itty.d.ts

This file was deleted.

4 changes: 3 additions & 1 deletion src/lib/apiKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ const AUTH_TAG_LEN = 8;

export interface APIKey {
id: number;
/** The maximum number of requests per hour */
/** @deprecated The maximum number of requests per hour */
rateLimit: number;
/** Which ratelimiting bucket to use */
bucket?: string;
}

// Encoded API key format:
Expand Down
2 changes: 1 addition & 1 deletion src/maintenance/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ void (async () => {
})),
};
console.log(
`Uplaoding files ${cursor + 1}...${
`Uploading files ${cursor + 1}...${
cursor + currentBatch.length
} of ${files.length}...`
);
Expand Down
32 changes: 0 additions & 32 deletions src/routes/admin.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { withDurables } from "itty-durable";
import {
json,
missing,
text,
withParams,
type ThrowableRouter,
} from "itty-router-extras";
import type { RateLimiterProps } from "../durable_objects/RateLimiter";
import { encryptAPIKey } from "../lib/apiKeys";
import {
createCachedR2FS,
Expand Down Expand Up @@ -161,34 +159,4 @@ export default function register(router: ThrowableRouter): void {
return text(ret || "");
}
);

router.post(
"/admin/resetRateLimit/:id/:limit",
withParams,
withDurables({ parse: true }),
async (
req: RequestWithProps<
[{ params: { id: string; limit: string } }, RateLimiterProps]
>,
env: CloudflareEnvironment
) => {
const id = parseInt(req.params.id);
const limit = parseInt(req.params.limit);
if (
Number.isNaN(id) ||
id < 1 ||
Number.isNaN(limit) ||
limit < 1
) {
console.error("Usage: /admin/resetRateLimit/:id/:limit");
return clientError("Invalid id or limit");
}

const objId = env.RateLimiter.idFromName(req.params.id);
const RateLimiter = req.RateLimiter.get(objId);
await RateLimiter.setTo(limit);

return json({ ok: true });
}
);
}
47 changes: 26 additions & 21 deletions src/routes/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withDurables } from "itty-durable";
import type { RateLimit } from "@cloudflare/workers-types/experimental";
import { json, type ThrowableRouter } from "itty-router-extras";
import { compare } from "semver";
import {
Expand All @@ -8,7 +8,6 @@ import {
APIv3_RequestSchema,
APIv3_Response,
} from "../apiDefinitions";
import type { RateLimiterProps } from "../durable_objects/RateLimiter";
import { withCache } from "../lib/cache";
import { lookupConfig } from "../lib/config";
import type { UpgradeInfo } from "../lib/configSchema";
Expand Down Expand Up @@ -132,35 +131,41 @@ async function handleUpdateRequest(
);
}

function getBucket(rateLimit: number): string {
if (rateLimit === 9) return "TEST";
if (rateLimit <= 100) return "FREE";
if (rateLimit <= 1000) return "1k";
if (rateLimit <= 10000) return "10k";
if (rateLimit <= 100000) return "100k";
return "FREE";
}

export default function register(router: ThrowableRouter): void {
// Check API keys and apply rate limiter
router.all("/api/*", withAPIKey, withDurables({ parse: true }), (async (
req: RequestWithProps<[APIKeyProps, RateLimiterProps]>,
router.all("/api/*", withAPIKey, (async (
req: RequestWithProps<[APIKeyProps]>,
env: CloudflareEnvironment
) => {
const objId = env.RateLimiter.idFromName(
(req.apiKey?.id ?? 0).toString()
);
const RateLimiter = req.RateLimiter.get(objId);
const bucket =
req.apiKey?.bucket ??
(req.apiKey?.rateLimit && getBucket(req.apiKey.rateLimit)) ??
"FREE";
const rateLimiter = ((env as any)[`RL_${bucket}`] ??
env.RL_FREE) as RateLimit;
const apiKeyId = req.apiKey?.id ?? 0;

const maxPerHour = req.apiKey?.rateLimit ?? 10000;
const result = await RateLimiter.request(maxPerHour);
const { success } = await rateLimiter.limit({
key: apiKeyId.toString(),
});

env.responseHeaders = {
...env.responseHeaders,
"x-ratelimit-limit": maxPerHour.toString(),
"x-ratelimit-remaining": result.remaining.toString(),
"x-ratelimit-reset": Math.ceil(result.resetDate / 1000).toString(),
};

if (result.limitExceeded) {
if (!success) {
// Rate limit exceeded

return new Response(undefined, {
status: 429,
headers: {
"retry-after": Math.ceil(
(result.resetDate - Date.now()) / 1000
).toString(),
// Right now we have no way to know when the rate limit will reset
"retry-after": "60",
},
});
}
Expand Down
5 changes: 1 addition & 4 deletions src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,14 @@ export interface CloudflareEnvironment {

CONFIG_FILES: R2Bucket;

RateLimiter: DurableObjectNamespace;

API_KEYS: KVNamespace;
RL_FREE: RateLimit;

responseHeaders: Record<string, string>;
}

const router = build();

export { RateLimiter as RateLimiterDurableObject } from "./durable_objects/RateLimiter";

export default {
async fetch(
request: Request,
Expand Down
35 changes: 31 additions & 4 deletions wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,38 @@ kv_namespaces = [
tag = "v1" # Should be unique for each entry
new_classes = ["RateLimiterDurableObject"]

[durable_objects]
bindings = [
{name = "RateLimiter", class_name = "RateLimiterDurableObject"},
]
[[migrations]]
tag = "v2"
deleted_classes = ["RateLimiterDurableObject"]

[[r2_buckets]]
binding = "CONFIG_FILES"
bucket_name = "zwave-js-firmware-updates--config-files"

[[unsafe.bindings]]
type = "ratelimit"
# Free ratelimit bucket - 120 requests per hour
name = "RL_FREE"
namespace_id = "1001"
simple = { limit = 2, period = 60 } # <limit> requests per <period> seconds

[[unsafe.bindings]]
type = "ratelimit"
# Ratelimit bucket: 1200 requests per hour
name = "RL_1k"
namespace_id = "1002"
simple = { limit = 20, period = 60 } # <limit> requests per <period> seconds

[[unsafe.bindings]]
type = "ratelimit"
# Ratelimit bucket: 12000 requests per hour
name = "RL_10k"
namespace_id = "1003"
simple = { limit = 200, period = 60 } # <limit> requests per <period> seconds

[[unsafe.bindings]]
type = "ratelimit"
# Ratelimit bucket: 120000 requests per hour
name = "RL_100k"
namespace_id = "1004"
simple = { limit = 2000, period = 60 } # <limit> requests per <period> seconds
11 changes: 0 additions & 11 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -990,7 +990,6 @@ __metadata:
eslint: ^8.23.1
eslint-config-prettier: ^8.5.0
eslint-plugin-prettier: ^4.2.1
itty-durable: ^1.2.0
itty-router: ^2.6.1
itty-router-extras: ^0.4.2
json-logic-js: ^2.0.2
Expand Down Expand Up @@ -3154,16 +3153,6 @@ __metadata:
languageName: node
linkType: hard

"itty-durable@npm:^1.2.0":
version: 1.2.0
resolution: "itty-durable@npm:1.2.0"
dependencies:
itty-router: ^2.6.1
itty-router-extras: ^0.4.2
checksum: 11e945a19b56c5e5ab896a7eaf042f4d2f8e760af528cac7d21468aac7e3c47a604365518720ffcda719dc2016b6babb5864f4ed21124594d892b11a7cab1e64
languageName: node
linkType: hard

"itty-router-extras@npm:^0.4.2":
version: 0.4.2
resolution: "itty-router-extras@npm:0.4.2"
Expand Down

0 comments on commit 3a869a4

Please sign in to comment.