Skip to content

Commit b0d07a4

Browse files
authored
Token tweaks (home-assistant#5599)
* Base status code on auth when entity not found * Also allow previous camera token * Fix tests * Address comments
1 parent e1412a2 commit b0d07a4

File tree

2 files changed

+40
-13
lines changed

2 files changed

+40
-13
lines changed

homeassistant/components/camera/__init__.py

+30-11
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,25 @@
66
https://home-assistant.io/components/camera/
77
"""
88
import asyncio
9+
import collections
910
from datetime import timedelta
1011
import logging
1112
import hashlib
13+
from random import SystemRandom
1214

1315
import aiohttp
1416
from aiohttp import web
1517
import async_timeout
1618

19+
from homeassistant.core import callback
1720
from homeassistant.const import ATTR_ENTITY_PICTURE
1821
from homeassistant.exceptions import HomeAssistantError
1922
from homeassistant.helpers.aiohttp_client import async_get_clientsession
2023
from homeassistant.helpers.entity import Entity
2124
from homeassistant.helpers.entity_component import EntityComponent
2225
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
2326
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
27+
from homeassistant.helpers.event import async_track_time_interval
2428

2529
_LOGGER = logging.getLogger(__name__)
2630

@@ -35,6 +39,9 @@
3539

3640
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
3741

42+
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
43+
_RND = SystemRandom()
44+
3845

3946
@asyncio.coroutine
4047
def async_get_image(hass, entity_id, timeout=10):
@@ -80,6 +87,15 @@ def async_setup(hass, config):
8087
hass.http.register_view(CameraMjpegStream(component.entities))
8188

8289
yield from component.async_setup(config)
90+
91+
@callback
92+
def update_tokens(time):
93+
"""Update tokens of the entities."""
94+
for entity in component.entities.values():
95+
entity.async_update_token()
96+
hass.async_add_job(entity.async_update_ha_state())
97+
98+
async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL)
8399
return True
84100

85101

@@ -89,13 +105,8 @@ class Camera(Entity):
89105
def __init__(self):
90106
"""Initialize a camera."""
91107
self.is_streaming = False
92-
self._access_token = hashlib.sha256(
93-
str.encode(str(id(self)))).hexdigest()
94-
95-
@property
96-
def access_token(self):
97-
"""Access token for this camera."""
98-
return self._access_token
108+
self.access_tokens = collections.deque([], 2)
109+
self.async_update_token()
99110

100111
@property
101112
def should_poll(self):
@@ -105,7 +116,7 @@ def should_poll(self):
105116
@property
106117
def entity_picture(self):
107118
"""Return a link to the camera feed as entity picture."""
108-
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_token)
119+
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1])
109120

110121
@property
111122
def is_recording(self):
@@ -196,7 +207,7 @@ def state(self):
196207
def state_attributes(self):
197208
"""Camera state attributes."""
198209
attr = {
199-
'access_token': self.access_token,
210+
'access_token': self.access_tokens[-1],
200211
}
201212

202213
if self.model:
@@ -207,6 +218,13 @@ def state_attributes(self):
207218

208219
return attr
209220

221+
@callback
222+
def async_update_token(self):
223+
"""Update the used token."""
224+
self.access_tokens.append(
225+
hashlib.sha256(
226+
_RND.getrandbits(256).to_bytes(32, 'little')).hexdigest())
227+
210228

211229
class CameraView(HomeAssistantView):
212230
"""Base CameraView."""
@@ -223,10 +241,11 @@ def get(self, request, entity_id):
223241
camera = self.entities.get(entity_id)
224242

225243
if camera is None:
226-
return web.Response(status=404)
244+
status = 404 if request[KEY_AUTHENTICATED] else 401
245+
return web.Response(status=status)
227246

228247
authenticated = (request[KEY_AUTHENTICATED] or
229-
request.GET.get('token') == camera.access_token)
248+
request.GET.get('token') in camera.access_tokens)
230249

231250
if not authenticated:
232251
return web.Response(status=401)

homeassistant/components/media_player/__init__.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import hashlib
1111
import logging
1212
import os
13+
from random import SystemRandom
1314

1415
from aiohttp import web
1516
import async_timeout
@@ -32,6 +33,7 @@
3233
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
3334

3435
_LOGGER = logging.getLogger(__name__)
36+
_RND = SystemRandom()
3537

3638
DOMAIN = 'media_player'
3739
DEPENDENCIES = ['http']
@@ -389,6 +391,8 @@ def async_service_handler(service):
389391
class MediaPlayerDevice(Entity):
390392
"""ABC for media player devices."""
391393

394+
_access_token = None
395+
392396
# pylint: disable=no-self-use
393397
# Implement these for your media player
394398
@property
@@ -399,7 +403,10 @@ def state(self):
399403
@property
400404
def access_token(self):
401405
"""Access token for this media player."""
402-
return str(id(self))
406+
if self._access_token is None:
407+
self._access_token = hashlib.sha256(
408+
_RND.getrandbits(256).to_bytes(32, 'little')).hexdigest()
409+
return self._access_token
403410

404411
@property
405412
def volume_level(self):
@@ -895,7 +902,8 @@ def get(self, request, entity_id):
895902
"""Start a get request."""
896903
player = self.entities.get(entity_id)
897904
if player is None:
898-
return web.Response(status=404)
905+
status = 404 if request[KEY_AUTHENTICATED] else 401
906+
return web.Response(status=status)
899907

900908
authenticated = (request[KEY_AUTHENTICATED] or
901909
request.GET.get('token') == player.access_token)

0 commit comments

Comments
 (0)