/*
 * Tests bugs 597706, 655389: prevent duplicate headers with differing values
 * for some headers like Content-Length, Location, etc.
 */

////////////////////////////////////////////////////////////////////////////////
// Test infrastructure

"use strict";

// The tests in this file use number indexes to run, which can't be detected
// via ESLint.
/* eslint-disable no-unused-vars */

const { HttpServer } = ChromeUtils.importESModule(
  "resource://testing-common/httpd.sys.mjs"
);

ChromeUtils.defineLazyGetter(this, "URL", function () {
  return "http://localhost:" + httpserver.identity.primaryPort;
});

var httpserver = new HttpServer();
var test_flags = [];
var testPathBase = "/dupe_hdrs";

function run_test() {
  httpserver.start(-1);

  do_test_pending();
  run_test_number(1);
}

function run_test_number(num) {
  let testPath = testPathBase + num;
  httpserver.registerPathHandler(testPath, globalThis["handler" + num]);

  var channel = setupChannel(testPath);
  let flags = test_flags[num]; // OK if flags undefined for test
  channel.asyncOpen(
    new ChannelListener(globalThis["completeTest" + num], channel, flags)
  );
}

function setupChannel(url) {
  var chan = NetUtil.newChannel({
    uri: URL + url,
    loadUsingSystemPrincipal: true,
  });
  var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
  return httpChan;
}

function endTests() {
  httpserver.stop(do_test_finished);
}

////////////////////////////////////////////////////////////////////////////////
// Test 1: FAIL because of conflicting Content-Length headers
test_flags[1] = CL_EXPECT_FAILURE;

