| import crypto from 'node:crypto' |
| import fs from 'node:fs' |
| import { stat } from 'node:fs/promises' |
| import http from 'node:http' |
| import path from 'node:path' |
| import { fileURLToPath } from 'node:url' |
|
|
| const __filename = fileURLToPath(import.meta.url) |
| const __dirname = path.dirname(__filename) |
|
|
| const distDir = path.join(__dirname, 'dist') |
| const port = Number(process.env.PORT || process.env.SERVER_PORT || 7860) |
| const host = process.env.HOST || '0.0.0.0' |
| const authUsername = process.env.HF_USERNAME || process.env.APP_USERNAME || '' |
| const authPassword = process.env.HF_PASSWORD || process.env.APP_PASSWORD || '' |
| const authEnabled = authUsername.length > 0 && authPassword.length > 0 |
|
|
| if ((authUsername && !authPassword) || (!authUsername && authPassword)) { |
| console.error('Set both HF_USERNAME and HF_PASSWORD to enable Basic Auth.') |
| process.exit(1) |
| } |
|
|
| const mimeTypes = { |
| '.css': 'text/css; charset=utf-8', |
| '.gif': 'image/gif', |
| '.html': 'text/html; charset=utf-8', |
| '.ico': 'image/x-icon', |
| '.js': 'text/javascript; charset=utf-8', |
| '.json': 'application/json; charset=utf-8', |
| '.map': 'application/json; charset=utf-8', |
| '.mid': 'audio/midi', |
| '.midi': 'audio/midi', |
| '.mp3': 'audio/mpeg', |
| '.ogg': 'audio/ogg', |
| '.png': 'image/png', |
| '.svg': 'image/svg+xml', |
| '.txt': 'text/plain; charset=utf-8', |
| '.wasm': 'application/wasm', |
| '.wav': 'audio/wav', |
| '.webp': 'image/webp' |
| } |
|
|
| function safeEqual(value, expected) { |
| const valueBuffer = Buffer.from(value) |
| const expectedBuffer = Buffer.from(expected) |
|
|
| if (valueBuffer.length !== expectedBuffer.length) { |
| return false |
| } |
|
|
| return crypto.timingSafeEqual(valueBuffer, expectedBuffer) |
| } |
|
|
| function parseBasicAuth(header) { |
| if (!header?.startsWith('Basic ')) { |
| return null |
| } |
|
|
| try { |
| const decoded = Buffer.from(header.slice(6), 'base64').toString('utf8') |
| const separatorIndex = decoded.indexOf(':') |
|
|
| if (separatorIndex === -1) { |
| return null |
| } |
|
|
| return { |
| username: decoded.slice(0, separatorIndex), |
| password: decoded.slice(separatorIndex + 1) |
| } |
| } catch { |
| return null |
| } |
| } |
|
|
| function isAuthorized(req) { |
| if (!authEnabled) { |
| return true |
| } |
|
|
| const credentials = parseBasicAuth(req.headers.authorization) |
|
|
| if (!credentials) { |
| return false |
| } |
|
|
| return ( |
| safeEqual(credentials.username, authUsername) && |
| safeEqual(credentials.password, authPassword) |
| ) |
| } |
|
|
| function sendAuthChallenge(res) { |
| res.writeHead(401, { |
| 'Content-Type': 'text/plain; charset=utf-8', |
| 'WWW-Authenticate': 'Basic realm="Jam Track Studio", charset="UTF-8"' |
| }) |
| res.end('Authentication required') |
| } |
|
|
| function getRequestedPath(req) { |
| const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`) |
| let decodedPath = '/' |
|
|
| try { |
| decodedPath = decodeURIComponent(url.pathname) |
| } catch { |
| return null |
| } |
|
|
| const normalizedPath = path.normalize(decodedPath) |
| const relativePath = |
| normalizedPath === '/' || normalizedPath === '.' |
| ? 'index.html' |
| : normalizedPath.replace(/^[/\\]+/, '') |
| const requestedPath = path.resolve(distDir, relativePath) |
| const relativeToDist = path.relative(distDir, requestedPath) |
|
|
| if (relativeToDist.startsWith('..') || path.isAbsolute(relativeToDist)) { |
| return null |
| } |
|
|
| return requestedPath |
| } |
|
|
| async function resolveStaticPath(req) { |
| const requestedPath = getRequestedPath(req) |
|
|
| if (!requestedPath) { |
| return null |
| } |
|
|
| try { |
| const fileStat = await stat(requestedPath) |
| if (fileStat.isFile()) { |
| return requestedPath |
| } |
| } catch { |
| |
| } |
|
|
| const hasFileExtension = path.extname(requestedPath).length > 0 |
| return hasFileExtension ? null : path.join(distDir, 'index.html') |
| } |
|
|
| async function handleRequest(req, res) { |
| if (!isAuthorized(req)) { |
| sendAuthChallenge(res) |
| return |
| } |
|
|
| if (req.method !== 'GET' && req.method !== 'HEAD') { |
| res.writeHead(405, { 'Content-Type': 'text/plain; charset=utf-8' }) |
| res.end('Method not allowed') |
| return |
| } |
|
|
| const filePath = await resolveStaticPath(req) |
|
|
| if (!filePath) { |
| res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }) |
| res.end('Not found') |
| return |
| } |
|
|
| const extension = path.extname(filePath).toLowerCase() |
| const headers = { |
| 'Content-Type': mimeTypes[extension] || 'application/octet-stream' |
| } |
|
|
| if (filePath.includes(`${path.sep}assets${path.sep}`)) { |
| headers['Cache-Control'] = 'public, max-age=31536000, immutable' |
| } |
|
|
| res.writeHead(200, headers) |
|
|
| if (req.method === 'HEAD') { |
| res.end() |
| return |
| } |
|
|
| fs.createReadStream(filePath).pipe(res) |
| } |
|
|
| const server = http.createServer((req, res) => { |
| handleRequest(req, res).catch((error) => { |
| console.error(error) |
| res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' }) |
| res.end('Internal server error') |
| }) |
| }) |
|
|
| server.listen(port, host, () => { |
| const authMessage = authEnabled |
| ? 'Basic Auth enabled from HF_USERNAME/HF_PASSWORD' |
| : 'Basic Auth disabled because HF_USERNAME/HF_PASSWORD are not both set' |
| console.log(`Jam Track Studio serving on http://${host}:${port}`) |
| console.log(authMessage) |
| }) |
|
|