jam-tracks / frontend /server.js
Mina Emadi
Add Space basic auth server
f33aeaa
Raw
History Blame Contribute Delete
5.19 kB
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 {
// Fall through to SPA fallback below.
}
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)
})