Update server.js
Browse files
server.js
CHANGED
|
@@ -7,6 +7,39 @@ require('dotenv').config();
|
|
| 7 |
|
| 8 |
const app = express();
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
app.use(helmet({
|
| 11 |
contentSecurityPolicy: false,
|
| 12 |
crossOriginEmbedderPolicy: false
|
|
@@ -24,16 +57,14 @@ function authenticateRequest(req) {
|
|
| 24 |
const origin = req.headers.origin;
|
| 25 |
const apiKey = req.headers['x-api-key'];
|
| 26 |
|
| 27 |
-
// CASE 1: Browser request (has Origin)
|
| 28 |
if (origin) {
|
| 29 |
if (allowedOrigins.length === 0) return { valid: true, source: 'open-mode' };
|
| 30 |
return {
|
| 31 |
valid: allowedOrigins.includes(origin),
|
| 32 |
-
source: origin
|
| 33 |
};
|
| 34 |
}
|
| 35 |
|
| 36 |
-
// CASE 2: Backend request (no Origin) - MUST have API key
|
| 37 |
if (apiKey) {
|
| 38 |
if (API_KEYS.length === 0) return { valid: true, source: 'no-keys-configured' };
|
| 39 |
return {
|
|
@@ -42,11 +73,10 @@ function authenticateRequest(req) {
|
|
| 42 |
};
|
| 43 |
}
|
| 44 |
|
| 45 |
-
// CASE 3: No Origin, No API Key - BLOCKED
|
| 46 |
return { valid: false, source: 'unauthorized' };
|
| 47 |
}
|
| 48 |
|
| 49 |
-
// --- CORS
|
| 50 |
const allowedOrigins = process.env.ALLOWED_ORIGINS
|
| 51 |
? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
|
| 52 |
: [];
|
|
@@ -60,7 +90,7 @@ app.use(cors({
|
|
| 60 |
if (allowedOrigins.includes(origin)) {
|
| 61 |
callback(null, true);
|
| 62 |
} else {
|
| 63 |
-
|
| 64 |
callback(new Error('Not allowed by CORS'));
|
| 65 |
}
|
| 66 |
},
|
|
@@ -83,14 +113,14 @@ app.use((req, res, next) => {
|
|
| 83 |
const auth = authenticateRequest(req);
|
| 84 |
|
| 85 |
if (!auth.valid) {
|
| 86 |
-
|
| 87 |
return res.status(401).json({
|
| 88 |
error: 'Unauthorized',
|
| 89 |
message: 'Valid origin or API key required'
|
| 90 |
});
|
| 91 |
}
|
| 92 |
|
| 93 |
-
|
| 94 |
next();
|
| 95 |
});
|
| 96 |
|
|
@@ -106,12 +136,14 @@ const limiter = rateLimit({
|
|
| 106 |
});
|
| 107 |
app.use(limiter);
|
| 108 |
|
| 109 |
-
// --- REQUEST LOGGING ---
|
| 110 |
app.use((req, res, next) => {
|
| 111 |
-
const ip = (req.ip || 'unknown').replace(/:\d+[^:]*$/, '');
|
| 112 |
-
const origin = req.headers.origin || 'no-origin';
|
| 113 |
const apiKey = req.headers['x-api-key'] ? '***' : 'none';
|
| 114 |
-
|
|
|
|
|
|
|
| 115 |
next();
|
| 116 |
});
|
| 117 |
|
|
@@ -124,7 +156,7 @@ function checkDailyReset() {
|
|
| 124 |
if (today !== lastResetDate) {
|
| 125 |
dailyUsage.clear();
|
| 126 |
lastResetDate = today;
|
| 127 |
-
|
| 128 |
}
|
| 129 |
}
|
| 130 |
|
|
@@ -147,7 +179,7 @@ app.use((req, res, next) => {
|
|
| 147 |
dailyUsage.set(ip, count + 1);
|
| 148 |
|
| 149 |
if (dailyUsage.size > 10000) {
|
| 150 |
-
|
| 151 |
const entries = Array.from(dailyUsage.entries()).slice(0, 1000);
|
| 152 |
entries.forEach(([key]) => dailyUsage.delete(key));
|
| 153 |
}
|
|
@@ -172,7 +204,7 @@ app.use((req, res, next) => {
|
|
| 172 |
const isBot = suspiciousBots.some(bot => userAgent.includes(bot));
|
| 173 |
|
| 174 |
if (isBot) {
|
| 175 |
-
|
| 176 |
return res.status(403).json({
|
| 177 |
error: 'Automated access detected',
|
| 178 |
message: 'This service is for web browsers only.'
|
|
@@ -185,12 +217,12 @@ app.use((req, res, next) => {
|
|
| 185 |
let INSTANCES = [];
|
| 186 |
try {
|
| 187 |
INSTANCES = JSON.parse(process.env.FLOWISE_INSTANCES || '[]');
|
| 188 |
-
|
| 189 |
if (!Array.isArray(INSTANCES) || INSTANCES.length === 0) {
|
| 190 |
-
|
| 191 |
}
|
| 192 |
} catch (e) {
|
| 193 |
-
|
| 194 |
}
|
| 195 |
|
| 196 |
// --- CACHE WITH AUTO-CLEANUP ---
|
|
@@ -229,7 +261,7 @@ async function resolveChatflowId(instanceNum, botName) {
|
|
| 229 |
}
|
| 230 |
|
| 231 |
const instance = INSTANCES[instanceNum - 1];
|
| 232 |
-
|
| 233 |
|
| 234 |
const headers = {};
|
| 235 |
if (instance.key && instance.key.length > 0) {
|
|
@@ -260,35 +292,34 @@ async function resolveChatflowId(instanceNum, botName) {
|
|
| 260 |
timestamp: Date.now()
|
| 261 |
});
|
| 262 |
|
| 263 |
-
|
| 264 |
|
| 265 |
return { id: match.id, instance };
|
| 266 |
}
|
| 267 |
|
| 268 |
-
// ---
|
| 269 |
async function handleStreamingResponse(flowiseResponse, clientRes) {
|
| 270 |
clientRes.setHeader('Content-Type', 'text/event-stream');
|
| 271 |
clientRes.setHeader('Cache-Control', 'no-cache');
|
| 272 |
clientRes.setHeader('Connection', 'keep-alive');
|
| 273 |
clientRes.setHeader('X-Accel-Buffering', 'no');
|
| 274 |
|
| 275 |
-
|
| 276 |
|
| 277 |
let streamStarted = false;
|
| 278 |
let dataReceived = false;
|
| 279 |
let lastDataTime = Date.now();
|
| 280 |
let totalBytes = 0;
|
| 281 |
|
| 282 |
-
// Check for stream timeout every 5 seconds
|
| 283 |
const timeoutCheck = setInterval(() => {
|
| 284 |
const timeSinceData = Date.now() - lastDataTime;
|
| 285 |
|
| 286 |
-
if (timeSinceData > 45000) {
|
| 287 |
-
|
| 288 |
clearInterval(timeoutCheck);
|
| 289 |
|
| 290 |
if (!dataReceived) {
|
| 291 |
-
|
| 292 |
if (!streamStarted) {
|
| 293 |
clientRes.status(504).json({
|
| 294 |
error: 'Gateway timeout',
|
|
@@ -309,7 +340,7 @@ async function handleStreamingResponse(flowiseResponse, clientRes) {
|
|
| 309 |
lastDataTime = Date.now();
|
| 310 |
totalBytes += chunk.length;
|
| 311 |
|
| 312 |
-
|
| 313 |
clientRes.write(chunk);
|
| 314 |
});
|
| 315 |
|
|
@@ -317,9 +348,9 @@ async function handleStreamingResponse(flowiseResponse, clientRes) {
|
|
| 317 |
clearInterval(timeoutCheck);
|
| 318 |
|
| 319 |
if (dataReceived) {
|
| 320 |
-
|
| 321 |
} else {
|
| 322 |
-
|
| 323 |
}
|
| 324 |
|
| 325 |
clientRes.end();
|
|
@@ -327,7 +358,7 @@ async function handleStreamingResponse(flowiseResponse, clientRes) {
|
|
| 327 |
|
| 328 |
flowiseResponse.body.on('error', (err) => {
|
| 329 |
clearInterval(timeoutCheck);
|
| 330 |
-
|
| 331 |
|
| 332 |
if (streamStarted && dataReceived) {
|
| 333 |
clientRes.write(`\n\nevent: error\ndata: {"error": "Stream interrupted"}\n\n`);
|
|
@@ -338,7 +369,7 @@ async function handleStreamingResponse(flowiseResponse, clientRes) {
|
|
| 338 |
});
|
| 339 |
}
|
| 340 |
|
| 341 |
-
// ---
|
| 342 |
app.post('/api/v1/prediction/:instanceNum/:botName', async (req, res) => {
|
| 343 |
try {
|
| 344 |
const instanceNum = parseInt(req.params.instanceNum);
|
|
@@ -365,9 +396,8 @@ app.post('/api/v1/prediction/:instanceNum/:botName', async (req, res) => {
|
|
| 365 |
headers['Authorization'] = `Bearer ${instance.key}`;
|
| 366 |
}
|
| 367 |
|
| 368 |
-
// TIMING: Track how long Flowise takes
|
| 369 |
const startTime = Date.now();
|
| 370 |
-
|
| 371 |
|
| 372 |
const response = await fetchWithTimeout(
|
| 373 |
`${instance.url}/api/v1/prediction/${id}`,
|
|
@@ -376,15 +406,15 @@ app.post('/api/v1/prediction/:instanceNum/:botName', async (req, res) => {
|
|
| 376 |
headers,
|
| 377 |
body: JSON.stringify(req.body)
|
| 378 |
},
|
| 379 |
-
60000
|
| 380 |
);
|
| 381 |
|
| 382 |
const duration = Date.now() - startTime;
|
| 383 |
-
|
| 384 |
|
| 385 |
if (!response.ok) {
|
| 386 |
const errorText = await response.text();
|
| 387 |
-
|
| 388 |
return res.status(response.status).json({
|
| 389 |
error: 'Flowise instance error',
|
| 390 |
message: 'The chatbot instance returned an error.'
|
|
@@ -394,23 +424,23 @@ app.post('/api/v1/prediction/:instanceNum/:botName', async (req, res) => {
|
|
| 394 |
const contentType = response.headers.get('content-type') || '';
|
| 395 |
|
| 396 |
if (contentType.includes('text/event-stream')) {
|
| 397 |
-
|
| 398 |
return handleStreamingResponse(response, res);
|
| 399 |
}
|
| 400 |
|
| 401 |
-
|
| 402 |
const text = await response.text();
|
| 403 |
|
| 404 |
try {
|
| 405 |
const data = JSON.parse(text);
|
| 406 |
res.status(200).json(data);
|
| 407 |
} catch (e) {
|
| 408 |
-
|
| 409 |
res.status(500).json({ error: 'Invalid response from Flowise' });
|
| 410 |
}
|
| 411 |
|
| 412 |
} catch (error) {
|
| 413 |
-
|
| 414 |
res.status(500).json({
|
| 415 |
error: 'Request failed',
|
| 416 |
message: error.message
|
|
@@ -418,7 +448,7 @@ app.post('/api/v1/prediction/:instanceNum/:botName', async (req, res) => {
|
|
| 418 |
}
|
| 419 |
});
|
| 420 |
|
| 421 |
-
// --- ROUTE
|
| 422 |
app.get('/api/v1/public-chatbotConfig/:instanceNum/:botName', async (req, res) => {
|
| 423 |
try {
|
| 424 |
const instanceNum = parseInt(req.params.instanceNum);
|
|
@@ -445,12 +475,12 @@ app.get('/api/v1/public-chatbotConfig/:instanceNum/:botName', async (req, res) =
|
|
| 445 |
res.status(200).json(data);
|
| 446 |
|
| 447 |
} catch (error) {
|
| 448 |
-
|
| 449 |
res.status(404).json({ error: error.message });
|
| 450 |
}
|
| 451 |
});
|
| 452 |
|
| 453 |
-
// ---
|
| 454 |
app.get('/api/v1/chatflows-streaming/:instanceNum/:botName', async (req, res) => {
|
| 455 |
try {
|
| 456 |
const instanceNum = parseInt(req.params.instanceNum);
|
|
@@ -477,46 +507,30 @@ app.get('/api/v1/chatflows-streaming/:instanceNum/:botName', async (req, res) =>
|
|
| 477 |
res.status(200).json(data);
|
| 478 |
|
| 479 |
} catch (error) {
|
| 480 |
-
|
| 481 |
res.status(200).json({ isStreaming: false });
|
| 482 |
}
|
| 483 |
});
|
| 484 |
|
| 485 |
-
// --- TEST
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
});
|
| 503 |
-
|
| 504 |
-
// --- TEST ENDPOINT: Delay Test ---
|
| 505 |
-
app.post('/test-delay', async (req, res) => {
|
| 506 |
-
const start = Date.now();
|
| 507 |
-
const delaySeconds = req.body.delay || 35;
|
| 508 |
-
|
| 509 |
-
console.log(`[Test] Starting ${delaySeconds}s delay test...`);
|
| 510 |
-
await new Promise(resolve => setTimeout(resolve, delaySeconds * 1000));
|
| 511 |
-
|
| 512 |
-
const duration = Date.now() - start;
|
| 513 |
-
res.json({
|
| 514 |
-
message: 'Test completed',
|
| 515 |
-
duration_ms: duration,
|
| 516 |
-
duration_sec: (duration/1000).toFixed(1),
|
| 517 |
-
requested_delay: delaySeconds
|
| 518 |
});
|
| 519 |
-
}
|
| 520 |
|
| 521 |
// --- HEALTH CHECK ---
|
| 522 |
app.get('/', (req, res) => res.send('Federated Proxy Active'));
|
|
@@ -528,7 +542,7 @@ app.get('/health', (req, res) => {
|
|
| 528 |
cached_bots: flowCache.size,
|
| 529 |
daily_active_ips: dailyUsage.size,
|
| 530 |
uptime: process.uptime(),
|
| 531 |
-
|
| 532 |
});
|
| 533 |
});
|
| 534 |
|
|
@@ -539,32 +553,33 @@ app.use((req, res) => {
|
|
| 539 |
|
| 540 |
// --- GLOBAL ERROR HANDLER ---
|
| 541 |
app.use((err, req, res, next) => {
|
| 542 |
-
|
| 543 |
res.status(500).json({ error: 'Internal server error' });
|
| 544 |
});
|
| 545 |
|
| 546 |
-
// ---
|
| 547 |
const server = app.listen(7860, '0.0.0.0', () => {
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
|
|
|
| 554 |
});
|
| 555 |
|
| 556 |
process.on('SIGTERM', () => {
|
| 557 |
-
|
| 558 |
server.close(() => {
|
| 559 |
-
|
| 560 |
process.exit(0);
|
| 561 |
});
|
| 562 |
});
|
| 563 |
|
| 564 |
process.on('SIGINT', () => {
|
| 565 |
-
|
| 566 |
server.close(() => {
|
| 567 |
-
|
| 568 |
process.exit(0);
|
| 569 |
});
|
| 570 |
});
|
|
|
|
| 7 |
|
| 8 |
const app = express();
|
| 9 |
|
| 10 |
+
// --- PRODUCTION MODE ---
|
| 11 |
+
const PRODUCTION_MODE = process.env.PRODUCTION_MODE === 'true';
|
| 12 |
+
|
| 13 |
+
function log(message, level = 'info') {
|
| 14 |
+
if (PRODUCTION_MODE && level === 'debug') return;
|
| 15 |
+
console.log(message);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function logSensitive(message) {
|
| 19 |
+
if (!PRODUCTION_MODE) console.log(message);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// Mask sensitive data
|
| 23 |
+
function maskIP(ip) {
|
| 24 |
+
if (PRODUCTION_MODE) {
|
| 25 |
+
const parts = ip.split('.');
|
| 26 |
+
return parts.length === 4 ? `${parts[0]}.${parts[1]}.***.**` : 'masked';
|
| 27 |
+
}
|
| 28 |
+
return ip;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function maskOrigin(origin) {
|
| 32 |
+
if (PRODUCTION_MODE && origin && origin !== 'no-origin') {
|
| 33 |
+
try {
|
| 34 |
+
const url = new URL(origin);
|
| 35 |
+
return `${url.protocol}//${url.hostname.substring(0, 3)}***`;
|
| 36 |
+
} catch {
|
| 37 |
+
return 'masked';
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
return origin;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
app.use(helmet({
|
| 44 |
contentSecurityPolicy: false,
|
| 45 |
crossOriginEmbedderPolicy: false
|
|
|
|
| 57 |
const origin = req.headers.origin;
|
| 58 |
const apiKey = req.headers['x-api-key'];
|
| 59 |
|
|
|
|
| 60 |
if (origin) {
|
| 61 |
if (allowedOrigins.length === 0) return { valid: true, source: 'open-mode' };
|
| 62 |
return {
|
| 63 |
valid: allowedOrigins.includes(origin),
|
| 64 |
+
source: PRODUCTION_MODE ? 'authorized-origin' : origin
|
| 65 |
};
|
| 66 |
}
|
| 67 |
|
|
|
|
| 68 |
if (apiKey) {
|
| 69 |
if (API_KEYS.length === 0) return { valid: true, source: 'no-keys-configured' };
|
| 70 |
return {
|
|
|
|
| 73 |
};
|
| 74 |
}
|
| 75 |
|
|
|
|
| 76 |
return { valid: false, source: 'unauthorized' };
|
| 77 |
}
|
| 78 |
|
| 79 |
+
// --- CORS ---
|
| 80 |
const allowedOrigins = process.env.ALLOWED_ORIGINS
|
| 81 |
? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
|
| 82 |
: [];
|
|
|
|
| 90 |
if (allowedOrigins.includes(origin)) {
|
| 91 |
callback(null, true);
|
| 92 |
} else {
|
| 93 |
+
log(`[Security] Blocked origin: ${maskOrigin(origin)}`);
|
| 94 |
callback(new Error('Not allowed by CORS'));
|
| 95 |
}
|
| 96 |
},
|
|
|
|
| 113 |
const auth = authenticateRequest(req);
|
| 114 |
|
| 115 |
if (!auth.valid) {
|
| 116 |
+
log(`[Security] Blocked unauthorized request from ${maskIP(req.ip)}`);
|
| 117 |
return res.status(401).json({
|
| 118 |
error: 'Unauthorized',
|
| 119 |
message: 'Valid origin or API key required'
|
| 120 |
});
|
| 121 |
}
|
| 122 |
|
| 123 |
+
log(`[Auth] Request authorized from: ${auth.source}`);
|
| 124 |
next();
|
| 125 |
});
|
| 126 |
|
|
|
|
| 136 |
});
|
| 137 |
app.use(limiter);
|
| 138 |
|
| 139 |
+
// --- REQUEST LOGGING (SAFE) ---
|
| 140 |
app.use((req, res, next) => {
|
| 141 |
+
const ip = maskIP((req.ip || 'unknown').replace(/:\d+[^:]*$/, ''));
|
| 142 |
+
const origin = maskOrigin(req.headers.origin || 'no-origin');
|
| 143 |
const apiKey = req.headers['x-api-key'] ? '***' : 'none';
|
| 144 |
+
const path = PRODUCTION_MODE ? req.path.split('/').slice(0, 4).join('/') + '/***' : req.path;
|
| 145 |
+
|
| 146 |
+
log(`[${new Date().toISOString()}] ${ip} -> ${req.method} ${path} | Origin: ${origin} | Key: ${apiKey}`);
|
| 147 |
next();
|
| 148 |
});
|
| 149 |
|
|
|
|
| 156 |
if (today !== lastResetDate) {
|
| 157 |
dailyUsage.clear();
|
| 158 |
lastResetDate = today;
|
| 159 |
+
log('[System] Daily usage counters reset');
|
| 160 |
}
|
| 161 |
}
|
| 162 |
|
|
|
|
| 179 |
dailyUsage.set(ip, count + 1);
|
| 180 |
|
| 181 |
if (dailyUsage.size > 10000) {
|
| 182 |
+
log('[System] Daily usage map too large, clearing oldest entries', 'debug');
|
| 183 |
const entries = Array.from(dailyUsage.entries()).slice(0, 1000);
|
| 184 |
entries.forEach(([key]) => dailyUsage.delete(key));
|
| 185 |
}
|
|
|
|
| 204 |
const isBot = suspiciousBots.some(bot => userAgent.includes(bot));
|
| 205 |
|
| 206 |
if (isBot) {
|
| 207 |
+
log(`[Security] Blocked bot from ${maskIP(req.ip)}`);
|
| 208 |
return res.status(403).json({
|
| 209 |
error: 'Automated access detected',
|
| 210 |
message: 'This service is for web browsers only.'
|
|
|
|
| 217 |
let INSTANCES = [];
|
| 218 |
try {
|
| 219 |
INSTANCES = JSON.parse(process.env.FLOWISE_INSTANCES || '[]');
|
| 220 |
+
log(`[System] Loaded ${INSTANCES.length} instances`);
|
| 221 |
if (!Array.isArray(INSTANCES) || INSTANCES.length === 0) {
|
| 222 |
+
log('ERROR: FLOWISE_INSTANCES must be a non-empty array');
|
| 223 |
}
|
| 224 |
} catch (e) {
|
| 225 |
+
log("CRITICAL ERROR: Could not parse FLOWISE_INSTANCES JSON");
|
| 226 |
}
|
| 227 |
|
| 228 |
// --- CACHE WITH AUTO-CLEANUP ---
|
|
|
|
| 261 |
}
|
| 262 |
|
| 263 |
const instance = INSTANCES[instanceNum - 1];
|
| 264 |
+
logSensitive(`[System] Looking up '${botName}' in instance ${instanceNum}...`);
|
| 265 |
|
| 266 |
const headers = {};
|
| 267 |
if (instance.key && instance.key.length > 0) {
|
|
|
|
| 292 |
timestamp: Date.now()
|
| 293 |
});
|
| 294 |
|
| 295 |
+
logSensitive(`[System] Found '${botName}' -> ${match.id}`);
|
| 296 |
|
| 297 |
return { id: match.id, instance };
|
| 298 |
}
|
| 299 |
|
| 300 |
+
// --- STREAMING HANDLER ---
|
| 301 |
async function handleStreamingResponse(flowiseResponse, clientRes) {
|
| 302 |
clientRes.setHeader('Content-Type', 'text/event-stream');
|
| 303 |
clientRes.setHeader('Cache-Control', 'no-cache');
|
| 304 |
clientRes.setHeader('Connection', 'keep-alive');
|
| 305 |
clientRes.setHeader('X-Accel-Buffering', 'no');
|
| 306 |
|
| 307 |
+
log('[Streaming] Forwarding SSE stream...');
|
| 308 |
|
| 309 |
let streamStarted = false;
|
| 310 |
let dataReceived = false;
|
| 311 |
let lastDataTime = Date.now();
|
| 312 |
let totalBytes = 0;
|
| 313 |
|
|
|
|
| 314 |
const timeoutCheck = setInterval(() => {
|
| 315 |
const timeSinceData = Date.now() - lastDataTime;
|
| 316 |
|
| 317 |
+
if (timeSinceData > 45000) {
|
| 318 |
+
log(`[Streaming] Timeout - no data for ${(timeSinceData/1000).toFixed(1)}s`);
|
| 319 |
clearInterval(timeoutCheck);
|
| 320 |
|
| 321 |
if (!dataReceived) {
|
| 322 |
+
log('[Streaming] Stream completed with NO data received!');
|
| 323 |
if (!streamStarted) {
|
| 324 |
clientRes.status(504).json({
|
| 325 |
error: 'Gateway timeout',
|
|
|
|
| 340 |
lastDataTime = Date.now();
|
| 341 |
totalBytes += chunk.length;
|
| 342 |
|
| 343 |
+
logSensitive(`[Streaming] Received chunk: ${chunk.length} bytes (total: ${totalBytes})`);
|
| 344 |
clientRes.write(chunk);
|
| 345 |
});
|
| 346 |
|
|
|
|
| 348 |
clearInterval(timeoutCheck);
|
| 349 |
|
| 350 |
if (dataReceived) {
|
| 351 |
+
log(`[Streaming] Stream completed - ${totalBytes} bytes`);
|
| 352 |
} else {
|
| 353 |
+
log('[Streaming] Stream completed but NO data received!');
|
| 354 |
}
|
| 355 |
|
| 356 |
clientRes.end();
|
|
|
|
| 358 |
|
| 359 |
flowiseResponse.body.on('error', (err) => {
|
| 360 |
clearInterval(timeoutCheck);
|
| 361 |
+
log('[Streaming Error]');
|
| 362 |
|
| 363 |
if (streamStarted && dataReceived) {
|
| 364 |
clientRes.write(`\n\nevent: error\ndata: {"error": "Stream interrupted"}\n\n`);
|
|
|
|
| 369 |
});
|
| 370 |
}
|
| 371 |
|
| 372 |
+
// --- PREDICTION ROUTE ---
|
| 373 |
app.post('/api/v1/prediction/:instanceNum/:botName', async (req, res) => {
|
| 374 |
try {
|
| 375 |
const instanceNum = parseInt(req.params.instanceNum);
|
|
|
|
| 396 |
headers['Authorization'] = `Bearer ${instance.key}`;
|
| 397 |
}
|
| 398 |
|
|
|
|
| 399 |
const startTime = Date.now();
|
| 400 |
+
logSensitive(`[Timing] Calling Flowise at ${new Date().toISOString()}`);
|
| 401 |
|
| 402 |
const response = await fetchWithTimeout(
|
| 403 |
`${instance.url}/api/v1/prediction/${id}`,
|
|
|
|
| 406 |
headers,
|
| 407 |
body: JSON.stringify(req.body)
|
| 408 |
},
|
| 409 |
+
60000
|
| 410 |
);
|
| 411 |
|
| 412 |
const duration = Date.now() - startTime;
|
| 413 |
+
log(`[Timing] Response received in ${(duration/1000).toFixed(1)}s`);
|
| 414 |
|
| 415 |
if (!response.ok) {
|
| 416 |
const errorText = await response.text();
|
| 417 |
+
logSensitive(`[Error] Instance returned ${response.status}: ${errorText.substring(0, 100)}`);
|
| 418 |
return res.status(response.status).json({
|
| 419 |
error: 'Flowise instance error',
|
| 420 |
message: 'The chatbot instance returned an error.'
|
|
|
|
| 424 |
const contentType = response.headers.get('content-type') || '';
|
| 425 |
|
| 426 |
if (contentType.includes('text/event-stream')) {
|
| 427 |
+
log('[Streaming] Detected SSE response');
|
| 428 |
return handleStreamingResponse(response, res);
|
| 429 |
}
|
| 430 |
|
| 431 |
+
log('[Non-streaming] Parsing JSON response');
|
| 432 |
const text = await response.text();
|
| 433 |
|
| 434 |
try {
|
| 435 |
const data = JSON.parse(text);
|
| 436 |
res.status(200).json(data);
|
| 437 |
} catch (e) {
|
| 438 |
+
log('[Error] Invalid JSON response');
|
| 439 |
res.status(500).json({ error: 'Invalid response from Flowise' });
|
| 440 |
}
|
| 441 |
|
| 442 |
} catch (error) {
|
| 443 |
+
log(`[Error] ${error.message}`);
|
| 444 |
res.status(500).json({
|
| 445 |
error: 'Request failed',
|
| 446 |
message: error.message
|
|
|
|
| 448 |
}
|
| 449 |
});
|
| 450 |
|
| 451 |
+
// --- CONFIG ROUTE ---
|
| 452 |
app.get('/api/v1/public-chatbotConfig/:instanceNum/:botName', async (req, res) => {
|
| 453 |
try {
|
| 454 |
const instanceNum = parseInt(req.params.instanceNum);
|
|
|
|
| 475 |
res.status(200).json(data);
|
| 476 |
|
| 477 |
} catch (error) {
|
| 478 |
+
log('[Error] Config request failed');
|
| 479 |
res.status(404).json({ error: error.message });
|
| 480 |
}
|
| 481 |
});
|
| 482 |
|
| 483 |
+
// --- STREAMING CHECK ROUTE ---
|
| 484 |
app.get('/api/v1/chatflows-streaming/:instanceNum/:botName', async (req, res) => {
|
| 485 |
try {
|
| 486 |
const instanceNum = parseInt(req.params.instanceNum);
|
|
|
|
| 507 |
res.status(200).json(data);
|
| 508 |
|
| 509 |
} catch (error) {
|
| 510 |
+
log('[Error] Streaming check failed', 'debug');
|
| 511 |
res.status(200).json({ isStreaming: false });
|
| 512 |
}
|
| 513 |
});
|
| 514 |
|
| 515 |
+
// --- TEST ENDPOINTS (DISABLED IN PRODUCTION) ---
|
| 516 |
+
if (!PRODUCTION_MODE) {
|
| 517 |
+
app.get('/test-stream', (req, res) => {
|
| 518 |
+
res.setHeader('Content-Type', 'text/event-stream');
|
| 519 |
+
res.setHeader('Cache-Control', 'no-cache');
|
| 520 |
+
res.setHeader('Connection', 'keep-alive');
|
| 521 |
+
|
| 522 |
+
let count = 0;
|
| 523 |
+
const interval = setInterval(() => {
|
| 524 |
+
count++;
|
| 525 |
+
res.write(`data: {"message": "Test ${count}"}\n\n`);
|
| 526 |
+
|
| 527 |
+
if (count >= 5) {
|
| 528 |
+
clearInterval(interval);
|
| 529 |
+
res.end();
|
| 530 |
+
}
|
| 531 |
+
}, 500);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
});
|
| 533 |
+
}
|
| 534 |
|
| 535 |
// --- HEALTH CHECK ---
|
| 536 |
app.get('/', (req, res) => res.send('Federated Proxy Active'));
|
|
|
|
| 542 |
cached_bots: flowCache.size,
|
| 543 |
daily_active_ips: dailyUsage.size,
|
| 544 |
uptime: process.uptime(),
|
| 545 |
+
production_mode: PRODUCTION_MODE
|
| 546 |
});
|
| 547 |
});
|
| 548 |
|
|
|
|
| 553 |
|
| 554 |
// --- GLOBAL ERROR HANDLER ---
|
| 555 |
app.use((err, req, res, next) => {
|
| 556 |
+
log('[Error] Unhandled error');
|
| 557 |
res.status(500).json({ error: 'Internal server error' });
|
| 558 |
});
|
| 559 |
|
| 560 |
+
// --- SERVER START ---
|
| 561 |
const server = app.listen(7860, '0.0.0.0', () => {
|
| 562 |
+
log('===== Federated Proxy Started =====');
|
| 563 |
+
log(`Port: 7860`);
|
| 564 |
+
log(`Mode: ${PRODUCTION_MODE ? 'PRODUCTION' : 'DEVELOPMENT'}`);
|
| 565 |
+
log(`Instances: ${INSTANCES.length}`);
|
| 566 |
+
log(`Allowed Origins: ${allowedOrigins.length || 'Open'}`);
|
| 567 |
+
log(`API Keys: ${API_KEYS.length || 'None'}`);
|
| 568 |
+
log('====================================');
|
| 569 |
});
|
| 570 |
|
| 571 |
process.on('SIGTERM', () => {
|
| 572 |
+
log('[System] Shutting down gracefully...');
|
| 573 |
server.close(() => {
|
| 574 |
+
log('[System] Server closed');
|
| 575 |
process.exit(0);
|
| 576 |
});
|
| 577 |
});
|
| 578 |
|
| 579 |
process.on('SIGINT', () => {
|
| 580 |
+
log('[System] Shutting down gracefully...');
|
| 581 |
server.close(() => {
|
| 582 |
+
log('[System] Server closed');
|
| 583 |
process.exit(0);
|
| 584 |
});
|
| 585 |
});
|