@lzpong/httpm 1.0.3 → 1.2.0

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.
Files changed (4) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +33 -24
  3. package/httpm.js +702 -253
  4. package/package.json +2 -2
package/httpm.js CHANGED
@@ -2,11 +2,11 @@
2
2
  * httpm - 基于 Node.js 原生模块的单文件、零依赖 HTTP 服务库
3
3
  *
4
4
  * @name httpm
5
- * @version 1.0.0
5
+ * @version 1.2.0
6
6
  * @description 兼容 Express API,内置路由、中间件、静态文件服务、
7
7
  * WebSocket、SSE、流式上传、日志系统等功能
8
8
  * @license MIT
9
- * @requires node >= 14.0.0
9
+ * @requires node >= 18.0.0
10
10
  * @module httpm
11
11
  * @author lzpong
12
12
  * @link https://gitee.com/lzpong/httpm
@@ -41,24 +41,44 @@ const zlib = require('zlib');
41
41
  * 解析 URL,拆分路径与 Query 参数
42
42
  */
43
43
  function parseUrl(urlStr) {
44
+ const hashIdx = urlStr.indexOf('#');
45
+ if (hashIdx !== -1) {
46
+ urlStr = urlStr.substring(0, hashIdx);
47
+ }
44
48
  const qIdx = urlStr.indexOf('?');
45
49
  if (qIdx === -1) {
46
50
  return { pathname: urlStr, query: {} };
47
51
  }
48
- const pathname = urlStr.substring(0, qIdx);
49
- const qs = urlStr.substring(qIdx + 1);
52
+ return { pathname: urlStr.substring(0, qIdx), query: _parseQueryString(urlStr.substring(qIdx + 1)) };
53
+ }
54
+
55
+ /**
56
+ * 解析查询字符串为键值对象
57
+ */
58
+ function _parseQueryString(qs, plusAsSpace = false) {
50
59
  const query = {};
51
- if (qs) {
52
- qs.split('&').forEach(pair => {
53
- const eIdx = pair.indexOf('=');
54
- if (eIdx === -1) {
55
- query[decodeURIComponent(pair)] = '';
56
- } else {
57
- query[decodeURIComponent(pair.substring(0, eIdx))] = decodeURIComponent(pair.substring(eIdx + 1));
60
+ if (!qs) return query;
61
+ qs.split('&').forEach(pair => {
62
+ const eIdx = pair.indexOf('=');
63
+ if (eIdx === -1) {
64
+ try {
65
+ query[decodeURIComponent(plusAsSpace ? pair.replace(/\+/g, ' ') : pair)] = '';
66
+ } catch (e) {
67
+ // 非法 URI 编码,保留原始值
68
+ query[plusAsSpace ? pair.replace(/\+/g, ' ') : pair] = '';
58
69
  }
59
- });
60
- }
61
- return { pathname, query };
70
+ } else {
71
+ const key = pair.substring(0, eIdx);
72
+ const val = pair.substring(eIdx + 1);
73
+ try {
74
+ query[decodeURIComponent(plusAsSpace ? key.replace(/\+/g, ' ') : key)] = decodeURIComponent(plusAsSpace ? val.replace(/\+/g, ' ') : val);
75
+ } catch (e) {
76
+ // 非法 URI 编码,保留原始值
77
+ query[plusAsSpace ? key.replace(/\+/g, ' ') : key] = plusAsSpace ? val.replace(/\+/g, ' ') : val;
78
+ }
79
+ }
80
+ });
81
+ return query;
62
82
  }
63
83
 
64
84
  /**
@@ -77,6 +97,13 @@ function parseCookies(cookieStr) {
77
97
  return cookies;
78
98
  }
79
99
 
100
+ /**
101
+ * HTML 实体转义,防止 XSS
102
+ */
103
+ function escapeHtml(str) {
104
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
105
+ }
106
+
80
107
  /**
81
108
  * MIME 类型映射表
82
109
  */
@@ -146,10 +173,14 @@ function getMimeType(ext) {
146
173
  * 字节单位格式化(B/KB/MB/GB)
147
174
  */
148
175
  function fmtSize(bytes) {
176
+ if (!Number.isFinite(bytes) || bytes < 0) return '0B';
149
177
  if (bytes === 0) return '0B';
150
178
  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
151
179
  const i = Math.floor(Math.log(bytes) / Math.log(1024));
152
- return (bytes / Math.pow(1024, i)).toFixed(2) + units[i];
180
+ const value = bytes / (1024 ** i);
181
+ // 小于 10 时保留 2 位小数,否则保留 1 位,整数不显示小数
182
+ const formatted = value < 10 ? value.toFixed(2) : (value === Math.floor(value) ? value.toString() : value.toFixed(1));
183
+ return formatted + units[i];
153
184
  }
154
185
 
155
186
  /**
@@ -164,19 +195,19 @@ function fmtTime(ms) {
164
195
  /**
165
196
  * 路径安全校验,防遍历攻击
166
197
  */
167
- function isPathSafe(requestPath, rootDir) {
168
- // 去掉前导斜杠,防止 path.resolve 将其视为绝对路径
198
+ function isPathSafe(requestPath, rootDir, allowAllFiles = false) {
169
199
  const normalized = requestPath.replace(/^\/+/, '');
170
200
  const resolved = path.resolve(rootDir, normalized);
171
201
  const root = path.resolve(rootDir);
172
- // 确保解析后的路径在根目录内
173
202
  if (!resolved.startsWith(root + path.sep) && resolved !== root) {
174
203
  return false;
175
204
  }
176
- // 禁止访问隐藏文件/目录(以 . 开头)
177
- const parts = normalized.split(/[/\\]/);
178
- for (const part of parts) {
179
- if (part.startsWith('.')) return false;
205
+ // allowAllFiles=true 时允许访问所有文件(包括 .env、.git 等隐藏文件)
206
+ if (!allowAllFiles) {
207
+ const parts = normalized.split(/[/\\]/);
208
+ for (const part of parts) {
209
+ if (part.startsWith('.')) return false;
210
+ }
180
211
  }
181
212
  return true;
182
213
  }
@@ -240,13 +271,14 @@ class Logger {
240
271
  this.name = options.name || '';
241
272
  this.level = options.level || 'info';
242
273
  this.logDir = options.logDir || './log';
243
- this._levels = { debug: 0, info: 1, notice: 2, warn: 3, error: 4 };
274
+ this._levels = { debug: 0, info: 1, notice: 2, warn: 3, error: 4, fatal: 5 };
244
275
  this._colors = {
245
276
  debug: '\x1b[1;30m', // 灰色
246
277
  info: '\x1b[1;37m', // 白色
247
278
  notice: '\x1b[1;35m', // 品红
248
279
  warn: '\x1b[1;33m', // 黄色
249
- error: '\x1b[1;31m' // 红色
280
+ error: '\x1b[1;31m', // 红色
281
+ fatal: '\x1b[1;31;1m' // 红色加粗
250
282
  };
251
283
  this._reset = '\x1b[0m';
252
284
  this._stream = null;
@@ -269,23 +301,24 @@ class Logger {
269
301
  const year = now.getFullYear().toString();
270
302
  const month = (now.getMonth() + 1).toString().padStart(2, '0');
271
303
  const day = now.getDate().toString().padStart(2, '0');
272
- const dateStr = `${day}`;
273
304
 
274
305
  // 同一天复用同一个流
275
306
  const streamKey = `${year}-${month}-${day}`;
276
307
  if (this._stream && this._streamDate === streamKey) {
277
308
  return this._stream;
278
309
  }
279
- // 关闭旧流
310
+ // 关闭旧流(等待写入完成后再销毁,避免跨日切换时丢失日志)
311
+ // this._stream 已置 null,新日志将写入新流,旧流异步刷盘不会与新流冲突
280
312
  if (this._stream) {
281
- this._stream.end();
313
+ const oldStream = this._stream;
314
+ oldStream.end(() => {
315
+ oldStream.destroy();
316
+ });
282
317
  this._stream = null;
283
318
  }
284
- // 创建日志目录: ./log/YYYY/MM/
319
+ // 创建日志目录: ./log/YYYY/MM/(recursive: true 自动处理已存在的情况)
285
320
  const dir = path.join(this.logDir, year, month);
286
- if (!fs.existsSync(dir)) {
287
- fs.mkdirSync(dir, { recursive: true });
288
- }
321
+ fs.mkdirSync(dir, { recursive: true });
289
322
  // 文件名: name_DD.log 或 DD.log
290
323
  const prefix = this.name ? this.name + '_' : '';
291
324
  const filePath = path.join(dir, `${prefix}${day}.log`);
@@ -321,6 +354,7 @@ class Logger {
321
354
  notice(...args) { this.log('notice', ...args); }
322
355
  warn(...args) { this.log('warn', ...args); }
323
356
  error(...args) { this.log('error', ...args); }
357
+ fatal(...args) { this.log('fatal', ...args); }
324
358
  }
325
359
 
326
360
  // ============================================================
@@ -363,10 +397,6 @@ class Router {
363
397
  if (this.routes[key]) {
364
398
  this.routes[key].push(route);
365
399
  }
366
- // 同时注册到 ALL
367
- if (key !== 'ALL') {
368
- // ALL 路由单独处理,不在此处添加
369
- }
370
400
  return this;
371
401
  }
372
402
 
@@ -395,18 +425,30 @@ class Router {
395
425
  const results = [];
396
426
  const m = method.toUpperCase();
397
427
 
428
+ // HEAD 请求同时匹配 GET 路由(Express 兼容行为)
429
+ const methods = [m];
430
+ if (m === 'HEAD') methods.push('GET');
431
+
398
432
  // 检查 ALL 路由
399
433
  const allRoutes = this.routes['ALL'] || [];
400
434
  // 检查对应方法路由
401
- const methodRoutes = this.routes[m] || [];
402
- const candidates = [...allRoutes, ...methodRoutes];
435
+ let methodRoutes = [...allRoutes];
436
+ for (const meth of methods) {
437
+ const routes = this.routes[meth] || [];
438
+ methodRoutes = methodRoutes.concat(routes);
439
+ }
403
440
 
404
- for (const route of candidates) {
441
+ for (const route of methodRoutes) {
405
442
  const match = route.pattern.exec(pathname);
406
443
  if (match) {
407
444
  const params = {};
408
445
  route.params.forEach((name, i) => {
409
- params[name] = decodeURIComponent(match[i + 1]);
446
+ try {
447
+ params[name] = decodeURIComponent(match[i + 1]);
448
+ } catch (e) {
449
+ // 非法 URI 编码,保留原始值
450
+ params[name] = match[i + 1];
451
+ }
410
452
  });
411
453
  results.push({ route, params, handlers: route.handlers });
412
454
  }
@@ -434,7 +476,12 @@ class Router {
434
476
  const match = mw.pattern.exec(pathname);
435
477
  if (match) {
436
478
  mw.params.forEach((name, i) => {
437
- params[name] = decodeURIComponent(match[i + 1]);
479
+ try {
480
+ params[name] = decodeURIComponent(match[i + 1]);
481
+ } catch (e) {
482
+ // 非法 URI 编码,保留原始值
483
+ params[name] = match[i + 1];
484
+ }
438
485
  });
439
486
  }
440
487
  }
@@ -463,6 +510,8 @@ class Router {
463
510
  class Request {
464
511
  constructor(incomingMessage) {
465
512
  this._req = incomingMessage;
513
+ this._app = null;
514
+ this._res = null;
466
515
  this.query = {};
467
516
  this.params = {};
468
517
  this.body = null;
@@ -494,22 +543,61 @@ class Request {
494
543
  }
495
544
 
496
545
  /**
497
- * 读取请求体原始数据
546
+ * 获取请求头(Express 兼容),不区分大小写
547
+ * req.get('Content-Type') / req.get('content-type')
548
+ */
549
+ get(name) {
550
+ if (!name) return undefined;
551
+ const lower = name.toLowerCase();
552
+ // 特殊别名
553
+ if (lower === 'referrer' || lower === 'referer') {
554
+ return this._req.headers['referer'] || this._req.headers['referrer'];
555
+ }
556
+ return this._req.headers[lower];
557
+ }
558
+
559
+ /**
560
+ * 读取请求体原始数据(带超时保护和大小限制)
498
561
  */
499
- _readBody() {
562
+ _readBody(timeoutMs = 30000, maxSize = null) {
500
563
  return new Promise((resolve, reject) => {
501
564
  if (this._bodyParsed) {
502
565
  resolve(this._rawBody);
503
566
  return;
504
567
  }
505
568
  const chunks = [];
506
- this._req.on('data', chunk => chunks.push(chunk));
569
+ let totalSize = 0;
570
+ const limit = maxSize || (this._app?.settings?.maxBodySize) || 128 * 1024 * 1024;
571
+ let timedOut = false;
572
+ // 超时定时器
573
+ const timer = setTimeout(() => {
574
+ timedOut = true;
575
+ this._req.destroy();
576
+ reject(new Error('Request body read timeout', { cause: { timeoutMs } }));
577
+ }, timeoutMs);
578
+ this._req.on('data', chunk => {
579
+ totalSize += chunk.length;
580
+ // 流式大小检查,超限时立即中断
581
+ if (totalSize > limit) {
582
+ clearTimeout(timer);
583
+ this._req.destroy();
584
+ const err = new Error(`Request body exceeds maximum size of ${fmtSize(limit)}`, { cause: { actual: totalSize, maxSize: limit } });
585
+ err.status = 413;
586
+ reject(err);
587
+ return;
588
+ }
589
+ chunks.push(chunk);
590
+ });
507
591
  this._req.on('end', () => {
592
+ clearTimeout(timer);
508
593
  this._rawBody = Buffer.concat(chunks);
509
594
  this._bodyParsed = true;
510
595
  resolve(this._rawBody);
511
596
  });
512
- this._req.on('error', reject);
597
+ this._req.on('error', (err) => {
598
+ clearTimeout(timer);
599
+ if (!timedOut) reject(err);
600
+ });
513
601
  });
514
602
  }
515
603
  }
@@ -524,6 +612,7 @@ class Response {
524
612
  this._app = app;
525
613
  this.statusCode = 200;
526
614
  this._headersSent = false;
615
+ this._isHead = false;
527
616
  this._sse = null;
528
617
  }
529
618
 
@@ -545,6 +634,59 @@ class Response {
545
634
  return this;
546
635
  }
547
636
 
637
+ /**
638
+ * 设置响应头(Express 兼容),支持单键和对象批量设置
639
+ * res.set('Content-Type', 'text/html')
640
+ * res.set({ 'Content-Type': 'text/html', 'X-Custom': 'value' })
641
+ */
642
+ set(name, value) {
643
+ if (typeof name === 'object') {
644
+ for (const key of Object.keys(name)) {
645
+ this._res.setHeader(key, name[key]);
646
+ }
647
+ } else {
648
+ this._res.setHeader(name, value);
649
+ }
650
+ return this;
651
+ }
652
+
653
+ /**
654
+ * 设置 Content-Type(Express 兼容),支持简写
655
+ * res.type('html') → text/html
656
+ * res.type('.html') → text/html
657
+ * res.type('text/html') → text/html
658
+ */
659
+ type(contentType) {
660
+ // 已是完整 MIME 类型,直接设置
661
+ if (contentType.includes('/')) {
662
+ this.setHeader('Content-Type', contentType);
663
+ return this;
664
+ }
665
+ // 简写或带点号扩展名,通过 getMimeType 解析
666
+ const ext = contentType.startsWith('.') ? contentType : '.' + contentType;
667
+ const mime = getMimeType(ext);
668
+ this.setHeader('Content-Type', mime);
669
+ return this;
670
+ }
671
+
672
+ /**
673
+ * 代理原生 ServerResponse 事件监听(Express 兼容)
674
+ * res.on('finish', fn) / res.on('close', fn)
675
+ */
676
+ on(event, listener) {
677
+ this._res.on(event, listener);
678
+ return this;
679
+ }
680
+
681
+ /**
682
+ * 获取响应头(Express 兼容),不区分大小写
683
+ * res.get('Content-Type')
684
+ */
685
+ get(name) {
686
+ if (!name) return undefined;
687
+ return this._res.getHeader(name);
688
+ }
689
+
548
690
  /**
549
691
  * 设置 HTTP 状态码,支持链式调用
550
692
  */
@@ -567,11 +709,18 @@ class Response {
567
709
  * 通用输出,支持字符串、HTML、Buffer、对象
568
710
  */
569
711
  send(data) {
570
- if (data === undefined || data === null) {
712
+ // Express 兼容:null 序列化为 "null",undefined 返回空响应
713
+ if (data === undefined) {
571
714
  this.setHeader('Content-Length', 0);
572
715
  this._send('');
573
716
  return;
574
717
  }
718
+ if (data === null) {
719
+ this.setHeader('Content-Type', 'text/html; charset=utf-8');
720
+ this.setHeader('Content-Length', 4);
721
+ this._send('null');
722
+ return;
723
+ }
575
724
  if (Buffer.isBuffer(data)) {
576
725
  this.setHeader('Content-Type', 'application/octet-stream');
577
726
  this.setHeader('Content-Length', data.length);
@@ -602,7 +751,12 @@ class Response {
602
751
  if (this._res.finished) return;
603
752
  this._res.statusCode = this.statusCode;
604
753
  this._headersSent = true;
605
- this._res.end(data);
754
+ // HEAD 请求只发送头部,不发送响应体
755
+ if (this._isHead) {
756
+ this._res.end();
757
+ } else {
758
+ this._res.end(data);
759
+ }
606
760
  }
607
761
 
608
762
  /**
@@ -635,7 +789,10 @@ class Response {
635
789
  const ifNoneMatch = this._req?.headers?.['if-none-match'];
636
790
  const ifModifiedSince = this._req?.headers?.['if-modified-since'];
637
791
  if (ifNoneMatch === etag || (ifModifiedSince && new Date(ifModifiedSince) >= stat.mtime)) {
638
- this.status(304)._send('');
792
+ this.status(304);
793
+ this.removeHeader('Content-Length');
794
+ this._res.statusCode = 304;
795
+ this._res.end();
639
796
  return;
640
797
  }
641
798
  }
@@ -648,7 +805,7 @@ class Response {
648
805
  this.status(206);
649
806
  this.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${range.total}`);
650
807
  this.setHeader('Content-Length', range.end - range.start + 1);
651
- this._streamFile(fullPath, range.start, range.end, stat.size);
808
+ this._streamFile(fullPath, range.start, range.end);
652
809
  return;
653
810
  }
654
811
  }
@@ -658,50 +815,57 @@ class Response {
658
815
  // Gzip 压缩(仅文本类文件)
659
816
  const acceptEncoding = this._req?.headers?.['accept-encoding'] || '';
660
817
  if (this._app.settings.enableGzip && isTextMime(mime) && acceptEncoding.includes('gzip')) {
818
+ // HEAD 请求不传输内容
819
+ if (this._isHead) {
820
+ this._send('');
821
+ return;
822
+ }
661
823
  this.removeHeader('Content-Length');
662
824
  this.setHeader('Content-Encoding', 'gzip');
663
- this._res.statusCode = this.statusCode;
825
+ this.status(this.statusCode);
664
826
  this._headersSent = true;
665
827
  const raw = fs.createReadStream(fullPath);
666
828
  const gzip = zlib.createGzip();
829
+ // 流错误处理:文件读取或压缩出错时返回 500
830
+ const onError = (err) => {
831
+ raw.destroy();
832
+ gzip.destroy();
833
+ if (!this._res.finished) {
834
+ this._res.statusCode = 500;
835
+ this._res.end('Internal Server Error');
836
+ }
837
+ };
838
+ raw.on('error', onError);
839
+ gzip.on('error', onError);
667
840
  raw.pipe(gzip).pipe(this._res);
668
841
  return;
669
842
  }
670
843
 
671
- this._streamFile(fullPath, 0, stat.size - 1, stat.size);
844
+ this._streamFile(fullPath, 0, stat.size - 1);
672
845
  });
673
846
  }
674
847
 
675
848
  /**
676
849
  * 流式发送文件
677
850
  */
678
- _streamFile(fullPath, start, end, total) {
851
+ _streamFile(fullPath, start, end) {
679
852
  if (this._res.finished) return;
680
853
  this._res.statusCode = this.statusCode;
681
854
  this._headersSent = true;
682
- const stream = fs.createReadStream(fullPath, { start, end });
683
- // 大文件进度展示(>1MB)
684
- if (total > 1024 * 1024) {
685
- const name = path.basename(fullPath);
686
- const totalSize = end - start + 1;
687
- let sent = 0;
688
- const startTime = Date.now();
689
- let lastLine = '';
690
- stream.on('data', (chunk) => {
691
- sent += chunk.length;
692
- const pct = ((sent / totalSize) * 100).toFixed(1);
693
- const elapsed = Date.now() - startTime;
694
- const speed = elapsed > 0 ? sent / (elapsed / 1000) : 0;
695
- const line = `\r[${name}] ${fmtSize(sent)}/${fmtSize(totalSize)} ${pct}% ${fmtSize(speed)}/s ${fmtTime(elapsed)}`;
696
- if (line !== lastLine) {
697
- process.stdout.write(line);
698
- lastLine = line;
699
- }
700
- });
701
- stream.on('end', () => {
702
- if (sent > 0) process.stdout.write('\n');
703
- });
855
+ // HEAD 请求只发送头部,不传输文件内容
856
+ if (this._isHead) {
857
+ this._res.end();
858
+ return;
704
859
  }
860
+ const stream = fs.createReadStream(fullPath, { start, end });
861
+ // 流错误处理:文件读取出错时返回 500
862
+ stream.on('error', (err) => {
863
+ stream.destroy();
864
+ if (!this._res.finished) {
865
+ this._res.statusCode = 500;
866
+ this._res.end('Internal Server Error');
867
+ }
868
+ });
705
869
  stream.pipe(this._res);
706
870
  }
707
871
 
@@ -715,11 +879,18 @@ class Response {
715
879
  }
716
880
 
717
881
  /**
718
- * 重定向响应
882
+ * 重定向响应(兼容 Express: redirect(status, url) 或 redirect(url))
719
883
  */
720
- redirect(url, code = 302) {
721
- this.status(code);
722
- this.setHeader('Location', url);
884
+ redirect(...args) {
885
+ if (typeof args[0] === 'number') {
886
+ // redirect(status, url)
887
+ this.status(args[0]);
888
+ this.setHeader('Location', args[1]);
889
+ } else {
890
+ // redirect(url) 默认 302
891
+ this.status(302);
892
+ this.setHeader('Location', args[0]);
893
+ }
723
894
  this.setHeader('Content-Length', 0);
724
895
  this._send('');
725
896
  }
@@ -737,16 +908,16 @@ class Response {
737
908
  * 设置响应 Cookie
738
909
  */
739
910
  cookie(name, value, opts = {}) {
740
- let cookieValue = value;
741
- // 签名 Cookie:s:value.signature
911
+ let encodedValue = encodeURIComponent(value);
912
+ // 签名 Cookie:s:value.signature(s: 前缀不参与编码)
742
913
  if (opts.signed) {
743
914
  const secret = this._app && this._app.settings && this._app.settings.cookieParserSecret;
744
915
  if (secret) {
745
916
  const sig = crypto.createHmac('sha256', secret).update(value).digest('base64').replace(/=+$/, '');
746
- cookieValue = 's:' + value + '.' + sig;
917
+ encodedValue = 's:' + encodedValue + '.' + sig;
747
918
  }
748
919
  }
749
- let str = `${encodeURIComponent(name)}=${encodeURIComponent(cookieValue)}`;
920
+ let str = `${encodeURIComponent(name)}=${encodedValue}`;
750
921
  if (opts.maxAge !== undefined) str += `; Max-Age=${opts.maxAge}`;
751
922
  if (opts.domain) str += `; Domain=${opts.domain}`;
752
923
  if (opts.path) str += `; Path=${opts.path}`;
@@ -778,18 +949,24 @@ class SSE {
778
949
  this._res = res;
779
950
  this.connected = true;
780
951
 
781
- // 设置 SSE 标准响应头
782
- res.writeHead(200, {
783
- 'Content-Type': 'text/event-stream',
784
- 'Cache-Control': 'no-cache',
785
- 'Connection': 'keep-alive',
786
- 'Access-Control-Allow-Origin': '*'
787
- });
952
+ // 设置 SSE 标准响应头(仅在 headers 未发送时)
953
+ if (!res.headersSent) {
954
+ res.writeHead(200, {
955
+ 'Content-Type': 'text/event-stream',
956
+ 'Cache-Control': 'no-cache',
957
+ 'Connection': 'keep-alive',
958
+ 'Access-Control-Allow-Origin': '*'
959
+ });
960
+ }
788
961
 
789
- // 监听连接关闭
790
- res.on('close', () => {
962
+ // 监听连接关闭(兼容 HTTP/1.1 和 HTTP/2)
963
+ const onClose = () => {
791
964
  this.connected = false;
792
- });
965
+ };
966
+ this._onClose = onClose;
967
+ res.on('close', onClose);
968
+ // HTTP/1.1 兼容:aborted 事件在请求被客户端中断时触发
969
+ res.on('aborted', onClose);
793
970
  }
794
971
 
795
972
  /**
@@ -831,11 +1008,13 @@ class SSE {
831
1008
  }
832
1009
 
833
1010
  /**
834
- * 主动关闭 SSE 连接
1011
+ * 主动关闭 SSE 连接,移除监听器防止内存泄漏
835
1012
  */
836
1013
  close() {
837
1014
  if (!this.connected) return;
838
1015
  this.connected = false;
1016
+ this._res.removeListener('close', this._onClose);
1017
+ this._res.removeListener('aborted', this._onClose);
839
1018
  this._res.end();
840
1019
  }
841
1020
  }
@@ -844,14 +1023,41 @@ class SSE {
844
1023
  // WebSocket 类
845
1024
  // ============================================================
846
1025
 
1026
+ /**
1027
+ * 通用事件触发函数(WebSocket 和 WebSocketServer 共用)
1028
+ */
1029
+ function _emitEvent(handlers, event, args) {
1030
+ const list = handlers[event];
1031
+ if (list) {
1032
+ list.forEach(h => {
1033
+ try { h(...args); } catch (e) { console.error(`[httpm] Event handler error on "${event}":`, e); }
1034
+ });
1035
+ }
1036
+ }
1037
+
847
1038
  class WebSocket {
848
- constructor(socket, pathStr) {
1039
+ constructor(socket, pathStr, options = {}) {
849
1040
  this.socket = socket;
850
1041
  this.path = pathStr;
851
1042
  this.id = uid();
852
1043
  this.connected = true;
853
1044
  this._lastHeartbeat = Date.now();
854
1045
  this._handlers = {};
1046
+ // 最大帧负载大小(默认 100MB,防止恶意超大帧耗尽内存)
1047
+ this._maxPayload = options.maxPayload || 100 * 1024 * 1024;
1048
+ // 写入背压标记
1049
+ this._writeBacklogged = false;
1050
+ // 帧解析状态:缓存不完整帧数据
1051
+ this._frameBuffer = Buffer.alloc(0);
1052
+ // 分片帧状态
1053
+ this._fragmented = false;
1054
+ this._fragmentOpcode = 0;
1055
+ this._fragmentPayloads = [];
1056
+ // 防止 close 事件重复触发
1057
+ this._closed = false;
1058
+ // 关闭握手状态
1059
+ this._closing = false;
1060
+ this._closeTimer = null;
855
1061
 
856
1062
  // 监听底层 Pong 帧
857
1063
  socket.on('pong', () => {
@@ -861,28 +1067,43 @@ class WebSocket {
861
1067
  // 监听连接关闭
862
1068
  socket.on('close', () => {
863
1069
  this.connected = false;
864
- this._emit('close');
1070
+ this._emitClose();
865
1071
  });
866
1072
 
867
1073
  // 监听数据帧
868
1074
  socket.on('data', (data) => {
869
- this._handleFrame(data);
1075
+ this._frameBuffer = Buffer.concat([this._frameBuffer, data]);
1076
+ this._parseFrames();
870
1077
  });
871
1078
 
872
- socket.on('error', () => {
1079
+ // 监听底层错误,传递错误对象
1080
+ socket.on('error', (err) => {
873
1081
  this.connected = false;
874
- this._emit('error');
1082
+ this._emit('error', err);
875
1083
  });
876
1084
  }
877
1085
 
878
1086
  /**
879
- * 解析 WebSocket 帧
1087
+ * 循环解析帧缓冲区,处理多帧/半帧
880
1088
  */
881
- _handleFrame(data) {
882
- if (data.length < 2) return;
1089
+ _parseFrames() {
1090
+ while (this._frameBuffer.length >= 2) {
1091
+ const result = this._decodeFrame(this._frameBuffer);
1092
+ if (result === null) break; // 数据不完整,等待更多数据
1093
+ this._frameBuffer = this._frameBuffer.slice(result.bytesConsumed);
1094
+ this._processFrame(result.opcode, result.payload, result.fin, result.oversize);
1095
+ }
1096
+ }
883
1097
 
884
- const firstByte = data[0];
885
- const secondByte = data[1];
1098
+ /**
1099
+ * 从缓冲区解码一个帧,返回 { opcode, payload, fin, bytesConsumed } 或 null(数据不完整)
1100
+ */
1101
+ _decodeFrame(buf) {
1102
+ if (buf.length < 2) return null;
1103
+
1104
+ const firstByte = buf[0];
1105
+ const secondByte = buf[1];
1106
+ const fin = (firstByte & 0x80) !== 0;
886
1107
  const opcode = firstByte & 0x0F;
887
1108
  const isMasked = (secondByte & 0x80) !== 0;
888
1109
  let payloadLength = secondByte & 0x7F;
@@ -890,51 +1111,132 @@ class WebSocket {
890
1111
 
891
1112
  // 解析长度
892
1113
  if (payloadLength === 126) {
893
- payloadLength = data.readUInt16BE(offset);
1114
+ if (buf.length < 4) return null;
1115
+ payloadLength = buf.readUInt16BE(offset);
894
1116
  offset += 2;
895
1117
  } else if (payloadLength === 127) {
896
- payloadLength = Number(data.readBigUInt64BE(offset));
1118
+ if (buf.length < 10) return null;
1119
+ const bigLen = buf.readBigUInt64BE(offset);
1120
+ // 超过 Number.MAX_SAFE_INTEGER 时精度丢失,直接视为超限
1121
+ if (bigLen > BigInt(Number.MAX_SAFE_INTEGER)) {
1122
+ return { opcode: 0xFF, payload: Buffer.alloc(0), fin: true, bytesConsumed: buf.length, oversize: true };
1123
+ }
1124
+ payloadLength = Number(bigLen);
897
1125
  offset += 8;
898
1126
  }
899
1127
 
900
1128
  // 解析掩码
901
1129
  let mask = null;
902
1130
  if (isMasked) {
903
- mask = data.slice(offset, offset + 4);
1131
+ if (buf.length < offset + 4) return null;
1132
+ mask = buf.slice(offset, offset + 4);
904
1133
  offset += 4;
905
1134
  }
906
1135
 
1136
+ // 检查负载数据是否完整
1137
+ if (buf.length < offset + payloadLength) return null;
1138
+
1139
+ // 最大负载限制检查,超限时返回错误标记
1140
+ if (payloadLength > this._maxPayload) {
1141
+ return { opcode: 0xFF, payload: Buffer.alloc(0), fin: true, bytesConsumed: offset + payloadLength, oversize: true };
1142
+ }
1143
+
907
1144
  // 提取负载
908
- let payload = data.slice(offset, offset + payloadLength);
1145
+ let payload = buf.slice(offset, offset + payloadLength);
909
1146
  if (isMasked && mask) {
910
1147
  for (let i = 0; i < payload.length; i++) {
911
1148
  payload[i] ^= mask[i % 4];
912
1149
  }
913
1150
  }
914
1151
 
915
- switch (opcode) {
916
- case 0x01: { // 文本帧
917
- const text = payload.toString('utf8');
918
- this._emit('data', { type: 'text', data: text });
919
- this._emit('text', text);
920
- break;
1152
+ return { opcode, payload, fin, bytesConsumed: offset + payloadLength };
1153
+ }
1154
+
1155
+ /**
1156
+ * 处理解码后的帧,支持分片帧(continuation frame, opcode=0x00)
1157
+ */
1158
+ _processFrame(opcode, payload, fin, oversize) {
1159
+ // 超限帧:直接关闭连接
1160
+ if (oversize) {
1161
+ this.close(1009, 'Frame payload too large');
1162
+ return;
1163
+ }
1164
+ // 关闭握手期间忽略数据帧
1165
+ if (this._closing && opcode !== 0x08 && opcode !== 0x09 && opcode !== 0x0A) {
1166
+ return;
1167
+ }
1168
+ // 控制帧(close/ping/pong)不分片,立即处理
1169
+ if (opcode === 0x08) {
1170
+ // 解析 Close 帧状态码和原因(RFC 6455 Section 5.5.1)
1171
+ let code = 1005;
1172
+ let reason = '';
1173
+ if (payload.length >= 2) {
1174
+ code = payload.readUInt16BE(0);
1175
+ // RFC 6455 Section 7.4: 状态码 0-999 为非法,1005 表示无状态码
1176
+ if (code < 1000) code = 1005;
1177
+ reason = payload.length > 2 ? payload.slice(2).toString('utf8') : '';
921
1178
  }
922
- case 0x02: { // 二进制帧
923
- this._emit('data', { type: 'binary', data: payload });
924
- this._emit('binary', payload);
925
- break;
1179
+ // 如果正在关闭握手中,对端已回复 Close 帧,完成握手
1180
+ if (this._closing) {
1181
+ clearTimeout(this._closeTimer);
1182
+ this._closeTimer = null;
1183
+ try { this.socket.end(); } catch (e) { /* 忽略 */ }
1184
+ this._emitClose(code, reason);
1185
+ return;
1186
+ }
1187
+ // 非关闭握手状态:回复 Close 帧后关闭
1188
+ // RFC 6455 Section 7.4.1: 1005/1006 状态码不得在 Close 帧中发送
1189
+ this._sendCloseFrame(code === 1005 || code === 1006 ? undefined : code);
1190
+ this.connected = false;
1191
+ this._emitClose(code, reason);
1192
+ return;
1193
+ }
1194
+ if (opcode === 0x09) {
1195
+ this._sendPong(payload);
1196
+ return;
1197
+ }
1198
+ if (opcode === 0x0A) {
1199
+ this._lastHeartbeat = Date.now();
1200
+ return;
1201
+ }
1202
+
1203
+ // 数据帧:处理分片
1204
+ if (opcode === 0x00) {
1205
+ // 分片续帧:必须有前导帧
1206
+ if (!this._fragmented) return;
1207
+ this._fragmentPayloads.push(payload);
1208
+ if (fin) {
1209
+ // 分片结束,合并并触发事件
1210
+ const fullPayload = Buffer.concat(this._fragmentPayloads);
1211
+ this._emitData(this._fragmentOpcode, fullPayload);
1212
+ this._fragmented = false;
1213
+ this._fragmentPayloads = [];
1214
+ }
1215
+ } else {
1216
+ // 新消息帧(opcode=0x01/0x02)
1217
+ if (fin) {
1218
+ // 非分片:直接触发
1219
+ this._emitData(opcode, payload);
1220
+ } else {
1221
+ // 分片开始
1222
+ this._fragmented = true;
1223
+ this._fragmentOpcode = opcode;
1224
+ this._fragmentPayloads = [payload];
926
1225
  }
927
- case 0x08: // 关闭帧
928
- this._sendCloseFrame();
929
- this.connected = false;
930
- this._emit('close');
931
- break;
932
- case 0x09: // Ping 帧
933
- this._sendPong(payload);
934
- break;
935
- case 0x0A: // Pong 帧
936
- this._lastHeartbeat = Date.now();
937
- break;
1226
+ }
1227
+ }
1228
+
1229
+ /**
1230
+ * 根据操作码触发数据事件
1231
+ */
1232
+ _emitData(opcode, payload) {
1233
+ if (opcode === 0x01) {
1234
+ const text = payload.toString('utf8');
1235
+ this._emit('data', { type: 'text', data: text });
1236
+ this._emit('text', text);
1237
+ } else if (opcode === 0x02) {
1238
+ this._emit('data', { type: 'binary', data: payload });
1239
+ this._emit('binary', payload);
938
1240
  }
939
1241
  }
940
1242
 
@@ -980,9 +1282,15 @@ class WebSocket {
980
1282
  frames.push(payload);
981
1283
  const frame = Buffer.concat(frames);
982
1284
  try {
983
- this.socket.write(frame);
1285
+ const canWrite = this.socket.write(frame);
1286
+ // 写入缓冲区满时记录警告(WebSocket 不像 HTTP 可暂停请求流,只能记录)
1287
+ if (!canWrite) {
1288
+ this._writeBacklogged = true;
1289
+ }
984
1290
  } catch (e) {
985
1291
  this.connected = false;
1292
+ // 发送失败时触发 error 事件,便于用户感知和处理
1293
+ this._emit('error', e);
986
1294
  }
987
1295
  }
988
1296
 
@@ -1003,22 +1311,57 @@ class WebSocket {
1003
1311
  /**
1004
1312
  * 发送关闭帧
1005
1313
  */
1006
- _sendCloseFrame() {
1314
+ _sendCloseFrame(code, reason) {
1007
1315
  try {
1008
- this._sendFrame(0x08, Buffer.alloc(0));
1316
+ let payload = Buffer.alloc(0);
1317
+ // RFC 6455: Close 帧可携带状态码(2字节)+ 可选原因(UTF-8)
1318
+ if (code !== undefined && code !== null) {
1319
+ const reasonBuf = reason ? Buffer.from(reason, 'utf8') : Buffer.alloc(0);
1320
+ payload = Buffer.alloc(2 + reasonBuf.length);
1321
+ payload.writeUInt16BE(code, 0);
1322
+ reasonBuf.copy(payload, 2);
1323
+ }
1324
+ this._sendFrame(0x08, payload);
1009
1325
  } catch (e) {
1010
1326
  // 忽略关闭帧发送错误
1011
1327
  }
1012
1328
  }
1013
1329
 
1014
1330
  /**
1015
- * 关闭连接
1331
+ * 关闭连接(限时等待对端 Close 帧完成握手)
1332
+ * @param {number} [code] 关闭状态码(RFC 6455 Section 7.4)
1333
+ * @param {string} [reason] 关闭原因
1016
1334
  */
1017
- close() {
1018
- this._sendCloseFrame();
1335
+ close(code, reason) {
1336
+ if (this._closed) return;
1337
+ // 重要:必须先发送 Close 帧再设 connected=false
1338
+ // _sendFrame 内部检查 this.connected,若先断开则 Close 帧无法发出
1339
+ this._sendCloseFrame(code, reason);
1340
+ // 标记为关闭中,拒绝后续数据帧发送和 _sendFrame 写入
1019
1341
  this.connected = false;
1020
- try { this.socket.end(); } catch (e) { /* 忽略 */ }
1021
- this._emit('close');
1342
+ this._closing = true;
1343
+ // 限时等待对端 Close 帧(2秒超时)
1344
+ this._closeTimer = setTimeout(() => {
1345
+ this._closeTimer = null;
1346
+ // 先标记已关闭,防止 socket.destroy 触发 close 事件时重复 _emitClose
1347
+ this._closed = true;
1348
+ try { this.socket.destroy(); } catch (e) { /* 忽略 */ }
1349
+ this._emitClose(code, reason);
1350
+ }, 2000);
1351
+ }
1352
+
1353
+ /**
1354
+ * 安全触发 close 事件(防止重复触发)
1355
+ */
1356
+ _emitClose(code, reason) {
1357
+ if (this._closed) return;
1358
+ this._closed = true;
1359
+ // 清理关闭握手定时器
1360
+ if (this._closeTimer) {
1361
+ clearTimeout(this._closeTimer);
1362
+ this._closeTimer = null;
1363
+ }
1364
+ this._emit('close', code, reason);
1022
1365
  }
1023
1366
 
1024
1367
  /**
@@ -1034,12 +1377,7 @@ class WebSocket {
1034
1377
  * 触发事件
1035
1378
  */
1036
1379
  _emit(event, ...args) {
1037
- const handlers = this._handlers[event];
1038
- if (handlers) {
1039
- handlers.forEach(h => {
1040
- try { h(...args); } catch (e) { /* 忽略 */ }
1041
- });
1042
- }
1380
+ _emitEvent(this._handlers, event, args);
1043
1381
  }
1044
1382
  }
1045
1383
 
@@ -1050,7 +1388,7 @@ class WebSocket {
1050
1388
  /**
1051
1389
  * WebSocket 握手辅助函数:计算 Sec-WebSocket-Accept 值
1052
1390
  */
1053
- function WebSocketHandShark(key) {
1391
+ function WebSocketHandShak(key) {
1054
1392
  return crypto.createHash('sha1')
1055
1393
  .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
1056
1394
  .digest('base64');
@@ -1062,6 +1400,8 @@ class WebSocketServer {
1062
1400
  this.groups = new Map(); // path -> Set<WebSocket>
1063
1401
  this._heartbeatInterval = options.heartbeatInterval || 30000;
1064
1402
  this._heartbeatTimeout = options.heartbeatTimeout || 30000;
1403
+ this._maxPayload = options.maxPayload || 100 * 1024 * 1024;
1404
+ this._allowedOrigins = options.allowedOrigins || null;
1065
1405
  this._timer = null;
1066
1406
  this._handlers = {};
1067
1407
  }
@@ -1079,9 +1419,17 @@ class WebSocketServer {
1079
1419
  return null;
1080
1420
  }
1081
1421
 
1082
- const accept = crypto.createHash('sha1')
1083
- .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
1084
- .digest('base64');
1422
+ // Origin 校验(防止跨站 WebSocket 劫持 CSWSH)
1423
+ if (this._allowedOrigins) {
1424
+ const origin = req.headers['origin'];
1425
+ if (!origin || !this._allowedOrigins.includes(origin)) {
1426
+ socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
1427
+ socket.destroy();
1428
+ return null;
1429
+ }
1430
+ }
1431
+
1432
+ const accept = WebSocketHandShak(key);
1085
1433
 
1086
1434
  // 发送握手响应
1087
1435
  const responseHeaders = [
@@ -1093,7 +1441,7 @@ class WebSocketServer {
1093
1441
  socket.write(responseHeaders.join('\r\n') + '\r\n\r\n');
1094
1442
 
1095
1443
  // 创建 WebSocket 实例
1096
- const ws = new WebSocket(socket, pathname);
1444
+ const ws = new WebSocket(socket, pathname, { maxPayload: this._maxPayload });
1097
1445
  this.connections.set(ws.id, ws);
1098
1446
 
1099
1447
  // 按路径分组
@@ -1141,19 +1489,24 @@ class WebSocketServer {
1141
1489
  if (this._timer) return;
1142
1490
  this._timer = setInterval(() => {
1143
1491
  const now = Date.now();
1492
+ // 先收集需移除的连接,遍历结束后统一移除,避免迭代中修改 Map
1493
+ const toRemove = [];
1144
1494
  for (const ws of this.connections.values()) {
1145
1495
  if (!ws.connected) {
1146
- this._removeConnection(ws);
1496
+ toRemove.push(ws);
1147
1497
  continue;
1148
1498
  }
1149
1499
  // 检查心跳超时
1150
1500
  if (now - ws._lastHeartbeat > this._heartbeatInterval + this._heartbeatTimeout) {
1151
1501
  ws.close();
1152
- this._removeConnection(ws);
1502
+ toRemove.push(ws);
1153
1503
  continue;
1154
1504
  }
1155
1505
  ws._sendPing();
1156
1506
  }
1507
+ for (const ws of toRemove) {
1508
+ this._removeConnection(ws);
1509
+ }
1157
1510
  }, this._heartbeatInterval);
1158
1511
  }
1159
1512
 
@@ -1170,7 +1523,7 @@ class WebSocketServer {
1170
1523
  /**
1171
1524
  * 按路径广播消息
1172
1525
  */
1173
- broadcastTo(pathStr, data, exclude = null) {
1526
+ broadcast(pathStr, data, exclude = null) {
1174
1527
  const group = this.groups.get(pathStr);
1175
1528
  if (!group) return;
1176
1529
  for (const ws of group) {
@@ -1183,7 +1536,7 @@ class WebSocketServer {
1183
1536
  /**
1184
1537
  * 全局广播消息
1185
1538
  */
1186
- broadcast(data, exclude = null) {
1539
+ broadcastAll(data, exclude = null) {
1187
1540
  for (const ws of this.connections.values()) {
1188
1541
  if (ws.connected && ws !== exclude) {
1189
1542
  ws.send(data);
@@ -1214,12 +1567,7 @@ class WebSocketServer {
1214
1567
  * 触发事件
1215
1568
  */
1216
1569
  _emit(event, ...args) {
1217
- const handlers = this._handlers[event];
1218
- if (handlers) {
1219
- handlers.forEach(h => {
1220
- try { h(...args); } catch (e) { /* 忽略 */ }
1221
- });
1222
- }
1570
+ _emitEvent(this._handlers, event, args);
1223
1571
  }
1224
1572
  }
1225
1573
 
@@ -1257,6 +1605,10 @@ function bodyParser(options = {}) {
1257
1605
  const boundary = _extractBoundary(contentType);
1258
1606
  if (boundary) {
1259
1607
  _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next);
1608
+ // 仅 multipart 需要临时文件清理(使用 res.on 保持封装一致性)
1609
+ res.on('finish', () => {
1610
+ _cleanupTempFiles(req._tempFiles);
1611
+ });
1260
1612
  } else {
1261
1613
  next();
1262
1614
  }
@@ -1267,11 +1619,6 @@ function bodyParser(options = {}) {
1267
1619
  next();
1268
1620
  }).catch(next);
1269
1621
  }
1270
-
1271
- // 监听响应完成,自动清理临时文件
1272
- res._res.on('finish', () => {
1273
- _cleanupTempFiles(req._tempFiles);
1274
- });
1275
1622
  };
1276
1623
  }
1277
1624
 
@@ -1281,7 +1628,7 @@ function bodyParser(options = {}) {
1281
1628
  function _parseJSON(req, maxSize, next) {
1282
1629
  req._readBody().then(buf => {
1283
1630
  if (buf.length > maxSize) {
1284
- const err = new Error(`Body exceeds maximum size of ${fmtSize(maxSize)}`);
1631
+ const err = new Error(`Body exceeds maximum size of ${fmtSize(maxSize)}`, { cause: { actual: buf.length, maxSize } });
1285
1632
  err.status = 413;
1286
1633
  next(err);
1287
1634
  return;
@@ -1305,21 +1652,12 @@ function _parseJSON(req, maxSize, next) {
1305
1652
  function _parseUrlencoded(req, maxSize, next) {
1306
1653
  req._readBody().then(buf => {
1307
1654
  if (buf.length > maxSize) {
1308
- const err = new Error(`Body exceeds maximum size of ${fmtSize(maxSize)}`);
1655
+ const err = new Error(`Body exceeds maximum size of ${fmtSize(maxSize)}`, { cause: { actual: buf.length, maxSize } });
1309
1656
  err.status = 413;
1310
1657
  next(err);
1311
1658
  return;
1312
1659
  }
1313
- const str = buf.toString('utf8');
1314
- const parsed = {};
1315
- str.split('&').forEach(pair => {
1316
- const eIdx = pair.indexOf('=');
1317
- if (eIdx === -1) {
1318
- parsed[decodeURIComponent(pair)] = '';
1319
- } else {
1320
- parsed[decodeURIComponent(pair.substring(0, eIdx))] = decodeURIComponent(pair.substring(eIdx + 1));
1321
- }
1322
- });
1660
+ const parsed = _parseQueryString(buf.toString('utf8'), true);
1323
1661
  req.body = parsed;
1324
1662
  Object.assign(req.formData.fields, parsed);
1325
1663
  next();
@@ -1342,6 +1680,8 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1342
1680
  const tempDir = req._app?.settings?.tempDir || 'tempupdir';
1343
1681
  const delimiter = Buffer.from('--' + boundary);
1344
1682
  const endDelimiter = Buffer.from('--' + boundary + '--');
1683
+ // 回看长度:分隔符最大可能被截断的字节数
1684
+ const lookBehind = delimiter.length - 1;
1345
1685
  let state = 'INIT'; // INIT, HEADERS, BODY_FIELD, BODY_FILE
1346
1686
  let partHeadersBuf = Buffer.alloc(0);
1347
1687
  let currentField = { name: '', value: '' };
@@ -1350,6 +1690,7 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1350
1690
  let fileSize = 0;
1351
1691
  let buffer = Buffer.alloc(0);
1352
1692
  let cleanupOnError = false;
1693
+ let paused = false;
1353
1694
 
1354
1695
  // 确保临时目录存在
1355
1696
  if (!fs.existsSync(tempDir)) {
@@ -1361,7 +1702,13 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1361
1702
  if (state === 'INIT') {
1362
1703
  // 查找第一个分隔符
1363
1704
  const idx = buffer.indexOf(delimiter);
1364
- if (idx === -1) break;
1705
+ if (idx === -1) {
1706
+ // 保留尾部回看字节,防止分隔符跨 chunk 截断
1707
+ if (buffer.length > lookBehind) {
1708
+ buffer = buffer.slice(buffer.length - lookBehind);
1709
+ }
1710
+ break;
1711
+ }
1365
1712
  buffer = buffer.slice(idx + delimiter.length);
1366
1713
  // 跳过 \r\n
1367
1714
  if (buffer.length >= 2 && buffer[0] === 0x0D && buffer[1] === 0x0A) {
@@ -1411,23 +1758,25 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1411
1758
  const idx = buffer.indexOf(delimiter);
1412
1759
  if (idx === -1) {
1413
1760
  // 还没结束,缓存数据(但检查大小限制)
1414
- const chunk = buffer.toString('utf8');
1761
+ // 保留尾部回看字节,防止分隔符跨 chunk 截断
1762
+ const safeLen = Math.max(0, buffer.length - lookBehind);
1763
+ const chunk = buffer.toString('utf8', 0, safeLen);
1415
1764
  fieldSize += chunk.length;
1416
1765
  if (fieldSize > maxFieldSize) {
1417
- const err = new Error(`Field exceeds maximum size of ${fmtSize(maxFieldSize)}`);
1766
+ const err = new Error(`Field exceeds maximum size of ${fmtSize(maxFieldSize)}`, { cause: { actual: fieldSize, maxSize: maxFieldSize } });
1418
1767
  err.status = 413;
1419
1768
  next(err);
1420
1769
  return;
1421
1770
  }
1422
1771
  currentField.value += chunk;
1423
- buffer = Buffer.alloc(0);
1772
+ buffer = buffer.slice(safeLen);
1424
1773
  break;
1425
1774
  }
1426
1775
  // 字段结束
1427
1776
  const chunk = buffer.toString('utf8', 0, idx);
1428
1777
  fieldSize += chunk.length;
1429
1778
  if (fieldSize > maxFieldSize) {
1430
- const err = new Error(`Field exceeds maximum size of ${fmtSize(maxFieldSize)}`);
1779
+ const err = new Error(`Field exceeds maximum size of ${fmtSize(maxFieldSize)}`, { cause: { actual: fieldSize, maxSize: maxFieldSize } });
1431
1780
  err.status = 413;
1432
1781
  next(err);
1433
1782
  return;
@@ -1450,30 +1799,41 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1450
1799
  const idx = buffer.indexOf(delimiter);
1451
1800
  if (idx === -1) {
1452
1801
  // 还没结束,写入临时文件
1802
+ // 保留尾部回看字节,防止分隔符跨 chunk 截断
1803
+ const safeLen = Math.max(0, buffer.length - lookBehind);
1804
+ const writeData = buffer.slice(0, safeLen);
1453
1805
  if (!currentFile.stream) {
1454
1806
  currentFile.stream = fs.createWriteStream(currentFile.path);
1807
+ // 背压处理:写入流满时暂停请求读取,drain 后恢复
1808
+ currentFile.stream.on('drain', () => {
1809
+ if (paused) {
1810
+ paused = false;
1811
+ req._req.resume();
1812
+ }
1813
+ });
1455
1814
  }
1456
- currentFile.stream.write(buffer);
1457
- fileSize += buffer.length;
1815
+ const canWrite = currentFile.stream.write(writeData);
1816
+ // 写入流缓冲区满时暂停请求读取,避免内存积压
1817
+ if (!canWrite) {
1818
+ paused = true;
1819
+ req._req.pause();
1820
+ }
1821
+ fileSize += writeData.length;
1458
1822
  currentFile.size = fileSize;
1459
1823
  if (fileSize > maxFileSize) {
1460
- if (currentFile.stream) currentFile.stream.close();
1461
- const err = new Error(`File exceeds maximum size of ${fmtSize(maxFileSize)}`);
1824
+ if (currentFile.stream) currentFile.stream.destroy();
1825
+ const err = new Error(`File exceeds maximum size of ${fmtSize(maxFileSize)}`, { cause: { actual: fileSize, maxSize: maxFileSize, filename: currentFile.filename } });
1462
1826
  err.status = 413;
1463
1827
  next(err);
1464
1828
  return;
1465
1829
  }
1466
- // 进度展示(>1MB)
1467
- if (fileSize > 1024 * 1024) {
1468
- _showUploadProgress(currentFile.filename, fileSize, maxFileSize);
1469
- }
1470
- buffer = Buffer.alloc(0);
1830
+ buffer = buffer.slice(safeLen);
1471
1831
  break;
1472
1832
  }
1473
1833
  // 文件结束
1474
1834
  const fileData = buffer.slice(0, idx);
1475
1835
  // 去掉文件数据前的 \r\n
1476
- const trimmedData = fileData.length >= 2 && fileData[fileData.length - 2] === 0x0D && fileData[fileData.length - 1] === 0x0A
1836
+ const trimmedData = fileData.length >= 2 && fileData.at(-2) === 0x0D && fileData.at(-1) === 0x0A
1477
1837
  ? fileData.slice(0, -2)
1478
1838
  : fileData;
1479
1839
 
@@ -1481,6 +1841,8 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1481
1841
  currentFile.stream = fs.createWriteStream(currentFile.path);
1482
1842
  }
1483
1843
  currentFile.stream.write(trimmedData);
1844
+ // 结束写入(注意:stream.end 为异步操作,极端情况如磁盘满时可能写入不完整,
1845
+ // 但 fileInfo 仍会被记录。若需严格保证写入完整性,需改为异步流程)
1484
1846
  currentFile.stream.end();
1485
1847
  fileSize += trimmedData.length;
1486
1848
  currentFile.size = fileSize;
@@ -1498,6 +1860,7 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1498
1860
  req.files.push(fileInfo);
1499
1861
  req._tempFiles.push(currentFile.path);
1500
1862
 
1863
+ // 清理上传进度条目
1501
1864
  cleanupOnError = false;
1502
1865
  buffer = buffer.slice(idx + delimiter.length);
1503
1866
  // 跳过 \r\n
@@ -1526,26 +1889,13 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1526
1889
  req._req.on('error', (err) => {
1527
1890
  // 客户端断开,清理临时文件
1528
1891
  if (cleanupOnError && currentFile.stream) {
1529
- currentFile.stream.close();
1892
+ currentFile.stream.destroy();
1530
1893
  }
1531
1894
  _cleanupTempFiles(req._tempFiles);
1532
1895
  next(err);
1533
1896
  });
1534
1897
  }
1535
1898
 
1536
- /**
1537
- * 上传进度展示
1538
- */
1539
- let _lastProgressLine = '';
1540
- function _showUploadProgress(filename, current, total) {
1541
- const pct = ((current / total) * 100).toFixed(1);
1542
- const line = `[${filename}] ${fmtSize(current)}/${fmtSize(total)} ${pct}%`;
1543
- if (line !== _lastProgressLine) {
1544
- process.stdout.write('\r' + line);
1545
- _lastProgressLine = line;
1546
- }
1547
- }
1548
-
1549
1899
  /**
1550
1900
  * 清理临时文件
1551
1901
  */
@@ -1571,15 +1921,22 @@ function cookieParser(secret) {
1571
1921
  req.signedCookies = {};
1572
1922
  for (const [key, val] of Object.entries(req.cookies)) {
1573
1923
  if (val.startsWith('s:')) {
1574
- // 签名 Cookie 格式: s:value.signature
1924
+ // 签名 Cookie 格式: s:encodedValue.signature
1575
1925
  const unsigned = val.slice(2);
1576
1926
  const dotIdx = unsigned.lastIndexOf('.');
1577
1927
  if (dotIdx !== -1) {
1578
- const value = unsigned.slice(0, dotIdx);
1928
+ const encodedValue = unsigned.slice(0, dotIdx);
1579
1929
  const sig = unsigned.slice(dotIdx + 1);
1580
- const expected = crypto.createHmac('sha256', secret).update(value).digest('base64').replace(/=+$/, '');
1930
+ // 签名是对原始值计算的,需先解码
1931
+ let rawValue;
1932
+ try {
1933
+ rawValue = decodeURIComponent(encodedValue);
1934
+ } catch (e) {
1935
+ continue;
1936
+ }
1937
+ const expected = crypto.createHmac('sha256', secret).update(rawValue).digest('base64').replace(/=+$/, '');
1581
1938
  if (sig === expected) {
1582
- req.signedCookies[key] = value;
1939
+ req.signedCookies[key] = rawValue;
1583
1940
  }
1584
1941
  }
1585
1942
  }
@@ -1651,7 +2008,7 @@ class Application extends Router {
1651
2008
  if (this.settings.http2) {
1652
2009
  // HTTP2 模式
1653
2010
  if (!this.settings.https || !this.settings.https.key || !this.settings.https.cert) {
1654
- throw new Error('HTTP2 requires HTTPS configuration (key and cert)');
2011
+ throw new Error('HTTP2 requires HTTPS configuration (key and cert)', { cause: { https: !!this.settings.https, hasKey: !!(this.settings.https && this.settings.https.key), hasCert: !!(this.settings.https && this.settings.https.cert) } });
1655
2012
  }
1656
2013
  const opts = {
1657
2014
  key: fs.readFileSync(this.settings.https.key),
@@ -1685,7 +2042,9 @@ class Application extends Router {
1685
2042
  // 初始化 WebSocket 服务
1686
2043
  this._wss = new WebSocketServer({
1687
2044
  heartbeatInterval: this.settings.wsHeartbeatInterval,
1688
- heartbeatTimeout: this.settings.wsHeartbeatTimeout
2045
+ heartbeatTimeout: this.settings.wsHeartbeatTimeout,
2046
+ maxPayload: this.settings.wsMaxPayload,
2047
+ allowedOrigins: this.settings.wsAllowedOrigins
1689
2048
  });
1690
2049
 
1691
2050
  // 监听 WebSocket 升级请求
@@ -1734,11 +2093,20 @@ class Application extends Router {
1734
2093
 
1735
2094
  // 基础解析
1736
2095
  const parsed = parseUrl(incomingMessage.url);
1737
- req.path = decodeURIComponent(parsed.pathname);
2096
+ try {
2097
+ req.path = decodeURIComponent(parsed.pathname);
2098
+ } catch (e) {
2099
+ // 非法 URI 编码,返回 400
2100
+ res.status(400).send('Bad Request: Invalid URI encoding');
2101
+ return;
2102
+ }
1738
2103
  req.query = parsed.query;
2104
+ // Cookie 由 cookieParser 中间件统一解析,此处不再重复处理
1739
2105
 
1740
- // 解析 Cookie(中间件会再次处理,此处先做基础解析)
1741
- req.cookies = parseCookies(incomingMessage.headers['cookie'] || '');
2106
+ // HEAD 请求标记:执行路由处理器但丢弃响应体
2107
+ if (req.method === 'HEAD') {
2108
+ res._isHead = true;
2109
+ }
1742
2110
 
1743
2111
  // 构建中间件 + 路由处理器执行链
1744
2112
  this._dispatch(req, res);
@@ -1786,10 +2154,17 @@ class Application extends Router {
1786
2154
 
1787
2155
  const item = stack[idx++];
1788
2156
  const handler = item.handler;
2157
+ // 防止同一 handler 多次调用 next()
2158
+ let handlerCalledNext = false;
2159
+ const safeNext = (e) => {
2160
+ if (handlerCalledNext && !e) return;
2161
+ handlerCalledNext = true;
2162
+ next(e);
2163
+ };
1789
2164
 
1790
2165
  try {
1791
2166
  // 路由处理器返回 false → 进入静态文件兜底
1792
- const result = handler(req, res, next);
2167
+ const result = handler(req, res, safeNext);
1793
2168
  if (result === false) {
1794
2169
  this._serveStatic(req, res);
1795
2170
  return;
@@ -1835,7 +2210,7 @@ class Application extends Router {
1835
2210
  const status = err.status || 500;
1836
2211
  const msg = err.message || 'Internal Server Error';
1837
2212
  this._logger.error(`[${status}] ${req.method} ${req.path} - ${msg}`);
1838
- if (!res.headersSent && !res._res.headersSent) {
2213
+ if (!res.headersSent) {
1839
2214
  res.status(status).json({ error: msg, status });
1840
2215
  }
1841
2216
  }
@@ -1853,8 +2228,9 @@ class Application extends Router {
1853
2228
  // CORS 预检响应
1854
2229
  this._handleCORS(req, res);
1855
2230
  } else if (method === 'POST' || method === 'PUT' || method === 'DELETE' || method === 'PATCH') {
1856
- // 已知方法但无匹配路由,返回 405 Method Not Allowed
1857
- res.status(405).json({ error: 'Method Not Allowed', status: 405 });
2231
+ // 已知方法但无匹配路由,返回 405 Method Not Allowed(RFC 7231 要求必须包含 Allow 头)
2232
+ const allowed = this._getAllowedMethods(req.path);
2233
+ res.set('Allow', allowed.join(', ')).status(405).json({ error: 'Method Not Allowed', status: 405 });
1858
2234
  } else {
1859
2235
  // 其他未知方法返回 404
1860
2236
  res.status(404).json({ error: 'Not Found', status: 404 });
@@ -1862,7 +2238,7 @@ class Application extends Router {
1862
2238
  }
1863
2239
 
1864
2240
  /**
1865
- * CORS 预检响应
2241
+ * CORS 预检响应(动态查询该路径支持的 HTTP 方法)
1866
2242
  */
1867
2243
  _handleCORS(req, res) {
1868
2244
  const cors = this.settings.cors;
@@ -1870,17 +2246,53 @@ class Application extends Router {
1870
2246
  res.status(204)._send('');
1871
2247
  return;
1872
2248
  }
2249
+ // 动态查找该路径匹配的所有 HTTP 方法
2250
+ const allowedMethods = this._getAllowedMethods(req.path);
1873
2251
  const origin = typeof cors.origin === 'string' ? cors.origin : '*';
1874
2252
  res.setHeader('Access-Control-Allow-Origin', origin);
1875
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
2253
+ // 动态设置允许的方法列表,而非硬编码
2254
+ res.setHeader('Access-Control-Allow-Methods', allowedMethods.join(', '));
1876
2255
  res.setHeader('Access-Control-Allow-Headers', cors.headers || 'Content-Type, Authorization');
1877
- res.setHeader('Access-Control-Max-Age', cors.maxAge || '86400');
2256
+ res.setHeader('Access-Control-Max-Age', parseInt(cors.maxAge, 10) || 86400);
2257
+ // Allow 头告知客户端该路径实际支持的方法
2258
+ res.setHeader('Allow', allowedMethods.join(', '));
1878
2259
  if (cors.credentials) {
1879
2260
  res.setHeader('Access-Control-Allow-Credentials', 'true');
1880
2261
  }
1881
2262
  res.status(204)._send('');
1882
2263
  }
1883
2264
 
2265
+ /**
2266
+ * 查询指定路径支持的所有 HTTP 方法(含隐式 HEAD)
2267
+ * 用于 OPTIONS Allow 头和 405 Method Not Allowed 响应
2268
+ */
2269
+ _getAllowedMethods(pathname) {
2270
+ const methods = new Set();
2271
+ // 遍历所有已注册方法的路由,查找匹配该路径的方法
2272
+ for (const method of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']) {
2273
+ const routes = this.routes[method] || [];
2274
+ for (const route of routes) {
2275
+ if (route.pattern.exec(pathname)) {
2276
+ methods.add(method);
2277
+ break;
2278
+ }
2279
+ }
2280
+ }
2281
+ // 检查 ALL 中间件路由
2282
+ const allRoutes = this.routes['ALL'] || [];
2283
+ for (const route of allRoutes) {
2284
+ if (route.pattern.exec(pathname)) {
2285
+ ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].forEach(m => methods.add(m));
2286
+ break;
2287
+ }
2288
+ }
2289
+ // GET 路由隐式支持 HEAD
2290
+ if (methods.has('GET')) methods.add('HEAD');
2291
+ // OPTIONS 始终可用(CORS 预检)
2292
+ methods.add('OPTIONS');
2293
+ return [...methods].sort();
2294
+ }
2295
+
1884
2296
  /**
1885
2297
  * 静态文件服务
1886
2298
  */
@@ -1890,7 +2302,7 @@ class Application extends Router {
1890
2302
  let requestPath = req.path.replace(/^\/+/, '');
1891
2303
 
1892
2304
  // 路径安全校验
1893
- if (!isPathSafe(requestPath, rootPath)) {
2305
+ if (!isPathSafe(requestPath, rootPath, this.settings.allowAccessToAllFiles)) {
1894
2306
  res.status(403).send('Forbidden');
1895
2307
  return;
1896
2308
  }
@@ -1928,18 +2340,23 @@ class Application extends Router {
1928
2340
  * 目录列表展示
1929
2341
  */
1930
2342
  _serveDirectory(req, res, dirPath, requestPath) {
1931
- fs.readdir(dirPath, (err, files) => {
2343
+ // 使用 withFileTypes 避免对每个文件做 statSync 调用
2344
+ fs.readdir(dirPath, { withFileTypes: true }, (err, entries) => {
1932
2345
  if (err) {
1933
2346
  res.status(500).send('Internal Server Error');
1934
2347
  return;
1935
2348
  }
1936
2349
 
1937
- const items = files.map(f => {
2350
+ const items = entries.map(entry => {
1938
2351
  try {
1939
- const stat = fs.statSync(path.join(dirPath, f));
2352
+ // 目录只需名称和类型,文件需要额外 stat 获取大小和修改时间
2353
+ if (entry.isDirectory()) {
2354
+ return { name: entry.name, isDirectory: true, size: 0, modified: '' };
2355
+ }
2356
+ const stat = fs.statSync(path.join(dirPath, entry.name));
1940
2357
  return {
1941
- name: f,
1942
- isDirectory: stat.isDirectory(),
2358
+ name: entry.name,
2359
+ isDirectory: false,
1943
2360
  size: stat.size,
1944
2361
  modified: stat.mtime.toISOString()
1945
2362
  };
@@ -1966,22 +2383,28 @@ class Application extends Router {
1966
2383
  * 渲染目录列表 HTML
1967
2384
  */
1968
2385
  _renderDirectoryHTML(requestPath, items) {
1969
- const parentPath = path.dirname(requestPath);
1970
- let html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Directory: ${requestPath}</title>`;
2386
+ // requestPath 已去掉前导 /,空字符串表示根目录
2387
+ // path.dirname('subdir') 返回 '.',应映射为根目录 ''
2388
+ let parentPath = requestPath ? path.dirname(requestPath) : '/';
2389
+ if (parentPath === '.') parentPath = '';
2390
+ // 根目录时显示 '/',否则显示请求路径
2391
+ const displayPath = requestPath || '/';
2392
+ let html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Directory: ${escapeHtml(displayPath)}</title>`;
1971
2393
  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>`;
1972
- html += `</head><body><h1>Directory: ${requestPath}</h1><table><tr><th>Name</th><th>Size</th><th>Modified</th></tr>`;
2394
+ html += `</head><body><h1>Directory: ${escapeHtml(displayPath)}</h1><table><tr><th>Name</th><th>Size</th><th>Modified</th></tr>`;
1973
2395
 
1974
- // 父目录链接
1975
- if (requestPath !== '/') {
1976
- html += `<tr><td><a href="${parentPath}" class="dir">../</a></td><td class="size">-</td><td>-</td></tr>`;
2396
+ // 父目录链接(根目录时不显示)
2397
+ if (requestPath && requestPath !== '/') {
2398
+ html += `<tr><td><a href="${escapeHtml(parentPath)}" class="dir">../</a></td><td class="size">-</td><td>-</td></tr>`;
1977
2399
  }
1978
2400
 
1979
2401
  for (const item of items) {
1980
- const href = path.join(requestPath, item.name);
2402
+ // 使用 '/' 拼接路径,防止 Windows 上 path.join 产生反斜杠导致 href 失效
2403
+ const href = requestPath ? requestPath + '/' + item.name : item.name;
1981
2404
  const name = item.isDirectory ? item.name + '/' : item.name;
1982
2405
  const size = item.isDirectory ? '-' : fmtSize(item.size);
1983
2406
  const cls = item.isDirectory ? 'dir' : '';
1984
- html += `<tr><td><a href="${href}" class="${cls}">${name}</a></td><td class="size">${size}</td><td class="size">${item.modified}</td></tr>`;
2407
+ html += `<tr><td><a href="${escapeHtml(href)}" class="${cls}">${escapeHtml(name)}</a></td><td class="size">${size}</td><td class="size">${item.modified}</td></tr>`;
1985
2408
  }
1986
2409
 
1987
2410
  html += `</table></body></html>`;
@@ -1995,11 +2418,30 @@ class Application extends Router {
1995
2418
  const ws = this._wss.handleUpgrade(req, socket, head);
1996
2419
  if (!ws) return;
1997
2420
 
1998
- // 匹配 app.ws() 注册的处理器
2421
+ // 匹配 app.ws() 注册的处理器(支持动态参数路径)
2422
+ // _compilePath 始终返回 pattern,静态和动态路径均通过正则精确匹配
1999
2423
  if (this._wsHandlers) {
2000
2424
  const pathname = parseUrl(req.url).pathname;
2001
2425
  for (const entry of this._wsHandlers) {
2002
- if (pathname === entry.path || pathname.startsWith(entry.path + '/')) {
2426
+ let matched = false;
2427
+ let params = {};
2428
+ const m = entry.pattern.exec(pathname);
2429
+ if (m) {
2430
+ matched = true;
2431
+ entry.params.forEach((name, i) => {
2432
+ try {
2433
+ params[name] = decodeURIComponent(m[i + 1]);
2434
+ } catch (e) {
2435
+ // 非法 URI 编码,保留原始值
2436
+ params[name] = m[i + 1];
2437
+ }
2438
+ });
2439
+ }
2440
+ if (matched) {
2441
+ // 将动态参数挂载到 req 上
2442
+ if (Object.keys(params).length > 0) {
2443
+ req.params = params;
2444
+ }
2003
2445
  const cleanup = entry.handler(ws, req);
2004
2446
  if (typeof cleanup === 'function') {
2005
2447
  ws.on('close', cleanup);
@@ -2038,7 +2480,9 @@ class Application extends Router {
2038
2480
  */
2039
2481
  ws(pathStr, handler) {
2040
2482
  if (!this._wsHandlers) this._wsHandlers = [];
2041
- this._wsHandlers.push({ path: pathStr, handler });
2483
+ // 支持动态参数路径,复用 Router 的路径编译逻辑
2484
+ const { pattern, params } = this._compilePath(pathStr);
2485
+ this._wsHandlers.push({ path: pathStr, pattern, params, handler });
2042
2486
  }
2043
2487
  }
2044
2488
 
@@ -2077,10 +2521,12 @@ const defaultConfig = {
2077
2521
  tempDir: 'tempupdir',
2078
2522
  maxFileSize: 128 * 1024 * 1024,
2079
2523
  maxFieldSize: 1024 * 1024,
2524
+ maxBodySize: 128 * 1024 * 1024,
2080
2525
  svrPort: 80,
2081
2526
  svrIP: null,
2082
2527
 
2083
2528
  showDir: false,
2529
+ allowAccessToAllFiles: false,
2084
2530
  enableCache: false,
2085
2531
  enableGzip: false,
2086
2532
  enableRange: true,
@@ -2095,7 +2541,7 @@ const defaultConfig = {
2095
2541
  logLevel: 'info',
2096
2542
  logDir: './log',
2097
2543
 
2098
- cors: { origin: '*', headers: 'Content-Type, Authorization', maxAge: '86400' },
2544
+ cors: { origin: '*', headers: 'Content-Type, Authorization', maxAge: 86400 },
2099
2545
 
2100
2546
  useBodyParser: true,
2101
2547
  useCookieParser: true,
@@ -2103,7 +2549,9 @@ const defaultConfig = {
2103
2549
  cookieParserSecret: null,
2104
2550
 
2105
2551
  wsHeartbeatInterval: 30000,
2106
- wsHeartbeatTimeout: 30000
2552
+ wsHeartbeatTimeout: 30000,
2553
+ wsMaxPayload: 100 * 1024 * 1024,
2554
+ wsAllowedOrigins: null
2107
2555
  };
2108
2556
 
2109
2557
  // ============================================================
@@ -2135,19 +2583,29 @@ httpm.cookieParser = cookieParser;
2135
2583
  * static 中间件:Express 兼容的静态文件服务
2136
2584
  * 用法: app.use(httpm.static('public'))
2137
2585
  */
2138
- function staticMiddleware(rootPath) {
2586
+ function staticMiddleware(rootPath, options = {}) {
2139
2587
  return function staticHandler(req, res, next) {
2140
2588
  if (req.method !== 'GET' && req.method !== 'HEAD') {
2141
2589
  return next();
2142
2590
  }
2143
2591
  const root = path.resolve(rootPath || process.cwd());
2592
+ const allowAll = options.allowAccessToAllFiles || false;
2144
2593
  let requestPath = req.path.replace(/^\/+/, '');
2145
- if (!isPathSafe(requestPath, root)) {
2594
+ if (!isPathSafe(requestPath, root, allowAll)) {
2146
2595
  return next();
2147
2596
  }
2148
2597
  const fullPath = path.join(root, requestPath);
2149
2598
  fs.stat(fullPath, (err, stat) => {
2150
- if (err || !stat.isFile()) {
2599
+ if (err) {
2600
+ return next();
2601
+ }
2602
+ if (stat.isDirectory()) {
2603
+ // 目录:尝试 index.html
2604
+ const indexPath = path.join(fullPath, 'index.html');
2605
+ if (fs.existsSync(indexPath)) {
2606
+ res.sendFile(path.relative(root, indexPath), { root });
2607
+ return;
2608
+ }
2151
2609
  return next();
2152
2610
  }
2153
2611
  res.sendFile(requestPath, { root });
@@ -2165,23 +2623,14 @@ httpm.fmtTime = fmtTime;
2165
2623
  httpm.isPathSafe = isPathSafe;
2166
2624
  httpm.generateETag = generateETag;
2167
2625
  httpm.parseRange = parseRange;
2168
- httpm.WebSocketHandShark = WebSocketHandShark;
2626
+ httpm.WebSocketHandShak = WebSocketHandShak;
2627
+ httpm.escapeHtml = escapeHtml;
2169
2628
 
2170
2629
  /**
2171
- * parseQuery:独立导出的 Query 解析函数
2630
+ * parseQuery:独立导出的 Query 解析函数(复用内部 _parseQueryString)
2172
2631
  */
2173
2632
  function parseQuery(qs) {
2174
- const query = {};
2175
- if (!qs) return query;
2176
- qs.split('&').forEach(pair => {
2177
- const eIdx = pair.indexOf('=');
2178
- if (eIdx === -1) {
2179
- query[decodeURIComponent(pair)] = '';
2180
- } else {
2181
- query[decodeURIComponent(pair.substring(0, eIdx))] = decodeURIComponent(pair.substring(eIdx + 1));
2182
- }
2183
- });
2184
- return query;
2633
+ return _parseQueryString(qs);
2185
2634
  }
2186
2635
  httpm.parseQuery = parseQuery;
2187
2636