Remove kinto client, Firefox kinto storage adapter, blocklist update client and integration with sync, OneCRL and the custom time check for derives system time.

This commit is contained in:
NTD 2018-02-02 09:21:33 -05:00 • committed by Roy Tam
commit f5048f2a1d
28 changed files with 24 additions and 10664 deletions

View file

@ -1,224 +0,0 @@
const { Constructor: CC } = Components;
Cu.import("resource://testing-common/httpd.js");
const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.js");
const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream", "setInputStream");
let server;
// set up what we need to make storage adapters
const Kinto = loadKinto();
const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
const kintoFilename = "kinto.sqlite";
let kintoClient;
function do_get_kinto_collection(collectionName) {
if (!kintoClient) {
let config = {
// Set the remote to be some server that will cause test failure when
// hit since we should never hit the server directly, only via maybeSync()
remote: "https://firefox.settings.services.mozilla.com/v1/",
// Set up the adapter and bucket as normal
adapter: FirefoxAdapter,
bucket: "blocklists"
};
kintoClient = new Kinto(config);
}
return kintoClient.collection(collectionName);
}
// Some simple tests to demonstrate that the logic inside maybeSync works
// correctly and that simple kinto operations are working as expected. There
// are more tests for core Kinto.js (and its storage adapter) in the
// xpcshell tests under /services/common
add_task(function* test_something(){
const configPath = "/v1/";
const recordsPath = "/v1/buckets/blocklists/collections/certificates/records";
Services.prefs.setCharPref("services.settings.server",
`http://localhost:${server.identity.primaryPort}/v1`);
// register a handler
function handleResponse (request, response) {
try {
const sample = getSampleResponse(request, server.identity.primaryPort);
if (!sample) {
do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
}
response.setStatusLine(null, sample.status.status,
sample.status.statusText);
// send the headers
for (let headerLine of sample.sampleHeaders) {
let headerElements = headerLine.split(':');
response.setHeader(headerElements[0], headerElements[1].trimLeft());
}
response.setHeader("Date", (new Date()).toUTCString());
response.write(sample.responseBody);
} catch (e) {
do_print(e);
}
}
server.registerPathHandler(configPath, handleResponse);
server.registerPathHandler(recordsPath, handleResponse);
// Test an empty db populates
let result = yield OneCRLBlocklistClient.maybeSync(2000, Date.now());
// Open the collection, verify it's been populated:
// Our test data has a single record; it should be in the local collection
let collection = do_get_kinto_collection("certificates");
yield collection.db.open();
let list = yield collection.list();
do_check_eq(list.data.length, 1);
yield collection.db.close();
// Test the db is updated when we call again with a later lastModified value
result = yield OneCRLBlocklistClient.maybeSync(4000, Date.now());
// Open the collection, verify it's been updated:
// Our test data now has two records; both should be in the local collection
collection = do_get_kinto_collection("certificates");
yield collection.db.open();
list = yield collection.list();
do_check_eq(list.data.length, 3);
yield collection.db.close();
// Try to maybeSync with the current lastModified value - no connection
// should be attempted.
// Clear the kinto base pref so any connections will cause a test failure
Services.prefs.clearUserPref("services.settings.server");
yield OneCRLBlocklistClient.maybeSync(4000, Date.now());
// Try again with a lastModified value at some point in the past
yield OneCRLBlocklistClient.maybeSync(3000, Date.now());
// Check the OneCRL check time pref is modified, even if the collection
// hasn't changed
Services.prefs.setIntPref("services.blocklist.onecrl.checked", 0);
yield OneCRLBlocklistClient.maybeSync(3000, Date.now());
let newValue = Services.prefs.getIntPref("services.blocklist.onecrl.checked");
do_check_neq(newValue, 0);
// Check that a sync completes even when there's bad data in the
// collection. This will throw on fail, so just calling maybeSync is an
// acceptible test.
Services.prefs.setCharPref("services.settings.server",
`http://localhost:${server.identity.primaryPort}/v1`);
yield OneCRLBlocklistClient.maybeSync(5000, Date.now());
});
function run_test() {
// Ensure that signature verification is disabled to prevent interference
// with basic certificate sync tests
Services.prefs.setBoolPref("services.blocklist.signing.enforced", false);
// Set up an HTTP Server
server = new HttpServer();
server.start(-1);
run_next_test();
do_register_cleanup(function() {
server.stop(() => { });
});
}
// get a response for a given request from sample data
function getSampleResponse(req, port) {
const responses = {
"OPTIONS": {
"sampleHeaders": [
"Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
"Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
"Access-Control-Allow-Origin: *",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress"
],
"status": {status: 200, statusText: "OK"},
"responseBody": "null"
},
"GET:/v1/?": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress"
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
},
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"3000\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{
"issuerName": "MEQxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwx0aGF3dGUsIEluYy4xHjAcBgNVBAMTFXRoYXd0ZSBFViBTU0wgQ0EgLSBHMw==",
"serialNumber":"CrTHPEE6AZSfI3jysin2bA==",
"id":"78cf8900-fdea-4ce5-f8fb-b78710617718",
"last_modified":3000
}]})
},
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=3000": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"4000\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{
"issuerName":"MFkxCzAJBgNVBAYTAk5MMR4wHAYDVQQKExVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKjAoBgNVBAMTIVN0YWF0IGRlciBOZWRlcmxhbmRlbiBPdmVyaGVpZCBDQQ",
"serialNumber":"ATFpsA==",
"id":"dabafde9-df4a-ddba-2548-748da04cc02c",
"last_modified":4000
},{
"subject":"MCIxIDAeBgNVBAMMF0Fub3RoZXIgVGVzdCBFbmQtZW50aXR5",
"pubKeyHash":"VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8=",
"id":"dabafde9-df4a-ddba-2548-748da04cc02d",
"last_modified":4000
}]})
},
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"5000\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{
"issuerName":"not a base64 encoded issuer",
"serialNumber":"not a base64 encoded serial",
"id":"dabafde9-df4a-ddba-2548-748da04cc02e",
"last_modified":5000
},{
"subject":"not a base64 encoded subject",
"pubKeyHash":"not a base64 encoded pubKeyHash",
"id":"dabafde9-df4a-ddba-2548-748da04cc02f",
"last_modified":5000
},{
"subject":"MCIxIDAeBgNVBAMMF0Fub3RoZXIgVGVzdCBFbmQtZW50aXR5",
"pubKeyHash":"VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8=",
"id":"dabafde9-df4a-ddba-2548-748da04cc02g",
"last_modified":5000
}]})
}
};
return responses[`${req.method}:${req.path}?${req.queryString}`] ||
responses[req.method];
}

