Juanoto2012 commited on
Commit
82ed7bf
verified
1 Parent(s): f1b4d5d

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +132 -292
server.js CHANGED
@@ -1,14 +1,13 @@
1
  import express from 'express';
2
  import cors from 'cors';
3
  import rateLimit from 'express-rate-limit';
4
- import { Readable } from 'stream';
5
  import crypto from 'crypto';
6
 
7
  const app = express();
8
  const PORT = 7860;
9
 
10
- // --- 1. CONFIGURACI脫N DE SEGURIDAD PARANOICA ---
11
- // CR脥TICO: trust proxy en 1 es vital para leer la IP real del usuario a trav茅s de Cloudflare/HF
12
  app.set('trust proxy', 1);
13
  app.disable('x-powered-by');
14
  app.use(cors());
@@ -18,7 +17,7 @@ app.use((req, res, next) => {
18
  res.setHeader('X-Frame-Options', 'DENY');
19
  res.setHeader('X-XSS-Protection', '1; mode=block');
20
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
21
- res.setHeader('Referrer-Policy', 'no-referrer'); // Oculta que vienen de Ventarys AI
22
  res.setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none';");
23
  next();
24
  });
@@ -26,333 +25,174 @@ app.use((req, res, next) => {
26
  app.use(express.json({ limit: '50mb' }));
27
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
28
 
29
- function logError(providerId, reason) {
30
- console.error(`[SYSTEM] Provider Error: ${providerId} | ${reason}`);
31
- }
32
-
33
- // Falsificaci贸n de huella digital del navegador
34
  function getRandomUserAgent() {
35
  const agents = [
36
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
37
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
38
- "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0",
39
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0"
40
  ];
41
  return agents[Math.floor(Math.random() * agents.length)];
42
  }
43
 
44
- // --- PROVEEDORES P脷BLICOS ---
45
- const PROVIDERS = [
46
- {
47
- id: "pollinations-ai",
48
- url: "https://text.pollinations.ai/openai/v1/chat/completions",
49
- modelsUrl: "https://text.pollinations.ai/openai/models"
50
- },
51
- {
52
- id: "kepler-cloud",
53
- url: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/chat/completions"
54
- },
55
- {
56
- // Proveedor p煤blico alternativo "Airforce" (reemplaza a churchless)
57
- id: "airforce-api",
58
- url: "https://api.airforce/v1/chat/completions",
59
- modelsUrl: "https://api.airforce/v1/models"
60
- }
61
- ];
62
 
63
- // CR脥TICO: Subimos el l铆mite a 500 para permitir alta concurrencia.
64
- // Ahora el proxy dejar谩 pasar todo el tr谩fico y el proveedor final aplicar谩 el rate limit por IP.
65
- const MAX_PER_PROVIDER = 500;
66
- const QUEUE_TIMEOUT = 45000;
67
- let currentLoad = { "pollinations-ai": 0, "kepler-cloud": 0, "airforce-api": 0 };
68
-
69
- // --- RATE LIMITING LOCAL ---
70
  const limiter = rateLimit({
71
  windowMs: 60 * 1000,
72
- max: 50, // 50 peticiones por IP localmente para evitar que alguien tire TU servidor
73
  keyGenerator: (req) => req.ip,
74
- message: { error: { message: "Too many requests.", code: 429 } },
75
  standardHeaders: false,
76
  legacyHeaders: false,
77
  });
78
 
79
- const IMAGE_KEYWORDS = ["flux", "dall", "midjourney", "sdxl", "stable-diffusion", "image", "vision"];
80
- const AUDIO_KEYWORDS = ["suno", "udio", "music", "audio", "song", "voice", "tts", "whisper"];
81
-
82
- function isImageModel(model) {
83
- if (!model) return false;
84
- if (model.type === 'image' || model.supports_images === true) return true;
85
- return IMAGE_KEYWORDS.some(kw => (model.id || model.name || "").toLowerCase().includes(kw));
86
- }
87
-
88
- function isAudioModel(model) {
89
- if (model.type === 'audio' || model.type === 'music') return true;
90
- return AUDIO_KEYWORDS.some(kw => (model.id || model.name || "").toLowerCase().includes(kw));
91
- }
92
-
93
- async function fetchAllModels() {
94
- const fetchPromises = PROVIDERS.map(async (provider) => {
95
- const modelsUrl = provider.modelsUrl || provider.url.replace("/chat/completions", "/models");
96
-
97
- try {
98
- const resp = await fetch(modelsUrl, {
99
- method: "GET",
100
- headers: {
101
- "Content-Type": "application/json",
102
- "User-Agent": getRandomUserAgent()
103
- }
104
- });
105
- if (!resp.ok) return [];
106
-
107
- const json = await resp.json();
108
- let modelsArray = Array.isArray(json) ? json : (json && Array.isArray(json.data) ? json.data : []);
109
-
110
- if (modelsArray.length > 0) {
111
- // FILTRO: Excluir modelos de audio
112
- let filteredModels = modelsArray.filter(model => !isAudioModel(model));
113
-
114
- // 馃敟 FILTRO ESPEC脥FICO PARA KEPLER-CLOUD (SOLO FREE) 馃敟
115
- if (provider.id === "kepler-cloud") {
116
- filteredModels = filteredModels.filter(model => {
117
- const mId = (model.id || model.name || "").toLowerCase();
118
- // Filtra si el ID incluye la palabra "free" o si tiene un flag de coste cero
119
- return mId.includes("free") || model.is_free === true || model.pricing?.prompt === "0";
120
- });
121
- }
122
-
123
- return filteredModels.map(model => ({ ...model, id: model.id || model.name, owned_by: provider.id }));
124
- }
125
- return [];
126
- } catch (error) {
127
- return [];
128
- }
129
- });
130
-
131
- const results = await Promise.allSettled(fetchPromises);
132
- let allModels = [];
133
- results.forEach(result => {
134
- if (result.status === "fulfilled") allModels = allModels.concat(result.value);
135
- });
136
-
137
- // Fallback de seguridad si todas las APIs fallan al cargar modelos
138
- if (allModels.length === 0) {
139
- allModels = [
140
- { id: "gpt-4o", object: "model", type: "text", owned_by: "pollinations-ai" },
141
- { id: "claude-3-5-sonnet", object: "model", type: "text", owned_by: "pollinations-ai" },
142
- { id: "gemini-1.5-pro", object: "model", type: "text", owned_by: "airforce-api" },
143
- { id: "gemini-1.5-flash", object: "model", type: "text", owned_by: "airforce-api" }
144
- ];
145
- }
146
- return allModels;
147
- }
148
-
149
  app.get('/health', (req, res) => {
150
- res.json({ status: "online" });
151
  });
152
 
153
- app.get('/v1/models', async (req, res) => {
154
- try {
155
- const allModels = await fetchAllModels();
156
- const textModels = allModels
157
- .filter(m => !isImageModel(m) && m.supports_chat !== false)
158
- .map(m => { m.type = 'text'; return m; });
159
- res.json({ object: "list", data: textModels });
160
- } catch (error) {
161
- res.status(500).json({ error: "No models available." });
162
- }
163
  });
164
 
165
- app.get('/v1/images/models', async (req, res) => {
166
- try {
167
- const allModels = await fetchAllModels();
168
- let imageModels = allModels
169
- .filter(m => isImageModel(m))
170
- .map(m => { m.type = 'image'; return m; });
171
-
172
- const baseImages = [
173
- { id: "flux", object: "model", type: "image", owned_by: "system" },
174
- { id: "dall-e-3", object: "model", type: "image", owned_by: "system" }
175
- ];
176
-
177
- baseImages.forEach(baseMod => {
178
- if (!imageModels.find(m => m.id.toLowerCase() === baseMod.id)) imageModels.push(baseMod);
179
- });
180
-
181
- res.json({ object: "list", data: imageModels });
182
- } catch (error) {
183
- res.status(500).json({ error: "No models available." });
184
- }
185
- });
186
-
187
- app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req, res) => {
188
- const isImage = req.path === '/v1/images/generations';
189
- let availableProviders = isImage ? PROVIDERS.filter(p => p.imageUrl) : [...PROVIDERS];
190
- if (isImage && availableProviders.length === 0) availableProviders = [...PROVIDERS];
191
-
192
  const startTime = Date.now();
193
- let responseSent = false;
194
-
195
- // Extraemos la IP del cliente para reenviarla
196
  const clientIp = req.ip || req.headers['x-forwarded-for'] || req.socket.remoteAddress;
197
 
198
- let payload = JSON.stringify(req.body);
199
- req.body = null;
200
-
201
- while (availableProviders.length > 0 && Date.now() - startTime < QUEUE_TIMEOUT) {
202
- let selectedProvider = null;
203
- let shuffled = [...availableProviders].sort(() => Math.random() - 0.5);
204
-
205
- for (let provider of shuffled) {
206
- if (currentLoad[provider.id] < MAX_PER_PROVIDER) {
207
- selectedProvider = provider;
208
- currentLoad[provider.id]++;
209
- break;
 
210
  }
211
- }
212
 
213
- if (!selectedProvider) {
214
- await new Promise(r => setTimeout(r, 1500));
215
- continue;
216
  }
217
-
218
- let isReleased = false;
219
- const releaseSlot = () => {
220
- if (!isReleased) {
221
- currentLoad[selectedProvider.id] = Math.max(0, currentLoad[selectedProvider.id] - 1);
222
- isReleased = true;
223
- }
224
- };
225
-
226
- try {
227
- let targetUrl = isImage && selectedProvider.imageUrl ? selectedProvider.imageUrl : selectedProvider.url;
228
-
229
- // --- OFUSCACI脫N + DELEGACI脫N DE IP ---
230
- const fetchHeaders = {
231
  "Content-Type": "application/json",
 
232
  "User-Agent": getRandomUserAgent(),
233
- "Accept": "*/*",
234
- "Connection": "keep-alive",
235
- "X-Forwarded-For": clientIp, // El proveedor rate-limitea esta IP
236
- "X-Real-IP": clientIp // El proveedor rate-limitea esta IP
237
- };
238
-
239
- const response = await fetch(targetUrl, {
240
- method: "POST",
241
- headers: fetchHeaders,
242
- body: payload
243
- });
244
-
245
- if (!response.ok) {
246
- releaseSlot();
247
- availableProviders = availableProviders.filter(p => p.id !== selectedProvider.id);
248
- continue;
249
- }
250
-
251
- const responseHeaders = new Headers(response.headers);
252
- responseHeaders.delete('set-cookie');
253
- responseHeaders.delete('server');
254
- responseHeaders.delete('x-powered-by');
255
- responseHeaders.delete('cf-ray');
256
- responseHeaders.delete('access-control-allow-origin');
257
 
258
- if (isImage) {
259
- const contentType = responseHeaders.get("content-type") || "";
 
260
 
261
- if (contentType.includes("application/json")) {
262
- const jsonResp = await response.json();
263
- let dataArray = jsonResp.data;
264
- if (!dataArray && jsonResp.url) dataArray = [{ url: jsonResp.url }];
265
-
266
- if (dataArray && Array.isArray(dataArray)) {
267
- for (let item of dataArray) {
268
- if (item.url && !item.b64_json) {
269
- try {
270
- const imgRes = await fetch(item.url);
271
- const arrayBuffer = await imgRes.arrayBuffer();
272
- item.b64_json = Buffer.from(arrayBuffer).toString('base64');
273
- delete item.url;
274
- } catch (e) {}
275
- }
276
- }
277
- releaseSlot();
278
- responseSent = true;
279
- payload = null;
280
-
281
- return res.status(response.status).json({
282
- created: Math.floor(Date.now() / 1000),
283
- data: dataArray
284
- });
285
  }
286
-
287
- releaseSlot();
288
- responseSent = true;
289
- payload = null;
290
- return res.status(response.status).json(jsonResp);
291
- }
292
- else if (contentType.includes("image/")) {
293
- const arrayBuffer = await response.arrayBuffer();
294
- const b64 = Buffer.from(arrayBuffer).toString('base64');
295
- releaseSlot();
296
- responseSent = true;
297
- payload = null;
298
- return res.status(200).json({
299
- created: Math.floor(Date.now() / 1000),
300
- data: [{ b64_json: b64 }]
301
- });
302
- }
303
- else {
304
- const textResp = await response.text();
305
- releaseSlot();
306
- responseSent = true;
307
- payload = null;
308
- return res.status(response.status).type(contentType).send(textResp);
309
  }
310
- }
311
 
312
- res.writeHead(response.status, {
313
- 'Content-Type': responseHeaders.get('content-type') || 'text/event-stream',
314
- 'Cache-Control': 'no-cache',
315
- 'Connection': 'keep-alive'
 
 
316
  });
317
-
318
- if (response.body) {
319
- const stream = Readable.fromWeb(response.body);
320
- stream.pipe(res);
321
-
322
- stream.on('end', () => {
323
- releaseSlot();
324
- payload = null;
325
- });
326
- stream.on('error', () => {
327
- releaseSlot();
328
- payload = null;
329
- });
330
- req.on('close', () => {
331
- releaseSlot();
332
- payload = null;
333
- });
334
- } else {
335
- releaseSlot();
336
- payload = null;
337
- res.end();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  }
339
-
340
- responseSent = true;
341
- return;
342
-
343
- } catch (err) {
344
- releaseSlot();
345
- availableProviders = availableProviders.filter(p => p.id !== selectedProvider.id);
 
 
346
  }
347
  }
348
-
349
- payload = null;
350
-
351
- if (!responseSent) {
352
- return res.status(503).json({ error: { message: "Servicio no disponible.", code: 503 } });
353
- }
354
  });
