Commit 4d76cc4b authored by Tsuyoshi Horo's avatar Tsuyoshi Horo Committed by Commit Bot

Check "Bypass for network" flag in DocumentThreadableLoader

There is a bug that CORS requests can't work at all when "Bypass for network"
in DevTools is enabled.

Currently the CORS logic is implemented in DocumentThreadableLoader in the
renderer process. If the page is not controlled by a service worker,
DocumentThreadableLoader sets the Origin header for the CORS request and sends
the request to the browser process. And the browser process sends the request to
the network server.

But if the page is controlled by a service worker, DocumentThreadableLoader just
sends the original request to the browser process. And the browser process sends 
the request to the service worker. If the service worker doesn't call
FetchEvent.respondWith(), the browser process returns a "FallbackRequired"
response to the DocumentThreadableLoader. And DocumentThreadableLoader will
start the CORS logic such as setting the Origin header.

When "Bypass for network" is enabled, InspectorNetworkAgent::WillSendRequest()
sets the request service worker mode to None. So the browser process doesn't
pass the request to the service worker.

But InspectorNetworkAgent::WillSendRequest() is called just before the renderer
process sends the request to the browser process. So the request which should be
sent to the service worker will directly go to the network server.

This CL adds a check of the bypass flag in DocumentThreadableLoader, so that the
CORS logic will be correctly executed.

