|
1 /* Tests various aspects of nsIResumableChannel in combination with HTTP */ |
|
2 |
|
3 Cu.import("resource://testing-common/httpd.js"); |
|
4 |
|
5 XPCOMUtils.defineLazyGetter(this, "URL", function() { |
|
6 return "http://localhost:" + httpserver.identity.primaryPort; |
|
7 }); |
|
8 |
|
9 var httpserver = null; |
|
10 |
|
11 const NS_ERROR_ENTITY_CHANGED = 0x804b0020; |
|
12 const NS_ERROR_NOT_RESUMABLE = 0x804b0019; |
|
13 |
|
14 const rangeBody = "Body of the range request handler.\r\n"; |
|
15 |
|
16 function make_channel(url, callback, ctx) { |
|
17 var ios = Cc["@mozilla.org/network/io-service;1"]. |
|
18 getService(Ci.nsIIOService); |
|
19 return ios.newChannel(url, "", null); |
|
20 } |
|
21 |
|
22 function AuthPrompt2() { |
|
23 } |
|
24 |
|
25 AuthPrompt2.prototype = { |
|
26 user: "guest", |
|
27 pass: "guest", |
|
28 |
|
29 QueryInterface: function authprompt2_qi(iid) { |
|
30 if (iid.equals(Components.interfaces.nsISupports) || |
|
31 iid.equals(Components.interfaces.nsIAuthPrompt2)) |
|
32 return this; |
|
33 throw Components.results.NS_ERROR_NO_INTERFACE; |
|
34 }, |
|
35 |
|
36 promptAuth: |
|
37 function ap2_promptAuth(channel, level, authInfo) |
|
38 { |
|
39 authInfo.username = this.user; |
|
40 authInfo.password = this.pass; |
|
41 return true; |
|
42 }, |
|
43 |
|
44 asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) { |
|
45 throw 0x80004001; |
|
46 } |
|
47 }; |
|
48 |
|
49 function Requestor() { |
|
50 } |
|
51 |
|
52 Requestor.prototype = { |
|
53 QueryInterface: function requestor_qi(iid) { |
|
54 if (iid.equals(Components.interfaces.nsISupports) || |
|
55 iid.equals(Components.interfaces.nsIInterfaceRequestor)) |
|
56 return this; |
|
57 throw Components.results.NS_ERROR_NO_INTERFACE; |
|
58 }, |
|
59 |
|
60 getInterface: function requestor_gi(iid) { |
|
61 if (iid.equals(Components.interfaces.nsIAuthPrompt2)) { |
|
62 // Allow the prompt to store state by caching it here |
|
63 if (!this.prompt2) |
|
64 this.prompt2 = new AuthPrompt2(); |
|
65 return this.prompt2; |
|
66 } |
|
67 |
|
68 throw Components.results.NS_ERROR_NO_INTERFACE; |
|
69 }, |
|
70 |
|
71 prompt2: null |
|
72 }; |
|
73 |
|
74 function run_test() { |
|
75 dump("*** run_test\n"); |
|
76 httpserver = new HttpServer(); |
|
77 httpserver.registerPathHandler("/auth", authHandler); |
|
78 httpserver.registerPathHandler("/range", rangeHandler); |
|
79 httpserver.registerPathHandler("/acceptranges", acceptRangesHandler); |
|
80 httpserver.registerPathHandler("/redir", redirHandler); |
|
81 |
|
82 var entityID; |
|
83 |
|
84 function get_entity_id(request, data, ctx) { |
|
85 dump("*** get_entity_id()\n"); |
|
86 do_check_true(request instanceof Ci.nsIResumableChannel, |
|
87 "must be a resumable channel"); |
|
88 entityID = request.entityID; |
|
89 dump("*** entity id = " + entityID + "\n"); |
|
90 |
|
91 // Try a non-resumable URL (responds with 200) |
|
92 var chan = make_channel(URL); |
|
93 chan.nsIResumableChannel.resumeAt(1, entityID); |
|
94 chan.asyncOpen(new ChannelListener(try_resume, null, CL_EXPECT_FAILURE), null); |
|
95 } |
|
96 |
|
97 function try_resume(request, data, ctx) { |
|
98 dump("*** try_resume()\n"); |
|
99 do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); |
|
100 |
|
101 // Try a successful resume |
|
102 var chan = make_channel(URL + "/range"); |
|
103 chan.nsIResumableChannel.resumeAt(1, entityID); |
|
104 chan.asyncOpen(new ChannelListener(try_resume_zero, null), null); |
|
105 } |
|
106 |
|
107 function try_resume_zero(request, data, ctx) { |
|
108 dump("*** try_resume_zero()\n"); |
|
109 do_check_true(request.nsIHttpChannel.requestSucceeded); |
|
110 do_check_eq(data, rangeBody.substring(1)); |
|
111 |
|
112 // Try a server which doesn't support range requests |
|
113 var chan = make_channel(URL + "/acceptranges"); |
|
114 chan.nsIResumableChannel.resumeAt(0, entityID); |
|
115 chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "none", false); |
|
116 chan.asyncOpen(new ChannelListener(try_no_range, null, CL_EXPECT_FAILURE), null); |
|
117 } |
|
118 |
|
119 function try_no_range(request, data, ctx) { |
|
120 dump("*** try_no_range()\n"); |
|
121 do_check_true(request.nsIHttpChannel.requestSucceeded); |
|
122 do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); |
|
123 |
|
124 // Try a server which supports "bytes" range requests |
|
125 var chan = make_channel(URL + "/acceptranges"); |
|
126 chan.nsIResumableChannel.resumeAt(0, entityID); |
|
127 chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytes", false); |
|
128 chan.asyncOpen(new ChannelListener(try_bytes_range, null), null); |
|
129 } |
|
130 |
|
131 function try_bytes_range(request, data, ctx) { |
|
132 dump("*** try_bytes_range()\n"); |
|
133 do_check_true(request.nsIHttpChannel.requestSucceeded); |
|
134 do_check_eq(data, rangeBody); |
|
135 |
|
136 // Try a server which supports "foo" and "bar" range requests |
|
137 var chan = make_channel(URL + "/acceptranges"); |
|
138 chan.nsIResumableChannel.resumeAt(0, entityID); |
|
139 chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foo, bar", false); |
|
140 chan.asyncOpen(new ChannelListener(try_foo_bar_range, null, CL_EXPECT_FAILURE), null); |
|
141 } |
|
142 |
|
143 function try_foo_bar_range(request, data, ctx) { |
|
144 dump("*** try_foo_bar_range()\n"); |
|
145 do_check_true(request.nsIHttpChannel.requestSucceeded); |
|
146 do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); |
|
147 |
|
148 // Try a server which supports "foobar" range requests |
|
149 var chan = make_channel(URL + "/acceptranges"); |
|
150 chan.nsIResumableChannel.resumeAt(0, entityID); |
|
151 chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foobar", false); |
|
152 chan.asyncOpen(new ChannelListener(try_foobar_range, null, CL_EXPECT_FAILURE), null); |
|
153 } |
|
154 |
|
155 function try_foobar_range(request, data, ctx) { |
|
156 dump("*** try_foobar_range()\n"); |
|
157 do_check_true(request.nsIHttpChannel.requestSucceeded); |
|
158 do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); |
|
159 |
|
160 // Try a server which supports "bytes" and "foobar" range requests |
|
161 var chan = make_channel(URL + "/acceptranges"); |
|
162 chan.nsIResumableChannel.resumeAt(0, entityID); |
|
163 chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytes, foobar", false); |
|
164 chan.asyncOpen(new ChannelListener(try_bytes_foobar_range, null), null); |
|
165 } |
|
166 |
|
167 function try_bytes_foobar_range(request, data, ctx) { |
|
168 dump("*** try_bytes_foobar_range()\n"); |
|
169 do_check_true(request.nsIHttpChannel.requestSucceeded); |
|
170 do_check_eq(data, rangeBody); |
|
171 |
|
172 // Try a server which supports "bytesfoo" and "bar" range requests |
|
173 var chan = make_channel(URL + "/acceptranges"); |
|
174 chan.nsIResumableChannel.resumeAt(0, entityID); |
|
175 chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytesfoo, bar", false); |
|
176 chan.asyncOpen(new ChannelListener(try_bytesfoo_bar_range, null, CL_EXPECT_FAILURE), null); |
|
177 } |
|
178 |
|
179 function try_bytesfoo_bar_range(request, data, ctx) { |
|
180 dump("*** try_bytesfoo_bar_range()\n"); |
|
181 do_check_true(request.nsIHttpChannel.requestSucceeded); |
|
182 do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); |
|
183 |
|
184 // Try a server which doesn't send Accept-Ranges header at all |
|
185 var chan = make_channel(URL + "/acceptranges"); |
|
186 chan.nsIResumableChannel.resumeAt(0, entityID); |
|
187 chan.asyncOpen(new ChannelListener(try_no_accept_ranges, null), null); |
|
188 } |
|
189 |
|
190 function try_no_accept_ranges(request, data, ctx) { |
|
191 dump("*** try_no_accept_ranges()\n"); |
|
192 do_check_true(request.nsIHttpChannel.requestSucceeded); |
|
193 do_check_eq(data, rangeBody); |
|
194 |
|
195 // Try a successful suspend/resume from 0 |
|
196 var chan = make_channel(URL + "/range"); |
|
197 chan.nsIResumableChannel.resumeAt(0, entityID); |
|
198 chan.asyncOpen(new ChannelListener(try_suspend_resume, null, |
|
199 CL_SUSPEND | CL_EXPECT_3S_DELAY), null); |
|
200 } |
|
201 |
|
202 function try_suspend_resume(request, data, ctx) { |
|
203 dump("*** try_suspend_resume()\n"); |
|
204 do_check_true(request.nsIHttpChannel.requestSucceeded); |
|
205 do_check_eq(data, rangeBody); |
|
206 |
|
207 // Try a successful resume from 0 |
|
208 var chan = make_channel(URL + "/range"); |
|
209 chan.nsIResumableChannel.resumeAt(0, entityID); |
|
210 chan.asyncOpen(new ChannelListener(success, null), null); |
|
211 } |
|
212 |
|
213 function success(request, data, ctx) { |
|
214 dump("*** success()\n"); |
|
215 do_check_true(request.nsIHttpChannel.requestSucceeded); |
|
216 do_check_eq(data, rangeBody); |
|
217 |
|
218 |
|
219 // Authentication (no password; working resume) |
|
220 // (should not give us any data) |
|
221 var chan = make_channel(URL + "/range"); |
|
222 chan.nsIResumableChannel.resumeAt(1, entityID); |
|
223 chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false); |
|
224 chan.asyncOpen(new ChannelListener(test_auth_nopw, null, CL_EXPECT_FAILURE), null); |
|
225 } |
|
226 |
|
227 function test_auth_nopw(request, data, ctx) { |
|
228 dump("*** test_auth_nopw()\n"); |
|
229 do_check_false(request.nsIHttpChannel.requestSucceeded); |
|
230 do_check_eq(request.status, NS_ERROR_ENTITY_CHANGED); |
|
231 |
|
232 // Authentication + not working resume |
|
233 var chan = make_channel("http://guest:guest@localhost:" + |
|
234 httpserver.identity.primaryPort + "/auth"); |
|
235 chan.nsIResumableChannel.resumeAt(1, entityID); |
|
236 chan.notificationCallbacks = new Requestor(); |
|
237 chan.asyncOpen(new ChannelListener(test_auth, null, CL_EXPECT_FAILURE), null); |
|
238 } |
|
239 function test_auth(request, data, ctx) { |
|
240 dump("*** test_auth()\n"); |
|
241 do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); |
|
242 do_check_true(request.nsIHttpChannel.responseStatus < 300); |
|
243 |
|
244 // Authentication + working resume |
|
245 var chan = make_channel("http://guest:guest@localhost:" + |
|
246 httpserver.identity.primaryPort + "/range"); |
|
247 chan.nsIResumableChannel.resumeAt(1, entityID); |
|
248 chan.notificationCallbacks = new Requestor(); |
|
249 chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false); |
|
250 chan.asyncOpen(new ChannelListener(test_auth_resume, null), null); |
|
251 } |
|
252 |
|
253 function test_auth_resume(request, data, ctx) { |
|
254 dump("*** test_auth_resume()\n"); |
|
255 do_check_eq(data, rangeBody.substring(1)); |
|
256 do_check_true(request.nsIHttpChannel.requestSucceeded); |
|
257 |
|
258 // 404 page (same content length as real content) |
|
259 var chan = make_channel(URL + "/range"); |
|
260 chan.nsIResumableChannel.resumeAt(1, entityID); |
|
261 chan.nsIHttpChannel.setRequestHeader("X-Want-404", "true", false); |
|
262 chan.asyncOpen(new ChannelListener(test_404, null, CL_EXPECT_FAILURE), null); |
|
263 } |
|
264 |
|
265 function test_404(request, data, ctx) { |
|
266 dump("*** test_404()\n"); |
|
267 do_check_eq(request.status, NS_ERROR_ENTITY_CHANGED); |
|
268 do_check_eq(request.nsIHttpChannel.responseStatus, 404); |
|
269 |
|
270 // 416 Requested Range Not Satisfiable |
|
271 var chan = make_channel(URL + "/range"); |
|
272 chan.nsIResumableChannel.resumeAt(1000, entityID); |
|
273 chan.asyncOpen(new ChannelListener(test_416, null, CL_EXPECT_FAILURE), null); |
|
274 } |
|
275 |
|
276 function test_416(request, data, ctx) { |
|
277 dump("*** test_416()\n"); |
|
278 do_check_eq(request.status, NS_ERROR_ENTITY_CHANGED); |
|
279 do_check_eq(request.nsIHttpChannel.responseStatus, 416); |
|
280 |
|
281 // Redirect + successful resume |
|
282 var chan = make_channel(URL + "/redir"); |
|
283 chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/range", false); |
|
284 chan.nsIResumableChannel.resumeAt(1, entityID); |
|
285 chan.asyncOpen(new ChannelListener(test_redir_resume, null), null); |
|
286 } |
|
287 |
|
288 function test_redir_resume(request, data, ctx) { |
|
289 dump("*** test_redir_resume()\n"); |
|
290 do_check_true(request.nsIHttpChannel.requestSucceeded); |
|
291 do_check_eq(data, rangeBody.substring(1)); |
|
292 do_check_eq(request.nsIHttpChannel.responseStatus, 206); |
|
293 |
|
294 // Redirect + failed resume |
|
295 var chan = make_channel(URL + "/redir"); |
|
296 chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/", false); |
|
297 chan.nsIResumableChannel.resumeAt(1, entityID); |
|
298 chan.asyncOpen(new ChannelListener(test_redir_noresume, null, CL_EXPECT_FAILURE), null); |
|
299 } |
|
300 |
|
301 function test_redir_noresume(request, data, ctx) { |
|
302 dump("*** test_redir_noresume()\n"); |
|
303 do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); |
|
304 |
|
305 httpserver.stop(do_test_finished); |
|
306 } |
|
307 |
|
308 httpserver.start(-1); |
|
309 var chan = make_channel(URL + "/range"); |
|
310 chan.asyncOpen(new ChannelListener(get_entity_id, null), null); |
|
311 do_test_pending(); |
|
312 } |
|
313 |
|
314 // HANDLERS |
|
315 |
|
316 function handleAuth(metadata, response) { |
|
317 // btoa("guest:guest"), but that function is not available here |
|
318 var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; |
|
319 |
|
320 var body; |
|
321 if (metadata.hasHeader("Authorization") && |
|
322 metadata.getHeader("Authorization") == expectedHeader) |
|
323 { |
|
324 response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); |
|
325 response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); |
|
326 |
|
327 return true; |
|
328 } |
|
329 else |
|
330 { |
|
331 // didn't know guest:guest, failure |
|
332 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); |
|
333 response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); |
|
334 |
|
335 return false; |
|
336 } |
|
337 } |
|
338 |
|
339 // /auth |
|
340 function authHandler(metadata, response) { |
|
341 response.setHeader("Content-Type", "text/html", false); |
|
342 var body = handleAuth(metadata, response) ? "success" : "failure"; |
|
343 response.bodyOutputStream.write(body, body.length); |
|
344 } |
|
345 |
|
346 // /range |
|
347 function rangeHandler(metadata, response) { |
|
348 response.setHeader("Content-Type", "text/html", false); |
|
349 |
|
350 if (metadata.hasHeader("X-Need-Auth")) { |
|
351 if (!handleAuth(metadata, response)) { |
|
352 body = "auth failed"; |
|
353 response.bodyOutputStream.write(body, body.length); |
|
354 return; |
|
355 } |
|
356 } |
|
357 |
|
358 if (metadata.hasHeader("X-Want-404")) { |
|
359 response.setStatusLine(metadata.httpVersion, 404, "Not Found"); |
|
360 body = rangeBody; |
|
361 response.bodyOutputStream.write(body, body.length); |
|
362 return; |
|
363 } |
|
364 |
|
365 var body = rangeBody; |
|
366 |
|
367 if (metadata.hasHeader("Range")) { |
|
368 // Syntax: bytes=[from]-[to] (we don't support multiple ranges) |
|
369 var matches = metadata.getHeader("Range").match(/^\s*bytes=(\d+)?-(\d+)?\s*$/); |
|
370 var from = (matches[1] === undefined) ? 0 : matches[1]; |
|
371 var to = (matches[2] === undefined) ? rangeBody.length - 1 : matches[2]; |
|
372 if (from >= rangeBody.length) { |
|
373 response.setStatusLine(metadata.httpVersion, 416, "Start pos too high"); |
|
374 response.setHeader("Content-Range", "*/" + rangeBody.length, false); |
|
375 return; |
|
376 } |
|
377 body = body.substring(from, to + 1); |
|
378 // always respond to successful range requests with 206 |
|
379 response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); |
|
380 response.setHeader("Content-Range", from + "-" + to + "/" + rangeBody.length, false); |
|
381 } |
|
382 |
|
383 response.bodyOutputStream.write(body, body.length); |
|
384 } |
|
385 |
|
386 // /acceptranges |
|
387 function acceptRangesHandler(metadata, response) { |
|
388 response.setHeader("Content-Type", "text/html", false); |
|
389 if (metadata.hasHeader("X-Range-Type")) |
|
390 response.setHeader("Accept-Ranges", metadata.getHeader("X-Range-Type"), false); |
|
391 response.bodyOutputStream.write(rangeBody, rangeBody.length); |
|
392 } |
|
393 |
|
394 // /redir |
|
395 function redirHandler(metadata, response) { |
|
396 response.setStatusLine(metadata.httpVersion, 302, "Found"); |
|
397 response.setHeader("Content-Type", "text/html", false); |
|
398 response.setHeader("Location", metadata.getHeader("X-Redir-To"), false); |
|
399 var body = "redirect\r\n"; |
|
400 response.bodyOutputStream.write(body, body.length); |
|
401 } |
|
402 |
|
403 |