most-box 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js CHANGED
@@ -3,20 +3,30 @@ import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import os from 'node:os'
5
5
  import { fileURLToPath } from 'node:url'
6
- import crypto from 'node:crypto'
7
- import { exec } from 'node:child_process'
6
+ import { spawn } from 'node:child_process'
7
+ import Busboy from 'busboy'
8
+ import { WebSocketServer } from 'ws'
8
9
  import { MostBoxEngine } from './src/index.js'
9
10
  import { parseMostLink } from './src/core/cid.js'
11
+ import { MAX_FILE_SIZE } from './src/config.js'
10
12
 
11
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
12
- const PORT = Number(process.env.MOSTBOX_PORT) || 1976
13
- const HOST = '127.0.0.1'
14
+ const PORT = Number(process.env.MOSTBOX_PORT || process.env.PORT) || 1976
15
+ const HOST = process.env.MOSTBOX_HOST || '127.0.0.1'
16
+
17
+ const MAX_JSON_BODY_SIZE = 10 * 1024 * 1024 // 10MB
18
+ const MAX_UPLOAD_SIZE = MAX_FILE_SIZE
19
+ const UPLOAD_TMP_DIR = path.join(os.tmpdir(), 'most-box-uploads')
20
+
21
+ const rateLimitMap = new Map()
22
+ const RATE_LIMIT_WINDOW = 60 * 1000
23
+ const RATE_LIMIT_MAX_REQUESTS = 120
14
24
 
15
- const wsClients = new Set()
16
25
  let engine = null
17
26
  let serverInstance = null
27
+ let wss = null
18
28
 
19
- // --- Config ---
29
+ // --- 配置 ---
20
30
  const CONFIG_DIR = path.join(os.homedir(), '.most-box')
21
31
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
22
32
 
@@ -36,7 +46,9 @@ function saveConfig(config) {
36
46
  if (!fs.existsSync(CONFIG_DIR)) {
37
47
  fs.mkdirSync(CONFIG_DIR, { recursive: true })
38
48
  }
39
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8')
49
+ const tmpPath = CONFIG_FILE + '.tmp'
50
+ fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8')
51
+ fs.renameSync(tmpPath, CONFIG_FILE)
40
52
  return true
41
53
  } catch (err) {
42
54
  console.error('[Config] Save error:', err.message)
@@ -44,13 +56,33 @@ function saveConfig(config) {
44
56
  }
45
57
  }
46
58
 
47
- // --- Storage path ---
59
+ // --- 存储路径 ---
48
60
  function getDataPath() {
49
61
  const config = loadConfig()
50
62
  return config.dataPath || path.join(os.homedir(), 'most-data')
51
63
  }
52
64
 
53
- // --- Static file serving ---
65
+ // --- 速率限制 ---
66
+ function checkRateLimit(clientIp) {
67
+ const now = Date.now()
68
+ if (!rateLimitMap.has(clientIp)) {
69
+ rateLimitMap.set(clientIp, [])
70
+ }
71
+ const requests = rateLimitMap.get(clientIp)
72
+ while (requests.length > 0 && requests[0] < now - RATE_LIMIT_WINDOW) {
73
+ requests.shift()
74
+ }
75
+ if (requests.length === 0) {
76
+ rateLimitMap.delete(clientIp)
77
+ }
78
+ if (requests.length >= RATE_LIMIT_MAX_REQUESTS) {
79
+ return false
80
+ }
81
+ requests.push(now)
82
+ return true
83
+ }
84
+
85
+ // --- 静态文件服务 ---
54
86
  const MIME_TYPES = {
55
87
  '.html': 'text/html; charset=utf-8',
56
88
  '.js': 'application/javascript; charset=utf-8',
@@ -76,6 +108,11 @@ const MIME_TYPES = {
76
108
  '.woff': 'font/woff'
77
109
  }
78
110
 
111
+ function getMimeType(fileName) {
112
+ const ext = path.extname(fileName).toLowerCase()
113
+ return MIME_TYPES[ext] || 'application/octet-stream'
114
+ }
115
+
79
116
  function serveStatic(req, res) {
80
117
  let filePath = req.url === '/' ? '/index.html' : req.url
81
118
  filePath = filePath.split('?')[0]
@@ -97,123 +134,139 @@ function serveStatic(req, res) {
97
134
  return
98
135
  }
99
136
 
100
- let content = data
101
- if (ext === '.html') {
102
- content = data.toString()
103
- }
104
-
105
137
  res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' })
106
- res.end(content)
138
+ res.end(data)
107
139
  })
