From 89f4b56859d75c31bb32db7ca94a9609e68bee35 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sun, 6 Mar 2022 14:14:00 +0100 Subject: [PATCH] refactor bumper_confserver_api_appsvr plugin --- bumper/__init__.py | 6 +- bumper/confserver.py | 55 +++-- bumper/db.py | 2 +- bumper/models.py | 2 +- bumper/plugins.py | 32 +++ .../plugins/bumper_confserver_api_appsvr.py | 127 +++++++++++ .../bumper_confserver_portal_appsvr.py | 209 ------------------ tests/conftest.py | 4 +- tests/test_confserver.py | 8 +- 9 files changed, 202 insertions(+), 243 deletions(-) create mode 100644 bumper/plugins/bumper_confserver_api_appsvr.py delete mode 100644 bumper/plugins/bumper_confserver_portal_appsvr.py diff --git a/bumper/__init__.py b/bumper/__init__.py index 6dc7910..f0e90df 100644 --- a/bumper/__init__.py +++ b/bumper/__init__.py @@ -5,7 +5,7 @@ import pkgutil import socket import sys -from bumper.confserver import ConfServer, WebServerBinding +from bumper.confserver import ConfServer, WebserverBinding from bumper.db import * from bumper.models import * from bumper.mqttserver import MQTTHelperBot, MQTTServer @@ -76,8 +76,8 @@ conf_server_https_port = os.environ.get("WEB_SERVER_HTTPS_PORT") or 443 mqtt_listen_port = 8883 xmpp_listen_port = 5223 conf_server_bindings = [ - WebServerBinding(bumper_listen, conf_server_https_port, True), - WebServerBinding(bumper_listen, 8007, False), + WebserverBinding(bumper_listen, conf_server_https_port, True), + WebserverBinding(bumper_listen, 8007, False), ] diff --git a/bumper/confserver.py b/bumper/confserver.py index 2e413ae..f9b9eda 100644 --- a/bumper/confserver.py +++ b/bumper/confserver.py @@ -23,7 +23,7 @@ from aiohttp.web_response import Response, StreamResponse import bumper -from .plugins import ConfServerApp +from .plugins import ConfServerApp, WebserverPlugin, WebserverSubApi from .util import get_logger @@ -46,8 +46,8 @@ logging.getLogger("aiohttp.access").addFilter(_aiohttp_filter()) @dataclasses.dataclass(frozen=True) -class WebServerBinding: - """Web server binding.""" +class WebserverBinding: + """Webserver binding.""" host: str port: int @@ -59,10 +59,10 @@ class ConfServer: _EXCLUDE_FROM_LOGGING = ["base", "remove-bot", "remove-client", "restart-service"] - def __init__(self, bindings: Union[list[WebServerBinding], WebServerBinding]): + def __init__(self, bindings: Union[list[WebserverBinding], WebserverBinding]): self._runners: list[web.AppRunner] = [] - if isinstance(bindings, WebServerBinding): + if isinstance(bindings, WebserverBinding): bindings = [bindings] self._bindings = bindings @@ -109,10 +109,10 @@ class ConfServer: upload_api = {"prefix": "/upload/", "app": web.Application()} # for /upload/ apis = { - "api_v1": api_v1, - "api_v2": api_v2, - "portal_api": portal_api, - "upload_api": upload_api, + WebserverSubApi.V1: api_v1, + WebserverSubApi.V2: api_v2, + WebserverSubApi.API: portal_api, + WebserverSubApi.UPLOAD: upload_api, } # Load plugins @@ -122,22 +122,31 @@ class ConfServer: for m in inspect.getmembers(module, inspect.isclass) if m[1].__module__ == module.__name__ ] - for plugin_type in plugins: - if not issubclass(plugin_type, ConfServerApp): - continue - - plugin = plugin_type() - if plugin.plugin_type == "sub_api": # app or sub_api - if plugin.sub_api in apis: - if plugin.routes: + for plugin_class in plugins: + if issubclass(plugin_class, WebserverPlugin): + plugin = plugin_class() + logging.debug( + f"Adding confserver sub_api ({plugin.__class__.__name__})" + ) + apis[plugin.sub_api]["app"].add_routes(plugin.routes) + elif issubclass(plugin_class, ConfServerApp): + plugin = plugin_class() + if plugin.plugin_type == "sub_api": # app or sub_api + convert_api = { + "api_v1": WebserverSubApi.V1, + "api_v2": WebserverSubApi.V2, + "portal_api": WebserverSubApi.API, + "upload_api": WebserverSubApi.UPLOAD, + } + api = convert_api.get(plugin.sub_api, None) + if api and plugin.routes: logging.debug(f"Adding confserver sub_api ({plugin.name})") - apis[plugin.sub_api]["app"].add_routes(plugin.routes) - - elif plugin.plugin_type == "app": - if plugin.path_prefix and plugin.app: - logging.debug(f"Adding confserver plugin ({plugin.name})") - self._app.add_subapp(plugin.path_prefix, plugin.app) + apis[api]["app"].add_routes(plugin.routes) + elif plugin.plugin_type == "app": + if plugin.path_prefix and plugin.app: + logging.debug(f"Adding confserver plugin ({plugin.name})") + self._app.add_subapp(plugin.path_prefix, plugin.app) for api in apis: self._app.add_subapp(apis[api]["prefix"], apis[api]["app"]) diff --git a/bumper/db.py b/bumper/db.py index 977e950..fab9759 100644 --- a/bumper/db.py +++ b/bumper/db.py @@ -246,7 +246,7 @@ def user_add_oauth(userid) -> OAuth: return oauth -def token_by_authcode(authcode): +def token_by_authcode(authcode: str): tokens = db_get().table("tokens") return tokens.get(Query().authcode == authcode) diff --git a/bumper/models.py b/bumper/models.py index 43980ca..d55c07d 100644 --- a/bumper/models.py +++ b/bumper/models.py @@ -114,7 +114,7 @@ class OAuth: def toDB(self): return self.__dict__ - def toResponse(self): + def toResponse(self) -> dict: data = self.__dict__ data["expire_at"] = convert_to_millis( datetime.fromisoformat(self.expire_at).timestamp() diff --git a/bumper/plugins.py b/bumper/plugins.py index 9517bcd..b09f181 100644 --- a/bumper/plugins.py +++ b/bumper/plugins.py @@ -1,3 +1,10 @@ +from abc import abstractmethod +from enum import Enum +from typing import Iterable + +from aiohttp.web_routedef import AbstractRouteDef + + class ConfServerApp: name = None plugin_type = None @@ -5,3 +12,28 @@ class ConfServerApp: app = None sub_api = None routes = None + + +class WebserverSubApi(str, Enum): + """Enum with all different sub apis.""" + + V1 = "v1" + V2 = "v2" + API = "api" + UPLOAD = "upload" + + +class WebserverPlugin: + """Abstract webserver plugin.""" + + @property + @abstractmethod + def sub_api(self) -> WebserverSubApi: + """Sub api.""" + raise NotImplementedError + + @property + @abstractmethod + def routes(self) -> Iterable[AbstractRouteDef]: + """Plugin routes.""" + raise NotImplementedError diff --git a/bumper/plugins/bumper_confserver_api_appsvr.py b/bumper/plugins/bumper_confserver_api_appsvr.py new file mode 100644 index 0000000..5ef48dd --- /dev/null +++ b/bumper/plugins/bumper_confserver_api_appsvr.py @@ -0,0 +1,127 @@ +"""Api appsvr plugin module.""" +import json +import logging +from typing import Iterable + +from aiohttp import web +from aiohttp.web_exceptions import HTTPInternalServerError +from aiohttp.web_request import Request +from aiohttp.web_response import Response +from aiohttp.web_routedef import AbstractRouteDef + +import bumper +from bumper.plugins import WebserverPlugin, WebserverSubApi + + +# pylint: disable=no-self-use +class ApiAppsvrPlugin(WebserverPlugin): + """Api appsvr plugin.""" + + sub_api = WebserverSubApi.API + + @property + def routes(self) -> Iterable[AbstractRouteDef]: + """Plugin routes.""" + return [ + web.route( + "*", + "/appsvr/app.do", + self._handle_appsvr_app, + ), + web.route( + "*", + "/appsvr/service/list", + self._handle_appsvr_service_list, + ), + web.route( + "*", + "/appsvr/oauth_callback", + self._handle_appsvr_oauth_callback, + ), + ] + + async def _handle_appsvr_app(self, request: Request) -> Response: + if request.method == "GET": + # Skip GET for now + return web.json_response({"result": "fail", "todo": "result"}) + + try: + if request.content_type == "application/x-www-form-urlencoded": + postbody = await request.post() + else: + postbody = json.loads(await request.text()) + + todo = postbody["todo"] + + if todo == "GetGlobalDeviceList": # EcoVacs Home + bots = bumper.db_get().table("bots").all() + devices = [] + for bot in bots: + if bot["class"] != "": + device = bumper.include_EcoVacsHomeProducts_info(bot) + # Happens if the bot isn't on the EcoVacs Home list + if device is not None: + devices.append(device) + + body = { + "code": 0, + "devices": devices, + "ret": "ok", + "todo": "result", + } + + return web.json_response(body) + except Exception: # pylint: disable=broad-except + logging.error("Unexpected exception occurred", exc_info=True) + + raise HTTPInternalServerError + + async def _handle_appsvr_service_list(self, _: Request) -> Response: + try: + # original urls comment out as they are sub sub domain, which the current certificate is not valid + # using url, where the certs is valid + # data = { + # "account": "users-base.dc-eu.ww.ecouser.net", + # "jmq": "jmq-ngiot-eu.dc.ww.ecouser.net", + # "lb": "lbo.ecouser.net", + # "magw": "api-app.dc-eu.ww.ecouser.net", + # "msgcloud": "msg-eu.ecouser.net:5223", + # "ngiotLb": "jmq-ngiot-eu.area.ww.ecouser.net", + # "rop": "api-rop.dc-eu.ww.ecouser.net" + # } + + data = { + "account": "users-base.ecouser.net", + "jmq": "jmq-ngiot-eu.ecouser.net", + "lb": "lbo.ecouser.net", + "magw": "api-app.ecouser.net", + "msgcloud": "msg-eu.ecouser.net:5223", + "ngiotLb": "jmq-ngiot-eu.ecouser.net", + "rop": "api-rop.ecouser.net", + } + + body = {"code": 0, "data": data, "ret": "ok", "todo": "result"} + + return web.json_response(body) + except Exception: # pylint: disable=broad-except + logging.error("Unexpected exception occurred", exc_info=True) + + raise HTTPInternalServerError + + async def _handle_appsvr_oauth_callback(self, request: Request) -> Response: + try: + token = bumper.token_by_authcode(request.query["code"]) + oauth = bumper.user_add_oauth(token["userid"]) + body = { + "code": 0, + "data": oauth.toResponse(), + "ret": "ok", + "todo": "result", + } + + return web.json_response(body) + + except Exception: # pylint: disable=broad-except + logging.error("Unexpected exception occurred", exc_info=True) + + raise HTTPInternalServerError diff --git a/bumper/plugins/bumper_confserver_portal_appsvr.py b/bumper/plugins/bumper_confserver_portal_appsvr.py deleted file mode 100644 index 77361de..0000000 --- a/bumper/plugins/bumper_confserver_portal_appsvr.py +++ /dev/null @@ -1,209 +0,0 @@ -import json -import logging - -from aiohttp import web - -import bumper -from bumper.plugins import ConfServerApp - - -class portal_api_appsvr(ConfServerApp): - def __init__(self): - self.name = "portal_api_appsvr" - self.plugin_type = "sub_api" - self.sub_api = "portal_api" - - self.routes = [ - web.route( - "*", - "/appsvr/app.do", - self.handle_appsvr_app, - name="portal_api_appsvr_app", - ), - web.route( - "*", - "/appsvr/service/list", - self.handle_appsvr_service_list, - name="portal_api_appsvr_service_list", - ), - web.route( - "*", - "/appsvr/oauth_callback", - self.handle_appsvr_oauth_callback, - name="portal_api_appsvr_oauth_callback", - ), - ] - - async def handle_appsvr_app(self, request): - if not request.method == "GET": # Skip GET for now - try: - if request.content_type == "application/x-www-form-urlencoded": - postbody = await request.post() - else: - postbody = json.loads(await request.text()) - - todo = postbody["todo"] - - if todo == "GetGlobalDeviceList": # EcoVacs Home - bots = bumper.db_get().table("bots").all() - botlist = [] - for bot in bots: - if bot["class"] != "": - b = bumper.include_EcoVacsHomeProducts_info(bot) - # Happens if the bot isn't on the EcoVacs Home list - if b is not None: - botlist.append(b) - - body = { - "code": 0, - "devices": botlist, - "ret": "ok", - "todo": "result", - } - - return web.json_response(body) - - # elif todo == "GetShareDeviceList": - # example response - # { - # "code": 0, - # "devices": [ - # { - # "deviceName": "DEEBOT 900 Series (Cleaner Cleaner)", - # "did": "did", - # "icon": "https://portal-ww.ecouser.net/api/pim/file/get/5ba4a2cb6c2f120001c32839", - # "mid": "ls1ok3", - # "ownUsers": { - # "isMe": true, - # "nickname": "user@gmail.com", - # "user": "cg****" - # }, - # "resource": "wC3g", - # "share": true, - # "shareUsers": [] - # } - # ], - # "ret": "ok", - # "todo": "result" - # } - - # if shared shareUsers - # "shareUsers": [ - # { - # "isMe": false, - # "nickname": "user@gmail.com", - # "status": "sharing", - # "user": "eafg****" - # } - # ] - - # elif todo == "ShareDevice": - # example post - # { - # "todo": "ShareDevice", - # "accountType": "EMAIL", - # "auth": { - # "realm": "ecouser.net", - # "resource": "res", - # "token": "token***", - # "userid": "cg***", - # "with": "users" - # }, - # "country": "US", - # "did": "did", - # "resource": "wC3g", - # "username": "email to share to" - # } - - # fail response (no user) - # { - # "todo": "result", - # "code": -3, - # "errno": -3, - # "ret": "fail" - # } - - # success response - # {"ret":"ok","code":0,"todo":"result"} - - # elif todo == "ShareUnRegisterDevice": - # example post - # { - # "todo": "ShareUnRegisterDevice", - # "account": "email to share to", - # "auth": { - # "realm": "ecouser.net", - # "resource": "res", - # "token": "token", - # "userid": "userid", - # "with": "users" - # }, - # "country": "US", - # "did": "did", - # "lang": "EN", - # "mid": "ls1ok3" - # } - # example response - # { - # "todo": "result", - # "code": 0, - # "data": { - # "mailContent": "

