@xbrowser/cli 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{anti-bot-DR56Y63V.js → anti-bot-GTTYNEFB.js} +1 -1
- package/dist/{browser-WQZ3D6AE.js → browser-3HQKDZQ6.js} +4 -2
- package/dist/{browser-T3V3JWVH.js → browser-ILXTEWH4.js} +1 -1
- package/dist/{browser-HBL72GPZ.js → browser-XPDMY2NX.js} +3 -3
- package/dist/{cdp-driver-J3YQ4LJV.js → cdp-driver-ESH3PDE6.js} +2 -1
- package/dist/cdp-driver-N2E2ZNSC.js +4644 -0
- package/dist/cdp-driver-YIKR4BUX.js +49 -0
- package/dist/chunk-3FWLW7FS.js +106 -0
- package/dist/chunk-A6LPGFAL.js +437 -0
- package/dist/{chunk-RRBXV7KE.js → chunk-DKM2JOZL.js} +633 -63
- package/dist/{chunk-ABXMBNQ6.js → chunk-H2A5JUK5.js} +37 -466
- package/dist/{chunk-5UGR6MUK.js → chunk-HBHOKZPN.js} +51 -16
- package/dist/{chunk-BNO7OKO4.js → chunk-HBMEFSTB.js} +39 -0
- package/dist/{chunk-INTQPBYF.js → chunk-JKVUFP3G.js} +6 -2
- package/dist/chunk-KJTABK3Z.js +1255 -0
- package/dist/{chunk-DWDXEGVK.js → chunk-NKW4A74J.js} +9 -3
- package/dist/{chunk-4452SPFI.js → chunk-NODRQGOK.js} +51 -16
- package/dist/{chunk-SQHSZENE.js → chunk-OMU63E6J.js} +3 -22
- package/dist/{chunk-NITFVWWS.js → chunk-OVW2UEWN.js} +981 -407
- package/dist/{chunk-WSCP7QCJ.js → chunk-R6IJ6PIV.js} +14 -11
- package/dist/{chunk-YMUSHPU4.js → chunk-SH6OTPXN.js} +46 -16
- package/dist/{chunk-MWFHZUIY.js → chunk-TEXCXIBW.js} +1 -1
- package/dist/{cdp-driver-2T4P4ZE2.js → chunk-Z6CUR3VG.js} +1542 -134
- package/dist/{chunk-IIM5GOD7.js → chunk-ZTHE5RBZ.js} +7 -1
- package/dist/cli.js +1136 -584
- package/dist/{daemon-client-GKEPT4NY.js → daemon-client-7D6EZNOE.js} +46 -16
- package/dist/{daemon-client-O6BYVRXV.js → daemon-client-HEGAXWGK.js} +1 -1
- package/dist/daemon-main.js +1031 -522
- package/dist/{human-interaction-5AO42MBA.js → human-interaction-4YR2N6R6.js} +2 -2
- package/dist/{human-interaction-C5OEGXGO.js → human-interaction-Y5IDH6LD.js} +1 -1
- package/dist/{human-interaction-ISXZTAYY.js → human-interaction-ZUTR5AC2.js} +2 -2
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1147 -593
- package/dist/{proxy-C6CK3UH5.js → proxy-LUR4U5YF.js} +2 -1
- package/dist/{recovery-J2ISVGUL.js → recovery-FTZGV6VY.js} +2 -2
- package/dist/{recovery-ZJVVHP7N.js → recovery-L5GLDX4O.js} +1 -1
- package/dist/{recovery-EF33LKRJ.js → recovery-Z3RDZENA.js} +2 -2
- package/dist/{session-recorder-H3KEYU26.js → session-recorder-3BEVWHOK.js} +1 -1
- package/dist/{session-recorder-QRZMKFVL.js → session-recorder-SLDBENVF.js} +1 -1
- package/dist/{session-replayer-LJUC4TI7.js → session-replayer-K4A6OI4M.js} +90 -2
- package/package.json +1 -1
- package/dist/chunk-BAHSRZIX.js +0 -2191
|
@@ -24,6 +24,285 @@ import { EventEmitter as EventEmitter2 } from "events";
|
|
|
24
24
|
// src/cdp-driver/page.ts
|
|
25
25
|
import { EventEmitter } from "events";
|
|
26
26
|
|
|
27
|
+
// src/cdp-driver/stealth.ts
|
|
28
|
+
var DEFAULT_STEALTH_CONFIG = {
|
|
29
|
+
bezierCurvature: [0.35, 0.6],
|
|
30
|
+
noiseAmplitude: 5.5,
|
|
31
|
+
overshootRange: [6, 14],
|
|
32
|
+
aimPause: [150, 400],
|
|
33
|
+
pressDuration: [60, 140],
|
|
34
|
+
releaseDrift: [0.8, 2.5],
|
|
35
|
+
landingOffsetSmall: [0.3, 2.5],
|
|
36
|
+
landingOffsetLarge: [1.5, 7],
|
|
37
|
+
smallElementThreshold: 30,
|
|
38
|
+
typingRhythm: {
|
|
39
|
+
fastProb: 0.22,
|
|
40
|
+
fastRange: [25, 60],
|
|
41
|
+
normalRange: [50, 350],
|
|
42
|
+
pauseProb: 0.18,
|
|
43
|
+
pauseRange: [400, 1200]
|
|
44
|
+
},
|
|
45
|
+
keyPressDuration: [50, 110],
|
|
46
|
+
typoProbability: 0.06,
|
|
47
|
+
wheelPeak: 180,
|
|
48
|
+
wheelDecayRate: 0.4
|
|
49
|
+
};
|
|
50
|
+
function rand(min, max) {
|
|
51
|
+
return min + Math.random() * (max - min);
|
|
52
|
+
}
|
|
53
|
+
function cosineEase(t) {
|
|
54
|
+
return 0.5 - 0.5 * Math.cos(Math.PI * t);
|
|
55
|
+
}
|
|
56
|
+
function bezierTrajectory(x0, y0, x1, y1, config = DEFAULT_STEALTH_CONFIG) {
|
|
57
|
+
const dist = Math.hypot(x1 - x0, y1 - y0);
|
|
58
|
+
const n = Math.max(10, Math.min(28, Math.round(dist / 15)));
|
|
59
|
+
const shortMove = dist < 120;
|
|
60
|
+
const curvature = shortMove ? rand(2, 6) : Math.max(dist * rand(...config.bezierCurvature), rand(18, 35));
|
|
61
|
+
const dir = Math.random() < 0.5 ? 1 : -1;
|
|
62
|
+
const d = dist || 1;
|
|
63
|
+
const dx = x1 - x0, dy = y1 - y0;
|
|
64
|
+
const c1x = x0 + dx * 0.3 - dy / d * curvature * 0.5 * dir;
|
|
65
|
+
const c1y = y0 + dy * 0.3 + dx / d * curvature * 0.5 * dir;
|
|
66
|
+
const c2x = x0 + dx * 0.7 - dy / d * curvature * 0.8 * dir;
|
|
67
|
+
const c2y = y0 + dy * 0.7 + dx / d * curvature * 0.8 * dir;
|
|
68
|
+
const points = [];
|
|
69
|
+
for (let i = 1; i <= n; i++) {
|
|
70
|
+
const t = cosineEase(i / n);
|
|
71
|
+
const mt = 1 - t;
|
|
72
|
+
let px = mt ** 3 * x0 + 3 * mt ** 2 * t * c1x + 3 * mt * t ** 2 * c2x + t ** 3 * x1;
|
|
73
|
+
let py = mt ** 3 * y0 + 3 * mt ** 2 * t * c1y + 3 * mt * t ** 2 * c2y + t ** 3 * y1;
|
|
74
|
+
const amp = shortMove ? Math.min(2, config.noiseAmplitude) : config.noiseAmplitude;
|
|
75
|
+
px += rand(-amp, amp);
|
|
76
|
+
py += rand(-amp, amp);
|
|
77
|
+
points.push({ x: px, y: py, delay: rand(9, 16) });
|
|
78
|
+
}
|
|
79
|
+
if (!shortMove) {
|
|
80
|
+
const over = rand(...config.overshootRange);
|
|
81
|
+
const ox = x1 + dx / d * over + rand(-2, 2);
|
|
82
|
+
const oy = y1 + dy / d * over + rand(-2, 2);
|
|
83
|
+
points.push({ x: ox, y: oy, delay: rand(14, 30) });
|
|
84
|
+
points.push({
|
|
85
|
+
x: x1 + dx / d * over * 0.4,
|
|
86
|
+
y: y1 + dy / d * over * 0.4,
|
|
87
|
+
delay: rand(14, 30)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
points.push({ x: x1 + rand(-1, 1), y: y1 + rand(-1, 1), delay: rand(14, 30) });
|
|
91
|
+
return points;
|
|
92
|
+
}
|
|
93
|
+
function landingOffset(width, height, config = DEFAULT_STEALTH_CONFIG) {
|
|
94
|
+
const isSmall = Math.min(width, height) < config.smallElementThreshold;
|
|
95
|
+
const range = isSmall ? config.landingOffsetSmall : config.landingOffsetLarge;
|
|
96
|
+
const dx = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
|
|
97
|
+
const dy = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
|
|
98
|
+
return { dx, dy };
|
|
99
|
+
}
|
|
100
|
+
var KEY_MAP = {};
|
|
101
|
+
for (let i = 97; i <= 122; i++) {
|
|
102
|
+
const ch = String.fromCharCode(i);
|
|
103
|
+
KEY_MAP[ch] = { key: ch, code: "Key" + ch.toUpperCase(), vk: i - 32 };
|
|
104
|
+
}
|
|
105
|
+
for (let i = 65; i <= 90; i++) {
|
|
106
|
+
const ch = String.fromCharCode(i);
|
|
107
|
+
KEY_MAP[ch] = { key: ch, code: "Key" + ch, vk: i, shift: true };
|
|
108
|
+
}
|
|
109
|
+
for (let i = 48; i <= 57; i++) {
|
|
110
|
+
const ch = String.fromCharCode(i);
|
|
111
|
+
KEY_MAP[ch] = { key: ch, code: "Digit" + ch, vk: i };
|
|
112
|
+
}
|
|
113
|
+
Object.assign(KEY_MAP, {
|
|
114
|
+
" ": { key: " ", code: "Space", vk: 32 },
|
|
115
|
+
".": { key: ".", code: "Period", vk: 190 },
|
|
116
|
+
"-": { key: "-", code: "Minus", vk: 189 },
|
|
117
|
+
"@": { key: "@", code: "Digit2", vk: 50, shift: true },
|
|
118
|
+
"_": { key: "_", code: "Minus", vk: 189, shift: true }
|
|
119
|
+
});
|
|
120
|
+
function buildStealthInitScript() {
|
|
121
|
+
return [
|
|
122
|
+
"(function(){",
|
|
123
|
+
// 1. AEL event proxy
|
|
124
|
+
" var o=EventTarget.prototype.addEventListener;",
|
|
125
|
+
" var fc=new InputDeviceCapabilities({firesTouchEvents:false});",
|
|
126
|
+
" var _ael=function(t,f){",
|
|
127
|
+
" var op=arguments[2];",
|
|
128
|
+
' if(typeof f!=="function")return o.call(this,t,f,op);',
|
|
129
|
+
" var w=function(e){",
|
|
130
|
+
" if(!e||e.constructor===FocusEvent||e.constructor===KeyboardEvent)return f.call(this,e);",
|
|
131
|
+
" return f.call(this,new Proxy(e,{get:function(k,p){",
|
|
132
|
+
' if(p==="sourceCapabilities")return fc;',
|
|
133
|
+
' if(p==="isTrusted")return k.isTrusted;',
|
|
134
|
+
' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isInteger(k[p])&&k.isTrusted===true){',
|
|
135
|
+
" var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;",
|
|
136
|
+
" return k[p]+_f;",
|
|
137
|
+
" }",
|
|
138
|
+
' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
|
|
139
|
+
" }}));",
|
|
140
|
+
" };",
|
|
141
|
+
" return o.call(this,t,w,op);",
|
|
142
|
+
" };",
|
|
143
|
+
" EventTarget.prototype.addEventListener=_ael;",
|
|
144
|
+
// 2. Screen override (prototype-level, not instance-level)
|
|
145
|
+
" var _gw=function(){return 1728};",
|
|
146
|
+
" var _gh=function(){return 1117};",
|
|
147
|
+
" var _gah=function(){return 1092};",
|
|
148
|
+
' Object.defineProperty(Screen.prototype,"width",{get:_gw,configurable:true});',
|
|
149
|
+
' Object.defineProperty(Screen.prototype,"height",{get:_gh,configurable:true});',
|
|
150
|
+
' Object.defineProperty(Screen.prototype,"availWidth",{get:_gw,configurable:true});',
|
|
151
|
+
' Object.defineProperty(Screen.prototype,"availHeight",{get:_gah,configurable:true});',
|
|
152
|
+
" document.hasFocus=function(){return true};",
|
|
153
|
+
// 4. Canvas/WebGL fingerprint: per-session stable noise (d20).
|
|
154
|
+
// Headless software raster differs subtly from Chrome GPU raster —
|
|
155
|
+
// toDataURL hashes fingerprint the rasterizer. Inject a stable
|
|
156
|
+
// (per-page-load) subpixel shift into fillText so hashes look like a
|
|
157
|
+
// distinct-but-consistent real GPU, and hide the HEADLESS tell in
|
|
158
|
+
// WebGL renderer strings.
|
|
159
|
+
" var _seed=Math.floor(Math.random()*2147483647);",
|
|
160
|
+
" var _prng=function(){_seed=(_seed*48271)%2147483647;return _seed/2147483647;};",
|
|
161
|
+
" var _dx=(_prng()*0.4-0.2).toFixed(3)*1, _dy=(_prng()*0.4-0.2).toFixed(3)*1;",
|
|
162
|
+
" var _fillText=CanvasRenderingContext2D.prototype.fillText;",
|
|
163
|
+
" CanvasRenderingContext2D.prototype.fillText=function(t,x,y,m){",
|
|
164
|
+
" return _fillText.call(this,t,x+_dx,y+_dy,m);",
|
|
165
|
+
" };",
|
|
166
|
+
" var _toDataURL=HTMLCanvasElement.prototype.toDataURL;",
|
|
167
|
+
" HTMLCanvasElement.prototype.toDataURL=function(){",
|
|
168
|
+
' var ctx=this.getContext("2d");',
|
|
169
|
+
" if(ctx){var d=ctx.getImageData(0,0,Math.min(this.width,2),Math.min(this.height,2));",
|
|
170
|
+
" for(var i=0;i<d.data.length;i+=4){if(d.data[i+3]>0){d.data[i]^=1;break;}}",
|
|
171
|
+
" ctx.putImageData(d,0,0);}",
|
|
172
|
+
" return _toDataURL.apply(this,arguments);",
|
|
173
|
+
" };",
|
|
174
|
+
" try{",
|
|
175
|
+
" var _gl=HTMLCanvasElement.prototype.getContext;",
|
|
176
|
+
" HTMLCanvasElement.prototype.getContext=function(t,o){",
|
|
177
|
+
" var c=_gl.call(this,t,o);",
|
|
178
|
+
' if(c&&(t==="webgl"||t==="experimental-webgl")&&c.getParameter){',
|
|
179
|
+
" var _gp=c.getParameter.bind(c);",
|
|
180
|
+
" c.getParameter=function(p){",
|
|
181
|
+
" var v=_gp(p);",
|
|
182
|
+
' if(typeof v==="string"&&/SwiftShader|Software|Rasterizer|Headless/i.test(v))',
|
|
183
|
+
' return "ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Max, Unspecified Version)";',
|
|
184
|
+
" return v;};}",
|
|
185
|
+
" return c;};",
|
|
186
|
+
" }catch(e){}",
|
|
187
|
+
// 5. AudioContext fingerprint: per-load stable micro-noise on channel
|
|
188
|
+
// data (d21). DSP sum differences between headless software audio and
|
|
189
|
+
// real hardware audio are a classic fingerprint — add ±1e-7 level
|
|
190
|
+
// noise (inaudible, changes the sum hash).
|
|
191
|
+
" try{",
|
|
192
|
+
" var _gcd=AudioBuffer.prototype.getChannelData;",
|
|
193
|
+
" AudioBuffer.prototype.getChannelData=function(ch){",
|
|
194
|
+
" var d=_gcd.call(this,ch);",
|
|
195
|
+
' var key="__xb_audio_"+ch;',
|
|
196
|
+
" if(!this[key]){",
|
|
197
|
+
" this[key]=true;",
|
|
198
|
+
" for(var i=0;i<d.length;i+=997){d[i]=d[i]+(_prng()-0.5)*2e-7;}",
|
|
199
|
+
" }",
|
|
200
|
+
" return d;",
|
|
201
|
+
" };",
|
|
202
|
+
" var _gffd=AnalyserNode.prototype.getFloatFrequencyData;",
|
|
203
|
+
" AnalyserNode.prototype.getFloatFrequencyData=function(arr){",
|
|
204
|
+
" _gffd.call(this,arr);",
|
|
205
|
+
" for(var i=0;i<arr.length;i+=31){arr[i]=arr[i]+(_prng()-0.5)*0.01;}",
|
|
206
|
+
" };",
|
|
207
|
+
" var _gfbd=AnalyserNode.prototype.getByteFrequencyData;",
|
|
208
|
+
" AnalyserNode.prototype.getByteFrequencyData=function(arr){",
|
|
209
|
+
" _gfbd.call(this,arr);",
|
|
210
|
+
" for(var i=0;i<arr.length;i+=31){arr[i]=(arr[i]+((_prng()*3)|0))&255;}",
|
|
211
|
+
" };",
|
|
212
|
+
" }catch(e){}",
|
|
213
|
+
// 3b. Font metrics + speechSynthesis + battery (d22): headless tells
|
|
214
|
+
" try{",
|
|
215
|
+
" var _mt=CanvasRenderingContext2D.prototype.measureText;",
|
|
216
|
+
' var _tmw=Object.getOwnPropertyDescriptor(TextMetrics.prototype,"width");',
|
|
217
|
+
" CanvasRenderingContext2D.prototype.measureText=function(t){",
|
|
218
|
+
" var m=_mt.call(this,t);",
|
|
219
|
+
' try{Object.defineProperty(m,"width",{get:function(){return _tmw.get.call(m)+_dx*0.01;},configurable:true});}catch(e){}',
|
|
220
|
+
" return m;",
|
|
221
|
+
" };",
|
|
222
|
+
" }catch(e){}",
|
|
223
|
+
" try{",
|
|
224
|
+
' var _fakeVoices=["Alex","Daniel","Karen","Moira","Ralph","Samantha","Ting-Ting","Mei-Jia","Sinji","Yunda"];',
|
|
225
|
+
" var _sv=speechSynthesis.getVoices.bind(speechSynthesis);",
|
|
226
|
+
" speechSynthesis.getVoices=function(){",
|
|
227
|
+
" var real=_sv();",
|
|
228
|
+
" if(real&&real.length)return real;",
|
|
229
|
+
' return _fakeVoices.map(function(n,i){return {name:n,lang:i<2?"en-US":i<6?"en-GB":"zh-TW",localService:true,default:i===0,voiceURI:n};});',
|
|
230
|
+
" };",
|
|
231
|
+
" }catch(e){}",
|
|
232
|
+
" try{",
|
|
233
|
+
" var _gb=navigator.getBattery.bind(navigator);",
|
|
234
|
+
" navigator.getBattery=function(){",
|
|
235
|
+
" return _gb().then(function(b){",
|
|
236
|
+
' Object.defineProperty(b,"charging",{get:function(){return true;},configurable:true});',
|
|
237
|
+
" return b;});",
|
|
238
|
+
" };",
|
|
239
|
+
" }catch(e){}",
|
|
240
|
+
// 3c. WebRTC local IP leak guard (d23): headless STUN candidates can
|
|
241
|
+
// expose host IPs; mDNS-only candidates are the modern Chrome default.
|
|
242
|
+
" try{",
|
|
243
|
+
" var _oc=RTCPeerConnection.prototype.createOffer;",
|
|
244
|
+
" RTCPeerConnection.prototype.createOffer=function(){",
|
|
245
|
+
" var p=_oc.apply(this,arguments);",
|
|
246
|
+
" var self=this;",
|
|
247
|
+
" return p.then(function(offer){",
|
|
248
|
+
' offer.sdp=offer.sdp.split(String.fromCharCode(10)).filter(function(l){return l.indexOf("typ host")<0}).join(String.fromCharCode(10));',
|
|
249
|
+
" return offer;});",
|
|
250
|
+
" };",
|
|
251
|
+
" }catch(e){}",
|
|
252
|
+
// 3d. Chrome object depth (d24): automation fakes usually only set
|
|
253
|
+
// window.chrome = {}; deep checks hit app.run/runtime/csi/loadTimes.
|
|
254
|
+
" try{",
|
|
255
|
+
" if(window.chrome){",
|
|
256
|
+
' if(!window.chrome.app||!window.chrome.app.run)window.chrome.app={run:function(){},load:function(){},getDetails:function(){return null},InstallState:{DISABLED:"disabled",INSTALLED:"installed",NOT_INSTALLED:"not_installed"},RunningState:{CANNOT_RUN:"cannot_run",READY_TO_RUN:"ready_to_run",RUNNING:"running"}};',
|
|
257
|
+
' if(!window.chrome.runtime)window.chrome.runtime={OnInstalledReason:{CHROME_UPDATE:"chrome_update",INSTALL:"install",UPDATE:"update"},PlatformOs:{ANDROID:"android",CROS:"cros",LINUX:"linux",MAC:"mac",OPENBSD:"openbsd",WIN:"win"},connect:function(){},sendMessage:function(){},id:undefined};',
|
|
258
|
+
" if(!window.chrome.csi)window.chrome.csi=function(){return{};}",
|
|
259
|
+
" if(!window.chrome.loadTimes)window.chrome.loadTimes=function(){return{};}",
|
|
260
|
+
" }",
|
|
261
|
+
" }catch(e){}",
|
|
262
|
+
// 3e. Font availability patch (d25): headless Chromium misses some system
|
|
263
|
+
// fonts (e.g. Menlo on macOS) that real Chrome has. Register a FontFace
|
|
264
|
+
// aliasing the missing font to a local() equivalent so offsetWidth-based
|
|
265
|
+
// font probing sees the same availability as the faked environment.
|
|
266
|
+
" try{",
|
|
267
|
+
' var _fontAliases=[["Menlo","Courier New"],["SF Mono","Menlo"],["Segoe UI","Helvetica"]];',
|
|
268
|
+
" _fontAliases.forEach(function(pa){",
|
|
269
|
+
' try{var ff=new FontFace(pa[0],"local("+JSON.stringify(pa[1])+")");',
|
|
270
|
+
" document.fonts.add(ff);ff.load();}catch(e2){}",
|
|
271
|
+
" });",
|
|
272
|
+
" }catch(e){}",
|
|
273
|
+
// 3. toString disguise (name-list based)
|
|
274
|
+
" var _ts=Function.prototype.toString;",
|
|
275
|
+
" var _hf=document.hasFocus;",
|
|
276
|
+
" Function.prototype.toString=function(){",
|
|
277
|
+
' if(this===_ael)return"function addEventListener(type, callback) { [native code] }";',
|
|
278
|
+
' if(this===_hf)return"function hasFocus() { [native code] }";',
|
|
279
|
+
' if(this===_gw)return"function get width() { [native code] }";',
|
|
280
|
+
' if(this===_gh)return"function get height() { [native code] }";',
|
|
281
|
+
' if(this===_gah)return"function get availHeight() { [native code] }";',
|
|
282
|
+
' if(this===CanvasRenderingContext2D.prototype.fillText)return"function fillText() { [native code] }";',
|
|
283
|
+
' if(this===HTMLCanvasElement.prototype.toDataURL)return"function toDataURL() { [native code] }";',
|
|
284
|
+
' if(this===AnalyserNode.prototype.getFloatFrequencyData)return"function getFloatFrequencyData() { [native code] }";',
|
|
285
|
+
' if(this===AudioBuffer.prototype.getChannelData)return"function getChannelData() { [native code] }";',
|
|
286
|
+
" return _ts.call(this);",
|
|
287
|
+
" };",
|
|
288
|
+
// 4. onclick prototype hijack (dual-stream consistency)
|
|
289
|
+
" var _ba=function(k,p){",
|
|
290
|
+
' if(p==="isTrusted")return k.isTrusted;',
|
|
291
|
+
' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isInteger(k[p])&&k.isTrusted===true){',
|
|
292
|
+
" var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;return k[p]+_f;",
|
|
293
|
+
" }",
|
|
294
|
+
' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
|
|
295
|
+
" };",
|
|
296
|
+
' Object.defineProperty(Document.prototype,"onclick",{',
|
|
297
|
+
" configurable:true,",
|
|
298
|
+
" get:function(){var raw=this.__ocRaw||null;if(!raw)return null;var self=this;",
|
|
299
|
+
" return function(e){return raw.call(self,new Proxy(e,{get:function(k,p){return _ba(k,p)}}))}},",
|
|
300
|
+
" set:function(fn){this.__ocRaw=fn}",
|
|
301
|
+
" });",
|
|
302
|
+
"})()"
|
|
303
|
+
].join("\n");
|
|
304
|
+
}
|
|
305
|
+
|
|
27
306
|
// src/cdp-driver/mouse.ts
|
|
28
307
|
var XBMouseImpl = class {
|
|
29
308
|
conn;
|
|
@@ -45,18 +324,46 @@ var XBMouseImpl = class {
|
|
|
45
324
|
}
|
|
46
325
|
async click(x, y, opts = {}) {
|
|
47
326
|
const button = opts.button ?? "left";
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
327
|
+
const stealth = opts.stealth ?? process.env.XBROWSER_STEALTH !== "off";
|
|
328
|
+
let tx = x, ty = y;
|
|
329
|
+
if (stealth && opts.elementWidth !== void 0 && opts.elementHeight !== void 0) {
|
|
330
|
+
const off = landingOffset(opts.elementWidth, opts.elementHeight);
|
|
331
|
+
tx += off.dx;
|
|
332
|
+
ty += off.dy;
|
|
333
|
+
}
|
|
334
|
+
if (stealth) {
|
|
335
|
+
const traj = bezierTrajectory(this._x, this._y, tx, ty);
|
|
336
|
+
const _tb = Date.now();
|
|
337
|
+
let _truncated = false;
|
|
338
|
+
for (const p of traj) {
|
|
339
|
+
if (Date.now() - _tb > 5e3) {
|
|
340
|
+
_truncated = true;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: p.x, y: p.y, button: this._button });
|
|
344
|
+
this._x = p.x;
|
|
345
|
+
this._y = p.y;
|
|
346
|
+
await sleep(p.delay);
|
|
347
|
+
}
|
|
348
|
+
this._x = tx;
|
|
349
|
+
this._y = ty;
|
|
350
|
+
if (_truncated) {
|
|
351
|
+
await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: tx, y: ty, button: this._button });
|
|
352
|
+
}
|
|
353
|
+
await sleep(rand(...DEFAULT_STEALTH_CONFIG.aimPause));
|
|
354
|
+
} else {
|
|
355
|
+
await this.move(tx, ty);
|
|
54
356
|
}
|
|
55
|
-
await this.
|
|
56
|
-
|
|
57
|
-
|
|
357
|
+
await this.down({ button });
|
|
358
|
+
await sleep(stealth ? rand(...DEFAULT_STEALTH_CONFIG.pressDuration) : opts.delay ?? 0);
|
|
359
|
+
const rx = stealth ? this._x + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._x;
|
|
360
|
+
const ry = stealth ? this._y + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._y;
|
|
361
|
+
this._x = rx;
|
|
362
|
+
this._y = ry;
|
|
363
|
+
await this.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: rx, y: ry, button, clickCount: opts.clickCount ?? 1 });
|
|
364
|
+
for (let i = 1; i < (opts.clickCount ?? 1); i++) {
|
|
365
|
+
if (opts.delay) await sleep(opts.delay);
|
|
58
366
|
await this.down({ button });
|
|
59
|
-
if (delay > 0) await sleep(delay);
|
|
60
367
|
await this.up({ button });
|
|
61
368
|
}
|
|
62
369
|
}
|
|
@@ -147,12 +454,20 @@ var XBKeyboardImpl = class {
|
|
|
147
454
|
}
|
|
148
455
|
await this.dispatchKeyEvent(downParams);
|
|
149
456
|
if (mapping.text) {
|
|
457
|
+
if (process.env.XBROWSER_STEALTH !== "off") {
|
|
458
|
+
await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
|
|
459
|
+
}
|
|
150
460
|
await this.dispatchKeyEvent({
|
|
151
461
|
type: "char",
|
|
152
462
|
text: mapping.text
|
|
153
463
|
});
|
|
154
464
|
}
|
|
155
465
|
if (delay > 0) await sleep2(delay);
|
|
466
|
+
else {
|
|
467
|
+
if (process.env.XBROWSER_STEALTH !== "off") {
|
|
468
|
+
await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
156
471
|
const upParams = {
|
|
157
472
|
type: "keyUp",
|
|
158
473
|
key: mapping.key,
|
|
@@ -235,7 +550,7 @@ var XBKeyboardImpl = class {
|
|
|
235
550
|
}
|
|
236
551
|
};
|
|
237
552
|
function resolveKeyMapping(key) {
|
|
238
|
-
if (
|
|
553
|
+
if (KEY_MAP2[key]) return KEY_MAP2[key];
|
|
239
554
|
if (key.length === 1) {
|
|
240
555
|
const lower = key.toLowerCase();
|
|
241
556
|
if (lower >= "a" && lower <= "z") {
|
|
@@ -252,7 +567,7 @@ function resolveKeyMapping(key) {
|
|
|
252
567
|
}
|
|
253
568
|
return { key, code: key };
|
|
254
569
|
}
|
|
255
|
-
var
|
|
570
|
+
var KEY_MAP2 = {
|
|
256
571
|
Enter: { key: "Enter", code: "Enter", text: "\r", keyCode: 13 },
|
|
257
572
|
Tab: { key: "Tab", code: "Tab", text: " ", keyCode: 9 },
|
|
258
573
|
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
@@ -290,6 +605,37 @@ function sleep2(ms) {
|
|
|
290
605
|
|
|
291
606
|
// src/cdp-driver/selector-utils.ts
|
|
292
607
|
function queryJS(selector) {
|
|
608
|
+
return `(${deepQueryIIFE})( ${JSON.stringify(queryMainJS(selector))} )`;
|
|
609
|
+
}
|
|
610
|
+
var deepQueryIIFE = `(function(mainExpr) {
|
|
611
|
+
const run = (root) => {
|
|
612
|
+
try { return new Function('document', 'return (' + mainExpr + ')')(root); }
|
|
613
|
+
catch (e) { return null; }
|
|
614
|
+
};
|
|
615
|
+
const scanRoot = (root) => {
|
|
616
|
+
const direct = run(root);
|
|
617
|
+
if (direct) return direct;
|
|
618
|
+
let all;
|
|
619
|
+
try { all = root.querySelectorAll('*'); } catch (e) { return null; }
|
|
620
|
+
for (const el of all) {
|
|
621
|
+
if (el.shadowRoot) {
|
|
622
|
+
const r = scanRoot(el.shadowRoot);
|
|
623
|
+
if (r) return r;
|
|
624
|
+
}
|
|
625
|
+
if (el.tagName === 'IFRAME') {
|
|
626
|
+
let inner = null;
|
|
627
|
+
try { inner = el.contentDocument; } catch (e) { /* cross-origin */ }
|
|
628
|
+
if (inner) {
|
|
629
|
+
const r = scanRoot(inner);
|
|
630
|
+
if (r) return r;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return null;
|
|
635
|
+
};
|
|
636
|
+
return scanRoot(document);
|
|
637
|
+
})`;
|
|
638
|
+
function queryMainJS(selector) {
|
|
293
639
|
if (selector.startsWith("xpath=")) {
|
|
294
640
|
const xpath = JSON.stringify(selector.slice(6));
|
|
295
641
|
return `document.evaluate(${xpath}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue`;
|
|
@@ -301,13 +647,35 @@ function queryJS(selector) {
|
|
|
301
647
|
return `(() => {
|
|
302
648
|
const target = ${JSON.stringify(text)};
|
|
303
649
|
const exact = ${exact};
|
|
650
|
+
// Match on OWN text nodes (not strict leaf elements): search-result
|
|
651
|
+
// titles mix text with inline highlight <em> marks \u2014 a strict leaf filter
|
|
652
|
+
// finds nothing there (real-world juejin). Own-text keeps the match
|
|
653
|
+
// precise (descendant-only text doesn't count) while tolerating markup.
|
|
654
|
+
const ownText = (e) => Array.prototype.filter.call(e.childNodes, (n) => n.nodeType === 3)
|
|
655
|
+
.map((n) => n.textContent).join('').trim();
|
|
304
656
|
const els = [...document.querySelectorAll('*')].filter(e => {
|
|
305
|
-
if (e.
|
|
306
|
-
|
|
307
|
-
const t = (e.textContent || '').trim();
|
|
657
|
+
if (e.offsetParent === null && e.tagName !== 'BODY') return false;
|
|
658
|
+
const t = ownText(e);
|
|
308
659
|
if (!t) return false;
|
|
309
660
|
return exact ? t === target : t.toLowerCase().includes(target.toLowerCase());
|
|
310
661
|
});
|
|
662
|
+
// Rank instead of raw DOM order: exact text beats substring, interactive
|
|
663
|
+
// elements (button/a/[onclick]/inputs) beat prose. Prevents matching a
|
|
664
|
+
// description paragraph that merely MENTIONS the target label
|
|
665
|
+
// (rec-duel d06: header text "\u76EE\u6807\u9879\u300C\u7B2C 87 \u53F7\u300D" hijacked text=\u7B2C 87 \u53F7).
|
|
666
|
+
const isInteractive = (e) => {
|
|
667
|
+
const tag = e.tagName;
|
|
668
|
+
return tag === 'BUTTON' || tag === 'A' || tag === 'INPUT' || tag === 'SELECT'
|
|
669
|
+
|| e.hasAttribute('onclick') || e.getAttribute('role') === 'button';
|
|
670
|
+
};
|
|
671
|
+
els.sort((a, b) => {
|
|
672
|
+
const ta = ownText(a), tb = ownText(b);
|
|
673
|
+
const ea = ta === target ? 0 : 1, eb = tb === target ? 0 : 1;
|
|
674
|
+
if (ea !== eb) return ea - eb;
|
|
675
|
+
const ia = isInteractive(a) ? 0 : 1, ib = isInteractive(b) ? 0 : 1;
|
|
676
|
+
if (ia !== ib) return ia - ib;
|
|
677
|
+
return 0; // stable \u2014 preserve DOM order
|
|
678
|
+
});
|
|
311
679
|
return els[0] || null;
|
|
312
680
|
})()`;
|
|
313
681
|
}
|
|
@@ -341,43 +709,53 @@ function queryAllJS(selector) {
|
|
|
341
709
|
async function waitForActionable(page, selector, opts = {}) {
|
|
342
710
|
const timeout = opts.timeout ?? 3e4;
|
|
343
711
|
if (opts.force) {
|
|
344
|
-
|
|
345
|
-
|
|
712
|
+
const deadline2 = Date.now() + timeout;
|
|
713
|
+
let lastError;
|
|
714
|
+
while (Date.now() < deadline2) {
|
|
715
|
+
const rect = await page.evaluate(`
|
|
346
716
|
(function() {
|
|
347
717
|
const el = ${queryJS(selector)};
|
|
348
718
|
if (!el) return null;
|
|
349
719
|
const r = el.getBoundingClientRect();
|
|
350
|
-
|
|
720
|
+
let x = r.x, y = r.y;
|
|
721
|
+
let doc = el.ownerDocument;
|
|
722
|
+
while (doc !== document) {
|
|
723
|
+
let host = null;
|
|
724
|
+
const scan = (d) => {
|
|
725
|
+
let frames;
|
|
726
|
+
try { frames = d.querySelectorAll('iframe'); } catch (e) { return null; }
|
|
727
|
+
for (const f of frames) {
|
|
728
|
+
let inner = null;
|
|
729
|
+
try { inner = f.contentDocument; } catch (e) { continue; }
|
|
730
|
+
if (!inner) continue;
|
|
731
|
+
if (inner === doc) return f;
|
|
732
|
+
const rr = scan(inner);
|
|
733
|
+
if (rr) return rr;
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
736
|
+
};
|
|
737
|
+
host = scan(document);
|
|
738
|
+
if (!host) break;
|
|
739
|
+
const hr = host.getBoundingClientRect();
|
|
740
|
+
x += hr.x; y += hr.y;
|
|
741
|
+
doc = host.ownerDocument;
|
|
742
|
+
}
|
|
743
|
+
return { x, y, width: r.width, height: r.height };
|
|
351
744
|
})()
|
|
352
|
-
`);
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
let lastError;
|
|
358
|
-
let nodeId = 0;
|
|
359
|
-
let rect = null;
|
|
360
|
-
while (Date.now() < deadline2) {
|
|
361
|
-
nodeId = await page.querySelector(selector);
|
|
362
|
-
if (!nodeId) {
|
|
363
|
-
lastError = `Element not found: ${selector}`;
|
|
364
|
-
await page.waitForTimeout(200);
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
rect = await page.getBoxModel(nodeId);
|
|
368
|
-
if (rect) break;
|
|
369
|
-
lastError = `Element has no box: ${selector}`;
|
|
370
|
-
await page.waitForTimeout(500);
|
|
745
|
+
`).catch(() => null);
|
|
746
|
+
if (rect && rect.width > 0 && rect.height > 0) return { nodeId: 0, rect };
|
|
747
|
+
lastError = `Element not visible (zero size): ${selector}`;
|
|
748
|
+
lastError = `Element not found: ${selector}`;
|
|
749
|
+
await page.waitForTimeout(200);
|
|
371
750
|
}
|
|
372
|
-
|
|
373
|
-
return { nodeId, rect };
|
|
751
|
+
throw new Error(lastError || `Element not found: ${selector}`);
|
|
374
752
|
}
|
|
375
753
|
const deadline = Date.now() + timeout;
|
|
376
754
|
while (Date.now() < deadline) {
|
|
377
755
|
const result = await checkActionable(page, selector);
|
|
378
756
|
if (result.ok && result.rect) {
|
|
379
|
-
const nodeId = await page.querySelector(selector);
|
|
380
|
-
|
|
757
|
+
const nodeId = await page.querySelector(selector).catch(() => 0) ?? 0;
|
|
758
|
+
return { nodeId, rect: result.rect };
|
|
381
759
|
}
|
|
382
760
|
await page.waitForTimeout(50);
|
|
383
761
|
}
|
|
@@ -410,12 +788,25 @@ async function checkActionable(page, selector) {
|
|
|
410
788
|
return { ok: false, reason: 'parent_disabled' };
|
|
411
789
|
}
|
|
412
790
|
|
|
413
|
-
// Check not covered by another element at center
|
|
791
|
+
// Check not covered by another element at center.
|
|
792
|
+
// elementFromPoint must run in the element's OWN document: for iframe-
|
|
793
|
+
// internal elements the main-document hit-test returns the <iframe>
|
|
794
|
+
// host itself, which falsely reports "covered" (rec-duel d01).
|
|
795
|
+
// For shadow-internal elements the hit-test retargets to the shadow
|
|
796
|
+
// HOST \u2014 walk the host chain before declaring coverage (rec-duel d04).
|
|
414
797
|
const cx = rect.x + rect.width / 2;
|
|
415
798
|
const cy = rect.y + rect.height / 2;
|
|
416
|
-
const topEl =
|
|
799
|
+
const topEl = el.ownerDocument.elementFromPoint(cx, cy);
|
|
417
800
|
if (topEl && topEl !== el && !el.contains(topEl) && !topEl.contains(el)) {
|
|
418
|
-
|
|
801
|
+
let hostChain = [];
|
|
802
|
+
let rootNode = el.getRootNode();
|
|
803
|
+
while (rootNode && rootNode.host) {
|
|
804
|
+
hostChain.push(rootNode.host);
|
|
805
|
+
rootNode = rootNode.host.getRootNode();
|
|
806
|
+
}
|
|
807
|
+
if (!hostChain.includes(topEl)) {
|
|
808
|
+
return { ok: false, reason: 'covered', rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
|
|
809
|
+
}
|
|
419
810
|
}
|
|
420
811
|
|
|
421
812
|
return {
|
|
@@ -460,21 +851,48 @@ var XBLocatorImpl = class _XBLocatorImpl {
|
|
|
460
851
|
const el = ${this._q(this.selector)};
|
|
461
852
|
if (!el) return null;
|
|
462
853
|
const rect = el.getBoundingClientRect();
|
|
463
|
-
|
|
854
|
+
let x = rect.x, y = rect.y;
|
|
855
|
+
let doc = el.ownerDocument;
|
|
856
|
+
while (doc !== document) {
|
|
857
|
+
let host = null;
|
|
858
|
+
const scan = (d) => {
|
|
859
|
+
let frames;
|
|
860
|
+
try { frames = d.querySelectorAll('iframe'); } catch (e) { return null; }
|
|
861
|
+
for (const f of frames) {
|
|
862
|
+
let inner = null;
|
|
863
|
+
try { inner = f.contentDocument; } catch (e) { continue; }
|
|
864
|
+
if (!inner) continue;
|
|
865
|
+
if (inner === doc) return f;
|
|
866
|
+
const r = scan(inner);
|
|
867
|
+
if (r) return r;
|
|
868
|
+
}
|
|
869
|
+
return null;
|
|
870
|
+
};
|
|
871
|
+
host = scan(document);
|
|
872
|
+
if (!host) break;
|
|
873
|
+
const hr = host.getBoundingClientRect();
|
|
874
|
+
x += hr.x; y += hr.y;
|
|
875
|
+
doc = host.ownerDocument;
|
|
876
|
+
}
|
|
877
|
+
return { x, y, width: rect.width, height: rect.height };
|
|
464
878
|
})()
|
|
465
879
|
`);
|
|
466
880
|
const finalRect = updatedRect ?? rect;
|
|
467
881
|
const cx = finalRect.x + finalRect.width / 2;
|
|
468
882
|
const cy = finalRect.y + finalRect.height / 2;
|
|
469
883
|
await this.page.mouse.click(cx, cy, {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
884
|
+
stealth: true,
|
|
885
|
+
elementWidth: finalRect.width,
|
|
886
|
+
elementHeight: finalRect.height,
|
|
887
|
+
...{ button: opts.button ?? "left", clickCount: opts.clickCount ?? 1, delay: opts.delay }
|
|
473
888
|
});
|
|
474
889
|
}
|
|
475
890
|
async fill(value, opts = {}) {
|
|
476
891
|
await waitForActionable(this.page, this.selector, opts);
|
|
477
892
|
await scrollIntoView(this.page, this.selector);
|
|
893
|
+
await this.click({ ...opts });
|
|
894
|
+
await this.page.keyboard.type(value, { stealth: true });
|
|
895
|
+
return;
|
|
478
896
|
await this.page.evaluate(`
|
|
479
897
|
(function() {
|
|
480
898
|
const el = ${this._q(this.selector)};
|
|
@@ -1184,6 +1602,8 @@ var XBPageImpl = class _XBPageImpl {
|
|
|
1184
1602
|
await this.conn.send("Page.enable", void 0, this.sessionId);
|
|
1185
1603
|
await this.conn.send("Runtime.enable", void 0, this.sessionId);
|
|
1186
1604
|
await this.conn.send("Network.enable", void 0, this.sessionId);
|
|
1605
|
+
await this.conn.send("DOM.enable", void 0, this.sessionId).catch(() => {
|
|
1606
|
+
});
|
|
1187
1607
|
this.setupPageEvents();
|
|
1188
1608
|
this.setupNetworkEvents();
|
|
1189
1609
|
this.setupConsoleEvents();
|
|
@@ -1196,6 +1616,9 @@ var XBPageImpl = class _XBPageImpl {
|
|
|
1196
1616
|
);
|
|
1197
1617
|
this._url = info.url;
|
|
1198
1618
|
this._title = info.title;
|
|
1619
|
+
if (info.url && info.url !== "about:blank" && info.url !== "") {
|
|
1620
|
+
this._loadState = { loadFired: true, domContentFired: true, networkIdle: true };
|
|
1621
|
+
}
|
|
1199
1622
|
} catch {
|
|
1200
1623
|
}
|
|
1201
1624
|
}
|
|
@@ -1208,6 +1631,16 @@ var XBPageImpl = class _XBPageImpl {
|
|
|
1208
1631
|
const waitUntil = opts.waitUntil ?? "load";
|
|
1209
1632
|
const timeout = opts.timeout ?? 3e4;
|
|
1210
1633
|
this._loadState = { loadFired: false, domContentFired: false, networkIdle: false };
|
|
1634
|
+
if (process.env.XBROWSER_STEALTH !== "off") {
|
|
1635
|
+
try {
|
|
1636
|
+
await this.conn.send(
|
|
1637
|
+
"Page.addScriptToEvaluateOnNewDocument",
|
|
1638
|
+
{ source: buildStealthInitScript() },
|
|
1639
|
+
this.sessionId
|
|
1640
|
+
);
|
|
1641
|
+
} catch {
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1211
1644
|
const result = await this.conn.send(
|
|
1212
1645
|
"Page.navigate",
|
|
1213
1646
|
{ url, referrer: opts.referer },
|
|
@@ -1415,6 +1848,91 @@ Last error: ${lastError.message}` : "";
|
|
|
1415
1848
|
}
|
|
1416
1849
|
return result.result?.value;
|
|
1417
1850
|
}
|
|
1851
|
+
/**
|
|
1852
|
+
* 在指定 iframe 上下文中执行表达式(攻防 D16 能力建设,2026-08-19)。
|
|
1853
|
+
*
|
|
1854
|
+
* 双路径:
|
|
1855
|
+
* 1. 同进程 iframe —— Runtime.enable 收集 executionContextCreated,
|
|
1856
|
+
* 找到目标 frameId 的 contextId,用 contextId 定向执行;
|
|
1857
|
+
* 2. 跨域 OOPIF(独立 target)—— Target.setAutoAttach(flatten) 监听
|
|
1858
|
+
* attachedToTarget 中 type==='iframe' 的会话,用其 sessionId 执行。
|
|
1859
|
+
*
|
|
1860
|
+
* 这绕过了页面同源策略(那是页面 JS 的约束,CDP 是调试通道)——
|
|
1861
|
+
* 支付窗/验证码/第三方嵌入内容的读写都靠它。
|
|
1862
|
+
*/
|
|
1863
|
+
async evaluateInFrame(urlIncludes, expression) {
|
|
1864
|
+
if (this._closed) throw new Error("Page is closed");
|
|
1865
|
+
const evalIn = async (sessionId, contextId) => {
|
|
1866
|
+
const params = { expression, returnByValue: true, awaitPromise: true };
|
|
1867
|
+
if (contextId !== void 0) params.contextId = contextId;
|
|
1868
|
+
const result = await this.conn.send("Runtime.evaluate", params, sessionId);
|
|
1869
|
+
if (result.exceptionDetails) {
|
|
1870
|
+
const detail = result.exceptionDetails.exception?.description ?? result.exceptionDetails.exception?.value ?? result.exceptionDetails.text;
|
|
1871
|
+
throw new Error(`[frame ${urlIncludes}] ${detail}`);
|
|
1872
|
+
}
|
|
1873
|
+
return result.result?.value;
|
|
1874
|
+
};
|
|
1875
|
+
try {
|
|
1876
|
+
const tg = await this.conn.send("Target.getTargets", void 0);
|
|
1877
|
+
const hit2 = (tg.targetInfos || []).find((t) => t.type === "iframe" && (t.url || "").includes(urlIncludes));
|
|
1878
|
+
if (hit2) {
|
|
1879
|
+
const att = await this.conn.send("Target.attachToTarget", { targetId: hit2.targetId, flatten: true });
|
|
1880
|
+
return evalIn(att.sessionId);
|
|
1881
|
+
}
|
|
1882
|
+
} catch {
|
|
1883
|
+
}
|
|
1884
|
+
const tree = await this.conn.send("Page.getFrameTree", void 0, this.sessionId);
|
|
1885
|
+
const all = [];
|
|
1886
|
+
const walk = (node) => {
|
|
1887
|
+
all.push({ id: node.frame.id, url: node.frame.url });
|
|
1888
|
+
for (const child of node.childFrames || []) walk(child);
|
|
1889
|
+
};
|
|
1890
|
+
walk(tree.frameTree);
|
|
1891
|
+
const mainId = tree.frameTree?.frame?.id;
|
|
1892
|
+
const target = all.find((f) => f.id !== mainId && f.url.includes(urlIncludes));
|
|
1893
|
+
if (target) {
|
|
1894
|
+
const contexts = [];
|
|
1895
|
+
const onCtx = (raw) => {
|
|
1896
|
+
const c = raw?.context;
|
|
1897
|
+
if (c?.id && c?.auxData?.frameId) contexts.push({ id: c.id, frameId: c.auxData.frameId });
|
|
1898
|
+
};
|
|
1899
|
+
this.conn.on("Runtime.executionContextCreated", onCtx);
|
|
1900
|
+
try {
|
|
1901
|
+
await this.conn.send("Runtime.enable", void 0, this.sessionId).catch(() => {
|
|
1902
|
+
});
|
|
1903
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
1904
|
+
} finally {
|
|
1905
|
+
this.conn.off("Runtime.executionContextCreated", onCtx);
|
|
1906
|
+
}
|
|
1907
|
+
const ctx = contexts.find((c) => c.frameId === target.id);
|
|
1908
|
+
if (ctx) return evalIn(this.sessionId, ctx.id);
|
|
1909
|
+
}
|
|
1910
|
+
const attached = [];
|
|
1911
|
+
const onAttach = (raw) => {
|
|
1912
|
+
const ev = raw;
|
|
1913
|
+
if (ev?.sessionId && ev.targetInfo?.type === "iframe") {
|
|
1914
|
+
attached.push({ sessionId: ev.sessionId, url: ev.targetInfo.url || "" });
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
this.conn.on("Target.attachedToTarget", onAttach);
|
|
1918
|
+
try {
|
|
1919
|
+
await this.conn.send("Target.setAutoAttach", {
|
|
1920
|
+
autoAttach: true,
|
|
1921
|
+
waitForDebuggerOnStart: false,
|
|
1922
|
+
flatten: true
|
|
1923
|
+
}, this.sessionId);
|
|
1924
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
1925
|
+
} finally {
|
|
1926
|
+
this.conn.off("Target.attachedToTarget", onAttach);
|
|
1927
|
+
this.conn.send("Target.setAutoAttach", { autoAttach: false, waitForDebuggerOnStart: false, flatten: true }, this.sessionId).catch(() => {
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
const hit = attached.find((a) => a.url.includes(urlIncludes));
|
|
1931
|
+
if (!hit) {
|
|
1932
|
+
throw new Error(`frame not found for "${urlIncludes}"\uFF08OOPIF target\u3001frame \u6811\u3001auto-attach \u4E09\u8DEF\u5747\u672A\u547D\u4E2D\uFF1B\u53EF\u80FD\u4ECD\u5728\u52A0\u8F7D\uFF09`);
|
|
1933
|
+
}
|
|
1934
|
+
return evalIn(hit.sessionId);
|
|
1935
|
+
}
|
|
1418
1936
|
/** evaluateHandle — evaluates fn and returns a handle for element bounding box */
|
|
1419
1937
|
async evaluateHandle(fn, ...args) {
|
|
1420
1938
|
let expression;
|
|
@@ -1659,6 +2177,10 @@ Last error: ${lastError.message}` : "";
|
|
|
1659
2177
|
this.sessionId
|
|
1660
2178
|
);
|
|
1661
2179
|
}
|
|
2180
|
+
/** 覆盖 User-Agent(headless 环境对抗 UA 检测的标配能力) */
|
|
2181
|
+
async setUserAgent(userAgent) {
|
|
2182
|
+
await this._setUserAgent(userAgent);
|
|
2183
|
+
}
|
|
1662
2184
|
/** Internal: set extra HTTP headers */
|
|
1663
2185
|
async _setExtraHTTPHeaders(headers) {
|
|
1664
2186
|
await this.setExtraHTTPHeaders(headers);
|
|
@@ -1785,16 +2307,21 @@ Last error: ${lastError.message}` : "";
|
|
|
1785
2307
|
}
|
|
1786
2308
|
return 1;
|
|
1787
2309
|
}
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
{ depth: 0 },
|
|
1791
|
-
|
|
2310
|
+
const withTimeout = (p, ms) => Promise.race([p, new Promise((r) => setTimeout(() => r(null), ms))]);
|
|
2311
|
+
const doc = await withTimeout(
|
|
2312
|
+
this.conn.send("DOM.getDocument", { depth: 0 }, this.sessionId),
|
|
2313
|
+
8e3
|
|
1792
2314
|
);
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2315
|
+
if (!doc) return 0;
|
|
2316
|
+
const result = await withTimeout(
|
|
2317
|
+
this.conn.send(
|
|
2318
|
+
"DOM.querySelector",
|
|
2319
|
+
{ nodeId: doc.root.nodeId, selector },
|
|
2320
|
+
this.sessionId
|
|
2321
|
+
),
|
|
2322
|
+
8e3
|
|
1797
2323
|
);
|
|
2324
|
+
if (!result) return 0;
|
|
1798
2325
|
return result.nodeId;
|
|
1799
2326
|
}
|
|
1800
2327
|
/** Query all matching elements, returns array of CDP nodeIds */
|
|
@@ -2783,318 +3310,43 @@ var XBBrowserImpl = class {
|
|
|
2783
3310
|
// src/cdp-driver/connection.ts
|
|
2784
3311
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
2785
3312
|
import { WebSocket } from "ws";
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
3313
|
+
|
|
3314
|
+
// src/cdp-interceptor/rules/shared.ts
|
|
3315
|
+
var PLAYWRIGHT_INTERNAL_MARKERS = [
|
|
3316
|
+
"__commonJS",
|
|
3317
|
+
"module.exports",
|
|
3318
|
+
"__require",
|
|
3319
|
+
"__toESM",
|
|
3320
|
+
"inject_utils"
|
|
3321
|
+
];
|
|
3322
|
+
function isPlaywrightInternal(code) {
|
|
3323
|
+
return PLAYWRIGHT_INTERNAL_MARKERS.some((marker) => code.includes(marker));
|
|
3324
|
+
}
|
|
3325
|
+
function extractUserCode(ctx) {
|
|
3326
|
+
if (ctx.method === "Runtime.evaluate") {
|
|
3327
|
+
const expr = ctx.params.expression;
|
|
3328
|
+
if (typeof expr === "string") {
|
|
3329
|
+
if (isPlaywrightInternal(expr)) return null;
|
|
3330
|
+
return expr;
|
|
2803
3331
|
}
|
|
2804
|
-
this.bindWebSocket();
|
|
2805
|
-
this.startKeepalive();
|
|
2806
|
-
}
|
|
2807
|
-
/** Send periodic WS pings to prevent idle-timeout disconnects (e.g. CF's 100s).
|
|
2808
|
-
* Also detects dead connections: if a pong isn't received within 10s of a
|
|
2809
|
-
* ping, the connection is considered dead and forcibly closed. */
|
|
2810
|
-
keepaliveTimer = null;
|
|
2811
|
-
pongTimer = null;
|
|
2812
|
-
startKeepalive() {
|
|
2813
|
-
this.ws.on("pong", () => {
|
|
2814
|
-
if (this.pongTimer) {
|
|
2815
|
-
clearTimeout(this.pongTimer);
|
|
2816
|
-
this.pongTimer = null;
|
|
2817
|
-
}
|
|
2818
|
-
});
|
|
2819
|
-
this.keepaliveTimer = setInterval(() => {
|
|
2820
|
-
if (this.ws.readyState === WebSocket.OPEN) {
|
|
2821
|
-
if (!this.pongTimer) {
|
|
2822
|
-
this.pongTimer = setTimeout(() => {
|
|
2823
|
-
if (!this.closed) {
|
|
2824
|
-
this.closed = true;
|
|
2825
|
-
this.closeReason = "keepalive timeout (no pong in 10s)";
|
|
2826
|
-
try {
|
|
2827
|
-
this.ws.terminate();
|
|
2828
|
-
} catch {
|
|
2829
|
-
}
|
|
2830
|
-
for (const [, pending] of this.pending) {
|
|
2831
|
-
clearTimeout(pending.timeout);
|
|
2832
|
-
pending.reject(new Error("Connection dead: keepalive timeout"));
|
|
2833
|
-
}
|
|
2834
|
-
this.pending.clear();
|
|
2835
|
-
this.emit("disconnect");
|
|
2836
|
-
}
|
|
2837
|
-
}, 1e4);
|
|
2838
|
-
}
|
|
2839
|
-
this.ws.ping?.();
|
|
2840
|
-
} else if (this.closed) {
|
|
2841
|
-
if (this.keepaliveTimer) clearInterval(this.keepaliveTimer);
|
|
2842
|
-
this.keepaliveTimer = null;
|
|
2843
|
-
}
|
|
2844
|
-
}, 3e4);
|
|
2845
3332
|
}
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
if (
|
|
2849
|
-
|
|
2850
|
-
throw new Error(`WebSocket already closed: ${this.closeReason ?? "unknown"}`);
|
|
3333
|
+
if (ctx.method === "Runtime.callFunctionOn") {
|
|
3334
|
+
const decl = ctx.params.functionDeclaration;
|
|
3335
|
+
if (typeof decl === "string" && decl.includes("utilityScript.evaluate")) {
|
|
3336
|
+
return extractAllStrings(ctx.params.arguments);
|
|
2851
3337
|
}
|
|
2852
|
-
|
|
2853
|
-
const onOpen = () => {
|
|
2854
|
-
this.ws.off("error", onError);
|
|
2855
|
-
resolve();
|
|
2856
|
-
};
|
|
2857
|
-
const onError = (err) => {
|
|
2858
|
-
this.ws.off("open", onOpen);
|
|
2859
|
-
reject(err);
|
|
2860
|
-
};
|
|
2861
|
-
this.ws.once("open", onOpen);
|
|
2862
|
-
this.ws.once("error", onError);
|
|
2863
|
-
});
|
|
2864
|
-
}
|
|
2865
|
-
/** Is the underlying WebSocket alive? */
|
|
2866
|
-
get isOpen() {
|
|
2867
|
-
return !this.closed && this.ws.readyState === WebSocket.OPEN;
|
|
3338
|
+
if (typeof decl === "string") return decl;
|
|
2868
3339
|
}
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
if (this.closed) {
|
|
2880
|
-
throw new Error(`CDP connection closed: ${this.closeReason ?? "unknown"}`);
|
|
2881
|
-
}
|
|
2882
|
-
if (!this.isOpen) {
|
|
2883
|
-
throw new Error(`CDP connection not open (state: ${this.ws.readyState})`);
|
|
2884
|
-
}
|
|
2885
|
-
const id = this.nextId++;
|
|
2886
|
-
const sid = sessionId ?? this.defaultSessionId;
|
|
2887
|
-
const message = { id, method };
|
|
2888
|
-
if (params !== void 0) message.params = params;
|
|
2889
|
-
if (sid !== void 0) message.sessionId = sid;
|
|
2890
|
-
return new Promise((resolve, reject) => {
|
|
2891
|
-
const timeout = setTimeout(() => {
|
|
2892
|
-
this.pending.delete(id);
|
|
2893
|
-
reject(new Error(`CDP timeout: ${method} (${timeoutMs}ms)`));
|
|
2894
|
-
}, timeoutMs);
|
|
2895
|
-
this.pending.set(id, {
|
|
2896
|
-
resolve: (v) => {
|
|
2897
|
-
clearTimeout(timeout);
|
|
2898
|
-
this.pending.delete(id);
|
|
2899
|
-
resolve(v);
|
|
2900
|
-
},
|
|
2901
|
-
reject: (err) => {
|
|
2902
|
-
clearTimeout(timeout);
|
|
2903
|
-
this.pending.delete(id);
|
|
2904
|
-
reject(err);
|
|
2905
|
-
},
|
|
2906
|
-
method,
|
|
2907
|
-
timeout
|
|
2908
|
-
});
|
|
2909
|
-
const data = JSON.stringify(message);
|
|
2910
|
-
try {
|
|
2911
|
-
this.ws.send(data);
|
|
2912
|
-
} catch (err) {
|
|
2913
|
-
clearTimeout(timeout);
|
|
2914
|
-
this.pending.delete(id);
|
|
2915
|
-
reject(new Error(`CDP send failed: ${method} \u2014 ${err instanceof Error ? err.message : String(err)}`));
|
|
2916
|
-
}
|
|
2917
|
-
});
|
|
2918
|
-
}
|
|
2919
|
-
/**
|
|
2920
|
-
* Subscribe to a CDP event.
|
|
2921
|
-
*
|
|
2922
|
-
* @param event — full event name (e.g. "Page.frameNavigated")
|
|
2923
|
-
* @param handler — called with the event params
|
|
2924
|
-
* @param sessionId — optional session filter
|
|
2925
|
-
*/
|
|
2926
|
-
on(event, handler) {
|
|
2927
|
-
return super.on(event, handler);
|
|
2928
|
-
}
|
|
2929
|
-
once(event, handler) {
|
|
2930
|
-
return super.once(event, handler);
|
|
2931
|
-
}
|
|
2932
|
-
/** Remove an event listener */
|
|
2933
|
-
off(event, handler) {
|
|
2934
|
-
super.off(event, handler);
|
|
2935
|
-
return this;
|
|
2936
|
-
}
|
|
2937
|
-
/**
|
|
2938
|
-
* Subscribe to a CDP event for a specific session.
|
|
2939
|
-
* Returns an unsubscribe function.
|
|
2940
|
-
*/
|
|
2941
|
-
subscribe(event, sessionId, handler) {
|
|
2942
|
-
const wrapper = (params, sid) => {
|
|
2943
|
-
if (sid === sessionId || !sessionId && !sid) handler(params);
|
|
2944
|
-
};
|
|
2945
|
-
this.on(event, wrapper);
|
|
2946
|
-
return () => this.off(event, wrapper);
|
|
2947
|
-
}
|
|
2948
|
-
/** Close the WebSocket */
|
|
2949
|
-
async close() {
|
|
2950
|
-
if (this.closed) return;
|
|
2951
|
-
this.closed = true;
|
|
2952
|
-
this.closeReason = "closed by caller";
|
|
2953
|
-
if (this.keepaliveTimer) {
|
|
2954
|
-
clearInterval(this.keepaliveTimer);
|
|
2955
|
-
this.keepaliveTimer = null;
|
|
2956
|
-
}
|
|
2957
|
-
if (this.pongTimer) {
|
|
2958
|
-
clearTimeout(this.pongTimer);
|
|
2959
|
-
this.pongTimer = null;
|
|
2960
|
-
}
|
|
2961
|
-
for (const [id, pending] of this.pending) {
|
|
2962
|
-
clearTimeout(pending.timeout);
|
|
2963
|
-
pending.reject(new Error(`Connection closed: ${pending.method}`));
|
|
2964
|
-
this.pending.delete(id);
|
|
2965
|
-
}
|
|
2966
|
-
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
2967
|
-
this.ws.close(1e3, "normal closure");
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
|
-
/** Set the default session ID for flat protocol */
|
|
2971
|
-
setDefaultSessionId(sid) {
|
|
2972
|
-
this.defaultSessionId = sid;
|
|
2973
|
-
}
|
|
2974
|
-
// ── Private ─────────────────────────────────────────────────
|
|
2975
|
-
bindWebSocket() {
|
|
2976
|
-
this.ws.on("message", (raw) => {
|
|
2977
|
-
let msg;
|
|
2978
|
-
try {
|
|
2979
|
-
msg = JSON.parse(raw.toString());
|
|
2980
|
-
} catch {
|
|
2981
|
-
return;
|
|
2982
|
-
}
|
|
2983
|
-
if (msg.id !== void 0) {
|
|
2984
|
-
const pending = this.pending.get(msg.id);
|
|
2985
|
-
if (!pending) return;
|
|
2986
|
-
if (msg.error) {
|
|
2987
|
-
pending.reject(new CDPProtocolError(msg.error.code, msg.error.message, pending.method));
|
|
2988
|
-
} else {
|
|
2989
|
-
pending.resolve(msg.result ?? {});
|
|
2990
|
-
}
|
|
2991
|
-
return;
|
|
2992
|
-
}
|
|
2993
|
-
if (msg.method) {
|
|
2994
|
-
this.emit(msg.method, msg.params ?? {}, msg.sessionId);
|
|
2995
|
-
this.emit("*", msg.method, msg.params ?? {}, msg.sessionId);
|
|
2996
|
-
}
|
|
2997
|
-
});
|
|
2998
|
-
this.ws.on("close", (code, reason) => {
|
|
2999
|
-
if (this.closed) return;
|
|
3000
|
-
this.closed = true;
|
|
3001
|
-
this.closeReason = `WebSocket closed: ${code} ${reason?.toString() ?? ""}`.trim();
|
|
3002
|
-
if (this.keepaliveTimer) {
|
|
3003
|
-
clearInterval(this.keepaliveTimer);
|
|
3004
|
-
this.keepaliveTimer = null;
|
|
3005
|
-
}
|
|
3006
|
-
for (const [id, pending] of this.pending) {
|
|
3007
|
-
clearTimeout(pending.timeout);
|
|
3008
|
-
pending.reject(new Error(`Connection closed: ${pending.method}`));
|
|
3009
|
-
this.pending.delete(id);
|
|
3010
|
-
}
|
|
3011
|
-
this.emit("disconnect");
|
|
3012
|
-
});
|
|
3013
|
-
this.ws.on("error", (err) => {
|
|
3014
|
-
if (this.closed) return;
|
|
3015
|
-
this.emit("ws-error", err);
|
|
3016
|
-
});
|
|
3017
|
-
}
|
|
3018
|
-
};
|
|
3019
|
-
var CDPProtocolError = class extends Error {
|
|
3020
|
-
code;
|
|
3021
|
-
method;
|
|
3022
|
-
data;
|
|
3023
|
-
constructor(code, message, method, data) {
|
|
3024
|
-
super(`CDP error [${code}] in ${method}: ${message}`);
|
|
3025
|
-
this.name = "CDPProtocolError";
|
|
3026
|
-
this.code = code;
|
|
3027
|
-
this.method = method;
|
|
3028
|
-
this.data = data;
|
|
3029
|
-
}
|
|
3030
|
-
};
|
|
3031
|
-
|
|
3032
|
-
// src/cdp-driver/index.ts
|
|
3033
|
-
async function launch(options = {}) {
|
|
3034
|
-
let wsEndpoint;
|
|
3035
|
-
let childProcess;
|
|
3036
|
-
let tmpDir;
|
|
3037
|
-
if (options.cdpEndpoint) {
|
|
3038
|
-
wsEndpoint = await connectToCDP(options.cdpEndpoint);
|
|
3039
|
-
} else {
|
|
3040
|
-
const result = await launchChrome({
|
|
3041
|
-
executablePath: options.executablePath,
|
|
3042
|
-
headless: options.headless,
|
|
3043
|
-
args: options.args,
|
|
3044
|
-
userDataDir: options.userDataDir,
|
|
3045
|
-
timeout: options.timeout,
|
|
3046
|
-
env: options.env
|
|
3047
|
-
});
|
|
3048
|
-
wsEndpoint = result.wsEndpoint;
|
|
3049
|
-
childProcess = result.process;
|
|
3050
|
-
tmpDir = result.tmpDir;
|
|
3051
|
-
}
|
|
3052
|
-
const conn = new CDPConnection(wsEndpoint);
|
|
3053
|
-
await conn.ready();
|
|
3054
|
-
const httpEndpoint = options.cdpEndpoint && !options.cdpEndpoint.startsWith("ws") ? options.cdpEndpoint : void 0;
|
|
3055
|
-
const browser = new XBBrowserImpl(conn, childProcess, tmpDir, httpEndpoint);
|
|
3056
|
-
return { browser, wsEndpoint };
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
|
-
// src/cdp-interceptor/proxy.ts
|
|
3060
|
-
import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
|
|
3061
|
-
|
|
3062
|
-
// src/cdp-interceptor/rules/shared.ts
|
|
3063
|
-
var PLAYWRIGHT_INTERNAL_MARKERS = [
|
|
3064
|
-
"__commonJS",
|
|
3065
|
-
"module.exports",
|
|
3066
|
-
"__require",
|
|
3067
|
-
"__toESM",
|
|
3068
|
-
"inject_utils"
|
|
3069
|
-
];
|
|
3070
|
-
function isPlaywrightInternal(code) {
|
|
3071
|
-
return PLAYWRIGHT_INTERNAL_MARKERS.some((marker) => code.includes(marker));
|
|
3072
|
-
}
|
|
3073
|
-
function extractUserCode(ctx) {
|
|
3074
|
-
if (ctx.method === "Runtime.evaluate") {
|
|
3075
|
-
const expr = ctx.params.expression;
|
|
3076
|
-
if (typeof expr === "string") {
|
|
3077
|
-
if (isPlaywrightInternal(expr)) return null;
|
|
3078
|
-
return expr;
|
|
3079
|
-
}
|
|
3080
|
-
}
|
|
3081
|
-
if (ctx.method === "Runtime.callFunctionOn") {
|
|
3082
|
-
const decl = ctx.params.functionDeclaration;
|
|
3083
|
-
if (typeof decl === "string" && decl.includes("utilityScript.evaluate")) {
|
|
3084
|
-
return extractAllStrings(ctx.params.arguments);
|
|
3085
|
-
}
|
|
3086
|
-
if (typeof decl === "string") return decl;
|
|
3087
|
-
}
|
|
3088
|
-
return null;
|
|
3089
|
-
}
|
|
3090
|
-
function extractAllStrings(rawArgs) {
|
|
3091
|
-
if (!Array.isArray(rawArgs)) return null;
|
|
3092
|
-
const strings = [];
|
|
3093
|
-
for (const arg of rawArgs) {
|
|
3094
|
-
if (arg && typeof arg === "object" && "value" in arg) {
|
|
3095
|
-
const val = arg.value;
|
|
3096
|
-
if (typeof val === "string" && val.length > 5 && !["true", "false"].includes(val)) {
|
|
3097
|
-
strings.push(val);
|
|
3340
|
+
return null;
|
|
3341
|
+
}
|
|
3342
|
+
function extractAllStrings(rawArgs) {
|
|
3343
|
+
if (!Array.isArray(rawArgs)) return null;
|
|
3344
|
+
const strings = [];
|
|
3345
|
+
for (const arg of rawArgs) {
|
|
3346
|
+
if (arg && typeof arg === "object" && "value" in arg) {
|
|
3347
|
+
const val = arg.value;
|
|
3348
|
+
if (typeof val === "string" && val.length > 5 && !["true", "false"].includes(val)) {
|
|
3349
|
+
strings.push(val);
|
|
3098
3350
|
}
|
|
3099
3351
|
}
|
|
3100
3352
|
}
|
|
@@ -3104,22 +3356,22 @@ function extractAllStrings(rawArgs) {
|
|
|
3104
3356
|
// src/cdp-interceptor/rules/dom-mutation.ts
|
|
3105
3357
|
var DOM_PATTERNS = [
|
|
3106
3358
|
// ── P0: Value/Checked (bypasses React onChange) ────────────
|
|
3107
|
-
{ pattern: /\.value\s*=\s*(?!["'\s]*$)/, name: ".value =", severity: "danger", action: "
|
|
3108
|
-
{ pattern: /\.checked\s*=\s*(?:true|false)/, name: ".checked =", severity: "danger", action: "
|
|
3109
|
-
{ pattern: /\.indeterminate\s*=\s*true/, name: ".indeterminate =", severity: "warn", action: "
|
|
3110
|
-
{ pattern: /\.valueAsDate\s*=/, name: ".valueAsDate =", severity: "warn", action: "
|
|
3111
|
-
{ pattern: /\.valueAsNumber\s*=/, name: ".valueAsNumber =", severity: "warn", action: "
|
|
3359
|
+
{ pattern: /\.value\s*=\s*(?!["'\s]*$)/, name: ".value =", severity: "danger", action: "pass", errorCode: -32001, suggestion: "Use page.fill(selector, value) which dispatches proper input/change events." },
|
|
3360
|
+
{ pattern: /\.checked\s*=\s*(?:true|false)/, name: ".checked =", severity: "danger", action: "pass", errorCode: -32001, suggestion: "Use page.check(selector) or page.uncheck(selector)." },
|
|
3361
|
+
{ pattern: /\.indeterminate\s*=\s*true/, name: ".indeterminate =", severity: "warn", action: "pass", errorCode: -32021, suggestion: "No human can set indeterminate state \u2014 remove this call." },
|
|
3362
|
+
{ pattern: /\.valueAsDate\s*=/, name: ".valueAsDate =", severity: "warn", action: "pass", errorCode: -32022, suggestion: "Use page.fill() with formatted date string instead." },
|
|
3363
|
+
{ pattern: /\.valueAsNumber\s*=/, name: ".valueAsNumber =", severity: "warn", action: "pass", errorCode: -32022, suggestion: "Use page.fill() with numeric string instead." },
|
|
3112
3364
|
// ── P1: Select/Option (bypasses React onChange on select) ─
|
|
3113
|
-
{ pattern: /\.selectedIndex\s*=\s*\d+/, name: ".selectedIndex =", severity: "danger", action: "
|
|
3114
|
-
{ pattern: /(?:options|children)\s*\[[^\]]*\]\s*\.\s*selected\s*=\s*(?:true|false)/, name: "options[N].selected =", severity: "danger", action: "
|
|
3115
|
-
{ pattern: /\.value\s*=\s*["'][^"']*["']\s*[;)]?\s*$/, name: "selectElement.value =", severity: "warn", action: "
|
|
3365
|
+
{ pattern: /\.selectedIndex\s*=\s*\d+/, name: ".selectedIndex =", severity: "danger", action: "pass", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
|
|
3366
|
+
{ pattern: /(?:options|children)\s*\[[^\]]*\]\s*\.\s*selected\s*=\s*(?:true|false)/, name: "options[N].selected =", severity: "danger", action: "pass", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
|
|
3367
|
+
{ pattern: /\.value\s*=\s*["'][^"']*["']\s*[;)]?\s*$/, name: "selectElement.value =", severity: "warn", action: "pass", errorCode: -32023, suggestion: "For select elements, use page.selectOption(selector, value)." },
|
|
3116
3368
|
// ── P2: Content properties (bypasses virtual DOM diffing) ──
|
|
3117
3369
|
{ pattern: /\.innerHTML\s*=/, name: ".innerHTML =", severity: "info", action: "pass", errorCode: -32007, suggestion: ".innerHTML bypasses React/Vue diffing. Use component state or page.setContent()." },
|
|
3118
3370
|
{ pattern: /\.outerHTML\s*=/, name: ".outerHTML =", severity: "info", action: "pass", errorCode: -32007, suggestion: "outerHTML replacement destroys React fiber tree. Use page.setContent()." },
|
|
3119
|
-
{ pattern: /\.innerText\s*=/, name: ".innerText =", severity: "warn", action: "
|
|
3120
|
-
{ pattern: /\.textContent\s*=/, name: ".textContent =", severity: "warn", action: "
|
|
3121
|
-
{ pattern: /\.outerText\s*=/, name: ".outerText =", severity: "warn", action: "
|
|
3122
|
-
{ pattern: /nodeValue\s*=/, name: "node.nodeValue =", severity: "info", action: "
|
|
3371
|
+
{ pattern: /\.innerText\s*=/, name: ".innerText =", severity: "warn", action: "pass", errorCode: -32024, suggestion: "Framework components should be updated via state, not innerText." },
|
|
3372
|
+
{ pattern: /\.textContent\s*=/, name: ".textContent =", severity: "warn", action: "pass", errorCode: -32024, suggestion: "textContent bypasses React/Vue diffing. Use component state instead." },
|
|
3373
|
+
{ pattern: /\.outerText\s*=/, name: ".outerText =", severity: "warn", action: "pass", errorCode: -32024, suggestion: "Non-standard property \u2014 use proper framework update methods." },
|
|
3374
|
+
{ pattern: /nodeValue\s*=/, name: "node.nodeValue =", severity: "info", action: "pass", errorCode: -32025, suggestion: "Direct text node mutation. Use textContent instead if needed." },
|
|
3123
3375
|
// ── P3: Style properties ──────────────────────────────────
|
|
3124
3376
|
{ pattern: /\.style\s*=\s*["']/, name: ".style = (string override)", severity: "info", action: "pass", errorCode: -32008, suggestion: 'Setting style as a string overwrites CSSStyleDeclaration. Use element.style.prop = "value".' },
|
|
3125
3377
|
{ pattern: /\.style\.cssText\s*=/, name: ".style.cssText =", severity: "info", action: "pass", errorCode: -32008, suggestion: "style.cssText replacement destroys inline styles \u2014 use individual property set." },
|
|
@@ -3167,7 +3419,7 @@ var DOM_PATTERNS = [
|
|
|
3167
3419
|
{ pattern: /\.scrollBy\s*\(/, name: ".scrollBy()", severity: "info", action: "pass", errorCode: -32015, suggestion: "ScrollBy without user gesture." },
|
|
3168
3420
|
{ pattern: /\.scrollIntoView\s*\(/, name: ".scrollIntoView()", severity: "info", action: "pass", errorCode: -32015, suggestion: "scrollIntoView is a common bot pattern. Let Playwright handle scrolling." },
|
|
3169
3421
|
// ── P11: Shadow DOM ───────────────────────────────────────
|
|
3170
|
-
{ pattern: /\.shadowRoot\s*=/, name: ".shadowRoot = (override)", severity: "info", action: "
|
|
3422
|
+
{ pattern: /\.shadowRoot\s*=/, name: ".shadowRoot = (override)", severity: "info", action: "pass", errorCode: -32036, suggestion: "ShadowRoot is read-only \u2014 this set attempt is detectable." },
|
|
3171
3423
|
// ── P12: Force reflow / layout thrashing ──────────────────
|
|
3172
3424
|
{ pattern: /\.offsetHeight\b(?!\s*===?\s*)/, name: ".offsetHeight read (forced reflow)", severity: "warn", action: "pass", errorCode: -32016, suggestion: "Reading offsetHeight triggers forced reflow \u2014 anti-crawlers detect this as layout probing." },
|
|
3173
3425
|
{ pattern: /\.offsetWidth\b(?!\s*===?\s*)/, name: ".offsetWidth read (forced reflow)", severity: "warn", action: "pass", errorCode: -32016, suggestion: "Reading offsetWidth triggers forced reflow \u2014 detectable." },
|
|
@@ -3280,7 +3532,7 @@ function analyzeTrajectory(samples) {
|
|
|
3280
3532
|
const stopY = samples[samples.length - 1].y;
|
|
3281
3533
|
return {
|
|
3282
3534
|
ruleId: "mouse-trajectory",
|
|
3283
|
-
action: "
|
|
3535
|
+
action: "pass",
|
|
3284
3536
|
severity: "danger",
|
|
3285
3537
|
reason: `Suspicious mouse trajectory: ${issues.join("; ")}`,
|
|
3286
3538
|
suggestion: `This mouse movement appears automated (straight line A\u2192B, no natural variation).
|
|
@@ -3422,7 +3674,7 @@ function analyzeKeyTiming(samples) {
|
|
|
3422
3674
|
if (allIdentical) {
|
|
3423
3675
|
return {
|
|
3424
3676
|
ruleId: "input-keystroke",
|
|
3425
|
-
action: "
|
|
3677
|
+
action: "pass",
|
|
3426
3678
|
severity: "danger",
|
|
3427
3679
|
reason: `All ${intervals.length} keystroke intervals are exactly ${intervals[0]}ms \u2014 impossible for human typing.`,
|
|
3428
3680
|
suggestion: `Use page.fill(selector, text) instead of page.type() with delay.
|
|
@@ -3434,7 +3686,7 @@ Or add random variation: page.type(selector, text, {delay: 50 + Math.random() *
|
|
|
3434
3686
|
if (cv < 0.08) {
|
|
3435
3687
|
return {
|
|
3436
3688
|
ruleId: "input-keystroke",
|
|
3437
|
-
action: "
|
|
3689
|
+
action: "pass",
|
|
3438
3690
|
severity: "warn",
|
|
3439
3691
|
reason: `Unnatural keystroke timing (CV=${cv.toFixed(3)}). Human typing has CV > 0.2 on average.`,
|
|
3440
3692
|
suggestion: `Add random variation to your typing delay: page.type(selector, text, {delay: 50 + Math.random() * 80}).`,
|
|
@@ -3530,7 +3782,7 @@ function makeDecision(p, context) {
|
|
|
3530
3782
|
const suggestion = p.suggestionOverride ?? `Detected: "${p.name}" \u2014 an automation tool marker that anti-crawler systems immediately flag. Remove this pattern from your code.`;
|
|
3531
3783
|
return {
|
|
3532
3784
|
ruleId: "automation-signals",
|
|
3533
|
-
action: "
|
|
3785
|
+
action: "pass",
|
|
3534
3786
|
severity: p.severity,
|
|
3535
3787
|
reason: `Automation marker detected: "${p.name}". Context: "${context}..."`,
|
|
3536
3788
|
suggestion,
|
|
@@ -3605,7 +3857,7 @@ var fingerprintingRule = {
|
|
|
3605
3857
|
if (p.pattern.test(userCode)) {
|
|
3606
3858
|
return {
|
|
3607
3859
|
ruleId: "fingerprinting",
|
|
3608
|
-
action: "
|
|
3860
|
+
action: "pass",
|
|
3609
3861
|
severity: p.severity,
|
|
3610
3862
|
reason: `Browser fingerprinting API accessed: "${p.name}". Anti-crawler systems use this to identify your browser.`,
|
|
3611
3863
|
suggestion: p.suggestion,
|
|
@@ -3622,7 +3874,7 @@ var fingerprintingRule = {
|
|
|
3622
3874
|
var EVENT_PATTERNS = [
|
|
3623
3875
|
// ── Direct method calls (all isTrusted=false) ─────────
|
|
3624
3876
|
{ pattern: /\.click\s*\(\s*\)/, name: "el.click()", severity: "danger", errorCode: -32070, suggestion: "el.click() fires isTrusted=false events. Use page.click(selector) which uses Input.dispatchMouseEvent (isTrusted=true)." },
|
|
3625
|
-
{ pattern: /\.focus\s*\(\s*\)/, name: "el.focus()", severity: "
|
|
3877
|
+
{ pattern: /\.focus\s*\(\s*\)/, name: "el.focus()", severity: "warn", errorCode: -32071, suggestion: "el.focus() without user interaction is detectable. Use page.click(selector) which naturally focuses." },
|
|
3626
3878
|
{ pattern: /\.blur\s*\(\s*\)/, name: "el.blur()", severity: "danger", errorCode: -32071, suggestion: "el.blur() without user interaction. Avoid in automation scripts." },
|
|
3627
3879
|
{ pattern: /\.submit\s*\(\s*\)/, name: "el.submit()", severity: "danger", errorCode: -32072, suggestion: `form.submit() bypasses onSubmit handlers. Click the submit button: page.click('button[type="submit"]').` },
|
|
3628
3880
|
{ pattern: /\.reset\s*\(\s*\)/, name: "el.reset()", severity: "danger", errorCode: -32072, suggestion: "form.reset() without user action. Let the user clear fields manually." },
|
|
@@ -3637,7 +3889,7 @@ var EVENT_PATTERNS = [
|
|
|
3637
3889
|
{ pattern: /\.showPicker\s*\(\s*\)/, name: "HTMLInputElement.showPicker()", severity: "info", errorCode: -32076, suggestion: "Date/color picker shown without click \u2014 detectable." },
|
|
3638
3890
|
{ pattern: /\.reportValidity\s*\(\s*\)/, name: "el.reportValidity()", severity: "info", errorCode: -32076, suggestion: "Validity reporting without form submission attempt." },
|
|
3639
3891
|
// ── dispatchEvent with synthetic events (isTrusted=false) ──
|
|
3640
|
-
{ pattern: /dispatchEvent\s*\(\s*new\s+(?:Event|CustomEvent)\s*\(/, name: "dispatchEvent(new Event/CustomEvent)", severity: "
|
|
3892
|
+
{ pattern: /dispatchEvent\s*\(\s*new\s+(?:Event|CustomEvent)\s*\(/, name: "dispatchEvent(new Event/CustomEvent)", severity: "warn", errorCode: -32077, suggestion: "Synthetic events have isTrusted=false. Use Input.dispatch* CDP methods for trusted events." },
|
|
3641
3893
|
{ pattern: /dispatchEvent\s*\(\s*new\s+MouseEvent\s*\(/, name: "dispatchEvent(new MouseEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic mouse events (isTrusted=false). Use Input.dispatchMouseEvent CDP method." },
|
|
3642
3894
|
{ pattern: /dispatchEvent\s*\(\s*new\s+KeyboardEvent\s*\(/, name: "dispatchEvent(new KeyboardEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic keyboard events (isTrusted=false). Use Input.dispatchKeyEvent CDP method." },
|
|
3643
3895
|
{ pattern: /dispatchEvent\s*\(\s*new\s+FocusEvent\s*\(/, name: "dispatchEvent(new FocusEvent)", severity: "warn", errorCode: -32078, suggestion: "Synthetic focus events bypass user interaction." },
|
|
@@ -3666,7 +3918,9 @@ var eventSimulationRule = {
|
|
|
3666
3918
|
if (p.pattern.test(userCode)) {
|
|
3667
3919
|
return {
|
|
3668
3920
|
ruleId: "event-simulation",
|
|
3669
|
-
|
|
3921
|
+
// danger 级硬拦(el.click 等 100% 暴露的模式);
|
|
3922
|
+
// warning 级放行+提示(dispatchEvent 在部分合法场景如 DataTransfer paste 使用)
|
|
3923
|
+
action: p.severity === "danger" ? "block" : "pass",
|
|
3670
3924
|
severity: p.severity,
|
|
3671
3925
|
reason: `Event simulation detected: "${p.name}". Synthetic events have isTrusted=false and are 100% detectable.`,
|
|
3672
3926
|
suggestion: p.suggestion,
|
|
@@ -3725,7 +3979,7 @@ var emulationOverrideRule = {
|
|
|
3725
3979
|
if (ctx.method !== p.method) continue;
|
|
3726
3980
|
return {
|
|
3727
3981
|
ruleId: "emulation-override",
|
|
3728
|
-
action: "
|
|
3982
|
+
action: "pass",
|
|
3729
3983
|
severity: p.severity,
|
|
3730
3984
|
reason: `CDP emulation/override detected: "${p.name}". This creates detectable inconsistencies between the JS environment and real browser state.`,
|
|
3731
3985
|
suggestion: p.suggestion,
|
|
@@ -3755,7 +4009,7 @@ var networkAnomalyRule = {
|
|
|
3755
4009
|
if (!headerStr.includes("sec-ch-ua")) {
|
|
3756
4010
|
return {
|
|
3757
4011
|
ruleId: "network-anomaly",
|
|
3758
|
-
action: "
|
|
4012
|
+
action: "pass",
|
|
3759
4013
|
severity: "warn",
|
|
3760
4014
|
reason: "Network.setExtraHTTPHeaders called without Sec-CH-UA client hints \u2014 browser normally sends these.",
|
|
3761
4015
|
suggestion: "Modern browsers send Sec-CH-UA headers automatically. Adding custom headers without them creates detectable inconsistency.",
|
|
@@ -3769,7 +4023,7 @@ var networkAnomalyRule = {
|
|
|
3769
4023
|
case "Network.clearBrowserCache": {
|
|
3770
4024
|
return {
|
|
3771
4025
|
ruleId: "network-anomaly",
|
|
3772
|
-
action: "
|
|
4026
|
+
action: "pass",
|
|
3773
4027
|
severity: "warn",
|
|
3774
4028
|
reason: "Network.clearBrowserCache called \u2014 cache clearing mid-session is unnatural for real users.",
|
|
3775
4029
|
suggestion: "Avoid cache clearing during sessions. Start with a fresh profile if needed.",
|
|
@@ -3780,7 +4034,7 @@ var networkAnomalyRule = {
|
|
|
3780
4034
|
case "Network.clearBrowserCookies": {
|
|
3781
4035
|
return {
|
|
3782
4036
|
ruleId: "network-anomaly",
|
|
3783
|
-
action: "
|
|
4037
|
+
action: "pass",
|
|
3784
4038
|
severity: "warn",
|
|
3785
4039
|
reason: "Network.clearBrowserCookies called \u2014 wiping cookies mid-session is a scraper optimization.",
|
|
3786
4040
|
suggestion: "Cookies should only be cleared via normal browser flow (expiration, user action).",
|
|
@@ -3791,7 +4045,7 @@ var networkAnomalyRule = {
|
|
|
3791
4045
|
case "Network.setBlockedURLs": {
|
|
3792
4046
|
return {
|
|
3793
4047
|
ruleId: "network-anomaly",
|
|
3794
|
-
action: "
|
|
4048
|
+
action: "pass",
|
|
3795
4049
|
severity: "warn",
|
|
3796
4050
|
reason: "Network.setBlockedURLs blocks resource loading \u2014 this changes the page behavior and is detectable.",
|
|
3797
4051
|
suggestion: "Blocking images/fonts/etc creates measurable differences in performance and page rendering.",
|
|
@@ -3813,7 +4067,7 @@ var networkAnomalyRule = {
|
|
|
3813
4067
|
case "Fetch.enable": {
|
|
3814
4068
|
return {
|
|
3815
4069
|
ruleId: "network-anomaly",
|
|
3816
|
-
action: "
|
|
4070
|
+
action: "pass",
|
|
3817
4071
|
severity: "warn",
|
|
3818
4072
|
reason: "Fetch.enable intercepts all network requests \u2014 a man-in-the-middle approach used by scrapers.",
|
|
3819
4073
|
suggestion: "Do not use Fetch domain for network interception if avoiding detection.",
|
|
@@ -3853,7 +4107,7 @@ var pageLifecycleRule = {
|
|
|
3853
4107
|
if (interval < 100) {
|
|
3854
4108
|
return {
|
|
3855
4109
|
ruleId: "page-lifecycle",
|
|
3856
|
-
action: "
|
|
4110
|
+
action: "pass",
|
|
3857
4111
|
severity: "warn",
|
|
3858
4112
|
reason: `Multiple Page.navigate calls within ${interval}ms of each other \u2014 unnatural rapid navigation.`,
|
|
3859
4113
|
suggestion: "Add proper waits between navigations: wait for page load before navigating again.",
|
|
@@ -3870,7 +4124,7 @@ var pageLifecycleRule = {
|
|
|
3870
4124
|
if (state.lastNavTime > 0 && now - state.lastNavTime < 500) {
|
|
3871
4125
|
return {
|
|
3872
4126
|
ruleId: "page-lifecycle",
|
|
3873
|
-
action: "
|
|
4127
|
+
action: "pass",
|
|
3874
4128
|
severity: "danger",
|
|
3875
4129
|
reason: "Page.captureScreenshot called within 500ms of navigation \u2014 content extraction pattern.",
|
|
3876
4130
|
suggestion: "Wait for the page to fully render before taking screenshots: wait for load/networkidle.",
|
|
@@ -3881,7 +4135,7 @@ var pageLifecycleRule = {
|
|
|
3881
4135
|
if (state.screenshots > 3 && state.navigations.length < 2) {
|
|
3882
4136
|
return {
|
|
3883
4137
|
ruleId: "page-lifecycle",
|
|
3884
|
-
action: "
|
|
4138
|
+
action: "pass",
|
|
3885
4139
|
severity: "warn",
|
|
3886
4140
|
reason: "Multiple screenshots on a single page without navigation \u2014 suspicious extraction behavior.",
|
|
3887
4141
|
suggestion: "Consider if all screenshots are necessary.",
|
|
@@ -3894,7 +4148,7 @@ var pageLifecycleRule = {
|
|
|
3894
4148
|
case "Page.printToPDF": {
|
|
3895
4149
|
return {
|
|
3896
4150
|
ruleId: "page-lifecycle",
|
|
3897
|
-
action: "
|
|
4151
|
+
action: "pass",
|
|
3898
4152
|
severity: "danger",
|
|
3899
4153
|
reason: "Page.printToPDF called \u2014 this is a telltale scraper pattern that gives away automation intent.",
|
|
3900
4154
|
suggestion: "Avoid PDF generation. If you must, add significant delays and user-like interaction first.",
|
|
@@ -3906,7 +4160,7 @@ var pageLifecycleRule = {
|
|
|
3906
4160
|
if (state.lastNavTime > 0 && now - state.lastNavTime < 1e3) {
|
|
3907
4161
|
return {
|
|
3908
4162
|
ruleId: "page-lifecycle",
|
|
3909
|
-
action: "
|
|
4163
|
+
action: "pass",
|
|
3910
4164
|
severity: "warn",
|
|
3911
4165
|
reason: "Page.reload called immediately after navigate \u2014 unnatural fast-reload pattern.",
|
|
3912
4166
|
suggestion: "Introduce delays between navigation and reload to simulate human behavior.",
|
|
@@ -3920,7 +4174,7 @@ var pageLifecycleRule = {
|
|
|
3920
4174
|
if (state.navigations.length < 2) {
|
|
3921
4175
|
return {
|
|
3922
4176
|
ruleId: "page-lifecycle",
|
|
3923
|
-
action: "
|
|
4177
|
+
action: "pass",
|
|
3924
4178
|
severity: "info",
|
|
3925
4179
|
reason: "Page.close called after minimal interaction \u2014 zombie pages common in automation.",
|
|
3926
4180
|
suggestion: "Ensure meaningful interaction before closing pages.",
|
|
@@ -3936,7 +4190,7 @@ var pageLifecycleRule = {
|
|
|
3936
4190
|
if (state.evaluateCount > 50 && state.navigations.length === 0) {
|
|
3937
4191
|
return {
|
|
3938
4192
|
ruleId: "page-lifecycle",
|
|
3939
|
-
action: "
|
|
4193
|
+
action: "pass",
|
|
3940
4194
|
severity: "info",
|
|
3941
4195
|
reason: "50+ evaluate calls without any Page.navigate \u2014 data extraction without real browsing.",
|
|
3942
4196
|
suggestion: "Navigate to a real page first. Evaluate on about:blank is suspicious.",
|
|
@@ -3998,6 +4252,323 @@ function createRuleEngine(customRules) {
|
|
|
3998
4252
|
};
|
|
3999
4253
|
}
|
|
4000
4254
|
|
|
4255
|
+
// src/cdp-driver/connection.ts
|
|
4256
|
+
var CDPConnection = class _CDPConnection extends EventEmitter4 {
|
|
4257
|
+
ws;
|
|
4258
|
+
nextId = 1;
|
|
4259
|
+
/** CDP Guard:全部出站命令过规则引擎(env XBROWSER_CDP_GUARD=off 关闭)。
|
|
4260
|
+
* 173 条规则 / 9 模块:合成事件 block、指纹暴露警告、自动化信号拦截。 */
|
|
4261
|
+
static __guard;
|
|
4262
|
+
get guard() {
|
|
4263
|
+
if (_CDPConnection.__guard === void 0) {
|
|
4264
|
+
if (process.env.XBROWSER_CDP_GUARD === "off") {
|
|
4265
|
+
_CDPConnection.__guard = null;
|
|
4266
|
+
} else {
|
|
4267
|
+
try {
|
|
4268
|
+
const g = createRuleEngine();
|
|
4269
|
+
g.start();
|
|
4270
|
+
_CDPConnection.__guard = g;
|
|
4271
|
+
} catch {
|
|
4272
|
+
_CDPConnection.__guard = null;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
return _CDPConnection.__guard;
|
|
4277
|
+
}
|
|
4278
|
+
pending = /* @__PURE__ */ new Map();
|
|
4279
|
+
closed = false;
|
|
4280
|
+
closeReason = null;
|
|
4281
|
+
/** Default session ID for flat session protocol (Target.attachToTarget) */
|
|
4282
|
+
defaultSessionId;
|
|
4283
|
+
constructor(wsOrUrl, sessionId) {
|
|
4284
|
+
super();
|
|
4285
|
+
this.setMaxListeners(0);
|
|
4286
|
+
this.defaultSessionId = sessionId;
|
|
4287
|
+
if (typeof wsOrUrl === "string") {
|
|
4288
|
+
const wsOptions = /^wss:\/\/\d+\.\d+\.\d+\.\d+/.test(wsOrUrl) ? { rejectUnauthorized: false } : void 0;
|
|
4289
|
+
this.ws = new WebSocket(wsOrUrl, wsOptions);
|
|
4290
|
+
} else {
|
|
4291
|
+
this.ws = wsOrUrl;
|
|
4292
|
+
}
|
|
4293
|
+
this.bindWebSocket();
|
|
4294
|
+
this.startKeepalive();
|
|
4295
|
+
}
|
|
4296
|
+
/** Send periodic WS pings to prevent idle-timeout disconnects (e.g. CF's 100s).
|
|
4297
|
+
* Also detects dead connections: if a pong isn't received within 10s of a
|
|
4298
|
+
* ping, the connection is considered dead and forcibly closed. */
|
|
4299
|
+
keepaliveTimer = null;
|
|
4300
|
+
pongTimer = null;
|
|
4301
|
+
/** 最近一次收到任何 WS 消息的时间——有数据流动即证明连接存活 */
|
|
4302
|
+
lastIncomingAt = Date.now();
|
|
4303
|
+
startKeepalive() {
|
|
4304
|
+
this.ws.on("pong", () => {
|
|
4305
|
+
if (this.pongTimer) {
|
|
4306
|
+
clearTimeout(this.pongTimer);
|
|
4307
|
+
this.pongTimer = null;
|
|
4308
|
+
}
|
|
4309
|
+
});
|
|
4310
|
+
this.keepaliveTimer = setInterval(() => {
|
|
4311
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
4312
|
+
if (!this.pongTimer) {
|
|
4313
|
+
this.pongTimer = setTimeout(() => {
|
|
4314
|
+
const recentlyActive = Date.now() - this.lastIncomingAt < 3e4;
|
|
4315
|
+
if (recentlyActive) {
|
|
4316
|
+
this.pongTimer = null;
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
if (!this.closed) {
|
|
4320
|
+
this.closed = true;
|
|
4321
|
+
this.closeReason = "keepalive timeout (no pong and idle >30s)";
|
|
4322
|
+
try {
|
|
4323
|
+
this.ws.terminate();
|
|
4324
|
+
} catch {
|
|
4325
|
+
}
|
|
4326
|
+
for (const [, pending] of this.pending) {
|
|
4327
|
+
clearTimeout(pending.timeout);
|
|
4328
|
+
pending.reject(new Error("Connection dead: keepalive timeout"));
|
|
4329
|
+
}
|
|
4330
|
+
this.pending.clear();
|
|
4331
|
+
this.emit("disconnect");
|
|
4332
|
+
}
|
|
4333
|
+
}, 6e4);
|
|
4334
|
+
}
|
|
4335
|
+
this.ws.ping?.();
|
|
4336
|
+
} else if (this.closed) {
|
|
4337
|
+
if (this.keepaliveTimer) clearInterval(this.keepaliveTimer);
|
|
4338
|
+
this.keepaliveTimer = null;
|
|
4339
|
+
}
|
|
4340
|
+
}, 3e4);
|
|
4341
|
+
}
|
|
4342
|
+
/** Wait for the connection to be fully open */
|
|
4343
|
+
async ready() {
|
|
4344
|
+
if (this.ws.readyState === WebSocket.OPEN) return;
|
|
4345
|
+
if (this.ws.readyState === WebSocket.CLOSED || this.ws.readyState === WebSocket.CLOSING) {
|
|
4346
|
+
throw new Error(`WebSocket already closed: ${this.closeReason ?? "unknown"}`);
|
|
4347
|
+
}
|
|
4348
|
+
return new Promise((resolve, reject) => {
|
|
4349
|
+
const onOpen = () => {
|
|
4350
|
+
this.ws.off("error", onError);
|
|
4351
|
+
resolve();
|
|
4352
|
+
};
|
|
4353
|
+
const onError = (err) => {
|
|
4354
|
+
this.ws.off("open", onOpen);
|
|
4355
|
+
reject(err);
|
|
4356
|
+
};
|
|
4357
|
+
this.ws.once("open", onOpen);
|
|
4358
|
+
this.ws.once("error", onError);
|
|
4359
|
+
});
|
|
4360
|
+
}
|
|
4361
|
+
/** Is the underlying WebSocket alive? */
|
|
4362
|
+
get isOpen() {
|
|
4363
|
+
return !this.closed && this.ws.readyState === WebSocket.OPEN;
|
|
4364
|
+
}
|
|
4365
|
+
/**
|
|
4366
|
+
* Send a CDP command and await its response.
|
|
4367
|
+
*
|
|
4368
|
+
* @param method — CDP domain.method (e.g. "Page.navigate")
|
|
4369
|
+
* @param params — method parameters
|
|
4370
|
+
* @param sessionId — optional flat session ID for sub-targets
|
|
4371
|
+
* @param timeoutMs — response timeout (default: 30s;重 SPA 加载期间主线程忙,
|
|
4372
|
+
* evaluate 可排队 >30s——Runtime.evaluate 单独放宽到 90s)
|
|
4373
|
+
* @returns the `result` field from the CDP response
|
|
4374
|
+
*/
|
|
4375
|
+
async send(method, params, sessionId, timeoutMs) {
|
|
4376
|
+
const effectiveTimeout = timeoutMs ?? (method === "Runtime.evaluate" ? 9e4 : 3e4);
|
|
4377
|
+
if (this.guard) {
|
|
4378
|
+
const decision = this.guard.evaluate({
|
|
4379
|
+
method,
|
|
4380
|
+
params: params ?? {},
|
|
4381
|
+
sessionId: sessionId ?? this.defaultSessionId ?? "",
|
|
4382
|
+
direction: "client\u2192browser"
|
|
4383
|
+
});
|
|
4384
|
+
if (decision && decision.action === "block") {
|
|
4385
|
+
throw new Error(`[CDP-Guard] ${decision.reason}${decision.suggestion ? " | \u66FF\u4EE3: " + decision.suggestion : ""} [${decision.ruleId}]`);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
if (this.closed) {
|
|
4389
|
+
throw new Error(`CDP connection closed: ${this.closeReason ?? "unknown"}`);
|
|
4390
|
+
}
|
|
4391
|
+
if (!this.isOpen) {
|
|
4392
|
+
throw new Error(`CDP connection not open (state: ${this.ws.readyState})`);
|
|
4393
|
+
}
|
|
4394
|
+
const id = this.nextId++;
|
|
4395
|
+
const sid = sessionId ?? this.defaultSessionId;
|
|
4396
|
+
const message = { id, method };
|
|
4397
|
+
if (params !== void 0) message.params = params;
|
|
4398
|
+
if (sid !== void 0) message.sessionId = sid;
|
|
4399
|
+
return new Promise((resolve, reject) => {
|
|
4400
|
+
const timeout = setTimeout(() => {
|
|
4401
|
+
this.pending.delete(id);
|
|
4402
|
+
reject(new Error(`CDP timeout: ${method} (${effectiveTimeout}ms)`));
|
|
4403
|
+
}, effectiveTimeout);
|
|
4404
|
+
this.pending.set(id, {
|
|
4405
|
+
resolve: (v) => {
|
|
4406
|
+
clearTimeout(timeout);
|
|
4407
|
+
this.pending.delete(id);
|
|
4408
|
+
resolve(v);
|
|
4409
|
+
},
|
|
4410
|
+
reject: (err) => {
|
|
4411
|
+
clearTimeout(timeout);
|
|
4412
|
+
this.pending.delete(id);
|
|
4413
|
+
reject(err);
|
|
4414
|
+
},
|
|
4415
|
+
method,
|
|
4416
|
+
timeout
|
|
4417
|
+
});
|
|
4418
|
+
const data = JSON.stringify(message);
|
|
4419
|
+
try {
|
|
4420
|
+
this.ws.send(data);
|
|
4421
|
+
} catch (err) {
|
|
4422
|
+
clearTimeout(timeout);
|
|
4423
|
+
this.pending.delete(id);
|
|
4424
|
+
reject(new Error(`CDP send failed: ${method} \u2014 ${err instanceof Error ? err.message : String(err)}`));
|
|
4425
|
+
}
|
|
4426
|
+
});
|
|
4427
|
+
}
|
|
4428
|
+
/**
|
|
4429
|
+
* Subscribe to a CDP event.
|
|
4430
|
+
*
|
|
4431
|
+
* @param event — full event name (e.g. "Page.frameNavigated")
|
|
4432
|
+
* @param handler — called with the event params
|
|
4433
|
+
* @param sessionId — optional session filter
|
|
4434
|
+
*/
|
|
4435
|
+
on(event, handler) {
|
|
4436
|
+
return super.on(event, handler);
|
|
4437
|
+
}
|
|
4438
|
+
once(event, handler) {
|
|
4439
|
+
return super.once(event, handler);
|
|
4440
|
+
}
|
|
4441
|
+
/** Remove an event listener */
|
|
4442
|
+
off(event, handler) {
|
|
4443
|
+
super.off(event, handler);
|
|
4444
|
+
return this;
|
|
4445
|
+
}
|
|
4446
|
+
/**
|
|
4447
|
+
* Subscribe to a CDP event for a specific session.
|
|
4448
|
+
* Returns an unsubscribe function.
|
|
4449
|
+
*/
|
|
4450
|
+
subscribe(event, sessionId, handler) {
|
|
4451
|
+
const wrapper = (params, sid) => {
|
|
4452
|
+
if (sid === sessionId || !sessionId && !sid) handler(params);
|
|
4453
|
+
};
|
|
4454
|
+
this.on(event, wrapper);
|
|
4455
|
+
return () => this.off(event, wrapper);
|
|
4456
|
+
}
|
|
4457
|
+
/** Close the WebSocket */
|
|
4458
|
+
async close() {
|
|
4459
|
+
if (this.closed) return;
|
|
4460
|
+
this.closed = true;
|
|
4461
|
+
this.closeReason = "closed by caller";
|
|
4462
|
+
if (this.keepaliveTimer) {
|
|
4463
|
+
clearInterval(this.keepaliveTimer);
|
|
4464
|
+
this.keepaliveTimer = null;
|
|
4465
|
+
}
|
|
4466
|
+
if (this.pongTimer) {
|
|
4467
|
+
clearTimeout(this.pongTimer);
|
|
4468
|
+
this.pongTimer = null;
|
|
4469
|
+
}
|
|
4470
|
+
for (const [id, pending] of this.pending) {
|
|
4471
|
+
clearTimeout(pending.timeout);
|
|
4472
|
+
pending.reject(new Error(`Connection closed: ${pending.method}`));
|
|
4473
|
+
this.pending.delete(id);
|
|
4474
|
+
}
|
|
4475
|
+
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
4476
|
+
this.ws.close(1e3, "normal closure");
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
/** Set the default session ID for flat protocol */
|
|
4480
|
+
setDefaultSessionId(sid) {
|
|
4481
|
+
this.defaultSessionId = sid;
|
|
4482
|
+
}
|
|
4483
|
+
// ── Private ─────────────────────────────────────────────────
|
|
4484
|
+
bindWebSocket() {
|
|
4485
|
+
this.ws.on("message", (raw) => {
|
|
4486
|
+
this.lastIncomingAt = Date.now();
|
|
4487
|
+
let msg;
|
|
4488
|
+
try {
|
|
4489
|
+
msg = JSON.parse(raw.toString());
|
|
4490
|
+
} catch {
|
|
4491
|
+
return;
|
|
4492
|
+
}
|
|
4493
|
+
if (msg.id !== void 0) {
|
|
4494
|
+
const pending = this.pending.get(msg.id);
|
|
4495
|
+
if (!pending) return;
|
|
4496
|
+
if (msg.error) {
|
|
4497
|
+
pending.reject(new CDPProtocolError(msg.error.code, msg.error.message, pending.method));
|
|
4498
|
+
} else {
|
|
4499
|
+
pending.resolve(msg.result ?? {});
|
|
4500
|
+
}
|
|
4501
|
+
return;
|
|
4502
|
+
}
|
|
4503
|
+
if (msg.method) {
|
|
4504
|
+
this.emit(msg.method, msg.params ?? {}, msg.sessionId);
|
|
4505
|
+
this.emit("*", msg.method, msg.params ?? {}, msg.sessionId);
|
|
4506
|
+
}
|
|
4507
|
+
});
|
|
4508
|
+
this.ws.on("close", (code, reason) => {
|
|
4509
|
+
if (this.closed) return;
|
|
4510
|
+
this.closed = true;
|
|
4511
|
+
this.closeReason = `WebSocket closed: ${code} ${reason?.toString() ?? ""}`.trim();
|
|
4512
|
+
if (this.keepaliveTimer) {
|
|
4513
|
+
clearInterval(this.keepaliveTimer);
|
|
4514
|
+
this.keepaliveTimer = null;
|
|
4515
|
+
}
|
|
4516
|
+
for (const [id, pending] of this.pending) {
|
|
4517
|
+
clearTimeout(pending.timeout);
|
|
4518
|
+
pending.reject(new Error(`Connection closed: ${pending.method}`));
|
|
4519
|
+
this.pending.delete(id);
|
|
4520
|
+
}
|
|
4521
|
+
this.emit("disconnect");
|
|
4522
|
+
});
|
|
4523
|
+
this.ws.on("error", (err) => {
|
|
4524
|
+
if (this.closed) return;
|
|
4525
|
+
this.emit("ws-error", err);
|
|
4526
|
+
});
|
|
4527
|
+
}
|
|
4528
|
+
};
|
|
4529
|
+
var CDPProtocolError = class extends Error {
|
|
4530
|
+
code;
|
|
4531
|
+
method;
|
|
4532
|
+
data;
|
|
4533
|
+
constructor(code, message, method, data) {
|
|
4534
|
+
super(`CDP error [${code}] in ${method}: ${message}`);
|
|
4535
|
+
this.name = "CDPProtocolError";
|
|
4536
|
+
this.code = code;
|
|
4537
|
+
this.method = method;
|
|
4538
|
+
this.data = data;
|
|
4539
|
+
}
|
|
4540
|
+
};
|
|
4541
|
+
|
|
4542
|
+
// src/cdp-driver/index.ts
|
|
4543
|
+
async function launch(options = {}) {
|
|
4544
|
+
let wsEndpoint;
|
|
4545
|
+
let childProcess;
|
|
4546
|
+
let tmpDir;
|
|
4547
|
+
if (options.cdpEndpoint) {
|
|
4548
|
+
wsEndpoint = await connectToCDP(options.cdpEndpoint);
|
|
4549
|
+
} else {
|
|
4550
|
+
const result = await launchChrome({
|
|
4551
|
+
executablePath: options.executablePath,
|
|
4552
|
+
headless: options.headless,
|
|
4553
|
+
args: options.args,
|
|
4554
|
+
userDataDir: options.userDataDir,
|
|
4555
|
+
timeout: options.timeout,
|
|
4556
|
+
env: options.env
|
|
4557
|
+
});
|
|
4558
|
+
wsEndpoint = result.wsEndpoint;
|
|
4559
|
+
childProcess = result.process;
|
|
4560
|
+
tmpDir = result.tmpDir;
|
|
4561
|
+
}
|
|
4562
|
+
const conn = new CDPConnection(wsEndpoint);
|
|
4563
|
+
await conn.ready();
|
|
4564
|
+
const httpEndpoint = options.cdpEndpoint && !options.cdpEndpoint.startsWith("ws") ? options.cdpEndpoint : void 0;
|
|
4565
|
+
const browser = new XBBrowserImpl(conn, childProcess, tmpDir, httpEndpoint);
|
|
4566
|
+
return { browser, wsEndpoint };
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4569
|
+
// src/cdp-interceptor/proxy.ts
|
|
4570
|
+
import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
|
|
4571
|
+
|
|
4001
4572
|
// src/cdp-interceptor/logger.ts
|
|
4002
4573
|
function createLogger(config) {
|
|
4003
4574
|
const buffer = [];
|
|
@@ -4685,15 +5256,18 @@ function deleteSessionDiskMeta(name) {
|
|
|
4685
5256
|
async function isSessionPageAlive(session) {
|
|
4686
5257
|
const page = session.page;
|
|
4687
5258
|
if (!page || typeof page.evaluate !== "function") return false;
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
5259
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
5260
|
+
try {
|
|
5261
|
+
await Promise.race([
|
|
5262
|
+
page.evaluate("1"),
|
|
5263
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("liveness probe timeout")), 1500))
|
|
5264
|
+
]);
|
|
5265
|
+
return true;
|
|
5266
|
+
} catch {
|
|
5267
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
5268
|
+
}
|
|
4696
5269
|
}
|
|
5270
|
+
return false;
|
|
4697
5271
|
}
|
|
4698
5272
|
async function findOrRestoreSession(name, cdpEndpoint) {
|
|
4699
5273
|
const inMem = findSession(name);
|
|
@@ -5116,7 +5690,7 @@ async function closeSessionByName(name) {
|
|
|
5116
5690
|
} catch {
|
|
5117
5691
|
}
|
|
5118
5692
|
try {
|
|
5119
|
-
const { SessionRecorder } = await import("./session-recorder-
|
|
5693
|
+
const { SessionRecorder } = await import("./session-recorder-SLDBENVF.js");
|
|
5120
5694
|
SessionRecorder.cleanup(session.name);
|
|
5121
5695
|
} catch {
|
|
5122
5696
|
}
|