355
 
356
  app.listen(PORT, '0.0.0.0', () => {
357
- console.log(`[SYS] Nivel de cifrado: Tor (Delegando IP). Concurrencia m谩xima: 500/prov. Puerto: ${PORT}`);
358
  });
 
1
  import express from 'express';
2
  import cors from 'cors';
3
  import rateLimit from 'express-rate-limit';
4
+ import { Readable, Transform } from 'stream';
5
  import crypto from 'crypto';
6
 
7
  const app = express();
8
  const PORT = 7860;
9
 
10
+ // --- CONFIGURACI脫N DE SEGURIDAD ---
 
11
  app.set('trust proxy', 1);
12
  app.disable('x-powered-by');
13
  app.use(cors());
 
17
  res.setHeader('X-Frame-Options', 'DENY');
18
  res.setHeader('X-XSS-Protection', '1; mode=block');
19
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
20
+ res.setHeader('Referrer-Policy', 'no-referrer');
21
  res.setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none';");
22
  next();
23
  });
 
25
  app.use(express.json({ limit: '50mb' }));
26
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
27
 
28
+ // Falsificaci贸n de huella digital
 
 
 
 
29
  function getRandomUserAgent() {
30
  const agents = [
31
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
32
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
33
+ "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0"
 
34
  ];
35
  return agents[Math.floor(Math.random() * agents.length)];
36
  }
37
 
38
+ // Modelos soportados por DuckDuckGo AI
39
+ const DDG_MODELS = {
40
+ "gpt-4o-mini": "gpt-4o-mini",
41
+ "claude-3-haiku": "claude-3-haiku-20240307",
42
+ "llama-3.1-70b": "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
43
+ "mixtral-8x7b": "mistralai/Mixtral-8x7B-Instruct-v0.1"
44
+ };
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ // --- RATE LIMITING ---
 
 
 
 
 
 
47
  const limiter = rateLimit({
48
  windowMs: 60 * 1000,
49
+ max: 50,
50
  keyGenerator: (req) => req.ip,
51
+ message: { error: { message: "Too many requests local.", code: 429 } },
52
  standardHeaders: false,
53
  legacyHeaders: false,
54
  });
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  app.get('/health', (req, res) => {
57
+ res.json({ status: "online", proxy: "DuckDuckGo AI" });
58
  });
59
 
60
+ // Endpoint de modelos (Formato OpenAI)
61
+ app.get('/v1/models', (req, res) => {
62
+ const models = Object.keys(DDG_MODELS).map(id => ({
63
+ id: id,
64
+ object: "model",
65
+ type: "text",
66
+ owned_by: "duckduckgo"
67
+ }));
68
+ res.json({ object: "list", data: models });
 
69
  });
