diff --git a/main.py b/main.py index f2ad7ec..eaefab9 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ import functools +import math from flask import Flask, jsonify, request, abort from sqlalchemy import and_, func, or_ from werkzeug.exceptions import HTTPException @@ -78,8 +79,14 @@ def ref_match(results, collection, hadith_number): @app.before_request def verify_secret(): - if not app.debug and request.headers.get("x-aws-secret") != app.config["AWS_SECRET"]: - abort(401) + if app.debug: + return + + secret = request.headers.get("x-aws-secret") + if secret is None: + abort(401, "Missing 'x-aws-secret' header.") + if secret != app.config["AWS_SECRET"]: + abort(401, "Invalid 'x-aws-secret' header value.") @app.errorhandler(HTTPException) @@ -89,6 +96,16 @@ def jsonify_http_error(error): return jsonify(response), error.code +MAX_PARAM_ECHO_LEN = 50 + + +def _truncate_param(value): + value = str(value) + if len(value) > MAX_PARAM_ECHO_LEN: + return value[:MAX_PARAM_ECHO_LEN] + "..." + return value + + def unpack_query(result): """Allow a route to return a query, or a (query, serialize kwargs) pair.""" return result if isinstance(result, tuple) else (result, {}) @@ -97,8 +114,23 @@ def unpack_query(result): def paginate_results(f): @functools.wraps(f) def decorated_function(*args, **kwargs): - limit = int(request.args.get("limit", 50)) - page = int(request.args.get("page", 1)) + limit_param = request.args.get("limit", 50) + page_param = request.args.get("page", 1) + + try: + limit = int(limit_param) + except (TypeError, ValueError): + abort(400, f"Invalid 'limit' query parameter: '{_truncate_param(limit_param)}' is not an integer.") + + try: + page = int(page_param) + except (TypeError, ValueError): + abort(400, f"Invalid 'page' query parameter: '{_truncate_param(page_param)}' is not an integer.") + + if limit < 1: + abort(400, "Invalid 'limit' query parameter: must be >= 1.") + if page < 1: + abort(400, "Invalid 'page' query parameter: must be >= 1.") query, opts = unpack_query(f(*args, **kwargs)) queryset = query.paginate(page=page, per_page=limit, max_per_page=100) @@ -241,7 +273,14 @@ def api_hadiths(): chapter_id = request.args.get("chapterId") if chapter_id: - query = query.filter_by(babID=float(chapter_id)) + try: + parsed_chapter_id = float(chapter_id) + except (TypeError, ValueError): + abort(400, f"Invalid 'chapterId' query parameter: '{_truncate_param(chapter_id)}' is not a number.") + if not math.isfinite(parsed_chapter_id): + abort(400, f"Invalid 'chapterId' query parameter: '{_truncate_param(chapter_id)}' is not a finite number.") + chapter_id = parsed_chapter_id + query = query.filter_by(babID=chapter_id) hadith_number = request.args.get("hadithNumber") if hadith_number: diff --git a/spec.v1.yml b/spec.v1.yml index 4f49a8f..7d9dd08 100644 --- a/spec.v1.yml +++ b/spec.v1.yml @@ -47,6 +47,8 @@ paths: items: $ref: "#/components/schemas/Collection" - $ref: "#/components/schemas/PaginatedResponse" + "400": + description: Bad request (invalid 'limit' or 'page' query parameter) parameters: - $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/page" @@ -86,6 +88,8 @@ paths: items: $ref: "#/components/schemas/Book" - $ref: "#/components/schemas/PaginatedResponse" + "400": + description: Bad request (invalid 'limit' or 'page' query parameter) parameters: - in: path name: collectionName @@ -137,6 +141,8 @@ paths: items: $ref: "#/components/schemas/Chapter" - $ref: "#/components/schemas/PaginatedResponse" + "400": + description: Bad request (invalid 'limit' or 'page' query parameter) parameters: - in: path name: collectionName @@ -201,6 +207,8 @@ paths: items: $ref: "#/components/schemas/Hadith" - $ref: "#/components/schemas/PaginatedResponse" + "400": + description: Bad request (invalid 'limit' or 'page' query parameter) parameters: - in: path name: collectionName @@ -258,6 +266,8 @@ paths: items: $ref: "#/components/schemas/Hadith" - $ref: "#/components/schemas/PaginatedResponse" + "400": + description: Bad request (invalid 'limit', 'page', or 'chapterId' query parameter) parameters: - in: query name: collection