1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
|
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* exported ProductAddonChecker */
var EXPORTED_SYMBOLS = ["ProductAddonChecker"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
const { CertUtils } = ChromeUtils.import(
"resource://gre/modules/CertUtils.jsm"
);
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["XMLHttpRequest"]);
// This exists so that tests can override the XHR behaviour for downloading
// the addon update XML file.
var CreateXHR = function() {
return new XMLHttpRequest();
};
var logger = Log.repository.getLogger("addons.productaddons");
/**
* Number of milliseconds after which we need to cancel `downloadXML`.
*
* Bug 1087674 suggests that the XHR we use in `downloadXML` may
* never terminate in presence of network nuisances (e.g. strange
* antivirus behavior). This timeout is a defensive measure to ensure
* that we fail cleanly in such case.
*/
const TIMEOUT_DELAY_MS = 20000;
// How much of a file to read into memory at a time for hashing
const HASH_CHUNK_SIZE = 8192;
/**
* Gets the status of an XMLHttpRequest either directly or from its underlying
* channel.
*
* @param request
* The XMLHttpRequest.
* @return an integer status value.
*/
function getRequestStatus(request) {
let status = null;
try {
status = request.status;
} catch (e) {}
if (status != null) {
return status;
}
return request.channel.QueryInterface(Ci.nsIRequest).status;
}
/**
* Downloads an XML document from a URL optionally testing the SSL certificate
* for certain attributes.
*
* @param url
* The url to download from.
* @param allowNonBuiltIn
* Whether to trust SSL certificates without a built-in CA issuer.
* @param allowedCerts
* The list of certificate attributes to match the SSL certificate
* against or null to skip checks.
* @return a promise that resolves to the DOM document downloaded or rejects
* with a JS exception in case of error.
*/
function downloadXML(url, allowNonBuiltIn = false, allowedCerts = null) {
return new Promise((resolve, reject) => {
let request = CreateXHR();
// This is here to let unit test code override XHR
if (request.wrappedJSObject) {
request = request.wrappedJSObject;
}
request.open("GET", url, true);
request.channel.notificationCallbacks = new CertUtils.BadCertHandler(
allowNonBuiltIn
);
// Prevent the request from reading from the cache.
request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
// Prevent the request from writing to the cache.
request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
// Don't send any cookies
request.channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;
// Use conservative TLS settings. See bug 1325501.
// TODO move to ServiceRequest.
if (request.channel instanceof Ci.nsIHttpChannelInternal) {
request.channel.QueryInterface(
Ci.nsIHttpChannelInternal
).beConservative = true;
}
request.timeout = TIMEOUT_DELAY_MS;
request.overrideMimeType("text/xml");
// The Cache-Control header is only interpreted by proxies and the
// final destination. It does not help if a resource is already
// cached locally.
request.setRequestHeader("Cache-Control", "no-cache");
// HTTP/1.0 servers might not implement Cache-Control and
// might only implement Pragma: no-cache
request.setRequestHeader("Pragma", "no-cache");
let fail = event => {
let request = event.target;
let status = getRequestStatus(request);
let message =
"Failed downloading XML, status: " + status + ", reason: " + event.type;
logger.warn(message);
let ex = new Error(message);
ex.status = status;
reject(ex);
};
let success = event => {
logger.info("Completed downloading document");
let request = event.target;
try {
CertUtils.checkCert(request.channel, allowNonBuiltIn, allowedCerts);
} catch (ex) {
logger.error("Request failed certificate checks: " + ex);
ex.status = getRequestStatus(request);
reject(ex);
return;
}
resolve(request.responseXML);
};
request.addEventListener("error", fail);
request.addEventListener("abort", fail);
request.addEventListener("timeout", fail);
request.addEventListener("load", success);
logger.info("sending request to: " + url);
request.send(null);
});
}
/**
* Parses a list of add-ons from a DOM document.
*
* @param document
* The DOM document to parse.
* @return null if there is no <addons> element otherwise an object containing
* an array of the addons listed and a field notifying whether the
* fallback was used.
*/
function parseXML(document) {
// Check that the root element is correct
if (document.documentElement.localName != "updates") {
throw new Error(
"got node name: " +
document.documentElement.localName +
", expected: updates"
);
}
// Check if there are any addons elements in the updates element
let addons = document.querySelector("updates:root > addons");
if (!addons) {
return null;
}
let results = [];
let addonList = document.querySelectorAll("updates:root > addons > addon");
for (let addonElement of addonList) {
let addon = {};
for (let name of [
"id",
"URL",
"hashFunction",
"hashValue",
"version",
"size",
]) {
if (addonElement.hasAttribute(name)) {
addon[name] = addonElement.getAttribute(name);
}
}
addon.size = Number(addon.size) || undefined;
results.push(addon);
}
return {
usedFallback: false,
addons: results,
};
}
/**
* Downloads file from a URL using XHR.
*
* @param url
* The url to download from.
* @param options (optional)
* @param options.httpsOnlyNoUpgrade
* Prevents upgrade to https:// when HTTPS-Only Mode is enabled.
* @return a promise that resolves to the path of a temporary file or rejects
* with a JS exception in case of error.
*/
function downloadFile(url, options = { httpsOnlyNoUpgrade: false }) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.onload = function(response) {
logger.info("downloadXHR File download. status=" + xhr.status);
if (xhr.status != 200 && xhr.status != 206) {
reject(Components.Exception("File download failed", xhr.status));
return;
}
(async function() {
let f = await OS.File.openUnique(
OS.Path.join(OS.Constants.Path.tmpDir, "tmpaddon")
);
let path = f.path;
logger.info(`Downloaded file will be saved to ${path}`);
await f.file.close();
await OS.File.writeAtomic(path, new Uint8Array(xhr.response));
return path;
})().then(resolve, reject);
};
let fail = event => {
let request = event.target;
let status = getRequestStatus(request);
let message =
"Failed downloading via XHR, status: " +
status +
", reason: " +
event.type;
logger.warn(message);
let ex = new Error(message);
ex.status = status;
reject(ex);
};
xhr.addEventListener("error", fail);
xhr.addEventListener("abort", fail);
xhr.responseType = "arraybuffer";
try {
xhr.open("GET", url);
if (options.httpsOnlyNoUpgrade) {
xhr.channel.loadInfo.httpsOnlyStatus |=
Ci.nsILoadInfo.HTTPS_ONLY_EXEMPT;
}
// Allow deprecated HTTP request from SystemPrincipal
xhr.channel.loadInfo.allowDeprecatedSystemRequests = true;
// Use conservative TLS settings. See bug 1325501.
// TODO move to ServiceRequest.
if (xhr.channel instanceof Ci.nsIHttpChannelInternal) {
xhr.channel.QueryInterface(
Ci.nsIHttpChannelInternal
).beConservative = true;
}
xhr.send(null);
} catch (ex) {
reject(ex);
}
});
}
/**
* Convert a string containing binary values to hex.
*/
function binaryToHex(input) {
let result = "";
for (let i = 0; i < input.length; ++i) {
let hex = input.charCodeAt(i).toString(16);
if (hex.length == 1) {
hex = "0" + hex;
}
result += hex;
}
return result;
}
/**
* Calculates the hash of a file.
*
* @param hashFunction
* The type of hash function to use, must be supported by nsICryptoHash.
* @param path
* The path of the file to hash.
* @return a promise that resolves to hash of the file or rejects with a JS
* exception in case of error.
*/
var computeHash = async function(hashFunction, path) {
let file = await OS.File.open(path, { existing: true, read: true });
try {
let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
Ci.nsICryptoHash
);
hasher.initWithString(hashFunction);
let bytes;
do {
bytes = await file.read(HASH_CHUNK_SIZE);
hasher.update(bytes, bytes.length);
} while (bytes.length == HASH_CHUNK_SIZE);
return binaryToHex(hasher.finish(false));
} finally {
await file.close();
}
};
/**
* Verifies that a downloaded file matches what was expected.
*
* @param properties
* The properties to check, `hashFunction` with `hashValue`
* are supported. Any properties missing won't be checked.
* @param path
* The path of the file to check.
* @return a promise that resolves if the file matched or rejects with a JS
* exception in case of error.
*/
var verifyFile = async function(properties, path) {
if (properties.size !== undefined) {
let stat = await OS.File.stat(path);
if (stat.size != properties.size) {
throw new Error(
"Downloaded file was " +
stat.size +
" bytes but expected " +
properties.size +
" bytes."
);
}
}
if (properties.hashFunction !== undefined) {
let expectedDigest = properties.hashValue.toLowerCase();
let digest = await computeHash(properties.hashFunction, path);
if (digest != expectedDigest) {
throw new Error(
"Hash was `" + digest + "` but expected `" + expectedDigest + "`."
);
}
}
};
const ProductAddonChecker = {
/**
* Downloads a list of add-ons from a URL optionally testing the SSL
* certificate for certain attributes.
*
* @param url
* The url to download from.
* @param allowNonBuiltIn
* Whether to trust SSL certificates without a built-in CA issuer.
* @param allowedCerts
* The list of certificate attributes to match the SSL certificate
* against or null to skip checks.
* @return a promise that resolves to an object containing the list of add-ons
* and whether the local fallback was used, or rejects with a JS
* exception in case of error.
*/
getProductAddonList(url, allowNonBuiltIn = false, allowedCerts = null) {
return downloadXML(url, allowNonBuiltIn, allowedCerts).then(parseXML);
},
/**
* Downloads an add-on to a local file and checks that it matches the expected
* file. The caller is responsible for deleting the temporary file returned.
*
* @param addon
* The addon to download.
* @param options (optional)
* @param options.httpsOnlyNoUpgrade
* Prevents upgrade to https:// when HTTPS-Only Mode is enabled.
* @return a promise that resolves to the temporary file downloaded or rejects
* with a JS exception in case of error.
*/
async downloadAddon(addon, options = { httpsOnlyNoUpgrade: false }) {
let path = await downloadFile(addon.URL, options);
try {
await verifyFile(addon, path);
return path;
} catch (e) {
await OS.File.remove(path);
throw e;
}
},
};
|