chore: 更新 Cloudflare 及浏览器自动化攻防文章并补发 blog 链接

This commit is contained in:
leeguooooo
2026-03-04 09:39:38 +09:00
parent d32a1d046a
commit c07eb7ee52
16 changed files with 1324 additions and 248 deletions
+199
View File
@@ -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();
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env tsx
/**
* Deterministic Turnstile check using Cloudflare official testing sitekey.
*
* Usage:
* pnpm run check:turnstile-testkey
* pnpm run check:turnstile-testkey -- --headed
*/
import http from 'node:http';
import { BrowserManager } from '../src/browser.js';
const args = process.argv.slice(2);
const headed = args.includes('--headed');
const waitMsRaw = args.includes('--wait-ms')
? args[args.indexOf('--wait-ms') + 1]
: undefined;
const waitMs = Number.isFinite(Number(waitMsRaw)) ? Number(waitMsRaw) : 9000;
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>agent-browser turnstile testkey</title>
</head>
<body>
<h1>Turnstile Testkey Probe</h1>
<div class="cf-turnstile" data-sitekey="1x00000000000000000000AA" data-callback="onTurnstileToken"></div>
<script>
window.__turnstileToken = '';
function onTurnstileToken(token) {
window.__turnstileToken = token || '';
}
</script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</body>
</html>`;
function createServer(): Promise<http.Server> {
const server = http.createServer((_, res) => {
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store',
});
res.end(html);
});
return new Promise((resolve, reject) => {
server.on('error', reject);
server.listen(0, '127.0.0.1', () => resolve(server));
});
}
function closeServer(server: http.Server): Promise<void> {
return new Promise((resolve) => server.close(() => resolve()));
}
async function main(): Promise<void> {
const server = await createServer();
const address = server.address();
if (!address || typeof address === 'string') {
await closeServer(server);
throw new Error('Unable to start local HTTP server for Turnstile probe');
}
const localUrl = `http://127.0.0.1:${address.port}/`;
const browser = new BrowserManager();
try {
await browser.launch({
id: 'turnstile-testkey',
action: 'launch',
browser: 'chromium',
stealth: true,
headless: !headed,
});
const page = browser.getPage();
await page.goto(localUrl, { waitUntil: 'domcontentloaded', timeout: 45_000 });
await page.waitForTimeout(waitMs);
const result = await page.evaluate(() => {
const hidden = document.querySelector('input[name="cf-turnstile-response"]') as
| HTMLInputElement
| null;
const hiddenValue = hidden?.value || '';
const callbackValue =
typeof (window as any).__turnstileToken === 'string'
? (window as any).__turnstileToken
: '';
const token = hiddenValue || callbackValue || '';
return {
url: location.href,
title: document.title || '',
tokenLength: token.length,
tokenSample: token.slice(0, 40),
isDummyToken: token.includes('DUMMY'),
hiddenFieldLength: hiddenValue.length,
callbackLength: callbackValue.length,
widgetCount: document.querySelectorAll('.cf-turnstile').length,
};
});
const report = {
timestamp: new Date().toISOString(),
headed,
waitMs,
ok: result.isDummyToken,
result,
};
console.log(JSON.stringify(report, null, 2));
process.exit(result.isDummyToken ? 0 : 1);
} finally {
await browser.close().catch(() => {});
await closeServer(server);
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});