123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794 |
- /**
- * Copyright 2017 Google Inc. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const EventEmitter = require('events');
- const {helper, assert, debugError} = require('./helper');
- const {Events} = require('./Events');
- class NetworkManager extends EventEmitter {
- /**
- * @param {!Puppeteer.CDPSession} client
- * @param {!Puppeteer.FrameManager} frameManager
- */
- constructor(client, ignoreHTTPSErrors, frameManager) {
- super();
- this._client = client;
- this._ignoreHTTPSErrors = ignoreHTTPSErrors;
- this._frameManager = frameManager;
- /** @type {!Map<string, !Request>} */
- this._requestIdToRequest = new Map();
- /** @type {!Map<string, !Protocol.Network.requestWillBeSentPayload>} */
- this._requestIdToRequestWillBeSentEvent = new Map();
- /** @type {!Object<string, string>} */
- this._extraHTTPHeaders = {};
- this._offline = false;
- /** @type {?{username: string, password: string}} */
- this._credentials = null;
- /** @type {!Set<string>} */
- this._attemptedAuthentications = new Set();
- this._userRequestInterceptionEnabled = false;
- this._protocolRequestInterceptionEnabled = false;
- this._userCacheDisabled = false;
- /** @type {!Map<string, string>} */
- this._requestIdToInterceptionId = new Map();
- this._client.on('Fetch.requestPaused', this._onRequestPaused.bind(this));
- this._client.on('Fetch.authRequired', this._onAuthRequired.bind(this));
- this._client.on('Network.requestWillBeSent', this._onRequestWillBeSent.bind(this));
- this._client.on('Network.requestServedFromCache', this._onRequestServedFromCache.bind(this));
- this._client.on('Network.responseReceived', this._onResponseReceived.bind(this));
- this._client.on('Network.loadingFinished', this._onLoadingFinished.bind(this));
- this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this));
- }
- async initialize() {
- await this._client.send('Network.enable');
- if (this._ignoreHTTPSErrors)
- await this._client.send('Security.setIgnoreCertificateErrors', {ignore: true});
- }
- /**
- * @param {?{username: string, password: string}} credentials
- */
- async authenticate(credentials) {
- this._credentials = credentials;
- await this._updateProtocolRequestInterception();
- }
- /**
- * @param {!Object<string, string>} extraHTTPHeaders
- */
- async setExtraHTTPHeaders(extraHTTPHeaders) {
- this._extraHTTPHeaders = {};
- for (const key of Object.keys(extraHTTPHeaders)) {
- const value = extraHTTPHeaders[key];
- assert(helper.isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
- this._extraHTTPHeaders[key.toLowerCase()] = value;
- }
- await this._client.send('Network.setExtraHTTPHeaders', { headers: this._extraHTTPHeaders });
- }
- /**
- * @return {!Object<string, string>}
- */
- extraHTTPHeaders() {
- return Object.assign({}, this._extraHTTPHeaders);
- }
- /**
- * @param {boolean} value
- */
- async setOfflineMode(value) {
- if (this._offline === value)
- return;
- this._offline = value;
- await this._client.send('Network.emulateNetworkConditions', {
- offline: this._offline,
- // values of 0 remove any active throttling. crbug.com/456324#c9
- latency: 0,
- downloadThroughput: -1,
- uploadThroughput: -1
- });
- }
- /**
- * @param {string} userAgent
- */
- async setUserAgent(userAgent) {
- await this._client.send('Network.setUserAgentOverride', { userAgent });
- }
- /**
- * @param {boolean} enabled
- */
- async setCacheEnabled(enabled) {
- this._userCacheDisabled = !enabled;
- await this._updateProtocolCacheDisabled();
- }
- /**
- * @param {boolean} value
- */
- async setRequestInterception(value) {
- this._userRequestInterceptionEnabled = value;
- await this._updateProtocolRequestInterception();
- }
- async _updateProtocolRequestInterception() {
- const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
- if (enabled === this._protocolRequestInterceptionEnabled)
- return;
- this._protocolRequestInterceptionEnabled = enabled;
- if (enabled) {
- await Promise.all([
- this._updateProtocolCacheDisabled(),
- this._client.send('Fetch.enable', {
- handleAuthRequests: true,
- patterns: [{urlPattern: '*'}],
- }),
- ]);
- } else {
- await Promise.all([
- this._updateProtocolCacheDisabled(),
- this._client.send('Fetch.disable')
- ]);
- }
- }
- async _updateProtocolCacheDisabled() {
- await this._client.send('Network.setCacheDisabled', {
- cacheDisabled: this._userCacheDisabled || this._protocolRequestInterceptionEnabled
- });
- }
- /**
- * @param {!Protocol.Network.requestWillBeSentPayload} event
- */
- _onRequestWillBeSent(event) {
- // Request interception doesn't happen for data URLs with Network Service.
- if (this._protocolRequestInterceptionEnabled && !event.request.url.startsWith('data:')) {
- const requestId = event.requestId;
- const interceptionId = this._requestIdToInterceptionId.get(requestId);
- if (interceptionId) {
- this._onRequest(event, interceptionId);
- this._requestIdToInterceptionId.delete(requestId);
- } else {
- this._requestIdToRequestWillBeSentEvent.set(event.requestId, event);
- }
- return;
- }
- this._onRequest(event, null);
- }
- /**
- * @param {!Protocol.Fetch.authRequiredPayload} event
- */
- _onAuthRequired(event) {
- /** @type {"Default"|"CancelAuth"|"ProvideCredentials"} */
- let response = 'Default';
- if (this._attemptedAuthentications.has(event.requestId)) {
- response = 'CancelAuth';
- } else if (this._credentials) {
- response = 'ProvideCredentials';
- this._attemptedAuthentications.add(event.requestId);
- }
- const {username, password} = this._credentials || {username: undefined, password: undefined};
- this._client.send('Fetch.continueWithAuth', {
- requestId: event.requestId,
- authChallengeResponse: { response, username, password },
- }).catch(debugError);
- }
- /**
- * @param {!Protocol.Fetch.requestPausedPayload} event
- */
- _onRequestPaused(event) {
- if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) {
- this._client.send('Fetch.continueRequest', {
- requestId: event.requestId
- }).catch(debugError);
- }
- const requestId = event.networkId;
- const interceptionId = event.requestId;
- if (requestId && this._requestIdToRequestWillBeSentEvent.has(requestId)) {
- const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(requestId);
- this._onRequest(requestWillBeSentEvent, interceptionId);
- this._requestIdToRequestWillBeSentEvent.delete(requestId);
- } else {
- this._requestIdToInterceptionId.set(requestId, interceptionId);
- }
- }
- /**
- * @param {!Protocol.Network.requestWillBeSentPayload} event
- * @param {?string} interceptionId
- */
- _onRequest(event, interceptionId) {
- let redirectChain = [];
- if (event.redirectResponse) {
- const request = this._requestIdToRequest.get(event.requestId);
- // If we connect late to the target, we could have missed the requestWillBeSent event.
- if (request) {
- this._handleRequestRedirect(request, event.redirectResponse);
- redirectChain = request._redirectChain;
- }
- }
- const frame = event.frameId ? this._frameManager.frame(event.frameId) : null;
- const request = new Request(this._client, frame, interceptionId, this._userRequestInterceptionEnabled, event, redirectChain);
- this._requestIdToRequest.set(event.requestId, request);
- this.emit(Events.NetworkManager.Request, request);
- }
- /**
- * @param {!Protocol.Network.requestServedFromCachePayload} event
- */
- _onRequestServedFromCache(event) {
- const request = this._requestIdToRequest.get(event.requestId);
- if (request)
- request._fromMemoryCache = true;
- }
- /**
- * @param {!Request} request
- * @param {!Protocol.Network.Response} responsePayload
- */
- _handleRequestRedirect(request, responsePayload) {
- const response = new Response(this._client, request, responsePayload);
- request._response = response;
- request._redirectChain.push(request);
- response._bodyLoadedPromiseFulfill.call(null, new Error('Response body is unavailable for redirect responses'));
- this._requestIdToRequest.delete(request._requestId);
- this._attemptedAuthentications.delete(request._interceptionId);
- this.emit(Events.NetworkManager.Response, response);
- this.emit(Events.NetworkManager.RequestFinished, request);
- }
- /**
- * @param {!Protocol.Network.responseReceivedPayload} event
- */
- _onResponseReceived(event) {
- const request = this._requestIdToRequest.get(event.requestId);
- // FileUpload sends a response without a matching request.
- if (!request)
- return;
- const response = new Response(this._client, request, event.response);
- request._response = response;
- this.emit(Events.NetworkManager.Response, response);
- }
- /**
- * @param {!Protocol.Network.loadingFinishedPayload} event
- */
- _onLoadingFinished(event) {
- const request = this._requestIdToRequest.get(event.requestId);
- // For certain requestIds we never receive requestWillBeSent event.
- // @see https://crbug.com/750469
- if (!request)
- return;
- // Under certain conditions we never get the Network.responseReceived
- // event from protocol. @see https://crbug.com/883475
- if (request.response())
- request.response()._bodyLoadedPromiseFulfill.call(null);
- this._requestIdToRequest.delete(request._requestId);
- this._attemptedAuthentications.delete(request._interceptionId);
- this.emit(Events.NetworkManager.RequestFinished, request);
- }
- /**
- * @param {!Protocol.Network.loadingFailedPayload} event
- */
- _onLoadingFailed(event) {
- const request = this._requestIdToRequest.get(event.requestId);
- // For certain requestIds we never receive requestWillBeSent event.
- // @see https://crbug.com/750469
- if (!request)
- return;
- request._failureText = event.errorText;
- const response = request.response();
- if (response)
- response._bodyLoadedPromiseFulfill.call(null);
- this._requestIdToRequest.delete(request._requestId);
- this._attemptedAuthentications.delete(request._interceptionId);
- this.emit(Events.NetworkManager.RequestFailed, request);
- }
- }
- class Request {
- /**
- * @param {!Puppeteer.CDPSession} client
- * @param {?Puppeteer.Frame} frame
- * @param {string} interceptionId
- * @param {boolean} allowInterception
- * @param {!Protocol.Network.requestWillBeSentPayload} event
- * @param {!Array<!Request>} redirectChain
- */
- constructor(client, frame, interceptionId, allowInterception, event, redirectChain) {
- this._client = client;
- this._requestId = event.requestId;
- this._isNavigationRequest = event.requestId === event.loaderId && event.type === 'Document';
- this._interceptionId = interceptionId;
- this._allowInterception = allowInterception;
- this._interceptionHandled = false;
- this._response = null;
- this._failureText = null;
- this._url = event.request.url;
- this._resourceType = event.type.toLowerCase();
- this._method = event.request.method;
- this._postData = event.request.postData;
- this._headers = {};
- this._frame = frame;
- this._redirectChain = redirectChain;
- for (const key of Object.keys(event.request.headers))
- this._headers[key.toLowerCase()] = event.request.headers[key];
- this._fromMemoryCache = false;
- }
- /**
- * @return {string}
- */
- url() {
- return this._url;
- }
- /**
- * @return {string}
- */
- resourceType() {
- return this._resourceType;
- }
- /**
- * @return {string}
- */
- method() {
- return this._method;
- }
- /**
- * @return {string|undefined}
- */
- postData() {
- return this._postData;
- }
- /**
- * @return {!Object}
- */
- headers() {
- return this._headers;
- }
- /**
- * @return {?Response}
- */
- response() {
- return this._response;
- }
- /**
- * @return {?Puppeteer.Frame}
- */
- frame() {
- return this._frame;
- }
- /**
- * @return {boolean}
- */
- isNavigationRequest() {
- return this._isNavigationRequest;
- }
- /**
- * @return {!Array<!Request>}
- */
- redirectChain() {
- return this._redirectChain.slice();
- }
- /**
- * @return {?{errorText: string}}
- */
- failure() {
- if (!this._failureText)
- return null;
- return {
- errorText: this._failureText
- };
- }
- /**
- * @param {!{url?: string, method?:string, postData?: string, headers?: !Object}} overrides
- */
- async continue(overrides = {}) {
- // Request interception is not supported for data: urls.
- if (this._url.startsWith('data:'))
- return;
- assert(this._allowInterception, 'Request Interception is not enabled!');
- assert(!this._interceptionHandled, 'Request is already handled!');
- const {
- url,
- method,
- postData,
- headers
- } = overrides;
- this._interceptionHandled = true;
- await this._client.send('Fetch.continueRequest', {
- requestId: this._interceptionId,
- url,
- method,
- postData,
- headers: headers ? headersArray(headers) : undefined,
- }).catch(error => {
- // In certain cases, protocol will return error if the request was already canceled
- // or the page was closed. We should tolerate these errors.
- debugError(error);
- });
- }
- /**
- * @param {!{status: number, headers: Object, contentType: string, body: (string|Buffer)}} response
- */
- async respond(response) {
- // Mocking responses for dataURL requests is not currently supported.
- if (this._url.startsWith('data:'))
- return;
- assert(this._allowInterception, 'Request Interception is not enabled!');
- assert(!this._interceptionHandled, 'Request is already handled!');
- this._interceptionHandled = true;
- const responseBody = response.body && helper.isString(response.body) ? Buffer.from(/** @type {string} */(response.body)) : /** @type {?Buffer} */(response.body || null);
- /** @type {!Object<string, string>} */
- const responseHeaders = {};
- if (response.headers) {
- for (const header of Object.keys(response.headers))
- responseHeaders[header.toLowerCase()] = response.headers[header];
- }
- if (response.contentType)
- responseHeaders['content-type'] = response.contentType;
- if (responseBody && !('content-length' in responseHeaders))
- responseHeaders['content-length'] = String(Buffer.byteLength(responseBody));
- await this._client.send('Fetch.fulfillRequest', {
- requestId: this._interceptionId,
- responseCode: response.status || 200,
- responsePhrase: STATUS_TEXTS[response.status || 200],
- responseHeaders: headersArray(responseHeaders),
- body: responseBody ? responseBody.toString('base64') : undefined,
- }).catch(error => {
- // In certain cases, protocol will return error if the request was already canceled
- // or the page was closed. We should tolerate these errors.
- debugError(error);
- });
- }
- /**
- * @param {string=} errorCode
- */
- async abort(errorCode = 'failed') {
- // Request interception is not supported for data: urls.
- if (this._url.startsWith('data:'))
- return;
- const errorReason = errorReasons[errorCode];
- assert(errorReason, 'Unknown error code: ' + errorCode);
- assert(this._allowInterception, 'Request Interception is not enabled!');
- assert(!this._interceptionHandled, 'Request is already handled!');
- this._interceptionHandled = true;
- await this._client.send('Fetch.failRequest', {
- requestId: this._interceptionId,
- errorReason
- }).catch(error => {
- // In certain cases, protocol will return error if the request was already canceled
- // or the page was closed. We should tolerate these errors.
- debugError(error);
- });
- }
- }
- const errorReasons = {
- 'aborted': 'Aborted',
- 'accessdenied': 'AccessDenied',
- 'addressunreachable': 'AddressUnreachable',
- 'blockedbyclient': 'BlockedByClient',
- 'blockedbyresponse': 'BlockedByResponse',
- 'connectionaborted': 'ConnectionAborted',
- 'connectionclosed': 'ConnectionClosed',
- 'connectionfailed': 'ConnectionFailed',
- 'connectionrefused': 'ConnectionRefused',
- 'connectionreset': 'ConnectionReset',
- 'internetdisconnected': 'InternetDisconnected',
- 'namenotresolved': 'NameNotResolved',
- 'timedout': 'TimedOut',
- 'failed': 'Failed',
- };
- class Response {
- /**
- * @param {!Puppeteer.CDPSession} client
- * @param {!Request} request
- * @param {!Protocol.Network.Response} responsePayload
- */
- constructor(client, request, responsePayload) {
- this._client = client;
- this._request = request;
- this._contentPromise = null;
- this._bodyLoadedPromise = new Promise(fulfill => {
- this._bodyLoadedPromiseFulfill = fulfill;
- });
- this._remoteAddress = {
- ip: responsePayload.remoteIPAddress,
- port: responsePayload.remotePort,
- };
- this._status = responsePayload.status;
- this._statusText = responsePayload.statusText;
- this._url = request.url();
- this._fromDiskCache = !!responsePayload.fromDiskCache;
- this._fromServiceWorker = !!responsePayload.fromServiceWorker;
- this._headers = {};
- for (const key of Object.keys(responsePayload.headers))
- this._headers[key.toLowerCase()] = responsePayload.headers[key];
- this._securityDetails = responsePayload.securityDetails ? new SecurityDetails(responsePayload.securityDetails) : null;
- }
- /**
- * @return {{ip: string, port: number}}
- */
- remoteAddress() {
- return this._remoteAddress;
- }
- /**
- * @return {string}
- */
- url() {
- return this._url;
- }
- /**
- * @return {boolean}
- */
- ok() {
- return this._status === 0 || (this._status >= 200 && this._status <= 299);
- }
- /**
- * @return {number}
- */
- status() {
- return this._status;
- }
- /**
- * @return {string}
- */
- statusText() {
- return this._statusText;
- }
- /**
- * @return {!Object}
- */
- headers() {
- return this._headers;
- }
- /**
- * @return {?SecurityDetails}
- */
- securityDetails() {
- return this._securityDetails;
- }
- /**
- * @return {!Promise<!Buffer>}
- */
- buffer() {
- if (!this._contentPromise) {
- this._contentPromise = this._bodyLoadedPromise.then(async error => {
- if (error)
- throw error;
- const response = await this._client.send('Network.getResponseBody', {
- requestId: this._request._requestId
- });
- return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
- });
- }
- return this._contentPromise;
- }
- /**
- * @return {!Promise<string>}
- */
- async text() {
- const content = await this.buffer();
- return content.toString('utf8');
- }
- /**
- * @return {!Promise<!Object>}
- */
- async json() {
- const content = await this.text();
- return JSON.parse(content);
- }
- /**
- * @return {!Request}
- */
- request() {
- return this._request;
- }
- /**
- * @return {boolean}
- */
- fromCache() {
- return this._fromDiskCache || this._request._fromMemoryCache;
- }
- /**
- * @return {boolean}
- */
- fromServiceWorker() {
- return this._fromServiceWorker;
- }
- /**
- * @return {?Puppeteer.Frame}
- */
- frame() {
- return this._request.frame();
- }
- }
- class SecurityDetails {
- /**
- * @param {!Protocol.Network.SecurityDetails} securityPayload
- */
- constructor(securityPayload) {
- this._subjectName = securityPayload['subjectName'];
- this._issuer = securityPayload['issuer'];
- this._validFrom = securityPayload['validFrom'];
- this._validTo = securityPayload['validTo'];
- this._protocol = securityPayload['protocol'];
- }
- /**
- * @return {string}
- */
- subjectName() {
- return this._subjectName;
- }
- /**
- * @return {string}
- */
- issuer() {
- return this._issuer;
- }
- /**
- * @return {number}
- */
- validFrom() {
- return this._validFrom;
- }
- /**
- * @return {number}
- */
- validTo() {
- return this._validTo;
- }
- /**
- * @return {string}
- */
- protocol() {
- return this._protocol;
- }
- }
- /**
- * @param {Object<string, string>} headers
- * @return {!Array<{name: string, value: string}>}
- */
- function headersArray(headers) {
- const result = [];
- for (const name in headers) {
- if (!Object.is(headers[name], undefined))
- result.push({name, value: headers[name] + ''});
- }
- return result;
- }
- // List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
- const STATUS_TEXTS = {
- '100': 'Continue',
- '101': 'Switching Protocols',
- '102': 'Processing',
- '103': 'Early Hints',
- '200': 'OK',
- '201': 'Created',
- '202': 'Accepted',
- '203': 'Non-Authoritative Information',
- '204': 'No Content',
- '205': 'Reset Content',
- '206': 'Partial Content',
- '207': 'Multi-Status',
- '208': 'Already Reported',
- '226': 'IM Used',
- '300': 'Multiple Choices',
- '301': 'Moved Permanently',
- '302': 'Found',
- '303': 'See Other',
- '304': 'Not Modified',
- '305': 'Use Proxy',
- '306': 'Switch Proxy',
- '307': 'Temporary Redirect',
- '308': 'Permanent Redirect',
- '400': 'Bad Request',
- '401': 'Unauthorized',
- '402': 'Payment Required',
- '403': 'Forbidden',
- '404': 'Not Found',
- '405': 'Method Not Allowed',
- '406': 'Not Acceptable',
- '407': 'Proxy Authentication Required',
- '408': 'Request Timeout',
- '409': 'Conflict',
- '410': 'Gone',
- '411': 'Length Required',
- '412': 'Precondition Failed',
- '413': 'Payload Too Large',
- '414': 'URI Too Long',
- '415': 'Unsupported Media Type',
- '416': 'Range Not Satisfiable',
- '417': 'Expectation Failed',
- '418': 'I\'m a teapot',
- '421': 'Misdirected Request',
- '422': 'Unprocessable Entity',
- '423': 'Locked',
- '424': 'Failed Dependency',
- '425': 'Too Early',
- '426': 'Upgrade Required',
- '428': 'Precondition Required',
- '429': 'Too Many Requests',
- '431': 'Request Header Fields Too Large',
- '451': 'Unavailable For Legal Reasons',
- '500': 'Internal Server Error',
- '501': 'Not Implemented',
- '502': 'Bad Gateway',
- '503': 'Service Unavailable',
- '504': 'Gateway Timeout',
- '505': 'HTTP Version Not Supported',
- '506': 'Variant Also Negotiates',
- '507': 'Insufficient Storage',
- '508': 'Loop Detected',
- '510': 'Not Extended',
- '511': 'Network Authentication Required',
- };
- module.exports = {Request, Response, NetworkManager, SecurityDetails};
|