const fs = require("fs"); const target = process.argv[2]; if (!target) { throw new Error("server.js path is required"); } let source = fs.readFileSync(target, "utf8"); const replaceOnce = (label, before, after) => { const first = source.indexOf(before); if (first === -1) { throw new Error(`proxy patch failed: ${label} pattern not found`); } if (source.indexOf(before, first + before.length) !== -1) { throw new Error(`proxy patch failed: ${label} pattern matched more than once`); } source = source.slice(0, first) + after + source.slice(first + before.length); }; const replaceAllCount = (label, before, after, expectedCount) => { const parts = source.split(before); const count = parts.length - 1; if (count !== expectedCount) { throw new Error(`proxy patch failed: ${label} expected ${expectedCount} matches, found ${count}`); } source = parts.join(after); }; replaceOnce( "referer target guard", [ " if (!PRIVATE_IP.test(parsedUpstream.hostname)) {", " return null;", " }" ].join("\n"), [ " if (!isProxyTargetAllowed(parsedUpstream)) {", " return null;", " }" ].join("\n") ); replaceOnce( "cookie target guard", [ " if (!PRIVATE_IP.test(parsed.hostname)) {", " return null;", " }" ].join("\n"), [ " if (!isProxyTargetAllowed(parsed)) {", " return null;", " }" ].join("\n") ); replaceOnce( "resource rewrite hook", [ " cleaned = rewriteUpstreamAbsoluteAssetUrls(cleaned, annotationSite);", "", " if (cleaned.includes(\"\")) {" ].join("\n"), [ " cleaned = rewriteUpstreamAbsoluteAssetUrls(cleaned, annotationSite);", " cleaned = rewriteProxyResourceUrls(cleaned, annotationUrl || annotationSite, apiBase);", "", " if (cleaned.includes(\"\")) {" ].join("\n") ); replaceOnce( "dev asset target guard", [ " if (!PRIVATE_IP.test(upstreamAssetUrl.hostname)) {", " return res.status(403).json({ error: \"only private/LAN addresses allowed\" });", " }" ].join("\n"), [ " if (!isProxyTargetAllowed(upstreamAssetUrl)) {", " return res.status(403).json({ error: \"target host is not allowed by proxy policy\" });", " }" ].join("\n") ); replaceOnce( "proxy policy block", [ "// Server-side proxy — fetches target page and injects the annotation overlay.", "// Restricted to private/LAN IP ranges only to prevent SSRF abuse.", "const PRIVATE_IP = /^(localhost|127\\.\\d+\\.\\d+\\.\\d+|10\\.\\d+\\.\\d+\\.\\d+|172\\.(1[6-9]|2\\d|3[01])\\.\\d+\\.\\d+|192\\.168\\.\\d+\\.\\d+)/i;" ].join("\n"), [ "// Server-side proxy target policy. Public production targets must be explicitly", "// allowlisted. Private/LAN targets are disabled by default and can be enabled", "// only for trusted local-development deployments.", "const PRIVATE_IP = /^(localhost|127\\.\\d+\\.\\d+\\.\\d+|10\\.\\d+\\.\\d+\\.\\d+|172\\.(1[6-9]|2\\d|3[01])\\.\\d+\\.\\d+|192\\.168\\.\\d+\\.\\d+|169\\.254\\.\\d+\\.\\d+|::1|\\[::1\\])$/i;", "const PROXY_ALLOW_PRIVATE = [\"1\", \"true\", \"yes\"].includes(", " String(process.env.ANNONET_PROXY_ALLOW_PRIVATE || \"\").trim().toLowerCase()", ");", "const PROXY_ALLOWED_HOSTS = String(process.env.ANNONET_PROXY_ALLOWED_HOSTS || \"\")", " .split(\",\")", " .map((value) => value.trim().toLowerCase())", " .filter(Boolean);", "", "const proxyHostMatches = (hostname, pattern) => {", " if (pattern === hostname) return true;", " if (pattern.startsWith(\"*.\")) {", " const suffix = pattern.slice(1);", " return hostname.endsWith(suffix) && hostname.length > suffix.length;", " }", " return false;", "};", "", "const isProxyTargetAllowed = (parsed) => {", " if (!parsed || ![\"http:\", \"https:\"].includes(parsed.protocol)) return false;", " if (parsed.username || parsed.password) return false;", "", " const hostname = String(parsed.hostname || \"\").trim().toLowerCase();", " if (!hostname) return false;", " if (PRIVATE_IP.test(hostname)) return PROXY_ALLOW_PRIVATE;", "", " return PROXY_ALLOWED_HOSTS.some((pattern) => proxyHostMatches(hostname, pattern));", "};", "", "const toProxyResourceUrl = (rawValue, pageUrl, apiBase) => {", " const raw = String(rawValue || \"\").trim();", " if (!raw || raw.startsWith(\"#\") || /^(?:data:|blob:|javascript:|mailto:|tel:)/i.test(raw)) return rawValue;", "", " try {", " const base = new URL(pageUrl);", " const resolved = new URL(raw, base);", " if (resolved.origin !== base.origin) return rawValue;", " return apiBase + \"/api/proxy?url=\" + encodeURIComponent(resolved.toString());", " } catch (_error) {", " return rawValue;", " }", "};", "", "const rewriteProxyResourceUrls = (html, pageUrl, apiBase) => {", " return String(html || \"\").replace(", " /(<(?:script|link|img|source|video|audio|iframe)\\b[^>]*?\\s(?:src|href|poster)=[\"'])([^\"']+)([\"'])/gi,", " (_match, prefix, value, suffix) => prefix + toProxyResourceUrl(value, pageUrl, apiBase) + suffix", " );", "};", "", "const rewriteProxyCssUrls = (css, pageUrl, apiBase) => {", " return String(css || \"\").replace(", " /url\\(\\s*([\"']?)([^)\"']+)\\1\\s*\\)/gi,", " (_match, quote, value) => {", " const rewritten = toProxyResourceUrl(value, pageUrl, apiBase);", " return \"url(\" + quote + rewritten + quote + \")\";", " }", " );", "};" ].join("\n") ); replaceOnce( "redirect target guard", [ " if (!PRIVATE_IP.test(next.hostname)) {", " // Redirect leaves private network — stop following, return current response.", " return { response, allSetCookies, finalUrl: url };", " }" ].join("\n"), [ " if (!isProxyTargetAllowed(next)) {", " const error = new Error(\"redirect target host is not allowed by proxy policy\");", " error.status = 403;", " throw error;", " }" ].join("\n") ); replaceAllCount( "main proxy target guards", [ " if (!PRIVATE_IP.test(parsed.hostname)) {", " return res.status(403).json({ error: \"only private/LAN addresses allowed\" });", " }" ].join("\n"), [ " if (!isProxyTargetAllowed(parsed)) {", " return res.status(403).json({ error: \"target host is not allowed by proxy policy\" });", " }" ].join("\n"), 2 ); replaceOnce( "proxied CSS rewrite", [ " if (!contentType.includes(\"text/html\")) {", " const body = await upstream.arrayBuffer();", " res.set(\"Content-Type\", contentType);", " return res.send(Buffer.from(body));", " }" ].join("\n"), [ " if (!contentType.includes(\"text/html\")) {", " res.set(\"Content-Type\", contentType);", " if (contentType.includes(\"text/css\")) {", " const css = await upstream.text();", " return res.send(rewriteProxyCssUrls(css, finalUrl, apiBase));", " }", " const body = await upstream.arrayBuffer();", " return res.send(Buffer.from(body));", " }" ].join("\n") ); if (source.includes("only private/LAN addresses allowed")) { throw new Error("proxy patch failed: stale LAN-only error remains"); } fs.writeFileSync(target, source); console.log("Applied annoNet public proxy allowlist patch");