addon-sdk/source/lib/sdk/test/httpd.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2 * License, v. 2.0. If a copy of the MPL was not distributed with this
     3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 /*
     6 * An implementation of an HTTP server both as a loadable script and as an XPCOM
     7 * component. See the accompanying README file for user documentation on
     8 * httpd.js.
     9 */
    11 module.metadata = {
    12   "stability": "experimental"
    13 };
    15 const { components, CC, Cc, Ci, Cr, Cu } = require("chrome");
    16 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    19 const PR_UINT32_MAX = Math.pow(2, 32) - 1;
    21 /** True if debugging output is enabled, false otherwise. */
    22 var DEBUG = false; // non-const *only* so tweakable in server tests
    24 /** True if debugging output should be timestamped. */
    25 var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests
    27 var gGlobalObject = Cc["@mozilla.org/systemprincipal;1"].createInstance();
    29 /**
    30 * Asserts that the given condition holds. If it doesn't, the given message is
    31 * dumped, a stack trace is printed, and an exception is thrown to attempt to
    32 * stop execution (which unfortunately must rely upon the exception not being
    33 * accidentally swallowed by the code that uses it).
    34 */
    35 function NS_ASSERT(cond, msg)
    36 {
    37   if (DEBUG && !cond)
    38   {
    39     dumpn("###!!!");
    40     dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!"));
    41     dumpn("###!!! Stack follows:");
    43     var stack = new Error().stack.split(/\n/);
    44     dumpn(stack.map(function(val) { return "###!!! " + val; }).join("\n"));
    46     throw Cr.NS_ERROR_ABORT;
    47   }
    48 }
    50 /** Constructs an HTTP error object. */
    51 function HttpError(code, description)
    52 {
    53   this.code = code;
    54   this.description = description;
    55 }
    56 HttpError.prototype =
    57 {
    58   toString: function()
    59   {
    60     return this.code + " " + this.description;
    61   }
    62 };
    64 /**
    65 * Errors thrown to trigger specific HTTP server responses.
    66 */
    67 const HTTP_400 = new HttpError(400, "Bad Request");
    68 const HTTP_401 = new HttpError(401, "Unauthorized");
    69 const HTTP_402 = new HttpError(402, "Payment Required");
    70 const HTTP_403 = new HttpError(403, "Forbidden");
    71 const HTTP_404 = new HttpError(404, "Not Found");
    72 const HTTP_405 = new HttpError(405, "Method Not Allowed");
    73 const HTTP_406 = new HttpError(406, "Not Acceptable");
    74 const HTTP_407 = new HttpError(407, "Proxy Authentication Required");
    75 const HTTP_408 = new HttpError(408, "Request Timeout");
    76 const HTTP_409 = new HttpError(409, "Conflict");
    77 const HTTP_410 = new HttpError(410, "Gone");
    78 const HTTP_411 = new HttpError(411, "Length Required");
    79 const HTTP_412 = new HttpError(412, "Precondition Failed");
    80 const HTTP_413 = new HttpError(413, "Request Entity Too Large");
    81 const HTTP_414 = new HttpError(414, "Request-URI Too Long");
    82 const HTTP_415 = new HttpError(415, "Unsupported Media Type");
    83 const HTTP_417 = new HttpError(417, "Expectation Failed");
    85 const HTTP_500 = new HttpError(500, "Internal Server Error");
    86 const HTTP_501 = new HttpError(501, "Not Implemented");
    87 const HTTP_502 = new HttpError(502, "Bad Gateway");
    88 const HTTP_503 = new HttpError(503, "Service Unavailable");
    89 const HTTP_504 = new HttpError(504, "Gateway Timeout");
    90 const HTTP_505 = new HttpError(505, "HTTP Version Not Supported");
    92 /** Creates a hash with fields corresponding to the values in arr. */
    93 function array2obj(arr)
    94 {
    95   var obj = {};
    96   for (var i = 0; i < arr.length; i++)
    97     obj[arr[i]] = arr[i];
    98   return obj;
    99 }
   101 /** Returns an array of the integers x through y, inclusive. */
   102 function range(x, y)
   103 {
   104   var arr = [];
   105   for (var i = x; i <= y; i++)
   106     arr.push(i);
   107   return arr;
   108 }
   110 /** An object (hash) whose fields are the numbers of all HTTP error codes. */
   111 const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505)));
   114 /**
   115 * The character used to distinguish hidden files from non-hidden files, a la
   116 * the leading dot in Apache. Since that mechanism also hides files from
   117 * easy display in LXR, ls output, etc. however, we choose instead to use a
   118 * suffix character. If a requested file ends with it, we append another
   119 * when getting the file on the server. If it doesn't, we just look up that
   120 * file. Therefore, any file whose name ends with exactly one of the character
   121 * is "hidden" and available for use by the server.
   122 */
   123 const HIDDEN_CHAR = "^";
   125 /**
   126 * The file name suffix indicating the file containing overridden headers for
   127 * a requested file.
   128 */
   129 const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR;
   131 /** Type used to denote SJS scripts for CGI-like functionality. */
   132 const SJS_TYPE = "sjs";
   134 /** Base for relative timestamps produced by dumpn(). */
   135 var firstStamp = 0;
   137 /** dump(str) with a trailing "\n" -- only outputs if DEBUG. */
   138 function dumpn(str)
   139 {
   140   if (DEBUG)
   141   {
   142     var prefix = "HTTPD-INFO | ";
   143     if (DEBUG_TIMESTAMP)
   144     {
   145       if (firstStamp === 0)
   146         firstStamp = Date.now();
   148       var elapsed = Date.now() - firstStamp; // milliseconds
   149       var min = Math.floor(elapsed / 60000);
   150       var sec = (elapsed % 60000) / 1000;
   152       if (sec < 10)
   153         prefix += min + ":0" + sec.toFixed(3) + " | ";
   154       else
   155         prefix += min + ":" + sec.toFixed(3) + " | ";
   156     }
   158     dump(prefix + str + "\n");
   159   }
   160 }
   162 /** Dumps the current JS stack if DEBUG. */
   163 function dumpStack()
   164 {
   165   // peel off the frames for dumpStack() and Error()
   166   var stack = new Error().stack.split(/\n/).slice(2);
   167   stack.forEach(dumpn);
   168 }
   171 /** The XPCOM thread manager. */
   172 var gThreadManager = null;
   174 /** The XPCOM prefs service. */
   175 var gRootPrefBranch = null;
   176 function getRootPrefBranch()
   177 {
   178   if (!gRootPrefBranch)
   179   {
   180     gRootPrefBranch = Cc["@mozilla.org/preferences-service;1"]
   181                         .getService(Ci.nsIPrefBranch);
   182   }
   183   return gRootPrefBranch;
   184 }
   186 /**
   187 * JavaScript constructors for commonly-used classes; precreating these is a
   188 * speedup over doing the same from base principles. See the docs at
   189 * http://developer.mozilla.org/en/docs/components.Constructor for details.
   190 */
   191 const ServerSocket = CC("@mozilla.org/network/server-socket;1",
   192                         "nsIServerSocket",
   193                         "init");
   194 const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
   195                                  "nsIScriptableInputStream",
   196                                  "init");
   197 const Pipe = CC("@mozilla.org/pipe;1",
   198                 "nsIPipe",
   199                 "init");
   200 const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
   201                            "nsIFileInputStream",
   202                            "init");
   203 const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1",
   204                                 "nsIConverterInputStream",
   205                                 "init");
   206 const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1",
   207                                "nsIWritablePropertyBag2");
   208 const SupportsString = CC("@mozilla.org/supports-string;1",
   209                           "nsISupportsString");
   211 /* These two are non-const only so a test can overwrite them. */
   212 var BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
   213                            "nsIBinaryInputStream",
   214                            "setInputStream");
   215 var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
   216                             "nsIBinaryOutputStream",
   217                             "setOutputStream");
   219 /**
   220 * Returns the RFC 822/1123 representation of a date.
   221 *
   222 * @param date : Number
   223 * the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT
   224 * @returns string
   225 * the representation of the given date
   226 */
   227 function toDateString(date)
   228 {
   229   //
   230   // rfc1123-date = wkday "," SP date1 SP time SP "GMT"
   231   // date1 = 2DIGIT SP month SP 4DIGIT
   232   // ; day month year (e.g., 02 Jun 1982)
   233   // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
   234   // ; 00:00:00 - 23:59:59
   235   // wkday = "Mon" | "Tue" | "Wed"
   236   // | "Thu" | "Fri" | "Sat" | "Sun"
   237   // month = "Jan" | "Feb" | "Mar" | "Apr"
   238   // | "May" | "Jun" | "Jul" | "Aug"
   239   // | "Sep" | "Oct" | "Nov" | "Dec"
   240   //
   242   const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
   243   const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
   244                         "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
   246   /**
   247 * Processes a date and returns the encoded UTC time as a string according to
   248 * the format specified in RFC 2616.
   249 *
   250 * @param date : Date
   251 * the date to process
   252 * @returns string
   253 * a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
   254 */
   255   function toTime(date)
   256   {
   257     var hrs = date.getUTCHours();
   258     var rv = (hrs < 10) ? "0" + hrs : hrs;
   260     var mins = date.getUTCMinutes();
   261     rv += ":";
   262     rv += (mins < 10) ? "0" + mins : mins;
   264     var secs = date.getUTCSeconds();
   265     rv += ":";
   266     rv += (secs < 10) ? "0" + secs : secs;
   268     return rv;
   269   }
   271   /**
   272 * Processes a date and returns the encoded UTC date as a string according to
   273 * the date1 format specified in RFC 2616.
   274 *
   275 * @param date : Date
   276 * the date to process
   277 * @returns string
   278 * a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
   279 */
   280   function toDate1(date)
   281   {
   282     var day = date.getUTCDate();
   283     var month = date.getUTCMonth();
   284     var year = date.getUTCFullYear();
   286     var rv = (day < 10) ? "0" + day : day;
   287     rv += " " + monthStrings[month];
   288     rv += " " + year;
   290     return rv;
   291   }
   293   date = new Date(date);
   295   const fmtString = "%wkday%, %date1% %time% GMT";
   296   var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]);
   297   rv = rv.replace("%time%", toTime(date));
   298   return rv.replace("%date1%", toDate1(date));
   299 }
   301 /**
   302 * Prints out a human-readable representation of the object o and its fields,
   303 * omitting those whose names begin with "_" if showMembers != true (to ignore
   304 * "private" properties exposed via getters/setters).
   305 */
   306 function printObj(o, showMembers)
   307 {
   308   var s = "******************************\n";
   309   s += "o = {\n";
   310   for (var i in o)
   311   {
   312     if (typeof(i) != "string" ||
   313         (showMembers || (i.length > 0 && i[0] != "_")))
   314       s+= " " + i + ": " + o[i] + ",\n";
   315   }
   316   s += " };\n";
   317   s += "******************************";
   318   dumpn(s);
   319 }
   321 /**
   322 * Instantiates a new HTTP server.
   323 */
   324 function nsHttpServer()
   325 {
   326   if (!gThreadManager)
   327     gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService();
   329   /** The port on which this server listens. */
   330   this._port = undefined;
   332   /** The socket associated with this. */
   333   this._socket = null;
   335   /** The handler used to process requests to this server. */
   336   this._handler = new ServerHandler(this);
   338   /** Naming information for this server. */
   339   this._identity = new ServerIdentity();
   341   /**
   342 * Indicates when the server is to be shut down at the end of the request.
   343 */
   344   this._doQuit = false;
   346   /**
   347 * True if the socket in this is closed (and closure notifications have been
   348 * sent and processed if the socket was ever opened), false otherwise.
   349 */
   350   this._socketClosed = true;
   352   /**
   353 * Used for tracking existing connections and ensuring that all connections
   354 * are properly cleaned up before server shutdown; increases by 1 for every
   355 * new incoming connection.
   356 */
   357   this._connectionGen = 0;
   359   /**
   360 * Hash of all open connections, indexed by connection number at time of
   361 * creation.
   362 */
   363   this._connections = {};
   364 }
   365 nsHttpServer.prototype =
   366 {
   367   classID: components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"),
   369   // NSISERVERSOCKETLISTENER
   371   /**
   372 * Processes an incoming request coming in on the given socket and contained
   373 * in the given transport.
   374 *
   375 * @param socket : nsIServerSocket
   376 * the socket through which the request was served
   377 * @param trans : nsISocketTransport
   378 * the transport for the request/response
   379 * @see nsIServerSocketListener.onSocketAccepted
   380 */
   381   onSocketAccepted: function(socket, trans)
   382   {
   383     dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")");
   385     dumpn(">>> new connection on " + trans.host + ":" + trans.port);
   387     const SEGMENT_SIZE = 8192;
   388     const SEGMENT_COUNT = 1024;
   389     try
   390     {
   391       var input = trans.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
   392                        .QueryInterface(Ci.nsIAsyncInputStream);
   393       var output = trans.openOutputStream(0, 0, 0);
   394     }
   395     catch (e)
   396     {
   397       dumpn("*** error opening transport streams: " + e);
   398       trans.close(Cr.NS_BINDING_ABORTED);
   399       return;
   400     }
   402     var connectionNumber = ++this._connectionGen;
   404     try
   405     {
   406       var conn = new Connection(input, output, this, socket.port, trans.port,
   407                                 connectionNumber);
   408       var reader = new RequestReader(conn);
   410       // XXX add request timeout functionality here!
   412       // Note: must use main thread here, or we might get a GC that will cause
   413       // threadsafety assertions. We really need to fix XPConnect so that
   414       // you can actually do things in multi-threaded JS. :-(
   415       input.asyncWait(reader, 0, 0, gThreadManager.mainThread);
   416     }
   417     catch (e)
   418     {
   419       // Assume this connection can't be salvaged and bail on it completely;
   420       // don't attempt to close it so that we can assert that any connection
   421       // being closed is in this._connections.
   422       dumpn("*** error in initial request-processing stages: " + e);
   423       trans.close(Cr.NS_BINDING_ABORTED);
   424       return;
   425     }
   427     this._connections[connectionNumber] = conn;
   428     dumpn("*** starting connection " + connectionNumber);
   429   },
   431   /**
   432 * Called when the socket associated with this is closed.
   433 *
   434 * @param socket : nsIServerSocket
   435 * the socket being closed
   436 * @param status : nsresult
   437 * the reason the socket stopped listening (NS_BINDING_ABORTED if the server
   438 * was stopped using nsIHttpServer.stop)
   439 * @see nsIServerSocketListener.onStopListening
   440 */
   441   onStopListening: function(socket, status)
   442   {
   443     dumpn(">>> shutting down server on port " + socket.port);
   444     this._socketClosed = true;
   445     if (!this._hasOpenConnections())
   446     {
   447       dumpn("*** no open connections, notifying async from onStopListening");
   449       // Notify asynchronously so that any pending teardown in stop() has a
   450       // chance to run first.
   451       var self = this;
   452       var stopEvent =
   453         {
   454           run: function()
   455           {
   456             dumpn("*** _notifyStopped async callback");
   457             self._notifyStopped();
   458           }
   459         };
   460       gThreadManager.currentThread
   461                     .dispatch(stopEvent, Ci.nsIThread.DISPATCH_NORMAL);
   462     }
   463   },
   465   // NSIHTTPSERVER
   467   //
   468   // see nsIHttpServer.start
   469   //
   470   start: function(port)
   471   {
   472     this._start(port, "localhost")
   473   },
   475   _start: function(port, host)
   476   {
   477     if (this._socket)
   478       throw Cr.NS_ERROR_ALREADY_INITIALIZED;
   480     this._port = port;
   481     this._doQuit = this._socketClosed = false;
   483     this._host = host;
   485     // The listen queue needs to be long enough to handle
   486     // network.http.max-persistent-connections-per-server concurrent connections,
   487     // plus a safety margin in case some other process is talking to
   488     // the server as well.
   489     var prefs = getRootPrefBranch();
   490     var maxConnections;
   491     try {
   492       // Bug 776860: The original pref was removed in favor of this new one:
   493       maxConnections = prefs.getIntPref("network.http.max-persistent-connections-per-server") + 5;
   494     }
   495     catch(e) {
   496       maxConnections = prefs.getIntPref("network.http.max-connections-per-server") + 5;
   497     }
   499     try
   500     {
   501       var loopback = true;
   502       if (this._host != "127.0.0.1" && this._host != "localhost") {
   503         var loopback = false;
   504       }
   506       var socket = new ServerSocket(this._port,
   507                                     loopback, // true = localhost, false = everybody
   508                                     maxConnections);
   509       dumpn(">>> listening on port " + socket.port + ", " + maxConnections +
   510             " pending connections");
   511       socket.asyncListen(this);
   512       this._identity._initialize(socket.port, host, true);
   513       this._socket = socket;
   514     }
   515     catch (e)
   516     {
   517       dumpn("!!! could not start server on port " + port + ": " + e);
   518       throw Cr.NS_ERROR_NOT_AVAILABLE;
   519     }
   520   },
   522   //
   523   // see nsIHttpServer.stop
   524   //
   525   stop: function(callback)
   526   {
   527     if (!callback)
   528       throw Cr.NS_ERROR_NULL_POINTER;
   529     if (!this._socket)
   530       throw Cr.NS_ERROR_UNEXPECTED;
   532     this._stopCallback = typeof callback === "function"
   533                        ? callback
   534                        : function() { callback.onStopped(); };
   536     dumpn(">>> stopping listening on port " + this._socket.port);
   537     this._socket.close();
   538     this._socket = null;
   540     // We can't have this identity any more, and the port on which we're running
   541     // this server now could be meaningless the next time around.
   542     this._identity._teardown();
   544     this._doQuit = false;
   546     // socket-close notification and pending request completion happen async
   547   },
   549   //
   550   // see nsIHttpServer.registerFile
   551   //
   552   registerFile: function(path, file)
   553   {
   554     if (file && (!file.exists() || file.isDirectory()))
   555       throw Cr.NS_ERROR_INVALID_ARG;
   557     this._handler.registerFile(path, file);
   558   },
   560   //
   561   // see nsIHttpServer.registerDirectory
   562   //
   563   registerDirectory: function(path, directory)
   564   {
   565     // XXX true path validation!
   566     if (path.charAt(0) != "/" ||
   567         path.charAt(path.length - 1) != "/" ||
   568         (directory &&
   569          (!directory.exists() || !directory.isDirectory())))
   570       throw Cr.NS_ERROR_INVALID_ARG;
   572     // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping
   573     // exists!
   575     this._handler.registerDirectory(path, directory);
   576   },
   578   //
   579   // see nsIHttpServer.registerPathHandler
   580   //
   581   registerPathHandler: function(path, handler)
   582   {
   583     this._handler.registerPathHandler(path, handler);
   584   },
   586   //
   587   // see nsIHttpServer.registerPrefixHandler
   588   //
   589   registerPrefixHandler: function(prefix, handler)
   590   {
   591     this._handler.registerPrefixHandler(prefix, handler);
   592   },
   594   //
   595   // see nsIHttpServer.registerErrorHandler
   596   //
   597   registerErrorHandler: function(code, handler)
   598   {
   599     this._handler.registerErrorHandler(code, handler);
   600   },
   602   //
   603   // see nsIHttpServer.setIndexHandler
   604   //
   605   setIndexHandler: function(handler)
   606   {
   607     this._handler.setIndexHandler(handler);
   608   },
   610   //
   611   // see nsIHttpServer.registerContentType
   612   //
   613   registerContentType: function(ext, type)
   614   {
   615     this._handler.registerContentType(ext, type);
   616   },
   618   //
   619   // see nsIHttpServer.serverIdentity
   620   //
   621   get identity()
   622   {
   623     return this._identity;
   624   },
   626   //
   627   // see nsIHttpServer.getState
   628   //
   629   getState: function(path, k)
   630   {
   631     return this._handler._getState(path, k);
   632   },
   634   //
   635   // see nsIHttpServer.setState
   636   //
   637   setState: function(path, k, v)
   638   {
   639     return this._handler._setState(path, k, v);
   640   },
   642   //
   643   // see nsIHttpServer.getSharedState
   644   //
   645   getSharedState: function(k)
   646   {
   647     return this._handler._getSharedState(k);
   648   },
   650   //
   651   // see nsIHttpServer.setSharedState
   652   //
   653   setSharedState: function(k, v)
   654   {
   655     return this._handler._setSharedState(k, v);
   656   },
   658   //
   659   // see nsIHttpServer.getObjectState
   660   //
   661   getObjectState: function(k)
   662   {
   663     return this._handler._getObjectState(k);
   664   },
   666   //
   667   // see nsIHttpServer.setObjectState
   668   //
   669   setObjectState: function(k, v)
   670   {
   671     return this._handler._setObjectState(k, v);
   672   },
   675   // NSISUPPORTS
   677   //
   678   // see nsISupports.QueryInterface
   679   //
   680   QueryInterface: function(iid)
   681   {
   682     if (iid.equals(Ci.nsIServerSocketListener) || iid.equals(Ci.nsISupports))
   683       return this;
   685     throw Cr.NS_ERROR_NO_INTERFACE;
   686   },
   689   // NON-XPCOM PUBLIC API
   691   /**
   692 * Returns true iff this server is not running (and is not in the process of
   693 * serving any requests still to be processed when the server was last
   694 * stopped after being run).
   695 */
   696   isStopped: function()
   697   {
   698     return this._socketClosed && !this._hasOpenConnections();
   699   },
   701   // PRIVATE IMPLEMENTATION
   703   /** True if this server has any open connections to it, false otherwise. */
   704   _hasOpenConnections: function()
   705   {
   706     //
   707     // If we have any open connections, they're tracked as numeric properties on
   708     // |this._connections|. The non-standard __count__ property could be used
   709     // to check whether there are any properties, but standard-wise, even
   710     // looking forward to ES5, there's no less ugly yet still O(1) way to do
   711     // this.
   712     //
   713     for (var n in this._connections)
   714       return true;
   715     return false;
   716   },
   718   /** Calls the server-stopped callback provided when stop() was called. */
   719   _notifyStopped: function()
   720   {
   721     NS_ASSERT(this._stopCallback !== null, "double-notifying?");
   722     NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now");
   724     //
   725     // NB: We have to grab this now, null out the member, *then* call the
   726     // callback here, or otherwise the callback could (indirectly) futz with
   727     // this._stopCallback by starting and immediately stopping this, at
   728     // which point we'd be nulling out a field we no longer have a right to
   729     // modify.
   730     //
   731     var callback = this._stopCallback;
   732     this._stopCallback = null;
   733     try
   734     {
   735       callback();
   736     }
   737     catch (e)
   738     {
   739       // not throwing because this is specified as being usually (but not
   740       // always) asynchronous
   741       dump("!!! error running onStopped callback: " + e + "\n");
   742     }
   743   },
   745   /**
   746 * Notifies this server that the given connection has been closed.
   747 *
   748 * @param connection : Connection
   749 * the connection that was closed
   750 */
   751   _connectionClosed: function(connection)
   752   {
   753     NS_ASSERT(connection.number in this._connections,
   754               "closing a connection " + this + " that we never added to the " +
   755               "set of open connections?");
   756     NS_ASSERT(this._connections[connection.number] === connection,
   757               "connection number mismatch? " +
   758               this._connections[connection.number]);
   759     delete this._connections[connection.number];
   761     // Fire a pending server-stopped notification if it's our responsibility.
   762     if (!this._hasOpenConnections() && this._socketClosed)
   763       this._notifyStopped();
   764   },
   766   /**
   767 * Requests that the server be shut down when possible.
   768 */
   769   _requestQuit: function()
   770   {
   771     dumpn(">>> requesting a quit");
   772     dumpStack();
   773     this._doQuit = true;
   774   }
   775 };
   778 //
   779 // RFC 2396 section 3.2.2:
   780 //
   781 // host = hostname | IPv4address
   782 // hostname = *( domainlabel "." ) toplabel [ "." ]
   783 // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
   784 // toplabel = alpha | alpha *( alphanum | "-" ) alphanum
   785 // IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit
   786 //
   788 const HOST_REGEX =
   789   new RegExp("^(?:" +
   790                // *( domainlabel "." )
   791                "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" +
   792                // toplabel
   793                "[a-z](?:[a-z0-9-]*[a-z0-9])?" +
   794              "|" +
   795                // IPv4 address
   796                "\\d+\\.\\d+\\.\\d+\\.\\d+" +
   797              ")$",
   798              "i");
   801 /**
   802 * Represents the identity of a server. An identity consists of a set of
   803 * (scheme, host, port) tuples denoted as locations (allowing a single server to
   804 * serve multiple sites or to be used behind both HTTP and HTTPS proxies for any
   805 * host/port). Any incoming request must be to one of these locations, or it
   806 * will be rejected with an HTTP 400 error. One location, denoted as the
   807 * primary location, is the location assigned in contexts where a location
   808 * cannot otherwise be endogenously derived, such as for HTTP/1.0 requests.
   809 *
   810 * A single identity may contain at most one location per unique host/port pair;
   811 * other than that, no restrictions are placed upon what locations may
   812 * constitute an identity.
   813 */
   814 function ServerIdentity()
   815 {
   816   /** The scheme of the primary location. */
   817   this._primaryScheme = "http";
   819   /** The hostname of the primary location. */
   820   this._primaryHost = "127.0.0.1"
   822   /** The port number of the primary location. */
   823   this._primaryPort = -1;
   825   /**
   826 * The current port number for the corresponding server, stored so that a new
   827 * primary location can always be set if the current one is removed.
   828 */
   829   this._defaultPort = -1;
   831   /**
   832 * Maps hosts to maps of ports to schemes, e.g. the following would represent
   833 * https://example.com:789/ and http://example.org/:
   834 *
   835 * {
   836 * "xexample.com": { 789: "https" },
   837 * "xexample.org": { 80: "http" }
   838 * }
   839 *
   840 * Note the "x" prefix on hostnames, which prevents collisions with special
   841 * JS names like "prototype".
   842 */
   843   this._locations = { "xlocalhost": {} };
   844 }
   845 ServerIdentity.prototype =
   846 {
   847   // NSIHTTPSERVERIDENTITY
   849   //
   850   // see nsIHttpServerIdentity.primaryScheme
   851   //
   852   get primaryScheme()
   853   {
   854     if (this._primaryPort === -1)
   855       throw Cr.NS_ERROR_NOT_INITIALIZED;
   856     return this._primaryScheme;
   857   },
   859   //
   860   // see nsIHttpServerIdentity.primaryHost
   861   //
   862   get primaryHost()
   863   {
   864     if (this._primaryPort === -1)
   865       throw Cr.NS_ERROR_NOT_INITIALIZED;
   866     return this._primaryHost;
   867   },
   869   //
   870   // see nsIHttpServerIdentity.primaryPort
   871   //
   872   get primaryPort()
   873   {
   874     if (this._primaryPort === -1)
   875       throw Cr.NS_ERROR_NOT_INITIALIZED;
   876     return this._primaryPort;
   877   },
   879   //
   880   // see nsIHttpServerIdentity.add
   881   //
   882   add: function(scheme, host, port)
   883   {
   884     this._validate(scheme, host, port);
   886     var entry = this._locations["x" + host];
   887     if (!entry)
   888       this._locations["x" + host] = entry = {};
   890     entry[port] = scheme;
   891   },
   893   //
   894   // see nsIHttpServerIdentity.remove
   895   //
   896   remove: function(scheme, host, port)
   897   {
   898     this._validate(scheme, host, port);
   900     var entry = this._locations["x" + host];
   901     if (!entry)
   902       return false;
   904     var present = port in entry;
   905     delete entry[port];
   907     if (this._primaryScheme == scheme &&
   908         this._primaryHost == host &&
   909         this._primaryPort == port &&
   910         this._defaultPort !== -1)
   911     {
   912       // Always keep at least one identity in existence at any time, unless
   913       // we're in the process of shutting down (the last condition above).
   914       this._primaryPort = -1;
   915       this._initialize(this._defaultPort, host, false);
   916     }
   918     return present;
   919   },
   921   //
   922   // see nsIHttpServerIdentity.has
   923   //
   924   has: function(scheme, host, port)
   925   {
   926     this._validate(scheme, host, port);
   928     return "x" + host in this._locations &&
   929            scheme === this._locations["x" + host][port];
   930   },
   932   //
   933   // see nsIHttpServerIdentity.has
   934   //
   935   getScheme: function(host, port)
   936   {
   937     this._validate("http", host, port);
   939     var entry = this._locations["x" + host];
   940     if (!entry)
   941       return "";
   943     return entry[port] || "";
   944   },
   946   //
   947   // see nsIHttpServerIdentity.setPrimary
   948   //
   949   setPrimary: function(scheme, host, port)
   950   {
   951     this._validate(scheme, host, port);
   953     this.add(scheme, host, port);
   955     this._primaryScheme = scheme;
   956     this._primaryHost = host;
   957     this._primaryPort = port;
   958   },
   961   // NSISUPPORTS
   963   //
   964   // see nsISupports.QueryInterface
   965   //
   966   QueryInterface: function(iid)
   967   {
   968     if (iid.equals(Ci.nsIHttpServerIdentity) || iid.equals(Ci.nsISupports))
   969       return this;
   971     throw Cr.NS_ERROR_NO_INTERFACE;
   972   },
   975   // PRIVATE IMPLEMENTATION
   977   /**
   978 * Initializes the primary name for the corresponding server, based on the
   979 * provided port number.
   980 */
   981   _initialize: function(port, host, addSecondaryDefault)
   982   {
   983     this._host = host;
   984     if (this._primaryPort !== -1)
   985       this.add("http", host, port);
   986     else
   987       this.setPrimary("http", "localhost", port);
   988     this._defaultPort = port;
   990     // Only add this if we're being called at server startup
   991     if (addSecondaryDefault && host != "127.0.0.1")
   992       this.add("http", "127.0.0.1", port);
   993   },
   995   /**
   996 * Called at server shutdown time, unsets the primary location only if it was
   997 * the default-assigned location and removes the default location from the
   998 * set of locations used.
   999 */
  1000   _teardown: function()
  1002     if (this._host != "127.0.0.1") {
  1003       // Not the default primary location, nothing special to do here
  1004       this.remove("http", "127.0.0.1", this._defaultPort);
  1007     // This is a *very* tricky bit of reasoning here; make absolutely sure the
  1008     // tests for this code pass before you commit changes to it.
  1009     if (this._primaryScheme == "http" &&
  1010         this._primaryHost == this._host &&
  1011         this._primaryPort == this._defaultPort)
  1013       // Make sure we don't trigger the readding logic in .remove(), then remove
  1014       // the default location.
  1015       var port = this._defaultPort;
  1016       this._defaultPort = -1;
  1017       this.remove("http", this._host, port);
  1019       // Ensure a server start triggers the setPrimary() path in ._initialize()
  1020       this._primaryPort = -1;
  1022     else
  1024       // No reason not to remove directly as it's not our primary location
  1025       this.remove("http", this._host, this._defaultPort);
  1027   },
  1029   /**
  1030 * Ensures scheme, host, and port are all valid with respect to RFC 2396.
  1032 * @throws NS_ERROR_ILLEGAL_VALUE
  1033 * if any argument doesn't match the corresponding production
  1034 */
  1035   _validate: function(scheme, host, port)
  1037     if (scheme !== "http" && scheme !== "https")
  1039       dumpn("*** server only supports http/https schemes: '" + scheme + "'");
  1040       dumpStack();
  1041       throw Cr.NS_ERROR_ILLEGAL_VALUE;
  1043     if (!HOST_REGEX.test(host))
  1045       dumpn("*** unexpected host: '" + host + "'");
  1046       throw Cr.NS_ERROR_ILLEGAL_VALUE;
  1048     if (port < 0 || port > 65535)
  1050       dumpn("*** unexpected port: '" + port + "'");
  1051       throw Cr.NS_ERROR_ILLEGAL_VALUE;
  1054 };
  1057 /**
  1058 * Represents a connection to the server (and possibly in the future the thread
  1059 * on which the connection is processed).
  1061 * @param input : nsIInputStream
  1062 * stream from which incoming data on the connection is read
  1063 * @param output : nsIOutputStream
  1064 * stream to write data out the connection
  1065 * @param server : nsHttpServer
  1066 * the server handling the connection
  1067 * @param port : int
  1068 * the port on which the server is running
  1069 * @param outgoingPort : int
  1070 * the outgoing port used by this connection
  1071 * @param number : uint
  1072 * a serial number used to uniquely identify this connection
  1073 */
  1074 function Connection(input, output, server, port, outgoingPort, number)
  1076   dumpn("*** opening new connection " + number + " on port " + outgoingPort);
  1078   /** Stream of incoming data. */
  1079   this.input = input;
  1081   /** Stream for outgoing data. */
  1082   this.output = output;
  1084   /** The server associated with this request. */
  1085   this.server = server;
  1087   /** The port on which the server is running. */
  1088   this.port = port;
  1090   /** The outgoing poort used by this connection. */
  1091   this._outgoingPort = outgoingPort;
  1093   /** The serial number of this connection. */
  1094   this.number = number;
  1096   /**
  1097 * The request for which a response is being generated, null if the
  1098 * incoming request has not been fully received or if it had errors.
  1099 */
  1100   this.request = null;
  1102   /** State variables for debugging. */
  1103   this._closed = this._processed = false;
  1105 Connection.prototype =
  1107   /** Closes this connection's input/output streams. */
  1108   close: function()
  1110     dumpn("*** closing connection " + this.number +
  1111           " on port " + this._outgoingPort);
  1113     this.input.close();
  1114     this.output.close();
  1115     this._closed = true;
  1117     var server = this.server;
  1118     server._connectionClosed(this);
  1120     // If an error triggered a server shutdown, act on it now
  1121     if (server._doQuit)
  1122       server.stop(function() { /* not like we can do anything better */ });
  1123   },
  1125   /**
  1126 * Initiates processing of this connection, using the data in the given
  1127 * request.
  1129 * @param request : Request
  1130 * the request which should be processed
  1131 */
  1132   process: function(request)
  1134     NS_ASSERT(!this._closed && !this._processed);
  1136     this._processed = true;
  1138     this.request = request;
  1139     this.server._handler.handleResponse(this);
  1140   },
  1142   /**
  1143 * Initiates processing of this connection, generating a response with the
  1144 * given HTTP error code.
  1146 * @param code : uint
  1147 * an HTTP code, so in the range [0, 1000)
  1148 * @param request : Request
  1149 * incomplete data about the incoming request (since there were errors
  1150 * during its processing
  1151 */
  1152   processError: function(code, request)
  1154     NS_ASSERT(!this._closed && !this._processed);
  1156     this._processed = true;
  1157     this.request = request;
  1158     this.server._handler.handleError(code, this);
  1159   },
  1161   /** Converts this to a string for debugging purposes. */
  1162   toString: function()
  1164     return "<Connection(" + this.number +
  1165            (this.request ? ", " + this.request.path : "") +"): " +
  1166            (this._closed ? "closed" : "open") + ">";
  1168 };
  1172 /** Returns an array of count bytes from the given input stream. */
  1173 function readBytes(inputStream, count)
  1175   return new BinaryInputStream(inputStream).readByteArray(count);
  1180 /** Request reader processing states; see RequestReader for details. */
  1181 const READER_IN_REQUEST_LINE = 0;
  1182 const READER_IN_HEADERS = 1;
  1183 const READER_IN_BODY = 2;
  1184 const READER_FINISHED = 3;
  1187 /**
  1188 * Reads incoming request data asynchronously, does any necessary preprocessing,
  1189 * and forwards it to the request handler. Processing occurs in three states:
  1191 * READER_IN_REQUEST_LINE Reading the request's status line
  1192 * READER_IN_HEADERS Reading headers in the request
  1193 * READER_IN_BODY Reading the body of the request
  1194 * READER_FINISHED Entire request has been read and processed
  1196 * During the first two stages, initial metadata about the request is gathered
  1197 * into a Request object. Once the status line and headers have been processed,
  1198 * we start processing the body of the request into the Request. Finally, when
  1199 * the entire body has been read, we create a Response and hand it off to the
  1200 * ServerHandler to be given to the appropriate request handler.
  1202 * @param connection : Connection
  1203 * the connection for the request being read
  1204 */
  1205 function RequestReader(connection)
  1207   /** Connection metadata for this request. */
  1208   this._connection = connection;
  1210   /**
  1211 * A container providing line-by-line access to the raw bytes that make up the
  1212 * data which has been read from the connection but has not yet been acted
  1213 * upon (by passing it to the request handler or by extracting request
  1214 * metadata from it).
  1215 */
  1216   this._data = new LineData();
  1218   /**
  1219 * The amount of data remaining to be read from the body of this request.
  1220 * After all headers in the request have been read this is the value in the
  1221 * Content-Length header, but as the body is read its value decreases to zero.
  1222 */
  1223   this._contentLength = 0;
  1225   /** The current state of parsing the incoming request. */
  1226   this._state = READER_IN_REQUEST_LINE;
  1228   /** Metadata constructed from the incoming request for the request handler. */
  1229   this._metadata = new Request(connection.port);
  1231   /**
  1232 * Used to preserve state if we run out of line data midway through a
  1233 * multi-line header. _lastHeaderName stores the name of the header, while
  1234 * _lastHeaderValue stores the value we've seen so far for the header.
  1236 * These fields are always either both undefined or both strings.
  1237 */
  1238   this._lastHeaderName = this._lastHeaderValue = undefined;
  1240 RequestReader.prototype =
  1242   // NSIINPUTSTREAMCALLBACK
  1244   /**
  1245 * Called when more data from the incoming request is available. This method
  1246 * then reads the available data from input and deals with that data as
  1247 * necessary, depending upon the syntax of already-downloaded data.
  1249 * @param input : nsIAsyncInputStream
  1250 * the stream of incoming data from the connection
  1251 */
  1252   onInputStreamReady: function(input)
  1254     dumpn("*** onInputStreamReady(input=" + input + ") on thread " +
  1255           gThreadManager.currentThread + " (main is " +
  1256           gThreadManager.mainThread + ")");
  1257     dumpn("*** this._state == " + this._state);
  1259     // Handle cases where we get more data after a request error has been
  1260     // discovered but *before* we can close the connection.
  1261     var data = this._data;
  1262     if (!data)
  1263       return;
  1265     try
  1267       data.appendBytes(readBytes(input, input.available()));
  1269     catch (e)
  1271       if (streamClosed(e))
  1273         dumpn("*** WARNING: unexpected error when reading from socket; will " +
  1274               "be treated as if the input stream had been closed");
  1275         dumpn("*** WARNING: actual error was: " + e);
  1278       // We've lost a race -- input has been closed, but we're still expecting
  1279       // to read more data. available() will throw in this case, and since
  1280       // we're dead in the water now, destroy the connection.
  1281       dumpn("*** onInputStreamReady called on a closed input, destroying " +
  1282             "connection");
  1283       this._connection.close();
  1284       return;
  1287     switch (this._state)
  1289       default:
  1290         NS_ASSERT(false, "invalid state: " + this._state);
  1291         break;
  1293       case READER_IN_REQUEST_LINE:
  1294         if (!this._processRequestLine())
  1295           break;
  1296         /* fall through */
  1298       case READER_IN_HEADERS:
  1299         if (!this._processHeaders())
  1300           break;
  1301         /* fall through */
  1303       case READER_IN_BODY:
  1304         this._processBody();
  1307     if (this._state != READER_FINISHED)
  1308       input.asyncWait(this, 0, 0, gThreadManager.currentThread);
  1309   },
  1311   //
  1312   // see nsISupports.QueryInterface
  1313   //
  1314   QueryInterface: function(aIID)
  1316     if (aIID.equals(Ci.nsIInputStreamCallback) ||
  1317         aIID.equals(Ci.nsISupports))
  1318       return this;
  1320     throw Cr.NS_ERROR_NO_INTERFACE;
  1321   },
  1324   // PRIVATE API
  1326   /**
  1327 * Processes unprocessed, downloaded data as a request line.
  1329 * @returns boolean
  1330 * true iff the request line has been fully processed
  1331 */
  1332   _processRequestLine: function()
  1334     NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
  1336     // Servers SHOULD ignore any empty line(s) received where a Request-Line
  1337     // is expected (section 4.1).
  1338     var data = this._data;
  1339     var line = {};
  1340     var readSuccess;
  1341     while ((readSuccess = data.readLine(line)) && line.value == "")
  1342       dumpn("*** ignoring beginning blank line...");
  1344     // if we don't have a full line, wait until we do
  1345     if (!readSuccess)
  1346       return false;
  1348     // we have the first non-blank line
  1349     try
  1351       this._parseRequestLine(line.value);
  1352       this._state = READER_IN_HEADERS;
  1353       return true;
  1355     catch (e)
  1357       this._handleError(e);
  1358       return false;
  1360   },
  1362   /**
  1363 * Processes stored data, assuming it is either at the beginning or in
  1364 * the middle of processing request headers.
  1366 * @returns boolean
  1367 * true iff header data in the request has been fully processed
  1368 */
  1369   _processHeaders: function()
  1371     NS_ASSERT(this._state == READER_IN_HEADERS);
  1373     // XXX things to fix here:
  1374     //
  1375     // - need to support RFC 2047-encoded non-US-ASCII characters
  1377     try
  1379       var done = this._parseHeaders();
  1380       if (done)
  1382         var request = this._metadata;
  1384         // XXX this is wrong for requests with transfer-encodings applied to
  1385         // them, particularly chunked (which by its nature can have no
  1386         // meaningful Content-Length header)!
  1387         this._contentLength = request.hasHeader("Content-Length")
  1388                             ? parseInt(request.getHeader("Content-Length"), 10)
  1389                             : 0;
  1390         dumpn("_processHeaders, Content-length=" + this._contentLength);
  1392         this._state = READER_IN_BODY;
  1394       return done;
  1396     catch (e)
  1398       this._handleError(e);
  1399       return false;
  1401   },
  1403   /**
  1404 * Processes stored data, assuming it is either at the beginning or in
  1405 * the middle of processing the request body.
  1407 * @returns boolean
  1408 * true iff the request body has been fully processed
  1409 */
  1410   _processBody: function()
  1412     NS_ASSERT(this._state == READER_IN_BODY);
  1414     // XXX handle chunked transfer-coding request bodies!
  1416     try
  1418       if (this._contentLength > 0)
  1420         var data = this._data.purge();
  1421         var count = Math.min(data.length, this._contentLength);
  1422         dumpn("*** loading data=" + data + " len=" + data.length +
  1423               " excess=" + (data.length - count));
  1425         var bos = new BinaryOutputStream(this._metadata._bodyOutputStream);
  1426         bos.writeByteArray(data, count);
  1427         this._contentLength -= count;
  1430       dumpn("*** remaining body data len=" + this._contentLength);
  1431       if (this._contentLength == 0)
  1433         this._validateRequest();
  1434         this._state = READER_FINISHED;
  1435         this._handleResponse();
  1436         return true;
  1439       return false;
  1441     catch (e)
  1443       this._handleError(e);
  1444       return false;
  1446   },
  1448   /**
  1449 * Does various post-header checks on the data in this request.
  1451 * @throws : HttpError
  1452 * if the request was malformed in some way
  1453 */
  1454   _validateRequest: function()
  1456     NS_ASSERT(this._state == READER_IN_BODY);
  1458     dumpn("*** _validateRequest");
  1460     var metadata = this._metadata;
  1461     var headers = metadata._headers;
  1463     // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header
  1464     var identity = this._connection.server.identity;
  1465     if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
  1467       if (!headers.hasHeader("Host"))
  1469         dumpn("*** malformed HTTP/1.1 or greater request with no Host header!");
  1470         throw HTTP_400;
  1473       // If the Request-URI wasn't absolute, then we need to determine our host.
  1474       // We have to determine what scheme was used to access us based on the
  1475       // server identity data at this point, because the request just doesn't
  1476       // contain enough data on its own to do this, sadly.
  1477       if (!metadata._host)
  1479         var host, port;
  1480         var hostPort = headers.getHeader("Host");
  1481         var colon = hostPort.indexOf(":");
  1482         if (colon < 0)
  1484           host = hostPort;
  1485           port = "";
  1487         else
  1489           host = hostPort.substring(0, colon);
  1490           port = hostPort.substring(colon + 1);
  1493         // NB: We allow an empty port here because, oddly, a colon may be
  1494         // present even without a port number, e.g. "example.com:"; in this
  1495         // case the default port applies.
  1496         if (!HOST_REGEX.test(host) || !/^\d*$/.test(port))
  1498           dumpn("*** malformed hostname (" + hostPort + ") in Host " +
  1499                 "header, 400 time");
  1500           throw HTTP_400;
  1503         // If we're not given a port, we're stuck, because we don't know what
  1504         // scheme to use to look up the correct port here, in general. Since
  1505         // the HTTPS case requires a tunnel/proxy and thus requires that the
  1506         // requested URI be absolute (and thus contain the necessary
  1507         // information), let's assume HTTP will prevail and use that.
  1508         port = +port || 80;
  1510         var scheme = identity.getScheme(host, port);
  1511         if (!scheme)
  1513           dumpn("*** unrecognized hostname (" + hostPort + ") in Host " +
  1514                 "header, 400 time");
  1515           throw HTTP_400;
  1518         metadata._scheme = scheme;
  1519         metadata._host = host;
  1520         metadata._port = port;
  1523     else
  1525       NS_ASSERT(metadata._host === undefined,
  1526                 "HTTP/1.0 doesn't allow absolute paths in the request line!");
  1528       metadata._scheme = identity.primaryScheme;
  1529       metadata._host = identity.primaryHost;
  1530       metadata._port = identity.primaryPort;
  1533     NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port),
  1534               "must have a location we recognize by now!");
  1535   },
  1537   /**
  1538 * Handles responses in case of error, either in the server or in the request.
  1540 * @param e
  1541 * the specific error encountered, which is an HttpError in the case where
  1542 * the request is in some way invalid or cannot be fulfilled; if this isn't
  1543 * an HttpError we're going to be paranoid and shut down, because that
  1544 * shouldn't happen, ever
  1545 */
  1546   _handleError: function(e)
  1548     // Don't fall back into normal processing!
  1549     this._state = READER_FINISHED;
  1551     var server = this._connection.server;
  1552     if (e instanceof HttpError)
  1554       var code = e.code;
  1556     else
  1558       dumpn("!!! UNEXPECTED ERROR: " + e +
  1559             (e.lineNumber ? ", line " + e.lineNumber : ""));
  1561       // no idea what happened -- be paranoid and shut down
  1562       code = 500;
  1563       server._requestQuit();
  1566     // make attempted reuse of data an error
  1567     this._data = null;
  1569     this._connection.processError(code, this._metadata);
  1570   },
  1572   /**
  1573 * Now that we've read the request line and headers, we can actually hand off
  1574 * the request to be handled.
  1576 * This method is called once per request, after the request line and all
  1577 * headers and the body, if any, have been received.
  1578 */
  1579   _handleResponse: function()
  1581     NS_ASSERT(this._state == READER_FINISHED);
  1583     // We don't need the line-based data any more, so make attempted reuse an
  1584     // error.
  1585     this._data = null;
  1587     this._connection.process(this._metadata);
  1588   },
  1591   // PARSING
  1593   /**
  1594 * Parses the request line for the HTTP request associated with this.
  1596 * @param line : string
  1597 * the request line
  1598 */
  1599   _parseRequestLine: function(line)
  1601     NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
  1603     dumpn("*** _parseRequestLine('" + line + "')");
  1605     var metadata = this._metadata;
  1607     // clients and servers SHOULD accept any amount of SP or HT characters
  1608     // between fields, even though only a single SP is required (section 19.3)
  1609     var request = line.split(/[ \t]+/);
  1610     if (!request || request.length != 3)
  1611       throw HTTP_400;
  1613     metadata._method = request[0];
  1615     // get the HTTP version
  1616     var ver = request[2];
  1617     var match = ver.match(/^HTTP\/(\d+\.\d+)$/);
  1618     if (!match)
  1619       throw HTTP_400;
  1621     // determine HTTP version
  1622     try
  1624       metadata._httpVersion = new nsHttpVersion(match[1]);
  1625       if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0))
  1626         throw "unsupported HTTP version";
  1628     catch (e)
  1630       // we support HTTP/1.0 and HTTP/1.1 only
  1631       throw HTTP_501;
  1635     var fullPath = request[1];
  1636     var serverIdentity = this._connection.server.identity;
  1638     var scheme, host, port;
  1640     if (fullPath.charAt(0) != "/")
  1642       // No absolute paths in the request line in HTTP prior to 1.1
  1643       if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
  1644         throw HTTP_400;
  1646       try
  1648         var uri = Cc["@mozilla.org/network/io-service;1"]
  1649                     .getService(Ci.nsIIOService)
  1650                     .newURI(fullPath, null, null);
  1651         fullPath = uri.path;
  1652         scheme = uri.scheme;
  1653         host = metadata._host = uri.asciiHost;
  1654         port = uri.port;
  1655         if (port === -1)
  1657           if (scheme === "http")
  1658             port = 80;
  1659           else if (scheme === "https")
  1660             port = 443;
  1661           else
  1662             throw HTTP_400;
  1665       catch (e)
  1667         // If the host is not a valid host on the server, the response MUST be a
  1668         // 400 (Bad Request) error message (section 5.2). Alternately, the URI
  1669         // is malformed.
  1670         throw HTTP_400;
  1673       if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/")
  1674         throw HTTP_400;
  1677     var splitter = fullPath.indexOf("?");
  1678     if (splitter < 0)
  1680       // _queryString already set in ctor
  1681       metadata._path = fullPath;
  1683     else
  1685       metadata._path = fullPath.substring(0, splitter);
  1686       metadata._queryString = fullPath.substring(splitter + 1);
  1689     metadata._scheme = scheme;
  1690     metadata._host = host;
  1691     metadata._port = port;
  1692   },
  1694   /**
  1695 * Parses all available HTTP headers in this until the header-ending CRLFCRLF,
  1696 * adding them to the store of headers in the request.
  1698 * @throws
  1699 * HTTP_400 if the headers are malformed
  1700 * @returns boolean
  1701 * true if all headers have now been processed, false otherwise
  1702 */
  1703   _parseHeaders: function()
  1705     NS_ASSERT(this._state == READER_IN_HEADERS);
  1707     dumpn("*** _parseHeaders");
  1709     var data = this._data;
  1711     var headers = this._metadata._headers;
  1712     var lastName = this._lastHeaderName;
  1713     var lastVal = this._lastHeaderValue;
  1715     var line = {};
  1716     while (true)
  1718       NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)),
  1719                 lastName === undefined ?
  1720                   "lastVal without lastName? lastVal: '" + lastVal + "'" :
  1721                   "lastName without lastVal? lastName: '" + lastName + "'");
  1723       if (!data.readLine(line))
  1725         // save any data we have from the header we might still be processing
  1726         this._lastHeaderName = lastName;
  1727         this._lastHeaderValue = lastVal;
  1728         return false;
  1731       var lineText = line.value;
  1732       var firstChar = lineText.charAt(0);
  1734       // blank line means end of headers
  1735       if (lineText == "")
  1737         // we're finished with the previous header
  1738         if (lastName)
  1740           try
  1742             headers.setHeader(lastName, lastVal, true);
  1744           catch (e)
  1746             dumpn("*** e == " + e);
  1747             throw HTTP_400;
  1750         else
  1752           // no headers in request -- valid for HTTP/1.0 requests
  1755         // either way, we're done processing headers
  1756         this._state = READER_IN_BODY;
  1757         return true;
  1759       else if (firstChar == " " || firstChar == "\t")
  1761         // multi-line header if we've already seen a header line
  1762         if (!lastName)
  1764           // we don't have a header to continue!
  1765           throw HTTP_400;
  1768         // append this line's text to the value; starts with SP/HT, so no need
  1769         // for separating whitespace
  1770         lastVal += lineText;
  1772       else
  1774         // we have a new header, so set the old one (if one existed)
  1775         if (lastName)
  1777           try
  1779             headers.setHeader(lastName, lastVal, true);
  1781           catch (e)
  1783             dumpn("*** e == " + e);
  1784             throw HTTP_400;
  1788         var colon = lineText.indexOf(":"); // first colon must be splitter
  1789         if (colon < 1)
  1791           // no colon or missing header field-name
  1792           throw HTTP_400;
  1795         // set header name, value (to be set in the next loop, usually)
  1796         lastName = lineText.substring(0, colon);
  1797         lastVal = lineText.substring(colon + 1);
  1798       } // empty, continuation, start of header
  1799     } // while (true)
  1801 };
  1804 /** The character codes for CR and LF. */
  1805 const CR = 0x0D, LF = 0x0A;
  1807 /**
  1808 * Calculates the number of characters before the first CRLF pair in array, or
  1809 * -1 if the array contains no CRLF pair.
  1811 * @param array : Array
  1812 * an array of numbers in the range [0, 256), each representing a single
  1813 * character; the first CRLF is the lowest index i where
  1814 * |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|,
  1815 * if such an |i| exists, and -1 otherwise
  1816 * @returns int
  1817 * the index of the first CRLF if any were present, -1 otherwise
  1818 */
  1819 function findCRLF(array)
  1821   for (var i = array.indexOf(CR); i >= 0; i = array.indexOf(CR, i + 1))
  1823     if (array[i + 1] == LF)
  1824       return i;
  1826   return -1;
  1830 /**
  1831 * A container which provides line-by-line access to the arrays of bytes with
  1832 * which it is seeded.
  1833 */
  1834 function LineData()
  1836   /** An array of queued bytes from which to get line-based characters. */
  1837   this._data = [];
  1839 LineData.prototype =
  1841   /**
  1842 * Appends the bytes in the given array to the internal data cache maintained
  1843 * by this.
  1844 */
  1845   appendBytes: function(bytes)
  1847     Array.prototype.push.apply(this._data, bytes);
  1848   },
  1850   /**
  1851 * Removes and returns a line of data, delimited by CRLF, from this.
  1853 * @param out
  1854 * an object whose "value" property will be set to the first line of text
  1855 * present in this, sans CRLF, if this contains a full CRLF-delimited line
  1856 * of text; if this doesn't contain enough data, the value of the property
  1857 * is undefined
  1858 * @returns boolean
  1859 * true if a full line of data could be read from the data in this, false
  1860 * otherwise
  1861 */
  1862   readLine: function(out)
  1864     var data = this._data;
  1865     var length = findCRLF(data);
  1866     if (length < 0)
  1867       return false;
  1869     //
  1870     // We have the index of the CR, so remove all the characters, including
  1871     // CRLF, from the array with splice, and convert the removed array into the
  1872     // corresponding string, from which we then strip the trailing CRLF.
  1873     //
  1874     // Getting the line in this matter acknowledges that substring is an O(1)
  1875     // operation in SpiderMonkey because strings are immutable, whereas two
  1876     // splices, both from the beginning of the data, are less likely to be as
  1877     // cheap as a single splice plus two extra character conversions.
  1878     //
  1879     var line = String.fromCharCode.apply(null, data.splice(0, length + 2));
  1880     out.value = line.substring(0, length);
  1882     return true;
  1883   },
  1885   /**
  1886 * Removes the bytes currently within this and returns them in an array.
  1888 * @returns Array
  1889 * the bytes within this when this method is called
  1890 */
  1891   purge: function()
  1893     var data = this._data;
  1894     this._data = [];
  1895     return data;
  1897 };
  1901 /**
  1902 * Creates a request-handling function for an nsIHttpRequestHandler object.
  1903 */
  1904 function createHandlerFunc(handler)
  1906   return function(metadata, response) { handler.handle(metadata, response); };
  1910 /**
  1911 * The default handler for directories; writes an HTML response containing a
  1912 * slightly-formatted directory listing.
  1913 */
  1914 function defaultIndexHandler(metadata, response)
  1916   response.setHeader("Content-Type", "text/html", false);
  1918   var path = htmlEscape(decodeURI(metadata.path));
  1920   //
  1921   // Just do a very basic bit of directory listings -- no need for too much
  1922   // fanciness, especially since we don't have a style sheet in which we can
  1923   // stick rules (don't want to pollute the default path-space).
  1924   //
  1926   var body = '<html>\
  1927 <head>\
  1928 <title>' + path + '</title>\
  1929 </head>\
  1930 <body>\
  1931 <h1>' + path + '</h1>\
  1932 <ol style="list-style-type: none">';
  1934   var directory = metadata.getProperty("directory").QueryInterface(Ci.nsILocalFile);
  1935   NS_ASSERT(directory && directory.isDirectory());
  1937   var fileList = [];
  1938   var files = directory.directoryEntries;
  1939   while (files.hasMoreElements())
  1941     var f = files.getNext().QueryInterface(Ci.nsIFile);
  1942     var name = f.leafName;
  1943     if (!f.isHidden() &&
  1944         (name.charAt(name.length - 1) != HIDDEN_CHAR ||
  1945          name.charAt(name.length - 2) == HIDDEN_CHAR))
  1946       fileList.push(f);
  1949   fileList.sort(fileSort);
  1951   for (var i = 0; i < fileList.length; i++)
  1953     var file = fileList[i];
  1954     try
  1956       var name = file.leafName;
  1957       if (name.charAt(name.length - 1) == HIDDEN_CHAR)
  1958         name = name.substring(0, name.length - 1);
  1959       var sep = file.isDirectory() ? "/" : "";
  1961       // Note: using " to delimit the attribute here because encodeURIComponent
  1962       // passes through '.
  1963       var item = '<li><a href="' + encodeURIComponent(name) + sep + '">' +
  1964                    htmlEscape(name) + sep +
  1965                  '</a></li>';
  1967       body += item;
  1969     catch (e) { /* some file system error, ignore the file */ }
  1972   body += ' </ol>\
  1973 </body>\
  1974 </html>';
  1976   response.bodyOutputStream.write(body, body.length);
  1979 /**
  1980 * Sorts a and b (nsIFile objects) into an aesthetically pleasing order.
  1981 */
  1982 function fileSort(a, b)
  1984   var dira = a.isDirectory(), dirb = b.isDirectory();
  1986   if (dira && !dirb)
  1987     return -1;
  1988   if (dirb && !dira)
  1989     return 1;
  1991   var namea = a.leafName.toLowerCase(), nameb = b.leafName.toLowerCase();
  1992   return nameb > namea ? -1 : 1;
  1996 /**
  1997 * Converts an externally-provided path into an internal path for use in
  1998 * determining file mappings.
  2000 * @param path
  2001 * the path to convert
  2002 * @param encoded
  2003 * true if the given path should be passed through decodeURI prior to
  2004 * conversion
  2005 * @throws URIError
  2006 * if path is incorrectly encoded
  2007 */
  2008 function toInternalPath(path, encoded)
  2010   if (encoded)
  2011     path = decodeURI(path);
  2013   var comps = path.split("/");
  2014   for (var i = 0, sz = comps.length; i < sz; i++)
  2016     var comp = comps[i];
  2017     if (comp.charAt(comp.length - 1) == HIDDEN_CHAR)
  2018       comps[i] = comp + HIDDEN_CHAR;
  2020   return comps.join("/");
  2024 /**
  2025 * Adds custom-specified headers for the given file to the given response, if
  2026 * any such headers are specified.
  2028 * @param file
  2029 * the file on the disk which is to be written
  2030 * @param metadata
  2031 * metadata about the incoming request
  2032 * @param response
  2033 * the Response to which any specified headers/data should be written
  2034 * @throws HTTP_500
  2035 * if an error occurred while processing custom-specified headers
  2036 */
  2037 function maybeAddHeaders(file, metadata, response)
  2039   var name = file.leafName;
  2040   if (name.charAt(name.length - 1) == HIDDEN_CHAR)
  2041     name = name.substring(0, name.length - 1);
  2043   var headerFile = file.parent;
  2044   headerFile.append(name + HEADERS_SUFFIX);
  2046   if (!headerFile.exists())
  2047     return;
  2049   const PR_RDONLY = 0x01;
  2050   var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8),
  2051                                 Ci.nsIFileInputStream.CLOSE_ON_EOF);
  2053   try
  2055     var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
  2056     lis.QueryInterface(Ci.nsIUnicharLineInputStream);
  2058     var line = {value: ""};
  2059     var more = lis.readLine(line);
  2061     if (!more && line.value == "")
  2062       return;
  2065     // request line
  2067     var status = line.value;
  2068     if (status.indexOf("HTTP ") == 0)
  2070       status = status.substring(5);
  2071       var space = status.indexOf(" ");
  2072       var code, description;
  2073       if (space < 0)
  2075         code = status;
  2076         description = "";
  2078       else
  2080         code = status.substring(0, space);
  2081         description = status.substring(space + 1, status.length);
  2084       response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description);
  2086       line.value = "";
  2087       more = lis.readLine(line);
  2090     // headers
  2091     while (more || line.value != "")
  2093       var header = line.value;
  2094       var colon = header.indexOf(":");
  2096       response.setHeader(header.substring(0, colon),
  2097                          header.substring(colon + 1, header.length),
  2098                          false); // allow overriding server-set headers
  2100       line.value = "";
  2101       more = lis.readLine(line);
  2104   catch (e)
  2106     dumpn("WARNING: error in headers for " + metadata.path + ": " + e);
  2107     throw HTTP_500;
  2109   finally
  2111     fis.close();
  2116 /**
  2117 * An object which handles requests for a server, executing default and
  2118 * overridden behaviors as instructed by the code which uses and manipulates it.
  2119 * Default behavior includes the paths / and /trace (diagnostics), with some
  2120 * support for HTTP error pages for various codes and fallback to HTTP 500 if
  2121 * those codes fail for any reason.
  2123 * @param server : nsHttpServer
  2124 * the server in which this handler is being used
  2125 */
  2126 function ServerHandler(server)
  2128   // FIELDS
  2130   /**
  2131 * The nsHttpServer instance associated with this handler.
  2132 */
  2133   this._server = server;
  2135   /**
  2136 * A FileMap object containing the set of path->nsILocalFile mappings for
  2137 * all directory mappings set in the server (e.g., "/" for /var/www/html/,
  2138 * "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2).
  2140 * Note carefully: the leading and trailing "/" in each path (not file) are
  2141 * removed before insertion to simplify the code which uses this. You have
  2142 * been warned!
  2143 */
  2144   this._pathDirectoryMap = new FileMap();
  2146   /**
  2147 * Custom request handlers for the server in which this resides. Path-handler
  2148 * pairs are stored as property-value pairs in this property.
  2150 * @see ServerHandler.prototype._defaultPaths
  2151 */
  2152   this._overridePaths = {};
  2154   /**
  2155 * Custom request handlers for the server in which this resides. Prefix-handler
  2156 * pairs are stored as property-value pairs in this property.
  2157 */
  2158   this._overridePrefixes = {};
  2160   /**
  2161 * Custom request handlers for the error handlers in the server in which this
  2162 * resides. Path-handler pairs are stored as property-value pairs in this
  2163 * property.
  2165 * @see ServerHandler.prototype._defaultErrors
  2166 */
  2167   this._overrideErrors = {};
  2169   /**
  2170 * Maps file extensions to their MIME types in the server, overriding any
  2171 * mapping that might or might not exist in the MIME service.
  2172 */
  2173   this._mimeMappings = {};
  2175   /**
  2176 * The default handler for requests for directories, used to serve directories
  2177 * when no index file is present.
  2178 */
  2179   this._indexHandler = defaultIndexHandler;
  2181   /** Per-path state storage for the server. */
  2182   this._state = {};
  2184   /** Entire-server state storage. */
  2185   this._sharedState = {};
  2187   /** Entire-server state storage for nsISupports values. */
  2188   this._objectState = {};
  2190 ServerHandler.prototype =
  2192   // PUBLIC API
  2194   /**
  2195 * Handles a request to this server, responding to the request appropriately
  2196 * and initiating server shutdown if necessary.
  2198 * This method never throws an exception.
  2200 * @param connection : Connection
  2201 * the connection for this request
  2202 */
  2203   handleResponse: function(connection)
  2205     var request = connection.request;
  2206     var response = new Response(connection);
  2208     var path = request.path;
  2209     dumpn("*** path == " + path);
  2211     try
  2213       try
  2215         if (path in this._overridePaths)
  2217           // explicit paths first, then files based on existing directory mappings,
  2218           // then (if the file doesn't exist) built-in server default paths
  2219           dumpn("calling override for " + path);
  2220           this._overridePaths[path](request, response);
  2222         else
  2224           let longestPrefix = "";
  2225           for (let prefix in this._overridePrefixes)
  2227             if (prefix.length > longestPrefix.length && path.startsWith(prefix))
  2229               longestPrefix = prefix;
  2232           if (longestPrefix.length > 0)
  2234             dumpn("calling prefix override for " + longestPrefix);
  2235             this._overridePrefixes[longestPrefix](request, response);
  2237           else
  2239             this._handleDefault(request, response);
  2243       catch (e)
  2245         if (response.partiallySent())
  2247           response.abort(e);
  2248           return;
  2251         if (!(e instanceof HttpError))
  2253           dumpn("*** unexpected error: e == " + e);
  2254           throw HTTP_500;
  2256         if (e.code !== 404)
  2257           throw e;
  2259         dumpn("*** default: " + (path in this._defaultPaths));
  2261         response = new Response(connection);
  2262         if (path in this._defaultPaths)
  2263           this._defaultPaths[path](request, response);
  2264         else
  2265           throw HTTP_404;
  2268     catch (e)
  2270       if (response.partiallySent())
  2272         response.abort(e);
  2273         return;
  2276       var errorCode = "internal";
  2278       try
  2280         if (!(e instanceof HttpError))
  2281           throw e;
  2283         errorCode = e.code;
  2284         dumpn("*** errorCode == " + errorCode);
  2286         response = new Response(connection);
  2287         if (e.customErrorHandling)
  2288           e.customErrorHandling(response);
  2289         this._handleError(errorCode, request, response);
  2290         return;
  2292       catch (e2)
  2294         dumpn("*** error handling " + errorCode + " error: " +
  2295               "e2 == " + e2 + ", shutting down server");
  2297         connection.server._requestQuit();
  2298         response.abort(e2);
  2299         return;
  2303     response.complete();
  2304   },
  2306   //
  2307   // see nsIHttpServer.registerFile
  2308   //
  2309   registerFile: function(path, file)
  2311     if (!file)
  2313       dumpn("*** unregistering '" + path + "' mapping");
  2314       delete this._overridePaths[path];
  2315       return;
  2318     dumpn("*** registering '" + path + "' as mapping to " + file.path);
  2319     file = file.clone();
  2321     var self = this;
  2322     this._overridePaths[path] =
  2323       function(request, response)
  2325         if (!file.exists())
  2326           throw HTTP_404;
  2328         response.setStatusLine(request.httpVersion, 200, "OK");
  2329         self._writeFileResponse(request, file, response, 0, file.fileSize);
  2330       };
  2331   },
  2333   //
  2334   // see nsIHttpServer.registerPathHandler
  2335   //
  2336   registerPathHandler: function(path, handler)
  2338     // XXX true path validation!
  2339     if (path.charAt(0) != "/")
  2340       throw Cr.NS_ERROR_INVALID_ARG;
  2342     this._handlerToField(handler, this._overridePaths, path);
  2343   },
  2345   //
  2346   // see nsIHttpServer.registerPrefixHandler
  2347   //
  2348   registerPrefixHandler: function(prefix, handler)
  2350     // XXX true prefix validation!
  2351     if (!(prefix.startsWith("/") && prefix.endsWith("/")))
  2352       throw Cr.NS_ERROR_INVALID_ARG;
  2354     this._handlerToField(handler, this._overridePrefixes, prefix);
  2355   },
  2357   //
  2358   // see nsIHttpServer.registerDirectory
  2359   //
  2360   registerDirectory: function(path, directory)
  2362     // strip off leading and trailing '/' so that we can use lastIndexOf when
  2363     // determining exactly how a path maps onto a mapped directory --
  2364     // conditional is required here to deal with "/".substring(1, 0) being
  2365     // converted to "/".substring(0, 1) per the JS specification
  2366     var key = path.length == 1 ? "" : path.substring(1, path.length - 1);
  2368     // the path-to-directory mapping code requires that the first character not
  2369     // be "/", or it will go into an infinite loop
  2370     if (key.charAt(0) == "/")
  2371       throw Cr.NS_ERROR_INVALID_ARG;
  2373     key = toInternalPath(key, false);
  2375     if (directory)
  2377       dumpn("*** mapping '" + path + "' to the location " + directory.path);
  2378       this._pathDirectoryMap.put(key, directory);
  2380     else
  2382       dumpn("*** removing mapping for '" + path + "'");
  2383       this._pathDirectoryMap.put(key, null);
  2385   },
  2387   //
  2388   // see nsIHttpServer.registerErrorHandler
  2389   //
  2390   registerErrorHandler: function(err, handler)
  2392     if (!(err in HTTP_ERROR_CODES))
  2393       dumpn("*** WARNING: registering non-HTTP/1.1 error code " +
  2394             "(" + err + ") handler -- was this intentional?");
  2396     this._handlerToField(handler, this._overrideErrors, err);
  2397   },
  2399   //
  2400   // see nsIHttpServer.setIndexHandler
  2401   //
  2402   setIndexHandler: function(handler)
  2404     if (!handler)
  2405       handler = defaultIndexHandler;
  2406     else if (typeof(handler) != "function")
  2407       handler = createHandlerFunc(handler);
  2409     this._indexHandler = handler;
  2410   },
  2412   //
  2413   // see nsIHttpServer.registerContentType
  2414   //
  2415   registerContentType: function(ext, type)
  2417     if (!type)
  2418       delete this._mimeMappings[ext];
  2419     else
  2420       this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type);
  2421   },
  2423   // PRIVATE API
  2425   /**
  2426 * Sets or remove (if handler is null) a handler in an object with a key.
  2428 * @param handler
  2429 * a handler, either function or an nsIHttpRequestHandler
  2430 * @param dict
  2431 * The object to attach the handler to.
  2432 * @param key
  2433 * The field name of the handler.
  2434 */
  2435   _handlerToField: function(handler, dict, key)
  2437     // for convenience, handler can be a function if this is run from xpcshell
  2438     if (typeof(handler) == "function")
  2439       dict[key] = handler;
  2440     else if (handler)
  2441       dict[key] = createHandlerFunc(handler);
  2442     else
  2443       delete dict[key];
  2444   },
  2446   /**
  2447 * Handles a request which maps to a file in the local filesystem (if a base
  2448 * path has already been set; otherwise the 404 error is thrown).
  2450 * @param metadata : Request
  2451 * metadata for the incoming request
  2452 * @param response : Response
  2453 * an uninitialized Response to the given request, to be initialized by a
  2454 * request handler
  2455 * @throws HTTP_###
  2456 * if an HTTP error occurred (usually HTTP_404); note that in this case the
  2457 * calling code must handle post-processing of the response
  2458 */
  2459   _handleDefault: function(metadata, response)
  2461     dumpn("*** _handleDefault()");
  2463     response.setStatusLine(metadata.httpVersion, 200, "OK");
  2465     var path = metadata.path;
  2466     NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">");
  2468     // determine the actual on-disk file; this requires finding the deepest
  2469     // path-to-directory mapping in the requested URL
  2470     var file = this._getFileForPath(path);
  2472     // the "file" might be a directory, in which case we either serve the
  2473     // contained index.html or make the index handler write the response
  2474     if (file.exists() && file.isDirectory())
  2476       file.append("index.html"); // make configurable?
  2477       if (!file.exists() || file.isDirectory())
  2479         metadata._ensurePropertyBag();
  2480         metadata._bag.setPropertyAsInterface("directory", file.parent);
  2481         this._indexHandler(metadata, response);
  2482         return;
  2486     // alternately, the file might not exist
  2487     if (!file.exists())
  2488       throw HTTP_404;
  2490     var start, end;
  2491     if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) &&
  2492         metadata.hasHeader("Range") &&
  2493         this._getTypeFromFile(file) !== SJS_TYPE)
  2495       var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/);
  2496       if (!rangeMatch)
  2497         throw HTTP_400;
  2499       if (rangeMatch[1] !== undefined)
  2500         start = parseInt(rangeMatch[1], 10);
  2502       if (rangeMatch[2] !== undefined)
  2503         end = parseInt(rangeMatch[2], 10);
  2505       if (start === undefined && end === undefined)
  2506         throw HTTP_400;
  2508       // No start given, so the end is really the count of bytes from the
  2509       // end of the file.
  2510       if (start === undefined)
  2512         start = Math.max(0, file.fileSize - end);
  2513         end = file.fileSize - 1;
  2516       // start and end are inclusive
  2517       if (end === undefined || end >= file.fileSize)
  2518         end = file.fileSize - 1;
  2520       if (start !== undefined && start >= file.fileSize) {
  2521         var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable");
  2522         HTTP_416.customErrorHandling = function(errorResponse)
  2524           maybeAddHeaders(file, metadata, errorResponse);
  2525         };
  2526         throw HTTP_416;
  2529       if (end < start)
  2531         response.setStatusLine(metadata.httpVersion, 200, "OK");
  2532         start = 0;
  2533         end = file.fileSize - 1;
  2535       else
  2537         response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
  2538         var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize;
  2539         response.setHeader("Content-Range", contentRange);
  2542     else
  2544       start = 0;
  2545       end = file.fileSize - 1;
  2548     // finally...
  2549     dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " +
  2550           start + " to " + end + " inclusive");
  2551     this._writeFileResponse(metadata, file, response, start, end - start + 1);
  2552   },
  2554   /**
  2555 * Writes an HTTP response for the given file, including setting headers for
  2556 * file metadata.
  2558 * @param metadata : Request
  2559 * the Request for which a response is being generated
  2560 * @param file : nsILocalFile
  2561 * the file which is to be sent in the response
  2562 * @param response : Response
  2563 * the response to which the file should be written
  2564 * @param offset: uint
  2565 * the byte offset to skip to when writing
  2566 * @param count: uint
  2567 * the number of bytes to write
  2568 */
  2569   _writeFileResponse: function(metadata, file, response, offset, count)
  2571     const PR_RDONLY = 0x01;
  2573     var type = this._getTypeFromFile(file);
  2574     if (type === SJS_TYPE)
  2576       var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
  2577                                     Ci.nsIFileInputStream.CLOSE_ON_EOF);
  2579       try
  2581         var sis = new ScriptableInputStream(fis);
  2582         var s = Cu.Sandbox(gGlobalObject);
  2583         s.importFunction(dump, "dump");
  2585         // Define a basic key-value state-preservation API across requests, with
  2586         // keys initially corresponding to the empty string.
  2587         var self = this;
  2588         var path = metadata.path;
  2589         s.importFunction(function getState(k)
  2591           return self._getState(path, k);
  2592         });
  2593         s.importFunction(function setState(k, v)
  2595           self._setState(path, k, v);
  2596         });
  2597         s.importFunction(function getSharedState(k)
  2599           return self._getSharedState(k);
  2600         });
  2601         s.importFunction(function setSharedState(k, v)
  2603           self._setSharedState(k, v);
  2604         });
  2605         s.importFunction(function getObjectState(k, callback)
  2607           callback(self._getObjectState(k));
  2608         });
  2609         s.importFunction(function setObjectState(k, v)
  2611           self._setObjectState(k, v);
  2612         });
  2613         s.importFunction(function registerPathHandler(p, h)
  2615           self.registerPathHandler(p, h);
  2616         });
  2618         // Make it possible for sjs files to access their location
  2619         this._setState(path, "__LOCATION__", file.path);
  2621         try
  2623           // Alas, the line number in errors dumped to console when calling the
  2624           // request handler is simply an offset from where we load the SJS file.
  2625           // Work around this in a reasonably non-fragile way by dynamically
  2626           // getting the line number where we evaluate the SJS file. Don't
  2627           // separate these two lines!
  2628           var line = new Error().lineNumber;
  2629           Cu.evalInSandbox(sis.read(file.fileSize), s);
  2631         catch (e)
  2633           dumpn("*** syntax error in SJS at " + file.path + ": " + e);
  2634           throw HTTP_500;
  2637         try
  2639           s.handleRequest(metadata, response);
  2641         catch (e)
  2643           dump("*** error running SJS at " + file.path + ": " +
  2644                e + " on line " +
  2645                (e instanceof Error
  2646                ? e.lineNumber + " in httpd.js"
  2647                : (e.lineNumber - line)) + "\n");
  2648           throw HTTP_500;
  2651       finally
  2653         fis.close();
  2656     else
  2658       try
  2660         response.setHeader("Last-Modified",
  2661                            toDateString(file.lastModifiedTime),
  2662                            false);
  2664       catch (e) { /* lastModifiedTime threw, ignore */ }
  2666       response.setHeader("Content-Type", type, false);
  2667       maybeAddHeaders(file, metadata, response);
  2668       response.setHeader("Content-Length", "" + count, false);
  2670       var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
  2671                                     Ci.nsIFileInputStream.CLOSE_ON_EOF);
  2673       offset = offset || 0;
  2674       count = count || file.fileSize;
  2675       NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset");
  2676       NS_ASSERT(count >= 0, "bad count");
  2677       NS_ASSERT(offset + count <= file.fileSize, "bad total data size");
  2679       try
  2681         if (offset !== 0)
  2683           // Seek (or read, if seeking isn't supported) to the correct offset so
  2684           // the data sent to the client matches the requested range.
  2685           if (fis instanceof Ci.nsISeekableStream)
  2686             fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset);
  2687           else
  2688             new ScriptableInputStream(fis).read(offset);
  2691       catch (e)
  2693         fis.close();
  2694         throw e;
  2697       let writeMore = function writeMore()
  2699         gThreadManager.currentThread
  2700                       .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL);
  2703       var input = new BinaryInputStream(fis);
  2704       var output = new BinaryOutputStream(response.bodyOutputStream);
  2705       var writeData =
  2707           run: function()
  2709             var chunkSize = Math.min(65536, count);
  2710             count -= chunkSize;
  2711             NS_ASSERT(count >= 0, "underflow");
  2713             try
  2715               var data = input.readByteArray(chunkSize);
  2716               NS_ASSERT(data.length === chunkSize,
  2717                         "incorrect data returned? got " + data.length +
  2718                         ", expected " + chunkSize);
  2719               output.writeByteArray(data, data.length);
  2720               if (count === 0)
  2722                 fis.close();
  2723                 response.finish();
  2725               else
  2727                 writeMore();
  2730             catch (e)
  2732               try
  2734                 fis.close();
  2736               finally
  2738                 response.finish();
  2740               throw e;
  2743         };
  2745       writeMore();
  2747       // Now that we know copying will start, flag the response as async.
  2748       response.processAsync();
  2750   },
  2752   /**
  2753 * Get the value corresponding to a given key for the given path for SJS state
  2754 * preservation across requests.
  2756 * @param path : string
  2757 * the path from which the given state is to be retrieved
  2758 * @param k : string
  2759 * the key whose corresponding value is to be returned
  2760 * @returns string
  2761 * the corresponding value, which is initially the empty string
  2762 */
  2763   _getState: function(path, k)
  2765     var state = this._state;
  2766     if (path in state && k in state[path])
  2767       return state[path][k];
  2768     return "";
  2769   },
  2771   /**
  2772 * Set the value corresponding to a given key for the given path for SJS state
  2773 * preservation across requests.
  2775 * @param path : string
  2776 * the path from which the given state is to be retrieved
  2777 * @param k : string
  2778 * the key whose corresponding value is to be set
  2779 * @param v : string
  2780 * the value to be set
  2781 */
  2782   _setState: function(path, k, v)
  2784     if (typeof v !== "string")
  2785       throw new Error("non-string value passed");
  2786     var state = this._state;
  2787     if (!(path in state))
  2788       state[path] = {};
  2789     state[path][k] = v;
  2790   },
  2792   /**
  2793 * Get the value corresponding to a given key for SJS state preservation
  2794 * across requests.
  2796 * @param k : string
  2797 * the key whose corresponding value is to be returned
  2798 * @returns string
  2799 * the corresponding value, which is initially the empty string
  2800 */
  2801   _getSharedState: function(k)
  2803     var state = this._sharedState;
  2804     if (k in state)
  2805       return state[k];
  2806     return "";
  2807   },
  2809   /**
  2810 * Set the value corresponding to a given key for SJS state preservation
  2811 * across requests.
  2813 * @param k : string
  2814 * the key whose corresponding value is to be set
  2815 * @param v : string
  2816 * the value to be set
  2817 */
  2818   _setSharedState: function(k, v)
  2820     if (typeof v !== "string")
  2821       throw new Error("non-string value passed");
  2822     this._sharedState[k] = v;
  2823   },
  2825   /**
  2826 * Returns the object associated with the given key in the server for SJS
  2827 * state preservation across requests.
  2829 * @param k : string
  2830 * the key whose corresponding object is to be returned
  2831 * @returns nsISupports
  2832 * the corresponding object, or null if none was present
  2833 */
  2834   _getObjectState: function(k)
  2836     if (typeof k !== "string")
  2837       throw new Error("non-string key passed");
  2838     return this._objectState[k] || null;
  2839   },
  2841   /**
  2842 * Sets the object associated with the given key in the server for SJS
  2843 * state preservation across requests.
  2845 * @param k : string
  2846 * the key whose corresponding object is to be set
  2847 * @param v : nsISupports
  2848 * the object to be associated with the given key; may be null
  2849 */
  2850   _setObjectState: function(k, v)
  2852     if (typeof k !== "string")
  2853       throw new Error("non-string key passed");
  2854     if (typeof v !== "object")
  2855       throw new Error("non-object value passed");
  2856     if (v && !("QueryInterface" in v))
  2858       throw new Error("must pass an nsISupports; use wrappedJSObject to ease " +
  2859                       "pain when using the server from JS");
  2862     this._objectState[k] = v;
  2863   },
  2865   /**
  2866 * Gets a content-type for the given file, first by checking for any custom
  2867 * MIME-types registered with this handler for the file's extension, second by
  2868 * asking the global MIME service for a content-type, and finally by failing
  2869 * over to application/octet-stream.
  2871 * @param file : nsIFile
  2872 * the nsIFile for which to get a file type
  2873 * @returns string
  2874 * the best content-type which can be determined for the file
  2875 */
  2876   _getTypeFromFile: function(file)
  2878     try
  2880       var name = file.leafName;
  2881       var dot = name.lastIndexOf(".");
  2882       if (dot > 0)
  2884         var ext = name.slice(dot + 1);
  2885         if (ext in this._mimeMappings)
  2886           return this._mimeMappings[ext];
  2888       return Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
  2889                .getService(Ci.nsIMIMEService)
  2890                .getTypeFromFile(file);
  2892     catch (e)
  2894       return "application/octet-stream";
  2896   },
  2898   /**
  2899 * Returns the nsILocalFile which corresponds to the path, as determined using
  2900 * all registered path->directory mappings and any paths which are explicitly
  2901 * overridden.
  2903 * @param path : string
  2904 * the server path for which a file should be retrieved, e.g. "/foo/bar"
  2905 * @throws HttpError
  2906 * when the correct action is the corresponding HTTP error (i.e., because no
  2907 * mapping was found for a directory in path, the referenced file doesn't
  2908 * exist, etc.)
  2909 * @returns nsILocalFile
  2910 * the file to be sent as the response to a request for the path
  2911 */
  2912   _getFileForPath: function(path)
  2914     // decode and add underscores as necessary
  2915     try
  2917       path = toInternalPath(path, true);
  2919     catch (e)
  2921       throw HTTP_400; // malformed path
  2924     // next, get the directory which contains this path
  2925     var pathMap = this._pathDirectoryMap;
  2927     // An example progression of tmp for a path "/foo/bar/baz/" might be:
  2928     // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", ""
  2929     var tmp = path.substring(1);
  2930     while (true)
  2932       // do we have a match for current head of the path?
  2933       var file = pathMap.get(tmp);
  2934       if (file)
  2936         // XXX hack; basically disable showing mapping for /foo/bar/ when the
  2937         // requested path was /foo/bar, because relative links on the page
  2938         // will all be incorrect -- we really need the ability to easily
  2939         // redirect here instead
  2940         if (tmp == path.substring(1) &&
  2941             tmp.length != 0 &&
  2942             tmp.charAt(tmp.length - 1) != "/")
  2943           file = null;
  2944         else
  2945           break;
  2948       // if we've finished trying all prefixes, exit
  2949       if (tmp == "")
  2950         break;
  2952       tmp = tmp.substring(0, tmp.lastIndexOf("/"));
  2955     // no mapping applies, so 404
  2956     if (!file)
  2957       throw HTTP_404;
  2960     // last, get the file for the path within the determined directory
  2961     var parentFolder = file.parent;
  2962     var dirIsRoot = (parentFolder == null);
  2964     // Strategy here is to append components individually, making sure we
  2965     // never move above the given directory; this allows paths such as
  2966     // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling";
  2967     // this component-wise approach also means the code works even on platforms
  2968     // which don't use "/" as the directory separator, such as Windows
  2969     var leafPath = path.substring(tmp.length + 1);
  2970     var comps = leafPath.split("/");
  2971     for (var i = 0, sz = comps.length; i < sz; i++)
  2973       var comp = comps[i];
  2975       if (comp == "..")
  2976         file = file.parent;
  2977       else if (comp == "." || comp == "")
  2978         continue;
  2979       else
  2980         file.append(comp);
  2982       if (!dirIsRoot && file.equals(parentFolder))
  2983         throw HTTP_403;
  2986     return file;
  2987   },
  2989   /**
  2990 * Writes the error page for the given HTTP error code over the given
  2991 * connection.
  2993 * @param errorCode : uint
  2994 * the HTTP error code to be used
  2995 * @param connection : Connection
  2996 * the connection on which the error occurred
  2997 */
  2998   handleError: function(errorCode, connection)
  3000     var response = new Response(connection);
  3002     dumpn("*** error in request: " + errorCode);
  3004     this._handleError(errorCode, new Request(connection.port), response);
  3005   },
  3007   /**
  3008 * Handles a request which generates the given error code, using the
  3009 * user-defined error handler if one has been set, gracefully falling back to
  3010 * the x00 status code if the code has no handler, and failing to status code
  3011 * 500 if all else fails.
  3013 * @param errorCode : uint
  3014 * the HTTP error which is to be returned
  3015 * @param metadata : Request
  3016 * metadata for the request, which will often be incomplete since this is an
  3017 * error
  3018 * @param response : Response
  3019 * an uninitialized Response should be initialized when this method
  3020 * completes with information which represents the desired error code in the
  3021 * ideal case or a fallback code in abnormal circumstances (i.e., 500 is a
  3022 * fallback for 505, per HTTP specs)
  3023 */
  3024   _handleError: function(errorCode, metadata, response)
  3026     if (!metadata)
  3027       throw Cr.NS_ERROR_NULL_POINTER;
  3029     var errorX00 = errorCode - (errorCode % 100);
  3031     try
  3033       if (!(errorCode in HTTP_ERROR_CODES))
  3034         dumpn("*** WARNING: requested invalid error: " + errorCode);
  3036       // RFC 2616 says that we should try to handle an error by its class if we
  3037       // can't otherwise handle it -- if that fails, we revert to handling it as
  3038       // a 500 internal server error, and if that fails we throw and shut down
  3039       // the server
  3041       // actually handle the error
  3042       try
  3044         if (errorCode in this._overrideErrors)
  3045           this._overrideErrors[errorCode](metadata, response);
  3046         else
  3047           this._defaultErrors[errorCode](metadata, response);
  3049       catch (e)
  3051         if (response.partiallySent())
  3053           response.abort(e);
  3054           return;
  3057         // don't retry the handler that threw
  3058         if (errorX00 == errorCode)
  3059           throw HTTP_500;
  3061         dumpn("*** error in handling for error code " + errorCode + ", " +
  3062               "falling back to " + errorX00 + "...");
  3063         response = new Response(response._connection);
  3064         if (errorX00 in this._overrideErrors)
  3065           this._overrideErrors[errorX00](metadata, response);
  3066         else if (errorX00 in this._defaultErrors)
  3067           this._defaultErrors[errorX00](metadata, response);
  3068         else
  3069           throw HTTP_500;
  3072     catch (e)
  3074       if (response.partiallySent())
  3076         response.abort();
  3077         return;
  3080       // we've tried everything possible for a meaningful error -- now try 500
  3081       dumpn("*** error in handling for error code " + errorX00 + ", falling " +
  3082             "back to 500...");
  3084       try
  3086         response = new Response(response._connection);
  3087         if (500 in this._overrideErrors)
  3088           this._overrideErrors[500](metadata, response);
  3089         else
  3090           this._defaultErrors[500](metadata, response);
  3092       catch (e2)
  3094         dumpn("*** multiple errors in default error handlers!");
  3095         dumpn("*** e == " + e + ", e2 == " + e2);
  3096         response.abort(e2);
  3097         return;
  3101     response.complete();
  3102   },
  3104   // FIELDS
  3106   /**
  3107 * This object contains the default handlers for the various HTTP error codes.
  3108 */
  3109   _defaultErrors:
  3111     400: function(metadata, response)
  3113       // none of the data in metadata is reliable, so hard-code everything here
  3114       response.setStatusLine("1.1", 400, "Bad Request");
  3115       response.setHeader("Content-Type", "text/plain", false);
  3117       var body = "Bad request\n";
  3118       response.bodyOutputStream.write(body, body.length);
  3119     },
  3120     403: function(metadata, response)
  3122       response.setStatusLine(metadata.httpVersion, 403, "Forbidden");
  3123       response.setHeader("Content-Type", "text/html", false);
  3125       var body = "<html>\
  3126 <head><title>403 Forbidden</title></head>\
  3127 <body>\
  3128 <h1>403 Forbidden</h1>\
  3129 </body>\
  3130 </html>";
  3131       response.bodyOutputStream.write(body, body.length);
  3132     },
  3133     404: function(metadata, response)
  3135       response.setStatusLine(metadata.httpVersion, 404, "Not Found");
  3136       response.setHeader("Content-Type", "text/html", false);
  3138       var body = "<html>\
  3139 <head><title>404 Not Found</title></head>\
  3140 <body>\
  3141 <h1>404 Not Found</h1>\
  3142 <p>\
  3143 <span style='font-family: monospace;'>" +
  3144                           htmlEscape(metadata.path) +
  3145                        "</span> was not found.\
  3146 </p>\
  3147 </body>\
  3148 </html>";
  3149       response.bodyOutputStream.write(body, body.length);
  3150     },
  3151     416: function(metadata, response)
  3153       response.setStatusLine(metadata.httpVersion,
  3154                             416,
  3155                             "Requested Range Not Satisfiable");
  3156       response.setHeader("Content-Type", "text/html", false);
  3158       var body = "<html>\
  3159 <head>\
  3160 <title>416 Requested Range Not Satisfiable</title></head>\
  3161 <body>\
  3162 <h1>416 Requested Range Not Satisfiable</h1>\
  3163 <p>The byte range was not valid for the\
  3164 requested resource.\
  3165 </p>\
  3166 </body>\
  3167 </html>";
  3168       response.bodyOutputStream.write(body, body.length);
  3169     },
  3170     500: function(metadata, response)
  3172       response.setStatusLine(metadata.httpVersion,
  3173                              500,
  3174                              "Internal Server Error");
  3175       response.setHeader("Content-Type", "text/html", false);
  3177       var body = "<html>\
  3178 <head><title>500 Internal Server Error</title></head>\
  3179 <body>\
  3180 <h1>500 Internal Server Error</h1>\
  3181 <p>Something's broken in this server and\
  3182 needs to be fixed.</p>\
  3183 </body>\
  3184 </html>";
  3185       response.bodyOutputStream.write(body, body.length);
  3186     },
  3187     501: function(metadata, response)
  3189       response.setStatusLine(metadata.httpVersion, 501, "Not Implemented");
  3190       response.setHeader("Content-Type", "text/html", false);
  3192       var body = "<html>\
  3193 <head><title>501 Not Implemented</title></head>\
  3194 <body>\
  3195 <h1>501 Not Implemented</h1>\
  3196 <p>This server is not (yet) Apache.</p>\
  3197 </body>\
  3198 </html>";
  3199       response.bodyOutputStream.write(body, body.length);
  3200     },
  3201     505: function(metadata, response)
  3203       response.setStatusLine("1.1", 505, "HTTP Version Not Supported");
  3204       response.setHeader("Content-Type", "text/html", false);
  3206       var body = "<html>\
  3207 <head><title>505 HTTP Version Not Supported</title></head>\
  3208 <body>\
  3209 <h1>505 HTTP Version Not Supported</h1>\
  3210 <p>This server only supports HTTP/1.0 and HTTP/1.1\
  3211 connections.</p>\
  3212 </body>\
  3213 </html>";
  3214       response.bodyOutputStream.write(body, body.length);
  3216   },
  3218   /**
  3219 * Contains handlers for the default set of URIs contained in this server.
  3220 */
  3221   _defaultPaths:
  3223     "/": function(metadata, response)
  3225       response.setStatusLine(metadata.httpVersion, 200, "OK");
  3226       response.setHeader("Content-Type", "text/html", false);
  3228       var body = "<html>\
  3229 <head><title>httpd.js</title></head>\
  3230 <body>\
  3231 <h1>httpd.js</h1>\
  3232 <p>If you're seeing this page, httpd.js is up and\
  3233 serving requests! Now set a base path and serve some\
  3234 files!</p>\
  3235 </body>\
  3236 </html>";
  3238       response.bodyOutputStream.write(body, body.length);
  3239     },
  3241     "/trace": function(metadata, response)
  3243       response.setStatusLine(metadata.httpVersion, 200, "OK");
  3244       response.setHeader("Content-Type", "text/plain", false);
  3246       var body = "Request-URI: " +
  3247                  metadata.scheme + "://" + metadata.host + ":" + metadata.port +
  3248                  metadata.path + "\n\n";
  3249       body += "Request (semantically equivalent, slightly reformatted):\n\n";
  3250       body += metadata.method + " " + metadata.path;
  3252       if (metadata.queryString)
  3253         body += "?" + metadata.queryString;
  3255       body += " HTTP/" + metadata.httpVersion + "\r\n";
  3257       var headEnum = metadata.headers;
  3258       while (headEnum.hasMoreElements())
  3260         var fieldName = headEnum.getNext()
  3261                                 .QueryInterface(Ci.nsISupportsString)
  3262                                 .data;
  3263         body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n";
  3266       response.bodyOutputStream.write(body, body.length);
  3269 };
  3272 /**
  3273 * Maps absolute paths to files on the local file system (as nsILocalFiles).
  3274 */
  3275 function FileMap()
  3277   /** Hash which will map paths to nsILocalFiles. */
  3278   this._map = {};
  3280 FileMap.prototype =
  3282   // PUBLIC API
  3284   /**
  3285 * Maps key to a clone of the nsILocalFile value if value is non-null;
  3286 * otherwise, removes any extant mapping for key.
  3288 * @param key : string
  3289 * string to which a clone of value is mapped
  3290 * @param value : nsILocalFile
  3291 * the file to map to key, or null to remove a mapping
  3292 */
  3293   put: function(key, value)
  3295     if (value)
  3296       this._map[key] = value.clone();
  3297     else
  3298       delete this._map[key];
  3299   },
  3301   /**
  3302 * Returns a clone of the nsILocalFile mapped to key, or null if no such
  3303 * mapping exists.
  3305 * @param key : string
  3306 * key to which the returned file maps
  3307 * @returns nsILocalFile
  3308 * a clone of the mapped file, or null if no mapping exists
  3309 */
  3310   get: function(key)
  3312     var val = this._map[key];
  3313     return val ? val.clone() : null;
  3315 };
  3318 // Response CONSTANTS
  3320 // token = *<any CHAR except CTLs or separators>
  3321 // CHAR = <any US-ASCII character (0-127)>
  3322 // CTL = <any US-ASCII control character (0-31) and DEL (127)>
  3323 // separators = "(" | ")" | "<" | ">" | "@"
  3324 // | "," | ";" | ":" | "\" | <">
  3325 // | "/" | "[" | "]" | "?" | "="
  3326 // | "{" | "}" | SP | HT
  3327 const IS_TOKEN_ARRAY =
  3328   [0, 0, 0, 0, 0, 0, 0, 0, // 0
  3329    0, 0, 0, 0, 0, 0, 0, 0, // 8
  3330    0, 0, 0, 0, 0, 0, 0, 0, // 16
  3331    0, 0, 0, 0, 0, 0, 0, 0, // 24
  3333    0, 1, 0, 1, 1, 1, 1, 1, // 32
  3334    0, 0, 1, 1, 0, 1, 1, 0, // 40
  3335    1, 1, 1, 1, 1, 1, 1, 1, // 48
  3336    1, 1, 0, 0, 0, 0, 0, 0, // 56
  3338    0, 1, 1, 1, 1, 1, 1, 1, // 64
  3339    1, 1, 1, 1, 1, 1, 1, 1, // 72
  3340    1, 1, 1, 1, 1, 1, 1, 1, // 80
  3341    1, 1, 1, 0, 0, 0, 1, 1, // 88
  3343    1, 1, 1, 1, 1, 1, 1, 1, // 96
  3344    1, 1, 1, 1, 1, 1, 1, 1, // 104
  3345    1, 1, 1, 1, 1, 1, 1, 1, // 112
  3346    1, 1, 1, 0, 1, 0, 1]; // 120
  3349 /**
  3350 * Determines whether the given character code is a CTL.
  3352 * @param code : uint
  3353 * the character code
  3354 * @returns boolean
  3355 * true if code is a CTL, false otherwise
  3356 */
  3357 function isCTL(code)
  3359   return (code >= 0 && code <= 31) || (code == 127);
  3362 /**
  3363 * Represents a response to an HTTP request, encapsulating all details of that
  3364 * response. This includes all headers, the HTTP version, status code and
  3365 * explanation, and the entity itself.
  3367 * @param connection : Connection
  3368 * the connection over which this response is to be written
  3369 */
  3370 function Response(connection)
  3372   /** The connection over which this response will be written. */
  3373   this._connection = connection;
  3375   /**
  3376 * The HTTP version of this response; defaults to 1.1 if not set by the
  3377 * handler.
  3378 */
  3379   this._httpVersion = nsHttpVersion.HTTP_1_1;
  3381   /**
  3382 * The HTTP code of this response; defaults to 200.
  3383 */
  3384   this._httpCode = 200;
  3386   /**
  3387 * The description of the HTTP code in this response; defaults to "OK".
  3388 */
  3389   this._httpDescription = "OK";
  3391   /**
  3392 * An nsIHttpHeaders object in which the headers in this response should be
  3393 * stored. This property is null after the status line and headers have been
  3394 * written to the network, and it may be modified up until it is cleared,
  3395 * except if this._finished is set first (in which case headers are written
  3396 * asynchronously in response to a finish() call not preceded by
  3397 * flushHeaders()).
  3398 */
  3399   this._headers = new nsHttpHeaders();
  3401   /**
  3402 * Set to true when this response is ended (completely constructed if possible
  3403 * and the connection closed); further actions on this will then fail.
  3404 */
  3405   this._ended = false;
  3407   /**
  3408 * A stream used to hold data written to the body of this response.
  3409 */
  3410   this._bodyOutputStream = null;
  3412   /**
  3413 * A stream containing all data that has been written to the body of this
  3414 * response so far. (Async handlers make the data contained in this
  3415 * unreliable as a way of determining content length in general, but auxiliary
  3416 * saved information can sometimes be used to guarantee reliability.)
  3417 */
  3418   this._bodyInputStream = null;
  3420   /**
  3421 * A stream copier which copies data to the network. It is initially null
  3422 * until replaced with a copier for response headers; when headers have been
  3423 * fully sent it is replaced with a copier for the response body, remaining
  3424 * so for the duration of response processing.
  3425 */
  3426   this._asyncCopier = null;
  3428   /**
  3429 * True if this response has been designated as being processed
  3430 * asynchronously rather than for the duration of a single call to
  3431 * nsIHttpRequestHandler.handle.
  3432 */
  3433   this._processAsync = false;
  3435   /**
  3436 * True iff finish() has been called on this, signaling that no more changes
  3437 * to this may be made.
  3438 */
  3439   this._finished = false;
  3441   /**
  3442 * True iff powerSeized() has been called on this, signaling that this
  3443 * response is to be handled manually by the response handler (which may then
  3444 * send arbitrary data in response, even non-HTTP responses).
  3445 */
  3446   this._powerSeized = false;
  3448 Response.prototype =
  3450   // PUBLIC CONSTRUCTION API
  3452   //
  3453   // see nsIHttpResponse.bodyOutputStream
  3454   //
  3455   get bodyOutputStream()
  3457     if (this._finished)
  3458       throw Cr.NS_ERROR_NOT_AVAILABLE;
  3460     if (!this._bodyOutputStream)
  3462       var pipe = new Pipe(true, false, Response.SEGMENT_SIZE, PR_UINT32_MAX,
  3463                           null);
  3464       this._bodyOutputStream = pipe.outputStream;
  3465       this._bodyInputStream = pipe.inputStream;
  3466       if (this._processAsync || this._powerSeized)
  3467         this._startAsyncProcessor();
  3470     return this._bodyOutputStream;
  3471   },
  3473   //
  3474   // see nsIHttpResponse.write
  3475   //
  3476   write: function(data)
  3478     if (this._finished)
  3479       throw Cr.NS_ERROR_NOT_AVAILABLE;
  3481     var dataAsString = String(data);
  3482     this.bodyOutputStream.write(dataAsString, dataAsString.length);
  3483   },
  3485   //
  3486   // see nsIHttpResponse.setStatusLine
  3487   //
  3488   setStatusLine: function(httpVersion, code, description)
  3490     if (!this._headers || this._finished || this._powerSeized)
  3491       throw Cr.NS_ERROR_NOT_AVAILABLE;
  3492     this._ensureAlive();
  3494     if (!(code >= 0 && code < 1000))
  3495       throw Cr.NS_ERROR_INVALID_ARG;
  3497     try
  3499       var httpVer;
  3500       // avoid version construction for the most common cases
  3501       if (!httpVersion || httpVersion == "1.1")
  3502         httpVer = nsHttpVersion.HTTP_1_1;
  3503       else if (httpVersion == "1.0")
  3504         httpVer = nsHttpVersion.HTTP_1_0;
  3505       else
  3506         httpVer = new nsHttpVersion(httpVersion);
  3508     catch (e)
  3510       throw Cr.NS_ERROR_INVALID_ARG;
  3513     // Reason-Phrase = *<TEXT, excluding CR, LF>
  3514     // TEXT = <any OCTET except CTLs, but including LWS>
  3515     //
  3516     // XXX this ends up disallowing octets which aren't Unicode, I think -- not
  3517     // much to do if description is IDL'd as string
  3518     if (!description)
  3519       description = "";
  3520     for (var i = 0; i < description.length; i++)
  3521       if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t")
  3522         throw Cr.NS_ERROR_INVALID_ARG;
  3524     // set the values only after validation to preserve atomicity
  3525     this._httpDescription = description;
  3526     this._httpCode = code;
  3527     this._httpVersion = httpVer;
  3528   },
  3530   //
  3531   // see nsIHttpResponse.setHeader
  3532   //
  3533   setHeader: function(name, value, merge)
  3535     if (!this._headers || this._finished || this._powerSeized)
  3536       throw Cr.NS_ERROR_NOT_AVAILABLE;
  3537     this._ensureAlive();
  3539     this._headers.setHeader(name, value, merge);
  3540   },
  3542   //
  3543   // see nsIHttpResponse.processAsync
  3544   //
  3545   processAsync: function()
  3547     if (this._finished)
  3548       throw Cr.NS_ERROR_UNEXPECTED;
  3549     if (this._powerSeized)
  3550       throw Cr.NS_ERROR_NOT_AVAILABLE;
  3551     if (this._processAsync)
  3552       return;
  3553     this._ensureAlive();
  3555     dumpn("*** processing connection " + this._connection.number + " async");
  3556     this._processAsync = true;
  3558     /*
  3559 * Either the bodyOutputStream getter or this method is responsible for
  3560 * starting the asynchronous processor and catching writes of data to the
  3561 * response body of async responses as they happen, for the purpose of
  3562 * forwarding those writes to the actual connection's output stream.
  3563 * If bodyOutputStream is accessed first, calling this method will create
  3564 * the processor (when it first is clear that body data is to be written
  3565 * immediately, not buffered). If this method is called first, accessing
  3566 * bodyOutputStream will create the processor. If only this method is
  3567 * called, we'll write nothing, neither headers nor the nonexistent body,
  3568 * until finish() is called. Since that delay is easily avoided by simply
  3569 * getting bodyOutputStream or calling write(""), we don't worry about it.
  3570 */
  3571     if (this._bodyOutputStream && !this._asyncCopier)
  3572       this._startAsyncProcessor();
  3573   },
  3575   //
  3576   // see nsIHttpResponse.seizePower
  3577   //
  3578   seizePower: function()
  3580     if (this._processAsync)
  3581       throw Cr.NS_ERROR_NOT_AVAILABLE;
  3582     if (this._finished)
  3583       throw Cr.NS_ERROR_UNEXPECTED;
  3584     if (this._powerSeized)
  3585       return;
  3586     this._ensureAlive();
  3588     dumpn("*** forcefully seizing power over connection " +
  3589           this._connection.number + "...");
  3591     // Purge any already-written data without sending it. We could as easily
  3592     // swap out the streams entirely, but that makes it possible to acquire and
  3593     // unknowingly use a stale reference, so we require there only be one of
  3594     // each stream ever for any response to avoid this complication.
  3595     if (this._asyncCopier)
  3596       this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED);
  3597     this._asyncCopier = null;
  3598     if (this._bodyOutputStream)
  3600       var input = new BinaryInputStream(this._bodyInputStream);
  3601       var avail;
  3602       while ((avail = input.available()) > 0)
  3603         input.readByteArray(avail);
  3606     this._powerSeized = true;
  3607     if (this._bodyOutputStream)
  3608       this._startAsyncProcessor();
  3609   },
  3611   //
  3612   // see nsIHttpResponse.finish
  3613   //
  3614   finish: function()
  3616     if (!this._processAsync && !this._powerSeized)
  3617       throw Cr.NS_ERROR_UNEXPECTED;
  3618     if (this._finished)
  3619       return;
  3621     dumpn("*** finishing connection " + this._connection.number);
  3622     this._startAsyncProcessor(); // in case bodyOutputStream was never accessed
  3623     if (this._bodyOutputStream)
  3624       this._bodyOutputStream.close();
  3625     this._finished = true;
  3626   },
  3629   // NSISUPPORTS
  3631   //
  3632   // see nsISupports.QueryInterface
  3633   //
  3634   QueryInterface: function(iid)
  3636     if (iid.equals(Ci.nsIHttpResponse) || iid.equals(Ci.nsISupports))
  3637       return this;
  3639     throw Cr.NS_ERROR_NO_INTERFACE;
  3640   },
  3643   // POST-CONSTRUCTION API (not exposed externally)
  3645   /**
  3646 * The HTTP version number of this, as a string (e.g. "1.1").
  3647 */
  3648   get httpVersion()
  3650     this._ensureAlive();
  3651     return this._httpVersion.toString();
  3652   },
  3654   /**
  3655 * The HTTP status code of this response, as a string of three characters per
  3656 * RFC 2616.
  3657 */
  3658   get httpCode()
  3660     this._ensureAlive();
  3662     var codeString = (this._httpCode < 10 ? "0" : "") +
  3663                      (this._httpCode < 100 ? "0" : "") +
  3664                      this._httpCode;
  3665     return codeString;
  3666   },
  3668   /**
  3669 * The description of the HTTP status code of this response, or "" if none is
  3670 * set.
  3671 */
  3672   get httpDescription()
  3674     this._ensureAlive();
  3676     return this._httpDescription;
  3677   },
  3679   /**
  3680 * The headers in this response, as an nsHttpHeaders object.
  3681 */
  3682   get headers()
  3684     this._ensureAlive();
  3686     return this._headers;
  3687   },
  3689   //
  3690   // see nsHttpHeaders.getHeader
  3691   //
  3692   getHeader: function(name)
  3694     this._ensureAlive();
  3696     return this._headers.getHeader(name);
  3697   },
  3699   /**
  3700 * Determines whether this response may be abandoned in favor of a newly
  3701 * constructed response. A response may be abandoned only if it is not being
  3702 * sent asynchronously and if raw control over it has not been taken from the
  3703 * server.
  3705 * @returns boolean
  3706 * true iff no data has been written to the network
  3707 */
  3708   partiallySent: function()
  3710     dumpn("*** partiallySent()");
  3711     return this._processAsync || this._powerSeized;
  3712   },
  3714   /**
  3715 * If necessary, kicks off the remaining request processing needed to be done
  3716 * after a request handler performs its initial work upon this response.
  3717 */
  3718   complete: function()
  3720     dumpn("*** complete()");
  3721     if (this._processAsync || this._powerSeized)
  3723       NS_ASSERT(this._processAsync ^ this._powerSeized,
  3724                 "can't both send async and relinquish power");
  3725       return;
  3728     NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?");
  3730     this._startAsyncProcessor();
  3732     // Now make sure we finish processing this request!
  3733     if (this._bodyOutputStream)
  3734       this._bodyOutputStream.close();
  3735   },
  3737   /**
  3738 * Abruptly ends processing of this response, usually due to an error in an
  3739 * incoming request but potentially due to a bad error handler. Since we
  3740 * cannot handle the error in the usual way (giving an HTTP error page in
  3741 * response) because data may already have been sent (or because the response
  3742 * might be expected to have been generated asynchronously or completely from
  3743 * scratch by the handler), we stop processing this response and abruptly
  3744 * close the connection.
  3746 * @param e : Error
  3747 * the exception which precipitated this abort, or null if no such exception
  3748 * was generated
  3749 */
  3750   abort: function(e)
  3752     dumpn("*** abort(<" + e + ">)");
  3754     // This response will be ended by the processor if one was created.
  3755     var copier = this._asyncCopier;
  3756     if (copier)
  3758       // We dispatch asynchronously here so that any pending writes of data to
  3759       // the connection will be deterministically written. This makes it easier
  3760       // to specify exact behavior, and it makes observable behavior more
  3761       // predictable for clients. Note that the correctness of this depends on
  3762       // callbacks in response to _waitToReadData in WriteThroughCopier
  3763       // happening asynchronously with respect to the actual writing of data to
  3764       // bodyOutputStream, as they currently do; if they happened synchronously,
  3765       // an event which ran before this one could write more data to the
  3766       // response body before we get around to canceling the copier. We have
  3767       // tests for this in test_seizepower.js, however, and I can't think of a
  3768       // way to handle both cases without removing bodyOutputStream access and
  3769       // moving its effective write(data, length) method onto Response, which
  3770       // would be slower and require more code than this anyway.
  3771       gThreadManager.currentThread.dispatch({
  3772         run: function()
  3774           dumpn("*** canceling copy asynchronously...");
  3775           copier.cancel(Cr.NS_ERROR_UNEXPECTED);
  3777       }, Ci.nsIThread.DISPATCH_NORMAL);
  3779     else
  3781       this.end();
  3783   },
  3785   /**
  3786 * Closes this response's network connection, marks the response as finished,
  3787 * and notifies the server handler that the request is done being processed.
  3788 */
  3789   end: function()
  3791     NS_ASSERT(!this._ended, "ending this response twice?!?!");
  3793     this._connection.close();
  3794     if (this._bodyOutputStream)
  3795       this._bodyOutputStream.close();
  3797     this._finished = true;
  3798     this._ended = true;
  3799   },
  3801   // PRIVATE IMPLEMENTATION
  3803   /**
  3804 * Sends the status line and headers of this response if they haven't been
  3805 * sent and initiates the process of copying data written to this response's
  3806 * body to the network.
  3807 */
  3808   _startAsyncProcessor: function()
  3810     dumpn("*** _startAsyncProcessor()");
  3812     // Handle cases where we're being called a second time. The former case
  3813     // happens when this is triggered both by complete() and by processAsync(),
  3814     // while the latter happens when processAsync() in conjunction with sent
  3815     // data causes abort() to be called.
  3816     if (this._asyncCopier || this._ended)
  3818       dumpn("*** ignoring second call to _startAsyncProcessor");
  3819       return;
  3822     // Send headers if they haven't been sent already and should be sent, then
  3823     // asynchronously continue to send the body.
  3824     if (this._headers && !this._powerSeized)
  3826       this._sendHeaders();
  3827       return;
  3830     this._headers = null;
  3831     this._sendBody();
  3832   },
  3834   /**
  3835 * Signals that all modifications to the response status line and headers are
  3836 * complete and then sends that data over the network to the client. Once
  3837 * this method completes, a different response to the request that resulted
  3838 * in this response cannot be sent -- the only possible action in case of
  3839 * error is to abort the response and close the connection.
  3840 */
  3841   _sendHeaders: function()
  3843     dumpn("*** _sendHeaders()");
  3845     NS_ASSERT(this._headers);
  3846     NS_ASSERT(!this._powerSeized);
  3848     // request-line
  3849     var statusLine = "HTTP/" + this.httpVersion + " " +
  3850                      this.httpCode + " " +
  3851                      this.httpDescription + "\r\n";
  3853     // header post-processing
  3855     var headers = this._headers;
  3856     headers.setHeader("Connection", "close", false);
  3857     headers.setHeader("Server", "httpd.js", false);
  3858     if (!headers.hasHeader("Date"))
  3859       headers.setHeader("Date", toDateString(Date.now()), false);
  3861     // Any response not being processed asynchronously must have an associated
  3862     // Content-Length header for reasons of backwards compatibility with the
  3863     // initial server, which fully buffered every response before sending it.
  3864     // Beyond that, however, it's good to do this anyway because otherwise it's
  3865     // impossible to test behaviors that depend on the presence or absence of a
  3866     // Content-Length header.
  3867     if (!this._processAsync)
  3869       dumpn("*** non-async response, set Content-Length");
  3871       var bodyStream = this._bodyInputStream;
  3872       var avail = bodyStream ? bodyStream.available() : 0;
  3874       // XXX assumes stream will always report the full amount of data available
  3875       headers.setHeader("Content-Length", "" + avail, false);
  3879     // construct and send response
  3880     dumpn("*** header post-processing completed, sending response head...");
  3882     // request-line
  3883     var preambleData = [statusLine];
  3885     // headers
  3886     var headEnum = headers.enumerator;
  3887     while (headEnum.hasMoreElements())
  3889       var fieldName = headEnum.getNext()
  3890                               .QueryInterface(Ci.nsISupportsString)
  3891                               .data;
  3892       var values = headers.getHeaderValues(fieldName);
  3893       for (var i = 0, sz = values.length; i < sz; i++)
  3894         preambleData.push(fieldName + ": " + values[i] + "\r\n");
  3897     // end request-line/headers
  3898     preambleData.push("\r\n");
  3900     var preamble = preambleData.join("");
  3902     var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null);
  3903     responseHeadPipe.outputStream.write(preamble, preamble.length);
  3905     var response = this;
  3906     var copyObserver =
  3908         onStartRequest: function(request, cx)
  3910           dumpn("*** preamble copying started");
  3911         },
  3913         onStopRequest: function(request, cx, statusCode)
  3915           dumpn("*** preamble copying complete " +
  3916                 "[status=0x" + statusCode.toString(16) + "]");
  3918           if (!components.isSuccessCode(statusCode))
  3920             dumpn("!!! header copying problems: non-success statusCode, " +
  3921                   "ending response");
  3923             response.end();
  3925           else
  3927             response._sendBody();
  3929         },
  3931         QueryInterface: function(aIID)
  3933           if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports))
  3934             return this;
  3936           throw Cr.NS_ERROR_NO_INTERFACE;
  3938       };
  3940     var headerCopier = this._asyncCopier =
  3941       new WriteThroughCopier(responseHeadPipe.inputStream,
  3942                              this._connection.output,
  3943                              copyObserver, null);
  3945     responseHeadPipe.outputStream.close();
  3947     // Forbid setting any more headers or modifying the request line.
  3948     this._headers = null;
  3949   },
  3951   /**
  3952 * Asynchronously writes the body of the response (or the entire response, if
  3953 * seizePower() has been called) to the network.
  3954 */
  3955   _sendBody: function()
  3957     dumpn("*** _sendBody");
  3959     NS_ASSERT(!this._headers, "still have headers around but sending body?");
  3961     // If no body data was written, we're done
  3962     if (!this._bodyInputStream)
  3964       dumpn("*** empty body, response finished");
  3965       this.end();
  3966       return;
  3969     var response = this;
  3970     var copyObserver =
  3972         onStartRequest: function(request, context)
  3974           dumpn("*** onStartRequest");
  3975         },
  3977         onStopRequest: function(request, cx, statusCode)
  3979           dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]");
  3981           if (statusCode === Cr.NS_BINDING_ABORTED)
  3983             dumpn("*** terminating copy observer without ending the response");
  3985           else
  3987             if (!components.isSuccessCode(statusCode))
  3988               dumpn("*** WARNING: non-success statusCode in onStopRequest");
  3990             response.end();
  3992         },
  3994         QueryInterface: function(aIID)
  3996           if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports))
  3997             return this;
  3999           throw Cr.NS_ERROR_NO_INTERFACE;
  4001       };
  4003     dumpn("*** starting async copier of body data...");
  4004     this._asyncCopier =
  4005       new WriteThroughCopier(this._bodyInputStream, this._connection.output,
  4006                             copyObserver, null);
  4007   },
  4009   /** Ensures that this hasn't been ended. */
  4010   _ensureAlive: function()
  4012     NS_ASSERT(!this._ended, "not handling response lifetime correctly");
  4014 };
  4016 /**
  4017 * Size of the segments in the buffer used in storing response data and writing
  4018 * it to the socket.
  4019 */
  4020 Response.SEGMENT_SIZE = 8192;
  4022 /** Serves double duty in WriteThroughCopier implementation. */
  4023 function notImplemented()
  4025   throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  4028 /** Returns true iff the given exception represents stream closure. */
  4029 function streamClosed(e)
  4031   return e === Cr.NS_BASE_STREAM_CLOSED ||
  4032          (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED);
  4035 /** Returns true iff the given exception represents a blocked stream. */
  4036 function wouldBlock(e)
  4038   return e === Cr.NS_BASE_STREAM_WOULD_BLOCK ||
  4039          (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK);
  4042 /**
  4043 * Copies data from source to sink as it becomes available, when that data can
  4044 * be written to sink without blocking.
  4046 * @param source : nsIAsyncInputStream
  4047 * the stream from which data is to be read
  4048 * @param sink : nsIAsyncOutputStream
  4049 * the stream to which data is to be copied
  4050 * @param observer : nsIRequestObserver
  4051 * an observer which will be notified when the copy starts and finishes
  4052 * @param context : nsISupports
  4053 * context passed to observer when notified of start/stop
  4054 * @throws NS_ERROR_NULL_POINTER
  4055 * if source, sink, or observer are null
  4056 */
  4057 function WriteThroughCopier(source, sink, observer, context)
  4059   if (!source || !sink || !observer)
  4060     throw Cr.NS_ERROR_NULL_POINTER;
  4062   /** Stream from which data is being read. */
  4063   this._source = source;
  4065   /** Stream to which data is being written. */
  4066   this._sink = sink;
  4068   /** Observer watching this copy. */
  4069   this._observer = observer;
  4071   /** Context for the observer watching this. */
  4072   this._context = context;
  4074   /**
  4075 * True iff this is currently being canceled (cancel has been called, the
  4076 * callback may not yet have been made).
  4077 */
  4078   this._canceled = false;
  4080   /**
  4081 * False until all data has been read from input and written to output, at
  4082 * which point this copy is completed and cancel() is asynchronously called.
  4083 */
  4084   this._completed = false;
  4086   /** Required by nsIRequest, meaningless. */
  4087   this.loadFlags = 0;
  4088   /** Required by nsIRequest, meaningless. */
  4089   this.loadGroup = null;
  4090   /** Required by nsIRequest, meaningless. */
  4091   this.name = "response-body-copy";
  4093   /** Status of this request. */
  4094   this.status = Cr.NS_OK;
  4096   /** Arrays of byte strings waiting to be written to output. */
  4097   this._pendingData = [];
  4099   // start copying
  4100   try
  4102     observer.onStartRequest(this, context);
  4103     this._waitToReadData();
  4104     this._waitForSinkClosure();
  4106   catch (e)
  4108     dumpn("!!! error starting copy: " + e +
  4109           ("lineNumber" in e ? ", line " + e.lineNumber : ""));
  4110     dumpn(e.stack);
  4111     this.cancel(Cr.NS_ERROR_UNEXPECTED);
  4114 WriteThroughCopier.prototype =
  4116   /* nsISupports implementation */
  4118   QueryInterface: function(iid)
  4120     if (iid.equals(Ci.nsIInputStreamCallback) ||
  4121         iid.equals(Ci.nsIOutputStreamCallback) ||
  4122         iid.equals(Ci.nsIRequest) ||
  4123         iid.equals(Ci.nsISupports))
  4125       return this;
  4128     throw Cr.NS_ERROR_NO_INTERFACE;
  4129   },
  4132   // NSIINPUTSTREAMCALLBACK
  4134   /**
  4135 * Receives a more-data-in-input notification and writes the corresponding
  4136 * data to the output.
  4138 * @param input : nsIAsyncInputStream
  4139 * the input stream on whose data we have been waiting
  4140 */
  4141   onInputStreamReady: function(input)
  4143     if (this._source === null)
  4144       return;
  4146     dumpn("*** onInputStreamReady");
  4148     //
  4149     // Ordinarily we'll read a non-zero amount of data from input, queue it up
  4150     // to be written and then wait for further callbacks. The complications in
  4151     // this method are the cases where we deviate from that behavior when errors
  4152     // occur or when copying is drawing to a finish.
  4153     //
  4154     // The edge cases when reading data are:
  4155     //
  4156     // Zero data is read
  4157     // If zero data was read, we're at the end of available data, so we can
  4158     // should stop reading and move on to writing out what we have (or, if
  4159     // we've already done that, onto notifying of completion).
  4160     // A stream-closed exception is thrown
  4161     // This is effectively a less kind version of zero data being read; the
  4162     // only difference is that we notify of completion with that result
  4163     // rather than with NS_OK.
  4164     // Some other exception is thrown
  4165     // This is the least kind result. We don't know what happened, so we
  4166     // act as though the stream closed except that we notify of completion
  4167     // with the result NS_ERROR_UNEXPECTED.
  4168     //
  4170     var bytesWanted = 0, bytesConsumed = -1;
  4171     try
  4173       input = new BinaryInputStream(input);
  4175       bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE);
  4176       dumpn("*** input wanted: " + bytesWanted);
  4178       if (bytesWanted > 0)
  4180         var data = input.readByteArray(bytesWanted);
  4181         bytesConsumed = data.length;
  4182         this._pendingData.push(String.fromCharCode.apply(String, data));
  4185       dumpn("*** " + bytesConsumed + " bytes read");
  4187       // Handle the zero-data edge case in the same place as all other edge
  4188       // cases are handled.
  4189       if (bytesWanted === 0)
  4190         throw Cr.NS_BASE_STREAM_CLOSED;
  4192     catch (e)
  4194       if (streamClosed(e))
  4196         dumpn("*** input stream closed");
  4197         e = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED;
  4199       else
  4201         dumpn("!!! unexpected error reading from input, canceling: " + e);
  4202         e = Cr.NS_ERROR_UNEXPECTED;
  4205       this._doneReadingSource(e);
  4206       return;
  4209     var pendingData = this._pendingData;
  4211     NS_ASSERT(bytesConsumed > 0);
  4212     NS_ASSERT(pendingData.length > 0, "no pending data somehow?");
  4213     NS_ASSERT(pendingData[pendingData.length - 1].length > 0,
  4214               "buffered zero bytes of data?");
  4216     NS_ASSERT(this._source !== null);
  4218     // Reading has gone great, and we've gotten data to write now. What if we
  4219     // don't have a place to write that data, because output went away just
  4220     // before this read? Drop everything on the floor, including new data, and
  4221     // cancel at this point.
  4222     if (this._sink === null)
  4224       pendingData.length = 0;
  4225       this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED);
  4226       return;
  4229     // Okay, we've read the data, and we know we have a place to write it. We
  4230     // need to queue up the data to be written, but *only* if none is queued
  4231     // already -- if data's already queued, the code that actually writes the
  4232     // data will make sure to wait on unconsumed pending data.
  4233     try
  4235       if (pendingData.length === 1)
  4236         this._waitToWriteData();
  4238     catch (e)
  4240       dumpn("!!! error waiting to write data just read, swallowing and " +
  4241             "writing only what we already have: " + e);
  4242       this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
  4243       return;
  4246     // Whee! We successfully read some data, and it's successfully queued up to
  4247     // be written. All that remains now is to wait for more data to read.
  4248     try
  4250       this._waitToReadData();
  4252     catch (e)
  4254       dumpn("!!! error waiting to read more data: " + e);
  4255       this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED);
  4257   },
  4260   // NSIOUTPUTSTREAMCALLBACK
  4262   /**
  4263 * Callback when data may be written to the output stream without blocking, or
  4264 * when the output stream has been closed.
  4266 * @param output : nsIAsyncOutputStream
  4267 * the output stream on whose writability we've been waiting, also known as
  4268 * this._sink
  4269 */
  4270   onOutputStreamReady: function(output)
  4272     if (this._sink === null)
  4273       return;
  4275     dumpn("*** onOutputStreamReady");
  4277     var pendingData = this._pendingData;
  4278     if (pendingData.length === 0)
  4280       // There's no pending data to write. The only way this can happen is if
  4281       // we're waiting on the output stream's closure, so we can respond to a
  4282       // copying failure as quickly as possible (rather than waiting for data to
  4283       // be available to read and then fail to be copied). Therefore, we must
  4284       // be done now -- don't bother to attempt to write anything and wrap
  4285       // things up.
  4286       dumpn("!!! output stream closed prematurely, ending copy");
  4288       this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
  4289       return;
  4293     NS_ASSERT(pendingData[0].length > 0, "queued up an empty quantum?");
  4295     //
  4296     // Write out the first pending quantum of data. The possible errors here
  4297     // are:
  4298     //
  4299     // The write might fail because we can't write that much data
  4300     // Okay, we've written what we can now, so re-queue what's left and
  4301     // finish writing it out later.
  4302     // The write failed because the stream was closed
  4303     // Discard pending data that we can no longer write, stop reading, and
  4304     // signal that copying finished.
  4305     // Some other error occurred.
  4306     // Same as if the stream were closed, but notify with the status
  4307     // NS_ERROR_UNEXPECTED so the observer knows something was wonky.
  4308     //
  4310     try
  4312       var quantum = pendingData[0];
  4314       // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on
  4315       // undefined behavior! We're only using this because writeByteArray
  4316       // is unusably broken for asynchronous output streams; see bug 532834
  4317       // for details.
  4318       var bytesWritten = output.write(quantum, quantum.length);
  4319       if (bytesWritten === quantum.length)
  4320         pendingData.shift();
  4321       else
  4322         pendingData[0] = quantum.substring(bytesWritten);
  4324       dumpn("*** wrote " + bytesWritten + " bytes of data");
  4326     catch (e)
  4328       if (wouldBlock(e))
  4330         NS_ASSERT(pendingData.length > 0,
  4331                   "stream-blocking exception with no data to write?");
  4332         NS_ASSERT(pendingData[0].length > 0,
  4333                   "stream-blocking exception with empty quantum?");
  4334         this._waitToWriteData();
  4335         return;
  4338       if (streamClosed(e))
  4339         dumpn("!!! output stream prematurely closed, signaling error...");
  4340       else
  4341         dumpn("!!! unknown error: " + e + ", quantum=" + quantum);
  4343       this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
  4344       return;
  4347     // The day is ours! Quantum written, now let's see if we have more data
  4348     // still to write.
  4349     try
  4351       if (pendingData.length > 0)
  4353         this._waitToWriteData();
  4354         return;
  4357     catch (e)
  4359       dumpn("!!! unexpected error waiting to write pending data: " + e);
  4360       this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
  4361       return;
  4364     // Okay, we have no more pending data to write -- but might we get more in
  4365     // the future?
  4366     if (this._source !== null)
  4368       /*
  4369 * If we might, then wait for the output stream to be closed. (We wait
  4370 * only for closure because we have no data to write -- and if we waited
  4371 * for a specific amount of data, we would get repeatedly notified for no
  4372 * reason if over time the output stream permitted more and more data to
  4373 * be written to it without blocking.)
  4374 */
  4375        this._waitForSinkClosure();
  4377     else
  4379       /*
  4380 * On the other hand, if we can't have more data because the input
  4381 * stream's gone away, then it's time to notify of copy completion.
  4382 * Victory!
  4383 */
  4384       this._sink = null;
  4385       this._cancelOrDispatchCancelCallback(Cr.NS_OK);
  4387   },
  4390   // NSIREQUEST
  4392   /** Returns true if the cancel observer hasn't been notified yet. */
  4393   isPending: function()
  4395     return !this._completed;
  4396   },
  4398   /** Not implemented, don't use! */
  4399   suspend: notImplemented,
  4400   /** Not implemented, don't use! */
  4401   resume: notImplemented,
  4403   /**
  4404 * Cancels data reading from input, asynchronously writes out any pending
  4405 * data, and causes the observer to be notified with the given error code when
  4406 * all writing has finished.
  4408 * @param status : nsresult
  4409 * the status to pass to the observer when data copying has been canceled
  4410 */
  4411   cancel: function(status)
  4413     dumpn("*** cancel(" + status.toString(16) + ")");
  4415     if (this._canceled)
  4417       dumpn("*** suppressing a late cancel");
  4418       return;
  4421     this._canceled = true;
  4422     this.status = status;
  4424     // We could be in the middle of absolutely anything at this point. Both
  4425     // input and output might still be around, we might have pending data to
  4426     // write, and in general we know nothing about the state of the world. We
  4427     // therefore must assume everything's in progress and take everything to its
  4428     // final steady state (or so far as it can go before we need to finish
  4429     // writing out remaining data).
  4431     this._doneReadingSource(status);
  4432   },
  4435   // PRIVATE IMPLEMENTATION
  4437   /**
  4438 * Stop reading input if we haven't already done so, passing e as the status
  4439 * when closing the stream, and kick off a copy-completion notice if no more
  4440 * data remains to be written.
  4442 * @param e : nsresult
  4443 * the status to be used when closing the input stream
  4444 */
  4445   _doneReadingSource: function(e)
  4447     dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")");
  4449     this._finishSource(e);
  4450     if (this._pendingData.length === 0)
  4451       this._sink = null;
  4452     else
  4453       NS_ASSERT(this._sink !== null, "null output?");
  4455     // If we've written out all data read up to this point, then it's time to
  4456     // signal completion.
  4457     if (this._sink === null)
  4459       NS_ASSERT(this._pendingData.length === 0, "pending data still?");
  4460       this._cancelOrDispatchCancelCallback(e);
  4462   },
  4464   /**
  4465 * Stop writing output if we haven't already done so, discard any data that
  4466 * remained to be sent, close off input if it wasn't already closed, and kick
  4467 * off a copy-completion notice.
  4469 * @param e : nsresult
  4470 * the status to be used when closing input if it wasn't already closed
  4471 */
  4472   _doneWritingToSink: function(e)
  4474     dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")");
  4476     this._pendingData.length = 0;
  4477     this._sink = null;
  4478     this._doneReadingSource(e);
  4479   },
  4481   /**
  4482 * Completes processing of this copy: either by canceling the copy if it
  4483 * hasn't already been canceled using the provided status, or by dispatching
  4484 * the cancel callback event (with the originally provided status, of course)
  4485 * if it already has been canceled.
  4487 * @param status : nsresult
  4488 * the status code to use to cancel this, if this hasn't already been
  4489 * canceled
  4490 */
  4491   _cancelOrDispatchCancelCallback: function(status)
  4493     dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")");
  4495     NS_ASSERT(this._source === null, "should have finished input");
  4496     NS_ASSERT(this._sink === null, "should have finished output");
  4497     NS_ASSERT(this._pendingData.length === 0, "should have no pending data");
  4499     if (!this._canceled)
  4501       this.cancel(status);
  4502       return;
  4505     var self = this;
  4506     var event =
  4508         run: function()
  4510           dumpn("*** onStopRequest async callback");
  4512           self._completed = true;
  4513           try
  4515             self._observer.onStopRequest(self, self._context, self.status);
  4517           catch (e)
  4519             NS_ASSERT(false,
  4520                       "how are we throwing an exception here? we control " +
  4521                       "all the callers! " + e);
  4524       };
  4526     gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
  4527   },
  4529   /**
  4530 * Kicks off another wait for more data to be available from the input stream.
  4531 */
  4532   _waitToReadData: function()
  4534     dumpn("*** _waitToReadData");
  4535     this._source.asyncWait(this, 0, Response.SEGMENT_SIZE,
  4536                            gThreadManager.mainThread);
  4537   },
  4539   /**
  4540 * Kicks off another wait until data can be written to the output stream.
  4541 */
  4542   _waitToWriteData: function()
  4544     dumpn("*** _waitToWriteData");
  4546     var pendingData = this._pendingData;
  4547     NS_ASSERT(pendingData.length > 0, "no pending data to write?");
  4548     NS_ASSERT(pendingData[0].length > 0, "buffered an empty write?");
  4550     this._sink.asyncWait(this, 0, pendingData[0].length,
  4551                          gThreadManager.mainThread);
  4552   },
  4554   /**
  4555 * Kicks off a wait for the sink to which data is being copied to be closed.
  4556 * We wait for stream closure when we don't have any data to be copied, rather
  4557 * than waiting to write a specific amount of data. We can't wait to write
  4558 * data because the sink might be infinitely writable, and if no data appears
  4559 * in the source for a long time we might have to spin quite a bit waiting to
  4560 * write, waiting to write again, &c. Waiting on stream closure instead means
  4561 * we'll get just one notification if the sink dies. Note that when data
  4562 * starts arriving from the sink we'll resume waiting for data to be written,
  4563 * dropping this closure-only callback entirely.
  4564 */
  4565   _waitForSinkClosure: function()
  4567     dumpn("*** _waitForSinkClosure");
  4569     this._sink.asyncWait(this, Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY, 0,
  4570                          gThreadManager.mainThread);
  4571   },
  4573   /**
  4574 * Closes input with the given status, if it hasn't already been closed;
  4575 * otherwise a no-op.
  4577 * @param status : nsresult
  4578 * status code use to close the source stream if necessary
  4579 */
  4580   _finishSource: function(status)
  4582     dumpn("*** _finishSource(" + status.toString(16) + ")");
  4584     if (this._source !== null)
  4586       this._source.closeWithStatus(status);
  4587       this._source = null;
  4590 };
  4593 /**
  4594 * A container for utility functions used with HTTP headers.
  4595 */
  4596 const headerUtils =
  4598   /**
  4599 * Normalizes fieldName (by converting it to lowercase) and ensures it is a
  4600 * valid header field name (although not necessarily one specified in RFC
  4601 * 2616).
  4603 * @throws NS_ERROR_INVALID_ARG
  4604 * if fieldName does not match the field-name production in RFC 2616
  4605 * @returns string
  4606 * fieldName converted to lowercase if it is a valid header, for characters
  4607 * where case conversion is possible
  4608 */
  4609   normalizeFieldName: function(fieldName)
  4611     if (fieldName == "")
  4612       throw Cr.NS_ERROR_INVALID_ARG;
  4614     for (var i = 0, sz = fieldName.length; i < sz; i++)
  4616       if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)])
  4618         dumpn(fieldName + " is not a valid header field name!");
  4619         throw Cr.NS_ERROR_INVALID_ARG;
  4623     return fieldName.toLowerCase();
  4624   },
  4626   /**
  4627 * Ensures that fieldValue is a valid header field value (although not
  4628 * necessarily as specified in RFC 2616 if the corresponding field name is
  4629 * part of the HTTP protocol), normalizes the value if it is, and
  4630 * returns the normalized value.
  4632 * @param fieldValue : string
  4633 * a value to be normalized as an HTTP header field value
  4634 * @throws NS_ERROR_INVALID_ARG
  4635 * if fieldValue does not match the field-value production in RFC 2616
  4636 * @returns string
  4637 * fieldValue as a normalized HTTP header field value
  4638 */
  4639   normalizeFieldValue: function(fieldValue)
  4641     // field-value = *( field-content | LWS )
  4642     // field-content = <the OCTETs making up the field-value
  4643     // and consisting of either *TEXT or combinations
  4644     // of token, separators, and quoted-string>
  4645     // TEXT = <any OCTET except CTLs,
  4646     // but including LWS>
  4647     // LWS = [CRLF] 1*( SP | HT )
  4648     //
  4649     // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
  4650     // qdtext = <any TEXT except <">>
  4651     // quoted-pair = "\" CHAR
  4652     // CHAR = <any US-ASCII character (octets 0 - 127)>
  4654     // Any LWS that occurs between field-content MAY be replaced with a single
  4655     // SP before interpreting the field value or forwarding the message
  4656     // downstream (section 4.2); we replace 1*LWS with a single SP
  4657     var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " ");
  4659     // remove leading/trailing LWS (which has been converted to SP)
  4660     val = val.replace(/^ +/, "").replace(/ +$/, "");
  4662     // that should have taken care of all CTLs, so val should contain no CTLs
  4663     for (var i = 0, len = val.length; i < len; i++)
  4664       if (isCTL(val.charCodeAt(i)))
  4665         throw Cr.NS_ERROR_INVALID_ARG;
  4667     // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly
  4668     // normalize, however, so this can be construed as a tightening of the
  4669     // spec and not entirely as a bug
  4670     return val;
  4672 };
  4676 /**
  4677 * Converts the given string into a string which is safe for use in an HTML
  4678 * context.
  4680 * @param str : string
  4681 * the string to make HTML-safe
  4682 * @returns string
  4683 * an HTML-safe version of str
  4684 */
  4685 function htmlEscape(str)
  4687   // this is naive, but it'll work
  4688   var s = "";
  4689   for (var i = 0; i < str.length; i++)
  4690     s += "&#" + str.charCodeAt(i) + ";";
  4691   return s;
  4695 /**
  4696 * Constructs an object representing an HTTP version (see section 3.1).
  4698 * @param versionString
  4699 * a string of the form "#.#", where # is an non-negative decimal integer with
  4700 * or without leading zeros
  4701 * @throws
  4702 * if versionString does not specify a valid HTTP version number
  4703 */
  4704 function nsHttpVersion(versionString)
  4706   var matches = /^(\d+)\.(\d+)$/.exec(versionString);
  4707   if (!matches)
  4708     throw "Not a valid HTTP version!";
  4710   /** The major version number of this, as a number. */
  4711   this.major = parseInt(matches[1], 10);
  4713   /** The minor version number of this, as a number. */
  4714   this.minor = parseInt(matches[2], 10);
  4716   if (isNaN(this.major) || isNaN(this.minor) ||
  4717       this.major < 0 || this.minor < 0)
  4718     throw "Not a valid HTTP version!";
  4720 nsHttpVersion.prototype =
  4722   /**
  4723 * Returns the standard string representation of the HTTP version represented
  4724 * by this (e.g., "1.1").
  4725 */
  4726   toString: function ()
  4728     return this.major + "." + this.minor;
  4729   },
  4731   /**
  4732 * Returns true if this represents the same HTTP version as otherVersion,
  4733 * false otherwise.
  4735 * @param otherVersion : nsHttpVersion
  4736 * the version to compare against this
  4737 */
  4738   equals: function (otherVersion)
  4740     return this.major == otherVersion.major &&
  4741            this.minor == otherVersion.minor;
  4742   },
  4744   /** True if this >= otherVersion, false otherwise. */
  4745   atLeast: function(otherVersion)
  4747     return this.major > otherVersion.major ||
  4748            (this.major == otherVersion.major &&
  4749             this.minor >= otherVersion.minor);
  4751 };
  4753 nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0");
  4754 nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1");
  4757 /**
  4758 * An object which stores HTTP headers for a request or response.
  4760 * Note that since headers are case-insensitive, this object converts headers to
  4761 * lowercase before storing them. This allows the getHeader and hasHeader
  4762 * methods to work correctly for any case of a header, but it means that the
  4763 * values returned by .enumerator may not be equal case-sensitively to the
  4764 * values passed to setHeader when adding headers to this.
  4765 */
  4766 function nsHttpHeaders()
  4768   /**
  4769 * A hash of headers, with header field names as the keys and header field
  4770 * values as the values. Header field names are case-insensitive, but upon
  4771 * insertion here they are converted to lowercase. Header field values are
  4772 * normalized upon insertion to contain no leading or trailing whitespace.
  4774 * Note also that per RFC 2616, section 4.2, two headers with the same name in
  4775 * a message may be treated as one header with the same field name and a field
  4776 * value consisting of the separate field values joined together with a "," in
  4777 * their original order. This hash stores multiple headers with the same name
  4778 * in this manner.
  4779 */
  4780   this._headers = {};
  4782 nsHttpHeaders.prototype =
  4784   /**
  4785 * Sets the header represented by name and value in this.
  4787 * @param name : string
  4788 * the header name
  4789 * @param value : string
  4790 * the header value
  4791 * @throws NS_ERROR_INVALID_ARG
  4792 * if name or value is not a valid header component
  4793 */
  4794   setHeader: function(fieldName, fieldValue, merge)
  4796     var name = headerUtils.normalizeFieldName(fieldName);
  4797     var value = headerUtils.normalizeFieldValue(fieldValue);
  4799     // The following three headers are stored as arrays because their real-world
  4800     // syntax prevents joining individual headers into a single header using
  4801     // ",". See also <http://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77>
  4802     if (merge && name in this._headers)
  4804       if (name === "www-authenticate" ||
  4805           name === "proxy-authenticate" ||
  4806           name === "set-cookie")
  4808         this._headers[name].push(value);
  4810       else
  4812         this._headers[name][0] += "," + value;
  4813         NS_ASSERT(this._headers[name].length === 1,
  4814             "how'd a non-special header have multiple values?")
  4817     else
  4819       this._headers[name] = [value];
  4821   },
  4823   /**
  4824 * Returns the value for the header specified by this.
  4826 * @throws NS_ERROR_INVALID_ARG
  4827 * if fieldName does not constitute a valid header field name
  4828 * @throws NS_ERROR_NOT_AVAILABLE
  4829 * if the given header does not exist in this
  4830 * @returns string
  4831 * the field value for the given header, possibly with non-semantic changes
  4832 * (i.e., leading/trailing whitespace stripped, whitespace runs replaced
  4833 * with spaces, etc.) at the option of the implementation; multiple
  4834 * instances of the header will be combined with a comma, except for
  4835 * the three headers noted in the description of getHeaderValues
  4836 */
  4837   getHeader: function(fieldName)
  4839     return this.getHeaderValues(fieldName).join("\n");
  4840   },
  4842   /**
  4843 * Returns the value for the header specified by fieldName as an array.
  4845 * @throws NS_ERROR_INVALID_ARG
  4846 * if fieldName does not constitute a valid header field name
  4847 * @throws NS_ERROR_NOT_AVAILABLE
  4848 * if the given header does not exist in this
  4849 * @returns [string]
  4850 * an array of all the header values in this for the given
  4851 * header name. Header values will generally be collapsed
  4852 * into a single header by joining all header values together
  4853 * with commas, but certain headers (Proxy-Authenticate,
  4854 * WWW-Authenticate, and Set-Cookie) violate the HTTP spec
  4855 * and cannot be collapsed in this manner. For these headers
  4856 * only, the returned array may contain multiple elements if
  4857 * that header has been added more than once.
  4858 */
  4859   getHeaderValues: function(fieldName)
  4861     var name = headerUtils.normalizeFieldName(fieldName);
  4863     if (name in this._headers)
  4864       return this._headers[name];
  4865     else
  4866       throw Cr.NS_ERROR_NOT_AVAILABLE;
  4867   },
  4869   /**
  4870 * Returns true if a header with the given field name exists in this, false
  4871 * otherwise.
  4873 * @param fieldName : string
  4874 * the field name whose existence is to be determined in this
  4875 * @throws NS_ERROR_INVALID_ARG
  4876 * if fieldName does not constitute a valid header field name
  4877 * @returns boolean
  4878 * true if the header's present, false otherwise
  4879 */
  4880   hasHeader: function(fieldName)
  4882     var name = headerUtils.normalizeFieldName(fieldName);
  4883     return (name in this._headers);
  4884   },
  4886   /**
  4887 * Returns a new enumerator over the field names of the headers in this, as
  4888 * nsISupportsStrings. The names returned will be in lowercase, regardless of
  4889 * how they were input using setHeader (header names are case-insensitive per
  4890 * RFC 2616).
  4891 */
  4892   get enumerator()
  4894     var headers = [];
  4895     for (var i in this._headers)
  4897       var supports = new SupportsString();
  4898       supports.data = i;
  4899       headers.push(supports);
  4902     return new nsSimpleEnumerator(headers);
  4904 };
  4907 /**
  4908 * Constructs an nsISimpleEnumerator for the given array of items.
  4910 * @param items : Array
  4911 * the items, which must all implement nsISupports
  4912 */
  4913 function nsSimpleEnumerator(items)
  4915   this._items = items;
  4916   this._nextIndex = 0;
  4918 nsSimpleEnumerator.prototype =
  4920   hasMoreElements: function()
  4922     return this._nextIndex < this._items.length;
  4923   },
  4924   getNext: function()
  4926     if (!this.hasMoreElements())
  4927       throw Cr.NS_ERROR_NOT_AVAILABLE;
  4929     return this._items[this._nextIndex++];
  4930   },
  4931   QueryInterface: function(aIID)
  4933     if (Ci.nsISimpleEnumerator.equals(aIID) ||
  4934         Ci.nsISupports.equals(aIID))
  4935       return this;
  4937     throw Cr.NS_ERROR_NO_INTERFACE;
  4939 };
  4942 /**
  4943 * A representation of the data in an HTTP request.
  4945 * @param port : uint
  4946 * the port on which the server receiving this request runs
  4947 */
  4948 function Request(port)
  4950   /** Method of this request, e.g. GET or POST. */
  4951   this._method = "";
  4953   /** Path of the requested resource; empty paths are converted to '/'. */
  4954   this._path = "";
  4956   /** Query string, if any, associated with this request (not including '?'). */
  4957   this._queryString = "";
  4959   /** Scheme of requested resource, usually http, always lowercase. */
  4960   this._scheme = "http";
  4962   /** Hostname on which the requested resource resides. */
  4963   this._host = undefined;
  4965   /** Port number over which the request was received. */
  4966   this._port = port;
  4968   var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null);
  4970   /** Stream from which data in this request's body may be read. */
  4971   this._bodyInputStream = bodyPipe.inputStream;
  4973   /** Stream to which data in this request's body is written. */
  4974   this._bodyOutputStream = bodyPipe.outputStream;
  4976   /**
  4977 * The headers in this request.
  4978 */
  4979   this._headers = new nsHttpHeaders();
  4981   /**
  4982 * For the addition of ad-hoc properties and new functionality without having
  4983 * to change nsIHttpRequest every time; currently lazily created, as its only
  4984 * use is in directory listings.
  4985 */
  4986   this._bag = null;
  4988 Request.prototype =
  4990   // SERVER METADATA
  4992   //
  4993   // see nsIHttpRequest.scheme
  4994   //
  4995   get scheme()
  4997     return this._scheme;
  4998   },
  5000   //
  5001   // see nsIHttpRequest.host
  5002   //
  5003   get host()
  5005     return this._host;
  5006   },
  5008   //
  5009   // see nsIHttpRequest.port
  5010   //
  5011   get port()
  5013     return this._port;
  5014   },
  5016   // REQUEST LINE
  5018   //
  5019   // see nsIHttpRequest.method
  5020   //
  5021   get method()
  5023     return this._method;
  5024   },
  5026   //
  5027   // see nsIHttpRequest.httpVersion
  5028   //
  5029   get httpVersion()
  5031     return this._httpVersion.toString();
  5032   },
  5034   //
  5035   // see nsIHttpRequest.path
  5036   //
  5037   get path()
  5039     return this._path;
  5040   },
  5042   //
  5043   // see nsIHttpRequest.queryString
  5044   //
  5045   get queryString()
  5047     return this._queryString;
  5048   },
  5050   // HEADERS
  5052   //
  5053   // see nsIHttpRequest.getHeader
  5054   //
  5055   getHeader: function(name)
  5057     return this._headers.getHeader(name);
  5058   },
  5060   //
  5061   // see nsIHttpRequest.hasHeader
  5062   //
  5063   hasHeader: function(name)
  5065     return this._headers.hasHeader(name);
  5066   },
  5068   //
  5069   // see nsIHttpRequest.headers
  5070   //
  5071   get headers()
  5073     return this._headers.enumerator;
  5074   },
  5076   //
  5077   // see nsIPropertyBag.enumerator
  5078   //
  5079   get enumerator()
  5081     this._ensurePropertyBag();
  5082     return this._bag.enumerator;
  5083   },
  5085   //
  5086   // see nsIHttpRequest.headers
  5087   //
  5088   get bodyInputStream()
  5090     return this._bodyInputStream;
  5091   },
  5093   //
  5094   // see nsIPropertyBag.getProperty
  5095   //
  5096   getProperty: function(name)
  5098     this._ensurePropertyBag();
  5099     return this._bag.getProperty(name);
  5100   },
  5103   // NSISUPPORTS
  5105   //
  5106   // see nsISupports.QueryInterface
  5107   //
  5108   QueryInterface: function(iid)
  5110     if (iid.equals(Ci.nsIHttpRequest) || iid.equals(Ci.nsISupports))
  5111       return this;
  5113     throw Cr.NS_ERROR_NO_INTERFACE;
  5114   },
  5117   // PRIVATE IMPLEMENTATION
  5119   /** Ensures a property bag has been created for ad-hoc behaviors. */
  5120   _ensurePropertyBag: function()
  5122     if (!this._bag)
  5123       this._bag = new WritablePropertyBag();
  5125 };
  5128 // XPCOM trappings
  5129 if ("XPCOMUtils" in this && // Firefox 3.6 doesn't load XPCOMUtils in this scope for some reason...
  5130     "generateNSGetFactory" in XPCOMUtils) {
  5131   var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]);
  5136 /**
  5137 * Creates a new HTTP server listening for loopback traffic on the given port,
  5138 * starts it, and runs the server until the server processes a shutdown request,
  5139 * spinning an event loop so that events posted by the server's socket are
  5140 * processed.
  5142 * This method is primarily intended for use in running this script from within
  5143 * xpcshell and running a functional HTTP server without having to deal with
  5144 * non-essential details.
  5146 * Note that running multiple servers using variants of this method probably
  5147 * doesn't work, simply due to how the internal event loop is spun and stopped.
  5149 * @note
  5150 * This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code);
  5151 * you should use this server as a component in Mozilla 1.8.
  5152 * @param port
  5153 * the port on which the server will run, or -1 if there exists no preference
  5154 * for a specific port; note that attempting to use some values for this
  5155 * parameter (particularly those below 1024) may cause this method to throw or
  5156 * may result in the server being prematurely shut down
  5157 * @param basePath
  5158 * a local directory from which requests will be served (i.e., if this is
  5159 * "/home/jwalden/" then a request to /index.html will load
  5160 * /home/jwalden/index.html); if this is omitted, only the default URLs in
  5161 * this server implementation will be functional
  5162 */
  5163 function server(port, basePath)
  5165   if (basePath)
  5167     var lp = Cc["@mozilla.org/file/local;1"]
  5168                .createInstance(Ci.nsILocalFile);
  5169     lp.initWithPath(basePath);
  5172   // if you're running this, you probably want to see debugging info
  5173   DEBUG = true;
  5175   var srv = new nsHttpServer();
  5176   if (lp)
  5177     srv.registerDirectory("/", lp);
  5178   srv.registerContentType("sjs", SJS_TYPE);
  5179   srv.start(port);
  5181   var thread = gThreadManager.currentThread;
  5182   while (!srv.isStopped())
  5183     thread.processNextEvent(true);
  5185   // get rid of any pending requests
  5186   while (thread.hasPendingEvents())
  5187     thread.processNextEvent(true);
  5189   DEBUG = false;
  5192 function startServerAsync(port, basePath)
  5194   if (basePath)
  5196     var lp = Cc["@mozilla.org/file/local;1"]
  5197                .createInstance(Ci.nsILocalFile);
  5198     lp.initWithPath(basePath);
  5201   var srv = new nsHttpServer();
  5202   if (lp)
  5203     srv.registerDirectory("/", lp);
  5204   srv.registerContentType("sjs", "sjs");
  5205   srv.start(port);
  5206   return srv;
  5209 exports.nsHttpServer = nsHttpServer;
  5210 exports.ScriptableInputStream = ScriptableInputStream;
  5211 exports.server = server;
  5212 exports.startServerAsync = startServerAsync;

mercurial