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();
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user