Resolve and caches paths for CachingStaticResource in the executor (#74474)

This commit is contained in:
J. Nick Koston 2022-07-06 13:49:48 -05:00 committed by GitHub
parent 113ccfe6af
commit 332cf3cd2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 69 additions and 21 deletions

View file

@ -9,11 +9,31 @@ from aiohttp import hdrs
from aiohttp.web import FileResponse, Request, StreamResponse
from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound
from aiohttp.web_urldispatcher import StaticResource
from lru import LRU # pylint: disable=no-name-in-module
from homeassistant.core import HomeAssistant
from .const import KEY_HASS
CACHE_TIME: Final = 31 * 86400 # = 1 month
CACHE_HEADERS: Final[Mapping[str, str]] = {
hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}"
}
PATH_CACHE = LRU(512)
def _get_file_path(
filename: str, directory: Path, follow_symlinks: bool
) -> Path | None:
filepath = directory.joinpath(filename).resolve()
if not follow_symlinks:
filepath.relative_to(directory)
# on opening a dir, load its contents if allowed
if filepath.is_dir():
return None
if filepath.is_file():
return filepath
raise HTTPNotFound
class CachingStaticResource(StaticResource):
@ -21,16 +41,19 @@ class CachingStaticResource(StaticResource):
async def _handle(self, request: Request) -> StreamResponse:
rel_url = request.match_info["filename"]
hass: HomeAssistant = request.app[KEY_HASS]
filename = Path(rel_url)
if filename.anchor:
# rel_url is an absolute name like
# /static/\\machine_name\c$ or /static/D:\path
# where the static dir is totally different
raise HTTPForbidden()
try:
filename = Path(rel_url)
if filename.anchor:
# rel_url is an absolute name like
# /static/\\machine_name\c$ or /static/D:\path
# where the static dir is totally different
raise HTTPForbidden()
filepath = self._directory.joinpath(filename).resolve()
if not self._follow_symlinks:
filepath.relative_to(self._directory)
key = (filename, self._directory, self._follow_symlinks)
if (filepath := PATH_CACHE.get(key)) is None:
filepath = PATH_CACHE[key] = await hass.async_add_executor_job(
_get_file_path, filename, self._directory, self._follow_symlinks
)
except (ValueError, FileNotFoundError) as error:
# relatively safe
raise HTTPNotFound() from error
@ -39,13 +62,10 @@ class CachingStaticResource(StaticResource):
request.app.logger.exception(error)
raise HTTPNotFound() from error
# on opening a dir, load its contents if allowed
if filepath.is_dir():
return await super()._handle(request)
if filepath.is_file():
if filepath:
return FileResponse(
filepath,
chunk_size=self._chunk_size,
headers=CACHE_HEADERS,
)
raise HTTPNotFound
return await super()._handle(request)

View file

@ -2,7 +2,7 @@
"domain": "recorder",
"name": "Recorder",
"documentation": "https://www.home-assistant.io/integrations/recorder",
"requirements": ["sqlalchemy==1.4.38", "fnvhash==0.1.0", "lru-dict==1.1.7"],
"requirements": ["sqlalchemy==1.4.38", "fnvhash==0.1.0"],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal",
"iot_class": "local_push"

View file

@ -38,6 +38,7 @@ dependencies = [
"httpx==0.23.0",
"ifaddr==0.1.7",
"jinja2==3.1.2",
"lru-dict==1.1.7",
"PyJWT==2.4.0",
# PyJWT has loose dependency. We want the latest one.
"cryptography==36.0.2",

View file

@ -13,6 +13,7 @@ ciso8601==2.2.0
httpx==0.23.0
ifaddr==0.1.7
jinja2==3.1.2
lru-dict==1.1.7
PyJWT==2.4.0
cryptography==36.0.2
orjson==3.7.5

View file

@ -974,9 +974,6 @@ logi_circle==0.2.3
# homeassistant.components.london_underground
london-tube-status==0.5
# homeassistant.components.recorder
lru-dict==1.1.7
# homeassistant.components.luftdaten
luftdaten==0.7.2

View file

@ -681,9 +681,6 @@ life360==4.1.1
# homeassistant.components.logi_circle
logi_circle==0.2.3
# homeassistant.components.recorder
lru-dict==1.1.7
# homeassistant.components.luftdaten
luftdaten==0.7.2

View file

@ -578,3 +578,35 @@ async def test_manifest_json(hass, frontend_themes, mock_http_client):
json = await resp.json()
assert json["theme_color"] != DEFAULT_THEME_COLOR
async def test_static_path_cache(hass, mock_http_client):
"""Test static paths cache."""
resp = await mock_http_client.get("/lovelace/default_view", allow_redirects=False)
assert resp.status == 404
resp = await mock_http_client.get("/frontend_latest/", allow_redirects=False)
assert resp.status == 403
resp = await mock_http_client.get(
"/static/icons/favicon.ico", allow_redirects=False
)
assert resp.status == 200
# and again to make sure the cache works
resp = await mock_http_client.get(
"/static/icons/favicon.ico", allow_redirects=False
)
assert resp.status == 200
resp = await mock_http_client.get(
"/static/fonts/roboto/Roboto-Bold.woff2", allow_redirects=False
)
assert resp.status == 200
resp = await mock_http_client.get("/static/does-not-exist", allow_redirects=False)
assert resp.status == 404
# and again to make sure the cache works
resp = await mock_http_client.get("/static/does-not-exist", allow_redirects=False)
assert resp.status == 404