Bug 1163862 - Switch to HTTP observer + support requestId & data: URIs + test fixes r=billm

MozReview-Commit-ID: 30nEXQpWEHg
This commit is contained in:
Giorgio Maone 2016-02-26 19:08:32 +01:00
parent f49e32052e
commit 3598a71e3a
7 changed files with 155 additions and 49 deletions

View File

@ -33,6 +33,7 @@ function WebRequestEventManager(context, eventName) {
}
let data2 = {
requestId: data.requestId,
url: data.url,
method: data.method,
type: data.type,

View File

@ -11,9 +11,9 @@
<div id="test">Sample text</div>
<img id="img_redirect" src="file_image_redirect.png">
<img id="img_good" src="file_image_good.png">
<img id="img_bad" src="file_image_bad.png">
<img id="img_redirect" src="file_image_redirect.png">
<script src="file_script_good.js"></script>
<script src="file_script_bad.js"></script>
@ -25,5 +25,7 @@
<iframe src="file_WebRequest_page2.html" width="200" height="200"></iframe>
<iframe src="redirection.sjs" width="200" height="200"></iframe>
<iframe src="data:text/plain,webRequestTest" width="200" height="200"></iframe>
</body>
</html>

View File

@ -31,7 +31,9 @@ const expected_requested = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_WebRequest_page2.html",
BASE + "/nonexistent_script_url.js",
BASE + "/redirection.sjs",
BASE + "/xhr_resource"];
BASE + "/dummy_page.html",
BASE + "/xhr_resource",
"data:text/plain,webRequestTest"];
const expected_beforeSendHeaders = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_style_good.css",
@ -53,7 +55,7 @@ const expected_sendHeaders = expected_beforeSendHeaders.filter(u => !/_redirect\
const expected_redirect = expected_beforeSendHeaders.filter(u => /_redirect\./.test(u))
.concat(BASE + "/redirection.sjs");
const expected_complete = [BASE + "/file_WebRequest_page1.html",
const expected_response = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_style_good.css",
BASE + "/file_image_good.png",
BASE + "/file_script_good.js",
@ -63,6 +65,8 @@ const expected_complete = [BASE + "/file_WebRequest_page1.html",
BASE + "/dummy_page.html",
BASE + "/xhr_resource"];
const expected_complete = expected_response.concat("data:text/plain,webRequestTest");
function removeDupes(list) {
let j = 0;
for (let i = 1; i < list.length; i++) {
@ -85,11 +89,13 @@ function compareLists(list1, list2, kind) {
}
function backgroundScript() {
const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
let checkCompleted = true;
let savedTabId = -1;
function shouldRecord(url) {
return url.startsWith(BASE) || /^data:.*\bwebRequestTest\b/.test(url);
}
function checkType(details) {
let expected_type = "???";
if (details.url.indexOf("style") != -1) {
@ -100,7 +106,7 @@ function backgroundScript() {
expected_type = "script";
} else if (details.url.indexOf("page1") != -1) {
expected_type = "main_frame";
} else if (/page2|redirection|dummy_page/.test(details.url)) {
} else if (/page2|redirection|dummy_page|data:text\/(?:plain|html),/.test(details.url)) {
expected_type = "sub_frame";
} else if (details.url.indexOf("xhr") != -1) {
expected_type = "xmlhttprequest";
@ -108,6 +114,16 @@ function backgroundScript() {
browser.test.assertEq(details.type, expected_type, "resource type is correct");
}
let requestIDs = new Map();
let idDisposalEvents = new Set(["completed", "error", "redirect"]);
function checkRequestId(details, event = "unknown") {
let ids = requestIDs.get(details.url);
browser.test.assertTrue(ids && ids.has(details.requestId), `correct requestId for ${details.url} (${details.requestId} in [${ids && [...ids].join(", ")}])`);
if (ids && idDisposalEvents.has(event)) {
ids.delete(details.requestId);
}
}
let frameIDs = new Map();
let recorded = {requested: [],
@ -119,13 +135,21 @@ function backgroundScript() {
function checkResourceType(type) {
let key = type.toUpperCase();
browser.test.assertTrue(key in browser.webRequest.ResourceType);
browser.test.assertTrue(key in browser.webRequest.ResourceType, `valid resource type ${key}`);
}
function onBeforeRequest(details) {
browser.test.log(`onBeforeRequest ${details.url}`);
browser.test.log(`onBeforeRequest ${details.requestId} ${details.url}`);
browser.test.assertTrue(details.requestId > 0, `valid requestId ${details.requestId}`);
let ids = requestIDs.get(details.url);
if (ids) {
ids.add(details.requestId);
} else {
requestIDs.set(details.url, new Set([details.requestId]));
}
checkResourceType(details.type);
if (details.url.startsWith(BASE)) {
if (shouldRecord(details.url)) {
recorded.requested.push(details.url);
if (savedTabId == -1) {
@ -155,8 +179,9 @@ function backgroundScript() {
function onBeforeSendHeaders(details) {
browser.test.log(`onBeforeSendHeaders ${details.url}`);
checkRequestId(details);
checkResourceType(details.type);
if (details.url.startsWith(BASE)) {
if (shouldRecord(details.url)) {
recorded.beforeSendHeaders.push(details.url);
browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
@ -173,8 +198,9 @@ function backgroundScript() {
function onBeforeRedirect(details) {
browser.test.log(`onBeforeRedirect ${details.url} -> ${details.redirectUrl}`);
checkRequestId(details, "redirect");
checkResourceType(details.type);
if (details.url.startsWith(BASE)) {
if (shouldRecord(details.url)) {
recorded.beforeRedirect.push(details.url);
browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
@ -192,8 +218,10 @@ function backgroundScript() {
}
function onRecord(kind, details) {
browser.test.log(`${kind} ${details.url}`);
checkResourceType(details.type);
if (details.url.startsWith(BASE)) {
checkRequestId(details, kind);
if (shouldRecord(details.url)) {
recorded[kind].push(details.url);
}
}
@ -209,7 +237,10 @@ function backgroundScript() {
// When resources are cached, the ip property is not present,
// so only check for the ip property the first time around.
if (checkCompleted && !completedUrls[kind].has(details.url)) {
browser.test.assertEq(details.ip, "127.0.0.1", "correct ip");
// We can only tell IPs for HTTP requests.
if (/^https?:/.test(details.url)) {
browser.test.assertEq(details.ip, "127.0.0.1", "correct ip");
}
completedUrls[kind].add(details.url);
}
}
@ -243,7 +274,7 @@ function* test_once(skipCompleted) {
"webRequestBlocking",
],
},
background: "(" + backgroundScript.toString() + ")()",
background: `const BASE = ${JSON.stringify(BASE)}; (${backgroundScript.toString()})()`,
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
@ -297,7 +328,7 @@ function* test_once(skipCompleted) {
compareLists(recorded.beforeSendHeaders, expected_beforeSendHeaders, "beforeSendHeaders");
compareLists(recorded.sendHeaders, expected_sendHeaders, "sendHeaders");
compareLists(recorded.beforeRedirect, expected_redirect, "beforeRedirect");
compareLists(recorded.responseStarted, expected_complete, "responseStarted");
compareLists(recorded.responseStarted, expected_response, "responseStarted");
compareLists(recorded.completed, expected_complete, "completed");
yield extension.unload();

View File

@ -15,7 +15,8 @@ this.EXPORTED_SYMBOLS = ["MatchPattern"];
/* globals MatchPattern */
const PERMITTED_SCHEMES = ["http", "https", "file", "ftp", "app"];
const PERMITTED_SCHEMES = ["http", "https", "file", "ftp", "app", "data"];
const PERMITTED_SCHEMES_REGEXP = PERMITTED_SCHEMES.join("|");
// This function converts a glob pattern (containing * and possibly ?
// as wildcards) to a regular expression.
@ -42,7 +43,7 @@ function SingleMatchPattern(pat) {
} else if (!pat) {
this.schemes = [];
} else {
let re = new RegExp("^(http|https|file|ftp|app|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$");
let re = new RegExp(`^(${PERMITTED_SCHEMES_REGEXP}|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$`);
let match = re.exec(pat);
if (!match) {
Cu.reportError(`Invalid match pattern: '${pat}'`);

View File

@ -21,10 +21,44 @@ XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon",
"resource://gre/modules/WebRequestCommon.jsm");
// TODO
// Figure out how to handle requestId. Gecko seems to have no such thing. (Bug 1163862)
// We also don't know the method for content policy. (Bug 1163862)
// We don't even have a window ID for HTTP observer stuff. (Bug 1163861)
function attachToChannel(channel, key, data) {
if (channel instanceof Ci.nsIWritablePropertyBag2) {
let wrapper = {value: data};
wrapper.wrappedJSObject = wrapper;
channel.setPropertyAsInterface(key, wrapper);
}
}
function extractFromChannel(channel, key) {
if (channel instanceof Ci.nsIPropertyBag2 && channel.hasKey(key)) {
let data = channel.get(key);
if (data && data.wrappedJSObject) {
data = data.wrappedJSObject;
}
return "value" in data ? data.value : data;
}
return null;
}
var RequestId = {
count: 1,
KEY: "mozilla.webRequest.requestId",
create(channel = null) {
let id = this.count++;
if (channel) {
attachToChannel(channel, this.KEY, id);
}
return id;
},
get(channel) {
return channel && extractFromChannel(channel, this.KEY) || this.create(channel);
},
};
function runLater(job) {
Services.tm.currentThread.dispatch(job, Ci.nsIEventTarget.DISPATCH_NORMAL);
}
function parseFilter(filter) {
if (!filter) {
@ -53,6 +87,8 @@ function parseExtra(extra, allowed) {
return result;
}
var HttpObserverManager;
var ContentPolicyManager = {
policyData: new Map(),
policies: new Map(),
@ -77,32 +113,50 @@ var ContentPolicyManager = {
continue;
}
let response = null;
let data = {
url: msg.data.url,
windowId: msg.data.windowId,
parentWindowId: msg.data.parentWindowId,
type: msg.data.type,
browser: browser,
requestId: RequestId.create(),
};
try {
response = callback({
url: msg.data.url,
windowId: msg.data.windowId,
parentWindowId: msg.data.parentWindowId,
type: msg.data.type,
browser: browser,
});
response = callback(data);
if (response && response.cancel) {
return {cancel: true};
}
// FIXME: Need to handle redirection here. (Bug 1163862)
} catch (e) {
Cu.reportError(e);
} finally {
runLater(() => this.runChannelListener("onStop", data));
}
if (response && response.cancel) {
return {cancel: true};
}
// FIXME: Need to handle redirection here. (Bug 1163862)
}
return {};
},
runChannelListener(kind, data) {
let listeners = HttpObserverManager.listeners[kind];
let uri = BrowserUtils.makeURI(data.url);
let policyType = data.type;
for (let [callback, opts] of listeners.entries()) {
if (!HttpObserverManager.shouldRunListener(policyType, uri, opts.filter)) {
continue;
}
callback(data);
}
},
addListener(callback, opts) {
// Clone opts, since we're going to modify them for IPC.
opts = Object.assign({}, opts);
let id = this.nextId++;
opts.id = id;
if (opts.filter.urls) {
opts.filter = Object.assign({}, opts.filter);
opts.filter.urls = opts.filter.urls.serialize();
}
Services.ppmm.broadcastAsyncMessage("WebRequest:AddContentPolicy", opts);
@ -151,8 +205,6 @@ StartStopListener.prototype = {
},
};
var HttpObserverManager;
var ChannelEventSink = {
_classDescription: "WebRequest channel event sink",
_classID: Components.ID("115062f8-92f1-11e5-8b7f-080027b0f7ec"),
@ -178,7 +230,7 @@ var ChannelEventSink = {
// nsIChannelEventSink implementation
asyncOnChannelRedirect(oldChannel, newChannel, flags, redirectCallback) {
Services.tm.currentThread.dispatch(() => redirectCallback.onRedirectVerifyCallback(Cr.NS_OK), Ci.nsIEventTarget.DISPATCH_NORMAL);
runLater(() => redirectCallback.onRedirectVerifyCallback(Cr.NS_OK));
try {
HttpObserverManager.onChannelReplaced(oldChannel, newChannel);
} catch (e) {
@ -203,6 +255,7 @@ HttpObserverManager = {
redirectInitialized: false,
listeners: {
opening: new Map(),
modify: new Map(),
afterModify: new Map(),
headersReceived: new Map(),
@ -212,7 +265,7 @@ HttpObserverManager = {
},
addOrRemove() {
let needModify = this.listeners.modify.size || this.listeners.afterModify.size;
let needModify = this.listeners.opening.size || this.listeners.modify.size || this.listeners.afterModify.size;
if (needModify && !this.modifyInitialized) {
this.modifyInitialized = true;
Services.obs.addObserver(this, "http-on-modify-request", false);
@ -289,13 +342,15 @@ HttpObserverManager = {
observe(subject, topic, data) {
let channel = subject.QueryInterface(Ci.nsIHttpChannel);
if (topic == "http-on-modify-request") {
this.modify(channel, topic, data);
} else if (topic == "http-on-examine-response" ||
topic == "http-on-examine-cached-response" ||
topic == "http-on-examine-merged-response") {
this.examine(channel, topic, data);
switch (topic) {
case "http-on-modify-request":
this.modify(channel, topic, data);
break;
case "http-on-examine-response":
case "http-on-examine-cached-response":
case "http-on-examine-merged-response":
this.examine(channel, topic, data);
break;
}
},
@ -305,6 +360,9 @@ HttpObserverManager = {
},
runChannelListener(channel, loadContext, kind, extraData = null) {
if (channel.status === Cr.NS_ERROR_ABORT) {
return false;
}
let listeners = this.listeners[kind];
let browser = loadContext ? loadContext.topFrameElement : null;
let loadInfo = channel.loadInfo;
@ -326,6 +384,7 @@ HttpObserverManager = {
}
let data = {
requestId: RequestId.get(channel),
url: channel.URI.spec,
method: channel.requestMethod,
browser: browser,
@ -372,7 +431,7 @@ HttpObserverManager = {
return true;
}
if (result.cancel) {
channel.cancel();
channel.cancel(Cr.NS_ERROR_ABORT);
return false;
}
if (result.redirectUrl) {
@ -407,7 +466,8 @@ HttpObserverManager = {
modify(channel, topic, data) {
let loadContext = this.getLoadContext(channel);
if (this.runChannelListener(channel, loadContext, "modify")) {
if (this.runChannelListener(channel, loadContext, "opening") &&
this.runChannelListener(channel, loadContext, "modify")) {
this.runChannelListener(channel, loadContext, "afterModify");
}
},
@ -450,9 +510,11 @@ var onBeforeRequest = {
let opts = parseExtra(opt_extraInfoSpec, ["blocking"]);
opts.filter = parseFilter(filter);
ContentPolicyManager.addListener(callback, opts);
HttpObserverManager.addListener("opening", callback, opts);
},
removeListener(callback) {
HttpObserverManager.removeListener("opening", callback);
ContentPolicyManager.removeListener(callback);
},
};
@ -482,7 +544,7 @@ var onResponseStarted = new HttpEvent("onStart", ["responseHeaders"]);
var onCompleted = new HttpEvent("onStop", ["responseHeaders"]);
var WebRequest = {
// Handled via content policy.
// http-on-modify observer for HTTP(S), content policy for the other protocols (notably, data:)
onBeforeRequest: onBeforeRequest,
// http-on-modify observer.

View File

@ -17,6 +17,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon",
"resource://gre/modules/WebRequestCommon.jsm");
const IS_HTTP = /^https?:/;
var ContentPolicy = {
_classDescription: "WebRequest content policy",
_classID: Components.ID("938e5d24-9ccc-4b55-883e-c252a41f7ce9"),
@ -78,6 +80,12 @@ var ContentPolicy = {
shouldLoad(policyType, contentLocation, requestOrigin,
node, mimeTypeGuess, extra, requestPrincipal) {
let url = contentLocation.spec;
if (IS_HTTP.test(url)) {
// We'll handle this in our parent process HTTP observer.
return Ci.nsIContentPolicy.ACCEPT;
}
let block = false;
let ids = [];
for (let [id, {blocking, filter}] of this.contentPolicies.entries()) {
@ -146,7 +154,7 @@ var ContentPolicy = {
}
let data = {ids,
url: contentLocation.spec,
url,
type: WebRequestCommon.typeForPolicyType(policyType),
windowId,
parentWindowId};

View File

@ -117,6 +117,7 @@ const expected_requested = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_WebRequest_page2.html",
BASE + "/nonexistent_script_url.js",
BASE + "/WebRequest_redirection.sjs",
BASE + "/dummy_page.html",
BASE + "/xhr_resource"];
const expected_sendHeaders = [BASE + "/file_WebRequest_page1.html",