diff options
Diffstat (limited to 'netwerk/test/httpserver/test/head_utils.js')
-rw-r--r-- | netwerk/test/httpserver/test/head_utils.js | 578 |
1 files changed, 578 insertions, 0 deletions
diff --git a/netwerk/test/httpserver/test/head_utils.js b/netwerk/test/httpserver/test/head_utils.js new file mode 100644 index 0000000000..76e7ed6fd3 --- /dev/null +++ b/netwerk/test/httpserver/test/head_utils.js @@ -0,0 +1,578 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* global __LOCATION__ */ +/* import-globals-from ../httpd.js */ + +var _HTTPD_JS_PATH = __LOCATION__.parent; +_HTTPD_JS_PATH.append("httpd.js"); +load(_HTTPD_JS_PATH.path); + +// if these tests fail, we'll want the debug output +var linDEBUG = true; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +/** + * Constructs a new nsHttpServer instance. This function is intended to + * encapsulate construction of a server so that at some point in the future it + * is possible to run these tests (with at most slight modifications) against + * the server when used as an XPCOM component (not as an inline script). + */ +function createServer() { + return new nsHttpServer(); +} + +/** + * Creates a new HTTP channel. + * + * @param url + * the URL of the channel to create + */ +function makeChannel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +/** + * Make a binary input stream wrapper for the given stream. + * + * @param stream + * the nsIInputStream to wrap + */ +function makeBIS(stream) { + return new BinaryInputStream(stream); +} + +/** + * Returns the contents of the file as a string. + * + * @param file : nsIFile + * the file whose contents are to be read + * @returns string + * the contents of the file + */ +function fileContents(file) { + const PR_RDONLY = 0x01; + var fis = new FileInputStream( + file, + PR_RDONLY, + 0o444, + Ci.nsIFileInputStream.CLOSE_ON_EOF + ); + var sis = new ScriptableInputStream(fis); + var contents = sis.read(file.fileSize); + sis.close(); + return contents; +} + +/** + * Iterates over the lines, delimited by CRLF, in data, returning each line + * without the trailing line separator. + * + * @param data : string + * a string consisting of lines of data separated by CRLFs + * @returns Iterator + * an Iterator which returns each line from data in turn; note that this + * includes a final empty line if data ended with a CRLF + */ +function* LineIterator(data) { + var index = 0; + do { + index = data.indexOf("\r\n"); + if (index >= 0) { + yield data.substring(0, index); + } else { + yield data; + } + + data = data.substring(index + 2); + } while (index >= 0); +} + +/** + * Throws if iter does not contain exactly the CRLF-separated lines in the + * array expectedLines. + * + * @param iter : Iterator + * an Iterator which returns lines of text + * @param expectedLines : [string] + * an array of the expected lines of text + * @throws an error message if iter doesn't agree with expectedLines + */ +function expectLines(iter, expectedLines) { + var index = 0; + for (var line of iter) { + if (expectedLines.length == index) { + throw new Error( + `Error: got more than ${expectedLines.length} expected lines!` + ); + } + + var expected = expectedLines[index++]; + if (expected !== line) { + throw new Error(`Error on line ${index}! + actual: '${line}', + expect: '${expected}'`); + } + } + + if (expectedLines.length !== index) { + throw new Error( + `Expected more lines! Got ${index}, expected ${expectedLines.length}` + ); + } +} + +/** + * Spew a bunch of HTTP metadata from request into the body of response. + * + * @param request : nsIHttpRequest + * the request whose metadata should be output + * @param response : nsIHttpResponse + * the response to which the metadata is written + */ +function writeDetails(request, response) { + response.write("Method: " + request.method + "\r\n"); + response.write("Path: " + request.path + "\r\n"); + response.write("Query: " + request.queryString + "\r\n"); + response.write("Version: " + request.httpVersion + "\r\n"); + response.write("Scheme: " + request.scheme + "\r\n"); + response.write("Host: " + request.host + "\r\n"); + response.write("Port: " + request.port); +} + +/** + * Advances iter past all non-blank lines and a single blank line, after which + * point the body of the response will be returned next from the iterator. + * + * @param iter : Iterator + * an iterator over the CRLF-delimited lines in an HTTP response, currently + * just after the Request-Line + */ +function skipHeaders(iter) { + var line = iter.next().value; + while (line !== "") { + line = iter.next().value; + } +} + +/** + * Checks that the exception e (which may be an XPConnect-created exception + * object or a raw nsresult number) is the given nsresult. + * + * @param e : Exception or nsresult + * the actual exception + * @param code : nsresult + * the expected exception + */ +function isException(e, code) { + if (e !== code && e.result !== code) { + do_throw("unexpected error: " + e); + } +} + +/** + * Calls the given function at least the specified number of milliseconds later. + * The callback will not undershoot the given time, but it might overshoot -- + * don't expect precision! + * + * @param milliseconds : uint + * the number of milliseconds to delay + * @param callback : function() : void + * the function to call + */ +function callLater(msecs, callback) { + do_timeout(msecs, callback); +} + +/** ***************************************************** + * SIMPLE SUPPORT FOR LOADING/TESTING A SERIES OF URLS * + *******************************************************/ + +/** + * Create a completion callback which will stop the given server and end the + * test, assuming nothing else remains to be done at that point. + */ +function testComplete(srv) { + return function complete() { + do_test_pending(); + srv.stop(function quit() { + do_test_finished(); + }); + }; +} + +/** + * Represents a path to load from the tested HTTP server, along with actions to + * take before, during, and after loading the associated page. + * + * @param path + * the URL to load from the server + * @param initChannel + * a function which takes as a single parameter a channel created for path and + * initializes its state, or null if no additional initialization is needed + * @param onStartRequest + * called during onStartRequest for the load of the URL, with the same + * parameters; the request parameter has been QI'd to nsIHttpChannel and + * nsIHttpChannelInternal for convenience; may be null if nothing needs to be + * done + * @param onStopRequest + * called during onStopRequest for the channel, with the same parameters plus + * a trailing parameter containing an array of the bytes of data downloaded in + * the body of the channel response; the request parameter has been QI'd to + * nsIHttpChannel and nsIHttpChannelInternal for convenience; may be null if + * nothing needs to be done + */ +function Test(path, initChannel, onStartRequest, onStopRequest) { + function nil() {} + + this.path = path; + this.initChannel = initChannel || nil; + this.onStartRequest = onStartRequest || nil; + this.onStopRequest = onStopRequest || nil; +} + +/** + * Runs all the tests in testArray. + * + * @param testArray + * a non-empty array of Tests to run, in order + * @param done + * function to call when all tests have run (e.g. to shut down the server) + */ +function runHttpTests(testArray, done) { + /** Kicks off running the next test in the array. */ + function performNextTest() { + if (++testIndex == testArray.length) { + try { + done(); + } catch (e) { + do_report_unexpected_exception(e, "running test-completion callback"); + } + return; + } + + do_test_pending(); + + var test = testArray[testIndex]; + var ch = makeChannel(test.path); + try { + test.initChannel(ch); + } catch (e) { + try { + do_report_unexpected_exception( + e, + "testArray[" + testIndex + "].initChannel(ch)" + ); + } catch (x) { + /* swallow and let tests continue */ + } + } + + listener._channel = ch; + ch.asyncOpen(listener); + } + + /** Index of the test being run. */ + var testIndex = -1; + + /** Stream listener for the channels. */ + var listener = { + /** Current channel being observed by this. */ + _channel: null, + /** Array of bytes of data in body of response. */ + _data: [], + + onStartRequest(request) { + Assert.ok(request === this._channel); + var ch = request + .QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + this._data.length = 0; + try { + try { + testArray[testIndex].onStartRequest(ch); + } catch (e) { + do_report_unexpected_exception( + e, + "testArray[" + testIndex + "].onStartRequest" + ); + } + } catch (e) { + do_note_exception( + e, + "!!! swallowing onStartRequest exception so onStopRequest is " + + "called..." + ); + } + }, + onDataAvailable(request, inputStream, offset, count) { + var quantum = 262144; // just above half the argument-count limit + var bis = makeBIS(inputStream); + for (var start = 0; start < count; start += quantum) { + var newData = bis.readByteArray(Math.min(quantum, count - start)); + Array.prototype.push.apply(this._data, newData); + } + }, + onStopRequest(request, status) { + this._channel = null; + + var ch = request + .QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + // NB: The onStopRequest callback must run before performNextTest here, + // because the latter runs the next test's initChannel callback, and + // we want one test to be sequentially processed before the next + // one. + try { + testArray[testIndex].onStopRequest(ch, status, this._data); + } catch (e) { + do_report_unexpected_exception( + e, + "testArray[" + testIndex + "].onStartRequest" + ); + } finally { + try { + performNextTest(); + } finally { + do_test_finished(); + } + } + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + }; + + performNextTest(); +} + +/** ************************************** + * RAW REQUEST FORMAT TESTING FUNCTIONS * + ****************************************/ + +/** + * Sends a raw string of bytes to the given host and port and checks that the + * response is acceptable. + * + * @param host : string + * the host to which a connection should be made + * @param port : PRUint16 + * the port to use for the connection + * @param data : string or [string...] + * either: + * - the raw data to send, as a string of characters with codes in the + * range 0-255, or + * - an array of such strings whose concatenation forms the raw data + * @param responseCheck : function(string) : void + * a function which is provided with the data sent by the remote host which + * conducts whatever tests it wants on that data; useful for tweaking the test + * environment between tests + */ +function RawTest(host, port, data, responseCheck) { + if (0 > port || 65535 < port || port % 1 !== 0) { + throw new Error("bad port"); + } + if (!(data instanceof Array)) { + data = [data]; + } + if (data.length <= 0) { + throw new Error("bad data length"); + } + + if ( + !data.every(function (v) { + // eslint-disable-next-line no-control-regex + return /^[\x00-\xff]*$/.test(v); + }) + ) { + throw new Error("bad data contained non-byte-valued character"); + } + + this.host = host; + this.port = port; + this.data = data; + this.responseCheck = responseCheck; +} + +/** + * Runs all the tests in testArray, an array of RawTests. + * + * @param testArray : [RawTest] + * an array of RawTests to run, in order + * @param done + * function to call when all tests have run (e.g. to shut down the server) + * @param beforeTestCallback + * function to call before each test is run. Gets passed testIndex when called + */ +function runRawTests(testArray, done, beforeTestCallback) { + do_test_pending(); + + var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( + Ci.nsISocketTransportService + ); + + var currentThread = + Cc["@mozilla.org/thread-manager;1"].getService().currentThread; + + /** Kicks off running the next test in the array. */ + function performNextTest() { + if (++testIndex == testArray.length) { + do_test_finished(); + try { + done(); + } catch (e) { + do_report_unexpected_exception(e, "running test-completion callback"); + } + return; + } + + if (beforeTestCallback) { + try { + beforeTestCallback(testIndex); + } catch (e) { + /* We don't care if this call fails */ + } + } + + var rawTest = testArray[testIndex]; + + var transport = sts.createTransport( + [], + rawTest.host, + rawTest.port, + null, + null + ); + + var inStream = transport.openInputStream(0, 0, 0); + var outStream = transport.openOutputStream(0, 0, 0); + + // reset + dataIndex = 0; + received = ""; + + waitForMoreInput(inStream); + waitToWriteOutput(outStream); + } + + function waitForMoreInput(stream) { + reader.stream = stream; + stream = stream.QueryInterface(Ci.nsIAsyncInputStream); + stream.asyncWait(reader, 0, 0, currentThread); + } + + function waitToWriteOutput(stream) { + // Do the QueryInterface here, not earlier, because there is no + // guarantee that 'stream' passed in here been QIed to nsIAsyncOutputStream + // since the last GC. + stream = stream.QueryInterface(Ci.nsIAsyncOutputStream); + stream.asyncWait( + writer, + 0, + testArray[testIndex].data[dataIndex].length, + currentThread + ); + } + + /** Index of the test being run. */ + var testIndex = -1; + + /** + * Index of remaining data strings to be written to the socket in current + * test. + */ + var dataIndex = 0; + + /** Data received so far from the server. */ + var received = ""; + + /** Reads data from the socket. */ + var reader = { + onInputStreamReady(stream) { + Assert.ok(stream === this.stream); + try { + var bis = new BinaryInputStream(stream); + + var av = 0; + try { + av = bis.available(); + } catch (e) { + /* default to 0 */ + do_note_exception(e); + } + + if (av > 0) { + var quantum = 262144; + for (var start = 0; start < av; start += quantum) { + var bytes = bis.readByteArray(Math.min(quantum, av - start)); + received += String.fromCharCode.apply(null, bytes); + } + waitForMoreInput(stream); + return; + } + } catch (e) { + do_report_unexpected_exception(e); + } + + var rawTest = testArray[testIndex]; + try { + rawTest.responseCheck(received); + } catch (e) { + do_report_unexpected_exception(e); + } finally { + try { + stream.close(); + performNextTest(); + } catch (e) { + do_report_unexpected_exception(e); + } + } + }, + }; + + /** Writes data to the socket. */ + var writer = { + onOutputStreamReady(stream) { + var str = testArray[testIndex].data[dataIndex]; + + var written = 0; + try { + written = stream.write(str, str.length); + if (written == str.length) { + dataIndex++; + } else { + testArray[testIndex].data[dataIndex] = str.substring(written); + } + } catch (e) { + do_note_exception(e); + /* stream could have been closed, just ignore */ + } + + try { + // Keep writing data while we can write and + // until there's no more data to read + if (written > 0 && dataIndex < testArray[testIndex].data.length) { + waitToWriteOutput(stream); + } else { + stream.close(); + } + } catch (e) { + do_report_unexpected_exception(e); + } + }, + }; + + performNextTest(); +} |