From 1cd5520154fdaec11e8691aceab9c1a129dd28b8 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 26 Mar 2026 20:37:14 -0700 Subject: [PATCH] feat: Add proxying support for images (#2427) - Closes #2418 --- .eslintrc.cjs | 7 +- INSTRUCTIONS.md | 3 + docs/configuration/cameras/README.md | 28 +- docs/configuration/cameras/engine.md | 14 +- docs/configuration/cameras/live-provider.md | 4 +- docs/configuration/common/proxy-warning.md | 23 + docs/configuration/image.md | 32 +- docs/images/proxy.png | Bin 91534 -> 107212 bytes package.json | 2 +- src/camera-manager/camera.ts | 42 +- src/camera-manager/types.ts | 7 +- src/camera-manager/utils/go2rtc/audio.ts | 11 +- src/card-controller/view/item-manager.ts | 27 +- src/components-lib/cached-value-controller.ts | 52 +- .../media-player/updating-image.ts | 2 +- src/components-lib/signed-url-controller.ts | 232 ++++ src/components/image-updating-player.ts | 144 ++- src/components/image.ts | 17 +- src/components/live/provider.ts | 1 + src/components/live/providers/go2rtc/index.ts | 150 +-- src/components/live/providers/image.ts | 5 + src/components/live/providers/jsmpeg.ts | 6 +- src/components/viewer/provider.ts | 127 +-- src/config/schema/cameras.ts | 19 +- src/config/schema/common/image.ts | 5 - src/config/schema/common/proxy.ts | 45 + src/config/schema/image.ts | 18 +- src/config/schema/types.ts | 3 +- src/const.ts | 5 + src/editor.ts | 163 ++- src/ha/fetch.ts | 33 +- src/ha/sign-path.ts | 22 +- src/ha/web-proxy.ts | 48 +- src/localize/languages/de.json | 56 +- src/localize/languages/en.json | 47 +- src/localize/languages/fr.json | 62 +- src/localize/languages/pl.json | 48 +- src/utils/live-provider.ts | 4 +- tests/camera-manager/camera.test.ts | 99 +- .../camera-manager/utils/go2rtc-audio.test.ts | 6 + .../actions/actions/set.test.ts | 4 +- .../card-controller/view/item-manager.test.ts | 22 +- .../cached-value-controller.test.ts | 158 ++- .../media-player/updating-image.test.ts | 2 +- .../signed-url-controller.test.ts | 1004 +++++++++++++++++ tests/config/types.test.ts | 6 + tests/ha/fetch.test.ts | 52 +- tests/ha/sign-path.test.ts | 54 +- tests/ha/web-proxy.test.ts | 153 +-- tests/test-utils.ts | 1 + tests/utils/zod/parse-errors.test.ts | 16 +- 51 files changed, 2404 insertions(+), 687 deletions(-) create mode 100644 docs/configuration/common/proxy-warning.md create mode 100644 src/components-lib/signed-url-controller.ts create mode 100644 src/config/schema/common/proxy.ts create mode 100644 tests/components-lib/signed-url-controller.test.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1f0d97d8..250cf3f7 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -9,7 +9,10 @@ module.exports = { 'prettier', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier ], rules: { - // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs - // e.g. "@typescript-eslint/explicit-function-return-type": "off", + // Place to specify ESLint rules. Can be used to overwrite rules specified + // from the extended configs e.g. + // "@typescript-eslint/explicit-function-return-type": "off", + '@typescript-eslint/no-explicit-any': 'error', + 'no-multi-assign': 'error', }, }; diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index 75fbd2c5..6fba8d17 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -35,6 +35,9 @@ - Never use non-null assertions (`!`); use a null check with an early return or conditional instead. - Use `zod` for runtime validation if external data is involved. - `noUnusedParameters` and `noImplicitReturns` are enforced — all parameters must be used and all code paths must return. + - Prefer explicitly returning `null` rather than `undefined` for absent or empty state values. + - Prefer `async`/`await` over verbose Promise chaining (`.then().catch()`). + - Prefer instantiating `Date` objects for timestamps rather than using raw numbers (e.g. `Date.now()`). - **Testing (Vitest):** diff --git a/docs/configuration/cameras/README.md b/docs/configuration/cameras/README.md index 3ed7c126..d9820f8e 100644 --- a/docs/configuration/cameras/README.md +++ b/docs/configuration/cameras/README.md @@ -334,12 +334,7 @@ Configures whether and how the content is proxied via [hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration) (this must be installed separately). This allows fetching media or live streams **through** the Home Assistant process itself, allowing the card to access -resources it otherwise would not be able to directly access. There are [security -and performance -implications](https://github.com/dermotduffy/hass-web-proxy-integration?tab=readme-ov-file#considerations) -to consider before installing -[hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration) -and using this functionality. +resources it otherwise would not be able to directly access. ```yaml cameras: @@ -350,6 +345,8 @@ cameras: ![Camera Proxying](../../images/proxy.png 'Camera Proxying :size=400') +[](../common/proxy-warning.md ':include') + For live streams, only the `go2rtc` [live provider](./live-provider.md) currently supports live stream proxying. For media, not all [engines](./engine.md) benefit from proxying: @@ -360,20 +357,15 @@ For media, not all [engines](./engine.md) benefit from proxying: | `reolink`, `motioneye` | May be used to fetch videos in cases where the browser may not be able to access the camera/NVR, or the camera/NVR may use a self-signed SSL certificate that your browser would otherwise reject due to [mixed content](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Mixed_content). | | `generic` | `generic` cameras do not have media, so proxying currently would serve no purpose. | -Regardless of the parameters, the integration will never attempt to proxy -content if the -[hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration) -is not detected. - Proxying parameters: -| Option | Default | Description | -| ------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `live` | `auto` | Whether or not to proxy live streams. `true` to proxy, `false` to not proxy, or `auto` to allow the camera engine to decide whether to proxy or not. Not all [live providers](./live-provider.md) support proxying live streams. | -| `media` | `auto` | Whether or not to proxy media items. `true` to proxy, `false` to not proxy, or `auto` to allow the camera engine to decide whether to proxy or not. | -| `dynamic` | `true` | Whether to dynamically (at the time) request proxying of the required media item, or rely on statically user-configured pre-existing proxying. See the [hass-web-proxy-integration documentation](https://github.com/dermotduffy/hass-web-proxy-integration). | -| `ssl_verification` | `auto` | Whether to verify the validity of SSL certificates. If `true` always verifies, if `false` never verifies and if `auto` the [engine](./engine.md) decides the best setting for that camera ecosystem. | -| `ssl_ciphers` | `auto` | Whether to use `default`, `intermediate`, `insecure` or `modern` SSL ciphers. See the [Home Assistant code](https://github.com/home-assistant/core/blob/dev/homeassistant/util/ssl.py) for the precise list of SSL ciphers each implies. If `auto` the [engine](./engine.md) decides the best setting for that camera ecosystem. | +| Option | Default | Description | +| ------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `live` | `auto` | Whether or not to proxy live streams. `true` to proxy (will show an error if the proxy integration is unavailable), `false` to not proxy, or `auto` to allow the camera engine to decide whether to proxy or not. If `auto` resolves to proxying, the card will still fall back to the original URL if the proxy integration is unavailable. Not all [live providers](./live-provider.md) support proxying live streams. | +| `media` | `auto` | Whether or not to proxy media items. `true` to proxy (will show an error if the proxy integration is unavailable), `false` to not proxy, or `auto` to allow the camera engine to decide whether to proxy or not. If `auto` resolves to proxying, the card will still fall back to the original URL if the proxy integration is unavailable. | +| `dynamic` | `true` | Whether to dynamically (at the time) request proxying of the required media item, or rely on statically user-configured pre-existing proxying. See the [hass-web-proxy-integration documentation](https://github.com/dermotduffy/hass-web-proxy-integration). | +| `ssl_verification` | `auto` | Whether to verify the validity of SSL certificates. If `true` always verifies, if `false` never verifies and if `auto` the [engine](./engine.md) decides the best setting for that camera ecosystem. | +| `ssl_ciphers` | `auto` | Whether to use `default`, `intermediate`, `insecure` or `modern` SSL ciphers. See the [Home Assistant code](https://github.com/home-assistant/core/blob/dev/homeassistant/util/ssl.py) for the precise list of SSL ciphers each implies. If `auto` the [engine](./engine.md) decides the best setting for that camera ecosystem. | ## `triggers` diff --git a/docs/configuration/cameras/engine.md b/docs/configuration/cameras/engine.md index daea3f9e..97eead63 100644 --- a/docs/configuration/cameras/engine.md +++ b/docs/configuration/cameras/engine.md @@ -4,13 +4,13 @@ A "Camera Engine" defines what "type" of camera is being configured (e.g. `frigate`), each engine offers different capabilities: -| Engine | Live | Supports clips | Supports Snapshots | Supports Recordings | Supports Timeline | Supports PTZ out of the box | Supports manually configured PTZ | Favorite events | Favorite recordings | Detect new events | Detect new snapshots | Detect new clips | May requiring [proxying](./README.md?id=proxy) | Thumbnails | -| ----------- | ------------------ | ------------------------ | ------------------------ | ------------------------ | ------------------------ | --------------------------- | -------------------------------- | ------------------------ | ------------------------ | ------------------------ | ------------------------ | ------------------------ | ---------------------------------------------- | ------------------------ | -| `frigate` | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | -| `generic` | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | -| `motioneye` | :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :white_check_mark: | -| `reolink` | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :eight_spoked_asterisk: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | -| `tplink` | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :eight_spoked_asterisk: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | +| Engine | Live | Supports clips | Supports Snapshots | Supports Recordings | Supports Timeline | Supports PTZ out of the box | Supports manually configured PTZ | Favorite events | Favorite recordings | Detect new events | Detect new snapshots | Detect new clips | May require [proxying](./README.md?id=proxy) | Thumbnails | +| ----------- | ------------------ | ------------------------ | ------------------------ | ------------------------ | ------------------------ | --------------------------- | -------------------------------- | ------------------------ | ------------------------ | ------------------------ | ------------------------ | ------------------------ | -------------------------------------------- | ------------------------ | +| `frigate` | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | +| `generic` | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | +| `motioneye` | :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :white_check_mark: | +| `reolink` | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :eight_spoked_asterisk: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | +| `tplink` | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :eight_spoked_asterisk: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | ### Live providers supported per Engine diff --git a/docs/configuration/cameras/live-provider.md b/docs/configuration/cameras/live-provider.md index e134bf84..9c8eeb29 100644 --- a/docs/configuration/cameras/live-provider.md +++ b/docs/configuration/cameras/live-provider.md @@ -61,7 +61,9 @@ cameras: | `entity_parameters` | | Optional URL parameters to add to the URL generated for entity-based modes (i.e. when `mode` is `camera` or `entity`). | | `mode` | `auto` | Value must be one of `url` (to fetch an arbitrary image URL), `camera` (to show a still of the currently selected camera entity using either `camera_entity` or `webrtc_card.entity` in that order of precedence), `entity` to show an image associated with a named entity (see the `entity` parameter below), or `screensaver` (to show an [embedded image](https://github.com/dermotduffy/advanced-camera-card/blob/main/src/images/iris-screensaver.jpg)). If `auto`, the mode is chosen automatically based on whether `url` or `entity` parameters have been specified. | | `refresh_seconds` | 1 | The image will be refreshed at least every `refresh_seconds` (it may refresh more frequently, e.g. whenever Home Assistant updates its camera security token). `0` implies no refreshing. | -| `url` | | A static image URL to be used when the `mode` is set to `url` or when a temporary image is required (e.g. may appear momentarily prior to load of a camera snapshot in the `camera` mode). Note that a `_t=[timestsamp]` query parameter will be automatically added to all URLs such that the image will not be cached by the browser. | +| `url` | | A static image URL to be used when the `mode` is set to `url` or when a temporary image is required (e.g. may appear momentarily prior to load of a camera snapshot in the `camera` mode). Note that a `_t=[timestamp]` cache-busting value will be added automatically. | + +[](../common/proxy-warning.md ':include') ## `jsmpeg` diff --git a/docs/configuration/common/proxy-warning.md b/docs/configuration/common/proxy-warning.md new file mode 100644 index 00000000..dc2ee556 --- /dev/null +++ b/docs/configuration/common/proxy-warning.md @@ -0,0 +1,23 @@ +There are [security and performance +implications](https://github.com/dermotduffy/hass-web-proxy-integration?tab=readme-ov-file#considerations) +to consider before installing +[hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration) +and using this functionality. + +If the +[hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration) +is not detected, the card will fall back to the original URL unless proxying was +explicitly enabled (i.e. set to `true`), in which case an error will be shown. + +> [!WARNING] +> When proxying, the card's ability to ensure caching doesn't interfere with +> refresh accuracy depends on what is being proxied. Specifically, `image` +> based proxying will have less reliable behavior because signed URLs cannot +> have additional query parameters added without invalidating the signature. The +> card uses a URL fragment (`#_t=...`) for cache-busting instead, which does not +> affect the signature but is less reliable: the fragment is not sent to the +> server, so the browser may serve a cached response. Reliable refreshing will +> depend on the source server providing appropriate `Cache-Control` headers. The +> Home Assistant companion app's WebView does not re-fetch images when only the +> fragment changes, so proxied image refreshing will not work in the companion +> app. diff --git a/docs/configuration/image.md b/docs/configuration/image.md index da41c015..da52edac 100644 --- a/docs/configuration/image.md +++ b/docs/configuration/image.md @@ -13,12 +13,37 @@ image: | `entity` | | The entity to use when `mode` is set to `entity`. This entity is expected to have an `entity_picture` attribute that specifies the image URL. | | `entity_parameters` | | Optional URL parameters to add to the URL generated for entity-based modes (i.e. when `mode` is `camera` or `entity`). | | `mode` | `auto` | Value must be one of `url` (to fetch an arbitrary image URL), `camera` (to show a still of the currently selected camera entity using either `camera_entity` or `webrtc_card.entity` in that order of precedence), `entity` to show an image associated with a named entity (see the `entity` parameter below), or `screensaver` (to show an [embedded image](https://github.com/dermotduffy/advanced-camera-card/blob/main/src/images/iris-screensaver.jpg)). If `auto`, the mode is chosen automatically based on whether `url` or `entity` parameters have been specified. | +| `proxy` | | Proxy configuration for `url` mode images. See [proxy](#proxy) below. | | `refresh_seconds` | 1 | The image will be refreshed at least every `refresh_seconds` (it may refresh more frequently, e.g. whenever Home Assistant updates its camera security token). `0` implies no refreshing. | -| `url` | | A static image URL to be used when the `mode` is set to `url` or when a temporary image is required (e.g. may appear momentarily prior to load of a camera snapshot in the `camera` mode). Note that a `_t=[timestsamp]` query parameter will be automatically added to all URLs such that the image will not be cached by the browser. | +| `url` | | A static image URL to be used when the `mode` is set to `url` or when a temporary image is required (e.g. may appear momentarily prior to load of a camera snapshot in the `camera` mode). Note that a `_t=[timestamp]` cache-busting value will be added automatically. | > [!NOTE] > When `mode` is set to `camera` this is effectively providing the same image as the `image` [live provider](cameras/live-provider.md) would show in the live camera carousel. +## `proxy` + +Configures whether and how the image URL is proxied via +[hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration) +(this must be installed separately). This is useful when the image URL uses HTTP +but Home Assistant is served over HTTPS, which would otherwise be blocked by the +browser as [mixed +content](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Mixed_content). + +```yaml +image: + proxy: + # [...] +``` + +[](common/proxy-warning.md ':include') + +| Option | Default | Description | +| ------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `enabled` | `false` | Whether or not to proxy image URLs when in `url` mode. `true` to proxy (will show an error if the proxy integration is unavailable), `false` to not proxy. | +| `dynamic` | `true` | Whether to dynamically (at the time) request proxying of the required URL, or rely on statically user-configured pre-existing proxying. See the [hass-web-proxy-integration documentation](https://github.com/dermotduffy/hass-web-proxy-integration). | +| `ssl_verification` | `auto` | Whether to verify the validity of SSL certificates. If `true` always verifies, if `false` never verifies and if `auto` defaults to `true`. | +| `ssl_ciphers` | `auto` | Whether to use `default`, `intermediate`, `insecure` or `modern` SSL ciphers. See the [Home Assistant code](https://github.com/home-assistant/core/blob/dev/homeassistant/util/ssl.py) for the precise list of SSL ciphers each implies. If `auto` defaults to `default`. | + ## Fully expanded reference [](common/expanded-warning.md ':include') @@ -30,6 +55,11 @@ image: url: 'https://path/to/image.png' entity: image.office_person entity_parameters: 'width=400&height=200' + proxy: + enabled: false + dynamic: true + ssl_verification: auto + ssl_ciphers: auto actions: entity: light.office_main_lights tap_action: diff --git a/docs/images/proxy.png b/docs/images/proxy.png index 6f16a626731a2d3b09692a919268127d85f7f3a7..5fa85ddb217f32ce98fa88041c56193eb5d18851 100644 GIT binary patch literal 107212 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 diff --git a/package.json b/package.json index 5d71d935..240354b2 100644 --- a/package.json +++ b/package.json @@ -185,7 +185,7 @@ "docs-check-links": "docker run --init --rm -v \"$(pwd):/input\" -w /input lycheeverse/lychee --config lychee.toml \"./docs/\"", "docs-update-images": "./scripts/docs-update-images.sh", "docs-update-dependencies": "./scripts/docs-update-dependencies.sh", - "lint": "eslint 'src/**/*.ts'", + "lint": "eslint '{src,tests}/**/*.ts'", "format": "prettier --write .", "format-check": "prettier --check .", "rollup": "rollup -c", diff --git a/src/camera-manager/camera.ts b/src/camera-manager/camera.ts index ea9dc8e4..6cca62b7 100644 --- a/src/camera-manager/camera.ts +++ b/src/camera-manager/camera.ts @@ -2,6 +2,7 @@ import { ActionsExecutor } from '../card-controller/actions/types'; import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher'; import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz'; import { CameraConfig } from '../config/schema/cameras'; +import { EnabledProxyConfig, resolveProxyConfig } from '../config/schema/common/proxy'; import { isTriggeredState } from '../ha/is-triggered-state'; import { HassStateDifference, HomeAssistant } from '../ha/types'; import { localize } from '../localize/localize'; @@ -100,6 +101,8 @@ export class Camera { } protected async _has2WayAudioCapability(hass: HomeAssistant): Promise { + // Check disable/disableExcept/force early to short-circuit the expensive + // network call to fetch go2rtc metadata. if (this._config.capabilities?.disable?.includes('2-way-audio')) { return false; } @@ -115,7 +118,7 @@ export class Camera { this.getConfig(), this.getConfig().go2rtc.metadata_fetch_timeout_seconds, this._getGo2RTCMetadataEndpoint(), - this.getProxyConfig(), + this.getLiveProxyConfig(), ); } @@ -137,7 +140,7 @@ export class Camera { } public async destroy(): Promise { - this._destroyCallbacks.forEach((callback) => callback()); + await Promise.all(this._destroyCallbacks.map((callback) => callback())); } public getConfig(): CameraConfig { @@ -204,6 +207,7 @@ export class Camera { public getProxyConfig(): CameraProxyConfig { return { + ...resolveProxyConfig(this._config.proxy), live: this._config.proxy.live === 'auto' ? // Live is proxied if the live provider is go2rtc and if a go2rtc @@ -211,13 +215,32 @@ export class Camera { this._config.live_provider === 'go2rtc' && !!this._config.go2rtc?.url : this._config.proxy.live, media: this._config.proxy.media === 'auto' ? false : this._config.proxy.media, + }; + } - dynamic: this._config.proxy.dynamic, - ssl_verification: this._config.proxy.ssl_verification !== false, - ssl_ciphers: - this._config.proxy.ssl_ciphers === 'auto' - ? 'default' - : this._config.proxy.ssl_ciphers, + public getLiveProxyConfig(): EnabledProxyConfig { + const config = this.getProxyConfig(); + return { + ...config, + + // `enabled` uses the resolved engine decision (so `auto` may become + // true), whereas `enforce` uses the raw user setting so only an explicit + // `true` means "fail instead of falling back" if the proxy is unavailable. + enabled: config.live, + enforce: this._config.proxy.live === true, + }; + } + + public getMediaProxyConfig(): EnabledProxyConfig { + const config = this.getProxyConfig(); + return { + ...config, + + // `enabled` uses the resolved engine decision (so `auto` may become + // true), whereas `enforce` uses the raw user setting so only an explicit + // `true` means "fail instead of falling back" if the proxy is unavailable. + enabled: config.media, + enforce: this._config.proxy.media === true, }; } @@ -250,11 +273,10 @@ export class Camera { this._destroyCallbacks.push(callback); } - protected _subscribeBasedOnCapabilities( + private _subscribeBasedOnCapabilities( stateWatcher: StateWatcherSubscriptionInterface, ): void { if (this._capabilities?.has('trigger')) { - stateWatcher.unsubscribe(this._stateChangeHandler); stateWatcher.subscribe(this._stateChangeHandler, this._config.triggers.entities); } } diff --git a/src/camera-manager/types.ts b/src/camera-manager/types.ts index b67f5619..71c8c24f 100644 --- a/src/camera-manager/types.ts +++ b/src/camera-manager/types.ts @@ -1,7 +1,7 @@ import { ExpiringEqualityCache } from '../cache/expiring-cache'; -import { SSLCiphers } from '../config/schema/cameras'; import { AdvancedCameraCardView } from '../config/schema/common/const'; import { InternalIcon } from '../config/schema/common/icon'; +import { ResolvedProxyConfig } from '../config/schema/common/proxy'; import { BaseQuery, QueryFilters, QuerySource } from '../query-source'; import { CapabilityKey, Endpoint } from '../types'; import { ViewMedia } from '../view/item'; @@ -137,12 +137,9 @@ export interface CameraEndpoints { webrtcCard?: Endpoint; } -export interface CameraProxyConfig { - dynamic: boolean; +export interface CameraProxyConfig extends ResolvedProxyConfig { live: boolean; media: boolean; - ssl_verification: boolean; - ssl_ciphers: SSLCiphers; } export interface EngineOptions { diff --git a/src/camera-manager/utils/go2rtc/audio.ts b/src/camera-manager/utils/go2rtc/audio.ts index 2878ac4b..94e3c947 100644 --- a/src/camera-manager/utils/go2rtc/audio.ts +++ b/src/camera-manager/utils/go2rtc/audio.ts @@ -1,9 +1,9 @@ +import { EnabledProxyConfig } from '../../../config/schema/common/proxy'; import { homeAssistantSignAndFetch } from '../../../ha/fetch'; import { HomeAssistant } from '../../../ha/types'; import { createProxiedEndpointIfNecessary } from '../../../ha/web-proxy'; import { Endpoint } from '../../../types'; import { errorToConsole } from '../../../utils/basic'; -import { CameraProxyConfig } from '../../types'; import { Go2RTCStreamInfo, go2RTCStreamInfoSchema } from './types'; const getGo2RTCStreamMetadata = async ( @@ -44,14 +44,14 @@ const streamSupports2WayAudio = (streamInfo: Go2RTCStreamInfo | null): boolean = * * @param hass Home Assistant instance. * @param go2rtcMetadataEndpoint The go2rtc metadata endpoint. - * @param proxyConfig The camera's proxy configuration for live streams. + * @param proxyConfig The resolved proxy configuration for live streams. * @returns True if supports 2-way audio, false otherwise. */ export const supports2WayAudio = async ( hass: HomeAssistant, metadataFetchTimeoutSeconds: number, go2rtcMetadataEndpoint?: Endpoint | null, - proxyConfig?: CameraProxyConfig, + proxyConfig?: EnabledProxyConfig, ): Promise => { if (!go2rtcMetadataEndpoint) { return false; @@ -61,8 +61,11 @@ export const supports2WayAudio = async ( hass, go2rtcMetadataEndpoint, proxyConfig, - { context: 'live', openLimit: 1 }, + { openLimit: 1 }, ); + if (!endpoint) { + return false; + } const streamInfo = await getGo2RTCStreamMetadata( hass, diff --git a/src/card-controller/view/item-manager.ts b/src/card-controller/view/item-manager.ts index 275b0506..7dcb484b 100644 --- a/src/card-controller/view/item-manager.ts +++ b/src/card-controller/view/item-manager.ts @@ -1,9 +1,9 @@ import { format } from 'date-fns'; +import { homeAssistantGetSignedURLIfNecessary } from '../../ha/sign-path'; import { localize } from '../../localize/localize'; import { AdvancedCameraCardError } from '../../types'; import { errorToConsole } from '../../utils/basic'; import { downloadURL } from '../../utils/download'; -import { homeAssistantSignPath } from '../../ha/sign-path'; import { ViewItem } from '../../view/item'; import { ViewItemClassifier } from '../../view/item-classifier'; import { ViewItemCapabilities } from '../../view/types'; @@ -90,22 +90,19 @@ export class ViewItemManager { throw new AdvancedCameraCardError(localize('error.download_no_media')); } - let finalURL = endpoint.endpoint; - if (endpoint.sign) { - let response: string | null | undefined; - try { - response = await homeAssistantSignPath(hass, endpoint.endpoint); - } catch (e) { - errorToConsole(e as Error); - } - - if (!response) { - throw new AdvancedCameraCardError(localize('error.download_sign_failed')); - } - finalURL = response; + let url: string | null; + try { + url = await homeAssistantGetSignedURLIfNecessary(hass, endpoint); + } catch (e) { + errorToConsole(e as Error); + url = null; } - downloadURL(finalURL, this._generateDownloadFilename(item)); + if (!url) { + throw new AdvancedCameraCardError(localize('error.download_sign_failed')); + } + + downloadURL(url, this._generateDownloadFilename(item)); } private _generateDownloadFilename(item: ViewItem): string { diff --git a/src/components-lib/cached-value-controller.ts b/src/components-lib/cached-value-controller.ts index 056f24f1..f946ca3e 100644 --- a/src/components-lib/cached-value-controller.ts +++ b/src/components-lib/cached-value-controller.ts @@ -2,40 +2,42 @@ import { ReactiveController, ReactiveControllerHost } from 'lit'; import { Timer } from '../utils/timer'; export class CachedValueController implements ReactiveController { - private _value?: T; - private _host: ReactiveControllerHost; - private _timerSeconds: number; + private _host: ReactiveControllerHost & HTMLElement; + + private _value: T | null = null; + + private _timerSeconds: number | null = null; + private _callback: () => T; + + private _getTimerSecondsCallback: () => number | null; + private _timerStartCallback?: () => void; private _timerStopCallback?: () => void; + private _timerTickCallback?: () => void; private _timer = new Timer(); constructor( - host: ReactiveControllerHost, - timerSeconds: number, + host: ReactiveControllerHost & HTMLElement, + getTimerSecondsCallback: () => number | null, callback: () => T, timerStartCallback?: () => void, timerStopCallback?: () => void, + timerTickCallback?: () => void, ) { - this._timerSeconds = timerSeconds; + this._getTimerSecondsCallback = getTimerSecondsCallback; + this._timerSeconds = getTimerSecondsCallback(); this._callback = callback; this._timerStartCallback = timerStartCallback; this._timerStopCallback = timerStopCallback; + this._timerTickCallback = timerTickCallback; (this._host = host).addController(this); } - /** - * Remove the controller for the host. - */ - public removeController(): void { - this.stopTimer(); - this._host.removeController(this); - } - /** * Get the value. */ - get value(): T | undefined { + public getValue(): T | null { return this._value; } @@ -44,13 +46,14 @@ export class CachedValueController implements ReactiveController { */ public updateValue(): void { this._value = this._callback(); + this._host.requestUpdate(); } /** * Clear the cached value. */ public clearValue(): void { - this._value = undefined; + this._value = null; } /** @@ -69,10 +72,14 @@ export class CachedValueController implements ReactiveController { public startTimer(): void { this.stopTimer(); + if (!this._timerSeconds || this._timerSeconds <= 0) { + return; + } + this._timerStartCallback?.(); this._timer.startRepeated(this._timerSeconds, () => { + this._timerTickCallback?.(); this.updateValue(); - this._host.requestUpdate(); }); } @@ -80,13 +87,22 @@ export class CachedValueController implements ReactiveController { return this._timer.isRunning(); } + public hostUpdate(): void { + const newTimerSeconds = this._getTimerSecondsCallback(); + if (newTimerSeconds !== this._timerSeconds) { + this._timerSeconds = newTimerSeconds; + if (this._host.isConnected) { + this.startTimer(); + } + } + } + /** * Host has connected to the cache. */ hostConnected(): void { this.updateValue(); this.startTimer(); - this._host.requestUpdate(); } /** diff --git a/src/components-lib/media-player/updating-image.ts b/src/components-lib/media-player/updating-image.ts index 97b98e0d..dfc791af 100644 --- a/src/components-lib/media-player/updating-image.ts +++ b/src/components-lib/media-player/updating-image.ts @@ -55,7 +55,7 @@ export class UpdatingImageMediaPlayerController implements MediaPlayerController public async getScreenshotURL(): Promise { await this._host.updateComplete; - return this._getCachedValueController()?.value ?? null; + return this._getCachedValueController()?.getValue() ?? null; } public getFullscreenElement(): FullscreenElement | null { diff --git a/src/components-lib/signed-url-controller.ts b/src/components-lib/signed-url-controller.ts new file mode 100644 index 00000000..19c2fbc2 --- /dev/null +++ b/src/components-lib/signed-url-controller.ts @@ -0,0 +1,232 @@ +import { ReactiveController, ReactiveControllerHost } from 'lit'; +import { isEqual } from 'lodash-es'; +import { EnabledProxyConfig } from '../config/schema/common/proxy.js'; +import { homeAssistantGetSignedURLIfNecessary } from '../ha/sign-path.js'; +import { HomeAssistant } from '../ha/types.js'; +import { + CreateProxiedEndpointOptions, + createProxiedEndpointIfNecessary, +} from '../ha/web-proxy.js'; +import { Endpoint } from '../types.js'; +import { errorToConsole } from '../utils/basic.js'; + +const PROXY_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60; + +// Re-register and re-sign well before the signed URL expires. +const PROXY_CACHE_TTL_SECONDS = PROXY_URL_SIGN_EXPIRY_SECONDS / 2; + +interface SignedURLControllerOptions { + // The endpoint to resolve. The `sign` flag on the endpoint controls whether + // the URL requires HA authentication even when proxying is disabled (e.g. + // HA-relative API paths like go2rtc streams served through Frigate). + endpoint?: Endpoint; + + hass?: HomeAssistant; + proxyConfig?: EnabledProxyConfig | null; + proxyEndpointOptions?: CreateProxiedEndpointOptions; +} + +type SignedURLErrorType = 'sign' | 'proxy'; + +export class SignedURLController implements ReactiveController { + private _host: ReactiveControllerHost; + private _getOptionsCallback: () => SignedURLControllerOptions; + private _valueChangeCallback: (() => void) | undefined; + + private _value: string | null = null; + private _error: SignedURLErrorType | null = null; + private _cachedAt: Date | null = null; + + // Caching and race-condition state. + // The targetURL and proxy config are tracked to detect when inputs change and + // invalidate the cache. The requestID tracks the most recent valid fetch, to + // ensure that older, slower in-flight requests do not overwrite newer ones. + private _targetURL: string | null = null; + private _targetProxyConfig: EnabledProxyConfig | null = null; + private _requestID = 0; + + constructor( + host: ReactiveControllerHost, + getOptionsCallback: () => SignedURLControllerOptions, + valueChangeCallback?: () => void, + ) { + (this._host = host).addController(this); + this._getOptionsCallback = getOptionsCallback; + this._valueChangeCallback = valueChangeCallback; + } + + public getError(): SignedURLErrorType | null { + return this._error; + } + + public getValue(): string | null { + const options = this._getOptionsCallback(); + + // When the endpoint requires signing or proxying, the URL must go through + // the async resolution path. For proxied URLs, under no circumstances + // should we fall back to returning the unproxied URL — doing so risks + // leaking traffic or causing mixed-content errors. + if (options.proxyConfig?.enabled || options.endpoint?.sign) { + return this._value; + } + return options.endpoint?.endpoint ?? null; + } + + public hostDisconnected(): void { + ++this._requestID; + this._value = null; + this._error = null; + this._cachedAt = null; + this._targetURL = null; + this._targetProxyConfig = null; + } + + public async hostUpdate(): Promise { + const { hass, endpoint, proxyConfig, proxyEndpointOptions } = + this._getOptionsCallback(); + if (!hass || !endpoint || (!proxyConfig?.enabled && !endpoint.sign)) { + // Invalidate any in-flight async work so a stale proxy/sign result cannot + // repopulate the controller after inputs have been cleared or disabled. + ++this._requestID; + this._value = null; + this._error = null; + this._targetURL = null; + this._targetProxyConfig = null; + this._cachedAt = null; + return; + } + + const targetURL = new URL(endpoint.endpoint, document.baseURI).toString(); + + // Pick only the EnabledProxyConfig fields so that extraneous properties + // (e.g. `live`/`media` from CameraProxyConfig spreads) don't cause + // spurious cache invalidations. When only signing (no proxy), the config + // is null. + const comparableConfig: EnabledProxyConfig | null = proxyConfig?.enabled + ? { + dynamic: proxyConfig.dynamic, + ssl_verification: proxyConfig.ssl_verification, + ssl_ciphers: proxyConfig.ssl_ciphers, + enabled: proxyConfig.enabled, + enforce: proxyConfig.enforce, + } + : null; + + if ( + targetURL !== this._targetURL || + !isEqual(comparableConfig, this._targetProxyConfig) + ) { + this._targetURL = targetURL; + this._targetProxyConfig = comparableConfig; + this._cachedAt = null; + this._error = null; + this._value = null; + } else if ( + this._cachedAt && + new Date().getTime() - this._cachedAt.getTime() < PROXY_CACHE_TTL_SECONDS * 1000 + ) { + return; + } else if (!this._cachedAt) { + // Either async work for these exact inputs is already in flight, or + // we already failed for these exact inputs. Either way, don't + // restart: inputs must change before we retry. + return; + } + + // Mark as in-flight so the `!this._cachedAt` guard above prevents + // subsequent hostUpdate() calls from restarting the async work. + this._cachedAt = null; + const requestID = ++this._requestID; + + const resolvedEndpoint = await this._proxy( + hass, + targetURL, + endpoint, + proxyConfig, + proxyEndpointOptions, + ); + if (this._isStale(requestID)) { + return; + } + if (!resolvedEndpoint) { + this._applyError('proxy'); + return; + } + + const signedURL = await this._sign(hass, resolvedEndpoint); + if (this._isStale(requestID)) { + return; + } + if (!signedURL) { + this._applyError('sign'); + return; + } + + this._applySuccess(signedURL); + } + + /** + * Proxy the endpoint if proxying is enabled, otherwise return it as-is. + */ + private async _proxy( + hass: HomeAssistant, + targetURL: string, + endpoint: Endpoint, + proxyConfig: EnabledProxyConfig | null | undefined, + proxyEndpointOptions: CreateProxiedEndpointOptions | undefined, + ): Promise { + if (!proxyConfig?.enabled) { + return { endpoint: targetURL, sign: endpoint.sign }; + } + + try { + return await createProxiedEndpointIfNecessary( + hass, + { endpoint: targetURL, sign: false }, + proxyConfig, + { + ttl: PROXY_URL_SIGN_EXPIRY_SECONDS, + openLimit: 0, + ...proxyEndpointOptions, + }, + ); + } catch (e: unknown) { + errorToConsole(e as Error); + return null; + } + } + + /** + * Sign the endpoint if it requires signing, otherwise return the URL as-is. + */ + private async _sign(hass: HomeAssistant, endpoint: Endpoint): Promise { + try { + return await homeAssistantGetSignedURLIfNecessary( + hass, + endpoint, + PROXY_URL_SIGN_EXPIRY_SECONDS, + ); + } catch (e: unknown) { + errorToConsole(e as Error); + return null; + } + } + + private _isStale(requestID: number): boolean { + return this._requestID !== requestID; + } + + private _applySuccess(url: string): void { + this._value = url; + this._error = null; + this._valueChangeCallback?.(); + this._cachedAt = new Date(); + this._host.requestUpdate(); + } + + private _applyError(error: SignedURLErrorType): void { + this._value = null; + this._error = error; + this._host.requestUpdate(); + } +} diff --git a/src/components/image-updating-player.ts b/src/components/image-updating-player.ts index c4100c01..1edef9a7 100644 --- a/src/components/image-updating-player.ts +++ b/src/components/image-updating-player.ts @@ -11,12 +11,13 @@ import { customElement, property, state } from 'lit/decorators.js'; import { live } from 'lit/directives/live.js'; import { createRef, ref, Ref } from 'lit/directives/ref.js'; import { isEqual } from 'lodash-es'; -import { CameraManager } from '../camera-manager/manager.js'; import { getCameraEntityFromConfig } from '../camera-manager/utils/camera-entity-from-config.js'; import { CachedValueController } from '../components-lib/cached-value-controller.js'; import { UpdatingImageMediaPlayerController } from '../components-lib/media-player/updating-image.js'; +import { SignedURLController } from '../components-lib/signed-url-controller.js'; import { CameraConfig } from '../config/schema/cameras.js'; import { type ImageBaseConfig, ImageMode } from '../config/schema/common/image.js'; +import { EnabledProxyConfig } from '../config/schema/common/proxy.js'; import { isHassDifferent } from '../ha/is-hass-different.js'; import { HomeAssistant } from '../ha/types.js'; import defaultImage from '../images/iris-screensaver.jpg'; @@ -79,8 +80,8 @@ export class AdvancedCameraCardImageUpdatingPlayer @property({ attribute: false }) public cameraConfig?: CameraConfig; - @property({ attribute: false }) - public cameraManager?: CameraManager; + @property({ attribute: false, hasChanged: contentsChanged }) + public proxyConfig?: EnabledProxyConfig; // Using contentsChanged to ensure overridden configs (e.g. when the // 'show_image_during_load' option is true for live views, an overridden @@ -89,11 +90,36 @@ export class AdvancedCameraCardImageUpdatingPlayer public imageConfig?: ImageBaseConfig; @state() - private _message: Message | null = null; + private _imageLoadError = false; private _refImage: Ref = createRef(); - private _cachedValueController?: CachedValueController; + private _cachedValueController = new CachedValueController( + this, + () => this.imageConfig?.refresh_seconds ?? null, + () => this._getImageSource(), + () => dispatchMediaPlayEvent(this), + () => dispatchMediaPauseEvent(this), + // Clear image load errors on each timer tick so the next render retries the + // . Retries are bounded by refresh_seconds, not a tight loop. + () => { + this._imageLoadError = false; + }, + ); + + private _signedURLController = new SignedURLController( + this, + () => ({ + hass: this.hass, + endpoint: this.imageConfig?.url ? { endpoint: this.imageConfig.url } : undefined, + proxyConfig: this.proxyConfig, + }), + () => { + this._cachedValueController.clearValue(); + this._imageLoadError = false; + }, + ); + private _boundVisibilityHandler = this._visibilityHandler.bind(this); private _mediaLoadedInfo: MediaLoadedInfo | null = null; @@ -101,7 +127,7 @@ export class AdvancedCameraCardImageUpdatingPlayer private _mediaPlayerController = new UpdatingImageMediaPlayerController( this, () => this._refImage.value ?? null, - () => this._cachedValueController ?? null, + () => this._cachedValueController, ); public async getMediaPlayerController(): Promise { @@ -143,21 +169,6 @@ export class AdvancedCameraCardImageUpdatingPlayer * @param _changedProps The changed properties */ protected willUpdate(changedProps: PropertyValues): void { - if (changedProps.has('imageConfig')) { - if (this._cachedValueController) { - this._cachedValueController.removeController(); - } - if (this.imageConfig) { - this._cachedValueController = new CachedValueController( - this, - this.imageConfig.refresh_seconds, - this._getImageSource.bind(this), - () => dispatchMediaPlayEvent(this), - () => dispatchMediaPauseEvent(this), - ); - } - } - const relevantEntity = this._getRelevantEntityForMode( resolveImageMode({ imageConfig: this.imageConfig, @@ -170,20 +181,19 @@ export class AdvancedCameraCardImageUpdatingPlayer // the state is not acceptable, discard the old value (to allow a stock or // backup image to be displayed). if ( + changedProps.has('imageConfig') || changedProps.has('cameraConfig') || + changedProps.has('proxyConfig') || changedProps.has('view') || (relevantEntity && !this._getAcceptableState(relevantEntity)) ) { this._cachedValueController?.clearValue(); + this._imageLoadError = false; } - if (!this._cachedValueController?.value) { + if (!this._cachedValueController?.getValue()) { this._cachedValueController?.updateValue(); } - - if (['imageConfig', 'view'].some((prop) => changedProps.has(prop))) { - this._message = null; - } } /** @@ -220,7 +230,7 @@ export class AdvancedCameraCardImageUpdatingPlayer */ disconnectedCallback(): void { this._cachedValueController?.stopTimer(); - this._message = null; + this._imageLoadError = false; document.removeEventListener('visibilitychange', this._boundVisibilityHandler); super.disconnectedCallback(); } @@ -254,12 +264,23 @@ export class AdvancedCameraCardImageUpdatingPlayer } /** - * Build a working absolute image URL that the browser will not cache. - * @param url An input URL (may be relative to document origin) - * @returns A new URL as a string (absolute, will not be browser cached). + * Build an image URL that the browser will not cache. Supports two modes: + * - 'query-string': Appends a `_t` parameter. This is the most robust way to + * defeat caching (it bypasses HTTP caches) but it changes the path sent to + * the server and so can invalidate signed URLs. + * - 'fragment': Appends a `_t` fragment. This is less robust (the browser + * might still serve from its HTTP cache) but it does not change the URL + * sent to the server so it is safe for signed URLs. + * @param url The input URL. + * @param mode The cache-busting mode. + * @returns The cache-busted URL string. */ - private _buildImageURL(url: URL): string { - url.searchParams.append('_t', String(Date.now())); + private _buildCacheBustURL(url: URL, mode: 'query-string' | 'fragment'): string { + if (mode === 'query-string') { + url.searchParams.append('_t', String(Date.now())); + } else { + url.hash = `_t=${Date.now()}`; + } return url.toString(); } @@ -294,7 +315,7 @@ export class AdvancedCameraCardImageUpdatingPlayer if (state?.attributes.entity_picture) { const urlObj = new URL(state.attributes.entity_picture, document.baseURI); this._addQueryParametersToURL(urlObj, this.imageConfig?.entity_parameters); - return this._buildImageURL(urlObj); + return this._buildCacheBustURL(urlObj, 'query-string'); } } @@ -303,12 +324,21 @@ export class AdvancedCameraCardImageUpdatingPlayer if (state?.attributes.entity_picture) { const urlObj = new URL(state.attributes.entity_picture, document.baseURI); this._addQueryParametersToURL(urlObj, this.imageConfig?.entity_parameters); - return this._buildImageURL(urlObj); + return this._buildCacheBustURL(urlObj, 'query-string'); } } if (mode === 'url' && this.imageConfig?.url) { - return this._buildImageURL(new URL(this.imageConfig.url, document.baseURI)); + const url = this._signedURLController.getValue(); + if (url) { + const urlObj = new URL(url, document.baseURI); + if (this.proxyConfig?.enabled) { + // Use a fragment for cache-busting proxied URLs, as this does not + // change the path and thus preserves the validity of the signed URL. + return this._buildCacheBustURL(urlObj, 'fragment'); + } + return this._buildCacheBustURL(urlObj, 'query-string'); + } } return defaultImage; @@ -319,17 +349,43 @@ export class AdvancedCameraCardImageUpdatingPlayer */ private _forceSafeImage(stockOnly?: boolean): void { if (this._refImage.value) { - this._refImage.value.src = - !stockOnly && this.imageConfig?.url ? this.imageConfig.url : defaultImage; + // Avoid restoring the raw configured URL when proxying is enabled, since + // that would bypass the proxied/signed URL path on visibility changes. + const configuredURL = + !stockOnly && !this.proxyConfig?.enabled ? this.imageConfig?.url ?? null : null; + this._refImage.value.src = configuredURL ?? defaultImage; } } + private _getDisplayMessage(): Message | null { + const error = this._signedURLController.getError(); + if (error) { + return { + type: 'error', + message: localize( + error === 'proxy' ? 'error.failed_proxy' : 'error.failed_sign', + ), + context: this.proxyConfig, + }; + } + if (this._imageLoadError) { + return { + type: 'error', + message: localize('error.image_load_error'), + context: this.imageConfig, + }; + } + return null; + } + protected render(): TemplateResult | void { - if (this._message) { - return renderMessage(this._message); + const message = this._getDisplayMessage(); + if (message) { + return renderMessage(message); } - const src = this._cachedValueController?.value; + const src = this._cachedValueController?.getValue(); + // Note the use of live() below to ensure the update will restore the image // src if it's been changed via _forceSafeImage(). return src @@ -364,13 +420,7 @@ export class AdvancedCameraCardImageUpdatingPlayer // failed to load. this._forceSafeImage(true); } else if (mode === 'url') { - // In url mode, the user likely specified a URL that cannot be - // resolved. Show an error message. - this._message = { - type: 'error', - message: localize('error.image_load_error'), - context: this.imageConfig, - }; + this._imageLoadError = true; } }} /> diff --git a/src/components/image.ts b/src/components/image.ts index 669ecf21..e9ef1984 100644 --- a/src/components/image.ts +++ b/src/components/image.ts @@ -7,7 +7,11 @@ import { ViewManagerEpoch } from '../card-controller/view/types'; import { ZoomSettingsObserved } from '../components-lib/zoom/types'; import { handleZoomSettingsObservedEvent } from '../components-lib/zoom/zoom-view-context'; import { CameraConfig } from '../config/schema/cameras'; -import { ImageViewConfig } from '../config/schema/image'; +import { + type EnabledProxyConfig, + resolveProxyConfig, +} from '../config/schema/common/proxy'; +import { ImageViewConfig, type ImageViewProxyConfig } from '../config/schema/image'; import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../const'; import { HomeAssistant } from '../ha/types'; import { localize } from '../localize/localize.js'; @@ -82,6 +86,16 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer { : intermediateTemplate}`; } + private _resolveProxyConfig(proxy?: ImageViewProxyConfig): EnabledProxyConfig | null { + return proxy + ? { + ...resolveProxyConfig(proxy), + enabled: proxy.enabled, + enforce: proxy.enabled, + } + : null; + } + protected render(): TemplateResult | void { if (!this.hass) { return; @@ -108,6 +122,7 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer { .view=${this.viewManagerEpoch?.manager.getView()} .imageConfig=${this.imageConfig} .cameraConfig=${this.cameraConfig} + .proxyConfig=${this._resolveProxyConfig(this.imageConfig?.proxy) ?? undefined} > `); diff --git a/src/components/live/provider.ts b/src/components/live/provider.ts index 41fd5583..7717ae6f 100644 --- a/src/components/live/provider.ts +++ b/src/components/live/provider.ts @@ -302,6 +302,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP ${ref(this._refProvider)} .hass=${this.hass} .cameraConfig=${cameraConfig} + .proxyConfig=${this.camera.getLiveProxyConfig()} class=${classMap({ ...classes, // The image provider is providing the temporary loading image, diff --git a/src/components/live/providers/go2rtc/index.ts b/src/components/live/providers/go2rtc/index.ts index 989224d9..03345c71 100644 --- a/src/components/live/providers/go2rtc/index.ts +++ b/src/components/live/providers/go2rtc/index.ts @@ -6,35 +6,26 @@ import { TemplateResult, unsafeCSS, } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; +import { customElement, property } from 'lit/decorators.js'; import { Camera } from '../../../../camera-manager/camera.js'; import { CameraEndpoints } from '../../../../camera-manager/types.js'; import { MicrophoneState } from '../../../../card-controller/types.js'; import { dispatchLiveErrorEvent } from '../../../../components-lib/live/utils/dispatch-live-error.js'; import { VideoMediaPlayerController } from '../../../../components-lib/media-player/video.js'; +import { SignedURLController } from '../../../../components-lib/signed-url-controller.js'; import { MicrophoneConfig } from '../../../../config/schema/live.js'; -import { homeAssistantSignPath } from '../../../../ha/sign-path.js'; import { HomeAssistant } from '../../../../ha/types.js'; -import { createProxiedEndpointIfNecessary } from '../../../../ha/web-proxy.js'; import { localize } from '../../../../localize/localize.js'; import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss'; -import { MediaPlayer, MediaPlayerController, Message } from '../../../../types.js'; -import { errorToConsole } from '../../../../utils/basic.js'; +import { MediaPlayer, MediaPlayerController } from '../../../../types.js'; import { renderMessage } from '../../../message.js'; import { VideoRTC } from './video-rtc.js'; customElements.define('advanced-camera-card-live-go2rtc-player', VideoRTC); -// Note (2023-02-18): Depending on the behavior of the player / browser is -// possible this URL will need to be re-signed in order to avoid HA spamming -// logs after the expiry time, but this complexity is not added for now until -// there are verified cases of this being an issue (see equivalent in the JSMPEG -// provider). -const GO2RTC_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60; - @customElement('advanced-camera-card-live-go2rtc') export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer { - // Not an reactive property to avoid resetting the video. + // Not a reactive property to avoid resetting the video. public hass?: HomeAssistant; @property({ attribute: false }) @@ -52,10 +43,8 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer @property({ attribute: true, type: Boolean }) public controls = false; - @state() - private _message: Message | null = null; - private _player?: VideoRTC; + private _hasLiveError = false; private _mediaPlayerController = new VideoMediaPlayerController( this, @@ -63,13 +52,29 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer () => this.controls, ); + private _signedURLController = new SignedURLController( + this, + () => { + const endpoint = this.cameraEndpoints?.go2rtc; + if (!this.hass || !endpoint) { + return {}; + } + return { + hass: this.hass, + endpoint, + proxyConfig: this.camera?.getLiveProxyConfig(), + proxyEndpointOptions: { websocket: true }, + }; + }, + () => this._createPlayer(), + ); + public async getMediaPlayerController(): Promise { return this._mediaPlayerController; } disconnectedCallback(): void { this._player = undefined; - this._message = null; super.disconnectedCallback(); } @@ -81,82 +86,8 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer this.requestUpdate(); } - private _handleError(message: Message, e?: Error): void { - if (e) { - errorToConsole(e as Error); - } - - this._message = { - type: 'error', - ...message, - }; - dispatchLiveErrorEvent(this); - return; - } - - private async _getPlayerSource(): Promise { - const cameraConfig = this.camera?.getConfig(); - const proxyConfig = this.camera?.getProxyConfig(); - if (!this.hass || !cameraConfig) { - return null; - } - - const streamEndpoint = this.cameraEndpoints?.go2rtc; - if (!streamEndpoint) { - this._handleError({ - message: localize('error.live_camera_no_endpoint'), - context: cameraConfig, - }); - return null; - } - - let result: string | null = null; - - try { - const endpoint = await createProxiedEndpointIfNecessary( - this.hass, - streamEndpoint, - proxyConfig, - { - context: 'live', - ttl: GO2RTC_URL_SIGN_EXPIRY_SECONDS, - websocket: true, - - // The link may need to be opened multiple times. - openLimit: 0, - }, - ); - - if (endpoint.sign) { - result = await homeAssistantSignPath( - this.hass, - endpoint.endpoint, - GO2RTC_URL_SIGN_EXPIRY_SECONDS, - ); - if (!result) { - this._handleError({ - message: localize('error.failed_sign'), - context: cameraConfig, - }); - } - } else { - result = endpoint.endpoint; - } - } catch (e) { - this._handleError( - { - message: localize('error.failed_proxy'), - context: cameraConfig, - }, - e as Error, - ); - } - - return result; - } - - private async _createPlayer(): Promise { - const src = await this._getPlayerSource(); + private _createPlayer(): void { + const src = this._signedURLController.getValue(); if (!src) { return; } @@ -178,12 +109,20 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer protected willUpdate(changedProps: PropertyValues): void { if (changedProps.has('cameraEndpoints')) { - this._message = null; + // Clear old player; the new one is created by the + // SignedURLController's valueChangeCallback once the URL resolves. + this._player = undefined; } - if (!this._message && (!this._player || changedProps.has('cameraEndpoints'))) { - this._createPlayer(); + // Only treat a missing go2rtc endpoint as an error after cameraEndpoints + // has been explicitly set (not undefined / still loading). + const hasError = + !!this._signedURLController.getError() || + (!!this.cameraEndpoints && !this.cameraEndpoints.go2rtc); + if (hasError && !this._hasLiveError) { + dispatchLiveErrorEvent(this); } + this._hasLiveError = hasError; if (changedProps.has('controls') && this._player) { this._player.setControls(this.controls); @@ -203,8 +142,22 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer } protected render(): TemplateResult | void { - if (this._message) { - return renderMessage(this._message); + const error = this._signedURLController.getError(); + if (error) { + return renderMessage({ + type: 'error', + message: localize( + error === 'proxy' ? 'error.failed_proxy' : 'error.failed_sign', + ), + context: this.camera?.getConfig(), + }); + } + if (!this.cameraEndpoints?.go2rtc) { + return renderMessage({ + type: 'error', + message: localize('error.live_camera_no_endpoint'), + context: this.camera?.getConfig(), + }); } return html`${this._player}`; } @@ -216,6 +169,7 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer declare global { interface HTMLElementTagNameMap { + 'advanced-camera-card-live-go2rtc-player': VideoRTC; 'advanced-camera-card-live-go2rtc': AdvancedCameraCardGo2RTC; } } diff --git a/src/components/live/providers/image.ts b/src/components/live/providers/image.ts index 5433fb8b..55dad3f8 100644 --- a/src/components/live/providers/image.ts +++ b/src/components/live/providers/image.ts @@ -2,6 +2,7 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit import { customElement, property } from 'lit/decorators.js'; import { createRef, ref, Ref } from 'lit/directives/ref.js'; import { CameraConfig } from '../../../config/schema/cameras'; +import { EnabledProxyConfig } from '../../../config/schema/common/proxy'; import { HomeAssistant } from '../../../ha/types'; import basicBlockStyle from '../../../scss/basic-block.scss'; import { @@ -19,6 +20,9 @@ export class AdvancedCameraCardLiveImage extends LitElement implements MediaPlay @property({ attribute: false }) public cameraConfig?: CameraConfig; + @property({ attribute: false }) + public proxyConfig?: EnabledProxyConfig; + private _refImage: Ref = createRef(); public async getMediaPlayerController(): Promise { @@ -37,6 +41,7 @@ export class AdvancedCameraCardLiveImage extends LitElement implements MediaPlay .hass=${this.hass} .imageConfig=${this.cameraConfig.image} .cameraConfig=${this.cameraConfig} + .proxyConfig=${this.proxyConfig} > `; diff --git a/src/components/live/providers/jsmpeg.ts b/src/components/live/providers/jsmpeg.ts index 350f964e..a1627b99 100644 --- a/src/components/live/providers/jsmpeg.ts +++ b/src/components/live/providers/jsmpeg.ts @@ -14,7 +14,7 @@ import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispa import { JSMPEGMediaPlayerController } from '../../../components-lib/media-player/jsmpeg.js'; import { CameraConfig } from '../../../config/schema/cameras.js'; import { CardWideConfig } from '../../../config/schema/types.js'; -import { homeAssistantSignPath } from '../../../ha/sign-path.js'; +import { homeAssistantGetSignedURLIfNecessary } from '../../../ha/sign-path.js'; import { HomeAssistant } from '../../../ha/types.js'; import { localize } from '../../../localize/localize.js'; import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss'; @@ -186,9 +186,9 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla let response: string | null | undefined; try { - response = await homeAssistantSignPath( + response = await homeAssistantGetSignedURLIfNecessary( this.hass, - endpoint.endpoint, + endpoint, JSMPEG_URL_SIGN_EXPIRY_SECONDS, ); } catch (e) { diff --git a/src/components/viewer/provider.ts b/src/components/viewer/provider.ts index 52559c4c..cbfc8e85 100644 --- a/src/components/viewer/provider.ts +++ b/src/components/viewer/provider.ts @@ -6,13 +6,14 @@ import { TemplateResult, unsafeCSS, } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; +import { customElement, property } from 'lit/decorators.js'; import { guard } from 'lit/directives/guard.js'; import { createRef, Ref, ref } from 'lit/directives/ref.js'; import { CameraManager } from '../../camera-manager/manager.js'; import { QueryType } from '../../camera-manager/types.js'; import { ViewManagerEpoch } from '../../card-controller/view/types.js'; import { LazyLoadController } from '../../components-lib/lazy-load-controller.js'; +import { SignedURLController } from '../../components-lib/signed-url-controller.js'; import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js'; import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js'; import { CameraConfig } from '../../config/schema/cameras.js'; @@ -21,17 +22,16 @@ import { ViewerConfig } from '../../config/schema/viewer.js'; import { canonicalizeHAURL } from '../../ha/canonical-url.js'; import { isHARelativeURL } from '../../ha/is-ha-relative-url.js'; import { ResolvedMediaCache, resolveMedia } from '../../ha/resolved-media.js'; -import { homeAssistantSignPath } from '../../ha/sign-path.js'; -import { HomeAssistant, ResolvedMedia } from '../../ha/types.js'; -import { createProxiedEndpointIfNecessary } from '../../ha/web-proxy.js'; +import { HomeAssistant } from '../../ha/types.js'; +import { localize } from '../../localize/localize.js'; import '../../patches/ha-hls-player.js'; import viewerProviderStyle from '../../scss/viewer-provider.scss'; import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../../types.js'; -import { errorToConsole } from '../../utils/basic.js'; import { ViewItemClassifier } from '../../view/item-classifier.js'; import { VideoContentType, ViewMedia } from '../../view/item.js'; import { UnifiedQueryTransformer } from '../../view/unified-query-transformer.js'; import '../image-player.js'; +import { renderMessage } from '../message.js'; import { renderProgressIndicator } from '../progress-indicator.js'; import '../video-player.js'; import './../media-dimensions-container'; @@ -60,15 +60,32 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi public cardWideConfig?: CardWideConfig; private _refProvider: Ref = createRef(); - private _refContainer: Ref = createRef(); private _lazyLoadController: LazyLoadController = new LazyLoadController(this); - @state() - private _url: string | null = null; + private _resolvedMediaURL: string | null = null; + + private _signedURLController = new SignedURLController(this, () => { + if (!this.hass || !this._resolvedMediaURL) { + return {}; + } + // HA-relative URLs need no proxying or signing. + if (isHARelativeURL(this._resolvedMediaURL)) { + return { + endpoint: { endpoint: canonicalizeHAURL(this.hass, this._resolvedMediaURL) }, + }; + } + const cameraID = this.media?.getCameraID(); + const camera = cameraID ? this.cameraManager?.getStore().getCamera(cameraID) : null; + return { + hass: this.hass, + endpoint: { endpoint: this._resolvedMediaURL }, + proxyConfig: camera?.getMediaProxyConfig(), + }; + }); constructor() { super(); - this._lazyLoadController.addListener((loaded) => loaded && this._setURL()); + this._lazyLoadController.addListener((loaded) => loaded && this._resolveURL()); } public async getMediaPlayerController(): Promise { @@ -108,69 +125,23 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi }); } - private async _setURL(): Promise { - const mediaContentID = this.media?.getContentID(); - if ( - !this.media || - !mediaContentID || - !this.hass || - !this._lazyLoadController?.isLoaded() - ) { + private async _resolveURL(): Promise { + const contentID = this.media?.getContentID(); + if (!contentID || !this.hass || !this._lazyLoadController?.isLoaded()) { + this._resolvedMediaURL = null; return; } - let resolvedMedia: ResolvedMedia | null = - this.resolvedMediaCache?.get(mediaContentID) ?? null; - if (!resolvedMedia) { - resolvedMedia = await resolveMedia( - this.hass, - mediaContentID, - this.resolvedMediaCache, - ); - } + // Clear immediately so the SignedURLController doesn't see a stale URL + // from the previous media item during the async gap. + this._resolvedMediaURL = null; - if (!resolvedMedia) { - return; - } + const resolved = + this.resolvedMediaCache?.get(contentID) ?? + (await resolveMedia(this.hass, contentID, this.resolvedMediaCache)); - const unsignedURL = resolvedMedia.url; - if (isHARelativeURL(unsignedURL)) { - // No need to proxy or sign local resolved URLs. - this._url = canonicalizeHAURL(this.hass, unsignedURL); - return; - } - - const cameraID = this.media.getCameraID(); - const camera = cameraID ? this.cameraManager?.getStore().getCamera(cameraID) : null; - const proxyConfig = camera?.getProxyConfig(); - - if (!proxyConfig) { - this._url = unsignedURL; - return; - } - - try { - // Create endpoint from unsigned URL - it doesn't need signing initially - const unsignedEndpoint = { endpoint: unsignedURL, sign: false }; - const proxiedEndpoint = await createProxiedEndpointIfNecessary( - this.hass, - unsignedEndpoint, - proxyConfig, - { - context: 'media', - // The link may need to be opened multiple times. - openLimit: 0, - }, - ); - - if (proxiedEndpoint.sign) { - this._url = await homeAssistantSignPath(this.hass, proxiedEndpoint.endpoint); - } else { - this._url = proxiedEndpoint.endpoint; - } - } catch (e) { - errorToConsole(e as Error); - } + this._resolvedMediaURL = resolved?.url ?? null; + this.requestUpdate(); } protected willUpdate(changedProps: PropertyValues): void { @@ -187,7 +158,7 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi changedProps.has('resolvedMediaCache') || changedProps.has('hass') ) { - this._setURL(); + this._resolveURL(); } if (changedProps.has('viewerConfig') && this.viewerConfig?.zoomable) { @@ -260,7 +231,19 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi return; } - if (!this._url) { + const error = this._signedURLController.getError(); + if (error) { + return renderMessage({ + type: 'error', + message: localize( + error === 'proxy' ? 'error.failed_proxy' : 'error.failed_sign', + ), + context: this.media?.getContentID(), + }); + } + + const url = this._signedURLController.getValue(); + if (!url) { return renderProgressIndicator({ cardWideConfig: this.cardWideConfig, }); @@ -280,7 +263,7 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi muted playsinline title="${this.media.getTitle() ?? ''}" - url=${this._url} + url=${url} .hass=${this.hass} ?controls=${this.viewerConfig.controls.builtin} > @@ -288,7 +271,7 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi : html` { diff --git a/src/config/schema/cameras.ts b/src/config/schema/cameras.ts index 5aa21671..cd1e632a 100644 --- a/src/config/schema/cameras.ts +++ b/src/config/schema/cameras.ts @@ -5,6 +5,7 @@ import { ptzCameraConfigDefaults, ptzCameraConfigSchema } from './camera/ptz'; import { aspectRatioSchema } from './common/aspect-ratio'; import { eventsMediaTypeSchema } from './common/events-media'; import { imageBaseConfigDefault, imageBaseConfigSchema } from './common/image'; +import { proxyBaseConfigDefault, proxyBaseConfigSchema } from './common/proxy'; import { severitySchema } from './common/severity'; const CAMERA_TRIGGER_EVENT_TYPES = [ @@ -143,32 +144,18 @@ export const cameraConfigDefault = { }, }, proxy: { - dynamic: true, + ...proxyBaseConfigDefault, live: 'auto' as const, media: 'auto' as const, - ssl_ciphers: 'auto' as const, - ssl_verification: 'auto' as const, }, go2rtc: go2rtcConfigDefault, image: imageBaseConfigDefault, always_error_if_entity_unavailable: false, }; -const SSL_CIPHERS = ['default', 'insecure', 'intermediate', 'modern'] as const; -export type SSLCiphers = (typeof SSL_CIPHERS)[number]; - -const proxyConfigSchema = z.object({ +const proxyConfigSchema = proxyBaseConfigSchema.extend({ live: z.boolean().or(z.literal('auto')).default(cameraConfigDefault.proxy.live), media: z.boolean().or(z.literal('auto')).default(cameraConfigDefault.proxy.media), - dynamic: z.boolean().default(cameraConfigDefault.proxy.dynamic), - ssl_verification: z - .boolean() - .or(z.literal('auto')) - .default(cameraConfigDefault.proxy.ssl_verification), - ssl_ciphers: z - .enum(SSL_CIPHERS) - .or(z.literal('auto')) - .default(cameraConfigDefault.proxy.ssl_ciphers), }); const rotationSchema = z diff --git a/src/config/schema/common/image.ts b/src/config/schema/common/image.ts index d374c1e9..1f883402 100644 --- a/src/config/schema/common/image.ts +++ b/src/config/schema/common/image.ts @@ -5,11 +5,6 @@ export const imageBaseConfigDefault = { refresh_seconds: 1, }; -export const imageConfigDefault = { - ...imageBaseConfigDefault, - zoomable: true, -}; - const IMAGE_MODES = ['auto', 'camera', 'entity', 'screensaver', 'url'] as const; export type ImageMode = (typeof IMAGE_MODES)[number]; diff --git a/src/config/schema/common/proxy.ts b/src/config/schema/common/proxy.ts new file mode 100644 index 00000000..49188d1c --- /dev/null +++ b/src/config/schema/common/proxy.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; + +const SSL_CIPHERS = ['default', 'insecure', 'intermediate', 'modern'] as const; +type SSLCiphers = (typeof SSL_CIPHERS)[number]; + +export const proxyBaseConfigDefault = { + dynamic: true, + ssl_ciphers: 'auto' as const, + ssl_verification: 'auto' as const, +}; + +export const proxyBaseConfigSchema = z.object({ + dynamic: z.boolean().default(proxyBaseConfigDefault.dynamic), + ssl_verification: z + .boolean() + .or(z.literal('auto')) + .default(proxyBaseConfigDefault.ssl_verification), + ssl_ciphers: z + .enum(SSL_CIPHERS) + .or(z.literal('auto')) + .default(proxyBaseConfigDefault.ssl_ciphers), +}); +type UnresolvedProxyConfig = z.output; + +export interface ResolvedProxyConfig { + dynamic: boolean; + ssl_verification: boolean; + ssl_ciphers: SSLCiphers; +} + +export interface EnabledProxyConfig extends ResolvedProxyConfig { + enabled: boolean; + + // Whether proxying is a strict requirement. When false, callers may fall + // back to the original URL if the proxy integration is unavailable. + enforce?: boolean; +} + +export const resolveProxyConfig = ( + config: UnresolvedProxyConfig, +): ResolvedProxyConfig => ({ + dynamic: config.dynamic, + ssl_verification: config.ssl_verification === 'auto' ? true : config.ssl_verification, + ssl_ciphers: config.ssl_ciphers === 'auto' ? 'default' : config.ssl_ciphers, +}); diff --git a/src/config/schema/image.ts b/src/config/schema/image.ts index 5140a89e..2b2ade16 100644 --- a/src/config/schema/image.ts +++ b/src/config/schema/image.ts @@ -1,9 +1,25 @@ import { z } from 'zod'; import { actionsSchema } from './actions/types'; -import { imageBaseConfigSchema, imageConfigDefault } from './common/image'; +import { imageBaseConfigDefault, imageBaseConfigSchema } from './common/image'; +import { proxyBaseConfigDefault, proxyBaseConfigSchema } from './common/proxy'; + +export const imageConfigDefault = { + ...imageBaseConfigDefault, + proxy: { + ...proxyBaseConfigDefault, + enabled: false, + }, + zoomable: true, +}; + +const imageProxyConfigSchema = proxyBaseConfigSchema.extend({ + enabled: z.boolean().default(imageConfigDefault.proxy.enabled), +}); +export type ImageViewProxyConfig = z.infer; export const imageConfigSchema = imageBaseConfigSchema .extend({ + proxy: imageProxyConfigSchema.optional(), zoomable: z.boolean().default(imageConfigDefault.zoomable), }) .extend(actionsSchema.shape) diff --git a/src/config/schema/types.ts b/src/config/schema/types.ts index df7ed2f6..e282aa10 100644 --- a/src/config/schema/types.ts +++ b/src/config/schema/types.ts @@ -3,12 +3,11 @@ import { deepRemoveDefaults } from '../../utils/zod/deep-remove-defaults'; import { automationsSchema } from './automations'; import { cameraConfigDefault, cameraConfigSchema, camerasConfigSchema } from './cameras'; import { cardIDRegex } from './common/const'; -import { imageConfigDefault } from './common/image'; import { DebugConfig, debugConfigDefault, debugConfigSchema } from './debug'; import { dimensionsConfigSchema } from './dimensions'; import { pictureElementsSchema } from './elements/types'; import { foldersConfigSchema } from './folders'; -import { imageConfigSchema } from './image'; +import { imageConfigDefault, imageConfigSchema } from './image'; import { liveConfigDefault, liveConfigSchema } from './live'; import { mediaGalleryConfigDefault, mediaGalleryConfigSchema } from './media-gallery'; import { menuConfigDefault, menuConfigSchema } from './menu'; diff --git a/src/const.ts b/src/const.ts index 76af50cf..60241c1b 100644 --- a/src/const.ts +++ b/src/const.ts @@ -364,6 +364,11 @@ const CONF_IMAGE = 'image' as const; export const CONF_IMAGE_ENTITY = `${CONF_IMAGE}.entity` as const; export const CONF_IMAGE_ENTITY_PARAMETERS = `${CONF_IMAGE}.entity_parameters` as const; export const CONF_IMAGE_MODE = `${CONF_IMAGE}.mode` as const; +export const CONF_IMAGE_PROXY_DYNAMIC = `${CONF_IMAGE}.proxy.dynamic` as const; +export const CONF_IMAGE_PROXY_ENABLED = `${CONF_IMAGE}.proxy.enabled` as const; +export const CONF_IMAGE_PROXY_SSL_CIPHERS = `${CONF_IMAGE}.proxy.ssl_ciphers` as const; +export const CONF_IMAGE_PROXY_SSL_VERIFICATION = + `${CONF_IMAGE}.proxy.ssl_verification` as const; export const CONF_IMAGE_REFRESH_SECONDS = `${CONF_IMAGE}.refresh_seconds` as const; export const CONF_IMAGE_URL = `${CONF_IMAGE}.url` as const; diff --git a/src/editor.ts b/src/editor.ts index ee65f1de..9c74b30b 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -110,6 +110,10 @@ import { CONF_IMAGE_ENTITY, CONF_IMAGE_ENTITY_PARAMETERS, CONF_IMAGE_MODE, + CONF_IMAGE_PROXY_DYNAMIC, + CONF_IMAGE_PROXY_ENABLED, + CONF_IMAGE_PROXY_SSL_CIPHERS, + CONF_IMAGE_PROXY_SSL_VERIFICATION, CONF_IMAGE_REFRESH_SECONDS, CONF_IMAGE_URL, CONF_LIVE_AUTO_MUTE, @@ -315,6 +319,7 @@ const MENU_MEDIA_VIEWER_CONTROLS_TIMELINE = 'media_viewer.controls.timeline'; const MENU_MEDIA_VIEWER_CONTROLS_TIMELINE_FORMAT = 'media_viewer.controls.timeline.format'; const MENU_MEDIA_VIEWER_DISPLAY = 'media_viewer.display'; +const MENU_IMAGE_PROXY = 'image.proxy'; const MENU_MENU_BUTTONS = 'menu.buttons'; const MENU_OPTIONS = 'options'; const MENU_PERFORMANCE_FEATURES = 'performance.features'; @@ -388,6 +393,7 @@ const SUBMENU_DOC_LINKS: Record = { [MENU_MEDIA_VIEWER_CONTROLS_TIMELINE]: 'configuration/media-viewer?id=timeline', [MENU_MEDIA_VIEWER_CONTROLS_TIMELINE_FORMAT]: 'configuration/media-viewer?id=format', [MENU_MEDIA_VIEWER_DISPLAY]: 'configuration/media-viewer?id=display', + [MENU_IMAGE_PROXY]: 'configuration/image?id=proxy', [MENU_MENU_BUTTONS]: 'configuration/menu?id=buttons', [MENU_OPTIONS]: 'configuration/README', [MENU_PERFORMANCE_FEATURES]: 'configuration/performance?id=features', @@ -1053,15 +1059,15 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard { value: '', label: '' }, { value: 'auto', - label: localize('config.cameras.proxy.modes.auto'), + label: localize('config.common.proxy.modes.auto'), }, { value: true, - label: localize('config.cameras.proxy.modes.true'), + label: localize('config.common.proxy.modes.true'), }, { value: false, - label: localize('config.cameras.proxy.modes.false'), + label: localize('config.common.proxy.modes.false'), }, ]; @@ -1069,23 +1075,23 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard { value: '', label: '' }, { value: 'auto', - label: localize('config.cameras.proxy.ssl_ciphers.auto'), + label: localize('config.common.proxy.ssl_ciphers.auto'), }, { value: 'default', - label: localize('config.cameras.proxy.ssl_ciphers.default'), + label: localize('config.common.proxy.ssl_ciphers.default'), }, { value: 'insecure', - label: localize('config.cameras.proxy.ssl_ciphers.insecure'), + label: localize('config.common.proxy.ssl_ciphers.insecure'), }, { value: 'intermediate', - label: localize('config.cameras.proxy.ssl_ciphers.intermediate'), + label: localize('config.common.proxy.ssl_ciphers.intermediate'), }, { value: 'modern', - label: localize('config.cameras.proxy.ssl_ciphers.modern'), + label: localize('config.common.proxy.ssl_ciphers.modern'), }, ]; @@ -1093,15 +1099,15 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard { value: '', label: '' }, { value: 'auto', - label: localize('config.cameras.proxy.ssl_verification.auto'), + label: localize('config.common.proxy.ssl_verification.auto'), }, { value: true, - label: localize('config.cameras.proxy.ssl_verification.true'), + label: localize('config.common.proxy.ssl_verification.true'), }, { value: false, - label: localize('config.cameras.proxy.ssl_verification.false'), + label: localize('config.common.proxy.ssl_verification.false'), }, ]; @@ -2102,6 +2108,9 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard configPathEntity: string, configPathEntityParameters: string, configPathRefreshSeconds: string, + options?: { + proxyMenu?: TemplateResult; + }, ): TemplateResult { return html` ${this._renderOptionSelector(configPathMode, this._imageModes, { @@ -2123,9 +2132,64 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard ${this._renderNumberInput(configPathRefreshSeconds, { label: localize('config.common.image.refresh_seconds'), })} + ${options?.proxyMenu ?? html``} `; } + private _renderProxySubmenu( + domain: string, + key: unknown, + labelPath: string, + configPathDynamic: string, + dynamicDefault: boolean, + configPathSSLCiphers: string, + configPathSSLVerification: string, + options?: { + configPathEnabled?: string; + configPathLive?: string; + configPathMedia?: string; + enabledDefault?: boolean; + }, + ): TemplateResult { + return this._putInSubmenu( + domain, + key, + labelPath, + 'mdi:arrow-decision', + html` + ${options?.configPathEnabled !== undefined && + options.enabledDefault !== undefined + ? this._renderSwitch(options.configPathEnabled, options.enabledDefault, { + label: localize('config.common.proxy.modes.true'), + }) + : html``} + ${options?.configPathLive + ? this._renderOptionSelector(options.configPathLive, this._proxyModes, { + label: localize('config.cameras.proxy.live'), + }) + : html``} + ${options?.configPathMedia + ? this._renderOptionSelector(options.configPathMedia, this._proxyModes, { + label: localize('config.cameras.proxy.media'), + }) + : html``} + ${this._renderSwitch(configPathDynamic, dynamicDefault, { + label: localize('config.common.proxy.dynamic'), + })} + ${this._renderOptionSelector( + configPathSSLVerification, + this._proxySSLVerification, + { + label: localize('config.common.proxy.ssl_verification.editor_label'), + }, + )} + ${this._renderOptionSelector(configPathSSLCiphers, this._proxySSLCiphers, { + label: localize('config.common.proxy.ssl_ciphers.editor_label'), + })} + `, + ); + } + private _modifyConfig(func: (config: RawAdvancedCameraCardConfig) => boolean): void { if (this._config) { const newConfig = copyConfig(this._config); @@ -2835,53 +2899,27 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard )} `, )} - ${this._putInSubmenu( + ${this._renderProxySubmenu( MENU_CAMERAS_PROXY, cameraIndex, 'config.cameras.proxy.editor_label', - 'mdi:arrow-decision', - html` - ${this._renderOptionSelector( - getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_LIVE, cameraIndex), - this._proxyModes, - { - label: localize('config.cameras.proxy.live'), - }, - )} - ${this._renderOptionSelector( - getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_MEDIA, cameraIndex), - this._proxyModes, - { - label: localize('config.cameras.proxy.media'), - }, - )} - ${this._renderSwitch( - getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_DYNAMIC, cameraIndex), - this._defaults.cameras.proxy.dynamic, - )} - ${this._renderOptionSelector( - getArrayConfigPath( - CONF_CAMERAS_ARRAY_PROXY_SSL_VERIFICATION, - cameraIndex, - ), - this._proxySSLVerification, - { - label: localize( - 'config.cameras.proxy.ssl_verification.editor_label', - ), - }, - )} - ${this._renderOptionSelector( - getArrayConfigPath( - CONF_CAMERAS_ARRAY_PROXY_SSL_CIPHERS, - cameraIndex, - ), - this._proxySSLCiphers, - { - label: localize('config.cameras.proxy.ssl_ciphers.editor_label'), - }, - )} - `, + getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_DYNAMIC, cameraIndex), + this._defaults.cameras.proxy.dynamic, + getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_SSL_CIPHERS, cameraIndex), + getArrayConfigPath( + CONF_CAMERAS_ARRAY_PROXY_SSL_VERIFICATION, + cameraIndex, + ), + { + configPathLive: getArrayConfigPath( + CONF_CAMERAS_ARRAY_PROXY_LIVE, + cameraIndex, + ), + configPathMedia: getArrayConfigPath( + CONF_CAMERAS_ARRAY_PROXY_MEDIA, + cameraIndex, + ), + }, )} ` : ``} @@ -3470,6 +3508,21 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard CONF_IMAGE_ENTITY, CONF_IMAGE_ENTITY_PARAMETERS, CONF_IMAGE_REFRESH_SECONDS, + { + proxyMenu: this._renderProxySubmenu( + MENU_IMAGE_PROXY, + true, + 'config.common.image.proxy.editor_label', + CONF_IMAGE_PROXY_DYNAMIC, + this._defaults.image.proxy.dynamic, + CONF_IMAGE_PROXY_SSL_CIPHERS, + CONF_IMAGE_PROXY_SSL_VERIFICATION, + { + configPathEnabled: CONF_IMAGE_PROXY_ENABLED, + enabledDefault: this._defaults.image.proxy.enabled, + }, + ), + }, )} ` : ''} diff --git a/src/ha/fetch.ts b/src/ha/fetch.ts index 3af7cfb8..da909c0c 100644 --- a/src/ha/fetch.ts +++ b/src/ha/fetch.ts @@ -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 ( timeoutSeconds?: number; }, ): Promise => { - 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; diff --git a/src/ha/sign-path.ts b/src/ha/sign-path.ts index 4e70b566..a052ba60 100644 --- a/src/ha/sign-path.ts +++ b/src/ha/sign-path.ts @@ -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 { + if (!endpoint.sign) { + return endpoint.endpoint; + } + return await homeAssistantSignPath(hass, endpoint.endpoint, expires); +} diff --git a/src/ha/web-proxy.ts b/src/ha/web-proxy.ts index 50865ee2..025cf544 100644 --- a/src/ha/web-proxy.ts +++ b/src/ha/web-proxy.ts @@ -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 => { - const context = options?.context ?? 'media'; - if (!proxyConfig || !shouldUseWebProxy(hass, proxyConfig, context)) { +): Promise => { + 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, diff --git a/src/localize/languages/de.json b/src/localize/languages/de.json index 7cd01ab8..f6251ac5 100644 --- a/src/localize/languages/de.json +++ b/src/localize/languages/de.json @@ -64,38 +64,18 @@ "rotation": "Rotation", "rotations": { "0": "Keine Rotation", - "90": "90 Grad im Uhrzeigersinn", "180": "180 Grad im Uhrzeigersinn", - "270": "270 Grad im Uhrzeigersinn" + "270": "270 Grad im Uhrzeigersinn", + "90": "90 Grad im Uhrzeigersinn" } }, "go2rtc": { "url": "go2rtc URL" }, "proxy": { - "dynamic": "Dynamischer Proxy", "editor_label": "Kamera Proxy", "live": "Live Proxy", - "media": "Medien Proxy", - "modes": { - "auto": "Proxy automatisch konfigurieren", - "false": "Proxy deaktiviert", - "true": "Proxy aktiviert" - }, - "ssl_ciphers": { - "auto": "SSL ciphers automatisch konfiguriert", - "default": "Standard SSL ciphers", - "editor_label": "SSL ciphers", - "insecure": "Unsichere SSL ciphers", - "intermediate": "Zwischen SSL ciphers", - "modern": "Moderne SSL ciphers" - }, - "ssl_verification": { - "auto": "SSL Überprüfung automatisch konfiguriert", - "editor_label": "SSL Überprüfung", - "false": "SSL Überprüfung deaktiviert", - "true": "SSL Überprüfung aktiviert" - } + "media": "Medien Proxy" }, "reolink": { "editor_label": "Reolink Optionen", @@ -116,10 +96,6 @@ } }, "common": { - "media_types": { - "events": "Ereignisse", - "recordings": "Aufzeichnungen" - }, "controls": { "builtin": "Eingebettete Video Bedienelemente", "thumbnails": { @@ -165,6 +141,32 @@ "microphone_mute": "Bei Mikrofon Stummschaltung", "microphone_unmute": "Bei Mikrofon Reaktivierung" }, + "media_types": { + "events": "Ereignisse", + "recordings": "Aufzeichnungen" + }, + "proxy": { + "dynamic": "Dynamischer Proxy", + "modes": { + "auto": "Proxy automatisch konfigurieren", + "false": "Proxy deaktiviert", + "true": "Proxy aktiviert" + }, + "ssl_ciphers": { + "auto": "SSL ciphers automatisch konfiguriert", + "default": "Standard SSL ciphers", + "editor_label": "SSL ciphers", + "insecure": "Unsichere SSL ciphers", + "intermediate": "Zwischen SSL ciphers", + "modern": "Moderne SSL ciphers" + }, + "ssl_verification": { + "auto": "SSL Überprüfung automatisch konfiguriert", + "editor_label": "SSL Überprüfung", + "false": "SSL Überprüfung deaktiviert", + "true": "SSL Überprüfung aktiviert" + } + }, "timeline": { "style": "Zeitleisten Stil", "styles": { diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 4771dc2e..34943a89 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -173,29 +173,9 @@ "url": "MotionEye UI URL" }, "proxy": { - "dynamic": "Dynamic proxying", "editor_label": "Camera proxying", "live": "Live proxying", - "media": "Media proxying", - "modes": { - "auto": "Proxying automatically configured", - "false": "Proxying disabled", - "true": "Proxying enabled" - }, - "ssl_ciphers": { - "auto": "SSL ciphers automatically configured", - "default": "Default SSL ciphers", - "editor_label": "SSL ciphers", - "insecure": "Insecure SSL ciphers", - "intermediate": "Intermediate SSL ciphers", - "modern": "Modern SSL ciphers" - }, - "ssl_verification": { - "auto": "SSL verification automatically configured", - "editor_label": "SSL verification", - "false": "SSL verification disabled", - "true": "SSL verification enabled" - } + "media": "Media proxying" }, "reolink": { "editor_label": "Reolink options", @@ -322,6 +302,9 @@ "screensaver": "Embedded screensaver image", "url": "Arbitrary image specified by URL" }, + "proxy": { + "editor_label": "Image proxying" + }, "refresh_seconds": "Number of seconds after which to refresh (0=never)", "url": "Static image URL" }, @@ -342,6 +325,28 @@ "recordings": "Recordings", "reviews": "Reviews" }, + "proxy": { + "dynamic": "Dynamic proxying", + "modes": { + "auto": "Proxying automatically configured", + "false": "Proxying disabled", + "true": "Proxying enabled" + }, + "ssl_ciphers": { + "auto": "SSL ciphers automatically configured", + "default": "Default SSL ciphers", + "editor_label": "SSL ciphers", + "insecure": "Insecure SSL ciphers", + "intermediate": "Intermediate SSL ciphers", + "modern": "Modern SSL ciphers" + }, + "ssl_verification": { + "auto": "SSL verification automatically configured", + "editor_label": "SSL verification", + "false": "SSL verification disabled", + "true": "SSL verification enabled" + } + }, "timeline": { "clustering_threshold": "The count of events at which they are clustered (0=no clustering)", "events_media_type": "The events media the timeline displays", diff --git a/src/localize/languages/fr.json b/src/localize/languages/fr.json index fd670592..25d653cd 100644 --- a/src/localize/languages/fr.json +++ b/src/localize/languages/fr.json @@ -127,27 +127,7 @@ "url": "URL de l’interface MotionEye" }, "proxy": { - "dynamic": "Proxy dynamique", - "editor_label": "Proxy de la caméra", - "modes": { - "auto": "Proxy automatique", - "false": "Proxy désactivé", - "true": "Proxy activé" - }, - "ssl_ciphers": { - "auto": "Chiffrement SSL automatique", - "default": "Chiffrement SSL par défaut", - "editor_label": "Chiffrement SSL", - "insecure": "Chiffrement SSL non sécurisé", - "intermediate": "Chiffrement SSL intermédiaire", - "modern": "Chiffrement SSL moderne" - }, - "ssl_verification": { - "auto": "Vérification SSL automatique", - "editor_label": "Vérification SSL", - "false": "Vérification SSL désactivée", - "true": "Vérification SSL activée" - } + "editor_label": "Proxy de la caméra" }, "reolink": { "editor_label": "Option Reolink", @@ -178,15 +158,6 @@ } }, "common": { - "media_types": { - "events": "Événements", - "recordings": "Enregistrements" - }, - "events_media_types": { - "all": "Tous types de médias", - "clips": "Extraits", - "snapshots": "Instantanés" - }, "controls": { "builtin": "Commandes vidéo intégrées", "filter": { @@ -257,6 +228,11 @@ "grid_selected_width_factor": "Augmenter la largeur du média sélectionnée par ce facteur", "mode": "Mode" }, + "events_media_types": { + "all": "Tous types de médias", + "clips": "Extraits", + "snapshots": "Instantanés" + }, "image": { "entity": "Entité à utiliser avec le mode entité", "entity_parameters": "Paramètres de requête ajoutés aux URL des images basées sur l'entité (par exemple, width=1920&height=1080)", @@ -279,6 +255,32 @@ "unselected": "Lors de la désélection", "visible": "Sur la visibilité du navigateur/onglet" }, + "media_types": { + "events": "Événements", + "recordings": "Enregistrements" + }, + "proxy": { + "dynamic": "Proxy dynamique", + "modes": { + "auto": "Proxy automatique", + "false": "Proxy désactivé", + "true": "Proxy activé" + }, + "ssl_ciphers": { + "auto": "Chiffrement SSL automatique", + "default": "Chiffrement SSL par défaut", + "editor_label": "Chiffrement SSL", + "insecure": "Chiffrement SSL non sécurisé", + "intermediate": "Chiffrement SSL intermédiaire", + "modern": "Chiffrement SSL moderne" + }, + "ssl_verification": { + "auto": "Vérification SSL automatique", + "editor_label": "Vérification SSL", + "false": "Vérification SSL désactivée", + "true": "Vérification SSL activée" + } + }, "timeline": { "clustering_threshold": "Nombre d'événements pour lesquels ils sont regroupés (0 = pas de clustering)", "events_media_type": "Médias affichés par la chronologie", diff --git a/src/localize/languages/pl.json b/src/localize/languages/pl.json index 2c3f219c..d71e94a0 100644 --- a/src/localize/languages/pl.json +++ b/src/localize/languages/pl.json @@ -83,9 +83,9 @@ "rotation": "Obrót", "rotations": { "0": "Brak obrotu", - "90": "90 stopni zgodnie z zegarem", "180": "180 stopni zgodnie z zegarem", - "270": "270 stopni zgodnie z zegarem" + "270": "270 stopni zgodnie z zegarem", + "90": "90 stopni zgodnie z zegarem" } }, "engines": { @@ -143,29 +143,9 @@ "url": "URL interfejsu MotionEye" }, "proxy": { - "dynamic": "Proxy dynamiczne", "editor_label": "Proxy kamery", "live": "Proxy na żywo", - "media": "Proxy mediów", - "modes": { - "auto": "Proxy skonfigurowane automatycznie", - "false": "Proxy wyłączone", - "true": "Proxy włączone" - }, - "ssl_ciphers": { - "auto": "Szyfry SSL skonfigurowane automatycznie", - "default": "Domyślne szyfry SSL", - "editor_label": "Szyfry SSL", - "insecure": "Niezabezpieczone szyfry SSL", - "intermediate": "Pośrednie szyfry SSL", - "modern": "Nowoczesne szyfry SSL" - }, - "ssl_verification": { - "auto": "Weryfikacja SSL skonfigurowana automatycznie", - "editor_label": "Weryfikacja SSL", - "false": "Weryfikacja SSL wyłączona", - "true": "Weryfikacja SSL włączona" - } + "media": "Proxy mediów" }, "reolink": { "editor_label": "Opcje Reolink", @@ -302,6 +282,28 @@ "unselected": "Po odznaczeniu", "visible": "Gdy przeglądarka/karta jest widoczna" }, + "proxy": { + "dynamic": "Proxy dynamiczne", + "modes": { + "auto": "Proxy skonfigurowane automatycznie", + "false": "Proxy wyłączone", + "true": "Proxy włączone" + }, + "ssl_ciphers": { + "auto": "Szyfry SSL skonfigurowane automatycznie", + "default": "Domyślne szyfry SSL", + "editor_label": "Szyfry SSL", + "insecure": "Niezabezpieczone szyfry SSL", + "intermediate": "Pośrednie szyfry SSL", + "modern": "Nowoczesne szyfry SSL" + }, + "ssl_verification": { + "auto": "Weryfikacja SSL skonfigurowana automatycznie", + "editor_label": "Weryfikacja SSL", + "false": "Weryfikacja SSL wyłączona", + "true": "Weryfikacja SSL włączona" + } + }, "timeline": { "clustering_threshold": "Liczba zdarzeń, przy której są grupowane (0=brak)", "events_media_type": "Typ mediów wyświetlany na osi czasu", diff --git a/src/utils/live-provider.ts b/src/utils/live-provider.ts index fe41d330..13ec43d8 100644 --- a/src/utils/live-provider.ts +++ b/src/utils/live-provider.ts @@ -1,7 +1,7 @@ -import { CameraProxyConfig } from '../camera-manager/types'; import { supports2WayAudio as gortcSupports2WayAudio } from '../camera-manager/utils/go2rtc/audio'; import { CameraConfig } from '../config/schema/cameras'; import { LiveProvider } from '../config/schema/cameras.js'; +import { EnabledProxyConfig } from '../config/schema/common/proxy'; import { HomeAssistant } from '../ha/types'; import { Endpoint } from '../types'; @@ -27,7 +27,7 @@ export const liveProviderSupports2WayAudio = async ( config: CameraConfig, metadataFetchTimeoutSeconds: number, go2rtcMetadataEndpoint?: Endpoint | null, - proxyConfig?: CameraProxyConfig, + proxyConfig?: EnabledProxyConfig, ): Promise => { if (getResolvedLiveProvider(config) !== 'go2rtc') { return false; diff --git a/tests/camera-manager/camera.test.ts b/tests/camera-manager/camera.test.ts index 25a6bb8b..5c47a353 100644 --- a/tests/camera-manager/camera.test.ts +++ b/tests/camera-manager/camera.test.ts @@ -133,7 +133,12 @@ describe('Camera', () => { 'http://go2rtc/api/streams?src=stream&video=all&audio=allµphone', sign: false, }, - expect.anything(), + expect.objectContaining({ + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default', + enabled: false, + }), ); expect(camera.getCapabilities()?.has('2-way-audio')).toBe(true); @@ -184,7 +189,99 @@ describe('Camera', () => { expect.anything(), 20, expect.anything(), + expect.objectContaining({ enabled: false }), + ); + }); + + it('should pass proxy config when web proxy is available', async () => { + const camera = new Camera( + createCameraConfig({ + go2rtc: { + url: 'http://go2rtc', + stream: 'stream', + }, + proxy: { + live: true, + }, + }), + new GenericCameraManagerEngine(mock()), + ); + + vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true); + + const hass = createHASS(); + hass.config.components = ['hass_web_proxy']; + + await camera.initialize({ + hass, + stateWatcher: mock(), + }); + + expect(liveProviderSupports2WayAudio).toHaveBeenCalledWith( expect.anything(), + expect.anything(), + 2, + expect.anything(), + { + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default', + live: true, + media: false, + enabled: true, + enforce: true, + }, + ); + + expect(camera.getCapabilities()?.has('2-way-audio')).toBe(true); + }); + + it('should return live proxy config', () => { + const camera = new Camera( + createCameraConfig({ + proxy: { live: true }, + }), + new GenericCameraManagerEngine(mock()), + ); + expect(camera.getLiveProxyConfig()).toEqual( + expect.objectContaining({ enabled: true, enforce: true }), + ); + }); + + it('should return media proxy config', () => { + const camera = new Camera( + createCameraConfig({ + proxy: { media: true }, + }), + new GenericCameraManagerEngine(mock()), + ); + expect(camera.getMediaProxyConfig()).toEqual( + expect.objectContaining({ enabled: true, enforce: true }), + ); + }); + + it('should not enforce live proxy config when live proxying is auto', () => { + const camera = new Camera( + createCameraConfig({ + live_provider: 'go2rtc', + go2rtc: { url: 'http://go2rtc' }, + }), + new GenericCameraManagerEngine(mock()), + ); + expect(camera.getLiveProxyConfig()).toEqual( + expect.objectContaining({ enabled: true, enforce: false }), + ); + }); + + it('should not enforce media proxy config when media proxying is auto', () => { + const camera = new Camera( + createCameraConfig({ + proxy: { media: 'auto' }, + }), + new GenericCameraManagerEngine(mock()), + ); + expect(camera.getMediaProxyConfig()).toEqual( + expect.objectContaining({ enabled: false, enforce: false }), ); }); diff --git a/tests/camera-manager/utils/go2rtc-audio.test.ts b/tests/camera-manager/utils/go2rtc-audio.test.ts index 97dc9e62..5c8207f1 100644 --- a/tests/camera-manager/utils/go2rtc-audio.test.ts +++ b/tests/camera-manager/utils/go2rtc-audio.test.ts @@ -119,4 +119,10 @@ describe('supports2WayAudio', () => { expect(await supports2WayAudio(hass, 2, endpoint)).toBe(false); }); + + it('should return false if proxied endpoint is null', async () => { + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue(null); + + expect(await supports2WayAudio(hass, 2, endpoint)).toBe(false); + }); }); diff --git a/tests/card-controller/actions/actions/set.test.ts b/tests/card-controller/actions/actions/set.test.ts index e05d6f6a..cd0ece7e 100644 --- a/tests/card-controller/actions/actions/set.test.ts +++ b/tests/card-controller/actions/actions/set.test.ts @@ -6,7 +6,7 @@ import { createCardAPI } from '../../../test-utils'; describe('ActionSet', () => { it('should execute single action', async () => { const api = createCardAPI(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const set = new ActionSet({}, createLogAction('Hello, world!')); const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined); @@ -30,7 +30,7 @@ describe('ActionSet', () => { it('should stop execution', async () => { const api = createCardAPI(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const set = new ActionSet({}, createLogAction('Hello, world!')); const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined); diff --git a/tests/card-controller/view/item-manager.test.ts b/tests/card-controller/view/item-manager.test.ts index c9cf1e3d..5c75a433 100644 --- a/tests/card-controller/view/item-manager.test.ts +++ b/tests/card-controller/view/item-manager.test.ts @@ -1,7 +1,7 @@ import { format } from 'date-fns'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { ViewItemManager } from '../../../src/card-controller/view/item-manager'; -import { homeAssistantSignPath } from '../../../src/ha/sign-path.js'; +import { homeAssistantGetSignedURLIfNecessary } from '../../../src/ha/sign-path.js'; import { downloadURL } from '../../../src/utils/download'; import { ViewFolder, ViewMediaType } from '../../../src/view/item'; import { @@ -57,6 +57,12 @@ describe('ViewItemManager', () => { }); describe('download', () => { + beforeEach(() => { + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockImplementation( + async (_hass, endpoint) => endpoint.endpoint, + ); + }); + afterEach(() => { vi.restoreAllMocks(); }); @@ -85,7 +91,7 @@ describe('ViewItemManager', () => { }); const signError = new Error('sign-error'); - vi.mocked(homeAssistantSignPath).mockRejectedValue(signError); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockRejectedValue(signError); expect(await manager.download(item)).toBe(false); expect(api.getMessageManager().setErrorIfHigherPriority).toHaveBeenCalledWith( @@ -108,7 +114,9 @@ describe('ViewItemManager', () => { endpoint: 'foo', }); - vi.mocked(homeAssistantSignPath).mockResolvedValue('http://foo/signed-url'); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://foo/signed-url', + ); expect(await manager.download(item)).toBe(true); expect(downloadURL).toBeCalledWith( @@ -129,7 +137,9 @@ describe('ViewItemManager', () => { endpoint: 'foo', }); - vi.mocked(homeAssistantSignPath).mockResolvedValue('http://foo/signed-url'); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://foo/signed-url', + ); expect(await manager.download(item)).toBe(true); expect(downloadURL).toBeCalledWith('http://foo/signed-url', 'media_id.mp4'); @@ -147,8 +157,6 @@ describe('ViewItemManager', () => { endpoint: 'foo', }); - expect(homeAssistantSignPath).not.toBeCalled(); - expect(await manager.download(item)).toBe(true); expect(downloadURL).toBeCalledWith('foo', 'camera-office_id.mp4'); }); diff --git a/tests/components-lib/cached-value-controller.test.ts b/tests/components-lib/cached-value-controller.test.ts index 3fbcff62..e2073445 100644 --- a/tests/components-lib/cached-value-controller.test.ts +++ b/tests/components-lib/cached-value-controller.test.ts @@ -9,25 +9,50 @@ describe('CachedValueController', () => { vi.useRealTimers(); }); + it('should not restart timer on hostUpdate if not connected', () => { + const host = mock(); + const callback = vi.fn().mockReturnValue(42); + let refreshSeconds: number | null = 10; + + const controller = new CachedValueController(host, () => refreshSeconds, callback); + + Object.defineProperty(host, 'isConnected', { get: () => false }); + + refreshSeconds = 20; + controller.hostUpdate(); + expect(controller.hasTimer()).toBeFalsy(); + }); + + it('should not restart timer if unchanged on hostUpdate', () => { + const host = mock(); + const callback = vi.fn().mockReturnValue(42); + const refreshSeconds: number | null = 10; + + const controller = new CachedValueController(host, () => refreshSeconds, callback); + + Object.defineProperty(host, 'isConnected', { get: () => true }); + + // This starts the timer as isConnected is true + controller.hostConnected(); + + const startTimerSpy = vi.spyOn(controller, 'startTimer'); + + controller.hostUpdate(); + + // Should not restart since refreshSeconds hasn't changed + expect(startTimerSpy).not.toBeCalled(); + }); + it('should construct', () => { - const host = mock(); + const host = mock(); const callback = vi.fn(); - const controller = new CachedValueController(host, 10, callback); + const controller = new CachedValueController(host, () => 10, callback); expect(controller).toBeTruthy(); }); - it('should remove host', () => { - const host = mock(); - const callback = vi.fn(); - const controller = new CachedValueController(host, 10, callback); - - controller.removeController(); - expect(host.removeController).toBeCalled(); - }); - it('should have timer', () => { - const host = mock(); + const host = mock(); const callback = vi.fn(); const startCallback = vi.fn(); const stopCallback = vi.fn(); @@ -36,7 +61,7 @@ describe('CachedValueController', () => { const controller = new CachedValueController( host, - 10, + () => 10, callback, startCallback, stopCallback, @@ -49,13 +74,13 @@ describe('CachedValueController', () => { vi.runOnlyPendingTimers(); expect(callback).toBeCalled(); expect(host.requestUpdate).toBeCalled(); - expect(controller.value).toBe(3); + expect(controller.getValue()).toBe(3); callback.mockReturnValue(4); vi.runOnlyPendingTimers(); expect(callback).toBeCalled(); expect(host.requestUpdate).toBeCalled(); - expect(controller.value).toBe(4); + expect(controller.getValue()).toBe(4); expect(controller.hasTimer()).toBeTruthy(); @@ -68,42 +93,129 @@ describe('CachedValueController', () => { }); it('should clear value', () => { - const host = mock(); + const host = mock(); const callback = vi.fn().mockReturnValue(42); vi.useFakeTimers(); - const controller = new CachedValueController(host, 10, callback); + const controller = new CachedValueController(host, () => 10, callback); controller.startTimer(); vi.runOnlyPendingTimers(); - expect(controller.value).equal(42); + expect(controller.getValue()).equal(42); controller.clearValue(); - expect(controller.value).toBeUndefined(); + expect(controller.getValue()).toBeNull(); }); it('should connect and disconnect host', () => { - const host = mock(); + const host = mock(); const callback = vi.fn().mockReturnValue(43); const startCallback = vi.fn(); const stopCallback = vi.fn(); const controller = new CachedValueController( host, - 10, + () => 10, callback, startCallback, stopCallback, ); controller.hostConnected(); - expect(controller.value).equal(43); + expect(controller.getValue()).equal(43); expect(startCallback).toBeCalled(); expect(host.requestUpdate).toBeCalled(); controller.hostDisconnected(); - expect(controller.value).toBeUndefined(); + expect(controller.getValue()).toBeNull(); expect(stopCallback).toBeCalled(); }); + + it('should call timer tick callback on each tick before updateValue', () => { + const host = mock(); + const callback = vi.fn().mockReturnValue('value'); + const tickCallback = vi.fn(); + + vi.useFakeTimers(); + + const controller = new CachedValueController( + host, + () => 5, + callback, + undefined, + undefined, + tickCallback, + ); + + controller.startTimer(); + + vi.runOnlyPendingTimers(); + expect(tickCallback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledTimes(1); + + vi.runOnlyPendingTimers(); + expect(tickCallback).toHaveBeenCalledTimes(2); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('should not call timerTickCallback on manual updateValue', () => { + const host = mock(); + const callback = vi.fn().mockReturnValue('value'); + const tickCallback = vi.fn(); + + const controller = new CachedValueController( + host, + () => 5, + callback, + undefined, + undefined, + tickCallback, + ); + + controller.updateValue(); + expect(callback).toHaveBeenCalledTimes(1); + expect(tickCallback).not.toHaveBeenCalled(); + }); + + it('should restart timer with new interval on hostUpdate', () => { + const host = mock(); + const callback = vi.fn().mockReturnValue(42); + let refreshSeconds: number | null = null; + + const controller = new CachedValueController(host, () => refreshSeconds, callback); + + vi.useFakeTimers(); + Object.defineProperty(host, 'isConnected', { get: () => true }); + controller.hostConnected(); + expect(controller.hasTimer()).toBeFalsy(); + + refreshSeconds = 20; + controller.hostUpdate(); + + // Timer should have been restarted. Fast forward 15 seconds. If it didn't + // restart, it would fire at 10 seconds. Since it restarted at 20 seconds, + // it shouldn't fire at 15 seconds. + callback.mockClear(); + vi.advanceTimersByTime(15 * 1000); + expect(callback).not.toBeCalled(); + + vi.advanceTimersByTime(5 * 1000); + expect(callback).toBeCalled(); + + // Now set it to null -> stops timer + refreshSeconds = null; + controller.hostUpdate(); + expect(controller.hasTimer()).toBeFalsy(); + + // Now set it to 0 -> stops timer + refreshSeconds = 0; + controller.hostUpdate(); + expect(controller.hasTimer()).toBeFalsy(); + + // Now set it to negative -> stops timer + refreshSeconds = -1; + controller.hostUpdate(); + expect(controller.hasTimer()).toBeFalsy(); + }); }); diff --git a/tests/components-lib/media-player/updating-image.test.ts b/tests/components-lib/media-player/updating-image.test.ts index 5398a4e3..daed79f2 100644 --- a/tests/components-lib/media-player/updating-image.test.ts +++ b/tests/components-lib/media-player/updating-image.test.ts @@ -144,7 +144,7 @@ describe('UpdatingImageMediaPlayerController', () => { it('should return screenshot URL with cached value controller', async () => { const url = 'data:image/png;base64,'; const cachedValueController = mock>(); - Object.defineProperty(cachedValueController, 'value', { value: url }); + cachedValueController.getValue.mockReturnValue(url); const controller = new UpdatingImageMediaPlayerController( createLitElement(), diff --git a/tests/components-lib/signed-url-controller.test.ts b/tests/components-lib/signed-url-controller.test.ts new file mode 100644 index 00000000..2376f864 --- /dev/null +++ b/tests/components-lib/signed-url-controller.test.ts @@ -0,0 +1,1004 @@ +import { ReactiveControllerHost } from 'lit'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { mock } from 'vitest-mock-extended'; +import { SignedURLController } from '../../src/components-lib/signed-url-controller'; +import { homeAssistantGetSignedURLIfNecessary } from '../../src/ha/sign-path'; +import { createProxiedEndpointIfNecessary } from '../../src/ha/web-proxy'; +import { Endpoint } from '../../src/types'; +import { createHASS, flushPromises } from '../test-utils'; + +vi.mock('../../src/ha/sign-path'); +vi.mock('../../src/ha/web-proxy'); + +const createEndpoint = (url: string, sign?: boolean): Endpoint => ({ + endpoint: url, + ...(sign !== undefined && { sign }), +}); + +// @vitest-environment jsdom +describe('SignedURLController', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should initialize correctly', () => { + const host = mock(); + const controller = new SignedURLController(host, () => ({})); + + expect(host.addController).toBeCalledWith(controller); + expect(controller.getValue()).toBeNull(); + }); + + it('should fetch proxied url successfully', async () => { + const host = mock(); + const hass = createHASS(); + const proxyConfig = { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }; + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied-url.com', + sign: true, + }); + + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://signed-proxied-url.com', + ); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig, + })); + + controller.hostUpdate(); + + expect(controller.getValue()).toBeNull(); + + expect(createProxiedEndpointIfNecessary).toBeCalledWith( + hass, + { endpoint: 'http://test-url.com/', sign: false }, + proxyConfig, + { ttl: 86400, openLimit: 0 }, + ); + + await flushPromises(); + + expect(homeAssistantGetSignedURLIfNecessary).toBeCalled(); + expect(controller.getValue()).toBe('http://signed-proxied-url.com'); + expect(host.requestUpdate).toBeCalled(); + }); + + it('should not fetch if inputs are missing', async () => { + const host = mock(); + const controller = new SignedURLController(host, () => ({})); + + controller.hostUpdate(); + await flushPromises(); + + expect(createProxiedEndpointIfNecessary).not.toBeCalled(); + expect(controller.getValue()).toBeNull(); + }); + + it('should return unproxied url if proxy is disabled', async () => { + const host = mock(); + const hass = createHASS(); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + })); + + controller.hostUpdate(); + await flushPromises(); + + expect(createProxiedEndpointIfNecessary).not.toBeCalled(); + expect(controller.getValue()).toBe('http://test-url.com'); + }); + + it('should clear value if inputs turn invalid', async () => { + const host = mock(); + const hass = createHASS(); + let endpoint: Endpoint | undefined = createEndpoint('http://test-url.com'); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied-url.com', + sign: false, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://signed-proxied-url.com', + ); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint, + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }, + })); + + controller.hostUpdate(); + await flushPromises(); + expect(controller.getValue()).toBe('http://signed-proxied-url.com'); + + // Make endpoint invalid + endpoint = undefined; + controller.hostUpdate(); + + expect(controller.getValue()).toBeNull(); + }); + + it('should call valueChangeCallback when a new URL is successfully resolved', async () => { + const host = mock(); + const hass = createHASS(); + const valueChangeCallback = vi.fn(); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied-url.com', + sign: false, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://signed-proxied-url.com', + ); + + const controller = new SignedURLController( + host, + () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }, + }), + valueChangeCallback, + ); + + controller.hostUpdate(); + await flushPromises(); + + expect(controller.getValue()).toBe('http://signed-proxied-url.com'); + expect(valueChangeCallback).toHaveBeenCalledTimes(1); + }); + + it('should not call valueChangeCallback on null proxy endpoint', async () => { + const host = mock(); + const hass = createHASS(); + const valueChangeCallback = vi.fn(); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue(null); + + const controller = new SignedURLController( + host, + () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }, + }), + valueChangeCallback, + ); + + controller.hostUpdate(); + await flushPromises(); + + expect(controller.getValue()).toBeNull(); + expect(valueChangeCallback).not.toBeCalled(); + }); + + it('should not call valueChangeCallback on null signed URL', async () => { + const host = mock(); + const hass = createHASS(); + const valueChangeCallback = vi.fn(); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied-url.com', + sign: true, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue(null); + + const controller = new SignedURLController( + host, + () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }, + }), + valueChangeCallback, + ); + + controller.hostUpdate(); + await flushPromises(); + + expect(controller.getValue()).toBeNull(); + expect(valueChangeCallback).not.toBeCalled(); + }); + + it('should ignore successful fetch if inputs become invalid', async () => { + const host = mock(); + const hass = createHASS(); + let endpoint: Endpoint | undefined = createEndpoint('http://test-url.com'); + + let resolveProxy: ((value: Endpoint) => void) | undefined; + vi.mocked(createProxiedEndpointIfNecessary).mockReturnValueOnce( + new Promise((resolve) => { + resolveProxy = resolve; + }), + ); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue('stale'); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint, + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }, + })); + + controller.hostUpdate(); + + endpoint = undefined; + controller.hostUpdate(); + + resolveProxy?.({ endpoint: 'old', sign: false }); + await flushPromises(); + + expect(controller.getValue()).toBeNull(); + expect(host.requestUpdate).not.toBeCalled(); + }); + + it('should invalidate cache if input changes', async () => { + const host = mock(); + const hass = createHASS(); + let url = 'http://test-url.com'; + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied-url.com', + sign: false, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://signed-proxied-url.com', + ); + + const proxyConfig = { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }; + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint(url), + proxyConfig, + })); + + controller.hostUpdate(); + await flushPromises(); + expect(controller.getValue()).toBe('http://signed-proxied-url.com'); + + // Change url + url = 'http://new-url.com'; + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://new-signed-proxied-url.com', + ); + + controller.hostUpdate(); + + expect(controller.getValue()).toBeNull(); + + await flushPromises(); + + expect(controller.getValue()).toBe('http://new-signed-proxied-url.com'); + }); + + it('should invalidate cache if proxy config changes', async () => { + const host = mock(); + const hass = createHASS(); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied-url.com', + sign: false, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://signed-proxied-url.com', + ); + + let proxyConfig = { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }; + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig, + })); + + controller.hostUpdate(); + await flushPromises(); + expect(controller.getValue()).toBe('http://signed-proxied-url.com'); + + // Change a relevant proxy config field. + proxyConfig = { ...proxyConfig, dynamic: false }; + + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://new-signed-proxied-url.com', + ); + + controller.hostUpdate(); + expect(controller.getValue()).toBeNull(); + + await flushPromises(); + expect(controller.getValue()).toBe('http://new-signed-proxied-url.com'); + }); + + it('should handle errors gracefully', async () => { + const host = mock(); + const hass = createHASS(); + const consoleSpy = vi.spyOn(console, 'warn').mockReturnValue(); + + vi.mocked(createProxiedEndpointIfNecessary).mockRejectedValue( + new Error('test-error'), + ); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }, + })); + + controller.hostUpdate(); + await flushPromises(); + + expect(controller.getValue()).toBeNull(); + expect(controller.getError()).toBe('proxy'); + expect(host.requestUpdate).toBeCalledTimes(1); + consoleSpy.mockRestore(); + }); + + it('should not retry after sign error with same inputs', async () => { + const host = mock(); + const hass = createHASS(); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied-url.com', + sign: true, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue(null); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + }, + })); + + await controller.hostUpdate(); + + expect(controller.getValue()).toBeNull(); + expect(controller.getError()).toBe('sign'); + expect(homeAssistantGetSignedURLIfNecessary).toHaveBeenCalledTimes(1); + + // Re-render with same inputs should NOT retry. + await controller.hostUpdate(); + expect(homeAssistantGetSignedURLIfNecessary).toHaveBeenCalledTimes(1); + }); + + it('should set sign error when signing throws', async () => { + const host = mock(); + const hass = createHASS(); + const consoleSpy = vi.spyOn(console, 'error').mockReturnValue(undefined); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied-url.com', + sign: true, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockRejectedValue( + new Error('sign failure'), + ); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + }, + })); + + await controller.hostUpdate(); + + expect(controller.getValue()).toBeNull(); + expect(controller.getError()).toBe('sign'); + consoleSpy.mockRestore(); + }); + + it('should not fetch again if within cache TTL', async () => { + const host = mock(); + const hass = createHASS(); + + vi.useFakeTimers(); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied-url.com', + sign: false, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://signed-proxied-url.com', + ); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default', + live: true, + media: true, + }, + })); + + controller.hostUpdate(); + await flushPromises(); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); + + // Call again within TTL, inputs unchanged + controller.hostUpdate(); + await flushPromises(); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); + + vi.useRealTimers(); + }); + + it('should re-fetch after cache TTL expires', async () => { + const host = mock(); + const hass = createHASS(); + + vi.useFakeTimers(); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied-url.com', + sign: false, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://signed-proxied-url.com', + ); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }, + })); + + controller.hostUpdate(); + await flushPromises(); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); + + // Advance time past the TTL (12 hours + 1 second). + vi.advanceTimersByTime(12 * 60 * 60 * 1000 + 1000); + + controller.hostUpdate(); + await flushPromises(); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(2); + + vi.useRealTimers(); + }); + + it('should not re-enter while re-signing after cache expiry', async () => { + const host = mock(); + const hass = createHASS(); + + vi.useFakeTimers(); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied-url.com', + sign: false, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://signed-proxied-url.com', + ); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + }, + })); + + controller.hostUpdate(); + await flushPromises(); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); + + // Advance past the default TTL. + vi.advanceTimersByTime(12 * 60 * 60 * 1000 + 1000); + + // First call after expiry starts a re-sign. + controller.hostUpdate(); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(2); + + // Subsequent calls while in-flight should NOT start another re-sign. + controller.hostUpdate(); + controller.hostUpdate(); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(2); + + await flushPromises(); + expect(controller.getValue()).toBe('http://signed-proxied-url.com'); + + vi.useRealTimers(); + }); + + it('should ignore successful fetch if request ID changed', async () => { + const host = mock(); + const hass = createHASS(); + let url = 'http://test-url.com'; + + let resolveProxy: ((value: Endpoint) => void) | undefined; + vi.mocked(createProxiedEndpointIfNecessary) + .mockReturnValueOnce( + new Promise((resolve) => { + resolveProxy = resolve; + }), + ) + .mockReturnValue( + // Request 2 pending forever. + new Promise(() => {}), + ); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue('ok'); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint(url), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default', + live: true, + media: true, + }, + })); + + // Request 1 starts. + controller.hostUpdate(); + + url = 'http://new-url.com'; + // Request 2 starts + controller.hostUpdate(); + + resolveProxy?.({ endpoint: 'old', sign: false }); + await flushPromises(); + + expect(controller.getValue()).toBeNull(); + }); + + it('should ignore failed fetch if request ID changed', async () => { + const host = mock(); + const hass = createHASS(); + let url = 'http://test-url.com'; + + let rejectProxy: ((error: Error) => void) | undefined; + vi.mocked(createProxiedEndpointIfNecessary) + .mockReturnValueOnce( + new Promise((_, reject) => { + rejectProxy = reject; + }), + ) + .mockReturnValue( + // Pending forever for request 2. + new Promise(() => {}), + ); + vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint(url), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default', + live: true, + media: true, + }, + })); + + // Request 1 starts. + controller.hostUpdate(); + + url = 'http://new-url.com'; + + // Request 2 starts. + controller.hostUpdate(); + + rejectProxy?.(new Error('fail')); + await flushPromises(); + + expect(host.requestUpdate).not.toBeCalled(); + }); + + it('should clear value if proxy endpoint is null', async () => { + const host = mock(); + const hass = createHASS(); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue(null); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }, + })); + + controller.hostUpdate(); + await flushPromises(); + + expect(controller.getValue()).toBeNull(); + expect(controller.getError()).toBe('proxy'); + expect(host.requestUpdate).toBeCalled(); + }); + + it('should not retry after proxy error with same inputs', async () => { + const host = mock(); + const hass = createHASS(); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue(null); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + }, + })); + + await controller.hostUpdate(); + expect(controller.getError()).toBe('proxy'); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); + + // Re-render with same inputs should NOT retry. + await controller.hostUpdate(); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); + }); + + it('should not restart in-flight work on re-render with same inputs', async () => { + const host = mock(); + const hass = createHASS(); + + let resolveProxy: ((value: Endpoint) => void) | undefined; + vi.mocked(createProxiedEndpointIfNecessary).mockReturnValueOnce( + new Promise((resolve) => { + resolveProxy = resolve; + }), + ); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://signed-proxied-url.com', + ); + + const proxyConfig = { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }; + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig, + })); + + // First call starts async work. + controller.hostUpdate(); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); + + // Second call with identical inputs should be a no-op (in-flight guard). + controller.hostUpdate(); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); + + // Resolve the original request — should still succeed. + resolveProxy?.({ endpoint: 'http://proxied-url.com', sign: false }); + await flushPromises(); + + expect(controller.getValue()).toBe('http://signed-proxied-url.com'); + expect(host.requestUpdate).toHaveBeenCalledTimes(1); + }); + + it('should not invalidate cache when extraneous proxy config fields change', async () => { + const host = mock(); + const hass = createHASS(); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied.com', + sign: false, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://signed.com', + ); + + // Start with extraneous `live: true`. + let extraneous = true; + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: extraneous, + }, + })); + + await controller.hostUpdate(); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); + expect(controller.getValue()).toBe('http://signed.com'); + + // Change only the extraneous field — should hit the cache, not re-fetch. + extraneous = false; + await controller.hostUpdate(); + expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1); + }); + + it('should reset all state on hostDisconnected', async () => { + const host = mock(); + const hass = createHASS(); + + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied-url.com', + sign: false, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://signed-proxied-url.com', + ); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + }, + })); + + controller.hostUpdate(); + await flushPromises(); + expect(controller.getValue()).toBe('http://signed-proxied-url.com'); + expect(controller.getError()).toBeNull(); + + controller.hostDisconnected(); + + expect(controller.getValue()).toBeNull(); + expect(controller.getError()).toBeNull(); + }); + + it('should discard in-flight result after hostDisconnected', async () => { + const host = mock(); + const hass = createHASS(); + + let resolveProxy: ((value: Endpoint) => void) | undefined; + vi.mocked(createProxiedEndpointIfNecessary).mockReturnValueOnce( + new Promise((resolve) => { + resolveProxy = resolve; + }), + ); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://signed.com', + ); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('http://test-url.com'), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + }, + })); + + controller.hostUpdate(); + + controller.hostDisconnected(); + + resolveProxy?.({ endpoint: 'http://proxied.com', sign: false }); + await flushPromises(); + + expect(controller.getValue()).toBeNull(); + expect(host.requestUpdate).not.toBeCalled(); + }); + + it('should ignore stale null signed URL after request ID changed', async () => { + const host = mock(); + const hass = createHASS(); + let url = 'http://test-url.com'; + + let resolveSign: ((value: string | null) => void) | undefined; + vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue({ + endpoint: 'http://proxied.com', + sign: true, + }); + vi.mocked(homeAssistantGetSignedURLIfNecessary) + .mockReturnValueOnce( + new Promise((resolve) => { + resolveSign = resolve; + }), + ) + .mockReturnValue(new Promise(() => {})); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint(url), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + }, + })); + + controller.hostUpdate(); + await flushPromises(); + + // Change URL to start a new request. + url = 'http://new-url.com'; + controller.hostUpdate(); + + // Resolve the first signing request with null (stale). + resolveSign?.(null); + await flushPromises(); + + // Stale result should be discarded. + expect(host.requestUpdate).not.toBeCalled(); + }); + + it('should sign endpoint without proxying when sign is set', async () => { + const host = mock(); + const hass = createHASS(); + + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://ha.local/api/some/endpoint?authSig=abc', + ); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint('/api/some/endpoint', true), + })); + + expect(controller.getValue()).toBeNull(); + + controller.hostUpdate(); + await flushPromises(); + + expect(createProxiedEndpointIfNecessary).not.toBeCalled(); + expect(homeAssistantGetSignedURLIfNecessary).toBeCalled(); + expect(controller.getValue()).toBe('http://ha.local/api/some/endpoint?authSig=abc'); + expect(host.requestUpdate).toBeCalled(); + }); + + it('should return url directly when sign is false and proxy is disabled', () => { + const host = mock(); + + const controller = new SignedURLController(host, () => ({ + hass: createHASS(), + endpoint: createEndpoint('http://example.com/stream'), + })); + + expect(controller.getValue()).toBe('http://example.com/stream'); + }); + + it('should ignore null proxy endpoint if request ID changed', async () => { + const host = mock(); + const hass = createHASS(); + let url = 'http://test-url.com'; + + let resolveProxy: ((value: Endpoint | null) => void) | undefined; + vi.mocked(createProxiedEndpointIfNecessary) + .mockReturnValueOnce( + new Promise((resolve) => { + resolveProxy = resolve; + }), + ) + .mockReturnValue(new Promise(() => {})); + + const controller = new SignedURLController(host, () => ({ + hass, + endpoint: createEndpoint(url), + proxyConfig: { + enabled: true, + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'default' as const, + live: true, + media: true, + }, + })); + + controller.hostUpdate(); + + url = 'http://new-url.com'; + controller.hostUpdate(); + + resolveProxy?.(null); + await flushPromises(); + + expect(host.requestUpdate).not.toBeCalled(); + }); +}); diff --git a/tests/config/types.test.ts b/tests/config/types.test.ts index 8673a05c..0aea2ef2 100644 --- a/tests/config/types.test.ts +++ b/tests/config/types.test.ts @@ -77,6 +77,12 @@ describe('config defaults', () => { elements: [], image: { mode: 'auto', + proxy: { + dynamic: true, + ssl_ciphers: 'auto', + ssl_verification: 'auto', + enabled: false, + }, refresh_seconds: 1, zoomable: true, }, diff --git a/tests/ha/fetch.test.ts b/tests/ha/fetch.test.ts index 3748b813..f7cbc381 100644 --- a/tests/ha/fetch.test.ts +++ b/tests/ha/fetch.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { z } from 'zod'; import { homeAssistantSignAndFetch } from '../../src/ha/fetch'; -import { homeAssistantSignPath } from '../../src/ha/sign-path'; +import { homeAssistantGetSignedURLIfNecessary } from '../../src/ha/sign-path'; import { AdvancedCameraCardError, Endpoint } from '../../src/types'; import { createHASS } from '../test-utils'; @@ -18,7 +18,6 @@ describe('homeAssistantSignAndFetch', () => { beforeEach(() => { vi.stubGlobal('fetch', fetchMock); - vi.mocked(homeAssistantSignPath).mockResolvedValue('http://signed'); }); afterEach(() => { @@ -27,26 +26,31 @@ describe('homeAssistantSignAndFetch', () => { }); it('should return parsed data on successful call with endpoint', async () => { + const endpoint: Endpoint = { endpoint: 'http://example.com' }; + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://example.com', + ); fetchMock.mockResolvedValueOnce({ ok: true, json: async () => response, }); - const endpoint: Endpoint = { endpoint: 'http://example.com' }; - expect(await homeAssistantSignAndFetch(createHASS(), endpoint, schema)).toEqual( - response, - ); - expect(homeAssistantSignPath).not.toHaveBeenCalled(); + const hass = createHASS(); + expect(await homeAssistantSignAndFetch(hass, endpoint, schema)).toEqual(response); + expect(homeAssistantGetSignedURLIfNecessary).toHaveBeenCalledWith(hass, endpoint); expect(fetchMock).toHaveBeenCalledWith('http://example.com', {}); }); it('should pass timeout signal when timeoutSeconds is provided', async () => { + const endpoint: Endpoint = { endpoint: 'http://example.com' }; + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://example.com', + ); fetchMock.mockResolvedValueOnce({ ok: true, json: async () => response, }); - const endpoint: Endpoint = { endpoint: 'http://example.com' }; expect( await homeAssistantSignAndFetch(createHASS(), endpoint, schema, { timeoutSeconds: 5, @@ -58,6 +62,7 @@ describe('homeAssistantSignAndFetch', () => { }); it('should sign path if requested', async () => { + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue('http://signed'); fetchMock.mockResolvedValueOnce({ ok: true, json: async () => response, @@ -69,12 +74,14 @@ describe('homeAssistantSignAndFetch', () => { }; const hass = createHASS(); expect(await homeAssistantSignAndFetch(hass, endpoint, schema)).toEqual(response); - expect(homeAssistantSignPath).toHaveBeenCalledWith(hass, 'http://example.com'); + expect(homeAssistantGetSignedURLIfNecessary).toHaveBeenCalledWith(hass, endpoint); expect(fetchMock).toHaveBeenCalledWith('http://signed', {}); }); it('should throw on sign failure', async () => { - vi.mocked(homeAssistantSignPath).mockRejectedValueOnce(new Error('Sign failed')); + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockRejectedValueOnce( + new Error('Sign failed'), + ); const endpoint: Endpoint = { endpoint: 'http://example.com', @@ -85,8 +92,8 @@ describe('homeAssistantSignAndFetch', () => { ).rejects.toThrow(/Could not sign Home Assistant URL/); }); - it('should throw if sign path returns null', async () => { - vi.mocked(homeAssistantSignPath).mockResolvedValue(null); + it('should throw if sign endpoint returns null', async () => { + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue(null); const endpoint: Endpoint = { endpoint: 'http://example.com', @@ -98,9 +105,12 @@ describe('homeAssistantSignAndFetch', () => { }); it('should throw on fetch failure', async () => { + const endpoint: Endpoint = { endpoint: 'http://example.com' }; + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://example.com', + ); fetchMock.mockRejectedValueOnce(new Error('Fetch failed')); - const endpoint: Endpoint = { endpoint: 'http://example.com' }; try { await homeAssistantSignAndFetch(createHASS(), endpoint, schema); expect.fail('Should have thrown'); @@ -117,6 +127,10 @@ describe('homeAssistantSignAndFetch', () => { }); it('should throw on non-ok response', async () => { + const endpoint: Endpoint = { endpoint: 'http://example.com' }; + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://example.com', + ); const response = { ok: false, status: 404, @@ -124,7 +138,6 @@ describe('homeAssistantSignAndFetch', () => { } as Response; fetchMock.mockResolvedValueOnce(response); - const endpoint: Endpoint = { endpoint: 'http://example.com' }; try { await homeAssistantSignAndFetch(createHASS(), endpoint, schema); expect.fail('Should have thrown'); @@ -140,6 +153,10 @@ describe('homeAssistantSignAndFetch', () => { }); it('should throw on JSON parse failure', async () => { + const endpoint: Endpoint = { endpoint: 'http://example.com' }; + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://example.com', + ); const response = { ok: true, json: async () => { @@ -148,7 +165,6 @@ describe('homeAssistantSignAndFetch', () => { } as unknown as Response; fetchMock.mockResolvedValueOnce(response); - const endpoint: Endpoint = { endpoint: 'http://example.com' }; try { await homeAssistantSignAndFetch(createHASS(), endpoint, schema); expect.fail('Should have thrown'); @@ -166,6 +182,10 @@ describe('homeAssistantSignAndFetch', () => { }); it('should throw on schema validation failure', async () => { + const endpoint: Endpoint = { endpoint: 'http://example.com' }; + vi.mocked(homeAssistantGetSignedURLIfNecessary).mockResolvedValue( + 'http://example.com', + ); const data = { val: 'string' }; const response = { ok: true, @@ -173,8 +193,6 @@ describe('homeAssistantSignAndFetch', () => { } as unknown as Response; fetchMock.mockResolvedValueOnce(response); - const endpoint: Endpoint = { endpoint: 'http://example.com' }; - try { await homeAssistantSignAndFetch(createHASS(), endpoint, schema); expect.fail('Should have thrown'); diff --git a/tests/ha/sign-path.test.ts b/tests/ha/sign-path.test.ts index da644914..53aa7806 100644 --- a/tests/ha/sign-path.test.ts +++ b/tests/ha/sign-path.test.ts @@ -1,5 +1,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { homeAssistantSignPath } from '../../src/ha/sign-path'; +import { + homeAssistantGetSignedURLIfNecessary, + homeAssistantSignPath, +} from '../../src/ha/sign-path'; import { homeAssistantWSRequest } from '../../src/ha/ws-request.js'; import { signedPathSchema } from '../../src/types'; import { createHASS } from '../test-utils'; @@ -36,3 +39,52 @@ describe('homeAssistantSignPath', () => { expect(await homeAssistantSignPath(createHASS(), 'unsigned/path', 42)).toBeNull(); }); }); + +describe('homeAssistantSignEndpoint', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return endpoint URL without signing when sign is false', async () => { + const endpoint = { endpoint: 'http://example.com', sign: false }; + expect(await homeAssistantGetSignedURLIfNecessary(createHASS(), endpoint)).toBe( + 'http://example.com', + ); + expect(homeAssistantWSRequest).not.toHaveBeenCalled(); + }); + + it('should return endpoint URL without signing when sign is undefined', async () => { + const endpoint = { endpoint: 'http://example.com' }; + expect(await homeAssistantGetSignedURLIfNecessary(createHASS(), endpoint)).toBe( + 'http://example.com', + ); + expect(homeAssistantWSRequest).not.toHaveBeenCalled(); + }); + + it('should sign endpoint when sign is true', async () => { + const hass = createHASS(); + vi.mocked(homeAssistantWSRequest).mockResolvedValue({ + path: 'signed/path', + }); + vi.mocked(hass.hassUrl).mockImplementation((url) => 'hass:' + url); + + const endpoint = { endpoint: 'http://example.com', sign: true }; + expect(await homeAssistantGetSignedURLIfNecessary(hass, endpoint, 60)).toBe( + 'hass:signed/path', + ); + expect(homeAssistantWSRequest).toHaveBeenCalledWith(hass, signedPathSchema, { + type: 'auth/sign_path', + path: 'http://example.com', + expires: 60, + }); + }); + + it('should return null when signing fails', async () => { + vi.mocked(homeAssistantWSRequest).mockResolvedValue(null); + + const endpoint = { endpoint: 'http://example.com', sign: true }; + expect( + await homeAssistantGetSignedURLIfNecessary(createHASS(), endpoint), + ).toBeNull(); + }); +}); diff --git a/tests/ha/web-proxy.test.ts b/tests/ha/web-proxy.test.ts index 1e1c50cb..d5aa623d 100644 --- a/tests/ha/web-proxy.test.ts +++ b/tests/ha/web-proxy.test.ts @@ -1,10 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { CameraProxyConfig } from '../../src/camera-manager/types.js'; +import { EnabledProxyConfig } from '../../src/config/schema/common/proxy.js'; import { addDynamicProxyURL, createProxiedEndpointIfNecessary, getWebProxiedURL, - shouldUseWebProxy, } from '../../src/ha/web-proxy.js'; import { createHASS } from '../test-utils.js'; @@ -28,50 +27,19 @@ describe('getWebProxiedURL', () => { }); }); -describe('shouldUseWebProxy', () => { - const createProxyConfig = ( - config: Partial = {}, - ): CameraProxyConfig => ({ - media: true, - live: true, - ssl_verification: true, - ssl_ciphers: 'default', - dynamic: true, - ...config, - }); - - it('should return false without a the proxy installed', () => { - const hass = createHASS(); - hass.config.components = []; - - expect(shouldUseWebProxy(hass, createProxyConfig())).toBe(false); - }); - - it('should return when proxy config does not want proxying', () => { - const hass = createHASS(); - hass.config.components = ['hass_web_proxy']; - - const proxyConfig = createProxyConfig({ media: false }); - expect(shouldUseWebProxy(hass, proxyConfig, 'media')).toBe(false); - }); - - it('should return when proxy config does want proxying', () => { - const hass = createHASS(); - hass.config.components = ['hass_web_proxy']; - - const proxyConfig = createProxyConfig({ media: true }); - expect(shouldUseWebProxy(hass, proxyConfig, 'media')).toBe(true); - }); -}); - describe('addDynamicProxyURL', () => { - it('should add dynamic proxy URL', async () => { + it('should add dynamic proxy URL with proxy config', async () => { const hass = createHASS(); + const proxyConfig: EnabledProxyConfig = { + dynamic: true, + ssl_verification: true, + ssl_ciphers: 'modern', + enabled: true, + }; await addDynamicProxyURL(hass, 'http://example.com', { + proxyConfig, urlID: 'id', - sslVerification: true, - sslCiphers: 'modern', openLimit: 5, ttl: 60, allowUnauthenticated: false, @@ -92,40 +60,29 @@ describe('addDynamicProxyURL', () => { ); }); - it('should add dynamic proxy URL using config defaults', async () => { + it('should add dynamic proxy URL without options', async () => { const hass = createHASS(); - const proxyConfig: CameraProxyConfig = { - media: true, - live: true, - ssl_verification: false, - ssl_ciphers: 'insecure', - dynamic: true, - }; - await addDynamicProxyURL(hass, 'http://example.com', { - proxyConfig: proxyConfig, - }); + await addDynamicProxyURL(hass, 'http://example.com'); expect(hass.callService).toHaveBeenCalledWith( 'hass_web_proxy', 'create_proxied_url', - expect.objectContaining({ - ssl_verification: false, - ssl_ciphers: 'insecure', - }), + { + url_pattern: 'http://example.com', + }, ); }); }); describe('createProxiedEndpointIfNecessary', () => { - const createProxyConfig = ( - config: Partial = {}, - ): CameraProxyConfig => ({ - media: true, - live: true, + const createEnabledProxyConfig = ( + config: Partial = {}, + ): EnabledProxyConfig => ({ ssl_verification: true, ssl_ciphers: 'default', dynamic: true, + enabled: true, ...config, }); @@ -133,33 +90,41 @@ describe('createProxiedEndpointIfNecessary', () => { it('should return original endpoint when proxyConfig is undefined', async () => { const hass = createHASS(); - hass.config.components = ['hass_web_proxy']; const result = await createProxiedEndpointIfNecessary(hass, testEndpoint); expect(result).toBe(testEndpoint); }); - it('should return original endpoint when proxy is not available', async () => { - const hass = createHASS(); - hass.config.components = []; - - const result = await createProxiedEndpointIfNecessary( - hass, - testEndpoint, - createProxyConfig(), - ); - expect(result).toBe(testEndpoint); - }); - - it('should return original endpoint when context is not enabled', async () => { + it('should return original endpoint when enabled is false', async () => { const hass = createHASS(); hass.config.components = ['hass_web_proxy']; const result = await createProxiedEndpointIfNecessary( hass, testEndpoint, - createProxyConfig({ media: false }), - { context: 'media' }, + createEnabledProxyConfig({ enabled: false }), + ); + expect(result).toBe(testEndpoint); + }); + + it('should return null when web proxy is not available and enforced', async () => { + const hass = createHASS(); + + const result = await createProxiedEndpointIfNecessary( + hass, + testEndpoint, + createEnabledProxyConfig({ enforce: true }), + ); + expect(result).toBeNull(); + }); + + it('should return original endpoint when web proxy is not available but not enforced', async () => { + const hass = createHASS(); + + const result = await createProxiedEndpointIfNecessary( + hass, + testEndpoint, + createEnabledProxyConfig(), ); expect(result).toBe(testEndpoint); }); @@ -171,8 +136,8 @@ describe('createProxiedEndpointIfNecessary', () => { const result = await createProxiedEndpointIfNecessary( hass, testEndpoint, - createProxyConfig(), - { context: 'media', ttl: 300, openLimit: 5 }, + createEnabledProxyConfig(), + { ttl: 300, openLimit: 5 }, ); expect(hass.callService).toHaveBeenCalledWith( @@ -200,7 +165,11 @@ describe('createProxiedEndpointIfNecessary', () => { sign: false, }; - await createProxiedEndpointIfNecessary(hass, endpointWithHash, createProxyConfig()); + await createProxiedEndpointIfNecessary( + hass, + endpointWithHash, + createEnabledProxyConfig(), + ); expect(hass.callService).toHaveBeenCalledWith( 'hass_web_proxy', @@ -218,7 +187,7 @@ describe('createProxiedEndpointIfNecessary', () => { const result = await createProxiedEndpointIfNecessary( hass, testEndpoint, - createProxyConfig({ dynamic: false }), + createEnabledProxyConfig({ dynamic: false }), ); expect(hass.callService).not.toHaveBeenCalled(); @@ -235,7 +204,7 @@ describe('createProxiedEndpointIfNecessary', () => { const result = await createProxiedEndpointIfNecessary( hass, testEndpoint, - createProxyConfig({ dynamic: false }), + createEnabledProxyConfig({ dynamic: false }), { websocket: true }, ); @@ -245,25 +214,15 @@ describe('createProxiedEndpointIfNecessary', () => { }); }); - it('should use live context when specified', async () => { - const hass = createHASS(); - hass.config.components = ['hass_web_proxy']; - - const result = await createProxiedEndpointIfNecessary( - hass, - testEndpoint, - createProxyConfig({ media: false, live: true }), - { context: 'live' }, - ); - - expect(result.endpoint).toContain('/api/hass_web_proxy/'); - }); - it('should default openLimit to 0 when not specified', async () => { const hass = createHASS(); hass.config.components = ['hass_web_proxy']; - await createProxiedEndpointIfNecessary(hass, testEndpoint, createProxyConfig()); + await createProxiedEndpointIfNecessary( + hass, + testEndpoint, + createEnabledProxyConfig(), + ); expect(hass.callService).toHaveBeenCalledWith( 'hass_web_proxy', diff --git a/tests/test-utils.ts b/tests/test-utils.ts index e964f207..88ca8397 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -130,6 +130,7 @@ export const createHASS = (states?: HassEntities, user?: CurrentUser): HomeAssis if (user) { hass.user = user; } + hass.config.components = []; hass.connection.subscribeMessage = vi.fn(); // ha-nunjucks calls sendMessagePromise to fetch label registry; return empty array to prevent crash. diff --git a/tests/utils/zod/parse-errors.test.ts b/tests/utils/zod/parse-errors.test.ts index f334ec77..d53d56ca 100644 --- a/tests/utils/zod/parse-errors.test.ts +++ b/tests/utils/zod/parse-errors.test.ts @@ -7,7 +7,9 @@ describe('getParseErrorPaths', () => { const result = z .object({ a: z.string(), b: z.number() }) .safeParse({ a: 1, b: 'a' }); - if (result.success) return; + if (result.success) { + return; + } expect(getParseErrorPaths(result.error)).toEqual(new Set(['a', 'b'])); }); @@ -15,13 +17,17 @@ describe('getParseErrorPaths', () => { const result = z .object({ a: z.object({ b: z.string() }) }) .safeParse({ a: { b: 1 } }); - if (result.success) return; + if (result.success) { + return; + } expect(getParseErrorPaths(result.error)).toEqual(new Set(['a.b'])); }); it('should get array error paths', () => { const result = z.array(z.string()).safeParse([1, 'a', 2]); - if (result.success) return; + if (result.success) { + return; + } expect(getParseErrorPaths(result.error)).toEqual(new Set(['[0]', '[2]'])); }); @@ -29,7 +35,9 @@ describe('getParseErrorPaths', () => { const result = z .object({ a: z.array(z.object({ b: z.string() })) }) .safeParse({ a: [{ b: 1 }, { b: 'a' }, { b: 2 }] }); - if (result.success) return; + if (result.success) { + return; + } expect(getParseErrorPaths(result.error)).toEqual(new Set(['a[0].b', 'a[2].b'])); }); });