most-box 0.0.1 → 0.0.4

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.
Files changed (71) hide show
  1. package/README.md +182 -73
  2. package/out/404/index.html +15 -0
  3. package/out/404.html +15 -0
  4. package/out/__next.__PAGE__.txt +9 -0
  5. package/out/__next._full.txt +18 -0
  6. package/out/__next._head.txt +5 -0
  7. package/out/__next._index.txt +6 -0
  8. package/out/__next._tree.txt +2 -0
  9. package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_buildManifest.js +11 -0
  10. package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_clientMiddlewareManifest.js +1 -0
  11. package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_ssgManifest.js +1 -0
  12. package/out/_next/static/chunks/00l-yd3t8dvwz.js +5 -0
  13. package/out/_next/static/chunks/03k8t3tgym~8~.js +1 -0
  14. package/out/_next/static/chunks/03~yq9q893hmn.js +1 -0
  15. package/out/_next/static/chunks/09vfh8lfuacc0.css +1 -0
  16. package/out/_next/static/chunks/0bogtdbh.dcu1.js +1 -0
  17. package/out/_next/static/chunks/0dbhjjzl8qfwv.js +1 -0
  18. package/out/_next/static/chunks/0f73psqhr8dre.css +1 -0
  19. package/out/_next/static/chunks/0fbi7z4_.4j1j.js +1 -0
  20. package/out/_next/static/chunks/0ht900cau6_ur.js +31 -0
  21. package/out/_next/static/chunks/0ohm.ia-4ec60.js +1 -0
  22. package/out/_next/static/chunks/0u5ydb-f0.vxl.js +1 -0
  23. package/out/_next/static/chunks/14t2m1on-s5v~.js +1 -0
  24. package/out/_next/static/chunks/turbopack-076ce9exut_h3.js +1 -0
  25. package/out/_not-found/__next._full.txt +16 -0
  26. package/out/_not-found/__next._head.txt +5 -0
  27. package/out/_not-found/__next._index.txt +6 -0
  28. package/out/_not-found/__next._not-found/__PAGE__.txt +5 -0
  29. package/out/_not-found/__next._not-found.txt +5 -0
  30. package/out/_not-found/__next._tree.txt +2 -0
  31. package/out/_not-found/index.html +15 -0
  32. package/out/_not-found/index.txt +16 -0
  33. package/out/app.css +1535 -0
  34. package/out/bundle.js +107 -0
  35. package/out/bundle.js.map +7 -0
  36. package/out/chat/__next._full.txt +19 -0
  37. package/out/chat/__next._head.txt +5 -0
  38. package/out/chat/__next._index.txt +6 -0
  39. package/out/chat/__next._tree.txt +3 -0
  40. package/out/chat/__next.chat/__PAGE__.txt +9 -0
  41. package/out/chat/__next.chat.txt +5 -0
  42. package/out/chat/index.html +15 -0
  43. package/out/chat/index.txt +19 -0
  44. package/out/chat-page.js +112 -0
  45. package/out/chat.css +378 -0
  46. package/out/favicon.ico +0 -0
  47. package/out/index.html +15 -0
  48. package/out/index.js +148 -0
  49. package/out/index.txt +18 -0
  50. package/package.json +16 -7
  51. package/public/app.css +1535 -0
  52. package/public/bundle.js +10 -14
  53. package/public/bundle.js.map +4 -4
  54. package/public/chat-page.js +112 -0
  55. package/public/chat.css +378 -0
  56. package/public/index.js +148 -0
  57. package/server.js +464 -199
  58. package/src/config.js +36 -8
  59. package/src/core/cid.js +28 -19
  60. package/src/index.js +872 -276
  61. package/src/utils/api.js +6 -0
  62. package/src/utils/security.js +27 -24
  63. package/build.mjs +0 -40
  64. package/public/app.jsx +0 -1335
  65. package/public/icons/apple-touch-icon.png +0 -0
  66. package/public/icons/mask-icon.svg +0 -3
  67. package/public/icons/most.png +0 -0
  68. package/public/icons/pwa-192x192.png +0 -0
  69. package/public/icons/pwa-512x512.png +0 -0
  70. package/public/index.html +0 -15
  71. package/public/index.jsx +0 -5
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,13 +108,18 @@ 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) {
117
+ const publicDir = path.join(__dirname, 'out')
80
118
  let filePath = req.url === '/' ? '/index.html' : req.url
81
119
  filePath = filePath.split('?')[0]
82
120
 
83
- const fullPath = path.join(__dirname, 'public', filePath)
121
+ const fullPath = path.join(publicDir, filePath)
84
122
  const ext = path.extname(fullPath)
85
- const publicDir = path.join(__dirname, 'public')
86
123
 
87
124
  if (!fullPath.startsWith(publicDir)) {
88
125
  res.writeHead(403)
@@ -92,128 +129,166 @@ function serveStatic(req, res) {
92
129
 
93
130
  fs.readFile(fullPath, (err, data) => {
94
131
  if (err) {
95
- res.writeHead(404)
96
- res.end('Not found')
132
+ if (err.code === 'EISDIR') {
133
+ const indexPath = path.join(fullPath, 'index.html')
134
+ fs.readFile(indexPath, (err2, indexData) => {
135
+ if (err2) {
136
+ res.writeHead(404)
137
+ res.end('Not found')
138
+ return
139
+ }
140
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
141
+ res.end(indexData)
142
+ })
143
+ } else {
144
+ const indexPath = path.join(publicDir, 'index.html')
145
+ fs.readFile(indexPath, (err2, indexData) => {
146
+ if (err2) {
147
+ res.writeHead(404)
148
+ res.end('Not found')
149
+ return
150
+ }
151
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
152
+ res.end(indexData)
153
+ })
154
+ }
97
155
  return
98
156
  }
99
157
 
100
- let content = data
101
- if (ext === '.html') {
102
- content = data.toString()
103
- }
104
-
158
+ const ext = path.extname(fullPath)
105
159
  res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' })
106
- res.end(content)
160
+ res.end(data)
107
161
  })
108
162
  }
109
163
 
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]
164
+ function decodeFilenameFromHeader(headerStr) {
165
+ if (!headerStr) return null
115
166
 
116
- const chunks = []
117
- for await (const chunk of req) {
118
- chunks.push(chunk)
167
+ const filenameStarMatch = headerStr.match(/filename\*=(?:UTF-8''|utf-8'')([^;\r\n]+)/i)
168
+ if (filenameStarMatch) {
169
+ return decodeURIComponent(filenameStarMatch[1])
119
170
  }
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
171
+
172
+ const filenameMatch = headerStr.match(/filename="([^"]+)"/)
173
+ if (filenameMatch) {
174
+ const rawFilename = filenameMatch[1]
175
+ try {
176
+ const buf = Buffer.from(rawFilename, 'latin1')
177
+ const decoded = buf.toString('utf8')
178
+ if (decoded.includes('\ufffd')) {
179
+ return rawFilename
137
180
  }
181
+ return decoded
182
+ } catch {
183
+ return rawFilename
184
+ }
185
+ }
138
186
 
139
- let partEnd = idx - 1
140
- if (buffer[partEnd] === 0x0a) {
141
- partEnd--
142
- if (buffer[partEnd] === 0x0d) {
143
- partEnd--
144
- }
187
+ const filenamePlainMatch = headerStr.match(/filename=([^;\r\n]+)/)
188
+ if (filenamePlainMatch) {
189
+ return filenamePlainMatch[1].trim()
190
+ }
191
+ return null
192
+ }
193
+
194
+ function parseMultipartBusboy(req) {
195
+ return new Promise((resolve, reject) => {
196
+ if (!fs.existsSync(UPLOAD_TMP_DIR)) {
197
+ fs.mkdirSync(UPLOAD_TMP_DIR, { recursive: true })
198
+ }
199
+
200
+ const busboy = Busboy({
201
+ headers: req.headers,
202
+ limits: {
203
+ fileSize: MAX_UPLOAD_SIZE,
204
+ files: 1,
205
+ fields: 0
145
206
  }
207
+ })
208
+
209
+ const result = { filePath: null, filename: null }
210
+ let fileSize = 0
211
+ let writeStream = null
212
+ let tempPath = null
213
+
214
+ busboy.on('file', (name, stream, info) => {
215
+ result.filename = decodeFilenameFromHeader(`filename="${info.filename}"`)
216
+ tempPath = path.join(UPLOAD_TMP_DIR, `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`)
217
+ writeStream = fs.createWriteStream(tempPath)
218
+
219
+ stream.on('data', (chunk) => {
220
+ fileSize += chunk.length
221
+ if (fileSize > MAX_UPLOAD_SIZE) {
222
+ stream.destroy()
223
+ writeStream.destroy()
224
+ fs.unlink(tempPath, () => {})
225
+ reject(new Error('File too large'))
226
+ return
227
+ }
228
+ })
146
229
 
147
- const partData = buffer.slice(partStart, partEnd + 1)
230
+ stream.on('error', () => {
231
+ if (tempPath) fs.unlink(tempPath, () => {})
232
+ })
148
233
 
149
- const headerEnd = partData.indexOf('\r\n\r\n')
150
- const headerEndAlt = partData.indexOf('\n\n')
234
+ stream.pipe(writeStream)
151
235
 
152
- let headerEndIdx = -1
153
- let bodyStart = -1
236
+ writeStream.on('finish', () => {
237
+ result.filePath = tempPath
238
+ resolve(result)
239
+ })
154
240
 
155
- if (headerEnd !== -1) {
156
- headerEndIdx = headerEnd
157
- bodyStart = headerEnd + 4
158
- } else if (headerEndAlt !== -1) {
159
- headerEndIdx = headerEndAlt
160
- bodyStart = headerEndAlt + 2
161
- }
241
+ writeStream.on('error', (err) => {
242
+ if (tempPath) fs.unlink(tempPath, () => {})
243
+ reject(err)
244
+ })
245
+ })
162
246
 
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
- }
247
+ busboy.on('error', (err) => {
248
+ if (tempPath) fs.unlink(tempPath, () => {})
249
+ reject(err)
250
+ })
177
251
 
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
- }
252
+ busboy.on('close', () => {
253
+ if (!result.filename) {
254
+ resolve(null)
255
+ }
256
+ })
192
257
 
193
- return parts
258
+ req.on('error', (err) => {
259
+ if (tempPath) fs.unlink(tempPath, () => {})
260
+ reject(err)
261
+ })
262
+ req.pipe(busboy)
263
+ })
194
264
  }
