├── .github └── FUNDING.yml ├── .gitignore ├── .nojekyll ├── LICENSE ├── README.md ├── assets ├── brotector_error_message.png ├── example_screenshot_headless.png └── test.pdf ├── brotector.js ├── favicon.ico ├── index.html ├── main.js ├── package.json ├── requirements_test.txt ├── style.css ├── tests ├── conftest.py ├── test_driverless.py ├── test_nodriver.py ├── test_playwright.py ├── test_pyppeteer.py ├── test_selenium.py └── utils.py └── tests_nodejs ├── test_crash.mjs ├── test_hero.mjs ├── test_puppeteer.mjs └── utils.mjs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [kaliiiiiiiiii] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: kaliiii # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */yolov8m-seg.pt 2 | yolov8m-seg.pt 3 | downloaded_files/* 4 | .idea/* 5 | */__pycache__/* 6 | node_modules/* 7 | package-lock.json -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Aurin Aegerter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brotector 2 | 3 | 4 | Detects or even **crashes** (❌) webdrivers such as: 5 | - [x] [driverless](https://github.com/kaliiiiiiiiii/Selenium-Driverless) 6 | - [ ] **with [cdp-patches](https://github.com/Kaliiiiiiiiii-Vinyzu/CDP-Patches)** 7 | - [x] [selenium](https://github.com/SeleniumHQ/selenium/tree/trunk/py#selenium-client-driver) ❌ 8 | - [x] [undetected-chromedriver](https://github.com/ultrafunkamsterdam/undetected-chromedriver) 9 | - [x] [seleniumbase](https://github.com/seleniumbase/SeleniumBase) 10 | - [x] [puppeteer](https://github.com/puppeteer/puppeteer) ❌ 11 | - [ ] [puppeteer-extra-stealth](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth#puppeteer-extra-plugin-stealth---) 12 | - [x] [pyppeteer](https://github.com/pyppeteer/pyppeteer) 13 | - [x] [pyppeteer-stealth](https://github.com/MeiK2333/pyppeteer_stealth) 14 | - [x] [playwright](https://github.com/microsoft/playwright-python) ❌ 15 | - [x] [undetected-playwright](https://github.com/kaliiiiiiiiii/undetected-playwright-python) (buggy) 16 | - [ ] with [cdp-patches](https://github.com/Kaliiiiiiiiii-Vinyzu/CDP-Patches) (no test yet) 17 | - [x] [botright](https://github.com/Vinyzu/Botright) 18 | - [x] with [uc-playwright](https://github.com/kaliiiiiiiiii/undetected-playwright-python) (buggy) 19 | - [ ] with [cdp-patches](https://github.com/Kaliiiiiiiiii-Vinyzu/CDP-Patches) (no test yet) 20 | - [x] [nodriver](https://github.com/ultrafunkamsterdam/nodriver) 21 | - [x] [@ulixee/hero](https://github.com/ulixee/hero) 22 | 23 | For the tests, each webdriver has at least to click the button with the **ID** `clickHere` 24 | 25 | 26 | ## Detections 27 | 28 |

29 | 30 | 31 |

