<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=786347
-->
<head>
  <meta charset="utf-8">
  <title>Test for Bug 786347</title>
  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
  <script type="application/javascript">

  /** Test for Bug 786347 **/

SimpleTest.waitForExplicitFinish();

const {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
const {HttpServer} = ChromeUtils.import("resource://testing-common/httpd.js");

addLoadEvent(function () {
  (async function run_tests() {
    while (tests.length) {
      let test = tests.shift();
      info("-- running " + test.name);
      await test();
    }

    SimpleTest.finish();
  })();
});

let tests = [

  // Ensure that sending pings is enabled.
  function setup() {
    Services.prefs.setBoolPref("browser.send_pings", true);
    Services.prefs.setIntPref("browser.send_pings.max_per_link", -1);

    SimpleTest.registerCleanupFunction(() => {
      Services.prefs.clearUserPref("browser.send_pings");
      Services.prefs.clearUserPref("browser.send_pings.max_per_link");
    });
  },

  // If both the address of the document containing the hyperlink being audited
  // and ping URL have the same origin then the request must include a Ping-From
  // HTTP header with, as its value, the address of the document containing the
  // hyperlink, and a Ping-To HTTP header with, as its value, the target URL.
  // The request must not include a Referer (sic) HTTP header.
  async function same_origin() {
    let from = "/ping-from/" + Math.random();
    let to = "/ping-to/" + Math.random();
    let ping = "/ping/" + Math.random();

    let base;
    let server = new HttpServer();

    // The page that contains the link.
    createFromPathHandler(server, from, to, () => ping);

    // The page that the link's href points to.
    let promiseHref = createToPathHandler(server, to);

    // The ping we want to receive.
    let promisePing = createPingPathHandler(server, ping, () => {
      return {from: base + from, to: base + to};
    });

    // Start the server, get its base URL and run the test.
    server.start(-1);
    base = "http://localhost:" + server.identity.primaryPort;
    navigate(base + from);

    // Wait until the target and ping url have loaded.
    await Promise.all([promiseHref, promisePing]);

    // Cleanup.
    await stopServer(server);
  },

  // If the origins are different, but the document containing the hyperlink
  // being audited was not retrieved over an encrypted connection then the
  // request must include a Referer (sic) HTTP header with, as its value, the
  // address of the document containing the hyperlink, a Ping-From HTTP header
  // with the same value, and a Ping-To HTTP header with, as its value, target
  // URL.
  async function diff_origin() {
    let from = "/ping-from/" + Math.random();
    let to = "/ping-to/" + Math.random();
    let ping = "/ping/" + Math.random();

    // We will use two servers to simulate two different origins.
    let base, base2;
    let server = new HttpServer();
    let server2 = new HttpServer();

    // The page that contains the link.
    createFromPathHandler(server, from, to, () => base2 + ping);

    // The page that the link's href points to.
    let promiseHref = createToPathHandler(server, to);

    // Start the first server and get its base URL.
    server.start(-1);
    base = "http://localhost:" + server.identity.primaryPort;

    // The ping we want to receive.
    let promisePing = createPingPathHandler(server2, ping, () => {
      return {referrer: base + from, from: base + from, to: base + to};
    });

    // Start the second server, get its base URL and run the test.
    server2.start(-1);
    base2 = "http://localhost:" + server2.identity.primaryPort;
    navigate(base + from);

    // Wait until the target and ping url have loaded.
    await Promise.all([promiseHref, promisePing]);

    // Cleanup.
    await stopServer(server);
    await stopServer(server2);
  },

  // If the origins are different and the document containing the hyperlink
  // being audited was retrieved over an encrypted connection then the request
  // must include a Ping-To HTTP header with, as its value, target URL. The
  // request must neither include a Referer (sic) HTTP header nor include a
  // Ping-From HTTP header.
  async function diff_origin_secure_referrer() {
    let ping = "/ping/" + Math.random();
    let server = new HttpServer();

    // The ping we want to receive.
    let promisePing = createPingPathHandler(server, ping, () => {
      return {to: "https://example.com/"};
    });

    // Start the server and run the test.
    server.start(-1);

    // The referrer will be loaded using a secure channel.
    navigate("https://example.com/chrome/dom/html/test/" +
             "file_anchor_ping.html?" + "http://127.0.0.1:" +
             server.identity.primaryPort + ping);

    // Wait until the ping has been sent.
    await promisePing;

    // Cleanup.
    await stopServer(server);
  },

  // Test that the <a ping> attribute is properly tokenized using ASCII white
  // space characters as separators.
  async function tokenize_white_space() {
    let from = "/ping-from/" + Math.random();
    let to = "/ping-to/" + Math.random();

    let base;
    let server = new HttpServer();

    let pings = [
      "/ping1/" + Math.random(),
      "/ping2/" + Math.random(),
      "/ping3/" + Math.random(),
      "/ping4/" + Math.random()
    ];

    // The page that contains the link.
    createFromPathHandler(server, from, to, () => {
      return " " + pings[0] + " \r " + pings[1] + " \t " +
                   pings[2] + " \n " + pings[3] + "   ";
    });

    // The page that the link's href points to.
    let promiseHref = createToPathHandler(server, to);

    // The pings we want to receive.
    let pingPathHandlers = createPingPathHandlers(server, pings, () => {
      return {from: base + from, to: base + to};
    });

    // Start the server, get its base URL and run the test.
    server.start(-1);
    base = "http://localhost:" + server.identity.primaryPort;
    navigate(base + from);

    // Wait until the target and ping url have loaded.
    await Promise.all([promiseHref, ...pingPathHandlers]);

    // Cleanup.
    await stopServer(server);
  }
];

// Navigate the iframe used for testing to a new URL.
function navigate(uri) {
  document.getElementById("frame").src = uri;
}

// Registers a path handler for the given server that will serve a page
// containing an <a ping> element. The page will automatically simulate
// clicking the link after it has loaded.
function createFromPathHandler(server, path, href, lazyPing) {
  server.registerPathHandler(path, function (request, response) {
    response.setStatusLine(request.httpVersion, 200, "OK");
    response.setHeader("Content-Type", "text/html;charset=utf-8", false);
    response.setHeader("Cache-Control", "no-cache", false);

    let body = '<body onload="document.body.firstChild.click()">' +
               '<a href="' + href + '" ping="' + lazyPing() + '"></a></body>';
    response.write(body);
  });
}

// Registers a path handler for the given server that will serve a simple empty
// page we can use as the href attribute for links. It returns a promise that
// will be resolved once the page has been requested.
function createToPathHandler(server, path) {
  return new Promise(resolve => {

    server.registerPathHandler(path, function (request, response) {
      response.setStatusLine(request.httpVersion, 200, "OK");
      response.setHeader("Content-Type", "text/html;charset=utf-8", false);
      response.setHeader("Cache-Control", "no-cache", false);
      response.write("OK");

      resolve();
    });

  });
}

// Register multiple path handlers for the given server that will receive
// pings as sent when an <a ping> element is clicked. This method uses
// createPingPathHandler() defined below to ensure all headers are sent
// and received as expected.
function createPingPathHandlers(server, paths, lazyHeaders) {
  return Array.from(paths, (path) => createPingPathHandler(server, path, lazyHeaders));
}

// Registers a path handler for the given server that will receive pings as
// sent when an <a ping> element has been clicked. It will check that the
// correct http method has been used, the post data is correct and all headers
// are given as expected. It returns a promise that will be resolved once the
// ping has been received.
function createPingPathHandler(server, path, lazyHeaders) {
  return new Promise(resolve => {

    server.registerPathHandler(path, function (request, response) {
      let headers = lazyHeaders();

      is(request.method, "POST", "correct http method used");
      is(request.getHeader("Ping-To"), headers.to, "valid ping-to header");

      if ("from" in headers) {
        is(request.getHeader("Ping-From"), headers.from, "valid ping-from header");
      } else {
        ok(!request.hasHeader("Ping-From"), "no ping-from header");
      }

      if ("referrer" in headers) {
        let expectedReferrer = headers.referrer.match(/https?:\/\/[^\/]+\/?/i)[0];
        is(request.getHeader("Referer"), expectedReferrer, "valid referer header");
      } else {
        ok(!request.hasHeader("Referer"), "no referer header");
      }

      let bs = request.bodyInputStream;
      let body = NetUtil.readInputStreamToString(bs, bs.available());
      is(body, "PING", "correct body sent");

      response.setStatusLine(request.httpVersion, 200, "OK");
      response.setHeader("Content-Type", "text/html;charset=utf-8", false);
      response.setHeader("Cache-Control", "no-cache", false);
      response.write("OK");

      resolve();
    });

  });
}

// Returns a promise that is resolved when the given http server instance has
// been stopped.
function stopServer(server) {
  return new Promise(resolve => {
    server.stop(resolve);
  });
}

  </script>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=786347">Mozilla Bug 786347</a>
<p id="display"></p>
<iframe id="frame" />
</body>
</html>
