From 6262239dc74502cf6a2b5b10a4b6dfdce4f0d5ea Mon Sep 17 00:00:00 2001 From: "jwalden%mit.edu" Date: Fri, 15 Dec 2006 02:13:54 +0000 Subject: [PATCH] Bug 342877 - Substantially revamp/rewrite the testing-only HTTP server used in necko unit tests, making it suitable for use in Mozilla tests which require files be stored on an HTTP server (e.g. because the test depends on certain HTTP headers being set or not set, the functionality uses HTTP, etc.) but which don't require that the server have a specified hostname. Docs, implementation, and tests (!) are in netwerk/test/httpserver/; if you have questions about using the server in unit tests or have some use case which the server could support better or doesn't support at all, CC me on the bug and I'll see how I can help. Huge (huge) thanks to biesi for the review of the huge patch. r=davel on test-harness changes, r=biesi on the server and test changes git-svn-id: svn://10.0.0.236/trunk@217001 18797224-902f-48f8-a5cc-f745e15eee43 --- mozilla/netwerk/test/Makefile.in | 4 + mozilla/netwerk/test/httpserver/Makefile.in | 81 + mozilla/netwerk/test/httpserver/README | 97 + mozilla/netwerk/test/httpserver/TODO | 22 + mozilla/netwerk/test/httpserver/httpd.js | 2505 +++++++++++++++++ .../netwerk/test/httpserver/nsIHttpServer.idl | 320 +++ .../test/httpserver/test/head_utils.js | 81 + .../test/test_basic_functionality.js | 201 ++ .../test/httpserver/test/test_empty_body.js | 120 + .../test/test_errorhandler_exception.js | 152 + .../test/httpserver/test/test_headers.js | 221 ++ .../httpserver/test/test_registerdirectory.js | 314 +++ .../httpserver/test/test_setstatusline.js | 225 ++ mozilla/netwerk/test/unit/head_http_server.js | 226 -- .../netwerk/test/unit/test_authentication.js | 53 +- mozilla/netwerk/test/unit/test_bug331825.js | 22 +- .../netwerk/test/unit/test_content_sniffer.js | 7 +- .../netwerk/test/unit/test_cookie_header.js | 8 +- mozilla/netwerk/test/unit/test_event_sink.js | 31 +- mozilla/netwerk/test/unit/test_httpcancel.js | 14 +- .../test-harness/xpcshell-simple/head.js | 13 + .../test-harness/xpcshell-simple/test_all.sh | 2 +- 22 files changed, 4472 insertions(+), 247 deletions(-) create mode 100644 mozilla/netwerk/test/httpserver/Makefile.in create mode 100644 mozilla/netwerk/test/httpserver/README create mode 100644 mozilla/netwerk/test/httpserver/TODO create mode 100644 mozilla/netwerk/test/httpserver/httpd.js create mode 100644 mozilla/netwerk/test/httpserver/nsIHttpServer.idl create mode 100644 mozilla/netwerk/test/httpserver/test/head_utils.js create mode 100644 mozilla/netwerk/test/httpserver/test/test_basic_functionality.js create mode 100644 mozilla/netwerk/test/httpserver/test/test_empty_body.js create mode 100644 mozilla/netwerk/test/httpserver/test/test_errorhandler_exception.js create mode 100644 mozilla/netwerk/test/httpserver/test/test_headers.js create mode 100644 mozilla/netwerk/test/httpserver/test/test_registerdirectory.js create mode 100644 mozilla/netwerk/test/httpserver/test/test_setstatusline.js delete mode 100644 mozilla/netwerk/test/unit/head_http_server.js diff --git a/mozilla/netwerk/test/Makefile.in b/mozilla/netwerk/test/Makefile.in index 0fd24443155..3eb26e7a297 100644 --- a/mozilla/netwerk/test/Makefile.in +++ b/mozilla/netwerk/test/Makefile.in @@ -45,6 +45,10 @@ include $(DEPTH)/config/autoconf.mk MODULE = test_necko +DIRS = \ + httpserver \ + $(NULL) + REQUIRES = xpcom \ string \ necko \ diff --git a/mozilla/netwerk/test/httpserver/Makefile.in b/mozilla/netwerk/test/httpserver/Makefile.in new file mode 100644 index 00000000000..129786431ca --- /dev/null +++ b/mozilla/netwerk/test/httpserver/Makefile.in @@ -0,0 +1,81 @@ +# vim: noexpandtab ts=8 sw=8 +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is MozJSHTTP code. +# +# The Initial Developer of the Original Code is +# Jeff Walden . +# Portions created by the Initial Developer are Copyright (C) 2006 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = test_necko + +XPIDLSRCS = \ + nsIHttpServer.idl \ + $(NULL) + +include $(topsrcdir)/config/rules.mk + +_UNIT_FILES := $(wildcard $(srcdir)/test/*.js) + +_SERVER_SCRIPT_FILES = \ + httpd.js \ + $(NULL) + +# copy the server to the xpcshell test harness directory +libs:: $(_SERVER_SCRIPT_FILES) + $(INSTALL) $^ $(DIST)/bin/test-harness/xpcshell-simple + +# +# Server tests don't go in with necko's tests because: +# +# - we then have to think about file name conflicts in two different locations +# - head_*.js files in separate locations are both run, even when this is not +# desired (e.g., we must either always import httpd.js [slowing down test +# processing even for tests which don't need it] or we must manually import +# it in every test that uses it [which would be extremely annoying for the +# server tests which all want it]) +# - httpd.js doesn't clutter the global scope too much, but it does become a +# concern (consider Cc/Ci/Cr in particular, which as constants cannot be +# safely redefined) +# +libs:: $(_UNIT_FILES) + $(INSTALL) $^ $(DIST)/bin/httpserver_tests + +check:: + $(RUN_TEST_PROGRAM) $(DIST)/bin/test_all.sh $(DIST)/bin/httpserver_tests diff --git a/mozilla/netwerk/test/httpserver/README b/mozilla/netwerk/test/httpserver/README new file mode 100644 index 00000000000..9e2d0b976c1 --- /dev/null +++ b/mozilla/netwerk/test/httpserver/README @@ -0,0 +1,97 @@ +MozJSHTTP README +================ + +MozJSHTTP is a small cross-platform implementation of an HTTP/1.1 server in +JavaScript for the Mozilla platform. + +MozJSHTTP 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 MozJSHTTP 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 MozJSHTTP. See + 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, create an instance of the server using the following command: + + var server = Components.classes["@mozilla.org/server/jshttp;1"] + .createInstance(Components.interfaces.nsIHttpServer); + +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(); + + +Using MozJSHTTP as an Inline Script or from xpcshell +---------------------------------------------------- + +Using MozJSHTTP 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 MozJSHTTP 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 MozJSHTTP as an XPCOM component +whenever possible. + + +Known Issues +------------ + +While MozJSHTTP runs on Mozilla 1.8 and 1.9 platforms, it doesn't run quite as +well on 1.8 due to the absence of some APIs, specifically the threading APIs. +The biggest problem here is that server shutdown (see nsIHttpServer.stop) is not +guaranteed to complete after all pending requests have been served; if you are +using the server in 1.8 code, you should probably wait a few seconds after +calling server.stop() before the host application closes to ensure that all +requests have completed. Things probably aren't going to break too horribly if +you don't do this, but better safe than sorry. + +To be clear: the guarantee that nsIHttpServer.stop says implementations should +make when possible (that .stop returns only when all pending requests have been +serviced) cannot be made in a 1.8 environment; it can be made in a 1.9 +environment. Use 1.9 if this matters to you, or hack around it as described +here. + + +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 MozJSHTTP, if you need one. + +Have fun! diff --git a/mozilla/netwerk/test/httpserver/TODO b/mozilla/netwerk/test/httpserver/TODO new file mode 100644 index 00000000000..e21dbdbaa61 --- /dev/null +++ b/mozilla/netwerk/test/httpserver/TODO @@ -0,0 +1,22 @@ +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: +- mod_cern_meta or Apache asis functionality (probably the former, since + asis+binary files looks annoying but is a definite want, and line endings are + likely a pain): + and + +- 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/mozilla/netwerk/test/httpserver/httpd.js b/mozilla/netwerk/test/httpserver/httpd.js new file mode 100644 index 00000000000..5257a84c1b0 --- /dev/null +++ b/mozilla/netwerk/test/httpserver/httpd.js @@ -0,0 +1,2505 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the MozJSHTTP server. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Darin Fisher (v1, netwerk/test/TestServ.js) + * Christian Biesinger (v2, netwerk/test/unit/head_http_server.js) + * Jeff Walden (v3, netwerk/test/httpserver/httpd.js) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* + * 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 + * MozJSHTTP. + */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +/** + * 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 (debugEnabled() && !cond) + { + dumpn("###!!!"); + dumpn("###!!! ASSERTION: " + msg); + dumpn("###!!! Stack follows:"); + + var stack = new Error().stack.split(/\n/); + dumpn(stack.map(function(val) { return "###!!! " + val; }).join("\n")); + + throw msg; + } +} + +/** Constructs an HTTP error object. */ +function HttpError(code, description) +{ + this.code = code; + this.description = description; +} + +/** + * Errors thrown to trigger specific HTTP server responses. + */ +const HTTP_400 = new HttpError(400, "Bad Request"); +const HTTP_401 = new HttpError(401, "Unauthorized"); +const HTTP_402 = new HttpError(402, "Payment Required"); +const HTTP_403 = new HttpError(403, "Forbidden"); +const HTTP_404 = new HttpError(404, "Not Found"); +const HTTP_405 = new HttpError(405, "Method Not Allowed"); +const HTTP_406 = new HttpError(406, "Not Acceptable"); +const HTTP_407 = new HttpError(407, "Proxy Authentication Required"); +const HTTP_408 = new HttpError(408, "Request Timeout"); +const HTTP_409 = new HttpError(409, "Conflict"); +const HTTP_410 = new HttpError(410, "Gone"); +const HTTP_411 = new HttpError(411, "Length Required"); +const HTTP_412 = new HttpError(412, "Precondition Failed"); +const HTTP_413 = new HttpError(413, "Request Entity Too Large"); +const HTTP_414 = new HttpError(414, "Request-URI Too Long"); +const HTTP_415 = new HttpError(415, "Unsupported Media Type"); +const HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable"); +const HTTP_417 = new HttpError(417, "Expectation Failed"); + +const HTTP_500 = new HttpError(500, "Internal Server Error"); +const HTTP_501 = new HttpError(501, "Not Implemented"); +const HTTP_502 = new HttpError(502, "Bad Gateway"); +const HTTP_503 = new HttpError(503, "Service Unavailable"); +const HTTP_504 = new HttpError(504, "Gateway Timeout"); +const 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))); + +/** + * Returns true if debugging is enabled, false otherwise. + */ +function debugEnabled() +{ + return true; // this code's pretty new and has no good set of tests (yet) +} + +/** dump(str) with a trailing "\n" */ +function dumpn(str) +{ + if (debugEnabled()) + dump(str + "\n"); +} + + +/** + * Returns the RFC 822/1123 representation of a date. + * + * @param date + * the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT + * @returns + * the string specifying 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 + * the date as a JavaScript Date object + * @returns + * 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 + * the date as a JavaScript Date object + * @returns + * 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)); +} + +/** + * Prints out a human-readable representation of the object o and its fields, + * omitting those whose names begin with "_" if showMembers != true (to ignore + * hidden properties exposed via getters/setters). + */ +function printObj(o, showMembers) +{ + var s = "******************************\n"; + s += "o = {\n"; + for (var i in o) + { + if (typeof(i) != "string" || + (showMembers || (i.length > 0 && i[0] != "_"))) + s+= " " + i + ": " + o[i] + ",\n"; + } + s += " };\n"; + s += "******************************"; + dumpn(s); +} + +/** + * Instantiates a new HTTP server. + */ +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); + + /** + * 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; +} +nsHttpServer.prototype = +{ + // NSISERVERSOCKETLISTENER + + /** + * This function signals that a new connection has been accepted. It is the + * method through which all requests must be handled, and by the end of this + * method any and all response requests must be sent. + * + * @see nsIServerSocketListener.onSocketAccepted + */ + onSocketAccepted: function(serverSocket, trans) + { + dumpn(">>> new connection on " + trans.host + ":" + trans.port); + + var input = trans.openInputStream(Ci.nsITransport.OPEN_BLOCKING, 0, 0); + var output = trans.openOutputStream(Ci.nsITransport.OPEN_BLOCKING, 0, 0); + + this._processConnection(serverSocket.port, input, output); + }, + + /** + * Called when the socket associated with this is closed. + * + * @see nsIServerSocketListener.onStopListening + */ + onStopListening: function(serverSocket, status) + { + dumpn(">>> shutting down server"); + this._socketClosed = true; + }, + + // NSIHTTPSERVER + + // + // see nsIHttpServer.start + // + start: function(port) + { + if (this._socket) + throw Cr.NS_ERROR_ALREADY_INITIALIZED; + + this._port = port; + this._doQuit = this._socketClosed = false; + + var socket = Cc["@mozilla.org/network/server-socket;1"] + .createInstance(Ci.nsIServerSocket); + socket.init(this._port, + true, // loopback only + -1); // default number of pending connections + + dumpn(">>> listening on port " + socket.port); + socket.asyncListen(this); + this._socket = socket; + }, + + // + // see nsIHttpServer.stop + // + stop: function() + { + if (!this._socket) + return; + + dumpn(">>> stopping listening on port " + this._socket.port); + this._socket.close(); + this._socket = null; + this._doQuit = false; + + // spin an event loop and wait for the socket-close notification, if we can; + // this is only possible in Mozilla 1.9 code, but in situations where + // nothing really horrible happens at shutdown, nothing bad will happen in + // Mozilla 1.8-based code (and we want to support that) + if ("@mozilla.org/thread-manager;1" in Cc) + { + var thr = Cc["@mozilla.org/thread-manager;1"] + .getService() + .currentThread; + while (!this._socketClosed || this._handler.hasPendingRequests()) + thr.processNextEvent(true); + } + }, + + // + // see nsIHttpServer.registerFile + // + registerFile: function(path, file) + { + if (!file.exists() || lf.isDirectory()) + throw Cr.NS_ERROR_INVALID_ARG; + + this._handler.registerFile(path, file); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory: function(path, directory) + { + // XXX true path validation! + if (path.charAt(0) != "/" || + path.charAt(path.length - 1) != "/" || + (directory && + (!directory.exists() || !directory.isDirectory()))) + throw Cr.NS_ERROR_INVALID_ARG; + + // XXX determine behavior of non-existent /foo/bar when a /foo/bar/ mapping + // exists! + + this._handler.registerDirectory(path, directory); + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler: function(path, handler) + { + this._handler.registerPathHandler(path, handler); + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler: function(code, handler) + { + this._handler.registerErrorHandler(code, handler); + }, + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpServer) || + iid.equals(Ci.nsIServerSocketListener) || + iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + // 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: function() + { + return this._socketClosed && !this._handler.hasPendingRequests(); + }, + + // PRIVATE IMPLEMENTATION + + /** + * Processes an incoming request in inStream served through the given port and + * writes the response to outStream. + * + * @param port + * the port on which the request was served + * @param inStream + * an nsIInputStream containing the incoming request + * @param outStream + * the nsIOutputStream to which the response should be written + */ + _processConnection: function(port, inStream, outStream) + { + try + { + var metadata = new RequestMetadata(port); + metadata.init(inStream); + + inStream.close(); + + this._handler.handleRequest(outStream, metadata); + } + catch (e) + { + dumpn(">>> internal error, shutting down server: " + e); + dumpn("*** stack trace: " + e.stack); + + inStream.close(); // in case we failed before then + + this._doQuit = true; + this._endConnection(this._handler, outStream); + } + + // stream cleanup, etc. happens in _endConnection below -- called from + // stream copier observer when all data has been copied + }, + + /** + * Closes the passed-in output stream and (if necessary) shuts down this + * server. + * + * @param handler + * the request handler which handled the request + * @param outStream + * the nsIOutputStream for the processed request which must be closed + */ + _endConnection: function(handler, outStream) + { + // + // Order is important below: we must decrement handler._pendingRequests + // BEFORE calling this.stop(), if needed, because this.stop() returns only + // when the server socket's closed AND all pending requests are complete, + // which clearly isn't (and never will be) the case if it were the other way + // around + // + + outStream.close(); // inputstream already closed after processing + handler._pendingRequests--; + + // handle possible server quit -- note that this doesn't affect extant open + // connections and pending requests: nsIServerSocket.close is specified not + // to affect open connections, and nsIHttpServer.stop attempts to do its + // best to return only when the server socket is closed and all pending + // requests have been served + if (this._doQuit) + this.stop(); + }, + + /** + * Requests that the server be shut down when possible. + */ + _requestQuit: function() + { + dumpn(">>> requesting a quit"); + this._doQuit = true; + } + +}; + + +/** + * Gets a content-type for the given file, as best as it is possible to do so. + * + * @param file + * the nsIFile for which to get a file type + * @returns + * the best content-type which can be determined for the file + */ +function getTypeFromFile(file) +{ + try + { + var type = Cc["@mozilla.org/uriloader/external-helper-app-service;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromFile(file); + } + catch (e) + { + type = "application/octet-stream"; + } + return type; +} + + + +/** + * Creates a request-handling function for an nsIHttpRequestHandler object. + */ +function createHandlerFunc(handler) +{ + return function(metadata, response) { handler.handle(metadata, response); }; +} + + +/** + * 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 srv + * the nsHttpServer in which this handler is being used + */ +function ServerHandler(srv) +{ + // FIELDS + + /** + * The nsHttpServer instance associated with this handler. + */ + this._server = srv; + + /** + * A variable used to ensure that all requests are fully complete before the + * server shuts down, to avoid callbacks from compiled code calling back into + * empty contexts. See also the comment before this field is next modified. + */ + this._pendingRequests = 0; + + /** + * A FileMap object containing the set of path->nsILocalFile 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 also ServerHandler.prototype._defaultPaths + */ + this._overridePaths = {}; + + /** + * 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 also ServerHandler.prototype._defaultErrors + */ + this._overrideErrors = {}; +} +ServerHandler.prototype = +{ + // PUBLIC API + + /** + * Handles a request to this server, responding to the request appropriately + * and initiating server shutdown if necessary. If the request metadata + * specifies an error code, the appropriate error response will be sent. + * + * @param outStream + * an nsIOutputStream to which a response should be written + * @param metadata + * request metadata as generated from the initial request + */ + handleRequest: function(outStream, metadata) + { + var response = new Response(); + + // handle any existing error codes + if (metadata.errorCode) + { + dumpn("*** errorCode == " + metadata.errorCode); + this._handleError(metadata, response); + return this._end(response, outStream); + } + + var path = metadata.path; + dumpn("*** path == " + path); + + try + { + try + { + // explicit paths first, then files based on existing directory mappings, + // then (if the file doesn't exist) built-in server default paths + if (path in this._overridePaths) + this._overridePaths[path](metadata, response); + else + this._handleDefault(metadata, response); + } + catch (e) + { + response.recycle(); + + if (!(e instanceof HttpError) || e.code != 404) + throw HTTP_500; + + if (path in this._defaultPaths) + this._defaultPaths[path](metadata, response); + else + throw HTTP_404; + } + } + catch (e2) + { + if (!(e2 instanceof HttpError)) + { + dumpn("*** internal error: e2 == " + e2); + throw e2; + } + + var errorCode = e2.code; + dumpn("*** errorCode == " + errorCode); + + response.recycle(); + + metadata.errorCode = errorCode; + this._handleError(metadata, response); + } + + return this._end(response, outStream); + }, + + /** + * Associates a file with a server path so that it is returned by future + * requests to that path. + * + * @param path + * the path on the server, which must begin with a "/" + * @param file + * the nsILocalFile representing the file to be served; must not be a + * directory + */ + registerFile: function(path, file) + { + dumpn("*** registering '" + path + "' as mapping to " + file.path); + file = file.clone(); + + this._overridePaths[path] = + function(metadata, response) + { + if (!file.exists()) + throw HTTP_404; + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + + try + { + this._writeFileResponse(file, response); + } + catch (e) + { + // something happened -- but the calling code should handle the error + // and clean up the response + throw e; + } + }; + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler: function(path, handler) + { + // XXX true path validation! + if (path.charAt(0) != "/") + throw Cr.NS_ERROR_INVALID_ARG; + + // for convenience, handler can be a function if this is run from xpcshell + if (typeof(handler) == "function") + this._overridePaths[path] = handler; + else if (handler) + this._overridePaths[path] = createHandlerFunc(handler); + else + delete this._overridePaths[path]; + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory: function(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); + + 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: function(err, handler) + { + if (!(err in HTTP_ERROR_CODES)) + dumpn("*** WARNING: registering non-HTTP/1.1 error code " + + "(" + err + ") handler -- was this intentional?"); + + // for convenience, handler can be a function if this is run from xpcshell + if (typeof(handler) == "function") + this._overrideErrors[err] = handler; + else if (handler) + this._overrideErrors[err] = createHandlerFunc(handler); + else + delete this._overrideErrors[err]; + }, + + /** + * Returns true if this handler is in the middle of handling any current + * requests; this must be false before the server in which this is used may be + * safely shut down. + */ + hasPendingRequests: function() + { + return this._pendingRequests > 0; + }, + + // PRIVATE API + + /** + * 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-related data as a RequestMetadata object + * @param response + * an uninitialized Response to the given request which must 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 cleanup of the response by calling .destroy() + * or .recycle() + */ + _handleDefault: function(metadata, response) + { + try + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + + var path = metadata.path; + NS_ASSERT(path.charAt(0) == "/"); + + // 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 -- deal + if (file.exists() && file.isDirectory()) + { + file.append("index.html"); // make configurable? + if (!file.exists() || + file.isDirectory()) + throw HTTP_501; // need directory listings ftw! + } + + // alternately, the file might not exist + if (!file.exists()) + throw HTTP_404; + + // finally... + dumpn("*** handling '" + path + "' as mapping to " + file.path); + this._writeFileResponse(file, response); + } + catch (e) + { + // something failed, but make the calling code handle Response cleanup + throw e; + } + }, + + /** + * Writes an HTTP response for the given file, including setting headers for + * file metadata. + * + * @param file + * the file which is to be sent in the response + * @param response + * the response to which the file should be written + */ + _writeFileResponse: function(file, response) + { + try + { + response.setHeader("Last-Modified", + toDateString(file.lastModifiedTime), + false); + } + catch (e) { /* lastModifiedTime threw, ignore */ } + + response.setHeader("Content-Type", getTypeFromFile(file), false); + + var fis = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + const PR_RDONLY = 0x01; + fis.init(file, PR_RDONLY, 0444, Ci.nsIFileInputStream.CLOSE_ON_EOF); + response.bodyOutputStream.writeFrom(fis, file.fileSize); + fis.close(); + }, + + /** + * Returns the nsILocalFile which corresponds to the path, as determined using + * all registered path->directory mappings and any paths which are explicitly + * overridden. + * + * @returns + * the nsILocalFile which is to be sent as the response to a request for + * path + * @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.) + */ + _getFileForPath: function(path) + { + var pathMap = this._pathDirectoryMap; + + // first, decode path and strip the leading '/' + try + { + path = decodeURI(path); + } + catch (e) + { + throw HTTP_400; // malformed path + } + + // next, get the directory which contains this path + + // 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 != 0 && + 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 + // "/foo/../bar" but prevents paths such as "/../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; + }, + + /** + * 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 metadata + * metadata for the request, which will often be incomplete since this is an + * error -- must have its .errorCode set for the desired error + * @param 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: function(metadata, response) + { + if (!metadata) + throw Cr.NS_ERROR_NULL_POINTER; + + var errorCode = metadata.errorCode; + 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 if (errorCode in this._defaultErrors) + this._defaultErrors[errorCode](metadata, response); + } + catch (e) + { + // 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 + "..."); + 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) + { + response.recycle(); + + // 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 + { + 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); + throw e2; + } + } + }, + + /** + * Called when all processing necessary for the current request has completed + * and response headers and data have been determined. This method takes + * those headers and data, sends them to the HTTP client, and halts further + * processing. It will also send a quit message to the server if necessary. + * + * @param response + * a Response object representing the desired response + * @param outStream + * a stream to which the response should be written + * @note + * after completion, response must be considered "dead", and none of its + * methods or properties may be accessed + */ + _end: function(response, outStream) + { + try + { + // post-processing + response.setHeader("Connection", "close", false); + response.setHeader("Server", "MozJSHTTP", false); + response.setHeader("Date", toDateString(Date.now()), false); + + var bodyStream = response.bodyInputStream + .QueryInterface(Ci.nsIInputStream); + + // XXX suckage time! + // + // If the body of the response has had no data written to it (or has never + // been accessed -- same deal internally since we'll create one if we have + // to access bodyInputStream but have neither an input stream nor an + // output stream), the in-tree implementation of nsIPipe is such that + // when we try to close the pipe's output stream with no data in it, this + // is interpreted as an error and closing the output stream also closes + // the input stream. .available then throws, so we catch and deal as best + // as we can. + // + // Unfortunately, the easy alternative (substitute a storage stream for a + // pipe) also doesn't work. There's no problem writing zero bytes to the + // output end of the stream, but then attempting to get an input stream to + // read fails because the seek position must be strictly less than the + // buffer size. + // + // Much as I'd like the zero-byte body to be a mostly-unimportant problem, + // there are some HTTP responses such as 304 Not Modified which MUST have + // zero-byte bodies, so this *is* a necessary hack. + try + { + var available = bodyStream.available(); + } + catch (e) + { + available = 0; + } + + response.setHeader("Content-Length", available.toString(), false); + + + // construct and send response + + // request-line + var preamble = "HTTP/" + response.httpVersion + " " + + response.httpCode + " " + + response.httpDescription + "\r\n"; + + // headers + var head = response.headers; + var headEnum = head.enumerator; + while (headEnum.hasMoreElements()) + { + var fieldName = headEnum.getNext() + .QueryInterface(Ci.nsISupportsString) + .data; + preamble += fieldName + ": " + head.getHeader(fieldName) + "\r\n"; + } + + // end request-line/headers + preamble += "\r\n"; + outStream.write(preamble, preamble.length); + + + // In certain situations, it's possible for us to have a race between + // the copy observer's onStopRequest and the listener for a channel + // opened to this server. Since we include a Content-Length header with + // every response, if the channel snarfs up all the data we promise, + // calls onStopRequest on the listener (and the server is shut down + // by that listener, causing the script to finish executing), and then + // tries to call onStopRequest on the copyObserver, we'll call into a + // scope with no Components and cause assertions *and* fail to close the + // connection properly. To combat this, during server shutdown we delay + // full shutdown until any pending requests are fully copied using this + // property on the server handler. We increment before (possibly) + // starting the copy observer and decrement when the copy completes, + // ensuring that all copies complete before the server fully shuts down. + // + // We do this for every request primarily to simplify maintenance of this + // property (and also because it's less fragile when we can remove the + // zero-sized body hack used above). + this._pendingRequests++; + + // If we have a body, send it -- if we don't, then don't bother with a + // heavyweight async copy which doesn't need to happen and just do + // response post-processing (usually handled by the copy observer) + // directly + if (available != 0) + { + // local references for use in the copy observer + var server = this._server; + var handler = this; + + /** + * Observer of the copying of data from the body stream generated by a + * request handler to the output stream for the server socket. It + * handles all post-request-writing cleanup details, such as closing + * open streams and shutting down the server in case of errors. + */ + var copyObserver = + { + onStartRequest: function(request, context) { /* don't care */ }, + + /** + * Called when the async stream copy completes. This is place where + * final cleanup should occur, including stream closures and + * response destruction. Note that errors which are detected here + * should still shut down the server, for safety. + */ + onStopRequest: function(request, context, statusCode) + { + // if something bad happened during the copy, be paranoid + if (!Components.isSuccessCode(statusCode)) + server._requestQuit(); + + // we're completely finished with this response + response.destroy(); + + server._endConnection(handler, outStream); + }, + + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIRequestObserver) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + + // body -- written async, because pipes deadlock if we do + // |outStream.writeFrom(bodyStream, bodyStream.available());| + var copier = Cc["@mozilla.org/network/async-stream-copier;1"] + .createInstance(Ci.nsIAsyncStreamCopier); + copier.init(bodyStream, outStream, null, true, true, 8192); + copier.asyncCopy(copyObserver, null); + } + else + { + // finished with the response -- destroy + response.destroy(); + this._server._endConnection(this, outStream); + } + } + catch (e) + { + // something bad happened -- throw and make calling code deal with the + // response given to us + throw e; + } + }, + + // FIELDS + + /** + * This object contains the default handlers for the various HTTP error codes. + */ + _defaultErrors: + { + 400: function(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", false); + + var body = "Bad request\n"; + response.bodyOutputStream.write(body, body.length); + }, + 403: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 403, "Forbidden"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ + 403 Forbidden\ + \ +

