mm_https 1.6.5 → 1.6.7

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.
@@ -0,0 +1,68 @@
1
+ const jsdoc = require('eslint-plugin-jsdoc');
2
+
3
+ module.exports = [
4
+ {
5
+ files: ['index.js','lib/**/*.js'],
6
+ languageOptions: {
7
+ ecmaVersion: 2020,
8
+ sourceType: 'script'
9
+ },
10
+ plugins: {
11
+ jsdoc: jsdoc
12
+ },
13
+ rules: {
14
+ // JSDoc相关规则 - 设置为error级别强制要求
15
+ 'jsdoc/require-jsdoc': ['error', {
16
+ require: {
17
+ FunctionDeclaration: true,
18
+ MethodDefinition: true,
19
+ ClassDeclaration: true,
20
+ ArrowFunctionExpression: true
21
+ }
22
+ }],
23
+ 'jsdoc/check-param-names': 'off',
24
+ 'jsdoc/check-tag-names': 'off',
25
+ 'jsdoc/check-types': 'off',
26
+ 'jsdoc/require-param': 'off',
27
+ 'jsdoc/require-param-description': 'off',
28
+ 'jsdoc/require-returns': 'off',
29
+ 'jsdoc/require-returns-description': 'off',
30
+
31
+ // 基础语法规则
32
+ 'no-unused-vars': ['error', { args: 'none', vars: 'all' }],
33
+ 'no-console': 'warn',
34
+
35
+ // 代码风格规则
36
+ 'indent': ['error', 2],
37
+ 'quotes': ['error', 'single'],
38
+ 'semi': ['error', 'always'],
39
+ 'comma-dangle': ['error', 'never'],
40
+ 'no-trailing-spaces': 'error',
41
+ 'eol-last': 'error',
42
+ 'max-len': ['error', { code: 100 }],
43
+ 'max-lines-per-function': ['error', { max: 40 }],
44
+ 'complexity': ['error', 12],
45
+ 'max-depth': ['error', 4],
46
+ 'max-params': ['error', 4],
47
+
48
+ // 命名规则
49
+ 'id-length': ['error', {
50
+ min: 1,
51
+ max: 20,
52
+ properties: 'always'
53
+ }]
54
+ }
55
+ },
56
+ {
57
+ files: ['test/**/*.js'],
58
+ languageOptions: {
59
+ ecmaVersion: 2020,
60
+ sourceType: 'script'
61
+ },
62
+ rules: {
63
+ // 测试文件中允许使用console
64
+ 'no-console': 'off',
65
+ 'no-unused-vars': 'off'
66
+ }
67
+ }
68
+ ];
package/index.js CHANGED
@@ -5,10 +5,11 @@
5
5
  */
6
6
  const http = require('http');
7
7
  const https = require('https');
8
- const Url = require('url');
9
8
  const HttpProxyAgent = require('https-proxy-agent');
10
9
  const fs = require("fs");
11
10
  const {
11
+ createGunzip,
12
+ createInflate,
12
13
  unzipSync
13
14
  } = require('zlib');
14
15
  const iconv = require('iconv-lite');
@@ -36,6 +37,11 @@ class Http {
36
37
  */
37
38
  this._cookies = {};
38
39
 
40
+ /**
41
+ * multipart表单边界值
42
+ */
43
+ this._boundary = "";
44
+
39
45
  /**
40
46
  * headers协议头
41
47
  */
@@ -56,9 +62,21 @@ class Http {
56
62
  var headers = this.changeHeaders(config.headers);
57
63
  Object.assign(this._headers, headers);
58
64
  }
65
+
66
+ this._logger = $.log || console;
59
67
  }
60
68
  }
61
69
 
