unknownfriend00007 commited on
Commit
190b2ce
·
verified ·
1 Parent(s): df8dbc1

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +95 -303
server.js CHANGED
@@ -2,402 +2,194 @@ const express = require('express');
2
  const fetch = require('node-fetch');
3
  const cors = require('cors');
4
  const rateLimit = require('express-rate-limit');
5
- const helmet = require('helmet');
6
  require('dotenv').config();
7
 
8
  const app = express();
9
 
10
- // --- 1. SECURITY HEADERS (Helmet) ---
11
- app.use(helmet({
12
- contentSecurityPolicy: false, // Allow embed from any site
13
- crossOriginEmbedderPolicy: false
14
- }));
15
-
16
- // --- 2. SECURITY: TRUST PROXY ---
17
  app.set('trust proxy', 1);
 
18
 
19
- // --- 3. BODY SIZE LIMITS ---
20
- app.use(express.json({ limit: '1mb' })); // Prevent large payloads
21
-
22
- // --- 4. SECURITY: RATE LIMITING (Fixed IP extraction) ---
23
- const limiter = rateLimit({
24
- windowMs: 15 * 60 * 1000,
25
- max: 100,
26
- standardHeaders: true,
27
- legacyHeaders: false,
28
- message: { error: "Too many requests, please try again later." },
29
- keyGenerator: (req) => {
30
- // Strip port from IP to prevent bypass
31
- const ip = req.ip || req.connection.remoteAddress || 'unknown';
32
- return ip.replace(/:\d+[^:]*$/, '');
33
- }
34
- });
35
- app.use(limiter);
36
-
37
- // --- 5. SECURITY: STRICT CORS (Fixed) ---
38
  const allowedOrigins = process.env.ALLOWED_ORIGINS
39
  ? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
40
  : [];
41
 
42
  app.use(cors({
43
  origin: function (origin, callback) {
44
- // Allow requests with no origin (mobile apps, Postman, etc.)
45
- if (!origin) {
46
- return allowedOrigins.length === 0 ? callback(null, true) : callback(new Error('Not allowed by CORS'));
47
- }
48
-
49
- // In open mode, allow all
50
- if (allowedOrigins.length === 0) {
51
- return callback(null, true);
52
- }
53
-
54
- // EXACT match only (prevent bypass)
55
  if (allowedOrigins.includes(origin)) {
56
  callback(null, true);
57
  } else {
58
- console.log(`[Security] Blocked origin: ${origin}`);
59
  callback(new Error('Not allowed by CORS'));
60
  }
61
- },
62
- credentials: true
63
  }));
64
 
65
- // JSON error handler for CORS failures
66
  app.use((err, req, res, next) => {
67
  if (err.message === 'Not allowed by CORS') {
68
- return res.status(403).json({
69
- error: 'Access denied',
70
- message: 'This chatbot can only be used on authorized websites.'
71
- });
72
  }
73
  next(err);
74
  });
75
 
76
- // --- 6. REQUEST LOGGING (With sanitization) ---
77
- app.use((req, res, next) => {
78
- const ip = (req.ip || req.connection.remoteAddress || 'unknown').replace(/:\d+[^:]*$/, '');
79
- const origin = (req.headers.origin || 'unknown').substring(0, 100); // Prevent log injection
80
- console.log(`[${new Date().toISOString()}] ${ip} -> ${req.method} ${req.path} from ${origin}`);
81
- next();
82
- });
83
-
84
- // --- 7. DAILY USAGE CAPS (Fixed memory leak) ---
85
- const dailyUsage = new Map();
86
- let lastResetDate = new Date().toDateString();
87
-
88
- // Check and reset daily if date changed (handles timezone issues)
89
- function checkDailyReset() {
90
- const today = new Date().toDateString();
91
- if (today !== lastResetDate) {
92
- dailyUsage.clear();
93
- lastResetDate = today;
94
- console.log('[System] Daily usage counters reset');
95
- }
96
- }
97
-
98
- // Check every hour instead of 24-hour timer
99
- setInterval(checkDailyReset, 60 * 60 * 1000);
100
-
101
- app.use((req, res, next) => {
102
- if (req.method === 'POST' && (req.path.includes('/prediction/') || req.path.includes('/vector/upsert/'))) {
103
- checkDailyReset(); // Check on every request too
104
-
105
- const ip = (req.ip || 'unknown').replace(/:\d+[^:]*$/, '');
106
- const count = dailyUsage.get(ip) || 0;
107
-
108
- if (count >= 200) {
109
- return res.status(429).json({
110
- error: 'Daily limit reached',
111
- message: 'You have reached your daily usage limit. Try again tomorrow.'
112
- });
113
- }
114
-
115
- dailyUsage.set(ip, count + 1);
116
- }
117
- next();
118
  });
 
