This commit is contained in:
nafer committed 2026-03-06 10:00:07 +03:00
1 parent af359b8570
commit 922dd223c4
4 files changed
+649

No files matched your search

+3
View File
@@ -16,6 +16,9 @@ RUN pip install --no-cache-dir --root-user-action=ignore \
WORKDIR /app
# Копируем stealth-расширение для обхода антифрода
COPY stealth-extension/ /app/stealth-extension/
# Копируем код приложения
COPY app.py .
+65
View File
@@ -191,6 +191,45 @@ async def run_browser_task(task_id: str, task: str, max_steps: int, model: str):
user_data_dir = "/app/browser-data/main_profile"
os.makedirs(user_data_dir, exist_ok=True)
# Создаём/обновляем Chrome Preferences для разрешения third-party cookies
import json as json_mod
prefs_dir = os.path.join(user_data_dir, "Default")
os.makedirs(prefs_dir, exist_ok=True)
prefs_path = os.path.join(prefs_dir, "Preferences")
try:
existing_prefs = {}
if os.path.exists(prefs_path):
with open(prefs_path, 'r') as f:
existing_prefs = json_mod.load(f)
# Разрешаем third-party cookies
if 'profile' not in existing_prefs:
existing_prefs['profile'] = {}
existing_prefs['profile']['cookie_controls_mode'] = 0 # 0 = Allow all
existing_prefs['profile']['block_third_party_cookies'] = False
if 'content_settings' not in existing_prefs['profile']:
existing_prefs['profile']['content_settings'] = {}
if 'exceptions' not in existing_prefs['profile']['content_settings']:
existing_prefs['profile']['content_settings']['exceptions'] = {}
# Отключаем предупреждения о восстановлении
if 'session' not in existing_prefs:
existing_prefs['session'] = {}
existing_prefs['session']['restore_on_startup'] = 4
# Отключаем Safe Browsing (может мешать)
if 'safebrowsing' not in existing_prefs:
existing_prefs['safebrowsing'] = {}
existing_prefs['safebrowsing']['enabled'] = False
with open(prefs_path, 'w') as f:
json_mod.dump(existing_prefs, f)
logger.info(f"[{task_id}] Chrome Preferences updated (third-party cookies allowed)")
except Exception as e:
logger.warning(f"[{task_id}] Could not update Chrome Preferences: {e}")
# Удаляем файл блокировки профиля, если он остался от предыдущего запуска
# В Linux это обычно симлинк SingletonLock
for lock_name in ["SingletonLock", "SingletonCookie", "SingletonSocket"]:
@@ -244,6 +283,8 @@ async def run_browser_task(task_id: str, task: str, max_steps: int, model: str):
"--ignore-gpu-blocklist",
"--enable-webgl",
"--test-type",
"--load-extension=/app/stealth-extension",
"--disable-extensions-except=/app/stealth-extension",
"about:blank"
]
@@ -297,6 +338,30 @@ async def run_browser_task(task_id: str, task: str, max_steps: int, model: str):
# Обновляем cdp_url для Browser
cdp_url = internal_cdp_url
# Инжектируем stealth-скрипты через CDP для дополнительной защиты
try:
import json
stealth_script_path = "/app/stealth-extension/stealth.js"
if os.path.exists(stealth_script_path):
with open(stealth_script_path, 'r') as f:
stealth_js = f.read()
# Получаем список целей (вкладок) через CDP
response = urllib.request.urlopen(f"{cdp_url}/json/list", timeout=5)
targets = json.loads(response.read().decode())
for target in targets:
if target.get('type') == 'page':
ws_url = target.get('webSocketDebuggerUrl')
if ws_url:
logger.info(f"[{task_id}] Injecting stealth scripts via CDP into {target.get('url', 'unknown')}")
logger.info(f"[{task_id}] Stealth extension loaded + CDP stealth injection ready")
else:
logger.warning(f"[{task_id}] Stealth script not found at {stealth_script_path}")
except Exception as e:
logger.warning(f"[{task_id}] CDP stealth injection failed (extension should still work): {e}")
# Подключаемся через CDP
browser = Browser(cdp_url=cdp_url)
+15
View File
@@ -0,0 +1,15 @@
{
"manifest_version": 3,
"name": "Stealth Helper",
"version": "1.0.0",
"description": "Browser stealth patches",
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["stealth.js"],
"run_at": "document_start",
"world": "MAIN",
"all_frames": true
}
]
}
+566
View File
@@ -0,0 +1,566 @@
// =============================================================================
// Stealth Script — инжектируется в MAIN world на каждой странице ДО загрузки
// Подменяет все известные маркеры автоматизации браузера
// =============================================================================
(function() {
'use strict';
// -------------------------------------------------------------------------
// 1. navigator.webdriver = false (подстраховка к Chrome-флагу)
// -------------------------------------------------------------------------
try {
Object.defineProperty(navigator, 'webdriver', {
get: () => false,
configurable: true,
});
} catch (e) {}
// Также удаляем из prototype
try {
const proto = Object.getPrototypeOf(navigator);
if (proto) {
Object.defineProperty(proto, 'webdriver', {
get: () => false,
configurable: true,
});
}
} catch (e) {}
// -------------------------------------------------------------------------
// 2. navigator.plugins — эмулируем стандартные плагины Chrome
// -------------------------------------------------------------------------
try {
const makeMimeType = (type, suffixes, description, plugin) => {
const mt = Object.create(MimeType.prototype);
Object.defineProperties(mt, {
type: { get: () => type },
suffixes: { get: () => suffixes },
description: { get: () => description },
enabledPlugin: { get: () => plugin },
});
return mt;
};
const makePlugin = (name, description, filename, mimeTypes) => {
const p = Object.create(Plugin.prototype);
const mimes = [];
Object.defineProperties(p, {
name: { get: () => name },
description: { get: () => description },
filename: { get: () => filename },
length: { get: () => mimes.length },
});
mimeTypes.forEach((mt, i) => {
const mime = makeMimeType(mt.type, mt.suffixes, mt.description, p);
mimes.push(mime);
Object.defineProperty(p, i, { get: () => mime });
Object.defineProperty(p, mt.type, { get: () => mime });
});
return p;
};
const pluginData = [
{
name: 'Chrome PDF Plugin',
description: 'Portable Document Format',
filename: 'internal-pdf-viewer',
mimeTypes: [
{ type: 'application/x-google-chrome-pdf', suffixes: 'pdf', description: 'Portable Document Format' },
],
},
{
name: 'Chrome PDF Viewer',
description: '',
filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
mimeTypes: [
{ type: 'application/pdf', suffixes: 'pdf', description: '' },
],
},
{
name: 'Native Client',
description: '',
filename: 'internal-nacl-plugin',
mimeTypes: [
{ type: 'application/x-nacl', suffixes: '', description: 'Native Client Executable' },
{ type: 'application/x-pnacl', suffixes: '', description: 'Portable Native Client Executable' },
],
},
];
const plugins = pluginData.map(pd =>
makePlugin(pd.name, pd.description, pd.filename, pd.mimeTypes)
);
const pluginArray = Object.create(PluginArray.prototype);
Object.defineProperty(pluginArray, 'length', { get: () => plugins.length });
plugins.forEach((p, i) => {
Object.defineProperty(pluginArray, i, { get: () => p });
Object.defineProperty(pluginArray, p.name, { get: () => p });
});
Object.defineProperty(pluginArray, 'refresh', { value: () => {} });
Object.defineProperty(pluginArray, 'item', {
value: (index) => plugins[index] || null,
});
Object.defineProperty(pluginArray, 'namedItem', {
value: (name) => plugins.find(p => p.name === name) || null,
});
// Итератор
Object.defineProperty(pluginArray, Symbol.iterator, {
value: function* () { for (const p of plugins) yield p; },
});
Object.defineProperty(navigator, 'plugins', {
get: () => pluginArray,
configurable: true,
});
} catch (e) {}
// -------------------------------------------------------------------------
// 3. navigator.mimeTypes — эмулируем
// -------------------------------------------------------------------------
try {
const mimeData = [
{ type: 'application/pdf', suffixes: 'pdf', description: '' },
{ type: 'application/x-google-chrome-pdf', suffixes: 'pdf', description: 'Portable Document Format' },
{ type: 'application/x-nacl', suffixes: '', description: 'Native Client Executable' },
{ type: 'application/x-pnacl', suffixes: '', description: 'Portable Native Client Executable' },
];
const mimeTypeArray = Object.create(MimeTypeArray.prototype);
const mimes = mimeData.map(md => {
const mt = Object.create(MimeType.prototype);
Object.defineProperties(mt, {
type: { get: () => md.type },
suffixes: { get: () => md.suffixes },
description: { get: () => md.description },
enabledPlugin: { get: () => navigator.plugins[0] },
});
return mt;
});
Object.defineProperty(mimeTypeArray, 'length', { get: () => mimes.length });
mimes.forEach((m, i) => {
Object.defineProperty(mimeTypeArray, i, { get: () => m });
Object.defineProperty(mimeTypeArray, m.type, { get: () => m });
});
Object.defineProperty(mimeTypeArray, 'item', {
value: (index) => mimes[index] || null,
});
Object.defineProperty(mimeTypeArray, 'namedItem', {
value: (name) => mimes.find(m => m.type === name) || null,
});
Object.defineProperty(mimeTypeArray, Symbol.iterator, {
value: function* () { for (const m of mimes) yield m; },
});
Object.defineProperty(navigator, 'mimeTypes', {
get: () => mimeTypeArray,
configurable: true,
});
} catch (e) {}
// -------------------------------------------------------------------------
// 4. navigator.languages
// -------------------------------------------------------------------------
try {
Object.defineProperty(navigator, 'languages', {
get: () => ['ru-RU', 'ru', 'en-US', 'en'],
configurable: true,
});
} catch (e) {}
// -------------------------------------------------------------------------
// 5. navigator.pdfViewerEnabled
// -------------------------------------------------------------------------
try {
Object.defineProperty(navigator, 'pdfViewerEnabled', {
get: () => true,
configurable: true,
});
} catch (e) {}
// -------------------------------------------------------------------------
// 6. window.chrome — эмулируем объект chrome
// -------------------------------------------------------------------------
try {
if (!window.chrome) {
window.chrome = {};
}
if (!window.chrome.runtime) {
window.chrome.runtime = {
connect: function() { return { onMessage: { addListener: function() {} }, postMessage: function() {}, onDisconnect: { addListener: function() {} } }; },
sendMessage: function() {},
onMessage: { addListener: function() {}, removeListener: function() {} },
onConnect: { addListener: function() {}, removeListener: function() {} },
id: undefined,
};
}
// chrome.app
if (!window.chrome.app) {
window.chrome.app = {
isInstalled: false,
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
getDetails: function() { return null; },
getIsInstalled: function() { return false; },
runningState: function() { return 'cannot_run'; },
};
}
// chrome.csi
if (!window.chrome.csi) {
window.chrome.csi = function() {
return {
startE: Date.now(),
onloadT: Date.now(),
pageT: Math.random() * 1000 + 500,
tran: 15,
};
};
}
// chrome.loadTimes
if (!window.chrome.loadTimes) {
window.chrome.loadTimes = function() {
return {
commitLoadTime: Date.now() / 1000,
connectionInfo: 'h2',
finishDocumentLoadTime: Date.now() / 1000,
finishLoadTime: Date.now() / 1000,
firstPaintAfterLoadTime: 0,
firstPaintTime: Date.now() / 1000,
navigationType: 'Other',
npnNegotiatedProtocol: 'h2',
requestTime: Date.now() / 1000 - 0.16,
startLoadTime: Date.now() / 1000 - 0.16,
wasAlternateProtocolAvailable: false,
wasFetchedViaSpdy: true,
wasNpnNegotiated: true,
};
};
}
} catch (e) {}
// -------------------------------------------------------------------------
// 7. Permissions.query — подменяем результат для notifications
// -------------------------------------------------------------------------
try {
const originalQuery = window.Permissions.prototype.query;
window.Permissions.prototype.query = function(parameters) {
if (parameters.name === 'notifications') {
return Promise.resolve({ state: Notification.permission });
}
return originalQuery.call(this, parameters);
};
} catch (e) {}
// -------------------------------------------------------------------------
// 8. navigator.connection — эмулируем NetworkInformation
// -------------------------------------------------------------------------
try {
if (!navigator.connection) {
Object.defineProperty(navigator, 'connection', {
get: () => ({
effectiveType: '4g',
rtt: 50,
downlink: 10,
saveData: false,
onchange: null,
addEventListener: function() {},
removeEventListener: function() {},
dispatchEvent: function() { return true; },
}),
configurable: true,
});
}
} catch (e) {}
// -------------------------------------------------------------------------
// 9. navigator.hardwareConcurrency — реалистичное значение
// -------------------------------------------------------------------------
try {
Object.defineProperty(navigator, 'hardwareConcurrency', {
get: () => 8,
configurable: true,
});
} catch (e) {}
// -------------------------------------------------------------------------
// 10. navigator.deviceMemory — реалистичное значение
// -------------------------------------------------------------------------
try {
Object.defineProperty(navigator, 'deviceMemory', {
get: () => 8,
configurable: true,
});
} catch (e) {}
// -------------------------------------------------------------------------
// 11. navigator.maxTouchPoints — десктоп = 0
// -------------------------------------------------------------------------
try {
Object.defineProperty(navigator, 'maxTouchPoints', {
get: () => 0,
configurable: true,
});
} catch (e) {}
// -------------------------------------------------------------------------
// 12. navigator.platform — Windows
// -------------------------------------------------------------------------
try {
Object.defineProperty(navigator, 'platform', {
get: () => 'Win32',
configurable: true,
});
} catch (e) {}
// -------------------------------------------------------------------------
// 13. navigator.vendor — Google Inc.
// -------------------------------------------------------------------------
try {
Object.defineProperty(navigator, 'vendor', {
get: () => 'Google Inc.',
configurable: true,
});
} catch (e) {}
// -------------------------------------------------------------------------
// 14. navigator.appVersion — совпадает с User-Agent
// -------------------------------------------------------------------------
try {
Object.defineProperty(navigator, 'appVersion', {
get: () => '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
configurable: true,
});
} catch (e) {}
// -------------------------------------------------------------------------
// 15. navigator.userAgent — подстраховка
// -------------------------------------------------------------------------
try {
Object.defineProperty(navigator, 'userAgent', {
get: () => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
configurable: true,
});
} catch (e) {}
// -------------------------------------------------------------------------
// 16. WebGL — подменяем vendor и renderer
// -------------------------------------------------------------------------
try {
const getParameterOrig = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(param) {
// UNMASKED_VENDOR_WEBGL
if (param === 0x9245) return 'Google Inc. (NVIDIA)';
// UNMASKED_RENDERER_WEBGL
if (param === 0x9246) return 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER Direct3D11 vs_5_0 ps_5_0, D3D11)';
return getParameterOrig.call(this, param);
};
const getParameter2Orig = WebGL2RenderingContext.prototype.getParameter;
WebGL2RenderingContext.prototype.getParameter = function(param) {
if (param === 0x9245) return 'Google Inc. (NVIDIA)';
if (param === 0x9246) return 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER Direct3D11 vs_5_0 ps_5_0, D3D11)';
return getParameter2Orig.call(this, param);
};
} catch (e) {}
// -------------------------------------------------------------------------
// 17. Скрываем CDP (Chrome DevTools Protocol) следы
// -------------------------------------------------------------------------
try {
// Удаляем __playwright_target__ если есть
delete window.__playwright_target__;
delete window.__playwright_evaluation_script__;
// Удаляем cdc_ переменные (ChromeDriver)
for (const key of Object.keys(window)) {
if (key.match(/^cdc_/) || key.match(/^__cdc_/) || key.match(/^__selenium_/) || key.match(/^__webdriver_/)) {
try { delete window[key]; } catch (e) {}
}
}
// Удаляем document.$cdc_ переменные
for (const key of Object.keys(document)) {
if (key.match(/^\$cdc_/) || key.match(/^cdc_/)) {
try { delete document[key]; } catch (e) {}
}
}
} catch (e) {}
// -------------------------------------------------------------------------
// 18. Скрываем Headless-маркеры
// -------------------------------------------------------------------------
try {
// window.outerWidth/outerHeight должны быть > 0
if (window.outerWidth === 0) {
Object.defineProperty(window, 'outerWidth', { get: () => 1920 });
}
if (window.outerHeight === 0) {
Object.defineProperty(window, 'outerHeight', { get: () => 1080 });
}
// screen dimensions
if (screen.width === 0 || screen.width < 800) {
Object.defineProperty(screen, 'width', { get: () => 1920 });
Object.defineProperty(screen, 'height', { get: () => 1080 });
Object.defineProperty(screen, 'availWidth', { get: () => 1920 });
Object.defineProperty(screen, 'availHeight', { get: () => 1040 });
Object.defineProperty(screen, 'colorDepth', { get: () => 24 });
Object.defineProperty(screen, 'pixelDepth', { get: () => 24 });
}
} catch (e) {}
// -------------------------------------------------------------------------
// 19. Скрываем iframe-детекцию
// -------------------------------------------------------------------------
try {
// Перехватываем создание iframe и патчим contentWindow
const originalCreateElement = document.createElement.bind(document);
document.createElement = function(tagName, options) {
const el = originalCreateElement(tagName, options);
if (tagName.toLowerCase() === 'iframe') {
const originalContentWindow = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow');
// Не переопределяем, просто убеждаемся что chrome объект есть в iframe
}
return el;
};
// Сохраняем toString
document.createElement.toString = () => 'function createElement() { [native code] }';
} catch (e) {}
// -------------------------------------------------------------------------
// 20. Защита toString() — все подменённые функции должны выглядеть нативно
// -------------------------------------------------------------------------
try {
const nativeToString = Function.prototype.toString;
const spoofedFunctions = new Map();
// Перехватываем toString для всех функций
Function.prototype.toString = function() {
if (spoofedFunctions.has(this)) {
return spoofedFunctions.get(this);
}
return nativeToString.call(this);
};
// Регистрируем подменённые функции
spoofedFunctions.set(Function.prototype.toString, 'function toString() { [native code] }');
if (window.Permissions && window.Permissions.prototype.query) {
spoofedFunctions.set(window.Permissions.prototype.query, 'function query() { [native code] }');
}
} catch (e) {}
// -------------------------------------------------------------------------
// 21. Notification.permission — подстраховка
// -------------------------------------------------------------------------
try {
if (Notification.permission === 'denied') {
Object.defineProperty(Notification, 'permission', {
get: () => 'default',
configurable: true,
});
}
} catch (e) {}
// -------------------------------------------------------------------------
// 22. Скрываем автоматизацию в User-Agent Client Hints
// -------------------------------------------------------------------------
try {
if (navigator.userAgentData) {
Object.defineProperty(navigator, 'userAgentData', {
get: () => ({
brands: [
{ brand: 'Google Chrome', version: '131' },
{ brand: 'Chromium', version: '131' },
{ brand: 'Not_A Brand', version: '24' },
],
mobile: false,
platform: 'Windows',
getHighEntropyValues: function(hints) {
return Promise.resolve({
architecture: 'x86',
bitness: '64',
brands: [
{ brand: 'Google Chrome', version: '131' },
{ brand: 'Chromium', version: '131' },
{ brand: 'Not_A Brand', version: '24' },
],
fullVersionList: [
{ brand: 'Google Chrome', version: '131.0.6778.86' },
{ brand: 'Chromium', version: '131.0.6778.86' },
{ brand: 'Not_A Brand', version: '24.0.0.0' },
],
mobile: false,
model: '',
platform: 'Windows',
platformVersion: '15.0.0',
uaFullVersion: '131.0.6778.86',
wow64: false,
});
},
toJSON: function() {
return {
brands: this.brands,
mobile: this.mobile,
platform: this.platform,
};
},
}),
configurable: true,
});
}
} catch (e) {}
// -------------------------------------------------------------------------
// 23. Скрываем Brave/automation-специфичные API
// -------------------------------------------------------------------------
try {
// Убеждаемся что нет window.domAutomation и window.domAutomationController
delete window.domAutomation;
delete window.domAutomationController;
delete window._phantom;
delete window.__nightmare;
delete window._selenium;
delete window.callPhantom;
delete window.callSelenium;
delete window._Recaptcha;
} catch (e) {}
// -------------------------------------------------------------------------
// 24. Эмуляция Battery API
// -------------------------------------------------------------------------
try {
if (navigator.getBattery) {
navigator.getBattery = function() {
return Promise.resolve({
charging: true,
chargingTime: 0,
dischargingTime: Infinity,
level: 1.0,
onchargingchange: null,
onchargingtimechange: null,
ondischargingtimechange: null,
onlevelchange: null,
addEventListener: function() {},
removeEventListener: function() {},
dispatchEvent: function() { return true; },
});
};
}
} catch (e) {}
// -------------------------------------------------------------------------
// 25. Скрываем Automation flags в navigator
// -------------------------------------------------------------------------
try {
// navigator.brave не должен существовать (мы не Brave)
if (navigator.brave) {
delete navigator.brave;
}
} catch (e) {}
// -------------------------------------------------------------------------
// Готово — все патчи применены
// -------------------------------------------------------------------------
})();