mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-03 10:33:33 +00:00
Bug 1254204: Part 1 - Apply WebRequest header changes differentially, after all listeners have run in parallel. r=mixedpuppy
MozReview-Commit-ID: Jk1ja5Y3lMI --HG-- extra : rebase_source : 83802582693ca6bed14eee489adc909654f31fb6
This commit is contained in:
parent
37a3b973b9
commit
5aed24393a
@ -170,10 +170,10 @@ function backgroundScript() {
|
||||
"X-WebRequest-request-binary": "binary",
|
||||
},
|
||||
modified: {
|
||||
"User-Agent": "WebRequest",
|
||||
"user-agent": "WebRequest",
|
||||
},
|
||||
deleted: [
|
||||
"Referer",
|
||||
"referer",
|
||||
],
|
||||
},
|
||||
response: {
|
||||
@ -182,11 +182,11 @@ function backgroundScript() {
|
||||
"X-WebRequest-response-binary": "binary",
|
||||
},
|
||||
modified: {
|
||||
"Server": "WebRequest",
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
"server": "WebRequest",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
},
|
||||
deleted: [
|
||||
"Connection",
|
||||
"connection",
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -200,8 +200,8 @@ function backgroundScript() {
|
||||
let headers = details[`${phase}Headers`];
|
||||
browser.test.assertTrue(Array.isArray(headers), `${phase}Headers array present`);
|
||||
|
||||
let processedMark = "WebRequest-processed";
|
||||
if (headers.find(h => h.name === processedMark)) {
|
||||
let processedMark = "webrequest-processed";
|
||||
if (headers.find(h => h.name.toLowerCase() === processedMark)) {
|
||||
// This may happen because of redirections or cache
|
||||
browser.test.log(`${phase}Headers in ${details.requestId} already processed`);
|
||||
skippedRequests.add(details.requestId);
|
||||
@ -223,15 +223,17 @@ function backgroundScript() {
|
||||
}
|
||||
|
||||
let modifiedAny = false;
|
||||
for (let header of headers.filter(h => h.name in modified)) {
|
||||
header.value = modified[header.name];
|
||||
modifiedAny = true;
|
||||
for (let header of headers) {
|
||||
if (header.name.toLowerCase() in modified) {
|
||||
header.value = modified[header.name.toLowerCase()];
|
||||
modifiedAny = true;
|
||||
}
|
||||
}
|
||||
browser.test.assertTrue(modifiedAny, `at least one ${phase}Headers element to modify`);
|
||||
|
||||
let deletedAny = false;
|
||||
for (let j = headers.length; j-- > 0;) {
|
||||
if (deleted.includes(headers[j].name)) {
|
||||
if (deleted.includes(headers[j].name.toLowerCase())) {
|
||||
headers.splice(j, 1);
|
||||
deletedAny = true;
|
||||
}
|
||||
@ -251,7 +253,7 @@ function backgroundScript() {
|
||||
|
||||
let {added, modified, deleted} = testHeaders[phase];
|
||||
for (let name in added) {
|
||||
browser.test.assertTrue(headers.some(h => h.name === name && h.value === added[name]), `header ${name} correctly injected in ${phase}Headers`);
|
||||
browser.test.assertTrue(headers.some(h => h.name.toLowerCase() === name.toLowerCase() && h.value === added[name]), `header ${name} correctly injected in ${phase}Headers`);
|
||||
}
|
||||
|
||||
let modifiedAny = false;
|
||||
|
@ -115,6 +115,115 @@ function mergeStatus(data, channel, event) {
|
||||
}
|
||||
}
|
||||
|
||||
class HeaderChanger {
|
||||
constructor(channel) {
|
||||
this.channel = channel;
|
||||
|
||||
this.originalHeaders = new Map();
|
||||
this.visitHeaders((name, value) => {
|
||||
this.originalHeaders.set(name.toLowerCase(), value);
|
||||
});
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return Array.from(this.originalHeaders,
|
||||
([name, value]) => ({name, value}));
|
||||
}
|
||||
|
||||
validateHeaders(headers) {
|
||||
// We should probably use schema validation for this.
|
||||
|
||||
if (!Array.isArray(headers)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return headers.every(header => {
|
||||
if (typeof header !== "object" || header === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof header.name !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (typeof header.value === "string" ||
|
||||
Array.isArray(header.binaryValue));
|
||||
});
|
||||
}
|
||||
|
||||
applyChanges(headers) {
|
||||
if (!this.validateHeaders(headers)) {
|
||||
/* globals uneval */
|
||||
Cu.reportError(`Invalid header array: ${uneval(headers)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let newHeaders = new Set(headers.map(
|
||||
({name}) => name.toLowerCase()));
|
||||
|
||||
// Remove missing headers.
|
||||
for (let name of this.originalHeaders.keys()) {
|
||||
if (!newHeaders.has(name)) {
|
||||
this.setHeader(name, "");
|
||||
}
|
||||
}
|
||||
|
||||
// Set new or changed headers.
|
||||
for (let {name, value, binaryValue} of headers) {
|
||||
if (binaryValue) {
|
||||
value = String.fromCharCode(...binaryValue);
|
||||
}
|
||||
if (value !== this.originalHeaders.get(name.toLowerCase())) {
|
||||
this.setHeader(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RequestHeaderChanger extends HeaderChanger {
|
||||
setHeader(name, value) {
|
||||
try {
|
||||
this.channel.setRequestHeader(name, value, false);
|
||||
} catch (e) {
|
||||
Cu.reportError(new Error(`Error setting request header ${name}: ${e}`));
|
||||
}
|
||||
}
|
||||
|
||||
visitHeaders(visitor) {
|
||||
this.channel.visitRequestHeaders(visitor);
|
||||
}
|
||||
}
|
||||
|
||||
class ResponseHeaderChanger extends HeaderChanger {
|
||||
setHeader(name, value) {
|
||||
try {
|
||||
if (name.toLowerCase() === "content-type" && value) {
|
||||
// The Content-Type header value can't be modified, so we
|
||||
// set the channel's content type directly, instead, and
|
||||
// record that we made the change for the sake of
|
||||
// subsequent observers.
|
||||
this.channel.contentType = value;
|
||||
|
||||
getData(this.channel).contentType = value;
|
||||
} else {
|
||||
this.channel.setResponseHeader(name, value, false);
|
||||
}
|
||||
} catch (e) {
|
||||
Cu.reportError(new Error(`Error setting response header ${name}: ${e}`));
|
||||
}
|
||||
}
|
||||
|
||||
visitHeaders(visitor) {
|
||||
this.channel.visitResponseHeaders((name, value) => {
|
||||
if (name.toLowerCase() === "content-type") {
|
||||
value = getData(this.channel).contentType || value;
|
||||
}
|
||||
|
||||
visitor(name, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var HttpObserverManager;
|
||||
|
||||
var ContentPolicyManager = {
|
||||
@ -371,61 +480,6 @@ HttpObserverManager = {
|
||||
}
|
||||
},
|
||||
|
||||
getHeaders(channel, method, event) {
|
||||
let headers = [];
|
||||
let visitor = {
|
||||
visitHeader(name, value) {
|
||||
try {
|
||||
value = channel.getProperty(`webrequest-header-${name.toLowerCase()}`);
|
||||
} catch (e) {
|
||||
// This will throw if the property does not exist.
|
||||
}
|
||||
headers.push({name, value});
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIHttpHeaderVisitor,
|
||||
Ci.nsISupports]),
|
||||
};
|
||||
|
||||
try {
|
||||
channel.QueryInterface(Ci.nsIPropertyBag);
|
||||
channel[method](visitor);
|
||||
} catch (e) {
|
||||
Cu.reportError(`webRequest Error: ${e} trying to perform ${method} in ${event}@${channel.name}`);
|
||||
}
|
||||
return headers;
|
||||
},
|
||||
|
||||
replaceHeaders(headers, originalNames, setHeader) {
|
||||
let failures = new Set();
|
||||
// Start by clearing everything.
|
||||
for (let name of originalNames) {
|
||||
try {
|
||||
setHeader(name, "");
|
||||
} catch (e) {
|
||||
// Let's collect physiological failures in order
|
||||
// to know what is worth reporting.
|
||||
failures.add(name);
|
||||
}
|
||||
}
|
||||
try {
|
||||
for (let {name, value, binaryValue} of headers) {
|
||||
try {
|
||||
if (Array.isArray(binaryValue)) {
|
||||
value = String.fromCharCode.apply(String, binaryValue);
|
||||
}
|
||||
setHeader(name, value);
|
||||
} catch (e) {
|
||||
if (!failures.has(name)) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
let channel = subject.QueryInterface(Ci.nsIHttpChannel);
|
||||
switch (topic) {
|
||||
@ -518,9 +572,6 @@ HttpObserverManager = {
|
||||
loadInfo.externalContentPolicyType :
|
||||
Ci.nsIContentPolicy.TYPE_OTHER;
|
||||
|
||||
let requestHeaderNames;
|
||||
let responseHeaderNames;
|
||||
|
||||
let requestBody;
|
||||
|
||||
let includeStatus = (
|
||||
@ -530,8 +581,18 @@ HttpObserverManager = {
|
||||
kind === "onStop"
|
||||
) && channel instanceof Ci.nsIHttpChannel;
|
||||
|
||||
let requestHeaders = new RequestHeaderChanger(channel);
|
||||
let responseHeaders;
|
||||
try {
|
||||
responseHeaders = new ResponseHeaderChanger(channel);
|
||||
} catch (e) {
|
||||
// Just ignore this for the request phases where response headers
|
||||
// aren't yet available.
|
||||
}
|
||||
|
||||
let commonData = null;
|
||||
let uri = channel.URI;
|
||||
let handlerResults = [];
|
||||
for (let [callback, opts] of listeners.entries()) {
|
||||
if (!this.shouldRunListener(policyType, uri, opts.filter)) {
|
||||
continue;
|
||||
@ -581,15 +642,17 @@ HttpObserverManager = {
|
||||
Object.assign(commonData, extraData);
|
||||
}
|
||||
}
|
||||
|
||||
let data = Object.assign({}, commonData);
|
||||
|
||||
if (opts.requestHeaders) {
|
||||
data.requestHeaders = this.getHeaders(channel, "visitRequestHeaders", kind);
|
||||
requestHeaderNames = data.requestHeaders.map(h => h.name);
|
||||
data.requestHeaders = requestHeaders.toArray();
|
||||
}
|
||||
if (opts.responseHeaders) {
|
||||
data.responseHeaders = this.getHeaders(channel, "visitResponseHeaders", kind);
|
||||
responseHeaderNames = data.responseHeaders.map(h => h.name);
|
||||
|
||||
if (opts.responseHeaders && responseHeaders) {
|
||||
data.responseHeaders = responseHeaders.toArray();
|
||||
}
|
||||
|
||||
if (opts.requestBody) {
|
||||
if (requestBody === undefined) {
|
||||
requestBody = WebRequestUpload.createRequestBody(channel);
|
||||
@ -598,53 +661,44 @@ HttpObserverManager = {
|
||||
data.requestBody = requestBody;
|
||||
}
|
||||
}
|
||||
|
||||
if (includeStatus) {
|
||||
mergeStatus(data, channel, kind);
|
||||
}
|
||||
|
||||
let result = null;
|
||||
try {
|
||||
result = callback(data);
|
||||
let result = callback(data);
|
||||
|
||||
if (result && typeof result === "object" && opts.blocking) {
|
||||
handlerResults.push({opts, result});
|
||||
}
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!result || !opts.blocking) {
|
||||
continue;
|
||||
}
|
||||
for (let {opts, result} of handlerResults) {
|
||||
if (result.cancel) {
|
||||
channel.cancel(Cr.NS_ERROR_ABORT);
|
||||
this.errorCheck(channel, loadContext);
|
||||
return false;
|
||||
}
|
||||
if (result.redirectUrl) {
|
||||
channel.redirectTo(BrowserUtils.makeURI(result.redirectUrl));
|
||||
return false;
|
||||
}
|
||||
if (opts.requestHeaders && result.requestHeaders) {
|
||||
this.replaceHeaders(
|
||||
result.requestHeaders, requestHeaderNames,
|
||||
(name, value) => channel.setRequestHeader(name, value, false)
|
||||
);
|
||||
}
|
||||
if (opts.responseHeaders && result.responseHeaders) {
|
||||
this.replaceHeaders(
|
||||
result.responseHeaders, responseHeaderNames,
|
||||
(name, value) => {
|
||||
if (name.toLowerCase() === "content-type" && value) {
|
||||
// The Content-Type header value can't be modified, so we
|
||||
// set the channel's content type directly, instead, and
|
||||
// record that we made the change for the sake of
|
||||
// subsequent observers.
|
||||
channel.contentType = value;
|
||||
|
||||
channel.QueryInterface(Ci.nsIWritablePropertyBag);
|
||||
channel.setProperty("webrequest-header-content-type", value);
|
||||
} else {
|
||||
channel.setResponseHeader(name, value, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
if (result.redirectUrl) {
|
||||
try {
|
||||
channel.redirectTo(BrowserUtils.makeURI(result.redirectUrl));
|
||||
return false;
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.requestHeaders && result.requestHeaders) {
|
||||
requestHeaders.applyChanges(result.requestHeaders);
|
||||
}
|
||||
|
||||
if (opts.responseHeaders && result.responseHeaders) {
|
||||
responseHeaders.applyChanges(result.responseHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user