mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
refactor(config): ♻️ Redo config structure from scratch, simplify validation code, improve checks, add support for loading sensitive data from paths
This commit is contained in:
parent
d4afd84019
commit
54fd81f076
118 changed files with 3892 additions and 5291 deletions
10
utils/api.ts
10
utils/api.ts
|
|
@ -28,7 +28,7 @@ import { type ParsedQs, parse } from "qs";
|
|||
import { fromZodError } from "zod-validation-error";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import type { AuthData } from "~/classes/functions/user";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { config } from "~/config.ts";
|
||||
import { ErrorSchema, type HonoEnv } from "~/types/api";
|
||||
|
||||
export const reusedResponses = {
|
||||
|
|
@ -229,7 +229,7 @@ export const checkRouteNeedsChallenge = async (
|
|||
required: boolean,
|
||||
context: Context,
|
||||
): Promise<void> => {
|
||||
if (!required) {
|
||||
if (!(required && config.validation.challenges)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -325,7 +325,7 @@ export const auth = <AuthRequired extends boolean>(options: {
|
|||
}
|
||||
|
||||
// Challenge check
|
||||
if (options.challenge && config.validation.challenges.enabled) {
|
||||
if (options.challenge && config.validation.challenges) {
|
||||
await checkRouteNeedsChallenge(options.challenge, context);
|
||||
}
|
||||
|
||||
|
|
@ -573,7 +573,7 @@ export const debugRequest = async (req: Request): Promise<void> => {
|
|||
|
||||
const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`;
|
||||
|
||||
if (config.logging.log_requests_verbose) {
|
||||
if (config.logging.types.requests_content) {
|
||||
logger.debug`${urlAndMethod}\n${hash}\n${headers}\n${bodyLog}`;
|
||||
} else {
|
||||
logger.debug`${urlAndMethod}`;
|
||||
|
|
@ -594,7 +594,7 @@ export const debugResponse = async (res: Response): Promise<void> => {
|
|||
|
||||
const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`;
|
||||
|
||||
if (config.logging.log_requests_verbose) {
|
||||
if (config.logging.types.requests_content) {
|
||||
logger.debug`${status}\n${headers}\n${bodyLog}`;
|
||||
} else {
|
||||
logger.debug`${status}`;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import { fetchQueue } from "~/classes/queues/fetch";
|
|||
import { inboxQueue } from "~/classes/queues/inbox";
|
||||
import { mediaQueue } from "~/classes/queues/media";
|
||||
import { pushQueue } from "~/classes/queues/push";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { config } from "~/config.ts";
|
||||
import pkg from "~/package.json";
|
||||
import type { HonoEnv } from "~/types/api";
|
||||
|
||||
export const applyToHono = (app: OpenAPIHono<HonoEnv>): void => {
|
||||
|
|
@ -31,9 +32,7 @@ export const applyToHono = (app: OpenAPIHono<HonoEnv>): void => {
|
|||
alternative: "/favicon.ico",
|
||||
},
|
||||
boardLogo: {
|
||||
path:
|
||||
config.instance.logo?.toString() ??
|
||||
"https://cdn.versia.pub/branding/icon.svg",
|
||||
path: config.instance.branding.logo?.origin ?? pkg.icon,
|
||||
height: 40,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,16 +3,20 @@ import { Challenges } from "@versia/kit/tables";
|
|||
import { createChallenge } from "altcha-lib";
|
||||
import type { Challenge } from "altcha-lib/types";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
export const generateChallenge = async (
|
||||
maxNumber = config.validation.challenges.difficulty,
|
||||
maxNumber?: number,
|
||||
): Promise<{
|
||||
id: string;
|
||||
challenge: Challenge;
|
||||
expiresAt: string;
|
||||
createdAt: string;
|
||||
}> => {
|
||||
if (!config.validation.challenges) {
|
||||
throw new Error("Challenges are not enabled");
|
||||
}
|
||||
|
||||
const expirationDate = new Date(
|
||||
Date.now() + config.validation.challenges.expiration * 1000,
|
||||
);
|
||||
|
|
@ -23,7 +27,7 @@ export const generateChallenge = async (
|
|||
const challenge = await createChallenge({
|
||||
hmacKey: config.validation.challenges.key,
|
||||
expires: expirationDate,
|
||||
maxNumber,
|
||||
maxNumber: maxNumber ?? config.validation.challenges.difficulty,
|
||||
algorithm: "SHA-256",
|
||||
params: {
|
||||
challenge_id: uuid,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
export const localObjectUri = (id: string): URL =>
|
||||
new URL(`/objects/${id}`, config.http.base_url);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ContentFormat } from "@versia/federation/types";
|
||||
import { htmlToText as htmlToTextLib } from "html-to-text";
|
||||
import { lookup } from "mime-types";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
export const getBestContentType = (
|
||||
content?: ContentFormat | null,
|
||||
|
|
@ -67,7 +67,7 @@ export const mimeLookup = (url: URL): Promise<string> => {
|
|||
const fetchLookup = fetch(url, {
|
||||
method: "HEAD",
|
||||
// @ts-expect-error Proxy is a Bun-specific feature
|
||||
proxy: config.http.proxy.address,
|
||||
proxy: config.http.proxy_address,
|
||||
})
|
||||
.then(
|
||||
(response) =>
|
||||
|
|
|
|||
152
utils/init.ts
152
utils/init.ts
|
|
@ -1,152 +0,0 @@
|
|||
import { getLogger } from "@logtape/logtape";
|
||||
import { User } from "@versia/kit/db";
|
||||
import chalk from "chalk";
|
||||
import { generateVAPIDKeys } from "web-push";
|
||||
import type { Config } from "~/packages/config-manager";
|
||||
|
||||
export const checkConfig = async (config: Config): Promise<void> => {
|
||||
await checkFederationConfig(config);
|
||||
|
||||
await checkHttpProxyConfig(config);
|
||||
|
||||
await checkChallengeConfig(config);
|
||||
|
||||
await checkVapidConfig(config);
|
||||
};
|
||||
|
||||
const checkHttpProxyConfig = async (config: Config): Promise<void> => {
|
||||
const logger = getLogger("server");
|
||||
|
||||
if (config.http.proxy.enabled) {
|
||||
logger.info`HTTP proxy enabled at ${chalk.gray(config.http.proxy.address)}, testing...`;
|
||||
|
||||
// Test the proxy
|
||||
const response = await fetch("https://api.ipify.org?format=json", {
|
||||
// @ts-expect-error Proxy is a Bun-specific feature
|
||||
proxy: config.http.proxy.address,
|
||||
});
|
||||
|
||||
const ip = (await response.json()).ip;
|
||||
|
||||
logger.info`Your IPv4 address is ${chalk.gray(ip)}`;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
"The HTTP proxy is enabled, but the proxy address is not reachable",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkChallengeConfig = async (config: Config): Promise<void> => {
|
||||
const logger = getLogger("server");
|
||||
|
||||
if (
|
||||
config.validation.challenges.enabled &&
|
||||
!config.validation.challenges.key
|
||||
) {
|
||||
logger.fatal`Challenges are enabled, but the challenge key is not set in the config`;
|
||||
logger.fatal`Below is a generated key for you to copy in the config at validation.challenges.key`;
|
||||
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: "HMAC",
|
||||
hash: "SHA-256",
|
||||
},
|
||||
true,
|
||||
["sign"],
|
||||
);
|
||||
|
||||
const exported = await crypto.subtle.exportKey("raw", key);
|
||||
|
||||
const base64 = Buffer.from(exported).toString("base64");
|
||||
|
||||
logger.fatal`Generated key: ${chalk.gray(base64)}`;
|
||||
|
||||
// Hang until Ctrl+C is pressed
|
||||
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||
}
|
||||
};
|
||||
|
||||
const checkFederationConfig = async (config: Config): Promise<void> => {
|
||||
const logger = getLogger("server");
|
||||
|
||||
if (!(config.instance.keys.public && config.instance.keys.private)) {
|
||||
logger.fatal`The federation keys are not set in the config`;
|
||||
logger.fatal`Below are generated keys for you to copy in the config at instance.keys.public and instance.keys.private`;
|
||||
|
||||
// Generate a key for them
|
||||
const { public_key, private_key } = await User.generateKeys();
|
||||
|
||||
logger.fatal`Generated public key: ${chalk.gray(public_key)}`;
|
||||
logger.fatal`Generated private key: ${chalk.gray(private_key)}`;
|
||||
|
||||
// Hang until Ctrl+C is pressed
|
||||
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||
}
|
||||
|
||||
// Try and import the key
|
||||
const privateKey = await crypto.subtle
|
||||
.importKey(
|
||||
"pkcs8",
|
||||
Buffer.from(config.instance.keys.private, "base64"),
|
||||
"Ed25519",
|
||||
false,
|
||||
["sign"],
|
||||
)
|
||||
.catch((e) => e as Error);
|
||||
|
||||
// Try and import the key
|
||||
const publicKey = await crypto.subtle
|
||||
.importKey(
|
||||
"spki",
|
||||
Buffer.from(config.instance.keys.public, "base64"),
|
||||
"Ed25519",
|
||||
false,
|
||||
["verify"],
|
||||
)
|
||||
.catch((e) => e as Error);
|
||||
|
||||
if (privateKey instanceof Error || publicKey instanceof Error) {
|
||||
throw new Error(
|
||||
"The federation keys could not be imported! You may generate new ones by removing the old ones from the config and restarting the server.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const checkVapidConfig = async (config: Config): Promise<void> => {
|
||||
const logger = getLogger("server");
|
||||
|
||||
if (
|
||||
config.notifications.push.enabled &&
|
||||
!(
|
||||
config.notifications.push.vapid.public ||
|
||||
config.notifications.push.vapid.private
|
||||
)
|
||||
) {
|
||||
logger.fatal`The VAPID keys are not set in the config, but push notifications are enabled.`;
|
||||
logger.fatal`Below are generated keys for you to copy in the config at notifications.push.vapid`;
|
||||
|
||||
const { privateKey, publicKey } = await generateVAPIDKeys();
|
||||
|
||||
logger.fatal`Generated public key: ${chalk.gray(publicKey)}`;
|
||||
logger.fatal`Generated private key: ${chalk.gray(privateKey)}`;
|
||||
|
||||
// Hang until Ctrl+C is pressed
|
||||
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||
}
|
||||
|
||||
// These use a format I don't understand, so I'm just going to check the length
|
||||
const validateKey = (key: string): boolean => key.length > 10;
|
||||
|
||||
if (
|
||||
!(
|
||||
validateKey(config.notifications.push.vapid.public) &&
|
||||
validateKey(config.notifications.push.vapid.private)
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
"The VAPID keys could not be imported! You may generate new ones by removing the old ones from the config and restarting the server.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -19,7 +19,7 @@ import {
|
|||
getLevelFilter,
|
||||
} from "@logtape/logtape";
|
||||
import chalk from "chalk";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
// HACK: This is a workaround for the lack of type exports in the Logtape package.
|
||||
// biome-ignore format: Biome formatter bug
|
||||
|
|
@ -156,7 +156,7 @@ export const configureLoggers = (silent = false): Promise<void> =>
|
|||
console: getConsoleSink({
|
||||
formatter: defaultConsoleFormatter,
|
||||
}),
|
||||
file: getBaseRotatingFileSink(config.logging.storage.requests, {
|
||||
file: getBaseRotatingFileSink(config.logging.log_file_path, {
|
||||
maxFiles: 10,
|
||||
maxSize: 10 * 1024 * 1024,
|
||||
formatter: defaultTextFormatter,
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
import { getLogger } from "@logtape/logtape";
|
||||
import { markdownParse } from "~/classes/functions/status";
|
||||
import { sentry } from "./sentry.ts";
|
||||
|
||||
export const renderMarkdownInPath = async (
|
||||
path: string,
|
||||
defaultText?: string,
|
||||
): Promise<{
|
||||
content: string;
|
||||
lastModified: Date;
|
||||
}> => {
|
||||
let content = await markdownParse(defaultText ?? "");
|
||||
let lastModified = new Date(1970, 0, 0);
|
||||
|
||||
const extendedDescriptionFile = Bun.file(path || "");
|
||||
|
||||
if (path && (await extendedDescriptionFile.exists())) {
|
||||
content =
|
||||
(await markdownParse(
|
||||
(await extendedDescriptionFile.text().catch(async (e) => {
|
||||
await getLogger("server").error`${e}`;
|
||||
sentry?.captureException(e);
|
||||
return "";
|
||||
})) ||
|
||||
defaultText ||
|
||||
"",
|
||||
)) || "";
|
||||
lastModified = new Date(extendedDescriptionFile.lastModified);
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
lastModified,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import IORedis from "ioredis";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
export const connection = new IORedis({
|
||||
host: config.redis.queue.host,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { config } from "~/packages/config-manager";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
export type Json =
|
||||
| string
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import * as Sentry from "@sentry/bun";
|
||||
import { config } from "~/config.ts";
|
||||
import pkg from "~/package.json";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
|
||||
const sentryInstance =
|
||||
config.logging.sentry.enabled &&
|
||||
config.logging.sentry &&
|
||||
Sentry.init({
|
||||
dsn: config.logging.sentry.dsn,
|
||||
dsn: config.logging.sentry.dsn.origin,
|
||||
debug: config.logging.sentry.debug,
|
||||
sampleRate: config.logging.sentry.sample_rate,
|
||||
maxBreadcrumbs: config.logging.sentry.max_breadcrumbs,
|
||||
|
|
|
|||
|
|
@ -1,31 +1,29 @@
|
|||
import type { OpenAPIHono } from "@hono/zod-openapi";
|
||||
import type { OpenAPIHono, z } from "@hono/zod-openapi";
|
||||
import type { Server } from "bun";
|
||||
import type { Config } from "~/packages/config-manager/config.type";
|
||||
import type { ConfigSchema } from "~/classes/config/schema.ts";
|
||||
import type { HonoEnv } from "~/types/api";
|
||||
import { debugResponse } from "./api.ts";
|
||||
|
||||
export const createServer = (
|
||||
config: Config,
|
||||
config: z.infer<typeof ConfigSchema>,
|
||||
app: OpenAPIHono<HonoEnv>,
|
||||
): Server =>
|
||||
Bun.serve({
|
||||
port: config.http.bind_port,
|
||||
reusePort: true,
|
||||
tls: config.http.tls.enabled
|
||||
tls: config.http.tls
|
||||
? {
|
||||
key: Bun.file(config.http.tls.key),
|
||||
cert: Bun.file(config.http.tls.cert),
|
||||
key: config.http.tls.key.file,
|
||||
cert: config.http.tls.cert.file,
|
||||
passphrase: config.http.tls.passphrase,
|
||||
ca: config.http.tls.ca
|
||||
? Bun.file(config.http.tls.ca)
|
||||
: undefined,
|
||||
ca: config.http.tls.ca?.file,
|
||||
}
|
||||
: undefined,
|
||||
hostname: config.http.bind || "0.0.0.0", // defaults to "0.0.0.0"
|
||||
hostname: config.http.bind,
|
||||
async fetch(req, server): Promise<Response> {
|
||||
const output = await app.fetch(req, { ip: server.requestIP(req) });
|
||||
|
||||
if (config.logging.log_responses) {
|
||||
if (config.logging.types.responses) {
|
||||
await debugResponse(output.clone());
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue