2024-05-29 02:59:49 +02:00
|
|
|
import { errorResponse } from "@/response";
|
2024-05-22 02:59:03 +02:00
|
|
|
import chalk from "chalk";
|
2024-04-07 06:16:54 +02:00
|
|
|
import { config } from "config-manager";
|
2024-05-06 09:16:33 +02:00
|
|
|
import type { Context } from "hono";
|
|
|
|
|
import { createMiddleware } from "hono/factory";
|
|
|
|
|
import { validator } from "hono/validator";
|
2024-04-14 12:36:25 +02:00
|
|
|
import {
|
|
|
|
|
anyOf,
|
|
|
|
|
caseInsensitive,
|
|
|
|
|
charIn,
|
|
|
|
|
createRegExp,
|
|
|
|
|
digit,
|
|
|
|
|
exactly,
|
2024-05-13 04:27:40 +02:00
|
|
|
global,
|
2024-05-12 03:27:28 +02:00
|
|
|
letter,
|
2024-05-13 04:27:40 +02:00
|
|
|
maybe,
|
2024-05-12 03:27:28 +02:00
|
|
|
oneOrMore,
|
2024-04-14 12:36:25 +02:00
|
|
|
} from "magic-regexp";
|
2024-05-06 09:16:33 +02:00
|
|
|
import { parse } from "qs";
|
|
|
|
|
import type { z } from "zod";
|
|
|
|
|
import { fromZodError } from "zod-validation-error";
|
2024-06-13 04:26:43 +02:00
|
|
|
import type { Application } from "~/database/entities/application";
|
|
|
|
|
import { type AuthData, getFromHeader } from "~/database/entities/user";
|
2024-05-29 02:59:49 +02:00
|
|
|
import type { User } from "~/packages/database-interface/user";
|
|
|
|
|
import { LogLevel, LogManager } from "~/packages/log-manager";
|
2024-06-13 04:26:43 +02:00
|
|
|
import type { ApiRouteMetadata, HttpVerb } from "~/types/api";
|
2023-10-16 05:51:29 +02:00
|
|
|
|
2024-06-13 04:26:43 +02:00
|
|
|
export const applyConfig = (routeMeta: ApiRouteMetadata) => {
|
2024-04-07 07:30:49 +02:00
|
|
|
const newMeta = routeMeta;
|
2023-10-16 05:51:29 +02:00
|
|
|
|
2024-04-07 07:30:49 +02:00
|
|
|
// Apply ratelimits from config
|
|
|
|
|
newMeta.ratelimits.duration *= config.ratelimits.duration_coeff;
|
|
|
|
|
newMeta.ratelimits.max *= config.ratelimits.max_coeff;
|
2023-10-16 05:51:29 +02:00
|
|
|
|
2024-05-16 04:37:25 +02:00
|
|
|
if (config.ratelimits.custom[routeMeta.route]) {
|
|
|
|
|
newMeta.ratelimits = config.ratelimits.custom[routeMeta.route];
|
2024-04-07 07:30:49 +02:00
|
|
|
}
|
2023-10-16 05:51:29 +02:00
|
|
|
|
2024-04-07 07:30:49 +02:00
|
|
|
return newMeta;
|
2023-10-16 05:51:29 +02:00
|
|
|
};
|
2024-03-10 23:48:14 +01:00
|
|
|
|
2024-04-14 12:36:25 +02:00
|
|
|
export const idValidator = createRegExp(
|
|
|
|
|
anyOf(digit, charIn("ABCDEF")).times(8),
|
|
|
|
|
exactly("-"),
|
|
|
|
|
anyOf(digit, charIn("ABCDEF")).times(4),
|
|
|
|
|
exactly("-"),
|
|
|
|
|
exactly("7"),
|
|
|
|
|
anyOf(digit, charIn("ABCDEF")).times(3),
|
|
|
|
|
exactly("-"),
|
|
|
|
|
anyOf("8", "9", "A", "B").times(1),
|
|
|
|
|
anyOf(digit, charIn("ABCDEF")).times(3),
|
|
|
|
|
exactly("-"),
|
|
|
|
|
anyOf(digit, charIn("ABCDEF")).times(12),
|
|
|
|
|
[caseInsensitive],
|
|
|
|
|
);
|
2024-05-06 09:16:33 +02:00
|
|
|
|
2024-05-12 03:27:28 +02:00
|
|
|
export const emojiValidator = createRegExp(
|
|
|
|
|
// A-Z a-z 0-9 _ -
|
|
|
|
|
oneOrMore(letter.or(digit).or(exactly("_")).or(exactly("-"))),
|
2024-05-13 07:14:37 +02:00
|
|
|
[caseInsensitive, global],
|
2024-05-12 03:27:28 +02:00
|
|
|
);
|
|
|
|
|
|
2024-05-13 03:07:55 +02:00
|
|
|
export const emojiValidatorWithColons = createRegExp(
|
|
|
|
|
exactly(":"),
|
|
|
|
|
oneOrMore(letter.or(digit).or(exactly("_")).or(exactly("-"))),
|
|
|
|
|
exactly(":"),
|
2024-05-13 07:14:37 +02:00
|
|
|
[caseInsensitive, global],
|
2024-05-13 03:07:55 +02:00
|
|
|
);
|
|
|
|
|
|
2024-05-13 04:27:40 +02:00
|
|
|
export const mentionValidator = createRegExp(
|
|
|
|
|
exactly("@"),
|
|
|
|
|
oneOrMore(anyOf(letter.lowercase, digit, charIn("-"))).groupedAs(
|
|
|
|
|
"username",
|
|
|
|
|
),
|
|
|
|
|
maybe(
|
|
|
|
|
exactly("@"),
|
|
|
|
|
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs("domain"),
|
|
|
|
|
),
|
|
|
|
|
[global],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const webfingerMention = createRegExp(
|
|
|
|
|
exactly("acct:"),
|
|
|
|
|
oneOrMore(anyOf(letter, digit, charIn("-"))).groupedAs("username"),
|
|
|
|
|
maybe(
|
|
|
|
|
exactly("@"),
|
|
|
|
|
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs("domain"),
|
|
|
|
|
),
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
2024-05-06 09:16:33 +02:00
|
|
|
export const handleZodError = (
|
|
|
|
|
result:
|
|
|
|
|
| { success: true; data?: object }
|
|
|
|
|
| { success: false; error: z.ZodError<z.AnyZodObject>; data?: object },
|
2024-06-13 04:26:43 +02:00
|
|
|
_context: Context,
|
2024-05-06 09:16:33 +02:00
|
|
|
) => {
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
return errorResponse(fromZodError(result.error).message, 422);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-06-13 04:26:43 +02:00
|
|
|
const getAuth = async (value: Record<string, string>) => {
|
|
|
|
|
return value.authorization
|
|
|
|
|
? await getFromHeader(value.authorization)
|
|
|
|
|
: null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const checkPermissions = (
|
|
|
|
|
auth: AuthData | null,
|
|
|
|
|
permissionData: ApiRouteMetadata["permissions"],
|
|
|
|
|
context: Context,
|
|
|
|
|
) => {
|
|
|
|
|
const userPerms = auth?.user
|
|
|
|
|
? auth.user.getAllPermissions()
|
|
|
|
|
: config.permissions.anonymous;
|
|
|
|
|
const requiredPerms =
|
|
|
|
|
permissionData?.methodOverrides?.[context.req.method as HttpVerb] ??
|
|
|
|
|
permissionData?.required ??
|
|
|
|
|
[];
|
|
|
|
|
const error = errorResponse("Unauthorized", 401);
|
|
|
|
|
|
|
|
|
|
if (!requiredPerms.every((perm) => userPerms.includes(perm))) {
|
|
|
|
|
const missingPerms = requiredPerms.filter(
|
|
|
|
|
(perm) => !userPerms.includes(perm),
|
|
|
|
|
);
|
|
|
|
|
return context.json(
|
|
|
|
|
{
|
|
|
|
|
error: `You do not have the required permissions to access this route. Missing: ${missingPerms.join(", ")}`,
|
|
|
|
|
},
|
|
|
|
|
403,
|
|
|
|
|
error.headers.toJSON(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const checkRouteNeedsAuth = (
|
|
|
|
|
auth: AuthData | null,
|
|
|
|
|
authData: ApiRouteMetadata["auth"],
|
|
|
|
|
context: Context,
|
|
|
|
|
) => {
|
|
|
|
|
const error = errorResponse("Unauthorized", 401);
|
|
|
|
|
|
|
|
|
|
if (auth?.user) {
|
|
|
|
|
return {
|
|
|
|
|
user: auth.user as User,
|
|
|
|
|
token: auth.token as string,
|
|
|
|
|
application: auth.application as Application | null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (authData.required) {
|
|
|
|
|
return context.json(
|
|
|
|
|
{
|
|
|
|
|
error: "Unauthorized",
|
|
|
|
|
},
|
|
|
|
|
401,
|
|
|
|
|
error.headers.toJSON(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (authData.requiredOnMethods?.includes(context.req.method as HttpVerb)) {
|
|
|
|
|
return context.json(
|
|
|
|
|
{
|
|
|
|
|
error: "Unauthorized",
|
|
|
|
|
},
|
|
|
|
|
401,
|
|
|
|
|
error.headers.toJSON(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
user: null,
|
|
|
|
|
token: null,
|
|
|
|
|
application: null,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2024-06-08 06:57:29 +02:00
|
|
|
export const auth = (
|
2024-06-13 04:26:43 +02:00
|
|
|
authData: ApiRouteMetadata["auth"],
|
|
|
|
|
permissionData?: ApiRouteMetadata["permissions"],
|
|
|
|
|
) =>
|
|
|
|
|
validator("header", async (value, context) => {
|
|
|
|
|
const auth = await getAuth(value);
|
|
|
|
|
|
|
|
|
|
// Permissions check
|
|
|
|
|
if (permissionData) {
|
|
|
|
|
const permissionCheck = checkPermissions(
|
|
|
|
|
auth,
|
|
|
|
|
permissionData,
|
|
|
|
|
context,
|
|
|
|
|
);
|
|
|
|
|
if (permissionCheck) {
|
|
|
|
|
return permissionCheck;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return checkRouteNeedsAuth(auth, authData, context);
|
|
|
|
|
});
|
|
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
// Helper function to parse form data
|
|
|
|
|
async function parseFormData(context: Context) {
|
|
|
|
|
const formData = await context.req.formData();
|
|
|
|
|
const urlparams = new URLSearchParams();
|
|
|
|
|
const files = new Map<string, File>();
|
|
|
|
|
for (const [key, value] of [...formData.entries()]) {
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
for (const val of value) {
|
|
|
|
|
urlparams.append(key, val);
|
2024-06-08 06:57:29 +02:00
|
|
|
}
|
2024-06-13 06:16:59 +02:00
|
|
|
} else if (value instanceof File) {
|
|
|
|
|
if (!files.has(key)) {
|
|
|
|
|
files.set(key, value);
|
2024-05-08 13:16:16 +02:00
|
|
|
}
|
2024-06-13 06:16:59 +02:00
|
|
|
} else {
|
|
|
|
|
urlparams.append(key, String(value));
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-08 13:16:16 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
const parsed = parse(urlparams.toString(), {
|
|
|
|
|
parseArrays: true,
|
|
|
|
|
interpretNumericEntities: true,
|
|
|
|
|
});
|
2024-05-08 13:16:16 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
return {
|
|
|
|
|
parsed,
|
|
|
|
|
files,
|
|
|
|
|
};
|
|
|
|
|
}
|
2024-05-08 13:16:16 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
// Helper function to parse urlencoded data
|
|
|
|
|
async function parseUrlEncoded(context: Context) {
|
|
|
|
|
const parsed = parse(await context.req.text(), {
|
|
|
|
|
parseArrays: true,
|
|
|
|
|
interpretNumericEntities: true,
|
2024-05-06 09:16:33 +02:00
|
|
|
});
|
2024-06-13 06:16:59 +02:00
|
|
|
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
2024-05-06 09:16:33 +02:00
|
|
|
|
|
|
|
|
export const qsQuery = () => {
|
|
|
|
|
return createMiddleware(async (context, next) => {
|
|
|
|
|
const parsed = parse(context.req.query(), {
|
|
|
|
|
parseArrays: true,
|
|
|
|
|
interpretNumericEntities: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// @ts-ignore Very bad hack
|
|
|
|
|
context.req.query = () => parsed;
|
2024-06-13 06:16:59 +02:00
|
|
|
|
2024-05-06 10:19:42 +02:00
|
|
|
// @ts-ignore I'm so sorry for this
|
|
|
|
|
context.req.queries = () => parsed;
|
2024-05-06 09:16:33 +02:00
|
|
|
await next();
|
|
|
|
|
});
|
|
|
|
|
};
|
2024-05-06 10:31:12 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
export const setContextFormDataToObject = (
|
|
|
|
|
context: Context,
|
|
|
|
|
setTo: object,
|
|
|
|
|
): Context => {
|
|
|
|
|
// @ts-expect-error HACK
|
|
|
|
|
context.req.bodyCache.formData = setTo;
|
|
|
|
|
context.req.parseBody = async () =>
|
|
|
|
|
context.req.bodyCache.formData as FormData;
|
|
|
|
|
context.req.formData = async () =>
|
|
|
|
|
context.req.bodyCache.formData as FormData;
|
|
|
|
|
|
|
|
|
|
return context;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Middleware to magically unfuck forms
|
|
|
|
|
* Add it to random Hono routes and hope it works
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2024-05-06 10:31:12 +02:00
|
|
|
export const jsonOrForm = () => {
|
|
|
|
|
return createMiddleware(async (context, next) => {
|
|
|
|
|
const contentType = context.req.header("content-type");
|
|
|
|
|
|
|
|
|
|
if (contentType?.includes("application/json")) {
|
2024-06-13 06:16:59 +02:00
|
|
|
setContextFormDataToObject(context, await context.req.json());
|
2024-05-06 10:31:12 +02:00
|
|
|
} else if (contentType?.includes("application/x-www-form-urlencoded")) {
|
2024-06-13 06:16:59 +02:00
|
|
|
const parsed = await parseUrlEncoded(context);
|
2024-05-06 10:31:12 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
setContextFormDataToObject(context, parsed);
|
2024-05-12 03:27:28 +02:00
|
|
|
} else if (contentType?.includes("multipart/form-data")) {
|
2024-06-13 06:16:59 +02:00
|
|
|
const { parsed, files } = await parseFormData(context);
|
2024-05-12 03:27:28 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
setContextFormDataToObject(context, {
|
2024-05-12 03:27:28 +02:00
|
|
|
...parsed,
|
|
|
|
|
...Object.fromEntries(files),
|
2024-06-13 06:16:59 +02:00
|
|
|
});
|
2024-05-06 10:31:12 +02:00
|
|
|
}
|
2024-06-08 05:31:17 +02:00
|
|
|
|
2024-05-06 10:31:12 +02:00
|
|
|
await next();
|
|
|
|
|
});
|
|
|
|
|
};
|
2024-05-22 02:59:03 +02:00
|
|
|
|
2024-05-22 03:23:48 +02:00
|
|
|
export const debugRequest = async (
|
|
|
|
|
req: Request,
|
|
|
|
|
logger = new LogManager(Bun.stdout),
|
|
|
|
|
) => {
|
2024-05-22 02:59:03 +02:00
|
|
|
const body = await req.clone().text();
|
|
|
|
|
await logger.log(
|
2024-06-13 04:26:43 +02:00
|
|
|
LogLevel.Debug,
|
2024-05-22 02:59:03 +02:00
|
|
|
"RequestDebugger",
|
|
|
|
|
`\n${chalk.green(req.method)} ${chalk.blue(req.url)}\n${chalk.bold(
|
|
|
|
|
"Hash",
|
|
|
|
|
)}: ${chalk.yellow(
|
|
|
|
|
new Bun.SHA256().update(body).digest("hex"),
|
|
|
|
|
)}\n${chalk.bold("Headers")}:\n${Array.from(req.headers.entries())
|
|
|
|
|
.map(
|
|
|
|
|
([key, value]) =>
|
|
|
|
|
` - ${chalk.cyan(key)}: ${chalk.white(value)}`,
|
|
|
|
|
)
|
|
|
|
|
.join("\n")}\n${chalk.bold("Body")}: ${chalk.gray(body)}`,
|
|
|
|
|
);
|
|
|
|
|
};
|