// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- // 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/. // Tests that CRLite filter downloading works correctly. // The file `test_crlite_filters/20201017-0-filter` can be regenerated using // the rust-create-cascade program from https://github.com/mozilla/crlite. // // The input to this program is a list of known serial numbers and a list of // revoked serial numbers. The lists are presented as directories of files in // which each file holds serials for one issuer. The file names are // urlsafe-base64 encoded SHA256 hashes of issuer SPKIs. The file contents are // ascii hex encoded serial numbers. The program crlite_key.py in this directory // can generate these values for you. // // The test filter was generated as follows: // // $ ./crlite_key.py test_crlite_filters/issuer.pem test_crlite_filters/valid.pem // 8Rw90Ej3Ttt8RRkrg-WYDS9n7IS03bk5bjP_UXPtaY8= // 00da4f392bfd8bcea8 // // $ ./crlite_key.py test_crlite_filters/issuer.pem test_crlite_filters/revoked.pem // 8Rw90Ej3Ttt8RRkrg-WYDS9n7IS03bk5bjP_UXPtaY8= // 2d35ca6503fb1ba3 // // $ mkdir known revoked // $ echo "00da4f392bfd8bcea8" > known/8Rw90Ej3Ttt8RRkrg-WYDS9n7IS03bk5bjP_UXPtaY8\= // $ echo "2d35ca6503fb1ba3" >> known/8Rw90Ej3Ttt8RRkrg-WYDS9n7IS03bk5bjP_UXPtaY8\= // $ echo "2d35ca6503fb1ba3" > revoked/8Rw90Ej3Ttt8RRkrg-WYDS9n7IS03bk5bjP_UXPtaY8\= // // $ rust-create-cascade --known ./known/ --revoked ./revoked/ // "use strict"; do_get_profile(); // must be called before getting nsIX509CertDB const { RemoteSecuritySettings } = ChromeUtils.importESModule( "resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs" ); const { TestUtils } = ChromeUtils.importESModule( "resource://testing-common/TestUtils.sys.mjs" ); const { CRLiteFiltersClient } = RemoteSecuritySettings.init(); const CRLITE_FILTERS_ENABLED_PREF = "security.remote_settings.crlite_filters.enabled"; const INTERMEDIATES_ENABLED_PREF = "security.remote_settings.intermediates.enabled"; const INTERMEDIATES_DL_PER_POLL_PREF = "security.remote_settings.intermediates.downloads_per_poll"; // crlite_enrollment_id.py test_crlite_filters/issuer.pem const ISSUER_PEM_UID = "UbH9/ZAnjuqf79Xhah1mFOWo6ZvgQCgsdheWfjvVUM8="; // crlite_enrollment_id.py test_crlite_filters/no-sct-issuer.pem const NO_SCT_ISSUER_PEM_UID = "Myn7EasO1QikOtNmo/UZdh6snCAw0BOY6wgU8OsUeeY="; function getHashCommon(aStr, useBase64) { let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( Ci.nsICryptoHash ); hasher.init(Ci.nsICryptoHash.SHA256); let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( Ci.nsIStringInputStream ); stringStream.data = aStr; hasher.updateFromStream(stringStream, -1); return hasher.finish(useBase64); } // Get a hexified SHA-256 hash of the given string. function getHash(aStr) { return hexify(getHashCommon(aStr, false)); } // Get the name of the file in the test directory to serve as the attachment // for the given filter. function getFilenameForFilter(filter) { if (filter.type == "full") { return "20201017-0-filter"; } if (filter.id == "0001") { return "20201017-1-filter.stash"; } // The addition of another stash file was written more than a month after // other parts of this test. As such, the second stash file for October 17th, // 2020 was not readily available. Since the structure of stash files don't // depend on each other, though, any two stash files are compatible, and so // this stash from December 1st is used instead. return "20201201-3-filter.stash"; } /** * Simulate a Remote Settings synchronization by filling up the local data with * fake records. * * @param {*} filters List of filters for which we will create records. * @param {boolean} clear Whether or not to clear the local DB first. Defaults * to true. */ async function syncAndDownload(filters, clear = true) { const localDB = await CRLiteFiltersClient.client.db; if (clear) { await localDB.clear(); } for (let filter of filters) { const filename = getFilenameForFilter(filter); const file = do_get_file(`test_crlite_filters/${filename}`); const fileBytes = readFile(file); const record = { details: { name: `${filter.timestamp}-${filter.type}`, }, attachment: { hash: getHash(fileBytes), size: fileBytes.length, filename, location: `security-state-workspace/cert-revocations/test_crlite_filters/${filename}`, mimetype: "application/octet-stream", }, incremental: filter.type == "diff", effectiveTimestamp: new Date(filter.timestamp).getTime(), parent: filter.type == "diff" ? filter.parent : undefined, id: filter.id, coverage: filter.type == "full" ? filter.coverage : undefined, enrolledIssuers: filter.type == "full" ? filter.enrolledIssuers : undefined, }; await localDB.create(record); } // This promise will wait for the end of downloading. let promise = TestUtils.topicObserved( "remote-security-settings:crlite-filters-downloaded" ); // Simulate polling for changes, trigger the download of attachments. Services.obs.notifyObservers(null, "remote-settings:changes-poll-end"); let results = await promise; return results[1]; // topicObserved gives back a 2-array } add_task(async function test_crlite_filters_disabled() { Services.prefs.setBoolPref(CRLITE_FILTERS_ENABLED_PREF, false); let result = await syncAndDownload([ { timestamp: "2019-01-01T00:00:00Z", type: "full", id: "0000", coverage: [ { logID: "9lyUL9F3MCIUVBgIMJRWjuNNExkzv98MLyALzE7xZOM=", minTimestamp: 0, maxTimestamp: 9999999999999, }, ], }, ]); equal(result, "disabled", "CRLite filter download should not have run"); }); add_task(async function test_crlite_no_filters() { Services.prefs.setBoolPref(CRLITE_FILTERS_ENABLED_PREF, true); let result = await syncAndDownload([]); equal( result, "unavailable", "CRLite filter download should have run, but nothing was available" ); }); add_task(async function test_crlite_only_incremental_filters() { Services.prefs.setBoolPref(CRLITE_FILTERS_ENABLED_PREF, true); let result = await syncAndDownload([ { timestamp: "2019-01-01T06:00:00Z", type: "diff", id: "0001", parent: "0000", }, { timestamp: "2019-01-01T18:00:00Z", type: "diff", id: "0002", parent: "0001", }, { timestamp: "2019-01-01T12:00:00Z", type: "diff", id: "0003", parent: "0002", }, ]); equal( result, "unavailable", "CRLite filter download should have run, but no full filters were available" ); }); add_task(async function test_crlite_incremental_filters_with_wrong_parent() { Services.prefs.setBoolPref(CRLITE_FILTERS_ENABLED_PREF, true); let result = await syncAndDownload([ { timestamp: "2019-01-01T00:00:00Z", type: "full", id: "0000" }, { timestamp: "2019-01-01T06:00:00Z", type: "diff", id: "0001", parent: "0000", }, { timestamp: "2019-01-01T12:00:00Z", type: "diff", id: "0003", parent: "0002", }, { timestamp: "2019-01-01T18:00:00Z", type: "diff", id: "0004", parent: "0003", }, ]); let [status, filters] = result.split(";"); equal(status, "finished", "CRLite filter download should have run"); let filtersSplit = filters.split(","); deepEqual( filtersSplit, ["2019-01-01T00:00:00Z-full", "2019-01-01T06:00:00Z-diff"], "Should have downloaded the expected CRLite filters" ); }); add_task(async function test_crlite_incremental_filter_too_early() { Services.prefs.setBoolPref(CRLITE_FILTERS_ENABLED_PREF, true); let result = await syncAndDownload([ { timestamp: "2019-01-02T00:00:00Z", type: "full", id: "0000" }, { timestamp: "2019-01-01T00:00:00Z", type: "diff", id: "0001", parent: "0000", }, ]); equal( result, "finished;2019-01-02T00:00:00Z-full", "CRLite filter download should have run" ); }); add_task(async function test_crlite_filters_basic() { Services.prefs.setBoolPref(CRLITE_FILTERS_ENABLED_PREF, true); let result = await syncAndDownload([ { timestamp: "2019-01-01T00:00:00Z", type: "full", id: "0000" }, ]); equal( result, "finished;2019-01-01T00:00:00Z-full", "CRLite filter download should have run" ); }); add_task(async function test_crlite_filters_not_cached() { Services.prefs.setBoolPref(CRLITE_FILTERS_ENABLED_PREF, true); let filters = [ { timestamp: "2019-01-01T00:00:00Z", type: "full", id: "0000" }, ]; let result = await syncAndDownload(filters); equal( result, "finished;2019-01-01T00:00:00Z-full", "CRLite filter download should have run" ); let records = await CRLiteFiltersClient.client.db.list(); // `syncAndDownload` should not cache the attachment, so this download should // get the attachment from the source. let attachment = await CRLiteFiltersClient.client.attachments.download( records[0] ); equal(attachment._source, "remote_match"); await CRLiteFiltersClient.client.attachments.deleteDownloaded(records[0]); }); add_task(async function test_crlite_filters_full_and_incremental() { Services.prefs.setBoolPref(CRLITE_FILTERS_ENABLED_PREF, true); let result = await syncAndDownload([ // These are deliberately listed out of order. { timestamp: "2019-01-01T06:00:00Z", type: "diff", id: "0001", parent: "0000", }, { timestamp: "2019-01-01T00:00:00Z", type: "full", id: "0000" }, { timestamp: "2019-01-01T18:00:00Z", type: "diff", id: "0003", parent: "0002", }, { timestamp: "2019-01-01T12:00:00Z", type: "diff", id: "0002", parent: "0001", }, ]); let [status, filters] = result.split(";"); equal(status, "finished", "CRLite filter download should have run"); let filtersSplit = filters.split(","); deepEqual( filtersSplit, [ "2019-01-01T00:00:00Z-full", "2019-01-01T06:00:00Z-diff", "2019-01-01T12:00:00Z-diff", "2019-01-01T18:00:00Z-diff", ], "Should have downloaded the expected CRLite filters" ); }); add_task(async function test_crlite_filters_multiple_days() { Services.prefs.setBoolPref(CRLITE_FILTERS_ENABLED_PREF, true); let result = await syncAndDownload([ // These are deliberately listed out of order. { timestamp: "2019-01-02T06:00:00Z", type: "diff", id: "0011", parent: "0010", }, { timestamp: "2019-01-03T12:00:00Z", type: "diff", id: "0022", parent: "0021", }, { timestamp: "2019-01-02T12:00:00Z", type: "diff", id: "0012", parent: "0011", }, { timestamp: "2019-01-03T18:00:00Z", type: "diff", id: "0023", parent: "0022", }, { timestamp: "2019-01-02T18:00:00Z", type: "diff", id: "0013", parent: "0012", }, { timestamp: "2019-01-02T00:00:00Z", type: "full", id: "0010" }, { timestamp: "2019-01-03T00:00:00Z", type: "full", id: "0020" }, { timestamp: "2019-01-01T06:00:00Z", type: "diff", id: "0001", parent: "0000", }, { timestamp: "2019-01-01T18:00:00Z", type: "diff", id: "0003", parent: "0002", }, { timestamp: "2019-01-01T12:00:00Z", type: "diff", id: "0002", parent: "0001", }, { timestamp: "2019-01-01T00:00:00Z", type: "full", id: "0000" }, { timestamp: "2019-01-03T06:00:00Z", type: "diff", id: "0021", parent: "0020", }, ]); let [status, filters] = result.split(";"); equal(status, "finished", "CRLite filter download should have run"); let filtersSplit = filters.split(","); deepEqual( filtersSplit, [ "2019-01-03T00:00:00Z-full", "2019-01-03T06:00:00Z-diff", "2019-01-03T12:00:00Z-diff", "2019-01-03T18:00:00Z-diff", ], "Should have downloaded the expected CRLite filters" ); }); add_task(async function test_crlite_confirm_revocations_mode() { Services.prefs.setBoolPref(CRLITE_FILTERS_ENABLED_PREF, true); Services.prefs.setIntPref( "security.pki.crlite_mode", CRLiteModeConfirmRevocationsValue ); Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true); let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( Ci.nsIX509CertDB ); addCertFromFile(certdb, "test_crlite_filters/issuer.pem", ",,"); addCertFromFile(certdb, "test_crlite_filters/no-sct-issuer.pem", ",,"); let result = await syncAndDownload([ { timestamp: "2020-10-17T00:00:00Z", type: "full", id: "0000", coverage: [ { logID: "9lyUL9F3MCIUVBgIMJRWjuNNExkzv98MLyALzE7xZOM=", minTimestamp: 0, maxTimestamp: 9999999999999, }, { logID: "pLkJkLQYWBSHuxOizGdwCjw1mAT5G9+443fNDsgN3BA=", minTimestamp: 0, maxTimestamp: 9999999999999, }, ], enrolledIssuers: [ISSUER_PEM_UID, NO_SCT_ISSUER_PEM_UID], }, ]); equal( result, "finished;2020-10-17T00:00:00Z-full", "CRLite filter download should have run" ); // The CRLite result should be enforced for this certificate and // OCSP should not be consulted. let validCert = constructCertFromFile("test_crlite_filters/valid.pem"); await checkCertErrorGenericAtTime( certdb, validCert, PRErrorCodeSuccess, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, undefined, "vpn.worldofspeed.org", 0 ); // OCSP should be consulted for this certificate, but OCSP is disabled by // Ci.nsIX509CertDB.FLAG_LOCAL_ONLY so this will be treated as a soft-failure // and the CRLite result will be used. let revokedCert = constructCertFromFile("test_crlite_filters/revoked.pem"); await checkCertErrorGenericAtTime( certdb, revokedCert, SEC_ERROR_REVOKED_CERTIFICATE, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, undefined, "us-datarecovery.com", Ci.nsIX509CertDB.FLAG_LOCAL_ONLY ); // Reload the filter w/o coverage and enrollment metadata. result = await syncAndDownload([ { timestamp: "2020-10-17T00:00:00Z", type: "full", id: "0000", coverage: [], enrolledIssuers: [], }, ]); equal( result, "finished;2020-10-17T00:00:00Z-full", "CRLite filter download should have run" ); // OCSP will be consulted for the revoked certificate, but a soft-failure // should now result in a Success return. await checkCertErrorGenericAtTime( certdb, revokedCert, PRErrorCodeSuccess, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, undefined, "us-datarecovery.com", Ci.nsIX509CertDB.FLAG_LOCAL_ONLY ); }); add_task(async function test_crlite_filters_and_check_revocation() { Services.prefs.setBoolPref(CRLITE_FILTERS_ENABLED_PREF, true); Services.prefs.setIntPref( "security.pki.crlite_mode", CRLiteModeEnforcePrefValue ); Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true); let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( Ci.nsIX509CertDB ); addCertFromFile(certdb, "test_crlite_filters/issuer.pem", ",,"); addCertFromFile(certdb, "test_crlite_filters/no-sct-issuer.pem", ",,"); let result = await syncAndDownload([ { timestamp: "2020-10-17T00:00:00Z", type: "full", id: "0000", coverage: [ { logID: "9lyUL9F3MCIUVBgIMJRWjuNNExkzv98MLyALzE7xZOM=", minTimestamp: 0, maxTimestamp: 9999999999999, }, { logID: "pLkJkLQYWBSHuxOizGdwCjw1mAT5G9+443fNDsgN3BA=", minTimestamp: 0, maxTimestamp: 9999999999999, }, ], enrolledIssuers: [ISSUER_PEM_UID, NO_SCT_ISSUER_PEM_UID], }, ]); equal( result, "finished;2020-10-17T00:00:00Z-full", "CRLite filter download should have run" ); let validCert = constructCertFromFile("test_crlite_filters/valid.pem"); // NB: by not specifying Ci.nsIX509CertDB.FLAG_LOCAL_ONLY, this tests that // the implementation does not fall back to OCSP fetching, because if it // did, the implementation would attempt to connect to a server outside the // test infrastructure, which would result in a crash in the test // environment, which would be treated as a test failure. await checkCertErrorGenericAtTime( certdb, validCert, PRErrorCodeSuccess, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, false, "vpn.worldofspeed.org", 0 ); let revokedCert = constructCertFromFile("test_crlite_filters/revoked.pem"); await checkCertErrorGenericAtTime( certdb, revokedCert, SEC_ERROR_REVOKED_CERTIFICATE, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, false, "us-datarecovery.com", 0 ); // Before any stashes are downloaded, this should verify successfully. let revokedInStashCert = constructCertFromFile( "test_crlite_filters/revoked-in-stash.pem" ); await checkCertErrorGenericAtTime( certdb, revokedInStashCert, PRErrorCodeSuccess, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, false, "stokedmoto.com", 0 ); result = await syncAndDownload( [ { timestamp: "2020-10-17T03:00:00Z", type: "diff", id: "0001", parent: "0000", }, ], false ); equal( result, "finished;2020-10-17T03:00:00Z-diff", "Should have downloaded the expected CRLite filters" ); // After downloading the first stash, this should be revoked. await checkCertErrorGenericAtTime( certdb, revokedInStashCert, SEC_ERROR_REVOKED_CERTIFICATE, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, false, "stokedmoto.com", 0 ); // Before downloading the second stash, this should not be revoked. let revokedInStash2Cert = constructCertFromFile( "test_crlite_filters/revoked-in-stash-2.pem" ); await checkCertErrorGenericAtTime( certdb, revokedInStash2Cert, PRErrorCodeSuccess, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, false, "icsreps.com", 0 ); result = await syncAndDownload( [ { timestamp: "2020-10-17T06:00:00Z", type: "diff", id: "0002", parent: "0001", }, ], false ); equal( result, "finished;2020-10-17T06:00:00Z-diff", "Should have downloaded the expected CRLite filters" ); // After downloading the second stash, this should be revoked. await checkCertErrorGenericAtTime( certdb, revokedInStash2Cert, SEC_ERROR_REVOKED_CERTIFICATE, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, false, "icsreps.com", 0 ); // The other certificates should still get the same results as they did before. await checkCertErrorGenericAtTime( certdb, validCert, PRErrorCodeSuccess, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, false, "vpn.worldofspeed.org", 0 ); await checkCertErrorGenericAtTime( certdb, revokedCert, SEC_ERROR_REVOKED_CERTIFICATE, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, false, "us-datarecovery.com", 0 ); await checkCertErrorGenericAtTime( certdb, revokedInStashCert, SEC_ERROR_REVOKED_CERTIFICATE, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, false, "stokedmoto.com", 0 ); // This certificate has no embedded SCTs, so it is not guaranteed to be in // CT, so CRLite can't be guaranteed to give the correct answer, so it is // not consulted, and the implementation falls back to OCSP. Since the real // OCSP responder can't be reached, this results in a // SEC_ERROR_OCSP_SERVER_ERROR. let noSCTCert = constructCertFromFile("test_crlite_filters/no-sct.pem"); // NB: this will cause an OCSP request to be sent to localhost:80, but // since an OCSP responder shouldn't be running on that port, this should // fail safely. Services.prefs.setCharPref("network.dns.localDomains", "ocsp.digicert.com"); Services.prefs.setBoolPref("security.OCSP.require", true); Services.prefs.setIntPref("security.OCSP.enabled", 1); await checkCertErrorGenericAtTime( certdb, noSCTCert, SEC_ERROR_OCSP_SERVER_ERROR, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, false, "mail233.messagelabs.com", 0 ); Services.prefs.clearUserPref("network.dns.localDomains"); Services.prefs.clearUserPref("security.OCSP.require"); Services.prefs.clearUserPref("security.OCSP.enabled"); // The revoked certificate example has one SCT from the log with ID "9ly...=" // at time 1598140096613 and another from the log with ID "XNx...=" at time // 1598140096917. The filter we construct here fails to cover it by one // millisecond in each case. The implementation will fall back to OCSP // fetching. Since this would result in a crash and test failure, the // Ci.nsIX509CertDB.FLAG_LOCAL_ONLY is used. result = await syncAndDownload([ { timestamp: "2020-10-17T00:00:00Z", type: "full", id: "0000", coverage: [ { logID: "9lyUL9F3MCIUVBgIMJRWjuNNExkzv98MLyALzE7xZOM=", minTimestamp: 0, maxTimestamp: 1598140096612, }, { logID: "XNxDkv7mq0VEsV6a1FbmEDf71fpH3KFzlLJe5vbHDso=", minTimestamp: 1598140096917, maxTimestamp: 9999999999999, }, ], enrolledIssuers: [ISSUER_PEM_UID, NO_SCT_ISSUER_PEM_UID], }, ]); equal( result, "finished;2020-10-17T00:00:00Z-full", "CRLite filter download should have run" ); await checkCertErrorGenericAtTime( certdb, revokedCert, PRErrorCodeSuccess, certificateUsageSSLServer, new Date("2020-10-20T00:00:00Z").getTime() / 1000, false, "us-datarecovery.com", Ci.nsIX509CertDB.FLAG_LOCAL_ONLY ); }); add_task(async function test_crlite_filters_avoid_reprocessing_filters() { Services.prefs.setBoolPref(CRLITE_FILTERS_ENABLED_PREF, true); let result = await syncAndDownload([ { timestamp: "2019-01-01T00:00:00Z", type: "full", id: "0000", coverage: [ { logID: "9lyUL9F3MCIUVBgIMJRWjuNNExkzv98MLyALzE7xZOM=", minTimestamp: 0, maxTimestamp: 9999999999999, }, ], enrolledIssuers: [ISSUER_PEM_UID, NO_SCT_ISSUER_PEM_UID], }, { timestamp: "2019-01-01T06:00:00Z", type: "diff", id: "0001", parent: "0000", }, { timestamp: "2019-01-01T12:00:00Z", type: "diff", id: "0002", parent: "0001", }, { timestamp: "2019-01-01T18:00:00Z", type: "diff", id: "0003", parent: "0002", }, ]); let [status, filters] = result.split(";"); equal(status, "finished", "CRLite filter download should have run"); let filtersSplit = filters.split(","); deepEqual( filtersSplit, [ "2019-01-01T00:00:00Z-full", "2019-01-01T06:00:00Z-diff", "2019-01-01T12:00:00Z-diff", "2019-01-01T18:00:00Z-diff", ], "Should have downloaded the expected CRLite filters" ); // This simulates another poll without clearing the database first. The // filter and stashes should not be re-downloaded. result = await syncAndDownload([], false); equal(result, "finished;"); // If a new stash is added, only it should be downloaded. result = await syncAndDownload( [ { timestamp: "2019-01-02T00:00:00Z", type: "diff", id: "0004", parent: "0003", }, ], false ); equal(result, "finished;2019-01-02T00:00:00Z-diff"); }); let server; function run_test() { server = new HttpServer(); server.start(-1); registerCleanupFunction(() => server.stop(() => {})); server.registerDirectory( "/cdn/security-state-workspace/cert-revocations/", do_get_file(".") ); 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` ); // Set intermediate preloading to download 0 intermediates at a time. Services.prefs.setIntPref(INTERMEDIATES_DL_PER_POLL_PREF, 0); Services.prefs.setCharPref("browser.policies.loglevel", "debug"); run_next_test(); }