UNPKG

cloudworker-proxy

Version:
1,686 lines (1,460 loc) 41.8 kB
import { ReadableStream, TransformStream } from '@mattiasbuelens/web-streams-polyfill/ponyfill/es6'; import Stream, { PassThrough } from 'stream'; import Busboy from 'busboy'; import FormData from 'formdata-node'; import http, { STATUS_CODES } from 'http'; import { format, parse, resolve } from 'url'; import https from 'https'; import zlib from 'zlib'; // Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js // (MIT licensed) const BUFFER = Symbol('buffer'); const TYPE = Symbol('type'); class Blob { constructor() { this[TYPE] = ''; const blobParts = arguments[0]; const options = arguments[1]; const buffers = []; if (blobParts) { const a = blobParts; const length = Number(a.length); for (let i = 0; i < length; i++) { const element = a[i]; let buffer; if (element instanceof Buffer) { buffer = element; } else if (ArrayBuffer.isView(element)) { buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); } else if (element instanceof ArrayBuffer) { buffer = Buffer.from(element); } else if (element instanceof Blob) { buffer = element[BUFFER]; } else { buffer = Buffer.from(typeof element === 'string' ? element : String(element)); } buffers.push(buffer); } } this[BUFFER] = Buffer.concat(buffers); let type = options && options.type !== undefined && String(options.type).toLowerCase(); if (type && !/[^\u0020-\u007E]/.test(type)) { this[TYPE] = type; } } get size() { return this[BUFFER].length; } get type() { return this[TYPE]; } slice() { const size = this.size; const start = arguments[0]; const end = arguments[1]; let relativeStart, relativeEnd; if (start === undefined) { relativeStart = 0; } else if (start < 0) { relativeStart = Math.max(size + start, 0); } else { relativeStart = Math.min(start, size); } if (end === undefined) { relativeEnd = size; } else if (end < 0) { relativeEnd = Math.max(size + end, 0); } else { relativeEnd = Math.min(end, size); } const span = Math.max(relativeEnd - relativeStart, 0); const buffer = this[BUFFER]; const slicedBuffer = buffer.slice(relativeStart, relativeStart + span); const blob = new Blob([], { type: arguments[2] }); blob[BUFFER] = slicedBuffer; return blob; } } Object.defineProperties(Blob.prototype, { size: { enumerable: true }, type: { enumerable: true }, slice: { enumerable: true } }); Object.defineProperty(Blob.prototype, Symbol.toStringTag, { value: 'Blob', writable: false, enumerable: false, configurable: true }); /** * fetch-error.js * * FetchError interface for operational errors */ /** * Create FetchError instance * * @param String message Error message for human * @param String type Error type for machine * @param String systemError For Node.js system error * @return FetchError */ function FetchError(message, type, systemError) { Error.call(this, message); this.message = message; this.type = type; // when err.type is `system`, err.code contains system error code if (systemError) { this.code = this.errno = systemError.code; } // hide custom error implementation details from end-users Error.captureStackTrace(this, this.constructor); } FetchError.prototype = Object.create(Error.prototype); FetchError.prototype.constructor = FetchError; FetchError.prototype.name = 'FetchError'; let convert; try { convert = require('encoding').convert; } catch (e) {} const INTERNALS = Symbol('Body internals'); function getTypeOfBody(body) { if (body == null) { return "null"; } else if (typeof body === 'string') { return "String"; } else if (isURLSearchParams(body)) { return "URLSearchParams"; } else if (body instanceof Blob) { return "Blob"; } else if (Buffer.isBuffer(body)) { return "Buffer"; } else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') { return "ArrayBuffer"; } else if (ArrayBuffer.isView(body)) { return "ArrayBufferView"; } else if (body.toString() === '[object FormData]' || Object.prototype.toString.call(body) === '[object FormData]') { return "FormData"; } else if (body instanceof Stream) { return "Stream"; } else if (body instanceof ReadableStream || // Allow detecting a "ReadableStream" from a different install of web-streams-polyfill body.constructor.name === "ReadableStream" && typeof body.getReader == "function") { return "ReadableStream"; } else { return "other"; } } function readableNodeToWeb(nodeStream, instance) { return new ReadableStream({ start(controller) { nodeStream.pause(); nodeStream.on('data', function (chunk) { // TODO: Should we only do Buffer.from() if chunk is a UInt8Array? // Potentially it makes more sense for down-stream consumers of fetch to cast to Buffer, instead? // if(isUInt8Array(chunk)) { controller.enqueue(new Uint8Array(Buffer.from(chunk))); // HELP WANTED: The node-web-streams package pauses the nodeStream here, however, // if we do that, then it gets permanently paused. Why? // nodeStream.pause(); }); nodeStream.on('end', function () { controller.close(); const pending = controller.byobRequest; if (pending) { pending.respond(0); } }); nodeStream.on('error', function (err) { controller.error(new FetchError(`Invalid response body while trying to fetch ${instance.url}: ${err.message}`, 'system', err)); }); }, pull(controller) { nodeStream.resume(); }, cancel(reason) { nodeStream.pause(); }, type: "bytes" }); } function createReadableStream(instance) { const body = getInstanceBody(instance); const bodyType = getTypeOfBody(body); if (bodyType === "null") { return null; } if (bodyType === 'ReadableStream') { return body.pipeThrough(new TransformStream({ transform(chunk, controller) { // TODO: Should we only do Buffer.from() if chunk is a UInt8Array? // Potentially it makes more sense for down-stream consumers of fetch to cast to Buffer, instead? // if(isUInt8Array(chunk)) { controller.enqueue(new Uint8Array(Buffer.from(chunk))); } })); } if (bodyType === "Stream") { body.pause(); return readableNodeToWeb(body, instance); } const readable = new ReadableStream({ start(controller) { switch (bodyType) { case "String": // body is a string: controller.enqueue(new Uint8Array(Buffer.from(body))); controller.close(); break; case "URLSearchParams": // body is a URLSearchParams controller.enqueue(new Uint8Array(Buffer.from(body.toString()))); controller.close(); break; case "Blob": // body is blob controller.enqueue(new Uint8Array(Buffer.from(body[BUFFER]))); controller.close(); break; case "Buffer": // body is Buffer controller.enqueue(new Uint8Array(Buffer.from(body))); controller.close(); break; case "ArrayBuffer": // body is ArrayBuffer controller.enqueue(new Uint8Array(Buffer.from(body))); controller.close(); break; case "ArrayBufferView": // body is ArrayBufferView controller.enqueue(new Uint8Array(Buffer.from(body.buffer))); controller.close(); break; case "FormData": controller.enqueue(new Uint8Array(Buffer.from(body.toString()))); controller.close(); break; case "other": controller.enqueue(new Uint8Array(Buffer.from(String(body)))); controller.close(); break; default: throw new Error("createReadableStream received an instance body that getTypeOfBody could not understand"); } }, type: "bytes" }); return readable; } /** * Body mixin * * Ref: https://fetch.spec.whatwg.org/#body * * @param Stream body Readable stream * @param Object opts Response options * @return Void */ function Body(body) { var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, _ref$size = _ref.size; let size = _ref$size === undefined ? 0 : _ref$size; var _ref$timeout = _ref.timeout; let timeout = _ref$timeout === undefined ? 0 : _ref$timeout; var _ref$name = _ref.name; let name = _ref$name === undefined ? "Body" : _ref$name; this.size = size; this.timeout = timeout; this[INTERNALS] = { body: body, readableStream: null, disturbed: false, name }; const bodyType = getTypeOfBody(body); if (bodyType !== 'ReadableStream') { this[INTERNALS].readableStream = createReadableStream(this); } else { this[INTERNALS].readableStream = body; } } Body.prototype = { // NOTE: Firefox and Chrome return `undefined` if initial body is undefined, when looking at Request.body, they always return undefined. get body() { return getInstanceReadableStream(this); }, get bodyUsed() { return this[INTERNALS].disturbed; }, /** * Decode response as ArrayBuffer * * @return Promise */ arrayBuffer() { return consumeBody.call(this).then(function (buf) { var ab = new ArrayBuffer(buf.length); var view = new Uint8Array(ab); for (var i = 0; i < buf.length; ++i) { view[i] = buf[i]; } return ab; }); }, /** * Return raw response as Blob * * @return Promise */ blob() { let ct = this.headers && this.headers.get('content-type') || ''; return consumeBody.call(this).then(function (buf) { return Object.assign( // Prevent copying new Blob([], { type: ct.toLowerCase() }), { [BUFFER]: buf }); }); }, /** * Decode response as json * * @return Promise */ json() { var _this = this; return consumeBody.call(this).then(function (buffer) { try { return JSON.parse(buffer.toString()); } catch (err) { return Promise.reject(new FetchError(`invalid json response body at ${_this.url} reason: ${err.message}`, 'invalid-json')); } }); }, /** * Decode response as text * * @return Promise */ text() { return consumeBody.call(this).then(function (buffer) { return buffer.toString(); }); }, /** * Decode response as buffer (non-spec api) * * @return Promise */ buffer() { return consumeBody.call(this); }, /** * Decode response as text, while automatically detecting the encoding and * trying to decode to UTF-8 (non-spec api) * * @return Promise */ textConverted() { var _this2 = this; return consumeBody.call(this).then(function (buffer) { return convertBody(buffer, _this2.headers); }); }, formData() { var _this3 = this; return consumeBody.call(this).then(function (buffer) { return new Promise(function (resolve$$1, reject) { var formdata = new FormData(); var busboy = new Busboy({ headers: { 'content-type': _this3.headers.get('content-type') } }); busboy.on('field', function (fieldname, val) { return formdata.append(fieldname, val); }); busboy.on('file', function (fieldname, file, filename, encoding, mimetype) { let val = ""; file.on('data', function (data) { return val += data; }); file.on('end', function () { return formdata.append(fieldname, val, filename); }); }); busboy.on('finish', function () { return resolve$$1(formdata); }); writeToStream(busboy, _this3); }); }); } }; // In browsers, all properties are enumerable. Object.defineProperties(Body.prototype, { body: { enumerable: true }, bodyUsed: { enumerable: true }, arrayBuffer: { enumerable: true }, blob: { enumerable: true }, json: { enumerable: true }, text: { enumerable: true } }); Body.mixIn = function (proto) { for (const name of Object.getOwnPropertyNames(Body.prototype)) { // istanbul ignore else: future proof if (!(name in proto)) { const desc = Object.getOwnPropertyDescriptor(Body.prototype, name); Object.defineProperty(proto, name, desc); } } }; /** * Consume and convert an entire Body to a Buffer. * * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body * * @return Promise */ function consumeBody() { const instance = this; if (instance[INTERNALS].disturbed) { return Promise.reject(new TypeError(`body used already for: ${instance.url}`)); } instance[INTERNALS].disturbed = true; let resTimeout; const promise = new Promise(function (resolve$$1, reject) { const readable = getInstanceReadableStream(instance); if (readable == null) { return resolve$$1(Buffer.alloc(0)); } const reader = readable.getReader(); let timedOut = false; // allow timeout on slow response body if (instance.timeout) { resTimeout = setTimeout(function () { timedOut = true; reject(new FetchError(`Response timeout while trying to fetch ${instance.url} (over ${instance.timeout}ms)`, 'body-timeout')); }, instance.timeout); } let buffers = []; let totalBytes = 0; function push() { reader.read().then(function (read) { let bufferedData; let chunk; if (timedOut) { return; } if (read.done) { try { bufferedData = Buffer.concat(buffers, totalBytes); } catch (err) { // handle streams that have accumulated too much data (issue #414) reject(new FetchError(`Could not create Buffer from response body for ${instance.url}: ${err.message}`, 'system', err)); return; } resolve$$1(bufferedData); return; } chunk = Buffer.from(read.value); if (instance.size && totalBytes + chunk.length > instance.size) { reject(new FetchError(`content size at ${instance.url} over limit: ${instance.size}`, 'max-size')); return; } buffers.push(chunk); totalBytes += chunk.length; push(); }, function (err) { reject(err); }); } push(); }); promise.then(function () { return resTimeout && clearTimeout(resTimeout); }, function () { return resTimeout && clearTimeout(resTimeout); }); return promise; } /** * Detect buffer encoding and convert to target encoding * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding * * @param Buffer buffer Incoming buffer * @param String encoding Target encoding * @return String */ function convertBody(buffer, headers) { if (typeof convert !== 'function') { throw new Error('The package `encoding` must be installed to use the textConverted() function'); } const ct = headers.get('content-type'); let charset = 'utf-8'; let res, str; // header if (ct) { res = /charset=([^;]*)/i.exec(ct); } // no charset in content type, peek at response body for at most 1024 bytes str = buffer.slice(0, 1024).toString(); // html5 if (!res && str) { res = /<meta.+?charset=(['"])(.+?)\1/i.exec(str); } // html4 if (!res && str) { res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str); if (res) { res = /charset=(.*)/i.exec(res.pop()); } } // xml if (!res && str) { res = /<\?xml.+?encoding=(['"])(.+?)\1/i.exec(str); } // found charset if (res) { charset = res.pop(); // prevent decode issues when sites use incorrect encoding // ref: https://hsivonen.fi/encoding-menu/ if (charset === 'gb2312' || charset === 'gbk') { charset = 'gb18030'; } } // turn raw buffers into a single utf-8 buffer return convert(buffer, 'UTF-8', charset).toString(); } /** * Detect a URLSearchParams object * ref: https://github.com/bitinn/node-fetch/issues/296#issuecomment-307598143 * * @param Object obj Object to detect by type or brand * @return String */ function isURLSearchParams(obj) { // Duck-typing as a necessary condition. if (typeof obj !== 'object' || typeof obj.append !== 'function' || typeof obj.delete !== 'function' || typeof obj.get !== 'function' || typeof obj.getAll !== 'function' || typeof obj.has !== 'function' || typeof obj.set !== 'function') { return false; } // Brand-checking and more duck-typing as optional condition. return obj.constructor.name === 'URLSearchParams' || Object.prototype.toString.call(obj) === '[object URLSearchParams]' || typeof obj.sort === 'function'; } /** * Clone body given Res/Req instance * * @param Mixed instance Response or Request instance * @return Mixed */ function cloneBody(instance) { const body = getInstanceBody(instance); const bodyType = getTypeOfBody(body); // don't allow cloning a used body if (instance.bodyUsed) { throw new Error('cannot clone body after it is used'); } // check that body is a stream and not form-data object // note: we can't clone the form-data object without having it as a dependency if (bodyType === "Stream") { // tee instance body let p1 = new PassThrough(); let p2 = new PassThrough(); // set instance body to teed body and return the other teed body instance[INTERNALS].body = p1; instance[INTERNALS].readableStream = createReadableStream(instance); body.pipe(p1); body.pipe(p2); // body.resume(); return p2; } else if (bodyType === "ReadableStream") { var _body$tee = body.tee(); let p1 = _body$tee[0], p2 = _body$tee[1]; // set instance body to teed body and return the other teed body instance[INTERNALS].body = p1; instance[INTERNALS].readableStream = p1; return p2; } // Note the early returns return body; } /** * Performs the operation "extract a `Content-Type` value from |object|" as * specified in the specification: * https://fetch.spec.whatwg.org/#concept-bodyinit-extract * * This function assumes that instance.body is present. * * @param Mixed instance Response or Request instance */ function extractContentType(instance) { const body = getInstanceBody(instance); const bodyType = getTypeOfBody(body); switch (bodyType) { case "String": case "other": return 'text/plain;charset=UTF-8'; case "URLSearchParams": return 'application/x-www-form-urlencoded;charset=UTF-8'; case "Blob": return body.type || null; case "FormData": return `multipart/form-data;boundary=${body.getBoundary()}`; default: return null; } } /** * The Fetch Standard treats this as if "total bytes" is a property on the body. * For us, we have to explicitly get it with a function. * * ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes * * @param Body instance Instance of Body * @return Number? Number of bytes, or null if not possible */ function getTotalBytes(instance) { const body = getInstanceBody(instance); const bodyType = getTypeOfBody(body); switch (bodyType) { case "null": return 0; case "String": return Buffer.byteLength(body); case "URLSearchParams": case "other": return Buffer.byteLength(String(body)); case "Blob": return body.size; case "Buffer": return body.length; case "ArrayBuffer": case "ArrayBufferView": return body.byteLength; case "FormData": if (body._lengthRetrievers && body._lengthRetrievers.length == 0 || // 1.x body.hasKnownLength && body.hasKnownLength()) { // 2.x return body.getLengthSync(); } default: return null; } } /** * Write a Body to a Node.j (e.g. http.Request) object. * * @param Body instance Instance of Body * @return Void */ function writeToStream(dest, instance) { const body = getInstanceBody(instance); const bodyType = getTypeOfBody(body); switch (bodyType) { case "null": dest.end(); break; case "Stream": body.pipe(dest); break; case "ReadableStream": var _body$tee2 = body.tee(); const out1 = _body$tee2[0], out2 = _body$tee2[1]; const reader = out2.getReader(); function push() { reader.read().then(function (read) { if (read.done) { dest.end(); return; } // TODO: Should we only do Buffer.from() if chunk is a UInt8Array? // if(isUInt8Array(chunk)) { dest.write(Buffer.from(read.value)); push(); }); } instance[INTERNALS].body = out1; push(); break; case "String": dest.write(body); dest.end(); break; // case "URLSearchParams": // dest.write(body.toString()); // dest.end(); // break; case "Blob": dest.write(body[BUFFER]); dest.end(); break; case "Buffer": dest.write(body); dest.end(); break; case "ArrayBuffer": dest.write(Buffer.from(body)); dest.end(); break; case "ArrayBufferView": dest.write(Buffer.from(body.buffer, body.byteOffset, body.byteLength)); dest.end(); break; case "FormData": body.pipe(dest); break; default: dest.write(String(body)); dest.end(); break; } } function getInstanceName(instance) { return instance[INTERNALS].name; } function getInstanceBody(instance) { return instance[INTERNALS].body; } function getInstanceReadableStream(instance) { return instance[INTERNALS].readableStream; } /** * headers.js * * Headers class offers convenient helpers */ const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/; const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/; function validateName(name) { name = `${name}`; if (invalidTokenRegex.test(name)) { throw new TypeError(`${name} is not a legal HTTP header name`); } } function validateValue(value) { value = `${value}`; if (invalidHeaderCharRegex.test(value)) { throw new TypeError(`${value} is not a legal HTTP header value`); } } /** * Find the key in the map object given a header name. * * Returns undefined if not found. * * @param String name Header name * @return String|Undefined */ function find(map, name) { name = name.toLowerCase(); for (const key in map) { if (key.toLowerCase() === name) { return key; } } return undefined; } const MAP = Symbol('map'); class Headers { /** * Headers class * * @param Object headers Response headers * @return Void */ constructor() { let init = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; this[MAP] = Object.create(null); if (init instanceof Headers) { const rawHeaders = init.raw(); const headerNames = Object.keys(rawHeaders); for (const headerName of headerNames) { for (const value of rawHeaders[headerName]) { this.append(headerName, value); } } return; } // We don't worry about converting prop to ByteString here as append() // will handle it. if (init == null) ; else if (typeof init === 'object') { const method = init[Symbol.iterator]; if (method != null) { if (typeof method !== 'function') { throw new TypeError('Header pairs must be iterable'); } // sequence<sequence<ByteString>> // Note: per spec we have to first exhaust the lists then process them const pairs = []; for (const pair of init) { if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') { throw new TypeError('Each header pair must be iterable'); } pairs.push(Array.from(pair)); } for (const pair of pairs) { if (pair.length !== 2) { throw new TypeError('Each header pair must be a name/value tuple'); } this.append(pair[0], pair[1]); } } else { // record<ByteString, ByteString> for (const key of Object.keys(init)) { const value = init[key]; this.append(key, value); } } } else { throw new TypeError('Provided initializer must be an object'); } } /** * Return combined header value given name * * @param String name Header name * @return Mixed */ get(name) { name = `${name}`; validateName(name); const key = find(this[MAP], name); if (key === undefined) { return null; } return this[MAP][key].join(', '); } /** * Iterate over all headers * * @param Function callback Executed for each item with parameters (value, name, thisArg) * @param Boolean thisArg `this` context for callback function * @return Void */ forEach(callback) { let thisArg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; let pairs = getHeaders(this); let i = 0; while (i < pairs.length) { var _pairs$i = pairs[i]; const name = _pairs$i[0], value = _pairs$i[1]; callback.call(thisArg, value, name, this); pairs = getHeaders(this); i++; } } /** * Overwrite header values given name * * @param String name Header name * @param String value Header value * @return Void */ set(name, value) { name = `${name}`; value = `${value}`; validateName(name); validateValue(value); const key = find(this[MAP], name); this[MAP][key !== undefined ? key : name] = [value]; } /** * Append a value onto existing header * * @param String name Header name * @param String value Header value * @return Void */ append(name, value) { name = `${name}`; value = `${value}`; validateName(name); validateValue(value); const key = find(this[MAP], name); if (key !== undefined) { this[MAP][key].push(value); } else { this[MAP][name] = [value]; } } /** * Check for header name existence * * @param String name Header name * @return Boolean */ has(name) { name = `${name}`; validateName(name); return find(this[MAP], name) !== undefined; } /** * Delete all header values given name * * @param String name Header name * @return Void */ delete(name) { name = `${name}`; validateName(name); const key = find(this[MAP], name); if (key !== undefined) { delete this[MAP][key]; } } /** * Return raw headers (non-spec api) * * @return Object */ raw() { return this[MAP]; } /** * Get an iterator on keys. * * @return Iterator */ keys() { return createHeadersIterator(this, 'key'); } /** * Get an iterator on values. * * @return Iterator */ values() { return createHeadersIterator(this, 'value'); } /** * Get an iterator on entries. * * This is the default iterator of the Headers object. * * @return Iterator */ [Symbol.iterator]() { return createHeadersIterator(this, 'key+value'); } } Headers.prototype.entries = Headers.prototype[Symbol.iterator]; Object.defineProperty(Headers.prototype, Symbol.toStringTag, { value: 'Headers', writable: false, enumerable: false, configurable: true }); Object.defineProperties(Headers.prototype, { get: { enumerable: true }, forEach: { enumerable: true }, set: { enumerable: true }, append: { enumerable: true }, has: { enumerable: true }, delete: { enumerable: true }, keys: { enumerable: true }, values: { enumerable: true }, entries: { enumerable: true } }); function getHeaders(headers) { let kind = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'key+value'; const keys = Object.keys(headers[MAP]).sort(); return keys.map(kind === 'key' ? function (k) { return k.toLowerCase(); } : kind === 'value' ? function (k) { return headers[MAP][k].join(', '); } : function (k) { return [k.toLowerCase(), headers[MAP][k].join(', ')]; }); } const INTERNAL = Symbol('internal'); function createHeadersIterator(target, kind) { const iterator = Object.create(HeadersIteratorPrototype); iterator[INTERNAL] = { target, kind, index: 0 }; return iterator; } const HeadersIteratorPrototype = Object.setPrototypeOf({ next() { // istanbul ignore if if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { throw new TypeError('Value of `this` is not a HeadersIterator'); } var _INTERNAL = this[INTERNAL]; const target = _INTERNAL.target, kind = _INTERNAL.kind, index = _INTERNAL.index; const values = getHeaders(target, kind); const len = values.length; if (index >= len) { return { value: undefined, done: true }; } this[INTERNAL].index = index + 1; return { value: values[index], done: false }; } }, Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))); Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { value: 'HeadersIterator', writable: false, enumerable: false, configurable: true }); /** * Export the Headers object in a form that Node.js can consume. * * @param Headers headers * @return Object */ function exportNodeCompatibleHeaders(headers) { const obj = Object.assign({ __proto__: null }, headers[MAP]); // http.request() only supports string as Host header. This hack makes // specifying custom Host header possible. const hostHeaderKey = find(headers[MAP], 'Host'); if (hostHeaderKey !== undefined) { obj[hostHeaderKey] = obj[hostHeaderKey][0]; } return obj; } /** * Create a Headers object from an object of headers, ignoring those that do * not conform to HTTP grammar productions. * * @param Object obj Object of headers * @return Headers */ function createHeadersLenient(obj) { const headers = new Headers(); for (const name of Object.keys(obj)) { if (invalidTokenRegex.test(name)) { continue; } if (Array.isArray(obj[name])) { for (const val of obj[name]) { if (invalidHeaderCharRegex.test(val)) { continue; } if (headers[MAP][name] === undefined) { headers[MAP][name] = [val]; } else { headers[MAP][name].push(val); } } } else if (!invalidHeaderCharRegex.test(obj[name])) { headers[MAP][name] = [obj[name]]; } } return headers; } const INTERNALS$1 = Symbol('Response internals'); /** * Response class * * @param Stream body Readable stream * @param Object opts Response options * @return Void */ class Response { constructor() { let body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; let opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (!opts.name) { opts.name = "Response"; } Body.call(this, body, opts); const status = opts.status || 200; this[INTERNALS$1] = { url: opts.url, status, statusText: opts.statusText || STATUS_CODES[status], headers: new Headers(opts.headers) }; } get url() { return this[INTERNALS$1].url; } get status() { return this[INTERNALS$1].status; } /** * Convenience property representing if the request ended normally */ get ok() { return this[INTERNALS$1].status >= 200 && this[INTERNALS$1].status < 300; } get statusText() { return this[INTERNALS$1].statusText; } get headers() { return this[INTERNALS$1].headers; } /** * Clone this response * * @return Response */ clone() { return new Response(cloneBody(this), { url: this.url, status: this.status, statusText: this.statusText, headers: this.headers, ok: this.ok, name: `cloned(${getInstanceName(this)})` }); } } Body.mixIn(Response.prototype); Object.defineProperties(Response.prototype, { url: { enumerable: true }, status: { enumerable: true }, ok: { enumerable: true }, statusText: { enumerable: true }, headers: { enumerable: true }, clone: { enumerable: true } }); Object.defineProperty(Response.prototype, Symbol.toStringTag, { value: 'Response', writable: false, enumerable: false, configurable: true }); const INTERNALS$2 = Symbol('Request internals'); /** * Check if a value is an instance of Request. * * @param Mixed input * @return Boolean */ function isRequest(input) { return typeof input === 'object' && typeof input[INTERNALS$2] === 'object'; } /** * Request class * * @param Mixed input Url or Request instance * @param Object init Custom options * @return Void */ class Request { constructor(input) { let init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; let parsedURL; // normalize input if (!isRequest(input)) { if (input && input.href) { // in order to support Node.js' Url objects; though WHATWG's URL objects // will fall into this branch also (since their `toString()` will return // `href` property anyway) parsedURL = parse(input.href); } else { // coerce input to a string before attempting to parse parsedURL = parse(`${input}`); } input = {}; } else { parsedURL = parse(input.url); } let method = init.method || input.method || 'GET'; method = method.toUpperCase(); if ((init.body != null || isRequest(input) && getInstanceBody(input) !== null) && (method === 'GET' || method === 'HEAD')) { throw new TypeError('Request with GET/HEAD method cannot have body'); } let inputBody = null; if (init.body != null) { inputBody = init.body; } else if (isRequest(input) && getInstanceBody(input) != null) { inputBody = cloneBody(input); } Body.call(this, inputBody, { timeout: init.timeout || input.timeout || 0, size: init.size || input.size || 0, parent: "Request" }); const headers = new Headers(init.headers || input.headers || {}); if (inputBody !== null) { const contentType = extractContentType(this); if (contentType !== null && !headers.has('Content-Type')) { headers.append('Content-Type', contentType); } } this[INTERNALS$2] = { method, redirect: init.redirect || input.redirect || 'follow', headers, parsedURL }; // node-fetch-only options this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? input.follow : 20; this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? input.compress : true; this.counter = init.counter || input.counter || 0; this.agent = init.agent || input.agent; } get method() { return this[INTERNALS$2].method; } get url() { return format(this[INTERNALS$2].parsedURL); } get headers() { return this[INTERNALS$2].headers; } get redirect() { return this[INTERNALS$2].redirect; } /** * Clone this request * * @return Request */ clone() { return new Request(this); } } Body.mixIn(Request.prototype); Object.defineProperty(Request.prototype, Symbol.toStringTag, { value: 'Request', writable: false, enumerable: false, configurable: true }); Object.defineProperties(Request.prototype, { method: { enumerable: true }, url: { enumerable: true }, headers: { enumerable: true }, redirect: { enumerable: true }, clone: { enumerable: true } }); /** * Convert a Request to Node.js http request options. * * @param Request A Request instance * @return Object The options object to be passed to http.request */ function getNodeRequestOptions(request) { const parsedURL = request[INTERNALS$2].parsedURL; const headers = new Headers(request[INTERNALS$2].headers); const body = getInstanceBody(request); // fetch step 1.3 if (!headers.has('Accept')) { headers.set('Accept', '*/*'); } // Basic fetch if (!parsedURL.protocol || !parsedURL.hostname) { throw new TypeError('Only absolute URLs are supported'); } if (!/^https?:$/.test(parsedURL.protocol)) { throw new TypeError('Only HTTP(S) protocols are supported'); } // HTTP-network-or-cache fetch steps 2.4-2.7 let contentLengthValue = null; if (body == null && /^(POST|PUT)$/i.test(request.method)) { contentLengthValue = '0'; } if (body != null) { const totalBytes = getTotalBytes(request); if (typeof totalBytes === 'number') { contentLengthValue = String(totalBytes); } } if (contentLengthValue) { headers.set('Content-Length', contentLengthValue); } // HTTP-network-or-cache fetch step 2.11 if (!headers.has('User-Agent')) { headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); } // HTTP-network-or-cache fetch step 2.15 if (request.compress) { headers.set('Accept-Encoding', 'gzip,deflate'); } if (!headers.has('Connection') && !request.agent) { headers.set('Connection', 'close'); } // HTTP-network fetch step 4.2 // chunked encoding is handled by Node.js return Object.assign({}, parsedURL, { method: request.method, headers: exportNodeCompatibleHeaders(headers), agent: request.agent }); } /** * Fetch function * * @param Mixed url Absolute url or Request instance * @param Object opts Fetch options * @return Promise */ function fetch(url, opts) { // wrap http.request into fetch return new Promise(function (resolve$$1, reject) { // build request object const request = new Request(url, opts); const options = getNodeRequestOptions(request); const send = (options.protocol === 'https:' ? https : http).request; // send request const req = send(options); let reqTimeout; function finalize() { req.abort(); clearTimeout(reqTimeout); } if (request.timeout) { req.once('socket', function (socket) { reqTimeout = setTimeout(function () { reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); finalize(); }, request.timeout); }); } req.on('error', function (err) { reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); finalize(); }); req.on('response', function (res) { clearTimeout(reqTimeout); const headers = createHeadersLenient(res.headers); // HTTP fetch step 5 if (fetch.isRedirect(res.statusCode)) { // HTTP fetch step 5.2 const location = headers.get('Location'); // HTTP fetch step 5.3 const locationURL = location === null ? null : resolve(request.url, location); // HTTP fetch step 5.5 switch (request.redirect) { case 'error': reject(new FetchError(`redirect mode is set to error: ${request.url}`, 'no-redirect')); finalize(); return; case 'manual': // node-fetch-specific step: make manual redirect a bit easier to use by setting the Location header value to the resolved URL. if (locationURL !== null) { headers.set('Location', locationURL); } break; case 'follow': // HTTP-redirect fetch step 2 if (locationURL === null) { break; } // HTTP-redirect fetch step 5 if (request.counter >= request.follow) { reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect')); finalize(); return; } const requestBody = getInstanceBody(request); // HTTP-redirect fetch step 6 (counter increment) // Create a new Request object. const requestOpts = { headers: new Headers(request.headers), follow: request.follow, counter: request.counter + 1, agent: request.agent, compress: request.compress, method: request.method, body: requestBody }; // HTTP-redirect fetch step 9 if (res.statusCode !== 303 && requestBody && getTotalBytes(request) === null) { reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect')); finalize(); return; } // HTTP-redirect fetch step 11 if (res.statusCode === 303 || (res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST') { requestOpts.method = 'GET'; requestOpts.body = undefined; requestOpts.headers.delete('content-length'); } // HTTP-redirect fetch step 15 resolve$$1(fetch(new Request(locationURL, requestOpts))); finalize(); return; } } // prepare response // const { readable, writable } = new TransformStream(); // const writer = writable.getWriter(); let body = res.pipe(new PassThrough()); // res.on("data", (data) => writer.write(data)); // res.on("end", () => writer.close()); const response_options = { url: request.url, status: res.statusCode, statusText: res.statusMessage, headers: headers, size: request.size, timeout: request.timeout }; // HTTP-network fetch step 12.1.1.3 const codings = headers.get('Content-Encoding'); // HTTP-network fetch step 12.1.1.4: handle content codings // in following scenarios we ignore compression support // 1. compression support is disabled // 2. HEAD request // 3. no Content-Encoding header // 4. no content response (204) // 5. content not modified response (304) if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { resolve$$1(new Response(body, response_options)); return; } // For Node v6+ // Be less strict when decoding compressed responses, since sometimes // servers send slightly invalid responses that are still accepted // by common browsers. // Always using Z_SYNC_FLUSH is what cURL does. const zlibOptions = { flush: zlib.Z_SYNC_FLUSH, finishFlush: zlib.Z_SYNC_FLUSH }; // for gzip if (codings == 'gzip' || codings == 'x-gzip') { body = body.pipe(zlib.createGunzip(zlibOptions)); resolve$$1(new Response(body, response_options)); // for deflate } else if (codings == 'deflate' || codings == 'x-deflate') { // handle the infamous raw deflate response from old servers // a hack for old IIS and Apache servers const raw = res.pipe(new PassThrough()); raw.once('data', function (chunk) { // see http://stackoverflow.com/questions/37519828 if ((chunk[0] & 0x0F) === 0x08) { body = body.pipe(zlib.createInflate()); } else { body = body.pipe(zlib.createInflateRaw()); } resolve$$1(new Response(body, response_options)); }); // otherwise, use response as-is } else { resolve$$1(new Response(body, response_options)); } }); writeToStream(req, request); }); } /** * Redirect code matching * * @param Number code Status code * @return Boolean */ fetch.isRedirect = function (code) { return code === 301 || code === 302 || code === 303 || code === 307 || code === 308; }; export default fetch; export { Headers, Request, Response, FetchError };