Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
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/. */
5 const Cc = Components.classes;
6 const Ci = Components.interfaces;
7 const Cr = Components.results;
8 const Cu = Components.utils;
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;
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.
26 const BACKOFF_ERRORS = 2;
27 const BACKOFF_INTERVAL = 30 * 60;
28 const BACKOFF_MAX = 8 * 60 * 60;
29 const BACKOFF_TIME = 5 * 60;
31 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
32 Cu.import("resource://gre/modules/Services.jsm");
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;
40 // Whether we have been informed of a shutdown by the xpcom-shutdown event.
41 this._shuttingDown = false;
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 = [];
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]),
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);
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 }
83 this._currentRequest.add(aPartialHash, aCallback);
84 },
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 }
95 if (!this._currentRequest) {
96 return;
97 }
99 if (!this._getHashUrl) {
100 throw Cr.NS_ERROR_NOT_INITIALIZED;
101 }
103 let url = this._getHashUrl;
105 let uri = Services.io.newURI(url, null, null);
106 this._currentRequest.setURI(uri);
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 },
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;
125 if (this._currentRequest) {
126 Services.tm.currentThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL);
127 }
128 },
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 }
148 let now = Date.now();
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 }
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 }
165 if (this._backoff) {
166 this._backoffTime = Math.min(this._backoffTime, BACKOFF_MAX);
167 this._nextRequestTime = now + (this._backoffTime * 1000);
168 }
169 },
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 },
177 observe: function HC_observe(aSubject, aTopic, aData) {
178 if (aTopic == "xpcom-shutdown") {
179 this._shuttingDown = true;
180 }
181 },
182 };
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]),
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 },
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 }
223 Services.obs.addObserver(this, "xpcom-shutdown", false);
225 try {
226 this.openChannel();
227 }
228 catch (err) {
229 this.notifyFailure(err);
230 throw err;
231 }
232 },
234 setURI: function HCR_setURI(aURI) {
235 this._uri = aURI;
236 },
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;
243 let channel = Services.io.newChannelFromURI(this._uri);
244 channel.loadFlags = loadFlags;
246 this._channel = channel;
248 let body = this.buildRequest();
249 this.addRequestBody(body);
251 channel.asyncOpen(this, null);
252 },
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 = [];
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 }
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 }
278 let body;
279 body = PARTIAL_LENGTH + ":" + (PARTIAL_LENGTH * prefixes.length) +
280 "\n" + prefixes.join("");
282 return body;
283 },
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);
290 inputStream.setData(aBody, aBody.length);
292 let uploadChannel = this._channel.QueryInterface(Ci.nsIUploadChannel);
293 uploadChannel.setUploadStream(inputStream, "text/plain", -1);
295 let httpChannel = this._channel.QueryInterface(Ci.nsIHttpChannel);
296 httpChannel.requestMethod = "POST";
297 },
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 }
306 let start = 0;
308 let length = this._response.length;
309 while (start != length)
310 start = this.handleTable(start);
311 },
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);
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 }
330 let list = entries[0];
331 let addChunk = parseInt(entries[1]);
332 let dataLength = parseInt(entries[2]);
334 if (dataLength % COMPLETE_LENGTH != 0 ||
335 dataLength == 0 ||
336 dataLength > body.length - (newlineIndex + 1)) {
337 throw errorWithStack();
338 }
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 }
346 return aStart + newlineIndex + 1 + dataLength;
347 },
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 },
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 }
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 },
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 },
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 },
400 onStopRequest: function HCR_onStopRequest(aRequest, aContext, aStatusCode) {
401 Services.obs.removeObserver(this, "xpcom-shutdown");
403 if (this._shuttingDown) {
404 throw Cr.NS_ERROR_ABORT;
405 }
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 }
415 let success = Components.isSuccessCode(aStatusCode);
416 this._completer.noteServerResponse(success);
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 }
429 if (success) {
430 this.notifySuccess();
431 } else {
432 this.notifyFailure(aStatusCode);
433 }
434 },
436 observe: function HCR_observe(aSubject, aTopic, aData) {
437 if (aTopic != "xpcom-shutdown") {
438 return;
439 }
441 this._shuttingDown = true;
442 if (this._channel) {
443 this._channel.cancel(Cr.NS_ERROR_ABORT);
444 }
445 },
446 };
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 }
456 function errorWithStack() {
457 let err = new Error();
458 err.value = Cr.NS_ERROR_FAILURE;
459 return err;
460 }
462 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HashCompleter]);