"use strict";

/**
 * This test tests various redirection scenarios, and checks whether sameSite
 * cookies are sent.
 *
 * The file has the following tests:
 * - verify_firstparty_web_behavior - base case, confirms normal web behavior.
 * - samesite_is_foreign_without_host_permissions
 * - wildcard_host_permissions_enable_samesite_cookies
 * - explicit_host_permissions_enable_samesite_cookies
 * - some_host_permissions_enable_some_samesite_cookies
 */

// This simulates a common pattern used for sites that require authentication.
// After logging in, there may be multiple redirects, HTTP and scripted.
const SITE_START = "start.example.net";
// set "start" cookies + 302 redirects to found.
const SITE_FOUND = "found.example.net";
// set "found" cookies + uses a HTML redirect to redir.
const SITE_REDIR = "redir.example.net";
// set "redir" cookies + 302 redirects to final.
const SITE_FINAL = "final.example.net";

const SITE = "example.net";

const URL_START = `http://${SITE_START}/start`;

const server = createHttpServer({
  hosts: [SITE_START, SITE_FOUND, SITE_REDIR, SITE_FINAL],
});

function getCookies(request) {
  return request.hasHeader("Cookie") ? request.getHeader("Cookie") : "";
}

function sendCookies(response, prefix, suffix = "") {
  const cookies = [
    prefix + "-none=1; sameSite=none; domain=" + SITE + suffix,
    prefix + "-lax=1; sameSite=lax; domain=" + SITE + suffix,
    prefix + "-strict=1; sameSite=strict; domain=" + SITE + suffix,
  ];
  for (let cookie of cookies) {
    response.setHeader("Set-Cookie", cookie, true);
  }
}

function deleteCookies(response, prefix) {
  sendCookies(response, prefix, "; expires=Thu, 01 Jan 1970 00:00:00 GMT");
}

var receivedCookies = [];

server.registerPathHandler("/start", (request, response) => {
  Assert.equal(request.host, SITE_START);
  Assert.equal(getCookies(request), "", "No cookies at start of test");

  response.setStatusLine(request.httpVersion, 302, "Found");
  sendCookies(response, "start");
  response.setHeader("Location", `http://${SITE_FOUND}/found`);
});

server.registerPathHandler("/found", (request, response) => {
  Assert.equal(request.host, SITE_FOUND);
  receivedCookies.push(getCookies(request));

  response.setHeader("Content-Type", "text/html; charset=utf-8", false);
  deleteCookies(response, "start");
  sendCookies(response, "found");
  response.write(`<script>location = "http://${SITE_REDIR}/redir";</script>`);
});

server.registerPathHandler("/redir", (request, response) => {
  Assert.equal(request.host, SITE_REDIR);
  receivedCookies.push(getCookies(request));

  response.setStatusLine(request.httpVersion, 302, "Found");
  deleteCookies(response, "found");
  sendCookies(response, "redir");
  response.setHeader("Location", `http://${SITE_FINAL}/final`);
});

server.registerPathHandler("/final", (request, response) => {
  Assert.equal(request.host, SITE_FINAL);
  receivedCookies.push(getCookies(request));

  response.setStatusLine(request.httpVersion, 302, "Found");
  deleteCookies(response, "redir");
  // In test some_host_permissions_enable_some_samesite_cookies, the cookies
  // from the start haven't been cleared due to the lack of host permissions.
  // Do that here instead.
  deleteCookies(response, "start");
  response.setHeader("Location", "/final_and_clean");
});

// Should be called before any request is made.
function promiseFinalResponse() {
  Assert.deepEqual(receivedCookies, [], "Test starts without observed cookies");
  return new Promise(resolve => {
    server.registerPathHandler("/final_and_clean", (request, response) => {
      Assert.equal(request.host, SITE_FINAL);
      Assert.equal(getCookies(request), "", "Cookies cleaned up");
      resolve(receivedCookies.splice(0));
    });
  });
}

