136 lines
5.1 KiB
Python
136 lines
5.1 KiB
Python
import hashlib
|
|
import json
|
|
import uuid
|
|
|
|
from flask import (
|
|
current_app,
|
|
request,
|
|
Response,
|
|
after_this_request
|
|
)
|
|
from flask import g as global_context
|
|
from flask.views import MethodView
|
|
|
|
from selene.api.etag import ETagManager
|
|
from selene.util.auth import AuthenticationError
|
|
from selene.util.db import connect_to_db
|
|
from selene.util.not_modified import NotModifiedError
|
|
from ..util.cache import SeleneCache
|
|
|
|
ONE_DAY = 86400
|
|
|
|
|
|
def check_oauth_token():
|
|
global_context.url = request.url
|
|
exclude_paths = ['/v1/device/code', '/v1/device/activate', '/api/account', '/v1/auth/token', '/v1/auth/callback']
|
|
exclude = any(request.path.startswith(path) for path in exclude_paths)
|
|
if not exclude:
|
|
headers = request.headers
|
|
if 'Authorization' not in headers:
|
|
raise AuthenticationError('Oauth token not found')
|
|
token_header = headers['Authorization']
|
|
device_authenticated = False
|
|
if token_header.startswith('Bearer '):
|
|
token = token_header[len('Bearer '):]
|
|
session = current_app.config['SELENE_CACHE'].get('device.token.access:{access}'.format(access=token))
|
|
if session:
|
|
device_authenticated = True
|
|
if not device_authenticated:
|
|
raise AuthenticationError('device not authorized')
|
|
|
|
|
|
def generate_device_login(device_id: str, cache: SeleneCache) -> dict:
|
|
"""Generates a login session for a given device id"""
|
|
sha512 = hashlib.sha512()
|
|
sha512.update(bytes(str(uuid.uuid4()), 'utf-8'))
|
|
access = sha512.hexdigest()
|
|
sha512.update(bytes(str(uuid.uuid4()), 'utf-8'))
|
|
refresh = sha512.hexdigest()
|
|
login = dict(
|
|
uuid=device_id,
|
|
accessToken=access,
|
|
refreshToken=refresh,
|
|
expiration=ONE_DAY
|
|
)
|
|
login_json = json.dumps(login)
|
|
# Storing device access token for one:
|
|
cache.set_with_expiration(
|
|
'device.token.access:{access}'.format(access=access),
|
|
login_json,
|
|
ONE_DAY
|
|
)
|
|
# Storing device refresh token for ever:
|
|
cache.set('device.token.refresh:{refresh}'.format(refresh=refresh), login_json)
|
|
|
|
# Storing the login session by uuid (that allows us to delete session using the uuid)
|
|
cache.set('device.session:{uuid}'.format(uuid=device_id), login_json)
|
|
return login
|
|
|
|
|
|
def delete_device_login(device_id: str, cache: SeleneCache):
|
|
session = cache.get('device.session:{uuid}'.format(uuid=device_id))
|
|
if session is not None:
|
|
session = json.loads(session)
|
|
access_token = session['accessToken']
|
|
cache.delete('device.token.access:{access}'.format(access=access_token))
|
|
refresh_token = session['refreshToken']
|
|
cache.delete('device.refresh.token:{refresh}'.format(refresh=refresh_token))
|
|
cache.delete('device.session:{uuid}'.format(uuid=device_id))
|
|
|
|
|
|
class PublicEndpoint(MethodView):
|
|
"""Abstract class for all endpoints used by Mycroft devices"""
|
|
|
|
def __init__(self):
|
|
self.config: dict = current_app.config
|
|
self.request = request
|
|
global_context.url = request.url
|
|
self.cache: SeleneCache = self.config['SELENE_CACHE']
|
|
self.etag_manager: ETagManager = ETagManager(self.cache, self.config)
|
|
|
|
@property
|
|
def db(self):
|
|
if 'db' not in global_context:
|
|
global_context.db = connect_to_db(
|
|
current_app.config['DB_CONNECTION_CONFIG']
|
|
)
|
|
|
|
return global_context.db
|
|
|
|
def _authenticate(self, device_id: str = None):
|
|
headers = self.request.headers
|
|
if 'Authorization' not in headers:
|
|
raise AuthenticationError('Oauth token not found')
|
|
token_header = self.request.headers['Authorization']
|
|
device_authenticated = False
|
|
if token_header.startswith('Bearer '):
|
|
token = token_header[len('Bearer '):]
|
|
session = self.cache.get('device.token.access:{access}'.format(access=token))
|
|
if session is not None:
|
|
session = json.loads(session)
|
|
device_uuid = session['uuid']
|
|
global_context.device_id = device_uuid
|
|
if device_id is not None:
|
|
device_authenticated = (device_id == device_uuid)
|
|
else:
|
|
device_authenticated = True
|
|
if not device_authenticated:
|
|
raise AuthenticationError('device not authorized')
|
|
|
|
def _add_etag(self, key):
|
|
"""Add a etag header to the response. We try to get the etag from the cache using the given key.
|
|
If the cache has the etag, we use it, otherwise we generate a etag, store it and add it to the response"""
|
|
etag = self.etag_manager.get(key)
|
|
|
|
@after_this_request
|
|
def set_etag_header(response: Response):
|
|
response.headers['ETag'] = etag
|
|
return response
|
|
|
|
def _validate_etag(self, key):
|
|
etag_from_request = self.request.headers.get('If-None-Match')
|
|
if etag_from_request is not None:
|
|
etag_from_cache = self.cache.get(key)
|
|
if etag_from_cache is not None and etag_from_request == etag_from_cache.decode('utf-8'):
|
|
raise NotModifiedError()
|