/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; /* Unit tests for the nsIUrlClassifierRemoteSettingsService implementation. */ const { RemoteSettings } = ChromeUtils.importESModule( "resource://services-settings/remote-settings.sys.mjs" ); const { SBRS_UPDATE_MINIMUM_DELAY } = ChromeUtils.importESModule( "resource://gre/modules/UrlClassifierRemoteSettingsService.sys.mjs" ); const COLLECTION_NAME = "tracking-protection-lists"; const REMOTE_SETTINGS_DATA = [ { Name: "content-fingerprinting-track-digest256", attachment: { hash: "96a4a850a1a475001148fa8a3a5efea58951f7176d3624ad7614fbf32732ee48", size: 948, filename: "content-fingerprinting-track-digest256", location: "main-workspace/tracking-protection-lists/content-fingerprinting-track-digest256", mimetype: "text/plain", }, id: "content-fingerprinting-track-digest256", Version: 1597417364, }, { Name: "mozplugin-block-digest256", attachment: { hash: "dd2b800c7e4bad17e1c79f3e530c0b94e0a039adf4566f30bc3c285a547fa4fc", size: 3029, filename: "mozplugin-block-digest256", location: "main-workspace/tracking-protection-lists/mozplugin-block-digest256", mimetype: "text/plain", }, id: "mozplugin-block-digest256", Version: 1575583456, }, { Name: "google-trackwhite-digest256", attachment: { hash: "1cd6d9353e97d66ac737a9716cd3a33416d6a4884dd12dcd1d65266e4c81dfad", size: 1470328, filename: "google-trackwhite-digest256", location: "main-workspace/tracking-protection-lists/google-trackwhite-digest256", mimetype: "text/plain", }, id: "google-trackwhite-digest256", Version: 1575583456, }, // Entry with non-exist attachment { Name: "social-track-digest256", attachment: { location: "main-workspace/tracking-protection-lists/not-exist", }, id: "social-track-digest256", Version: 1111111111, }, // Entry with corrupted attachment { Name: "analytic-track-digest256", attachment: { hash: "644a0662bcf7313570ee68490e3805f5cc7a0503c097f040525c28dc5bfe4c97", size: 58, filename: "invalid.chunk", location: "main-workspace/tracking-protection-lists/invalid.chunk", mimetype: "text/plain", }, id: "analytic-track-digest256", Version: 1111111111, }, ]; let gListService = Cc["@mozilla.org/url-classifier/list-service;1"].getService( Ci.nsIUrlClassifierRemoteSettingsService ); let gDbService = Cc["@mozilla.org/url-classifier/dbservice;1"].getService( Ci.nsIUrlClassifierDBService ); class UpdateEvent extends EventTarget {} function waitForEvent(element, eventName) { return new Promise(function (resolve) { element.addEventListener(eventName, e => resolve(e.detail), { once: true }); }); } function buildPayload(tables) { let payload = ``; for (let table of tables) { payload += table[0]; if (table[1] != null) { payload += `;a:${table[1]}`; } payload += `\n`; } return payload; } let server; add_setup(async function init() { Services.prefs.setCharPref( "browser.safebrowsing.provider.mozilla.updateURL", `moz-sbrs://tracking-protection-list` ); // Setup HTTP server for remote setting server = new HttpServer(); server.start(-1); registerCleanupFunction(() => server.stop(() => {})); server.registerDirectory( "/cdn/main-workspace/tracking-protection-lists/", do_get_file("data") ); server.registerPathHandler("/v1/", (request, response) => { response.write( JSON.stringify({ capabilities: { attachments: { base_url: `http://localhost:${server.identity.primaryPort}/cdn/`, }, }, }) ); response.setHeader("Content-Type", "application/json; charset=UTF-8"); response.setStatusLine(null, 200, "OK"); }); Services.prefs.setCharPref( "services.settings.server", `http://localhost:${server.identity.primaryPort}/v1` ); // Setup remote setting initial data let db = await RemoteSettings(COLLECTION_NAME).db; await db.importChanges({}, 42, REMOTE_SETTINGS_DATA); registerCleanupFunction(() => { Services.prefs.clearUserPref( "browser.safebrowsing.provider.mozilla.updateURL" ); Services.prefs.clearUserPref("services.settings.server"); }); }); // Test updates from RemoteSettings when there is no local data add_task(async function test_empty_update() { let updateEvent = new UpdateEvent(); let promise = waitForEvent(updateEvent, "update"); const TEST_TABLES = [ ["mozplugin-block-digest256", null], // empty ["content-fingerprinting-track-digest256", null], // empty ]; gListService.fetchList(buildPayload(TEST_TABLES), { // nsIStreamListener observer onStartRequest(request) {}, onDataAvailable(aRequest, aStream, aOffset, aCount) { let stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( Ci.nsIScriptableInputStream ); stream.init(aStream); let event = new CustomEvent("update", { detail: stream.readBytes(aCount), }); updateEvent.dispatchEvent(event); }, onStopRequest(request, status) {}, }); let expected = "n:" + SBRS_UPDATE_MINIMUM_DELAY + "\n"; for (const table of TEST_TABLES) { expected += `i:${table[0]}\n` + readFileToString(`data/${table[0]}`); } Assert.equal( await promise, expected, "Receive expected data from onDataAvailable" ); gListService.clear(); }); // Test updates from RemoteSettings when we have an empty table, // a table with an older version, and a table which is up-to-date. add_task(async function test_update() { let updateEvent = new UpdateEvent(); let promise = waitForEvent(updateEvent, "update"); const TEST_TABLES = [ ["mozplugin-block-digest256", 1575583456], // up-to-date ["content-fingerprinting-track-digest256", 1575583456 - 1], // older version ]; gListService.fetchList(buildPayload(TEST_TABLES), { // observer // nsIStreamListener observer onStartRequest(request) {}, onDataAvailable(aRequest, aStream, aOffset, aCount) { let stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( Ci.nsIScriptableInputStream ); stream.init(aStream); let event = new CustomEvent("update", { detail: stream.readBytes(aCount), }); updateEvent.dispatchEvent(event); }, onStopRequest(request, status) {}, }); // Build request with no version let expected = "n:" + SBRS_UPDATE_MINIMUM_DELAY + "\n"; for (const table of TEST_TABLES) { if (["content-fingerprinting-track-digest256"].includes(table[0])) { expected += `i:${table[0]}\n` + readFileToString(`data/${table[0]}`); } } Assert.equal( await promise, expected, "Receive expected data from onDataAvailable" ); gListService.clear(); }); // Test updates from RemoteSettings service when all tables are up-to-date. add_task(async function test_no_update() { let updateEvent = new UpdateEvent(); let promise = waitForEvent(updateEvent, "update"); const TEST_TABLES = [ ["mozplugin-block-digest256", 1575583456], // up-to-date ["content-fingerprinting-track-digest256", 1597417364], // up-to-date ]; gListService.fetchList(buildPayload(TEST_TABLES), { // nsIStreamListener observer onStartRequest(request) {}, onDataAvailable(aRequest, aStream, aOffset, aCount) { let stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( Ci.nsIScriptableInputStream ); stream.init(aStream); let event = new CustomEvent("update", { detail: stream.readBytes(aCount), }); updateEvent.dispatchEvent(event); }, onStopRequest(request, status) {}, }); // No data is expected let expected = "n:" + SBRS_UPDATE_MINIMUM_DELAY + "\n"; Assert.equal( await promise, expected, "Receive expected data from onDataAvailable" ); gListService.clear(); }); add_test(function test_update() { let streamUpdater = Cc[ "@mozilla.org/url-classifier/streamupdater;1" ].getService(Ci.nsIUrlClassifierStreamUpdater); // Download some updates, and don't continue until the downloads are done. function updateSuccess(aEvent) { Assert.equal(SBRS_UPDATE_MINIMUM_DELAY, aEvent); info("All data processed"); run_next_test(); } // Just throw if we ever get an update or download error. function handleError(aEvent) { do_throw("We didn't download or update correctly: " + aEvent); } streamUpdater.downloadUpdates( "content-fingerprinting-track-digest256", "content-fingerprinting-track-digest256;\n", true, "moz-sbrs://remote-setting", updateSuccess, handleError, handleError ); }); add_test(function test_url_not_denylisted() { let uri = Services.io.newURI("http://example.com"); let principal = Services.scriptSecurityManager.createContentPrincipal( uri, {} ); gDbService.lookup( principal, "content-fingerprinting-track-digest256", function handleEvent(aEvent) { // This URI is not on any lists. Assert.equal("", aEvent); run_next_test(); } ); }); add_test(function test_url_denylisted() { let uri = Services.io.newURI("https://www.foresee.com"); let principal = Services.scriptSecurityManager.createContentPrincipal( uri, {} ); gDbService.lookup( principal, "content-fingerprinting-track-digest256", function handleEvent(aEvent) { Assert.equal("content-fingerprinting-track-digest256", aEvent); run_next_test(); } ); }); add_test(function test_update_download_error() { let streamUpdater = Cc[ "@mozilla.org/url-classifier/streamupdater;1" ].getService(Ci.nsIUrlClassifierStreamUpdater); // Download some updates, and don't continue until the downloads are done. function updateSuccessOrError(aEvent) { do_throw("Should be downbload error"); } // Just throw if we ever get an update or download error. function downloadError(aEvent) { run_next_test(); } streamUpdater.downloadUpdates( "social-track-digest256", "social-track-digest256;\n", true, "moz-sbrs://remote-setting", updateSuccessOrError, updateSuccessOrError, downloadError ); }); add_test(function test_update_update_error() { let streamUpdater = Cc[ "@mozilla.org/url-classifier/streamupdater;1" ].getService(Ci.nsIUrlClassifierStreamUpdater); // Download some updates, and don't continue until the downloads are done. function updateSuccessOrDownloadError(aEvent) { do_throw("Should be update error"); } // Just throw if we ever get an update or download error. function updateError(aEvent) { run_next_test(); } streamUpdater.downloadUpdates( "analytic-track-digest256", "analytic-track-digest256;\n", true, "moz-sbrs://remote-setting", updateSuccessOrDownloadError, updateError, updateSuccessOrDownloadError ); }); add_task(async function test_update_large_file() { let updateEvent = new UpdateEvent(); let promise = waitForEvent(updateEvent, "update"); const TEST_TABLES = [ ["google-trackwhite-digest256", 1575583456 - 1], // up-to-date ]; gListService.fetchList(buildPayload(TEST_TABLES), { // observer // nsIStreamListener observer onStartRequest(request) {}, onDataAvailable(aRequest, aStream, aOffset, aCount) { let stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( Ci.nsIScriptableInputStream ); stream.init(aStream); let event = new CustomEvent("update", { detail: stream.readBytes(aCount), }); updateEvent.dispatchEvent(event); }, onStopRequest(request, status) {}, }); // Build request with no version let expected = "n:" + SBRS_UPDATE_MINIMUM_DELAY + "\n"; for (const table of TEST_TABLES) { if (["google-trackwhite-digest256"].includes(table[0])) { expected += `i:${table[0]}\n` + readFileToString(`data/${table[0]}`); } } Assert.equal( await promise, expected, "Receive expected data from onDataAvailable" ); gListService.clear(); });