@lzpong/httpm 1.2.1 → 1.2.3
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 +7 -4
- package/httpm.js +109 -82
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,9 +122,11 @@ app.use((err, req, res, next) => {
|
|
|
122
122
|
| `req.body` | 请求体(JSON/URL-encoded 自动解析) |
|
|
123
123
|
| `req.formData` | 表单数据(multipart 解析后) |
|
|
124
124
|
| `req.cookies` | Cookie 对象 |
|
|
125
|
+
| `req.signedCookies` | 签名 Cookie 对象(需配置 `cookieParserSecret`) |
|
|
125
126
|
| `req.get(name)` | 获取请求头(不区分大小写) |
|
|
126
127
|
| `req.headers` | 请求头对象 |
|
|
127
128
|
| `req.ip` | 客户端 IP |
|
|
129
|
+
| `req.hostname` | 请求域名 |
|
|
128
130
|
| `req.protocol` | 协议(http/https) |
|
|
129
131
|
|
|
130
132
|
## 响应对象 (res)
|
|
@@ -134,13 +136,14 @@ app.use((err, req, res, next) => {
|
|
|
134
136
|
| `res.status(code)` | 设置状态码,链式调用 |
|
|
135
137
|
| `res.json(obj)` | 发送 JSON 响应 |
|
|
136
138
|
| `res.send(data)` | 发送响应(自动识别类型;`null`→`"null"`,`undefined`→空响应) |
|
|
137
|
-
| `res.sendFile(path, opts)` |
|
|
138
|
-
| `res.download(path, name)` |
|
|
139
|
+
| `res.sendFile(path, opts)` | 发送文件,opts 支持 `{ root, contentType }` |
|
|
140
|
+
| `res.download(path, [name], [opts])` | 下载文件,兼容 Express 签名:opts 传递给 sendFile |
|
|
139
141
|
| `res.redirect([code,] url)` | 重定向,兼容 Express 签名:`redirect(url)` 默认 302,`redirect(status, url)` 指定状态码 |
|
|
140
142
|
| `res.cookie(name, value, opts)` | 设置 Cookie |
|
|
141
143
|
| `res.clearCookie(name, opts)` | 清除 Cookie |
|
|
142
|
-
| `res.set(name, value)` / `res.setHeader()` | 设置响应头(set
|
|
143
|
-
| `res.get(name)` / `res.getHeader()` |
|
|
144
|
+
| `res.set(name, value)` / `res.setHeader()` | 设置响应头(set 支持对象批量;setHeader 为底层方法,功能相同) |
|
|
145
|
+
| `res.get(name)` / `res.getHeader()` | 获取响应头(get 为 Express 兼容方法,功能相同) |
|
|
146
|
+
| `res.removeHeader(name)` | 移除已设置的响应头 |
|
|
144
147
|
| `res.type(type)` | 设置 Content-Type(支持简写:html→text/html) |
|
|
145
148
|
| `res.on(event, fn)` | 监听响应事件(finish/close 等) |
|
|
146
149
|
| `res.sse()` | 创建 SSE 实例 |
|
package/httpm.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* httpm - 基于 Node.js 原生模块的单文件、零依赖 HTTP 服务库
|
|
3
3
|
*
|
|
4
4
|
* @name httpm
|
|
5
|
-
* @version 1.2.
|
|
5
|
+
* @version 1.2.3
|
|
6
6
|
* @description 兼容 Express API,内置路由、中间件、静态文件服务、
|
|
7
7
|
* WebSocket、SSE、流式上传、日志系统等功能
|
|
8
8
|
* @license MIT
|
|
@@ -164,8 +164,7 @@ const MIME_TYPES = {
|
|
|
164
164
|
* 根据文件后缀匹配标准 MIME 类型
|
|
165
165
|
*/
|
|
166
166
|
function getMimeType(ext) {
|
|
167
|
-
|
|
168
|
-
const lower = ext.toLowerCase();
|
|
167
|
+
const lower = ext?.toString().toLowerCase();
|
|
169
168
|
return MIME_TYPES[lower] || 'application/octet-stream';
|
|
170
169
|
}
|
|
171
170
|
|
|
@@ -773,7 +772,12 @@ class Response {
|
|
|
773
772
|
}
|
|
774
773
|
|
|
775
774
|
const mime = getMimeType(path.extname(fullPath));
|
|
776
|
-
|
|
775
|
+
// Content-Type 优先级:options.contentType > 已设置的 Content-Type > 自动检测
|
|
776
|
+
if (options.contentType) {
|
|
777
|
+
this.setHeader('Content-Type', options.contentType);
|
|
778
|
+
} else if (!this.getHeader('Content-Type')) {
|
|
779
|
+
this.setHeader('Content-Type', mime);
|
|
780
|
+
}
|
|
777
781
|
this.setHeader('Accept-Ranges', 'bytes');
|
|
778
782
|
|
|
779
783
|
// ETag 缓存校验
|
|
@@ -872,10 +876,19 @@ class Response {
|
|
|
872
876
|
/**
|
|
873
877
|
* 触发浏览器文件下载,支持大文件进度展示
|
|
874
878
|
*/
|
|
875
|
-
download(filePath, filename) {
|
|
876
|
-
|
|
879
|
+
download(filePath, filename, options) {
|
|
880
|
+
// Express 兼容签名:download(path), download(path, filename), download(path, filename, options), download(path, options)
|
|
881
|
+
let name = filename;
|
|
882
|
+
let opts = options;
|
|
883
|
+
if (typeof filename === 'object' && filename !== null) {
|
|
884
|
+
// download(path, options) 形式
|
|
885
|
+
opts = filename;
|
|
886
|
+
name = null;
|
|
887
|
+
}
|
|
888
|
+
name = name || path.basename(filePath);
|
|
889
|
+
opts = opts || {};
|
|
877
890
|
this.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(name)}"`);
|
|
878
|
-
this.sendFile(filePath);
|
|
891
|
+
this.sendFile(filePath, opts);
|
|
879
892
|
}
|
|
880
893
|
|
|
881
894
|
/**
|
|
@@ -1045,8 +1058,6 @@ class WebSocket {
|
|
|
1045
1058
|
this._handlers = {};
|
|
1046
1059
|
// 最大帧负载大小(默认 100MB,防止恶意超大帧耗尽内存)
|
|
1047
1060
|
this._maxPayload = options.maxPayload || 100 * 1024 * 1024;
|
|
1048
|
-
// 写入背压标记
|
|
1049
|
-
this._writeBacklogged = false;
|
|
1050
1061
|
// 帧解析状态:缓存不完整帧数据
|
|
1051
1062
|
this._frameBuffer = Buffer.alloc(0);
|
|
1052
1063
|
// 分片帧状态
|
|
@@ -1090,7 +1101,7 @@ class WebSocket {
|
|
|
1090
1101
|
while (this._frameBuffer.length >= 2) {
|
|
1091
1102
|
const result = this._decodeFrame(this._frameBuffer);
|
|
1092
1103
|
if (result === null) break; // 数据不完整,等待更多数据
|
|
1093
|
-
this._frameBuffer = this._frameBuffer.
|
|
1104
|
+
this._frameBuffer = this._frameBuffer.subarray(result.bytesConsumed);
|
|
1094
1105
|
this._processFrame(result.opcode, result.payload, result.fin, result.oversize);
|
|
1095
1106
|
}
|
|
1096
1107
|
}
|
|
@@ -1129,7 +1140,7 @@ class WebSocket {
|
|
|
1129
1140
|
let mask = null;
|
|
1130
1141
|
if (isMasked) {
|
|
1131
1142
|
if (buf.length < offset + 4) return null;
|
|
1132
|
-
mask = buf.
|
|
1143
|
+
mask = buf.subarray(offset, offset + 4);
|
|
1133
1144
|
offset += 4;
|
|
1134
1145
|
}
|
|
1135
1146
|
|
|
@@ -1142,7 +1153,7 @@ class WebSocket {
|
|
|
1142
1153
|
}
|
|
1143
1154
|
|
|
1144
1155
|
// 提取负载
|
|
1145
|
-
let payload = buf.
|
|
1156
|
+
let payload = buf.subarray(offset, offset + payloadLength);
|
|
1146
1157
|
if (isMasked && mask) {
|
|
1147
1158
|
for (let i = 0; i < payload.length; i++) {
|
|
1148
1159
|
payload[i] ^= mask[i % 4];
|
|
@@ -1174,7 +1185,7 @@ class WebSocket {
|
|
|
1174
1185
|
code = payload.readUInt16BE(0);
|
|
1175
1186
|
// RFC 6455 Section 7.4: 状态码 0-999 为非法,1005 表示无状态码
|
|
1176
1187
|
if (code < 1000) code = 1005;
|
|
1177
|
-
reason = payload.length > 2 ? payload.
|
|
1188
|
+
reason = payload.length > 2 ? payload.subarray(2).toString('utf8') : '';
|
|
1178
1189
|
}
|
|
1179
1190
|
// 如果正在关闭握手中,对端已回复 Close 帧,完成握手
|
|
1180
1191
|
if (this._closing) {
|
|
@@ -1282,11 +1293,7 @@ class WebSocket {
|
|
|
1282
1293
|
frames.push(payload);
|
|
1283
1294
|
const frame = Buffer.concat(frames);
|
|
1284
1295
|
try {
|
|
1285
|
-
|
|
1286
|
-
// 写入缓冲区满时记录警告(WebSocket 不像 HTTP 可暂停请求流,只能记录)
|
|
1287
|
-
if (!canWrite) {
|
|
1288
|
-
this._writeBacklogged = true;
|
|
1289
|
-
}
|
|
1296
|
+
this.socket.write(frame);
|
|
1290
1297
|
} catch (e) {
|
|
1291
1298
|
this.connected = false;
|
|
1292
1299
|
// 发送失败时触发 error 事件,便于用户感知和处理
|
|
@@ -1692,10 +1699,8 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1692
1699
|
let cleanupOnError = false;
|
|
1693
1700
|
let paused = false;
|
|
1694
1701
|
|
|
1695
|
-
//
|
|
1696
|
-
|
|
1697
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
1698
|
-
}
|
|
1702
|
+
// 确保临时目录存在(recursive: true 时目录已存在不报错,无需 existsSync)
|
|
1703
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1699
1704
|
|
|
1700
1705
|
function processBuffer() {
|
|
1701
1706
|
while (buffer.length > 0) {
|
|
@@ -1705,14 +1710,14 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1705
1710
|
if (idx === -1) {
|
|
1706
1711
|
// 保留尾部回看字节,防止分隔符跨 chunk 截断
|
|
1707
1712
|
if (buffer.length > lookBehind) {
|
|
1708
|
-
buffer = buffer.
|
|
1713
|
+
buffer = buffer.subarray(buffer.length - lookBehind);
|
|
1709
1714
|
}
|
|
1710
1715
|
break;
|
|
1711
1716
|
}
|
|
1712
|
-
buffer = buffer.
|
|
1717
|
+
buffer = buffer.subarray(idx + delimiter.length);
|
|
1713
1718
|
// 跳过 \r\n
|
|
1714
1719
|
if (buffer.length >= 2 && buffer[0] === 0x0D && buffer[1] === 0x0A) {
|
|
1715
|
-
buffer = buffer.
|
|
1720
|
+
buffer = buffer.subarray(2);
|
|
1716
1721
|
}
|
|
1717
1722
|
state = 'HEADERS';
|
|
1718
1723
|
partHeadersBuf = Buffer.alloc(0);
|
|
@@ -1725,8 +1730,8 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1725
1730
|
buffer = Buffer.alloc(0);
|
|
1726
1731
|
break;
|
|
1727
1732
|
}
|
|
1728
|
-
partHeadersBuf = Buffer.concat([partHeadersBuf, buffer.
|
|
1729
|
-
buffer = buffer.
|
|
1733
|
+
partHeadersBuf = Buffer.concat([partHeadersBuf, buffer.subarray(0, headerEnd)]);
|
|
1734
|
+
buffer = buffer.subarray(headerEnd + 4);
|
|
1730
1735
|
const partHeaders = partHeadersBuf.toString('utf8');
|
|
1731
1736
|
|
|
1732
1737
|
// 解析 Content-Disposition
|
|
@@ -1761,7 +1766,7 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1761
1766
|
// 保留尾部回看字节,防止分隔符跨 chunk 截断
|
|
1762
1767
|
const safeLen = Math.max(0, buffer.length - lookBehind);
|
|
1763
1768
|
const chunk = buffer.toString('utf8', 0, safeLen);
|
|
1764
|
-
fieldSize +=
|
|
1769
|
+
fieldSize += safeLen;
|
|
1765
1770
|
if (fieldSize > maxFieldSize) {
|
|
1766
1771
|
const err = new Error(`Field exceeds maximum size of ${fmtSize(maxFieldSize)}`, { cause: { actual: fieldSize, maxSize: maxFieldSize } });
|
|
1767
1772
|
err.status = 413;
|
|
@@ -1769,12 +1774,12 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1769
1774
|
return;
|
|
1770
1775
|
}
|
|
1771
1776
|
currentField.value += chunk;
|
|
1772
|
-
buffer = buffer.
|
|
1777
|
+
buffer = buffer.subarray(safeLen);
|
|
1773
1778
|
break;
|
|
1774
1779
|
}
|
|
1775
1780
|
// 字段结束
|
|
1776
1781
|
const chunk = buffer.toString('utf8', 0, idx);
|
|
1777
|
-
fieldSize +=
|
|
1782
|
+
fieldSize += idx;
|
|
1778
1783
|
if (fieldSize > maxFieldSize) {
|
|
1779
1784
|
const err = new Error(`Field exceeds maximum size of ${fmtSize(maxFieldSize)}`, { cause: { actual: fieldSize, maxSize: maxFieldSize } });
|
|
1780
1785
|
err.status = 413;
|
|
@@ -1787,10 +1792,10 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1787
1792
|
currentField.value = currentField.value.slice(0, -2);
|
|
1788
1793
|
}
|
|
1789
1794
|
req.formData.fields[currentField.name] = currentField.value;
|
|
1790
|
-
buffer = buffer.
|
|
1795
|
+
buffer = buffer.subarray(idx + delimiter.length);
|
|
1791
1796
|
// 跳过 \r\n
|
|
1792
1797
|
if (buffer.length >= 2 && buffer[0] === 0x0D && buffer[1] === 0x0A) {
|
|
1793
|
-
buffer = buffer.
|
|
1798
|
+
buffer = buffer.subarray(2);
|
|
1794
1799
|
}
|
|
1795
1800
|
state = 'HEADERS';
|
|
1796
1801
|
partHeadersBuf = Buffer.alloc(0);
|
|
@@ -1801,7 +1806,7 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1801
1806
|
// 还没结束,写入临时文件
|
|
1802
1807
|
// 保留尾部回看字节,防止分隔符跨 chunk 截断
|
|
1803
1808
|
const safeLen = Math.max(0, buffer.length - lookBehind);
|
|
1804
|
-
const writeData = buffer.
|
|
1809
|
+
const writeData = buffer.subarray(0, safeLen);
|
|
1805
1810
|
if (!currentFile.stream) {
|
|
1806
1811
|
currentFile.stream = fs.createWriteStream(currentFile.path);
|
|
1807
1812
|
// 背压处理:写入流满时暂停请求读取,drain 后恢复
|
|
@@ -1827,14 +1832,14 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1827
1832
|
next(err);
|
|
1828
1833
|
return;
|
|
1829
1834
|
}
|
|
1830
|
-
buffer = buffer.
|
|
1835
|
+
buffer = buffer.subarray(safeLen);
|
|
1831
1836
|
break;
|
|
1832
1837
|
}
|
|
1833
1838
|
// 文件结束
|
|
1834
|
-
const fileData = buffer.
|
|
1839
|
+
const fileData = buffer.subarray(0, idx);
|
|
1835
1840
|
// 去掉文件数据前的 \r\n
|
|
1836
1841
|
const trimmedData = fileData.length >= 2 && fileData.at(-2) === 0x0D && fileData.at(-1) === 0x0A
|
|
1837
|
-
? fileData.
|
|
1842
|
+
? fileData.subarray(0, -2)
|
|
1838
1843
|
: fileData;
|
|
1839
1844
|
|
|
1840
1845
|
if (!currentFile.stream) {
|
|
@@ -1862,10 +1867,10 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1862
1867
|
|
|
1863
1868
|
// 清理上传进度条目
|
|
1864
1869
|
cleanupOnError = false;
|
|
1865
|
-
buffer = buffer.
|
|
1870
|
+
buffer = buffer.subarray(idx + delimiter.length);
|
|
1866
1871
|
// 跳过 \r\n
|
|
1867
1872
|
if (buffer.length >= 2 && buffer[0] === 0x0D && buffer[1] === 0x0A) {
|
|
1868
|
-
buffer = buffer.
|
|
1873
|
+
buffer = buffer.subarray(2);
|
|
1869
1874
|
}
|
|
1870
1875
|
state = 'HEADERS';
|
|
1871
1876
|
partHeadersBuf = Buffer.alloc(0);
|
|
@@ -2318,18 +2323,20 @@ class Application extends Router {
|
|
|
2318
2323
|
}
|
|
2319
2324
|
|
|
2320
2325
|
if (stat.isDirectory()) {
|
|
2321
|
-
// 目录:查找 index.html
|
|
2326
|
+
// 目录:查找 index.html(异步检查避免阻塞事件循环)
|
|
2322
2327
|
const indexPath = path.join(fullPath, 'index.html');
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
this.
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2328
|
+
fs.stat(indexPath, (idxErr) => {
|
|
2329
|
+
if (!idxErr) {
|
|
2330
|
+
res.sendFile(path.relative(rootPath, indexPath), { root: rootPath });
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
// 展示目录列表
|
|
2334
|
+
if (this.settings.showDir) {
|
|
2335
|
+
this._serveDirectory(req, res, fullPath, requestPath);
|
|
2336
|
+
} else {
|
|
2337
|
+
res.status(404).json({ error: 'Not Found', status: 404 });
|
|
2338
|
+
}
|
|
2339
|
+
});
|
|
2333
2340
|
return;
|
|
2334
2341
|
}
|
|
2335
2342
|
|
|
@@ -2349,35 +2356,37 @@ class Application extends Router {
|
|
|
2349
2356
|
return;
|
|
2350
2357
|
}
|
|
2351
2358
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
return {
|
|
2359
|
+
// 异步获取文件信息,避免同步阻塞事件循环
|
|
2360
|
+
const tasks = entries.map(entry => {
|
|
2361
|
+
if (entry.isDirectory()) {
|
|
2362
|
+
return Promise.resolve({ name: entry.name, isDirectory: true, size: 0, modified: '' });
|
|
2363
|
+
}
|
|
2364
|
+
return fs.promises.stat(path.join(dirPath, entry.name))
|
|
2365
|
+
.then(stat => ({
|
|
2360
2366
|
name: entry.name,
|
|
2361
2367
|
isDirectory: false,
|
|
2362
2368
|
size: stat.size,
|
|
2363
2369
|
modified: stat.mtime.toISOString()
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
return null;
|
|
2367
|
-
}
|
|
2368
|
-
}).filter(Boolean);
|
|
2369
|
-
|
|
2370
|
-
// 排序:目录在前
|
|
2371
|
-
items.sort((a, b) => {
|
|
2372
|
-
if (a.isDirectory && !b.isDirectory) return -1;
|
|
2373
|
-
if (!a.isDirectory && b.isDirectory) return 1;
|
|
2374
|
-
return a.name.localeCompare(b.name);
|
|
2370
|
+
}))
|
|
2371
|
+
.catch(() => null);
|
|
2375
2372
|
});
|
|
2376
2373
|
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2374
|
+
Promise.all(tasks).then(results => {
|
|
2375
|
+
const items = results.filter(Boolean);
|
|
2376
|
+
// 排序:目录在前
|
|
2377
|
+
items.sort((a, b) => {
|
|
2378
|
+
if (a.isDirectory && !b.isDirectory) return -1;
|
|
2379
|
+
if (!a.isDirectory && b.isDirectory) return 1;
|
|
2380
|
+
return a.name.localeCompare(b.name);
|
|
2381
|
+
});
|
|
2382
|
+
|
|
2383
|
+
// 生成 HTML 目录列表
|
|
2384
|
+
const html = this._renderDirectoryHTML(requestPath, items);
|
|
2385
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
2386
|
+
res.send(html);
|
|
2387
|
+
}).catch(() => {
|
|
2388
|
+
res.status(500).send('Internal Server Error');
|
|
2389
|
+
});
|
|
2381
2390
|
});
|
|
2382
2391
|
}
|
|
2383
2392
|
|
|
@@ -2388,18 +2397,21 @@ class Application extends Router {
|
|
|
2388
2397
|
// requestPath 已去掉前导 /,空字符串表示根目录
|
|
2389
2398
|
// 根目录时显示 '/',否则显示请求路径
|
|
2390
2399
|
const displayPath = requestPath || '/';
|
|
2400
|
+
// 构建链接前缀:确保以 / 开头并以 / 结尾,用于生成绝对路径 href
|
|
2401
|
+
const prefix = '/' + (requestPath ? requestPath.replace(/\/+$/, '') + '/' : '');
|
|
2391
2402
|
let html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Directory: ${escapeHtml(displayPath)}</title>`;
|
|
2392
2403
|
html += `<style>body{font-family:-apple-system,sans-serif;margin:20px;background:#f5f5f5}h1{font-size:18px;color:#333}table{width:100%;border-collapse:collapse;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.1)}th{text-align:left;padding:10px 12px;background:#f8f8f8;border-bottom:2px solid #ddd;font-size:13px;color:#666}td{padding:8px 12px;border-bottom:1px solid #eee;font-size:13px}a{color:#0066cc;text-decoration:none}a:hover{text-decoration:underline}.dir{font-weight:bold}.size{color:#999}</style>`;
|
|
2393
2404
|
html += `</head><body><h1>Directory: ${escapeHtml(displayPath)}</h1><table><tr><th>Name</th><th>Size</th><th>Modified</th></tr>`;
|
|
2394
2405
|
|
|
2395
|
-
//
|
|
2406
|
+
// 父目录链接:使用绝对路径返回上一级(根目录时不显示)
|
|
2396
2407
|
if (requestPath && requestPath !== '/') {
|
|
2397
|
-
|
|
2408
|
+
const parentHref = prefix + '..';
|
|
2409
|
+
html += `<tr><td><a href="${escapeHtml(parentHref)}" class="dir">../</a></td><td class="size">-</td><td>-</td></tr>`;
|
|
2398
2410
|
}
|
|
2399
2411
|
|
|
2400
2412
|
for (const item of items) {
|
|
2401
|
-
//
|
|
2402
|
-
const href = item.name;
|
|
2413
|
+
// 使用绝对路径 href,避免 URL 缺少尾部斜杠时解析错误
|
|
2414
|
+
const href = prefix + item.name;
|
|
2403
2415
|
const name = item.isDirectory ? item.name + '/' : item.name;
|
|
2404
2416
|
const size = item.isDirectory ? '-' : fmtSize(item.size);
|
|
2405
2417
|
const cls = item.isDirectory ? 'dir' : '';
|
|
@@ -2414,6 +2426,18 @@ class Application extends Router {
|
|
|
2414
2426
|
* 处理 WebSocket 升级请求
|
|
2415
2427
|
*/
|
|
2416
2428
|
_handleUpgrade(req, socket, head) {
|
|
2429
|
+
// 防御性检查:_wss 在 listen() 中初始化,正常流程不会为 null
|
|
2430
|
+
if (!this._wss) {
|
|
2431
|
+
this._logger.warn('WebSocket server not initialized (call listen() first)');
|
|
2432
|
+
socket.destroy();
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
// 校验 Upgrade 头必须为 websocket
|
|
2436
|
+
if (req.headers['upgrade']?.toLowerCase() !== 'websocket') {
|
|
2437
|
+
this._logger.warn('Invalid upgrade header:', req.headers['upgrade'], 'expected: websocket');
|
|
2438
|
+
socket.destroy();
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2417
2441
|
const ws = this._wss.handleUpgrade(req, socket, head);
|
|
2418
2442
|
if (!ws) return;
|
|
2419
2443
|
|
|
@@ -2599,13 +2623,16 @@ function staticMiddleware(rootPath, options = {}) {
|
|
|
2599
2623
|
return next();
|
|
2600
2624
|
}
|
|
2601
2625
|
if (stat.isDirectory()) {
|
|
2602
|
-
// 目录:尝试 index.html
|
|
2626
|
+
// 目录:尝试 index.html(异步检查避免阻塞事件循环)
|
|
2603
2627
|
const indexPath = path.join(fullPath, 'index.html');
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2628
|
+
fs.stat(indexPath, (idxErr) => {
|
|
2629
|
+
if (!idxErr) {
|
|
2630
|
+
res.sendFile(path.relative(root, indexPath), { root });
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
return next();
|
|
2634
|
+
});
|
|
2635
|
+
return;
|
|
2609
2636
|
}
|
|
2610
2637
|
res.sendFile(requestPath, { root });
|
|
2611
2638
|
});
|