michael@0: // michael@0: // This test makes sure range-requests are sent and treated the way we want michael@0: // See bug #612135 for a thorough discussion on the subject michael@0: // michael@0: // Necko does a range-request for a partial cache-entry iff michael@0: // michael@0: // 1) size of the cached entry < value of the cached Content-Length header michael@0: // (not tested here - see bug #612135 comments 108-110) michael@0: // 2) the size of the cached entry is > 0 (see bug #628607) michael@0: // 3) the cached entry does not have a "no-store" Cache-Control header michael@0: // 4) the cached entry does not have a Content-Encoding (see bug #613159) michael@0: // 5) the request does not have a conditional-request header set by client michael@0: // 6) nsHttpResponseHead::IsResumable() is true for the cached entry michael@0: // 7) a basic positive test that makes sure byte ranges work michael@0: // 8) ensure NS_ERROR_CORRUPTED_CONTENT is thrown when total entity size michael@0: // of 206 does not match content-length of 200 michael@0: // michael@0: // The test has one handler for each case and run_tests() fires one request michael@0: // for each. None of the handlers should see a Range-header. michael@0: michael@0: Cu.import("resource://testing-common/httpd.js"); michael@0: michael@0: var httpserver = null; michael@0: michael@0: const clearTextBody = "This is a slightly longer test\n"; michael@0: const encodedBody = [0x1f, 0x8b, 0x08, 0x08, 0xef, 0x70, 0xe6, 0x4c, 0x00, 0x03, 0x74, 0x65, 0x78, 0x74, 0x66, 0x69, michael@0: 0x6c, 0x65, 0x2e, 0x74, 0x78, 0x74, 0x00, 0x0b, 0xc9, 0xc8, 0x2c, 0x56, 0x00, 0xa2, 0x44, 0x85, michael@0: 0xe2, 0x9c, 0xcc, 0xf4, 0x8c, 0x92, 0x9c, 0x4a, 0x85, 0x9c, 0xfc, 0xbc, 0xf4, 0xd4, 0x22, 0x85, michael@0: 0x92, 0xd4, 0xe2, 0x12, 0x2e, 0x2e, 0x00, 0x00, 0xe5, 0xe6, 0xf0, 0x20, 0x00, 0x00, 0x00]; michael@0: const decodedBody = [0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x73, 0x6c, 0x69, 0x67, 0x68, 0x74, michael@0: 0x6c, 0x79, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x65, 0x72, 0x20, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x0a]; michael@0: michael@0: const partial_data_length = 4; michael@0: var port = null; // set in run_test michael@0: michael@0: function make_channel(url, callback, ctx) { michael@0: var ios = Cc["@mozilla.org/network/io-service;1"]. michael@0: getService(Ci.nsIIOService); michael@0: var chan = ios.newChannel(url, "", null); michael@0: return chan.QueryInterface(Ci.nsIHttpChannel); michael@0: } michael@0: michael@0: // StreamListener which cancels its request on first data available michael@0: function Canceler(continueFn) { michael@0: this.continueFn = continueFn; michael@0: } michael@0: Canceler.prototype = { michael@0: QueryInterface: function(iid) { michael@0: if (iid.equals(Ci.nsIStreamListener) || michael@0: iid.equals(Ci.nsIRequestObserver) || michael@0: iid.equals(Ci.nsISupports)) michael@0: return this; michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: onStartRequest: function(request, context) { }, michael@0: michael@0: onDataAvailable: function(request, context, stream, offset, count) { michael@0: request.QueryInterface(Ci.nsIChannel) michael@0: .cancel(Components.results.NS_BINDING_ABORTED); michael@0: }, michael@0: onStopRequest: function(request, context, status) { michael@0: do_check_eq(status, Components.results.NS_BINDING_ABORTED); michael@0: this.continueFn(request, null); michael@0: } michael@0: }; michael@0: // Simple StreamListener which performs no validations michael@0: function MyListener(continueFn) { michael@0: this.continueFn = continueFn; michael@0: this._buffer = null; michael@0: } michael@0: MyListener.prototype = { michael@0: QueryInterface: function(iid) { michael@0: if (iid.equals(Ci.nsIStreamListener) || michael@0: iid.equals(Ci.nsIRequestObserver) || michael@0: iid.equals(Ci.nsISupports)) michael@0: return this; michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: onStartRequest: function(request, context) { this._buffer = ""; }, michael@0: michael@0: onDataAvailable: function(request, context, stream, offset, count) { michael@0: this._buffer = this._buffer.concat(read_stream(stream, count)); michael@0: }, michael@0: onStopRequest: function(request, context, status) { michael@0: this.continueFn(request, this._buffer); michael@0: } michael@0: }; michael@0: michael@0: var case_8_range_request = false; michael@0: function FailedChannelListener(continueFn) { michael@0: this.continueFn = continueFn; michael@0: } michael@0: FailedChannelListener.prototype = { michael@0: QueryInterface: function(iid) { michael@0: if (iid.equals(Ci.nsIStreamListener) || michael@0: iid.equals(Ci.nsIRequestObserver) || michael@0: iid.equals(Ci.nsISupports)) michael@0: return this; michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: onStartRequest: function(request, context) { }, michael@0: michael@0: onDataAvailable: function(request, context, stream, offset, count) { }, michael@0: michael@0: onStopRequest: function(request, context, status) { michael@0: if (case_8_range_request) michael@0: do_check_eq(status, Components.results.NS_ERROR_CORRUPTED_CONTENT); michael@0: this.continueFn(request, null); michael@0: } michael@0: }; michael@0: michael@0: function received_cleartext(request, data) { michael@0: do_check_eq(clearTextBody, data); michael@0: testFinished(); michael@0: } michael@0: michael@0: function setStdHeaders(response, length) { michael@0: response.setHeader("Content-Type", "text/plain", false); michael@0: response.setHeader("ETag", "Just testing"); michael@0: response.setHeader("Cache-Control", "max-age: 360000"); michael@0: response.setHeader("Accept-Ranges", "bytes"); michael@0: response.setHeader("Content-Length", "" + length); michael@0: } michael@0: michael@0: function handler_2(metadata, response) { michael@0: setStdHeaders(response, clearTextBody.length); michael@0: do_check_false(metadata.hasHeader("Range")); michael@0: response.bodyOutputStream.write(clearTextBody, clearTextBody.length); michael@0: } michael@0: function received_partial_2(request, data) { michael@0: do_check_eq(data, undefined); michael@0: var chan = make_channel("http://localhost:" + port + "/test_2"); michael@0: chan.asyncOpen(new ChannelListener(received_cleartext, null), null); michael@0: } michael@0: michael@0: var case_3_request_no = 0; michael@0: function handler_3(metadata, response) { michael@0: var body = clearTextBody; michael@0: setStdHeaders(response, body.length); michael@0: response.setHeader("Cache-Control", "no-store", false); michael@0: switch (case_3_request_no) { michael@0: case 0: michael@0: do_check_false(metadata.hasHeader("Range")); michael@0: body = body.slice(0, partial_data_length); michael@0: response.processAsync(); michael@0: response.bodyOutputStream.write(body, body.length); michael@0: response.finish(); michael@0: break; michael@0: case 1: michael@0: do_check_false(metadata.hasHeader("Range")); michael@0: response.bodyOutputStream.write(body, body.length); michael@0: break; michael@0: default: michael@0: response.setStatusLine(metadata.httpVersion, 404, "Not Found"); michael@0: } michael@0: case_3_request_no++; michael@0: } michael@0: function received_partial_3(request, data) { michael@0: do_check_eq(partial_data_length, data.length); michael@0: var chan = make_channel("http://localhost:" + port + "/test_3"); michael@0: chan.asyncOpen(new ChannelListener(received_cleartext, null), null); michael@0: } michael@0: michael@0: var case_4_request_no = 0; michael@0: function handler_4(metadata, response) { michael@0: switch (case_4_request_no) { michael@0: case 0: michael@0: do_check_false(metadata.hasHeader("Range")); michael@0: var body = encodedBody; michael@0: setStdHeaders(response, body.length); michael@0: response.setHeader("Content-Encoding", "gzip", false); michael@0: body = body.slice(0, partial_data_length); michael@0: var bos = Cc["@mozilla.org/binaryoutputstream;1"] michael@0: .createInstance(Ci.nsIBinaryOutputStream); michael@0: bos.setOutputStream(response.bodyOutputStream); michael@0: response.processAsync(); michael@0: bos.writeByteArray(body, body.length); michael@0: response.finish(); michael@0: break; michael@0: case 1: michael@0: do_check_false(metadata.hasHeader("Range")); michael@0: setStdHeaders(response, clearTextBody.length); michael@0: response.bodyOutputStream.write(clearTextBody, clearTextBody.length); michael@0: break; michael@0: default: michael@0: response.setStatusLine(metadata.httpVersion, 404, "Not Found"); michael@0: } michael@0: case_4_request_no++; michael@0: } michael@0: function received_partial_4(request, data) { michael@0: // checking length does not work with encoded data michael@0: // do_check_eq(partial_data_length, data.length); michael@0: var chan = make_channel("http://localhost:" + port + "/test_4"); michael@0: chan.asyncOpen(new MyListener(received_cleartext), null); michael@0: } michael@0: michael@0: var case_5_request_no = 0; michael@0: function handler_5(metadata, response) { michael@0: var body = clearTextBody; michael@0: setStdHeaders(response, body.length); michael@0: switch (case_5_request_no) { michael@0: case 0: michael@0: do_check_false(metadata.hasHeader("Range")); michael@0: body = body.slice(0, partial_data_length); michael@0: response.processAsync(); michael@0: response.bodyOutputStream.write(body, body.length); michael@0: response.finish(); michael@0: break; michael@0: case 1: michael@0: do_check_false(metadata.hasHeader("Range")); michael@0: response.bodyOutputStream.write(body, body.length); michael@0: break; michael@0: default: michael@0: response.setStatusLine(metadata.httpVersion, 404, "Not Found"); michael@0: } michael@0: case_5_request_no++; michael@0: } michael@0: function received_partial_5(request, data) { michael@0: do_check_eq(partial_data_length, data.length); michael@0: var chan = make_channel("http://localhost:" + port + "/test_5"); michael@0: chan.setRequestHeader("If-Match", "Some eTag", false); michael@0: chan.asyncOpen(new ChannelListener(received_cleartext, null), null); michael@0: } michael@0: michael@0: var case_6_request_no = 0; michael@0: function handler_6(metadata, response) { michael@0: switch (case_6_request_no) { michael@0: case 0: michael@0: do_check_false(metadata.hasHeader("Range")); michael@0: var body = clearTextBody; michael@0: setStdHeaders(response, body.length); michael@0: response.setHeader("Accept-Ranges", "", false); michael@0: body = body.slice(0, partial_data_length); michael@0: response.processAsync(); michael@0: response.bodyOutputStream.write(body, body.length); michael@0: response.finish(); michael@0: break; michael@0: case 1: michael@0: do_check_false(metadata.hasHeader("Range")); michael@0: setStdHeaders(response, clearTextBody.length); michael@0: response.bodyOutputStream.write(clearTextBody, clearTextBody.length); michael@0: break; michael@0: default: michael@0: response.setStatusLine(metadata.httpVersion, 404, "Not Found"); michael@0: } michael@0: case_6_request_no++; michael@0: } michael@0: function received_partial_6(request, data) { michael@0: // would like to verify that the response does not have Accept-Ranges michael@0: do_check_eq(partial_data_length, data.length); michael@0: var chan = make_channel("http://localhost:" + port + "/test_6"); michael@0: chan.asyncOpen(new ChannelListener(received_cleartext, null), null); michael@0: } michael@0: michael@0: const simpleBody = "0123456789"; michael@0: michael@0: function received_simple(request, data) { michael@0: do_check_eq(simpleBody, data); michael@0: testFinished(); michael@0: } michael@0: michael@0: var case_7_request_no = 0; michael@0: function handler_7(metadata, response) { michael@0: switch (case_7_request_no) { michael@0: case 0: michael@0: do_check_false(metadata.hasHeader("Range")); michael@0: response.setHeader("Content-Type", "text/plain", false); michael@0: response.setHeader("ETag", "test7Etag"); michael@0: response.setHeader("Accept-Ranges", "bytes"); michael@0: response.setHeader("Cache-Control", "max-age=360000"); michael@0: response.setHeader("Content-Length", "10"); michael@0: response.processAsync(); michael@0: response.bodyOutputStream.write(simpleBody.slice(0, 4), 4); michael@0: response.finish(); michael@0: break; michael@0: case 1: michael@0: response.setHeader("Content-Type", "text/plain", false); michael@0: response.setHeader("ETag", "test7Etag"); michael@0: if (metadata.hasHeader("Range")) { michael@0: do_check_true(metadata.hasHeader("If-Range")); michael@0: response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); michael@0: response.setHeader("Content-Range", "4-9/10"); michael@0: response.setHeader("Content-Length", "6"); michael@0: response.bodyOutputStream.write(simpleBody.slice(4), 6); michael@0: } else { michael@0: response.setHeader("Content-Length", "10"); michael@0: response.bodyOutputStream.write(simpleBody, 10); michael@0: } michael@0: break; michael@0: default: michael@0: response.setStatusLine(metadata.httpVersion, 404, "Not Found"); michael@0: } michael@0: case_7_request_no++; michael@0: } michael@0: function received_partial_7(request, data) { michael@0: // make sure we get the first 4 bytes michael@0: do_check_eq(4, data.length); michael@0: // do it again to get the rest michael@0: var chan = make_channel("http://localhost:" + port + "/test_7"); michael@0: chan.asyncOpen(new ChannelListener(received_simple, null), null); michael@0: } michael@0: michael@0: var case_8_request_no = 0; michael@0: function handler_8(metadata, response) { michael@0: switch (case_8_request_no) { michael@0: case 0: michael@0: do_check_false(metadata.hasHeader("Range")); michael@0: response.setHeader("Content-Type", "text/plain", false); michael@0: response.setHeader("ETag", "test8Etag"); michael@0: response.setHeader("Accept-Ranges", "bytes"); michael@0: response.setHeader("Cache-Control", "max-age=360000"); michael@0: response.setHeader("Content-Length", "10"); michael@0: response.processAsync(); michael@0: response.bodyOutputStream.write(simpleBody.slice(0, 4), 4); michael@0: response.finish(); michael@0: break; michael@0: case 1: michael@0: if (metadata.hasHeader("Range")) { michael@0: do_check_true(metadata.hasHeader("If-Range")); michael@0: case_8_range_request = true; michael@0: } michael@0: response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); michael@0: response.setHeader("Content-Type", "text/plain", false); michael@0: response.setHeader("ETag", "test8Etag"); michael@0: response.setHeader("Content-Range", "4-8/9"); // intentionally broken michael@0: response.setHeader("Content-Length", "5"); michael@0: response.bodyOutputStream.write(simpleBody.slice(4), 5); michael@0: break; michael@0: default: michael@0: response.setStatusLine(metadata.httpVersion, 404, "Not Found"); michael@0: } michael@0: case_8_request_no++; michael@0: } michael@0: function received_partial_8(request, data) { michael@0: // make sure we get the first 4 bytes michael@0: do_check_eq(4, data.length); michael@0: // do it again to get the rest michael@0: var chan = make_channel("http://localhost:" + port + "/test_8"); michael@0: chan.asyncOpen(new FailedChannelListener(testFinished, null, CL_EXPECT_LATE_FAILURE), null); michael@0: } michael@0: michael@0: // Simple mechanism to keep track of tests and stop the server michael@0: var numTestsFinished = 0; michael@0: function testFinished() { michael@0: if (++numTestsFinished == 7) michael@0: httpserver.stop(do_test_finished); michael@0: } michael@0: michael@0: function run_test() { michael@0: httpserver = new HttpServer(); michael@0: httpserver.registerPathHandler("/test_2", handler_2); michael@0: httpserver.registerPathHandler("/test_3", handler_3); michael@0: httpserver.registerPathHandler("/test_4", handler_4); michael@0: httpserver.registerPathHandler("/test_5", handler_5); michael@0: httpserver.registerPathHandler("/test_6", handler_6); michael@0: httpserver.registerPathHandler("/test_7", handler_7); michael@0: httpserver.registerPathHandler("/test_8", handler_8); michael@0: httpserver.start(-1); michael@0: michael@0: port = httpserver.identity.primaryPort; michael@0: michael@0: // wipe out cached content michael@0: evict_cache_entries(); michael@0: michael@0: // Case 2: zero-length partial entry must not trigger range-request michael@0: var chan = make_channel("http://localhost:" + port + "/test_2"); michael@0: chan.asyncOpen(new Canceler(received_partial_2), null); michael@0: michael@0: // Case 3: no-store response must not trigger range-request michael@0: var chan = make_channel("http://localhost:" + port + "/test_3"); michael@0: chan.asyncOpen(new MyListener(received_partial_3), null); michael@0: michael@0: // Case 4: response with content-encoding must not trigger range-request michael@0: var chan = make_channel("http://localhost:" + port + "/test_4"); michael@0: chan.asyncOpen(new MyListener(received_partial_4), null); michael@0: michael@0: // Case 5: conditional request-header set by client michael@0: var chan = make_channel("http://localhost:" + port + "/test_5"); michael@0: chan.asyncOpen(new MyListener(received_partial_5), null); michael@0: michael@0: // Case 6: response is not resumable (drop the Accept-Ranges header) michael@0: var chan = make_channel("http://localhost:" + port + "/test_6"); michael@0: chan.asyncOpen(new MyListener(received_partial_6), null); michael@0: michael@0: // Case 7: a basic positive test michael@0: var chan = make_channel("http://localhost:" + port + "/test_7"); michael@0: chan.asyncOpen(new MyListener(received_partial_7), null); michael@0: michael@0: // Case 8: check that mismatched 206 and 200 sizes throw error michael@0: var chan = make_channel("http://localhost:" + port + "/test_8"); michael@0: chan.asyncOpen(new MyListener(received_partial_8), null); michael@0: michael@0: do_test_pending(); michael@0: }