From f664f325af4d9a558fdb4f25f089f1d9792a2d46 Mon Sep 17 00:00:00 2001 From: Striker72rus Date: Wed, 25 Mar 2026 15:57:53 +0300 Subject: [PATCH] - --- custom_components/wall_panel/__init__.py | 6 +- .../wall_panel/frontend/panel.js | 65 +++-------- custom_components/wall_panel/views.py | 109 ++++++++++++++++++ run.sh | 42 +++---- storage/popup_state.json | 2 +- wall_panel/config.yaml | 2 +- wall_panel/run.sh | 42 +++---- 7 files changed, 177 insertions(+), 91 deletions(-) diff --git a/custom_components/wall_panel/__init__.py b/custom_components/wall_panel/__init__.py index 0726c88..ff7ede0 100755 --- a/custom_components/wall_panel/__init__.py +++ b/custom_components/wall_panel/__init__.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from .const import CONF_CONFIG, DOMAIN from .frontend import async_setup_frontend from .helpers import current_entry_config -from .views import WallPanelConfigView, WallPanelPanelView +from .views import WallPanelConfigView, WallPanelPanelView, WallPanelProxyView async def async_setup_entry(hass: HomeAssistant, entry) -> bool: @@ -30,6 +30,10 @@ async def async_setup_entry(hass: HomeAssistant, entry) -> bool: hass.http.register_view(WallPanelPanelView) state["_panel_view_registered"] = True + if not state.get("_proxy_view_registered"): + hass.http.register_view(WallPanelProxyView) + state["_proxy_view_registered"] = True + entry.async_on_unload(entry.add_update_listener(_async_options_updated)) return True diff --git a/custom_components/wall_panel/frontend/panel.js b/custom_components/wall_panel/frontend/panel.js index 0dced3e..fa6110b 100755 --- a/custom_components/wall_panel/frontend/panel.js +++ b/custom_components/wall_panel/frontend/panel.js @@ -74,41 +74,20 @@ class WallPanelPanel extends HTMLElement { return `/api/wall_panel/config/${encodeURIComponent(entryId)}`; } - async _fetchPanelUrl() { + _proxyUrl() { const payload = this._panelConfig(); - const syncToken = String(payload.sync_token || '').trim(); - if (!syncToken) { + const entryId = String(payload.entry_id || '').trim(); + if (!entryId) { return ''; } - const configUrls = [this._configUrl(), this._legacyConfigUrl()].filter(Boolean); - for (const configUrl of configUrls) { - try { - const response = await fetch(configUrl, { - method: 'GET', - headers: { - 'X-Wall-Panel-Token': syncToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - cache: 'no-store', - }); + return `/api/wall_panel/proxy/${encodeURIComponent(entryId)}/`; + } - if (!response.ok) { - continue; - } - - const data = await response.json(); - const panelUrl = String(data?.panel?.panel_url || data?.panel_url || '').trim(); - if (panelUrl) { - return panelUrl; - } - } catch (error) { - continue; - } - } - - return ''; + _hasPanelUrl() { + const payload = this._panelConfig(); + const panelUrl = String(payload.panel_url || payload.ingress_url || '').trim(); + return panelUrl !== ''; } _renderMessage(title, body, extra = '') { @@ -197,33 +176,23 @@ class WallPanelPanel extends HTMLElement { async _tryAttachPanel() { const payload = this._panelConfig(); - const initialUrl = this._resolveUrl( - payload.panel_url || payload.ingress_url || '' - ); - - if (initialUrl && initialUrl === this._activePanelUrl) { + const proxyUrl = this._proxyUrl(); + if (proxyUrl && proxyUrl === this._activePanelUrl) { return; } - if (initialUrl && !(window.location.protocol === 'https:' && initialUrl.startsWith('http://'))) { - this._renderIframe(initialUrl); - return; - } - - const panelUrl = this._resolveUrl(await this._fetchPanelUrl()); - if (panelUrl && panelUrl === this._activePanelUrl) { - return; - } - - if (panelUrl && !(window.location.protocol === 'https:' && panelUrl.startsWith('http://'))) { - this._renderIframe(panelUrl); + if (proxyUrl && this._hasPanelUrl()) { + this._renderIframe(proxyUrl); return; } + const panelUrl = this._resolveUrl(payload.panel_url || payload.ingress_url || ''); const configUrl = this._configUrl(); this._renderMessage( 'Waiting for Wall Panel', - 'The add-on will publish a secure HTTPS ingress URL here.', + panelUrl + ? 'Open this panel through Home Assistant after the add-on URL is configured.' + : 'Set the PHP panel URL in the integration options.', configUrl ? `${configUrl}` : '' ); } diff --git a/custom_components/wall_panel/views.py b/custom_components/wall_panel/views.py index 2844286..9aa604b 100755 --- a/custom_components/wall_panel/views.py +++ b/custom_components/wall_panel/views.py @@ -3,11 +3,14 @@ from __future__ import annotations import secrets +from urllib.parse import urljoin from typing import Any from aiohttp import web from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from yarl import URL from .const import CONF_CONFIG, CONF_PANEL_URL, CONF_SYNC_TOKEN, DOMAIN from .helpers import ( @@ -88,6 +91,34 @@ def _save_entry_panel_url(hass: HomeAssistant, entry, panel_url: str) -> None: hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})["panel_url"] = panel_url +def _proxy_root(entry_id: str) -> str: + return f"/api/wall_panel/proxy/{entry_id}/" + + +def _forward_headers(request: web.Request) -> dict[str, str]: + headers: dict[str, str] = {} + for key in ("Accept", "Content-Type", "Range", "If-Modified-Since", "If-None-Match", "Origin", "Referer", "User-Agent"): + value = request.headers.get(key) + if value: + headers[key] = value + return headers + + +def _rewrite_location(location: str, base_url: str, proxy_root: str) -> str: + location = location.strip() + if not location: + return location + + if location.startswith(base_url): + suffix = location[len(base_url):].lstrip("/") + return proxy_root + suffix + + if location.startswith("/"): + return proxy_root + location.lstrip("/") + + return location + + class WallPanelConfigView(HomeAssistantView): """Serve and update the canonical Wall Panel config.""" @@ -248,3 +279,81 @@ class WallPanelPanelView(HomeAssistantView): "ok": True, "panel": current_entry_panel(entry), }) + + +class WallPanelProxyView(HomeAssistantView): + """Reverse proxy the PHP panel through Home Assistant.""" + + url = "/api/wall_panel/proxy/{entry_id}/{path:.*}" + name = "api:wall_panel:proxy" + requires_auth = True + + async def _proxy(self, request: web.Request, entry_id: str, path: str, method: str) -> web.StreamResponse: + hass = request.app["hass"] + entry = _entry_from_hass(hass, entry_id) + if entry is None: + return _response({"ok": False, "error": "Unknown entry"}, 404) + + base_url = str(entry.options.get(CONF_PANEL_URL, "") or "").strip() + if not base_url: + return _response({"ok": False, "error": "PHP panel URL is not configured"}, 400) + if not base_url.endswith("/"): + base_url += "/" + + upstream_url = urljoin(base_url, path or "") + if request.query_string: + upstream_url = upstream_url + "?" + request.query_string + + body = None + if method not in {"GET", "HEAD"}: + body = await request.read() + + session = async_get_clientsession(hass) + async with session.request( + method, + upstream_url, + headers=_forward_headers(request), + data=body, + allow_redirects=False, + ) as upstream: + response_headers: dict[str, str] = {} + for key, value in upstream.headers.items(): + lower = key.lower() + if lower in { + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailers", + "transfer-encoding", + "upgrade", + "content-encoding", + }: + continue + if lower == "location": + response_headers[key] = _rewrite_location(value, base_url, _proxy_root(entry_id)) + continue + response_headers[key] = value + + response_body = await upstream.read() + return web.Response( + body=response_body, + status=upstream.status, + headers=response_headers, + ) + + async def async_get(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse: + return await self._proxy(request, entry_id, path, "GET") + + async def async_post(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse: + return await self._proxy(request, entry_id, path, "POST") + + async def async_put(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse: + return await self._proxy(request, entry_id, path, "PUT") + + async def async_patch(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse: + return await self._proxy(request, entry_id, path, "PATCH") + + async def async_delete(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse: + return await self._proxy(request, entry_id, path, "DELETE") diff --git a/run.sh b/run.sh index c42a005..c789320 100755 --- a/run.sh +++ b/run.sh @@ -26,26 +26,28 @@ log "config path: ${CONFIG_PATH}" log "storage dir: ${STORAGE_DIR}" if [ "$RUNTIME_MODE" = "addon" ]; then - ( - i=0 - while [ "$i" -lt 120 ]; do - log "registering ingress attempt $((i + 1))/120" - output="$(php -r 'require "/app/lib/bootstrap.php"; $config = app_load_config(); exit(app_register_ingress_url($config) ? 0 : 1);' 2>&1)" - status=$? - if [ "$status" -eq 0 ]; then - log "ingress registered" - exit 0 - fi - if [ -n "$output" ]; then - printf '%s\n' "$output" | while IFS= read -r line; do - [ -n "$line" ] && log "$line" - done - fi - i=$((i + 1)) - sleep 2 - done - log "ingress registration failed after retries" - ) || true & + if [ "${WALL_PANEL_ENABLE_INGRESS_REGISTRATION:-false}" = "true" ]; then + ( + i=0 + while [ "$i" -lt 120 ]; do + log "registering ingress attempt $((i + 1))/120" + output="$(php -r 'require "/app/lib/bootstrap.php"; $config = app_load_config(); exit(app_register_ingress_url($config) ? 0 : 1);' 2>&1)" + status=$? + if [ "$status" -eq 0 ]; then + log "ingress registered" + exit 0 + fi + if [ -n "$output" ]; then + printf '%s\n' "$output" | while IFS= read -r line; do + [ -n "$line" ] && log "$line" + done + fi + i=$((i + 1)) + sleep 2 + done + log "ingress registration failed after retries" + ) || true & + fi fi exec php -S "0.0.0.0:${PORT}" -t "$DOCROOT" diff --git a/storage/popup_state.json b/storage/popup_state.json index 0b9ecc6..70e4a92 100755 --- a/storage/popup_state.json +++ b/storage/popup_state.json @@ -1,5 +1,5 @@ { - "active": true, + "active": false, "sensor_entity_id": "binary_sensor.doorbell_all_occupancy", "opened_at": 1774443242, "expires_at": null diff --git a/wall_panel/config.yaml b/wall_panel/config.yaml index 2e6bfae..fe40acd 100755 --- a/wall_panel/config.yaml +++ b/wall_panel/config.yaml @@ -1,6 +1,6 @@ name: Wall Panel description: Wall Panel PHP interface as a Home Assistant add-on -version: "1.0.20" +version: "1.0.22" slug: wall_panel url: https://git.striker72rus.ru/PHP/wallpanell.git init: false diff --git a/wall_panel/run.sh b/wall_panel/run.sh index 8f088ab..6e51ca0 100755 --- a/wall_panel/run.sh +++ b/wall_panel/run.sh @@ -24,25 +24,27 @@ log "starting add-on on port ${PORT}" log "config path: ${CONFIG_PATH}" log "storage dir: ${STORAGE_DIR}" -( - i=0 - while [ "$i" -lt 120 ]; do - log "registering ingress attempt $((i + 1))/120" - output="$(php -r 'require "/app/lib/bootstrap.php"; $config = app_load_config(); exit(app_register_ingress_url($config) ? 0 : 1);' 2>&1)" - status=$? - if [ "$status" -eq 0 ]; then - log "ingress registered" - exit 0 - fi - if [ -n "$output" ]; then - printf '%s\n' "$output" | while IFS= read -r line; do - [ -n "$line" ] && log "$line" - done - fi - i=$((i + 1)) - sleep 2 - done - log "ingress registration failed after retries" -) || true & +if [ "${WALL_PANEL_ENABLE_INGRESS_REGISTRATION:-false}" = "true" ]; then + ( + i=0 + while [ "$i" -lt 120 ]; do + log "registering ingress attempt $((i + 1))/120" + output="$(php -r 'require "/app/lib/bootstrap.php"; $config = app_load_config(); exit(app_register_ingress_url($config) ? 0 : 1);' 2>&1)" + status=$? + if [ "$status" -eq 0 ]; then + log "ingress registered" + exit 0 + fi + if [ -n "$output" ]; then + printf '%s\n' "$output" | while IFS= read -r line; do + [ -n "$line" ] && log "$line" + done + fi + i=$((i + 1)) + sleep 2 + done + log "ingress registration failed after retries" + ) || true & +fi exec php -S "0.0.0.0:${PORT}" -t "$DOCROOT"