Files

125 lines
3.2 KiB
JavaScript

'use strict';
const http = require('http');
const https = require('https');
const PORT = Number(process.env.PORT) || 3000;
const PROXY_TIMEOUT_MS = Number(process.env.PROXY_TIMEOUT_MS) || 30000;
const VM_DOCS_BASE_URL = process.env.VM_DOCS_BASE_URL;
if (!VM_DOCS_BASE_URL) {
throw new Error('VM_DOCS_BASE_URL environment variable is required');
}
const UPSTREAM = new URL(VM_DOCS_BASE_URL);
if (UPSTREAM.protocol !== 'http:' && UPSTREAM.protocol !== 'https:') {
throw new Error('VM_DOCS_BASE_URL must start with http:// or https://');
}
const UPSTREAM_MODULE = UPSTREAM.protocol === 'https:' ? https : http;
const UPSTREAM_BASE_PATH = UPSTREAM.pathname.replace(/\/+$/, '');
const UPSTREAM_PORT = UPSTREAM.port || (UPSTREAM.protocol === 'https:' ? 443 : 80);
const STAND_RE = /^[A-Za-z0-9._-]+$/;
const FORWARD_HEADERS = ['content-type', 'content-length', 'cache-control', 'content-encoding', 'location'];
function respond(res, status, body) {
if (!res.headersSent) {
res.writeHead(status, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': Buffer.byteLength(body),
});
}
res.end(body);
}
function proxyRequest(req, res, targetPath) {
const options = {
protocol: UPSTREAM.protocol,
hostname: UPSTREAM.hostname,
port: UPSTREAM_PORT,
method: req.method,
path: targetPath,
timeout: PROXY_TIMEOUT_MS,
};
const upstreamReq = UPSTREAM_MODULE.request(options, (upstreamRes) => {
const headers = {};
for (const name of FORWARD_HEADERS) {
if (upstreamRes.headers[name] !== undefined) {
headers[name] = upstreamRes.headers[name];
}
}
res.writeHead(upstreamRes.statusCode || 502, headers);
upstreamRes.pipe(res);
});
upstreamReq.on('timeout', () => {
upstreamReq.destroy(new Error('Upstream timeout'));
});
upstreamReq.on('error', () => {
if (res.headersSent) {
res.destroy();
} else {
respond(res, 502, 'Bad Gateway');
}
});
req.on('aborted', () => upstreamReq.destroy());
upstreamReq.end();
}
const server = http.createServer((req, res) => {
if (req.method !== 'GET' && req.method !== 'HEAD') {
respond(res, 405, 'Method Not Allowed');
return;
}
const rawUrl = req.url || '/';
const qIndex = rawUrl.indexOf('?');
const rawPath = qIndex === -1 ? rawUrl : rawUrl.slice(0, qIndex);
const rawQuery = qIndex === -1 ? '' : rawUrl.slice(qIndex);
if (rawPath === '/health') {
respond(res, 200, 'ok');
return;
}
let decoded;
try {
decoded = decodeURIComponent(rawPath);
} catch (_) {
respond(res, 400, 'Bad Request');
return;
}
if (decoded.split('/').some((seg) => seg === '..')) {
respond(res, 400, 'Bad Request');
return;
}
let targetPath;
if (decoded === '/') {
targetPath = UPSTREAM_BASE_PATH + '/' + rawQuery;
} else {
const stand = decoded.split('/').filter(Boolean)[0];
if (!stand || !STAND_RE.test(stand)) {
respond(res, 400, 'Bad Request');
return;
}
targetPath = UPSTREAM_BASE_PATH + rawPath + rawQuery;
}
proxyRequest(req, res, targetPath);
});
if (require.main === module) {
server.listen(PORT, () => {
console.log(`[docs-proxy] listening on port ${PORT}, upstream: ${VM_DOCS_BASE_URL}`);
});
}
module.exports = { server };