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