mm_https 1.6.6 → 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.
Files changed (4) hide show
  1. package/eslint.config.js +67 -67
  2. package/index.js +259 -163
  3. package/package.json +4 -4
  4. package/test.js +214 -168
package/eslint.config.js CHANGED
@@ -1,68 +1,68 @@
1
- const jsdoc = require('eslint-plugin-jsdoc');
2
-
3
- module.exports = [
4
- {
5
- files: ['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
- }
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
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
  */
@@ -78,14 +84,9 @@ Http.prototype.log = function (level, message, ...args) {
78
84
  Http.prototype.setCookie = function (cookies) {
79
85
  if (typeof (cookies) == "string") {
80
86
  var arr = cookies.split('; ');
81
- var name;
82
87
  if (arr.length > 0) {
83
- for (var i = 0; i < arr.length; i++) {
84
- var kv = arr[i];
85
- var kvArr = kv.split("=");
86
- name = kvArr[0];
87
- this._cookies[name] = encodeURIComponent(kvArr[1]);
88
- }
88
+ var kv_arr = arr[0].split("=");
89
+ this._cookies[kv_arr[0]] = encodeURIComponent(kv_arr[1]);
89
90
  }
90
91
  }
91
92
  else if (typeof (cookies) == "object") {
@@ -103,8 +104,8 @@ Http.prototype.getCookie = function () {
103
104
  for (var key in cookies) {
104
105
  str += "; " + key + "=" + cookies[key];
105
106
  }
106
- var cookieStr = str.replace("; ", "");
107
- return cookieStr;
107
+ var cookie_str = str.replace("; ", "");
108
+ return cookie_str;
108
109
  }
109
110
 
110
111
  /**
@@ -118,15 +119,15 @@ Http.prototype.changeHeaders = function (headers) {
118
119
  if (headers.indexOf("{") === 0) {
119
120
  return headers.toJson();
120
121
  } else {
121
- var headerDict = {};
122
+ var header_dict = {};
122
123
  var lines = headers.split('\n');
123
124
  for (var i = 0; i < lines.length; i++) {
124
125
  var line = lines[i].trim();
125
126
  if (line) {
126
- headerDict[line.left(":").trim()] = line.right(":").trim();
127
+ header_dict[line.left(":").trim()] = line.right(":").trim();
127
128
  }
128
129
  }
129
- return headerDict;
130
+ return header_dict;
130
131
  }
131
132
  } else {
132
133
  return headers;
@@ -162,22 +163,48 @@ Http.prototype.httpOptions = function (options) {
162
163
  headers,
163
164
  method,
164
165
  cookie,
165
- type
166
+ type,
167
+ stream_callback
166
168
  } = options;
167
- 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
+ };
168
187
  if (cookie) {
169
188
  this.setCookie(cookie);
170
189
  }
171
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
+ }
172
196
  return Object.assign({}, {
173
- hostProtocol: protocol,
197
+ host_protocol: protocol,
174
198
  body,
175
199
  type,
176
200
  method: method || "GET",
177
- headers: headers ? Object.assign({}, this._headers, headers) : Object.assign({}, this._headers),
201
+ headers: http_headers,
178
202
  agent: cfg.proxy ? new HttpProxyAgent(cfg.proxy) : null,
179
- cookie: this.getCookie()
180
- }, urlObj);
203
+ rejectUnauthorized: cfg.rejectUnauthorized,
204
+ timeout: options.timeout || cfg.timeout || 0,
205
+ stream_callback: stream_callback,
206
+ cookie: cookie_str
207
+ }, url_obj);
181
208
  }
182
209
 
183
210
  /**
@@ -186,47 +213,78 @@ Http.prototype.httpOptions = function (options) {
186
213
  */
187
214
  Http.prototype.postOption = function (op) {
188
215
  var param = op.body;
189
- var bodyStr = "";
190
- var paramType = typeof (param);
191
- var contentType = op.type;
192
- if (paramType === "object") {
193
- if (!contentType) {
194
- contentType = 'application/json; charset=UTF-8';
195
- } else if (contentType.indexOf('json') !== -1) {
196
- contentType = "application/json; charset=UTF-8";
197
- } else if (contentType.indexOf('html') !== -1) {
198
- contentType = "text/html; charset=UTF-8";
199
- } else if (contentType.indexOf('form') !== -1) {
200
- contentType = "application/x-www-form-urlencoded";
201
- } else {
202
- 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';
203
222
  }
204
- if (contentType.indexOf('/json') !== -1) {
205
- bodyStr = JSON.stringify(param);
206
- } else if (contentType.indexOf('form') !== -1) {
207
- 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";
208
235
  } else {
209
- bodyStr = $.toXml(param);
236
+ body_str = $.toXml(param);
237
+ content_type = "text/plain; charset=UTF-8";
210
238
  }
211
239
  } else {
212
- bodyStr = param.trim();
213
- if (!contentType) {
214
- if (bodyStr.startsWith('{') && bodyStr.endsWith('}')) {
215
- contentType = "application/json; charset=UTF-8";
216
- } else if (bodyStr.startsWith('[') && bodyStr.endsWith(']')) {
217
- contentType = "application/json; charset=UTF-8";
218
- } else if (bodyStr.indexOf("<html>") !== -1) {
219
- 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";
220
248
  } else {
221
- contentType = "text/plain; charset=UTF-8";
249
+ content_type = "text/plain; charset=UTF-8";
222
250
  }
223
251
  }
224
252
  }
225
- op.body = bodyStr;
253
+ op.body = body_str;
226
254
  if (!op.headers['Content-Type']) {
227
- op.headers['Content-Type'] = contentType;
255
+ op.headers['Content-Type'] = content_type;
228
256
  }
229
- op.headers['Content-Length'] = Buffer.byteLength(bodyStr);
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
+ }
285
+ }
286
+ chunks.push(Buffer.from('--' + boundary + '--\r\n'));
287
+ return Buffer.concat(chunks);
230
288
  }
231
289
 
232
290
  /**
@@ -240,57 +298,49 @@ Http.prototype.req = async function (options) {
240
298
  this.postOption(op);
241
299
  }
242
300
  var res = await this.request(op);
243
- if (res.status.code == 302 || res.status.code == 307) {
244
- var redirectUrl = res.headers.location;
245
- var cookieList = res.headers['set-cookie'];
246
- if (cookieList) {
247
- var len = cookieList.length;
248
- for (var i = 0; i < len; i++) {
249
- this.setCookie(cookieList[i]);
250
- }
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次)");
251
306
  }
252
- options.href = redirectUrl;
307
+ var location = res.headers.location;
308
+ if (!location) {
309
+ break;
310
+ }
311
+ var redirect_url = new URL(location, op.href).href;
312
+ options.href = redirect_url;
253
313
  op = this.httpOptions(options);
254
- op.referer = options.href;
314
+ op.referer = redirect_url;
255
315
  res = await this.request(op);
256
316
  }
257
317
  return res;
258
318
  }
259
319
 
260
320
  /**
261
- * 解压数据
262
- * @param {string} encoding - 编码方式
321
+ * 解压数据流
322
+ * @param {Stream} stream - 输入数据流
323
+ * @param {string} encoding - 压缩方式,gzip使用gunzip,其他使用inflate
324
+ * @param {string} charset - 字符编码,指定时返回字符串
325
+ * @returns {Promise<Buffer|string>} 解压后的数据
263
326
  */
264
- Http.prototype.undata = function (encoding) {
265
- var bufarr = [];
266
- var errored = false;
267
- var zpipe;
268
- if (encoding) {
269
- zpipe = zlib.createGunzip();
270
- } else {
271
- zpipe = zlib.createInflate();
272
- }
273
- zpipe.on('data', function (d) {
274
- bufarr.push(d);
275
- })
276
- .on('end', function () {
277
- if (errored) return;
278
- errored = true;
279
- cb(null, response, enc ? Buffer.concat(bufarr).toString(
280
- enc) : Buffer.concat(bufarr));
281
- })
282
- .on('error', function (err) {
283
- if (errored) return;
284
- errored = true;
285
- 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);
286
338
  });
287
- response.pipe(zpipe);
288
- response
289
- .on('error', function (err) {
290
- if (errored) return;
291
- errored = true;
292
- cb(err, response, null);
339
+ stream.on('error', function (err) {
340
+ reject(err);
293
341
  });
342
+ stream.pipe(zpipe);
343
+ });
294
344
  }
295
345
 
296
346
  /**
@@ -299,7 +349,7 @@ Http.prototype.undata = function (encoding) {
299
349
  * @returns {Promise<Object>} 请求结果
300
350
  */
301
351
  Http.prototype.request = function (options) {
302
- var protocol = options.hostProtocol === "https" ? https : http;
352
+ var protocol = options.host_protocol === "https" ? https : http;
303
353
  var self = this;
304
354
  return new Promise(function (resolve, reject) {
305
355
  var body = '';
@@ -307,59 +357,82 @@ Http.prototype.request = function (options) {
307
357
  var {
308
358
  headers
309
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
+ }
310
368
 
311
- if (res.statusCode == 200) {
312
- var chunks = [];
313
- 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
+ }
314
389
 
315
- res.on('data', function (chunk) {
316
- chunks.push(chunk);
317
- }).on('end', function () {
318
- var buffer = Buffer.concat(chunks);
319
- switch (encoding) {
320
- case 'br':
321
- case 'gzip':
322
- case 'deflate':
323
- buffer = unzipSync(buffer);
324
- body = buffer.toString();
325
- break;
326
- default:
327
- body = buffer.toString();
328
- break;
329
- }
330
- var charsetMatch = body.match(/charset=gb2312"/i);
331
- if (charsetMatch) {
332
- body = iconv.decode(buffer, 'gb2312');
333
- } else {
334
- charsetMatch = body.match(/charset=GBK"/i);
335
- if (charsetMatch) {
336
- body = iconv.decode(buffer, 'GBK');
337
- }
338
- }
339
- var cookieList = headers['set-cookie'];
340
- if (cookieList) {
341
- var len = cookieList.length;
342
- for (var i = 0; i < len; i++) {
343
- self.setCookie(cookieList[i]);
344
- }
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');
345
415
  }
416
+ }
417
+ if (status_code >= 200 && status_code < 300) {
346
418
  resolve({
347
- status: 200,
419
+ status: status_code,
348
420
  headers,
349
421
  buffer,
350
422
  body
351
423
  });
352
- });
353
- } else {
354
- resolve({
355
- headers,
356
- status: {
357
- code: res.statusCode,
358
- message: res.statusMessage
359
- }
360
- });
361
- reject(res);
362
- }
424
+ } else {
425
+ resolve({
426
+ headers,
427
+ status: {
428
+ code: status_code,
429
+ message: res.statusMessage
430
+ },
431
+ buffer,
432
+ body
433
+ });
434
+ }
435
+ });
363
436
  });
364
437
 
365
438
  req.on("error", function (err) {
@@ -368,8 +441,12 @@ Http.prototype.request = function (options) {
368
441
  status: 0,
369
442
  message: err.message
370
443
  });
371
- reject(err);
372
444
  });
445
+ if (options.timeout) {
446
+ req.setTimeout(options.timeout, function () {
447
+ req.destroy(new Error("请求超时"));
448
+ });
449
+ }
373
450
  if (options.method.toLocaleUpperCase() == "POST") {
374
451
  req.write(options.body);
375
452
  }
@@ -413,6 +490,25 @@ Http.prototype.post = async function (url, body, headers, type, cookie) {
413
490
  });
