<!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">

<script class="testbody" type="text/javascript">

const MALWARE_LIST = "test-malware-simple";
const MALWARE_HOST1 = "malware.example.com/";
const MALWARE_HOST2 = "test1.example.com/";

const UNWANTED_LIST = "test-unwanted-simple";
const UNWANTED_HOST1 = "unwanted.example.com/";
const UNWANTED_HOST2 = "test2.example.com/";


const UNUSED_MALWARE_HOST = "unused.malware.com/";
const UNUSED_UNWANTED_HOST = "unused.unwanted.com/";

const GETHASH_URL =
  "http://mochi.test:8888/tests/toolkit/components/url-classifier/tests/mochitest/gethash.sjs";

var gPreGethashCounter = 0;
var gCurGethashCounter = 0;

var expectLoad = false;

function loadTestFrame() {
  return new Promise(function(resolve, reject) {
    var iframe = document.createElement("iframe");
    iframe.setAttribute("src", "gethashFrame.html");
    document.body.appendChild(iframe);

    iframe.onload = function() {
      document.body.removeChild(iframe);
      resolve();
    };
  }).then(getGethashCounter);
}

function getGethashCounter() {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest;
    xhr.open("PUT", GETHASH_URL + "?gethashcount");
    xhr.setRequestHeader("Content-Type", "text/plain");
    xhr.onreadystatechange = function() {
      if (this.readyState == this.DONE) {
        gPreGethashCounter = gCurGethashCounter;
        gCurGethashCounter = parseInt(xhr.response);
        resolve();
      }
    };
    xhr.send();
  });
}

// 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);
}

// setup function allows classifier send gethash request for test database
// also it calculate to fullhash for url and store those hashes in gethash sjs.
function setup() {
  classifierHelper.allowCompletion([MALWARE_LIST, UNWANTED_LIST], GETHASH_URL);

  return Promise.all([
    addCompletionToServer(MALWARE_LIST, MALWARE_HOST1),
    addCompletionToServer(MALWARE_LIST, MALWARE_HOST2),
    addCompletionToServer(UNWANTED_LIST, UNWANTED_HOST1),
    addCompletionToServer(UNWANTED_LIST, UNWANTED_HOST2),
  ]);
}

// Reset function in helper try to simulate the behavior we restart firefox
function reset() {
  return classifierHelper.resetDB()
    .catch(err => {
      ok(false, "Couldn't update classifier. Error code: " + errorCode);
      // Abort test.
      SimpleTest.finish();
    });
}

function updateUnusedUrl() {
  var testData  = [
    { url: UNUSED_MALWARE_HOST,  db: MALWARE_LIST },
    { url: UNUSED_UNWANTED_HOST, db: UNWANTED_LIST }
  ];

  return classifierHelper.addUrlToDB(testData)
    .catch(err => {
      ok(false, "Couldn't update classifier. Error code: " + err);
      // Abort test.
      SimpleTest.finish();
    });
}

function addPrefixToDB() {
  return update(true);
}

function addCompletionToDB() {
  return update(false);
}

function update(prefix = false) {
  var length = prefix ? 4 : 32;
  var testData  = [
    { url: MALWARE_HOST1,  db: MALWARE_LIST,  len: length },
    { url: MALWARE_HOST2,  db: MALWARE_LIST,  len: length },
    { url: UNWANTED_HOST1, db: UNWANTED_LIST, len: length },
    { url: UNWANTED_HOST2, db: UNWANTED_LIST, len: length }
  ];

  return classifierHelper.addUrlToDB(testData)
    .catch(err => {
      ok(false, "Couldn't update classifier. Error code: " + errorCode);
      // Abort test.
      SimpleTest.finish();
    });
}

// This testcase is to make sure gethash works:
// 1. Add prefixes to DB.
// 2. Load test frame contains malware & unwanted url, those urls should be blocked.
// 3. The second step should also trigger a gethash request since completions is not in
//    either cache or DB.
// 4. Load test frame again, since completions is stored in cache now, no gethash
//    request should be triggered.
function testGethash() {
  return Promise.resolve()
    .then(addPrefixToDB)
    .then(loadTestFrame)
    .then(() => {
       ok(gCurGethashCounter > gPreGethashCounter, "Gethash request is triggered."); })
    .then(loadTestFrame)
    .then(() => {
      ok(gCurGethashCounter == gPreGethashCounter, "Gethash request is not triggered."); })
    .then(reset);
}

