Files
controller-v2/firDevProxy.ts
T

37 lines
2.2 KiB
TypeScript
Raw Permalink Normal View History

/** 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);
});
}};
}