UNPKG

18.3 kBJavaScriptView Raw
1import http from 'http';
2import https from 'https';
3import zlib from 'zlib';
4import { URL } from 'url';
5import Stream, { Transform, PassThrough } from 'stream';
6
7const extractContentType = (body) => {
8 if (typeof body === "string") {
9 return "text/plain;charset=UTF-8";
10 }
11 return null;
12};
13const isRedirect = (code) => {
14 if (typeof code !== "number")
15 return false;
16 return code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
17};
18const 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};
42const 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};
72const 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
103const sanitizeKey = (name) => {
104 if (!checkIsHttpToken(name)) {
105 throw new TypeError(`${name} is not a legal HTTP header name`);
106 }
107 return name.toLowerCase();
108};
109const sanitizeValue = (value) => {
110 if (checkInvalidHeaderChar(value)) {
111 throw new TypeError(`${value} is not a legal HTTP header value`);
112 }
113 return value;
114};
115class 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
156class 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
233class 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
273var 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 = {}));
283var 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
290class 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
420const DEFAULT_OPTIONS = {
421 method: "GET",
422 body: null,
423 followRedirect: true,
424 maxRedirectCount: 20,
425 timeout: 0,
426 size: 0,
427 redirectCount: 0
428};
429const SUPPORTED_COMPRESSIONS = ["gzip", "deflate"];
430
431const 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};
440const 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};
480class 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
601const main = (requestURL, options = {}) => {
602 const request = new Request({ requestURL, ...options });
603 return request.send();
604};
605
606export default main;