195
265
 
196
- // --- JSON body parser ---
266
+ // --- JSON 请求体解析器(带大小限制) ---
197
267
  async function parseJSON(req) {
198
268
  const chunks = []
269
+ let totalSize = 0
199
270
  for await (const chunk of req) {
271
+ totalSize += chunk.length
272
+ if (totalSize > MAX_JSON_BODY_SIZE) {
273
+ throw new Error('Request body too large')
274
+ }
200
275
  chunks.push(chunk)
201
276
  }
202
- try {
203
- return JSON.parse(Buffer.concat(chunks).toString())
204
- } catch {
205
- return {}
277
+ const text = Buffer.concat(chunks).toString()
278
+ if (!text.trim()) {
279
+ throw new Error('Empty request body')
206
280
  }
281
+ return JSON.parse(text)
207
282
  }
208
283
 
209
- // --- API Routes ---
284
+ // --- API 路由 ---
210
285
  async function handleAPI(req, res) {
211
286
  const url = new URL(req.url, `http://${HOST}:${PORT}`)
212
287
  const pathname = url.pathname
213
288
  const method = req.method
214
289
 
215
290
  const json = (data, status = 200) => {
216
- res.writeHead(status, { 'Content-Type': 'application/json' })
291
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' })
217
292
  res.end(JSON.stringify(data))
218
293
  }
219
294
 
@@ -224,6 +299,12 @@ async function handleAPI(req, res) {
224
299
  return
225
300
  }
226
301
 
302
+ // GET /api/peer-id
303
+ if (pathname === '/api/peer-id' && method === 'GET') {
304
+ json({ peerId: engine.getNodeId() })
305
+ return
306
+ }
307
+
227
308
  // GET /api/config
228
309
  if (pathname === '/api/config' && method === 'GET') {
229
310
  const config = loadConfig()
@@ -231,34 +312,34 @@ async function handleAPI(req, res) {
231
312
  return
232
313
  }
233
314
 
234
- // POST /api/config
315
+ // POST /api/config — 更新配置
235
316
  if (pathname === '/api/config' && method === 'POST') {
236
317
  const body = await parseJSON(req)
237
318
  const config = loadConfig()
238
-
319
+
239
320
  if (body.resetStorage) {
240
321
  config.dataPath = ''
241
322
  } else if (body.dataPath !== undefined) {
242
323
  let dataPath = body.dataPath.trim()
243
324
  let basePath = dataPath
244
-
325
+
245
326
  if (dataPath.match(/^[A-Za-z]:\\$/)) {
246
327
  basePath = dataPath
247
328
  dataPath = path.join(dataPath, 'most-data')
248
329
  }
249
-
330
+
250
331
  if (!fs.existsSync(basePath)) {
251
332
  json({ error: '目录不存在' }, 400)
252
333
  return
253
334
  }
254
-
335
+
255
336
  if (!fs.existsSync(dataPath)) {
256
337
  fs.mkdirSync(dataPath, { recursive: true })
257
338
  }
258
-
339
+
259
340
  config.dataPath = dataPath
260
341
  }
261
-
342
+
262
343
  const success = saveConfig(config)
263
344
  json({ success, dataPath: getDataPath() })
264
345
  return
@@ -285,23 +366,25 @@ async function handleAPI(req, res) {
285
366
  return
286
367
  }
287
368
 
288
- // POST /api/publish — multipart file upload
369
+ // POST /api/publish — multipart 文件上传
289
370
  if (pathname === '/api/publish' && method === 'POST') {
290
- const parts = await parseMultipart(req)
371
+ const result = await parseMultipartBusboy(req)
291
372
 
292
- const filePart = parts.find(p => p.name === 'file')
293
- if (!filePart || !filePart.filename) {
373
+ if (!result || !result.filename) {
294
374
  json({ error: 'No file provided' }, 400)
295
375
  return
296
376
  }
297
377
 
298
- const result = await engine.publishFile(filePart.data, filePart.filename)
299
-
300
- json({ success: true, ...result })
378
+ try {
379
+ const publishResult = await engine.publishFile(result.filePath, result.filename)
380
+ json({ success: true, ...publishResult })
381
+ } finally {
382
+ fs.unlink(result.filePath, () => {})
383
+ }
301
384
  return
302
385
  }
303
386
 
304
- // POST /api/download — start async download from P2P
387
+ // POST /api/download — P2P 开始异步下载
305
388
  if (pathname === '/api/download' && method === 'POST') {
306
389
  const body = await parseJSON(req)
307
390
  if (!body.link) {
@@ -311,14 +394,12 @@ async function handleAPI(req, res) {
311
394
 
312
395
  const taskId = `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
313
396
 
314
- // Parse link to check if file already exists
315
397
  const parsed = parseMostLink(body.link)
316
398
  if (parsed.error) {
317
399
  json({ error: parsed.error }, 400)
318
400
  return
319
401
  }
320
402
 
321
- // Check if file already exists in published files
322
403
  const existingFile = engine.getPublishedFiles().find(f => f.cid === parsed.cid)
323
404
  if (existingFile) {
324
405
  console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
@@ -326,7 +407,6 @@ async function handleAPI(req, res) {
326
407
  return
327
408
  }
328
409
 
329
- // Async download — do not block HTTP response
330
410
  engine.downloadFile(body.link, taskId).catch(err => {
331
411
  if (err.message === 'Download cancelled') {
332
412
  wsBroadcast('download:cancelled', { taskId })
@@ -339,7 +419,7 @@ async function handleAPI(req, res) {
339
419
  return
340
420
  }
341
421
 
342
- // POST /api/download/cancel — cancel an active download
422
+ // POST /api/download/cancel — 取消活动下载
343
423
  if (pathname === '/api/download/cancel' && method === 'POST') {
344
424
  const body = await parseJSON(req)
345
425
  if (!body.taskId) {
@@ -359,7 +439,7 @@ async function handleAPI(req, res) {
359
439
  return
360
440
  }
361
441
 
362
- // POST /api/move — rename/move a published file (changes path without re-uploading)
442
+ // POST /api/move — 重命名/移动已发布文件
363
443
  if (pathname === '/api/move' && method === 'POST') {
364
444
  const body = await parseJSON(req)
365
445
  if (!body.cid || !body.newFileName) {
@@ -367,7 +447,7 @@ async function handleAPI(req, res) {
367
447
  return
368
448
  }
369
449
  try {
370
- const result = await engine.moveFile(body.cid, body.newFileName)
450
+ const result = engine.moveFile(body.cid, body.newFileName)
371
451
  json({ success: true, ...result })
372
452
  } catch (err) {
373
453
  json({ error: err.message }, 400)
@@ -375,7 +455,7 @@ async function handleAPI(req, res) {
375
455
  return
376
456
  }
377
457
 
378
- // POST /api/folder/rename — rename a folder (renames all files within)
458
+ // POST /api/folder/rename — 重命名文件夹
379
459
  if (pathname === '/api/folder/rename' && method === 'POST') {
380
460
  const body = await parseJSON(req)
381
461
  if (!body.oldPath || !body.newPath) {
@@ -391,14 +471,62 @@ async function handleAPI(req, res) {
391
471
  return
392
472
  }
393
473
 
394
- // GET /api/files/:cid/download — serve file inline for preview / download
474
+ // GET /api/files/:cid/download — 内联服务文件,支持 Range
395
475
  if (pathname.match(/^\/api\/files\/[^/]+\/download$/) && method === 'GET') {
396
- json({ error: 'Use P2P network to download this file' }, 400)
476
+ const cid = pathname.split('/')[3]
477
+ const rangeHeader = req.headers['range']
478
+
479
+ try {
480
+ if (rangeHeader) {
481
+ const rangeMatch = rangeHeader.match(/bytes=(\d+)-(\d*)/)
482
+ if (rangeMatch) {
483
+ const start = parseInt(rangeMatch[1], 10)
484
+ const end = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : undefined
485
+
486
+ const offset = start
487
+ const limit = end !== undefined ? end - start + 1 : undefined
488
+
489
+ const result = await engine.readFileRaw(cid, { offset, limit })
490
+ const contentType = getMimeType(result.fileName)
491
+
492
+ res.writeHead(206, {
493
+ 'Content-Type': contentType,
494
+ 'Content-Length': result.buffer.length,
495
+ 'Content-Range': `bytes ${offset}-${offset + result.buffer.length - 1}/${result.totalSize}`,
496
+ 'Accept-Ranges': 'bytes'
497
+ })
498
+ res.end(result.buffer)
499
+ return
500
+ }
501
+ }
502
+
503
+ const result = await engine.readFileRaw(cid)
504
+ const contentType = getMimeType(result.fileName)
505
+ res.writeHead(200, {
506
+ 'Content-Type': contentType,
507
+ 'Content-Length': result.totalSize,
508
+ 'Accept-Ranges': 'bytes',
509
+ 'Content-Disposition': `inline; filename="${encodeURIComponent(result.fileName)}"`
510
+ })
511
+ res.end(result.buffer)
512
+ } catch (err) {
513
+ if (err.message === 'File not found') {
514
+ json({ error: err.message }, 404)
515
+ } else {
516
+ json({ error: err.message }, 400)
517
+ }
518
+ }
397
519
  return
398
520
  }
399
521
 
400
- // POST /api/shutdown — graceful server shutdown
522
+ // POST /api/shutdown — 优雅关闭服务器(仅允许 localhost 连接)
401
523
  if (pathname === '/api/shutdown' && method === 'POST') {
524
+ const clientIp = req.socket.remoteAddress
525
+ const isLocalhost = clientIp === '127.0.0.1' || clientIp === '::1' || clientIp === '::ffff:127.0.0.1'
526
+ if (!isLocalhost) {
527
+ json({ error: 'Forbidden' }, 403)
528
+ return
529
+ }
402
530
  json({ success: true })
403
531
  console.log('[MostBox] Shutdown requested via API...')
404
532
  setTimeout(async () => {
@@ -410,13 +538,13 @@ async function handleAPI(req, res) {
410
538
  return
411
539
  }
412
540
 
413
- // GET /api/trash — list trash files
541
+ // GET /api/trash — 列出回收站文件
414
542
  if (pathname === '/api/trash' && method === 'GET') {
415
543
  json(engine.listTrashFiles())
416
544
  return
417
545
  }
418
546
 
419
- // POST /api/trash/:cid/restore — restore file from trash
547
+ // POST /api/trash/:cid/restore — 从回收站恢复文件
420
548
  if (pathname.match(/^\/api\/trash\/[^/]+\/restore$/) && method === 'POST') {
421
549
  const cid = pathname.split('/')[3]
422
550
  try {
@@ -428,7 +556,7 @@ async function handleAPI(req, res) {
428
556
  return
429
557
  }
430
558
 
431
- // DELETE /api/trash/:cid — permanently delete a trash file
559
+ // DELETE /api/trash/:cid — 永久删除回收站文件
432
560
  if (pathname.match(/^\/api\/trash\/[^/]+$/) && method === 'DELETE') {
433
561
  const cid = pathname.split('/')[3]
434
562
  const result = await engine.permanentDeleteTrashFile(cid)
@@ -436,14 +564,14 @@ async function handleAPI(req, res) {
436
564
  return
437
565
  }
438
566
 
439
- // DELETE /api/trash — empty trash
567
+ // DELETE /api/trash — 清空回收站
440
568
  if (pathname === '/api/trash' && method === 'DELETE') {
441
569
  const result = await engine.emptyTrash()
442
570
  json({ success: true, trashFiles: result })
443
571
  return
444
572
  }
445
573
 
446
- // POST /api/files/:cid/star — toggle starred status
574
+ // POST /api/files/:cid/star — 切换星标状态
447
575
  if (pathname.match(/^\/api\/files\/[^/]+\/star$/) && method === 'POST') {
448
576
  const cid = pathname.split('/')[3]
449
577
  try {
@@ -455,13 +583,104 @@ async function handleAPI(req, res) {
455
583
  return
456
584
  }
457
585
 
458
- // GET /api/storage — get storage statistics
586
+ // GET /api/storage — 获取存储统计信息
459
587
  if (pathname === '/api/storage' && method === 'GET') {
460
588
  const result = await engine.getStorageStats()
461
589
  json(result)
462
590
  return
463
591
  }
464
592
 
593
+ // GET /api/display-name — 获取显示名
594
+ if (pathname === '/api/display-name' && method === 'GET') {
595
+ json({ displayName: engine.getDisplayName() })
596
+ return
597
+ }
598
+
599
+ // POST /api/display-name — 设置显示名
600
+ if (pathname === '/api/display-name' && method === 'POST') {
601
+ const body = await parseJSON(req)
602
+ if (!body.name || !body.name.trim()) {
603
+ json({ error: 'name is required' }, 400)
604
+ return
605
+ }
606
+ const success = engine.setDisplayName(body.name)
607
+ json({ success, displayName: engine.getDisplayName() })
608
+ return
609
+ }
610
+
611
+ // POST /api/channels — 创建/加入频道
612
+ if (pathname === '/api/channels' && method === 'POST') {
613
+ const body = await parseJSON(req)
614
+ if (!body.name || !body.name.trim()) {
615
+ json({ error: 'name is required' }, 400)
616
+ return
617
+ }
618
+ try {
619
+ const result = await engine.createChannel(body.name.trim(), body.type || 'personal')
620
+ json({ success: true, ...result })
621
+ } catch (err) {
622
+ json({ error: err.message }, 400)
623
+ }
624
+ return
625
+ }
626
+
627
+ // GET /api/channels — 获取频道列表
628
+ if (pathname === '/api/channels' && method === 'GET') {
629
+ json(engine.listChannels())
630
+ return
631
+ }
632
+
633
+ // DELETE /api/channels/:name — 离开频道
634
+ if (pathname.startsWith('/api/channels/') && method === 'DELETE') {
635
+ const name = pathname.split('/')[3]
636
+ try {
637
+ const result = await engine.leaveChannel(name)
638
+ json({ success: true, channels: result })
639
+ } catch (err) {
640
+ json({ error: err.message }, 400)
641
+ }
642
+ return
643
+ }
644
+
645
+ // GET /api/channels/:name/messages — 获取频道消息
646
+ if (pathname.match(/^\/api\/channels\/[^/]+\/messages$/) && method === 'GET') {
647
+ const name = pathname.split('/')[3]
648
+ const urlObj = new URL(req.url, `http://${HOST}:${PORT}`)
649
+ const limit = parseInt(urlObj.searchParams.get('limit') || '100', 10)
650
+ const offset = parseInt(urlObj.searchParams.get('offset') || '0', 10)
651
+ try {
652
+ const messages = await engine.getChannelMessages(name, { limit, offset })
653
+ json(messages)
654
+ } catch (err) {
655
+ json({ error: err.message }, 400)
656
+ }
657
+ return
658
+ }
659
+
660
+ // POST /api/channels/:name/messages — 发送消息
661
+ if (pathname.match(/^\/api\/channels\/[^/]+\/messages$/) && method === 'POST') {
662
+ const name = pathname.split('/')[3]
663
+ const body = await parseJSON(req)
664
+ if (!body.content || !body.content.trim()) {
665
+ json({ error: 'content is required' }, 400)
666
+ return
667
+ }
668
+ try {
669
+ const message = await engine.sendMessage(name, body.content, body.authorName)
670
+ json({ success: true, message })
671
+ } catch (err) {
672
+ json({ error: err.message }, 400)
673
+ }
674
+ return
675
+ }
676
+
677
+ // GET /api/channels/:name/peers — 获取频道内在线用户
678
+ if (pathname.match(/^\/api\/channels\/[^/]+\/peers$/) && method === 'GET') {
679
+ const name = pathname.split('/')[3]
680
+ json(engine.getChannelPeers(name))
681
+ return
682
+ }
683
+
465
684
  json({ error: 'Not found' }, 404)
466
685
  } catch (err) {
467
686
  console.error('[API Error]', err)
@@ -469,79 +688,43 @@ async function handleAPI(req, res) {
469
688
  }
470
689
  }
471
690
 
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
691
  function wsBroadcast(event, data) {
513
692
  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)
693
+ if (wss) {
694
+ wss.clients.forEach((client) => {
695
+ if (client.readyState === 1) {
696
+ try { client.send(payload) } catch {}
697
+ }
698
+ })
534
699
  }
700
+ }
701
+
702
+ const channelSubscriptions = new Map()
535
703
 
536
- for (const client of wsClients) {
537
- try { client.write(frame) } catch {}
704
+ function wsSendToChannel(channelName, event, data) {
705
+ const payload = JSON.stringify({ event, data })
706
+ const subscribers = channelSubscriptions.get(channelName)
707
+ if (subscribers) {
708
+ subscribers.forEach((ws) => {
709
+ if (ws.readyState === 1) {
710
+ try { ws.send(payload) } catch {}
711
+ }
712
+ })
538
713
  }
539
714
  }
540
715
 
541
- // --- Main ---
716
+ // --- 主函数 ---
542
717
  async function main() {
543
718
  console.log('[MostBox] Starting core daemon...')
544
719
 
720
+ if (fs.existsSync(UPLOAD_TMP_DIR)) {
721
+ const staleFiles = fs.readdirSync(UPLOAD_TMP_DIR)
722
+ for (const file of staleFiles) {
723
+ try { fs.unlinkSync(path.join(UPLOAD_TMP_DIR, file)) } catch {}
724
+ }
725
+ console.log(`[MostBox] Cleaned ${staleFiles.length} stale upload temp files`)
726
+ }
727
+
545
728
  const dataPath = getDataPath()
546
729
  console.log(`[MostBox] Storage: ${dataPath}`)
547
730
 
@@ -556,12 +739,17 @@ async function main() {
556
739
  engine.on('connection', () => {
557
740
  wsBroadcast('network:status', engine.getNetworkStatus())
558
741
  })
742
+ engine.on('channel:message', (data) => wsSendToChannel(data.channel, 'channel:message', data))
743
+ engine.on('channel:peer:online', (data) => wsBroadcast('channel:peer:online', data))
744
+ engine.on('channel:peer:offline', (data) => wsBroadcast('channel:peer:offline', data))
745
+ engine.on('channel:joined', (data) => wsBroadcast('channel:joined', data))
746
+ engine.on('channel:left', (data) => wsBroadcast('channel:left', data))
559
747
 
560
748
  await engine.start()
561
749
  console.log('[MostBox] Engine ready')
562
750
 
563
751
  serverInstance = http.createServer((req, res) => {
564
- res.setHeader('Access-Control-Allow-Origin', '*')
752
+ res.setHeader('Access-Control-Allow-Origin', `http://${HOST}:${PORT}`)
565
753
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
566
754
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
567
755
 
@@ -571,16 +759,82 @@ async function main() {
571
759
  return
572
760
  }
573
761
 
762
+ const clientIp = req.socket.remoteAddress || 'unknown'
763
+
764
+ if (!checkRateLimit(clientIp)) {
765
+ res.writeHead(429, { 'Content-Type': 'application/json' })
766
+ res.end(JSON.stringify({ error: 'Too many requests' }))
767
+ return
768
+ }
769
+
574
770
  if (req.url.startsWith('/api/')) {
575
- handleAPI(req, res)
771
+ handleAPI(req, res).catch(err => {
772
+ console.error('[Unhandled API Error]', err)
773
+ if (!res.headersSent) {
774
+ res.writeHead(500, { 'Content-Type': 'application/json' })
775
+ res.end(JSON.stringify({ error: 'Internal server error' }))
776
+ }
777
+ })
576
778
  } else {
577
779
  serveStatic(req, res)
578
780
  }
579
781
  })
580
782
 
581
- serverInstance.on('upgrade', (req, socket) => {
783
+ wss = new WebSocketServer({ noServer: true })
784
+ wss.on('connection', (ws) => {
785
+ ws.on('error', () => {})
786
+ ws.on('close', () => {
787
+ for (const [channelName, subscribers] of channelSubscriptions) {
788
+ if (subscribers.has(ws)) {
789
+ subscribers.delete(ws)
790
+ if (subscribers.size === 0) {
791
+ channelSubscriptions.delete(channelName)
792
+ }
793
+ }
794
+ }
795
+ })
796
+ ws.on('message', (raw) => {
797
+ try {
798
+ const msg = JSON.parse(raw)
799
+ const { event, data } = msg
800
+
801
+ switch (event) {
802
+ case 'register':
803
+ ws.peerId = data.peerId
804
+ break
805
+ case 'channel:subscribe':
806
+ if (data.channel) {
807
+ const channelName = data.channel
808
+ if (!channelSubscriptions.has(channelName)) {
809
+ channelSubscriptions.set(channelName, new Set())
810
+ }
811
+ channelSubscriptions.get(channelName).add(ws)
812
+ }
813
+ break
814
+ case 'channel:unsubscribe':
815
+ if (data.channel) {
816
+ const channelName = data.channel
817
+ const subscribers = channelSubscriptions.get(channelName)
818
+ if (subscribers) {
819
+ subscribers.delete(ws)
820
+ if (subscribers.size === 0) {
821
+ channelSubscriptions.delete(channelName)
822
+ }
823
+ }
824
+ }
825
+ break
826
+ }
827
+ } catch (err) {
828
+ console.error('[WS Message Error]', err.message)
829
+ }
830
+ })
831
+ })
832
+
833
+ serverInstance.on('upgrade', (req, socket, head) => {
582
834
  if (req.url.startsWith('/ws')) {
583
- upgradeToWebSocket(req, socket)
835
+ wss.handleUpgrade(req, socket, head, (ws) => {
836
+ wss.emit('connection', ws, req)
837
+ })
584
838
  } else {
585
839
  socket.destroy()
586
840
  }
@@ -590,20 +844,31 @@ async function main() {
590
844
  const url = `http://${HOST}:${PORT}`
591
845
  console.log(`[MostBox] Server running at ${url}`)
592
846
 
593
- const cmd = process.platform === 'win32' ? 'start ""'
594
- : process.platform === 'darwin' ? 'open' : 'xdg-open'
595
- exec(`${cmd} "${url}"`)
847
+ if (process.platform === 'win32') {
848
+ spawn('cmd.exe', ['/c', 'start', '', url], {
849
+ detached: true,
850
+ stdio: 'ignore'
851
+ }).unref()
852
+ } else {
853
+ const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open'
854
+ spawn(cmd, [url], {
855
+ detached: true,
856
+ stdio: 'ignore'
857
+ }).unref()
858
+ }
596
859
  })
597
860
 
598
861
  process.on('SIGINT', async () => {
599
862
  console.log('\n[MostBox] Shutting down...')
600
863
  await engine.stop()
864
+ if (wss) wss.close()
601
865
  serverInstance.close()
602
866
  process.exit(0)
603
867
  })
604
868
 
605
869
  process.on('SIGTERM', async () => {
606
870
  await engine.stop()
871
+ if (wss) wss.close()
607
872
  serverInstance.close()
608
873
  process.exit(0)
609
874
  })