import fs from 'node:fs/promises'; import net from 'node:net'; import path from 'node:path'; const SCANNED_PUBLIC_EXTENSIONS = new Set(['.css', '.html']); const ALLOWED_TKMIND_HOSTS = new Set([ 'asr.tkmind.cn', 'go.tkmind.cn', 'm.tkmind.cn', 'plaza.tkmind.cn', 'rybbit.tkmind.cn', ]); function isPrivateIpv4(hostname) { const parts = hostname.split('.').map(Number); if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part))) return false; return parts[0] === 10 || parts[0] === 127 || (parts[0] === 169 && parts[1] === 254) || (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || (parts[0] === 192 && parts[1] === 168); } export function classifyPublicUrl(rawUrl) { let url; try { url = new URL(rawUrl); } catch { return 'invalid_absolute_url'; } const hostname = url.hostname.toLowerCase(); if ( hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1' || isPrivateIpv4(hostname) ) { return 'loopback_or_private_host'; } if (hostname.endsWith('.tkmind.cn') && !ALLOWED_TKMIND_HOSTS.has(hostname)) { return 'unknown_tkmind_public_host'; } return null; } function extractPublicDocumentUrls(source, extension) { const urls = []; if (extension === '.html') { const attributePattern = /\b(?:action|content|href|poster|src)\s*=\s*["'](https?:\/\/[^"'<> \t\r\n]+)["']/gi; for (const match of source.matchAll(attributePattern)) urls.push(match[1]); } if (extension === '.css') { const cssPattern = /url\(\s*["']?(https?:\/\/[^"') \t\r\n]+)["']?\s*\)/gi; for (const match of source.matchAll(cssPattern)) urls.push(match[1]); } return urls; } async function walkPublicDocuments(root, current = root) { const entries = await fs.readdir(current, { withFileTypes: true }); const files = []; for (const entry of entries) { const absolute = path.join(current, entry.name); if (entry.isDirectory()) files.push(...await walkPublicDocuments(root, absolute)); else if (entry.isFile() && SCANNED_PUBLIC_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) { files.push(absolute); } } return files; } export async function inspectRuntimePublicUrls(runtimeRoot) { const distRoot = path.join(runtimeRoot, 'dist'); const findings = []; for (const file of await walkPublicDocuments(distRoot)) { const extension = path.extname(file).toLowerCase(); const source = await fs.readFile(file, 'utf8'); for (const rawUrl of extractPublicDocumentUrls(source, extension)) { const reason = classifyPublicUrl(rawUrl); if (reason) { findings.push({ file: path.relative(runtimeRoot, file).split(path.sep).join('/'), url: rawUrl, reason, }); } } } const entryScript = await fs.readFile( path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'), 'utf8', ); if (!/H5_PUBLIC_BASE_URL[^\n]*https:\/\/m\.tkmind\.cn/.test(entryScript)) { findings.push({ file: 'scripts/run-memind-portal-prod.sh', url: null, reason: 'missing_canonical_h5_public_base', }); } return findings; }