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// COMPLETE_LENGTH and PARTIAL_LENGTH copied from nsUrlClassifierDBService.h, 6// they correspond to the length, in bytes, of a hash prefix and the total 7// hash. 8const COMPLETE_LENGTH = 32; 9const PARTIAL_LENGTH = 4; 10 11// Upper limit on the server response minimumWaitDuration 12const MIN_WAIT_DURATION_MAX_VALUE = 24 * 60 * 60 * 1000; 13const PREF_DEBUG_ENABLED = "browser.safebrowsing.debug"; 14 15const { XPCOMUtils } = ChromeUtils.import( 16 "resource://gre/modules/XPCOMUtils.jsm" 17); 18const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 19const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); 20 21XPCOMUtils.defineLazyServiceGetter( 22 this, 23 "gDbService", 24 "@mozilla.org/url-classifier/dbservice;1", 25 "nsIUrlClassifierDBService" 26); 27 28XPCOMUtils.defineLazyServiceGetter( 29 this, 30 "gUrlUtil", 31 "@mozilla.org/url-classifier/utils;1", 32 "nsIUrlClassifierUtils" 33); 34 35let loggingEnabled = false; 36 37// Log only if browser.safebrowsing.debug is true 38function log(...stuff) { 39 if (!loggingEnabled) { 40 return; 41 } 42 43 var d = new Date(); 44 let msg = "hashcompleter: " + d.toTimeString() + ": " + stuff.join(" "); 45 dump(Services.urlFormatter.trimSensitiveURLs(msg) + "\n"); 46} 47 48// Map the HTTP response code to a Telemetry bucket 49// https://developers.google.com/safe-browsing/developers_guide_v2?hl=en 50// eslint-disable-next-line complexity 51function httpStatusToBucket(httpStatus) { 52 var statusBucket; 53 switch (httpStatus) { 54 case 100: 55 case 101: 56 // Unexpected 1xx return code 57 statusBucket = 0; 58 break; 59 case 200: 60 // OK - Data is available in the HTTP response body. 61 statusBucket = 1; 62 break; 63 case 201: 64 case 202: 65 case 203: 66 case 205: 67 case 206: 68 // Unexpected 2xx return code 69 statusBucket = 2; 70 break; 71 case 204: 72 // No Content - There are no full-length hashes with the requested prefix. 73 statusBucket = 3; 74 break; 75 case 300: 76 case 301: 77 case 302: 78 case 303: 79 case 304: 80 case 305: 81 case 307: 82 case 308: 83 // Unexpected 3xx return code 84 statusBucket = 4; 85 break; 86 case 400: 87 // Bad Request - The HTTP request was not correctly formed. 88 // The client did not provide all required CGI parameters. 89 statusBucket = 5; 90 break; 91 case 401: 92 case 402: 93 case 405: 94 case 406: 95 case 407: 96 case 409: 97 case 410: 98 case 411: 99 case 412: 100 case 414: 101 case 415: 102 case 416: 103 case 417: 104 case 421: 105 case 426: 106 case 428: 107 case 429: 108 case 431: 109 case 451: 110 // Unexpected 4xx return code 111 statusBucket = 6; 112 break; 113 case 403: 114 // Forbidden - The client id is invalid. 115 statusBucket = 7; 116 break; 117 case 404: 118 // Not Found 119 statusBucket = 8; 120 break; 121 case 408: 122 // Request Timeout 123 statusBucket = 9; 124 break; 125 case 413: 126 // Request Entity Too Large - Bug 1150334 127 statusBucket = 10; 128 break; 129 case 500: 130 case 501: 131 case 510: 132 // Unexpected 5xx return code 133 statusBucket = 11; 134 break; 135 case 502: 136 case 504: 137 case 511: 138 // Local network errors, we'll ignore these. 139 statusBucket = 12; 140 break; 141 case 503: 142 // Service Unavailable - The server cannot handle the request. 143 // Clients MUST follow the backoff behavior specified in the 144 // Request Frequency section. 145 statusBucket = 13; 146 break; 147 case 505: 148 // HTTP Version Not Supported - The server CANNOT handle the requested 149 // protocol major version. 150 statusBucket = 14; 151 break; 152 default: 153 statusBucket = 15; 154 } 155 return statusBucket; 156} 157 158function FullHashMatch(table, hash, duration) { 159 this.tableName = table; 160 this.fullHash = hash; 161 this.cacheDuration = duration; 162} 163 164FullHashMatch.prototype = { 165 QueryInterface: ChromeUtils.generateQI([Ci.nsIFullHashMatch]), 166 167 tableName: null, 168 fullHash: null, 169 cacheDuration: null, 170}; 171 172function HashCompleter() { 173 // The current HashCompleterRequest in flight. Once it is started, it is set 174 // to null. It may be used by multiple calls to |complete| in succession to 175 // avoid creating multiple requests to the same gethash URL. 176 this._currentRequest = null; 177 // An Array of ongoing gethash requests which is used to find requests for 178 // the same hash prefix. 179 this._ongoingRequests = []; 180 // A map of gethashUrls to HashCompleterRequests that haven't yet begun. 181 this._pendingRequests = {}; 182 183 // A map of gethash URLs to RequestBackoff objects. 184 this._backoffs = {}; 185 186 // Whether we have been informed of a shutdown by the shutdown event. 187 this._shuttingDown = false; 188 189 // A map of gethash URLs to next gethash time in miliseconds 190 this._nextGethashTimeMs = {}; 191 192 Services.obs.addObserver(this, "quit-application"); 193 Services.prefs.addObserver(PREF_DEBUG_ENABLED, this); 194 195 loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED); 196} 197 198HashCompleter.prototype = { 199 classID: Components.ID("{9111de73-9322-4bfc-8b65-2b727f3e6ec8}"), 200 QueryInterface: ChromeUtils.generateQI([ 201 Ci.nsIUrlClassifierHashCompleter, 202 Ci.nsIRunnable, 203 Ci.nsIObserver, 204 Ci.nsISupportsWeakReference, 205 Ci.nsITimerCallback, 206 ]), 207 208 // This is mainly how the HashCompleter interacts with other components. 209 // Even though it only takes one partial hash and callback, subsequent 210 // calls are made into the same HTTP request by using a thread dispatch. 211 complete: function HC_complete( 212 aPartialHash, 213 aGethashUrl, 214 aTableName, 215 aCallback 216 ) { 217 if (!aGethashUrl) { 218 throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED); 219 } 220 221 // Check ongoing requests before creating a new HashCompleteRequest 222 for (let r of this._ongoingRequests) { 223 if (r.find(aPartialHash, aGethashUrl, aTableName)) { 224 log( 225 "Merge gethash request in " + 226 aTableName + 227 " for prefix : " + 228 btoa(aPartialHash) 229 ); 230 r.add(aPartialHash, aCallback, aTableName); 231 return; 232 } 233 } 234 235 if (!this._currentRequest) { 236 this._currentRequest = new HashCompleterRequest(this, aGethashUrl); 237 } 238 if (this._currentRequest.gethashUrl == aGethashUrl) { 239 this._currentRequest.add(aPartialHash, aCallback, aTableName); 240 } else { 241 if (!this._pendingRequests[aGethashUrl]) { 242 this._pendingRequests[aGethashUrl] = new HashCompleterRequest( 243 this, 244 aGethashUrl 245 ); 246 } 247 this._pendingRequests[aGethashUrl].add( 248 aPartialHash, 249 aCallback, 250 aTableName 251 ); 252 } 253 254 if (!this._backoffs[aGethashUrl]) { 255 // Initialize request backoffs separately, since requests are deleted 256 // after they are dispatched. 257 var jslib = Cc["@mozilla.org/url-classifier/jslib;1"].getService() 258 .wrappedJSObject; 259 260 // Using the V4 backoff algorithm for both V2 and V4. See bug 1273398. 261 this._backoffs[aGethashUrl] = new jslib.RequestBackoffV4( 262 10 /* keep track of max requests */, 263 0 /* don't throttle on successful requests per time period */, 264 gUrlUtil.getProvider(aTableName) /* used by testcase */ 265 ); 266 } 267 268 if (!this._nextGethashTimeMs[aGethashUrl]) { 269 this._nextGethashTimeMs[aGethashUrl] = 0; 270 } 271 272 // Start off this request. Without dispatching to a thread, every call to 273 // complete makes an individual HTTP request. 274 Services.tm.dispatchToMainThread(this); 275 }, 276 277 // This is called after several calls to |complete|, or after the 278 // currentRequest has finished. It starts off the HTTP request by making a 279 // |begin| call to the HashCompleterRequest. 280 run() { 281 // Clear everything on shutdown 282 if (this._shuttingDown) { 283 this._currentRequest = null; 284 this._pendingRequests = null; 285 this._nextGethashTimeMs = null; 286 287 for (var url in this._backoffs) { 288 this._backoffs[url] = null; 289 } 290 throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED); 291 } 292 293 // If we don't have an in-flight request, make one 294 let pendingUrls = Object.keys(this._pendingRequests); 295 if (!this._currentRequest && pendingUrls.length) { 296 let nextUrl = pendingUrls[0]; 297 this._currentRequest = this._pendingRequests[nextUrl]; 298 delete this._pendingRequests[nextUrl]; 299 } 300 301 if (this._currentRequest) { 302 try { 303 if (this._currentRequest.begin()) { 304 this._ongoingRequests.push(this._currentRequest); 305 } 306 } finally { 307 // If |begin| fails, we should get rid of our request. 308 this._currentRequest = null; 309 } 310 } 311 }, 312 313 // Pass the server response status to the RequestBackoff for the given 314 // gethashUrl and fetch the next pending request, if there is one. 315 finishRequest(aRequest, aStatus) { 316 this._ongoingRequests = this._ongoingRequests.filter(v => v != aRequest); 317 318 this._backoffs[aRequest.gethashUrl].noteServerResponse(aStatus); 319 Services.tm.dispatchToMainThread(this); 320 }, 321 322 // Returns true if we can make a request from the given url, false otherwise. 323 canMakeRequest(aGethashUrl) { 324 return ( 325 this._backoffs[aGethashUrl].canMakeRequest() && 326 Date.now() >= this._nextGethashTimeMs[aGethashUrl] 327 ); 328 }, 329 330 // Notifies the RequestBackoff of a new request so we can throttle based on 331 // max requests/time period. This must be called before a channel is opened, 332 // and finishRequest must be called once the response is received. 333 noteRequest(aGethashUrl) { 334 return this._backoffs[aGethashUrl].noteRequest(); 335 }, 336 337 observe: function HC_observe(aSubject, aTopic, aData) { 338 switch (aTopic) { 339 case "quit-application": 340 this._shuttingDown = true; 341 Services.obs.removeObserver(this, "quit-application"); 342 break; 343 case "nsPref:changed": 344 if (aData == PREF_DEBUG_ENABLED) { 345 loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED); 346 } 347 break; 348 } 349 }, 350}; 351 352function HashCompleterRequest(aCompleter, aGethashUrl) { 353 // HashCompleter object that created this HashCompleterRequest. 354 this._completer = aCompleter; 355 // The internal set of hashes and callbacks that this request corresponds to. 356 this._requests = []; 357 // nsIChannel that the hash completion query is transmitted over. 358 this._channel = null; 359 // Response body of hash completion. Created in onDataAvailable. 360 this._response = ""; 361 // Whether we have been informed of a shutdown by the quit-application event. 362 this._shuttingDown = false; 363 this.gethashUrl = aGethashUrl; 364 365 this.provider = ""; 366 // Multiple partial hashes can be associated with the same tables 367 // so we use a map here. 368 this.tableNames = new Map(); 369 370 this.telemetryProvider = ""; 371 this.telemetryClockStart = 0; 372} 373HashCompleterRequest.prototype = { 374 QueryInterface: ChromeUtils.generateQI([ 375 Ci.nsIRequestObserver, 376 Ci.nsIStreamListener, 377 Ci.nsIObserver, 378 ]), 379 380 // This is called by the HashCompleter to add a hash and callback to the 381 // HashCompleterRequest. It must be called before calling |begin|. 382 add: function HCR_add(aPartialHash, aCallback, aTableName) { 383 this._requests.push({ 384 partialHash: aPartialHash, 385 callback: aCallback, 386 tableName: aTableName, 387 response: { matches: [] }, 388 }); 389 390 if (aTableName) { 391 let isTableNameV4 = aTableName.endsWith("-proto"); 392 if (0 === this.tableNames.size) { 393 // Decide if this request is v4 by the first added partial hash. 394 this.isV4 = isTableNameV4; 395 } else if (this.isV4 !== isTableNameV4) { 396 log( 397 'ERROR: Cannot mix "proto" tables with other types within ' + 398 "the same gethash URL." 399 ); 400 } 401 if (!this.tableNames.has(aTableName)) { 402 this.tableNames.set(aTableName); 403 } 404 405 // Assuming all tables with the same gethash URL have the same provider 406 if (this.provider == "") { 407 this.provider = gUrlUtil.getProvider(aTableName); 408 } 409 410 if (this.telemetryProvider == "") { 411 this.telemetryProvider = gUrlUtil.getTelemetryProvider(aTableName); 412 } 413 } 414 }, 415 416 find: function HCR_find(aPartialHash, aGetHashUrl, aTableName) { 417 if (this.gethashUrl != aGetHashUrl || !this.tableNames.has(aTableName)) { 418 return false; 419 } 420 421 return this._requests.find(function(r) { 422 return r.partialHash === aPartialHash; 423 }); 424 }, 425 426 fillTableStatesBase64: function HCR_fillTableStatesBase64(aCallback) { 427 gDbService.getTables(aTableData => { 428 aTableData.split("\n").forEach(line => { 429 let p = line.indexOf(";"); 430 if (-1 === p) { 431 return; 432 } 433 // [tableName];[stateBase64]:[checksumBase64] 434 let tableName = line.substring(0, p); 435 if (this.tableNames.has(tableName)) { 436 let metadata = line.substring(p + 1).split(":"); 437 let stateBase64 = metadata[0]; 438 this.tableNames.set(tableName, stateBase64); 439 } 440 }); 441 442 aCallback(); 443 }); 444 }, 445 446 // This initiates the HTTP request. It can fail due to backoff timings and 447 // will notify all callbacks as necessary. We notify the backoff object on 448 // begin. 449 begin: function HCR_begin() { 450 if (!this._completer.canMakeRequest(this.gethashUrl)) { 451 log("Can't make request to " + this.gethashUrl + "\n"); 452 this.notifyFailure(Cr.NS_ERROR_ABORT); 453 return false; 454 } 455 456 Services.obs.addObserver(this, "quit-application"); 457 458 // V4 requires table states to build the request so we need 459 // a async call to retrieve the table states from disk. 460 // Note that |HCR_begin| is fine to be sync because 461 // it doesn't appear in a sync call chain. 462 this.fillTableStatesBase64(() => { 463 try { 464 this.openChannel(); 465 // Notify the RequestBackoff if opening the channel succeeded. At this 466 // point, finishRequest must be called. 467 this._completer.noteRequest(this.gethashUrl); 468 } catch (err) { 469 this._completer._ongoingRequests = this._completer._ongoingRequests.filter( 470 v => v != this 471 ); 472 this.notifyFailure(err); 473 throw err; 474 } 475 }); 476 477 return true; 478 }, 479 480 notify: function HCR_notify() { 481 // If we haven't gotten onStopRequest, just cancel. This will call us 482 // with onStopRequest since we implement nsIStreamListener on the 483 // channel. 484 if (this._channel && this._channel.isPending()) { 485 log("cancelling request to " + this.gethashUrl + " (timeout)\n"); 486 Services.telemetry 487 .getKeyedHistogramById("URLCLASSIFIER_COMPLETE_TIMEOUT2") 488 .add(this.telemetryProvider, 1); 489 this._channel.cancel(Cr.NS_BINDING_ABORTED); 490 } 491 }, 492 493 // Creates an nsIChannel for the request and fills the body. 494 // Enforce bypassing URL Classifier check because if the request is 495 // blocked, it means SafeBrowsing is malfunction. 496 openChannel: function HCR_openChannel() { 497 let loadFlags = 498 Ci.nsIChannel.INHIBIT_CACHING | 499 Ci.nsIChannel.LOAD_BYPASS_CACHE | 500 Ci.nsIChannel.LOAD_BYPASS_URL_CLASSIFIER; 501 502 this.request = { 503 url: this.gethashUrl, 504 body: "", 505 }; 506 507 if (this.isV4) { 508 // As per spec, we add the request payload to the gethash url. 509 this.request.url += "&$req=" + this.buildRequestV4(); 510 } 511 512 log("actualGethashUrl: " + this.request.url); 513 514 let channel = NetUtil.newChannel({ 515 uri: this.request.url, 516 loadUsingSystemPrincipal: true, 517 }); 518 channel.loadFlags = loadFlags; 519 channel.loadInfo.originAttributes = { 520 // The firstPartyDomain value should sync with NECKO_SAFEBROWSING_FIRST_PARTY_DOMAIN 521 // defined in nsNetUtil.h. 522 firstPartyDomain: 523 "safebrowsing.86868755-6b82-4842-b301-72671a0db32e.mozilla", 524 }; 525 526 // Disable keepalive. 527 let httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); 528 httpChannel.setRequestHeader("Connection", "close", false); 529 530 this._channel = channel; 531 532 if (this.isV4) { 533 httpChannel.setRequestHeader("X-HTTP-Method-Override", "POST", false); 534 } else { 535 let body = this.buildRequest(); 536 this.addRequestBody(body); 537 } 538 539 // Set a timer that cancels the channel after timeout_ms in case we 540 // don't get a gethash response. 541 this.timer_ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 542 // Ask the timer to use nsITimerCallback (.notify()) when ready 543 let timeout = Services.prefs.getIntPref("urlclassifier.gethash.timeout_ms"); 544 this.timer_.initWithCallback(this, timeout, this.timer_.TYPE_ONE_SHOT); 545 channel.asyncOpen(this); 546 this.telemetryClockStart = Date.now(); 547 }, 548 549 buildRequestV4: function HCR_buildRequestV4() { 550 // Convert the "name to state" mapping to two equal-length arrays. 551 let tableNameArray = []; 552 let stateArray = []; 553 this.tableNames.forEach((state, name) => { 554 // We skip the table which is not associated with a state. 555 if (state) { 556 tableNameArray.push(name); 557 stateArray.push(state); 558 } 559 }); 560 561 // Build the "distinct" prefix array. 562 // The array is sorted to make sure the entries are arbitrary mixed in a 563 // deterministic way 564 let prefixSet = new Set(); 565 this._requests.forEach(r => prefixSet.add(btoa(r.partialHash))); 566 let prefixArray = Array.from(prefixSet).sort(); 567 568 log( 569 "Build v4 gethash request with " + 570 JSON.stringify(tableNameArray) + 571 ", " + 572 JSON.stringify(stateArray) + 573 ", " + 574 JSON.stringify(prefixArray) 575 ); 576 577 return gUrlUtil.makeFindFullHashRequestV4( 578 tableNameArray, 579 stateArray, 580 prefixArray 581 ); 582 }, 583 584 // Returns a string for the request body based on the contents of 585 // this._requests. 586 buildRequest: function HCR_buildRequest() { 587 // Sometimes duplicate entries are sent to HashCompleter but we do not need 588 // to propagate these to the server. (bug 633644) 589 let prefixes = []; 590 591 for (let i = 0; i < this._requests.length; i++) { 592 let request = this._requests[i]; 593 if (!prefixes.includes(request.partialHash)) { 594 prefixes.push(request.partialHash); 595 } 596 } 597 598 // Sort to make sure the entries are arbitrary mixed in a deterministic way 599 prefixes.sort(); 600 601 let body; 602 body = 603 PARTIAL_LENGTH + 604 ":" + 605 PARTIAL_LENGTH * prefixes.length + 606 "\n" + 607 prefixes.join(""); 608 609 log( 610 "Requesting completions for " + 611 prefixes.length + 612 " " + 613 PARTIAL_LENGTH + 614 "-byte prefixes: " + 615 body 616 ); 617 return body; 618 }, 619 620 // Sets the request body of this._channel. 621 addRequestBody: function HCR_addRequestBody(aBody) { 622 let inputStream = Cc[ 623 "@mozilla.org/io/string-input-stream;1" 624 ].createInstance(Ci.nsIStringInputStream); 625 626 inputStream.setData(aBody, aBody.length); 627 628 let uploadChannel = this._channel.QueryInterface(Ci.nsIUploadChannel); 629 uploadChannel.setUploadStream(inputStream, "text/plain", -1); 630 631 let httpChannel = this._channel.QueryInterface(Ci.nsIHttpChannel); 632 httpChannel.requestMethod = "POST"; 633 }, 634 635 // Parses the response body and eventually adds items to the |response.matches| array 636 // for elements of |this._requests|. 637 handleResponse: function HCR_handleResponse() { 638 if (this._response == "") { 639 return; 640 } 641 642 if (this.isV4) { 643 this.handleResponseV4(); 644 return; 645 } 646 647 let start = 0; 648 649 let length = this._response.length; 650 while (start != length) { 651 start = this.handleTable(start); 652 } 653 }, 654 655 handleResponseV4: function HCR_handleResponseV4() { 656 let callback = { 657 // onCompleteHashFound will be called for each fullhash found in 658 // FullHashResponse. 659 onCompleteHashFound: ( 660 aCompleteHash, 661 aTableNames, 662 aPerHashCacheDuration 663 ) => { 664 log( 665 "V4 fullhash response complete hash found callback: " + 666 aTableNames + 667 ", CacheDuration(" + 668 aPerHashCacheDuration + 669 ")" 670 ); 671 672 // Filter table names which we didn't requested. 673 let filteredTables = aTableNames.split(",").filter(name => { 674 return this.tableNames.get(name); 675 }); 676 if (0 === filteredTables.length) { 677 log("ERROR: Got complete hash which is from unknown table."); 678 return; 679 } 680 if (filteredTables.length > 1) { 681 log("WARNING: Got complete hash which has ambigious threat type."); 682 } 683 684 this.handleItem({ 685 completeHash: aCompleteHash, 686 tableName: filteredTables[0], 687 cacheDuration: aPerHashCacheDuration, 688 }); 689 }, 690 691 // onResponseParsed will be called no matter if there is match in 692 // FullHashResponse, the callback is mainly used to pass negative cache 693 // duration and minimum wait duration. 694 onResponseParsed: (aMinWaitDuration, aNegCacheDuration) => { 695 log( 696 "V4 fullhash response parsed callback: " + 697 "MinWaitDuration(" + 698 aMinWaitDuration + 699 "), " + 700 "NegativeCacheDuration(" + 701 aNegCacheDuration + 702 ")" 703 ); 704 705 let minWaitDuration = aMinWaitDuration; 706 707 if (aMinWaitDuration > MIN_WAIT_DURATION_MAX_VALUE) { 708 log( 709 "WARNING: Minimum wait duration too large, clamping it down " + 710 "to a reasonable value." 711 ); 712 minWaitDuration = MIN_WAIT_DURATION_MAX_VALUE; 713 } else if (aMinWaitDuration < 0) { 714 log("WARNING: Minimum wait duration is negative, reset it to 0"); 715 minWaitDuration = 0; 716 } 717 718 this._completer._nextGethashTimeMs[this.gethashUrl] = 719 Date.now() + minWaitDuration; 720 721 // A fullhash request may contain more than one prefix, so the negative 722 // cache duration should be set for all the prefixes in the request. 723 this._requests.forEach(request => { 724 request.response.negCacheDuration = aNegCacheDuration; 725 }); 726 }, 727 }; 728 729 gUrlUtil.parseFindFullHashResponseV4(this._response, callback); 730 }, 731 732 // This parses a table entry in the response body and calls |handleItem| 733 // for complete hash in the table entry. 734 handleTable: function HCR_handleTable(aStart) { 735 let body = this._response.substring(aStart); 736 737 // deal with new line indexes as there could be 738 // new line characters in the data parts. 739 let newlineIndex = body.indexOf("\n"); 740 if (newlineIndex == -1) { 741 throw errorWithStack(); 742 } 743 let header = body.substring(0, newlineIndex); 744 let entries = header.split(":"); 745 if (entries.length != 3) { 746 throw errorWithStack(); 747 } 748 749 let list = entries[0]; 750 let addChunk = parseInt(entries[1]); 751 let dataLength = parseInt(entries[2]); 752 753 log("Response includes add chunks for " + list + ": " + addChunk); 754 if ( 755 dataLength % COMPLETE_LENGTH != 0 || 756 dataLength == 0 || 757 dataLength > body.length - (newlineIndex + 1) 758 ) { 759 throw errorWithStack(); 760 } 761 762 let data = body.substr(newlineIndex + 1, dataLength); 763 for (let i = 0; i < dataLength / COMPLETE_LENGTH; i++) { 764 this.handleItem({ 765 completeHash: data.substr(i * COMPLETE_LENGTH, COMPLETE_LENGTH), 766 tableName: list, 767 chunkId: addChunk, 768 }); 769 } 770 771 return aStart + newlineIndex + 1 + dataLength; 772 }, 773 774 // This adds a complete hash to any entry in |this._requests| that matches 775 // the hash. 776 handleItem: function HCR_handleItem(aData) { 777 let provider = gUrlUtil.getProvider(aData.tableName); 778 if (provider != this.provider) { 779 log( 780 "Ignoring table " + 781 aData.tableName + 782 " since it belongs to " + 783 provider + 784 " while the response came from " + 785 this.provider + 786 "." 787 ); 788 return; 789 } 790 791 for (let i = 0; i < this._requests.length; i++) { 792 let request = this._requests[i]; 793 if (aData.completeHash.startsWith(request.partialHash)) { 794 request.response.matches.push(aData); 795 } 796 } 797 }, 798 799 // notifySuccess and notifyFailure are used to alert the callbacks with 800 // results. notifySuccess makes |completion| and |completionFinished| calls 801 // while notifyFailure only makes a |completionFinished| call with the error 802 // code. 803 notifySuccess: function HCR_notifySuccess() { 804 // V2 completion handler 805 let completionV2 = req => { 806 req.response.matches.forEach(m => { 807 req.callback.completionV2(m.completeHash, m.tableName, m.chunkId); 808 }); 809 810 req.callback.completionFinished(Cr.NS_OK); 811 }; 812 813 // V4 completion handler 814 let completionV4 = req => { 815 let matches = Cc["@mozilla.org/array;1"].createInstance( 816 Ci.nsIMutableArray 817 ); 818 819 req.response.matches.forEach(m => { 820 matches.appendElement( 821 new FullHashMatch(m.tableName, m.completeHash, m.cacheDuration) 822 ); 823 }); 824 825 req.callback.completionV4( 826 req.partialHash, 827 req.tableName, 828 req.response.negCacheDuration, 829 matches 830 ); 831 832 req.callback.completionFinished(Cr.NS_OK); 833 }; 834 835 let completion = this.isV4 ? completionV4 : completionV2; 836 this._requests.forEach(req => { 837 completion(req); 838 }); 839 }, 840 841 notifyFailure: function HCR_notifyFailure(aStatus) { 842 log("notifying failure\n"); 843 for (let i = 0; i < this._requests.length; i++) { 844 let request = this._requests[i]; 845 request.callback.completionFinished(aStatus); 846 } 847 }, 848 849 onDataAvailable: function HCR_onDataAvailable( 850 aRequest, 851 aInputStream, 852 aOffset, 853 aCount 854 ) { 855 let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( 856 Ci.nsIScriptableInputStream 857 ); 858 sis.init(aInputStream); 859 this._response += sis.readBytes(aCount); 860 }, 861 862 onStartRequest: function HCR_onStartRequest(aRequest) { 863 // At this point no data is available for us and we have no reason to 864 // terminate the connection, so we do nothing until |onStopRequest|. 865 this._completer._nextGethashTimeMs[this.gethashUrl] = 0; 866 867 if (this.telemetryClockStart > 0) { 868 let msecs = Date.now() - this.telemetryClockStart; 869 Services.telemetry 870 .getKeyedHistogramById("URLCLASSIFIER_COMPLETE_SERVER_RESPONSE_TIME") 871 .add(this.telemetryProvider, msecs); 872 } 873 }, 874 875 onStopRequest: function HCR_onStopRequest(aRequest, aStatusCode) { 876 Services.obs.removeObserver(this, "quit-application"); 877 878 if (this.timer_) { 879 this.timer_.cancel(); 880 this.timer_ = null; 881 } 882 883 this.telemetryClockStart = 0; 884 885 if (this._shuttingDown) { 886 throw Components.Exception("", Cr.NS_ERROR_ABORT); 887 } 888 889 // Default HTTP status to service unavailable, in case we can't retrieve 890 // the true status from the channel. 891 let httpStatus = 503; 892 if (Components.isSuccessCode(aStatusCode)) { 893 let channel = aRequest.QueryInterface(Ci.nsIHttpChannel); 894 let success = channel.requestSucceeded; 895 httpStatus = channel.responseStatus; 896 if (!success) { 897 aStatusCode = Cr.NS_ERROR_ABORT; 898 } 899 } 900 let success = Components.isSuccessCode(aStatusCode); 901 log( 902 "Received a " + 903 httpStatus + 904 " status code from the " + 905 this.provider + 906 " gethash server (success=" + 907 success + 908 "): " + 909 btoa(this._response) 910 ); 911 912 Services.telemetry 913 .getKeyedHistogramById("URLCLASSIFIER_COMPLETE_REMOTE_STATUS2") 914 .add(this.telemetryProvider, httpStatusToBucket(httpStatus)); 915 if (httpStatus == 400) { 916 dump( 917 "Safe Browsing server returned a 400 during completion: request= " + 918 this.request.url + 919 ",payload= " + 920 this.request.body + 921 "\n" 922 ); 923 } 924 925 Services.telemetry 926 .getKeyedHistogramById("URLCLASSIFIER_COMPLETE_TIMEOUT2") 927 .add(this.telemetryProvider, 0); 928 929 // Notify the RequestBackoff once a response is received. 930 this._completer.finishRequest(this, httpStatus); 931 932 if (success) { 933 try { 934 this.handleResponse(); 935 } catch (err) { 936 log(err.stack); 937 aStatusCode = err.value; 938 success = false; 939 } 940 } 941 942 if (success) { 943 this.notifySuccess(); 944 } else { 945 this.notifyFailure(aStatusCode); 946 } 947 }, 948 949 observe: function HCR_observe(aSubject, aTopic, aData) { 950 if (aTopic == "quit-application") { 951 this._shuttingDown = true; 952 if (this._channel) { 953 this._channel.cancel(Cr.NS_ERROR_ABORT); 954 this.telemetryClockStart = 0; 955 } 956 957 Services.obs.removeObserver(this, "quit-application"); 958 } 959 }, 960}; 961 962function errorWithStack() { 963 let err = new Error(); 964 err.value = Cr.NS_ERROR_FAILURE; 965 return err; 966} 967 968var EXPORTED_SYMBOLS = ["HashCompleter"]; 969