2019-02-14 15:01:46 +00:00
|
|
|
"""Support for views."""
|
2018-03-09 01:51:49 +00:00
|
|
|
import asyncio
|
|
|
|
import json
|
|
|
|
import logging
|
2019-08-12 03:38:18 +00:00
|
|
|
from typing import List, Optional
|
2018-03-09 01:51:49 +00:00
|
|
|
|
|
|
|
from aiohttp import web
|
2018-11-30 20:28:35 +00:00
|
|
|
from aiohttp.web_exceptions import (
|
2019-07-31 19:25:30 +00:00
|
|
|
HTTPBadRequest,
|
|
|
|
HTTPInternalServerError,
|
|
|
|
HTTPUnauthorized,
|
|
|
|
)
|
2018-11-30 20:28:35 +00:00
|
|
|
import voluptuous as vol
|
2018-03-09 01:51:49 +00:00
|
|
|
|
2019-02-14 15:01:46 +00:00
|
|
|
from homeassistant import exceptions
|
2020-04-08 16:47:38 +00:00
|
|
|
from homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK
|
2019-02-14 15:01:46 +00:00
|
|
|
from homeassistant.core import Context, is_callback
|
2018-08-21 13:49:58 +00:00
|
|
|
from homeassistant.helpers.json import JSONEncoder
|
2018-03-09 01:51:49 +00:00
|
|
|
|
2019-03-21 05:56:46 +00:00
|
|
|
from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_REAL_IP
|
2018-03-09 01:51:49 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2019-08-12 03:38:18 +00:00
|
|
|
# mypy: allow-untyped-defs, no-check-untyped-defs
|
|
|
|
|
|
|
|
|
2018-07-20 08:45:20 +00:00
|
|
|
class HomeAssistantView:
|
2018-03-09 01:51:49 +00:00
|
|
|
"""Base view for all views."""
|
|
|
|
|
2019-08-12 03:38:18 +00:00
|
|
|
url: Optional[str] = None
|
|
|
|
extra_urls: List[str] = []
|
2018-07-19 06:37:00 +00:00
|
|
|
# Views inheriting from this class can override this
|
|
|
|
requires_auth = True
|
|
|
|
cors_allowed = False
|
2018-03-09 01:51:49 +00:00
|
|
|
|
2019-11-26 10:30:21 +00:00
|
|
|
@staticmethod
|
|
|
|
def context(request):
|
2018-07-29 00:53:37 +00:00
|
|
|
"""Generate a context from a request."""
|
2019-07-31 19:25:30 +00:00
|
|
|
user = request.get("hass_user")
|
2018-07-29 00:53:37 +00:00
|
|
|
if user is None:
|
|
|
|
return Context()
|
|
|
|
|
|
|
|
return Context(user_id=user.id)
|
|
|
|
|
2019-11-26 10:30:21 +00:00
|
|
|
@staticmethod
|
2020-04-08 16:47:38 +00:00
|
|
|
def json(result, status_code=HTTP_OK, headers=None):
|
2018-03-09 01:51:49 +00:00
|
|
|
"""Return a JSON response."""
|
2018-03-12 22:22:08 +00:00
|
|
|
try:
|
|
|
|
msg = json.dumps(
|
2018-11-28 12:25:23 +00:00
|
|
|
result, sort_keys=True, cls=JSONEncoder, allow_nan=False
|
2019-07-31 19:25:30 +00:00
|
|
|
).encode("UTF-8")
|
2018-11-28 12:25:23 +00:00
|
|
|
except (ValueError, TypeError) as err:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Unable to serialize to JSON: %s\n%s", err, result)
|
2018-03-12 22:22:08 +00:00
|
|
|
raise HTTPInternalServerError
|
2018-03-09 01:51:49 +00:00
|
|
|
response = web.Response(
|
2019-07-31 19:25:30 +00:00
|
|
|
body=msg,
|
|
|
|
content_type=CONTENT_TYPE_JSON,
|
|
|
|
status=status_code,
|
|
|
|
headers=headers,
|
|
|
|
)
|
2018-03-09 01:51:49 +00:00
|
|
|
response.enable_compression()
|
|
|
|
return response
|
|
|
|
|
2020-04-08 16:47:38 +00:00
|
|
|
def json_message(
|
|
|
|
self, message, status_code=HTTP_OK, message_code=None, headers=None
|
|
|
|
):
|
2018-03-09 01:51:49 +00:00
|
|
|
"""Return a JSON message response."""
|
2019-07-31 19:25:30 +00:00
|
|
|
data = {"message": message}
|
2018-03-09 01:51:49 +00:00
|
|
|
if message_code is not None:
|
2019-07-31 19:25:30 +00:00
|
|
|
data["code"] = message_code
|
2018-03-09 01:51:49 +00:00
|
|
|
return self.json(data, status_code, headers=headers)
|
|
|
|
|
2018-07-19 06:37:00 +00:00
|
|
|
def register(self, app, router):
|
2018-03-09 01:51:49 +00:00
|
|
|
"""Register the view with a router."""
|
2019-07-31 19:25:30 +00:00
|
|
|
assert self.url is not None, "No url set for view"
|
2018-03-09 01:51:49 +00:00
|
|
|
urls = [self.url] + self.extra_urls
|
2018-07-19 06:37:00 +00:00
|
|
|
routes = []
|
2018-03-09 01:51:49 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
for method in ("get", "post", "delete", "put", "patch", "head", "options"):
|
2018-03-09 01:51:49 +00:00
|
|
|
handler = getattr(self, method, None)
|
|
|
|
|
|
|
|
if not handler:
|
|
|
|
continue
|
|
|
|
|
|
|
|
handler = request_handler_factory(self, handler)
|
|
|
|
|
|
|
|
for url in urls:
|
2018-07-25 09:36:44 +00:00
|
|
|
routes.append(router.add_route(method, url, handler))
|
2018-03-09 01:51:49 +00:00
|
|
|
|
2018-07-19 06:37:00 +00:00
|
|
|
if not self.cors_allowed:
|
|
|
|
return
|
2018-03-09 01:51:49 +00:00
|
|
|
|
2018-07-25 09:36:44 +00:00
|
|
|
for route in routes:
|
2019-07-31 19:25:30 +00:00
|
|
|
app["allow_cors"](route)
|
2018-03-09 01:51:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
def request_handler_factory(view, handler):
|
|
|
|
"""Wrap the handler classes."""
|
2019-07-31 19:25:30 +00:00
|
|
|
assert asyncio.iscoroutinefunction(handler) or is_callback(
|
|
|
|
handler
|
|
|
|
), "Handler should be a coroutine or a callback."
|
2018-03-09 01:51:49 +00:00
|
|
|
|
|
|
|
async def handle(request):
|
|
|
|
"""Handle incoming request."""
|
2019-03-11 02:55:36 +00:00
|
|
|
if not request.app[KEY_HASS].is_running:
|
2018-03-09 01:51:49 +00:00
|
|
|
return web.Response(status=503)
|
|
|
|
|
|
|
|
authenticated = request.get(KEY_AUTHENTICATED, False)
|
|
|
|
|
2019-10-14 21:56:45 +00:00
|
|
|
if view.requires_auth and not authenticated:
|
|
|
|
raise HTTPUnauthorized()
|
2018-03-09 01:51:49 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"Serving %s to %s (auth: %s)",
|
|
|
|
request.path,
|
|
|
|
request.get(KEY_REAL_IP),
|
|
|
|
authenticated,
|
|
|
|
)
|
2018-03-09 01:51:49 +00:00
|
|
|
|
2018-11-25 17:04:48 +00:00
|
|
|
try:
|
|
|
|
result = handler(request, **request.match_info)
|
2018-03-09 01:51:49 +00:00
|
|
|
|
2018-11-25 17:04:48 +00:00
|
|
|
if asyncio.iscoroutine(result):
|
|
|
|
result = await result
|
2018-11-30 20:28:35 +00:00
|
|
|
except vol.Invalid:
|
|
|
|
raise HTTPBadRequest()
|
|
|
|
except exceptions.ServiceNotFound:
|
|
|
|
raise HTTPInternalServerError()
|
2018-11-25 17:04:48 +00:00
|
|
|
except exceptions.Unauthorized:
|
|
|
|
raise HTTPUnauthorized()
|
2018-03-09 01:51:49 +00:00
|
|
|
|
|
|
|
if isinstance(result, web.StreamResponse):
|
|
|
|
# The method handler returned a ready-made Response, how nice of it
|
|
|
|
return result
|
|
|
|
|
2020-04-08 16:47:38 +00:00
|
|
|
status_code = HTTP_OK
|
2018-03-09 01:51:49 +00:00
|
|
|
|
|
|
|
if isinstance(result, tuple):
|
|
|
|
result, status_code = result
|
|
|
|
|
|
|
|
if isinstance(result, str):
|
2019-07-31 19:25:30 +00:00
|
|
|
result = result.encode("utf-8")
|
2018-03-09 01:51:49 +00:00
|
|
|
elif result is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
result = b""
|
2018-03-09 01:51:49 +00:00
|
|
|
elif not isinstance(result, bytes):
|
2020-01-02 19:17:10 +00:00
|
|
|
assert (
|
|
|
|
False
|
2020-02-28 11:39:29 +00:00
|
|
|
), f"Result should be None, string, bytes or Response. Got: {result}"
|
2018-03-09 01:51:49 +00:00
|
|
|
|
|
|
|
return web.Response(body=result, status=status_code)
|
|
|
|
|
|
|
|
return handle
|