/*
 * Description of the test:
 *   Check that HSTS priming occurs correctly with mixed content
 *
 *   This test uses three hostnames, each of which treats an HSTS priming
 *   request differently.
 *   * no-ssl never returns an ssl response
 *   * reject-upgrade returns an ssl response, but with no STS header
 *   * prime-hsts returns an ssl response with the appropriate STS header
 *
 *   For each server, test that it response appropriately when the we allow
 *   or block active or display content, as well as when we send an hsts priming
 *   request, but do not change the order of mixed-content and HSTS.
 *
 *   This test uses http-on-examine-response, so must be run in browser context.
 */
'use strict';

var TOP_URI = "https://example.com/browser/dom/security/test/hsts/file_priming-top.html";

var test_servers = {
  // a test server that does not support TLS
  'no-ssl': {
    host: 'example.co.jp',
    response: false,
    id: 'no-ssl',
  },
  // a test server which does not support STS upgrade
  'reject-upgrade': {
    host: 'example.org',
    response: true,
    id: 'reject-upgrade',
  },
  // a test server when sends an STS header when priming
  'prime-hsts': {
    host: 'test1.example.com',
    response: true,
    id: 'prime-hsts'
  },
};
// The number of priming responses we expect to see
var priming_count = 2;

var test_settings = {
  // mixed active content is allowed, priming will upgrade
  allow_active: {
    block_active: false,
    block_display: false,
    use_hsts: true,
    send_hsts_priming: true,
    type: 'script',
    result: {
      'no-ssl': 'insecure',
      'reject-upgrade': 'insecure',
      'prime-hsts': 'secure',
    },
  },
  // mixed active content is blocked, priming will upgrade
  block_active: {
    block_active: true,
    block_display: false,
    use_hsts: true,
    send_hsts_priming: true,
    type: 'script',
    result: {
      'no-ssl': 'blocked',
      'reject-upgrade': 'blocked',
      'prime-hsts': 'secure',
    },
  },
  // keep the original order of mixed-content and HSTS, but send
  // priming requests
  hsts_after_mixed: {
    block_active: true,
    block_display: false,
    use_hsts: false,
    send_hsts_priming: true,
    type: 'script',
    result: {
      'no-ssl': 'blocked',
      'reject-upgrade': 'blocked',
      'prime-hsts': 'blocked',
    },
  },
  // mixed display content is allowed, priming will upgrade
  allow_display: {
    block_active: true,
    block_display: false,
    use_hsts: true,
    send_hsts_priming: true,
    type: 'img',
    result: {
      'no-ssl': 'insecure',
      'reject-upgrade': 'insecure',
      'prime-hsts': 'secure',
    },
  },
  // mixed display content is blocked, priming will upgrade
  block_display: {
    block_active: true,
    block_display: true,
    use_hsts: true,
    send_hsts_priming: true,
    type: 'img',
    result: {
      'no-ssl': 'blocked',
      'reject-upgrade': 'blocked',
      'prime-hsts': 'secure',
    },
  },
  // mixed active content is blocked, priming will upgrade (css)
  block_active_css: {
    block_active: true,
    block_display: false,
    use_hsts: true,
    send_hsts_priming: true,
    type: 'css',
    result: {
      'no-ssl': 'blocked',
      'reject-upgrade': 'blocked',
      'prime-hsts': 'secure',
    },
  },
  // mixed active content is blocked, priming will upgrade
  // redirect to the same host
  block_active_with_redir_same: {
    block_active: true,
    block_display: false,
    use_hsts: true,
    send_hsts_priming: true,
    type: 'script',
    redir: 'same',
    result: {
      'no-ssl': 'blocked',
      'reject-upgrade': 'blocked',
      'prime-hsts': 'secure',
    },
  },
}
// track which test we are on
var which_test = "";

