codekingpro/portable-devtools
114k
1'use strict';2 3import utils from './../utils.js';4import settle from './../core/settle.js';5import buildFullPath from '../core/buildFullPath.js';6import buildURL from './../helpers/buildURL.js';7import {getProxyForUrl} from 'proxy-from-env';8import http from 'http';9import https from 'https';10import util from 'util';11import followRedirects from 'follow-redirects';12import zlib from 'zlib';13import {VERSION} from '../env/data.js';14import transitionalDefaults from '../defaults/transitional.js';15import AxiosError from '../core/AxiosError.js';16import CanceledError from '../cancel/CanceledError.js';17import platform from '../platform/index.js';18import fromDataURI from '../helpers/fromDataURI.js';19import stream from 'stream';20import AxiosHeaders from '../core/AxiosHeaders.js';21import AxiosTransformStream from '../helpers/AxiosTransformStream.js';22import {EventEmitter} from 'events';23import formDataToStream from "../helpers/formDataToStream.js";24import readBlob from "../helpers/readBlob.js";25import ZlibHeaderTransformStream from '../helpers/ZlibHeaderTransformStream.js';26import callbackify from "../helpers/callbackify.js";27 28const zlibOptions = {29 flush: zlib.constants.Z_SYNC_FLUSH,30 finishFlush: zlib.constants.Z_SYNC_FLUSH31};32 33const brotliOptions = {34 flush: zlib.constants.BROTLI_OPERATION_FLUSH,35 finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH36}37 38const isBrotliSupported = utils.isFunction(zlib.createBrotliDecompress);39 40const {http: httpFollow, https: httpsFollow} = followRedirects;41 42const isHttps = /https:?/;43 44const supportedProtocols = platform.protocols.map(protocol => {45 return protocol + ':';46});47 48/**49 * If the proxy or config beforeRedirects functions are defined, call them with the options50 * object.51 *52 * @param {Object<string, any>} options - The options object that was passed to the request.53 *54 * @returns {Object<string, any>}55 */56function dispatchBeforeRedirect(options, responseDetails) {57 if (options.beforeRedirects.proxy) {58 options.beforeRedirects.proxy(options);59 }60 if (options.beforeRedirects.config) {61 options.beforeRedirects.config(options, responseDetails);62 }63}64 65/**66 * If the proxy or config afterRedirects functions are defined, call them with the options67 *68 * @param {http.ClientRequestArgs} options69 * @param {AxiosProxyConfig} configProxy configuration from Axios options object70 * @param {string} location71 *72 * @returns {http.ClientRequestArgs}73 */74function setProxy(options, configProxy, location) {75 let proxy = configProxy;76 if (!proxy && proxy !== false) {77 const proxyUrl = getProxyForUrl(location);78 if (proxyUrl) {79 proxy = new URL(proxyUrl);80 }81 }82 if (proxy) {83 // Basic proxy authorization84 if (proxy.username) {85 proxy.auth = (proxy.username || '') + ':' + (proxy.password || '');86 }87 88 if (proxy.auth) {89 // Support proxy auth object form90 if (proxy.auth.username || proxy.auth.password) {91 proxy.auth = (proxy.auth.username || '') + ':' + (proxy.auth.password || '');92 }93 const base64 = Buffer94 .from(proxy.auth, 'utf8')95 .toString('base64');96 options.headers['Proxy-Authorization'] = 'Basic ' + base64;97 }98 99 options.headers.host = options.hostname + (options.port ? ':' + options.port : '');100 const proxyHost = proxy.hostname || proxy.host;101 options.hostname = proxyHost;102 // Replace 'host' since options is not a URL object103 options.host = proxyHost;104 options.port = proxy.port;105 options.path = location;106 if (proxy.protocol) {107 options.protocol = proxy.protocol.includes(':') ? proxy.protocol : `${proxy.protocol}:`;108 }109 }110 111 options.beforeRedirects.proxy = function beforeRedirect(redirectOptions) {112 // Configure proxy for redirected request, passing the original config proxy to apply113 // the exact same logic as if the redirected request was performed by axios directly.114 setProxy(redirectOptions, configProxy, redirectOptions.href);115 };116}117 118const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';119 120// temporary hotfix121 122const wrapAsync = (asyncExecutor) => {123 return new Promise((resolve, reject) => {124 let onDone;125 let isDone;126 127 const done = (value, isRejected) => {128 if (isDone) return;129 isDone = true;130 onDone && onDone(value, isRejected);131 }132 133 const _resolve = (value) => {134 done(value);135 resolve(value);136 };137 138 const _reject = (reason) => {139 done(reason, true);140 reject(reason);141 }142 143 asyncExecutor(_resolve, _reject, (onDoneHandler) => (onDone = onDoneHandler)).catch(_reject);144 })145};146 147const resolveFamily = ({address, family}) => {148 if (!utils.isString(address)) {149 throw TypeError('address must be a string');150 }151 return ({152 address,153 family: family || (address.indexOf('.') < 0 ? 6 : 4)154 });155}156 157const buildAddressEntry = (address, family) => resolveFamily(utils.isObject(address) ? address : {address, family});158 159/*eslint consistent-return:0*/160export default isHttpAdapterSupported && function httpAdapter(config) {161 return wrapAsync(async function dispatchHttpRequest(resolve, reject, onDone) {162 let {data, lookup, family} = config;163 const {responseType, responseEncoding} = config;164 const method = config.method.toUpperCase();165 let isDone;166 let rejected = false;167 let req;168 169 if (lookup) {170 const _lookup = callbackify(lookup, (value) => utils.isArray(value) ? value : [value]);171 // hotfix to support opt.all option which is required for node 20.x172 lookup = (hostname, opt, cb) => {173 _lookup(hostname, opt, (err, arg0, arg1) => {174 if (err) {175 return cb(err);176 }177 178 const addresses = utils.isArray(arg0) ? arg0.map(addr => buildAddressEntry(addr)) : [buildAddressEntry(arg0, arg1)];179 180 opt.all ? cb(err, addresses) : cb(err, addresses[0].address, addresses[0].family);181 });182 }183 }184 185 // temporary internal emitter until the AxiosRequest class will be implemented186 const emitter = new EventEmitter();187 188 const onFinished = () => {189 if (config.cancelToken) {190 config.cancelToken.unsubscribe(abort);191 }192 193 if (config.signal) {194 config.signal.removeEventListener('abort', abort);195 }196 197 emitter.removeAllListeners();198 }199 200 onDone((value, isRejected) => {201 isDone = true;202 if (isRejected) {203 rejected = true;204 onFinished();205 }206 });207 208 function abort(reason) {209 emitter.emit('abort', !reason || reason.type ? new CanceledError(null, config, req) : reason);210 }211 212 emitter.once('abort', reject);213 214 if (config.cancelToken || config.signal) {215 config.cancelToken && config.cancelToken.subscribe(abort);216 if (config.signal) {217 config.signal.aborted ? abort() : config.signal.addEventListener('abort', abort);218 }219 }220 221 // Parse url222 const fullPath = buildFullPath(config.baseURL, config.url);223 const parsed = new URL(fullPath, 'http://localhost');224 const protocol = parsed.protocol || supportedProtocols[0];225 226 if (protocol === 'data:') {227 let convertedData;228 229 if (method !== 'GET') {230 return settle(resolve, reject, {231 status: 405,232 statusText: 'method not allowed',233 headers: {},234 config235 });236 }237 238 try {239 convertedData = fromDataURI(config.url, responseType === 'blob', {240 Blob: config.env && config.env.Blob241 });242 } catch (err) {243 throw AxiosError.from(err, AxiosError.ERR_BAD_REQUEST, config);244 }245 246 if (responseType === 'text') {247 convertedData = convertedData.toString(responseEncoding);248 249 if (!responseEncoding || responseEncoding === 'utf8') {250 convertedData = utils.stripBOM(convertedData);251 }252 } else if (responseType === 'stream') {253 convertedData = stream.Readable.from(convertedData);254 }255 256 return settle(resolve, reject, {257 data: convertedData,258 status: 200,259 statusText: 'OK',260 headers: new AxiosHeaders(),261 config262 });263 }264 265 if (supportedProtocols.indexOf(protocol) === -1) {266 return reject(new AxiosError(267 'Unsupported protocol ' + protocol,268 AxiosError.ERR_BAD_REQUEST,269 config270 ));271 }272 273 const headers = AxiosHeaders.from(config.headers).normalize();274 275 // Set User-Agent (required by some servers)276 // See https://github.com/axios/axios/issues/69277 // User-Agent is specified; handle case where no UA header is desired278 // Only set header if it hasn't been set in config279 headers.set('User-Agent', 'axios/' + VERSION, false);280 281 const onDownloadProgress = config.onDownloadProgress;282 const onUploadProgress = config.onUploadProgress;283 const maxRate = config.maxRate;284 let maxUploadRate = undefined;285 let maxDownloadRate = undefined;286 287 // support for spec compliant FormData objects288 if (utils.isSpecCompliantForm(data)) {289 const userBoundary = headers.getContentType(/boundary=([-_\w\d]{10,70})/i);290 291 data = formDataToStream(data, (formHeaders) => {292 headers.set(formHeaders);293 }, {294 tag: `axios-${VERSION}-boundary`,295 boundary: userBoundary && userBoundary[1] || undefined296 });297 // support for https://www.npmjs.com/package/form-data api298 } else if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) {299 headers.set(data.getHeaders());300 301 if (!headers.hasContentLength()) {302 try {303 const knownLength = await util.promisify(data.getLength).call(data);304 Number.isFinite(knownLength) && knownLength >= 0 && headers.setContentLength(knownLength);305 /*eslint no-empty:0*/306 } catch (e) {307 }308 }309 } else if (utils.isBlob(data)) {310 data.size && headers.setContentType(data.type || 'application/octet-stream');311 headers.setContentLength(data.size || 0);312 data = stream.Readable.from(readBlob(data));313 } else if (data && !utils.isStream(data)) {314 if (Buffer.isBuffer(data)) {315 // Nothing to do...316 } else if (utils.isArrayBuffer(data)) {317 data = Buffer.from(new Uint8Array(data));318 } else if (utils.isString(data)) {319 data = Buffer.from(data, 'utf-8');320 } else {321 return reject(new AxiosError(322 'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream',323 AxiosError.ERR_BAD_REQUEST,324 config325 ));326 }327 328 // Add Content-Length header if data exists329 headers.setContentLength(data.length, false);330 331 if (config.maxBodyLength > -1 && data.length > config.maxBodyLength) {332 return reject(new AxiosError(333 'Request body larger than maxBodyLength limit',334 AxiosError.ERR_BAD_REQUEST,335 config336 ));337 }338 }339 340 const contentLength = utils.toFiniteNumber(headers.getContentLength());341 342 if (utils.isArray(maxRate)) {343 maxUploadRate = maxRate[0];344 maxDownloadRate = maxRate[1];345 } else {346 maxUploadRate = maxDownloadRate = maxRate;347 }348 349 if (data && (onUploadProgress || maxUploadRate)) {350 if (!utils.isStream(data)) {351 data = stream.Readable.from(data, {objectMode: false});352 }353 354 data = stream.pipeline([data, new AxiosTransformStream({355 length: contentLength,356 maxRate: utils.toFiniteNumber(maxUploadRate)357 })], utils.noop);358 359 onUploadProgress && data.on('progress', progress => {360 onUploadProgress(Object.assign(progress, {361 upload: true362 }));363 });364 }365 366 // HTTP basic authentication367 let auth = undefined;368 if (config.auth) {369 const username = config.auth.username || '';370 const password = config.auth.password || '';371 auth = username + ':' + password;372 }373 374 if (!auth && parsed.username) {375 const urlUsername = parsed.username;376 const urlPassword = parsed.password;377 auth = urlUsername + ':' + urlPassword;378 }379 380 auth && headers.delete('authorization');381 382 let path;383 384 try {385 path = buildURL(386 parsed.pathname + parsed.search,387 config.params,388 config.paramsSerializer389 ).replace(/^\?/, '');390 } catch (err) {391 const customErr = new Error(err.message);392 customErr.config = config;393 customErr.url = config.url;394 customErr.exists = true;395 return reject(customErr);396 }397 398 headers.set(399 'Accept-Encoding',400 'gzip, compress, deflate' + (isBrotliSupported ? ', br' : ''), false401 );402 403 const options = {404 path,405 method: method,406 headers: headers.toJSON(),407 agents: { http: config.httpAgent, https: config.httpsAgent },408 auth,409 protocol,410 family,411 beforeRedirect: dispatchBeforeRedirect,412 beforeRedirects: {}413 };414 415 // cacheable-lookup integration hotfix416 !utils.isUndefined(lookup) && (options.lookup = lookup);417 418 if (config.socketPath) {419 options.socketPath = config.socketPath;420 } else {421 options.hostname = parsed.hostname;422 options.port = parsed.port;423 setProxy(options, config.proxy, protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path);424 }425 426 let transport;427 const isHttpsRequest = isHttps.test(options.protocol);428 options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;429 if (config.transport) {430 transport = config.transport;431 } else if (config.maxRedirects === 0) {432 transport = isHttpsRequest ? https : http;433 } else {434 if (config.maxRedirects) {435 options.maxRedirects = config.maxRedirects;436 }437 if (config.beforeRedirect) {438 options.beforeRedirects.config = config.beforeRedirect;439 }440 transport = isHttpsRequest ? httpsFollow : httpFollow;441 }442 443 if (config.maxBodyLength > -1) {444 options.maxBodyLength = config.maxBodyLength;445 } else {446 // follow-redirects does not skip comparison, so it should always succeed for axios -1 unlimited447 options.maxBodyLength = Infinity;448 }449 450 if (config.insecureHTTPParser) {451 options.insecureHTTPParser = config.insecureHTTPParser;452 }453 454 // Create the request455 req = transport.request(options, function handleResponse(res) {456 if (req.destroyed) return;457 458 const streams = [res];459 460 const responseLength = +res.headers['content-length'];461 462 if (onDownloadProgress) {463 const transformStream = new AxiosTransformStream({464 length: utils.toFiniteNumber(responseLength),465 maxRate: utils.toFiniteNumber(maxDownloadRate)466 });467 468 onDownloadProgress && transformStream.on('progress', progress => {469 onDownloadProgress(Object.assign(progress, {470 download: true471 }));472 });473 474 streams.push(transformStream);475 }476 477 // decompress the response body transparently if required478 let responseStream = res;479 480 // return the last request in case of redirects481 const lastRequest = res.req || req;482 483 // if decompress disabled we should not decompress484 if (config.decompress !== false && res.headers['content-encoding']) {485 // if no content, but headers still say that it is encoded,486 // remove the header not confuse downstream operations487 if (method === 'HEAD' || res.statusCode === 204) {488 delete res.headers['content-encoding'];489 }490 491 switch ((res.headers['content-encoding'] || '').toLowerCase()) {492 /*eslint default-case:0*/493 case 'gzip':494 case 'x-gzip':495 case 'compress':496 case 'x-compress':497 // add the unzipper to the body stream processing pipeline498 streams.push(zlib.createUnzip(zlibOptions));499 500 // remove the content-encoding in order to not confuse downstream operations501 delete res.headers['content-encoding'];502 break;503 case 'deflate':504 streams.push(new ZlibHeaderTransformStream());505 506 // add the unzipper to the body stream processing pipeline507 streams.push(zlib.createUnzip(zlibOptions));508 509 // remove the content-encoding in order to not confuse downstream operations510 delete res.headers['content-encoding'];511 break;512 case 'br':513 if (isBrotliSupported) {514 streams.push(zlib.createBrotliDecompress(brotliOptions));515 delete res.headers['content-encoding'];516 }517 }518 }519 520 responseStream = streams.length > 1 ? stream.pipeline(streams, utils.noop) : streams[0];521 522 const offListeners = stream.finished(responseStream, () => {523 offListeners();524 onFinished();525 });526 527 const response = {528 status: res.statusCode,529 statusText: res.statusMessage,530 headers: new AxiosHeaders(res.headers),531 config,532 request: lastRequest533 };534 535 if (responseType === 'stream') {536 response.data = responseStream;537 settle(resolve, reject, response);538 } else {539 const responseBuffer = [];540 let totalResponseBytes = 0;541 542 responseStream.on('data', function handleStreamData(chunk) {543 responseBuffer.push(chunk);544 totalResponseBytes += chunk.length;545 546 // make sure the content length is not over the maxContentLength if specified547 if (config.maxContentLength > -1 && totalResponseBytes > config.maxContentLength) {548 // stream.destroy() emit aborted event before calling reject() on Node.js v16549 rejected = true;550 responseStream.destroy();551 reject(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded',552 AxiosError.ERR_BAD_RESPONSE, config, lastRequest));553 }554 });555 556 responseStream.on('aborted', function handlerStreamAborted() {557 if (rejected) {558 return;559 }560 561 const err = new AxiosError(562 'maxContentLength size of ' + config.maxContentLength + ' exceeded',563 AxiosError.ERR_BAD_RESPONSE,564 config,565 lastRequest566 );567 responseStream.destroy(err);568 reject(err);569 });570 571 responseStream.on('error', function handleStreamError(err) {572 if (req.destroyed) return;573 reject(AxiosError.from(err, null, config, lastRequest));574 });575 576 responseStream.on('end', function handleStreamEnd() {577 try {578 let responseData = responseBuffer.length === 1 ? responseBuffer[0] : Buffer.concat(responseBuffer);579 if (responseType !== 'arraybuffer') {580 responseData = responseData.toString(responseEncoding);581 if (!responseEncoding || responseEncoding === 'utf8') {582 responseData = utils.stripBOM(responseData);583 }584 }585 response.data = responseData;586 } catch (err) {587 return reject(AxiosError.from(err, null, config, response.request, response));588 }589 settle(resolve, reject, response);590 });591 }592 593 emitter.once('abort', err => {594 if (!responseStream.destroyed) {595 responseStream.emit('error', err);596 responseStream.destroy();597 }598 });599 });600 601 emitter.once('abort', err => {602 reject(err);603 req.destroy(err);604 });605 606 // Handle errors607 req.on('error', function handleRequestError(err) {608 // @todo remove609 // if (req.aborted && err.code !== AxiosError.ERR_FR_TOO_MANY_REDIRECTS) return;610 reject(AxiosError.from(err, null, config, req));611 });612 613 // set tcp keep alive to prevent drop connection by peer614 req.on('socket', function handleRequestSocket(socket) {615 // default interval of sending ack packet is 1 minute616 socket.setKeepAlive(true, 1000 * 60);617 });618 619 // Handle request timeout620 if (config.timeout) {621 // This is forcing a int timeout to avoid problems if the `req` interface doesn't handle other types.622 const timeout = parseInt(config.timeout, 10);623 624 if (Number.isNaN(timeout)) {625 reject(new AxiosError(626 'error trying to parse `config.timeout` to int',627 AxiosError.ERR_BAD_OPTION_VALUE,628 config,629 req630 ));631 632 return;633 }634 635 // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system.636 // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET.637 // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up.638 // And then these socket which be hang up will devouring CPU little by little.639 // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect.640 req.setTimeout(timeout, function handleRequestTimeout() {641 if (isDone) return;642 let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';643 const transitional = config.transitional || transitionalDefaults;644 if (config.timeoutErrorMessage) {645 timeoutErrorMessage = config.timeoutErrorMessage;646 }647 reject(new AxiosError(648 timeoutErrorMessage,649 transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,650 config,651 req652 ));653 abort();654 });655 }656 657 658 // Send the request659 if (utils.isStream(data)) {660 let ended = false;661 let errored = false;662 663 data.on('end', () => {664 ended = true;665 });666 667 data.once('error', err => {668 errored = true;669 req.destroy(err);670 });671 672 data.on('close', () => {673 if (!ended && !errored) {674 abort(new CanceledError('Request stream has been aborted', config, req));675 }676 });677 678 data.pipe(req);679 } else {680 req.end(data);681 }682 });683}684 685export const __setProxy = setProxy;686 