michael@0: /* Tests various aspects of nsIResumableChannel in combination with HTTP */ michael@0: michael@0: Cu.import("resource://testing-common/httpd.js"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "URL", function() { michael@0: return "http://localhost:" + httpserver.identity.primaryPort; michael@0: }); michael@0: michael@0: var httpserver = null; michael@0: michael@0: const NS_ERROR_ENTITY_CHANGED = 0x804b0020; michael@0: const NS_ERROR_NOT_RESUMABLE = 0x804b0019; michael@0: michael@0: const rangeBody = "Body of the range request handler.\r\n"; michael@0: michael@0: function make_channel(url, callback, ctx) { michael@0: var ios = Cc["@mozilla.org/network/io-service;1"]. michael@0: getService(Ci.nsIIOService); michael@0: return ios.newChannel(url, "", null); michael@0: } michael@0: michael@0: function AuthPrompt2() { michael@0: } michael@0: michael@0: AuthPrompt2.prototype = { michael@0: user: "guest", michael@0: pass: "guest", michael@0: michael@0: QueryInterface: function authprompt2_qi(iid) { michael@0: if (iid.equals(Components.interfaces.nsISupports) || michael@0: iid.equals(Components.interfaces.nsIAuthPrompt2)) michael@0: return this; michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: promptAuth: michael@0: function ap2_promptAuth(channel, level, authInfo) michael@0: { michael@0: authInfo.username = this.user; michael@0: authInfo.password = this.pass; michael@0: return true; michael@0: }, michael@0: michael@0: asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) { michael@0: throw 0x80004001; michael@0: } michael@0: }; michael@0: michael@0: function Requestor() { michael@0: } michael@0: michael@0: Requestor.prototype = { michael@0: QueryInterface: function requestor_qi(iid) { michael@0: if (iid.equals(Components.interfaces.nsISupports) || michael@0: iid.equals(Components.interfaces.nsIInterfaceRequestor)) michael@0: return this; michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: getInterface: function requestor_gi(iid) { michael@0: if (iid.equals(Components.interfaces.nsIAuthPrompt2)) { michael@0: // Allow the prompt to store state by caching it here michael@0: if (!this.prompt2) michael@0: this.prompt2 = new AuthPrompt2(); michael@0: return this.prompt2; michael@0: } michael@0: michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: prompt2: null michael@0: }; michael@0: michael@0: function run_test() { michael@0: dump("*** run_test\n"); michael@0: httpserver = new HttpServer(); michael@0: httpserver.registerPathHandler("/auth", authHandler); michael@0: httpserver.registerPathHandler("/range", rangeHandler); michael@0: httpserver.registerPathHandler("/acceptranges", acceptRangesHandler); michael@0: httpserver.registerPathHandler("/redir", redirHandler); michael@0: michael@0: var entityID; michael@0: michael@0: function get_entity_id(request, data, ctx) { michael@0: dump("*** get_entity_id()\n"); michael@0: do_check_true(request instanceof Ci.nsIResumableChannel, michael@0: "must be a resumable channel"); michael@0: entityID = request.entityID; michael@0: dump("*** entity id = " + entityID + "\n"); michael@0: michael@0: // Try a non-resumable URL (responds with 200) michael@0: var chan = make_channel(URL); michael@0: chan.nsIResumableChannel.resumeAt(1, entityID); michael@0: chan.asyncOpen(new ChannelListener(try_resume, null, CL_EXPECT_FAILURE), null); michael@0: } michael@0: michael@0: function try_resume(request, data, ctx) { michael@0: dump("*** try_resume()\n"); michael@0: do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); michael@0: michael@0: // Try a successful resume michael@0: var chan = make_channel(URL + "/range"); michael@0: chan.nsIResumableChannel.resumeAt(1, entityID); michael@0: chan.asyncOpen(new ChannelListener(try_resume_zero, null), null); michael@0: } michael@0: michael@0: function try_resume_zero(request, data, ctx) { michael@0: dump("*** try_resume_zero()\n"); michael@0: do_check_true(request.nsIHttpChannel.requestSucceeded); michael@0: do_check_eq(data, rangeBody.substring(1)); michael@0: michael@0: // Try a server which doesn't support range requests michael@0: var chan = make_channel(URL + "/acceptranges"); michael@0: chan.nsIResumableChannel.resumeAt(0, entityID); michael@0: chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "none", false); michael@0: chan.asyncOpen(new ChannelListener(try_no_range, null, CL_EXPECT_FAILURE), null); michael@0: } michael@0: michael@0: function try_no_range(request, data, ctx) { michael@0: dump("*** try_no_range()\n"); michael@0: do_check_true(request.nsIHttpChannel.requestSucceeded); michael@0: do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); michael@0: michael@0: // Try a server which supports "bytes" range requests michael@0: var chan = make_channel(URL + "/acceptranges"); michael@0: chan.nsIResumableChannel.resumeAt(0, entityID); michael@0: chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytes", false); michael@0: chan.asyncOpen(new ChannelListener(try_bytes_range, null), null); michael@0: } michael@0: michael@0: function try_bytes_range(request, data, ctx) { michael@0: dump("*** try_bytes_range()\n"); michael@0: do_check_true(request.nsIHttpChannel.requestSucceeded); michael@0: do_check_eq(data, rangeBody); michael@0: michael@0: // Try a server which supports "foo" and "bar" range requests michael@0: var chan = make_channel(URL + "/acceptranges"); michael@0: chan.nsIResumableChannel.resumeAt(0, entityID); michael@0: chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foo, bar", false); michael@0: chan.asyncOpen(new ChannelListener(try_foo_bar_range, null, CL_EXPECT_FAILURE), null); michael@0: } michael@0: michael@0: function try_foo_bar_range(request, data, ctx) { michael@0: dump("*** try_foo_bar_range()\n"); michael@0: do_check_true(request.nsIHttpChannel.requestSucceeded); michael@0: do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); michael@0: michael@0: // Try a server which supports "foobar" range requests michael@0: var chan = make_channel(URL + "/acceptranges"); michael@0: chan.nsIResumableChannel.resumeAt(0, entityID); michael@0: chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foobar", false); michael@0: chan.asyncOpen(new ChannelListener(try_foobar_range, null, CL_EXPECT_FAILURE), null); michael@0: } michael@0: michael@0: function try_foobar_range(request, data, ctx) { michael@0: dump("*** try_foobar_range()\n"); michael@0: do_check_true(request.nsIHttpChannel.requestSucceeded); michael@0: do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); michael@0: michael@0: // Try a server which supports "bytes" and "foobar" range requests michael@0: var chan = make_channel(URL + "/acceptranges"); michael@0: chan.nsIResumableChannel.resumeAt(0, entityID); michael@0: chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytes, foobar", false); michael@0: chan.asyncOpen(new ChannelListener(try_bytes_foobar_range, null), null); michael@0: } michael@0: michael@0: function try_bytes_foobar_range(request, data, ctx) { michael@0: dump("*** try_bytes_foobar_range()\n"); michael@0: do_check_true(request.nsIHttpChannel.requestSucceeded); michael@0: do_check_eq(data, rangeBody); michael@0: michael@0: // Try a server which supports "bytesfoo" and "bar" range requests michael@0: var chan = make_channel(URL + "/acceptranges"); michael@0: chan.nsIResumableChannel.resumeAt(0, entityID); michael@0: chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytesfoo, bar", false); michael@0: chan.asyncOpen(new ChannelListener(try_bytesfoo_bar_range, null, CL_EXPECT_FAILURE), null); michael@0: } michael@0: michael@0: function try_bytesfoo_bar_range(request, data, ctx) { michael@0: dump("*** try_bytesfoo_bar_range()\n"); michael@0: do_check_true(request.nsIHttpChannel.requestSucceeded); michael@0: do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); michael@0: michael@0: // Try a server which doesn't send Accept-Ranges header at all michael@0: var chan = make_channel(URL + "/acceptranges"); michael@0: chan.nsIResumableChannel.resumeAt(0, entityID); michael@0: chan.asyncOpen(new ChannelListener(try_no_accept_ranges, null), null); michael@0: } michael@0: michael@0: function try_no_accept_ranges(request, data, ctx) { michael@0: dump("*** try_no_accept_ranges()\n"); michael@0: do_check_true(request.nsIHttpChannel.requestSucceeded); michael@0: do_check_eq(data, rangeBody); michael@0: michael@0: // Try a successful suspend/resume from 0 michael@0: var chan = make_channel(URL + "/range"); michael@0: chan.nsIResumableChannel.resumeAt(0, entityID); michael@0: chan.asyncOpen(new ChannelListener(try_suspend_resume, null, michael@0: CL_SUSPEND | CL_EXPECT_3S_DELAY), null); michael@0: } michael@0: michael@0: function try_suspend_resume(request, data, ctx) { michael@0: dump("*** try_suspend_resume()\n"); michael@0: do_check_true(request.nsIHttpChannel.requestSucceeded); michael@0: do_check_eq(data, rangeBody); michael@0: michael@0: // Try a successful resume from 0 michael@0: var chan = make_channel(URL + "/range"); michael@0: chan.nsIResumableChannel.resumeAt(0, entityID); michael@0: chan.asyncOpen(new ChannelListener(success, null), null); michael@0: } michael@0: michael@0: function success(request, data, ctx) { michael@0: dump("*** success()\n"); michael@0: do_check_true(request.nsIHttpChannel.requestSucceeded); michael@0: do_check_eq(data, rangeBody); michael@0: michael@0: michael@0: // Authentication (no password; working resume) michael@0: // (should not give us any data) michael@0: var chan = make_channel(URL + "/range"); michael@0: chan.nsIResumableChannel.resumeAt(1, entityID); michael@0: chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false); michael@0: chan.asyncOpen(new ChannelListener(test_auth_nopw, null, CL_EXPECT_FAILURE), null); michael@0: } michael@0: michael@0: function test_auth_nopw(request, data, ctx) { michael@0: dump("*** test_auth_nopw()\n"); michael@0: do_check_false(request.nsIHttpChannel.requestSucceeded); michael@0: do_check_eq(request.status, NS_ERROR_ENTITY_CHANGED); michael@0: michael@0: // Authentication + not working resume michael@0: var chan = make_channel("http://guest:guest@localhost:" + michael@0: httpserver.identity.primaryPort + "/auth"); michael@0: chan.nsIResumableChannel.resumeAt(1, entityID); michael@0: chan.notificationCallbacks = new Requestor(); michael@0: chan.asyncOpen(new ChannelListener(test_auth, null, CL_EXPECT_FAILURE), null); michael@0: } michael@0: function test_auth(request, data, ctx) { michael@0: dump("*** test_auth()\n"); michael@0: do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); michael@0: do_check_true(request.nsIHttpChannel.responseStatus < 300); michael@0: michael@0: // Authentication + working resume michael@0: var chan = make_channel("http://guest:guest@localhost:" + michael@0: httpserver.identity.primaryPort + "/range"); michael@0: chan.nsIResumableChannel.resumeAt(1, entityID); michael@0: chan.notificationCallbacks = new Requestor(); michael@0: chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false); michael@0: chan.asyncOpen(new ChannelListener(test_auth_resume, null), null); michael@0: } michael@0: michael@0: function test_auth_resume(request, data, ctx) { michael@0: dump("*** test_auth_resume()\n"); michael@0: do_check_eq(data, rangeBody.substring(1)); michael@0: do_check_true(request.nsIHttpChannel.requestSucceeded); michael@0: michael@0: // 404 page (same content length as real content) michael@0: var chan = make_channel(URL + "/range"); michael@0: chan.nsIResumableChannel.resumeAt(1, entityID); michael@0: chan.nsIHttpChannel.setRequestHeader("X-Want-404", "true", false); michael@0: chan.asyncOpen(new ChannelListener(test_404, null, CL_EXPECT_FAILURE), null); michael@0: } michael@0: michael@0: function test_404(request, data, ctx) { michael@0: dump("*** test_404()\n"); michael@0: do_check_eq(request.status, NS_ERROR_ENTITY_CHANGED); michael@0: do_check_eq(request.nsIHttpChannel.responseStatus, 404); michael@0: michael@0: // 416 Requested Range Not Satisfiable michael@0: var chan = make_channel(URL + "/range"); michael@0: chan.nsIResumableChannel.resumeAt(1000, entityID); michael@0: chan.asyncOpen(new ChannelListener(test_416, null, CL_EXPECT_FAILURE), null); michael@0: } michael@0: michael@0: function test_416(request, data, ctx) { michael@0: dump("*** test_416()\n"); michael@0: do_check_eq(request.status, NS_ERROR_ENTITY_CHANGED); michael@0: do_check_eq(request.nsIHttpChannel.responseStatus, 416); michael@0: michael@0: // Redirect + successful resume michael@0: var chan = make_channel(URL + "/redir"); michael@0: chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/range", false); michael@0: chan.nsIResumableChannel.resumeAt(1, entityID); michael@0: chan.asyncOpen(new ChannelListener(test_redir_resume, null), null); michael@0: } michael@0: michael@0: function test_redir_resume(request, data, ctx) { michael@0: dump("*** test_redir_resume()\n"); michael@0: do_check_true(request.nsIHttpChannel.requestSucceeded); michael@0: do_check_eq(data, rangeBody.substring(1)); michael@0: do_check_eq(request.nsIHttpChannel.responseStatus, 206); michael@0: michael@0: // Redirect + failed resume michael@0: var chan = make_channel(URL + "/redir"); michael@0: chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/", false); michael@0: chan.nsIResumableChannel.resumeAt(1, entityID); michael@0: chan.asyncOpen(new ChannelListener(test_redir_noresume, null, CL_EXPECT_FAILURE), null); michael@0: } michael@0: michael@0: function test_redir_noresume(request, data, ctx) { michael@0: dump("*** test_redir_noresume()\n"); michael@0: do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); michael@0: michael@0: httpserver.stop(do_test_finished); michael@0: } michael@0: michael@0: httpserver.start(-1); michael@0: var chan = make_channel(URL + "/range"); michael@0: chan.asyncOpen(new ChannelListener(get_entity_id, null), null); michael@0: do_test_pending(); michael@0: } michael@0: michael@0: // HANDLERS michael@0: michael@0: function handleAuth(metadata, response) { michael@0: // btoa("guest:guest"), but that function is not available here michael@0: var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; michael@0: michael@0: var body; michael@0: if (metadata.hasHeader("Authorization") && michael@0: metadata.getHeader("Authorization") == expectedHeader) michael@0: { michael@0: response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); michael@0: response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); michael@0: michael@0: return true; michael@0: } michael@0: else michael@0: { michael@0: // didn't know guest:guest, failure michael@0: response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); michael@0: response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); michael@0: michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: // /auth michael@0: function authHandler(metadata, response) { michael@0: response.setHeader("Content-Type", "text/html", false); michael@0: var body = handleAuth(metadata, response) ? "success" : "failure"; michael@0: response.bodyOutputStream.write(body, body.length); michael@0: } michael@0: michael@0: // /range michael@0: function rangeHandler(metadata, response) { michael@0: response.setHeader("Content-Type", "text/html", false); michael@0: michael@0: if (metadata.hasHeader("X-Need-Auth")) { michael@0: if (!handleAuth(metadata, response)) { michael@0: body = "auth failed"; michael@0: response.bodyOutputStream.write(body, body.length); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: if (metadata.hasHeader("X-Want-404")) { michael@0: response.setStatusLine(metadata.httpVersion, 404, "Not Found"); michael@0: body = rangeBody; michael@0: response.bodyOutputStream.write(body, body.length); michael@0: return; michael@0: } michael@0: michael@0: var body = rangeBody; michael@0: michael@0: if (metadata.hasHeader("Range")) { michael@0: // Syntax: bytes=[from]-[to] (we don't support multiple ranges) michael@0: var matches = metadata.getHeader("Range").match(/^\s*bytes=(\d+)?-(\d+)?\s*$/); michael@0: var from = (matches[1] === undefined) ? 0 : matches[1]; michael@0: var to = (matches[2] === undefined) ? rangeBody.length - 1 : matches[2]; michael@0: if (from >= rangeBody.length) { michael@0: response.setStatusLine(metadata.httpVersion, 416, "Start pos too high"); michael@0: response.setHeader("Content-Range", "*/" + rangeBody.length, false); michael@0: return; michael@0: } michael@0: body = body.substring(from, to + 1); michael@0: // always respond to successful range requests with 206 michael@0: response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); michael@0: response.setHeader("Content-Range", from + "-" + to + "/" + rangeBody.length, false); michael@0: } michael@0: michael@0: response.bodyOutputStream.write(body, body.length); michael@0: } michael@0: michael@0: // /acceptranges michael@0: function acceptRangesHandler(metadata, response) { michael@0: response.setHeader("Content-Type", "text/html", false); michael@0: if (metadata.hasHeader("X-Range-Type")) michael@0: response.setHeader("Accept-Ranges", metadata.getHeader("X-Range-Type"), false); michael@0: response.bodyOutputStream.write(rangeBody, rangeBody.length); michael@0: } michael@0: michael@0: // /redir michael@0: function redirHandler(metadata, response) { michael@0: response.setStatusLine(metadata.httpVersion, 302, "Found"); michael@0: response.setHeader("Content-Type", "text/html", false); michael@0: response.setHeader("Location", metadata.getHeader("X-Redir-To"), false); michael@0: var body = "redirect\r\n"; michael@0: response.bodyOutputStream.write(body, body.length); michael@0: } michael@0: michael@0: