//* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- * function dumpn(s) { dump(s + "\n"); } const NS_APP_USER_PROFILE_50_DIR = "ProfD"; const NS_APP_USER_PROFILE_LOCAL_50_DIR = "ProfLD"; var { HTTP_400, HTTP_401, HTTP_402, HTTP_403, HTTP_404, HTTP_405, HTTP_406, HTTP_407, HTTP_408, HTTP_409, HTTP_410, HTTP_411, HTTP_412, HTTP_413, HTTP_414, HTTP_415, HTTP_417, HTTP_500, HTTP_501, HTTP_502, HTTP_503, HTTP_504, HTTP_505, HttpError, HttpServer, } = ChromeUtils.importESModule("resource://testing-common/httpd.sys.mjs"); do_get_profile(); // Ensure PSM is initialized before the test Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); // Disable hashcompleter noise for tests Services.prefs.setIntPref("urlclassifier.gethashnoise", 0); // Enable malware/phishing checking for tests Services.prefs.setBoolPref("browser.safebrowsing.malware.enabled", true); Services.prefs.setBoolPref("browser.safebrowsing.blockedURIs.enabled", true); Services.prefs.setBoolPref("browser.safebrowsing.phishing.enabled", true); Services.prefs.setBoolPref( "browser.safebrowsing.provider.test.disableBackoff", true ); // Add testing tables, we don't use moztest-* here because it doesn't support update Services.prefs.setCharPref("urlclassifier.phishTable", "test-phish-simple"); Services.prefs.setCharPref( "urlclassifier.malwareTable", "test-harmful-simple,test-malware-simple,test-unwanted-simple" ); Services.prefs.setCharPref("urlclassifier.blockedTable", "test-block-simple"); Services.prefs.setCharPref("urlclassifier.trackingTable", "test-track-simple"); Services.prefs.setCharPref( "urlclassifier.trackingWhitelistTable", "test-trackwhite-simple" ); // Enable all completions for tests Services.prefs.setCharPref("urlclassifier.disallow_completions", ""); // Hash completion timeout Services.prefs.setIntPref("urlclassifier.gethash.timeout_ms", 5000); function delFile(name) { try { // Delete a previously created sqlite file var file = Services.dirsvc.get("ProfLD", Ci.nsIFile); file.append(name); if (file.exists()) { file.remove(false); } } catch (e) {} } function cleanUp() { delFile("urlclassifier3.sqlite"); delFile("safebrowsing/classifier.hashkey"); delFile("safebrowsing/test-phish-simple.sbstore"); delFile("safebrowsing/test-malware-simple.sbstore"); delFile("safebrowsing/test-unwanted-simple.sbstore"); delFile("safebrowsing/test-block-simple.sbstore"); delFile("safebrowsing/test-harmful-simple.sbstore"); delFile("safebrowsing/test-track-simple.sbstore"); delFile("safebrowsing/test-trackwhite-simple.sbstore"); delFile("safebrowsing/test-phish-simple.pset"); delFile("safebrowsing/test-malware-simple.pset"); delFile("safebrowsing/test-unwanted-simple.pset"); delFile("safebrowsing/test-block-simple.pset"); delFile("safebrowsing/test-harmful-simple.pset"); delFile("safebrowsing/test-track-simple.pset"); delFile("safebrowsing/test-trackwhite-simple.pset"); delFile("safebrowsing/moz-phish-simple.sbstore"); delFile("safebrowsing/moz-phish-simple.pset"); delFile("testLarge.pset"); delFile("testNoDelta.pset"); } // Update uses allTables by default var allTables = "test-phish-simple,test-malware-simple,test-unwanted-simple,test-track-simple,test-trackwhite-simple,test-block-simple"; var mozTables = "moz-phish-simple"; var dbservice = Cc["@mozilla.org/url-classifier/dbservice;1"].getService( Ci.nsIUrlClassifierDBService ); var streamUpdater = Cc[ "@mozilla.org/url-classifier/streamupdater;1" ].getService(Ci.nsIUrlClassifierStreamUpdater); /* * Builds an update from an object that looks like: *{ "test-phish-simple" : [{ * "chunkType" : "a", // 'a' is assumed if not specified * "chunkNum" : 1, // numerically-increasing chunk numbers are assumed * // if not specified * "urls" : [ "foo.com/a", "foo.com/b", "bar.com/" ] * } */ function buildUpdate(update, hashSize) { if (!hashSize) { hashSize = 32; } var updateStr = "n:1000\n"; for (var tableName in update) { if (tableName != "") { updateStr += "i:" + tableName + "\n"; } var chunks = update[tableName]; for (var j = 0; j < chunks.length; j++) { var chunk = chunks[j]; var chunkType = chunk.chunkType ? chunk.chunkType : "a"; var chunkNum = chunk.chunkNum ? chunk.chunkNum : j; updateStr += chunkType + ":" + chunkNum + ":" + hashSize; if (chunk.urls) { var chunkData = chunk.urls.join("\n"); updateStr += ":" + chunkData.length + "\n" + chunkData; } updateStr += "\n"; } } return updateStr; } function buildPhishingUpdate(chunks, hashSize) { return buildUpdate({ "test-phish-simple": chunks }, hashSize); } function buildMalwareUpdate(chunks, hashSize) { return buildUpdate({ "test-malware-simple": chunks }, hashSize); } function buildUnwantedUpdate(chunks, hashSize) { return buildUpdate({ "test-unwanted-simple": chunks }, hashSize); } function buildBlockedUpdate(chunks, hashSize) { return buildUpdate({ "test-block-simple": chunks }, hashSize); } function buildMozPhishingUpdate(chunks, hashSize) { return buildUpdate({ "moz-phish-simple": chunks }, hashSize); } function buildBareUpdate(chunks, hashSize) { return buildUpdate({ "": chunks }, hashSize); } /** * Performs an update of the dbservice manually, bypassing the stream updater */ function doSimpleUpdate(updateText, success, failure) { var listener = { QueryInterface: ChromeUtils.generateQI(["nsIUrlClassifierUpdateObserver"]), updateUrlRequested(url) {}, streamFinished(status) {}, updateError(errorCode) { failure(errorCode); }, updateSuccess(requestedTimeout) { success(requestedTimeout); }, }; dbservice.beginUpdate(listener, allTables); dbservice.beginStream("", ""); dbservice.updateStream(updateText); dbservice.finishStream(); dbservice.finishUpdate(); } /** * Simulates a failed database update. */ function doErrorUpdate(tables, success, failure) { var listener = { QueryInterface: ChromeUtils.generateQI(["nsIUrlClassifierUpdateObserver"]), updateUrlRequested(url) {}, streamFinished(status) {}, updateError(errorCode) { success(errorCode); }, updateSuccess(requestedTimeout) { failure(requestedTimeout); }, }; dbservice.beginUpdate(listener, tables, null); dbservice.beginStream("", ""); dbservice.cancelUpdate(); } /** * Performs an update of the dbservice using the stream updater and a * data: uri */ function doStreamUpdate(updateText, success, failure, downloadFailure) { var dataUpdate = "data:," + encodeURIComponent(updateText); if (!downloadFailure) { downloadFailure = failure; } streamUpdater.downloadUpdates( allTables, "", true, dataUpdate, success, failure, downloadFailure ); } var gAssertions = { tableData(expectedTables, cb) { dbservice.getTables(function (tables) { // rebuild the tables in a predictable order. var parts = tables.split("\n"); while (parts[parts.length - 1] == "") { parts.pop(); } parts.sort(); tables = parts.join("\n"); Assert.equal(tables, expectedTables); cb(); }); }, checkUrls(urls, expected, cb, useMoz = false) { // work with a copy of the list. urls = urls.slice(0); var doLookup = function () { if (urls.length) { var tables = useMoz ? mozTables : allTables; var fragment = urls.shift(); var principal = Services.scriptSecurityManager.createContentPrincipal( Services.io.newURI("http://" + fragment), {} ); dbservice.lookup( principal, tables, function (arg) { Assert.equal(expected, arg); doLookup(); }, true ); } else { cb(); } }; doLookup(); }, checkTables(url, expected, cb) { var principal = Services.scriptSecurityManager.createContentPrincipal( Services.io.newURI("http://" + url), {} ); dbservice.lookup( principal, allTables, function (tables) { // Rebuild tables in a predictable order. var parts = tables.split(","); while (parts[parts.length - 1] == "") { parts.pop(); } parts.sort(); tables = parts.join(","); Assert.equal(tables, expected); cb(); }, true ); }, urlsDontExist(urls, cb) { this.checkUrls(urls, "", cb); }, urlsExist(urls, cb) { this.checkUrls(urls, "test-phish-simple", cb); }, malwareUrlsExist(urls, cb) { this.checkUrls(urls, "test-malware-simple", cb); }, unwantedUrlsExist(urls, cb) { this.checkUrls(urls, "test-unwanted-simple", cb); }, blockedUrlsExist(urls, cb) { this.checkUrls(urls, "test-block-simple", cb); }, mozPhishingUrlsExist(urls, cb) { this.checkUrls(urls, "moz-phish-simple", cb, true); }, subsDontExist(urls, cb) { // XXX: there's no interface for checking items in the subs table cb(); }, subsExist(urls, cb) { // XXX: there's no interface for checking items in the subs table cb(); }, urlExistInMultipleTables(data, cb) { this.checkTables(data.url, data.tables, cb); }, }; /** * Check a set of assertions against the gAssertions table. */ function checkAssertions(assertions, doneCallback) { var checkAssertion = function () { for (var i in assertions) { var data = assertions[i]; delete assertions[i]; gAssertions[i](data, checkAssertion); return; } doneCallback(); }; checkAssertion(); } function updateError(arg) { do_throw(arg); } /** * Utility functions */ ChromeUtils.defineESModuleGetters(this, { NetUtil: "resource://gre/modules/NetUtil.sys.mjs", }); function readFileToString(aFilename) { let f = do_get_file(aFilename); let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( Ci.nsIFileInputStream ); stream.init(f, -1, 0, 0); let buf = NetUtil.readInputStreamToString(stream, stream.available()); return buf; } // Runs a set of updates, and then checks a set of assertions. function doUpdateTest(updates, assertions, successCallback, errorCallback) { var errorUpdate = function () { checkAssertions(assertions, errorCallback); }; var runUpdate = function () { if (updates.length) { var update = updates.shift(); doStreamUpdate(update, runUpdate, errorUpdate, null); } else { checkAssertions(assertions, successCallback); } }; runUpdate(); } var gTests; var gNextTest = 0; function runNextTest() { if (gNextTest >= gTests.length) { do_test_finished(); return; } dbservice.resetDatabase(); dbservice.setHashCompleter("test-phish-simple", null); let test = gTests[gNextTest++]; dump("running " + test.name + "\n"); test(); } function runTests(tests) { gTests = tests; runNextTest(); } var timerArray = []; function Timer(delay, cb) { this.cb = cb; var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); timer.initWithCallback(this, delay, timer.TYPE_ONE_SHOT); timerArray.push(timer); } Timer.prototype = { QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]), notify(timer) { this.cb(); }, }; // LFSRgenerator is a 32-bit linear feedback shift register random number // generator. It is highly predictable and is not intended to be used for // cryptography but rather to allow easier debugging than a test that uses // Math.random(). function LFSRgenerator(seed) { // Force |seed| to be a number. seed = +seed; // LFSR generators do not work with a value of 0. if (seed == 0) { seed = 1; } this._value = seed; } LFSRgenerator.prototype = { // nextNum returns a random unsigned integer of in the range [0,2^|bits|]. nextNum(bits) { if (!bits) { bits = 32; } let val = this._value; // Taps are 32, 22, 2 and 1. let bit = ((val >>> 0) ^ (val >>> 10) ^ (val >>> 30) ^ (val >>> 31)) & 1; val = (val >>> 1) | (bit << 31); this._value = val; return val >>> (32 - bits); }, }; function waitUntilMetaDataSaved(expectedState, expectedChecksum, callback) { let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"].getService( Ci.nsIUrlClassifierDBService ); dbService.getTables(metaData => { info("metadata: " + metaData); let didCallback = false; metaData.split("\n").some(line => { // Parse [tableName];[stateBase64] let p = line.indexOf(";"); if (-1 === p) { return false; // continue. } let tableName = line.substring(0, p); let metadata = line.substring(p + 1).split(":"); let stateBase64 = metadata[0]; let checksumBase64 = metadata[1]; if (tableName !== "test-phish-proto") { return false; // continue. } if ( stateBase64 === btoa(expectedState) && checksumBase64 === btoa(expectedChecksum) ) { info("State has been saved to disk!"); // We slightly defer the callback to see if the in-memory // |getTables| caching works correctly. dbService.getTables(cachedMetadata => { equal(cachedMetadata, metaData); callback(); }); // Even though we haven't done callback at this moment // but we still claim "we have" in order to stop repeating // a new timer. didCallback = true; } return true; // break no matter whether the state is matching. }); if (!didCallback) { do_timeout( 1000, waitUntilMetaDataSaved.bind( null, expectedState, expectedChecksum, callback ) ); } }); } var gUpdateFinishedObserverEnabled = false; var gUpdateFinishedObserver = function (aSubject, aTopic, aData) { info("[" + aTopic + "] " + aData); if (aData != "success") { updateError(aData); } }; function throwOnUpdateErrors() { Services.obs.addObserver( gUpdateFinishedObserver, "safebrowsing-update-finished" ); gUpdateFinishedObserverEnabled = true; } function stopThrowingOnUpdateErrors() { if (gUpdateFinishedObserverEnabled) { Services.obs.removeObserver( gUpdateFinishedObserver, "safebrowsing-update-finished" ); gUpdateFinishedObserverEnabled = false; } } cleanUp(); registerCleanupFunction(function () { cleanUp(); });