blob: 1c8f6b48d366c648fbda30122f4cffd48c56cc92 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Protocol from '../../generated/protocol.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as Persistence from '../../models/persistence/persistence.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as Workspace from '../../models/workspace/workspace.js';
import {createTarget, describeWithEnvironment, updateHostConfig} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../testing/MockConnection.js';
import {createWorkspaceProject} from '../../testing/OverridesHelpers.js';
import * as Common from '../common/common.js';
import * as Platform from '../platform/platform.js';
import * as Root from '../root/root.js';
import * as SDK from './sdk.js';
const {urlString} = Platform.DevToolsPath;
const LONG_URL_PART =
'LoremIpsumDolorSitAmetConsecteturAdipiscingElitPhasellusVitaeOrciInAugueCondimentumTinciduntUtEgetDolorQuisqueEfficiturUltricesTinciduntVivamusVelitPurusCommodoQuisErosSitAmetTemporMalesuadaNislNullamTtempusVulputateAugueEgetScelerisqueLacusVestibulumNon/index.html';
describeWithMockConnection('NetworkManager', () => {
it('setCookieControls is not invoked if the browsers enterprise setting blocks third party cookies', () => {
Object.assign(
Root.Runtime.hostConfig,
{thirdPartyCookieControls: {managedBlockThirdPartyCookies: true}, devToolsPrivacyUI: {enabled: true}});
const enableThirdPartyCookieRestrictionSetting =
Common.Settings.Settings.instance().createSetting('cookie-control-override-enabled', false);
const disableThirdPartyCookieMetadataSetting =
Common.Settings.Settings.instance().createSetting('grace-period-mitigation-disabled', true);
const disableThirdPartyCookieHeuristicsSetting =
Common.Settings.Settings.instance().createSetting('heuristic-mitigation-disabled', true);
assert.isFalse(enableThirdPartyCookieRestrictionSetting.get());
assert.isTrue(disableThirdPartyCookieMetadataSetting.get());
assert.isTrue(disableThirdPartyCookieHeuristicsSetting.get());
const target = createTarget();
const expectedCall = sinon.spy(target.networkAgent(), 'invoke_setCookieControls');
new SDK.NetworkManager.NetworkManager(target);
// function should not be called since there is a enterprise policy blocking third-party cookies
assert.isTrue(expectedCall.notCalled);
});
it('setCookieControls gets invoked with expected values when network agent auto attach', () => {
updateHostConfig({devToolsPrivacyUI: {enabled: true}});
const enableThirdPartyCookieRestrictionSetting =
Common.Settings.Settings.instance().createSetting('cookie-control-override-enabled', false);
const disableThirdPartyCookieMetadataSetting =
Common.Settings.Settings.instance().createSetting('grace-period-mitigation-disabled', true);
const disableThirdPartyCookieHeuristicsSetting =
Common.Settings.Settings.instance().createSetting('heuristic-mitigation-disabled', true);
assert.isFalse(enableThirdPartyCookieRestrictionSetting.get());
assert.isTrue(disableThirdPartyCookieMetadataSetting.get());
assert.isTrue(disableThirdPartyCookieHeuristicsSetting.get());
const target = createTarget();
const expectedCall = sinon.spy(target.networkAgent(), 'invoke_setCookieControls');
new SDK.NetworkManager.NetworkManager(target);
// Metadata and heuristics should be disabled when cookie controls is disabled.
assert.isTrue(expectedCall.calledOnceWith({
enableThirdPartyCookieRestriction: false,
disableThirdPartyCookieMetadata: false,
disableThirdPartyCookieHeuristics: false
}));
});
});
describeWithMockConnection('MultitargetNetworkManager', () => {
describe('Trust Token done event', () => {
it('is not lost when arriving before the corresponding requestWillBeSent event', () => {
// 1) Setup a NetworkManager and listen to "RequestStarted" events.
const networkManager = new Common.ObjectWrapper.ObjectWrapper<SDK.NetworkManager.EventTypes>();
const startedRequests: SDK.NetworkRequest.NetworkRequest[] = [];
networkManager.addEventListener(SDK.NetworkManager.Events.RequestStarted, event => {
startedRequests.push(event.data.request);
});
const networkDispatcher =
new SDK.NetworkManager.NetworkDispatcher(networkManager as SDK.NetworkManager.NetworkManager);
// 2) Fire a trust token event, followed by a requestWillBeSent event.
const mockEvent = {requestId: 'mockId'} as Protocol.Network.TrustTokenOperationDoneEvent;
networkDispatcher.trustTokenOperationDone(mockEvent);
networkDispatcher.requestWillBeSent(
{requestId: 'mockId', request: {url: 'example.com'}} as Protocol.Network.RequestWillBeSentEvent);
// 3) Check that the resulting NetworkRequest has the Trust Token Event data associated with it.
assert.lengthOf(startedRequests, 1);
assert.strictEqual(startedRequests[0].trustTokenOperationDoneEvent(), mockEvent);
});
});
it('handles worker requests originating from the frame target', async () => {
const target = createTarget();
const workerTarget = createTarget({type: SDK.Target.Type.Worker});
const multiTargetNetworkManager = SDK.NetworkManager.MultitargetNetworkManager.instance();
const initialNetworkManager = target.model(SDK.NetworkManager.NetworkManager)!;
assert.strictEqual(multiTargetNetworkManager.inflightMainResourceRequests.size, 0);
const requestId = 'mockId';
const requestPromise = initialNetworkManager.once(SDK.NetworkManager.Events.RequestStarted);
initialNetworkManager.dispatcher.requestWillBeSent(
{requestId, loaderId: '', request: {url: 'example.com'}} as Protocol.Network.RequestWillBeSentEvent);
const {request} = await requestPromise;
assert.isOk(SDK.NetworkManager.NetworkManager.forRequest(request) === initialNetworkManager);
assert.isOk(multiTargetNetworkManager.inflightMainResourceRequests.has(requestId));
const workerNetworkManager = workerTarget.model(SDK.NetworkManager.NetworkManager)!;
workerNetworkManager.dispatcher.loadingFinished({requestId} as Protocol.Network.LoadingFinishedEvent);
assert.isOk(SDK.NetworkManager.NetworkManager.forRequest(request) === workerNetworkManager);
assert.isOk(!multiTargetNetworkManager.inflightMainResourceRequests.has(requestId));
});
it('uses main frame to get certificate', () => {
SDK.ChildTargetManager.ChildTargetManager.install();
const tabTarget = createTarget({type: SDK.Target.Type.TAB});
const mainFrameTarget = createTarget({parentTarget: tabTarget});
const prerenderTarget = createTarget({parentTarget: tabTarget, subtype: 'prerender'});
const subframeTarget = createTarget({parentTarget: mainFrameTarget, subtype: ''});
const unexpectedCalls =
[tabTarget, prerenderTarget, subframeTarget].map(t => sinon.spy(t.networkAgent(), 'invoke_getCertificate'));
const expectedCall = sinon.spy(mainFrameTarget.networkAgent(), 'invoke_getCertificate');
void SDK.NetworkManager.MultitargetNetworkManager.instance().getCertificate('https://example.com');
for (const unexpectedCall of unexpectedCalls) {
assert.isTrue(unexpectedCall.notCalled);
}
assert.isTrue(expectedCall.calledOnceWith({origin: 'https://example.com'}));
});
it('blocking settings are consistent after change', async () => {
const multitargetNetworkManager = SDK.NetworkManager.MultitargetNetworkManager.instance({forceNew: true});
let eventCounter = 0;
multitargetNetworkManager.addEventListener(
SDK.NetworkManager.MultitargetNetworkManager.Events.BLOCKED_PATTERNS_CHANGED, () => eventCounter++);
const blockingEnabledSetting = Common.Settings.Settings.instance().moduleSetting('request-blocking-enabled');
const blockedPatternsSetting: Common.Settings.Setting<SDK.NetworkManager.BlockedPattern[]> =
Common.Settings.Settings.instance().createSetting('network-blocked-patterns', []);
// Change blocking setting via Common.Settings.Settings.
assert.isFalse(multitargetNetworkManager.isBlocking());
assert.isFalse(multitargetNetworkManager.blockingEnabled());
blockingEnabledSetting.set(true);
assert.strictEqual(eventCounter, 1);
assert.isFalse(multitargetNetworkManager.isBlocking());
assert.isTrue(multitargetNetworkManager.blockingEnabled());
blockedPatternsSetting.set([{url: 'example.com', enabled: true}]);
assert.strictEqual(eventCounter, 2);
assert.isTrue(multitargetNetworkManager.isBlocking());
assert.isTrue(multitargetNetworkManager.blockingEnabled());
blockedPatternsSetting.set([]);
assert.strictEqual(eventCounter, 3);
assert.isFalse(multitargetNetworkManager.isBlocking());
assert.isTrue(multitargetNetworkManager.blockingEnabled());
blockingEnabledSetting.set(false);
assert.strictEqual(eventCounter, 4);
assert.isFalse(multitargetNetworkManager.isBlocking());
assert.isFalse(multitargetNetworkManager.blockingEnabled());
// Change blocking setting via MultitargetNetworkManager.
assert.isFalse(multitargetNetworkManager.isBlocking());
assert.isFalse(multitargetNetworkManager.blockingEnabled());
multitargetNetworkManager.setBlockingEnabled(true);
assert.strictEqual(eventCounter, 5);
assert.isFalse(multitargetNetworkManager.isBlocking());
assert.isTrue(multitargetNetworkManager.blockingEnabled());
multitargetNetworkManager.setBlockedPatterns([{url: 'example.com', enabled: true}]);
assert.strictEqual(eventCounter, 6);
assert.isTrue(multitargetNetworkManager.isBlocking());
assert.isTrue(multitargetNetworkManager.blockingEnabled());
multitargetNetworkManager.setBlockedPatterns([]);
assert.strictEqual(eventCounter, 7);
assert.isFalse(multitargetNetworkManager.isBlocking());
assert.isTrue(multitargetNetworkManager.blockingEnabled());
multitargetNetworkManager.setBlockingEnabled(false);
assert.strictEqual(eventCounter, 8);
assert.isFalse(multitargetNetworkManager.isBlocking());
assert.isFalse(multitargetNetworkManager.blockingEnabled());
});
});
describe('NetworkDispatcher', () => {
const requestWillBeSentEvent = {requestId: 'mockId', request: {url: 'example.com'}} as
Protocol.Network.RequestWillBeSentEvent;
const loadingFinishedEvent = {requestId: 'mockId', timestamp: 42, encodedDataLength: 42} as
Protocol.Network.LoadingFinishedEvent;
describeWithEnvironment('request', () => {
let networkDispatcher: SDK.NetworkManager.NetworkDispatcher;
beforeEach(() => {
const networkManager: Common.ObjectWrapper.ObjectWrapper<unknown>&{target?: () => void} =
new Common.ObjectWrapper.ObjectWrapper();
networkManager.target = () => ({
model: () => null,
});
networkDispatcher = new SDK.NetworkManager.NetworkDispatcher(networkManager as SDK.NetworkManager.NetworkManager);
});
it('is preserved after loadingFinished', () => {
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.loadingFinished(loadingFinishedEvent);
assert.exists(networkDispatcher.requestForId('mockId'));
});
it('clears finished requests on clearRequests()', () => {
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.loadingFinished(loadingFinishedEvent);
const unfinishedRequestWillBeSentEvent = {requestId: 'unfinishedRequestId', request: {url: 'example.com'}} as
Protocol.Network.RequestWillBeSentEvent;
networkDispatcher.requestWillBeSent(unfinishedRequestWillBeSentEvent);
networkDispatcher.clearRequests();
assert.notExists(networkDispatcher.requestForId('mockId'));
assert.exists(networkDispatcher.requestForId('unfinishedRequestId'));
});
it('preserves extra info for unfinished clearRequests()', () => {
const requestWillBeSentExtraInfoEvent = {
requestId: 'mockId',
associatedCookies: [],
headers: {'Header-From-Extra-Info': 'foo'},
connectTiming: {requestTime: 0},
} as unknown as Protocol.Network.RequestWillBeSentExtraInfoEvent;
networkDispatcher.requestWillBeSentExtraInfo(requestWillBeSentExtraInfoEvent);
networkDispatcher.clearRequests();
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
assert.exists(networkDispatcher.requestForId('mockId'));
assert.deepEqual(
networkDispatcher.requestForId('mockId')?.requestHeaders(), [{name: 'Header-From-Extra-Info', value: 'foo'}]);
});
it('response headers are overwritten by request interception', () => {
const responseReceivedExtraInfoEvent = {
requestId: 'mockId' as Protocol.Network.RequestId,
blockedCookies: [],
headers: {
'test-header': 'first',
} as Protocol.Network.Headers,
resourceIPAddressSpace: Protocol.Network.IPAddressSpace.Public,
statusCode: 200,
} as Protocol.Network.ResponseReceivedExtraInfoEvent;
const mockResponseReceivedEventWithHeaders = (headers: Protocol.Network.Headers) => {
return {
requestId: 'mockId',
loaderId: 'mockLoaderId',
frameId: 'mockFrameId',
timestamp: 581734.083213,
type: Protocol.Network.ResourceType.Document,
response: {
url: 'example.com',
status: 200,
statusText: '',
headers,
mimeType: 'text/html',
connectionReused: true,
connectionId: 12345,
encodedDataLength: 100,
securityState: 'secure',
} as Protocol.Network.Response,
} as Protocol.Network.ResponseReceivedEvent;
};
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.responseReceivedExtraInfo(responseReceivedExtraInfoEvent);
// ResponseReceived does not overwrite response headers.
networkDispatcher.responseReceived(mockResponseReceivedEventWithHeaders({'test-header': 'second'}));
assert.deepEqual(
networkDispatcher.requestForId('mockId')?.responseHeaders, [{name: 'test-header', value: 'first'}]);
// ResponseReceived does overwrite response headers if request is marked as intercepted.
SDK.NetworkManager.MultitargetNetworkManager.instance().dispatchEventToListeners(
SDK.NetworkManager.MultitargetNetworkManager.Events.REQUEST_INTERCEPTED, 'mockId');
networkDispatcher.responseReceived(mockResponseReceivedEventWithHeaders({'test-header': 'third'}));
assert.deepEqual(
networkDispatcher.requestForId('mockId')?.responseHeaders, [{name: 'test-header', value: 'third'}]);
});
it('has populated \'originalHeaders\' after receiving \'responseReceivedExtraInfo\'', () => {
const responseReceivedExtraInfoEvent = {
requestId: 'mockId' as Protocol.Network.RequestId,
blockedCookies: [],
headers: {
'test-header': 'first',
'set-cookie': 'foo=bar\ncolor=green',
} as Protocol.Network.Headers,
resourceIPAddressSpace: Protocol.Network.IPAddressSpace.Public,
statusCode: 200,
} as Protocol.Network.ResponseReceivedExtraInfoEvent;
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.responseReceivedExtraInfo(responseReceivedExtraInfoEvent);
assert.deepEqual(networkDispatcher.requestForId('mockId')?.responseHeaders, [
{name: 'test-header', value: 'first'},
{name: 'set-cookie', value: 'foo=bar'},
{name: 'set-cookie', value: 'color=green'},
]);
});
it('Correctly set early hints properties on receivedResponse event', () => {
const responseReceivedEvent = {
requestId: 'mockId',
loaderId: 'mockLoaderId',
frameId: 'mockFrameId',
timestamp: 581734.083213,
type: Protocol.Network.ResourceType.Document,
response: {
url: 'example.com',
status: 200,
statusText: '',
headers: {
'test-header': 'first',
} as Protocol.Network.Headers,
mimeType: 'text/html',
connectionReused: true,
connectionId: 12345,
encodedDataLength: 100,
securityState: 'secure',
fromEarlyHints: true,
} as Protocol.Network.Response,
} as Protocol.Network.ResponseReceivedEvent;
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.responseReceived(responseReceivedEvent);
assert.isTrue(networkDispatcher.requestForId('mockId')?.fromEarlyHints());
});
it('has populated early hints headers after receiving \'repsonseReceivedEarlyHints\'', () => {
const earlyHintsEvent = {
requestId: 'mockId' as Protocol.Network.RequestId,
headers: {
link: '</style.css>; as=style;',
} as Protocol.Network.Headers,
};
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.loadingFinished(loadingFinishedEvent);
networkDispatcher.responseReceivedEarlyHints(earlyHintsEvent);
assert.deepEqual(networkDispatcher.requestForId('mockId')?.earlyHintsHeaders, [
{name: 'link', value: '</style.css>; as=style;'},
]);
});
});
describeWithEnvironment('WebBundle requests', () => {
let networkDispatcher: SDK.NetworkManager.NetworkDispatcher;
const webBundleMetadataReceivedEvent = {requestId: 'mockId', urls: ['foo']} as
Protocol.Network.SubresourceWebBundleMetadataReceivedEvent;
const webBundleInnerResponseParsedEvent = {bundleRequestId: 'bundleRequestId', innerRequestId: 'mockId'} as
Protocol.Network.SubresourceWebBundleInnerResponseParsedEvent;
const resourceUrlsFoo = ['foo'] as Platform.DevToolsPath.UrlString[];
beforeEach(() => {
const networkManager = new Common.ObjectWrapper.ObjectWrapper();
networkDispatcher = new SDK.NetworkManager.NetworkDispatcher(networkManager as SDK.NetworkManager.NetworkManager);
});
it('have webbundle info when webbundle event happen between browser events', () => {
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.subresourceWebBundleMetadataReceived(webBundleMetadataReceivedEvent);
networkDispatcher.loadingFinished(loadingFinishedEvent);
assert.deepEqual(networkDispatcher.requestForId('mockId')?.webBundleInfo()?.resourceUrls, resourceUrlsFoo);
});
it('have webbundle info when webbundle event happen before browser events', () => {
networkDispatcher.subresourceWebBundleMetadataReceived(webBundleMetadataReceivedEvent);
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.loadingFinished(loadingFinishedEvent);
assert.deepEqual(networkDispatcher.requestForId('mockId')?.webBundleInfo()?.resourceUrls, resourceUrlsFoo);
});
it('have webbundle info when webbundle event happen after browser events', () => {
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.loadingFinished(loadingFinishedEvent);
networkDispatcher.subresourceWebBundleMetadataReceived(webBundleMetadataReceivedEvent);
assert.deepEqual(networkDispatcher.requestForId('mockId')?.webBundleInfo()?.resourceUrls, resourceUrlsFoo);
});
it('have webbundle info only for the final request but nor redirect', () => {
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.requestWillBeSent(
{requestId: 'mockId', request: {url: 'redirect.example.com'}, redirectResponse: {url: 'example.com'}} as
Protocol.Network.RequestWillBeSentEvent);
networkDispatcher.subresourceWebBundleMetadataReceived(webBundleMetadataReceivedEvent);
networkDispatcher.loadingFinished(loadingFinishedEvent);
assert.deepEqual(networkDispatcher.requestForId('mockId')?.webBundleInfo()?.resourceUrls, resourceUrlsFoo);
assert.exists(networkDispatcher.requestForId('mockId')?.redirectSource());
assert.notExists(networkDispatcher.requestForId('mockId')?.redirectSource()?.webBundleInfo());
});
it('have webbundle info on error', () => {
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.loadingFinished(loadingFinishedEvent);
networkDispatcher.subresourceWebBundleMetadataError(
{requestId: 'mockId', errorMessage: 'Kaboom!'} as Protocol.Network.SubresourceWebBundleMetadataErrorEvent);
assert.deepEqual(networkDispatcher.requestForId('mockId')?.webBundleInfo()?.errorMessage, 'Kaboom!');
});
it('have webbundle inner request info when webbundle event happen between browser events', () => {
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.subresourceWebBundleInnerResponseParsed(webBundleInnerResponseParsedEvent);
networkDispatcher.loadingFinished(loadingFinishedEvent);
assert.deepEqual(
networkDispatcher.requestForId('mockId')?.webBundleInnerRequestInfo()?.bundleRequestId, 'bundleRequestId');
});
it('have webbundle inner request info when webbundle event happen before browser events', () => {
networkDispatcher.subresourceWebBundleInnerResponseParsed(webBundleInnerResponseParsedEvent);
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.loadingFinished(loadingFinishedEvent);
assert.deepEqual(
networkDispatcher.requestForId('mockId')?.webBundleInnerRequestInfo()?.bundleRequestId, 'bundleRequestId');
});
it('have webbundle inner request info when webbundle event happen after browser events', () => {
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.loadingFinished(loadingFinishedEvent);
networkDispatcher.subresourceWebBundleInnerResponseParsed(webBundleInnerResponseParsedEvent);
assert.deepEqual(
networkDispatcher.requestForId('mockId')?.webBundleInnerRequestInfo()?.bundleRequestId, 'bundleRequestId');
});
it('have webbundle inner request info on error', () => {
networkDispatcher.requestWillBeSent(requestWillBeSentEvent);
networkDispatcher.loadingFinished(loadingFinishedEvent);
networkDispatcher.subresourceWebBundleInnerResponseError(
{innerRequestId: 'mockId', errorMessage: 'Kaboom!'} as
Protocol.Network.SubresourceWebBundleInnerResponseErrorEvent);
assert.deepEqual(networkDispatcher.requestForId('mockId')?.webBundleInnerRequestInfo()?.errorMessage, 'Kaboom!');
});
});
});
interface OverriddenResponse {
requestId: Protocol.Fetch.RequestId;
responseCode: number;
body: string;
responseHeaders: Protocol.Fetch.HeaderEntry[];
}
describeWithMockConnection('InterceptedRequest', () => {
let target: SDK.Target.Target;
let fulfillRequestSpy: sinon.SinonSpy;
async function checkRequestOverride(
target: SDK.Target.Target, request: Protocol.Network.Request, requestId: Protocol.Fetch.RequestId,
responseStatusCode: number, responseHeaders: Protocol.Fetch.HeaderEntry[], responseBody: string,
expectedOverriddenResponse: OverriddenResponse, expectedSetCookieHeaders: Protocol.Fetch.HeaderEntry[] = []) {
const multitargetNetworkManager = SDK.NetworkManager.MultitargetNetworkManager.instance();
const fetchAgent = target.fetchAgent();
const fulfilledRequest = new Promise(resolve => {
multitargetNetworkManager.addEventListener(
SDK.NetworkManager.MultitargetNetworkManager.Events.REQUEST_FULFILLED, resolve);
});
const networkRequest = SDK.NetworkRequest.NetworkRequest.create(
requestId as unknown as Protocol.Network.RequestId, urlString`${request.url}`, urlString`${request.url}`, null,
null, null);
networkRequest.originalResponseHeaders = responseHeaders;
// The response headers passed to 'interceptedRequest' do not contain any
// 'set-cookie' headers, because they originate from CDP's 'Fetch.requestPaused'
// which receives its header information via mojo which in turn filters out
// 'set-cookie' headers.
const filteredResponseHeaders = responseHeaders.filter(header => header.name !== 'set-cookie');
const interceptedRequest = new SDK.NetworkManager.InterceptedRequest(
fetchAgent, request, Protocol.Network.ResourceType.Document, requestId, networkRequest, responseStatusCode,
filteredResponseHeaders);
interceptedRequest.responseBody = async () => {
return new TextUtils.ContentData.ContentData(responseBody, false, 'text/html');
};
assert.isTrue(fulfillRequestSpy.notCalled);
await multitargetNetworkManager.requestIntercepted(interceptedRequest);
await fulfilledRequest;
assert.isTrue(fulfillRequestSpy.calledOnceWithExactly(expectedOverriddenResponse));
assert.deepEqual(networkRequest.setCookieHeaders, expectedSetCookieHeaders);
fulfillRequestSpy.resetHistory();
}
async function checkSetCookieOverride(
url: string, headersFromServer: Protocol.Fetch.HeaderEntry[],
expectedOverriddenHeaders: Protocol.Fetch.HeaderEntry[],
expectedPersistedSetCookieHeaders: Protocol.Fetch.HeaderEntry[]): Promise<void> {
const responseCode = 200;
const requestId = 'request_id_for_cookies' as Protocol.Fetch.RequestId;
const responseBody = 'interceptedRequest content';
const networkRequest = {
method: 'GET',
url,
} as Protocol.Network.Request;
await checkRequestOverride(
target, networkRequest, requestId, responseCode, headersFromServer, responseBody, {
requestId,
responseCode,
body: btoa(responseBody),
responseHeaders: expectedOverriddenHeaders,
},
expectedPersistedSetCookieHeaders);
}
beforeEach(async () => {
SDK.NetworkManager.MultitargetNetworkManager.dispose();
target = createTarget();
const networkPersistenceManager = await createWorkspaceProject(urlString`file:///path/to/overrides`, [
{
name: '.headers',
path: 'www.example.com/',
content: `[
{
"applyTo": "index.html",
"headers": [{
"name": "index-only",
"value": "only added to index.html"
}]
},
{
"applyTo": "*.css",
"headers": [{
"name": "css-only",
"value": "only added to css files"
}]
},
{
"applyTo": "path/to/*.js",
"headers": [{
"name": "another-header",
"value": "only added to specific path"
}]
},
{
"applyTo": "withCookie.html",
"headers": [{
"name": "set-cookie",
"value": "userId=12345"
}]
},
{
"applyTo": "withCookie2.html",
"headers": [
{
"name": "set-cookie",
"value": "userName=DevTools"
},
{
"name": "set-cookie",
"value": "themeColour=dark"
}
]
},
{
"applyTo": "withCookie3.html",
"headers": [
{
"name": "set-cookie",
"value": "userName=DevTools"
},
{
"name": "set-cookie",
"value": "malformed_override"
}
]
},
{
"applyTo": "cookies/*",
"headers": [
{
"name": "set-cookie",
"value": "unique=value"
},
{
"name": "set-cookie",
"value": "override-me=first"
}
]
},
{
"applyTo": "cookies/mergeCookies.html",
"headers": [
{
"name": "set-cookie",
"value": "override-me=second"
},
{
"name": "set-cookie",
"value": "foo=bar"
}
]
}
]`,
},
{
name: '.headers',
path: '',
content: `[
{
"applyTo": "*",
"headers": [{
"name": "age",
"value": "overridden"
}]
}
]`,
},
{name: 'helloWorld.html', path: 'www.example.com/', content: 'Hello World!'},
{name: 'utf16.html', path: 'www.example.com/', content: 'Overwritten with non-UTF16 (TODO: fix this!)'},
{name: 'something.html', path: 'file:/usr/local/foo/content/', content: 'Override for something'},
{
name: '.headers',
path: 'file:/usr/local/example/',
content: `[
{
"applyTo": "*",
"headers": [{
"name": "test-file-urls",
"value": "file url value"
}]
}
]`,
},
{name: 'index.html', path: 'file:/usr/local/example/', content: 'Overridden file content'},
{
name: '.headers',
path: 'www.longurl.com/longurls/',
content: `[
{
"applyTo": "index.html-${
Platform.StringUtilities.hashCode('www.longurl.com/' + LONG_URL_PART).toString(16)}.html",
"headers": [{
"name": "long-url-header",
"value": "long url header value"
}]
}
]`,
},
{
name: `index.html-${Platform.StringUtilities.hashCode('www.longurl.com/' + LONG_URL_PART).toString(16)}.html`,
path: 'www.longurl.com/longurls/',
content: 'Overridden long URL file content',
},
{
name: '.headers',
path: 'file:/longurls/',
content: `[
{
"applyTo": "index.html-${
Platform.StringUtilities
.hashCode(
Persistence.NetworkPersistenceManager.NetworkPersistenceManager.encodeEncodedPathToLocalPathParts(
'file:' as Platform.DevToolsPath.EncodedPathString)[0] +
'/' + LONG_URL_PART)
.toString(16)}.html",
"headers": [{
"name": "long-file-url-header",
"value": "long file url header value"
}]
}
]`,
},
]);
sinon.stub(target.fetchAgent(), 'invoke_enable');
fulfillRequestSpy = sinon.spy(target.fetchAgent(), 'invoke_fulfillRequest');
await networkPersistenceManager.updateInterceptionPatternsForTests();
});
it('can override headers-only for a status 200 request', async () => {
const responseCode = 200;
const requestId = 'request_id_1' as Protocol.Fetch.RequestId;
const responseBody = 'interceptedRequest content';
await checkRequestOverride(
target, {
method: 'GET',
url: 'https://www.example.com/styles.css',
} as Protocol.Network.Request,
requestId, responseCode, [{name: 'content-type', value: 'text/html; charset=utf-8'}], responseBody, {
requestId,
responseCode,
body: btoa(responseBody),
responseHeaders: [
{name: 'css-only', value: 'only added to css files'},
{name: 'age', value: 'overridden'},
{name: 'content-type', value: 'text/html; charset=utf-8'},
],
});
});
it('does not intercept OPTIONS requests', async () => {
const requestId = 'request_id_1' as Protocol.Fetch.RequestId;
const request = {
method: 'OPTIONS',
url: 'https://www.example.com/styles.css',
} as Protocol.Network.Request;
const fetchAgent = target.fetchAgent();
const continueRequestSpy = sinon.spy(fetchAgent, 'invoke_continueRequest');
const networkRequest = SDK.NetworkRequest.NetworkRequest.create(
requestId as unknown as Protocol.Network.RequestId, urlString`${request.url}`, urlString`${request.url}`, null,
null, null);
const interceptedRequest = new SDK.NetworkManager.InterceptedRequest(
fetchAgent, request, Protocol.Network.ResourceType.Document, requestId, networkRequest);
interceptedRequest.responseBody = async () => {
return new TextUtils.ContentData.ContentData('interceptedRequest content', false, 'text/html');
};
assert.isTrue(continueRequestSpy.notCalled);
await SDK.NetworkManager.MultitargetNetworkManager.instance().requestIntercepted(interceptedRequest);
assert.isTrue(fulfillRequestSpy.notCalled);
assert.isTrue(continueRequestSpy.calledOnce);
});
it('can override headers and content for a status 200 request', async () => {
const responseCode = 200;
const requestId = 'request_id_2' as Protocol.Fetch.RequestId;
const responseBody = 'interceptedRequest content';
await checkRequestOverride(
target, {
method: 'GET',
url: 'https://www.example.com/helloWorld.html',
} as Protocol.Network.Request,
requestId, responseCode, [{name: 'content-type', value: 'text/html; charset=utf-8'}], responseBody, {
requestId,
responseCode,
body: btoa('Hello World!'),
responseHeaders: [
{name: 'age', value: 'overridden'},
{name: 'content-type', value: 'text/html; charset=utf-8'},
],
});
});
describe('NetworkPersistenceManager', () => {
it('decodes the intercepted response body with the right charset', async () => {
const requestId = 'request_id_utf_16' as Protocol.Fetch.RequestId;
const request = {
method: 'GET',
url: 'https://www.example.com/utf16.html',
} as Protocol.Network.Request;
const fetchAgent = target.fetchAgent();
sinon.spy(fetchAgent, 'invoke_continueRequest');
const networkRequest = SDK.NetworkRequest.NetworkRequest.create(
requestId as unknown as Protocol.Network.RequestId, urlString`${request.url}`, urlString`${request.url}`,
null, null, null);
networkRequest.originalResponseHeaders = [{name: 'content-type', value: 'text/html; charset-utf-16'}];
// Create a quick'n dirty network UISourceCode for the request manually. We need to establish a binding to the
// overridden file system UISourceCode.
const networkProject = new Bindings.ContentProviderBasedProject.ContentProviderBasedProject(
Workspace.Workspace.WorkspaceImpl.instance(), 'testing-network', Workspace.Workspace.projectTypes.Network,
'Override network project', false);
Workspace.Workspace.WorkspaceImpl.instance().addProject(networkProject);
const uiSourceCode = networkProject.createUISourceCode(
urlString`https://www.example.com/utf16.html`, Common.ResourceType.resourceTypes.Document);
networkProject.addUISourceCode(uiSourceCode);
const interceptedRequest = new SDK.NetworkManager.InterceptedRequest(
fetchAgent, request, Protocol.Network.ResourceType.Document, requestId, networkRequest, 200,
[{name: 'content-type', value: 'text/html; charset-utf-16'}]);
interceptedRequest.responseBody = async () => {
// Very simple HTML doc base64 encoded.
return new TextUtils.ContentData.ContentData(
'//48ACEARABPAEMAVABZAFAARQAgAGgAdABtAGwAPgAKADwAcAA+AEkA8QB0AOsAcgBuAOIAdABpAPQAbgDgAGwAaQB6AOYAdABpAPgAbgADJjTYBt88AC8AcAA+AAoA',
true, 'text/html', 'utf-16');
};
await SDK.NetworkManager.MultitargetNetworkManager.instance().requestIntercepted(interceptedRequest);
const content = await Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance()
.originalContentForUISourceCode(uiSourceCode);
assert.strictEqual(content, '<!DOCTYPE html>\n<p>Iñtërnâtiônàlizætiøn☃𝌆</p>\n');
});
});
it('can override headers-only for a status 300 (redirect) request', async () => {
const responseCode = 300;
const requestId = 'request_id_3' as Protocol.Fetch.RequestId;
const responseBody = 'interceptedRequest content';
await checkRequestOverride(
target, {
method: 'GET',
url: 'https://www.example.com/path/to/foo.js',
} as Protocol.Network.Request,
requestId, responseCode, [{name: 'content-type', value: 'text/html; charset=utf-8'}], responseBody, {
requestId,
responseCode,
body: '',
responseHeaders: [
{name: 'another-header', value: 'only added to specific path'},
{name: 'age', value: 'overridden'},
{name: 'content-type', value: 'text/html; charset=utf-8'},
],
});
});
it('can override headers and content for a status 300 (redirect) request', async () => {
const responseCode = 300;
const requestId = 'request_id_4' as Protocol.Fetch.RequestId;
const responseBody = 'interceptedRequest content';
await checkRequestOverride(
target, {
method: 'GET',
url: 'https://www.example.com/helloWorld.html',
} as Protocol.Network.Request,
requestId, responseCode, [{name: 'content-type', value: 'text/html; charset=utf-8'}], responseBody, {
requestId,
responseCode: 200,
body: btoa('Hello World!'),
responseHeaders: [
{name: 'age', value: 'overridden'},
{name: 'content-type', value: 'text/html; charset=utf-8'},
],
});
});
it('can override headers-only for a status 404 (not found) request', async () => {
const responseCode = 404;
const requestId = 'request_id_5' as Protocol.Fetch.RequestId;
const responseBody = 'interceptedRequest content';
await checkRequestOverride(
target, {
method: 'GET',
url: 'https://www.example.com/doesNotExist.html',
} as Protocol.Network.Request,
requestId, responseCode, [{name: 'content-type', value: 'text/html; charset=utf-8'}], responseBody, {
requestId,
responseCode,
body: btoa(responseBody),
responseHeaders: [
{name: 'age', value: 'overridden'},
{name: 'content-type', value: 'text/html; charset=utf-8'},
],
});
});
it('can override headers and content for a status 404 (not found) request', async () => {
const responseCode = 404;
const requestId = 'request_id_6' as Protocol.Fetch.RequestId;
const responseBody = 'interceptedRequest content';
await checkRequestOverride(
target, {
method: 'GET',
url: 'https://www.example.com/helloWorld.html',
} as Protocol.Network.Request,
requestId, responseCode, [{name: 'content-type', value: 'text/html; charset=utf-8'}], responseBody, {
requestId,
responseCode: 200,
body: btoa('Hello World!'),
responseHeaders: [
{name: 'age', value: 'overridden'},
{name: 'content-type', value: 'text/html; charset=utf-8'},
],
});
});
it('can override headers and content for a request with a \'file:/\'-URL', async () => {
const responseCode = 200;
const requestId = 'request_id_8' as Protocol.Fetch.RequestId;
const responseBody = 'interceptedRequest content';
await checkRequestOverride(
target, {
method: 'GET',
url: 'file:///usr/local/example/index.html',
} as Protocol.Network.Request,
requestId, responseCode,
[
{name: 'content-type', value: 'text/html; charset=utf-8'},
{name: 'age', value: 'original'},
],
responseBody, {
requestId,
responseCode,
body: btoa('Overridden file content'),
responseHeaders: [
{name: 'test-file-urls', value: 'file url value'},
{name: 'age', value: 'overridden'},
{name: 'content-type', value: 'text/html; charset=utf-8'},
],
});
});
it('can apply global header overrides to a request with a \'file:/\'-URL', async () => {
const responseCode = 200;
const requestId = 'request_id_9' as Protocol.Fetch.RequestId;
const responseBody = 'content of something/index.html';
await checkRequestOverride(
target, {
method: 'GET',
url: 'file:///usr/local/whatever/index.html',
} as Protocol.Network.Request,
requestId, responseCode,
[
{name: 'content-type', value: 'text/html; charset=utf-8'},
{name: 'age', value: 'original'},
],
responseBody, {
requestId,
responseCode,
body: btoa(responseBody),
responseHeaders: [
{name: 'age', value: 'overridden'},
{name: 'content-type', value: 'text/html; charset=utf-8'},
],
});
});
it('can override headers and content for a request with a very long URL', async () => {
const responseCode = 200;
const requestId = 'request_id_10' as Protocol.Fetch.RequestId;
const responseBody = 'interceptedRequest content';
await checkRequestOverride(
target, {
method: 'GET',
url: `https://www.longurl.com/${LONG_URL_PART}`,
} as Protocol.Network.Request,
requestId, responseCode,
[
{name: 'content-type', value: 'text/html; charset=utf-8'},
{name: 'age', value: 'original'},
],
responseBody, {
requestId,
responseCode,
body: btoa('Overridden long URL file content'),
responseHeaders: [
{name: 'long-url-header', value: 'long url header value'},
{name: 'age', value: 'overridden'},
{name: 'content-type', value: 'text/html; charset=utf-8'},
],
});
});
it('can override headers for a request with a very long \'file:/\'-URL', async () => {
const responseCode = 200;
const requestId = 'request_id_11' as Protocol.Fetch.RequestId;
const responseBody = 'interceptedRequest content';
await checkRequestOverride(
target, {
method: 'GET',
url: 'file:///' + LONG_URL_PART,
} as Protocol.Network.Request,
requestId, responseCode,
[
{name: 'content-type', value: 'text/html; charset=utf-8'},
{name: 'age', value: 'original'},
],
responseBody, {
requestId,
responseCode,
body: btoa(responseBody),
responseHeaders: [
{name: 'long-file-url-header', value: 'long file url header value'},
{name: 'age', value: 'overridden'},
{name: 'content-type', value: 'text/html; charset=utf-8'},
],
});
});
it('can override \'set-cookie\' headers', async () => {
const headersFromServer = [{name: 'content-type', value: 'text/html; charset=utf-8'}];
const expectedOverriddenHeaders = [
{name: 'age', value: 'overridden'},
{name: 'content-type', value: 'text/html; charset=utf-8'},
{name: 'set-cookie', value: 'userId=12345'},
];
const expectedPersistedSetCookieHeaders = [{name: 'set-cookie', value: 'userId=12345'}];
await checkSetCookieOverride(
'https://www.example.com/withCookie.html', headersFromServer, expectedOverriddenHeaders,
expectedPersistedSetCookieHeaders);
});
it('marks both requests as overridden when there are 2 requests with the same URL', async () => {
const responseCode = 200;
const requestId1 = 'request_id_1' as Protocol.Fetch.RequestId;
const requestId2 = 'request_id_2' as Protocol.Fetch.RequestId;
const body = 'interceptedRequest content';
const request = {
method: 'GET',
url: 'https://www.example.com/styles.css',
} as Protocol.Network.Request;
const originalResponseHeaders = [{name: 'content-type', value: 'text/html; charset=utf-8'}];
const responseHeaders = [
{name: 'css-only', value: 'only added to css files'},
{name: 'age', value: 'overridden'},
{name: 'content-type', value: 'text/html; charset=utf-8'},
];
const {dispatcher} = target.model(SDK.NetworkManager.NetworkManager)!;
dispatcher.requestWillBeSent({requestId: requestId1 as string, request} as Protocol.Network.RequestWillBeSentEvent);
dispatcher.requestWillBeSent({requestId: requestId2 as string, request} as Protocol.Network.RequestWillBeSentEvent);
await checkRequestOverride(target, request, requestId1, responseCode, originalResponseHeaders, body, {
requestId: requestId1,
responseCode,
body: btoa(body),
responseHeaders,
});
await checkRequestOverride(target, request, requestId2, responseCode, originalResponseHeaders, body, {
requestId: requestId2,
responseCode,
body: btoa(body),
responseHeaders,
});
assert.isTrue(dispatcher.requestForId(requestId1)?.wasIntercepted());
assert.isTrue(dispatcher.requestForId(requestId2)?.wasIntercepted());
});
it('stores \'set-cookie\' headers on the request', async () => {
const headersFromServer = [{name: 'set-cookie', value: 'foo=bar'}];
const expectedOverriddenHeaders = [
{name: 'age', value: 'overridden'},
];
const expectedPersistedSetCookieHeaders = [{name: 'set-cookie', value: 'foo=bar'}];
await checkSetCookieOverride(
'https://www.example.com/noCookie.html', headersFromServer, expectedOverriddenHeaders,
expectedPersistedSetCookieHeaders);
});
it('can override \'set-cookie\' headers when there server also sends \'set-cookie\' headers', async () => {
const headersFromServer = [{name: 'set-cookie', value: 'foo=bar'}];
const expectedOverriddenHeaders = [
{name: 'age', value: 'overridden'},
{name: 'set-cookie', value: 'userId=12345'},
];
const expectedPersistedSetCookieHeaders =
[{name: 'set-cookie', value: 'foo=bar'}, {name: 'set-cookie', value: 'userId=12345'}];
await checkSetCookieOverride(
'https://www.example.com/withCookie.html', headersFromServer, expectedOverriddenHeaders,
expectedPersistedSetCookieHeaders);
});
it('can overwrite a cookie value from server with a cookie value from overrides', async () => {
const headersFromServer = [{name: 'set-cookie', value: 'userId=999'}];
const expectedOverriddenHeaders = [
{name: 'age', value: 'overridden'},
{name: 'set-cookie', value: 'userId=12345'},
];
const expectedPersistedSetCookieHeaders = [{name: 'set-cookie', value: 'userId=12345'}];
await checkSetCookieOverride(
'https://www.example.com/withCookie.html', headersFromServer, expectedOverriddenHeaders,
expectedPersistedSetCookieHeaders);
});
it('correctly merges cookies from server and from overrides', async () => {
const headersFromServer = [
{name: 'set-cookie', value: 'foo=bar'},
{name: 'set-cookie', value: 'userName=server'},
];
const expectedOverriddenHeaders = [
{name: 'age', value: 'overridden'},
{name: 'set-cookie', value: 'userName=DevTools'},
{name: 'set-cookie', value: 'themeColour=dark'},
];
const expectedPersistedSetCookieHeaders = [
{name: 'set-cookie', value: 'foo=bar'},
{name: 'set-cookie', value: 'userName=DevTools'},
{name: 'set-cookie', value: 'themeColour=dark'},
];
await checkSetCookieOverride(
'https://www.example.com/withCookie2.html', headersFromServer, expectedOverriddenHeaders,
expectedPersistedSetCookieHeaders);
});
it('correctly merges malformed cookies from server and from overrides', async () => {
const headersFromServer = [
{name: 'set-cookie', value: 'malformed_original'},
{name: 'set-cookie', value: 'userName=server'},
];
const expectedOverriddenHeaders = [
{name: 'age', value: 'overridden'},
{name: 'set-cookie', value: 'userName=DevTools'},
{name: 'set-cookie', value: 'malformed_override'},
];
const expectedPersistedSetCookieHeaders = [
{name: 'set-cookie', value: 'malformed_original'},
{name: 'set-cookie', value: 'userName=DevTools'},
{name: 'set-cookie', value: 'malformed_override'},
];
await checkSetCookieOverride(
'https://www.example.com/withCookie3.html', headersFromServer, expectedOverriddenHeaders,
expectedPersistedSetCookieHeaders);
});
it('correctly merges \'set-cookie\' headers from server with multiple defined overrides', async () => {
const headersFromServer = [
{name: 'set-cookie', value: 'userName=server'},
{name: 'set-cookie', value: 'override-me=zero'},
];
const expectedOverriddenHeaders = [
{name: 'age', value: 'overridden'},
{name: 'set-cookie', value: 'unique=value'},
{name: 'set-cookie', value: 'override-me=second'},
{name: 'set-cookie', value: 'foo=bar'},
];
const expectedPersistedSetCookieHeaders = [
{name: 'set-cookie', value: 'userName=server'},
{name: 'set-cookie', value: 'override-me=second'},
{name: 'set-cookie', value: 'unique=value'},
{name: 'set-cookie', value: 'foo=bar'},
];
await checkSetCookieOverride(
'https://www.example.com/cookies/mergeCookies.html', headersFromServer, expectedOverriddenHeaders,
expectedPersistedSetCookieHeaders);
});
it('correctly merges \'set-cookie\' headers with duplicates', () => {
const original = [
{name: 'set-cookie', value: 'foo=original'},
{name: 'set-cookie', value: 'bar=original'},
{name: 'set-cookie', value: 'baz=original'},
{name: 'set-cookie', value: 'duplicate=duplicate'},
{name: 'set-cookie', value: 'duplicate=duplicate'},
{name: 'set-cookie', value: 'duplicate2=duplicate2'},
{name: 'set-cookie', value: 'duplicate2=duplicate2'},
{name: 'set-cookie', value: 'duplicate3=duplicate3'},
{name: 'set-cookie', value: 'duplicate3=duplicate3'},
{name: 'set-cookie', value: 'malformed'},
{name: 'set-cookie', value: 'both'},
{name: 'set-cookie', value: 'double'},
{name: 'set-cookie', value: 'double'},
{name: 'set-cookie', value: 'original_duplicate'},
{name: 'set-cookie', value: 'original_duplicate'},
{name: 'set-cookie', value: 'override_duplicate'},
];
const overrides = [
{name: 'set-cookie', value: 'bar=overridden'},
{name: 'set-cookie', value: 'baz=overridden1'},
{name: 'set-cookie', value: 'baz=overridden2'},
{name: 'set-cookie', value: 'duplicate2=overridden'},
{name: 'set-cookie', value: 'duplicate3=overridden'},
{name: 'set-cookie', value: 'duplicate3=overridden'},
{name: 'set-cookie', value: 'malformed_override'},
{name: 'set-cookie', value: 'both'},
{name: 'set-cookie', value: 'original_duplicate'},
{name: 'set-cookie', value: 'override_duplicate'},
{name: 'set-cookie', value: 'override_duplicate'},
];
const expected = [
{name: 'set-cookie', value: 'foo=original'},
{name: 'set-cookie', value: 'bar=overridden'},
{name: 'set-cookie', value: 'baz=overridden1'},
{name: 'set-cookie', value: 'baz=overridden2'},
{name: 'set-cookie', value: 'duplicate=duplicate'},
{name: 'set-cookie', value: 'duplicate=duplicate'},
{name: 'set-cookie', value: 'duplicate2=overridden'},
{name: 'set-cookie', value: 'duplicate3=overridden'},
{name: 'set-cookie', value: 'duplicate3=overridden'},
{name: 'set-cookie', value: 'malformed'},
{name: 'set-cookie', value: 'both'},
{name: 'set-cookie', value: 'double'},
{name: 'set-cookie', value: 'double'},
{name: 'set-cookie', value: 'original_duplicate'},
{name: 'set-cookie', value: 'override_duplicate'},
{name: 'set-cookie', value: 'override_duplicate'},
{name: 'set-cookie', value: 'malformed_override'},
];
assert.deepEqual(SDK.NetworkManager.InterceptedRequest.mergeSetCookieHeaders(original, overrides), expected);
});
});