Changeset 21522ae in Klonkt for src/routes
- Timestamp:
- 05/20/2026 10:14:01 PM (4 months ago)
- Branches:
- main
- Children:
- 353c39c
- Parents:
- 46f23fd
- git-author:
- Robin Genis <roboburr@…> (05/20/2026 10:13:26 PM)
- git-committer:
- Robin Genis <roboburr@…> (05/20/2026 10:14:01 PM)
- Location:
- src/routes
- Files:
-
- 4 edited
-
admin-audio.js (modified) (3 diffs)
-
admin-playlists.js (modified) (1 diff)
-
audio.js (modified) (3 diffs)
-
posts.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/admin-audio.js
r46f23fd r21522ae 20 20 import { requireGod } from '../middleware/auth.js'; 21 21 import { transcodeToMp3 } from '../services/AudioTranscoder.js'; 22 import { signUrl } from '../services/AudioStreamService.js';22 import { audioUrl } from '../services/AudioStreamService.js'; 23 23 24 24 const __dirname = path.dirname(fileURLToPath(import.meta.url)); … … 80 80 `).all(site.id); 81 81 82 // Sign each track's stream URL so admins can preview audio inline. 83 // Short TTL (default 10 min from AudioStreamService) means the URL on 84 // the page expires if it sits open too long; a refresh re-signs. 82 // Build each track's stream URL so admins can preview audio inline. 85 83 const tracks = rows.map(t => ({ 86 84 ...t, 87 stream_url: t.filename ? signUrl(t.filename).url: null,85 stream_url: t.filename ? audioUrl(t.filename) : null, 88 86 })); 89 87 … … 335 333 `).get(req.params.id, site.id); 336 334 if (!t) return res.status(404).json({ error: 'Track niet gevonden' }); 337 // S ign the stream URL so the modal can render an inline preview player.338 const stream_url = t.filename ? signUrl(t.filename).url: null;335 // Stream URL so the modal can render an inline preview player. 336 const stream_url = t.filename ? audioUrl(t.filename) : null; 339 337 res.json({ ok: true, track: { ...t, stream_url } }); 340 338 }); -
src/routes/admin-playlists.js
r46f23fd r21522ae 112 112 if (!site) return res.status(404).json({ error: 'Site required' }); 113 113 114 // Editor needs the raw track-id list (not s igned URLs) — pass no signUrl.114 // Editor needs the raw track-id list (not stream URLs) — pass no urlFor. 115 115 const playlist = PlaylistService.get(site.id, req.params.id, null); 116 116 if (!playlist) return res.status(404).json({ error: 'Playlist niet gevonden' }); -
src/routes/audio.js
r46f23fd r21522ae 1 1 /** 2 * Audio streaming routes — v9-style signed URL + byte-range support.2 * Audio streaming routes — byte-range streaming. 3 3 * 4 * Files live in storage/media/audio/ and are NOT served by the static 5 * /media handler — every fetch must go through this verified route. 4 * Files live in storage/audio/ and are NOT served by the static /media 5 * handler — every fetch goes through this route, which adds byte-range 6 * support so HTML5 <audio> can seek. 6 7 * 7 * GET /audio/stream/:filename?t=<hmac>&exp=<unix> 8 * Verifies the token. If valid, streams the file with byte-range support 9 * so HTML5 <audio> can seek. Anything invalid returns 403. 8 * GET /audio/stream/:filename 9 * Streams the file with byte-range support. 10 * 11 * ANTI-THEFT (Spotify-flavoured, step 1 — 2026-05-20): 12 * The player never exposes this URL to the user — it fetch()es the bytes 13 * and plays from a blob: object URL (no shareable link, no "save audio as"). 14 * This route additionally refuses anything that isn't a same-origin browser 15 * fetch, so the raw URL can't be pasted into the address bar, hotlinked from 16 * another site, or pulled with curl/yt-dlp. 17 * 18 * A request is allowed when EITHER: 19 * - it carries the X-Audio-Player header (our fetch sets it), OR 20 * - Sec-Fetch-Site is same-origin/same-site (covers the admin <audio> 21 * preview, which can't set custom headers). 22 * Address-bar paste sends Sec-Fetch-Site: none; hotlinks send cross-site; 23 * curl/yt-dlp send neither signal → all rejected. 10 24 */ 11 25 … … 14 28 import path from 'path'; 15 29 import { fileURLToPath } from 'url'; 16 import { verifyToken } from '../services/AudioStreamService.js';17 30 18 31 const __dirname = path.dirname(fileURLToPath(import.meta.url)); 19 32 // Audio files live OUTSIDE storage/media — the public /media static handler 20 // cannot reach them. Every fetch must go through this signed route.33 // cannot reach them. Every fetch must go through this gated route. 21 34 const AUDIO_DIR = path.resolve( 22 35 process.env.AUDIO_PATH || path.join(__dirname, '..', '..', 'storage', 'audio') … … 39 52 }; 40 53 54 // Access gate: allow only same-origin browser fetches / media loads. 55 function isAllowedAudioRequest(req) { 56 if (req.get('X-Audio-Player') === '1') return true; // our blob fetch 57 const site = req.get('Sec-Fetch-Site'); // set by modern browsers 58 return site === 'same-origin' || site === 'same-site'; 59 } 60 41 61 router.get('/stream/:filename', (req, res) => { 42 62 const { filename } = req.params; 43 const { t, exp } = req.query; 63 64 if (!isAllowedAudioRequest(req)) { 65 return res.status(403).send('Direct access not allowed'); 66 } 44 67 45 68 // Sanity: no path traversal, no slashes 46 69 if (!filename || filename.includes('/') || filename.includes('\\') || filename.includes('..')) { 47 70 return res.status(400).send('Bad filename'); 48 }49 50 if (!verifyToken(filename, t, exp)) {51 return res.status(403).send('Invalid or expired token');52 71 } 53 72 -
src/routes/posts.js
r46f23fd r21522ae 13 13 import AudioEmbedService from '../services/AudioEmbedService.js'; 14 14 import PlaylistService from '../services/PlaylistService.js'; 15 import { signUrl } from '../services/AudioStreamService.js';15 import { audioUrl } from '../services/AudioStreamService.js'; 16 16 17 17 const __dirname = path.dirname(fileURLToPath(import.meta.url)); … … 419 419 artist: r.artist, 420 420 cover: r.cover_url, 421 url: signUrl(r.filename).url,421 url: audioUrl(r.filename), 422 422 }; 423 423 }); … … 439 439 if (!byAlbum.has(r.album)) byAlbum.set(r.album, []); 440 440 byAlbum.get(r.album).push({ 441 url: signUrl(r.filename).url,441 url: audioUrl(r.filename), 442 442 title: r.title || 'Untitled', 443 443 artist: r.artist || '', … … 464 464 const isAdmin = req.session?.user?.role === 'god'; 465 465 html = AudioEmbedService.embedPlaylistShortcodes(html, (id) => { 466 return PlaylistService.get(site.id, id, signUrl);466 return PlaylistService.get(site.id, id, audioUrl); 467 467 }, { isAdmin }); 468 468 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)