| /* |
| * Copyright (C) 2011 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| import * as Common from '../common/common.js'; |
| import * as Host from '../host/host.js'; |
| import * as Platform from '../platform/platform.js'; |
| import * as TextUtils from '../text_utils/text_utils.js'; // eslint-disable-line no-unused-vars |
| |
| import {Cookie} from './Cookie.js'; |
| import {BlockedCookieWithReason, ContentData, Events as NetworkRequestEvents, ExtraRequestInfo, ExtraResponseInfo, MIME_TYPE, MIME_TYPE_TO_RESOURCE_TYPE, NameValue, NetworkRequest} from './NetworkRequest.js'; // eslint-disable-line no-unused-vars |
| import {Capability, SDKModel, SDKModelObserver, Target, TargetManager} from './SDKModel.js'; // eslint-disable-line no-unused-vars |
| |
| /** @type {!WeakMap<!NetworkRequest, !NetworkManager>} */ |
| const requestToManagerMap = new WeakMap(); |
| |
| const CONNECTION_TYPES = new Map([ |
| ['2g', Protocol.Network.ConnectionType.Cellular2g], |
| ['3g', Protocol.Network.ConnectionType.Cellular3g], |
| ['4g', Protocol.Network.ConnectionType.Cellular4g], |
| ['bluetooth', Protocol.Network.ConnectionType.Bluetooth], |
| ['wifi', Protocol.Network.ConnectionType.Wifi], |
| ['wimax', Protocol.Network.ConnectionType.Wimax], |
| ]); |
| |
| |
| export class NetworkManager extends SDKModel { |
| /** |
| * @param {!Target} target |
| */ |
| constructor(target) { |
| super(target); |
| this._dispatcher = new NetworkDispatcher(this); |
| this._networkAgent = target.networkAgent(); |
| target.registerNetworkDispatcher(this._dispatcher); |
| if (Common.Settings.Settings.instance().moduleSetting('cacheDisabled').get()) { |
| this._networkAgent.invoke_setCacheDisabled({cacheDisabled: true}); |
| } |
| |
| this._networkAgent.invoke_enable({maxPostDataSize: MAX_EAGER_POST_REQUEST_BODY_LENGTH}); |
| |
| this._bypassServiceWorkerSetting = Common.Settings.Settings.instance().createSetting('bypassServiceWorker', false); |
| if (this._bypassServiceWorkerSetting.get()) { |
| this._bypassServiceWorkerChanged(); |
| } |
| this._bypassServiceWorkerSetting.addChangeListener(this._bypassServiceWorkerChanged, this); |
| |
| Common.Settings.Settings.instance() |
| .moduleSetting('cacheDisabled') |
| .addChangeListener(this._cacheDisabledSettingChanged, this); |
| } |
| |
| /** |
| * @param {!NetworkRequest} request |
| * @return {?NetworkManager} |
| */ |
| static forRequest(request) { |
| return requestToManagerMap.get(request) || null; |
| } |
| |
| /** |
| * @param {!NetworkRequest} request |
| * @return {boolean} |
| */ |
| static canReplayRequest(request) { |
| return !!requestToManagerMap.get(request) && request.resourceType() === Common.ResourceType.resourceTypes.XHR; |
| } |
| |
| /** |
| * @param {!NetworkRequest} request |
| */ |
| static replayRequest(request) { |
| const manager = requestToManagerMap.get(request); |
| if (!manager) { |
| return; |
| } |
| manager._networkAgent.invoke_replayXHR({requestId: request.requestId()}); |
| } |
| |
| /** |
| * @param {!NetworkRequest} request |
| * @param {string} query |
| * @param {boolean} caseSensitive |
| * @param {boolean} isRegex |
| * @return {!Promise<!Array<!TextUtils.ContentProvider.SearchMatch>>} |
| */ |
| static async searchInRequest(request, query, caseSensitive, isRegex) { |
| const manager = NetworkManager.forRequest(request); |
| if (!manager) { |
| return []; |
| } |
| const response = await manager._networkAgent.invoke_searchInResponseBody( |
| {requestId: request.requestId(), query: query, caseSensitive: caseSensitive, isRegex: isRegex}); |
| return response.result || []; |
| } |
| |
| /** |
| * @param {!NetworkRequest} request |
| * @return {!Promise<!ContentData>} |
| */ |
| static async requestContentData(request) { |
| if (request.resourceType() === Common.ResourceType.resourceTypes.WebSocket) { |
| return {error: 'Content for WebSockets is currently not supported', content: null, encoded: false}; |
| } |
| if (!request.finished) { |
| await request.once(NetworkRequestEvents.FinishedLoading); |
| } |
| const manager = NetworkManager.forRequest(request); |
| if (!manager) { |
| return {error: 'No network manager for request', content: null, encoded: false}; |
| } |
| const response = await manager._networkAgent.invoke_getResponseBody({requestId: request.requestId()}); |
| const error = response.getError() || null; |
| return {error: error, content: error ? null : response.body, encoded: response.base64Encoded}; |
| } |
| |
| /** |
| * @param {!NetworkRequest} request |
| * @return {!Promise<?string>} |
| */ |
| static async requestPostData(request) { |
| const manager = NetworkManager.forRequest(request); |
| if (manager) { |
| try { |
| const {postData} = |
| await manager._networkAgent.invoke_getRequestPostData({requestId: request.backendRequestId()}); |
| return postData; |
| } catch (e) { |
| return e.message; |
| } |
| } |
| console.error('No network manager for request'); |
| return /** @type {!Promise<?string>} */ (Promise.resolve(null)); |
| } |
| |
| /** |
| * @param {!Conditions} conditions |
| * @return {!Protocol.Network.ConnectionType} |
| * TODO(allada): this belongs to NetworkConditionsSelector, which should hardcode/guess it. |
| */ |
| static _connectionType(conditions) { |
| if (!conditions.download && !conditions.upload) { |
| return Protocol.Network.ConnectionType.None; |
| } |
| const title = conditions.title.toLowerCase(); |
| for (const [name, protocolType] of CONNECTION_TYPES) { |
| if (title.includes(name)) { |
| return protocolType; |
| } |
| } |
| return Protocol.Network.ConnectionType.Other; |
| } |
| |
| /** |
| * @param {!Object<string,string>} headers |
| * @return {!Object<string, string>} |
| */ |
| static lowercaseHeaders(headers) { |
| /** @type {!Object<string, string>} */ |
| const newHeaders = {}; |
| for (const headerName in headers) { |
| newHeaders[headerName.toLowerCase()] = headers[headerName]; |
| } |
| return newHeaders; |
| } |
| |
| /** |
| * @param {string} url |
| * @return {!NetworkRequest} |
| */ |
| inflightRequestForURL(url) { |
| return this._dispatcher._inflightRequestsByURL[url]; |
| } |
| |
| /** |
| * @param {!Common.EventTarget.EventTargetEvent} event |
| */ |
| _cacheDisabledSettingChanged(event) { |
| const enabled = /** @type {boolean} */ (event.data); |
| this._networkAgent.invoke_setCacheDisabled({cacheDisabled: enabled}); |
| } |
| |
| /** |
| * @override |
| */ |
| dispose() { |
| Common.Settings.Settings.instance() |
| .moduleSetting('cacheDisabled') |
| .removeChangeListener(this._cacheDisabledSettingChanged, this); |
| } |
| |
| _bypassServiceWorkerChanged() { |
| this._networkAgent.invoke_setBypassServiceWorker({bypass: this._bypassServiceWorkerSetting.get()}); |
| } |
| } |
| |
| /** @enum {symbol} */ |
| export const Events = { |
| RequestStarted: Symbol('RequestStarted'), |
| RequestUpdated: Symbol('RequestUpdated'), |
| RequestFinished: Symbol('RequestFinished'), |
| RequestUpdateDropped: Symbol('RequestUpdateDropped'), |
| ResponseReceived: Symbol('ResponseReceived'), |
| MessageGenerated: Symbol('MessageGenerated'), |
| RequestRedirected: Symbol('RequestRedirected'), |
| LoadingFinished: Symbol('LoadingFinished'), |
| }; |
| |
| /** @type {!Conditions} */ |
| export const NoThrottlingConditions = { |
| title: Common.UIString.UIString('Online'), |
| download: -1, |
| upload: -1, |
| latency: 0 |
| }; |
| |
| /** @type {!Conditions} */ |
| export const OfflineConditions = { |
| title: Common.UIString.UIString('Offline'), |
| download: 0, |
| upload: 0, |
| latency: 0, |
| }; |
| |
| /** @type {!Conditions} */ |
| export const Slow3GConditions = { |
| title: Common.UIString.UIString('Slow 3G'), |
| download: 500 * 1024 / 8 * .8, |
| upload: 500 * 1024 / 8 * .8, |
| latency: 400 * 5, |
| }; |
| |
| /** @type {!Conditions} */ |
| export const Fast3GConditions = { |
| title: Common.UIString.UIString('Fast 3G'), |
| download: 1.6 * 1024 * 1024 / 8 * .9, |
| upload: 750 * 1024 / 8 * .9, |
| latency: 150 * 3.75, |
| }; |
| |
| const MAX_EAGER_POST_REQUEST_BODY_LENGTH = 64 * 1024; // bytes |
| |
| /** |
| * @implements {ProtocolProxyApiWorkaround_NetworkDispatcher} |
| * @unrestricted |
| */ |
| export class NetworkDispatcher { |
| /** |
| * @param {!NetworkManager} manager |
| */ |
| constructor(manager) { |
| this._manager = manager; |
| /** @type {!Map<!Protocol.Network.RequestId, !NetworkRequest>} */ |
| this._inflightRequestsById = new Map(); |
| /** @type {!Object<string, !NetworkRequest>} */ |
| this._inflightRequestsByURL = {}; |
| /** @type {!Map<string, !RedirectExtraInfoBuilder>} */ |
| this._requestIdToRedirectExtraInfoBuilder = new Map(); |
| } |
| |
| /** |
| * @return {!Protocol.UsesObjectNotation} |
| */ |
| usesObjectNotation() { |
| return true; |
| } |
| |
| /** |
| * @param {!Protocol.Network.Headers} headersMap |
| * @return {!Array.<!NameValue>} |
| */ |
| _headersMapToHeadersArray(headersMap) { |
| const result = []; |
| for (const name in headersMap) { |
| const values = headersMap[name].split('\n'); |
| for (let i = 0; i < values.length; ++i) { |
| result.push({name: name, value: values[i]}); |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * @param {!NetworkRequest} networkRequest |
| * @param {!Protocol.Network.Request} request |
| */ |
| _updateNetworkRequestWithRequest(networkRequest, request) { |
| networkRequest.requestMethod = request.method; |
| networkRequest.setRequestHeaders(this._headersMapToHeadersArray(request.headers)); |
| networkRequest.setRequestFormData(!!request.hasPostData, request.postData || null); |
| networkRequest.setInitialPriority(request.initialPriority); |
| networkRequest.mixedContentType = request.mixedContentType || Protocol.Security.MixedContentType.None; |
| networkRequest.setReferrerPolicy(request.referrerPolicy); |
| } |
| |
| /** |
| * @param {!NetworkRequest} networkRequest |
| * @param {!Protocol.Network.Response} response |
| */ |
| _updateNetworkRequestWithResponse(networkRequest, response) { |
| if (response.url && networkRequest.url() !== response.url) { |
| networkRequest.setUrl(response.url); |
| } |
| networkRequest.mimeType = /** @type {!MIME_TYPE} */ (response.mimeType); |
| networkRequest.statusCode = response.status; |
| networkRequest.statusText = response.statusText; |
| if (!networkRequest.hasExtraResponseInfo()) { |
| networkRequest.responseHeaders = this._headersMapToHeadersArray(response.headers); |
| } |
| |
| if (response.encodedDataLength >= 0) { |
| networkRequest.setTransferSize(response.encodedDataLength); |
| } |
| |
| if (response.requestHeaders && !networkRequest.hasExtraRequestInfo()) { |
| // TODO(http://crbug.com/1004979): Stop using response.requestHeaders and |
| // response.requestHeadersText once shared workers |
| // emit Network.*ExtraInfo events for their network requests. |
| networkRequest.setRequestHeaders(this._headersMapToHeadersArray(response.requestHeaders)); |
| networkRequest.setRequestHeadersText(response.requestHeadersText || ''); |
| } |
| |
| networkRequest.connectionReused = response.connectionReused; |
| networkRequest.connectionId = String(response.connectionId); |
| if (response.remoteIPAddress) { |
| networkRequest.setRemoteAddress(response.remoteIPAddress, response.remotePort || -1); |
| } |
| |
| if (response.fromServiceWorker) { |
| networkRequest.fetchedViaServiceWorker = true; |
| } |
| |
| if (response.fromDiskCache) { |
| networkRequest.setFromDiskCache(); |
| } |
| |
| if (response.fromPrefetchCache) { |
| networkRequest.setFromPrefetchCache(); |
| } |
| |
| networkRequest.timing = response.timing; |
| |
| networkRequest.protocol = response.protocol || ''; |
| |
| networkRequest.setSecurityState(response.securityState); |
| |
| if (!this._mimeTypeIsConsistentWithType(networkRequest)) { |
| const message = Common.UIString.UIString( |
| 'Resource interpreted as %s but transferred with MIME type %s: "%s".', networkRequest.resourceType().title(), |
| networkRequest.mimeType, networkRequest.url()); |
| this._manager.dispatchEventToListeners( |
| Events.MessageGenerated, {message: message, requestId: networkRequest.requestId(), warning: true}); |
| } |
| |
| if (response.securityDetails) { |
| networkRequest.setSecurityDetails(response.securityDetails); |
| } |
| } |
| |
| /** |
| * @param {!NetworkRequest} networkRequest |
| * @return {boolean} |
| */ |
| _mimeTypeIsConsistentWithType(networkRequest) { |
| // If status is an error, content is likely to be of an inconsistent type, |
| // as it's going to be an error message. We do not want to emit a warning |
| // for this, though, as this will already be reported as resource loading failure. |
| // Also, if a URL like http://localhost/wiki/load.php?debug=true&lang=en produces text/css and gets reloaded, |
| // it is 304 Not Modified and its guessed mime-type is text/php, which is wrong. |
| // Don't check for mime-types in 304-resources. |
| if (networkRequest.hasErrorStatusCode() || networkRequest.statusCode === 304 || networkRequest.statusCode === 204) { |
| return true; |
| } |
| |
| const resourceType = networkRequest.resourceType(); |
| if (resourceType !== Common.ResourceType.resourceTypes.Stylesheet && |
| resourceType !== Common.ResourceType.resourceTypes.Document && |
| resourceType !== Common.ResourceType.resourceTypes.TextTrack) { |
| return true; |
| } |
| |
| |
| if (!networkRequest.mimeType) { |
| return true; |
| } // Might be not known for cached resources with null responses. |
| |
| if (MIME_TYPE_TO_RESOURCE_TYPE.has(networkRequest.mimeType)) { |
| return resourceType.name() in MIME_TYPE_TO_RESOURCE_TYPE.get(networkRequest.mimeType); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.ResourceChangedPriorityEvent} request |
| */ |
| resourceChangedPriority({requestId, newPriority, timestamp}) { |
| const networkRequest = this._inflightRequestsById.get(requestId); |
| if (networkRequest) { |
| networkRequest.setPriority(newPriority); |
| } |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.SignedExchangeReceivedEvent} request |
| */ |
| signedExchangeReceived({requestId, info}) { |
| // While loading a signed exchange, a signedExchangeReceived event is sent |
| // between two requestWillBeSent events. |
| // 1. The first requestWillBeSent is sent while starting the navigation (or |
| // prefetching). |
| // 2. This signedExchangeReceived event is sent when the browser detects the |
| // signed exchange. |
| // 3. The second requestWillBeSent is sent with the generated redirect |
| // response and a new redirected request which URL is the inner request |
| // URL of the signed exchange. |
| let networkRequest = this._inflightRequestsById.get(requestId); |
| // |requestId| is available only for navigation requests. If the request was |
| // sent from a renderer process for prefetching, it is not available. In the |
| // case, need to fallback to look for the URL. |
| // TODO(crbug/841076): Sends the request ID of prefetching to the browser |
| // process and DevTools to find the matching request. |
| if (!networkRequest) { |
| networkRequest = this._inflightRequestsByURL[info.outerResponse.url]; |
| if (!networkRequest) { |
| return; |
| } |
| } |
| networkRequest.setSignedExchangeInfo(info); |
| networkRequest.setResourceType(Common.ResourceType.resourceTypes.SignedExchange); |
| |
| this._updateNetworkRequestWithResponse(networkRequest, info.outerResponse); |
| this._updateNetworkRequest(networkRequest); |
| this._manager.dispatchEventToListeners(Events.ResponseReceived, networkRequest); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.RequestWillBeSentEvent} request |
| */ |
| requestWillBeSent( |
| {requestId, loaderId, documentURL, request, timestamp, wallTime, initiator, redirectResponse, type, frameId}) { |
| let networkRequest = this._inflightRequestsById.get(requestId); |
| if (networkRequest) { |
| // FIXME: move this check to the backend. |
| if (!redirectResponse) { |
| return; |
| } |
| // If signedExchangeReceived event has already been sent for the request, |
| // ignores the internally generated |redirectResponse|. The |
| // |outerResponse| of SignedExchangeInfo was set to |networkRequest| in |
| // signedExchangeReceived(). |
| if (!networkRequest.signedExchangeInfo()) { |
| this.responseReceived({ |
| requestId, |
| loaderId, |
| timestamp, |
| type: Protocol.Network.ResourceType.Other, |
| response: redirectResponse, |
| frameId |
| }); |
| } |
| networkRequest = this._appendRedirect(requestId, timestamp, request.url); |
| this._manager.dispatchEventToListeners(Events.RequestRedirected, networkRequest); |
| } else { |
| networkRequest = |
| this._createNetworkRequest(requestId, frameId || '', loaderId, request.url, documentURL, initiator); |
| } |
| networkRequest.hasNetworkData = true; |
| this._updateNetworkRequestWithRequest(networkRequest, request); |
| networkRequest.setIssueTime(timestamp, wallTime); |
| networkRequest.setResourceType( |
| type ? Common.ResourceType.resourceTypes[type] : Common.ResourceType.resourceTypes.Other); |
| |
| this._getExtraInfoBuilder(requestId).addRequest(networkRequest); |
| |
| this._startNetworkRequest(networkRequest); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.RequestServedFromCacheEvent} request |
| */ |
| requestServedFromCache({requestId}) { |
| const networkRequest = this._inflightRequestsById.get(requestId); |
| if (!networkRequest) { |
| return; |
| } |
| |
| networkRequest.setFromMemoryCache(); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.ResponseReceivedEvent} request |
| */ |
| responseReceived({requestId, loaderId, timestamp, type, response, frameId}) { |
| const networkRequest = this._inflightRequestsById.get(requestId); |
| const lowercaseHeaders = NetworkManager.lowercaseHeaders(response.headers); |
| if (!networkRequest) { |
| const lastModifiedHeader = lowercaseHeaders['last-modified']; |
| // We missed the requestWillBeSent. |
| const eventData = { |
| url: response.url, |
| frameId: frameId || '', |
| loaderId: loaderId, |
| resourceType: type, |
| mimeType: response.mimeType, |
| lastModified: lastModifiedHeader ? new Date(lastModifiedHeader) : null, |
| }; |
| this._manager.dispatchEventToListeners(Events.RequestUpdateDropped, eventData); |
| return; |
| } |
| |
| networkRequest.responseReceivedTime = timestamp; |
| networkRequest.setResourceType(Common.ResourceType.resourceTypes[type]); |
| |
| // net::ParsedCookie::kMaxCookieSize = 4096 (net/cookies/parsed_cookie.h) |
| if ('set-cookie' in lowercaseHeaders && lowercaseHeaders['set-cookie'].length > 4096) { |
| const values = lowercaseHeaders['set-cookie'].split('\n'); |
| for (let i = 0; i < values.length; ++i) { |
| if (values[i].length <= 4096) { |
| continue; |
| } |
| const message = Common.UIString.UIString( |
| 'Set-Cookie header is ignored in response from url: %s. Cookie length should be less than or equal to 4096 characters.', |
| response.url); |
| this._manager.dispatchEventToListeners( |
| Events.MessageGenerated, {message: message, requestId: requestId, warning: true}); |
| } |
| } |
| |
| this._updateNetworkRequestWithResponse(networkRequest, response); |
| |
| this._updateNetworkRequest(networkRequest); |
| this._manager.dispatchEventToListeners(Events.ResponseReceived, networkRequest); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.DataReceivedEvent} request |
| */ |
| dataReceived({requestId, timestamp, dataLength, encodedDataLength}) { |
| /** @type {?(!NetworkRequest|undefined)} */ |
| let networkRequest = this._inflightRequestsById.get(requestId); |
| if (!networkRequest) { |
| networkRequest = this._maybeAdoptMainResourceRequest(requestId); |
| } |
| if (!networkRequest) { |
| return; |
| } |
| |
| networkRequest.resourceSize += dataLength; |
| if (encodedDataLength !== -1) { |
| networkRequest.increaseTransferSize(encodedDataLength); |
| } |
| networkRequest.endTime = timestamp; |
| |
| this._updateNetworkRequest(networkRequest); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.LoadingFinishedEvent} request |
| */ |
| loadingFinished({requestId, timestamp: finishTime, encodedDataLength, shouldReportCorbBlocking}) { |
| /** @type {?(!NetworkRequest|undefined)} */ |
| let networkRequest = this._inflightRequestsById.get(requestId); |
| if (!networkRequest) { |
| networkRequest = this._maybeAdoptMainResourceRequest(requestId); |
| } |
| if (!networkRequest) { |
| return; |
| } |
| this._getExtraInfoBuilder(requestId).finished(); |
| this._finishNetworkRequest(networkRequest, finishTime, encodedDataLength, shouldReportCorbBlocking); |
| this._manager.dispatchEventToListeners(Events.LoadingFinished, networkRequest); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.LoadingFailedEvent} request |
| */ |
| loadingFailed( |
| {requestId, timestamp: time, type: resourceType, errorText: localizedDescription, canceled, blockedReason}) { |
| const networkRequest = this._inflightRequestsById.get(requestId); |
| if (!networkRequest) { |
| return; |
| } |
| |
| networkRequest.failed = true; |
| networkRequest.setResourceType(Common.ResourceType.resourceTypes[resourceType]); |
| networkRequest.canceled = !!canceled; |
| if (blockedReason) { |
| networkRequest.setBlockedReason(blockedReason); |
| if (blockedReason === Protocol.Network.BlockedReason.Inspector) { |
| const message = Common.UIString.UIString('Request was blocked by DevTools: "%s".', networkRequest.url()); |
| this._manager.dispatchEventToListeners( |
| Events.MessageGenerated, {message: message, requestId: requestId, warning: true}); |
| } |
| } |
| networkRequest.localizedFailDescription = localizedDescription; |
| this._getExtraInfoBuilder(requestId).finished(); |
| this._finishNetworkRequest(networkRequest, time, -1); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.WebSocketCreatedEvent} request |
| */ |
| webSocketCreated({requestId, url: requestURL, initiator}) { |
| const networkRequest = new NetworkRequest(requestId, requestURL, '', '', '', initiator || null); |
| requestToManagerMap.set(networkRequest, this._manager); |
| networkRequest.setResourceType(Common.ResourceType.resourceTypes.WebSocket); |
| this._startNetworkRequest(networkRequest); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.WebSocketWillSendHandshakeRequestEvent} request |
| */ |
| webSocketWillSendHandshakeRequest({requestId, timestamp: time, wallTime, request}) { |
| const networkRequest = this._inflightRequestsById.get(requestId); |
| if (!networkRequest) { |
| return; |
| } |
| |
| networkRequest.requestMethod = 'GET'; |
| networkRequest.setRequestHeaders(this._headersMapToHeadersArray(request.headers)); |
| networkRequest.setIssueTime(time, wallTime); |
| |
| this._updateNetworkRequest(networkRequest); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.WebSocketHandshakeResponseReceivedEvent} request |
| */ |
| webSocketHandshakeResponseReceived({requestId, timestamp: time, response}) { |
| const networkRequest = this._inflightRequestsById.get(requestId); |
| if (!networkRequest) { |
| return; |
| } |
| |
| networkRequest.statusCode = response.status; |
| networkRequest.statusText = response.statusText; |
| networkRequest.responseHeaders = this._headersMapToHeadersArray(response.headers); |
| networkRequest.responseHeadersText = response.headersText || ''; |
| if (response.requestHeaders) { |
| networkRequest.setRequestHeaders(this._headersMapToHeadersArray(response.requestHeaders)); |
| } |
| if (response.requestHeadersText) { |
| networkRequest.setRequestHeadersText(response.requestHeadersText); |
| } |
| networkRequest.responseReceivedTime = time; |
| networkRequest.protocol = 'websocket'; |
| |
| this._updateNetworkRequest(networkRequest); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.WebSocketFrameReceivedEvent} request |
| */ |
| webSocketFrameReceived({requestId, timestamp: time, response}) { |
| const networkRequest = this._inflightRequestsById.get(requestId); |
| if (!networkRequest) { |
| return; |
| } |
| |
| networkRequest.addProtocolFrame(response, time, false); |
| networkRequest.responseReceivedTime = time; |
| |
| this._updateNetworkRequest(networkRequest); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.WebSocketFrameSentEvent} request |
| */ |
| webSocketFrameSent({requestId, timestamp: time, response}) { |
| const networkRequest = this._inflightRequestsById.get(requestId); |
| if (!networkRequest) { |
| return; |
| } |
| |
| networkRequest.addProtocolFrame(response, time, true); |
| networkRequest.responseReceivedTime = time; |
| |
| this._updateNetworkRequest(networkRequest); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.WebSocketFrameErrorEvent} request |
| */ |
| webSocketFrameError({requestId, timestamp: time, errorMessage}) { |
| const networkRequest = this._inflightRequestsById.get(requestId); |
| if (!networkRequest) { |
| return; |
| } |
| |
| networkRequest.addProtocolFrameError(errorMessage, time); |
| networkRequest.responseReceivedTime = time; |
| |
| this._updateNetworkRequest(networkRequest); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.WebSocketClosedEvent} request |
| */ |
| webSocketClosed({requestId, timestamp: time}) { |
| const networkRequest = this._inflightRequestsById.get(requestId); |
| if (!networkRequest) { |
| return; |
| } |
| this._finishNetworkRequest(networkRequest, time, -1); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.EventSourceMessageReceivedEvent} request |
| */ |
| eventSourceMessageReceived({requestId, timestamp: time, eventName, eventId, data}) { |
| const networkRequest = this._inflightRequestsById.get(requestId); |
| if (!networkRequest) { |
| return; |
| } |
| networkRequest.addEventSourceMessage(time, eventName, eventId, data); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.RequestInterceptedEvent} request |
| */ |
| requestIntercepted({ |
| interceptionId, |
| request, |
| frameId, |
| resourceType, |
| isNavigationRequest, |
| isDownload, |
| redirectUrl, |
| authChallenge, |
| responseErrorReason, |
| responseStatusCode, |
| responseHeaders, |
| requestId |
| }) { |
| MultitargetNetworkManager.instance()._requestIntercepted(new InterceptedRequest( |
| this._manager.target().networkAgent(), interceptionId, request, frameId, resourceType, isNavigationRequest, |
| isDownload, redirectUrl, authChallenge, responseErrorReason, responseStatusCode, responseHeaders, requestId)); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.RequestWillBeSentExtraInfoEvent} request |
| */ |
| requestWillBeSentExtraInfo({requestId, associatedCookies, headers}) { |
| /** @type {!Array<!BlockedCookieWithReason>} */ |
| const blockedRequestCookies = []; |
| const includedRequestCookies = []; |
| for (const {blockedReasons, cookie} of associatedCookies) { |
| if (blockedReasons.length === 0) { |
| includedRequestCookies.push(Cookie.fromProtocolCookie(cookie)); |
| } else { |
| blockedRequestCookies.push({blockedReasons, cookie: Cookie.fromProtocolCookie(cookie)}); |
| } |
| } |
| const extraRequestInfo = { |
| blockedRequestCookies, |
| includedRequestCookies, |
| requestHeaders: this._headersMapToHeadersArray(headers) |
| }; |
| this._getExtraInfoBuilder(requestId).addRequestExtraInfo(extraRequestInfo); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.Network.ResponseReceivedExtraInfoEvent} request |
| */ |
| responseReceivedExtraInfo({requestId, blockedCookies, headers, headersText}) { |
| /** @type {!ExtraResponseInfo} */ |
| const extraResponseInfo = { |
| blockedResponseCookies: blockedCookies.map(blockedCookie => { |
| return { |
| blockedReasons: blockedCookie.blockedReasons, |
| cookieLine: blockedCookie.cookieLine, |
| cookie: blockedCookie.cookie ? Cookie.fromProtocolCookie(blockedCookie.cookie) : null |
| }; |
| }), |
| responseHeaders: this._headersMapToHeadersArray(headers), |
| responseHeadersText: headersText |
| }; |
| this._getExtraInfoBuilder(requestId).addResponseExtraInfo(extraResponseInfo); |
| } |
| |
| /** |
| * @param {string} requestId |
| * @return {!RedirectExtraInfoBuilder} |
| */ |
| _getExtraInfoBuilder(requestId) { |
| /** @type {!RedirectExtraInfoBuilder} */ |
| let builder; |
| if (!this._requestIdToRedirectExtraInfoBuilder.has(requestId)) { |
| const deleteCallback = () => { |
| this._requestIdToRedirectExtraInfoBuilder.delete(requestId); |
| }; |
| builder = new RedirectExtraInfoBuilder(deleteCallback); |
| this._requestIdToRedirectExtraInfoBuilder.set(requestId, builder); |
| } else { |
| builder = /** @type {!RedirectExtraInfoBuilder} */ (this._requestIdToRedirectExtraInfoBuilder.get(requestId)); |
| } |
| return builder; |
| } |
| |
| /** |
| * @param {!Protocol.Network.RequestId} requestId |
| * @param {!Protocol.Network.MonotonicTime} time |
| * @param {string} redirectURL |
| * @return {!NetworkRequest} |
| */ |
| _appendRedirect(requestId, time, redirectURL) { |
| const originalNetworkRequest = this._inflightRequestsById.get(requestId); |
| if (!originalNetworkRequest) { |
| throw new Error(`Could not find original network request for ${requestId}`); |
| } |
| let redirectCount = 0; |
| for (let redirect = originalNetworkRequest.redirectSource(); redirect; redirect = redirect.redirectSource()) { |
| redirectCount++; |
| } |
| |
| originalNetworkRequest.markAsRedirect(redirectCount); |
| this._finishNetworkRequest(originalNetworkRequest, time, -1); |
| const newNetworkRequest = this._createNetworkRequest( |
| requestId, originalNetworkRequest.frameId, originalNetworkRequest.loaderId, redirectURL, |
| originalNetworkRequest.documentURL, originalNetworkRequest.initiator()); |
| newNetworkRequest.setRedirectSource(originalNetworkRequest); |
| originalNetworkRequest.setRedirectDestination(newNetworkRequest); |
| return newNetworkRequest; |
| } |
| |
| /** |
| * @param {string} requestId |
| * @return {?NetworkRequest} |
| */ |
| _maybeAdoptMainResourceRequest(requestId) { |
| const request = MultitargetNetworkManager.instance()._inflightMainResourceRequests.get(requestId); |
| if (!request) { |
| return null; |
| } |
| const oldDispatcher = /** @type {!NetworkManager} */ (NetworkManager.forRequest(request))._dispatcher; |
| oldDispatcher._inflightRequestsById.delete(requestId); |
| delete oldDispatcher._inflightRequestsByURL[request.url()]; |
| this._inflightRequestsById.set(requestId, request); |
| this._inflightRequestsByURL[request.url()] = request; |
| requestToManagerMap.set(request, this._manager); |
| return request; |
| } |
| |
| /** |
| * @param {!NetworkRequest} networkRequest |
| */ |
| _startNetworkRequest(networkRequest) { |
| this._inflightRequestsById.set(networkRequest.requestId(), networkRequest); |
| this._inflightRequestsByURL[networkRequest.url()] = networkRequest; |
| // The following relies on the fact that loaderIds and requestIds are |
| // globally unique and that the main request has them equal. |
| if (networkRequest.loaderId === networkRequest.requestId()) { |
| MultitargetNetworkManager.instance()._inflightMainResourceRequests.set( |
| networkRequest.requestId(), networkRequest); |
| } |
| |
| this._manager.dispatchEventToListeners(Events.RequestStarted, networkRequest); |
| } |
| |
| /** |
| * @param {!NetworkRequest} networkRequest |
| */ |
| _updateNetworkRequest(networkRequest) { |
| this._manager.dispatchEventToListeners(Events.RequestUpdated, networkRequest); |
| } |
| |
| /** |
| * @param {!NetworkRequest} networkRequest |
| * @param {!Protocol.Network.MonotonicTime} finishTime |
| * @param {number} encodedDataLength |
| * @param {boolean=} shouldReportCorbBlocking |
| */ |
| _finishNetworkRequest(networkRequest, finishTime, encodedDataLength, shouldReportCorbBlocking) { |
| networkRequest.endTime = finishTime; |
| networkRequest.finished = true; |
| if (encodedDataLength >= 0) { |
| const redirectSource = networkRequest.redirectSource(); |
| if (redirectSource && redirectSource.signedExchangeInfo()) { |
| networkRequest.setTransferSize(0); |
| redirectSource.setTransferSize(encodedDataLength); |
| this._updateNetworkRequest(redirectSource); |
| } else { |
| networkRequest.setTransferSize(encodedDataLength); |
| } |
| } |
| this._manager.dispatchEventToListeners(Events.RequestFinished, networkRequest); |
| this._inflightRequestsById.delete(networkRequest.requestId()); |
| delete this._inflightRequestsByURL[networkRequest.url()]; |
| MultitargetNetworkManager.instance()._inflightMainResourceRequests.delete(networkRequest.requestId()); |
| |
| if (shouldReportCorbBlocking) { |
| const message = Common.UIString.UIString( |
| 'Cross-Origin Read Blocking (CORB) blocked cross-origin response %s with MIME type %s. See https://www.chromestatus.com/feature/5629709824032768 for more details.', |
| networkRequest.url(), networkRequest.mimeType); |
| this._manager.dispatchEventToListeners( |
| Events.MessageGenerated, {message: message, requestId: networkRequest.requestId(), warning: true}); |
| } |
| |
| if (Common.Settings.Settings.instance().moduleSetting('monitoringXHREnabled').get() && |
| networkRequest.resourceType().category() === Common.ResourceType.resourceCategories.XHR) { |
| let message; |
| const failedToLoad = networkRequest.failed || networkRequest.hasErrorStatusCode(); |
| if (failedToLoad) { |
| message = Common.UIString.UIString( |
| '%s failed loading: %s "%s".', networkRequest.resourceType().title(), networkRequest.requestMethod, |
| networkRequest.url()); |
| } else { |
| message = Common.UIString.UIString( |
| '%s finished loading: %s "%s".', networkRequest.resourceType().title(), networkRequest.requestMethod, |
| networkRequest.url()); |
| } |
| |
| this._manager.dispatchEventToListeners( |
| Events.MessageGenerated, {message: message, requestId: networkRequest.requestId(), warning: false}); |
| } |
| } |
| |
| /** |
| * @param {!Protocol.Network.RequestId} requestId |
| * @param {string} frameId |
| * @param {!Protocol.Network.LoaderId} loaderId |
| * @param {string} url |
| * @param {string} documentURL |
| * @param {?Protocol.Network.Initiator} initiator |
| */ |
| _createNetworkRequest(requestId, frameId, loaderId, url, documentURL, initiator) { |
| const request = new NetworkRequest(requestId, url, documentURL, frameId, loaderId, initiator); |
| requestToManagerMap.set(request, this._manager); |
| return request; |
| } |
| } |
| |
| /** |
| * @type {?MultitargetNetworkManager} |
| */ |
| let multiTargetNetworkManagerInstance; |
| |
| /** |
| * @implements {SDKModelObserver<!NetworkManager>} |
| * @unrestricted |
| */ |
| export class MultitargetNetworkManager extends Common.ObjectWrapper.ObjectWrapper { |
| constructor() { |
| super(); |
| /** @type {string} */ |
| this._userAgentOverride = ''; |
| /** @type {?Protocol.Emulation.UserAgentMetadata} */ |
| this._userAgentMetadataOverride = null; |
| /** @type {!Set<!ProtocolProxyApi.NetworkApi>} */ |
| this._agents = new Set(); |
| /** @type {!Map<string, !NetworkRequest>} */ |
| this._inflightMainResourceRequests = new Map(); |
| /** @type {!Conditions} */ |
| this._networkConditions = NoThrottlingConditions; |
| /** @type {?Promise<void>} */ |
| this._updatingInterceptionPatternsPromise = null; |
| |
| // TODO(allada) Remove these and merge it with request interception. |
| this._blockingEnabledSetting = Common.Settings.Settings.instance().moduleSetting('requestBlockingEnabled'); |
| this._blockedPatternsSetting = Common.Settings.Settings.instance().createSetting('networkBlockedPatterns', []); |
| /** @type {!Array<string>} */ |
| this._effectiveBlockedURLs = []; |
| this._updateBlockedPatterns(); |
| |
| /** @type {!Platform.Multimap<!RequestInterceptor, !InterceptionPattern>} */ |
| this._urlsForRequestInterceptor = new Platform.Multimap(); |
| |
| TargetManager.instance().observeModels(NetworkManager, this); |
| } |
| |
| /** |
| * @param {{forceNew: ?boolean}} opts |
| * @return {!MultitargetNetworkManager} |
| */ |
| static instance(opts = {forceNew: null}) { |
| const {forceNew} = opts; |
| if (!multiTargetNetworkManagerInstance || forceNew) { |
| multiTargetNetworkManagerInstance = new MultitargetNetworkManager(); |
| } |
| |
| return multiTargetNetworkManagerInstance; |
| } |
| |
| /** |
| * @return {string} |
| */ |
| static getChromeVersion() { |
| const chromeRegex = new RegExp('(?:^|\\W)Chrome/(\\S+)'); |
| const chromeMatch = navigator.userAgent.match(chromeRegex); |
| if (chromeMatch && chromeMatch.length > 1) { |
| return chromeMatch[1]; |
| } |
| return ''; |
| } |
| |
| |
| /** |
| * Generate a brand version list for Chrome, including some randomization |
| * to try to ensure proper parsing behavior. See |
| * |
| * https://wicg.github.io/ua-client-hints/#grease |
| * |
| * This implementation matches GenerateBrandVersionList() in |
| * chrome_content_browser_client.cc |
| * @param {number} seed |
| * @param {string} brand |
| * @param {string} majorVersion |
| * |
| * @return {!Array<!Protocol.Emulation.UserAgentBrandVersion>} |
| */ |
| static _generateBrandVersionList(seed, brand, majorVersion) { |
| // Pick a stable permutation seeded by major version number. any values here |
| // and in order should be under three. |
| const orders = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]; |
| const permutation = seed % orders.length; |
| const order = orders[permutation]; |
| |
| const escapedChars = ['\\', '\"', ';']; |
| const greaseyBrand = |
| escapedChars[order[0]] + 'Not' + escapedChars[order[1]] + 'A' + escapedChars[order[2]] + 'Brand'; |
| |
| const greasey = {brand: greaseyBrand, version: '99'}; |
| const chromium = {brand: 'Chromium', version: majorVersion}; |
| const branded = {brand: brand, version: majorVersion}; |
| |
| const greasedBrandVersionList = /** @type {!Array<!Protocol.Emulation.UserAgentBrandVersion>} */ ([]); |
| greasedBrandVersionList[order[0]] = greasey; |
| greasedBrandVersionList[order[1]] = chromium; |
| greasedBrandVersionList[order[2]] = branded; |
| return greasedBrandVersionList; |
| } |
| |
| /** |
| * @return {!Array<!Protocol.Emulation.UserAgentBrandVersion>} |
| */ |
| static getChromeBrands() { |
| const chromeVersion = MultitargetNetworkManager.getChromeVersion(); |
| if (chromeVersion.length > 0) { |
| const majorVersion = chromeVersion.split('.', 1)[0]; |
| return MultitargetNetworkManager._generateBrandVersionList( |
| Number.parseInt(majorVersion, 10), 'Google Chrome', majorVersion); |
| } |
| return []; |
| } |
| |
| /** |
| * @param {string} uaString |
| * @return {string} |
| */ |
| static patchUserAgentWithChromeVersion(uaString) { |
| // Patches Chrome/CriOS version from user agent ("1.2.3.4" when user agent is: "Chrome/1.2.3.4"). |
| // Edge also contains an appVersion which should be patched to match the Chrome major version. |
| // Otherwise, ignore it. This assumes additional appVersions appear after the Chrome version. |
| const chromeVersion = MultitargetNetworkManager.getChromeVersion(); |
| if (chromeVersion.length > 0) { |
| // "1.2.3.4" becomes "1.0.100.0" |
| const additionalAppVersion = chromeVersion.split('.', 1)[0] + '.0.100.0'; |
| return Platform.StringUtilities.sprintf(uaString, chromeVersion, additionalAppVersion); |
| } |
| return uaString; |
| } |
| |
| /** |
| * @override |
| * @param {!NetworkManager} networkManager |
| */ |
| modelAdded(networkManager) { |
| const networkAgent = networkManager.target().networkAgent(); |
| if (this._extraHeaders) { |
| networkAgent.invoke_setExtraHTTPHeaders({headers: this._extraHeaders}); |
| } |
| if (this.currentUserAgent()) { |
| networkAgent.invoke_setUserAgentOverride( |
| {userAgent: this.currentUserAgent(), userAgentMetadata: this._userAgentMetadataOverride || undefined}); |
| } |
| if (this._effectiveBlockedURLs.length) { |
| networkAgent.invoke_setBlockedURLs({urls: this._effectiveBlockedURLs}); |
| } |
| if (this.isIntercepting()) { |
| networkAgent.invoke_setRequestInterception({patterns: this._urlsForRequestInterceptor.valuesArray()}); |
| } |
| this._agents.add(networkAgent); |
| if (this.isThrottling()) { |
| this._updateNetworkConditions(networkAgent); |
| } |
| } |
| |
| /** |
| * @override |
| * @param {!NetworkManager} networkManager |
| */ |
| modelRemoved(networkManager) { |
| for (const entry of this._inflightMainResourceRequests) { |
| const manager = NetworkManager.forRequest(/** @type {!NetworkRequest} */ (entry[1])); |
| if (manager !== networkManager) { |
| continue; |
| } |
| this._inflightMainResourceRequests.delete(/** @type {string} */ (entry[0])); |
| } |
| this._agents.delete(networkManager.target().networkAgent()); |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| isThrottling() { |
| return this._networkConditions.download >= 0 || this._networkConditions.upload >= 0 || |
| this._networkConditions.latency > 0; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| isOffline() { |
| return !this._networkConditions.download && !this._networkConditions.upload; |
| } |
| |
| /** |
| * @param {!Conditions} conditions |
| */ |
| setNetworkConditions(conditions) { |
| this._networkConditions = conditions; |
| for (const agent of this._agents) { |
| this._updateNetworkConditions(agent); |
| } |
| this.dispatchEventToListeners(MultitargetNetworkManager.Events.ConditionsChanged); |
| } |
| |
| /** |
| * @return {!Conditions} |
| */ |
| networkConditions() { |
| return this._networkConditions; |
| } |
| |
| /** |
| * @param {!ProtocolProxyApi.NetworkApi} networkAgent |
| */ |
| _updateNetworkConditions(networkAgent) { |
| const conditions = this._networkConditions; |
| if (!this.isThrottling()) { |
| networkAgent.invoke_emulateNetworkConditions( |
| {offline: false, latency: 0, downloadThroughput: 0, uploadThroughput: 0}); |
| } else { |
| networkAgent.invoke_emulateNetworkConditions({ |
| offline: this.isOffline(), |
| latency: conditions.latency, |
| downloadThroughput: conditions.download < 0 ? 0 : conditions.download, |
| uploadThroughput: conditions.upload < 0 ? 0 : conditions.upload, |
| connectionType: NetworkManager._connectionType(conditions) |
| }); |
| } |
| } |
| |
| /** |
| * @param {!Protocol.Network.Headers} headers |
| */ |
| setExtraHTTPHeaders(headers) { |
| this._extraHeaders = headers; |
| for (const agent of this._agents) { |
| agent.invoke_setExtraHTTPHeaders({headers: this._extraHeaders}); |
| } |
| } |
| |
| /** |
| * @return {string} |
| */ |
| currentUserAgent() { |
| return this._customUserAgent ? this._customUserAgent : this._userAgentOverride; |
| } |
| |
| _updateUserAgentOverride() { |
| const userAgent = this.currentUserAgent(); |
| for (const agent of this._agents) { |
| agent.invoke_setUserAgentOverride( |
| {userAgent: userAgent, userAgentMetadata: this._userAgentMetadataOverride || undefined}); |
| } |
| } |
| |
| /** |
| * @param {string} userAgent |
| * @param {?Protocol.Emulation.UserAgentMetadata} userAgentMetadataOverride |
| */ |
| setUserAgentOverride(userAgent, userAgentMetadataOverride) { |
| if (this._userAgentOverride === userAgent) { |
| return; |
| } |
| this._userAgentOverride = userAgent; |
| if (!this._customUserAgent) { |
| this._userAgentMetadataOverride = userAgentMetadataOverride; |
| this._updateUserAgentOverride(); |
| } else { |
| this._userAgentMetadataOverride = null; |
| } |
| this.dispatchEventToListeners(MultitargetNetworkManager.Events.UserAgentChanged); |
| } |
| |
| /** |
| * @return {string} |
| */ |
| userAgentOverride() { |
| return this._userAgentOverride; |
| } |
| |
| /** |
| * @param {string} userAgent |
| */ |
| setCustomUserAgentOverride(userAgent) { |
| this._customUserAgent = userAgent; |
| this._userAgentMetadataOverride = null; |
| this._updateUserAgentOverride(); |
| } |
| |
| // TODO(allada) Move all request blocking into interception and let view manage blocking. |
| /** |
| * @return {!Array<!BlockedPattern>} |
| */ |
| blockedPatterns() { |
| return this._blockedPatternsSetting.get().slice(); |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| blockingEnabled() { |
| return this._blockingEnabledSetting.get(); |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| isBlocking() { |
| return !!this._effectiveBlockedURLs.length; |
| } |
| |
| /** |
| * @param {!Array<!BlockedPattern>} patterns |
| */ |
| setBlockedPatterns(patterns) { |
| this._blockedPatternsSetting.set(patterns); |
| this._updateBlockedPatterns(); |
| this.dispatchEventToListeners(MultitargetNetworkManager.Events.BlockedPatternsChanged); |
| } |
| |
| /** |
| * @param {boolean} enabled |
| */ |
| setBlockingEnabled(enabled) { |
| if (this._blockingEnabledSetting.get() === enabled) { |
| return; |
| } |
| this._blockingEnabledSetting.set(enabled); |
| this._updateBlockedPatterns(); |
| this.dispatchEventToListeners(MultitargetNetworkManager.Events.BlockedPatternsChanged); |
| } |
| |
| _updateBlockedPatterns() { |
| const urls = []; |
| if (this._blockingEnabledSetting.get()) { |
| for (const pattern of this._blockedPatternsSetting.get()) { |
| if (pattern.enabled) { |
| urls.push(pattern.url); |
| } |
| } |
| } |
| |
| if (!urls.length && !this._effectiveBlockedURLs.length) { |
| return; |
| } |
| this._effectiveBlockedURLs = urls; |
| for (const agent of this._agents) { |
| agent.invoke_setBlockedURLs({urls: this._effectiveBlockedURLs}); |
| } |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| isIntercepting() { |
| return !!this._urlsForRequestInterceptor.size; |
| } |
| |
| /** |
| * @param {!Array<!InterceptionPattern>} patterns |
| * @param {!RequestInterceptor} requestInterceptor |
| * @return {!Promise<void>} |
| */ |
| setInterceptionHandlerForPatterns(patterns, requestInterceptor) { |
| // Note: requestInterceptors may recieve interception requests for patterns they did not subscribe to. |
| this._urlsForRequestInterceptor.deleteAll(requestInterceptor); |
| for (const newPattern of patterns) { |
| this._urlsForRequestInterceptor.set(requestInterceptor, newPattern); |
| } |
| return this._updateInterceptionPatternsOnNextTick(); |
| } |
| |
| /** |
| * @return {!Promise<void>} |
| */ |
| _updateInterceptionPatternsOnNextTick() { |
| // This is used so we can register and unregister patterns in loops without sending lots of protocol messages. |
| if (!this._updatingInterceptionPatternsPromise) { |
| this._updatingInterceptionPatternsPromise = Promise.resolve().then(this._updateInterceptionPatterns.bind(this)); |
| } |
| return this._updatingInterceptionPatternsPromise; |
| } |
| |
| /** |
| * @return {!Promise<void>} |
| */ |
| _updateInterceptionPatterns() { |
| if (!Common.Settings.Settings.instance().moduleSetting('cacheDisabled').get()) { |
| Common.Settings.Settings.instance().moduleSetting('cacheDisabled').set(true); |
| } |
| this._updatingInterceptionPatternsPromise = null; |
| const promises = /** @type {!Array<!Promise<*>>} */ ([]); |
| for (const agent of this._agents) { |
| promises.push(agent.invoke_setRequestInterception({patterns: this._urlsForRequestInterceptor.valuesArray()})); |
| } |
| this.dispatchEventToListeners(MultitargetNetworkManager.Events.InterceptorsChanged); |
| return Promise.all(promises).then(values => Promise.resolve()); |
| } |
| |
| /** |
| * @param {!InterceptedRequest} interceptedRequest |
| */ |
| async _requestIntercepted(interceptedRequest) { |
| for (const requestInterceptor of this._urlsForRequestInterceptor.keysArray()) { |
| await requestInterceptor(interceptedRequest); |
| if (interceptedRequest.hasResponded()) { |
| return; |
| } |
| } |
| if (!interceptedRequest.hasResponded()) { |
| interceptedRequest.continueRequestWithoutChange(); |
| } |
| } |
| |
| clearBrowserCache() { |
| for (const agent of this._agents) { |
| agent.invoke_clearBrowserCache(); |
| } |
| } |
| |
| clearBrowserCookies() { |
| for (const agent of this._agents) { |
| agent.invoke_clearBrowserCookies(); |
| } |
| } |
| |
| /** |
| * @param {string} origin |
| * @return {!Promise<!Array<string>>} |
| */ |
| getCertificate(origin) { |
| const target = TargetManager.instance().mainTarget(); |
| if (!target) { |
| return Promise.resolve([]); |
| } |
| return target.networkAgent().invoke_getCertificate({origin}).then( |
| certificate => (certificate && certificate.tableNames) || []); |
| } |
| |
| /** |
| * @param {string} url |
| * @param {function(boolean, !Object.<string, string>, string, !Host.ResourceLoader.LoadErrorDescription):void} callback |
| */ |
| loadResource(url, callback) { |
| /** @type {!Object<string, string>} */ |
| const headers = {}; |
| |
| const currentUserAgent = this.currentUserAgent(); |
| if (currentUserAgent) { |
| headers['User-Agent'] = currentUserAgent; |
| } |
| |
| if (Common.Settings.Settings.instance().moduleSetting('cacheDisabled').get()) { |
| headers['Cache-Control'] = 'no-cache'; |
| } |
| |
| Host.ResourceLoader.load(url, headers, callback); |
| } |
| } |
| |
| /** @enum {symbol} */ |
| MultitargetNetworkManager.Events = { |
| BlockedPatternsChanged: Symbol('BlockedPatternsChanged'), |
| ConditionsChanged: Symbol('ConditionsChanged'), |
| UserAgentChanged: Symbol('UserAgentChanged'), |
| InterceptorsChanged: Symbol('InterceptorsChanged') |
| }; |
| |
| export class InterceptedRequest { |
| /** |
| * @param {!ProtocolProxyApi.NetworkApi} networkAgent |
| * @param {!Protocol.Network.InterceptionId} interceptionId |
| * @param {!Protocol.Network.Request} request |
| * @param {!Protocol.Page.FrameId} frameId |
| * @param {!Protocol.Network.ResourceType} resourceType |
| * @param {boolean} isNavigationRequest |
| * @param {boolean=} isDownload |
| * @param {string=} redirectUrl |
| * @param {!Protocol.Network.AuthChallenge=} authChallenge |
| * @param {!Protocol.Network.ErrorReason=} responseErrorReason |
| * @param {number=} responseStatusCode |
| * @param {!Protocol.Network.Headers=} responseHeaders |
| * @param {!Protocol.Network.RequestId=} requestId |
| */ |
| constructor( |
| networkAgent, interceptionId, request, frameId, resourceType, isNavigationRequest, isDownload, redirectUrl, |
| authChallenge, responseErrorReason, responseStatusCode, responseHeaders, requestId) { |
| this._networkAgent = networkAgent; |
| this._interceptionId = interceptionId; |
| this._hasResponded = false; |
| this.request = request; |
| this.frameId = frameId; |
| this.resourceType = resourceType; |
| this.isNavigationRequest = isNavigationRequest; |
| this.isDownload = !!isDownload; |
| this.redirectUrl = redirectUrl; |
| this.authChallenge = authChallenge; |
| this.responseErrorReason = responseErrorReason; |
| this.responseStatusCode = responseStatusCode; |
| this.responseHeaders = responseHeaders; |
| this.requestId = requestId; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| hasResponded() { |
| return this._hasResponded; |
| } |
| |
| /** |
| * @param {!Blob} contentBlob |
| */ |
| async continueRequestWithContent(contentBlob) { |
| this._hasResponded = true; |
| const headers = [ |
| 'HTTP/1.1 200 OK', |
| 'Date: ' + (new Date()).toUTCString(), |
| 'Server: Chrome Devtools Request Interceptor', |
| 'Connection: closed', |
| 'Content-Length: ' + contentBlob.size, |
| 'Content-Type: ' + contentBlob.type || 'text/x-unknown', |
| ]; |
| const encodedResponse = await blobToBase64(new Blob([headers.join('\r\n'), '\r\n\r\n', contentBlob])); |
| this._networkAgent.invoke_continueInterceptedRequest( |
| {interceptionId: this._interceptionId, rawResponse: encodedResponse}); |
| |
| /** |
| * @param {!Blob} blob |
| * @return {!Promise<string>} |
| */ |
| async function blobToBase64(blob) { |
| const reader = new FileReader(); |
| const fileContentsLoadedPromise = new Promise(resolve => reader.onloadend = resolve); |
| reader.readAsDataURL(blob); |
| await fileContentsLoadedPromise; |
| if (reader.error) { |
| console.error('Could not convert blob to base64.', reader.error); |
| return ''; |
| } |
| const result = reader.result; |
| if (result === undefined || result === null || typeof result !== 'string') { |
| console.error('Could not convert blob to base64.'); |
| return ''; |
| } |
| return result.substring(result.indexOf(',') + 1); |
| } |
| } |
| |
| continueRequestWithoutChange() { |
| console.assert(!this._hasResponded); |
| this._hasResponded = true; |
| this._networkAgent.invoke_continueInterceptedRequest({interceptionId: this._interceptionId}); |
| } |
| |
| /** |
| * @param {!Protocol.Network.ErrorReason} errorReason |
| */ |
| continueRequestWithError(errorReason) { |
| console.assert(!this._hasResponded); |
| this._hasResponded = true; |
| this._networkAgent.invoke_continueInterceptedRequest({interceptionId: this._interceptionId, errorReason}); |
| } |
| |
| /** |
| * @return {!Promise<!ContentData>} |
| */ |
| async responseBody() { |
| const response = |
| await this._networkAgent.invoke_getResponseBodyForInterception({interceptionId: this._interceptionId}); |
| const error = response.getError() || null; |
| return {error: error, content: error ? null : response.body, encoded: response.base64Encoded}; |
| } |
| } |
| |
| /** |
| * Helper class to match requests created from requestWillBeSent with |
| * requestWillBeSentExtraInfo and responseReceivedExtraInfo when they have the |
| * same requestId due to redirects. |
| */ |
| class RedirectExtraInfoBuilder { |
| /** |
| * @param {function():void} deleteCallback |
| */ |
| constructor(deleteCallback) { |
| /** @type {!Array<!NetworkRequest>} */ |
| this._requests = []; |
| /** @type {!Array<?ExtraRequestInfo>} */ |
| this._requestExtraInfos = []; |
| /** @type {!Array<?ExtraResponseInfo>} */ |
| this._responseExtraInfos = []; |
| /** @type {boolean} */ |
| this._finished = false; |
| /** @type {boolean} */ |
| this._hasExtraInfo = false; |
| /** @type {function():void} */ |
| this._deleteCallback = deleteCallback; |
| } |
| |
| /** |
| * @param {!NetworkRequest} req |
| */ |
| addRequest(req) { |
| this._requests.push(req); |
| this._sync(this._requests.length - 1); |
| } |
| |
| /** |
| * @param {!ExtraRequestInfo} info |
| */ |
| addRequestExtraInfo(info) { |
| this._hasExtraInfo = true; |
| this._requestExtraInfos.push(info); |
| this._sync(this._requestExtraInfos.length - 1); |
| } |
| |
| /** |
| * @param {!ExtraResponseInfo} info |
| */ |
| addResponseExtraInfo(info) { |
| this._responseExtraInfos.push(info); |
| this._sync(this._responseExtraInfos.length - 1); |
| } |
| |
| finished() { |
| this._finished = true; |
| this._deleteIfComplete(); |
| } |
| |
| /** |
| * @param {number} index |
| */ |
| _sync(index) { |
| const req = this._requests[index]; |
| if (!req) { |
| return; |
| } |
| |
| const requestExtraInfo = this._requestExtraInfos[index]; |
| if (requestExtraInfo) { |
| req.addExtraRequestInfo(requestExtraInfo); |
| this._requestExtraInfos[index] = null; |
| } |
| |
| const responseExtraInfo = this._responseExtraInfos[index]; |
| if (responseExtraInfo) { |
| req.addExtraResponseInfo(responseExtraInfo); |
| this._responseExtraInfos[index] = null; |
| } |
| |
| this._deleteIfComplete(); |
| } |
| |
| _deleteIfComplete() { |
| if (!this._finished) { |
| return; |
| } |
| |
| if (this._hasExtraInfo) { |
| // if we haven't gotten the last responseExtraInfo event, we have to wait for it. |
| const lastItem = this._requests.peekLast(); |
| if (lastItem && !lastItem.hasExtraResponseInfo()) { |
| return; |
| } |
| } |
| |
| this._deleteCallback(); |
| } |
| } |
| |
| SDKModel.register(NetworkManager, Capability.Network, true); |
| |
| /** |
| * @typedef {{ |
| * download: number, |
| * upload: number, |
| * latency: number, |
| * title: string, |
| * }} |
| */ |
| // @ts-ignore typedef |
| export let Conditions; |
| |
| /** @typedef {{url: string, enabled: boolean}} */ |
| // @ts-ignore typedef |
| export let BlockedPattern; |
| |
| /** @typedef {{message: string, requestId: string, warning: boolean}} */ |
| // @ts-ignore typedef |
| export let Message; |
| |
| /** @typedef {!{urlPattern: string, interceptionStage: !Protocol.Network.InterceptionStage}} */ |
| // @ts-ignore typedef |
| export let InterceptionPattern; |
| |
| /** @typedef {!function(!InterceptedRequest):!Promise<void>} */ |
| // @ts-ignore typedef |
| export let RequestInterceptor; |