From b0d07a414bc5297deb4f43c76473d9740b5dc8f7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Jan 2017 11:51:35 -0800 Subject: [PATCH] Token tweaks (#5599) * Base status code on auth when entity not found * Also allow previous camera token * Fix tests * Address comments --- homeassistant/components/camera/__init__.py | 41 ++++++++++++++----- .../components/media_player/__init__.py | 12 +++++- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 174d0f5a2981bd..b531a931a7af4a 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -6,14 +6,17 @@ https://home-assistant.io/components/camera/ """ import asyncio +import collections from datetime import timedelta import logging import hashlib +from random import SystemRandom import aiohttp from aiohttp import web import async_timeout +from homeassistant.core import callback from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -21,6 +24,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED +from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -35,6 +39,9 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' +TOKEN_CHANGE_INTERVAL = timedelta(minutes=5) +_RND = SystemRandom() + @asyncio.coroutine 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)) 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 @@ -89,13 +105,8 @@ class Camera(Entity): def __init__(self): """Initialize a camera.""" self.is_streaming = False - self._access_token = hashlib.sha256( - str.encode(str(id(self)))).hexdigest() - - @property - def access_token(self): - """Access token for this camera.""" - return self._access_token + self.access_tokens = collections.deque([], 2) + self.async_update_token() @property def should_poll(self): @@ -105,7 +116,7 @@ def should_poll(self): @property def entity_picture(self): """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 def is_recording(self): @@ -196,7 +207,7 @@ def state(self): def state_attributes(self): """Camera state attributes.""" attr = { - 'access_token': self.access_token, + 'access_token': self.access_tokens[-1], } if self.model: @@ -207,6 +218,13 @@ def state_attributes(self): 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): """Base CameraView.""" @@ -223,10 +241,11 @@ def get(self, request, entity_id): camera = self.entities.get(entity_id) 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 - request.GET.get('token') == camera.access_token) + request.GET.get('token') in camera.access_tokens) if not authenticated: return web.Response(status=401) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 576dca25a6a5ad..71901b6256ad2f 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -10,6 +10,7 @@ import hashlib import logging import os +from random import SystemRandom from aiohttp import web import async_timeout @@ -32,6 +33,7 @@ SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK) _LOGGER = logging.getLogger(__name__) +_RND = SystemRandom() DOMAIN = 'media_player' DEPENDENCIES = ['http'] @@ -389,6 +391,8 @@ def async_service_handler(service): class MediaPlayerDevice(Entity): """ABC for media player devices.""" + _access_token = None + # pylint: disable=no-self-use # Implement these for your media player @property @@ -399,7 +403,10 @@ def state(self): @property def access_token(self): """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 def volume_level(self): @@ -895,7 +902,8 @@ def get(self, request, entity_id): """Start a get request.""" player = self.entities.get(entity_id) 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 request.GET.get('token') == player.access_token)