Some checks failed
Client Admin / build-deploy (push) Failing after 8s
Client Client / build-deploy (push) Failing after 3s
Client Registration / build-deploy (push) Failing after 20s
Client Tech / build-deploy (push) Failing after 1s
Client Home / build-deploy (push) Successful in 14s
200 lines
6.0 KiB
JavaScript
200 lines
6.0 KiB
JavaScript
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/<sub>/');
|
||
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';
|
||
}
|
||
}
|