From 8f39fe8e21375edfec194dcb3e07e7d86b41e84e Mon Sep 17 00:00:00 2001 From: Chris Double Date: Fri, 17 Oct 2008 12:38:14 +1300 Subject: [PATCH] Bug 398185 - Add byte range request support to JS httpd - r=jwalden+bmo --- netwerk/test/httpserver/httpd.js | 96 +++++- .../httpserver/test/data/ranges/empty.txt | 0 .../httpserver/test/data/ranges/range.txt | 1 + .../test/httpserver/test/test_byte_range.js | 291 ++++++++++++++++++ 4 files changed, 384 insertions(+), 4 deletions(-) create mode 100644 netwerk/test/httpserver/test/data/ranges/empty.txt create mode 100644 netwerk/test/httpserver/test/data/ranges/range.txt create mode 100644 netwerk/test/httpserver/test/test_byte_range.js diff --git a/netwerk/test/httpserver/httpd.js b/netwerk/test/httpserver/httpd.js index caddb6fa4783..e0b295039517 100644 --- a/netwerk/test/httpserver/httpd.js +++ b/netwerk/test/httpserver/httpd.js @@ -2269,9 +2269,61 @@ ServerHandler.prototype = if (!file.exists()) throw HTTP_404; + var start, end; + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) && + metadata.hasHeader("Range")) + { + var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/); + if (!rangeMatch) + throw HTTP_400; + + if (rangeMatch[1] !== undefined) + start = parseInt(rangeMatch[1], 10); + + if (rangeMatch[2] !== undefined) + end = parseInt(rangeMatch[2], 10); + + if (start === undefined && end === undefined) + throw HTTP_400; + + // No start given, so the end is really the count of bytes from the + // end of the file. + if (start === undefined) + { + start = Math.max(0, file.fileSize - end); + end = file.fileSize - 1; + } + + // start and end are inclusive + if (end === undefined || end >= file.fileSize) + end = file.fileSize - 1; + + if (start !== undefined && start >= file.fileSize) + throw HTTP_416; + + if (end < start) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + start = 0; + end = file.fileSize - 1; + } + else + { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize; + response.setHeader("Content-Range", contentRange); + } + } + else + { + start = 0; + end = file.fileSize - 1; + } + // finally... - dumpn("*** handling '" + path + "' as mapping to " + file.path); - this._writeFileResponse(metadata, file, response); + dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " + + start + " to " + end + " inclusive"); + this._writeFileResponse(metadata, file, response, start, end - start + 1); }, /** @@ -2284,8 +2336,12 @@ ServerHandler.prototype = * the file which is to be sent in the response * @param response : Response * the response to which the file should be written + * @param offset: uint + * the byte offset to skip to when writing + * @param count: uint + * the number of bytes to write */ - _writeFileResponse: function(metadata, file, response) + _writeFileResponse: function(metadata, file, response, offset, count) { const PR_RDONLY = 0x01; @@ -2322,7 +2378,20 @@ ServerHandler.prototype = var fis = new FileInputStream(file, PR_RDONLY, 0444, Ci.nsIFileInputStream.CLOSE_ON_EOF); - response.bodyOutputStream.writeFrom(fis, file.fileSize); + offset = offset || 0; + count = count || file.fileSize; + + NS_ASSERT(offset == 0 || offset < file.fileSize, "bad offset"); + NS_ASSERT(count >= 0, "bad count"); + + if (offset != 0) + { + // Read and discard data up to offset so the data sent to + // the client matches the requested range request. + var sis = new ScriptableInputStream(fis); + sis.read(offset); + } + response.bodyOutputStream.writeFrom(fis, count); fis.close(); maybeAddHeaders(file, metadata, response); @@ -2794,6 +2863,25 @@ ServerHandler.prototype = "; response.bodyOutputStream.write(body, body.length); }, + 416: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, + 416, + "Requested Range Not Satisfiable"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ + \ + 416 Requested Range Not Satisfiable\ + \ +

416 Requested Range Not Satisfiable

\ +

The byte range was not valid for the\ + requested resource.\ +

