|
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. |
|
20 |
|
21 Cu.import("resource://testing-common/httpd.js"); |
|
22 |
|
23 var httpserver = null; |
|
24 |
|
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]; |
|
32 |
|
33 const partial_data_length = 4; |
|
34 var port = null; // set in run_test |
|
35 |
|
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 } |
|
42 |
|
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) { }, |
|
56 |
|
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 = ""; }, |
|
80 |
|
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 }; |
|
88 |
|
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) { }, |
|
102 |
|
103 onDataAvailable: function(request, context, stream, offset, count) { }, |
|
104 |
|
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 }; |
|
111 |
|
112 function received_cleartext(request, data) { |
|
113 do_check_eq(clearTextBody, data); |
|
114 testFinished(); |
|
115 } |
|
116 |
|
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 } |
|
124 |
|
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 } |
|
135 |
|
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 } |
|
163 |
|
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 } |
|
196 |
|
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 } |
|
224 |
|
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 } |
|
254 |
|
255 const simpleBody = "0123456789"; |
|
256 |
|
257 function received_simple(request, data) { |
|
258 do_check_eq(simpleBody, data); |
|
259 testFinished(); |
|
260 } |
|
261 |
|
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 } |
|
302 |
|
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 } |
|
341 |
|
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 } |
|
348 |
|
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); |
|
359 |
|
360 port = httpserver.identity.primaryPort; |
|
361 |
|
362 // wipe out cached content |
|
363 evict_cache_entries(); |
|
364 |
|
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); |
|
368 |
|
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); |
|
372 |
|
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); |
|
376 |
|
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); |
|
380 |
|
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); |
|
384 |
|
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); |
|
388 |
|
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); |
|
392 |
|
393 do_test_pending(); |
|
394 } |