mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-18 20:59:59 +02:00
feat: security hardening, streaming overhaul, design polish, tests
Security: - Replace unsalted SHA-256 password hashing with bcrypt (lazy rehash on login) - Add authenticated /api/xtream-api gateway: Xtream credentials are injected server-side and never sent to the frontend; /api/playlists no longer returns passwords - Redact credentials from all logs (login body, proxy/FFmpeg/scheduler URLs) - Add auth to recordings, EPG, season-passes and streaming routes (HttpOnly session cookie for hls.js; loopback bypass for local FFmpeg) - Lock player postMessage to same-origin in both directions - Vendor and pin hls.js 1.6.7 / mpegts.js 1.7.3 (drop CDN @latest) - Fix rate limiter (client IP was never resolved), add login rate limit, restrict CORS, add CSP Report-Only, block private-IP SSRF targets, fix path traversal in recording log retrieval, chmod 777 -> 770 - Remove dead HiveService (seeded admin/admin into IndexedDB with SHA-256) - Fix authMiddleware not populating 'user' context (getPlaylist ignored the logged-in user; admin purge always returned 403) Streaming: - New FfmpegSessionManager: process registry, idle reaper (4 min live / 15 min VOD), orphan cleanup at startup, clean SIGTERM shutdown, fast-fail with stderr instead of 30 s timeout - Quality selection (source/high/medium/low) for live and VOD; source mode streams with -c:v copy (zero transcoding); selector wired into the player - Concurrent recordings (MAX_CONCURRENT_RECORDINGS, default 2); conflicts retry on the next tick instead of silently failing - Lower live latency (HLS window 20 -> 10 segments, liveSync 10 -> 3) - Fix recording log lookup (.mp4 vs .mkv mismatch) Design: - Replace hardcoded colors with AppColors tokens (12 files) - web/theme.css syncs HTML players with the Flutter palette - DPAD/keyboard navigation (arrow-key focus, player shortcuts) - Tooltips on player icon buttons, Semantics on content cards - Remove 7 dead widgets broken since the Stitch merge Quality: - bin/test/: 21 unit tests (bcrypt, redaction, traversal, SSRF, recording conflicts) plus a quality-selector widget test - GitHub Actions CI (analyze + test + build web) - Archive stale status docs into docs/archive/ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb5eca7547
commit
60d3f42901
107 files changed
+4525
-3815
No files matched your search
+25
-19
@@ -4,9 +4,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>IPTV Player Lite</title>
|
||||
<!-- HLS.js and mpegts.js for MPEG-TS support -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.4.12/dist/hls.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mpegts.js@1.7.3/dist/mpegts.min.js"></script>
|
||||
<!-- Vendored, pinned player libraries (no CDN dependency) -->
|
||||
<script src="vendor/hls.min.js"></script>
|
||||
<script src="vendor/mpegts.min.js"></script>
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -17,7 +18,7 @@
|
||||
|
||||
html,
|
||||
body {
|
||||
background: #000;
|
||||
background: var(--color-bg);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
@@ -36,7 +37,7 @@
|
||||
#video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
#error {
|
||||
@@ -45,14 +46,14 @@
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #fff;
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
#error-icon {
|
||||
font-size: 64px;
|
||||
color: #f44336;
|
||||
color: var(--color-error);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #fff;
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
z-index: 20;
|
||||
}
|
||||
@@ -70,8 +71,8 @@
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: #fff;
|
||||
border: 4px solid var(--color-border);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||
margin: 0 auto 16px;
|
||||
@@ -91,13 +92,13 @@
|
||||
transform: translate(-50%, -50%);
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
background: var(--color-overlay);
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border: 2px solid var(--color-border);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
z-index: 25;
|
||||
@@ -180,6 +181,9 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Parent (Flutter app) is always same-origin: never broadcast to '*'
|
||||
const PARENT_ORIGIN = window.location.origin;
|
||||
|
||||
// Report position to parent
|
||||
let lastReportedTime = 0;
|
||||
function reportPosition() {
|
||||
@@ -190,14 +194,14 @@
|
||||
type: 'playback_position',
|
||||
currentTime: video.currentTime,
|
||||
duration: isFinite(video.duration) ? video.duration : 0
|
||||
}, '*');
|
||||
}, PARENT_ORIGIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
setInterval(reportPosition, 5000);
|
||||
|
||||
video.addEventListener('pause', () => {
|
||||
window.parent.postMessage({ type: 'playback_status', status: 'paused' }, '*');
|
||||
window.parent.postMessage({ type: 'playback_status', status: 'paused' }, PARENT_ORIGIN);
|
||||
if (video.currentTime === 0 || video.paused) {
|
||||
loading.style.display = 'none';
|
||||
playOverlay.style.display = 'flex';
|
||||
@@ -205,7 +209,7 @@
|
||||
});
|
||||
|
||||
video.addEventListener('playing', () => {
|
||||
window.parent.postMessage({ type: 'playback_status', status: 'playing' }, '*');
|
||||
window.parent.postMessage({ type: 'playback_status', status: 'playing' }, PARENT_ORIGIN);
|
||||
hideLoading();
|
||||
playOverlay.style.display = 'none';
|
||||
});
|
||||
@@ -214,12 +218,12 @@
|
||||
window.parent.postMessage({
|
||||
type: 'playback_ended',
|
||||
duration: isFinite(video.duration) ? video.duration : 0
|
||||
}, '*');
|
||||
}, PARENT_ORIGIN);
|
||||
});
|
||||
|
||||
// Activity reporting
|
||||
function reportActivity() {
|
||||
window.parent.postMessage({ type: 'user_activity' }, '*');
|
||||
window.parent.postMessage({ type: 'user_activity' }, PARENT_ORIGIN);
|
||||
}
|
||||
document.addEventListener('mousemove', reportActivity);
|
||||
document.addEventListener('touchstart', reportActivity);
|
||||
@@ -248,8 +252,8 @@
|
||||
maxBufferLength: 120,
|
||||
maxMaxBufferLength: 240,
|
||||
enableWorker: true,
|
||||
liveSyncDurationCount: 10,
|
||||
liveMaxLatencyDurationCount: 30,
|
||||
liveSyncDurationCount: 3,
|
||||
liveMaxLatencyDurationCount: 10,
|
||||
lowLatencyMode: false,
|
||||
nudgeOffset: 0.8,
|
||||
nudgeMaxRetries: 30,
|
||||
@@ -347,7 +351,9 @@
|
||||
}
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
const data = event.data;
|
||||
if (!data) return;
|
||||
if (data.type === 'play') video.play().catch(() => playOverlay.style.display = 'flex');
|
||||
else if (data.type === 'pause') video.pause();
|
||||
else if (data.type === 'set_volume') { video.volume = Math.max(0, Math.min(1, data.value)); video.muted = video.volume === 0; }
|
||||
|
||||
Reference in new issue
Block a user