UNPKG

slimerjs-firefox

Version:

This repo includes slimerjs as well as downloads a local copy of Firefox.

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