"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const http_1 = __importDefault(require("http")); const https_1 = __importDefault(require("https")); const lodash_1 = require("lodash"); const requestAgent = __importStar(require("./agent")); const events_1 = require("events"); const webauth_1 = require("webauth"); const connection_reset_guard_1 = require("../connection-reset-guard"); const messages_1 = require("../../messages"); const logger_1 = __importDefault(require("../../utils/logger")); const requestCache = __importStar(require("../cache")); const http2_1 = require("./http2"); const default_request_timeout_1 = __importDefault(require("./default-request-timeout")); const TUNNELING_SOCKET_ERR_RE = /tunneling socket could not be established/i; const TUNNELING_AUTHORIZE_ERR_RE = /statusCode=407/i; const SOCKET_HANG_UP_ERR_RE = /socket hang up/i; const IS_DNS_ERR_MSG_RE = /ECONNREFUSED|ENOTFOUND|EPROTO/; const IS_DNS_ERR_CODE_RE = /ECONNRESET/; class DestinationRequest extends events_1.EventEmitter { constructor(opts, cache = false) { super(); this.opts = opts; this.cache = cache; this.hasResponse = false; this.credentialsSent = false; this.aborted = false; this.protocolInterface = this.opts.isHttps ? https_1.default : http_1.default; this.timeout = this._getTimeout(); if (this.opts.isHttps) opts.ignoreSSLAuth(); requestAgent.assign(this.opts); this._send(); } _getTimeout() { return this.opts.isAjax ? this.opts.requestTimeout && this.opts.requestTimeout.ajax || default_request_timeout_1.default.ajax : this.opts.requestTimeout && this.opts.requestTimeout.page || default_request_timeout_1.default.page; } static _isHttp2ProtocolError(err) { return err.code === 'ERR_HTTP2_STREAM_ERROR' && (err.message.includes('NGHTTP2_PROTOCOL_ERROR') || err.message.includes('NGHTTP2_HTTP_1_1_REQUIRED')); } _sendRealThroughHttp2(session) { const reqHeaders = (0, http2_1.formatRequestHttp2Headers)(this.opts); const endStream = !this.opts.body.length; const stream = session.request(reqHeaders, { endStream }); stream.setTimeout(this.timeout, () => this._onTimeout()); stream.on('error', (err) => { if (DestinationRequest._isHttp2ProtocolError(err)) { session.destroy(); this._sendReal(); } else this._onError(err); }); stream.on('response', headers => { const http2res = (0, http2_1.createResponseLike)(stream, headers); this._onResponse(http2res); }); if (!endStream) { stream.write(this.opts.body); stream.end(); } this.req = stream; logger_1.default.destination.onHttp2Stream(this.opts.requestId, reqHeaders); } _sendReal(waitForData) { (0, connection_reset_guard_1.connectionResetGuard)(() => { const preparedOpts = this.opts.prepare(); this.req = this.protocolInterface.request(preparedOpts, res => { if (waitForData) { res.on('data', lodash_1.noop); res.once('end', () => this._onResponse(res)); } }); if (logger_1.default.destinationSocket.enabled) { this.req.on('socket', socket => { socket.once('data', data => logger_1.default.destinationSocket.onFirstChunk(this.opts, data)); socket.once('error', err => logger_1.default.destinationSocket.onError(this.opts, err)); }); } if (!waitForData) this.req.on('response', (res) => this._onResponse(res)); this.req.on('upgrade', (res, socket, head) => this._onUpgrade(res, socket, head)); this.req.on('error', (err) => this._onError(err)); this.req.setTimeout(this.timeout, () => this._onTimeout()); this.req.write(this.opts.body); this.req.end(); logger_1.default.destination.onRequest(this.opts); }); } async _send(waitForData) { if (this.cache) { const cachedResponse = requestCache.getResponse(this.opts); if (cachedResponse) { // NOTE: To store async order of the 'response' event setImmediate(() => this._emitOnResponse(cachedResponse.res)); logger_1.default.destination.onCachedRequest(this.opts, cachedResponse.hitCount); return; } } const http2Session = !this.opts.disableHttp2 && this.opts.isHttps && !this.opts.isWebSocket && !this.opts.proxy && await (0, http2_1.getHttp2Session)(this.opts.requestId, this.opts.protocol + '//' + this.opts.host); if (http2Session && !http2Session.closed) this._sendRealThroughHttp2(http2Session); else this._sendReal(waitForData); } _shouldResendWithCredentials(res) { if (res.statusCode === 401 && this.opts.credentials) { const authInfo = (0, webauth_1.getAuthInfo)(res); // NOTE: If we get 401 status code after credentials are sent, we should stop trying to authenticate. if (!authInfo.isChallengeMessage && this.credentialsSent) return false; return authInfo.canAuthorize; } return false; } _onResponse(res) { logger_1.default.destination.onResponse(this.opts, res); if (this._shouldResendWithCredentials(res)) this._resendWithCredentials(res); else if (!this.opts.isHttps && this.opts.proxy && res.statusCode === 407) { logger_1.default.destination.onProxyAuthenticationError(this.opts); this._fatalError(messages_1.MESSAGE.cantAuthorizeToProxy, this.opts.proxy.host); } else this._emitOnResponse(res); } _emitOnResponse(res) { this.hasResponse = true; this.emit('response', res); } _onUpgrade(res, socket, head) { logger_1.default.destination.onUpgradeRequest(this.opts, res); if (head && head.length) socket.unshift(head); this._onResponse(res); } _resendWithCredentials(res) { logger_1.default.destination.onResendWithCredentials(this.opts); (0, webauth_1.addCredentials)(this.opts.credentials, this.opts, res, this.protocolInterface); this.credentialsSent = true; // NOTE: NTLM authentication requires using the same socket for the "negotiate" and "authenticate" requests. // So, before sending the "authenticate" message, we should wait for data from the "challenge" response. It // will mean that the socket is free. this._send((0, webauth_1.requiresResBody)(res)); } _fatalError(msg, url) { if (!this.aborted) { this.aborted = true; this.req.destroy(); this.emit('fatalError', (0, messages_1.getText)(msg, { url: url || this.opts.url })); } } _isDNSErr(err) { return err.message && IS_DNS_ERR_MSG_RE.test(err.message) || !this.aborted && !this.hasResponse && err.code && IS_DNS_ERR_CODE_RE.test(err.code); } _isTunnelingErr(err) { return this.opts.isHttps && err.message && TUNNELING_SOCKET_ERR_RE.test(err.message); } _isSocketHangUpErr(err) { return err.message && SOCKET_HANG_UP_ERR_RE.test(err.message) && // NOTE: At this moment, we determinate the socket hand up error by internal stack trace. // TODO: After what we will change minimal node.js version up to 8 need to rethink this code. err.stack && (err.stack.includes('createHangUpError') || err.stack.includes('connResetException')); } _onTimeout() { logger_1.default.destination.onTimeoutError(this.opts, this.timeout); // NOTE: this handler is also called if we get an error response (for example, 404). So, we should check // for the response presence before raising the timeout error. if (!this.hasResponse) this._fatalError(messages_1.MESSAGE.destRequestTimeout); } _onError(err) { logger_1.default.destination.onError(this.opts, err); if (this._isSocketHangUpErr(err)) this.emit('socketHangUp'); else if (requestAgent.shouldRegressHttps(err, this.opts)) { requestAgent.regressHttps(this.opts); this._send(); } else if (this.opts.proxy && this._isTunnelingErr(err)) { if (TUNNELING_AUTHORIZE_ERR_RE.test(err.message)) this._fatalError(messages_1.MESSAGE.cantAuthorizeToProxy, this.opts.proxy.host); else this._fatalError(messages_1.MESSAGE.cantEstablishTunnelingConnection, this.opts.proxy.host); } else if (this._isDNSErr(err)) { if (!this.opts.isHttps && this.opts.proxy) this._fatalError(messages_1.MESSAGE.cantEstablishProxyConnection, this.opts.proxy.host); else this._fatalError(messages_1.MESSAGE.cantResolveUrl); } else this.emit('error', err); } } exports.default = DestinationRequest;module.exports = exports.default;