119
 
120
- // --- 8. BOT DETECTION (Improved) ---
121
  app.use((req, res, next) => {
122
- if (!(req.path.includes('/prediction/') || req.path.includes('/vector/upsert/'))) {
123
- return next();
124
- }
125
-
126
- const userAgent = (req.headers['user-agent'] || '').toLowerCase();
127
- const suspiciousBots = ['python-requests', 'curl/', 'wget/', 'scrapy', 'crawler'];
128
-
129
- // Check for bot patterns
130
- const isBot = suspiciousBots.some(bot => userAgent.includes(bot));
131
-
132
- // Additional check: require Accept header (most bots forget this)
133
- const hasAccept = req.headers['accept'];
134
-
135
- if (isBot || !hasAccept) {
136
- console.log(`[Security] Blocked suspicious request: ${userAgent.substring(0, 50)} from ${req.ip}`);
137
- return res.status(403).json({
138
- error: 'Automated access detected',
139
- message: 'This service is for web browsers only.'
140
- });
141
- }
142
  next();
143
  });
144
 
145
- // --- 9. INSTANCES CONFIGURATION ---
146
  let INSTANCES = [];
147
  try {
148
  INSTANCES = JSON.parse(process.env.FLOWISE_INSTANCES || '[]');
149
- if (!Array.isArray(INSTANCES) || INSTANCES.length === 0) {
150
- console.error('ERROR: FLOWISE_INSTANCES must be a non-empty array');
151
- }
152
  } catch (e) {
153
- console.error("CRITICAL ERROR: Could not parse FLOWISE_INSTANCES JSON", e);
154
  }
155
 
156
- console.log(`[System] Loaded ${INSTANCES.length} instances`);
157
-
158
- // Cache for chatflow lookups (with auto-cleanup)
159
  const flowCache = new Map();
160
 
161
- // Clean up old cache entries every 10 minutes
162
- setInterval(() => {
163
- const now = Date.now();
164
- for (const [key, value] of flowCache.entries()) {
165
- if (now - value.timestamp > 10 * 60 * 1000) { // 10 minutes
166
- flowCache.delete(key);
167
- }
168
- }
169
- }, 10 * 60 * 1000);
170
-
171
- // --- 10. HELPER: FETCH WITH TIMEOUT ---
172
- async function fetchWithTimeout(url, options, timeout = 10000) {
173
- const controller = new AbortController();
174
- const timeoutId = setTimeout(() => controller.abort(), timeout);
175
-
176
- try {
177
- const response = await fetch(url, {
178
- ...options,
179
- signal: controller.signal
180
- });
181
- clearTimeout(timeoutId);
182
- return response;
183
- } catch (error) {
184
- clearTimeout(timeoutId);
185
- if (error.name === 'AbortError') {
186
- throw new Error('Request timeout');
187
- }
188
- throw error;
189
- }
190
- }
191
-
192
- // --- 11. HELPER: RESOLVE BOT TO CHATFLOW ID ---
193
  async function resolveChatflowId(instanceNum, botName) {
194
  const cacheKey = `${instanceNum}-${botName}`;
195
-
196
- // Check cache
197
  const cached = flowCache.get(cacheKey);
198
- if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
199
- return { id: cached.id, instance: cached.instance };
200
- }
201
 
