@lzpong/httpm 1.2.2 → 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 +4 -2
- package/httpm.js +58 -32
- 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,8 +136,8 @@ 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 |
|
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
|
/**
|
|
@@ -1686,10 +1699,8 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
|
|
|
1686
1699
|
let cleanupOnError = false;
|
|
1687
1700
|
let paused = false;
|
|
1688
1701
|
|
|
1689
|
-
//
|
|
1690
|
-
|
|
1691
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
1692
|
-
}
|
|
1702
|
+
// 确保临时目录存在(recursive: true 时目录已存在不报错,无需 existsSync)
|
|
1703
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1693
1704
|
|
|
1694
1705
|
function processBuffer() {
|
|
1695
1706
|
while (buffer.length > 0) {
|
|
@@ -2312,18 +2323,20 @@ class Application extends Router {
|
|
|
2312
2323
|
}
|
|
2313
2324
|
|
|
2314
2325
|
if (stat.isDirectory()) {
|
|
2315
|
-
// 目录:查找 index.html
|
|
2326
|
+
// 目录:查找 index.html(异步检查避免阻塞事件循环)
|
|
2316
2327
|
const indexPath = path.join(fullPath, 'index.html');
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
this.
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
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
|
+
});
|
|
2327
2340
|
return;
|
|
2328
2341
|
}
|
|
2329
2342
|
|
|
@@ -2384,18 +2397,21 @@ class Application extends Router {
|
|
|
2384
2397
|
// requestPath 已去掉前导 /,空字符串表示根目录
|
|
2385
2398
|
// 根目录时显示 '/',否则显示请求路径
|
|
2386
2399
|
const displayPath = requestPath || '/';
|
|
2400
|
+
// 构建链接前缀:确保以 / 开头并以 / 结尾,用于生成绝对路径 href
|
|
2401
|
+
const prefix = '/' + (requestPath ? requestPath.replace(/\/+$/, '') + '/' : '');
|
|
2387
2402
|
let html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Directory: ${escapeHtml(displayPath)}</title>`;
|
|
2388
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>`;
|
|
2389
2404
|
html += `</head><body><h1>Directory: ${escapeHtml(displayPath)}</h1><table><tr><th>Name</th><th>Size</th><th>Modified</th></tr>`;
|
|
2390
2405
|
|
|
2391
|
-
//
|
|
2406
|
+
// 父目录链接:使用绝对路径返回上一级(根目录时不显示)
|
|
2392
2407
|
if (requestPath && requestPath !== '/') {
|
|
2393
|
-
|
|
2408
|
+
const parentHref = prefix + '..';
|
|
2409
|
+
html += `<tr><td><a href="${escapeHtml(parentHref)}" class="dir">../</a></td><td class="size">-</td><td>-</td></tr>`;
|
|
2394
2410
|
}
|
|
2395
2411
|
|
|
2396
2412
|
for (const item of items) {
|
|
2397
|
-
//
|
|
2398
|
-
const href = item.name;
|
|
2413
|
+
// 使用绝对路径 href,避免 URL 缺少尾部斜杠时解析错误
|
|
2414
|
+
const href = prefix + item.name;
|
|
2399
2415
|
const name = item.isDirectory ? item.name + '/' : item.name;
|
|
2400
2416
|
const size = item.isDirectory ? '-' : fmtSize(item.size);
|
|
2401
2417
|
const cls = item.isDirectory ? 'dir' : '';
|
|
@@ -2412,6 +2428,13 @@ class Application extends Router {
|
|
|
2412
2428
|
_handleUpgrade(req, socket, head) {
|
|
2413
2429
|
// 防御性检查:_wss 在 listen() 中初始化,正常流程不会为 null
|
|
2414
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');
|
|
2415
2438
|
socket.destroy();
|
|
2416
2439
|
return;
|
|
2417
2440
|
}
|
|
@@ -2600,13 +2623,16 @@ function staticMiddleware(rootPath, options = {}) {
|
|
|
2600
2623
|
return next();
|
|
2601
2624
|
}
|
|
2602
2625
|
if (stat.isDirectory()) {
|
|
2603
|
-
// 目录:尝试 index.html
|
|
2626
|
+
// 目录:尝试 index.html(异步检查避免阻塞事件循环)
|
|
2604
2627
|
const indexPath = path.join(fullPath, 'index.html');
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
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;
|
|
2610
2636
|
}
|
|
2611
2637
|
res.sendFile(requestPath, { root });
|
|
2612
2638
|
});
|