403 Forbidden

\ + \ + "; + response.bodyOutputStream.write(body, body.length); + }, + 404: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ + 404 Not Found\ + \ +

404 Not Found

\ +

\ + " + + htmlEscape(metadata.path) + + " was not found.\ +

\ + \ + "; + response.bodyOutputStream.write(body, body.length); + }, + 500: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, + 500, + "Internal Server Error"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ + 500 Internal Server Error\ + \ +

500 Internal Server Error

\ +

Something's broken in this server and\ + needs to be fixed.

\ + \ + "; + response.bodyOutputStream.write(body, body.length); + }, + 501: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 501, "Not Implemented"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ + 501 Not Implemented\ + \ +

501 Not Implemented

\ +

This server is not (yet) Apache.

\ + \ + "; + response.bodyOutputStream.write(body, body.length); + }, + 505: function(metadata, response) + { + response.setStatusLine("1.1", 505, "HTTP Version Not Supported"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ + 505 HTTP Version Not Supported\ + \ +

505 HTTP Version Not Supported

\ +

This server only supports HTTP/1.0 and HTTP/1.1\ + connections.

\ + \ + "; + 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", false); + + var body = "\ + MozJSHTTP\ + \ +

MozJSHTTP

\ +

If you're seeing this page, MozJSHTTP is up and\ + serving requests! Now set a base path and serve some\ + files!

