harden: stream-token host allowlist (SSRF), CSRF header on mutating routes, atomic JSON writes
- /stream/{token}: tokens are plain base64; allowlist https://hls.anidb.app
so a session can't use the proxy for arbitrary URLs (new test)
- X-Aniweb:1 required on POST/DELETE except /login — SameSite=Lax is
still sent on cross-site form POSTs, which could hit /account/delete
(new test; SPA + tests send the header)
- tmp+os.replace writes for users/history/searches: a crash mid-write
could corrupt the JSON -> load() returned {} -> silent data loss
- resume positions now read from full history (?all=1), not top-5
- pagehide bound once; savePos reads currentEp (no stale closures)
- account delete also removes the searches file; episodes TTL 5m
- ani-web.service: supervised restart (not installed; see header)
- gitignore pi-session dumps + .pytest_cache
This commit is contained in:
1 parent
d39ee838ff
commit
aae5857dba
8 files changed
+122
-32
No files matched your search
+25
-17
@@ -9,7 +9,7 @@ const main = document.getElementById("main");
|
||||
let hls = null, detail = null, lang = "jpn", seasonId = null;
|
||||
|
||||
async function api(path) {
|
||||
const r = await fetch(BASE + path.replace(/^\//, ""));
|
||||
const r = await fetch(BASE + path.replace(/^\//, ""), {headers: {"X-Aniweb": "1"}});
|
||||
if (r.status === 401) {
|
||||
location.href = BASE + "login?next=" + encodeURIComponent(location.pathname + location.search);
|
||||
throw new Error("not authenticated");
|
||||
@@ -56,10 +56,11 @@ function showStreamError(msg) {
|
||||
// ---------- history (server-persisted, incl. watch position) ----------
|
||||
let history = [];
|
||||
async function refreshHistory() {
|
||||
try { history = await api("/api/history"); } catch { /* home just shows empty */ }
|
||||
// ?all=1: resume positions live beyond the top-5 cap
|
||||
try { history = await api("/api/history?all=1"); } catch { /* home just shows empty */ }
|
||||
}
|
||||
function postHistory(entry) {
|
||||
fetch(BASE + "api/history", {method: "POST", headers: {"Content-Type": "application/json"},
|
||||
fetch(BASE + "api/history", {method: "POST", headers: {"Content-Type": "application/json", "X-Aniweb": "1"},
|
||||
body: JSON.stringify(entry)}).catch(() => {});
|
||||
}
|
||||
const fmtPos = s => { s = Math.floor(s || 0); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; };
|
||||
@@ -139,7 +140,7 @@ function showHome() {
|
||||
|
||||
// ---------- search ----------
|
||||
function saveSearch(q) {
|
||||
fetch(BASE + "api/searches", {method: "POST", headers: {"Content-Type": "application/json"},
|
||||
fetch(BASE + "api/searches", {method: "POST", headers: {"Content-Type": "application/json", "X-Aniweb": "1"},
|
||||
body: JSON.stringify({q})}).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -313,7 +314,8 @@ function histCard(x) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await fetch(BASE + "api/history?slug_id=" + encodeURIComponent(x.slug_id) +
|
||||
"&ep_id=" + encodeURIComponent(x.ep_id), {method: "DELETE"});
|
||||
"&ep_id=" + encodeURIComponent(x.ep_id),
|
||||
{method: "DELETE", headers: {"X-Aniweb": "1"}});
|
||||
toast("Removed from history");
|
||||
await showHistory();
|
||||
};
|
||||
@@ -477,18 +479,24 @@ async function play(ep_id, ep_no) {
|
||||
const meta = v => ({...(Number.isFinite(v.duration) && v.duration > 0 ? {dur: v.duration} : {}),
|
||||
poster: detail ? (detail.poster || null) : null});
|
||||
|
||||
function watchPos(v) {
|
||||
const save = () => {
|
||||
if (!currentEp || v.currentTime <= 3) return;
|
||||
lastSavedPos = v.currentTime;
|
||||
postHistory({...meta(v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
|
||||
ep_id: currentEp.ep_id, lang, ep: currentEp.ep_no, pos: v.currentTime});
|
||||
};
|
||||
v.addEventListener("timeupdate", () => { if (v.currentTime - lastSavedPos >= 10) save(); });
|
||||
v.addEventListener("pause", save);
|
||||
window.addEventListener("pagehide", save);
|
||||
// one savePos covers all video elements: it reads currentEp, so stale
|
||||
// closures from earlier plays can't write the wrong episode's position
|
||||
function savePos() {
|
||||
if (!currentEp || currentEp.v.currentTime <= 3) return;
|
||||
lastSavedPos = currentEp.v.currentTime;
|
||||
postHistory({...meta(currentEp.v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
|
||||
ep_id: currentEp.ep_id, lang, ep: currentEp.ep_no, pos: currentEp.v.currentTime});
|
||||
}
|
||||
|
||||
function watchPos(v) {
|
||||
v.addEventListener("timeupdate", () => { if (v.currentTime - lastSavedPos >= 10) savePos(); });
|
||||
v.addEventListener("pause", savePos);
|
||||
// pagehide is bound once at boot (per-play listeners accumulated and fired
|
||||
// with stale video elements)
|
||||
}
|
||||
|
||||
window.addEventListener("pagehide", savePos);
|
||||
|
||||
// ---------- boot ----------
|
||||
(async () => {
|
||||
document.querySelector(".logo").href = BASE;
|
||||
@@ -498,7 +506,7 @@ function watchPos(v) {
|
||||
initRecent();
|
||||
const lo = document.getElementById("logout");
|
||||
lo.onclick = async () => {
|
||||
await fetch(BASE + "logout", {method: "POST"}); location.href = BASE + "login?signedout=1";
|
||||
await fetch(BASE + "logout", {method: "POST", headers: {"X-Aniweb": "1"}}); location.href = BASE + "login?signedout=1";
|
||||
};
|
||||
const mb = document.getElementById("menubox");
|
||||
mb.style.display = "";
|
||||
@@ -510,7 +518,7 @@ function watchPos(v) {
|
||||
const da = document.getElementById("delacct");
|
||||
da.onclick = async () => {
|
||||
if (!confirm("Delete your account and watch history?")) return;
|
||||
await fetch(BASE + "account/delete", {method: "POST"}); location.href = BASE + "login?deleted=1";
|
||||
await fetch(BASE + "account/delete", {method: "POST", headers: {"X-Aniweb": "1"}}); location.href = BASE + "login?deleted=1";
|
||||
};
|
||||
} catch { /* not logged in: api() already redirected to /login */ }
|
||||
await refreshHistory();
|
||||
|
||||
Reference in new issue
Block a user