From 532acdc9e4623b6d560226bffd3013aab52aeb96 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Simon=20K=C3=BCnzel?= <simonk@fsmpi.rwth-aachen.de>
Date: Tue, 29 Apr 2025 19:55:47 +0200
Subject: [PATCH] Add separate rate limiter for resources

---
 api/config/api_example_config.py | 15 +++++++++++++++
 api/src/api/routes/resources.py  | 19 ++++++++++++++-----
 2 files changed, 29 insertions(+), 5 deletions(-)

diff --git a/api/config/api_example_config.py b/api/config/api_example_config.py
index 3535503..cf43636 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 8e0ce29..68c7e75 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'"))
-- 
GitLab