108
140
  }
109
141
 
110
- // --- Streaming multipart parser for large files ---
111
- async function parseMultipart(req) {
112
- const boundaryMatch = req.headers['content-type']?.match(/boundary=(?:"([^"]+)"|([^\s;]+))/)
113
- if (!boundaryMatch) throw new Error('No boundary in content-type')
114
- const boundary = boundaryMatch[1] || boundaryMatch[2]
142
+ function decodeFilenameFromHeader(headerStr) {
143
+ if (!headerStr) return null
115
144
 
116
- const chunks = []
117
- for await (const chunk of req) {
118
- chunks.push(chunk)
145
+ const filenameStarMatch = headerStr.match(/filename\*=(?:UTF-8''|utf-8'')([^;\r\n]+)/i)
146
+ if (filenameStarMatch) {
147
+ return decodeURIComponent(filenameStarMatch[1])
119
148
  }
120
- const buffer = Buffer.concat(chunks)
121
-
122
- const parts = []
123
- const boundaryBuf = Buffer.from('--' + boundary)
124
- let start = 0
125
-
126
- while (true) {
127
- const idx = buffer.indexOf(boundaryBuf, start)
128
- if (idx === -1) break
129
-
130
- if (start > 0) {
131
- // Handle both \r\n and \n line endings
132
- let partStart = start
133
- if (buffer[partStart] === 0x0d && buffer[partStart + 1] === 0x0a) {
134
- partStart += 2
135
- } else if (buffer[partStart] === 0x0a) {
136
- partStart += 1
149
+
150
+ const filenameMatch = headerStr.match(/filename="([^"]+)"/)
151
+ if (filenameMatch) {
152
+ const rawFilename = filenameMatch[1]
153
+ try {
154
+ const buf = Buffer.from(rawFilename, 'latin1')
155
+ const decoded = buf.toString('utf8')
156
+ if (decoded.includes('\ufffd')) {
157
+ return rawFilename
137
158
  }
159
+ return decoded
160
+ } catch {
161
+ return rawFilename
162
+ }
163
+ }
138
164
 
139
- let partEnd = idx - 1
140
- if (buffer[partEnd] === 0x0a) {
141
- partEnd--
142
- if (buffer[partEnd] === 0x0d) {
143
- partEnd--
144
- }
165
+ const filenamePlainMatch = headerStr.match(/filename=([^;\r\n]+)/)
166
+ if (filenamePlainMatch) {
167
+ return filenamePlainMatch[1].trim()
168
+ }
169
+ return null
170
+ }
171
+
172
+ function parseMultipartBusboy(req) {
173
+ return new Promise((resolve, reject) => {
174
+ if (!fs.existsSync(UPLOAD_TMP_DIR)) {
175
+ fs.mkdirSync(UPLOAD_TMP_DIR, { recursive: true })
176
+ }
177
+
178
+ const busboy = Busboy({
179
+ headers: req.headers,
180
+ limits: {
181
+ fileSize: MAX_UPLOAD_SIZE,
182
+ files: 1,
183
+ fields: 0
145
184
  }
185
+ })
186
+
187
+ const result = { filePath: null, filename: null }
188
+ let fileSize = 0
189
+ let writeStream = null
190
+ let tempPath = null
191
+
192
+ busboy.on('file', (name, stream, info) => {
193
+ result.filename = decodeFilenameFromHeader(`filename="${info.filename}"`)
194
+ tempPath = path.join(UPLOAD_TMP_DIR, `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`)
195
+ writeStream = fs.createWriteStream(tempPath)
196
+
197
+ stream.on('data', (chunk) => {
198
+ fileSize += chunk.length
199
+ if (fileSize > MAX_UPLOAD_SIZE) {
200
+ stream.destroy()
201
+ writeStream.destroy()
202
+ fs.unlink(tempPath, () => {})
203
+ reject(new Error('File too large'))
204
+ return
205
+ }
206
+ })
146
207
 
147
- const partData = buffer.slice(partStart, partEnd + 1)
208
+ stream.on('error', () => {
209
+ if (tempPath) fs.unlink(tempPath, () => {})
210
+ })
148
211
 
149
- const headerEnd = partData.indexOf('\r\n\r\n')
150
- const headerEndAlt = partData.indexOf('\n\n')
212
+ stream.pipe(writeStream)
151
213
 
152
- let headerEndIdx = -1
153
- let bodyStart = -1
214
+ writeStream.on('finish', () => {
215
+ result.filePath = tempPath
216
+ resolve(result)
217
+ })
154
218
 
155
- if (headerEnd !== -1) {
156
- headerEndIdx = headerEnd
157
- bodyStart = headerEnd + 4
158
- } else if (headerEndAlt !== -1) {
159
- headerEndIdx = headerEndAlt
160
- bodyStart = headerEndAlt + 2
161
- }
219
+ writeStream.on('error', (err) => {
220
+ if (tempPath) fs.unlink(tempPath, () => {})
221
+ reject(err)
222
+ })
223
+ })
162
224
 
163
- if (headerEndIdx !== -1) {
164
- const headers = partData.slice(0, headerEndIdx).toString()
165
- const body = partData.slice(bodyStart)
166
-
167
- const nameMatch = headers.match(/name="([^"]+)"/)
168
- const filenameMatch = headers.match(/filename="([^"]+)"/)
169
- parts.push({
170
- name: nameMatch?.[1],
171
- filename: filenameMatch?.[1],
172
- data: body,
173
- headers
174
- })
175
- }
176
- }
225
+ busboy.on('error', (err) => {
226
+ if (tempPath) fs.unlink(tempPath, () => {})
227
+ reject(err)
228
+ })
177
229
 
178
- // Move to after the boundary
179
- start = idx + boundaryBuf.length
180
- // Skip optional whitespace after boundary
181
- while (start < buffer.length && (buffer[start] === 0x20 || buffer[start] === 0x09)) {
182
- start++
183
- }
184
- // Skip line ending
185
- if (start < buffer.length && buffer[start] === 0x0d) {
186
- start++
187
- }
188
- if (start < buffer.length && buffer[start] === 0x0a) {
189
- start++
190
- }
191
- }
230
+ busboy.on('close', () => {
231
+ if (!result.filename) {
232
+ resolve(null)
233
+ }
234
+ })
192
235
 
193
- return parts
236
+ req.on('error', (err) => {
237
+ if (tempPath) fs.unlink(tempPath, () => {})
238
+ reject(err)
239
+ })
240
+ req.pipe(busboy)
241
+ })
194
242
  }
195
243
 
196
- // --- JSON body parser ---
244
+ // --- JSON 请求体解析器(带大小限制) ---
197
245
  async function parseJSON(req) {
198
246
  const chunks = []
247
+ let totalSize = 0
199
248
  for await (const chunk of req) {
249
+ totalSize += chunk.length
250
+ if (totalSize > MAX_JSON_BODY_SIZE) {
251
+ throw new Error('Request body too large')
252
+ }
200
253
  chunks.push(chunk)
201
254
  }
202
- try {
203
- return JSON.parse(Buffer.concat(chunks).toString())
204
- } catch {
205
- return {}
255
+ const text = Buffer.concat(chunks).toString()
256
+ if (!text.trim()) {
257
+ throw new Error('Empty request body')
206
258
  }
259
+ return JSON.parse(text)
207
260
  }
208
261
 
209
- // --- API Routes ---
262
+ // --- API 路由 ---
210
263
  async function handleAPI(req, res) {
211
264
  const url = new URL(req.url, `http://${HOST}:${PORT}`)
212
265
  const pathname = url.pathname
213
266
  const method = req.method
214
267
 
215
268
  const json = (data, status = 200) => {
216
- res.writeHead(status, { 'Content-Type': 'application/json' })
269
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' })
217
270
  res.end(JSON.stringify(data))
218
271
  }
219
272
 
@@ -231,34 +284,34 @@ async function handleAPI(req, res) {
231
284
  return
232
285
  }
233
286
 
234
- // POST /api/config
287
+ // POST /api/config — 更新配置
235
288
  if (pathname === '/api/config' && method === 'POST') {
236
289
  const body = await parseJSON(req)
237
290
  const config = loadConfig()
238
-
291
+
239
292
  if (body.resetStorage) {
240
293
  config.dataPath = ''
241
294
  } else if (body.dataPath !== undefined) {
242
295
  let dataPath = body.dataPath.trim()
243
296
  let basePath = dataPath
244
-
297
+
245
298
  if (dataPath.match(/^[A-Za-z]:\\$/)) {
246
299
  basePath = dataPath
247
300
  dataPath = path.join(dataPath, 'most-data')
248
301
  }
249
-
302
+
250
303
  if (!fs.existsSync(basePath)) {
251
304
  json({ error: '目录不存在' }, 400)
252
305
  return
253
306
  }
254
-
307
+
255
308
  if (!fs.existsSync(dataPath)) {
256
309
  fs.mkdirSync(dataPath, { recursive: true })
257
310
  }
258
-
311
+
259
312
  config.dataPath = dataPath
260
313
  }
261
-
314
+
262
315
  const success = saveConfig(config)
263
316
  json({ success, dataPath: getDataPath() })
264
317
  return
@@ -285,23 +338,25 @@ async function handleAPI(req, res) {
285
338
  return
286
339
  }
287
340
 
288
- // POST /api/publish — multipart file upload
341
+ // POST /api/publish — multipart 文件上传
289
342
  if (pathname === '/api/publish' && method === 'POST') {
290
- const parts = await parseMultipart(req)
343
+ const result = await parseMultipartBusboy(req)
291
344
 
292
- const filePart = parts.find(p => p.name === 'file')
293
- if (!filePart || !filePart.filename) {
345
+ if (!result || !result.filename) {
294
346
  json({ error: 'No file provided' }, 400)
295
347
  return
296
348
  }
297
349
 
298
- const result = await engine.publishFile(filePart.data, filePart.filename)
299
-
300
- json({ success: true, ...result })
350
+ try {
351
+ const publishResult = await engine.publishFile(result.filePath, result.filename)
352
+ json({ success: true, ...publishResult })
353
+ } finally {
354
+ fs.unlink(result.filePath, () => {})
355
+ }
301
356
  return
302
357
  }
303
358
 
304
- // POST /api/download — start async download from P2P
359
+ // POST /api/download — P2P 开始异步下载
305
360
  if (pathname === '/api/download' && method === 'POST') {
306
361
  const body = await parseJSON(req)
307
362
  if (!body.link) {
@@ -311,14 +366,12 @@ async function handleAPI(req, res) {
311
366
 
312
367
  const taskId = `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
313
368
 
314
- // Parse link to check if file already exists
315
369
  const parsed = parseMostLink(body.link)
316
370
  if (parsed.error) {
317
371
  json({ error: parsed.error }, 400)
318
372
  return
319
373
  }
320
374
 
321
- // Check if file already exists in published files
322
375
  const existingFile = engine.getPublishedFiles().find(f => f.cid === parsed.cid)
323
376
  if (existingFile) {
324
377
  console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
@@ -326,7 +379,6 @@ async function handleAPI(req, res) {
326
379
  return
327
380
  }
328
381
 
329
- // Async download — do not block HTTP response
330
382
  engine.downloadFile(body.link, taskId).catch(err => {
331
383
  if (err.message === 'Download cancelled') {
332
384
  wsBroadcast('download:cancelled', { taskId })
@@ -339,7 +391,7 @@ async function handleAPI(req, res) {
339
391
  return
340
392
  }
341
393
 
342
- // POST /api/download/cancel — cancel an active download
394
+ // POST /api/download/cancel — 取消活动下载
343
395
  if (pathname === '/api/download/cancel' && method === 'POST') {
344
396
  const body = await parseJSON(req)
345
397
  if (!body.taskId) {
@@ -359,7 +411,7 @@ async function handleAPI(req, res) {
359
411
  return
360
412
  }
361
413
 
362
- // POST /api/move — rename/move a published file (changes path without re-uploading)
414
+ // POST /api/move — 重命名/移动已发布文件
363
415
  if (pathname === '/api/move' && method === 'POST') {
364
416
  const body = await parseJSON(req)
365
417
  if (!body.cid || !body.newFileName) {
@@ -367,7 +419,7 @@ async function handleAPI(req, res) {
367
419
  return
368
420
  }
369
421
  try {
370
- const result = await engine.moveFile(body.cid, body.newFileName)
422
+ const result = engine.moveFile(body.cid, body.newFileName)
371
423
  json({ success: true, ...result })
372
424
  } catch (err) {
373
425
  json({ error: err.message }, 400)
@@ -375,7 +427,7 @@ async function handleAPI(req, res) {
375
427
  return
376
428
  }
377
429
 
378
- // POST /api/folder/rename — rename a folder (renames all files within)
430
+ // POST /api/folder/rename — 重命名文件夹
379
431
  if (pathname === '/api/folder/rename' && method === 'POST') {
380
432
  const body = await parseJSON(req)
381
433
  if (!body.oldPath || !body.newPath) {
@@ -391,14 +443,62 @@ async function handleAPI(req, res) {
391
443
  return
392
444
  }
393
445
 
394
- // GET /api/files/:cid/download — serve file inline for preview / download
446
+ // GET /api/files/:cid/download — 内联服务文件,支持 Range
395
447
  if (pathname.match(/^\/api\/files\/[^/]+\/download$/) && method === 'GET') {
396
- json({ error: 'Use P2P network to download this file' }, 400)
448
+ const cid = pathname.split('/')[3]
449
+ const rangeHeader = req.headers['range']
450
+
451
+ try {
452
+ if (rangeHeader) {
453
+ const rangeMatch = rangeHeader.match(/bytes=(\d+)-(\d*)/)
454
+ if (rangeMatch) {
455
+ const start = parseInt(rangeMatch[1], 10)
456
+ const end = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : undefined
457
+
458
+ const offset = start
459
+ const limit = end !== undefined ? end - start + 1 : undefined
460
+
461
+ const result = await engine.readFileRaw(cid, { offset, limit })
462
+ const contentType = getMimeType(result.fileName)
463
+
464
+ res.writeHead(206, {
465
+ 'Content-Type': contentType,
466
+ 'Content-Length': result.buffer.length,
467
+ 'Content-Range': `bytes ${offset}-${offset + result.buffer.length - 1}/${result.totalSize}`,
468
+ 'Accept-Ranges': 'bytes'
469
+ })
470
+ res.end(result.buffer)
471
+ return
472
+ }
473
+ }
474
+
475
+ const result = await engine.readFileRaw(cid)
476
+ const contentType = getMimeType(result.fileName)
477
+ res.writeHead(200, {
478
+ 'Content-Type': contentType,
479
+ 'Content-Length': result.totalSize,
480
+ 'Accept-Ranges': 'bytes',
481
+ 'Content-Disposition': `inline; filename="${encodeURIComponent(result.fileName)}"`
482
+ })
483
+ res.end(result.buffer)
484
+ } catch (err) {
485
+ if (err.message === 'File not found') {
486
+ json({ error: err.message }, 404)
487
+ } else {
488
+ json({ error: err.message }, 400)
489
+ }
490
+ }
397
491
  return
398
492
  }
399
493
 
400
- // POST /api/shutdown — graceful server shutdown
494
+ // POST /api/shutdown — 优雅关闭服务器(仅允许 localhost 连接)
401
495
  if (pathname === '/api/shutdown' && method === 'POST') {
496
+ const clientIp = req.socket.remoteAddress
497
+ const isLocalhost = clientIp === '127.0.0.1' || clientIp === '::1' || clientIp === '::ffff:127.0.0.1'
498
+ if (!isLocalhost) {
499
+ json({ error: 'Forbidden' }, 403)
500
+ return
501
+ }
402
502
  json({ success: true })
403
503
  console.log('[MostBox] Shutdown requested via API...')
404
504
  setTimeout(async () => {
@@ -410,13 +510,13 @@ async function handleAPI(req, res) {
410
510
  return
411
511
  }
412
512
 
413
- // GET /api/trash — list trash files
513
+ // GET /api/trash — 列出回收站文件
414
514
  if (pathname === '/api/trash' && method === 'GET') {
415
515
  json(engine.listTrashFiles())
416
516
  return
417
517
  }
418
518
 
419
- // POST /api/trash/:cid/restore — restore file from trash
519
+ // POST /api/trash/:cid/restore — 从回收站恢复文件
420
520
  if (pathname.match(/^\/api\/trash\/[^/]+\/restore$/) && method === 'POST') {
421
521
  const cid = pathname.split('/')[3]
422
522
  try {
@@ -428,7 +528,7 @@ async function handleAPI(req, res) {
428
528
  return
429
529
  }
430
530
 
431
- // DELETE /api/trash/:cid — permanently delete a trash file
531
+ // DELETE /api/trash/:cid — 永久删除回收站文件
432
532
  if (pathname.match(/^\/api\/trash\/[^/]+$/) && method === 'DELETE') {
433
533
  const cid = pathname.split('/')[3]
434
534
  const result = await engine.permanentDeleteTrashFile(cid)
@@ -436,14 +536,14 @@ async function handleAPI(req, res) {
436
536
  return
437
537
  }
438
538
 
439
- // DELETE /api/trash — empty trash
539
+ // DELETE /api/trash — 清空回收站
440
540
  if (pathname === '/api/trash' && method === 'DELETE') {
441
541
  const result = await engine.emptyTrash()
442
542
  json({ success: true, trashFiles: result })
443
543
  return
444
544
  }
445
545
 
446
- // POST /api/files/:cid/star — toggle starred status
546
+ // POST /api/files/:cid/star — 切换星标状态
447
547
  if (pathname.match(/^\/api\/files\/[^/]+\/star$/) && method === 'POST') {
448
548
  const cid = pathname.split('/')[3]
449
549
  try {
@@ -455,7 +555,7 @@ async function handleAPI(req, res) {
455
555
  return
456
556
  }
457
557
 
458
- // GET /api/storage — get storage statistics
558
+ // GET /api/storage — 获取存储统计信息
459
559
  if (pathname === '/api/storage' && method === 'GET') {
460
560
  const result = await engine.getStorageStats()
461
561
  json(result)
@@ -469,79 +569,29 @@ async function handleAPI(req, res) {
469
569
  }
470
570
  }
471
571
 
472
- // --- Minimal WebSocket (RFC 6455) ---
473
- function upgradeToWebSocket(req, socket) {
474
- const key = req.headers['sec-websocket-key']
475
- if (!key) { socket.destroy(); return }
476
-
477
- const MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
478
- const accept = crypto.createHash('sha1')
479
- .update(key + MAGIC)
480
- .digest('base64')
481
-
482
- socket.write(
483
- 'HTTP/1.1 101 Switching Protocols\r\n' +
484
- 'Upgrade: websocket\r\n' +
485
- 'Connection: Upgrade\r\n' +
486
- `Sec-WebSocket-Accept: ${accept}\r\n` +
487
- '\r\n'
488
- )
489
-
490
- wsClients.add(socket)
491
- socket.on('close', () => wsClients.delete(socket))
492
- socket.on('error', () => wsClients.delete(socket))
493
-
494
- socket.on('data', (buf) => {
495
- if (buf.length < 2) return
496
- const opcode = buf[0] & 0x0f
497
- if (opcode === 0x8) {
498
- wsClients.delete(socket)
499
- socket.end()
500
- }
501
- if (opcode === 0x9) {
502
- const pong = Buffer.from(buf)
503
- pong[0] = (pong[0] & 0xf0) | 0xa
504
- socket.write(pong)
505
- }
506
- if (opcode === 0x1 || opcode === 0x2) {
507
- // Text or binary message - could broadcast to other clients if needed
508
- }
509
- })
510
- }
511
-
512
572
  function wsBroadcast(event, data) {
513
573
  const payload = JSON.stringify({ event, data })
514
- const buf = Buffer.from(payload)
515
-
516
- let frame
517
- if (buf.length < 126) {
518
- frame = Buffer.alloc(2 + buf.length)
519
- frame[0] = 0x81
520
- frame[1] = buf.length
521
- buf.copy(frame, 2)
522
- } else if (buf.length < 65536) {
523
- frame = Buffer.alloc(4 + buf.length)
524
- frame[0] = 0x81
525
- frame[1] = 126
526
- frame.writeUInt16BE(buf.length, 2)
527
- buf.copy(frame, 4)
528
- } else {
529
- frame = Buffer.alloc(10 + buf.length)
530
- frame[0] = 0x81
531
- frame[1] = 127
532
- frame.writeBigUInt64BE(BigInt(buf.length), 2)
533
- buf.copy(frame, 10)
534
- }
535
-
536
- for (const client of wsClients) {
537
- try { client.write(frame) } catch {}
574
+ if (wss) {
575
+ wss.clients.forEach((client) => {
576
+ if (client.readyState === 1) {
577
+ try { client.send(payload) } catch {}
578
+ }
579
+ })
538
580
  }
539
581
  }
540
582
 
541
- // --- Main ---
583
+ // --- 主函数 ---
542
584
  async function main() {
543
585
  console.log('[MostBox] Starting core daemon...')
544
586
 
587
+ if (fs.existsSync(UPLOAD_TMP_DIR)) {
588
+ const staleFiles = fs.readdirSync(UPLOAD_TMP_DIR)
589
+ for (const file of staleFiles) {
590
+ try { fs.unlinkSync(path.join(UPLOAD_TMP_DIR, file)) } catch {}
591
+ }
592
+ console.log(`[MostBox] Cleaned ${staleFiles.length} stale upload temp files`)
593
+ }
594
+
545
595
  const dataPath = getDataPath()
546
596
  console.log(`[MostBox] Storage: ${dataPath}`)
547
597
 
@@ -561,7 +611,8 @@ async function main() {
561
611
  console.log('[MostBox] Engine ready')
562
612
 
563
613
  serverInstance = http.createServer((req, res) => {
564
- res.setHeader('Access-Control-Allow-Origin', '*')
614
+ const allowedOrigin = `http://${HOST}:${PORT}`
615
+ res.setHeader('Access-Control-Allow-Origin', allowedOrigin)
565
616
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
566
617
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
567
618
 
@@ -571,16 +622,37 @@ async function main() {
571
622
  return
572
623
  }
573
624
 
625
+ const clientIp = req.socket.remoteAddress || 'unknown'
626
+
627
+ if (!checkRateLimit(clientIp)) {
628
+ res.writeHead(429, { 'Content-Type': 'application/json' })
629
+ res.end(JSON.stringify({ error: 'Too many requests' }))
630
+ return
631
+ }
632
+
574
633
  if (req.url.startsWith('/api/')) {
575
- handleAPI(req, res)
634
+ handleAPI(req, res).catch(err => {
635
+ console.error('[Unhandled API Error]', err)
636
+ if (!res.headersSent) {
637
+ res.writeHead(500, { 'Content-Type': 'application/json' })
638
+ res.end(JSON.stringify({ error: 'Internal server error' }))
639
+ }
640
+ })
576
641
  } else {
577
642
  serveStatic(req, res)
578
643
  }
579
644
  })
580
645
 
581
- serverInstance.on('upgrade', (req, socket) => {
646
+ wss = new WebSocketServer({ noServer: true })
647
+ wss.on('connection', (ws) => {
648
+ ws.on('error', () => {})
649
+ })
650
+
651
+ serverInstance.on('upgrade', (req, socket, head) => {
582
652
  if (req.url.startsWith('/ws')) {
583
- upgradeToWebSocket(req, socket)
653
+ wss.handleUpgrade(req, socket, head, (ws) => {
654
+ wss.emit('connection', ws, req)
655
+ })
584
656
  } else {
585
657
  socket.destroy()
586
658
  }
@@ -590,20 +662,31 @@ async function main() {
590
662
  const url = `http://${HOST}:${PORT}`
591
663
  console.log(`[MostBox] Server running at ${url}`)
592
664
 
593
- const cmd = process.platform === 'win32' ? 'start ""'
594
- : process.platform === 'darwin' ? 'open' : 'xdg-open'
595
- exec(`${cmd} "${url}"`)
665
+ if (process.platform === 'win32') {
666
+ spawn('cmd.exe', ['/c', 'start', '', url], {
667
+ detached: true,
668
+ stdio: 'ignore'
669
+ }).unref()
670
+ } else {
671
+ const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open'
672
+ spawn(cmd, [url], {
673
+ detached: true,
674
+ stdio: 'ignore'
675
+ }).unref()
676
+ }
596
677
  })
597
678
 
598
679
  process.on('SIGINT', async () => {
599
680
  console.log('\n[MostBox] Shutting down...')
600
681
  await engine.stop()
682
+ if (wss) wss.close()
601
683
  serverInstance.close()
602
684
  process.exit(0)
603
685
  })
604
686
 
605
687
  process.on('SIGTERM', async () => {
606
688
  await engine.stop()
689
+ if (wss) wss.close()
607
690
  serverInstance.close()
608
691
  process.exit(0)
609
692
  })