feat: add image-proxy key ttl and shared http dispatcher

This commit is contained in:
copilot-swe-agent[bot] authored and GitHub committed 2026-06-03 06:01:34 +00:00
1 parent 3ff0d72ba1
commit e1ff84830f
3 files changed
+69 -11

No files matched your search

@@ -9,11 +9,18 @@ import {
getAllTrustedCertificatesAsync,
getTrustedCertificateHostnamesAsync,
} from "@homarr/core/infrastructure/certificates";
import { UndiciHttpAgent } from "@homarr/core/infrastructure/http";
import { UndiciHttpAgent } from "@homarr/core/infrastructure/http/http-agent";
import type { TrustedCertificateHostname } from "../certificates/hostnames";
import { withTimeoutAsync } from "./timeout";
const CERTIFICATE_DISPATCHER_TTL_MS = 5 * 60 * 1000;
let trustedCertificateDispatcher: {
dispatcher: UndiciHttpAgent;
expiresAt: number;
} | null = null;
let creatingTrustedCertificateDispatcherPromise: Promise<UndiciHttpAgent> | null = null;
export const createCustomCheckServerIdentity = (
trustedHostnames: TrustedCertificateHostname[],
): typeof checkServerIdentity => {
@@ -41,6 +48,36 @@ export const createCertificateAgentAsync = async (override?: {
});
};
const getSharedCertificateAgentAsync = async () => {
const now = Date.now();
if (trustedCertificateDispatcher && trustedCertificateDispatcher.expiresAt > now) {
return trustedCertificateDispatcher.dispatcher;
}
if (creatingTrustedCertificateDispatcherPromise) {
return await creatingTrustedCertificateDispatcherPromise;
}
creatingTrustedCertificateDispatcherPromise = createCertificateAgentAsync(undefined)
.then((dispatcher) => {
const previousDispatcher = trustedCertificateDispatcher?.dispatcher;
trustedCertificateDispatcher = {
dispatcher,
expiresAt: Date.now() + CERTIFICATE_DISPATCHER_TTL_MS,
};
if (previousDispatcher) {
void previousDispatcher.close().catch(() => {
previousDispatcher.destroy();
});
}
return dispatcher;
})
.finally(() => {
creatingTrustedCertificateDispatcherPromise = null;
});
return await creatingTrustedCertificateDispatcherPromise;
};
export const createHttpsAgentAsync = async (override?: Pick<AgentOptions, "ca" | "checkServerIdentity">) => {
return new HttpsAgent({
ca: await getAllTrustedCertificatesAsync(),
@@ -63,7 +100,7 @@ export const fetchWithTrustedCertificatesAsync = async (
url: RequestInfo,
options?: RequestInit & { timeout?: number },
): Promise<Response> => {
const agent = await createCertificateAgentAsync(undefined);
const agent = await getSharedCertificateAgentAsync();
if (options?.timeout) {
return await withTimeoutAsync(
async (signal) =>
+22 -7
View File
@@ -9,6 +9,7 @@ import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import { createGetSetChannel } from "@homarr/redis";
const logger = createLogger({ module: "imageProxy" });
const IMAGE_PROXY_ENTRY_TTL_SECONDS = 60 * 60 * 24 * 7;
const createHmacChannel = (hmac: `${string}.${string}`) => createGetSetChannel<string>(`image-proxy:hmac:${hmac}`);
const createUrlByIdChannel = (id: string) =>
@@ -31,12 +32,17 @@ export class ImageProxy {
public async createImageAsync(url: string, headers?: Record<string, string>): Promise<string> {
const existingId = await this.getExistingIdAsync(url, headers);
if (existingId) {
logger.debug("Image already exists in the proxy", {
id: existingId,
url: this.redactUrl(url),
headers: this.redactHeaders(headers ?? null),
});
return this.createImageUrl(existingId);
const existingImageData = await createUrlByIdChannel(existingId).getAsync();
if (!existingImageData) {
await this.removeHashForImageAsync(url, headers);
} else {
logger.debug("Image already exists in the proxy", {
id: existingId,
url: this.redactUrl(url),
headers: this.redactHeaders(headers ?? null),
});
return this.createImageUrl(existingId);
}
}
const id = createId();
@@ -116,8 +122,10 @@ export class ImageProxy {
await urlHeaderChannel.setAsync({
url: encryptSecret(url),
headers: encryptSecret(JSON.stringify(headers ?? null)),
}, {
ttlSeconds: IMAGE_PROXY_ENTRY_TTL_SECONDS,
});
await hashChannel.setAsync(id);
await hashChannel.setAsync(id, { ttlSeconds: IMAGE_PROXY_ENTRY_TTL_SECONDS });
logger.debug("Stored image in the proxy", {
id,
@@ -126,6 +134,13 @@ export class ImageProxy {
});
}
private async removeHashForImageAsync(url: string, headers: Record<string, string> | undefined): Promise<void> {
const urlHash = this.hashSecret(url);
const headerHash = this.hashSecret(JSON.stringify(headers ?? null));
await createHmacChannel(`${urlHash}.${headerHash}`).removeAsync();
}
private hashSecret(value: string): string {
return createHmac("sha256", ImageProxy.getHmacKey()).update(value).digest("hex");
}
+8 -2
View File
@@ -113,8 +113,14 @@ export const createGetSetChannel = <TData>(name: string) => {
* Set data in the channel
* @param data data to be stored in the channel
*/
setAsync: async (data: TData) => {
await getSetClient.set(name, superjson.stringify(data));
setAsync: async (data: TData, options?: { ttlSeconds?: number }) => {
const ttlSeconds = options?.ttlSeconds;
const serializedData = superjson.stringify(data);
if (ttlSeconds && ttlSeconds > 0) {
await getSetClient.set(name, serializedData, "EX", Math.ceil(ttlSeconds));
return;
}
await getSetClient.set(name, serializedData);
},
/**
* Remove data from the channel