fix(update): Lampe bekommt genau die geprüften Bytes
Die Registry für Einmal-URLs speicherte bisher nur einen Pfad; View und verify() lasen die Datei getrennt und zu unterschiedlichen Zeitpunkten erneut ein. Wurde die Datei zwischen Prüfung und Abruf ersetzt (z. B. gleicher Dateiname in build/ und build-nocreds/), erreichte ein nie geprüftes Image die Lampe und hebelte damit die WLAN-Sicherung aus. Jetzt werden die Bytes einmal im Install-Pfad gelesen, Gerätetyp/Version und WLAN-SSID auf genau diesen Bytes geprüft (FirmwareCatalog.verify_and_read, firmware.contains_ssid) und exakt diese Bytes in der Registry hinterlegt; die View serviert nur noch das, was tatsächlich geprüft wurde. Außerdem: Bewertungen (Katalog-Rescan-Signal) werden nicht mehr angestoßen, während ein Update läuft - das würde eine gerade flashende Lampe zusätzlich anfragen und Angebot/Issue unter dem laufenden Update verändern; nach jedem Update wird stattdessen genau einmal neu bewertet. Kommentar zum Install-Lock korrigiert (der eigentliche Lücke liegt vor dem Setzen der Flags, nicht in HAs eigener "bereits läuft"-Prüfung). Testlücke geschlossen: die View liefert jetzt nachweislich nur die zu ihrem Schlüssel gehörenden Bytes. Timeout in einem WLAN-Regressionstest verkürzt, damit er bei einem Rückfall nicht 12 Minuten hängt.
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
from typing import Any
|
||||
|
||||
@@ -17,24 +16,29 @@ from .const import CONF_LAMP_BASE_URL, DOMAIN, FIRMWARE_URL_PREFIX
|
||||
|
||||
|
||||
class OtaRegistry:
|
||||
"""Random keys, each bound to exactly one file, valid only while its update runs."""
|
||||
"""Random keys, each bound to exactly the verified bytes of one image, valid only while its
|
||||
update runs.
|
||||
|
||||
Bytes rather than a path: the file on disk could be replaced between verification and
|
||||
download (spec §7 guard would otherwise apply to a file the lamp never actually receives).
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._paths: dict[str, Path] = {}
|
||||
self._images: dict[str, bytes] = {}
|
||||
|
||||
def create(self, path: Path) -> str:
|
||||
def create(self, data: bytes) -> str:
|
||||
key = secrets.token_urlsafe(32)
|
||||
self._paths[key] = path
|
||||
self._images[key] = data
|
||||
return key
|
||||
|
||||
def revoke(self, key: str) -> None:
|
||||
self._paths.pop(key, None)
|
||||
self._images.pop(key, None)
|
||||
|
||||
def get(self, key: str) -> Path | None:
|
||||
return self._paths.get(key)
|
||||
def get(self, key: str) -> bytes | None:
|
||||
return self._images.get(key)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._paths)
|
||||
return len(self._images)
|
||||
|
||||
|
||||
class NinlightFirmwareView(HomeAssistantView):
|
||||
@@ -49,12 +53,8 @@ class NinlightFirmwareView(HomeAssistantView):
|
||||
self._registry = registry
|
||||
|
||||
async def get(self, request: web.Request, key: str) -> web.Response:
|
||||
path = self._registry.get(key)
|
||||
if path is None:
|
||||
return web.Response(status=404)
|
||||
try:
|
||||
body = await self._hass.async_add_executor_job(path.read_bytes)
|
||||
except OSError:
|
||||
body = self._registry.get(key)
|
||||
if body is None:
|
||||
return web.Response(status=404)
|
||||
return web.Response(body=body, content_type="application/octet-stream")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user