Wed, 31 Dec 2014 06:55:46 +0100
Added tag TORBROWSER_REPLICA for changeset 6474c204b198
1 //
2 // This test makes sure range-requests are sent and treated the way we want
3 // See bug #612135 for a thorough discussion on the subject
4 //
5 // Necko does a range-request for a partial cache-entry iff
6 //
7 // 1) size of the cached entry < value of the cached Content-Length header
8 // (not tested here - see bug #612135 comments 108-110)
9 // 2) the size of the cached entry is > 0 (see bug #628607)
10 // 3) the cached entry does not have a "no-store" Cache-Control header
11 // 4) the cached entry does not have a Content-Encoding (see bug #613159)
12 // 5) the request does not have a conditional-request header set by client
13 // 6) nsHttpResponseHead::IsResumable() is true for the cached entry
14 // 7) a basic positive test that makes sure byte ranges work
15 // 8) ensure NS_ERROR_CORRUPTED_CONTENT is thrown when total entity size
16 // of 206 does not match content-length of 200
17 //
18 // The test has one handler for each case and run_tests() fires one request
19 // for each. None of the handlers should see a Range-header.
21 Cu.import("resource://testing-common/httpd.js");
23 var httpserver = null;
25 const clearTextBody = "This is a slightly longer test\n";
26 const encodedBody = [0x1f, 0x8b, 0x08, 0x08, 0xef, 0x70, 0xe6, 0x4c, 0x00, 0x03, 0x74, 0x65, 0x78, 0x74, 0x66, 0x69,
27 0x6c, 0x65, 0x2e, 0x74, 0x78, 0x74, 0x00, 0x0b, 0xc9, 0xc8, 0x2c, 0x56, 0x00, 0xa2, 0x44, 0x85,
28 0xe2, 0x9c, 0xcc, 0xf4, 0x8c, 0x92, 0x9c, 0x4a, 0x85, 0x9c, 0xfc, 0xbc, 0xf4, 0xd4, 0x22, 0x85,
29 0x92, 0xd4, 0xe2, 0x12, 0x2e, 0x2e, 0x00, 0x00, 0xe5, 0xe6, 0xf0, 0x20, 0x00, 0x00, 0x00];
30 const decodedBody = [0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x73, 0x6c, 0x69, 0x67, 0x68, 0x74,
31 0x6c, 0x79, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x65, 0x72, 0x20, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x0a];
33 const partial_data_length = 4;
34 var port = null; // set in run_test
36 function make_channel(url, callback, ctx) {
37 var ios = Cc["@mozilla.org/network/io-service;1"].
38 getService(Ci.nsIIOService);
39 var chan = ios.newChannel(url, "", null);
40 return chan.QueryInterface(Ci.nsIHttpChannel);
41 }
43 // StreamListener which cancels its request on first data available
44 function Canceler(continueFn) {
45 this.continueFn = continueFn;
46 }
47 Canceler.prototype = {
48 QueryInterface: function(iid) {
49 if (iid.equals(Ci.nsIStreamListener) ||
50 iid.equals(Ci.nsIRequestObserver) ||
51 iid.equals(Ci.nsISupports))
52 return this;
53 throw Components.results.NS_ERROR_NO_INTERFACE;
54 },
55 onStartRequest: function(request, context) { },
57 onDataAvailable: function(request, context, stream, offset, count) {
58 request.QueryInterface(Ci.nsIChannel)
59 .cancel(Components.results.NS_BINDING_ABORTED);
60 },
61 onStopRequest: function(request, context, status) {
62 do_check_eq(status, Components.results.NS_BINDING_ABORTED);
63 this.continueFn(request, null);
64 }
65 };
66 // Simple StreamListener which performs no validations
67 function MyListener(continueFn) {
68 this.continueFn = continueFn;
69 this._buffer = null;
70 }
71 MyListener.prototype = {
72 QueryInterface: function(iid) {
73 if (iid.equals(Ci.nsIStreamListener) ||
74 iid.equals(Ci.nsIRequestObserver) ||
75 iid.equals(Ci.nsISupports))
76 return this;
77 throw Components.results.NS_ERROR_NO_INTERFACE;
78 },
79 onStartRequest: function(request, context) { this._buffer = ""; },
81 onDataAvailable: function(request, context, stream, offset, count) {
82 this._buffer = this._buffer.concat(read_stream(stream, count));
83 },
84 onStopRequest: function(request, context, status) {
85 this.continueFn(request, this._buffer);
86 }
87 };
89 var case_8_range_request = false;
90 function FailedChannelListener(continueFn) {
91 this.continueFn = continueFn;
92 }
93 FailedChannelListener.prototype = {
94 QueryInterface: function(iid) {
95 if (iid.equals(Ci.nsIStreamListener) ||
96 iid.equals(Ci.nsIRequestObserver) ||
97 iid.equals(Ci.nsISupports))
98 return this;
99 throw Components.results.NS_ERROR_NO_INTERFACE;
100 },
101 onStartRequest: function(request, context) { },
103 onDataAvailable: function(request, context, stream, offset, count) { },
105 onStopRequest: function(request, context, status) {
106 if (case_8_range_request)
107 do_check_eq(status, Components.results.NS_ERROR_CORRUPTED_CONTENT);
108 this.continueFn(request, null);
109 }
110 };
112 function received_cleartext(request, data) {
113 do_check_eq(clearTextBody, data);
114 testFinished();
115 }
117 function setStdHeaders(response, length) {
118 response.setHeader("Content-Type", "text/plain", false);
119 response.setHeader("ETag", "Just testing");
120 response.setHeader("Cache-Control", "max-age: 360000");
121 response.setHeader("Accept-Ranges", "bytes");
122 response.setHeader("Content-Length", "" + length);
123 }
125 function handler_2(metadata, response) {
126 setStdHeaders(response, clearTextBody.length);
127 do_check_false(metadata.hasHeader("Range"));
128 response.bodyOutputStream.write(clearTextBody, clearTextBody.length);
129 }
130 function received_partial_2(request, data) {
131 do_check_eq(data, undefined);
132 var chan = make_channel("http://localhost:" + port + "/test_2");
133 chan.asyncOpen(new ChannelListener(received_cleartext, null), null);
134 }
136 var case_3_request_no = 0;
137 function handler_3(metadata, response) {
138 var body = clearTextBody;
139 setStdHeaders(response, body.length);
140 response.setHeader("Cache-Control", "no-store", false);
141 switch (case_3_request_no) {
142 case 0:
143 do_check_false(metadata.hasHeader("Range"));
144 body = body.slice(0, partial_data_length);
145 response.processAsync();
146 response.bodyOutputStream.write(body, body.length);
147 response.finish();
148 break;
149 case 1:
150 do_check_false(metadata.hasHeader("Range"));
151 response.bodyOutputStream.write(body, body.length);
152 break;
153 default:
154 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
155 }
156 case_3_request_no++;
157 }
158 function received_partial_3(request, data) {
159 do_check_eq(partial_data_length, data.length);
160 var chan = make_channel("http://localhost:" + port + "/test_3");
161 chan.asyncOpen(new ChannelListener(received_cleartext, null), null);
162 }
164 var case_4_request_no = 0;
165 function handler_4(metadata, response) {
166 switch (case_4_request_no) {
167 case 0:
168 do_check_false(metadata.hasHeader("Range"));
169 var body = encodedBody;
170 setStdHeaders(response, body.length);
171 response.setHeader("Content-Encoding", "gzip", false);
172 body = body.slice(0, partial_data_length);
173 var bos = Cc["@mozilla.org/binaryoutputstream;1"]
174 .createInstance(Ci.nsIBinaryOutputStream);
175 bos.setOutputStream(response.bodyOutputStream);
176 response.processAsync();
177 bos.writeByteArray(body, body.length);
178 response.finish();
179 break;
180 case 1:
181 do_check_false(metadata.hasHeader("Range"));
182 setStdHeaders(response, clearTextBody.length);
183 response.bodyOutputStream.write(clearTextBody, clearTextBody.length);
184 break;
185 default:
186 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
187 }
188 case_4_request_no++;
189 }
190 function received_partial_4(request, data) {
191 // checking length does not work with encoded data
192 // do_check_eq(partial_data_length, data.length);
193 var chan = make_channel("http://localhost:" + port + "/test_4");
194 chan.asyncOpen(new MyListener(received_cleartext), null);
195 }
197 var case_5_request_no = 0;
198 function handler_5(metadata, response) {
199 var body = clearTextBody;
200 setStdHeaders(response, body.length);
201 switch (case_5_request_no) {
202 case 0:
203 do_check_false(metadata.hasHeader("Range"));
204 body = body.slice(0, partial_data_length);
205 response.processAsync();
206 response.bodyOutputStream.write(body, body.length);
207 response.finish();
208 break;
209 case 1:
210 do_check_false(metadata.hasHeader("Range"));
211 response.bodyOutputStream.write(body, body.length);
212 break;
213 default:
214 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
215 }
216 case_5_request_no++;
217 }
218 function received_partial_5(request, data) {
219 do_check_eq(partial_data_length, data.length);
220 var chan = make_channel("http://localhost:" + port + "/test_5");
221 chan.setRequestHeader("If-Match", "Some eTag", false);
222 chan.asyncOpen(new ChannelListener(received_cleartext, null), null);
223 }
225 var case_6_request_no = 0;
226 function handler_6(metadata, response) {
227 switch (case_6_request_no) {
228 case 0:
229 do_check_false(metadata.hasHeader("Range"));
230 var body = clearTextBody;
231 setStdHeaders(response, body.length);
232 response.setHeader("Accept-Ranges", "", false);
233 body = body.slice(0, partial_data_length);
234 response.processAsync();
235 response.bodyOutputStream.write(body, body.length);
236 response.finish();
237 break;
238 case 1:
239 do_check_false(metadata.hasHeader("Range"));
240 setStdHeaders(response, clearTextBody.length);
241 response.bodyOutputStream.write(clearTextBody, clearTextBody.length);
242 break;
243 default:
244 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
245 }
246 case_6_request_no++;
247 }
248 function received_partial_6(request, data) {
249 // would like to verify that the response does not have Accept-Ranges
250 do_check_eq(partial_data_length, data.length);
251 var chan = make_channel("http://localhost:" + port + "/test_6");
252 chan.asyncOpen(new ChannelListener(received_cleartext, null), null);
253 }
255 const simpleBody = "0123456789";
257 function received_simple(request, data) {
258 do_check_eq(simpleBody, data);
259 testFinished();
260 }
262 var case_7_request_no = 0;
263 function handler_7(metadata, response) {
264 switch (case_7_request_no) {
265 case 0:
266 do_check_false(metadata.hasHeader("Range"));
267 response.setHeader("Content-Type", "text/plain", false);
268 response.setHeader("ETag", "test7Etag");
269 response.setHeader("Accept-Ranges", "bytes");
270 response.setHeader("Cache-Control", "max-age=360000");
271 response.setHeader("Content-Length", "10");
272 response.processAsync();
273 response.bodyOutputStream.write(simpleBody.slice(0, 4), 4);
274 response.finish();
275 break;
276 case 1:
277 response.setHeader("Content-Type", "text/plain", false);
278 response.setHeader("ETag", "test7Etag");
279 if (metadata.hasHeader("Range")) {
280 do_check_true(metadata.hasHeader("If-Range"));
281 response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
282 response.setHeader("Content-Range", "4-9/10");
283 response.setHeader("Content-Length", "6");
284 response.bodyOutputStream.write(simpleBody.slice(4), 6);
285 } else {
286 response.setHeader("Content-Length", "10");
287 response.bodyOutputStream.write(simpleBody, 10);
288 }
289 break;
290 default:
291 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
292 }
293 case_7_request_no++;
294 }
295 function received_partial_7(request, data) {
296 // make sure we get the first 4 bytes
297 do_check_eq(4, data.length);
298 // do it again to get the rest
299 var chan = make_channel("http://localhost:" + port + "/test_7");
300 chan.asyncOpen(new ChannelListener(received_simple, null), null);
301 }
303 var case_8_request_no = 0;
304 function handler_8(metadata, response) {
305 switch (case_8_request_no) {
306 case 0:
307 do_check_false(metadata.hasHeader("Range"));
308 response.setHeader("Content-Type", "text/plain", false);
309 response.setHeader("ETag", "test8Etag");
310 response.setHeader("Accept-Ranges", "bytes");
311 response.setHeader("Cache-Control", "max-age=360000");
312 response.setHeader("Content-Length", "10");
313 response.processAsync();
314 response.bodyOutputStream.write(simpleBody.slice(0, 4), 4);
315 response.finish();
316 break;
317 case 1:
318 if (metadata.hasHeader("Range")) {
319 do_check_true(metadata.hasHeader("If-Range"));
320 case_8_range_request = true;
321 }
322 response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
323 response.setHeader("Content-Type", "text/plain", false);
324 response.setHeader("ETag", "test8Etag");
325 response.setHeader("Content-Range", "4-8/9"); // intentionally broken
326 response.setHeader("Content-Length", "5");
327 response.bodyOutputStream.write(simpleBody.slice(4), 5);
328 break;
329 default:
330 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
331 }
332 case_8_request_no++;
333 }
334 function received_partial_8(request, data) {
335 // make sure we get the first 4 bytes
336 do_check_eq(4, data.length);
337 // do it again to get the rest
338 var chan = make_channel("http://localhost:" + port + "/test_8");
339 chan.asyncOpen(new FailedChannelListener(testFinished, null, CL_EXPECT_LATE_FAILURE), null);
340 }
342 // Simple mechanism to keep track of tests and stop the server
343 var numTestsFinished = 0;
344 function testFinished() {
345 if (++numTestsFinished == 7)
346 httpserver.stop(do_test_finished);
347 }
349 function run_test() {
350 httpserver = new HttpServer();
351 httpserver.registerPathHandler("/test_2", handler_2);
352 httpserver.registerPathHandler("/test_3", handler_3);
353 httpserver.registerPathHandler("/test_4", handler_4);
354 httpserver.registerPathHandler("/test_5", handler_5);
355 httpserver.registerPathHandler("/test_6", handler_6);
356 httpserver.registerPathHandler("/test_7", handler_7);
357 httpserver.registerPathHandler("/test_8", handler_8);
358 httpserver.start(-1);
360 port = httpserver.identity.primaryPort;
362 // wipe out cached content
363 evict_cache_entries();
365 // Case 2: zero-length partial entry must not trigger range-request
366 var chan = make_channel("http://localhost:" + port + "/test_2");
367 chan.asyncOpen(new Canceler(received_partial_2), null);
369 // Case 3: no-store response must not trigger range-request
370 var chan = make_channel("http://localhost:" + port + "/test_3");
371 chan.asyncOpen(new MyListener(received_partial_3), null);
373 // Case 4: response with content-encoding must not trigger range-request
374 var chan = make_channel("http://localhost:" + port + "/test_4");
375 chan.asyncOpen(new MyListener(received_partial_4), null);
377 // Case 5: conditional request-header set by client
378 var chan = make_channel("http://localhost:" + port + "/test_5");
379 chan.asyncOpen(new MyListener(received_partial_5), null);
381 // Case 6: response is not resumable (drop the Accept-Ranges header)
382 var chan = make_channel("http://localhost:" + port + "/test_6");
383 chan.asyncOpen(new MyListener(received_partial_6), null);
385 // Case 7: a basic positive test
386 var chan = make_channel("http://localhost:" + port + "/test_7");
387 chan.asyncOpen(new MyListener(received_partial_7), null);
389 // Case 8: check that mismatched 206 and 200 sizes throw error
390 var chan = make_channel("http://localhost:" + port + "/test_8");
391 chan.asyncOpen(new MyListener(received_partial_8), null);
393 do_test_pending();
394 }