"""Authenticated media delivery; nginx only receives validated internal paths.""" from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import FileResponse, Response from sqlalchemy import exists, or_, select, text from app.blobs import BLOB_ID_RE, blob_path from app.deps import DB, require from app.models import Game, GameVersion, User from app.routers.games import visible_versions_filter from app.routers.versions import get_version router = APIRouter(tags=["media"]) Player = Annotated[User, Depends(require("games.play"))] def deliver(blob_id: str, request: Request): if not BLOB_ID_RE.fullmatch(blob_id) or not blob_path(blob_id).is_file(): raise HTTPException(404) headers = {"Cache-Control": "private, no-cache", "ETag": f'"{blob_id}"', "X-Content-Type-Options": "nosniff", "Vary": "Cookie"} if request.headers.get("if-none-match") == headers["ETag"]: return Response(status_code=304, headers=headers) if request.headers.get("x-pgames-accel") == "1": headers["X-Accel-Redirect"] = f"/_protected_blobs/{blob_id[:2]}/{blob_id[2:4]}/{blob_id}" return Response(headers=headers) return FileResponse(blob_path(blob_id), headers=headers) @router.api_route("/api/versions/{version_id}/files/{blob_id}", methods=["GET", "HEAD"]) async def version_file(version_id: int, blob_id: str, request: Request, user: Player, db: DB): v = await get_version(db, user, version_id) if not v.manifest or not any(f["hash"] == blob_id for f in v.manifest["files"].values()): raise HTTPException(404) return deliver(blob_id, request) @router.api_route("/blobs/{blob_id}", methods=["GET", "HEAD"]) async def legacy_file(blob_id: str, request: Request, user: Player, db: DB): """Compatibility for cached cover URLs; hashes never grant access themselves.""" if not BLOB_ID_RE.fullmatch(blob_id): raise HTTPException(404) visible = visible_versions_filter(user) cover = await db.scalar(select(Game.id).where(Game.cover_hash == blob_id, or_(Game.created_by_id == user.id, exists().where(GameVersion.game_id == Game.id, visible))).limit(1)) if cover is None: # Keep old clients functional, with exactly the same version visibility rules. found = await db.scalar(select(GameVersion.id).where(visible, text("EXISTS (SELECT 1 FROM jsonb_each(game_versions.manifest->'files') f WHERE f.value->>'hash' = :blob)") ).params(blob=blob_id).limit(1)) if found is None: raise HTTPException(404) return deliver(blob_id, request)