/** Development relay for FIR WAV POSTs and GETs to private IPv4 devices. */ import http from 'node:http'; import type { Plugin } from 'vite'; export function firDevProxy(): Plugin { return { name: 'fir-device-dev-proxy', apply: 'serve', configureServer(server) { server.middlewares.use((req, res, next) => { if (!req.url?.startsWith('/fir-device/')) return next(); const match = /^\/fir-device\/(\d+\.\d+\.\d+\.\d+)\/(left|right)$/.exec(req.url); const octets = match?.[1].split('.').map(Number); const privateIp = octets && octets.every(n => n >= 0 && n <= 255) && (octets[0] === 10 || (octets[0] === 192 && octets[1] === 168) || (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31)); if (!['GET', 'POST'].includes(req.method || '') || !match || !privateIp) { res.statusCode = 400; res.end('Invalid FIR device request'); return; } const length = Number(req.headers['content-length']); if (req.method === 'POST' && ![32822, 32826].includes(length)) { res.statusCode = 422; res.end('Invalid FIR payload size'); return; } let upstreamResponse: http.IncomingMessage | undefined; const upstream = http.request({ hostname: match[1], port: 80, method: req.method, agent: false, path: `/dev/info.cgi?action=fir&channel=${match[2]}`, headers: req.method === 'POST' ? { 'Content-Type': 'audio/wav', 'Content-Length': length } : {}, }, response => { upstreamResponse = response; res.statusCode = response.statusCode || 502; res.setHeader('Content-Type', response.headers['content-type'] || 'text/plain'); response.pipe(res); response.on('error', () => res.destroy()); }); upstream.setTimeout(3500, () => upstream.destroy(new Error('Device upload timed out'))); upstream.on('error', () => { // Firmware can append bytes after Content-Length. Node reports a parse error // for that tail even when the entire declared WAV has already arrived. if (upstreamResponse?.complete) return; if (!res.headersSent) { res.statusCode = 502; res.end('Device FIR upload failed'); } else res.destroy(); }); req.on('aborted', () => upstream.destroy()); req.pipe(upstream); }); }}; }