GeminiBot commited on
Commit
e97c1d1
·
1 Parent(s): 02bd6b6

Restore original iframe-based challenge solver logic

Browse files
Files changed (1) hide show
  1. src/duckai.ts +139 -241
src/duckai.ts CHANGED
@@ -1,193 +1,66 @@
1
  import { JSDOM } from "jsdom";
2
  import { createHash } from "node:crypto";
 
3
  import UserAgent from "user-agents";
4
 
5
  export class DuckAI {
6
- private async solveChallenge(vqdHash: string, ua: string): Promise<string> {
7
- try {
8
- const jsScript = Buffer.from(vqdHash, 'base64').toString('utf-8');
9
-
10
- const dom = new JSDOM(`<!DOCTYPE html><html><body></body></html>`, {
11
- url: "https://duckduckgo.com/",
12
- referrer: "https://duckduckgo.com/",
13
- runScripts: "dangerously",
14
- resources: "usable"
15
- });
16
-
17
- const mockWindow = dom.window as any;
18
-
19
- // Self-reference fixes
20
- mockWindow.self = mockWindow;
21
- mockWindow.parent = mockWindow;
22
- mockWindow.top = mockWindow;
23
-
24
- // IMMORTAL DOM Strategy
25
- const originalGetElementById = mockWindow.document.getElementById;
26
- mockWindow.document.getElementById = function(id: string) {
27
- const el = originalGetElementById.call(this, id);
28
- if (el) return el;
29
- return mockWindow.document.createElement('div');
30
- };
31
-
32
- const originalQuerySelector = mockWindow.document.querySelector;
33
- mockWindow.document.querySelector = function(selector: string) {
34
- try {
35
- const el = originalQuerySelector.call(this, selector);
36
- if (el) return el;
37
- } catch(e) {}
38
- return mockWindow.document.createElement('div');
39
- };
40
-
41
- // ГЛУБОКАЯ ЭМУЛЯЦИЯ
42
- Object.defineProperties(mockWindow.navigator, {
43
- userAgent: { value: ua },
44
- platform: { value: 'Win32' },
45
- webdriver: { value: false },
46
- languages: { value: ['en-US', 'en'] }
47
- });
48
-
49
- mockWindow.screen = { width: 1920, height: 1080, availWidth: 1920, availHeight: 1080 };
50
- mockWindow.chrome = { runtime: {} };
51
-
52
- // Aggressive Patch for iframe contentDocument
53
- Object.defineProperty(mockWindow.HTMLIFrameElement.prototype, 'contentDocument', {
54
- get: function() { return mockWindow.document; },
55
- configurable: true
56
- });
57
-
58
- Object.defineProperty(mockWindow.HTMLIFrameElement.prototype, 'contentWindow', {
59
- get: function() { return mockWindow; },
60
- configurable: true
61
- });
62
-
63
- // Also keep the createElement hook just in case
64
- const originalCreateElement = mockWindow.document.createElement;
65
- mockWindow.document.createElement = function(tagName: string) {
66
- const element = originalCreateElement.call(this, tagName);
67
- if (tagName.toLowerCase() === 'iframe') {
68
- try {
69
- Object.defineProperty(element, 'contentDocument', {
70
- get: () => mockWindow.document,
71
- configurable: true
72
- });
73
- Object.defineProperty(element, 'contentWindow', {
74
- get: () => mockWindow,
75
- configurable: true
76
- });
77
- } catch (e) {}
78
- }
79
- return element;
80
- };
81
-
82
- // Robust Canvas Mock
83
- const originalGetContext = mockWindow.HTMLCanvasElement.prototype.getContext;
84
- mockWindow.HTMLCanvasElement.prototype.getContext = function (type: string, options?: any) {
85
- try {
86
- const ctx = originalGetContext.call(this, type, options);
87
- if (ctx) return ctx;
88
- } catch (e) {}
89
-
90
- return {
91
- canvas: this,
92
- fillRect: () => {},
93
- clearRect: () => {},
94
- getImageData: (x: number, y: number, w: number, h: number) => ({
95
- data: new Uint8ClampedArray(w * h * 4),
96
- width: w,
97
- height: h
98
- }),
99
- putImageData: () => {},
100
- createImageData: () => ({ data: new Uint8ClampedArray(4) }),
101
- setTransform: () => {},
102
- drawImage: () => {},
103
- save: () => {},
104
- restore: () => {},
105
- beginPath: () => {},
106
- moveTo: () => {},
107
- lineTo: () => {},
108
- closePath: () => {},
109
- stroke: () => {},
110
- translate: () => {},
111
- scale: () => {},
112
- rotate: () => {},
113
- arc: () => {},
114
- fill: () => {},
115
- measureText: () => ({ width: 0, actualBoundingBoxAscent: 0, actualBoundingBoxDescent: 0 }),
116
- transform: () => {},
117
- rect: () => {},
118
- clip: () => {},
119
- createLinearGradient: () => ({ addColorStop: () => {} }),
120
- createRadialGradient: () => ({ addColorStop: () => {} }),
121
- createPattern: () => ({}),
122
- bezierCurveTo: () => {},
123
- quadraticCurveTo: () => {},
124
- fillText: () => {},
125
- strokeText: () => {},
126
- globalAlpha: 1,
127
- globalCompositeOperation: 'source-over',
128
- fillStyle: '#000000',
129
- strokeStyle: '#000000',
130
- lineWidth: 1,
131
- lineCap: 'butt',
132
- lineJoin: 'miter',
133
- miterLimit: 10,
134
- shadowOffsetX: 0,
135
- shadowOffsetY: 0,
136
- shadowBlur: 0,
137
- shadowColor: 'rgba(0, 0, 0, 0)',
138
- font: '10px sans-serif',
139
- textAlign: 'start',
140
- textBaseline: 'alphabetic'
141
- };
142
- } as any;
143
-
144
- const originalToDataURL = mockWindow.HTMLCanvasElement.prototype.toDataURL;
145
- mockWindow.HTMLCanvasElement.prototype.toDataURL = function(type?: string, quality?: any) {
146
- try {
147
- const result = originalToDataURL.call(this, type, quality);
148
- if (result && result !== "data:,") return result;
149
- } catch(e) {}
150
- return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
151
- };
152
-
153
- // --- CONTEXT PREPARATION ---
154
- // Inject everything directly into the window context to avoid scope issues
155
- mockWindow.document = mockWindow.document;
156
- mockWindow.window = mockWindow;
157
- mockWindow.self = mockWindow;
158
- mockWindow.parent = mockWindow;
159
- mockWindow.top = mockWindow;
160
-
161
- // Immortal DOM patches (already applied above) are good.
162
-
163
- // Execute script directly in the window context
164
- // Explicitly define globals for the script scope to prevent 'undefined' errors
165
- const result = await mockWindow.eval(`
166
- var window = this;
167
- var document = this.document;
168
- var navigator = this.navigator;
169
- var screen = this.screen;
170
-
171
- ${jsScript}
172
- `) as any;
173
-
174
- if (!result || !result.client_hashes) {
175
- throw new Error("Script executed but returned invalid result (no client_hashes)");
176
  }
177
-
178
- result.client_hashes[0] = ua;
179
- result.client_hashes = result.client_hashes.map((t: string) => {
180
- const hash = createHash('sha256');
181
- hash.update(t);
182
- return hash.digest('base64');
183
- });
184
-
185
- return btoa(JSON.stringify(result));
186
- } catch (e: any) {
187
- console.error(`[DuckAI] Challenge Solver Failed: ${e.message}`);
188
- console.log("[DuckAI] Attempting fallback with original VQD...");
189
- return vqdHash;
190
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  }
192
 
193
  getAvailableModels(): string[] {
@@ -198,16 +71,22 @@ export class DuckAI {
198
  ];
199
  }
200
 
201
- getRateLimitStatus() {
202
- return { status: "unknown" };
203
- }
204
-
205
- async chat(request: any, retries = 1): Promise<string> {
206
- const ua = new UserAgent({ deviceCategory: 'desktop' }).toString();
207
  const headers: any = {
208
- "User-Agent": ua,
209
- "Accept": "text/event-stream",
210
- "x-vqd-accept": "1"
 
 
 
 
 
 
 
 
211
  };
212
 
213
  try {
@@ -216,63 +95,82 @@ export class DuckAI {
216
  console.log(`[DuckAI] Status response: ${statusRes.status}`);
217
 
218
  const hashHeader = statusRes.headers.get("x-vqd-hash-1");
219
- console.log(`[DuckAI] x-vqd-hash-1: ${hashHeader ? "FOUND" : "MISSING"}`);
220
 
221
- if (!hashHeader) throw new Error("Missing x-vqd-hash-1 - DuckDuckGo might be blocking this IP or Challenge failed.");
 
 
 
 
 
 
 
 
222
 
223
- console.log("[DuckAI] Solving challenge...");
224
- const solvedVqd = await this.solveChallenge(hashHeader, ua);
225
- console.log("[DuckAI] Challenge solved (or fallback used).");
226
 
 
227
  const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", {
228
- method: "POST",
229
- headers: { ...headers, "x-vqd-hash-1": solvedVqd, "Content-Type": "application/json" },
230
- body: JSON.stringify(request)
 
 
 
 
231
  });
232
 
233
- console.log(`[DuckAI] Chat response status: ${response.status}`);
234
 
235
- if (!response.ok) {
236
- const errorText = await response.text();
237
-
238
- // Если ошибка 418 (Challenge Failed) и есть попытки - пробуем снова
239
- if (response.status === 418 && retries > 0) {
240
- console.log(`[DuckAI] Got 418 Error. Retrying... (${retries} attempts left)`);
241
- return this.chat(request, retries - 1);
242
- }
243
 
244
- console.log(`[DuckAI] Error body: ${errorText}`);
245
- throw new Error(`DuckDuckGo API Error (${response.status}): ${errorText.substring(0, 100)}`);
246
- }
247
 
248
- const text = await response.text();
249
-
250
- let llmResponse = "";
251
- const lines = text.split("\n");
252
- for (const line of lines) {
253
- if (line.startsWith("data: ")) {
254
- try {
255
- const chunk = line.slice(6);
256
- if (chunk === "[DONE]") break;
257
- const json = JSON.parse(chunk);
258
- if (json.message) llmResponse += json.message;
259
- } catch (e) {}
260
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  }
262
-
263
- if (!llmResponse) {
264
- console.log("[DuckAI] Warning: Empty LLM response extracted.");
265
- throw new Error("Empty response from DuckDuckGo");
266
- }
267
-
268
- return llmResponse.trim();
269
- } catch (error: any) {
270
- // Если была ошибка сети или другая, и есть попытки - тоже ретраим
271
- if (retries > 0) {
272
- console.log(`[DuckAI] Error: ${error.message}. Retrying... (${retries} attempts left)`);
273
- return this.chat(request, retries - 1);
274
- }
275
- throw error;
276
- }
 
 
 
277
  }
278
- }
 
1
  import { JSDOM } from "jsdom";
2
  import { createHash } from "node:crypto";
3
+ import { Buffer } from "node:buffer";
4
  import UserAgent from "user-agents";
5
 
6
  export class DuckAI {
7
+
8
+ // Логика из старого рабочего кода
9
+ private async solveChallenge(vqdHash: string): Promise<string> {
10
+ const jsScript = Buffer.from(vqdHash, 'base64').toString('utf-8');
11
+
12
+ // 1. Создаем JSDOM с правильной структурой (iframe #jsa)
13
+ const dom = new JSDOM(
14
+ `<iframe id="jsa" sandbox="allow-scripts allow-same-origin" srcdoc="<!DOCTYPE html>
15
+ <html>
16
+ <head>
17
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'">
18
+ </head>
19
+ <body></body>
20
+ </html>" style="position: absolute; left: -9999px; top: -9999px;"></iframe>`,
21
+ {
22
+ runScripts: 'dangerously',
23
+ resources: "usable",
24
+ url: "https://duckduckgo.com/"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
26
+ );
27
+
28
+ const window = dom.window as any;
29
+
30
+ // Моки, которые нужны JSDOM, чтобы чувствовать себя браузером
31
+ window.screen = { width: 1920, height: 1080, availWidth: 1920, availHeight: 1080 };
32
+ window.chrome = { runtime: {} };
33
+
34
+ // Глобальные переменные для eval
35
+ window.top.__DDG_BE_VERSION__ = 1;
36
+ window.top.__DDG_FE_CHAT_HASH__ = 1;
37
+
38
+ // 2. Получаем доступ к iframe и вставляем CSP (как в старом коде)
39
+ const jsa = window.document.querySelector('#jsa');
40
+ if (!jsa) throw new Error("Iframe #jsa not found in JSDOM");
41
+
42
+ const contentDoc = jsa.contentDocument || jsa.contentWindow.document;
43
+ const meta = contentDoc.createElement('meta');
44
+ meta.setAttribute('http-equiv', 'Content-Security-Policy');
45
+ meta.setAttribute('content', "default-src 'none'; script-src 'unsafe-inline';");
46
+ contentDoc.head.appendChild(meta);
47
+
48
+ // 3. Выполняем скрипт
49
+ const result = await window.eval(jsScript) as any;
50
+
51
+ if (!result) throw new Error("Challenge script returned nothing");
52
+
53
+ // 4. Жестко задаем User-Agent, который проходит проверку (из старого кода)
54
+ // Chrome/138.0.0.0 - это фейковый будущий UA, который использует скрипт DDG
55
+ result.client_hashes[0] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36';
56
+
57
+ result.client_hashes = result.client_hashes.map((t: string) => {
58
+ const hash = createHash('sha256');
59
+ hash.update(t);
60
+ return hash.digest('base64');
61
+ });
62
+
63
+ return btoa(JSON.stringify(result));
64
  }
65
 
66
  getAvailableModels(): string[] {
 
71
  ];
72
  }
73
 
74
+ async chat(request: any): Promise<string> {
75
+ // Используем тот же UA для запросов, что и в хеше, для консистентности
76
+ const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36';
77
+
 
 
78
  const headers: any = {
79
+ "User-Agent": userAgent,
80
+ "Accept": "text/event-stream",
81
+ "x-vqd-accept": "1",
82
+ "accept-language": "en-US,en;q=0.9",
83
+ "cache-control": "no-cache",
84
+ "pragma": "no-cache",
85
+ "sec-fetch-dest": "empty",
86
+ "sec-fetch-mode": "cors",
87
+ "sec-fetch-site": "same-origin",
88
+ // Важный хедер из старого кода!
89
+ "x-fe-version": "serp_20250401_100419_ET-19d438eb199b2bf7c300"
90
  };
91
 
92
  try {
 
95
  console.log(`[DuckAI] Status response: ${statusRes.status}`);
96
 
97
  const hashHeader = statusRes.headers.get("x-vqd-hash-1");
 
98
 
99
+ if (!hashHeader) {
100
+ // Иногда DDG не выдает challenge, а сразу дает x-vqd-4 (редко, но бывает)
101
+ const vqd4 = statusRes.headers.get("x-vqd-4");
102
+ if (vqd4) {
103
+ console.log("[DuckAI] No challenge, got x-vqd-4 directly.");
104
+ return await this.sendChatRequest(request, vqd4, headers);
105
+ }
106
+ throw new Error("Missing x-vqd-hash-1 and x-vqd-4");
107
+ }
108
 
109
+ console.log("[DuckAI] Solving challenge (Old Method)...");
110
+ const solvedVqd = await this.solveChallenge(hashHeader);
111
+ console.log("[DuckAI] Challenge solved.");
112
 
113
+ // Отправляем запрос с решенной капчей
114
  const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", {
115
+ method: "POST",
116
+ headers: {
117
+ ...headers,
118
+ "Content-Type": "application/json",
119
+ "x-vqd-hash-1": solvedVqd
120
+ },
121
+ body: JSON.stringify(request)
122
  });
123
 
124
+ return await this.processResponse(response);
125
 
126
+ } catch (error: any) {
127
+ console.error(`[DuckAI] Error: ${error.message}`);
128
+ throw error; // Пробрасываем ошибку, чтобы сервер вернул 500 JSON, а не упал
129
+ }
130
+ }
 
 
 
