Changeset 21522ae in Klonkt for src/routes/audio.js
- 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)
- File:
-
- 1 edited
-
src/routes/audio.js (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
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
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)