414
491
  };
415
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
+
416
512
  /**
417
513
  * 下载文件
418
514
  * @param {string} url - 下载地址
@@ -423,36 +519,36 @@ Http.prototype.post = async function (url, body, headers, type, cookie) {
423
519
  * @returns {Promise<string>} 成功返回保存路径,失败返回null
424
520
  */
425
521
  Http.prototype.download = async function (url, filename, auto, headers, cookie) {
426
- var res = await this.get(url, headers, cookie);
427
- var filePath = null;
428
- if (res.body) {
429
- if (auto) {
430
- var parts = url.split('/');
431
- var fullName = parts[parts.length - 1];
432
- if (fullName.indexOf('.') !== -1) {
433
- var ext = fullName.right('.');
434
- if (ext) {
435
- 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();
436
541
  }
437
- } else {
438
- var contentType = res.headers["content-type"];
439
- var ext = contentType.replace("image/", "").replace("application/x-", "").replace("text/", "")
440
- .replace("audio/", "").replace("jpeg", "jpg");
441
- filePath = filename + "." + ext;
442
542
  }
443
- } else {
444
- filePath = filename;
445
- }
446
- if (filePath) {
447
- var bytesWritten = await fs.writeFileSync(filePath, res.buffer, "binary");
448
- if (bytesWritten == 0) {
449
- filePath = null;
543
+ file_path = ext ? filename + "." + ext : filename;
544
+ if (!file_path) {
545
+ self.log("debug", '保存文件名(filename)不能为空!');
546
+ return null;
450
547
  }
451
- } else {
452
- this.log("debug", '保存文件名(filename)不能为!');
548
+ return fs.createWriteStream(file_path);
453
549
  }
454
- }
455
- return filePath;
550
+ }));
551
+ return file_path;
456
552
  };
457
553
 
458
554
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mm_https",
3
- "version": "1.6.6",
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.1",
38
- "mm_expand": "^2.0.0"
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();