202
- // Validate instance
203
- if (isNaN(instanceNum) || instanceNum < 1 || instanceNum > INSTANCES.length) {
204
- throw new Error(`Instance ${instanceNum} does not exist. Valid: 1-${INSTANCES.length}`);
205
  }
206
 
207
  const instance = INSTANCES[instanceNum - 1];
208
-
209
  console.log(`[System] Looking up '${botName}' in instance ${instanceNum}...`);
210
 
211
  const headers = {};
212
- if (instance.key && instance.key.length > 0) {
213
- headers['Authorization'] = `Bearer ${instance.key}`;
214
- }
215
-
216
- const listResponse = await fetchWithTimeout(`${instance.url}/api/v1/chatflows`, { headers }, 10000);
217
 
218
- if (!listResponse.ok) {
219
- throw new Error(`Instance ${instanceNum} returned status ${listResponse.status}`);
 
220
  }
221
 
222
- const flows = await listResponse.json();
 
223
 
224
- if (!Array.isArray(flows)) {
225
- throw new Error(`Instance ${instanceNum} returned invalid response`);
226
- }
227
-
228
- const matchedFlow = flows.find(f => f.name && f.name.toLowerCase().replace(/\s+/g, '-') === botName);
229
-
230
- if (!matchedFlow || !matchedFlow.id) {
231
- throw new Error(`Bot '${botName}' not found in instance ${instanceNum}`);
232
- }
233
 
234
- const chatflowId = matchedFlow.id;
 
235
 
236
- // Cache with timestamp
237
- flowCache.set(cacheKey, {
238
- id: chatflowId,
239
- instance: instance,
240
- timestamp: Date.now()
241
- });
242
-
243
- console.log(`[System] Found '${botName}' -> ${chatflowId}`);
244
 
245
- return { id: chatflowId, instance };
 
 
 
 
 
 
 
 
246
  }
247
 
248
- // --- 12. ROUTE 1: CHATBOT CONFIG ---
249
- app.get('/api/v1/public-chatbotConfig/:instanceNum/:botName', async (req, res) => {
250
  try {
251
  const instanceNum = parseInt(req.params.instanceNum);
252
- const botName = req.params.botName.toLowerCase().substring(0, 100); // Limit length
 
 
 
 
253
 
254
  const { id, instance } = await resolveChatflowId(instanceNum, botName);
255
 
256
- const headers = {};
257
- if (instance.key && instance.key.length > 0) {
258
- headers['Authorization'] = `Bearer ${instance.key}`;
259
- }
 
 
 
 
260
 
261
- const configResponse = await fetchWithTimeout(
262
- `${instance.url}/api/v1/public-chatbotConfig/${id}`,
263
- { headers },
264
- 10000
265
- );
 
 
 
 
266
 
267
- const data = await configResponse.json();
268
- res.status(configResponse.status).json(data);
269
 
270
  } catch (error) {
271
- console.error('[Error] Config fetch failed:', error.message);
272
- res.status(404).json({ error: error.message });
 
 
 
273
  }
274
  });
275
 
276
- // --- 13. ROUTE 2: STREAMING CHECK ---
277
- app.get('/api/v1/chatflows-streaming/:instanceNum/:botName', async (req, res) => {
278
  try {
279
  const instanceNum = parseInt(req.params.instanceNum);
280
- const botName = req.params.botName.toLowerCase().substring(0, 100);
281
-
282
  const { id, instance } = await resolveChatflowId(instanceNum, botName);
283
 
284
  const headers = {};
285
- if (instance.key && instance.key.length > 0) {
286
- headers['Authorization'] = `Bearer ${instance.key}`;
287
- }
288
 
289
- const streamResponse = await fetchWithTimeout(
290
- `${instance.url}/api/v1/chatflows-streaming/${id}`,
291
- { headers },
292
- 10000
293
- );
294
 
295
- const data = await streamResponse.json();
296
- res.status(streamResponse.status).json(data);
 
297
 
 
 
298
  } catch (error) {
299
- console.error('[Error] Streaming check failed:', error.message);
300
  res.status(404).json({ error: error.message });
301
  }
302
  });
