feat: split web and api into separate apps

This commit is contained in:
M1
2026-03-18 09:33:46 +04:00
parent ba437e3c5a
commit 841a852491
10 changed files with 593 additions and 34 deletions
+95
View File
@@ -0,0 +1,95 @@
import { createHash } from "crypto";
import dns from "dns/promises";
const BLOCKED_TLDS = [".local", ".internal", ".corp", ".lan"];
const BLOCKED_HOSTNAMES = ["localhost", "localhost."];
/**
* Checks whether an IP address is in a private/reserved range.
*/
function isPrivateIP(ip: string): boolean {
// IPv4
if (ip === "0.0.0.0") return true;
if (ip.startsWith("127.")) return true; // 127.0.0.0/8
if (ip.startsWith("10.")) return true; // 10.0.0.0/8
if (ip.startsWith("192.168.")) return true; // 192.168.0.0/16
if (ip.startsWith("169.254.")) return true; // 169.254.0.0/16 (link-local + cloud metadata)
// 172.16.0.0/12: 172.16.x.x 172.31.x.x
if (ip.startsWith("172.")) {
const second = parseInt(ip.split(".")[1] ?? "", 10);
if (second >= 16 && second <= 31) return true;
}
// IPv6
if (ip === "::1" || ip === "::") return true;
if (ip.toLowerCase().startsWith("fe80")) return true; // fe80::/10
if (ip.toLowerCase().startsWith("fd00:ec2::254")) return true; // AWS EC2 metadata
if (ip.toLowerCase() === "::ffff:127.0.0.1") return true;
if (ip.toLowerCase().startsWith("::ffff:")) {
// IPv4-mapped IPv6 — extract the IPv4 part and re-check
const v4 = ip.slice(7);
return isPrivateIP(v4);
}
return false;
}
/**
* Validates a monitor URL is safe to fetch (not targeting internal resources).
* Returns null if safe, or an error string if blocked.
*/
export async function validateMonitorUrl(url: string): Promise<string | null> {
let parsed: URL;
try {
parsed = new URL(url);
} catch {
return "Invalid URL";
}
// Only allow http and https
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return `Blocked scheme: ${parsed.protocol} — only http: and https: are allowed`;
}
const hostname = parsed.hostname.toLowerCase();
// Block localhost by name
if (BLOCKED_HOSTNAMES.includes(hostname)) {
return "Blocked hostname: localhost is not allowed";
}
// Block non-public TLDs
for (const tld of BLOCKED_TLDS) {
if (hostname.endsWith(tld)) {
return `Blocked TLD: ${tld} is not allowed`;
}
}
// Resolve DNS and check all IPs
try {
const ips: string[] = [];
try {
const v4 = await dns.resolve4(hostname);
ips.push(...v4);
} catch {}
try {
const v6 = await dns.resolve6(hostname);
ips.push(...v6);
} catch {}
if (ips.length === 0) {
return "Could not resolve hostname";
}
for (const ip of ips) {
if (isPrivateIP(ip)) {
return `Blocked: ${hostname} resolves to private/reserved IP ${ip}`;
}
}
} catch {
return "DNS resolution failed";
}
return null;
}