@lzpong/httpm 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/httpm.js +52 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -139,8 +139,9 @@ app.use((err, req, res, next) => {
|
|
|
139
139
|
| `res.redirect([code,] url)` | 重定向,兼容 Express 签名:`redirect(url)` 默认 302,`redirect(status, url)` 指定状态码 |
|
|
140
140
|
| `res.cookie(name, value, opts)` | 设置 Cookie |
|
|
141
141
|
| `res.clearCookie(name, opts)` | 清除 Cookie |
|
|
142
|
-
| `res.set(name, value)` / `res.setHeader()` | 设置响应头(set
|
|
143
|
-
| `res.get(name)` / `res.getHeader()` |
|
|
142
|
+
| `res.set(name, value)` / `res.setHeader()` | 设置响应头(set 支持对象批量;setHeader 为底层方法,功能相同) |
|
|
143
|
+
| `res.get(name)` / `res.getHeader()` | 获取响应头(get 为 Express 兼容方法,功能相同) |
|
|
144
|
+
| `res.removeHeader(name)` | 移除已设置的响应头 |
|
|
144
145
|
| `res.type(type)` | 设置 Content-Type(支持简写:html→text/html) |
|
|
145
146
|
| `res.on(event, fn)` | 监听响应事件(finish/close 等) |
|
|
146
147
|
| `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.2
|
|
6
6
|
* @description 兼容 Express API,内置路由、中间件、静态文件服务、
|
|
7
7
|
* WebSocket、SSE、流式上传、日志系统等功能
|
|
8
8
|
* @license MIT
|
|
@@ -1045,8 +1045,6 @@ class WebSocket {
|
|
|
1045
1045
|
this._handlers = {};
|
|
1046
1046
|
// 最大帧负载大小(默认 100MB,防止恶意超大帧耗尽内存)
|
|
1047
1047
|
this._maxPayload = options.maxPayload || 100 * 1024 * 1024;
|
|
1048
|
-
// 写入背压标记
|
|
1049
|
-
this._writeBacklogged = false;
|
|
1050
1048
|
// 帧解析状态:缓存不完整帧数据
|
|
1051
1049
|
this._frameBuffer = Buffer.alloc(0);
|
|
1052
1050
|
// 分片帧状态
|
|
@@ -1090,7 +1088,7 @@ class WebSocket {
|
|
|
1090
1088
|
while (this._frameBuffer.length >= 2) {
|
|
1091
1089
|
const result = this._decodeFrame(this._frameBuffer);
|
|
1092
1090
|
if (result === null) break; // 数据不完整,等待更多数据
|
|
1093
|
-
this._frameBuffer = this._frameBuffer.
|
|
1091
|
+
this._frameBuffer = this._frameBuffer.subarray(result.bytesConsumed);
|
|
1094
1092
|
this._processFrame(result.opcode, result.payload, result.fin, result.oversize);
|
|
1095
1093
|
}
|
|
1096
1094
|
}
|
|
@@ -1129,7 +1127,7 @@ class WebSocket {
|
|
|
1129
1127
|
let mask = null;
|
|
1130
1128
|
if (isMasked) {
|
|
1131
1129
|
if (buf.length < offset + 4) return null;
|
|
1132
|
-
mask = buf.
|
|
1130
|
+
mask = buf.subarray(offset, offset + 4);
|
|
1133
1131
|
offset += 4;
|
|
1134
1132
|
}
|
|
1135
1133
|
|
|
@@ -1142,7 +1140,7 @@ class WebSocket {
|
|
|
1142
1140
|
}
|
|
1143
1141
|
|
|
1144
1142
|
// 提取负载
|
|
1145
|
-
let payload = buf.
|
|
1143
|
+
let payload = buf.subarray(offset, offset + payloadLength);
|
|
1146
1144
|
if (isMasked && mask) {
|
|
1147
1145
|
for (let i = 0; i < payload.length; i++) {
|
|
1148
1146
|
payload[i] ^= mask[i % 4];
|
|
@@ -1174,7 +1172,7 @@ class WebSocket {
|
|
|
1174
1172
|
code = payload.readUInt16BE(0);
|
|
1175
1173
|
// RFC 6455 Section 7.4: 状态码 0-999 为非法,1005 表示无状态码
|
|
1176
1174
|
if (code < 1000) code = 1005;
|
|
1177
|
-
reason = payload.length > 2 ? payload.
|
|
1175
|
+
reason = payload.length > 2 ? payload.subarray(2).toString('utf8') : '';
|
|
1178
1176
|
}
|
|
1179
1177
|
// 如果正在关闭握手中,对端已回复 Close 帧,完成握手
|
|
1180
1178
|
if (this._closing) {
|
|
@@ -1282,11 +1280,7 @@ class WebSocket {
|
|
|
1282
1280
|
frames.push(payload);
|
|
1283
1281
|
const frame = Buffer.concat(frames);
|
|
1284
1282
|
try {
|
|
1285
|
-
|
|
1286
|
-
// 写入缓冲区满时记录警告(WebSocket 不像 HTTP 可暂停请求流,只能记录)
|
|
1287
|
-
if (!canWrite) {
|
|
1288
|
-
this._writeBacklogged = true;
|
|
1289
|
-
}
|
|
1283
|
+
this.socket.write(frame);
|
|
1290
1284
|
} catch (e) {
|
|
1291
1285
|
this.connected = false;
|
|
1292
1286
|
// 发送失败时触发 error 事件,便于用户感知和处理
|
|
@@ -1705,14 +1699,14 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1705
1699
|
if (idx === -1) {
|
|
1706
1700
|
// 保留尾部回看字节,防止分隔符跨 chunk 截断
|
|
1707
1701
|
if (buffer.length > lookBehind) {
|
|
1708
|
-
buffer = buffer.
|
|
1702
|
+
buffer = buffer.subarray(buffer.length - lookBehind);
|
|
1709
1703
|
}
|
|
1710
1704
|
break;
|
|
1711
1705
|
}
|
|
1712
|
-
buffer = buffer.
|
|
1706
|
+
buffer = buffer.subarray(idx + delimiter.length);
|
|
1713
1707
|
// 跳过 \r\n
|
|
1714
1708
|
if (buffer.length >= 2 && buffer[0] === 0x0D && buffer[1] === 0x0A) {
|
|
1715
|
-
buffer = buffer.
|
|
1709
|
+
buffer = buffer.subarray(2);
|
|
1716
1710
|
}
|
|
1717
1711
|
state = 'HEADERS';
|
|
1718
1712
|
partHeadersBuf = Buffer.alloc(0);
|
|
@@ -1725,8 +1719,8 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1725
1719
|
buffer = Buffer.alloc(0);
|
|
1726
1720
|
break;
|
|
1727
1721
|
}
|
|
1728
|
-
partHeadersBuf = Buffer.concat([partHeadersBuf, buffer.
|
|
1729
|
-
buffer = buffer.
|
|
1722
|
+
partHeadersBuf = Buffer.concat([partHeadersBuf, buffer.subarray(0, headerEnd)]);
|
|
1723
|
+
buffer = buffer.subarray(headerEnd + 4);
|
|
1730
1724
|
const partHeaders = partHeadersBuf.toString('utf8');
|
|
1731
1725
|
|
|
1732
1726
|
// 解析 Content-Disposition
|
|
@@ -1761,7 +1755,7 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1761
1755
|
// 保留尾部回看字节,防止分隔符跨 chunk 截断
|
|
1762
1756
|
const safeLen = Math.max(0, buffer.length - lookBehind);
|
|
1763
1757
|
const chunk = buffer.toString('utf8', 0, safeLen);
|
|
1764
|
-
fieldSize +=
|
|
1758
|
+
fieldSize += safeLen;
|
|
1765
1759
|
if (fieldSize > maxFieldSize) {
|
|
1766
1760
|
const err = new Error(`Field exceeds maximum size of ${fmtSize(maxFieldSize)}`, { cause: { actual: fieldSize, maxSize: maxFieldSize } });
|
|
1767
1761
|
err.status = 413;
|
|
@@ -1769,12 +1763,12 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1769
1763
|
return;
|
|
1770
1764
|
}
|
|
1771
1765
|
currentField.value += chunk;
|
|
1772
|
-
buffer = buffer.
|
|
1766
|
+
buffer = buffer.subarray(safeLen);
|
|
1773
1767
|
break;
|
|
1774
1768
|
}
|
|
1775
1769
|
// 字段结束
|
|
1776
1770
|
const chunk = buffer.toString('utf8', 0, idx);
|
|
1777
|
-
fieldSize +=
|
|
1771
|
+
fieldSize += idx;
|
|
1778
1772
|
if (fieldSize > maxFieldSize) {
|
|
1779
1773
|
const err = new Error(`Field exceeds maximum size of ${fmtSize(maxFieldSize)}`, { cause: { actual: fieldSize, maxSize: maxFieldSize } });
|
|
1780
1774
|
err.status = 413;
|
|
@@ -1787,10 +1781,10 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1787
1781
|
currentField.value = currentField.value.slice(0, -2);
|
|
1788
1782
|
}
|
|
1789
1783
|
req.formData.fields[currentField.name] = currentField.value;
|
|
1790
|
-
buffer = buffer.
|
|
1784
|
+
buffer = buffer.subarray(idx + delimiter.length);
|
|
1791
1785
|
// 跳过 \r\n
|
|
1792
1786
|
if (buffer.length >= 2 && buffer[0] === 0x0D && buffer[1] === 0x0A) {
|
|
1793
|
-
buffer = buffer.
|
|
1787
|
+
buffer = buffer.subarray(2);
|
|
1794
1788
|
}
|
|
1795
1789
|
state = 'HEADERS';
|
|
1796
1790
|
partHeadersBuf = Buffer.alloc(0);
|
|
@@ -1801,7 +1795,7 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1801
1795
|
// 还没结束,写入临时文件
|
|
1802
1796
|
// 保留尾部回看字节,防止分隔符跨 chunk 截断
|
|
1803
1797
|
const safeLen = Math.max(0, buffer.length - lookBehind);
|
|
1804
|
-
const writeData = buffer.
|
|
1798
|
+
const writeData = buffer.subarray(0, safeLen);
|
|
1805
1799
|
if (!currentFile.stream) {
|
|
1806
1800
|
currentFile.stream = fs.createWriteStream(currentFile.path);
|
|
1807
1801
|
// 背压处理:写入流满时暂停请求读取,drain 后恢复
|
|
@@ -1827,14 +1821,14 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1827
1821
|
next(err);
|
|
1828
1822
|
return;
|
|
1829
1823
|
}
|
|
1830
|
-
buffer = buffer.
|
|
1824
|
+
buffer = buffer.subarray(safeLen);
|
|
1831
1825
|
break;
|
|
1832
1826
|
}
|
|
1833
1827
|
// 文件结束
|
|
1834
|
-
const fileData = buffer.
|
|
1828
|
+
const fileData = buffer.subarray(0, idx);
|
|
1835
1829
|
// 去掉文件数据前的 \r\n
|
|
1836
1830
|
const trimmedData = fileData.length >= 2 && fileData.at(-2) === 0x0D && fileData.at(-1) === 0x0A
|
|
1837
|
-
? fileData.
|
|
1831
|
+
? fileData.subarray(0, -2)
|
|
1838
1832
|
: fileData;
|
|
1839
1833
|
|
|
1840
1834
|
if (!currentFile.stream) {
|
|
@@ -1862,10 +1856,10 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1862
1856
|
|
|
1863
1857
|
// 清理上传进度条目
|
|
1864
1858
|
cleanupOnError = false;
|
|
1865
|
-
buffer = buffer.
|
|
1859
|
+
buffer = buffer.subarray(idx + delimiter.length);
|
|
1866
1860
|
// 跳过 \r\n
|
|
1867
1861
|
if (buffer.length >= 2 && buffer[0] === 0x0D && buffer[1] === 0x0A) {
|
|
1868
|
-
buffer = buffer.
|
|
1862
|
+
buffer = buffer.subarray(2);
|
|
1869
1863
|
}
|
|
1870
1864
|
state = 'HEADERS';
|
|
1871
1865
|
partHeadersBuf = Buffer.alloc(0);
|
|
@@ -2349,35 +2343,37 @@ class Application extends Router {
|
|
|
2349
2343
|
return;
|
|
2350
2344
|
}
|
|
2351
2345
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
return {
|
|
2346
|
+
// 异步获取文件信息,避免同步阻塞事件循环
|
|
2347
|
+
const tasks = entries.map(entry => {
|
|
2348
|
+
if (entry.isDirectory()) {
|
|
2349
|
+
return Promise.resolve({ name: entry.name, isDirectory: true, size: 0, modified: '' });
|
|
2350
|
+
}
|
|
2351
|
+
return fs.promises.stat(path.join(dirPath, entry.name))
|
|
2352
|
+
.then(stat => ({
|
|
2360
2353
|
name: entry.name,
|
|
2361
2354
|
isDirectory: false,
|
|
2362
2355
|
size: stat.size,
|
|
2363
2356
|
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);
|
|
2357
|
+
}))
|
|
2358
|
+
.catch(() => null);
|
|
2375
2359
|
});
|
|
2376
2360
|
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2361
|
+
Promise.all(tasks).then(results => {
|
|
2362
|
+
const items = results.filter(Boolean);
|
|
2363
|
+
// 排序:目录在前
|
|
2364
|
+
items.sort((a, b) => {
|
|
2365
|
+
if (a.isDirectory && !b.isDirectory) return -1;
|
|
2366
|
+
if (!a.isDirectory && b.isDirectory) return 1;
|
|
2367
|
+
return a.name.localeCompare(b.name);
|
|
2368
|
+
});
|
|
2369
|
+
|
|
2370
|
+
// 生成 HTML 目录列表
|
|
2371
|
+
const html = this._renderDirectoryHTML(requestPath, items);
|
|
2372
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
2373
|
+
res.send(html);
|
|
2374
|
+
}).catch(() => {
|
|
2375
|
+
res.status(500).send('Internal Server Error');
|
|
2376
|
+
});
|
|
2381
2377
|
});
|
|
2382
2378
|
}
|
|
2383
2379
|
|
|
@@ -2414,6 +2410,11 @@ class Application extends Router {
|
|
|
2414
2410
|
* 处理 WebSocket 升级请求
|
|
2415
2411
|
*/
|
|
2416
2412
|
_handleUpgrade(req, socket, head) {
|
|
2413
|
+
// 防御性检查:_wss 在 listen() 中初始化,正常流程不会为 null
|
|
2414
|
+
if (!this._wss) {
|
|
2415
|
+
socket.destroy();
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2417
2418
|
const ws = this._wss.handleUpgrade(req, socket, head);
|
|
2418
2419
|
if (!ws) return;
|
|
2419
2420
|
|