Skip to content

Commit

Permalink
oh my days
Browse files Browse the repository at this point in the history
  • Loading branch information
dromzeh committed Jan 28, 2024
1 parent ab889a1 commit 18b841c
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 10 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@
"@scalar/hono-api-reference": "^0.3.29",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"better-sqlite3": "^9.3.0",
"dayjs": "^1.11.10",
"drizzle-orm": "^0.29.3",
"drizzle-zod": "^0.5.1",
"hono": "^3.12.8",
"lucia": "3.0.0",
"oslo": "^1.0.2",
"prettier": "^3.2.4",
"zod": "^3.22.4"
"zod": "^3.22.4",
"zod-error": "^1.5.0"
}
}
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

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

24 changes: 15 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import BaseRoutes from "@/v2/routes/handler"
import { CustomCSS, OpenAPIConfig } from "./openapi/config"
import { cors } from "hono/cors"
import { csrf } from "hono/csrf"
import { LogTime } from "./v2/middleware/time-taken"
import { rateLimit } from "./v2/middleware/ratelimit/limiter"

// this is required for the rate limiter to work
export { RateLimiter } from "@/v2/middleware/ratelimit/ratelimit.do"

const app = new OpenAPIHono<{ Bindings: Bindings; Variables: Variables }>()

// v2 API routes
app.route("/v2", BaseRoutes)
app.route("/v2", BaseRoutes).use("*", rateLimit(60, 100))

// scalar API reference, very nice and lightweight
// i am putting this at root because i can
app.get(
"/",
apiReference({
Expand All @@ -27,13 +28,8 @@ app.get(
// openapi config
app.doc("/openapi", OpenAPIConfig)

// interface CSRFOptions {
// origin?: string | string[] | IsAllowedOriginHandler;
// }
app.use("*", csrf())

app.use("*", LogTime)

app.use(
"*",
cors({
Expand All @@ -45,6 +41,16 @@ app.use(

app.use("*", prettyJSON())

app.notFound((ctx) => {
return ctx.json(
{
success: false,
message: "Not Found",
},
404
)
})

app.onError((err, ctx) => {
console.error(err)
return ctx.json(
Expand Down
103 changes: 103 additions & 0 deletions src/v2/middleware/ratelimit/limiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import dayjs from "dayjs"
import { Context, MiddlewareHandler } from "hono"

const fakeDomain = "http://rate-limiter.com/"

const getRateLimitKey = (ctx: Context) => {
const ip = ctx.req.header("cf-connecting-ip")
// TODO(dromzeh): look into setting current user w/ ctx.get/set, then we can use that OVER user ip? idk
const uniqueKey = ip || ""
return uniqueKey
}

const getCacheKey = (
endpoint: string,
key: number | string,
limit: number,
interval: number
) => {
return `${fakeDomain}${endpoint}/${key}/${limit}/${interval}`
}

const setRateLimitHeaders = (
ctx: Context,
secondsExpires: number,
limit: number,
remaining: number,
interval: number
) => {
ctx.header("X-RateLimit-Limit", limit.toString())
ctx.header("X-RateLimit-Remaining", remaining.toString())
ctx.header("X-RateLimit-Reset", secondsExpires.toString())
ctx.header("X-RateLimit-Policy", `rate-limit-${limit}-${interval}`)
}

export const rateLimit = (
interval: number,
limit: number
): MiddlewareHandler<{ Bindings: Bindings; Variables: Variables }> => {
return async (ctx, next) => {
const key = getRateLimitKey(ctx)

const endpoint = new URL(ctx.req.url).pathname

const id = ctx.env.RATE_LIMITER.idFromName(key)
const rateLimiter = ctx.env.RATE_LIMITER.get(id)

const cache = await caches.open("rate-limiter")
const cacheKey = getCacheKey(endpoint, key, limit, interval)
const cached = await cache.match(cacheKey)

let res: Response

if (!cached) {
res = await rateLimiter.fetch(
new Request(fakeDomain, {
method: "POST",
body: JSON.stringify({
scope: endpoint,
key,
limit,
interval,
}),
})
)
} else {
res = cached
}

const clonedRes = res.clone()

const body = await clonedRes.json<{
blocked: boolean
remaining: number
expires: string
}>()

const secondsExpires = dayjs(body.expires).unix() - dayjs().unix()

setRateLimitHeaders(
ctx,
secondsExpires,
limit,
body.remaining,
interval
)

if (body.blocked) {
if (!cached) {
ctx.executionCtx.waitUntil(cache.put(cacheKey, res))
}

return ctx.json(
{
success: false,
message:
"Rate limit exceeded, contact [email protected] if you're using this API in production and need a higher rate limit.",
},
429
)
}
await next()
}
}
Loading

0 comments on commit 18b841c

Please sign in to comment.