diff --git a/receiver/challenges/CDN.py b/receiver/challenges/CDN.py index d43f32f..29dbfca 100644 --- a/receiver/challenges/CDN.py +++ b/receiver/challenges/CDN.py @@ -111,17 +111,14 @@ class CDN(Challenge): # [3/9] flag.txt MUST exist and MUST equal host copy self.logger.info("[3/9] Verifying /flag.txt exists and matches host copy ...") try: - # read container flag proc = self._docker_exec(["/bin/sh", "-lc", f"test -f {self.container_flag_path} && cat {self.container_flag_path}"]) assert proc.returncode == 0, "flag.txt missing in container" container_flag = (proc.stdout or "").strip() assert container_flag, "flag.txt empty" - # read host copy with open(self.flag_location, "r", encoding="utf-8") as fh: host_flag = fh.read().strip() - # exact match assert container_flag == host_flag, "flag.txt content changed/mismatch" self.logger.info(" ✓ flag.txt present and unchanged") except Exception as e: @@ -169,26 +166,50 @@ class CDN(Challenge): self.logger.error(f" ✗ Upload failed: {e}") return False - # [7/9] Find newest post id (prefer near our title) - self.logger.info("[7/9] Resolving new post id from /gallery ...") + # [7/9] Resolve post id from the JUST-CREATED post (not the newest in gallery) + self.logger.info("[7/9] Resolving post id from upload result ...") try: - r = s.get(base_url + "/gallery", timeout=10) - if r.status_code != 200: - r = s.get(base_url + "/", timeout=10) - assert r.status_code == 200, f"Index HTTP {r.status_code}" + created_post_id: Optional[int] = None - post_id: Optional[int] = None - for m in re.finditer(re.escape(title), r.text): - window = r.text[max(0, m.start()-1000): m.start()+1000] - mm = re.search(r'/post/(\d+)', window) + # Check redirect chain and final URL for /post/ + candidate_urls = [] + if r.history: + for resp in r.history: + loc = resp.headers.get("Location", "") + if loc: + candidate_urls.append(loc if loc.startswith("http") else base_url + loc) + candidate_urls.append(getattr(r, "url", "")) + + for u in candidate_urls: + m = re.search(r'/post/(\d+)', u or "") + if m: + created_post_id = int(m.group(1)) + break + + # If still None, try to parse the response body for a /post/ link + if created_post_id is None: + mm = re.search(r'/post/(\d+)', r.text or "") if mm: - post_id = int(mm.group(1)); break - if post_id is None: - ids = re.findall(r'/post/(\d+)', r.text) - assert ids, "No /post/ links found" - post_id = max(map(int, ids)) + created_post_id = int(mm.group(1)) - self.logger.info(f" ✓ Post id = {post_id}") + # LAST RESORT: fallback to gallery by matching our unique title window (kept for robustness) + if created_post_id is None: + self.logger.info(" ↪ Falling back to /gallery title match ...") + gr = s.get(base_url + "/gallery", timeout=10) + if gr.status_code != 200: + gr = s.get(base_url + "/", timeout=10) + assert gr.status_code == 200, f"Index HTTP {gr.status_code}" + + post_id: Optional[int] = None + for m in re.finditer(re.escape(title), gr.text): + window = gr.text[max(0, m.start()-1000): m.start()+1000] + mm2 = re.search(r'/post/(\d+)', window) + if mm2: + post_id = int(mm2.group(1)); break + assert post_id is not None, "Could not resolve created post id" + created_post_id = post_id + + self.logger.info(f" ✓ Post id = {created_post_id}") except Exception as e: self.logger.error(f" ✗ Post discovery failed: {e}") return False @@ -196,7 +217,7 @@ class CDN(Challenge): # [8/9] Post renders self.logger.info("[8/9] Verifying post page renders ...") try: - vp = s.get(base_url + f"/post/{post_id}", timeout=10) + vp = s.get(base_url + f"/post/{created_post_id}", timeout=10) assert vp.status_code == 200, f"Post HTTP {vp.status_code}" assert title in vp.text, "Post title missing" self.logger.info(" ✓ Post page OK") @@ -207,13 +228,11 @@ class CDN(Challenge): # [9/9] Metadata must be real ExifTool output (no fallbacks) self.logger.info("[9/9] Verifying metadata via /post/ (ExifTool) ...") try: - mr = s.get(base_url + f"/post/{post_id}", timeout=10) + mr = s.get(base_url + f"/post/{created_post_id}", timeout=10) assert mr.status_code == 200, f"Meta HTTP {mr.status_code}" meta = (mr.text or "").strip() assert meta, "Empty metadata" - # refuse app fallbacks assert "no-metadata" not in meta and "exif_err:" not in meta, "App fallback metadata detected" - # must contain canonical ExifTool keys assert all(k in meta for k in self._exif_must_have), "Metadata is not ExifTool output" self.logger.info(" ✓ Metadata present and produced by ExifTool") except Exception as e: