Refactor video session handling and CORS configuration

Co-authored-by: mathieumoran1 <[email protected]>
This commit is contained in:
Cursor Agent
2025-12-22 05:35:50 +00:00
co-authored by mathieumoran1
parent e6537884a3
commit ea37bf95ef
3 changed files with 84 additions and 11 deletions
+56 -9
View File
@@ -352,6 +352,58 @@ function setOpenOriginal(raw) {
} }
} }
function isApiVideoUrl(raw) {
const s = String(raw || '').trim();
if (!s) return false;
if (s.startsWith('/api/video/')) return true;
try {
const u = new URL(s);
return u.pathname.startsWith('/api/video/');
} catch (_) {
return false;
}
}
function toVideoSessionUrl(videoSrc) {
const s = String(videoSrc || '').trim();
if (!s) return null;
if (s.startsWith('/')) return '/api/video/session';
try {
const u = new URL(s);
return new URL('/api/video/session', u.origin).toString();
} catch (_) {
return null;
}
}
async function ensureVideoSessionForSource(videoSrc, token) {
// Only needed for our protected /api/video/* streams.
if (!token) return;
if (!isApiVideoUrl(videoSrc)) return;
const sessionUrl = toVideoSessionUrl(videoSrc);
if (!sessionUrl) return;
// If sessionUrl is cross-origin, we need credentials so the cookie is stored for that domain.
const isCrossOrigin = (() => {
try {
const u = new URL(sessionUrl, window.location.origin);
return u.origin !== window.location.origin;
} catch (_) {
return false;
}
})();
try {
await fetch(sessionUrl, {
method: 'POST',
headers: token ? { 'Authorization': `Bearer ${token}` } : undefined,
...(isCrossOrigin ? { credentials: 'include' } : {}),
});
} catch (_) {
// best-effort
}
}
async function playVodLink(vodLink) { async function playVodLink(vodLink) {
const raw = normalizeVodLink(vodLink); const raw = normalizeVodLink(vodLink);
if (!raw) { if (!raw) {
@@ -520,15 +572,6 @@ window.onload = async function () {
setHeader(movie); setHeader(movie);
// Allow <video> to call /api/video/* without Authorization headers by using a cookie.
// Best-effort: if it fails, external video_src URLs can still work.
try {
await fetch('/api/video/session', {
method: 'POST',
headers: token ? { 'Authorization': `Bearer ${token}` } : undefined,
});
} catch (_) {}
const resolved = resolveMovieSource(movie); const resolved = resolveMovieSource(movie);
if (!resolved?.src) { if (!resolved?.src) {
setOpenOriginal(null); setOpenOriginal(null);
@@ -538,6 +581,10 @@ window.onload = async function () {
return; return;
} }
// Allow <video> to call protected /api/video/* sources without Authorization headers by using a cookie.
// If the source is on a different origin (Python video host), we call THAT host’s /api/video/session.
await ensureVideoSessionForSource(resolved.src, token);
if (resolved.isLegacy) { if (resolved.isLegacy) {
// Legacy source: never embed it in the player. // Legacy source: never embed it in the player.
setOpenOriginal(resolved.src); setOpenOriginal(resolved.src);
+28 -2
View File
@@ -9,6 +9,7 @@ from urllib.parse import quote
import jwt import jwt
from bson import ObjectId from bson import ObjectId
from fastapi import FastAPI, Header, HTTPException, Request, Response from fastapi import FastAPI, Header, HTTPException, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
from pymongo import MongoClient from pymongo import MongoClient
@@ -22,6 +23,7 @@ JWT_SECRET = _env_str("JWT_SECRET")
MONGO_URI = _env_str("MONGO_URI") MONGO_URI = _env_str("MONGO_URI")
VIDEO_FILES_DIR = Path(_env_str("VIDEO_FILES_DIR", str(Path.cwd() / "public" / "video"))).resolve() VIDEO_FILES_DIR = Path(_env_str("VIDEO_FILES_DIR", str(Path.cwd() / "public" / "video"))).resolve()
COOKIE_MAX_AGE_SECONDS = int(_env_str("VIDEO_SESSION_MAX_AGE_SECONDS", "28800")) # 8h COOKIE_MAX_AGE_SECONDS = int(_env_str("VIDEO_SESSION_MAX_AGE_SECONDS", "28800")) # 8h
COOKIE_SAMESITE = _env_str("VIDEO_COOKIE_SAMESITE", "Lax") # Lax|Strict|None
def _parse_bearer(authorization: Optional[str]) -> str: def _parse_bearer(authorization: Optional[str]) -> str:
@@ -115,6 +117,25 @@ def _mongo_client() -> MongoClient:
app = FastAPI() app = FastAPI()
# CORS: needed so the app can call the Python host's /api/video/session
_cors_origins_raw = _env_str("CORS_ALLOW_ORIGINS", "*")
_cors_origins = [o.strip() for o in _cors_origins_raw.split(",") if o.strip()]
_cors_allow_origin_regex = None
if len(_cors_origins) == 0 or (len(_cors_origins) == 1 and _cors_origins[0] == "*"):
# With credentials, browsers can't accept Access-Control-Allow-Origin: *
# Using a regex makes Starlette reflect the requesting Origin instead.
_cors_origins = []
_cors_allow_origin_regex = ".*"
app.add_middleware(
CORSMiddleware,
allow_origins=_cors_origins,
allow_origin_regex=_cors_allow_origin_regex,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.post("/api/video/session") @app.post("/api/video/session")
def create_video_session(request: Request, response: Response, authorization: Optional[str] = Header(default=None)): def create_video_session(request: Request, response: Response, authorization: Optional[str] = Header(default=None)):
@@ -123,14 +144,19 @@ def create_video_session(request: Request, response: Response, authorization: Op
secure = request.url.scheme == "https" or (request.headers.get("x-forwarded-proto") == "https") secure = request.url.scheme == "https" or (request.headers.get("x-forwarded-proto") == "https")
encoded = quote(token, safe="") encoded = quote(token, safe="")
samesite = (COOKIE_SAMESITE or "Lax").strip().lower()
if samesite not in ("lax", "strict", "none"):
samesite = "lax"
samesite_attr = "None" if samesite == "none" else ("Strict" if samesite == "strict" else "Lax")
cookie_parts = [ cookie_parts = [
f"video_auth={encoded}", f"video_auth={encoded}",
"Path=/api/video", "Path=/api/video",
f"Max-Age={COOKIE_MAX_AGE_SECONDS}", f"Max-Age={COOKIE_MAX_AGE_SECONDS}",
"SameSite=Lax", f"SameSite={samesite_attr}",
"HttpOnly", "HttpOnly",
] ]
if secure: # Modern browsers require Secure when SameSite=None
if secure or samesite == "none":
cookie_parts.append("Secure") cookie_parts.append("Secure")
response.headers["Set-Cookie"] = "; ".join(cookie_parts) response.headers["Set-Cookie"] = "; ".join(cookie_parts)
response.status_code = 204 response.status_code = 204