/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable mozilla/no-arbitrary-setTimeout */
const URL_HOST = "http://localhost";
const PR_USEC_PER_MSEC = 1000;
const { GMPExtractor, GMPInstallManager } = ChromeUtils.importESModule(
"resource://gre/modules/GMPInstallManager.sys.mjs"
);
const { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
const { FileUtils } = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
);
const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
const { Preferences } = ChromeUtils.importESModule(
"resource://gre/modules/Preferences.sys.mjs"
);
const { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
const { UpdateUtils } = ChromeUtils.importESModule(
"resource://gre/modules/UpdateUtils.sys.mjs"
);
const { GMPPrefs, OPEN_H264_ID } = ChromeUtils.importESModule(
"resource://gre/modules/GMPUtils.sys.mjs"
);
const { ProductAddonCheckerTestUtils } = ChromeUtils.importESModule(
"resource://gre/modules/addons/ProductAddonChecker.sys.mjs"
);
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
Services.prefs.setBoolPref("media.gmp-manager.updateEnabled", true);
// Gather the telemetry even where the probes don't exist (i.e. Thunderbird).
Services.prefs.setBoolPref(
"toolkit.telemetry.testing.overrideProductsCheck",
true
);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
Services.prefs.clearUserPref("media.gmp-manager.updateEnabled");
Services.prefs.clearUserPref(
"toolkit.telemetry.testing.overrideProductsCheck"
);
});
// Most tests do no handle the machinery for content signatures, so let
// specific tests that need it turn it on as needed.
Preferences.set("media.gmp-manager.checkContentSignature", false);
do_get_profile();
add_task(function test_setup() {
// We should already have a profile from `do_get_profile`, but also need
// FOG to be setup for tests that touch it/Glean.
Services.fog.initializeFOG();
});
function run_test() {
Preferences.set("media.gmp.log.dump", true);
Preferences.set("media.gmp.log.level", 0);
run_next_test();
}
/**
* Tests that the helper used for preferences works correctly
*/
add_task(async function test_prefs() {
let addon1 = "addon1",
addon2 = "addon2";
GMPPrefs.setString(GMPPrefs.KEY_URL, "http://not-really-used");
GMPPrefs.setString(GMPPrefs.KEY_URL_OVERRIDE, "http://not-really-used-2");
GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, 1, addon1);
GMPPrefs.setString(GMPPrefs.KEY_PLUGIN_VERSION, "2", addon1);
GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, 3, addon2);
GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_VERSION, 4, addon2);
GMPPrefs.setBool(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, addon2);
GMPPrefs.setBool(GMPPrefs.KEY_CERT_CHECKATTRS, true);
GMPPrefs.setString(GMPPrefs.KEY_PLUGIN_HASHVALUE, "5", addon1);
Assert.equal(GMPPrefs.getString(GMPPrefs.KEY_URL), "http://not-really-used");
Assert.equal(
GMPPrefs.getString(GMPPrefs.KEY_URL_OVERRIDE),
"http://not-really-used-2"
);
Assert.equal(GMPPrefs.getInt(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", addon1), 1);
Assert.equal(
GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_VERSION, "", addon1),
"2"
);
Assert.equal(GMPPrefs.getInt(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", addon2), 3);
Assert.equal(GMPPrefs.getInt(GMPPrefs.KEY_PLUGIN_VERSION, "", addon2), 4);
Assert.equal(
GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, undefined, addon2),
false
);
Assert.ok(GMPPrefs.getBool(GMPPrefs.KEY_CERT_CHECKATTRS));
GMPPrefs.setBool(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, addon2);
Assert.equal(
GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_HASHVALUE, "", addon1),
"5"
);
});
/**
* Tests that an uninit without a check works fine
*/
add_task(async function test_checkForAddons_uninitWithoutCheck() {
let installManager = new GMPInstallManager();
installManager.uninit();
});
/**
* Tests that an uninit without an install works fine
*/
add_test(function test_checkForAddons_uninitWithoutInstall() {
let myRequest = new mockRequest(200, "");
let installManager = new GMPInstallManager();
let promise = ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
promise.then(res => {
Assert.ok(res.usedFallback);
installManager.uninit();
run_next_test();
});
});
/**
* Tests that no response returned rejects
*/
add_test(function test_checkForAddons_noResponse() {
let myRequest = new mockRequest(200, "");
let installManager = new GMPInstallManager();
let promise = ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
promise.then(res => {
Assert.ok(res.usedFallback);
installManager.uninit();
run_next_test();
});
});
/**
* Tests that no addons element returned resolves with no addons
*/
add_task(async function test_checkForAddons_noAddonsElement() {
let myRequest = new mockRequest(200, "");
let installManager = new GMPInstallManager();
let res = await ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
Assert.equal(res.addons.length, 0);
installManager.uninit();
});
/**
* Tests that empty addons element returned resolves with no addons
*/
add_task(async function test_checkForAddons_emptyAddonsElement() {
let myRequest = new mockRequest(200, "");
let installManager = new GMPInstallManager();
let res = await ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
Assert.equal(res.addons.length, 0);
installManager.uninit();
});
/**
* Tests that a response with the wrong root element rejects
*/
add_test(function test_checkForAddons_wrongResponseXML() {
let myRequest = new mockRequest(
200,
"3.141592653589793...."
);
let installManager = new GMPInstallManager();
let promise = ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
promise.then(res => {
Assert.ok(res.usedFallback);
installManager.uninit();
run_next_test();
});
});
/**
* Tests that a 404 error works as expected
*/
add_test(function test_checkForAddons_404Error() {
let myRequest = new mockRequest(404, "");
let installManager = new GMPInstallManager();
let promise = ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
promise.then(res => {
Assert.ok(res.usedFallback);
installManager.uninit();
run_next_test();
});
});
/**
* Tests that a xhr/ServiceRequest abort() works as expected
*/
add_test(function test_checkForAddons_abort() {
let overriddenServiceRequest = new mockRequest(200, "", {
dropRequest: true,
});
let installManager = new GMPInstallManager();
let promise = ProductAddonCheckerTestUtils.overrideServiceRequest(
overriddenServiceRequest,
() => installManager.checkForAddons()
);
// Since the ServiceRequest is created in checkForAddons asynchronously,
// we need to delay aborting till the request is running.
// Since checkForAddons returns a Promise already only after
// the abort is triggered, we can't use that, and instead
// we'll use a fake timer.
setTimeout(() => {
overriddenServiceRequest.abort();
}, 100);
promise.then(res => {
Assert.ok(res.usedFallback);
installManager.uninit();
run_next_test();
});
});
/**
* Tests that a defensive timeout works as expected
*/
add_test(function test_checkForAddons_timeout() {
let myRequest = new mockRequest(200, "", {
dropRequest: true,
timeout: true,
});
let installManager = new GMPInstallManager();
let promise = ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
promise.then(res => {
Assert.ok(res.usedFallback);
installManager.uninit();
run_next_test();
});
});
/**
* Tests that we throw correctly in case of ssl certification error.
*/
add_test(function test_checkForAddons_bad_ssl() {
//
// Add random stuff that cause CertUtil to require https.
//
let PREF_KEY_URL_OVERRIDE_BACKUP = Preferences.get(
GMPPrefs.KEY_URL_OVERRIDE,
""
);
Preferences.reset(GMPPrefs.KEY_URL_OVERRIDE);
let CERTS_BRANCH_DOT_ONE = GMPPrefs.KEY_CERTS_BRANCH + ".1";
let PREF_CERTS_BRANCH_DOT_ONE_BACKUP = Preferences.get(
CERTS_BRANCH_DOT_ONE,
""
);
Services.prefs.setCharPref(CERTS_BRANCH_DOT_ONE, "funky value");
let myRequest = new mockRequest(200, "");
let installManager = new GMPInstallManager();
let promise = ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
promise.then(res => {
Assert.ok(res.usedFallback);
installManager.uninit();
if (PREF_KEY_URL_OVERRIDE_BACKUP) {
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, PREF_KEY_URL_OVERRIDE_BACKUP);
}
if (PREF_CERTS_BRANCH_DOT_ONE_BACKUP) {
Preferences.set(CERTS_BRANCH_DOT_ONE, PREF_CERTS_BRANCH_DOT_ONE_BACKUP);
}
run_next_test();
});
});
/**
* Tests that gettinga a funky non XML response works as expected
*/
add_test(function test_checkForAddons_notXML() {
let myRequest = new mockRequest(200, "3.141592653589793....");
let installManager = new GMPInstallManager();
let promise = ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
promise.then(res => {
Assert.ok(res.usedFallback);
installManager.uninit();
run_next_test();
});
});
/**
* Tests that getting a response with a single addon works as expected
*/
add_task(async function test_checkForAddons_singleAddon() {
let responseXML =
'' +
"" +
" " +
' ' +
" " +
"";
let myRequest = new mockRequest(200, responseXML);
let installManager = new GMPInstallManager();
let res = await ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
Assert.equal(res.addons.length, 1);
let gmpAddon = res.addons[0];
Assert.equal(gmpAddon.id, "gmp-gmpopenh264");
Assert.equal(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
Assert.equal(gmpAddon.hashFunction, "sha256");
Assert.equal(
gmpAddon.hashValue,
"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"
);
Assert.equal(gmpAddon.version, "1.1");
Assert.ok(gmpAddon.isValid);
Assert.ok(!gmpAddon.isInstalled);
installManager.uninit();
});
/**
* Tests that getting a response with a single addon with the optional size
* attribute parses as expected.
*/
add_task(async function test_checkForAddons_singleAddonWithSize() {
let responseXML =
'' +
"" +
" " +
' ' +
" " +
"";
let myRequest = new mockRequest(200, responseXML);
let installManager = new GMPInstallManager();
let res = await ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
Assert.equal(res.addons.length, 1);
let gmpAddon = res.addons[0];
Assert.equal(gmpAddon.id, "openh264-plugin-no-at-symbol");
Assert.equal(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
Assert.equal(gmpAddon.hashFunction, "sha256");
Assert.equal(
gmpAddon.hashValue,
"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"
);
Assert.equal(gmpAddon.size, 42);
Assert.equal(gmpAddon.version, "1.1");
Assert.ok(gmpAddon.isValid);
Assert.ok(!gmpAddon.isInstalled);
installManager.uninit();
});
/**
* Tests that checking for multiple addons work correctly.
* Also tests that invalid addons work correctly.
*/
add_task(
async function test_checkForAddons_multipleAddonNoUpdatesSomeInvalid() {
let responseXML =
'' +
"" +
" " +
// valid openh264
' ' +
// valid not openh264
' ' +
// noid
' ' +
// no URL
' ' +
// no hash function
' ' +
// no hash function
' ' +
// not version
' ' +
" " +
"";
let myRequest = new mockRequest(200, responseXML);
let installManager = new GMPInstallManager();
let res = await ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
Assert.equal(res.addons.length, 7);
let gmpAddon = res.addons[0];
Assert.equal(gmpAddon.id, "gmp-gmpopenh264");
Assert.equal(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
Assert.equal(gmpAddon.hashFunction, "sha256");
Assert.equal(
gmpAddon.hashValue,
"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"
);
Assert.equal(gmpAddon.version, "1.1");
Assert.ok(gmpAddon.isValid);
Assert.ok(!gmpAddon.isInstalled);
gmpAddon = res.addons[1];
Assert.equal(gmpAddon.id, "NOT-gmp-gmpopenh264");
Assert.equal(
gmpAddon.URL,
"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip"
);
Assert.equal(gmpAddon.hashFunction, "sha512");
Assert.equal(
gmpAddon.hashValue,
"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"
);
Assert.equal(gmpAddon.version, "9.1");
Assert.ok(gmpAddon.isValid);
Assert.ok(!gmpAddon.isInstalled);
for (let i = 2; i < res.addons.length; i++) {
Assert.ok(!res.addons[i].isValid);
Assert.ok(!res.addons[i].isInstalled);
}
installManager.uninit();
}
);
/**
* Tests that checking for addons when there are also updates available
* works as expected.
*/
add_task(async function test_checkForAddons_updatesWithAddons() {
let responseXML =
'' +
" " +
' ' +
' ' +
" " +
" " +
' ' +
" " +
"";
let myRequest = new mockRequest(200, responseXML);
let installManager = new GMPInstallManager();
let res = await ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
Assert.equal(res.addons.length, 1);
let gmpAddon = res.addons[0];
Assert.equal(gmpAddon.id, "gmp-gmpopenh264");
Assert.equal(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
Assert.equal(gmpAddon.hashFunction, "sha256");
Assert.equal(
gmpAddon.hashValue,
"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"
);
Assert.equal(gmpAddon.version, "1.1");
Assert.ok(gmpAddon.isValid);
Assert.ok(!gmpAddon.isInstalled);
installManager.uninit();
});
/**
* Tests that checkForAddons() works as expected when content signature
* checking is enabled and the signature check passes.
*/
add_task(async function test_checkForAddons_contentSignatureSuccess() {
const previousUrlOverride = setupContentSigTestPrefs();
const xmlFetchResultHistogram = resetGmpTelemetryAndGetHistogram();
const testServerInfo = getTestServerForContentSignatureTests();
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, testServerInfo.validUpdateUri);
let installManager = new GMPInstallManager();
try {
let res = await installManager.checkForAddons();
Assert.ok(true, "checkForAddons should succeed");
// Smoke test the results are as expected.
// If the checkForAddons fails we'll get a fallback config,
// so we'll get incorrect addons and these asserts will fail.
Assert.equal(res.usedFallback, false);
Assert.equal(res.addons.length, 5);
Assert.equal(res.addons[0].id, "test1");
Assert.equal(res.addons[1].id, "test2");
Assert.equal(res.addons[2].id, "test3");
Assert.equal(res.addons[3].id, "test4");
Assert.equal(res.addons[4].id, undefined);
} catch (e) {
Assert.ok(false, "checkForAddons should succeed");
}
// # Ok content sig fetches should be 1, all others should be 0.
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 2, 1);
// Test that glean has 1 success for content sig and no other metrics.
const expectedGleanValues = {
cert_pin_success: 0,
cert_pin_net_request_error: 0,
cert_pin_net_timeout: 0,
cert_pin_abort: 0,
cert_pin_missing_data: 0,
cert_pin_failed: 0,
cert_pin_invalid: 0,
cert_pin_unknown_error: 0,
content_sig_success: 1,
content_sig_net_request_error: 0,
content_sig_net_timeout: 0,
content_sig_abort: 0,
content_sig_missing_data: 0,
content_sig_failed: 0,
content_sig_invalid: 0,
content_sig_unknown_error: 0,
};
checkGleanMetricCounts(expectedGleanValues);
revertContentSigTestPrefs(previousUrlOverride);
});
/**
* Tests that checkForAddons() works as expected when content signature
* checking is enabled and the check fails.
*/
add_task(async function test_checkForAddons_contentSignatureFailure() {
const previousUrlOverride = setupContentSigTestPrefs();
const xmlFetchResultHistogram = resetGmpTelemetryAndGetHistogram();
const testServerInfo = getTestServerForContentSignatureTests();
Preferences.set(
GMPPrefs.KEY_URL_OVERRIDE,
testServerInfo.missingContentSigUri
);
let installManager = new GMPInstallManager();
try {
let res = await installManager.checkForAddons();
Assert.ok(true, "checkForAddons should succeed");
// Smoke test the results are as expected.
// Check addons will succeed above, but it will have fallen back to local
// config. So the results will not be those from the HTTP server.
Assert.equal(res.usedFallback, true);
// Some platforms don't have fallback config for all GMPs, but we should
// always get at least 1.
Assert.greaterOrEqual(res.addons.length, 1);
if (res.addons.length == 1) {
Assert.equal(res.addons[0].id, "gmp-widevinecdm");
} else {
Assert.equal(res.addons[0].id, "gmp-gmpopenh264");
Assert.equal(res.addons[1].id, "gmp-widevinecdm");
}
} catch (e) {
Assert.ok(false, "checkForAddons should succeed");
}
// # Failed content sig fetches should be 1, all others should be 0.
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 3, 1);
// Glean values should reflect the content sig algo failed.
Assert.equal(
Glean.gmp.updateXmlFetchResult.content_sig_missing_data.testGetValue(),
1
);
// Check further failure cases. We've already smoke tested the results above,
// don't bother doing so again.
// Fail due to bad content signature.
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, testServerInfo.badContentSigUri);
await installManager.checkForAddons();
// Should have another failure...
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 3, 2);
// ... and it should be due to the signature being bad, which causes
// verification to fail.
Assert.equal(
Glean.gmp.updateXmlFetchResult.content_sig_failed.testGetValue(),
1
);
// Fail due to bad invalid content signature.
Preferences.set(
GMPPrefs.KEY_URL_OVERRIDE,
testServerInfo.invalidContentSigUri
);
await installManager.checkForAddons();
// Should have another failure...
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 3, 3);
// ... and it should be due to the signature being invalid.
Assert.equal(
Glean.gmp.updateXmlFetchResult.content_sig_invalid.testGetValue(),
1
);
// Fail by pointing to a bad URL.
Preferences.set(
GMPPrefs.KEY_URL_OVERRIDE,
"https://this.url.doesnt/go/anywhere"
);
await installManager.checkForAddons();
// Should have another failure...
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 3, 4);
// ... and it should be due to a bad request.
Assert.equal(
Glean.gmp.updateXmlFetchResult.content_sig_net_request_error.testGetValue(),
1
);
// Fail via timeout. This case uses our mock machinery in order to abort the
// request, as I (:bryce) couldn't figure out a nice way to do it with the
// HttpServer.
let overriddenServiceRequest = new mockRequest(200, "", {
dropRequest: true,
timeout: true,
});
await ProductAddonCheckerTestUtils.overrideServiceRequest(
overriddenServiceRequest,
() => installManager.checkForAddons()
);
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 3, 5);
// ... and it should be due to a timeout.
Assert.equal(
Glean.gmp.updateXmlFetchResult.content_sig_net_timeout.testGetValue(),
1
);
// Fail via abort. This case uses our mock machinery in order to abort the
// request, as I (:bryce) couldn't figure out a nice way to do it with the
// HttpServer.
overriddenServiceRequest = new mockRequest(200, "", {
dropRequest: true,
});
let promise = ProductAddonCheckerTestUtils.overrideServiceRequest(
overriddenServiceRequest,
() => installManager.checkForAddons()
);
setTimeout(() => {
overriddenServiceRequest.abort();
}, 100);
await promise;
// Should have another failure...
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 3, 6);
// ... and it should be due to an abort.
Assert.equal(
Glean.gmp.updateXmlFetchResult.content_sig_abort.testGetValue(),
1
);
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, testServerInfo.badXmlUri);
await installManager.checkForAddons();
// Should have another failure...
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 3, 7);
// ... and it should be due to the xml response being unrecognized.
Assert.equal(
Glean.gmp.updateXmlFetchResult.content_sig_xml_parse_error.testGetValue(),
1
);
// Fail via bad request during the x5u look up.
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, testServerInfo.badX5uRequestUri);
await installManager.checkForAddons();
// Should have another failure...
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 3, 8);
// ... and it should be due to a bad request.
Assert.equal(
Glean.gmp.updateXmlFetchResult.content_sig_net_request_error.testGetValue(),
2
);
// Fail by timing out during the x5u look up.
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, testServerInfo.x5uTimeoutUri);
// We need to expose this promise back to the server so it can handle
// setting up a mock request in the middle of checking for addons.
testServerInfo.promiseHolder.installPromise = installManager.checkForAddons();
await testServerInfo.promiseHolder.installPromise;
// We wait sequentially because serverPromise won't be set until the server
// receives our request.
await testServerInfo.promiseHolder.serverPromise;
delete testServerInfo.promiseHolder.installPromise;
delete testServerInfo.promiseHolder.serverPromise;
// Should have another failure...
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 3, 9);
// ... and it should be due to a timeout.
Assert.equal(
Glean.gmp.updateXmlFetchResult.content_sig_net_timeout.testGetValue(),
2
);
// Fail by aborting during the x5u look up.
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, testServerInfo.x5uAbortUri);
// We need to expose this promise back to the server so it can handle
// setting up a mock request in the middle of checking for addons.
testServerInfo.promiseHolder.installPromise = installManager.checkForAddons();
await testServerInfo.promiseHolder.installPromise;
// We wait sequentially because serverPromise won't be set until the server
// receives our request.
await testServerInfo.promiseHolder.serverPromise;
delete testServerInfo.promiseHolder.installPromise;
delete testServerInfo.promiseHolder.serverPromise;
// Should have another failure...
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 3, 10);
// ... and it should be due to an abort.
Assert.equal(
Glean.gmp.updateXmlFetchResult.content_sig_abort.testGetValue(),
2
);
// Check all glean metrics have expected values at test end.
const expectedGleanValues = {
cert_pin_success: 0,
cert_pin_net_request_error: 0,
cert_pin_net_timeout: 0,
cert_pin_abort: 0,
cert_pin_missing_data: 0,
cert_pin_failed: 0,
cert_pin_invalid: 0,
cert_pin_xml_parse_error: 0,
cert_pin_unknown_error: 0,
content_sig_success: 0,
content_sig_net_request_error: 2,
content_sig_net_timeout: 2,
content_sig_abort: 2,
content_sig_missing_data: 1,
content_sig_failed: 1,
content_sig_invalid: 1,
content_sig_xml_parse_error: 1,
content_sig_unknown_error: 0,
};
checkGleanMetricCounts(expectedGleanValues);
revertContentSigTestPrefs(previousUrlOverride);
});
/**
* Tests that the signature verification URL is as expected.
*/
add_task(async function test_checkForAddons_get_verifier_url() {
const previousUrlOverride = setupContentSigTestPrefs();
let installManager = new GMPInstallManager();
// checkForAddons() calls _getContentSignatureRootForURL() with the return
// value of _getURL(), which is effectively KEY_URL_OVERRIDE or KEY_URL
// followed by some normalization.
const rootForUrl = async () => {
const url = await installManager._getURL();
return installManager._getContentSignatureRootForURL(url);
};
Assert.equal(
await rootForUrl(),
Ci.nsIX509CertDB.AppXPCShellRoot,
"XPCShell root used by default in xpcshell test"
);
const defaultPrefs = Services.prefs.getDefaultBranch("");
const defaultUrl = defaultPrefs.getStringPref(GMPPrefs.KEY_URL);
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, defaultUrl);
Assert.equal(
await rootForUrl(),
Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot,
"Production cert should be used for the default Balrog URL: " + defaultUrl
);
// The current Balrog endpoint is at aus5.mozilla.org. Confirm that the prod
// cert is used even if we bump the version (e.g. aus6):
const potentialProdUrl = "https://aus1337.mozilla.org/potential/prod/URL";
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, potentialProdUrl);
Assert.equal(
await rootForUrl(),
Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot,
"Production cert should be used for: " + potentialProdUrl
);
// Stage URL documented at https://mozilla-balrog.readthedocs.io/en/latest/infrastructure.html
const stageUrl = "https://stage.balrog.nonprod.cloudops.mozgcp.net/etc.";
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, stageUrl);
Assert.equal(
await rootForUrl(),
Ci.nsIContentSignatureVerifier.ContentSignatureStageRoot,
"Stage cert should be used with the stage URL: " + stageUrl
);
installManager.uninit();
revertContentSigTestPrefs(previousUrlOverride);
});
/**
* Tests that checkForAddons() works as expected when certificate pinning
* checking is enabled. We plan to move away from cert pinning in favor of
* content signature checks, but part of doing this is comparing the telemetry
* from both methods. We want test coverage to ensure the telemetry is being
* gathered for cert pinning failures correctly before we remove the code.
*/
add_task(async function test_checkForAddons_telemetry_certPinning() {
// Grab state so we can restore it at the end of the test.
const previousUrlOverride = Preferences.get(GMPPrefs.KEY_URL_OVERRIDE, "");
let xmlFetchResultHistogram = resetGmpTelemetryAndGetHistogram();
// Re-use the content-sig test server config. We're not going to need any of
// the content signature specific config but this gives us a server to get
// update.xml files from, and also tests that cert pinning doesn't break even
// if we're getting content sig headers sent.
const testServerInfo = getTestServerForContentSignatureTests();
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, testServerInfo.validUpdateUri);
let installManager = new GMPInstallManager();
try {
// This should work because once we override the GMP URL, no cert pin
// checks are actually done. I.e. we don't need to do any pinning in
// the test, just use a valid URL.
await installManager.checkForAddons();
Assert.ok(true, "checkForAddons should succeed");
} catch (e) {
Assert.ok(false, "checkForAddons should succeed");
}
// # Ok cert pin fetches should be 1, all others should be 0.
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 0, 1);
// Glean values should reflect the same.
Assert.equal(
Glean.gmp.updateXmlFetchResult.cert_pin_success.testGetValue(),
1
);
// Reset the histogram because we want to check a different index.
xmlFetchResultHistogram = TelemetryTestUtils.getAndClearHistogram(
"MEDIA_GMP_UPDATE_XML_FETCH_RESULT"
);
// Fail by pointing to a bad URL.
Preferences.set(
GMPPrefs.KEY_URL_OVERRIDE,
"https://this.url.doesnt/go/anywhere"
);
await installManager.checkForAddons();
// Should have another failure...
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 1, 1);
// ... and it should be due to a bad request.
Assert.equal(
Glean.gmp.updateXmlFetchResult.cert_pin_net_request_error.testGetValue(),
1
);
// Fail via timeout. This case uses our mock machinery in order to abort the
// request, as I (:bryce) couldn't figure out a nice way to do it with the
// HttpServer.
let overriddenServiceRequest = new mockRequest(200, "", {
dropRequest: true,
timeout: true,
});
await ProductAddonCheckerTestUtils.overrideServiceRequest(
overriddenServiceRequest,
() => installManager.checkForAddons()
);
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 1, 2);
// ... and it should be due to a timeout.
Assert.equal(
Glean.gmp.updateXmlFetchResult.cert_pin_net_timeout.testGetValue(),
1
);
// Fail via abort. This case uses our mock machinery in order to abort the
// request, as I (:bryce) couldn't figure out a nice way to do it with the
// HttpServer.
overriddenServiceRequest = new mockRequest(200, "", {
dropRequest: true,
});
let promise = ProductAddonCheckerTestUtils.overrideServiceRequest(
overriddenServiceRequest,
() => installManager.checkForAddons()
);
setTimeout(() => {
overriddenServiceRequest.abort();
}, 100);
await promise;
// Should have another failure...
TelemetryTestUtils.assertHistogram(xmlFetchResultHistogram, 1, 3);
// ... and it should be due to an abort.
Assert.equal(Glean.gmp.updateXmlFetchResult.cert_pin_abort.testGetValue(), 1);
// Check all glean metrics have expected values at test end.
const expectedGleanValues = {
cert_pin_success: 1,
cert_pin_net_request_error: 1,
cert_pin_net_timeout: 1,
cert_pin_abort: 1,
cert_pin_missing_data: 0,
cert_pin_failed: 0,
cert_pin_invalid: 0,
cert_pin_unknown_error: 0,
content_sig_success: 0,
content_sig_net_request_error: 0,
content_sig_net_timeout: 0,
content_sig_abort: 0,
content_sig_missing_data: 0,
content_sig_failed: 0,
content_sig_invalid: 0,
content_sig_unknown_error: 0,
};
checkGleanMetricCounts(expectedGleanValues);
// Restore the URL override now that we're done.
if (previousUrlOverride) {
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, previousUrlOverride);
} else {
Preferences.reset(GMPPrefs.KEY_URL_OVERRIDE);
}
});
/**
* Tests that installing found addons works as expected
*/
async function test_checkForAddons_installAddon(
id,
includeSize,
wantInstallReject
) {
info(
"Running installAddon for id: " +
id +
", includeSize: " +
includeSize +
" and wantInstallReject: " +
wantInstallReject
);
let httpServer = new HttpServer();
let dir = FileUtils.getDir("TmpD", [], true);
httpServer.registerDirectory("/", dir);
httpServer.start(-1);
let testserverPort = httpServer.identity.primaryPort;
let zipFileName = "test_" + id + "_GMP.zip";
let zipURL = URL_HOST + ":" + testserverPort + "/" + zipFileName;
info("zipURL: " + zipURL);
let data = "e~=0.5772156649";
let zipFile = createNewZipFile(zipFileName, data);
let hashFunc = "sha256";
let expectedDigest = await IOUtils.computeHexDigest(zipFile.path, hashFunc);
let fileSize = zipFile.fileSize;
if (wantInstallReject) {
fileSize = 1;
}
let responseXML =
'' +
"" +
" " +
' ' +
" " +
"";
let myRequest = new mockRequest(200, responseXML);
let installManager = new GMPInstallManager();
let res = await ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
Assert.equal(res.addons.length, 1);
let gmpAddon = res.addons[0];
Assert.ok(!gmpAddon.isInstalled);
try {
let extractedPaths = await installManager.installAddon(gmpAddon);
if (wantInstallReject) {
Assert.ok(false); // installAddon() should have thrown.
}
Assert.equal(extractedPaths.length, 1);
let extractedPath = extractedPaths[0];
info("Extracted path: " + extractedPath);
let extractedFile = Cc["@mozilla.org/file/local;1"].createInstance(
Ci.nsIFile
);
extractedFile.initWithPath(extractedPath);
Assert.ok(extractedFile.exists());
let readData = readStringFromFile(extractedFile);
Assert.equal(readData, data);
// Make sure the prefs are set correctly
Assert.ok(
!!GMPPrefs.getInt(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", gmpAddon.id)
);
Assert.equal(
GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_HASHVALUE, "", gmpAddon.id),
expectedDigest
);
Assert.equal(
GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_VERSION, "", gmpAddon.id),
"1.1"
);
Assert.equal(
GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_ABI, "", gmpAddon.id),
UpdateUtils.ABI
);
// Make sure it reports as being installed
Assert.ok(gmpAddon.isInstalled);
// Cleanup
extractedFile.parent.remove(true);
zipFile.remove(false);
httpServer.stop(function () {});
installManager.uninit();
} catch (ex) {
zipFile.remove(false);
if (!wantInstallReject) {
do_throw("install update should not reject " + ex.message);
}
}
}
add_task(test_checkForAddons_installAddon.bind(null, "1", true, false));
add_task(test_checkForAddons_installAddon.bind(null, "2", false, false));
add_task(test_checkForAddons_installAddon.bind(null, "3", true, true));
/**
* Tests simpleCheckAndInstall when autoupdate is disabled for a GMP
*/
add_task(async function test_simpleCheckAndInstall_autoUpdateDisabled() {
GMPPrefs.setBool(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, OPEN_H264_ID);
let responseXML =
'' +
"" +
" " +
// valid openh264
' ' +
" " +
"";
let myRequest = new mockRequest(200, responseXML);
let installManager = new GMPInstallManager();
let result = await ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.simpleCheckAndInstall()
);
Assert.equal(result.status, "nothing-new-to-install");
Preferences.reset(GMPPrefs.KEY_UPDATE_LAST_CHECK);
GMPPrefs.setBool(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, OPEN_H264_ID);
});
/**
* Tests simpleCheckAndInstall nothing to install
*/
add_task(async function test_simpleCheckAndInstall_nothingToInstall() {
let responseXML = '';
let myRequest = new mockRequest(200, responseXML);
let installManager = new GMPInstallManager();
let result = await ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.simpleCheckAndInstall()
);
Assert.equal(result.status, "nothing-new-to-install");
});
/**
* Tests simpleCheckAndInstall too frequent
*/
add_task(async function test_simpleCheckAndInstall_tooFrequent() {
let responseXML = '';
let myRequest = new mockRequest(200, responseXML);
let installManager = new GMPInstallManager();
let result = await ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.simpleCheckAndInstall()
);
Assert.equal(result.status, "too-frequent-no-check");
});
/**
* Tests that installing addons when there is no server works as expected
*/
add_test(function test_installAddon_noServer() {
let zipFileName = "test_GMP.zip";
let zipURL = URL_HOST + ":0/" + zipFileName;
let responseXML =
'' +
"" +
" " +
' ' +
" " +
"";
let myRequest = new mockRequest(200, responseXML);
let installManager = new GMPInstallManager();
let checkPromise = ProductAddonCheckerTestUtils.overrideServiceRequest(
myRequest,
() => installManager.checkForAddons()
);
checkPromise.then(
res => {
Assert.equal(res.addons.length, 1);
let gmpAddon = res.addons[0];
GMPInstallManager.overrideLeaveDownloadedZip = true;
let installPromise = installManager.installAddon(gmpAddon);
installPromise.then(
extractedPaths => {
do_throw("No server for install should reject");
},
err => {
Assert.ok(!!err);
installManager.uninit();
run_next_test();
}
);
},
() => {
do_throw("check should not reject for install no server");
}
);
});
/***
* Tests GMPExtractor (an internal component of GMPInstallManager) to ensure
* it handles paths with certain characters.
*
* On Mac, test that the com.apple.quarantine extended attribute is removed
* from installed plugin files.
*/
add_task(async function test_GMPExtractor_paths() {
registerCleanupFunction(async function () {
// Must stop holding on to the zip file using the JAR cache:
let zipFile = new FileUtils.File(
PathUtils.join(tempDir.path, "dummy_gmp.zip")
);
Services.obs.notifyObservers(zipFile, "flush-cache-entry");
await IOUtils.remove(extractedDir, { recursive: true });
await IOUtils.remove(tempDir.path, { recursive: true });
});
// Create a dir with the following in the name
// - # -- this is used to delimit URI fragments and tests that
// we escape any internal URIs appropriately.
// - 猫 -- ensure we handle non-ascii characters appropriately.
const srcPath = PathUtils.join(
Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
"zips",
"dummy_gmp.zip"
);
let tempDirName = "TmpDir#猫";
let tempDir = FileUtils.getDir("TmpD", [tempDirName], true);
let zipPath = PathUtils.join(tempDir.path, "dummy_gmp.zip");
await IOUtils.copy(srcPath, zipPath);
// The path inside the profile dir we'll extract to. Make sure we handle
// the characters there too.
let relativeExtractPath = "extracted#猫";
let extractor = new GMPExtractor(zipPath, [relativeExtractPath]);
let extractedPaths = await extractor.install();
// extractedPaths should contain the files extracted. In this case we
// should have a single file extracted to our profile dir -- the zip
// contains two files, but one should be skipped by the extraction logic.
Assert.equal(extractedPaths.length, 1, "One file should be extracted");
Assert.ok(
extractedPaths[0].includes("dummy_file.txt"),
"dummy_file.txt should be on extracted path"
);
Assert.ok(
!extractedPaths[0].includes("verified_contents.json"),
"verified_contents.json should not be on extracted path"
);
let extractedDir = PathUtils.join(PathUtils.profileDir, relativeExtractPath);
Assert.ok(
await IOUtils.exists(extractedDir),
"Extraction should have created a directory"
);
let extractedFile = PathUtils.join(
PathUtils.profileDir,
relativeExtractPath,
"dummy_file.txt"
);
Assert.ok(
await IOUtils.exists(extractedFile),
"Extraction should have created dummy_file.txt"
);
if (AppConstants.platform == "macosx") {
await Assert.rejects(
IOUtils.getMacXAttr(extractedFile, "com.apple.quarantine"),
/NotFoundError: The file `.+' does not have an extended attribute `com.apple.quarantine'/,
"The 'com.apple.quarantine' attribute should not be present"
);
}
let unextractedFile = PathUtils.join(
PathUtils.profileDir,
relativeExtractPath,
"verified_contents.json"
);
Assert.ok(
!(await IOUtils.exists(unextractedFile)),
"Extraction should not have created verified_contents.json"
);
});
/**
* Returns the read stream into a string
*/
function readStringFromInputStream(inputStream) {
let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
Ci.nsIScriptableInputStream
);
sis.init(inputStream);
let text = sis.read(sis.available());
sis.close();
return text;
}
/**
* Reads a string of text from a file.
* This function only works with ASCII text.
*/
function readStringFromFile(file) {
if (!file.exists()) {
info("readStringFromFile - file doesn't exist: " + file.path);
return null;
}
let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
Ci.nsIFileInputStream
);
fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
return readStringFromInputStream(fis);
}
/**
* Constructs a mock xhr/ServiceRequest which is used for testing different
* aspects of responses.
*/
function mockRequest(inputStatus, inputResponse, options) {
this.inputStatus = inputStatus;
this.inputResponse = inputResponse;
this.status = 0;
this.responseXML = null;
this._aborted = false;
this._onabort = null;
this._onprogress = null;
this._onerror = null;
this._onload = null;
this._onloadend = null;
this._ontimeout = null;
this._url = null;
this._method = null;
this._timeout = 0;
this._notified = false;
this._options = options || {};
}
mockRequest.prototype = {
overrideMimeType(aMimetype) {},
setRequestHeader(aHeader, aValue) {},
status: null,
channel: { set notificationCallbacks(aVal) {} },
open(aMethod, aUrl) {
this.channel.originalURI = Services.io.newURI(aUrl);
this._method = aMethod;
this._url = aUrl;
},
abort() {
this._dropRequest = true;
this._notify(["abort", "loadend"]);
},
responseXML: null,
responseText: null,
send(aBody) {
executeSoon(() => {
try {
if (this._options.dropRequest) {
if (this._timeout > 0 && this._options.timeout) {
this._notify(["timeout", "loadend"]);
}
return;
}
this.status = this.inputStatus;
this.responseText = this.inputResponse;
try {
let parser = new DOMParser();
this.responseXML = parser.parseFromString(
this.inputResponse,
"application/xml"
);
} catch (e) {
this.responseXML = null;
}
if (this.inputStatus === 200) {
this._notify(["load", "loadend"]);
} else {
this._notify(["error", "loadend"]);
}
} catch (ex) {
do_throw(ex);
}
});
},
set onabort(aValue) {
this._onabort = aValue;
},
get onabort() {
return this._onabort;
},
set onprogress(aValue) {
this._onprogress = aValue;
},
get onprogress() {
return this._onprogress;
},
set onerror(aValue) {
this._onerror = aValue;
},
get onerror() {
return this._onerror;
},
set onload(aValue) {
this._onload = aValue;
},
get onload() {
return this._onload;
},
set onloadend(aValue) {
this._onloadend = aValue;
},
get onloadend() {
return this._onloadend;
},
set ontimeout(aValue) {
this._ontimeout = aValue;
},
get ontimeout() {
return this._ontimeout;
},
set timeout(aValue) {
this._timeout = aValue;
},
_notify(events) {
if (this._notified) {
return;
}
this._notified = true;
for (let item of events) {
let k = "on" + item;
if (this[k]) {
info("Notifying " + item);
let e = {
target: this,
type: item,
};
this[k](e);
} else {
info("Notifying " + item + ", but there are no listeners");
}
}
},
addEventListener(aEvent, aValue, aCapturing) {
// eslint-disable-next-line no-eval
eval("this._on" + aEvent + " = aValue");
},
get wrappedJSObject() {
return this;
},
};
/**
* Creates a new zip file containing a file with the specified data
* @param zipName The name of the zip file
* @param data The data to go inside the zip for the filename entry1.info
*/
function createNewZipFile(zipName, data) {
// Create a zip file which will be used for extracting
let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
Ci.nsIStringInputStream
);
stream.setData(data, data.length);
let zipWriter = Cc["@mozilla.org/zipwriter;1"].createInstance(
Ci.nsIZipWriter
);
let zipFile = FileUtils.getFile("TmpD", [zipName]);
if (zipFile.exists()) {
zipFile.remove(false);
}
// From prio.h
const PR_RDWR = 0x04;
const PR_CREATE_FILE = 0x08;
const PR_TRUNCATE = 0x20;
zipWriter.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
zipWriter.addEntryStream(
"entry1.info",
Date.now() * PR_USEC_PER_MSEC,
Ci.nsIZipWriter.COMPRESSION_BEST,
stream,
false
);
zipWriter.close();
stream.close();
info("zip file created on disk at: " + zipFile.path);
return zipFile;
}
/***
* Set up pref(s) as appropriate for content sig tests. Return the value of our
* current GMP url override so it can be restored at test teardown.
*/
function setupContentSigTestPrefs() {
Preferences.set("media.gmp-manager.checkContentSignature", true);
// Return the URL override so tests can restore it to its previous value
// once they're done.
return Preferences.get(GMPPrefs.KEY_URL_OVERRIDE, "");
}
/***
* Revert prefs used for content signature tests.
*
* @param previousUrlOverride - The GMP URL override value prior to test being
* run. The function will revert the URL back to this, or reset the URL if no
* value is passed.
*/
function revertContentSigTestPrefs(previousUrlOverride) {
if (previousUrlOverride) {
Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, previousUrlOverride);
} else {
Preferences.reset(GMPPrefs.KEY_URL_OVERRIDE);
}
Preferences.set("media.gmp-manager.checkContentSignature", false);
}
/***
* Reset telemetry data related to gmp updates, and get the histogram
* associated with MEDIA_GMP_UPDATE_XML_FETCH_RESULT.
*
* @returns The freshly cleared MEDIA_GMP_UPDATE_XML_FETCH_RESULT histogram.
*/
function resetGmpTelemetryAndGetHistogram() {
Services.fog.testResetFOG();
return TelemetryTestUtils.getAndClearHistogram(
"MEDIA_GMP_UPDATE_XML_FETCH_RESULT"
);
}
/***
* A helper to check that glean metrics have expected counts.
* @param expectedGleanValues a object that has properties with names set to glean metrics to be checked
* and the values are the expected count. Eg { cert_pin_success: 1 }.
*/
function checkGleanMetricCounts(expectedGleanValues) {
for (const property in expectedGleanValues) {
if (Glean.gmp.updateXmlFetchResult[property].testGetValue()) {
Assert.equal(
Glean.gmp.updateXmlFetchResult[property].testGetValue(),
expectedGleanValues[property],
`${property} should have been recorded ${expectedGleanValues[property]} times`
);
} else {
Assert.equal(
expectedGleanValues[property],
0,
"testGetValue() being undefined should mean we expect a metric to not have been gathered"
);
}
}
}
/***
* Sets up a `HttpServer` for use in content singature checking tests. This
* server will expose different endpoints that can be used to simulate different
* pass and failure scenarios when fetching an update.xml file.
*
* @returns An object that has the following properties
* - testServer - the HttpServer itself.
* - promiseHolder - an object used to hold promises as properties. More complex test cases need this to sync different steps.
* - validUpdateUri - a URI that should return a valid update xml + content sig.
* - missingContentSigUri - a URI that returns a valid update xml, but misses the content sig header.
* - badContentSigUri - a URI that returns a valid update xml, but provides data that is not a content sig in the header.
* - invalidContentSigUri - a URI that returns a valid update xml, but provides an incorrect content sig.
* - badXmlUri - a URI that returns an invalid update xml, but provides a correct content sig.
* - x5uAbortUri - a URI that returns a valid update xml, but timesout the x5u request. Requires the caller to set
* `promiseHolder.installPromise` to the `checkForAddons()` promise` before hitting the endpoint. The server will set
* `promiseHolder.serverPromise` once it has started servicing the initial update request, and the caller should
* await that promise to ensure the server has restored state.
* - x5uAbortUri - a URI that returns a valid update xml, but aborts the x5u request. Requires the caller to set
* `promiseHolder.installPromise` to the `checkForAddons()` promise` before hitting the endpoint. The server will set
* `promiseHolder.serverPromise` once it has started servicing the initial update request, and the caller should
* await that promise to ensure the server has restored state.
*/
function getTestServerForContentSignatureTests() {
const testServer = new HttpServer();
// Start the server so we can grab the identity. We need to know this so the
// server can reference itself in the handlers that will be set up.
testServer.start();
const baseUri =
testServer.identity.primaryScheme +
"://" +
testServer.identity.primaryHost +
":" +
testServer.identity.primaryPort;
// The promise holder has no properties by default. Different endpoints and
// tests will set its properties as needed.
let promiseHolder = {};
const goodXml = readStringFromFile(do_get_file("good.xml"));
// This sig is generated using the following command at mozilla-central root
// `cat toolkit/mozapps/extensions/test/xpcshell/data/productaddons/good.xml | ./mach python security/manager/ssl/tests/unit/test_content_signing/pysign.py`
// If test certificates are regenerated, this signature must also be.
const goodXmlContentSignature =
"7QYnPqFoOlS02BpDdIRIljzmPr6BFwPs1z1y8KJUBlnU7EVG6FbnXmVVt5Op9wDzgvhXX7th8qFJvpPOZs_B_tHRDNJ8SK0HN95BAN15z3ZW2r95SSHmU-fP2JgoNOR3";
// Setup endpoint to handle x5u lookups correctly.
const validX5uPath = "/valid_x5u";
const validCertChain = [
readStringFromFile(do_get_file("content_signing_aus_ee.pem")),
readStringFromFile(do_get_file("content_signing_int.pem")),
];
testServer.registerPathHandler(validX5uPath, (req, res) => {
res.write(validCertChain.join("\n"));
});
const validX5uUrl = baseUri + validX5uPath;
// Handler for path that serves valid xml with valid signature.
const validUpdatePath = "/valid_update.xml";
testServer.registerPathHandler(validUpdatePath, (req, res) => {
const validContentSignatureHeader = `x5u=${validX5uUrl}; p384ecdsa=${goodXmlContentSignature}`;
res.setHeader("content-signature", validContentSignatureHeader);
res.write(goodXml);
});
const missingContentSigPath = "/update_missing_content_sig.xml";
testServer.registerPathHandler(missingContentSigPath, (req, res) => {
// Content signature header omitted.
res.write(goodXml);
});
const badContentSigPath = "/update_bad_content_sig.xml";
testServer.registerPathHandler(badContentSigPath, (req, res) => {
res.setHeader(
"content-signature",
`x5u=${validX5uUrl}; p384ecdsa=I'm a bad content signature`
);
res.write(goodXml);
});
// Make an invalid signature by change first char.
const invalidXmlContentSignature = "Z" + goodXmlContentSignature.slice(1);
const invalidContentSigPath = "/update_invalid_content_sig.xml";
testServer.registerPathHandler(invalidContentSigPath, (req, res) => {
res.setHeader(
"content-signature",
`x5u=${validX5uUrl}; p384ecdsa=${invalidXmlContentSignature}`
);
res.write(goodXml);
});
const badXml = readStringFromFile(do_get_file("bad.xml"));
// This sig is generated using the following command at mozilla-central root
// `cat toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.xml | ./mach python security/manager/ssl/tests/unit/test_content_signing/pysign.py`
// If test certificates are regenerated, this signature must also be.
const badXmlContentSignature =
"7QYnPqFoOlS02BpDdIRIljzmPr6BFwPs1z1y8KJUBlnU7EVG6FbnXmVVt5Op9wDz8YoQ_b-3i9rWpj40s8QZsMgo2eImx83LW9JE0d0z6sSAnwRb4lHFPpJXC_hv7wi7";
const badXmlPath = "/bad.xml";
testServer.registerPathHandler(badXmlPath, (req, res) => {
const validContentSignatureHeader = `x5u=${validX5uUrl}; p384ecdsa=${badXmlContentSignature}`;
res.setHeader("content-signature", validContentSignatureHeader);
res.write(badXml);
});
const badX5uRequestPath = "/bad_x5u_request.xml";
testServer.registerPathHandler(badX5uRequestPath, (req, res) => {
const badX5uUrlHeader = `x5u=https://this.is.a/bad/url; p384ecdsa=${goodXmlContentSignature}`;
res.setHeader("content-signature", badX5uUrlHeader);
res.write(badXml);
});
const x5uTimeoutPath = "/x5u_timeout.xml";
testServer.registerPathHandler(x5uTimeoutPath, (req, res) => {
const validContentSignatureHeader = `x5u=${validX5uUrl}; p384ecdsa=${goodXmlContentSignature}`;
// Write the correct header and xml, but setup the next request to timeout.
// This should cause the request for the x5u URL to fail via timeout.
let overriddenServiceRequest = new mockRequest(200, "", {
dropRequest: true,
timeout: true,
});
// We expose this promise so that tests can wait until the server has
// reverted the overridden request (to avoid double overrides).
promiseHolder.serverPromise =
ProductAddonCheckerTestUtils.overrideServiceRequest(
overriddenServiceRequest,
() => {
res.setHeader("content-signature", validContentSignatureHeader);
res.write(goodXml);
return promiseHolder.installPromise;
}
);
});
const x5uAbortPath = "/x5u_abort.xml";
testServer.registerPathHandler(x5uAbortPath, (req, res) => {
const validContentSignatureHeader = `x5u=${validX5uUrl}; p384ecdsa=${goodXmlContentSignature}`;
// Write the correct header and xml, but setup the next request to fail.
// This should cause the request for the x5u URL to fail via abort.
let overriddenServiceRequest = new mockRequest(200, "", {
dropRequest: true,
});
// We expose this promise so that tests can wait until the server has
// reverted the overridden request (to avoid double overrides).
promiseHolder.serverPromise =
ProductAddonCheckerTestUtils.overrideServiceRequest(
overriddenServiceRequest,
() => {
res.setHeader("content-signature", validContentSignatureHeader);
res.write(goodXml);
return promiseHolder.installPromise;
}
);
setTimeout(() => {
overriddenServiceRequest.abort();
}, 100);
});
return {
testServer,
promiseHolder,
validUpdateUri: baseUri + validUpdatePath,
missingContentSigUri: baseUri + missingContentSigPath,
badContentSigUri: baseUri + badContentSigPath,
invalidContentSigUri: baseUri + invalidContentSigPath,
badXmlUri: baseUri + badXmlPath,
badX5uRequestUri: baseUri + badX5uRequestPath,
x5uTimeoutUri: baseUri + x5uTimeoutPath,
x5uAbortUri: baseUri + x5uAbortPath,
};
}