32 | 33 | #### navigator.webdriver 34 | 35 | `navigator.webdriver` (JavaScript) is set to `true` 36 | 37 | ----- 38 | #### runtime.enabled 39 | 40 | [`Runtime`](https://chromedevtools.github.io/devtools-protocol/tot/Runtime) is enabled \ 41 | score here refers to the certainty of the 42 | occurs when: 43 | - [`Runtime.enable`](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-enable) or [`Console.enable`](https://chromedevtools.github.io/devtools-protocol/tot/Console/#method-enable) (CDP) has been called (most libraries do that, type=webdriver) 44 | - the user opens the devtools (type=devtools) 45 | 46 | ----- 47 | #### Input.cordinatesLeak 48 | occurs due to [crbug#1477537](https://bugs.chromium.org/p/chromium/issues/detail?id=1477537) \ 49 | [CDP-Patches](https://github.com/Kaliiiiiiiiii-Vinyzu/CDP-Patches) can be used to bypass this 50 | 51 | ----- 52 | #### window.cdc 53 | a leak specific to **chromedriver** (selenium) \ 54 | see [stackoverflow-answer](https://stackoverflow.com/a/75776883/20443541) 55 | 56 | ---- 57 | #### "Input.untrusted" 58 | Mouse event not dispatched by a user detected 59 | see [`Event.isTrusted`](https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted) property 60 | 61 | ---- 62 | #### canvasMouseVisualizer 63 | `CanvasRenderingContext2D.arc` has been called with 64 | - cordinates +-5px at current mouse position 65 | - canvas +-1px covers the whole page 66 | - canvas passes pointerEvents through 67 | 68 | ---- 69 | #### UAOverride 70 | [`navigator.userAgentData.getHighEntropyValues`](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues) has empty data \ 71 | (type=`HighEntropyValues.empty`, UA meaning UserAgent) 72 | 73 | ---- 74 | #### SeleniumScriptInjection 75 | - Detects when selenium tries to [inject a script](https://github.com/kaliiiiiiiiii/brotector/issues/6) (even used for finding elements) 76 | - makes selenium (any chromedriver-based framework) crash (bypassable for testing [`?crash=false`](https://kaliiiiiiiiii.github.io/brotector/?crash=false)) 77 | - just **don't use selenium** lol 78 | 79 | ---- 80 | #### PWinitScript 81 | - detects `playwright>=1.46.1`, induced with [commit](https://github.com/microsoft/playwright/commit/c9e673c6dca746384338ab6bb0cf63c7e7caa9b2#diff-087773eea292da9db5a3f27de8f1a2940cdb895383ad750c3cd8e01772a35b40R909-R924) 82 | 83 | ---- 84 | #### stack.signature 85 | detects injected javascript based on the stack trace in hooks 86 | 87 | ---- 88 | #### pdfStyle 89 | a detection regarding PDF rendering, 90 | specific to puppeteer [github issue](https://github.com/kaliiiiiiiiii/brotector/issues/5) or `--enable-field-trial-config` \ 91 | Note: There might be false-positives 92 | 93 | 94 | ---- 95 | #### popupCrash 96 | crashes chrome when automated with playwright or puppeteer due to [crbug#340836884](https://issues.chromium.org/issues/340836884) 97 | (bypassable for testing [`?crash=false`](https://kaliiiiiiiiii.github.io/brotector/?crash=false)) 98 | 99 | ## Contribution 100 | feel free to 101 | - open `[feature request]`s for driver detections 102 | - open PRs 103 | - use the discussions 104 | 105 | ## Licence 106 | see [LICENSE](https://github.com/kaliiiiiiiiii/brotector/blob/master/LICENSE) 107 | 108 | ## Author & Copyright 109 | 110 | Aurin Aegerter (aka Steve, kaliiiiiiiiii) 111 | 112 | ## Thanks // References 113 | - [selenium-detector](https://github.com/HMaker/HMaker.github.io/tree/master/selenium-detector) 114 | - [jdetects](https://github.com/zswang/jdetects) 115 | - thanks @ProseccoRider - some further `Runtime.enable` detection insights 116 | -------------------------------------------------------------------------------- /assets/brotector_error_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/brotector/98b330999e9928a226ca0c8254dddf909d1583bd/assets/brotector_error_message.png -------------------------------------------------------------------------------- /assets/example_screenshot_headless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/brotector/98b330999e9928a226ca0c8254dddf909d1583bd/assets/example_screenshot_headless.png -------------------------------------------------------------------------------- /assets/test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/brotector/98b330999e9928a226ca0c8254dddf909d1583bd/assets/test.pdf -------------------------------------------------------------------------------- /brotector.js: -------------------------------------------------------------------------------- 1 | const chromedriverSourceMatches = [ 2 | "WebDriver", "W3C", "Execute-Script", "cdc_adoQpoasnfa76pfcZLmcfl", "Chromium", "shadow-6066-11e4-a52e-4f735466cecf", 3 | "element-6066-11e4-a52e-4f735466cecf", "STALE_ELEMENT_REFERENCE", "crbug.com/40229283", 4 | "shadow root is detached from the current frame","stale element not found in the current frame"] 5 | 6 | const stackScriptInjectionMatches = { 7 | "pyppeteer":" at [\\s\\S]* \\(__pyppeteer_evaluation_script__:[0-9]+:[0-9]+\\)", 8 | "puppeteer":" at [\\s\\S]* \\(__puppeteer_evaluation_script__:[0-9]+:[0-9]+\\)", 9 | "puppeteer":" at pptr:evaluate;file%3A%2F%2F%2F[\\s\\S]*%3A[0-9]+%3A[0-9]+:[0-9]+:[0-9]+", 10 | "puppeteerPluginStealth":" at newHandler\\. \\[as apply\\] \\((:[0-9]+:[0-9]+|eval at \\(:[0-9]+:[0-9]+\\), :[0-9]+:[0-9]+)\\)" 11 | } 12 | 13 | const hookers = [ 14 | ["HTMLElement", "click"], 15 | ["HTMLElement","querySelector"], 16 | ["HTMLElement","querySelectorAll"], 17 | ] 18 | 19 | const brotectorBanner = ` 20 | 21 | 22 | You've been detected by 23 | ################################################################### 24 | # ____ _ _ # 25 | # | __ ) _ __ ___ | |_ ___ ___ | |_ ___ _ __ # 26 | # | _ \\ | '__| / _ \\ | __| / _ \\ / __| | __| / _ \\ | '__| # 27 | # | |_) | | | | (_) | | |_ | __/ | (__ | |_ | (_) | | | # 28 | # |____/ |_| \\___/ \\__| \\___| \\___| \\__| \\___/ |_| # 29 | ################################################################### 30 | https://github.com/kaliiiiiiiiii/brotector 31 | 32 | ` 33 | 34 | function getDebuggerTiming(){ 35 | var start = globalThis.performance.now(); 36 | debugger; 37 | return (globalThis.performance.now()-start) 38 | } 39 | function isEmpty() { 40 | for (var prop in this) if (this.hasOwnProperty(prop)) return false; 41 | return true; 42 | }; 43 | 44 | async function getHighEntropyValues(){ 45 | const n = globalThis.navigator? globalThis.navigator: globalThis.WorkerNavigator 46 | data = await n.userAgentData.getHighEntropyValues([ 47 | "architecture", 48 | "bitness", 49 | "formFactor", 50 | "model", 51 | "platform", 52 | "platformVersion", 53 | "uaFullVersion", 54 | "wow64" 55 | ]) 56 | data["userAgent"] = n.userAgent 57 | return data 58 | } 59 | 60 | function get_worker_response(fn, timeout=undefined) { 61 | try { 62 | const URL = window.URL || window.webkitURL; 63 | var fn = "self.onmessage=async function(e){postMessage(await (" + fn.toString() + ")())}"; 64 | var blob; 65 | try { 66 | blob = new Blob([fn], { type: "application/javascript" }); 67 | } catch (e) { 68 | // Backwards-compatibility 69 | window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder; 70 | blob = new BlobBuilder(); 71 | blob.append(response); 72 | blob = blob.getBlob(); 73 | } 74 | var url = URL.createObjectURL(blob); 75 | var worker = new Worker(url); 76 | var _promise = new Promise((resolve, reject) => { 77 | if(timeout !== undefined){setTimeout(()=>{worker.terminate(); reject(new Error("timeout"))}, timeout)} 78 | worker.onmessage = (m) => { 79 | worker.terminate(); 80 | resolve(m.data); 81 | }; 82 | worker.onerror = (e)=>{reject(new Error("Worker onerror"))} 83 | }); 84 | worker.postMessage("call"); 85 | return _promise; 86 | } catch (e) { 87 | return new Promise((resolve, reject) => { 88 | reject(e); 89 | }); 90 | } 91 | } 92 | 93 | const startTime = window.performance.now(); 94 | class Brotector { 95 | constructor(on_detection, interval=50, crash=true) { 96 | // on_detection(data:dict) 97 | this._isMouseHooked = false 98 | 99 | this.on_detection = on_detection 100 | this.crash = crash 101 | this.detections = [] 102 | this.mousePos = [0, 0] 103 | this._detections = [] 104 | this.interval = interval 105 | this.init_done = this.init() 106 | this._doing_devtoolsTest = false 107 | this.devtools_open = false 108 | this._runtime_detected = false 109 | this._canvasMouseVisualizer = false 110 | this._lastStackLookupCount = 0 111 | this._nameLookupCount = 0 112 | this.__pwInitScripts = false 113 | this.__playwright__binding__ = false 114 | } 115 | log(data){ 116 | data["msSinceLoad"] = window.performance.now() - startTime; 117 | this.detections.push(data) 118 | this._detections.push(data.detection) 119 | this.on_detection(data) 120 | } 121 | async init(){ 122 | this.test_navigator_webdriver() 123 | this.test_runtimeEnabled() 124 | this.test_PWinitScripts() 125 | this.test_window_cdc() 126 | this.test_HighEntropyValues() 127 | this.hook_mouseEvents() 128 | this.hook_canvasVisualize() 129 | this.hook_SeleniumScriptInjection() 130 | if(this.crash){this.test_popupCrash()} 131 | await this.test_pdfStyle() 132 | 133 | for (const [obj, func] of hookers){ 134 | this.hookFunc(obj, func, ()=>{}) 135 | } 136 | 137 | Object.defineProperty(Error.prototype, 'name', { 138 | configurable: false, 139 | enumerable: false, 140 | get: (() => {this._nameLookupCount += 1; return "Error"}).bind(this) 141 | }); 142 | // instead of setInterval 143 | (async () => {while(true){ 144 | this.intervalled.bind(this)(); 145 | await new Promise( 146 | ((resolve)=>{ 147 | setTimeout(resolve, this.interval) 148 | }).bind(this) 149 | ) 150 | } 151 | }).bind(this)() 152 | 153 | await new Promise((resolve)=>{setTimeout(resolve, 200)}) 154 | return this.detections 155 | } 156 | async intervalled(){ 157 | this.test_runtimeEnabled() 158 | this.test_PWinitScripts() 159 | } 160 | hookFunc(obj, func, callback){ 161 | const proxy = new Proxy(globalThis[obj].prototype[func], { 162 | apply: ((target, thisArg, argumentsList) => { 163 | try{this.test_stack(`${obj}:${func}`, argumentsList)}catch(e){console.error(e)}; 164 | callback(target, thisArg, argumentsList) 165 | return Reflect.apply(target, thisArg, argumentsList) 166 | }).bind(this) 167 | } 168 | ); 169 | Object.defineProperty(globalThis[obj].prototype, func, {value:proxy}) 170 | } 171 | test_stack(hook, args){ 172 | var stack 173 | try{throw Error()}catch(e){stack = e.stack} 174 | for (const line of stack.split("\n")){ 175 | for (const [type, regex] of Object.entries(stackScriptInjectionMatches)){ 176 | if(line.match(regex)){ 177 | this.log({detection:"stack.signature", type:type, score:1, data:{stack:stack, hook:hook}}) 178 | } 179 | } 180 | } 181 | } 182 | test_navigator_webdriver(){ 183 | if(navigator.webdriver === true){ 184 | this.log({detection:"navigator.webdriver", score:1}) 185 | } 186 | } 187 | test_popupCrash(){ 188 | const f = document.createElement("iframe"); 189 | f.src = "data:text/html;charset=utf-8,