// Load the page as a child frame of an extension, for the given permissions.
async function getCookiesForLoadInExtension({ permissions }) {
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions,
    },
    files: {
      "embedder.html": `<iframe src="${URL_START}"></iframe>`,
    },
  });
  await extension.startup();
  let cookiesPromise = promiseFinalResponse();
  let contentPage = await ExtensionTestUtils.loadContentPage(
    `moz-extension://${extension.uuid}/embedder.html`,
    { extension }
  );
  let cookies = await cookiesPromise;
  await contentPage.close();
  await extension.unload();
  return cookies;
}

add_task(async function setup() {
  Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
  Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", true);

  // Test server runs on http, so disable Secure requirement of sameSite=none.
  Services.prefs.setBoolPref(
    "network.cookie.sameSite.noneRequiresSecure",
    false
  );
});

// First verify that our expectations match with the actual behavior on the web.
add_task(async function verify_firstparty_web_behavior() {
  let cookiesPromise = promiseFinalResponse();
  let contentPage = await ExtensionTestUtils.loadContentPage(URL_START);
  let cookies = await cookiesPromise;
  await contentPage.close();
  Assert.deepEqual(
    cookies,
    // Same expectations as in host_permissions_enable_samesite_cookies
    [
      "start-none=1; start-lax=1; start-strict=1",
      "found-none=1; found-lax=1; found-strict=1",
      "redir-none=1; redir-lax=1; redir-strict=1",
    ],
    "Expected cookies from a first-party load on the web"
  );
});

// Verify that an extension without permission behaves like a third-party page.
add_task(async function samesite_is_foreign_without_host_permissions() {
  let cookies = await getCookiesForLoadInExtension({
    permissions: [],
  });

  Assert.deepEqual(
    cookies,
    ["start-none=1", "found-none=1", "redir-none=1"],
    "SameSite cookies excluded without permissions"
  );
});

// When an extension has permissions for the site, cookies should be included.
add_task(async function wildcard_host_permissions_enable_samesite_cookies() {
  let cookies = await getCookiesForLoadInExtension({
    permissions: ["*://*.example.net/*"], // = *.SITE
  });

  Assert.deepEqual(
    cookies,
    // Same expectations as in verify_firstparty_web_behavior.
    [
      "start-none=1; start-lax=1; start-strict=1",
      "found-none=1; found-lax=1; found-strict=1",
      "redir-none=1; redir-lax=1; redir-strict=1",
    ],
    "Expected cookies from a load in an extension frame"
  );
});

// When an extension has permissions for the site, cookies should be included.
add_task(async function explicit_host_permissions_enable_samesite_cookies() {
  let cookies = await getCookiesForLoadInExtension({
    permissions: [
      "*://start.example.net/*",
      "*://found.example.net/*",
      "*://redir.example.net/*",
      "*://final.example.net/*",
    ],
  });

  Assert.deepEqual(
    cookies,
    // Same expectations as in verify_firstparty_web_behavior.
    [
      "start-none=1; start-lax=1; start-strict=1",
      "found-none=1; found-lax=1; found-strict=1",
      "redir-none=1; redir-lax=1; redir-strict=1",
    ],
    "Expected cookies from a load in an extension frame"
  );
});

// When an extension does not have host permissions for all sites, but only
// some, then same-site cookies are only included in requests with the right
// permissions.
add_task(async function some_host_permissions_enable_some_samesite_cookies() {
  let cookies = await getCookiesForLoadInExtension({
    permissions: ["*://start.example.net/*", "*://final.example.net/*"],
  });

  Assert.deepEqual(
    cookies,
    [
      // Missing permission for "found.example.net":
      "start-none=1",
      // Missing permission for "redir.example.net":
      "found-none=1",
      // "final.example.net" can see cookies from "start.example.net":
      "start-lax=1; start-strict=1; redir-none=1",
    ],
    "Expected some cookies from a load in an extension frame"
  );
});