70
+ /**
71
+ * 日志记录
72
+ * @param {string} level - 日志级别
73
+ * @param {string} message - 日志消息
74
+ * @param {...*} args - 日志参数
75
+ */
76
+ Http.prototype.log = function (level, message, ...args) {
77
+ this._logger[level](`[${this.constructor.name}] ${message}`, ...args);
78
+ }
79
+
62
80
  /**
63
81
  * 设置Cookie
64
82
  * @param {Object|String} cookies - Cookie字符串
@@ -66,14 +84,9 @@ class Http {
66
84
  Http.prototype.setCookie = function (cookies) {
67
85
  if (typeof (cookies) == "string") {
68
86
  var arr = cookies.split('; ');
69
- var name;
70
87
  if (arr.length > 0) {
71
- for (var i = 0; i < arr.length; i++) {
72
- var kv = arr[i];
73
- var kvArr = kv.split("=");
74
- name = kvArr[0];
75
- this._cookies[name] = encodeURIComponent(kvArr[1]);
76
- }
88
+ var kv_arr = arr[0].split("=");
89
+ this._cookies[kv_arr[0]] = encodeURIComponent(kv_arr[1]);
77
90
  }
78
91
  }
79
92
  else if (typeof (cookies) == "object") {
@@ -91,8 +104,8 @@ Http.prototype.getCookie = function () {
91
104
  for (var key in cookies) {
92
105
  str += "; " + key + "=" + cookies[key];
93
106
  }
94
- var cookieStr = str.replace("; ", "");
95
- return cookieStr;
107
+ var cookie_str = str.replace("; ", "");
108
+ return cookie_str;
96
109
  }
97
110
 
98
111
  /**
@@ -106,15 +119,15 @@ Http.prototype.changeHeaders = function (headers) {
106
119
  if (headers.indexOf("{") === 0) {
107
120
  return headers.toJson();
108
121
  } else {
109
- var headerDict = {};
122
+ var header_dict = {};
110
123
  var lines = headers.split('\n');
111
124
  for (var i = 0; i < lines.length; i++) {
112
125
  var line = lines[i].trim();
113
126
  if (line) {
114
- headerDict[line.left(":").trim()] = line.right(":").trim();
127
+ header_dict[line.left(":").trim()] = line.right(":").trim();
115
128
  }
116
129
  }
117
- return headerDict;
130
+ return header_dict;
118
131
  }
119
132
  } else {
120
133
  return headers;
@@ -150,22 +163,48 @@ Http.prototype.httpOptions = function (options) {
150
163
  headers,
151
164
  method,
152
165
  cookie,
153
- type
166
+ type,
167
+ stream_callback
154
168
  } = options;
155
- var urlObj = Url.parse(href, true);
169
+ var url_obj = new URL(href);
170
+ var query = {};
171
+ url_obj.searchParams.forEach(function (value, key) {
172
+ query[key] = value;
173
+ });
174
+ var port = url_obj.port ? parseInt(url_obj.port, 10) : (url_obj.protocol === "https:" ? 443 : 80);
175
+ url_obj = {
176
+ href: url_obj.href,
177
+ protocol: url_obj.protocol,
178
+ host: url_obj.host,
179
+ hostname: url_obj.hostname,
180
+ port: port,
181
+ pathname: url_obj.pathname,
182
+ search: url_obj.search,
183
+ path: url_obj.pathname + url_obj.search,
184
+ query: query,
185
+ hash: url_obj.hash
186
+ };
156
187
  if (cookie) {
157
188
  this.setCookie(cookie);
158
189
  }
159
190
  var protocol = href.left(":/") || 'http';
191
+ var http_headers = headers ? Object.assign({}, this._headers, headers) : Object.assign({}, this._headers);
192
+ var cookie_str = this.getCookie();
193
+ if (cookie_str) {
194
+ http_headers['Cookie'] = cookie_str;
195
+ }
160
196
  return Object.assign({}, {
161
- hostProtocol: protocol,
197
+ host_protocol: protocol,
162
198
  body,
163
199
  type,
164
200
  method: method || "GET",
165
- headers: headers ? Object.assign({}, this._headers, headers) : Object.assign({}, this._headers),
201
+ headers: http_headers,
166
202
  agent: cfg.proxy ? new HttpProxyAgent(cfg.proxy) : null,
167
- cookie: this.getCookie()
168
- }, urlObj);
203
+ rejectUnauthorized: cfg.rejectUnauthorized,
204
+ timeout: options.timeout || cfg.timeout || 0,
205
+ stream_callback: stream_callback,
206
+ cookie: cookie_str
207
+ }, url_obj);
169
208
  }
170
209
 
171
210
  /**
@@ -174,47 +213,78 @@ Http.prototype.httpOptions = function (options) {
174
213
  */