function handler1(metadata, response) {
  var body = "012345678901234567890123456789";
  // Comrades!  We must seize power from the petty-bourgeois running dogs of
  // httpd.js in order to reply with multiple instances of the same header!
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  response.write("Content-Length: 20\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest1(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);

  run_test_number(2);
}

////////////////////////////////////////////////////////////////////////////////
// Test 2: OK to have duplicate same Content-Length headers

function handler2(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  response.write("Content-Length: 30\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest2(request, data, ctx) {
  Assert.equal(request.status, 0);
  run_test_number(3);
}

////////////////////////////////////////////////////////////////////////////////
// Test 3: FAIL: 2nd Content-length is blank
test_flags[3] = CL_EXPECT_FAILURE;

function handler3(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  response.write("Content-Length:\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest3(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);

  run_test_number(4);
}

////////////////////////////////////////////////////////////////////////////////
// Test 4: ensure that blank C-len header doesn't allow attacker to reset Clen,
// then insert CRLF attack
test_flags[4] = CL_EXPECT_FAILURE;

function handler4(metadata, response) {
  var body = "012345678901234567890123456789";

  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");

  // Bad Mr Hacker!  Bad!
  var evilBody = "We are the Evil bytes, Evil bytes, Evil bytes!";
  response.write("Content-Length:\r\n");
  response.write("Content-Length: %s\r\n\r\n%s" % (evilBody.length, evilBody));
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest4(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);

  run_test_number(5);
}

////////////////////////////////////////////////////////////////////////////////
// Test 5: ensure that we take 1st instance of duplicate, nonmerged headers that
// are permitted : (ex: Referrer)

function handler5(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  response.write("Referer: naive.org\r\n");
  response.write("Referer: evil.net\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest5(request, data, ctx) {
  try {
    let referer = request.getResponseHeader("Referer");
    Assert.equal(referer, "naive.org");
  } catch (ex) {
    do_throw("Referer header should be present");
  }

  run_test_number(6);
}

////////////////////////////////////////////////////////////////////////////////
// Test 5: FAIL if multiple, different Location: headers present
// - needed to prevent CRLF injection attacks
test_flags[6] = CL_EXPECT_FAILURE;

function handler6(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 301 Moved\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  response.write("Location: " + URL + "/content\r\n");
  response.write("Location: http://www.microsoft.com/\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest6(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);

  //  run_test_number(7);   // Test 7 leaking under e10s: unrelated bug?
  run_test_number(8);
}

////////////////////////////////////////////////////////////////////////////////
// Test 7: OK to have multiple Location: headers with same value

function handler7(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 301 Moved\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  // redirect to previous test handler that completes OK: test 5
  response.write("Location: " + URL + testPathBase + "5\r\n");
  response.write("Location: " + URL + testPathBase + "5\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest7(request, data, ctx) {
  // for some reason need this here
  request.QueryInterface(Ci.nsIHttpChannel);

  try {
    let referer = request.getResponseHeader("Referer");
    Assert.equal(referer, "naive.org");
  } catch (ex) {
    do_throw("Referer header should be present");
  }

  run_test_number(8);
}

////////////////////////////////////////////////////////////////////////////////
// FAIL if 2nd Location: headers blank
test_flags[8] = CL_EXPECT_FAILURE;

function handler8(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 301 Moved\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  // redirect to previous test handler that completes OK: test 4
  response.write("Location: " + URL + testPathBase + "4\r\n");
  response.write("Location:\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest8(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);

  run_test_number(9);
}

////////////////////////////////////////////////////////////////////////////////
// Test 9: ensure that blank Location header doesn't allow attacker to reset,
// then insert an evil one
test_flags[9] = CL_EXPECT_FAILURE;

function handler9(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 301 Moved\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  // redirect to previous test handler that completes OK: test 2
  response.write("Location: " + URL + testPathBase + "2\r\n");
  response.write("Location:\r\n");
  // redirect to previous test handler that completes OK: test 4
  response.write("Location: " + URL + testPathBase + "4\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest9(request, data, ctx) {
  // All redirection should fail:
  Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);

  run_test_number(10);
}

////////////////////////////////////////////////////////////////////////////////
// Test 10: FAIL:  if conflicting values for Content-Dispo
test_flags[10] = CL_EXPECT_FAILURE;

function handler10(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  response.write("Content-Disposition: attachment; filename=foo\r\n");
  response.write("Content-Disposition: attachment; filename=bar\r\n");
  response.write("Content-Disposition: attachment; filename=baz\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest10(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);

  run_test_number(11);
}

////////////////////////////////////////////////////////////////////////////////
// Test 11: OK to have duplicate same Content-Disposition headers

function handler11(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  response.write("Content-Disposition: attachment; filename=foo\r\n");
  response.write("Content-Disposition: attachment; filename=foo\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest11(request, data, ctx) {
  Assert.equal(request.status, 0);

  try {
    var chan = request.QueryInterface(Ci.nsIChannel);
    Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT);
    Assert.equal(chan.contentDispositionFilename, "foo");
    Assert.equal(chan.contentDispositionHeader, "attachment; filename=foo");
  } catch (ex) {
    do_throw("error parsing Content-Disposition: " + ex);
  }

  run_test_number(12);
}

////////////////////////////////////////////////////////////////////////////////
// Bug 716801 OK for Location: header to be blank

function handler12(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  response.write("Location:\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest12(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_OK);
  Assert.equal(30, data.length);

  run_test_number(13);
}

////////////////////////////////////////////////////////////////////////////////
// Negative content length is ok
test_flags[13] = CL_ALLOW_UNKNOWN_CL;

function handler13(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: -1\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest13(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_OK);
  Assert.equal(30, data.length);

  run_test_number(14);
}

////////////////////////////////////////////////////////////////////////////////
// leading negative content length is not ok if paired with positive one

test_flags[14] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL;

function handler14(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: -1\r\n");
  response.write("Content-Length: 30\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest14(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);

  run_test_number(15);
}

////////////////////////////////////////////////////////////////////////////////
// trailing negative content length is not ok if paired with positive one

test_flags[15] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL;

function handler15(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  response.write("Content-Length: -1\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest15(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);

  run_test_number(16);
}

////////////////////////////////////////////////////////////////////////////////
// empty content length is ok
test_flags[16] = CL_ALLOW_UNKNOWN_CL;
let reran16 = false;

function handler16(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: \r\n");
  response.write("Cache-Control: max-age=600\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest16(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_OK);
  Assert.equal(30, data.length);

  if (!reran16) {
    reran16 = true;
    run_test_number(16);
  } else {
    run_test_number(17);
  }
}

////////////////////////////////////////////////////////////////////////////////
// empty content length paired with non empty is not ok
test_flags[17] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL;

function handler17(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: \r\n");
  response.write("Content-Length: 30\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest17(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);

  run_test_number(18);
}

////////////////////////////////////////////////////////////////////////////////
// alpha content-length is just like -1
test_flags[18] = CL_ALLOW_UNKNOWN_CL;

function handler18(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: seventeen\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest18(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_OK);
  Assert.equal(30, data.length);

  run_test_number(19);
}

////////////////////////////////////////////////////////////////////////////////
// semi-colons are ok too in the content-length
test_flags[19] = CL_ALLOW_UNKNOWN_CL;

function handler19(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 200 OK\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30;\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest19(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_OK);
  Assert.equal(30, data.length);

  run_test_number(20);
}

////////////////////////////////////////////////////////////////////////////////
// FAIL if 1st Location: header is blank, followed by non-blank
test_flags[20] = CL_EXPECT_FAILURE;

function handler20(metadata, response) {
  var body = "012345678901234567890123456789";
  response.seizePower();
  response.write("HTTP/1.0 301 Moved\r\n");
  response.write("Content-Type: text/plain\r\n");
  response.write("Content-Length: 30\r\n");
  // redirect to previous test handler that completes OK: test 4
  response.write("Location:\r\n");
  response.write("Location: " + URL + testPathBase + "4\r\n");
  response.write("Connection: close\r\n");
  response.write("\r\n");
  response.write(body);
  response.finish();
}

function completeTest20(request, data, ctx) {
  Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);

  endTests();
}
