Update server.js
Browse files
server.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
|
|
| 1 |
const express = require('express');
|
| 2 |
const { v4: uuidv4 } = require('uuid');
|
| 3 |
const crypto = require('crypto');
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
const app = express();
|
| 6 |
const port = process.env.PORT || 3000;
|
|
@@ -10,129 +14,213 @@ app.use(express.json({ limit: '5mb' }));
|
|
| 10 |
const challengeStore = {};
|
| 11 |
const CHALLENGE_EXPIRY_MS = 5 * 60 * 1000;
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
app.use((req, res, next) => {
|
| 14 |
res.header('Access-Control-Allow-Origin', '*');
|
| 15 |
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
| 16 |
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
| 17 |
-
if (req.method === 'OPTIONS')
|
| 18 |
-
return res.sendStatus(200);
|
| 19 |
-
}
|
| 20 |
next();
|
| 21 |
});
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
app.get('/api/challenge', (req, res) => {
|
| 24 |
-
const
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
}
|
|
|
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
app.post('/api/check', (req, res) => {
|
| 35 |
const clientData = req.body;
|
| 36 |
-
let score = 100;
|
| 37 |
let verdict = "Не определено";
|
| 38 |
const reasons = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
score -= 90;
|
| 45 |
-
reasons.push("Отсутствует challenge токен.");
|
| 46 |
-
} else if (!challengeStore[usedChallengeToken]) {
|
| 47 |
-
score -= 90;
|
| 48 |
-
reasons.push("Неизвестный challenge токен.");
|
| 49 |
-
} else if (challengeStore[usedChallengeToken].used) {
|
| 50 |
-
score -= 90;
|
| 51 |
-
reasons.push("Challenge токен уже был использован.");
|
| 52 |
-
} else if (Date.now() > challengeStore[usedChallengeToken].expiresAt) {
|
| 53 |
-
score -= 90;
|
| 54 |
-
reasons.push("Challenge токен истек.");
|
| 55 |
-
} else {
|
| 56 |
-
challengeStore[usedChallengeToken].used = true;
|
| 57 |
-
canvasCheckPassed = true;
|
| 58 |
-
reasons.push("Challenge токен валиден.");
|
| 59 |
-
|
| 60 |
-
if (!canvasDataUrl || !canvasDataUrl.startsWith('data:image/png;base64,')) {
|
| 61 |
-
score -= 20;
|
| 62 |
-
reasons.push("Canvas data URL отсутствует или имеет неверный формат.");
|
| 63 |
-
canvasCheckPassed = false;
|
| 64 |
} else {
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
}
|
| 67 |
}
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
score
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
}
|
| 83 |
-
|
| 84 |
-
score -= 5;
|
| 85 |
-
reasons.push(`WebGL: Ошибка инициализации/работы (${clientData.webglError}).`);
|
| 86 |
-
} else {
|
| 87 |
-
reasons.push("WebGL: Данные о пикселях отсутствуют.");
|
| 88 |
-
}
|
| 89 |
|
| 90 |
-
if (
|
| 91 |
-
score -=
|
| 92 |
-
|
|
|
|
| 93 |
}
|
| 94 |
|
| 95 |
-
if (clientData.automationVars && Object.keys(clientData.automationVars).length > 0) {
|
| 96 |
-
score -= 30;
|
| 97 |
-
reasons.push(`Обнаружены переменные автоматизации: ${Object.keys(clientData.automationVars).join(', ')}.`);
|
| 98 |
-
}
|
| 99 |
|
| 100 |
-
|
| 101 |
-
if (
|
| 102 |
-
if (
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
}
|
| 106 |
-
}
|
| 107 |
|
| 108 |
-
score
|
| 109 |
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
} else {
|
| 115 |
-
verdict = "Блокировать (
|
|
|
|
| 116 |
}
|
| 117 |
|
| 118 |
-
|
| 119 |
-
console.log(`
|
|
|
|
| 120 |
|
| 121 |
-
res.json({
|
| 122 |
-
verdictText: verdict,
|
| 123 |
-
score: score,
|
| 124 |
-
reasons: reasons,
|
| 125 |
-
receivedData: {
|
| 126 |
-
usedChallengeToken: clientData.usedChallengeToken,
|
| 127 |
-
canvasDataUrlLength: clientData.canvasDataUrl?.length,
|
| 128 |
-
webglPixelSampleStart: clientData.webglPixelSample?.slice(0,8),
|
| 129 |
-
webdriver: clientData.navigator?.webdriver,
|
| 130 |
-
userAgent: clientData.navigator?.userAgent
|
| 131 |
-
}
|
| 132 |
-
});
|
| 133 |
});
|
| 134 |
|
| 135 |
-
setInterval(() => {
|
| 136 |
const now = Date.now();
|
| 137 |
for (const token in challengeStore) {
|
| 138 |
if (challengeStore[token].expiresAt < now || (challengeStore[token].used && (now - challengeStore[token].issuedAt > CHALLENGE_EXPIRY_MS * 2))) {
|
|
@@ -142,5 +230,5 @@ setInterval(() => {
|
|
| 142 |
}, 60 * 1000);
|
| 143 |
|
| 144 |
app.listen(port, () => {
|
| 145 |
-
console.log(`Browser check API server listening on port ${port}`);
|
| 146 |
});
|
|
|
|
| 1 |
+
// server.js
|
| 2 |
const express = require('express');
|
| 3 |
const { v4: uuidv4 } = require('uuid');
|
| 4 |
const crypto = require('crypto');
|
| 5 |
+
const { exec } = require('child_process');
|
| 6 |
+
const fs = require('fs').promises;
|
| 7 |
+
const path = require('path');
|
| 8 |
|
| 9 |
const app = express();
|
| 10 |
const port = process.env.PORT || 3000;
|
|
|
|
| 14 |
const challengeStore = {};
|
| 15 |
const CHALLENGE_EXPIRY_MS = 5 * 60 * 1000;
|
| 16 |
|
| 17 |
+
// Обновленный OCR_CHAR_SET
|
| 18 |
+
const OCR_CHAR_SET = "ABCDEFGHJKMNPQRSTUVWXYZ2345689"; // Без 0, O, I, 1, L, 7 (7 похожа на 1)
|
| 19 |
+
const OCR_CHALLENGE_LENGTH = 6;
|
| 20 |
+
|
| 21 |
+
const WEBGL_CHALLENGE_COLORS = [
|
| 22 |
+
{ name: "red", rgb: [1.0, 0.0, 0.0] }, { name: "green", rgb: [0.0, 1.0, 0.0] },
|
| 23 |
+
{ name: "blue", rgb: [0.0, 0.0, 1.0] }, { name: "yellow", rgb: [1.0, 1.0, 0.0] },
|
| 24 |
+
{ name: "magenta", rgb: [1.0, 0.0, 1.0] }, { name: "cyan", rgb: [0.0, 1.0, 1.0] }
|
| 25 |
+
];
|
| 26 |
+
|
| 27 |
app.use((req, res, next) => {
|
| 28 |
res.header('Access-Control-Allow-Origin', '*');
|
| 29 |
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
| 30 |
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
| 31 |
+
if (req.method === 'OPTIONS') return res.sendStatus(200);
|
|
|
|
|
|
|
| 32 |
next();
|
| 33 |
});
|
| 34 |
|
| 35 |
+
async function recognizeTextWithTesseract(imageDataUrl) {
|
| 36 |
+
if (!imageDataUrl || !imageDataUrl.startsWith('data:image/png;base64,')) return null;
|
| 37 |
+
const base64Data = imageDataUrl.replace(/^data:image\/png;base64,/, "");
|
| 38 |
+
const tempImageFileName = `temp_ocr_image_${uuidv4()}.png`;
|
| 39 |
+
const tempImagePath = path.join(__dirname, tempImageFileName);
|
| 40 |
+
|
| 41 |
+
try {
|
| 42 |
+
await fs.writeFile(tempImagePath, base64Data, 'base64');
|
| 43 |
+
// Используем обновленный whitelist
|
| 44 |
+
const tesseractConfig = `-c tessedit_char_whitelist=${OCR_CHAR_SET}`;
|
| 45 |
+
return new Promise((resolve, reject) => {
|
| 46 |
+
// --psm 7: Treat the image as a single text line.
|
| 47 |
+
// --psm 8: Treat the image as a single word. (Может быть лучше для коротких строк)
|
| 48 |
+
// --psm 10: Treat the image as a single character.
|
| 49 |
+
exec(`tesseract ${tempImagePath} stdout ${tesseractConfig} -l eng --psm 8`, (error, stdout, stderr) => {
|
| 50 |
+
if (error) {
|
| 51 |
+
console.error(`Tesseract error: ${error.message} (stderr: ${stderr})`);
|
| 52 |
+
return reject(new Error(`Tesseract execution failed`));
|
| 53 |
+
}
|
| 54 |
+
resolve(stdout.trim().replace(new RegExp(`[^${OCR_CHAR_SET}]`, 'g'), '')); // Очищаем строго по OCR_CHAR_SET
|
| 55 |
+
});
|
| 56 |
+
});
|
| 57 |
+
} catch (err) {
|
| 58 |
+
console.error("Error in Tesseract processing:", err);
|
| 59 |
+
return null;
|
| 60 |
+
} finally {
|
| 61 |
+
try { await fs.unlink(tempImagePath); } catch (e) { /* ignore */ }
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
app.get('/api/challenge', (req, res) => {
|
| 66 |
+
const sessionToken = uuidv4();
|
| 67 |
+
let ocrChallengeString = "";
|
| 68 |
+
for (let i = 0; i < OCR_CHALLENGE_LENGTH; i++) {
|
| 69 |
+
ocrChallengeString += OCR_CHAR_SET[Math.floor(Math.random() * OCR_CHAR_SET.length)];
|
| 70 |
+
}
|
| 71 |
+
const webglChallengeColor = WEBGL_CHALLENGE_COLORS[Math.floor(Math.random() * WEBGL_CHALLENGE_COLORS.length)];
|
| 72 |
|
| 73 |
+
challengeStore[sessionToken] = {
|
| 74 |
+
issuedAt: Date.now(), expiresAt: Date.now() + CHALLENGE_EXPIRY_MS, used: false, ip: req.ip,
|
| 75 |
+
expectedOcrText: ocrChallengeString,
|
| 76 |
+
expectedWebglColorName: webglChallengeColor.name, expectedWebglColorRgb: webglChallengeColor.rgb
|
| 77 |
+
};
|
| 78 |
+
res.json({
|
| 79 |
+
sessionToken: sessionToken, ocrChallengeText: ocrChallengeString,
|
| 80 |
+
webglChallengeColor: webglChallengeColor.rgb,
|
| 81 |
+
});
|
| 82 |
+
});
|
| 83 |
|
| 84 |
+
app.post('/api/check', async (req, res) => {
|
| 85 |
const clientData = req.body;
|
| 86 |
+
let score = 100; // Начинаем с идеального счета
|
| 87 |
let verdict = "Не определено";
|
| 88 |
const reasons = [];
|
| 89 |
+
let isLikelyBot = false; // Флаг для явного определения бота
|
| 90 |
+
|
| 91 |
+
const { sessionToken, ocrCanvasDataUrl, webglChallengeResult, navigatorInfo, performanceTimings } = clientData;
|
| 92 |
+
let challengeSessionValid = false;
|
| 93 |
+
let serverChallengeDetails = null;
|
| 94 |
+
|
| 95 |
+
// 1. Проверка сессионного токена (КРИТИЧНО)
|
| 96 |
+
if (!sessionToken) { score = 0; reasons.push("FATAL: Отсутствует сессионный токен."); isLikelyBot = true; }
|
| 97 |
+
else if (!challengeStore[sessionToken]) { score = 0; reasons.push("FATAL: Неизвестный сессионный токен."); isLikelyBot = true; }
|
| 98 |
+
else if (challengeStore[sessionToken].used) { score = 0; reasons.push("FATAL: Сессионный токен уже был использован."); isLikelyBot = true; }
|
| 99 |
+
else if (Date.now() > challengeStore[sessionToken].expiresAt) { score = 0; reasons.push("FATAL: Сессионный токен истек."); isLikelyBot = true; }
|
| 100 |
+
else {
|
| 101 |
+
serverChallengeDetails = challengeStore[sessionToken];
|
| 102 |
+
challengeSessionValid = true;
|
| 103 |
+
reasons.push("OK: Сессионный токен валиден.");
|
| 104 |
+
}
|
| 105 |
|
| 106 |
+
// 2. OCR Проверка Canvas (КРИТИЧНО, если токен валиден)
|
| 107 |
+
if (challengeSessionValid) {
|
| 108 |
+
if (!ocrCanvasDataUrl || !ocrCanvasDataUrl.startsWith('data:image/png;base64,') || ocrCanvasDataUrl.length < 300) { // Немного увеличил мин. длину
|
| 109 |
+
score -= 70; reasons.push("CRITICAL_OCR: Canvas для OCR отсутствует, невалиден или слишком мал."); isLikelyBot = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
} else {
|
| 111 |
+
const recognizedText = await recognizeTextWithTesseract(ocrCanvasDataUrl);
|
| 112 |
+
const expectedText = serverChallengeDetails.expectedOcrText;
|
| 113 |
+
if (recognizedText !== null) {
|
| 114 |
+
if (recognizedText === expectedText) {
|
| 115 |
+
score = Math.min(score + 5, 100); // Небольшой бонус, но не выше 100
|
| 116 |
+
reasons.push(`OK_OCR: Tesseract успешно распознал текст: "${recognizedText}".`);
|
| 117 |
+
} else {
|
| 118 |
+
score -= 80; reasons.push(`CRITICAL_OCR: Tesseract распознал текст НЕВЕРНО. Ожидалось: "${expectedText}", Распознано: "${recognizedText}".`); isLikelyBot = true;
|
| 119 |
+
}
|
| 120 |
+
} else { score -= 75; reasons.push("CRITICAL_OCR: Ошибка выполнения Tesseract OCR на сервере."); isLikelyBot = true; }
|
| 121 |
}
|
| 122 |
}
|
| 123 |
|
| 124 |
+
// 3. WebGL Challenge Проверка (ВАЖНО, если токен валиден)
|
| 125 |
+
if (challengeSessionValid && webglChallengeResult) {
|
| 126 |
+
const expectedRgb = serverChallengeDetails.expectedWebglColorRgb;
|
| 127 |
+
// Alpha может быть не 1.0 если фон прозрачный, но цвет треугольника должен быть с alpha 1.0
|
| 128 |
+
const expectedPixel = [Math.round(expectedRgb[0]*255), Math.round(expectedRgb[1]*255), Math.round(expectedRgb[2]*255)]; // Сравниваем только RGB
|
| 129 |
+
|
| 130 |
+
if (webglChallengeResult.pixels && webglChallengeResult.pixels.length >= 3) {
|
| 131 |
+
const clientPixel = webglChallengeResult.pixels.slice(0,3);
|
| 132 |
+
// Более строгий допуск для цвета
|
| 133 |
+
let match = clientPixel.every((val, idx) => Math.abs(val - expectedPixel[idx]) <= 5);
|
| 134 |
+
if (match) { score = Math.min(score + 3, 100); reasons.push(`OK_WEBGL: WebGL пиксели соответствуют цвету "${serverChallengeDetails.expectedWebglColorName}".`);}
|
| 135 |
+
else { score -= 35; reasons.push(`FAIL_WEBGL: WebGL пиксели Н�� соответствуют цвету "${serverChallengeDetails.expectedWebglColorName}". Ожидались ~[${expectedPixel.join(',')}], получены [${clientPixel.join(',')}].`); isLikelyBot = true; }
|
| 136 |
+
} else { score -= 30; reasons.push("FAIL_WEBGL: Данные о пикселях WebGL challenge отсутствуют или неполные."); isLikelyBot = true; }
|
| 137 |
+
|
| 138 |
+
const ua = navigatorInfo?.userAgent?.toLowerCase() || "";
|
| 139 |
+
const renderer = webglChallengeResult.renderer?.toLowerCase() || "";
|
| 140 |
+
const vendor = webglChallengeResult.vendor?.toLowerCase() || "";
|
| 141 |
+
if (renderer.includes("swiftshader") || renderer.includes("llvmpipe")) { // Явные признаки программного рендеринга
|
| 142 |
+
score -= 25; reasons.push("SUSPICIOUS_WEBGL: Обнаружен программный WebGL рендерер (SwiftShader/LLVMpipe)."); isLikelyBot = true;
|
| 143 |
}
|
| 144 |
+
// ... (другие проверки renderer/vendor можно добавить) ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
+
} else if (challengeSessionValid && !webglChallengeResult?.error) {
|
| 147 |
+
score -= 25; reasons.push("FAIL_WEBGL: Данные WebGL challenge отсутствуют, хотя ошибок на клиенте не заявлено."); isLikelyBot = true;
|
| 148 |
+
} else if (webglChallengeResult?.error) {
|
| 149 |
+
score -= 15; reasons.push(`WARN_WEBGL: Ошибка WebGL на клиенте: ${webglChallengeResult.error}.`);
|
| 150 |
}
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
+
// 4. Анализ NavigatorInfo (ВТОРИЧНЫЕ ПРИЗНАКИ)
|
| 154 |
+
if (navigatorInfo) {
|
| 155 |
+
if (navigatorInfo.webdriver === true) { score -= 60; reasons.push("FATAL_NAV: navigator.webdriver=true."); isLikelyBot = true; }
|
| 156 |
+
|
| 157 |
+
if (!navigatorInfo.userAgent || navigatorInfo.userAgent === "") { score -= 20; reasons.push("FAIL_NAV: User-Agent пустой."); isLikelyBot = true; }
|
| 158 |
+
else if (navigatorInfo.userAgent.toLowerCase().includes("bot") ||
|
| 159 |
+
navigatorInfo.userAgent.toLowerCase().includes("spider") ||
|
| 160 |
+
navigatorInfo.userAgent.toLowerCase().includes("headless")) {
|
| 161 |
+
if (!navigatorInfo.userAgent.toLowerCase().includes("headlesschrome")) { // HeadlessChrome может быть для тестов
|
| 162 |
+
score -= 40; reasons.push("FAIL_NAV: User-Agent идентифицирован как бот/паук/headless."); isLikelyBot = true;
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
// Проверка на несоответствие платформы и User-Agent (очень упрощенно)
|
| 166 |
+
const platform = navigatorInfo.platform?.toLowerCase() || "";
|
| 167 |
+
const ua = navigatorInfo.userAgent?.toLowerCase() || "";
|
| 168 |
+
if (platform.includes("win") && (ua.includes("linux") || ua.includes("mac os"))) {
|
| 169 |
+
score -= 15; reasons.push("SUSPICIOUS_NAV: Несоответствие Platform (Win) и User-Agent (Linux/Mac).");
|
| 170 |
+
}
|
| 171 |
+
if ((platform.includes("linux") || platform.includes("mac")) && ua.includes("windows nt")) {
|
| 172 |
+
score -= 15; reasons.push("SUSPICIOUS_NAV: Несоответствие Platform (Linux/Mac) и User-Agent (Win).");
|
| 173 |
}
|
|
|
|
| 174 |
|
| 175 |
+
} else { score -= 10; reasons.push("WARN_NAV: Информация Navigator отсутствует."); }
|
| 176 |
|
| 177 |
+
// 5. Анализ PerformanceTimings (ВТОРИЧНЫЕ ПРИЗНАКИ)
|
| 178 |
+
if (performanceTimings) {
|
| 179 |
+
const { dataCollectionTime, ocrCanvasRenderTime, webglRenderTime } = performanceTimings;
|
| 180 |
+
// Сделаем проверки строже
|
| 181 |
+
if (typeof dataCollectionTime === 'number' && dataCollectionTime < 5 && challengeSessionValid) {
|
| 182 |
+
score -= 20; reasons.push("SUSPICIOUS_PERF: Аномально быстрое время сбора данных (<5ms)."); isLikelyBot = true;
|
| 183 |
+
}
|
| 184 |
+
if (typeof ocrCanvasRenderTime === 'number' && ocrCanvasRenderTime < 1 && ocrCanvasDataUrl && challengeSessionValid) {
|
| 185 |
+
score -= 25; reasons.push("SUSPICIOUS_PERF: Аномально быстрое время рендера OCR Canvas (<1ms)."); isLikelyBot = true;
|
| 186 |
+
}
|
| 187 |
+
if (typeof webglRenderTime === 'number' && webglRenderTime < 1 && webglChallengeResult?.pixels && challengeSessionValid) {
|
| 188 |
+
score -= 25; reasons.push("SUSPICIOUS_PERF: Аномально быстрое время рендера WebGL (<1ms)."); isLikelyBot = true;
|
| 189 |
+
}
|
| 190 |
+
} else { score -= 5; reasons.push("WARN_PERF: Данные о времени выполнения отсутствуют."); }
|
| 191 |
|
| 192 |
+
// 6. Переменные автоматизации (КРИТИЧНО)
|
| 193 |
+
if (clientData.automationVars && Object.keys(clientData.automationVars).length > 0) {
|
| 194 |
+
score -= 70; reasons.push(`FATAL_AUTO: Обнаружены переменные автоматизации: ${Object.keys(clientData.automationVars).join(', ')}.`); isLikelyBot = true;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
score = Math.max(0, Math.min(score, 100)); // Окончательный счет
|
| 198 |
+
|
| 199 |
+
// --- Вынесение вердикта ---
|
| 200 |
+
// Если любая из FATAL или CRITICAL проверок провалена, или isLikelyBot=true, то это почти всегда блок.
|
| 201 |
+
// Порог для "Разрешено" должен быть высоким.
|
| 202 |
+
let blockThreshold = 30; // Если score упал ниже этого, точно блок.
|
| 203 |
+
let allowThreshold = 85; // Нужно набрать столько для разрешения.
|
| 204 |
+
|
| 205 |
+
if (isLikelyBot || score < blockThreshold) {
|
| 206 |
+
verdict = "Блокировать (Bot Detected)";
|
| 207 |
+
if (!isLikelyBot) reasons.push("OVERRIDE: Блокировка из-за низкого общего балла.");
|
| 208 |
+
} else if (score >= allowThreshold && challengeSessionValid) { // Дополнительно проверяем challengeSessionValid
|
| 209 |
+
verdict = "Разрешено (Human Likely)";
|
| 210 |
+
if (serverChallengeDetails) serverChallengeDetails.used = true; // Помечаем токен как использованный
|
| 211 |
} else {
|
| 212 |
+
verdict = "Блокировать (Suspicious)"; // Если не бот, но и не прошел порог
|
| 213 |
+
reasons.push("INFO: Недостаточный балл для разрешения, но не определен как явный бот.");
|
| 214 |
}
|
| 215 |
|
| 216 |
+
// Логирование
|
| 217 |
+
console.log(`[${new Date().toISOString()}] Check: Token=${sessionToken || 'N/A'}, Score=${score}, Verdict=${verdict}, IP=${req.ip}, LikelyBot=${isLikelyBot}`);
|
| 218 |
+
if (reasons.length > 0) console.log(`Reasons: ${reasons.join(' | ')}`);
|
| 219 |
|
| 220 |
+
res.json({ verdictText: verdict, score: score, reasons: reasons, isBot: isLikelyBot });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
});
|
| 222 |
|
| 223 |
+
setInterval(() => { /* ... (очистка токенов как раньше) ... */
|
| 224 |
const now = Date.now();
|
| 225 |
for (const token in challengeStore) {
|
| 226 |
if (challengeStore[token].expiresAt < now || (challengeStore[token].used && (now - challengeStore[token].issuedAt > CHALLENGE_EXPIRY_MS * 2))) {
|
|
|
|
| 230 |
}, 60 * 1000);
|
| 231 |
|
| 232 |
app.listen(port, () => {
|
| 233 |
+
console.log(`Strict Browser check API server listening on port ${port}`);
|
| 234 |
});
|