incognitolm commited on
Commit
92f389e
·
1 Parent(s): 1a7c116

Update index.js

Browse files
Files changed (1) hide show
  1. server/index.js +108 -15
server/index.js CHANGED
@@ -32,6 +32,7 @@ const CDN_BASE = `https://cdn.jsdelivr.net/gh/${GITHUB_REPO}`;
32
  let latestSHA = null;
33
  const ADMIN_TOKEN = process.env.ADMIN_TOKEN || 'supersecret';
34
  const TURNSTILE_SITE_KEY = process.env.TURNSTILE_SITE_KEY || '0x4AAAAAAC1ZXKIhZ9Kdz8j9';
 
35
  const LOCAL_UI_DIR = [
36
  process.env.UI_LOCAL_PATH,
37
  path.resolve(__dirname, '..', '..', 'InferencePort-Pages'),
@@ -45,7 +46,16 @@ const LOCAL_UI_DIR = [
45
  }) || null;
46
 
47
  // Rate limiter for admin endpoints (5 attempts per IP per minute)
48
- const verifyLimiter = rateLimit({ windowMs: 60*1000, max: 5, standardHeaders: true, legacyHeaders: false });
 
 
 
 
 
 
 
 
 
49
 
50
  const DATA_DIR = "/data";
51
  const VERSION_FILE = path.join(DATA_DIR, 'version.json');
