//
// This test makes sure range-requests are sent and treated the way we want
// See bug #612135 for a thorough discussion on the subject
//
// Necko does a range-request for a partial cache-entry iff
//
//   1) size of the cached entry < value of the cached Content-Length header
//      (not tested here - see bug #612135 comments 108-110)
//   2) the size of the cached entry is > 0  (see bug #628607)
//   3) the cached entry does not have a "no-store" Cache-Control header
//   4) the cached entry does not have a Content-Encoding (see bug #613159)
//   5) the request does not have a conditional-request header set by client
//   6) nsHttpResponseHead::IsResumable() is true for the cached entry
//   7) a basic positive test that makes sure byte ranges work
//   8) ensure NS_ERROR_CORRUPTED_CONTENT is thrown when total entity size
//      of 206 does not match content-length of 200
//
//  The test has one handler for each case and run_tests() fires one request
//  for each. None of the handlers should see a Range-header.

"use strict";

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

var httpserver = null;

const clearTextBody = "This is a slightly longer test\n";
const encodedBody = [
  0x1f, 0x8b, 0x08, 0x08, 0xef, 0x70, 0xe6, 0x4c, 0x00, 0x03, 0x74, 0x65, 0x78,
  0x74, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x74, 0x78, 0x74, 0x00, 0x0b, 0xc9, 0xc8,
  0x2c, 0x56, 0x00, 0xa2, 0x44, 0x85, 0xe2, 0x9c, 0xcc, 0xf4, 0x8c, 0x92, 0x9c,
  0x4a, 0x85, 0x9c, 0xfc, 0xbc, 0xf4, 0xd4, 0x22, 0x85, 0x92, 0xd4, 0xe2, 0x12,
  0x2e, 0x2e, 0x00, 0x00, 0xe5, 0xe6, 0xf0, 0x20, 0x00, 0x00, 0x00,
];

const partial_data_length = 4;
var port = null; // set in run_test

function make_channel(url, callback, ctx) {
  return NetUtil.newChannel({
    uri: url,
    loadUsingSystemPrincipal: true,
  }).QueryInterface(Ci.nsIHttpChannel);
}

// StreamListener which cancels its request on first data available
function Canceler(continueFn) {
  this.continueFn = continueFn;
}
Canceler.prototype = {
  QueryInterface: ChromeUtils.generateQI([
    "nsIStreamListener",
    "nsIRequestObserver",
  ]),
  onStartRequest(request) {},

  onDataAvailable(request, stream, offset, count) {
    // Read stream so we don't assert for not reading from the stream
    // if cancelling the channel is slow.
    read_stream(stream, count);

    request.QueryInterface(Ci.nsIChannel).cancel(Cr.NS_BINDING_ABORTED);
  },
  onStopRequest(request, status) {
    Assert.equal(status, Cr.NS_BINDING_ABORTED);
    this.continueFn(request, null);
  },
};
// Simple StreamListener which performs no validations
function MyListener(continueFn) {
  this.continueFn = continueFn;
  this._buffer = null;
}
MyListener.prototype = {
  QueryInterface: ChromeUtils.generateQI([
    "nsIStreamListener",
    "nsIRequestObserver",
  ]),
  onStartRequest(request) {
    this._buffer = "";
  },

  onDataAvailable(request, stream, offset, count) {
    this._buffer = this._buffer.concat(read_stream(stream, count));
  },
  onStopRequest(request, status) {
    this.continueFn(request, this._buffer);
  },
};

var case_8_range_request = false;
function FailedChannelListener(continueFn) {
  this.continueFn = continueFn;
}
FailedChannelListener.prototype = {
  QueryInterface: ChromeUtils.generateQI([
    "nsIStreamListener",
    "nsIRequestObserver",
  ]),
  onStartRequest(request) {},

  onDataAvailable(request, stream, offset, count) {
    read_stream(stream, count);
  },

  onStopRequest(request, status) {
    if (case_8_range_request) {
      Assert.equal(status, Cr.NS_ERROR_CORRUPTED_CONTENT);
    }
    this.continueFn(request, null);
  },
};

