@e-mc/request 0.13.6 → 0.13.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.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  ## Interface
11
11
 
12
- * [View Source](https://www.unpkg.com/@e-mc/types@0.13.6/lib/index.d.ts)
12
+ * [View Source](https://www.unpkg.com/@e-mc/types@0.13.7/lib/index.d.ts)
13
13
 
14
14
  ```typescript
15
15
  import type { IModule, ModuleConstructor } from "./index";
@@ -252,9 +252,9 @@ instance.get("http://hostname/path/config.yml", options).then(data => {
252
252
 
253
253
  ## References
254
254
 
255
- - https://www.unpkg.com/@e-mc/types@0.13.6/lib/http.d.ts
256
- - https://www.unpkg.com/@e-mc/types@0.13.6/lib/request.d.ts
257
- - https://www.unpkg.com/@e-mc/types@0.13.6/lib/settings.d.ts
255
+ - https://www.unpkg.com/@e-mc/types@0.13.7/lib/http.d.ts
256
+ - https://www.unpkg.com/@e-mc/types@0.13.7/lib/request.d.ts
257
+ - https://www.unpkg.com/@e-mc/types@0.13.7/lib/settings.d.ts
258
258
 
259
259
  * https://www.npmjs.com/package/@types/node
260
260
 
@@ -355,7 +355,12 @@ class HttpAdapter {
355
355
  result = new (require(packageName = 'fast-xml-parser').XMLParser)(parser).parse(buffer);
356
356
  break;
357
357
  case 'toml':
358
- result = require(packageName = 'toml').parse(buffer);
358
+ try {
359
+ result = require('smol-toml').parse(buffer);
360
+ }
361
+ catch {
362
+ result = require(packageName = 'toml').parse(buffer);
363
+ }
359
364
  break;
360
365
  default:
361
366
  result = JSON.parse(buffer);
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ class HttpHostAltSvc {
3
+ host;
4
+ #location = {};
5
+ #available = [];
6
+ #errors = [];
7
+ #versionData;
8
+ constructor(host, versionData) {
9
+ this.host = host;
10
+ this.#versionData = versionData;
11
+ }
12
+ did(version) {
13
+ return this.#versionData[version].status !== -1;
14
+ }
15
+ next() {
16
+ const queue = this.#available.shift();
17
+ if (queue) {
18
+ const { hostname, port, version, expires } = queue;
19
+ const ms = expires - Date.now();
20
+ if (ms < 0) {
21
+ return this.next();
22
+ }
23
+ let timeout = null;
24
+ if (!isNaN(ms)) {
25
+ timeout = setTimeout(() => {
26
+ if (!this.next()) {
27
+ this.host.version = 1;
28
+ }
29
+ }, ms);
30
+ }
31
+ this.#location = { hostname, port, version, timeout, origin: this.host.protocol + '//' + hostname + ':' + port };
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+ close(error) {
37
+ const { hostname, port, version, timeout } = this.#location;
38
+ if (hostname) {
39
+ this.#location = {};
40
+ if (timeout) {
41
+ clearTimeout(timeout);
42
+ }
43
+ if (error) {
44
+ this.#errors.push({ hostname, port, version });
45
+ return this.next();
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+ clear(version) {
51
+ if (version) {
52
+ if (this.#available.length === 0) {
53
+ this.flag(version, 0);
54
+ }
55
+ else if (!this.close(true)) {
56
+ this.flag(version, -1);
57
+ }
58
+ }
59
+ else {
60
+ this.close();
61
+ this.#available = [];
62
+ this.#errors = [];
63
+ for (const item of this.#versionData) {
64
+ if (item.alpn !== 0) {
65
+ item.status = -1;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ flag(version, value) {
71
+ this.#versionData[version - 1].status = value;
72
+ }
73
+ valid(hostname, port, version) {
74
+ return !this.errors.find(item => item.hostname === hostname && item.port === port && item.version === version);
75
+ }
76
+ set available(value) {
77
+ this.#available = value;
78
+ }
79
+ get errors() {
80
+ return this.#errors;
81
+ }
82
+ get hostname() {
83
+ return this.#location.hostname;
84
+ }
85
+ get port() {
86
+ return this.#location.port;
87
+ }
88
+ get origin() {
89
+ return this.#location.origin;
90
+ }
91
+ }
92
+ module.exports = HttpHostAltSvc;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  const tls = require("node:tls");
3
3
  const types_1 = require("@e-mc/types");
4
+ const altsvc_1 = require("@e-mc/request/http/host/altsvc");
4
5
  const HOST_LOCAL = new Set(['localhost']);
5
6
  const HOST_STREAM = new Map();
6
7
  const HOST_HTTP_1_1 = [];
@@ -11,6 +12,7 @@ for (const network of Object.entries(require('node:os').networkInterfaces())) {
11
12
  HOST_LOCAL.add(address);
12
13
  }
13
14
  }
15
+ const createData = () => ({ success: 0, failed: 0, errors: 0, alpn: 1, status: -1 });
14
16
  class HttpHost {
15
17
  static normalizeOrigin(value) {
16
18
  return (value = value.trim()).replace(/\/+$/, '') + (!/:\d+$/.test(value) ? ':' + (value.startsWith('https') ? '443' : '80') : '');
@@ -61,19 +63,14 @@ class HttpHost {
61
63
  localhost;
62
64
  _tlsConnect = null;
63
65
  #version;
64
- #altSvc = [];
65
- #altSvcQueue = [];
66
- #altSvcError = [];
67
66
  #protocol;
68
67
  #secure;
69
68
  #hostname;
70
69
  #port;
71
70
  #origin;
72
71
  #streamSize;
73
- #versionData = [
74
- [0, 0, 0, 1, -1],
75
- [0, 0, 0, -1, -1]
76
- ];
72
+ #versionData = [createData(), createData()];
73
+ #altSvc;
77
74
  constructor(url, httpVersion = 1) {
78
75
  const { protocol, hostname } = url;
79
76
  const secure = protocol === 'https:';
@@ -86,6 +83,7 @@ class HttpHost {
86
83
  this.#origin = url.origin;
87
84
  this.localhost = HOST_LOCAL.has(hostname);
88
85
  this.#streamSize = HOST_STREAM.get(address) || (this.localhost ? 65536 : 4096);
86
+ this.#altSvc = new altsvc_1(this, this.#versionData);
89
87
  if (protocol !== 'file:' && !HOST_HTTP_1_1.includes(address = protocol + '//' + address)) {
90
88
  if (secure) {
91
89
  this.#version = HOST_ALPN_H2.includes(address) ? 2 : httpVersion;
@@ -98,7 +96,7 @@ class HttpHost {
98
96
  }
99
97
  this.#version = 1;
100
98
  for (const version of this.#versionData) {
101
- version[4] = 0;
99
+ version.status = 0;
102
100
  }
103
101
  }
104
102
  async hasProtocol(version) {
@@ -107,17 +105,17 @@ class HttpHost {
107
105
  if (!data || !this.secure) {
108
106
  return 0;
109
107
  }
110
- const status = data[3];
111
- switch (status) {
108
+ switch (data.alpn) {
112
109
  case 0:
113
110
  case 1:
114
- return status;
111
+ return data.alpn;
115
112
  default:
116
113
  return this._tlsConnect ||= new Promise(resolve => {
117
114
  const alpn = 'h' + version;
118
115
  const socket = tls.connect(+this.port, this.hostname, { ALPNProtocols: [alpn], requestCert: true, rejectUnauthorized: false }, () => {
119
116
  this._tlsConnect = null;
120
- resolve(data[3] = alpn === socket.alpnProtocol ? 1 : 0);
117
+ data.alpn = alpn === socket.alpnProtocol ? 1 : 0;
118
+ resolve(data.alpn);
121
119
  });
122
120
  socket
123
121
  .setNoDelay(false)
@@ -127,7 +125,8 @@ class HttpHost {
127
125
  if (this._tlsConnect) {
128
126
  this._tlsConnect = null;
129
127
  if (this.error(version) >= 10) {
130
- resolve(data[3] = 0);
128
+ data.alpn = 0;
129
+ resolve(0);
131
130
  }
132
131
  else {
133
132
  resolve(2);
@@ -137,7 +136,8 @@ class HttpHost {
137
136
  .on('error', () => {
138
137
  this.failed(version);
139
138
  this._tlsConnect = null;
140
- resolve(data[3] = 0);
139
+ data.alpn = 0;
140
+ resolve(0);
141
141
  })
142
142
  .end();
143
143
  });
@@ -147,179 +147,132 @@ class HttpHost {
147
147
  }
148
148
  success(version, status) {
149
149
  const data = this.#versionData[version - 1];
150
- return status ? data[0] : ++data[0];
150
+ return status ? data.success : ++data.success;
151
151
  }
152
152
  failed(version, status) {
153
153
  const data = this.#versionData[version - 1];
154
154
  if (status) {
155
- return data[1];
155
+ return data.failed;
156
156
  }
157
- this.clearAltSvc(version);
158
- return ++data[1];
157
+ this.altSvc.clear(version);
158
+ return ++data.failed;
159
159
  }
160
160
  error(version, status) {
161
161
  const data = this.#versionData[version - 1];
162
162
  if (status) {
163
- return data[2];
163
+ return data.errors;
164
164
  }
165
- if (data[4] !== 2) {
166
- this.closeAltSvc(true);
165
+ if (data.status !== 2) {
166
+ this.altSvc.close(true);
167
167
  }
168
- return ++data[2];
168
+ return ++data.errors;
169
169
  }
170
170
  upgrade(version, altSvc) {
171
171
  if (altSvc && this.secure) {
172
172
  if (altSvc === 'clear') {
173
- this.clearAltSvc();
173
+ this.altSvc.clear();
174
174
  return;
175
175
  }
176
176
  const data = this.#versionData;
177
177
  for (let i = data.length - 1; i >= version; --i) {
178
178
  const host = data[i];
179
- if (host[4] === 0) {
179
+ if (host.status === 0) {
180
180
  continue;
181
181
  }
182
+ const h = i + 1;
182
183
  const increment = (flag) => {
183
- host[4] = flag;
184
- if (this.#version < i + 1) {
185
- this.#version = i + 1;
184
+ host.status = flag;
185
+ if (this.#version < h) {
186
+ this.#version = h;
186
187
  return true;
187
188
  }
188
189
  return false;
189
190
  };
190
- const addresses = [];
191
- const time = Date.now();
191
+ const available = [];
192
192
  const hostname = this.#hostname;
193
- const excluded = this.#altSvcError;
194
- for (const match of altSvc.matchAll(new RegExp(`h${i + 1}(?:-\\d+)?="([^:]*):(\\d+)"([^,]*)`, 'g'))) {
193
+ for (const match of altSvc.matchAll(new RegExp(`h${h}(?:-\\d+)?="([^:]*):(\\d+)"([^,]*)`, 'g'))) {
195
194
  const port = match[2];
196
195
  if (!match[1] && port === this.port) {
197
196
  increment(2);
198
- addresses.length = 0;
197
+ available.length = 0;
199
198
  break;
200
199
  }
201
200
  const address = match[1] || hostname;
202
201
  const ma = +(/ma=(\d+)/.exec(match[3])?.[1] || 86400);
203
- if (!excluded.includes(`h${i + 1}:${address}:${port}`)) {
204
- addresses.push([
205
- address,
202
+ if (this.altSvc.valid(address, port, h)) {
203
+ available.push({
204
+ hostname: address,
206
205
  port,
207
- ma >= 2592000 ? NaN : time + Math.min(ma * 1000, 2147483647),
208
- i + 1,
209
- match[3].includes('persist=1')
210
- ]);
206
+ version: h,
207
+ expires: ma >= 2592000 ? NaN : Date.now() + Math.min(ma * 1000, 2147483647),
208
+ persist: match[3].includes('persist=1')
209
+ });
211
210
  }
212
211
  }
213
- if (addresses.length > 0) {
214
- this.closeAltSvc();
215
- this.#altSvcQueue = addresses.sort((a, b) => {
216
- if (a[0] === hostname) {
212
+ if (available.length > 0) {
213
+ this.altSvc.close();
214
+ this.altSvc.available = available.sort((a, b) => {
215
+ if (a.hostname === hostname) {
217
216
  return -1;
218
217
  }
219
- if (b[0] === hostname) {
218
+ if (b.hostname === hostname) {
220
219
  return 1;
221
220
  }
222
- if (isNaN(a[2]) || a[2] > b[2]) {
221
+ if (isNaN(a.expires) || a.expires > b.expires) {
223
222
  return -1;
224
223
  }
225
- if (isNaN(b[2]) || a[2] < b[2]) {
224
+ if (isNaN(b.expires) || a.expires < b.expires) {
226
225
  return 1;
227
226
  }
228
- if (a[3] && !b[3]) {
229
- return -1;
230
- }
231
- if (!a[3] && b[3]) {
232
- return -1;
233
- }
234
227
  return 0;
235
228
  });
236
229
  if (increment(1)) {
237
- this.nextAltSvc();
230
+ this.altSvc.next();
238
231
  }
239
232
  }
240
233
  }
241
234
  }
242
235
  }
243
236
  didAltSvc(version) {
244
- return this.#versionData[version][4] !== -1;
237
+ return this.#versionData[version].status !== -1;
245
238
  }
246
239
  nextAltSvc() {
247
- const queue = this.#altSvcQueue.shift();
248
- if (queue) {
249
- const [hostname, port, expires, version] = queue;
250
- const timeout = expires - Date.now();
251
- if (timeout < 0) {
252
- return this.nextAltSvc();
253
- }
254
- let timer = null;
255
- if (!isNaN(timeout)) {
256
- timer = setTimeout(() => {
257
- if (!this.nextAltSvc()) {
258
- this.version = 1;
259
- }
260
- }, timeout);
261
- }
262
- this.#altSvc = [hostname, port, timer, version, this.protocol + '//' + hostname + ':' + port];
263
- return true;
264
- }
265
- return false;
240
+ return this.altSvc.next();
266
241
  }
267
242
  closeAltSvc(error) {
268
- const [hostname, port, timeout, version] = this.#altSvc;
269
- if (hostname) {
270
- this.#altSvc = [];
271
- if (timeout) {
272
- clearTimeout(timeout);
273
- }
274
- if (error) {
275
- this.#altSvcError.push(`h${version}:${hostname}:${port}`);
276
- return this.nextAltSvc();
277
- }
278
- }
279
- return false;
243
+ return this.altSvc.close(error);
280
244
  }
281
245
  clearAltSvc(version) {
282
- if (version) {
283
- if (this.#altSvcQueue.length === 0) {
284
- this.flagAltSvc(version, 0);
285
- }
286
- else if (!this.closeAltSvc(true)) {
287
- this.flagAltSvc(version, -1);
288
- }
289
- }
290
- else {
291
- this.closeAltSvc();
292
- this.#altSvcQueue = [];
293
- this.#altSvcError = [];
294
- for (const item of this.#versionData) {
295
- if (item[3]) {
296
- item[4] = -1;
297
- }
298
- }
299
- }
246
+ this.altSvc.clear(version);
300
247
  }
301
248
  flagAltSvc(version, value) {
302
- this.#versionData[version - 1][4] = value;
249
+ this.altSvc.flag(version, value);
303
250
  }
304
251
  reset() {
305
- this.clearAltSvc();
252
+ this.altSvc.clear();
306
253
  this.#versionData.forEach((item, index) => {
307
- item[0] = 0;
308
- item[1] = 0;
309
- item[2] = 0;
254
+ item.success = 0;
255
+ item.failed = 0;
256
+ item.errors = 0;
310
257
  if (index > 0) {
311
- item[3] = -1;
258
+ item.alpn = -1;
312
259
  }
313
260
  });
314
261
  }
262
+ v1() {
263
+ return this.#version === 1;
264
+ }
315
265
  v2() {
316
266
  return this.#version === 2;
317
267
  }
268
+ get altSvc() {
269
+ return this.#altSvc;
270
+ }
318
271
  set version(value) {
319
272
  switch (value) {
320
273
  case 1:
321
274
  case 2:
322
- this.flagAltSvc(this.#version = value, -1);
275
+ this.altSvc.flag(this.#version = value, -1);
323
276
  break;
324
277
  }
325
278
  }
@@ -333,13 +286,13 @@ class HttpHost {
333
286
  return this.#secure;
334
287
  }
335
288
  get hostname() {
336
- return this.#altSvc[0] || this.#hostname;
289
+ return this.altSvc.hostname || this.#hostname;
337
290
  }
338
291
  get port() {
339
- return this.#altSvc[1] || this.#port;
292
+ return this.altSvc.port || this.#port;
340
293
  }
341
294
  get origin() {
342
- return this.#altSvc[4] || this.#origin;
295
+ return this.altSvc.origin || this.#origin;
343
296
  }
344
297
  get streamSize() {
345
298
  return this.#streamSize;
package/index.js CHANGED
@@ -28,13 +28,14 @@ const REGEXP_GLOBWITHIN = /\\\?|(?:(?<!\\)(?:\*|\[!?[^!\]]+\]|\{(?:[^,]+,)+[^}]+
28
28
  const REGEXP_RCLONE = /^rclone:\?/i;
29
29
  const HTTP = {
30
30
  HOST: {},
31
- HEADERS: {},
31
+ HEADERS: Object.create(null),
32
+ CACHE: new WeakMap(),
32
33
  VERSION: 1,
33
34
  PROXY: null
34
35
  };
35
36
  const TLS = {
36
- TEXT: {},
37
- FILE: {}
37
+ TEXT: Object.create(null),
38
+ FILE: Object.create(null)
38
39
  };
39
40
  const DNS = {
40
41
  CACHE: Object.create(null),
@@ -61,7 +62,7 @@ const ARIA2 = {
61
62
  LOWEST_SPEED_LIMIT: null,
62
63
  ALWAYS_RESUME: false,
63
64
  FILE_ALLOCATION: 'none',
64
- PROXY: {},
65
+ PROXY: Object.create(null),
65
66
  NO_PROXY: '',
66
67
  CONF_PATH: ''
67
68
  };
@@ -115,21 +116,6 @@ let READ_TIMEOUT = 0;
115
116
  let AGENT_TIMEOUT = 0;
116
117
  let LOG_HTTP = false;
117
118
  let LOG_TIMEPROCESS = true;
118
- function getBaseHeaders(uri, headers) {
119
- let result;
120
- uri = (0, util_1.trimPath)(uri);
121
- for (const pathname in headers) {
122
- if (pathname === uri || uri.startsWith(pathname + '/')) {
123
- (result ||= []).push([pathname, headers[pathname]]);
124
- }
125
- }
126
- if (result) {
127
- if (result.length > 1) {
128
- result.sort((a, b) => b[0].length - a[0].length);
129
- }
130
- return result[0][1];
131
- }
132
- }
133
119
  function setDnsCache(hostname, value, expires) {
134
120
  expires ??= DNS.EXPIRES;
135
121
  if (expires > 0 && !DNS.CACHE[hostname]) {
@@ -146,11 +132,6 @@ function setDnsCache(hostname, value, expires) {
146
132
  }
147
133
  }
148
134
  }
149
- function setOutgoingHeaders(output, headers) {
150
- for (const href in headers) {
151
- output[href] = (0, util_1.normalizeHeaders)(headers[href]);
152
- }
153
- }
154
135
  function getProxySettings(request, agentTimeout) {
155
136
  const proxy = request.proxy;
156
137
  if (proxy && (proxy.origin || proxy.address && proxy.port)) {
@@ -175,11 +156,9 @@ function getProxySettings(request, agentTimeout) {
175
156
  return null;
176
157
  }
177
158
  function closeTorrent(pid) {
178
- if (typeof pid === 'number') {
179
- const index = ARIA2.PID_QUEUE.findIndex(value => pid === value[0]);
180
- if (index !== -1) {
181
- ARIA2.PID_QUEUE.splice(index, 1);
182
- }
159
+ const index = ARIA2.PID_QUEUE.findIndex(value => value[0] === pid);
160
+ if (index !== -1) {
161
+ ARIA2.PID_QUEUE.splice(index, 1);
183
162
  }
184
163
  }
185
164
  function clearDnsLookup() {
@@ -199,9 +178,9 @@ function resetHttpHost(version) {
199
178
  case 2:
200
179
  for (const origin in HTTP.HOST) {
201
180
  const host = HTTP.HOST[origin];
202
- if (host.secure && host.version === 1) {
181
+ if (host.secure && host.v1()) {
203
182
  const failed = host.failed(2, true);
204
- if (failed === 0 && host.failed(2, true) < 10 || failed < 3 && host.success(2, true) > 0) {
183
+ if (failed === 0 && host.error(2, true) < 10 || failed < 3 && host.success(2, true) > 0) {
205
184
  host.version = version;
206
185
  }
207
186
  }
@@ -296,50 +275,16 @@ function copySearchParams(url, base) {
296
275
  }
297
276
  });
298
277
  }
299
- function checkEncoding(request, response, statusCode, outStream, contentEncoding) {
278
+ function checkEncoding(request, statusCode, contentEncoding) {
300
279
  switch (statusCode) {
301
280
  case 206:
302
281
  request.emit('error', (0, types_1.errorValue)("Aborted", 'Partial content'));
303
282
  case 204:
304
283
  case 205:
305
284
  case 304:
306
- return;
307
- }
308
- if (!contentEncoding) {
309
- return;
310
- }
311
- contentEncoding = contentEncoding.trim().toLowerCase();
312
- const chunkSize = outStream?.writableHighWaterMark;
313
- let pipeTo;
314
- if (!contentEncoding.includes(',')) {
315
- pipeTo = decompressEncoding(contentEncoding, chunkSize);
316
- }
317
- else {
318
- for (const value of contentEncoding.split(/\s*,\s*/).reverse()) {
319
- const next = decompressEncoding(value, chunkSize);
320
- if (!next) {
321
- return;
322
- }
323
- pipeTo = pipeTo ? pipeTo.pipe(next) : next;
324
- }
325
- }
326
- if (pipeTo) {
327
- if (outStream) {
328
- stream.pipeline(response, pipeTo, outStream, err => {
329
- if (err) {
330
- response.emit('error', err);
331
- }
332
- });
333
- }
334
- else {
335
- stream.pipeline(response, pipeTo, err => {
336
- if (err) {
337
- response.emit('error', err);
338
- }
339
- });
340
- }
341
- return pipeTo;
285
+ return false;
342
286
  }
287
+ return !!contentEncoding;
343
288
  }
344
289
  function sendBody(request, options) {
345
290
  const postData = options.postData;
@@ -351,26 +296,11 @@ function sendBody(request, options) {
351
296
  }
352
297
  request.end();
353
298
  }
354
- function decompressEncoding(value, chunkSize) {
355
- switch (value) {
356
- case 'gzip':
357
- return zlib.createGunzip({ chunkSize });
358
- case 'br':
359
- return zlib.createBrotliDecompress({ chunkSize });
360
- case 'deflate':
361
- return zlib.createInflate({ chunkSize });
362
- case 'deflate-raw':
363
- return zlib.createInflateRaw({ chunkSize });
364
- case 'zstd':
365
- if (SUPPORTED_ZSTD) {
366
- return zlib.createZstdDecompress({ chunkSize });
367
- }
368
- break;
369
- }
370
- }
371
299
  function resetAria2() {
372
- clearInterval(ARIA2.PID_TIMER);
373
- ARIA2.PID_TIMER = null;
300
+ if (ARIA2.PID_TIMER) {
301
+ clearInterval(ARIA2.PID_TIMER);
302
+ ARIA2.PID_TIMER = null;
303
+ }
374
304
  }
375
305
  function escapeShellQuote(value) {
376
306
  value = value.replace(/(?<!\\)"/g, '\\"');
@@ -464,32 +394,7 @@ function setBinHeaders(args, headers) {
464
394
  args.push(...items.map(value => `--header="${name}: ${escapeShellQuote(value)}"`));
465
395
  }
466
396
  }
467
- function finalizeBinArgs(instance, name, bin, args, opts, binOpts, cmd = []) {
468
- if (binOpts) {
469
- for (const leading of binOpts) {
470
- if (leading.startsWith('-')) {
471
- const pattern = new RegExp(`^${leading}(?:=|$)`);
472
- for (let i = 0; i < opts.length; ++i) {
473
- if (pattern.test(opts[i])) {
474
- opts.splice(i--, i);
475
- break;
476
- }
477
- }
478
- }
479
- }
480
- args = binOpts.concat(args);
481
- }
482
- if (args.length > 0) {
483
- if (module_1.hasLogType(32768)) {
484
- instance.formatMessage(32768, name.toUpperCase(), [bin].concat(cmd).join(' '), args.join(' '), { ...module_1.LOG_STYLE_WARN });
485
- }
486
- else {
487
- instance.addLog(4, path.basename(bin) + ' ' + args.join(' '), name);
488
- }
489
- }
490
- return args;
491
- }
492
- function addAria2Proxy(args, protocol, host) {
397
+ function appendAria2Proxy(args, protocol, host) {
493
398
  const { origin, username, password } = host;
494
399
  args.push(`--${protocol}-proxy="${origin}"`);
495
400
  if (username) {
@@ -716,7 +621,9 @@ class Request extends module_1 {
716
621
  }
717
622
  }
718
623
  if ((0, types_1.isPlainObject)(headers)) {
719
- setOutgoingHeaders(HTTP.HEADERS, headers);
624
+ for (const href in headers) {
625
+ HTTP.HEADERS[href] = (0, util_1.normalizeHeaders)(headers[href]);
626
+ }
720
627
  }
721
628
  if ((0, types_1.isPlainObject)(certs)) {
722
629
  [TLS.TEXT, TLS.FILE] = validateCerts(certs);
@@ -827,9 +734,9 @@ class Request extends module_1 {
827
734
  };
828
735
  #singleton = false;
829
736
  #httpVersion = null;
737
+ #headers = null;
830
738
  #ipVersion;
831
739
  #agentTimeout;
832
- #headers = null;
833
740
  #baseUrl = null;
834
741
  #connectDns = Object.create(null);
835
742
  #pendingDns = Object.create(null);
@@ -839,7 +746,7 @@ class Request extends module_1 {
839
746
  #adapter = HTTP_ADAPTER;
840
747
  #certs = null;
841
748
  #downloading = new Set();
842
- #hostInfo = {};
749
+ #hostInfo = Object.create(null);
843
750
  #session = [Object.create(null)];
844
751
  constructor(data) {
845
752
  super();
@@ -865,9 +772,7 @@ class Request extends module_1 {
865
772
  if (proxy) {
866
773
  this.proxy = proxy;
867
774
  }
868
- if ((0, types_1.isObject)(headers)) {
869
- setOutgoingHeaders(this.#headers = {}, headers);
870
- }
775
+ this.parseHeaders(headers);
871
776
  if ((0, types_1.isObject)(certs)) {
872
777
  this.#certs = validateCerts(certs);
873
778
  }
@@ -975,9 +880,7 @@ class Request extends module_1 {
975
880
  init(config) {
976
881
  if (config) {
977
882
  const { headers, httpVersion, ipVersion, readTimeout } = config;
978
- if ((0, types_1.isObject)(headers)) {
979
- setOutgoingHeaders(this.#headers ||= {}, headers);
980
- }
883
+ this.parseHeaders(headers);
981
884
  if (httpVersion !== undefined) {
982
885
  this.httpVersion = httpVersion;
983
886
  }
@@ -1201,8 +1104,7 @@ class Request extends module_1 {
1201
1104
  }
1202
1105
  }
1203
1106
  headersOf(uri) {
1204
- const headers = this.#headers;
1205
- return headers && getBaseHeaders(uri, headers) || (this.host ? getBaseHeaders(uri, HTTP.HEADERS) : undefined);
1107
+ return this.findHeadersByUri(uri) || (this.host ? this.findHeadersByUri(uri, HTTP.HEADERS) : undefined);
1206
1108
  }
1207
1109
  async aria2c(uri, options = {}) {
1208
1110
  if (!ARIA2.BIN) {
@@ -1220,7 +1122,7 @@ class Request extends module_1 {
1220
1122
  ({ pathname, headers, binOpts } = this.parseBinOpts(options, ['--daemon'], ['--input-file']));
1221
1123
  }
1222
1124
  try {
1223
- if (typeof uri === 'string' && module_1.isURL(uri)) {
1125
+ if ((0, types_1.isString)(uri) && module_1.isURL(uri)) {
1224
1126
  uri = new URL(uri);
1225
1127
  }
1226
1128
  pathname = checkBinTarget(this, "aria2", uri, pathname, 'aria2', binOpts);
@@ -1346,12 +1248,12 @@ class Request extends module_1 {
1346
1248
  }
1347
1249
  }
1348
1250
  if (proxy) {
1349
- addAria2Proxy(args, protocol, proxy.host);
1251
+ appendAria2Proxy(args, protocol, proxy.host);
1350
1252
  }
1351
1253
  else if (ARIA2.PROXY.all) {
1352
- addAria2Proxy(opts, 'all', ARIA2.PROXY.all.host);
1254
+ appendAria2Proxy(opts, 'all', ARIA2.PROXY.all.host);
1353
1255
  }
1354
- args = finalizeBinArgs(this, "aria2", ARIA2.BIN, args, opts, binOpts);
1256
+ args = this.mergeBinOpts(args, opts, binOpts, { name: "aria2", bin: ARIA2.BIN });
1355
1257
  opts.push(`"${escapeShellQuote(uri)}"`);
1356
1258
  args = args.concat(init, opts);
1357
1259
  const startTime = Date.now();
@@ -1640,7 +1542,7 @@ class Request extends module_1 {
1640
1542
  setBinHeaders(args, headers);
1641
1543
  const cwd = module_1.isDir(pathname) ? pathname : path.dirname(pathname);
1642
1544
  const cmd = [source, pathname];
1643
- args = finalizeBinArgs(this, "rclone", RCLONE.BIN, args, opts, binOpts, cmd).concat(init, opts);
1545
+ args = this.mergeBinOpts(args, opts, binOpts, { name: "rclone", bin: RCLONE.BIN, cmd }).concat(init, opts);
1644
1546
  args.push(...cmd.map(value => (0, types_1.sanitizeCmd)(value)));
1645
1547
  args.unshift(command);
1646
1548
  const startTime = Date.now();
@@ -1866,7 +1768,8 @@ class Request extends module_1 {
1866
1768
  }
1867
1769
  connected = true;
1868
1770
  if (this.matchStatus(statusCode, url, response, request, options) && hasResponse(statusCode)) {
1869
- if (emitter = checkEncoding(request, request, statusCode, outStream, response['content-encoding'])) {
1771
+ const contentEncoding = response['content-encoding'];
1772
+ if (checkEncoding(request, statusCode, contentEncoding) && (emitter = this.pipeline(request, contentEncoding, outStream))) {
1870
1773
  for (const event in listenerMap) {
1871
1774
  const [name, type] = event.split('-');
1872
1775
  for (const listener of listenerMap[event]) {
@@ -1942,7 +1845,7 @@ class Request extends module_1 {
1942
1845
  if (proxy) {
1943
1846
  keepAlive ??= proxy.keepAlive;
1944
1847
  agentTimeout ??= proxy.agentTimeout;
1945
- const proxyHeaders = this.#headers && getBaseHeaders(proxy.host.href, this.#headers) || getBaseHeaders(proxy.host.href, HTTP.HEADERS);
1848
+ const proxyHeaders = this.findHeadersByUri(proxy.host.href) || this.findHeadersByUri(proxy.host.href, HTTP.HEADERS);
1946
1849
  const pkg = secure ? 'https-proxy-agent' : 'http-proxy-agent';
1947
1850
  try {
1948
1851
  agent = require(pkg)(proxy.host, keepAlive === true || keepAlive === false && agentTimeout !== 0 || agentTimeout > 0 ? { ...agentOptions, keepAlive: keepAlive ?? true, timeout: agentTimeout, headers: proxyHeaders } : { ...agentOptions, headers: proxyHeaders });
@@ -1993,8 +1896,9 @@ class Request extends module_1 {
1993
1896
  const statusCode = response.statusCode;
1994
1897
  const incoming = response.headers;
1995
1898
  if (!expectContinue && this.matchStatus(statusCode, url, incoming, request, options) && (getting || posting) && hasResponse(statusCode)) {
1996
- let source = checkEncoding(request, response, statusCode, outStream, incoming['content-encoding']);
1997
- if (source) {
1899
+ const contentEncoding = incoming['content-encoding'];
1900
+ let source;
1901
+ if (checkEncoding(request, statusCode, contentEncoding) && (source = this.pipeline(response, contentEncoding, outStream))) {
1998
1902
  source.once('finish', () => {
1999
1903
  request.emit('end');
2000
1904
  });
@@ -2042,7 +1946,7 @@ class Request extends module_1 {
2042
1946
  if (version === 2 && incoming.upgrade?.includes('h2')) {
2043
1947
  host.version = 2;
2044
1948
  }
2045
- else if (!host.didAltSvc(1)) {
1949
+ else if (!host.altSvc.did(1)) {
2046
1950
  host.upgrade(1, incoming['alt-svc']);
2047
1951
  }
2048
1952
  }
@@ -2162,13 +2066,7 @@ class Request extends module_1 {
2162
2066
  else {
2163
2067
  options = {};
2164
2068
  }
2165
- const headers = (0, util_1.parseOutgoingHeaders)(options.headers) || {};
2166
- for (const attr in headers) {
2167
- const name = attr.toLowerCase();
2168
- if (name === 'content-type' || name === 'content-length') {
2169
- delete headers[attr];
2170
- }
2171
- }
2069
+ const headers = (0, util_1.parseOutgoingHeaders)(options.headers, 'content-type', 'content-length') || {};
2172
2070
  if (!putting && (parts || contentType === "multipart/form-data" || contentType === 'form-data')) {
2173
2071
  let valid = false;
2174
2072
  if ((0, types_1.isArray)(parts)) {
@@ -2317,6 +2215,57 @@ class Request extends module_1 {
2317
2215
  });
2318
2216
  this.#downloading.clear();
2319
2217
  }
2218
+ pipeline(response, encoding, outStream) {
2219
+ const chunkSize = outStream?.writableHighWaterMark;
2220
+ let pipeTo;
2221
+ encoding = encoding.trim().toLowerCase();
2222
+ if (!encoding.includes(',')) {
2223
+ pipeTo = this.fromEncoding(encoding, { chunkSize });
2224
+ }
2225
+ else {
2226
+ for (const value of encoding.split(/\s*,\s*/).reverse()) {
2227
+ const next = this.fromEncoding(value, { chunkSize });
2228
+ if (!next) {
2229
+ return;
2230
+ }
2231
+ pipeTo = pipeTo ? pipeTo.pipe(next) : next;
2232
+ }
2233
+ }
2234
+ if (pipeTo) {
2235
+ if (outStream) {
2236
+ stream.pipeline(response, pipeTo, outStream, err => {
2237
+ if (err) {
2238
+ response.emit('error', err);
2239
+ }
2240
+ });
2241
+ }
2242
+ else {
2243
+ stream.pipeline(response, pipeTo, err => {
2244
+ if (err) {
2245
+ response.emit('error', err);
2246
+ }
2247
+ });
2248
+ }
2249
+ return pipeTo;
2250
+ }
2251
+ }
2252
+ fromEncoding(value, options) {
2253
+ switch (value) {
2254
+ case 'gzip':
2255
+ return zlib.createGunzip(options);
2256
+ case 'br':
2257
+ return zlib.createBrotliDecompress(options);
2258
+ case 'deflate':
2259
+ return zlib.createInflate(options);
2260
+ case 'deflate-raw':
2261
+ return zlib.createInflateRaw(options);
2262
+ case 'zstd':
2263
+ if (SUPPORTED_ZSTD) {
2264
+ return zlib.createZstdDecompress(options);
2265
+ }
2266
+ break;
2267
+ }
2268
+ }
2320
2269
  matchStatus(code, url, headers, request, options) {
2321
2270
  const status = this.#statusOn?.get(code);
2322
2271
  if (status) {
@@ -2395,7 +2344,7 @@ class Request extends module_1 {
2395
2344
  }
2396
2345
  if ((0, types_1.isArray)(options.binOpts)) {
2397
2346
  let next = false;
2398
- binOpts = options.binOpts.filter((opt) => !((0, types_1.isString)(opt) && /^-[a-z].*$/i.test(opt.trim()))).map((opt) => {
2347
+ binOpts = options.binOpts.filter((opt) => !((0, types_1.isString)(opt) && /^-[a-z].*$/i.test(opt))).map((opt) => {
2399
2348
  if (next) {
2400
2349
  if (!module_1.asString(opt).startsWith('--')) {
2401
2350
  return [];
@@ -2445,6 +2394,58 @@ class Request extends module_1 {
2445
2394
  }
2446
2395
  return { pathname, headers: (0, util_1.parseOutgoingHeaders)(options.headers), binOpts };
2447
2396
  }
2397
+ mergeBinOpts(args, opts, binOpts, options) {
2398
+ if (binOpts) {
2399
+ for (const leading of binOpts) {
2400
+ if (leading.charAt(0) === '-') {
2401
+ const pattern = new RegExp(`^${leading}(?:=|$)`);
2402
+ for (let i = 0; i < opts.length; ++i) {
2403
+ if (pattern.test(opts[i])) {
2404
+ opts.splice(i--, i);
2405
+ break;
2406
+ }
2407
+ }
2408
+ }
2409
+ }
2410
+ args = binOpts.concat(args);
2411
+ }
2412
+ if (options && args.length > 0) {
2413
+ const { name, bin, cmd = [] } = options;
2414
+ if (module_1.hasLogType(32768)) {
2415
+ this.formatMessage(32768, name.toUpperCase(), [bin].concat(cmd).join(' '), args.join(' '), { ...module_1.LOG_STYLE_WARN });
2416
+ }
2417
+ else {
2418
+ this.addLog(4, path.basename(bin) + ' ' + args.join(' '), name);
2419
+ }
2420
+ }
2421
+ return args;
2422
+ }
2423
+ parseHeaders(outgoing) {
2424
+ if ((0, types_1.isPlainObject)(outgoing)) {
2425
+ Object.assign(this.#headers ||= {}, outgoing);
2426
+ }
2427
+ }
2428
+ findHeadersByUri(uri, outgoing = this.#headers) {
2429
+ if (outgoing) {
2430
+ const data = [];
2431
+ uri = (0, util_1.trimPath)(uri);
2432
+ for (const pathname in outgoing) {
2433
+ if (pathname === uri || uri.startsWith(pathname + '/')) {
2434
+ data.push([pathname, outgoing[pathname]]);
2435
+ }
2436
+ }
2437
+ if (data.length > 0) {
2438
+ data.sort((a, b) => b[0].length - a[0].length);
2439
+ const headers = data[0][1];
2440
+ let result = HTTP.CACHE.get(headers);
2441
+ if (!result) {
2442
+ result = (0, util_1.normalizeHeaders)(headers);
2443
+ HTTP.CACHE.set(headers, result);
2444
+ }
2445
+ return result;
2446
+ }
2447
+ }
2448
+ }
2448
2449
  set adapter(value) {
2449
2450
  if (adapter_1.constructorOf(value)) {
2450
2451
  this.#adapter = value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e-mc/request",
3
- "version": "0.13.6",
3
+ "version": "0.13.7",
4
4
  "description": "Request constructor for E-mc.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -19,8 +19,8 @@
19
19
  "license": "BSD-3-Clause",
20
20
  "homepage": "https://github.com/anpham6/e-mc#readme",
21
21
  "dependencies": {
22
- "@e-mc/module": "0.13.6",
23
- "@e-mc/types": "0.13.6",
22
+ "@e-mc/module": "0.13.7",
23
+ "@e-mc/types": "0.13.7",
24
24
  "combined-stream": "^1.0.8",
25
25
  "js-yaml": "^4.1.1",
26
26
  "picomatch": "^4.0.3",
package/util.d.ts CHANGED
@@ -6,15 +6,15 @@ import type { Readable, Writable } from 'node:stream';
6
6
 
7
7
  declare namespace util {
8
8
  function parseHeader<T = unknown>(headers: IncomingHttpHeaders, name: string): T | undefined;
9
- function parseOutgoingHeaders(headers: OutgoingHttpHeaders | Headers | undefined): OutgoingHttpHeaders | undefined;
10
- function normalizeHeaders(headers: OutgoingHttpHeaders): OutgoingHttpHeaders;
9
+ function parseOutgoingHeaders(headers: OutgoingHttpHeaders | Headers | undefined, ...ignore: string[]): OutgoingHttpHeaders | undefined;
10
+ function normalizeHeaders(headers: OutgoingHttpHeaders | Headers): OutgoingHttpHeaders;
11
11
  function getBasicAuth(auth: AuthValue): string;
12
12
  function getBasicAuth(username: unknown, password?: unknown): string;
13
13
  function hasBasicAuth(value: string): boolean;
14
14
  function checkRetryable(err: unknown): boolean;
15
15
  function isRetryable(value: number, timeout?: boolean): boolean;
16
16
  function parseHttpProxy(value?: string): HttpProxySettings | undefined;
17
- function trimPath(value: string): string;
17
+ function trimPath(value: string, char?: string): string;
18
18
  function asInt(value: unknown): number;
19
19
  function asFloat(value: unknown): number;
20
20
  function fromSeconds(value: unknown): number;
package/util.js CHANGED
@@ -25,45 +25,63 @@ const node_util_1 = require("node:util");
25
25
  const types_1 = require("@e-mc/types");
26
26
  const module_1 = require("@e-mc/module");
27
27
  const host_1 = require("@e-mc/request/http/host");
28
+ function setHeader(result, key, value) {
29
+ if (key === 'set-cookie') {
30
+ (result[key] ||= []).push(value);
31
+ }
32
+ else {
33
+ result[key] = value;
34
+ }
35
+ }
28
36
  const safeInt = (value) => value >= 0 ? Math.min(value, Number.MAX_SAFE_INTEGER) : NaN;
29
37
  function parseHeader(headers, name) {
30
38
  const value = headers[name];
31
- if (!value) {
32
- return;
33
- }
34
39
  switch (name.toLowerCase()) {
35
- case 'content-disposition': {
36
- let result;
37
- for (const match of value.matchAll(/\bfilename(?:\*\s*=\s*UTF-8''([^\s;]+)|\s*=\s*(?:"([^"]+)"|([^\s;]+)))/gi)) {
38
- if (match[1]) {
39
- return decodeURIComponent(match[1]).trim();
40
+ case 'content-disposition':
41
+ if ((0, types_1.isString)(value)) {
42
+ let result;
43
+ for (const match of value.matchAll(/\bfilename(?:\*\s*=\s*UTF-8''([^\s;]+)|\s*=\s*(?:"([^"]+)"|([^\s;]+)))/gi)) {
44
+ if (match[1]) {
45
+ return decodeURIComponent(match[1]).trim();
46
+ }
47
+ result = (match[2] || match[3]).trim();
40
48
  }
41
- result = (match[2] || match[3]).trim();
49
+ return result;
42
50
  }
43
- return result;
44
- }
51
+ break;
45
52
  }
46
53
  }
47
- function parseOutgoingHeaders(headers) {
54
+ function parseOutgoingHeaders(headers, ...ignore) {
48
55
  if (!headers) {
49
56
  return;
50
57
  }
51
58
  if (headers instanceof Headers) {
52
- const result = {};
59
+ const result = Object.create(null);
53
60
  headers.forEach((value, key) => {
54
- if (key === 'set-cookie') {
55
- (result[key] ||= []).push(value);
56
- }
57
- else {
58
- result[key] = value;
61
+ if (!ignore.includes(key)) {
62
+ setHeader(result, key, value);
59
63
  }
60
64
  });
61
65
  return result;
62
66
  }
67
+ if (ignore.length > 0) {
68
+ const result = {};
69
+ for (const attr in headers) {
70
+ const name = attr.toLowerCase();
71
+ if (!ignore.includes(name)) {
72
+ result[name] = headers[attr];
73
+ }
74
+ }
75
+ return result;
76
+ }
63
77
  return { ...headers };
64
78
  }
65
79
  function normalizeHeaders(headers) {
66
80
  const result = Object.create(null);
81
+ if (headers instanceof Headers) {
82
+ headers.forEach((value, key) => setHeader(result, key, value));
83
+ return result;
84
+ }
67
85
  for (const name in headers) {
68
86
  let value = headers[name];
69
87
  switch (typeof value) {
@@ -182,9 +200,11 @@ function parseHttpProxy(value, ignoreEnv) {
182
200
  }
183
201
  }
184
202
  }
185
- function trimPath(value) {
186
- const length = value.length - 1;
187
- return value[length] === '/' ? value.substring(0, length) : value;
203
+ function trimPath(value, char = '/') {
204
+ while (value.at(-1) === char) {
205
+ value = value.slice(0, -1);
206
+ }
207
+ return value;
188
208
  }
189
209
  function asInt(value) {
190
210
  switch (typeof value) {