const http = require('http'); const fs = require('fs'); const fsp = require('fs/promises'); const path = require('path'); const { URL } = require('url'); // Azure sets PORT const PORT = process.env.PORT || 8080; // Default site if host doesn't map const DEFAULT_SITE = 'default'; // Explicit host ? folder mapping const HOST_MAP = { 'adpadmin.usimdev.com': 'admin', 'adpclient.usimdev.com': 'client', 'adpregist.usimdev.com': 'regist', 'adptestapi.usimdev.com': 'testapi', 'localhost': 'default' }; const server = http.createServer(async (req, res) => { try { const hostHeader = (req.headers.host || '').toLowerCase(); console.log(`[SRV ${new Date().toISOString()}] ${req.method} ${req.url} (Host: ${hostHeader})`); // Parse URL with WHATWG API (avoids url.parse deprecation) const parsedUrl = new URL(req.url || '/', `http://${hostHeader || 'localhost'}`); const pathname = decodeURIComponent(parsedUrl.pathname || '/'); const parts = pathname.replace(/^\/+/, '').split('/'); const firstSegment = parts[0] || ''; // Decide which site folder to serve from const siteFolder = getSiteFolder(hostHeader); const BASEDIR = path.join(__dirname, 'websites', siteFolder); console.log(`Site folder resolved: /websites/${siteFolder} (host=${hostHeader})`); // --- favicon special case --- if (firstSegment === 'favicon.ico') { const favPath = safeJoin(BASEDIR, 'favicon.ico'); if (await exists(favPath)) { res.writeHead(200, { 'Content-Type': 'image/x-icon' }); return fs.createReadStream(favPath).pipe(res); } else { res.writeHead(204); return res.end(); } } // --- robots.txt (optional per-site) --- if (firstSegment === 'robots.txt') { const robotsPath = safeJoin(BASEDIR, 'robots.txt'); if (await exists(robotsPath)) { res.writeHead(200, { 'Content-Type': 'text/plain' }); return fs.createReadStream(robotsPath).pipe(res); } // no robots for this site ? 404 res.writeHead(404, { 'Content-Type': 'text/plain' }); return res.end('Not found'); } // Static file resolution let requested = firstSegment || 'index.html'; if (parts.length > 1) { requested = parts.join('/'); } let filePath = safeJoin(BASEDIR, requested); if (await isDir(filePath)) { filePath = safeJoin(filePath, 'index.html'); } // Handle Index.html vs index.html (case difference on dev machines) if (!(await exists(filePath)) && /Index\.html$/.test(filePath)) { const alt = filePath.replace(/Index\.html$/, 'index.html'); if (await exists(alt)) filePath = alt; } // If the requested file doesn't exist: // 1) try this site's index.html (SPA routing) // 2) try shared 404.html in root if (!(await exists(filePath))) { const siteIndex = safeJoin(BASEDIR, 'index.html'); if (await exists(siteIndex)) { res.writeHead(200, { 'Content-Type': 'text/html' }); return fs.createReadStream(siteIndex).pipe(res); } const shared404 = path.join(__dirname, '404.html'); if (await exists(shared404)) { res.writeHead(404, { 'Content-Type': 'text/html' }); return fs.createReadStream(shared404).pipe(res); } res.writeHead(404, { 'Content-Type': 'text/plain' }); return res.end('404 Not Found'); } // Serve the static file res.writeHead(200, { 'Content-Type': getType(filePath) }); return fs.createReadStream(filePath).pipe(res); } catch (err) { console.error('[SERVER ERROR]', err); res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ status: 'error', error: err?.message || String(err) })); } }); server.listen(PORT, () => { console.log('==========================================='); console.log(`Static HTTP server listening on ${PORT}`); console.log(`Default site: /websites/${DEFAULT_SITE}/`); console.log('Host mappings:'); Object.entries(HOST_MAP).forEach(([host, folder]) => { console.log(` ${host} ? /websites/${folder}/`); }); console.log('Fallback (subdomain): sub.domain.tld ? /websites//'); console.log('Shared 404 (optional): /404.html'); console.log('==========================================='); }); module.exports = server; /* ------------ helpers ------------ */ function getSiteFolder(hostnameRaw) { if (!hostnameRaw) return DEFAULT_SITE; const hostname = hostnameRaw.toLowerCase(); // 1) Explicit map wins if (HOST_MAP[hostname]) { return HOST_MAP[hostname]; } // 2) localhost or IP ? default if (hostname === 'localhost' || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { return DEFAULT_SITE; } // 3) For multi-tenant subdomains: foo.example.com ? "foo" const parts = hostname.split('.'); if (parts.length >= 3) { return parts[0]; } // 4) Anything else ? default return DEFAULT_SITE; } function safeJoin(base, target) { const resolved = path.resolve(base, target); if (!resolved.startsWith(base)) { // Prevent directory traversal – fall back to base return base; } return resolved; } async function exists(p) { try { await fsp.access(p, fs.constants.R_OK); return true; } catch { return false; } } async function isDir(p) { try { return (await fsp.stat(p)).isDirectory(); } catch { return false; } } function getType(filename) { const ext = path.extname(filename).toLowerCase(); switch (ext) { case '.html': return 'text/html'; case '.css': return 'text/css'; case '.js': return 'application/javascript'; case '.png': return 'image/png'; case '.jpg': case '.jpeg': return 'image/jpeg'; case '.svg': return 'image/svg+xml'; case '.ico': return 'image/x-icon'; case '.json': return 'application/json'; case '.map': return 'application/json'; case '.woff2': return 'font/woff2'; case '.woff': return 'font/woff'; case '.ttf': return 'font/ttf'; case '.eot': return 'application/vnd.ms-fontobject'; default: return 'text/plain'; } }