feat(http): add proxy support (#4721)
This commit is contained in:
@@ -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>;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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¤t_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¤t_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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user