Refactor video session handling and CORS configuration
Co-authored-by: mathieumoran1 <[email protected]>
This commit is contained in:
co-authored by
mathieumoran1
parent
e6537884a3
commit
ea37bf95ef
+56
-9
@@ -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) {
|
||||
const raw = normalizeVodLink(vodLink);
|
||||
if (!raw) {
|
||||
@@ -520,15 +572,6 @@ window.onload = async function () {
|
||||
|
||||
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);
|
||||
if (!resolved?.src) {
|
||||
setOpenOriginal(null);
|
||||
@@ -538,6 +581,10 @@ window.onload = async function () {
|
||||
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) {
|
||||
// Legacy source: never embed it in the player.
|
||||
setOpenOriginal(resolved.src);
|
||||
|
||||
Binary file not shown.
@@ -9,6 +9,7 @@ from urllib.parse import quote
|
||||
import jwt
|
||||
from bson import ObjectId
|
||||
from fastapi import FastAPI, Header, HTTPException, Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from pymongo import MongoClient
|
||||
|
||||
@@ -22,6 +23,7 @@ JWT_SECRET = _env_str("JWT_SECRET")
|
||||
MONGO_URI = _env_str("MONGO_URI")
|
||||
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_SAMESITE = _env_str("VIDEO_COOKIE_SAMESITE", "Lax") # Lax|Strict|None
|
||||
|
||||
|
||||
def _parse_bearer(authorization: Optional[str]) -> str:
|
||||
@@ -115,6 +117,25 @@ def _mongo_client() -> MongoClient:
|
||||
|
||||
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")
|
||||
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")
|
||||
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 = [
|
||||
f"video_auth={encoded}",
|
||||
"Path=/api/video",
|
||||
f"Max-Age={COOKIE_MAX_AGE_SECONDS}",
|
||||
"SameSite=Lax",
|
||||
f"SameSite={samesite_attr}",
|
||||
"HttpOnly",
|
||||
]
|
||||
if secure:
|
||||
# Modern browsers require Secure when SameSite=None
|
||||
if secure or samesite == "none":
|
||||
cookie_parts.append("Secure")
|
||||
response.headers["Set-Cookie"] = "; ".join(cookie_parts)
|
||||
response.status_code = 204
|
||||
|
||||
Reference in New Issue
Block a user