1 """HTTP Support for Hass.io."""
3 from __future__
import annotations
5 from http
import HTTPStatus
9 from typing
import TYPE_CHECKING
10 from urllib.parse
import quote, unquote
13 from aiohttp
import web
14 from aiohttp.client
import ClientTimeout
15 from aiohttp.hdrs
import (
24 from aiohttp.web_exceptions
import HTTPBadGateway
34 from .const
import X_HASS_SOURCE
36 _LOGGER = logging.getLogger(__name__)
38 MAX_UPLOAD_SIZE = 1024 * 1024 * 1024
40 NO_TIMEOUT = re.compile(
43 r"|backups/.+/partial"
44 r"|backups/[^/]+/(?:upload|download)"
45 r"|audio/logs/(follow|boots/-?\d+(/follow)?)"
46 r"|cli/logs/(follow|boots/-?\d+(/follow)?)"
47 r"|core/logs/(follow|boots/-?\d+(/follow)?)"
48 r"|dns/logs/(follow|boots/-?\d+(/follow)?)"
49 r"|host/logs/(follow|boots/-?\d+(/follow)?)"
50 r"|multicast/logs/(follow|boots/-?\d+(/follow)?)"
51 r"|observer/logs/(follow|boots/-?\d+(/follow)?)"
52 r"|supervisor/logs/(follow|boots/-?\d+(/follow)?)"
53 r"|addons/[^/]+/logs/(follow|boots/-?\d+(/follow)?)"
59 PATHS_NOT_ONBOARDED = re.compile(
61 r"|backups/[a-f0-9]{8}(/info|/new/upload|/download|/restore/full|/restore/partial)?"
62 r"|backups/new/upload"
67 PATHS_ADMIN = re.compile(
69 r"|backups/[a-f0-9]{8}(/info|/download|/restore/full|/restore/partial)?"
70 r"|backups/new/upload"
71 r"|audio/logs(/follow|/boots/-?\d+(/follow)?)?"
72 r"|cli/logs(/follow|/boots/-?\d+(/follow)?)?"
73 r"|core/logs(/follow|/boots/-?\d+(/follow)?)?"
74 r"|dns/logs(/follow|/boots/-?\d+(/follow)?)?"
75 r"|host/logs(/follow|/boots(/-?\d+(/follow)?)?)?"
76 r"|multicast/logs(/follow|/boots/-?\d+(/follow)?)?"
77 r"|observer/logs(/follow|/boots/-?\d+(/follow)?)?"
78 r"|supervisor/logs(/follow|/boots/-?\d+(/follow)?)?"
79 r"|addons/[^/]+/(changelog|documentation)"
80 r"|addons/[^/]+/logs(/follow|/boots/-?\d+(/follow)?)?"
85 PATHS_NO_AUTH = re.compile(
88 r"|(store/)?addons/[^/]+/(logo|icon)"
92 NO_STORE = re.compile(
99 NO_COMPRESS = re.compile(
101 r"|audio/logs/(follow|boots/-?\d+(/follow)?)"
102 r"|cli/logs/(follow|boots/-?\d+(/follow)?)"
103 r"|core/logs/(follow|boots/-?\d+(/follow)?)"
104 r"|dns/logs/(follow|boots/-?\d+(/follow)?)"
105 r"|host/logs/(follow|boots/-?\d+(/follow)?)"
106 r"|multicast/logs/(follow|boots/-?\d+(/follow)?)"
107 r"|observer/logs/(follow|boots/-?\d+(/follow)?)"
108 r"|supervisor/logs/(follow|boots/-?\d+(/follow)?)"
109 r"|addons/[^/]+/logs/(follow|boots/-?\d+(/follow)?)"
113 PATHS_LOGS = re.compile(
115 r"|audio/logs(/follow|/boots/-?\d+(/follow)?)?"
116 r"|cli/logs(/follow|/boots/-?\d+(/follow)?)?"
117 r"|core/logs(/follow|/boots/-?\d+(/follow)?)?"
118 r"|dns/logs(/follow|/boots/-?\d+(/follow)?)?"
119 r"|host/logs(/follow|/boots/-?\d+(/follow)?)?"
120 r"|multicast/logs(/follow|/boots/-?\d+(/follow)?)?"
121 r"|observer/logs(/follow|/boots/-?\d+(/follow)?)?"
122 r"|supervisor/logs(/follow|/boots/-?\d+(/follow)?)?"
123 r"|addons/[^/]+/logs(/follow|/boots/-?\d+(/follow)?)?"
129 RESPONSE_HEADERS_FILTER = {
138 """Hass.io view to handle base part."""
141 url =
"/api/hassio/{path:.+}"
142 requires_auth =
False
144 def __init__(self, host: str, websession: aiohttp.ClientSession) ->
None:
145 """Initialize a Hass.io base view."""
149 async
def _handle(self, request: web.Request, path: str) -> web.StreamResponse:
150 """Return a client request with proxy origin for Hass.io supervisor.
153 - Onboarding allows restoring backups
154 - Load Supervisor panel and add-on logo unauthenticated
155 - User upload/restore backups
158 if path != unquote(path):
159 return web.Response(status=HTTPStatus.BAD_REQUEST)
161 hass = request.app[KEY_HASS]
162 is_admin = request[KEY_AUTHENTICATED]
and request[KEY_HASS_USER].is_admin
163 authorized = is_admin
166 allowed_paths = PATHS_ADMIN
169 allowed_paths = PATHS_NOT_ONBOARDED
176 allowed_paths = PATHS_NO_AUTH
178 no_auth_path = PATHS_NO_AUTH.match(path)
180 X_HASS_SOURCE:
"core.http",
184 if request.method !=
"GET":
185 return web.Response(status=HTTPStatus.METHOD_NOT_ALLOWED)
188 if not allowed_paths.match(path):
189 return web.Response(status=HTTPStatus.UNAUTHORIZED)
192 headers[AUTHORIZATION] = (
193 f
"Bearer {os.environ.get('SUPERVISOR_TOKEN', '')}"
196 if request.method ==
"POST":
197 headers[CONTENT_TYPE] = request.content_type
199 if path ==
"backups/new/upload":
202 assert isinstance(request._stored_content_type, str)
203 headers[CONTENT_TYPE] = request._stored_content_type
206 if PATHS_LOGS.match(path)
and request.headers.get(RANGE):
207 headers[RANGE] = request.headers[RANGE]
210 client = await self.
_websession_websession.request(
211 method=request.method,
212 url=f
"http://{self._host}/{quote(path)}",
213 params=request.query,
214 data=request.content
if request.method !=
"GET" else None,
220 response = web.StreamResponse(
223 response.content_type = client.content_type
226 response.enable_compression()
227 await response.prepare(request)
231 async
for data, _
in client.content.iter_chunks():
232 await response.write(data)
234 except aiohttp.ClientError
as err:
235 _LOGGER.error(
"Client error on api %s request %s", path, err)
236 raise HTTPBadGateway
from err
237 except TimeoutError
as err:
238 _LOGGER.error(
"Client timeout error on API request %s", path)
239 raise HTTPBadGateway
from err
247 """Create response header."""
250 for name, value
in response.headers.items()
251 if name
not in RESPONSE_HEADERS_FILTER
253 if NO_STORE.match(path):
254 headers[CACHE_CONTROL] =
"no-store, max-age=0"
259 """Return timeout for a URL path."""
260 if NO_TIMEOUT.match(path):
261 return ClientTimeout(connect=10, total=
None)
262 return ClientTimeout(connect=10, total=300)
266 """Return if we should compress a response."""
267 if path
is not None and NO_COMPRESS.match(path):
269 if content_type.startswith(
"image/"):
270 return "svg" in content_type
271 if content_type.startswith(
"application/"):
273 "json" in content_type
274 or "xml" in content_type
275 or "javascript" in content_type
277 return not content_type.startswith((
"video/",
"audio/",
"font/"))
None __init__(self, str host, aiohttp.ClientSession websession)
web.StreamResponse _handle(self, web.Request request, str path)
dict[str, str] _response_header(aiohttp.ClientResponse response, str path)
bool should_compress(str content_type, str|None path=None)
ClientTimeout _get_timeout(str path)
bool async_is_onboarded(HomeAssistant hass)