bug 1247793 - Implement download() options: method, headers and body, r=aswan

MozReview-Commit-ID: 2muhcweY8Fo

--HG--
extra : rebase_source : 30f0aabce48717caa963610c2fcf831a0b890702
This commit is contained in:
Tomislav Jovanovic 2016-10-22 12:06:22 +02:00
Родитель 3b68400cf3
Коммит 204e680596
3 изменённых файлов: 155 добавлений и 10 удалений

Просмотреть файл

@ -38,6 +38,14 @@ const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
const DOWNLOAD_ITEM_CHANGE_FIELDS = ["endTime", "state", "paused", "canResume",
"error", "exists"];
// From https://fetch.spec.whatwg.org/#forbidden-header-name
const FORBIDDEN_HEADERS = ["ACCEPT-CHARSET", "ACCEPT-ENCODING",
"ACCESS-CONTROL-REQUEST-HEADERS", "ACCESS-CONTROL-REQUEST-METHOD",
"CONNECTION", "CONTENT-LENGTH", "COOKIE", "COOKIE2", "DATE", "DNT",
"EXPECT", "HOST", "KEEP-ALIVE", "ORIGIN", "REFERER", "TE", "TRAILER",
"TRANSFER-ENCODING", "UPGRADE", "VIA"];
const FORBIDDEN_PREFIXES = /^PROXY-|^SEC-/i;
class DownloadItem {
constructor(id, download, extension) {
@ -418,6 +426,38 @@ extensions.registerSchemaAPI("downloads", "addon_parent", context => {
return Promise.reject({message: "conflictAction prompt not yet implemented"});
}
if (options.headers) {
for (let {name} of options.headers) {
if (FORBIDDEN_HEADERS.includes(name.toUpperCase()) || name.match(FORBIDDEN_PREFIXES)) {
return Promise.reject({message: "Forbidden request header name"});
}
}
}
// Handle method, headers and body options.
function adjustChannel(channel) {
if (channel instanceof Ci.nsIHttpChannel) {
const method = options.method || "GET";
channel.requestMethod = method;
if (options.headers) {
for (let {name, value} of options.headers) {
channel.setRequestHeader(name, value, false);
}
}
if (options.body != null) {
const stream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stream.setData(options.body, options.body.length);
channel.QueryInterface(Ci.nsIUploadChannel2);
channel.explicitSetUploadStream(stream, null, -1, method, false);
}
}
return Promise.resolve();
}
function createTarget(downloadsDir) {
let target;
if (filename) {
@ -481,13 +521,23 @@ extensions.registerSchemaAPI("downloads", "addon_parent", context => {
let download;
return Downloads.getPreferredDownloadsDirectory()
.then(downloadsDir => createTarget(downloadsDir))
.then(target => Downloads.createDownload({
source: options.url,
target: {
path: target,
partFilePath: target + ".part",
},
})).then(dl => {
.then(target => {
const source = {
url: options.url,
};
if (options.method || options.headers || options.body) {
source.adjustChannel = adjustChannel;
}
return Downloads.createDownload({
source,
target: {
path: target,
partFilePath: target + ".part",
},
});
}).then(dl => {
download = dl;
return DownloadMap.getDownloadList();
}).then(list => {

Просмотреть файл

@ -386,7 +386,6 @@
"type": "boolean"
},
"method": {
"unsupported": true,
"description": "The HTTP method to use if the URL uses the HTTP[S] protocol.",
"enum": [
"GET",
@ -396,7 +395,6 @@
"type": "string"
},
"headers": {
"unsupported": true,
"optional": true,
"type": "array",
"description": "Extra HTTP headers to send with the request if the URL uses the HTTP[s] protocol. Each header is represented as a dictionary containing the keys <code>name</code> and either <code>value</code> or <code>binaryValue</code>, restricted to those allowed by XMLHttpRequest.",
@ -415,7 +413,6 @@
}
},
"body": {
"unsupported": true,
"description": "Post body.",
"optional": true,
"type": "string"

Просмотреть файл

@ -255,3 +255,101 @@ add_task(function* test_downloads() {
yield extension.unload();
});
add_task(function* test_download_post() {
const server = createHttpServer();
const url = `http://localhost:${server.identity.primaryPort}/post-log`;
let received;
server.registerPathHandler("/post-log", request => {
received = request;
});
// Confirm received vs. expected values.
function confirm(method, headers = {}, body) {
equal(received.method, method, "method is correct");
for (let name in headers) {
ok(received.hasHeader(name), `header ${name} received`);
equal(received.getHeader(name), headers[name], `header ${name} is correct`);
}
if (body) {
const str = NetUtil.readInputStreamToString(received.bodyInputStream,
received.bodyInputStream.available());
equal(str, body, "body is correct");
}
}
function background() {
browser.test.onMessage.addListener(options => {
Promise.resolve()
.then(() => browser.downloads.download(options))
.catch(err => browser.test.sendMessage("done", {err: err.message}));
});
browser.downloads.onChanged.addListener(({state}) => {
if (state && state.current === "complete") {
browser.test.sendMessage("done", {ok: true});
}
});
}
const manifest = {permissions: ["downloads"]};
const extension = ExtensionTestUtils.loadExtension({background, manifest});
yield extension.startup();
function download(options) {
options.url = url;
options.conflictAction = "overwrite";
extension.sendMessage(options);
return extension.awaitMessage("done");
}
// Test method option.
let result = yield download({});
ok(result.ok, "download works without the method option, defaults to GET");
confirm("GET");
result = yield download({method: "PUT"});
ok(!result.ok, "download rejected with PUT method");
ok(/method: Invalid enumeration/.test(result.err), "descriptive error message");
result = yield download({method: "POST"});
ok(result.ok, "download works with POST method");
confirm("POST");
// Test body option values.
result = yield download({body: []});
ok(!result.ok, "download rejected because of non-string body");
ok(/body: Expected string/.test(result.err), "descriptive error message");
result = yield download({method: "POST", body: "of work"});
ok(result.ok, "download works with POST method and body");
confirm("POST", {"Content-Length": 7}, "of work");
// Test custom headers.
result = yield download({headers: [{name: "X-Custom"}]});
ok(!result.ok, "download rejected because of missing header value");
ok(/"value" is required/.test(result.err), "descriptive error message");
result = yield download({headers: [{name: "X-Custom", value: "13"}]});
ok(result.ok, "download works with a custom header");
confirm("GET", {"X-Custom": "13"});
// Test forbidden headers.
result = yield download({headers: [{name: "DNT", value: "1"}]});
ok(!result.ok, "download rejected because of forbidden header name DNT");
ok(/Forbidden request header/.test(result.err), "descriptive error message");
result = yield download({headers: [{name: "Proxy-Connection", value: "keep"}]});
ok(!result.ok, "download rejected because of forbidden header name prefix Proxy-");
ok(/Forbidden request header/.test(result.err), "descriptive error message");
result = yield download({headers: [{name: "Sec-ret", value: "13"}]});
ok(!result.ok, "download rejected because of forbidden header name prefix Sec-");
ok(/Forbidden request header/.test(result.err), "descriptive error message");
remove("post-log");
yield extension.unload();
});