const http = require("http"); const https = require("https"); const tls = require("tls"); const url = require("url"); const path = require("path"); const fs = require("fs"); const httpProxy = require("http-proxy"); const { logDebug, logError, logWarn, logInfo } = require("../infrastructure/logger"); const state = require("../infrastructure/state"); const { getOrCreateDomainCert, getCaChain } = require("../security/certificate_authority"); // Maps for managing proxy servers per bind IP const httpServers = new Map(); // bindIp => httpServer const tlsServers = new Map(); // bindIp => tlsServer const configPerIp = new Map(); // bindIp => Map(lowercaseDomain => {key, cert, targetPort}) // Helper function to normalize IPs function getIPs(ipInput) { const ips = []; const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; function extract(input) { if ( typeof input === "string" && (input.match(ipv4Regex) || input.match(ipv6Regex)) ) { ips.push(input); } else if (Array.isArray(input)) { input.forEach(extract); } else if (typeof input === "object" && input !== null) { ["ip", "address", "value", "ipv4", "ipv6"].forEach((key) => { if (input[key]) extract(input[key]); }); } } extract(ipInput); return [...new Set(ips)]; // unique IPs } // Start TLS proxy with HTTP redirect function createTlsProxy(domain, ipInput, targetPort, logDebug) { const { key, cert } = getOrCreateDomainCert(domain, ipInput); const ips = getIPs(ipInput); const bindIp = ips.length > 0 ? ips[0] : "0.0.0.0"; let ipConfig = configPerIp.get(bindIp); if (!ipConfig) { ipConfig = new Map(); configPerIp.set(bindIp, ipConfig); } ipConfig.set(domain.toLowerCase(), { key, cert, targetPort }); // HTTP server per bindIp let httpServer = httpServers.get(bindIp); if (!httpServer) { httpServer = http.createServer((req, res) => { const httpsPort = process.env.HTTPS_PORT || 443; const host = req.headers.host ? req.headers.host.split(":")[0] : "localhost"; const portPart = httpsPort === 443 ? "" : `:${httpsPort}`; const redirectUrl = `https://${host}${portPart}${req.url}`; res.writeHead(301, { Location: redirectUrl }); res.end(); logInfo( "HTTP", `Redirected ${req.headers.host || "unknown"} to ${redirectUrl}` ); }); httpServer.listen(80, bindIp, () => { logInfo("HTTP", `HTTP redirect server listening on ${bindIp}:80`); }); httpServer.on("error", (err) => { logError("HTTP", `HTTP server error on ${bindIp}: ${err.message}`); }); httpServers.set(bindIp, httpServer); } // TLS server per bindIp with SNI let tlsServer = tlsServers.get(bindIp); if (!tlsServer) { tlsServer = https.createServer( { SNICallback: (servername, cb) => { const conf = ipConfig.get(servername.toLowerCase()); if (conf) { const ctx = tls.createSecureContext({ key: conf.key, cert: conf.cert, }); cb(null, ctx); } else { logWarn("TLS", `No cert for servername: ${servername}`); cb(new Error("No cert found for this domain")); } }, }, async (req, res) => { const admin = require("../admin"); const servername = req.headers.host ? req.headers.host.split(":")[0] : req.socket.servername; if (!servername) { logError("TLS", "No servername provided"); res.writeHead(400); res.end("Bad Request"); return; } const conf = ipConfig.get(servername.toLowerCase()); if (!conf) { logError("TLS", `No configuration found for ${servername}`); res.writeHead(404); res.end("Not Found"); return; } let cookies = {}; if (req.headers.cookie) { req.headers.cookie.split(";").forEach((c) => { const [key, val] = c.trim().split("="); cookies[key] = val; }); } // Serve Tailwind CSS if (req.url === '/tailwind.css' || req.url.startsWith('/tailwind.css?')) { try { const css = await fs.promises.readFile(path.join(__dirname, '..', 'css', 'tailwind.css'), 'utf8'); res.writeHead(200, { 'Content-Type': 'text/css' }); res.end(css); } catch (err) { logError('TLS', `Failed to serve tailwind.css: ${err.message}`); res.writeHead(500); res.end('Failed to load Tailwind CSS'); } return; } let version = cookies.version; let isChoose = req.url.startsWith("/_choose?"); let parsedUrl = new url.URL(req.url, `https://${servername}`); if (isChoose) { version = parsedUrl.searchParams.get("version"); if (version === "p2p" || version === "public") { state.versionPreferences.set(servername.toLowerCase(), version); await admin.saveSelectorCache(); // Save to selector_cache.json admin.broadcast({ type: "update-local-dns" }); // Notify clients to update DNS Conflict Selector const redirect = parsedUrl.searchParams.get("redirect") || "/"; res.writeHead(302, { "Set-Cookie": `version=${version}; Path=/; Max-Age=31536000; HttpOnly`, Location: redirect, }); res.end(); logInfo( "TLS", `Set version preference for ${servername} to ${version} and saved to selector_cache.json` ); return; } } // Check if a version preference is set in state.versionPreferences const stateVersion = state.versionPreferences.get( servername.toLowerCase() ); if ( state.domainsWithBoth.has(servername.toLowerCase()) && !version && !stateVersion ) { // Show middleware page only if no cookie and no state preference const redirect = encodeURIComponent(req.url); res.writeHead(200, { "Content-Type": "text/html" }); res.end(` Choose Version for ${servername}

