feat(http): add proxy support (#4721)

This commit is contained in:
Meier Lukas
2025-12-25 11:32:14 +01:00
committed by GitHub
parent 707a4dad83
commit 5a57115ca0
10 changed files with 52 additions and 30 deletions

View File

@@ -1,6 +1,7 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createTRPCRouter, publicProcedure } from "../trpc"; import { createTRPCRouter, publicProcedure } from "../trpc";
@@ -36,7 +37,12 @@ export const locationRouter = createTRPCRouter({
.input(locationSearchCityInput) .input(locationSearchCityInput)
.output(locationSearchCityOutput) .output(locationSearchCityOutput)
.query(async ({ input }) => { .query(async ({ input }) => {
const res = await fetchWithTimeoutAsync(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`); const res = await withTimeoutAsync(async (signal) => {
return await fetchWithTrustedCertificatesAsync(
`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`,
{ signal },
);
});
return (await res.json()) as z.infer<typeof locationSearchCityOutput>; return (await res.json()) as z.infer<typeof locationSearchCityOutput>;
}), }),
}); });

View File

@@ -1,5 +1,5 @@
import type { Dispatcher } from "undici"; import type { Dispatcher } from "undici";
import { Agent } from "undici"; import { EnvHttpProxyAgent } from "undici";
import type { ILogger } from "@homarr/core/infrastructure/logs"; import type { ILogger } from "@homarr/core/infrastructure/logs";
import { createLogger } from "@homarr/core/infrastructure/logs"; import { createLogger } from "@homarr/core/infrastructure/logs";
@@ -7,16 +7,15 @@ import { createLogger } from "@homarr/core/infrastructure/logs";
// The below import statement initializes dns-caching // The below import statement initializes dns-caching
import "@homarr/core/infrastructure/dns/init"; import "@homarr/core/infrastructure/dns/init";
interface HttpAgentOptions extends Agent.Options { interface HttpAgentOptions extends EnvHttpProxyAgent.Options {
logger?: ILogger; logger?: ILogger;
} }
export class UndiciHttpAgent extends Agent { export class UndiciHttpAgent extends EnvHttpProxyAgent {
private logger: ILogger; private logger: ILogger;
constructor(props?: HttpAgentOptions) { constructor(props?: HttpAgentOptions) {
super(props); super(props);
this.logger = props?.logger ?? createLogger({ module: "httpAgent" }); this.logger = props?.logger ?? createLogger({ module: "httpAgent" });
} }

View File

@@ -42,12 +42,13 @@ export const createCertificateAgentAsync = async (override?: {
}; };
export const createHttpsAgentAsync = async (override?: Pick<AgentOptions, "ca" | "checkServerIdentity">) => { export const createHttpsAgentAsync = async (override?: Pick<AgentOptions, "ca" | "checkServerIdentity">) => {
return new HttpsAgent( return new HttpsAgent({
override ?? { ca: await getAllTrustedCertificatesAsync(),
ca: await getAllTrustedCertificatesAsync(), checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()), // Override the ca and checkServerIdentity if provided
}, ...override,
); proxyEnv: process.env,
});
}; };
export const createAxiosCertificateInstanceAsync = async ( export const createAxiosCertificateInstanceAsync = async (

View File

@@ -7,7 +7,7 @@ import { TestLogger } from "../logs";
vi.mock("undici", () => { vi.mock("undici", () => {
return { return {
Agent: class Agent { EnvHttpProxyAgent: class EnvHttpProxyAgent {
dispatch(_options: Dispatcher.DispatchOptions, _handler: Dispatcher.DispatchHandler): boolean { dispatch(_options: Dispatcher.DispatchOptions, _handler: Dispatcher.DispatchHandler): boolean {
return true; return true;
} }

View File

@@ -1,6 +1,7 @@
import { parse } from "path"; import { parse } from "path";
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import type { IconRepositoryLicense } from "../types/icon-repository-license"; import type { IconRepositoryLicense } from "../types/icon-repository-license";
import type { RepositoryIconGroup } from "../types/repository-icon-group"; import type { RepositoryIconGroup } from "../types/repository-icon-group";
@@ -19,11 +20,12 @@ export class GitHubIconRepository extends IconRepository {
} }
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> { protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
if (!this.repositoryIndexingUrl || !this.repositoryBlobUrlTemplate) { const url = this.repositoryIndexingUrl;
if (!url || !this.repositoryBlobUrlTemplate) {
throw new Error("Repository URLs are required for this repository"); throw new Error("Repository URLs are required for this repository");
} }
const response = await fetchWithTimeoutAsync(this.repositoryIndexingUrl); const response = await withTimeoutAsync(async (signal) => fetchWithTrustedCertificatesAsync(url, { signal }));
const listOfFiles = (await response.json()) as GitHubApiResponse; const listOfFiles = (await response.json()) as GitHubApiResponse;
return { return {

View File

@@ -1,6 +1,7 @@
import { parse } from "path"; import { parse } from "path";
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import type { IconRepositoryLicense } from "../types/icon-repository-license"; import type { IconRepositoryLicense } from "../types/icon-repository-license";
import type { RepositoryIconGroup } from "../types/repository-icon-group"; import type { RepositoryIconGroup } from "../types/repository-icon-group";
@@ -19,7 +20,9 @@ export class JsdelivrIconRepository extends IconRepository {
} }
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> { protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
const response = await fetchWithTimeoutAsync(this.repositoryIndexingUrl); const response = await withTimeoutAsync(async (signal) =>
fetchWithTrustedCertificatesAsync(this.repositoryIndexingUrl, { signal }),
);
const listOfFiles = (await response.json()) as JsdelivrApiResponse; const listOfFiles = (await response.json()) as JsdelivrApiResponse;
return { return {

View File

@@ -1,7 +1,8 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
@@ -11,7 +12,9 @@ export const minecraftServerStatusRequestHandler = createCachedWidgetRequestHand
async requestAsync(input: { domain: string; isBedrockServer: boolean }) { async requestAsync(input: { domain: string; isBedrockServer: boolean }) {
const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`; const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`;
const response = await fetchWithTimeoutAsync(`https://api.mcsrvstat.us${path}`); const response = await withTimeoutAsync(async (signal) =>
fetchWithTrustedCertificatesAsync(`https://api.mcsrvstat.us${path}`, { signal }),
);
return responseSchema.parse(await response.json()); return responseSchema.parse(await response.json());
}, },
cacheDuration: dayjs.duration(5, "minutes"), cacheDuration: dayjs.duration(5, "minutes"),

View File

@@ -1,7 +1,8 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
@@ -9,9 +10,12 @@ export const fetchStockPriceHandler = createCachedWidgetRequestHandler({
queryKey: "fetchStockPriceResult", queryKey: "fetchStockPriceResult",
widgetKind: "stockPrice", widgetKind: "stockPrice",
async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) { async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) {
const response = await fetchWithTimeoutAsync( const response = await withTimeoutAsync(async (signal) => {
`https://query1.finance.yahoo.com/v8/finance/chart/${input.stock}?range=${input.timeRange}&interval=${input.timeInterval}`, return await fetchWithTrustedCertificatesAsync(
); `https://query1.finance.yahoo.com/v8/finance/chart/${input.stock}?range=${input.timeRange}&interval=${input.timeInterval}`,
{ signal },
);
});
const data = dataSchema.parse(await response.json()); const data = dataSchema.parse(await response.json());
if ("error" in data) { if ("error" in data) {

View File

@@ -3,7 +3,7 @@ import { Octokit } from "octokit";
import { compareSemVer, isValidSemVer } from "semver-parser"; import { compareSemVer, isValidSemVer } from "semver-parser";
import { env } from "@homarr/common/env"; import { env } from "@homarr/common/env";
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs"; import { createLogger } from "@homarr/core/infrastructure/logs";
import { createChannelWithLatestAndEvents } from "@homarr/redis"; import { createChannelWithLatestAndEvents } from "@homarr/redis";
import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler";
@@ -23,7 +23,7 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({
const octokit = new Octokit({ const octokit = new Octokit({
request: { request: {
fetch: fetchWithTimeoutAsync, fetch: fetchWithTrustedCertificatesAsync,
}, },
}); });
const releases = await octokit.rest.repos.listReleases({ const releases = await octokit.rest.repos.listReleases({

View File

@@ -1,7 +1,8 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { z } from "zod"; import { z } from "zod";
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
@@ -9,9 +10,12 @@ export const weatherRequestHandler = createCachedWidgetRequestHandler({
queryKey: "weatherAtLocation", queryKey: "weatherAtLocation",
widgetKind: "weather", widgetKind: "weather",
async requestAsync(input: { latitude: number; longitude: number }) { async requestAsync(input: { latitude: number; longitude: number }) {
const res = await fetchWithTimeoutAsync( const res = await withTimeoutAsync(async (signal) => {
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max&current_weather=true&timezone=auto`, return await fetchWithTrustedCertificatesAsync(
); `https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max&current_weather=true&timezone=auto`,
{ signal },
);
});
const json: unknown = await res.json(); const json: unknown = await res.json();
const weather = await atLocationOutput.parseAsync(json); const weather = await atLocationOutput.parseAsync(json);
return { return {