mm_https 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * mm_https 模块最小可运行示例
3
+ * @description 演示 Http 客户端的 GET/POST 请求,
4
+ * 请求本地测试服务器,不访问外部网络
5
+ */
6
+ const http = require('http');
7
+ const { Http } = require('../index.js');
8
+
9
+ /**
10
+ * 启动本地测试服务器
11
+ * @returns {http.Server} 服务器实例
12
+ */
13
+ function startServer() {
14
+ const server = http.createServer(function (req, res) {
15
+ if (req.url.startsWith('/get')) {
16
+ res.writeHead(200, { 'Content-Type': 'application/json' });
17
+ res.end(JSON.stringify({ ok: true, path: req.url }));
18
+ } else if (req.url.startsWith('/post')) {
19
+ const body = [];
20
+ req.on('data', (c) => body.push(c));
21
+ req.on('end', function () {
22
+ res.writeHead(200, { 'Content-Type': 'application/json' });
23
+ res.end(JSON.stringify({ body: Buffer.concat(body).toString() }));
24
+ });
25
+ } else {
26
+ res.writeHead(404);
27
+ res.end('not found');
28
+ }
29
+ });
30
+ return server;
31
+ }
32
+
33
+ /**
34
+ * 主演示流程
35
+ */
36
+ async function main() {
37
+ const server = startServer();
38
+ await new Promise((resolve) => server.listen(0, resolve));
39
+ const baseUrl = 'http://127.0.0.1:' + server.address().port;
40
+
41
+ try {
42
+ const client = new Http({ proxy: null });
43
+
44
+ // GET 请求
45
+ const getRes = await client.get(baseUrl + '/get?page=1');
46
+ console.log('GET 状态:', getRes.status, '| 响应:', getRes.body);
47
+
48
+ // POST JSON 请求
49
+ const postRes = await client.post(baseUrl + '/post', { a: 1, b: 'x' }, null, 'json');
50
+ console.log('POST 状态:', postRes.status, '| 响应:', postRes.body);
51
+ } finally {
52
+ server.close();
53
+ }
54
+ }
55
+
56
+ main().catch((err) => {
57
+ console.error('示例运行失败:', err);
58
+ process.exit(1);
59
+ });
package/index.js CHANGED
@@ -1,576 +1,17 @@
1
- /**
2
- * @fileOverview HTTP请求模块
3
- * @author <a href="http://qww.elins.cn">邱文武</a>
4
- * @version 1.2
5
- */
6
- const http = require('http');
7
- const https = require('https');
8
- const { ProxyAgent } = require('proxy-agent');
9
- const fs = require("fs");
10
- const {
11
- createGunzip,
12
- createInflate,
13
- unzipSync
14
- } = require('zlib');
15
- const iconv = require('iconv-lite');
16
- require('mm_expand');
1
+ const { Http } = require('./lib/http');
2
+ const Admin = require('mm_corejs').Admin;
17
3
 