function received_cleartext(request, data) {
  Assert.equal(clearTextBody, data);
  testFinished();
}

function setStdHeaders(response, length) {
  response.setHeader("Content-Type", "text/plain", false);
  response.setHeader("ETag", "Just testing");
  response.setHeader("Cache-Control", "max-age: 360000");
  response.setHeader("Accept-Ranges", "bytes");
  response.setHeader("Content-Length", "" + length);
}

function handler_2(metadata, response) {
  setStdHeaders(response, clearTextBody.length);
  Assert.ok(!metadata.hasHeader("Range"));
  response.bodyOutputStream.write(clearTextBody, clearTextBody.length);
}
function received_partial_2(request, data) {
  Assert.equal(data, undefined);
  var chan = make_channel("http://localhost:" + port + "/test_2");
  chan.asyncOpen(new ChannelListener(received_cleartext, null));
}

var case_3_request_no = 0;
function handler_3(metadata, response) {
  var body = clearTextBody;
  setStdHeaders(response, body.length);
  response.setHeader("Cache-Control", "no-store", false);
  switch (case_3_request_no) {
    case 0:
      Assert.ok(!metadata.hasHeader("Range"));
      body = body.slice(0, partial_data_length);
      response.processAsync();
      response.bodyOutputStream.write(body, body.length);
      response.finish();
      break;
    case 1:
      Assert.ok(!metadata.hasHeader("Range"));
      response.bodyOutputStream.write(body, body.length);
      break;
    default:
      response.setStatusLine(metadata.httpVersion, 404, "Not Found");
  }
  case_3_request_no++;
}
function received_partial_3(request, data) {
  Assert.equal(partial_data_length, data.length);
  var chan = make_channel("http://localhost:" + port + "/test_3");
  chan.asyncOpen(new ChannelListener(received_cleartext, null));
}

var case_4_request_no = 0;
function handler_4(metadata, response) {
  switch (case_4_request_no) {
    case 0:
      Assert.ok(!metadata.hasHeader("Range"));
      var body = encodedBody;
      setStdHeaders(response, body.length);
      response.setHeader("Content-Encoding", "gzip", false);
      body = body.slice(0, partial_data_length);
      var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
        Ci.nsIBinaryOutputStream
      );
      bos.setOutputStream(response.bodyOutputStream);
      response.processAsync();
      bos.writeByteArray(body);
      response.finish();
      break;
    case 1:
      Assert.ok(!metadata.hasHeader("Range"));
      setStdHeaders(response, clearTextBody.length);
      response.bodyOutputStream.write(clearTextBody, clearTextBody.length);
      break;
    default:
      response.setStatusLine(metadata.httpVersion, 404, "Not Found");
  }
  case_4_request_no++;
}
function received_partial_4(request, data) {
  // checking length does not work with encoded data
  //  do_check_eq(partial_data_length, data.length);
  var chan = make_channel("http://localhost:" + port + "/test_4");
  chan.asyncOpen(new MyListener(received_cleartext));
}

var case_5_request_no = 0;
function handler_5(metadata, response) {
  var body = clearTextBody;
  setStdHeaders(response, body.length);
  switch (case_5_request_no) {
    case 0:
      Assert.ok(!metadata.hasHeader("Range"));
      body = body.slice(0, partial_data_length);
      response.processAsync();
      response.bodyOutputStream.write(body, body.length);
      response.finish();
      break;
    case 1:
      Assert.ok(!metadata.hasHeader("Range"));
      response.bodyOutputStream.write(body, body.length);
      break;
    default:
      response.setStatusLine(metadata.httpVersion, 404, "Not Found");
  }
  case_5_request_no++;
}
function received_partial_5(request, data) {
  Assert.equal(partial_data_length, data.length);
  var chan = make_channel("http://localhost:" + port + "/test_5");
  chan.setRequestHeader("If-Match", "Some eTag", false);
  chan.asyncOpen(new ChannelListener(received_cleartext, null));
}

