Bug 1215197: Implements onBeforeRedirect by using a channel event sink (r=billm)

This commit is contained in:
Bill McCloskey 2015-12-29 15:47:04 -08:00
parent bde9fef694
commit 41e2463206
10 changed files with 184 additions and 78 deletions

View File

@ -48,7 +48,7 @@ function WebRequestEventManager(context, eventName) {
return;
}
let optional = ["requestHeaders", "responseHeaders", "statusCode"];
let optional = ["requestHeaders", "responseHeaders", "statusCode", "redirectUrl"];
for (let opt of optional) {
if (opt in data) {
data2[opt] = data[opt];
@ -100,6 +100,7 @@ extensions.registerSchemaAPI("webRequest", "webRequest", (extension, context) =>
onBeforeSendHeaders: new WebRequestEventManager(context, "onBeforeSendHeaders").api(),
onSendHeaders: new WebRequestEventManager(context, "onSendHeaders").api(),
onHeadersReceived: new WebRequestEventManager(context, "onHeadersReceived").api(),
onBeforeRedirect: new WebRequestEventManager(context, "onBeforeRedirect").api(),
onResponseStarted: new WebRequestEventManager(context, "onResponseStarted").api(),
onCompleted: new WebRequestEventManager(context, "onCompleted").api(),
handlerBehaviorChanged: function() {
@ -107,7 +108,6 @@ extensions.registerSchemaAPI("webRequest", "webRequest", (extension, context) =>
},
// TODO
onBeforeRedirect: ignoreEvent(context, "webRequest.onBeforeRedirect"),
onErrorOccurred: ignoreEvent(context, "webRequest.onErrorOccurred"),
},
};

View File

@ -24,6 +24,6 @@
<script src="nonexistent_script_url.js"></script>
<iframe src="file_WebRequest_page2.html" width="200" height="200"></iframe>
<iframe src="redirection.sjs" width="200" height="200"></iframe>
</body>
</html>

View File

@ -18,6 +18,7 @@ support-files =
file_script_redirect.js
file_script_xhr.js
file_sample.html
redirection.sjs
file_privilege_escalation.html
file_ext_background_api_injection.js
file_permission_xhr.html

View File

@ -0,0 +1,4 @@
function handleRequest(aRequest, aResponse) {
aResponse.setStatusLine(aRequest.httpVersion, 302);
aResponse.setHeader("Location", "./dummy_page.html");
}

View File

@ -28,9 +28,10 @@ const expected_requested = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_script_xhr.js",
BASE + "/file_WebRequest_page2.html",
BASE + "/nonexistent_script_url.js",
BASE + "/redirection.sjs",
BASE + "/xhr_resource"];
const expected_sendHeaders = [BASE + "/file_WebRequest_page1.html",
const expected_beforeSendHeaders = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_style_good.css",
BASE + "/file_style_redirect.css",
BASE + "/file_image_good.png",
@ -40,8 +41,16 @@ const expected_sendHeaders = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_script_xhr.js",
BASE + "/file_WebRequest_page2.html",
BASE + "/nonexistent_script_url.js",
BASE + "/redirection.sjs",
BASE + "/dummy_page.html",
BASE + "/xhr_resource"];
const expected_sendHeaders = expected_beforeSendHeaders.filter(u => !/_redirect\./.test(u))
.concat(BASE + "/redirection.sjs");
const expected_redirect = expected_beforeSendHeaders.filter(u => /_redirect\./.test(u))
.concat(BASE + "/redirection.sjs");
const expected_complete = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_style_good.css",
BASE + "/file_image_good.png",
@ -49,6 +58,7 @@ const expected_complete = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_script_xhr.js",
BASE + "/file_WebRequest_page2.html",
BASE + "/nonexistent_script_url.js",
BASE + "/dummy_page.html",
BASE + "/xhr_resource"];
function removeDupes(list)
@ -91,7 +101,7 @@ function backgroundScript()
expected_type = "script";
} else if (details.url.indexOf("page1") != -1) {
expected_type = "main_frame";
} else if (details.url.indexOf("page2") != -1) {
} else if (/page2|redirection|dummy_page/.test(details.url)) {
expected_type = "sub_frame";
} else if (details.url.indexOf("xhr") != -1) {
expected_type = "xmlhttprequest";
@ -103,6 +113,7 @@ function backgroundScript()
var recorded = {requested: [],
beforeSendHeaders: [],
beforeRedirect: [],
sendHeaders: [],
responseStarted: [],
completed: []};
@ -164,6 +175,27 @@ function backgroundScript()
return {};
}
function onBeforeRedirect(details)
{
browser.test.log(`onBeforeRedirect ${details.url} -> ${details.redirectUrl}`);
checkResourceType(details.type);
if (details.url.startsWith(BASE)) {
recorded.beforeRedirect.push(details.url);
browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
checkType(details);
var id = frameIDs.get(details.url);
browser.test.assertEq(id, details.frameId, "frame ID same in onBeforeRedirect as onBeforeRequest");
frameIDs.set(details.redirectUrl, details.frameId);
}
if (details.url.indexOf("_redirect.") != -1) {
let expectedUrl = details.url.replace("_redirect.", "_good.");
browser.test.assertEq(details.redirectUrl, expectedUrl, "correct redirectUrl value");
}
return {};
}
function onRecord(kind, details)
{
checkResourceType(details.type);
@ -175,6 +207,7 @@ function backgroundScript()
browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ["<all_urls>"]}, ["blocking"]);
browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, {urls: ["<all_urls>"]}, ["blocking"]);
browser.webRequest.onSendHeaders.addListener(onRecord.bind(null, "sendHeaders"), {urls: ["<all_urls>"]});
browser.webRequest.onBeforeRedirect.addListener(onBeforeRedirect, {urls: ["<all_urls>"]});
browser.webRequest.onResponseStarted.addListener(onRecord.bind(null, "responseStarted"), {urls: ["<all_urls>"]});
browser.webRequest.onCompleted.addListener(onRecord.bind(null, "completed"), {urls: ["<all_urls>"]});
@ -243,8 +276,9 @@ function* test_once()
let recorded = yield extension.awaitMessage("results");
compareLists(recorded.requested, expected_requested, "requested");
compareLists(recorded.beforeSendHeaders, expected_sendHeaders, "beforeSendHeaders");
compareLists(recorded.sendHeaders, expected_complete, "sendHeaders");
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.completed, expected_complete, "completed");

