WalleGriffkinder commited on
Commit
fe3af69
·
verified ·
1 Parent(s): 7eda510

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +179 -91
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 token = uuidv4();
25
- const expiresAt = Date.now() + CHALLENGE_EXPIRY_MS;
26
- challengeStore[token] = { issuedAt: Date.now(), expiresAt, used: false };
27
- res.json({ challengeToken: token });
28
- });
 
29
 
30
- function sha256(data) {
31
- return crypto.createHash('sha256').update(data).digest('hex');
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
- const { usedChallengeToken, canvasDataUrl } = clientData;
41
- let canvasCheckPassed = false;
42
-
43
- if (!usedChallengeToken) {
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
- reasons.push("Canvas data URL присутствует.");
 
 
 
 
 
 
 
 
 
66
  }
67
  }
68
 
69
- if (!canvasCheckPassed && score > 10) {
70
- score = Math.min(score, 10);
71
- reasons.push("Основная проверка Canvas по токену не пройдена, сильно снижен рейтинг.");
72
- }
73
-
74
- if (clientData.webglPixelSample) {
75
- const sample = clientData.webglPixelSample;
76
- if (sample.length >= 4 && sample.slice(0, 4).every(p => p === 0)) {
77
- score -= 10;
78
- reasons.push("WebGL: Считанные пиксели нулевые, возможно, рендеринг не удался или подделан.");
79
- } else if (sample.length > 0) {
80
- score += 5;
81
- reasons.push("WebGL: Данные о пикселях присутствуют.");
 
 
 
 
 
 
82
  }
83
- } else if (clientData.webglError) {
84
- score -= 5;
85
- reasons.push(`WebGL: Ошибка инициализации/работы (${clientData.webglError}).`);
86
- } else {
87
- reasons.push("WebGL: Данные о пикселях отсутствуют.");
88
- }
89
 
90
- if (clientData.navigator?.webdriver === true) {
91
- score -= 50;
92
- reasons.push("Обнаружен navigator.webdriver=true.");
 
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
- const ua = clientData.navigator?.userAgent || "";
101
- if (ua.toLowerCase().includes("bot") || ua.toLowerCase().includes("spider") || ua.toLowerCase().includes("headless")) {
102
- if (!ua.toLowerCase().includes("headlesschrome")) {
103
- score -= 20;
104
- reasons.push("Подозрительный User-Agent.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
- }
107
 
108
- score = Math.max(0, Math.min(score, 100));
109
 
110
- let threshold = canvasCheckPassed ? 50 : 85;
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
- if (score >= threshold) {
113
- verdict = "Разрешено (Allow)";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  } else {
115
- verdict = "Блокировать (Block)";
 
116
  }
117
 
118
- console.log(`Check for token ${usedChallengeToken}: Score=${score}, Verdict=${verdict}`);
119
- console.log(`Client User-Agent: ${clientData.navigator?.userAgent}`);
 
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
  });