Files
RepoDructor/server/api/music/[filename].get.ts
josedario87 bf7413b45f Fix 403 error when loading music files
- Fix path traversal security check by using absolute paths
- Remove problematic fetch override that forced JSON headers on all API requests
- Add error tracking and visual indicators for failed tracks
- Correct music directory resolution for both relative and absolute paths

The main issue was the security validation comparing relative paths incorrectly,
causing legitimate music file requests to be rejected with 403 errors.
2025-08-10 01:28:16 -06:00

120 lines
3.6 KiB
TypeScript

import { promises as fs } from 'fs'
import { join, resolve, sep } from 'path'
import { createReadStream } from 'fs'
export default defineEventHandler(async (event) => {
let filename = getRouterParam(event, 'filename')
if (!filename) {
throw createError({
statusCode: 400,
statusMessage: 'Filename is required'
})
}
// Log incoming request
const headers = getHeaders(event)
const realIP = headers['x-real-ip'] || headers['x-forwarded-for'] || 'unknown'
console.log(`[MUSIC API] Request from ${realIP} for file: ${filename}`)
// Decode the filename
try {
filename = decodeURIComponent(filename)
} catch (error) {
console.error('Error decoding filename:', error)
// If decoding fails, use original filename
}
try {
const config = useRuntimeConfig()
// Determine the music directory path
let musicDir: string
if (process.env.MUSIC_DIR) {
// If MUSIC_DIR is set, resolve it (handles both absolute and relative paths)
musicDir = resolve(process.cwd(), process.env.MUSIC_DIR)
} else {
// Fallback to public/music
const defaultPublicPath = config.public?.musicPath || '/music'
const publicRel = defaultPublicPath.replace(/^\//, '')
musicDir = resolve(process.cwd(), 'public', publicRel)
}
// Resolve the full file path
const filePath = resolve(musicDir, filename)
// Security check: ensure the file is within the music directory
// Normalize both paths and add separator to ensure exact directory match
const normalizedMusicDir = musicDir.endsWith(sep) ? musicDir : musicDir + sep
if (!filePath.startsWith(normalizedMusicDir)) {
throw createError({
statusCode: 403,
statusMessage: 'Access denied'
})
}
// Check if file exists
try {
await fs.access(filePath)
} catch (error) {
throw createError({
statusCode: 404,
statusMessage: 'File not found'
})
}
// Get file stats
const stats = await fs.stat(filePath)
// Set appropriate headers
const headers = getHeaders(event)
// Handle range requests for audio streaming
const range = headers.range
if (range) {
const parts = range.replace(/bytes=/, "").split("-")
const start = parseInt(parts[0], 10)
const end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1
const chunksize = (end - start) + 1
setHeader(event, 'Content-Range', `bytes ${start}-${end}/${stats.size}`)
setHeader(event, 'Accept-Ranges', 'bytes')
setHeader(event, 'Content-Length', chunksize.toString())
setResponseStatus(event, 206)
return sendStream(event, createReadStream(filePath, { start, end }))
} else {
// Send entire file
setHeader(event, 'Content-Length', stats.size.toString())
setHeader(event, 'Accept-Ranges', 'bytes')
// Set content type based on file extension
const ext = filename.toLowerCase().split('.').pop()
const contentTypes = {
'mp3': 'audio/mpeg',
'wav': 'audio/wav',
'flac': 'audio/flac',
'm4a': 'audio/mp4',
'ogg': 'audio/ogg',
'aac': 'audio/aac'
}
setHeader(event, 'Content-Type', contentTypes[ext] || 'audio/mpeg')
return sendStream(event, createReadStream(filePath))
}
} catch (error) {
if (error.statusCode) {
throw error
}
console.error('Error serving music file:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to serve music file'
})
}
})