18
- /**
19
- * HTTP客户端类
20
- * @class
21
- */
22
- class Http {
23
- /**
24
- * 创建HTTP客户端实例
25
- * @param {Object} config - 配置选项
26
- */
27
- constructor(config) {
28
- this._config = Object.assign({
29
- headers: "",
30
- rejectUnauthorized: false,
31
- cookie: "",
32
- proxy: null
33
- }, config);
4
+ const admin = new Admin();
5
+ admin.setClass(Http);
34
6
 
35
- /**
36
- * cookies缓存
37
- */
38
- this._cookies = {};
39
-
40
- /**
41
- * multipart表单边界值
42
- */
43
- this._boundary = "";
44
-
45
- /**
46
- * 代理agent缓存,复用连接
47
- */
48
- var proxy_url = this._config.proxy;
49
- this._agent = proxy_url ? new ProxyAgent({
50
- getProxyForUrl: function () {
51
- return proxy_url;
52
- }
53
- }) : null;
54
-
55
- /**
56
- * headers协议头
57
- */
58
- this._headers = {
59
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
60
- 'Accept-Encoding': 'gzip, deflate',
61
- 'Accept-Language': 'zh-CN,zh;q=0.9',
62
- 'Cache-Control': 'no-cache',
63
- 'Connection': 'keep-alive',
64
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36'
65
- };
66
-
67
- if (this._config.cookie) {
68
- this.setCookie(this._config.cookie);
69
- }
70
-
71
- if (this._config.headers) {
72
- var headers = this.changeHeaders(config.headers);
73
- Object.assign(this._headers, headers);
74
- }
75
-
76
- this._logger = $.log || console;
77
- }
78
- }
79
-
80
- /**
81
- * 日志记录
82
- * @param {string} level - 日志级别
83
- * @param {string} message - 日志消息
84
- * @param {...*} args - 日志参数
85
- */
86
- Http.prototype.log = function (level, message, ...args) {
87
- this._logger[level](`[${this.constructor.name}] ${message}`, ...args);
88
- }
89
-
90
- /**
91
- * 设置Cookie
92
- * @param {Object|String} cookies - Cookie字符串
93
- */
94
- Http.prototype.setCookie = function (cookies) {
95
- if (typeof (cookies) == "string") {
96
- var arr = cookies.split('; ');
97
- if (arr.length > 0) {
98
- var kv_arr = arr[0].split("=");
99
- this._cookies[kv_arr[0]] = encodeURIComponent(kv_arr[1]);
100
- }
101
- }
102
- else if (typeof (cookies) == "object") {
103
- Object.assign(this._cookies, cookies);
104
- }
105
- }
106
-
107
- /**
108
- * 获取Cookie字符串
109
- * @returns {string} Cookie字符串
110
- */
111
- Http.prototype.getCookie = function () {
112
- var cookies = this._cookies;
113
- var str = "";
114
- for (var key in cookies) {
115
- str += "; " + key + "=" + cookies[key];
116
- }
117
- var cookie_str = str.replace("; ", "");
118
- return cookie_str;
119
- }
120
-
121
- /**
122
- * 转换协议头
123
- * @param {Object|string} headers - 协议头
124
- * @returns {Object} 转换后的协议头对象
125
- */
126
- Http.prototype.changeHeaders = function (headers) {
127
- if (headers) {
128
- if (typeof (headers) == "string") {
129
- if (headers.indexOf("{") === 0) {
130
- return headers.toJson();
131
- } else {
132
- var header_dict = {};
133
- var lines = headers.split('\n');
134
- for (var i = 0; i < lines.length; i++) {
135
- var line = lines[i].trim();
136
- if (line) {
137
- header_dict[line.left(":").trim()] = line.right(":").trim();
138
- }
139
- }
140
- return header_dict;
141
- }
142
- } else {
143
- return headers;
144
- }
145
- }
146
- return {};
147
- }
148
-
149
- /**
150
- * 获取默认选项
151
- * @returns {Object} 默认选项对象
152
- */
153
- Http.prototype.options = function () {
154
- return {
155
- href: "",
156
- method: "GET",
157
- body: {},
158
- proxy: "http://127.0.0.1:10809",
159
- headers: Object.assign({}, this._headers)
160
- }
161
- }
162
-
163
- /**
164
- * 生成HTTP选项
165
- * @param {Object} options - 选项参数
166
- * @returns {Object} HTTP选项对象
167
- */
168
- Http.prototype.httpOptions = function (options) {
169
- var cfg = this._config;
170
- var {
171
- href,
172
- body,
173
- headers,
174
- method,
175
- cookie,
176
- type,
177
- stream_callback
178
- } = options;
179
- var url_obj = new URL(href);
180
- var query = {};
181
- url_obj.searchParams.forEach(function (value, key) {
182
- query[key] = value;
183
- });
184
- var port = url_obj.port ? parseInt(url_obj.port, 10) : (url_obj.protocol === "https:" ? 443 : 80);
185
- url_obj = {
186
- href: url_obj.href,
187
- protocol: url_obj.protocol,
188
- host: url_obj.host,
189
- hostname: url_obj.hostname,
190
- port: port,
191
- pathname: url_obj.pathname,
192
- search: url_obj.search,
193
- path: url_obj.pathname + url_obj.search,
194
- query: query,
195
- hash: url_obj.hash
196
- };
197
- if (cookie) {
198
- this.setCookie(cookie);
199
- }
200
- var protocol = href.left(":/") || 'http';
201
- var http_headers = headers ? Object.assign({}, this._headers, headers) : Object.assign({}, this._headers);
202
- var cookie_str = this.getCookie();
203
- if (cookie_str) {
204
- http_headers['Cookie'] = cookie_str;
205
- }
206
- return Object.assign({}, {
207
- host_protocol: protocol,
208
- body,
209
- type,
210
- method: method || "GET",
211
- headers: http_headers,
212
- agent: this._agent,
213
- rejectUnauthorized: cfg.rejectUnauthorized,
214
- timeout: options.timeout || cfg.timeout || 0,
215
- stream_callback: stream_callback,
216
- cookie: cookie_str
217
- }, url_obj);
218
- }
219
-
220
- /**
221
- * 处理POST选项
222
- * @param {Object} op - 选项对象
223
- */
224
- Http.prototype.postOption = function (op) {
225
- var param = op.body;
226
- var body_str = "";
227
- var param_type = typeof (param);
228
- var content_type = op.type;
229
- if (param_type === "object") {
230
- if (!content_type) {
231
- content_type = 'application/json; charset=UTF-8';
232
- }
233
- if (content_type.indexOf('multipart') !== -1 || content_type.indexOf('form-data') !== -1) {
234
- body_str = this.multipartBody(param);
235
- content_type = "multipart/form-data; boundary=" + this._boundary;
236
- } else if (content_type.indexOf('json') !== -1) {
237
- body_str = JSON.stringify(param);
238
- content_type = "application/json; charset=UTF-8";
239
- } else if (content_type.indexOf('html') !== -1) {
240
- body_str = $.toXml(param);
241
- content_type = "text/html; charset=UTF-8";
242
- } else if (content_type.indexOf('form') !== -1) {
243
- body_str = $.toUrl(param);
244
- content_type = "application/x-www-form-urlencoded";
245
- } else {
246
- body_str = $.toXml(param);
247
- content_type = "text/plain; charset=UTF-8";
248
- }
249
- } else {
250
- body_str = param.trim();
251
- if (!content_type) {
252
- if (body_str.startsWith('{') && body_str.endsWith('}')) {
253
- content_type = "application/json; charset=UTF-8";
254
- } else if (body_str.startsWith('[') && body_str.endsWith(']')) {
255
- content_type = "application/json; charset=UTF-8";
256
- } else if (body_str.indexOf("<html>") !== -1) {
257
- content_type = "text/html";
258
- } else {
259
- content_type = "text/plain; charset=UTF-8";
260
- }
261
- }
262
- }
263
- op.body = body_str;
264
- if (!op.headers['Content-Type']) {
265
- op.headers['Content-Type'] = content_type;
266
- }
267
- op.headers['Content-Length'] = Buffer.byteLength(body_str);
268
- }
269
-
270
- /**
271
- * 生成multipart/form-data请求体
272
- * @param {Object} param - 表单参数,文件字段值为 {value, filename, content_type}
273
- * @returns {Buffer} multipart请求体
274
- */
275
- Http.prototype.multipartBody = function (param) {
276
- var boundary = "----mm_https_" + Date.now().toString(16);
277
- this._boundary = boundary;
278
- var chunks = [];
279
- for (var key in param) {
280
- var value = param[key];
281
- var value_type = typeof (value);
282
- if (value_type === "object" && value !== null) {
283
- var filename = value.filename || key;
284
- var file_type = value.content_type || "application/octet-stream";
285
- var content = value.value;
286
- if (!Buffer.isBuffer(content)) {
287
- content = Buffer.from(String(content));
288
- }
289
- chunks.push(Buffer.from('--' + boundary + '\r\nContent-Disposition: form-data; name="' + key + '"; filename="' + filename + '"\r\nContent-Type: ' + file_type + '\r\n\r\n'));
290
- chunks.push(content);
291
- chunks.push(Buffer.from('\r\n'));
292
- } else {
293
- chunks.push(Buffer.from('--' + boundary + '\r\nContent-Disposition: form-data; name="' + key + '"\r\n\r\n' + value + '\r\n'));
294
- }
295
- }
296
- chunks.push(Buffer.from('--' + boundary + '--\r\n'));
297
- return Buffer.concat(chunks);
298
- }
299
-
300
- /**
301
- * HTTP请求
302
- * @param {Object} options - 配置选项
303
- * @returns {Promise<Object>} 执行结果
304
- */
305
- Http.prototype.req = async function (options) {
306
- var op = this.httpOptions(options);
307
- if (op.method.toLocaleUpperCase() === "POST") {
308
- this.postOption(op);
309
- }
310
- var res = await this.request(op);
311
- var redirects = [301, 302, 303, 307, 308];
312
- var count = 0;
313
- while (redirects.indexOf(res.status.code) !== -1) {
314
- if (++count > 5) {
315
- throw new Error("重定向次数超过上限(5次)");
316
- }
317
- var location = res.headers.location;
318
- if (!location) {
319
- break;
320
- }
321
- var redirect_url = new URL(location, op.href).href;
322
- options.href = redirect_url;
323
- op = this.httpOptions(options);
324
- op.referer = redirect_url;
325
- res = await this.request(op);
326
- }
327
- return res;
328
- }
329
-
330
- /**
331
- * 解压数据流
332
- * @param {Stream} stream - 输入数据流
333
- * @param {string} encoding - 压缩方式,gzip使用gunzip,其他使用inflate
334
- * @param {string} charset - 字符编码,指定时返回字符串
335
- * @returns {Promise<Buffer|string>} 解压后的数据
336
- */
337
- Http.prototype.undata = function (stream, encoding, charset) {
338
- var buf_arr = [];
339
- return new Promise(function (resolve, reject) {
340
- var zpipe = encoding ? createGunzip() : createInflate();
341
- zpipe.on('data', function (d) {
342
- buf_arr.push(d);
343
- }).on('end', function () {
344
- var buffer = Buffer.concat(buf_arr);
345
- resolve(charset ? buffer.toString(charset) : buffer);
346
- }).on('error', function (err) {
347
- reject(err);
348
- });
349
- stream.on('error', function (err) {
350
- reject(err);
351
- });
352
- stream.pipe(zpipe);
353
- });
354
- }
355
-
356
- /**
357
- * 发送请求
358
- * @param {Object} options - 配置选项
359
- * @returns {Promise<Object>} 请求结果
360
- */
361
- Http.prototype.request = function (options) {
362
- var protocol = options.host_protocol === "https" ? https : http;
363
- var self = this;
364
- return new Promise(function (resolve, reject) {
365
- var body = '';
366
- var req = protocol.request(options, (res) => {
367
- var {
368
- headers
369
- } = res;
370
- var status_code = res.statusCode;
371
- var cookie_list = headers['set-cookie'];
372
- if (cookie_list) {
373
- var len = cookie_list.length;
374
- for (var i = 0; i < len; i++) {
375
- self.setCookie(cookie_list[i]);
376
- }
377
- }
378
-
379
- // 流式下载:仅对成功响应生效
380
- if (status_code >= 200 && status_code < 300 && options.stream_callback) {
381
- var stream = options.stream_callback(headers);
382
- if (stream) {
383
- res.on('error', function (err) {
384
- reject(err);
385
- });
386
- res.pipe(stream);
387
- stream.on('finish', function () {
388
- resolve({
389
- status: status_code,
390
- headers
391
- });
392
- });
393
- stream.on('error', function (err) {
394
- reject(err);
395
- });
396
- return;
397
- }
398
- }
399
-
400
- var chunks = [];
401
- var encoding = headers['content-encoding'];
402
-
403
- res.on('data', function (chunk) {
404
- chunks.push(chunk);
405
- }).on('end', function () {
406
- var buffer = Buffer.concat(chunks);
407
- switch (encoding) {
408
- case 'br':
409
- case 'gzip':
410
- case 'deflate':
411
- buffer = unzipSync(buffer);
412
- body = buffer.toString();
413
- break;
414
- default:
415
- body = buffer.toString();
416
- break;
417
- }
418
- var charset_match = body.match(/charset=gb2312"/i);
419
- if (charset_match) {
420
- body = iconv.decode(buffer, 'gb2312');
421
- } else {
422
- charset_match = body.match(/charset=GBK"/i);
423
- if (charset_match) {
424
- body = iconv.decode(buffer, 'GBK');
425
- }
426
- }
427
- if (status_code >= 200 && status_code < 300) {
428
- resolve({
429
- status: status_code,
430
- headers,
431
- buffer,
432
- body
433
- });
434
- } else {
435
- resolve({
436
- headers,
437
- status: {
438
- code: status_code,
439
- message: res.statusMessage
440
- },
441
- buffer,
442
- body
443
- });
444
- }
445
- });
446
- });
447
-
448
- req.on("error", function (err) {
449
- self.log("debug", "错误", err);
450
- resolve({
451
- status: 0,
452
- message: err.message
453
- });
454
- });
455
- if (options.timeout) {
456
- req.setTimeout(options.timeout, function () {
457
- req.destroy(new Error("请求超时"));
458
- });
459
- }
460
- if (options.method.toLocaleUpperCase() == "POST") {
461
- req.write(options.body);
462
- }
463
- req.end();
464
- });
465
- }
466
-
467
- /**
468
- * GET请求
469
- * @param {string} url - 请求地址
470
- * @param {Object} headers - 请求头
471
- * @param {string} cookie - Cookie字符串
472
- * @returns {Promise<Object>} 响应结果
473
- */
474
- Http.prototype.get = async function (url, headers, cookie) {
475
- return await this.req({
476
- method: "GET",
477
- href: url,
478
- headers,
479
- cookie
480
- });
481
- };
482
-
483
- /**
484
- * POST请求
485
- * @param {string} url - 请求地址
486
- * @param {Object} body - 提交参数
487
- * @param {Object} headers - 请求头
488
- * @param {string} type - 传参方式 json, form, text
489
- * @param {string} cookie - Cookie字符串
490
- * @returns {Promise<Object>} 响应结果
491
- */
492
- Http.prototype.post = async function (url, body, headers, type, cookie) {
493
- return await this.req({
494
- method: "POST",
495
- href: url,
496
- body,
497
- type,
498
- headers,
499
- cookie
500
- });
501
- };
502
-
503
- /**
504
- * 上传文件
505
- * @param {string} url - 请求地址
506
- * @param {Object} body - 表单参数,文件字段值为 {value: Buffer|string, filename, content_type}
507
- * @param {Object} headers - 请求头
508
- * @param {string} cookie - Cookie字符串
509
- * @returns {Promise<Object>} 响应结果
510
- */
511
- Http.prototype.upload = async function (url, body, headers, cookie) {
512
- return await this.req({
513
- method: "POST",
514
- href: url,
515
- body,
516
- type: "form-data",
517
- headers,
518
- cookie
519
- });
520
- };
521
-
522
- /**
523
- * 下载文件
524
- * @param {string} url - 下载地址
525
- * @param {string} filename - 文件名
526
- * @param {string} auto - 自动类型
527
- * @param {Object} headers - 协议头
528
- * @param {string} cookie - Cookie字符串
529
- * @returns {Promise<string>} 成功返回保存路径,失败返回null
530
- */
531
- Http.prototype.download = async function (url, filename, auto, headers, cookie) {
532
- var self = this;
533
- var file_path = null;
534
- await this.request(this.httpOptions({
535
- method: "GET",
536
- href: url,
537
- headers,
538
- cookie,
539
- stream_callback: function (res_headers) {
540
- var ext = "";
541
- if (auto) {
542
- var parts = url.split('/');
543
- var full_name = parts[parts.length - 1];
544
- if (full_name.indexOf('.') !== -1) {
545
- ext = full_name.right('.');
546
- }
547
- if (!ext) {
548
- var content_type = res_headers["content-type"] || "";
549
- ext = content_type.replace("image/", "").replace("application/x-", "").replace("text/", "")
550
- .replace("audio/", "").replace("jpeg", "jpg").split(";")[0].trim();
551
- }
552
- }
553
- file_path = ext ? filename + "." + ext : filename;
554
- if (!file_path) {
555
- self.log("debug", '保存文件名(filename)不能为空!');
556
- return null;
557
- }
558
- return fs.createWriteStream(file_path);
559
- }
560
- }));
561
- return file_path;
562
- };
563
-
564
- /**
565
- * 清除缓存
566
- */
567
- Http.prototype.clear = function () {
568
- this._cookies = {};
569
- };
570
-
571
- /**
572
- * 导出构造函数
573
- */
574
7
  module.exports = {
575
- Http
576
- };
8
+ Http,
9
+ admin,
10
+ httpAdmin(scope, config) {
11
+ if (!scope) {
12
+ scope = 'os';
13
+ }
14
+ admin.set(scope, config);
15
+ return admin;
16
+ }
17
+ }
@@ -1,4 +1,5 @@
1
- require('mm_expand');
1
+ require('mm_helper');
2
+ const { push } = require('mm_helper');
2
3
 
3
4
  /**
4
5
  * @class Cookie类
@@ -108,7 +109,7 @@ Cookie.prototype.str = function(str) {
108
109
  var domain = cookie.domain;
109
110
  var obj = this.get(name, path, domain);
110
111
  if (obj) {
111
- $.push(obj, cookie, true);
112
+ push(obj, cookie, true);
112
113
  } else {
113
114
  this.list.push(cookie);
114
115
  }
package/lib/http.js ADDED
@@ -0,0 +1,577 @@
1
+ /**
2
+ * @fileOverview HTTP请求模块
3
+ * @author <a href="http://qww.elins.cn">邱文武</a>
4
+ * @version 1.2
5
+ */
6
+ const http = require('http');
7
+ const https = require('https');
8
+ const { ProxyAgent } = require('proxy-agent');
9
+ const fs = require("fs");
10
+ const {
11
+ createGunzip,
12
+ createInflate,
13
+ unzipSync
14
+ } = require('zlib');
15
+ const iconv = require('iconv-lite');
16
+ require('mm_helper');
17
+ const { toXml, toUrl } = require('mm_helper');
18
+
19
+ /**
20
+ * HTTP客户端类
21
+ * @class
22
+ */
23
+ class Http {
24
+ /**
25
+ * 创建HTTP客户端实例
26
+ * @param {Object} config - 配置选项
27
+ */
28
+ constructor(config) {
29
+ this._config = Object.assign({
30
+ headers: "",
31
+ rejectUnauthorized: false,
32
+ cookie: "",
33
+ proxy: null
34
+ }, config);
35
+
36
+ /**
37
+ * cookies缓存
38
+ */
39
+ this._cookies = {};
40
+
41
+ /**
42
+ * multipart表单边界值
43
+ */
44
+ this._boundary = "";
45
+
46
+ /**
47
+ * 代理agent缓存,复用连接
48
+ */
49
+ var proxy_url = this._config.proxy;
50
+ this._agent = proxy_url ? new ProxyAgent({
51
+ getProxyForUrl: function () {
52
+ return proxy_url;
53
+ }
54
+ }) : null;
55
+
56
+ /**
57
+ * headers协议头
58
+ */
59
+ this._headers = {
60
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
61
+ 'Accept-Encoding': 'gzip, deflate',
62
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
63
+ 'Cache-Control': 'no-cache',
64
+ 'Connection': 'keep-alive',
65
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36'
66
+ };
67
+
68
+ if (this._config.cookie) {
69
+ this.setCookie(this._config.cookie);
70
+ }
71
+
72
+ if (this._config.headers) {
73
+ var headers = this.changeHeaders(config.headers);
74
+ Object.assign(this._headers, headers);
75
+ }
76
+
77
+ this._logger = console;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * 日志记录
83
+ * @param {string} level - 日志级别
84
+ * @param {string} message - 日志消息
85
+ * @param {...*} args - 日志参数
86
+ */
87
+ Http.prototype.log = function (level, message, ...args) {
88
+ this._logger[level](`[${this.constructor.name}] ${message}`, ...args);
89
+ }
90
+
91
+ /**
92
+ * 设置Cookie
93
+ * @param {Object|String} cookies - Cookie字符串
94
+ */
95
+ Http.prototype.setCookie = function (cookies) {
96
+ if (typeof (cookies) == "string") {
97
+ var arr = cookies.split('; ');
98
+ if (arr.length > 0) {
99
+ var kv_arr = arr[0].split("=");
100
+ this._cookies[kv_arr[0]] = encodeURIComponent(kv_arr[1]);
101
+ }
102
+ }
103
+ else if (typeof (cookies) == "object") {
104
+ Object.assign(this._cookies, cookies);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * 获取Cookie字符串
110
+ * @returns {string} Cookie字符串
111
+ */
112
+ Http.prototype.getCookie = function () {
113
+ var cookies = this._cookies;
114
+ var str = "";
115
+ for (var key in cookies) {
116
+ str += "; " + key + "=" + cookies[key];
117
+ }
118
+ var cookie_str = str.replace("; ", "");
119
+ return cookie_str;
120
+ }
121
+
122
+ /**
123
+ * 转换协议头
124
+ * @param {Object|string} headers - 协议头
125
+ * @returns {Object} 转换后的协议头对象
126
+ */
127
+ Http.prototype.changeHeaders = function (headers) {
128
+ if (headers) {
129
+ if (typeof (headers) == "string") {
130
+ if (headers.indexOf("{") === 0) {
131
+ return headers.toJson();
132
+ } else {
133
+ var header_dict = {};
134
+ var lines = headers.split('\n');
135
+ for (var i = 0; i < lines.length; i++) {
136
+ var line = lines[i].trim();
137
+ if (line) {
138
+ header_dict[line.left(":").trim()] = line.right(":").trim();
139
+ }
140
+ }
141
+ return header_dict;
142
+ }
143
+ } else {
144
+ return headers;
145
+ }
146
+ }
147
+ return {};
148
+ }
149
+
150
+ /**
151
+ * 获取默认选项
152
+ * @returns {Object} 默认选项对象
153
+ */
154
+ Http.prototype.options = function () {
155
+ return {
156
+ href: "",
157
+ method: "GET",
158
+ body: {},
159
+ proxy: "http://127.0.0.1:10809",
160
+ headers: Object.assign({}, this._headers)
161
+ }
162
+ }
163
+
164
+ /**
165
+ * 生成HTTP选项
166
+ * @param {Object} options - 选项参数
167
+ * @returns {Object} HTTP选项对象
168
+ */
169
+ Http.prototype.httpOptions = function (options) {
170
+ var cfg = this._config;
171
+ var {
172
+ href,
173
+ body,
174
+ headers,
175
+ method,
176
+ cookie,
177
+ type,
178
+ stream_callback
179
+ } = options;
180
+ var url_obj = new URL(href);
181
+ var query = {};
182
+ url_obj.searchParams.forEach(function (value, key) {
183
+ query[key] = value;
184
+ });
185
+ var port = url_obj.port ? parseInt(url_obj.port, 10) : (url_obj.protocol === "https:" ? 443 : 80);
186
+ url_obj = {
187
+ href: url_obj.href,
188
+ protocol: url_obj.protocol,
189
+ host: url_obj.host,
190
+ hostname: url_obj.hostname,
191
+ port: port,
192
+ pathname: url_obj.pathname,
193
+ search: url_obj.search,
194
+ path: url_obj.pathname + url_obj.search,
195
+ query: query,
196
+ hash: url_obj.hash
197
+ };
198
+ if (cookie) {
199
+ this.setCookie(cookie);
200
+ }
201
+ var protocol = href.left(":/") || 'http';
202
+ var http_headers = headers ? Object.assign({}, this._headers, headers) : Object.assign({}, this._headers);
203
+ var cookie_str = this.getCookie();
204
+ if (cookie_str) {
205
+ http_headers['Cookie'] = cookie_str;
206
+ }
207
+ return Object.assign({}, {
208
+ host_protocol: protocol,
209
+ body,
210
+ type,
211
+ method: method || "GET",
212
+ headers: http_headers,
213
+ agent: this._agent,
214
+ rejectUnauthorized: cfg.rejectUnauthorized,
215
+ timeout: options.timeout || cfg.timeout || 0,
216
+ stream_callback: stream_callback,
217
+ cookie: cookie_str
218
+ }, url_obj);
219
+ }
220
+
221
+ /**
222
+ * 处理POST选项
223
+ * @param {Object} op - 选项对象
224
+ */
225
+ Http.prototype.postOption = function (op) {
226
+ var param = op.body;
227
+ var body_str = "";
228
+ var param_type = typeof (param);
229
+ var content_type = op.type;
230
+ if (param_type === "object") {
231
+ if (!content_type) {
232
+ content_type = 'application/json; charset=UTF-8';
233
+ }
234
+ if (content_type.indexOf('multipart') !== -1 || content_type.indexOf('form-data') !== -1) {
235
+ body_str = this.multipartBody(param);
236
+ content_type = "multipart/form-data; boundary=" + this._boundary;
237
+ } else if (content_type.indexOf('json') !== -1) {
238
+ body_str = JSON.stringify(param);
239
+ content_type = "application/json; charset=UTF-8";
240
+ } else if (content_type.indexOf('html') !== -1) {
241
+ body_str = toXml(param);
242
+ content_type = "text/html; charset=UTF-8";
243
+ } else if (content_type.indexOf('form') !== -1) {
244
+ body_str = toUrl(param);
245
+ content_type = "application/x-www-form-urlencoded";
246
+ } else {
247
+ body_str = toXml(param);
248
+ content_type = "text/plain; charset=UTF-8";
249
+ }
250
+ } else {
251
+ body_str = param.trim();
252
+ if (!content_type) {
253
+ if (body_str.startsWith('{') && body_str.endsWith('}')) {
254
+ content_type = "application/json; charset=UTF-8";
255
+ } else if (body_str.startsWith('[') && body_str.endsWith(']')) {
256
+ content_type = "application/json; charset=UTF-8";
257
+ } else if (body_str.indexOf("<html>") !== -1) {
258
+ content_type = "text/html";
259
+ } else {
260
+ content_type = "text/plain; charset=UTF-8";
261
+ }
262
+ }
263
+ }
264
+ op.body = body_str;
265
+ if (!op.headers['Content-Type']) {
266
+ op.headers['Content-Type'] = content_type;
267
+ }
268
+ op.headers['Content-Length'] = Buffer.byteLength(body_str);
269
+ }
270
+
271
+ /**
272
+ * 生成multipart/form-data请求体
273
+ * @param {Object} param - 表单参数,文件字段值为 {value, filename, content_type}
274
+ * @returns {Buffer} multipart请求体
275
+ */
276
+ Http.prototype.multipartBody = function (param) {
277
+ var boundary = "----mm_https_" + Date.now().toString(16);
278
+ this._boundary = boundary;
279
+ var chunks = [];
280
+ for (var key in param) {
281
+ var value = param[key];
282
+ var value_type = typeof (value);
283
+ if (value_type === "object" && value !== null) {
284
+ var filename = value.filename || key;
285
+ var file_type = value.content_type || "application/octet-stream";
286
+ var content = value.value;
287
+ if (!Buffer.isBuffer(content)) {
288
+ content = Buffer.from(String(content));
289
+ }
290
+ chunks.push(Buffer.from('--' + boundary + '\r\nContent-Disposition: form-data; name="' + key + '"; filename="' + filename + '"\r\nContent-Type: ' + file_type + '\r\n\r\n'));
291
+ chunks.push(content);
292
+ chunks.push(Buffer.from('\r\n'));
293
+ } else {
294
+ chunks.push(Buffer.from('--' + boundary + '\r\nContent-Disposition: form-data; name="' + key + '"\r\n\r\n' + value + '\r\n'));
295
+ }
296
+ }
297
+ chunks.push(Buffer.from('--' + boundary + '--\r\n'));
298
+ return Buffer.concat(chunks);
299
+ }
300
+
301
+ /**
302
+ * HTTP请求
303
+ * @param {Object} options - 配置选项
304
+ * @returns {Promise<Object>} 执行结果
305
+ */
306
+ Http.prototype.req = async function (options) {
307
+ var op = this.httpOptions(options);
308
+ if (op.method.toLocaleUpperCase() === "POST") {
309
+ this.postOption(op);
310
+ }
311
+ var res = await this.request(op);
312
+ var redirects = [301, 302, 303, 307, 308];
313
+ var count = 0;
314
+ while (redirects.indexOf(res.status.code) !== -1) {
315
+ if (++count > 5) {
316
+ throw new Error("重定向次数超过上限(5次)");
317
+ }
318
+ var location = res.headers.location;
319
+ if (!location) {
320
+ break;
321
+ }
322
+ var redirect_url = new URL(location, op.href).href;
323
+ options.href = redirect_url;
324
+ op = this.httpOptions(options);
325
+ op.referer = redirect_url;
326
+ res = await this.request(op);
327
+ }
328
+ return res;
329
+ }
330
+
331
+ /**
332
+ * 解压数据流
333
+ * @param {Stream} stream - 输入数据流
334
+ * @param {string} encoding - 压缩方式,gzip使用gunzip,其他使用inflate
335
+ * @param {string} charset - 字符编码,指定时返回字符串
336
+ * @returns {Promise<Buffer|string>} 解压后的数据
337
+ */
338
+ Http.prototype.undata = function (stream, encoding, charset) {
339
+ var buf_arr = [];
340
+ return new Promise(function (resolve, reject) {
341
+ var zpipe = encoding ? createGunzip() : createInflate();
342
+ zpipe.on('data', function (d) {
343
+ buf_arr.push(d);
344
+ }).on('end', function () {
345
+ var buffer = Buffer.concat(buf_arr);
346
+ resolve(charset ? buffer.toString(charset) : buffer);
347
+ }).on('error', function (err) {
348
+ reject(err);
349
+ });
350
+ stream.on('error', function (err) {
351
+ reject(err);
352
+ });
353
+ stream.pipe(zpipe);
354
+ });
355
+ }
356
+
357
+ /**
358
+ * 发送请求
359
+ * @param {Object} options - 配置选项
360
+ * @returns {Promise<Object>} 请求结果
361
+ */
362
+ Http.prototype.request = function (options) {
363
+ var protocol = options.host_protocol === "https" ? https : http;
364
+ var self = this;
365
+ return new Promise(function (resolve, reject) {
366
+ var body = '';
367
+ var req = protocol.request(options, (res) => {
368
+ var {
369
+ headers
370
+ } = res;
371
+ var status_code = res.statusCode;
372
+ var cookie_list = headers['set-cookie'];
373
+ if (cookie_list) {
374
+ var len = cookie_list.length;
375
+ for (var i = 0; i < len; i++) {
376
+ self.setCookie(cookie_list[i]);
377
+ }
378
+ }
379
+
380
+ // 流式下载:仅对成功响应生效
381
+ if (status_code >= 200 && status_code < 300 && options.stream_callback) {
382
+ var stream = options.stream_callback(headers);
383
+ if (stream) {
384
+ res.on('error', function (err) {
385
+ reject(err);
386
+ });
387
+ res.pipe(stream);
388
+ stream.on('finish', function () {
389
+ resolve({
390
+ status: status_code,
391
+ headers
392
+ });
393
+ });
394
+ stream.on('error', function (err) {
395
+ reject(err);
396
+ });
397
+ return;
398
+ }
399
+ }
400
+
401
+ var chunks = [];
402
+ var encoding = headers['content-encoding'];
403
+
404
+ res.on('data', function (chunk) {
405
+ chunks.push(chunk);
406
+ }).on('end', function () {
407
+ var buffer = Buffer.concat(chunks);
408
+ switch (encoding) {
409
+ case 'br':
410
+ case 'gzip':
411
+ case 'deflate':
412
+ buffer = unzipSync(buffer);
413
+ body = buffer.toString();
414
+ break;
415
+ default:
416
+ body = buffer.toString();
417
+ break;
418
+ }
419
+ var charset_match = body.match(/charset=gb2312"/i);
420
+ if (charset_match) {
421
+ body = iconv.decode(buffer, 'gb2312');
422
+ } else {
423
+ charset_match = body.match(/charset=GBK"/i);
424
+ if (charset_match) {
425
+ body = iconv.decode(buffer, 'GBK');
426
+ }
427
+ }
428
+ if (status_code >= 200 && status_code < 300) {
429
+ resolve({
430
+ status: status_code,
431
+ headers,
432
+ buffer,
433
+ body
434
+ });
435
+ } else {
436
+ resolve({
437
+ headers,
438
+ status: {
439
+ code: status_code,
440
+ message: res.statusMessage
441
+ },
442
+ buffer,
443
+ body
444
+ });
445
+ }
446
+ });
447
+ });
448
+
449
+ req.on("error", function (err) {
450
+ self.log("debug", "错误", err);
451
+ resolve({
452
+ status: 0,
453
+ message: err.message
454
+ });
455
+ });
456
+ if (options.timeout) {
457
+ req.setTimeout(options.timeout, function () {
458
+ req.destroy(new Error("请求超时"));
459
+ });
460
+ }
461
+ if (options.method.toLocaleUpperCase() == "POST") {
462
+ req.write(options.body);
463
+ }
464
+ req.end();
465
+ });
466
+ }
467
+
468
+ /**
469
+ * GET请求
470
+ * @param {string} url - 请求地址
471
+ * @param {Object} headers - 请求头
472
+ * @param {string} cookie - Cookie字符串
473
+ * @returns {Promise<Object>} 响应结果
474
+ */
475
+ Http.prototype.get = async function (url, headers, cookie) {
476
+ return await this.req({
477
+ method: "GET",
478
+ href: url,
479
+ headers,
480
+ cookie
481
+ });
482
+ };
483
+
484
+ /**
485
+ * POST请求
486
+ * @param {string} url - 请求地址
487
+ * @param {Object} body - 提交参数
488
+ * @param {Object} headers - 请求头
489
+ * @param {string} type - 传参方式 json, form, text
490
+ * @param {string} cookie - Cookie字符串
491
+ * @returns {Promise<Object>} 响应结果
492
+ */
493
+ Http.prototype.post = async function (url, body, headers, type, cookie) {
494
+ return await this.req({
495
+ method: "POST",
496
+ href: url,
497
+ body,
498
+ type,
499
+ headers,
500
+ cookie
501
+ });
502
+ };
503
+
504
+ /**
505
+ * 上传文件
506
+ * @param {string} url - 请求地址
507
+ * @param {Object} body - 表单参数,文件字段值为 {value: Buffer|string, filename, content_type}
508
+ * @param {Object} headers - 请求头
509
+ * @param {string} cookie - Cookie字符串
510
+ * @returns {Promise<Object>} 响应结果
511
+ */
512
+ Http.prototype.upload = async function (url, body, headers, cookie) {
513
+ return await this.req({
514
+ method: "POST",
515
+ href: url,
516
+ body,
517
+ type: "form-data",
518
+ headers,
519
+ cookie
520
+ });
521
+ };
522
+
523
+ /**
524
+ * 下载文件
525
+ * @param {string} url - 下载地址
526
+ * @param {string} filename - 文件名
527
+ * @param {string} auto - 自动类型
528
+ * @param {Object} headers - 协议头
529
+ * @param {string} cookie - Cookie字符串
530
+ * @returns {Promise<string>} 成功返回保存路径,失败返回null
531
+ */
532
+ Http.prototype.download = async function (url, filename, auto, headers, cookie) {
533
+ var self = this;
534
+ var file_path = null;
535
+ await this.request(this.httpOptions({
536
+ method: "GET",
537
+ href: url,
538
+ headers,
539
+ cookie,
540
+ stream_callback: function (res_headers) {
541
+ var ext = "";
542
+ if (auto) {
543
+ var parts = url.split('/');
544
+ var full_name = parts[parts.length - 1];
545
+ if (full_name.indexOf('.') !== -1) {
546
+ ext = full_name.right('.');
547
+ }
548
+ if (!ext) {
549
+ var content_type = res_headers["content-type"] || "";
550
+ ext = content_type.replace("image/", "").replace("application/x-", "").replace("text/", "")
551
+ .replace("audio/", "").replace("jpeg", "jpg").split(";")[0].trim();
552
+ }
553
+ }
554
+ file_path = ext ? filename + "." + ext : filename;
555
+ if (!file_path) {
556
+ self.log("debug", '保存文件名(filename)不能为空!');
557
+ return null;
558
+ }
559
+ return fs.createWriteStream(file_path);
560
+ }
561
+ }));
562
+ return file_path;
563
+ };
564
+
565
+ /**
566
+ * 清除缓存
567
+ */
568
+ Http.prototype.clear = function () {
569
+ this._cookies = {};
570
+ };
571
+
572
+ /**
573
+ * 导出构造函数
574
+ */
575
+ module.exports = {
576
+ Http
577
+ };
package/package.json CHANGED
@@ -1,40 +1,46 @@
1
- {
2
- "name": "mm_https",
3
- "version": "1.7.0",
4
- "description": "超级美眉HTTP请求模块 - 支持自动Cookie管理、代理设置、文件下载等功能",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "node test.js",
8
- "dev": "nodemon test.js"
9
- },
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://gitee.com/qiuwenwu91/mm_https.git"
13
- },
14
- "keywords": [
15
- "http",
16
- "https",
17
- "request",
18
- "async",
19
- "promise",
20
- "cookie",
21
- "proxy",
22
- "download",
23
- "nodejs",
24
- "http-client"
25
- ],
26
- "author": "邱文武",
27
- "license": "ISC",
28
- "bugs": {
29
- "url": "https://gitee.com/qiuwenwu91/mm_https/issues"
30
- },
31
- "homepage": "https://gitee.com/qiuwenwu91/mm_https#readme",
32
- "engines": {
33
- "node": ">=20.0.0"
34
- },
35
- "dependencies": {
36
- "iconv-lite": "^0.7.3",
37
- "mm_expand": "^2.5.3",
38
- "proxy-agent": "^8.0.2"
39
- }
40
- }
1
+ {
2
+ "name": "mm_https",
3
+ "version": "1.7.2",
4
+ "description": "超级美眉HTTP请求模块 - 支持自动Cookie管理、代理设置、文件下载等功能",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "node test.js",
8
+ "dev": "nodemon test.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://gitee.com/qiuwenwu91/mm_https.git"
13
+ },
14
+ "keywords": [
15
+ "mm_mod",
16
+ "mm_infra",
17
+ "mm_http_client",
18
+ "http",
19
+ "https",
20
+ "request",
21
+ "async",
22
+ "promise",
23
+ "cookie",
24
+ "proxy",
25
+ "download",
26
+ "nodejs",
27
+ "http-client"
28
+ ],
29
+ "layer": "adapter",
30
+ "group": "http_client",
31
+ "author": "邱文武",
32
+ "license": "ISC",
33
+ "bugs": {
34
+ "url": "https://gitee.com/qiuwenwu91/mm_https/issues"
35
+ },
36
+ "homepage": "https://gitee.com/qiuwenwu91/mm_https#readme",
37
+ "engines": {
38
+ "node": ">=20.0.0"
39
+ },
40
+ "dependencies": {
41
+ "iconv-lite": "^0.7.3",
42
+ "mm_corejs": "^1.1.6",
43
+ "mm_helper": "^1.0.0",
44
+ "proxy-agent": "^8.0.2"
45
+ }
46
+ }