", 190 | f.style.height = 0 191 | f.style.width = 0 192 | f.style.opacity = 0 193 | document.body.appendChild(f) 194 | try{f.contentWindow.open("", "", "top=9999,left=9999,width=1,height=1")}finally{ 195 | document.body.removeChild(f) 196 | } 197 | } 198 | test_window_cdc(){ 199 | let matches = [] 200 | for(let prop in window) { 201 | prop.match(/cdc_[a-z0-9]/ig) && matches.push(prop) 202 | } 203 | if(matches.length > 0){ 204 | this.log({detection:"window.cdc", data:matches, score:1}) 205 | } 206 | } 207 | test_PWinitScripts(){ 208 | const keys = ["__pwInitScripts", "__playwright__binding__"] 209 | var key 210 | for (key of keys){ 211 | if((globalThis[key] !== undefined) && !this[key]){ 212 | this[key] = true 213 | this.log({detection:"PWinitScript", data:{"value":globalThis[key]}, score:1, type:key}) 214 | } 215 | } 216 | 217 | } 218 | async test_runtimeEnabled() { 219 | 220 | const key = "runtime.enabled" 221 | var type = "webdriver" 222 | var score = 1 223 | 224 | if (this._lastStackLookupCount === 0){ 225 | 226 | // stacklookup 227 | var stackLookupCount = 0 228 | const e = new Error() 229 | Object.defineProperty(e, 'stack', { 230 | configurable: false, 231 | enumerable: false, 232 | get: function() { 233 | stackLookupCount += 1 234 | return ""; 235 | } 236 | }); 237 | var c = undefined 238 | try{c = console.context("Brotector: ");}catch{} 239 | if (c===undefined){c=console} 240 | c.debug(e); 241 | this._nameLookupCount = 0 242 | c.debug(new Error("")) 243 | const nameLookupCount = this._nameLookupCount 244 | if(stackLookupCount > 0 || nameLookupCount >= 2){ 245 | if(this._doing_devtoolsTest){return} 246 | this._doing_devtoolsTest = true 247 | var start = globalThis.performance.now(); 248 | var time = undefined 249 | await new Promise((resolve) => {setTimeout(resolve, 200)}) 250 | try{var time = await get_worker_response(getDebuggerTiming, 200)} 251 | catch(e){if(e.message !== "timeout"){throw e}} 252 | 253 | if(time === undefined){time = globalThis.performance.now()-start} 254 | c.clear() 255 | if(stackLookupCount > 1 && time>180 && nameLookupCount >= 2){ 256 | type = "devtools" 257 | score = 0.1 258 | }else if((stackLookupCount > 1 || nameLookupCount === 3) && time>180){ 259 | type = "devtools" 260 | score = 0.2 261 | }else if (time>180){ 262 | score = 0.3 263 | type = "devtools" 264 | this.devtools_open = true 265 | }else{this.devtools_open = false} 266 | 267 | this.log({detection:key, score:score, "type":type, 268 | data:{ 269 | "stackLookupCount":stackLookupCount, 270 | "nameLookupCount":nameLookupCount 271 | } 272 | } 273 | ) 274 | this._doing_devtoolsTest = false 275 | this._lastStackLookupCount = stackLookupCount 276 | } 277 | } 278 | } 279 | async test_HighEntropyValues(){ 280 | let data = await get_worker_response(getHighEntropyValues) 281 | var score = 0 282 | if(data.architecture === "" && 283 | data.model === "" && data.platformVersion == "" && 284 | data.uaFullVersion === "" && data.bitness == ""){ 285 | this.log({"detection":"UA_Override", "type":"HighEntropyValues.empty", score:0.9}) 286 | } 287 | } 288 | async test_pdfStyle(){ 289 | const iframe = document.createElement("iframe") 290 | iframe.style.height = 0 291 | iframe.style.width = 0 292 | iframe.style.position = "absolute" 293 | iframe.style.x = 0 294 | iframe.style.y = 0 295 | iframe.style.opacity = 0 296 | iframe.src = "assets/test.pdf"; 297 | document.body.appendChild(iframe); 298 | const style = await new Promise((resolve, reject)=>{ 299 | iframe.onload = ()=>{ 300 | try{ 301 | if(iframe.contentDocument === null) {console.error("Could not load PDF iframe propperly, possibly running on file: url"); resolve(undefined)} 302 | const result = iframe.contentDocument.querySelector('style')?.textContent||false 303 | document.body.removeChild(iframe) 304 | resolve(result) 305 | }catch(e){reject(e)} 306 | } 307 | }) 308 | if(style){ 309 | this.log({detection:"pdfStyle",score:0.5, data:{style:style}}) 310 | } 311 | } 312 | hook_mouseEvents() { 313 | if (!this._isMouseHooked){ 314 | for (let event of ["mousedown", "mouseup", "mousemove", "pointermove", "click", "touchstart", "touchend", "touchmove", "touch", "wheel"]){ 315 | document.addEventListener(event,this.mouseEventHandler.bind(this)) 316 | } 317 | } 318 | } 319 | mouseEventHandler(e) { 320 | const key = "Input.cordinatesLeak" 321 | var is_touch = false 322 | if (["touchstart", "touchend", "touchmove", "touch"].includes(e.type)) { 323 | is_touch = true; 324 | e = e.touches[0] || e.changedTouches[0]; 325 | } 326 | 327 | if(e.type === "mousemove"){ 328 | this.mousePos = [e.clientX, e.clientY] 329 | } 330 | 331 | if(e.pageY == e.screenY && e.pageX == e.screenX){var score=1}else{var score=0}; 332 | if (score !== 0 && 1 >= outerHeight - innerHeight) { 333 | // fullscreen 334 | score = 0; 335 | }; 336 | if (score !== 0 && is_touch && navigator.userAgentData.mobile) { 337 | score = 0.5; // mobile touch can have e.pageY == e.screenY && e.pageX == e.screenX 338 | } 339 | if (e.isTrusted === false) { 340 | this.log({"detection":"Input.untrusted", "type":e.type, score:1}) 341 | } 342 | else if(document.activeElement === e.srcElement && 343 | document.activeElement === e.target && 344 | e.type === "click" && e.x === 0 && e.y === 0 && 345 | e.screenX === 0 && e.screenY === 0 && 346 | e.clientX === 0 && e.clientY === 0){} // click over select via TAB + (ENTER or SPACE) 347 | else if (score > 0){ 348 | this.log({"detection":key, "type":e.type, "score":score}) 349 | } 350 | } 351 | hook_canvasVisualize() { 352 | this.hookFunc("CanvasRenderingContext2D", "arc", this.canvasVisualizeHandler.bind(this)) 353 | } 354 | canvasVisualizeHandler(target, thisArg, argumentsList){ 355 | if(this._canvasMouseVisualizer){return} 356 | var pos = this.mousePos 357 | const canvas = thisArg.canvas 358 | var range = 5 359 | if( 360 | canvas.style.position === "fixed" && canvas.style.pointerEvents == "none" && 361 | canvas.style.left[0] === "0" && canvas.style.top[0] === "0" && 362 | ((canvas.width - 1) <= window.innerWidth) && ((canvas.height - 1) <= window.innerHeight) && 363 | ((pos[0] - range) <= argumentsList[0] && (pos[0] + range) >= argumentsList[0]) && 364 | ((pos[0] - range) <= argumentsList[0] && (pos[0] + range) >= argumentsList[0]) 365 | ){ 366 | this._canvasMouseVisualizer = true 367 | this.log({"detection":"canvasMouseVisualizer", "score":0.8}) 368 | } 369 | } 370 | hook_SeleniumScriptInjection(){ 371 | this.hookFunc("Function", "apply", this.SeleniumScriptInjectionHandler.bind(this)) 372 | } 373 | SeleniumScriptInjectionHandler(target, thisArg, argumentsList){ 374 | let code = thisArg.toString() 375 | let matches = {} 376 | let testStr = undefined 377 | for (testStr of chromedriverSourceMatches){ 378 | if(code.indexOf(testStr) !== -1){ 379 | if (matches[testStr] == undefined){matches[testStr] = 0} 380 | matches[testStr] += 1 381 | } 382 | } 383 | const len = Object.keys(matches).length 384 | if (len > 0){ 385 | this.log({"detection":"SeleniumScriptInjection", "score":0.9, data:{args:argumentsList,matches:matches}}) 386 | if(this.crash){throw Error(brotectorBanner)} 387 | } 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/brotector/98b330999e9928a226ca0c8254dddf909d1583bd/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Brotector 9 | 10 | 11 |
12 |

