Files
hasaddon/hassio-google-drive-backup/backup/server/server.py
T
admins 702d080a1e New Addon
Google BK
2023-04-23 18:17:03 +07:00

219 lines
9.4 KiB
Python

import json
import aiohttp_jinja2
import jinja2
import base64
from os.path import abspath, join
from aiohttp.web import Application, json_response, Request, TCPSite, AppRunner, post, Response, static, get
from aiohttp.client_exceptions import ClientResponseError, ClientConnectorError, ServerConnectionError, ServerDisconnectedError, ServerTimeoutError
from aiohttp.web_exceptions import HTTPBadRequest, HTTPSeeOther
from backup.creds import Exchanger
from backup.config import Config, Setting, VERSION
from backup.exceptions import GoogleCredentialsExpired, ensureKey, KnownError
from injector import ClassAssistedBuilder, inject, singleton
from .errorstore import ErrorStore
from .cloudlogger import CloudLogger
from yarl import URL
from backup.config import Version
from urllib.parse import unquote
NEW_AUTH_MINIMUM = Version(0, 101, 3)
@singleton
class Server():
@inject
def __init__(self,
config: Config,
exchanger_builder: ClassAssistedBuilder[Exchanger],
logger: CloudLogger,
error_store: ErrorStore):
self.exchanger = exchanger_builder.build(
client_id=config.get(Setting.DEFAULT_DRIVE_CLIENT_ID),
client_secret=config.get(Setting.DEFAULT_DRIVE_CLIENT_SECRET),
redirect=URL(config.get(Setting.AUTHORIZATION_HOST)).with_path("/drive/authorize"))
self.logger = logger
self.config = config
self.error_store = error_store
def base_context(self, request: Request):
return {
'version': VERSION,
'backgroundColor': request.query.get('bg', self.config.get(Setting.BACKGROUND_COLOR)),
'accentColor': request.query.get('ac', self.config.get(Setting.ACCENT_COLOR)),
'bmc_logo_path': "/static/" + VERSION + "/images/bmc.svg"
}
async def authorize(self, request: Request):
if 'redirectbacktoken' in request.query:
version = Version.parse(request.query.get('version', "0"))
token_url = request.query.get('redirectbacktoken')
return_url = request.query.get('return', None)
state = {
'v': str(version),
'token': token_url,
'return': return_url,
'bg': self.base_context(request).get('backgroundColor'),
'ac': self.base_context(request).get('accentColor'),
}
# Someone is trying to authenticate with the add-on, direct them to the google auth url
raise HTTPSeeOther(await self.exchanger.getAuthorizationUrl(json.dumps(state)))
elif 'state' in request.query and 'code' in request.query:
state = json.loads(unquote(request.query.get('state')))
code = request.query.get('code')
try:
version = Version.parse(state["v"])
creds = (await self.exchanger.exchange(code)).serialize(include_secret=False)
if version < NEW_AUTH_MINIMUM:
# Redirect back to the addon, since this is the older addon
url = URL(state['token']).with_query({'creds': json.dumps(creds)})
raise HTTPSeeOther(url)
serialized_creds = str(base64.b64encode(json.dumps(creds).encode("utf-8")), "utf-8")
url = URL(state['token']).with_query({
'creds': serialized_creds,
'host': state['return']})
context = {
**self.base_context(request),
'redirect_url': str(url),
'credentials_serialized': serialized_creds,
}
if 'bg' in state:
context['backgroundColor'] = state['bg']
if 'ac' in state:
context['accentColor'] = state['ac']
return aiohttp_jinja2.render_template(
"authorize.jinja2",
request,
context)
except Exception as e:
if isinstance(e, HTTPSeeOther):
# expected, pass this thorugh
raise
self.logError(request, e)
content = "The server encountered an error while processing this request: " + str(e) + "<br/>"
content += "Please <a href='https://github.com/sabeechen/hassio-google-drive-backup/issues'>file an issue</a> on Home Assistant Google Backup's GitHub page so I'm aware of this problem or attempt authorizing with Google Drive again."
return Response(status=500, body=content)
else:
raise HTTPBadRequest()
async def error(self, request: Request):
try:
self.logReport(request, await request.json())
except BaseException as e:
self.logError(request, e)
return Response()
async def refresh(self, request: Request):
try:
token = ensureKey('refresh_token', await request.json(), "the request payload")
creds = self.exchanger.refreshCredentials(token)
new_creds = await self.exchanger.refresh(creds)
return json_response(new_creds.serialize(include_secret=False))
except ClientResponseError as e:
if e.status == 401:
return json_response({
"error": "expired"
}, status=401)
else:
self.logError(request, e)
return json_response({
"error": "Google returned HTTP {}".format(e.status)
}, status=503)
except ClientConnectorError:
return json_response({
"error": "Couldn't connect to Google's servers"
}, status=503)
except ServerConnectionError:
return json_response({
"error": "Couldn't connect to Google's servers"
}, status=503)
except ServerDisconnectedError:
return json_response({
"error": "Couldn't connect to Google's servers"
}, status=503)
except ServerTimeoutError:
return json_response({
"error": "Google's servers timed out"
}, status=503)
except GoogleCredentialsExpired:
return json_response({
"error": "expired"
}, status=401)
except KnownError as e:
return json_response({
"error": e.message()
}, status=503)
except Exception as e:
self.logError(request, e)
return json_response({
"error": str(e)
}, status=500)
@aiohttp_jinja2.template('picker.jinja2')
async def picker(self, request: Request):
version = Version.parse(request.query.get('version', "0"))
bg = request.query.get('bg', self.config.get(Setting.BACKGROUND_COLOR))
ac = request.query.get('ac', self.config.get(Setting.ACCENT_COLOR))
return {
**self.base_context(request),
"client_id": self.config.get(Setting.DEFAULT_DRIVE_CLIENT_ID),
"developer_key": self.config.get(Setting.DRIVE_PICKER_API_KEY),
"app_id": self.config.get(Setting.DEFAULT_DRIVE_CLIENT_ID).split("-")[0],
'backgroundColor': bg,
'accentColor': ac,
"do_redirect": str(version < NEW_AUTH_MINIMUM).lower()
}
@aiohttp_jinja2.template('server-index.jinja2')
async def index(self, request: Request):
return self.base_context(request)
async def health(self, request: Request):
return json_response({
'status': 'ok',
'messages': []
})
def buildApp(self, app):
path = abspath(join(__file__, "..", "..", "static"))
app.add_routes([
static("/static/" + VERSION, path, append_version=True),
static("/drive/static/" + VERSION, path, append_version=True),
get("/drive/picker", self.picker),
get("/", self.index),
get("/drive/authorize", self.authorize),
post("/drive/refresh", self.refresh),
post("/logerror", self.error),
get("/health", self.health)
])
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(path))
return app
async def start(self):
runner = AppRunner(self.buildApp(Application()))
await runner.setup()
site = TCPSite(runner, "0.0.0.0", int(self.config.get(Setting.PORT)))
await site.start()
self.logger.info("Backup Auth Server Started")
def logError(self, request: Request, exception: Exception):
data = self.getRequestInfo(request)
data['exception'] = self.logger.formatException(exception)
self.logger.log_struct(data)
def logReport(self, request, report):
data = self.getRequestInfo(request)
data['report'] = report
self.logger.log_struct(data)
self.error_store.store(data)
def getRequestInfo(self, request: Request):
return {
'client': request.headers.get('client', "unknown"),
'version': request.headers.get('addon_version', "unknown"),
'address': request.remote,
'url': str(request.url),
'length': request.content_length
}