\ + \ + "; + response.bodyOutputStream.write(body, body.length); + }, 500: function(metadata, response) { response.setStatusLine(metadata.httpVersion, diff --git a/netwerk/test/httpserver/test/data/ranges/empty.txt b/netwerk/test/httpserver/test/data/ranges/empty.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/netwerk/test/httpserver/test/data/ranges/range.txt b/netwerk/test/httpserver/test/data/ranges/range.txt new file mode 100644 index 000000000000..ab71eabaf034 --- /dev/null +++ b/netwerk/test/httpserver/test/data/ranges/range.txt @@ -0,0 +1 @@ +This should be seen. diff --git a/netwerk/test/httpserver/test/test_byte_range.js b/netwerk/test/httpserver/test/test_byte_range.js new file mode 100644 index 000000000000..26d98d5eaee9 --- /dev/null +++ b/netwerk/test/httpserver/test/test_byte_range.js @@ -0,0 +1,291 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is MozJSHTTP code. + * + * The Initial Developer of the Original Code is + * Chris Double . + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// checks if a byte range request and non-byte range request retrieve the +// correct data. + +const PREFIX = "http://localhost:4444"; + +var tests = + [ + new Test(PREFIX + "/range.txt", + init_byterange, start_byterange, stop_byterange), + new Test(PREFIX + "/range.txt", + init_byterange2, start_byterange2), + new Test(PREFIX + "/range.txt", + init_byterange3, start_byterange3, stop_byterange3), + new Test(PREFIX + "/range.txt", + init_byterange4, start_byterange4), + new Test(PREFIX + "/range.txt", + init_byterange5, start_byterange5, stop_byterange5), + new Test(PREFIX + "/range.txt", + init_byterange6, start_byterange6, stop_byterange6), + new Test(PREFIX + "/range.txt", + init_byterange7, start_byterange7, stop_byterange7), + new Test(PREFIX + "/range.txt", + init_byterange8, start_byterange8, stop_byterange8), + new Test(PREFIX + "/range.txt", + init_byterange9, start_byterange9, stop_byterange9), + new Test(PREFIX + "/range.txt", + init_byterange10, start_byterange10), + new Test(PREFIX + "/range.txt", + init_byterange11, start_byterange11, stop_byterange11), + new Test(PREFIX + "/empty.txt", + null, start_byterange12, stop_byterange12), + new Test(PREFIX + "/range.txt", + null, start_normal, stop_normal) + ]; + +function run_test() +{ + var srv = createServer(); + var dir = do_get_file("netwerk/test/httpserver/test/data/ranges/"); + srv.registerDirectory("/", dir); + + srv.start(4444); + + runHttpTests(tests, function() { srv.stop(); }); +} + +function start_normal(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.getResponseHeader("Content-Length"), "21"); + do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain"); +} + +function stop_normal(ch, cx, status, data) +{ + do_check_eq(data.length, 21); + do_check_eq(data[0], 0x54); + do_check_eq(data[20], 0x0a); +} + +function init_byterange(ch) +{ + ch.setRequestHeader("Range", "bytes=10-", false); +} + +function start_byterange(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); + do_check_eq(ch.getResponseHeader("Content-Length"), "11"); + do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain"); + do_check_eq(ch.getResponseHeader("Content-Range"), "bytes 10-20/21"); +} + +function stop_byterange(ch, cx, status, data) +{ + do_check_eq(data.length, 11); + do_check_eq(data[0], 0x64); + do_check_eq(data[10], 0x0a); +} + +function init_byterange2(ch) +{ + ch.setRequestHeader("Range", "bytes=21-", false); +} + +function start_byterange2(ch, cx) +{ + do_check_eq(ch.responseStatus, 416); +} + +function init_byterange3(ch) +{ + ch.setRequestHeader("Range", "bytes=10-15", false); +} + +function start_byterange3(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); + do_check_eq(ch.getResponseHeader("Content-Length"), "6"); + do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain"); + do_check_eq(ch.getResponseHeader("Content-Range"), "bytes 10-15/21"); +} + +function stop_byterange3(ch, cx, status, data) +{ + do_check_eq(data.length, 6); + do_check_eq(data[0], 0x64); + do_check_eq(data[1], 0x20); + do_check_eq(data[2], 0x62); + do_check_eq(data[3], 0x65); + do_check_eq(data[4], 0x20); + do_check_eq(data[5], 0x73); +} + +function init_byterange4(ch) +{ + ch.setRequestHeader("Range", "xbytes=21-", false); +} + +function start_byterange4(ch, cx) +{ + do_check_eq(ch.responseStatus, 400); +} + +function init_byterange5(ch) +{ + ch.setRequestHeader("Range", "bytes=-5", false); +} + +function start_byterange5(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); +} + +function stop_byterange5(ch, cx, status, data) +{ + do_check_eq(data.length, 5); + do_check_eq(data[0], 0x65); + do_check_eq(data[1], 0x65); + do_check_eq(data[2], 0x6e); + do_check_eq(data[3], 0x2e); + do_check_eq(data[4], 0x0a); +} + +function init_byterange6(ch) +{ + ch.setRequestHeader("Range", "bytes=15-12", false); +} + +function start_byterange6(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); +} + +function stop_byterange6(ch, cx, status, data) +{ + do_check_eq(data.length, 21); + do_check_eq(data[0], 0x54); + do_check_eq(data[20], 0x0a); +} + +function init_byterange7(ch) +{ + ch.setRequestHeader("Range", "bytes=0-5", false); +} + +function start_byterange7(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); + do_check_eq(ch.getResponseHeader("Content-Length"), "6"); + do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain"); + do_check_eq(ch.getResponseHeader("Content-Range"), "bytes 0-5/21"); +} + +function stop_byterange7(ch, cx, status, data) +{ + do_check_eq(data.length, 6); + do_check_eq(data[0], 0x54); + do_check_eq(data[1], 0x68); + do_check_eq(data[2], 0x69); + do_check_eq(data[3], 0x73); + do_check_eq(data[4], 0x20); + do_check_eq(data[5], 0x73); +} + +function init_byterange8(ch) +{ + ch.setRequestHeader("Range", "bytes=20-21", false); +} + +function start_byterange8(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); + do_check_eq(ch.getResponseHeader("Content-Range"), "bytes 20-20/21"); +} + +function stop_byterange8(ch, cx, status, data) +{ + do_check_eq(data.length, 1); + do_check_eq(data[0], 0x0a); +} + +function init_byterange9(ch) +{ + ch.setRequestHeader("Range", "bytes=020-021", false); +} + +function start_byterange9(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); +} + +function stop_byterange9(ch, cx, status, data) +{ + do_check_eq(data.length, 1); + do_check_eq(data[0], 0x0a); +} + +function init_byterange10(ch) +{ + ch.setRequestHeader("Range", "bytes=-", false); +} + +function start_byterange10(ch, cx) +{ + do_check_eq(ch.responseStatus, 400); +} + +function init_byterange11(ch) +{ + ch.setRequestHeader("Range", "bytes=-500", false); +} + +function start_byterange11(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); +} + +function stop_byterange11(ch, cx, status, data) +{ + do_check_eq(data.length, 21); + do_check_eq(data[0], 0x54); + do_check_eq(data[20], 0x0a); +} + +function start_byterange12(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.getResponseHeader("Content-Length"), "0"); +} + +function stop_byterange12(ch, cx, status, data) +{ + do_check_eq(data.length, 0); +}