View file

@ -1,412 +0,0 @@
const { Constructor: CC } = Components;
const KEY_PROFILEDIR = "ProfD";
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://testing-common/httpd.js");
Cu.import("resource://gre/modules/Timer.jsm");
const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm");
const { OS } = Cu.import("resource://gre/modules/osfile.jsm");
const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
const BlocklistClients = Cu.import("resource://services-common/blocklist-clients.js");
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream", "setInputStream");
const gBlocklistClients = [
{client: BlocklistClients.AddonBlocklistClient, filename: BlocklistClients.FILENAME_ADDONS_JSON, testData: ["i808","i720", "i539"]},
{client: BlocklistClients.PluginBlocklistClient, filename: BlocklistClients.FILENAME_PLUGINS_JSON, testData: ["p1044","p32","p28"]},
{client: BlocklistClients.GfxBlocklistClient, filename: BlocklistClients.FILENAME_GFX_JSON, testData: ["g204","g200","g36"]},
];
let server;
let kintoClient;
function kintoCollection(collectionName) {
if (!kintoClient) {
const Kinto = loadKinto();
const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
const config = {
// Set the remote to be some server that will cause test failure when
// hit since we should never hit the server directly, only via maybeSync()
remote: "https://firefox.settings.services.mozilla.com/v1/",
adapter: FirefoxAdapter,
bucket: "blocklists"
};
kintoClient = new Kinto(config);
}
return kintoClient.collection(collectionName);
}
function* readJSON(filepath) {
const binaryData = yield OS.File.read(filepath);
const textData = (new TextDecoder()).decode(binaryData);
return Promise.resolve(JSON.parse(textData));
}
function* clear_state() {
for (let {client} of gBlocklistClients) {
// Remove last server times.
Services.prefs.clearUserPref(client.lastCheckTimePref);
// Clear local DB.
const collection = kintoCollection(client.collectionName);
try {
yield collection.db.open();
yield collection.clear();
} finally {
yield collection.db.close();
}
}
// Remove profile data.
for (let {filename} of gBlocklistClients) {
const blocklist = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
if (blocklist.exists()) {
blocklist.remove(true);
}
}
}
function run_test() {
// Set up an HTTP Server
server = new HttpServer();
server.start(-1);
// Point the blocklist clients to use this local HTTP server.
Services.prefs.setCharPref("services.settings.server",
`http://localhost:${server.identity.primaryPort}/v1`);
// Setup server fake responses.
function handleResponse(request, response) {
try {
const sample = getSampleResponse(request, server.identity.primaryPort);
if (!sample) {
do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
}
response.setStatusLine(null, sample.status.status,
sample.status.statusText);
// send the headers
for (let headerLine of sample.sampleHeaders) {
let headerElements = headerLine.split(':');
response.setHeader(headerElements[0], headerElements[1].trimLeft());
}
response.setHeader("Date", (new Date()).toUTCString());
response.write(sample.responseBody);
response.finish();
} catch (e) {
do_print(e);
}
}
const configPath = "/v1/";
const addonsRecordsPath = "/v1/buckets/blocklists/collections/addons/records";
const gfxRecordsPath = "/v1/buckets/blocklists/collections/gfx/records";
const pluginsRecordsPath = "/v1/buckets/blocklists/collections/plugins/records";
server.registerPathHandler(configPath, handleResponse);
server.registerPathHandler(addonsRecordsPath, handleResponse);
server.registerPathHandler(gfxRecordsPath, handleResponse);
server.registerPathHandler(pluginsRecordsPath, handleResponse);
run_next_test();
do_register_cleanup(function() {
server.stop(() => { });
});
}
add_task(function* test_records_obtained_from_server_are_stored_in_db(){
for (let {client} of gBlocklistClients) {
// Test an empty db populates
let result = yield client.maybeSync(2000, Date.now());
// Open the collection, verify it's been populated:
// Our test data has a single record; it should be in the local collection
let collection = kintoCollection(client.collectionName);
yield collection.db.open();
let list = yield collection.list();
equal(list.data.length, 1);
yield collection.db.close();
}
});
add_task(clear_state);
add_task(function* test_list_is_written_to_file_in_profile(){
for (let {client, filename, testData} of gBlocklistClients) {
const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
strictEqual(profFile.exists(), false);
let result = yield client.maybeSync(2000, Date.now());
strictEqual(profFile.exists(), true);
const content = yield readJSON(profFile.path);
equal(content.data[0].blockID, testData[testData.length - 1]);
}
});
add_task(clear_state);
add_task(function* test_current_server_time_is_saved_in_pref(){
for (let {client} of gBlocklistClients) {
const before = Services.prefs.getIntPref(client.lastCheckTimePref);
const serverTime = Date.now();
yield client.maybeSync(2000, serverTime);
const after = Services.prefs.getIntPref(client.lastCheckTimePref);
equal(after, Math.round(serverTime / 1000));
}
});
add_task(clear_state);
add_task(function* test_update_json_file_when_addons_has_changes(){
for (let {client, filename, testData} of gBlocklistClients) {
yield client.maybeSync(2000, Date.now() - 1000);
const before = Services.prefs.getIntPref(client.lastCheckTimePref);
const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
const fileLastModified = profFile.lastModifiedTime = profFile.lastModifiedTime - 1000;
const serverTime = Date.now();
yield client.maybeSync(3001, serverTime);
// File was updated.
notEqual(fileLastModified, profFile.lastModifiedTime);
const content = yield readJSON(profFile.path);
deepEqual(content.data.map((r) => r.blockID), testData);
// Server time was updated.
const after = Services.prefs.getIntPref(client.lastCheckTimePref);
equal(after, Math.round(serverTime / 1000));
}
});
add_task(clear_state);
add_task(function* test_sends_reload_message_when_blocklist_has_changes(){
for (let {client, filename} of gBlocklistClients) {
let received = yield new Promise((resolve, reject) => {
Services.ppmm.addMessageListener("Blocklist:reload-from-disk", {
receiveMessage(aMsg) { resolve(aMsg) }
});
client.maybeSync(2000, Date.now() - 1000);
});
equal(received.data.filename, filename);
}
});
add_task(clear_state);
add_task(function* test_do_nothing_when_blocklist_is_up_to_date(){
for (let {client, filename} of gBlocklistClients) {
yield client.maybeSync(2000, Date.now() - 1000);
const before = Services.prefs.getIntPref(client.lastCheckTimePref);
const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
const fileLastModified = profFile.lastModifiedTime = profFile.lastModifiedTime - 1000;
const serverTime = Date.now();
yield client.maybeSync(3000, serverTime);
// File was not updated.
equal(fileLastModified, profFile.lastModifiedTime);
// Server time was updated.
const after = Services.prefs.getIntPref(client.lastCheckTimePref);
equal(after, Math.round(serverTime / 1000));
}
});
add_task(clear_state);
// get a response for a given request from sample data
function getSampleResponse(req, port) {
const responses = {
"OPTIONS": {
"sampleHeaders": [
"Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
"Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
"Access-Control-Allow-Origin: *",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress"
],
"status": {status: 200, statusText: "OK"},
"responseBody": "null"
},
"GET:/v1/?": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress"
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
},
"GET:/v1/buckets/blocklists/collections/addons/records?_sort=-last_modified": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"3000\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{
"prefs": [],
"blockID": "i539",
"last_modified": 3000,
"versionRange": [{
"targetApplication": [],
"maxVersion": "*",
"minVersion": "0",
"severity": "1"
}],
"guid": "ScorpionSaver@jetpack",
"id": "9d500963-d80e-3a91-6e74-66f3811b99cc"
}]})
},
"GET:/v1/buckets/blocklists/collections/plugins/records?_sort=-last_modified": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"3000\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{
"matchFilename": "NPFFAddOn.dll",
"blockID": "p28",
"id": "7b1e0b3c-e390-a817-11b6-a6887f65f56e",
"last_modified": 3000,
"versionRange": []
}]})
},
"GET:/v1/buckets/blocklists/collections/gfx/records?_sort=-last_modified": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"3000\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{
"driverVersionComparator": "LESS_THAN_OR_EQUAL",
"driverVersion": "8.17.12.5896",
"vendor": "0x10de",
"blockID": "g36",
"feature": "DIRECT3D_9_LAYERS",
"devices": ["0x0a6c"],
"featureStatus": "BLOCKED_DRIVER_VERSION",
"last_modified": 3000,
"os": "WINNT 6.1",
"id": "3f947f16-37c2-4e96-d356-78b26363729b"
}]})
},
"GET:/v1/buckets/blocklists/collections/addons/records?_sort=-last_modified&_since=3000": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"4000\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{
"prefs": [],
"blockID": "i808",
"last_modified": 4000,
"versionRange": [{
"targetApplication": [],
"maxVersion": "*",
"minVersion": "0",
"severity": "3"
}],
"guid": "{c96d1ae6-c4cf-4984-b110-f5f561b33b5a}",
"id": "9ccfac91-e463-c30c-f0bd-14143794a8dd"
}, {
"prefs": ["browser.startup.homepage"],
"blockID": "i720",
"last_modified": 3500,
"versionRange": [{
"targetApplication": [],
"maxVersion": "*",
"minVersion": "0",
"severity": "1"
}],
"guid": "FXqG@xeeR.net",
"id": "cf9b3129-a97e-dbd7-9525-a8575ac03c25"
}]})
},
"GET:/v1/buckets/blocklists/collections/plugins/records?_sort=-last_modified&_since=3000": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"4000\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{
"infoURL": "https://get.adobe.com/flashplayer/",
"blockID": "p1044",
"matchFilename": "libflashplayer\\.so",
"last_modified": 4000,
"versionRange": [{
"targetApplication": [],
"minVersion": "11.2.202.509",
"maxVersion": "11.2.202.539",
"severity": "0",
"vulnerabilityStatus": "1"
}],
"os": "Linux",
"id": "aabad965-e556-ffe7-4191-074f5dee3df3"
}, {
"matchFilename": "npViewpoint.dll",
"blockID": "p32",
"id": "1f48af42-c508-b8ef-b8d5-609d48e4f6c9",
"last_modified": 3500,
"versionRange": [{
"targetApplication": [{
"minVersion": "3.0",
"guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
"maxVersion": "*"
}]
}]
}]})
},
"GET:/v1/buckets/blocklists/collections/gfx/records?_sort=-last_modified&_since=3000": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"4000\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{
"vendor": "0x8086",
"blockID": "g204",
"feature": "WEBGL_MSAA",
"devices": [],
"id": "c96bca82-e6bd-044d-14c4-9c1d67e9283a",
"last_modified": 4000,
"os": "Darwin 10",
"featureStatus": "BLOCKED_DEVICE"
}, {
"vendor": "0x10de",
"blockID": "g200",
"feature": "WEBGL_MSAA",
"devices": [],
"id": "c3a15ba9-e0e2-421f-e399-c995e5b8d14e",
"last_modified": 3500,
"os": "Darwin 11",
"featureStatus": "BLOCKED_DEVICE"
}]})
}
};
return responses[`${req.method}:${req.path}?${req.queryString}`] ||
responses[req.method];
}

