mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 00:18:19 +01:00
feat(api): ✨ Implement rate limiting
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 42s
Build Docker Images / lint (push) Successful in 31s
Build Docker Images / check (push) Successful in 1m3s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 13s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 33m18s
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 42s
Build Docker Images / lint (push) Successful in 31s
Build Docker Images / check (push) Successful in 1m3s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 13s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 33m18s
This commit is contained in:
parent
1993231663
commit
3d3e64edab
|
|
@ -9,6 +9,7 @@ import { User, db } from "@versia/kit/db";
|
|||
import type { Users } from "@versia/kit/tables";
|
||||
import { type InferSelectModel, sql } from "drizzle-orm";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
|
|
@ -26,6 +27,7 @@ const route = createRoute({
|
|||
scopes: ["read:follows"],
|
||||
permissions: [RolePermission.ManageOwnFollows],
|
||||
}),
|
||||
rateLimit(5),
|
||||
qsQuery(),
|
||||
] as const,
|
||||
request: {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { and, eq, isNull } from "drizzle-orm";
|
|||
import ISO6391 from "iso-639-1";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config.ts";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
const schema = z.object({
|
||||
username: z.string().openapi({
|
||||
|
|
@ -55,6 +56,7 @@ const route = createRoute({
|
|||
scopes: ["write:accounts"],
|
||||
challenge: true,
|
||||
}),
|
||||
rateLimit(5),
|
||||
jsonOrForm(),
|
||||
] as const,
|
||||
request: {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Users } from "@versia/kit/tables";
|
|||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config.ts";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
|
|
@ -19,6 +20,7 @@ const route = createRoute({
|
|||
auth: false,
|
||||
permissions: [RolePermission.Search],
|
||||
}),
|
||||
rateLimit(5),
|
||||
] as const,
|
||||
request: {
|
||||
query: z.object({
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
|
|
@ -20,6 +21,7 @@ const route = createRoute({
|
|||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
rateLimit(10),
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["read:follows"],
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Users } from "@versia/kit/tables";
|
|||
import { eq, ilike, not, or, sql } from "drizzle-orm";
|
||||
import stringComparison from "string-comparison";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
export const route = createRoute({
|
||||
method: "get",
|
||||
|
|
@ -18,6 +19,7 @@ export const route = createRoute({
|
|||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
rateLimit(5),
|
||||
auth({
|
||||
auth: false,
|
||||
permissions: [RolePermission.Search, RolePermission.ViewAccounts],
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { and, eq, isNull } from "drizzle-orm";
|
|||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { contentToHtml } from "~/classes/functions/status";
|
||||
import { config } from "~/config.ts";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
const route = createRoute({
|
||||
method: "patch",
|
||||
|
|
@ -21,6 +22,7 @@ const route = createRoute({
|
|||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
rateLimit(5),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnAccount],
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from "@versia/client/schemas";
|
||||
import { Application } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
|
|
@ -17,7 +18,7 @@ const route = createRoute({
|
|||
url: "https://docs.joinmastodon.org/methods/apps/#create",
|
||||
},
|
||||
tags: ["Apps"],
|
||||
middleware: [jsonOrForm()],
|
||||
middleware: [jsonOrForm(), rateLimit(4)],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Emojis } from "@versia/kit/tables";
|
|||
import { and, eq, isNull, or } from "drizzle-orm";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
const schema = z.object({
|
||||
shortcode: CustomEmojiSchema.shape.shortcode,
|
||||
element: z
|
||||
|
|
|
|||
9
app.ts
9
app.ts
|
|
@ -22,6 +22,7 @@ import { agentBans } from "./middlewares/agent-bans.ts";
|
|||
import { boundaryCheck } from "./middlewares/boundary-check.ts";
|
||||
import { ipBans } from "./middlewares/ip-bans.ts";
|
||||
import { logger } from "./middlewares/logger.ts";
|
||||
import { rateLimit } from "./middlewares/rate-limit.ts";
|
||||
import { routes } from "./routes.ts";
|
||||
import type { ApiRouteExports, HonoEnv } from "./types/api.ts";
|
||||
|
||||
|
|
@ -87,6 +88,14 @@ export const appFactory = async (): Promise<OpenAPIHono<HonoEnv>> => {
|
|||
}),
|
||||
);
|
||||
|
||||
// Set default ratelimits to 100 requests per minute
|
||||
app.use("/api/*", rateLimit(100));
|
||||
app.use("/api/v1/media", rateLimit(40));
|
||||
app.use("/api/v1/media/*", rateLimit(40));
|
||||
app.use("/api/v2/media", rateLimit(40));
|
||||
app.use("/api/v1/timelines/*", rateLimit(40));
|
||||
app.use("/api/v1/push/*", rateLimit(10));
|
||||
|
||||
/* app.use("*", registerMetrics);
|
||||
app.get("/metrics", printMetrics); */
|
||||
// Disabled as federation now checks for this
|
||||
|
|
|
|||
3
bun.lock
3
bun.lock
|
|
@ -32,6 +32,7 @@
|
|||
"confbox": "^0.2.1",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
"hono": "^4.7.5",
|
||||
"hono-rate-limiter": "^0.4.2",
|
||||
"html-to-text": "^9.0.5",
|
||||
"ioredis": "^5.6.0",
|
||||
"ip-matching": "^2.1.2",
|
||||
|
|
@ -854,6 +855,8 @@
|
|||
|
||||
"hono": ["hono@4.7.5", "", {}, "sha512-fDOK5W2C1vZACsgLONigdZTRZxuBqFtcKh7bUQ5cVSbwI2RWjloJDcgFOVzbQrlI6pCmhlTsVYZ7zpLj4m4qMQ=="],
|
||||
|
||||
"hono-rate-limiter": ["hono-rate-limiter@0.4.2", "", { "peerDependencies": { "hono": "^4.1.1" } }, "sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"html-to-text": ["html-to-text@9.0.5", "", { "dependencies": { "@selderee/plugin-htmlparser2": "^0.11.0", "deepmerge": "^4.3.1", "dom-serializer": "^2.0.0", "htmlparser2": "^8.0.2", "selderee": "^0.11.0" } }, "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg=="],
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@ export class ApiError extends Error {
|
|||
|
||||
public static zodSchema = z.object({
|
||||
error: z.string(),
|
||||
details: z.string().or(z.record(z.string(), z.string())).optional(),
|
||||
details: z
|
||||
.string()
|
||||
.or(z.record(z.string(), z.string().or(z.number())))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
public get schema(): ResponseConfig {
|
||||
|
|
|
|||
40
middlewares/rate-limit.ts
Normal file
40
middlewares/rate-limit.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { z } from "@hono/zod-openapi";
|
||||
import { env } from "bun";
|
||||
import type { MiddlewareHandler } from "hono";
|
||||
import { rateLimiter } from "hono-rate-limiter";
|
||||
import type { ApiError } from "~/classes/errors/api-error";
|
||||
import type { HonoEnv } from "~/types/api";
|
||||
|
||||
// Not exported by hono-rate-limiter
|
||||
// So we define it ourselves
|
||||
type RateLimitEnv = HonoEnv & {
|
||||
Variables: {
|
||||
rateLimit: {
|
||||
limit: number;
|
||||
remaining: number;
|
||||
resetTime: Date;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const rateLimit = (
|
||||
limit: number,
|
||||
windowMs = 60 * 1000,
|
||||
): MiddlewareHandler<RateLimitEnv> =>
|
||||
env.DISABLE_RATE_LIMIT === "true"
|
||||
? (_, next): Promise<void> => next()
|
||||
: rateLimiter<RateLimitEnv>({
|
||||
keyGenerator: (c): string => c.req.path,
|
||||
message: (c): z.infer<typeof ApiError.zodSchema> => ({
|
||||
error: "Too many requests, please try again later.",
|
||||
details: {
|
||||
limit: c.get("rateLimit").limit,
|
||||
remaining: c.get("rateLimit").remaining,
|
||||
reset: c.get("rateLimit").resetTime.toISOString(),
|
||||
resetInMs:
|
||||
c.get("rateLimit").resetTime.getTime() - Date.now(),
|
||||
},
|
||||
}),
|
||||
windowMs,
|
||||
limit,
|
||||
});
|
||||
|
|
@ -104,6 +104,7 @@
|
|||
"confbox": "^0.2.1",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
"hono": "^4.7.5",
|
||||
"hono-rate-limiter": "^0.4.2",
|
||||
"html-to-text": "^9.0.5",
|
||||
"ioredis": "^5.6.0",
|
||||
"ip-matching": "^2.1.2",
|
||||
|
|
|
|||
|
|
@ -5,11 +5,14 @@ import { Client as VersiaClient } from "@versia/client";
|
|||
import { Note, Token, User, db } from "@versia/kit/db";
|
||||
import { Notes, Users } from "@versia/kit/tables";
|
||||
import { solveChallenge } from "altcha-lib";
|
||||
import { env } from "bun";
|
||||
import { type InferSelectModel, asc, inArray, like } from "drizzle-orm";
|
||||
import { appFactory } from "~/app";
|
||||
import { searchManager } from "~/classes/search/search-manager";
|
||||
import { config } from "~/config.ts";
|
||||
import { setupDatabase } from "~/drizzle/db";
|
||||
|
||||
env.DISABLE_RATE_LIMIT = "true";
|
||||
await setupDatabase();
|
||||
|
||||
if (config.search.enabled) {
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ export const checkRouteNeedsChallenge = async (
|
|||
.where(eq(Challenges.id, challenge_id));
|
||||
};
|
||||
|
||||
type HonoEnvWithAuth = HonoEnv & {
|
||||
export type HonoEnvWithAuth = HonoEnv & {
|
||||
Variables: {
|
||||
auth: AuthData & {
|
||||
user: NonNullable<AuthData["user"]>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue