Update server.js
Browse files
server.js
CHANGED
|
@@ -7,7 +7,6 @@ require('dotenv').config();
|
|
| 7 |
|
| 8 |
const app = express();
|
| 9 |
|
| 10 |
-
// --- SECURITY HEADERS ---
|
| 11 |
app.use(helmet({
|
| 12 |
contentSecurityPolicy: false,
|
| 13 |
crossOriginEmbedderPolicy: false
|
|
@@ -16,14 +15,48 @@ app.use(helmet({
|
|
| 16 |
app.set('trust proxy', 1);
|
| 17 |
app.use(express.json({ limit: '1mb' }));
|
| 18 |
|
| 19 |
-
// ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
const allowedOrigins = process.env.ALLOWED_ORIGINS
|
| 21 |
? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
|
| 22 |
: [];
|
| 23 |
|
| 24 |
app.use(cors({
|
| 25 |
origin: function (origin, callback) {
|
| 26 |
-
if (!origin
|
|
|
|
|
|
|
|
|
|
| 27 |
if (allowedOrigins.includes(origin)) {
|
| 28 |
callback(null, true);
|
| 29 |
} else {
|
|
@@ -41,7 +74,27 @@ app.use((err, req, res, next) => {
|
|
| 41 |
next(err);
|
| 42 |
});
|
| 43 |
|
| 44 |
-
// ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
const limiter = rateLimit({
|
| 46 |
windowMs: 15 * 60 * 1000,
|
| 47 |
max: 100,
|
|
@@ -56,8 +109,9 @@ app.use(limiter);
|
|
| 56 |
// --- REQUEST LOGGING ---
|
| 57 |
app.use((req, res, next) => {
|
| 58 |
const ip = (req.ip || 'unknown').replace(/:\d+[^:]*$/, '');
|
| 59 |
-
const origin =
|
| 60 |
-
|
|
|
|
| 61 |
next();
|
| 62 |
});
|
| 63 |
|
|
@@ -92,7 +146,6 @@ app.use((req, res, next) => {
|
|
| 92 |
|
| 93 |
dailyUsage.set(ip, count + 1);
|
| 94 |
|
| 95 |
-
// FIXED: Prevent unbounded growth
|
| 96 |
if (dailyUsage.size > 10000) {
|
| 97 |
console.warn('[System] Daily usage map too large, clearing oldest entries');
|
| 98 |
const entries = Array.from(dailyUsage.entries()).slice(0, 1000);
|
|
@@ -104,7 +157,6 @@ app.use((req, res, next) => {
|
|
| 104 |
|
| 105 |
// --- BOT DETECTION ---
|
| 106 |
app.use((req, res, next) => {
|
| 107 |
-
// FIXED: Check all POST requests, not just /prediction/
|
| 108 |
if (req.method !== 'POST') {
|
| 109 |
return next();
|
| 110 |
}
|
|
@@ -112,6 +164,11 @@ app.use((req, res, next) => {
|
|
| 112 |
const userAgent = (req.headers['user-agent'] || '').toLowerCase();
|
| 113 |
const suspiciousBots = ['python-requests', 'curl/', 'wget/', 'scrapy', 'crawler'];
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
const isBot = suspiciousBots.some(bot => userAgent.includes(bot));
|
| 116 |
|
| 117 |
if (isBot) {
|
|
@@ -142,7 +199,6 @@ const flowCache = new Map();
|
|
| 142 |
setInterval(() => {
|
| 143 |
const now = Date.now();
|
| 144 |
for (const [key, value] of flowCache.entries()) {
|
| 145 |
-
// FIXED: Safety check for timestamp
|
| 146 |
if (value.timestamp && now - value.timestamp > 10 * 60 * 1000) {
|
| 147 |
flowCache.delete(key);
|
| 148 |
}
|
|
@@ -232,7 +288,6 @@ async function handleStreamingResponse(flowiseResponse, clientRes) {
|
|
| 232 |
flowiseResponse.body.on('error', (err) => {
|
| 233 |
console.error('[Streaming Error]', err.message);
|
| 234 |
|
| 235 |
-
// FIXED: Send error event if stream already started
|
| 236 |
if (streamStarted) {
|
| 237 |
clientRes.write(`\n\nevent: error\ndata: {"error": "Stream interrupted"}\n\n`);
|
| 238 |
}
|
|
@@ -240,7 +295,7 @@ async function handleStreamingResponse(flowiseResponse, clientRes) {
|
|
| 240 |
});
|
| 241 |
}
|
| 242 |
|
| 243 |
-
// ---
|
| 244 |
app.post('/api/v1/prediction/:instanceNum/:botName', async (req, res) => {
|
| 245 |
try {
|
| 246 |
const instanceNum = parseInt(req.params.instanceNum);
|
|
@@ -288,13 +343,11 @@ app.post('/api/v1/prediction/:instanceNum/:botName', async (req, res) => {
|
|
| 288 |
|
| 289 |
const contentType = response.headers.get('content-type') || '';
|
| 290 |
|
| 291 |
-
// STREAMING RESPONSE
|
| 292 |
if (contentType.includes('text/event-stream')) {
|
| 293 |
console.log('[Streaming] Detected SSE response');
|
| 294 |
return handleStreamingResponse(response, res);
|
| 295 |
}
|
| 296 |
|
| 297 |
-
// NON-STREAMING RESPONSE
|
| 298 |
console.log('[Non-streaming] Parsing JSON response');
|
| 299 |
const text = await response.text();
|
| 300 |
|
|
@@ -315,7 +368,6 @@ app.post('/api/v1/prediction/:instanceNum/:botName', async (req, res) => {
|
|
| 315 |
}
|
| 316 |
});
|
| 317 |
|
| 318 |
-
// --- ROUTE 2: CHATBOT CONFIG ---
|
| 319 |
app.get('/api/v1/public-chatbotConfig/:instanceNum/:botName', async (req, res) => {
|
| 320 |
try {
|
| 321 |
const instanceNum = parseInt(req.params.instanceNum);
|
|
@@ -347,7 +399,6 @@ app.get('/api/v1/public-chatbotConfig/:instanceNum/:botName', async (req, res) =
|
|
| 347 |
}
|
| 348 |
});
|
| 349 |
|
| 350 |
-
// --- ROUTE 3: STREAMING CHECK ---
|
| 351 |
app.get('/api/v1/chatflows-streaming/:instanceNum/:botName', async (req, res) => {
|
| 352 |
try {
|
| 353 |
const instanceNum = parseInt(req.params.instanceNum);
|
|
@@ -379,7 +430,6 @@ app.get('/api/v1/chatflows-streaming/:instanceNum/:botName', async (req, res) =>
|
|
| 379 |
}
|
| 380 |
});
|
| 381 |
|
| 382 |
-
// --- HEALTH CHECK ---
|
| 383 |
app.get('/', (req, res) => res.send('Federated Proxy Active'));
|
| 384 |
|
| 385 |
app.get('/health', (req, res) => {
|
|
@@ -392,18 +442,15 @@ app.get('/health', (req, res) => {
|
|
| 392 |
});
|
| 393 |
});
|
| 394 |
|
| 395 |
-
// --- 404 HANDLER ---
|
| 396 |
app.use((req, res) => {
|
| 397 |
res.status(404).json({ error: 'Route not found' });
|
| 398 |
});
|
| 399 |
|
| 400 |
-
// --- GLOBAL ERROR HANDLER ---
|
| 401 |
app.use((err, req, res, next) => {
|
| 402 |
console.error('[Error] Unhandled error:', err);
|
| 403 |
res.status(500).json({ error: 'Internal server error' });
|
| 404 |
});
|
| 405 |
|
| 406 |
-
// --- GRACEFUL SHUTDOWN ---
|
| 407 |
const server = app.listen(7860, '0.0.0.0', () => {
|
| 408 |
console.log('Federated Proxy running on port 7860');
|
| 409 |
});
|
|
|
|
| 7 |
|
| 8 |
const app = express();
|
| 9 |
|
|
|
|
| 10 |
app.use(helmet({
|
| 11 |
contentSecurityPolicy: false,
|
| 12 |
crossOriginEmbedderPolicy: false
|
|
|
|
| 15 |
app.set('trust proxy', 1);
|
| 16 |
app.use(express.json({ limit: '1mb' }));
|
| 17 |
|
| 18 |
+
// --- API KEY AUTHENTICATION ---
|
| 19 |
+
const API_KEYS = process.env.API_KEYS
|
| 20 |
+
? process.env.API_KEYS.split(',').map(k => k.trim())
|
| 21 |
+
: [];
|
| 22 |
+
|
| 23 |
+
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 {
|
| 40 |
+
valid: API_KEYS.includes(apiKey),
|
| 41 |
+
source: 'api-key'
|
| 42 |
+
};
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// CASE 3: No Origin, No API Key - BLOCKED
|
| 46 |
+
return { valid: false, source: 'unauthorized' };
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// --- CORS (For browsers) ---
|
| 50 |
const allowedOrigins = process.env.ALLOWED_ORIGINS
|
| 51 |
? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
|
| 52 |
: [];
|
| 53 |
|
| 54 |
app.use(cors({
|
| 55 |
origin: function (origin, callback) {
|
| 56 |
+
if (!origin) {
|
| 57 |
+
return callback(null, true);
|
| 58 |
+
}
|
| 59 |
+
if (allowedOrigins.length === 0) return callback(null, true);
|
| 60 |
if (allowedOrigins.includes(origin)) {
|
| 61 |
callback(null, true);
|
| 62 |
} else {
|
|
|
|
| 74 |
next(err);
|
| 75 |
});
|
| 76 |
|
| 77 |
+
// --- API KEY AUTHENTICATION MIDDLEWARE ---
|
| 78 |
+
app.use((req, res, next) => {
|
| 79 |
+
if (req.path === '/' || req.path === '/health') {
|
| 80 |
+
return next();
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
const auth = authenticateRequest(req);
|
| 84 |
+
|
| 85 |
+
if (!auth.valid) {
|
| 86 |
+
console.log(`[Security] Blocked unauthorized request from ${req.ip} (${auth.source})`);
|
| 87 |
+
return res.status(401).json({
|
| 88 |
+
error: 'Unauthorized',
|
| 89 |
+
message: 'Valid origin or API key required'
|
| 90 |
+
});
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
console.log(`[Auth] Request authorized from: ${auth.source}`);
|
| 94 |
+
next();
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
// --- RATE LIMITING ---
|
| 98 |
const limiter = rateLimit({
|
| 99 |
windowMs: 15 * 60 * 1000,
|
| 100 |
max: 100,
|
|
|
|
| 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 |
+
console.log(`[${new Date().toISOString()}] ${ip} -> ${req.method} ${req.path} | Origin: ${origin} | Key: ${apiKey}`);
|
| 115 |
next();
|
| 116 |
});
|
| 117 |
|
|
|
|
| 146 |
|
| 147 |
dailyUsage.set(ip, count + 1);
|
| 148 |
|
|
|
|
| 149 |
if (dailyUsage.size > 10000) {
|
| 150 |
console.warn('[System] Daily usage map too large, clearing oldest entries');
|
| 151 |
const entries = Array.from(dailyUsage.entries()).slice(0, 1000);
|
|
|
|
| 157 |
|
| 158 |
// --- BOT DETECTION ---
|
| 159 |
app.use((req, res, next) => {
|
|
|
|
| 160 |
if (req.method !== 'POST') {
|
| 161 |
return next();
|
| 162 |
}
|
|
|
|
| 164 |
const userAgent = (req.headers['user-agent'] || '').toLowerCase();
|
| 165 |
const suspiciousBots = ['python-requests', 'curl/', 'wget/', 'scrapy', 'crawler'];
|
| 166 |
|
| 167 |
+
const hasValidApiKey = req.headers['x-api-key'] && API_KEYS.includes(req.headers['x-api-key']);
|
| 168 |
+
if (hasValidApiKey) {
|
| 169 |
+
return next();
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
const isBot = suspiciousBots.some(bot => userAgent.includes(bot));
|
| 173 |
|
| 174 |
if (isBot) {
|
|
|
|
| 199 |
setInterval(() => {
|
| 200 |
const now = Date.now();
|
| 201 |
for (const [key, value] of flowCache.entries()) {
|
|
|
|
| 202 |
if (value.timestamp && now - value.timestamp > 10 * 60 * 1000) {
|
| 203 |
flowCache.delete(key);
|
| 204 |
}
|
|
|
|
| 288 |
flowiseResponse.body.on('error', (err) => {
|
| 289 |
console.error('[Streaming Error]', err.message);
|
| 290 |
|
|
|
|
| 291 |
if (streamStarted) {
|
| 292 |
clientRes.write(`\n\nevent: error\ndata: {"error": "Stream interrupted"}\n\n`);
|
| 293 |
}
|
|
|
|
| 295 |
});
|
| 296 |
}
|
| 297 |
|
| 298 |
+
// --- ROUTES ---
|
| 299 |
app.post('/api/v1/prediction/:instanceNum/:botName', async (req, res) => {
|
| 300 |
try {
|
| 301 |
const instanceNum = parseInt(req.params.instanceNum);
|
|
|
|
| 343 |
|
| 344 |
const contentType = response.headers.get('content-type') || '';
|
| 345 |
|
|
|
|
| 346 |
if (contentType.includes('text/event-stream')) {
|
| 347 |
console.log('[Streaming] Detected SSE response');
|
| 348 |
return handleStreamingResponse(response, res);
|
| 349 |
}
|
| 350 |
|
|
|
|
| 351 |
console.log('[Non-streaming] Parsing JSON response');
|
| 352 |
const text = await response.text();
|
| 353 |
|
|
|
|
| 368 |
}
|
| 369 |
});
|
| 370 |
|
|
|
|
| 371 |
app.get('/api/v1/public-chatbotConfig/:instanceNum/:botName', async (req, res) => {
|
| 372 |
try {
|
| 373 |
const instanceNum = parseInt(req.params.instanceNum);
|
|
|
|
| 399 |
}
|
| 400 |
});
|
| 401 |
|
|
|
|
| 402 |
app.get('/api/v1/chatflows-streaming/:instanceNum/:botName', async (req, res) => {
|
| 403 |
try {
|
| 404 |
const instanceNum = parseInt(req.params.instanceNum);
|
|
|
|
| 430 |
}
|
| 431 |
});
|
| 432 |
|
|
|
|
| 433 |
app.get('/', (req, res) => res.send('Federated Proxy Active'));
|
| 434 |
|
| 435 |
app.get('/health', (req, res) => {
|
|
|
|
| 442 |
});
|
| 443 |
});
|
| 444 |
|
|
|
|
| 445 |
app.use((req, res) => {
|
| 446 |
res.status(404).json({ error: 'Route not found' });
|
| 447 |
});
|
| 448 |
|
|
|
|
| 449 |
app.use((err, req, res, next) => {
|
| 450 |
console.error('[Error] Unhandled error:', err);
|
| 451 |
res.status(500).json({ error: 'Internal server error' });
|
| 452 |
});
|
| 453 |
|
|
|
|
| 454 |
const server = app.listen(7860, '0.0.0.0', () => {
|
| 455 |
console.log('Federated Proxy running on port 7860');
|
| 456 |
});
|