Skip to content

Commit

Permalink
Resolve and caches paths for CachingStaticResource in the executor (h…
Browse files Browse the repository at this point in the history
  • Loading branch information
bdraco authored Jul 6, 2022
1 parent 113ccfe commit 332cf3c
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 21 deletions.
48 changes: 34 additions & 14 deletions homeassistant/components/http/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,51 @@
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):
"""Static Resource handler that will add cache headers."""

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
Expand All @@ -39,13 +62,10 @@ async def _handle(self, request: Request) -> StreamResponse:
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)
2 changes: 1 addition & 1 deletion homeassistant/components/recorder/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 0 additions & 3 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
32 changes: 32 additions & 0 deletions tests/components/frontend/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 332cf3c

Please sign in to comment.