feat: Add proxying support for images (#2427)

- Closes #2418
This commit is contained in:
Dermot Duffy
2026-06-30 17:45:12 -07:00
committed by dermotduffy
parent 9384785d37
commit 1cd5520154
51 changed files with 2404 additions and 687 deletions
+14 -19
View File
@@ -1,7 +1,7 @@
import { ZodSchema } from 'zod';
import { localize } from '../localize/localize';
import { AdvancedCameraCardError, Endpoint } from '../types';
import { homeAssistantSignPath } from './sign-path';
import { homeAssistantGetSignedURLIfNecessary } from './sign-path';
import { HomeAssistant } from './types';
/**
@@ -22,25 +22,20 @@ export const homeAssistantSignAndFetch = async <T>(
timeoutSeconds?: number;
},
): Promise<T> => {
let url: string | null = endpoint.endpoint;
const sign = endpoint.sign;
let url: string | null;
try {
url = await homeAssistantGetSignedURLIfNecessary(hass, endpoint);
} catch (error) {
throw new AdvancedCameraCardError(localize('error.failed_sign'), {
endpoint,
error,
});
}
// Sign the path if needed
if (sign) {
try {
url = await homeAssistantSignPath(hass, url);
} catch (error) {
throw new AdvancedCameraCardError(localize('error.failed_sign'), {
endpoint,
error,
});
}
if (!url) {
throw new AdvancedCameraCardError(localize('error.failed_sign'), {
endpoint,
});
}
if (!url) {
throw new AdvancedCameraCardError(localize('error.failed_sign'), {
endpoint,
});
}
let response: Response;
+20 -2
View File
@@ -1,6 +1,6 @@
import { SignedPath, signedPathSchema } from '../types';
import { homeAssistantWSRequest } from './ws-request';
import { type Endpoint, SignedPath, signedPathSchema } from '../types';
import { HomeAssistant } from './types';
import { homeAssistantWSRequest } from './ws-request';
/**
* Request that HA sign a path. May throw.
@@ -30,3 +30,21 @@ export async function homeAssistantSignPath(
}
return hass.hassUrl(response.path);
}
/**
* Sign an endpoint's path if the endpoint requires signing.
* @param hass The HomeAssistant object used to request the signature.
* @param endpoint The endpoint to potentially sign.
* @param expires An optional number of seconds to sign the path for.
* @returns The signed or unsigned URL, or null if signing failed.
*/
export async function homeAssistantGetSignedURLIfNecessary(
hass: HomeAssistant,
endpoint: Endpoint,
expires?: number,
): Promise<string | null> {
if (!endpoint.sign) {
return endpoint.endpoint;
}
return await homeAssistantSignPath(hass, endpoint.endpoint, expires);
}
+21 -27
View File
@@ -1,4 +1,4 @@
import { CameraProxyConfig } from '../camera-manager/types';
import { EnabledProxyConfig, ResolvedProxyConfig } from '../config/schema/common/proxy';
import { Endpoint } from '../types';
import { HomeAssistant } from './types';
@@ -26,22 +26,12 @@ export const getWebProxiedURL = (url: string, options?: ProxiedURLOptions): stri
);
};
export const shouldUseWebProxy = (
hass: HomeAssistant,
proxyConfig: CameraProxyConfig,
context: 'media' | 'live' = 'media',
): boolean => {
return hasWebProxyAvailable(hass) && !!proxyConfig[context];
};
export async function addDynamicProxyURL(
hass: HomeAssistant,
url_pattern: string,
options?: {
proxyConfig?: CameraProxyConfig;
proxyConfig?: ResolvedProxyConfig;
urlID?: string;
sslVerification?: boolean;
sslCiphers?: string;
openLimit?: number;
ttl?: number;
allowUnauthenticated?: boolean;
@@ -51,9 +41,8 @@ export async function addDynamicProxyURL(
url_pattern: url_pattern,
...(options && {
url_id: options.urlID,
ssl_verification:
options.sslVerification ?? options?.proxyConfig?.ssl_verification,
ssl_ciphers: options.sslCiphers ?? options?.proxyConfig?.ssl_ciphers,
ssl_verification: options.proxyConfig?.ssl_verification,
ssl_ciphers: options.proxyConfig?.ssl_ciphers,
open_limit: options.openLimit,
ttl: options.ttl,
allow_unauthenticated: options.allowUnauthenticated,
@@ -61,8 +50,7 @@ export async function addDynamicProxyURL(
});
}
interface CreateProxiedEndpointOptions {
context?: 'live' | 'media';
export interface CreateProxiedEndpointOptions {
ttl?: number;
websocket?: boolean;
openLimit?: number;
@@ -73,25 +61,31 @@ interface CreateProxiedEndpointOptions {
* Handles dynamic proxy registration and returns a proxied Endpoint.
* @param hass Home Assistant instance.
* @param endpoint The endpoint to potentially proxy.
* @param proxyConfig The camera proxy configuration. If undefined, returns original endpoint.
* @param proxyConfig The proxy configuration. If undefined or not enabled,
* returns the original endpoint.
* @param options Additional options for proxy registration.
* @returns Proxied Endpoint if proxying needed, original endpoint otherwise.
* @returns Proxied Endpoint if proxying needed, original endpoint if proxying
* is not enabled, or null if proxying is required but unavailable.
*/
export const createProxiedEndpointIfNecessary = async (
hass: HomeAssistant,
endpoint: Endpoint,
proxyConfig?: CameraProxyConfig,
proxyConfig?: EnabledProxyConfig,
options?: CreateProxiedEndpointOptions,
): Promise<Endpoint> => {
const context = options?.context ?? 'media';
if (!proxyConfig || !shouldUseWebProxy(hass, proxyConfig, context)) {
): Promise<Endpoint | null> => {
if (!proxyConfig || !proxyConfig.enabled) {
return endpoint;
}
if (!hasWebProxyAvailable(hass)) {
return proxyConfig.enforce === true ? null : endpoint;
}
if (proxyConfig.dynamic) {
// Strip hash fragment for registration - it's client-side only and
// not relevant for proxy pattern matching.
const registrationUrl = endpoint.endpoint.split(/#/)[0];
await addDynamicProxyURL(hass, registrationUrl, {
// Strip hash fragment — it's client-side only and not relevant for
// proxy pattern matching.
const url = endpoint.endpoint.split(/#/)[0];
await addDynamicProxyURL(hass, url, {
proxyConfig,
ttl: options?.ttl,
openLimit: options?.openLimit ?? 0,