370 lines
14 KiB
JavaScript
370 lines
14 KiB
JavaScript
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(`<!DOCTYPE html>
|
|
<html lang="en" class="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Choose Version for ${servername}</title>
|
|
<link rel="stylesheet" href="/tailwind.css">
|
|
<script>
|
|
function flushDNSCache() {
|
|
// Attempt to flush DNS cache by making a unique request
|
|
const timestamp = new Date().getTime();
|
|
fetch(\`https://\${window.location.hostname}/_dnsflush?t=\${timestamp}\`, { cache: 'no-store' })
|
|
.catch(() => console.log('DNS flush request attempted'));
|
|
}
|
|
function showLoadingMessage(button) {
|
|
// Hide the main content and show the loading message
|
|
document.getElementById('main-content').classList.add('hidden');
|
|
document.getElementById('loading-message').classList.remove('hidden');
|
|
|
|
// Flush DNS cache and redirect
|
|
flushDNSCache();
|
|
setTimeout(() => {
|
|
window.location.href = button.getAttribute('data-href');
|
|
}, 3000); // Small delay to ensure DNS flush attempt
|
|
}
|
|
</script>
|
|
</head>
|
|
<body class="bg-gray-900 text-white flex items-center justify-center min-h-screen">
|
|
<div class="container mx-auto p-4 max-w-md">
|
|
<div id="main-content">
|
|
<h1 class="text-2xl font-bold mb-4">Multiple Versions Available for ${servername}</h1>
|
|
<p class="mb-6">This domain has both a public internet version and a P2P version. Please choose which one you want to access:</p>
|
|
<div class="flex flex-col space-y-4">
|
|
<button
|
|
onclick="showLoadingMessage(this)"
|
|
data-href="/_choose?version=public&redirect=${redirect}"
|
|
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-center">
|
|
Public Version
|
|
</button>
|
|
<button
|
|
onclick="showLoadingMessage(this)"
|
|
data-href="/_choose?version=p2p&redirect=${redirect}"
|
|
class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded text-center">
|
|
P2P Version
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="loading-message" class="hidden flex flex-col items-center justify-center">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-t-4 border-b-4 border-blue-500 mb-4"></div>
|
|
<p class="text-lg font-semibold">Please wait while we redirect you.</p>
|
|
<p class="text-sm text-gray-400">You can enable or disable your choice within the admin area</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`);
|
|
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,
|
|
};
|
|
|