mirror of
https://github.com/home-assistant/core.git
synced 2025-07-08 13:57:10 +00:00
Token tweaks (#5599)
* Base status code on auth when entity not found * Also allow previous camera token * Fix tests * Address comments
This commit is contained in:
parent
e1412a223c
commit
b0d07a414b
@ -6,14 +6,17 @@ For more details about this component, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/camera/
|
https://home-assistant.io/components/camera/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import collections
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
import hashlib
|
import hashlib
|
||||||
|
from random import SystemRandom
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import ATTR_ENTITY_PICTURE
|
from homeassistant.const import ATTR_ENTITY_PICTURE
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
@ -21,6 +24,7 @@ from homeassistant.helpers.entity import Entity
|
|||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||||
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -35,6 +39,9 @@ STATE_IDLE = 'idle'
|
|||||||
|
|
||||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||||
|
|
||||||
|
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
|
||||||
|
_RND = SystemRandom()
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_get_image(hass, entity_id, timeout=10):
|
def async_get_image(hass, entity_id, timeout=10):
|
||||||
@ -80,6 +87,15 @@ def async_setup(hass, config):
|
|||||||
hass.http.register_view(CameraMjpegStream(component.entities))
|
hass.http.register_view(CameraMjpegStream(component.entities))
|
||||||
|
|
||||||
yield from component.async_setup(config)
|
yield from component.async_setup(config)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def update_tokens(time):
|
||||||
|
"""Update tokens of the entities."""
|
||||||
|
for entity in component.entities.values():
|
||||||
|
entity.async_update_token()
|
||||||
|
hass.async_add_job(entity.async_update_ha_state())
|
||||||
|
|
||||||
|
async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -89,13 +105,8 @@ class Camera(Entity):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize a camera."""
|
"""Initialize a camera."""
|
||||||
self.is_streaming = False
|
self.is_streaming = False
|
||||||
self._access_token = hashlib.sha256(
|
self.access_tokens = collections.deque([], 2)
|
||||||
str.encode(str(id(self)))).hexdigest()
|
self.async_update_token()
|
||||||
|
|
||||||
@property
|
|
||||||
def access_token(self):
|
|
||||||
"""Access token for this camera."""
|
|
||||||
return self._access_token
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
@ -105,7 +116,7 @@ class Camera(Entity):
|
|||||||
@property
|
@property
|
||||||
def entity_picture(self):
|
def entity_picture(self):
|
||||||
"""Return a link to the camera feed as entity picture."""
|
"""Return a link to the camera feed as entity picture."""
|
||||||
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_token)
|
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_recording(self):
|
def is_recording(self):
|
||||||
@ -196,7 +207,7 @@ class Camera(Entity):
|
|||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
"""Camera state attributes."""
|
"""Camera state attributes."""
|
||||||
attr = {
|
attr = {
|
||||||
'access_token': self.access_token,
|
'access_token': self.access_tokens[-1],
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.model:
|
if self.model:
|
||||||
@ -207,6 +218,13 @@ class Camera(Entity):
|
|||||||
|
|
||||||
return attr
|
return attr
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_token(self):
|
||||||
|
"""Update the used token."""
|
||||||
|
self.access_tokens.append(
|
||||||
|
hashlib.sha256(
|
||||||
|
_RND.getrandbits(256).to_bytes(32, 'little')).hexdigest())
|
||||||
|
|
||||||
|
|
||||||
class CameraView(HomeAssistantView):
|
class CameraView(HomeAssistantView):
|
||||||
"""Base CameraView."""
|
"""Base CameraView."""
|
||||||
@ -223,10 +241,11 @@ class CameraView(HomeAssistantView):
|
|||||||
camera = self.entities.get(entity_id)
|
camera = self.entities.get(entity_id)
|
||||||
|
|
||||||
if camera is None:
|
if camera is None:
|
||||||
return web.Response(status=404)
|
status = 404 if request[KEY_AUTHENTICATED] else 401
|
||||||
|
return web.Response(status=status)
|
||||||
|
|
||||||
authenticated = (request[KEY_AUTHENTICATED] or
|
authenticated = (request[KEY_AUTHENTICATED] or
|
||||||
request.GET.get('token') == camera.access_token)
|
request.GET.get('token') in camera.access_tokens)
|
||||||
|
|
||||||
if not authenticated:
|
if not authenticated:
|
||||||
return web.Response(status=401)
|
return web.Response(status=401)
|
||||||
|
@ -10,6 +10,7 @@ import functools as ft
|
|||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from random import SystemRandom
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import async_timeout
|
import async_timeout
|
||||||
@ -32,6 +33,7 @@ from homeassistant.const import (
|
|||||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
|
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
_RND = SystemRandom()
|
||||||
|
|
||||||
DOMAIN = 'media_player'
|
DOMAIN = 'media_player'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
@ -389,6 +391,8 @@ def async_setup(hass, config):
|
|||||||
class MediaPlayerDevice(Entity):
|
class MediaPlayerDevice(Entity):
|
||||||
"""ABC for media player devices."""
|
"""ABC for media player devices."""
|
||||||
|
|
||||||
|
_access_token = None
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
# Implement these for your media player
|
# Implement these for your media player
|
||||||
@property
|
@property
|
||||||
@ -399,7 +403,10 @@ class MediaPlayerDevice(Entity):
|
|||||||
@property
|
@property
|
||||||
def access_token(self):
|
def access_token(self):
|
||||||
"""Access token for this media player."""
|
"""Access token for this media player."""
|
||||||
return str(id(self))
|
if self._access_token is None:
|
||||||
|
self._access_token = hashlib.sha256(
|
||||||
|
_RND.getrandbits(256).to_bytes(32, 'little')).hexdigest()
|
||||||
|
return self._access_token
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
@ -895,7 +902,8 @@ class MediaPlayerImageView(HomeAssistantView):
|
|||||||
"""Start a get request."""
|
"""Start a get request."""
|
||||||
player = self.entities.get(entity_id)
|
player = self.entities.get(entity_id)
|
||||||
if player is None:
|
if player is None:
|
||||||
return web.Response(status=404)
|
status = 404 if request[KEY_AUTHENTICATED] else 401
|
||||||
|
return web.Response(status=status)
|
||||||
|
|
||||||
authenticated = (request[KEY_AUTHENTICATED] or
|
authenticated = (request[KEY_AUTHENTICATED] or
|
||||||
request.GET.get('token') == player.access_token)
|
request.GET.get('token') == player.access_token)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user