Bug: 771742
Change-Id: I923fc9f9e2e361aea3d25ace653c138707d9a682
Reviewed-on: https://chromium-review.googlesource.com/717960Reviewed-by: default avatarWill Chen <chenwilliam@chromium.org>
Reviewed-by: default avatarDmitry Gozman <dgozman@chromium.org>
Reviewed-by: default avatarTakeshi Yoshino <tyoshino@chromium.org>
Commit-Queue: Tsuyoshi Horo <horo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#509578}
parent 445fcc90
<script>
function xhr(url) {
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.addEventListener('error',() => { reject(new Error()); });
request.addEventListener('load', (e) => { resolve(request.response); });
request.open('GET', url);
request.send();
});
}
function load_cors_image(url) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
document.body.appendChild(img);
img.onload = () => { resolve(); };
img.onerror = () => { reject(new Error()); };
img.crossOrigin = 'anonymous';
img.src = url;
});
}
</script>
<?php
if ($_SERVER['HTTP_ORIGIN'] !== "http://127.0.0.1:8000") {
header("HTTP/1.1 403 Forbidden");
exit;
}
header("Access-Control-Allow-Origin: http://127.0.0.1:8000");
$type = $_GET["type"];
if ($type === "img") {
$image = "../../../resources/square100.png";
header("Content-Type: image/png");
header("Content-Length: " . filesize($image));
readfile($image);
} else if ($type === "txt") {
header("Content-Type: text/plain");
print("hello");
exit;
}
?>
let requests = [];
self.addEventListener('fetch', (event) => {
requests.push({
url: event.request.url,
mode: event.request.mode
});
});
self.addEventListener('message', (event) => {
event.data.port.postMessage(requests);
requests = [];
});
Tests "Bypass for network" checkbox works with CORS requests. crbug.com/771742
Loading an iframe.
The iframe loaded.
CORS fetch(): 1
CORS XHR: 1
CORS image: 1
Enable bypassServiceWorker
CORS fetch(): 2
CORS XHR: 2
CORS image: 2
Disable bypassServiceWorker
CORS fetch(): 3
CORS XHR: 3
CORS image: 3
Intercepted requests:
url: http://127.0.0.1:8000/devtools/service-workers/resources/service-workers-bypass-for-network-cors-iframe.html
mode: navigate
url: http://localhost:8000/devtools/service-workers/resources/service-workers-bypass-for-network-cors-target.php?type=txt&fetch1
mode: cors
url: http://localhost:8000/devtools/service-workers/resources/service-workers-bypass-for-network-cors-target.php?type=txt&xhr1
mode: cors
url: http://localhost:8000/devtools/service-workers/resources/service-workers-bypass-for-network-cors-target.php?type=img&img1
mode: cors
url: http://localhost:8000/devtools/service-workers/resources/service-workers-bypass-for-network-cors-target.php?type=txt&fetch3
mode: cors
url: http://localhost:8000/devtools/service-workers/resources/service-workers-bypass-for-network-cors-target.php?type=txt&xhr3
mode: cors
url: http://localhost:8000/devtools/service-workers/resources/service-workers-bypass-for-network-cors-target.php?type=img&img3
mode: cors
<html>
<head>
<script src="../../inspector/inspector-test.js"></script>
<script src="../../inspector/service-workers/service-workers-test.js"></script>
<script>
function takeInterceptedRequests(scope) {
return new Promise((resolve) => {
let channel = new MessageChannel();
channel.port1.onmessage = msg => { resolve(JSON.stringify(msg.data)); };
registrations[scope].active.postMessage({port: channel.port2}, [channel.port2]);
});
}
function fetchInIframe(url, frame_id) {
return document.getElementById(frame_id).contentWindow
.fetch(url, {mode: 'cors'}).then((r) => r.text());
}
function xhrInIframe(url, frame_id) {
return document.getElementById(frame_id).contentWindow.xhr(url);
}
function corsImageInIframe(url, frame_id) {
return document.getElementById(frame_id).contentWindow.load_cors_image(url);
}
function test() {
const scriptURL = 'http://127.0.0.1:8000/devtools/service-workers/resources/service-workers-bypass-for-network-cors-worker.js';
const scope = 'http://127.0.0.1:8000/devtools/service-workers/resources/service-workers-bypass-for-network-cors-iframe.html';
const target = 'http://localhost:8000/devtools/service-workers/resources/service-workers-bypass-for-network-cors-target.php';
const frameId = 'frame_id';
function dumpInterceptedRequests() {
return TestRunner.callFunctionInPageAsync('takeInterceptedRequests', [scope])
.then((data) => {
TestRunner.addResult('Intercepted requests:');
JSON.parse(data.value).forEach((request) => {
TestRunner.addResult(' url: ' + request.url);
TestRunner.addResult(' mode: ' + request.mode);
});
});
}
function testCorsRequests(index) {
TestRunner.addResult('CORS fetch(): ' + index);
return TestRunner.callFunctionInPageAsync(
'fetchInIframe', [target + '?type=txt&fetch' + index, frameId])
.then((data) => {
if (data.value !== 'hello') {
TestRunner.addResult('fetch response miss match: ' + data.value);
}
TestRunner.addResult('CORS XHR: ' + index);
return TestRunner.callFunctionInPageAsync(
'xhrInIframe', [target + '?type=txt&xhr' + index, frameId]);
})
.then((data) => {
if (data.value !== 'hello') {
TestRunner.addResult('XHR response miss match: ' + data.value);
}
TestRunner.addResult('CORS image: ' + index);
return TestRunner.callFunctionInPageAsync(
'corsImageInIframe', [target + '?type=img&img' + index, frameId]);
});
}
ApplicationTestRunner.registerServiceWorker(scriptURL, scope)
.then(_ => ApplicationTestRunner.waitForActivated(scope))
.then(() => {
TestRunner.addResult('Loading an iframe.');
return TestRunner.addIframe(scope, {id: frameId});
})
.then(() => {
TestRunner.addResult('The iframe loaded.');
return testCorsRequests('1');
})
.then(() => {
TestRunner.addResult('Enable bypassServiceWorker');
Common.settings.settingForTest('bypassServiceWorker').set(true);
return testCorsRequests('2');
})
.then(() => {
TestRunner.addResult('Disable bypassServiceWorker');
Common.settings.settingForTest('bypassServiceWorker').set(false);
return testCorsRequests('3');
})
.then(() => {
return dumpInterceptedRequests();
})
.then(() => {
TestRunner.completeTest();
});
}
</script>
</head>
<body onload="runTest()">
<p>Tests "Bypass for network" checkbox works with CORS requests. crbug.com/771742</p>
</body>
</html>
......@@ -5,26 +5,22 @@
<script src="../../inspector/resources-test.js"></script>
<script>
function loadIframe()
{
var frame = document.createElement('iframe');
frame.src = "http://localhost:8000/devtools/service-workers/resources/" +
"bypass-for-network-redirect.php"
document.body.appendChild(frame);
}
function test() {
const url = "http://localhost:8000/devtools/service-workers/resources/" +
"bypass-for-network-redirect.php";
const frameId = 'frame_id';
UI.inspectorView.showPanel('sources')
.then(function() {
Common.settings.settingForTest('bypassServiceWorker').set(true);
var callback;
var promise = new Promise((fulfill) => callback = fulfill);
let callback;
const promise = new Promise((fulfill) => callback = fulfill);
ConsoleTestRunner.addConsoleSniffer(message => {
if (message.messageText == 'getRegistration finished') {
callback();
}
}, true);
TestRunner.evaluateInPage('loadIframe()');
TestRunner.addIframe(url, {id: frameId});
return promise;
})
.then(function() {
......
......@@ -5,20 +5,9 @@
<script src="../../inspector/resources-test.js"></script>
<script>
function loadIframe(url)
{
var callback;
var promise = new Promise((fulfill) => callback = fulfill);
var frame = document.createElement('iframe');
frame.src = url;
frame.onload = callback;
document.body.appendChild(frame);
return promise;
}
function test() {
var scriptURL = 'http://127.0.0.1:8000/devtools/service-workers/resources/force-update-on-page-load-worker.php';
var scope = 'http://127.0.0.1:8000/devtools/service-workers/resources/service-worker-force-update-on-page-load/';
const scriptURL = 'http://127.0.0.1:8000/devtools/service-workers/resources/force-update-on-page-load-worker.php';
const scope = 'http://127.0.0.1:8000/devtools/service-workers/resources/service-worker-force-update-on-page-load/';
function waitForWorkerActivated(scope) {
return new Promise(function(resolve) {
......@@ -55,27 +44,27 @@ function test() {
.then(() => waitForWorkerActivated(scope))
.then(function() {
TestRunner.addResult('The first ServiceWorker is activated.');
return TestRunner.callFunctionInPageAsync('loadIframe', [scope]);
return TestRunner.addIframe(scope);
})
.then(function() {
TestRunner.addResult('The first frame loaded.');
return TestRunner.callFunctionInPageAsync('loadIframe', [scope]);
return TestRunner.addIframe(scope);
})
.then(function() {
TestRunner.addResult('The second frame loaded.');
TestRunner.addResult('Check "Force update on page load" check box');
Common.settings.settingForTest('serviceWorkerUpdateOnReload').set(true);
return TestRunner.callFunctionInPageAsync('loadIframe', [scope]);
return TestRunner.addIframe(scope);
})
.then(function() {
TestRunner.addResult('The third frame loaded. The second worker must be activated before here.');
return TestRunner.callFunctionInPageAsync('loadIframe', [scope]);
return TestRunner.addIframe(scope);
})
.then(function() {
TestRunner.addResult('The fourth frame loaded. The third worker must be activated before here.');
TestRunner.addResult('Uncheck "Force update on page load" check box');
Common.settings.settingForTest('serviceWorkerUpdateOnReload').set(false);
return TestRunner.callFunctionInPageAsync('loadIframe', [scope]);
return TestRunner.addIframe(scope);
})
.then(function() {
TestRunner.addResult('The fifth frame loaded.');
......
......@@ -4,42 +4,13 @@
<script src="../../inspector/service-workers/service-workers-test.js"></script>
<script>
function initializeServiceWorker(script, scope) {
return navigator.serviceWorker.register(script, {scope: scope})
.then(reg => waitForActivated(reg.installing));
}
function waitForActivated(worker) {
if (worker.state === 'activated')
return Promise.resolve();
if (worker.state === 'redundant')
return Promise.reject(new Error('The worker is redundant'));
return new Promise(resolve => {
worker.addEventListener('statechange', _ => {
if (worker.state === 'activated')
resolve();
});
});
}
function loadIframe(url)
{
var callback;
var promise = new Promise((fulfill) => callback = fulfill);
var frame = document.createElement('iframe');
frame.src = url;
frame.onload = callback;
document.body.appendChild(frame);
return promise;
}
function test() {
var scriptURL = 'http://127.0.0.1:8000/devtools/service-workers/resources/navigation-preload-worker.js';
var scope = 'http://127.0.0.1:8000/devtools/service-workers/resources/navigation-preload-scope.php';
var preloadRequestIDs = {};
const scriptURL = 'http://127.0.0.1:8000/devtools/service-workers/resources/navigation-preload-worker.js';
const scope = 'http://127.0.0.1:8000/devtools/service-workers/resources/navigation-preload-scope.php';
const preloadRequestIDs = {};
function onRequestStarted(event) {
var request = event.data;
const request = event.data;
if (request.initiator().type != 'preload') {
return;
}
......@@ -49,7 +20,7 @@ function test() {
}
function onResponseReceived(event) {
var request = event.data;
const request = event.data;
if (!preloadRequestIDs[request.requestId()]) {
return;
}
......@@ -63,7 +34,7 @@ function test() {
});
}
function onRequestFinished(event) {
var request = event.data;
const request = event.data;
if (!preloadRequestIDs[request.requestId()]) {
return;
}
......@@ -78,24 +49,24 @@ function test() {
SDK.NetworkManager, SDK.NetworkManager.Events.ResponseReceived, onResponseReceived);
SDK.targetManager.addModelListener(SDK.NetworkManager, SDK.NetworkManager.Events.RequestFinished, onRequestFinished);
TestRunner.callFunctionInPageAsync('initializeServiceWorker', [scriptURL, scope])
ApplicationTestRunner.registerServiceWorker(scriptURL, scope)
.then(_ => ApplicationTestRunner.waitForActivated(scope))
.then(_ => {
TestRunner.addResult('-----------------');
TestRunner.addResult('Loading an iframe.');
return TestRunner.callFunctionInPageAsync('loadIframe', [scope]);
return TestRunner.addIframe(scope);
})
.then(_ => {
TestRunner.addResult('The iframe loaded.');
TestRunner.addResult('-----------------');
TestRunner.addResult('Loading another iframe.');
return TestRunner.callFunctionInPageAsync('loadIframe', [scope + '?BrokenChunked']);
return TestRunner.addIframe(scope + '?BrokenChunked');
})
.then(_ => {
TestRunner.addResult('The iframe loaded.');
TestRunner.addResult('-----------------');
TestRunner.addResult('Loading another iframe.');
return TestRunner.callFunctionInPageAsync('loadIframe', [scope + '?Redirect']);
return TestRunner.addIframe(scope + '?Redirect');
})
.then(_ => {
TestRunner.addResult('The iframe loaded.');
......
......@@ -6,26 +6,13 @@
<script src="../../inspector/console-test.js"></script>
<script>
var iframe;
function loadIframe(scope)
{
iframe = document.createElement("iframe");
document.body.appendChild(iframe);
iframe.src = scope;
}
function unloadIframe()
{
iframe.remove();
}
function test() {
var scriptURL = 'http://127.0.0.1:8000/devtools/service-workers/resources/changing-worker.php';
var scope = 'http://127.0.0.1:8000/devtools/service-workers/resources/service-worker-redundant-scope/';
var step = 0;
var firstVersionId = -1;
var secondVersionId = -1;
const scriptURL = 'http://127.0.0.1:8000/devtools/service-workers/resources/changing-worker.php';
const scope = 'http://127.0.0.1:8000/devtools/service-workers/resources/service-worker-redundant-scope/';
const frameId = 'frame_id';
let step = 0;
let firstVersionId = -1;
let secondVersionId = -1;
Resources.ServiceWorkersView._noThrottle = true;
TestRunner.evaluateInPage('frontendReopeningCount', function(result) {
......@@ -34,7 +21,7 @@ function test() {
function updateRegistration(registration) {
if (registration.scopeURL != scope)
return;
for (var version of registration.versions.values()) {
for (let version of registration.versions.values()) {
if (step == 0 && version.isRunning() && version.isActivated()) {
++step;
firstVersionId = version.id;
......@@ -42,7 +29,7 @@ function test() {
TestRunner.addResult('==== ServiceWorkersView ====');
TestRunner.addResult(ApplicationTestRunner.dumpServiceWorkersView([scope]));
TestRunner.addResult('============================');
TestRunner.evaluateInPage('loadIframe("' + scope + '");');
TestRunner.addIframe(scope, {id: frameId});
} else if (step == 1 && version.isRunning() && version.isInstalled()) {
++step;
secondVersionId = version.id;
......@@ -50,13 +37,13 @@ function test() {
TestRunner.addResult('==== ServiceWorkersView ====');
TestRunner.addResult(ApplicationTestRunner.dumpServiceWorkersView([scope]));
TestRunner.addResult('============================');
TestRunner.evaluateInPage('unloadIframe();');
TestRunner.evaluateInPagePromise(`document.getElementById('${frameId}').remove();`);
}
}
if (step != 2)
return;
var firstVersion = registration.versions.get(firstVersionId);
var secondVersion = registration.versions.get(secondVersionId);
const firstVersion = registration.versions.get(firstVersionId);
const secondVersion = registration.versions.get(secondVersionId);
if ((!firstVersion || (firstVersion.isStopped() && firstVersion.isRedundant())) &&
secondVersion.isActivated() && secondVersion.isRunning()) {
++step;
......
......@@ -576,6 +576,11 @@ void InspectorNetworkAgent::ShouldBlockRequest(const KURL& url, bool* result) {
return;
}
void InspectorNetworkAgent::ShouldBypassServiceWorker(bool* result) {
*result =
state_->booleanProperty(NetworkAgentState::kBypassServiceWorker, false);
}
void InspectorNetworkAgent::DidBlockRequest(
ExecutionContext* execution_context,
const ResourceRequest& request,
......
......@@ -124,6 +124,7 @@ class CORE_EXPORT InspectorNetworkAgent final
void DidReceiveScriptResponse(unsigned long identifier);
void ShouldForceCORSPreflight(bool* result);
void ShouldBlockRequest(const KURL&, bool* result);
void ShouldBypassServiceWorker(bool* result);
void DocumentThreadableLoaderStartedLoadingForClient(unsigned long identifier,
ThreadableLoaderClient*);
......
......@@ -293,6 +293,14 @@ void DocumentThreadableLoader::StartBlinkCORS(const ResourceRequest& request) {
ResourceRequest new_request(request);
// Set the service worker mode to none if "bypass for network" in DevTools is
// enabled.
bool should_bypass_service_worker = false;
probe::shouldBypassServiceWorker(GetExecutionContext(),
&should_bypass_service_worker);
if (should_bypass_service_worker)
new_request.SetServiceWorkerMode(WebURLRequest::ServiceWorkerMode::kNone);
// Process the CORS protocol inside the DocumentThreadableLoader for the
// following cases:
//
......@@ -316,10 +324,10 @@ void DocumentThreadableLoader::StartBlinkCORS(const ResourceRequest& request) {
// intercepted since LoadPreflightRequest() sets the flag to kNone in
// advance.
if (!async_ ||
request.GetServiceWorkerMode() !=
new_request.GetServiceWorkerMode() !=
WebURLRequest::ServiceWorkerMode::kAll ||
!SchemeRegistry::ShouldTreatURLSchemeAsAllowingServiceWorkers(
request.Url().Protocol()) ||
new_request.Url().Protocol()) ||
!loading_context_->GetResourceFetcher()->IsControlledByServiceWorker()) {
DispatchInitialRequestBlinkCORS(new_request);
return;
......
......@@ -115,6 +115,7 @@
"markResourceAsCached",
"scriptImported",
"shouldBlockRequest",
"shouldBypassServiceWorker",
"shouldForceCORSPreflight",
"willDispatchEventSourceEvent",
"willLoadXHR",
......
......@@ -161,6 +161,7 @@ interface CoreProbes {
void shouldWaitForDebuggerOnWorkerStart(ExecutionContext* context, bool* result);
void shouldForceCORSPreflight(ExecutionContext*, bool* result);
void shouldBlockRequest(ExecutionContext*, const KURL&, bool* result);
void shouldBypassServiceWorker(ExecutionContext* context, bool* result);
void consoleTimeStamp(ExecutionContext*, const String& title);
void lifecycleEvent([Keep] LocalFrame*, const char* name, double timestamp);
void paintTiming([Keep] Document*, const char* name, double timestamp);
......
......@@ -11,6 +11,10 @@ ApplicationTestRunner.registerServiceWorker = function(script, scope) {
return TestRunner.callFunctionInPageAsync('registerServiceWorker', [script, scope]);
};
ApplicationTestRunner.waitForActivated = function(scope) {
return TestRunner.callFunctionInPageAsync('waitForActivated', [scope]);
};
ApplicationTestRunner.unregisterServiceWorker = function(scope) {
return TestRunner.callFunctionInPageAsync('unregisterServiceWorker', [scope]);
};
......@@ -66,6 +70,23 @@ TestRunner.initAsync(`
}).then(reg => registrations[scope] = reg);
}
function waitForActivated(scope) {
let reg = registrations[scope];
if (!reg)
return Promise.reject(new Error('The registration'));
let worker = reg.installing || reg.waiting || reg.active;
if (worker.state === 'activated')
return Promise.resolve();
if (worker.state === 'redundant')
return Promise.reject(new Error('The worker is redundant'));
return new Promise(resolve => {
worker.addEventListener('statechange', () => {
if (worker.state === 'activated')
resolve();
});
});
}
function postToServiceWorker(scope, message) {
registrations[scope].active.postMessage(message);
}
......
......@@ -503,16 +503,18 @@ TestRunner.addStylesheetTag = function(path) {
})();
`);
};
/**
* @param {string} path
* @param {!Object|undefined} options
* @return {!Promise<!SDK.RemoteObject|undefined>}
*/
TestRunner.addIframe = function(path) {
TestRunner.addIframe = function(path, options = {}) {
options.id = options.id || '';
return TestRunner.evaluateInPageAsync(`
(function(){
var iframe = document.createElement('iframe');
iframe.src = '${path}';
iframe.id = '${options.id}';
document.body.appendChild(iframe);
return new Promise(f => iframe.onload = f);
})();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment