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

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/addon-sdk/source/lib/sdk/test/httpd.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,5212 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 +* License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +/*
     1.9 +* An implementation of an HTTP server both as a loadable script and as an XPCOM
    1.10 +* component. See the accompanying README file for user documentation on
    1.11 +* httpd.js.
    1.12 +*/
    1.13 +
    1.14 +module.metadata = {
    1.15 +  "stability": "experimental"
    1.16 +};
    1.17 +
    1.18 +const { components, CC, Cc, Ci, Cr, Cu } = require("chrome");
    1.19 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.20 +
    1.21 +
    1.22 +const PR_UINT32_MAX = Math.pow(2, 32) - 1;
    1.23 +
    1.24 +/** True if debugging output is enabled, false otherwise. */
    1.25 +var DEBUG = false; // non-const *only* so tweakable in server tests
    1.26 +
    1.27 +/** True if debugging output should be timestamped. */
    1.28 +var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests
    1.29 +
    1.30 +var gGlobalObject = Cc["@mozilla.org/systemprincipal;1"].createInstance();
    1.31 +
    1.32 +/**
    1.33 +* Asserts that the given condition holds. If it doesn't, the given message is
    1.34 +* dumped, a stack trace is printed, and an exception is thrown to attempt to
    1.35 +* stop execution (which unfortunately must rely upon the exception not being
    1.36 +* accidentally swallowed by the code that uses it).
    1.37 +*/
    1.38 +function NS_ASSERT(cond, msg)
    1.39 +{
    1.40 +  if (DEBUG && !cond)
    1.41 +  {
    1.42 +    dumpn("###!!!");
    1.43 +    dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!"));
    1.44 +    dumpn("###!!! Stack follows:");
    1.45 +
    1.46 +    var stack = new Error().stack.split(/\n/);
    1.47 +    dumpn(stack.map(function(val) { return "###!!! " + val; }).join("\n"));
    1.48 +
    1.49 +    throw Cr.NS_ERROR_ABORT;
    1.50 +  }
    1.51 +}
    1.52 +
    1.53 +/** Constructs an HTTP error object. */
    1.54 +function HttpError(code, description)
    1.55 +{
    1.56 +  this.code = code;
    1.57 +  this.description = description;
    1.58 +}
    1.59 +HttpError.prototype =
    1.60 +{
    1.61 +  toString: function()
    1.62 +  {
    1.63 +    return this.code + " " + this.description;
    1.64 +  }
    1.65 +};
    1.66 +
    1.67 +/**
    1.68 +* Errors thrown to trigger specific HTTP server responses.
    1.69 +*/
    1.70 +const HTTP_400 = new HttpError(400, "Bad Request");
    1.71 +const HTTP_401 = new HttpError(401, "Unauthorized");
    1.72 +const HTTP_402 = new HttpError(402, "Payment Required");
    1.73 +const HTTP_403 = new HttpError(403, "Forbidden");
    1.74 +const HTTP_404 = new HttpError(404, "Not Found");
    1.75 +const HTTP_405 = new HttpError(405, "Method Not Allowed");
    1.76 +const HTTP_406 = new HttpError(406, "Not Acceptable");
    1.77 +const HTTP_407 = new HttpError(407, "Proxy Authentication Required");
    1.78 +const HTTP_408 = new HttpError(408, "Request Timeout");
    1.79 +const HTTP_409 = new HttpError(409, "Conflict");
    1.80 +const HTTP_410 = new HttpError(410, "Gone");
    1.81 +const HTTP_411 = new HttpError(411, "Length Required");
    1.82 +const HTTP_412 = new HttpError(412, "Precondition Failed");
    1.83 +const HTTP_413 = new HttpError(413, "Request Entity Too Large");
    1.84 +const HTTP_414 = new HttpError(414, "Request-URI Too Long");
    1.85 +const HTTP_415 = new HttpError(415, "Unsupported Media Type");
    1.86 +const HTTP_417 = new HttpError(417, "Expectation Failed");
    1.87 +
    1.88 +const HTTP_500 = new HttpError(500, "Internal Server Error");
    1.89 +const HTTP_501 = new HttpError(501, "Not Implemented");
    1.90 +const HTTP_502 = new HttpError(502, "Bad Gateway");
    1.91 +const HTTP_503 = new HttpError(503, "Service Unavailable");
    1.92 +const HTTP_504 = new HttpError(504, "Gateway Timeout");
    1.93 +const HTTP_505 = new HttpError(505, "HTTP Version Not Supported");
    1.94 +
    1.95 +/** Creates a hash with fields corresponding to the values in arr. */
    1.96 +function array2obj(arr)
    1.97 +{
    1.98 +  var obj = {};
    1.99 +  for (var i = 0; i < arr.length; i++)
   1.100 +    obj[arr[i]] = arr[i];
   1.101 +  return obj;
   1.102 +}
   1.103 +
   1.104 +/** Returns an array of the integers x through y, inclusive. */
   1.105 +function range(x, y)
   1.106 +{
   1.107 +  var arr = [];
   1.108 +  for (var i = x; i <= y; i++)
   1.109 +    arr.push(i);
   1.110 +  return arr;
   1.111 +}
   1.112 +
   1.113 +/** An object (hash) whose fields are the numbers of all HTTP error codes. */
   1.114 +const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505)));
   1.115 +
   1.116 +
   1.117 +/**
   1.118 +* The character used to distinguish hidden files from non-hidden files, a la
   1.119 +* the leading dot in Apache. Since that mechanism also hides files from
   1.120 +* easy display in LXR, ls output, etc. however, we choose instead to use a
   1.121 +* suffix character. If a requested file ends with it, we append another
   1.122 +* when getting the file on the server. If it doesn't, we just look up that
   1.123 +* file. Therefore, any file whose name ends with exactly one of the character
   1.124 +* is "hidden" and available for use by the server.
   1.125 +*/
   1.126 +const HIDDEN_CHAR = "^";
   1.127 +
   1.128 +/**
   1.129 +* The file name suffix indicating the file containing overridden headers for
   1.130 +* a requested file.
   1.131 +*/
   1.132 +const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR;
   1.133 +
   1.134 +/** Type used to denote SJS scripts for CGI-like functionality. */
   1.135 +const SJS_TYPE = "sjs";
   1.136 +
   1.137 +/** Base for relative timestamps produced by dumpn(). */
   1.138 +var firstStamp = 0;
   1.139 +
   1.140 +/** dump(str) with a trailing "\n" -- only outputs if DEBUG. */
   1.141 +function dumpn(str)
   1.142 +{
   1.143 +  if (DEBUG)
   1.144 +  {
   1.145 +    var prefix = "HTTPD-INFO | ";
   1.146 +    if (DEBUG_TIMESTAMP)
   1.147 +    {
   1.148 +      if (firstStamp === 0)
   1.149 +        firstStamp = Date.now();
   1.150 +
   1.151 +      var elapsed = Date.now() - firstStamp; // milliseconds
   1.152 +      var min = Math.floor(elapsed / 60000);
   1.153 +      var sec = (elapsed % 60000) / 1000;
   1.154 +
   1.155 +      if (sec < 10)
   1.156 +        prefix += min + ":0" + sec.toFixed(3) + " | ";
   1.157 +      else
   1.158 +        prefix += min + ":" + sec.toFixed(3) + " | ";
   1.159 +    }
   1.160 +
   1.161 +    dump(prefix + str + "\n");
   1.162 +  }
   1.163 +}
   1.164 +
   1.165 +/** Dumps the current JS stack if DEBUG. */
   1.166 +function dumpStack()
   1.167 +{
   1.168 +  // peel off the frames for dumpStack() and Error()
   1.169 +  var stack = new Error().stack.split(/\n/).slice(2);
   1.170 +  stack.forEach(dumpn);
   1.171 +}
   1.172 +
   1.173 +
   1.174 +/** The XPCOM thread manager. */
   1.175 +var gThreadManager = null;
   1.176 +
   1.177 +/** The XPCOM prefs service. */
   1.178 +var gRootPrefBranch = null;
   1.179 +function getRootPrefBranch()
   1.180 +{
   1.181 +  if (!gRootPrefBranch)
   1.182 +  {
   1.183 +    gRootPrefBranch = Cc["@mozilla.org/preferences-service;1"]
   1.184 +                        .getService(Ci.nsIPrefBranch);
   1.185 +  }
   1.186 +  return gRootPrefBranch;
   1.187 +}
   1.188 +
   1.189 +/**
   1.190 +* JavaScript constructors for commonly-used classes; precreating these is a
   1.191 +* speedup over doing the same from base principles. See the docs at
   1.192 +* http://developer.mozilla.org/en/docs/components.Constructor for details.
   1.193 +*/
   1.194 +const ServerSocket = CC("@mozilla.org/network/server-socket;1",
   1.195 +                        "nsIServerSocket",
   1.196 +                        "init");
   1.197 +const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
   1.198 +                                 "nsIScriptableInputStream",
   1.199 +                                 "init");
   1.200 +const Pipe = CC("@mozilla.org/pipe;1",
   1.201 +                "nsIPipe",
   1.202 +                "init");
   1.203 +const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
   1.204 +                           "nsIFileInputStream",
   1.205 +                           "init");
   1.206 +const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1",
   1.207 +                                "nsIConverterInputStream",
   1.208 +                                "init");
   1.209 +const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1",
   1.210 +                               "nsIWritablePropertyBag2");
   1.211 +const SupportsString = CC("@mozilla.org/supports-string;1",
   1.212 +                          "nsISupportsString");
   1.213 +
   1.214 +/* These two are non-const only so a test can overwrite them. */
   1.215 +var BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
   1.216 +                           "nsIBinaryInputStream",
   1.217 +                           "setInputStream");
   1.218 +var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
   1.219 +                            "nsIBinaryOutputStream",
   1.220 +                            "setOutputStream");
   1.221 +
   1.222 +/**
   1.223 +* Returns the RFC 822/1123 representation of a date.
   1.224 +*
   1.225 +* @param date : Number
   1.226 +* the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT
   1.227 +* @returns string
   1.228 +* the representation of the given date
   1.229 +*/
   1.230 +function toDateString(date)
   1.231 +{
   1.232 +  //
   1.233 +  // rfc1123-date = wkday "," SP date1 SP time SP "GMT"
   1.234 +  // date1 = 2DIGIT SP month SP 4DIGIT
   1.235 +  // ; day month year (e.g., 02 Jun 1982)
   1.236 +  // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
   1.237 +  // ; 00:00:00 - 23:59:59
   1.238 +  // wkday = "Mon" | "Tue" | "Wed"
   1.239 +  // | "Thu" | "Fri" | "Sat" | "Sun"
   1.240 +  // month = "Jan" | "Feb" | "Mar" | "Apr"
   1.241 +  // | "May" | "Jun" | "Jul" | "Aug"
   1.242 +  // | "Sep" | "Oct" | "Nov" | "Dec"
   1.243 +  //
   1.244 +
   1.245 +  const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
   1.246 +  const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
   1.247 +                        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
   1.248 +
   1.249 +  /**
   1.250 +* Processes a date and returns the encoded UTC time as a string according to
   1.251 +* the format specified in RFC 2616.
   1.252 +*
   1.253 +* @param date : Date
   1.254 +* the date to process
   1.255 +* @returns string
   1.256 +* a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
   1.257 +*/
   1.258 +  function toTime(date)
   1.259 +  {
   1.260 +    var hrs = date.getUTCHours();
   1.261 +    var rv = (hrs < 10) ? "0" + hrs : hrs;
   1.262 +    
   1.263 +    var mins = date.getUTCMinutes();
   1.264 +    rv += ":";
   1.265 +    rv += (mins < 10) ? "0" + mins : mins;
   1.266 +
   1.267 +    var secs = date.getUTCSeconds();
   1.268 +    rv += ":";
   1.269 +    rv += (secs < 10) ? "0" + secs : secs;
   1.270 +
   1.271 +    return rv;
   1.272 +  }
   1.273 +
   1.274 +  /**
   1.275 +* Processes a date and returns the encoded UTC date as a string according to
   1.276 +* the date1 format specified in RFC 2616.
   1.277 +*
   1.278 +* @param date : Date
   1.279 +* the date to process
   1.280 +* @returns string
   1.281 +* a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
   1.282 +*/
   1.283 +  function toDate1(date)
   1.284 +  {
   1.285 +    var day = date.getUTCDate();
   1.286 +    var month = date.getUTCMonth();
   1.287 +    var year = date.getUTCFullYear();
   1.288 +
   1.289 +    var rv = (day < 10) ? "0" + day : day;
   1.290 +    rv += " " + monthStrings[month];
   1.291 +    rv += " " + year;
   1.292 +
   1.293 +    return rv;
   1.294 +  }
   1.295 +
   1.296 +  date = new Date(date);
   1.297 +
   1.298 +  const fmtString = "%wkday%, %date1% %time% GMT";
   1.299 +  var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]);
   1.300 +  rv = rv.replace("%time%", toTime(date));
   1.301 +  return rv.replace("%date1%", toDate1(date));
   1.302 +}
   1.303 +
   1.304 +/**
   1.305 +* Prints out a human-readable representation of the object o and its fields,
   1.306 +* omitting those whose names begin with "_" if showMembers != true (to ignore
   1.307 +* "private" properties exposed via getters/setters).
   1.308 +*/
   1.309 +function printObj(o, showMembers)
   1.310 +{
   1.311 +  var s = "******************************\n";
   1.312 +  s += "o = {\n";
   1.313 +  for (var i in o)
   1.314 +  {
   1.315 +    if (typeof(i) != "string" ||
   1.316 +        (showMembers || (i.length > 0 && i[0] != "_")))
   1.317 +      s+= " " + i + ": " + o[i] + ",\n";
   1.318 +  }
   1.319 +  s += " };\n";
   1.320 +  s += "******************************";
   1.321 +  dumpn(s);
   1.322 +}
   1.323 +
   1.324 +/**
   1.325 +* Instantiates a new HTTP server.
   1.326 +*/
   1.327 +function nsHttpServer()
   1.328 +{
   1.329 +  if (!gThreadManager)
   1.330 +    gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService();
   1.331 +
   1.332 +  /** The port on which this server listens. */
   1.333 +  this._port = undefined;
   1.334 +
   1.335 +  /** The socket associated with this. */
   1.336 +  this._socket = null;
   1.337 +
   1.338 +  /** The handler used to process requests to this server. */
   1.339 +  this._handler = new ServerHandler(this);
   1.340 +
   1.341 +  /** Naming information for this server. */
   1.342 +  this._identity = new ServerIdentity();
   1.343 +
   1.344 +  /**
   1.345 +* Indicates when the server is to be shut down at the end of the request.
   1.346 +*/
   1.347 +  this._doQuit = false;
   1.348 +
   1.349 +  /**
   1.350 +* True if the socket in this is closed (and closure notifications have been
   1.351 +* sent and processed if the socket was ever opened), false otherwise.
   1.352 +*/
   1.353 +  this._socketClosed = true;
   1.354 +
   1.355 +  /**
   1.356 +* Used for tracking existing connections and ensuring that all connections
   1.357 +* are properly cleaned up before server shutdown; increases by 1 for every
   1.358 +* new incoming connection.
   1.359 +*/
   1.360 +  this._connectionGen = 0;
   1.361 +
   1.362 +  /**
   1.363 +* Hash of all open connections, indexed by connection number at time of
   1.364 +* creation.
   1.365 +*/
   1.366 +  this._connections = {};
   1.367 +}
   1.368 +nsHttpServer.prototype =
   1.369 +{
   1.370 +  classID: components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"),
   1.371 +
   1.372 +  // NSISERVERSOCKETLISTENER
   1.373 +
   1.374 +  /**
   1.375 +* Processes an incoming request coming in on the given socket and contained
   1.376 +* in the given transport.
   1.377 +*
   1.378 +* @param socket : nsIServerSocket
   1.379 +* the socket through which the request was served
   1.380 +* @param trans : nsISocketTransport
   1.381 +* the transport for the request/response
   1.382 +* @see nsIServerSocketListener.onSocketAccepted
   1.383 +*/
   1.384 +  onSocketAccepted: function(socket, trans)
   1.385 +  {
   1.386 +    dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")");
   1.387 +
   1.388 +    dumpn(">>> new connection on " + trans.host + ":" + trans.port);
   1.389 +
   1.390 +    const SEGMENT_SIZE = 8192;
   1.391 +    const SEGMENT_COUNT = 1024;
   1.392 +    try
   1.393 +    {
   1.394 +      var input = trans.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
   1.395 +                       .QueryInterface(Ci.nsIAsyncInputStream);
   1.396 +      var output = trans.openOutputStream(0, 0, 0);
   1.397 +    }
   1.398 +    catch (e)
   1.399 +    {
   1.400 +      dumpn("*** error opening transport streams: " + e);
   1.401 +      trans.close(Cr.NS_BINDING_ABORTED);
   1.402 +      return;
   1.403 +    }
   1.404 +
   1.405 +    var connectionNumber = ++this._connectionGen;
   1.406 +
   1.407 +    try
   1.408 +    {
   1.409 +      var conn = new Connection(input, output, this, socket.port, trans.port,
   1.410 +                                connectionNumber);
   1.411 +      var reader = new RequestReader(conn);
   1.412 +
   1.413 +      // XXX add request timeout functionality here!
   1.414 +
   1.415 +      // Note: must use main thread here, or we might get a GC that will cause
   1.416 +      // threadsafety assertions. We really need to fix XPConnect so that
   1.417 +      // you can actually do things in multi-threaded JS. :-(
   1.418 +      input.asyncWait(reader, 0, 0, gThreadManager.mainThread);
   1.419 +    }
   1.420 +    catch (e)
   1.421 +    {
   1.422 +      // Assume this connection can't be salvaged and bail on it completely;
   1.423 +      // don't attempt to close it so that we can assert that any connection
   1.424 +      // being closed is in this._connections.
   1.425 +      dumpn("*** error in initial request-processing stages: " + e);
   1.426 +      trans.close(Cr.NS_BINDING_ABORTED);
   1.427 +      return;
   1.428 +    }
   1.429 +
   1.430 +    this._connections[connectionNumber] = conn;
   1.431 +    dumpn("*** starting connection " + connectionNumber);
   1.432 +  },
   1.433 +
   1.434 +  /**
   1.435 +* Called when the socket associated with this is closed.
   1.436 +*
   1.437 +* @param socket : nsIServerSocket
   1.438 +* the socket being closed
   1.439 +* @param status : nsresult
   1.440 +* the reason the socket stopped listening (NS_BINDING_ABORTED if the server
   1.441 +* was stopped using nsIHttpServer.stop)
   1.442 +* @see nsIServerSocketListener.onStopListening
   1.443 +*/
   1.444 +  onStopListening: function(socket, status)
   1.445 +  {
   1.446 +    dumpn(">>> shutting down server on port " + socket.port);
   1.447 +    this._socketClosed = true;
   1.448 +    if (!this._hasOpenConnections())
   1.449 +    {
   1.450 +      dumpn("*** no open connections, notifying async from onStopListening");
   1.451 +
   1.452 +      // Notify asynchronously so that any pending teardown in stop() has a
   1.453 +      // chance to run first.
   1.454 +      var self = this;
   1.455 +      var stopEvent =
   1.456 +        {
   1.457 +          run: function()
   1.458 +          {
   1.459 +            dumpn("*** _notifyStopped async callback");
   1.460 +            self._notifyStopped();
   1.461 +          }
   1.462 +        };
   1.463 +      gThreadManager.currentThread
   1.464 +                    .dispatch(stopEvent, Ci.nsIThread.DISPATCH_NORMAL);
   1.465 +    }
   1.466 +  },
   1.467 +
   1.468 +  // NSIHTTPSERVER
   1.469 +
   1.470 +  //
   1.471 +  // see nsIHttpServer.start
   1.472 +  //
   1.473 +  start: function(port)
   1.474 +  {
   1.475 +    this._start(port, "localhost")
   1.476 +  },
   1.477 +
   1.478 +  _start: function(port, host)
   1.479 +  {
   1.480 +    if (this._socket)
   1.481 +      throw Cr.NS_ERROR_ALREADY_INITIALIZED;
   1.482 +
   1.483 +    this._port = port;
   1.484 +    this._doQuit = this._socketClosed = false;
   1.485 +
   1.486 +    this._host = host;
   1.487 +
   1.488 +    // The listen queue needs to be long enough to handle
   1.489 +    // network.http.max-persistent-connections-per-server concurrent connections,
   1.490 +    // plus a safety margin in case some other process is talking to
   1.491 +    // the server as well.
   1.492 +    var prefs = getRootPrefBranch();
   1.493 +    var maxConnections;
   1.494 +    try {
   1.495 +      // Bug 776860: The original pref was removed in favor of this new one:
   1.496 +      maxConnections = prefs.getIntPref("network.http.max-persistent-connections-per-server") + 5;
   1.497 +    }
   1.498 +    catch(e) {
   1.499 +      maxConnections = prefs.getIntPref("network.http.max-connections-per-server") + 5;
   1.500 +    }
   1.501 +
   1.502 +    try
   1.503 +    {
   1.504 +      var loopback = true;
   1.505 +      if (this._host != "127.0.0.1" && this._host != "localhost") {
   1.506 +        var loopback = false;
   1.507 +      }
   1.508 +
   1.509 +      var socket = new ServerSocket(this._port,
   1.510 +                                    loopback, // true = localhost, false = everybody
   1.511 +                                    maxConnections);
   1.512 +      dumpn(">>> listening on port " + socket.port + ", " + maxConnections +
   1.513 +            " pending connections");
   1.514 +      socket.asyncListen(this);
   1.515 +      this._identity._initialize(socket.port, host, true);
   1.516 +      this._socket = socket;
   1.517 +    }
   1.518 +    catch (e)
   1.519 +    {
   1.520 +      dumpn("!!! could not start server on port " + port + ": " + e);
   1.521 +      throw Cr.NS_ERROR_NOT_AVAILABLE;
   1.522 +    }
   1.523 +  },
   1.524 +
   1.525 +  //
   1.526 +  // see nsIHttpServer.stop
   1.527 +  //
   1.528 +  stop: function(callback)
   1.529 +  {
   1.530 +    if (!callback)
   1.531 +      throw Cr.NS_ERROR_NULL_POINTER;
   1.532 +    if (!this._socket)
   1.533 +      throw Cr.NS_ERROR_UNEXPECTED;
   1.534 +
   1.535 +    this._stopCallback = typeof callback === "function"
   1.536 +                       ? callback
   1.537 +                       : function() { callback.onStopped(); };
   1.538 +
   1.539 +    dumpn(">>> stopping listening on port " + this._socket.port);
   1.540 +    this._socket.close();
   1.541 +    this._socket = null;
   1.542 +
   1.543 +    // We can't have this identity any more, and the port on which we're running
   1.544 +    // this server now could be meaningless the next time around.
   1.545 +    this._identity._teardown();
   1.546 +
   1.547 +    this._doQuit = false;
   1.548 +
   1.549 +    // socket-close notification and pending request completion happen async
   1.550 +  },
   1.551 +
   1.552 +  //
   1.553 +  // see nsIHttpServer.registerFile
   1.554 +  //
   1.555 +  registerFile: function(path, file)
   1.556 +  {
   1.557 +    if (file && (!file.exists() || file.isDirectory()))
   1.558 +      throw Cr.NS_ERROR_INVALID_ARG;
   1.559 +
   1.560 +    this._handler.registerFile(path, file);
   1.561 +  },
   1.562 +
   1.563 +  //
   1.564 +  // see nsIHttpServer.registerDirectory
   1.565 +  //
   1.566 +  registerDirectory: function(path, directory)
   1.567 +  {
   1.568 +    // XXX true path validation!
   1.569 +    if (path.charAt(0) != "/" ||
   1.570 +        path.charAt(path.length - 1) != "/" ||
   1.571 +        (directory &&
   1.572 +         (!directory.exists() || !directory.isDirectory())))
   1.573 +      throw Cr.NS_ERROR_INVALID_ARG;
   1.574 +
   1.575 +    // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping
   1.576 +    // exists!
   1.577 +
   1.578 +    this._handler.registerDirectory(path, directory);
   1.579 +  },
   1.580 +
   1.581 +  //
   1.582 +  // see nsIHttpServer.registerPathHandler
   1.583 +  //
   1.584 +  registerPathHandler: function(path, handler)
   1.585 +  {
   1.586 +    this._handler.registerPathHandler(path, handler);
   1.587 +  },
   1.588 +
   1.589 +  //
   1.590 +  // see nsIHttpServer.registerPrefixHandler
   1.591 +  //
   1.592 +  registerPrefixHandler: function(prefix, handler)
   1.593 +  {
   1.594 +    this._handler.registerPrefixHandler(prefix, handler);
   1.595 +  },
   1.596 +
   1.597 +  //
   1.598 +  // see nsIHttpServer.registerErrorHandler
   1.599 +  //
   1.600 +  registerErrorHandler: function(code, handler)
   1.601 +  {
   1.602 +    this._handler.registerErrorHandler(code, handler);
   1.603 +  },
   1.604 +
   1.605 +  //
   1.606 +  // see nsIHttpServer.setIndexHandler
   1.607 +  //
   1.608 +  setIndexHandler: function(handler)
   1.609 +  {
   1.610 +    this._handler.setIndexHandler(handler);
   1.611 +  },
   1.612 +
   1.613 +  //
   1.614 +  // see nsIHttpServer.registerContentType
   1.615 +  //
   1.616 +  registerContentType: function(ext, type)
   1.617 +  {
   1.618 +    this._handler.registerContentType(ext, type);
   1.619 +  },
   1.620 +
   1.621 +  //
   1.622 +  // see nsIHttpServer.serverIdentity
   1.623 +  //
   1.624 +  get identity()
   1.625 +  {
   1.626 +    return this._identity;
   1.627 +  },
   1.628 +
   1.629 +  //
   1.630 +  // see nsIHttpServer.getState
   1.631 +  //
   1.632 +  getState: function(path, k)
   1.633 +  {
   1.634 +    return this._handler._getState(path, k);
   1.635 +  },
   1.636 +
   1.637 +  //
   1.638 +  // see nsIHttpServer.setState
   1.639 +  //
   1.640 +  setState: function(path, k, v)
   1.641 +  {
   1.642 +    return this._handler._setState(path, k, v);
   1.643 +  },
   1.644 +
   1.645 +  //
   1.646 +  // see nsIHttpServer.getSharedState
   1.647 +  //
   1.648 +  getSharedState: function(k)
   1.649 +  {
   1.650 +    return this._handler._getSharedState(k);
   1.651 +  },
   1.652 +
   1.653 +  //
   1.654 +  // see nsIHttpServer.setSharedState
   1.655 +  //
   1.656 +  setSharedState: function(k, v)
   1.657 +  {
   1.658 +    return this._handler._setSharedState(k, v);
   1.659 +  },
   1.660 +
   1.661 +  //
   1.662 +  // see nsIHttpServer.getObjectState
   1.663 +  //
   1.664 +  getObjectState: function(k)
   1.665 +  {
   1.666 +    return this._handler._getObjectState(k);
   1.667 +  },
   1.668 +
   1.669 +  //
   1.670 +  // see nsIHttpServer.setObjectState
   1.671 +  //
   1.672 +  setObjectState: function(k, v)
   1.673 +  {
   1.674 +    return this._handler._setObjectState(k, v);
   1.675 +  },
   1.676 +
   1.677 +
   1.678 +  // NSISUPPORTS
   1.679 +
   1.680 +  //
   1.681 +  // see nsISupports.QueryInterface
   1.682 +  //
   1.683 +  QueryInterface: function(iid)
   1.684 +  {
   1.685 +    if (iid.equals(Ci.nsIServerSocketListener) || iid.equals(Ci.nsISupports))
   1.686 +      return this;
   1.687 +
   1.688 +    throw Cr.NS_ERROR_NO_INTERFACE;
   1.689 +  },
   1.690 +
   1.691 +
   1.692 +  // NON-XPCOM PUBLIC API
   1.693 +
   1.694 +  /**
   1.695 +* Returns true iff this server is not running (and is not in the process of
   1.696 +* serving any requests still to be processed when the server was last
   1.697 +* stopped after being run).
   1.698 +*/
   1.699 +  isStopped: function()
   1.700 +  {
   1.701 +    return this._socketClosed && !this._hasOpenConnections();
   1.702 +  },
   1.703 +
   1.704 +  // PRIVATE IMPLEMENTATION
   1.705 +
   1.706 +  /** True if this server has any open connections to it, false otherwise. */
   1.707 +  _hasOpenConnections: function()
   1.708 +  {
   1.709 +    //
   1.710 +    // If we have any open connections, they're tracked as numeric properties on
   1.711 +    // |this._connections|. The non-standard __count__ property could be used
   1.712 +    // to check whether there are any properties, but standard-wise, even
   1.713 +    // looking forward to ES5, there's no less ugly yet still O(1) way to do
   1.714 +    // this.
   1.715 +    //
   1.716 +    for (var n in this._connections)
   1.717 +      return true;
   1.718 +    return false;
   1.719 +  },
   1.720 +
   1.721 +  /** Calls the server-stopped callback provided when stop() was called. */
   1.722 +  _notifyStopped: function()
   1.723 +  {
   1.724 +    NS_ASSERT(this._stopCallback !== null, "double-notifying?");
   1.725 +    NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now");
   1.726 +
   1.727 +    //
   1.728 +    // NB: We have to grab this now, null out the member, *then* call the
   1.729 +    // callback here, or otherwise the callback could (indirectly) futz with
   1.730 +    // this._stopCallback by starting and immediately stopping this, at
   1.731 +    // which point we'd be nulling out a field we no longer have a right to
   1.732 +    // modify.
   1.733 +    //
   1.734 +    var callback = this._stopCallback;
   1.735 +    this._stopCallback = null;
   1.736 +    try
   1.737 +    {
   1.738 +      callback();
   1.739 +    }
   1.740 +    catch (e)
   1.741 +    {
   1.742 +      // not throwing because this is specified as being usually (but not
   1.743 +      // always) asynchronous
   1.744 +      dump("!!! error running onStopped callback: " + e + "\n");
   1.745 +    }
   1.746 +  },
   1.747 +
   1.748 +  /**
   1.749 +* Notifies this server that the given connection has been closed.
   1.750 +*
   1.751 +* @param connection : Connection
   1.752 +* the connection that was closed
   1.753 +*/
   1.754 +  _connectionClosed: function(connection)
   1.755 +  {
   1.756 +    NS_ASSERT(connection.number in this._connections,
   1.757 +              "closing a connection " + this + " that we never added to the " +
   1.758 +              "set of open connections?");
   1.759 +    NS_ASSERT(this._connections[connection.number] === connection,
   1.760 +              "connection number mismatch? " +
   1.761 +              this._connections[connection.number]);
   1.762 +    delete this._connections[connection.number];
   1.763 +
   1.764 +    // Fire a pending server-stopped notification if it's our responsibility.
   1.765 +    if (!this._hasOpenConnections() && this._socketClosed)
   1.766 +      this._notifyStopped();
   1.767 +  },
   1.768 +
   1.769 +  /**
   1.770 +* Requests that the server be shut down when possible.
   1.771 +*/
   1.772 +  _requestQuit: function()
   1.773 +  {
   1.774 +    dumpn(">>> requesting a quit");
   1.775 +    dumpStack();
   1.776 +    this._doQuit = true;
   1.777 +  }
   1.778 +};
   1.779 +
   1.780 +
   1.781 +//
   1.782 +// RFC 2396 section 3.2.2:
   1.783 +//
   1.784 +// host = hostname | IPv4address
   1.785 +// hostname = *( domainlabel "." ) toplabel [ "." ]
   1.786 +// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
   1.787 +// toplabel = alpha | alpha *( alphanum | "-" ) alphanum
   1.788 +// IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit
   1.789 +//
   1.790 +
   1.791 +const HOST_REGEX =
   1.792 +  new RegExp("^(?:" +
   1.793 +               // *( domainlabel "." )
   1.794 +               "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" +
   1.795 +               // toplabel
   1.796 +               "[a-z](?:[a-z0-9-]*[a-z0-9])?" +
   1.797 +             "|" +
   1.798 +               // IPv4 address
   1.799 +               "\\d+\\.\\d+\\.\\d+\\.\\d+" +
   1.800 +             ")$",
   1.801 +             "i");
   1.802 +
   1.803 +
   1.804 +/**
   1.805 +* Represents the identity of a server. An identity consists of a set of
   1.806 +* (scheme, host, port) tuples denoted as locations (allowing a single server to
   1.807 +* serve multiple sites or to be used behind both HTTP and HTTPS proxies for any
   1.808 +* host/port). Any incoming request must be to one of these locations, or it
   1.809 +* will be rejected with an HTTP 400 error. One location, denoted as the
   1.810 +* primary location, is the location assigned in contexts where a location
   1.811 +* cannot otherwise be endogenously derived, such as for HTTP/1.0 requests.
   1.812 +*
   1.813 +* A single identity may contain at most one location per unique host/port pair;
   1.814 +* other than that, no restrictions are placed upon what locations may
   1.815 +* constitute an identity.
   1.816 +*/
   1.817 +function ServerIdentity()
   1.818 +{
   1.819 +  /** The scheme of the primary location. */
   1.820 +  this._primaryScheme = "http";
   1.821 +
   1.822 +  /** The hostname of the primary location. */
   1.823 +  this._primaryHost = "127.0.0.1"
   1.824 +
   1.825 +  /** The port number of the primary location. */
   1.826 +  this._primaryPort = -1;
   1.827 +
   1.828 +  /**
   1.829 +* The current port number for the corresponding server, stored so that a new
   1.830 +* primary location can always be set if the current one is removed.
   1.831 +*/
   1.832 +  this._defaultPort = -1;
   1.833 +
   1.834 +  /**
   1.835 +* Maps hosts to maps of ports to schemes, e.g. the following would represent
   1.836 +* https://example.com:789/ and http://example.org/:
   1.837 +*
   1.838 +* {
   1.839 +* "xexample.com": { 789: "https" },
   1.840 +* "xexample.org": { 80: "http" }
   1.841 +* }
   1.842 +*
   1.843 +* Note the "x" prefix on hostnames, which prevents collisions with special
   1.844 +* JS names like "prototype".
   1.845 +*/
   1.846 +  this._locations = { "xlocalhost": {} };
   1.847 +}
   1.848 +ServerIdentity.prototype =
   1.849 +{
   1.850 +  // NSIHTTPSERVERIDENTITY
   1.851 +
   1.852 +  //
   1.853 +  // see nsIHttpServerIdentity.primaryScheme
   1.854 +  //
   1.855 +  get primaryScheme()
   1.856 +  {
   1.857 +    if (this._primaryPort === -1)
   1.858 +      throw Cr.NS_ERROR_NOT_INITIALIZED;
   1.859 +    return this._primaryScheme;
   1.860 +  },
   1.861 +
   1.862 +  //
   1.863 +  // see nsIHttpServerIdentity.primaryHost
   1.864 +  //
   1.865 +  get primaryHost()
   1.866 +  {
   1.867 +    if (this._primaryPort === -1)
   1.868 +      throw Cr.NS_ERROR_NOT_INITIALIZED;
   1.869 +    return this._primaryHost;
   1.870 +  },
   1.871 +
   1.872 +  //
   1.873 +  // see nsIHttpServerIdentity.primaryPort
   1.874 +  //
   1.875 +  get primaryPort()
   1.876 +  {
   1.877 +    if (this._primaryPort === -1)
   1.878 +      throw Cr.NS_ERROR_NOT_INITIALIZED;
   1.879 +    return this._primaryPort;
   1.880 +  },
   1.881 +
   1.882 +  //
   1.883 +  // see nsIHttpServerIdentity.add
   1.884 +  //
   1.885 +  add: function(scheme, host, port)
   1.886 +  {
   1.887 +    this._validate(scheme, host, port);
   1.888 +
   1.889 +    var entry = this._locations["x" + host];
   1.890 +    if (!entry)
   1.891 +      this._locations["x" + host] = entry = {};
   1.892 +
   1.893 +    entry[port] = scheme;
   1.894 +  },
   1.895 +
   1.896 +  //
   1.897 +  // see nsIHttpServerIdentity.remove
   1.898 +  //
   1.899 +  remove: function(scheme, host, port)
   1.900 +  {
   1.901 +    this._validate(scheme, host, port);
   1.902 +
   1.903 +    var entry = this._locations["x" + host];
   1.904 +    if (!entry)
   1.905 +      return false;
   1.906 +
   1.907 +    var present = port in entry;
   1.908 +    delete entry[port];
   1.909 +
   1.910 +    if (this._primaryScheme == scheme &&
   1.911 +        this._primaryHost == host &&
   1.912 +        this._primaryPort == port &&
   1.913 +        this._defaultPort !== -1)
   1.914 +    {
   1.915 +      // Always keep at least one identity in existence at any time, unless
   1.916 +      // we're in the process of shutting down (the last condition above).
   1.917 +      this._primaryPort = -1;
   1.918 +      this._initialize(this._defaultPort, host, false);
   1.919 +    }
   1.920 +
   1.921 +    return present;
   1.922 +  },
   1.923 +
   1.924 +  //
   1.925 +  // see nsIHttpServerIdentity.has
   1.926 +  //
   1.927 +  has: function(scheme, host, port)
   1.928 +  {
   1.929 +    this._validate(scheme, host, port);
   1.930 +
   1.931 +    return "x" + host in this._locations &&
   1.932 +           scheme === this._locations["x" + host][port];
   1.933 +  },
   1.934 +
   1.935 +  //
   1.936 +  // see nsIHttpServerIdentity.has
   1.937 +  //
   1.938 +  getScheme: function(host, port)
   1.939 +  {
   1.940 +    this._validate("http", host, port);
   1.941 +
   1.942 +    var entry = this._locations["x" + host];
   1.943 +    if (!entry)
   1.944 +      return "";
   1.945 +
   1.946 +    return entry[port] || "";
   1.947 +  },
   1.948 +
   1.949 +  //
   1.950 +  // see nsIHttpServerIdentity.setPrimary
   1.951 +  //
   1.952 +  setPrimary: function(scheme, host, port)
   1.953 +  {
   1.954 +    this._validate(scheme, host, port);
   1.955 +
   1.956 +    this.add(scheme, host, port);
   1.957 +
   1.958 +    this._primaryScheme = scheme;
   1.959 +    this._primaryHost = host;
   1.960 +    this._primaryPort = port;
   1.961 +  },
   1.962 +
   1.963 +
   1.964 +  // NSISUPPORTS
   1.965 +
   1.966 +  //
   1.967 +  // see nsISupports.QueryInterface
   1.968 +  //
   1.969 +  QueryInterface: function(iid)
   1.970 +  {
   1.971 +    if (iid.equals(Ci.nsIHttpServerIdentity) || iid.equals(Ci.nsISupports))
   1.972 +      return this;
   1.973 +
   1.974 +    throw Cr.NS_ERROR_NO_INTERFACE;
   1.975 +  },
   1.976 +
   1.977 +
   1.978 +  // PRIVATE IMPLEMENTATION
   1.979 +
   1.980 +  /**
   1.981 +* Initializes the primary name for the corresponding server, based on the
   1.982 +* provided port number.
   1.983 +*/
   1.984 +  _initialize: function(port, host, addSecondaryDefault)
   1.985 +  {
   1.986 +    this._host = host;
   1.987 +    if (this._primaryPort !== -1)
   1.988 +      this.add("http", host, port);
   1.989 +    else
   1.990 +      this.setPrimary("http", "localhost", port);
   1.991 +    this._defaultPort = port;
   1.992 +
   1.993 +    // Only add this if we're being called at server startup
   1.994 +    if (addSecondaryDefault && host != "127.0.0.1")
   1.995 +      this.add("http", "127.0.0.1", port);
   1.996 +  },
   1.997 +
   1.998 +  /**
   1.999 +* Called at server shutdown time, unsets the primary location only if it was
  1.1000 +* the default-assigned location and removes the default location from the
  1.1001 +* set of locations used.
  1.1002 +*/
  1.1003 +  _teardown: function()
  1.1004 +  {
  1.1005 +    if (this._host != "127.0.0.1") {
  1.1006 +      // Not the default primary location, nothing special to do here
  1.1007 +      this.remove("http", "127.0.0.1", this._defaultPort);
  1.1008 +    }
  1.1009 +    
  1.1010 +    // This is a *very* tricky bit of reasoning here; make absolutely sure the
  1.1011 +    // tests for this code pass before you commit changes to it.
  1.1012 +    if (this._primaryScheme == "http" &&
  1.1013 +        this._primaryHost == this._host &&
  1.1014 +        this._primaryPort == this._defaultPort)
  1.1015 +    {
  1.1016 +      // Make sure we don't trigger the readding logic in .remove(), then remove
  1.1017 +      // the default location.
  1.1018 +      var port = this._defaultPort;
  1.1019 +      this._defaultPort = -1;
  1.1020 +      this.remove("http", this._host, port);
  1.1021 +
  1.1022 +      // Ensure a server start triggers the setPrimary() path in ._initialize()
  1.1023 +      this._primaryPort = -1;
  1.1024 +    }
  1.1025 +    else
  1.1026 +    {
  1.1027 +      // No reason not to remove directly as it's not our primary location
  1.1028 +      this.remove("http", this._host, this._defaultPort);
  1.1029 +    }
  1.1030 +  },
  1.1031 +
  1.1032 +  /**
  1.1033 +* Ensures scheme, host, and port are all valid with respect to RFC 2396.
  1.1034 +*
  1.1035 +* @throws NS_ERROR_ILLEGAL_VALUE
  1.1036 +* if any argument doesn't match the corresponding production
  1.1037 +*/
  1.1038 +  _validate: function(scheme, host, port)
  1.1039 +  {
  1.1040 +    if (scheme !== "http" && scheme !== "https")
  1.1041 +    {
  1.1042 +      dumpn("*** server only supports http/https schemes: '" + scheme + "'");
  1.1043 +      dumpStack();
  1.1044 +      throw Cr.NS_ERROR_ILLEGAL_VALUE;
  1.1045 +    }
  1.1046 +    if (!HOST_REGEX.test(host))
  1.1047 +    {
  1.1048 +      dumpn("*** unexpected host: '" + host + "'");
  1.1049 +      throw Cr.NS_ERROR_ILLEGAL_VALUE;
  1.1050 +    }
  1.1051 +    if (port < 0 || port > 65535)
  1.1052 +    {
  1.1053 +      dumpn("*** unexpected port: '" + port + "'");
  1.1054 +      throw Cr.NS_ERROR_ILLEGAL_VALUE;
  1.1055 +    }
  1.1056 +  }
  1.1057 +};
  1.1058 +
  1.1059 +
  1.1060 +/**
  1.1061 +* Represents a connection to the server (and possibly in the future the thread
  1.1062 +* on which the connection is processed).
  1.1063 +*
  1.1064 +* @param input : nsIInputStream
  1.1065 +* stream from which incoming data on the connection is read
  1.1066 +* @param output : nsIOutputStream
  1.1067 +* stream to write data out the connection
  1.1068 +* @param server : nsHttpServer
  1.1069 +* the server handling the connection
  1.1070 +* @param port : int
  1.1071 +* the port on which the server is running
  1.1072 +* @param outgoingPort : int
  1.1073 +* the outgoing port used by this connection
  1.1074 +* @param number : uint
  1.1075 +* a serial number used to uniquely identify this connection
  1.1076 +*/
  1.1077 +function Connection(input, output, server, port, outgoingPort, number)
  1.1078 +{
  1.1079 +  dumpn("*** opening new connection " + number + " on port " + outgoingPort);
  1.1080 +
  1.1081 +  /** Stream of incoming data. */
  1.1082 +  this.input = input;
  1.1083 +
  1.1084 +  /** Stream for outgoing data. */
  1.1085 +  this.output = output;
  1.1086 +
  1.1087 +  /** The server associated with this request. */
  1.1088 +  this.server = server;
  1.1089 +
  1.1090 +  /** The port on which the server is running. */
  1.1091 +  this.port = port;
  1.1092 +
  1.1093 +  /** The outgoing poort used by this connection. */
  1.1094 +  this._outgoingPort = outgoingPort;
  1.1095 +
  1.1096 +  /** The serial number of this connection. */
  1.1097 +  this.number = number;
  1.1098 +
  1.1099 +  /**
  1.1100 +* The request for which a response is being generated, null if the
  1.1101 +* incoming request has not been fully received or if it had errors.
  1.1102 +*/
  1.1103 +  this.request = null;
  1.1104 +
  1.1105 +  /** State variables for debugging. */
  1.1106 +  this._closed = this._processed = false;
  1.1107 +}
  1.1108 +Connection.prototype =
  1.1109 +{
  1.1110 +  /** Closes this connection's input/output streams. */
  1.1111 +  close: function()
  1.1112 +  {
  1.1113 +    dumpn("*** closing connection " + this.number +
  1.1114 +          " on port " + this._outgoingPort);
  1.1115 +
  1.1116 +    this.input.close();
  1.1117 +    this.output.close();
  1.1118 +    this._closed = true;
  1.1119 +
  1.1120 +    var server = this.server;
  1.1121 +    server._connectionClosed(this);
  1.1122 +
  1.1123 +    // If an error triggered a server shutdown, act on it now
  1.1124 +    if (server._doQuit)
  1.1125 +      server.stop(function() { /* not like we can do anything better */ });
  1.1126 +  },
  1.1127 +
  1.1128 +  /**
  1.1129 +* Initiates processing of this connection, using the data in the given
  1.1130 +* request.
  1.1131 +*
  1.1132 +* @param request : Request
  1.1133 +* the request which should be processed
  1.1134 +*/
  1.1135 +  process: function(request)
  1.1136 +  {
  1.1137 +    NS_ASSERT(!this._closed && !this._processed);
  1.1138 +
  1.1139 +    this._processed = true;
  1.1140 +
  1.1141 +    this.request = request;
  1.1142 +    this.server._handler.handleResponse(this);
  1.1143 +  },
  1.1144 +
  1.1145 +  /**
  1.1146 +* Initiates processing of this connection, generating a response with the
  1.1147 +* given HTTP error code.
  1.1148 +*
  1.1149 +* @param code : uint
  1.1150 +* an HTTP code, so in the range [0, 1000)
  1.1151 +* @param request : Request
  1.1152 +* incomplete data about the incoming request (since there were errors
  1.1153 +* during its processing
  1.1154 +*/
  1.1155 +  processError: function(code, request)
  1.1156 +  {
  1.1157 +    NS_ASSERT(!this._closed && !this._processed);
  1.1158 +
  1.1159 +    this._processed = true;
  1.1160 +    this.request = request;
  1.1161 +    this.server._handler.handleError(code, this);
  1.1162 +  },
  1.1163 +
  1.1164 +  /** Converts this to a string for debugging purposes. */
  1.1165 +  toString: function()
  1.1166 +  {
  1.1167 +    return "<Connection(" + this.number +
  1.1168 +           (this.request ? ", " + this.request.path : "") +"): " +
  1.1169 +           (this._closed ? "closed" : "open") + ">";
  1.1170 +  }
  1.1171 +};
  1.1172 +
  1.1173 +
  1.1174 +
  1.1175 +/** Returns an array of count bytes from the given input stream. */
  1.1176 +function readBytes(inputStream, count)
  1.1177 +{
  1.1178 +  return new BinaryInputStream(inputStream).readByteArray(count);
  1.1179 +}
  1.1180 +
  1.1181 +
  1.1182 +
  1.1183 +/** Request reader processing states; see RequestReader for details. */
  1.1184 +const READER_IN_REQUEST_LINE = 0;
  1.1185 +const READER_IN_HEADERS = 1;
  1.1186 +const READER_IN_BODY = 2;
  1.1187 +const READER_FINISHED = 3;
  1.1188 +
  1.1189 +
  1.1190 +/**
  1.1191 +* Reads incoming request data asynchronously, does any necessary preprocessing,
  1.1192 +* and forwards it to the request handler. Processing occurs in three states:
  1.1193 +*
  1.1194 +* READER_IN_REQUEST_LINE Reading the request's status line
  1.1195 +* READER_IN_HEADERS Reading headers in the request
  1.1196 +* READER_IN_BODY Reading the body of the request
  1.1197 +* READER_FINISHED Entire request has been read and processed
  1.1198 +*
  1.1199 +* During the first two stages, initial metadata about the request is gathered
  1.1200 +* into a Request object. Once the status line and headers have been processed,
  1.1201 +* we start processing the body of the request into the Request. Finally, when
  1.1202 +* the entire body has been read, we create a Response and hand it off to the
  1.1203 +* ServerHandler to be given to the appropriate request handler.
  1.1204 +*
  1.1205 +* @param connection : Connection
  1.1206 +* the connection for the request being read
  1.1207 +*/
  1.1208 +function RequestReader(connection)
  1.1209 +{
  1.1210 +  /** Connection metadata for this request. */
  1.1211 +  this._connection = connection;
  1.1212 +
  1.1213 +  /**
  1.1214 +* A container providing line-by-line access to the raw bytes that make up the
  1.1215 +* data which has been read from the connection but has not yet been acted
  1.1216 +* upon (by passing it to the request handler or by extracting request
  1.1217 +* metadata from it).
  1.1218 +*/
  1.1219 +  this._data = new LineData();
  1.1220 +
  1.1221 +  /**
  1.1222 +* The amount of data remaining to be read from the body of this request.
  1.1223 +* After all headers in the request have been read this is the value in the
  1.1224 +* Content-Length header, but as the body is read its value decreases to zero.
  1.1225 +*/
  1.1226 +  this._contentLength = 0;
  1.1227 +
  1.1228 +  /** The current state of parsing the incoming request. */
  1.1229 +  this._state = READER_IN_REQUEST_LINE;
  1.1230 +
  1.1231 +  /** Metadata constructed from the incoming request for the request handler. */
  1.1232 +  this._metadata = new Request(connection.port);
  1.1233 +
  1.1234 +  /**
  1.1235 +* Used to preserve state if we run out of line data midway through a
  1.1236 +* multi-line header. _lastHeaderName stores the name of the header, while
  1.1237 +* _lastHeaderValue stores the value we've seen so far for the header.
  1.1238 +*
  1.1239 +* These fields are always either both undefined or both strings.
  1.1240 +*/
  1.1241 +  this._lastHeaderName = this._lastHeaderValue = undefined;
  1.1242 +}
  1.1243 +RequestReader.prototype =
  1.1244 +{
  1.1245 +  // NSIINPUTSTREAMCALLBACK
  1.1246 +
  1.1247 +  /**
  1.1248 +* Called when more data from the incoming request is available. This method
  1.1249 +* then reads the available data from input and deals with that data as
  1.1250 +* necessary, depending upon the syntax of already-downloaded data.
  1.1251 +*
  1.1252 +* @param input : nsIAsyncInputStream
  1.1253 +* the stream of incoming data from the connection
  1.1254 +*/
  1.1255 +  onInputStreamReady: function(input)
  1.1256 +  {
  1.1257 +    dumpn("*** onInputStreamReady(input=" + input + ") on thread " +
  1.1258 +          gThreadManager.currentThread + " (main is " +
  1.1259 +          gThreadManager.mainThread + ")");
  1.1260 +    dumpn("*** this._state == " + this._state);
  1.1261 +
  1.1262 +    // Handle cases where we get more data after a request error has been
  1.1263 +    // discovered but *before* we can close the connection.
  1.1264 +    var data = this._data;
  1.1265 +    if (!data)
  1.1266 +      return;
  1.1267 +
  1.1268 +    try
  1.1269 +    {
  1.1270 +      data.appendBytes(readBytes(input, input.available()));
  1.1271 +    }
  1.1272 +    catch (e)
  1.1273 +    {
  1.1274 +      if (streamClosed(e))
  1.1275 +      {
  1.1276 +        dumpn("*** WARNING: unexpected error when reading from socket; will " +
  1.1277 +              "be treated as if the input stream had been closed");
  1.1278 +        dumpn("*** WARNING: actual error was: " + e);
  1.1279 +      }
  1.1280 +
  1.1281 +      // We've lost a race -- input has been closed, but we're still expecting
  1.1282 +      // to read more data. available() will throw in this case, and since
  1.1283 +      // we're dead in the water now, destroy the connection.
  1.1284 +      dumpn("*** onInputStreamReady called on a closed input, destroying " +
  1.1285 +            "connection");
  1.1286 +      this._connection.close();
  1.1287 +      return;
  1.1288 +    }
  1.1289 +
  1.1290 +    switch (this._state)
  1.1291 +    {
  1.1292 +      default:
  1.1293 +        NS_ASSERT(false, "invalid state: " + this._state);
  1.1294 +        break;
  1.1295 +
  1.1296 +      case READER_IN_REQUEST_LINE:
  1.1297 +        if (!this._processRequestLine())
  1.1298 +          break;
  1.1299 +        /* fall through */
  1.1300 +
  1.1301 +      case READER_IN_HEADERS:
  1.1302 +        if (!this._processHeaders())
  1.1303 +          break;
  1.1304 +        /* fall through */
  1.1305 +
  1.1306 +      case READER_IN_BODY:
  1.1307 +        this._processBody();
  1.1308 +    }
  1.1309 +
  1.1310 +    if (this._state != READER_FINISHED)
  1.1311 +      input.asyncWait(this, 0, 0, gThreadManager.currentThread);
  1.1312 +  },
  1.1313 +
  1.1314 +  //
  1.1315 +  // see nsISupports.QueryInterface
  1.1316 +  //
  1.1317 +  QueryInterface: function(aIID)
  1.1318 +  {
  1.1319 +    if (aIID.equals(Ci.nsIInputStreamCallback) ||
  1.1320 +        aIID.equals(Ci.nsISupports))
  1.1321 +      return this;
  1.1322 +
  1.1323 +    throw Cr.NS_ERROR_NO_INTERFACE;
  1.1324 +  },
  1.1325 +
  1.1326 +
  1.1327 +  // PRIVATE API
  1.1328 +
  1.1329 +  /**
  1.1330 +* Processes unprocessed, downloaded data as a request line.
  1.1331 +*
  1.1332 +* @returns boolean
  1.1333 +* true iff the request line has been fully processed
  1.1334 +*/
  1.1335 +  _processRequestLine: function()
  1.1336 +  {
  1.1337 +    NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
  1.1338 +
  1.1339 +    // Servers SHOULD ignore any empty line(s) received where a Request-Line
  1.1340 +    // is expected (section 4.1).
  1.1341 +    var data = this._data;
  1.1342 +    var line = {};
  1.1343 +    var readSuccess;
  1.1344 +    while ((readSuccess = data.readLine(line)) && line.value == "")
  1.1345 +      dumpn("*** ignoring beginning blank line...");
  1.1346 +
  1.1347 +    // if we don't have a full line, wait until we do
  1.1348 +    if (!readSuccess)
  1.1349 +      return false;
  1.1350 +
  1.1351 +    // we have the first non-blank line
  1.1352 +    try
  1.1353 +    {
  1.1354 +      this._parseRequestLine(line.value);
  1.1355 +      this._state = READER_IN_HEADERS;
  1.1356 +      return true;
  1.1357 +    }
  1.1358 +    catch (e)
  1.1359 +    {
  1.1360 +      this._handleError(e);
  1.1361 +      return false;
  1.1362 +    }
  1.1363 +  },
  1.1364 +
  1.1365 +  /**
  1.1366 +* Processes stored data, assuming it is either at the beginning or in
  1.1367 +* the middle of processing request headers.
  1.1368 +*
  1.1369 +* @returns boolean
  1.1370 +* true iff header data in the request has been fully processed
  1.1371 +*/
  1.1372 +  _processHeaders: function()
  1.1373 +  {
  1.1374 +    NS_ASSERT(this._state == READER_IN_HEADERS);
  1.1375 +
  1.1376 +    // XXX things to fix here:
  1.1377 +    //
  1.1378 +    // - need to support RFC 2047-encoded non-US-ASCII characters
  1.1379 +
  1.1380 +    try
  1.1381 +    {
  1.1382 +      var done = this._parseHeaders();
  1.1383 +      if (done)
  1.1384 +      {
  1.1385 +        var request = this._metadata;
  1.1386 +
  1.1387 +        // XXX this is wrong for requests with transfer-encodings applied to
  1.1388 +        // them, particularly chunked (which by its nature can have no
  1.1389 +        // meaningful Content-Length header)!
  1.1390 +        this._contentLength = request.hasHeader("Content-Length")
  1.1391 +                            ? parseInt(request.getHeader("Content-Length"), 10)
  1.1392 +                            : 0;
  1.1393 +        dumpn("_processHeaders, Content-length=" + this._contentLength);
  1.1394 +
  1.1395 +        this._state = READER_IN_BODY;
  1.1396 +      }
  1.1397 +      return done;
  1.1398 +    }
  1.1399 +    catch (e)
  1.1400 +    {
  1.1401 +      this._handleError(e);
  1.1402 +      return false;
  1.1403 +    }
  1.1404 +  },
  1.1405 +
  1.1406 +  /**
  1.1407 +* Processes stored data, assuming it is either at the beginning or in
  1.1408 +* the middle of processing the request body.
  1.1409 +*
  1.1410 +* @returns boolean
  1.1411 +* true iff the request body has been fully processed
  1.1412 +*/
  1.1413 +  _processBody: function()
  1.1414 +  {
  1.1415 +    NS_ASSERT(this._state == READER_IN_BODY);
  1.1416 +
  1.1417 +    // XXX handle chunked transfer-coding request bodies!
  1.1418 +
  1.1419 +    try
  1.1420 +    {
  1.1421 +      if (this._contentLength > 0)
  1.1422 +      {
  1.1423 +        var data = this._data.purge();
  1.1424 +        var count = Math.min(data.length, this._contentLength);
  1.1425 +        dumpn("*** loading data=" + data + " len=" + data.length +
  1.1426 +              " excess=" + (data.length - count));
  1.1427 +
  1.1428 +        var bos = new BinaryOutputStream(this._metadata._bodyOutputStream);
  1.1429 +        bos.writeByteArray(data, count);
  1.1430 +        this._contentLength -= count;
  1.1431 +      }
  1.1432 +
  1.1433 +      dumpn("*** remaining body data len=" + this._contentLength);
  1.1434 +      if (this._contentLength == 0)
  1.1435 +      {
  1.1436 +        this._validateRequest();
  1.1437 +        this._state = READER_FINISHED;
  1.1438 +        this._handleResponse();
  1.1439 +        return true;
  1.1440 +      }
  1.1441 +      
  1.1442 +      return false;
  1.1443 +    }
  1.1444 +    catch (e)
  1.1445 +    {
  1.1446 +      this._handleError(e);
  1.1447 +      return false;
  1.1448 +    }
  1.1449 +  },
  1.1450 +
  1.1451 +  /**
  1.1452 +* Does various post-header checks on the data in this request.
  1.1453 +*
  1.1454 +* @throws : HttpError
  1.1455 +* if the request was malformed in some way
  1.1456 +*/
  1.1457 +  _validateRequest: function()
  1.1458 +  {
  1.1459 +    NS_ASSERT(this._state == READER_IN_BODY);
  1.1460 +
  1.1461 +    dumpn("*** _validateRequest");
  1.1462 +
  1.1463 +    var metadata = this._metadata;
  1.1464 +    var headers = metadata._headers;
  1.1465 +
  1.1466 +    // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header
  1.1467 +    var identity = this._connection.server.identity;
  1.1468 +    if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
  1.1469 +    {
  1.1470 +      if (!headers.hasHeader("Host"))
  1.1471 +      {
  1.1472 +        dumpn("*** malformed HTTP/1.1 or greater request with no Host header!");
  1.1473 +        throw HTTP_400;
  1.1474 +      }
  1.1475 +
  1.1476 +      // If the Request-URI wasn't absolute, then we need to determine our host.
  1.1477 +      // We have to determine what scheme was used to access us based on the
  1.1478 +      // server identity data at this point, because the request just doesn't
  1.1479 +      // contain enough data on its own to do this, sadly.
  1.1480 +      if (!metadata._host)
  1.1481 +      {
  1.1482 +        var host, port;
  1.1483 +        var hostPort = headers.getHeader("Host");
  1.1484 +        var colon = hostPort.indexOf(":");
  1.1485 +        if (colon < 0)
  1.1486 +        {
  1.1487 +          host = hostPort;
  1.1488 +          port = "";
  1.1489 +        }
  1.1490 +        else
  1.1491 +        {
  1.1492 +          host = hostPort.substring(0, colon);
  1.1493 +          port = hostPort.substring(colon + 1);
  1.1494 +        }
  1.1495 +
  1.1496 +        // NB: We allow an empty port here because, oddly, a colon may be
  1.1497 +        // present even without a port number, e.g. "example.com:"; in this
  1.1498 +        // case the default port applies.
  1.1499 +        if (!HOST_REGEX.test(host) || !/^\d*$/.test(port))
  1.1500 +        {
  1.1501 +          dumpn("*** malformed hostname (" + hostPort + ") in Host " +
  1.1502 +                "header, 400 time");
  1.1503 +          throw HTTP_400;
  1.1504 +        }
  1.1505 +
  1.1506 +        // If we're not given a port, we're stuck, because we don't know what
  1.1507 +        // scheme to use to look up the correct port here, in general. Since
  1.1508 +        // the HTTPS case requires a tunnel/proxy and thus requires that the
  1.1509 +        // requested URI be absolute (and thus contain the necessary
  1.1510 +        // information), let's assume HTTP will prevail and use that.
  1.1511 +        port = +port || 80;
  1.1512 +
  1.1513 +        var scheme = identity.getScheme(host, port);
  1.1514 +        if (!scheme)
  1.1515 +        {
  1.1516 +          dumpn("*** unrecognized hostname (" + hostPort + ") in Host " +
  1.1517 +                "header, 400 time");
  1.1518 +          throw HTTP_400;
  1.1519 +        }
  1.1520 +
  1.1521 +        metadata._scheme = scheme;
  1.1522 +        metadata._host = host;
  1.1523 +        metadata._port = port;
  1.1524 +      }
  1.1525 +    }
  1.1526 +    else
  1.1527 +    {
  1.1528 +      NS_ASSERT(metadata._host === undefined,
  1.1529 +                "HTTP/1.0 doesn't allow absolute paths in the request line!");
  1.1530 +
  1.1531 +      metadata._scheme = identity.primaryScheme;
  1.1532 +      metadata._host = identity.primaryHost;
  1.1533 +      metadata._port = identity.primaryPort;
  1.1534 +    }
  1.1535 +
  1.1536 +    NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port),
  1.1537 +              "must have a location we recognize by now!");
  1.1538 +  },
  1.1539 +
  1.1540 +  /**
  1.1541 +* Handles responses in case of error, either in the server or in the request.
  1.1542 +*
  1.1543 +* @param e
  1.1544 +* the specific error encountered, which is an HttpError in the case where
  1.1545 +* the request is in some way invalid or cannot be fulfilled; if this isn't
  1.1546 +* an HttpError we're going to be paranoid and shut down, because that
  1.1547 +* shouldn't happen, ever
  1.1548 +*/
  1.1549 +  _handleError: function(e)
  1.1550 +  {
  1.1551 +    // Don't fall back into normal processing!
  1.1552 +    this._state = READER_FINISHED;
  1.1553 +
  1.1554 +    var server = this._connection.server;
  1.1555 +    if (e instanceof HttpError)
  1.1556 +    {
  1.1557 +      var code = e.code;
  1.1558 +    }
  1.1559 +    else
  1.1560 +    {
  1.1561 +      dumpn("!!! UNEXPECTED ERROR: " + e +
  1.1562 +            (e.lineNumber ? ", line " + e.lineNumber : ""));
  1.1563 +
  1.1564 +      // no idea what happened -- be paranoid and shut down
  1.1565 +      code = 500;
  1.1566 +      server._requestQuit();
  1.1567 +    }
  1.1568 +
  1.1569 +    // make attempted reuse of data an error
  1.1570 +    this._data = null;
  1.1571 +
  1.1572 +    this._connection.processError(code, this._metadata);
  1.1573 +  },
  1.1574 +
  1.1575 +  /**
  1.1576 +* Now that we've read the request line and headers, we can actually hand off
  1.1577 +* the request to be handled.
  1.1578 +*
  1.1579 +* This method is called once per request, after the request line and all
  1.1580 +* headers and the body, if any, have been received.
  1.1581 +*/
  1.1582 +  _handleResponse: function()
  1.1583 +  {
  1.1584 +    NS_ASSERT(this._state == READER_FINISHED);
  1.1585 +
  1.1586 +    // We don't need the line-based data any more, so make attempted reuse an
  1.1587 +    // error.
  1.1588 +    this._data = null;
  1.1589 +
  1.1590 +    this._connection.process(this._metadata);
  1.1591 +  },
  1.1592 +
  1.1593 +
  1.1594 +  // PARSING
  1.1595 +
  1.1596 +  /**
  1.1597 +* Parses the request line for the HTTP request associated with this.
  1.1598 +*
  1.1599 +* @param line : string
  1.1600 +* the request line
  1.1601 +*/
  1.1602 +  _parseRequestLine: function(line)
  1.1603 +  {
  1.1604 +    NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
  1.1605 +
  1.1606 +    dumpn("*** _parseRequestLine('" + line + "')");
  1.1607 +
  1.1608 +    var metadata = this._metadata;
  1.1609 +
  1.1610 +    // clients and servers SHOULD accept any amount of SP or HT characters
  1.1611 +    // between fields, even though only a single SP is required (section 19.3)
  1.1612 +    var request = line.split(/[ \t]+/);
  1.1613 +    if (!request || request.length != 3)
  1.1614 +      throw HTTP_400;
  1.1615 +
  1.1616 +    metadata._method = request[0];
  1.1617 +
  1.1618 +    // get the HTTP version
  1.1619 +    var ver = request[2];
  1.1620 +    var match = ver.match(/^HTTP\/(\d+\.\d+)$/);
  1.1621 +    if (!match)
  1.1622 +      throw HTTP_400;
  1.1623 +
  1.1624 +    // determine HTTP version
  1.1625 +    try
  1.1626 +    {
  1.1627 +      metadata._httpVersion = new nsHttpVersion(match[1]);
  1.1628 +      if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0))
  1.1629 +        throw "unsupported HTTP version";
  1.1630 +    }
  1.1631 +    catch (e)
  1.1632 +    {
  1.1633 +      // we support HTTP/1.0 and HTTP/1.1 only
  1.1634 +      throw HTTP_501;
  1.1635 +    }
  1.1636 +
  1.1637 +
  1.1638 +    var fullPath = request[1];
  1.1639 +    var serverIdentity = this._connection.server.identity;
  1.1640 +
  1.1641 +    var scheme, host, port;
  1.1642 +
  1.1643 +    if (fullPath.charAt(0) != "/")
  1.1644 +    {
  1.1645 +      // No absolute paths in the request line in HTTP prior to 1.1
  1.1646 +      if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
  1.1647 +        throw HTTP_400;
  1.1648 +
  1.1649 +      try
  1.1650 +      {
  1.1651 +        var uri = Cc["@mozilla.org/network/io-service;1"]
  1.1652 +                    .getService(Ci.nsIIOService)
  1.1653 +                    .newURI(fullPath, null, null);
  1.1654 +        fullPath = uri.path;
  1.1655 +        scheme = uri.scheme;
  1.1656 +        host = metadata._host = uri.asciiHost;
  1.1657 +        port = uri.port;
  1.1658 +        if (port === -1)
  1.1659 +        {
  1.1660 +          if (scheme === "http")
  1.1661 +            port = 80;
  1.1662 +          else if (scheme === "https")
  1.1663 +            port = 443;
  1.1664 +          else
  1.1665 +            throw HTTP_400;
  1.1666 +        }
  1.1667 +      }
  1.1668 +      catch (e)
  1.1669 +      {
  1.1670 +        // If the host is not a valid host on the server, the response MUST be a
  1.1671 +        // 400 (Bad Request) error message (section 5.2). Alternately, the URI
  1.1672 +        // is malformed.
  1.1673 +        throw HTTP_400;
  1.1674 +      }
  1.1675 +
  1.1676 +      if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/")
  1.1677 +        throw HTTP_400;
  1.1678 +    }
  1.1679 +
  1.1680 +    var splitter = fullPath.indexOf("?");
  1.1681 +    if (splitter < 0)
  1.1682 +    {
  1.1683 +      // _queryString already set in ctor
  1.1684 +      metadata._path = fullPath;
  1.1685 +    }
  1.1686 +    else
  1.1687 +    {
  1.1688 +      metadata._path = fullPath.substring(0, splitter);
  1.1689 +      metadata._queryString = fullPath.substring(splitter + 1);
  1.1690 +    }
  1.1691 +
  1.1692 +    metadata._scheme = scheme;
  1.1693 +    metadata._host = host;
  1.1694 +    metadata._port = port;
  1.1695 +  },
  1.1696 +
  1.1697 +  /**
  1.1698 +* Parses all available HTTP headers in this until the header-ending CRLFCRLF,
  1.1699 +* adding them to the store of headers in the request.
  1.1700 +*
  1.1701 +* @throws
  1.1702 +* HTTP_400 if the headers are malformed
  1.1703 +* @returns boolean
  1.1704 +* true if all headers have now been processed, false otherwise
  1.1705 +*/
  1.1706 +  _parseHeaders: function()
  1.1707 +  {
  1.1708 +    NS_ASSERT(this._state == READER_IN_HEADERS);
  1.1709 +
  1.1710 +    dumpn("*** _parseHeaders");
  1.1711 +
  1.1712 +    var data = this._data;
  1.1713 +
  1.1714 +    var headers = this._metadata._headers;
  1.1715 +    var lastName = this._lastHeaderName;
  1.1716 +    var lastVal = this._lastHeaderValue;
  1.1717 +
  1.1718 +    var line = {};
  1.1719 +    while (true)
  1.1720 +    {
  1.1721 +      NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)),
  1.1722 +                lastName === undefined ?
  1.1723 +                  "lastVal without lastName? lastVal: '" + lastVal + "'" :
  1.1724 +                  "lastName without lastVal? lastName: '" + lastName + "'");
  1.1725 +
  1.1726 +      if (!data.readLine(line))
  1.1727 +      {
  1.1728 +        // save any data we have from the header we might still be processing
  1.1729 +        this._lastHeaderName = lastName;
  1.1730 +        this._lastHeaderValue = lastVal;
  1.1731 +        return false;
  1.1732 +      }
  1.1733 +
  1.1734 +      var lineText = line.value;
  1.1735 +      var firstChar = lineText.charAt(0);
  1.1736 +
  1.1737 +      // blank line means end of headers
  1.1738 +      if (lineText == "")
  1.1739 +      {
  1.1740 +        // we're finished with the previous header
  1.1741 +        if (lastName)
  1.1742 +        {
  1.1743 +          try
  1.1744 +          {
  1.1745 +            headers.setHeader(lastName, lastVal, true);
  1.1746 +          }
  1.1747 +          catch (e)
  1.1748 +          {
  1.1749 +            dumpn("*** e == " + e);
  1.1750 +            throw HTTP_400;
  1.1751 +          }
  1.1752 +        }
  1.1753 +        else
  1.1754 +        {
  1.1755 +          // no headers in request -- valid for HTTP/1.0 requests
  1.1756 +        }
  1.1757 +
  1.1758 +        // either way, we're done processing headers
  1.1759 +        this._state = READER_IN_BODY;
  1.1760 +        return true;
  1.1761 +      }
  1.1762 +      else if (firstChar == " " || firstChar == "\t")
  1.1763 +      {
  1.1764 +        // multi-line header if we've already seen a header line
  1.1765 +        if (!lastName)
  1.1766 +        {
  1.1767 +          // we don't have a header to continue!
  1.1768 +          throw HTTP_400;
  1.1769 +        }
  1.1770 +
  1.1771 +        // append this line's text to the value; starts with SP/HT, so no need
  1.1772 +        // for separating whitespace
  1.1773 +        lastVal += lineText;
  1.1774 +      }
  1.1775 +      else
  1.1776 +      {
  1.1777 +        // we have a new header, so set the old one (if one existed)
  1.1778 +        if (lastName)
  1.1779 +        {
  1.1780 +          try
  1.1781 +          {
  1.1782 +            headers.setHeader(lastName, lastVal, true);
  1.1783 +          }
  1.1784 +          catch (e)
  1.1785 +          {
  1.1786 +            dumpn("*** e == " + e);
  1.1787 +            throw HTTP_400;
  1.1788 +          }
  1.1789 +        }
  1.1790 +
  1.1791 +        var colon = lineText.indexOf(":"); // first colon must be splitter
  1.1792 +        if (colon < 1)
  1.1793 +        {
  1.1794 +          // no colon or missing header field-name
  1.1795 +          throw HTTP_400;
  1.1796 +        }
  1.1797 +
  1.1798 +        // set header name, value (to be set in the next loop, usually)
  1.1799 +        lastName = lineText.substring(0, colon);
  1.1800 +        lastVal = lineText.substring(colon + 1);
  1.1801 +      } // empty, continuation, start of header
  1.1802 +    } // while (true)
  1.1803 +  }
  1.1804 +};
  1.1805 +
  1.1806 +
  1.1807 +/** The character codes for CR and LF. */
  1.1808 +const CR = 0x0D, LF = 0x0A;
  1.1809 +
  1.1810 +/**
  1.1811 +* Calculates the number of characters before the first CRLF pair in array, or
  1.1812 +* -1 if the array contains no CRLF pair.
  1.1813 +*
  1.1814 +* @param array : Array
  1.1815 +* an array of numbers in the range [0, 256), each representing a single
  1.1816 +* character; the first CRLF is the lowest index i where
  1.1817 +* |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|,
  1.1818 +* if such an |i| exists, and -1 otherwise
  1.1819 +* @returns int
  1.1820 +* the index of the first CRLF if any were present, -1 otherwise
  1.1821 +*/
  1.1822 +function findCRLF(array)
  1.1823 +{
  1.1824 +  for (var i = array.indexOf(CR); i >= 0; i = array.indexOf(CR, i + 1))
  1.1825 +  {
  1.1826 +    if (array[i + 1] == LF)
  1.1827 +      return i;
  1.1828 +  }
  1.1829 +  return -1;
  1.1830 +}
  1.1831 +
  1.1832 +
  1.1833 +/**
  1.1834 +* A container which provides line-by-line access to the arrays of bytes with
  1.1835 +* which it is seeded.
  1.1836 +*/
  1.1837 +function LineData()
  1.1838 +{
  1.1839 +  /** An array of queued bytes from which to get line-based characters. */
  1.1840 +  this._data = [];
  1.1841 +}
  1.1842 +LineData.prototype =
  1.1843 +{
  1.1844 +  /**
  1.1845 +* Appends the bytes in the given array to the internal data cache maintained
  1.1846 +* by this.
  1.1847 +*/
  1.1848 +  appendBytes: function(bytes)
  1.1849 +  {
  1.1850 +    Array.prototype.push.apply(this._data, bytes);
  1.1851 +  },
  1.1852 +
  1.1853 +  /**
  1.1854 +* Removes and returns a line of data, delimited by CRLF, from this.
  1.1855 +*
  1.1856 +* @param out
  1.1857 +* an object whose "value" property will be set to the first line of text
  1.1858 +* present in this, sans CRLF, if this contains a full CRLF-delimited line
  1.1859 +* of text; if this doesn't contain enough data, the value of the property
  1.1860 +* is undefined
  1.1861 +* @returns boolean
  1.1862 +* true if a full line of data could be read from the data in this, false
  1.1863 +* otherwise
  1.1864 +*/
  1.1865 +  readLine: function(out)
  1.1866 +  {
  1.1867 +    var data = this._data;
  1.1868 +    var length = findCRLF(data);
  1.1869 +    if (length < 0)
  1.1870 +      return false;
  1.1871 +
  1.1872 +    //
  1.1873 +    // We have the index of the CR, so remove all the characters, including
  1.1874 +    // CRLF, from the array with splice, and convert the removed array into the
  1.1875 +    // corresponding string, from which we then strip the trailing CRLF.
  1.1876 +    //
  1.1877 +    // Getting the line in this matter acknowledges that substring is an O(1)
  1.1878 +    // operation in SpiderMonkey because strings are immutable, whereas two
  1.1879 +    // splices, both from the beginning of the data, are less likely to be as
  1.1880 +    // cheap as a single splice plus two extra character conversions.
  1.1881 +    //
  1.1882 +    var line = String.fromCharCode.apply(null, data.splice(0, length + 2));
  1.1883 +    out.value = line.substring(0, length);
  1.1884 +
  1.1885 +    return true;
  1.1886 +  },
  1.1887 +
  1.1888 +  /**
  1.1889 +* Removes the bytes currently within this and returns them in an array.
  1.1890 +*
  1.1891 +* @returns Array
  1.1892 +* the bytes within this when this method is called
  1.1893 +*/
  1.1894 +  purge: function()
  1.1895 +  {
  1.1896 +    var data = this._data;
  1.1897 +    this._data = [];
  1.1898 +    return data;
  1.1899 +  }
  1.1900 +};
  1.1901 +
  1.1902 +
  1.1903 +
  1.1904 +/**
  1.1905 +* Creates a request-handling function for an nsIHttpRequestHandler object.
  1.1906 +*/
  1.1907 +function createHandlerFunc(handler)
  1.1908 +{
  1.1909 +  return function(metadata, response) { handler.handle(metadata, response); };
  1.1910 +}
  1.1911 +
  1.1912 +
  1.1913 +/**
  1.1914 +* The default handler for directories; writes an HTML response containing a
  1.1915 +* slightly-formatted directory listing.
  1.1916 +*/
  1.1917 +function defaultIndexHandler(metadata, response)
  1.1918 +{
  1.1919 +  response.setHeader("Content-Type", "text/html", false);
  1.1920 +
  1.1921 +  var path = htmlEscape(decodeURI(metadata.path));
  1.1922 +
  1.1923 +  //
  1.1924 +  // Just do a very basic bit of directory listings -- no need for too much
  1.1925 +  // fanciness, especially since we don't have a style sheet in which we can
  1.1926 +  // stick rules (don't want to pollute the default path-space).
  1.1927 +  //
  1.1928 +
  1.1929 +  var body = '<html>\
  1.1930 +<head>\
  1.1931 +<title>' + path + '</title>\
  1.1932 +</head>\
  1.1933 +<body>\
  1.1934 +<h1>' + path + '</h1>\
  1.1935 +<ol style="list-style-type: none">';
  1.1936 +
  1.1937 +  var directory = metadata.getProperty("directory").QueryInterface(Ci.nsILocalFile);
  1.1938 +  NS_ASSERT(directory && directory.isDirectory());
  1.1939 +
  1.1940 +  var fileList = [];
  1.1941 +  var files = directory.directoryEntries;
  1.1942 +  while (files.hasMoreElements())
  1.1943 +  {
  1.1944 +    var f = files.getNext().QueryInterface(Ci.nsIFile);
  1.1945 +    var name = f.leafName;
  1.1946 +    if (!f.isHidden() &&
  1.1947 +        (name.charAt(name.length - 1) != HIDDEN_CHAR ||
  1.1948 +         name.charAt(name.length - 2) == HIDDEN_CHAR))
  1.1949 +      fileList.push(f);
  1.1950 +  }
  1.1951 +
  1.1952 +  fileList.sort(fileSort);
  1.1953 +
  1.1954 +  for (var i = 0; i < fileList.length; i++)
  1.1955 +  {
  1.1956 +    var file = fileList[i];
  1.1957 +    try
  1.1958 +    {
  1.1959 +      var name = file.leafName;
  1.1960 +      if (name.charAt(name.length - 1) == HIDDEN_CHAR)
  1.1961 +        name = name.substring(0, name.length - 1);
  1.1962 +      var sep = file.isDirectory() ? "/" : "";
  1.1963 +
  1.1964 +      // Note: using " to delimit the attribute here because encodeURIComponent
  1.1965 +      // passes through '.
  1.1966 +      var item = '<li><a href="' + encodeURIComponent(name) + sep + '">' +
  1.1967 +                   htmlEscape(name) + sep +
  1.1968 +                 '</a></li>';
  1.1969 +
  1.1970 +      body += item;
  1.1971 +    }
  1.1972 +    catch (e) { /* some file system error, ignore the file */ }
  1.1973 +  }
  1.1974 +
  1.1975 +  body += ' </ol>\
  1.1976 +</body>\
  1.1977 +</html>';
  1.1978 +
  1.1979 +  response.bodyOutputStream.write(body, body.length);
  1.1980 +}
  1.1981 +
  1.1982 +/**
  1.1983 +* Sorts a and b (nsIFile objects) into an aesthetically pleasing order.
  1.1984 +*/
  1.1985 +function fileSort(a, b)
  1.1986 +{
  1.1987 +  var dira = a.isDirectory(), dirb = b.isDirectory();
  1.1988 +
  1.1989 +  if (dira && !dirb)
  1.1990 +    return -1;
  1.1991 +  if (dirb && !dira)
  1.1992 +    return 1;
  1.1993 +
  1.1994 +  var namea = a.leafName.toLowerCase(), nameb = b.leafName.toLowerCase();
  1.1995 +  return nameb > namea ? -1 : 1;
  1.1996 +}
  1.1997 +
  1.1998 +
  1.1999 +/**
  1.2000 +* Converts an externally-provided path into an internal path for use in
  1.2001 +* determining file mappings.
  1.2002 +*
  1.2003 +* @param path
  1.2004 +* the path to convert
  1.2005 +* @param encoded
  1.2006 +* true if the given path should be passed through decodeURI prior to
  1.2007 +* conversion
  1.2008 +* @throws URIError
  1.2009 +* if path is incorrectly encoded
  1.2010 +*/
  1.2011 +function toInternalPath(path, encoded)
  1.2012 +{
  1.2013 +  if (encoded)
  1.2014 +    path = decodeURI(path);
  1.2015 +
  1.2016 +  var comps = path.split("/");
  1.2017 +  for (var i = 0, sz = comps.length; i < sz; i++)
  1.2018 +  {
  1.2019 +    var comp = comps[i];
  1.2020 +    if (comp.charAt(comp.length - 1) == HIDDEN_CHAR)
  1.2021 +      comps[i] = comp + HIDDEN_CHAR;
  1.2022 +  }
  1.2023 +  return comps.join("/");
  1.2024 +}
  1.2025 +
  1.2026 +
  1.2027 +/**
  1.2028 +* Adds custom-specified headers for the given file to the given response, if
  1.2029 +* any such headers are specified.
  1.2030 +*
  1.2031 +* @param file
  1.2032 +* the file on the disk which is to be written
  1.2033 +* @param metadata
  1.2034 +* metadata about the incoming request
  1.2035 +* @param response
  1.2036 +* the Response to which any specified headers/data should be written
  1.2037 +* @throws HTTP_500
  1.2038 +* if an error occurred while processing custom-specified headers
  1.2039 +*/
  1.2040 +function maybeAddHeaders(file, metadata, response)
  1.2041 +{
  1.2042 +  var name = file.leafName;
  1.2043 +  if (name.charAt(name.length - 1) == HIDDEN_CHAR)
  1.2044 +    name = name.substring(0, name.length - 1);
  1.2045 +
  1.2046 +  var headerFile = file.parent;
  1.2047 +  headerFile.append(name + HEADERS_SUFFIX);
  1.2048 +
  1.2049 +  if (!headerFile.exists())
  1.2050 +    return;
  1.2051 +
  1.2052 +  const PR_RDONLY = 0x01;
  1.2053 +  var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8),
  1.2054 +                                Ci.nsIFileInputStream.CLOSE_ON_EOF);
  1.2055 +
  1.2056 +  try
  1.2057 +  {
  1.2058 +    var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
  1.2059 +    lis.QueryInterface(Ci.nsIUnicharLineInputStream);
  1.2060 +
  1.2061 +    var line = {value: ""};
  1.2062 +    var more = lis.readLine(line);
  1.2063 +
  1.2064 +    if (!more && line.value == "")
  1.2065 +      return;
  1.2066 +
  1.2067 +
  1.2068 +    // request line
  1.2069 +
  1.2070 +    var status = line.value;
  1.2071 +    if (status.indexOf("HTTP ") == 0)
  1.2072 +    {
  1.2073 +      status = status.substring(5);
  1.2074 +      var space = status.indexOf(" ");
  1.2075 +      var code, description;
  1.2076 +      if (space < 0)
  1.2077 +      {
  1.2078 +        code = status;
  1.2079 +        description = "";
  1.2080 +      }
  1.2081 +      else
  1.2082 +      {
  1.2083 +        code = status.substring(0, space);
  1.2084 +        description = status.substring(space + 1, status.length);
  1.2085 +      }
  1.2086 +    
  1.2087 +      response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description);
  1.2088 +
  1.2089 +      line.value = "";
  1.2090 +      more = lis.readLine(line);
  1.2091 +    }
  1.2092 +
  1.2093 +    // headers
  1.2094 +    while (more || line.value != "")
  1.2095 +    {
  1.2096 +      var header = line.value;
  1.2097 +      var colon = header.indexOf(":");
  1.2098 +
  1.2099 +      response.setHeader(header.substring(0, colon),
  1.2100 +                         header.substring(colon + 1, header.length),
  1.2101 +                         false); // allow overriding server-set headers
  1.2102 +
  1.2103 +      line.value = "";
  1.2104 +      more = lis.readLine(line);
  1.2105 +    }
  1.2106 +  }
  1.2107 +  catch (e)
  1.2108 +  {
  1.2109 +    dumpn("WARNING: error in headers for " + metadata.path + ": " + e);
  1.2110 +    throw HTTP_500;
  1.2111 +  }
  1.2112 +  finally
  1.2113 +  {
  1.2114 +    fis.close();
  1.2115 +  }
  1.2116 +}
  1.2117 +
  1.2118 +
  1.2119 +/**
  1.2120 +* An object which handles requests for a server, executing default and
  1.2121 +* overridden behaviors as instructed by the code which uses and manipulates it.
  1.2122 +* Default behavior includes the paths / and /trace (diagnostics), with some
  1.2123 +* support for HTTP error pages for various codes and fallback to HTTP 500 if
  1.2124 +* those codes fail for any reason.
  1.2125 +*
  1.2126 +* @param server : nsHttpServer
  1.2127 +* the server in which this handler is being used
  1.2128 +*/
  1.2129 +function ServerHandler(server)
  1.2130 +{
  1.2131 +  // FIELDS
  1.2132 +
  1.2133 +  /**
  1.2134 +* The nsHttpServer instance associated with this handler.
  1.2135 +*/
  1.2136 +  this._server = server;
  1.2137 +
  1.2138 +  /**
  1.2139 +* A FileMap object containing the set of path->nsILocalFile mappings for
  1.2140 +* all directory mappings set in the server (e.g., "/" for /var/www/html/,
  1.2141 +* "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2).
  1.2142 +*
  1.2143 +* Note carefully: the leading and trailing "/" in each path (not file) are
  1.2144 +* removed before insertion to simplify the code which uses this. You have
  1.2145 +* been warned!
  1.2146 +*/
  1.2147 +  this._pathDirectoryMap = new FileMap();
  1.2148 +
  1.2149 +  /**
  1.2150 +* Custom request handlers for the server in which this resides. Path-handler
  1.2151 +* pairs are stored as property-value pairs in this property.
  1.2152 +*
  1.2153 +* @see ServerHandler.prototype._defaultPaths
  1.2154 +*/
  1.2155 +  this._overridePaths = {};
  1.2156 +
  1.2157 +  /**
  1.2158 +* Custom request handlers for the server in which this resides. Prefix-handler
  1.2159 +* pairs are stored as property-value pairs in this property.
  1.2160 +*/
  1.2161 +  this._overridePrefixes = {};
  1.2162 +  
  1.2163 +  /**
  1.2164 +* Custom request handlers for the error handlers in the server in which this
  1.2165 +* resides. Path-handler pairs are stored as property-value pairs in this
  1.2166 +* property.
  1.2167 +*
  1.2168 +* @see ServerHandler.prototype._defaultErrors
  1.2169 +*/
  1.2170 +  this._overrideErrors = {};
  1.2171 +
  1.2172 +  /**
  1.2173 +* Maps file extensions to their MIME types in the server, overriding any
  1.2174 +* mapping that might or might not exist in the MIME service.
  1.2175 +*/
  1.2176 +  this._mimeMappings = {};
  1.2177 +
  1.2178 +  /**
  1.2179 +* The default handler for requests for directories, used to serve directories
  1.2180 +* when no index file is present.
  1.2181 +*/
  1.2182 +  this._indexHandler = defaultIndexHandler;
  1.2183 +
  1.2184 +  /** Per-path state storage for the server. */
  1.2185 +  this._state = {};
  1.2186 +
  1.2187 +  /** Entire-server state storage. */
  1.2188 +  this._sharedState = {};
  1.2189 +
  1.2190 +  /** Entire-server state storage for nsISupports values. */
  1.2191 +  this._objectState = {};
  1.2192 +}
  1.2193 +ServerHandler.prototype =
  1.2194 +{
  1.2195 +  // PUBLIC API
  1.2196 +
  1.2197 +  /**
  1.2198 +* Handles a request to this server, responding to the request appropriately
  1.2199 +* and initiating server shutdown if necessary.
  1.2200 +*
  1.2201 +* This method never throws an exception.
  1.2202 +*
  1.2203 +* @param connection : Connection
  1.2204 +* the connection for this request
  1.2205 +*/
  1.2206 +  handleResponse: function(connection)
  1.2207 +  {
  1.2208 +    var request = connection.request;
  1.2209 +    var response = new Response(connection);
  1.2210 +
  1.2211 +    var path = request.path;
  1.2212 +    dumpn("*** path == " + path);
  1.2213 +
  1.2214 +    try
  1.2215 +    {
  1.2216 +      try
  1.2217 +      {
  1.2218 +        if (path in this._overridePaths)
  1.2219 +        {
  1.2220 +          // explicit paths first, then files based on existing directory mappings,
  1.2221 +          // then (if the file doesn't exist) built-in server default paths
  1.2222 +          dumpn("calling override for " + path);
  1.2223 +          this._overridePaths[path](request, response);
  1.2224 +        }
  1.2225 +        else
  1.2226 +        {
  1.2227 +          let longestPrefix = "";
  1.2228 +          for (let prefix in this._overridePrefixes)
  1.2229 +          {
  1.2230 +            if (prefix.length > longestPrefix.length && path.startsWith(prefix))
  1.2231 +            {
  1.2232 +              longestPrefix = prefix;
  1.2233 +            }
  1.2234 +          }
  1.2235 +          if (longestPrefix.length > 0)
  1.2236 +          {
  1.2237 +            dumpn("calling prefix override for " + longestPrefix);
  1.2238 +            this._overridePrefixes[longestPrefix](request, response);
  1.2239 +          }
  1.2240 +          else
  1.2241 +          {
  1.2242 +            this._handleDefault(request, response);
  1.2243 +          }
  1.2244 +        }
  1.2245 +      }
  1.2246 +      catch (e)
  1.2247 +      {
  1.2248 +        if (response.partiallySent())
  1.2249 +        {
  1.2250 +          response.abort(e);
  1.2251 +          return;
  1.2252 +        }
  1.2253 +
  1.2254 +        if (!(e instanceof HttpError))
  1.2255 +        {
  1.2256 +          dumpn("*** unexpected error: e == " + e);
  1.2257 +          throw HTTP_500;
  1.2258 +        }
  1.2259 +        if (e.code !== 404)
  1.2260 +          throw e;
  1.2261 +
  1.2262 +        dumpn("*** default: " + (path in this._defaultPaths));
  1.2263 +
  1.2264 +        response = new Response(connection);
  1.2265 +        if (path in this._defaultPaths)
  1.2266 +          this._defaultPaths[path](request, response);
  1.2267 +        else
  1.2268 +          throw HTTP_404;
  1.2269 +      }
  1.2270 +    }
  1.2271 +    catch (e)
  1.2272 +    {
  1.2273 +      if (response.partiallySent())
  1.2274 +      {
  1.2275 +        response.abort(e);
  1.2276 +        return;
  1.2277 +      }
  1.2278 +
  1.2279 +      var errorCode = "internal";
  1.2280 +
  1.2281 +      try
  1.2282 +      {
  1.2283 +        if (!(e instanceof HttpError))
  1.2284 +          throw e;
  1.2285 +
  1.2286 +        errorCode = e.code;
  1.2287 +        dumpn("*** errorCode == " + errorCode);
  1.2288 +
  1.2289 +        response = new Response(connection);
  1.2290 +        if (e.customErrorHandling)
  1.2291 +          e.customErrorHandling(response);
  1.2292 +        this._handleError(errorCode, request, response);
  1.2293 +        return;
  1.2294 +      }
  1.2295 +      catch (e2)
  1.2296 +      {
  1.2297 +        dumpn("*** error handling " + errorCode + " error: " +
  1.2298 +              "e2 == " + e2 + ", shutting down server");
  1.2299 +
  1.2300 +        connection.server._requestQuit();
  1.2301 +        response.abort(e2);
  1.2302 +        return;
  1.2303 +      }
  1.2304 +    }
  1.2305 +
  1.2306 +    response.complete();
  1.2307 +  },
  1.2308 +
  1.2309 +  //
  1.2310 +  // see nsIHttpServer.registerFile
  1.2311 +  //
  1.2312 +  registerFile: function(path, file)
  1.2313 +  {
  1.2314 +    if (!file)
  1.2315 +    {
  1.2316 +      dumpn("*** unregistering '" + path + "' mapping");
  1.2317 +      delete this._overridePaths[path];
  1.2318 +      return;
  1.2319 +    }
  1.2320 +
  1.2321 +    dumpn("*** registering '" + path + "' as mapping to " + file.path);
  1.2322 +    file = file.clone();
  1.2323 +
  1.2324 +    var self = this;
  1.2325 +    this._overridePaths[path] =
  1.2326 +      function(request, response)
  1.2327 +      {
  1.2328 +        if (!file.exists())
  1.2329 +          throw HTTP_404;
  1.2330 +
  1.2331 +        response.setStatusLine(request.httpVersion, 200, "OK");
  1.2332 +        self._writeFileResponse(request, file, response, 0, file.fileSize);
  1.2333 +      };
  1.2334 +  },
  1.2335 +
  1.2336 +  //
  1.2337 +  // see nsIHttpServer.registerPathHandler
  1.2338 +  //
  1.2339 +  registerPathHandler: function(path, handler)
  1.2340 +  {
  1.2341 +    // XXX true path validation!
  1.2342 +    if (path.charAt(0) != "/")
  1.2343 +      throw Cr.NS_ERROR_INVALID_ARG;
  1.2344 +
  1.2345 +    this._handlerToField(handler, this._overridePaths, path);
  1.2346 +  },
  1.2347 +
  1.2348 +  //
  1.2349 +  // see nsIHttpServer.registerPrefixHandler
  1.2350 +  //
  1.2351 +  registerPrefixHandler: function(prefix, handler)
  1.2352 +  {
  1.2353 +    // XXX true prefix validation!
  1.2354 +    if (!(prefix.startsWith("/") && prefix.endsWith("/")))
  1.2355 +      throw Cr.NS_ERROR_INVALID_ARG;
  1.2356 +
  1.2357 +    this._handlerToField(handler, this._overridePrefixes, prefix);
  1.2358 +  },
  1.2359 +
  1.2360 +  //
  1.2361 +  // see nsIHttpServer.registerDirectory
  1.2362 +  //
  1.2363 +  registerDirectory: function(path, directory)
  1.2364 +  {
  1.2365 +    // strip off leading and trailing '/' so that we can use lastIndexOf when
  1.2366 +    // determining exactly how a path maps onto a mapped directory --
  1.2367 +    // conditional is required here to deal with "/".substring(1, 0) being
  1.2368 +    // converted to "/".substring(0, 1) per the JS specification
  1.2369 +    var key = path.length == 1 ? "" : path.substring(1, path.length - 1);
  1.2370 +
  1.2371 +    // the path-to-directory mapping code requires that the first character not
  1.2372 +    // be "/", or it will go into an infinite loop
  1.2373 +    if (key.charAt(0) == "/")
  1.2374 +      throw Cr.NS_ERROR_INVALID_ARG;
  1.2375 +
  1.2376 +    key = toInternalPath(key, false);
  1.2377 +
  1.2378 +    if (directory)
  1.2379 +    {
  1.2380 +      dumpn("*** mapping '" + path + "' to the location " + directory.path);
  1.2381 +      this._pathDirectoryMap.put(key, directory);
  1.2382 +    }
  1.2383 +    else
  1.2384 +    {
  1.2385 +      dumpn("*** removing mapping for '" + path + "'");
  1.2386 +      this._pathDirectoryMap.put(key, null);
  1.2387 +    }
  1.2388 +  },
  1.2389 +
  1.2390 +  //
  1.2391 +  // see nsIHttpServer.registerErrorHandler
  1.2392 +  //
  1.2393 +  registerErrorHandler: function(err, handler)
  1.2394 +  {
  1.2395 +    if (!(err in HTTP_ERROR_CODES))
  1.2396 +      dumpn("*** WARNING: registering non-HTTP/1.1 error code " +
  1.2397 +            "(" + err + ") handler -- was this intentional?");
  1.2398 +
  1.2399 +    this._handlerToField(handler, this._overrideErrors, err);
  1.2400 +  },
  1.2401 +
  1.2402 +  //
  1.2403 +  // see nsIHttpServer.setIndexHandler
  1.2404 +  //
  1.2405 +  setIndexHandler: function(handler)
  1.2406 +  {
  1.2407 +    if (!handler)
  1.2408 +      handler = defaultIndexHandler;
  1.2409 +    else if (typeof(handler) != "function")
  1.2410 +      handler = createHandlerFunc(handler);
  1.2411 +
  1.2412 +    this._indexHandler = handler;
  1.2413 +  },
  1.2414 +
  1.2415 +  //
  1.2416 +  // see nsIHttpServer.registerContentType
  1.2417 +  //
  1.2418 +  registerContentType: function(ext, type)
  1.2419 +  {
  1.2420 +    if (!type)
  1.2421 +      delete this._mimeMappings[ext];
  1.2422 +    else
  1.2423 +      this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type);
  1.2424 +  },
  1.2425 +
  1.2426 +  // PRIVATE API
  1.2427 +
  1.2428 +  /**
  1.2429 +* Sets or remove (if handler is null) a handler in an object with a key.
  1.2430 +*
  1.2431 +* @param handler
  1.2432 +* a handler, either function or an nsIHttpRequestHandler
  1.2433 +* @param dict
  1.2434 +* The object to attach the handler to.
  1.2435 +* @param key
  1.2436 +* The field name of the handler.
  1.2437 +*/
  1.2438 +  _handlerToField: function(handler, dict, key)
  1.2439 +  {
  1.2440 +    // for convenience, handler can be a function if this is run from xpcshell
  1.2441 +    if (typeof(handler) == "function")
  1.2442 +      dict[key] = handler;
  1.2443 +    else if (handler)
  1.2444 +      dict[key] = createHandlerFunc(handler);
  1.2445 +    else
  1.2446 +      delete dict[key];
  1.2447 +  },
  1.2448 +
  1.2449 +  /**
  1.2450 +* Handles a request which maps to a file in the local filesystem (if a base
  1.2451 +* path has already been set; otherwise the 404 error is thrown).
  1.2452 +*
  1.2453 +* @param metadata : Request
  1.2454 +* metadata for the incoming request
  1.2455 +* @param response : Response
  1.2456 +* an uninitialized Response to the given request, to be initialized by a
  1.2457 +* request handler
  1.2458 +* @throws HTTP_###
  1.2459 +* if an HTTP error occurred (usually HTTP_404); note that in this case the
  1.2460 +* calling code must handle post-processing of the response
  1.2461 +*/
  1.2462 +  _handleDefault: function(metadata, response)
  1.2463 +  {
  1.2464 +    dumpn("*** _handleDefault()");
  1.2465 +
  1.2466 +    response.setStatusLine(metadata.httpVersion, 200, "OK");
  1.2467 +
  1.2468 +    var path = metadata.path;
  1.2469 +    NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">");
  1.2470 +
  1.2471 +    // determine the actual on-disk file; this requires finding the deepest
  1.2472 +    // path-to-directory mapping in the requested URL
  1.2473 +    var file = this._getFileForPath(path);
  1.2474 +
  1.2475 +    // the "file" might be a directory, in which case we either serve the
  1.2476 +    // contained index.html or make the index handler write the response
  1.2477 +    if (file.exists() && file.isDirectory())
  1.2478 +    {
  1.2479 +      file.append("index.html"); // make configurable?
  1.2480 +      if (!file.exists() || file.isDirectory())
  1.2481 +      {
  1.2482 +        metadata._ensurePropertyBag();
  1.2483 +        metadata._bag.setPropertyAsInterface("directory", file.parent);
  1.2484 +        this._indexHandler(metadata, response);
  1.2485 +        return;
  1.2486 +      }
  1.2487 +    }
  1.2488 +
  1.2489 +    // alternately, the file might not exist
  1.2490 +    if (!file.exists())
  1.2491 +      throw HTTP_404;
  1.2492 +
  1.2493 +    var start, end;
  1.2494 +    if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) &&
  1.2495 +        metadata.hasHeader("Range") &&
  1.2496 +        this._getTypeFromFile(file) !== SJS_TYPE)
  1.2497 +    {
  1.2498 +      var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/);
  1.2499 +      if (!rangeMatch)
  1.2500 +        throw HTTP_400;
  1.2501 +
  1.2502 +      if (rangeMatch[1] !== undefined)
  1.2503 +        start = parseInt(rangeMatch[1], 10);
  1.2504 +
  1.2505 +      if (rangeMatch[2] !== undefined)
  1.2506 +        end = parseInt(rangeMatch[2], 10);
  1.2507 +
  1.2508 +      if (start === undefined && end === undefined)
  1.2509 +        throw HTTP_400;
  1.2510 +
  1.2511 +      // No start given, so the end is really the count of bytes from the
  1.2512 +      // end of the file.
  1.2513 +      if (start === undefined)
  1.2514 +      {
  1.2515 +        start = Math.max(0, file.fileSize - end);
  1.2516 +        end = file.fileSize - 1;
  1.2517 +      }
  1.2518 +
  1.2519 +      // start and end are inclusive
  1.2520 +      if (end === undefined || end >= file.fileSize)
  1.2521 +        end = file.fileSize - 1;
  1.2522 +
  1.2523 +      if (start !== undefined && start >= file.fileSize) {
  1.2524 +        var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable");
  1.2525 +        HTTP_416.customErrorHandling = function(errorResponse)
  1.2526 +        {
  1.2527 +          maybeAddHeaders(file, metadata, errorResponse);
  1.2528 +        };
  1.2529 +        throw HTTP_416;
  1.2530 +      }
  1.2531 +
  1.2532 +      if (end < start)
  1.2533 +      {
  1.2534 +        response.setStatusLine(metadata.httpVersion, 200, "OK");
  1.2535 +        start = 0;
  1.2536 +        end = file.fileSize - 1;
  1.2537 +      }
  1.2538 +      else
  1.2539 +      {
  1.2540 +        response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
  1.2541 +        var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize;
  1.2542 +        response.setHeader("Content-Range", contentRange);
  1.2543 +      }
  1.2544 +    }
  1.2545 +    else
  1.2546 +    {
  1.2547 +      start = 0;
  1.2548 +      end = file.fileSize - 1;
  1.2549 +    }
  1.2550 +
  1.2551 +    // finally...
  1.2552 +    dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " +
  1.2553 +          start + " to " + end + " inclusive");
  1.2554 +    this._writeFileResponse(metadata, file, response, start, end - start + 1);
  1.2555 +  },
  1.2556 +
  1.2557 +  /**
  1.2558 +* Writes an HTTP response for the given file, including setting headers for
  1.2559 +* file metadata.
  1.2560 +*
  1.2561 +* @param metadata : Request
  1.2562 +* the Request for which a response is being generated
  1.2563 +* @param file : nsILocalFile
  1.2564 +* the file which is to be sent in the response
  1.2565 +* @param response : Response
  1.2566 +* the response to which the file should be written
  1.2567 +* @param offset: uint
  1.2568 +* the byte offset to skip to when writing
  1.2569 +* @param count: uint
  1.2570 +* the number of bytes to write
  1.2571 +*/
  1.2572 +  _writeFileResponse: function(metadata, file, response, offset, count)
  1.2573 +  {
  1.2574 +    const PR_RDONLY = 0x01;
  1.2575 +
  1.2576 +    var type = this._getTypeFromFile(file);
  1.2577 +    if (type === SJS_TYPE)
  1.2578 +    {
  1.2579 +      var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
  1.2580 +                                    Ci.nsIFileInputStream.CLOSE_ON_EOF);
  1.2581 +
  1.2582 +      try
  1.2583 +      {
  1.2584 +        var sis = new ScriptableInputStream(fis);
  1.2585 +        var s = Cu.Sandbox(gGlobalObject);
  1.2586 +        s.importFunction(dump, "dump");
  1.2587 +
  1.2588 +        // Define a basic key-value state-preservation API across requests, with
  1.2589 +        // keys initially corresponding to the empty string.
  1.2590 +        var self = this;
  1.2591 +        var path = metadata.path;
  1.2592 +        s.importFunction(function getState(k)
  1.2593 +        {
  1.2594 +          return self._getState(path, k);
  1.2595 +        });
  1.2596 +        s.importFunction(function setState(k, v)
  1.2597 +        {
  1.2598 +          self._setState(path, k, v);
  1.2599 +        });
  1.2600 +        s.importFunction(function getSharedState(k)
  1.2601 +        {
  1.2602 +          return self._getSharedState(k);
  1.2603 +        });
  1.2604 +        s.importFunction(function setSharedState(k, v)
  1.2605 +        {
  1.2606 +          self._setSharedState(k, v);
  1.2607 +        });
  1.2608 +        s.importFunction(function getObjectState(k, callback)
  1.2609 +        {
  1.2610 +          callback(self._getObjectState(k));
  1.2611 +        });
  1.2612 +        s.importFunction(function setObjectState(k, v)
  1.2613 +        {
  1.2614 +          self._setObjectState(k, v);
  1.2615 +        });
  1.2616 +        s.importFunction(function registerPathHandler(p, h)
  1.2617 +        {
  1.2618 +          self.registerPathHandler(p, h);
  1.2619 +        });
  1.2620 +
  1.2621 +        // Make it possible for sjs files to access their location
  1.2622 +        this._setState(path, "__LOCATION__", file.path);
  1.2623 +
  1.2624 +        try
  1.2625 +        {
  1.2626 +          // Alas, the line number in errors dumped to console when calling the
  1.2627 +          // request handler is simply an offset from where we load the SJS file.
  1.2628 +          // Work around this in a reasonably non-fragile way by dynamically
  1.2629 +          // getting the line number where we evaluate the SJS file. Don't
  1.2630 +          // separate these two lines!
  1.2631 +          var line = new Error().lineNumber;
  1.2632 +          Cu.evalInSandbox(sis.read(file.fileSize), s);
  1.2633 +        }
  1.2634 +        catch (e)
  1.2635 +        {
  1.2636 +          dumpn("*** syntax error in SJS at " + file.path + ": " + e);
  1.2637 +          throw HTTP_500;
  1.2638 +        }
  1.2639 +
  1.2640 +        try
  1.2641 +        {
  1.2642 +          s.handleRequest(metadata, response);
  1.2643 +        }
  1.2644 +        catch (e)
  1.2645 +        {
  1.2646 +          dump("*** error running SJS at " + file.path + ": " +
  1.2647 +               e + " on line " +
  1.2648 +               (e instanceof Error
  1.2649 +               ? e.lineNumber + " in httpd.js"
  1.2650 +               : (e.lineNumber - line)) + "\n");
  1.2651 +          throw HTTP_500;
  1.2652 +        }
  1.2653 +      }
  1.2654 +      finally
  1.2655 +      {
  1.2656 +        fis.close();
  1.2657 +      }
  1.2658 +    }
  1.2659 +    else
  1.2660 +    {
  1.2661 +      try
  1.2662 +      {
  1.2663 +        response.setHeader("Last-Modified",
  1.2664 +                           toDateString(file.lastModifiedTime),
  1.2665 +                           false);
  1.2666 +      }
  1.2667 +      catch (e) { /* lastModifiedTime threw, ignore */ }
  1.2668 +
  1.2669 +      response.setHeader("Content-Type", type, false);
  1.2670 +      maybeAddHeaders(file, metadata, response);
  1.2671 +      response.setHeader("Content-Length", "" + count, false);
  1.2672 +
  1.2673 +      var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
  1.2674 +                                    Ci.nsIFileInputStream.CLOSE_ON_EOF);
  1.2675 +
  1.2676 +      offset = offset || 0;
  1.2677 +      count = count || file.fileSize;
  1.2678 +      NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset");
  1.2679 +      NS_ASSERT(count >= 0, "bad count");
  1.2680 +      NS_ASSERT(offset + count <= file.fileSize, "bad total data size");
  1.2681 +
  1.2682 +      try
  1.2683 +      {
  1.2684 +        if (offset !== 0)
  1.2685 +        {
  1.2686 +          // Seek (or read, if seeking isn't supported) to the correct offset so
  1.2687 +          // the data sent to the client matches the requested range.
  1.2688 +          if (fis instanceof Ci.nsISeekableStream)
  1.2689 +            fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset);
  1.2690 +          else
  1.2691 +            new ScriptableInputStream(fis).read(offset);
  1.2692 +        }
  1.2693 +      }
  1.2694 +      catch (e)
  1.2695 +      {
  1.2696 +        fis.close();
  1.2697 +        throw e;
  1.2698 +      }
  1.2699 +
  1.2700 +      let writeMore = function writeMore()
  1.2701 +      {
  1.2702 +        gThreadManager.currentThread
  1.2703 +                      .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL);
  1.2704 +      }
  1.2705 +
  1.2706 +      var input = new BinaryInputStream(fis);
  1.2707 +      var output = new BinaryOutputStream(response.bodyOutputStream);
  1.2708 +      var writeData =
  1.2709 +        {
  1.2710 +          run: function()
  1.2711 +          {
  1.2712 +            var chunkSize = Math.min(65536, count);
  1.2713 +            count -= chunkSize;
  1.2714 +            NS_ASSERT(count >= 0, "underflow");
  1.2715 +
  1.2716 +            try
  1.2717 +            {
  1.2718 +              var data = input.readByteArray(chunkSize);
  1.2719 +              NS_ASSERT(data.length === chunkSize,
  1.2720 +                        "incorrect data returned? got " + data.length +
  1.2721 +                        ", expected " + chunkSize);
  1.2722 +              output.writeByteArray(data, data.length);
  1.2723 +              if (count === 0)
  1.2724 +              {
  1.2725 +                fis.close();
  1.2726 +                response.finish();
  1.2727 +              }
  1.2728 +              else
  1.2729 +              {
  1.2730 +                writeMore();
  1.2731 +              }
  1.2732 +            }
  1.2733 +            catch (e)
  1.2734 +            {
  1.2735 +              try
  1.2736 +              {
  1.2737 +                fis.close();
  1.2738 +              }
  1.2739 +              finally
  1.2740 +              {
  1.2741 +                response.finish();
  1.2742 +              }
  1.2743 +              throw e;
  1.2744 +            }
  1.2745 +          }
  1.2746 +        };
  1.2747 +
  1.2748 +      writeMore();
  1.2749 +
  1.2750 +      // Now that we know copying will start, flag the response as async.
  1.2751 +      response.processAsync();
  1.2752 +    }
  1.2753 +  },
  1.2754 +
  1.2755 +  /**
  1.2756 +* Get the value corresponding to a given key for the given path for SJS state
  1.2757 +* preservation across requests.
  1.2758 +*
  1.2759 +* @param path : string
  1.2760 +* the path from which the given state is to be retrieved
  1.2761 +* @param k : string
  1.2762 +* the key whose corresponding value is to be returned
  1.2763 +* @returns string
  1.2764 +* the corresponding value, which is initially the empty string
  1.2765 +*/
  1.2766 +  _getState: function(path, k)
  1.2767 +  {
  1.2768 +    var state = this._state;
  1.2769 +    if (path in state && k in state[path])
  1.2770 +      return state[path][k];
  1.2771 +    return "";
  1.2772 +  },
  1.2773 +
  1.2774 +  /**
  1.2775 +* Set the value corresponding to a given key for the given path for SJS state
  1.2776 +* preservation across requests.
  1.2777 +*
  1.2778 +* @param path : string
  1.2779 +* the path from which the given state is to be retrieved
  1.2780 +* @param k : string
  1.2781 +* the key whose corresponding value is to be set
  1.2782 +* @param v : string
  1.2783 +* the value to be set
  1.2784 +*/
  1.2785 +  _setState: function(path, k, v)
  1.2786 +  {
  1.2787 +    if (typeof v !== "string")
  1.2788 +      throw new Error("non-string value passed");
  1.2789 +    var state = this._state;
  1.2790 +    if (!(path in state))
  1.2791 +      state[path] = {};
  1.2792 +    state[path][k] = v;
  1.2793 +  },
  1.2794 +
  1.2795 +  /**
  1.2796 +* Get the value corresponding to a given key for SJS state preservation
  1.2797 +* across requests.
  1.2798 +*
  1.2799 +* @param k : string
  1.2800 +* the key whose corresponding value is to be returned
  1.2801 +* @returns string
  1.2802 +* the corresponding value, which is initially the empty string
  1.2803 +*/
  1.2804 +  _getSharedState: function(k)
  1.2805 +  {
  1.2806 +    var state = this._sharedState;
  1.2807 +    if (k in state)
  1.2808 +      return state[k];
  1.2809 +    return "";
  1.2810 +  },
  1.2811 +
  1.2812 +  /**
  1.2813 +* Set the value corresponding to a given key for SJS state preservation
  1.2814 +* across requests.
  1.2815 +*
  1.2816 +* @param k : string
  1.2817 +* the key whose corresponding value is to be set
  1.2818 +* @param v : string
  1.2819 +* the value to be set
  1.2820 +*/
  1.2821 +  _setSharedState: function(k, v)
  1.2822 +  {
  1.2823 +    if (typeof v !== "string")
  1.2824 +      throw new Error("non-string value passed");
  1.2825 +    this._sharedState[k] = v;
  1.2826 +  },
  1.2827 +
  1.2828 +  /**
  1.2829 +* Returns the object associated with the given key in the server for SJS
  1.2830 +* state preservation across requests.
  1.2831 +*
  1.2832 +* @param k : string
  1.2833 +* the key whose corresponding object is to be returned
  1.2834 +* @returns nsISupports
  1.2835 +* the corresponding object, or null if none was present
  1.2836 +*/
  1.2837 +  _getObjectState: function(k)
  1.2838 +  {
  1.2839 +    if (typeof k !== "string")
  1.2840 +      throw new Error("non-string key passed");
  1.2841 +    return this._objectState[k] || null;
  1.2842 +  },
  1.2843 +
  1.2844 +  /**
  1.2845 +* Sets the object associated with the given key in the server for SJS
  1.2846 +* state preservation across requests.
  1.2847 +*
  1.2848 +* @param k : string
  1.2849 +* the key whose corresponding object is to be set
  1.2850 +* @param v : nsISupports
  1.2851 +* the object to be associated with the given key; may be null
  1.2852 +*/
  1.2853 +  _setObjectState: function(k, v)
  1.2854 +  {
  1.2855 +    if (typeof k !== "string")
  1.2856 +      throw new Error("non-string key passed");
  1.2857 +    if (typeof v !== "object")
  1.2858 +      throw new Error("non-object value passed");
  1.2859 +    if (v && !("QueryInterface" in v))
  1.2860 +    {
  1.2861 +      throw new Error("must pass an nsISupports; use wrappedJSObject to ease " +
  1.2862 +                      "pain when using the server from JS");
  1.2863 +    }
  1.2864 +
  1.2865 +    this._objectState[k] = v;
  1.2866 +  },
  1.2867 +
  1.2868 +  /**
  1.2869 +* Gets a content-type for the given file, first by checking for any custom
  1.2870 +* MIME-types registered with this handler for the file's extension, second by
  1.2871 +* asking the global MIME service for a content-type, and finally by failing
  1.2872 +* over to application/octet-stream.
  1.2873 +*
  1.2874 +* @param file : nsIFile
  1.2875 +* the nsIFile for which to get a file type
  1.2876 +* @returns string
  1.2877 +* the best content-type which can be determined for the file
  1.2878 +*/
  1.2879 +  _getTypeFromFile: function(file)
  1.2880 +  {
  1.2881 +    try
  1.2882 +    {
  1.2883 +      var name = file.leafName;
  1.2884 +      var dot = name.lastIndexOf(".");
  1.2885 +      if (dot > 0)
  1.2886 +      {
  1.2887 +        var ext = name.slice(dot + 1);
  1.2888 +        if (ext in this._mimeMappings)
  1.2889 +          return this._mimeMappings[ext];
  1.2890 +      }
  1.2891 +      return Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
  1.2892 +               .getService(Ci.nsIMIMEService)
  1.2893 +               .getTypeFromFile(file);
  1.2894 +    }
  1.2895 +    catch (e)
  1.2896 +    {
  1.2897 +      return "application/octet-stream";
  1.2898 +    }
  1.2899 +  },
  1.2900 +
  1.2901 +  /**
  1.2902 +* Returns the nsILocalFile which corresponds to the path, as determined using
  1.2903 +* all registered path->directory mappings and any paths which are explicitly
  1.2904 +* overridden.
  1.2905 +*
  1.2906 +* @param path : string
  1.2907 +* the server path for which a file should be retrieved, e.g. "/foo/bar"
  1.2908 +* @throws HttpError
  1.2909 +* when the correct action is the corresponding HTTP error (i.e., because no
  1.2910 +* mapping was found for a directory in path, the referenced file doesn't
  1.2911 +* exist, etc.)
  1.2912 +* @returns nsILocalFile
  1.2913 +* the file to be sent as the response to a request for the path
  1.2914 +*/
  1.2915 +  _getFileForPath: function(path)
  1.2916 +  {
  1.2917 +    // decode and add underscores as necessary
  1.2918 +    try
  1.2919 +    {
  1.2920 +      path = toInternalPath(path, true);
  1.2921 +    }
  1.2922 +    catch (e)
  1.2923 +    {
  1.2924 +      throw HTTP_400; // malformed path
  1.2925 +    }
  1.2926 +
  1.2927 +    // next, get the directory which contains this path
  1.2928 +    var pathMap = this._pathDirectoryMap;
  1.2929 +
  1.2930 +    // An example progression of tmp for a path "/foo/bar/baz/" might be:
  1.2931 +    // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", ""
  1.2932 +    var tmp = path.substring(1);
  1.2933 +    while (true)
  1.2934 +    {
  1.2935 +      // do we have a match for current head of the path?
  1.2936 +      var file = pathMap.get(tmp);
  1.2937 +      if (file)
  1.2938 +      {
  1.2939 +        // XXX hack; basically disable showing mapping for /foo/bar/ when the
  1.2940 +        // requested path was /foo/bar, because relative links on the page
  1.2941 +        // will all be incorrect -- we really need the ability to easily
  1.2942 +        // redirect here instead
  1.2943 +        if (tmp == path.substring(1) &&
  1.2944 +            tmp.length != 0 &&
  1.2945 +            tmp.charAt(tmp.length - 1) != "/")
  1.2946 +          file = null;
  1.2947 +        else
  1.2948 +          break;
  1.2949 +      }
  1.2950 +
  1.2951 +      // if we've finished trying all prefixes, exit
  1.2952 +      if (tmp == "")
  1.2953 +        break;
  1.2954 +
  1.2955 +      tmp = tmp.substring(0, tmp.lastIndexOf("/"));
  1.2956 +    }
  1.2957 +
  1.2958 +    // no mapping applies, so 404
  1.2959 +    if (!file)
  1.2960 +      throw HTTP_404;
  1.2961 +
  1.2962 +
  1.2963 +    // last, get the file for the path within the determined directory
  1.2964 +    var parentFolder = file.parent;
  1.2965 +    var dirIsRoot = (parentFolder == null);
  1.2966 +
  1.2967 +    // Strategy here is to append components individually, making sure we
  1.2968 +    // never move above the given directory; this allows paths such as
  1.2969 +    // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling";
  1.2970 +    // this component-wise approach also means the code works even on platforms
  1.2971 +    // which don't use "/" as the directory separator, such as Windows
  1.2972 +    var leafPath = path.substring(tmp.length + 1);
  1.2973 +    var comps = leafPath.split("/");
  1.2974 +    for (var i = 0, sz = comps.length; i < sz; i++)
  1.2975 +    {
  1.2976 +      var comp = comps[i];
  1.2977 +
  1.2978 +      if (comp == "..")
  1.2979 +        file = file.parent;
  1.2980 +      else if (comp == "." || comp == "")
  1.2981 +        continue;
  1.2982 +      else
  1.2983 +        file.append(comp);
  1.2984 +
  1.2985 +      if (!dirIsRoot && file.equals(parentFolder))
  1.2986 +        throw HTTP_403;
  1.2987 +    }
  1.2988 +
  1.2989 +    return file;
  1.2990 +  },
  1.2991 +
  1.2992 +  /**
  1.2993 +* Writes the error page for the given HTTP error code over the given
  1.2994 +* connection.
  1.2995 +*
  1.2996 +* @param errorCode : uint
  1.2997 +* the HTTP error code to be used
  1.2998 +* @param connection : Connection
  1.2999 +* the connection on which the error occurred
  1.3000 +*/
  1.3001 +  handleError: function(errorCode, connection)
  1.3002 +  {
  1.3003 +    var response = new Response(connection);
  1.3004 +
  1.3005 +    dumpn("*** error in request: " + errorCode);
  1.3006 +
  1.3007 +    this._handleError(errorCode, new Request(connection.port), response);
  1.3008 +  },
  1.3009 +
  1.3010 +  /**
  1.3011 +* Handles a request which generates the given error code, using the
  1.3012 +* user-defined error handler if one has been set, gracefully falling back to
  1.3013 +* the x00 status code if the code has no handler, and failing to status code
  1.3014 +* 500 if all else fails.
  1.3015 +*
  1.3016 +* @param errorCode : uint
  1.3017 +* the HTTP error which is to be returned
  1.3018 +* @param metadata : Request
  1.3019 +* metadata for the request, which will often be incomplete since this is an
  1.3020 +* error
  1.3021 +* @param response : Response
  1.3022 +* an uninitialized Response should be initialized when this method
  1.3023 +* completes with information which represents the desired error code in the
  1.3024 +* ideal case or a fallback code in abnormal circumstances (i.e., 500 is a
  1.3025 +* fallback for 505, per HTTP specs)
  1.3026 +*/
  1.3027 +  _handleError: function(errorCode, metadata, response)
  1.3028 +  {
  1.3029 +    if (!metadata)
  1.3030 +      throw Cr.NS_ERROR_NULL_POINTER;
  1.3031 +
  1.3032 +    var errorX00 = errorCode - (errorCode % 100);
  1.3033 +
  1.3034 +    try
  1.3035 +    {
  1.3036 +      if (!(errorCode in HTTP_ERROR_CODES))
  1.3037 +        dumpn("*** WARNING: requested invalid error: " + errorCode);
  1.3038 +
  1.3039 +      // RFC 2616 says that we should try to handle an error by its class if we
  1.3040 +      // can't otherwise handle it -- if that fails, we revert to handling it as
  1.3041 +      // a 500 internal server error, and if that fails we throw and shut down
  1.3042 +      // the server
  1.3043 +
  1.3044 +      // actually handle the error
  1.3045 +      try
  1.3046 +      {
  1.3047 +        if (errorCode in this._overrideErrors)
  1.3048 +          this._overrideErrors[errorCode](metadata, response);
  1.3049 +        else
  1.3050 +          this._defaultErrors[errorCode](metadata, response);
  1.3051 +      }
  1.3052 +      catch (e)
  1.3053 +      {
  1.3054 +        if (response.partiallySent())
  1.3055 +        {
  1.3056 +          response.abort(e);
  1.3057 +          return;
  1.3058 +        }
  1.3059 +
  1.3060 +        // don't retry the handler that threw
  1.3061 +        if (errorX00 == errorCode)
  1.3062 +          throw HTTP_500;
  1.3063 +
  1.3064 +        dumpn("*** error in handling for error code " + errorCode + ", " +
  1.3065 +              "falling back to " + errorX00 + "...");
  1.3066 +        response = new Response(response._connection);
  1.3067 +        if (errorX00 in this._overrideErrors)
  1.3068 +          this._overrideErrors[errorX00](metadata, response);
  1.3069 +        else if (errorX00 in this._defaultErrors)
  1.3070 +          this._defaultErrors[errorX00](metadata, response);
  1.3071 +        else
  1.3072 +          throw HTTP_500;
  1.3073 +      }
  1.3074 +    }
  1.3075 +    catch (e)
  1.3076 +    {
  1.3077 +      if (response.partiallySent())
  1.3078 +      {
  1.3079 +        response.abort();
  1.3080 +        return;
  1.3081 +      }
  1.3082 +
  1.3083 +      // we've tried everything possible for a meaningful error -- now try 500
  1.3084 +      dumpn("*** error in handling for error code " + errorX00 + ", falling " +
  1.3085 +            "back to 500...");
  1.3086 +
  1.3087 +      try
  1.3088 +      {
  1.3089 +        response = new Response(response._connection);
  1.3090 +        if (500 in this._overrideErrors)
  1.3091 +          this._overrideErrors[500](metadata, response);
  1.3092 +        else
  1.3093 +          this._defaultErrors[500](metadata, response);
  1.3094 +      }
  1.3095 +      catch (e2)
  1.3096 +      {
  1.3097 +        dumpn("*** multiple errors in default error handlers!");
  1.3098 +        dumpn("*** e == " + e + ", e2 == " + e2);
  1.3099 +        response.abort(e2);
  1.3100 +        return;
  1.3101 +      }
  1.3102 +    }
  1.3103 +
  1.3104 +    response.complete();
  1.3105 +  },
  1.3106 +
  1.3107 +  // FIELDS
  1.3108 +
  1.3109 +  /**
  1.3110 +* This object contains the default handlers for the various HTTP error codes.
  1.3111 +*/
  1.3112 +  _defaultErrors:
  1.3113 +  {
  1.3114 +    400: function(metadata, response)
  1.3115 +    {
  1.3116 +      // none of the data in metadata is reliable, so hard-code everything here
  1.3117 +      response.setStatusLine("1.1", 400, "Bad Request");
  1.3118 +      response.setHeader("Content-Type", "text/plain", false);
  1.3119 +
  1.3120 +      var body = "Bad request\n";
  1.3121 +      response.bodyOutputStream.write(body, body.length);
  1.3122 +    },
  1.3123 +    403: function(metadata, response)
  1.3124 +    {
  1.3125 +      response.setStatusLine(metadata.httpVersion, 403, "Forbidden");
  1.3126 +      response.setHeader("Content-Type", "text/html", false);
  1.3127 +
  1.3128 +      var body = "<html>\
  1.3129 +<head><title>403 Forbidden</title></head>\
  1.3130 +<body>\
  1.3131 +<h1>403 Forbidden</h1>\
  1.3132 +</body>\
  1.3133 +</html>";
  1.3134 +      response.bodyOutputStream.write(body, body.length);
  1.3135 +    },
  1.3136 +    404: function(metadata, response)
  1.3137 +    {
  1.3138 +      response.setStatusLine(metadata.httpVersion, 404, "Not Found");
  1.3139 +      response.setHeader("Content-Type", "text/html", false);
  1.3140 +
  1.3141 +      var body = "<html>\
  1.3142 +<head><title>404 Not Found</title></head>\
  1.3143 +<body>\
  1.3144 +<h1>404 Not Found</h1>\
  1.3145 +<p>\
  1.3146 +<span style='font-family: monospace;'>" +
  1.3147 +                          htmlEscape(metadata.path) +
  1.3148 +                       "</span> was not found.\
  1.3149 +</p>\
  1.3150 +</body>\
  1.3151 +</html>";
  1.3152 +      response.bodyOutputStream.write(body, body.length);
  1.3153 +    },
  1.3154 +    416: function(metadata, response)
  1.3155 +    {
  1.3156 +      response.setStatusLine(metadata.httpVersion,
  1.3157 +                            416,
  1.3158 +                            "Requested Range Not Satisfiable");
  1.3159 +      response.setHeader("Content-Type", "text/html", false);
  1.3160 +
  1.3161 +      var body = "<html>\
  1.3162 +<head>\
  1.3163 +<title>416 Requested Range Not Satisfiable</title></head>\
  1.3164 +<body>\
  1.3165 +<h1>416 Requested Range Not Satisfiable</h1>\
  1.3166 +<p>The byte range was not valid for the\
  1.3167 +requested resource.\
  1.3168 +</p>\
  1.3169 +</body>\
  1.3170 +</html>";
  1.3171 +      response.bodyOutputStream.write(body, body.length);
  1.3172 +    },
  1.3173 +    500: function(metadata, response)
  1.3174 +    {
  1.3175 +      response.setStatusLine(metadata.httpVersion,
  1.3176 +                             500,
  1.3177 +                             "Internal Server Error");
  1.3178 +      response.setHeader("Content-Type", "text/html", false);
  1.3179 +
  1.3180 +      var body = "<html>\
  1.3181 +<head><title>500 Internal Server Error</title></head>\
  1.3182 +<body>\
  1.3183 +<h1>500 Internal Server Error</h1>\
  1.3184 +<p>Something's broken in this server and\
  1.3185 +needs to be fixed.</p>\
  1.3186 +</body>\
  1.3187 +</html>";
  1.3188 +      response.bodyOutputStream.write(body, body.length);
  1.3189 +    },
  1.3190 +    501: function(metadata, response)
  1.3191 +    {
  1.3192 +      response.setStatusLine(metadata.httpVersion, 501, "Not Implemented");
  1.3193 +      response.setHeader("Content-Type", "text/html", false);
  1.3194 +
  1.3195 +      var body = "<html>\
  1.3196 +<head><title>501 Not Implemented</title></head>\
  1.3197 +<body>\
  1.3198 +<h1>501 Not Implemented</h1>\
  1.3199 +<p>This server is not (yet) Apache.</p>\
  1.3200 +</body>\
  1.3201 +</html>";
  1.3202 +      response.bodyOutputStream.write(body, body.length);
  1.3203 +    },
  1.3204 +    505: function(metadata, response)
  1.3205 +    {
  1.3206 +      response.setStatusLine("1.1", 505, "HTTP Version Not Supported");
  1.3207 +      response.setHeader("Content-Type", "text/html", false);
  1.3208 +
  1.3209 +      var body = "<html>\
  1.3210 +<head><title>505 HTTP Version Not Supported</title></head>\
  1.3211 +<body>\
  1.3212 +<h1>505 HTTP Version Not Supported</h1>\
  1.3213 +<p>This server only supports HTTP/1.0 and HTTP/1.1\
  1.3214 +connections.</p>\
  1.3215 +</body>\
  1.3216 +</html>";
  1.3217 +      response.bodyOutputStream.write(body, body.length);
  1.3218 +    }
  1.3219 +  },
  1.3220 +
  1.3221 +  /**
  1.3222 +* Contains handlers for the default set of URIs contained in this server.
  1.3223 +*/
  1.3224 +  _defaultPaths:
  1.3225 +  {
  1.3226 +    "/": function(metadata, response)
  1.3227 +    {
  1.3228 +      response.setStatusLine(metadata.httpVersion, 200, "OK");
  1.3229 +      response.setHeader("Content-Type", "text/html", false);
  1.3230 +
  1.3231 +      var body = "<html>\
  1.3232 +<head><title>httpd.js</title></head>\
  1.3233 +<body>\
  1.3234 +<h1>httpd.js</h1>\
  1.3235 +<p>If you're seeing this page, httpd.js is up and\
  1.3236 +serving requests! Now set a base path and serve some\
  1.3237 +files!</p>\
  1.3238 +</body>\
  1.3239 +</html>";
  1.3240 +
  1.3241 +      response.bodyOutputStream.write(body, body.length);
  1.3242 +    },
  1.3243 +
  1.3244 +    "/trace": function(metadata, response)
  1.3245 +    {
  1.3246 +      response.setStatusLine(metadata.httpVersion, 200, "OK");
  1.3247 +      response.setHeader("Content-Type", "text/plain", false);
  1.3248 +
  1.3249 +      var body = "Request-URI: " +
  1.3250 +                 metadata.scheme + "://" + metadata.host + ":" + metadata.port +
  1.3251 +                 metadata.path + "\n\n";
  1.3252 +      body += "Request (semantically equivalent, slightly reformatted):\n\n";
  1.3253 +      body += metadata.method + " " + metadata.path;
  1.3254 +
  1.3255 +      if (metadata.queryString)
  1.3256 +        body += "?" + metadata.queryString;
  1.3257 +        
  1.3258 +      body += " HTTP/" + metadata.httpVersion + "\r\n";
  1.3259 +
  1.3260 +      var headEnum = metadata.headers;
  1.3261 +      while (headEnum.hasMoreElements())
  1.3262 +      {
  1.3263 +        var fieldName = headEnum.getNext()
  1.3264 +                                .QueryInterface(Ci.nsISupportsString)
  1.3265 +                                .data;
  1.3266 +        body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n";
  1.3267 +      }
  1.3268 +
  1.3269 +      response.bodyOutputStream.write(body, body.length);
  1.3270 +    }
  1.3271 +  }
  1.3272 +};
  1.3273 +
  1.3274 +
  1.3275 +/**
  1.3276 +* Maps absolute paths to files on the local file system (as nsILocalFiles).
  1.3277 +*/
  1.3278 +function FileMap()
  1.3279 +{
  1.3280 +  /** Hash which will map paths to nsILocalFiles. */
  1.3281 +  this._map = {};
  1.3282 +}
  1.3283 +FileMap.prototype =
  1.3284 +{
  1.3285 +  // PUBLIC API
  1.3286 +
  1.3287 +  /**
  1.3288 +* Maps key to a clone of the nsILocalFile value if value is non-null;
  1.3289 +* otherwise, removes any extant mapping for key.
  1.3290 +*
  1.3291 +* @param key : string
  1.3292 +* string to which a clone of value is mapped
  1.3293 +* @param value : nsILocalFile
  1.3294 +* the file to map to key, or null to remove a mapping
  1.3295 +*/
  1.3296 +  put: function(key, value)
  1.3297 +  {
  1.3298 +    if (value)
  1.3299 +      this._map[key] = value.clone();
  1.3300 +    else
  1.3301 +      delete this._map[key];
  1.3302 +  },
  1.3303 +
  1.3304 +  /**
  1.3305 +* Returns a clone of the nsILocalFile mapped to key, or null if no such
  1.3306 +* mapping exists.
  1.3307 +*
  1.3308 +* @param key : string
  1.3309 +* key to which the returned file maps
  1.3310 +* @returns nsILocalFile
  1.3311 +* a clone of the mapped file, or null if no mapping exists
  1.3312 +*/
  1.3313 +  get: function(key)
  1.3314 +  {
  1.3315 +    var val = this._map[key];
  1.3316 +    return val ? val.clone() : null;
  1.3317 +  }
  1.3318 +};
  1.3319 +
  1.3320 +
  1.3321 +// Response CONSTANTS
  1.3322 +
  1.3323 +// token = *<any CHAR except CTLs or separators>
  1.3324 +// CHAR = <any US-ASCII character (0-127)>
  1.3325 +// CTL = <any US-ASCII control character (0-31) and DEL (127)>
  1.3326 +// separators = "(" | ")" | "<" | ">" | "@"
  1.3327 +// | "," | ";" | ":" | "\" | <">
  1.3328 +// | "/" | "[" | "]" | "?" | "="
  1.3329 +// | "{" | "}" | SP | HT
  1.3330 +const IS_TOKEN_ARRAY =
  1.3331 +  [0, 0, 0, 0, 0, 0, 0, 0, // 0
  1.3332 +   0, 0, 0, 0, 0, 0, 0, 0, // 8
  1.3333 +   0, 0, 0, 0, 0, 0, 0, 0, // 16
  1.3334 +   0, 0, 0, 0, 0, 0, 0, 0, // 24
  1.3335 +
  1.3336 +   0, 1, 0, 1, 1, 1, 1, 1, // 32
  1.3337 +   0, 0, 1, 1, 0, 1, 1, 0, // 40
  1.3338 +   1, 1, 1, 1, 1, 1, 1, 1, // 48
  1.3339 +   1, 1, 0, 0, 0, 0, 0, 0, // 56
  1.3340 +
  1.3341 +   0, 1, 1, 1, 1, 1, 1, 1, // 64
  1.3342 +   1, 1, 1, 1, 1, 1, 1, 1, // 72
  1.3343 +   1, 1, 1, 1, 1, 1, 1, 1, // 80
  1.3344 +   1, 1, 1, 0, 0, 0, 1, 1, // 88
  1.3345 +
  1.3346 +   1, 1, 1, 1, 1, 1, 1, 1, // 96
  1.3347 +   1, 1, 1, 1, 1, 1, 1, 1, // 104
  1.3348 +   1, 1, 1, 1, 1, 1, 1, 1, // 112
  1.3349 +   1, 1, 1, 0, 1, 0, 1]; // 120
  1.3350 +
  1.3351 +
  1.3352 +/**
  1.3353 +* Determines whether the given character code is a CTL.
  1.3354 +*
  1.3355 +* @param code : uint
  1.3356 +* the character code
  1.3357 +* @returns boolean
  1.3358 +* true if code is a CTL, false otherwise
  1.3359 +*/
  1.3360 +function isCTL(code)
  1.3361 +{
  1.3362 +  return (code >= 0 && code <= 31) || (code == 127);
  1.3363 +}
  1.3364 +
  1.3365 +/**
  1.3366 +* Represents a response to an HTTP request, encapsulating all details of that
  1.3367 +* response. This includes all headers, the HTTP version, status code and
  1.3368 +* explanation, and the entity itself.
  1.3369 +*
  1.3370 +* @param connection : Connection
  1.3371 +* the connection over which this response is to be written
  1.3372 +*/
  1.3373 +function Response(connection)
  1.3374 +{
  1.3375 +  /** The connection over which this response will be written. */
  1.3376 +  this._connection = connection;
  1.3377 +
  1.3378 +  /**
  1.3379 +* The HTTP version of this response; defaults to 1.1 if not set by the
  1.3380 +* handler.
  1.3381 +*/
  1.3382 +  this._httpVersion = nsHttpVersion.HTTP_1_1;
  1.3383 +
  1.3384 +  /**
  1.3385 +* The HTTP code of this response; defaults to 200.
  1.3386 +*/
  1.3387 +  this._httpCode = 200;
  1.3388 +
  1.3389 +  /**
  1.3390 +* The description of the HTTP code in this response; defaults to "OK".
  1.3391 +*/
  1.3392 +  this._httpDescription = "OK";
  1.3393 +
  1.3394 +  /**
  1.3395 +* An nsIHttpHeaders object in which the headers in this response should be
  1.3396 +* stored. This property is null after the status line and headers have been
  1.3397 +* written to the network, and it may be modified up until it is cleared,
  1.3398 +* except if this._finished is set first (in which case headers are written
  1.3399 +* asynchronously in response to a finish() call not preceded by
  1.3400 +* flushHeaders()).
  1.3401 +*/
  1.3402 +  this._headers = new nsHttpHeaders();
  1.3403 +
  1.3404 +  /**
  1.3405 +* Set to true when this response is ended (completely constructed if possible
  1.3406 +* and the connection closed); further actions on this will then fail.
  1.3407 +*/
  1.3408 +  this._ended = false;
  1.3409 +
  1.3410 +  /**
  1.3411 +* A stream used to hold data written to the body of this response.
  1.3412 +*/
  1.3413 +  this._bodyOutputStream = null;
  1.3414 +
  1.3415 +  /**
  1.3416 +* A stream containing all data that has been written to the body of this
  1.3417 +* response so far. (Async handlers make the data contained in this
  1.3418 +* unreliable as a way of determining content length in general, but auxiliary
  1.3419 +* saved information can sometimes be used to guarantee reliability.)
  1.3420 +*/
  1.3421 +  this._bodyInputStream = null;
  1.3422 +
  1.3423 +  /**
  1.3424 +* A stream copier which copies data to the network. It is initially null
  1.3425 +* until replaced with a copier for response headers; when headers have been
  1.3426 +* fully sent it is replaced with a copier for the response body, remaining
  1.3427 +* so for the duration of response processing.
  1.3428 +*/
  1.3429 +  this._asyncCopier = null;
  1.3430 +
  1.3431 +  /**
  1.3432 +* True if this response has been designated as being processed
  1.3433 +* asynchronously rather than for the duration of a single call to
  1.3434 +* nsIHttpRequestHandler.handle.
  1.3435 +*/
  1.3436 +  this._processAsync = false;
  1.3437 +
  1.3438 +  /**
  1.3439 +* True iff finish() has been called on this, signaling that no more changes
  1.3440 +* to this may be made.
  1.3441 +*/
  1.3442 +  this._finished = false;
  1.3443 +
  1.3444 +  /**
  1.3445 +* True iff powerSeized() has been called on this, signaling that this
  1.3446 +* response is to be handled manually by the response handler (which may then
  1.3447 +* send arbitrary data in response, even non-HTTP responses).
  1.3448 +*/
  1.3449 +  this._powerSeized = false;
  1.3450 +}
  1.3451 +Response.prototype =
  1.3452 +{
  1.3453 +  // PUBLIC CONSTRUCTION API
  1.3454 +
  1.3455 +  //
  1.3456 +  // see nsIHttpResponse.bodyOutputStream
  1.3457 +  //
  1.3458 +  get bodyOutputStream()
  1.3459 +  {
  1.3460 +    if (this._finished)
  1.3461 +      throw Cr.NS_ERROR_NOT_AVAILABLE;
  1.3462 +
  1.3463 +    if (!this._bodyOutputStream)
  1.3464 +    {
  1.3465 +      var pipe = new Pipe(true, false, Response.SEGMENT_SIZE, PR_UINT32_MAX,
  1.3466 +                          null);
  1.3467 +      this._bodyOutputStream = pipe.outputStream;
  1.3468 +      this._bodyInputStream = pipe.inputStream;
  1.3469 +      if (this._processAsync || this._powerSeized)
  1.3470 +        this._startAsyncProcessor();
  1.3471 +    }
  1.3472 +
  1.3473 +    return this._bodyOutputStream;
  1.3474 +  },
  1.3475 +
  1.3476 +  //
  1.3477 +  // see nsIHttpResponse.write
  1.3478 +  //
  1.3479 +  write: function(data)
  1.3480 +  {
  1.3481 +    if (this._finished)
  1.3482 +      throw Cr.NS_ERROR_NOT_AVAILABLE;
  1.3483 +
  1.3484 +    var dataAsString = String(data);
  1.3485 +    this.bodyOutputStream.write(dataAsString, dataAsString.length);
  1.3486 +  },
  1.3487 +
  1.3488 +  //
  1.3489 +  // see nsIHttpResponse.setStatusLine
  1.3490 +  //
  1.3491 +  setStatusLine: function(httpVersion, code, description)
  1.3492 +  {
  1.3493 +    if (!this._headers || this._finished || this._powerSeized)
  1.3494 +      throw Cr.NS_ERROR_NOT_AVAILABLE;
  1.3495 +    this._ensureAlive();
  1.3496 +
  1.3497 +    if (!(code >= 0 && code < 1000))
  1.3498 +      throw Cr.NS_ERROR_INVALID_ARG;
  1.3499 +
  1.3500 +    try
  1.3501 +    {
  1.3502 +      var httpVer;
  1.3503 +      // avoid version construction for the most common cases
  1.3504 +      if (!httpVersion || httpVersion == "1.1")
  1.3505 +        httpVer = nsHttpVersion.HTTP_1_1;
  1.3506 +      else if (httpVersion == "1.0")
  1.3507 +        httpVer = nsHttpVersion.HTTP_1_0;
  1.3508 +      else
  1.3509 +        httpVer = new nsHttpVersion(httpVersion);
  1.3510 +    }
  1.3511 +    catch (e)
  1.3512 +    {
  1.3513 +      throw Cr.NS_ERROR_INVALID_ARG;
  1.3514 +    }
  1.3515 +
  1.3516 +    // Reason-Phrase = *<TEXT, excluding CR, LF>
  1.3517 +    // TEXT = <any OCTET except CTLs, but including LWS>
  1.3518 +    //
  1.3519 +    // XXX this ends up disallowing octets which aren't Unicode, I think -- not
  1.3520 +    // much to do if description is IDL'd as string
  1.3521 +    if (!description)
  1.3522 +      description = "";
  1.3523 +    for (var i = 0; i < description.length; i++)
  1.3524 +      if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t")
  1.3525 +        throw Cr.NS_ERROR_INVALID_ARG;
  1.3526 +
  1.3527 +    // set the values only after validation to preserve atomicity
  1.3528 +    this._httpDescription = description;
  1.3529 +    this._httpCode = code;
  1.3530 +    this._httpVersion = httpVer;
  1.3531 +  },
  1.3532 +
  1.3533 +  //
  1.3534 +  // see nsIHttpResponse.setHeader
  1.3535 +  //
  1.3536 +  setHeader: function(name, value, merge)
  1.3537 +  {
  1.3538 +    if (!this._headers || this._finished || this._powerSeized)
  1.3539 +      throw Cr.NS_ERROR_NOT_AVAILABLE;
  1.3540 +    this._ensureAlive();
  1.3541 +
  1.3542 +    this._headers.setHeader(name, value, merge);
  1.3543 +  },
  1.3544 +
  1.3545 +  //
  1.3546 +  // see nsIHttpResponse.processAsync
  1.3547 +  //
  1.3548 +  processAsync: function()
  1.3549 +  {
  1.3550 +    if (this._finished)
  1.3551 +      throw Cr.NS_ERROR_UNEXPECTED;
  1.3552 +    if (this._powerSeized)
  1.3553 +      throw Cr.NS_ERROR_NOT_AVAILABLE;
  1.3554 +    if (this._processAsync)
  1.3555 +      return;
  1.3556 +    this._ensureAlive();
  1.3557 +
  1.3558 +    dumpn("*** processing connection " + this._connection.number + " async");
  1.3559 +    this._processAsync = true;
  1.3560 +
  1.3561 +    /*
  1.3562 +* Either the bodyOutputStream getter or this method is responsible for
  1.3563 +* starting the asynchronous processor and catching writes of data to the
  1.3564 +* response body of async responses as they happen, for the purpose of
  1.3565 +* forwarding those writes to the actual connection's output stream.
  1.3566 +* If bodyOutputStream is accessed first, calling this method will create
  1.3567 +* the processor (when it first is clear that body data is to be written
  1.3568 +* immediately, not buffered). If this method is called first, accessing
  1.3569 +* bodyOutputStream will create the processor. If only this method is
  1.3570 +* called, we'll write nothing, neither headers nor the nonexistent body,
  1.3571 +* until finish() is called. Since that delay is easily avoided by simply
  1.3572 +* getting bodyOutputStream or calling write(""), we don't worry about it.
  1.3573 +*/
  1.3574 +    if (this._bodyOutputStream && !this._asyncCopier)
  1.3575 +      this._startAsyncProcessor();
  1.3576 +  },
  1.3577 +
  1.3578 +  //
  1.3579 +  // see nsIHttpResponse.seizePower
  1.3580 +  //
  1.3581 +  seizePower: function()
  1.3582 +  {
  1.3583 +    if (this._processAsync)
  1.3584 +      throw Cr.NS_ERROR_NOT_AVAILABLE;
  1.3585 +    if (this._finished)
  1.3586 +      throw Cr.NS_ERROR_UNEXPECTED;
  1.3587 +    if (this._powerSeized)
  1.3588 +      return;
  1.3589 +    this._ensureAlive();
  1.3590 +
  1.3591 +    dumpn("*** forcefully seizing power over connection " +
  1.3592 +          this._connection.number + "...");
  1.3593 +
  1.3594 +    // Purge any already-written data without sending it. We could as easily
  1.3595 +    // swap out the streams entirely, but that makes it possible to acquire and
  1.3596 +    // unknowingly use a stale reference, so we require there only be one of
  1.3597 +    // each stream ever for any response to avoid this complication.
  1.3598 +    if (this._asyncCopier)
  1.3599 +      this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED);
  1.3600 +    this._asyncCopier = null;
  1.3601 +    if (this._bodyOutputStream)
  1.3602 +    {
  1.3603 +      var input = new BinaryInputStream(this._bodyInputStream);
  1.3604 +      var avail;
  1.3605 +      while ((avail = input.available()) > 0)
  1.3606 +        input.readByteArray(avail);
  1.3607 +    }
  1.3608 +
  1.3609 +    this._powerSeized = true;
  1.3610 +    if (this._bodyOutputStream)
  1.3611 +      this._startAsyncProcessor();
  1.3612 +  },
  1.3613 +
  1.3614 +  //
  1.3615 +  // see nsIHttpResponse.finish
  1.3616 +  //
  1.3617 +  finish: function()
  1.3618 +  {
  1.3619 +    if (!this._processAsync && !this._powerSeized)
  1.3620 +      throw Cr.NS_ERROR_UNEXPECTED;
  1.3621 +    if (this._finished)
  1.3622 +      return;
  1.3623 +
  1.3624 +    dumpn("*** finishing connection " + this._connection.number);
  1.3625 +    this._startAsyncProcessor(); // in case bodyOutputStream was never accessed
  1.3626 +    if (this._bodyOutputStream)
  1.3627 +      this._bodyOutputStream.close();
  1.3628 +    this._finished = true;
  1.3629 +  },
  1.3630 +
  1.3631 +
  1.3632 +  // NSISUPPORTS
  1.3633 +
  1.3634 +  //
  1.3635 +  // see nsISupports.QueryInterface
  1.3636 +  //
  1.3637 +  QueryInterface: function(iid)
  1.3638 +  {
  1.3639 +    if (iid.equals(Ci.nsIHttpResponse) || iid.equals(Ci.nsISupports))
  1.3640 +      return this;
  1.3641 +
  1.3642 +    throw Cr.NS_ERROR_NO_INTERFACE;
  1.3643 +  },
  1.3644 +
  1.3645 +
  1.3646 +  // POST-CONSTRUCTION API (not exposed externally)
  1.3647 +
  1.3648 +  /**
  1.3649 +* The HTTP version number of this, as a string (e.g. "1.1").
  1.3650 +*/
  1.3651 +  get httpVersion()
  1.3652 +  {
  1.3653 +    this._ensureAlive();
  1.3654 +    return this._httpVersion.toString();
  1.3655 +  },
  1.3656 +
  1.3657 +  /**
  1.3658 +* The HTTP status code of this response, as a string of three characters per
  1.3659 +* RFC 2616.
  1.3660 +*/
  1.3661 +  get httpCode()
  1.3662 +  {
  1.3663 +    this._ensureAlive();
  1.3664 +
  1.3665 +    var codeString = (this._httpCode < 10 ? "0" : "") +
  1.3666 +                     (this._httpCode < 100 ? "0" : "") +
  1.3667 +                     this._httpCode;
  1.3668 +    return codeString;
  1.3669 +  },
  1.3670 +
  1.3671 +  /**
  1.3672 +* The description of the HTTP status code of this response, or "" if none is
  1.3673 +* set.
  1.3674 +*/
  1.3675 +  get httpDescription()
  1.3676 +  {
  1.3677 +    this._ensureAlive();
  1.3678 +
  1.3679 +    return this._httpDescription;
  1.3680 +  },
  1.3681 +
  1.3682 +  /**
  1.3683 +* The headers in this response, as an nsHttpHeaders object.
  1.3684 +*/
  1.3685 +  get headers()
  1.3686 +  {
  1.3687 +    this._ensureAlive();
  1.3688 +
  1.3689 +    return this._headers;
  1.3690 +  },
  1.3691 +
  1.3692 +  //
  1.3693 +  // see nsHttpHeaders.getHeader
  1.3694 +  //
  1.3695 +  getHeader: function(name)
  1.3696 +  {
  1.3697 +    this._ensureAlive();
  1.3698 +
  1.3699 +    return this._headers.getHeader(name);
  1.3700 +  },
  1.3701 +
  1.3702 +  /**
  1.3703 +* Determines whether this response may be abandoned in favor of a newly
  1.3704 +* constructed response. A response may be abandoned only if it is not being
  1.3705 +* sent asynchronously and if raw control over it has not been taken from the
  1.3706 +* server.
  1.3707 +*
  1.3708 +* @returns boolean
  1.3709 +* true iff no data has been written to the network
  1.3710 +*/
  1.3711 +  partiallySent: function()
  1.3712 +  {
  1.3713 +    dumpn("*** partiallySent()");
  1.3714 +    return this._processAsync || this._powerSeized;
  1.3715 +  },
  1.3716 +
  1.3717 +  /**
  1.3718 +* If necessary, kicks off the remaining request processing needed to be done
  1.3719 +* after a request handler performs its initial work upon this response.
  1.3720 +*/
  1.3721 +  complete: function()
  1.3722 +  {
  1.3723 +    dumpn("*** complete()");
  1.3724 +    if (this._processAsync || this._powerSeized)
  1.3725 +    {
  1.3726 +      NS_ASSERT(this._processAsync ^ this._powerSeized,
  1.3727 +                "can't both send async and relinquish power");
  1.3728 +      return;
  1.3729 +    }
  1.3730 +
  1.3731 +    NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?");
  1.3732 +
  1.3733 +    this._startAsyncProcessor();
  1.3734 +
  1.3735 +    // Now make sure we finish processing this request!
  1.3736 +    if (this._bodyOutputStream)
  1.3737 +      this._bodyOutputStream.close();
  1.3738 +  },
  1.3739 +
  1.3740 +  /**
  1.3741 +* Abruptly ends processing of this response, usually due to an error in an
  1.3742 +* incoming request but potentially due to a bad error handler. Since we
  1.3743 +* cannot handle the error in the usual way (giving an HTTP error page in
  1.3744 +* response) because data may already have been sent (or because the response
  1.3745 +* might be expected to have been generated asynchronously or completely from
  1.3746 +* scratch by the handler), we stop processing this response and abruptly
  1.3747 +* close the connection.
  1.3748 +*
  1.3749 +* @param e : Error
  1.3750 +* the exception which precipitated this abort, or null if no such exception
  1.3751 +* was generated
  1.3752 +*/
  1.3753 +  abort: function(e)
  1.3754 +  {
  1.3755 +    dumpn("*** abort(<" + e + ">)");
  1.3756 +
  1.3757 +    // This response will be ended by the processor if one was created.
  1.3758 +    var copier = this._asyncCopier;
  1.3759 +    if (copier)
  1.3760 +    {
  1.3761 +      // We dispatch asynchronously here so that any pending writes of data to
  1.3762 +      // the connection will be deterministically written. This makes it easier
  1.3763 +      // to specify exact behavior, and it makes observable behavior more
  1.3764 +      // predictable for clients. Note that the correctness of this depends on
  1.3765 +      // callbacks in response to _waitToReadData in WriteThroughCopier
  1.3766 +      // happening asynchronously with respect to the actual writing of data to
  1.3767 +      // bodyOutputStream, as they currently do; if they happened synchronously,
  1.3768 +      // an event which ran before this one could write more data to the
  1.3769 +      // response body before we get around to canceling the copier. We have
  1.3770 +      // tests for this in test_seizepower.js, however, and I can't think of a
  1.3771 +      // way to handle both cases without removing bodyOutputStream access and
  1.3772 +      // moving its effective write(data, length) method onto Response, which
  1.3773 +      // would be slower and require more code than this anyway.
  1.3774 +      gThreadManager.currentThread.dispatch({
  1.3775 +        run: function()
  1.3776 +        {
  1.3777 +          dumpn("*** canceling copy asynchronously...");
  1.3778 +          copier.cancel(Cr.NS_ERROR_UNEXPECTED);
  1.3779 +        }
  1.3780 +      }, Ci.nsIThread.DISPATCH_NORMAL);
  1.3781 +    }
  1.3782 +    else
  1.3783 +    {
  1.3784 +      this.end();
  1.3785 +    }
  1.3786 +  },
  1.3787 +
  1.3788 +  /**
  1.3789 +* Closes this response's network connection, marks the response as finished,
  1.3790 +* and notifies the server handler that the request is done being processed.
  1.3791 +*/
  1.3792 +  end: function()
  1.3793 +  {
  1.3794 +    NS_ASSERT(!this._ended, "ending this response twice?!?!");
  1.3795 +
  1.3796 +    this._connection.close();
  1.3797 +    if (this._bodyOutputStream)
  1.3798 +      this._bodyOutputStream.close();
  1.3799 +
  1.3800 +    this._finished = true;
  1.3801 +    this._ended = true;
  1.3802 +  },
  1.3803 +
  1.3804 +  // PRIVATE IMPLEMENTATION
  1.3805 +
  1.3806 +  /**
  1.3807 +* Sends the status line and headers of this response if they haven't been
  1.3808 +* sent and initiates the process of copying data written to this response's
  1.3809 +* body to the network.
  1.3810 +*/
  1.3811 +  _startAsyncProcessor: function()
  1.3812 +  {
  1.3813 +    dumpn("*** _startAsyncProcessor()");
  1.3814 +
  1.3815 +    // Handle cases where we're being called a second time. The former case
  1.3816 +    // happens when this is triggered both by complete() and by processAsync(),
  1.3817 +    // while the latter happens when processAsync() in conjunction with sent
  1.3818 +    // data causes abort() to be called.
  1.3819 +    if (this._asyncCopier || this._ended)
  1.3820 +    {
  1.3821 +      dumpn("*** ignoring second call to _startAsyncProcessor");
  1.3822 +      return;
  1.3823 +    }
  1.3824 +
  1.3825 +    // Send headers if they haven't been sent already and should be sent, then
  1.3826 +    // asynchronously continue to send the body.
  1.3827 +    if (this._headers && !this._powerSeized)
  1.3828 +    {
  1.3829 +      this._sendHeaders();
  1.3830 +      return;
  1.3831 +    }
  1.3832 +
  1.3833 +    this._headers = null;
  1.3834 +    this._sendBody();
  1.3835 +  },
  1.3836 +
  1.3837 +  /**
  1.3838 +* Signals that all modifications to the response status line and headers are
  1.3839 +* complete and then sends that data over the network to the client. Once
  1.3840 +* this method completes, a different response to the request that resulted
  1.3841 +* in this response cannot be sent -- the only possible action in case of
  1.3842 +* error is to abort the response and close the connection.
  1.3843 +*/
  1.3844 +  _sendHeaders: function()
  1.3845 +  {
  1.3846 +    dumpn("*** _sendHeaders()");
  1.3847 +
  1.3848 +    NS_ASSERT(this._headers);
  1.3849 +    NS_ASSERT(!this._powerSeized);
  1.3850 +
  1.3851 +    // request-line
  1.3852 +    var statusLine = "HTTP/" + this.httpVersion + " " +
  1.3853 +                     this.httpCode + " " +
  1.3854 +                     this.httpDescription + "\r\n";
  1.3855 +
  1.3856 +    // header post-processing
  1.3857 +
  1.3858 +    var headers = this._headers;
  1.3859 +    headers.setHeader("Connection", "close", false);
  1.3860 +    headers.setHeader("Server", "httpd.js", false);
  1.3861 +    if (!headers.hasHeader("Date"))
  1.3862 +      headers.setHeader("Date", toDateString(Date.now()), false);
  1.3863 +
  1.3864 +    // Any response not being processed asynchronously must have an associated
  1.3865 +    // Content-Length header for reasons of backwards compatibility with the
  1.3866 +    // initial server, which fully buffered every response before sending it.
  1.3867 +    // Beyond that, however, it's good to do this anyway because otherwise it's
  1.3868 +    // impossible to test behaviors that depend on the presence or absence of a
  1.3869 +    // Content-Length header.
  1.3870 +    if (!this._processAsync)
  1.3871 +    {
  1.3872 +      dumpn("*** non-async response, set Content-Length");
  1.3873 +
  1.3874 +      var bodyStream = this._bodyInputStream;
  1.3875 +      var avail = bodyStream ? bodyStream.available() : 0;
  1.3876 +
  1.3877 +      // XXX assumes stream will always report the full amount of data available
  1.3878 +      headers.setHeader("Content-Length", "" + avail, false);
  1.3879 +    }
  1.3880 +
  1.3881 +
  1.3882 +    // construct and send response
  1.3883 +    dumpn("*** header post-processing completed, sending response head...");
  1.3884 +
  1.3885 +    // request-line
  1.3886 +    var preambleData = [statusLine];
  1.3887 +
  1.3888 +    // headers
  1.3889 +    var headEnum = headers.enumerator;
  1.3890 +    while (headEnum.hasMoreElements())
  1.3891 +    {
  1.3892 +      var fieldName = headEnum.getNext()
  1.3893 +                              .QueryInterface(Ci.nsISupportsString)
  1.3894 +                              .data;
  1.3895 +      var values = headers.getHeaderValues(fieldName);
  1.3896 +      for (var i = 0, sz = values.length; i < sz; i++)
  1.3897 +        preambleData.push(fieldName + ": " + values[i] + "\r\n");
  1.3898 +    }
  1.3899 +
  1.3900 +    // end request-line/headers
  1.3901 +    preambleData.push("\r\n");
  1.3902 +
  1.3903 +    var preamble = preambleData.join("");
  1.3904 +
  1.3905 +    var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null);
  1.3906 +    responseHeadPipe.outputStream.write(preamble, preamble.length);
  1.3907 +
  1.3908 +    var response = this;
  1.3909 +    var copyObserver =
  1.3910 +      {
  1.3911 +        onStartRequest: function(request, cx)
  1.3912 +        {
  1.3913 +          dumpn("*** preamble copying started");
  1.3914 +        },
  1.3915 +
  1.3916 +        onStopRequest: function(request, cx, statusCode)
  1.3917 +        {
  1.3918 +          dumpn("*** preamble copying complete " +
  1.3919 +                "[status=0x" + statusCode.toString(16) + "]");
  1.3920 +
  1.3921 +          if (!components.isSuccessCode(statusCode))
  1.3922 +          {
  1.3923 +            dumpn("!!! header copying problems: non-success statusCode, " +
  1.3924 +                  "ending response");
  1.3925 +
  1.3926 +            response.end();
  1.3927 +          }
  1.3928 +          else
  1.3929 +          {
  1.3930 +            response._sendBody();
  1.3931 +          }
  1.3932 +        },
  1.3933 +
  1.3934 +        QueryInterface: function(aIID)
  1.3935 +        {
  1.3936 +          if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports))
  1.3937 +            return this;
  1.3938 +
  1.3939 +          throw Cr.NS_ERROR_NO_INTERFACE;
  1.3940 +        }
  1.3941 +      };
  1.3942 +
  1.3943 +    var headerCopier = this._asyncCopier =
  1.3944 +      new WriteThroughCopier(responseHeadPipe.inputStream,
  1.3945 +                             this._connection.output,
  1.3946 +                             copyObserver, null);
  1.3947 +
  1.3948 +    responseHeadPipe.outputStream.close();
  1.3949 +
  1.3950 +    // Forbid setting any more headers or modifying the request line.
  1.3951 +    this._headers = null;
  1.3952 +  },
  1.3953 +
  1.3954 +  /**
  1.3955 +* Asynchronously writes the body of the response (or the entire response, if
  1.3956 +* seizePower() has been called) to the network.
  1.3957 +*/
  1.3958 +  _sendBody: function()
  1.3959 +  {
  1.3960 +    dumpn("*** _sendBody");
  1.3961 +
  1.3962 +    NS_ASSERT(!this._headers, "still have headers around but sending body?");
  1.3963 +
  1.3964 +    // If no body data was written, we're done
  1.3965 +    if (!this._bodyInputStream)
  1.3966 +    {
  1.3967 +      dumpn("*** empty body, response finished");
  1.3968 +      this.end();
  1.3969 +      return;
  1.3970 +    }
  1.3971 +
  1.3972 +    var response = this;
  1.3973 +    var copyObserver =
  1.3974 +      {
  1.3975 +        onStartRequest: function(request, context)
  1.3976 +        {
  1.3977 +          dumpn("*** onStartRequest");
  1.3978 +        },
  1.3979 +
  1.3980 +        onStopRequest: function(request, cx, statusCode)
  1.3981 +        {
  1.3982 +          dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]");
  1.3983 +
  1.3984 +          if (statusCode === Cr.NS_BINDING_ABORTED)
  1.3985 +          {
  1.3986 +            dumpn("*** terminating copy observer without ending the response");
  1.3987 +          }
  1.3988 +          else
  1.3989 +          {
  1.3990 +            if (!components.isSuccessCode(statusCode))
  1.3991 +              dumpn("*** WARNING: non-success statusCode in onStopRequest");
  1.3992 +
  1.3993 +            response.end();
  1.3994 +          }
  1.3995 +        },
  1.3996 +
  1.3997 +        QueryInterface: function(aIID)
  1.3998 +        {
  1.3999 +          if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports))
  1.4000 +            return this;
  1.4001 +
  1.4002 +          throw Cr.NS_ERROR_NO_INTERFACE;
  1.4003 +        }
  1.4004 +      };
  1.4005 +
  1.4006 +    dumpn("*** starting async copier of body data...");
  1.4007 +    this._asyncCopier =
  1.4008 +      new WriteThroughCopier(this._bodyInputStream, this._connection.output,
  1.4009 +                            copyObserver, null);
  1.4010 +  },
  1.4011 +
  1.4012 +  /** Ensures that this hasn't been ended. */
  1.4013 +  _ensureAlive: function()
  1.4014 +  {
  1.4015 +    NS_ASSERT(!this._ended, "not handling response lifetime correctly");
  1.4016 +  }
  1.4017 +};
  1.4018 +
  1.4019 +/**
  1.4020 +* Size of the segments in the buffer used in storing response data and writing
  1.4021 +* it to the socket.
  1.4022 +*/
  1.4023 +Response.SEGMENT_SIZE = 8192;
  1.4024 +
  1.4025 +/** Serves double duty in WriteThroughCopier implementation. */
  1.4026 +function notImplemented()
  1.4027 +{
  1.4028 +  throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  1.4029 +}
  1.4030 +
  1.4031 +/** Returns true iff the given exception represents stream closure. */
  1.4032 +function streamClosed(e)
  1.4033 +{
  1.4034 +  return e === Cr.NS_BASE_STREAM_CLOSED ||
  1.4035 +         (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED);
  1.4036 +}
  1.4037 +
  1.4038 +/** Returns true iff the given exception represents a blocked stream. */
  1.4039 +function wouldBlock(e)
  1.4040 +{
  1.4041 +  return e === Cr.NS_BASE_STREAM_WOULD_BLOCK ||
  1.4042 +         (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK);
  1.4043 +}
  1.4044 +
  1.4045 +/**
  1.4046 +* Copies data from source to sink as it becomes available, when that data can
  1.4047 +* be written to sink without blocking.
  1.4048 +*
  1.4049 +* @param source : nsIAsyncInputStream
  1.4050 +* the stream from which data is to be read
  1.4051 +* @param sink : nsIAsyncOutputStream
  1.4052 +* the stream to which data is to be copied
  1.4053 +* @param observer : nsIRequestObserver
  1.4054 +* an observer which will be notified when the copy starts and finishes
  1.4055 +* @param context : nsISupports
  1.4056 +* context passed to observer when notified of start/stop
  1.4057 +* @throws NS_ERROR_NULL_POINTER
  1.4058 +* if source, sink, or observer are null
  1.4059 +*/
  1.4060 +function WriteThroughCopier(source, sink, observer, context)
  1.4061 +{
  1.4062 +  if (!source || !sink || !observer)
  1.4063 +    throw Cr.NS_ERROR_NULL_POINTER;
  1.4064 +
  1.4065 +  /** Stream from which data is being read. */
  1.4066 +  this._source = source;
  1.4067 +
  1.4068 +  /** Stream to which data is being written. */
  1.4069 +  this._sink = sink;
  1.4070 +
  1.4071 +  /** Observer watching this copy. */
  1.4072 +  this._observer = observer;
  1.4073 +
  1.4074 +  /** Context for the observer watching this. */
  1.4075 +  this._context = context;
  1.4076 +
  1.4077 +  /**
  1.4078 +* True iff this is currently being canceled (cancel has been called, the
  1.4079 +* callback may not yet have been made).
  1.4080 +*/
  1.4081 +  this._canceled = false;
  1.4082 +
  1.4083 +  /**
  1.4084 +* False until all data has been read from input and written to output, at
  1.4085 +* which point this copy is completed and cancel() is asynchronously called.
  1.4086 +*/
  1.4087 +  this._completed = false;
  1.4088 +
  1.4089 +  /** Required by nsIRequest, meaningless. */
  1.4090 +  this.loadFlags = 0;
  1.4091 +  /** Required by nsIRequest, meaningless. */
  1.4092 +  this.loadGroup = null;
  1.4093 +  /** Required by nsIRequest, meaningless. */
  1.4094 +  this.name = "response-body-copy";
  1.4095 +
  1.4096 +  /** Status of this request. */
  1.4097 +  this.status = Cr.NS_OK;
  1.4098 +
  1.4099 +  /** Arrays of byte strings waiting to be written to output. */
  1.4100 +  this._pendingData = [];
  1.4101 +
  1.4102 +  // start copying
  1.4103 +  try
  1.4104 +  {
  1.4105 +    observer.onStartRequest(this, context);
  1.4106 +    this._waitToReadData();
  1.4107 +    this._waitForSinkClosure();
  1.4108 +  }
  1.4109 +  catch (e)
  1.4110 +  {
  1.4111 +    dumpn("!!! error starting copy: " + e +
  1.4112 +          ("lineNumber" in e ? ", line " + e.lineNumber : ""));
  1.4113 +    dumpn(e.stack);
  1.4114 +    this.cancel(Cr.NS_ERROR_UNEXPECTED);
  1.4115 +  }
  1.4116 +}
  1.4117 +WriteThroughCopier.prototype =
  1.4118 +{
  1.4119 +  /* nsISupports implementation */
  1.4120 +
  1.4121 +  QueryInterface: function(iid)
  1.4122 +  {
  1.4123 +    if (iid.equals(Ci.nsIInputStreamCallback) ||
  1.4124 +        iid.equals(Ci.nsIOutputStreamCallback) ||
  1.4125 +        iid.equals(Ci.nsIRequest) ||
  1.4126 +        iid.equals(Ci.nsISupports))
  1.4127 +    {
  1.4128 +      return this;
  1.4129 +    }
  1.4130 +
  1.4131 +    throw Cr.NS_ERROR_NO_INTERFACE;
  1.4132 +  },
  1.4133 +
  1.4134 +
  1.4135 +  // NSIINPUTSTREAMCALLBACK
  1.4136 +
  1.4137 +  /**
  1.4138 +* Receives a more-data-in-input notification and writes the corresponding
  1.4139 +* data to the output.
  1.4140 +*
  1.4141 +* @param input : nsIAsyncInputStream
  1.4142 +* the input stream on whose data we have been waiting
  1.4143 +*/
  1.4144 +  onInputStreamReady: function(input)
  1.4145 +  {
  1.4146 +    if (this._source === null)
  1.4147 +      return;
  1.4148 +
  1.4149 +    dumpn("*** onInputStreamReady");
  1.4150 +
  1.4151 +    //
  1.4152 +    // Ordinarily we'll read a non-zero amount of data from input, queue it up
  1.4153 +    // to be written and then wait for further callbacks. The complications in
  1.4154 +    // this method are the cases where we deviate from that behavior when errors
  1.4155 +    // occur or when copying is drawing to a finish.
  1.4156 +    //
  1.4157 +    // The edge cases when reading data are:
  1.4158 +    //
  1.4159 +    // Zero data is read
  1.4160 +    // If zero data was read, we're at the end of available data, so we can
  1.4161 +    // should stop reading and move on to writing out what we have (or, if
  1.4162 +    // we've already done that, onto notifying of completion).
  1.4163 +    // A stream-closed exception is thrown
  1.4164 +    // This is effectively a less kind version of zero data being read; the
  1.4165 +    // only difference is that we notify of completion with that result
  1.4166 +    // rather than with NS_OK.
  1.4167 +    // Some other exception is thrown
  1.4168 +    // This is the least kind result. We don't know what happened, so we
  1.4169 +    // act as though the stream closed except that we notify of completion
  1.4170 +    // with the result NS_ERROR_UNEXPECTED.
  1.4171 +    //
  1.4172 +
  1.4173 +    var bytesWanted = 0, bytesConsumed = -1;
  1.4174 +    try
  1.4175 +    {
  1.4176 +      input = new BinaryInputStream(input);
  1.4177 +
  1.4178 +      bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE);
  1.4179 +      dumpn("*** input wanted: " + bytesWanted);
  1.4180 +
  1.4181 +      if (bytesWanted > 0)
  1.4182 +      {
  1.4183 +        var data = input.readByteArray(bytesWanted);
  1.4184 +        bytesConsumed = data.length;
  1.4185 +        this._pendingData.push(String.fromCharCode.apply(String, data));
  1.4186 +      }
  1.4187 +
  1.4188 +      dumpn("*** " + bytesConsumed + " bytes read");
  1.4189 +
  1.4190 +      // Handle the zero-data edge case in the same place as all other edge
  1.4191 +      // cases are handled.
  1.4192 +      if (bytesWanted === 0)
  1.4193 +        throw Cr.NS_BASE_STREAM_CLOSED;
  1.4194 +    }
  1.4195 +    catch (e)
  1.4196 +    {
  1.4197 +      if (streamClosed(e))
  1.4198 +      {
  1.4199 +        dumpn("*** input stream closed");
  1.4200 +        e = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED;
  1.4201 +      }
  1.4202 +      else
  1.4203 +      {
  1.4204 +        dumpn("!!! unexpected error reading from input, canceling: " + e);
  1.4205 +        e = Cr.NS_ERROR_UNEXPECTED;
  1.4206 +      }
  1.4207 +
  1.4208 +      this._doneReadingSource(e);
  1.4209 +      return;
  1.4210 +    }
  1.4211 +
  1.4212 +    var pendingData = this._pendingData;
  1.4213 +
  1.4214 +    NS_ASSERT(bytesConsumed > 0);
  1.4215 +    NS_ASSERT(pendingData.length > 0, "no pending data somehow?");
  1.4216 +    NS_ASSERT(pendingData[pendingData.length - 1].length > 0,
  1.4217 +              "buffered zero bytes of data?");
  1.4218 +
  1.4219 +    NS_ASSERT(this._source !== null);
  1.4220 +
  1.4221 +    // Reading has gone great, and we've gotten data to write now. What if we
  1.4222 +    // don't have a place to write that data, because output went away just
  1.4223 +    // before this read? Drop everything on the floor, including new data, and
  1.4224 +    // cancel at this point.
  1.4225 +    if (this._sink === null)
  1.4226 +    {
  1.4227 +      pendingData.length = 0;
  1.4228 +      this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED);
  1.4229 +      return;
  1.4230 +    }
  1.4231 +
  1.4232 +    // Okay, we've read the data, and we know we have a place to write it. We
  1.4233 +    // need to queue up the data to be written, but *only* if none is queued
  1.4234 +    // already -- if data's already queued, the code that actually writes the
  1.4235 +    // data will make sure to wait on unconsumed pending data.
  1.4236 +    try
  1.4237 +    {
  1.4238 +      if (pendingData.length === 1)
  1.4239 +        this._waitToWriteData();
  1.4240 +    }
  1.4241 +    catch (e)
  1.4242 +    {
  1.4243 +      dumpn("!!! error waiting to write data just read, swallowing and " +
  1.4244 +            "writing only what we already have: " + e);
  1.4245 +      this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
  1.4246 +      return;
  1.4247 +    }
  1.4248 +
  1.4249 +    // Whee! We successfully read some data, and it's successfully queued up to
  1.4250 +    // be written. All that remains now is to wait for more data to read.
  1.4251 +    try
  1.4252 +    {
  1.4253 +      this._waitToReadData();
  1.4254 +    }
  1.4255 +    catch (e)
  1.4256 +    {
  1.4257 +      dumpn("!!! error waiting to read more data: " + e);
  1.4258 +      this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED);
  1.4259 +    }
  1.4260 +  },
  1.4261 +
  1.4262 +
  1.4263 +  // NSIOUTPUTSTREAMCALLBACK
  1.4264 +
  1.4265 +  /**
  1.4266 +* Callback when data may be written to the output stream without blocking, or
  1.4267 +* when the output stream has been closed.
  1.4268 +*
  1.4269 +* @param output : nsIAsyncOutputStream
  1.4270 +* the output stream on whose writability we've been waiting, also known as
  1.4271 +* this._sink
  1.4272 +*/
  1.4273 +  onOutputStreamReady: function(output)
  1.4274 +  {
  1.4275 +    if (this._sink === null)
  1.4276 +      return;
  1.4277 +
  1.4278 +    dumpn("*** onOutputStreamReady");
  1.4279 +
  1.4280 +    var pendingData = this._pendingData;
  1.4281 +    if (pendingData.length === 0)
  1.4282 +    {
  1.4283 +      // There's no pending data to write. The only way this can happen is if
  1.4284 +      // we're waiting on the output stream's closure, so we can respond to a
  1.4285 +      // copying failure as quickly as possible (rather than waiting for data to
  1.4286 +      // be available to read and then fail to be copied). Therefore, we must
  1.4287 +      // be done now -- don't bother to attempt to write anything and wrap
  1.4288 +      // things up.
  1.4289 +      dumpn("!!! output stream closed prematurely, ending copy");
  1.4290 +
  1.4291 +      this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
  1.4292 +      return;
  1.4293 +    }
  1.4294 +
  1.4295 +
  1.4296 +    NS_ASSERT(pendingData[0].length > 0, "queued up an empty quantum?");
  1.4297 +
  1.4298 +    //
  1.4299 +    // Write out the first pending quantum of data. The possible errors here
  1.4300 +    // are:
  1.4301 +    //
  1.4302 +    // The write might fail because we can't write that much data
  1.4303 +    // Okay, we've written what we can now, so re-queue what's left and
  1.4304 +    // finish writing it out later.
  1.4305 +    // The write failed because the stream was closed
  1.4306 +    // Discard pending data that we can no longer write, stop reading, and
  1.4307 +    // signal that copying finished.
  1.4308 +    // Some other error occurred.
  1.4309 +    // Same as if the stream were closed, but notify with the status
  1.4310 +    // NS_ERROR_UNEXPECTED so the observer knows something was wonky.
  1.4311 +    //
  1.4312 +
  1.4313 +    try
  1.4314 +    {
  1.4315 +      var quantum = pendingData[0];
  1.4316 +
  1.4317 +      // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on
  1.4318 +      // undefined behavior! We're only using this because writeByteArray
  1.4319 +      // is unusably broken for asynchronous output streams; see bug 532834
  1.4320 +      // for details.
  1.4321 +      var bytesWritten = output.write(quantum, quantum.length);
  1.4322 +      if (bytesWritten === quantum.length)
  1.4323 +        pendingData.shift();
  1.4324 +      else
  1.4325 +        pendingData[0] = quantum.substring(bytesWritten);
  1.4326 +
  1.4327 +      dumpn("*** wrote " + bytesWritten + " bytes of data");
  1.4328 +    }
  1.4329 +    catch (e)
  1.4330 +    {
  1.4331 +      if (wouldBlock(e))
  1.4332 +      {
  1.4333 +        NS_ASSERT(pendingData.length > 0,
  1.4334 +                  "stream-blocking exception with no data to write?");
  1.4335 +        NS_ASSERT(pendingData[0].length > 0,
  1.4336 +                  "stream-blocking exception with empty quantum?");
  1.4337 +        this._waitToWriteData();
  1.4338 +        return;
  1.4339 +      }
  1.4340 +
  1.4341 +      if (streamClosed(e))
  1.4342 +        dumpn("!!! output stream prematurely closed, signaling error...");
  1.4343 +      else
  1.4344 +        dumpn("!!! unknown error: " + e + ", quantum=" + quantum);
  1.4345 +
  1.4346 +      this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
  1.4347 +      return;
  1.4348 +    }
  1.4349 +
  1.4350 +    // The day is ours! Quantum written, now let's see if we have more data
  1.4351 +    // still to write.
  1.4352 +    try
  1.4353 +    {
  1.4354 +      if (pendingData.length > 0)
  1.4355 +      {
  1.4356 +        this._waitToWriteData();
  1.4357 +        return;
  1.4358 +      }
  1.4359 +    }
  1.4360 +    catch (e)
  1.4361 +    {
  1.4362 +      dumpn("!!! unexpected error waiting to write pending data: " + e);
  1.4363 +      this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
  1.4364 +      return;
  1.4365 +    }
  1.4366 +
  1.4367 +    // Okay, we have no more pending data to write -- but might we get more in
  1.4368 +    // the future?
  1.4369 +    if (this._source !== null)
  1.4370 +    {
  1.4371 +      /*
  1.4372 +* If we might, then wait for the output stream to be closed. (We wait
  1.4373 +* only for closure because we have no data to write -- and if we waited
  1.4374 +* for a specific amount of data, we would get repeatedly notified for no
  1.4375 +* reason if over time the output stream permitted more and more data to
  1.4376 +* be written to it without blocking.)
  1.4377 +*/
  1.4378 +       this._waitForSinkClosure();
  1.4379 +    }
  1.4380 +    else
  1.4381 +    {
  1.4382 +      /*
  1.4383 +* On the other hand, if we can't have more data because the input
  1.4384 +* stream's gone away, then it's time to notify of copy completion.
  1.4385 +* Victory!
  1.4386 +*/
  1.4387 +      this._sink = null;
  1.4388 +      this._cancelOrDispatchCancelCallback(Cr.NS_OK);
  1.4389 +    }
  1.4390 +  },
  1.4391 +
  1.4392 +
  1.4393 +  // NSIREQUEST
  1.4394 +
  1.4395 +  /** Returns true if the cancel observer hasn't been notified yet. */
  1.4396 +  isPending: function()
  1.4397 +  {
  1.4398 +    return !this._completed;
  1.4399 +  },
  1.4400 +
  1.4401 +  /** Not implemented, don't use! */
  1.4402 +  suspend: notImplemented,
  1.4403 +  /** Not implemented, don't use! */
  1.4404 +  resume: notImplemented,
  1.4405 +
  1.4406 +  /**
  1.4407 +* Cancels data reading from input, asynchronously writes out any pending
  1.4408 +* data, and causes the observer to be notified with the given error code when
  1.4409 +* all writing has finished.
  1.4410 +*
  1.4411 +* @param status : nsresult
  1.4412 +* the status to pass to the observer when data copying has been canceled
  1.4413 +*/
  1.4414 +  cancel: function(status)
  1.4415 +  {
  1.4416 +    dumpn("*** cancel(" + status.toString(16) + ")");
  1.4417 +
  1.4418 +    if (this._canceled)
  1.4419 +    {
  1.4420 +      dumpn("*** suppressing a late cancel");
  1.4421 +      return;
  1.4422 +    }
  1.4423 +
  1.4424 +    this._canceled = true;
  1.4425 +    this.status = status;
  1.4426 +
  1.4427 +    // We could be in the middle of absolutely anything at this point. Both
  1.4428 +    // input and output might still be around, we might have pending data to
  1.4429 +    // write, and in general we know nothing about the state of the world. We
  1.4430 +    // therefore must assume everything's in progress and take everything to its
  1.4431 +    // final steady state (or so far as it can go before we need to finish
  1.4432 +    // writing out remaining data).
  1.4433 +
  1.4434 +    this._doneReadingSource(status);
  1.4435 +  },
  1.4436 +
  1.4437 +
  1.4438 +  // PRIVATE IMPLEMENTATION
  1.4439 +
  1.4440 +  /**
  1.4441 +* Stop reading input if we haven't already done so, passing e as the status
  1.4442 +* when closing the stream, and kick off a copy-completion notice if no more
  1.4443 +* data remains to be written.
  1.4444 +*
  1.4445 +* @param e : nsresult
  1.4446 +* the status to be used when closing the input stream
  1.4447 +*/
  1.4448 +  _doneReadingSource: function(e)
  1.4449 +  {
  1.4450 +    dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")");
  1.4451 +
  1.4452 +    this._finishSource(e);
  1.4453 +    if (this._pendingData.length === 0)
  1.4454 +      this._sink = null;
  1.4455 +    else
  1.4456 +      NS_ASSERT(this._sink !== null, "null output?");
  1.4457 +
  1.4458 +    // If we've written out all data read up to this point, then it's time to
  1.4459 +    // signal completion.
  1.4460 +    if (this._sink === null)
  1.4461 +    {
  1.4462 +      NS_ASSERT(this._pendingData.length === 0, "pending data still?");
  1.4463 +      this._cancelOrDispatchCancelCallback(e);
  1.4464 +    }
  1.4465 +  },
  1.4466 +
  1.4467 +  /**
  1.4468 +* Stop writing output if we haven't already done so, discard any data that
  1.4469 +* remained to be sent, close off input if it wasn't already closed, and kick
  1.4470 +* off a copy-completion notice.
  1.4471 +*
  1.4472 +* @param e : nsresult
  1.4473 +* the status to be used when closing input if it wasn't already closed
  1.4474 +*/
  1.4475 +  _doneWritingToSink: function(e)
  1.4476 +  {
  1.4477 +    dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")");
  1.4478 +
  1.4479 +    this._pendingData.length = 0;
  1.4480 +    this._sink = null;
  1.4481 +    this._doneReadingSource(e);
  1.4482 +  },
  1.4483 +
  1.4484 +  /**
  1.4485 +* Completes processing of this copy: either by canceling the copy if it
  1.4486 +* hasn't already been canceled using the provided status, or by dispatching
  1.4487 +* the cancel callback event (with the originally provided status, of course)
  1.4488 +* if it already has been canceled.
  1.4489 +*
  1.4490 +* @param status : nsresult
  1.4491 +* the status code to use to cancel this, if this hasn't already been
  1.4492 +* canceled
  1.4493 +*/
  1.4494 +  _cancelOrDispatchCancelCallback: function(status)
  1.4495 +  {
  1.4496 +    dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")");
  1.4497 +
  1.4498 +    NS_ASSERT(this._source === null, "should have finished input");
  1.4499 +    NS_ASSERT(this._sink === null, "should have finished output");
  1.4500 +    NS_ASSERT(this._pendingData.length === 0, "should have no pending data");
  1.4501 +
  1.4502 +    if (!this._canceled)
  1.4503 +    {
  1.4504 +      this.cancel(status);
  1.4505 +      return;
  1.4506 +    }
  1.4507 +
  1.4508 +    var self = this;
  1.4509 +    var event =
  1.4510 +      {
  1.4511 +        run: function()
  1.4512 +        {
  1.4513 +          dumpn("*** onStopRequest async callback");
  1.4514 +
  1.4515 +          self._completed = true;
  1.4516 +          try
  1.4517 +          {
  1.4518 +            self._observer.onStopRequest(self, self._context, self.status);
  1.4519 +          }
  1.4520 +          catch (e)
  1.4521 +          {
  1.4522 +            NS_ASSERT(false,
  1.4523 +                      "how are we throwing an exception here? we control " +
  1.4524 +                      "all the callers! " + e);
  1.4525 +          }
  1.4526 +        }
  1.4527 +      };
  1.4528 +
  1.4529 +    gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
  1.4530 +  },
  1.4531 +
  1.4532 +  /**
  1.4533 +* Kicks off another wait for more data to be available from the input stream.
  1.4534 +*/
  1.4535 +  _waitToReadData: function()
  1.4536 +  {
  1.4537 +    dumpn("*** _waitToReadData");
  1.4538 +    this._source.asyncWait(this, 0, Response.SEGMENT_SIZE,
  1.4539 +                           gThreadManager.mainThread);
  1.4540 +  },
  1.4541 +
  1.4542 +  /**
  1.4543 +* Kicks off another wait until data can be written to the output stream.
  1.4544 +*/
  1.4545 +  _waitToWriteData: function()
  1.4546 +  {
  1.4547 +    dumpn("*** _waitToWriteData");
  1.4548 +
  1.4549 +    var pendingData = this._pendingData;
  1.4550 +    NS_ASSERT(pendingData.length > 0, "no pending data to write?");
  1.4551 +    NS_ASSERT(pendingData[0].length > 0, "buffered an empty write?");
  1.4552 +
  1.4553 +    this._sink.asyncWait(this, 0, pendingData[0].length,
  1.4554 +                         gThreadManager.mainThread);
  1.4555 +  },
  1.4556 +
  1.4557 +  /**
  1.4558 +* Kicks off a wait for the sink to which data is being copied to be closed.
  1.4559 +* We wait for stream closure when we don't have any data to be copied, rather
  1.4560 +* than waiting to write a specific amount of data. We can't wait to write
  1.4561 +* data because the sink might be infinitely writable, and if no data appears
  1.4562 +* in the source for a long time we might have to spin quite a bit waiting to
  1.4563 +* write, waiting to write again, &c. Waiting on stream closure instead means
  1.4564 +* we'll get just one notification if the sink dies. Note that when data
  1.4565 +* starts arriving from the sink we'll resume waiting for data to be written,
  1.4566 +* dropping this closure-only callback entirely.
  1.4567 +*/
  1.4568 +  _waitForSinkClosure: function()
  1.4569 +  {
  1.4570 +    dumpn("*** _waitForSinkClosure");
  1.4571 +
  1.4572 +    this._sink.asyncWait(this, Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY, 0,
  1.4573 +                         gThreadManager.mainThread);
  1.4574 +  },
  1.4575 +
  1.4576 +  /**
  1.4577 +* Closes input with the given status, if it hasn't already been closed;
  1.4578 +* otherwise a no-op.
  1.4579 +*
  1.4580 +* @param status : nsresult
  1.4581 +* status code use to close the source stream if necessary
  1.4582 +*/
  1.4583 +  _finishSource: function(status)
  1.4584 +  {
  1.4585 +    dumpn("*** _finishSource(" + status.toString(16) + ")");
  1.4586 +
  1.4587 +    if (this._source !== null)
  1.4588 +    {
  1.4589 +      this._source.closeWithStatus(status);
  1.4590 +      this._source = null;
  1.4591 +    }
  1.4592 +  }
  1.4593 +};
  1.4594 +
  1.4595 +
  1.4596 +/**
  1.4597 +* A container for utility functions used with HTTP headers.
  1.4598 +*/
  1.4599 +const headerUtils =
  1.4600 +{
  1.4601 +  /**
  1.4602 +* Normalizes fieldName (by converting it to lowercase) and ensures it is a
  1.4603 +* valid header field name (although not necessarily one specified in RFC
  1.4604 +* 2616).
  1.4605 +*
  1.4606 +* @throws NS_ERROR_INVALID_ARG
  1.4607 +* if fieldName does not match the field-name production in RFC 2616
  1.4608 +* @returns string
  1.4609 +* fieldName converted to lowercase if it is a valid header, for characters
  1.4610 +* where case conversion is possible
  1.4611 +*/
  1.4612 +  normalizeFieldName: function(fieldName)
  1.4613 +  {
  1.4614 +    if (fieldName == "")
  1.4615 +      throw Cr.NS_ERROR_INVALID_ARG;
  1.4616 +
  1.4617 +    for (var i = 0, sz = fieldName.length; i < sz; i++)
  1.4618 +    {
  1.4619 +      if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)])
  1.4620 +      {
  1.4621 +        dumpn(fieldName + " is not a valid header field name!");
  1.4622 +        throw Cr.NS_ERROR_INVALID_ARG;
  1.4623 +      }
  1.4624 +    }
  1.4625 +
  1.4626 +    return fieldName.toLowerCase();
  1.4627 +  },
  1.4628 +
  1.4629 +  /**
  1.4630 +* Ensures that fieldValue is a valid header field value (although not
  1.4631 +* necessarily as specified in RFC 2616 if the corresponding field name is
  1.4632 +* part of the HTTP protocol), normalizes the value if it is, and
  1.4633 +* returns the normalized value.
  1.4634 +*
  1.4635 +* @param fieldValue : string
  1.4636 +* a value to be normalized as an HTTP header field value
  1.4637 +* @throws NS_ERROR_INVALID_ARG
  1.4638 +* if fieldValue does not match the field-value production in RFC 2616
  1.4639 +* @returns string
  1.4640 +* fieldValue as a normalized HTTP header field value
  1.4641 +*/
  1.4642 +  normalizeFieldValue: function(fieldValue)
  1.4643 +  {
  1.4644 +    // field-value = *( field-content | LWS )
  1.4645 +    // field-content = <the OCTETs making up the field-value
  1.4646 +    // and consisting of either *TEXT or combinations
  1.4647 +    // of token, separators, and quoted-string>
  1.4648 +    // TEXT = <any OCTET except CTLs,
  1.4649 +    // but including LWS>
  1.4650 +    // LWS = [CRLF] 1*( SP | HT )
  1.4651 +    //
  1.4652 +    // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
  1.4653 +    // qdtext = <any TEXT except <">>
  1.4654 +    // quoted-pair = "\" CHAR
  1.4655 +    // CHAR = <any US-ASCII character (octets 0 - 127)>
  1.4656 +
  1.4657 +    // Any LWS that occurs between field-content MAY be replaced with a single
  1.4658 +    // SP before interpreting the field value or forwarding the message
  1.4659 +    // downstream (section 4.2); we replace 1*LWS with a single SP
  1.4660 +    var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " ");
  1.4661 +
  1.4662 +    // remove leading/trailing LWS (which has been converted to SP)
  1.4663 +    val = val.replace(/^ +/, "").replace(/ +$/, "");
  1.4664 +
  1.4665 +    // that should have taken care of all CTLs, so val should contain no CTLs
  1.4666 +    for (var i = 0, len = val.length; i < len; i++)
  1.4667 +      if (isCTL(val.charCodeAt(i)))
  1.4668 +        throw Cr.NS_ERROR_INVALID_ARG;
  1.4669 +
  1.4670 +    // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly
  1.4671 +    // normalize, however, so this can be construed as a tightening of the
  1.4672 +    // spec and not entirely as a bug
  1.4673 +    return val;
  1.4674 +  }
  1.4675 +};
  1.4676 +
  1.4677 +
  1.4678 +
  1.4679 +/**
  1.4680 +* Converts the given string into a string which is safe for use in an HTML
  1.4681 +* context.
  1.4682 +*
  1.4683 +* @param str : string
  1.4684 +* the string to make HTML-safe
  1.4685 +* @returns string
  1.4686 +* an HTML-safe version of str
  1.4687 +*/
  1.4688 +function htmlEscape(str)
  1.4689 +{
  1.4690 +  // this is naive, but it'll work
  1.4691 +  var s = "";
  1.4692 +  for (var i = 0; i < str.length; i++)
  1.4693 +    s += "&#" + str.charCodeAt(i) + ";";
  1.4694 +  return s;
  1.4695 +}
  1.4696 +
  1.4697 +
  1.4698 +/**
  1.4699 +* Constructs an object representing an HTTP version (see section 3.1).
  1.4700 +*
  1.4701 +* @param versionString
  1.4702 +* a string of the form "#.#", where # is an non-negative decimal integer with
  1.4703 +* or without leading zeros
  1.4704 +* @throws
  1.4705 +* if versionString does not specify a valid HTTP version number
  1.4706 +*/
  1.4707 +function nsHttpVersion(versionString)
  1.4708 +{
  1.4709 +  var matches = /^(\d+)\.(\d+)$/.exec(versionString);
  1.4710 +  if (!matches)
  1.4711 +    throw "Not a valid HTTP version!";
  1.4712 +
  1.4713 +  /** The major version number of this, as a number. */
  1.4714 +  this.major = parseInt(matches[1], 10);
  1.4715 +
  1.4716 +  /** The minor version number of this, as a number. */
  1.4717 +  this.minor = parseInt(matches[2], 10);
  1.4718 +
  1.4719 +  if (isNaN(this.major) || isNaN(this.minor) ||
  1.4720 +      this.major < 0 || this.minor < 0)
  1.4721 +    throw "Not a valid HTTP version!";
  1.4722 +}
  1.4723 +nsHttpVersion.prototype =
  1.4724 +{
  1.4725 +  /**
  1.4726 +* Returns the standard string representation of the HTTP version represented
  1.4727 +* by this (e.g., "1.1").
  1.4728 +*/
  1.4729 +  toString: function ()
  1.4730 +  {
  1.4731 +    return this.major + "." + this.minor;
  1.4732 +  },
  1.4733 +
  1.4734 +  /**
  1.4735 +* Returns true if this represents the same HTTP version as otherVersion,
  1.4736 +* false otherwise.
  1.4737 +*
  1.4738 +* @param otherVersion : nsHttpVersion
  1.4739 +* the version to compare against this
  1.4740 +*/
  1.4741 +  equals: function (otherVersion)
  1.4742 +  {
  1.4743 +    return this.major == otherVersion.major &&
  1.4744 +           this.minor == otherVersion.minor;
  1.4745 +  },
  1.4746 +
  1.4747 +  /** True if this >= otherVersion, false otherwise. */
  1.4748 +  atLeast: function(otherVersion)
  1.4749 +  {
  1.4750 +    return this.major > otherVersion.major ||
  1.4751 +           (this.major == otherVersion.major &&
  1.4752 +            this.minor >= otherVersion.minor);
  1.4753 +  }
  1.4754 +};
  1.4755 +
  1.4756 +nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0");
  1.4757 +nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1");
  1.4758 +
  1.4759 +
  1.4760 +/**
  1.4761 +* An object which stores HTTP headers for a request or response.
  1.4762 +*
  1.4763 +* Note that since headers are case-insensitive, this object converts headers to
  1.4764 +* lowercase before storing them. This allows the getHeader and hasHeader
  1.4765 +* methods to work correctly for any case of a header, but it means that the
  1.4766 +* values returned by .enumerator may not be equal case-sensitively to the
  1.4767 +* values passed to setHeader when adding headers to this.
  1.4768 +*/
  1.4769 +function nsHttpHeaders()
  1.4770 +{
  1.4771 +  /**
  1.4772 +* A hash of headers, with header field names as the keys and header field
  1.4773 +* values as the values. Header field names are case-insensitive, but upon
  1.4774 +* insertion here they are converted to lowercase. Header field values are
  1.4775 +* normalized upon insertion to contain no leading or trailing whitespace.
  1.4776 +*
  1.4777 +* Note also that per RFC 2616, section 4.2, two headers with the same name in
  1.4778 +* a message may be treated as one header with the same field name and a field
  1.4779 +* value consisting of the separate field values joined together with a "," in
  1.4780 +* their original order. This hash stores multiple headers with the same name
  1.4781 +* in this manner.
  1.4782 +*/
  1.4783 +  this._headers = {};
  1.4784 +}
  1.4785 +nsHttpHeaders.prototype =
  1.4786 +{
  1.4787 +  /**
  1.4788 +* Sets the header represented by name and value in this.
  1.4789 +*
  1.4790 +* @param name : string
  1.4791 +* the header name
  1.4792 +* @param value : string
  1.4793 +* the header value
  1.4794 +* @throws NS_ERROR_INVALID_ARG
  1.4795 +* if name or value is not a valid header component
  1.4796 +*/
  1.4797 +  setHeader: function(fieldName, fieldValue, merge)
  1.4798 +  {
  1.4799 +    var name = headerUtils.normalizeFieldName(fieldName);
  1.4800 +    var value = headerUtils.normalizeFieldValue(fieldValue);
  1.4801 +
  1.4802 +    // The following three headers are stored as arrays because their real-world
  1.4803 +    // syntax prevents joining individual headers into a single header using
  1.4804 +    // ",". See also <http://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77>
  1.4805 +    if (merge && name in this._headers)
  1.4806 +    {
  1.4807 +      if (name === "www-authenticate" ||
  1.4808 +          name === "proxy-authenticate" ||
  1.4809 +          name === "set-cookie")
  1.4810 +      {
  1.4811 +        this._headers[name].push(value);
  1.4812 +      }
  1.4813 +      else
  1.4814 +      {
  1.4815 +        this._headers[name][0] += "," + value;
  1.4816 +        NS_ASSERT(this._headers[name].length === 1,
  1.4817 +            "how'd a non-special header have multiple values?")
  1.4818 +      }
  1.4819 +    }
  1.4820 +    else
  1.4821 +    {
  1.4822 +      this._headers[name] = [value];
  1.4823 +    }
  1.4824 +  },
  1.4825 +
  1.4826 +  /**
  1.4827 +* Returns the value for the header specified by this.
  1.4828 +*
  1.4829 +* @throws NS_ERROR_INVALID_ARG
  1.4830 +* if fieldName does not constitute a valid header field name
  1.4831 +* @throws NS_ERROR_NOT_AVAILABLE
  1.4832 +* if the given header does not exist in this
  1.4833 +* @returns string
  1.4834 +* the field value for the given header, possibly with non-semantic changes
  1.4835 +* (i.e., leading/trailing whitespace stripped, whitespace runs replaced
  1.4836 +* with spaces, etc.) at the option of the implementation; multiple
  1.4837 +* instances of the header will be combined with a comma, except for
  1.4838 +* the three headers noted in the description of getHeaderValues
  1.4839 +*/
  1.4840 +  getHeader: function(fieldName)
  1.4841 +  {
  1.4842 +    return this.getHeaderValues(fieldName).join("\n");
  1.4843 +  },
  1.4844 +
  1.4845 +  /**
  1.4846 +* Returns the value for the header specified by fieldName as an array.
  1.4847 +*
  1.4848 +* @throws NS_ERROR_INVALID_ARG
  1.4849 +* if fieldName does not constitute a valid header field name
  1.4850 +* @throws NS_ERROR_NOT_AVAILABLE
  1.4851 +* if the given header does not exist in this
  1.4852 +* @returns [string]
  1.4853 +* an array of all the header values in this for the given
  1.4854 +* header name. Header values will generally be collapsed
  1.4855 +* into a single header by joining all header values together
  1.4856 +* with commas, but certain headers (Proxy-Authenticate,
  1.4857 +* WWW-Authenticate, and Set-Cookie) violate the HTTP spec
  1.4858 +* and cannot be collapsed in this manner. For these headers
  1.4859 +* only, the returned array may contain multiple elements if
  1.4860 +* that header has been added more than once.
  1.4861 +*/
  1.4862 +  getHeaderValues: function(fieldName)
  1.4863 +  {
  1.4864 +    var name = headerUtils.normalizeFieldName(fieldName);
  1.4865 +
  1.4866 +    if (name in this._headers)
  1.4867 +      return this._headers[name];
  1.4868 +    else
  1.4869 +      throw Cr.NS_ERROR_NOT_AVAILABLE;
  1.4870 +  },
  1.4871 +
  1.4872 +  /**
  1.4873 +* Returns true if a header with the given field name exists in this, false
  1.4874 +* otherwise.
  1.4875 +*
  1.4876 +* @param fieldName : string
  1.4877 +* the field name whose existence is to be determined in this
  1.4878 +* @throws NS_ERROR_INVALID_ARG
  1.4879 +* if fieldName does not constitute a valid header field name
  1.4880 +* @returns boolean
  1.4881 +* true if the header's present, false otherwise
  1.4882 +*/
  1.4883 +  hasHeader: function(fieldName)
  1.4884 +  {
  1.4885 +    var name = headerUtils.normalizeFieldName(fieldName);
  1.4886 +    return (name in this._headers);
  1.4887 +  },
  1.4888 +
  1.4889 +  /**
  1.4890 +* Returns a new enumerator over the field names of the headers in this, as
  1.4891 +* nsISupportsStrings. The names returned will be in lowercase, regardless of
  1.4892 +* how they were input using setHeader (header names are case-insensitive per
  1.4893 +* RFC 2616).
  1.4894 +*/
  1.4895 +  get enumerator()
  1.4896 +  {
  1.4897 +    var headers = [];
  1.4898 +    for (var i in this._headers)
  1.4899 +    {
  1.4900 +      var supports = new SupportsString();
  1.4901 +      supports.data = i;
  1.4902 +      headers.push(supports);
  1.4903 +    }
  1.4904 +
  1.4905 +    return new nsSimpleEnumerator(headers);
  1.4906 +  }
  1.4907 +};
  1.4908 +
  1.4909 +
  1.4910 +/**
  1.4911 +* Constructs an nsISimpleEnumerator for the given array of items.
  1.4912 +*
  1.4913 +* @param items : Array
  1.4914 +* the items, which must all implement nsISupports
  1.4915 +*/
  1.4916 +function nsSimpleEnumerator(items)
  1.4917 +{
  1.4918 +  this._items = items;
  1.4919 +  this._nextIndex = 0;
  1.4920 +}
  1.4921 +nsSimpleEnumerator.prototype =
  1.4922 +{
  1.4923 +  hasMoreElements: function()
  1.4924 +  {
  1.4925 +    return this._nextIndex < this._items.length;
  1.4926 +  },
  1.4927 +  getNext: function()
  1.4928 +  {
  1.4929 +    if (!this.hasMoreElements())
  1.4930 +      throw Cr.NS_ERROR_NOT_AVAILABLE;
  1.4931 +
  1.4932 +    return this._items[this._nextIndex++];
  1.4933 +  },
  1.4934 +  QueryInterface: function(aIID)
  1.4935 +  {
  1.4936 +    if (Ci.nsISimpleEnumerator.equals(aIID) ||
  1.4937 +        Ci.nsISupports.equals(aIID))
  1.4938 +      return this;
  1.4939 +
  1.4940 +    throw Cr.NS_ERROR_NO_INTERFACE;
  1.4941 +  }
  1.4942 +};
  1.4943 +
  1.4944 +
  1.4945 +/**
  1.4946 +* A representation of the data in an HTTP request.
  1.4947 +*
  1.4948 +* @param port : uint
  1.4949 +* the port on which the server receiving this request runs
  1.4950 +*/
  1.4951 +function Request(port)
  1.4952 +{
  1.4953 +  /** Method of this request, e.g. GET or POST. */
  1.4954 +  this._method = "";
  1.4955 +
  1.4956 +  /** Path of the requested resource; empty paths are converted to '/'. */
  1.4957 +  this._path = "";
  1.4958 +
  1.4959 +  /** Query string, if any, associated with this request (not including '?'). */
  1.4960 +  this._queryString = "";
  1.4961 +
  1.4962 +  /** Scheme of requested resource, usually http, always lowercase. */
  1.4963 +  this._scheme = "http";
  1.4964 +
  1.4965 +  /** Hostname on which the requested resource resides. */
  1.4966 +  this._host = undefined;
  1.4967 +
  1.4968 +  /** Port number over which the request was received. */
  1.4969 +  this._port = port;
  1.4970 +
  1.4971 +  var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null);
  1.4972 +
  1.4973 +  /** Stream from which data in this request's body may be read. */
  1.4974 +  this._bodyInputStream = bodyPipe.inputStream;
  1.4975 +
  1.4976 +  /** Stream to which data in this request's body is written. */
  1.4977 +  this._bodyOutputStream = bodyPipe.outputStream;
  1.4978 +
  1.4979 +  /**
  1.4980 +* The headers in this request.
  1.4981 +*/
  1.4982 +  this._headers = new nsHttpHeaders();
  1.4983 +
  1.4984 +  /**
  1.4985 +* For the addition of ad-hoc properties and new functionality without having
  1.4986 +* to change nsIHttpRequest every time; currently lazily created, as its only
  1.4987 +* use is in directory listings.
  1.4988 +*/
  1.4989 +  this._bag = null;
  1.4990 +}
  1.4991 +Request.prototype =
  1.4992 +{
  1.4993 +  // SERVER METADATA
  1.4994 +
  1.4995 +  //
  1.4996 +  // see nsIHttpRequest.scheme
  1.4997 +  //
  1.4998 +  get scheme()
  1.4999 +  {
  1.5000 +    return this._scheme;
  1.5001 +  },
  1.5002 +
  1.5003 +  //
  1.5004 +  // see nsIHttpRequest.host
  1.5005 +  //
  1.5006 +  get host()
  1.5007 +  {
  1.5008 +    return this._host;
  1.5009 +  },
  1.5010 +
  1.5011 +  //
  1.5012 +  // see nsIHttpRequest.port
  1.5013 +  //
  1.5014 +  get port()
  1.5015 +  {
  1.5016 +    return this._port;
  1.5017 +  },
  1.5018 +
  1.5019 +  // REQUEST LINE
  1.5020 +
  1.5021 +  //
  1.5022 +  // see nsIHttpRequest.method
  1.5023 +  //
  1.5024 +  get method()
  1.5025 +  {
  1.5026 +    return this._method;
  1.5027 +  },
  1.5028 +
  1.5029 +  //
  1.5030 +  // see nsIHttpRequest.httpVersion
  1.5031 +  //
  1.5032 +  get httpVersion()
  1.5033 +  {
  1.5034 +    return this._httpVersion.toString();
  1.5035 +  },
  1.5036 +
  1.5037 +  //
  1.5038 +  // see nsIHttpRequest.path
  1.5039 +  //
  1.5040 +  get path()
  1.5041 +  {
  1.5042 +    return this._path;
  1.5043 +  },
  1.5044 +
  1.5045 +  //
  1.5046 +  // see nsIHttpRequest.queryString
  1.5047 +  //
  1.5048 +  get queryString()
  1.5049 +  {
  1.5050 +    return this._queryString;
  1.5051 +  },
  1.5052 +
  1.5053 +  // HEADERS
  1.5054 +
  1.5055 +  //
  1.5056 +  // see nsIHttpRequest.getHeader
  1.5057 +  //
  1.5058 +  getHeader: function(name)
  1.5059 +  {
  1.5060 +    return this._headers.getHeader(name);
  1.5061 +  },
  1.5062 +
  1.5063 +  //
  1.5064 +  // see nsIHttpRequest.hasHeader
  1.5065 +  //
  1.5066 +  hasHeader: function(name)
  1.5067 +  {
  1.5068 +    return this._headers.hasHeader(name);
  1.5069 +  },
  1.5070 +
  1.5071 +  //
  1.5072 +  // see nsIHttpRequest.headers
  1.5073 +  //
  1.5074 +  get headers()
  1.5075 +  {
  1.5076 +    return this._headers.enumerator;
  1.5077 +  },
  1.5078 +
  1.5079 +  //
  1.5080 +  // see nsIPropertyBag.enumerator
  1.5081 +  //
  1.5082 +  get enumerator()
  1.5083 +  {
  1.5084 +    this._ensurePropertyBag();
  1.5085 +    return this._bag.enumerator;
  1.5086 +  },
  1.5087 +
  1.5088 +  //
  1.5089 +  // see nsIHttpRequest.headers
  1.5090 +  //
  1.5091 +  get bodyInputStream()
  1.5092 +  {
  1.5093 +    return this._bodyInputStream;
  1.5094 +  },
  1.5095 +
  1.5096 +  //
  1.5097 +  // see nsIPropertyBag.getProperty
  1.5098 +  //
  1.5099 +  getProperty: function(name)
  1.5100 +  {
  1.5101 +    this._ensurePropertyBag();
  1.5102 +    return this._bag.getProperty(name);
  1.5103 +  },
  1.5104 +
  1.5105 +
  1.5106 +  // NSISUPPORTS
  1.5107 +
  1.5108 +  //
  1.5109 +  // see nsISupports.QueryInterface
  1.5110 +  //
  1.5111 +  QueryInterface: function(iid)
  1.5112 +  {
  1.5113 +    if (iid.equals(Ci.nsIHttpRequest) || iid.equals(Ci.nsISupports))
  1.5114 +      return this;
  1.5115 +
  1.5116 +    throw Cr.NS_ERROR_NO_INTERFACE;
  1.5117 +  },
  1.5118 +
  1.5119 +
  1.5120 +  // PRIVATE IMPLEMENTATION
  1.5121 +  
  1.5122 +  /** Ensures a property bag has been created for ad-hoc behaviors. */
  1.5123 +  _ensurePropertyBag: function()
  1.5124 +  {
  1.5125 +    if (!this._bag)
  1.5126 +      this._bag = new WritablePropertyBag();
  1.5127 +  }
  1.5128 +};
  1.5129 +
  1.5130 +
  1.5131 +// XPCOM trappings
  1.5132 +if ("XPCOMUtils" in this && // Firefox 3.6 doesn't load XPCOMUtils in this scope for some reason...
  1.5133 +    "generateNSGetFactory" in XPCOMUtils) {
  1.5134 +  var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]);
  1.5135 +}
  1.5136 +
  1.5137 +
  1.5138 +
  1.5139 +/**
  1.5140 +* Creates a new HTTP server listening for loopback traffic on the given port,
  1.5141 +* starts it, and runs the server until the server processes a shutdown request,
  1.5142 +* spinning an event loop so that events posted by the server's socket are
  1.5143 +* processed.
  1.5144 +*
  1.5145 +* This method is primarily intended for use in running this script from within
  1.5146 +* xpcshell and running a functional HTTP server without having to deal with
  1.5147 +* non-essential details.
  1.5148 +*
  1.5149 +* Note that running multiple servers using variants of this method probably
  1.5150 +* doesn't work, simply due to how the internal event loop is spun and stopped.
  1.5151 +*
  1.5152 +* @note
  1.5153 +* This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code);
  1.5154 +* you should use this server as a component in Mozilla 1.8.
  1.5155 +* @param port
  1.5156 +* the port on which the server will run, or -1 if there exists no preference
  1.5157 +* for a specific port; note that attempting to use some values for this
  1.5158 +* parameter (particularly those below 1024) may cause this method to throw or
  1.5159 +* may result in the server being prematurely shut down
  1.5160 +* @param basePath
  1.5161 +* a local directory from which requests will be served (i.e., if this is
  1.5162 +* "/home/jwalden/" then a request to /index.html will load
  1.5163 +* /home/jwalden/index.html); if this is omitted, only the default URLs in
  1.5164 +* this server implementation will be functional
  1.5165 +*/
  1.5166 +function server(port, basePath)
  1.5167 +{
  1.5168 +  if (basePath)
  1.5169 +  {
  1.5170 +    var lp = Cc["@mozilla.org/file/local;1"]
  1.5171 +               .createInstance(Ci.nsILocalFile);
  1.5172 +    lp.initWithPath(basePath);
  1.5173 +  }
  1.5174 +
  1.5175 +  // if you're running this, you probably want to see debugging info
  1.5176 +  DEBUG = true;
  1.5177 +
  1.5178 +  var srv = new nsHttpServer();
  1.5179 +  if (lp)
  1.5180 +    srv.registerDirectory("/", lp);
  1.5181 +  srv.registerContentType("sjs", SJS_TYPE);
  1.5182 +  srv.start(port);
  1.5183 +
  1.5184 +  var thread = gThreadManager.currentThread;
  1.5185 +  while (!srv.isStopped())
  1.5186 +    thread.processNextEvent(true);
  1.5187 +
  1.5188 +  // get rid of any pending requests
  1.5189 +  while (thread.hasPendingEvents())
  1.5190 +    thread.processNextEvent(true);
  1.5191 +
  1.5192 +  DEBUG = false;
  1.5193 +}
  1.5194 +
  1.5195 +function startServerAsync(port, basePath)
  1.5196 +{
  1.5197 +  if (basePath)
  1.5198 +  {
  1.5199 +    var lp = Cc["@mozilla.org/file/local;1"]
  1.5200 +               .createInstance(Ci.nsILocalFile);
  1.5201 +    lp.initWithPath(basePath);
  1.5202 +  }
  1.5203 +
  1.5204 +  var srv = new nsHttpServer();
  1.5205 +  if (lp)
  1.5206 +    srv.registerDirectory("/", lp);
  1.5207 +  srv.registerContentType("sjs", "sjs");
  1.5208 +  srv.start(port);
  1.5209 +  return srv;
  1.5210 +}
  1.5211 +
  1.5212 +exports.nsHttpServer = nsHttpServer;
  1.5213 +exports.ScriptableInputStream = ScriptableInputStream;
  1.5214 +exports.server = server;
  1.5215 +exports.startServerAsync = startServerAsync;

mercurial