michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /* michael@0: * An implementation of an HTTP server both as a loadable script and as an XPCOM michael@0: * component. See the accompanying README file for user documentation on michael@0: * httpd.js. michael@0: */ michael@0: michael@0: module.metadata = { michael@0: "stability": "experimental" michael@0: }; michael@0: michael@0: const { components, CC, Cc, Ci, Cr, Cu } = require("chrome"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: michael@0: const PR_UINT32_MAX = Math.pow(2, 32) - 1; michael@0: michael@0: /** True if debugging output is enabled, false otherwise. */ michael@0: var DEBUG = false; // non-const *only* so tweakable in server tests michael@0: michael@0: /** True if debugging output should be timestamped. */ michael@0: var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests michael@0: michael@0: var gGlobalObject = Cc["@mozilla.org/systemprincipal;1"].createInstance(); michael@0: michael@0: /** michael@0: * Asserts that the given condition holds. If it doesn't, the given message is michael@0: * dumped, a stack trace is printed, and an exception is thrown to attempt to michael@0: * stop execution (which unfortunately must rely upon the exception not being michael@0: * accidentally swallowed by the code that uses it). michael@0: */ michael@0: function NS_ASSERT(cond, msg) michael@0: { michael@0: if (DEBUG && !cond) michael@0: { michael@0: dumpn("###!!!"); michael@0: dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!")); michael@0: dumpn("###!!! Stack follows:"); michael@0: michael@0: var stack = new Error().stack.split(/\n/); michael@0: dumpn(stack.map(function(val) { return "###!!! " + val; }).join("\n")); michael@0: michael@0: throw Cr.NS_ERROR_ABORT; michael@0: } michael@0: } michael@0: michael@0: /** Constructs an HTTP error object. */ michael@0: function HttpError(code, description) michael@0: { michael@0: this.code = code; michael@0: this.description = description; michael@0: } michael@0: HttpError.prototype = michael@0: { michael@0: toString: function() michael@0: { michael@0: return this.code + " " + this.description; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Errors thrown to trigger specific HTTP server responses. michael@0: */ michael@0: const HTTP_400 = new HttpError(400, "Bad Request"); michael@0: const HTTP_401 = new HttpError(401, "Unauthorized"); michael@0: const HTTP_402 = new HttpError(402, "Payment Required"); michael@0: const HTTP_403 = new HttpError(403, "Forbidden"); michael@0: const HTTP_404 = new HttpError(404, "Not Found"); michael@0: const HTTP_405 = new HttpError(405, "Method Not Allowed"); michael@0: const HTTP_406 = new HttpError(406, "Not Acceptable"); michael@0: const HTTP_407 = new HttpError(407, "Proxy Authentication Required"); michael@0: const HTTP_408 = new HttpError(408, "Request Timeout"); michael@0: const HTTP_409 = new HttpError(409, "Conflict"); michael@0: const HTTP_410 = new HttpError(410, "Gone"); michael@0: const HTTP_411 = new HttpError(411, "Length Required"); michael@0: const HTTP_412 = new HttpError(412, "Precondition Failed"); michael@0: const HTTP_413 = new HttpError(413, "Request Entity Too Large"); michael@0: const HTTP_414 = new HttpError(414, "Request-URI Too Long"); michael@0: const HTTP_415 = new HttpError(415, "Unsupported Media Type"); michael@0: const HTTP_417 = new HttpError(417, "Expectation Failed"); michael@0: michael@0: const HTTP_500 = new HttpError(500, "Internal Server Error"); michael@0: const HTTP_501 = new HttpError(501, "Not Implemented"); michael@0: const HTTP_502 = new HttpError(502, "Bad Gateway"); michael@0: const HTTP_503 = new HttpError(503, "Service Unavailable"); michael@0: const HTTP_504 = new HttpError(504, "Gateway Timeout"); michael@0: const HTTP_505 = new HttpError(505, "HTTP Version Not Supported"); michael@0: michael@0: /** Creates a hash with fields corresponding to the values in arr. */ michael@0: function array2obj(arr) michael@0: { michael@0: var obj = {}; michael@0: for (var i = 0; i < arr.length; i++) michael@0: obj[arr[i]] = arr[i]; michael@0: return obj; michael@0: } michael@0: michael@0: /** Returns an array of the integers x through y, inclusive. */ michael@0: function range(x, y) michael@0: { michael@0: var arr = []; michael@0: for (var i = x; i <= y; i++) michael@0: arr.push(i); michael@0: return arr; michael@0: } michael@0: michael@0: /** An object (hash) whose fields are the numbers of all HTTP error codes. */ michael@0: const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505))); michael@0: michael@0: michael@0: /** michael@0: * The character used to distinguish hidden files from non-hidden files, a la michael@0: * the leading dot in Apache. Since that mechanism also hides files from michael@0: * easy display in LXR, ls output, etc. however, we choose instead to use a michael@0: * suffix character. If a requested file ends with it, we append another michael@0: * when getting the file on the server. If it doesn't, we just look up that michael@0: * file. Therefore, any file whose name ends with exactly one of the character michael@0: * is "hidden" and available for use by the server. michael@0: */ michael@0: const HIDDEN_CHAR = "^"; michael@0: michael@0: /** michael@0: * The file name suffix indicating the file containing overridden headers for michael@0: * a requested file. michael@0: */ michael@0: const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR; michael@0: michael@0: /** Type used to denote SJS scripts for CGI-like functionality. */ michael@0: const SJS_TYPE = "sjs"; michael@0: michael@0: /** Base for relative timestamps produced by dumpn(). */ michael@0: var firstStamp = 0; michael@0: michael@0: /** dump(str) with a trailing "\n" -- only outputs if DEBUG. */ michael@0: function dumpn(str) michael@0: { michael@0: if (DEBUG) michael@0: { michael@0: var prefix = "HTTPD-INFO | "; michael@0: if (DEBUG_TIMESTAMP) michael@0: { michael@0: if (firstStamp === 0) michael@0: firstStamp = Date.now(); michael@0: michael@0: var elapsed = Date.now() - firstStamp; // milliseconds michael@0: var min = Math.floor(elapsed / 60000); michael@0: var sec = (elapsed % 60000) / 1000; michael@0: michael@0: if (sec < 10) michael@0: prefix += min + ":0" + sec.toFixed(3) + " | "; michael@0: else michael@0: prefix += min + ":" + sec.toFixed(3) + " | "; michael@0: } michael@0: michael@0: dump(prefix + str + "\n"); michael@0: } michael@0: } michael@0: michael@0: /** Dumps the current JS stack if DEBUG. */ michael@0: function dumpStack() michael@0: { michael@0: // peel off the frames for dumpStack() and Error() michael@0: var stack = new Error().stack.split(/\n/).slice(2); michael@0: stack.forEach(dumpn); michael@0: } michael@0: michael@0: michael@0: /** The XPCOM thread manager. */ michael@0: var gThreadManager = null; michael@0: michael@0: /** The XPCOM prefs service. */ michael@0: var gRootPrefBranch = null; michael@0: function getRootPrefBranch() michael@0: { michael@0: if (!gRootPrefBranch) michael@0: { michael@0: gRootPrefBranch = Cc["@mozilla.org/preferences-service;1"] michael@0: .getService(Ci.nsIPrefBranch); michael@0: } michael@0: return gRootPrefBranch; michael@0: } michael@0: michael@0: /** michael@0: * JavaScript constructors for commonly-used classes; precreating these is a michael@0: * speedup over doing the same from base principles. See the docs at michael@0: * http://developer.mozilla.org/en/docs/components.Constructor for details. michael@0: */ michael@0: const ServerSocket = CC("@mozilla.org/network/server-socket;1", michael@0: "nsIServerSocket", michael@0: "init"); michael@0: const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1", michael@0: "nsIScriptableInputStream", michael@0: "init"); michael@0: const Pipe = CC("@mozilla.org/pipe;1", michael@0: "nsIPipe", michael@0: "init"); michael@0: const FileInputStream = CC("@mozilla.org/network/file-input-stream;1", michael@0: "nsIFileInputStream", michael@0: "init"); michael@0: const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1", michael@0: "nsIConverterInputStream", michael@0: "init"); michael@0: const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1", michael@0: "nsIWritablePropertyBag2"); michael@0: const SupportsString = CC("@mozilla.org/supports-string;1", michael@0: "nsISupportsString"); michael@0: michael@0: /* These two are non-const only so a test can overwrite them. */ michael@0: var BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", michael@0: "nsIBinaryInputStream", michael@0: "setInputStream"); michael@0: var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", michael@0: "nsIBinaryOutputStream", michael@0: "setOutputStream"); michael@0: michael@0: /** michael@0: * Returns the RFC 822/1123 representation of a date. michael@0: * michael@0: * @param date : Number michael@0: * the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT michael@0: * @returns string michael@0: * the representation of the given date michael@0: */ michael@0: function toDateString(date) michael@0: { michael@0: // michael@0: // rfc1123-date = wkday "," SP date1 SP time SP "GMT" michael@0: // date1 = 2DIGIT SP month SP 4DIGIT michael@0: // ; day month year (e.g., 02 Jun 1982) michael@0: // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT michael@0: // ; 00:00:00 - 23:59:59 michael@0: // wkday = "Mon" | "Tue" | "Wed" michael@0: // | "Thu" | "Fri" | "Sat" | "Sun" michael@0: // month = "Jan" | "Feb" | "Mar" | "Apr" michael@0: // | "May" | "Jun" | "Jul" | "Aug" michael@0: // | "Sep" | "Oct" | "Nov" | "Dec" michael@0: // michael@0: michael@0: const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; michael@0: const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", michael@0: "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; michael@0: michael@0: /** michael@0: * Processes a date and returns the encoded UTC time as a string according to michael@0: * the format specified in RFC 2616. michael@0: * michael@0: * @param date : Date michael@0: * the date to process michael@0: * @returns string michael@0: * a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" michael@0: */ michael@0: function toTime(date) michael@0: { michael@0: var hrs = date.getUTCHours(); michael@0: var rv = (hrs < 10) ? "0" + hrs : hrs; michael@0: michael@0: var mins = date.getUTCMinutes(); michael@0: rv += ":"; michael@0: rv += (mins < 10) ? "0" + mins : mins; michael@0: michael@0: var secs = date.getUTCSeconds(); michael@0: rv += ":"; michael@0: rv += (secs < 10) ? "0" + secs : secs; michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: /** michael@0: * Processes a date and returns the encoded UTC date as a string according to michael@0: * the date1 format specified in RFC 2616. michael@0: * michael@0: * @param date : Date michael@0: * the date to process michael@0: * @returns string michael@0: * a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" michael@0: */ michael@0: function toDate1(date) michael@0: { michael@0: var day = date.getUTCDate(); michael@0: var month = date.getUTCMonth(); michael@0: var year = date.getUTCFullYear(); michael@0: michael@0: var rv = (day < 10) ? "0" + day : day; michael@0: rv += " " + monthStrings[month]; michael@0: rv += " " + year; michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: date = new Date(date); michael@0: michael@0: const fmtString = "%wkday%, %date1% %time% GMT"; michael@0: var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]); michael@0: rv = rv.replace("%time%", toTime(date)); michael@0: return rv.replace("%date1%", toDate1(date)); michael@0: } michael@0: michael@0: /** michael@0: * Prints out a human-readable representation of the object o and its fields, michael@0: * omitting those whose names begin with "_" if showMembers != true (to ignore michael@0: * "private" properties exposed via getters/setters). michael@0: */ michael@0: function printObj(o, showMembers) michael@0: { michael@0: var s = "******************************\n"; michael@0: s += "o = {\n"; michael@0: for (var i in o) michael@0: { michael@0: if (typeof(i) != "string" || michael@0: (showMembers || (i.length > 0 && i[0] != "_"))) michael@0: s+= " " + i + ": " + o[i] + ",\n"; michael@0: } michael@0: s += " };\n"; michael@0: s += "******************************"; michael@0: dumpn(s); michael@0: } michael@0: michael@0: /** michael@0: * Instantiates a new HTTP server. michael@0: */ michael@0: function nsHttpServer() michael@0: { michael@0: if (!gThreadManager) michael@0: gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService(); michael@0: michael@0: /** The port on which this server listens. */ michael@0: this._port = undefined; michael@0: michael@0: /** The socket associated with this. */ michael@0: this._socket = null; michael@0: michael@0: /** The handler used to process requests to this server. */ michael@0: this._handler = new ServerHandler(this); michael@0: michael@0: /** Naming information for this server. */ michael@0: this._identity = new ServerIdentity(); michael@0: michael@0: /** michael@0: * Indicates when the server is to be shut down at the end of the request. michael@0: */ michael@0: this._doQuit = false; michael@0: michael@0: /** michael@0: * True if the socket in this is closed (and closure notifications have been michael@0: * sent and processed if the socket was ever opened), false otherwise. michael@0: */ michael@0: this._socketClosed = true; michael@0: michael@0: /** michael@0: * Used for tracking existing connections and ensuring that all connections michael@0: * are properly cleaned up before server shutdown; increases by 1 for every michael@0: * new incoming connection. michael@0: */ michael@0: this._connectionGen = 0; michael@0: michael@0: /** michael@0: * Hash of all open connections, indexed by connection number at time of michael@0: * creation. michael@0: */ michael@0: this._connections = {}; michael@0: } michael@0: nsHttpServer.prototype = michael@0: { michael@0: classID: components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"), michael@0: michael@0: // NSISERVERSOCKETLISTENER michael@0: michael@0: /** michael@0: * Processes an incoming request coming in on the given socket and contained michael@0: * in the given transport. michael@0: * michael@0: * @param socket : nsIServerSocket michael@0: * the socket through which the request was served michael@0: * @param trans : nsISocketTransport michael@0: * the transport for the request/response michael@0: * @see nsIServerSocketListener.onSocketAccepted michael@0: */ michael@0: onSocketAccepted: function(socket, trans) michael@0: { michael@0: dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")"); michael@0: michael@0: dumpn(">>> new connection on " + trans.host + ":" + trans.port); michael@0: michael@0: const SEGMENT_SIZE = 8192; michael@0: const SEGMENT_COUNT = 1024; michael@0: try michael@0: { michael@0: var input = trans.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT) michael@0: .QueryInterface(Ci.nsIAsyncInputStream); michael@0: var output = trans.openOutputStream(0, 0, 0); michael@0: } michael@0: catch (e) michael@0: { michael@0: dumpn("*** error opening transport streams: " + e); michael@0: trans.close(Cr.NS_BINDING_ABORTED); michael@0: return; michael@0: } michael@0: michael@0: var connectionNumber = ++this._connectionGen; michael@0: michael@0: try michael@0: { michael@0: var conn = new Connection(input, output, this, socket.port, trans.port, michael@0: connectionNumber); michael@0: var reader = new RequestReader(conn); michael@0: michael@0: // XXX add request timeout functionality here! michael@0: michael@0: // Note: must use main thread here, or we might get a GC that will cause michael@0: // threadsafety assertions. We really need to fix XPConnect so that michael@0: // you can actually do things in multi-threaded JS. :-( michael@0: input.asyncWait(reader, 0, 0, gThreadManager.mainThread); michael@0: } michael@0: catch (e) michael@0: { michael@0: // Assume this connection can't be salvaged and bail on it completely; michael@0: // don't attempt to close it so that we can assert that any connection michael@0: // being closed is in this._connections. michael@0: dumpn("*** error in initial request-processing stages: " + e); michael@0: trans.close(Cr.NS_BINDING_ABORTED); michael@0: return; michael@0: } michael@0: michael@0: this._connections[connectionNumber] = conn; michael@0: dumpn("*** starting connection " + connectionNumber); michael@0: }, michael@0: michael@0: /** michael@0: * Called when the socket associated with this is closed. michael@0: * michael@0: * @param socket : nsIServerSocket michael@0: * the socket being closed michael@0: * @param status : nsresult michael@0: * the reason the socket stopped listening (NS_BINDING_ABORTED if the server michael@0: * was stopped using nsIHttpServer.stop) michael@0: * @see nsIServerSocketListener.onStopListening michael@0: */ michael@0: onStopListening: function(socket, status) michael@0: { michael@0: dumpn(">>> shutting down server on port " + socket.port); michael@0: this._socketClosed = true; michael@0: if (!this._hasOpenConnections()) michael@0: { michael@0: dumpn("*** no open connections, notifying async from onStopListening"); michael@0: michael@0: // Notify asynchronously so that any pending teardown in stop() has a michael@0: // chance to run first. michael@0: var self = this; michael@0: var stopEvent = michael@0: { michael@0: run: function() michael@0: { michael@0: dumpn("*** _notifyStopped async callback"); michael@0: self._notifyStopped(); michael@0: } michael@0: }; michael@0: gThreadManager.currentThread michael@0: .dispatch(stopEvent, Ci.nsIThread.DISPATCH_NORMAL); michael@0: } michael@0: }, michael@0: michael@0: // NSIHTTPSERVER michael@0: michael@0: // michael@0: // see nsIHttpServer.start michael@0: // michael@0: start: function(port) michael@0: { michael@0: this._start(port, "localhost") michael@0: }, michael@0: michael@0: _start: function(port, host) michael@0: { michael@0: if (this._socket) michael@0: throw Cr.NS_ERROR_ALREADY_INITIALIZED; michael@0: michael@0: this._port = port; michael@0: this._doQuit = this._socketClosed = false; michael@0: michael@0: this._host = host; michael@0: michael@0: // The listen queue needs to be long enough to handle michael@0: // network.http.max-persistent-connections-per-server concurrent connections, michael@0: // plus a safety margin in case some other process is talking to michael@0: // the server as well. michael@0: var prefs = getRootPrefBranch(); michael@0: var maxConnections; michael@0: try { michael@0: // Bug 776860: The original pref was removed in favor of this new one: michael@0: maxConnections = prefs.getIntPref("network.http.max-persistent-connections-per-server") + 5; michael@0: } michael@0: catch(e) { michael@0: maxConnections = prefs.getIntPref("network.http.max-connections-per-server") + 5; michael@0: } michael@0: michael@0: try michael@0: { michael@0: var loopback = true; michael@0: if (this._host != "127.0.0.1" && this._host != "localhost") { michael@0: var loopback = false; michael@0: } michael@0: michael@0: var socket = new ServerSocket(this._port, michael@0: loopback, // true = localhost, false = everybody michael@0: maxConnections); michael@0: dumpn(">>> listening on port " + socket.port + ", " + maxConnections + michael@0: " pending connections"); michael@0: socket.asyncListen(this); michael@0: this._identity._initialize(socket.port, host, true); michael@0: this._socket = socket; michael@0: } michael@0: catch (e) michael@0: { michael@0: dumpn("!!! could not start server on port " + port + ": " + e); michael@0: throw Cr.NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.stop michael@0: // michael@0: stop: function(callback) michael@0: { michael@0: if (!callback) michael@0: throw Cr.NS_ERROR_NULL_POINTER; michael@0: if (!this._socket) michael@0: throw Cr.NS_ERROR_UNEXPECTED; michael@0: michael@0: this._stopCallback = typeof callback === "function" michael@0: ? callback michael@0: : function() { callback.onStopped(); }; michael@0: michael@0: dumpn(">>> stopping listening on port " + this._socket.port); michael@0: this._socket.close(); michael@0: this._socket = null; michael@0: michael@0: // We can't have this identity any more, and the port on which we're running michael@0: // this server now could be meaningless the next time around. michael@0: this._identity._teardown(); michael@0: michael@0: this._doQuit = false; michael@0: michael@0: // socket-close notification and pending request completion happen async michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.registerFile michael@0: // michael@0: registerFile: function(path, file) michael@0: { michael@0: if (file && (!file.exists() || file.isDirectory())) michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: this._handler.registerFile(path, file); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.registerDirectory michael@0: // michael@0: registerDirectory: function(path, directory) michael@0: { michael@0: // XXX true path validation! michael@0: if (path.charAt(0) != "/" || michael@0: path.charAt(path.length - 1) != "/" || michael@0: (directory && michael@0: (!directory.exists() || !directory.isDirectory()))) michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping michael@0: // exists! michael@0: michael@0: this._handler.registerDirectory(path, directory); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.registerPathHandler michael@0: // michael@0: registerPathHandler: function(path, handler) michael@0: { michael@0: this._handler.registerPathHandler(path, handler); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.registerPrefixHandler michael@0: // michael@0: registerPrefixHandler: function(prefix, handler) michael@0: { michael@0: this._handler.registerPrefixHandler(prefix, handler); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.registerErrorHandler michael@0: // michael@0: registerErrorHandler: function(code, handler) michael@0: { michael@0: this._handler.registerErrorHandler(code, handler); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.setIndexHandler michael@0: // michael@0: setIndexHandler: function(handler) michael@0: { michael@0: this._handler.setIndexHandler(handler); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.registerContentType michael@0: // michael@0: registerContentType: function(ext, type) michael@0: { michael@0: this._handler.registerContentType(ext, type); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.serverIdentity michael@0: // michael@0: get identity() michael@0: { michael@0: return this._identity; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.getState michael@0: // michael@0: getState: function(path, k) michael@0: { michael@0: return this._handler._getState(path, k); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.setState michael@0: // michael@0: setState: function(path, k, v) michael@0: { michael@0: return this._handler._setState(path, k, v); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.getSharedState michael@0: // michael@0: getSharedState: function(k) michael@0: { michael@0: return this._handler._getSharedState(k); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.setSharedState michael@0: // michael@0: setSharedState: function(k, v) michael@0: { michael@0: return this._handler._setSharedState(k, v); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.getObjectState michael@0: // michael@0: getObjectState: function(k) michael@0: { michael@0: return this._handler._getObjectState(k); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.setObjectState michael@0: // michael@0: setObjectState: function(k, v) michael@0: { michael@0: return this._handler._setObjectState(k, v); michael@0: }, michael@0: michael@0: michael@0: // NSISUPPORTS michael@0: michael@0: // michael@0: // see nsISupports.QueryInterface michael@0: // michael@0: QueryInterface: function(iid) michael@0: { michael@0: if (iid.equals(Ci.nsIServerSocketListener) || iid.equals(Ci.nsISupports)) michael@0: return this; michael@0: michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: michael@0: // NON-XPCOM PUBLIC API michael@0: michael@0: /** michael@0: * Returns true iff this server is not running (and is not in the process of michael@0: * serving any requests still to be processed when the server was last michael@0: * stopped after being run). michael@0: */ michael@0: isStopped: function() michael@0: { michael@0: return this._socketClosed && !this._hasOpenConnections(); michael@0: }, michael@0: michael@0: // PRIVATE IMPLEMENTATION michael@0: michael@0: /** True if this server has any open connections to it, false otherwise. */ michael@0: _hasOpenConnections: function() michael@0: { michael@0: // michael@0: // If we have any open connections, they're tracked as numeric properties on michael@0: // |this._connections|. The non-standard __count__ property could be used michael@0: // to check whether there are any properties, but standard-wise, even michael@0: // looking forward to ES5, there's no less ugly yet still O(1) way to do michael@0: // this. michael@0: // michael@0: for (var n in this._connections) michael@0: return true; michael@0: return false; michael@0: }, michael@0: michael@0: /** Calls the server-stopped callback provided when stop() was called. */ michael@0: _notifyStopped: function() michael@0: { michael@0: NS_ASSERT(this._stopCallback !== null, "double-notifying?"); michael@0: NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now"); michael@0: michael@0: // michael@0: // NB: We have to grab this now, null out the member, *then* call the michael@0: // callback here, or otherwise the callback could (indirectly) futz with michael@0: // this._stopCallback by starting and immediately stopping this, at michael@0: // which point we'd be nulling out a field we no longer have a right to michael@0: // modify. michael@0: // michael@0: var callback = this._stopCallback; michael@0: this._stopCallback = null; michael@0: try michael@0: { michael@0: callback(); michael@0: } michael@0: catch (e) michael@0: { michael@0: // not throwing because this is specified as being usually (but not michael@0: // always) asynchronous michael@0: dump("!!! error running onStopped callback: " + e + "\n"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Notifies this server that the given connection has been closed. michael@0: * michael@0: * @param connection : Connection michael@0: * the connection that was closed michael@0: */ michael@0: _connectionClosed: function(connection) michael@0: { michael@0: NS_ASSERT(connection.number in this._connections, michael@0: "closing a connection " + this + " that we never added to the " + michael@0: "set of open connections?"); michael@0: NS_ASSERT(this._connections[connection.number] === connection, michael@0: "connection number mismatch? " + michael@0: this._connections[connection.number]); michael@0: delete this._connections[connection.number]; michael@0: michael@0: // Fire a pending server-stopped notification if it's our responsibility. michael@0: if (!this._hasOpenConnections() && this._socketClosed) michael@0: this._notifyStopped(); michael@0: }, michael@0: michael@0: /** michael@0: * Requests that the server be shut down when possible. michael@0: */ michael@0: _requestQuit: function() michael@0: { michael@0: dumpn(">>> requesting a quit"); michael@0: dumpStack(); michael@0: this._doQuit = true; michael@0: } michael@0: }; michael@0: michael@0: michael@0: // michael@0: // RFC 2396 section 3.2.2: michael@0: // michael@0: // host = hostname | IPv4address michael@0: // hostname = *( domainlabel "." ) toplabel [ "." ] michael@0: // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum michael@0: // toplabel = alpha | alpha *( alphanum | "-" ) alphanum michael@0: // IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit michael@0: // michael@0: michael@0: const HOST_REGEX = michael@0: new RegExp("^(?:" + michael@0: // *( domainlabel "." ) michael@0: "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" + michael@0: // toplabel michael@0: "[a-z](?:[a-z0-9-]*[a-z0-9])?" + michael@0: "|" + michael@0: // IPv4 address michael@0: "\\d+\\.\\d+\\.\\d+\\.\\d+" + michael@0: ")$", michael@0: "i"); michael@0: michael@0: michael@0: /** michael@0: * Represents the identity of a server. An identity consists of a set of michael@0: * (scheme, host, port) tuples denoted as locations (allowing a single server to michael@0: * serve multiple sites or to be used behind both HTTP and HTTPS proxies for any michael@0: * host/port). Any incoming request must be to one of these locations, or it michael@0: * will be rejected with an HTTP 400 error. One location, denoted as the michael@0: * primary location, is the location assigned in contexts where a location michael@0: * cannot otherwise be endogenously derived, such as for HTTP/1.0 requests. michael@0: * michael@0: * A single identity may contain at most one location per unique host/port pair; michael@0: * other than that, no restrictions are placed upon what locations may michael@0: * constitute an identity. michael@0: */ michael@0: function ServerIdentity() michael@0: { michael@0: /** The scheme of the primary location. */ michael@0: this._primaryScheme = "http"; michael@0: michael@0: /** The hostname of the primary location. */ michael@0: this._primaryHost = "127.0.0.1" michael@0: michael@0: /** The port number of the primary location. */ michael@0: this._primaryPort = -1; michael@0: michael@0: /** michael@0: * The current port number for the corresponding server, stored so that a new michael@0: * primary location can always be set if the current one is removed. michael@0: */ michael@0: this._defaultPort = -1; michael@0: michael@0: /** michael@0: * Maps hosts to maps of ports to schemes, e.g. the following would represent michael@0: * https://example.com:789/ and http://example.org/: michael@0: * michael@0: * { michael@0: * "xexample.com": { 789: "https" }, michael@0: * "xexample.org": { 80: "http" } michael@0: * } michael@0: * michael@0: * Note the "x" prefix on hostnames, which prevents collisions with special michael@0: * JS names like "prototype". michael@0: */ michael@0: this._locations = { "xlocalhost": {} }; michael@0: } michael@0: ServerIdentity.prototype = michael@0: { michael@0: // NSIHTTPSERVERIDENTITY michael@0: michael@0: // michael@0: // see nsIHttpServerIdentity.primaryScheme michael@0: // michael@0: get primaryScheme() michael@0: { michael@0: if (this._primaryPort === -1) michael@0: throw Cr.NS_ERROR_NOT_INITIALIZED; michael@0: return this._primaryScheme; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServerIdentity.primaryHost michael@0: // michael@0: get primaryHost() michael@0: { michael@0: if (this._primaryPort === -1) michael@0: throw Cr.NS_ERROR_NOT_INITIALIZED; michael@0: return this._primaryHost; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServerIdentity.primaryPort michael@0: // michael@0: get primaryPort() michael@0: { michael@0: if (this._primaryPort === -1) michael@0: throw Cr.NS_ERROR_NOT_INITIALIZED; michael@0: return this._primaryPort; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServerIdentity.add michael@0: // michael@0: add: function(scheme, host, port) michael@0: { michael@0: this._validate(scheme, host, port); michael@0: michael@0: var entry = this._locations["x" + host]; michael@0: if (!entry) michael@0: this._locations["x" + host] = entry = {}; michael@0: michael@0: entry[port] = scheme; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServerIdentity.remove michael@0: // michael@0: remove: function(scheme, host, port) michael@0: { michael@0: this._validate(scheme, host, port); michael@0: michael@0: var entry = this._locations["x" + host]; michael@0: if (!entry) michael@0: return false; michael@0: michael@0: var present = port in entry; michael@0: delete entry[port]; michael@0: michael@0: if (this._primaryScheme == scheme && michael@0: this._primaryHost == host && michael@0: this._primaryPort == port && michael@0: this._defaultPort !== -1) michael@0: { michael@0: // Always keep at least one identity in existence at any time, unless michael@0: // we're in the process of shutting down (the last condition above). michael@0: this._primaryPort = -1; michael@0: this._initialize(this._defaultPort, host, false); michael@0: } michael@0: michael@0: return present; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServerIdentity.has michael@0: // michael@0: has: function(scheme, host, port) michael@0: { michael@0: this._validate(scheme, host, port); michael@0: michael@0: return "x" + host in this._locations && michael@0: scheme === this._locations["x" + host][port]; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServerIdentity.has michael@0: // michael@0: getScheme: function(host, port) michael@0: { michael@0: this._validate("http", host, port); michael@0: michael@0: var entry = this._locations["x" + host]; michael@0: if (!entry) michael@0: return ""; michael@0: michael@0: return entry[port] || ""; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServerIdentity.setPrimary michael@0: // michael@0: setPrimary: function(scheme, host, port) michael@0: { michael@0: this._validate(scheme, host, port); michael@0: michael@0: this.add(scheme, host, port); michael@0: michael@0: this._primaryScheme = scheme; michael@0: this._primaryHost = host; michael@0: this._primaryPort = port; michael@0: }, michael@0: michael@0: michael@0: // NSISUPPORTS michael@0: michael@0: // michael@0: // see nsISupports.QueryInterface michael@0: // michael@0: QueryInterface: function(iid) michael@0: { michael@0: if (iid.equals(Ci.nsIHttpServerIdentity) || iid.equals(Ci.nsISupports)) michael@0: return this; michael@0: michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: michael@0: // PRIVATE IMPLEMENTATION michael@0: michael@0: /** michael@0: * Initializes the primary name for the corresponding server, based on the michael@0: * provided port number. michael@0: */ michael@0: _initialize: function(port, host, addSecondaryDefault) michael@0: { michael@0: this._host = host; michael@0: if (this._primaryPort !== -1) michael@0: this.add("http", host, port); michael@0: else michael@0: this.setPrimary("http", "localhost", port); michael@0: this._defaultPort = port; michael@0: michael@0: // Only add this if we're being called at server startup michael@0: if (addSecondaryDefault && host != "127.0.0.1") michael@0: this.add("http", "127.0.0.1", port); michael@0: }, michael@0: michael@0: /** michael@0: * Called at server shutdown time, unsets the primary location only if it was michael@0: * the default-assigned location and removes the default location from the michael@0: * set of locations used. michael@0: */ michael@0: _teardown: function() michael@0: { michael@0: if (this._host != "127.0.0.1") { michael@0: // Not the default primary location, nothing special to do here michael@0: this.remove("http", "127.0.0.1", this._defaultPort); michael@0: } michael@0: michael@0: // This is a *very* tricky bit of reasoning here; make absolutely sure the michael@0: // tests for this code pass before you commit changes to it. michael@0: if (this._primaryScheme == "http" && michael@0: this._primaryHost == this._host && michael@0: this._primaryPort == this._defaultPort) michael@0: { michael@0: // Make sure we don't trigger the readding logic in .remove(), then remove michael@0: // the default location. michael@0: var port = this._defaultPort; michael@0: this._defaultPort = -1; michael@0: this.remove("http", this._host, port); michael@0: michael@0: // Ensure a server start triggers the setPrimary() path in ._initialize() michael@0: this._primaryPort = -1; michael@0: } michael@0: else michael@0: { michael@0: // No reason not to remove directly as it's not our primary location michael@0: this.remove("http", this._host, this._defaultPort); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Ensures scheme, host, and port are all valid with respect to RFC 2396. michael@0: * michael@0: * @throws NS_ERROR_ILLEGAL_VALUE michael@0: * if any argument doesn't match the corresponding production michael@0: */ michael@0: _validate: function(scheme, host, port) michael@0: { michael@0: if (scheme !== "http" && scheme !== "https") michael@0: { michael@0: dumpn("*** server only supports http/https schemes: '" + scheme + "'"); michael@0: dumpStack(); michael@0: throw Cr.NS_ERROR_ILLEGAL_VALUE; michael@0: } michael@0: if (!HOST_REGEX.test(host)) michael@0: { michael@0: dumpn("*** unexpected host: '" + host + "'"); michael@0: throw Cr.NS_ERROR_ILLEGAL_VALUE; michael@0: } michael@0: if (port < 0 || port > 65535) michael@0: { michael@0: dumpn("*** unexpected port: '" + port + "'"); michael@0: throw Cr.NS_ERROR_ILLEGAL_VALUE; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Represents a connection to the server (and possibly in the future the thread michael@0: * on which the connection is processed). michael@0: * michael@0: * @param input : nsIInputStream michael@0: * stream from which incoming data on the connection is read michael@0: * @param output : nsIOutputStream michael@0: * stream to write data out the connection michael@0: * @param server : nsHttpServer michael@0: * the server handling the connection michael@0: * @param port : int michael@0: * the port on which the server is running michael@0: * @param outgoingPort : int michael@0: * the outgoing port used by this connection michael@0: * @param number : uint michael@0: * a serial number used to uniquely identify this connection michael@0: */ michael@0: function Connection(input, output, server, port, outgoingPort, number) michael@0: { michael@0: dumpn("*** opening new connection " + number + " on port " + outgoingPort); michael@0: michael@0: /** Stream of incoming data. */ michael@0: this.input = input; michael@0: michael@0: /** Stream for outgoing data. */ michael@0: this.output = output; michael@0: michael@0: /** The server associated with this request. */ michael@0: this.server = server; michael@0: michael@0: /** The port on which the server is running. */ michael@0: this.port = port; michael@0: michael@0: /** The outgoing poort used by this connection. */ michael@0: this._outgoingPort = outgoingPort; michael@0: michael@0: /** The serial number of this connection. */ michael@0: this.number = number; michael@0: michael@0: /** michael@0: * The request for which a response is being generated, null if the michael@0: * incoming request has not been fully received or if it had errors. michael@0: */ michael@0: this.request = null; michael@0: michael@0: /** State variables for debugging. */ michael@0: this._closed = this._processed = false; michael@0: } michael@0: Connection.prototype = michael@0: { michael@0: /** Closes this connection's input/output streams. */ michael@0: close: function() michael@0: { michael@0: dumpn("*** closing connection " + this.number + michael@0: " on port " + this._outgoingPort); michael@0: michael@0: this.input.close(); michael@0: this.output.close(); michael@0: this._closed = true; michael@0: michael@0: var server = this.server; michael@0: server._connectionClosed(this); michael@0: michael@0: // If an error triggered a server shutdown, act on it now michael@0: if (server._doQuit) michael@0: server.stop(function() { /* not like we can do anything better */ }); michael@0: }, michael@0: michael@0: /** michael@0: * Initiates processing of this connection, using the data in the given michael@0: * request. michael@0: * michael@0: * @param request : Request michael@0: * the request which should be processed michael@0: */ michael@0: process: function(request) michael@0: { michael@0: NS_ASSERT(!this._closed && !this._processed); michael@0: michael@0: this._processed = true; michael@0: michael@0: this.request = request; michael@0: this.server._handler.handleResponse(this); michael@0: }, michael@0: michael@0: /** michael@0: * Initiates processing of this connection, generating a response with the michael@0: * given HTTP error code. michael@0: * michael@0: * @param code : uint michael@0: * an HTTP code, so in the range [0, 1000) michael@0: * @param request : Request michael@0: * incomplete data about the incoming request (since there were errors michael@0: * during its processing michael@0: */ michael@0: processError: function(code, request) michael@0: { michael@0: NS_ASSERT(!this._closed && !this._processed); michael@0: michael@0: this._processed = true; michael@0: this.request = request; michael@0: this.server._handler.handleError(code, this); michael@0: }, michael@0: michael@0: /** Converts this to a string for debugging purposes. */ michael@0: toString: function() michael@0: { michael@0: return ""; michael@0: } michael@0: }; michael@0: michael@0: michael@0: michael@0: /** Returns an array of count bytes from the given input stream. */ michael@0: function readBytes(inputStream, count) michael@0: { michael@0: return new BinaryInputStream(inputStream).readByteArray(count); michael@0: } michael@0: michael@0: michael@0: michael@0: /** Request reader processing states; see RequestReader for details. */ michael@0: const READER_IN_REQUEST_LINE = 0; michael@0: const READER_IN_HEADERS = 1; michael@0: const READER_IN_BODY = 2; michael@0: const READER_FINISHED = 3; michael@0: michael@0: michael@0: /** michael@0: * Reads incoming request data asynchronously, does any necessary preprocessing, michael@0: * and forwards it to the request handler. Processing occurs in three states: michael@0: * michael@0: * READER_IN_REQUEST_LINE Reading the request's status line michael@0: * READER_IN_HEADERS Reading headers in the request michael@0: * READER_IN_BODY Reading the body of the request michael@0: * READER_FINISHED Entire request has been read and processed michael@0: * michael@0: * During the first two stages, initial metadata about the request is gathered michael@0: * into a Request object. Once the status line and headers have been processed, michael@0: * we start processing the body of the request into the Request. Finally, when michael@0: * the entire body has been read, we create a Response and hand it off to the michael@0: * ServerHandler to be given to the appropriate request handler. michael@0: * michael@0: * @param connection : Connection michael@0: * the connection for the request being read michael@0: */ michael@0: function RequestReader(connection) michael@0: { michael@0: /** Connection metadata for this request. */ michael@0: this._connection = connection; michael@0: michael@0: /** michael@0: * A container providing line-by-line access to the raw bytes that make up the michael@0: * data which has been read from the connection but has not yet been acted michael@0: * upon (by passing it to the request handler or by extracting request michael@0: * metadata from it). michael@0: */ michael@0: this._data = new LineData(); michael@0: michael@0: /** michael@0: * The amount of data remaining to be read from the body of this request. michael@0: * After all headers in the request have been read this is the value in the michael@0: * Content-Length header, but as the body is read its value decreases to zero. michael@0: */ michael@0: this._contentLength = 0; michael@0: michael@0: /** The current state of parsing the incoming request. */ michael@0: this._state = READER_IN_REQUEST_LINE; michael@0: michael@0: /** Metadata constructed from the incoming request for the request handler. */ michael@0: this._metadata = new Request(connection.port); michael@0: michael@0: /** michael@0: * Used to preserve state if we run out of line data midway through a michael@0: * multi-line header. _lastHeaderName stores the name of the header, while michael@0: * _lastHeaderValue stores the value we've seen so far for the header. michael@0: * michael@0: * These fields are always either both undefined or both strings. michael@0: */ michael@0: this._lastHeaderName = this._lastHeaderValue = undefined; michael@0: } michael@0: RequestReader.prototype = michael@0: { michael@0: // NSIINPUTSTREAMCALLBACK michael@0: michael@0: /** michael@0: * Called when more data from the incoming request is available. This method michael@0: * then reads the available data from input and deals with that data as michael@0: * necessary, depending upon the syntax of already-downloaded data. michael@0: * michael@0: * @param input : nsIAsyncInputStream michael@0: * the stream of incoming data from the connection michael@0: */ michael@0: onInputStreamReady: function(input) michael@0: { michael@0: dumpn("*** onInputStreamReady(input=" + input + ") on thread " + michael@0: gThreadManager.currentThread + " (main is " + michael@0: gThreadManager.mainThread + ")"); michael@0: dumpn("*** this._state == " + this._state); michael@0: michael@0: // Handle cases where we get more data after a request error has been michael@0: // discovered but *before* we can close the connection. michael@0: var data = this._data; michael@0: if (!data) michael@0: return; michael@0: michael@0: try michael@0: { michael@0: data.appendBytes(readBytes(input, input.available())); michael@0: } michael@0: catch (e) michael@0: { michael@0: if (streamClosed(e)) michael@0: { michael@0: dumpn("*** WARNING: unexpected error when reading from socket; will " + michael@0: "be treated as if the input stream had been closed"); michael@0: dumpn("*** WARNING: actual error was: " + e); michael@0: } michael@0: michael@0: // We've lost a race -- input has been closed, but we're still expecting michael@0: // to read more data. available() will throw in this case, and since michael@0: // we're dead in the water now, destroy the connection. michael@0: dumpn("*** onInputStreamReady called on a closed input, destroying " + michael@0: "connection"); michael@0: this._connection.close(); michael@0: return; michael@0: } michael@0: michael@0: switch (this._state) michael@0: { michael@0: default: michael@0: NS_ASSERT(false, "invalid state: " + this._state); michael@0: break; michael@0: michael@0: case READER_IN_REQUEST_LINE: michael@0: if (!this._processRequestLine()) michael@0: break; michael@0: /* fall through */ michael@0: michael@0: case READER_IN_HEADERS: michael@0: if (!this._processHeaders()) michael@0: break; michael@0: /* fall through */ michael@0: michael@0: case READER_IN_BODY: michael@0: this._processBody(); michael@0: } michael@0: michael@0: if (this._state != READER_FINISHED) michael@0: input.asyncWait(this, 0, 0, gThreadManager.currentThread); michael@0: }, michael@0: michael@0: // michael@0: // see nsISupports.QueryInterface michael@0: // michael@0: QueryInterface: function(aIID) michael@0: { michael@0: if (aIID.equals(Ci.nsIInputStreamCallback) || michael@0: aIID.equals(Ci.nsISupports)) michael@0: return this; michael@0: michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: michael@0: // PRIVATE API michael@0: michael@0: /** michael@0: * Processes unprocessed, downloaded data as a request line. michael@0: * michael@0: * @returns boolean michael@0: * true iff the request line has been fully processed michael@0: */ michael@0: _processRequestLine: function() michael@0: { michael@0: NS_ASSERT(this._state == READER_IN_REQUEST_LINE); michael@0: michael@0: // Servers SHOULD ignore any empty line(s) received where a Request-Line michael@0: // is expected (section 4.1). michael@0: var data = this._data; michael@0: var line = {}; michael@0: var readSuccess; michael@0: while ((readSuccess = data.readLine(line)) && line.value == "") michael@0: dumpn("*** ignoring beginning blank line..."); michael@0: michael@0: // if we don't have a full line, wait until we do michael@0: if (!readSuccess) michael@0: return false; michael@0: michael@0: // we have the first non-blank line michael@0: try michael@0: { michael@0: this._parseRequestLine(line.value); michael@0: this._state = READER_IN_HEADERS; michael@0: return true; michael@0: } michael@0: catch (e) michael@0: { michael@0: this._handleError(e); michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Processes stored data, assuming it is either at the beginning or in michael@0: * the middle of processing request headers. michael@0: * michael@0: * @returns boolean michael@0: * true iff header data in the request has been fully processed michael@0: */ michael@0: _processHeaders: function() michael@0: { michael@0: NS_ASSERT(this._state == READER_IN_HEADERS); michael@0: michael@0: // XXX things to fix here: michael@0: // michael@0: // - need to support RFC 2047-encoded non-US-ASCII characters michael@0: michael@0: try michael@0: { michael@0: var done = this._parseHeaders(); michael@0: if (done) michael@0: { michael@0: var request = this._metadata; michael@0: michael@0: // XXX this is wrong for requests with transfer-encodings applied to michael@0: // them, particularly chunked (which by its nature can have no michael@0: // meaningful Content-Length header)! michael@0: this._contentLength = request.hasHeader("Content-Length") michael@0: ? parseInt(request.getHeader("Content-Length"), 10) michael@0: : 0; michael@0: dumpn("_processHeaders, Content-length=" + this._contentLength); michael@0: michael@0: this._state = READER_IN_BODY; michael@0: } michael@0: return done; michael@0: } michael@0: catch (e) michael@0: { michael@0: this._handleError(e); michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Processes stored data, assuming it is either at the beginning or in michael@0: * the middle of processing the request body. michael@0: * michael@0: * @returns boolean michael@0: * true iff the request body has been fully processed michael@0: */ michael@0: _processBody: function() michael@0: { michael@0: NS_ASSERT(this._state == READER_IN_BODY); michael@0: michael@0: // XXX handle chunked transfer-coding request bodies! michael@0: michael@0: try michael@0: { michael@0: if (this._contentLength > 0) michael@0: { michael@0: var data = this._data.purge(); michael@0: var count = Math.min(data.length, this._contentLength); michael@0: dumpn("*** loading data=" + data + " len=" + data.length + michael@0: " excess=" + (data.length - count)); michael@0: michael@0: var bos = new BinaryOutputStream(this._metadata._bodyOutputStream); michael@0: bos.writeByteArray(data, count); michael@0: this._contentLength -= count; michael@0: } michael@0: michael@0: dumpn("*** remaining body data len=" + this._contentLength); michael@0: if (this._contentLength == 0) michael@0: { michael@0: this._validateRequest(); michael@0: this._state = READER_FINISHED; michael@0: this._handleResponse(); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: catch (e) michael@0: { michael@0: this._handleError(e); michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Does various post-header checks on the data in this request. michael@0: * michael@0: * @throws : HttpError michael@0: * if the request was malformed in some way michael@0: */ michael@0: _validateRequest: function() michael@0: { michael@0: NS_ASSERT(this._state == READER_IN_BODY); michael@0: michael@0: dumpn("*** _validateRequest"); michael@0: michael@0: var metadata = this._metadata; michael@0: var headers = metadata._headers; michael@0: michael@0: // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header michael@0: var identity = this._connection.server.identity; michael@0: if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) michael@0: { michael@0: if (!headers.hasHeader("Host")) michael@0: { michael@0: dumpn("*** malformed HTTP/1.1 or greater request with no Host header!"); michael@0: throw HTTP_400; michael@0: } michael@0: michael@0: // If the Request-URI wasn't absolute, then we need to determine our host. michael@0: // We have to determine what scheme was used to access us based on the michael@0: // server identity data at this point, because the request just doesn't michael@0: // contain enough data on its own to do this, sadly. michael@0: if (!metadata._host) michael@0: { michael@0: var host, port; michael@0: var hostPort = headers.getHeader("Host"); michael@0: var colon = hostPort.indexOf(":"); michael@0: if (colon < 0) michael@0: { michael@0: host = hostPort; michael@0: port = ""; michael@0: } michael@0: else michael@0: { michael@0: host = hostPort.substring(0, colon); michael@0: port = hostPort.substring(colon + 1); michael@0: } michael@0: michael@0: // NB: We allow an empty port here because, oddly, a colon may be michael@0: // present even without a port number, e.g. "example.com:"; in this michael@0: // case the default port applies. michael@0: if (!HOST_REGEX.test(host) || !/^\d*$/.test(port)) michael@0: { michael@0: dumpn("*** malformed hostname (" + hostPort + ") in Host " + michael@0: "header, 400 time"); michael@0: throw HTTP_400; michael@0: } michael@0: michael@0: // If we're not given a port, we're stuck, because we don't know what michael@0: // scheme to use to look up the correct port here, in general. Since michael@0: // the HTTPS case requires a tunnel/proxy and thus requires that the michael@0: // requested URI be absolute (and thus contain the necessary michael@0: // information), let's assume HTTP will prevail and use that. michael@0: port = +port || 80; michael@0: michael@0: var scheme = identity.getScheme(host, port); michael@0: if (!scheme) michael@0: { michael@0: dumpn("*** unrecognized hostname (" + hostPort + ") in Host " + michael@0: "header, 400 time"); michael@0: throw HTTP_400; michael@0: } michael@0: michael@0: metadata._scheme = scheme; michael@0: metadata._host = host; michael@0: metadata._port = port; michael@0: } michael@0: } michael@0: else michael@0: { michael@0: NS_ASSERT(metadata._host === undefined, michael@0: "HTTP/1.0 doesn't allow absolute paths in the request line!"); michael@0: michael@0: metadata._scheme = identity.primaryScheme; michael@0: metadata._host = identity.primaryHost; michael@0: metadata._port = identity.primaryPort; michael@0: } michael@0: michael@0: NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port), michael@0: "must have a location we recognize by now!"); michael@0: }, michael@0: michael@0: /** michael@0: * Handles responses in case of error, either in the server or in the request. michael@0: * michael@0: * @param e michael@0: * the specific error encountered, which is an HttpError in the case where michael@0: * the request is in some way invalid or cannot be fulfilled; if this isn't michael@0: * an HttpError we're going to be paranoid and shut down, because that michael@0: * shouldn't happen, ever michael@0: */ michael@0: _handleError: function(e) michael@0: { michael@0: // Don't fall back into normal processing! michael@0: this._state = READER_FINISHED; michael@0: michael@0: var server = this._connection.server; michael@0: if (e instanceof HttpError) michael@0: { michael@0: var code = e.code; michael@0: } michael@0: else michael@0: { michael@0: dumpn("!!! UNEXPECTED ERROR: " + e + michael@0: (e.lineNumber ? ", line " + e.lineNumber : "")); michael@0: michael@0: // no idea what happened -- be paranoid and shut down michael@0: code = 500; michael@0: server._requestQuit(); michael@0: } michael@0: michael@0: // make attempted reuse of data an error michael@0: this._data = null; michael@0: michael@0: this._connection.processError(code, this._metadata); michael@0: }, michael@0: michael@0: /** michael@0: * Now that we've read the request line and headers, we can actually hand off michael@0: * the request to be handled. michael@0: * michael@0: * This method is called once per request, after the request line and all michael@0: * headers and the body, if any, have been received. michael@0: */ michael@0: _handleResponse: function() michael@0: { michael@0: NS_ASSERT(this._state == READER_FINISHED); michael@0: michael@0: // We don't need the line-based data any more, so make attempted reuse an michael@0: // error. michael@0: this._data = null; michael@0: michael@0: this._connection.process(this._metadata); michael@0: }, michael@0: michael@0: michael@0: // PARSING michael@0: michael@0: /** michael@0: * Parses the request line for the HTTP request associated with this. michael@0: * michael@0: * @param line : string michael@0: * the request line michael@0: */ michael@0: _parseRequestLine: function(line) michael@0: { michael@0: NS_ASSERT(this._state == READER_IN_REQUEST_LINE); michael@0: michael@0: dumpn("*** _parseRequestLine('" + line + "')"); michael@0: michael@0: var metadata = this._metadata; michael@0: michael@0: // clients and servers SHOULD accept any amount of SP or HT characters michael@0: // between fields, even though only a single SP is required (section 19.3) michael@0: var request = line.split(/[ \t]+/); michael@0: if (!request || request.length != 3) michael@0: throw HTTP_400; michael@0: michael@0: metadata._method = request[0]; michael@0: michael@0: // get the HTTP version michael@0: var ver = request[2]; michael@0: var match = ver.match(/^HTTP\/(\d+\.\d+)$/); michael@0: if (!match) michael@0: throw HTTP_400; michael@0: michael@0: // determine HTTP version michael@0: try michael@0: { michael@0: metadata._httpVersion = new nsHttpVersion(match[1]); michael@0: if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0)) michael@0: throw "unsupported HTTP version"; michael@0: } michael@0: catch (e) michael@0: { michael@0: // we support HTTP/1.0 and HTTP/1.1 only michael@0: throw HTTP_501; michael@0: } michael@0: michael@0: michael@0: var fullPath = request[1]; michael@0: var serverIdentity = this._connection.server.identity; michael@0: michael@0: var scheme, host, port; michael@0: michael@0: if (fullPath.charAt(0) != "/") michael@0: { michael@0: // No absolute paths in the request line in HTTP prior to 1.1 michael@0: if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) michael@0: throw HTTP_400; michael@0: michael@0: try michael@0: { michael@0: var uri = Cc["@mozilla.org/network/io-service;1"] michael@0: .getService(Ci.nsIIOService) michael@0: .newURI(fullPath, null, null); michael@0: fullPath = uri.path; michael@0: scheme = uri.scheme; michael@0: host = metadata._host = uri.asciiHost; michael@0: port = uri.port; michael@0: if (port === -1) michael@0: { michael@0: if (scheme === "http") michael@0: port = 80; michael@0: else if (scheme === "https") michael@0: port = 443; michael@0: else michael@0: throw HTTP_400; michael@0: } michael@0: } michael@0: catch (e) michael@0: { michael@0: // If the host is not a valid host on the server, the response MUST be a michael@0: // 400 (Bad Request) error message (section 5.2). Alternately, the URI michael@0: // is malformed. michael@0: throw HTTP_400; michael@0: } michael@0: michael@0: if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/") michael@0: throw HTTP_400; michael@0: } michael@0: michael@0: var splitter = fullPath.indexOf("?"); michael@0: if (splitter < 0) michael@0: { michael@0: // _queryString already set in ctor michael@0: metadata._path = fullPath; michael@0: } michael@0: else michael@0: { michael@0: metadata._path = fullPath.substring(0, splitter); michael@0: metadata._queryString = fullPath.substring(splitter + 1); michael@0: } michael@0: michael@0: metadata._scheme = scheme; michael@0: metadata._host = host; michael@0: metadata._port = port; michael@0: }, michael@0: michael@0: /** michael@0: * Parses all available HTTP headers in this until the header-ending CRLFCRLF, michael@0: * adding them to the store of headers in the request. michael@0: * michael@0: * @throws michael@0: * HTTP_400 if the headers are malformed michael@0: * @returns boolean michael@0: * true if all headers have now been processed, false otherwise michael@0: */ michael@0: _parseHeaders: function() michael@0: { michael@0: NS_ASSERT(this._state == READER_IN_HEADERS); michael@0: michael@0: dumpn("*** _parseHeaders"); michael@0: michael@0: var data = this._data; michael@0: michael@0: var headers = this._metadata._headers; michael@0: var lastName = this._lastHeaderName; michael@0: var lastVal = this._lastHeaderValue; michael@0: michael@0: var line = {}; michael@0: while (true) michael@0: { michael@0: NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)), michael@0: lastName === undefined ? michael@0: "lastVal without lastName? lastVal: '" + lastVal + "'" : michael@0: "lastName without lastVal? lastName: '" + lastName + "'"); michael@0: michael@0: if (!data.readLine(line)) michael@0: { michael@0: // save any data we have from the header we might still be processing michael@0: this._lastHeaderName = lastName; michael@0: this._lastHeaderValue = lastVal; michael@0: return false; michael@0: } michael@0: michael@0: var lineText = line.value; michael@0: var firstChar = lineText.charAt(0); michael@0: michael@0: // blank line means end of headers michael@0: if (lineText == "") michael@0: { michael@0: // we're finished with the previous header michael@0: if (lastName) michael@0: { michael@0: try michael@0: { michael@0: headers.setHeader(lastName, lastVal, true); michael@0: } michael@0: catch (e) michael@0: { michael@0: dumpn("*** e == " + e); michael@0: throw HTTP_400; michael@0: } michael@0: } michael@0: else michael@0: { michael@0: // no headers in request -- valid for HTTP/1.0 requests michael@0: } michael@0: michael@0: // either way, we're done processing headers michael@0: this._state = READER_IN_BODY; michael@0: return true; michael@0: } michael@0: else if (firstChar == " " || firstChar == "\t") michael@0: { michael@0: // multi-line header if we've already seen a header line michael@0: if (!lastName) michael@0: { michael@0: // we don't have a header to continue! michael@0: throw HTTP_400; michael@0: } michael@0: michael@0: // append this line's text to the value; starts with SP/HT, so no need michael@0: // for separating whitespace michael@0: lastVal += lineText; michael@0: } michael@0: else michael@0: { michael@0: // we have a new header, so set the old one (if one existed) michael@0: if (lastName) michael@0: { michael@0: try michael@0: { michael@0: headers.setHeader(lastName, lastVal, true); michael@0: } michael@0: catch (e) michael@0: { michael@0: dumpn("*** e == " + e); michael@0: throw HTTP_400; michael@0: } michael@0: } michael@0: michael@0: var colon = lineText.indexOf(":"); // first colon must be splitter michael@0: if (colon < 1) michael@0: { michael@0: // no colon or missing header field-name michael@0: throw HTTP_400; michael@0: } michael@0: michael@0: // set header name, value (to be set in the next loop, usually) michael@0: lastName = lineText.substring(0, colon); michael@0: lastVal = lineText.substring(colon + 1); michael@0: } // empty, continuation, start of header michael@0: } // while (true) michael@0: } michael@0: }; michael@0: michael@0: michael@0: /** The character codes for CR and LF. */ michael@0: const CR = 0x0D, LF = 0x0A; michael@0: michael@0: /** michael@0: * Calculates the number of characters before the first CRLF pair in array, or michael@0: * -1 if the array contains no CRLF pair. michael@0: * michael@0: * @param array : Array michael@0: * an array of numbers in the range [0, 256), each representing a single michael@0: * character; the first CRLF is the lowest index i where michael@0: * |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|, michael@0: * if such an |i| exists, and -1 otherwise michael@0: * @returns int michael@0: * the index of the first CRLF if any were present, -1 otherwise michael@0: */ michael@0: function findCRLF(array) michael@0: { michael@0: for (var i = array.indexOf(CR); i >= 0; i = array.indexOf(CR, i + 1)) michael@0: { michael@0: if (array[i + 1] == LF) michael@0: return i; michael@0: } michael@0: return -1; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * A container which provides line-by-line access to the arrays of bytes with michael@0: * which it is seeded. michael@0: */ michael@0: function LineData() michael@0: { michael@0: /** An array of queued bytes from which to get line-based characters. */ michael@0: this._data = []; michael@0: } michael@0: LineData.prototype = michael@0: { michael@0: /** michael@0: * Appends the bytes in the given array to the internal data cache maintained michael@0: * by this. michael@0: */ michael@0: appendBytes: function(bytes) michael@0: { michael@0: Array.prototype.push.apply(this._data, bytes); michael@0: }, michael@0: michael@0: /** michael@0: * Removes and returns a line of data, delimited by CRLF, from this. michael@0: * michael@0: * @param out michael@0: * an object whose "value" property will be set to the first line of text michael@0: * present in this, sans CRLF, if this contains a full CRLF-delimited line michael@0: * of text; if this doesn't contain enough data, the value of the property michael@0: * is undefined michael@0: * @returns boolean michael@0: * true if a full line of data could be read from the data in this, false michael@0: * otherwise michael@0: */ michael@0: readLine: function(out) michael@0: { michael@0: var data = this._data; michael@0: var length = findCRLF(data); michael@0: if (length < 0) michael@0: return false; michael@0: michael@0: // michael@0: // We have the index of the CR, so remove all the characters, including michael@0: // CRLF, from the array with splice, and convert the removed array into the michael@0: // corresponding string, from which we then strip the trailing CRLF. michael@0: // michael@0: // Getting the line in this matter acknowledges that substring is an O(1) michael@0: // operation in SpiderMonkey because strings are immutable, whereas two michael@0: // splices, both from the beginning of the data, are less likely to be as michael@0: // cheap as a single splice plus two extra character conversions. michael@0: // michael@0: var line = String.fromCharCode.apply(null, data.splice(0, length + 2)); michael@0: out.value = line.substring(0, length); michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Removes the bytes currently within this and returns them in an array. michael@0: * michael@0: * @returns Array michael@0: * the bytes within this when this method is called michael@0: */ michael@0: purge: function() michael@0: { michael@0: var data = this._data; michael@0: this._data = []; michael@0: return data; michael@0: } michael@0: }; michael@0: michael@0: michael@0: michael@0: /** michael@0: * Creates a request-handling function for an nsIHttpRequestHandler object. michael@0: */ michael@0: function createHandlerFunc(handler) michael@0: { michael@0: return function(metadata, response) { handler.handle(metadata, response); }; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * The default handler for directories; writes an HTML response containing a michael@0: * slightly-formatted directory listing. michael@0: */ michael@0: function defaultIndexHandler(metadata, response) michael@0: { michael@0: response.setHeader("Content-Type", "text/html", false); michael@0: michael@0: var path = htmlEscape(decodeURI(metadata.path)); michael@0: michael@0: // michael@0: // Just do a very basic bit of directory listings -- no need for too much michael@0: // fanciness, especially since we don't have a style sheet in which we can michael@0: // stick rules (don't want to pollute the default path-space). michael@0: // michael@0: michael@0: var body = '\ michael@0: \ michael@0: ' + path + '\ michael@0: \ michael@0: \ michael@0:

' + path + '

\ michael@0:
    '; michael@0: michael@0: var directory = metadata.getProperty("directory").QueryInterface(Ci.nsILocalFile); michael@0: NS_ASSERT(directory && directory.isDirectory()); michael@0: michael@0: var fileList = []; michael@0: var files = directory.directoryEntries; michael@0: while (files.hasMoreElements()) michael@0: { michael@0: var f = files.getNext().QueryInterface(Ci.nsIFile); michael@0: var name = f.leafName; michael@0: if (!f.isHidden() && michael@0: (name.charAt(name.length - 1) != HIDDEN_CHAR || michael@0: name.charAt(name.length - 2) == HIDDEN_CHAR)) michael@0: fileList.push(f); michael@0: } michael@0: michael@0: fileList.sort(fileSort); michael@0: michael@0: for (var i = 0; i < fileList.length; i++) michael@0: { michael@0: var file = fileList[i]; michael@0: try michael@0: { michael@0: var name = file.leafName; michael@0: if (name.charAt(name.length - 1) == HIDDEN_CHAR) michael@0: name = name.substring(0, name.length - 1); michael@0: var sep = file.isDirectory() ? "/" : ""; michael@0: michael@0: // Note: using " to delimit the attribute here because encodeURIComponent michael@0: // passes through '. michael@0: var item = '
  1. ' + michael@0: htmlEscape(name) + sep + michael@0: '
  2. '; michael@0: michael@0: body += item; michael@0: } michael@0: catch (e) { /* some file system error, ignore the file */ } michael@0: } michael@0: michael@0: body += '
\ michael@0: \ michael@0: '; michael@0: michael@0: response.bodyOutputStream.write(body, body.length); michael@0: } michael@0: michael@0: /** michael@0: * Sorts a and b (nsIFile objects) into an aesthetically pleasing order. michael@0: */ michael@0: function fileSort(a, b) michael@0: { michael@0: var dira = a.isDirectory(), dirb = b.isDirectory(); michael@0: michael@0: if (dira && !dirb) michael@0: return -1; michael@0: if (dirb && !dira) michael@0: return 1; michael@0: michael@0: var namea = a.leafName.toLowerCase(), nameb = b.leafName.toLowerCase(); michael@0: return nameb > namea ? -1 : 1; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Converts an externally-provided path into an internal path for use in michael@0: * determining file mappings. michael@0: * michael@0: * @param path michael@0: * the path to convert michael@0: * @param encoded michael@0: * true if the given path should be passed through decodeURI prior to michael@0: * conversion michael@0: * @throws URIError michael@0: * if path is incorrectly encoded michael@0: */ michael@0: function toInternalPath(path, encoded) michael@0: { michael@0: if (encoded) michael@0: path = decodeURI(path); michael@0: michael@0: var comps = path.split("/"); michael@0: for (var i = 0, sz = comps.length; i < sz; i++) michael@0: { michael@0: var comp = comps[i]; michael@0: if (comp.charAt(comp.length - 1) == HIDDEN_CHAR) michael@0: comps[i] = comp + HIDDEN_CHAR; michael@0: } michael@0: return comps.join("/"); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Adds custom-specified headers for the given file to the given response, if michael@0: * any such headers are specified. michael@0: * michael@0: * @param file michael@0: * the file on the disk which is to be written michael@0: * @param metadata michael@0: * metadata about the incoming request michael@0: * @param response michael@0: * the Response to which any specified headers/data should be written michael@0: * @throws HTTP_500 michael@0: * if an error occurred while processing custom-specified headers michael@0: */ michael@0: function maybeAddHeaders(file, metadata, response) michael@0: { michael@0: var name = file.leafName; michael@0: if (name.charAt(name.length - 1) == HIDDEN_CHAR) michael@0: name = name.substring(0, name.length - 1); michael@0: michael@0: var headerFile = file.parent; michael@0: headerFile.append(name + HEADERS_SUFFIX); michael@0: michael@0: if (!headerFile.exists()) michael@0: return; michael@0: michael@0: const PR_RDONLY = 0x01; michael@0: var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8), michael@0: Ci.nsIFileInputStream.CLOSE_ON_EOF); michael@0: michael@0: try michael@0: { michael@0: var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); michael@0: lis.QueryInterface(Ci.nsIUnicharLineInputStream); michael@0: michael@0: var line = {value: ""}; michael@0: var more = lis.readLine(line); michael@0: michael@0: if (!more && line.value == "") michael@0: return; michael@0: michael@0: michael@0: // request line michael@0: michael@0: var status = line.value; michael@0: if (status.indexOf("HTTP ") == 0) michael@0: { michael@0: status = status.substring(5); michael@0: var space = status.indexOf(" "); michael@0: var code, description; michael@0: if (space < 0) michael@0: { michael@0: code = status; michael@0: description = ""; michael@0: } michael@0: else michael@0: { michael@0: code = status.substring(0, space); michael@0: description = status.substring(space + 1, status.length); michael@0: } michael@0: michael@0: response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description); michael@0: michael@0: line.value = ""; michael@0: more = lis.readLine(line); michael@0: } michael@0: michael@0: // headers michael@0: while (more || line.value != "") michael@0: { michael@0: var header = line.value; michael@0: var colon = header.indexOf(":"); michael@0: michael@0: response.setHeader(header.substring(0, colon), michael@0: header.substring(colon + 1, header.length), michael@0: false); // allow overriding server-set headers michael@0: michael@0: line.value = ""; michael@0: more = lis.readLine(line); michael@0: } michael@0: } michael@0: catch (e) michael@0: { michael@0: dumpn("WARNING: error in headers for " + metadata.path + ": " + e); michael@0: throw HTTP_500; michael@0: } michael@0: finally michael@0: { michael@0: fis.close(); michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * An object which handles requests for a server, executing default and michael@0: * overridden behaviors as instructed by the code which uses and manipulates it. michael@0: * Default behavior includes the paths / and /trace (diagnostics), with some michael@0: * support for HTTP error pages for various codes and fallback to HTTP 500 if michael@0: * those codes fail for any reason. michael@0: * michael@0: * @param server : nsHttpServer michael@0: * the server in which this handler is being used michael@0: */ michael@0: function ServerHandler(server) michael@0: { michael@0: // FIELDS michael@0: michael@0: /** michael@0: * The nsHttpServer instance associated with this handler. michael@0: */ michael@0: this._server = server; michael@0: michael@0: /** michael@0: * A FileMap object containing the set of path->nsILocalFile mappings for michael@0: * all directory mappings set in the server (e.g., "/" for /var/www/html/, michael@0: * "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2). michael@0: * michael@0: * Note carefully: the leading and trailing "/" in each path (not file) are michael@0: * removed before insertion to simplify the code which uses this. You have michael@0: * been warned! michael@0: */ michael@0: this._pathDirectoryMap = new FileMap(); michael@0: michael@0: /** michael@0: * Custom request handlers for the server in which this resides. Path-handler michael@0: * pairs are stored as property-value pairs in this property. michael@0: * michael@0: * @see ServerHandler.prototype._defaultPaths michael@0: */ michael@0: this._overridePaths = {}; michael@0: michael@0: /** michael@0: * Custom request handlers for the server in which this resides. Prefix-handler michael@0: * pairs are stored as property-value pairs in this property. michael@0: */ michael@0: this._overridePrefixes = {}; michael@0: michael@0: /** michael@0: * Custom request handlers for the error handlers in the server in which this michael@0: * resides. Path-handler pairs are stored as property-value pairs in this michael@0: * property. michael@0: * michael@0: * @see ServerHandler.prototype._defaultErrors michael@0: */ michael@0: this._overrideErrors = {}; michael@0: michael@0: /** michael@0: * Maps file extensions to their MIME types in the server, overriding any michael@0: * mapping that might or might not exist in the MIME service. michael@0: */ michael@0: this._mimeMappings = {}; michael@0: michael@0: /** michael@0: * The default handler for requests for directories, used to serve directories michael@0: * when no index file is present. michael@0: */ michael@0: this._indexHandler = defaultIndexHandler; michael@0: michael@0: /** Per-path state storage for the server. */ michael@0: this._state = {}; michael@0: michael@0: /** Entire-server state storage. */ michael@0: this._sharedState = {}; michael@0: michael@0: /** Entire-server state storage for nsISupports values. */ michael@0: this._objectState = {}; michael@0: } michael@0: ServerHandler.prototype = michael@0: { michael@0: // PUBLIC API michael@0: michael@0: /** michael@0: * Handles a request to this server, responding to the request appropriately michael@0: * and initiating server shutdown if necessary. michael@0: * michael@0: * This method never throws an exception. michael@0: * michael@0: * @param connection : Connection michael@0: * the connection for this request michael@0: */ michael@0: handleResponse: function(connection) michael@0: { michael@0: var request = connection.request; michael@0: var response = new Response(connection); michael@0: michael@0: var path = request.path; michael@0: dumpn("*** path == " + path); michael@0: michael@0: try michael@0: { michael@0: try michael@0: { michael@0: if (path in this._overridePaths) michael@0: { michael@0: // explicit paths first, then files based on existing directory mappings, michael@0: // then (if the file doesn't exist) built-in server default paths michael@0: dumpn("calling override for " + path); michael@0: this._overridePaths[path](request, response); michael@0: } michael@0: else michael@0: { michael@0: let longestPrefix = ""; michael@0: for (let prefix in this._overridePrefixes) michael@0: { michael@0: if (prefix.length > longestPrefix.length && path.startsWith(prefix)) michael@0: { michael@0: longestPrefix = prefix; michael@0: } michael@0: } michael@0: if (longestPrefix.length > 0) michael@0: { michael@0: dumpn("calling prefix override for " + longestPrefix); michael@0: this._overridePrefixes[longestPrefix](request, response); michael@0: } michael@0: else michael@0: { michael@0: this._handleDefault(request, response); michael@0: } michael@0: } michael@0: } michael@0: catch (e) michael@0: { michael@0: if (response.partiallySent()) michael@0: { michael@0: response.abort(e); michael@0: return; michael@0: } michael@0: michael@0: if (!(e instanceof HttpError)) michael@0: { michael@0: dumpn("*** unexpected error: e == " + e); michael@0: throw HTTP_500; michael@0: } michael@0: if (e.code !== 404) michael@0: throw e; michael@0: michael@0: dumpn("*** default: " + (path in this._defaultPaths)); michael@0: michael@0: response = new Response(connection); michael@0: if (path in this._defaultPaths) michael@0: this._defaultPaths[path](request, response); michael@0: else michael@0: throw HTTP_404; michael@0: } michael@0: } michael@0: catch (e) michael@0: { michael@0: if (response.partiallySent()) michael@0: { michael@0: response.abort(e); michael@0: return; michael@0: } michael@0: michael@0: var errorCode = "internal"; michael@0: michael@0: try michael@0: { michael@0: if (!(e instanceof HttpError)) michael@0: throw e; michael@0: michael@0: errorCode = e.code; michael@0: dumpn("*** errorCode == " + errorCode); michael@0: michael@0: response = new Response(connection); michael@0: if (e.customErrorHandling) michael@0: e.customErrorHandling(response); michael@0: this._handleError(errorCode, request, response); michael@0: return; michael@0: } michael@0: catch (e2) michael@0: { michael@0: dumpn("*** error handling " + errorCode + " error: " + michael@0: "e2 == " + e2 + ", shutting down server"); michael@0: michael@0: connection.server._requestQuit(); michael@0: response.abort(e2); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: response.complete(); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.registerFile michael@0: // michael@0: registerFile: function(path, file) michael@0: { michael@0: if (!file) michael@0: { michael@0: dumpn("*** unregistering '" + path + "' mapping"); michael@0: delete this._overridePaths[path]; michael@0: return; michael@0: } michael@0: michael@0: dumpn("*** registering '" + path + "' as mapping to " + file.path); michael@0: file = file.clone(); michael@0: michael@0: var self = this; michael@0: this._overridePaths[path] = michael@0: function(request, response) michael@0: { michael@0: if (!file.exists()) michael@0: throw HTTP_404; michael@0: michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: self._writeFileResponse(request, file, response, 0, file.fileSize); michael@0: }; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.registerPathHandler michael@0: // michael@0: registerPathHandler: function(path, handler) michael@0: { michael@0: // XXX true path validation! michael@0: if (path.charAt(0) != "/") michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: this._handlerToField(handler, this._overridePaths, path); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.registerPrefixHandler michael@0: // michael@0: registerPrefixHandler: function(prefix, handler) michael@0: { michael@0: // XXX true prefix validation! michael@0: if (!(prefix.startsWith("/") && prefix.endsWith("/"))) michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: this._handlerToField(handler, this._overridePrefixes, prefix); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.registerDirectory michael@0: // michael@0: registerDirectory: function(path, directory) michael@0: { michael@0: // strip off leading and trailing '/' so that we can use lastIndexOf when michael@0: // determining exactly how a path maps onto a mapped directory -- michael@0: // conditional is required here to deal with "/".substring(1, 0) being michael@0: // converted to "/".substring(0, 1) per the JS specification michael@0: var key = path.length == 1 ? "" : path.substring(1, path.length - 1); michael@0: michael@0: // the path-to-directory mapping code requires that the first character not michael@0: // be "/", or it will go into an infinite loop michael@0: if (key.charAt(0) == "/") michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: key = toInternalPath(key, false); michael@0: michael@0: if (directory) michael@0: { michael@0: dumpn("*** mapping '" + path + "' to the location " + directory.path); michael@0: this._pathDirectoryMap.put(key, directory); michael@0: } michael@0: else michael@0: { michael@0: dumpn("*** removing mapping for '" + path + "'"); michael@0: this._pathDirectoryMap.put(key, null); michael@0: } michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.registerErrorHandler michael@0: // michael@0: registerErrorHandler: function(err, handler) michael@0: { michael@0: if (!(err in HTTP_ERROR_CODES)) michael@0: dumpn("*** WARNING: registering non-HTTP/1.1 error code " + michael@0: "(" + err + ") handler -- was this intentional?"); michael@0: michael@0: this._handlerToField(handler, this._overrideErrors, err); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.setIndexHandler michael@0: // michael@0: setIndexHandler: function(handler) michael@0: { michael@0: if (!handler) michael@0: handler = defaultIndexHandler; michael@0: else if (typeof(handler) != "function") michael@0: handler = createHandlerFunc(handler); michael@0: michael@0: this._indexHandler = handler; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpServer.registerContentType michael@0: // michael@0: registerContentType: function(ext, type) michael@0: { michael@0: if (!type) michael@0: delete this._mimeMappings[ext]; michael@0: else michael@0: this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type); michael@0: }, michael@0: michael@0: // PRIVATE API michael@0: michael@0: /** michael@0: * Sets or remove (if handler is null) a handler in an object with a key. michael@0: * michael@0: * @param handler michael@0: * a handler, either function or an nsIHttpRequestHandler michael@0: * @param dict michael@0: * The object to attach the handler to. michael@0: * @param key michael@0: * The field name of the handler. michael@0: */ michael@0: _handlerToField: function(handler, dict, key) michael@0: { michael@0: // for convenience, handler can be a function if this is run from xpcshell michael@0: if (typeof(handler) == "function") michael@0: dict[key] = handler; michael@0: else if (handler) michael@0: dict[key] = createHandlerFunc(handler); michael@0: else michael@0: delete dict[key]; michael@0: }, michael@0: michael@0: /** michael@0: * Handles a request which maps to a file in the local filesystem (if a base michael@0: * path has already been set; otherwise the 404 error is thrown). michael@0: * michael@0: * @param metadata : Request michael@0: * metadata for the incoming request michael@0: * @param response : Response michael@0: * an uninitialized Response to the given request, to be initialized by a michael@0: * request handler michael@0: * @throws HTTP_### michael@0: * if an HTTP error occurred (usually HTTP_404); note that in this case the michael@0: * calling code must handle post-processing of the response michael@0: */ michael@0: _handleDefault: function(metadata, response) michael@0: { michael@0: dumpn("*** _handleDefault()"); michael@0: michael@0: response.setStatusLine(metadata.httpVersion, 200, "OK"); michael@0: michael@0: var path = metadata.path; michael@0: NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">"); michael@0: michael@0: // determine the actual on-disk file; this requires finding the deepest michael@0: // path-to-directory mapping in the requested URL michael@0: var file = this._getFileForPath(path); michael@0: michael@0: // the "file" might be a directory, in which case we either serve the michael@0: // contained index.html or make the index handler write the response michael@0: if (file.exists() && file.isDirectory()) michael@0: { michael@0: file.append("index.html"); // make configurable? michael@0: if (!file.exists() || file.isDirectory()) michael@0: { michael@0: metadata._ensurePropertyBag(); michael@0: metadata._bag.setPropertyAsInterface("directory", file.parent); michael@0: this._indexHandler(metadata, response); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // alternately, the file might not exist michael@0: if (!file.exists()) michael@0: throw HTTP_404; michael@0: michael@0: var start, end; michael@0: if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) && michael@0: metadata.hasHeader("Range") && michael@0: this._getTypeFromFile(file) !== SJS_TYPE) michael@0: { michael@0: var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/); michael@0: if (!rangeMatch) michael@0: throw HTTP_400; michael@0: michael@0: if (rangeMatch[1] !== undefined) michael@0: start = parseInt(rangeMatch[1], 10); michael@0: michael@0: if (rangeMatch[2] !== undefined) michael@0: end = parseInt(rangeMatch[2], 10); michael@0: michael@0: if (start === undefined && end === undefined) michael@0: throw HTTP_400; michael@0: michael@0: // No start given, so the end is really the count of bytes from the michael@0: // end of the file. michael@0: if (start === undefined) michael@0: { michael@0: start = Math.max(0, file.fileSize - end); michael@0: end = file.fileSize - 1; michael@0: } michael@0: michael@0: // start and end are inclusive michael@0: if (end === undefined || end >= file.fileSize) michael@0: end = file.fileSize - 1; michael@0: michael@0: if (start !== undefined && start >= file.fileSize) { michael@0: var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable"); michael@0: HTTP_416.customErrorHandling = function(errorResponse) michael@0: { michael@0: maybeAddHeaders(file, metadata, errorResponse); michael@0: }; michael@0: throw HTTP_416; michael@0: } michael@0: michael@0: if (end < start) michael@0: { michael@0: response.setStatusLine(metadata.httpVersion, 200, "OK"); michael@0: start = 0; michael@0: end = file.fileSize - 1; michael@0: } michael@0: else michael@0: { michael@0: response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); michael@0: var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize; michael@0: response.setHeader("Content-Range", contentRange); michael@0: } michael@0: } michael@0: else michael@0: { michael@0: start = 0; michael@0: end = file.fileSize - 1; michael@0: } michael@0: michael@0: // finally... michael@0: dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " + michael@0: start + " to " + end + " inclusive"); michael@0: this._writeFileResponse(metadata, file, response, start, end - start + 1); michael@0: }, michael@0: michael@0: /** michael@0: * Writes an HTTP response for the given file, including setting headers for michael@0: * file metadata. michael@0: * michael@0: * @param metadata : Request michael@0: * the Request for which a response is being generated michael@0: * @param file : nsILocalFile michael@0: * the file which is to be sent in the response michael@0: * @param response : Response michael@0: * the response to which the file should be written michael@0: * @param offset: uint michael@0: * the byte offset to skip to when writing michael@0: * @param count: uint michael@0: * the number of bytes to write michael@0: */ michael@0: _writeFileResponse: function(metadata, file, response, offset, count) michael@0: { michael@0: const PR_RDONLY = 0x01; michael@0: michael@0: var type = this._getTypeFromFile(file); michael@0: if (type === SJS_TYPE) michael@0: { michael@0: var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8), michael@0: Ci.nsIFileInputStream.CLOSE_ON_EOF); michael@0: michael@0: try michael@0: { michael@0: var sis = new ScriptableInputStream(fis); michael@0: var s = Cu.Sandbox(gGlobalObject); michael@0: s.importFunction(dump, "dump"); michael@0: michael@0: // Define a basic key-value state-preservation API across requests, with michael@0: // keys initially corresponding to the empty string. michael@0: var self = this; michael@0: var path = metadata.path; michael@0: s.importFunction(function getState(k) michael@0: { michael@0: return self._getState(path, k); michael@0: }); michael@0: s.importFunction(function setState(k, v) michael@0: { michael@0: self._setState(path, k, v); michael@0: }); michael@0: s.importFunction(function getSharedState(k) michael@0: { michael@0: return self._getSharedState(k); michael@0: }); michael@0: s.importFunction(function setSharedState(k, v) michael@0: { michael@0: self._setSharedState(k, v); michael@0: }); michael@0: s.importFunction(function getObjectState(k, callback) michael@0: { michael@0: callback(self._getObjectState(k)); michael@0: }); michael@0: s.importFunction(function setObjectState(k, v) michael@0: { michael@0: self._setObjectState(k, v); michael@0: }); michael@0: s.importFunction(function registerPathHandler(p, h) michael@0: { michael@0: self.registerPathHandler(p, h); michael@0: }); michael@0: michael@0: // Make it possible for sjs files to access their location michael@0: this._setState(path, "__LOCATION__", file.path); michael@0: michael@0: try michael@0: { michael@0: // Alas, the line number in errors dumped to console when calling the michael@0: // request handler is simply an offset from where we load the SJS file. michael@0: // Work around this in a reasonably non-fragile way by dynamically michael@0: // getting the line number where we evaluate the SJS file. Don't michael@0: // separate these two lines! michael@0: var line = new Error().lineNumber; michael@0: Cu.evalInSandbox(sis.read(file.fileSize), s); michael@0: } michael@0: catch (e) michael@0: { michael@0: dumpn("*** syntax error in SJS at " + file.path + ": " + e); michael@0: throw HTTP_500; michael@0: } michael@0: michael@0: try michael@0: { michael@0: s.handleRequest(metadata, response); michael@0: } michael@0: catch (e) michael@0: { michael@0: dump("*** error running SJS at " + file.path + ": " + michael@0: e + " on line " + michael@0: (e instanceof Error michael@0: ? e.lineNumber + " in httpd.js" michael@0: : (e.lineNumber - line)) + "\n"); michael@0: throw HTTP_500; michael@0: } michael@0: } michael@0: finally michael@0: { michael@0: fis.close(); michael@0: } michael@0: } michael@0: else michael@0: { michael@0: try michael@0: { michael@0: response.setHeader("Last-Modified", michael@0: toDateString(file.lastModifiedTime), michael@0: false); michael@0: } michael@0: catch (e) { /* lastModifiedTime threw, ignore */ } michael@0: michael@0: response.setHeader("Content-Type", type, false); michael@0: maybeAddHeaders(file, metadata, response); michael@0: response.setHeader("Content-Length", "" + count, false); michael@0: michael@0: var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8), michael@0: Ci.nsIFileInputStream.CLOSE_ON_EOF); michael@0: michael@0: offset = offset || 0; michael@0: count = count || file.fileSize; michael@0: NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset"); michael@0: NS_ASSERT(count >= 0, "bad count"); michael@0: NS_ASSERT(offset + count <= file.fileSize, "bad total data size"); michael@0: michael@0: try michael@0: { michael@0: if (offset !== 0) michael@0: { michael@0: // Seek (or read, if seeking isn't supported) to the correct offset so michael@0: // the data sent to the client matches the requested range. michael@0: if (fis instanceof Ci.nsISeekableStream) michael@0: fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset); michael@0: else michael@0: new ScriptableInputStream(fis).read(offset); michael@0: } michael@0: } michael@0: catch (e) michael@0: { michael@0: fis.close(); michael@0: throw e; michael@0: } michael@0: michael@0: let writeMore = function writeMore() michael@0: { michael@0: gThreadManager.currentThread michael@0: .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL); michael@0: } michael@0: michael@0: var input = new BinaryInputStream(fis); michael@0: var output = new BinaryOutputStream(response.bodyOutputStream); michael@0: var writeData = michael@0: { michael@0: run: function() michael@0: { michael@0: var chunkSize = Math.min(65536, count); michael@0: count -= chunkSize; michael@0: NS_ASSERT(count >= 0, "underflow"); michael@0: michael@0: try michael@0: { michael@0: var data = input.readByteArray(chunkSize); michael@0: NS_ASSERT(data.length === chunkSize, michael@0: "incorrect data returned? got " + data.length + michael@0: ", expected " + chunkSize); michael@0: output.writeByteArray(data, data.length); michael@0: if (count === 0) michael@0: { michael@0: fis.close(); michael@0: response.finish(); michael@0: } michael@0: else michael@0: { michael@0: writeMore(); michael@0: } michael@0: } michael@0: catch (e) michael@0: { michael@0: try michael@0: { michael@0: fis.close(); michael@0: } michael@0: finally michael@0: { michael@0: response.finish(); michael@0: } michael@0: throw e; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: writeMore(); michael@0: michael@0: // Now that we know copying will start, flag the response as async. michael@0: response.processAsync(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Get the value corresponding to a given key for the given path for SJS state michael@0: * preservation across requests. michael@0: * michael@0: * @param path : string michael@0: * the path from which the given state is to be retrieved michael@0: * @param k : string michael@0: * the key whose corresponding value is to be returned michael@0: * @returns string michael@0: * the corresponding value, which is initially the empty string michael@0: */ michael@0: _getState: function(path, k) michael@0: { michael@0: var state = this._state; michael@0: if (path in state && k in state[path]) michael@0: return state[path][k]; michael@0: return ""; michael@0: }, michael@0: michael@0: /** michael@0: * Set the value corresponding to a given key for the given path for SJS state michael@0: * preservation across requests. michael@0: * michael@0: * @param path : string michael@0: * the path from which the given state is to be retrieved michael@0: * @param k : string michael@0: * the key whose corresponding value is to be set michael@0: * @param v : string michael@0: * the value to be set michael@0: */ michael@0: _setState: function(path, k, v) michael@0: { michael@0: if (typeof v !== "string") michael@0: throw new Error("non-string value passed"); michael@0: var state = this._state; michael@0: if (!(path in state)) michael@0: state[path] = {}; michael@0: state[path][k] = v; michael@0: }, michael@0: michael@0: /** michael@0: * Get the value corresponding to a given key for SJS state preservation michael@0: * across requests. michael@0: * michael@0: * @param k : string michael@0: * the key whose corresponding value is to be returned michael@0: * @returns string michael@0: * the corresponding value, which is initially the empty string michael@0: */ michael@0: _getSharedState: function(k) michael@0: { michael@0: var state = this._sharedState; michael@0: if (k in state) michael@0: return state[k]; michael@0: return ""; michael@0: }, michael@0: michael@0: /** michael@0: * Set the value corresponding to a given key for SJS state preservation michael@0: * across requests. michael@0: * michael@0: * @param k : string michael@0: * the key whose corresponding value is to be set michael@0: * @param v : string michael@0: * the value to be set michael@0: */ michael@0: _setSharedState: function(k, v) michael@0: { michael@0: if (typeof v !== "string") michael@0: throw new Error("non-string value passed"); michael@0: this._sharedState[k] = v; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the object associated with the given key in the server for SJS michael@0: * state preservation across requests. michael@0: * michael@0: * @param k : string michael@0: * the key whose corresponding object is to be returned michael@0: * @returns nsISupports michael@0: * the corresponding object, or null if none was present michael@0: */ michael@0: _getObjectState: function(k) michael@0: { michael@0: if (typeof k !== "string") michael@0: throw new Error("non-string key passed"); michael@0: return this._objectState[k] || null; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the object associated with the given key in the server for SJS michael@0: * state preservation across requests. michael@0: * michael@0: * @param k : string michael@0: * the key whose corresponding object is to be set michael@0: * @param v : nsISupports michael@0: * the object to be associated with the given key; may be null michael@0: */ michael@0: _setObjectState: function(k, v) michael@0: { michael@0: if (typeof k !== "string") michael@0: throw new Error("non-string key passed"); michael@0: if (typeof v !== "object") michael@0: throw new Error("non-object value passed"); michael@0: if (v && !("QueryInterface" in v)) michael@0: { michael@0: throw new Error("must pass an nsISupports; use wrappedJSObject to ease " + michael@0: "pain when using the server from JS"); michael@0: } michael@0: michael@0: this._objectState[k] = v; michael@0: }, michael@0: michael@0: /** michael@0: * Gets a content-type for the given file, first by checking for any custom michael@0: * MIME-types registered with this handler for the file's extension, second by michael@0: * asking the global MIME service for a content-type, and finally by failing michael@0: * over to application/octet-stream. michael@0: * michael@0: * @param file : nsIFile michael@0: * the nsIFile for which to get a file type michael@0: * @returns string michael@0: * the best content-type which can be determined for the file michael@0: */ michael@0: _getTypeFromFile: function(file) michael@0: { michael@0: try michael@0: { michael@0: var name = file.leafName; michael@0: var dot = name.lastIndexOf("."); michael@0: if (dot > 0) michael@0: { michael@0: var ext = name.slice(dot + 1); michael@0: if (ext in this._mimeMappings) michael@0: return this._mimeMappings[ext]; michael@0: } michael@0: return Cc["@mozilla.org/uriloader/external-helper-app-service;1"] michael@0: .getService(Ci.nsIMIMEService) michael@0: .getTypeFromFile(file); michael@0: } michael@0: catch (e) michael@0: { michael@0: return "application/octet-stream"; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns the nsILocalFile which corresponds to the path, as determined using michael@0: * all registered path->directory mappings and any paths which are explicitly michael@0: * overridden. michael@0: * michael@0: * @param path : string michael@0: * the server path for which a file should be retrieved, e.g. "/foo/bar" michael@0: * @throws HttpError michael@0: * when the correct action is the corresponding HTTP error (i.e., because no michael@0: * mapping was found for a directory in path, the referenced file doesn't michael@0: * exist, etc.) michael@0: * @returns nsILocalFile michael@0: * the file to be sent as the response to a request for the path michael@0: */ michael@0: _getFileForPath: function(path) michael@0: { michael@0: // decode and add underscores as necessary michael@0: try michael@0: { michael@0: path = toInternalPath(path, true); michael@0: } michael@0: catch (e) michael@0: { michael@0: throw HTTP_400; // malformed path michael@0: } michael@0: michael@0: // next, get the directory which contains this path michael@0: var pathMap = this._pathDirectoryMap; michael@0: michael@0: // An example progression of tmp for a path "/foo/bar/baz/" might be: michael@0: // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", "" michael@0: var tmp = path.substring(1); michael@0: while (true) michael@0: { michael@0: // do we have a match for current head of the path? michael@0: var file = pathMap.get(tmp); michael@0: if (file) michael@0: { michael@0: // XXX hack; basically disable showing mapping for /foo/bar/ when the michael@0: // requested path was /foo/bar, because relative links on the page michael@0: // will all be incorrect -- we really need the ability to easily michael@0: // redirect here instead michael@0: if (tmp == path.substring(1) && michael@0: tmp.length != 0 && michael@0: tmp.charAt(tmp.length - 1) != "/") michael@0: file = null; michael@0: else michael@0: break; michael@0: } michael@0: michael@0: // if we've finished trying all prefixes, exit michael@0: if (tmp == "") michael@0: break; michael@0: michael@0: tmp = tmp.substring(0, tmp.lastIndexOf("/")); michael@0: } michael@0: michael@0: // no mapping applies, so 404 michael@0: if (!file) michael@0: throw HTTP_404; michael@0: michael@0: michael@0: // last, get the file for the path within the determined directory michael@0: var parentFolder = file.parent; michael@0: var dirIsRoot = (parentFolder == null); michael@0: michael@0: // Strategy here is to append components individually, making sure we michael@0: // never move above the given directory; this allows paths such as michael@0: // "/foo/../bar" but prevents paths such as "/../base-sibling"; michael@0: // this component-wise approach also means the code works even on platforms michael@0: // which don't use "/" as the directory separator, such as Windows michael@0: var leafPath = path.substring(tmp.length + 1); michael@0: var comps = leafPath.split("/"); michael@0: for (var i = 0, sz = comps.length; i < sz; i++) michael@0: { michael@0: var comp = comps[i]; michael@0: michael@0: if (comp == "..") michael@0: file = file.parent; michael@0: else if (comp == "." || comp == "") michael@0: continue; michael@0: else michael@0: file.append(comp); michael@0: michael@0: if (!dirIsRoot && file.equals(parentFolder)) michael@0: throw HTTP_403; michael@0: } michael@0: michael@0: return file; michael@0: }, michael@0: michael@0: /** michael@0: * Writes the error page for the given HTTP error code over the given michael@0: * connection. michael@0: * michael@0: * @param errorCode : uint michael@0: * the HTTP error code to be used michael@0: * @param connection : Connection michael@0: * the connection on which the error occurred michael@0: */ michael@0: handleError: function(errorCode, connection) michael@0: { michael@0: var response = new Response(connection); michael@0: michael@0: dumpn("*** error in request: " + errorCode); michael@0: michael@0: this._handleError(errorCode, new Request(connection.port), response); michael@0: }, michael@0: michael@0: /** michael@0: * Handles a request which generates the given error code, using the michael@0: * user-defined error handler if one has been set, gracefully falling back to michael@0: * the x00 status code if the code has no handler, and failing to status code michael@0: * 500 if all else fails. michael@0: * michael@0: * @param errorCode : uint michael@0: * the HTTP error which is to be returned michael@0: * @param metadata : Request michael@0: * metadata for the request, which will often be incomplete since this is an michael@0: * error michael@0: * @param response : Response michael@0: * an uninitialized Response should be initialized when this method michael@0: * completes with information which represents the desired error code in the michael@0: * ideal case or a fallback code in abnormal circumstances (i.e., 500 is a michael@0: * fallback for 505, per HTTP specs) michael@0: */ michael@0: _handleError: function(errorCode, metadata, response) michael@0: { michael@0: if (!metadata) michael@0: throw Cr.NS_ERROR_NULL_POINTER; michael@0: michael@0: var errorX00 = errorCode - (errorCode % 100); michael@0: michael@0: try michael@0: { michael@0: if (!(errorCode in HTTP_ERROR_CODES)) michael@0: dumpn("*** WARNING: requested invalid error: " + errorCode); michael@0: michael@0: // RFC 2616 says that we should try to handle an error by its class if we michael@0: // can't otherwise handle it -- if that fails, we revert to handling it as michael@0: // a 500 internal server error, and if that fails we throw and shut down michael@0: // the server michael@0: michael@0: // actually handle the error michael@0: try michael@0: { michael@0: if (errorCode in this._overrideErrors) michael@0: this._overrideErrors[errorCode](metadata, response); michael@0: else michael@0: this._defaultErrors[errorCode](metadata, response); michael@0: } michael@0: catch (e) michael@0: { michael@0: if (response.partiallySent()) michael@0: { michael@0: response.abort(e); michael@0: return; michael@0: } michael@0: michael@0: // don't retry the handler that threw michael@0: if (errorX00 == errorCode) michael@0: throw HTTP_500; michael@0: michael@0: dumpn("*** error in handling for error code " + errorCode + ", " + michael@0: "falling back to " + errorX00 + "..."); michael@0: response = new Response(response._connection); michael@0: if (errorX00 in this._overrideErrors) michael@0: this._overrideErrors[errorX00](metadata, response); michael@0: else if (errorX00 in this._defaultErrors) michael@0: this._defaultErrors[errorX00](metadata, response); michael@0: else michael@0: throw HTTP_500; michael@0: } michael@0: } michael@0: catch (e) michael@0: { michael@0: if (response.partiallySent()) michael@0: { michael@0: response.abort(); michael@0: return; michael@0: } michael@0: michael@0: // we've tried everything possible for a meaningful error -- now try 500 michael@0: dumpn("*** error in handling for error code " + errorX00 + ", falling " + michael@0: "back to 500..."); michael@0: michael@0: try michael@0: { michael@0: response = new Response(response._connection); michael@0: if (500 in this._overrideErrors) michael@0: this._overrideErrors[500](metadata, response); michael@0: else michael@0: this._defaultErrors[500](metadata, response); michael@0: } michael@0: catch (e2) michael@0: { michael@0: dumpn("*** multiple errors in default error handlers!"); michael@0: dumpn("*** e == " + e + ", e2 == " + e2); michael@0: response.abort(e2); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: response.complete(); michael@0: }, michael@0: michael@0: // FIELDS michael@0: michael@0: /** michael@0: * This object contains the default handlers for the various HTTP error codes. michael@0: */ michael@0: _defaultErrors: michael@0: { michael@0: 400: function(metadata, response) michael@0: { michael@0: // none of the data in metadata is reliable, so hard-code everything here michael@0: response.setStatusLine("1.1", 400, "Bad Request"); michael@0: response.setHeader("Content-Type", "text/plain", false); michael@0: michael@0: var body = "Bad request\n"; michael@0: response.bodyOutputStream.write(body, body.length); michael@0: }, michael@0: 403: function(metadata, response) michael@0: { michael@0: response.setStatusLine(metadata.httpVersion, 403, "Forbidden"); michael@0: response.setHeader("Content-Type", "text/html", false); michael@0: michael@0: var body = "\ michael@0: 403 Forbidden\ michael@0: \ michael@0:

403 Forbidden

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

404 Not Found

\ michael@0:

\ michael@0: " + michael@0: htmlEscape(metadata.path) + michael@0: " was not found.\ michael@0:

\ michael@0: \ michael@0: "; michael@0: response.bodyOutputStream.write(body, body.length); michael@0: }, michael@0: 416: function(metadata, response) michael@0: { michael@0: response.setStatusLine(metadata.httpVersion, michael@0: 416, michael@0: "Requested Range Not Satisfiable"); michael@0: response.setHeader("Content-Type", "text/html", false); michael@0: michael@0: var body = "\ michael@0: \ michael@0: 416 Requested Range Not Satisfiable\ michael@0: \ michael@0:

416 Requested Range Not Satisfiable

\ michael@0:

The byte range was not valid for the\ michael@0: requested resource.\ michael@0:

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

500 Internal Server Error

\ michael@0:

Something's broken in this server and\ michael@0: needs to be fixed.

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

501 Not Implemented

\ michael@0:

This server is not (yet) Apache.

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

505 HTTP Version Not Supported

\ michael@0:

This server only supports HTTP/1.0 and HTTP/1.1\ michael@0: connections.

\ michael@0: \ michael@0: "; michael@0: response.bodyOutputStream.write(body, body.length); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Contains handlers for the default set of URIs contained in this server. michael@0: */ michael@0: _defaultPaths: michael@0: { michael@0: "/": function(metadata, response) michael@0: { michael@0: response.setStatusLine(metadata.httpVersion, 200, "OK"); michael@0: response.setHeader("Content-Type", "text/html", false); michael@0: michael@0: var body = "\ michael@0: httpd.js\ michael@0: \ michael@0:

httpd.js

\ michael@0:

If you're seeing this page, httpd.js is up and\ michael@0: serving requests! Now set a base path and serve some\ michael@0: files!

\ michael@0: \ michael@0: "; michael@0: michael@0: response.bodyOutputStream.write(body, body.length); michael@0: }, michael@0: michael@0: "/trace": function(metadata, response) michael@0: { michael@0: response.setStatusLine(metadata.httpVersion, 200, "OK"); michael@0: response.setHeader("Content-Type", "text/plain", false); michael@0: michael@0: var body = "Request-URI: " + michael@0: metadata.scheme + "://" + metadata.host + ":" + metadata.port + michael@0: metadata.path + "\n\n"; michael@0: body += "Request (semantically equivalent, slightly reformatted):\n\n"; michael@0: body += metadata.method + " " + metadata.path; michael@0: michael@0: if (metadata.queryString) michael@0: body += "?" + metadata.queryString; michael@0: michael@0: body += " HTTP/" + metadata.httpVersion + "\r\n"; michael@0: michael@0: var headEnum = metadata.headers; michael@0: while (headEnum.hasMoreElements()) michael@0: { michael@0: var fieldName = headEnum.getNext() michael@0: .QueryInterface(Ci.nsISupportsString) michael@0: .data; michael@0: body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n"; michael@0: } michael@0: michael@0: response.bodyOutputStream.write(body, body.length); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Maps absolute paths to files on the local file system (as nsILocalFiles). michael@0: */ michael@0: function FileMap() michael@0: { michael@0: /** Hash which will map paths to nsILocalFiles. */ michael@0: this._map = {}; michael@0: } michael@0: FileMap.prototype = michael@0: { michael@0: // PUBLIC API michael@0: michael@0: /** michael@0: * Maps key to a clone of the nsILocalFile value if value is non-null; michael@0: * otherwise, removes any extant mapping for key. michael@0: * michael@0: * @param key : string michael@0: * string to which a clone of value is mapped michael@0: * @param value : nsILocalFile michael@0: * the file to map to key, or null to remove a mapping michael@0: */ michael@0: put: function(key, value) michael@0: { michael@0: if (value) michael@0: this._map[key] = value.clone(); michael@0: else michael@0: delete this._map[key]; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a clone of the nsILocalFile mapped to key, or null if no such michael@0: * mapping exists. michael@0: * michael@0: * @param key : string michael@0: * key to which the returned file maps michael@0: * @returns nsILocalFile michael@0: * a clone of the mapped file, or null if no mapping exists michael@0: */ michael@0: get: function(key) michael@0: { michael@0: var val = this._map[key]; michael@0: return val ? val.clone() : null; michael@0: } michael@0: }; michael@0: michael@0: michael@0: // Response CONSTANTS michael@0: michael@0: // token = * michael@0: // CHAR = michael@0: // CTL = michael@0: // separators = "(" | ")" | "<" | ">" | "@" michael@0: // | "," | ";" | ":" | "\" | <"> michael@0: // | "/" | "[" | "]" | "?" | "=" michael@0: // | "{" | "}" | SP | HT michael@0: const IS_TOKEN_ARRAY = michael@0: [0, 0, 0, 0, 0, 0, 0, 0, // 0 michael@0: 0, 0, 0, 0, 0, 0, 0, 0, // 8 michael@0: 0, 0, 0, 0, 0, 0, 0, 0, // 16 michael@0: 0, 0, 0, 0, 0, 0, 0, 0, // 24 michael@0: michael@0: 0, 1, 0, 1, 1, 1, 1, 1, // 32 michael@0: 0, 0, 1, 1, 0, 1, 1, 0, // 40 michael@0: 1, 1, 1, 1, 1, 1, 1, 1, // 48 michael@0: 1, 1, 0, 0, 0, 0, 0, 0, // 56 michael@0: michael@0: 0, 1, 1, 1, 1, 1, 1, 1, // 64 michael@0: 1, 1, 1, 1, 1, 1, 1, 1, // 72 michael@0: 1, 1, 1, 1, 1, 1, 1, 1, // 80 michael@0: 1, 1, 1, 0, 0, 0, 1, 1, // 88 michael@0: michael@0: 1, 1, 1, 1, 1, 1, 1, 1, // 96 michael@0: 1, 1, 1, 1, 1, 1, 1, 1, // 104 michael@0: 1, 1, 1, 1, 1, 1, 1, 1, // 112 michael@0: 1, 1, 1, 0, 1, 0, 1]; // 120 michael@0: michael@0: michael@0: /** michael@0: * Determines whether the given character code is a CTL. michael@0: * michael@0: * @param code : uint michael@0: * the character code michael@0: * @returns boolean michael@0: * true if code is a CTL, false otherwise michael@0: */ michael@0: function isCTL(code) michael@0: { michael@0: return (code >= 0 && code <= 31) || (code == 127); michael@0: } michael@0: michael@0: /** michael@0: * Represents a response to an HTTP request, encapsulating all details of that michael@0: * response. This includes all headers, the HTTP version, status code and michael@0: * explanation, and the entity itself. michael@0: * michael@0: * @param connection : Connection michael@0: * the connection over which this response is to be written michael@0: */ michael@0: function Response(connection) michael@0: { michael@0: /** The connection over which this response will be written. */ michael@0: this._connection = connection; michael@0: michael@0: /** michael@0: * The HTTP version of this response; defaults to 1.1 if not set by the michael@0: * handler. michael@0: */ michael@0: this._httpVersion = nsHttpVersion.HTTP_1_1; michael@0: michael@0: /** michael@0: * The HTTP code of this response; defaults to 200. michael@0: */ michael@0: this._httpCode = 200; michael@0: michael@0: /** michael@0: * The description of the HTTP code in this response; defaults to "OK". michael@0: */ michael@0: this._httpDescription = "OK"; michael@0: michael@0: /** michael@0: * An nsIHttpHeaders object in which the headers in this response should be michael@0: * stored. This property is null after the status line and headers have been michael@0: * written to the network, and it may be modified up until it is cleared, michael@0: * except if this._finished is set first (in which case headers are written michael@0: * asynchronously in response to a finish() call not preceded by michael@0: * flushHeaders()). michael@0: */ michael@0: this._headers = new nsHttpHeaders(); michael@0: michael@0: /** michael@0: * Set to true when this response is ended (completely constructed if possible michael@0: * and the connection closed); further actions on this will then fail. michael@0: */ michael@0: this._ended = false; michael@0: michael@0: /** michael@0: * A stream used to hold data written to the body of this response. michael@0: */ michael@0: this._bodyOutputStream = null; michael@0: michael@0: /** michael@0: * A stream containing all data that has been written to the body of this michael@0: * response so far. (Async handlers make the data contained in this michael@0: * unreliable as a way of determining content length in general, but auxiliary michael@0: * saved information can sometimes be used to guarantee reliability.) michael@0: */ michael@0: this._bodyInputStream = null; michael@0: michael@0: /** michael@0: * A stream copier which copies data to the network. It is initially null michael@0: * until replaced with a copier for response headers; when headers have been michael@0: * fully sent it is replaced with a copier for the response body, remaining michael@0: * so for the duration of response processing. michael@0: */ michael@0: this._asyncCopier = null; michael@0: michael@0: /** michael@0: * True if this response has been designated as being processed michael@0: * asynchronously rather than for the duration of a single call to michael@0: * nsIHttpRequestHandler.handle. michael@0: */ michael@0: this._processAsync = false; michael@0: michael@0: /** michael@0: * True iff finish() has been called on this, signaling that no more changes michael@0: * to this may be made. michael@0: */ michael@0: this._finished = false; michael@0: michael@0: /** michael@0: * True iff powerSeized() has been called on this, signaling that this michael@0: * response is to be handled manually by the response handler (which may then michael@0: * send arbitrary data in response, even non-HTTP responses). michael@0: */ michael@0: this._powerSeized = false; michael@0: } michael@0: Response.prototype = michael@0: { michael@0: // PUBLIC CONSTRUCTION API michael@0: michael@0: // michael@0: // see nsIHttpResponse.bodyOutputStream michael@0: // michael@0: get bodyOutputStream() michael@0: { michael@0: if (this._finished) michael@0: throw Cr.NS_ERROR_NOT_AVAILABLE; michael@0: michael@0: if (!this._bodyOutputStream) michael@0: { michael@0: var pipe = new Pipe(true, false, Response.SEGMENT_SIZE, PR_UINT32_MAX, michael@0: null); michael@0: this._bodyOutputStream = pipe.outputStream; michael@0: this._bodyInputStream = pipe.inputStream; michael@0: if (this._processAsync || this._powerSeized) michael@0: this._startAsyncProcessor(); michael@0: } michael@0: michael@0: return this._bodyOutputStream; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpResponse.write michael@0: // michael@0: write: function(data) michael@0: { michael@0: if (this._finished) michael@0: throw Cr.NS_ERROR_NOT_AVAILABLE; michael@0: michael@0: var dataAsString = String(data); michael@0: this.bodyOutputStream.write(dataAsString, dataAsString.length); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpResponse.setStatusLine michael@0: // michael@0: setStatusLine: function(httpVersion, code, description) michael@0: { michael@0: if (!this._headers || this._finished || this._powerSeized) michael@0: throw Cr.NS_ERROR_NOT_AVAILABLE; michael@0: this._ensureAlive(); michael@0: michael@0: if (!(code >= 0 && code < 1000)) michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: try michael@0: { michael@0: var httpVer; michael@0: // avoid version construction for the most common cases michael@0: if (!httpVersion || httpVersion == "1.1") michael@0: httpVer = nsHttpVersion.HTTP_1_1; michael@0: else if (httpVersion == "1.0") michael@0: httpVer = nsHttpVersion.HTTP_1_0; michael@0: else michael@0: httpVer = new nsHttpVersion(httpVersion); michael@0: } michael@0: catch (e) michael@0: { michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: } michael@0: michael@0: // Reason-Phrase = * michael@0: // TEXT = michael@0: // michael@0: // XXX this ends up disallowing octets which aren't Unicode, I think -- not michael@0: // much to do if description is IDL'd as string michael@0: if (!description) michael@0: description = ""; michael@0: for (var i = 0; i < description.length; i++) michael@0: if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t") michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: // set the values only after validation to preserve atomicity michael@0: this._httpDescription = description; michael@0: this._httpCode = code; michael@0: this._httpVersion = httpVer; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpResponse.setHeader michael@0: // michael@0: setHeader: function(name, value, merge) michael@0: { michael@0: if (!this._headers || this._finished || this._powerSeized) michael@0: throw Cr.NS_ERROR_NOT_AVAILABLE; michael@0: this._ensureAlive(); michael@0: michael@0: this._headers.setHeader(name, value, merge); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpResponse.processAsync michael@0: // michael@0: processAsync: function() michael@0: { michael@0: if (this._finished) michael@0: throw Cr.NS_ERROR_UNEXPECTED; michael@0: if (this._powerSeized) michael@0: throw Cr.NS_ERROR_NOT_AVAILABLE; michael@0: if (this._processAsync) michael@0: return; michael@0: this._ensureAlive(); michael@0: michael@0: dumpn("*** processing connection " + this._connection.number + " async"); michael@0: this._processAsync = true; michael@0: michael@0: /* michael@0: * Either the bodyOutputStream getter or this method is responsible for michael@0: * starting the asynchronous processor and catching writes of data to the michael@0: * response body of async responses as they happen, for the purpose of michael@0: * forwarding those writes to the actual connection's output stream. michael@0: * If bodyOutputStream is accessed first, calling this method will create michael@0: * the processor (when it first is clear that body data is to be written michael@0: * immediately, not buffered). If this method is called first, accessing michael@0: * bodyOutputStream will create the processor. If only this method is michael@0: * called, we'll write nothing, neither headers nor the nonexistent body, michael@0: * until finish() is called. Since that delay is easily avoided by simply michael@0: * getting bodyOutputStream or calling write(""), we don't worry about it. michael@0: */ michael@0: if (this._bodyOutputStream && !this._asyncCopier) michael@0: this._startAsyncProcessor(); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpResponse.seizePower michael@0: // michael@0: seizePower: function() michael@0: { michael@0: if (this._processAsync) michael@0: throw Cr.NS_ERROR_NOT_AVAILABLE; michael@0: if (this._finished) michael@0: throw Cr.NS_ERROR_UNEXPECTED; michael@0: if (this._powerSeized) michael@0: return; michael@0: this._ensureAlive(); michael@0: michael@0: dumpn("*** forcefully seizing power over connection " + michael@0: this._connection.number + "..."); michael@0: michael@0: // Purge any already-written data without sending it. We could as easily michael@0: // swap out the streams entirely, but that makes it possible to acquire and michael@0: // unknowingly use a stale reference, so we require there only be one of michael@0: // each stream ever for any response to avoid this complication. michael@0: if (this._asyncCopier) michael@0: this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED); michael@0: this._asyncCopier = null; michael@0: if (this._bodyOutputStream) michael@0: { michael@0: var input = new BinaryInputStream(this._bodyInputStream); michael@0: var avail; michael@0: while ((avail = input.available()) > 0) michael@0: input.readByteArray(avail); michael@0: } michael@0: michael@0: this._powerSeized = true; michael@0: if (this._bodyOutputStream) michael@0: this._startAsyncProcessor(); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpResponse.finish michael@0: // michael@0: finish: function() michael@0: { michael@0: if (!this._processAsync && !this._powerSeized) michael@0: throw Cr.NS_ERROR_UNEXPECTED; michael@0: if (this._finished) michael@0: return; michael@0: michael@0: dumpn("*** finishing connection " + this._connection.number); michael@0: this._startAsyncProcessor(); // in case bodyOutputStream was never accessed michael@0: if (this._bodyOutputStream) michael@0: this._bodyOutputStream.close(); michael@0: this._finished = true; michael@0: }, michael@0: michael@0: michael@0: // NSISUPPORTS michael@0: michael@0: // michael@0: // see nsISupports.QueryInterface michael@0: // michael@0: QueryInterface: function(iid) michael@0: { michael@0: if (iid.equals(Ci.nsIHttpResponse) || iid.equals(Ci.nsISupports)) michael@0: return this; michael@0: michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: michael@0: // POST-CONSTRUCTION API (not exposed externally) michael@0: michael@0: /** michael@0: * The HTTP version number of this, as a string (e.g. "1.1"). michael@0: */ michael@0: get httpVersion() michael@0: { michael@0: this._ensureAlive(); michael@0: return this._httpVersion.toString(); michael@0: }, michael@0: michael@0: /** michael@0: * The HTTP status code of this response, as a string of three characters per michael@0: * RFC 2616. michael@0: */ michael@0: get httpCode() michael@0: { michael@0: this._ensureAlive(); michael@0: michael@0: var codeString = (this._httpCode < 10 ? "0" : "") + michael@0: (this._httpCode < 100 ? "0" : "") + michael@0: this._httpCode; michael@0: return codeString; michael@0: }, michael@0: michael@0: /** michael@0: * The description of the HTTP status code of this response, or "" if none is michael@0: * set. michael@0: */ michael@0: get httpDescription() michael@0: { michael@0: this._ensureAlive(); michael@0: michael@0: return this._httpDescription; michael@0: }, michael@0: michael@0: /** michael@0: * The headers in this response, as an nsHttpHeaders object. michael@0: */ michael@0: get headers() michael@0: { michael@0: this._ensureAlive(); michael@0: michael@0: return this._headers; michael@0: }, michael@0: michael@0: // michael@0: // see nsHttpHeaders.getHeader michael@0: // michael@0: getHeader: function(name) michael@0: { michael@0: this._ensureAlive(); michael@0: michael@0: return this._headers.getHeader(name); michael@0: }, michael@0: michael@0: /** michael@0: * Determines whether this response may be abandoned in favor of a newly michael@0: * constructed response. A response may be abandoned only if it is not being michael@0: * sent asynchronously and if raw control over it has not been taken from the michael@0: * server. michael@0: * michael@0: * @returns boolean michael@0: * true iff no data has been written to the network michael@0: */ michael@0: partiallySent: function() michael@0: { michael@0: dumpn("*** partiallySent()"); michael@0: return this._processAsync || this._powerSeized; michael@0: }, michael@0: michael@0: /** michael@0: * If necessary, kicks off the remaining request processing needed to be done michael@0: * after a request handler performs its initial work upon this response. michael@0: */ michael@0: complete: function() michael@0: { michael@0: dumpn("*** complete()"); michael@0: if (this._processAsync || this._powerSeized) michael@0: { michael@0: NS_ASSERT(this._processAsync ^ this._powerSeized, michael@0: "can't both send async and relinquish power"); michael@0: return; michael@0: } michael@0: michael@0: NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?"); michael@0: michael@0: this._startAsyncProcessor(); michael@0: michael@0: // Now make sure we finish processing this request! michael@0: if (this._bodyOutputStream) michael@0: this._bodyOutputStream.close(); michael@0: }, michael@0: michael@0: /** michael@0: * Abruptly ends processing of this response, usually due to an error in an michael@0: * incoming request but potentially due to a bad error handler. Since we michael@0: * cannot handle the error in the usual way (giving an HTTP error page in michael@0: * response) because data may already have been sent (or because the response michael@0: * might be expected to have been generated asynchronously or completely from michael@0: * scratch by the handler), we stop processing this response and abruptly michael@0: * close the connection. michael@0: * michael@0: * @param e : Error michael@0: * the exception which precipitated this abort, or null if no such exception michael@0: * was generated michael@0: */ michael@0: abort: function(e) michael@0: { michael@0: dumpn("*** abort(<" + e + ">)"); michael@0: michael@0: // This response will be ended by the processor if one was created. michael@0: var copier = this._asyncCopier; michael@0: if (copier) michael@0: { michael@0: // We dispatch asynchronously here so that any pending writes of data to michael@0: // the connection will be deterministically written. This makes it easier michael@0: // to specify exact behavior, and it makes observable behavior more michael@0: // predictable for clients. Note that the correctness of this depends on michael@0: // callbacks in response to _waitToReadData in WriteThroughCopier michael@0: // happening asynchronously with respect to the actual writing of data to michael@0: // bodyOutputStream, as they currently do; if they happened synchronously, michael@0: // an event which ran before this one could write more data to the michael@0: // response body before we get around to canceling the copier. We have michael@0: // tests for this in test_seizepower.js, however, and I can't think of a michael@0: // way to handle both cases without removing bodyOutputStream access and michael@0: // moving its effective write(data, length) method onto Response, which michael@0: // would be slower and require more code than this anyway. michael@0: gThreadManager.currentThread.dispatch({ michael@0: run: function() michael@0: { michael@0: dumpn("*** canceling copy asynchronously..."); michael@0: copier.cancel(Cr.NS_ERROR_UNEXPECTED); michael@0: } michael@0: }, Ci.nsIThread.DISPATCH_NORMAL); michael@0: } michael@0: else michael@0: { michael@0: this.end(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Closes this response's network connection, marks the response as finished, michael@0: * and notifies the server handler that the request is done being processed. michael@0: */ michael@0: end: function() michael@0: { michael@0: NS_ASSERT(!this._ended, "ending this response twice?!?!"); michael@0: michael@0: this._connection.close(); michael@0: if (this._bodyOutputStream) michael@0: this._bodyOutputStream.close(); michael@0: michael@0: this._finished = true; michael@0: this._ended = true; michael@0: }, michael@0: michael@0: // PRIVATE IMPLEMENTATION michael@0: michael@0: /** michael@0: * Sends the status line and headers of this response if they haven't been michael@0: * sent and initiates the process of copying data written to this response's michael@0: * body to the network. michael@0: */ michael@0: _startAsyncProcessor: function() michael@0: { michael@0: dumpn("*** _startAsyncProcessor()"); michael@0: michael@0: // Handle cases where we're being called a second time. The former case michael@0: // happens when this is triggered both by complete() and by processAsync(), michael@0: // while the latter happens when processAsync() in conjunction with sent michael@0: // data causes abort() to be called. michael@0: if (this._asyncCopier || this._ended) michael@0: { michael@0: dumpn("*** ignoring second call to _startAsyncProcessor"); michael@0: return; michael@0: } michael@0: michael@0: // Send headers if they haven't been sent already and should be sent, then michael@0: // asynchronously continue to send the body. michael@0: if (this._headers && !this._powerSeized) michael@0: { michael@0: this._sendHeaders(); michael@0: return; michael@0: } michael@0: michael@0: this._headers = null; michael@0: this._sendBody(); michael@0: }, michael@0: michael@0: /** michael@0: * Signals that all modifications to the response status line and headers are michael@0: * complete and then sends that data over the network to the client. Once michael@0: * this method completes, a different response to the request that resulted michael@0: * in this response cannot be sent -- the only possible action in case of michael@0: * error is to abort the response and close the connection. michael@0: */ michael@0: _sendHeaders: function() michael@0: { michael@0: dumpn("*** _sendHeaders()"); michael@0: michael@0: NS_ASSERT(this._headers); michael@0: NS_ASSERT(!this._powerSeized); michael@0: michael@0: // request-line michael@0: var statusLine = "HTTP/" + this.httpVersion + " " + michael@0: this.httpCode + " " + michael@0: this.httpDescription + "\r\n"; michael@0: michael@0: // header post-processing michael@0: michael@0: var headers = this._headers; michael@0: headers.setHeader("Connection", "close", false); michael@0: headers.setHeader("Server", "httpd.js", false); michael@0: if (!headers.hasHeader("Date")) michael@0: headers.setHeader("Date", toDateString(Date.now()), false); michael@0: michael@0: // Any response not being processed asynchronously must have an associated michael@0: // Content-Length header for reasons of backwards compatibility with the michael@0: // initial server, which fully buffered every response before sending it. michael@0: // Beyond that, however, it's good to do this anyway because otherwise it's michael@0: // impossible to test behaviors that depend on the presence or absence of a michael@0: // Content-Length header. michael@0: if (!this._processAsync) michael@0: { michael@0: dumpn("*** non-async response, set Content-Length"); michael@0: michael@0: var bodyStream = this._bodyInputStream; michael@0: var avail = bodyStream ? bodyStream.available() : 0; michael@0: michael@0: // XXX assumes stream will always report the full amount of data available michael@0: headers.setHeader("Content-Length", "" + avail, false); michael@0: } michael@0: michael@0: michael@0: // construct and send response michael@0: dumpn("*** header post-processing completed, sending response head..."); michael@0: michael@0: // request-line michael@0: var preambleData = [statusLine]; michael@0: michael@0: // headers michael@0: var headEnum = headers.enumerator; michael@0: while (headEnum.hasMoreElements()) michael@0: { michael@0: var fieldName = headEnum.getNext() michael@0: .QueryInterface(Ci.nsISupportsString) michael@0: .data; michael@0: var values = headers.getHeaderValues(fieldName); michael@0: for (var i = 0, sz = values.length; i < sz; i++) michael@0: preambleData.push(fieldName + ": " + values[i] + "\r\n"); michael@0: } michael@0: michael@0: // end request-line/headers michael@0: preambleData.push("\r\n"); michael@0: michael@0: var preamble = preambleData.join(""); michael@0: michael@0: var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null); michael@0: responseHeadPipe.outputStream.write(preamble, preamble.length); michael@0: michael@0: var response = this; michael@0: var copyObserver = michael@0: { michael@0: onStartRequest: function(request, cx) michael@0: { michael@0: dumpn("*** preamble copying started"); michael@0: }, michael@0: michael@0: onStopRequest: function(request, cx, statusCode) michael@0: { michael@0: dumpn("*** preamble copying complete " + michael@0: "[status=0x" + statusCode.toString(16) + "]"); michael@0: michael@0: if (!components.isSuccessCode(statusCode)) michael@0: { michael@0: dumpn("!!! header copying problems: non-success statusCode, " + michael@0: "ending response"); michael@0: michael@0: response.end(); michael@0: } michael@0: else michael@0: { michael@0: response._sendBody(); michael@0: } michael@0: }, michael@0: michael@0: QueryInterface: function(aIID) michael@0: { michael@0: if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) michael@0: return this; michael@0: michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: }; michael@0: michael@0: var headerCopier = this._asyncCopier = michael@0: new WriteThroughCopier(responseHeadPipe.inputStream, michael@0: this._connection.output, michael@0: copyObserver, null); michael@0: michael@0: responseHeadPipe.outputStream.close(); michael@0: michael@0: // Forbid setting any more headers or modifying the request line. michael@0: this._headers = null; michael@0: }, michael@0: michael@0: /** michael@0: * Asynchronously writes the body of the response (or the entire response, if michael@0: * seizePower() has been called) to the network. michael@0: */ michael@0: _sendBody: function() michael@0: { michael@0: dumpn("*** _sendBody"); michael@0: michael@0: NS_ASSERT(!this._headers, "still have headers around but sending body?"); michael@0: michael@0: // If no body data was written, we're done michael@0: if (!this._bodyInputStream) michael@0: { michael@0: dumpn("*** empty body, response finished"); michael@0: this.end(); michael@0: return; michael@0: } michael@0: michael@0: var response = this; michael@0: var copyObserver = michael@0: { michael@0: onStartRequest: function(request, context) michael@0: { michael@0: dumpn("*** onStartRequest"); michael@0: }, michael@0: michael@0: onStopRequest: function(request, cx, statusCode) michael@0: { michael@0: dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]"); michael@0: michael@0: if (statusCode === Cr.NS_BINDING_ABORTED) michael@0: { michael@0: dumpn("*** terminating copy observer without ending the response"); michael@0: } michael@0: else michael@0: { michael@0: if (!components.isSuccessCode(statusCode)) michael@0: dumpn("*** WARNING: non-success statusCode in onStopRequest"); michael@0: michael@0: response.end(); michael@0: } michael@0: }, michael@0: michael@0: QueryInterface: function(aIID) michael@0: { michael@0: if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) michael@0: return this; michael@0: michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: }; michael@0: michael@0: dumpn("*** starting async copier of body data..."); michael@0: this._asyncCopier = michael@0: new WriteThroughCopier(this._bodyInputStream, this._connection.output, michael@0: copyObserver, null); michael@0: }, michael@0: michael@0: /** Ensures that this hasn't been ended. */ michael@0: _ensureAlive: function() michael@0: { michael@0: NS_ASSERT(!this._ended, "not handling response lifetime correctly"); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Size of the segments in the buffer used in storing response data and writing michael@0: * it to the socket. michael@0: */ michael@0: Response.SEGMENT_SIZE = 8192; michael@0: michael@0: /** Serves double duty in WriteThroughCopier implementation. */ michael@0: function notImplemented() michael@0: { michael@0: throw Cr.NS_ERROR_NOT_IMPLEMENTED; michael@0: } michael@0: michael@0: /** Returns true iff the given exception represents stream closure. */ michael@0: function streamClosed(e) michael@0: { michael@0: return e === Cr.NS_BASE_STREAM_CLOSED || michael@0: (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED); michael@0: } michael@0: michael@0: /** Returns true iff the given exception represents a blocked stream. */ michael@0: function wouldBlock(e) michael@0: { michael@0: return e === Cr.NS_BASE_STREAM_WOULD_BLOCK || michael@0: (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK); michael@0: } michael@0: michael@0: /** michael@0: * Copies data from source to sink as it becomes available, when that data can michael@0: * be written to sink without blocking. michael@0: * michael@0: * @param source : nsIAsyncInputStream michael@0: * the stream from which data is to be read michael@0: * @param sink : nsIAsyncOutputStream michael@0: * the stream to which data is to be copied michael@0: * @param observer : nsIRequestObserver michael@0: * an observer which will be notified when the copy starts and finishes michael@0: * @param context : nsISupports michael@0: * context passed to observer when notified of start/stop michael@0: * @throws NS_ERROR_NULL_POINTER michael@0: * if source, sink, or observer are null michael@0: */ michael@0: function WriteThroughCopier(source, sink, observer, context) michael@0: { michael@0: if (!source || !sink || !observer) michael@0: throw Cr.NS_ERROR_NULL_POINTER; michael@0: michael@0: /** Stream from which data is being read. */ michael@0: this._source = source; michael@0: michael@0: /** Stream to which data is being written. */ michael@0: this._sink = sink; michael@0: michael@0: /** Observer watching this copy. */ michael@0: this._observer = observer; michael@0: michael@0: /** Context for the observer watching this. */ michael@0: this._context = context; michael@0: michael@0: /** michael@0: * True iff this is currently being canceled (cancel has been called, the michael@0: * callback may not yet have been made). michael@0: */ michael@0: this._canceled = false; michael@0: michael@0: /** michael@0: * False until all data has been read from input and written to output, at michael@0: * which point this copy is completed and cancel() is asynchronously called. michael@0: */ michael@0: this._completed = false; michael@0: michael@0: /** Required by nsIRequest, meaningless. */ michael@0: this.loadFlags = 0; michael@0: /** Required by nsIRequest, meaningless. */ michael@0: this.loadGroup = null; michael@0: /** Required by nsIRequest, meaningless. */ michael@0: this.name = "response-body-copy"; michael@0: michael@0: /** Status of this request. */ michael@0: this.status = Cr.NS_OK; michael@0: michael@0: /** Arrays of byte strings waiting to be written to output. */ michael@0: this._pendingData = []; michael@0: michael@0: // start copying michael@0: try michael@0: { michael@0: observer.onStartRequest(this, context); michael@0: this._waitToReadData(); michael@0: this._waitForSinkClosure(); michael@0: } michael@0: catch (e) michael@0: { michael@0: dumpn("!!! error starting copy: " + e + michael@0: ("lineNumber" in e ? ", line " + e.lineNumber : "")); michael@0: dumpn(e.stack); michael@0: this.cancel(Cr.NS_ERROR_UNEXPECTED); michael@0: } michael@0: } michael@0: WriteThroughCopier.prototype = michael@0: { michael@0: /* nsISupports implementation */ michael@0: michael@0: QueryInterface: function(iid) michael@0: { michael@0: if (iid.equals(Ci.nsIInputStreamCallback) || michael@0: iid.equals(Ci.nsIOutputStreamCallback) || michael@0: iid.equals(Ci.nsIRequest) || michael@0: iid.equals(Ci.nsISupports)) michael@0: { michael@0: return this; michael@0: } michael@0: michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: michael@0: // NSIINPUTSTREAMCALLBACK michael@0: michael@0: /** michael@0: * Receives a more-data-in-input notification and writes the corresponding michael@0: * data to the output. michael@0: * michael@0: * @param input : nsIAsyncInputStream michael@0: * the input stream on whose data we have been waiting michael@0: */ michael@0: onInputStreamReady: function(input) michael@0: { michael@0: if (this._source === null) michael@0: return; michael@0: michael@0: dumpn("*** onInputStreamReady"); michael@0: michael@0: // michael@0: // Ordinarily we'll read a non-zero amount of data from input, queue it up michael@0: // to be written and then wait for further callbacks. The complications in michael@0: // this method are the cases where we deviate from that behavior when errors michael@0: // occur or when copying is drawing to a finish. michael@0: // michael@0: // The edge cases when reading data are: michael@0: // michael@0: // Zero data is read michael@0: // If zero data was read, we're at the end of available data, so we can michael@0: // should stop reading and move on to writing out what we have (or, if michael@0: // we've already done that, onto notifying of completion). michael@0: // A stream-closed exception is thrown michael@0: // This is effectively a less kind version of zero data being read; the michael@0: // only difference is that we notify of completion with that result michael@0: // rather than with NS_OK. michael@0: // Some other exception is thrown michael@0: // This is the least kind result. We don't know what happened, so we michael@0: // act as though the stream closed except that we notify of completion michael@0: // with the result NS_ERROR_UNEXPECTED. michael@0: // michael@0: michael@0: var bytesWanted = 0, bytesConsumed = -1; michael@0: try michael@0: { michael@0: input = new BinaryInputStream(input); michael@0: michael@0: bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE); michael@0: dumpn("*** input wanted: " + bytesWanted); michael@0: michael@0: if (bytesWanted > 0) michael@0: { michael@0: var data = input.readByteArray(bytesWanted); michael@0: bytesConsumed = data.length; michael@0: this._pendingData.push(String.fromCharCode.apply(String, data)); michael@0: } michael@0: michael@0: dumpn("*** " + bytesConsumed + " bytes read"); michael@0: michael@0: // Handle the zero-data edge case in the same place as all other edge michael@0: // cases are handled. michael@0: if (bytesWanted === 0) michael@0: throw Cr.NS_BASE_STREAM_CLOSED; michael@0: } michael@0: catch (e) michael@0: { michael@0: if (streamClosed(e)) michael@0: { michael@0: dumpn("*** input stream closed"); michael@0: e = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED; michael@0: } michael@0: else michael@0: { michael@0: dumpn("!!! unexpected error reading from input, canceling: " + e); michael@0: e = Cr.NS_ERROR_UNEXPECTED; michael@0: } michael@0: michael@0: this._doneReadingSource(e); michael@0: return; michael@0: } michael@0: michael@0: var pendingData = this._pendingData; michael@0: michael@0: NS_ASSERT(bytesConsumed > 0); michael@0: NS_ASSERT(pendingData.length > 0, "no pending data somehow?"); michael@0: NS_ASSERT(pendingData[pendingData.length - 1].length > 0, michael@0: "buffered zero bytes of data?"); michael@0: michael@0: NS_ASSERT(this._source !== null); michael@0: michael@0: // Reading has gone great, and we've gotten data to write now. What if we michael@0: // don't have a place to write that data, because output went away just michael@0: // before this read? Drop everything on the floor, including new data, and michael@0: // cancel at this point. michael@0: if (this._sink === null) michael@0: { michael@0: pendingData.length = 0; michael@0: this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); michael@0: return; michael@0: } michael@0: michael@0: // Okay, we've read the data, and we know we have a place to write it. We michael@0: // need to queue up the data to be written, but *only* if none is queued michael@0: // already -- if data's already queued, the code that actually writes the michael@0: // data will make sure to wait on unconsumed pending data. michael@0: try michael@0: { michael@0: if (pendingData.length === 1) michael@0: this._waitToWriteData(); michael@0: } michael@0: catch (e) michael@0: { michael@0: dumpn("!!! error waiting to write data just read, swallowing and " + michael@0: "writing only what we already have: " + e); michael@0: this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); michael@0: return; michael@0: } michael@0: michael@0: // Whee! We successfully read some data, and it's successfully queued up to michael@0: // be written. All that remains now is to wait for more data to read. michael@0: try michael@0: { michael@0: this._waitToReadData(); michael@0: } michael@0: catch (e) michael@0: { michael@0: dumpn("!!! error waiting to read more data: " + e); michael@0: this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); michael@0: } michael@0: }, michael@0: michael@0: michael@0: // NSIOUTPUTSTREAMCALLBACK michael@0: michael@0: /** michael@0: * Callback when data may be written to the output stream without blocking, or michael@0: * when the output stream has been closed. michael@0: * michael@0: * @param output : nsIAsyncOutputStream michael@0: * the output stream on whose writability we've been waiting, also known as michael@0: * this._sink michael@0: */ michael@0: onOutputStreamReady: function(output) michael@0: { michael@0: if (this._sink === null) michael@0: return; michael@0: michael@0: dumpn("*** onOutputStreamReady"); michael@0: michael@0: var pendingData = this._pendingData; michael@0: if (pendingData.length === 0) michael@0: { michael@0: // There's no pending data to write. The only way this can happen is if michael@0: // we're waiting on the output stream's closure, so we can respond to a michael@0: // copying failure as quickly as possible (rather than waiting for data to michael@0: // be available to read and then fail to be copied). Therefore, we must michael@0: // be done now -- don't bother to attempt to write anything and wrap michael@0: // things up. michael@0: dumpn("!!! output stream closed prematurely, ending copy"); michael@0: michael@0: this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); michael@0: return; michael@0: } michael@0: michael@0: michael@0: NS_ASSERT(pendingData[0].length > 0, "queued up an empty quantum?"); michael@0: michael@0: // michael@0: // Write out the first pending quantum of data. The possible errors here michael@0: // are: michael@0: // michael@0: // The write might fail because we can't write that much data michael@0: // Okay, we've written what we can now, so re-queue what's left and michael@0: // finish writing it out later. michael@0: // The write failed because the stream was closed michael@0: // Discard pending data that we can no longer write, stop reading, and michael@0: // signal that copying finished. michael@0: // Some other error occurred. michael@0: // Same as if the stream were closed, but notify with the status michael@0: // NS_ERROR_UNEXPECTED so the observer knows something was wonky. michael@0: // michael@0: michael@0: try michael@0: { michael@0: var quantum = pendingData[0]; michael@0: michael@0: // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on michael@0: // undefined behavior! We're only using this because writeByteArray michael@0: // is unusably broken for asynchronous output streams; see bug 532834 michael@0: // for details. michael@0: var bytesWritten = output.write(quantum, quantum.length); michael@0: if (bytesWritten === quantum.length) michael@0: pendingData.shift(); michael@0: else michael@0: pendingData[0] = quantum.substring(bytesWritten); michael@0: michael@0: dumpn("*** wrote " + bytesWritten + " bytes of data"); michael@0: } michael@0: catch (e) michael@0: { michael@0: if (wouldBlock(e)) michael@0: { michael@0: NS_ASSERT(pendingData.length > 0, michael@0: "stream-blocking exception with no data to write?"); michael@0: NS_ASSERT(pendingData[0].length > 0, michael@0: "stream-blocking exception with empty quantum?"); michael@0: this._waitToWriteData(); michael@0: return; michael@0: } michael@0: michael@0: if (streamClosed(e)) michael@0: dumpn("!!! output stream prematurely closed, signaling error..."); michael@0: else michael@0: dumpn("!!! unknown error: " + e + ", quantum=" + quantum); michael@0: michael@0: this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); michael@0: return; michael@0: } michael@0: michael@0: // The day is ours! Quantum written, now let's see if we have more data michael@0: // still to write. michael@0: try michael@0: { michael@0: if (pendingData.length > 0) michael@0: { michael@0: this._waitToWriteData(); michael@0: return; michael@0: } michael@0: } michael@0: catch (e) michael@0: { michael@0: dumpn("!!! unexpected error waiting to write pending data: " + e); michael@0: this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); michael@0: return; michael@0: } michael@0: michael@0: // Okay, we have no more pending data to write -- but might we get more in michael@0: // the future? michael@0: if (this._source !== null) michael@0: { michael@0: /* michael@0: * If we might, then wait for the output stream to be closed. (We wait michael@0: * only for closure because we have no data to write -- and if we waited michael@0: * for a specific amount of data, we would get repeatedly notified for no michael@0: * reason if over time the output stream permitted more and more data to michael@0: * be written to it without blocking.) michael@0: */ michael@0: this._waitForSinkClosure(); michael@0: } michael@0: else michael@0: { michael@0: /* michael@0: * On the other hand, if we can't have more data because the input michael@0: * stream's gone away, then it's time to notify of copy completion. michael@0: * Victory! michael@0: */ michael@0: this._sink = null; michael@0: this._cancelOrDispatchCancelCallback(Cr.NS_OK); michael@0: } michael@0: }, michael@0: michael@0: michael@0: // NSIREQUEST michael@0: michael@0: /** Returns true if the cancel observer hasn't been notified yet. */ michael@0: isPending: function() michael@0: { michael@0: return !this._completed; michael@0: }, michael@0: michael@0: /** Not implemented, don't use! */ michael@0: suspend: notImplemented, michael@0: /** Not implemented, don't use! */ michael@0: resume: notImplemented, michael@0: michael@0: /** michael@0: * Cancels data reading from input, asynchronously writes out any pending michael@0: * data, and causes the observer to be notified with the given error code when michael@0: * all writing has finished. michael@0: * michael@0: * @param status : nsresult michael@0: * the status to pass to the observer when data copying has been canceled michael@0: */ michael@0: cancel: function(status) michael@0: { michael@0: dumpn("*** cancel(" + status.toString(16) + ")"); michael@0: michael@0: if (this._canceled) michael@0: { michael@0: dumpn("*** suppressing a late cancel"); michael@0: return; michael@0: } michael@0: michael@0: this._canceled = true; michael@0: this.status = status; michael@0: michael@0: // We could be in the middle of absolutely anything at this point. Both michael@0: // input and output might still be around, we might have pending data to michael@0: // write, and in general we know nothing about the state of the world. We michael@0: // therefore must assume everything's in progress and take everything to its michael@0: // final steady state (or so far as it can go before we need to finish michael@0: // writing out remaining data). michael@0: michael@0: this._doneReadingSource(status); michael@0: }, michael@0: michael@0: michael@0: // PRIVATE IMPLEMENTATION michael@0: michael@0: /** michael@0: * Stop reading input if we haven't already done so, passing e as the status michael@0: * when closing the stream, and kick off a copy-completion notice if no more michael@0: * data remains to be written. michael@0: * michael@0: * @param e : nsresult michael@0: * the status to be used when closing the input stream michael@0: */ michael@0: _doneReadingSource: function(e) michael@0: { michael@0: dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")"); michael@0: michael@0: this._finishSource(e); michael@0: if (this._pendingData.length === 0) michael@0: this._sink = null; michael@0: else michael@0: NS_ASSERT(this._sink !== null, "null output?"); michael@0: michael@0: // If we've written out all data read up to this point, then it's time to michael@0: // signal completion. michael@0: if (this._sink === null) michael@0: { michael@0: NS_ASSERT(this._pendingData.length === 0, "pending data still?"); michael@0: this._cancelOrDispatchCancelCallback(e); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Stop writing output if we haven't already done so, discard any data that michael@0: * remained to be sent, close off input if it wasn't already closed, and kick michael@0: * off a copy-completion notice. michael@0: * michael@0: * @param e : nsresult michael@0: * the status to be used when closing input if it wasn't already closed michael@0: */ michael@0: _doneWritingToSink: function(e) michael@0: { michael@0: dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")"); michael@0: michael@0: this._pendingData.length = 0; michael@0: this._sink = null; michael@0: this._doneReadingSource(e); michael@0: }, michael@0: michael@0: /** michael@0: * Completes processing of this copy: either by canceling the copy if it michael@0: * hasn't already been canceled using the provided status, or by dispatching michael@0: * the cancel callback event (with the originally provided status, of course) michael@0: * if it already has been canceled. michael@0: * michael@0: * @param status : nsresult michael@0: * the status code to use to cancel this, if this hasn't already been michael@0: * canceled michael@0: */ michael@0: _cancelOrDispatchCancelCallback: function(status) michael@0: { michael@0: dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")"); michael@0: michael@0: NS_ASSERT(this._source === null, "should have finished input"); michael@0: NS_ASSERT(this._sink === null, "should have finished output"); michael@0: NS_ASSERT(this._pendingData.length === 0, "should have no pending data"); michael@0: michael@0: if (!this._canceled) michael@0: { michael@0: this.cancel(status); michael@0: return; michael@0: } michael@0: michael@0: var self = this; michael@0: var event = michael@0: { michael@0: run: function() michael@0: { michael@0: dumpn("*** onStopRequest async callback"); michael@0: michael@0: self._completed = true; michael@0: try michael@0: { michael@0: self._observer.onStopRequest(self, self._context, self.status); michael@0: } michael@0: catch (e) michael@0: { michael@0: NS_ASSERT(false, michael@0: "how are we throwing an exception here? we control " + michael@0: "all the callers! " + e); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); michael@0: }, michael@0: michael@0: /** michael@0: * Kicks off another wait for more data to be available from the input stream. michael@0: */ michael@0: _waitToReadData: function() michael@0: { michael@0: dumpn("*** _waitToReadData"); michael@0: this._source.asyncWait(this, 0, Response.SEGMENT_SIZE, michael@0: gThreadManager.mainThread); michael@0: }, michael@0: michael@0: /** michael@0: * Kicks off another wait until data can be written to the output stream. michael@0: */ michael@0: _waitToWriteData: function() michael@0: { michael@0: dumpn("*** _waitToWriteData"); michael@0: michael@0: var pendingData = this._pendingData; michael@0: NS_ASSERT(pendingData.length > 0, "no pending data to write?"); michael@0: NS_ASSERT(pendingData[0].length > 0, "buffered an empty write?"); michael@0: michael@0: this._sink.asyncWait(this, 0, pendingData[0].length, michael@0: gThreadManager.mainThread); michael@0: }, michael@0: michael@0: /** michael@0: * Kicks off a wait for the sink to which data is being copied to be closed. michael@0: * We wait for stream closure when we don't have any data to be copied, rather michael@0: * than waiting to write a specific amount of data. We can't wait to write michael@0: * data because the sink might be infinitely writable, and if no data appears michael@0: * in the source for a long time we might have to spin quite a bit waiting to michael@0: * write, waiting to write again, &c. Waiting on stream closure instead means michael@0: * we'll get just one notification if the sink dies. Note that when data michael@0: * starts arriving from the sink we'll resume waiting for data to be written, michael@0: * dropping this closure-only callback entirely. michael@0: */ michael@0: _waitForSinkClosure: function() michael@0: { michael@0: dumpn("*** _waitForSinkClosure"); michael@0: michael@0: this._sink.asyncWait(this, Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY, 0, michael@0: gThreadManager.mainThread); michael@0: }, michael@0: michael@0: /** michael@0: * Closes input with the given status, if it hasn't already been closed; michael@0: * otherwise a no-op. michael@0: * michael@0: * @param status : nsresult michael@0: * status code use to close the source stream if necessary michael@0: */ michael@0: _finishSource: function(status) michael@0: { michael@0: dumpn("*** _finishSource(" + status.toString(16) + ")"); michael@0: michael@0: if (this._source !== null) michael@0: { michael@0: this._source.closeWithStatus(status); michael@0: this._source = null; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * A container for utility functions used with HTTP headers. michael@0: */ michael@0: const headerUtils = michael@0: { michael@0: /** michael@0: * Normalizes fieldName (by converting it to lowercase) and ensures it is a michael@0: * valid header field name (although not necessarily one specified in RFC michael@0: * 2616). michael@0: * michael@0: * @throws NS_ERROR_INVALID_ARG michael@0: * if fieldName does not match the field-name production in RFC 2616 michael@0: * @returns string michael@0: * fieldName converted to lowercase if it is a valid header, for characters michael@0: * where case conversion is possible michael@0: */ michael@0: normalizeFieldName: function(fieldName) michael@0: { michael@0: if (fieldName == "") michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: for (var i = 0, sz = fieldName.length; i < sz; i++) michael@0: { michael@0: if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)]) michael@0: { michael@0: dumpn(fieldName + " is not a valid header field name!"); michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: } michael@0: } michael@0: michael@0: return fieldName.toLowerCase(); michael@0: }, michael@0: michael@0: /** michael@0: * Ensures that fieldValue is a valid header field value (although not michael@0: * necessarily as specified in RFC 2616 if the corresponding field name is michael@0: * part of the HTTP protocol), normalizes the value if it is, and michael@0: * returns the normalized value. michael@0: * michael@0: * @param fieldValue : string michael@0: * a value to be normalized as an HTTP header field value michael@0: * @throws NS_ERROR_INVALID_ARG michael@0: * if fieldValue does not match the field-value production in RFC 2616 michael@0: * @returns string michael@0: * fieldValue as a normalized HTTP header field value michael@0: */ michael@0: normalizeFieldValue: function(fieldValue) michael@0: { michael@0: // field-value = *( field-content | LWS ) michael@0: // field-content = michael@0: // TEXT = michael@0: // LWS = [CRLF] 1*( SP | HT ) michael@0: // michael@0: // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) michael@0: // qdtext = > michael@0: // quoted-pair = "\" CHAR michael@0: // CHAR = michael@0: michael@0: // Any LWS that occurs between field-content MAY be replaced with a single michael@0: // SP before interpreting the field value or forwarding the message michael@0: // downstream (section 4.2); we replace 1*LWS with a single SP michael@0: var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " "); michael@0: michael@0: // remove leading/trailing LWS (which has been converted to SP) michael@0: val = val.replace(/^ +/, "").replace(/ +$/, ""); michael@0: michael@0: // that should have taken care of all CTLs, so val should contain no CTLs michael@0: for (var i = 0, len = val.length; i < len; i++) michael@0: if (isCTL(val.charCodeAt(i))) michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly michael@0: // normalize, however, so this can be construed as a tightening of the michael@0: // spec and not entirely as a bug michael@0: return val; michael@0: } michael@0: }; michael@0: michael@0: michael@0: michael@0: /** michael@0: * Converts the given string into a string which is safe for use in an HTML michael@0: * context. michael@0: * michael@0: * @param str : string michael@0: * the string to make HTML-safe michael@0: * @returns string michael@0: * an HTML-safe version of str michael@0: */ michael@0: function htmlEscape(str) michael@0: { michael@0: // this is naive, but it'll work michael@0: var s = ""; michael@0: for (var i = 0; i < str.length; i++) michael@0: s += "&#" + str.charCodeAt(i) + ";"; michael@0: return s; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Constructs an object representing an HTTP version (see section 3.1). michael@0: * michael@0: * @param versionString michael@0: * a string of the form "#.#", where # is an non-negative decimal integer with michael@0: * or without leading zeros michael@0: * @throws michael@0: * if versionString does not specify a valid HTTP version number michael@0: */ michael@0: function nsHttpVersion(versionString) michael@0: { michael@0: var matches = /^(\d+)\.(\d+)$/.exec(versionString); michael@0: if (!matches) michael@0: throw "Not a valid HTTP version!"; michael@0: michael@0: /** The major version number of this, as a number. */ michael@0: this.major = parseInt(matches[1], 10); michael@0: michael@0: /** The minor version number of this, as a number. */ michael@0: this.minor = parseInt(matches[2], 10); michael@0: michael@0: if (isNaN(this.major) || isNaN(this.minor) || michael@0: this.major < 0 || this.minor < 0) michael@0: throw "Not a valid HTTP version!"; michael@0: } michael@0: nsHttpVersion.prototype = michael@0: { michael@0: /** michael@0: * Returns the standard string representation of the HTTP version represented michael@0: * by this (e.g., "1.1"). michael@0: */ michael@0: toString: function () michael@0: { michael@0: return this.major + "." + this.minor; michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if this represents the same HTTP version as otherVersion, michael@0: * false otherwise. michael@0: * michael@0: * @param otherVersion : nsHttpVersion michael@0: * the version to compare against this michael@0: */ michael@0: equals: function (otherVersion) michael@0: { michael@0: return this.major == otherVersion.major && michael@0: this.minor == otherVersion.minor; michael@0: }, michael@0: michael@0: /** True if this >= otherVersion, false otherwise. */ michael@0: atLeast: function(otherVersion) michael@0: { michael@0: return this.major > otherVersion.major || michael@0: (this.major == otherVersion.major && michael@0: this.minor >= otherVersion.minor); michael@0: } michael@0: }; michael@0: michael@0: nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0"); michael@0: nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1"); michael@0: michael@0: michael@0: /** michael@0: * An object which stores HTTP headers for a request or response. michael@0: * michael@0: * Note that since headers are case-insensitive, this object converts headers to michael@0: * lowercase before storing them. This allows the getHeader and hasHeader michael@0: * methods to work correctly for any case of a header, but it means that the michael@0: * values returned by .enumerator may not be equal case-sensitively to the michael@0: * values passed to setHeader when adding headers to this. michael@0: */ michael@0: function nsHttpHeaders() michael@0: { michael@0: /** michael@0: * A hash of headers, with header field names as the keys and header field michael@0: * values as the values. Header field names are case-insensitive, but upon michael@0: * insertion here they are converted to lowercase. Header field values are michael@0: * normalized upon insertion to contain no leading or trailing whitespace. michael@0: * michael@0: * Note also that per RFC 2616, section 4.2, two headers with the same name in michael@0: * a message may be treated as one header with the same field name and a field michael@0: * value consisting of the separate field values joined together with a "," in michael@0: * their original order. This hash stores multiple headers with the same name michael@0: * in this manner. michael@0: */ michael@0: this._headers = {}; michael@0: } michael@0: nsHttpHeaders.prototype = michael@0: { michael@0: /** michael@0: * Sets the header represented by name and value in this. michael@0: * michael@0: * @param name : string michael@0: * the header name michael@0: * @param value : string michael@0: * the header value michael@0: * @throws NS_ERROR_INVALID_ARG michael@0: * if name or value is not a valid header component michael@0: */ michael@0: setHeader: function(fieldName, fieldValue, merge) michael@0: { michael@0: var name = headerUtils.normalizeFieldName(fieldName); michael@0: var value = headerUtils.normalizeFieldValue(fieldValue); michael@0: michael@0: // The following three headers are stored as arrays because their real-world michael@0: // syntax prevents joining individual headers into a single header using michael@0: // ",". See also michael@0: if (merge && name in this._headers) michael@0: { michael@0: if (name === "www-authenticate" || michael@0: name === "proxy-authenticate" || michael@0: name === "set-cookie") michael@0: { michael@0: this._headers[name].push(value); michael@0: } michael@0: else michael@0: { michael@0: this._headers[name][0] += "," + value; michael@0: NS_ASSERT(this._headers[name].length === 1, michael@0: "how'd a non-special header have multiple values?") michael@0: } michael@0: } michael@0: else michael@0: { michael@0: this._headers[name] = [value]; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns the value for the header specified by this. michael@0: * michael@0: * @throws NS_ERROR_INVALID_ARG michael@0: * if fieldName does not constitute a valid header field name michael@0: * @throws NS_ERROR_NOT_AVAILABLE michael@0: * if the given header does not exist in this michael@0: * @returns string michael@0: * the field value for the given header, possibly with non-semantic changes michael@0: * (i.e., leading/trailing whitespace stripped, whitespace runs replaced michael@0: * with spaces, etc.) at the option of the implementation; multiple michael@0: * instances of the header will be combined with a comma, except for michael@0: * the three headers noted in the description of getHeaderValues michael@0: */ michael@0: getHeader: function(fieldName) michael@0: { michael@0: return this.getHeaderValues(fieldName).join("\n"); michael@0: }, michael@0: michael@0: /** michael@0: * Returns the value for the header specified by fieldName as an array. michael@0: * michael@0: * @throws NS_ERROR_INVALID_ARG michael@0: * if fieldName does not constitute a valid header field name michael@0: * @throws NS_ERROR_NOT_AVAILABLE michael@0: * if the given header does not exist in this michael@0: * @returns [string] michael@0: * an array of all the header values in this for the given michael@0: * header name. Header values will generally be collapsed michael@0: * into a single header by joining all header values together michael@0: * with commas, but certain headers (Proxy-Authenticate, michael@0: * WWW-Authenticate, and Set-Cookie) violate the HTTP spec michael@0: * and cannot be collapsed in this manner. For these headers michael@0: * only, the returned array may contain multiple elements if michael@0: * that header has been added more than once. michael@0: */ michael@0: getHeaderValues: function(fieldName) michael@0: { michael@0: var name = headerUtils.normalizeFieldName(fieldName); michael@0: michael@0: if (name in this._headers) michael@0: return this._headers[name]; michael@0: else michael@0: throw Cr.NS_ERROR_NOT_AVAILABLE; michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if a header with the given field name exists in this, false michael@0: * otherwise. michael@0: * michael@0: * @param fieldName : string michael@0: * the field name whose existence is to be determined in this michael@0: * @throws NS_ERROR_INVALID_ARG michael@0: * if fieldName does not constitute a valid header field name michael@0: * @returns boolean michael@0: * true if the header's present, false otherwise michael@0: */ michael@0: hasHeader: function(fieldName) michael@0: { michael@0: var name = headerUtils.normalizeFieldName(fieldName); michael@0: return (name in this._headers); michael@0: }, michael@0: michael@0: /** michael@0: * Returns a new enumerator over the field names of the headers in this, as michael@0: * nsISupportsStrings. The names returned will be in lowercase, regardless of michael@0: * how they were input using setHeader (header names are case-insensitive per michael@0: * RFC 2616). michael@0: */ michael@0: get enumerator() michael@0: { michael@0: var headers = []; michael@0: for (var i in this._headers) michael@0: { michael@0: var supports = new SupportsString(); michael@0: supports.data = i; michael@0: headers.push(supports); michael@0: } michael@0: michael@0: return new nsSimpleEnumerator(headers); michael@0: } michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Constructs an nsISimpleEnumerator for the given array of items. michael@0: * michael@0: * @param items : Array michael@0: * the items, which must all implement nsISupports michael@0: */ michael@0: function nsSimpleEnumerator(items) michael@0: { michael@0: this._items = items; michael@0: this._nextIndex = 0; michael@0: } michael@0: nsSimpleEnumerator.prototype = michael@0: { michael@0: hasMoreElements: function() michael@0: { michael@0: return this._nextIndex < this._items.length; michael@0: }, michael@0: getNext: function() michael@0: { michael@0: if (!this.hasMoreElements()) michael@0: throw Cr.NS_ERROR_NOT_AVAILABLE; michael@0: michael@0: return this._items[this._nextIndex++]; michael@0: }, michael@0: QueryInterface: function(aIID) michael@0: { michael@0: if (Ci.nsISimpleEnumerator.equals(aIID) || michael@0: Ci.nsISupports.equals(aIID)) michael@0: return this; michael@0: michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * A representation of the data in an HTTP request. michael@0: * michael@0: * @param port : uint michael@0: * the port on which the server receiving this request runs michael@0: */ michael@0: function Request(port) michael@0: { michael@0: /** Method of this request, e.g. GET or POST. */ michael@0: this._method = ""; michael@0: michael@0: /** Path of the requested resource; empty paths are converted to '/'. */ michael@0: this._path = ""; michael@0: michael@0: /** Query string, if any, associated with this request (not including '?'). */ michael@0: this._queryString = ""; michael@0: michael@0: /** Scheme of requested resource, usually http, always lowercase. */ michael@0: this._scheme = "http"; michael@0: michael@0: /** Hostname on which the requested resource resides. */ michael@0: this._host = undefined; michael@0: michael@0: /** Port number over which the request was received. */ michael@0: this._port = port; michael@0: michael@0: var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null); michael@0: michael@0: /** Stream from which data in this request's body may be read. */ michael@0: this._bodyInputStream = bodyPipe.inputStream; michael@0: michael@0: /** Stream to which data in this request's body is written. */ michael@0: this._bodyOutputStream = bodyPipe.outputStream; michael@0: michael@0: /** michael@0: * The headers in this request. michael@0: */ michael@0: this._headers = new nsHttpHeaders(); michael@0: michael@0: /** michael@0: * For the addition of ad-hoc properties and new functionality without having michael@0: * to change nsIHttpRequest every time; currently lazily created, as its only michael@0: * use is in directory listings. michael@0: */ michael@0: this._bag = null; michael@0: } michael@0: Request.prototype = michael@0: { michael@0: // SERVER METADATA michael@0: michael@0: // michael@0: // see nsIHttpRequest.scheme michael@0: // michael@0: get scheme() michael@0: { michael@0: return this._scheme; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpRequest.host michael@0: // michael@0: get host() michael@0: { michael@0: return this._host; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpRequest.port michael@0: // michael@0: get port() michael@0: { michael@0: return this._port; michael@0: }, michael@0: michael@0: // REQUEST LINE michael@0: michael@0: // michael@0: // see nsIHttpRequest.method michael@0: // michael@0: get method() michael@0: { michael@0: return this._method; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpRequest.httpVersion michael@0: // michael@0: get httpVersion() michael@0: { michael@0: return this._httpVersion.toString(); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpRequest.path michael@0: // michael@0: get path() michael@0: { michael@0: return this._path; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpRequest.queryString michael@0: // michael@0: get queryString() michael@0: { michael@0: return this._queryString; michael@0: }, michael@0: michael@0: // HEADERS michael@0: michael@0: // michael@0: // see nsIHttpRequest.getHeader michael@0: // michael@0: getHeader: function(name) michael@0: { michael@0: return this._headers.getHeader(name); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpRequest.hasHeader michael@0: // michael@0: hasHeader: function(name) michael@0: { michael@0: return this._headers.hasHeader(name); michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpRequest.headers michael@0: // michael@0: get headers() michael@0: { michael@0: return this._headers.enumerator; michael@0: }, michael@0: michael@0: // michael@0: // see nsIPropertyBag.enumerator michael@0: // michael@0: get enumerator() michael@0: { michael@0: this._ensurePropertyBag(); michael@0: return this._bag.enumerator; michael@0: }, michael@0: michael@0: // michael@0: // see nsIHttpRequest.headers michael@0: // michael@0: get bodyInputStream() michael@0: { michael@0: return this._bodyInputStream; michael@0: }, michael@0: michael@0: // michael@0: // see nsIPropertyBag.getProperty michael@0: // michael@0: getProperty: function(name) michael@0: { michael@0: this._ensurePropertyBag(); michael@0: return this._bag.getProperty(name); michael@0: }, michael@0: michael@0: michael@0: // NSISUPPORTS michael@0: michael@0: // michael@0: // see nsISupports.QueryInterface michael@0: // michael@0: QueryInterface: function(iid) michael@0: { michael@0: if (iid.equals(Ci.nsIHttpRequest) || iid.equals(Ci.nsISupports)) michael@0: return this; michael@0: michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: michael@0: // PRIVATE IMPLEMENTATION michael@0: michael@0: /** Ensures a property bag has been created for ad-hoc behaviors. */ michael@0: _ensurePropertyBag: function() michael@0: { michael@0: if (!this._bag) michael@0: this._bag = new WritablePropertyBag(); michael@0: } michael@0: }; michael@0: michael@0: michael@0: // XPCOM trappings michael@0: if ("XPCOMUtils" in this && // Firefox 3.6 doesn't load XPCOMUtils in this scope for some reason... michael@0: "generateNSGetFactory" in XPCOMUtils) { michael@0: var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]); michael@0: } michael@0: michael@0: michael@0: michael@0: /** michael@0: * Creates a new HTTP server listening for loopback traffic on the given port, michael@0: * starts it, and runs the server until the server processes a shutdown request, michael@0: * spinning an event loop so that events posted by the server's socket are michael@0: * processed. michael@0: * michael@0: * This method is primarily intended for use in running this script from within michael@0: * xpcshell and running a functional HTTP server without having to deal with michael@0: * non-essential details. michael@0: * michael@0: * Note that running multiple servers using variants of this method probably michael@0: * doesn't work, simply due to how the internal event loop is spun and stopped. michael@0: * michael@0: * @note michael@0: * This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code); michael@0: * you should use this server as a component in Mozilla 1.8. michael@0: * @param port michael@0: * the port on which the server will run, or -1 if there exists no preference michael@0: * for a specific port; note that attempting to use some values for this michael@0: * parameter (particularly those below 1024) may cause this method to throw or michael@0: * may result in the server being prematurely shut down michael@0: * @param basePath michael@0: * a local directory from which requests will be served (i.e., if this is michael@0: * "/home/jwalden/" then a request to /index.html will load michael@0: * /home/jwalden/index.html); if this is omitted, only the default URLs in michael@0: * this server implementation will be functional michael@0: */ michael@0: function server(port, basePath) michael@0: { michael@0: if (basePath) michael@0: { michael@0: var lp = Cc["@mozilla.org/file/local;1"] michael@0: .createInstance(Ci.nsILocalFile); michael@0: lp.initWithPath(basePath); michael@0: } michael@0: michael@0: // if you're running this, you probably want to see debugging info michael@0: DEBUG = true; michael@0: michael@0: var srv = new nsHttpServer(); michael@0: if (lp) michael@0: srv.registerDirectory("/", lp); michael@0: srv.registerContentType("sjs", SJS_TYPE); michael@0: srv.start(port); michael@0: michael@0: var thread = gThreadManager.currentThread; michael@0: while (!srv.isStopped()) michael@0: thread.processNextEvent(true); michael@0: michael@0: // get rid of any pending requests michael@0: while (thread.hasPendingEvents()) michael@0: thread.processNextEvent(true); michael@0: michael@0: DEBUG = false; michael@0: } michael@0: michael@0: function startServerAsync(port, basePath) michael@0: { michael@0: if (basePath) michael@0: { michael@0: var lp = Cc["@mozilla.org/file/local;1"] michael@0: .createInstance(Ci.nsILocalFile); michael@0: lp.initWithPath(basePath); michael@0: } michael@0: michael@0: var srv = new nsHttpServer(); michael@0: if (lp) michael@0: srv.registerDirectory("/", lp); michael@0: srv.registerContentType("sjs", "sjs"); michael@0: srv.start(port); michael@0: return srv; michael@0: } michael@0: michael@0: exports.nsHttpServer = nsHttpServer; michael@0: exports.ScriptableInputStream = ScriptableInputStream; michael@0: exports.server = server; michael@0: exports.startServerAsync = startServerAsync;