1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/netwerk/test/unit/test_range_requests.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,394 @@ 1.4 +// 1.5 +// This test makes sure range-requests are sent and treated the way we want 1.6 +// See bug #612135 for a thorough discussion on the subject 1.7 +// 1.8 +// Necko does a range-request for a partial cache-entry iff 1.9 +// 1.10 +// 1) size of the cached entry < value of the cached Content-Length header 1.11 +// (not tested here - see bug #612135 comments 108-110) 1.12 +// 2) the size of the cached entry is > 0 (see bug #628607) 1.13 +// 3) the cached entry does not have a "no-store" Cache-Control header 1.14 +// 4) the cached entry does not have a Content-Encoding (see bug #613159) 1.15 +// 5) the request does not have a conditional-request header set by client 1.16 +// 6) nsHttpResponseHead::IsResumable() is true for the cached entry 1.17 +// 7) a basic positive test that makes sure byte ranges work 1.18 +// 8) ensure NS_ERROR_CORRUPTED_CONTENT is thrown when total entity size 1.19 +// of 206 does not match content-length of 200 1.20 +// 1.21 +// The test has one handler for each case and run_tests() fires one request 1.22 +// for each. None of the handlers should see a Range-header. 1.23 + 1.24 +Cu.import("resource://testing-common/httpd.js"); 1.25 + 1.26 +var httpserver = null; 1.27 + 1.28 +const clearTextBody = "This is a slightly longer test\n"; 1.29 +const encodedBody = [0x1f, 0x8b, 0x08, 0x08, 0xef, 0x70, 0xe6, 0x4c, 0x00, 0x03, 0x74, 0x65, 0x78, 0x74, 0x66, 0x69, 1.30 + 0x6c, 0x65, 0x2e, 0x74, 0x78, 0x74, 0x00, 0x0b, 0xc9, 0xc8, 0x2c, 0x56, 0x00, 0xa2, 0x44, 0x85, 1.31 + 0xe2, 0x9c, 0xcc, 0xf4, 0x8c, 0x92, 0x9c, 0x4a, 0x85, 0x9c, 0xfc, 0xbc, 0xf4, 0xd4, 0x22, 0x85, 1.32 + 0x92, 0xd4, 0xe2, 0x12, 0x2e, 0x2e, 0x00, 0x00, 0xe5, 0xe6, 0xf0, 0x20, 0x00, 0x00, 0x00]; 1.33 +const decodedBody = [0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x73, 0x6c, 0x69, 0x67, 0x68, 0x74, 1.34 + 0x6c, 0x79, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x65, 0x72, 0x20, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x0a]; 1.35 + 1.36 +const partial_data_length = 4; 1.37 +var port = null; // set in run_test 1.38 + 1.39 +function make_channel(url, callback, ctx) { 1.40 + var ios = Cc["@mozilla.org/network/io-service;1"]. 1.41 + getService(Ci.nsIIOService); 1.42 + var chan = ios.newChannel(url, "", null); 1.43 + return chan.QueryInterface(Ci.nsIHttpChannel); 1.44 +} 1.45 + 1.46 +// StreamListener which cancels its request on first data available 1.47 +function Canceler(continueFn) { 1.48 + this.continueFn = continueFn; 1.49 +} 1.50 +Canceler.prototype = { 1.51 + QueryInterface: function(iid) { 1.52 + if (iid.equals(Ci.nsIStreamListener) || 1.53 + iid.equals(Ci.nsIRequestObserver) || 1.54 + iid.equals(Ci.nsISupports)) 1.55 + return this; 1.56 + throw Components.results.NS_ERROR_NO_INTERFACE; 1.57 + }, 1.58 + onStartRequest: function(request, context) { }, 1.59 + 1.60 + onDataAvailable: function(request, context, stream, offset, count) { 1.61 + request.QueryInterface(Ci.nsIChannel) 1.62 + .cancel(Components.results.NS_BINDING_ABORTED); 1.63 + }, 1.64 + onStopRequest: function(request, context, status) { 1.65 + do_check_eq(status, Components.results.NS_BINDING_ABORTED); 1.66 + this.continueFn(request, null); 1.67 + } 1.68 +}; 1.69 +// Simple StreamListener which performs no validations 1.70 +function MyListener(continueFn) { 1.71 + this.continueFn = continueFn; 1.72 + this._buffer = null; 1.73 +} 1.74 +MyListener.prototype = { 1.75 + QueryInterface: function(iid) { 1.76 + if (iid.equals(Ci.nsIStreamListener) || 1.77 + iid.equals(Ci.nsIRequestObserver) || 1.78 + iid.equals(Ci.nsISupports)) 1.79 + return this; 1.80 + throw Components.results.NS_ERROR_NO_INTERFACE; 1.81 + }, 1.82 + onStartRequest: function(request, context) { this._buffer = ""; }, 1.83 + 1.84 + onDataAvailable: function(request, context, stream, offset, count) { 1.85 + this._buffer = this._buffer.concat(read_stream(stream, count)); 1.86 + }, 1.87 + onStopRequest: function(request, context, status) { 1.88 + this.continueFn(request, this._buffer); 1.89 + } 1.90 +}; 1.91 + 1.92 +var case_8_range_request = false; 1.93 +function FailedChannelListener(continueFn) { 1.94 + this.continueFn = continueFn; 1.95 +} 1.96 +FailedChannelListener.prototype = { 1.97 + QueryInterface: function(iid) { 1.98 + if (iid.equals(Ci.nsIStreamListener) || 1.99 + iid.equals(Ci.nsIRequestObserver) || 1.100 + iid.equals(Ci.nsISupports)) 1.101 + return this; 1.102 + throw Components.results.NS_ERROR_NO_INTERFACE; 1.103 + }, 1.104 + onStartRequest: function(request, context) { }, 1.105 + 1.106 + onDataAvailable: function(request, context, stream, offset, count) { }, 1.107 + 1.108 + onStopRequest: function(request, context, status) { 1.109 + if (case_8_range_request) 1.110 + do_check_eq(status, Components.results.NS_ERROR_CORRUPTED_CONTENT); 1.111 + this.continueFn(request, null); 1.112 + } 1.113 +}; 1.114 + 1.115 +function received_cleartext(request, data) { 1.116 + do_check_eq(clearTextBody, data); 1.117 + testFinished(); 1.118 +} 1.119 + 1.120 +function setStdHeaders(response, length) { 1.121 + response.setHeader("Content-Type", "text/plain", false); 1.122 + response.setHeader("ETag", "Just testing"); 1.123 + response.setHeader("Cache-Control", "max-age: 360000"); 1.124 + response.setHeader("Accept-Ranges", "bytes"); 1.125 + response.setHeader("Content-Length", "" + length); 1.126 +} 1.127 + 1.128 +function handler_2(metadata, response) { 1.129 + setStdHeaders(response, clearTextBody.length); 1.130 + do_check_false(metadata.hasHeader("Range")); 1.131 + response.bodyOutputStream.write(clearTextBody, clearTextBody.length); 1.132 +} 1.133 +function received_partial_2(request, data) { 1.134 + do_check_eq(data, undefined); 1.135 + var chan = make_channel("http://localhost:" + port + "/test_2"); 1.136 + chan.asyncOpen(new ChannelListener(received_cleartext, null), null); 1.137 +} 1.138 + 1.139 +var case_3_request_no = 0; 1.140 +function handler_3(metadata, response) { 1.141 + var body = clearTextBody; 1.142 + setStdHeaders(response, body.length); 1.143 + response.setHeader("Cache-Control", "no-store", false); 1.144 + switch (case_3_request_no) { 1.145 + case 0: 1.146 + do_check_false(metadata.hasHeader("Range")); 1.147 + body = body.slice(0, partial_data_length); 1.148 + response.processAsync(); 1.149 + response.bodyOutputStream.write(body, body.length); 1.150 + response.finish(); 1.151 + break; 1.152 + case 1: 1.153 + do_check_false(metadata.hasHeader("Range")); 1.154 + response.bodyOutputStream.write(body, body.length); 1.155 + break; 1.156 + default: 1.157 + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 1.158 + } 1.159 + case_3_request_no++; 1.160 +} 1.161 +function received_partial_3(request, data) { 1.162 + do_check_eq(partial_data_length, data.length); 1.163 + var chan = make_channel("http://localhost:" + port + "/test_3"); 1.164 + chan.asyncOpen(new ChannelListener(received_cleartext, null), null); 1.165 +} 1.166 + 1.167 +var case_4_request_no = 0; 1.168 +function handler_4(metadata, response) { 1.169 + switch (case_4_request_no) { 1.170 + case 0: 1.171 + do_check_false(metadata.hasHeader("Range")); 1.172 + var body = encodedBody; 1.173 + setStdHeaders(response, body.length); 1.174 + response.setHeader("Content-Encoding", "gzip", false); 1.175 + body = body.slice(0, partial_data_length); 1.176 + var bos = Cc["@mozilla.org/binaryoutputstream;1"] 1.177 + .createInstance(Ci.nsIBinaryOutputStream); 1.178 + bos.setOutputStream(response.bodyOutputStream); 1.179 + response.processAsync(); 1.180 + bos.writeByteArray(body, body.length); 1.181 + response.finish(); 1.182 + break; 1.183 + case 1: 1.184 + do_check_false(metadata.hasHeader("Range")); 1.185 + setStdHeaders(response, clearTextBody.length); 1.186 + response.bodyOutputStream.write(clearTextBody, clearTextBody.length); 1.187 + break; 1.188 + default: 1.189 + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 1.190 + } 1.191 + case_4_request_no++; 1.192 +} 1.193 +function received_partial_4(request, data) { 1.194 +// checking length does not work with encoded data 1.195 +// do_check_eq(partial_data_length, data.length); 1.196 + var chan = make_channel("http://localhost:" + port + "/test_4"); 1.197 + chan.asyncOpen(new MyListener(received_cleartext), null); 1.198 +} 1.199 + 1.200 +var case_5_request_no = 0; 1.201 +function handler_5(metadata, response) { 1.202 + var body = clearTextBody; 1.203 + setStdHeaders(response, body.length); 1.204 + switch (case_5_request_no) { 1.205 + case 0: 1.206 + do_check_false(metadata.hasHeader("Range")); 1.207 + body = body.slice(0, partial_data_length); 1.208 + response.processAsync(); 1.209 + response.bodyOutputStream.write(body, body.length); 1.210 + response.finish(); 1.211 + break; 1.212 + case 1: 1.213 + do_check_false(metadata.hasHeader("Range")); 1.214 + response.bodyOutputStream.write(body, body.length); 1.215 + break; 1.216 + default: 1.217 + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 1.218 + } 1.219 + case_5_request_no++; 1.220 +} 1.221 +function received_partial_5(request, data) { 1.222 + do_check_eq(partial_data_length, data.length); 1.223 + var chan = make_channel("http://localhost:" + port + "/test_5"); 1.224 + chan.setRequestHeader("If-Match", "Some eTag", false); 1.225 + chan.asyncOpen(new ChannelListener(received_cleartext, null), null); 1.226 +} 1.227 + 1.228 +var case_6_request_no = 0; 1.229 +function handler_6(metadata, response) { 1.230 + switch (case_6_request_no) { 1.231 + case 0: 1.232 + do_check_false(metadata.hasHeader("Range")); 1.233 + var body = clearTextBody; 1.234 + setStdHeaders(response, body.length); 1.235 + response.setHeader("Accept-Ranges", "", false); 1.236 + body = body.slice(0, partial_data_length); 1.237 + response.processAsync(); 1.238 + response.bodyOutputStream.write(body, body.length); 1.239 + response.finish(); 1.240 + break; 1.241 + case 1: 1.242 + do_check_false(metadata.hasHeader("Range")); 1.243 + setStdHeaders(response, clearTextBody.length); 1.244 + response.bodyOutputStream.write(clearTextBody, clearTextBody.length); 1.245 + break; 1.246 + default: 1.247 + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 1.248 + } 1.249 + case_6_request_no++; 1.250 +} 1.251 +function received_partial_6(request, data) { 1.252 +// would like to verify that the response does not have Accept-Ranges 1.253 + do_check_eq(partial_data_length, data.length); 1.254 + var chan = make_channel("http://localhost:" + port + "/test_6"); 1.255 + chan.asyncOpen(new ChannelListener(received_cleartext, null), null); 1.256 +} 1.257 + 1.258 +const simpleBody = "0123456789"; 1.259 + 1.260 +function received_simple(request, data) { 1.261 + do_check_eq(simpleBody, data); 1.262 + testFinished(); 1.263 +} 1.264 + 1.265 +var case_7_request_no = 0; 1.266 +function handler_7(metadata, response) { 1.267 + switch (case_7_request_no) { 1.268 + case 0: 1.269 + do_check_false(metadata.hasHeader("Range")); 1.270 + response.setHeader("Content-Type", "text/plain", false); 1.271 + response.setHeader("ETag", "test7Etag"); 1.272 + response.setHeader("Accept-Ranges", "bytes"); 1.273 + response.setHeader("Cache-Control", "max-age=360000"); 1.274 + response.setHeader("Content-Length", "10"); 1.275 + response.processAsync(); 1.276 + response.bodyOutputStream.write(simpleBody.slice(0, 4), 4); 1.277 + response.finish(); 1.278 + break; 1.279 + case 1: 1.280 + response.setHeader("Content-Type", "text/plain", false); 1.281 + response.setHeader("ETag", "test7Etag"); 1.282 + if (metadata.hasHeader("Range")) { 1.283 + do_check_true(metadata.hasHeader("If-Range")); 1.284 + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); 1.285 + response.setHeader("Content-Range", "4-9/10"); 1.286 + response.setHeader("Content-Length", "6"); 1.287 + response.bodyOutputStream.write(simpleBody.slice(4), 6); 1.288 + } else { 1.289 + response.setHeader("Content-Length", "10"); 1.290 + response.bodyOutputStream.write(simpleBody, 10); 1.291 + } 1.292 + break; 1.293 + default: 1.294 + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 1.295 + } 1.296 + case_7_request_no++; 1.297 +} 1.298 +function received_partial_7(request, data) { 1.299 + // make sure we get the first 4 bytes 1.300 + do_check_eq(4, data.length); 1.301 + // do it again to get the rest 1.302 + var chan = make_channel("http://localhost:" + port + "/test_7"); 1.303 + chan.asyncOpen(new ChannelListener(received_simple, null), null); 1.304 +} 1.305 + 1.306 +var case_8_request_no = 0; 1.307 +function handler_8(metadata, response) { 1.308 + switch (case_8_request_no) { 1.309 + case 0: 1.310 + do_check_false(metadata.hasHeader("Range")); 1.311 + response.setHeader("Content-Type", "text/plain", false); 1.312 + response.setHeader("ETag", "test8Etag"); 1.313 + response.setHeader("Accept-Ranges", "bytes"); 1.314 + response.setHeader("Cache-Control", "max-age=360000"); 1.315 + response.setHeader("Content-Length", "10"); 1.316 + response.processAsync(); 1.317 + response.bodyOutputStream.write(simpleBody.slice(0, 4), 4); 1.318 + response.finish(); 1.319 + break; 1.320 + case 1: 1.321 + if (metadata.hasHeader("Range")) { 1.322 + do_check_true(metadata.hasHeader("If-Range")); 1.323 + case_8_range_request = true; 1.324 + } 1.325 + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); 1.326 + response.setHeader("Content-Type", "text/plain", false); 1.327 + response.setHeader("ETag", "test8Etag"); 1.328 + response.setHeader("Content-Range", "4-8/9"); // intentionally broken 1.329 + response.setHeader("Content-Length", "5"); 1.330 + response.bodyOutputStream.write(simpleBody.slice(4), 5); 1.331 + break; 1.332 + default: 1.333 + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 1.334 + } 1.335 + case_8_request_no++; 1.336 +} 1.337 +function received_partial_8(request, data) { 1.338 + // make sure we get the first 4 bytes 1.339 + do_check_eq(4, data.length); 1.340 + // do it again to get the rest 1.341 + var chan = make_channel("http://localhost:" + port + "/test_8"); 1.342 + chan.asyncOpen(new FailedChannelListener(testFinished, null, CL_EXPECT_LATE_FAILURE), null); 1.343 +} 1.344 + 1.345 +// Simple mechanism to keep track of tests and stop the server 1.346 +var numTestsFinished = 0; 1.347 +function testFinished() { 1.348 + if (++numTestsFinished == 7) 1.349 + httpserver.stop(do_test_finished); 1.350 +} 1.351 + 1.352 +function run_test() { 1.353 + httpserver = new HttpServer(); 1.354 + httpserver.registerPathHandler("/test_2", handler_2); 1.355 + httpserver.registerPathHandler("/test_3", handler_3); 1.356 + httpserver.registerPathHandler("/test_4", handler_4); 1.357 + httpserver.registerPathHandler("/test_5", handler_5); 1.358 + httpserver.registerPathHandler("/test_6", handler_6); 1.359 + httpserver.registerPathHandler("/test_7", handler_7); 1.360 + httpserver.registerPathHandler("/test_8", handler_8); 1.361 + httpserver.start(-1); 1.362 + 1.363 + port = httpserver.identity.primaryPort; 1.364 + 1.365 + // wipe out cached content 1.366 + evict_cache_entries(); 1.367 + 1.368 + // Case 2: zero-length partial entry must not trigger range-request 1.369 + var chan = make_channel("http://localhost:" + port + "/test_2"); 1.370 + chan.asyncOpen(new Canceler(received_partial_2), null); 1.371 + 1.372 + // Case 3: no-store response must not trigger range-request 1.373 + var chan = make_channel("http://localhost:" + port + "/test_3"); 1.374 + chan.asyncOpen(new MyListener(received_partial_3), null); 1.375 + 1.376 + // Case 4: response with content-encoding must not trigger range-request 1.377 + var chan = make_channel("http://localhost:" + port + "/test_4"); 1.378 + chan.asyncOpen(new MyListener(received_partial_4), null); 1.379 + 1.380 + // Case 5: conditional request-header set by client 1.381 + var chan = make_channel("http://localhost:" + port + "/test_5"); 1.382 + chan.asyncOpen(new MyListener(received_partial_5), null); 1.383 + 1.384 + // Case 6: response is not resumable (drop the Accept-Ranges header) 1.385 + var chan = make_channel("http://localhost:" + port + "/test_6"); 1.386 + chan.asyncOpen(new MyListener(received_partial_6), null); 1.387 + 1.388 + // Case 7: a basic positive test 1.389 + var chan = make_channel("http://localhost:" + port + "/test_7"); 1.390 + chan.asyncOpen(new MyListener(received_partial_7), null); 1.391 + 1.392 + // Case 8: check that mismatched 206 and 200 sizes throw error 1.393 + var chan = make_channel("http://localhost:" + port + "/test_8"); 1.394 + chan.asyncOpen(new MyListener(received_partial_8), null); 1.395 + 1.396 + do_test_pending(); 1.397 +}