View file

@ -1,510 +0,0 @@
"use strict";
Cu.import("resource://services-common/blocklist-updater.js");
Cu.import("resource://testing-common/httpd.js");
const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.js");
let server;
const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket";
const PREF_BLOCKLIST_ENFORCE_SIGNING = "services.blocklist.signing.enforced";
const PREF_BLOCKLIST_ONECRL_COLLECTION = "services.blocklist.onecrl.collection";
const PREF_SETTINGS_SERVER = "services.settings.server";
const PREF_SIGNATURE_ROOT = "security.content.signature.root_hash";
const CERT_DIR = "test_blocklist_signatures/";
const CHAIN_FILES =
["collection_signing_ee.pem",
"collection_signing_int.pem",
"collection_signing_root.pem"];
function getFileData(file) {
const stream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
stream.init(file, -1, 0, 0);
const data = NetUtil.readInputStreamToString(stream, stream.available());
stream.close();
return data;
}
function setRoot() {
const filename = CERT_DIR + CHAIN_FILES[0];
const certFile = do_get_file(filename, false);
const b64cert = getFileData(certFile)
.replace(/-----BEGIN CERTIFICATE-----/, "")
.replace(/-----END CERTIFICATE-----/, "")
.replace(/[\r\n]/g, "");
const certdb = Cc["@mozilla.org/security/x509certdb;1"]
.getService(Ci.nsIX509CertDB);
const cert = certdb.constructX509FromBase64(b64cert);
Services.prefs.setCharPref(PREF_SIGNATURE_ROOT, cert.sha256Fingerprint);
}
function getCertChain() {
const chain = [];
for (let file of CHAIN_FILES) {
chain.push(getFileData(do_get_file(CERT_DIR + file)));
}
return chain.join("\n");
}
function* checkRecordCount(count) {
// open the collection manually
const base = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
const bucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET);
const collectionName =
Services.prefs.getCharPref(PREF_BLOCKLIST_ONECRL_COLLECTION);
const Kinto = loadKinto();
const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
const config = {
remote: base,
bucket: bucket,
adapter: FirefoxAdapter,
};
const db = new Kinto(config);
const collection = db.collection(collectionName);
yield collection.db.open();
// Check we have the expected number of records
let records = yield collection.list();
do_check_eq(count, records.data.length);
// Close the collection so the test can exit cleanly
yield collection.db.close();
}
// Check to ensure maybeSync is called with correct values when a changes
// document contains information on when a collection was last modified
add_task(function* test_check_signatures(){
const port = server.identity.primaryPort;
// a response to give the client when the cert chain is expected
function makeMetaResponseBody(lastModified, signature) {
return {
data: {
id: "certificates",
last_modified: lastModified,
signature: {
x5u: `http://localhost:${port}/test_blocklist_signatures/test_cert_chain.pem`,
public_key: "fake",
"content-signature": `x5u=http://localhost:${port}/test_blocklist_signatures/test_cert_chain.pem;p384ecdsa=${signature}`,
signature_encoding: "rs_base64url",
signature: signature,
hash_algorithm: "sha384",
ref: "1yryrnmzou5rf31ou80znpnq8n"
}
}
};
}
function makeMetaResponse(eTag, body, comment) {
return {
comment: comment,
sampleHeaders: [
"Content-Type: application/json; charset=UTF-8",
`ETag: \"${eTag}\"`
],
status: {status: 200, statusText: "OK"},
responseBody: JSON.stringify(body)
};
}
function registerHandlers(responses){
function handleResponse (serverTimeMillis, request, response) {
const key = `${request.method}:${request.path}?${request.queryString}`;
const available = responses[key];
const sampled = available.length > 1 ? available.shift() : available[0];
if (!sampled) {
do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
}
response.setStatusLine(null, sampled.status.status,
sampled.status.statusText);
// send the headers
for (let headerLine of sampled.sampleHeaders) {
let headerElements = headerLine.split(':');
response.setHeader(headerElements[0], headerElements[1].trimLeft());
}
// set the server date
response.setHeader("Date", (new Date(serverTimeMillis)).toUTCString());
response.write(sampled.responseBody);
}
for (let key of Object.keys(responses)) {
const keyParts = key.split(":");
const method = keyParts[0];
const valueParts = keyParts[1].split("?");
const path = valueParts[0];
server.registerPathHandler(path, handleResponse.bind(null, 2000));
}
}
// First, perform a signature verification with known data and signature
// to ensure things are working correctly
let verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
.createInstance(Ci.nsIContentSignatureVerifier);
const emptyData = '[]';
const emptySignature = "p384ecdsa=zbugm2FDitsHwk5-IWsas1PpWwY29f0Fg5ZHeqD8fzep7AVl2vfcaHA7LdmCZ28qZLOioGKvco3qT117Q4-HlqFTJM7COHzxGyU2MMJ0ZTnhJrPOC1fP3cVQjU1PTWi9";
const name = "onecrl.content-signature.mozilla.org";
ok(verifier.verifyContentSignature(emptyData, emptySignature,
getCertChain(), name));
verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
.createInstance(Ci.nsIContentSignatureVerifier);
const collectionData = '[{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:43:37Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"97fbf7c4-3ef2-f54f-0029-1ba6540c63ea","issuerName":"MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==","last_modified":2000,"serialNumber":"BAAAAAABA/A35EU="},{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:48:11Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc","issuerName":"MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB","last_modified":3000,"serialNumber":"BAAAAAABI54PryQ="}]';
const collectionSignature = "p384ecdsa=f4pA2tYM5jQgWY6YUmhUwQiBLj6QO5sHLD_5MqLePz95qv-7cNCuQoZnPQwxoptDtW8hcWH3kLb0quR7SB-r82gkpR9POVofsnWJRA-ETb0BcIz6VvI3pDT49ZLlNg3p";
ok(verifier.verifyContentSignature(collectionData, collectionSignature, getCertChain(), name));
// set up prefs so the kinto updater talks to the test server
Services.prefs.setCharPref(PREF_SETTINGS_SERVER,
`http://localhost:${server.identity.primaryPort}/v1`);
// Set up some data we need for our test
let startTime = Date.now();
// These are records we'll use in the test collections
const RECORD1 = {
details: {
bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
created: "2016-01-18T14:43:37Z",
name: "GlobalSign certs",
who: ".",
why: "."
},
enabled: true,
id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea",
issuerName: "MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==",
last_modified: 2000,
serialNumber: "BAAAAAABA/A35EU="
};
const RECORD2 = {
details: {
bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
created: "2016-01-18T14:48:11Z",
name: "GlobalSign certs",
who: ".",
why: "."
},
enabled: true,
id: "e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc",
issuerName: "MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB",
last_modified: 3000,
serialNumber: "BAAAAAABI54PryQ="
};
const RECORD3 = {
details: {
bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
created: "2016-01-18T14:48:11Z",
name: "GlobalSign certs",
who: ".",
why: "."
},
enabled: true,
id: "c7c49b69-a4ab-418e-92a9-e1961459aa7f",
issuerName: "MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB",
last_modified: 4000,
serialNumber: "BAAAAAABI54PryQ="
};
const RECORD1_DELETION = {
deleted: true,
enabled: true,
id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea",
last_modified: 3500,
};
// Check that a signature on an empty collection is OK
// We need to set up paths on the HTTP server to return specific data from
// specific paths for each test. Here we prepare data for each response.
// A cert chain response (this the cert chain that contains the signing
// cert, the root and any intermediates in between). This is used in each
// sync.
const RESPONSE_CERT_CHAIN = {
comment: "RESPONSE_CERT_CHAIN",
sampleHeaders: [
"Content-Type: text/plain; charset=UTF-8"
],
status: {status: 200, statusText: "OK"},
responseBody: getCertChain()
};
// A server settings response. This is used in each sync.
const RESPONSE_SERVER_SETTINGS = {
comment: "RESPONSE_SERVER_SETTINGS",
sampleHeaders: [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress"
],
status: {status: 200, statusText: "OK"},
responseBody: JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
};
// This is the initial, empty state of the collection. This is only used
// for the first sync.
const RESPONSE_EMPTY_INITIAL = {
comment: "RESPONSE_EMPTY_INITIAL",
sampleHeaders: [
"Content-Type: application/json; charset=UTF-8",
"ETag: \"1000\""
],
status: {status: 200, statusText: "OK"},
responseBody: JSON.stringify({"data": []})
};
const RESPONSE_BODY_META_EMPTY_SIG = makeMetaResponseBody(1000,
"vxuAg5rDCB-1pul4a91vqSBQRXJG_j7WOYUTswxRSMltdYmbhLRH8R8brQ9YKuNDF56F-w6pn4HWxb076qgKPwgcEBtUeZAO_RtaHXRkRUUgVzAr86yQL4-aJTbv3D6u");
// The collection metadata containing the signature for the empty
// collection.
const RESPONSE_META_EMPTY_SIG =
makeMetaResponse(1000, RESPONSE_BODY_META_EMPTY_SIG,
"RESPONSE_META_EMPTY_SIG");
// Here, we map request method and path to the available responses
const emptyCollectionResponses = {
"GET:/test_blocklist_signatures/test_cert_chain.pem?":[RESPONSE_CERT_CHAIN],
"GET:/v1/?": [RESPONSE_SERVER_SETTINGS],
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified":
[RESPONSE_EMPTY_INITIAL],
"GET:/v1/buckets/blocklists/collections/certificates?":
[RESPONSE_META_EMPTY_SIG]
};
// .. and use this map to register handlers for each path
registerHandlers(emptyCollectionResponses);
// With all of this set up, we attempt a sync. This will resolve if all is
// well and throw if something goes wrong.
yield OneCRLBlocklistClient.maybeSync(1000, startTime);
// Check that some additions (2 records) to the collection have a valid
// signature.
// This response adds two entries (RECORD1 and RECORD2) to the collection
const RESPONSE_TWO_ADDED = {
comment: "RESPONSE_TWO_ADDED",
sampleHeaders: [
"Content-Type: application/json; charset=UTF-8",
"ETag: \"3000\""
],
status: {status: 200, statusText: "OK"},
responseBody: JSON.stringify({"data": [RECORD2, RECORD1]})
};
const RESPONSE_BODY_META_TWO_ITEMS_SIG = makeMetaResponseBody(3000,
"dwhJeypadNIyzGj3QdI0KMRTPnHhFPF_j73mNrsPAHKMW46S2Ftf4BzsPMvPMB8h0TjDus13wo_R4l432DHe7tYyMIWXY0PBeMcoe5BREhFIxMxTsh9eGVXBD1e3UwRy");
// A signature response for the collection containg RECORD1 and RECORD2
const RESPONSE_META_TWO_ITEMS_SIG =
makeMetaResponse(3000, RESPONSE_BODY_META_TWO_ITEMS_SIG,
"RESPONSE_META_TWO_ITEMS_SIG");
const twoItemsResponses = {
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=1000":
[RESPONSE_TWO_ADDED],
"GET:/v1/buckets/blocklists/collections/certificates?":
[RESPONSE_META_TWO_ITEMS_SIG]
};
registerHandlers(twoItemsResponses);
yield OneCRLBlocklistClient.maybeSync(3000, startTime);
// Check the collection with one addition and one removal has a valid
// signature
// Remove RECORD1, add RECORD3
const RESPONSE_ONE_ADDED_ONE_REMOVED = {
comment: "RESPONSE_ONE_ADDED_ONE_REMOVED ",
sampleHeaders: [
"Content-Type: application/json; charset=UTF-8",
"ETag: \"4000\""
],
status: {status: 200, statusText: "OK"},
responseBody: JSON.stringify({"data": [RECORD3, RECORD1_DELETION]})
};
const RESPONSE_BODY_META_THREE_ITEMS_SIG = makeMetaResponseBody(4000,
"MIEmNghKnkz12UodAAIc3q_Y4a3IJJ7GhHF4JYNYmm8avAGyPM9fYU7NzVo94pzjotG7vmtiYuHyIX2rTHTbT587w0LdRWxipgFd_PC1mHiwUyjFYNqBBG-kifYk7kEw");
// signature response for the collection containing RECORD2 and RECORD3
const RESPONSE_META_THREE_ITEMS_SIG =
makeMetaResponse(4000, RESPONSE_BODY_META_THREE_ITEMS_SIG,
"RESPONSE_META_THREE_ITEMS_SIG");
const oneAddedOneRemovedResponses = {
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=3000":
[RESPONSE_ONE_ADDED_ONE_REMOVED],
"GET:/v1/buckets/blocklists/collections/certificates?":
[RESPONSE_META_THREE_ITEMS_SIG]
};
registerHandlers(oneAddedOneRemovedResponses);
yield OneCRLBlocklistClient.maybeSync(4000, startTime);
// Check the signature is still valid with no operation (no changes)
// Leave the collection unchanged
const RESPONSE_EMPTY_NO_UPDATE = {
comment: "RESPONSE_EMPTY_NO_UPDATE ",
sampleHeaders: [
"Content-Type: application/json; charset=UTF-8",
"ETag: \"4000\""
],
status: {status: 200, statusText: "OK"},
responseBody: JSON.stringify({"data": []})
};
const noOpResponses = {
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
[RESPONSE_EMPTY_NO_UPDATE],
"GET:/v1/buckets/blocklists/collections/certificates?":
[RESPONSE_META_THREE_ITEMS_SIG]
};
registerHandlers(noOpResponses);
yield OneCRLBlocklistClient.maybeSync(4100, startTime);
// Check the collection is reset when the signature is invalid
// Prepare a (deliberately) bad signature to check the collection state is
// reset if something is inconsistent
const RESPONSE_COMPLETE_INITIAL = {
comment: "RESPONSE_COMPLETE_INITIAL ",
sampleHeaders: [
"Content-Type: application/json; charset=UTF-8",
"ETag: \"4000\""
],
status: {status: 200, statusText: "OK"},
responseBody: JSON.stringify({"data": [RECORD2, RECORD3]})
};
const RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID = {
comment: "RESPONSE_COMPLETE_INITIAL ",
sampleHeaders: [
"Content-Type: application/json; charset=UTF-8",
"ETag: \"4000\""
],
status: {status: 200, statusText: "OK"},
responseBody: JSON.stringify({"data": [RECORD3, RECORD2]})
};
const RESPONSE_BODY_META_BAD_SIG = makeMetaResponseBody(4000,
"aW52YWxpZCBzaWduYXR1cmUK");
const RESPONSE_META_BAD_SIG =
makeMetaResponse(4000, RESPONSE_BODY_META_BAD_SIG, "RESPONSE_META_BAD_SIG");
const badSigGoodSigResponses = {
// In this test, we deliberately serve a bad signature initially. The
// subsequent signature returned is a valid one for the three item
// collection.
"GET:/v1/buckets/blocklists/collections/certificates?":
[RESPONSE_META_BAD_SIG, RESPONSE_META_THREE_ITEMS_SIG],
// The first collection state is the three item collection (since
// there's a sync with no updates) - but, since the signature is wrong,
// another request will be made...
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
[RESPONSE_EMPTY_NO_UPDATE],
// The next request is for the full collection. This will be checked
// against the valid signature - so the sync should succeed.
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified":
[RESPONSE_COMPLETE_INITIAL],
// The next request is for the full collection sorted by id. This will be
// checked against the valid signature - so the sync should succeed.
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id":
[RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID]
};
registerHandlers(badSigGoodSigResponses);
yield OneCRLBlocklistClient.maybeSync(5000, startTime);
const badSigGoodOldResponses = {
// In this test, we deliberately serve a bad signature initially. The
// subsequent sitnature returned is a valid one for the three item
// collection.
"GET:/v1/buckets/blocklists/collections/certificates?":
[RESPONSE_META_BAD_SIG, RESPONSE_META_EMPTY_SIG],
// The first collection state is the current state (since there's no update
// - but, since the signature is wrong, another request will be made)
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
[RESPONSE_EMPTY_NO_UPDATE],
// The next request is for the full collection sorted by id. This will be
// checked against the valid signature and last_modified times will be
// compared. Sync should fail, even though the signature is good,
// because the local collection is newer.
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id":
[RESPONSE_EMPTY_INITIAL],
};
// ensure our collection hasn't been replaced with an older, empty one
yield checkRecordCount(2);
registerHandlers(badSigGoodOldResponses);
yield OneCRLBlocklistClient.maybeSync(5000, startTime);
const allBadSigResponses = {
// In this test, we deliberately serve only a bad signature.
"GET:/v1/buckets/blocklists/collections/certificates?":
[RESPONSE_META_BAD_SIG],
// The first collection state is the three item collection (since
// there's a sync with no updates) - but, since the signature is wrong,
// another request will be made...
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
[RESPONSE_EMPTY_NO_UPDATE],
// The next request is for the full collection sorted by id. This will be
// checked against the valid signature - so the sync should succeed.
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id":
[RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID]
};
registerHandlers(allBadSigResponses);
try {
yield OneCRLBlocklistClient.maybeSync(6000, startTime);
do_throw("Sync should fail (the signature is intentionally bad)");
} catch (e) {
yield checkRecordCount(2);
}
});
function run_test() {
// ensure signatures are enforced
Services.prefs.setBoolPref(PREF_BLOCKLIST_ENFORCE_SIGNING, true);
// get a signature verifier to ensure nsNSSComponent is initialized
Cc["@mozilla.org/security/contentsignatureverifier;1"]
.createInstance(Ci.nsIContentSignatureVerifier);
// set the content signing root to our test root
setRoot();
// Set up an HTTP Server
server = new HttpServer();
server.start(-1);
run_next_test();
do_register_cleanup(function() {
server.stop(function() { });
});
}

