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:
Paulus Schoutsen 2017-01-28 11:51:35 -08:00 committed by GitHub
parent e1412a223c
commit b0d07a414b
2 changed files with 40 additions and 13 deletions

View File

@ -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)

View File

@ -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)