@lzpong/httpm 1.2.0 → 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 +20 -2
- package/httpm.js +58 -58
- 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 实例 |
|
|
@@ -224,6 +225,7 @@ app.ws('/chat/:room', (ws, req) => {
|
|
|
224
225
|
```javascript
|
|
225
226
|
app.wss.broadcast('/chat', 'Hello everyone'); // 按路径分组广播
|
|
226
227
|
app.wss.broadcast('/chat', 'Hello', ws); // 排除指定连接(传 ws 对象)
|
|
228
|
+
app.wss.broadcastAll('Hello everyone'); // 全局广播
|
|
227
229
|
app.wss.getConnections(); // 获取所有连接
|
|
228
230
|
```
|
|
229
231
|
|
|
@@ -237,6 +239,13 @@ app.wss.getConnections(); // 获取所有连接
|
|
|
237
239
|
| `close` | 连接关闭,回调参数 `(code, reason)` |
|
|
238
240
|
| `error` | 连接错误 |
|
|
239
241
|
|
|
242
|
+
### WebSocket 方法
|
|
243
|
+
|
|
244
|
+
| 方法 | 说明 |
|
|
245
|
+
|------|------|
|
|
246
|
+
| `ws.send(data)` | 发送消息(自动区分文本/JSON/二进制) |
|
|
247
|
+
| `ws.close(code, reason)` | 关闭连接,可选状态码和原因 |
|
|
248
|
+
|
|
240
249
|
### data 事件使用
|
|
241
250
|
|
|
242
251
|
```javascript
|
|
@@ -382,6 +391,15 @@ const app = httpm({
|
|
|
382
391
|
});
|
|
383
392
|
```
|
|
384
393
|
|
|
394
|
+
## 关闭服务
|
|
395
|
+
|
|
396
|
+
```javascript
|
|
397
|
+
// 关闭 HTTP/HTTPS 服务器并断开所有 WebSocket 连接
|
|
398
|
+
app.close(() => {
|
|
399
|
+
console.log('Server closed');
|
|
400
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
385
403
|
## 导出接口
|
|
386
404
|
|
|
387
405
|
```javascript
|
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);
|
|
@@ -2191,6 +2185,8 @@ class Application extends Router {
|
|
|
2191
2185
|
* 错误处理:查找错误处理中间件(4 个参数)
|
|
2192
2186
|
*/
|
|
2193
2187
|
_handleError(err, req, res, stack, startIdx) {
|
|
2188
|
+
// 无错误时直接返回(错误处理中间件调用 next() 无参数表示错误已处理)
|
|
2189
|
+
if (!err) return;
|
|
2194
2190
|
// 从当前栈中查找错误处理中间件
|
|
2195
2191
|
for (let i = startIdx; i < stack.length; i++) {
|
|
2196
2192
|
const handler = stack[i].handler;
|
|
@@ -2347,35 +2343,37 @@ class Application extends Router {
|
|
|
2347
2343
|
return;
|
|
2348
2344
|
}
|
|
2349
2345
|
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
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 => ({
|
|
2358
2353
|
name: entry.name,
|
|
2359
2354
|
isDirectory: false,
|
|
2360
2355
|
size: stat.size,
|
|
2361
2356
|
modified: stat.mtime.toISOString()
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
return null;
|
|
2365
|
-
}
|
|
2366
|
-
}).filter(Boolean);
|
|
2367
|
-
|
|
2368
|
-
// 排序:目录在前
|
|
2369
|
-
items.sort((a, b) => {
|
|
2370
|
-
if (a.isDirectory && !b.isDirectory) return -1;
|
|
2371
|
-
if (!a.isDirectory && b.isDirectory) return 1;
|
|
2372
|
-
return a.name.localeCompare(b.name);
|
|
2357
|
+
}))
|
|
2358
|
+
.catch(() => null);
|
|
2373
2359
|
});
|
|
2374
2360
|
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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
|
+
});
|
|
2379
2377
|
});
|
|
2380
2378
|
}
|
|
2381
2379
|
|
|
@@ -2384,23 +2382,20 @@ class Application extends Router {
|
|
|
2384
2382
|
*/
|
|
2385
2383
|
_renderDirectoryHTML(requestPath, items) {
|
|
2386
2384
|
// requestPath 已去掉前导 /,空字符串表示根目录
|
|
2387
|
-
// path.dirname('subdir') 返回 '.',应映射为根目录 ''
|
|
2388
|
-
let parentPath = requestPath ? path.dirname(requestPath) : '/';
|
|
2389
|
-
if (parentPath === '.') parentPath = '';
|
|
2390
2385
|
// 根目录时显示 '/',否则显示请求路径
|
|
2391
2386
|
const displayPath = requestPath || '/';
|
|
2392
2387
|
let html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Directory: ${escapeHtml(displayPath)}</title>`;
|
|
2393
2388
|
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>`;
|
|
2394
2389
|
html += `</head><body><h1>Directory: ${escapeHtml(displayPath)}</h1><table><tr><th>Name</th><th>Size</th><th>Modified</th></tr>`;
|
|
2395
2390
|
|
|
2396
|
-
//
|
|
2391
|
+
// 父目录链接:使用相对路径 .. 返回上一级(根目录时不显示)
|
|
2397
2392
|
if (requestPath && requestPath !== '/') {
|
|
2398
|
-
html += `<tr><td><a href="
|
|
2393
|
+
html += `<tr><td><a href=".." class="dir">../</a></td><td class="size">-</td><td>-</td></tr>`;
|
|
2399
2394
|
}
|
|
2400
2395
|
|
|
2401
2396
|
for (const item of items) {
|
|
2402
|
-
//
|
|
2403
|
-
const href =
|
|
2397
|
+
// 仅使用文件名作为相对 href,页面 URL 本身已包含目录路径
|
|
2398
|
+
const href = item.name;
|
|
2404
2399
|
const name = item.isDirectory ? item.name + '/' : item.name;
|
|
2405
2400
|
const size = item.isDirectory ? '-' : fmtSize(item.size);
|
|
2406
2401
|
const cls = item.isDirectory ? 'dir' : '';
|
|
@@ -2415,6 +2410,11 @@ class Application extends Router {
|
|
|
2415
2410
|
* 处理 WebSocket 升级请求
|
|
2416
2411
|
*/
|
|
2417
2412
|
_handleUpgrade(req, socket, head) {
|
|
2413
|
+
// 防御性检查:_wss 在 listen() 中初始化,正常流程不会为 null
|
|
2414
|
+
if (!this._wss) {
|
|
2415
|
+
socket.destroy();
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
2418
|
const ws = this._wss.handleUpgrade(req, socket, head);
|
|
2419
2419
|
if (!ws) return;
|
|
2420
2420
|
|