diff --git a/cli/commands/emoji/add.ts b/cli/commands/emoji/add.ts index ff8a9dd8..386b4166 100644 --- a/cli/commands/emoji/add.ts +++ b/cli/commands/emoji/add.ts @@ -63,6 +63,7 @@ export default class EmojiAdd extends BaseCommand { headers: { "Accept-Encoding": "identity", }, + proxy: config.http.proxy.address, }); if (!response.ok) { diff --git a/cli/commands/emoji/import.ts b/cli/commands/emoji/import.ts index 19fa5a95..bf6f6e17 100644 --- a/cli/commands/emoji/import.ts +++ b/cli/commands/emoji/import.ts @@ -70,6 +70,7 @@ export default class EmojiImport extends BaseCommand { headers: { "Accept-Encoding": "identity", }, + proxy: config.http.proxy.address, }); if (!response.ok) { diff --git a/config/config.example.toml b/config/config.example.toml index a69a338b..5c30e270 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -84,13 +84,10 @@ banned_user_agents = [ ] [http.proxy] -# For SOCKS proxies (e.g. Tor) +# For HTTP proxies (e.g. Tor proxies) # Will be used for all outgoing requests enabled = false -host = "127.0.0.1" -port = 9050 -# Can be socks4, socks4a or socks5 -type = "socks5" +address = "http://localhost:8118" [http.tls] # If these values are set, Lysand will use these files for TLS diff --git a/database/entities/instance.ts b/database/entities/instance.ts index 2b12133f..9b762ce7 100644 --- a/database/entities/instance.ts +++ b/database/entities/instance.ts @@ -1,6 +1,7 @@ import type { ServerMetadata } from "@lysand-org/federation/types"; import { db } from "~/drizzle/db"; import { Instances } from "~/drizzle/schema"; +import { config } from "~/packages/config-manager"; /** * Represents an instance in the database. @@ -24,9 +25,9 @@ export const addInstanceIfNotExists = async (url: string) => { } // Fetch the instance configuration - const metadata = (await fetch(new URL("/.well-known/lysand", origin)).then( - (res) => res.json(), - )) as ServerMetadata; + const metadata = (await fetch(new URL("/.well-known/lysand", origin), { + proxy: config.http.proxy.address, + }).then((res) => res.json())) as ServerMetadata; if (metadata.type !== "ServerMetadata") { throw new Error("Invalid instance metadata (wrong type)"); diff --git a/database/entities/status.ts b/database/entities/status.ts index b387b968..cddea61d 100644 --- a/database/entities/status.ts +++ b/database/entities/status.ts @@ -448,7 +448,9 @@ export const federateNote = async (note: Note) => { ); // Send request - const response = await fetch(request); + const response = await fetch(request, { + proxy: config.http.proxy.address, + }); if (!response.ok) { dualLogger.log( diff --git a/database/entities/user.ts b/database/entities/user.ts index 073491df..79452960 100644 --- a/database/entities/user.ts +++ b/database/entities/user.ts @@ -175,7 +175,9 @@ export const followRequestUser = async ( ); // Send request - const response = await fetch(request); + const response = await fetch(request, { + proxy: config.http.proxy.address, + }); if (!response.ok) { dualLogger.log( @@ -230,7 +232,9 @@ export const sendFollowAccept = async (follower: User, followee: User) => { ); // Send request - const response = await fetch(request); + const response = await fetch(request, { + proxy: config.http.proxy.address, + }); if (!response.ok) { dualLogger.log( @@ -258,7 +262,9 @@ export const sendFollowReject = async (follower: User, followee: User) => { ); // Send request - const response = await fetch(request); + const response = await fetch(request, { + proxy: config.http.proxy.address, + }); if (!response.ok) { dualLogger.log( @@ -375,6 +381,7 @@ export const resolveWebFinger = async ( headers: { Accept: "application/json", }, + proxy: config.http.proxy.address, }, ); diff --git a/index.ts b/index.ts index 94c8af3c..09b4d756 100644 --- a/index.ts +++ b/index.ts @@ -1,7 +1,7 @@ +import { checkConfig } from "@/init"; import { dualLogger } from "@/loggers"; import { connectMeili } from "@/meilisearch"; import { errorResponse, response } from "@/response"; -import chalk from "chalk"; import { config } from "config-manager"; import { Hono } from "hono"; import { LogLevel, LogManager, type MultiLogManager } from "log-manager"; @@ -46,113 +46,7 @@ process.on("SIGINT", () => { const postCount = await Note.getCount(); if (isEntry) { - // Check if JWT private key is set in config - if (!config.oidc.jwt_key) { - await dualServerLogger.log( - LogLevel.Critical, - "Server", - "The JWT private key is not set in the config", - ); - await dualServerLogger.log( - LogLevel.Critical, - "Server", - "Below is a generated key for you to copy in the config at oidc.jwt_key", - ); - // Generate a key for them - const keys = await crypto.subtle.generateKey("Ed25519", true, [ - "sign", - "verify", - ]); - - const privateKey = Buffer.from( - await crypto.subtle.exportKey("pkcs8", keys.privateKey), - ).toString("base64"); - - const publicKey = Buffer.from( - await crypto.subtle.exportKey("spki", keys.publicKey), - ).toString("base64"); - - await dualServerLogger.log( - LogLevel.Critical, - "Server", - chalk.gray(`${privateKey};${publicKey}`), - ); - - // 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.oidc.jwt_key.split(";")[0], "base64"), - "Ed25519", - false, - ["sign"], - ) - .catch((e) => e as Error); - - // Try and import the key - const publicKey = await crypto.subtle - .importKey( - "spki", - Buffer.from(config.oidc.jwt_key.split(";")[1], "base64"), - "Ed25519", - false, - ["verify"], - ) - .catch((e) => e as Error); - - if (privateKey instanceof Error || publicKey instanceof Error) { - await dualServerLogger.log( - LogLevel.Critical, - "Server", - "The JWT key could not be imported! You may generate a new one by removing the old one from the config and restarting the server (this will invalidate all current JWTs).", - ); - - // Hang until Ctrl+C is pressed - await Bun.sleep(Number.POSITIVE_INFINITY); - } - - if ( - config.validation.challenges.enabled && - !config.validation.challenges.key - ) { - await dualServerLogger.log( - LogLevel.Critical, - "Server", - "Challenges are enabled, but the challenge key is not set in the config", - ); - - await dualServerLogger.log( - LogLevel.Critical, - "Server", - "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"); - - await dualServerLogger.log( - LogLevel.Critical, - "Server", - `Generated key: ${chalk.gray(base64)}`, - ); - - // Hang until Ctrl+C is pressed - await Bun.sleep(Number.POSITIVE_INFINITY); - } + await checkConfig(config, dualServerLogger); } const app = new Hono({ diff --git a/packages/config-manager/config.type.ts b/packages/config-manager/config.type.ts index 0f6da297..3c9507fc 100644 --- a/packages/config-manager/config.type.ts +++ b/packages/config-manager/config.type.ts @@ -123,21 +123,16 @@ export const configValidator = z.object({ proxy: z .object({ enabled: z.boolean().default(false), - host: z.string().min(1).default("localhost"), - port: z - .number() - .int() - .min(1) - .max(2 ** 16 - 1) - .default(9050), - type: z.enum(["socks4", "socks4a", "socks5"]).default("socks5"), + address: zUrl, }) .default({ enabled: false, - host: "localhost", - port: 9050, - type: "socks5", - }), + address: "", + }) + .transform((arg) => ({ + ...arg, + address: arg.enabled ? arg.address : undefined, + })), tls: z .object({ enabled: z.boolean().default(false), diff --git a/packages/database-interface/note.ts b/packages/database-interface/note.ts index 19e86759..ce74d44b 100644 --- a/packages/database-interface/note.ts +++ b/packages/database-interface/note.ts @@ -595,6 +595,7 @@ export class Note extends BaseInterface { headers: { Accept: "application/json", }, + proxy: config.http.proxy.address, }); note = await new EntityValidator().Note(await response.json()); diff --git a/packages/database-interface/user.ts b/packages/database-interface/user.ts index 4ab1ada6..2e160102 100644 --- a/packages/database-interface/user.ts +++ b/packages/database-interface/user.ts @@ -253,6 +253,7 @@ export class User extends BaseInterface { headers: { Accept: "application/json", }, + proxy: config.http.proxy.address, }); const json = (await response.json()) as Partial; @@ -551,7 +552,9 @@ export class User extends BaseInterface { ); // FIXME: Add to new queue system when it's implemented - fetch(federationRequest); + fetch(federationRequest, { + proxy: config.http.proxy.address, + }); } } diff --git a/server/api/media/proxy/:id.ts b/server/api/media/proxy/:id.ts index be294061..22edea34 100644 --- a/server/api/media/proxy/:id.ts +++ b/server/api/media/proxy/:id.ts @@ -3,6 +3,7 @@ import { errorResponse, response } from "@/response"; import { zValidator } from "@hono/zod-validator"; import type { Hono } from "hono"; import { z } from "zod"; +import { config } from "~/packages/config-manager"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -44,6 +45,7 @@ export default (app: Hono) => headers: { "Accept-Encoding": "br", }, + proxy: config.http.proxy.address, }); // Check if file extension ends in svg or svg diff --git a/utils/content_types.ts b/utils/content_types.ts index 17b2e947..d593f622 100644 --- a/utils/content_types.ts +++ b/utils/content_types.ts @@ -1,5 +1,6 @@ import type { ContentFormat } from "@lysand-org/federation/types"; import { lookup } from "mime-types"; +import { config } from "~/packages/config-manager"; export const getBestContentType = (content?: ContentFormat) => { if (!content) { @@ -51,9 +52,10 @@ export const mimeLookup = async (url: string) => { return naiveLookup; } - const fetchLookup = fetch(url, { method: "HEAD" }).then( - (response) => response.headers.get("content-type") || "", - ); + const fetchLookup = fetch(url, { + method: "HEAD", + proxy: config.http.proxy.address, + }).then((response) => response.headers.get("content-type") || ""); return fetchLookup; }; diff --git a/utils/init.ts b/utils/init.ts new file mode 100644 index 00000000..dcf5a912 --- /dev/null +++ b/utils/init.ts @@ -0,0 +1,183 @@ +import chalk from "chalk"; +import type { Config } from "~/packages/config-manager"; +import { + LogLevel, + type LogManager, + type MultiLogManager, +} from "~/packages/log-manager"; + +export const checkConfig = async ( + config: Config, + logger: LogManager | MultiLogManager, +) => { + await checkOidcConfig(config, logger); + + await checkHttpProxyConfig(config, logger); + + await checkChallengeConfig(config, logger); +}; + +const checkHttpProxyConfig = async ( + config: Config, + logger: LogManager | MultiLogManager, +) => { + if (config.http.proxy.enabled) { + if (!config.http.proxy.address) { + await logger.log( + LogLevel.Critical, + "Server", + "The HTTP proxy is enabled, but the proxy address is not set in the config", + ); + + // Hang until Ctrl+C is pressed + await Bun.sleep(Number.POSITIVE_INFINITY); + } + + await logger.log( + LogLevel.Info, + "Server", + `HTTP proxy enabled at ${chalk.gray(config.http.proxy.address)}, testing...`, + ); + + // Test the proxy + const response = await fetch("https://api.ipify.org?format=json", { + proxy: config.http.proxy.address, + }); + + const ip = (await response.json()).ip; + + await logger.log( + LogLevel.Info, + "Server", + `Your IPv4 address is ${chalk.gray(ip)}`, + ); + + if (!response.ok) { + await logger.log( + LogLevel.Critical, + "Server", + "The HTTP proxy is enabled, but the proxy address is not reachable", + ); + + // Hang until Ctrl+C is pressed + await Bun.sleep(Number.POSITIVE_INFINITY); + } + } +}; + +const checkChallengeConfig = async ( + config: Config, + logger: LogManager | MultiLogManager, +) => { + if ( + config.validation.challenges.enabled && + !config.validation.challenges.key + ) { + await logger.log( + LogLevel.Critical, + "Server", + "Challenges are enabled, but the challenge key is not set in the config", + ); + + await logger.log( + LogLevel.Critical, + "Server", + "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"); + + await logger.log( + LogLevel.Critical, + "Server", + `Generated key: ${chalk.gray(base64)}`, + ); + + // Hang until Ctrl+C is pressed + await Bun.sleep(Number.POSITIVE_INFINITY); + } +}; + +const checkOidcConfig = async ( + config: Config, + logger: LogManager | MultiLogManager, +) => { + if (!config.oidc.jwt_key) { + await logger.log( + LogLevel.Critical, + "Server", + "The JWT private key is not set in the config", + ); + await logger.log( + LogLevel.Critical, + "Server", + "Below is a generated key for you to copy in the config at oidc.jwt_key", + ); + // Generate a key for them + const keys = await crypto.subtle.generateKey("Ed25519", true, [ + "sign", + "verify", + ]); + + const privateKey = Buffer.from( + await crypto.subtle.exportKey("pkcs8", keys.privateKey), + ).toString("base64"); + + const publicKey = Buffer.from( + await crypto.subtle.exportKey("spki", keys.publicKey), + ).toString("base64"); + + await logger.log( + LogLevel.Critical, + "Server", + chalk.gray(`${privateKey};${publicKey}`), + ); + + // 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.oidc.jwt_key.split(";")[0], "base64"), + "Ed25519", + false, + ["sign"], + ) + .catch((e) => e as Error); + + // Try and import the key + const publicKey = await crypto.subtle + .importKey( + "spki", + Buffer.from(config.oidc.jwt_key.split(";")[1], "base64"), + "Ed25519", + false, + ["verify"], + ) + .catch((e) => e as Error); + + if (privateKey instanceof Error || publicKey instanceof Error) { + await logger.log( + LogLevel.Critical, + "Server", + "The JWT key could not be imported! You may generate a new one by removing the old one from the config and restarting the server (this will invalidate all current JWTs).", + ); + + // Hang until Ctrl+C is pressed + await Bun.sleep(Number.POSITIVE_INFINITY); + } +};