var case_6_request_no = 0;
function handler_6(metadata, response) {
  switch (case_6_request_no) {
    case 0:
      Assert.ok(!metadata.hasHeader("Range"));
      var body = clearTextBody;
      setStdHeaders(response, body.length);
      response.setHeader("Accept-Ranges", "", false);
      body = body.slice(0, partial_data_length);
      response.processAsync();
      response.bodyOutputStream.write(body, body.length);
      response.finish();
      break;
    case 1:
      Assert.ok(!metadata.hasHeader("Range"));
      setStdHeaders(response, clearTextBody.length);
      response.bodyOutputStream.write(clearTextBody, clearTextBody.length);
      break;
    default:
      response.setStatusLine(metadata.httpVersion, 404, "Not Found");
  }
  case_6_request_no++;
}
function received_partial_6(request, data) {
  // would like to verify that the response does not have Accept-Ranges
  Assert.equal(partial_data_length, data.length);
  var chan = make_channel("http://localhost:" + port + "/test_6");
  chan.asyncOpen(new ChannelListener(received_cleartext, null));
}

const simpleBody = "0123456789";

function received_simple(request, data) {
  Assert.equal(simpleBody, data);
  testFinished();
}

var case_7_request_no = 0;
function handler_7(metadata, response) {
  switch (case_7_request_no) {
    case 0:
      Assert.ok(!metadata.hasHeader("Range"));
      response.setHeader("Content-Type", "text/plain", false);
      response.setHeader("ETag", "test7Etag");
      response.setHeader("Accept-Ranges", "bytes");
      response.setHeader("Cache-Control", "max-age=360000");
      response.setHeader("Content-Length", "10");
      response.processAsync();
      response.bodyOutputStream.write(simpleBody.slice(0, 4), 4);
      response.finish();
      break;
    case 1:
      response.setHeader("Content-Type", "text/plain", false);
      response.setHeader("ETag", "test7Etag");
      if (metadata.hasHeader("Range")) {
        Assert.ok(metadata.hasHeader("If-Range"));
        response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
        response.setHeader("Content-Range", "4-9/10");
        response.setHeader("Content-Length", "6");
        response.bodyOutputStream.write(simpleBody.slice(4), 6);
      } else {
        response.setHeader("Content-Length", "10");
        response.bodyOutputStream.write(simpleBody, 10);
      }
      break;
    default:
      response.setStatusLine(metadata.httpVersion, 404, "Not Found");
  }
  case_7_request_no++;
}
function received_partial_7(request, data) {
  // make sure we get the first 4 bytes
  Assert.equal(4, data.length);
  // do it again to get the rest
  var chan = make_channel("http://localhost:" + port + "/test_7");
  chan.asyncOpen(new ChannelListener(received_simple, null));
}

var case_8_request_no = 0;
function handler_8(metadata, response) {
  switch (case_8_request_no) {
    case 0:
      Assert.ok(!metadata.hasHeader("Range"));
      response.setHeader("Content-Type", "text/plain", false);
      response.setHeader("ETag", "test8Etag");
      response.setHeader("Accept-Ranges", "bytes");
      response.setHeader("Cache-Control", "max-age=360000");
      response.setHeader("Content-Length", "10");
      response.processAsync();
      response.bodyOutputStream.write(simpleBody.slice(0, 4), 4);
      response.finish();
      break;
    case 1:
      if (metadata.hasHeader("Range")) {
        Assert.ok(metadata.hasHeader("If-Range"));
        case_8_range_request = true;
      }
      response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
      response.setHeader("Content-Type", "text/plain", false);
      response.setHeader("ETag", "test8Etag");
      response.setHeader("Content-Range", "4-8/9"); // intentionally broken
      response.setHeader("Content-Length", "5");
      response.bodyOutputStream.write(simpleBody.slice(4), 5);
      break;
    default:
      response.setStatusLine(metadata.httpVersion, 404, "Not Found");
  }
  case_8_request_no++;
}
function received_partial_8(request, data) {
  // make sure we get the first 4 bytes
  Assert.equal(4, data.length);
  // do it again to get the rest
  var chan = make_channel("http://localhost:" + port + "/test_8");
  chan.asyncOpen(
    new FailedChannelListener(testFinished, null, CL_EXPECT_LATE_FAILURE)
  );
}

