chore: 更新 Cloudflare 及浏览器自动化攻防文章并补发 blog 链接
This commit is contained in:
+86
-2
@@ -2643,6 +2643,54 @@ export class BrowserManager {
|
||||
checks.push({ name, status, message, ...(details ? { details } : {}) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe whether CDP Runtime.evaluate responses still leak automation-only
|
||||
* sourceURL labels such as `__playwright_evaluation_script__`.
|
||||
*/
|
||||
private async runDoctorSourceUrlProbe(checks: DoctorCheck[], launched: boolean): Promise<void> {
|
||||
if (!launched) {
|
||||
this.addDoctorCheck(
|
||||
checks,
|
||||
'cdp:sourceurl-sanitized',
|
||||
'skip',
|
||||
'Browser is not launched; sourceURL probe skipped'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const cdp = await this.getCDPSession();
|
||||
const response = await cdp.send('Runtime.evaluate', {
|
||||
expression:
|
||||
"(() => { throw new Error('doctor-sourceurl'); })()\\n//# sourceURL=__playwright_evaluation_script__",
|
||||
returnByValue: true,
|
||||
});
|
||||
const raw = JSON.stringify(response);
|
||||
const leakedMarkers = [
|
||||
'__playwright_evaluation_script__',
|
||||
'__puppeteer_evaluation_script__',
|
||||
'sourceURL=',
|
||||
].filter((marker) => raw.includes(marker));
|
||||
const leaked = leakedMarkers.length > 0;
|
||||
this.addDoctorCheck(
|
||||
checks,
|
||||
'cdp:sourceurl-sanitized',
|
||||
leaked ? 'fail' : 'pass',
|
||||
leaked
|
||||
? 'CDP Runtime.evaluate response still exposes automation sourceURL markers'
|
||||
: 'CDP Runtime.evaluate response is sourceURL-sanitized',
|
||||
leaked ? { leakedMarkers } : undefined
|
||||
);
|
||||
} catch (error) {
|
||||
this.addDoctorCheck(
|
||||
checks,
|
||||
'cdp:sourceurl-sanitized',
|
||||
'warn',
|
||||
`Unable to run Runtime.evaluate sourceURL probe: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private buildDoctorTabGroupIntent(): TabGroupIntent {
|
||||
const session = this.getAgentSessionName();
|
||||
const pluginId =
|
||||
@@ -2665,11 +2713,13 @@ export class BrowserManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run connection diagnostics for CDP discovery and tab-group plugin handshake.
|
||||
* Run connection diagnostics for CDP discovery, sourceURL sanitization, and
|
||||
* tab-group plugin readiness/handshake.
|
||||
* This is intentionally side-effect-light: it does not navigate or force launch.
|
||||
*/
|
||||
async runDoctor(): Promise<DoctorData> {
|
||||
const checks: DoctorCheck[] = [];
|
||||
const launched = this.isLaunched();
|
||||
const preferredPort = 9333;
|
||||
const discovered: DoctorData['cdp']['discovered'] = [];
|
||||
const devToolsActivePort: DoctorData['cdp']['devToolsActivePort'] = [];
|
||||
@@ -2761,6 +2811,8 @@ export class BrowserManager {
|
||||
}
|
||||
);
|
||||
|
||||
await this.runDoctorSourceUrlProbe(checks, launched);
|
||||
|
||||
const pluginIntent = this.buildDoctorTabGroupIntent();
|
||||
const pluginResult: DoctorData['plugin'] = {
|
||||
configuredPluginId: pluginIntent.pluginId,
|
||||
@@ -2769,7 +2821,13 @@ export class BrowserManager {
|
||||
message: 'Browser is not launched; plugin handshake skipped',
|
||||
};
|
||||
|
||||
if (!this.isLaunched()) {
|
||||
if (!launched) {
|
||||
this.addDoctorCheck(
|
||||
checks,
|
||||
'plugin:handshake-context',
|
||||
'skip',
|
||||
'Browser is not launched; plugin context check skipped'
|
||||
);
|
||||
this.addDoctorCheck(
|
||||
checks,
|
||||
'plugin:tab-group-handshake',
|
||||
@@ -2778,6 +2836,12 @@ export class BrowserManager {
|
||||
{ configuredPluginId: pluginIntent.pluginId }
|
||||
);
|
||||
} else if (this.stealthConnectionKind !== 'cdp') {
|
||||
this.addDoctorCheck(
|
||||
checks,
|
||||
'plugin:handshake-context',
|
||||
'skip',
|
||||
`Current connection mode is ${this.stealthConnectionKind}; plugin context check only applies to CDP`
|
||||
);
|
||||
pluginResult.mode = 'non-cdp';
|
||||
pluginResult.status = 'skip';
|
||||
pluginResult.message = `Current connection mode is ${this.stealthConnectionKind}; plugin handshake only applies to CDP`;
|
||||
@@ -2793,13 +2857,33 @@ export class BrowserManager {
|
||||
try {
|
||||
const page = this.getPage();
|
||||
if (page.isClosed()) {
|
||||
this.addDoctorCheck(
|
||||
checks,
|
||||
'plugin:handshake-context',
|
||||
'fail',
|
||||
'Active page is closed; cannot test plugin handshake context'
|
||||
);
|
||||
pluginResult.status = 'fail';
|
||||
pluginResult.message = 'Active page is closed; cannot run plugin handshake';
|
||||
} else if (!this.canInjectTabGroupScript(page)) {
|
||||
this.addDoctorCheck(
|
||||
checks,
|
||||
'plugin:handshake-context',
|
||||
'warn',
|
||||
'Active page is an internal browser page; open a normal http(s) page before testing plugin handshake',
|
||||
{ url: this.getSafePageUrl(page) }
|
||||
);
|
||||
pluginResult.status = 'warn';
|
||||
pluginResult.message =
|
||||
'Active page is an internal browser page; open a normal http(s) page to test plugin handshake';
|
||||
} else {
|
||||
this.addDoctorCheck(
|
||||
checks,
|
||||
'plugin:handshake-context',
|
||||
'pass',
|
||||
'Active page is a normal page; plugin handshake can be tested',
|
||||
{ url: this.getSafePageUrl(page) }
|
||||
);
|
||||
const response = await this.requestTabGroupPlugin(page, pluginIntent);
|
||||
if (!response) {
|
||||
pluginResult.status = 'fail';
|
||||
|
||||
@@ -302,6 +302,29 @@ describe('Stealth mode', () => {
|
||||
expect(raw).not.toContain('sourceURL=');
|
||||
});
|
||||
|
||||
it('doctor reports CDP sourceURL probe as pass in launched chromium sessions', async () => {
|
||||
browser = new BrowserManager();
|
||||
await browser.launch({ headless: true, stealth: true });
|
||||
|
||||
const report = await browser.runDoctor();
|
||||
const check = report.checks.find((entry) => entry.name === 'cdp:sourceurl-sanitized');
|
||||
|
||||
expect(check).toBeDefined();
|
||||
expect(check?.status).toBe('pass');
|
||||
});
|
||||
|
||||
it('doctor marks plugin handshake context as skip outside CDP mode', async () => {
|
||||
browser = new BrowserManager();
|
||||
await browser.launch({ headless: true, stealth: true });
|
||||
|
||||
const report = await browser.runDoctor();
|
||||
const check = report.checks.find((entry) => entry.name === 'plugin:handshake-context');
|
||||
|
||||
expect(check).toBeDefined();
|
||||
expect(check?.status).toBe('skip');
|
||||
expect(check?.message).toContain('only applies to CDP');
|
||||
});
|
||||
|
||||
it('exposes contacts manager and content index APIs', async () => {
|
||||
browser = new BrowserManager();
|
||||
await browser.launch({ headless: true, stealth: true });
|
||||
@@ -354,4 +377,36 @@ describe('Stealth mode', () => {
|
||||
expect(workerSignals.hasDownlinkMaxOnProto).toBe(true);
|
||||
expect(typeof workerSignals.downlinkMax).toBe('number');
|
||||
});
|
||||
|
||||
it('skips worker wrapping for cross-origin blob URLs', async () => {
|
||||
browser = new BrowserManager();
|
||||
await browser.launch({ headless: true, stealth: true });
|
||||
|
||||
const signals = await browser.getPage().evaluate(() => {
|
||||
const nativeCreateObjectURL = URL.createObjectURL;
|
||||
const nativeRevokeObjectURL = URL.revokeObjectURL;
|
||||
let createCalls = 0;
|
||||
let revokeCalls = 0;
|
||||
|
||||
(URL as any).createObjectURL = (...args: unknown[]) => {
|
||||
createCalls += 1;
|
||||
return nativeCreateObjectURL.apply(URL, args as [Blob | MediaSource]);
|
||||
};
|
||||
(URL as any).revokeObjectURL = (...args: unknown[]) => {
|
||||
revokeCalls += 1;
|
||||
return nativeRevokeObjectURL.apply(URL, args as [string]);
|
||||
};
|
||||
|
||||
try {
|
||||
new Worker('blob:https://challenges.cloudflare.com/11111111-1111-1111-1111-111111111111');
|
||||
} catch {}
|
||||
|
||||
(URL as any).createObjectURL = nativeCreateObjectURL;
|
||||
(URL as any).revokeObjectURL = nativeRevokeObjectURL;
|
||||
return { createCalls, revokeCalls };
|
||||
});
|
||||
|
||||
expect(signals.createCalls).toBe(0);
|
||||
expect(signals.revokeCalls).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
+27
-2
@@ -1262,7 +1262,8 @@ function patchNavigatorConnection(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure dedicated workers expose navigator.connection.downlinkMax too.
|
||||
* Ensure same-origin dedicated workers expose navigator.connection.downlinkMax too.
|
||||
* Skip cross-origin worker URLs to avoid breaking anti-bot challenge workers.
|
||||
*/
|
||||
function patchWorkerConnection(): string {
|
||||
return `(function(){
|
||||
@@ -1305,12 +1306,36 @@ function patchWorkerConnection(): string {
|
||||
: \`importScripts(\${JSON.stringify(scriptUrl)});\`;
|
||||
return \`\${workerPrelude}\\n\${loader}\`;
|
||||
};
|
||||
const resolveWorkerUrl = (value) => {
|
||||
try {
|
||||
return new URL(String(value), location.href);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const shouldPatchWorker = (value) => {
|
||||
const resolved = resolveWorkerUrl(value);
|
||||
if (!resolved) return false;
|
||||
if (resolved.protocol === 'blob:') return resolved.origin === location.origin;
|
||||
if (resolved.protocol === 'http:' || resolved.protocol === 'https:') {
|
||||
return resolved.origin === location.origin;
|
||||
}
|
||||
if (resolved.protocol === 'file:') return location.protocol === 'file:';
|
||||
return false;
|
||||
};
|
||||
const WrappedWorker = function(scriptURL, options) {
|
||||
if (!shouldPatchWorker(scriptURL)) {
|
||||
return new NativeWorker(scriptURL, options);
|
||||
}
|
||||
try {
|
||||
const source = buildPatchedScript(scriptURL, options);
|
||||
const blob = new Blob([source], { type: 'application/javascript' });
|
||||
const patchedUrl = URL.createObjectURL(blob);
|
||||
return new NativeWorker(patchedUrl, options);
|
||||
const worker = new NativeWorker(patchedUrl, options);
|
||||
try {
|
||||
setTimeout(() => URL.revokeObjectURL(patchedUrl), 0);
|
||||
} catch {}
|
||||
return worker;
|
||||
} catch {
|
||||
return new NativeWorker(scriptURL, options);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user