diff --git a/api/config/api_example_config.py b/api/config/api_example_config.py index 35355038e414432321641e68dc9dffbd228bf5d5..cf43636ad05b981e441e6a2242f0a119004cf89f 100644 --- a/api/config/api_example_config.py +++ b/api/config/api_example_config.py @@ -111,6 +111,21 @@ API_AUTH_RATE_LIMIT = [ } ] +# Works the same as global but used for resource requests (should be higher than default, since loading a course page can +# send dozens of requests at once; jumping in the video, etc.) +API_RESOURCES_RATE_LIMIT = [ + { + "id": "short", + "window_size_seconds": 60, + "max_request_count": 500 + }, + { + "id": "long", + "window_size_seconds": 60 * 60, + "max_request_count": 4000 + } +] + # Absolute limit. If there are already 32 chapters (visible or not visible), no more suggestions are accepted API_CHAPTER_SUGGESTIONS_LIMIT_PER_LECTURE = 32 # This is NOT host based but globally. It uses a sliding window. For example for a window size of 24 hours, no more than diff --git a/api/src/api/routes/resources.py b/api/src/api/routes/resources.py index 8e0ce296e896f535c7d35e1786d4fd0b2068606a..68c7e755ffa990ce19bf433cfd7f7fde7dd2be39 100644 --- a/api/src/api/routes/resources.py +++ b/api/src/api/routes/resources.py @@ -18,6 +18,8 @@ _FILE_URI_PREFIX_PATH = url_parse.urlparse(_FILE_URI_PREFIX).path if _FILE_URI_PREFIX_PATH is None: raise ValueError("Cannot get path from FILE_URI_PREFIX") +_API_RESOURCE_RATE_LIMITERS = create_configured_host_rate_limiters("resources", api.config["API_RESOURCES_RATE_LIMIT"]) + def _check_access_medium_file(course_handle: str, medium_file_id: int) -> MediumFile: is_mod = is_moderator() @@ -54,17 +56,24 @@ def _decode_str_from_url(url_val: str) -> str: return base64.urlsafe_b64decode(url_val.encode(encoding="utf-8")).decode(encoding="utf-8") -@api_route("/course/<string:course_handle>/resources/medium_file/<int:medium_file_id>", "GET", allow_while_readonly=True, - no_documentation=True) +@api_add_route("/course/<string:course_handle>/resources/medium_file/<int:medium_file_id>", "GET") +@api_function( + rate_limiters=_API_RESOURCE_RATE_LIMITERS, + allow_while_readonly=True, + no_documentation=True, +) def api_route_access_target_medium(course_handle: str, medium_file_id: int): - download = request.args.get("download", "true").lower() == "true" + download = request.args.get("download", "false").lower() == "true" medium_file = _check_access_medium_file(course_handle, medium_file_id) # TODO download stats return redirect(f"{_FILE_URI_PREFIX}/{medium_file.file_path}?co_ha={_encode_str_for_url(course_handle)}&mf_id={medium_file.id}&download={"true" if download else "false"}") -@api_route("/resources/internal_auth_check", "GET", allow_while_readonly=True, - no_documentation=True) +@api_add_route("/resources/internal_auth_check", "GET") +@api_function( + rate_limiters=(), # Don't limit internal auth check. Always from same IP + allow_while_readonly=True, + no_documentation=True) def api_route_resource_internal_auth_check(): if "X-Original-URI" not in request.headers: raise ApiClientException(ERROR_REQUEST_MISSING_PARAMETER("Header 'X-Original-URI'"))