var case_9_request_no = 0;
function handler_9(metadata, response) {
  switch (case_9_request_no) {
    case 0:
      Assert.ok(!metadata.hasHeader("Range"));
      response.setHeader("Content-Type", "text/plain", false);
      response.setHeader("ETag", "W/test9WeakEtag");
      response.setHeader("Accept-Ranges", "bytes");
      response.setHeader("Cache-Control", "max-age=360000");
      response.setHeader("Content-Length", "10");
      response.processAsync();
      response.bodyOutputStream.write(simpleBody.slice(0, 4), 4);
      response.finish(); // truncated response
      break;
    case 1:
      Assert.ok(!metadata.hasHeader("Range"));
      response.setHeader("Content-Type", "text/plain", false);
      response.setHeader("ETag", "W/test9WeakEtag");
      response.setHeader("Accept-Ranges", "bytes");
      response.setHeader("Cache-Control", "max-age=360000");
      response.setHeader("Content-Length", "10");
      response.processAsync();
      response.bodyOutputStream.write(simpleBody, 10);
      response.finish(); // full response
      break;
    default:
      response.setStatusLine(metadata.httpVersion, 404, "Not Found");
  }
  case_9_request_no++;
}
function received_partial_9(request, data) {
  Assert.equal(partial_data_length, data.length);
  var chan = make_channel("http://localhost:" + port + "/test_9");
  chan.asyncOpen(new ChannelListener(received_simple, null));
}

// Simple mechanism to keep track of tests and stop the server
var numTestsFinished = 0;
function testFinished() {
  if (++numTestsFinished == 7) {
    httpserver.stop(do_test_finished);
  }
}

function run_test() {
  httpserver = new HttpServer();
  httpserver.registerPathHandler("/test_2", handler_2);
  httpserver.registerPathHandler("/test_3", handler_3);
  httpserver.registerPathHandler("/test_4", handler_4);
  httpserver.registerPathHandler("/test_5", handler_5);
  httpserver.registerPathHandler("/test_6", handler_6);
  httpserver.registerPathHandler("/test_7", handler_7);
  httpserver.registerPathHandler("/test_8", handler_8);
  httpserver.registerPathHandler("/test_9", handler_9);
  httpserver.start(-1);

  port = httpserver.identity.primaryPort;

  // wipe out cached content
  evict_cache_entries();

  // Case 2: zero-length partial entry must not trigger range-request
  let chan = make_channel("http://localhost:" + port + "/test_2");
  chan.asyncOpen(new Canceler(received_partial_2));

  // Case 3: no-store response must not trigger range-request
  chan = make_channel("http://localhost:" + port + "/test_3");
  chan.asyncOpen(new MyListener(received_partial_3));

  // Case 4: response with content-encoding must not trigger range-request
  chan = make_channel("http://localhost:" + port + "/test_4");
  chan.asyncOpen(new MyListener(received_partial_4));

  // Case 5: conditional request-header set by client
  chan = make_channel("http://localhost:" + port + "/test_5");
  chan.asyncOpen(new MyListener(received_partial_5));

  // Case 6: response is not resumable (drop the Accept-Ranges header)
  chan = make_channel("http://localhost:" + port + "/test_6");
  chan.asyncOpen(new MyListener(received_partial_6));

  // Case 7: a basic positive test
  chan = make_channel("http://localhost:" + port + "/test_7");
  chan.asyncOpen(new MyListener(received_partial_7));

  // Case 8: check that mismatched 206 and 200 sizes throw error
  chan = make_channel("http://localhost:" + port + "/test_8");
  chan.asyncOpen(new MyListener(received_partial_8));

  // Case 9: check that weak etag is not used for a range request
  chan = make_channel("http://localhost:" + port + "/test_9");
  chan.asyncOpen(new MyListener(received_partial_9));

  do_test_pending();
}