const Observer = {
  observe: function (subject, topic, data) {
    switch (topic) {
      case 'console-api-log-event':
        return Observer.console_api_log_event(subject, topic, data);
      case 'http-on-examine-response':
        return Observer.http_on_examine_response(subject, topic, data);
    }
    throw "Can't handle topic "+topic;
  },
  // When a load is blocked which results in an error event within a page, the
  // test logs to the console.
  console_api_log_event: function (subject, topic, data) {
    var message = subject.wrappedJSObject.arguments[0];
    // when we are blocked, this will match the message we sent to the console,
    // ignore everything else.
    var re = RegExp(/^HSTS_PRIMING: Blocked ([-\w]+).*$/);
    if (!re.test(message)) {
      return;
    }

    let id = message.replace(re, '$1');
    let curTest =test_servers[id];

    if (!curTest) {
      ok(false, "HSTS priming got a console message blocked, "+
          "but doesn't match expectations "+id+" (msg="+message);
      return;
    }

    is("blocked", test_settings[which_test].result[curTest.id], "HSTS priming "+
        which_test+":"+curTest.id+" expected "+
        test_settings[which_test].result[curTest.id]+", got blocked");
    test_settings[which_test].finished[curTest.id] = "blocked";
  },
  // When we see a response come back, peek at the response and test it is secure
  // or insecure as needed. Addtionally, watch the response for priming requests.
  http_on_examine_response: function (subject, topic, data) {
    let curTest = null;
    let channel = subject.QueryInterface(Ci.nsIHttpChannel);
    for (let item in test_servers) {
      let re = RegExp('https?://'+test_servers[item].host);
      if (re.test(channel.URI.asciiSpec)) {
        curTest = test_servers[item];
        break;
      }
    }

    if (!curTest) {
      return;
    }

    let result = (channel.URI.asciiSpec.startsWith('https:')) ? "secure" : "insecure";

    // This is a priming request, go ahead and validate we were supposed to see
    // a response from the server
    if (channel.requestMethod == 'HEAD') {
      is(true, curTest.response, "HSTS priming response found " + curTest.id);
      test_settings[which_test].priming[curTest.id] = true;
      return;
    }

    // This is the response to our query, make sure it matches
    is(result, test_settings[which_test].result[curTest.id],
        "HSTS priming result " + which_test + ":" + curTest.id);
    test_settings[which_test].finished[curTest.id] = result;
  },
};

// opens `uri' in a new tab and focuses it.
// returns the newly opened tab
function openTab(uri) {
  let tab = gBrowser.addTab(uri);

  // select tab and make sure its browser is focused
  gBrowser.selectedTab = tab;
  tab.ownerDocument.defaultView.focus();

  return tab;
}

function clear_sts_data() {
  for (let test in test_servers) {
    SpecialPowers.cleanUpSTSData('http://'+test_servers[test].host);
  }
}

function do_cleanup() {
  clear_sts_data();

  Services.obs.removeObserver(Observer, "console-api-log-event");
  Services.obs.removeObserver(Observer, "http-on-examine-response");
}

function SetupPrefTestEnvironment(which) {
  which_test = which;
  clear_sts_data();

  var settings = test_settings[which];
  // priming counts how many priming requests we saw
  settings.priming = {};
  // priming counts how many tests were finished
  settings.finished= {};

  SpecialPowers.pushPrefEnv({'set': [["security.mixed_content.block_active_content",
                                      settings.block_active],
                                     ["security.mixed_content.block_display_content",
                                      settings.block_display],
                                     ["security.mixed_content.use_hsts",
                                      settings.use_hsts],
                                     ["security.mixed_content.send_hsts_priming",
                                      settings.send_hsts_priming]]});
}

// make the top-level test uri
function build_test_uri(base_uri, host, test_id, type) {
  return base_uri +
          "?host=" + escape(host) +
          "&id=" + escape(test_id) +
          "&type=" + escape(type);
}

// open a new tab, load the test, and wait for it to finish
function execute_test(test, mimetype) {
  var src = build_test_uri(TOP_URI, test_servers[test].host,
      test, test_settings[which_test].type);

  let tab = openTab(src);
  test_servers[test]['tab'] = tab;

  let browser = gBrowser.getBrowserForTab(tab);
  yield BrowserTestUtils.browserLoaded(browser);

  yield BrowserTestUtils.removeTab(tab);
}

//jscs:disable
add_task(function*() {
  //jscs:enable
  Services.obs.addObserver(Observer, "console-api-log-event", false);
  Services.obs.addObserver(Observer, "http-on-examine-response", false);
  registerCleanupFunction(do_cleanup);
  requestLongerTimeout(4);

  for (let which of Object.keys(test_settings)) {
    SetupPrefTestEnvironment(which);

    for (let server of Object.keys(test_servers)) {
      yield execute_test(server, test_settings[which].mimetype);
    }

    SpecialPowers.popPrefEnv();
  }
});