View File

@ -151,14 +151,60 @@ StartStopListener.prototype = {
},
};
var ChannelEventSink = {
_classDescription: "WebRequest channel event sink",
_classID: Components.ID("115062f8-92f1-11e5-8b7f-080027b0f7ec"),
_contractID: "@mozilla.org/webrequest/channel-event-sink;1",
QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink,
Ci.nsIFactory]),
init() {
Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
.registerFactory(this._classID, this._classDescription, this._contractID, this);
},
register() {
let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
catMan.addCategoryEntry("net-channel-event-sinks", this._contractID, this._contractID, false, true);
},
unregister() {
let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
catMan.deleteCategoryEntry("net-channel-event-sinks", this._contractID, false);
},
// nsIChannelEventSink implementation
asyncOnChannelRedirect(oldChannel, newChannel, flags, redirectCallback) {
Services.tm.currentThread.dispatch(() => redirectCallback.onRedirectVerifyCallback(Cr.NS_OK), Ci.nsIEventTarget.DISPATCH_NORMAL);
try {
HttpObserverManager.onChannelReplaced(oldChannel, newChannel);
} catch (e) {
// we don't wanna throw: it would abort the redirection
}
},
// nsIFactory implementation
createInstance(outer, iid) {
if (outer) {
throw Cr.NS_ERROR_NO_AGGREGATION;
}
return this.QueryInterface(iid);
}
}
ChannelEventSink.init();
var HttpObserverManager = {
modifyInitialized: false,
examineInitialized: false,
redirectInitialized: false,
listeners: {
modify: new Map(),
afterModify: new Map(),
headersReceived: new Map(),
onRedirect: new Map(),
onStart: new Map(),
onStop: new Map(),
},
@ -187,6 +233,15 @@ var HttpObserverManager = {
Services.obs.removeObserver(this, "http-on-examine-cached-response");
Services.obs.removeObserver(this, "http-on-examine-merged-response");
}
let needRedirect = this.listeners.onRedirect.size;
if (needRedirect && !this.redirectInitialized) {
this.redirectInitialized = true;
ChannelEventSink.register();
} else if (!needRedirect && this.redirectInitialized) {
this.redirectInitialized = false;
ChannelEventSink.unregister();
}
},
addListener(kind, callback, opts) {
@ -247,7 +302,7 @@ var HttpObserverManager = {
WebRequestCommon.urlMatches(uri, filter.urls);
},
runChannelListener(channel, loadContext, kind) {
runChannelListener(channel, loadContext, kind, extraData = null) {
let listeners = this.listeners[kind];
let browser = loadContext ? loadContext.topFrameElement : null;
let loadInfo = channel.loadInfo;
@ -258,7 +313,10 @@ var HttpObserverManager = {
let requestHeaders;
let responseHeaders;
let includeStatus = kind == "headersReceived" || kind == "onStart" || kind == "onStop";
let includeStatus = kind === "headersReceived" ||
kind === "onBeforeRedirect" ||
kind === "onStart" ||
kind === "onStop";
for (let [callback, opts] of listeners.entries()) {
if (!this.shouldRunListener(policyType, channel.URI, opts.filter)) {
@ -273,6 +331,9 @@ var HttpObserverManager = {
windowId: loadInfo ? loadInfo.outerWindowID : 0,
parentWindowId: loadInfo ? loadInfo.parentOuterWindowID : 0,
};
if (extraData) {
Object.assign(data, extraData);
}
if (opts.requestHeaders) {
if (!requestHeaders) {
requestHeaders = this.getHeaders(channel, "visitRequestHeaders");
@ -344,16 +405,25 @@ var HttpObserverManager = {
let loadContext = this.getLoadContext(channel);
if (this.listeners.onStart.size || this.listeners.onStop.size) {
if (channel instanceof Components.interfaces.nsITraceableChannel) {
let listener = new StartStopListener(this, loadContext);
let orig = channel.setNewListener(listener);
listener.orig = orig;
if (channel instanceof Ci.nsITraceableChannel) {
let responseStatus = channel.responseStatus;
// skip redirections, https://bugzilla.mozilla.org/show_bug.cgi?id=728901#c8
if (responseStatus < 300 || responseStatus >= 400) {
let listener = new StartStopListener(this, loadContext);
let orig = channel.setNewListener(listener);
listener.orig = orig;
}
}
}
this.runChannelListener(channel, loadContext, "headersReceived");
},
onChannelReplaced(oldChannel, newChannel) {
this.runChannelListener(oldChannel, this.getLoadContext(oldChannel),
"onRedirect", { redirectUrl: newChannel.URI.spec });
},
onStartRequest(channel, loadContext) {
this.runChannelListener(channel, loadContext, "onStart");
},
@ -376,65 +446,29 @@ var onBeforeRequest = {
},
};
var onBeforeSendHeaders = {
function HttpEvent(internalEvent, options) {
this.internalEvent = internalEvent;
this.options = options;
}
HttpEvent.prototype = {
addListener(callback, filter = null, opt_extraInfoSpec = null) {
let opts = parseExtra(opt_extraInfoSpec, ["requestHeaders", "blocking"]);
let opts = parseExtra(opt_extraInfoSpec, this.options);
opts.filter = parseFilter(filter);
HttpObserverManager.addListener("modify", callback, opts);
HttpObserverManager.addListener(this.internalEvent, callback, opts);
},
removeListener(callback) {
HttpObserverManager.removeListener("modify", callback);
HttpObserverManager.removeListener(this.internalEvent, callback);
},
};
var onSendHeaders = {
addListener(callback, filter = null, opt_extraInfoSpec = null) {
let opts = parseExtra(opt_extraInfoSpec, ["requestHeaders"]);
opts.filter = parseFilter(filter);
HttpObserverManager.addListener("afterModify", callback, opts);
},
removeListener(callback) {
HttpObserverManager.removeListener("afterModify", callback);
},
};
var onHeadersReceived = {
addListener(callback, filter = null, opt_extraInfoSpec = null) {
let opts = parseExtra(opt_extraInfoSpec, ["blocking", "responseHeaders"]);
opts.filter = parseFilter(filter);
HttpObserverManager.addListener("headersReceived", callback, opts);
},
removeListener(callback) {
HttpObserverManager.removeListener("headersReceived", callback);
},
};
var onResponseStarted = {
addListener(callback, filter = null, opt_extraInfoSpec = null) {
let opts = parseExtra(opt_extraInfoSpec, ["responseHeaders"]);
opts.filter = parseFilter(filter);
HttpObserverManager.addListener("onStart", callback, opts);
},
removeListener(callback) {
HttpObserverManager.removeListener("onStart", callback);
},
};
var onCompleted = {
addListener(callback, filter = null, opt_extraInfoSpec = null) {
let opts = parseExtra(opt_extraInfoSpec, ["responseHeaders"]);
opts.filter = parseFilter(filter);
HttpObserverManager.addListener("onStop", callback, opts);
},
removeListener(callback) {
HttpObserverManager.removeListener("onStop", callback);
},
};
var onBeforeSendHeaders = new HttpEvent("modify", ["requestHeaders", "blocking"]);
var onSendHeaders = new HttpEvent("afterModify", ["requestHeaders"]);
var onHeadersReceived = new HttpEvent("headersReceived", ["blocking", "responseHeaders"]);
var onBeforeRedirect = new HttpEvent("onRedirect", ["responseHeaders"]);
var onResponseStarted = new HttpEvent("onStart", ["responseHeaders"]);
var onCompleted = new HttpEvent("onStop", ["responseHeaders"]);
var WebRequest = {
// Handled via content policy.
@ -449,6 +483,9 @@ var WebRequest = {
// http-on-examine-*observer.
onHeadersReceived: onHeadersReceived,
// nsIChannelEventSink
onBeforeRedirect: onBeforeRedirect,
// OnStartRequest channel listener.
onResponseStarted: onResponseStarted,

View File

@ -0,0 +1,4 @@
function handleRequest(aRequest, aResponse) {
aResponse.setStatusLine(aRequest.httpVersion, 302);
aResponse.setHeader("Location", "./dummy_page.html");
}

View File

@ -19,6 +19,7 @@ support-files =
file_script_redirect.js
file_script_xhr.js
WebRequest_dynamic.sjs
WebRequest_redirection.sjs
[browser_Battery.js]
[browser_Deprecated.js]

View File

@ -20,7 +20,7 @@ function checkType(details)
expected_type = "script";
} else if (details.url.indexOf("page1") != -1) {
expected_type = "main_frame";
} else if (details.url.indexOf("page2") != -1) {
} else if (/page2|_redirection\.|dummy_page/.test(details.url)) {
expected_type = "sub_frame";
} else if (details.url.indexOf("xhr") != -1) {
expected_type = "xmlhttprequest";
@ -72,6 +72,28 @@ function onBeforeSendHeaders(details)
}
}
var beforeRedirect = [];
function onBeforeRedirect(details)
{
info(`onBeforeRedirect ${details.url} -> ${details.redirectUrl}`);
checkType(details);
if (details.url.startsWith(BASE)) {
beforeRedirect.push(details.url);
is(details.browser, expected_browser, "correct <browser> element");
checkType(details);
let expectedUrl = details.url.replace("_redirect.", "_good.").replace(/\w+_redirection\..*/, "dummy_page.html")
is(details.redirectUrl, expectedUrl, "Correct redirectUrl value");
}
let id = windowIDs.get(details.url);
is(id, details.windowId, "window ID same in onBeforeRedirect as onBeforeRequest");
// associate stored windowId with final url
windowIDs.set(details.redirectUrl, details.windowId);
return {};
}
var headersReceived = [];
function onResponseStarted(details)
@ -94,6 +116,7 @@ const expected_requested = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_script_xhr.js",
BASE + "/file_WebRequest_page2.html",
BASE + "/nonexistent_script_url.js",
BASE + "/WebRequest_redirection.sjs",
BASE + "/xhr_resource"];
const expected_sendHeaders = [BASE + "/file_WebRequest_page1.html",
@ -106,8 +129,13 @@ const expected_sendHeaders = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_script_xhr.js",
BASE + "/file_WebRequest_page2.html",
BASE + "/nonexistent_script_url.js",
BASE + "/WebRequest_redirection.sjs",
BASE + "/dummy_page.html",
BASE + "/xhr_resource"];
const expected_beforeRedirect = expected_sendHeaders.filter(u => /_redirect\./.test(u))
.concat(BASE + "/WebRequest_redirection.sjs");
const expected_headersReceived = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_style_good.css",
BASE + "/file_image_good.png",
@ -115,6 +143,7 @@ const expected_headersReceived = [BASE + "/file_WebRequest_page1.html",
BASE + "/file_script_xhr.js",
BASE + "/file_WebRequest_page2.html",
BASE + "/nonexistent_script_url.js",
BASE + "/dummy_page.html",
BASE + "/xhr_resource"];
function removeDupes(list)
@ -144,17 +173,20 @@ function* test_once()
{
WebRequest.onBeforeRequest.addListener(onBeforeRequest, null, ["blocking"]);
WebRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, null, ["blocking"]);
WebRequest.onBeforeRedirect.addListener(onBeforeRedirect);
WebRequest.onResponseStarted.addListener(onResponseStarted);
gBrowser.selectedTab = gBrowser.addTab();
let browser = gBrowser.selectedBrowser;
expected_browser = browser;
yield waitForLoad();
yield BrowserTestUtils.browserLoaded(expected_browser);
browser.messageManager.loadFrameScript(`data:,content.location = "${URL}";`, false);
yield waitForLoad();
yield BrowserTestUtils.browserLoaded(expected_browser);
expected_browser = null;
let win = browser.contentWindow.wrappedJSObject;
is(win.success, 2, "Good script ran");
@ -163,26 +195,19 @@ function* test_once()
let style = browser.contentWindow.getComputedStyle(browser.contentDocument.getElementById("test"), null);
is(style.getPropertyValue("color"), "rgb(255, 0, 0)", "Good CSS loaded");
gBrowser.removeCurrentTab();
BrowserTestUtils.removeTab(gBrowser.selectedTab);
compareLists(requested, expected_requested, "requested");
compareLists(sendHeaders, expected_sendHeaders, "sendHeaders");
compareLists(beforeRedirect, expected_beforeRedirect, "beforeRedirect");
compareLists(headersReceived, expected_headersReceived, "headersReceived");
WebRequest.onBeforeRequest.removeListener(onBeforeRequest);
WebRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders);
WebRequest.onBeforeRedirect.removeListener(onBeforeRedirect);
WebRequest.onResponseStarted.removeListener(onResponseStarted);
}
// Run the test twice to make sure it works with caching.
add_task(test_once);
add_task(test_once);
function waitForLoad(browser = gBrowser.selectedBrowser) {
return new Promise(resolve => {
browser.addEventListener("load", function listener() {
browser.removeEventListener("load", listener, true);
resolve();
}, true);
});
}

View File

@ -24,6 +24,6 @@
<script src="nonexistent_script_url.js"></script>
<iframe src="file_WebRequest_page2.html" width="200" height="200"></iframe>
<iframe src="WebRequest_redirection.sjs" width="200" height="50"></iframe>
</body>
</html>