303
 
304
- // --- 14. ROUTE 3: PREDICTION (SEND MESSAGE) ---
305
- app.post('/api/v1/prediction/:instanceNum/:botName', async (req, res) => {
306
  try {
307
  const instanceNum = parseInt(req.params.instanceNum);
308
- const botName = req.params.botName.toLowerCase().substring(0, 100);
309
-
310
- // Validate question exists and is a string
311
- if (!req.body.question || typeof req.body.question !== 'string') {
312
- return res.status(400).json({
313
- error: 'Invalid request',
314
- message: 'Question must be a non-empty string.'
315
- });
316
- }
317
-
318
- // MESSAGE LENGTH LIMIT
319
- if (req.body.question.length > 2000) {
320
- return res.status(400).json({
321
- error: 'Message too long',
322
- message: 'Please keep messages under 2000 characters.'
323
- });
324
- }
325
-
326
  const { id, instance } = await resolveChatflowId(instanceNum, botName);
327
 
328
- const forwardHeaders = {
329
- 'Content-Type': 'application/json',
330
- 'HTTP-Referer': req.headers.origin || 'https://huggingface.co',
331
- 'X-Title': 'FederatedProxy'
332
- };
333
-
334
- if (instance.key && instance.key.length > 0) {
335
- forwardHeaders['Authorization'] = `Bearer ${instance.key}`;
336
- }
337
 
338
- const flowiseResponse = await fetchWithTimeout(
339
- `${instance.url}/api/v1/prediction/${id}`,
340
- {
341
- method: 'POST',
342
- headers: forwardHeaders,
343
- body: JSON.stringify(req.body)
344
- },
345
- 30000 // 30 second timeout for AI responses
346
- );
347
 
348
- const data = await flowiseResponse.json();
349
- res.status(flowiseResponse.status).json(data);
 
350
 
 
 
351
  } catch (error) {
352
- console.error('[Error] Prediction failed:', error.message);
353
- res.status(500).json({
354
- error: 'Proxy forwarding failed',
355
- message: error.message
356
- });
357
  }
358
  });
359
 
360
- // --- 15. HEALTH CHECK ---
361
  app.get('/', (req, res) => res.send('Federated Proxy Active'));
 
362
 
363
- app.get('/health', (req, res) => {
364
- res.json({
365
- status: 'healthy',
366
- instances: INSTANCES.length,
367
- cached_bots: flowCache.size,
368
- daily_active_ips: dailyUsage.size,
369
- uptime: process.uptime()
370
- });
371
- });
372
-
373
- // --- 16. 404 HANDLER ---
374
- app.use((req, res) => {
375
- res.status(404).json({ error: 'Route not found' });
376
- });
377
-
378
- // --- 17. GLOBAL ERROR HANDLER ---
379
- app.use((err, req, res, next) => {
380
- console.error('[Error] Unhandled error:', err);
381
- res.status(500).json({ error: 'Internal server error' });
382
- });
383
-
384
- // --- 18. GRACEFUL SHUTDOWN ---
385
- const server = app.listen(7860, '0.0.0.0', () => {
386
- console.log('Federated Proxy running on port 7860');
387
- });
388
-
389
- process.on('SIGTERM', () => {
390
- console.log('[System] SIGTERM received, shutting down gracefully...');
391
- server.close(() => {
392
- console.log('[System] Server closed');
393
- process.exit(0);
394
- });
395
- });
396
-
397
- process.on('SIGINT', () => {
398
- console.log('[System] SIGINT received, shutting down gracefully...');
399
- server.close(() => {
400
- console.log('[System] Server closed');
401
- process.exit(0);
402
- });
403
- });
 
2
  const fetch = require('node-fetch');
3
  const cors = require('cors');
4
  const rateLimit = require('express-rate-limit');
 
5
  require('dotenv').config();
6
 
7
  const app = express();
8
 
 
 
 
 
 
 
 
9
  app.set('trust proxy', 1);
10
+ app.use(express.json({ limit: '1mb' }));
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  const allowedOrigins = process.env.ALLOWED_ORIGINS
13
  ? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
14
  : [];
15
 
16
  app.use(cors({
17
  origin: function (origin, callback) {
18
+ if (!origin || allowedOrigins.length === 0) return callback(null, true);
 
 
 
 
 
 
 
 
 
 
19
  if (allowedOrigins.includes(origin)) {
20
  callback(null, true);
21
  } else {
 
22
  callback(new Error('Not allowed by CORS'));
23
  }
24
+ }
 
25
  }));
26
 
 
27
  app.use((err, req, res, next) => {
28
  if (err.message === 'Not allowed by CORS') {
29
+ return res.status(403).json({ error: 'Access denied' });
 
 
 
30
  }
31
  next(err);
32
  });
33
 
34
+ const limiter = rateLimit({
35
+ windowMs: 15 * 60 * 1000,
36
+ max: 100,
37
+ message: { error: "Too many requests" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  });
39
+ app.use(limiter);
40
 
 
41
  app.use((req, res, next) => {
42
+ const ip = (req.ip || 'unknown').replace(/:\d+[^:]*$/, '');
43
+ console.log(`[${new Date().toISOString()}] ${ip} -> ${req.method} ${req.path} from ${req.headers.origin || 'unknown'}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  next();
45
  });
46
 
 
47
  let INSTANCES = [];
48
  try {
49
  INSTANCES = JSON.parse(process.env.FLOWISE_INSTANCES || '[]');
50
+ console.log(`[System] Loaded ${INSTANCES.length} instances`);
 
 
51
  } catch (e) {
52
+ console.error("ERROR parsing FLOWISE_INSTANCES:", e);
53
  }
54
 
 
 
 
55
  const flowCache = new Map();
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  async function resolveChatflowId(instanceNum, botName) {
58
  const cacheKey = `${instanceNum}-${botName}`;
 
 
59
  const cached = flowCache.get(cacheKey);
60
+ if (cached) return cached;
 
 
61
 
62
+ if (instanceNum < 1 || instanceNum > INSTANCES.length) {
63
+ throw new Error(`Invalid instance: ${instanceNum}`);
 
64
  }
65
 
66
  const instance = INSTANCES[instanceNum - 1];
 
67
  console.log(`[System] Looking up '${botName}' in instance ${instanceNum}...`);
68
 
69
  const headers = {};
70
+ if (instance.key) headers['Authorization'] = `Bearer ${instance.key}`;
 
 
 
 
71
 
72
+ const response = await fetch(`${instance.url}/api/v1/chatflows`, { headers });
73
+ if (!response.ok) {
74
+ throw new Error(`Instance ${instanceNum} returned status ${response.status}. Instance may be paused.`);
75
  }
76
 
77
+ const flows = await response.json();
78
+ const match = flows.find(f => f.name.toLowerCase().replace(/\s+/g, '-') === botName);
79
 
80
+ if (!match) throw new Error(`Bot '${botName}' not found in instance ${instanceNum}`);
 
 
 
 
 
 
 
 
81
 
82
+ const result = { id: match.id, instance };
83
+ flowCache.set(cacheKey, result);
84
 
85
+ console.log(`[System] Found '${botName}' -> ${match.id}`);
86
+ return result;
87
+ }
88
+
89
+ // IMPROVED: Safe JSON parsing
90
+ async function safeJsonParse(response, url) {
91
+ const text = await response.text();
 
92
 
93
+ try {
94
+ return JSON.parse(text);
95
+ } catch (e) {
96
+ console.error(`[Error] Non-JSON response from ${url}`);
97
+ console.error(`[Error] Response starts with: ${text.substring(0, 200)}`);
98
+
99
+ // Return a user-friendly error
100
+ throw new Error('The Flowise instance returned an invalid response. It may be paused or experiencing errors.');
101
+ }
102
  }
103
 
104
+ app.post('/api/v1/prediction/:instanceNum/:botName', async (req, res) => {
 
105
  try {
106
  const instanceNum = parseInt(req.params.instanceNum);
107
+ const botName = req.params.botName.toLowerCase();
108
+
109
+ if (req.body.question && req.body.question.length > 2000) {
110
+ return res.status(400).json({ error: 'Message too long (max 2000 characters)' });
111
+ }
112
 
113
  const { id, instance } = await resolveChatflowId(instanceNum, botName);
114
 
115
+ const headers = { 'Content-Type': 'application/json' };
116
+ if (instance.key) headers['Authorization'] = `Bearer ${instance.key}`;
117
+
118
+ const response = await fetch(`${instance.url}/api/v1/prediction/${id}`, {
119
+ method: 'POST',
120
+ headers,
121
+ body: JSON.stringify(req.body)
122
+ });
123
 
124
+ // Check if response is OK before parsing
125
+ if (!response.ok) {
126
+ const errorText = await response.text();
127
+ console.error(`[Error] Instance returned ${response.status}: ${errorText.substring(0, 200)}`);
128
+ return res.status(response.status).json({
129
+ error: 'Flowise instance error',
130
+ message: 'The chatbot instance may be paused or misconfigured. Please check the Flowise instance logs.'
131
+ });
132
+ }
133
 
134
+ const data = await safeJsonParse(response, instance.url);
135
+ res.status(200).json(data);
136
 
137
  } catch (error) {
138
+ console.error('[Error]', error.message);
139
+ res.status(500).json({
140
+ error: 'Request failed',
141
+ message: error.message
142
+ });
143
  }
144
  });
145
 
146
+ app.get('/api/v1/public-chatbotConfig/:instanceNum/:botName', async (req, res) => {
 
147
  try {
148
  const instanceNum = parseInt(req.params.instanceNum);
149
+ const botName = req.params.botName.toLowerCase();
 
150
  const { id, instance } = await resolveChatflowId(instanceNum, botName);
151
 
152
  const headers = {};
153
+ if (instance.key) headers['Authorization'] = `Bearer ${instance.key}`;
 
 
154
 
155
+ const response = await fetch(`${instance.url}/api/v1/public-chatbotConfig/${id}`, { headers });
 
 
 
 
156
 
157
+ if (!response.ok) {
158
+ return res.status(response.status).json({ error: 'Config not available' });
159
+ }
160
 
161
+ const data = await safeJsonParse(response, instance.url);
162
+ res.status(200).json(data);
163
  } catch (error) {
164
+ console.error('[Error Config]', error.message);
165
  res.status(404).json({ error: error.message });
166
  }
167
  });
168
 
169
+ app.get('/api/v1/chatflows-streaming/:instanceNum/:botName', async (req, res) => {
 
170
  try {
171
  const instanceNum = parseInt(req.params.instanceNum);
172
+ const botName = req.params.botName.toLowerCase();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  const { id, instance } = await resolveChatflowId(instanceNum, botName);
174
 
175
+ const headers = {};
176
+ if (instance.key) headers['Authorization'] = `Bearer ${instance.key}`;
 
 
 
 
 
 
 
177
 
178
+ const response = await fetch(`${instance.url}/api/v1/chatflows-streaming/${id}`, { headers });
 
 
 
 
 
 
 
 
179
 
180
+ if (!response.ok) {
181
+ return res.status(response.status).json({ isStreaming: false });
182
+ }
183
 
184
+ const data = await safeJsonParse(response, instance.url);
185
+ res.status(200).json(data);
186
  } catch (error) {
187
+ console.error('[Error Streaming]', error.message);
188
+ res.status(200).json({ isStreaming: false }); // Default to non-streaming
 
 
 
189
  }
190
  });
191
 
 
192
  app.get('/', (req, res) => res.send('Federated Proxy Active'));
193
+ app.get('/health', (req, res) => res.json({ status: 'ok', instances: INSTANCES.length }));
194
 
195
+ app.listen(7860, '0.0.0.0', () => console.log('Federated Proxy running on port 7860'));