1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/netwerk/test/unit/test_resumable_channel.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,403 @@ 1.4 +/* Tests various aspects of nsIResumableChannel in combination with HTTP */ 1.5 + 1.6 +Cu.import("resource://testing-common/httpd.js"); 1.7 + 1.8 +XPCOMUtils.defineLazyGetter(this, "URL", function() { 1.9 + return "http://localhost:" + httpserver.identity.primaryPort; 1.10 +}); 1.11 + 1.12 +var httpserver = null; 1.13 + 1.14 +const NS_ERROR_ENTITY_CHANGED = 0x804b0020; 1.15 +const NS_ERROR_NOT_RESUMABLE = 0x804b0019; 1.16 + 1.17 +const rangeBody = "Body of the range request handler.\r\n"; 1.18 + 1.19 +function make_channel(url, callback, ctx) { 1.20 + var ios = Cc["@mozilla.org/network/io-service;1"]. 1.21 + getService(Ci.nsIIOService); 1.22 + return ios.newChannel(url, "", null); 1.23 +} 1.24 + 1.25 +function AuthPrompt2() { 1.26 +} 1.27 + 1.28 +AuthPrompt2.prototype = { 1.29 + user: "guest", 1.30 + pass: "guest", 1.31 + 1.32 + QueryInterface: function authprompt2_qi(iid) { 1.33 + if (iid.equals(Components.interfaces.nsISupports) || 1.34 + iid.equals(Components.interfaces.nsIAuthPrompt2)) 1.35 + return this; 1.36 + throw Components.results.NS_ERROR_NO_INTERFACE; 1.37 + }, 1.38 + 1.39 + promptAuth: 1.40 + function ap2_promptAuth(channel, level, authInfo) 1.41 + { 1.42 + authInfo.username = this.user; 1.43 + authInfo.password = this.pass; 1.44 + return true; 1.45 + }, 1.46 + 1.47 + asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) { 1.48 + throw 0x80004001; 1.49 + } 1.50 +}; 1.51 + 1.52 +function Requestor() { 1.53 +} 1.54 + 1.55 +Requestor.prototype = { 1.56 + QueryInterface: function requestor_qi(iid) { 1.57 + if (iid.equals(Components.interfaces.nsISupports) || 1.58 + iid.equals(Components.interfaces.nsIInterfaceRequestor)) 1.59 + return this; 1.60 + throw Components.results.NS_ERROR_NO_INTERFACE; 1.61 + }, 1.62 + 1.63 + getInterface: function requestor_gi(iid) { 1.64 + if (iid.equals(Components.interfaces.nsIAuthPrompt2)) { 1.65 + // Allow the prompt to store state by caching it here 1.66 + if (!this.prompt2) 1.67 + this.prompt2 = new AuthPrompt2(); 1.68 + return this.prompt2; 1.69 + } 1.70 + 1.71 + throw Components.results.NS_ERROR_NO_INTERFACE; 1.72 + }, 1.73 + 1.74 + prompt2: null 1.75 +}; 1.76 + 1.77 +function run_test() { 1.78 + dump("*** run_test\n"); 1.79 + httpserver = new HttpServer(); 1.80 + httpserver.registerPathHandler("/auth", authHandler); 1.81 + httpserver.registerPathHandler("/range", rangeHandler); 1.82 + httpserver.registerPathHandler("/acceptranges", acceptRangesHandler); 1.83 + httpserver.registerPathHandler("/redir", redirHandler); 1.84 + 1.85 + var entityID; 1.86 + 1.87 + function get_entity_id(request, data, ctx) { 1.88 + dump("*** get_entity_id()\n"); 1.89 + do_check_true(request instanceof Ci.nsIResumableChannel, 1.90 + "must be a resumable channel"); 1.91 + entityID = request.entityID; 1.92 + dump("*** entity id = " + entityID + "\n"); 1.93 + 1.94 + // Try a non-resumable URL (responds with 200) 1.95 + var chan = make_channel(URL); 1.96 + chan.nsIResumableChannel.resumeAt(1, entityID); 1.97 + chan.asyncOpen(new ChannelListener(try_resume, null, CL_EXPECT_FAILURE), null); 1.98 + } 1.99 + 1.100 + function try_resume(request, data, ctx) { 1.101 + dump("*** try_resume()\n"); 1.102 + do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); 1.103 + 1.104 + // Try a successful resume 1.105 + var chan = make_channel(URL + "/range"); 1.106 + chan.nsIResumableChannel.resumeAt(1, entityID); 1.107 + chan.asyncOpen(new ChannelListener(try_resume_zero, null), null); 1.108 + } 1.109 + 1.110 + function try_resume_zero(request, data, ctx) { 1.111 + dump("*** try_resume_zero()\n"); 1.112 + do_check_true(request.nsIHttpChannel.requestSucceeded); 1.113 + do_check_eq(data, rangeBody.substring(1)); 1.114 + 1.115 + // Try a server which doesn't support range requests 1.116 + var chan = make_channel(URL + "/acceptranges"); 1.117 + chan.nsIResumableChannel.resumeAt(0, entityID); 1.118 + chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "none", false); 1.119 + chan.asyncOpen(new ChannelListener(try_no_range, null, CL_EXPECT_FAILURE), null); 1.120 + } 1.121 + 1.122 + function try_no_range(request, data, ctx) { 1.123 + dump("*** try_no_range()\n"); 1.124 + do_check_true(request.nsIHttpChannel.requestSucceeded); 1.125 + do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); 1.126 + 1.127 + // Try a server which supports "bytes" range requests 1.128 + var chan = make_channel(URL + "/acceptranges"); 1.129 + chan.nsIResumableChannel.resumeAt(0, entityID); 1.130 + chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytes", false); 1.131 + chan.asyncOpen(new ChannelListener(try_bytes_range, null), null); 1.132 + } 1.133 + 1.134 + function try_bytes_range(request, data, ctx) { 1.135 + dump("*** try_bytes_range()\n"); 1.136 + do_check_true(request.nsIHttpChannel.requestSucceeded); 1.137 + do_check_eq(data, rangeBody); 1.138 + 1.139 + // Try a server which supports "foo" and "bar" range requests 1.140 + var chan = make_channel(URL + "/acceptranges"); 1.141 + chan.nsIResumableChannel.resumeAt(0, entityID); 1.142 + chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foo, bar", false); 1.143 + chan.asyncOpen(new ChannelListener(try_foo_bar_range, null, CL_EXPECT_FAILURE), null); 1.144 + } 1.145 + 1.146 + function try_foo_bar_range(request, data, ctx) { 1.147 + dump("*** try_foo_bar_range()\n"); 1.148 + do_check_true(request.nsIHttpChannel.requestSucceeded); 1.149 + do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); 1.150 + 1.151 + // Try a server which supports "foobar" range requests 1.152 + var chan = make_channel(URL + "/acceptranges"); 1.153 + chan.nsIResumableChannel.resumeAt(0, entityID); 1.154 + chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foobar", false); 1.155 + chan.asyncOpen(new ChannelListener(try_foobar_range, null, CL_EXPECT_FAILURE), null); 1.156 + } 1.157 + 1.158 + function try_foobar_range(request, data, ctx) { 1.159 + dump("*** try_foobar_range()\n"); 1.160 + do_check_true(request.nsIHttpChannel.requestSucceeded); 1.161 + do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); 1.162 + 1.163 + // Try a server which supports "bytes" and "foobar" range requests 1.164 + var chan = make_channel(URL + "/acceptranges"); 1.165 + chan.nsIResumableChannel.resumeAt(0, entityID); 1.166 + chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytes, foobar", false); 1.167 + chan.asyncOpen(new ChannelListener(try_bytes_foobar_range, null), null); 1.168 + } 1.169 + 1.170 + function try_bytes_foobar_range(request, data, ctx) { 1.171 + dump("*** try_bytes_foobar_range()\n"); 1.172 + do_check_true(request.nsIHttpChannel.requestSucceeded); 1.173 + do_check_eq(data, rangeBody); 1.174 + 1.175 + // Try a server which supports "bytesfoo" and "bar" range requests 1.176 + var chan = make_channel(URL + "/acceptranges"); 1.177 + chan.nsIResumableChannel.resumeAt(0, entityID); 1.178 + chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytesfoo, bar", false); 1.179 + chan.asyncOpen(new ChannelListener(try_bytesfoo_bar_range, null, CL_EXPECT_FAILURE), null); 1.180 + } 1.181 + 1.182 + function try_bytesfoo_bar_range(request, data, ctx) { 1.183 + dump("*** try_bytesfoo_bar_range()\n"); 1.184 + do_check_true(request.nsIHttpChannel.requestSucceeded); 1.185 + do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); 1.186 + 1.187 + // Try a server which doesn't send Accept-Ranges header at all 1.188 + var chan = make_channel(URL + "/acceptranges"); 1.189 + chan.nsIResumableChannel.resumeAt(0, entityID); 1.190 + chan.asyncOpen(new ChannelListener(try_no_accept_ranges, null), null); 1.191 + } 1.192 + 1.193 + function try_no_accept_ranges(request, data, ctx) { 1.194 + dump("*** try_no_accept_ranges()\n"); 1.195 + do_check_true(request.nsIHttpChannel.requestSucceeded); 1.196 + do_check_eq(data, rangeBody); 1.197 + 1.198 + // Try a successful suspend/resume from 0 1.199 + var chan = make_channel(URL + "/range"); 1.200 + chan.nsIResumableChannel.resumeAt(0, entityID); 1.201 + chan.asyncOpen(new ChannelListener(try_suspend_resume, null, 1.202 + CL_SUSPEND | CL_EXPECT_3S_DELAY), null); 1.203 + } 1.204 + 1.205 + function try_suspend_resume(request, data, ctx) { 1.206 + dump("*** try_suspend_resume()\n"); 1.207 + do_check_true(request.nsIHttpChannel.requestSucceeded); 1.208 + do_check_eq(data, rangeBody); 1.209 + 1.210 + // Try a successful resume from 0 1.211 + var chan = make_channel(URL + "/range"); 1.212 + chan.nsIResumableChannel.resumeAt(0, entityID); 1.213 + chan.asyncOpen(new ChannelListener(success, null), null); 1.214 + } 1.215 + 1.216 + function success(request, data, ctx) { 1.217 + dump("*** success()\n"); 1.218 + do_check_true(request.nsIHttpChannel.requestSucceeded); 1.219 + do_check_eq(data, rangeBody); 1.220 + 1.221 + 1.222 + // Authentication (no password; working resume) 1.223 + // (should not give us any data) 1.224 + var chan = make_channel(URL + "/range"); 1.225 + chan.nsIResumableChannel.resumeAt(1, entityID); 1.226 + chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false); 1.227 + chan.asyncOpen(new ChannelListener(test_auth_nopw, null, CL_EXPECT_FAILURE), null); 1.228 + } 1.229 + 1.230 + function test_auth_nopw(request, data, ctx) { 1.231 + dump("*** test_auth_nopw()\n"); 1.232 + do_check_false(request.nsIHttpChannel.requestSucceeded); 1.233 + do_check_eq(request.status, NS_ERROR_ENTITY_CHANGED); 1.234 + 1.235 + // Authentication + not working resume 1.236 + var chan = make_channel("http://guest:guest@localhost:" + 1.237 + httpserver.identity.primaryPort + "/auth"); 1.238 + chan.nsIResumableChannel.resumeAt(1, entityID); 1.239 + chan.notificationCallbacks = new Requestor(); 1.240 + chan.asyncOpen(new ChannelListener(test_auth, null, CL_EXPECT_FAILURE), null); 1.241 + } 1.242 + function test_auth(request, data, ctx) { 1.243 + dump("*** test_auth()\n"); 1.244 + do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); 1.245 + do_check_true(request.nsIHttpChannel.responseStatus < 300); 1.246 + 1.247 + // Authentication + working resume 1.248 + var chan = make_channel("http://guest:guest@localhost:" + 1.249 + httpserver.identity.primaryPort + "/range"); 1.250 + chan.nsIResumableChannel.resumeAt(1, entityID); 1.251 + chan.notificationCallbacks = new Requestor(); 1.252 + chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false); 1.253 + chan.asyncOpen(new ChannelListener(test_auth_resume, null), null); 1.254 + } 1.255 + 1.256 + function test_auth_resume(request, data, ctx) { 1.257 + dump("*** test_auth_resume()\n"); 1.258 + do_check_eq(data, rangeBody.substring(1)); 1.259 + do_check_true(request.nsIHttpChannel.requestSucceeded); 1.260 + 1.261 + // 404 page (same content length as real content) 1.262 + var chan = make_channel(URL + "/range"); 1.263 + chan.nsIResumableChannel.resumeAt(1, entityID); 1.264 + chan.nsIHttpChannel.setRequestHeader("X-Want-404", "true", false); 1.265 + chan.asyncOpen(new ChannelListener(test_404, null, CL_EXPECT_FAILURE), null); 1.266 + } 1.267 + 1.268 + function test_404(request, data, ctx) { 1.269 + dump("*** test_404()\n"); 1.270 + do_check_eq(request.status, NS_ERROR_ENTITY_CHANGED); 1.271 + do_check_eq(request.nsIHttpChannel.responseStatus, 404); 1.272 + 1.273 + // 416 Requested Range Not Satisfiable 1.274 + var chan = make_channel(URL + "/range"); 1.275 + chan.nsIResumableChannel.resumeAt(1000, entityID); 1.276 + chan.asyncOpen(new ChannelListener(test_416, null, CL_EXPECT_FAILURE), null); 1.277 + } 1.278 + 1.279 + function test_416(request, data, ctx) { 1.280 + dump("*** test_416()\n"); 1.281 + do_check_eq(request.status, NS_ERROR_ENTITY_CHANGED); 1.282 + do_check_eq(request.nsIHttpChannel.responseStatus, 416); 1.283 + 1.284 + // Redirect + successful resume 1.285 + var chan = make_channel(URL + "/redir"); 1.286 + chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/range", false); 1.287 + chan.nsIResumableChannel.resumeAt(1, entityID); 1.288 + chan.asyncOpen(new ChannelListener(test_redir_resume, null), null); 1.289 + } 1.290 + 1.291 + function test_redir_resume(request, data, ctx) { 1.292 + dump("*** test_redir_resume()\n"); 1.293 + do_check_true(request.nsIHttpChannel.requestSucceeded); 1.294 + do_check_eq(data, rangeBody.substring(1)); 1.295 + do_check_eq(request.nsIHttpChannel.responseStatus, 206); 1.296 + 1.297 + // Redirect + failed resume 1.298 + var chan = make_channel(URL + "/redir"); 1.299 + chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/", false); 1.300 + chan.nsIResumableChannel.resumeAt(1, entityID); 1.301 + chan.asyncOpen(new ChannelListener(test_redir_noresume, null, CL_EXPECT_FAILURE), null); 1.302 + } 1.303 + 1.304 + function test_redir_noresume(request, data, ctx) { 1.305 + dump("*** test_redir_noresume()\n"); 1.306 + do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); 1.307 + 1.308 + httpserver.stop(do_test_finished); 1.309 + } 1.310 + 1.311 + httpserver.start(-1); 1.312 + var chan = make_channel(URL + "/range"); 1.313 + chan.asyncOpen(new ChannelListener(get_entity_id, null), null); 1.314 + do_test_pending(); 1.315 +} 1.316 + 1.317 +// HANDLERS 1.318 + 1.319 +function handleAuth(metadata, response) { 1.320 + // btoa("guest:guest"), but that function is not available here 1.321 + var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; 1.322 + 1.323 + var body; 1.324 + if (metadata.hasHeader("Authorization") && 1.325 + metadata.getHeader("Authorization") == expectedHeader) 1.326 + { 1.327 + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); 1.328 + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); 1.329 + 1.330 + return true; 1.331 + } 1.332 + else 1.333 + { 1.334 + // didn't know guest:guest, failure 1.335 + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); 1.336 + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); 1.337 + 1.338 + return false; 1.339 + } 1.340 +} 1.341 + 1.342 +// /auth 1.343 +function authHandler(metadata, response) { 1.344 + response.setHeader("Content-Type", "text/html", false); 1.345 + var body = handleAuth(metadata, response) ? "success" : "failure"; 1.346 + response.bodyOutputStream.write(body, body.length); 1.347 +} 1.348 + 1.349 +// /range 1.350 +function rangeHandler(metadata, response) { 1.351 + response.setHeader("Content-Type", "text/html", false); 1.352 + 1.353 + if (metadata.hasHeader("X-Need-Auth")) { 1.354 + if (!handleAuth(metadata, response)) { 1.355 + body = "auth failed"; 1.356 + response.bodyOutputStream.write(body, body.length); 1.357 + return; 1.358 + } 1.359 + } 1.360 + 1.361 + if (metadata.hasHeader("X-Want-404")) { 1.362 + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 1.363 + body = rangeBody; 1.364 + response.bodyOutputStream.write(body, body.length); 1.365 + return; 1.366 + } 1.367 + 1.368 + var body = rangeBody; 1.369 + 1.370 + if (metadata.hasHeader("Range")) { 1.371 + // Syntax: bytes=[from]-[to] (we don't support multiple ranges) 1.372 + var matches = metadata.getHeader("Range").match(/^\s*bytes=(\d+)?-(\d+)?\s*$/); 1.373 + var from = (matches[1] === undefined) ? 0 : matches[1]; 1.374 + var to = (matches[2] === undefined) ? rangeBody.length - 1 : matches[2]; 1.375 + if (from >= rangeBody.length) { 1.376 + response.setStatusLine(metadata.httpVersion, 416, "Start pos too high"); 1.377 + response.setHeader("Content-Range", "*/" + rangeBody.length, false); 1.378 + return; 1.379 + } 1.380 + body = body.substring(from, to + 1); 1.381 + // always respond to successful range requests with 206 1.382 + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); 1.383 + response.setHeader("Content-Range", from + "-" + to + "/" + rangeBody.length, false); 1.384 + } 1.385 + 1.386 + response.bodyOutputStream.write(body, body.length); 1.387 +} 1.388 + 1.389 +// /acceptranges 1.390 +function acceptRangesHandler(metadata, response) { 1.391 + response.setHeader("Content-Type", "text/html", false); 1.392 + if (metadata.hasHeader("X-Range-Type")) 1.393 + response.setHeader("Accept-Ranges", metadata.getHeader("X-Range-Type"), false); 1.394 + response.bodyOutputStream.write(rangeBody, rangeBody.length); 1.395 +} 1.396 + 1.397 +// /redir 1.398 +function redirHandler(metadata, response) { 1.399 + response.setStatusLine(metadata.httpVersion, 302, "Found"); 1.400 + response.setHeader("Content-Type", "text/html", false); 1.401 + response.setHeader("Location", metadata.getHeader("X-Redir-To"), false); 1.402 + var body = "redirect\r\n"; 1.403 + response.bodyOutputStream.write(body, body.length); 1.404 +} 1.405 + 1.406 +