Bug 1272239 - Part 3: Testcase, test gethash. r=francois

MozReview-Commit-ID: 3IkrdJgZNP1
This commit is contained in:
dimi 2016-07-21 15:40:03 +08:00
parent 4135954a00
commit 4f45ea248f
8 changed files with 383 additions and 4 deletions

View File

@ -0,0 +1 @@
#styleBad { visibility: hidden; }

View File

@ -15,6 +15,14 @@ function checkLoads() {
var style = document.defaultView.getComputedStyle(elt, "");
window.parent.isnot(style.visibility, "hidden", "Should not load bad css");
elt = document.getElementById("styleBad");
style = document.defaultView.getComputedStyle(elt, "");
window.parent.isnot(style.visibility, "hidden", "Should not load bad css");
elt = document.getElementById("styleImport");
style = document.defaultView.getComputedStyle(elt, "");
window.parent.isnot(style.visibility, "visible", "Should import clean css");
// Call parent.loadTestFrame again to test classification metadata in HTTP
// cache entries.
if (window.parent.firstLoad) {
@ -36,7 +44,6 @@ function checkLoads() {
<!-- Try loading from an uwanted software css URI -->
<link rel="stylesheet" type="text/css" href="http://unwanted.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.css"></link>
<!-- XXX How is this part of the test supposed to work (= be checked)? -->
<!-- Try loading a marked-as-malware css through an @import from a clean URI -->
<link rel="stylesheet" type="text/css" href="import.css"></link>
</head>
@ -44,5 +51,7 @@ function checkLoads() {
<body onload="checkLoads()">
The following should not be hidden:
<div id="styleCheck">STYLE TEST</div>
<div id="styleBad">STYLE BAD</div>
<div id="styleImport">STYLE IMPORT</div>
</body>
</html>

View File

@ -9,6 +9,12 @@ const ADD_CHUNKNUM = 524;
const SUB_CHUNKNUM = 523;
const HASHLEN = 32;
const PREFS = {
PROVIDER_LISTS : "browser.safebrowsing.provider.mozilla.lists",
DISALLOW_COMPLETIONS : "urlclassifier.disallow_completions",
PROVIDER_GETHASHURL : "browser.safebrowsing.provider.mozilla.gethashURL"
};
// addUrlToDB & removeUrlFromDB are asynchronous, queue the task to ensure
// the callback follow correct order.
classifierHelper._updates = [];
@ -17,6 +23,27 @@ classifierHelper._updates = [];
// removed after test complete.
classifierHelper._updatesToCleanup = [];
// This function is used to allow completion for specific "list",
// some lists like "test-malware-simple" is default disabled to ask for complete.
// "list" is the db we would like to allow it
// "url" is the completion server
classifierHelper.allowCompletion = function(lists, url) {
for (var list of lists) {
// Add test db to provider
var pref = SpecialPowers.getCharPref(PREFS.PROVIDER_LISTS);
pref += "," + list;
SpecialPowers.setCharPref(PREFS.PROVIDER_LISTS, pref);
// Rename test db so we will not disallow it from completions
pref = SpecialPowers.getCharPref(PREFS.DISALLOW_COMPLETIONS);
pref = pref.replace(list, list + "-backup");
SpecialPowers.setCharPref(PREFS.DISALLOW_COMPLETIONS, pref);
}
// Set get hash url
SpecialPowers.setCharPref(PREFS.PROVIDER_GETHASHURL, url);
}
// Pass { url: ..., db: ... } to add url to database,
// onsuccess/onerror will be called when update complete.
classifierHelper.addUrlToDB = function(updateData) {
@ -26,6 +53,7 @@ classifierHelper.addUrlToDB = function(updateData) {
var LISTNAME = update.db;
var CHUNKDATA = update.url;
var CHUNKLEN = CHUNKDATA.length;
var HASHLEN = update.len ? update.len : 32;
classifierHelper._updatesToCleanup.push(update);
testUpdate +=
@ -49,6 +77,7 @@ classifierHelper.removeUrlFromDB = function(updateData) {
var LISTNAME = update.db;
var CHUNKDATA = ADD_CHUNKNUM + ":" + update.url;
var CHUNKLEN = CHUNKDATA.length;
var HASHLEN = update.len ? update.len : 32;
testUpdate +=
"n:1000\n" +
@ -127,6 +156,11 @@ classifierHelper._setup = function() {
};
classifierHelper._cleanup = function() {
// clean all the preferences may touch by helper
for (var pref in PREFS) {
SpecialPowers.clearUserPref(pref);
}
if (!classifierHelper._updatesToCleanup) {
return Promise.resolve();
}

View File

@ -0,0 +1,119 @@
const CC = Components.Constructor;
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream",
"setInputStream");
function handleRequest(request, response)
{
var query = {};
request.queryString.split('&').forEach(function (val) {
var idx = val.indexOf('=');
query[val.slice(0, idx)] = unescape(val.slice(idx + 1));
});
// Store fullhash in the server side.
if ("list" in query && "fullhash" in query) {
// In the server side we will store:
// 1. All the full hashes for a given list
// 2. All the lists we have right now
// data is separate by '\n'
let list = query["list"];
let hashes = getState(list);
let hash = base64ToString(query["fullhash"]);
hashes += hash + "\n";
setState(list, hashes);
let lists = getState("lists");
if (lists.indexOf(list) == -1) {
lists += list + "\n";
setState("lists", lists);
}
return;
}
var body = new BinaryInputStream(request.bodyInputStream);
var avail;
var bytes = [];
while ((avail = body.available()) > 0) {
Array.prototype.push.apply(bytes, body.readByteArray(avail));
}
var responseBody = parseV2Request(bytes);
response.setHeader("Content-Type", "text/plain", false);
response.write(responseBody);
}
function parseV2Request(bytes) {
var request = String.fromCharCode.apply(this, bytes);
var [HEADER, PREFIXES] = request.split("\n");
var [PREFIXSIZE, LENGTH] = HEADER.split(":").map(val => {
return parseInt(val);
});
var ret = "";
for(var start = 0; start < LENGTH; start += PREFIXSIZE) {
getState("lists").split("\n").forEach(function(list) {
var completions = getState(list).split("\n");
for (var completion of completions) {
if (completion.indexOf(PREFIXES.substr(start, PREFIXSIZE)) == 0) {
ret += list + ":" + "1" + ":" + "32" + "\n";
ret += completion;
}
}
});
}
return ret;
}
/* Convert Base64 data to a string */
const toBinaryTable = [
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
-1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
];
const base64Pad = '=';
function base64ToString(data) {
var result = '';
var leftbits = 0; // number of bits decoded, but yet to be appended
var leftdata = 0; // bits decoded, but yet to be appended
// Convert one by one.
for (var i = 0; i < data.length; i++) {
var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
var padding = (data[i] == base64Pad);
// Skip illegal characters and whitespace
if (c == -1) continue;
// Collect data into leftdata, update bitcount
leftdata = (leftdata << 6) | c;
leftbits += 6;
// If we have 8 or more bits, append 8 bits to the result
if (leftbits >= 8) {
leftbits -= 8;
// Append if not padding.
if (!padding)
result += String.fromCharCode((leftdata >> leftbits) & 0xff);
leftdata &= (1 << leftbits) - 1;
}
}
// If there are any bits left, the base64 string was corrupted
if (leftbits)
throw Components.Exception('Corrupted base64 string');
return result;
}

View File

@ -0,0 +1,62 @@
<html>
<head>
<title></title>
<script type="text/javascript">
var scriptItem = "untouched";
function checkLoads() {
var title = document.getElementById("title");
title.innerHTML = window.parent.shouldLoad ?
"The following should be hidden:" :
"The following should not be hidden:"
if (window.parent.shouldLoad) {
window.parent.is(scriptItem, "loaded malware javascript!", "Should load bad javascript");
} else {
window.parent.is(scriptItem, "untouched", "Should not load bad javascript");
}
var elt = document.getElementById("styleImport");
var style = document.defaultView.getComputedStyle(elt, "");
window.parent.isnot(style.visibility, "visible", "Should load clean css");
// Make sure the css did not load.
elt = document.getElementById("styleCheck");
style = document.defaultView.getComputedStyle(elt, "");
if (window.parent.shouldLoad) {
window.parent.isnot(style.visibility, "visible", "Should load bad css");
} else {
window.parent.isnot(style.visibility, "hidden", "Should not load bad css");
}
elt = document.getElementById("styleBad");
style = document.defaultView.getComputedStyle(elt, "");
if (window.parent.shouldLoad) {
window.parent.isnot(style.visibility, "visible", "Should import bad css");
} else {
window.parent.isnot(style.visibility, "hidden", "Should not import bad css");
}
}
</script>
<!-- Try loading from a malware javascript URI -->
<script type="text/javascript" src="http://malware.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js"></script>
<!-- Try loading from an uwanted software css URI -->
<link rel="stylesheet" type="text/css" href="http://unwanted.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.css"></link>
<!-- Try loading a marked-as-malware css through an @import from a clean URI -->
<link rel="stylesheet" type="text/css" href="import.css"></link>
</head>
<body onload="checkLoads()">
<div id="title"></div>
<div id="styleCheck">STYLE EVIL</div>
<div id="styleBad">STYLE BAD</div>
<div id="styleImport">STYLE IMPORT</div>
</body>
</html>

View File

@ -1,3 +1,3 @@
/* malware.example.com is in the malware database.
classifierBad.css does not actually exist. */
@import url("http://malware.example.com/tests/docshell/test/classifierBad.css");
/* malware.example.com is in the malware database. */
@import url("http://malware.example.com/tests/toolkit/components/url-classifier/tests/mochitest/bad.css");
#styleImport { visibility: hidden; }

View File

@ -23,9 +23,13 @@ support-files =
dnt.html
dnt.sjs
update.sjs
bad.css
gethash.sjs
gethashFrame.html
[test_classifier.html]
skip-if = (os == 'linux' && debug) #Bug 1199778
[test_classifier_worker.html]
[test_classify_ping.html]
[test_classify_track.html]
[test_gethash.html]

View File

@ -0,0 +1,150 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Bug 1272239 - Test gethash.</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="classifierHelper.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
<iframe id="testFrame1" onload=""></iframe>
<iframe id="testFrame2" onload=""></iframe>
<script class="testbody" type="text/javascript">
const MALWARE_LIST = "test-malware-simple";
const MALWARE_HOST = "malware.example.com/";
const UNWANTED_LIST = "test-unwanted-simple";
const UNWANTED_HOST = "unwanted.example.com/";
const GETHASH_URL = "http://mochi.test:8888/tests/toolkit/components/url-classifier/tests/mochitest/gethash.sjs";
const NOTEXIST_URL = "http://mochi.test:8888/tests/toolkit/components/url-classifier/tests/mochitest/nonexistserver.sjs";
var shouldLoad = false;
// In this testcase we store prefixes to localdb and send the fullhash to gethash server.
// When access the test page gecko should trigger gethash request to server and
// get the completion response.
function loadTestFrame(id) {
return new Promise(function(resolve, reject) {
var iframe = document.getElementById(id);
iframe.setAttribute("src", "gethashFrame.html");
iframe.onload = function() {
resolve();
};
});
}
// add 4-bytes prefixes to local database, so when we access the url,
// it will trigger gethash request.
function addPrefixToDB(list, url) {
var testData = [{ db: list, url: url, len: 4 }];
return classifierHelper.addUrlToDB(testData)
.catch(function(err) {
ok(false, "Couldn't update classifier. Error code: " + err);
// Abort test.
SimpleTest.finish();
});
}
// calculate the fullhash and send it to gethash server
function addCompletionToServer(list, url) {
return new Promise(function(resolve, reject) {
var listParam = "list=" + list;
var fullhashParam = "fullhash=" + hash(url);
var xhr = new XMLHttpRequest;
xhr.open("PUT", GETHASH_URL + "?" +
listParam + "&" +
fullhashParam, true);
xhr.setRequestHeader("Content-Type", "text/plain");
xhr.onreadystatechange = function() {
if (this.readyState == this.DONE) {
resolve();
}
};
xhr.send();
});
}
function hash(str) {
function bytesFromString(str) {
var converter =
SpecialPowers.Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(SpecialPowers.Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
return converter.convertToByteArray(str);
}
var hasher = SpecialPowers.Cc["@mozilla.org/security/hash;1"]
.createInstance(SpecialPowers.Ci.nsICryptoHash);
var data = bytesFromString(str);
hasher.init(hasher.SHA256);
hasher.update(data, data.length);
return hasher.finish(true);
}
function setup404() {
shouldLoad = true;
classifierHelper.allowCompletion([MALWARE_LIST, UNWANTED_LIST], NOTEXIST_URL);
return Promise.all([
addPrefixToDB(MALWARE_LIST, MALWARE_HOST),
addPrefixToDB(UNWANTED_LIST, UNWANTED_HOST)
]);
}
function setupCompletion() {
classifierHelper.allowCompletion([MALWARE_LIST, UNWANTED_LIST], GETHASH_URL);
return Promise.all([
addPrefixToDB(MALWARE_LIST, MALWARE_HOST),
addPrefixToDB(UNWANTED_LIST, UNWANTED_HOST),
addCompletionToServer(MALWARE_LIST, MALWARE_HOST),
addCompletionToServer(UNWANTED_LIST, UNWANTED_HOST),
]);
}
// manually reset DB to make sure next test won't be affected by cache.
function reset() {
return classifierHelper.resetDB;
}
function runTest() {
Promise.resolve()
// This test resources get blocked when gethash returns successfully
.then(setupCompletion)
.then(() => loadTestFrame("testFrame1"))
.then(reset)
// This test resources are not blocked when gethash returns an error
.then(setup404)
.then(() => loadTestFrame("testFrame2"))
.then(function() {
SimpleTest.finish();
}).catch(function(e) {
ok(false, "Some test failed with error " + e);
SimpleTest.finish();
});
}
SimpleTest.waitForExplicitFinish();
SpecialPowers.pushPrefEnv({"set": [
["browser.safebrowsing.malware.enabled", true]
]}, runTest);
</script>
</pre>
</body>
</html>