New Addon
Google BK
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user