103 lines
3.1 KiB
JavaScript
103 lines
3.1 KiB
JavaScript
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;
|
|
}
|