const http = require("node:http"); const https = require("node:https"); const fs = require("node:fs"); const path = require("node:path"); const { URL } = require("node:url"); const HOST = process.env.HOST || "0.0.0.0"; const PORT = Number(process.env.PORT || 4173); const ROOT = __dirname; const DEFAULT_WMS_ENDPOINT = "https://xpro-viz.jfig.net/geoserver/xpro/wms"; const WMS_ENDPOINT = normalizeEndpoint(process.env.WMS_ENDPOINT || DEFAULT_WMS_ENDPOINT); const ALLOWED_WMS_ENDPOINTS = new Set( `${process.env.ALLOWED_WMS_ENDPOINTS || ""},${WMS_ENDPOINT}` .split(",") .map((endpoint) => normalizeEndpoint(endpoint.trim())) .filter(Boolean), ); const GEOSERVER_AUTH_HEADER = buildGeoserverAuthHeader(); const VENDOR_FILES = { "/vendor/ol.css": path.join(ROOT, "node_modules", "ol", "ol.css"), "/vendor/ol.js": path.join(ROOT, "node_modules", "ol", "dist", "ol.js"), }; const MIME_TYPES = { ".html": "text/html; charset=utf-8", ".js": "application/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json; charset=utf-8", ".png": "image/png", ".svg": "image/svg+xml", }; const server = http.createServer(async (req, res) => { try { const requestUrl = new URL(req.url, `http://${req.headers.host}`); if (requestUrl.pathname === "/api/capabilities") { await proxyCapabilities(req, res, requestUrl); return; } if (requestUrl.pathname === "/api/wms") { await proxyWms(req, res, requestUrl); return; } if (VENDOR_FILES[requestUrl.pathname]) { serveFile(VENDOR_FILES[requestUrl.pathname], res); return; } serveStatic(requestUrl.pathname, res); } catch (error) { if (!error.statusCode || error.statusCode >= 500) { console.error(error); } json(res, error.statusCode || 500, { error: error.statusCode ? error.message : "Internal server error", }); } }); server.listen(PORT, HOST, () => { console.log(`XPro Map Visualizer running at http://${HOST}:${PORT}`); }); async function proxyCapabilities(req, res, requestUrl) { const upstream = buildUpstreamUrl(requestUrl.searchParams.get("url")); upstream.searchParams.set("service", "WMS"); upstream.searchParams.set("request", "GetCapabilities"); const upstreamResponse = await fetchUpstream(upstream); res.writeHead(upstreamResponse.statusCode, { "Content-Type": upstreamResponse.contentType || "text/xml; charset=utf-8", "Cache-Control": "no-store", }); res.end(upstreamResponse.body); } async function proxyWms(req, res, requestUrl) { const upstream = buildUpstreamUrl(requestUrl.searchParams.get("url")); requestUrl.searchParams.forEach((value, key) => { if (key !== "url") { upstream.searchParams.set(key, value); } }); const upstreamResponse = await fetchUpstream(upstream); res.writeHead(upstreamResponse.statusCode, { "Content-Type": upstreamResponse.contentType || "application/octet-stream", "Cache-Control": "no-store", }); res.end(upstreamResponse.body); } function buildUpstreamUrl(rawUrl) { const endpoint = normalizeEndpoint(rawUrl || WMS_ENDPOINT); if (!ALLOWED_WMS_ENDPOINTS.has(endpoint)) { throw httpError(403, "WMS endpoint is not allowed."); } const upstream = new URL(endpoint); if (!["http:", "https:"].includes(upstream.protocol)) { throw httpError(400, "Unsupported WMS endpoint protocol."); } return upstream; } function normalizeEndpoint(rawUrl) { if (!rawUrl) { return ""; } try { const endpoint = new URL(rawUrl); endpoint.hash = ""; endpoint.search = ""; endpoint.pathname = endpoint.pathname.replace(/\/+$/, ""); return endpoint.toString(); } catch { throw httpError(400, "Invalid WMS endpoint URL."); } } function fetchUpstream(url) { const client = url.protocol === "https:" ? https : http; return new Promise((resolve, reject) => { const upstreamReq = client.request( url, { method: "GET", headers: GEOSERVER_AUTH_HEADER ? { Authorization: GEOSERVER_AUTH_HEADER } : {}, }, (upstreamRes) => { const chunks = []; upstreamRes.on("data", (chunk) => chunks.push(chunk)); upstreamRes.on("end", () => { resolve({ statusCode: upstreamRes.statusCode || 502, contentType: upstreamRes.headers["content-type"], body: Buffer.concat(chunks), }); }); }, ); upstreamReq.on("error", reject); upstreamReq.end(); }); } function buildGeoserverAuthHeader() { if (process.env.GEOSERVER_BASIC_AUTH) { return process.env.GEOSERVER_BASIC_AUTH.startsWith("Basic ") ? process.env.GEOSERVER_BASIC_AUTH : `Basic ${process.env.GEOSERVER_BASIC_AUTH}`; } if (!process.env.GEOSERVER_USERNAME || !process.env.GEOSERVER_PASSWORD) { return ""; } const credentials = `${process.env.GEOSERVER_USERNAME}:${process.env.GEOSERVER_PASSWORD}`; return `Basic ${Buffer.from(credentials).toString("base64")}`; } function serveStatic(requestPath, res) { const safePath = requestPath === "/" ? "/index.html" : requestPath; const filePath = path.resolve(ROOT, `.${safePath}`); const relativePath = path.relative(ROOT, filePath); if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { json(res, 403, { error: "Forbidden" }); return; } serveFile(filePath, res); } function serveFile(filePath, res) { fs.readFile(filePath, (error, data) => { if (error) { json(res, 404, { error: "Not found" }); return; } const ext = path.extname(filePath); res.writeHead(200, { "Content-Type": MIME_TYPES[ext] || "application/octet-stream", "Cache-Control": "no-store", }); res.end(data); }); } function json(res, statusCode, payload) { res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" }); res.end(JSON.stringify(payload)); } function httpError(statusCode, message) { const error = new Error(message); error.statusCode = statusCode; return error; }