Brotector, a webdriver detector

13 |
14 | 15 | Source-code 16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
Detectionms since loadTypescoredata
Average0
35 |
36 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const scores = [] 2 | const perfs = [] 3 | 4 | const queryString = window.location.search 5 | const urlParams = new URLSearchParams(queryString) 6 | const crash = !(urlParams.get("crash") == "false") 7 | 8 | function avg(array){ 9 | sum = array.reduce((a,c) => a + c, 0) 10 | return sum / array.length 11 | }; 12 | function copyAsJSON(){ 13 | const data = JSON.stringify(window.brotector.detections, null, 2) 14 | navigator.clipboard.writeText(data) 15 | }; 16 | function OnClicked(){ 17 | let button = document.querySelector("#clickHere") 18 | button.textContent = "Clicked :)" 19 | } 20 | async function log(data){ 21 | console.log(data) 22 | const table = document.querySelector("#detections") 23 | 24 | const detection = data.detection 25 | const msSinceLoad = data.msSinceLoad 26 | const type = data.type 27 | const score = data.score 28 | data = data.data 29 | 30 | scores.push(score) 31 | perfs.push(msSinceLoad) 32 | 33 | row = table.insertRow(-1) 34 | 35 | var cell = row.insertCell(0); 36 | cell.textContent = detection 37 | row.appendChild(cell) 38 | 39 | var cell = row.insertCell(1); 40 | cell.textContent = msSinceLoad.toFixed(2).replace(/\B(?{ 69 | window.prevFocus = window.currFocus; 70 | if(document.activeElement){window.currFocus = document.activeElement} 71 | }) 72 | document.querySelector("#clickHere").addEventListener("focusin", ()=>{document.activeElement.blur();window.prevFocus.focus()}) 73 | 74 | window.brotector = new Brotector(log, 50, crash) 75 | } 76 | window.onload = main -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "@ulixee/hero-playground": "^2.0.0-alpha.30", 5 | "puppeteer": "^23.4.0", 6 | "puppeteer-extra": "^3.3.6", 7 | "puppeteer-extra-plugin-stealth": "^2.11.2", 8 | "static-server": "^3.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | selenium 2 | seleniumbase 3 | undetected-chromedriver 4 | webdriver-manager 5 | pyppeteer 6 | playwright 7 | undetected-playwright-patch 8 | botright==0.5.1 9 | nodriver 10 | selenium-driverless 11 | pyppeteer-stealth 12 | cdp-patches 13 | 14 | pytest-asyncio 15 | pytest 16 | pytest-subtests 17 | 18 | tf-keras -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @media (prefers-color-scheme: dark) { 2 | body, button { 3 | background-color: #1B1B1B; 4 | color: #E7E8EB; 5 | } 6 | a { 7 | color: #3387CC; 8 | } 9 | } 10 | @media (prefers-color-scheme: light) { 11 | body, button { 12 | background-color: white; 13 | color: black; 14 | } 15 | } 16 | #description { 17 | margin: 20px; 18 | } 19 | #copy-button { 20 | margin: 20px; 21 | } 22 | table { 23 | border: 2px solid black; 24 | margin: 20px; 25 | width: 80%; 26 | } 27 | th, td { 28 | border-top: 1px solid black; 29 | border-right: 1px solid black; 30 | margin: 0px; 31 | } 32 | a { 33 | border:2px solid red; 34 | } -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import threading 3 | import socketserver 4 | import atexit 5 | from utils import __port__, Handler 6 | 7 | 8 | @pytest.fixture(autouse=True, scope='session') 9 | def my_fixture(): 10 | with socketserver.TCPServer(("", __port__), Handler) as httpd: 11 | atexit.register(httpd.shutdown) 12 | t = threading.Thread(target=httpd.serve_forever) 13 | t.start() 14 | try: 15 | yield 16 | finally: 17 | httpd.shutdown() 18 | t.join() 19 | 20 | -------------------------------------------------------------------------------- /tests/test_driverless.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from selenium_driverless import webdriver 4 | from selenium_driverless.utils.utils import read 5 | from selenium_driverless.types.target import Target 6 | from selenium_driverless.types.by import By 7 | from cdp_patches.input import AsyncInput 8 | from utils import __server_url__, Detected, assert_detections 9 | import pytest 10 | import asyncio 11 | 12 | 13 | async def detect(target: Target, cdp_patches_input: typing.Union[AsyncInput, typing.Literal[False, None]] = False, 14 | add_visualizer=False): 15 | script = """ 16 | await brotector.init_done; 17 | return brotector.detections 18 | """ 19 | await target.get(__server_url__) 20 | if add_visualizer: 21 | await target.execute_script(script=await read("/files/js/show_mousemove.js", sel_root=True)) 22 | await asyncio.sleep(0.5) 23 | click_target = await target.find_element(By.ID, "clickHere") 24 | if cdp_patches_input: 25 | x, y = await click_target.mid_location() 26 | await cdp_patches_input.click("left", x, y) 27 | else: 28 | await click_target.click() 29 | await asyncio.sleep(0.5) 30 | for _ in range(2): 31 | detections = await target.eval_async(script) 32 | detections = [dict(**detection) for detection in detections] 33 | assert_detections(detections) 34 | 35 | if len(detections) > 0: 36 | print("\n") 37 | print(detections) 38 | print("\n") 39 | raise Detected(detections) 40 | await asyncio.sleep(5) 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_driverless(): 45 | async with webdriver.Chrome() as driver: 46 | with pytest.raises(Detected): 47 | await detect(driver.current_target, cdp_patches_input=False) 48 | 49 | 50 | @pytest.mark.asyncio 51 | async def test_driverless_with_cdp_patches(): 52 | async with webdriver.Chrome() as driver: 53 | with pytest.raises(Detected): 54 | await detect(driver.current_target, cdp_patches_input=await AsyncInput(browser=driver)) 55 | 56 | 57 | @pytest.mark.asyncio 58 | async def test_headless(): 59 | options = webdriver.ChromeOptions() 60 | options.add_argument("--headless=new") 61 | async with webdriver.Chrome(options=options) as driver: 62 | with pytest.raises(Detected): 63 | await detect(driver.current_target, cdp_patches_input=False) 64 | 65 | 66 | @pytest.mark.asyncio 67 | async def test_canvas_visualizer(): 68 | async with webdriver.Chrome() as driver: 69 | with pytest.raises(Detected): 70 | await detect(driver.current_target, await AsyncInput(browser=driver), add_visualizer=True) 71 | -------------------------------------------------------------------------------- /tests/test_nodriver.py: -------------------------------------------------------------------------------- 1 | import nodriver 2 | import nodriver as uc 3 | from utils import __server_url__, Detected, assert_detections 4 | import pytest 5 | import asyncio 6 | 7 | 8 | async def nodriver_eval(page, script: str, timeout: float = 5): 9 | cmd = nodriver.cdp.runtime.evaluate(expression="(async ()=>{" + script + "})()", await_promise=True, return_by_value=True, include_command_line_api=True) 10 | res = await asyncio.wait_for(page.send(cmd), timeout=timeout) 11 | return res[0].value 12 | 13 | 14 | async def detect(page: nodriver.Tab): 15 | script = """ 16 | await brotector.init_done; 17 | return brotector.detections 18 | """ 19 | await page.get(__server_url__) 20 | await asyncio.sleep(0.5) 21 | click_target = None 22 | while click_target is None: 23 | click_target = await page.query_selector("#clickHere") 24 | await click_target.click() 25 | await asyncio.sleep(0.5) 26 | for _ in range(2): 27 | detections = await nodriver_eval(page, script) 28 | assert_detections(detections) 29 | if len(detections) > 0: 30 | print("\n") 31 | print(detections) 32 | print("\n") 33 | raise Detected(detections) 34 | await asyncio.sleep(5) 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_nodriver(): 39 | browser = await uc.start() 40 | try: 41 | page = await browser.get("about:blank") 42 | with pytest.raises(Detected): 43 | await detect(page) 44 | finally: 45 | browser.stop() 46 | -------------------------------------------------------------------------------- /tests/test_playwright.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | from utils import __server_url__, Detected, assert_detections 4 | from playwright import async_api 5 | from undetected_playwright import async_api as uc_async_api 6 | import botright 7 | 8 | 9 | async def playwright_eval(cdp_session: async_api.CDPSession, script: str, timeout: float = 5): 10 | res = await cdp_session.send("Runtime.evaluate", { 11 | "expression": "(async ()=>{" + script + "})()", "awaitPromise": True, 12 | "returnByValue": True, "timeout": timeout * 1000, "includeCommandLineAPI": True 13 | }) 14 | exc = res.get("exceptionDetails") 15 | if exc: 16 | raise Exception(exc) 17 | return res["result"].get("value") 18 | 19 | 20 | async def detect(context: async_api.BrowserContext): 21 | page = await context.new_page() 22 | cdp_session = await context.new_cdp_session(page) 23 | await page.goto(__server_url__) 24 | await asyncio.sleep(0.5) 25 | click_target = await page.query_selector("#clickHere") 26 | await click_target.click() 27 | await asyncio.sleep(0.5) 28 | for _ in range(2): 29 | detections = await playwright_eval(cdp_session, """ 30 | await brotector.init_done; 31 | return brotector.detections 32 | """) 33 | assert_detections(detections) 34 | if len(detections) > 0: 35 | print("\n") 36 | print(detections) 37 | print("\n") 38 | raise Detected(detections) 39 | await asyncio.sleep(5) 40 | 41 | 42 | @pytest.mark.asyncio 43 | async def test_playwright(): 44 | async with async_api.async_playwright() as p: 45 | browser = await p.chromium.launch(channel="chrome", headless=False) 46 | context = await browser.new_context() 47 | with pytest.raises(Detected): 48 | await detect(context) 49 | await browser.close() 50 | 51 | 52 | @pytest.mark.asyncio 53 | async def test_playwright_stealthy(): 54 | async with async_api.async_playwright() as p: 55 | browser = await p.chromium.launch(channel="chrome", headless=False, 56 | args=["--disable-blink-features=AutomationControlled"]) 57 | context = await browser.new_context() 58 | with pytest.raises(Detected): 59 | await detect(context) 60 | await browser.close() 61 | 62 | 63 | @pytest.mark.asyncio 64 | async def test_uc_playwright(): 65 | async with uc_async_api.async_playwright() as p: 66 | browser = await p.chromium.launch(channel="chrome", headless=False, 67 | args=["--disable-blink-features=AutomationControlled"]) 68 | context = await browser.new_context() 69 | with pytest.raises(Detected): 70 | await detect(context) 71 | await browser.close() 72 | 73 | 74 | @pytest.mark.asyncio 75 | async def test_botright(): 76 | botright_client = await botright.Botright() 77 | try: 78 | browser = await botright_client.new_browser(channel="chrome") 79 | with pytest.raises(Detected): 80 | await detect(browser) 81 | finally: 82 | await botright_client.close() 83 | 84 | 85 | @pytest.mark.asyncio 86 | async def test_uc_botright(): 87 | botright_client = await botright.Botright(use_undetected_playwright=True) 88 | try: 89 | browser = await botright_client.new_browser(channel="chrome") 90 | with pytest.raises(Detected): 91 | await detect(browser) 92 | finally: 93 | await botright_client.close() 94 | 95 | 96 | @pytest.mark.asyncio 97 | async def test_canvas_botright(): 98 | botright_client = await botright.Botright(use_undetected_playwright=True, user_action_layer=True) 99 | try: 100 | browser = await botright_client.new_browser(channel="chrome") 101 | with pytest.raises(Detected): 102 | await detect(browser) 103 | finally: 104 | await botright_client.close() 105 | -------------------------------------------------------------------------------- /tests/test_pyppeteer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | from pyppeteer import launch 4 | from pyppeteer.errors import ElementHandleError 5 | from pyppeteer_stealth import stealth 6 | from utils import __server_url__, Detected, assert_detections 7 | from selenium_driverless.utils.utils import find_chrome_executable 8 | 9 | extra_args = ["--no-fist-run", "--disable-fre", "--no-default-browser-check", "--disable-features=FedCm"] 10 | 11 | 12 | async def detect(page): 13 | await page.goto(__server_url__) 14 | await asyncio.sleep(0.1) 15 | await page.click("#clickHere") 16 | for _ in range(2): 17 | detections = await page.evaluate(""" 18 | async ()=>{ 19 | // await brotector.init_done; // would crash due to infinite recursion at puppeteer stealth 20 | return brotector.detections 21 | } 22 | """) 23 | assert_detections(detections) 24 | if len(detections) > 0: 25 | raise Detected(detections) 26 | await asyncio.sleep(5) 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_pyppeteer(): 31 | browser = await asyncio.wait_for(launch(headless=False, executablePath=find_chrome_executable(), 32 | ignoreDefaultArgs=False, args=[*extra_args]), 10) 33 | try: 34 | page = await browser.newPage() 35 | with pytest.raises(Detected): 36 | await detect(page) 37 | finally: 38 | await browser.close() 39 | 40 | 41 | @pytest.mark.asyncio 42 | async def test_pyppeteer_stealthy(): 43 | browser = await asyncio.wait_for(launch(headless=False, executablePath=find_chrome_executable(), 44 | args=['--disable-blink-features=AutomationControlled', *extra_args]), 10) 45 | try: 46 | page = await browser.newPage() 47 | with pytest.raises(Detected): 48 | await detect(page) 49 | finally: 50 | await browser.close() 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_pyppeteer_stealth(): 55 | browser = await asyncio.wait_for(launch(headless=False, executablePath=find_chrome_executable(), 56 | args=['--disable-blink-features=AutomationControlled', *extra_args]), 10) 57 | try: 58 | page = await browser.newPage() 59 | with pytest.raises(Detected): 60 | await stealth(page) 61 | try: 62 | await detect(page) 63 | except ElementHandleError as e: 64 | if e.args[0] == 'Evaluation failed: RangeError: Maximum call stack size exceeded': 65 | raise Detected("Pyppeteer stealth infinite recursion crash at " 66 | "https://github.com/berstend/puppeteer-extra/blob" 67 | "/39248f1f5deeb21b1e7eb6ae07b8ef73f1231ab9/packages/puppeteer-extra-plugin-stealth" 68 | "/evasions/_utils/index.js#L42") 69 | raise e 70 | finally: 71 | await browser.close() 72 | -------------------------------------------------------------------------------- /tests/test_selenium.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pytest 3 | 4 | from selenium import webdriver 5 | import seleniumbase 6 | import undetected_chromedriver as uc_webdriver 7 | 8 | from webdriver_manager.chrome import ChromeDriverManager 9 | from selenium.webdriver.chrome.service import Service as ChromeService 10 | 11 | from selenium.webdriver.common.by import By 12 | from selenium.common.exceptions import WebDriverException 13 | from utils import __server_url__, Detected, assert_detections, __screenshot_path__ 14 | 15 | 16 | def sel_eval(driver: webdriver.Chrome, script: str, timeout: float = 5): 17 | res = driver.execute_cdp_cmd("Runtime.evaluate", { 18 | "expression": "(async ()=>{" + script + "})()", "awaitPromise": True, 19 | "returnByValue": True, "timeout": timeout * 1000, "includeCommandLineAPI": True 20 | }) 21 | exc = res.get("exceptionDetails") 22 | if exc: 23 | raise Exception(exc) 24 | return res["result"].get("value") 25 | 26 | 27 | def detect(driver, is_uc=False): 28 | try: 29 | is_sb = False 30 | if isinstance(driver, seleniumbase.BaseCase): 31 | is_sb = True 32 | driver.uc_open_with_reconnect(__server_url__, 0.1) 33 | driver.disconnect() 34 | else: 35 | driver.get(__server_url__) 36 | 37 | if is_sb: 38 | driver.connect() 39 | sel_eval(driver, "setTimeout(() => {document.querySelector('#copy-button').click()}, 200)") 40 | driver.disconnect() 41 | else: 42 | time.sleep(0.2) 43 | click_target = driver.find_element(By.ID, "clickHere") 44 | click_target.click() 45 | 46 | time.sleep(1) 47 | for _ in range(2): 48 | if is_sb: 49 | driver.connect() 50 | detections = sel_eval(driver, "return await brotector.init_done") 51 | if is_sb: 52 | driver.disconnect() 53 | assert_detections(detections) 54 | if len(detections) > 0: 55 | print("\n") 56 | print(detections) 57 | print("\n") 58 | raise Detected(detections) 59 | time.sleep(2) 60 | except WebDriverException as e: 61 | if not is_uc: 62 | driver.quit() 63 | if "https://github.com/kaliiiiiiiiii/brotector" in e.msg: 64 | raise Detected(e.msg) 65 | raise e 66 | 67 | 68 | def test_selenium(): 69 | options = webdriver.ChromeOptions() 70 | options.add_experimental_option("excludeSwitches", ["enable-logging"]) 71 | options.add_argument("--log-level=3") 72 | with webdriver.Chrome(options=options, service=ChromeService(ChromeDriverManager().install())) as driver: 73 | with pytest.raises(Detected): 74 | detect(driver) 75 | 76 | 77 | def test_selenium_headless_mk_screenshot(): 78 | options = webdriver.ChromeOptions() 79 | options.add_experimental_option("excludeSwitches", ["enable-logging"]) 80 | options.add_argument("--log-level=3") 81 | with webdriver.Chrome(options=options, service=ChromeService(ChromeDriverManager().install())) as driver: 82 | with pytest.raises(Detected): 83 | detect(driver) 84 | driver.save_screenshot(__screenshot_path__) 85 | 86 | 87 | def test_uc(): 88 | options = uc_webdriver.ChromeOptions() 89 | options.add_argument("--log-level=3") 90 | with uc_webdriver.Chrome(options=options, service=ChromeService(ChromeDriverManager().install())) as driver: 91 | with pytest.raises(Detected): 92 | detect(driver, is_uc=True) 93 | 94 | 95 | def test_seleniumbase(): 96 | with pytest.raises(Detected): 97 | with seleniumbase.SB(uc=True) as sb: 98 | detect(sb) 99 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import http.server 3 | 4 | from selenium_driverless.utils.utils import random_port 5 | 6 | __port__ = random_port("localhost") 7 | 8 | _dir = str(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 9 | __server_url__ = f"http://localhost:{__port__}/index.html?crash=false" 10 | __screenshot_path__ = _dir + "/assets/example_screenshot_headless.png" 11 | 12 | 13 | class Handler(http.server.SimpleHTTPRequestHandler): 14 | def __init__(self, *args, **kwargs): 15 | super().__init__(*args, directory=_dir, **kwargs) 16 | 17 | def log_message(self, format, *args, **kwargs): 18 | pass 19 | 20 | def log_error(self, format, *args): 21 | pass 22 | 23 | def handle(self): 24 | try: 25 | super().handle() 26 | except ConnectionResetError: 27 | pass 28 | 29 | 30 | def assert_detections(detections: list): 31 | for detection in detections: 32 | if detection.get("detection") == "runtime.enabled": 33 | assert detection.get("type") == "webdriver" 34 | data = detection.get("data", {}) 35 | assert data.get("stackLookupCount", 1) == 1 36 | assert data.get("nameLookupCount", 3) >= 3 37 | 38 | 39 | class Detected(Exception): 40 | pass 41 | -------------------------------------------------------------------------------- /tests_nodejs/test_crash.mjs: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | async function main() { 4 | const browser = await puppeteer.launch({ 5 | headless: false 6 | }); 7 | const page = await browser.newPage(); 8 | await page.evaluate(() => { 9 | const f = document.createElement("iframe"); 10 | f.src = "data:text/html;charset=utf-8,

", 11 | f.style.height = 0 12 | f.style.width = 0 13 | f.style.opacity = 0 14 | document.body.appendChild(f), 15 | console.log("fire"), 16 | f.contentWindow.open("", "", "top=9999,left=9999,width=1,height=1") 17 | }); 18 | } 19 | 20 | main(); -------------------------------------------------------------------------------- /tests_nodejs/test_hero.mjs: -------------------------------------------------------------------------------- 1 | import {__server_url__, sleep} from './utils.mjs'; 2 | import Hero from '@ulixee/hero-playground'; 3 | 4 | async function test(){ 5 | const hero = new Hero({ showChromeInteractions: true, showChrome: true }); 6 | 7 | const tab = hero.activeTab; 8 | await tab.goto(__server_url__) 9 | await sleep(1000) 10 | const aElem = hero.document.querySelector('button'); 11 | await hero.interact({ click: { element: aElem } }) 12 | var res = undefined 13 | while(res === undefined){ 14 | await sleep(10) 15 | res = await await tab.getJsValue("brotector.detections") 16 | } 17 | await sleep(500) 18 | res = await await tab.getJsValue("brotector.detections") 19 | console.log(res) 20 | await hero.close(); 21 | } 22 | 23 | await test() 24 | process.exit(0) -------------------------------------------------------------------------------- /tests_nodejs/test_puppeteer.mjs: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | import puppeteerExtra from 'puppeteer-extra'; 3 | import StealthPlugin from 'puppeteer-extra-plugin-stealth'; 4 | import {__server_url__, sleep} from './utils.mjs'; 5 | 6 | puppeteerExtra.use(StealthPlugin()) 7 | 8 | const script = async ()=>{ 9 | await brotector.init_done 10 | return brotector.detections 11 | } 12 | 13 | async function test(browser){ 14 | const page = await browser.newPage(); 15 | await page.goto(__server_url__); 16 | await sleep(500) 17 | await page.click("#clickHere") 18 | const detections = await page.evaluate(script) 19 | if(detections.length == 0){throw Error("Not detected")} 20 | console.log(detections) 21 | } 22 | 23 | async function test_puppeteer(){ 24 | const browser = await puppeteer.launch({ headless: false }); 25 | try{ 26 | await test(browser) 27 | }finally{ 28 | await browser.close(); 29 | } 30 | 31 | } 32 | async function test_puppeteerExtraStealth(){ 33 | const browser = await puppeteerExtra.launch({ headless: false }); 34 | try{ 35 | await test(browser) 36 | }catch(e){ 37 | if(e.name === "RangeError" && e.message === 'Maximum call stack size exceeded' 38 | && e.stack.match("\n at SeleniumScriptInjectionHandler")){ 39 | // infinite recursion induced by puppeteer stealth https://github.com/berstend/puppeteer-extra/blob/39248f1f5deeb21b1e7eb6ae07b8ef73f1231ab9/packages/puppeteer-extra-plugin-stealth/evasions/_utils/index.js#L42 40 | }else{ 41 | throw e 42 | }; 43 | }finally{ 44 | await browser.close(); 45 | }; 46 | 47 | } 48 | 49 | await test_puppeteer() 50 | await test_puppeteerExtraStealth() 51 | process.exit(0) 52 | -------------------------------------------------------------------------------- /tests_nodejs/utils.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import net from "net"; 4 | import StaticServer from 'static-server' 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | const __main_dir__ = dirname(__dirname) 8 | 9 | function sleep (time) { 10 | return new Promise((resolve) => setTimeout(resolve, time)); 11 | } 12 | 13 | const port = await new Promise( res => { 14 | const srv = net.createServer(); 15 | srv.listen(0, () => { 16 | const port = srv.address().port 17 | srv.close((err) => res(port)) 18 | }); 19 | }) 20 | 21 | const __server_url__ = `http://localhost:${port}/?crash=false` 22 | 23 | const server = new StaticServer({ 24 | rootPath: __main_dir__, // required, the root of the server file tree 25 | port: port, // required, the port to listen 26 | host: 'localhost', // optional, defaults to any interface 27 | }); 28 | 29 | await new Promise((resolve, reject)=>{ 30 | try{ 31 | server.start(resolve) 32 | }catch(e){reject(e)} 33 | }) 34 | 35 | export { __main_dir__, __server_url__, sleep} --------------------------------------------------------------------------------