forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			404 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			404 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* Any copyright is dedicated to the Public Domain.
 | |
|    http://creativecommons.org/publicdomain/zero/1.0/ */
 | |
| 
 | |
| // This test ensures that the nsIUrlClassifierHashCompleter works as expected
 | |
| // and simulates an HTTP server to provide completions.
 | |
| //
 | |
| // In order to test completions, each group of completions sent as one request
 | |
| // to the HTTP server is called a completion set. There is currently not
 | |
| // support for multiple requests being sent to the server at once, in this test.
 | |
| // This tests makes a request for each element of |completionSets|, waits for
 | |
| // a response and then moves to the next element.
 | |
| // Each element of |completionSets| is an array of completions, and each
 | |
| // completion is an object with the properties:
 | |
| //   hash: complete hash for the completion. Automatically right-padded
 | |
| //         to be COMPLETE_LENGTH.
 | |
| //   expectCompletion: boolean indicating whether the server should respond
 | |
| //                     with a full hash.
 | |
| //   forceServerError: boolean indicating whether the server should respond
 | |
| //                     with a 503.
 | |
| //   table: name of the table that the hash corresponds to. Only needs to be set
 | |
| //          if a completion is expected.
 | |
| //   chunkId: positive integer corresponding to the chunk that the hash belongs
 | |
| //            to. Only needs to be set if a completion is expected.
 | |
| //   multipleCompletions: boolean indicating whether the server should respond
 | |
| //                        with more than one full hash. If this is set to true
 | |
| //                        then |expectCompletion| must also be set to true and
 | |
| //                        |hash| must have the same prefix as all |completions|.
 | |
| //   completions: an array of completions (objects with a hash, table and
 | |
| //                chunkId property as described above). This property is only
 | |
| //                used when |multipleCompletions| is set to true.
 | |
| 
 | |
| // Basic prefixes with 2/3 completions.
 | |
| var basicCompletionSet = [
 | |
|   {
 | |
|     hash: "abcdefgh",
 | |
|     expectCompletion: true,
 | |
|     table: "test",
 | |
|     chunkId: 1234,
 | |
|   },
 | |
|   {
 | |
|     hash: "1234",
 | |
|     expectCompletion: false,
 | |
|   },
 | |
|   {
 | |
|     hash: "\u0000\u0000\u000012312",
 | |
|     expectCompletion: true,
 | |
|     table: "test",
 | |
|     chunkId: 1234,
 | |
|   }
 | |
| ];
 | |
| 
 | |
| // 3 prefixes with 0 completions to test HashCompleter handling a 204 status.
 | |
| var falseCompletionSet = [
 | |
|   {
 | |
|     hash: "1234",
 | |
|     expectCompletion: false,
 | |
|   },
 | |
|   {
 | |
|     hash: "",
 | |
|     expectCompletion: false,
 | |
|   },
 | |
|   {
 | |
|     hash: "abc",
 | |
|     expectCompletion: false,
 | |
|   }
 | |
| ];
 | |
| 
 | |
| // The current implementation (as of Mar 2011) sometimes sends duplicate
 | |
| // entries to HashCompleter and even expects responses for duplicated entries.
 | |
| var dupedCompletionSet = [
 | |
|   {
 | |
|     hash: "1234",
 | |
|     expectCompletion: true,
 | |
|     table: "test",
 | |
|     chunkId: 1,
 | |
|   },
 | |
|   {
 | |
|     hash: "5678",
 | |
|     expectCompletion: false,
 | |
|     table: "test2",
 | |
|     chunkId: 2,
 | |
|   },
 | |
|   {
 | |
|     hash: "1234",
 | |
|     expectCompletion: true,
 | |
|     table: "test",
 | |
|     chunkId: 1,
 | |
|   },
 | |
|   {
 | |
|     hash: "5678",
 | |
|     expectCompletion: false,
 | |
|     table: "test2",
 | |
|     chunkId: 2
 | |
|   }
 | |
| ];
 | |
| 
 | |
| // It is possible for a hash completion request to return with multiple
 | |
| // completions, the HashCompleter should return all of these.
 | |
| var multipleResponsesCompletionSet = [
 | |
|   {
 | |
|     hash: "1234",
 | |
|     expectCompletion: true,
 | |
|     multipleCompletions: true,
 | |
|     completions: [
 | |
|       {
 | |
|         hash: "123456",
 | |
|         table: "test1",
 | |
|         chunkId: 3,
 | |
|       },
 | |
|       {
 | |
|         hash: "123478",
 | |
|         table: "test2",
 | |
|         chunkId: 4,
 | |
|       }
 | |
|     ],
 | |
|   }
 | |
| ];
 | |
