125 lines
3.2 KiB
JavaScript
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 };
|