175
214
  Http.prototype.postOption = function (op) {
176
215
  var param = op.body;
177
- var bodyStr = "";
178
- var paramType = typeof (param);
179
- var contentType = op.type;
180
- if (paramType === "object") {
181
- if (!contentType) {
182
- contentType = 'application/json; charset=UTF-8';
183
- } else if (contentType.indexOf('json') !== -1) {
184
- contentType = "application/json; charset=UTF-8";
185
- } else if (contentType.indexOf('html') !== -1) {
186
- contentType = "text/html; charset=UTF-8";
187
- } else if (contentType.indexOf('form') !== -1) {
188
- contentType = "application/x-www-form-urlencoded";
189
- } else {
190
- contentType = "text/plain; charset=UTF-8";
216
+ var body_str = "";
217
+ var param_type = typeof (param);
218
+ var content_type = op.type;
219
+ if (param_type === "object") {
220
+ if (!content_type) {
221
+ content_type = 'application/json; charset=UTF-8';
191
222
  }
192
- if (contentType.indexOf('/json') !== -1) {
193
- bodyStr = JSON.stringify(param);
194
- } else if (contentType.indexOf('form') !== -1) {
195
- bodyStr = $.toUrl(param);
223
+ if (content_type.indexOf('multipart') !== -1 || content_type.indexOf('form-data') !== -1) {
224
+ body_str = this.multipartBody(param);
225
+ content_type = "multipart/form-data; boundary=" + this._boundary;
226
+ } else if (content_type.indexOf('json') !== -1) {
227
+ body_str = JSON.stringify(param);
228
+ content_type = "application/json; charset=UTF-8";
229
+ } else if (content_type.indexOf('html') !== -1) {
230
+ body_str = $.toXml(param);
231
+ content_type = "text/html; charset=UTF-8";
232
+ } else if (content_type.indexOf('form') !== -1) {
233
+ body_str = $.toUrl(param);
234
+ content_type = "application/x-www-form-urlencoded";
196
235
  } else {
197
- bodyStr = $.toXml(param);
236
+ body_str = $.toXml(param);
237
+ content_type = "text/plain; charset=UTF-8";
198
238
  }
199
239
  } else {
200
- bodyStr = param.trim();
201
- if (!contentType) {
202
- if (bodyStr.startsWith('{') && bodyStr.endsWith('}')) {
203
- contentType = "application/json; charset=UTF-8";
204
- } else if (bodyStr.startsWith('[') && bodyStr.endsWith(']')) {
205
- contentType = "application/json; charset=UTF-8";
206
- } else if (bodyStr.indexOf("<html>") !== -1) {
207
- contentType = "text/html";
240
+ body_str = param.trim();
241
+ if (!content_type) {
242
+ if (body_str.startsWith('{') && body_str.endsWith('}')) {
243
+ content_type = "application/json; charset=UTF-8";
244
+ } else if (body_str.startsWith('[') && body_str.endsWith(']')) {
245
+ content_type = "application/json; charset=UTF-8";
246
+ } else if (body_str.indexOf("<html>") !== -1) {
247
+ content_type = "text/html";
208
248
  } else {
209
- contentType = "text/plain; charset=UTF-8";
249
+ content_type = "text/plain; charset=UTF-8";
210
250
  }
211
251
  }
212
252
  }
213
- op.body = bodyStr;
253
+ op.body = body_str;
214
254
  if (!op.headers['Content-Type']) {
215
- op.headers['Content-Type'] = contentType;
255
+ op.headers['Content-Type'] = content_type;
256
+ }
257
+ op.headers['Content-Length'] = Buffer.byteLength(body_str);
258
+ }
259
+
260
+ /**
261
+ * 生成multipart/form-data请求体
262
+ * @param {Object} param - 表单参数,文件字段值为 {value, filename, content_type}
263
+ * @returns {Buffer} multipart请求体
264
+ */
265
+ Http.prototype.multipartBody = function (param) {
266
+ var boundary = "----mm_https_" + Date.now().toString(16);
267
+ this._boundary = boundary;
268
+ var chunks = [];
269
+ for (var key in param) {
270
+ var value = param[key];
271
+ var value_type = typeof (value);
272
+ if (value_type === "object" && value !== null) {
273
+ var filename = value.filename || key;
274
+ var file_type = value.content_type || "application/octet-stream";
275
+ var content = value.value;
276
+ if (!Buffer.isBuffer(content)) {
277
+ content = Buffer.from(String(content));
278
+ }
279
+ chunks.push(Buffer.from('--' + boundary + '\r\nContent-Disposition: form-data; name="' + key + '"; filename="' + filename + '"\r\nContent-Type: ' + file_type + '\r\n\r\n'));
280
+ chunks.push(content);
281
+ chunks.push(Buffer.from('\r\n'));
282
+ } else {
283
+ chunks.push(Buffer.from('--' + boundary + '\r\nContent-Disposition: form-data; name="' + key + '"\r\n\r\n' + value + '\r\n'));
284
+ }
216
285
  }
217
- op.headers['Content-Length'] = Buffer.byteLength(bodyStr);
286
+ chunks.push(Buffer.from('--' + boundary + '--\r\n'));
287
+ return Buffer.concat(chunks);
218
288
  }
219
289
 
220
290
  /**
@@ -228,57 +298,49 @@ Http.prototype.req = async function (options) {
228
298
  this.postOption(op);
229
299
  }
230
300
  var res = await this.request(op);
231
- if (res.status.code == 302 || res.status.code == 307) {
232
- var redirectUrl = res.headers.location;
233
- var cookieList = res.headers['set-cookie'];
234
- if (cookieList) {
235
- var len = cookieList.length;
236
- for (var i = 0; i < len; i++) {
237
- this.setCookie(cookieList[i]);
238
- }
301
+ var redirects = [301, 302, 303, 307, 308];
302
+ var count = 0;
303
+ while (redirects.indexOf(res.status.code) !== -1) {
304
+ if (++count > 5) {
305
+ throw new Error("重定向次数超过上限(5次)");
306
+ }
307
+ var location = res.headers.location;
308
+ if (!location) {
309
+ break;
239
310
  }
240
- options.href = redirectUrl;
311
+ var redirect_url = new URL(location, op.href).href;
312
+ options.href = redirect_url;
241
313
  op = this.httpOptions(options);
242
- op.referer = options.href;
314
+ op.referer = redirect_url;
243
315
  res = await this.request(op);
244
316
  }
245
317
  return res;
246
318
  }
247
319
 
248
320
  /**
249
- * 解压数据
250
- * @param {string} encoding - 编码方式
321
+ * 解压数据流
322
+ * @param {Stream} stream - 输入数据流
323
+ * @param {string} encoding - 压缩方式,gzip使用gunzip,其他使用inflate
324
+ * @param {string} charset - 字符编码,指定时返回字符串
325
+ * @returns {Promise<Buffer|string>} 解压后的数据
251
326
  */
252
- Http.prototype.undata = function (encoding) {
253
- var bufarr = [];
254
- var errored = false;
255
- var zpipe;
256
- if (encoding) {
257
- zpipe = zlib.createGunzip();
258
- } else {
259
- zpipe = zlib.createInflate();
260
- }
261
- zpipe.on('data', function (d) {
262
- bufarr.push(d);
263
- })
264
- .on('end', function () {
265
- if (errored) return;
266
- errored = true;
267
- cb(null, response, enc ? Buffer.concat(bufarr).toString(
268
- enc) : Buffer.concat(bufarr));
269
- })
270
- .on('error', function (err) {
271
- if (errored) return;
272
- errored = true;
273
- cb(err, response, null);
327
+ Http.prototype.undata = function (stream, encoding, charset) {
328
+ var buf_arr = [];
329
+ return new Promise(function (resolve, reject) {
330
+ var zpipe = encoding ? createGunzip() : createInflate();
331
+ zpipe.on('data', function (d) {
332
+ buf_arr.push(d);
333
+ }).on('end', function () {
334
+ var buffer = Buffer.concat(buf_arr);
335
+ resolve(charset ? buffer.toString(charset) : buffer);
336
+ }).on('error', function (err) {
337
+ reject(err);
274
338
  });
275
- response.pipe(zpipe);
276
- response
277
- .on('error', function (err) {
278
- if (errored) return;
279
- errored = true;
280
- cb(err, response, null);
339
+ stream.on('error', function (err) {
340
+ reject(err);
281
341
  });
342
+ stream.pipe(zpipe);
343
+ });
282
344
  }
283
345
 
284
346
  /**
@@ -287,7 +349,7 @@ Http.prototype.undata = function (encoding) {
287
349
  * @returns {Promise<Object>} 请求结果
288
350
  */
289
351
  Http.prototype.request = function (options) {
290
- var protocol = options.hostProtocol === "https" ? https : http;
352
+ var protocol = options.host_protocol === "https" ? https : http;
291
353
  var self = this;
292
354
  return new Promise(function (resolve, reject) {
293
355
  var body = '';
@@ -295,69 +357,96 @@ Http.prototype.request = function (options) {
295
357
  var {
296
358
  headers
297
359
  } = res;
360
+ var status_code = res.statusCode;
361
+ var cookie_list = headers['set-cookie'];
362
+ if (cookie_list) {
363
+ var len = cookie_list.length;
364
+ for (var i = 0; i < len; i++) {
365
+ self.setCookie(cookie_list[i]);
366
+ }
367
+ }
298
368
 
299
- if (res.statusCode == 200) {
300
- var chunks = [];
301
- var encoding = headers['content-encoding'];
369
+ // 流式下载:仅对成功响应生效
370
+ if (status_code >= 200 && status_code < 300 && options.stream_callback) {
371
+ var stream = options.stream_callback(headers);
372
+ if (stream) {
373
+ res.on('error', function (err) {
374
+ reject(err);
375
+ });
376
+ res.pipe(stream);
377
+ stream.on('finish', function () {
378
+ resolve({
379
+ status: status_code,
380
+ headers
381
+ });
382
+ });
383
+ stream.on('error', function (err) {
384
+ reject(err);
385
+ });
386
+ return;
387
+ }
388
+ }
302
389
 
303
- res.on('data', function (chunk) {
304
- chunks.push(chunk);
305
- }).on('end', function () {
306
- var buffer = Buffer.concat(chunks);
307
- switch (encoding) {
308
- case 'br':
309
- case 'gzip':
310
- case 'deflate':
311
- buffer = unzipSync(buffer);
312
- body = buffer.toString();
313
- break;
314
- default:
315
- body = buffer.toString();
316
- break;
317
- }
318
- var charsetMatch = body.match(/charset=gb2312"/i);
319
- if (charsetMatch) {
320
- body = iconv.decode(buffer, 'gb2312');
321
- } else {
322
- charsetMatch = body.match(/charset=GBK"/i);
323
- if (charsetMatch) {
324
- body = iconv.decode(buffer, 'GBK');
325
- }
326
- }
327
- var cookieList = headers['set-cookie'];
328
- if (cookieList) {
329
- var len = cookieList.length;
330
- for (var i = 0; i < len; i++) {
331
- self.setCookie(cookieList[i]);
332
- }
390
+ var chunks = [];
391
+ var encoding = headers['content-encoding'];
392
+
393
+ res.on('data', function (chunk) {
394
+ chunks.push(chunk);
395
+ }).on('end', function () {
396
+ var buffer = Buffer.concat(chunks);
397
+ switch (encoding) {
398
+ case 'br':
399
+ case 'gzip':
400
+ case 'deflate':
401
+ buffer = unzipSync(buffer);
402
+ body = buffer.toString();
403
+ break;
404
+ default:
405
+ body = buffer.toString();
406
+ break;
407
+ }
408
+ var charset_match = body.match(/charset=gb2312"/i);
409
+ if (charset_match) {
410
+ body = iconv.decode(buffer, 'gb2312');
411
+ } else {
412
+ charset_match = body.match(/charset=GBK"/i);
413
+ if (charset_match) {
414
+ body = iconv.decode(buffer, 'GBK');
333
415
  }
416
+ }
417
+ if (status_code >= 200 && status_code < 300) {
334
418
  resolve({
335
- status: 200,
419
+ status: status_code,
336
420
  headers,
337
421
  buffer,
338
422
  body
339
423
  });
340
- });
341
- } else {
342
- resolve({
343
- headers,
344
- status: {
345
- code: res.statusCode,
346
- message: res.statusMessage
347
- }
348
- });
349
- reject(res);
350
- }
424
+ } else {
425
+ resolve({
426
+ headers,
427
+ status: {
428
+ code: status_code,
429
+ message: res.statusMessage
430
+ },
431
+ buffer,
432
+ body
433
+ });
434
+ }
435
+ });
351
436
  });
352
437
 
353
438
  req.on("error", function (err) {
354
- $.log.debug("错误", err);
439
+ self.log("debug", "错误", err);
355
440
  resolve({
356
441
  status: 0,
357
442
  message: err.message
358
443
  });
359
- reject(err);
360
444
  });
445
+ if (options.timeout) {
446
+ req.setTimeout(options.timeout, function () {
447
+ req.destroy(new Error("请求超时"));
448
+ });
449
+ }
361
450
  if (options.method.toLocaleUpperCase() == "POST") {
362
451
  req.write(options.body);
363
452
  }
@@ -401,6 +490,25 @@ Http.prototype.post = async function (url, body, headers, type, cookie) {
401
490
  });
402
491
  };
403
492
 
493
+ /**
494
+ * 上传文件
495
+ * @param {string} url - 请求地址
496
+ * @param {Object} body - 表单参数,文件字段值为 {value: Buffer|string, filename, content_type}
497
+ * @param {Object} headers - 请求头
498
+ * @param {string} cookie - Cookie字符串
499
+ * @returns {Promise<Object>} 响应结果
500
+ */
501
+ Http.prototype.upload = async function (url, body, headers, cookie) {
502
+ return await this.req({
503
+ method: "POST",
504
+ href: url,
505
+ body,
506
+ type: "form-data",
507
+ headers,
508
+ cookie
509
+ });
510
+ };
511
+
404
512
  /**
405
513
  * 下载文件
406
514
  * @param {string} url - 下载地址
@@ -411,36 +519,36 @@ Http.prototype.post = async function (url, body, headers, type, cookie) {
411
519
  * @returns {Promise<string>} 成功返回保存路径,失败返回null
412
520
  */
413
521
  Http.prototype.download = async function (url, filename, auto, headers, cookie) {
414
- var res = await this.get(url, headers, cookie);
415
- var filePath = null;
416
- if (res.body) {
417
- if (auto) {
418
- var parts = url.split('/');
419
- var fullName = parts[parts.length - 1];
420
- if (fullName.indexOf('.') !== -1) {
421
- var ext = fullName.right('.');
422
- if (ext) {
423
- filePath = filename + "." + ext;
522
+ var self = this;
523
+ var file_path = null;
524
+ await this.request(this.httpOptions({
525
+ method: "GET",
526
+ href: url,
527
+ headers,
528
+ cookie,
529
+ stream_callback: function (res_headers) {
530
+ var ext = "";
531
+ if (auto) {
532
+ var parts = url.split('/');
533
+ var full_name = parts[parts.length - 1];
534
+ if (full_name.indexOf('.') !== -1) {
535
+ ext = full_name.right('.');
536
+ }
537
+ if (!ext) {
538
+ var content_type = res_headers["content-type"] || "";
539
+ ext = content_type.replace("image/", "").replace("application/x-", "").replace("text/", "")
540
+ .replace("audio/", "").replace("jpeg", "jpg").split(";")[0].trim();
424
541
  }
425
- } else {
426
- var contentType = res.headers["content-type"];
427
- var ext = contentType.replace("image/", "").replace("application/x-", "").replace("text/", "")
428
- .replace("audio/", "").replace("jpeg", "jpg");
429
- filePath = filename + "." + ext;
430
542
  }
431
- } else {
432
- filePath = filename;
433
- }
434
- if (filePath) {
435
- var bytesWritten = await fs.writeFileSync(filePath, res.buffer, "binary");
436
- if (bytesWritten == 0) {
437
- filePath = null;
543
+ file_path = ext ? filename + "." + ext : filename;
544
+ if (!file_path) {
545
+ self.log("debug", '保存文件名(filename)不能为空!');
546
+ return null;
438
547
  }
439
- } else {
440
- $.log.debug('保存文件名(filename)不能为!');
548
+ return fs.createWriteStream(file_path);
441
549
  }
442
- }
443
- return filePath;
550
+ }));
551
+ return file_path;
444
552
  };
445
553
 
446
554
  /**
@@ -450,10 +558,6 @@ Http.prototype.clear = function () {
450
558
  this._cookies = {};
451
559
  };
452
560
 
453
- if (global.$ && !$.Http) {
454
- $.Http = Http;
455
- }
456
-
457
561
  /**
458
562
  * 导出构造函数
459
563
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mm_https",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "description": "超级美眉HTTP请求模块 - 支持自动Cookie管理、代理设置、文件下载等功能",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -33,8 +33,8 @@
33
33
  "node": ">=12.0.0"
34
34
  },
35
35
  "dependencies": {
36
- "https-proxy-agent": "^7.0.6",
37
- "iconv-lite": "^0.7.0",
38
- "mm_expand": "^1.9.7"
36
+ "https-proxy-agent": "^9.1.0",
37
+ "iconv-lite": "^0.7.3",
38
+ "mm_expand": "^2.5.0"
39
39
  }
40
40
  }
package/test.js CHANGED
@@ -1,168 +1,214 @@
1
- /**
2
- * mm_https模块测试文件
3
- * 用于npm发布前的功能验证
4
- */
5
-
6
- const {
7
- Http
8
- } = require('./index.js');
9
-
10
- /**
11
- * 基础HTTP请求测试
12
- */
13
- async function testBasicRequests() {
14
- console.log('=== 基础HTTP请求测试 ===');
15
-
16
- const http = new Http();
17
-
18
- try {
19
- // 测试GET请求
20
- console.log('1. 测试GET请求...');
21
- const getRes = await http.get('https://httpbin.org/get');
22
- console.log('GET请求状态码:', getRes.status);
23
- console.log('GET请求成功');
24
-
25
- // 测试POST请求
26
- console.log('2. 测试POST请求...');
27
- const postData = { test: 'data', value: 123 };
28
- const postRes = await http.post('https://httpbin.org/post', postData);
29
- console.log('POST请求状态码:', postRes.status);
30
- console.log('POST请求成功');
31
-
32
- // 测试Cookie管理
33
- console.log('3. 测试Cookie管理...');
34
- http.setCookie('session=test_session; user=test_user');
35
- const cookieStr = http.getCookie();
36
- console.log('当前Cookie:', cookieStr);
37
-
38
- console.log('✅ 基础功能测试通过');
39
- return true;
40
- } catch (err) {
41
- console.error('❌ 基础功能测试失败:', err.message);
42
- return false;
43
- }
44
- }
45
-
46
- /**
47
- * 高级功能测试
48
- */
49
- async function testAdvancedFeatures() {
50
- console.log('\n=== 高级功能测试 ===');
51
-
52
- const http = new Http({
53
- headers: {
54
- 'User-Agent': 'Test-Agent/1.0'
55
- },
56
- cookie: 'initial=test_cookie'
57
- });
58
-
59
- try {
60
- // 测试自定义请求头
61
- console.log('1. 测试自定义请求头...');
62
- const headersRes = await http.get('https://httpbin.org/headers');
63
- console.log('自定义请求头测试成功');
64
-
65
- // 测试URL参数解析
66
- console.log('2. 测试URL参数解析...');
67
- const options = http.options();
68
- console.log('默认选项配置成功:', typeof options);
69
-
70
- // 测试HTTP选项配置
71
- console.log('3. 测试HTTP选项配置...');
72
- const httpOptions = http.httpOptions({
73
- method: 'GET',
74
- href: 'https://httpbin.org/get'
75
- });
76
- console.log('HTTP选项配置成功');
77
-
78
- console.log('✅ 高级功能测试通过');
79
- return true;
80
- } catch (err) {
81
- console.error('❌ 高级功能测试失败:', err.message);
82
- return false;
83
- }
84
- }
85
-
86
- /**
87
- * 错误处理测试
88
- */
89
- async function testErrorHandling() {
90
- console.log('\n=== 错误处理测试 ===');
91
-
92
- const http = new Http();
93
-
94
- try {
95
- // 测试不存在的域名
96
- console.log('1. 测试不存在的域名...');
97
- try {
98
- // 设置较短的超时时间,避免长时间等待
99
- const timeoutHttp = new Http();
100
- // 使用不存在的域名进行测试
101
- const res = await timeoutHttp.get('http://nonexistent-domain-12345.test');
102
- console.log('GET响应状态码:', res.status, '响应消息:', res.message, '响应内容:', res.body);
103
- console.log('⚠️ 不存在的域名测试 - 请求可能被重定向或超时');
104
- } catch (err) {
105
- console.log('不存在的域名处理正确:', err.message);
106
- }
107
-
108
- // 测试网络错误处理
109
- console.log('2. 测试网络错误处理...');
110
- try {
111
- // 尝试访问一个无效的端口
112
- await http.get('http://localhost:99999');
113
- console.log('⚠️ 无效端口测试 - 请求可能被系统拒绝');
114
- } catch (err) {
115
- console.log('无效端口处理正确:', err.message);
116
- }
117
-
118
- console.log('✅ 错误处理测试通过');
119
- return true;
120
- } catch (err) {
121
- console.error('❌ 错误处理测试失败:', err.message);
122
- return false;
123
- }
124
- }
125
-
126
- /**
127
- * 主测试函数
128
- */
129
- async function runTests() {
130
- console.log('开始mm_https模块测试...\n');
131
-
132
- const results = [];
133
-
134
- // 运行所有测试
135
- results.push(await testBasicRequests());
136
- results.push(await testAdvancedFeatures());
137
- results.push(await testErrorHandling());
138
-
139
- // 统计结果
140
- const passed = results.filter(r => r).length;
141
- const total = results.length;
142
-
143
- console.log('\n=== 测试结果汇总 ===');
144
- console.log(`通过: ${passed}/${total}`);
145
-
146
- if (passed === total) {
147
- console.log('🎉 所有测试通过!模块功能正常。');
148
- process.exit(0);
149
- } else {
150
- console.log('⚠️ 部分测试失败,请检查模块功能。');
151
- process.exit(1);
152
- }
153
- }
154
-
155
- // 如果直接运行此文件,则执行测试
156
- if (require.main === module) {
157
- runTests().catch(err => {
158
- console.error('测试执行失败:', err);
159
- process.exit(1);
160
- });
161
- }
162
-
163
- module.exports = {
164
- testBasicRequests,
165
- testAdvancedFeatures,
166
- testErrorHandling,
167
- runTests
168
- };
1
+ /**
2
+ * mm_https 模块功能测试
3
+ * @author 邱文武
4
+ * @description 启动本地服务器,测试 GET/POST/upload/download/重定向/超时/cookie/undata 等功能
5
+ */
6
+ const http = require('http');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { Readable } = require('stream');
10
+ const zlib = require('zlib');
11
+ const { Http } = require('./index.js');
12
+
13
+ let server_port;
14
+ let pass_count = 0;
15
+ let fail_count = 0;
16
+
17
+ /**
18
+ * 断言测试结果
19
+ * @param {string} name - 测试名称
20
+ * @param {boolean} cond - 断言条件
21
+ * @param {string} detail - 失败详情
22
+ */
23
+ function assert(name, cond, detail) {
24
+ if (cond) {
25
+ pass_count++;
26
+ console.log(' [通过] ' + name);
27
+ } else {
28
+ fail_count++;
29
+ console.log(' [失败] ' + name + (detail ? ' => ' + detail : ''));
30
+ }
31
+ }
32
+
33
+ /**
34
+ * 启动本地测试服务器
35
+ * @returns {http.Server} 服务器实例
36
+ */
37
+ function startServer() {
38
+ var server = http.createServer(function (req, res) {
39
+ var url = req.url;
40
+ if (url.startsWith('/get')) {
41
+ res.writeHead(200, { 'Content-Type': 'application/json' });
42
+ res.end(JSON.stringify({ ok: true, path: url }));
43
+ } else if (url.startsWith('/post')) {
44
+ var body = [];
45
+ req.on('data', function (c) { body.push(c); });
46
+ req.on('end', function () {
47
+ res.writeHead(200, { 'Content-Type': 'application/json' });
48
+ res.end(JSON.stringify({ type: req.headers['content-type'], body: Buffer.concat(body).toString() }));
49
+ });
50
+ } else if (url.startsWith('/upload')) {
51
+ var body = [];
52
+ req.on('data', function (c) { body.push(c); });
53
+ req.on('end', function () {
54
+ res.writeHead(200, { 'Content-Type': 'application/json' });
55
+ res.end(JSON.stringify({ type: req.headers['content-type'], length: req.headers['content-length'] }));
56
+ });
57
+ } else if (url.startsWith('/download-image')) {
58
+ res.writeHead(200, { 'Content-Type': 'image/png' });
59
+ res.end(Buffer.from([137, 80, 78, 71]));
60
+ } else if (url.startsWith('/download')) {
61
+ res.writeHead(200, { 'Content-Type': 'application/octet-stream' });
62
+ res.end(Buffer.from([1, 2, 3, 4, 5]));
63
+ } else if (url.startsWith('/redirect-relative')) {
64
+ res.writeHead(302, { 'Location': '/get' });
65
+ res.end();
66
+ } else if (url.startsWith('/redirect-absolute')) {
67
+ res.writeHead(301, { 'Location': 'http://127.0.0.1:' + server_port + '/get' });
68
+ res.end();
69
+ } else if (url.startsWith('/redirect-loop')) {
70
+ res.writeHead(302, { 'Location': '/redirect-loop' });
71
+ res.end();
72
+ } else if (url.startsWith('/redirect-empty')) {
73
+ res.writeHead(302, {});
74
+ res.end();
75
+ } else if (url.startsWith('/slow')) {
76
+ setTimeout(function () {
77
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
78
+ res.end('slow response');
79
+ }, 3000);
80
+ } else if (url.startsWith('/cookie')) {
81
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Set-Cookie': 'test=abc; Path=/' });
82
+ res.end('{}');
83
+ } else if (url.startsWith('/check-cookie')) {
84
+ res.writeHead(200, { 'Content-Type': 'application/json' });
85
+ res.end(JSON.stringify({ cookie: req.headers['cookie'] || '' }));
86
+ } else {
87
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
88
+ res.end('not found');
89
+ }
90
+ });
91
+ return server;
92
+ }
93
+
94
+ /**
95
+ * 主测试流程
96
+ */
97
+ async function main() {
98
+ console.log('== mm_https 模块功能测试 ==');
99
+ var server = startServer();
100
+ await new Promise(function (resolve) { server.listen(0, resolve); });
101
+ server_port = server.address().port;
102
+ var client = new Http({ proxy: null });
103
+ var base = 'http://127.0.0.1:' + server_port;
104
+ var tmp_dir = path.join(__dirname, 'tmp_test');
105
+ fs.mkdirSync(tmp_dir, { recursive: true });
106
+ try {
107
+ // 1. GET
108
+ console.log('\n[GET]');
109
+ var res = await client.get(base + '/get?page=1&size=10');
110
+ assert('GET 返回200', res.status === 200);
111
+ assert('GET 保留查询参数', res.body.indexOf('page=1') !== -1);
112
+
113
+ // 2. POST json
114
+ console.log('\n[POST json]');
115
+ res = await client.post(base + '/post', { a: 1, b: 'x' }, null, 'json');
116
+ var data = JSON.parse(res.body);
117
+ assert('POST json 类型正确', data.type.indexOf('application/json') === 0);
118
+ assert('POST json 内容正确', data.body === '{"a":1,"b":"x"}');
119
+
120
+ // 3. POST form
121
+ console.log('\n[POST form]');
122
+ res = await client.post(base + '/post', { a: 1, b: 'x' }, null, 'form');
123
+ data = JSON.parse(res.body);
124
+ assert('POST form 类型正确', data.type.indexOf('application/x-www-form-urlencoded') === 0);
125
+ assert('POST form 内容正确', data.body.indexOf('a=1') !== -1 && data.body.indexOf('b=x') !== -1);
126
+
127
+ // 4. POST text
128
+ console.log('\n[POST text]');
129
+ res = await client.post(base + '/post', 'plain text body', null, 'text');
130
+ data = JSON.parse(res.body);
131
+ assert('POST text 内容正确', data.body === 'plain text body');
132
+
133
+ // 5. upload
134
+ console.log('\n[UPLOAD]');
135
+ res = await client.upload(base + '/upload', {
136
+ name: '测试文件',
137
+ file: { value: Buffer.from([1, 2, 3]), filename: 'a.bin', contentType: 'application/octet-stream' }
138
+ });
139
+ data = JSON.parse(res.body);
140
+ assert('UPLOAD 类型为multipart', data.type.indexOf('multipart/form-data; boundary=') === 0);
141
+ assert('UPLOAD 携带Content-Length', parseInt(data.length, 10) > 0);
142
+
143
+ // 6. download
144
+ console.log('\n[DOWNLOAD]');
145
+ var file_path = await client.download(base + '/download', path.join(tmp_dir, 'dl'));
146
+ assert('DOWNLOAD 返回文件路径', !!file_path);
147
+ var buf = fs.readFileSync(file_path);
148
+ assert('DOWNLOAD 文件内容正确', buf.length === 5 && buf[0] === 1 && buf[4] === 5);
149
+ file_path = await client.download(base + '/download-image', path.join(tmp_dir, 'img'), true);
150
+ assert('DOWNLOAD auto扩展名', file_path === path.join(tmp_dir, 'img') + '.png');
151
+
152
+ // 7. 重定向
153
+ console.log('\n[REDIRECT]');
154
+ res = await client.get(base + '/redirect-relative');
155
+ assert('302 相对路径重定向', res.status === 200 && res.body.indexOf('ok') !== -1);
156
+ res = await client.get(base + '/redirect-absolute');
157
+ assert('301 绝对路径重定向', res.status === 200);
158
+ res = await client.get(base + '/redirect-empty');
159
+ assert('302 无Location不崩溃', res.status.code === 302);
160
+ var loop_error = null;
161
+ try {
162
+ await client.get(base + '/redirect-loop');
163
+ } catch (e) {
164
+ loop_error = e;
165
+ }
166
+ assert('重定向循环受限', loop_error && loop_error.message.indexOf('重定向次数超过上限') !== -1);
167
+
168
+ // 8. 非2xx状态码
169
+ console.log('\n[状态码]');
170
+ res = await client.get(base + '/status-404');
171
+ assert('404 返回状态对象', res.status && res.status.code === 404);
172
+ assert('404 携带响应体', res.body.indexOf('not found') !== -1);
173
+
174
+ // 9. cookie
175
+ console.log('\n[COOKIE]');
176
+ var cookie_client = new Http({ proxy: null });
177
+ await cookie_client.get(base + '/cookie');
178
+ res = await cookie_client.get(base + '/check-cookie');
179
+ data = JSON.parse(res.body);
180
+ assert('Cookie 自动保持', data.cookie.indexOf('test=abc') !== -1);
181
+
182
+ // 10. 超时
183
+ console.log('\n[TIMEOUT]');
184
+ var slow_client = new Http({ proxy: null, timeout: 500 });
185
+ var start = Date.now();
186
+ res = await slow_client.get(base + '/slow');
187
+ var elapsed = Date.now() - start;
188
+ assert('超时返回错误对象', res.status === 0 && res.message.indexOf('超时') !== -1);
189
+ assert('超时耗时合理(<2秒)', elapsed < 2000);
190
+
191
+ // 11. undata 解压
192
+ console.log('\n[UNDATA]');
193
+ var gz = zlib.gzipSync(Buffer.from('hello undata'));
194
+ var reader = new Readable({ read: function () { this.push(gz); this.push(null); } });
195
+ var out = await client.undata(reader, 'gzip', 'utf8');
196
+ assert('undata gzip解压', out === 'hello undata');
197
+ var def = zlib.deflateSync(Buffer.from('hello inflate'));
198
+ var reader2 = new Readable({ read: function () { this.push(def); this.push(null); } });
199
+ out = await client.undata(reader2, null, 'utf8');
200
+ assert('undata deflate解压', out === 'hello inflate');
201
+ } catch (e) {
202
+ console.error('测试异常:', e);
203
+ fail_count++;
204
+ } finally {
205
+ server.close();
206
+ fs.rmSync(tmp_dir, { recursive: true, force: true });
207
+ console.log('\n== 测试结果: 通过 ' + pass_count + ', 失败 ' + fail_count + ' ==');
208
+ if (fail_count > 0) {
209
+ process.exitCode = 1;
210
+ }
211
+ }
212
+ }
213
+
214
+ main();
@@ -1,99 +0,0 @@
1
- # mm_https模块 npm发布检查清单
2
-
3
- ## ✅ 已完成的项目
4
-
5
- ### 1. 代码重构和规范
6
- - [x] 类名和方法名使用小驼峰命名法
7
- - [x] 私有成员使用下划线前缀
8
- - [x] 变量名使用小写蛇形命名法
9
- - [x] 完整的JSDoc注释
10
- - [x] 符合ES6类和原型方法范式
11
-
12
- ### 2. 测试文件优化
13
- - [x] 创建适合npm发布的测试文件
14
- - [x] 包含基础功能测试
15
- - [x] 包含高级功能测试
16
- - [x] 包含错误处理测试
17
- - [x] 所有测试通过
18
-
19
- ### 3. 配置文件优化
20
- - [x] 更新package.json描述和关键词
21
- - [x] 添加Node.js版本要求
22
- - [x] 优化依赖管理
23
-
24
- ### 4. 功能验证
25
- - [x] 语法检查通过
26
- - [x] 依赖关系正常
27
- - [x] 模块功能完整
28
-
29
- ## 📋 npm发布前检查
30
-
31
- ### 版本管理
32
- - [ ] 确认当前版本号:1.6.4
33
- - [ ] 是否需要更新版本号
34
- - [ ] 更新CHANGELOG.md(如有)
35
-
36
- ### 文档检查
37
- - [ ] README.md内容完整且准确
38
- - [ ] 示例代码正确
39
- - [ ] 安装说明清晰
40
-
41
- ### 发布准备
42
- - [ ] 登录npm账号:`npm login`
43
- - [ ] 检查包名可用性:`npm search mm_https`
44
- - [ ] 执行发布命令:`npm publish`
45
-
46
- ## 🚀 发布命令
47
-
48
- ```bash
49
- # 登录npm(如果未登录)
50
- npm login
51
-
52
- # 检查包名是否可用
53
- npm search mm_https
54
-
55
- # 发布到npm
56
- npm publish
57
-
58
- # 或者发布为公开包(如果需要)
59
- npm publish --access public
60
- ```
61
-
62
- ## 📦 安装使用
63
-
64
- 发布后,用户可以通过以下方式安装:
65
-
66
- ```bash
67
- npm install mm_https
68
- ```
69
-
70
- 使用示例:
71
- ```javascript
72
- const { Http } = require('mm_https');
73
-
74
- async function example() {
75
- const http = new Http();
76
- const res = await http.get('https://httpbin.org/get');
77
- console.log(res.body);
78
- }
79
- ```
80
-
81
- ## 🔧 开发命令
82
-
83
- ```bash
84
- # 运行测试
85
- npm test
86
-
87
- # 开发模式
88
- npm run dev
89
-
90
- # 安装依赖
91
- npm install
92
- ```
93
-
94
- ## 📞 支持信息
95
-
96
- - 作者:邱文武
97
- - 许可证:ISC
98
- - 问题反馈:https://gitee.com/qiuwenwu91/mm_https/issues
99
- - 主页:https://gitee.com/qiuwenwu91/mm_https#readme