summaryrefslogtreecommitdiffstats
path: root/netwerk/test/httpserver
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /netwerk/test/httpserver
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/test/httpserver')
-rw-r--r--netwerk/test/httpserver/README101
-rw-r--r--netwerk/test/httpserver/TODO17
-rw-r--r--netwerk/test/httpserver/httpd.sys.mjs5576
-rw-r--r--netwerk/test/httpserver/moz.build21
-rw-r--r--netwerk/test/httpserver/nsIHttpServer.idl649
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^1
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^3
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_both.html2
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^2
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt9
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^1
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_status_override.html9
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^1
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt1
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^1
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/bar.html^^10
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^2
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^1
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^1
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt2
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/foo.html^9
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/normal-file.txt1
-rw-r--r--netwerk/test/httpserver/test/data/ranges/empty.txt0
-rw-r--r--netwerk/test/httpserver/test/data/ranges/headers.txt1
-rw-r--r--netwerk/test/httpserver/test/data/ranges/headers.txt^headers^1
-rw-r--r--netwerk/test/httpserver/test/data/ranges/range.txt1
-rw-r--r--netwerk/test/httpserver/test/data/sjs/cgi.sjs8
-rw-r--r--netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^2
-rw-r--r--netwerk/test/httpserver/test/data/sjs/object-state.sjs74
-rw-r--r--netwerk/test/httpserver/test/data/sjs/qi.sjs45
-rw-r--r--netwerk/test/httpserver/test/data/sjs/range-checker.sjs1
-rw-r--r--netwerk/test/httpserver/test/data/sjs/sjs4
-rw-r--r--netwerk/test/httpserver/test/data/sjs/state1.sjs38
-rw-r--r--netwerk/test/httpserver/test/data/sjs/state2.sjs38
-rw-r--r--netwerk/test/httpserver/test/data/sjs/thrower.sjs6
-rw-r--r--netwerk/test/httpserver/test/head_utils.js605
-rw-r--r--netwerk/test/httpserver/test/test_async_response_sending.js1661
-rw-r--r--netwerk/test/httpserver/test/test_basic_functionality.js182
-rw-r--r--netwerk/test/httpserver/test/test_body_length.js68
-rw-r--r--netwerk/test/httpserver/test/test_byte_range.js272
-rw-r--r--netwerk/test/httpserver/test/test_cern_meta.js79
-rw-r--r--netwerk/test/httpserver/test/test_default_index_handler.js248
-rw-r--r--netwerk/test/httpserver/test/test_empty_body.js59
-rw-r--r--netwerk/test/httpserver/test/test_errorhandler_exception.js95
-rw-r--r--netwerk/test/httpserver/test/test_header_array.js66
-rw-r--r--netwerk/test/httpserver/test/test_headers.js169
-rw-r--r--netwerk/test/httpserver/test/test_host.js608
-rw-r--r--netwerk/test/httpserver/test/test_host_identity.js115
-rw-r--r--netwerk/test/httpserver/test/test_linedata.js22
-rw-r--r--netwerk/test/httpserver/test/test_load_module.js18
-rw-r--r--netwerk/test/httpserver/test/test_name_scheme.js91
-rw-r--r--netwerk/test/httpserver/test/test_processasync.js272
-rw-r--r--netwerk/test/httpserver/test/test_qi.js107
-rw-r--r--netwerk/test/httpserver/test/test_registerdirectory.js278
-rw-r--r--netwerk/test/httpserver/test/test_registerfile.js44
-rw-r--r--netwerk/test/httpserver/test/test_registerprefix.js130
-rw-r--r--netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js137
-rw-r--r--netwerk/test/httpserver/test/test_response_write.js57
-rw-r--r--netwerk/test/httpserver/test/test_seizepower.js180
-rw-r--r--netwerk/test/httpserver/test/test_setindexhandler.js60
-rw-r--r--netwerk/test/httpserver/test/test_setstatusline.js142
-rw-r--r--netwerk/test/httpserver/test/test_sjs.js243
-rw-r--r--netwerk/test/httpserver/test/test_sjs_object_state.js305
-rw-r--r--netwerk/test/httpserver/test/test_sjs_state.js203
-rw-r--r--netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js73
-rw-r--r--netwerk/test/httpserver/test/test_start_stop.js166
-rw-r--r--netwerk/test/httpserver/test/test_start_stop_ipv6.js166
-rw-r--r--netwerk/test/httpserver/test/xpcshell.toml68
68 files changed, 13628 insertions, 0 deletions
diff --git a/netwerk/test/httpserver/README b/netwerk/test/httpserver/README
new file mode 100644
index 0000000000..97624789ca
--- /dev/null
+++ b/netwerk/test/httpserver/README
@@ -0,0 +1,101 @@
+httpd.js README
+===============
+
+httpd.js is a small cross-platform implementation of an HTTP/1.1 server in
+JavaScript for the Mozilla platform.
+
+httpd.js may be used as an XPCOM component, as an inline script in a document
+with XPCOM privileges, or from the XPCOM shell (xpcshell). Currently, its most-
+supported method of use is from the XPCOM shell, where you can get all the
+dynamicity of JS in adding request handlers and the like, but component-based
+equivalent functionality is planned.
+
+
+Using httpd.js as an XPCOM Component
+------------------------------------
+
+First, create an XPT file for nsIHttpServer.idl, using the xpidl tool included
+in the Mozilla SDK for the environment in which you wish to run httpd.js. See
+<http://developer.mozilla.org/en/docs/XPIDL:xpidl> for further details on how to
+do this.
+
+Next, register httpd.js and nsIHttpServer.xpt in your Mozilla application. In
+Firefox, these simply need to be added to the /components directory of your XPI.
+Other applications may require use of regxpcom or other techniques; consult the
+applicable documentation for further details.
+
+Finally, load httpd.js into the current file, and create an instance of the
+server using the following command:
+
+ var server = new nsHttpServer();
+
+At this point you'll want to initialize the server, since by default it doesn't
+serve many useful paths. For more information on this, see the IDL docs for the
+nsIHttpServer interface in nsIHttpServer.idl, particularly for
+registerDirectory (useful for mapping the contents of directories onto request
+paths), registerPathHandler (for setting a custom handler for a specific path on
+the server, such as CGI functionality), and registerFile (for mapping a file to
+a specific path).
+
+Finally, you'll want to start (and later stop) the server. Here's some example
+code which does this:
+
+ server.start(8080); // port on which server will operate
+
+ // ...server now runs and serves requests...
+
+ server.stop();
+
+This server will only respond to requests on 127.0.0.1:8080 or localhost:8080.
+If you want it to respond to requests at different hosts (say via a proxy
+mechanism), you must use server.identity.add() or server.identity.setPrimary()
+to add it.
+
+
+Using httpd.js as an Inline Script or from xpcshell
+---------------------------------------------------
+
+Using httpd.js as a script or from xpcshell isn't very different from using it
+as a component; the only real difference lies in how you create an instance of
+the server. To create an instance, do the following:
+
+ var server = new nsHttpServer();
+
+You now can use |server| exactly as you would when |server| was created as an
+XPCOM component. Note, however, that doing so will trample over the global
+namespace, and global values defined in httpd.js will leak into your script.
+This may typically be benign, but since some of the global values defined are
+constants (specifically, Cc/Ci/Cr as abbreviations for the classes, interfaces,
+and results properties of Components), it's possible this trampling could
+break your script. In general you should use httpd.js as an XPCOM component
+whenever possible.
+
+
+Known Issues
+------------
+
+httpd.js makes no effort to time out requests, beyond any the socket itself
+might or might not provide. I don't believe it provides any by default, but
+I haven't verified this.
+
+Every incoming request is processed by the corresponding request handler
+synchronously. In other words, once the first CRLFCRLF of a request is
+received, the entire response is created before any new incoming requests can be
+served. I anticipate adding asynchronous handler functionality in bug 396226,
+but it may be some time before that happens.
+
+There is no way to access the body of an incoming request. This problem is
+merely a symptom of the previous one, and they will probably both be addressed
+at the same time.
+
+
+Other Goodies
+-------------
+
+A special testing function, |server|, is provided for use in xpcshell for quick
+testing of the server; see the source code for details on its use. You don't
+want to use this in a script, however, because doing so will block until the
+server is shut down. It's also a good example of how to use the basic
+functionality of httpd.js, if you need one.
+
+Have fun!
diff --git a/netwerk/test/httpserver/TODO b/netwerk/test/httpserver/TODO
new file mode 100644
index 0000000000..3a95466117
--- /dev/null
+++ b/netwerk/test/httpserver/TODO
@@ -0,0 +1,17 @@
+Bugs to fix:
+- make content-length generation not rely on .available() returning the entire
+ size of the body stream's contents -- some sort of wrapper (but how does that
+ work for the unscriptable method WriteSegments, which is good to support from
+ a performance standpoint?)
+
+Ideas for future improvements:
+- add API to disable response buffering which, when called, causes errors when
+ you try to do anything other than write to the body stream (i.e., modify
+ headers or status line) once you've written anything to it -- useful when
+ storing the entire response in memory is unfeasible (e.g., you're testing
+ >4GB download characteristics)
+- add an API which performs asynchronous response processing (instead of
+ nsIHttpRequestHandler.handle, which must construct the response before control
+ returns; |void asyncHandle(request, response)|) -- useful, and can it be done
+ in JS?
+- other awesomeness?
diff --git a/netwerk/test/httpserver/httpd.sys.mjs b/netwerk/test/httpserver/httpd.sys.mjs
new file mode 100644
index 0000000000..569e4d1489
--- /dev/null
+++ b/netwerk/test/httpserver/httpd.sys.mjs
@@ -0,0 +1,5576 @@
+/* -*- 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/. */
+
+/*
+ * An implementation of an HTTP server both as a loadable script and as an XPCOM
+ * component. See the accompanying README file for user documentation on
+ * httpd.js.
+ */
+
+const CC = Components.Constructor;
+
+const PR_UINT32_MAX = Math.pow(2, 32) - 1;
+
+/** True if debugging output is enabled, false otherwise. */
+var DEBUG = false; // non-const *only* so tweakable in server tests
+
+/** True if debugging output should be timestamped. */
+var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests
+
+/**
+ * Sets the debugging status, intended for tweaking in server tests.
+ *
+ * @param {boolean} debug
+ * Enables debugging output
+ * @param {boolean} debugTimestamp
+ * Enables timestamping of the debugging output.
+ */
+export function setDebuggingStatus(debug, debugTimestamp) {
+ DEBUG = debug;
+ DEBUG_TIMESTAMP = debugTimestamp;
+}
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+/**
+ * Asserts that the given condition holds. If it doesn't, the given message is
+ * dumped, a stack trace is printed, and an exception is thrown to attempt to
+ * stop execution (which unfortunately must rely upon the exception not being
+ * accidentally swallowed by the code that uses it).
+ */
+function NS_ASSERT(cond, msg) {
+ if (DEBUG && !cond) {
+ dumpn("###!!!");
+ dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!"));
+ dumpn("###!!! Stack follows:");
+
+ var stack = new Error().stack.split(/\n/);
+ dumpn(
+ stack
+ .map(function (val) {
+ return "###!!! " + val;
+ })
+ .join("\n")
+ );
+
+ throw Components.Exception("", Cr.NS_ERROR_ABORT);
+ }
+}
+
+/** Constructs an HTTP error object. */
+export function HttpError(code, description) {
+ this.code = code;
+ this.description = description;
+}
+
+HttpError.prototype = {
+ toString() {
+ return this.code + " " + this.description;
+ },
+};
+
+/**
+ * Errors thrown to trigger specific HTTP server responses.
+ */
+export var HTTP_400 = new HttpError(400, "Bad Request");
+
+export var HTTP_401 = new HttpError(401, "Unauthorized");
+export var HTTP_402 = new HttpError(402, "Payment Required");
+export var HTTP_403 = new HttpError(403, "Forbidden");
+export var HTTP_404 = new HttpError(404, "Not Found");
+export var HTTP_405 = new HttpError(405, "Method Not Allowed");
+export var HTTP_406 = new HttpError(406, "Not Acceptable");
+export var HTTP_407 = new HttpError(407, "Proxy Authentication Required");
+export var HTTP_408 = new HttpError(408, "Request Timeout");
+export var HTTP_409 = new HttpError(409, "Conflict");
+export var HTTP_410 = new HttpError(410, "Gone");
+export var HTTP_411 = new HttpError(411, "Length Required");
+export var HTTP_412 = new HttpError(412, "Precondition Failed");
+export var HTTP_413 = new HttpError(413, "Request Entity Too Large");
+export var HTTP_414 = new HttpError(414, "Request-URI Too Long");
+export var HTTP_415 = new HttpError(415, "Unsupported Media Type");
+export var HTTP_417 = new HttpError(417, "Expectation Failed");
+export var HTTP_500 = new HttpError(500, "Internal Server Error");
+export var HTTP_501 = new HttpError(501, "Not Implemented");
+export var HTTP_502 = new HttpError(502, "Bad Gateway");
+export var HTTP_503 = new HttpError(503, "Service Unavailable");
+export var HTTP_504 = new HttpError(504, "Gateway Timeout");
+export var HTTP_505 = new HttpError(505, "HTTP Version Not Supported");
+
+/** Creates a hash with fields corresponding to the values in arr. */
+function array2obj(arr) {
+ var obj = {};
+ for (var i = 0; i < arr.length; i++) {
+ obj[arr[i]] = arr[i];
+ }
+ return obj;
+}
+
+/** Returns an array of the integers x through y, inclusive. */
+function range(x, y) {
+ var arr = [];
+ for (var i = x; i <= y; i++) {
+ arr.push(i);
+ }
+ return arr;
+}
+
+/** An object (hash) whose fields are the numbers of all HTTP error codes. */
+const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505)));
+
+/**
+ * The character used to distinguish hidden files from non-hidden files, a la
+ * the leading dot in Apache. Since that mechanism also hides files from
+ * easy display in LXR, ls output, etc. however, we choose instead to use a
+ * suffix character. If a requested file ends with it, we append another
+ * when getting the file on the server. If it doesn't, we just look up that
+ * file. Therefore, any file whose name ends with exactly one of the character
+ * is "hidden" and available for use by the server.
+ */
+const HIDDEN_CHAR = "^";
+
+/**
+ * The file name suffix indicating the file containing overridden headers for
+ * a requested file.
+ */
+const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR;
+const INFORMATIONAL_RESPONSE_SUFFIX =
+ HIDDEN_CHAR + "informationalResponse" + HIDDEN_CHAR;
+
+/** Type used to denote SJS scripts for CGI-like functionality. */
+const SJS_TYPE = "sjs";
+
+/** Base for relative timestamps produced by dumpn(). */
+var firstStamp = 0;
+
+/** dump(str) with a trailing "\n" -- only outputs if DEBUG. */
+export function dumpn(str) {
+ if (DEBUG) {
+ var prefix = "HTTPD-INFO | ";
+ if (DEBUG_TIMESTAMP) {
+ if (firstStamp === 0) {
+ firstStamp = Date.now();
+ }
+
+ var elapsed = Date.now() - firstStamp; // milliseconds
+ var min = Math.floor(elapsed / 60000);
+ var sec = (elapsed % 60000) / 1000;
+
+ if (sec < 10) {
+ prefix += min + ":0" + sec.toFixed(3) + " | ";
+ } else {
+ prefix += min + ":" + sec.toFixed(3) + " | ";
+ }
+ }
+
+ dump(prefix + str + "\n");
+ }
+}
+
+/** Dumps the current JS stack if DEBUG. */
+function dumpStack() {
+ // peel off the frames for dumpStack() and Error()
+ var stack = new Error().stack.split(/\n/).slice(2);
+ stack.forEach(dumpn);
+}
+
+/**
+ * JavaScript constructors for commonly-used classes; precreating these is a
+ * speedup over doing the same from base principles. See the docs at
+ * http://developer.mozilla.org/en/docs/Components.Constructor for details.
+ */
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+const ServerSocketIPv6 = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "initIPv6"
+);
+const ServerSocketDualStack = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "initDualStack"
+);
+const ScriptableInputStream = CC(
+ "@mozilla.org/scriptableinputstream;1",
+ "nsIScriptableInputStream",
+ "init"
+);
+const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init");
+const FileInputStream = CC(
+ "@mozilla.org/network/file-input-stream;1",
+ "nsIFileInputStream",
+ "init"
+);
+const ConverterInputStream = CC(
+ "@mozilla.org/intl/converter-input-stream;1",
+ "nsIConverterInputStream",
+ "init"
+);
+const WritablePropertyBag = CC(
+ "@mozilla.org/hash-property-bag;1",
+ "nsIWritablePropertyBag2"
+);
+const SupportsString = CC(
+ "@mozilla.org/supports-string;1",
+ "nsISupportsString"
+);
+
+/* These two are non-const only so a test can overwrite them. */
+var BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+var BinaryOutputStream = CC(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+
+export function overrideBinaryStreamsForTests(
+ inputStream,
+ outputStream,
+ responseSegmentSize
+) {
+ BinaryInputStream = inputStream;
+ BinaryOutputStream = outputStream;
+ Response.SEGMENT_SIZE = responseSegmentSize;
+}
+
+/**
+ * Returns the RFC 822/1123 representation of a date.
+ *
+ * @param date : Number
+ * the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT
+ * @returns string
+ * the representation of the given date
+ */
+function toDateString(date) {
+ //
+ // rfc1123-date = wkday "," SP date1 SP time SP "GMT"
+ // date1 = 2DIGIT SP month SP 4DIGIT
+ // ; day month year (e.g., 02 Jun 1982)
+ // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
+ // ; 00:00:00 - 23:59:59
+ // wkday = "Mon" | "Tue" | "Wed"
+ // | "Thu" | "Fri" | "Sat" | "Sun"
+ // month = "Jan" | "Feb" | "Mar" | "Apr"
+ // | "May" | "Jun" | "Jul" | "Aug"
+ // | "Sep" | "Oct" | "Nov" | "Dec"
+ //
+
+ const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+ const monthStrings = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+
+ /**
+ * Processes a date and returns the encoded UTC time as a string according to
+ * the format specified in RFC 2616.
+ *
+ * @param date : Date
+ * the date to process
+ * @returns string
+ * a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
+ */
+ function toTime(date) {
+ var hrs = date.getUTCHours();
+ var rv = hrs < 10 ? "0" + hrs : hrs;
+
+ var mins = date.getUTCMinutes();
+ rv += ":";
+ rv += mins < 10 ? "0" + mins : mins;
+
+ var secs = date.getUTCSeconds();
+ rv += ":";
+ rv += secs < 10 ? "0" + secs : secs;
+
+ return rv;
+ }
+
+ /**
+ * Processes a date and returns the encoded UTC date as a string according to
+ * the date1 format specified in RFC 2616.
+ *
+ * @param date : Date
+ * the date to process
+ * @returns string
+ * a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
+ */
+ function toDate1(date) {
+ var day = date.getUTCDate();
+ var month = date.getUTCMonth();
+ var year = date.getUTCFullYear();
+
+ var rv = day < 10 ? "0" + day : day;
+ rv += " " + monthStrings[month];
+ rv += " " + year;
+
+ return rv;
+ }
+
+ date = new Date(date);
+
+ const fmtString = "%wkday%, %date1% %time% GMT";
+ var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]);
+ rv = rv.replace("%time%", toTime(date));
+ return rv.replace("%date1%", toDate1(date));
+}
+
+/**
+ * Instantiates a new HTTP server.
+ */
+export function nsHttpServer() {
+ /** The port on which this server listens. */
+ this._port = undefined;
+
+ /** The socket associated with this. */
+ this._socket = null;
+
+ /** The handler used to process requests to this server. */
+ this._handler = new ServerHandler(this);
+
+ /** Naming information for this server. */
+ this._identity = new ServerIdentity();
+
+ /**
+ * Indicates when the server is to be shut down at the end of the request.
+ */
+ this._doQuit = false;
+
+ /**
+ * True if the socket in this is closed (and closure notifications have been
+ * sent and processed if the socket was ever opened), false otherwise.
+ */
+ this._socketClosed = true;
+
+ /**
+ * Used for tracking existing connections and ensuring that all connections
+ * are properly cleaned up before server shutdown; increases by 1 for every
+ * new incoming connection.
+ */
+ this._connectionGen = 0;
+
+ /**
+ * Hash of all open connections, indexed by connection number at time of
+ * creation.
+ */
+ this._connections = {};
+}
+
+nsHttpServer.prototype = {
+ // NSISERVERSOCKETLISTENER
+
+ /**
+ * Processes an incoming request coming in on the given socket and contained
+ * in the given transport.
+ *
+ * @param socket : nsIServerSocket
+ * the socket through which the request was served
+ * @param trans : nsISocketTransport
+ * the transport for the request/response
+ * @see nsIServerSocketListener.onSocketAccepted
+ */
+ onSocketAccepted(socket, trans) {
+ dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")");
+
+ dumpn(">>> new connection on " + trans.host + ":" + trans.port);
+
+ const SEGMENT_SIZE = 8192;
+ const SEGMENT_COUNT = 1024;
+ try {
+ var input = trans
+ .openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ var output = trans.openOutputStream(0, 0, 0);
+ } catch (e) {
+ dumpn("*** error opening transport streams: " + e);
+ trans.close(Cr.NS_BINDING_ABORTED);
+ return;
+ }
+
+ var connectionNumber = ++this._connectionGen;
+
+ try {
+ var conn = new Connection(
+ input,
+ output,
+ this,
+ socket.port,
+ trans.port,
+ connectionNumber,
+ trans
+ );
+ var reader = new RequestReader(conn);
+
+ // XXX add request timeout functionality here!
+
+ // Note: must use main thread here, or we might get a GC that will cause
+ // threadsafety assertions. We really need to fix XPConnect so that
+ // you can actually do things in multi-threaded JS. :-(
+ input.asyncWait(reader, 0, 0, Services.tm.mainThread);
+ } catch (e) {
+ // Assume this connection can't be salvaged and bail on it completely;
+ // don't attempt to close it so that we can assert that any connection
+ // being closed is in this._connections.
+ dumpn("*** error in initial request-processing stages: " + e);
+ trans.close(Cr.NS_BINDING_ABORTED);
+ return;
+ }
+
+ this._connections[connectionNumber] = conn;
+ dumpn("*** starting connection " + connectionNumber);
+ },
+
+ /**
+ * Called when the socket associated with this is closed.
+ *
+ * @param socket : nsIServerSocket
+ * the socket being closed
+ * @param status : nsresult
+ * the reason the socket stopped listening (NS_BINDING_ABORTED if the server
+ * was stopped using nsIHttpServer.stop)
+ * @see nsIServerSocketListener.onStopListening
+ */
+ onStopListening(socket, status) {
+ dumpn(">>> shutting down server on port " + socket.port);
+ for (var n in this._connections) {
+ if (!this._connections[n]._requestStarted) {
+ this._connections[n].close();
+ }
+ }
+ this._socketClosed = true;
+ if (this._hasOpenConnections()) {
+ dumpn("*** open connections!!!");
+ }
+ if (!this._hasOpenConnections()) {
+ dumpn("*** no open connections, notifying async from onStopListening");
+
+ // Notify asynchronously so that any pending teardown in stop() has a
+ // chance to run first.
+ var self = this;
+ var stopEvent = {
+ run() {
+ dumpn("*** _notifyStopped async callback");
+ self._notifyStopped();
+ },
+ };
+ Services.tm.currentThread.dispatch(
+ stopEvent,
+ Ci.nsIThread.DISPATCH_NORMAL
+ );
+ }
+ },
+
+ // NSIHTTPSERVER
+
+ //
+ // see nsIHttpServer.start
+ //
+ start(port) {
+ this._start(port, "localhost");
+ },
+
+ //
+ // see nsIHttpServer.start_ipv6
+ //
+ start_ipv6(port) {
+ this._start(port, "[::1]");
+ },
+
+ start_dualStack(port) {
+ this._start(port, "[::1]", true);
+ },
+
+ _start(port, host, dualStack) {
+ if (this._socket) {
+ throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED);
+ }
+
+ this._port = port;
+ this._doQuit = this._socketClosed = false;
+
+ this._host = host;
+
+ // The listen queue needs to be long enough to handle
+ // network.http.max-persistent-connections-per-server or
+ // network.http.max-persistent-connections-per-proxy concurrent
+ // connections, plus a safety margin in case some other process is
+ // talking to the server as well.
+ var maxConnections =
+ 5 +
+ Math.max(
+ Services.prefs.getIntPref(
+ "network.http.max-persistent-connections-per-server"
+ ),
+ Services.prefs.getIntPref(
+ "network.http.max-persistent-connections-per-proxy"
+ )
+ );
+
+ try {
+ var loopback = true;
+ if (
+ this._host != "127.0.0.1" &&
+ this._host != "localhost" &&
+ this._host != "[::1]"
+ ) {
+ loopback = false;
+ }
+
+ // When automatically selecting a port, sometimes the chosen port is
+ // "blocked" from clients. We don't want to use these ports because
+ // tests will intermittently fail. So, we simply keep trying to to
+ // get a server socket until a valid port is obtained. We limit
+ // ourselves to finite attempts just so we don't loop forever.
+ var socket;
+ for (var i = 100; i; i--) {
+ var temp = null;
+ if (dualStack) {
+ temp = new ServerSocketDualStack(this._port, maxConnections);
+ } else if (this._host.includes(":")) {
+ temp = new ServerSocketIPv6(
+ this._port,
+ loopback, // true = localhost, false = everybody
+ maxConnections
+ );
+ } else {
+ temp = new ServerSocket(
+ this._port,
+ loopback, // true = localhost, false = everybody
+ maxConnections
+ );
+ }
+
+ var allowed = Services.io.allowPort(temp.port, "http");
+ if (!allowed) {
+ dumpn(
+ ">>>Warning: obtained ServerSocket listens on a blocked " +
+ "port: " +
+ temp.port
+ );
+ }
+
+ if (!allowed && this._port == -1) {
+ dumpn(">>>Throwing away ServerSocket with bad port.");
+ temp.close();
+ continue;
+ }
+
+ socket = temp;
+ break;
+ }
+
+ if (!socket) {
+ throw new Error(
+ "No socket server available. Are there no available ports?"
+ );
+ }
+
+ socket.asyncListen(this);
+ this._port = socket.port;
+ this._identity._initialize(socket.port, host, true, dualStack);
+ this._socket = socket;
+ dumpn(
+ ">>> listening on port " +
+ socket.port +
+ ", " +
+ maxConnections +
+ " pending connections"
+ );
+ } catch (e) {
+ dump("\n!!! could not start server on port " + port + ": " + e + "\n\n");
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ },
+
+ //
+ // see nsIHttpServer.stop
+ //
+ stop(callback) {
+ if (!this._socket) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+
+ // If no argument was provided to stop, return a promise.
+ let returnValue = undefined;
+ if (!callback) {
+ returnValue = new Promise(resolve => {
+ callback = resolve;
+ });
+ }
+
+ this._stopCallback =
+ typeof callback === "function"
+ ? callback
+ : function () {
+ callback.onStopped();
+ };
+
+ dumpn(">>> stopping listening on port " + this._socket.port);
+ this._socket.close();
+ this._socket = null;
+
+ // We can't have this identity any more, and the port on which we're running
+ // this server now could be meaningless the next time around.
+ this._identity._teardown();
+
+ this._doQuit = false;
+
+ // socket-close notification and pending request completion happen async
+
+ return returnValue;
+ },
+
+ //
+ // see nsIHttpServer.registerFile
+ //
+ registerFile(path, file, handler) {
+ if (file && (!file.exists() || file.isDirectory())) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ this._handler.registerFile(path, file, handler);
+ },
+
+ //
+ // see nsIHttpServer.registerDirectory
+ //
+ registerDirectory(path, directory) {
+ // XXX true path validation!
+ if (
+ path.charAt(0) != "/" ||
+ path.charAt(path.length - 1) != "/" ||
+ (directory && (!directory.exists() || !directory.isDirectory()))
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping
+ // exists!
+
+ this._handler.registerDirectory(path, directory);
+ },
+
+ //
+ // see nsIHttpServer.registerPathHandler
+ //
+ registerPathHandler(path, handler) {
+ this._handler.registerPathHandler(path, handler);
+ },
+
+ //
+ // see nsIHttpServer.registerPrefixHandler
+ //
+ registerPrefixHandler(prefix, handler) {
+ this._handler.registerPrefixHandler(prefix, handler);
+ },
+
+ //
+ // see nsIHttpServer.registerErrorHandler
+ //
+ registerErrorHandler(code, handler) {
+ this._handler.registerErrorHandler(code, handler);
+ },
+
+ //
+ // see nsIHttpServer.setIndexHandler
+ //
+ setIndexHandler(handler) {
+ this._handler.setIndexHandler(handler);
+ },
+
+ //
+ // see nsIHttpServer.registerContentType
+ //
+ registerContentType(ext, type) {
+ this._handler.registerContentType(ext, type);
+ },
+
+ get connectionNumber() {
+ return this._connectionGen;
+ },
+
+ //
+ // see nsIHttpServer.serverIdentity
+ //
+ get identity() {
+ return this._identity;
+ },
+
+ //
+ // see nsIHttpServer.getState
+ //
+ getState(path, k) {
+ return this._handler._getState(path, k);
+ },
+
+ //
+ // see nsIHttpServer.setState
+ //
+ setState(path, k, v) {
+ return this._handler._setState(path, k, v);
+ },
+
+ //
+ // see nsIHttpServer.getSharedState
+ //
+ getSharedState(k) {
+ return this._handler._getSharedState(k);
+ },
+
+ //
+ // see nsIHttpServer.setSharedState
+ //
+ setSharedState(k, v) {
+ return this._handler._setSharedState(k, v);
+ },
+
+ //
+ // see nsIHttpServer.getObjectState
+ //
+ getObjectState(k) {
+ return this._handler._getObjectState(k);
+ },
+
+ //
+ // see nsIHttpServer.setObjectState
+ //
+ setObjectState(k, v) {
+ return this._handler._setObjectState(k, v);
+ },
+
+ get wrappedJSObject() {
+ return this;
+ },
+
+ // NSISUPPORTS
+
+ //
+ // see nsISupports.QueryInterface
+ //
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIHttpServer",
+ "nsIServerSocketListener",
+ ]),
+
+ // NON-XPCOM PUBLIC API
+
+ /**
+ * Returns true iff this server is not running (and is not in the process of
+ * serving any requests still to be processed when the server was last
+ * stopped after being run).
+ */
+ isStopped() {
+ return this._socketClosed && !this._hasOpenConnections();
+ },
+
+ // PRIVATE IMPLEMENTATION
+
+ /** True if this server has any open connections to it, false otherwise. */
+ _hasOpenConnections() {
+ //
+ // If we have any open connections, they're tracked as numeric properties on
+ // |this._connections|. The non-standard __count__ property could be used
+ // to check whether there are any properties, but standard-wise, even
+ // looking forward to ES5, there's no less ugly yet still O(1) way to do
+ // this.
+ //
+ for (var n in this._connections) {
+ return true;
+ }
+ return false;
+ },
+
+ /** Calls the server-stopped callback provided when stop() was called. */
+ _notifyStopped() {
+ NS_ASSERT(this._stopCallback !== null, "double-notifying?");
+ NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now");
+
+ //
+ // NB: We have to grab this now, null out the member, *then* call the
+ // callback here, or otherwise the callback could (indirectly) futz with
+ // this._stopCallback by starting and immediately stopping this, at
+ // which point we'd be nulling out a field we no longer have a right to
+ // modify.
+ //
+ var callback = this._stopCallback;
+ this._stopCallback = null;
+ try {
+ callback();
+ } catch (e) {
+ // not throwing because this is specified as being usually (but not
+ // always) asynchronous
+ dump("!!! error running onStopped callback: " + e + "\n");
+ }
+ },
+
+ /**
+ * Notifies this server that the given connection has been closed.
+ *
+ * @param connection : Connection
+ * the connection that was closed
+ */
+ _connectionClosed(connection) {
+ NS_ASSERT(
+ connection.number in this._connections,
+ "closing a connection " +
+ this +
+ " that we never added to the " +
+ "set of open connections?"
+ );
+ NS_ASSERT(
+ this._connections[connection.number] === connection,
+ "connection number mismatch? " + this._connections[connection.number]
+ );
+ delete this._connections[connection.number];
+
+ // Fire a pending server-stopped notification if it's our responsibility.
+ if (!this._hasOpenConnections() && this._socketClosed) {
+ this._notifyStopped();
+ }
+ },
+
+ /**
+ * Requests that the server be shut down when possible.
+ */
+ _requestQuit() {
+ dumpn(">>> requesting a quit");
+ dumpStack();
+ this._doQuit = true;
+ },
+};
+
+export var HttpServer = nsHttpServer;
+
+export class NodeServer {
+ // Executes command in the context of a node server.
+ // See handler in moz-http2.js
+ //
+ // Example use:
+ // let id = NodeServer.fork(); // id is a random string
+ // await NodeServer.execute(id, `"hello"`)
+ // > "hello"
+ // await NodeServer.execute(id, `(() => "hello")()`)
+ // > "hello"
+ // await NodeServer.execute(id, `(() => var_defined_on_server)()`)
+ // > "0"
+ // await NodeServer.execute(id, `var_defined_on_server`)
+ // > "0"
+ // function f(param) { if (param) return param; return "bla"; }
+ // await NodeServer.execute(id, f); // Defines the function on the server
+ // await NodeServer.execute(id, `f()`) // executes defined function
+ // > "bla"
+ // let result = await NodeServer.execute(id, `f("test")`);
+ // > "test"
+ // await NodeServer.kill(id); // shuts down the server
+
+ // Forks a new node server using moz-http2-child.js as a starting point
+ static fork() {
+ return this.sendCommand("", "/fork");
+ }
+ // Executes command in the context of the node server indicated by `id`
+ static execute(id, command) {
+ return this.sendCommand(command, `/execute/${id}`);
+ }
+ // Shuts down the server
+ static kill(id) {
+ return this.sendCommand("", `/kill/${id}`);
+ }
+
+ // Issues a request to the node server (handler defined in moz-http2.js)
+ // This method should not be called directly.
+ static sendCommand(command, path) {
+ let h2Port = Services.env.get("MOZNODE_EXEC_PORT");
+ if (!h2Port) {
+ throw new Error("Could not find MOZNODE_EXEC_PORT");
+ }
+
+ let req = new XMLHttpRequest();
+ const serverIP =
+ AppConstants.platform == "android" ? "10.0.2.2" : "127.0.0.1";
+ req.open("POST", `http://${serverIP}:${h2Port}${path}`);
+ req.channel.QueryInterface(Ci.nsIHttpChannelInternal).bypassProxy = true;
+
+ // Passing a function to NodeServer.execute will define that function
+ // in node. It can be called in a later execute command.
+ let isFunction = function (obj) {
+ return !!(obj && obj.constructor && obj.call && obj.apply);
+ };
+ let payload = command;
+ if (isFunction(command)) {
+ payload = `${command.name} = ${command.toString()};`;
+ }
+
+ return new Promise((resolve, reject) => {
+ req.onload = () => {
+ let x = null;
+
+ if (req.statusText != "OK") {
+ reject(`XHR request failed: ${req.statusText}`);
+ return;
+ }
+
+ try {
+ x = JSON.parse(req.responseText);
+ } catch (e) {
+ reject(`Failed to parse ${req.responseText} - ${e}`);
+ return;
+ }
+
+ if (x.error) {
+ let e = new Error(x.error, "", 0);
+ e.stack = x.errorStack;
+ reject(e);
+ return;
+ }
+ resolve(x.result);
+ };
+ req.onerror = e => {
+ reject(e);
+ };
+
+ req.send(payload.toString());
+ });
+ }
+}
+
+//
+// RFC 2396 section 3.2.2:
+//
+// host = hostname | IPv4address
+// hostname = *( domainlabel "." ) toplabel [ "." ]
+// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
+// toplabel = alpha | alpha *( alphanum | "-" ) alphanum
+// IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit
+//
+// IPv6 addresses are notably lacking in the above definition of 'host'.
+// RFC 2732 section 3 extends the host definition:
+// host = hostname | IPv4address | IPv6reference
+// ipv6reference = "[" IPv6address "]"
+//
+// RFC 3986 supersedes RFC 2732 and offers a more precise definition of a IPv6
+// address. For simplicity, the regexp below captures all canonical IPv6
+// addresses (e.g. [::1]), but may also match valid non-canonical IPv6 addresses
+// (e.g. [::127.0.0.1]) and even invalid bracketed addresses ([::], [99999::]).
+
+const HOST_REGEX = new RegExp(
+ "^(?:" +
+ // *( domainlabel "." )
+ "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" +
+ // toplabel [ "." ]
+ "[a-z](?:[a-z0-9-]*[a-z0-9])?\\.?" +
+ "|" +
+ // IPv4 address
+ "\\d+\\.\\d+\\.\\d+\\.\\d+" +
+ "|" +
+ // IPv6 addresses (e.g. [::1])
+ "\\[[:0-9a-f]+\\]" +
+ ")$",
+ "i"
+);
+
+/**
+ * Represents the identity of a server. An identity consists of a set of
+ * (scheme, host, port) tuples denoted as locations (allowing a single server to
+ * serve multiple sites or to be used behind both HTTP and HTTPS proxies for any
+ * host/port). Any incoming request must be to one of these locations, or it
+ * will be rejected with an HTTP 400 error. One location, denoted as the
+ * primary location, is the location assigned in contexts where a location
+ * cannot otherwise be endogenously derived, such as for HTTP/1.0 requests.
+ *
+ * A single identity may contain at most one location per unique host/port pair;
+ * other than that, no restrictions are placed upon what locations may
+ * constitute an identity.
+ */
+function ServerIdentity() {
+ /** The scheme of the primary location. */
+ this._primaryScheme = "http";
+
+ /** The hostname of the primary location. */
+ this._primaryHost = "127.0.0.1";
+
+ /** The port number of the primary location. */
+ this._primaryPort = -1;
+
+ /**
+ * The current port number for the corresponding server, stored so that a new
+ * primary location can always be set if the current one is removed.
+ */
+ this._defaultPort = -1;
+
+ /**
+ * Maps hosts to maps of ports to schemes, e.g. the following would represent
+ * https://example.com:789/ and http://example.org/:
+ *
+ * {
+ * "xexample.com": { 789: "https" },
+ * "xexample.org": { 80: "http" }
+ * }
+ *
+ * Note the "x" prefix on hostnames, which prevents collisions with special
+ * JS names like "prototype".
+ */
+ this._locations = { xlocalhost: {} };
+}
+ServerIdentity.prototype = {
+ // NSIHTTPSERVERIDENTITY
+
+ //
+ // see nsIHttpServerIdentity.primaryScheme
+ //
+ get primaryScheme() {
+ if (this._primaryPort === -1) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+ return this._primaryScheme;
+ },
+
+ //
+ // see nsIHttpServerIdentity.primaryHost
+ //
+ get primaryHost() {
+ if (this._primaryPort === -1) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+ return this._primaryHost;
+ },
+
+ //
+ // see nsIHttpServerIdentity.primaryPort
+ //
+ get primaryPort() {
+ if (this._primaryPort === -1) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+ return this._primaryPort;
+ },
+
+ //
+ // see nsIHttpServerIdentity.add
+ //
+ add(scheme, host, port) {
+ this._validate(scheme, host, port);
+
+ var entry = this._locations["x" + host];
+ if (!entry) {
+ this._locations["x" + host] = entry = {};
+ }
+
+ entry[port] = scheme;
+ },
+
+ //
+ // see nsIHttpServerIdentity.remove
+ //
+ remove(scheme, host, port) {
+ this._validate(scheme, host, port);
+
+ var entry = this._locations["x" + host];
+ if (!entry) {
+ return false;
+ }
+
+ var present = port in entry;
+ delete entry[port];
+
+ if (
+ this._primaryScheme == scheme &&
+ this._primaryHost == host &&
+ this._primaryPort == port &&
+ this._defaultPort !== -1
+ ) {
+ // Always keep at least one identity in existence at any time, unless
+ // we're in the process of shutting down (the last condition above).
+ this._primaryPort = -1;
+ this._initialize(this._defaultPort, host, false);
+ }
+
+ return present;
+ },
+
+ //
+ // see nsIHttpServerIdentity.has
+ //
+ has(scheme, host, port) {
+ this._validate(scheme, host, port);
+
+ return (
+ "x" + host in this._locations &&
+ scheme === this._locations["x" + host][port]
+ );
+ },
+
+ //
+ // see nsIHttpServerIdentity.has
+ //
+ getScheme(host, port) {
+ this._validate("http", host, port);
+
+ var entry = this._locations["x" + host];
+ if (!entry) {
+ return "";
+ }
+
+ return entry[port] || "";
+ },
+
+ //
+ // see nsIHttpServerIdentity.setPrimary
+ //
+ setPrimary(scheme, host, port) {
+ this._validate(scheme, host, port);
+
+ this.add(scheme, host, port);
+
+ this._primaryScheme = scheme;
+ this._primaryHost = host;
+ this._primaryPort = port;
+ },
+
+ // NSISUPPORTS
+
+ //
+ // see nsISupports.QueryInterface
+ //
+ QueryInterface: ChromeUtils.generateQI(["nsIHttpServerIdentity"]),
+
+ // PRIVATE IMPLEMENTATION
+
+ /**
+ * Initializes the primary name for the corresponding server, based on the
+ * provided port number.
+ */
+ _initialize(port, host, addSecondaryDefault, dualStack) {
+ this._host = host;
+ if (this._primaryPort !== -1) {
+ this.add("http", host, port);
+ } else {
+ this.setPrimary("http", "localhost", port);
+ }
+ this._defaultPort = port;
+
+ // Only add this if we're being called at server startup
+ if (addSecondaryDefault && host != "127.0.0.1") {
+ if (host.includes(":")) {
+ this.add("http", "[::1]", port);
+ if (dualStack) {
+ this.add("http", "127.0.0.1", port);
+ }
+ } else {
+ this.add("http", "127.0.0.1", port);
+ }
+ }
+ },
+
+ /**
+ * Called at server shutdown time, unsets the primary location only if it was
+ * the default-assigned location and removes the default location from the
+ * set of locations used.
+ */
+ _teardown() {
+ if (this._host != "127.0.0.1") {
+ // Not the default primary location, nothing special to do here
+ this.remove("http", "127.0.0.1", this._defaultPort);
+ }
+
+ // This is a *very* tricky bit of reasoning here; make absolutely sure the
+ // tests for this code pass before you commit changes to it.
+ if (
+ this._primaryScheme == "http" &&
+ this._primaryHost == this._host &&
+ this._primaryPort == this._defaultPort
+ ) {
+ // Make sure we don't trigger the readding logic in .remove(), then remove
+ // the default location.
+ var port = this._defaultPort;
+ this._defaultPort = -1;
+ this.remove("http", this._host, port);
+
+ // Ensure a server start triggers the setPrimary() path in ._initialize()
+ this._primaryPort = -1;
+ } else {
+ // No reason not to remove directly as it's not our primary location
+ this.remove("http", this._host, this._defaultPort);
+ }
+ },
+
+ /**
+ * Ensures scheme, host, and port are all valid with respect to RFC 2396.
+ *
+ * @throws NS_ERROR_ILLEGAL_VALUE
+ * if any argument doesn't match the corresponding production
+ */
+ _validate(scheme, host, port) {
+ if (scheme !== "http" && scheme !== "https") {
+ dumpn("*** server only supports http/https schemes: '" + scheme + "'");
+ dumpStack();
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ if (!HOST_REGEX.test(host)) {
+ dumpn("*** unexpected host: '" + host + "'");
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ if (port < 0 || port > 65535) {
+ dumpn("*** unexpected port: '" + port + "'");
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ },
+};
+
+/**
+ * Represents a connection to the server (and possibly in the future the thread
+ * on which the connection is processed).
+ *
+ * @param input : nsIInputStream
+ * stream from which incoming data on the connection is read
+ * @param output : nsIOutputStream
+ * stream to write data out the connection
+ * @param server : nsHttpServer
+ * the server handling the connection
+ * @param port : int
+ * the port on which the server is running
+ * @param outgoingPort : int
+ * the outgoing port used by this connection
+ * @param number : uint
+ * a serial number used to uniquely identify this connection
+ */
+function Connection(
+ input,
+ output,
+ server,
+ port,
+ outgoingPort,
+ number,
+ transport
+) {
+ dumpn("*** opening new connection " + number + " on port " + outgoingPort);
+
+ /** Stream of incoming data. */
+ this.input = input;
+
+ /** Stream for outgoing data. */
+ this.output = output;
+
+ /** The server associated with this request. */
+ this.server = server;
+
+ /** The port on which the server is running. */
+ this.port = port;
+
+ /** The outgoing poort used by this connection. */
+ this._outgoingPort = outgoingPort;
+
+ /** The serial number of this connection. */
+ this.number = number;
+
+ /** Reference to the underlying transport. */
+ this.transport = transport;
+
+ /**
+ * The request for which a response is being generated, null if the
+ * incoming request has not been fully received or if it had errors.
+ */
+ this.request = null;
+
+ /** This allows a connection to disambiguate between a peer initiating a
+ * close and the socket being forced closed on shutdown.
+ */
+ this._closed = false;
+
+ /** State variable for debugging. */
+ this._processed = false;
+
+ /** whether or not 1st line of request has been received */
+ this._requestStarted = false;
+}
+Connection.prototype = {
+ /** Closes this connection's input/output streams. */
+ close() {
+ if (this._closed) {
+ return;
+ }
+
+ dumpn(
+ "*** closing connection " + this.number + " on port " + this._outgoingPort
+ );
+
+ this.input.close();
+ this.output.close();
+ this._closed = true;
+
+ var server = this.server;
+ server._connectionClosed(this);
+
+ // If an error triggered a server shutdown, act on it now
+ if (server._doQuit) {
+ server.stop(function () {
+ /* not like we can do anything better */
+ });
+ }
+ },
+
+ /**
+ * Initiates processing of this connection, using the data in the given
+ * request.
+ *
+ * @param request : Request
+ * the request which should be processed
+ */
+ process(request) {
+ NS_ASSERT(!this._closed && !this._processed);
+
+ this._processed = true;
+
+ this.request = request;
+ this.server._handler.handleResponse(this);
+ },
+
+ /**
+ * Initiates processing of this connection, generating a response with the
+ * given HTTP error code.
+ *
+ * @param code : uint
+ * an HTTP code, so in the range [0, 1000)
+ * @param request : Request
+ * incomplete data about the incoming request (since there were errors
+ * during its processing
+ */
+ processError(code, request) {
+ NS_ASSERT(!this._closed && !this._processed);
+
+ this._processed = true;
+ this.request = request;
+ this.server._handler.handleError(code, this);
+ },
+
+ /** Converts this to a string for debugging purposes. */
+ toString() {
+ return (
+ "<Connection(" +
+ this.number +
+ (this.request ? ", " + this.request.path : "") +
+ "): " +
+ (this._closed ? "closed" : "open") +
+ ">"
+ );
+ },
+
+ requestStarted() {
+ this._requestStarted = true;
+ },
+};
+
+/** Returns an array of count bytes from the given input stream. */
+function readBytes(inputStream, count) {
+ return new BinaryInputStream(inputStream).readByteArray(count);
+}
+
+/** Request reader processing states; see RequestReader for details. */
+const READER_IN_REQUEST_LINE = 0;
+const READER_IN_HEADERS = 1;
+const READER_IN_BODY = 2;
+const READER_FINISHED = 3;
+
+/**
+ * Reads incoming request data asynchronously, does any necessary preprocessing,
+ * and forwards it to the request handler. Processing occurs in three states:
+ *
+ * READER_IN_REQUEST_LINE Reading the request's status line
+ * READER_IN_HEADERS Reading headers in the request
+ * READER_IN_BODY Reading the body of the request
+ * READER_FINISHED Entire request has been read and processed
+ *
+ * During the first two stages, initial metadata about the request is gathered
+ * into a Request object. Once the status line and headers have been processed,
+ * we start processing the body of the request into the Request. Finally, when
+ * the entire body has been read, we create a Response and hand it off to the
+ * ServerHandler to be given to the appropriate request handler.
+ *
+ * @param connection : Connection
+ * the connection for the request being read
+ */
+function RequestReader(connection) {
+ /** Connection metadata for this request. */
+ this._connection = connection;
+
+ /**
+ * A container providing line-by-line access to the raw bytes that make up the
+ * data which has been read from the connection but has not yet been acted
+ * upon (by passing it to the request handler or by extracting request
+ * metadata from it).
+ */
+ this._data = new LineData();
+
+ /**
+ * The amount of data remaining to be read from the body of this request.
+ * After all headers in the request have been read this is the value in the
+ * Content-Length header, but as the body is read its value decreases to zero.
+ */
+ this._contentLength = 0;
+
+ /** The current state of parsing the incoming request. */
+ this._state = READER_IN_REQUEST_LINE;
+
+ /** Metadata constructed from the incoming request for the request handler. */
+ this._metadata = new Request(connection.port);
+
+ /**
+ * Used to preserve state if we run out of line data midway through a
+ * multi-line header. _lastHeaderName stores the name of the header, while
+ * _lastHeaderValue stores the value we've seen so far for the header.
+ *
+ * These fields are always either both undefined or both strings.
+ */
+ this._lastHeaderName = this._lastHeaderValue = undefined;
+}
+RequestReader.prototype = {
+ // NSIINPUTSTREAMCALLBACK
+
+ /**
+ * Called when more data from the incoming request is available. This method
+ * then reads the available data from input and deals with that data as
+ * necessary, depending upon the syntax of already-downloaded data.
+ *
+ * @param input : nsIAsyncInputStream
+ * the stream of incoming data from the connection
+ */
+ onInputStreamReady(input) {
+ dumpn(
+ "*** onInputStreamReady(input=" +
+ input +
+ ") on thread " +
+ Services.tm.currentThread +
+ " (main is " +
+ Services.tm.mainThread +
+ ")"
+ );
+ dumpn("*** this._state == " + this._state);
+
+ // Handle cases where we get more data after a request error has been
+ // discovered but *before* we can close the connection.
+ var data = this._data;
+ if (!data) {
+ return;
+ }
+
+ try {
+ data.appendBytes(readBytes(input, input.available()));
+ } catch (e) {
+ if (streamClosed(e)) {
+ dumpn(
+ "*** WARNING: unexpected error when reading from socket; will " +
+ "be treated as if the input stream had been closed"
+ );
+ dumpn("*** WARNING: actual error was: " + e);
+ }
+
+ // We've lost a race -- input has been closed, but we're still expecting
+ // to read more data. available() will throw in this case, and since
+ // we're dead in the water now, destroy the connection.
+ dumpn(
+ "*** onInputStreamReady called on a closed input, destroying " +
+ "connection"
+ );
+ this._connection.close();
+ return;
+ }
+
+ switch (this._state) {
+ default:
+ NS_ASSERT(false, "invalid state: " + this._state);
+ break;
+
+ case READER_IN_REQUEST_LINE:
+ if (!this._processRequestLine()) {
+ break;
+ }
+ /* fall through */
+
+ case READER_IN_HEADERS:
+ if (!this._processHeaders()) {
+ break;
+ }
+ /* fall through */
+
+ case READER_IN_BODY:
+ this._processBody();
+ }
+
+ if (this._state != READER_FINISHED) {
+ input.asyncWait(this, 0, 0, Services.tm.currentThread);
+ }
+ },
+
+ //
+ // see nsISupports.QueryInterface
+ //
+ QueryInterface: ChromeUtils.generateQI(["nsIInputStreamCallback"]),
+
+ // PRIVATE API
+
+ /**
+ * Processes unprocessed, downloaded data as a request line.
+ *
+ * @returns boolean
+ * true iff the request line has been fully processed
+ */
+ _processRequestLine() {
+ NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
+
+ // Servers SHOULD ignore any empty line(s) received where a Request-Line
+ // is expected (section 4.1).
+ var data = this._data;
+ var line = {};
+ var readSuccess;
+ while ((readSuccess = data.readLine(line)) && line.value == "") {
+ dumpn("*** ignoring beginning blank line...");
+ }
+
+ // if we don't have a full line, wait until we do
+ if (!readSuccess) {
+ return false;
+ }
+
+ // we have the first non-blank line
+ try {
+ this._parseRequestLine(line.value);
+ this._state = READER_IN_HEADERS;
+ this._connection.requestStarted();
+ return true;
+ } catch (e) {
+ this._handleError(e);
+ return false;
+ }
+ },
+
+ /**
+ * Processes stored data, assuming it is either at the beginning or in
+ * the middle of processing request headers.
+ *
+ * @returns boolean
+ * true iff header data in the request has been fully processed
+ */
+ _processHeaders() {
+ NS_ASSERT(this._state == READER_IN_HEADERS);
+
+ // XXX things to fix here:
+ //
+ // - need to support RFC 2047-encoded non-US-ASCII characters
+
+ try {
+ var done = this._parseHeaders();
+ if (done) {
+ var request = this._metadata;
+
+ // XXX this is wrong for requests with transfer-encodings applied to
+ // them, particularly chunked (which by its nature can have no
+ // meaningful Content-Length header)!
+ this._contentLength = request.hasHeader("Content-Length")
+ ? parseInt(request.getHeader("Content-Length"), 10)
+ : 0;
+ dumpn("_processHeaders, Content-length=" + this._contentLength);
+
+ this._state = READER_IN_BODY;
+ }
+ return done;
+ } catch (e) {
+ this._handleError(e);
+ return false;
+ }
+ },
+
+ /**
+ * Processes stored data, assuming it is either at the beginning or in
+ * the middle of processing the request body.
+ *
+ * @returns boolean
+ * true iff the request body has been fully processed
+ */
+ _processBody() {
+ NS_ASSERT(this._state == READER_IN_BODY);
+
+ // XXX handle chunked transfer-coding request bodies!
+
+ try {
+ if (this._contentLength > 0) {
+ var data = this._data.purge();
+ var count = Math.min(data.length, this._contentLength);
+ dumpn(
+ "*** loading data=" +
+ data +
+ " len=" +
+ data.length +
+ " excess=" +
+ (data.length - count)
+ );
+ data.length = count;
+
+ var bos = new BinaryOutputStream(this._metadata._bodyOutputStream);
+ bos.writeByteArray(data);
+ this._contentLength -= count;
+ }
+
+ dumpn("*** remaining body data len=" + this._contentLength);
+ if (this._contentLength == 0) {
+ this._validateRequest();
+ this._state = READER_FINISHED;
+ this._handleResponse();
+ return true;
+ }
+
+ return false;
+ } catch (e) {
+ this._handleError(e);
+ return false;
+ }
+ },
+
+ /**
+ * Does various post-header checks on the data in this request.
+ *
+ * @throws : HttpError
+ * if the request was malformed in some way
+ */
+ _validateRequest() {
+ NS_ASSERT(this._state == READER_IN_BODY);
+
+ dumpn("*** _validateRequest");
+
+ var metadata = this._metadata;
+ var headers = metadata._headers;
+
+ // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header
+ var identity = this._connection.server.identity;
+ if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) {
+ if (!headers.hasHeader("Host")) {
+ dumpn("*** malformed HTTP/1.1 or greater request with no Host header!");
+ throw HTTP_400;
+ }
+
+ // If the Request-URI wasn't absolute, then we need to determine our host.
+ // We have to determine what scheme was used to access us based on the
+ // server identity data at this point, because the request just doesn't
+ // contain enough data on its own to do this, sadly.
+ if (!metadata._host) {
+ var host, port;
+ var hostPort = headers.getHeader("Host");
+ var colon = hostPort.lastIndexOf(":");
+ if (hostPort.lastIndexOf("]") > colon) {
+ colon = -1;
+ }
+ if (colon < 0) {
+ host = hostPort;
+ port = "";
+ } else {
+ host = hostPort.substring(0, colon);
+ port = hostPort.substring(colon + 1);
+ }
+
+ // NB: We allow an empty port here because, oddly, a colon may be
+ // present even without a port number, e.g. "example.com:"; in this
+ // case the default port applies.
+ if (!HOST_REGEX.test(host) || !/^\d*$/.test(port)) {
+ dumpn(
+ "*** malformed hostname (" +
+ hostPort +
+ ") in Host " +
+ "header, 400 time"
+ );
+ throw HTTP_400;
+ }
+
+ // If we're not given a port, we're stuck, because we don't know what
+ // scheme to use to look up the correct port here, in general. Since
+ // the HTTPS case requires a tunnel/proxy and thus requires that the
+ // requested URI be absolute (and thus contain the necessary
+ // information), let's assume HTTP will prevail and use that.
+ port = +port || 80;
+
+ var scheme = identity.getScheme(host, port);
+ if (!scheme) {
+ dumpn(
+ "*** unrecognized hostname (" +
+ hostPort +
+ ") in Host " +
+ "header, 400 time"
+ );
+ throw HTTP_400;
+ }
+
+ metadata._scheme = scheme;
+ metadata._host = host;
+ metadata._port = port;
+ }
+ } else {
+ NS_ASSERT(
+ metadata._host === undefined,
+ "HTTP/1.0 doesn't allow absolute paths in the request line!"
+ );
+
+ metadata._scheme = identity.primaryScheme;
+ metadata._host = identity.primaryHost;
+ metadata._port = identity.primaryPort;
+ }
+
+ NS_ASSERT(
+ identity.has(metadata._scheme, metadata._host, metadata._port),
+ "must have a location we recognize by now!"
+ );
+ },
+
+ /**
+ * Handles responses in case of error, either in the server or in the request.
+ *
+ * @param e
+ * the specific error encountered, which is an HttpError in the case where
+ * the request is in some way invalid or cannot be fulfilled; if this isn't
+ * an HttpError we're going to be paranoid and shut down, because that
+ * shouldn't happen, ever
+ */
+ _handleError(e) {
+ // Don't fall back into normal processing!
+ this._state = READER_FINISHED;
+
+ var server = this._connection.server;
+ if (e instanceof HttpError) {
+ var code = e.code;
+ } else {
+ dumpn(
+ "!!! UNEXPECTED ERROR: " +
+ e +
+ (e.lineNumber ? ", line " + e.lineNumber : "")
+ );
+
+ // no idea what happened -- be paranoid and shut down
+ code = 500;
+ server._requestQuit();
+ }
+
+ // make attempted reuse of data an error
+ this._data = null;
+
+ this._connection.processError(code, this._metadata);
+ },
+
+ /**
+ * Now that we've read the request line and headers, we can actually hand off
+ * the request to be handled.
+ *
+ * This method is called once per request, after the request line and all
+ * headers and the body, if any, have been received.
+ */
+ _handleResponse() {
+ NS_ASSERT(this._state == READER_FINISHED);
+
+ // We don't need the line-based data any more, so make attempted reuse an
+ // error.
+ this._data = null;
+
+ this._connection.process(this._metadata);
+ },
+
+ // PARSING
+
+ /**
+ * Parses the request line for the HTTP request associated with this.
+ *
+ * @param line : string
+ * the request line
+ */
+ _parseRequestLine(line) {
+ NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
+
+ dumpn("*** _parseRequestLine('" + line + "')");
+
+ var metadata = this._metadata;
+
+ // clients and servers SHOULD accept any amount of SP or HT characters
+ // between fields, even though only a single SP is required (section 19.3)
+ var request = line.split(/[ \t]+/);
+ if (!request || request.length != 3) {
+ dumpn("*** No request in line");
+ throw HTTP_400;
+ }
+
+ metadata._method = request[0];
+
+ // get the HTTP version
+ var ver = request[2];
+ var match = ver.match(/^HTTP\/(\d+\.\d+)$/);
+ if (!match) {
+ dumpn("*** No HTTP version in line");
+ throw HTTP_400;
+ }
+
+ // determine HTTP version
+ try {
+ metadata._httpVersion = new nsHttpVersion(match[1]);
+ if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0)) {
+ throw new Error("unsupported HTTP version");
+ }
+ } catch (e) {
+ // we support HTTP/1.0 and HTTP/1.1 only
+ throw HTTP_501;
+ }
+
+ var fullPath = request[1];
+
+ if (metadata._method == "CONNECT") {
+ metadata._path = "CONNECT";
+ metadata._scheme = "https";
+ [metadata._host, metadata._port] = fullPath.split(":");
+ return;
+ }
+
+ var serverIdentity = this._connection.server.identity;
+ var scheme, host, port;
+
+ if (fullPath.charAt(0) != "/") {
+ // No absolute paths in the request line in HTTP prior to 1.1
+ if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) {
+ dumpn("*** Metadata version too low");
+ throw HTTP_400;
+ }
+
+ try {
+ var uri = Services.io.newURI(fullPath);
+ fullPath = uri.pathQueryRef;
+ scheme = uri.scheme;
+ host = uri.asciiHost;
+ if (host.includes(":")) {
+ // If the host still contains a ":", then it is an IPv6 address.
+ // IPv6 addresses-as-host are registered with brackets, so we need to
+ // wrap the host in brackets because nsIURI's host lacks them.
+ // This inconsistency in nsStandardURL is tracked at bug 1195459.
+ host = `[${host}]`;
+ }
+ metadata._host = host;
+ port = uri.port;
+ if (port === -1) {
+ if (scheme === "http") {
+ port = 80;
+ } else if (scheme === "https") {
+ port = 443;
+ } else {
+ dumpn("*** Unknown scheme: " + scheme);
+ throw HTTP_400;
+ }
+ }
+ } catch (e) {
+ // If the host is not a valid host on the server, the response MUST be a
+ // 400 (Bad Request) error message (section 5.2). Alternately, the URI
+ // is malformed.
+ dumpn("*** Threw when dealing with URI: " + e);
+ throw HTTP_400;
+ }
+
+ if (
+ !serverIdentity.has(scheme, host, port) ||
+ fullPath.charAt(0) != "/"
+ ) {
+ dumpn("*** serverIdentity unknown or path does not start with '/'");
+ throw HTTP_400;
+ }
+ }
+
+ var splitter = fullPath.indexOf("?");
+ if (splitter < 0) {
+ // _queryString already set in ctor
+ metadata._path = fullPath;
+ } else {
+ metadata._path = fullPath.substring(0, splitter);
+ metadata._queryString = fullPath.substring(splitter + 1);
+ }
+
+ metadata._scheme = scheme;
+ metadata._host = host;
+ metadata._port = port;
+ },
+
+ /**
+ * Parses all available HTTP headers in this until the header-ending CRLFCRLF,
+ * adding them to the store of headers in the request.
+ *
+ * @throws
+ * HTTP_400 if the headers are malformed
+ * @returns boolean
+ * true if all headers have now been processed, false otherwise
+ */
+ _parseHeaders() {
+ NS_ASSERT(this._state == READER_IN_HEADERS);
+
+ dumpn("*** _parseHeaders");
+
+ var data = this._data;
+
+ var headers = this._metadata._headers;
+ var lastName = this._lastHeaderName;
+ var lastVal = this._lastHeaderValue;
+
+ var line = {};
+ while (true) {
+ dumpn("*** Last name: '" + lastName + "'");
+ dumpn("*** Last val: '" + lastVal + "'");
+ NS_ASSERT(
+ !((lastVal === undefined) ^ (lastName === undefined)),
+ lastName === undefined
+ ? "lastVal without lastName? lastVal: '" + lastVal + "'"
+ : "lastName without lastVal? lastName: '" + lastName + "'"
+ );
+
+ if (!data.readLine(line)) {
+ // save any data we have from the header we might still be processing
+ this._lastHeaderName = lastName;
+ this._lastHeaderValue = lastVal;
+ return false;
+ }
+
+ var lineText = line.value;
+ dumpn("*** Line text: '" + lineText + "'");
+ var firstChar = lineText.charAt(0);
+
+ // blank line means end of headers
+ if (lineText == "") {
+ // we're finished with the previous header
+ if (lastName) {
+ try {
+ headers.setHeader(lastName, lastVal, true);
+ } catch (e) {
+ dumpn("*** setHeader threw on last header, e == " + e);
+ throw HTTP_400;
+ }
+ } else {
+ // no headers in request -- valid for HTTP/1.0 requests
+ }
+
+ // either way, we're done processing headers
+ this._state = READER_IN_BODY;
+ return true;
+ } else if (firstChar == " " || firstChar == "\t") {
+ // multi-line header if we've already seen a header line
+ if (!lastName) {
+ dumpn("We don't have a header to continue!");
+ throw HTTP_400;
+ }
+
+ // append this line's text to the value; starts with SP/HT, so no need
+ // for separating whitespace
+ lastVal += lineText;
+ } else {
+ // we have a new header, so set the old one (if one existed)
+ if (lastName) {
+ try {
+ headers.setHeader(lastName, lastVal, true);
+ } catch (e) {
+ dumpn("*** setHeader threw on a header, e == " + e);
+ throw HTTP_400;
+ }
+ }
+
+ var colon = lineText.indexOf(":"); // first colon must be splitter
+ if (colon < 1) {
+ dumpn("*** No colon or missing header field-name");
+ throw HTTP_400;
+ }
+
+ // set header name, value (to be set in the next loop, usually)
+ lastName = lineText.substring(0, colon);
+ lastVal = lineText.substring(colon + 1);
+ } // empty, continuation, start of header
+ } // while (true)
+ },
+};
+
+/** The character codes for CR and LF. */
+const CR = 0x0d,
+ LF = 0x0a;
+
+/**
+ * Calculates the number of characters before the first CRLF pair in array, or
+ * -1 if the array contains no CRLF pair.
+ *
+ * @param array : Array
+ * an array of numbers in the range [0, 256), each representing a single
+ * character; the first CRLF is the lowest index i where
+ * |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|,
+ * if such an |i| exists, and -1 otherwise
+ * @param start : uint
+ * start index from which to begin searching in array
+ * @returns int
+ * the index of the first CRLF if any were present, -1 otherwise
+ */
+function findCRLF(array, start) {
+ for (var i = array.indexOf(CR, start); i >= 0; i = array.indexOf(CR, i + 1)) {
+ if (array[i + 1] == LF) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * A container which provides line-by-line access to the arrays of bytes with
+ * which it is seeded.
+ */
+export function LineData() {
+ /** An array of queued bytes from which to get line-based characters. */
+ this._data = [];
+
+ /** Start index from which to search for CRLF. */
+ this._start = 0;
+}
+LineData.prototype = {
+ /**
+ * Appends the bytes in the given array to the internal data cache maintained
+ * by this.
+ */
+ appendBytes(bytes) {
+ var count = bytes.length;
+ var quantum = 262144; // just above half SpiderMonkey's argument-count limit
+ if (count < quantum) {
+ Array.prototype.push.apply(this._data, bytes);
+ return;
+ }
+
+ // Large numbers of bytes may cause Array.prototype.push to be called with
+ // more arguments than the JavaScript engine supports. In that case append
+ // bytes in fixed-size amounts until all bytes are appended.
+ for (var start = 0; start < count; start += quantum) {
+ var slice = bytes.slice(start, Math.min(start + quantum, count));
+ Array.prototype.push.apply(this._data, slice);
+ }
+ },
+
+ /**
+ * Removes and returns a line of data, delimited by CRLF, from this.
+ *
+ * @param out
+ * an object whose "value" property will be set to the first line of text
+ * present in this, sans CRLF, if this contains a full CRLF-delimited line
+ * of text; if this doesn't contain enough data, the value of the property
+ * is undefined
+ * @returns boolean
+ * true if a full line of data could be read from the data in this, false
+ * otherwise
+ */
+ readLine(out) {
+ var data = this._data;
+ var length = findCRLF(data, this._start);
+ if (length < 0) {
+ this._start = data.length;
+
+ // But if our data ends in a CR, we have to back up one, because
+ // the first byte in the next packet might be an LF and if we
+ // start looking at data.length we won't find it.
+ if (data.length && data[data.length - 1] === CR) {
+ --this._start;
+ }
+
+ return false;
+ }
+
+ // Reset for future lines.
+ this._start = 0;
+
+ //
+ // We have the index of the CR, so remove all the characters, including
+ // CRLF, from the array with splice, and convert the removed array
+ // (excluding the trailing CRLF characters) into the corresponding string.
+ //
+ var leading = data.splice(0, length + 2);
+ var quantum = 262144;
+ var line = "";
+ for (var start = 0; start < length; start += quantum) {
+ var slice = leading.slice(start, Math.min(start + quantum, length));
+ line += String.fromCharCode.apply(null, slice);
+ }
+
+ out.value = line;
+ return true;
+ },
+
+ /**
+ * Removes the bytes currently within this and returns them in an array.
+ *
+ * @returns Array
+ * the bytes within this when this method is called
+ */
+ purge() {
+ var data = this._data;
+ this._data = [];
+ return data;
+ },
+};
+
+/**
+ * Creates a request-handling function for an nsIHttpRequestHandler object.
+ */
+function createHandlerFunc(handler) {
+ return function (metadata, response) {
+ handler.handle(metadata, response);
+ };
+}
+
+/**
+ * The default handler for directories; writes an HTML response containing a
+ * slightly-formatted directory listing.
+ */
+function defaultIndexHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ var path = htmlEscape(decodeURI(metadata.path));
+
+ //
+ // Just do a very basic bit of directory listings -- no need for too much
+ // fanciness, especially since we don't have a style sheet in which we can
+ // stick rules (don't want to pollute the default path-space).
+ //
+
+ var body =
+ "<html>\
+ <head>\
+ <title>" +
+ path +
+ "</title>\
+ </head>\
+ <body>\
+ <h1>" +
+ path +
+ '</h1>\
+ <ol style="list-style-type: none">';
+
+ var directory = metadata.getProperty("directory");
+ NS_ASSERT(directory && directory.isDirectory());
+
+ var fileList = [];
+ var files = directory.directoryEntries;
+ while (files.hasMoreElements()) {
+ var f = files.nextFile;
+ let name = f.leafName;
+ if (
+ !f.isHidden() &&
+ (name.charAt(name.length - 1) != HIDDEN_CHAR ||
+ name.charAt(name.length - 2) == HIDDEN_CHAR)
+ ) {
+ fileList.push(f);
+ }
+ }
+
+ fileList.sort(fileSort);
+
+ for (var i = 0; i < fileList.length; i++) {
+ var file = fileList[i];
+ try {
+ let name = file.leafName;
+ if (name.charAt(name.length - 1) == HIDDEN_CHAR) {
+ name = name.substring(0, name.length - 1);
+ }
+ var sep = file.isDirectory() ? "/" : "";
+
+ // Note: using " to delimit the attribute here because encodeURIComponent
+ // passes through '.
+ var item =
+ '<li><a href="' +
+ encodeURIComponent(name) +
+ sep +
+ '">' +
+ htmlEscape(name) +
+ sep +
+ "</a></li>";
+
+ body += item;
+ } catch (e) {
+ /* some file system error, ignore the file */
+ }
+ }
+
+ body +=
+ " </ol>\
+ </body>\
+ </html>";
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+/**
+ * Sorts a and b (nsIFile objects) into an aesthetically pleasing order.
+ */
+function fileSort(a, b) {
+ var dira = a.isDirectory(),
+ dirb = b.isDirectory();
+
+ if (dira && !dirb) {
+ return -1;
+ }
+ if (dirb && !dira) {
+ return 1;
+ }
+
+ var namea = a.leafName.toLowerCase(),
+ nameb = b.leafName.toLowerCase();
+ return nameb > namea ? -1 : 1;
+}
+
+/**
+ * Converts an externally-provided path into an internal path for use in
+ * determining file mappings.
+ *
+ * @param path
+ * the path to convert
+ * @param encoded
+ * true if the given path should be passed through decodeURI prior to
+ * conversion
+ * @throws URIError
+ * if path is incorrectly encoded
+ */
+function toInternalPath(path, encoded) {
+ if (encoded) {
+ path = decodeURI(path);
+ }
+
+ var comps = path.split("/");
+ for (var i = 0, sz = comps.length; i < sz; i++) {
+ var comp = comps[i];
+ if (comp.charAt(comp.length - 1) == HIDDEN_CHAR) {
+ comps[i] = comp + HIDDEN_CHAR;
+ }
+ }
+ return comps.join("/");
+}
+
+const PERMS_READONLY = (4 << 6) | (4 << 3) | 4;
+
+/**
+ * Adds custom-specified headers for the given file to the given response, if
+ * any such headers are specified.
+ *
+ * @param file
+ * the file on the disk which is to be written
+ * @param metadata
+ * metadata about the incoming request
+ * @param response
+ * the Response to which any specified headers/data should be written
+ * @throws HTTP_500
+ * if an error occurred while processing custom-specified headers
+ */
+function maybeAddHeadersInternal(
+ file,
+ metadata,
+ response,
+ informationalResponse
+) {
+ var name = file.leafName;
+ if (name.charAt(name.length - 1) == HIDDEN_CHAR) {
+ name = name.substring(0, name.length - 1);
+ }
+
+ var headerFile = file.parent;
+ if (!informationalResponse) {
+ headerFile.append(name + HEADERS_SUFFIX);
+ } else {
+ headerFile.append(name + INFORMATIONAL_RESPONSE_SUFFIX);
+ }
+
+ if (!headerFile.exists()) {
+ return;
+ }
+
+ const PR_RDONLY = 0x01;
+ var fis = new FileInputStream(
+ headerFile,
+ PR_RDONLY,
+ PERMS_READONLY,
+ Ci.nsIFileInputStream.CLOSE_ON_EOF
+ );
+
+ try {
+ var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
+ lis.QueryInterface(Ci.nsIUnicharLineInputStream);
+
+ var line = { value: "" };
+ var more = lis.readLine(line);
+
+ if (!more && line.value == "") {
+ return;
+ }
+
+ // request line
+
+ var status = line.value;
+ if (status.indexOf("HTTP ") == 0) {
+ status = status.substring(5);
+ var space = status.indexOf(" ");
+ var code, description;
+ if (space < 0) {
+ code = status;
+ description = "";
+ } else {
+ code = status.substring(0, space);
+ description = status.substring(space + 1, status.length);
+ }
+
+ if (!informationalResponse) {
+ response.setStatusLine(
+ metadata.httpVersion,
+ parseInt(code, 10),
+ description
+ );
+ } else {
+ response.setInformationalResponseStatusLine(
+ metadata.httpVersion,
+ parseInt(code, 10),
+ description
+ );
+ }
+
+ line.value = "";
+ more = lis.readLine(line);
+ } else if (informationalResponse) {
+ // An informational response must have a status line.
+ return;
+ }
+
+ // headers
+ while (more || line.value != "") {
+ var header = line.value;
+ var colon = header.indexOf(":");
+
+ if (!informationalResponse) {
+ response.setHeader(
+ header.substring(0, colon),
+ header.substring(colon + 1, header.length),
+ false
+ ); // allow overriding server-set headers
+ } else {
+ response.setInformationalResponseHeader(
+ header.substring(0, colon),
+ header.substring(colon + 1, header.length),
+ false
+ ); // allow overriding server-set headers
+ }
+
+ line.value = "";
+ more = lis.readLine(line);
+ }
+ } catch (e) {
+ dumpn("WARNING: error in headers for " + metadata.path + ": " + e);
+ throw HTTP_500;
+ } finally {
+ fis.close();
+ }
+}
+
+function maybeAddHeaders(file, metadata, response) {
+ maybeAddHeadersInternal(file, metadata, response, false);
+}
+
+function maybeAddInformationalResponse(file, metadata, response) {
+ maybeAddHeadersInternal(file, metadata, response, true);
+}
+
+/**
+ * An object which handles requests for a server, executing default and
+ * overridden behaviors as instructed by the code which uses and manipulates it.
+ * Default behavior includes the paths / and /trace (diagnostics), with some
+ * support for HTTP error pages for various codes and fallback to HTTP 500 if
+ * those codes fail for any reason.
+ *
+ * @param server : nsHttpServer
+ * the server in which this handler is being used
+ */
+function ServerHandler(server) {
+ // FIELDS
+
+ /**
+ * The nsHttpServer instance associated with this handler.
+ */
+ this._server = server;
+
+ /**
+ * A FileMap object containing the set of path->nsIFile mappings for
+ * all directory mappings set in the server (e.g., "/" for /var/www/html/,
+ * "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2).
+ *
+ * Note carefully: the leading and trailing "/" in each path (not file) are
+ * removed before insertion to simplify the code which uses this. You have
+ * been warned!
+ */
+ this._pathDirectoryMap = new FileMap();
+
+ /**
+ * Custom request handlers for the server in which this resides. Path-handler
+ * pairs are stored as property-value pairs in this property.
+ *
+ * @see ServerHandler.prototype._defaultPaths
+ */
+ this._overridePaths = {};
+
+ /**
+ * Custom request handlers for the path prefixes on the server in which this
+ * resides. Path-handler pairs are stored as property-value pairs in this
+ * property.
+ *
+ * @see ServerHandler.prototype._defaultPaths
+ */
+ this._overridePrefixes = {};
+
+ /**
+ * Custom request handlers for the error handlers in the server in which this
+ * resides. Path-handler pairs are stored as property-value pairs in this
+ * property.
+ *
+ * @see ServerHandler.prototype._defaultErrors
+ */
+ this._overrideErrors = {};
+
+ /**
+ * Maps file extensions to their MIME types in the server, overriding any
+ * mapping that might or might not exist in the MIME service.
+ */
+ this._mimeMappings = {};
+
+ /**
+ * The default handler for requests for directories, used to serve directories
+ * when no index file is present.
+ */
+ this._indexHandler = defaultIndexHandler;
+
+ /** Per-path state storage for the server. */
+ this._state = {};
+
+ /** Entire-server state storage. */
+ this._sharedState = {};
+
+ /** Entire-server state storage for nsISupports values. */
+ this._objectState = {};
+}
+ServerHandler.prototype = {
+ // PUBLIC API
+
+ /**
+ * Handles a request to this server, responding to the request appropriately
+ * and initiating server shutdown if necessary.
+ *
+ * This method never throws an exception.
+ *
+ * @param connection : Connection
+ * the connection for this request
+ */
+ handleResponse(connection) {
+ var request = connection.request;
+ var response = new Response(connection);
+
+ var path = request.path;
+ dumpn("*** path == " + path);
+
+ try {
+ try {
+ if (path in this._overridePaths) {
+ // explicit paths first, then files based on existing directory mappings,
+ // then (if the file doesn't exist) built-in server default paths
+ dumpn("calling override for " + path);
+ this._overridePaths[path](request, response);
+ } else {
+ var longestPrefix = "";
+ for (let prefix in this._overridePrefixes) {
+ if (
+ prefix.length > longestPrefix.length &&
+ path.substr(0, prefix.length) == prefix
+ ) {
+ longestPrefix = prefix;
+ }
+ }
+ if (longestPrefix.length) {
+ dumpn("calling prefix override for " + longestPrefix);
+ this._overridePrefixes[longestPrefix](request, response);
+ } else {
+ this._handleDefault(request, response);
+ }
+ }
+ } catch (e) {
+ if (response.partiallySent()) {
+ response.abort(e);
+ return;
+ }
+
+ if (!(e instanceof HttpError)) {
+ dumpn("*** unexpected error: e == " + e);
+ throw HTTP_500;
+ }
+ if (e.code !== 404) {
+ throw e;
+ }
+
+ dumpn("*** default: " + (path in this._defaultPaths));
+
+ response = new Response(connection);
+ if (path in this._defaultPaths) {
+ this._defaultPaths[path](request, response);
+ } else {
+ throw HTTP_404;
+ }
+ }
+ } catch (e) {
+ if (response.partiallySent()) {
+ response.abort(e);
+ return;
+ }
+
+ var errorCode = "internal";
+
+ try {
+ if (!(e instanceof HttpError)) {
+ throw e;
+ }
+
+ errorCode = e.code;
+ dumpn("*** errorCode == " + errorCode);
+
+ response = new Response(connection);
+ if (e.customErrorHandling) {
+ e.customErrorHandling(response);
+ }
+ this._handleError(errorCode, request, response);
+ return;
+ } catch (e2) {
+ dumpn(
+ "*** error handling " +
+ errorCode +
+ " error: " +
+ "e2 == " +
+ e2 +
+ ", shutting down server"
+ );
+
+ connection.server._requestQuit();
+ response.abort(e2);
+ return;
+ }
+ }
+
+ response.complete();
+ },
+
+ //
+ // see nsIHttpServer.registerFile
+ //
+ registerFile(path, file, handler) {
+ if (!file) {
+ dumpn("*** unregistering '" + path + "' mapping");
+ delete this._overridePaths[path];
+ return;
+ }
+
+ dumpn("*** registering '" + path + "' as mapping to " + file.path);
+ file = file.clone();
+
+ var self = this;
+ this._overridePaths[path] = function (request, response) {
+ if (!file.exists()) {
+ throw HTTP_404;
+ }
+
+ dumpn("*** responding '" + path + "' as mapping to " + file.path);
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ if (typeof handler === "function") {
+ handler(request, response);
+ }
+ self._writeFileResponse(request, file, response, 0, file.fileSize);
+ };
+ },
+
+ //
+ // see nsIHttpServer.registerPathHandler
+ //
+ registerPathHandler(path, handler) {
+ if (!path.length) {
+ throw Components.Exception(
+ "Handler path cannot be empty",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ // XXX true path validation!
+ if (path.charAt(0) != "/" && path != "CONNECT") {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ this._handlerToField(handler, this._overridePaths, path);
+ },
+
+ //
+ // see nsIHttpServer.registerPrefixHandler
+ //
+ registerPrefixHandler(path, handler) {
+ // XXX true path validation!
+ if (path.charAt(0) != "/" || path.charAt(path.length - 1) != "/") {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ this._handlerToField(handler, this._overridePrefixes, path);
+ },
+
+ //
+ // see nsIHttpServer.registerDirectory
+ //
+ registerDirectory(path, directory) {
+ // strip off leading and trailing '/' so that we can use lastIndexOf when
+ // determining exactly how a path maps onto a mapped directory --
+ // conditional is required here to deal with "/".substring(1, 0) being
+ // converted to "/".substring(0, 1) per the JS specification
+ var key = path.length == 1 ? "" : path.substring(1, path.length - 1);
+
+ // the path-to-directory mapping code requires that the first character not
+ // be "/", or it will go into an infinite loop
+ if (key.charAt(0) == "/") {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ key = toInternalPath(key, false);
+
+ if (directory) {
+ dumpn("*** mapping '" + path + "' to the location " + directory.path);
+ this._pathDirectoryMap.put(key, directory);
+ } else {
+ dumpn("*** removing mapping for '" + path + "'");
+ this._pathDirectoryMap.put(key, null);
+ }
+ },
+
+ //
+ // see nsIHttpServer.registerErrorHandler
+ //
+ registerErrorHandler(err, handler) {
+ if (!(err in HTTP_ERROR_CODES)) {
+ dumpn(
+ "*** WARNING: registering non-HTTP/1.1 error code " +
+ "(" +
+ err +
+ ") handler -- was this intentional?"
+ );
+ }
+
+ this._handlerToField(handler, this._overrideErrors, err);
+ },
+
+ //
+ // see nsIHttpServer.setIndexHandler
+ //
+ setIndexHandler(handler) {
+ if (!handler) {
+ handler = defaultIndexHandler;
+ } else if (typeof handler != "function") {
+ handler = createHandlerFunc(handler);
+ }
+
+ this._indexHandler = handler;
+ },
+
+ //
+ // see nsIHttpServer.registerContentType
+ //
+ registerContentType(ext, type) {
+ if (!type) {
+ delete this._mimeMappings[ext];
+ } else {
+ this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type);
+ }
+ },
+
+ // PRIVATE API
+
+ /**
+ * Sets or remove (if handler is null) a handler in an object with a key.
+ *
+ * @param handler
+ * a handler, either function or an nsIHttpRequestHandler
+ * @param dict
+ * The object to attach the handler to.
+ * @param key
+ * The field name of the handler.
+ */
+ _handlerToField(handler, dict, key) {
+ // for convenience, handler can be a function if this is run from xpcshell
+ if (typeof handler == "function") {
+ dict[key] = handler;
+ } else if (handler) {
+ dict[key] = createHandlerFunc(handler);
+ } else {
+ delete dict[key];
+ }
+ },
+
+ /**
+ * Handles a request which maps to a file in the local filesystem (if a base
+ * path has already been set; otherwise the 404 error is thrown).
+ *
+ * @param metadata : Request
+ * metadata for the incoming request
+ * @param response : Response
+ * an uninitialized Response to the given request, to be initialized by a
+ * request handler
+ * @throws HTTP_###
+ * if an HTTP error occurred (usually HTTP_404); note that in this case the
+ * calling code must handle post-processing of the response
+ */
+ _handleDefault(metadata, response) {
+ dumpn("*** _handleDefault()");
+
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+
+ var path = metadata.path;
+ NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">");
+
+ // determine the actual on-disk file; this requires finding the deepest
+ // path-to-directory mapping in the requested URL
+ var file = this._getFileForPath(path);
+
+ // the "file" might be a directory, in which case we either serve the
+ // contained index.html or make the index handler write the response
+ if (file.exists() && file.isDirectory()) {
+ file.append("index.html"); // make configurable?
+ if (!file.exists() || file.isDirectory()) {
+ metadata._ensurePropertyBag();
+ metadata._bag.setPropertyAsInterface("directory", file.parent);
+ this._indexHandler(metadata, response);
+ return;
+ }
+ }
+
+ // alternately, the file might not exist
+ if (!file.exists()) {
+ throw HTTP_404;
+ }
+
+ var start, end;
+ if (
+ metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) &&
+ metadata.hasHeader("Range") &&
+ this._getTypeFromFile(file) !== SJS_TYPE
+ ) {
+ var rangeMatch = metadata
+ .getHeader("Range")
+ .match(/^bytes=(\d+)?-(\d+)?$/);
+ if (!rangeMatch) {
+ dumpn(
+ "*** Range header bogosity: '" + metadata.getHeader("Range") + "'"
+ );
+ 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) {
+ dumpn(
+ "*** More Range header bogosity: '" +
+ metadata.getHeader("Range") +
+ "'"
+ );
+ 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) {
+ var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable");
+ HTTP_416.customErrorHandling = function (errorResponse) {
+ maybeAddHeaders(file, metadata, errorResponse);
+ };
+ 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 +
+ " from " +
+ start +
+ " to " +
+ end +
+ " inclusive"
+ );
+ this._writeFileResponse(metadata, file, response, start, end - start + 1);
+ },
+
+ /**
+ * Writes an HTTP response for the given file, including setting headers for
+ * file metadata.
+ *
+ * @param metadata : Request
+ * the Request for which a response is being generated
+ * @param file : nsIFile
+ * 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(metadata, file, response, offset, count) {
+ const PR_RDONLY = 0x01;
+
+ var type = this._getTypeFromFile(file);
+ if (type === SJS_TYPE) {
+ let fis = new FileInputStream(
+ file,
+ PR_RDONLY,
+ PERMS_READONLY,
+ Ci.nsIFileInputStream.CLOSE_ON_EOF
+ );
+
+ try {
+ // If you update the list of imports, please update the list in
+ // tools/lint/eslint/eslint-plugin-mozilla/lib/environments/sjs.js
+ // as well.
+ var s = Cu.Sandbox(Cu.getGlobalForObject({}), {
+ wantGlobalProperties: [
+ "atob",
+ "btoa",
+ "ChromeUtils",
+ "IOUtils",
+ "PathUtils",
+ "TextDecoder",
+ "TextEncoder",
+ "URLSearchParams",
+ "URL",
+ ],
+ });
+ s.importFunction(dump, "dump");
+ s.importFunction(Services, "Services");
+
+ // Define a basic key-value state-preservation API across requests, with
+ // keys initially corresponding to the empty string.
+ var self = this;
+ var path = metadata.path;
+ s.importFunction(function getState(k) {
+ return self._getState(path, k);
+ });
+ s.importFunction(function setState(k, v) {
+ self._setState(path, k, v);
+ });
+ s.importFunction(function getSharedState(k) {
+ return self._getSharedState(k);
+ });
+ s.importFunction(function setSharedState(k, v) {
+ self._setSharedState(k, v);
+ });
+ s.importFunction(function getObjectState(k, callback) {
+ callback(self._getObjectState(k));
+ });
+ s.importFunction(function setObjectState(k, v) {
+ self._setObjectState(k, v);
+ });
+ s.importFunction(function registerPathHandler(p, h) {
+ self.registerPathHandler(p, h);
+ });
+
+ // Make it possible for sjs files to access their location
+ this._setState(path, "__LOCATION__", file.path);
+
+ try {
+ // Alas, the line number in errors dumped to console when calling the
+ // request handler is simply an offset from where we load the SJS file.
+ // Work around this in a reasonably non-fragile way by dynamically
+ // getting the line number where we evaluate the SJS file. Don't
+ // separate these two lines!
+ var line = new Error().lineNumber;
+ let uri = Services.io.newFileURI(file);
+ Services.scriptloader.loadSubScript(uri.spec, s);
+ } catch (e) {
+ dumpn("*** syntax error in SJS at " + file.path + ": " + e);
+ throw HTTP_500;
+ }
+
+ try {
+ s.handleRequest(metadata, response);
+ } catch (e) {
+ dump(
+ "*** error running SJS at " +
+ file.path +
+ ": " +
+ e +
+ " on line " +
+ (e instanceof Error
+ ? e.lineNumber + " in httpd.js"
+ : e.lineNumber - line) +
+ "\n"
+ );
+ throw HTTP_500;
+ }
+ } finally {
+ fis.close();
+ }
+ } else {
+ try {
+ response.setHeader(
+ "Last-Modified",
+ toDateString(file.lastModifiedTime),
+ false
+ );
+ } catch (e) {
+ /* lastModifiedTime threw, ignore */
+ }
+
+ response.setHeader("Content-Type", type, false);
+ maybeAddInformationalResponse(file, metadata, response);
+ maybeAddHeaders(file, metadata, response);
+ // Allow overriding Content-Length
+ try {
+ response.getHeader("Content-Length");
+ } catch (e) {
+ response.setHeader("Content-Length", "" + count, false);
+ }
+
+ let fis = new FileInputStream(
+ file,
+ PR_RDONLY,
+ PERMS_READONLY,
+ Ci.nsIFileInputStream.CLOSE_ON_EOF
+ );
+
+ offset = offset || 0;
+ count = count || file.fileSize;
+ NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset");
+ NS_ASSERT(count >= 0, "bad count");
+ NS_ASSERT(offset + count <= file.fileSize, "bad total data size");
+
+ try {
+ if (offset !== 0) {
+ // Seek (or read, if seeking isn't supported) to the correct offset so
+ // the data sent to the client matches the requested range.
+ if (fis instanceof Ci.nsISeekableStream) {
+ fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset);
+ } else {
+ new ScriptableInputStream(fis).read(offset);
+ }
+ }
+ } catch (e) {
+ fis.close();
+ throw e;
+ }
+
+ let writeMore = function () {
+ Services.tm.currentThread.dispatch(
+ writeData,
+ Ci.nsIThread.DISPATCH_NORMAL
+ );
+ };
+
+ var input = new BinaryInputStream(fis);
+ var output = new BinaryOutputStream(response.bodyOutputStream);
+ var writeData = {
+ run() {
+ var chunkSize = Math.min(65536, count);
+ count -= chunkSize;
+ NS_ASSERT(count >= 0, "underflow");
+
+ try {
+ var data = input.readByteArray(chunkSize);
+ NS_ASSERT(
+ data.length === chunkSize,
+ "incorrect data returned? got " +
+ data.length +
+ ", expected " +
+ chunkSize
+ );
+ output.writeByteArray(data);
+ if (count === 0) {
+ fis.close();
+ response.finish();
+ } else {
+ writeMore();
+ }
+ } catch (e) {
+ try {
+ fis.close();
+ } finally {
+ response.finish();
+ }
+ throw e;
+ }
+ },
+ };
+
+ writeMore();
+
+ // Now that we know copying will start, flag the response as async.
+ response.processAsync();
+ }
+ },
+
+ /**
+ * Get the value corresponding to a given key for the given path for SJS state
+ * preservation across requests.
+ *
+ * @param path : string
+ * the path from which the given state is to be retrieved
+ * @param k : string
+ * the key whose corresponding value is to be returned
+ * @returns string
+ * the corresponding value, which is initially the empty string
+ */
+ _getState(path, k) {
+ var state = this._state;
+ if (path in state && k in state[path]) {
+ return state[path][k];
+ }
+ return "";
+ },
+
+ /**
+ * Set the value corresponding to a given key for the given path for SJS state
+ * preservation across requests.
+ *
+ * @param path : string
+ * the path from which the given state is to be retrieved
+ * @param k : string
+ * the key whose corresponding value is to be set
+ * @param v : string
+ * the value to be set
+ */
+ _setState(path, k, v) {
+ if (typeof v !== "string") {
+ throw new Error("non-string value passed");
+ }
+ var state = this._state;
+ if (!(path in state)) {
+ state[path] = {};
+ }
+ state[path][k] = v;
+ },
+
+ /**
+ * Get the value corresponding to a given key for SJS state preservation
+ * across requests.
+ *
+ * @param k : string
+ * the key whose corresponding value is to be returned
+ * @returns string
+ * the corresponding value, which is initially the empty string
+ */
+ _getSharedState(k) {
+ var state = this._sharedState;
+ if (k in state) {
+ return state[k];
+ }
+ return "";
+ },
+
+ /**
+ * Set the value corresponding to a given key for SJS state preservation
+ * across requests.
+ *
+ * @param k : string
+ * the key whose corresponding value is to be set
+ * @param v : string
+ * the value to be set
+ */
+ _setSharedState(k, v) {
+ if (typeof v !== "string") {
+ throw new Error("non-string value passed");
+ }
+ this._sharedState[k] = v;
+ },
+
+ /**
+ * Returns the object associated with the given key in the server for SJS
+ * state preservation across requests.
+ *
+ * @param k : string
+ * the key whose corresponding object is to be returned
+ * @returns nsISupports
+ * the corresponding object, or null if none was present
+ */
+ _getObjectState(k) {
+ if (typeof k !== "string") {
+ throw new Error("non-string key passed");
+ }
+ return this._objectState[k] || null;
+ },
+
+ /**
+ * Sets the object associated with the given key in the server for SJS
+ * state preservation across requests.
+ *
+ * @param k : string
+ * the key whose corresponding object is to be set
+ * @param v : nsISupports
+ * the object to be associated with the given key; may be null
+ */
+ _setObjectState(k, v) {
+ if (typeof k !== "string") {
+ throw new Error("non-string key passed");
+ }
+ if (typeof v !== "object") {
+ throw new Error("non-object value passed");
+ }
+ if (v && !("QueryInterface" in v)) {
+ throw new Error(
+ "must pass an nsISupports; use wrappedJSObject to ease " +
+ "pain when using the server from JS"
+ );
+ }
+
+ this._objectState[k] = v;
+ },
+
+ /**
+ * Gets a content-type for the given file, first by checking for any custom
+ * MIME-types registered with this handler for the file's extension, second by
+ * asking the global MIME service for a content-type, and finally by failing
+ * over to application/octet-stream.
+ *
+ * @param file : nsIFile
+ * the nsIFile for which to get a file type
+ * @returns string
+ * the best content-type which can be determined for the file
+ */
+ _getTypeFromFile(file) {
+ try {
+ var name = file.leafName;
+ var dot = name.lastIndexOf(".");
+ if (dot > 0) {
+ var ext = name.slice(dot + 1);
+ if (ext in this._mimeMappings) {
+ return this._mimeMappings[ext];
+ }
+ }
+ return Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromFile(file);
+ } catch (e) {
+ return "application/octet-stream";
+ }
+ },
+
+ /**
+ * Returns the nsIFile which corresponds to the path, as determined using
+ * all registered path->directory mappings and any paths which are explicitly
+ * overridden.
+ *
+ * @param path : string
+ * the server path for which a file should be retrieved, e.g. "/foo/bar"
+ * @throws HttpError
+ * when the correct action is the corresponding HTTP error (i.e., because no
+ * mapping was found for a directory in path, the referenced file doesn't
+ * exist, etc.)
+ * @returns nsIFile
+ * the file to be sent as the response to a request for the path
+ */
+ _getFileForPath(path) {
+ // decode and add underscores as necessary
+ try {
+ path = toInternalPath(path, true);
+ } catch (e) {
+ dumpn("*** toInternalPath threw " + e);
+ throw HTTP_400; // malformed path
+ }
+
+ // next, get the directory which contains this path
+ var pathMap = this._pathDirectoryMap;
+
+ // An example progression of tmp for a path "/foo/bar/baz/" might be:
+ // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", ""
+ var tmp = path.substring(1);
+ while (true) {
+ // do we have a match for current head of the path?
+ var file = pathMap.get(tmp);
+ if (file) {
+ // XXX hack; basically disable showing mapping for /foo/bar/ when the
+ // requested path was /foo/bar, because relative links on the page
+ // will all be incorrect -- we really need the ability to easily
+ // redirect here instead
+ if (
+ tmp == path.substring(1) &&
+ !!tmp.length &&
+ tmp.charAt(tmp.length - 1) != "/"
+ ) {
+ file = null;
+ } else {
+ break;
+ }
+ }
+
+ // if we've finished trying all prefixes, exit
+ if (tmp == "") {
+ break;
+ }
+
+ tmp = tmp.substring(0, tmp.lastIndexOf("/"));
+ }
+
+ // no mapping applies, so 404
+ if (!file) {
+ throw HTTP_404;
+ }
+
+ // last, get the file for the path within the determined directory
+ var parentFolder = file.parent;
+ var dirIsRoot = parentFolder == null;
+
+ // Strategy here is to append components individually, making sure we
+ // never move above the given directory; this allows paths such as
+ // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling";
+ // this component-wise approach also means the code works even on platforms
+ // which don't use "/" as the directory separator, such as Windows
+ var leafPath = path.substring(tmp.length + 1);
+ var comps = leafPath.split("/");
+ for (var i = 0, sz = comps.length; i < sz; i++) {
+ var comp = comps[i];
+
+ if (comp == "..") {
+ file = file.parent;
+ } else if (comp == "." || comp == "") {
+ continue;
+ } else {
+ file.append(comp);
+ }
+
+ if (!dirIsRoot && file.equals(parentFolder)) {
+ throw HTTP_403;
+ }
+ }
+
+ return file;
+ },
+
+ /**
+ * Writes the error page for the given HTTP error code over the given
+ * connection.
+ *
+ * @param errorCode : uint
+ * the HTTP error code to be used
+ * @param connection : Connection
+ * the connection on which the error occurred
+ */
+ handleError(errorCode, connection) {
+ var response = new Response(connection);
+
+ dumpn("*** error in request: " + errorCode);
+
+ this._handleError(errorCode, new Request(connection.port), response);
+ },
+
+ /**
+ * Handles a request which generates the given error code, using the
+ * user-defined error handler if one has been set, gracefully falling back to
+ * the x00 status code if the code has no handler, and failing to status code
+ * 500 if all else fails.
+ *
+ * @param errorCode : uint
+ * the HTTP error which is to be returned
+ * @param metadata : Request
+ * metadata for the request, which will often be incomplete since this is an
+ * error
+ * @param response : Response
+ * an uninitialized Response should be initialized when this method
+ * completes with information which represents the desired error code in the
+ * ideal case or a fallback code in abnormal circumstances (i.e., 500 is a
+ * fallback for 505, per HTTP specs)
+ */
+ _handleError(errorCode, metadata, response) {
+ if (!metadata) {
+ throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
+ }
+
+ var errorX00 = errorCode - (errorCode % 100);
+
+ try {
+ if (!(errorCode in HTTP_ERROR_CODES)) {
+ dumpn("*** WARNING: requested invalid error: " + errorCode);
+ }
+
+ // RFC 2616 says that we should try to handle an error by its class if we
+ // can't otherwise handle it -- if that fails, we revert to handling it as
+ // a 500 internal server error, and if that fails we throw and shut down
+ // the server
+
+ // actually handle the error
+ try {
+ if (errorCode in this._overrideErrors) {
+ this._overrideErrors[errorCode](metadata, response);
+ } else {
+ this._defaultErrors[errorCode](metadata, response);
+ }
+ } catch (e) {
+ if (response.partiallySent()) {
+ response.abort(e);
+ return;
+ }
+
+ // don't retry the handler that threw
+ if (errorX00 == errorCode) {
+ throw HTTP_500;
+ }
+
+ dumpn(
+ "*** error in handling for error code " +
+ errorCode +
+ ", " +
+ "falling back to " +
+ errorX00 +
+ "..."
+ );
+ response = new Response(response._connection);
+ if (errorX00 in this._overrideErrors) {
+ this._overrideErrors[errorX00](metadata, response);
+ } else if (errorX00 in this._defaultErrors) {
+ this._defaultErrors[errorX00](metadata, response);
+ } else {
+ throw HTTP_500;
+ }
+ }
+ } catch (e) {
+ if (response.partiallySent()) {
+ response.abort();
+ return;
+ }
+
+ // we've tried everything possible for a meaningful error -- now try 500
+ dumpn(
+ "*** error in handling for error code " +
+ errorX00 +
+ ", falling " +
+ "back to 500..."
+ );
+
+ try {
+ response = new Response(response._connection);
+ if (500 in this._overrideErrors) {
+ this._overrideErrors[500](metadata, response);
+ } else {
+ this._defaultErrors[500](metadata, response);
+ }
+ } catch (e2) {
+ dumpn("*** multiple errors in default error handlers!");
+ dumpn("*** e == " + e + ", e2 == " + e2);
+ response.abort(e2);
+ return;
+ }
+ }
+
+ response.complete();
+ },
+
+ // FIELDS
+
+ /**
+ * This object contains the default handlers for the various HTTP error codes.
+ */
+ _defaultErrors: {
+ 400(metadata, response) {
+ // none of the data in metadata is reliable, so hard-code everything here
+ response.setStatusLine("1.1", 400, "Bad Request");
+ response.setHeader("Content-Type", "text/plain;charset=utf-8", false);
+
+ var body = "Bad request\n";
+ response.bodyOutputStream.write(body, body.length);
+ },
+ 403(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 403, "Forbidden");
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ var body =
+ "<html>\
+ <head><title>403 Forbidden</title></head>\
+ <body>\
+ <h1>403 Forbidden</h1>\
+ </body>\
+ </html>";
+ response.bodyOutputStream.write(body, body.length);
+ },
+ 404(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 404, "Not Found");
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ var body =
+ "<html>\
+ <head><title>404 Not Found</title></head>\
+ <body>\
+ <h1>404 Not Found</h1>\
+ <p>\
+ <span style='font-family: monospace;'>" +
+ htmlEscape(metadata.path) +
+ "</span> was not found.\
+ </p>\
+ </body>\
+ </html>";
+ response.bodyOutputStream.write(body, body.length);
+ },
+ 416(metadata, response) {
+ response.setStatusLine(
+ metadata.httpVersion,
+ 416,
+ "Requested Range Not Satisfiable"
+ );
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ var body =
+ "<html>\
+ <head>\
+ <title>416 Requested Range Not Satisfiable</title></head>\
+ <body>\
+ <h1>416 Requested Range Not Satisfiable</h1>\
+ <p>The byte range was not valid for the\
+ requested resource.\
+ </p>\
+ </body>\
+ </html>";
+ response.bodyOutputStream.write(body, body.length);
+ },
+ 500(metadata, response) {
+ response.setStatusLine(
+ metadata.httpVersion,
+ 500,
+ "Internal Server Error"
+ );
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ var body =
+ "<html>\
+ <head><title>500 Internal Server Error</title></head>\
+ <body>\
+ <h1>500 Internal Server Error</h1>\
+ <p>Something's broken in this server and\
+ needs to be fixed.</p>\
+ </body>\
+ </html>";
+ response.bodyOutputStream.write(body, body.length);
+ },
+ 501(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 501, "Not Implemented");
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ var body =
+ "<html>\
+ <head><title>501 Not Implemented</title></head>\
+ <body>\
+ <h1>501 Not Implemented</h1>\
+ <p>This server is not (yet) Apache.</p>\
+ </body>\
+ </html>";
+ response.bodyOutputStream.write(body, body.length);
+ },
+ 505(metadata, response) {
+ response.setStatusLine("1.1", 505, "HTTP Version Not Supported");
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ var body =
+ "<html>\
+ <head><title>505 HTTP Version Not Supported</title></head>\
+ <body>\
+ <h1>505 HTTP Version Not Supported</h1>\
+ <p>This server only supports HTTP/1.0 and HTTP/1.1\
+ connections.</p>\
+ </body>\
+ </html>";
+ response.bodyOutputStream.write(body, body.length);
+ },
+ },
+
+ /**
+ * Contains handlers for the default set of URIs contained in this server.
+ */
+ _defaultPaths: {
+ "/": function (metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ var body =
+ "<html>\
+ <head><title>httpd.js</title></head>\
+ <body>\
+ <h1>httpd.js</h1>\
+ <p>If you're seeing this page, httpd.js is up and\
+ serving requests! Now set a base path and serve some\
+ files!</p>\
+ </body>\
+ </html>";
+
+ response.bodyOutputStream.write(body, body.length);
+ },
+
+ "/trace": function (metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain;charset=utf-8", false);
+
+ var body =
+ "Request-URI: " +
+ metadata.scheme +
+ "://" +
+ metadata.host +
+ ":" +
+ metadata.port +
+ metadata.path +
+ "\n\n";
+ body += "Request (semantically equivalent, slightly reformatted):\n\n";
+ body += metadata.method + " " + metadata.path;
+
+ if (metadata.queryString) {
+ body += "?" + metadata.queryString;
+ }
+
+ body += " HTTP/" + metadata.httpVersion + "\r\n";
+
+ var headEnum = metadata.headers;
+ while (headEnum.hasMoreElements()) {
+ var fieldName = headEnum
+ .getNext()
+ .QueryInterface(Ci.nsISupportsString).data;
+ body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n";
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+ },
+ },
+};
+
+/**
+ * Maps absolute paths to files on the local file system (as nsILocalFiles).
+ */
+function FileMap() {
+ /** Hash which will map paths to nsILocalFiles. */
+ this._map = {};
+}
+FileMap.prototype = {
+ // PUBLIC API
+
+ /**
+ * Maps key to a clone of the nsIFile value if value is non-null;
+ * otherwise, removes any extant mapping for key.
+ *
+ * @param key : string
+ * string to which a clone of value is mapped
+ * @param value : nsIFile
+ * the file to map to key, or null to remove a mapping
+ */
+ put(key, value) {
+ if (value) {
+ this._map[key] = value.clone();
+ } else {
+ delete this._map[key];
+ }
+ },
+
+ /**
+ * Returns a clone of the nsIFile mapped to key, or null if no such
+ * mapping exists.
+ *
+ * @param key : string
+ * key to which the returned file maps
+ * @returns nsIFile
+ * a clone of the mapped file, or null if no mapping exists
+ */
+ get(key) {
+ var val = this._map[key];
+ return val ? val.clone() : null;
+ },
+};
+
+// Response CONSTANTS
+
+// token = *<any CHAR except CTLs or separators>
+// CHAR = <any US-ASCII character (0-127)>
+// CTL = <any US-ASCII control character (0-31) and DEL (127)>
+// separators = "(" | ")" | "<" | ">" | "@"
+// | "," | ";" | ":" | "\" | <">
+// | "/" | "[" | "]" | "?" | "="
+// | "{" | "}" | SP | HT
+const IS_TOKEN_ARRAY = [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 0
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 8
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 16
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 24
+
+ 0,
+ 1,
+ 0,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1, // 32
+ 0,
+ 0,
+ 1,
+ 1,
+ 0,
+ 1,
+ 1,
+ 0, // 40
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1, // 48
+ 1,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 56
+
+ 0,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1, // 64
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1, // 72
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1, // 80
+ 1,
+ 1,
+ 1,
+ 0,
+ 0,
+ 0,
+ 1,
+ 1, // 88
+
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1, // 96
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1, // 104
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1, // 112
+ 1,
+ 1,
+ 1,
+ 0,
+ 1,
+ 0,
+ 1,
+]; // 120
+
+/**
+ * Determines whether the given character code is a CTL.
+ *
+ * @param code : uint
+ * the character code
+ * @returns boolean
+ * true if code is a CTL, false otherwise
+ */
+function isCTL(code) {
+ return (code >= 0 && code <= 31) || code == 127;
+}
+
+/**
+ * Represents a response to an HTTP request, encapsulating all details of that
+ * response. This includes all headers, the HTTP version, status code and
+ * explanation, and the entity itself.
+ *
+ * @param connection : Connection
+ * the connection over which this response is to be written
+ */
+function Response(connection) {
+ /** The connection over which this response will be written. */
+ this._connection = connection;
+
+ /**
+ * The HTTP version of this response; defaults to 1.1 if not set by the
+ * handler.
+ */
+ this._httpVersion = nsHttpVersion.HTTP_1_1;
+
+ /**
+ * The HTTP code of this response; defaults to 200.
+ */
+ this._httpCode = 200;
+
+ /**
+ * The description of the HTTP code in this response; defaults to "OK".
+ */
+ this._httpDescription = "OK";
+
+ /**
+ * An nsIHttpHeaders object in which the headers in this response should be
+ * stored. This property is null after the status line and headers have been
+ * written to the network, and it may be modified up until it is cleared,
+ * except if this._finished is set first (in which case headers are written
+ * asynchronously in response to a finish() call not preceded by
+ * flushHeaders()).
+ */
+ this._headers = new nsHttpHeaders();
+
+ /**
+ * Informational response:
+ * For example 103 Early Hint
+ **/
+ this._informationalResponseHttpVersion = nsHttpVersion.HTTP_1_1;
+ this._informationalResponseHttpCode = 0;
+ this._informationalResponseHttpDescription = "";
+ this._informationalResponseHeaders = new nsHttpHeaders();
+ this._informationalResponseSet = false;
+
+ /**
+ * Set to true when this response is ended (completely constructed if possible
+ * and the connection closed); further actions on this will then fail.
+ */
+ this._ended = false;
+
+ /**
+ * A stream used to hold data written to the body of this response.
+ */
+ this._bodyOutputStream = null;
+
+ /**
+ * A stream containing all data that has been written to the body of this
+ * response so far. (Async handlers make the data contained in this
+ * unreliable as a way of determining content length in general, but auxiliary
+ * saved information can sometimes be used to guarantee reliability.)
+ */
+ this._bodyInputStream = null;
+
+ /**
+ * A stream copier which copies data to the network. It is initially null
+ * until replaced with a copier for response headers; when headers have been
+ * fully sent it is replaced with a copier for the response body, remaining
+ * so for the duration of response processing.
+ */
+ this._asyncCopier = null;
+
+ /**
+ * True if this response has been designated as being processed
+ * asynchronously rather than for the duration of a single call to
+ * nsIHttpRequestHandler.handle.
+ */
+ this._processAsync = false;
+
+ /**
+ * True iff finish() has been called on this, signaling that no more changes
+ * to this may be made.
+ */
+ this._finished = false;
+
+ /**
+ * True iff powerSeized() has been called on this, signaling that this
+ * response is to be handled manually by the response handler (which may then
+ * send arbitrary data in response, even non-HTTP responses).
+ */
+ this._powerSeized = false;
+}
+Response.prototype = {
+ // PUBLIC CONSTRUCTION API
+
+ //
+ // see nsIHttpResponse.bodyOutputStream
+ //
+ get bodyOutputStream() {
+ if (this._finished) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ if (!this._bodyOutputStream) {
+ var pipe = new Pipe(
+ true,
+ false,
+ Response.SEGMENT_SIZE,
+ PR_UINT32_MAX,
+ null
+ );
+ this._bodyOutputStream = pipe.outputStream;
+ this._bodyInputStream = pipe.inputStream;
+ if (this._processAsync || this._powerSeized) {
+ this._startAsyncProcessor();
+ }
+ }
+
+ return this._bodyOutputStream;
+ },
+
+ //
+ // see nsIHttpResponse.write
+ //
+ write(data) {
+ if (this._finished) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ var dataAsString = String(data);
+ this.bodyOutputStream.write(dataAsString, dataAsString.length);
+ },
+
+ //
+ // see nsIHttpResponse.setStatusLine
+ //
+ setStatusLineInternal(httpVersion, code, description, informationalResponse) {
+ if (this._finished || this._powerSeized) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ if (!informationalResponse) {
+ if (!this._headers) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ } else if (!this._informationalResponseHeaders) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ this._ensureAlive();
+
+ if (!(code >= 0 && code < 1000)) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ try {
+ var httpVer;
+ // avoid version construction for the most common cases
+ if (!httpVersion || httpVersion == "1.1") {
+ httpVer = nsHttpVersion.HTTP_1_1;
+ } else if (httpVersion == "1.0") {
+ httpVer = nsHttpVersion.HTTP_1_0;
+ } else {
+ httpVer = new nsHttpVersion(httpVersion);
+ }
+ } catch (e) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // Reason-Phrase = *<TEXT, excluding CR, LF>
+ // TEXT = <any OCTET except CTLs, but including LWS>
+ //
+ // XXX this ends up disallowing octets which aren't Unicode, I think -- not
+ // much to do if description is IDL'd as string
+ if (!description) {
+ description = "";
+ }
+ for (var i = 0; i < description.length; i++) {
+ if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t") {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+
+ // set the values only after validation to preserve atomicity
+ if (!informationalResponse) {
+ this._httpDescription = description;
+ this._httpCode = code;
+ this._httpVersion = httpVer;
+ } else {
+ this._informationalResponseSet = true;
+ this._informationalResponseHttpDescription = description;
+ this._informationalResponseHttpCode = code;
+ this._informationalResponseHttpVersion = httpVer;
+ }
+ },
+
+ //
+ // see nsIHttpResponse.setStatusLine
+ //
+ setStatusLine(httpVersion, code, description) {
+ this.setStatusLineInternal(httpVersion, code, description, false);
+ },
+
+ setInformationalResponseStatusLine(httpVersion, code, description) {
+ this.setStatusLineInternal(httpVersion, code, description, true);
+ },
+
+ //
+ // see nsIHttpResponse.setHeader
+ //
+ setHeader(name, value, merge) {
+ if (!this._headers || this._finished || this._powerSeized) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ this._ensureAlive();
+
+ this._headers.setHeader(name, value, merge);
+ },
+
+ setInformationalResponseHeader(name, value, merge) {
+ if (
+ !this._informationalResponseHeaders ||
+ this._finished ||
+ this._powerSeized
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ this._ensureAlive();
+
+ this._informationalResponseHeaders.setHeader(name, value, merge);
+ },
+
+ setHeaderNoCheck(name, value) {
+ if (!this._headers || this._finished || this._powerSeized) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ this._ensureAlive();
+
+ this._headers.setHeaderNoCheck(name, value);
+ },
+
+ setInformationalHeaderNoCheck(name, value) {
+ if (!this._headers || this._finished || this._powerSeized) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ this._ensureAlive();
+
+ this._informationalResponseHeaders.setHeaderNoCheck(name, value);
+ },
+
+ //
+ // see nsIHttpResponse.processAsync
+ //
+ processAsync() {
+ if (this._finished) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+ if (this._powerSeized) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ if (this._processAsync) {
+ return;
+ }
+ this._ensureAlive();
+
+ dumpn("*** processing connection " + this._connection.number + " async");
+ this._processAsync = true;
+
+ /*
+ * Either the bodyOutputStream getter or this method is responsible for
+ * starting the asynchronous processor and catching writes of data to the
+ * response body of async responses as they happen, for the purpose of
+ * forwarding those writes to the actual connection's output stream.
+ * If bodyOutputStream is accessed first, calling this method will create
+ * the processor (when it first is clear that body data is to be written
+ * immediately, not buffered). If this method is called first, accessing
+ * bodyOutputStream will create the processor. If only this method is
+ * called, we'll write nothing, neither headers nor the nonexistent body,
+ * until finish() is called. Since that delay is easily avoided by simply
+ * getting bodyOutputStream or calling write(""), we don't worry about it.
+ */
+ if (this._bodyOutputStream && !this._asyncCopier) {
+ this._startAsyncProcessor();
+ }
+ },
+
+ //
+ // see nsIHttpResponse.seizePower
+ //
+ seizePower() {
+ if (this._processAsync) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ if (this._finished) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+ if (this._powerSeized) {
+ return;
+ }
+ this._ensureAlive();
+
+ dumpn(
+ "*** forcefully seizing power over connection " +
+ this._connection.number +
+ "..."
+ );
+
+ // Purge any already-written data without sending it. We could as easily
+ // swap out the streams entirely, but that makes it possible to acquire and
+ // unknowingly use a stale reference, so we require there only be one of
+ // each stream ever for any response to avoid this complication.
+ if (this._asyncCopier) {
+ this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ this._asyncCopier = null;
+ if (this._bodyOutputStream) {
+ var input = new BinaryInputStream(this._bodyInputStream);
+ var avail;
+ while ((avail = input.available()) > 0) {
+ input.readByteArray(avail);
+ }
+ }
+
+ this._powerSeized = true;
+ if (this._bodyOutputStream) {
+ this._startAsyncProcessor();
+ }
+ },
+
+ //
+ // see nsIHttpResponse.finish
+ //
+ finish() {
+ if (!this._processAsync && !this._powerSeized) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+ if (this._finished) {
+ return;
+ }
+
+ dumpn("*** finishing connection " + this._connection.number);
+ this._startAsyncProcessor(); // in case bodyOutputStream was never accessed
+ if (this._bodyOutputStream) {
+ this._bodyOutputStream.close();
+ }
+ this._finished = true;
+ },
+
+ // NSISUPPORTS
+
+ //
+ // see nsISupports.QueryInterface
+ //
+ QueryInterface: ChromeUtils.generateQI(["nsIHttpResponse"]),
+
+ // POST-CONSTRUCTION API (not exposed externally)
+
+ /**
+ * The HTTP version number of this, as a string (e.g. "1.1").
+ */
+ get httpVersion() {
+ this._ensureAlive();
+ return this._httpVersion.toString();
+ },
+
+ /**
+ * The HTTP status code of this response, as a string of three characters per
+ * RFC 2616.
+ */
+ get httpCode() {
+ this._ensureAlive();
+
+ var codeString =
+ (this._httpCode < 10 ? "0" : "") +
+ (this._httpCode < 100 ? "0" : "") +
+ this._httpCode;
+ return codeString;
+ },
+
+ /**
+ * The description of the HTTP status code of this response, or "" if none is
+ * set.
+ */
+ get httpDescription() {
+ this._ensureAlive();
+
+ return this._httpDescription;
+ },
+
+ /**
+ * The headers in this response, as an nsHttpHeaders object.
+ */
+ get headers() {
+ this._ensureAlive();
+
+ return this._headers;
+ },
+
+ //
+ // see nsHttpHeaders.getHeader
+ //
+ getHeader(name) {
+ this._ensureAlive();
+
+ return this._headers.getHeader(name);
+ },
+
+ /**
+ * Determines whether this response may be abandoned in favor of a newly
+ * constructed response. A response may be abandoned only if it is not being
+ * sent asynchronously and if raw control over it has not been taken from the
+ * server.
+ *
+ * @returns boolean
+ * true iff no data has been written to the network
+ */
+ partiallySent() {
+ dumpn("*** partiallySent()");
+ return this._processAsync || this._powerSeized;
+ },
+
+ /**
+ * If necessary, kicks off the remaining request processing needed to be done
+ * after a request handler performs its initial work upon this response.
+ */
+ complete() {
+ dumpn("*** complete()");
+ if (this._processAsync || this._powerSeized) {
+ NS_ASSERT(
+ this._processAsync ^ this._powerSeized,
+ "can't both send async and relinquish power"
+ );
+ return;
+ }
+
+ NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?");
+
+ this._startAsyncProcessor();
+
+ // Now make sure we finish processing this request!
+ if (this._bodyOutputStream) {
+ this._bodyOutputStream.close();
+ }
+ },
+
+ /**
+ * Abruptly ends processing of this response, usually due to an error in an
+ * incoming request but potentially due to a bad error handler. Since we
+ * cannot handle the error in the usual way (giving an HTTP error page in
+ * response) because data may already have been sent (or because the response
+ * might be expected to have been generated asynchronously or completely from
+ * scratch by the handler), we stop processing this response and abruptly
+ * close the connection.
+ *
+ * @param e : Error
+ * the exception which precipitated this abort, or null if no such exception
+ * was generated
+ * @param truncateConnection : Boolean
+ * ensures that we truncate the connection using an RST packet, so the
+ * client testing code is aware that an error occurred, otherwise it may
+ * consider the response as valid.
+ */
+ abort(e, truncateConnection = false) {
+ dumpn("*** abort(<" + e + ">)");
+
+ if (truncateConnection) {
+ dumpn("*** truncate connection");
+ this._connection.transport.setLinger(true, 0);
+ }
+
+ // This response will be ended by the processor if one was created.
+ var copier = this._asyncCopier;
+ if (copier) {
+ // We dispatch asynchronously here so that any pending writes of data to
+ // the connection will be deterministically written. This makes it easier
+ // to specify exact behavior, and it makes observable behavior more
+ // predictable for clients. Note that the correctness of this depends on
+ // callbacks in response to _waitToReadData in WriteThroughCopier
+ // happening asynchronously with respect to the actual writing of data to
+ // bodyOutputStream, as they currently do; if they happened synchronously,
+ // an event which ran before this one could write more data to the
+ // response body before we get around to canceling the copier. We have
+ // tests for this in test_seizepower.js, however, and I can't think of a
+ // way to handle both cases without removing bodyOutputStream access and
+ // moving its effective write(data, length) method onto Response, which
+ // would be slower and require more code than this anyway.
+ Services.tm.currentThread.dispatch(
+ {
+ run() {
+ dumpn("*** canceling copy asynchronously...");
+ copier.cancel(Cr.NS_ERROR_UNEXPECTED);
+ },
+ },
+ Ci.nsIThread.DISPATCH_NORMAL
+ );
+ } else {
+ this.end();
+ }
+ },
+
+ /**
+ * Closes this response's network connection, marks the response as finished,
+ * and notifies the server handler that the request is done being processed.
+ */
+ end() {
+ NS_ASSERT(!this._ended, "ending this response twice?!?!");
+
+ this._connection.close();
+ if (this._bodyOutputStream) {
+ this._bodyOutputStream.close();
+ }
+
+ this._finished = true;
+ this._ended = true;
+ },
+
+ // PRIVATE IMPLEMENTATION
+
+ /**
+ * Sends the status line and headers of this response if they haven't been
+ * sent and initiates the process of copying data written to this response's
+ * body to the network.
+ */
+ _startAsyncProcessor() {
+ dumpn("*** _startAsyncProcessor()");
+
+ // Handle cases where we're being called a second time. The former case
+ // happens when this is triggered both by complete() and by processAsync(),
+ // while the latter happens when processAsync() in conjunction with sent
+ // data causes abort() to be called.
+ if (this._asyncCopier || this._ended) {
+ dumpn("*** ignoring second call to _startAsyncProcessor");
+ return;
+ }
+
+ // Send headers if they haven't been sent already and should be sent, then
+ // asynchronously continue to send the body.
+ if (this._headers && !this._powerSeized) {
+ this._sendHeaders();
+ return;
+ }
+
+ this._headers = null;
+ this._sendBody();
+ },
+
+ /**
+ * Signals that all modifications to the response status line and headers are
+ * complete and then sends that data over the network to the client. Once
+ * this method completes, a different response to the request that resulted
+ * in this response cannot be sent -- the only possible action in case of
+ * error is to abort the response and close the connection.
+ */
+ _sendHeaders() {
+ dumpn("*** _sendHeaders()");
+
+ NS_ASSERT(this._headers);
+ NS_ASSERT(this._informationalResponseHeaders);
+ NS_ASSERT(!this._powerSeized);
+
+ var preambleData = [];
+
+ // Informational response, e.g. 103
+ if (this._informationalResponseSet) {
+ // request-line
+ let statusLine =
+ "HTTP/" +
+ this._informationalResponseHttpVersion +
+ " " +
+ this._informationalResponseHttpCode +
+ " " +
+ this._informationalResponseHttpDescription +
+ "\r\n";
+ preambleData.push(statusLine);
+
+ // headers
+ let headEnum = this._informationalResponseHeaders.enumerator;
+ while (headEnum.hasMoreElements()) {
+ let fieldName = headEnum
+ .getNext()
+ .QueryInterface(Ci.nsISupportsString).data;
+ let values =
+ this._informationalResponseHeaders.getHeaderValues(fieldName);
+ for (let i = 0, sz = values.length; i < sz; i++) {
+ preambleData.push(fieldName + ": " + values[i] + "\r\n");
+ }
+ }
+ // end request-line/headers
+ preambleData.push("\r\n");
+ }
+
+ // request-line
+ var statusLine =
+ "HTTP/" +
+ this.httpVersion +
+ " " +
+ this.httpCode +
+ " " +
+ this.httpDescription +
+ "\r\n";
+
+ // header post-processing
+
+ var headers = this._headers;
+ headers.setHeader("Connection", "close", false);
+ headers.setHeader("Server", "httpd.js", false);
+ if (!headers.hasHeader("Date")) {
+ headers.setHeader("Date", toDateString(Date.now()), false);
+ }
+
+ // Any response not being processed asynchronously must have an associated
+ // Content-Length header for reasons of backwards compatibility with the
+ // initial server, which fully buffered every response before sending it.
+ // Beyond that, however, it's good to do this anyway because otherwise it's
+ // impossible to test behaviors that depend on the presence or absence of a
+ // Content-Length header.
+ if (!this._processAsync) {
+ dumpn("*** non-async response, set Content-Length");
+
+ var bodyStream = this._bodyInputStream;
+ var avail = bodyStream ? bodyStream.available() : 0;
+
+ // XXX assumes stream will always report the full amount of data available
+ headers.setHeader("Content-Length", "" + avail, false);
+ }
+
+ // construct and send response
+ dumpn("*** header post-processing completed, sending response head...");
+
+ // request-line
+ preambleData.push(statusLine);
+
+ // headers
+ var headEnum = headers.enumerator;
+ while (headEnum.hasMoreElements()) {
+ var fieldName = headEnum
+ .getNext()
+ .QueryInterface(Ci.nsISupportsString).data;
+ var values = headers.getHeaderValues(fieldName);
+ for (var i = 0, sz = values.length; i < sz; i++) {
+ preambleData.push(fieldName + ": " + values[i] + "\r\n");
+ }
+ }
+
+ // end request-line/headers
+ preambleData.push("\r\n");
+
+ var preamble = preambleData.join("");
+
+ var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null);
+ responseHeadPipe.outputStream.write(preamble, preamble.length);
+
+ var response = this;
+ var copyObserver = {
+ onStartRequest(request) {
+ dumpn("*** preamble copying started");
+ },
+
+ onStopRequest(request, statusCode) {
+ dumpn(
+ "*** preamble copying complete " +
+ "[status=0x" +
+ statusCode.toString(16) +
+ "]"
+ );
+
+ if (!Components.isSuccessCode(statusCode)) {
+ dumpn(
+ "!!! header copying problems: non-success statusCode, " +
+ "ending response"
+ );
+
+ response.end();
+ } else {
+ response._sendBody();
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]),
+ };
+
+ this._asyncCopier = new WriteThroughCopier(
+ responseHeadPipe.inputStream,
+ this._connection.output,
+ copyObserver,
+ null
+ );
+
+ responseHeadPipe.outputStream.close();
+
+ // Forbid setting any more headers or modifying the request line.
+ this._headers = null;
+ },
+
+ /**
+ * Asynchronously writes the body of the response (or the entire response, if
+ * seizePower() has been called) to the network.
+ */
+ _sendBody() {
+ dumpn("*** _sendBody");
+
+ NS_ASSERT(!this._headers, "still have headers around but sending body?");
+
+ // If no body data was written, we're done
+ if (!this._bodyInputStream) {
+ dumpn("*** empty body, response finished");
+ this.end();
+ return;
+ }
+
+ var response = this;
+ var copyObserver = {
+ onStartRequest(request) {
+ dumpn("*** onStartRequest");
+ },
+
+ onStopRequest(request, statusCode) {
+ dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]");
+
+ if (statusCode === Cr.NS_BINDING_ABORTED) {
+ dumpn("*** terminating copy observer without ending the response");
+ } else {
+ if (!Components.isSuccessCode(statusCode)) {
+ dumpn("*** WARNING: non-success statusCode in onStopRequest");
+ }
+
+ response.end();
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]),
+ };
+
+ dumpn("*** starting async copier of body data...");
+ this._asyncCopier = new WriteThroughCopier(
+ this._bodyInputStream,
+ this._connection.output,
+ copyObserver,
+ null
+ );
+ },
+
+ /** Ensures that this hasn't been ended. */
+ _ensureAlive() {
+ NS_ASSERT(!this._ended, "not handling response lifetime correctly");
+ },
+};
+
+/**
+ * Size of the segments in the buffer used in storing response data and writing
+ * it to the socket.
+ */
+Response.SEGMENT_SIZE = 8192;
+
+/** Serves double duty in WriteThroughCopier implementation. */
+function notImplemented() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+}
+
+/** Returns true iff the given exception represents stream closure. */
+function streamClosed(e) {
+ return (
+ e === Cr.NS_BASE_STREAM_CLOSED ||
+ (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED)
+ );
+}
+
+/** Returns true iff the given exception represents a blocked stream. */
+function wouldBlock(e) {
+ return (
+ e === Cr.NS_BASE_STREAM_WOULD_BLOCK ||
+ (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK)
+ );
+}
+
+/**
+ * Copies data from source to sink as it becomes available, when that data can
+ * be written to sink without blocking.
+ *
+ * @param source : nsIAsyncInputStream
+ * the stream from which data is to be read
+ * @param sink : nsIAsyncOutputStream
+ * the stream to which data is to be copied
+ * @param observer : nsIRequestObserver
+ * an observer which will be notified when the copy starts and finishes
+ * @param context : nsISupports
+ * context passed to observer when notified of start/stop
+ * @throws NS_ERROR_NULL_POINTER
+ * if source, sink, or observer are null
+ */
+export function WriteThroughCopier(source, sink, observer, context) {
+ if (!source || !sink || !observer) {
+ throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
+ }
+
+ /** Stream from which data is being read. */
+ this._source = source;
+
+ /** Stream to which data is being written. */
+ this._sink = sink;
+
+ /** Observer watching this copy. */
+ this._observer = observer;
+
+ /** Context for the observer watching this. */
+ this._context = context;
+
+ /**
+ * True iff this is currently being canceled (cancel has been called, the
+ * callback may not yet have been made).
+ */
+ this._canceled = false;
+
+ /**
+ * False until all data has been read from input and written to output, at
+ * which point this copy is completed and cancel() is asynchronously called.
+ */
+ this._completed = false;
+
+ /** Required by nsIRequest, meaningless. */
+ this.loadFlags = 0;
+ /** Required by nsIRequest, meaningless. */
+ this.loadGroup = null;
+ /** Required by nsIRequest, meaningless. */
+ this.name = "response-body-copy";
+
+ /** Status of this request. */
+ this.status = Cr.NS_OK;
+
+ /** Arrays of byte strings waiting to be written to output. */
+ this._pendingData = [];
+
+ // start copying
+ try {
+ observer.onStartRequest(this);
+ this._waitToReadData();
+ this._waitForSinkClosure();
+ } catch (e) {
+ dumpn(
+ "!!! error starting copy: " +
+ e +
+ ("lineNumber" in e ? ", line " + e.lineNumber : "")
+ );
+ dumpn(e.stack);
+ this.cancel(Cr.NS_ERROR_UNEXPECTED);
+ }
+}
+WriteThroughCopier.prototype = {
+ /* nsISupports implementation */
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInputStreamCallback",
+ "nsIOutputStreamCallback",
+ "nsIRequest",
+ ]),
+
+ // NSIINPUTSTREAMCALLBACK
+
+ /**
+ * Receives a more-data-in-input notification and writes the corresponding
+ * data to the output.
+ *
+ * @param input : nsIAsyncInputStream
+ * the input stream on whose data we have been waiting
+ */
+ onInputStreamReady(input) {
+ if (this._source === null) {
+ return;
+ }
+
+ dumpn("*** onInputStreamReady");
+
+ //
+ // Ordinarily we'll read a non-zero amount of data from input, queue it up
+ // to be written and then wait for further callbacks. The complications in
+ // this method are the cases where we deviate from that behavior when errors
+ // occur or when copying is drawing to a finish.
+ //
+ // The edge cases when reading data are:
+ //
+ // Zero data is read
+ // If zero data was read, we're at the end of available data, so we can
+ // should stop reading and move on to writing out what we have (or, if
+ // we've already done that, onto notifying of completion).
+ // A stream-closed exception is thrown
+ // This is effectively a less kind version of zero data being read; the
+ // only difference is that we notify of completion with that result
+ // rather than with NS_OK.
+ // Some other exception is thrown
+ // This is the least kind result. We don't know what happened, so we
+ // act as though the stream closed except that we notify of completion
+ // with the result NS_ERROR_UNEXPECTED.
+ //
+
+ var bytesWanted = 0,
+ bytesConsumed = -1;
+ try {
+ input = new BinaryInputStream(input);
+
+ bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE);
+ dumpn("*** input wanted: " + bytesWanted);
+
+ if (bytesWanted > 0) {
+ var data = input.readByteArray(bytesWanted);
+ bytesConsumed = data.length;
+ this._pendingData.push(String.fromCharCode.apply(String, data));
+ }
+
+ dumpn("*** " + bytesConsumed + " bytes read");
+
+ // Handle the zero-data edge case in the same place as all other edge
+ // cases are handled.
+ if (bytesWanted === 0) {
+ throw Components.Exception("", Cr.NS_BASE_STREAM_CLOSED);
+ }
+ } catch (e) {
+ let rv;
+ if (streamClosed(e)) {
+ dumpn("*** input stream closed");
+ rv = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED;
+ } else {
+ dumpn("!!! unexpected error reading from input, canceling: " + e);
+ rv = Cr.NS_ERROR_UNEXPECTED;
+ }
+
+ this._doneReadingSource(rv);
+ return;
+ }
+
+ var pendingData = this._pendingData;
+
+ NS_ASSERT(bytesConsumed > 0);
+ NS_ASSERT(!!pendingData.length, "no pending data somehow?");
+ NS_ASSERT(
+ !!pendingData[pendingData.length - 1].length,
+ "buffered zero bytes of data?"
+ );
+
+ NS_ASSERT(this._source !== null);
+
+ // Reading has gone great, and we've gotten data to write now. What if we
+ // don't have a place to write that data, because output went away just
+ // before this read? Drop everything on the floor, including new data, and
+ // cancel at this point.
+ if (this._sink === null) {
+ pendingData.length = 0;
+ this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ // Okay, we've read the data, and we know we have a place to write it. We
+ // need to queue up the data to be written, but *only* if none is queued
+ // already -- if data's already queued, the code that actually writes the
+ // data will make sure to wait on unconsumed pending data.
+ try {
+ if (pendingData.length === 1) {
+ this._waitToWriteData();
+ }
+ } catch (e) {
+ dumpn(
+ "!!! error waiting to write data just read, swallowing and " +
+ "writing only what we already have: " +
+ e
+ );
+ this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ // Whee! We successfully read some data, and it's successfully queued up to
+ // be written. All that remains now is to wait for more data to read.
+ try {
+ this._waitToReadData();
+ } catch (e) {
+ dumpn("!!! error waiting to read more data: " + e);
+ this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED);
+ }
+ },
+
+ // NSIOUTPUTSTREAMCALLBACK
+
+ /**
+ * Callback when data may be written to the output stream without blocking, or
+ * when the output stream has been closed.
+ *
+ * @param output : nsIAsyncOutputStream
+ * the output stream on whose writability we've been waiting, also known as
+ * this._sink
+ */
+ onOutputStreamReady(output) {
+ if (this._sink === null) {
+ return;
+ }
+
+ dumpn("*** onOutputStreamReady");
+
+ var pendingData = this._pendingData;
+ if (pendingData.length === 0) {
+ // There's no pending data to write. The only way this can happen is if
+ // we're waiting on the output stream's closure, so we can respond to a
+ // copying failure as quickly as possible (rather than waiting for data to
+ // be available to read and then fail to be copied). Therefore, we must
+ // be done now -- don't bother to attempt to write anything and wrap
+ // things up.
+ dumpn("!!! output stream closed prematurely, ending copy");
+
+ this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ NS_ASSERT(!!pendingData[0].length, "queued up an empty quantum?");
+
+ //
+ // Write out the first pending quantum of data. The possible errors here
+ // are:
+ //
+ // The write might fail because we can't write that much data
+ // Okay, we've written what we can now, so re-queue what's left and
+ // finish writing it out later.
+ // The write failed because the stream was closed
+ // Discard pending data that we can no longer write, stop reading, and
+ // signal that copying finished.
+ // Some other error occurred.
+ // Same as if the stream were closed, but notify with the status
+ // NS_ERROR_UNEXPECTED so the observer knows something was wonky.
+ //
+
+ try {
+ var quantum = pendingData[0];
+
+ // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on
+ // undefined behavior! We're only using this because writeByteArray
+ // is unusably broken for asynchronous output streams; see bug 532834
+ // for details.
+ var bytesWritten = output.write(quantum, quantum.length);
+ if (bytesWritten === quantum.length) {
+ pendingData.shift();
+ } else {
+ pendingData[0] = quantum.substring(bytesWritten);
+ }
+
+ dumpn("*** wrote " + bytesWritten + " bytes of data");
+ } catch (e) {
+ if (wouldBlock(e)) {
+ NS_ASSERT(
+ !!pendingData.length,
+ "stream-blocking exception with no data to write?"
+ );
+ NS_ASSERT(
+ !!pendingData[0].length,
+ "stream-blocking exception with empty quantum?"
+ );
+ this._waitToWriteData();
+ return;
+ }
+
+ if (streamClosed(e)) {
+ dumpn("!!! output stream prematurely closed, signaling error...");
+ } else {
+ dumpn("!!! unknown error: " + e + ", quantum=" + quantum);
+ }
+
+ this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ // The day is ours! Quantum written, now let's see if we have more data
+ // still to write.
+ try {
+ if (pendingData.length) {
+ this._waitToWriteData();
+ return;
+ }
+ } catch (e) {
+ dumpn("!!! unexpected error waiting to write pending data: " + e);
+ this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ // Okay, we have no more pending data to write -- but might we get more in
+ // the future?
+ if (this._source !== null) {
+ /*
+ * If we might, then wait for the output stream to be closed. (We wait
+ * only for closure because we have no data to write -- and if we waited
+ * for a specific amount of data, we would get repeatedly notified for no
+ * reason if over time the output stream permitted more and more data to
+ * be written to it without blocking.)
+ */
+ this._waitForSinkClosure();
+ } else {
+ /*
+ * On the other hand, if we can't have more data because the input
+ * stream's gone away, then it's time to notify of copy completion.
+ * Victory!
+ */
+ this._sink = null;
+ this._cancelOrDispatchCancelCallback(Cr.NS_OK);
+ }
+ },
+
+ // NSIREQUEST
+
+ /** Returns true if the cancel observer hasn't been notified yet. */
+ isPending() {
+ return !this._completed;
+ },
+
+ /** Not implemented, don't use! */
+ suspend: notImplemented,
+ /** Not implemented, don't use! */
+ resume: notImplemented,
+
+ /**
+ * Cancels data reading from input, asynchronously writes out any pending
+ * data, and causes the observer to be notified with the given error code when
+ * all writing has finished.
+ *
+ * @param status : nsresult
+ * the status to pass to the observer when data copying has been canceled
+ */
+ cancel(status) {
+ dumpn("*** cancel(" + status.toString(16) + ")");
+
+ if (this._canceled) {
+ dumpn("*** suppressing a late cancel");
+ return;
+ }
+
+ this._canceled = true;
+ this.status = status;
+
+ // We could be in the middle of absolutely anything at this point. Both
+ // input and output might still be around, we might have pending data to
+ // write, and in general we know nothing about the state of the world. We
+ // therefore must assume everything's in progress and take everything to its
+ // final steady state (or so far as it can go before we need to finish
+ // writing out remaining data).
+
+ this._doneReadingSource(status);
+ },
+
+ // PRIVATE IMPLEMENTATION
+
+ /**
+ * Stop reading input if we haven't already done so, passing e as the status
+ * when closing the stream, and kick off a copy-completion notice if no more
+ * data remains to be written.
+ *
+ * @param e : nsresult
+ * the status to be used when closing the input stream
+ */
+ _doneReadingSource(e) {
+ dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")");
+
+ this._finishSource(e);
+ if (this._pendingData.length === 0) {
+ this._sink = null;
+ } else {
+ NS_ASSERT(this._sink !== null, "null output?");
+ }
+
+ // If we've written out all data read up to this point, then it's time to
+ // signal completion.
+ if (this._sink === null) {
+ NS_ASSERT(this._pendingData.length === 0, "pending data still?");
+ this._cancelOrDispatchCancelCallback(e);
+ }
+ },
+
+ /**
+ * Stop writing output if we haven't already done so, discard any data that
+ * remained to be sent, close off input if it wasn't already closed, and kick
+ * off a copy-completion notice.
+ *
+ * @param e : nsresult
+ * the status to be used when closing input if it wasn't already closed
+ */
+ _doneWritingToSink(e) {
+ dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")");
+
+ this._pendingData.length = 0;
+ this._sink = null;
+ this._doneReadingSource(e);
+ },
+
+ /**
+ * Completes processing of this copy: either by canceling the copy if it
+ * hasn't already been canceled using the provided status, or by dispatching
+ * the cancel callback event (with the originally provided status, of course)
+ * if it already has been canceled.
+ *
+ * @param status : nsresult
+ * the status code to use to cancel this, if this hasn't already been
+ * canceled
+ */
+ _cancelOrDispatchCancelCallback(status) {
+ dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")");
+
+ NS_ASSERT(this._source === null, "should have finished input");
+ NS_ASSERT(this._sink === null, "should have finished output");
+ NS_ASSERT(this._pendingData.length === 0, "should have no pending data");
+
+ if (!this._canceled) {
+ this.cancel(status);
+ return;
+ }
+
+ var self = this;
+ var event = {
+ run() {
+ dumpn("*** onStopRequest async callback");
+
+ self._completed = true;
+ try {
+ self._observer.onStopRequest(self, self.status);
+ } catch (e) {
+ NS_ASSERT(
+ false,
+ "how are we throwing an exception here? we control " +
+ "all the callers! " +
+ e
+ );
+ }
+ },
+ };
+
+ Services.tm.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ /**
+ * Kicks off another wait for more data to be available from the input stream.
+ */
+ _waitToReadData() {
+ dumpn("*** _waitToReadData");
+ this._source.asyncWait(
+ this,
+ 0,
+ Response.SEGMENT_SIZE,
+ Services.tm.mainThread
+ );
+ },
+
+ /**
+ * Kicks off another wait until data can be written to the output stream.
+ */
+ _waitToWriteData() {
+ dumpn("*** _waitToWriteData");
+
+ var pendingData = this._pendingData;
+ NS_ASSERT(!!pendingData.length, "no pending data to write?");
+ NS_ASSERT(!!pendingData[0].length, "buffered an empty write?");
+
+ this._sink.asyncWait(
+ this,
+ 0,
+ pendingData[0].length,
+ Services.tm.mainThread
+ );
+ },
+
+ /**
+ * Kicks off a wait for the sink to which data is being copied to be closed.
+ * We wait for stream closure when we don't have any data to be copied, rather
+ * than waiting to write a specific amount of data. We can't wait to write
+ * data because the sink might be infinitely writable, and if no data appears
+ * in the source for a long time we might have to spin quite a bit waiting to
+ * write, waiting to write again, &c. Waiting on stream closure instead means
+ * we'll get just one notification if the sink dies. Note that when data
+ * starts arriving from the sink we'll resume waiting for data to be written,
+ * dropping this closure-only callback entirely.
+ */
+ _waitForSinkClosure() {
+ dumpn("*** _waitForSinkClosure");
+
+ this._sink.asyncWait(
+ this,
+ Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY,
+ 0,
+ Services.tm.mainThread
+ );
+ },
+
+ /**
+ * Closes input with the given status, if it hasn't already been closed;
+ * otherwise a no-op.
+ *
+ * @param status : nsresult
+ * status code use to close the source stream if necessary
+ */
+ _finishSource(status) {
+ dumpn("*** _finishSource(" + status.toString(16) + ")");
+
+ if (this._source !== null) {
+ this._source.closeWithStatus(status);
+ this._source = null;
+ }
+ },
+};
+
+/**
+ * A container for utility functions used with HTTP headers.
+ */
+const headerUtils = {
+ /**
+ * Normalizes fieldName (by converting it to lowercase) and ensures it is a
+ * valid header field name (although not necessarily one specified in RFC
+ * 2616).
+ *
+ * @throws NS_ERROR_INVALID_ARG
+ * if fieldName does not match the field-name production in RFC 2616
+ * @returns string
+ * fieldName converted to lowercase if it is a valid header, for characters
+ * where case conversion is possible
+ */
+ normalizeFieldName(fieldName) {
+ if (fieldName == "") {
+ dumpn("*** Empty fieldName");
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ for (var i = 0, sz = fieldName.length; i < sz; i++) {
+ if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)]) {
+ dumpn(fieldName + " is not a valid header field name!");
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+
+ return fieldName.toLowerCase();
+ },
+
+ /**
+ * Ensures that fieldValue is a valid header field value (although not
+ * necessarily as specified in RFC 2616 if the corresponding field name is
+ * part of the HTTP protocol), normalizes the value if it is, and
+ * returns the normalized value.
+ *
+ * @param fieldValue : string
+ * a value to be normalized as an HTTP header field value
+ * @throws NS_ERROR_INVALID_ARG
+ * if fieldValue does not match the field-value production in RFC 2616
+ * @returns string
+ * fieldValue as a normalized HTTP header field value
+ */
+ normalizeFieldValue(fieldValue) {
+ // field-value = *( field-content | LWS )
+ // field-content = <the OCTETs making up the field-value
+ // and consisting of either *TEXT or combinations
+ // of token, separators, and quoted-string>
+ // TEXT = <any OCTET except CTLs,
+ // but including LWS>
+ // LWS = [CRLF] 1*( SP | HT )
+ //
+ // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
+ // qdtext = <any TEXT except <">>
+ // quoted-pair = "\" CHAR
+ // CHAR = <any US-ASCII character (octets 0 - 127)>
+
+ // Any LWS that occurs between field-content MAY be replaced with a single
+ // SP before interpreting the field value or forwarding the message
+ // downstream (section 4.2); we replace 1*LWS with a single SP
+ var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " ");
+
+ // remove leading/trailing LWS (which has been converted to SP)
+ val = val.replace(/^ +/, "").replace(/ +$/, "");
+
+ // that should have taken care of all CTLs, so val should contain no CTLs
+ dumpn("*** Normalized value: '" + val + "'");
+ for (var i = 0, len = val.length; i < len; i++) {
+ if (isCTL(val.charCodeAt(i))) {
+ dump("*** Char " + i + " has charcode " + val.charCodeAt(i));
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+
+ // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly
+ // normalize, however, so this can be construed as a tightening of the
+ // spec and not entirely as a bug
+ return val;
+ },
+};
+
+/**
+ * Converts the given string into a string which is safe for use in an HTML
+ * context.
+ *
+ * @param str : string
+ * the string to make HTML-safe
+ * @returns string
+ * an HTML-safe version of str
+ */
+function htmlEscape(str) {
+ // this is naive, but it'll work
+ var s = "";
+ for (var i = 0; i < str.length; i++) {
+ s += "&#" + str.charCodeAt(i) + ";";
+ }
+ return s;
+}
+
+/**
+ * Constructs an object representing an HTTP version (see section 3.1).
+ *
+ * @param versionString
+ * a string of the form "#.#", where # is an non-negative decimal integer with
+ * or without leading zeros
+ * @throws
+ * if versionString does not specify a valid HTTP version number
+ */
+function nsHttpVersion(versionString) {
+ var matches = /^(\d+)\.(\d+)$/.exec(versionString);
+ if (!matches) {
+ throw new Error("Not a valid HTTP version!");
+ }
+
+ /** The major version number of this, as a number. */
+ this.major = parseInt(matches[1], 10);
+
+ /** The minor version number of this, as a number. */
+ this.minor = parseInt(matches[2], 10);
+
+ if (
+ isNaN(this.major) ||
+ isNaN(this.minor) ||
+ this.major < 0 ||
+ this.minor < 0
+ ) {
+ throw new Error("Not a valid HTTP version!");
+ }
+}
+nsHttpVersion.prototype = {
+ /**
+ * Returns the standard string representation of the HTTP version represented
+ * by this (e.g., "1.1").
+ */
+ toString() {
+ return this.major + "." + this.minor;
+ },
+
+ /**
+ * Returns true if this represents the same HTTP version as otherVersion,
+ * false otherwise.
+ *
+ * @param otherVersion : nsHttpVersion
+ * the version to compare against this
+ */
+ equals(otherVersion) {
+ return this.major == otherVersion.major && this.minor == otherVersion.minor;
+ },
+
+ /** True if this >= otherVersion, false otherwise. */
+ atLeast(otherVersion) {
+ return (
+ this.major > otherVersion.major ||
+ (this.major == otherVersion.major && this.minor >= otherVersion.minor)
+ );
+ },
+};
+
+nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0");
+nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1");
+
+/**
+ * An object which stores HTTP headers for a request or response.
+ *
+ * Note that since headers are case-insensitive, this object converts headers to
+ * lowercase before storing them. This allows the getHeader and hasHeader
+ * methods to work correctly for any case of a header, but it means that the
+ * values returned by .enumerator may not be equal case-sensitively to the
+ * values passed to setHeader when adding headers to this.
+ */
+export function nsHttpHeaders() {
+ /**
+ * A hash of headers, with header field names as the keys and header field
+ * values as the values. Header field names are case-insensitive, but upon
+ * insertion here they are converted to lowercase. Header field values are
+ * normalized upon insertion to contain no leading or trailing whitespace.
+ *
+ * Note also that per RFC 2616, section 4.2, two headers with the same name in
+ * a message may be treated as one header with the same field name and a field
+ * value consisting of the separate field values joined together with a "," in
+ * their original order. This hash stores multiple headers with the same name
+ * in this manner.
+ */
+ this._headers = {};
+}
+nsHttpHeaders.prototype = {
+ /**
+ * Sets the header represented by name and value in this.
+ *
+ * @param name : string
+ * the header name
+ * @param value : string
+ * the header value
+ * @throws NS_ERROR_INVALID_ARG
+ * if name or value is not a valid header component
+ */
+ setHeader(fieldName, fieldValue, merge) {
+ var name = headerUtils.normalizeFieldName(fieldName);
+ var value = headerUtils.normalizeFieldValue(fieldValue);
+
+ // The following three headers are stored as arrays because their real-world
+ // syntax prevents joining individual headers into a single header using
+ // ",". See also <https://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77>
+ if (merge && name in this._headers) {
+ if (
+ name === "www-authenticate" ||
+ name === "proxy-authenticate" ||
+ name === "set-cookie"
+ ) {
+ this._headers[name].push(value);
+ } else {
+ this._headers[name][0] += "," + value;
+ NS_ASSERT(
+ this._headers[name].length === 1,
+ "how'd a non-special header have multiple values?"
+ );
+ }
+ } else {
+ this._headers[name] = [value];
+ }
+ },
+
+ setHeaderNoCheck(fieldName, fieldValue) {
+ var name = headerUtils.normalizeFieldName(fieldName);
+ var value = headerUtils.normalizeFieldValue(fieldValue);
+ if (name in this._headers) {
+ this._headers[name].push(value);
+ } else {
+ this._headers[name] = [value];
+ }
+ },
+
+ /**
+ * Returns the value for the header specified by this.
+ *
+ * @throws NS_ERROR_INVALID_ARG
+ * if fieldName does not constitute a valid header field name
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * if the given header does not exist in this
+ * @returns string
+ * the field value for the given header, possibly with non-semantic changes
+ * (i.e., leading/trailing whitespace stripped, whitespace runs replaced
+ * with spaces, etc.) at the option of the implementation; multiple
+ * instances of the header will be combined with a comma, except for
+ * the three headers noted in the description of getHeaderValues
+ */
+ getHeader(fieldName) {
+ return this.getHeaderValues(fieldName).join("\n");
+ },
+
+ /**
+ * Returns the value for the header specified by fieldName as an array.
+ *
+ * @throws NS_ERROR_INVALID_ARG
+ * if fieldName does not constitute a valid header field name
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * if the given header does not exist in this
+ * @returns [string]
+ * an array of all the header values in this for the given
+ * header name. Header values will generally be collapsed
+ * into a single header by joining all header values together
+ * with commas, but certain headers (Proxy-Authenticate,
+ * WWW-Authenticate, and Set-Cookie) violate the HTTP spec
+ * and cannot be collapsed in this manner. For these headers
+ * only, the returned array may contain multiple elements if
+ * that header has been added more than once.
+ */
+ getHeaderValues(fieldName) {
+ var name = headerUtils.normalizeFieldName(fieldName);
+
+ if (name in this._headers) {
+ return this._headers[name];
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ },
+
+ /**
+ * Returns true if a header with the given field name exists in this, false
+ * otherwise.
+ *
+ * @param fieldName : string
+ * the field name whose existence is to be determined in this
+ * @throws NS_ERROR_INVALID_ARG
+ * if fieldName does not constitute a valid header field name
+ * @returns boolean
+ * true if the header's present, false otherwise
+ */
+ hasHeader(fieldName) {
+ var name = headerUtils.normalizeFieldName(fieldName);
+ return name in this._headers;
+ },
+
+ /**
+ * Returns a new enumerator over the field names of the headers in this, as
+ * nsISupportsStrings. The names returned will be in lowercase, regardless of
+ * how they were input using setHeader (header names are case-insensitive per
+ * RFC 2616).
+ */
+ get enumerator() {
+ var headers = [];
+ for (var i in this._headers) {
+ var supports = new SupportsString();
+ supports.data = i;
+ headers.push(supports);
+ }
+
+ return new nsSimpleEnumerator(headers);
+ },
+};
+
+/**
+ * Constructs an nsISimpleEnumerator for the given array of items.
+ *
+ * @param items : Array
+ * the items, which must all implement nsISupports
+ */
+function nsSimpleEnumerator(items) {
+ this._items = items;
+ this._nextIndex = 0;
+}
+nsSimpleEnumerator.prototype = {
+ hasMoreElements() {
+ return this._nextIndex < this._items.length;
+ },
+ getNext() {
+ if (!this.hasMoreElements()) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ return this._items[this._nextIndex++];
+ },
+ [Symbol.iterator]() {
+ return this._items.values();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsISimpleEnumerator"]),
+};
+
+/**
+ * A representation of the data in an HTTP request.
+ *
+ * @param port : uint
+ * the port on which the server receiving this request runs
+ */
+function Request(port) {
+ /** Method of this request, e.g. GET or POST. */
+ this._method = "";
+
+ /** Path of the requested resource; empty paths are converted to '/'. */
+ this._path = "";
+
+ /** Query string, if any, associated with this request (not including '?'). */
+ this._queryString = "";
+
+ /** Scheme of requested resource, usually http, always lowercase. */
+ this._scheme = "http";
+
+ /** Hostname on which the requested resource resides. */
+ this._host = undefined;
+
+ /** Port number over which the request was received. */
+ this._port = port;
+
+ var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null);
+
+ /** Stream from which data in this request's body may be read. */
+ this._bodyInputStream = bodyPipe.inputStream;
+
+ /** Stream to which data in this request's body is written. */
+ this._bodyOutputStream = bodyPipe.outputStream;
+
+ /**
+ * The headers in this request.
+ */
+ this._headers = new nsHttpHeaders();
+
+ /**
+ * For the addition of ad-hoc properties and new functionality without having
+ * to change nsIHttpRequest every time; currently lazily created, as its only
+ * use is in directory listings.
+ */
+ this._bag = null;
+}
+Request.prototype = {
+ // SERVER METADATA
+
+ //
+ // see nsIHttpRequest.scheme
+ //
+ get scheme() {
+ return this._scheme;
+ },
+
+ //
+ // see nsIHttpRequest.host
+ //
+ get host() {
+ return this._host;
+ },
+
+ //
+ // see nsIHttpRequest.port
+ //
+ get port() {
+ return this._port;
+ },
+
+ // REQUEST LINE
+
+ //
+ // see nsIHttpRequest.method
+ //
+ get method() {
+ return this._method;
+ },
+
+ //
+ // see nsIHttpRequest.httpVersion
+ //
+ get httpVersion() {
+ return this._httpVersion.toString();
+ },
+
+ //
+ // see nsIHttpRequest.path
+ //
+ get path() {
+ return this._path;
+ },
+
+ //
+ // see nsIHttpRequest.queryString
+ //
+ get queryString() {
+ return this._queryString;
+ },
+
+ // HEADERS
+
+ //
+ // see nsIHttpRequest.getHeader
+ //
+ getHeader(name) {
+ return this._headers.getHeader(name);
+ },
+
+ //
+ // see nsIHttpRequest.hasHeader
+ //
+ hasHeader(name) {
+ return this._headers.hasHeader(name);
+ },
+
+ //
+ // see nsIHttpRequest.headers
+ //
+ get headers() {
+ return this._headers.enumerator;
+ },
+
+ //
+ // see nsIPropertyBag.enumerator
+ //
+ get enumerator() {
+ this._ensurePropertyBag();
+ return this._bag.enumerator;
+ },
+
+ //
+ // see nsIHttpRequest.headers
+ //
+ get bodyInputStream() {
+ return this._bodyInputStream;
+ },
+
+ //
+ // see nsIPropertyBag.getProperty
+ //
+ getProperty(name) {
+ this._ensurePropertyBag();
+ return this._bag.getProperty(name);
+ },
+
+ // NSISUPPORTS
+
+ //
+ // see nsISupports.QueryInterface
+ //
+ QueryInterface: ChromeUtils.generateQI(["nsIHttpRequest"]),
+
+ // PRIVATE IMPLEMENTATION
+
+ /** Ensures a property bag has been created for ad-hoc behaviors. */
+ _ensurePropertyBag() {
+ if (!this._bag) {
+ this._bag = new WritablePropertyBag();
+ }
+ },
+};
diff --git a/netwerk/test/httpserver/moz.build b/netwerk/test/httpserver/moz.build
new file mode 100644
index 0000000000..f4c978b160
--- /dev/null
+++ b/netwerk/test/httpserver/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsIHttpServer.idl",
+]
+
+XPIDL_MODULE = "test_necko"
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell.toml"]
+
+EXTRA_COMPONENTS += [
+ "httpd.sys.mjs",
+]
+
+TESTING_JS_MODULES += [
+ "httpd.sys.mjs",
+]
diff --git a/netwerk/test/httpserver/nsIHttpServer.idl b/netwerk/test/httpserver/nsIHttpServer.idl
new file mode 100644
index 0000000000..83614dbdb0
--- /dev/null
+++ b/netwerk/test/httpserver/nsIHttpServer.idl
@@ -0,0 +1,649 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIInputStream;
+interface nsIFile;
+interface nsIOutputStream;
+interface nsISimpleEnumerator;
+
+interface nsIHttpServer;
+interface nsIHttpServerStoppedCallback;
+interface nsIHttpRequestHandler;
+interface nsIHttpRequest;
+interface nsIHttpResponse;
+interface nsIHttpServerIdentity;
+
+/**
+ * An interface which represents an HTTP server.
+ */
+[scriptable, uuid(cea8812e-faa6-4013-9396-f9936cbb74ec)]
+interface nsIHttpServer : nsISupports
+{
+ /**
+ * Starts up this server, listening upon the given port.
+ *
+ * @param port
+ * the port upon which listening should happen, or -1 if no specific port is
+ * desired
+ * @throws NS_ERROR_ALREADY_INITIALIZED
+ * if this server is already started
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * if the server is not started and cannot be started on the desired port
+ * (perhaps because the port is already in use or because the process does
+ * not have privileges to do so)
+ * @note
+ * Behavior is undefined if this method is called after stop() has been
+ * called on this but before the provided callback function has been
+ * called.
+ */
+ void start(in long port);
+
+ /**
+ * Starts up this server, listening upon the given port on a ipv6 adddress.
+ *
+ * @param port
+ * the port upon which listening should happen, or -1 if no specific port is
+ * desired
+ * @throws NS_ERROR_ALREADY_INITIALIZED
+ * if this server is already started
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * if the server is not started and cannot be started on the desired port
+ * (perhaps because the port is already in use or because the process does
+ * not have privileges to do so)
+ * @note
+ * Behavior is undefined if this method is called after stop() has been
+ * called on this but before the provided callback function has been
+ * called.
+ */
+ void start_ipv6(in long port);
+
+ /**
+ * Like the two functions above, but this server supports both IPv6 and
+ * IPv4 addresses.
+ */
+ void start_dualStack(in long port);
+
+ /**
+ * Shuts down this server if it is running (including the period of time after
+ * stop() has been called but before the provided callback has been called).
+ *
+ * @param callback
+ * an asynchronous callback used to notify the user when this server is
+ * stopped and all pending requests have been fully served
+ * @throws NS_ERROR_NULL_POINTER
+ * if callback is null
+ * @throws NS_ERROR_UNEXPECTED
+ * if this server is not running
+ */
+ void stop(in nsIHttpServerStoppedCallback callback);
+
+ /**
+ * Associates the local file represented by the string file with all requests
+ * which match request.
+ *
+ * @param path
+ * the path which is to be mapped to the given file; must begin with "/" and
+ * be a valid URI path (i.e., no query string, hash reference, etc.)
+ * @param file
+ * the file to serve for the given path, or null to remove any mapping that
+ * might exist; this file must exist for the lifetime of the server
+ * @param handler
+ * an optional object which can be used to handle any further changes.
+ */
+ void registerFile(in string path,
+ in nsIFile file,
+ [optional] in nsIHttpRequestHandler handler);
+
+ /**
+ * Registers a custom path handler.
+ *
+ * @param path
+ * the path on the server (beginning with a "/") which is to be handled by
+ * handler; this path must not include a query string or hash component; it
+ * also should usually be canonicalized, since most browsers will do so
+ * before sending otherwise-matching requests
+ * @param handler
+ * an object which will handle any requests for the given path, or null to
+ * remove any existing handler; if while the server is running the handler
+ * throws an exception while responding to a request, an HTTP 500 response
+ * will be returned
+ * @throws NS_ERROR_INVALID_ARG
+ * if path does not begin with a "/"
+ */
+ void registerPathHandler(in string path, in nsIHttpRequestHandler handler);
+
+ /**
+ * Registers a custom prefix handler.
+ *
+ * @param prefix
+ * the path on the server (beginning and ending with "/") which is to be
+ * handled by handler; this path must not include a query string or hash
+ * component. All requests that start with this prefix will be directed to
+ * the given handler.
+ * @param handler
+ * an object which will handle any requests for the given path, or null to
+ * remove any existing handler; if while the server is running the handler
+ * throws an exception while responding to a request, an HTTP 500 response
+ * will be returned
+ * @throws NS_ERROR_INVALID_ARG
+ * if path does not begin with a "/" or does not end with a "/"
+ */
+ void registerPrefixHandler(in string prefix, in nsIHttpRequestHandler handler);
+
+ /**
+ * Registers a custom error page handler.
+ *
+ * @param code
+ * the error code which is to be handled by handler
+ * @param handler
+ * an object which will handle any requests which generate the given status
+ * code, or null to remove any existing handler. If the handler throws an
+ * exception during server operation, fallback is to the genericized error
+ * handler (the x00 version), then to 500, using a user-defined error
+ * handler if one exists or the server default handler otherwise. Fallback
+ * will never occur from a user-provided handler that throws to the same
+ * handler as provided by the server, e.g. a throwing user 404 falls back to
+ * 400, not a server-provided 404 that might not throw.
+ * @note
+ * If the error handler handles HTTP 500 and throws, behavior is undefined.
+ */
+ void registerErrorHandler(in unsigned long code, in nsIHttpRequestHandler handler);
+
+ /**
+ * Maps all requests to paths beneath path to the corresponding file beneath
+ * dir.
+ *
+ * @param path
+ * the absolute path on the server against which requests will be served
+ * from dir (e.g., "/", "/foo/", etc.); must begin and end with a forward
+ * slash
+ * @param dir
+ * the directory to be used to serve all requests for paths underneath path
+ * (except those further overridden by another, deeper path registered with
+ * another directory); if null, any current mapping for the given path is
+ * removed
+ * @throws NS_ERROR_INVALID_ARG
+ * if dir is non-null and does not exist or is not a directory, or if path
+ * does not begin with and end with a forward slash
+ */
+ void registerDirectory(in string path, in nsIFile dir);
+
+ /**
+ * Associates files with the given extension with the given Content-Type when
+ * served by this server, in the absence of any file-specific information
+ * about the desired Content-Type. If type is empty, removes any extant
+ * mapping, if one is present.
+ *
+ * @throws NS_ERROR_INVALID_ARG
+ * if the given type is not a valid header field value, i.e. if it doesn't
+ * match the field-value production in RFC 2616
+ * @note
+ * No syntax checking is done of the given type, beyond ensuring that it is
+ * a valid header field value. Behavior when not given a string matching
+ * the media-type production in RFC 2616 section 3.7 is undefined.
+ * Implementations may choose to define specific behavior for types which do
+ * not match the production, such as for CGI functionality.
+ * @note
+ * Implementations MAY treat type as a trusted argument; users who fail to
+ * generate this string from trusted data risk security vulnerabilities.
+ */
+ void registerContentType(in string extension, in string type);
+
+ /**
+ * Sets the handler used to display the contents of a directory if
+ * the directory contains no index page.
+ *
+ * @param handler
+ * an object which will handle any requests for directories which
+ * do not contain index pages, or null to reset to the default
+ * index handler; if while the server is running the handler
+ * throws an exception while responding to a request, an HTTP 500
+ * response will be returned. An nsIFile corresponding to the
+ * directory is available from the metadata object passed to the
+ * handler, under the key "directory".
+ */
+ void setIndexHandler(in nsIHttpRequestHandler handler);
+
+ /** Represents the locations at which this server is reachable. */
+ readonly attribute nsIHttpServerIdentity identity;
+
+ /**
+ * Retrieves the string associated with the given key in this, for the given
+ * path's saved state. All keys are initially associated with the empty
+ * string.
+ */
+ AString getState(in AString path, in AString key);
+
+ /**
+ * Sets the string associated with the given key in this, for the given path's
+ * saved state.
+ */
+ void setState(in AString path, in AString key, in AString value);
+
+ /**
+ * Retrieves the string associated with the given key in this, in
+ * entire-server saved state. All keys are initially associated with the
+ * empty string.
+ */
+ AString getSharedState(in AString key);
+
+ /**
+ * Sets the string associated with the given key in this, in entire-server
+ * saved state.
+ */
+ void setSharedState(in AString key, in AString value);
+
+ /**
+ * Retrieves the object associated with the given key in this in
+ * object-valued saved state. All keys are initially associated with null.
+ */
+ nsISupports getObjectState(in AString key);
+
+ /**
+ * Sets the object associated with the given key in this in object-valued
+ * saved state. The value may be null.
+ */
+ void setObjectState(in AString key, in nsISupports value);
+};
+
+/**
+ * An interface through which a notification of the complete stopping (socket
+ * closure, in-flight requests all fully served and responded to) of an HTTP
+ * server may be received.
+ */
+[scriptable, function, uuid(925a6d33-9937-4c63-abe1-a1c56a986455)]
+interface nsIHttpServerStoppedCallback : nsISupports
+{
+ /** Called when the corresponding server has been fully stopped. */
+ void onStopped();
+};
+
+/**
+ * Represents a set of names for a server, one of which is the primary name for
+ * the server and the rest of which are secondary. By default every server will
+ * contain ("http", "localhost", port) and ("http", "127.0.0.1", port) as names,
+ * where port is what was provided to the corresponding server when started;
+ * however, except for their being removed when the corresponding server stops
+ * they have no special importance.
+ */
+[scriptable, uuid(a89de175-ae8e-4c46-91a5-0dba99bbd284)]
+interface nsIHttpServerIdentity : nsISupports
+{
+ /**
+ * The primary scheme at which the corresponding server is located, defaulting
+ * to 'http'. This name will be the value of nsIHttpRequest.scheme for
+ * HTTP/1.0 requests.
+ *
+ * This value is always set when the corresponding server is running. If the
+ * server is not running, this value is set only if it has been set to a
+ * non-default name using setPrimary. In this case reading this value will
+ * throw NS_ERROR_NOT_INITIALIZED.
+ */
+ readonly attribute string primaryScheme;
+
+ /**
+ * The primary name by which the corresponding server is known, defaulting to
+ * 'localhost'. This name will be the value of nsIHttpRequest.host for
+ * HTTP/1.0 requests.
+ *
+ * This value is always set when the corresponding server is running. If the
+ * server is not running, this value is set only if it has been set to a
+ * non-default name using setPrimary. In this case reading this value will
+ * throw NS_ERROR_NOT_INITIALIZED.
+ */
+ readonly attribute string primaryHost;
+
+ /**
+ * The primary port on which the corresponding server runs, defaulting to the
+ * associated server's port. This name will be the value of
+ * nsIHttpRequest.port for HTTP/1.0 requests.
+ *
+ * This value is always set when the corresponding server is running. If the
+ * server is not running, this value is set only if it has been set to a
+ * non-default name using setPrimary. In this case reading this value will
+ * throw NS_ERROR_NOT_INITIALIZED.
+ */
+ readonly attribute long primaryPort;
+
+ /**
+ * Adds a location at which this server may be accessed.
+ *
+ * @throws NS_ERROR_ILLEGAL_VALUE
+ * if scheme or host do not match the scheme or host productions imported
+ * into RFC 2616 from RFC 2396, or if port is not a valid port number
+ */
+ void add(in string scheme, in string host, in long port);
+
+ /**
+ * Removes this name from the list of names by which the corresponding server
+ * is known. If name is also the primary name for the server, the primary
+ * name reverts to 'http://127.0.0.1' with the associated server's port.
+ *
+ * @throws NS_ERROR_ILLEGAL_VALUE
+ * if scheme or host do not match the scheme or host productions imported
+ * into RFC 2616 from RFC 2396, or if port is not a valid port number
+ * @returns
+ * true if the given name was a name for this server, false otherwise
+ */
+ boolean remove(in string scheme, in string host, in long port);
+
+ /**
+ * Returns true if the given name is in this, false otherwise.
+ *
+ * @throws NS_ERROR_ILLEGAL_VALUE
+ * if scheme or host do not match the scheme or host productions imported
+ * into RFC 2616 from RFC 2396, or if port is not a valid port number
+ */
+ boolean has(in string scheme, in string host, in long port);
+
+ /**
+ * Returns the scheme for the name with the given host and port, if one is
+ * present; otherwise returns the empty string.
+ *
+ * @throws NS_ERROR_ILLEGAL_VALUE
+ * if host does not match the host production imported into RFC 2616 from
+ * RFC 2396, or if port is not a valid port number
+ */
+ string getScheme(in string host, in long port);
+
+ /**
+ * Designates the given name as the primary name in this and adds it to this
+ * if it is not already present.
+ *
+ * @throws NS_ERROR_ILLEGAL_VALUE
+ * if scheme or host do not match the scheme or host productions imported
+ * into RFC 2616 from RFC 2396, or if port is not a valid port number
+ */
+ void setPrimary(in string scheme, in string host, in long port);
+};
+
+/**
+ * A representation of a handler for HTTP requests. The handler is used by
+ * calling its .handle method with data for an incoming request; it is the
+ * handler's job to use that data as it sees fit to make the desired response.
+ *
+ * @note
+ * This interface uses the [function] attribute, so you can pass a
+ * script-defined function with the functionality of handle() to any
+ * method which has a nsIHttpRequestHandler parameter, instead of wrapping
+ * it in an otherwise empty object.
+ */
+[scriptable, function, uuid(2bbb4db7-d285-42b3-a3ce-142b8cc7e139)]
+interface nsIHttpRequestHandler : nsISupports
+{
+ /**
+ * Processes an HTTP request and initializes the passed-in response to reflect
+ * the correct HTTP response.
+ *
+ * If this method throws an exception, externally observable behavior depends
+ * upon whether is being processed asynchronously. If such is the case, the
+ * output is some prefix (perhaps all, perhaps none, perhaps only some) of the
+ * data which would have been sent if, instead, the response had been finished
+ * at that point. If no data has been written, the response has not had
+ * seizePower() called on it, and it is not being asynchronously created, an
+ * error handler will be invoked (usually 500 unless otherwise specified).
+ *
+ * Some uses of nsIHttpRequestHandler may require this method to never throw
+ * an exception; in the general case, however, this method may throw an
+ * exception (causing an HTTP 500 response to occur, if the above conditions
+ * are met).
+ *
+ * @param request
+ * data representing an HTTP request
+ * @param response
+ * an initially-empty response which must be modified to reflect the data
+ * which should be sent as the response to the request described by metadata
+ */
+ void handle(in nsIHttpRequest request, in nsIHttpResponse response);
+};
+
+
+/**
+ * A representation of the data included in an HTTP request.
+ */
+[scriptable, uuid(978cf30e-ad73-42ee-8f22-fe0aaf1bf5d2)]
+interface nsIHttpRequest : nsISupports
+{
+ /**
+ * The request type for this request (see RFC 2616, section 5.1.1).
+ */
+ readonly attribute string method;
+
+ /**
+ * The scheme of the requested path, usually 'http' but might possibly be
+ * 'https' if some form of SSL tunneling is in use. Note that this value
+ * cannot be accurately determined unless the incoming request used the
+ * absolute-path form of the request line; it defaults to 'http', so only
+ * if it is something else can you be entirely certain it's correct.
+ */
+ readonly attribute string scheme;
+
+ /**
+ * The host of the data being requested (e.g. "localhost" for the
+ * http://localhost:8080/file resource). Note that the relevant port on the
+ * host is specified in this.port. This value is in the ASCII character
+ * encoding.
+ */
+ readonly attribute string host;
+
+ /**
+ * The port on the server on which the request was received.
+ */
+ readonly attribute unsigned long port;
+
+ /**
+ * The requested path, without any query string (e.g. "/dir/file.txt"). It is
+ * guaranteed to begin with a "/". The individual components in this string
+ * are URL-encoded.
+ */
+ readonly attribute string path;
+
+ /**
+ * The URL-encoded query string associated with this request, not including
+ * the initial "?", or "" if no query string was present.
+ */
+ readonly attribute string queryString;
+
+ /**
+ * A string containing the HTTP version of the request (i.e., "1.1"). Leading
+ * zeros for either component of the version will be omitted. (In other
+ * words, if the request contains the version "1.01", this attribute will be
+ * "1.1"; see RFC 2616, section 3.1.)
+ */
+ readonly attribute string httpVersion;
+
+ /**
+ * Returns the value for the header in this request specified by fieldName.
+ *
+ * @param fieldName
+ * the name of the field whose value is to be gotten; note that since HTTP
+ * header field names are case-insensitive, this method produces equivalent
+ * results for "HeAdER" and "hEADer" as fieldName
+ * @returns
+ * The result is a string containing the individual values of the header,
+ * usually separated with a comma. The headers WWW-Authenticate,
+ * Proxy-Authenticate, and Set-Cookie violate the HTTP specification,
+ * however, and for these headers only the separator string is '\n'.
+ *
+ * @throws NS_ERROR_INVALID_ARG
+ * if fieldName does not constitute a valid header field name
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * if the given header does not exist in this
+ */
+ string getHeader(in string fieldName);
+
+ /**
+ * Returns true if a header with the given field name exists in this, false
+ * otherwise.
+ *
+ * @param fieldName
+ * the field name whose existence is to be determined in this; note that
+ * since HTTP header field names are case-insensitive, this method produces
+ * equivalent results for "HeAdER" and "hEADer" as fieldName
+ * @throws NS_ERROR_INVALID_ARG
+ * if fieldName does not constitute a valid header field name
+ */
+ boolean hasHeader(in string fieldName);
+
+ /**
+ * An nsISimpleEnumerator of nsISupportsStrings over the names of the headers
+ * in this request. The header field names in the enumerator may not
+ * necessarily have the same case as they do in the request itself.
+ */
+ readonly attribute nsISimpleEnumerator headers;
+
+ /**
+ * A stream from which data appearing in the body of this request can be read.
+ */
+ readonly attribute nsIInputStream bodyInputStream;
+};
+
+
+/**
+ * Represents an HTTP response, as described in RFC 2616, section 6.
+ */
+[scriptable, uuid(1acd16c2-dc59-42fa-9160-4f26c43c1c21)]
+interface nsIHttpResponse : nsISupports
+{
+ /**
+ * Sets the status line for this. If this method is never called on this, the
+ * status line defaults to "HTTP/", followed by the server's default HTTP
+ * version (e.g. "1.1"), followed by " 200 OK".
+ *
+ * @param httpVersion
+ * the HTTP version of this, as a string (e.g. "1.1"); if null, the server
+ * default is used
+ * @param code
+ * the numeric HTTP status code for this
+ * @param description
+ * a human-readable description of code; may be null if no description is
+ * desired
+ * @throws NS_ERROR_INVALID_ARG
+ * if httpVersion is not a valid HTTP version string, statusCode is greater
+ * than 999, or description contains invalid characters
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * if this response is being processed asynchronously and data has been
+ * written to this response's body, or if seizePower() has been called on
+ * this
+ */
+ void setStatusLine(in string httpVersion,
+ in unsigned short statusCode,
+ in string description);
+
+ /**
+ * Sets the specified header in this.
+ *
+ * @param name
+ * the name of the header; must match the field-name production per RFC 2616
+ * @param value
+ * the value of the header; must match the field-value production per RFC
+ * 2616
+ * @param merge
+ * when true, if the given header already exists in this, the values passed
+ * to this function will be merged into the existing header, per RFC 2616
+ * header semantics (except for the Set-Cookie, WWW-Authenticate, and
+ * Proxy-Authenticate headers, which will treat each such merged header as
+ * an additional instance of the header, for real-world compatibility
+ * reasons); when false, replaces any existing header of the given name (if
+ * any exists) with a new header with the specified value
+ * @throws NS_ERROR_INVALID_ARG
+ * if name or value is not a valid header component
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * if this response is being processed asynchronously and data has been
+ * written to this response's body, or if seizePower() has been called on
+ * this
+ */
+ void setHeader(in string name, in string value, in boolean merge);
+
+ /**
+ * This is used for testing our header handling, so header will be sent out
+ * without transformation. There can be multiple headers.
+ */
+ void setHeaderNoCheck(in string name, in string value);
+
+ /**
+ * A stream to which data appearing in the body of this response (or in the
+ * totality of the response if seizePower() is called) should be written.
+ * After this response has been designated as being processed asynchronously,
+ * or after seizePower() has been called on this, subsequent writes will no
+ * longer be buffered and will be written to the underlying transport without
+ * delaying until the entire response is constructed. Write-through may or
+ * may not be synchronous in the implementation, and in any case particular
+ * behavior may not be observable to the HTTP client as intermediate buffers
+ * both in the server socket and in the client may delay written data; be
+ * prepared for delays at any time.
+ *
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * if accessed after this response is fully constructed
+ */
+ readonly attribute nsIOutputStream bodyOutputStream;
+
+ /**
+ * Writes a string to the response's output stream. This method is merely a
+ * convenient shorthand for writing the same data to bodyOutputStream
+ * directly.
+ *
+ * @note
+ * This method is only guaranteed to work with ASCII data.
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * if called after this response has been fully constructed
+ */
+ void write(in string data);
+
+ /**
+ * Signals that this response is being constructed asynchronously. Requests
+ * are typically completely constructed during nsIHttpRequestHandler.handle;
+ * however, responses which require significant resources (time, memory,
+ * processing) to construct can be created and sent incrementally by calling
+ * this method during the call to nsIHttpRequestHandler.handle. This method
+ * only has this effect when called during nsIHttpRequestHandler.handle;
+ * behavior is undefined if it is called at a later time. It may be called
+ * multiple times with no ill effect, so long as each call occurs before
+ * finish() is called.
+ *
+ * @throws NS_ERROR_UNEXPECTED
+ * if not initially called within a nsIHttpRequestHandler.handle call or if
+ * called after this response has been finished
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * if seizePower() has been called on this
+ */
+ void processAsync();
+
+ /**
+ * Seizes complete control of this response (and its connection) from the
+ * server, allowing raw and unfettered access to data being sent in the HTTP
+ * response. Once this method has been called the only property which may be
+ * accessed without an exception being thrown is bodyOutputStream, and the
+ * only methods which may be accessed without an exception being thrown are
+ * write(), finish(), and seizePower() (which may be called multiple times
+ * without ill effect so long as all calls are otherwise allowed).
+ *
+ * After a successful call, all data subsequently written to the body of this
+ * response is written directly to the corresponding connection. (Previously-
+ * written data is silently discarded.) No status line or headers are sent
+ * before doing so; if the response handler wishes to write such data, it must
+ * do so manually. Data generation completes only when finish() is called; it
+ * is not enough to simply call close() on bodyOutputStream.
+ *
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * if processAsync() has been called on this
+ * @throws NS_ERROR_UNEXPECTED
+ * if finish() has been called on this
+ */
+ void seizePower();
+
+ /**
+ * Signals that construction of this response is complete and that it may be
+ * sent over the network to the client, or if seizePower() has been called
+ * signals that all data has been written and that the underlying connection
+ * may be closed. This method may only be called after processAsync() or
+ * seizePower() has been called. This method is idempotent.
+ *
+ * @throws NS_ERROR_UNEXPECTED
+ * if processAsync() or seizePower() has not already been properly called
+ */
+ void finish();
+};
diff --git a/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^
new file mode 100644
index 0000000000..b005a65fd2
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^
@@ -0,0 +1 @@
+If this has goofy headers on it, it's a success.
diff --git a/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^
new file mode 100644
index 0000000000..66e1522317
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^
@@ -0,0 +1,3 @@
+HTTP 500 This Isn't A Server Error
+Foo-RFC: 3092
+Shaving-Cream-Atom: Illudium Phosdex
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_both.html b/netwerk/test/httpserver/test/data/cern_meta/test_both.html
new file mode 100644
index 0000000000..db18ea5d7a
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_both.html
@@ -0,0 +1,2 @@
+This page is a text file served with status 501. (That's really a lie, tho,
+because this is definitely Implemented.)
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^
new file mode 100644
index 0000000000..bb3c16a2e2
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^
@@ -0,0 +1,2 @@
+HTTP 501 Unimplemented
+Content-Type: text/plain
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt
new file mode 100644
index 0000000000..7235fa32a5
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <title>This is really HTML, not text</title>
+</head>
+<body>
+<p>This file is really HTML; the test_ctype_override.txt^headers^ file sets a
+ new header that overwrites the default text/plain header.</p>
+</body>
+</html>
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^
new file mode 100644
index 0000000000..156209f9c8
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^
@@ -0,0 +1 @@
+Content-Type: text/html
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html
new file mode 100644
index 0000000000..fd243c640e
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <title>This is a 404 page</title>
+</head>
+<body>
+<p>This page has a 404 HTTP status associated with it, via
+ <code>test_status_override.html^headers^</code>.</p>
+</body>
+</html>
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^
new file mode 100644
index 0000000000..f438a05746
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^
@@ -0,0 +1 @@
+HTTP 404 Can't Find This
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt
new file mode 100644
index 0000000000..4718ec282f
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt
@@ -0,0 +1 @@
+This page has an HTTP status override without a description (it defaults to "").
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^
new file mode 100644
index 0000000000..32da7632f9
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^
@@ -0,0 +1 @@
+HTTP 732
diff --git a/netwerk/test/httpserver/test/data/name-scheme/bar.html^^ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^
new file mode 100644
index 0000000000..bed1f34c9f
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <title>Welcome to bar.html^</title>
+</head>
+<body>
+<p>This file is named with two trailing carets, so the last is stripped
+ away, producing bar.html^ as the final name.</p>
+</body>
+</html>
+
diff --git a/netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^
new file mode 100644
index 0000000000..04fbaa08fe
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^
@@ -0,0 +1,2 @@
+HTTP 200 OK
+Content-Type: text/html
diff --git a/netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^
new file mode 100644
index 0000000000..dccee48e34
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^
@@ -0,0 +1 @@
+This file shouldn't be shown in directory listings.
diff --git a/netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^
new file mode 100644
index 0000000000..a8ee35a3b6
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^
@@ -0,0 +1 @@
+This file should show up in directory listings as SHOULD_SEE_THIS.txt^.
diff --git a/netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt b/netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt
new file mode 100644
index 0000000000..2ceca8ca9e
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt
@@ -0,0 +1,2 @@
+File in a directory named with a trailing caret (in the virtual FS; on disk it
+actually ends with two carets).
diff --git a/netwerk/test/httpserver/test/data/name-scheme/foo.html^ b/netwerk/test/httpserver/test/data/name-scheme/foo.html^
new file mode 100644
index 0000000000..a3efe8b5c3
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/foo.html^
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <title>ERROR</title>
+</head>
+<body>
+<p>This file should never be served by the web server because its name ends
+ with a caret not followed by another caret.</p>
+</body>
+</html>
diff --git a/netwerk/test/httpserver/test/data/name-scheme/normal-file.txt b/netwerk/test/httpserver/test/data/name-scheme/normal-file.txt
new file mode 100644
index 0000000000..ab71eabaf0
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/normal-file.txt
@@ -0,0 +1 @@
+This should be seen.
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 0000000000..e69de29bb2
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/ranges/empty.txt
diff --git a/netwerk/test/httpserver/test/data/ranges/headers.txt b/netwerk/test/httpserver/test/data/ranges/headers.txt
new file mode 100644
index 0000000000..6cf83528c8
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/ranges/headers.txt
@@ -0,0 +1 @@
+Hello Kitty
diff --git a/netwerk/test/httpserver/test/data/ranges/headers.txt^headers^ b/netwerk/test/httpserver/test/data/ranges/headers.txt^headers^
new file mode 100644
index 0000000000..d0a633f042
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/ranges/headers.txt^headers^
@@ -0,0 +1 @@
+X-SJS-Header: customized
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 0000000000..ab71eabaf0
--- /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/data/sjs/cgi.sjs b/netwerk/test/httpserver/test/data/sjs/cgi.sjs
new file mode 100644
index 0000000000..a6b987a8b7
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/cgi.sjs
@@ -0,0 +1,8 @@
+function handleRequest(request, response) {
+ if (request.queryString == "throw") {
+ throw new Error("monkey wrench!");
+ }
+
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write("PASS");
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^ b/netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^
new file mode 100644
index 0000000000..a83ff774ab
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^
@@ -0,0 +1,2 @@
+HTTP 500 Error
+This-Header: SHOULD NOT APPEAR IN CGI.JSC RESPONSES!
diff --git a/netwerk/test/httpserver/test/data/sjs/object-state.sjs b/netwerk/test/httpserver/test/data/sjs/object-state.sjs
new file mode 100644
index 0000000000..8f027dfedf
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/object-state.sjs
@@ -0,0 +1,74 @@
+function parseQueryString(str) {
+ var paramArray = str.split("&");
+ var regex = /^([^=]+)=(.*)$/;
+ var params = {};
+ for (var i = 0, sz = paramArray.length; i < sz; i++) {
+ var match = regex.exec(paramArray[i]);
+ if (!match) {
+ throw new Error("Bad parameter in queryString! '" + paramArray[i] + "'");
+ }
+ params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+ }
+
+ return params;
+}
+
+/*
+ * We're relying somewhat dubiously on all data being sent as soon as it's
+ * available at numerous levels (in Necko in the server-side part of the
+ * connection, in the OS's outgoing socket buffer, in the OS's incoming socket
+ * buffer, and in Necko in the client-side part of the connection), but to the
+ * best of my knowledge there's no way to force data flow at all those levels,
+ * so this is the best we can do.
+ */
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ /*
+ * NB: A Content-Type header is *necessary* to avoid content-sniffing, which
+ * will delay onStartRequest past the the point where the entire head of
+ * the response has been received.
+ */
+ response.setHeader("Content-Type", "text/plain", false);
+
+ var params = parseQueryString(request.queryString);
+
+ switch (params.state) {
+ case "initial":
+ response.processAsync();
+ response.write("do");
+ var state = {
+ QueryInterface: ChromeUtils.generateQI([]),
+ end() {
+ response.write("ne");
+ response.finish();
+ },
+ };
+ state.wrappedJSObject = state;
+ setObjectState("object-state-test", state);
+ getObjectState("object-state-test", function (obj) {
+ if (obj !== state) {
+ response.write("FAIL bad state save");
+ response.finish();
+ }
+ });
+ break;
+
+ case "intermediate":
+ response.write("intermediate");
+ break;
+
+ case "trigger":
+ response.write("trigger");
+ getObjectState("object-state-test", function (obj) {
+ obj.wrappedJSObject.end();
+ setObjectState("object-state-test", null);
+ });
+ break;
+
+ default:
+ response.setStatusLine(request.httpVersion, 500, "Unexpected State");
+ response.write("Bad state: " + params.state);
+ break;
+ }
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/qi.sjs b/netwerk/test/httpserver/test/data/sjs/qi.sjs
new file mode 100644
index 0000000000..ee0fc74a0f
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/qi.sjs
@@ -0,0 +1,45 @@
+function handleRequest(request, response) {
+ var exstr, qid;
+
+ response.setStatusLine(request.httpVersion, 500, "FAIL");
+
+ var passed = false;
+ try {
+ qid = request.QueryInterface(Ci.nsIHttpRequest);
+ passed = qid === request;
+ } catch (e) {
+ // eslint-disable-next-line no-control-regex
+ exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0];
+ response.setStatusLine(
+ request.httpVersion,
+ 500,
+ "request doesn't QI: " + exstr
+ );
+ return;
+ }
+ if (!passed) {
+ response.setStatusLine(request.httpVersion, 500, "request QI'd wrongly?");
+ return;
+ }
+
+ passed = false;
+ try {
+ qid = response.QueryInterface(Ci.nsIHttpResponse);
+ passed = qid === response;
+ } catch (e) {
+ // eslint-disable-next-line no-control-regex
+ exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0];
+ response.setStatusLine(
+ request.httpVersion,
+ 500,
+ "response doesn't QI: " + exstr
+ );
+ return;
+ }
+ if (!passed) {
+ response.setStatusLine(request.httpVersion, 500, "response QI'd wrongly?");
+ return;
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "SJS QI Tests Passed");
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/range-checker.sjs b/netwerk/test/httpserver/test/data/sjs/range-checker.sjs
new file mode 100644
index 0000000000..4bc447f739
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/range-checker.sjs
@@ -0,0 +1 @@
+function handleRequest(request, response) {}
diff --git a/netwerk/test/httpserver/test/data/sjs/sjs b/netwerk/test/httpserver/test/data/sjs/sjs
new file mode 100644
index 0000000000..374ca41674
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/sjs
@@ -0,0 +1,4 @@
+function handleRequest(request, response)
+{
+ response.write("FAIL");
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/state1.sjs b/netwerk/test/httpserver/test/data/sjs/state1.sjs
new file mode 100644
index 0000000000..1a2540eca1
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/state1.sjs
@@ -0,0 +1,38 @@
+function parseQueryString(str) {
+ var paramArray = str.split("&");
+ var regex = /^([^=]+)=(.*)$/;
+ var params = {};
+ for (var i = 0, sz = paramArray.length; i < sz; i++) {
+ var match = regex.exec(paramArray[i]);
+ if (!match) {
+ throw new Error("Bad parameter in queryString! '" + paramArray[i] + "'");
+ }
+ params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+ }
+
+ return params;
+}
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ var params = parseQueryString(request.queryString);
+
+ var oldShared = getSharedState("shared-value");
+ response.setHeader("X-Old-Shared-Value", oldShared, false);
+
+ var newShared = params.newShared;
+ if (newShared !== undefined) {
+ setSharedState("shared-value", newShared);
+ response.setHeader("X-New-Shared-Value", newShared, false);
+ }
+
+ var oldPrivate = getState("private-value");
+ response.setHeader("X-Old-Private-Value", oldPrivate, false);
+
+ var newPrivate = params.newPrivate;
+ if (newPrivate !== undefined) {
+ setState("private-value", newPrivate);
+ response.setHeader("X-New-Private-Value", newPrivate, false);
+ }
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/state2.sjs b/netwerk/test/httpserver/test/data/sjs/state2.sjs
new file mode 100644
index 0000000000..1a2540eca1
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/state2.sjs
@@ -0,0 +1,38 @@
+function parseQueryString(str) {
+ var paramArray = str.split("&");
+ var regex = /^([^=]+)=(.*)$/;
+ var params = {};
+ for (var i = 0, sz = paramArray.length; i < sz; i++) {
+ var match = regex.exec(paramArray[i]);
+ if (!match) {
+ throw new Error("Bad parameter in queryString! '" + paramArray[i] + "'");
+ }
+ params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+ }
+
+ return params;
+}
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ var params = parseQueryString(request.queryString);
+
+ var oldShared = getSharedState("shared-value");
+ response.setHeader("X-Old-Shared-Value", oldShared, false);
+
+ var newShared = params.newShared;
+ if (newShared !== undefined) {
+ setSharedState("shared-value", newShared);
+ response.setHeader("X-New-Shared-Value", newShared, false);
+ }
+
+ var oldPrivate = getState("private-value");
+ response.setHeader("X-Old-Private-Value", oldPrivate, false);
+
+ var newPrivate = params.newPrivate;
+ if (newPrivate !== undefined) {
+ setState("private-value", newPrivate);
+ response.setHeader("X-New-Private-Value", newPrivate, false);
+ }
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/thrower.sjs b/netwerk/test/httpserver/test/data/sjs/thrower.sjs
new file mode 100644
index 0000000000..b34de70e30
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/thrower.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response) {
+ if (request.queryString == "throw") {
+ undefined[5];
+ }
+ response.setHeader("X-Test-Status", "PASS", false);
+}
diff --git a/netwerk/test/httpserver/test/head_utils.js b/netwerk/test/httpserver/test/head_utils.js
new file mode 100644
index 0000000000..3f2fad4940
--- /dev/null
+++ b/netwerk/test/httpserver/test/head_utils.js
@@ -0,0 +1,605 @@
+/* -*- 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/. */
+
+const {
+ dumpn,
+ LineData,
+ nsHttpHeaders,
+ HttpServer,
+ WriteThroughCopier,
+ overrideBinaryStreamsForTests,
+} = ChromeUtils.importESModule("resource://testing-common/httpd.sys.mjs");
+
+// 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.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+const CC = Components.Constructor;
+const FileInputStream = CC(
+ "@mozilla.org/network/file-input-stream;1",
+ "nsIFileInputStream",
+ "init"
+);
+var BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+var BinaryOutputStream = CC(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+const ScriptableInputStream = CC(
+ "@mozilla.org/scriptableinputstream;1",
+ "nsIScriptableInputStream",
+ "init"
+);
+
+/**
+ * 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 HttpServer();
+}
+
+/**
+ * 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();
+}
diff --git a/netwerk/test/httpserver/test/test_async_response_sending.js b/netwerk/test/httpserver/test/test_async_response_sending.js
new file mode 100644
index 0000000000..bc025308ae
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_async_response_sending.js
@@ -0,0 +1,1661 @@
+/* -*- 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/. */
+
+/*
+ * Ensures that data a request handler writes out in response is sent only as
+ * quickly as the client can receive it, without racing ahead and being forced
+ * to block while writing that data.
+ *
+ * NB: These tests are extremely tied to the current implementation, in terms of
+ * when and how stream-ready notifications occur, the amount of data which will
+ * be read or written at each notification, and so on. If the implementation
+ * changes in any way with respect to stream copying, this test will probably
+ * have to change a little at the edges as well.
+ */
+
+function run_test() {
+ do_test_pending();
+ tests.push(function testsComplete(_) {
+ dumpn(
+ // eslint-disable-next-line no-useless-concat
+ "******************\n" + "* TESTS COMPLETE *\n" + "******************"
+ );
+ do_test_finished();
+ });
+
+ runNextTest();
+}
+
+function runNextTest() {
+ testIndex++;
+ dumpn("*** runNextTest(), testIndex: " + testIndex);
+
+ try {
+ var test = tests[testIndex];
+ test(runNextTest);
+ } catch (e) {
+ var msg = "exception running test " + testIndex + ": " + e;
+ if (e && "stack" in e) {
+ msg += "\nstack follows:\n" + e.stack;
+ }
+ do_throw(msg);
+ }
+}
+
+/** ***********
+ * TEST DATA *
+ *************/
+
+const NOTHING = [];
+
+const FIRST_SEGMENT = [1, 2, 3, 4];
+const SECOND_SEGMENT = [5, 6, 7, 8];
+const THIRD_SEGMENT = [9, 10, 11, 12];
+
+const SEGMENT = FIRST_SEGMENT;
+const TWO_SEGMENTS = [1, 2, 3, 4, 5, 6, 7, 8];
+const THREE_SEGMENTS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
+
+const SEGMENT_AND_HALF = [1, 2, 3, 4, 5, 6];
+
+const QUARTER_SEGMENT = [1];
+const HALF_SEGMENT = [1, 2];
+const SECOND_HALF_SEGMENT = [3, 4];
+const EXTRA_HALF_SEGMENT = [5, 6];
+const MIDDLE_HALF_SEGMENT = [2, 3];
+const LAST_QUARTER_SEGMENT = [4];
+const HALF_THIRD_SEGMENT = [9, 10];
+const LATTER_HALF_THIRD_SEGMENT = [11, 12];
+
+const TWO_HALF_SEGMENTS = [1, 2, 1, 2];
+
+/** *******
+ * TESTS *
+ *********/
+
+var tests = [
+ sourceClosedWithoutWrite,
+ writeOneSegmentThenClose,
+ simpleWriteThenRead,
+ writeLittleBeforeReading,
+ writeMultipleSegmentsThenRead,
+ writeLotsBeforeReading,
+ writeLotsBeforeReading2,
+ writeThenReadPartial,
+ manyPartialWrites,
+ partialRead,
+ partialWrite,
+ sinkClosedImmediately,
+ sinkClosedWithReadableData,
+ sinkClosedAfterWrite,
+ sourceAndSinkClosed,
+ sinkAndSourceClosed,
+ sourceAndSinkClosedWithPendingData,
+ sinkAndSourceClosedWithPendingData,
+];
+var testIndex = -1;
+
+function sourceClosedWithoutWrite(next) {
+ var t = new CopyTest("sourceClosedWithoutWrite", next);
+
+ t.closeSource(Cr.NS_OK);
+ t.expect(Cr.NS_OK, [NOTHING]);
+}
+
+function writeOneSegmentThenClose(next) {
+ var t = new CopyTest("writeLittleBeforeReading", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.closeSource(Cr.NS_OK);
+ t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]);
+ t.expect(Cr.NS_OK, [SEGMENT]);
+}
+
+function simpleWriteThenRead(next) {
+ var t = new CopyTest("simpleWriteThenRead", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]);
+ t.closeSource(Cr.NS_OK);
+ t.expect(Cr.NS_OK, [SEGMENT]);
+}
+
+function writeLittleBeforeReading(next) {
+ var t = new CopyTest("writeLittleBeforeReading", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.closeSource(Cr.NS_OK);
+ t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]);
+ t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]);
+ t.expect(Cr.NS_OK, [SEGMENT, SEGMENT]);
+}
+
+function writeMultipleSegmentsThenRead(next) {
+ var t = new CopyTest("writeMultipleSegmentsThenRead", next);
+
+ t.addToSource(TWO_SEGMENTS);
+ t.makeSourceReadable(TWO_SEGMENTS.length);
+ t.makeSinkWritableAndWaitFor(TWO_SEGMENTS.length, [
+ FIRST_SEGMENT,
+ SECOND_SEGMENT,
+ ]);
+ t.closeSource(Cr.NS_OK);
+ t.expect(Cr.NS_OK, [TWO_SEGMENTS]);
+}
+
+function writeLotsBeforeReading(next) {
+ var t = new CopyTest("writeLotsBeforeReading", next);
+
+ t.addToSource(TWO_SEGMENTS);
+ t.makeSourceReadable(TWO_SEGMENTS.length);
+ t.makeSinkWritableAndWaitFor(FIRST_SEGMENT.length, [FIRST_SEGMENT]);
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.makeSinkWritableAndWaitFor(SECOND_SEGMENT.length, [SECOND_SEGMENT]);
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.closeSource(Cr.NS_OK);
+ t.makeSinkWritableAndWaitFor(2 * SEGMENT.length, [SEGMENT, SEGMENT]);
+ t.expect(Cr.NS_OK, [TWO_SEGMENTS, SEGMENT, SEGMENT]);
+}
+
+function writeLotsBeforeReading2(next) {
+ var t = new CopyTest("writeLotsBeforeReading", next);
+
+ t.addToSource(THREE_SEGMENTS);
+ t.makeSourceReadable(THREE_SEGMENTS.length);
+ t.makeSinkWritableAndWaitFor(FIRST_SEGMENT.length, [FIRST_SEGMENT]);
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.makeSinkWritableAndWaitFor(SECOND_SEGMENT.length, [SECOND_SEGMENT]);
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.makeSinkWritableAndWaitFor(THIRD_SEGMENT.length, [THIRD_SEGMENT]);
+ t.closeSource(Cr.NS_OK);
+ t.makeSinkWritableAndWaitFor(2 * SEGMENT.length, [SEGMENT, SEGMENT]);
+ t.expect(Cr.NS_OK, [THREE_SEGMENTS, SEGMENT, SEGMENT]);
+}
+
+function writeThenReadPartial(next) {
+ var t = new CopyTest("writeThenReadPartial", next);
+
+ t.addToSource(SEGMENT_AND_HALF);
+ t.makeSourceReadable(SEGMENT_AND_HALF.length);
+ t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]);
+ t.closeSource(Cr.NS_OK);
+ t.makeSinkWritableAndWaitFor(EXTRA_HALF_SEGMENT.length, [EXTRA_HALF_SEGMENT]);
+ t.expect(Cr.NS_OK, [SEGMENT_AND_HALF]);
+}
+
+function manyPartialWrites(next) {
+ var t = new CopyTest("manyPartialWrites", next);
+
+ t.addToSource(HALF_SEGMENT);
+ t.makeSourceReadable(HALF_SEGMENT.length);
+
+ t.addToSource(HALF_SEGMENT);
+ t.makeSourceReadable(HALF_SEGMENT.length);
+ t.makeSinkWritableAndWaitFor(2 * HALF_SEGMENT.length, [TWO_HALF_SEGMENTS]);
+ t.closeSource(Cr.NS_OK);
+ t.expect(Cr.NS_OK, [TWO_HALF_SEGMENTS]);
+}
+
+function partialRead(next) {
+ var t = new CopyTest("partialRead", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.addToSource(HALF_SEGMENT);
+ t.makeSourceReadable(HALF_SEGMENT.length);
+ t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]);
+ t.closeSourceAndWaitFor(Cr.NS_OK, HALF_SEGMENT.length, [HALF_SEGMENT]);
+ t.expect(Cr.NS_OK, [SEGMENT, HALF_SEGMENT]);
+}
+
+function partialWrite(next) {
+ var t = new CopyTest("partialWrite", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.makeSinkWritableByIncrementsAndWaitFor(SEGMENT.length, [
+ QUARTER_SEGMENT,
+ MIDDLE_HALF_SEGMENT,
+ LAST_QUARTER_SEGMENT,
+ ]);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.makeSinkWritableByIncrementsAndWaitFor(SEGMENT.length, [
+ HALF_SEGMENT,
+ SECOND_HALF_SEGMENT,
+ ]);
+
+ t.addToSource(THREE_SEGMENTS);
+ t.makeSourceReadable(THREE_SEGMENTS.length);
+ t.makeSinkWritableByIncrementsAndWaitFor(THREE_SEGMENTS.length, [
+ HALF_SEGMENT,
+ SECOND_HALF_SEGMENT,
+ SECOND_SEGMENT,
+ HALF_THIRD_SEGMENT,
+ LATTER_HALF_THIRD_SEGMENT,
+ ]);
+
+ t.closeSource(Cr.NS_OK);
+ t.expect(Cr.NS_OK, [SEGMENT, SEGMENT, THREE_SEGMENTS]);
+}
+
+function sinkClosedImmediately(next) {
+ var t = new CopyTest("sinkClosedImmediately", next);
+
+ t.closeSink(Cr.NS_OK);
+ t.expect(Cr.NS_ERROR_UNEXPECTED, [NOTHING]);
+}
+
+function sinkClosedWithReadableData(next) {
+ var t = new CopyTest("sinkClosedWithReadableData", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.closeSink(Cr.NS_OK);
+ t.expect(Cr.NS_ERROR_UNEXPECTED, [NOTHING]);
+}
+
+function sinkClosedAfterWrite(next) {
+ var t = new CopyTest("sinkClosedAfterWrite", next);
+
+ t.addToSource(TWO_SEGMENTS);
+ t.makeSourceReadable(TWO_SEGMENTS.length);
+ t.makeSinkWritableAndWaitFor(FIRST_SEGMENT.length, [FIRST_SEGMENT]);
+ t.closeSink(Cr.NS_OK);
+ t.expect(Cr.NS_ERROR_UNEXPECTED, [FIRST_SEGMENT]);
+}
+
+function sourceAndSinkClosed(next) {
+ var t = new CopyTest("sourceAndSinkClosed", next);
+
+ t.closeSourceThenSink(Cr.NS_OK, Cr.NS_OK);
+ t.expect(Cr.NS_OK, []);
+}
+
+function sinkAndSourceClosed(next) {
+ var t = new CopyTest("sinkAndSourceClosed", next);
+
+ t.closeSinkThenSource(Cr.NS_OK, Cr.NS_OK);
+
+ // sink notify received first, hence error
+ t.expect(Cr.NS_ERROR_UNEXPECTED, []);
+}
+
+function sourceAndSinkClosedWithPendingData(next) {
+ var t = new CopyTest("sourceAndSinkClosedWithPendingData", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+
+ t.closeSourceThenSink(Cr.NS_OK, Cr.NS_OK);
+
+ // not all data from source copied, so error
+ t.expect(Cr.NS_ERROR_UNEXPECTED, []);
+}
+
+function sinkAndSourceClosedWithPendingData(next) {
+ var t = new CopyTest("sinkAndSourceClosedWithPendingData", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+
+ t.closeSinkThenSource(Cr.NS_OK, Cr.NS_OK);
+
+ // not all data from source copied, plus sink notify received first, so error
+ t.expect(Cr.NS_ERROR_UNEXPECTED, []);
+}
+
+/** ***********
+ * UTILITIES *
+ *************/
+
+/** Returns the sum of the elements in arr. */
+function sum(arr) {
+ var s = 0;
+ for (var i = 0, sz = arr.length; i < sz; i++) {
+ s += arr[i];
+ }
+ return s;
+}
+
+/**
+ * Returns a constructor for an input or output stream callback that will wrap
+ * the one provided to it as an argument.
+ *
+ * @param wrapperCallback : (nsIInputStreamCallback | nsIOutputStreamCallback) : void
+ * the original callback object (not a function!) being wrapped
+ * @param name : string
+ * either "onInputStreamReady" if we're wrapping an input stream callback or
+ * "onOutputStreamReady" if we're wrapping an output stream callback
+ * @returns function(nsIInputStreamCallback | nsIOutputStreamCallback) : (nsIInputStreamCallback | nsIOutputStreamCallback)
+ * a constructor function which constructs a callback object (not function!)
+ * which, when called, first calls the original callback provided to it and
+ * then calls wrapperCallback
+ */
+function createStreamReadyInterceptor(wrapperCallback, name) {
+ return function StreamReadyInterceptor(callback) {
+ this.wrappedCallback = callback;
+ this[name] = function streamReadyInterceptor(stream) {
+ dumpn("*** StreamReadyInterceptor." + name);
+
+ try {
+ dumpn("*** calling original " + name + "...");
+ callback[name](stream);
+ } catch (e) {
+ dumpn("!!! error running inner callback: " + e);
+ throw e;
+ } finally {
+ dumpn("*** calling wrapper " + name + "...");
+ wrapperCallback[name](stream);
+ }
+ };
+ };
+}
+
+/**
+ * Print out a banner with the given message, uppercased, for debugging
+ * purposes.
+ */
+function note(m) {
+ m = m.toUpperCase();
+ var asterisks = Array(m.length + 1 + 4).join("*");
+ dumpn(asterisks + "\n* " + m + " *\n" + asterisks);
+}
+
+/** *********
+ * MOCKERY *
+ ***********/
+
+/*
+ * Blatantly violate abstractions in the name of testability. THIS IS NOT
+ * PUBLIC API! If you use any of these I will knowingly break your code by
+ * changing the names of variables and properties.
+ */
+// These are used in head.js.
+BinaryInputStream = function BIS(stream) {
+ return stream;
+};
+BinaryOutputStream = function BOS(stream) {
+ return stream;
+};
+Response.SEGMENT_SIZE = SEGMENT.length;
+// This overrides in httpd.js.
+overrideBinaryStreamsForTests(
+ BinaryInputStream,
+ BinaryOutputStream,
+ SEGMENT.length
+);
+
+/**
+ * Roughly mocks an nsIPipe, presenting non-blocking input and output streams
+ * that appear to also be binary streams and whose readability and writability
+ * amounts are configurable. Only the methods used in this test have been
+ * implemented -- these aren't exact mocks (can't be, actually, because input
+ * streams have unscriptable methods).
+ *
+ * @param name : string
+ * a name for this pipe, used in debugging output
+ */
+function CustomPipe(name) {
+ var self = this;
+
+ /** Data read from input that's buffered until it can be written to output. */
+ this._data = [];
+
+ /**
+ * The status of this pipe, which is to say the error result the ends of this
+ * pipe will return when attempts are made to use them. This value is always
+ * an error result when copying has finished, because success codes are
+ * converted to NS_BASE_STREAM_CLOSED.
+ */
+ this._status = Cr.NS_OK;
+
+ /** The input end of this pipe. */
+ var input = (this.inputStream = {
+ /** A name for this stream, used in debugging output. */
+ name: name + " input",
+
+ /**
+ * The number of bytes of data available to be read from this pipe, or
+ * Infinity if any amount of data in this pipe is made readable as soon as
+ * it is written to the pipe output.
+ */
+ _readable: 0,
+
+ /**
+ * Data regarding a pending stream-ready callback on this, or null if no
+ * callback is currently waiting to be called.
+ */
+ _waiter: null,
+
+ /**
+ * The event currently dispatched to make a stream-ready callback, if any
+ * such callback is currently ready to be made and not already in
+ * progress, or null when no callback is waiting to happen.
+ */
+ _event: null,
+
+ /**
+ * A stream-ready constructor to wrap an existing callback to intercept
+ * stream-ready notifications, or null if notifications shouldn't be
+ * wrapped at all.
+ */
+ _streamReadyInterceptCreator: null,
+
+ /**
+ * Registers a stream-ready wrapper creator function so that a
+ * stream-ready callback made in the future can be wrapped.
+ */
+ interceptStreamReadyCallbacks(streamReadyInterceptCreator) {
+ dumpn("*** [" + this.name + "].interceptStreamReadyCallbacks");
+
+ Assert.ok(
+ this._streamReadyInterceptCreator === null,
+ "intercepting twice"
+ );
+ this._streamReadyInterceptCreator = streamReadyInterceptCreator;
+ if (this._waiter) {
+ this._waiter.callback = new streamReadyInterceptCreator(
+ this._waiter.callback
+ );
+ }
+ },
+
+ /**
+ * Removes a previously-registered stream-ready wrapper creator function,
+ * also clearing any current wrapping.
+ */
+ removeStreamReadyInterceptor() {
+ dumpn("*** [" + this.name + "].removeStreamReadyInterceptor()");
+
+ Assert.ok(
+ this._streamReadyInterceptCreator !== null,
+ "removing interceptor when none present?"
+ );
+ this._streamReadyInterceptCreator = null;
+ if (this._waiter) {
+ this._waiter.callback = this._waiter.callback.wrappedCallback;
+ }
+ },
+
+ //
+ // see nsIAsyncInputStream.asyncWait
+ //
+ asyncWait: function asyncWait(callback, flags, requestedCount, target) {
+ dumpn("*** [" + this.name + "].asyncWait");
+
+ Assert.ok(callback && typeof callback !== "function");
+
+ var closureOnly =
+ (flags & Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY) !== 0;
+
+ Assert.ok(
+ this._waiter === null || (this._waiter.closureOnly && !closureOnly),
+ "asyncWait already called with a non-closure-only " +
+ "callback? unexpected!"
+ );
+
+ this._waiter = {
+ callback: this._streamReadyInterceptCreator
+ ? new this._streamReadyInterceptCreator(callback)
+ : callback,
+ closureOnly,
+ requestedCount,
+ eventTarget: target,
+ };
+
+ if (
+ !Components.isSuccessCode(self._status) ||
+ (!closureOnly &&
+ this._readable >= requestedCount &&
+ self._data.length >= requestedCount)
+ ) {
+ this._notify();
+ }
+ },
+
+ //
+ // see nsIAsyncInputStream.closeWithStatus
+ //
+ closeWithStatus: function closeWithStatus(status) {
+ // eslint-disable-next-line no-useless-concat
+ dumpn("*** [" + this.name + "].closeWithStatus" + "(" + status + ")");
+
+ if (!Components.isSuccessCode(self._status)) {
+ dumpn(
+ "*** ignoring second closure of [input " +
+ this.name +
+ "] " +
+ "(status " +
+ self._status +
+ ")"
+ );
+ return;
+ }
+
+ if (Components.isSuccessCode(status)) {
+ status = Cr.NS_BASE_STREAM_CLOSED;
+ }
+
+ self._status = status;
+
+ if (this._waiter) {
+ this._notify();
+ }
+ if (output._waiter) {
+ output._notify();
+ }
+ },
+
+ //
+ // see nsIBinaryInputStream.readByteArray
+ //
+ readByteArray: function readByteArray(count) {
+ dumpn("*** [" + this.name + "].readByteArray(" + count + ")");
+
+ if (self._data.length === 0) {
+ throw Components.isSuccessCode(self._status)
+ ? Cr.NS_BASE_STREAM_WOULD_BLOCK
+ : self._status;
+ }
+
+ Assert.ok(
+ this._readable <= self._data.length || this._readable === Infinity,
+ "consistency check"
+ );
+
+ if (this._readable < count || self._data.length < count) {
+ throw Components.Exception("", Cr.NS_BASE_STREAM_WOULD_BLOCK);
+ }
+ this._readable -= count;
+ return self._data.splice(0, count);
+ },
+
+ /**
+ * Makes the given number of additional bytes of data previously written
+ * to the pipe's output stream available for reading, triggering future
+ * notifications when required.
+ *
+ * @param count : uint
+ * the number of bytes of additional data to make available; must not be
+ * greater than the number of bytes already buffered but not made
+ * available by previous makeReadable calls
+ */
+ makeReadable: function makeReadable(count) {
+ dumpn("*** [" + this.name + "].makeReadable(" + count + ")");
+
+ Assert.ok(Components.isSuccessCode(self._status), "errant call");
+ Assert.ok(
+ this._readable + count <= self._data.length ||
+ this._readable === Infinity,
+ "increasing readable beyond written amount"
+ );
+
+ this._readable += count;
+
+ dumpn("readable: " + this._readable + ", data: " + self._data);
+
+ var waiter = this._waiter;
+ if (waiter !== null) {
+ if (waiter.requestedCount <= this._readable && !waiter.closureOnly) {
+ this._notify();
+ }
+ }
+ },
+
+ /**
+ * Disables the readability limit on this stream, meaning that as soon as
+ * *any* amount of data is written to output it becomes available from
+ * this stream and a stream-ready event is dispatched (if any stream-ready
+ * callback is currently set).
+ */
+ disableReadabilityLimit: function disableReadabilityLimit() {
+ dumpn("*** [" + this.name + "].disableReadabilityLimit()");
+
+ this._readable = Infinity;
+ },
+
+ //
+ // see nsIInputStream.available
+ //
+ available: function available() {
+ dumpn("*** [" + this.name + "].available()");
+
+ if (self._data.length === 0 && !Components.isSuccessCode(self._status)) {
+ throw self._status;
+ }
+
+ return Math.min(this._readable, self._data.length);
+ },
+
+ /**
+ * Dispatches a pending stream-ready event ahead of schedule, rather than
+ * waiting for it to be dispatched in response to normal writes. This is
+ * useful when writing to the output has completed, and we need to have
+ * read all data written to this stream. If the output isn't closed and
+ * the reading of data from this races ahead of the last write to output,
+ * we need a notification to know when everything that's been written has
+ * been read. This ordinarily might be supplied by closing output, but
+ * in some cases it's not desirable to close output, so this supplies an
+ * alternative method to get notified when the last write has occurred.
+ */
+ maybeNotifyFinally: function maybeNotifyFinally() {
+ dumpn("*** [" + this.name + "].maybeNotifyFinally()");
+
+ Assert.ok(this._waiter !== null, "must be waiting now");
+
+ if (self._data.length) {
+ dumpn(
+ "*** data still pending, normal notifications will signal " +
+ "completion"
+ );
+ return;
+ }
+
+ // No data waiting to be written, so notify. We could just close the
+ // stream, but that's less faithful to the server's behavior (it doesn't
+ // close the stream, and we're pretending to impersonate the server as
+ // much as we can here), so instead we're going to notify when no data
+ // can be read. The CopyTest has already been flagged as complete, so
+ // the stream listener will detect that this is a wrap-it-up notify and
+ // invoke the next test.
+ this._notify();
+ },
+
+ /**
+ * Dispatches an event to call a previously-registered stream-ready
+ * callback.
+ */
+ _notify: function _notify() {
+ dumpn("*** [" + this.name + "]._notify()");
+
+ var waiter = this._waiter;
+ Assert.ok(waiter !== null, "no waiter?");
+
+ if (this._event === null) {
+ var event = (this._event = {
+ run: function run() {
+ input._waiter = null;
+ input._event = null;
+ try {
+ Assert.ok(
+ !Components.isSuccessCode(self._status) ||
+ input._readable >= waiter.requestedCount
+ );
+ waiter.callback.onInputStreamReady(input);
+ } catch (e) {
+ do_throw("error calling onInputStreamReady: " + e);
+ }
+ },
+ });
+ waiter.eventTarget.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
+ }
+ },
+ });
+
+ /** The output end of this pipe. */
+ var output = (this.outputStream = {
+ /** A name for this stream, used in debugging output. */
+ name: name + " output",
+
+ /**
+ * The number of bytes of data which may be written to this pipe without
+ * blocking.
+ */
+ _writable: 0,
+
+ /**
+ * The increments in which pending data should be written, rather than
+ * simply defaulting to the amount requested (which, given that
+ * input.asyncWait precisely respects the requestedCount argument, will
+ * ordinarily always be writable in that amount), as an array whose
+ * elements from start to finish are the number of bytes to write each
+ * time write() or writeByteArray() is subsequently called. The sum of
+ * the values in this array, if this array is not empty, is always equal
+ * to this._writable.
+ */
+ _writableAmounts: [],
+
+ /**
+ * Data regarding a pending stream-ready callback on this, or null if no
+ * callback is currently waiting to be called.
+ */
+ _waiter: null,
+
+ /**
+ * The event currently dispatched to make a stream-ready callback, if any
+ * such callback is currently ready to be made and not already in
+ * progress, or null when no callback is waiting to happen.
+ */
+ _event: null,
+
+ /**
+ * A stream-ready constructor to wrap an existing callback to intercept
+ * stream-ready notifications, or null if notifications shouldn't be
+ * wrapped at all.
+ */
+ _streamReadyInterceptCreator: null,
+
+ /**
+ * Registers a stream-ready wrapper creator function so that a
+ * stream-ready callback made in the future can be wrapped.
+ */
+ interceptStreamReadyCallbacks(streamReadyInterceptCreator) {
+ dumpn("*** [" + this.name + "].interceptStreamReadyCallbacks");
+
+ Assert.ok(
+ this._streamReadyInterceptCreator !== null,
+ "intercepting onOutputStreamReady twice"
+ );
+ this._streamReadyInterceptCreator = streamReadyInterceptCreator;
+ if (this._waiter) {
+ this._waiter.callback = new streamReadyInterceptCreator(
+ this._waiter.callback
+ );
+ }
+ },
+
+ /**
+ * Removes a previously-registered stream-ready wrapper creator function,
+ * also clearing any current wrapping.
+ */
+ removeStreamReadyInterceptor() {
+ dumpn("*** [" + this.name + "].removeStreamReadyInterceptor()");
+
+ Assert.ok(
+ this._streamReadyInterceptCreator !== null,
+ "removing interceptor when none present?"
+ );
+ this._streamReadyInterceptCreator = null;
+ if (this._waiter) {
+ this._waiter.callback = this._waiter.callback.wrappedCallback;
+ }
+ },
+
+ //
+ // see nsIAsyncOutputStream.asyncWait
+ //
+ asyncWait: function asyncWait(callback, flags, requestedCount, target) {
+ dumpn("*** [" + this.name + "].asyncWait");
+
+ Assert.ok(callback && typeof callback !== "function");
+
+ var closureOnly =
+ (flags & Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY) !== 0;
+
+ Assert.ok(
+ this._waiter === null || (this._waiter.closureOnly && !closureOnly),
+ "asyncWait already called with a non-closure-only " +
+ "callback? unexpected!"
+ );
+
+ this._waiter = {
+ callback: this._streamReadyInterceptCreator
+ ? new this._streamReadyInterceptCreator(callback)
+ : callback,
+ closureOnly,
+ requestedCount,
+ eventTarget: target,
+ toString: function toString() {
+ return (
+ "waiter(" +
+ (closureOnly ? "closure only, " : "") +
+ "requestedCount: " +
+ requestedCount +
+ ", target: " +
+ target +
+ ")"
+ );
+ },
+ };
+
+ if (
+ (!closureOnly && this._writable >= requestedCount) ||
+ !Components.isSuccessCode(this.status)
+ ) {
+ this._notify();
+ }
+ },
+
+ //
+ // see nsIAsyncOutputStream.closeWithStatus
+ //
+ closeWithStatus: function closeWithStatus(status) {
+ dumpn("*** [" + this.name + "].closeWithStatus(" + status + ")");
+
+ if (!Components.isSuccessCode(self._status)) {
+ dumpn(
+ "*** ignoring redundant closure of [input " +
+ this.name +
+ "] " +
+ "because it's already closed (status " +
+ self._status +
+ ")"
+ );
+ return;
+ }
+
+ if (Components.isSuccessCode(status)) {
+ status = Cr.NS_BASE_STREAM_CLOSED;
+ }
+
+ self._status = status;
+
+ if (input._waiter) {
+ input._notify();
+ }
+ if (this._waiter) {
+ this._notify();
+ }
+ },
+
+ //
+ // see nsIBinaryOutputStream.writeByteArray
+ //
+ writeByteArray: function writeByteArray(bytes) {
+ dumpn(`*** [${this.name}].writeByteArray([${bytes}])`);
+
+ if (!Components.isSuccessCode(self._status)) {
+ throw self._status;
+ }
+
+ Assert.equal(
+ this._writableAmounts.length,
+ 0,
+ "writeByteArray can't support specified-length writes"
+ );
+
+ if (this._writable < bytes.length) {
+ throw Components.Exception("", Cr.NS_BASE_STREAM_WOULD_BLOCK);
+ }
+
+ self._data.push.apply(self._data, bytes);
+ this._writable -= bytes.length;
+
+ if (
+ input._readable === Infinity &&
+ input._waiter &&
+ !input._waiter.closureOnly
+ ) {
+ input._notify();
+ }
+ },
+
+ //
+ // see nsIOutputStream.write
+ //
+ write: function write(str, length) {
+ dumpn("*** [" + this.name + "].write");
+
+ Assert.equal(str.length, length, "sanity");
+ if (!Components.isSuccessCode(self._status)) {
+ throw self._status;
+ }
+ if (this._writable === 0) {
+ throw Components.Exception("", Cr.NS_BASE_STREAM_WOULD_BLOCK);
+ }
+
+ var actualWritten;
+ if (this._writableAmounts.length === 0) {
+ actualWritten = Math.min(this._writable, length);
+ } else {
+ Assert.ok(
+ this._writable >= this._writableAmounts[0],
+ "writable amounts value greater than writable data?"
+ );
+ Assert.equal(
+ this._writable,
+ sum(this._writableAmounts),
+ "total writable amount not equal to sum of writable increments"
+ );
+ actualWritten = this._writableAmounts.shift();
+ }
+
+ var bytes = str
+ .substring(0, actualWritten)
+ .split("")
+ .map(function (v) {
+ return v.charCodeAt(0);
+ });
+
+ self._data.push.apply(self._data, bytes);
+ this._writable -= actualWritten;
+
+ if (
+ input._readable === Infinity &&
+ input._waiter &&
+ !input._waiter.closureOnly
+ ) {
+ input._notify();
+ }
+
+ return actualWritten;
+ },
+
+ /**
+ * Increase the amount of data that can be written without blocking by the
+ * given number of bytes, triggering future notifications when required.
+ *
+ * @param count : uint
+ * the number of bytes of additional data to make writable
+ */
+ makeWritable: function makeWritable(count) {
+ dumpn("*** [" + this.name + "].makeWritable(" + count + ")");
+
+ Assert.ok(Components.isSuccessCode(self._status));
+
+ this._writable += count;
+
+ var waiter = this._waiter;
+ if (
+ waiter &&
+ !waiter.closureOnly &&
+ waiter.requestedCount <= this._writable
+ ) {
+ this._notify();
+ }
+ },
+
+ /**
+ * Increase the amount of data that can be written without blocking, but
+ * do so by specifying a number of bytes that will be written each time
+ * a write occurs, even as asyncWait notifications are initially triggered
+ * as usual. Thus, rather than writes eagerly writing everything possible
+ * at each step, attempts to write out data by segment devolve into a
+ * partial segment write, then another, and so on until the amount of data
+ * specified as permitted to be written, has been written.
+ *
+ * Note that the writeByteArray method is incompatible with the previous
+ * calling of this method, in that, until all increments provided to this
+ * method have been consumed, writeByteArray cannot be called. Once all
+ * increments have been consumed, writeByteArray may again be called.
+ *
+ * @param increments : [uint]
+ * an array whose elements are positive numbers of bytes to permit to be
+ * written each time write() is subsequently called on this, ignoring
+ * the total amount of writable space specified by the sum of all
+ * increments
+ */
+ makeWritableByIncrements: function makeWritableByIncrements(increments) {
+ dumpn(
+ "*** [" +
+ this.name +
+ "].makeWritableByIncrements" +
+ "([" +
+ increments.join(", ") +
+ "])"
+ );
+
+ Assert.greater(increments.length, 0, "bad increments");
+ Assert.ok(
+ increments.every(function (v) {
+ return v > 0;
+ }),
+ "zero increment?"
+ );
+
+ Assert.ok(Components.isSuccessCode(self._status));
+
+ this._writable += sum(increments);
+ this._writableAmounts = increments;
+
+ var waiter = this._waiter;
+ if (
+ waiter &&
+ !waiter.closureOnly &&
+ waiter.requestedCount <= this._writable
+ ) {
+ this._notify();
+ }
+ },
+
+ /**
+ * Dispatches an event to call a previously-registered stream-ready
+ * callback.
+ */
+ _notify: function _notify() {
+ dumpn("*** [" + this.name + "]._notify()");
+
+ var waiter = this._waiter;
+ Assert.ok(waiter !== null, "no waiter?");
+
+ if (this._event === null) {
+ var event = (this._event = {
+ run: function run() {
+ output._waiter = null;
+ output._event = null;
+
+ try {
+ waiter.callback.onOutputStreamReady(output);
+ } catch (e) {
+ do_throw("error calling onOutputStreamReady: " + e);
+ }
+ },
+ });
+ waiter.eventTarget.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
+ }
+ },
+ });
+}
+
+/**
+ * Represents a sequence of interactions to perform with a copier, in a given
+ * order and at the desired time intervals.
+ *
+ * @param name : string
+ * test name, used in debugging output
+ */
+function CopyTest(name, next) {
+ /** Name used in debugging output. */
+ this.name = name;
+
+ /** A function called when the test completes. */
+ this._done = next;
+
+ var sourcePipe = new CustomPipe(name + "-source");
+
+ /** The source of data for the copier to copy. */
+ this._source = sourcePipe.inputStream;
+
+ /**
+ * The sink to which to write data which will appear in the copier's source.
+ */
+ this._copyableDataStream = sourcePipe.outputStream;
+
+ var sinkPipe = new CustomPipe(name + "-sink");
+
+ /** The sink to which the copier copies data. */
+ this._sink = sinkPipe.outputStream;
+
+ /** Input stream from which to read data the copier's written to its sink. */
+ this._copiedDataStream = sinkPipe.inputStream;
+
+ this._copiedDataStream.disableReadabilityLimit();
+
+ /**
+ * True if there's a callback waiting to read data written by the copier to
+ * its output, from the input end of the pipe representing the copier's sink.
+ */
+ this._waitingForData = false;
+
+ /**
+ * An array of the bytes of data expected to be written to output by the
+ * copier when this test runs.
+ */
+ this._expectedData = undefined;
+
+ /** Array of bytes of data received so far. */
+ this._receivedData = [];
+
+ /** The expected final status returned by the copier. */
+ this._expectedStatus = -1;
+
+ /** The actual final status returned by the copier. */
+ this._actualStatus = -1;
+
+ /** The most recent sequence of bytes written to output by the copier. */
+ this._lastQuantum = [];
+
+ /**
+ * True iff we've received the last quantum of data written to the sink by the
+ * copier.
+ */
+ this._allDataWritten = false;
+
+ /**
+ * True iff the copier has notified its associated stream listener of
+ * completion.
+ */
+ this._copyingFinished = false;
+
+ /** Index of the next task to execute while driving the copier. */
+ this._currentTask = 0;
+
+ /** Array containing all tasks to run. */
+ this._tasks = [];
+
+ /** The copier used by this test. */
+ this._copier = new WriteThroughCopier(this._source, this._sink, this, null);
+
+ // Start watching for data written by the copier to the sink.
+ this._waitForWrittenData();
+}
+CopyTest.prototype = {
+ /**
+ * Adds the given array of bytes to data in the copier's source.
+ *
+ * @param bytes : [uint]
+ * array of bytes of data to add to the source for the copier
+ */
+ addToSource: function addToSource(bytes) {
+ var self = this;
+ this._addToTasks(function addToSourceTask() {
+ note("addToSourceTask");
+
+ try {
+ self._copyableDataStream.makeWritable(bytes.length);
+ self._copyableDataStream.writeByteArray(bytes);
+ } finally {
+ self._stageNextTask();
+ }
+ });
+ },
+
+ /**
+ * Makes bytes of data previously added to the source available to be read by
+ * the copier.
+ *
+ * @param count : uint
+ * number of bytes to make available for reading
+ */
+ makeSourceReadable: function makeSourceReadable(count) {
+ var self = this;
+ this._addToTasks(function makeSourceReadableTask() {
+ note("makeSourceReadableTask");
+
+ self._source.makeReadable(count);
+ self._stageNextTask();
+ });
+ },
+
+ /**
+ * Increases available space in the sink by the given amount, waits for the
+ * given series of arrays of bytes to be written to sink by the copier, and
+ * causes execution to asynchronously continue to the next task when the last
+ * of those arrays of bytes is received.
+ *
+ * @param bytes : uint
+ * number of bytes of space to make available in the sink
+ * @param dataQuantums : [[uint]]
+ * array of byte arrays to expect to be written in sequence to the sink
+ */
+ makeSinkWritableAndWaitFor: function makeSinkWritableAndWaitFor(
+ bytes,
+ dataQuantums
+ ) {
+ var self = this;
+
+ Assert.equal(
+ bytes,
+ dataQuantums.reduce(function (partial, current) {
+ return partial + current.length;
+ }, 0),
+ "bytes/quantums mismatch"
+ );
+
+ function increaseSinkSpaceTask() {
+ /* Now do the actual work to trigger the interceptor. */
+ self._sink.makeWritable(bytes);
+ }
+
+ this._waitForHelper(
+ "increaseSinkSpaceTask",
+ dataQuantums,
+ increaseSinkSpaceTask
+ );
+ },
+
+ /**
+ * Increases available space in the sink by the given amount, waits for the
+ * given series of arrays of bytes to be written to sink by the copier, and
+ * causes execution to asynchronously continue to the next task when the last
+ * of those arrays of bytes is received.
+ *
+ * @param bytes : uint
+ * number of bytes of space to make available in the sink
+ * @param dataQuantums : [[uint]]
+ * array of byte arrays to expect to be written in sequence to the sink
+ */
+ makeSinkWritableByIncrementsAndWaitFor:
+ function makeSinkWritableByIncrementsAndWaitFor(bytes, dataQuantums) {
+ var self = this;
+
+ var desiredAmounts = dataQuantums.map(function (v) {
+ return v.length;
+ });
+ Assert.equal(bytes, sum(desiredAmounts), "bytes/quantums mismatch");
+
+ function increaseSinkSpaceByIncrementsTask() {
+ /* Now do the actual work to trigger the interceptor incrementally. */
+ self._sink.makeWritableByIncrements(desiredAmounts);
+ }
+
+ this._waitForHelper(
+ "increaseSinkSpaceByIncrementsTask",
+ dataQuantums,
+ increaseSinkSpaceByIncrementsTask
+ );
+ },
+
+ /**
+ * Close the copier's source stream, then asynchronously continue to the next
+ * task.
+ *
+ * @param status : nsresult
+ * the status to provide when closing the copier's source stream
+ */
+ closeSource: function closeSource(status) {
+ var self = this;
+
+ this._addToTasks(function closeSourceTask() {
+ note("closeSourceTask");
+
+ self._source.closeWithStatus(status);
+ self._stageNextTask();
+ });
+ },
+
+ /**
+ * Close the copier's source stream, then wait for the given number of bytes
+ * and for the given series of arrays of bytes to be written to the sink, then
+ * asynchronously continue to the next task.
+ *
+ * @param status : nsresult
+ * the status to provide when closing the copier's source stream
+ * @param bytes : uint
+ * number of bytes of space to make available in the sink
+ * @param dataQuantums : [[uint]]
+ * array of byte arrays to expect to be written in sequence to the sink
+ */
+ closeSourceAndWaitFor: function closeSourceAndWaitFor(
+ status,
+ bytes,
+ dataQuantums
+ ) {
+ var self = this;
+
+ Assert.equal(
+ bytes,
+ sum(
+ dataQuantums.map(function (v) {
+ return v.length;
+ })
+ ),
+ "bytes/quantums mismatch"
+ );
+
+ function closeSourceAndWaitForTask() {
+ self._sink.makeWritable(bytes);
+ self._copyableDataStream.closeWithStatus(status);
+ }
+
+ this._waitForHelper(
+ "closeSourceAndWaitForTask",
+ dataQuantums,
+ closeSourceAndWaitForTask
+ );
+ },
+
+ /**
+ * Closes the copier's sink stream, providing the given status, then
+ * asynchronously continue to the next task.
+ *
+ * @param status : nsresult
+ * the status to provide when closing the copier's sink stream
+ */
+ closeSink: function closeSink(status) {
+ var self = this;
+ this._addToTasks(function closeSinkTask() {
+ note("closeSinkTask");
+
+ self._sink.closeWithStatus(status);
+ self._stageNextTask();
+ });
+ },
+
+ /**
+ * Closes the copier's source stream, then immediately closes the copier's
+ * sink stream, then asynchronously continues to the next task.
+ *
+ * @param sourceStatus : nsresult
+ * the status to provide when closing the copier's source stream
+ * @param sinkStatus : nsresult
+ * the status to provide when closing the copier's sink stream
+ */
+ closeSourceThenSink: function closeSourceThenSink(sourceStatus, sinkStatus) {
+ var self = this;
+ this._addToTasks(function closeSourceThenSinkTask() {
+ note("closeSourceThenSinkTask");
+
+ self._source.closeWithStatus(sourceStatus);
+ self._sink.closeWithStatus(sinkStatus);
+ self._stageNextTask();
+ });
+ },
+
+ /**
+ * Closes the copier's sink stream, then immediately closes the copier's
+ * source stream, then asynchronously continues to the next task.
+ *
+ * @param sinkStatus : nsresult
+ * the status to provide when closing the copier's sink stream
+ * @param sourceStatus : nsresult
+ * the status to provide when closing the copier's source stream
+ */
+ closeSinkThenSource: function closeSinkThenSource(sinkStatus, sourceStatus) {
+ var self = this;
+ this._addToTasks(function closeSinkThenSourceTask() {
+ note("closeSinkThenSource");
+
+ self._sink.closeWithStatus(sinkStatus);
+ self._source.closeWithStatus(sourceStatus);
+ self._stageNextTask();
+ });
+ },
+
+ /**
+ * Indicates that the given status is expected to be returned when the stream
+ * listener for the copy indicates completion, that the expected data copied
+ * by the copier to sink are the concatenation of the arrays of bytes in
+ * receivedData, and kicks off the tasks in this test.
+ *
+ * @param expectedStatus : nsresult
+ * the status expected to be returned by the copier at completion
+ * @param receivedData : [[uint]]
+ * an array containing arrays of bytes whose concatenation constitutes the
+ * expected copied data
+ */
+ expect: function expect(expectedStatus, receivedData) {
+ this._expectedStatus = expectedStatus;
+ this._expectedData = [];
+ for (var i = 0, sz = receivedData.length; i < sz; i++) {
+ this._expectedData.push.apply(this._expectedData, receivedData[i]);
+ }
+
+ this._stageNextTask();
+ },
+
+ /**
+ * Sets up a stream interceptor that will verify that each piece of data
+ * written to the sink by the copier corresponds to the currently expected
+ * pieces of data, calls the trigger, then waits for those pieces of data to
+ * be received. Once all have been received, the interceptor is removed and
+ * the next task is asynchronously executed.
+ *
+ * @param name : string
+ * name of the task created by this, used in debugging output
+ * @param dataQuantums : [[uint]]
+ * array of expected arrays of bytes to be written to the sink by the copier
+ * @param trigger : function() : void
+ * function to call after setting up the interceptor to wait for
+ * notifications (which will be generated as a result of this function's
+ * actions)
+ */
+ _waitForHelper: function _waitForHelper(name, dataQuantums, trigger) {
+ var self = this;
+ this._addToTasks(function waitForHelperTask() {
+ note(name);
+
+ var quantumIndex = 0;
+
+ /*
+ * Intercept all data-available notifications so we can continue when all
+ * the ones we expect have been received.
+ */
+ var streamReadyCallback = {
+ onInputStreamReady: function wrapperOnInputStreamReady(input) {
+ dumpn(
+ "*** streamReadyCallback.onInputStreamReady" +
+ "(" +
+ input.name +
+ ")"
+ );
+
+ Assert.equal(this, streamReadyCallback, "sanity");
+
+ try {
+ if (quantumIndex < dataQuantums.length) {
+ var quantum = dataQuantums[quantumIndex++];
+ var sz = quantum.length;
+ Assert.equal(
+ self._lastQuantum.length,
+ sz,
+ "different quantum lengths"
+ );
+ for (var i = 0; i < sz; i++) {
+ Assert.equal(
+ self._lastQuantum[i],
+ quantum[i],
+ "bad data at " + i
+ );
+ }
+
+ dumpn(
+ "*** waiting to check remaining " +
+ (dataQuantums.length - quantumIndex) +
+ " quantums..."
+ );
+ }
+ } finally {
+ if (quantumIndex === dataQuantums.length) {
+ dumpn("*** data checks completed! next task...");
+ self._copiedDataStream.removeStreamReadyInterceptor();
+ self._stageNextTask();
+ }
+ }
+ },
+ };
+
+ var interceptor = createStreamReadyInterceptor(
+ streamReadyCallback,
+ "onInputStreamReady"
+ );
+ self._copiedDataStream.interceptStreamReadyCallbacks(interceptor);
+
+ /* Do the deed. */
+ trigger();
+ });
+ },
+
+ /**
+ * Initiates asynchronous waiting for data written to the copier's sink to be
+ * available for reading from the input end of the sink's pipe. The callback
+ * stores the received data for comparison in the interceptor used in the
+ * callback added by _waitForHelper and signals test completion when it
+ * receives a zero-data-available notification (if the copier has notified
+ * that it is finished; otherwise allows execution to continue until that has
+ * occurred).
+ */
+ _waitForWrittenData: function _waitForWrittenData() {
+ dumpn("*** _waitForWrittenData (" + this.name + ")");
+
+ var self = this;
+ var outputWrittenWatcher = {
+ onInputStreamReady: function onInputStreamReady(input) {
+ dumpn(
+ // eslint-disable-next-line no-useless-concat
+ "*** outputWrittenWatcher.onInputStreamReady" + "(" + input.name + ")"
+ );
+
+ if (self._allDataWritten) {
+ do_throw(
+ "ruh-roh! why are we getting notified of more data " +
+ "after we should have received all of it?"
+ );
+ }
+
+ self._waitingForData = false;
+
+ try {
+ var avail = input.available();
+ } catch (e) {
+ dumpn("*** available() threw! error: " + e);
+ if (self._completed) {
+ dumpn(
+ "*** NB: this isn't a problem, because we've copied " +
+ "completely now, and this notify may have been expedited " +
+ "by maybeNotifyFinally such that we're being called when " +
+ "we can *guarantee* nothing is available any more"
+ );
+ }
+ avail = 0;
+ }
+
+ if (avail > 0) {
+ var data = input.readByteArray(avail);
+ Assert.equal(
+ data.length,
+ avail,
+ "readByteArray returned wrong number of bytes?"
+ );
+ self._lastQuantum = data;
+ self._receivedData.push.apply(self._receivedData, data);
+ }
+
+ if (avail === 0) {
+ dumpn("*** all data received!");
+
+ self._allDataWritten = true;
+
+ if (self._copyingFinished) {
+ dumpn("*** copying already finished, continuing to next test");
+ self._testComplete();
+ } else {
+ dumpn("*** copying not finished, waiting for that to happen");
+ }
+
+ return;
+ }
+
+ self._waitForWrittenData();
+ },
+ };
+
+ this._copiedDataStream.asyncWait(
+ outputWrittenWatcher,
+ 0,
+ 1,
+ Services.tm.currentThread
+ );
+ this._waitingForData = true;
+ },
+
+ /**
+ * Indicates this test is complete, does the final data-received and copy
+ * status comparisons, and calls the test-completion function provided when
+ * this test was first created.
+ */
+ _testComplete: function _testComplete() {
+ dumpn("*** CopyTest(" + this.name + ") complete! On to the next test...");
+
+ try {
+ Assert.ok(this._allDataWritten, "expect all data written now!");
+ Assert.ok(this._copyingFinished, "expect copying finished now!");
+
+ Assert.equal(
+ this._actualStatus,
+ this._expectedStatus,
+ "wrong final status"
+ );
+
+ var expected = this._expectedData,
+ received = this._receivedData;
+ dumpn("received: [" + received + "], expected: [" + expected + "]");
+ Assert.equal(received.length, expected.length, "wrong data");
+ for (var i = 0, sz = expected.length; i < sz; i++) {
+ Assert.equal(received[i], expected[i], "bad data at " + i);
+ }
+ } catch (e) {
+ dumpn("!!! ERROR PERFORMING FINAL " + this.name + " CHECKS! " + e);
+ throw e;
+ } finally {
+ dumpn(
+ "*** CopyTest(" +
+ this.name +
+ ") complete! " +
+ "Invoking test-completion callback..."
+ );
+ this._done();
+ }
+ },
+
+ /** Dispatches an event at this thread which will run the next task. */
+ _stageNextTask: function _stageNextTask() {
+ dumpn("*** CopyTest(" + this.name + ")._stageNextTask()");
+
+ if (this._currentTask === this._tasks.length) {
+ dumpn("*** CopyTest(" + this.name + ") tasks complete!");
+ return;
+ }
+
+ var task = this._tasks[this._currentTask++];
+ var event = {
+ run: function run() {
+ try {
+ task();
+ } catch (e) {
+ do_throw("exception thrown running task: " + e);
+ }
+ },
+ };
+ Services.tm.dispatchToMainThread(event);
+ },
+
+ /**
+ * Adds the given function as a task to be run at a later time.
+ *
+ * @param task : function() : void
+ * the function to call as a task
+ */
+ _addToTasks: function _addToTasks(task) {
+ this._tasks.push(task);
+ },
+
+ //
+ // see nsIRequestObserver.onStartRequest
+ //
+ onStartRequest: function onStartRequest(self) {
+ dumpn("*** CopyTest.onStartRequest (" + self.name + ")");
+
+ Assert.equal(this._receivedData.length, 0);
+ Assert.equal(this._lastQuantum.length, 0);
+ },
+
+ //
+ // see nsIRequestObserver.onStopRequest
+ //
+ onStopRequest: function onStopRequest(self, status) {
+ dumpn("*** CopyTest.onStopRequest (" + self.name + ", " + status + ")");
+
+ this._actualStatus = status;
+
+ this._copyingFinished = true;
+
+ if (this._allDataWritten) {
+ dumpn("*** all data written, continuing with remaining tests...");
+ this._testComplete();
+ } else {
+ /*
+ * Everything's copied as far as the copier is concerned. However, there
+ * may be a backup transferring from the output end of the copy sink to
+ * the input end where we can actually verify that the expected data was
+ * written as expected, because that transfer occurs asynchronously. If
+ * we do final data-received checks now, we'll miss still-pending data.
+ * Therefore, to wrap up this copy test we still need to asynchronously
+ * wait on the input end of the sink until we hit end-of-stream or some
+ * error condition. Then we know we're done and can continue with the
+ * next test.
+ */
+ dumpn("*** not all data copied, waiting for that to happen...");
+
+ if (!this._waitingForData) {
+ this._waitForWrittenData();
+ }
+
+ this._copiedDataStream.maybeNotifyFinally();
+ }
+ },
+};
diff --git a/netwerk/test/httpserver/test/test_basic_functionality.js b/netwerk/test/httpserver/test/test_basic_functionality.js
new file mode 100644
index 0000000000..b8864abd17
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_basic_functionality.js
@@ -0,0 +1,182 @@
+/* -*- 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/. */
+
+/*
+ * Basic functionality test, from the client programmer's POV.
+ */
+
+ChromeUtils.defineLazyGetter(this, "port", function () {
+ return srv.identity.primaryPort;
+});
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(
+ "http://localhost:" + port + "/objHandler",
+ null,
+ start_objHandler,
+ null
+ ),
+ new Test(
+ "http://localhost:" + port + "/functionHandler",
+ null,
+ start_functionHandler,
+ null
+ ),
+ new Test(
+ "http://localhost:" + port + "/nonexistent-path",
+ null,
+ start_non_existent_path,
+ null
+ ),
+ new Test(
+ "http://localhost:" + port + "/lotsOfHeaders",
+ null,
+ start_lots_of_headers,
+ null
+ ),
+ ];
+});
+
+var srv;
+
+function run_test() {
+ srv = createServer();
+
+ // base path
+ // XXX should actually test this works with a file by comparing streams!
+ var path = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ srv.registerDirectory("/", path);
+
+ // register a few test paths
+ srv.registerPathHandler("/objHandler", objHandler);
+ srv.registerPathHandler("/functionHandler", functionHandler);
+ srv.registerPathHandler("/lotsOfHeaders", lotsOfHeadersHandler);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+const HEADER_COUNT = 1000;
+
+// TEST DATA
+
+// common properties *always* appended by server
+// or invariants for every URL in paths
+function commonCheck(ch) {
+ Assert.ok(ch.contentLength > -1);
+ Assert.equal(ch.getResponseHeader("connection"), "close");
+ Assert.ok(!ch.isNoStoreResponse());
+ Assert.ok(!ch.isPrivateResponse());
+}
+
+function start_objHandler(ch) {
+ commonCheck(ch);
+
+ Assert.equal(ch.responseStatus, 200);
+ Assert.ok(ch.requestSucceeded);
+ Assert.equal(ch.getResponseHeader("content-type"), "text/plain");
+ Assert.equal(ch.responseStatusText, "OK");
+
+ var reqMin = {},
+ reqMaj = {},
+ respMin = {},
+ respMaj = {};
+ ch.getRequestVersion(reqMaj, reqMin);
+ ch.getResponseVersion(respMaj, respMin);
+ Assert.ok(reqMaj.value == respMaj.value && reqMin.value == respMin.value);
+}
+
+function start_functionHandler(ch) {
+ commonCheck(ch);
+
+ Assert.equal(ch.responseStatus, 404);
+ Assert.ok(!ch.requestSucceeded);
+ Assert.equal(ch.getResponseHeader("foopy"), "quux-baz");
+ Assert.equal(ch.responseStatusText, "Page Not Found");
+
+ var reqMin = {},
+ reqMaj = {},
+ respMin = {},
+ respMaj = {};
+ ch.getRequestVersion(reqMaj, reqMin);
+ ch.getResponseVersion(respMaj, respMin);
+ Assert.ok(reqMaj.value == 1 && reqMin.value == 1);
+ Assert.ok(respMaj.value == 1 && respMin.value == 1);
+}
+
+function start_non_existent_path(ch) {
+ commonCheck(ch);
+
+ Assert.equal(ch.responseStatus, 404);
+ Assert.ok(!ch.requestSucceeded);
+}
+
+function start_lots_of_headers(ch) {
+ commonCheck(ch);
+
+ Assert.equal(ch.responseStatus, 200);
+ Assert.ok(ch.requestSucceeded);
+
+ for (var i = 0; i < HEADER_COUNT; i++) {
+ Assert.equal(ch.getResponseHeader("X-Header-" + i), "value " + i);
+ }
+}
+
+// PATH HANDLERS
+
+// /objHandler
+var objHandler = {
+ handle(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+
+ var body = "Request (slightly reformatted):\n\n";
+ body += metadata.method + " " + metadata.path;
+
+ Assert.equal(metadata.port, port);
+
+ if (metadata.queryString) {
+ body += "?" + metadata.queryString;
+ }
+
+ body += " HTTP/" + metadata.httpVersion + "\n";
+
+ var headEnum = metadata.headers;
+ while (headEnum.hasMoreElements()) {
+ var fieldName = headEnum
+ .getNext()
+ .QueryInterface(Ci.nsISupportsString).data;
+ body += fieldName + ": " + metadata.getHeader(fieldName) + "\n";
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIHttpRequestHandler"]),
+};
+
+// /functionHandler
+function functionHandler(metadata, response) {
+ response.setStatusLine("1.1", 404, "Page Not Found");
+ response.setHeader("foopy", "quux-baz", false);
+
+ Assert.equal(metadata.port, port);
+ Assert.equal(metadata.host, "localhost");
+ Assert.equal(metadata.path.charAt(0), "/");
+
+ var body = "this is text\n";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+// /lotsOfHeaders
+function lotsOfHeadersHandler(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+
+ for (var i = 0; i < HEADER_COUNT; i++) {
+ response.setHeader("X-Header-" + i, "value " + i, false);
+ }
+}
diff --git a/netwerk/test/httpserver/test/test_body_length.js b/netwerk/test/httpserver/test/test_body_length.js
new file mode 100644
index 0000000000..85f35d7441
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_body_length.js
@@ -0,0 +1,68 @@
+/* -*- 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/. */
+
+/*
+ * Tests that the Content-Length header in incoming requests is interpreted as
+ * a decimal number, even if it has the form (including leading zero) of an
+ * octal number.
+ */
+
+var srv;
+
+function run_test() {
+ srv = createServer();
+ srv.registerPathHandler("/content-length", contentLength);
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+const REQUEST_DATA = "12345678901234567";
+
+function contentLength(request, response) {
+ Assert.equal(request.method, "POST");
+ Assert.equal(request.getHeader("Content-Length"), "017");
+
+ var body = new ScriptableInputStream(request.bodyInputStream);
+
+ var avail;
+ var data = "";
+ while ((avail = body.available()) > 0) {
+ data += body.read(avail);
+ }
+
+ Assert.equal(data, REQUEST_DATA);
+}
+
+/** *************
+ * BEGIN TESTS *
+ ***************/
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(
+ "http://localhost:" + srv.identity.primaryPort + "/content-length",
+ init_content_length
+ ),
+ ];
+});
+
+function init_content_length(ch) {
+ var content = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ content.data = REQUEST_DATA;
+
+ ch.QueryInterface(Ci.nsIUploadChannel).setUploadStream(
+ content,
+ "text/plain",
+ REQUEST_DATA.length
+ );
+
+ // Override the values implicitly set by setUploadStream above.
+ ch.requestMethod = "POST";
+ ch.setRequestHeader("Content-Length", "017", false); // 17 bytes, not 15
+}
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 0000000000..ef92824734
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_byte_range.js
@@ -0,0 +1,272 @@
+/* -*- 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/. */
+
+// checks if a byte range request and non-byte range request retrieve the
+// correct data.
+
+var srv;
+ChromeUtils.defineLazyGetter(this, "PREFIX", function () {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ 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 + "/headers.txt",
+ init_byterange13,
+ start_byterange13,
+ null
+ ),
+ new Test(PREFIX + "/range.txt", null, start_normal, stop_normal),
+ ];
+});
+
+function run_test() {
+ srv = createServer();
+ var dir = do_get_file("data/ranges/");
+ srv.registerDirectory("/", dir);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+function start_normal(ch) {
+ Assert.equal(ch.responseStatus, 200);
+ Assert.equal(ch.getResponseHeader("Content-Length"), "21");
+ Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain");
+}
+
+function stop_normal(ch, status, data) {
+ Assert.equal(data.length, 21);
+ Assert.equal(data[0], 0x54);
+ Assert.equal(data[20], 0x0a);
+}
+
+function init_byterange(ch) {
+ ch.setRequestHeader("Range", "bytes=10-", false);
+}
+
+function start_byterange(ch) {
+ Assert.equal(ch.responseStatus, 206);
+ Assert.equal(ch.getResponseHeader("Content-Length"), "11");
+ Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain");
+ Assert.equal(ch.getResponseHeader("Content-Range"), "bytes 10-20/21");
+}
+
+function stop_byterange(ch, status, data) {
+ Assert.equal(data.length, 11);
+ Assert.equal(data[0], 0x64);
+ Assert.equal(data[10], 0x0a);
+}
+
+function init_byterange2(ch) {
+ ch.setRequestHeader("Range", "bytes=21-", false);
+}
+
+function start_byterange2(ch) {
+ Assert.equal(ch.responseStatus, 416);
+}
+
+function init_byterange3(ch) {
+ ch.setRequestHeader("Range", "bytes=10-15", false);
+}
+
+function start_byterange3(ch) {
+ Assert.equal(ch.responseStatus, 206);
+ Assert.equal(ch.getResponseHeader("Content-Length"), "6");
+ Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain");
+ Assert.equal(ch.getResponseHeader("Content-Range"), "bytes 10-15/21");
+}
+
+function stop_byterange3(ch, status, data) {
+ Assert.equal(data.length, 6);
+ Assert.equal(data[0], 0x64);
+ Assert.equal(data[1], 0x20);
+ Assert.equal(data[2], 0x62);
+ Assert.equal(data[3], 0x65);
+ Assert.equal(data[4], 0x20);
+ Assert.equal(data[5], 0x73);
+}
+
+function init_byterange4(ch) {
+ ch.setRequestHeader("Range", "xbytes=21-", false);
+}
+
+function start_byterange4(ch) {
+ Assert.equal(ch.responseStatus, 400);
+}
+
+function init_byterange5(ch) {
+ ch.setRequestHeader("Range", "bytes=-5", false);
+}
+
+function start_byterange5(ch) {
+ Assert.equal(ch.responseStatus, 206);
+}
+
+function stop_byterange5(ch, status, data) {
+ Assert.equal(data.length, 5);
+ Assert.equal(data[0], 0x65);
+ Assert.equal(data[1], 0x65);
+ Assert.equal(data[2], 0x6e);
+ Assert.equal(data[3], 0x2e);
+ Assert.equal(data[4], 0x0a);
+}
+
+function init_byterange6(ch) {
+ ch.setRequestHeader("Range", "bytes=15-12", false);
+}
+
+function start_byterange6(ch) {
+ Assert.equal(ch.responseStatus, 200);
+}
+
+function stop_byterange6(ch, status, data) {
+ Assert.equal(data.length, 21);
+ Assert.equal(data[0], 0x54);
+ Assert.equal(data[20], 0x0a);
+}
+
+function init_byterange7(ch) {
+ ch.setRequestHeader("Range", "bytes=0-5", false);
+}
+
+function start_byterange7(ch) {
+ Assert.equal(ch.responseStatus, 206);
+ Assert.equal(ch.getResponseHeader("Content-Length"), "6");
+ Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain");
+ Assert.equal(ch.getResponseHeader("Content-Range"), "bytes 0-5/21");
+}
+
+function stop_byterange7(ch, status, data) {
+ Assert.equal(data.length, 6);
+ Assert.equal(data[0], 0x54);
+ Assert.equal(data[1], 0x68);
+ Assert.equal(data[2], 0x69);
+ Assert.equal(data[3], 0x73);
+ Assert.equal(data[4], 0x20);
+ Assert.equal(data[5], 0x73);
+}
+
+function init_byterange8(ch) {
+ ch.setRequestHeader("Range", "bytes=20-21", false);
+}
+
+function start_byterange8(ch) {
+ Assert.equal(ch.responseStatus, 206);
+ Assert.equal(ch.getResponseHeader("Content-Range"), "bytes 20-20/21");
+}
+
+function stop_byterange8(ch, status, data) {
+ Assert.equal(data.length, 1);
+ Assert.equal(data[0], 0x0a);
+}
+
+function init_byterange9(ch) {
+ ch.setRequestHeader("Range", "bytes=020-021", false);
+}
+
+function start_byterange9(ch) {
+ Assert.equal(ch.responseStatus, 206);
+}
+
+function stop_byterange9(ch, status, data) {
+ Assert.equal(data.length, 1);
+ Assert.equal(data[0], 0x0a);
+}
+
+function init_byterange10(ch) {
+ ch.setRequestHeader("Range", "bytes=-", false);
+}
+
+function start_byterange10(ch) {
+ Assert.equal(ch.responseStatus, 400);
+}
+
+function init_byterange11(ch) {
+ ch.setRequestHeader("Range", "bytes=-500", false);
+}
+
+function start_byterange11(ch) {
+ Assert.equal(ch.responseStatus, 206);
+}
+
+function stop_byterange11(ch, status, data) {
+ Assert.equal(data.length, 21);
+ Assert.equal(data[0], 0x54);
+ Assert.equal(data[20], 0x0a);
+}
+
+function start_byterange12(ch) {
+ Assert.equal(ch.responseStatus, 200);
+ Assert.equal(ch.getResponseHeader("Content-Length"), "0");
+}
+
+function stop_byterange12(ch, status, data) {
+ Assert.equal(data.length, 0);
+}
+
+function init_byterange13(ch) {
+ ch.setRequestHeader("Range", "bytes=9999999-", false);
+}
+
+function start_byterange13(ch) {
+ Assert.equal(ch.responseStatus, 416);
+ Assert.equal(ch.getResponseHeader("X-SJS-Header"), "customized");
+}
diff --git a/netwerk/test/httpserver/test/test_cern_meta.js b/netwerk/test/httpserver/test/test_cern_meta.js
new file mode 100644
index 0000000000..3497a88ea7
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_cern_meta.js
@@ -0,0 +1,79 @@
+/* -*- 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/. */
+
+// exercises support for mod_cern_meta-style header/status line modification
+var srv;
+
+ChromeUtils.defineLazyGetter(this, "PREFIX", function () {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(PREFIX + "/test_both.html", null, start_testBoth, null),
+ new Test(
+ PREFIX + "/test_ctype_override.txt",
+ null,
+ start_test_ctype_override_txt,
+ null
+ ),
+ new Test(
+ PREFIX + "/test_status_override.html",
+ null,
+ start_test_status_override_html,
+ null
+ ),
+ new Test(
+ PREFIX + "/test_status_override_nodesc.txt",
+ null,
+ start_test_status_override_nodesc_txt,
+ null
+ ),
+ new Test(PREFIX + "/caret_test.txt^", null, start_caret_test_txt_, null),
+ ];
+});
+
+function run_test() {
+ srv = createServer();
+
+ var cernDir = do_get_file("data/cern_meta/");
+ srv.registerDirectory("/", cernDir);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+// TEST DATA
+
+function start_testBoth(ch) {
+ Assert.equal(ch.responseStatus, 501);
+ Assert.equal(ch.responseStatusText, "Unimplemented");
+
+ Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain");
+}
+
+function start_test_ctype_override_txt(ch) {
+ Assert.equal(ch.getResponseHeader("Content-Type"), "text/html");
+}
+
+function start_test_status_override_html(ch) {
+ Assert.equal(ch.responseStatus, 404);
+ Assert.equal(ch.responseStatusText, "Can't Find This");
+}
+
+function start_test_status_override_nodesc_txt(ch) {
+ Assert.equal(ch.responseStatus, 732);
+ Assert.equal(ch.responseStatusText, "");
+}
+
+function start_caret_test_txt_(ch) {
+ Assert.equal(ch.responseStatus, 500);
+ Assert.equal(ch.responseStatusText, "This Isn't A Server Error");
+
+ Assert.equal(ch.getResponseHeader("Foo-RFC"), "3092");
+ Assert.equal(ch.getResponseHeader("Shaving-Cream-Atom"), "Illudium Phosdex");
+}
diff --git a/netwerk/test/httpserver/test/test_default_index_handler.js b/netwerk/test/httpserver/test/test_default_index_handler.js
new file mode 100644
index 0000000000..efa02cb6e3
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_default_index_handler.js
@@ -0,0 +1,248 @@
+/* -*- 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/. */
+
+// checks for correct output with the default index handler, mostly to do
+// escaping checks -- highly dependent on the default index handler output
+// format
+
+var srv, dir, gDirEntries;
+
+ChromeUtils.defineLazyGetter(this, "BASE_URL", function () {
+ return "http://localhost:" + srv.identity.primaryPort + "/";
+});
+
+function run_test() {
+ createTestDirectory();
+
+ srv = createServer();
+ srv.registerDirectory("/", dir);
+
+ var nameDir = do_get_file("data/name-scheme/");
+ srv.registerDirectory("/bar/", nameDir);
+
+ srv.start(-1);
+
+ function done() {
+ do_test_pending();
+ destroyTestDirectory();
+ srv.stop(function () {
+ do_test_finished();
+ });
+ }
+
+ runHttpTests(tests, done);
+}
+
+function createTestDirectory() {
+ dir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ dir.append("index_handler_test_" + Math.random());
+ dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o744);
+
+ // populate with test directories, files, etc.
+ // Files must be in expected order of display on the index page!
+
+ var files = [];
+
+ makeFile("aa_directory", true, dir, files);
+ makeFile("Ba_directory", true, dir, files);
+ makeFile("bb_directory", true, dir, files);
+ makeFile("foo", true, dir, files);
+ makeFile("a_file", false, dir, files);
+ makeFile("B_file", false, dir, files);
+ makeFile("za'z", false, dir, files);
+ makeFile("zb&z", false, dir, files);
+ makeFile("zc<q", false, dir, files);
+ makeFile('zd"q', false, dir, files);
+ makeFile("ze%g", false, dir, files);
+ makeFile("zf%200h", false, dir, files);
+ makeFile("zg>m", false, dir, files);
+
+ gDirEntries = [files];
+
+ var subdir = dir.clone();
+ subdir.append("foo");
+
+ files = [];
+
+ makeFile("aa_dir", true, subdir, files);
+ makeFile("b_dir", true, subdir, files);
+ makeFile("AA_file.txt", false, subdir, files);
+ makeFile("test.txt", false, subdir, files);
+
+ gDirEntries.push(files);
+}
+
+function destroyTestDirectory() {
+ dir.remove(true);
+}
+
+/** ***********
+ * UTILITIES *
+ *************/
+
+/** Verifies data in bytes for the trailing-caret path above. */
+function hiddenDataCheck(bytes, uri, path) {
+ var data = String.fromCharCode.apply(null, bytes);
+
+ var parser = new DOMParser();
+
+ // Note: the index format isn't XML -- it's actually HTML -- but we require
+ // the index format also be valid XML, albeit XML without namespaces,
+ // XML declarations, etc. Doing this simplifies output checking.
+ try {
+ var doc = parser.parseFromString(data, "application/xml");
+ } catch (e) {
+ do_throw("document failed to parse as XML");
+ }
+
+ var body = doc.documentElement.getElementsByTagName("body");
+ Assert.equal(body.length, 1);
+ body = body[0];
+
+ // header
+ var header = body.getElementsByTagName("h1");
+ Assert.equal(header.length, 1);
+
+ Assert.equal(header[0].textContent, path);
+
+ // files
+ var lst = body.getElementsByTagName("ol");
+ Assert.equal(lst.length, 1);
+ var items = lst[0].getElementsByTagName("li");
+
+ var top = Services.io.newURI(uri);
+
+ // N.B. No ERROR_IF_SEE_THIS.txt^ file!
+ var dirEntries = [
+ { name: "file.txt", isDirectory: false },
+ { name: "SHOULD_SEE_THIS.txt^", isDirectory: false },
+ ];
+
+ for (var i = 0; i < items.length; i++) {
+ var link = items[i].childNodes[0];
+ var f = dirEntries[i];
+
+ var sep = f.isDirectory ? "/" : "";
+
+ Assert.equal(link.textContent, f.name + sep);
+
+ uri = Services.io.newURI(link.getAttribute("href"), null, top);
+ Assert.equal(decodeURIComponent(uri.pathQueryRef), path + f.name + sep);
+ }
+}
+
+/**
+ * Verifies data in bytes (an array of bytes) represents an index page for the
+ * given URI and path, which should be a page listing the given directory
+ * entries, in order.
+ *
+ * @param bytes
+ * array of bytes representing the index page's contents
+ * @param uri
+ * string which is the URI of the index page
+ * @param path
+ * the path portion of uri
+ * @param dirEntries
+ * sorted (in the manner the directory entries should be sorted) array of
+ * objects, each of which has a name property (whose value is the file's name,
+ * without / if it's a directory) and an isDirectory property (with expected
+ * value)
+ */
+function dataCheck(bytes, uri, path, dirEntries) {
+ var data = String.fromCharCode.apply(null, bytes);
+
+ var parser = new DOMParser();
+
+ // Note: the index format isn't XML -- it's actually HTML -- but we require
+ // the index format also be valid XML, albeit XML without namespaces,
+ // XML declarations, etc. Doing this simplifies output checking.
+ try {
+ var doc = parser.parseFromString(data, "application/xml");
+ } catch (e) {
+ do_throw("document failed to parse as XML");
+ }
+
+ var body = doc.documentElement.getElementsByTagName("body");
+ Assert.equal(body.length, 1);
+ body = body[0];
+
+ // header
+ var header = body.getElementsByTagName("h1");
+ Assert.equal(header.length, 1);
+
+ Assert.equal(header[0].textContent, path);
+
+ // files
+ var lst = body.getElementsByTagName("ol");
+ Assert.equal(lst.length, 1);
+ var items = lst[0].getElementsByTagName("li");
+ var top = Services.io.newURI(uri);
+
+ for (var i = 0; i < items.length; i++) {
+ var link = items[i].childNodes[0];
+ var f = dirEntries[i];
+
+ var sep = f.isDirectory ? "/" : "";
+
+ Assert.equal(link.textContent, f.name + sep);
+
+ uri = Services.io.newURI(link.getAttribute("href"), null, top);
+ Assert.equal(decodeURIComponent(uri.pathQueryRef), path + f.name + sep);
+ }
+}
+
+/**
+ * Create a file/directory with the given name underneath parentDir, and
+ * append an object with name/isDirectory properties to lst corresponding
+ * to it if the file/directory could be created.
+ */
+function makeFile(name, isDirectory, parentDir, lst) {
+ var type = Ci.nsIFile[isDirectory ? "DIRECTORY_TYPE" : "NORMAL_FILE_TYPE"];
+ var file = parentDir.clone();
+
+ try {
+ file.append(name);
+ file.create(type, 0o755);
+ lst.push({ name, isDirectory });
+ } catch (e) {
+ /* OS probably doesn't like file name, skip */
+ }
+}
+
+/** *******
+ * TESTS *
+ *********/
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(BASE_URL, null, start, stopRootDirectory),
+ new Test(BASE_URL + "foo/", null, start, stopFooDirectory),
+ new Test(
+ BASE_URL + "bar/folder^/",
+ null,
+ start,
+ stopTrailingCaretDirectory
+ ),
+ ];
+});
+
+// check top-level directory listing
+function start(ch) {
+ Assert.equal(ch.getResponseHeader("Content-Type"), "text/html;charset=utf-8");
+}
+function stopRootDirectory(ch, status, data) {
+ dataCheck(data, BASE_URL, "/", gDirEntries[0]);
+}
+
+// check non-top-level, too
+function stopFooDirectory(ch, status, data) {
+ dataCheck(data, BASE_URL + "foo/", "/foo/", gDirEntries[1]);
+}
+
+// trailing-caret leaf with hidden files
+function stopTrailingCaretDirectory(ch, status, data) {
+ hiddenDataCheck(data, BASE_URL + "bar/folder^/", "/bar/folder^/");
+}
diff --git a/netwerk/test/httpserver/test/test_empty_body.js b/netwerk/test/httpserver/test/test_empty_body.js
new file mode 100644
index 0000000000..fa9b7cbfdc
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_empty_body.js
@@ -0,0 +1,59 @@
+/* -*- 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/. */
+
+// in its original incarnation, the server didn't like empty response-bodies;
+// see the comment in _end for details
+
+var srv;
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(
+ "http://localhost:" + srv.identity.primaryPort + "/empty-body-unwritten",
+ null,
+ ensureEmpty,
+ null
+ ),
+ new Test(
+ "http://localhost:" + srv.identity.primaryPort + "/empty-body-written",
+ null,
+ ensureEmpty,
+ null
+ ),
+ ];
+});
+
+function run_test() {
+ srv = createServer();
+
+ // register a few test paths
+ srv.registerPathHandler("/empty-body-unwritten", emptyBodyUnwritten);
+ srv.registerPathHandler("/empty-body-written", emptyBodyWritten);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+// TEST DATA
+
+function ensureEmpty(ch) {
+ Assert.ok(ch.contentLength == 0);
+}
+
+// PATH HANDLERS
+
+// /empty-body-unwritten
+function emptyBodyUnwritten(metadata, response) {
+ response.setStatusLine("1.1", 200, "OK");
+}
+
+// /empty-body-written
+function emptyBodyWritten(metadata, response) {
+ response.setStatusLine("1.1", 200, "OK");
+ var body = "";
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/netwerk/test/httpserver/test/test_errorhandler_exception.js b/netwerk/test/httpserver/test/test_errorhandler_exception.js
new file mode 100644
index 0000000000..e9cdb0bf4f
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_errorhandler_exception.js
@@ -0,0 +1,95 @@
+/* -*- 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/. */
+
+// Request handlers may throw exceptions, and those exception should be caught
+// by the server and converted into the proper error codes.
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(
+ "http://localhost:" + srv.identity.primaryPort + "/throws/exception",
+ null,
+ start_throws_exception,
+ succeeded
+ ),
+ new Test(
+ "http://localhost:" +
+ srv.identity.primaryPort +
+ "/this/file/does/not/exist/and/404s",
+ null,
+ start_nonexistent_404_fails_so_400,
+ succeeded
+ ),
+ new Test(
+ "http://localhost:" +
+ srv.identity.primaryPort +
+ "/attempts/404/fails/so/400/fails/so/500s",
+ register400Handler,
+ start_multiple_exceptions_500,
+ succeeded
+ ),
+ ];
+});
+
+var srv;
+
+function run_test() {
+ srv = createServer();
+
+ srv.registerErrorHandler(404, throwsException);
+ srv.registerPathHandler("/throws/exception", throwsException);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+// TEST DATA
+
+function checkStatusLine(
+ channel,
+ httpMaxVer,
+ httpMinVer,
+ httpCode,
+ statusText
+) {
+ Assert.equal(channel.responseStatus, httpCode);
+ Assert.equal(channel.responseStatusText, statusText);
+
+ var respMaj = {},
+ respMin = {};
+ channel.getResponseVersion(respMaj, respMin);
+ Assert.equal(respMaj.value, httpMaxVer);
+ Assert.equal(respMin.value, httpMinVer);
+}
+
+function start_throws_exception(ch) {
+ checkStatusLine(ch, 1, 1, 500, "Internal Server Error");
+}
+
+function start_nonexistent_404_fails_so_400(ch) {
+ checkStatusLine(ch, 1, 1, 400, "Bad Request");
+}
+
+function start_multiple_exceptions_500(ch) {
+ checkStatusLine(ch, 1, 1, 500, "Internal Server Error");
+}
+
+function succeeded(ch, status, data) {
+ Assert.ok(Components.isSuccessCode(status));
+}
+
+function register400Handler(ch) {
+ srv.registerErrorHandler(400, throwsException);
+}
+
+// PATH HANDLERS
+
+// /throws/exception (and also a 404 and 400 error handler)
+function throwsException(metadata, response) {
+ throw new Error("this shouldn't cause an exit...");
+ do_throw("Not reached!"); // eslint-disable-line no-unreachable
+}
diff --git a/netwerk/test/httpserver/test/test_header_array.js b/netwerk/test/httpserver/test/test_header_array.js
new file mode 100644
index 0000000000..1579c5ceeb
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_header_array.js
@@ -0,0 +1,66 @@
+/* -*- 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/. */
+
+// test that special headers are sent as an array of headers with the same name
+
+var srv;
+
+function run_test() {
+ srv;
+
+ srv = createServer();
+ srv.registerPathHandler("/path-handler", pathHandler);
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+/** **********
+ * HANDLERS *
+ ************/
+
+function pathHandler(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ response.setHeader("Proxy-Authenticate", "First line 1", true);
+ response.setHeader("Proxy-Authenticate", "Second line 1", true);
+ response.setHeader("Proxy-Authenticate", "Third line 1", true);
+
+ response.setHeader("WWW-Authenticate", "Not merged line 1", false);
+ response.setHeader("WWW-Authenticate", "Not merged line 2", true);
+
+ response.setHeader("WWW-Authenticate", "First line 2", false);
+ response.setHeader("WWW-Authenticate", "Second line 2", true);
+ response.setHeader("WWW-Authenticate", "Third line 2", true);
+
+ response.setHeader("X-Single-Header-Merge", "Single 1", true);
+ response.setHeader("X-Single-Header-Merge", "Single 2", true);
+}
+
+/** *************
+ * BEGIN TESTS *
+ ***************/
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(
+ "http://localhost:" + srv.identity.primaryPort + "/path-handler",
+ null,
+ check
+ ),
+ ];
+});
+
+function check(ch) {
+ var headerValue;
+
+ headerValue = ch.getResponseHeader("Proxy-Authenticate");
+ Assert.equal(headerValue, "First line 1\nSecond line 1\nThird line 1");
+ headerValue = ch.getResponseHeader("WWW-Authenticate");
+ Assert.equal(headerValue, "First line 2\nSecond line 2\nThird line 2");
+ headerValue = ch.getResponseHeader("X-Single-Header-Merge");
+ Assert.equal(headerValue, "Single 1,Single 2");
+}
diff --git a/netwerk/test/httpserver/test/test_headers.js b/netwerk/test/httpserver/test/test_headers.js
new file mode 100644
index 0000000000..8e920c6f2f
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_headers.js
@@ -0,0 +1,169 @@
+/* -*- 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/. */
+
+// tests for header storage in httpd.js; nsHttpHeaders is an *internal* data
+// structure and is not to be used directly outside of httpd.js itself except
+// for testing purposes
+
+/**
+ * Ensures that a fieldname-fieldvalue combination is a valid header.
+ *
+ * @param fieldName
+ * the name of the header
+ * @param fieldValue
+ * the value of the header
+ * @param headers
+ * an nsHttpHeaders object to use to check validity
+ */
+function assertValidHeader(fieldName, fieldValue, headers) {
+ try {
+ headers.setHeader(fieldName, fieldValue, false);
+ } catch (e) {
+ do_throw("Unexpected exception thrown: " + e);
+ }
+}
+
+/**
+ * Ensures that a fieldname-fieldvalue combination is not a valid header.
+ *
+ * @param fieldName
+ * the name of the header
+ * @param fieldValue
+ * the value of the header
+ * @param headers
+ * an nsHttpHeaders object to use to check validity
+ */
+function assertInvalidHeader(fieldName, fieldValue, headers) {
+ try {
+ headers.setHeader(fieldName, fieldValue, false);
+ throw new Error(
+ `Setting (${fieldName}, ${fieldValue}) as header succeeded!`
+ );
+ } catch (e) {
+ if (e.result !== Cr.NS_ERROR_INVALID_ARG) {
+ do_throw("Unexpected exception thrown: " + e);
+ }
+ }
+}
+
+function run_test() {
+ testHeaderValidity();
+ testGetHeader();
+ testHeaderEnumerator();
+ testHasHeader();
+}
+
+function testHeaderValidity() {
+ var headers = new nsHttpHeaders();
+
+ assertInvalidHeader("f o", "bar", headers);
+ assertInvalidHeader("f\0n", "bar", headers);
+ assertInvalidHeader("foo:", "bar", headers);
+ assertInvalidHeader("f\\o", "bar", headers);
+ assertInvalidHeader("@xml", "bar", headers);
+ assertInvalidHeader("fiz(", "bar", headers);
+ assertInvalidHeader("HTTP/1.1", "bar", headers);
+ assertInvalidHeader('b"b', "bar", headers);
+ assertInvalidHeader("ascsd\t", "bar", headers);
+ assertInvalidHeader("{fds", "bar", headers);
+ assertInvalidHeader("baz?", "bar", headers);
+ assertInvalidHeader("a\\b\\c", "bar", headers);
+ assertInvalidHeader("\0x7F", "bar", headers);
+ assertInvalidHeader("\0x1F", "bar", headers);
+ assertInvalidHeader("f\n", "bar", headers);
+ assertInvalidHeader("foo", "b\nar", headers);
+ assertInvalidHeader("foo", "b\rar", headers);
+ assertInvalidHeader("foo", "b\0", headers);
+
+ // request splitting, fwiw -- we're actually immune to this type of attack so
+ // long as we don't implement persistent connections
+ assertInvalidHeader("f\r\nGET /badness HTTP/1.1\r\nFoo", "bar", headers);
+
+ assertValidHeader("f'", "baz", headers);
+ assertValidHeader("f`", "baz", headers);
+ assertValidHeader("f.", "baz", headers);
+ assertValidHeader("f---", "baz", headers);
+ assertValidHeader("---", "baz", headers);
+ assertValidHeader("~~~", "baz", headers);
+ assertValidHeader("~~~", "b\r\n bar", headers);
+ assertValidHeader("~~~", "b\r\n\tbar", headers);
+}
+
+function testGetHeader() {
+ var headers = new nsHttpHeaders();
+
+ headers.setHeader("Content-Type", "text/html", false);
+ var c = headers.getHeader("content-type");
+ Assert.equal(c, "text/html");
+
+ headers.setHeader("test", "FOO", false);
+ c = headers.getHeader("test");
+ Assert.equal(c, "FOO");
+
+ try {
+ headers.getHeader(":");
+ throw new Error("Failed to throw for invalid header");
+ } catch (e) {
+ if (e.result !== Cr.NS_ERROR_INVALID_ARG) {
+ do_throw("headers.getHeader(':') must throw invalid arg");
+ }
+ }
+
+ try {
+ headers.getHeader("valid");
+ throw new Error("header doesn't exist");
+ } catch (e) {
+ if (e.result !== Cr.NS_ERROR_NOT_AVAILABLE) {
+ do_throw("shouldn't be a header named 'valid' in headers!");
+ }
+ }
+}
+
+function testHeaderEnumerator() {
+ var headers = new nsHttpHeaders();
+
+ var heads = {
+ foo: "17",
+ baz: "two six niner",
+ decaf: "class Program { int .7; int main(){ .7 = 5; return 7 - .7; } }",
+ };
+
+ for (var i in heads) {
+ headers.setHeader(i, heads[i], false);
+ }
+
+ var en = headers.enumerator;
+ while (en.hasMoreElements()) {
+ var it = en.getNext().QueryInterface(Ci.nsISupportsString).data;
+ Assert.ok(it.toLowerCase() in heads);
+ delete heads[it.toLowerCase()];
+ }
+
+ if (Object.keys(heads).length) {
+ do_throw("still have properties in heads!?!?");
+ }
+}
+
+function testHasHeader() {
+ var headers = new nsHttpHeaders();
+
+ headers.setHeader("foo", "bar", false);
+ Assert.ok(headers.hasHeader("foo"));
+ Assert.ok(headers.hasHeader("fOo"));
+ Assert.ok(!headers.hasHeader("not-there"));
+
+ headers.setHeader("f`'~", "bar", false);
+ Assert.ok(headers.hasHeader("F`'~"));
+
+ try {
+ headers.hasHeader(":");
+ throw new Error("failed to throw");
+ } catch (e) {
+ if (e.result !== Cr.NS_ERROR_INVALID_ARG) {
+ do_throw(".hasHeader for an invalid name should throw");
+ }
+ }
+}
diff --git a/netwerk/test/httpserver/test/test_host.js b/netwerk/test/httpserver/test/test_host.js
new file mode 100644
index 0000000000..2f5fadde92
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_host.js
@@ -0,0 +1,608 @@
+/* -*- 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/. */
+
+/**
+ * Tests that the scheme, host, and port of the server are correctly recorded
+ * and used in HTTP requests and responses.
+ */
+
+"use strict";
+
+const PORT = 4444;
+const FAKE_PORT_ONE = 8888;
+const FAKE_PORT_TWO = 8889;
+
+let srv, id;
+
+add_task(async function run_test1() {
+ dump("*** run_test1");
+
+ srv = createServer();
+
+ srv.registerPathHandler("/http/1.0-request", http10Request);
+ srv.registerPathHandler("/http/1.1-good-host", http11goodHost);
+ srv.registerPathHandler(
+ "/http/1.1-good-host-wacky-port",
+ http11goodHostWackyPort
+ );
+ srv.registerPathHandler("/http/1.1-ip-host", http11ipHost);
+
+ srv.start(FAKE_PORT_ONE);
+
+ id = srv.identity;
+
+ // The default location is http://localhost:PORT, where PORT is whatever you
+ // provided when you started the server. http://127.0.0.1:PORT is also part
+ // of the default set of locations.
+ Assert.equal(id.primaryScheme, "http");
+ Assert.equal(id.primaryHost, "localhost");
+ Assert.equal(id.primaryPort, FAKE_PORT_ONE);
+ Assert.ok(id.has("http", "localhost", FAKE_PORT_ONE));
+ Assert.ok(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ // This should be a nop.
+ id.add("http", "localhost", FAKE_PORT_ONE);
+ Assert.equal(id.primaryScheme, "http");
+ Assert.equal(id.primaryHost, "localhost");
+ Assert.equal(id.primaryPort, FAKE_PORT_ONE);
+ Assert.ok(id.has("http", "localhost", FAKE_PORT_ONE));
+ Assert.ok(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ // Change the primary location and make sure all the getters work correctly.
+ id.setPrimary("http", "127.0.0.1", FAKE_PORT_ONE);
+ Assert.equal(id.primaryScheme, "http");
+ Assert.equal(id.primaryHost, "127.0.0.1");
+ Assert.equal(id.primaryPort, FAKE_PORT_ONE);
+ Assert.ok(id.has("http", "localhost", FAKE_PORT_ONE));
+ Assert.ok(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ // Okay, now remove the primary location -- we fall back to the original
+ // location.
+ id.remove("http", "127.0.0.1", FAKE_PORT_ONE);
+ Assert.equal(id.primaryScheme, "http");
+ Assert.equal(id.primaryHost, "localhost");
+ Assert.equal(id.primaryPort, FAKE_PORT_ONE);
+ Assert.ok(id.has("http", "localhost", FAKE_PORT_ONE));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ // You can't remove every location -- try this and the original default
+ // location will be silently readded.
+ id.remove("http", "localhost", FAKE_PORT_ONE);
+ Assert.equal(id.primaryScheme, "http");
+ Assert.equal(id.primaryHost, "localhost");
+ Assert.equal(id.primaryPort, FAKE_PORT_ONE);
+ Assert.ok(id.has("http", "localhost", FAKE_PORT_ONE));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ // Okay, now that we've exercised that behavior, shut down the server and
+ // restart it on the correct port, to exercise port-changing behaviors at
+ // server start and stop.
+
+ await new Promise(resolve => srv.stop(resolve));
+});
+
+add_task(async function run_test_2() {
+ dump("*** run_test_2");
+
+ // Our primary location is gone because it was dependent on the port on which
+ // the server was running.
+ checkPrimariesThrow(id);
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE));
+
+ srv.start(FAKE_PORT_TWO);
+
+ // We should have picked up http://localhost:8889 as our primary location now
+ // that we've restarted.
+ Assert.equal(id.primaryScheme, "http");
+ Assert.equal(id.primaryHost, "localhost");
+ Assert.equal(id.primaryPort, FAKE_PORT_TWO);
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+ Assert.ok(id.has("http", "localhost", FAKE_PORT_TWO));
+ Assert.ok(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+
+ // Now we're going to see what happens when we shut down with a primary
+ // location that wasn't a default. That location should persist, and the
+ // default we remove should still not be present.
+ id.setPrimary("http", "example.com", FAKE_PORT_TWO);
+ Assert.ok(id.has("http", "example.com", FAKE_PORT_TWO));
+ Assert.ok(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+ Assert.ok(id.has("http", "localhost", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE));
+
+ id.remove("http", "localhost", FAKE_PORT_TWO);
+ Assert.ok(id.has("http", "example.com", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_TWO));
+ Assert.ok(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ id.remove("http", "127.0.0.1", FAKE_PORT_TWO);
+ Assert.ok(id.has("http", "example.com", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ await new Promise(resolve => srv.stop(resolve));
+});
+
+add_task(async function run_test_3() {
+ dump("*** run_test_3");
+
+ // Only the default added location disappears; any others stay around,
+ // possibly as the primary location. We may have removed the default primary
+ // location, but the one we set manually should persist here.
+ Assert.equal(id.primaryScheme, "http");
+ Assert.equal(id.primaryHost, "example.com");
+ Assert.equal(id.primaryPort, FAKE_PORT_TWO);
+ Assert.ok(id.has("http", "example.com", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ srv.start(PORT);
+
+ // Starting always adds HTTP entries for 127.0.0.1:port and localhost:port.
+ Assert.ok(id.has("http", "example.com", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+ Assert.ok(id.has("http", "localhost", PORT));
+ Assert.ok(id.has("http", "127.0.0.1", PORT));
+
+ // Remove the primary location we'd left set from last time.
+ id.remove("http", "example.com", FAKE_PORT_TWO);
+
+ // Default-port behavior testing requires the server responds to requests
+ // claiming to be on one such port.
+ id.add("http", "localhost", 80);
+
+ // Make sure we don't have anything lying around from running on either the
+ // first or the second port -- all we should have is our generated default,
+ // plus the additional port to test "portless" hostport variants.
+ Assert.ok(id.has("http", "localhost", 80));
+ Assert.equal(id.primaryScheme, "http");
+ Assert.equal(id.primaryHost, "localhost");
+ Assert.equal(id.primaryPort, PORT);
+ Assert.ok(id.has("http", "localhost", PORT));
+ Assert.ok(id.has("http", "127.0.0.1", PORT));
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+ Assert.ok(!id.has("http", "example.com", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "localhost", FAKE_PORT_TWO));
+ Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+
+ // Okay, finally done with identity testing. Our primary location is the one
+ // we want it to be, so we're off!
+ await new Promise(resolve =>
+ runRawTests(tests, resolve, idx => dump(`running test no ${idx}`))
+ );
+
+ // Finally shut down the server.
+ await new Promise(resolve => srv.stop(resolve));
+});
+
+/** *******************
+ * UTILITY FUNCTIONS *
+ *********************/
+
+/**
+ * Verifies that all .primary* getters on a server identity correctly throw
+ * NS_ERROR_NOT_INITIALIZED.
+ *
+ * @param aId : nsIHttpServerIdentity
+ * the server identity to test
+ */
+function checkPrimariesThrow(aId) {
+ let threw = false;
+ try {
+ aId.primaryScheme;
+ } catch (e) {
+ threw = e.result === Cr.NS_ERROR_NOT_INITIALIZED;
+ }
+ Assert.ok(threw);
+
+ threw = false;
+ try {
+ aId.primaryHost;
+ } catch (e) {
+ threw = e.result === Cr.NS_ERROR_NOT_INITIALIZED;
+ }
+ Assert.ok(threw);
+
+ threw = false;
+ try {
+ aId.primaryPort;
+ } catch (e) {
+ threw = e.result === Cr.NS_ERROR_NOT_INITIALIZED;
+ }
+ Assert.ok(threw);
+}
+
+/**
+ * Utility function to check for a 400 response.
+ */
+function check400(aData) {
+ let iter = LineIterator(aData);
+
+ // Status-Line
+ let { value: firstLine } = iter.next();
+ Assert.equal(firstLine.substring(0, HTTP_400_LEADER_LENGTH), HTTP_400_LEADER);
+}
+
+/** *************
+ * BEGIN TESTS *
+ ***************/
+
+const HTTP_400_LEADER = "HTTP/1.1 400 ";
+const HTTP_400_LEADER_LENGTH = HTTP_400_LEADER.length;
+
+var test, data;
+var tests = [];
+
+// HTTP/1.0 request, to ensure we see our default scheme/host/port
+
+function http10Request(request, response) {
+ writeDetails(request, response);
+ response.setStatusLine("1.0", 200, "TEST PASSED");
+}
+data = "GET /http/1.0-request HTTP/1.0\r\n\r\n";
+function check10(aData) {
+ let iter = LineIterator(aData);
+
+ // Status-Line
+ Assert.equal(iter.next().value, "HTTP/1.0 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ let body = [
+ "Method: GET",
+ "Path: /http/1.0-request",
+ "Query: ",
+ "Version: 1.0",
+ "Scheme: http",
+ "Host: localhost",
+ "Port: 4444",
+ ];
+
+ expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check10);
+tests.push(test);
+
+// HTTP/1.1 request, no Host header, expect a 400 response
+
+// eslint-disable-next-line no-useless-concat
+data = "GET /http/1.1-request HTTP/1.1\r\n" + "\r\n";
+test = new RawTest("localhost", PORT, data, check400);
+tests.push(test);
+
+// HTTP/1.1 request, wrong host, expect a 400 response
+
+data =
+ // eslint-disable-next-line no-useless-concat
+ "GET /http/1.1-request HTTP/1.1\r\n" + "Host: not-localhost\r\n" + "\r\n";
+test = new RawTest("localhost", PORT, data, check400);
+tests.push(test);
+
+// HTTP/1.1 request, wrong host/right port, expect a 400 response
+
+data =
+ "GET /http/1.1-request HTTP/1.1\r\n" +
+ "Host: not-localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400);
+tests.push(test);
+
+// HTTP/1.1 request, Host header has host but no port, expect a 400 response
+
+// eslint-disable-next-line no-useless-concat
+data = "GET /http/1.1-request HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "\r\n";
+test = new RawTest("localhost", PORT, data, check400);
+tests.push(test);
+
+// HTTP/1.1 request, Request-URI has wrong port, expect a 400 response
+
+data =
+ "GET http://127.0.0.1/http/1.1-request HTTP/1.1\r\n" +
+ "Host: 127.0.0.1\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400);
+tests.push(test);
+
+// HTTP/1.1 request, Request-URI has wrong port, expect a 400 response
+
+data =
+ "GET http://localhost:31337/http/1.1-request HTTP/1.1\r\n" +
+ "Host: localhost:31337\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400);
+tests.push(test);
+
+// HTTP/1.1 request, Request-URI has wrong scheme, expect a 400 response
+
+data =
+ "GET https://localhost:4444/http/1.1-request HTTP/1.1\r\n" +
+ "Host: localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400);
+tests.push(test);
+
+// HTTP/1.1 request, correct Host header, expect handler's response
+
+function http11goodHost(request, response) {
+ writeDetails(request, response);
+ response.setStatusLine("1.1", 200, "TEST PASSED");
+}
+data =
+ // eslint-disable-next-line no-useless-concat
+ "GET /http/1.1-good-host HTTP/1.1\r\n" + "Host: localhost:4444\r\n" + "\r\n";
+function check11goodHost(aData) {
+ let iter = LineIterator(aData);
+
+ // Status-Line
+ Assert.equal(iter.next().value, "HTTP/1.1 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ let body = [
+ "Method: GET",
+ "Path: /http/1.1-good-host",
+ "Query: ",
+ "Version: 1.1",
+ "Scheme: http",
+ "Host: localhost",
+ "Port: 4444",
+ ];
+
+ expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check11goodHost);
+tests.push(test);
+
+// HTTP/1.1 request, Host header is secondary identity
+
+function http11ipHost(request, response) {
+ writeDetails(request, response);
+ response.setStatusLine("1.1", 200, "TEST PASSED");
+}
+data =
+ // eslint-disable-next-line no-useless-concat
+ "GET /http/1.1-ip-host HTTP/1.1\r\n" + "Host: 127.0.0.1:4444\r\n" + "\r\n";
+function check11ipHost(aData) {
+ let iter = LineIterator(aData);
+
+ // Status-Line
+ Assert.equal(iter.next().value, "HTTP/1.1 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ let body = [
+ "Method: GET",
+ "Path: /http/1.1-ip-host",
+ "Query: ",
+ "Version: 1.1",
+ "Scheme: http",
+ "Host: 127.0.0.1",
+ "Port: 4444",
+ ];
+
+ expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check11ipHost);
+tests.push(test);
+
+// HTTP/1.1 request, absolute path, accurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data =
+ "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+ "Host: localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHost);
+tests.push(test);
+
+// HTTP/1.1 request, absolute path, inaccurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data =
+ "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+ "Host: localhost:1234\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHost);
+tests.push(test);
+
+// HTTP/1.1 request, absolute path, different inaccurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data =
+ "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+ "Host: not-localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHost);
+tests.push(test);
+
+// HTTP/1.1 request, absolute path, yet another inaccurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data =
+ "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+ "Host: yippity-skippity\r\n" +
+ "\r\n";
+function checkInaccurate(aData) {
+ check11goodHost(aData);
+
+ // dynamism setup
+ srv.identity.setPrimary("http", "127.0.0.1", 4444);
+}
+test = new RawTest("localhost", PORT, data, checkInaccurate);
+tests.push(test);
+
+// HTTP/1.0 request, absolute path, different inaccurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data =
+ "GET /http/1.0-request HTTP/1.0\r\n" +
+ "Host: not-localhost:4444\r\n" +
+ "\r\n";
+function check10ip(aData) {
+ let iter = LineIterator(aData);
+
+ // Status-Line
+ Assert.equal(iter.next().value, "HTTP/1.0 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ let body = [
+ "Method: GET",
+ "Path: /http/1.0-request",
+ "Query: ",
+ "Version: 1.0",
+ "Scheme: http",
+ "Host: 127.0.0.1",
+ "Port: 4444",
+ ];
+
+ expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check10ip);
+tests.push(test);
+
+// HTTP/1.1 request, Host header with implied port
+
+function http11goodHostWackyPort(request, response) {
+ writeDetails(request, response);
+ response.setStatusLine("1.1", 200, "TEST PASSED");
+}
+data =
+ "GET /http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+function check11goodHostWackyPort(aData) {
+ let iter = LineIterator(aData);
+
+ // Status-Line
+ Assert.equal(iter.next().value, "HTTP/1.1 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ let body = [
+ "Method: GET",
+ "Path: /http/1.1-good-host-wacky-port",
+ "Query: ",
+ "Version: 1.1",
+ "Scheme: http",
+ "Host: localhost",
+ "Port: 80",
+ ];
+
+ expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort);
+tests.push(test);
+
+// HTTP/1.1 request, Host header with wacky implied port
+
+data =
+ "GET /http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+ "Host: localhost:\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort);
+tests.push(test);
+
+// HTTP/1.1 request, absolute URI with implied port
+
+data =
+ "GET http://localhost/http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort);
+tests.push(test);
+
+// HTTP/1.1 request, absolute URI with wacky implied port
+
+data =
+ "GET http://localhost:/http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort);
+tests.push(test);
+
+// HTTP/1.1 request, absolute URI with explicit implied port, ignored Host
+
+data =
+ "GET http://localhost:80/http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+ "Host: who-cares\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort);
+tests.push(test);
+
+// HTTP/1.1 request, a malformed Request-URI
+
+data =
+ "GET is-this-the-real-life-is-this-just-fantasy HTTP/1.1\r\n" +
+ "Host: localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400);
+tests.push(test);
+
+// HTTP/1.1 request, a malformed Host header
+
+// eslint-disable-next-line no-useless-concat
+data = "GET /http/1.1-request HTTP/1.1\r\n" + "Host: la la la\r\n" + "\r\n";
+test = new RawTest("localhost", PORT, data, check400);
+tests.push(test);
+
+// HTTP/1.1 request, a malformed Host header but absolute URI, 5.2 sez fine
+
+data =
+ "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+ "Host: la la la\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHost);
+tests.push(test);
+
+// HTTP/1.0 request, absolute URI, but those aren't valid in HTTP/1.0
+
+data =
+ "GET http://localhost:4444/http/1.1-request HTTP/1.0\r\n" +
+ "Host: localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400);
+tests.push(test);
+
+// HTTP/1.1 request, absolute URI with unrecognized host
+
+data =
+ "GET http://not-localhost:4444/http/1.1-request HTTP/1.1\r\n" +
+ "Host: not-localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400);
+tests.push(test);
+
+// HTTP/1.1 request, absolute URI with unrecognized host (but not in Host)
+
+data =
+ "GET http://not-localhost:4444/http/1.1-request HTTP/1.1\r\n" +
+ "Host: localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400);
+tests.push(test);
diff --git a/netwerk/test/httpserver/test/test_host_identity.js b/netwerk/test/httpserver/test/test_host_identity.js
new file mode 100644
index 0000000000..1a1662d8cf
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_host_identity.js
@@ -0,0 +1,115 @@
+/* -*- 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/. */
+
+/**
+ * Tests that the server accepts requests to custom host names.
+ * This is commonly used in tests that map custom host names to the server via
+ * a proxy e.g. by XPCShellContentUtils.createHttpServer.
+ */
+
+var srv = createServer();
+srv.start(-1);
+registerCleanupFunction(() => new Promise(resolve => srv.stop(resolve)));
+const PORT = srv.identity.primaryPort;
+srv.registerPathHandler("/dump-request", dumpRequestLines);
+
+function dumpRequestLines(request, response) {
+ writeDetails(request, response);
+ response.setStatusLine(request.httpVersion, 200, "TEST PASSED");
+}
+
+function makeRawRequest(requestLinePath, hostHeader) {
+ return `GET ${requestLinePath} HTTP/1.1\r\nHost: ${hostHeader}\r\n\r\n`;
+}
+
+function verifyResponseHostPort(data, query, expectedHost, expectedPort) {
+ var iter = LineIterator(data);
+
+ // Status-Line
+ Assert.equal(iter.next().value, "HTTP/1.1 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ var body = [
+ "Method: GET",
+ "Path: /dump-request",
+ "Query: " + query,
+ "Version: 1.1",
+ "Scheme: http",
+ "Host: " + expectedHost,
+ "Port: " + expectedPort,
+ ];
+
+ expectLines(iter, body);
+}
+
+function runIdentityTest(host, port) {
+ srv.identity.add("http", host, port);
+
+ function checkAbsoluteRequestURI(data) {
+ verifyResponseHostPort(data, "absolute", host, port);
+ }
+ function checkHostHeader(data) {
+ verifyResponseHostPort(data, "relative", host, port);
+ }
+
+ let tests = [];
+ let test, data;
+ let hostport = `${host}:${port}`;
+ data = makeRawRequest(`http://${hostport}/dump-request?absolute`, hostport);
+ test = new RawTest("localhost", PORT, data, checkAbsoluteRequestURI);
+ tests.push(test);
+
+ data = makeRawRequest("/dump-request?relative", hostport);
+ test = new RawTest("localhost", PORT, data, checkHostHeader);
+ tests.push(test);
+ return new Promise(resolve => {
+ runRawTests(tests, resolve);
+ });
+}
+
+/** *************
+ * BEGIN TESTS *
+ ***************/
+
+add_task(async function test_basic_example_com() {
+ await runIdentityTest("example.com", 1234);
+ await runIdentityTest("example.com", 5432);
+});
+
+add_task(async function test_fully_qualified_domain_name_aka_fqdn() {
+ await runIdentityTest("fully-qualified-domain-name.", 1234);
+});
+
+add_task(async function test_ipv4() {
+ await runIdentityTest("1.2.3.4", 1234);
+});
+
+add_task(async function test_ipv6() {
+ Assert.throws(
+ () => srv.identity.add("http", "[notipv6]", 1234),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "should reject invalid host, clearly not bracketed IPv6"
+ );
+ Assert.throws(
+ () => srv.identity.add("http", "[::127.0.0.1]", 1234),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "should reject non-canonical IPv6"
+ );
+ await runIdentityTest("[::123]", 1234);
+ await runIdentityTest("[1:2:3:a:b:c:d:abcd]", 1234);
+});
+
+add_task(async function test_internationalized_domain_name() {
+ Assert.throws(
+ () => srv.identity.add("http", "δοκιμή", 1234),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "should reject IDN not in punycode"
+ );
+
+ await runIdentityTest("xn--jxalpdlp", 1234);
+});
diff --git a/netwerk/test/httpserver/test/test_linedata.js b/netwerk/test/httpserver/test/test_linedata.js
new file mode 100644
index 0000000000..83fed3e8c0
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_linedata.js
@@ -0,0 +1,22 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ */
+/* 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/. */
+
+// test that the LineData internal data structure works correctly
+
+const CR = 0x0d;
+const LF = 0x0a;
+
+function run_test() {
+ var data = new LineData();
+ data.appendBytes(["a".charCodeAt(0), CR]);
+
+ var out = { value: "" };
+ Assert.ok(!data.readLine(out));
+
+ data.appendBytes([LF]);
+ Assert.ok(data.readLine(out));
+ Assert.equal(out.value, "a");
+}
diff --git a/netwerk/test/httpserver/test/test_load_module.js b/netwerk/test/httpserver/test/test_load_module.js
new file mode 100644
index 0000000000..38c2ca4170
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_load_module.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Ensure httpd.js can be imported as a module and that a server starts.
+ */
+function run_test() {
+ const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+ );
+
+ let server = new HttpServer();
+ server.start(-1);
+
+ do_test_pending();
+
+ server.stop(do_test_finished);
+}
diff --git a/netwerk/test/httpserver/test/test_name_scheme.js b/netwerk/test/httpserver/test/test_name_scheme.js
new file mode 100644
index 0000000000..4f9fefe159
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_name_scheme.js
@@ -0,0 +1,91 @@
+/* -*- 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/. */
+
+// requests for files ending with a caret (^) are handled specially to enable
+// htaccess-like functionality without the need to explicitly disable display
+// of such files
+
+var srv;
+
+ChromeUtils.defineLazyGetter(this, "PREFIX", function () {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(PREFIX + "/bar.html^", null, start_bar_html_, null),
+ new Test(PREFIX + "/foo.html^", null, start_foo_html_, null),
+ new Test(PREFIX + "/normal-file.txt", null, start_normal_file_txt, null),
+ new Test(PREFIX + "/folder^/file.txt", null, start_folder__file_txt, null),
+
+ new Test(PREFIX + "/foo/bar.html^", null, start_bar_html_, null),
+ new Test(PREFIX + "/foo/foo.html^", null, start_foo_html_, null),
+ new Test(
+ PREFIX + "/foo/normal-file.txt",
+ null,
+ start_normal_file_txt,
+ null
+ ),
+ new Test(
+ PREFIX + "/foo/folder^/file.txt",
+ null,
+ start_folder__file_txt,
+ null
+ ),
+
+ new Test(PREFIX + "/end-caret^/bar.html^", null, start_bar_html_, null),
+ new Test(PREFIX + "/end-caret^/foo.html^", null, start_foo_html_, null),
+ new Test(
+ PREFIX + "/end-caret^/normal-file.txt",
+ null,
+ start_normal_file_txt,
+ null
+ ),
+ new Test(
+ PREFIX + "/end-caret^/folder^/file.txt",
+ null,
+ start_folder__file_txt,
+ null
+ ),
+ ];
+});
+
+function run_test() {
+ srv = createServer();
+
+ // make sure underscores work in directories "mounted" in directories with
+ // folders starting with _
+ var nameDir = do_get_file("data/name-scheme/");
+ srv.registerDirectory("/", nameDir);
+ srv.registerDirectory("/foo/", nameDir);
+ srv.registerDirectory("/end-caret^/", nameDir);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+// TEST DATA
+
+function start_bar_html_(ch) {
+ Assert.equal(ch.responseStatus, 200);
+
+ Assert.equal(ch.getResponseHeader("Content-Type"), "text/html");
+}
+
+function start_foo_html_(ch) {
+ Assert.equal(ch.responseStatus, 404);
+}
+
+function start_normal_file_txt(ch) {
+ Assert.equal(ch.responseStatus, 200);
+ Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain");
+}
+
+function start_folder__file_txt(ch) {
+ Assert.equal(ch.responseStatus, 200);
+ Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain");
+}
diff --git a/netwerk/test/httpserver/test/test_processasync.js b/netwerk/test/httpserver/test/test_processasync.js
new file mode 100644
index 0000000000..321c9b086f
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_processasync.js
@@ -0,0 +1,272 @@
+/* -*- 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/. */
+
+/*
+ * Tests for correct behavior of asynchronous responses.
+ */
+
+ChromeUtils.defineLazyGetter(this, "PREPATH", function () {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+var srv;
+
+function run_test() {
+ srv = createServer();
+ for (var path in handlers) {
+ srv.registerPathHandler(path, handlers[path]);
+ }
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+/** *************
+ * BEGIN TESTS *
+ ***************/
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(PREPATH + "/handleSync", null, start_handleSync, null),
+ new Test(
+ PREPATH + "/handleAsync1",
+ null,
+ start_handleAsync1,
+ stop_handleAsync1
+ ),
+ new Test(
+ PREPATH + "/handleAsync2",
+ init_handleAsync2,
+ start_handleAsync2,
+ stop_handleAsync2
+ ),
+ new Test(
+ PREPATH + "/handleAsyncOrdering",
+ null,
+ null,
+ stop_handleAsyncOrdering
+ ),
+ ];
+});
+
+var handlers = {};
+
+function handleSync(request, response) {
+ response.setStatusLine(request.httpVersion, 500, "handleSync fail");
+
+ try {
+ response.finish();
+ do_throw("finish called on sync response");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_UNEXPECTED);
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "handleSync pass");
+}
+handlers["/handleSync"] = handleSync;
+
+function start_handleSync(ch) {
+ Assert.equal(ch.responseStatus, 200);
+ Assert.equal(ch.responseStatusText, "handleSync pass");
+}
+
+function handleAsync1(request, response) {
+ response.setStatusLine(request.httpVersion, 500, "Old status line!");
+ response.setHeader("X-Foo", "old value", false);
+
+ response.processAsync();
+
+ response.setStatusLine(request.httpVersion, 200, "New status line!");
+ response.setHeader("X-Foo", "new value", false);
+
+ response.finish();
+
+ try {
+ response.setStatusLine(request.httpVersion, 500, "Too late!");
+ do_throw("late setStatusLine didn't throw");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ try {
+ response.setHeader("X-Foo", "late value", false);
+ do_throw("late setHeader didn't throw");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ try {
+ response.bodyOutputStream;
+ do_throw("late bodyOutputStream get didn't throw");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ try {
+ response.write("fugly");
+ do_throw("late write() didn't throw");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+}
+handlers["/handleAsync1"] = handleAsync1;
+
+function start_handleAsync1(ch) {
+ Assert.equal(ch.responseStatus, 200);
+ Assert.equal(ch.responseStatusText, "New status line!");
+ Assert.equal(ch.getResponseHeader("X-Foo"), "new value");
+}
+
+function stop_handleAsync1(ch, status, data) {
+ Assert.equal(data.length, 0);
+}
+
+const startToHeaderDelay = 500;
+const startToFinishedDelay = 750;
+
+function handleAsync2(request, response) {
+ response.processAsync();
+
+ response.setStatusLine(request.httpVersion, 200, "Status line");
+ response.setHeader("X-Custom-Header", "value", false);
+
+ callLater(startToHeaderDelay, function () {
+ var preBody = "BO";
+ response.bodyOutputStream.write(preBody, preBody.length);
+
+ try {
+ response.setStatusLine(request.httpVersion, 500, "after body write");
+ do_throw("setStatusLine succeeded");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ try {
+ response.setHeader("X-Custom-Header", "new 1", false);
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ callLater(startToFinishedDelay - startToHeaderDelay, function () {
+ var postBody = "DY";
+ response.bodyOutputStream.write(postBody, postBody.length);
+
+ response.finish();
+ response.finish(); // idempotency
+
+ try {
+ response.setStatusLine(request.httpVersion, 500, "after finish");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ try {
+ response.setHeader("X-Custom-Header", "new 2", false);
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ try {
+ response.write("EVIL");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ });
+ });
+}
+handlers["/handleAsync2"] = handleAsync2;
+
+var startTime_handleAsync2;
+
+function init_handleAsync2(ch) {
+ var now = (startTime_handleAsync2 = Date.now());
+ dumpn("*** init_HandleAsync2: start time " + now);
+}
+
+function start_handleAsync2(ch) {
+ var now = Date.now();
+ dumpn(
+ "*** start_handleAsync2: onStartRequest time " +
+ now +
+ ", " +
+ (now - startTime_handleAsync2) +
+ "ms after start time"
+ );
+ Assert.ok(now >= startTime_handleAsync2 + startToHeaderDelay);
+
+ Assert.equal(ch.responseStatus, 200);
+ Assert.equal(ch.responseStatusText, "Status line");
+ Assert.equal(ch.getResponseHeader("X-Custom-Header"), "value");
+}
+
+function stop_handleAsync2(ch, status, data) {
+ var now = Date.now();
+ dumpn(
+ "*** stop_handleAsync2: onStopRequest time " +
+ now +
+ ", " +
+ (now - startTime_handleAsync2) +
+ "ms after header time"
+ );
+ Assert.ok(now >= startTime_handleAsync2 + startToFinishedDelay);
+
+ Assert.equal(String.fromCharCode.apply(null, data), "BODY");
+}
+
+/*
+ * Tests that accessing output stream *before* calling processAsync() works
+ * correctly, sending written data immediately as it is written, not buffering
+ * until finish() is called -- which for this much data would mean we would all
+ * but certainly deadlock, since we're trying to read/write all this data in one
+ * process on a single thread.
+ */
+function handleAsyncOrdering(request, response) {
+ var out = new BinaryOutputStream(response.bodyOutputStream);
+
+ var data = [];
+ for (var i = 0; i < 65536; i++) {
+ data[i] = 0;
+ }
+ var count = 20;
+
+ var writeData = {
+ run() {
+ if (count-- === 0) {
+ response.finish();
+ return;
+ }
+
+ try {
+ out.writeByteArray(data);
+ step();
+ } catch (e) {
+ try {
+ do_throw("error writing data: " + e);
+ } finally {
+ response.finish();
+ }
+ }
+ },
+ };
+ function step() {
+ // Use gThreadManager here because it's expedient, *not* because it's
+ // intended for public use! If you do this in client code, expect me to
+ // knowingly break your code by changing the variable name. :-P
+ Services.tm.dispatchToMainThread(writeData);
+ }
+ step();
+ response.processAsync();
+}
+handlers["/handleAsyncOrdering"] = handleAsyncOrdering;
+
+function stop_handleAsyncOrdering(ch, status, data) {
+ Assert.equal(data.length, 20 * 65536);
+ data.forEach(function (v, index) {
+ if (v !== 0) {
+ do_throw("value " + v + " at index " + index + " should be zero");
+ }
+ });
+}
diff --git a/netwerk/test/httpserver/test/test_qi.js b/netwerk/test/httpserver/test/test_qi.js
new file mode 100644
index 0000000000..e88e53119e
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_qi.js
@@ -0,0 +1,107 @@
+/* -*- 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/. */
+/* eslint-disable no-control-regex */
+
+/*
+ * Verify the presence of explicit QueryInterface methods on XPCOM objects
+ * exposed by httpd.js, rather than allowing QueryInterface to be implicitly
+ * created by XPConnect.
+ */
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(
+ "http://localhost:" + srv.identity.primaryPort + "/test",
+ null,
+ start_test,
+ null
+ ),
+ new Test(
+ "http://localhost:" + srv.identity.primaryPort + "/sjs/qi.sjs",
+ null,
+ start_sjs_qi,
+ null
+ ),
+ ];
+});
+
+var srv;
+
+function run_test() {
+ srv = createServer();
+
+ try {
+ srv.identity.QueryInterface(Ci.nsIHttpServerIdentity);
+ } catch (e) {
+ var exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0];
+ do_throw("server identity didn't QI: " + exstr);
+ return;
+ }
+
+ srv.registerPathHandler("/test", testHandler);
+ srv.registerDirectory("/", do_get_file("data/"));
+ srv.registerContentType("sjs", "sjs");
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+// TEST DATA
+
+function start_test(ch) {
+ Assert.equal(ch.responseStatusText, "QI Tests Passed");
+ Assert.equal(ch.responseStatus, 200);
+}
+
+function start_sjs_qi(ch) {
+ Assert.equal(ch.responseStatusText, "SJS QI Tests Passed");
+ Assert.equal(ch.responseStatus, 200);
+}
+
+function testHandler(request, response) {
+ var exstr;
+ var qid;
+
+ response.setStatusLine(request.httpVersion, 500, "FAIL");
+
+ var passed = false;
+ try {
+ qid = request.QueryInterface(Ci.nsIHttpRequest);
+ passed = qid === request;
+ } catch (e) {
+ exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0];
+ response.setStatusLine(
+ request.httpVersion,
+ 500,
+ "request doesn't QI: " + exstr
+ );
+ return;
+ }
+ if (!passed) {
+ response.setStatusLine(request.httpVersion, 500, "request QI'd wrongly?");
+ return;
+ }
+
+ passed = false;
+ try {
+ qid = response.QueryInterface(Ci.nsIHttpResponse);
+ passed = qid === response;
+ } catch (e) {
+ exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0];
+ response.setStatusLine(
+ request.httpVersion,
+ 500,
+ "response doesn't QI: " + exstr
+ );
+ return;
+ }
+ if (!passed) {
+ response.setStatusLine(request.httpVersion, 500, "response QI'd wrongly?");
+ return;
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "QI Tests Passed");
+}
diff --git a/netwerk/test/httpserver/test/test_registerdirectory.js b/netwerk/test/httpserver/test/test_registerdirectory.js
new file mode 100644
index 0000000000..84bb032efc
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_registerdirectory.js
@@ -0,0 +1,278 @@
+/* -*- 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/. */
+
+// tests the registerDirectory API
+
+ChromeUtils.defineLazyGetter(this, "BASE", function () {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+function nocache(ch) {
+ ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important!
+}
+
+function notFound(ch) {
+ Assert.equal(ch.responseStatus, 404);
+ Assert.ok(!ch.requestSucceeded);
+}
+
+function checkOverride(ch) {
+ Assert.equal(ch.responseStatus, 200);
+ Assert.equal(ch.responseStatusText, "OK");
+ Assert.ok(ch.requestSucceeded);
+ Assert.equal(ch.getResponseHeader("Override-Succeeded"), "yes");
+}
+
+function check200(ch) {
+ Assert.equal(ch.responseStatus, 200);
+ Assert.equal(ch.responseStatusText, "OK");
+}
+
+function checkFile(ch, status, data) {
+ Assert.equal(ch.responseStatus, 200);
+ Assert.ok(ch.requestSucceeded);
+
+ var actualFile = serverBasePath.clone();
+ actualFile.append("test_registerdirectory.js");
+ Assert.equal(
+ ch.getResponseHeader("Content-Length"),
+ actualFile.fileSize.toString()
+ );
+ Assert.equal(
+ data.map(v => String.fromCharCode(v)).join(""),
+ fileContents(actualFile)
+ );
+}
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ /** *********************
+ * without a base path *
+ ***********************/
+ new Test(BASE + "/test_registerdirectory.js", nocache, notFound, null),
+
+ /** ******************
+ * with a base path *
+ ********************/
+ new Test(
+ BASE + "/test_registerdirectory.js",
+ function (ch) {
+ nocache(ch);
+ serverBasePath = testsDirectory.clone();
+ srv.registerDirectory("/", serverBasePath);
+ },
+ null,
+ checkFile
+ ),
+
+ /** ***************************
+ * without a base path again *
+ *****************************/
+ new Test(
+ BASE + "/test_registerdirectory.js",
+ function (ch) {
+ nocache(ch);
+ serverBasePath = null;
+ srv.registerDirectory("/", serverBasePath);
+ },
+ notFound,
+ null
+ ),
+
+ /** *************************
+ * registered path handler *
+ ***************************/
+ new Test(
+ BASE + "/test_registerdirectory.js",
+ function (ch) {
+ nocache(ch);
+ srv.registerPathHandler(
+ "/test_registerdirectory.js",
+ override_test_registerdirectory
+ );
+ },
+ checkOverride,
+ null
+ ),
+
+ /** **********************
+ * removed path handler *
+ ************************/
+ new Test(
+ BASE + "/test_registerdirectory.js",
+ function init_registerDirectory6(ch) {
+ nocache(ch);
+ srv.registerPathHandler("/test_registerdirectory.js", null);
+ },
+ notFound,
+ null
+ ),
+
+ /** ******************
+ * with a base path *
+ ********************/
+ new Test(
+ BASE + "/test_registerdirectory.js",
+ function (ch) {
+ nocache(ch);
+
+ // set the base path again
+ serverBasePath = testsDirectory.clone();
+ srv.registerDirectory("/", serverBasePath);
+ },
+ null,
+ checkFile
+ ),
+
+ /** ***********************
+ * ...and a path handler *
+ *************************/
+ new Test(
+ BASE + "/test_registerdirectory.js",
+ function (ch) {
+ nocache(ch);
+ srv.registerPathHandler(
+ "/test_registerdirectory.js",
+ override_test_registerdirectory
+ );
+ },
+ checkOverride,
+ null
+ ),
+
+ /** **********************
+ * removed base handler *
+ ************************/
+ new Test(
+ BASE + "/test_registerdirectory.js",
+ function (ch) {
+ nocache(ch);
+ serverBasePath = null;
+ srv.registerDirectory("/", serverBasePath);
+ },
+ checkOverride,
+ null
+ ),
+
+ /** **********************
+ * removed path handler *
+ ************************/
+ new Test(
+ BASE + "/test_registerdirectory.js",
+ function (ch) {
+ nocache(ch);
+ srv.registerPathHandler("/test_registerdirectory.js", null);
+ },
+ notFound,
+ null
+ ),
+
+ /** ***********************
+ * mapping set up, works *
+ *************************/
+ new Test(
+ BASE + "/foo/test_registerdirectory.js",
+ function (ch) {
+ nocache(ch);
+ serverBasePath = testsDirectory.clone();
+ srv.registerDirectory("/foo/", serverBasePath);
+ },
+ check200,
+ null
+ ),
+
+ /** *******************
+ * no mapping, fails *
+ *********************/
+ new Test(
+ BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js",
+ nocache,
+ notFound,
+ null
+ ),
+
+ /** ****************
+ * mapping, works *
+ ******************/
+ new Test(
+ BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js",
+ function (ch) {
+ nocache(ch);
+ srv.registerDirectory(
+ "/foo/test_registerdirectory.js/",
+ serverBasePath
+ );
+ },
+ null,
+ checkFile
+ ),
+
+ /** **********************************
+ * two mappings set up, still works *
+ ************************************/
+ new Test(BASE + "/foo/test_registerdirectory.js", nocache, null, checkFile),
+
+ /** ************************
+ * remove topmost mapping *
+ **************************/
+ new Test(
+ BASE + "/foo/test_registerdirectory.js",
+ function (ch) {
+ nocache(ch);
+ srv.registerDirectory("/foo/", null);
+ },
+ notFound,
+ null
+ ),
+
+ /** ************************************
+ * lower mapping still present, works *
+ **************************************/
+ new Test(
+ BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js",
+ nocache,
+ null,
+ checkFile
+ ),
+
+ /** *****************
+ * mapping removed *
+ *******************/
+ new Test(
+ BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js",
+ function (ch) {
+ nocache(ch);
+ srv.registerDirectory("/foo/test_registerdirectory.js/", null);
+ },
+ notFound,
+ null
+ ),
+ ];
+});
+
+var srv;
+var serverBasePath;
+var testsDirectory;
+
+function run_test() {
+ testsDirectory = do_get_cwd();
+
+ srv = createServer();
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+// PATH HANDLERS
+
+// override of /test_registerdirectory.js
+function override_test_registerdirectory(metadata, response) {
+ response.setStatusLine("1.1", 200, "OK");
+ response.setHeader("Override-Succeeded", "yes", false);
+
+ var body = "success!";
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/netwerk/test/httpserver/test/test_registerfile.js b/netwerk/test/httpserver/test/test_registerfile.js
new file mode 100644
index 0000000000..9b93d62b07
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_registerfile.js
@@ -0,0 +1,44 @@
+/* -*- 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/. */
+
+// tests the registerFile API
+
+ChromeUtils.defineLazyGetter(this, "BASE", function () {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+var file = do_get_file("test_registerfile.js");
+
+function onStart(ch) {
+ Assert.equal(ch.responseStatus, 200);
+}
+
+function onStop(ch, status, data) {
+ // not sufficient for equality, but not likely to be wrong!
+ Assert.equal(data.length, file.fileSize);
+}
+
+ChromeUtils.defineLazyGetter(this, "test", function () {
+ return new Test(BASE + "/foo", null, onStart, onStop);
+});
+
+var srv;
+
+function run_test() {
+ srv = createServer();
+
+ try {
+ srv.registerFile("/foo", do_get_profile());
+ throw new Error("registerFile succeeded!");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ srv.registerFile("/foo", file);
+ srv.start(-1);
+
+ runHttpTests([test], testComplete(srv));
+}
diff --git a/netwerk/test/httpserver/test/test_registerprefix.js b/netwerk/test/httpserver/test/test_registerprefix.js
new file mode 100644
index 0000000000..5edb75225c
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_registerprefix.js
@@ -0,0 +1,130 @@
+/* -*- 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/. */
+
+// tests the registerPrefixHandler API
+
+ChromeUtils.defineLazyGetter(this, "BASE", function () {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+function nocache(ch) {
+ ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important!
+}
+
+function notFound(ch) {
+ Assert.equal(ch.responseStatus, 404);
+ Assert.ok(!ch.requestSucceeded);
+}
+
+function makeCheckOverride(magic) {
+ return function checkOverride(ch) {
+ Assert.equal(ch.responseStatus, 200);
+ Assert.equal(ch.responseStatusText, "OK");
+ Assert.ok(ch.requestSucceeded);
+ Assert.equal(ch.getResponseHeader("Override-Succeeded"), magic);
+ };
+}
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(
+ BASE + "/prefix/dummy",
+ prefixHandler,
+ null,
+ makeCheckOverride("prefix")
+ ),
+ new Test(
+ BASE + "/prefix/dummy",
+ pathHandler,
+ null,
+ makeCheckOverride("path")
+ ),
+ new Test(
+ BASE + "/prefix/subpath/dummy",
+ longerPrefixHandler,
+ null,
+ makeCheckOverride("subpath")
+ ),
+ new Test(BASE + "/prefix/dummy", removeHandlers, null, notFound),
+ new Test(
+ BASE + "/prefix/subpath/dummy",
+ newPrefixHandler,
+ null,
+ makeCheckOverride("subpath")
+ ),
+ ];
+});
+
+/** *************************
+ * registered prefix handler *
+ ***************************/
+
+function prefixHandler(channel) {
+ nocache(channel);
+ srv.registerPrefixHandler("/prefix/", makeOverride("prefix"));
+}
+
+/** ******************************
+ * registered path handler on top *
+ ********************************/
+
+function pathHandler(channel) {
+ nocache(channel);
+ srv.registerPathHandler("/prefix/dummy", makeOverride("path"));
+}
+
+/** ********************************
+ * registered longer prefix handler *
+ **********************************/
+
+function longerPrefixHandler(channel) {
+ nocache(channel);
+ srv.registerPrefixHandler("/prefix/subpath/", makeOverride("subpath"));
+}
+
+/** **********************
+ * removed prefix handler *
+ ************************/
+
+function removeHandlers(channel) {
+ nocache(channel);
+ srv.registerPrefixHandler("/prefix/", null);
+ srv.registerPathHandler("/prefix/dummy", null);
+}
+
+/** ***************************
+ * re-register shorter handler *
+ *****************************/
+
+function newPrefixHandler(channel) {
+ nocache(channel);
+ srv.registerPrefixHandler("/prefix/", makeOverride("prefix"));
+}
+
+var srv;
+
+function run_test() {
+ // Ensure the profile exists.
+ do_get_profile();
+
+ srv = createServer();
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+// PATH HANDLERS
+
+// generate an override
+function makeOverride(magic) {
+ return function override(metadata, response) {
+ response.setStatusLine("1.1", 200, "OK");
+ response.setHeader("Override-Succeeded", magic, false);
+
+ var body = "success!";
+ response.bodyOutputStream.write(body, body.length);
+ };
+}
diff --git a/netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js b/netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js
new file mode 100644
index 0000000000..b1d7a7d071
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js
@@ -0,0 +1,137 @@
+/* -*- 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/. */
+
+/**
+ * Tests that even when an incoming request's data for the Request-Line doesn't
+ * all fit in a single onInputStreamReady notification, the request is handled
+ * properly.
+ */
+
+var srv = createServer();
+srv.start(-1);
+const PORT = srv.identity.primaryPort;
+
+function run_test() {
+ srv.registerPathHandler(
+ "/lots-of-leading-blank-lines",
+ lotsOfLeadingBlankLines
+ );
+ srv.registerPathHandler("/very-long-request-line", veryLongRequestLine);
+
+ runRawTests(tests, testComplete(srv));
+}
+
+/** *************
+ * BEGIN TESTS *
+ ***************/
+
+var test, gData, str;
+var tests = [];
+
+function veryLongRequestLine(request, response) {
+ writeDetails(request, response);
+ response.setStatusLine(request.httpVersion, 200, "TEST PASSED");
+}
+
+var reallyLong = "0123456789ABCDEF0123456789ABCDEF"; // 32
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 128
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 512
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 2048
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 8192
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 32768
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 131072
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 524288
+if (reallyLong.length !== 524288) {
+ throw new TypeError("generated length not as long as expected");
+}
+str =
+ "GET /very-long-request-line?" +
+ reallyLong +
+ " HTTP/1.1\r\n" +
+ "Host: localhost:" +
+ PORT +
+ "\r\n" +
+ "\r\n";
+gData = [];
+for (let i = 0; i < str.length; i += 16384) {
+ gData.push(str.substr(i, 16384));
+}
+
+function checkVeryLongRequestLine(data) {
+ var iter = LineIterator(data);
+
+ print("data length: " + data.length);
+ print("iter object: " + iter);
+
+ // Status-Line
+ Assert.equal(iter.next().value, "HTTP/1.1 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ var body = [
+ "Method: GET",
+ "Path: /very-long-request-line",
+ "Query: " + reallyLong,
+ "Version: 1.1",
+ "Scheme: http",
+ "Host: localhost",
+ "Port: " + PORT,
+ ];
+
+ expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, gData, checkVeryLongRequestLine);
+tests.push(test);
+
+function lotsOfLeadingBlankLines(request, response) {
+ writeDetails(request, response);
+ response.setStatusLine(request.httpVersion, 200, "TEST PASSED");
+}
+
+var blankLines = "\r\n";
+for (let i = 0; i < 14; i++) {
+ blankLines += blankLines;
+}
+str =
+ blankLines +
+ "GET /lots-of-leading-blank-lines HTTP/1.1\r\n" +
+ "Host: localhost:" +
+ PORT +
+ "\r\n" +
+ "\r\n";
+gData = [];
+for (let i = 0; i < str.length; i += 100) {
+ gData.push(str.substr(i, 100));
+}
+
+function checkLotsOfLeadingBlankLines(data) {
+ var iter = LineIterator(data);
+
+ // Status-Line
+ print("data length: " + data.length);
+ print("iter object: " + iter);
+
+ Assert.equal(iter.next().value, "HTTP/1.1 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ var body = [
+ "Method: GET",
+ "Path: /lots-of-leading-blank-lines",
+ "Query: ",
+ "Version: 1.1",
+ "Scheme: http",
+ "Host: localhost",
+ "Port: " + PORT,
+ ];
+
+ expectLines(iter, body);
+}
+
+test = new RawTest("localhost", PORT, gData, checkLotsOfLeadingBlankLines);
+tests.push(test);
diff --git a/netwerk/test/httpserver/test/test_response_write.js b/netwerk/test/httpserver/test/test_response_write.js
new file mode 100644
index 0000000000..7f5837419e
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_response_write.js
@@ -0,0 +1,57 @@
+/* -*- 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/. */
+
+// make sure response.write works for strings, and coerces other args to strings
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(
+ "http://localhost:" + srv.identity.primaryPort + "/writeString",
+ null,
+ check_1234,
+ succeeded
+ ),
+ new Test(
+ "http://localhost:" + srv.identity.primaryPort + "/writeInt",
+ null,
+ check_1234,
+ succeeded
+ ),
+ ];
+});
+
+var srv;
+
+function run_test() {
+ srv = createServer();
+
+ srv.registerPathHandler("/writeString", writeString);
+ srv.registerPathHandler("/writeInt", writeInt);
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+// TEST DATA
+
+function succeeded(ch, status, data) {
+ Assert.ok(Components.isSuccessCode(status));
+ Assert.equal(data.map(v => String.fromCharCode(v)).join(""), "1234");
+}
+
+function check_1234(ch) {
+ Assert.equal(ch.getResponseHeader("Content-Length"), "4");
+}
+
+// PATH HANDLERS
+
+function writeString(metadata, response) {
+ response.write("1234");
+}
+
+function writeInt(metadata, response) {
+ response.write(1234);
+}
diff --git a/netwerk/test/httpserver/test/test_seizepower.js b/netwerk/test/httpserver/test/test_seizepower.js
new file mode 100644
index 0000000000..a74054ea37
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_seizepower.js
@@ -0,0 +1,180 @@
+/* -*- 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/. */
+
+/*
+ * Tests that the seizePower API works correctly.
+ */
+
+ChromeUtils.defineLazyGetter(this, "PORT", function () {
+ return srv.identity.primaryPort;
+});
+
+var srv;
+
+function run_test() {
+ Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+ srv = createServer();
+
+ srv.registerPathHandler("/raw-data", handleRawData);
+ srv.registerPathHandler("/called-too-late", handleTooLate);
+ srv.registerPathHandler("/exceptions", handleExceptions);
+ srv.registerPathHandler("/async-seizure", handleAsyncSeizure);
+ srv.registerPathHandler("/seize-after-async", handleSeizeAfterAsync);
+
+ srv.start(-1);
+
+ runRawTests(tests, function () {
+ Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+ testComplete(srv)();
+ });
+}
+
+function checkException(fun, err, msg) {
+ try {
+ fun();
+ } catch (e) {
+ if (e !== err && e.result !== err) {
+ do_throw(msg);
+ }
+ return;
+ }
+ do_throw(msg);
+}
+
+/** ***************
+ * PATH HANDLERS *
+ *****************/
+
+function handleRawData(request, response) {
+ response.seizePower();
+ response.write("Raw data!");
+ response.finish();
+}
+
+function handleTooLate(request, response) {
+ response.write("DO NOT WANT");
+ var output = response.bodyOutputStream;
+
+ response.seizePower();
+
+ if (response.bodyOutputStream !== output) {
+ response.write("bodyOutputStream changed!");
+ } else {
+ response.write("too-late passed");
+ }
+ response.finish();
+}
+
+function handleExceptions(request, response) {
+ response.seizePower();
+ checkException(
+ function () {
+ response.setStatusLine("1.0", 500, "ISE");
+ },
+ Cr.NS_ERROR_NOT_AVAILABLE,
+ "setStatusLine should throw not-available after seizePower"
+ );
+ checkException(
+ function () {
+ response.setHeader("X-Fail", "FAIL", false);
+ },
+ Cr.NS_ERROR_NOT_AVAILABLE,
+ "setHeader should throw not-available after seizePower"
+ );
+ checkException(
+ function () {
+ response.processAsync();
+ },
+ Cr.NS_ERROR_NOT_AVAILABLE,
+ "processAsync should throw not-available after seizePower"
+ );
+ var out = response.bodyOutputStream;
+ var data = "exceptions test passed";
+ out.write(data, data.length);
+ response.seizePower(); // idempotency test of seizePower
+ response.finish();
+ response.finish(); // idempotency test of finish after seizePower
+ checkException(
+ function () {
+ response.seizePower();
+ },
+ Cr.NS_ERROR_UNEXPECTED,
+ "seizePower should throw unexpected after finish"
+ );
+}
+
+function handleAsyncSeizure(request, response) {
+ response.seizePower();
+ callLater(1, function () {
+ response.write("async seizure passed");
+ response.bodyOutputStream.close();
+ callLater(1, function () {
+ response.finish();
+ });
+ });
+}
+
+function handleSeizeAfterAsync(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "async seizure pass");
+ response.processAsync();
+ checkException(
+ function () {
+ response.seizePower();
+ },
+ Cr.NS_ERROR_NOT_AVAILABLE,
+ "seizePower should throw not-available after processAsync"
+ );
+ callLater(1, function () {
+ response.finish();
+ });
+}
+
+/** *************
+ * BEGIN TESTS *
+ ***************/
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new RawTest("localhost", PORT, data0, checkRawData),
+ new RawTest("localhost", PORT, data1, checkTooLate),
+ new RawTest("localhost", PORT, data2, checkExceptions),
+ new RawTest("localhost", PORT, data3, checkAsyncSeizure),
+ new RawTest("localhost", PORT, data4, checkSeizeAfterAsync),
+ ];
+});
+
+// eslint-disable-next-line no-useless-concat
+var data0 = "GET /raw-data HTTP/1.0\r\n" + "\r\n";
+function checkRawData(data) {
+ Assert.equal(data, "Raw data!");
+}
+
+// eslint-disable-next-line no-useless-concat
+var data1 = "GET /called-too-late HTTP/1.0\r\n" + "\r\n";
+function checkTooLate(data) {
+ Assert.equal(LineIterator(data).next().value, "too-late passed");
+}
+
+// eslint-disable-next-line no-useless-concat
+var data2 = "GET /exceptions HTTP/1.0\r\n" + "\r\n";
+function checkExceptions(data) {
+ Assert.equal("exceptions test passed", data);
+}
+
+// eslint-disable-next-line no-useless-concat
+var data3 = "GET /async-seizure HTTP/1.0\r\n" + "\r\n";
+function checkAsyncSeizure(data) {
+ Assert.equal(data, "async seizure passed");
+}
+
+// eslint-disable-next-line no-useless-concat
+var data4 = "GET /seize-after-async HTTP/1.0\r\n" + "\r\n";
+function checkSeizeAfterAsync(data) {
+ Assert.equal(
+ LineIterator(data).next().value,
+ "HTTP/1.0 200 async seizure pass"
+ );
+}
diff --git a/netwerk/test/httpserver/test/test_setindexhandler.js b/netwerk/test/httpserver/test/test_setindexhandler.js
new file mode 100644
index 0000000000..e65f565673
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_setindexhandler.js
@@ -0,0 +1,60 @@
+/* -*- 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/. */
+
+// Make sure setIndexHandler works as expected
+
+var srv, serverBasePath;
+
+function run_test() {
+ srv = createServer();
+ serverBasePath = do_get_profile();
+ srv.registerDirectory("/", serverBasePath);
+ srv.setIndexHandler(myIndexHandler);
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + srv.identity.primaryPort + "/";
+});
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(URL, init, startCustomIndexHandler, stopCustomIndexHandler),
+ new Test(URL, init, startDefaultIndexHandler, stopDefaultIndexHandler),
+ ];
+});
+
+function init(ch) {
+ ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important!
+}
+function startCustomIndexHandler(ch) {
+ Assert.equal(ch.getResponseHeader("Content-Length"), "10");
+ srv.setIndexHandler(null);
+}
+function stopCustomIndexHandler(ch, status, data) {
+ Assert.ok(Components.isSuccessCode(status));
+ Assert.equal(String.fromCharCode.apply(null, data), "directory!");
+}
+
+function startDefaultIndexHandler(ch) {
+ Assert.equal(ch.responseStatus, 200);
+}
+function stopDefaultIndexHandler(ch, status, data) {
+ Assert.ok(Components.isSuccessCode(status));
+}
+
+// PATH HANDLERS
+
+function myIndexHandler(metadata, response) {
+ var dir = metadata.getProperty("directory");
+ Assert.ok(dir != null);
+ Assert.ok(dir instanceof Ci.nsIFile);
+ Assert.ok(dir.equals(serverBasePath));
+
+ response.write("directory!");
+}
diff --git a/netwerk/test/httpserver/test/test_setstatusline.js b/netwerk/test/httpserver/test/test_setstatusline.js
new file mode 100644
index 0000000000..f9cc189a68
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_setstatusline.js
@@ -0,0 +1,142 @@
+/* -*- 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/. */
+
+// exercise nsIHttpResponse.setStatusLine, ensure its atomicity, and ensure the
+// specified behavior occurs if it's not called
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+var srv;
+
+function run_test() {
+ srv = createServer();
+
+ srv.registerPathHandler("/no/setstatusline", noSetstatusline);
+ srv.registerPathHandler("/http1_0", http1_0);
+ srv.registerPathHandler("/http1_1", http1_1);
+ srv.registerPathHandler("/invalidVersion", invalidVersion);
+ srv.registerPathHandler("/invalidStatus", invalidStatus);
+ srv.registerPathHandler("/invalidDescription", invalidDescription);
+ srv.registerPathHandler("/crazyCode", crazyCode);
+ srv.registerPathHandler("/nullVersion", nullVersion);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+/** ***********
+ * UTILITIES *
+ *************/
+
+function checkStatusLine(
+ channel,
+ httpMaxVer,
+ httpMinVer,
+ httpCode,
+ statusText
+) {
+ Assert.equal(channel.responseStatus, httpCode);
+ Assert.equal(channel.responseStatusText, statusText);
+
+ var respMaj = {},
+ respMin = {};
+ channel.getResponseVersion(respMaj, respMin);
+ Assert.equal(respMaj.value, httpMaxVer);
+ Assert.equal(respMin.value, httpMinVer);
+}
+
+/** *******
+ * TESTS *
+ *********/
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(URL + "/no/setstatusline", null, startNoSetStatusLine, stop),
+ new Test(URL + "/http1_0", null, startHttp1_0, stop),
+ new Test(URL + "/http1_1", null, startHttp1_1, stop),
+ new Test(URL + "/invalidVersion", null, startPassedTrue, stop),
+ new Test(URL + "/invalidStatus", null, startPassedTrue, stop),
+ new Test(URL + "/invalidDescription", null, startPassedTrue, stop),
+ new Test(URL + "/crazyCode", null, startCrazy, stop),
+ new Test(URL + "/nullVersion", null, startNullVersion, stop),
+ ];
+});
+
+// /no/setstatusline
+function noSetstatusline(metadata, response) {}
+function startNoSetStatusLine(ch) {
+ checkStatusLine(ch, 1, 1, 200, "OK");
+}
+function stop(ch, status, data) {
+ Assert.ok(Components.isSuccessCode(status));
+}
+
+// /http1_0
+function http1_0(metadata, response) {
+ response.setStatusLine("1.0", 200, "OK");
+}
+function startHttp1_0(ch) {
+ checkStatusLine(ch, 1, 0, 200, "OK");
+}
+
+// /http1_1
+function http1_1(metadata, response) {
+ response.setStatusLine("1.1", 200, "OK");
+}
+function startHttp1_1(ch) {
+ checkStatusLine(ch, 1, 1, 200, "OK");
+}
+
+// /invalidVersion
+function invalidVersion(metadata, response) {
+ try {
+ response.setStatusLine(" 1.0", 200, "FAILED");
+ } catch (e) {
+ response.setHeader("Passed", "true", false);
+ }
+}
+function startPassedTrue(ch) {
+ checkStatusLine(ch, 1, 1, 200, "OK");
+ Assert.equal(ch.getResponseHeader("Passed"), "true");
+}
+
+// /invalidStatus
+function invalidStatus(metadata, response) {
+ try {
+ response.setStatusLine("1.0", 1000, "FAILED");
+ } catch (e) {
+ response.setHeader("Passed", "true", false);
+ }
+}
+
+// /invalidDescription
+function invalidDescription(metadata, response) {
+ try {
+ response.setStatusLine("1.0", 200, "FAILED\x01");
+ } catch (e) {
+ response.setHeader("Passed", "true", false);
+ }
+}
+
+// /crazyCode
+function crazyCode(metadata, response) {
+ response.setStatusLine("1.1", 617, "Crazy");
+}
+function startCrazy(ch) {
+ checkStatusLine(ch, 1, 1, 617, "Crazy");
+}
+
+// /nullVersion
+function nullVersion(metadata, response) {
+ response.setStatusLine(null, 255, "NULL");
+}
+function startNullVersion(ch) {
+ // currently, this server implementation defaults to 1.1
+ checkStatusLine(ch, 1, 1, 255, "NULL");
+}
diff --git a/netwerk/test/httpserver/test/test_sjs.js b/netwerk/test/httpserver/test/test_sjs.js
new file mode 100644
index 0000000000..51d92ec4e0
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_sjs.js
@@ -0,0 +1,243 @@
+/* -*- 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/. */
+
+// tests support for server JS-generated pages
+
+var srv = createServer();
+
+var sjs = do_get_file("data/sjs/cgi.sjs");
+// NB: The server has no state at this point -- all state is set up and torn
+// down in the tests, because we run the same tests twice with only a
+// different query string on the requests, followed by the oddball
+// test that doesn't care about throwing or not.
+srv.start(-1);
+const PORT = srv.identity.primaryPort;
+
+const BASE = "http://localhost:" + PORT;
+var test;
+var tests = [];
+
+/** *******************
+ * UTILITY FUNCTIONS *
+ *********************/
+
+function bytesToString(bytes) {
+ return bytes
+ .map(function (v) {
+ return String.fromCharCode(v);
+ })
+ .join("");
+}
+
+function skipCache(ch) {
+ ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+}
+
+/** ******************
+ * DEFINE THE TESTS *
+ ********************/
+
+/**
+ * Adds the set of tests defined in here, differentiating between tests with a
+ * SJS which throws an exception and creates a server error and tests with a
+ * normal, successful SJS.
+ */
+function setupTests(throwing) {
+ const TEST_URL = BASE + "/cgi.sjs" + (throwing ? "?throw" : "");
+
+ // registerFile with SJS => raw text
+
+ function setupFile(ch) {
+ srv.registerFile("/cgi.sjs", sjs);
+ skipCache(ch);
+ }
+
+ function verifyRawText(channel, status, bytes) {
+ dumpn(channel.originalURI.spec);
+ Assert.equal(bytesToString(bytes), fileContents(sjs));
+ }
+
+ test = new Test(TEST_URL, setupFile, null, verifyRawText);
+ tests.push(test);
+
+ // add mapping, => interpreted
+
+ function addTypeMapping(ch) {
+ srv.registerContentType("sjs", "sjs");
+ skipCache(ch);
+ }
+
+ function checkType(ch) {
+ if (throwing) {
+ Assert.ok(!ch.requestSucceeded);
+ Assert.equal(ch.responseStatus, 500);
+ } else {
+ Assert.equal(ch.contentType, "text/plain");
+ }
+ }
+
+ function checkContents(ch, status, data) {
+ if (!throwing) {
+ Assert.equal("PASS", bytesToString(data));
+ }
+ }
+
+ test = new Test(TEST_URL, addTypeMapping, checkType, checkContents);
+ tests.push(test);
+
+ // remove file/type mapping, map containing directory => raw text
+
+ function setupDirectoryAndRemoveType(ch) {
+ dumpn("removing type mapping");
+ srv.registerContentType("sjs", null);
+ srv.registerFile("/cgi.sjs", null);
+ srv.registerDirectory("/", sjs.parent);
+ skipCache(ch);
+ }
+
+ test = new Test(TEST_URL, setupDirectoryAndRemoveType, null, verifyRawText);
+ tests.push(test);
+
+ // add mapping, => interpreted
+
+ function contentAndCleanup(ch, status, data) {
+ checkContents(ch, status, data);
+
+ // clean up state we've set up
+ srv.registerDirectory("/", null);
+ srv.registerContentType("sjs", null);
+ }
+
+ test = new Test(TEST_URL, addTypeMapping, checkType, contentAndCleanup);
+ tests.push(test);
+
+ // NB: No remaining state in the server right now! If we have any here,
+ // either the second run of tests (without ?throw) or the tests added
+ // after the two sets will almost certainly fail.
+}
+
+/** ***************
+ * ADD THE TESTS *
+ *****************/
+
+setupTests(true);
+setupTests(false);
+
+// Test that when extension-mappings are used, the entire filename cannot be
+// treated as an extension -- there must be at least one dot for a filename to
+// match an extension.
+
+function init(ch) {
+ // clean up state we've set up
+ srv.registerDirectory("/", sjs.parent);
+ srv.registerContentType("sjs", "sjs");
+ skipCache(ch);
+}
+
+function checkNotSJS(ch, status, data) {
+ Assert.notEqual("FAIL", bytesToString(data));
+}
+
+test = new Test(BASE + "/sjs", init, null, checkNotSJS);
+tests.push(test);
+
+// Test that Range requests are passed through to the SJS file without
+// bounds checking.
+
+function rangeInit(expectedRangeHeader) {
+ return function setupRangeRequest(ch) {
+ ch.setRequestHeader("Range", expectedRangeHeader, false);
+ };
+}
+
+function checkRangeResult(ch) {
+ try {
+ var val = ch.getResponseHeader("Content-Range");
+ } catch (e) {
+ /* IDL doesn't specify a particular exception to require */
+ }
+ if (val !== undefined) {
+ do_throw(
+ "should not have gotten a Content-Range header, but got one " +
+ "with this value: " +
+ val
+ );
+ }
+ Assert.equal(200, ch.responseStatus);
+ Assert.equal("OK", ch.responseStatusText);
+}
+
+test = new Test(
+ BASE + "/range-checker.sjs",
+ rangeInit("not-a-bytes-equals-specifier"),
+ checkRangeResult,
+ null
+);
+tests.push(test);
+test = new Test(
+ BASE + "/range-checker.sjs",
+ rangeInit("bytes=-"),
+ checkRangeResult,
+ null
+);
+tests.push(test);
+test = new Test(
+ BASE + "/range-checker.sjs",
+ rangeInit("bytes=1000000-"),
+ checkRangeResult,
+ null
+);
+tests.push(test);
+test = new Test(
+ BASE + "/range-checker.sjs",
+ rangeInit("bytes=1-4"),
+ checkRangeResult,
+ null
+);
+tests.push(test);
+test = new Test(
+ BASE + "/range-checker.sjs",
+ rangeInit("bytes=-4"),
+ checkRangeResult,
+ null
+);
+tests.push(test);
+
+// One last test: for file mappings, the content-type is determined by the
+// extension of the file on the server, not by the extension of the requested
+// path.
+
+function setupFileMapping(ch) {
+ srv.registerFile("/script.html", sjs);
+}
+
+function onStart(ch) {
+ Assert.equal(ch.contentType, "text/plain");
+}
+
+function onStop(ch, status, data) {
+ Assert.equal("PASS", bytesToString(data));
+}
+
+test = new Test(BASE + "/script.html", setupFileMapping, onStart, onStop);
+tests.push(test);
+
+/** ***************
+ * RUN THE TESTS *
+ *****************/
+
+function run_test() {
+ // Test for a content-type which isn't a field-value
+ try {
+ srv.registerContentType("foo", "bar\nbaz");
+ throw new Error(
+ "this server throws on content-types which aren't field-values"
+ );
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_INVALID_ARG);
+ }
+ runHttpTests(tests, testComplete(srv));
+}
diff --git a/netwerk/test/httpserver/test/test_sjs_object_state.js b/netwerk/test/httpserver/test/test_sjs_object_state.js
new file mode 100644
index 0000000000..30c1e5c064
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_sjs_object_state.js
@@ -0,0 +1,305 @@
+/* -*- 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/. */
+
+/*
+ * Tests that the object-state-preservation mechanism works correctly.
+ */
+
+ChromeUtils.defineLazyGetter(this, "PATH", function () {
+ return "http://localhost:" + srv.identity.primaryPort + "/object-state.sjs";
+});
+
+var srv;
+
+function run_test() {
+ srv = createServer();
+ var sjsDir = do_get_file("data/sjs/");
+ srv.registerDirectory("/", sjsDir);
+ srv.registerContentType("sjs", "sjs");
+ srv.start(-1);
+
+ do_test_pending();
+
+ new HTTPTestLoader(PATH + "?state=initial", initialStart, initialStop);
+}
+
+/** ******************
+ * OBSERVER METHODS *
+ ********************/
+
+/*
+ * In practice the current implementation will guarantee an exact ordering of
+ * all start and stop callbacks. However, in the interests of robustness, this
+ * test will pass given any valid ordering of callbacks. Any ordering of calls
+ * which obeys the partial ordering shown by this directed acyclic graph will be
+ * handled correctly:
+ *
+ * initialStart
+ * |
+ * V
+ * intermediateStart
+ * |
+ * V
+ * intermediateStop
+ * | \
+ * | V
+ * | initialStop
+ * V
+ * triggerStart
+ * |
+ * V
+ * triggerStop
+ *
+ */
+
+var initialStarted = false;
+function initialStart(ch) {
+ dumpn("*** initialStart");
+
+ if (initialStarted) {
+ do_throw("initialStart: initialStarted is true?!?!");
+ }
+
+ initialStarted = true;
+
+ new HTTPTestLoader(
+ PATH + "?state=intermediate",
+ intermediateStart,
+ intermediateStop
+ );
+}
+
+var initialStopped = false;
+function initialStop(ch, status, data) {
+ dumpn("*** initialStop");
+
+ Assert.equal(
+ data
+ .map(function (v) {
+ return String.fromCharCode(v);
+ })
+ .join(""),
+ "done"
+ );
+
+ Assert.equal(srv.getObjectState("object-state-test"), null);
+
+ if (!initialStarted) {
+ do_throw("initialStop: initialStarted is false?!?!");
+ }
+ if (initialStopped) {
+ do_throw("initialStop: initialStopped is true?!?!");
+ }
+ if (!intermediateStarted) {
+ do_throw("initialStop: intermediateStarted is false?!?!");
+ }
+ if (!intermediateStopped) {
+ do_throw("initialStop: intermediateStopped is false?!?!");
+ }
+
+ initialStopped = true;
+
+ checkForFinish();
+}
+
+var intermediateStarted = false;
+function intermediateStart(ch) {
+ dumpn("*** intermediateStart");
+
+ Assert.notEqual(srv.getObjectState("object-state-test"), null);
+
+ if (!initialStarted) {
+ do_throw("intermediateStart: initialStarted is false?!?!");
+ }
+ if (intermediateStarted) {
+ do_throw("intermediateStart: intermediateStarted is true?!?!");
+ }
+
+ intermediateStarted = true;
+}
+
+var intermediateStopped = false;
+function intermediateStop(ch, status, data) {
+ dumpn("*** intermediateStop");
+
+ Assert.equal(
+ data
+ .map(function (v) {
+ return String.fromCharCode(v);
+ })
+ .join(""),
+ "intermediate"
+ );
+
+ Assert.notEqual(srv.getObjectState("object-state-test"), null);
+
+ if (!initialStarted) {
+ do_throw("intermediateStop: initialStarted is false?!?!");
+ }
+ if (!intermediateStarted) {
+ do_throw("intermediateStop: intermediateStarted is false?!?!");
+ }
+ if (intermediateStopped) {
+ do_throw("intermediateStop: intermediateStopped is true?!?!");
+ }
+
+ intermediateStopped = true;
+
+ new HTTPTestLoader(PATH + "?state=trigger", triggerStart, triggerStop);
+}
+
+var triggerStarted = false;
+function triggerStart(ch) {
+ dumpn("*** triggerStart");
+
+ if (!initialStarted) {
+ do_throw("triggerStart: initialStarted is false?!?!");
+ }
+ if (!intermediateStarted) {
+ do_throw("triggerStart: intermediateStarted is false?!?!");
+ }
+ if (!intermediateStopped) {
+ do_throw("triggerStart: intermediateStopped is false?!?!");
+ }
+ if (triggerStarted) {
+ do_throw("triggerStart: triggerStarted is true?!?!");
+ }
+
+ triggerStarted = true;
+}
+
+var triggerStopped = false;
+function triggerStop(ch, status, data) {
+ dumpn("*** triggerStop");
+
+ Assert.equal(
+ data
+ .map(function (v) {
+ return String.fromCharCode(v);
+ })
+ .join(""),
+ "trigger"
+ );
+
+ if (!initialStarted) {
+ do_throw("triggerStop: initialStarted is false?!?!");
+ }
+ if (!intermediateStarted) {
+ do_throw("triggerStop: intermediateStarted is false?!?!");
+ }
+ if (!intermediateStopped) {
+ do_throw("triggerStop: intermediateStopped is false?!?!");
+ }
+ if (!triggerStarted) {
+ do_throw("triggerStop: triggerStarted is false?!?!");
+ }
+ if (triggerStopped) {
+ do_throw("triggerStop: triggerStopped is false?!?!");
+ }
+
+ triggerStopped = true;
+
+ checkForFinish();
+}
+
+var finished = false;
+function checkForFinish() {
+ if (finished) {
+ try {
+ do_throw("uh-oh, how are we being finished twice?!?!");
+ } finally {
+ // eslint-disable-next-line no-undef
+ quit(1);
+ }
+ }
+
+ if (triggerStopped && initialStopped) {
+ finished = true;
+ try {
+ Assert.equal(srv.getObjectState("object-state-test"), null);
+
+ if (!initialStarted) {
+ do_throw("checkForFinish: initialStarted is false?!?!");
+ }
+ if (!intermediateStarted) {
+ do_throw("checkForFinish: intermediateStarted is false?!?!");
+ }
+ if (!intermediateStopped) {
+ do_throw("checkForFinish: intermediateStopped is false?!?!");
+ }
+ if (!triggerStarted) {
+ do_throw("checkForFinish: triggerStarted is false?!?!");
+ }
+ } finally {
+ srv.stop(do_test_finished);
+ }
+ }
+}
+
+/** *******************************
+ * UTILITY OBSERVABLE URL LOADER *
+ *********************************/
+
+/** Stream listener for the channels. */
+function HTTPTestLoader(path, start, stop) {
+ /** Path to load. */
+ this._path = path;
+
+ /** Array of bytes of data in body of response. */
+ this._data = [];
+
+ /** onStartRequest callback. */
+ this._start = start;
+
+ /** onStopRequest callback. */
+ this._stop = stop;
+
+ var channel = makeChannel(path);
+ channel.asyncOpen(this);
+}
+HTTPTestLoader.prototype = {
+ onStartRequest(request) {
+ dumpn("*** HTTPTestLoader.onStartRequest for " + this._path);
+
+ var ch = request
+ .QueryInterface(Ci.nsIHttpChannel)
+ .QueryInterface(Ci.nsIHttpChannelInternal);
+
+ try {
+ try {
+ this._start(ch);
+ } catch (e) {
+ do_throw(this._path + ": error in onStartRequest: " + e);
+ }
+ } catch (e) {
+ dumpn(
+ "!!! swallowing onStartRequest exception so onStopRequest is " +
+ "called..."
+ );
+ }
+ },
+ onDataAvailable(request, inputStream, offset, count) {
+ dumpn("*** HTTPTestLoader.onDataAvailable for " + this._path);
+
+ Array.prototype.push.apply(
+ this._data,
+ makeBIS(inputStream).readByteArray(count)
+ );
+ },
+ onStopRequest(request, status) {
+ dumpn("*** HTTPTestLoader.onStopRequest for " + this._path);
+
+ var ch = request
+ .QueryInterface(Ci.nsIHttpChannel)
+ .QueryInterface(Ci.nsIHttpChannelInternal);
+
+ this._stop(ch, status, this._data);
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+};
diff --git a/netwerk/test/httpserver/test/test_sjs_state.js b/netwerk/test/httpserver/test/test_sjs_state.js
new file mode 100644
index 0000000000..4d1fc37930
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_sjs_state.js
@@ -0,0 +1,203 @@
+/* -*- 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/. */
+
+// exercises the server's state-preservation API
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+var srv;
+
+function run_test() {
+ srv = createServer();
+ var sjsDir = do_get_file("data/sjs/");
+ srv.registerDirectory("/", sjsDir);
+ srv.registerContentType("sjs", "sjs");
+ srv.registerPathHandler("/path-handler", pathHandler);
+ srv.start(-1);
+
+ function done() {
+ Assert.equal(srv.getSharedState("shared-value"), "done!");
+ Assert.equal(
+ srv.getState("/path-handler", "private-value"),
+ "pathHandlerPrivate2"
+ );
+ Assert.equal(srv.getState("/state1.sjs", "private-value"), "");
+ Assert.equal(srv.getState("/state2.sjs", "private-value"), "newPrivate5");
+ do_test_pending();
+ srv.stop(function () {
+ do_test_finished();
+ });
+ }
+
+ runHttpTests(tests, done);
+}
+
+/** **********
+ * HANDLERS *
+ ************/
+
+var firstTime = true;
+
+function pathHandler(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ response.setHeader(
+ "X-Old-Shared-Value",
+ srv.getSharedState("shared-value"),
+ false
+ );
+ response.setHeader(
+ "X-Old-Private-Value",
+ srv.getState("/path-handler", "private-value"),
+ false
+ );
+
+ var privateValue, sharedValue;
+ if (firstTime) {
+ firstTime = false;
+ privateValue = "pathHandlerPrivate";
+ sharedValue = "pathHandlerShared";
+ } else {
+ privateValue = "pathHandlerPrivate2";
+ sharedValue = "";
+ }
+
+ srv.setState("/path-handler", "private-value", privateValue);
+ srv.setSharedState("shared-value", sharedValue);
+
+ response.setHeader("X-New-Private-Value", privateValue, false);
+ response.setHeader("X-New-Shared-Value", sharedValue, false);
+}
+
+/** *************
+ * BEGIN TESTS *
+ ***************/
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ return [
+ new Test(
+ // eslint-disable-next-line no-useless-concat
+ URL + "/state1.sjs?" + "newShared=newShared&newPrivate=newPrivate",
+ null,
+ start_initial,
+ null
+ ),
+ new Test(
+ // eslint-disable-next-line no-useless-concat
+ URL + "/state1.sjs?" + "newShared=newShared2&newPrivate=newPrivate2",
+ null,
+ start_overwrite,
+ null
+ ),
+ new Test(
+ // eslint-disable-next-line no-useless-concat
+ URL + "/state1.sjs?" + "newShared=&newPrivate=newPrivate3",
+ null,
+ start_remove,
+ null
+ ),
+ new Test(URL + "/path-handler", null, start_handler, null),
+ new Test(URL + "/path-handler", null, start_handler_again, null),
+ new Test(
+ // eslint-disable-next-line no-useless-concat
+ URL + "/state2.sjs?" + "newShared=newShared4&newPrivate=newPrivate4",
+ null,
+ start_other_initial,
+ null
+ ),
+ new Test(
+ // eslint-disable-next-line no-useless-concat
+ URL + "/state2.sjs?" + "newShared=",
+ null,
+ start_other_remove_ignore,
+ null
+ ),
+ new Test(
+ // eslint-disable-next-line no-useless-concat
+ URL + "/state2.sjs?" + "newShared=newShared5&newPrivate=newPrivate5",
+ null,
+ start_other_set_new,
+ null
+ ),
+ new Test(
+ // eslint-disable-next-line no-useless-concat
+ URL + "/state1.sjs?" + "newShared=done!&newPrivate=",
+ null,
+ start_set_remove_original,
+ null
+ ),
+ ];
+});
+
+/* Hack around bug 474845 for now. */
+function getHeaderFunction(ch) {
+ function getHeader(name) {
+ try {
+ return ch.getResponseHeader(name);
+ } catch (e) {
+ if (e.result !== Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw e;
+ }
+ }
+ return "";
+ }
+ return getHeader;
+}
+
+function expectValues(ch, oldShared, newShared, oldPrivate, newPrivate) {
+ var getHeader = getHeaderFunction(ch);
+
+ Assert.equal(ch.responseStatus, 200);
+ Assert.equal(getHeader("X-Old-Shared-Value"), oldShared);
+ Assert.equal(getHeader("X-New-Shared-Value"), newShared);
+ Assert.equal(getHeader("X-Old-Private-Value"), oldPrivate);
+ Assert.equal(getHeader("X-New-Private-Value"), newPrivate);
+}
+
+function start_initial(ch) {
+ dumpn("XXX start_initial");
+ expectValues(ch, "", "newShared", "", "newPrivate");
+}
+
+function start_overwrite(ch) {
+ expectValues(ch, "newShared", "newShared2", "newPrivate", "newPrivate2");
+}
+
+function start_remove(ch) {
+ expectValues(ch, "newShared2", "", "newPrivate2", "newPrivate3");
+}
+
+function start_handler(ch) {
+ expectValues(ch, "", "pathHandlerShared", "", "pathHandlerPrivate");
+}
+
+function start_handler_again(ch) {
+ expectValues(
+ ch,
+ "pathHandlerShared",
+ "",
+ "pathHandlerPrivate",
+ "pathHandlerPrivate2"
+ );
+}
+
+function start_other_initial(ch) {
+ expectValues(ch, "", "newShared4", "", "newPrivate4");
+}
+
+function start_other_remove_ignore(ch) {
+ expectValues(ch, "newShared4", "", "newPrivate4", "");
+}
+
+function start_other_set_new(ch) {
+ expectValues(ch, "", "newShared5", "newPrivate4", "newPrivate5");
+}
+
+function start_set_remove_original(ch) {
+ expectValues(ch, "newShared5", "done!", "newPrivate3", "");
+}
diff --git a/netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js b/netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js
new file mode 100644
index 0000000000..8610dcb7fb
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js
@@ -0,0 +1,73 @@
+/* -*- 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/. */
+
+/*
+ * Tests that running an SJS a whole lot of times doesn't have any ill effects
+ * (like exceeding open-file limits due to not closing the SJS file each time,
+ * then preventing any file from being opened).
+ */
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+var srv;
+
+function run_test() {
+ srv = createServer();
+ var sjsDir = do_get_file("data/sjs/");
+ srv.registerDirectory("/", sjsDir);
+ srv.registerContentType("sjs", "sjs");
+ srv.start(-1);
+
+ function done() {
+ do_test_pending();
+ srv.stop(function () {
+ do_test_finished();
+ });
+ Assert.equal(gStartCount, TEST_RUNS);
+ Assert.ok(lastPassed);
+ }
+
+ runHttpTests(tests, done);
+}
+
+/** *************
+ * BEGIN TESTS *
+ ***************/
+
+var gStartCount = 0;
+var lastPassed = false;
+
+// This hits the open-file limit for me on OS X; your mileage may vary.
+const TEST_RUNS = 250;
+
+ChromeUtils.defineLazyGetter(this, "tests", function () {
+ var _tests = new Array(TEST_RUNS + 1);
+ var _test = new Test(URL + "/thrower.sjs?throw", null, start_thrower);
+ for (var i = 0; i < TEST_RUNS; i++) {
+ _tests[i] = _test;
+ }
+ // ...and don't forget to stop!
+ _tests[TEST_RUNS] = new Test(URL + "/thrower.sjs", null, start_last);
+ return _tests;
+});
+
+function start_thrower(ch) {
+ Assert.equal(ch.responseStatus, 500);
+ Assert.ok(!ch.requestSucceeded);
+
+ gStartCount++;
+}
+
+function start_last(ch) {
+ Assert.equal(ch.responseStatus, 200);
+ Assert.ok(ch.requestSucceeded);
+
+ Assert.equal(ch.getResponseHeader("X-Test-Status"), "PASS");
+
+ lastPassed = true;
+}
diff --git a/netwerk/test/httpserver/test/test_start_stop.js b/netwerk/test/httpserver/test/test_start_stop.js
new file mode 100644
index 0000000000..9dcd7c673a
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_start_stop.js
@@ -0,0 +1,166 @@
+/* -*- 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/. */
+
+/*
+ * Tests for correct behavior of the server start() and stop() methods.
+ */
+
+ChromeUtils.defineLazyGetter(this, "PORT", function () {
+ return srv.identity.primaryPort;
+});
+
+ChromeUtils.defineLazyGetter(this, "PREPATH", function () {
+ return "http://localhost:" + PORT;
+});
+
+var srv, srv2;
+
+function run_test() {
+ if (mozinfo.os == "win") {
+ dumpn(
+ "*** not running test_start_stop.js on Windows for now, because " +
+ "Windows is dumb"
+ );
+ return;
+ }
+
+ dumpn("*** run_test");
+
+ srv = createServer();
+ srv.start(-1);
+
+ try {
+ srv.start(PORT);
+ do_throw("starting a started server");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_ALREADY_INITIALIZED);
+ }
+
+ do_test_pending();
+ srv.stop(function () {
+ try {
+ do_test_pending();
+ run_test_2();
+ } finally {
+ do_test_finished();
+ }
+ });
+}
+
+function run_test_2() {
+ dumpn("*** run_test_2");
+
+ do_test_finished();
+
+ srv.start(PORT);
+ srv2 = createServer();
+
+ try {
+ srv2.start(PORT);
+ do_throw("two servers on one port?");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ do_test_pending();
+ try {
+ srv.stop({
+ onStopped() {
+ try {
+ do_test_pending();
+ run_test_3();
+ } finally {
+ do_test_finished();
+ }
+ },
+ });
+ } catch (e) {
+ do_throw("error stopping with an object: " + e);
+ }
+}
+
+function run_test_3() {
+ dumpn("*** run_test_3");
+
+ do_test_finished();
+
+ srv.start(PORT);
+
+ do_test_pending();
+ try {
+ srv.stop().then(function () {
+ try {
+ do_test_pending();
+ run_test_4();
+ } finally {
+ do_test_finished();
+ }
+ });
+ } catch (e) {
+ do_throw("error stopping with an object: " + e);
+ }
+}
+
+function run_test_4() {
+ dumpn("*** run_test_4");
+
+ do_test_finished();
+
+ srv.registerPathHandler("/handle", handle);
+ srv.start(PORT);
+
+ // Don't rely on the exact (but implementation-constant) sequence of events
+ // as it currently exists by making either run_test_5 or serverStopped handle
+ // the final shutdown.
+ do_test_pending();
+
+ runHttpTests([new Test(PREPATH + "/handle")], run_test_5);
+}
+
+var testsComplete = false;
+
+function run_test_5() {
+ dumpn("*** run_test_5");
+
+ testsComplete = true;
+ if (stopped) {
+ do_test_finished();
+ }
+}
+
+const INTERVAL = 500;
+
+function handle(request, response) {
+ response.processAsync();
+
+ dumpn("*** stopping server...");
+ srv.stop(serverStopped);
+
+ callLater(INTERVAL, function () {
+ Assert.ok(!stopped);
+
+ callLater(INTERVAL, function () {
+ Assert.ok(!stopped);
+ response.finish();
+
+ try {
+ response.processAsync();
+ do_throw("late processAsync didn't throw?");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_UNEXPECTED);
+ }
+ });
+ });
+}
+
+var stopped = false;
+function serverStopped() {
+ dumpn("*** server really, fully shut down now");
+ stopped = true;
+ if (testsComplete) {
+ do_test_finished();
+ }
+}
diff --git a/netwerk/test/httpserver/test/test_start_stop_ipv6.js b/netwerk/test/httpserver/test/test_start_stop_ipv6.js
new file mode 100644
index 0000000000..060605d0e3
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_start_stop_ipv6.js
@@ -0,0 +1,166 @@
+/* -*- 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/. */
+
+/*
+ * Tests for correct behavior of the server start_ipv6() and stop() methods.
+ */
+
+ChromeUtils.defineLazyGetter(this, "PORT", function () {
+ return srv.identity.primaryPort;
+});
+
+ChromeUtils.defineLazyGetter(this, "PREPATH", function () {
+ return "http://localhost:" + PORT;
+});
+
+var srv, srv2;
+
+function run_test() {
+ if (mozinfo.os == "win") {
+ dumpn(
+ "*** not running test_start_stop.js on Windows for now, because " +
+ "Windows is dumb"
+ );
+ return;
+ }
+
+ dumpn("*** run_test");
+
+ srv = createServer();
+ srv.start_ipv6(-1);
+
+ try {
+ srv.start_ipv6(PORT);
+ do_throw("starting a started server");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_ALREADY_INITIALIZED);
+ }
+
+ do_test_pending();
+ srv.stop(function () {
+ try {
+ do_test_pending();
+ run_test_2();
+ } finally {
+ do_test_finished();
+ }
+ });
+}
+
+function run_test_2() {
+ dumpn("*** run_test_2");
+
+ do_test_finished();
+
+ srv.start_ipv6(PORT);
+ srv2 = createServer();
+
+ try {
+ srv2.start_ipv6(PORT);
+ do_throw("two servers on one port?");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ do_test_pending();
+ try {
+ srv.stop({
+ onStopped() {
+ try {
+ do_test_pending();
+ run_test_3();
+ } finally {
+ do_test_finished();
+ }
+ },
+ });
+ } catch (e) {
+ do_throw("error stopping with an object: " + e);
+ }
+}
+
+function run_test_3() {
+ dumpn("*** run_test_3");
+
+ do_test_finished();
+
+ srv.start_ipv6(PORT);
+
+ do_test_pending();
+ try {
+ srv.stop().then(function () {
+ try {
+ do_test_pending();
+ run_test_4();
+ } finally {
+ do_test_finished();
+ }
+ });
+ } catch (e) {
+ do_throw("error stopping with an object: " + e);
+ }
+}
+
+function run_test_4() {
+ dumpn("*** run_test_4");
+
+ do_test_finished();
+
+ srv.registerPathHandler("/handle", handle);
+ srv.start_ipv6(PORT);
+
+ // Don't rely on the exact (but implementation-constant) sequence of events
+ // as it currently exists by making either run_test_5 or serverStopped handle
+ // the final shutdown.
+ do_test_pending();
+
+ runHttpTests([new Test(PREPATH + "/handle")], run_test_5);
+}
+
+var testsComplete = false;
+
+function run_test_5() {
+ dumpn("*** run_test_5");
+
+ testsComplete = true;
+ if (stopped) {
+ do_test_finished();
+ }
+}
+
+const INTERVAL = 500;
+
+function handle(request, response) {
+ response.processAsync();
+
+ dumpn("*** stopping server...");
+ srv.stop(serverStopped);
+
+ callLater(INTERVAL, function () {
+ Assert.ok(!stopped);
+
+ callLater(INTERVAL, function () {
+ Assert.ok(!stopped);
+ response.finish();
+
+ try {
+ response.processAsync();
+ do_throw("late processAsync didn't throw?");
+ } catch (e) {
+ isException(e, Cr.NS_ERROR_UNEXPECTED);
+ }
+ });
+ });
+}
+
+var stopped = false;
+function serverStopped() {
+ dumpn("*** server really, fully shut down now");
+ stopped = true;
+ if (testsComplete) {
+ do_test_finished();
+ }
+}
diff --git a/netwerk/test/httpserver/test/xpcshell.toml b/netwerk/test/httpserver/test/xpcshell.toml
new file mode 100644
index 0000000000..0a687e0363
--- /dev/null
+++ b/netwerk/test/httpserver/test/xpcshell.toml
@@ -0,0 +1,68 @@
+[DEFAULT]
+head = "head_utils.js"
+support-files = ["data/**"]
+
+["test_async_response_sending.js"]
+
+["test_basic_functionality.js"]
+
+["test_body_length.js"]
+
+["test_byte_range.js"]
+
+["test_cern_meta.js"]
+
+["test_default_index_handler.js"]
+
+["test_empty_body.js"]
+
+["test_errorhandler_exception.js"]
+
+["test_header_array.js"]
+
+["test_headers.js"]
+
+["test_host.js"]
+skip-if = ["os == 'mac'"]
+run-sequentially = "Reusing same server on different specific ports."
+
+["test_host_identity.js"]
+
+["test_linedata.js"]
+
+["test_load_module.js"]
+
+["test_name_scheme.js"]
+
+["test_processasync.js"]
+
+["test_qi.js"]
+
+["test_registerdirectory.js"]
+
+["test_registerfile.js"]
+
+["test_registerprefix.js"]
+
+["test_request_line_split_in_two_packets.js"]
+
+["test_response_write.js"]
+
+["test_seizepower.js"]
+skip-if = ["os == 'mac' && socketprocess_networking"]
+
+["test_setindexhandler.js"]
+
+["test_setstatusline.js"]
+
+["test_sjs.js"]
+
+["test_sjs_object_state.js"]
+
+["test_sjs_state.js"]
+
+["test_sjs_throwing_exceptions.js"]
+
+["test_start_stop.js"]
+
+["test_start_stop_ipv6.js"]