netwerk/test/httpserver/httpd.js

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

mercurial