feat(config): Add support for HTTP proxies on outgoing requests

This commit is contained in:
Jesse Wierzbinski 2024-06-25 17:13:40 -10:00
parent 0ecb65de29
commit b8b822e553
No known key found for this signature in database
13 changed files with 225 additions and 136 deletions

View file

@ -63,6 +63,7 @@ export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
headers: { headers: {
"Accept-Encoding": "identity", "Accept-Encoding": "identity",
}, },
proxy: config.http.proxy.address,
}); });
if (!response.ok) { if (!response.ok) {

View file

@ -70,6 +70,7 @@ export default class EmojiImport extends BaseCommand<typeof EmojiImport> {
headers: { headers: {
"Accept-Encoding": "identity", "Accept-Encoding": "identity",
}, },
proxy: config.http.proxy.address,
}); });
if (!response.ok) { if (!response.ok) {

View file

@ -84,13 +84,10 @@ banned_user_agents = [
] ]
[http.proxy] [http.proxy]
# For SOCKS proxies (e.g. Tor) # For HTTP proxies (e.g. Tor proxies)
# Will be used for all outgoing requests # Will be used for all outgoing requests
enabled = false enabled = false
host = "127.0.0.1" address = "http://localhost:8118"
port = 9050
# Can be socks4, socks4a or socks5
type = "socks5"
[http.tls] [http.tls]
# If these values are set, Lysand will use these files for TLS # If these values are set, Lysand will use these files for TLS

View file

@ -1,6 +1,7 @@
import type { ServerMetadata } from "@lysand-org/federation/types"; import type { ServerMetadata } from "@lysand-org/federation/types";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Instances } from "~/drizzle/schema"; import { Instances } from "~/drizzle/schema";
import { config } from "~/packages/config-manager";
/** /**
* Represents an instance in the database. * Represents an instance in the database.
@ -24,9 +25,9 @@ export const addInstanceIfNotExists = async (url: string) => {
} }
// Fetch the instance configuration // Fetch the instance configuration
const metadata = (await fetch(new URL("/.well-known/lysand", origin)).then( const metadata = (await fetch(new URL("/.well-known/lysand", origin), {
(res) => res.json(), proxy: config.http.proxy.address,
)) as ServerMetadata; }).then((res) => res.json())) as ServerMetadata;
if (metadata.type !== "ServerMetadata") { if (metadata.type !== "ServerMetadata") {
throw new Error("Invalid instance metadata (wrong type)"); throw new Error("Invalid instance metadata (wrong type)");

View file

@ -448,7 +448,9 @@ export const federateNote = async (note: Note) => {
); );
// Send request // Send request
const response = await fetch(request); const response = await fetch(request, {
proxy: config.http.proxy.address,
});
if (!response.ok) { if (!response.ok) {
dualLogger.log( dualLogger.log(

View file

@ -175,7 +175,9 @@ export const followRequestUser = async (
); );
// Send request // Send request
const response = await fetch(request); const response = await fetch(request, {
proxy: config.http.proxy.address,
});
if (!response.ok) { if (!response.ok) {
dualLogger.log( dualLogger.log(
@ -230,7 +232,9 @@ export const sendFollowAccept = async (follower: User, followee: User) => {
); );
// Send request // Send request
const response = await fetch(request); const response = await fetch(request, {
proxy: config.http.proxy.address,
});
if (!response.ok) { if (!response.ok) {
dualLogger.log( dualLogger.log(
@ -258,7 +262,9 @@ export const sendFollowReject = async (follower: User, followee: User) => {
); );
// Send request // Send request
const response = await fetch(request); const response = await fetch(request, {
proxy: config.http.proxy.address,
});
if (!response.ok) { if (!response.ok) {
dualLogger.log( dualLogger.log(
@ -375,6 +381,7 @@ export const resolveWebFinger = async (
headers: { headers: {
Accept: "application/json", Accept: "application/json",
}, },
proxy: config.http.proxy.address,
}, },
); );

110
index.ts
View file

@ -1,7 +1,7 @@
import { checkConfig } from "@/init";
import { dualLogger } from "@/loggers"; import { dualLogger } from "@/loggers";
import { connectMeili } from "@/meilisearch"; import { connectMeili } from "@/meilisearch";
import { errorResponse, response } from "@/response"; import { errorResponse, response } from "@/response";
import chalk from "chalk";
import { config } from "config-manager"; import { config } from "config-manager";
import { Hono } from "hono"; import { Hono } from "hono";
import { LogLevel, LogManager, type MultiLogManager } from "log-manager"; import { LogLevel, LogManager, type MultiLogManager } from "log-manager";
@ -46,113 +46,7 @@ process.on("SIGINT", () => {
const postCount = await Note.getCount(); const postCount = await Note.getCount();
if (isEntry) { if (isEntry) {
// Check if JWT private key is set in config await checkConfig(config, dualServerLogger);
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);
}
} }
const app = new Hono({ const app = new Hono({

View file

@ -123,21 +123,16 @@ export const configValidator = z.object({
proxy: z proxy: z
.object({ .object({
enabled: z.boolean().default(false), enabled: z.boolean().default(false),
host: z.string().min(1).default("localhost"), address: zUrl,
port: z
.number()
.int()
.min(1)
.max(2 ** 16 - 1)
.default(9050),
type: z.enum(["socks4", "socks4a", "socks5"]).default("socks5"),
}) })
.default({ .default({
enabled: false, enabled: false,
host: "localhost", address: "",
port: 9050, })
type: "socks5", .transform((arg) => ({
}), ...arg,
address: arg.enabled ? arg.address : undefined,
})),
tls: z tls: z
.object({ .object({
enabled: z.boolean().default(false), enabled: z.boolean().default(false),

View file

@ -595,6 +595,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
headers: { headers: {
Accept: "application/json", Accept: "application/json",
}, },
proxy: config.http.proxy.address,
}); });
note = await new EntityValidator().Note(await response.json()); note = await new EntityValidator().Note(await response.json());

View file

@ -253,6 +253,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
headers: { headers: {
Accept: "application/json", Accept: "application/json",
}, },
proxy: config.http.proxy.address,
}); });
const json = (await response.json()) as Partial<LysandUser>; const json = (await response.json()) as Partial<LysandUser>;
@ -551,7 +552,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
); );
// FIXME: Add to new queue system when it's implemented // FIXME: Add to new queue system when it's implemented
fetch(federationRequest); fetch(federationRequest, {
proxy: config.http.proxy.address,
});
} }
} }

View file

@ -3,6 +3,7 @@ import { errorResponse, response } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { config } from "~/packages/config-manager";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -44,6 +45,7 @@ export default (app: Hono) =>
headers: { headers: {
"Accept-Encoding": "br", "Accept-Encoding": "br",
}, },
proxy: config.http.proxy.address,
}); });
// Check if file extension ends in svg or svg // Check if file extension ends in svg or svg

View file

@ -1,5 +1,6 @@
import type { ContentFormat } from "@lysand-org/federation/types"; import type { ContentFormat } from "@lysand-org/federation/types";
import { lookup } from "mime-types"; import { lookup } from "mime-types";
import { config } from "~/packages/config-manager";
export const getBestContentType = (content?: ContentFormat) => { export const getBestContentType = (content?: ContentFormat) => {
if (!content) { if (!content) {
@ -51,9 +52,10 @@ export const mimeLookup = async (url: string) => {
return naiveLookup; return naiveLookup;
} }
const fetchLookup = fetch(url, { method: "HEAD" }).then( const fetchLookup = fetch(url, {
(response) => response.headers.get("content-type") || "", method: "HEAD",
); proxy: config.http.proxy.address,
}).then((response) => response.headers.get("content-type") || "");
return fetchLookup; return fetchLookup;
}; };

183
utils/init.ts Normal file
View file

@ -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);
}
};