View file

@ -1,173 +0,0 @@
Cu.import("resource://testing-common/httpd.js");
var server;
const PREF_SETTINGS_SERVER = "services.settings.server";
const PREF_LAST_UPDATE = "services.blocklist.last_update_seconds";
const PREF_LAST_ETAG = "services.blocklist.last_etag";
const PREF_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
// Check to ensure maybeSync is called with correct values when a changes
// document contains information on when a collection was last modified
add_task(function* test_check_maybeSync(){
const changesPath = "/v1/buckets/monitor/collections/changes/records";
// register a handler
function handleResponse (serverTimeMillis, request, response) {
try {
const sampled = getSampleResponse(request, server.identity.primaryPort);
if (!sampled) {
do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
}
response.setStatusLine(null, sampled.status.status,
sampled.status.statusText);
// send the headers
for (let headerLine of sampled.sampleHeaders) {
let headerElements = headerLine.split(':');
response.setHeader(headerElements[0], headerElements[1].trimLeft());
}
// set the server date
response.setHeader("Date", (new Date(serverTimeMillis)).toUTCString());
response.write(sampled.responseBody);
} catch (e) {
dump(`${e}\n`);
}
}
server.registerPathHandler(changesPath, handleResponse.bind(null, 2000));
// set up prefs so the kinto updater talks to the test server
Services.prefs.setCharPref(PREF_SETTINGS_SERVER,
`http://localhost:${server.identity.primaryPort}/v1`);
// set some initial values so we can check these are updated appropriately
Services.prefs.setIntPref(PREF_LAST_UPDATE, 0);
Services.prefs.setIntPref(PREF_CLOCK_SKEW_SECONDS, 0);
Services.prefs.clearUserPref(PREF_LAST_ETAG);
let startTime = Date.now();
let updater = Cu.import("resource://services-common/blocklist-updater.js");
let syncPromise = new Promise(function(resolve, reject) {
// add a test kinto client that will respond to lastModified information
// for a collection called 'test-collection'
updater.addTestBlocklistClient("test-collection", {
maybeSync(lastModified, serverTime) {
do_check_eq(lastModified, 1000);
do_check_eq(serverTime, 2000);
resolve();
}
});
updater.checkVersions();
});
// ensure we get the maybeSync call
yield syncPromise;
// check the last_update is updated
do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
// How does the clock difference look?
let endTime = Date.now();
let clockDifference = Services.prefs.getIntPref(PREF_CLOCK_SKEW_SECONDS);
// we previously set the serverTime to 2 (seconds past epoch)
do_check_true(clockDifference <= endTime / 1000
&& clockDifference >= Math.floor(startTime / 1000) - 2);
// Last timestamp was saved. An ETag header value is a quoted string.
let lastEtag = Services.prefs.getCharPref(PREF_LAST_ETAG);
do_check_eq(lastEtag, "\"1100\"");
// Simulate a poll with up-to-date collection.
Services.prefs.setIntPref(PREF_LAST_UPDATE, 0);
// If server has no change, a 304 is received, maybeSync() is not called.
updater.addTestBlocklistClient("test-collection", {
maybeSync: () => {throw new Error("Should not be called");}
});
yield updater.checkVersions();
// Last update is overwritten
do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
// Simulate a server error.
function simulateErrorResponse (request, response) {
response.setHeader("Date", (new Date(3000)).toUTCString());
response.setHeader("Content-Type", "application/json; charset=UTF-8");
response.write(JSON.stringify({
code: 503,
errno: 999,
error: "Service Unavailable",
}));
response.setStatusLine(null, 503, "Service Unavailable");
}
server.registerPathHandler(changesPath, simulateErrorResponse);
// checkVersions() fails with adequate error.
let error;
try {
yield updater.checkVersions();
} catch (e) {
error = e;
}
do_check_eq(error.message, "Polling for changes failed.");
// When an error occurs, last update was not overwritten (see Date header above).
do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
// check negative clock skew times
// set to a time in the future
server.registerPathHandler(changesPath, handleResponse.bind(null, Date.now() + 10000));
yield updater.checkVersions();
clockDifference = Services.prefs.getIntPref(PREF_CLOCK_SKEW_SECONDS);
// we previously set the serverTime to Date.now() + 10000 ms past epoch
do_check_true(clockDifference <= 0 && clockDifference >= -10);
});
function run_test() {
// Set up an HTTP Server
server = new HttpServer();
server.start(-1);
run_next_test();
do_register_cleanup(function() {
server.stop(function() { });
});
}
// get a response for a given request from sample data
function getSampleResponse(req, port) {
const responses = {
"GET:/v1/buckets/monitor/collections/changes/records?": {
"sampleHeaders": [
"Content-Type: application/json; charset=UTF-8",
"ETag: \"1100\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data": [{
"host": "localhost",
"last_modified": 1100,
"bucket": "blocklists:aurora",
"id": "330a0c5f-fadf-ff0b-40c8-4eb0d924ff6a",
"collection": "test-collection"
}, {
"host": "localhost",
"last_modified": 1000,
"bucket": "blocklists",
"id": "254cbb9e-6888-4d9f-8e60-58b74faa8778",
"collection": "test-collection"
}]})
}
};
if (req.hasHeader("if-none-match") && req.getHeader("if-none-match", "") == "\"1100\"")
return {sampleHeaders: [], status: {status: 304, statusText: "Not Modified"}, responseBody: ""};
return responses[`${req.method}:${req.path}?${req.queryString}`] ||
responses[req.method];
}

