├── .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 |
17 |
18 |
19 |
20 |
21 | Detection
22 | ms since load
23 | Type
24 | score
25 | data
26 |
27 |
28 | Average
29 |
30 |
31 | 0
32 |
33 |
34 |
35 |
36 |
37 | copy as JSON
38 |
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}
--------------------------------------------------------------------------------