// This testcase is to make sure an update request will clear completion cache:
// 1. Add prefixes to DB.
// 2. Load test frame, this should trigger a gethash request
// 3. Trigger an update, completion cache should be cleared now.
// 4. Load test frame again, since cache is cleared now, gethash request should be triggered.
function testUpdateClearCache() {
  return Promise.resolve()
    .then(addPrefixToDB)
    .then(loadTestFrame)
    .then(() => {
      ok(gCurGethashCounter > gPreGethashCounter, "Gethash request is triggered."); })
    .then(updateUnusedUrl)
    .then(loadTestFrame)
    .then(() => {
      ok(gCurGethashCounter > gPreGethashCounter, "Gethash request is triggered."); })
    .then(reset);
}

// This testcae is to make sure completions in update works:
// 1. Add completions to DB.
// 2. Load test frame, since completions is stored in DB, gethash request should
//    not be triggered.
function testUpdate() {
  return Promise.resolve()
    .then(addCompletionToDB)
    .then(loadTestFrame)
    .then(() => {
      ok(gCurGethashCounter == gPreGethashCounter, "Gethash request is not triggered."); })
    .then(reset);
}

// This testcase is to make sure an update request will not clear completions in DB:
// 1. Add completions to DB.
// 2. Load test frame to make sure completions is stored in database, in this case, gethash
//    should not be triggered.
// 3. Trigger an update, cache is cleared, but completions in DB should still remain.
// 4. Load test frame again, since completions is in DB, gethash request should not be triggered.
function testUpdateNotClearCompletions() {
  return Promise.resolve()
    .then(addCompletionToDB)
    .then(loadTestFrame)
    .then(() => {
      ok(gCurGethashCounter == gPreGethashCounter, "Gethash request is not triggered."); })
    .then(updateUnusedUrl)
    .then(loadTestFrame)
    .then(() => {
      ok(gCurGethashCounter == gPreGethashCounter, "Gethash request is not triggered."); })
    .then(reset);
}

// This testcase is to make sure completion store in DB will properly load after restarting.
// 1. Add completions to DB.
// 2. Simulate firefox restart by calling reloadDatabase.
// 3. Load test frame, since completions should be loaded from DB, no gethash request should
//    be triggered.
function testUpdateCompletionsAfterReload() {
  return Promise.resolve()
    .then(addCompletionToDB)
    .then(classifierHelper.reloadDatabase)
    .then(loadTestFrame)
    .then(() => {
      ok(gCurGethashCounter == gPreGethashCounter, "Gethash request is not triggered."); })
    .then(reset);
}

// This testcase is to make sure cache will be cleared after restarting
// 1. Add prefixes to DB.
// 2. Load test frame, this should trigger a gethash request and completions will be stored in
//    cache.
// 3. Load test frame again, no gethash should be triggered because of cache.
// 4. Simulate firefox restart by calling reloadDatabase.
// 5. Load test frame again, since cache is cleared, gethash request should be triggered.
function testGethashCompletionsAfterReload() {
  return Promise.resolve()
    .then(addPrefixToDB)
    .then(loadTestFrame)
    .then(() => {
       ok(gCurGethashCounter > gPreGethashCounter, "Gethash request is triggered."); })
    .then(loadTestFrame)
    .then(() => {
      ok(gCurGethashCounter == gPreGethashCounter, "Gethash request is not triggered."); })
    .then(classifierHelper.reloadDatabase)
    .then(loadTestFrame)
    .then(() => {
       ok(gCurGethashCounter > gPreGethashCounter, "Gethash request is triggered."); })
    .then(reset);
}

function runTest() {
  Promise.resolve()
    .then(classifierHelper.waitForInit)
    .then(setup)
    .then(testGethash)
    .then(testUpdateClearCache)
    .then(testUpdate)
    .then(testUpdateNotClearCompletions)
    .then(testUpdateCompletionsAfterReload)
    .then(testGethashCompletionsAfterReload)
    .then(function() {
      SimpleTest.finish();
    }).catch(function(e) {
      ok(false, "Some test failed with error " + e);
      SimpleTest.finish();
    });
}

SimpleTest.waitForExplicitFinish();

// 'network.predictor.enabled' is disabled because if other testcase load
// evil.js, evil.css ...etc resources, it may cause we load them from cache
// directly and bypass classifier check
SpecialPowers.pushPrefEnv({"set": [
  ["browser.safebrowsing.malware.enabled", true],
  ["network.predictor.enabled", false],
  ["urlclassifier.gethash.timeout_ms", 30000],
]}, runTest);

</script>
</pre>
</body>
</html>