View file

@ -1,412 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://services-common/kinto-offline-client.js");
Cu.import("resource://testing-common/httpd.js");
const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream", "setInputStream");
var server;
// set up what we need to make storage adapters
const Kinto = loadKinto();
const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
const kintoFilename = "kinto.sqlite";
let kintoClient;
function do_get_kinto_collection() {
if (!kintoClient) {
let config = {
remote:`http://localhost:${server.identity.primaryPort}/v1/`,
headers: {Authorization: "Basic " + btoa("user:pass")},
adapter: FirefoxAdapter
};
kintoClient = new Kinto(config);
}
return kintoClient.collection("test_collection");
}
function* clear_collection() {
const collection = do_get_kinto_collection();
try {
yield collection.db.open();
yield collection.clear();
} finally {
yield collection.db.close();
}
}
// test some operations on a local collection
add_task(function* test_kinto_add_get() {
const collection = do_get_kinto_collection();
try {
yield collection.db.open();
let newRecord = { foo: "bar" };
// check a record is created
let createResult = yield collection.create(newRecord);
do_check_eq(createResult.data.foo, newRecord.foo);
// check getting the record gets the same info
let getResult = yield collection.get(createResult.data.id);
deepEqual(createResult.data, getResult.data);
// check what happens if we create the same item again (it should throw
// since you can't create with id)
try {
yield collection.create(createResult.data);
do_throw("Creation of a record with an id should fail");
} catch (err) { }
// try a few creates without waiting for the first few to resolve
let promises = [];
promises.push(collection.create(newRecord));
promises.push(collection.create(newRecord));
promises.push(collection.create(newRecord));
yield collection.create(newRecord);
yield Promise.all(promises);
} finally {
yield collection.db.close();
}
});
add_task(clear_collection);
// test some operations on multiple connections
add_task(function* test_kinto_add_get() {
const collection1 = do_get_kinto_collection();
const collection2 = kintoClient.collection("test_collection_2");
try {
yield collection1.db.open();
yield collection2.db.open();
let newRecord = { foo: "bar" };
// perform several write operations alternately without waiting for promises
// to resolve
let promises = [];
for (let i = 0; i < 10; i++) {
promises.push(collection1.create(newRecord));
promises.push(collection2.create(newRecord));
}
// ensure subsequent operations still work
yield Promise.all([collection1.create(newRecord),
collection2.create(newRecord)]);
yield Promise.all(promises);
} finally {
yield collection1.db.close();
yield collection2.db.close();
}
});
add_task(clear_collection);
add_task(function* test_kinto_update() {
const collection = do_get_kinto_collection();
try {
yield collection.db.open();
const newRecord = { foo: "bar" };
// check a record is created
let createResult = yield collection.create(newRecord);
do_check_eq(createResult.data.foo, newRecord.foo);
do_check_eq(createResult.data._status, "created");
// check we can update this OK
let copiedRecord = Object.assign(createResult.data, {});
deepEqual(createResult.data, copiedRecord);
copiedRecord.foo = "wibble";
let updateResult = yield collection.update(copiedRecord);
// check the field was updated
do_check_eq(updateResult.data.foo, copiedRecord.foo);
// check the status is still "created", since we haven't synced
// the record
do_check_eq(updateResult.data._status, "created");
} finally {
yield collection.db.close();
}
});
add_task(clear_collection);
add_task(function* test_kinto_clear() {
const collection = do_get_kinto_collection();
try {
yield collection.db.open();
// create an expected number of records
const expected = 10;
const newRecord = { foo: "bar" };
for (let i = 0; i < expected; i++) {
yield collection.create(newRecord);
}
// check the collection contains the correct number
let list = yield collection.list();
do_check_eq(list.data.length, expected);
// clear the collection and check again - should be 0
yield collection.clear();
list = yield collection.list();
do_check_eq(list.data.length, 0);
} finally {
yield collection.db.close();
}
});
add_task(clear_collection);
add_task(function* test_kinto_delete(){
const collection = do_get_kinto_collection();
try {
yield collection.db.open();
const newRecord = { foo: "bar" };
// check a record is created
let createResult = yield collection.create(newRecord);
do_check_eq(createResult.data.foo, newRecord.foo);
// check getting the record gets the same info
let getResult = yield collection.get(createResult.data.id);
deepEqual(createResult.data, getResult.data);
// delete that record
let deleteResult = yield collection.delete(createResult.data.id);
// check the ID is set on the result
do_check_eq(getResult.data.id, deleteResult.data.id);
// and check that get no longer returns the record
try {
getResult = yield collection.get(createResult.data.id);
do_throw("there should not be a result");
} catch (e) { }
} finally {
yield collection.db.close();
}
});
add_task(function* test_kinto_list(){
const collection = do_get_kinto_collection();
try {
yield collection.db.open();
const expected = 10;
const created = [];
for (let i = 0; i < expected; i++) {
let newRecord = { foo: "test " + i };
let createResult = yield collection.create(newRecord);
created.push(createResult.data);
}
// check the collection contains the correct number
let list = yield collection.list();
do_check_eq(list.data.length, expected);
// check that all created records exist in the retrieved list
for (let createdRecord of created) {
let found = false;
for (let retrievedRecord of list.data) {
if (createdRecord.id == retrievedRecord.id) {
deepEqual(createdRecord, retrievedRecord);
found = true;
}
}
do_check_true(found);
}
} finally {
yield collection.db.close();
}
});
add_task(clear_collection);
add_task(function* test_loadDump_ignores_already_imported_records(){
const collection = do_get_kinto_collection();
try {
yield collection.db.open();
const record = {id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", title: "foo", last_modified: 1457896541};
yield collection.loadDump([record]);
let impactedRecords = yield collection.loadDump([record]);
do_check_eq(impactedRecords.length, 0);
} finally {
yield collection.db.close();
}
});
add_task(clear_collection);
add_task(function* test_loadDump_should_overwrite_old_records(){
const collection = do_get_kinto_collection();
try {
yield collection.db.open();
const record = {id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", title: "foo", last_modified: 1457896541};
yield collection.loadDump([record]);
const updated = Object.assign({}, record, {last_modified: 1457896543});
let impactedRecords = yield collection.loadDump([updated]);
do_check_eq(impactedRecords.length, 1);
} finally {
yield collection.db.close();
}
});
add_task(clear_collection);
add_task(function* test_loadDump_should_not_overwrite_unsynced_records(){
const collection = do_get_kinto_collection();
try {
yield collection.db.open();
const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f";
yield collection.create({id: recordId, title: "foo"}, {useRecordId: true});
const record = {id: recordId, title: "bar", last_modified: 1457896541};
let impactedRecords = yield collection.loadDump([record]);
do_check_eq(impactedRecords.length, 0);
} finally {
yield collection.db.close();
}
});
add_task(clear_collection);
add_task(function* test_loadDump_should_not_overwrite_records_without_last_modified(){
const collection = do_get_kinto_collection();
try {
yield collection.db.open();
const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f";
yield collection.create({id: recordId, title: "foo"}, {synced: true});
const record = {id: recordId, title: "bar", last_modified: 1457896541};
let impactedRecords = yield collection.loadDump([record]);
do_check_eq(impactedRecords.length, 0);
} finally {
yield collection.db.close();
}
});
add_task(clear_collection);
// Now do some sanity checks against a server - we're not looking to test
// core kinto.js functionality here (there is excellent test coverage in
// kinto.js), more making sure things are basically working as expected.
add_task(function* test_kinto_sync(){
const configPath = "/v1/";
const recordsPath = "/v1/buckets/default/collections/test_collection/records";
// register a handler
function handleResponse (request, response) {
try {
const sampled = getSampleResponse(request, server.identity.primaryPort);
if (!sampled) {
do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
}
response.setStatusLine(null, sampled.status.status,
sampled.status.statusText);
// send the headers
for (let headerLine of sampled.sampleHeaders) {
let headerElements = headerLine.split(':');
response.setHeader(headerElements[0], headerElements[1].trimLeft());
}
response.setHeader("Date", (new Date()).toUTCString());
response.write(sampled.responseBody);
} catch (e) {
dump(`${e}\n`);
}
}
server.registerPathHandler(configPath, handleResponse);
server.registerPathHandler(recordsPath, handleResponse);
// create an empty collection, sync to populate
const collection = do_get_kinto_collection();
try {
let result;
yield collection.db.open();
result = yield collection.sync();
do_check_true(result.ok);
// our test data has a single record; it should be in the local collection
let list = yield collection.list();
do_check_eq(list.data.length, 1);
// now sync again; we should now have 2 records
result = yield collection.sync();
do_check_true(result.ok);
list = yield collection.list();
do_check_eq(list.data.length, 2);
// sync again; the second records should have been modified
const before = list.data[0].title;
result = yield collection.sync();
do_check_true(result.ok);
list = yield collection.list();
const after = list.data[0].title;
do_check_neq(before, after);
} finally {
yield collection.db.close();
}
});
function run_test() {
// Set up an HTTP Server
server = new HttpServer();
server.start(-1);
run_next_test();
do_register_cleanup(function() {
server.stop(function() { });
});
}
// get a response for a given request from sample data
function getSampleResponse(req, port) {
const responses = {
"OPTIONS": {
"sampleHeaders": [
"Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
"Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
"Access-Control-Allow-Origin: *",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress"
],
"status": {status: 200, statusText: "OK"},
"responseBody": "null"
},
"GET:/v1/?": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress"
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
},
"GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"1445606341071\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{"last_modified":1445606341071, "done":false, "id":"68db8313-686e-4fff-835e-07d78ad6f2af", "title":"New test"}]})
},
"GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445606341071": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"1445607941223\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{"last_modified":1445607941223, "done":false, "id":"901967b0-f729-4b30-8d8d-499cba7f4b1d", "title":"Another new test"}]})
},
"GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445607941223": {
"sampleHeaders": [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
"Etag: \"1445607541265\""
],
"status": {status: 200, statusText: "OK"},
"responseBody": JSON.stringify({"data":[{"last_modified":1445607541265, "done":false, "id":"901967b0-f729-4b30-8d8d-499cba7f4b1d", "title":"Modified title"}]})
}
};
return responses[`${req.method}:${req.path}?${req.queryString}`] ||
responses[req.method];
}

View file

@ -9,14 +9,6 @@ support-files =
# Test load modules first so syntax failures are caught early.
[test_load_modules.js]
[test_blocklist_certificates.js]
[test_blocklist_clients.js]
[test_blocklist_updater.js]
[test_kinto.js]
[test_blocklist_signatures.js]
[test_storage_adapter.js]
[test_utils_atob.js]
[test_utils_convert_string.js]
[test_utils_dateprefs.js]