\ + \ + "; + + response.bodyOutputStream.write(body, body.length); + }, + + "/trace": function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + + var body = "Request (semantically equivalent, slightly reformatted):\n\n"; + body += metadata.method + " " + metadata.path; + + 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); + } + } +}; + + +/** + * 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 nsILocalFile value if value is non-null; + * otherwise, removes any extant mapping for key. + */ + put: function(key, value) + { + if (value) + this._map[key] = value.clone(); + else + delete this._map[key]; + }, + + /** + * Returns a clone of the nsILocalFile mapped to key, or null if no such + * mapping exists. + */ + get: function(key) + { + var val = this._map[key]; + return val ? val.clone() : null; + } +}; + + +// Response CONSTANTS + +// token = * +// CHAR = +// CTL = +// 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 + * the character code + * @returns + * 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. + */ +function Response() +{ + // delegate initialization behavior to .recycle(), for code-sharing; + // see there for field descriptions as well + this.recycle(); +} +Response.prototype = +{ + // PUBLIC CONSTRUCTION API + + // + // see nsIHttpResponse.bodyOutputStream + // + get bodyOutputStream() + { + this._ensureAlive(); + + if (!this._bodyOutputStream && !this._outputProcessed) + { + var pipe = Cc["@mozilla.org/pipe;1"] + .createInstance(Ci.nsIPipe); + const PR_UINT32_MAX = Math.pow(2, 32) - 1; + pipe.init(false, false, 0, PR_UINT32_MAX, null); + this._bodyOutputStream = pipe.outputStream; + this._bodyInputStream = pipe.inputStream; + } + + return this._bodyOutputStream; + }, + + // + // see nsIHttpResponse.setStatusLine + // + setStatusLine: function(httpVersion, code, description) + { + this._ensureAlive(); + + if (!(code >= 0 && code < 1000)) + throw 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 Cr.NS_ERROR_INVALID_ARG; + } + + // Reason-Phrase = * + // TEXT = + // + // 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 Cr.NS_ERROR_INVALID_ARG; + + // set the values only after validation to preserve atomicity + this._httpDescription = description; + this._httpCode = code; + this._httpVersion = httpVer; + }, + + // + // see nsIHttpResponse.setHeader + // + setHeader: function(name, value, merge) + { + this._ensureAlive(); + + this._headers.setHeader(name, value, merge); + }, + + // 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: function(name) + { + this._ensureAlive(); + + return this._headers.getHeader(name); + }, + + /** + * A stream containing the data stored in the body of this response, which is + * the data written to this.bodyOutputStream. Accessing this property will + * prevent further writes to bodyOutputStream and will remove that property + * from this, so the only time this should be accessed should be after this + * Response has been fully processed by a request handler. + */ + get bodyInputStream() + { + this._ensureAlive(); + + if (!this._outputProcessed) + { + // if nothing was ever written to bodyOutputStream, we never created a + // pipe -- do so now by writing the empty string to this.bodyOutputStream + if (!this._bodyOutputStream) + this.bodyOutputStream.write("", 0); + + this._outputProcessed = true; + } + if (this._bodyOutputStream) + { + this._bodyOutputStream.close(); // flushes buffered data + this._bodyOutputStream = null; // don't try closing again + } + return this._bodyInputStream; + }, + + /** + * Resets this response to its original state, destroying any currently-held + * resources in the process. Use this method to invalidate an existing + * response and reuse it later, such as when an arbitrary handler has + * failed and may have altered the visible state of this (such as by + * setting headers). + * + * This method may be called on Responses which have been .destroy()ed. + */ + recycle: function() + { + if (this._bodyOutputStream) + { + this._bodyOutputStream.close(); + this._bodyOutputStream = null; + } + if (this._bodyInputStream) + { + this._bodyInputStream.close(); + this._bodyInputStream = null; + } + + /** + * 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._headers = new nsHttpHeaders(); + + /** + * Set to true when this has its .destroy() method called; further actions on + * this will then fail. + */ + this._destroyed = false; + + /** + * Flipped when this.bodyOutputStream is closed; prevents the body from being + * reopened after it has data written to it and has been closed. + */ + this._outputProcessed = false; + }, + + /** + * Destroys all resources held by this. After this method is called, no other + * method or property on this must be accessed (except .recycle, which may be + * used to reuse this Response). Although in many situations resources may be + * automagically cleaned up, it is highly recommended that this method be + * called whenever a Response is no longer used, both as a precaution and + * because this implementation may not always do so. + * + * This method is idempotent. + */ + destroy: function() + { + if (this._destroyed) + return; + + if (this._bodyOutputStream) + { + this._bodyOutputStream.close(); + this._bodyOutputStream = null; + } + if (this._bodyInputStream) + { + this._bodyInputStream.close(); + this._bodyInputStream = null; + } + + this._destroyed = true; + }, + + // PRIVATE IMPLEMENTATION + + /** Ensures that this hasn't had its .destroy() method called. */ + _ensureAlive: function() + { + if (this._destroyed) + throw Cr.NS_ERROR_FAILURE; + } +}; + + +/** + * 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 + * fieldName converted to lowercase if it is a valid header, for characters + * where case conversion is possible + */ + normalizeFieldName: function(fieldName) + { + 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 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 + * 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 + * fieldValue as a normalized HTTP header field value + */ + normalizeFieldValue: function(fieldValue) + { + // field-value = *( field-content | LWS ) + // field-content = + // TEXT = + // LWS = [CRLF] 1*( SP | HT ) + // + // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + // qdtext = > + // quoted-pair = "\" CHAR + // CHAR = + + // 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 + for (var i = 0, len = val.length; i < len; i++) + if (isCTL(val.charCodeAt(i))) + throw 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 + * the string to make HTML-safe + * @returns + * 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 "Not a valid HTTP version!"; + + this.major = parseInt(matches[1], 10); + this.minor = parseInt(matches[2], 10); + if (isNaN(this.major) || isNaN(this.minor) || + this.major < 0 || this.minor < 0) + throw "Not a valid HTTP version!"; +} +nsHttpVersion.prototype = +{ + /** + * The major version number of this, as a number + */ + major: undefined, + + /** + * The minor version number of this, as a number. + */ + minor: undefined, + + /** + * Returns the standard string representation of the HTTP version represented + * by this (e.g., "1.1"). + */ + toString: function () + { + return this.major + "." + this.minor; + }, + + /** + * Returns true if this represents the same HTTP version as otherVersion, + * false otherwise. + */ + equals: function (otherVersion) + { + return 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. + */ +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 + * the header name + * @param value + * the header value + * @throws NS_ERROR_INVALID_ARG + * if name or value is not a valid header component + */ + setHeader: function(fieldName, fieldValue, merge) + { + var name = headerUtils.normalizeFieldName(fieldName); + var value = headerUtils.normalizeFieldValue(fieldValue); + + if (merge && name in this._headers) + this._headers[name] = this._headers[name] + "," + value; + else + this._headers[name] = value; + }, + + /** + * Returns the value for the header specified by this. + * + * @returns + * 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 + * @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 + */ + getHeader: function(fieldName) + { + var name = headerUtils.normalizeFieldName(fieldName); + + if (name in this._headers) + return this._headers[name]; + else + throw Cr.NS_ERROR_NOT_AVAILABLE; + }, + + /** + * 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 + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not constitute a valid header field name + */ + hasHeader: function(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 = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + supports.data = i; + headers.push(supports); + } + + return new nsSimpleEnumerator(headers); + } +}; + + +/** + * Constructs an nsISimpleEnumerator for the given array of items. + * + * @param items + * the array of items, which must all implement nsISupports + */ +function nsSimpleEnumerator(items) +{ + this._items = items; + this._nextIndex = 0; +} +nsSimpleEnumerator.prototype = +{ + hasMoreElements: function() + { + return this._nextIndex < this._items.length; + }, + getNext: function() + { + if (!this.hasMoreElements()) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + return this._items[this._nextIndex++]; + }, + QueryInterface: function(aIID) + { + if (Ci.nsISimpleEnumerator.equals(aIID) || + Ci.nsISupports.equals(aIID)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + + +/** + * Parses a server request into a set of metadata, so far as is possible. Any + * detected errors will result in this.errorCode being set to an HTTP error code + * value. Users MUST check this value after creation and any external + * initialization of RequestMetadata objects to ensure that errors are handled + * correctly. + * + * @param port + * the port on which the server receiving this request runs + */ +function RequestMetadata(port) +{ + this._method = ""; + this._path = ""; + this._queryString = ""; + this._host = ""; + this._port = port; + + /** + * The headers in this request. + */ + this._headers = new nsHttpHeaders(); + + /** + * The numeric HTTP error, if any, associated with this request. This value + * may be set by the constructor but is usually only set by the handler after + * this has been constructed. After this has been initialized, this value + * MUST be checked for errors. + */ + this.errorCode = 0; +} +RequestMetadata.prototype = +{ + // SERVER METADATA + + // + // see nsIHttpRequestMetadata.host + // + get host() + { + return this._host; + }, + + // + // see nsIHttpRequestMetadata.port + // + get port() + { + return this._port; + }, + + // REQUEST LINE + + // + // see nsIHttpRequestMetadata.method + // + get method() + { + return this._method; + }, + + // + // see nsIHttpRequestMetadata.httpVersion + // + get httpVersion() + { + return this._httpVersion.toString(); + }, + + // + // see nsIHttpRequestMetadata.path + // + get path() + { + return this._path; + }, + + // + // see nsIHttpRequestMetadata.queryString + // + get queryString() + { + return this._queryString; + }, + + // HEADERS + + // + // see nsIHttpRequestMetadata.getHeader + // + getHeader: function(name) + { + return this._headers.getHeader(name); + }, + + // + // see nsIHttpRequestMetadata.hasHeader + // + hasHeader: function(name) + { + return this._headers.hasHeader(name); + }, + + // + // see nsIHttpRequestMetadata.headers + // + get headers() + { + return this._headers.enumerator; + }, + + // ENTITY + + /** + * An input stream which contains the body of this request. + */ + get bodyStream() + { + // we want this once we do real request processing -- expose externally when + // we do this + return null; + }, + + // PUBLIC CONSTRUCTION API + + /** + * The HTTP error code which should be the result of this request. It must be + * checked whenever other API documentation says it should be checked. + */ + errorCode: 0, + + // INITIALIZATION + init: function(input) + { + // XXX this is incredibly un-RFC2616 in every possible way: + // + // - accepts non-CRLF line endings + // - no real testing for non-US-ASCII text and throwing in that case + // - handles POSTs by displaying the URL and throwing away the request + // entity + // - need to support RFC 2047-encoded non-US-ASCII characters + // - support absolute URLs (requires telling the server its hostname, beyond + // just localhost:port and 127.0.0.1:port) + // - etc. + + // read the input line by line; the first line either tells us the requested + // path or is empty, in which case the second line contains the path + var is = Cc["@mozilla.org/intl/converter-input-stream;1"] + .createInstance(Ci.nsIConverterInputStream); + is.init(input, "ISO-8859-1", 1024, 0xFFFD); + + var lis = is.QueryInterface(Ci.nsIUnicharLineInputStream); + + + this._parseRequestLine(lis); + if (this.errorCode) + return; + + this._parseHeaders(lis); + if (this.errorCode) + return; + + // XXX need to put body transmitted with this request into an input stream! + + // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header + if (!this._headers.hasHeader("Host") && + this._httpVersion.equals(nsHttpVersion.HTTP_1_1)) + { + this.errorCode = 400; + return; + } + + // XXX set this based on Host or the request URI? + this._host = "localhost"; + }, + + // PRIVATE API + + /** + * Parses the request line for the HTTP request in the given input stream. On + * completion this.errorCode must be checked to determine whether any errors + * occurred during header parsing. + * + * @param lis + * an nsIUnicharLineInputStream from which to parse HTTP headers + */ + _parseRequestLine: function(lis) + { + // servers SHOULD ignore any empty line(s) received where a Request-Line + // is expected (section 4.1) + var line = {}; + while (lis.readLine(line) && line.value == "") + dumpn("*** ignoring beginning blank line..."); + + // 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.value.split(/[ \t]+/); + if (!request || request.length != 3) + { + this.errorCode = 400; + return; + } + + this._method = request[0]; + + // check the HTTP version + var ver = request[2]; + var match = ver.match(/^HTTP\/(\d+\.\d+)$/); + if (!match) + { + this.errorCode = 400; + return; + } + + // reject unrecognized methods + if (request[0] != "GET" && request[0] != "POST") + { + this.errorCode = 501; + return; + } + + // determine HTTP version + try + { + this._httpVersion = new nsHttpVersion(match[1]); + if (!this._httpVersion.equals(nsHttpVersion.HTTP_1_0) && + !this._httpVersion.equals(nsHttpVersion.HTTP_1_1)) + throw "unsupported HTTP version"; + } + catch (e) + { + // we support HTTP/1.0 and HTTP/1.1 only + this.errorCode = 501; + return; + } + + var fullPath = request[1]; + + // XXX we don't support absolute URIs yet -- a MUST for HTTP/1.1 + if (fullPath.charAt(0) != "/") + { + this.errorCode = 400; + return; + } + + var splitter = fullPath.indexOf("?"); + if (splitter < 0) + { + // _queryString already set in ctor + this._path = fullPath; + } + else + { + this._path = fullPath.substring(0, splitter); + this._queryString = fullPath.substring(splitter + 1); + } + }, + + /** + * Parses all available HTTP headers from the given input stream. On + * completion, this.errorCode must be checked to determine whether any errors + * occurred during header parsing. + * + * @param lis + * the nsIUnicharLineInputStream from which to parse HTTP headers + */ + _parseHeaders: function(lis) + { + var headers = this._headers; + var lastName, lastVal; + + var line = {}; + while (true) + { + NS_ASSERT((lastVal === undefined && lastName === undefined) || + (lastVal !== undefined && lastName !== undefined), + lastName === undefined ? + "lastVal without lastName? lastVal: '" + lastVal + "'" : + "lastName without lastVal? lastName: '" + lastName + "'"); + + lis.readLine(line); + var lineText = line.value; + 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("*** e == " + e); + this.errorCode = 400; + return; + } + } + else + { + // no headers in request -- valid for HTTP/1.0 requests + } + + // either way, we're done processing headers + break; + } + else if (firstChar == " " || firstChar == "\t") + { + // multi-line header if we've seen a header line + if (!lastName) + { + // we don't have a header to continue! + this.errorCode = 400; + return; + } + + // 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("*** e == " + e); + this.errorCode = 400; + return; + } + } + + var colon = lineText.indexOf(":"); // first colon must be splitter + if (colon < 1) + { + // no colon or missing header field-name + this.errorCode = 400; + return; + } + + // 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) + } +}; + + +// XPCOM trappings + +/** + * Creates a factory for instances of an object created using the passed-in + * constructor. + */ +function makeFactory(ctor) +{ + function ci(outer, iid) + { + if (outer != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return (new ctor()).QueryInterface(iid); + } + + return { + createInstance: ci, + lockFactory: function(lock) { }, + QueryInterface: function(aIID) + { + if (Ci.nsIFactory.equals(aIID) || + Ci.nsISupports.equals(aIID)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; +} + +/** The XPCOM module containing the HTTP server. */ +const module = +{ + // nsISupports + QueryInterface: function(aIID) + { + if (Ci.nsIModule.equals(aIID) || + Ci.nsISupports.equals(aIID)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + // nsIModule + registerSelf: function(compMgr, fileSpec, location, type) + { + compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar); + + for (var key in this._objects) + { + var obj = this._objects[key]; + compMgr.registerFactoryLocation(obj.CID, obj.className, obj.contractID, + fileSpec, location, type); + } + }, + unregisterSelf: function (compMgr, location, type) + { + compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar); + + for (var key in this._objects) + { + var obj = this._objects[key]; + compMgr.unregisterFactoryLocation(obj.CID, location); + } + }, + getClassObject: function(compMgr, cid, iid) + { + if (!iid.equals(Ci.nsIFactory)) + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + + for (var key in this._objects) + { + if (cid.equals(this._objects[key].CID)) + return this._objects[key].factory; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + canUnload: function(compMgr) + { + return true; + }, + + // private implementation + _objects: + { + server: + { + CID: Components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"), + contractID: "@mozilla.org/server/jshttp;1", + className: "MozJSHTTP server", + factory: makeFactory(nsHttpServer) + } + } +}; + + +/** + * NSGetModule, so this code can be used as a JS component. Whether multiple + * servers can run at once is currently questionable, but the code certainly + * isn't very far from supporting it. + */ +function NSGetModule(compMgr, fileSpec) +{ + return module; +} + + +/** + * Creates a new HTTP server listening for loopback traffic on the given port, + * starts it, and runs the server until the server processes a shutdown request, + * spinning an event loop so that events posted by the server's socket are + * processed. + * + * This method is primarily intended for use in running this script from within + * xpcshell and running a functional HTTP server without having to deal with + * non-essential details. + * + * Note that running multiple servers using variants of this method probably + * doesn't work, simply due to how the internal event loop is spun and stopped. + * + * @note + * This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code); + * you should use this server as a component in Mozilla 1.8. + * @param port + * the port on which the server will run, or -1 if there exists no preference + * for a specific port; note that attempting to use some values for this + * parameter (particularly those below 1024) may cause this method to throw or + * may result in the server being prematurely shut down + * @param basePath + * a local directory from which requests will be served (i.e., if this is + * "/home/jwalden/" then a request to /index.html will load + * /home/jwalden/index.html); if this is omitted, only the default URLs in + * this server implementation will be functional + */ +function server(port, basePath) +{ + if (basePath) + { + var lp = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + lp.initWithPath(basePath); + } + + var srv = new nsHttpServer(); + if (lp) + srv.registerDirectory("/", lp); + srv.start(port); + + var thread = Cc["@mozilla.org/thread-manager;1"] + .getService() + .currentThread; + while (!srv.isStopped()) + thread.processNextEvent(true); + + // get rid of any pending requests + while (thread.hasPendingEvents()) + thread.processNextEvent(true); +} diff --git a/mozilla/netwerk/test/httpserver/nsIHttpServer.idl b/mozilla/netwerk/test/httpserver/nsIHttpServer.idl new file mode 100644 index 00000000000..043a0d2e1fb --- /dev/null +++ b/mozilla/netwerk/test/httpserver/nsIHttpServer.idl @@ -0,0 +1,320 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the MozJSHTTP server. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jeff Walden (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "nsIServerSocket.idl" + +interface nsILocalFile; +interface nsISimpleEnumerator; +interface nsIOutputStream; + +interface nsIHttpServer; +interface nsIHttpRequestHandler; +interface nsIHttpRequestMetadata; +interface nsIHttpResponse; + +/** + * An interface which represents an HTTP server. + */ +[scriptable, uuid(5520f79e-ecd5-4c40-843b-97ee13a23747)] +interface nsIHttpServer : nsIServerSocketListener +{ + /** + * Starts up this server, listening upon the given port. This method may + * throw if the process does not have sufficient privileges to open a socket + * for the given port, and it also throws when called upon a server which has + * already been started. + * + * @param port + * the port upon which listening should happen, or -1 if no specific port is + * desired + */ + void start(in long port); + + /** + * Shuts down this server if it is running; if it is not, this method is a + * no-op. + * + * This method will do its best to return after the socket in this + * server has been closed and all pending requests have completed being + * served, but this may or may not actually happen, since in some + * implementations this may not actually be possible. Implementations which + * can make this promise should make it explicit in implementation + * documentation. + */ + void stop(); + + /** + * 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; this file must exist for the + * lifetime of the server + */ + void registerFile(in string path, in nsILocalFile file); + + /** + * 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 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 400 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 nsILocalFile dir); +}; + + +/** + * 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 the HTTP request represented by metadata and initializes the + * passed-in response to reflect the correct HTTP response. + * + * Note that in some uses of nsIHttpRequestHandler, this method is required to + * not throw an exception; in the general case, however, this method may throw + * an exception (causing an HTTP 500 response to occur). + * + * @param metadata + * 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 nsIHttpRequestMetadata metadata, in nsIHttpResponse response); +}; + + +/** + * A representation of the data included in an HTTP request. + */ +[scriptable, uuid(acbb5ea6-58b3-4750-bcc7-618910ebca9d)] +interface nsIHttpRequestMetadata : nsISupports +{ + /** + * The request type for this request (see RFC 2616, section 5.1.1). + */ + readonly attribute string method; + + /** + * 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. + */ + 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 "/". This string is in the + */ + readonly attribute string path; + + /** + * The URL-encoded query string associated with this request, not including + * the initial "?". + */ + 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 field value for the given header; note that this value may be + * normalized (e.g., leading/trailing whitespace removed from the value [or + * from the values which make this up, if the header is a comma-separated + * list of values], whitespace runs compressed to single spaces, etc.) + * @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 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; + + // XXX should expose body of request here! +}; + + +/** + * Represents an HTTP response, as described in RFC 2616, section 6. + */ +[scriptable, uuid(017f962b-137e-4911-887c-61808e89fa4f)] +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 + */ + 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; when false, if the given header already exists in this, + * it is overwritten with the passed-in values; if the header doesn't exist + * in this, it is set regardless of the value of this parameter + * @throws NS_ERROR_INVALID_ARG + * if name or value is not a valid header component + */ + void setHeader(in string name, in string value, in boolean merge); + + /** + * A stream to which data appearing in the body of this response should be + * written. + */ + readonly attribute nsIOutputStream bodyOutputStream; +}; diff --git a/mozilla/netwerk/test/httpserver/test/head_utils.js b/mozilla/netwerk/test/httpserver/test/head_utils.js new file mode 100644 index 00000000000..74dc332409b --- /dev/null +++ b/mozilla/netwerk/test/httpserver/test/head_utils.js @@ -0,0 +1,81 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is MozJSHTTP code. + * + * The Initial Developer of the Original Code is + * Jeff Walden . + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +do_import_script("test-harness/xpcshell-simple/httpd.js"); + +/** + * 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) with + * only slight modifications. + */ +function createServer() +{ + return new nsHttpServer(); +} + +/** + * Creates a new HTTP channel. + * + * @param url + * the URL of the channel to create + */ +function makeChannel(url) +{ + var ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + var chan = ios.newChannel(url, null, null) + .QueryInterface(Ci.nsIHttpChannel); + + return chan; +} + +/** + * Make a binary input stream wrapper for the given stream. + * + * @param stream + * the nsIInputStream to wrap + */ +function makeBIS(stream) +{ + var bis = Cc["@mozilla.org/binaryinputstream;1"] + .createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(stream); + return bis; +} diff --git a/mozilla/netwerk/test/httpserver/test/test_basic_functionality.js b/mozilla/netwerk/test/httpserver/test/test_basic_functionality.js new file mode 100644 index 00000000000..e6b95d738f7 --- /dev/null +++ b/mozilla/netwerk/test/httpserver/test/test_basic_functionality.js @@ -0,0 +1,201 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is MozJSHTTP code. + * + * The Initial Developer of the Original Code is + * Jeff Walden . + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// basic functionality test, from the client programmer's POV + + +var paths = + [ + "http://localhost:4444/objHandler", + "http://localhost:4444/functionHandler", + "http://localhost:4444/non-existent-path" // intended to produce 404 + ]; +var currPathIndex = 0; + +var listener = + { + // NSISTREAMLISTENER + onDataAvailable: function(request, cx, inputStream, offset, count) + { + makeBIS(inputStream).readByteArray(count); // required by API + }, + // NSIREQUESTOBSERVER + onStartRequest: function(request, cx) + { + var ch = request.QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + // common properties *always* appended by server or invariants for every + // URL in paths + do_check_true(ch.contentLength > -1); + do_check_eq(ch.getResponseHeader("connection"), "close"); + do_check_false(ch.isNoStoreResponse()); + + var reqMin = {}, reqMaj = {}, respMin = {}, respMaj = {}; + switch (currPathIndex) + { + case 0: + do_check_eq(ch.responseStatus, 200); + do_check_true(ch.requestSucceeded); + do_check_eq(ch.getResponseHeader("content-type"), "text/plain"); + do_check_eq(ch.responseStatusText, "OK"); + + ch.getRequestVersion(reqMaj, reqMin); + ch.getResponseVersion(respMaj, respMin); + do_check_true(reqMaj.value == respMaj.value && + reqMin.value == respMin.value); + break; + + case 1: + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); + do_check_eq(ch.getResponseHeader("foopy"), "quux-baz"); + do_check_eq(ch.responseStatusText, "Page Not Found"); + + ch.getResponseVersion(respMaj, respMin); + do_check_true(respMaj.value == 1 && respMin.value == 1); + break; + + case 2: + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); + break; + } + }, + onStopRequest: function(request, cx, status) + { + if (++currPathIndex == paths.length) + srv.stop(); + else + performNextTest(); + do_test_finished(); + }, + // NSISUPPORTS + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIStreamListener) || + aIID.equals(Ci.nsIRequestObserver) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + +function performNextTest() +{ + do_test_pending(); + + var ch = makeChannel(paths[currPathIndex]); + ch.asyncOpen(listener, null); +} + +var srv; + +function run_test() +{ + srv = createServer(); + + // base path + // XXX should actually test this works with a file by comparing streams! + var dirServ = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + var path = dirServ.get("CurProcD", Ci.nsILocalFile); + srv.registerDirectory("/", path); + + // register a few test paths + srv.registerPathHandler("/objHandler", objHandler); + srv.registerPathHandler("/functionHandler", functionHandler); + + srv.start(4444); + + performNextTest(); +} + +// PATH HANDLERS + +// /objHandler +var objHandler = + { + handle: function(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; + + do_check_eq(metadata.port, 4444); + + 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: function(id) + { + if (id.equals(Ci.nsISupports) || id.equals(Ci.nsIHttpRequestHandler)) + return this; + throw Cr.NS_ERROR_NOINTERFACE; + } + }; + +// /functionHandler +function functionHandler(metadata, response) +{ + response.setStatusLine("1.1", 404, "Page Not Found"); + response.setHeader("foopy", "quux-baz"); + + do_check_eq(metadata.port, 4444); + do_check_eq(metadata.host, "localhost"); + do_check_eq(metadata.path.charAt(0), "/"); + + var body = "this is text\n"; + response.bodyOutputStream.write(body, body.length); +} diff --git a/mozilla/netwerk/test/httpserver/test/test_empty_body.js b/mozilla/netwerk/test/httpserver/test/test_empty_body.js new file mode 100644 index 00000000000..1479690e5af --- /dev/null +++ b/mozilla/netwerk/test/httpserver/test/test_empty_body.js @@ -0,0 +1,120 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is MozJSHTTP code. + * + * The Initial Developer of the Original Code is + * Jeff Walden . + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// in its original incarnation, the server didn't like empty response-bodies; +// see the comment in _end for details + +var paths = + [ + "http://localhost:4444/empty-body-unwritten", + "http://localhost:4444/empty-body-written" + ]; +var currPathIndex = 0; + +var listener = + { + // NSISTREAMLISTENER + onDataAvailable: function(request, cx, inputStream, offset, count) + { + makeBIS(inputStream).readByteArray(count); // required by API + }, + // NSIREQUESTOBSERVER + onStartRequest: function(request, cx) + { + var ch = request.QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + do_check_true(ch.contentLength == 0); + }, + onStopRequest: function(request, cx, status) + { + if (++currPathIndex == paths.length) + srv.stop(); + else + performNextTest(); + do_test_finished(); + }, + // NSISUPPORTS + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIStreamListener) || + aIID.equals(Ci.nsIRequestObserver) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + +function performNextTest() +{ + do_test_pending(); + + var ch = makeChannel(paths[currPathIndex]); + ch.asyncOpen(listener, null); +} + +var srv; + +function run_test() +{ + srv = createServer(); + + // register a few test paths + srv.registerPathHandler("/empty-body-unwritten", emptyBodyUnwritten); + srv.registerPathHandler("/empty-body-written", emptyBodyWritten); + + srv.start(4444); + + performNextTest(); +} + +// 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/mozilla/netwerk/test/httpserver/test/test_errorhandler_exception.js b/mozilla/netwerk/test/httpserver/test/test_errorhandler_exception.js new file mode 100644 index 00000000000..4efe363d92d --- /dev/null +++ b/mozilla/netwerk/test/httpserver/test/test_errorhandler_exception.js @@ -0,0 +1,152 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is MozJSHTTP code. + * + * The Initial Developer of the Original Code is + * Jeff Walden . + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// Request handlers may throw exceptions, and those exception should be caught +// by the server and converted into the proper error codes. + +var paths = + [ + "http://localhost:4444/throws/exception", + "http://localhost:4444/this/file/does/not/exist/and/404s", + "http://localhost:4444/attempts/404/fails/so/400/fails/so/500s" + ]; +var currPathIndex = 0; + +var listener = + { + // NSISTREAMLISTENER + onDataAvailable: function(request, cx, inputStream, offset, count) + { + makeBIS(inputStream).readByteArray(count); // required by API + }, + // NSIREQUESTOBSERVER + onStartRequest: function(request, cx) + { + var ch = request.QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + switch (currPathIndex) + { + case 0: + checkStatusLine(ch, 1, 1, 500, "Internal Server Error"); + break; + + case 1: + checkStatusLine(ch, 1, 1, 400, "Bad Request"); + break; + + case 2: + checkStatusLine(ch, 1, 1, 500, "Internal Server Error"); + break; + } + }, + onStopRequest: function(request, cx, status) + { + do_check_true(Components.isSuccessCode(status)); + + switch (currPathIndex) + { + case 0: + break; + + case 1: + srv.registerErrorHandler(400, throwsException); + break; + + case 2: + break; + } + + if (++currPathIndex == paths.length) + srv.stop(); + else + performNextTest(); + do_test_finished(); + }, + // NSISUPPORTS + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIStreamListener) || + aIID.equals(Ci.nsIRequestObserver) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + +function checkStatusLine(channel, httpMaxVer, httpMinVer, httpCode, statusText) +{ + do_check_eq(channel.responseStatus, httpCode); + do_check_eq(channel.responseStatusText, statusText); + + var respMaj = {}, respMin = {}; + channel.getResponseVersion(respMaj, respMin); + do_check_eq(respMaj.value, httpMaxVer); + do_check_eq(respMin.value, httpMinVer); +} + +function performNextTest() +{ + do_test_pending(); + + var ch = makeChannel(paths[currPathIndex]); + ch.asyncOpen(listener, null); +} + +var srv; + +function run_test() +{ + srv = createServer(); + + srv.registerErrorHandler(404, throwsException); + srv.registerPathHandler("/throws/exception", throwsException); + + srv.start(4444); + + performNextTest(); +} + +// PATH HANDLERS + +// /throws/exception (and also a 404 and 400 error handler) +function throwsException(metadata, response) +{ + throw "this shouldn't cause an exit..."; + do_throw("Not reached!"); +} diff --git a/mozilla/netwerk/test/httpserver/test/test_headers.js b/mozilla/netwerk/test/httpserver/test/test_headers.js new file mode 100644 index 00000000000..167f47a8bc5 --- /dev/null +++ b/mozilla/netwerk/test/httpserver/test/test_headers.js @@ -0,0 +1,221 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is MozJSHTTP code. + * + * The Initial Developer of the Original Code is + * Jeff Walden . + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// tests for header storage in MozJSHTTP; 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 "Setting (" + fieldName + ", " + + fieldValue + ") as header succeeded!"; + } + catch (e) + { + if (e !== 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"); + do_check_eq(c, "text/html"); + + headers.setHeader("test", "FOO", false); + var c = headers.getHeader("test"); + do_check_eq(c, "FOO"); + + try + { + headers.getHeader(":"); + throw "Failed to throw for invalid header"; + } + catch (e) + { + if (e !== Cr.NS_ERROR_INVALID_ARG) + do_throw("headers.getHeader(':') must throw invalid arg"); + } + + try + { + headers.getHeader("valid"); + throw 'header doesn\'t exist'; + } + catch (e) + { + if (e !== 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; + do_check_true(it.toLowerCase() in heads); + delete heads[it.toLowerCase()]; + } + + for (var i in heads) + do_throw("still have properties in heads!?!?"); + +} + +function testHasHeader() +{ + var headers = new nsHttpHeaders(); + + headers.setHeader("foo", "bar"); + do_check_true(headers.hasHeader("foo")); + do_check_true(headers.hasHeader("fOo")); + do_check_false(headers.hasHeader("not-there")); + + headers.setHeader("f`'~", "bar"); + do_check_true(headers.hasHeader("F`'~")); + + try + { + headers.hasHeader(":"); + throw "failed to throw"; + } + catch (e) + { + if (e !== Cr.NS_ERROR_INVALID_ARG) + do_throw(".hasHeader for an invalid name should throw"); + } +} diff --git a/mozilla/netwerk/test/httpserver/test/test_registerdirectory.js b/mozilla/netwerk/test/httpserver/test/test_registerdirectory.js new file mode 100644 index 00000000000..e7de5f7466c --- /dev/null +++ b/mozilla/netwerk/test/httpserver/test/test_registerdirectory.js @@ -0,0 +1,314 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is MozJSHTTP code. + * + * The Initial Developer of the Original Code is + * Jeff Walden . + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// tests the registerDirectory API + +const BASE = "http://localhost:4444"; + +var paths = + [ +/* 0*/ BASE + "/test_registerdirectory.js", // without a base path +/* 1*/ BASE + "/test_registerdirectory.js", // with a base path +/* 2*/ BASE + "/test_registerdirectory.js", // without a base path + +/* 3*/ BASE + "/test_registerdirectory.js", // no registered path handler +/* 4*/ BASE + "/test_registerdirectory.js", // registered path handler +/* 5*/ BASE + "/test_registerdirectory.js", // removed path handler + +/* 6*/ BASE + "/test_registerdirectory.js", // with a base path +/* 7*/ BASE + "/test_registerdirectory.js", // ...and a path handler +/* 8*/ BASE + "/test_registerdirectory.js", // removed base handler +/* 9*/ BASE + "/test_registerdirectory.js", // removed path handler + +/*10*/ BASE + "/foo/test_registerdirectory.js", // mapping set up, works +/*11*/ BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js", // no mapping, fails +/*12*/ BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js", // mapping, works +/*13*/ BASE + "/foo/test_registerdirectory.js", // two mappings set up, still works +/*14*/ BASE + "/foo/test_registerdirectory.js", // mapping was removed +/*15*/ BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js", // mapping still present, works +/*16*/ BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js", // mapping removed + ]; +var currPathIndex = 0; + +var listener = + { + // NSISTREAMLISTENER + onDataAvailable: function(request, cx, inputStream, offset, count) + { + makeBIS(inputStream).readByteArray(count); // required by API + }, + // NSIREQUESTOBSERVER + onStartRequest: function(request, cx) + { + var ch = request.QueryInterface(Ci.nsIHttpChannel); + + switch (currPathIndex) + { + case 0: + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); + break; + + case 1: + do_check_eq(ch.responseStatus, 200); + do_check_true(ch.requestSucceeded); + + var actualFile = serverBasePath.clone(); + actualFile.append("test_registerdirectory.js"); + do_check_eq(ch.getResponseHeader("Content-Length"), + actualFile.fileSize.toString()); + break; + + case 2: + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); + break; + + case 3: + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); + break; + + case 4: + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.responseStatusText, "OK"); + do_check_true(ch.requestSucceeded); + do_check_eq(ch.getResponseHeader("Override-Succeeded"), "yes"); + break; + + case 5: + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); + break; + + case 6: + do_check_eq(ch.responseStatus, 200); + do_check_true(ch.requestSucceeded); + + var actualFile = serverBasePath.clone(); + actualFile.append("test_registerdirectory.js"); + do_check_eq(ch.getResponseHeader("Content-Length"), + actualFile.fileSize.toString()); + break; + + case 7: + case 8: + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.responseStatusText, "OK"); + do_check_true(ch.requestSucceeded); + do_check_eq(ch.getResponseHeader("Override-Succeeded"), "yes"); + break; + + case 9: + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); + break; + + case 10: + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.responseStatusText, "OK"); + break; + + case 11: + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); + break; + + case 12: + case 13: + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.responseStatusText, "OK"); + break; + + case 14: + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); + break; + + case 15: + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.responseStatusText, "OK"); + break; + + case 16: + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); + break; + } + }, + onStopRequest: function(request, cx, status) + { + var dirServ; + switch (currPathIndex) + { + case 0: + // now set a base path + dirServ = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + serverBasePath = dirServ.get("CurProcD", Ci.nsILocalFile); + serverBasePath.append("httpserver_tests"); + srv.registerDirectory("/", serverBasePath); + break; + + case 1: + // remove base path + serverBasePath = null; + srv.registerDirectory("/", serverBasePath); + break; + + case 3: + // register overriding path + srv.registerPathHandler("/test_registerdirectory.js", + override_test_registerdirectory); + break; + + case 4: + // unregister overriding path + srv.registerPathHandler("/test_registerdirectory.js", null); + break; + + case 5: + // set the base path again + dirServ = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + serverBasePath = dirServ.get("CurProcD", Ci.nsILocalFile); + serverBasePath.append("httpserver_tests"); + srv.registerDirectory("/", serverBasePath); + break; + + case 6: + // register overriding path + srv.registerPathHandler("/test_registerdirectory.js", + override_test_registerdirectory); + break; + + case 7: + // remove base path + serverBasePath = null; + srv.registerDirectory("/", serverBasePath); + break; + + case 8: + // unregister overriding path + srv.registerPathHandler("/test_registerdirectory.js", null); + break; + + case 9: + // register /foo/ as a base path + dirServ = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + serverBasePath = dirServ.get("CurProcD", Ci.nsILocalFile); + serverBasePath.append("httpserver_tests"); + srv.registerDirectory("/foo/", serverBasePath); + break; + + case 10: + // do nothing + break; + + case 11: + // now register an overriding path to handle the URL that just failed + srv.registerDirectory("/foo/test_registerdirectory.js/", serverBasePath); + break; + + case 12: + // do nothing + break; + + case 13: + srv.registerDirectory("/foo/", null); + break; + + case 14: + // do nothing + break; + + case 15: + srv.registerDirectory("/foo/test_registerdirectory.js/", null); + break; + } + + if (!paths[++currPathIndex]) + srv.stop(); + else + performNextTest(); + + do_test_finished(); + }, + // NSISUPPORTS + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIStreamListener) || + aIID.equals(Ci.nsIRequestObserver) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + +function performNextTest() +{ + do_test_pending(); + + var ch = makeChannel(paths[currPathIndex]); + ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important! + ch.asyncOpen(listener, null); +} + +var srv; +var serverBasePath; + +function run_test() +{ + srv = createServer(); + srv.start(4444); + + performNextTest(); +} + +// PATH HANDLERS + +// override of /test_registerdirectory.js +function override_test_registerdirectory(metadata, response) +{ + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Override-Succeeded", "yes"); + + var body = "success!"; + response.bodyOutputStream.write(body, body.length); +} diff --git a/mozilla/netwerk/test/httpserver/test/test_setstatusline.js b/mozilla/netwerk/test/httpserver/test/test_setstatusline.js new file mode 100644 index 00000000000..f417f935c45 --- /dev/null +++ b/mozilla/netwerk/test/httpserver/test/test_setstatusline.js @@ -0,0 +1,225 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is MozJSHTTP code. + * + * The Initial Developer of the Original Code is + * Jeff Walden . + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// exercise nsIHttpResponse.setStatusLine, ensure its atomicity, and ensure the +// specified behavior occurs if it's not called + +var paths = + [ + "http://localhost:4444/no/setstatusline", + "http://localhost:4444/http1_0", + "http://localhost:4444/http1_1", + "http://localhost:4444/invalidVersion", + "http://localhost:4444/invalidStatus", + "http://localhost:4444/invalidDescription", + "http://localhost:4444/crazyCode", + "http://localhost:4444/nullVersion" + ]; +var currPathIndex = 0; + +var listener = + { + // NSISTREAMLISTENER + onDataAvailable: function(request, cx, inputStream, offset, count) + { + makeBIS(inputStream).readByteArray(count); // required by API + }, + // NSIREQUESTOBSERVER + onStartRequest: function(request, cx) + { + var ch = request.QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + switch (currPathIndex) + { + case 0: + checkStatusLine(ch, 1, 1, 200, "OK"); + break; + + case 1: + checkStatusLine(ch, 1, 0, 200, "OK"); + break; + + case 2: + checkStatusLine(ch, 1, 1, 200, "OK"); + break; + + case 3: + case 4: + case 5: + checkStatusLine(ch, 1, 1, 200, "OK"); + do_check_eq(ch.getResponseHeader("Passed"), "true"); + break; + + case 6: + checkStatusLine(ch, 1, 1, 617, "Crazy"); + break; + + case 7: + // currently, this server implementation defaults to 1.1 + checkStatusLine(ch, 1, 1, 255, "NULL"); + break; + } + }, + onStopRequest: function(request, cx, status) + { + do_check_true(Components.isSuccessCode(status)); + if (++currPathIndex == paths.length) + srv.stop(); + else + performNextTest(); + do_test_finished(); + }, + // NSISUPPORTS + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIStreamListener) || + aIID.equals(Ci.nsIRequestObserver) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + +function checkStatusLine(channel, httpMaxVer, httpMinVer, httpCode, statusText) +{ + do_check_eq(channel.responseStatus, httpCode); + do_check_eq(channel.responseStatusText, statusText); + + var respMaj = {}, respMin = {}; + channel.getResponseVersion(respMaj, respMin); + do_check_eq(respMaj.value, httpMaxVer); + do_check_eq(respMin.value, httpMinVer); +} + +function performNextTest() +{ + do_test_pending(); + + var ch = makeChannel(paths[currPathIndex]); + ch.asyncOpen(listener, null); +} + +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(4444); + + performNextTest(); +} + +// PATH HANDLERS + +// /no/setstatusline +function noSetstatusline(metadata, response) +{ +} + +// /http1_0 +function http1_0(metadata, response) +{ + response.setStatusLine("1.0", 200, "OK"); +} + +// /http1_1 +function http1_1(metadata, response) +{ + response.setStatusLine("1.1", 200, "OK"); +} + +// /invalidVersion +function invalidVersion(metadata, response) +{ + try + { + response.setStatusLine(" 1.0", 200, "FAILED"); + } + catch (e) + { + response.setHeader("Passed", "true", false); + } +} + +// /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"); +} + +// /nullVersion +function nullVersion(metadata, response) +{ + response.setStatusLine(null, 255, "NULL"); +} diff --git a/mozilla/netwerk/test/unit/head_http_server.js b/mozilla/netwerk/test/unit/head_http_server.js deleted file mode 100644 index 25f9ec040df..00000000000 --- a/mozilla/netwerk/test/unit/head_http_server.js +++ /dev/null @@ -1,226 +0,0 @@ -// vim:set sw=2 ts=2 sts=2 cin: - -function nsTestServ(port) { - dump(">>> creating nsTestServ instance\n"); - this.port = port; -} - -nsTestServ.prototype = -{ - handler: { - // This function generates standard headers for a specific HTTP result - // code. The code can include text like "Not Found". The return value does - // not include a terminating empty line, so new headers can be added after - // it. - headers: function(code) { - return "HTTP/1.0 " + code + "\r\n" + - "Connection: close\r\n" + - "Server: Necko unit test server\r\n" + - "Content-Type: text/plain\r\n"; - }, - - // This object contains the handlers for the various request URIs - // numeric properties are HTTP result codes - 400: function(stream) - { - var response = this.headers("400 Bad Request") + - "\r\n" + - "Bad request"; - stream.write(response, response.length); - }, - 404: function(stream) - { - var response = this.headers("404 Not Found") + - "\r\n" + - "No such path"; - stream.write(response, response.length); - }, - - "/": function(stream) - { - var response = this.headers("200 OK") + - "\r\n" + - "200 OK"; - stream.write(response, response.length); - }, - - "/redirect": function(stream) - { - var response = this.headers("301 Moved Permanently") + - "Location: http://localhost:4444/\r\n" + - "\r\n" + - "Moved"; - stream.write(response, response.length); - }, - - "/redirectfile": function(stream) - { - var response = this.headers("301 Moved Permanently") + - "Location: file:///\r\n" + - "\r\n" + - "Moved to a file URI"; - stream.write(response, response.length); - }, - - "/auth": function(stream, req_head) { - // btoa("guest:guest"), but - // that function is not available here - var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; - var successResponse = this.headers("200 OK, authorized") + - 'WWW-Authenticate: Basic realm="secret"\r\n' + - "\r\n" + - "success"; - var failedResponse = this.headers("401 Unauthorized") + - 'WWW-Authenticate: Basic realm="secret"\r\n' + - "\r\n" + - "failed"; - if ("authorization" in req_head) { - // Make sure the credentials are right - if (req_head.authorization == expectedHeader) { - stream.write(successResponse, successResponse.length); - return; - } - // fall through to failure response - } - stream.write(failedResponse, failedResponse.length); - }, - - "/auth/ntlm/simple": function(stream) { - var response = this.headers("401 Unauthorized") + - "WWW-Authenticate: NTLM\r\n" + // realm=\"secret\"\r\n" + - "\r\n" + - "NOTE: This just sends an NTLM challenge, it never\n" + - "accepts the authentication. It also closes\n" + - "the connection after sending the challenge\n"; - stream.write(response, response.length); - }, - - // A URL that causes the current unit test to fail - // Use it to ensure that requests are cancelled before - // they reach the server - "/failtest": function (stream) { - do_throw("This should not be reached"); - } - }, - - QueryInterface: function(iid) - { - if (iid.equals(Components.interfaces.nsIServerSocketListener) || - iid.equals(Components.interfaces.nsISupports)) - return this; - - Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE; - return null; - }, - - /* this function is called when we receive a new connection */ - onSocketAccepted: function(serverSocket, clientSocket) - { - dump(">>> accepted connection on "+clientSocket.host+":"+clientSocket.port+"\n"); - - const nsITransport = Components.interfaces.nsITransport; - var input = clientSocket.openInputStream(nsITransport.OPEN_BLOCKING, 0, 0); - var output = clientSocket.openOutputStream(nsITransport.OPEN_BLOCKING, 0, 0); - - var request = this.parseInput(input); - - // Strip away query parameters, then unescape the request - var path = request[0].replace(/\?.*/, ""); - try { - path = decodeURI(path); - } catch (ex) { - path = 400; - } - - if (path in this.handler) - this.handler[path](output, request[1]); - else - this.handler[404](output, request[1]); - - input.close(); - output.close(); - }, - - onStopListening: function(serverSocket, status) - { - dump(">>> shutting down server socket\n"); - this.shutdown = true; - }, - - startListening: function() - { - const nsIServerSocket = Components.interfaces.nsIServerSocket; - const SERVERSOCKET_CONTRACTID = "@mozilla.org/network/server-socket;1"; - var socket = Components.classes[SERVERSOCKET_CONTRACTID].createInstance(nsIServerSocket); - socket.init(this.port, true /* loopback only */, 5); - dump(">>> listening on port "+socket.port+"\n"); - socket.asyncListen(this); - this.socket = socket; - }, - - // Shuts down the server. Note that this processes events. - stopListening: function() - { - if (!this.socket) - return; - this.socket.close(); - this.socket = null; - var thr = Components.classes["@mozilla.org/thread-manager;1"] - .getService().currentThread; - while (!this.shutdown) { - thr.processNextEvent(true); - } - }, - - parseInput: function(input) - { - // We read the input line by line. The first line tells us the requested - // path. - var is = Components.classes["@mozilla.org/intl/converter-input-stream;1"] - .createInstance(Components.interfaces.nsIConverterInputStream); - is.init(input, "ISO-8859-1", 1024, 0xFFFD); - - var lis = - is.QueryInterface(Components.interfaces.nsIUnicharLineInputStream); - var line = {}; - var cont = lis.readLine(line); - - var request = line.value.split(/ +/); - if (request[0] != "GET" && request[0] != "POST") - return [400]; - - // This server doesn't support HTTP 0.9 - if (request[2] != "HTTP/1.0" && request[2] != "HTTP/1.1") - return [400]; - - var req_head = {}; - while (true) { - lis.readLine(line); - if (line.value == "") - break; - var matches = line.value.match(/^([a-zA-Z]+): *(.*)$/); - if (matches !== null && matches.length == 3) { - // This is slightly wrong - should probably merge headers with the - // same name - req_head[matches[1].toLowerCase()] = matches[2]; - } - } - - if (request[1][0] != "/") - return [400]; - - return [request[1], req_head]; - }, - - socket: null, - shutdown: false -} - - - -function start_server(port) { - var serv = new nsTestServ(port); - serv.startListening(); - return serv; -} - diff --git a/mozilla/netwerk/test/unit/test_authentication.js b/mozilla/netwerk/test/unit/test_authentication.js index 17ddd524680..85c889881f3 100644 --- a/mozilla/netwerk/test/unit/test_authentication.js +++ b/mozilla/netwerk/test/unit/test_authentication.js @@ -1,11 +1,14 @@ // This file tests authentication prompt callbacks +do_import_script("test-harness/xpcshell-simple/httpd.js"); + const FLAG_RETURN_FALSE = 1 << 0; const FLAG_WRONG_PASSWORD = 1 << 1; const nsIAuthPrompt2 = Components.interfaces.nsIAuthPrompt2; const nsIAuthInformation = Components.interfaces.nsIAuthInformation; + function AuthPrompt1(flags) { this.flags = flags; } @@ -202,7 +205,7 @@ var listener = { current_test++; tests[current_test](); } else { - httpserv.stopListening(); + httpserv.stop(); } do_test_finished(); @@ -225,7 +228,13 @@ var current_test = 0; var httpserv = null; function run_test() { - httpserv = start_server(4444); + httpserv = new nsHttpServer(); + + httpserv.registerPathHandler("/auth", authHandler); + httpserv.registerPathHandler("/auth/ntlm/simple", authNtlmSimple); + + httpserv.start(4444); + tests[0](); } @@ -308,3 +317,43 @@ function test_ntlm() { do_test_pending(); } +// PATH HANDLERS + +// /auth +function authHandler(metadata, response) { + // btoa("guest:guest"), but that function is not available here + var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; + + var body; + if (metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader) + { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "success"; + } + else + { + // didn't know guest:guest, failure + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "failed"; + } + + response.bodyOutputStream.write(body, body.length); +} + +// /auth/ntlm/simple +function authNtlmSimple(metadata, response) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", "NTLM" /* + ' realm="secret"' */); + + var body = "NOTE: This just sends an NTLM challenge, it never\n" + + "accepts the authentication. It also closes\n" + + "the connection after sending the challenge\n"; + + + response.bodyOutputStream.write(body, body.length); +} diff --git a/mozilla/netwerk/test/unit/test_bug331825.js b/mozilla/netwerk/test/unit/test_bug331825.js index 7bcda0b8e4b..39c537e2937 100644 --- a/mozilla/netwerk/test/unit/test_bug331825.js +++ b/mozilla/netwerk/test/unit/test_bug331825.js @@ -1,11 +1,8 @@ +do_import_script("test-harness/xpcshell-simple/httpd.js"); + var server; var BUGID = "331825"; -function handle_response(stream) { - var response = server.handler.headers("304 Not Modified") + "\r\n"; - stream.write(response, response.length); -} - function TestListener() { } TestListener.prototype.onStartRequest = function(request, context) { @@ -14,15 +11,17 @@ TestListener.prototype.onStopRequest = function(request, context, status) { var channel = request.QueryInterface(Components.interfaces.nsIHttpChannel); do_check_eq(channel.responseStatus, 304); + server.stop(); do_test_finished(); - server.stopListening(); } function run_test() { // start server - server = new nsTestServ(4444); - server.handler["/bug" + BUGID] = handle_response; - server.startListening(); + server = new nsHttpServer(); + + server.registerPathHandler("/bug" + BUGID, bug331825); + + server.start(4444); // make request var channel = @@ -36,3 +35,8 @@ function run_test() { do_test_pending(); } + +// PATH HANDLER FOR /bug331825 +function bug331825(metadata, response) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); +} diff --git a/mozilla/netwerk/test/unit/test_content_sniffer.js b/mozilla/netwerk/test/unit/test_content_sniffer.js index e0de0432b32..664ff2b3f05 100644 --- a/mozilla/netwerk/test/unit/test_content_sniffer.js +++ b/mozilla/netwerk/test/unit/test_content_sniffer.js @@ -1,5 +1,7 @@ // This file tests nsIContentSniffer, introduced in bug 324985 +do_import_script("test-harness/xpcshell-simple/httpd.js"); + const unknownType = "application/x-unknown-content-type"; const sniffedType = "application/x-sniffed"; @@ -89,7 +91,8 @@ var urls = [ var httpserv = null; function run_test() { - httpserv = start_server(4444); + httpserv = new nsHttpServer(); + httpserv.start(4444); Components.manager.nsIComponentRegistrar.registerFactory(snifferCID, "Unit test content sniffer", snifferContract, sniffer); @@ -103,7 +106,7 @@ function run_test_iteration(index) { sniffing_enabled = false; index = listener._iteration = 1; } else { - httpserv.stopListening(); + httpserv.stop(); return; // we're done } } diff --git a/mozilla/netwerk/test/unit/test_cookie_header.js b/mozilla/netwerk/test/unit/test_cookie_header.js index 9e928ff98d2..5451ce9d4b1 100644 --- a/mozilla/netwerk/test/unit/test_cookie_header.js +++ b/mozilla/netwerk/test/unit/test_cookie_header.js @@ -1,5 +1,7 @@ // This file tests bug 250375 +do_import_script("test-harness/xpcshell-simple/httpd.js"); + function check_request_header(chan, name, value) { var chanValue; try { @@ -33,7 +35,7 @@ var listener = { if (this._iteration == 1) run_test_continued(); else - httpserv.stopListening(); + httpserv.stop(); do_test_finished(); }, @@ -52,7 +54,8 @@ function makeChan() { var httpserv = null; function run_test() { - httpserv = start_server(4444); + httpserv = new nsHttpServer(); + httpserv.start(4444); var chan = makeChan(); @@ -81,4 +84,3 @@ function run_test_continued() { do_test_pending(); } - diff --git a/mozilla/netwerk/test/unit/test_event_sink.js b/mozilla/netwerk/test/unit/test_event_sink.js index 64c544d88a1..371c6ee8e29 100644 --- a/mozilla/netwerk/test/unit/test_event_sink.js +++ b/mozilla/netwerk/test/unit/test_event_sink.js @@ -1,5 +1,7 @@ // This file tests channel event sinks (bug 315598 et al) +do_import_script("test-harness/xpcshell-simple/httpd.js"); + const sinkCID = Components.ID("{14aa4b81-e266-45cb-88f8-89595dece114}"); const sinkContract = "@mozilla.org/network/unittest/channeleventsink;1"; @@ -82,7 +84,7 @@ var listener = { if (this._iteration <= 2) run_test_continued(); else - httpserv.stopListening(); + httpserv.stop(); do_test_finished(); }, @@ -101,7 +103,10 @@ function makeChan(url) { var httpserv = null; function run_test() { - httpserv = start_server(4444); + httpserv = new nsHttpServer(); + httpserv.registerPathHandler("/redirect", redirect); + httpserv.registerPathHandler("/redirectfile", redirectfile); + httpserv.start(4444); Components.manager.nsIComponentRegistrar.registerFactory(sinkCID, "Unit test Event sink", sinkContract, eventsink); @@ -141,3 +146,25 @@ function run_test_continued() { do_test_pending(); } +// PATHS + +// /redirect +function redirect(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", + "http://localhost:" + metadata.port + "/", + false); + + var body = "Moved\n"; + response.bodyOutputStream.write(body, body.length); +} + +// /redirectfile +function redirectfile(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Location", "file:///etc/", false); + + var body = "Attempted to move to a file URI, but failed.\n"; + response.bodyOutputStream.write(body, body.length); +} diff --git a/mozilla/netwerk/test/unit/test_httpcancel.js b/mozilla/netwerk/test/unit/test_httpcancel.js index fd041a47d17..52a404af21e 100644 --- a/mozilla/netwerk/test/unit/test_httpcancel.js +++ b/mozilla/netwerk/test/unit/test_httpcancel.js @@ -1,6 +1,8 @@ // This file ensures that canceling a channel early does not // send the request to the server (bug 350790) +do_import_script("test-harness/xpcshell-simple/httpd.js"); + const NS_BINDING_ABORTED = 0x804b0002; var observer = { @@ -31,7 +33,7 @@ var listener = { }, onStopRequest: function test_onStopR(request, ctx, status) { - httpserv.stopListening(); + httpserv.stop(); do_test_finished(); } }; @@ -48,7 +50,9 @@ function makeChan(url) { var httpserv = null; function run_test() { - httpserv = start_server(4444); + httpserv = new nsHttpServer(); + httpserv.registerPathHandler("/failtest", failtest); + httpserv.start(4444); var obs = Components.classes["@mozilla.org/observer-service;1"].getService(); obs = obs.QueryInterface(Components.interfaces.nsIObserverService); @@ -61,3 +65,9 @@ function run_test() { do_test_pending(); } +// PATHS + +// /failtest +function failtest(metadata, response) { + do_throw("This should not be reached"); +} diff --git a/mozilla/tools/test-harness/xpcshell-simple/head.js b/mozilla/tools/test-harness/xpcshell-simple/head.js index 690e31c213b..dfc349a91eb 100644 --- a/mozilla/tools/test-harness/xpcshell-simple/head.js +++ b/mozilla/tools/test-harness/xpcshell-simple/head.js @@ -117,6 +117,10 @@ function do_check_true(condition) { do_check_eq(condition, true); } +function do_check_false(condition) { + do_check_eq(condition, false); +} + function do_test_pending() { dump("*** test pending\n"); _tests_pending++; @@ -127,3 +131,12 @@ function do_test_finished() { if (--_tests_pending == 0) _do_quit(); } + +function do_import_script(distRelativePath) { + var scriptPath = environment["DIST"]; + if (scriptPath.charAt(scriptPath.length - 1) != "/") + scriptPath += "/"; + scriptPath += distRelativePath; + + load(scriptPath); +} diff --git a/mozilla/tools/test-harness/xpcshell-simple/test_all.sh b/mozilla/tools/test-harness/xpcshell-simple/test_all.sh index e0ab0652aad..d348755ad4d 100755 --- a/mozilla/tools/test-harness/xpcshell-simple/test_all.sh +++ b/mozilla/tools/test-harness/xpcshell-simple/test_all.sh @@ -83,7 +83,7 @@ done for t in $testdir/test_*.js do echo -n "$t: " - $bin/xpcshell $headfiles -f $t $tailfiles 2> $t.log 1>&2 + DIST="$bin" $bin/xpcshell $headfiles -f $t $tailfiles 2> $t.log 1>&2 if [ `grep -c '\*\*\* PASS' $t.log` = 0 ] then echo "FAIL"