1 | import http from 'http';
|
2 | import https from 'https';
|
3 | import zlib from 'zlib';
|
4 | import { URL } from 'url';
|
5 | import Stream, { Transform, PassThrough } from 'stream';
|
6 |
|
7 | const extractContentType = (body) => {
|
8 | if (typeof body === "string") {
|
9 | return "text/plain;charset=UTF-8";
|
10 | }
|
11 | return null;
|
12 | };
|
13 | const isRedirect = (code) => {
|
14 | if (typeof code !== "number")
|
15 | return false;
|
16 | return code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
|
17 | };
|
18 | const isValidTokenChar = (ch) => {
|
19 | if (ch >= 94 && ch <= 122) {
|
20 | return true;
|
21 | }
|
22 | if (ch >= 65 && ch <= 90) {
|
23 | return true;
|
24 | }
|
25 | if (ch === 45) {
|
26 | return true;
|
27 | }
|
28 | if (ch >= 48 && ch <= 57) {
|
29 | return true;
|
30 | }
|
31 | if (ch === 34 || ch === 40 || ch === 41 || ch === 44) {
|
32 | return false;
|
33 | }
|
34 | if (ch >= 33 && ch <= 46) {
|
35 | return true;
|
36 | }
|
37 | if (ch === 124 || ch === 126) {
|
38 | return true;
|
39 | }
|
40 | return false;
|
41 | };
|
42 | const checkIsHttpToken = (val) => {
|
43 | if (typeof val !== "string" || val.length === 0) {
|
44 | return false;
|
45 | }
|
46 | if (!isValidTokenChar(val.charCodeAt(0))) {
|
47 | return false;
|
48 | }
|
49 | const len = val.length;
|
50 | if (len > 1) {
|
51 | if (!isValidTokenChar(val.charCodeAt(1))) {
|
52 | return false;
|
53 | }
|
54 | if (len > 2) {
|
55 | if (!isValidTokenChar(val.charCodeAt(2))) {
|
56 | return false;
|
57 | }
|
58 | if (len > 3) {
|
59 | if (!isValidTokenChar(val.charCodeAt(3))) {
|
60 | return false;
|
61 | }
|
62 | for (let i = 4; i < len; i += 1) {
|
63 | if (!isValidTokenChar(val.charCodeAt(i))) {
|
64 | return false;
|
65 | }
|
66 | }
|
67 | }
|
68 | }
|
69 | }
|
70 | return true;
|
71 | };
|
72 | const checkInvalidHeaderChar = (val) => {
|
73 | if (val.length < 1) {
|
74 | return false;
|
75 | }
|
76 | let c = val.charCodeAt(0);
|
77 | if (c <= 31 && c !== 9 || c > 255 || c === 127) {
|
78 | return true;
|
79 | }
|
80 | if (val.length < 2) {
|
81 | return false;
|
82 | }
|
83 | c = val.charCodeAt(1);
|
84 | if (c <= 31 && c !== 9 || c > 255 || c === 127) {
|
85 | return true;
|
86 | }
|
87 | if (val.length < 3) {
|
88 | return false;
|
89 | }
|
90 | c = val.charCodeAt(2);
|
91 | if (c <= 31 && c !== 9 || c > 255 || c === 127) {
|
92 | return true;
|
93 | }
|
94 | for (let i = 3; i < val.length; i += 1) {
|
95 | c = val.charCodeAt(i);
|
96 | if (c <= 31 && c !== 9 || c > 255 || c === 127) {
|
97 | return true;
|
98 | }
|
99 | }
|
100 | return false;
|
101 | };
|
102 |
|
103 | const sanitizeKey = (name) => {
|
104 | if (!checkIsHttpToken(name)) {
|
105 | throw new TypeError(`${name} is not a legal HTTP header name`);
|
106 | }
|
107 | return name.toLowerCase();
|
108 | };
|
109 | const sanitizeValue = (value) => {
|
110 | if (checkInvalidHeaderChar(value)) {
|
111 | throw new TypeError(`${value} is not a legal HTTP header value`);
|
112 | }
|
113 | return value;
|
114 | };
|
115 | class Headers {
|
116 | constructor(init = {}) {
|
117 | this.raw = () => {
|
118 | const result = {};
|
119 | for (const [key, value] of this.map.entries()) {
|
120 | result[key] = value;
|
121 | }
|
122 | return result;
|
123 | };
|
124 | this.append = (key, value) => {
|
125 | const prev = this.map.get(key);
|
126 | if (!prev) {
|
127 | this.set(key, value);
|
128 | } else {
|
129 | prev.push(sanitizeValue(value));
|
130 | }
|
131 | };
|
132 | this.get = (key) => {
|
133 | const value = this.map.get(sanitizeKey(key));
|
134 | if ((value == null ? void 0 : value.length) === 1) {
|
135 | return value[0];
|
136 | }
|
137 | return null;
|
138 | };
|
139 | this.has = (key) => this.map.has(sanitizeKey(key));
|
140 | this.set = (key, value) => {
|
141 | const data = Array.isArray(value) ? value.map(sanitizeValue) : [sanitizeValue(value)];
|
142 | this.map.set(sanitizeKey(key), data);
|
143 | };
|
144 | this.delete = (key) => {
|
145 | this.map.delete(sanitizeKey(key));
|
146 | };
|
147 | this.map = new Map();
|
148 | for (const [key, value] of Object.entries(init)) {
|
149 | if (value) {
|
150 | this.set(key, value);
|
151 | }
|
152 | }
|
153 | }
|
154 | }
|
155 |
|
156 | class BlobImpl {
|
157 | constructor(blobParts, options) {
|
158 | this.privateType = "";
|
159 | const buffers = [];
|
160 | if (blobParts) {
|
161 | if (!blobParts || typeof blobParts !== "object" || blobParts instanceof Date || blobParts instanceof RegExp) {
|
162 | throw new TypeError("Blob parts must be objects that are not Dates or RegExps");
|
163 | }
|
164 | for (let i = 0, l = Number(blobParts.length); i < l; i += 1) {
|
165 | const part = blobParts[i];
|
166 | let buf;
|
167 | if (part instanceof Buffer) {
|
168 | buf = part;
|
169 | } else if (part instanceof ArrayBuffer) {
|
170 | buf = Buffer.from(new Uint8Array(part));
|
171 | } else if (part instanceof BlobImpl) {
|
172 | buf = part.buffer;
|
173 | } else if (ArrayBuffer.isView(part)) {
|
174 | buf = Buffer.from(new Uint8Array(part.buffer, part.byteOffset, part.byteLength));
|
175 | } else {
|
176 | buf = Buffer.from(typeof part === "string" ? part : String(part));
|
177 | }
|
178 | buffers.push(buf);
|
179 | }
|
180 | }
|
181 | this.buffer = Buffer.concat(buffers);
|
182 | this.closed = false;
|
183 | const type = options && options.type !== void 0 && String(options.type).toLowerCase();
|
184 | if (type && !/[^\u0020-\u007E]/.test(type)) {
|
185 | this.privateType = type;
|
186 | }
|
187 | }
|
188 | get size() {
|
189 | return this.buffer.length;
|
190 | }
|
191 | get type() {
|
192 | return this.privateType;
|
193 | }
|
194 | get content() {
|
195 | return this.buffer;
|
196 | }
|
197 | get isClosed() {
|
198 | return this.closed;
|
199 | }
|
200 | slice(start, end, type) {
|
201 | const { size, buffer } = this;
|
202 | let relativeStart;
|
203 | let relativeEnd;
|
204 | if (start === void 0) {
|
205 | relativeStart = 0;
|
206 | } else if (start < 0) {
|
207 | relativeStart = Math.max(size + start, 0);
|
208 | } else {
|
209 | relativeStart = Math.min(start, size);
|
210 | }
|
211 | if (end === void 0) {
|
212 | relativeEnd = size;
|
213 | } else if (end < 0) {
|
214 | relativeEnd = Math.max(size + end, 0);
|
215 | } else {
|
216 | relativeEnd = Math.min(end, size);
|
217 | }
|
218 | const span = Math.max(relativeEnd - relativeStart, 0);
|
219 | const slicedBuffer = buffer.slice(relativeStart, relativeStart + span);
|
220 | const blob = new BlobImpl([], { type: type || this.type });
|
221 | blob.buffer = slicedBuffer;
|
222 | blob.closed = this.closed;
|
223 | return blob;
|
224 | }
|
225 | close() {
|
226 | this.closed = true;
|
227 | }
|
228 | toString() {
|
229 | return "[object Blob]";
|
230 | }
|
231 | }
|
232 |
|
233 | class ProgressCallbackTransform extends Transform {
|
234 | constructor(total, onProgress) {
|
235 | super();
|
236 | this.start = Date.now();
|
237 | this.transferred = 0;
|
238 | this.delta = 0;
|
239 | this.nextUpdate = this.start + 1e3;
|
240 | this.total = total;
|
241 | this.onProgress = onProgress;
|
242 | }
|
243 | _transform(chunk, encoding, callback) {
|
244 | this.transferred += chunk.length;
|
245 | this.delta += chunk.length;
|
246 | const now = Date.now();
|
247 | if (now >= this.nextUpdate && this.transferred !== this.total) {
|
248 | this.nextUpdate = now + 1e3;
|
249 | this.onProgress({
|
250 | total: this.total,
|
251 | delta: this.delta,
|
252 | transferred: this.transferred,
|
253 | percent: this.transferred / this.total * 100,
|
254 | bytesPerSecond: Math.round(this.transferred / ((now - this.start) / 1e3))
|
255 | });
|
256 | this.delta = 0;
|
257 | }
|
258 | callback(null, chunk);
|
259 | }
|
260 | _flush(callback) {
|
261 | this.onProgress({
|
262 | total: this.total,
|
263 | delta: this.delta,
|
264 | transferred: this.total,
|
265 | percent: 100,
|
266 | bytesPerSecond: Math.round(this.transferred / ((Date.now() - this.start) / 1e3))
|
267 | });
|
268 | this.delta = 0;
|
269 | callback(null);
|
270 | }
|
271 | }
|
272 |
|
273 | var HEADER_MAP;
|
274 | (function(HEADER_MAP2) {
|
275 | HEADER_MAP2["CONTENT_LENGTH"] = "Content-Length";
|
276 | HEADER_MAP2["ACCEPT_ENCODING"] = "Accept-Encoding";
|
277 | HEADER_MAP2["ACCEPT"] = "Accept";
|
278 | HEADER_MAP2["CONNECTION"] = "Connection";
|
279 | HEADER_MAP2["CONTENT_TYPE"] = "Content-Type";
|
280 | HEADER_MAP2["LOCATION"] = "Location";
|
281 | HEADER_MAP2["CONTENT_ENCODING"] = "Content-Encoding";
|
282 | })(HEADER_MAP || (HEADER_MAP = {}));
|
283 | var METHOD_MAP;
|
284 | (function(METHOD_MAP2) {
|
285 | METHOD_MAP2["GET"] = "GET";
|
286 | METHOD_MAP2["POST"] = "POST";
|
287 | METHOD_MAP2["HEAD"] = "HEAD";
|
288 | })(METHOD_MAP || (METHOD_MAP = {}));
|
289 |
|
290 | class ResponseImpl {
|
291 | constructor(body, options) {
|
292 | this.consumeResponse = () => {
|
293 | if (this.consumed) {
|
294 | return Promise.reject(new Error(`Response used already for: ${this.requestURL}`));
|
295 | }
|
296 | this.consumed = true;
|
297 | if (this.body === null) {
|
298 | return Promise.resolve(Buffer.alloc(0));
|
299 | }
|
300 | if (typeof this.body === "string") {
|
301 | return Promise.resolve(Buffer.from(this.body));
|
302 | }
|
303 | if (this.body instanceof BlobImpl) {
|
304 | return Promise.resolve(this.body.content);
|
305 | }
|
306 | if (Buffer.isBuffer(this.body)) {
|
307 | return Promise.resolve(this.body);
|
308 | }
|
309 | if (!(this.body instanceof Stream)) {
|
310 | return Promise.resolve(Buffer.alloc(0));
|
311 | }
|
312 | const accum = [];
|
313 | let accumBytes = 0;
|
314 | let abort = false;
|
315 | return new Promise((resolve, reject) => {
|
316 | let resTimeout;
|
317 | if (this.timeout) {
|
318 | resTimeout = setTimeout(() => {
|
319 | abort = true;
|
320 | reject(new Error(`Response timeout while trying to fetch ${this.requestURL} (over ${this.timeout}ms)`));
|
321 | this.body.emit("cancel-request");
|
322 | }, this.timeout);
|
323 | }
|
324 | this.body.on("error", (err) => {
|
325 | reject(new Error(`Invalid response body while trying to fetch ${this.requestURL}: ${err.message}`));
|
326 | });
|
327 | this.body.on("data", (chunk) => {
|
328 | if (abort || chunk === null) {
|
329 | return;
|
330 | }
|
331 | if (this.size && accumBytes + chunk.length > this.size) {
|
332 | abort = true;
|
333 | reject(new Error(`content size at ${this.requestURL} over limit: ${this.size}`));
|
334 | this.body.emit("cancel-request");
|
335 | return;
|
336 | }
|
337 | accumBytes += chunk.length;
|
338 | accum.push(chunk);
|
339 | });
|
340 | this.body.on("end", () => {
|
341 | if (abort) {
|
342 | return;
|
343 | }
|
344 | clearTimeout(resTimeout);
|
345 | resolve(Buffer.concat(accum));
|
346 | });
|
347 | });
|
348 | };
|
349 | this.download = (dest, onProgress) => {
|
350 | return new Promise((resolve, reject) => {
|
351 | const feedStreams = [dest];
|
352 | if (typeof onProgress === "function") {
|
353 | const contentLength = Number(this.headers.get(HEADER_MAP.CONTENT_LENGTH));
|
354 | const progressStream = new ProgressCallbackTransform(contentLength, onProgress);
|
355 | feedStreams.unshift(progressStream);
|
356 | }
|
357 | dest.once("finish", () => {
|
358 | dest.close();
|
359 | resolve();
|
360 | });
|
361 | dest.on("error", (error) => {
|
362 | reject(error);
|
363 | });
|
364 | let lastStream = this.stream;
|
365 | for (const stream of feedStreams) {
|
366 | lastStream = lastStream.pipe(stream);
|
367 | }
|
368 | });
|
369 | };
|
370 | this.arrayBuffer = async () => {
|
371 | const buf = await this.consumeResponse();
|
372 | return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
373 | };
|
374 | this.blob = async () => {
|
375 | const contentType = this.headers && this.headers.get(HEADER_MAP.CONTENT_TYPE) || "";
|
376 | const buffer = await this.consumeResponse();
|
377 | const blob = new BlobImpl([buffer], {
|
378 | type: contentType.toLowerCase()
|
379 | });
|
380 | return blob;
|
381 | };
|
382 | this.text = async () => {
|
383 | const buffer = await this.consumeResponse();
|
384 | return buffer.toString();
|
385 | };
|
386 | this.json = async () => {
|
387 | const buffer = await this.consumeResponse();
|
388 | let result;
|
389 | try {
|
390 | result = JSON.parse(buffer.toString());
|
391 | } catch (e) {
|
392 | result = buffer.toString();
|
393 | }
|
394 | return result;
|
395 | };
|
396 | this.buffer = () => {
|
397 | return this.consumeResponse();
|
398 | };
|
399 | const { statusCode = 200, requestURL, headers, timeout, size } = options;
|
400 | this.statusCode = statusCode;
|
401 | this.body = body;
|
402 | this.requestURL = requestURL;
|
403 | this.headers = headers;
|
404 | this.timeout = timeout;
|
405 | this.size = size;
|
406 | this.consumed = false;
|
407 | }
|
408 | get ok() {
|
409 | return this.statusCode >= 200 && this.statusCode < 300;
|
410 | }
|
411 | get stream() {
|
412 | if (this.consumed) {
|
413 | throw new Error(`Response used already for: ${this.requestURL}`);
|
414 | }
|
415 | this.consumed = true;
|
416 | return this.body;
|
417 | }
|
418 | }
|
419 |
|
420 | const DEFAULT_OPTIONS = {
|
421 | method: "GET",
|
422 | body: null,
|
423 | followRedirect: true,
|
424 | maxRedirectCount: 20,
|
425 | timeout: 0,
|
426 | size: 0,
|
427 | redirectCount: 0
|
428 | };
|
429 | const SUPPORTED_COMPRESSIONS = ["gzip", "deflate"];
|
430 |
|
431 | const adapterForHttp = (protocol) => {
|
432 | if (protocol === "http:") {
|
433 | return http;
|
434 | }
|
435 | if (protocol === "https:") {
|
436 | return https;
|
437 | }
|
438 | throw new TypeError("Only HTTP(S) protocols are supported");
|
439 | };
|
440 | const getRequestOptions = (constructorOptions) => {
|
441 | const options = { ...DEFAULT_OPTIONS, ...constructorOptions };
|
442 | const { method, body, requestURL, query, headers: headerOptions } = options;
|
443 | if (body !== null && (method === METHOD_MAP.GET || method === METHOD_MAP.HEAD)) {
|
444 | throw new TypeError("Request with GET/HEAD method cannot have body");
|
445 | }
|
446 | const parsedURL = new URL(requestURL);
|
447 | if (!parsedURL.protocol || !parsedURL.hostname) {
|
448 | throw new TypeError("Only absolute URLs are supported");
|
449 | }
|
450 | if (!/^https?:$/.test(parsedURL.protocol)) {
|
451 | throw new TypeError("Only HTTP(S) protocols are supported");
|
452 | }
|
453 | if (query) {
|
454 | for (const [queryKey, queryValue] of Object.entries(query)) {
|
455 | parsedURL.searchParams.append(queryKey, queryValue);
|
456 | }
|
457 | }
|
458 | const headers = new Headers(headerOptions);
|
459 | headers.delete(HEADER_MAP.CONTENT_LENGTH);
|
460 | headers.set(HEADER_MAP.ACCEPT_ENCODING, SUPPORTED_COMPRESSIONS.join(", "));
|
461 | if (!headers.has(HEADER_MAP.ACCEPT)) {
|
462 | headers.set(HEADER_MAP.ACCEPT, "*/*");
|
463 | }
|
464 | if (!headers.has(HEADER_MAP.CONNECTION)) {
|
465 | headers.set(HEADER_MAP.CONNECTION, "close");
|
466 | }
|
467 | if (body && !headers.has(HEADER_MAP.CONTENT_TYPE)) {
|
468 | const contentType = extractContentType(body);
|
469 | if (contentType) {
|
470 | headers.append(HEADER_MAP.CONTENT_TYPE, contentType);
|
471 | }
|
472 | }
|
473 | return {
|
474 | ...options,
|
475 | method: method.toUpperCase(),
|
476 | parsedURL,
|
477 | headers
|
478 | };
|
479 | };
|
480 | class Request {
|
481 | constructor(constructorOptions) {
|
482 | this.timeoutId = null;
|
483 | this.clientRequest = null;
|
484 | this.clearRequestTimeout = () => {
|
485 | if (this.timeoutId === null)
|
486 | return;
|
487 | clearTimeout(this.timeoutId);
|
488 | this.timeoutId = null;
|
489 | };
|
490 | this.createClientRequest = async () => {
|
491 | const {
|
492 | parsedURL: { protocol, host, hostname, port, pathname, search },
|
493 | headers,
|
494 | method
|
495 | } = this.options;
|
496 | const clientRequest = adapterForHttp(protocol).request({
|
497 | protocol,
|
498 | host,
|
499 | hostname,
|
500 | port,
|
501 | path: `${pathname}${search || ""}`,
|
502 | headers: headers.raw(),
|
503 | method
|
504 | });
|
505 | this.clientRequest = clientRequest;
|
506 | };
|
507 | this.cancelClientRequest = () => {
|
508 | if (!this.clientRequest)
|
509 | return;
|
510 | this.clientRequest.destroy();
|
511 | };
|
512 | this.send = async () => {
|
513 | await this.createClientRequest();
|
514 | return await new Promise((resolve, reject) => {
|
515 | if (this.clientRequest) {
|
516 | const {
|
517 | method,
|
518 | body: requestBody,
|
519 | followRedirect,
|
520 | redirectCount,
|
521 | maxRedirectCount,
|
522 | requestURL,
|
523 | parsedURL,
|
524 | size,
|
525 | timeout
|
526 | } = this.options;
|
527 | this.clientRequest.on("error", (error) => {
|
528 | this.clearRequestTimeout();
|
529 | reject(error);
|
530 | });
|
531 | this.clientRequest.on("abort", () => {
|
532 | this.clearRequestTimeout();
|
533 | reject(new Error("request was aborted by the server"));
|
534 | });
|
535 | this.clientRequest.on("response", (res) => {
|
536 | this.clearRequestTimeout();
|
537 | const headers = new Headers(res.headers);
|
538 | const { statusCode = 200 } = res;
|
539 | if (isRedirect(statusCode) && followRedirect) {
|
540 | if (maxRedirectCount && redirectCount >= maxRedirectCount) {
|
541 | reject(new Error(`maximum redirect reached at: ${requestURL}`));
|
542 | }
|
543 | if (!headers.get(HEADER_MAP.LOCATION)) {
|
544 | reject(new Error(`redirect location header missing at: ${requestURL}`));
|
545 | }
|
546 | if (statusCode === 303 || (statusCode === 301 || statusCode === 302) && method === METHOD_MAP.POST) {
|
547 | this.options.method = METHOD_MAP.GET;
|
548 | this.options.body = null;
|
549 | this.options.headers.delete(HEADER_MAP.CONTENT_LENGTH);
|
550 | }
|
551 | this.options.redirectCount += 1;
|
552 | this.options.parsedURL = new URL(String(headers.get(HEADER_MAP.LOCATION)), parsedURL.toString());
|
553 | resolve(this.createClientRequest().then(this.send));
|
554 | }
|
555 | let responseBody = new PassThrough();
|
556 | res.on("error", (error) => responseBody.emit("error", error));
|
557 | responseBody.on("error", this.cancelClientRequest);
|
558 | responseBody.on("cancel-request", this.cancelClientRequest);
|
559 | res.pipe(responseBody);
|
560 | const responseOptions = {
|
561 | requestURL,
|
562 | statusCode,
|
563 | headers,
|
564 | size,
|
565 | timeout
|
566 | };
|
567 | const resolveResponse = (body) => {
|
568 | const response = new ResponseImpl(body, responseOptions);
|
569 | resolve(response);
|
570 | };
|
571 | const codings = headers.get(HEADER_MAP.CONTENT_ENCODING);
|
572 | if (method !== METHOD_MAP.HEAD && codings !== null && statusCode !== 204 && statusCode !== 304) {
|
573 | if (codings === "gzip" || codings === "x-gzip") {
|
574 | responseBody = responseBody.pipe(zlib.createGunzip());
|
575 | } else if (codings === "deflate" || codings === "x-deflate") {
|
576 | const raw = res.pipe(new PassThrough());
|
577 | raw.once("data", (chunk) => {
|
578 | if ((chunk[0] & 15) === 8) {
|
579 | responseBody = responseBody.pipe(zlib.createInflate());
|
580 | } else {
|
581 | responseBody = responseBody.pipe(zlib.createInflateRaw());
|
582 | }
|
583 | resolveResponse(responseBody);
|
584 | });
|
585 | return;
|
586 | }
|
587 | }
|
588 | resolveResponse(responseBody);
|
589 | });
|
590 | if (requestBody) {
|
591 | this.clientRequest.write(requestBody);
|
592 | }
|
593 | this.clientRequest.end();
|
594 | }
|
595 | });
|
596 | };
|
597 | this.options = getRequestOptions(constructorOptions);
|
598 | }
|
599 | }
|
600 |
|
601 | const main = (requestURL, options = {}) => {
|
602 | const request = new Request({ requestURL, ...options });
|
603 | return request.send();
|
604 | };
|
605 |
|
606 | export default main;
|