chore: 更新 Cloudflare 及浏览器自动化攻防文章并补发 blog 链接
This commit is contained in:
Executable
+199
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* End-to-end stealth regression check across key anti-bot targets.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/check-stealth-regression.js
|
||||
* node scripts/check-stealth-regression.js --binary ./cli/target/release/agent-browser
|
||||
* node scripts/check-stealth-regression.js --session-name stealth-regression
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, '..');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const getArgValue = (name, fallback) => {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1 || index + 1 >= args.length) return fallback;
|
||||
return args[index + 1];
|
||||
};
|
||||
|
||||
const sessionName = getArgValue('--session-name', 'stealth-regression');
|
||||
const screenshotDir = getArgValue('--screenshot-dir', join('/tmp', 'agent-browser-stealth-regression'));
|
||||
const binaryArg = getArgValue('--binary', '');
|
||||
|
||||
const candidates = [
|
||||
binaryArg,
|
||||
join(rootDir, 'cli', 'target', 'release', 'agent-browser'),
|
||||
join(rootDir, 'bin', 'agent-browser.js'),
|
||||
'agent-browser-stealth',
|
||||
'agent-browser',
|
||||
].filter(Boolean);
|
||||
|
||||
function tryResolveBinary() {
|
||||
for (const candidate of candidates) {
|
||||
if (candidate.includes('/') && !existsSync(candidate)) continue;
|
||||
const probe = spawnSync(candidate, ['--version'], { encoding: 'utf8' });
|
||||
if (probe.status === 0) return candidate;
|
||||
}
|
||||
throw new Error(
|
||||
`Unable to find a runnable agent-browser binary. Tried: ${candidates.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
const binary = tryResolveBinary();
|
||||
const sessionArgs = ['--session', sessionName, '--session-name', sessionName];
|
||||
|
||||
function runBinary(commandArgs, { allowFailure = false } = {}) {
|
||||
const result = spawnSync(binary, commandArgs, {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
if (result.status !== 0 && !allowFailure) {
|
||||
const stderr = (result.stderr || '').trim();
|
||||
const stdout = (result.stdout || '').trim();
|
||||
throw new Error(
|
||||
`Command failed: ${binary} ${commandArgs.join(' ')}\n` +
|
||||
`${stderr || stdout || `exit code ${result.status}`}`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function runJson(actionArgs, options = {}) {
|
||||
const result = runBinary([...sessionArgs, '--json', ...actionArgs], options);
|
||||
const output = (result.stdout || '').trim();
|
||||
if (!output) return null;
|
||||
try {
|
||||
return JSON.parse(output);
|
||||
} catch {
|
||||
throw new Error(`Expected JSON output, got:\n${output}`);
|
||||
}
|
||||
}
|
||||
|
||||
const genericRiskScript = `(() => {
|
||||
const lowerTitle = String(document.title || '').toLowerCase();
|
||||
const lowerBody = String(document.body?.innerText || '').toLowerCase();
|
||||
const hasCloudflare =
|
||||
lowerTitle.includes('just a moment') ||
|
||||
lowerTitle.includes('performing security verification') ||
|
||||
lowerBody.includes('performing security verification') ||
|
||||
lowerBody.includes('checking your browser') ||
|
||||
lowerBody.includes('cloudflare');
|
||||
const hasCaptcha =
|
||||
lowerBody.includes('captcha') ||
|
||||
lowerBody.includes('recaptcha') ||
|
||||
lowerBody.includes('hcaptcha') ||
|
||||
lowerBody.includes('turnstile');
|
||||
return {
|
||||
title: document.title || '',
|
||||
url: location.href,
|
||||
hasCloudflare,
|
||||
hasCaptcha,
|
||||
hasTurnstile:
|
||||
!!document.querySelector('.cf-turnstile, iframe[src*="challenges.cloudflare.com"], [name="cf-turnstile-response"]'),
|
||||
bodySample: lowerBody.slice(0, 600),
|
||||
};
|
||||
})()`;
|
||||
|
||||
const sannysoftScript = `(() => {
|
||||
const normalize = (s) => String(s || '').replace(/\\s+/g, ' ').trim();
|
||||
const rows = Array.from(document.querySelectorAll('tr'));
|
||||
const failed = rows.filter((row) => /failed|fail/i.test(normalize(row.innerText)));
|
||||
return {
|
||||
failedCount: failed.length,
|
||||
failedRows: failed.map((row) => normalize(row.innerText)),
|
||||
navigatorWebdriver: navigator.webdriver,
|
||||
navigatorVendor: navigator.vendor,
|
||||
};
|
||||
})()`;
|
||||
|
||||
function sanitizeFileSegment(input) {
|
||||
return input.replace(/[^a-zA-Z0-9._-]+/g, '-');
|
||||
}
|
||||
|
||||
function main() {
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
const timestamp = new Date().toISOString();
|
||||
const targets = [
|
||||
'https://bot.sannysoft.com/',
|
||||
'https://chatgpt.com/',
|
||||
'https://super86.cc/login',
|
||||
];
|
||||
|
||||
runBinary([...sessionArgs, 'close'], { allowFailure: true });
|
||||
|
||||
const report = {
|
||||
binary,
|
||||
sessionName,
|
||||
timestamp,
|
||||
screenshotDir,
|
||||
doctor: null,
|
||||
targets: [],
|
||||
ok: true,
|
||||
};
|
||||
|
||||
try {
|
||||
const doctorResp = runJson(['doctor']);
|
||||
report.doctor = doctorResp?.data ?? null;
|
||||
|
||||
for (const target of targets) {
|
||||
const entry = {
|
||||
target,
|
||||
open: null,
|
||||
risk: null,
|
||||
sannysoft: null,
|
||||
screenshot: null,
|
||||
ok: true,
|
||||
error: null,
|
||||
};
|
||||
|
||||
try {
|
||||
const openResp = runJson(['open', target]);
|
||||
entry.open = openResp?.data ?? null;
|
||||
|
||||
runJson(['wait', '3000'], { allowFailure: true });
|
||||
|
||||
const riskResp = runJson(['eval', genericRiskScript]);
|
||||
entry.risk = riskResp?.data?.result ?? null;
|
||||
|
||||
if (target.includes('bot.sannysoft.com')) {
|
||||
const sannysoftResp = runJson(['eval', sannysoftScript]);
|
||||
entry.sannysoft = sannysoftResp?.data?.result ?? null;
|
||||
if ((entry.sannysoft?.failedCount ?? 1) > 0) {
|
||||
entry.ok = false;
|
||||
}
|
||||
} else if (entry.risk?.hasCloudflare || entry.risk?.hasCaptcha) {
|
||||
entry.ok = false;
|
||||
}
|
||||
|
||||
const host = sanitizeFileSegment(new URL(target).host);
|
||||
const shotPath = join(
|
||||
screenshotDir,
|
||||
`${host}-${Date.now().toString(36)}.png`
|
||||
);
|
||||
runJson(['screenshot', '--full', shotPath], { allowFailure: true });
|
||||
entry.screenshot = shotPath;
|
||||
} catch (error) {
|
||||
entry.ok = false;
|
||||
entry.error = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
if (!entry.ok) report.ok = false;
|
||||
report.targets.push(entry);
|
||||
}
|
||||
} finally {
|
||||
runBinary([...sessionArgs, 'close'], { allowFailure: true });
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
process.exit(report.ok ? 0 : 1);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user