131
 
132
+ // Вспомогательный метод для обработки ответа (чтобы не дублировать код)
133
+ private async processResponse(response: Response): Promise<string> {
134
+ console.log(`[DuckAI] Chat response status: ${response.status}`);
135
 
136
+ if (!response.ok) {
137
+ const errorText = await response.text();
138
+ console.log(`[DuckAI] Error body: ${errorText}`);
139
+ if (response.status === 429) {
140
+ throw new Error("Rate limit exceeded (429)");
 
 
 
 
 
 
 
141
  }
142
+ throw new Error(`DuckDuckGo API Error (${response.status}): ${errorText.substring(0, 100)}`);
143
+ }
144
+
145
+ const text = await response.text();
146
+ let llmResponse = "";
147
+ const lines = text.split("\n");
148
+ for (const line of lines) {
149
+ if (line.startsWith("data: ")) {
150
+ try {
151
+ const chunk = line.slice(6);
152
+ if (chunk === "[DONE]") break;
153
+ const json = JSON.parse(chunk);
154
+ if (json.message) llmResponse += json.message;
155
+ } catch (e) {}
156
  }
157
+ }
158
+
159
+ if (!llmResponse) throw new Error("Empty response from DuckDuckGo");
160
+ return llmResponse.trim();
161
+ }
162
+
163
+ // Метод для отправки запроса, если вдруг капча не нужна (на будущее)
164
+ private async sendChatRequest(request: any, vqd4: string, headers: any): Promise<string> {
165
+ const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", {
166
+ method: "POST",
167
+ headers: {
168
+ ...headers,
169
+ "Content-Type": "application/json",
170
+ "x-vqd-4": vqd4
171
+ },
172
+ body: JSON.stringify(request)
173
+ });
174
+ return await this.processResponse(response);
175
  }
176
+ }