diff --git a/backend/base/custom_exceptions.py b/backend/base/custom_exceptions.py index bbd9975..9f9c1b0 100644 --- a/backend/base/custom_exceptions.py +++ b/backend/base/custom_exceptions.py @@ -7,6 +7,54 @@ from backend.base.definitions import (ApiResponse, InvalidUsernameReason, from backend.base.logging import LOGGER +class LogUnauthMindException(MindException): + """ + MindExceptions that inherit from this one will trigger a log of the + requester's IP address once raised. + """ + + +# region REST responses +class NotFound(MindException): + @property + def api_response(self) -> ApiResponse: + return { + 'code': 404, + 'error': self.__class__.__name__, + 'result': {} + } + + +class BadRequest(MindException): + @property + def api_response(self) -> ApiResponse: + return { + 'code': 400, + 'error': self.__class__.__name__, + 'result': {} + } + + +class MethodNotAllowed(MindException): + @property + def api_response(self) -> ApiResponse: + return { + 'code': 405, + 'error': self.__class__.__name__, + 'result': {} + } + + +class InternalError(MindException): + @property + def api_response(self) -> ApiResponse: + return { + 'code': 500, + 'error': self.__class__.__name__, + 'result': {} + } + + # region Input/Output class KeyNotFound(MindException): "A key was not found in the input that is required to be given" @@ -56,7 +104,7 @@ class InvalidKeyValue(MindException): # region Auth -class AccessUnauthorized(MindException): +class AccessUnauthorized(LogUnauthMindException): "The password given is not correct" def __init__(self) -> None: @@ -74,7 +122,7 @@ class AccessUnauthorized(MindException): } -class APIKeyInvalid(MindException): +class APIKeyInvalid(LogUnauthMindException): "The API key is not correct" def __init__(self, api_key: str) -> None: @@ -92,7 +140,7 @@ class APIKeyInvalid(MindException): } -class APIKeyExpired(MindException): +class APIKeyExpired(LogUnauthMindException): "The API key has expired" def __init__(self, api_key: str) -> None: @@ -129,7 +177,7 @@ class OperationNotAllowed(MindException): } -class NewAccountsNotAllowed(MindException): +class NewAccountsNotAllowed(LogUnauthMindException): "It's not allowed to create a new account except for the admin" def __init__(self) -> None: @@ -266,7 +314,7 @@ class UsernameInvalid(MindException): } -class UserNotFound(MindException): +class UserNotFound(LogUnauthMindException): "The user requested can not be found" def __init__( diff --git a/backend/base/helpers.py b/backend/base/helpers.py index f642de2..58853aa 100644 --- a/backend/base/helpers.py +++ b/backend/base/helpers.py @@ -14,14 +14,16 @@ from secrets import token_bytes from shutil import copy2, move from sys import base_exec_prefix, executable, platform, version_info from threading import current_thread -from typing import Callable, Iterable, List, Sequence, Set, Tuple, Union, cast +from typing import (Any, Callable, Dict, Iterable, List, + Sequence, Set, Tuple, Union, cast) from apprise import Apprise, LogCapture from cron_converter import Cron from dateutil.relativedelta import relativedelta from backend.base.definitions import (WEEKDAY_NUMBER, GeneralReminderData, - RepeatQuantity, SendResult, T, U) + RepeatQuantity, SendResult, + Serialisable, T, U) from backend.base.logging import LOGGER @@ -199,6 +201,14 @@ def current_thread_id() -> int: return current_thread().native_id or -1 +def return_api( + result: Serialisable, + error: Union[str, None] = None, + code: int = 200 +) -> Tuple[Dict[str, Any], int]: + return {'error': error, 'result': result}, code + + # region Security def get_hash(salt: bytes, data: str) -> bytes: """Hash a string using the supplied salt. diff --git a/backend/internals/server.py b/backend/internals/server.py index d7d2a6c..a7cdea7 100644 --- a/backend/internals/server.py +++ b/backend/internals/server.py @@ -17,8 +17,12 @@ from waitress.server import create_server from waitress.task import ThreadedTaskDispatcher as TTD from werkzeug.middleware.dispatcher import DispatcherMiddleware -from backend.base.definitions import Constants, StartType, StartTypeHandler -from backend.base.helpers import Singleton, folder_path +from backend.base.custom_exceptions import (BadRequest, InternalError, + LogUnauthMindException, + MethodNotAllowed, NotFound) +from backend.base.definitions import (Constants, MindException, + StartType, StartTypeHandler) +from backend.base.helpers import Singleton, folder_path, return_api from backend.base.logging import LOGGER from backend.internals.db import DBConnectionManager, close_db from backend.internals.db_backup_import import revert_db_import @@ -84,23 +88,34 @@ class Server(metaclass=Singleton): # Add error handlers @app.errorhandler(400) def bad_request(e): - return {'error': "BadRequest", "result": {}}, 400 + return return_api(**BadRequest().api_response) @app.errorhandler(404) def not_found(e): if request.path.startswith( (Constants.API_PREFIX, Constants.ADMIN_PREFIX) ): - return {'error': "NotFound", "result": {}}, 404 + return return_api(**NotFound().api_response) return render("page_not_found.html") @app.errorhandler(405) def method_not_allowed(e): - return {'error': "MethodNotAllowed", "result": {}}, 405 + return return_api(**MethodNotAllowed().api_response) @app.errorhandler(500) def internal_error(e): - return {'error': "InternalError", "result": {}}, 500 + return return_api(**InternalError().api_response) + + @app.errorhandler(MindException) + def mind_exception(e: MindException): + if isinstance(e, LogUnauthMindException): + ip = request.environ.get( + 'HTTP_X_FORWARDED_FOR', + request.remote_addr + ) + LOGGER.warning(f'Unauthorised request from {ip}') + + return return_api(**e.api_response) # Add endpoints app.register_blueprint(ui) diff --git a/frontend/api.py b/frontend/api.py index db14004..77523f6 100644 --- a/frontend/api.py +++ b/frontend/api.py @@ -9,12 +9,11 @@ from typing import Any, Callable, Dict, Tuple, Union from flask import Response, g, request, send_file -from backend.base.custom_exceptions import (AccessUnauthorized, - APIKeyExpired, APIKeyInvalid, - LogFileNotFound, UserNotFound) -from backend.base.definitions import (ApiKeyEntry, Constants, MindException, - SendResult, Serialisable, StartType) -from backend.base.helpers import folder_path +from backend.base.custom_exceptions import (APIKeyExpired, APIKeyInvalid, + LogFileNotFound) +from backend.base.definitions import (ApiKeyEntry, Constants, + SendResult, StartType) +from backend.base.helpers import folder_path, return_api from backend.base.logging import LOGGER, get_log_filepath from backend.features.reminders import Reminders from backend.features.static_reminders import StaticReminders @@ -54,14 +53,6 @@ users = Users() api_key_map: Dict[int, ApiKeyEntry] = {} -def return_api( - result: Serialisable, - error: Union[str, None] = None, - code: int = 200 -) -> Tuple[Dict[str, Any], int]: - return {'error': error, 'result': result}, code - - def auth() -> None: """Checks if the client is logged in. @@ -120,28 +111,11 @@ def endpoint_wrapper( def wrapper(*args, **kwargs): requires_auth = get_api_docs(request).requires_auth - try: - if requires_auth: - auth() + if requires_auth: + auth() - inputs = input_validation() - result = method(inputs, *args, **kwargs) - - except MindException as e: - if isinstance( - e, - (APIKeyInvalid, APIKeyExpired, - UserNotFound, AccessUnauthorized) - ): - ip = request.environ.get( - 'HTTP_X_FORWARDED_FOR', - request.remote_addr - ) - LOGGER.warning(f'Unauthorised request from {ip}') - - result = return_api(**e.api_response) - - return result + inputs = input_validation() + return method(inputs, *args, **kwargs) wrapper.__name__ = method.__name__ return wrapper