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.
- package/README.md +182 -73
- package/out/404/index.html +15 -0
- package/out/404.html +15 -0
- package/out/__next.__PAGE__.txt +9 -0
- package/out/__next._full.txt +18 -0
- package/out/__next._head.txt +5 -0
- package/out/__next._index.txt +6 -0
- package/out/__next._tree.txt +2 -0
- package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_buildManifest.js +11 -0
- package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_clientMiddlewareManifest.js +1 -0
- package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_ssgManifest.js +1 -0
- package/out/_next/static/chunks/00l-yd3t8dvwz.js +5 -0
- package/out/_next/static/chunks/03k8t3tgym~8~.js +1 -0
- package/out/_next/static/chunks/03~yq9q893hmn.js +1 -0
- package/out/_next/static/chunks/09vfh8lfuacc0.css +1 -0
- package/out/_next/static/chunks/0bogtdbh.dcu1.js +1 -0
- package/out/_next/static/chunks/0dbhjjzl8qfwv.js +1 -0
- package/out/_next/static/chunks/0f73psqhr8dre.css +1 -0
- package/out/_next/static/chunks/0fbi7z4_.4j1j.js +1 -0
- package/out/_next/static/chunks/0ht900cau6_ur.js +31 -0
- package/out/_next/static/chunks/0ohm.ia-4ec60.js +1 -0
- package/out/_next/static/chunks/0u5ydb-f0.vxl.js +1 -0
- package/out/_next/static/chunks/14t2m1on-s5v~.js +1 -0
- package/out/_next/static/chunks/turbopack-076ce9exut_h3.js +1 -0
- package/out/_not-found/__next._full.txt +16 -0
- package/out/_not-found/__next._head.txt +5 -0
- package/out/_not-found/__next._index.txt +6 -0
- package/out/_not-found/__next._not-found/__PAGE__.txt +5 -0
- package/out/_not-found/__next._not-found.txt +5 -0
- package/out/_not-found/__next._tree.txt +2 -0
- package/out/_not-found/index.html +15 -0
- package/out/_not-found/index.txt +16 -0
- package/out/app.css +1535 -0
- package/out/bundle.js +107 -0
- package/out/bundle.js.map +7 -0
- package/out/chat/__next._full.txt +19 -0
- package/out/chat/__next._head.txt +5 -0
- package/out/chat/__next._index.txt +6 -0
- package/out/chat/__next._tree.txt +3 -0
- package/out/chat/__next.chat/__PAGE__.txt +9 -0
- package/out/chat/__next.chat.txt +5 -0
- package/out/chat/index.html +15 -0
- package/out/chat/index.txt +19 -0
- package/out/chat-page.js +112 -0
- package/out/chat.css +378 -0
- package/out/favicon.ico +0 -0
- package/out/index.html +15 -0
- package/out/index.js +148 -0
- package/out/index.txt +18 -0
- package/package.json +16 -7
- package/public/app.css +1535 -0
- package/public/bundle.js +10 -14
- package/public/bundle.js.map +4 -4
- package/public/chat-page.js +112 -0
- package/public/chat.css +378 -0
- package/public/index.js +148 -0
- package/server.js +464 -199
- package/src/config.js +36 -8
- package/src/core/cid.js +28 -19
- package/src/index.js +872 -276
- package/src/utils/api.js +6 -0
- package/src/utils/security.js +27 -24
- package/build.mjs +0 -40
- package/public/app.jsx +0 -1335
- package/public/icons/apple-touch-icon.png +0 -0
- package/public/icons/mask-icon.svg +0 -3
- package/public/icons/most.png +0 -0
- package/public/icons/pwa-192x192.png +0 -0
- package/public/icons/pwa-512x512.png +0 -0
- package/public/index.html +0 -15
- 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
|
|
7
|
-
import
|
|
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
|
-
// ---
|
|
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
|
-
|
|
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
|
-
// ---
|
|
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
|
-
// ---
|
|
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(
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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(
|
|
160
|
+
res.end(data)
|
|
107
161
|
})
|
|
108
162
|
}
|
|
109
163
|
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
167
|
+
const filenameStarMatch = headerStr.match(/filename\*=(?:UTF-8''|utf-8'')([^;\r\n]+)/i)
|
|
168
|
+
if (filenameStarMatch) {
|
|
169
|
+
return decodeURIComponent(filenameStarMatch[1])
|
|
119
170
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
230
|
+
stream.on('error', () => {
|
|
231
|
+
if (tempPath) fs.unlink(tempPath, () => {})
|
|
232
|
+
})
|
|
148
233
|
|
|
149
|
-
|
|
150
|
-
const headerEndAlt = partData.indexOf('\n\n')
|
|
234
|
+
stream.pipe(writeStream)
|
|
151
235
|
|
|
152
|
-
|
|
153
|
-
|
|
236
|
+
writeStream.on('finish', () => {
|
|
237
|
+
result.filePath = tempPath
|
|
238
|
+
resolve(result)
|
|
239
|
+
})
|
|
154
240
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
bodyStart = headerEndAlt + 2
|
|
161
|
-
}
|
|
241
|
+
writeStream.on('error', (err) => {
|
|
242
|
+
if (tempPath) fs.unlink(tempPath, () => {})
|
|
243
|
+
reject(err)
|
|
244
|
+
})
|
|
245
|
+
})
|
|
162
246
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
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
|
|
369
|
+
// POST /api/publish — multipart 文件上传
|
|
289
370
|
if (pathname === '/api/publish' && method === 'POST') {
|
|
290
|
-
const
|
|
371
|
+
const result = await parseMultipartBusboy(req)
|
|
291
372
|
|
|
292
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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 —
|
|
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 —
|
|
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 —
|
|
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 =
|
|
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 —
|
|
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 —
|
|
474
|
+
// GET /api/files/:cid/download — 内联服务文件,支持 Range
|
|
395
475
|
if (pathname.match(/^\/api\/files\/[^/]+\/download$/) && method === 'GET') {
|
|
396
|
-
|
|
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 —
|
|
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 —
|
|
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 —
|
|
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 —
|
|
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 —
|
|
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 —
|
|
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 —
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
537
|
-
|
|
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
|
-
// ---
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
})
|