Hey there,

\n\t\t\tCheck out my new awesome robot vacuum: DEEBOT 900 Series!
\n\t\t\tDownload the ECOVACS HOME App and sign up with your email: brian@bmartin.net, so you and I can control this kick-ass robot together.

\n\t\t\tIOS: https://itunes.apple.com/us/app/ecovacs-home/id1329458504?l=zh&ls=1&mt=8
\n\t\t\tAndroid: https://play.google.com/store/apps/details?id=com.eco.global.app

\n\t\t\tThis invitation is valid within 7 days.
\n\t\t\tIf I'm sending to the wrong person, please ignore this email.

Thank you.

", - # "mailTitle": "I'm sharing my DEEBOT and you're invited!" - # }, - # "ret": "ok" - # } - - except Exception as e: - logging.exception(f"{e}") - - # Return fail for GET - body = {"result": "fail", "todo": "result"} - return web.json_response(body) - - async def handle_appsvr_service_list(self, request): - try: - # original urls comment out as they are sub sub domain, which the current certificate is not valid - # using url, where the certs is valid - # data = { - # "account": "users-base.dc-eu.ww.ecouser.net", - # "jmq": "jmq-ngiot-eu.dc.ww.ecouser.net", - # "lb": "lbo.ecouser.net", - # "magw": "api-app.dc-eu.ww.ecouser.net", - # "msgcloud": "msg-eu.ecouser.net:5223", - # "ngiotLb": "jmq-ngiot-eu.area.ww.ecouser.net", - # "rop": "api-rop.dc-eu.ww.ecouser.net" - # } - - data = { - "account": "users-base.ecouser.net", - "jmq": "jmq-ngiot-eu.ecouser.net", - "lb": "lbo.ecouser.net", - "magw": "api-app.ecouser.net", - "msgcloud": "msg-eu.ecouser.net:5223", - "ngiotLb": "jmq-ngiot-eu.ecouser.net", - "rop": "api-rop.ecouser.net", - } - - body = {"code": 0, "data": data, "ret": "ok", "todo": "result"} - - return web.json_response(body) - - except Exception as e: - logging.exception(f"{e}") - - async def handle_appsvr_oauth_callback(self, request): - try: - token = bumper.token_by_authcode(request.query["code"]) - oauth = bumper.user_add_oauth(token["userid"]) - body = { - "code": 0, - "data": oauth.toResponse(), - "ret": "ok", - "todo": "result", - } - - return web.json_response(body) - - except Exception as e: - logging.exception(f"{e}") diff --git a/tests/conftest.py b/tests/conftest.py index 2495986..e1bea27 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ import pytest from amqtt.client import MQTTClient import bumper -from bumper import WebServerBinding +from bumper import WebserverBinding from tests import CONF_SERVER_PORT, HOST, MQTT_PORT @@ -38,7 +38,7 @@ async def mqtt_client(): @pytest.fixture async def conf_server_client(aiohttp_client): - confserver = bumper.ConfServer(WebServerBinding(HOST, CONF_SERVER_PORT, False)) + confserver = bumper.ConfServer(WebserverBinding(HOST, CONF_SERVER_PORT, False)) client = await aiohttp_client(confserver._app) yield client diff --git a/tests/test_confserver.py b/tests/test_confserver.py index e80e0d7..5a6df05 100644 --- a/tests/test_confserver.py +++ b/tests/test_confserver.py @@ -7,12 +7,12 @@ from unittest import mock import pytest import bumper -from bumper import WebServerBinding +from bumper import WebserverBinding from tests import CONF_SERVER_PORT, HOST, MQTT_PORT def create_confserver(): - return bumper.ConfServer(WebServerBinding(HOST, CONF_SERVER_PORT, False)) + return bumper.ConfServer(WebserverBinding(HOST, CONF_SERVER_PORT, False)) def async_return(result): @@ -27,12 +27,12 @@ def remove_existing_db(): async def test_confserver_ssl(): - conf_server = bumper.ConfServer(WebServerBinding(HOST, CONF_SERVER_PORT, True)) + conf_server = bumper.ConfServer(WebserverBinding(HOST, CONF_SERVER_PORT, True)) await conf_server.start() async def test_confserver_no_ssl(): - conf_server = bumper.ConfServer(WebServerBinding(HOST, 11112, False)) + conf_server = bumper.ConfServer(WebserverBinding(HOST, 11112, False)) await conf_server.start()