Multiple Versions Available for ${servername}

This domain has both a public internet version and a P2P version. Please choose which one you want to access:

`); return; } // Use stateVersion if available, otherwise fall back to cookie or default to 'p2p' const effectiveVersion = stateVersion || version || "p2p"; let target; let proxy; if ( state.domainsWithBoth.has(servername.toLowerCase()) && effectiveVersion === "public" ) { const publicIP = state.publicIpForDomain[servername.toLowerCase()]; if (!publicIP) { res.writeHead(500); res.end("No public IP available"); return; } let ca = state.caForDomain.get(servername); if (ca === undefined) { ca = await getCaChain(servername, publicIP); state.caForDomain.set(servername, ca); } let agentOptions = { servername: servername, checkServerIdentity: (host, cert) => { return tls.checkServerIdentity(servername, cert); }, createConnection: (options, cb) => { options.host = publicIP; return tls.connect(options, cb); }, }; if (ca) { agentOptions.ca = ca; } else { agentOptions.rejectUnauthorized = false; logWarn( "TLS", `No CA chain for ${servername}, using rejectUnauthorized: false` ); } const agent = new https.Agent(agentOptions); target = `https://${servername}`; proxy = httpProxy.createProxyServer({}); proxy.web(req, res, { target, agent, changeOrigin: false, secure: !!ca, }); } else { // Check if domain uses SSL/TLS const { getDomainSSLStatus } = require('../core/core'); const useSSL = await getDomainSSLStatus(servername); const protocol = useSSL ? 'https' : 'http'; target = `${protocol}://${bindIp}:${conf.targetPort}`; proxy = httpProxy.createProxyServer({}); proxy.web(req, res, { target }); } } ); tlsServer.on("upgrade", async (req, socket, head) => { const servername = req.headers.host ? req.headers.host.split(":")[0] : ""; const conf = ipConfig.get(servername.toLowerCase()); if (!conf) { socket.destroy(); return; } let cookies = {}; if (req.headers.cookie) { req.headers.cookie.split(";").forEach((c) => { const [key, val] = c.trim().split("="); cookies[key] = val; }); } let version = cookies.version || "p2p"; const stateVersion = state.versionPreferences.get( servername.toLowerCase() ); const effectiveVersion = stateVersion || version; let target; if ( state.domainsWithBoth.has(servername.toLowerCase()) && effectiveVersion === "public" ) { const publicIP = state.publicIpForDomain[servername.toLowerCase()]; if (!publicIP) { socket.destroy(); return; } let ca = state.caForDomain.get(servername); let agentOptions = { servername: servername, checkServerIdentity: (host, cert) => { return tls.checkServerIdentity(servername, cert); }, createConnection: (options, cb) => { options.host = publicIP; return tls.connect(options, cb); }, }; if (ca) { agentOptions.ca = ca; } else { agentOptions.rejectUnauthorized = false; logWarn( "TLS", `No CA chain for ${servername} (WS), using rejectUnauthorized: false` ); } const agent = new https.Agent(agentOptions); target = `wss://${servername}`; const proxy = httpProxy.createProxyServer({}); proxy.ws(req, socket, head, { target, agent, changeOrigin: false, secure: !!ca, }); } else { // Check if domain uses SSL/TLS for WebSocket const { getDomainSSLStatus } = require('../core/core'); const useSSL = await getDomainSSLStatus(servername); const protocol = useSSL ? 'wss' : 'ws'; target = `${protocol}://${bindIp}:${conf.targetPort}`; const proxy = httpProxy.createProxyServer({}); proxy.ws(req, socket, head, { target }); } }); tlsServer.listen(process.env.HTTPS_PORT || 443, bindIp, () => { logInfo( "TLS", `TLS proxy listening on ${bindIp}:${process.env.HTTPS_PORT || 443}` ); }); tlsServer.on("error", (err) => { logError("TLS", `TLS server error on ${bindIp}: ${err.message}`); }); tlsServers.set(bindIp, tlsServer); } return { tlsServer, httpServer }; } module.exports = { createTlsProxy, };