|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 const Cc = Components.classes; |
|
6 const Ci = Components.interfaces; |
|
7 const Cr = Components.results; |
|
8 const Cu = Components.utils; |
|
9 |
|
10 // COMPLETE_LENGTH and PARTIAL_LENGTH copied from nsUrlClassifierDBService.h, |
|
11 // they correspond to the length, in bytes, of a hash prefix and the total |
|
12 // hash. |
|
13 const COMPLETE_LENGTH = 32; |
|
14 const PARTIAL_LENGTH = 4; |
|
15 |
|
16 // These backoff related constants are taken from v2 of the Google Safe Browsing |
|
17 // API. |
|
18 // BACKOFF_ERRORS: the number of errors incurred until we start to back off. |
|
19 // BACKOFF_INTERVAL: the initial time, in seconds, to wait once we start backing |
|
20 // off. |
|
21 // BACKOFF_MAX: as the backoff time doubles after each failure, this is a |
|
22 // ceiling on the time to wait, in seconds. |
|
23 // BACKOFF_TIME: length of the interval of time, in seconds, during which errors |
|
24 // are taken into account. |
|
25 |
|
26 const BACKOFF_ERRORS = 2; |
|
27 const BACKOFF_INTERVAL = 30 * 60; |
|
28 const BACKOFF_MAX = 8 * 60 * 60; |
|
29 const BACKOFF_TIME = 5 * 60; |
|
30 |
|
31 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
32 Cu.import("resource://gre/modules/Services.jsm"); |
|
33 |
|
34 function HashCompleter() { |
|
35 // This is a HashCompleterRequest and is used by multiple calls to |complete| |
|
36 // in succession to avoid unnecessarily creating requests. Once it has been |
|
37 // started, this is set to null again. |
|
38 this._currentRequest = null; |
|
39 |
|
40 // Whether we have been informed of a shutdown by the xpcom-shutdown event. |
|
41 this._shuttingDown = false; |
|
42 |
|
43 // All of these backoff properties are different per completer as the DB |
|
44 // service keeps one completer per table. |
|
45 // |
|
46 // _backoff tells us whether we are "backing off" from making requests. |
|
47 // It is set in |noteServerResponse| and set after a number of failures. |
|
48 this._backoff = false; |
|
49 // _backoffTime tells us how long we should wait (in seconds) before making |
|
50 // another request. |
|
51 this._backoffTime = 0; |
|
52 // _nextRequestTime is the earliest time at which we are allowed to make |
|
53 // another request by the backoff policy. It is measured in milliseconds. |
|
54 this._nextRequestTime = 0; |
|
55 // A list of the times at which a failed request was made, recorded in |
|
56 // |noteServerResponse|. Sorted by oldest to newest and its length is clamped |
|
57 // by BACKOFF_ERRORS. |
|
58 this._errorTimes = []; |
|
59 |
|
60 Services.obs.addObserver(this, "xpcom-shutdown", true); |
|
61 } |
|
62 HashCompleter.prototype = { |
|
63 classID: Components.ID("{9111de73-9322-4bfc-8b65-2b727f3e6ec8}"), |
|
64 QueryInterface: XPCOMUtils.generateQI([Ci.nsIUrlClassifierHashCompleter, |
|
65 Ci.nsIRunnable, |
|
66 Ci.nsIObserver, |
|
67 Ci.nsISupportsWeakReference, |
|
68 Ci.nsISupports]), |
|
69 |
|
70 // This is mainly how the HashCompleter interacts with other components. |
|
71 // Even though it only takes one partial hash and callback, subsequent |
|
72 // calls are made into the same HTTP request by using a thread dispatch. |
|
73 complete: function HC_complete(aPartialHash, aCallback) { |
|
74 if (!this._currentRequest) { |
|
75 this._currentRequest = new HashCompleterRequest(this); |
|
76 |
|
77 // It's possible for getHashUrl to not be set, usually at start-up. |
|
78 if (this._getHashUrl) { |
|
79 Services.tm.currentThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL); |
|
80 } |
|
81 } |
|
82 |
|
83 this._currentRequest.add(aPartialHash, aCallback); |
|
84 }, |
|
85 |
|
86 // This is called when either the getHashUrl has been set or after a few calls |
|
87 // to |complete|. It starts off the HTTP request by making a |begin| call |
|
88 // to the HashCompleterRequest. |
|
89 run: function HC_run() { |
|
90 if (this._shuttingDown) { |
|
91 this._currentRequest = null; |
|
92 throw Cr.NS_ERROR_NOT_INITIALIZED; |
|
93 } |
|
94 |
|
95 if (!this._currentRequest) { |
|
96 return; |
|
97 } |
|
98 |
|
99 if (!this._getHashUrl) { |
|
100 throw Cr.NS_ERROR_NOT_INITIALIZED; |
|
101 } |
|
102 |
|
103 let url = this._getHashUrl; |
|
104 |
|
105 let uri = Services.io.newURI(url, null, null); |
|
106 this._currentRequest.setURI(uri); |
|
107 |
|
108 // If |begin| fails, we should get rid of our request. |
|
109 try { |
|
110 this._currentRequest.begin(); |
|
111 } |
|
112 finally { |
|
113 this._currentRequest = null; |
|
114 } |
|
115 }, |
|
116 |
|
117 get gethashUrl() { |
|
118 return this._getHashUrl; |
|
119 }, |
|
120 // Because we hold off on making a request until we have a valid getHashUrl, |
|
121 // we kick off the process here. |
|
122 set gethashUrl(aNewUrl) { |
|
123 this._getHashUrl = aNewUrl; |
|
124 |
|
125 if (this._currentRequest) { |
|
126 Services.tm.currentThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL); |
|
127 } |
|
128 }, |
|
129 |
|
130 // This handles all the logic about setting a back off time based on |
|
131 // server responses. It should only be called once in the life time of a |
|
132 // request. |
|
133 // The logic behind backoffs is documented in the Google Safe Browsing API, |
|
134 // the general idea is that a completer should go into backoff mode after |
|
135 // BACKOFF_ERRORS errors in the last BACKOFF_TIME seconds. From there, |
|
136 // we do not make a request for BACKOFF_INTERVAL seconds and for every failed |
|
137 // request after that we double how long to wait, to a maximum of BACKOFF_MAX. |
|
138 // Note that even in the case of a successful response we still keep a history |
|
139 // of past errors. |
|
140 noteServerResponse: function HC_noteServerResponse(aSuccess) { |
|
141 if (aSuccess) { |
|
142 this._backoff = false; |
|
143 this._nextRequestTime = 0; |
|
144 this._backoffTime = 0; |
|
145 return; |
|
146 } |
|
147 |
|
148 let now = Date.now(); |
|
149 |
|
150 // We only alter the size of |_errorTimes| here, so we can guarantee that |
|
151 // its length is at most BACKOFF_ERRORS. |
|
152 this._errorTimes.push(now); |
|
153 if (this._errorTimes.length > BACKOFF_ERRORS) { |
|
154 this._errorTimes.shift(); |
|
155 } |
|
156 |
|
157 if (this._backoff) { |
|
158 this._backoffTime *= 2; |
|
159 } else if (this._errorTimes.length == BACKOFF_ERRORS && |
|
160 ((now - this._errorTimes[0]) / 1000) <= BACKOFF_TIME) { |
|
161 this._backoff = true; |
|
162 this._backoffTime = BACKOFF_INTERVAL; |
|
163 } |
|
164 |
|
165 if (this._backoff) { |
|
166 this._backoffTime = Math.min(this._backoffTime, BACKOFF_MAX); |
|
167 this._nextRequestTime = now + (this._backoffTime * 1000); |
|
168 } |
|
169 }, |
|
170 |
|
171 // This is not included on the interface but is used to communicate to the |
|
172 // HashCompleterRequest. It returns a time in milliseconds. |
|
173 get nextRequestTime() { |
|
174 return this._nextRequestTime; |
|
175 }, |
|
176 |
|
177 observe: function HC_observe(aSubject, aTopic, aData) { |
|
178 if (aTopic == "xpcom-shutdown") { |
|
179 this._shuttingDown = true; |
|
180 } |
|
181 }, |
|
182 }; |
|
183 |
|
184 function HashCompleterRequest(aCompleter) { |
|
185 // HashCompleter object that created this HashCompleterRequest. |
|
186 this._completer = aCompleter; |
|
187 // The internal set of hashes and callbacks that this request corresponds to. |
|
188 this._requests = []; |
|
189 // URI to query for hash completions. Largely comes from the |
|
190 // browser.safebrowsing.gethashURL pref. |
|
191 this._uri = null; |
|
192 // nsIChannel that the hash completion query is transmitted over. |
|
193 this._channel = null; |
|
194 // Response body of hash completion. Created in onDataAvailable. |
|
195 this._response = ""; |
|
196 // Whether we have been informed of a shutdown by the xpcom-shutdown event. |
|
197 this._shuttingDown = false; |
|
198 } |
|
199 HashCompleterRequest.prototype = { |
|
200 QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver, |
|
201 Ci.nsIStreamListener, |
|
202 Ci.nsIObserver, |
|
203 Ci.nsISupports]), |
|
204 |
|
205 // This is called by the HashCompleter to add a hash and callback to the |
|
206 // HashCompleterRequest. It must be called before calling |begin|. |
|
207 add: function HCR_add(aPartialHash, aCallback) { |
|
208 this._requests.push({ |
|
209 partialHash: aPartialHash, |
|
210 callback: aCallback, |
|
211 responses: [], |
|
212 }); |
|
213 }, |
|
214 |
|
215 // This initiates the HTTP request. It can fail due to backoff timings and |
|
216 // will notify all callbacks as necessary. |
|
217 begin: function HCR_begin() { |
|
218 if (Date.now() < this._completer.nextRequestTime) { |
|
219 this.notifyFailure(Cr.NS_ERROR_ABORT); |
|
220 return; |
|
221 } |
|
222 |
|
223 Services.obs.addObserver(this, "xpcom-shutdown", false); |
|
224 |
|
225 try { |
|
226 this.openChannel(); |
|
227 } |
|
228 catch (err) { |
|
229 this.notifyFailure(err); |
|
230 throw err; |
|
231 } |
|
232 }, |
|
233 |
|
234 setURI: function HCR_setURI(aURI) { |
|
235 this._uri = aURI; |
|
236 }, |
|
237 |
|
238 // Creates an nsIChannel for the request and fills the body. |
|
239 openChannel: function HCR_openChannel() { |
|
240 let loadFlags = Ci.nsIChannel.INHIBIT_CACHING | |
|
241 Ci.nsIChannel.LOAD_BYPASS_CACHE; |
|
242 |
|
243 let channel = Services.io.newChannelFromURI(this._uri); |
|
244 channel.loadFlags = loadFlags; |
|
245 |
|
246 this._channel = channel; |
|
247 |
|
248 let body = this.buildRequest(); |
|
249 this.addRequestBody(body); |
|
250 |
|
251 channel.asyncOpen(this, null); |
|
252 }, |
|
253 |
|
254 // Returns a string for the request body based on the contents of |
|
255 // this._requests. |
|
256 buildRequest: function HCR_buildRequest() { |
|
257 // Sometimes duplicate entries are sent to HashCompleter but we do not need |
|
258 // to propagate these to the server. (bug 633644) |
|
259 let prefixes = []; |
|
260 |
|
261 for (let i = 0; i < this._requests.length; i++) { |
|
262 let request = this._requests[i]; |
|
263 if (prefixes.indexOf(request.partialHash) == -1) { |
|
264 prefixes.push(request.partialHash); |
|
265 } |
|
266 } |
|
267 |
|
268 // Randomize the order to obscure the original request from noise |
|
269 // unbiased Fisher-Yates shuffle |
|
270 let i = prefixes.length; |
|
271 while (i--) { |
|
272 let j = Math.floor(Math.random() * (i + 1)); |
|
273 let temp = prefixes[i]; |
|
274 prefixes[i] = prefixes[j]; |
|
275 prefixes[j] = temp; |
|
276 } |
|
277 |
|
278 let body; |
|
279 body = PARTIAL_LENGTH + ":" + (PARTIAL_LENGTH * prefixes.length) + |
|
280 "\n" + prefixes.join(""); |
|
281 |
|
282 return body; |
|
283 }, |
|
284 |
|
285 // Sets the request body of this._channel. |
|
286 addRequestBody: function HCR_addRequestBody(aBody) { |
|
287 let inputStream = Cc["@mozilla.org/io/string-input-stream;1"]. |
|
288 createInstance(Ci.nsIStringInputStream); |
|
289 |
|
290 inputStream.setData(aBody, aBody.length); |
|
291 |
|
292 let uploadChannel = this._channel.QueryInterface(Ci.nsIUploadChannel); |
|
293 uploadChannel.setUploadStream(inputStream, "text/plain", -1); |
|
294 |
|
295 let httpChannel = this._channel.QueryInterface(Ci.nsIHttpChannel); |
|
296 httpChannel.requestMethod = "POST"; |
|
297 }, |
|
298 |
|
299 // Parses the response body and eventually adds items to the |responses| array |
|
300 // for elements of |this._requests|. |
|
301 handleResponse: function HCR_handleResponse() { |
|
302 if (this._response == "") { |
|
303 return; |
|
304 } |
|
305 |
|
306 let start = 0; |
|
307 |
|
308 let length = this._response.length; |
|
309 while (start != length) |
|
310 start = this.handleTable(start); |
|
311 }, |
|
312 |
|
313 // This parses a table entry in the response body and calls |handleItem| |
|
314 // for complete hash in the table entry. |
|
315 handleTable: function HCR_handleTable(aStart) { |
|
316 let body = this._response.substring(aStart); |
|
317 |
|
318 // deal with new line indexes as there could be |
|
319 // new line characters in the data parts. |
|
320 let newlineIndex = body.indexOf("\n"); |
|
321 if (newlineIndex == -1) { |
|
322 throw errorWithStack(); |
|
323 } |
|
324 let header = body.substring(0, newlineIndex); |
|
325 let entries = header.split(":"); |
|
326 if (entries.length != 3) { |
|
327 throw errorWithStack(); |
|
328 } |
|
329 |
|
330 let list = entries[0]; |
|
331 let addChunk = parseInt(entries[1]); |
|
332 let dataLength = parseInt(entries[2]); |
|
333 |
|
334 if (dataLength % COMPLETE_LENGTH != 0 || |
|
335 dataLength == 0 || |
|
336 dataLength > body.length - (newlineIndex + 1)) { |
|
337 throw errorWithStack(); |
|
338 } |
|
339 |
|
340 let data = body.substr(newlineIndex + 1, dataLength); |
|
341 for (let i = 0; i < (dataLength / COMPLETE_LENGTH); i++) { |
|
342 this.handleItem(data.substr(i * COMPLETE_LENGTH, COMPLETE_LENGTH), list, |
|
343 addChunk); |
|
344 } |
|
345 |
|
346 return aStart + newlineIndex + 1 + dataLength; |
|
347 }, |
|
348 |
|
349 // This adds a complete hash to any entry in |this._requests| that matches |
|
350 // the hash. |
|
351 handleItem: function HCR_handleItem(aData, aTableName, aChunkId) { |
|
352 for (let i = 0; i < this._requests.length; i++) { |
|
353 let request = this._requests[i]; |
|
354 if (aData.substring(0,4) == request.partialHash) { |
|
355 request.responses.push({ |
|
356 completeHash: aData, |
|
357 tableName: aTableName, |
|
358 chunkId: aChunkId, |
|
359 }); |
|
360 } |
|
361 } |
|
362 }, |
|
363 |
|
364 // notifySuccess and notifyFailure are used to alert the callbacks with |
|
365 // results. notifySuccess makes |completion| and |completionFinished| calls |
|
366 // while notifyFailure only makes a |completionFinished| call with the error |
|
367 // code. |
|
368 notifySuccess: function HCR_notifySuccess() { |
|
369 for (let i = 0; i < this._requests.length; i++) { |
|
370 let request = this._requests[i]; |
|
371 for (let j = 0; j < request.responses.length; j++) { |
|
372 let response = request.responses[j]; |
|
373 request.callback.completion(response.completeHash, response.tableName, |
|
374 response.chunkId); |
|
375 } |
|
376 |
|
377 request.callback.completionFinished(Cr.NS_OK); |
|
378 } |
|
379 }, |
|
380 notifyFailure: function HCR_notifyFailure(aStatus) { |
|
381 for (let i = 0; i < this._requests; i++) { |
|
382 let request = this._requests[i]; |
|
383 request.callback.completionFinished(aStatus); |
|
384 } |
|
385 }, |
|
386 |
|
387 onDataAvailable: function HCR_onDataAvailable(aRequest, aContext, |
|
388 aInputStream, aOffset, aCount) { |
|
389 let sis = Cc["@mozilla.org/scriptableinputstream;1"]. |
|
390 createInstance(Ci.nsIScriptableInputStream); |
|
391 sis.init(aInputStream); |
|
392 this._response += sis.readBytes(aCount); |
|
393 }, |
|
394 |
|
395 onStartRequest: function HCR_onStartRequest(aRequest, aContext) { |
|
396 // At this point no data is available for us and we have no reason to |
|
397 // terminate the connection, so we do nothing until |onStopRequest|. |
|
398 }, |
|
399 |
|
400 onStopRequest: function HCR_onStopRequest(aRequest, aContext, aStatusCode) { |
|
401 Services.obs.removeObserver(this, "xpcom-shutdown"); |
|
402 |
|
403 if (this._shuttingDown) { |
|
404 throw Cr.NS_ERROR_ABORT; |
|
405 } |
|
406 |
|
407 if (Components.isSuccessCode(aStatusCode)) { |
|
408 let channel = aRequest.QueryInterface(Ci.nsIHttpChannel); |
|
409 let success = channel.requestSucceeded; |
|
410 if (!success) { |
|
411 aStatusCode = Cr.NS_ERROR_ABORT; |
|
412 } |
|
413 } |
|
414 |
|
415 let success = Components.isSuccessCode(aStatusCode); |
|
416 this._completer.noteServerResponse(success); |
|
417 |
|
418 if (success) { |
|
419 try { |
|
420 this.handleResponse(); |
|
421 } |
|
422 catch (err) { |
|
423 dump(err.stack); |
|
424 aStatusCode = err.value; |
|
425 success = false; |
|
426 } |
|
427 } |
|
428 |
|
429 if (success) { |
|
430 this.notifySuccess(); |
|
431 } else { |
|
432 this.notifyFailure(aStatusCode); |
|
433 } |
|
434 }, |
|
435 |
|
436 observe: function HCR_observe(aSubject, aTopic, aData) { |
|
437 if (aTopic != "xpcom-shutdown") { |
|
438 return; |
|
439 } |
|
440 |
|
441 this._shuttingDown = true; |
|
442 if (this._channel) { |
|
443 this._channel.cancel(Cr.NS_ERROR_ABORT); |
|
444 } |
|
445 }, |
|
446 }; |
|
447 |
|
448 // Converts a URL safe base64 string to a normal base64 string. Will not change |
|
449 // normal base64 strings. This is modelled after the same function in |
|
450 // nsUrlClassifierUtils.h. |
|
451 function unUrlsafeBase64(aStr) { |
|
452 return !aStr ? "" : aStr.replace(/-/g, "+") |
|
453 .replace(/_/g, "/"); |
|
454 } |
|
455 |
|
456 function errorWithStack() { |
|
457 let err = new Error(); |
|
458 err.value = Cr.NS_ERROR_FAILURE; |
|
459 return err; |
|
460 } |
|
461 |
|
462 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HashCompleter]); |