| 
 | |
| function buildCompletionRequest(aCompletionSet) {
 | |
|   let prefixes = [];
 | |
|   let prefixSet = new Set();
 | |
|   aCompletionSet.forEach(s => {
 | |
|     let prefix = s.hash.substring(0, 4);
 | |
|     if (prefixSet.has(prefix)) {
 | |
|       return;
 | |
|     }
 | |
|     prefixSet.add(prefix);
 | |
|     prefixes.push(prefix);
 | |
|   });
 | |
|   return 4 + ":" + (4 * prefixes.length) + "\n" + prefixes.join("");
 | |
| }
 | |
| 
 | |
| function parseCompletionRequest(aRequest) {
 | |
|   // Format: [partial_length]:[num_of_prefix * partial_length]\n[prefixes_data]
 | |
| 
 | |
|   let tokens = /(\d):(\d+)/.exec(aRequest);
 | |
|   if (tokens.length < 3) {
 | |
|     dump("Request format error.");
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   let partialLength = parseInt(tokens[1]);
 | |
| 
 | |
|   let payloadStart = tokens[1].length + // partial length
 | |
|                      1 + // ':'
 | |
|                      tokens[2].length + // payload length
 | |
|                      1; // '\n'
 | |
| 
 | |
|   let prefixSet = [];
 | |
|   for (let i = payloadStart; i < aRequest.length; i += partialLength) {
 | |
|     let prefix = aRequest.substr(i, partialLength);
 | |
|     if (prefix.length !== partialLength) {
 | |
|       dump("Header info not correct: " + aRequest.substr(0, payloadStart));
 | |
|       return null;
 | |
|     }
 | |
|     prefixSet.push(prefix);
 | |
|   }
 | |
|   prefixSet.sort();
 | |
| 
 | |
|   return prefixSet;
 | |
| }
 | |
| 
 | |
| // Compare the requests in string format.
 | |
| function compareCompletionRequest(aRequest1, aRequest2) {
 | |
|   let prefixSet1 = parseCompletionRequest(aRequest1);
 | |
|   let prefixSet2 = parseCompletionRequest(aRequest2);
 | |
| 
 | |
|   return equal(JSON.stringify(prefixSet1), JSON.stringify(prefixSet2));
 | |
| }
 | |
| 
 | |
| // The fifth completion set is added at runtime by getRandomCompletionSet.
 | |
| // Each completion in the set only has one response and its purpose is to
 | |
| // provide an easy way to test the HashCompleter handling an arbitrarily large
 | |
| // completion set (determined by SIZE_OF_RANDOM_SET).
 | |
| const SIZE_OF_RANDOM_SET = 16;
 | |
| function getRandomCompletionSet(forceServerError) {
 | |
|   let completionSet = [];
 | |
|   let hashPrefixes = [];
 | |
| 
 | |
|   let seed = Math.floor(Math.random() * Math.pow(2, 32));
 | |
|   dump("Using seed of " + seed + " for random completion set.\n");
 | |
|   let rand = new LFSRgenerator(seed);
 | |
| 
 | |
|   for (let i = 0; i < SIZE_OF_RANDOM_SET; i++) {
 | |
|     let completion = { expectCompletion: false, forceServerError: false, _finished: false };
 | |
| 
 | |
|     // Generate a random 256 bit hash. First we get a random number and then
 | |
|     // convert it to a string.
 | |
|     let hash;
 | |
|     let prefix;
 | |
|     do {
 | |
|       hash = "";
 | |
|       let length = 1 + rand.nextNum(5);
 | |
|       for (let j = 0; j < length; j++)
 | |
|         hash += String.fromCharCode(rand.nextNum(8));
 | |
|       prefix = hash.substring(0, 4);
 | |
|     } while (hashPrefixes.includes(prefix));
 | |
| 
 | |
|     hashPrefixes.push(prefix);
 | |
|     completion.hash = hash;
 | |
| 
 | |
|     if (!forceServerError) {
 | |
|       completion.expectCompletion = rand.nextNum(1) == 1;
 | |
|     } else {
 | |
|       completion.forceServerError = true;
 | |
|     }
 | |
|     if (completion.expectCompletion) {
 | |
|       // Generate a random alpha-numeric string of length start with "test" for the
 | |
|       // table name.
 | |
|       completion.table = "test" + (rand.nextNum(31)).toString(36);
 | |
| 
 | |
|       completion.chunkId = rand.nextNum(16);
 | |
|     }
 | |
|     completionSet.push(completion);
 | |
|   }
 | |
| 
 | |
|   return completionSet;
 | |
| }
 | |
| 
 | |
| var completionSets = [basicCompletionSet, falseCompletionSet,
 | |
|                       dupedCompletionSet, multipleResponsesCompletionSet];
 | |
| var currentCompletionSet = -1;
 | |
| var finishedCompletions = 0;
 | |
| 
 | |
| const SERVER_PATH = "/hash-completer";
 | |
| var server;
 | |
| 
 | |
| // Completion hashes are automatically right-padded with null chars to have a
 | |
| // length of COMPLETE_LENGTH.
 | |
| // Taken from nsUrlClassifierDBService.h
 | |
| const COMPLETE_LENGTH = 32;
 | |
| 
 | |
| var completer = Cc["@mozilla.org/url-classifier/hashcompleter;1"].
 | |
|                   getService(Ci.nsIUrlClassifierHashCompleter);
 | |
| 
 | |
| var gethashUrl;
 | |
| 
 | |
| // Expected highest completion set for which the server sends a response.
 | |
| var expectedMaxServerCompletionSet = 0;
 | |
| var maxServerCompletionSet = 0;
 | |
| 
 | |
| function run_test() {
 | |
|   // This test case exercises the backoff functionality so we can't leave it disabled.
 | |
|   Services.prefs.setBoolPref("browser.safebrowsing.provider.test.disableBackoff", false);
 | |
|   // Generate a random completion set that return successful responses.
 | |
|   completionSets.push(getRandomCompletionSet(false));
 | |
|   // We backoff after receiving an error, so requests shouldn't reach the
 | |
|   // server after that.
 | |
|   expectedMaxServerCompletionSet = completionSets.length;
 | |
|   // Generate some completion sets that return 503s.
 | |
|   for (let j = 0; j < 10; ++j) {
 | |
|     completionSets.push(getRandomCompletionSet(true));
 | |
|   }
 | |
| 
 | |
|   // Fix up the completions before running the test.
 | |
|   for (let completionSet of completionSets) {
 | |
|     for (let completion of completionSet) {
 | |
|       // Pad the right of each |hash| so that the length is COMPLETE_LENGTH.
 | |
|       if (completion.multipleCompletions) {
 | |
|         for (let responseCompletion of completion.completions) {
 | |
|           let numChars = COMPLETE_LENGTH - responseCompletion.hash.length;
 | |
|           responseCompletion.hash += (new Array(numChars + 1)).join("\u0000");
 | |
|         }
 | |
|       } else {
 | |
|         let numChars = COMPLETE_LENGTH - completion.hash.length;
 | |
|         completion.hash += (new Array(numChars + 1)).join("\u0000");
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   do_test_pending();
 | |
| 
 | |
|   server = new HttpServer();
 | |
|   server.registerPathHandler(SERVER_PATH, hashCompleterServer);
 | |
| 
 | |
|   server.start(-1);
 | |
|   const SERVER_PORT = server.identity.primaryPort;
 | |
| 
 | |
|   gethashUrl = "http://localhost:" + SERVER_PORT + SERVER_PATH;
 | |
| 
 | |
|   runNextCompletion();
 | |
| }
 | |
| 
 | |
| function runNextCompletion() {
 | |
|   // The server relies on currentCompletionSet to send the correct response, so
 | |
|   // don't increment it until we start the new set of callbacks.
 | |
|   currentCompletionSet++;
 | |
|   if (currentCompletionSet >= completionSets.length) {
 | |
|     finish();
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   dump("Now on completion set index " + currentCompletionSet + ", length " +
 | |
|        completionSets[currentCompletionSet].length + "\n");
 | |
|   // Number of finished completions for this set.
 | |
|   finishedCompletions = 0;
 | |
|   for (let completion of completionSets[currentCompletionSet]) {
 | |
|     completer.complete(completion.hash.substring(0, 4), gethashUrl,
 | |
|                        "test-phish-shavar", // Could be arbitrary v2 table name.
 | |
|                        (new callback(completion)));
 | |
|   }
 | |
| }
 | |
| 
 | |
| function hashCompleterServer(aRequest, aResponse) {
 | |
|   let stream = aRequest.bodyInputStream;
 | |
|   let wrapperStream = Cc["@mozilla.org/binaryinputstream;1"].
 | |
|                         createInstance(Ci.nsIBinaryInputStream);
 | |
|   wrapperStream.setInputStream(stream);
 | |
| 
 | |
|   let len = stream.available();
 | |
|   let data = wrapperStream.readBytes(len);
 | |
| 
 | |
|   // Check if we got the expected completion request.
 | |
|   let expectedRequest = buildCompletionRequest(completionSets[currentCompletionSet]);
 | |
|   compareCompletionRequest(data, expectedRequest);
 | |
| 
 | |
|   // To avoid a response with duplicate hash completions, we keep track of all
 | |
|   // completed hash prefixes so far.
 | |
|   let completedHashes = [];
 | |
|   let responseText = "";
 | |
| 
 | |
|   function responseForCompletion(x) {
 | |
|     return x.table + ":" + x.chunkId + ":" + x.hash.length + "\n" + x.hash;
 | |
|   }
 | |
|   // As per the spec, a server should response with a 204 if there are no
 | |
|   // full-length hashes that match the prefixes.
 | |
|   let httpStatus = 204;
 | |
|   for (let completion of completionSets[currentCompletionSet]) {
 | |
|     if (completion.expectCompletion &&
 | |
|         (!completedHashes.includes(completion.hash))) {
 | |
|       completedHashes.push(completion.hash);
 | |
| 
 | |
|       if (completion.multipleCompletions)
 | |
|         responseText += completion.completions.map(responseForCompletion).join("");
 | |
|       else
 | |
|         responseText += responseForCompletion(completion);
 | |
|     }
 | |
|     if (completion.forceServerError) {
 | |
|       httpStatus = 503;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   dump("Server sending response for " + currentCompletionSet + "\n");
 | |
|   maxServerCompletionSet = currentCompletionSet;
 | |
|   if (responseText && httpStatus != 503) {
 | |
|     aResponse.write(responseText);
 | |
|   } else {
 | |
|     aResponse.setStatusLine(null, httpStatus, null);
 | |
|   }
 | |
| }
 | |
| 
 | |
| 
 | |
| function callback(completion) {
 | |
|   this._completion = completion;
 | |
| }
 | |
| 
 | |
| callback.prototype = {
 | |
|   completionV2: function completionV2(hash, table, chunkId, trusted) {
 | |
|     Assert.ok(this._completion.expectCompletion);
 | |
|     if (this._completion.multipleCompletions) {
 | |
|       for (let completion of this._completion.completions) {
 | |
|         if (completion.hash == hash) {
 | |
|           Assert.equal(JSON.stringify(hash), JSON.stringify(completion.hash));
 | |
|           Assert.equal(table, completion.table);
 | |
|           Assert.equal(chunkId, completion.chunkId);
 | |
| 
 | |
|           completion._completed = true;
 | |
| 
 | |
|           if (this._completion.completions.every(x => x._completed))
 | |
|             this._completed = true;
 | |
| 
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|     } else {
 | |
|       // Hashes are not actually strings and can contain arbitrary data.
 | |
|       Assert.equal(JSON.stringify(hash), JSON.stringify(this._completion.hash));
 | |
|       Assert.equal(table, this._completion.table);
 | |
|       Assert.equal(chunkId, this._completion.chunkId);
 | |
| 
 | |
|       this._completed = true;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   completionFinished: function completionFinished(status) {
 | |
|     finishedCompletions++;
 | |
|     Assert.equal(!!this._completion.expectCompletion, !!this._completed);
 | |
|     this._completion._finished = true;
 | |
| 
 | |
|     // currentCompletionSet can mutate before all of the callbacks are complete.
 | |
|     if (currentCompletionSet < completionSets.length &&
 | |
|         finishedCompletions == completionSets[currentCompletionSet].length) {
 | |
|       runNextCompletion();
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| function finish() {
 | |
|   Services.prefs.clearUserPref("browser.safebrowsing.provider.test.disableBackoff");
 | |
| 
 | |
|   Assert.equal(expectedMaxServerCompletionSet, maxServerCompletionSet);
 | |
|   server.stop(function() {
 | |
|     do_test_finished();
 | |
|   });
 | |
| }
 | 