@@ -58,6 +68,27 @@ function getRequestIp(req) {
58
  || 'unknown';
59
  }
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  function getCookieMap(req) {
62
  const cookies = (req.headers.cookie || '')
63
  .split(';')
@@ -78,12 +109,36 @@ function hasAdminTurnstile(req) {
78
 
79
  function requireAdminTurnstile(req, res, next) {
80
  if (hasAdminTurnstile(req)) return next();
 
81
  return res.status(403).json({ error: 'turnstile:required' });
82
  }
83
 
84
- function logAdminEvent(req, action, detail = '') {
85
- const suffix = detail ? ` ${detail}` : '';
86
- console.log(`[ADMIN] ${action} from ${getRequestIp(req)}${suffix}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
  async function verifyTurnstileToken(token, remoteIp) {
@@ -236,10 +291,14 @@ app.post('/api/media/upload', express.raw({ type: () => true, limit: '100mb' }),
236
  const parentId = req.headers['x-parent-id'] ? String(req.headers['x-parent-id']) : null;
237
  const sessionId = req.headers['x-session-id'] ? String(req.headers['x-session-id']) : null;
238
  const kindHeader = req.headers['x-file-kind'] ? String(req.headers['x-file-kind']) : null;
 
 
 
 
239
  const item = await mediaStore.storeBuffer(resolved.owner, {
240
  name,
241
  mimeType,
242
- buffer: Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || []),
243
  parentId,
244
  sessionId,
245
  source: 'user_upload',
@@ -277,11 +336,16 @@ app.post('/api/media/documents', async (req, res) => {
277
  const resolved = await requireRequestOwner(req, res);
278
  if (!resolved) return;
279
  try {
 
 
 
 
 
280
  const item = await mediaStore.createDocument(resolved.owner, {
281
  name: req.body?.name,
282
  parentId: req.body?.parentId || null,
283
- richText: !!req.body?.richText,
284
- content: req.body?.content || '',
285
  source: 'user_upload',
286
  sessionId: req.body?.sessionId || null,
287
  });
@@ -366,8 +430,12 @@ app.put('/api/media/:id/text', async (req, res) => {
366
  const resolved = await requireRequestOwner(req, res);
367
  if (!resolved) return;
368
  try {
 
 
 
 
369
  const item = await mediaStore.updateContent(resolved.owner, req.params.id, {
370
- buffer: Buffer.from(String(req.body?.content || ''), 'utf8'),
371
  mimeType: req.body?.mimeType || null,
372
  kind: req.body?.richText ? 'rich_text' : null,
373
  name: req.body?.name || null,
@@ -438,6 +506,7 @@ if (!latestSHA) {
438
 
439
  // --- Admin endpoints ---
440
  app.get('/admin.html', async (req, res) => {
 
441
  if (serveLocalUiFile('/admin.html', res)) return;
442
  if (!latestSHA) return res.status(500).send('Server not ready');
443
 
@@ -457,6 +526,7 @@ app.get('/admin.html', async (req, res) => {
457
  });
458
 
459
  app.get('/admin/config', (req, res) => {
 
460
  res.json({
461
  siteKey: TURNSTILE_SITE_KEY || null,
462
  verified: hasAdminTurnstile(req),
@@ -467,11 +537,18 @@ app.post('/admin/turnstile', async (req, res) => {
467
  try {
468
  const token = req.body?.token;
469
  const result = await verifyTurnstileToken(token, getRequestIp(req));
470
- if (!result?.success) return res.status(403).json({ error: 'Verification failed' });
 
 
 
 
 
471
  res.cookie('admin_turnstile', '1', { maxAge: 2 * 3600 * 1000, path: '/', sameSite: 'lax' });
 
472
  return res.json({ success: true });
473
  } catch (err) {
474
  console.error('admin turnstile verify', err);
 
475
  return res.status(500).json({ error: err.message || 'Server error' });
476
  }
477
  });
@@ -479,32 +556,48 @@ app.post('/admin/turnstile', async (req, res) => {
479
  app.get('/admin/verify', requireAdminTurnstile, verifyLimiter, (req,res)=>{
480
  const token = req.query.token;
481
  const success = token===ADMIN_TOKEN;
482
- if (success) logAdminEvent(req, 'login');
 
 
 
483
  res.json({success});
484
  });
485
 
486
  app.get('/admin/refresh', requireAdminTurnstile, verifyLimiter, async (req, res) => {
487
  const token = req.query.token;
488
- if (token !== ADMIN_TOKEN) return res.status(403).send('Forbidden');
 
 
 
489
 
490
  const sha = req.query.sha?.trim();
491
  if (sha) {
492
- if (!/^[0-9a-f]{7,40}$/.test(sha)) return res.status(400).send('Invalid SHA');
 
 
 
493
  latestSHA = sha;
494
  saveStoredSHA(latestSHA);
495
- logAdminEvent(req, 'set_sha', `to ${latestSHA}`);
496
  console.log(`[${PUBLIC_URL}] Manual SHA set by admin: ${latestSHA}`);
497
  return res.send(`Version set to commit ${latestSHA}`);
498
  }
499
 
500
  await fetchLatestSHA();
501
- logAdminEvent(req, 'refresh_latest', `to ${latestSHA}`);
502
  res.send(`Latest version refreshed: ${latestSHA}`);
503
  });
504
 
505
  app.get('/admin/status', requireAdminTurnstile, verifyLimiter, async (req, res) => {
506
  const token = req.query.token;
507
- if (token !== ADMIN_TOKEN) return res.status(403).json({ error: 'Forbidden' });
 
 
 
 
 
 
 
508
  res.json({
509
  publicUrl: PUBLIC_URL,
510
  currentSha: latestSHA,
 
32
  let latestSHA = null;
33
  const ADMIN_TOKEN = process.env.ADMIN_TOKEN || 'supersecret';
34
  const TURNSTILE_SITE_KEY = process.env.TURNSTILE_SITE_KEY || '0x4AAAAAAC1ZXKIhZ9Kdz8j9';
35
+ const MAX_TEXT_UPLOAD_BYTES = 100 * 1024;
36
  const LOCAL_UI_DIR = [
37
  process.env.UI_LOCAL_PATH,
38
  path.resolve(__dirname, '..', '..', 'InferencePort-Pages'),
 
46
  }) || null;
47
 
48
  // Rate limiter for admin endpoints (5 attempts per IP per minute)
49
+ const verifyLimiter = rateLimit({
50
+ windowMs: 60 * 1000,
51
+ max: 5,
52
+ standardHeaders: true,
53
+ legacyHeaders: false,
54
+ handler: (req, res) => {
55
+ logAdminEvent(req, 'rate_limited');
56
+ res.status(429).json({ error: 'rate_limited' });
57
+ },
58
+ });
59
 
60
  const DATA_DIR = "/data";
61
  const VERSION_FILE = path.join(DATA_DIR, 'version.json');
 
68
  || 'unknown';
69
  }
70
 
71
+ function truncateForLog(value, max = 180) {
72
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
73
+ if (!text) return '';
74
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
75
+ }
76
+
77
+ function extensionFromName(name = '') {
78
+ const ext = path.extname(String(name || '')).toLowerCase();
79
+ return ext.startsWith('.') ? ext.slice(1) : ext;
80
+ }
81
+
82
+ function isTextLikeUpload(name = '', mimeType = '', kind = '') {
83
+ const normalizedKind = String(kind || '').toLowerCase();
84
+ const mime = String(mimeType || '').toLowerCase();
85
+ const ext = extensionFromName(name);
86
+ if (normalizedKind === 'text' || normalizedKind === 'rich_text') return true;
87
+ if (mime.startsWith('text/')) return true;
88
+ if (['application/json', 'application/javascript', 'application/xml'].includes(mime)) return true;
89
+ return ['txt', 'md', 'json', 'js', 'ts', 'css', 'py', 'html', 'htm', 'xml', 'csv', 'rtf'].includes(ext);
90
+ }
91
+
92
  function getCookieMap(req) {
93
  const cookies = (req.headers.cookie || '')
94
  .split(';')
 
109
 
110
  function requireAdminTurnstile(req, res, next) {
111
  if (hasAdminTurnstile(req)) return next();
112
+ logAdminEvent(req, 'blocked_missing_turnstile');
113
  return res.status(403).json({ error: 'turnstile:required' });
114
  }
115
 
116
+ function logAdminEvent(req, action, detail = null) {
117
+ const parts = [
118
+ `[ADMIN ${new Date().toISOString()}]`,
119
+ `action=${action}`,
120
+ `ip=${getRequestIp(req)}`,
121
+ `method=${req.method}`,
122
+ `path=${truncateForLog(req.originalUrl || req.url, 220)}`,
123
+ ];
124
+ const userAgent = truncateForLog(req.headers['user-agent'] || 'unknown', 140);
125
+ if (userAgent) parts.push(`ua="${userAgent}"`);
126
+ if (typeof detail === 'string' && detail.trim()) {
127
+ parts.push(detail.trim());
128
+ } else if (detail && typeof detail === 'object') {
129
+ Object.entries(detail).forEach(([key, value]) => {
130
+ if (value === undefined || value === null || value === '') return;
131
+ parts.push(`${key}=${JSON.stringify(String(value))}`);
132
+ });
133
+ }
134
+ console.log(parts.join(' | '));
135
+ }
136
+
137
+ function respondTextUploadTooLarge(res) {
138
+ return res.status(413).json({
139
+ error: 'media:text_too_large',
140
+ message: 'Text files must be 100 KB or smaller.',
141
+ });
142
  }
143
 
144
  async function verifyTurnstileToken(token, remoteIp) {
 
291
  const parentId = req.headers['x-parent-id'] ? String(req.headers['x-parent-id']) : null;
292
  const sessionId = req.headers['x-session-id'] ? String(req.headers['x-session-id']) : null;
293
  const kindHeader = req.headers['x-file-kind'] ? String(req.headers['x-file-kind']) : null;
294
+ const buffer = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || []);
295
+ if (isTextLikeUpload(name, mimeType, kindHeader) && buffer.byteLength > MAX_TEXT_UPLOAD_BYTES) {
296
+ return respondTextUploadTooLarge(res);
297
+ }
298
  const item = await mediaStore.storeBuffer(resolved.owner, {
299
  name,
300
  mimeType,
301
+ buffer,
302
  parentId,
303
  sessionId,
304
  source: 'user_upload',
 
336
  const resolved = await requireRequestOwner(req, res);
337
  if (!resolved) return;
338
  try {
339
+ const richText = !!req.body?.richText;
340
+ const content = String(req.body?.content || '');
341
+ if (Buffer.byteLength(content, 'utf8') > MAX_TEXT_UPLOAD_BYTES) {
342
+ return respondTextUploadTooLarge(res);
343
+ }
344
  const item = await mediaStore.createDocument(resolved.owner, {
345
  name: req.body?.name,
346
  parentId: req.body?.parentId || null,
347
+ richText,
348
+ content,
349
  source: 'user_upload',
350
  sessionId: req.body?.sessionId || null,
351
  });
 
430
  const resolved = await requireRequestOwner(req, res);
431
  if (!resolved) return;
432
  try {
433
+ const buffer = Buffer.from(String(req.body?.content || ''), 'utf8');
434
+ if (buffer.byteLength > MAX_TEXT_UPLOAD_BYTES) {
435
+ return respondTextUploadTooLarge(res);
436
+ }
437
  const item = await mediaStore.updateContent(resolved.owner, req.params.id, {
438
+ buffer,
439
  mimeType: req.body?.mimeType || null,
440
  kind: req.body?.richText ? 'rich_text' : null,
441
  name: req.body?.name || null,
 
506
 
507
  // --- Admin endpoints ---
508
  app.get('/admin.html', async (req, res) => {
509
+ logAdminEvent(req, 'page_view');
510
  if (serveLocalUiFile('/admin.html', res)) return;
511
  if (!latestSHA) return res.status(500).send('Server not ready');
512
 
 
526
  });
527
 
528
  app.get('/admin/config', (req, res) => {
529
+ logAdminEvent(req, 'config_view', { verified: hasAdminTurnstile(req) });
530
  res.json({
531
  siteKey: TURNSTILE_SITE_KEY || null,
532
  verified: hasAdminTurnstile(req),
 
537
  try {
538
  const token = req.body?.token;
539
  const result = await verifyTurnstileToken(token, getRequestIp(req));
540
+ if (!result?.success) {
541
+ logAdminEvent(req, 'turnstile_failed', {
542
+ errorCodes: Array.isArray(result?.['error-codes']) ? result['error-codes'].join(',') : '',
543
+ });
544
+ return res.status(403).json({ error: 'Verification failed' });
545
+ }
546
  res.cookie('admin_turnstile', '1', { maxAge: 2 * 3600 * 1000, path: '/', sameSite: 'lax' });
547
+ logAdminEvent(req, 'turnstile_verified');
548
  return res.json({ success: true });
549
  } catch (err) {
550
  console.error('admin turnstile verify', err);
551
+ logAdminEvent(req, 'turnstile_error', { message: err.message || 'unknown' });
552
  return res.status(500).json({ error: err.message || 'Server error' });
553
  }
554
  });
 
556
  app.get('/admin/verify', requireAdminTurnstile, verifyLimiter, (req,res)=>{
557
  const token = req.query.token;
558
  const success = token===ADMIN_TOKEN;
559
+ logAdminEvent(req, success ? 'login_success' : 'login_failed', {
560
+ turnstile: hasAdminTurnstile(req),
561
+ tokenProvided: !!token,
562
+ });
563
  res.json({success});
564
  });
565
 
566
  app.get('/admin/refresh', requireAdminTurnstile, verifyLimiter, async (req, res) => {
567
  const token = req.query.token;
568
+ if (token !== ADMIN_TOKEN) {
569
+ logAdminEvent(req, 'refresh_denied', { reason: 'bad_token' });
570
+ return res.status(403).send('Forbidden');
571
+ }
572
 
573
  const sha = req.query.sha?.trim();
574
  if (sha) {
575
+ if (!/^[0-9a-f]{7,40}$/.test(sha)) {
576
+ logAdminEvent(req, 'set_sha_invalid', { requestedSha: sha });
577
+ return res.status(400).send('Invalid SHA');
578
+ }
579
  latestSHA = sha;
580
  saveStoredSHA(latestSHA);
581
+ logAdminEvent(req, 'set_sha', { requestedSha: latestSHA });
582
  console.log(`[${PUBLIC_URL}] Manual SHA set by admin: ${latestSHA}`);
583
  return res.send(`Version set to commit ${latestSHA}`);
584
  }
585
 
586
  await fetchLatestSHA();
587
+ logAdminEvent(req, 'refresh_latest', { resolvedSha: latestSHA });
588
  res.send(`Latest version refreshed: ${latestSHA}`);
589
  });
590
 
591
  app.get('/admin/status', requireAdminTurnstile, verifyLimiter, async (req, res) => {
592
  const token = req.query.token;
593
+ if (token !== ADMIN_TOKEN) {
594
+ logAdminEvent(req, 'status_denied', { reason: 'bad_token' });
595
+ return res.status(403).json({ error: 'Forbidden' });
596
+ }
597
+ logAdminEvent(req, 'status_view', {
598
+ currentSha: latestSHA,
599
+ servingMode: LOCAL_UI_DIR ? 'local-ui' : 'cdn',
600
+ });
601
  res.json({
602
  publicUrl: PUBLIC_URL,
603
  currentSha: latestSHA,