70
 
71
+ app.post('/v1/chat/completions', limiter, async (req, res) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  const startTime = Date.now();
 
 
 
73
  const clientIp = req.ip || req.headers['x-forwarded-for'] || req.socket.remoteAddress;
74
 
75
+ // Configuraci贸n del modelo y mensajes
76
+ const requestedModel = req.body.model || "gpt-4o-mini";
77
+ const duckModel = DDG_MODELS[requestedModel] || "gpt-4o-mini";
78
+ const messages = req.body.messages || [];
79
+ const isStream = req.body.stream === true;
80
+
81
+ try {
82
+ // PASO 1: Obtener el token (VQD) necesario para chatear
83
+ const statusRes = await fetch("https://duckduckgo.com/duckchat/v1/status", {
84
+ headers: {
85
+ "x-vqd-accept": "1",
86
+ "User-Agent": getRandomUserAgent(),
87
+ "x-forwarded-for": clientIp
88
  }
89
+ });
90
 
91
+ const vqdToken = statusRes.headers.get("x-vqd-4");
92
+ if (!vqdToken) {
93
+ return res.status(503).json({ error: { message: "Error al negociar token con DuckDuckGo", code: 503 }});
94
  }
95
+
96
+ // PASO 2: Enviar el prompt a DuckDuckGo
97
+ const chatRes = await fetch("https://duckduckgo.com/duckchat/v1/chat", {
98
+ method: "POST",
99
+ headers: {
 
 
 
 
 
 
 
 
 
100
  "Content-Type": "application/json",
101
+ "x-vqd-4": vqdToken,
102
  "User-Agent": getRandomUserAgent(),
103
+ "Accept": "text/event-stream",
104
+ "x-forwarded-for": clientIp
105
+ },
106
+ body: JSON.stringify({
107
+ model: duckModel,
108
+ messages: messages
109
+ })
110
+ });
111
+
112
+ if (!chatRes.ok) {
113
+ const errorText = await chatRes.text();
114
+ return res.status(chatRes.status).json({ error: { message: `DuckDuckGo Error: ${errorText}`, code: chatRes.status }});
115
+ }
116
+
117
+ // PASO 3A: Manejar Petici贸n NO STREAM (Esperar y juntar todo el texto)
118
+ if (!isStream) {
119
+ let fullText = "";
120
+ const reader = chatRes.body.getReader();
121
+ const decoder = new TextDecoder("utf-8");
 
 
 
 
 
122
 
123
+ while (true) {
124
+ const { done, value } = await reader.read();
125
+ if (done) break;
126
 
127
+ const chunkStr = decoder.decode(value, { stream: true });
128
+ const lines = chunkStr.split('\n');
129
+ for (let line of lines) {
130
+ if (line.startsWith('data: ') && line !== 'data: [DONE]') {
131
+ try {
132
+ const data = JSON.parse(line.slice(6));
133
+ if (data.message) fullText += data.message;
134
+ } catch (e) { /* Ignorar chunks rotos */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  }
137
+ }
138
 
139
+ return res.json({
140
+ id: 'chatcmpl-' + crypto.randomUUID(),
141
+ object: 'chat.completion',
142
+ created: Math.floor(Date.now() / 1000),
143
+ model: requestedModel,
144
+ choices: [{ message: { role: 'assistant', content: fullText }, finish_reason: 'stop' }]
145
  });
146
+ }
147
+
148
+ // PASO 3B: Manejar Petici贸n STREAMING (Traducir sobre la marcha DDG -> OpenAI)
149
+ res.writeHead(200, {
150
+ 'Content-Type': 'text/event-stream',
151
+ 'Cache-Control': 'no-cache',
152
+ 'Connection': 'keep-alive'
153
+ });
154
+
155
+ let buffer = '';
156
+ const ddgToOpenAI = new Transform({
157
+ transform(chunk, encoding, callback) {
158
+ buffer += chunk.toString();
159
+ const lines = buffer.split('\n');
160
+ buffer = lines.pop(); // Guarda el fragmento incompleto para el pr贸ximo chunk
161
+
162
+ for (const line of lines) {
163
+ if (line.startsWith('data: ') && line !== 'data: [DONE]') {
164
+ try {
165
+ const data = JSON.parse(line.slice(6));
166
+ if (data.message) {
167
+ const openaiChunk = {
168
+ id: 'chatcmpl-' + crypto.randomUUID(),
169
+ object: 'chat.completion.chunk',
170
+ created: Math.floor(Date.now() / 1000),
171
+ model: requestedModel,
172
+ choices: [{ delta: { content: data.message } }]
173
+ };
174
+ this.push(`data: ${JSON.stringify(openaiChunk)}\n\n`);
175
+ }
176
+ } catch (e) { /* Ignorar errores de parseo parcial */ }
177
+ } else if (line === 'data: [DONE]') {
178
+ this.push('data: [DONE]\n\n');
179
+ }
180
+ }
181
+ callback();
182
  }
183
+ });
184
+
185
+ const stream = Readable.fromWeb(chatRes.body);
186
+ stream.pipe(ddgToOpenAI).pipe(res);
187
+
188
+ } catch (err) {
189
+ console.error("[ERROR]", err.message);
190
+ if (!res.headersSent) {
191
+ res.status(500).json({ error: { message: "Error interno del servidor", details: err.message, code: 500 }});
192
  }
193
  }
 
 
 
 
 
 
194
  });
195
 
196
  app.listen(PORT, '0.0.0.0', () => {
197
+ console.log(`[SYS] DuckDuckGo AI Proxy Activo. Modelos: GPT-4o-mini, Claude-3-Haiku, Llama-3.1-70b. Puerto: ${PORT}`);
198
  });