unrealon 2.0.9__py3-none-any.whl → 2.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {unrealon-2.0.9.dist-info → unrealon-2.0.11.dist-info}/METADATA +1 -1
- {unrealon-2.0.9.dist-info → unrealon-2.0.11.dist-info}/RECORD +15 -15
- unrealon_browser/managers/logger_bridge.py +3 -3
- unrealon_browser/managers/script_manager.py +45 -26
- unrealon_browser/stealth/{bypass_techniques.pyc → bypass_techniques.py} +0 -0
- unrealon_browser/stealth/manager.py +507 -0
- unrealon_browser/stealth/nodriver_stealth.py +432 -0
- unrealon_browser/stealth/playwright_stealth.py +179 -0
- unrealon_browser/stealth/scanner_tester.py +355 -0
- unrealon_browser/stealth/undetected_chrome.py +361 -0
- unrealon_driver/managers/browser.py +7 -2
- unrealon_browser/stealth/manager.pyc +0 -0
- unrealon_browser/stealth/nodriver_stealth.pyc +0 -0
- unrealon_browser/stealth/playwright_stealth.pyc +0 -0
- unrealon_browser/stealth/scanner_tester.pyc +0 -0
- unrealon_browser/stealth/undetected_chrome.pyc +0 -0
- {unrealon-2.0.9.dist-info → unrealon-2.0.11.dist-info}/LICENSE +0 -0
- {unrealon-2.0.9.dist-info → unrealon-2.0.11.dist-info}/WHEEL +0 -0
- {unrealon-2.0.9.dist-info → unrealon-2.0.11.dist-info}/entry_points.txt +0 -0
- {unrealon-2.0.9.dist-info → unrealon-2.0.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ScannerTester - Clean stealth testing through UnrealOn scanner
|
|
3
|
+
|
|
4
|
+
Provides streamlined stealth testing using our detection service with Pydantic models.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import Optional, List
|
|
10
|
+
from playwright.async_api import Page
|
|
11
|
+
|
|
12
|
+
from unrealon_core.config.urls import get_url_config
|
|
13
|
+
from unrealon_browser.dto import BotDetectionResults, BotDetectionResponse
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
logger.setLevel(logging.DEBUG)
|
|
17
|
+
|
|
18
|
+
class ScannerTester:
|
|
19
|
+
"""Clean stealth testing through UnrealOn scanner"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, logger_bridge=None):
|
|
22
|
+
"""Initialize ScannerTester"""
|
|
23
|
+
self.logger_bridge = logger_bridge
|
|
24
|
+
|
|
25
|
+
def _log(self, message: str, level: str = "info") -> None:
|
|
26
|
+
"""Log message with bridge support"""
|
|
27
|
+
if self.logger_bridge:
|
|
28
|
+
getattr(self.logger_bridge, f"log_{level}", self.logger_bridge.log_info)(message)
|
|
29
|
+
else:
|
|
30
|
+
getattr(logger, level, logger.info)(message)
|
|
31
|
+
|
|
32
|
+
def _get_scanner_url(self) -> str:
|
|
33
|
+
"""Get scanner URL from configuration"""
|
|
34
|
+
return get_url_config().stealth_test_url
|
|
35
|
+
|
|
36
|
+
async def test_stealth(self, browser_manager, stealth_manager, method: str = "comprehensive") -> BotDetectionResponse:
|
|
37
|
+
"""
|
|
38
|
+
Test stealth effectiveness with specified method
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
browser_manager: Browser manager instance
|
|
42
|
+
stealth_manager: Stealth manager instance
|
|
43
|
+
method: Stealth method to use
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
BotDetectionResponse with results
|
|
47
|
+
"""
|
|
48
|
+
if not browser_manager or not browser_manager.page:
|
|
49
|
+
return BotDetectionResponse.error_response(
|
|
50
|
+
"Browser manager or page not available",
|
|
51
|
+
"browser_error",
|
|
52
|
+
self._get_scanner_url()
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
scanner_url = self._get_scanner_url()
|
|
57
|
+
self._log(f"🔬 Testing stealth with method: {method}")
|
|
58
|
+
|
|
59
|
+
# Apply stealth
|
|
60
|
+
stealth_applied = await stealth_manager.apply_stealth(browser_manager.page, method=method)
|
|
61
|
+
if not stealth_applied:
|
|
62
|
+
return BotDetectionResponse.error_response(
|
|
63
|
+
f"Failed to apply stealth method: {method}",
|
|
64
|
+
"stealth_error",
|
|
65
|
+
scanner_url,
|
|
66
|
+
method
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Navigate to scanner
|
|
70
|
+
nav_result = await browser_manager.navigate_async(scanner_url)
|
|
71
|
+
if not nav_result["success"]:
|
|
72
|
+
return BotDetectionResponse.error_response(
|
|
73
|
+
f"Navigation failed: {nav_result.get('error', 'Unknown error')}",
|
|
74
|
+
"navigation_error",
|
|
75
|
+
scanner_url,
|
|
76
|
+
method
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Setup console logging to catch JavaScript errors
|
|
80
|
+
await self._setup_console_logging(browser_manager.page)
|
|
81
|
+
|
|
82
|
+
# Wait for scanner to complete
|
|
83
|
+
await self._wait_for_scanner(browser_manager)
|
|
84
|
+
|
|
85
|
+
# Extract and parse results
|
|
86
|
+
bot_results = await self._extract_bot_results(browser_manager.page)
|
|
87
|
+
if bot_results:
|
|
88
|
+
return BotDetectionResponse.success_response(bot_results, scanner_url, method)
|
|
89
|
+
else:
|
|
90
|
+
return BotDetectionResponse.error_response(
|
|
91
|
+
"Scanner results not available or invalid",
|
|
92
|
+
"extraction_error",
|
|
93
|
+
scanner_url,
|
|
94
|
+
method
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
self._log(f"❌ Stealth test failed: {e}", "error")
|
|
99
|
+
return BotDetectionResponse.error_response(
|
|
100
|
+
str(e),
|
|
101
|
+
"test_error",
|
|
102
|
+
self._get_scanner_url(),
|
|
103
|
+
method
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async def compare_methods(self, browser_manager, stealth_manager, methods: Optional[List[str]] = None) -> List[BotDetectionResponse]:
|
|
107
|
+
"""
|
|
108
|
+
Compare multiple stealth methods
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
browser_manager: Browser manager instance
|
|
112
|
+
stealth_manager: Stealth manager instance
|
|
113
|
+
methods: List of methods to test (default: ["playwright", "comprehensive", "aggressive"])
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
List of BotDetectionResponse for each method
|
|
117
|
+
"""
|
|
118
|
+
if methods is None:
|
|
119
|
+
methods = ["playwright", "comprehensive", "aggressive"]
|
|
120
|
+
|
|
121
|
+
self._log(f"🔬 Comparing {len(methods)} stealth methods")
|
|
122
|
+
|
|
123
|
+
results = []
|
|
124
|
+
for method in methods:
|
|
125
|
+
self._log(f"🧪 Testing method: {method}")
|
|
126
|
+
result = await self.test_stealth(browser_manager, stealth_manager, method)
|
|
127
|
+
results.append(result)
|
|
128
|
+
|
|
129
|
+
# Brief pause between tests
|
|
130
|
+
await asyncio.sleep(1)
|
|
131
|
+
|
|
132
|
+
return results
|
|
133
|
+
|
|
134
|
+
async def _setup_console_logging(self, page) -> None:
|
|
135
|
+
"""Setup console error logging to debug React issues"""
|
|
136
|
+
def handle_console_message(msg):
|
|
137
|
+
if msg.type in ['error', 'warning']:
|
|
138
|
+
self._log(f"🔍 CONSOLE {msg.type.upper()}: {msg.text}", "warning")
|
|
139
|
+
elif msg.type == 'log' and ('error' in msg.text.lower() or 'failed' in msg.text.lower()):
|
|
140
|
+
self._log(f"🔍 CONSOLE LOG: {msg.text}", "info")
|
|
141
|
+
|
|
142
|
+
page.on("console", handle_console_message)
|
|
143
|
+
self._log("🔍 Console logging enabled for JavaScript error detection", "info")
|
|
144
|
+
|
|
145
|
+
async def _wait_for_scanner(self, browser_manager) -> None:
|
|
146
|
+
"""Wait for scanner to complete tests using proper page wait methods"""
|
|
147
|
+
self._log("⏳ Waiting for React SPA and bot detection tests...")
|
|
148
|
+
|
|
149
|
+
# Use proper SPA wait method
|
|
150
|
+
if hasattr(browser_manager, 'page_wait') and browser_manager.page_wait:
|
|
151
|
+
self._log("🚀 Using SPA wait method...")
|
|
152
|
+
spa_ready = await browser_manager.page_wait.wait_spa()
|
|
153
|
+
if not spa_ready:
|
|
154
|
+
self._log("⚠️ SPA wait timeout, trying full load method", "warning")
|
|
155
|
+
await browser_manager.page_wait.wait_full_load()
|
|
156
|
+
else:
|
|
157
|
+
# Fallback to basic wait
|
|
158
|
+
try:
|
|
159
|
+
await browser_manager.page.wait_for_load_state("networkidle", timeout=15000)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
self._log(f"⚠️ Basic load timeout: {e}", "warning")
|
|
162
|
+
|
|
163
|
+
# Now wait for bot detection results to appear
|
|
164
|
+
self._log("⏳ Waiting for bot detection results...")
|
|
165
|
+
|
|
166
|
+
max_attempts = 45 # 45 seconds for bot detection
|
|
167
|
+
attempt = 0
|
|
168
|
+
|
|
169
|
+
while attempt < max_attempts:
|
|
170
|
+
try:
|
|
171
|
+
check_result = await browser_manager.page.evaluate("""
|
|
172
|
+
() => {
|
|
173
|
+
// Check if results are available
|
|
174
|
+
if (window.botDetectionResults && typeof window.botDetectionResults === 'object') {
|
|
175
|
+
return {
|
|
176
|
+
hasResults: true,
|
|
177
|
+
isComplete: window.botDetectionResults.tests && window.botDetectionResults.tests.length > 0,
|
|
178
|
+
testsCount: window.botDetectionResults.tests ? window.botDetectionResults.tests.length : 0
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check scanner status and page content
|
|
183
|
+
const bodyText = document.body.innerText || '';
|
|
184
|
+
const windowKeys = Object.keys(window).filter(k => k.includes('bot') || k.includes('detection') || k.includes('scan'));
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
hasResults: false,
|
|
188
|
+
isComplete: false,
|
|
189
|
+
testsCount: 0,
|
|
190
|
+
isInitializing: bodyText.includes('Initializing'),
|
|
191
|
+
isScanning: bodyText.includes('Scanning') || bodyText.includes('Running'),
|
|
192
|
+
bodyPreview: bodyText.substring(0, 200),
|
|
193
|
+
windowKeys: windowKeys,
|
|
194
|
+
botDetectionType: typeof window.botDetectionResults,
|
|
195
|
+
url: window.location.href,
|
|
196
|
+
title: document.title
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
""")
|
|
200
|
+
|
|
201
|
+
if check_result['hasResults'] and check_result['isComplete']:
|
|
202
|
+
self._log(f"✅ Bot detection completed! Found {check_result['testsCount']} tests")
|
|
203
|
+
await asyncio.sleep(1) # Final wait for completion
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
# Log detailed progress every 5 seconds
|
|
207
|
+
if attempt % 5 == 0:
|
|
208
|
+
status = "initializing" if check_result.get('isInitializing') else ("scanning" if check_result.get('isScanning') else "unknown")
|
|
209
|
+
self._log(f"🔍 DEBUG Wait Status ({attempt}s):")
|
|
210
|
+
self._log(f" URL: {check_result.get('url')}")
|
|
211
|
+
self._log(f" Title: {check_result.get('title')}")
|
|
212
|
+
self._log(f" Status: {status}")
|
|
213
|
+
self._log(f" botDetectionResults type: {check_result.get('botDetectionType')}")
|
|
214
|
+
self._log(f" Window keys: {check_result.get('windowKeys')}")
|
|
215
|
+
self._log(f" Body preview: {check_result.get('bodyPreview')}")
|
|
216
|
+
|
|
217
|
+
await asyncio.sleep(1)
|
|
218
|
+
attempt += 1
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
self._log(f"⚠️ Error checking results: {e}", "warning")
|
|
222
|
+
await asyncio.sleep(1)
|
|
223
|
+
attempt += 1
|
|
224
|
+
|
|
225
|
+
self._log("⚠️ Bot detection results not available after 45s timeout", "warning")
|
|
226
|
+
|
|
227
|
+
async def _extract_bot_results(self, page: Page) -> Optional[BotDetectionResults]:
|
|
228
|
+
"""Extract and parse bot detection results"""
|
|
229
|
+
try:
|
|
230
|
+
# First, let's debug what's on the page
|
|
231
|
+
debug_info = await page.evaluate("""
|
|
232
|
+
() => {
|
|
233
|
+
return {
|
|
234
|
+
title: document.title,
|
|
235
|
+
url: window.location.href,
|
|
236
|
+
bodyText: document.body.innerText.substring(0, 200),
|
|
237
|
+
hasBotDetectionResults: typeof window.botDetectionResults !== 'undefined',
|
|
238
|
+
botDetectionResultsType: typeof window.botDetectionResults,
|
|
239
|
+
botDetectionResultsValue: window.botDetectionResults,
|
|
240
|
+
windowKeys: Object.keys(window).filter(k => k.includes('bot') || k.includes('detection')).slice(0, 10)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
""")
|
|
244
|
+
|
|
245
|
+
self._log(f"🔍 DEBUG Page Info:", "info")
|
|
246
|
+
self._log(f" Title: {debug_info.get('title')}", "info")
|
|
247
|
+
self._log(f" URL: {debug_info.get('url')}", "info")
|
|
248
|
+
self._log(f" Body preview: {debug_info.get('bodyText')}", "info")
|
|
249
|
+
self._log(f" Has botDetectionResults: {debug_info.get('hasBotDetectionResults')}", "info")
|
|
250
|
+
self._log(f" Type: {debug_info.get('botDetectionResultsType')}", "info")
|
|
251
|
+
self._log(f" Value: {debug_info.get('botDetectionResultsValue')}", "info")
|
|
252
|
+
self._log(f" Window keys: {debug_info.get('windowKeys')}", "info")
|
|
253
|
+
|
|
254
|
+
# Extract results from window.botDetectionResults
|
|
255
|
+
raw_results = await page.evaluate("""
|
|
256
|
+
() => {
|
|
257
|
+
if (window.botDetectionResults) {
|
|
258
|
+
return window.botDetectionResults;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
""")
|
|
263
|
+
|
|
264
|
+
if not raw_results:
|
|
265
|
+
self._log("⚠️ No botDetectionResults found", "warning")
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
self._log(f"🔍 Raw results type: {type(raw_results)}, value: {raw_results}", "info")
|
|
269
|
+
|
|
270
|
+
# Parse with Pydantic
|
|
271
|
+
return BotDetectionResults(**raw_results)
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
self._log(f"❌ Failed to extract results: {e}", "error")
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
def print_results(self, response: BotDetectionResponse) -> None:
|
|
278
|
+
"""Print formatted test results"""
|
|
279
|
+
self._log("\n🔬 STEALTH TEST RESULTS", "info")
|
|
280
|
+
self._log("=" * 50, "info")
|
|
281
|
+
|
|
282
|
+
if not response.success:
|
|
283
|
+
self._log(f"❌ Test failed: {response.error.error if response.error else 'Unknown error'}", "error")
|
|
284
|
+
if response.scanner_url:
|
|
285
|
+
self._log(f"🌐 Scanner: {response.scanner_url}", "info")
|
|
286
|
+
if response.method:
|
|
287
|
+
self._log(f"🥷 Method: {response.method}", "info")
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
if not response.results:
|
|
291
|
+
self._log("❌ No results available", "error")
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
results = response.results
|
|
295
|
+
|
|
296
|
+
# Basic info
|
|
297
|
+
self._log(f"🌐 Scanner: {response.scanner_url or 'Unknown'}", "info")
|
|
298
|
+
self._log(f"🥷 Method: {response.method or 'Unknown'}", "info")
|
|
299
|
+
self._log(f"📊 Score: {results.overall_score}% | Bot: {results.is_bot} | Confidence: {results.confidence}", "info")
|
|
300
|
+
self._log(f"📈 Tests: {results.summary.passed}✅ {results.summary.failed}❌ {results.summary.warnings}⚠️ ({results.summary.total} total)", "info")
|
|
301
|
+
self._log(f"🏆 Effectiveness: {results.get_effectiveness_rating()}", "info")
|
|
302
|
+
|
|
303
|
+
# Failed tests
|
|
304
|
+
if results.failed_tests:
|
|
305
|
+
self._log(f"\n❌ Failed Tests ({len(results.failed_tests)}):", "warning")
|
|
306
|
+
for test in results.failed_tests[:3]: # Show top 3
|
|
307
|
+
self._log(f" • {test.name}: {test.description} (Score: {test.score})", "warning")
|
|
308
|
+
|
|
309
|
+
# Critical failures
|
|
310
|
+
if results.critical_failures:
|
|
311
|
+
self._log(f"\n🚨 Critical Failures ({len(results.critical_failures)}):", "error")
|
|
312
|
+
for failure in results.critical_failures:
|
|
313
|
+
self._log(f" • {failure.name}: {failure.description}", "error")
|
|
314
|
+
|
|
315
|
+
# Recommendations
|
|
316
|
+
recommendations = results.get_recommendations()
|
|
317
|
+
if recommendations:
|
|
318
|
+
self._log(f"\n💡 Recommendations:", "info")
|
|
319
|
+
for rec in recommendations[:3]: # Show top 3
|
|
320
|
+
self._log(f" • {rec}", "info")
|
|
321
|
+
|
|
322
|
+
def print_comparison(self, results: List[BotDetectionResponse]) -> None:
|
|
323
|
+
"""Print comparison results"""
|
|
324
|
+
self._log("\n🔬 STEALTH METHOD COMPARISON", "info")
|
|
325
|
+
self._log("=" * 60, "info")
|
|
326
|
+
|
|
327
|
+
successful_results = [r for r in results if r.success and r.results]
|
|
328
|
+
|
|
329
|
+
if not successful_results:
|
|
330
|
+
self._log("❌ No successful tests to compare", "error")
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
# Sort by score (best first)
|
|
334
|
+
successful_results.sort(key=lambda x: x.results.overall_score)
|
|
335
|
+
best = successful_results[0]
|
|
336
|
+
|
|
337
|
+
self._log(f"🏆 WINNER: {best.method} (Score: {best.results.overall_score}%)", "info")
|
|
338
|
+
|
|
339
|
+
# Show all results
|
|
340
|
+
for i, result in enumerate(results, 1):
|
|
341
|
+
if result.success and result.results:
|
|
342
|
+
r = result.results
|
|
343
|
+
status = "🏆" if result == best else f"{i}."
|
|
344
|
+
self._log(f"{status} {result.method}: {r.overall_score}% | Bot: {r.is_bot} | {r.get_effectiveness_rating()}", "info")
|
|
345
|
+
else:
|
|
346
|
+
self._log(f"{i}. {result.method}: ❌ Failed - {result.error.error if result.error else 'Unknown'}", "error")
|
|
347
|
+
|
|
348
|
+
def get_best_method(self, results: List[BotDetectionResponse]) -> Optional[str]:
|
|
349
|
+
"""Get the best performing method from comparison results"""
|
|
350
|
+
successful_results = [r for r in results if r.success and r.results]
|
|
351
|
+
if not successful_results:
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
best = min(successful_results, key=lambda x: x.results.overall_score)
|
|
355
|
+
return best.method
|