From 345d5154a96e56ebd0f7eb6eaba8ee1b73c6c321 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 7 Nov 2024 09:51:40 +0100 Subject: [PATCH] WebHost: fix missing timezone in tracker if-modified-since handling (#4125) * WebHost: fix missing timezone in tracker if-modified-since handling and add a test for it * WebHost, Test: fix running test_tracker in parallel --- WebHostLib/tracker.py | 20 ++-- test/webhost/data/One_Archipelago.archipelago | Bin 0 -> 632 bytes test/webhost/test_tracker.py | 95 ++++++++++++++++++ 3 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 test/webhost/data/One_Archipelago.archipelago create mode 100644 test/webhost/test_tracker.py diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 75b5fb02..5450ef51 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -5,7 +5,7 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, NamedTuple, from uuid import UUID from email.utils import parsedate_to_datetime -from flask import render_template, make_response, Response, request +from flask import make_response, render_template, request, Request, Response from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second @@ -298,17 +298,25 @@ class TrackerData: return self._multidata.get("spheres", []) -def _process_if_request_valid(incoming_request, room: Optional[Room]) -> Optional[Response]: +def _process_if_request_valid(incoming_request: Request, room: Optional[Room]) -> Optional[Response]: if not room: abort(404) - if_modified = incoming_request.headers.get("If-Modified-Since", None) - if if_modified: - if_modified = parsedate_to_datetime(if_modified) + if_modified_str: Optional[str] = incoming_request.headers.get("If-Modified-Since", None) + if if_modified_str: + if_modified = parsedate_to_datetime(if_modified_str) + if if_modified.tzinfo is None: + abort(400) # standard requires "GMT" timezone + # database may use datetime.utcnow(), which is timezone-naive. convert to timezone-aware. + last_activity = room.last_activity + if last_activity.tzinfo is None: + last_activity = room.last_activity.replace(tzinfo=datetime.timezone.utc) # if_modified has less precision than last_activity, so we bring them to same precision - if if_modified >= room.last_activity.replace(microsecond=0): + if if_modified >= last_activity.replace(microsecond=0): return make_response("", 304) + return None + @app.route("/tracker///") def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> Response: diff --git a/test/webhost/data/One_Archipelago.archipelago b/test/webhost/data/One_Archipelago.archipelago new file mode 100644 index 0000000000000000000000000000000000000000..8b7a8ce0a8a1596d459adaa20ee622d63e7ba582 GIT binary patch literal 632 zcmV-;0*Czrc-n1KOK;Oa5Kc>)N1HxW%AI4lpuGCXv8uS#BC03|F4bzYp4h9dy_R>~ zDu+sNfU43+9N7Lw{sk~=CxuA(lGQxE$IRq&{qg6sTKV0%F1C%*mWAAMcjtn&J7*Rl zna*?>!qx=r2P>6vxJA!fonK#4h5JG6%?rinkoPHdnu}Owpm?NR+`yKvc|Xs9>Vn!= z_s+et?hCrw^FC(Nz9IjL0lV>!*~u}B<>iqpLs~L=JZsZ2!RRFT13LgpojQsrSWFJS!!$Ov}_7KfrcOFDAUgU=uqzDBuQI zqC;?4=Y`b{J3=evr?L{=$syq~m1(q$N1BtkZJ%Z)E^Mf8Q<|@zYLXHCR4QrbE?CDc z^1{+;UsO?Ghg_6O7Td=CEQbXxgr%8k?pHMT)m&U!@wxAmYtlRn870(UBUrm9S(Zn} zT{&Doy>J}&64IO$30o9aJ8D#uth#E{)f7s?64>8B(71P|paTTr^ylI5VB9-C86O^v zj>f~I;b`1D91V}D)nqZ`P;zd0KN%O-JOXcP725$yww< zniE~UxxwAP^)zJ}`WwAC|4pC4gQqS37w^`qBD^kcpZU9Zc}DIv-5NG7EB9dV`6|Ye zqZ@xLN#alEzv!pHR(1PD;;_rbxIgN{n1{V#|0J9ZW_)lQ^26DnCk}i3WHjJ205Nq1 S{f%-i7UxBKfBgr4Zjl9^&OM$0 literal 0 HcmV?d00001 diff --git a/test/webhost/test_tracker.py b/test/webhost/test_tracker.py new file mode 100644 index 00000000..58145d77 --- /dev/null +++ b/test/webhost/test_tracker.py @@ -0,0 +1,95 @@ +import os +import pickle +from pathlib import Path +from typing import ClassVar +from uuid import UUID, uuid4 + +from flask import url_for + +from . import TestBase + + +class TestTracker(TestBase): + room_id: UUID + tracker_uuid: UUID + log_filename: str + data: ClassVar[bytes] + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + with (Path(__file__).parent / "data" / "One_Archipelago.archipelago").open("rb") as f: + cls.data = f.read() + + def setUp(self) -> None: + from pony.orm import db_session + from MultiServer import Context as MultiServerContext + from Utils import user_path + from WebHostLib.models import GameDataPackage, Room, Seed + + super().setUp() + + multidata = MultiServerContext.decompress(self.data) + + with self.client.session_transaction() as session: + session["_id"] = uuid4() + self.tracker_uuid = uuid4() + with db_session: + # store game datapackage(s) + for game, game_data in multidata["datapackage"].items(): + if not GameDataPackage.get(checksum=game_data["checksum"]): + GameDataPackage(checksum=game_data["checksum"], + data=pickle.dumps(game_data)) + # create an empty seed and a room from it + seed = Seed(multidata=self.data, owner=session["_id"]) + room = Room(seed=seed, owner=session["_id"], tracker=self.tracker_uuid) + self.room_id = room.id + self.log_filename = user_path("logs", f"{self.room_id}.txt") + + def tearDown(self) -> None: + from pony.orm import db_session, select + from WebHostLib.models import Command, Room + + with db_session: + for command in select(command for command in Command if command.room.id == self.room_id): # type: ignore + command.delete() + room: Room = Room.get(id=self.room_id) + room.seed.delete() + room.delete() + + try: + os.unlink(self.log_filename) + except FileNotFoundError: + pass + + def test_valid_if_modified_since(self) -> None: + """ + Verify that we get a 200 response for valid If-Modified-Since + """ + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get( + url_for( + "get_player_tracker", + tracker=self.tracker_uuid, + tracked_team=0, + tracked_player=1, + ), + headers={"If-Modified-Since": "Wed, 21 Oct 2015 07:28:00 GMT"}, + ) + self.assertEqual(response.status_code, 200) + + def test_invalid_if_modified_since(self) -> None: + """ + Verify that we get a 400 response for invalid If-Modified-Since + """ + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get( + url_for( + "get_player_tracker", + tracker=self.tracker_uuid, + tracked_team=1, + tracked_player=0, + ), + headers={"If-Modified-Since": "Wed, 21 Oct 2015 07:28:00"}, # missing timezone + ) + self.assertEqual(response.status_code, 400)