tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

httpd.sys.mjs (158336B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim:set ts=2 sw=2 sts=2 et: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 /*
      8 * An implementation of an HTTP server both as a loadable script and as an XPCOM
      9 * component.  See the accompanying README file for user documentation on
     10 * httpd.js.
     11 */
     12 
     13 /* eslint-disable no-shadow */
     14 
     15 const CC = Components.Constructor;
     16 
     17 const PR_UINT32_MAX = Math.pow(2, 32) - 1;
     18 
     19 /** True if debugging output is enabled, false otherwise. */
     20 var DEBUG = false; // non-const *only* so tweakable in server tests
     21 
     22 /** True if debugging output should be timestamped. */
     23 var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests
     24 
     25 /**
     26 * Sets the debugging status, intended for tweaking in server tests.
     27 *
     28 * @param {boolean} debug
     29 *   Enables debugging output
     30 * @param {boolean} debugTimestamp
     31 *   Enables timestamping of the debugging output.
     32 */
     33 export function setDebuggingStatus(debug, debugTimestamp) {
     34  DEBUG = debug;
     35  DEBUG_TIMESTAMP = debugTimestamp;
     36 }
     37 
     38 /**
     39 * Asserts that the given condition holds.  If it doesn't, the given message is
     40 * dumped, a stack trace is printed, and an exception is thrown to attempt to
     41 * stop execution (which unfortunately must rely upon the exception not being
     42 * accidentally swallowed by the code that uses it).
     43 */
     44 function NS_ASSERT(cond, msg) {
     45  if (DEBUG && !cond) {
     46    dumpn("###!!!");
     47    dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!"));
     48    dumpn("###!!! Stack follows:");
     49 
     50    var stack = new Error().stack.split(/\n/);
     51    dumpn(
     52      stack
     53        .map(function (val) {
     54          return "###!!!   " + val;
     55        })
     56        .join("\n")
     57    );
     58 
     59    throw Components.Exception("", Cr.NS_ERROR_ABORT);
     60  }
     61 }
     62 
     63 /** Constructs an HTTP error object. */
     64 export function HttpError(code, description) {
     65  this.code = code;
     66  this.description = description;
     67 }
     68 
     69 HttpError.prototype = {
     70  toString() {
     71    return this.code + " " + this.description;
     72  },
     73 };
     74 
     75 /**
     76 * Errors thrown to trigger specific HTTP server responses.
     77 */
     78 export var HTTP_400 = new HttpError(400, "Bad Request");
     79 
     80 export var HTTP_401 = new HttpError(401, "Unauthorized");
     81 export var HTTP_402 = new HttpError(402, "Payment Required");
     82 export var HTTP_403 = new HttpError(403, "Forbidden");
     83 export var HTTP_404 = new HttpError(404, "Not Found");
     84 export var HTTP_405 = new HttpError(405, "Method Not Allowed");
     85 export var HTTP_406 = new HttpError(406, "Not Acceptable");
     86 export var HTTP_407 = new HttpError(407, "Proxy Authentication Required");
     87 export var HTTP_408 = new HttpError(408, "Request Timeout");
     88 export var HTTP_409 = new HttpError(409, "Conflict");
     89 export var HTTP_410 = new HttpError(410, "Gone");
     90 export var HTTP_411 = new HttpError(411, "Length Required");
     91 export var HTTP_412 = new HttpError(412, "Precondition Failed");
     92 export var HTTP_413 = new HttpError(413, "Request Entity Too Large");
     93 export var HTTP_414 = new HttpError(414, "Request-URI Too Long");
     94 export var HTTP_415 = new HttpError(415, "Unsupported Media Type");
     95 export var HTTP_417 = new HttpError(417, "Expectation Failed");
     96 export var HTTP_500 = new HttpError(500, "Internal Server Error");
     97 export var HTTP_501 = new HttpError(501, "Not Implemented");
     98 export var HTTP_502 = new HttpError(502, "Bad Gateway");
     99 export var HTTP_503 = new HttpError(503, "Service Unavailable");
    100 export var HTTP_504 = new HttpError(504, "Gateway Timeout");
    101 export var HTTP_505 = new HttpError(505, "HTTP Version Not Supported");
    102 
    103 /** Creates a hash with fields corresponding to the values in arr. */
    104 function array2obj(arr) {
    105  var obj = {};
    106  for (var i = 0; i < arr.length; i++) {
    107    obj[arr[i]] = arr[i];
    108  }
    109  return obj;
    110 }
    111 
    112 /** Returns an array of the integers x through y, inclusive. */
    113 function range(x, y) {
    114  var arr = [];
    115  for (var i = x; i <= y; i++) {
    116    arr.push(i);
    117  }
    118  return arr;
    119 }
    120 
    121 /** An object (hash) whose fields are the numbers of all HTTP error codes. */
    122 const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505)));
    123 
    124 /**
    125 * The character used to distinguish hidden files from non-hidden files, a la
    126 * the leading dot in Apache.  Since that mechanism also hides files from
    127 * easy display in LXR, ls output, etc. however, we choose instead to use a
    128 * suffix character.  If a requested file ends with it, we append another
    129 * when getting the file on the server.  If it doesn't, we just look up that
    130 * file.  Therefore, any file whose name ends with exactly one of the character
    131 * is "hidden" and available for use by the server.
    132 */
    133 const HIDDEN_CHAR = "^";
    134 
    135 /**
    136 * The file name suffix indicating the file containing overridden headers for
    137 * a requested file.
    138 */
    139 const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR;
    140 const INFORMATIONAL_RESPONSE_SUFFIX =
    141  HIDDEN_CHAR + "informationalResponse" + HIDDEN_CHAR;
    142 
    143 /** Type used to denote SJS scripts for CGI-like functionality. */
    144 const SJS_TYPE = "sjs";
    145 
    146 /** Base for relative timestamps produced by dumpn(). */
    147 var firstStamp = 0;
    148 
    149 /** dump(str) with a trailing "\n" -- only outputs if DEBUG. */
    150 export function dumpn(str) {
    151  if (DEBUG) {
    152    var prefix = "HTTPD-INFO | ";
    153    if (DEBUG_TIMESTAMP) {
    154      if (firstStamp === 0) {
    155        firstStamp = Date.now();
    156      }
    157 
    158      var elapsed = Date.now() - firstStamp; // milliseconds
    159      var min = Math.floor(elapsed / 60000);
    160      var sec = (elapsed % 60000) / 1000;
    161 
    162      if (sec < 10) {
    163        prefix += min + ":0" + sec.toFixed(3) + " | ";
    164      } else {
    165        prefix += min + ":" + sec.toFixed(3) + " | ";
    166      }
    167    }
    168 
    169    dump(prefix + str + "\n");
    170  }
    171 }
    172 
    173 /** Dumps the current JS stack if DEBUG. */
    174 function dumpStack() {
    175  // peel off the frames for dumpStack() and Error()
    176  var stack = new Error().stack.split(/\n/).slice(2);
    177  stack.forEach(dumpn);
    178 }
    179 
    180 /**
    181 * JavaScript constructors for commonly-used classes; precreating these is a
    182 * speedup over doing the same from base principles.  See the docs at
    183 * http://developer.mozilla.org/en/docs/Components.Constructor for details.
    184 */
    185 const ServerSocket = CC(
    186  "@mozilla.org/network/server-socket;1",
    187  "nsIServerSocket",
    188  "init"
    189 );
    190 const ServerSocketIPv6 = CC(
    191  "@mozilla.org/network/server-socket;1",
    192  "nsIServerSocket",
    193  "initIPv6"
    194 );
    195 const ServerSocketDualStack = CC(
    196  "@mozilla.org/network/server-socket;1",
    197  "nsIServerSocket",
    198  "initDualStack"
    199 );
    200 const ScriptableInputStream = CC(
    201  "@mozilla.org/scriptableinputstream;1",
    202  "nsIScriptableInputStream",
    203  "init"
    204 );
    205 const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init");
    206 const FileInputStream = CC(
    207  "@mozilla.org/network/file-input-stream;1",
    208  "nsIFileInputStream",
    209  "init"
    210 );
    211 const ConverterInputStream = CC(
    212  "@mozilla.org/intl/converter-input-stream;1",
    213  "nsIConverterInputStream",
    214  "init"
    215 );
    216 const WritablePropertyBag = CC(
    217  "@mozilla.org/hash-property-bag;1",
    218  "nsIWritablePropertyBag2"
    219 );
    220 const SupportsString = CC(
    221  "@mozilla.org/supports-string;1",
    222  "nsISupportsString"
    223 );
    224 
    225 /* These two are non-const only so a test can overwrite them. */
    226 var BinaryInputStream = CC(
    227  "@mozilla.org/binaryinputstream;1",
    228  "nsIBinaryInputStream",
    229  "setInputStream"
    230 );
    231 var BinaryOutputStream = CC(
    232  "@mozilla.org/binaryoutputstream;1",
    233  "nsIBinaryOutputStream",
    234  "setOutputStream"
    235 );
    236 
    237 export function overrideBinaryStreamsForTests(
    238  inputStream,
    239  outputStream,
    240  responseSegmentSize
    241 ) {
    242  BinaryInputStream = inputStream;
    243  BinaryOutputStream = outputStream;
    244  Response.SEGMENT_SIZE = responseSegmentSize;
    245 }
    246 
    247 /**
    248 * Returns the RFC 822/1123 representation of a date.
    249 *
    250 * @param date : Number
    251 *   the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT
    252 * @returns string
    253 *   the representation of the given date
    254 */
    255 function toDateString(date) {
    256  //
    257  // rfc1123-date = wkday "," SP date1 SP time SP "GMT"
    258  // date1        = 2DIGIT SP month SP 4DIGIT
    259  //                ; day month year (e.g., 02 Jun 1982)
    260  // time         = 2DIGIT ":" 2DIGIT ":" 2DIGIT
    261  //                ; 00:00:00 - 23:59:59
    262  // wkday        = "Mon" | "Tue" | "Wed"
    263  //              | "Thu" | "Fri" | "Sat" | "Sun"
    264  // month        = "Jan" | "Feb" | "Mar" | "Apr"
    265  //              | "May" | "Jun" | "Jul" | "Aug"
    266  //              | "Sep" | "Oct" | "Nov" | "Dec"
    267  //
    268 
    269  const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
    270  const monthStrings = [
    271    "Jan",
    272    "Feb",
    273    "Mar",
    274    "Apr",
    275    "May",
    276    "Jun",
    277    "Jul",
    278    "Aug",
    279    "Sep",
    280    "Oct",
    281    "Nov",
    282    "Dec",
    283  ];
    284 
    285  /**
    286   * Processes a date and returns the encoded UTC time as a string according to
    287   * the format specified in RFC 2616.
    288   *
    289   * @param date : Date
    290   *   the date to process
    291   * @returns string
    292   *   a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
    293   */
    294  function toTime(date) {
    295    var hrs = date.getUTCHours();
    296    var rv = hrs < 10 ? "0" + hrs : hrs;
    297 
    298    var mins = date.getUTCMinutes();
    299    rv += ":";
    300    rv += mins < 10 ? "0" + mins : mins;
    301 
    302    var secs = date.getUTCSeconds();
    303    rv += ":";
    304    rv += secs < 10 ? "0" + secs : secs;
    305 
    306    return rv;
    307  }
    308 
    309  /**
    310   * Processes a date and returns the encoded UTC date as a string according to
    311   * the date1 format specified in RFC 2616.
    312   *
    313   * @param date : Date
    314   *   the date to process
    315   * @returns string
    316   *   a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
    317   */
    318  function toDate1(date) {
    319    var day = date.getUTCDate();
    320    var month = date.getUTCMonth();
    321    var year = date.getUTCFullYear();
    322 
    323    var rv = day < 10 ? "0" + day : day;
    324    rv += " " + monthStrings[month];
    325    rv += " " + year;
    326 
    327    return rv;
    328  }
    329 
    330  date = new Date(date);
    331 
    332  const fmtString = "%wkday%, %date1% %time% GMT";
    333  var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]);
    334  rv = rv.replace("%time%", toTime(date));
    335  return rv.replace("%date1%", toDate1(date));
    336 }
    337 
    338 /**
    339 * Instantiates a new HTTP server.
    340 */
    341 export function nsHttpServer() {
    342  /** The port on which this server listens. */
    343  this._port = undefined;
    344 
    345  /** The socket associated with this. */
    346  this._socket = null;
    347 
    348  /** The handler used to process requests to this server. */
    349  this._handler = new ServerHandler(this);
    350 
    351  /** Naming information for this server. */
    352  this._identity = new ServerIdentity();
    353 
    354  /**
    355   * Indicates when the server is to be shut down at the end of the request.
    356   */
    357  this._doQuit = false;
    358 
    359  /**
    360   * True if the socket in this is closed (and closure notifications have been
    361   * sent and processed if the socket was ever opened), false otherwise.
    362   */
    363  this._socketClosed = true;
    364 
    365  /**
    366   * Used for tracking existing connections and ensuring that all connections
    367   * are properly cleaned up before server shutdown; increases by 1 for every
    368   * new incoming connection.
    369   */
    370  this._connectionGen = 0;
    371 
    372  /**
    373   * Hash of all open connections, indexed by connection number at time of
    374   * creation.
    375   */
    376  this._connections = {};
    377 }
    378 
    379 nsHttpServer.prototype = {
    380  // NSISERVERSOCKETLISTENER
    381 
    382  /**
    383   * Processes an incoming request coming in on the given socket and contained
    384   * in the given transport.
    385   *
    386   * @param socket : nsIServerSocket
    387   *   the socket through which the request was served
    388   * @param trans : nsISocketTransport
    389   *   the transport for the request/response
    390   * @see nsIServerSocketListener.onSocketAccepted
    391   */
    392  onSocketAccepted(socket, trans) {
    393    dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")");
    394 
    395    dumpn(">>> new connection on " + trans.host + ":" + trans.port);
    396 
    397    const SEGMENT_SIZE = 8192;
    398    const SEGMENT_COUNT = 1024;
    399    try {
    400      var input = trans
    401        .openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
    402        .QueryInterface(Ci.nsIAsyncInputStream);
    403      var output = trans.openOutputStream(0, 0, 0);
    404    } catch (e) {
    405      dumpn("*** error opening transport streams: " + e);
    406      trans.close(Cr.NS_BINDING_ABORTED);
    407      return;
    408    }
    409 
    410    var connectionNumber = ++this._connectionGen;
    411 
    412    try {
    413      var conn = new Connection(
    414        input,
    415        output,
    416        this,
    417        socket.port,
    418        trans.port,
    419        connectionNumber,
    420        trans
    421      );
    422      var reader = new RequestReader(conn);
    423 
    424      // XXX add request timeout functionality here!
    425 
    426      // Note: must use main thread here, or we might get a GC that will cause
    427      //       threadsafety assertions.  We really need to fix XPConnect so that
    428      //       you can actually do things in multi-threaded JS.  :-(
    429      input.asyncWait(reader, 0, 0, Services.tm.mainThread);
    430    } catch (e) {
    431      // Assume this connection can't be salvaged and bail on it completely;
    432      // don't attempt to close it so that we can assert that any connection
    433      // being closed is in this._connections.
    434      dumpn("*** error in initial request-processing stages: " + e);
    435      trans.close(Cr.NS_BINDING_ABORTED);
    436      return;
    437    }
    438 
    439    this._connections[connectionNumber] = conn;
    440    dumpn("*** starting connection " + connectionNumber);
    441  },
    442 
    443  /**
    444   * Called when the socket associated with this is closed.
    445   *
    446   * @param socket : nsIServerSocket
    447   *   the socket being closed
    448   * @param status : nsresult
    449   *   the reason the socket stopped listening (NS_BINDING_ABORTED if the server
    450   *   was stopped using nsIHttpServer.stop)
    451   * @see nsIServerSocketListener.onStopListening
    452   */
    453  onStopListening(socket) {
    454    dumpn(">>> shutting down server on port " + socket.port);
    455    for (var n in this._connections) {
    456      if (!this._connections[n]._requestStarted) {
    457        this._connections[n].close();
    458      }
    459    }
    460    this._socketClosed = true;
    461    if (this._hasOpenConnections()) {
    462      dumpn("*** open connections!!!");
    463    }
    464    if (!this._hasOpenConnections()) {
    465      dumpn("*** no open connections, notifying async from onStopListening");
    466 
    467      // Notify asynchronously so that any pending teardown in stop() has a
    468      // chance to run first.
    469      var self = this;
    470      var stopEvent = {
    471        run() {
    472          dumpn("*** _notifyStopped async callback");
    473          self._notifyStopped();
    474        },
    475      };
    476      Services.tm.currentThread.dispatch(
    477        stopEvent,
    478        Ci.nsIThread.DISPATCH_NORMAL
    479      );
    480    }
    481  },
    482 
    483  // NSIHTTPSERVER
    484 
    485  //
    486  // see nsIHttpServer.start
    487  //
    488  start(port) {
    489    this._start(port, "localhost");
    490  },
    491 
    492  //
    493  // see nsIHttpServer.start_ipv6
    494  //
    495  start_ipv6(port) {
    496    this._start(port, "[::1]");
    497  },
    498 
    499  start_dualStack(port) {
    500    this._start(port, "[::1]", true);
    501  },
    502 
    503  _start(port, host, dualStack) {
    504    if (this._socket) {
    505      throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED);
    506    }
    507 
    508    this._port = port;
    509    this._doQuit = this._socketClosed = false;
    510 
    511    this._host = host;
    512 
    513    // The listen queue needs to be long enough to handle
    514    // network.http.max-persistent-connections-per-server or
    515    // network.http.max-persistent-connections-per-proxy concurrent
    516    // connections, plus a safety margin in case some other process is
    517    // talking to the server as well.
    518    var maxConnections =
    519      5 +
    520      Math.max(
    521        Services.prefs.getIntPref(
    522          "network.http.max-persistent-connections-per-server"
    523        ),
    524        Services.prefs.getIntPref(
    525          "network.http.max-persistent-connections-per-proxy"
    526        )
    527      );
    528 
    529    try {
    530      var loopback = true;
    531      if (
    532        this._host != "127.0.0.1" &&
    533        this._host != "localhost" &&
    534        this._host != "[::1]"
    535      ) {
    536        loopback = false;
    537      }
    538 
    539      // When automatically selecting a port, sometimes the chosen port is
    540      // "blocked" from clients. We don't want to use these ports because
    541      // tests will intermittently fail. So, we simply keep trying to to
    542      // get a server socket until a valid port is obtained. We limit
    543      // ourselves to finite attempts just so we don't loop forever.
    544      var socket;
    545      for (var i = 100; i; i--) {
    546        var temp = null;
    547        if (dualStack) {
    548          temp = new ServerSocketDualStack(this._port, maxConnections);
    549        } else if (this._host.includes(":")) {
    550          temp = new ServerSocketIPv6(
    551            this._port,
    552            loopback, // true = localhost, false = everybody
    553            maxConnections
    554          );
    555        } else {
    556          temp = new ServerSocket(
    557            this._port,
    558            loopback, // true = localhost, false = everybody
    559            maxConnections
    560          );
    561        }
    562 
    563        var allowed = Services.io.allowPort(temp.port, "http");
    564        if (!allowed) {
    565          dumpn(
    566            ">>>Warning: obtained ServerSocket listens on a blocked " +
    567              "port: " +
    568              temp.port
    569          );
    570        }
    571 
    572        if (!allowed && this._port == -1) {
    573          dumpn(">>>Throwing away ServerSocket with bad port.");
    574          temp.close();
    575          continue;
    576        }
    577 
    578        socket = temp;
    579        break;
    580      }
    581 
    582      if (!socket) {
    583        throw new Error(
    584          "No socket server available. Are there no available ports?"
    585        );
    586      }
    587 
    588      socket.asyncListen(this);
    589      this._port = socket.port;
    590      this._identity._initialize(socket.port, host, true, dualStack);
    591      this._socket = socket;
    592      dumpn(
    593        ">>> listening on port " +
    594          socket.port +
    595          ", " +
    596          maxConnections +
    597          " pending connections"
    598      );
    599    } catch (e) {
    600      dump("\n!!! could not start server on port " + port + ": " + e + "\n\n");
    601      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
    602    }
    603  },
    604 
    605  //
    606  // see nsIHttpServer.stop
    607  //
    608  stop(callback) {
    609    if (!this._socket) {
    610      throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
    611    }
    612 
    613    // If no argument was provided to stop, return a promise.
    614    let returnValue = undefined;
    615    if (!callback) {
    616      returnValue = new Promise(resolve => {
    617        callback = resolve;
    618      });
    619    }
    620 
    621    this._stopCallback =
    622      typeof callback === "function"
    623        ? callback
    624        : function () {
    625            callback.onStopped();
    626          };
    627 
    628    dumpn(">>> stopping listening on port " + this._socket.port);
    629    this._socket.close();
    630    this._socket = null;
    631 
    632    // We can't have this identity any more, and the port on which we're running
    633    // this server now could be meaningless the next time around.
    634    this._identity._teardown();
    635 
    636    this._doQuit = false;
    637 
    638    // socket-close notification and pending request completion happen async
    639 
    640    return returnValue;
    641  },
    642 
    643  //
    644  // see nsIHttpServer.registerFile
    645  //
    646  registerFile(path, file, handler) {
    647    if (file && (!file.exists() || file.isDirectory())) {
    648      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
    649    }
    650 
    651    this._handler.registerFile(path, file, handler);
    652  },
    653 
    654  //
    655  // see nsIHttpServer.registerDirectory
    656  //
    657  registerDirectory(path, directory) {
    658    // XXX true path validation!
    659    if (
    660      path.charAt(0) != "/" ||
    661      path.charAt(path.length - 1) != "/" ||
    662      (directory && (!directory.exists() || !directory.isDirectory()))
    663    ) {
    664      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
    665    }
    666 
    667    // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping
    668    //     exists!
    669 
    670    this._handler.registerDirectory(path, directory);
    671  },
    672 
    673  //
    674  // see nsIHttpServer.registerPathHandler
    675  //
    676  registerPathHandler(path, handler) {
    677    this._handler.registerPathHandler(path, handler);
    678  },
    679 
    680  //
    681  // see nsIHttpServer.registerPrefixHandler
    682  //
    683  registerPrefixHandler(prefix, handler) {
    684    this._handler.registerPrefixHandler(prefix, handler);
    685  },
    686 
    687  //
    688  // see nsIHttpServer.registerErrorHandler
    689  //
    690  registerErrorHandler(code, handler) {
    691    this._handler.registerErrorHandler(code, handler);
    692  },
    693 
    694  //
    695  // see nsIHttpServer.setIndexHandler
    696  //
    697  setIndexHandler(handler) {
    698    this._handler.setIndexHandler(handler);
    699  },
    700 
    701  //
    702  // see nsIHttpServer.registerContentType
    703  //
    704  registerContentType(ext, type) {
    705    this._handler.registerContentType(ext, type);
    706  },
    707 
    708  get connectionNumber() {
    709    return this._connectionGen;
    710  },
    711 
    712  //
    713  // see nsIHttpServer.serverIdentity
    714  //
    715  get identity() {
    716    return this._identity;
    717  },
    718 
    719  //
    720  // see nsIHttpServer.getState
    721  //
    722  getState(path, k) {
    723    return this._handler._getState(path, k);
    724  },
    725 
    726  //
    727  // see nsIHttpServer.setState
    728  //
    729  setState(path, k, v) {
    730    return this._handler._setState(path, k, v);
    731  },
    732 
    733  //
    734  // see nsIHttpServer.getSharedState
    735  //
    736  getSharedState(k) {
    737    return this._handler._getSharedState(k);
    738  },
    739 
    740  //
    741  // see nsIHttpServer.setSharedState
    742  //
    743  setSharedState(k, v) {
    744    return this._handler._setSharedState(k, v);
    745  },
    746 
    747  //
    748  // see nsIHttpServer.getObjectState
    749  //
    750  getObjectState(k) {
    751    return this._handler._getObjectState(k);
    752  },
    753 
    754  //
    755  // see nsIHttpServer.setObjectState
    756  //
    757  setObjectState(k, v) {
    758    return this._handler._setObjectState(k, v);
    759  },
    760 
    761  get wrappedJSObject() {
    762    return this;
    763  },
    764 
    765  // NSISUPPORTS
    766 
    767  //
    768  // see nsISupports.QueryInterface
    769  //
    770  QueryInterface: ChromeUtils.generateQI([
    771    "nsIHttpServer",
    772    "nsIServerSocketListener",
    773  ]),
    774 
    775  // NON-XPCOM PUBLIC API
    776 
    777  /**
    778   * Returns true iff this server is not running (and is not in the process of
    779   * serving any requests still to be processed when the server was last
    780   * stopped after being run).
    781   */
    782  isStopped() {
    783    return this._socketClosed && !this._hasOpenConnections();
    784  },
    785 
    786  // PRIVATE IMPLEMENTATION
    787 
    788  /** True if this server has any open connections to it, false otherwise. */
    789  _hasOpenConnections() {
    790    //
    791    // If we have any open connections, they're tracked as numeric properties on
    792    // |this._connections|.  The non-standard __count__ property could be used
    793    // to check whether there are any properties, but standard-wise, even
    794    // looking forward to ES5, there's no less ugly yet still O(1) way to do
    795    // this.
    796    //
    797    for (var n in this._connections) {
    798      return true;
    799    }
    800    return false;
    801  },
    802 
    803  /** Calls the server-stopped callback provided when stop() was called. */
    804  _notifyStopped() {
    805    NS_ASSERT(this._stopCallback !== null, "double-notifying?");
    806    NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now");
    807 
    808    //
    809    // NB: We have to grab this now, null out the member, *then* call the
    810    //     callback here, or otherwise the callback could (indirectly) futz with
    811    //     this._stopCallback by starting and immediately stopping this, at
    812    //     which point we'd be nulling out a field we no longer have a right to
    813    //     modify.
    814    //
    815    var callback = this._stopCallback;
    816    this._stopCallback = null;
    817    try {
    818      callback();
    819    } catch (e) {
    820      // not throwing because this is specified as being usually (but not
    821      // always) asynchronous
    822      dump("!!! error running onStopped callback: " + e + "\n");
    823    }
    824  },
    825 
    826  /**
    827   * Notifies this server that the given connection has been closed.
    828   *
    829   * @param connection : Connection
    830   *   the connection that was closed
    831   */
    832  _connectionClosed(connection) {
    833    NS_ASSERT(
    834      connection.number in this._connections,
    835      "closing a connection " +
    836        this +
    837        " that we never added to the " +
    838        "set of open connections?"
    839    );
    840    NS_ASSERT(
    841      this._connections[connection.number] === connection,
    842      "connection number mismatch?  " + this._connections[connection.number]
    843    );
    844    delete this._connections[connection.number];
    845 
    846    // Fire a pending server-stopped notification if it's our responsibility.
    847    if (!this._hasOpenConnections() && this._socketClosed) {
    848      this._notifyStopped();
    849    }
    850  },
    851 
    852  /**
    853   * Requests that the server be shut down when possible.
    854   */
    855  _requestQuit() {
    856    dumpn(">>> requesting a quit");
    857    dumpStack();
    858    this._doQuit = true;
    859  },
    860 };
    861 
    862 export var HttpServer = nsHttpServer;
    863 
    864 //
    865 // RFC 2396 section 3.2.2:
    866 //
    867 // host        = hostname | IPv4address
    868 // hostname    = *( domainlabel "." ) toplabel [ "." ]
    869 // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
    870 // toplabel    = alpha | alpha *( alphanum | "-" ) alphanum
    871 // IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit
    872 //
    873 // IPv6 addresses are notably lacking in the above definition of 'host'.
    874 // RFC 2732 section 3 extends the host definition:
    875 // host          = hostname | IPv4address | IPv6reference
    876 // ipv6reference = "[" IPv6address "]"
    877 //
    878 // RFC 3986 supersedes RFC 2732 and offers a more precise definition of a IPv6
    879 // address. For simplicity, the regexp below captures all canonical IPv6
    880 // addresses (e.g. [::1]), but may also match valid non-canonical IPv6 addresses
    881 // (e.g. [::127.0.0.1]) and even invalid bracketed addresses ([::], [99999::]).
    882 
    883 const HOST_REGEX = new RegExp(
    884  "^(?:" +
    885    // *( domainlabel "." )
    886    "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" +
    887    // toplabel [ "." ]
    888    "[a-z](?:[a-z0-9-]*[a-z0-9])?\\.?" +
    889    "|" +
    890    // IPv4 address
    891    "\\d+\\.\\d+\\.\\d+\\.\\d+" +
    892    "|" +
    893    // IPv6 addresses (e.g. [::1])
    894    "\\[[:0-9a-f]+\\]" +
    895    ")$",
    896  "i"
    897 );
    898 
    899 /**
    900 * Represents the identity of a server.  An identity consists of a set of
    901 * (scheme, host, port) tuples denoted as locations (allowing a single server to
    902 * serve multiple sites or to be used behind both HTTP and HTTPS proxies for any
    903 * host/port).  Any incoming request must be to one of these locations, or it
    904 * will be rejected with an HTTP 400 error.  One location, denoted as the
    905 * primary location, is the location assigned in contexts where a location
    906 * cannot otherwise be endogenously derived, such as for HTTP/1.0 requests.
    907 *
    908 * A single identity may contain at most one location per unique host/port pair;
    909 * other than that, no restrictions are placed upon what locations may
    910 * constitute an identity.
    911 */
    912 function ServerIdentity() {
    913  /** The scheme of the primary location. */
    914  this._primaryScheme = "http";
    915 
    916  /** The hostname of the primary location. */
    917  this._primaryHost = "127.0.0.1";
    918 
    919  /** The port number of the primary location. */
    920  this._primaryPort = -1;
    921 
    922  /**
    923   * The current port number for the corresponding server, stored so that a new
    924   * primary location can always be set if the current one is removed.
    925   */
    926  this._defaultPort = -1;
    927 
    928  /**
    929   * Maps hosts to maps of ports to schemes, e.g. the following would represent
    930   * https://example.com:789/ and http://example.org/:
    931   *
    932   *   {
    933   *     "xexample.com": { 789: "https" },
    934   *     "xexample.org": { 80: "http" }
    935   *   }
    936   *
    937   * Note the "x" prefix on hostnames, which prevents collisions with special
    938   * JS names like "prototype".
    939   */
    940  this._locations = { xlocalhost: {} };
    941 }
    942 ServerIdentity.prototype = {
    943  // NSIHTTPSERVERIDENTITY
    944 
    945  //
    946  // see nsIHttpServerIdentity.primaryScheme
    947  //
    948  get primaryScheme() {
    949    if (this._primaryPort === -1) {
    950      throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
    951    }
    952    return this._primaryScheme;
    953  },
    954 
    955  //
    956  // see nsIHttpServerIdentity.primaryHost
    957  //
    958  get primaryHost() {
    959    if (this._primaryPort === -1) {
    960      throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
    961    }
    962    return this._primaryHost;
    963  },
    964 
    965  //
    966  // see nsIHttpServerIdentity.primaryPort
    967  //
    968  get primaryPort() {
    969    if (this._primaryPort === -1) {
    970      throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
    971    }
    972    return this._primaryPort;
    973  },
    974 
    975  //
    976  // see nsIHttpServerIdentity.add
    977  //
    978  add(scheme, host, port) {
    979    this._validate(scheme, host, port);
    980 
    981    var entry = this._locations["x" + host];
    982    if (!entry) {
    983      this._locations["x" + host] = entry = {};
    984    }
    985 
    986    entry[port] = scheme;
    987  },
    988 
    989  //
    990  // see nsIHttpServerIdentity.remove
    991  //
    992  remove(scheme, host, port) {
    993    this._validate(scheme, host, port);
    994 
    995    var entry = this._locations["x" + host];
    996    if (!entry) {
    997      return false;
    998    }
    999 
   1000    var present = port in entry;
   1001    delete entry[port];
   1002 
   1003    if (
   1004      this._primaryScheme == scheme &&
   1005      this._primaryHost == host &&
   1006      this._primaryPort == port &&
   1007      this._defaultPort !== -1
   1008    ) {
   1009      // Always keep at least one identity in existence at any time, unless
   1010      // we're in the process of shutting down (the last condition above).
   1011      this._primaryPort = -1;
   1012      this._initialize(this._defaultPort, host, false);
   1013    }
   1014 
   1015    return present;
   1016  },
   1017 
   1018  //
   1019  // see nsIHttpServerIdentity.has
   1020  //
   1021  has(scheme, host, port) {
   1022    this._validate(scheme, host, port);
   1023 
   1024    return (
   1025      "x" + host in this._locations &&
   1026      scheme === this._locations["x" + host][port]
   1027    );
   1028  },
   1029 
   1030  //
   1031  // see nsIHttpServerIdentity.has
   1032  //
   1033  getScheme(host, port) {
   1034    this._validate("http", host, port);
   1035 
   1036    var entry = this._locations["x" + host];
   1037    if (!entry) {
   1038      return "";
   1039    }
   1040 
   1041    return entry[port] || "";
   1042  },
   1043 
   1044  //
   1045  // see nsIHttpServerIdentity.setPrimary
   1046  //
   1047  setPrimary(scheme, host, port) {
   1048    this._validate(scheme, host, port);
   1049 
   1050    this.add(scheme, host, port);
   1051 
   1052    this._primaryScheme = scheme;
   1053    this._primaryHost = host;
   1054    this._primaryPort = port;
   1055  },
   1056 
   1057  // NSISUPPORTS
   1058 
   1059  //
   1060  // see nsISupports.QueryInterface
   1061  //
   1062  QueryInterface: ChromeUtils.generateQI(["nsIHttpServerIdentity"]),
   1063 
   1064  // PRIVATE IMPLEMENTATION
   1065 
   1066  /**
   1067   * Initializes the primary name for the corresponding server, based on the
   1068   * provided port number.
   1069   */
   1070  _initialize(port, host, addSecondaryDefault, dualStack) {
   1071    this._host = host;
   1072    if (this._primaryPort !== -1) {
   1073      this.add("http", host, port);
   1074    } else {
   1075      this.setPrimary("http", "localhost", port);
   1076    }
   1077    this._defaultPort = port;
   1078 
   1079    // Only add this if we're being called at server startup
   1080    if (addSecondaryDefault && host != "127.0.0.1") {
   1081      if (host.includes(":")) {
   1082        this.add("http", "[::1]", port);
   1083        if (dualStack) {
   1084          this.add("http", "127.0.0.1", port);
   1085        }
   1086      } else {
   1087        this.add("http", "127.0.0.1", port);
   1088      }
   1089    }
   1090  },
   1091 
   1092  /**
   1093   * Called at server shutdown time, unsets the primary location only if it was
   1094   * the default-assigned location and removes the default location from the
   1095   * set of locations used.
   1096   */
   1097  _teardown() {
   1098    if (this._host != "127.0.0.1") {
   1099      // Not the default primary location, nothing special to do here
   1100      this.remove("http", "127.0.0.1", this._defaultPort);
   1101    }
   1102 
   1103    // This is a *very* tricky bit of reasoning here; make absolutely sure the
   1104    // tests for this code pass before you commit changes to it.
   1105    if (
   1106      this._primaryScheme == "http" &&
   1107      this._primaryHost == this._host &&
   1108      this._primaryPort == this._defaultPort
   1109    ) {
   1110      // Make sure we don't trigger the readding logic in .remove(), then remove
   1111      // the default location.
   1112      var port = this._defaultPort;
   1113      this._defaultPort = -1;
   1114      this.remove("http", this._host, port);
   1115 
   1116      // Ensure a server start triggers the setPrimary() path in ._initialize()
   1117      this._primaryPort = -1;
   1118    } else {
   1119      // No reason not to remove directly as it's not our primary location
   1120      this.remove("http", this._host, this._defaultPort);
   1121    }
   1122  },
   1123 
   1124  /**
   1125   * Ensures scheme, host, and port are all valid with respect to RFC 2396.
   1126   *
   1127   * @throws NS_ERROR_ILLEGAL_VALUE
   1128   *   if any argument doesn't match the corresponding production
   1129   */
   1130  _validate(scheme, host, port) {
   1131    if (scheme !== "http" && scheme !== "https") {
   1132      dumpn("*** server only supports http/https schemes: '" + scheme + "'");
   1133      dumpStack();
   1134      throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
   1135    }
   1136    if (!HOST_REGEX.test(host)) {
   1137      dumpn("*** unexpected host: '" + host + "'");
   1138      throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
   1139    }
   1140    if (port < 0 || port > 65535) {
   1141      dumpn("*** unexpected port: '" + port + "'");
   1142      throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
   1143    }
   1144  },
   1145 };
   1146 
   1147 /**
   1148 * Represents a connection to the server (and possibly in the future the thread
   1149 * on which the connection is processed).
   1150 *
   1151 * @param input : nsIInputStream
   1152 *   stream from which incoming data on the connection is read
   1153 * @param output : nsIOutputStream
   1154 *   stream to write data out the connection
   1155 * @param server : nsHttpServer
   1156 *   the server handling the connection
   1157 * @param port : int
   1158 *   the port on which the server is running
   1159 * @param outgoingPort : int
   1160 *   the outgoing port used by this connection
   1161 * @param number : uint
   1162 *   a serial number used to uniquely identify this connection
   1163 */
   1164 function Connection(
   1165  input,
   1166  output,
   1167  server,
   1168  port,
   1169  outgoingPort,
   1170  number,
   1171  transport
   1172 ) {
   1173  dumpn("*** opening new connection " + number + " on port " + outgoingPort);
   1174 
   1175  /** Stream of incoming data. */
   1176  this.input = input;
   1177 
   1178  /** Stream for outgoing data. */
   1179  this.output = output;
   1180 
   1181  /** The server associated with this request. */
   1182  this.server = server;
   1183 
   1184  /** The port on which the server is running. */
   1185  this.port = port;
   1186 
   1187  /** The outgoing poort used by this connection. */
   1188  this._outgoingPort = outgoingPort;
   1189 
   1190  /** The serial number of this connection. */
   1191  this.number = number;
   1192 
   1193  /** Reference to the underlying transport. */
   1194  this.transport = transport;
   1195 
   1196  /**
   1197   * The request for which a response is being generated, null if the
   1198   * incoming request has not been fully received or if it had errors.
   1199   */
   1200  this.request = null;
   1201 
   1202  /**
   1203   * This allows a connection to disambiguate between a peer initiating a
   1204   * close and the socket being forced closed on shutdown.
   1205   */
   1206  this._closed = false;
   1207 
   1208  /** State variable for debugging. */
   1209  this._processed = false;
   1210 
   1211  /** whether or not 1st line of request has been received */
   1212  this._requestStarted = false;
   1213 }
   1214 Connection.prototype = {
   1215  /** Closes this connection's input/output streams. */
   1216  close() {
   1217    if (this._closed) {
   1218      return;
   1219    }
   1220 
   1221    dumpn(
   1222      "*** closing connection " + this.number + " on port " + this._outgoingPort
   1223    );
   1224 
   1225    this.input.close();
   1226    this.output.close();
   1227    this._closed = true;
   1228 
   1229    var server = this.server;
   1230    server._connectionClosed(this);
   1231 
   1232    // If an error triggered a server shutdown, act on it now
   1233    if (server._doQuit) {
   1234      server.stop(function () {
   1235        /* not like we can do anything better */
   1236      });
   1237    }
   1238  },
   1239 
   1240  /**
   1241   * Initiates processing of this connection, using the data in the given
   1242   * request.
   1243   *
   1244   * @param request : Request
   1245   *   the request which should be processed
   1246   */
   1247  process(request) {
   1248    NS_ASSERT(!this._closed && !this._processed);
   1249 
   1250    this._processed = true;
   1251 
   1252    this.request = request;
   1253    this.server._handler.handleResponse(this);
   1254  },
   1255 
   1256  /**
   1257   * Initiates processing of this connection, generating a response with the
   1258   * given HTTP error code.
   1259   *
   1260   * @param code : uint
   1261   *   an HTTP code, so in the range [0, 1000)
   1262   * @param request : Request
   1263   *   incomplete data about the incoming request (since there were errors
   1264   *   during its processing
   1265   */
   1266  processError(code, request) {
   1267    NS_ASSERT(!this._closed && !this._processed);
   1268 
   1269    this._processed = true;
   1270    this.request = request;
   1271    this.server._handler.handleError(code, this);
   1272  },
   1273 
   1274  /** Converts this to a string for debugging purposes. */
   1275  toString() {
   1276    return (
   1277      "<Connection(" +
   1278      this.number +
   1279      (this.request ? ", " + this.request.path : "") +
   1280      "): " +
   1281      (this._closed ? "closed" : "open") +
   1282      ">"
   1283    );
   1284  },
   1285 
   1286  requestStarted() {
   1287    this._requestStarted = true;
   1288  },
   1289 };
   1290 
   1291 /** Returns an array of count bytes from the given input stream. */
   1292 function readBytes(inputStream, count) {
   1293  return new BinaryInputStream(inputStream).readByteArray(count);
   1294 }
   1295 
   1296 /** Request reader processing states; see RequestReader for details. */
   1297 const READER_IN_REQUEST_LINE = 0;
   1298 const READER_IN_HEADERS = 1;
   1299 const READER_IN_BODY = 2;
   1300 const READER_FINISHED = 3;
   1301 
   1302 /**
   1303 * Reads incoming request data asynchronously, does any necessary preprocessing,
   1304 * and forwards it to the request handler.  Processing occurs in three states:
   1305 *
   1306 *   READER_IN_REQUEST_LINE     Reading the request's status line
   1307 *   READER_IN_HEADERS          Reading headers in the request
   1308 *   READER_IN_BODY             Reading the body of the request
   1309 *   READER_FINISHED            Entire request has been read and processed
   1310 *
   1311 * During the first two stages, initial metadata about the request is gathered
   1312 * into a Request object.  Once the status line and headers have been processed,
   1313 * we start processing the body of the request into the Request.  Finally, when
   1314 * the entire body has been read, we create a Response and hand it off to the
   1315 * ServerHandler to be given to the appropriate request handler.
   1316 *
   1317 * @param connection : Connection
   1318 *   the connection for the request being read
   1319 */
   1320 function RequestReader(connection) {
   1321  /** Connection metadata for this request. */
   1322  this._connection = connection;
   1323 
   1324  /**
   1325   * A container providing line-by-line access to the raw bytes that make up the
   1326   * data which has been read from the connection but has not yet been acted
   1327   * upon (by passing it to the request handler or by extracting request
   1328   * metadata from it).
   1329   */
   1330  this._data = new LineData();
   1331 
   1332  /**
   1333   * The amount of data remaining to be read from the body of this request.
   1334   * After all headers in the request have been read this is the value in the
   1335   * Content-Length header, but as the body is read its value decreases to zero.
   1336   */
   1337  this._contentLength = 0;
   1338 
   1339  /** The current state of parsing the incoming request. */
   1340  this._state = READER_IN_REQUEST_LINE;
   1341 
   1342  /** Metadata constructed from the incoming request for the request handler. */
   1343  this._metadata = new Request(connection.port);
   1344 
   1345  /**
   1346   * Used to preserve state if we run out of line data midway through a
   1347   * multi-line header.  _lastHeaderName stores the name of the header, while
   1348   * _lastHeaderValue stores the value we've seen so far for the header.
   1349   *
   1350   * These fields are always either both undefined or both strings.
   1351   */
   1352  this._lastHeaderName = this._lastHeaderValue = undefined;
   1353 }
   1354 RequestReader.prototype = {
   1355  // NSIINPUTSTREAMCALLBACK
   1356 
   1357  /**
   1358   * Called when more data from the incoming request is available.  This method
   1359   * then reads the available data from input and deals with that data as
   1360   * necessary, depending upon the syntax of already-downloaded data.
   1361   *
   1362   * @param input : nsIAsyncInputStream
   1363   *   the stream of incoming data from the connection
   1364   */
   1365  onInputStreamReady(input) {
   1366    dumpn(
   1367      "*** onInputStreamReady(input=" +
   1368        input +
   1369        ") on thread " +
   1370        Services.tm.currentThread +
   1371        " (main is " +
   1372        Services.tm.mainThread +
   1373        ")"
   1374    );
   1375    dumpn("*** this._state == " + this._state);
   1376 
   1377    // Handle cases where we get more data after a request error has been
   1378    // discovered but *before* we can close the connection.
   1379    var data = this._data;
   1380    if (!data) {
   1381      return;
   1382    }
   1383 
   1384    try {
   1385      data.appendBytes(readBytes(input, input.available()));
   1386    } catch (e) {
   1387      if (streamClosed(e)) {
   1388        dumpn(
   1389          "*** WARNING: unexpected error when reading from socket; will " +
   1390            "be treated as if the input stream had been closed"
   1391        );
   1392        dumpn("*** WARNING: actual error was: " + e);
   1393      }
   1394 
   1395      // We've lost a race -- input has been closed, but we're still expecting
   1396      // to read more data.  available() will throw in this case, and since
   1397      // we're dead in the water now, destroy the connection.
   1398      dumpn(
   1399        "*** onInputStreamReady called on a closed input, destroying " +
   1400          "connection"
   1401      );
   1402      this._connection.close();
   1403      return;
   1404    }
   1405 
   1406    switch (this._state) {
   1407      default:
   1408        NS_ASSERT(false, "invalid state: " + this._state);
   1409        break;
   1410 
   1411      case READER_IN_REQUEST_LINE:
   1412        if (!this._processRequestLine()) {
   1413          break;
   1414        }
   1415      /* fall through */
   1416 
   1417      case READER_IN_HEADERS:
   1418        if (!this._processHeaders()) {
   1419          break;
   1420        }
   1421      /* fall through */
   1422 
   1423      case READER_IN_BODY:
   1424        this._processBody();
   1425    }
   1426 
   1427    if (this._state != READER_FINISHED) {
   1428      input.asyncWait(this, 0, 0, Services.tm.currentThread);
   1429    }
   1430  },
   1431 
   1432  //
   1433  // see nsISupports.QueryInterface
   1434  //
   1435  QueryInterface: ChromeUtils.generateQI(["nsIInputStreamCallback"]),
   1436 
   1437  // PRIVATE API
   1438 
   1439  /**
   1440   * Processes unprocessed, downloaded data as a request line.
   1441   *
   1442   * @returns boolean
   1443   *   true iff the request line has been fully processed
   1444   */
   1445  _processRequestLine() {
   1446    NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
   1447 
   1448    // Servers SHOULD ignore any empty line(s) received where a Request-Line
   1449    // is expected (section 4.1).
   1450    var data = this._data;
   1451    var line = {};
   1452    var readSuccess;
   1453    while ((readSuccess = data.readLine(line)) && line.value == "") {
   1454      dumpn("*** ignoring beginning blank line...");
   1455    }
   1456 
   1457    // if we don't have a full line, wait until we do
   1458    if (!readSuccess) {
   1459      return false;
   1460    }
   1461 
   1462    // we have the first non-blank line
   1463    try {
   1464      this._parseRequestLine(line.value);
   1465      this._state = READER_IN_HEADERS;
   1466      this._connection.requestStarted();
   1467      return true;
   1468    } catch (e) {
   1469      this._handleError(e);
   1470      return false;
   1471    }
   1472  },
   1473 
   1474  /**
   1475   * Processes stored data, assuming it is either at the beginning or in
   1476   * the middle of processing request headers.
   1477   *
   1478   * @returns boolean
   1479   *   true iff header data in the request has been fully processed
   1480   */
   1481  _processHeaders() {
   1482    NS_ASSERT(this._state == READER_IN_HEADERS);
   1483 
   1484    // XXX things to fix here:
   1485    //
   1486    // - need to support RFC 2047-encoded non-US-ASCII characters
   1487 
   1488    try {
   1489      var done = this._parseHeaders();
   1490      if (done) {
   1491        var request = this._metadata;
   1492 
   1493        // XXX this is wrong for requests with transfer-encodings applied to
   1494        //     them, particularly chunked (which by its nature can have no
   1495        //     meaningful Content-Length header)!
   1496        this._contentLength = request.hasHeader("Content-Length")
   1497          ? parseInt(request.getHeader("Content-Length"), 10)
   1498          : 0;
   1499        dumpn("_processHeaders, Content-length=" + this._contentLength);
   1500 
   1501        this._state = READER_IN_BODY;
   1502      }
   1503      return done;
   1504    } catch (e) {
   1505      this._handleError(e);
   1506      return false;
   1507    }
   1508  },
   1509 
   1510  /**
   1511   * Processes stored data, assuming it is either at the beginning or in
   1512   * the middle of processing the request body.
   1513   *
   1514   * @returns boolean
   1515   *   true iff the request body has been fully processed
   1516   */
   1517  _processBody() {
   1518    NS_ASSERT(this._state == READER_IN_BODY);
   1519 
   1520    // XXX handle chunked transfer-coding request bodies!
   1521 
   1522    try {
   1523      if (this._contentLength > 0) {
   1524        var data = this._data.purge();
   1525        var count = Math.min(data.length, this._contentLength);
   1526        dumpn(
   1527          "*** loading data=" +
   1528            data +
   1529            " len=" +
   1530            data.length +
   1531            " excess=" +
   1532            (data.length - count)
   1533        );
   1534        data.length = count;
   1535 
   1536        var bos = new BinaryOutputStream(this._metadata._bodyOutputStream);
   1537        bos.writeByteArray(data);
   1538        this._contentLength -= count;
   1539      }
   1540 
   1541      dumpn("*** remaining body data len=" + this._contentLength);
   1542      if (this._contentLength == 0) {
   1543        this._validateRequest();
   1544        this._state = READER_FINISHED;
   1545        this._handleResponse();
   1546        return true;
   1547      }
   1548 
   1549      return false;
   1550    } catch (e) {
   1551      this._handleError(e);
   1552      return false;
   1553    }
   1554  },
   1555 
   1556  /**
   1557   * Does various post-header checks on the data in this request.
   1558   *
   1559   * @throws : HttpError
   1560   *   if the request was malformed in some way
   1561   */
   1562  _validateRequest() {
   1563    NS_ASSERT(this._state == READER_IN_BODY);
   1564 
   1565    dumpn("*** _validateRequest");
   1566 
   1567    var metadata = this._metadata;
   1568    var headers = metadata._headers;
   1569 
   1570    // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header
   1571    var identity = this._connection.server.identity;
   1572    if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) {
   1573      if (!headers.hasHeader("Host")) {
   1574        dumpn("*** malformed HTTP/1.1 or greater request with no Host header!");
   1575        throw HTTP_400;
   1576      }
   1577 
   1578      // If the Request-URI wasn't absolute, then we need to determine our host.
   1579      // We have to determine what scheme was used to access us based on the
   1580      // server identity data at this point, because the request just doesn't
   1581      // contain enough data on its own to do this, sadly.
   1582      if (!metadata._host) {
   1583        var host, port;
   1584        var hostPort = headers.getHeader("Host");
   1585        var colon = hostPort.lastIndexOf(":");
   1586        if (hostPort.lastIndexOf("]") > colon) {
   1587          colon = -1;
   1588        }
   1589        if (colon < 0) {
   1590          host = hostPort;
   1591          port = "";
   1592        } else {
   1593          host = hostPort.substring(0, colon);
   1594          port = hostPort.substring(colon + 1);
   1595        }
   1596 
   1597        // NB: We allow an empty port here because, oddly, a colon may be
   1598        //     present even without a port number, e.g. "example.com:"; in this
   1599        //     case the default port applies.
   1600        if (!HOST_REGEX.test(host) || !/^\d*$/.test(port)) {
   1601          dumpn(
   1602            "*** malformed hostname (" +
   1603              hostPort +
   1604              ") in Host " +
   1605              "header, 400 time"
   1606          );
   1607          throw HTTP_400;
   1608        }
   1609 
   1610        // If we're not given a port, we're stuck, because we don't know what
   1611        // scheme to use to look up the correct port here, in general.  Since
   1612        // the HTTPS case requires a tunnel/proxy and thus requires that the
   1613        // requested URI be absolute (and thus contain the necessary
   1614        // information), let's assume HTTP will prevail and use that.
   1615        port = +port || 80;
   1616 
   1617        var scheme = identity.getScheme(host, port);
   1618        if (!scheme) {
   1619          dumpn(
   1620            "*** unrecognized hostname (" +
   1621              hostPort +
   1622              ") in Host " +
   1623              "header, 400 time"
   1624          );
   1625          throw HTTP_400;
   1626        }
   1627 
   1628        metadata._scheme = scheme;
   1629        metadata._host = host;
   1630        metadata._port = port;
   1631      }
   1632    } else {
   1633      NS_ASSERT(
   1634        metadata._host === undefined,
   1635        "HTTP/1.0 doesn't allow absolute paths in the request line!"
   1636      );
   1637 
   1638      metadata._scheme = identity.primaryScheme;
   1639      metadata._host = identity.primaryHost;
   1640      metadata._port = identity.primaryPort;
   1641    }
   1642 
   1643    NS_ASSERT(
   1644      identity.has(metadata._scheme, metadata._host, metadata._port),
   1645      "must have a location we recognize by now!"
   1646    );
   1647  },
   1648 
   1649  /**
   1650   * Handles responses in case of error, either in the server or in the request.
   1651   *
   1652   * @param e
   1653   *   the specific error encountered, which is an HttpError in the case where
   1654   *   the request is in some way invalid or cannot be fulfilled; if this isn't
   1655   *   an HttpError we're going to be paranoid and shut down, because that
   1656   *   shouldn't happen, ever
   1657   */
   1658  _handleError(e) {
   1659    // Don't fall back into normal processing!
   1660    this._state = READER_FINISHED;
   1661 
   1662    var server = this._connection.server;
   1663    if (e instanceof HttpError) {
   1664      var code = e.code;
   1665    } else {
   1666      dumpn(
   1667        "!!! UNEXPECTED ERROR: " +
   1668          e +
   1669          (e.lineNumber ? ", line " + e.lineNumber : "")
   1670      );
   1671 
   1672      // no idea what happened -- be paranoid and shut down
   1673      code = 500;
   1674      server._requestQuit();
   1675    }
   1676 
   1677    // make attempted reuse of data an error
   1678    this._data = null;
   1679 
   1680    this._connection.processError(code, this._metadata);
   1681  },
   1682 
   1683  /**
   1684   * Now that we've read the request line and headers, we can actually hand off
   1685   * the request to be handled.
   1686   *
   1687   * This method is called once per request, after the request line and all
   1688   * headers and the body, if any, have been received.
   1689   */
   1690  _handleResponse() {
   1691    NS_ASSERT(this._state == READER_FINISHED);
   1692 
   1693    // We don't need the line-based data any more, so make attempted reuse an
   1694    // error.
   1695    this._data = null;
   1696 
   1697    this._connection.process(this._metadata);
   1698  },
   1699 
   1700  // PARSING
   1701 
   1702  /**
   1703   * Parses the request line for the HTTP request associated with this.
   1704   *
   1705   * @param line : string
   1706   *   the request line
   1707   */
   1708  _parseRequestLine(line) {
   1709    NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
   1710 
   1711    dumpn("*** _parseRequestLine('" + line + "')");
   1712 
   1713    var metadata = this._metadata;
   1714 
   1715    // clients and servers SHOULD accept any amount of SP or HT characters
   1716    // between fields, even though only a single SP is required (section 19.3)
   1717    var request = line.split(/[ \t]+/);
   1718    if (!request || request.length != 3) {
   1719      dumpn("*** No request in line");
   1720      throw HTTP_400;
   1721    }
   1722 
   1723    metadata._method = request[0];
   1724 
   1725    // get the HTTP version
   1726    var ver = request[2];
   1727    var match = ver.match(/^HTTP\/(\d+\.\d+)$/);
   1728    if (!match) {
   1729      dumpn("*** No HTTP version in line");
   1730      throw HTTP_400;
   1731    }
   1732 
   1733    // determine HTTP version
   1734    try {
   1735      metadata._httpVersion = new nsHttpVersion(match[1]);
   1736      if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0)) {
   1737        throw new Error("unsupported HTTP version");
   1738      }
   1739    } catch (e) {
   1740      // we support HTTP/1.0 and HTTP/1.1 only
   1741      throw HTTP_501;
   1742    }
   1743 
   1744    var fullPath = request[1];
   1745 
   1746    if (metadata._method == "CONNECT") {
   1747      metadata._path = "CONNECT";
   1748      metadata._scheme = "https";
   1749      [metadata._host, metadata._port] = fullPath.split(":");
   1750      return;
   1751    }
   1752 
   1753    var serverIdentity = this._connection.server.identity;
   1754    var scheme, host, port;
   1755 
   1756    if (fullPath.charAt(0) != "/") {
   1757      // No absolute paths in the request line in HTTP prior to 1.1
   1758      if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) {
   1759        dumpn("*** Metadata version too low");
   1760        throw HTTP_400;
   1761      }
   1762 
   1763      try {
   1764        var uri = Services.io.newURI(fullPath);
   1765        fullPath = uri.pathQueryRef;
   1766        scheme = uri.scheme;
   1767        host = uri.asciiHost;
   1768        if (host.includes(":")) {
   1769          // If the host still contains a ":", then it is an IPv6 address.
   1770          // IPv6 addresses-as-host are registered with brackets, so we need to
   1771          // wrap the host in brackets because nsIURI's host lacks them.
   1772          // This inconsistency in nsStandardURL is tracked at bug 1195459.
   1773          host = `[${host}]`;
   1774        }
   1775        metadata._host = host;
   1776        port = uri.port;
   1777        if (port === -1) {
   1778          if (scheme === "http") {
   1779            port = 80;
   1780          } else if (scheme === "https") {
   1781            port = 443;
   1782          } else {
   1783            dumpn("*** Unknown scheme: " + scheme);
   1784            throw HTTP_400;
   1785          }
   1786        }
   1787      } catch (e) {
   1788        // If the host is not a valid host on the server, the response MUST be a
   1789        // 400 (Bad Request) error message (section 5.2).  Alternately, the URI
   1790        // is malformed.
   1791        dumpn("*** Threw when dealing with URI: " + e);
   1792        throw HTTP_400;
   1793      }
   1794 
   1795      if (
   1796        !serverIdentity.has(scheme, host, port) ||
   1797        fullPath.charAt(0) != "/"
   1798      ) {
   1799        dumpn("*** serverIdentity unknown or path does not start with '/'");
   1800        throw HTTP_400;
   1801      }
   1802    }
   1803 
   1804    var splitter = fullPath.indexOf("?");
   1805    if (splitter < 0) {
   1806      // _queryString already set in ctor
   1807      metadata._path = fullPath;
   1808    } else {
   1809      metadata._path = fullPath.substring(0, splitter);
   1810      metadata._queryString = fullPath.substring(splitter + 1);
   1811    }
   1812 
   1813    metadata._scheme = scheme;
   1814    metadata._host = host;
   1815    metadata._port = port;
   1816  },
   1817 
   1818  /**
   1819   * Parses all available HTTP headers in this until the header-ending CRLFCRLF,
   1820   * adding them to the store of headers in the request.
   1821   *
   1822   * @throws
   1823   *   HTTP_400 if the headers are malformed
   1824   * @returns boolean
   1825   *   true if all headers have now been processed, false otherwise
   1826   */
   1827  _parseHeaders() {
   1828    NS_ASSERT(this._state == READER_IN_HEADERS);
   1829 
   1830    dumpn("*** _parseHeaders");
   1831 
   1832    var data = this._data;
   1833 
   1834    var headers = this._metadata._headers;
   1835    var lastName = this._lastHeaderName;
   1836    var lastVal = this._lastHeaderValue;
   1837 
   1838    var line = {};
   1839    // eslint-disable-next-line no-constant-condition
   1840    while (true) {
   1841      dumpn("*** Last name: '" + lastName + "'");
   1842      dumpn("*** Last val: '" + lastVal + "'");
   1843      NS_ASSERT(
   1844        !((lastVal === undefined) ^ (lastName === undefined)),
   1845        lastName === undefined
   1846          ? "lastVal without lastName?  lastVal: '" + lastVal + "'"
   1847          : "lastName without lastVal?  lastName: '" + lastName + "'"
   1848      );
   1849 
   1850      if (!data.readLine(line)) {
   1851        // save any data we have from the header we might still be processing
   1852        this._lastHeaderName = lastName;
   1853        this._lastHeaderValue = lastVal;
   1854        return false;
   1855      }
   1856 
   1857      var lineText = line.value;
   1858      dumpn("*** Line text: '" + lineText + "'");
   1859      var firstChar = lineText.charAt(0);
   1860 
   1861      // blank line means end of headers
   1862      if (lineText == "") {
   1863        // we're finished with the previous header
   1864        if (lastName) {
   1865          try {
   1866            headers.setHeader(lastName, lastVal, true);
   1867          } catch (e) {
   1868            dumpn("*** setHeader threw on last header, e == " + e);
   1869            throw HTTP_400;
   1870          }
   1871        } else {
   1872          // no headers in request -- valid for HTTP/1.0 requests
   1873        }
   1874 
   1875        // either way, we're done processing headers
   1876        this._state = READER_IN_BODY;
   1877        return true;
   1878      } else if (firstChar == " " || firstChar == "\t") {
   1879        // multi-line header if we've already seen a header line
   1880        if (!lastName) {
   1881          dumpn("We don't have a header to continue!");
   1882          throw HTTP_400;
   1883        }
   1884 
   1885        // append this line's text to the value; starts with SP/HT, so no need
   1886        // for separating whitespace
   1887        lastVal += lineText;
   1888      } else {
   1889        // we have a new header, so set the old one (if one existed)
   1890        if (lastName) {
   1891          try {
   1892            headers.setHeader(lastName, lastVal, true);
   1893          } catch (e) {
   1894            dumpn("*** setHeader threw on a header, e == " + e);
   1895            throw HTTP_400;
   1896          }
   1897        }
   1898 
   1899        var colon = lineText.indexOf(":"); // first colon must be splitter
   1900        if (colon < 1) {
   1901          dumpn("*** No colon or missing header field-name");
   1902          throw HTTP_400;
   1903        }
   1904 
   1905        // set header name, value (to be set in the next loop, usually)
   1906        lastName = lineText.substring(0, colon);
   1907        lastVal = lineText.substring(colon + 1);
   1908      } // empty, continuation, start of header
   1909    } // while (true)
   1910  },
   1911 };
   1912 
   1913 /** The character codes for CR and LF. */
   1914 const CR = 0x0d,
   1915  LF = 0x0a;
   1916 
   1917 /**
   1918 * Calculates the number of characters before the first CRLF pair in array, or
   1919 * -1 if the array contains no CRLF pair.
   1920 *
   1921 * @param array : Array
   1922 *   an array of numbers in the range [0, 256), each representing a single
   1923 *   character; the first CRLF is the lowest index i where
   1924 *   |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|,
   1925 *   if such an |i| exists, and -1 otherwise
   1926 * @param start : uint
   1927 *   start index from which to begin searching in array
   1928 * @returns int
   1929 *   the index of the first CRLF if any were present, -1 otherwise
   1930 */
   1931 function findCRLF(array, start) {
   1932  for (var i = array.indexOf(CR, start); i >= 0; i = array.indexOf(CR, i + 1)) {
   1933    if (array[i + 1] == LF) {
   1934      return i;
   1935    }
   1936  }
   1937  return -1;
   1938 }
   1939 
   1940 /**
   1941 * A container which provides line-by-line access to the arrays of bytes with
   1942 * which it is seeded.
   1943 */
   1944 export function LineData() {
   1945  /** An array of queued bytes from which to get line-based characters. */
   1946  this._data = [];
   1947 
   1948  /** Start index from which to search for CRLF. */
   1949  this._start = 0;
   1950 }
   1951 LineData.prototype = {
   1952  /**
   1953   * Appends the bytes in the given array to the internal data cache maintained
   1954   * by this.
   1955   */
   1956  appendBytes(bytes) {
   1957    var count = bytes.length;
   1958    var quantum = 262144; // just above half SpiderMonkey's argument-count limit
   1959    if (count < quantum) {
   1960      Array.prototype.push.apply(this._data, bytes);
   1961      return;
   1962    }
   1963 
   1964    // Large numbers of bytes may cause Array.prototype.push to be called with
   1965    // more arguments than the JavaScript engine supports.  In that case append
   1966    // bytes in fixed-size amounts until all bytes are appended.
   1967    for (var start = 0; start < count; start += quantum) {
   1968      var slice = bytes.slice(start, Math.min(start + quantum, count));
   1969      Array.prototype.push.apply(this._data, slice);
   1970    }
   1971  },
   1972 
   1973  /**
   1974   * Removes and returns a line of data, delimited by CRLF, from this.
   1975   *
   1976   * @param out
   1977   *   an object whose "value" property will be set to the first line of text
   1978   *   present in this, sans CRLF, if this contains a full CRLF-delimited line
   1979   *   of text; if this doesn't contain enough data, the value of the property
   1980   *   is undefined
   1981   * @returns boolean
   1982   *   true if a full line of data could be read from the data in this, false
   1983   *   otherwise
   1984   */
   1985  readLine(out) {
   1986    var data = this._data;
   1987    var length = findCRLF(data, this._start);
   1988    if (length < 0) {
   1989      this._start = data.length;
   1990 
   1991      // But if our data ends in a CR, we have to back up one, because
   1992      // the first byte in the next packet might be an LF and if we
   1993      // start looking at data.length we won't find it.
   1994      if (data.length && data[data.length - 1] === CR) {
   1995        --this._start;
   1996      }
   1997 
   1998      return false;
   1999    }
   2000 
   2001    // Reset for future lines.
   2002    this._start = 0;
   2003 
   2004    //
   2005    // We have the index of the CR, so remove all the characters, including
   2006    // CRLF, from the array with splice, and convert the removed array
   2007    // (excluding the trailing CRLF characters) into the corresponding string.
   2008    //
   2009    var leading = data.splice(0, length + 2);
   2010    var quantum = 262144;
   2011    var line = "";
   2012    for (var start = 0; start < length; start += quantum) {
   2013      var slice = leading.slice(start, Math.min(start + quantum, length));
   2014      line += String.fromCharCode.apply(null, slice);
   2015    }
   2016 
   2017    out.value = line;
   2018    return true;
   2019  },
   2020 
   2021  /**
   2022   * Removes the bytes currently within this and returns them in an array.
   2023   *
   2024   * @returns Array
   2025   *   the bytes within this when this method is called
   2026   */
   2027  purge() {
   2028    var data = this._data;
   2029    this._data = [];
   2030    return data;
   2031  },
   2032 };
   2033 
   2034 /**
   2035 * Creates a request-handling function for an nsIHttpRequestHandler object.
   2036 */
   2037 function createHandlerFunc(handler) {
   2038  return function (metadata, response) {
   2039    handler.handle(metadata, response);
   2040  };
   2041 }
   2042 
   2043 /**
   2044 * The default handler for directories; writes an HTML response containing a
   2045 * slightly-formatted directory listing.
   2046 */
   2047 function defaultIndexHandler(metadata, response) {
   2048  response.setHeader("Content-Type", "text/html;charset=utf-8", false);
   2049 
   2050  var path = htmlEscape(decodeURI(metadata.path));
   2051 
   2052  //
   2053  // Just do a very basic bit of directory listings -- no need for too much
   2054  // fanciness, especially since we don't have a style sheet in which we can
   2055  // stick rules (don't want to pollute the default path-space).
   2056  //
   2057 
   2058  var body =
   2059    "<html>\
   2060                <head>\
   2061                  <title>" +
   2062    path +
   2063    "</title>\
   2064                </head>\
   2065                <body>\
   2066                  <h1>" +
   2067    path +
   2068    '</h1>\
   2069                  <ol style="list-style-type: none">';
   2070 
   2071  var directory = metadata.getProperty("directory");
   2072  NS_ASSERT(directory && directory.isDirectory());
   2073 
   2074  var fileList = [];
   2075  var files = directory.directoryEntries;
   2076  while (files.hasMoreElements()) {
   2077    var f = files.nextFile;
   2078    let name = f.leafName;
   2079    if (
   2080      !f.isHidden() &&
   2081      (name.charAt(name.length - 1) != HIDDEN_CHAR ||
   2082        name.charAt(name.length - 2) == HIDDEN_CHAR)
   2083    ) {
   2084      fileList.push(f);
   2085    }
   2086  }
   2087 
   2088  fileList.sort(fileSort);
   2089 
   2090  for (var i = 0; i < fileList.length; i++) {
   2091    var file = fileList[i];
   2092    try {
   2093      let name = file.leafName;
   2094      if (name.charAt(name.length - 1) == HIDDEN_CHAR) {
   2095        name = name.substring(0, name.length - 1);
   2096      }
   2097      var sep = file.isDirectory() ? "/" : "";
   2098 
   2099      // Note: using " to delimit the attribute here because encodeURIComponent
   2100      //       passes through '.
   2101      var item =
   2102        '<li><a href="' +
   2103        encodeURIComponent(name) +
   2104        sep +
   2105        '">' +
   2106        htmlEscape(name) +
   2107        sep +
   2108        "</a></li>";
   2109 
   2110      body += item;
   2111    } catch (e) {
   2112      /* some file system error, ignore the file */
   2113    }
   2114  }
   2115 
   2116  body +=
   2117    "    </ol>\
   2118                </body>\
   2119              </html>";
   2120 
   2121  response.bodyOutputStream.write(body, body.length);
   2122 }
   2123 
   2124 /**
   2125 * Sorts a and b (nsIFile objects) into an aesthetically pleasing order.
   2126 */
   2127 function fileSort(a, b) {
   2128  var dira = a.isDirectory(),
   2129    dirb = b.isDirectory();
   2130 
   2131  if (dira && !dirb) {
   2132    return -1;
   2133  }
   2134  if (dirb && !dira) {
   2135    return 1;
   2136  }
   2137 
   2138  var namea = a.leafName.toLowerCase(),
   2139    nameb = b.leafName.toLowerCase();
   2140  return nameb > namea ? -1 : 1;
   2141 }
   2142 
   2143 /**
   2144 * Converts an externally-provided path into an internal path for use in
   2145 * determining file mappings.
   2146 *
   2147 * @param path
   2148 *   the path to convert
   2149 * @param encoded
   2150 *   true if the given path should be passed through decodeURI prior to
   2151 *   conversion
   2152 * @throws URIError
   2153 *   if path is incorrectly encoded
   2154 */
   2155 function toInternalPath(path, encoded) {
   2156  if (encoded) {
   2157    path = decodeURI(path);
   2158  }
   2159 
   2160  var comps = path.split("/");
   2161  for (var i = 0, sz = comps.length; i < sz; i++) {
   2162    var comp = comps[i];
   2163    if (comp.charAt(comp.length - 1) == HIDDEN_CHAR) {
   2164      comps[i] = comp + HIDDEN_CHAR;
   2165    }
   2166  }
   2167  return comps.join("/");
   2168 }
   2169 
   2170 const PERMS_READONLY = (4 << 6) | (4 << 3) | 4;
   2171 
   2172 /**
   2173 * Adds custom-specified headers for the given file to the given response, if
   2174 * any such headers are specified.
   2175 *
   2176 * @param file
   2177 *   the file on the disk which is to be written
   2178 * @param metadata
   2179 *   metadata about the incoming request
   2180 * @param response
   2181 *   the Response to which any specified headers/data should be written
   2182 * @throws HTTP_500
   2183 *   if an error occurred while processing custom-specified headers
   2184 */
   2185 function maybeAddHeadersInternal(
   2186  file,
   2187  metadata,
   2188  response,
   2189  informationalResponse
   2190 ) {
   2191  var name = file.leafName;
   2192  if (name.charAt(name.length - 1) == HIDDEN_CHAR) {
   2193    name = name.substring(0, name.length - 1);
   2194  }
   2195 
   2196  var headerFile = file.parent;
   2197  if (!informationalResponse) {
   2198    headerFile.append(name + HEADERS_SUFFIX);
   2199  } else {
   2200    headerFile.append(name + INFORMATIONAL_RESPONSE_SUFFIX);
   2201  }
   2202 
   2203  if (!headerFile.exists()) {
   2204    return;
   2205  }
   2206 
   2207  const PR_RDONLY = 0x01;
   2208  var fis = new FileInputStream(
   2209    headerFile,
   2210    PR_RDONLY,
   2211    PERMS_READONLY,
   2212    Ci.nsIFileInputStream.CLOSE_ON_EOF
   2213  );
   2214 
   2215  try {
   2216    var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
   2217    lis.QueryInterface(Ci.nsIUnicharLineInputStream);
   2218 
   2219    var line = { value: "" };
   2220    var more = lis.readLine(line);
   2221 
   2222    if (!more && line.value == "") {
   2223      return;
   2224    }
   2225 
   2226    // request line
   2227 
   2228    var status = line.value;
   2229    if (status.indexOf("HTTP ") == 0) {
   2230      status = status.substring(5);
   2231      var space = status.indexOf(" ");
   2232      var code, description;
   2233      if (space < 0) {
   2234        code = status;
   2235        description = "";
   2236      } else {
   2237        code = status.substring(0, space);
   2238        description = status.substring(space + 1, status.length);
   2239      }
   2240 
   2241      if (!informationalResponse) {
   2242        response.setStatusLine(
   2243          metadata.httpVersion,
   2244          parseInt(code, 10),
   2245          description
   2246        );
   2247      } else {
   2248        response.setInformationalResponseStatusLine(
   2249          metadata.httpVersion,
   2250          parseInt(code, 10),
   2251          description
   2252        );
   2253      }
   2254 
   2255      line.value = "";
   2256      more = lis.readLine(line);
   2257    } else if (informationalResponse) {
   2258      // An informational response must have a status line.
   2259      return;
   2260    }
   2261 
   2262    // headers
   2263    while (more || line.value != "") {
   2264      var header = line.value;
   2265      var colon = header.indexOf(":");
   2266 
   2267      if (!informationalResponse) {
   2268        response.setHeader(
   2269          header.substring(0, colon),
   2270          header.substring(colon + 1, header.length),
   2271          false
   2272        ); // allow overriding server-set headers
   2273      } else {
   2274        response.setInformationalResponseHeader(
   2275          header.substring(0, colon),
   2276          header.substring(colon + 1, header.length),
   2277          false
   2278        ); // allow overriding server-set headers
   2279      }
   2280 
   2281      line.value = "";
   2282      more = lis.readLine(line);
   2283    }
   2284  } catch (e) {
   2285    dumpn("WARNING: error in headers for " + metadata.path + ": " + e);
   2286    throw HTTP_500;
   2287  } finally {
   2288    fis.close();
   2289  }
   2290 }
   2291 
   2292 function maybeAddHeaders(file, metadata, response) {
   2293  maybeAddHeadersInternal(file, metadata, response, false);
   2294 }
   2295 
   2296 function maybeAddInformationalResponse(file, metadata, response) {
   2297  maybeAddHeadersInternal(file, metadata, response, true);
   2298 }
   2299 
   2300 /**
   2301 * An object which handles requests for a server, executing default and
   2302 * overridden behaviors as instructed by the code which uses and manipulates it.
   2303 * Default behavior includes the paths / and /trace (diagnostics), with some
   2304 * support for HTTP error pages for various codes and fallback to HTTP 500 if
   2305 * those codes fail for any reason.
   2306 *
   2307 * @param server : nsHttpServer
   2308 *   the server in which this handler is being used
   2309 */
   2310 function ServerHandler(server) {
   2311  // FIELDS
   2312 
   2313  /**
   2314   * The nsHttpServer instance associated with this handler.
   2315   */
   2316  this._server = server;
   2317 
   2318  /**
   2319   * A FileMap object containing the set of path->nsIFile mappings for
   2320   * all directory mappings set in the server (e.g., "/" for /var/www/html/,
   2321   * "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2).
   2322   *
   2323   * Note carefully: the leading and trailing "/" in each path (not file) are
   2324   * removed before insertion to simplify the code which uses this.  You have
   2325   * been warned!
   2326   */
   2327  this._pathDirectoryMap = new FileMap();
   2328 
   2329  /**
   2330   * Custom request handlers for the server in which this resides.  Path-handler
   2331   * pairs are stored as property-value pairs in this property.
   2332   *
   2333   * @see ServerHandler.prototype._defaultPaths
   2334   */
   2335  this._overridePaths = {};
   2336 
   2337  /**
   2338   * Custom request handlers for the path prefixes on the server in which this
   2339   * resides.  Path-handler pairs are stored as property-value pairs in this
   2340   * property.
   2341   *
   2342   * @see ServerHandler.prototype._defaultPaths
   2343   */
   2344  this._overridePrefixes = {};
   2345 
   2346  /**
   2347   * Custom request handlers for the error handlers in the server in which this
   2348   * resides.  Path-handler pairs are stored as property-value pairs in this
   2349   * property.
   2350   *
   2351   * @see ServerHandler.prototype._defaultErrors
   2352   */
   2353  this._overrideErrors = {};
   2354 
   2355  /**
   2356   * Maps file extensions to their MIME types in the server, overriding any
   2357   * mapping that might or might not exist in the MIME service.
   2358   */
   2359  this._mimeMappings = {};
   2360 
   2361  /**
   2362   * The default handler for requests for directories, used to serve directories
   2363   * when no index file is present.
   2364   */
   2365  this._indexHandler = defaultIndexHandler;
   2366 
   2367  /** Per-path state storage for the server. */
   2368  this._state = {};
   2369 
   2370  /** Entire-server state storage. */
   2371  this._sharedState = {};
   2372 
   2373  /** Entire-server state storage for nsISupports values. */
   2374  this._objectState = {};
   2375 }
   2376 ServerHandler.prototype = {
   2377  // PUBLIC API
   2378 
   2379  /**
   2380   * Handles a request to this server, responding to the request appropriately
   2381   * and initiating server shutdown if necessary.
   2382   *
   2383   * This method never throws an exception.
   2384   *
   2385   * @param connection : Connection
   2386   *   the connection for this request
   2387   */
   2388  handleResponse(connection) {
   2389    var request = connection.request;
   2390    var response = new Response(connection);
   2391 
   2392    var path = request.path;
   2393    dumpn("*** path == " + path);
   2394 
   2395    try {
   2396      try {
   2397        if (path in this._overridePaths) {
   2398          // explicit paths first, then files based on existing directory mappings,
   2399          // then (if the file doesn't exist) built-in server default paths
   2400          dumpn("calling override for " + path);
   2401          this._overridePaths[path](request, response);
   2402        } else {
   2403          var longestPrefix = "";
   2404          for (let prefix in this._overridePrefixes) {
   2405            if (
   2406              prefix.length > longestPrefix.length &&
   2407              path.substr(0, prefix.length) == prefix
   2408            ) {
   2409              longestPrefix = prefix;
   2410            }
   2411          }
   2412          if (longestPrefix.length) {
   2413            dumpn("calling prefix override for " + longestPrefix);
   2414            this._overridePrefixes[longestPrefix](request, response);
   2415          } else {
   2416            this._handleDefault(request, response);
   2417          }
   2418        }
   2419      } catch (e) {
   2420        if (response.partiallySent()) {
   2421          response.abort(e);
   2422          return;
   2423        }
   2424 
   2425        if (!(e instanceof HttpError)) {
   2426          dumpn("*** unexpected error: e == " + e);
   2427          throw HTTP_500;
   2428        }
   2429        if (e.code !== 404) {
   2430          throw e;
   2431        }
   2432 
   2433        dumpn("*** default: " + (path in this._defaultPaths));
   2434 
   2435        response = new Response(connection);
   2436        if (path in this._defaultPaths) {
   2437          this._defaultPaths[path](request, response);
   2438        } else {
   2439          throw HTTP_404;
   2440        }
   2441      }
   2442    } catch (e) {
   2443      if (response.partiallySent()) {
   2444        response.abort(e);
   2445        return;
   2446      }
   2447 
   2448      var errorCode = "internal";
   2449 
   2450      try {
   2451        if (!(e instanceof HttpError)) {
   2452          throw e;
   2453        }
   2454 
   2455        errorCode = e.code;
   2456        dumpn("*** errorCode == " + errorCode);
   2457 
   2458        response = new Response(connection);
   2459        if (e.customErrorHandling) {
   2460          e.customErrorHandling(response);
   2461        }
   2462        this._handleError(errorCode, request, response);
   2463        return;
   2464      } catch (e2) {
   2465        dumpn(
   2466          "*** error handling " +
   2467            errorCode +
   2468            " error: " +
   2469            "e2 == " +
   2470            e2 +
   2471            ", shutting down server"
   2472        );
   2473 
   2474        connection.server._requestQuit();
   2475        response.abort(e2);
   2476        return;
   2477      }
   2478    }
   2479 
   2480    response.complete();
   2481  },
   2482 
   2483  //
   2484  // see nsIHttpServer.registerFile
   2485  //
   2486  registerFile(path, file, handler) {
   2487    if (!file) {
   2488      dumpn("*** unregistering '" + path + "' mapping");
   2489      delete this._overridePaths[path];
   2490      return;
   2491    }
   2492 
   2493    dumpn("*** registering '" + path + "' as mapping to " + file.path);
   2494    file = file.clone();
   2495 
   2496    var self = this;
   2497    this._overridePaths[path] = function (request, response) {
   2498      if (!file.exists()) {
   2499        throw HTTP_404;
   2500      }
   2501 
   2502      dumpn("*** responding '" + path + "' as mapping to " + file.path);
   2503 
   2504      response.setStatusLine(request.httpVersion, 200, "OK");
   2505      if (typeof handler === "function") {
   2506        handler(request, response);
   2507      }
   2508      self._writeFileResponse(request, file, response, 0, file.fileSize);
   2509    };
   2510  },
   2511 
   2512  //
   2513  // see nsIHttpServer.registerPathHandler
   2514  //
   2515  registerPathHandler(path, handler) {
   2516    if (!path.length) {
   2517      throw Components.Exception(
   2518        "Handler path cannot be empty",
   2519        Cr.NS_ERROR_INVALID_ARG
   2520      );
   2521    }
   2522 
   2523    // XXX true path validation!
   2524    if (path.charAt(0) != "/" && path != "CONNECT") {
   2525      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   2526    }
   2527 
   2528    this._handlerToField(handler, this._overridePaths, path);
   2529  },
   2530 
   2531  //
   2532  // see nsIHttpServer.registerPrefixHandler
   2533  //
   2534  registerPrefixHandler(path, handler) {
   2535    // XXX true path validation!
   2536    if (path.charAt(0) != "/" || path.charAt(path.length - 1) != "/") {
   2537      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   2538    }
   2539 
   2540    this._handlerToField(handler, this._overridePrefixes, path);
   2541  },
   2542 
   2543  //
   2544  // see nsIHttpServer.registerDirectory
   2545  //
   2546  registerDirectory(path, directory) {
   2547    // strip off leading and trailing '/' so that we can use lastIndexOf when
   2548    // determining exactly how a path maps onto a mapped directory --
   2549    // conditional is required here to deal with "/".substring(1, 0) being
   2550    // converted to "/".substring(0, 1) per the JS specification
   2551    var key = path.length == 1 ? "" : path.substring(1, path.length - 1);
   2552 
   2553    // the path-to-directory mapping code requires that the first character not
   2554    // be "/", or it will go into an infinite loop
   2555    if (key.charAt(0) == "/") {
   2556      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   2557    }
   2558 
   2559    key = toInternalPath(key, false);
   2560 
   2561    if (directory) {
   2562      dumpn("*** mapping '" + path + "' to the location " + directory.path);
   2563      this._pathDirectoryMap.put(key, directory);
   2564    } else {
   2565      dumpn("*** removing mapping for '" + path + "'");
   2566      this._pathDirectoryMap.put(key, null);
   2567    }
   2568  },
   2569 
   2570  //
   2571  // see nsIHttpServer.registerErrorHandler
   2572  //
   2573  registerErrorHandler(err, handler) {
   2574    if (!(err in HTTP_ERROR_CODES)) {
   2575      dumpn(
   2576        "*** WARNING: registering non-HTTP/1.1 error code " +
   2577          "(" +
   2578          err +
   2579          ") handler -- was this intentional?"
   2580      );
   2581    }
   2582 
   2583    this._handlerToField(handler, this._overrideErrors, err);
   2584  },
   2585 
   2586  //
   2587  // see nsIHttpServer.setIndexHandler
   2588  //
   2589  setIndexHandler(handler) {
   2590    if (!handler) {
   2591      handler = defaultIndexHandler;
   2592    } else if (typeof handler != "function") {
   2593      handler = createHandlerFunc(handler);
   2594    }
   2595 
   2596    this._indexHandler = handler;
   2597  },
   2598 
   2599  //
   2600  // see nsIHttpServer.registerContentType
   2601  //
   2602  registerContentType(ext, type) {
   2603    if (!type) {
   2604      delete this._mimeMappings[ext];
   2605    } else {
   2606      this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type);
   2607    }
   2608  },
   2609 
   2610  // PRIVATE API
   2611 
   2612  /**
   2613   * Sets or remove (if handler is null) a handler in an object with a key.
   2614   *
   2615   * @param handler
   2616   *   a handler, either function or an nsIHttpRequestHandler
   2617   * @param dict
   2618   *   The object to attach the handler to.
   2619   * @param key
   2620   *   The field name of the handler.
   2621   */
   2622  _handlerToField(handler, dict, key) {
   2623    // for convenience, handler can be a function if this is run from xpcshell
   2624    if (typeof handler == "function") {
   2625      dict[key] = handler;
   2626    } else if (handler) {
   2627      dict[key] = createHandlerFunc(handler);
   2628    } else {
   2629      delete dict[key];
   2630    }
   2631  },
   2632 
   2633  /**
   2634   * Handles a request which maps to a file in the local filesystem (if a base
   2635   * path has already been set; otherwise the 404 error is thrown).
   2636   *
   2637   * @param metadata : Request
   2638   *   metadata for the incoming request
   2639   * @param response : Response
   2640   *   an uninitialized Response to the given request, to be initialized by a
   2641   *   request handler
   2642   * @throws HTTP_###
   2643   *   if an HTTP error occurred (usually HTTP_404); note that in this case the
   2644   *   calling code must handle post-processing of the response
   2645   */
   2646  _handleDefault(metadata, response) {
   2647    dumpn("*** _handleDefault()");
   2648 
   2649    response.setStatusLine(metadata.httpVersion, 200, "OK");
   2650 
   2651    var path = metadata.path;
   2652    NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">");
   2653 
   2654    // determine the actual on-disk file; this requires finding the deepest
   2655    // path-to-directory mapping in the requested URL
   2656    var file = this._getFileForPath(path);
   2657 
   2658    // the "file" might be a directory, in which case we either serve the
   2659    // contained index.html or make the index handler write the response
   2660    if (file.exists() && file.isDirectory()) {
   2661      file.append("index.html"); // make configurable?
   2662      if (!file.exists() || file.isDirectory()) {
   2663        metadata._ensurePropertyBag();
   2664        metadata._bag.setPropertyAsInterface("directory", file.parent);
   2665        this._indexHandler(metadata, response);
   2666        return;
   2667      }
   2668    }
   2669 
   2670    // alternately, the file might not exist
   2671    if (!file.exists()) {
   2672      throw HTTP_404;
   2673    }
   2674 
   2675    var start, end;
   2676    if (
   2677      metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) &&
   2678      metadata.hasHeader("Range") &&
   2679      this._getTypeFromFile(file) !== SJS_TYPE
   2680    ) {
   2681      var rangeMatch = metadata
   2682        .getHeader("Range")
   2683        .match(/^bytes=(\d+)?-(\d+)?$/);
   2684      if (!rangeMatch) {
   2685        dumpn(
   2686          "*** Range header bogosity: '" + metadata.getHeader("Range") + "'"
   2687        );
   2688        throw HTTP_400;
   2689      }
   2690 
   2691      if (rangeMatch[1] !== undefined) {
   2692        start = parseInt(rangeMatch[1], 10);
   2693      }
   2694 
   2695      if (rangeMatch[2] !== undefined) {
   2696        end = parseInt(rangeMatch[2], 10);
   2697      }
   2698 
   2699      if (start === undefined && end === undefined) {
   2700        dumpn(
   2701          "*** More Range header bogosity: '" +
   2702            metadata.getHeader("Range") +
   2703            "'"
   2704        );
   2705        throw HTTP_400;
   2706      }
   2707 
   2708      // No start given, so the end is really the count of bytes from the
   2709      // end of the file.
   2710      if (start === undefined) {
   2711        start = Math.max(0, file.fileSize - end);
   2712        end = file.fileSize - 1;
   2713      }
   2714 
   2715      // start and end are inclusive
   2716      if (end === undefined || end >= file.fileSize) {
   2717        end = file.fileSize - 1;
   2718      }
   2719 
   2720      if (start !== undefined && start >= file.fileSize) {
   2721        var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable");
   2722        HTTP_416.customErrorHandling = function (errorResponse) {
   2723          maybeAddHeaders(file, metadata, errorResponse);
   2724        };
   2725        throw HTTP_416;
   2726      }
   2727 
   2728      if (end < start) {
   2729        response.setStatusLine(metadata.httpVersion, 200, "OK");
   2730        start = 0;
   2731        end = file.fileSize - 1;
   2732      } else {
   2733        response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
   2734        var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize;
   2735        response.setHeader("Content-Range", contentRange);
   2736      }
   2737    } else {
   2738      start = 0;
   2739      end = file.fileSize - 1;
   2740    }
   2741 
   2742    // finally...
   2743    dumpn(
   2744      "*** handling '" +
   2745        path +
   2746        "' as mapping to " +
   2747        file.path +
   2748        " from " +
   2749        start +
   2750        " to " +
   2751        end +
   2752        " inclusive"
   2753    );
   2754    this._writeFileResponse(metadata, file, response, start, end - start + 1);
   2755  },
   2756 
   2757  /**
   2758   * Writes an HTTP response for the given file, including setting headers for
   2759   * file metadata.
   2760   *
   2761   * @param metadata : Request
   2762   *   the Request for which a response is being generated
   2763   * @param file : nsIFile
   2764   *   the file which is to be sent in the response
   2765   * @param response : Response
   2766   *   the response to which the file should be written
   2767   * @param offset: uint
   2768   *   the byte offset to skip to when writing
   2769   * @param count: uint
   2770   *   the number of bytes to write
   2771   */
   2772  _writeFileResponse(metadata, file, response, offset, count) {
   2773    const PR_RDONLY = 0x01;
   2774 
   2775    var type = this._getTypeFromFile(file);
   2776    if (type === SJS_TYPE) {
   2777      let fis = new FileInputStream(
   2778        file,
   2779        PR_RDONLY,
   2780        PERMS_READONLY,
   2781        Ci.nsIFileInputStream.CLOSE_ON_EOF
   2782      );
   2783 
   2784      try {
   2785        // If you update the list of imports, please update the list in
   2786        // tools/lint/eslint/eslint-plugin-mozilla/lib/environments/sjs.js
   2787        // as well.
   2788        var s = Cu.Sandbox(Cu.getGlobalForObject({}), {
   2789          wantGlobalProperties: [
   2790            "atob",
   2791            "btoa",
   2792            "ChromeUtils",
   2793            "IOUtils",
   2794            "PathUtils",
   2795            "TextDecoder",
   2796            "TextEncoder",
   2797            "URLSearchParams",
   2798            "URL",
   2799          ],
   2800        });
   2801        s.importFunction(dump, "dump");
   2802        s.importFunction(Services, "Services");
   2803 
   2804        // Define a basic key-value state-preservation API across requests, with
   2805        // keys initially corresponding to the empty string.
   2806        var self = this;
   2807        var path = metadata.path;
   2808        s.importFunction(function getState(k) {
   2809          return self._getState(path, k);
   2810        });
   2811        s.importFunction(function setState(k, v) {
   2812          self._setState(path, k, v);
   2813        });
   2814        s.importFunction(function getSharedState(k) {
   2815          return self._getSharedState(k);
   2816        });
   2817        s.importFunction(function setSharedState(k, v) {
   2818          self._setSharedState(k, v);
   2819        });
   2820        s.importFunction(function getObjectState(k, callback) {
   2821          callback(self._getObjectState(k));
   2822        });
   2823        s.importFunction(function setObjectState(k, v) {
   2824          self._setObjectState(k, v);
   2825        });
   2826        s.importFunction(function registerPathHandler(p, h) {
   2827          self.registerPathHandler(p, h);
   2828        });
   2829 
   2830        // Make it possible for sjs files to access their location
   2831        this._setState(path, "__LOCATION__", file.path);
   2832 
   2833        try {
   2834          // Alas, the line number in errors dumped to console when calling the
   2835          // request handler is simply an offset from where we load the SJS file.
   2836          // Work around this in a reasonably non-fragile way by dynamically
   2837          // getting the line number where we evaluate the SJS file.  Don't
   2838          // separate these two lines!
   2839          var line = new Error().lineNumber;
   2840          let uri = Services.io.newFileURI(file);
   2841          Services.scriptloader.loadSubScript(uri.spec, s);
   2842        } catch (e) {
   2843          dumpn("*** syntax error in SJS at " + file.path + ": " + e);
   2844          throw HTTP_500;
   2845        }
   2846 
   2847        try {
   2848          s.handleRequest(metadata, response);
   2849        } catch (e) {
   2850          dump(
   2851            "*** error running SJS at " +
   2852              file.path +
   2853              ": " +
   2854              e +
   2855              " on line " +
   2856              (e instanceof Error
   2857                ? e.lineNumber + " in httpd.js"
   2858                : e.lineNumber - line) +
   2859              "\n"
   2860          );
   2861          throw HTTP_500;
   2862        }
   2863      } finally {
   2864        fis.close();
   2865      }
   2866    } else {
   2867      try {
   2868        response.setHeader(
   2869          "Last-Modified",
   2870          toDateString(file.lastModifiedTime),
   2871          false
   2872        );
   2873      } catch (e) {
   2874        /* lastModifiedTime threw, ignore */
   2875      }
   2876 
   2877      response.setHeader("Content-Type", type, false);
   2878      maybeAddInformationalResponse(file, metadata, response);
   2879      maybeAddHeaders(file, metadata, response);
   2880      // Allow overriding Content-Length
   2881      try {
   2882        response.getHeader("Content-Length");
   2883      } catch (e) {
   2884        response.setHeader("Content-Length", "" + count, false);
   2885      }
   2886 
   2887      let fis = new FileInputStream(
   2888        file,
   2889        PR_RDONLY,
   2890        PERMS_READONLY,
   2891        Ci.nsIFileInputStream.CLOSE_ON_EOF
   2892      );
   2893 
   2894      offset = offset || 0;
   2895      count = count || file.fileSize;
   2896      NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset");
   2897      NS_ASSERT(count >= 0, "bad count");
   2898      NS_ASSERT(offset + count <= file.fileSize, "bad total data size");
   2899 
   2900      try {
   2901        if (offset !== 0) {
   2902          // Seek (or read, if seeking isn't supported) to the correct offset so
   2903          // the data sent to the client matches the requested range.
   2904          if (fis instanceof Ci.nsISeekableStream) {
   2905            fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset);
   2906          } else {
   2907            new ScriptableInputStream(fis).read(offset);
   2908          }
   2909        }
   2910      } catch (e) {
   2911        fis.close();
   2912        throw e;
   2913      }
   2914 
   2915      let writeMore = function () {
   2916        Services.tm.currentThread.dispatch(
   2917          writeData,
   2918          Ci.nsIThread.DISPATCH_NORMAL
   2919        );
   2920      };
   2921 
   2922      var input = new BinaryInputStream(fis);
   2923      var output = new BinaryOutputStream(response.bodyOutputStream);
   2924      var writeData = {
   2925        run() {
   2926          var chunkSize = Math.min(65536, count);
   2927          count -= chunkSize;
   2928          NS_ASSERT(count >= 0, "underflow");
   2929 
   2930          try {
   2931            var data = input.readByteArray(chunkSize);
   2932            NS_ASSERT(
   2933              data.length === chunkSize,
   2934              "incorrect data returned?  got " +
   2935                data.length +
   2936                ", expected " +
   2937                chunkSize
   2938            );
   2939            output.writeByteArray(data);
   2940            if (count === 0) {
   2941              fis.close();
   2942              response.finish();
   2943            } else {
   2944              writeMore();
   2945            }
   2946          } catch (e) {
   2947            try {
   2948              fis.close();
   2949            } finally {
   2950              response.finish();
   2951            }
   2952            throw e;
   2953          }
   2954        },
   2955      };
   2956 
   2957      writeMore();
   2958 
   2959      // Now that we know copying will start, flag the response as async.
   2960      response.processAsync();
   2961    }
   2962  },
   2963 
   2964  /**
   2965   * Get the value corresponding to a given key for the given path for SJS state
   2966   * preservation across requests.
   2967   *
   2968   * @param path : string
   2969   *   the path from which the given state is to be retrieved
   2970   * @param k : string
   2971   *   the key whose corresponding value is to be returned
   2972   * @returns string
   2973   *   the corresponding value, which is initially the empty string
   2974   */
   2975  _getState(path, k) {
   2976    var state = this._state;
   2977    if (path in state && k in state[path]) {
   2978      return state[path][k];
   2979    }
   2980    return "";
   2981  },
   2982 
   2983  /**
   2984   * Set the value corresponding to a given key for the given path for SJS state
   2985   * preservation across requests.
   2986   *
   2987   * @param path : string
   2988   *   the path from which the given state is to be retrieved
   2989   * @param k : string
   2990   *   the key whose corresponding value is to be set
   2991   * @param v : string
   2992   *   the value to be set
   2993   */
   2994  _setState(path, k, v) {
   2995    if (typeof v !== "string") {
   2996      throw new Error("non-string value passed");
   2997    }
   2998    var state = this._state;
   2999    if (!(path in state)) {
   3000      state[path] = {};
   3001    }
   3002    state[path][k] = v;
   3003  },
   3004 
   3005  /**
   3006   * Get the value corresponding to a given key for SJS state preservation
   3007   * across requests.
   3008   *
   3009   * @param k : string
   3010   *   the key whose corresponding value is to be returned
   3011   * @returns string
   3012   *   the corresponding value, which is initially the empty string
   3013   */
   3014  _getSharedState(k) {
   3015    var state = this._sharedState;
   3016    if (k in state) {
   3017      return state[k];
   3018    }
   3019    return "";
   3020  },
   3021 
   3022  /**
   3023   * Set the value corresponding to a given key for SJS state preservation
   3024   * across requests.
   3025   *
   3026   * @param k : string
   3027   *   the key whose corresponding value is to be set
   3028   * @param v : string
   3029   *   the value to be set
   3030   */
   3031  _setSharedState(k, v) {
   3032    if (typeof v !== "string") {
   3033      throw new Error("non-string value passed");
   3034    }
   3035    this._sharedState[k] = v;
   3036  },
   3037 
   3038  /**
   3039   * Returns the object associated with the given key in the server for SJS
   3040   * state preservation across requests.
   3041   *
   3042   * @param k : string
   3043   *  the key whose corresponding object is to be returned
   3044   * @returns nsISupports
   3045   *  the corresponding object, or null if none was present
   3046   */
   3047  _getObjectState(k) {
   3048    if (typeof k !== "string") {
   3049      throw new Error("non-string key passed");
   3050    }
   3051    return this._objectState[k] || null;
   3052  },
   3053 
   3054  /**
   3055   * Sets the object associated with the given key in the server for SJS
   3056   * state preservation across requests.
   3057   *
   3058   * @param k : string
   3059   *  the key whose corresponding object is to be set
   3060   * @param v : nsISupports
   3061   *  the object to be associated with the given key; may be null
   3062   */
   3063  _setObjectState(k, v) {
   3064    if (typeof k !== "string") {
   3065      throw new Error("non-string key passed");
   3066    }
   3067    if (typeof v !== "object") {
   3068      throw new Error("non-object value passed");
   3069    }
   3070    if (v && !("QueryInterface" in v)) {
   3071      throw new Error(
   3072        "must pass an nsISupports; use wrappedJSObject to ease " +
   3073          "pain when using the server from JS"
   3074      );
   3075    }
   3076 
   3077    this._objectState[k] = v;
   3078  },
   3079 
   3080  /**
   3081   * Gets a content-type for the given file, first by checking for any custom
   3082   * MIME-types registered with this handler for the file's extension, second by
   3083   * asking the global MIME service for a content-type, and finally by failing
   3084   * over to application/octet-stream.
   3085   *
   3086   * @param file : nsIFile
   3087   *   the nsIFile for which to get a file type
   3088   * @returns string
   3089   *   the best content-type which can be determined for the file
   3090   */
   3091  _getTypeFromFile(file) {
   3092    try {
   3093      var name = file.leafName;
   3094      var dot = name.lastIndexOf(".");
   3095      if (dot > 0) {
   3096        var ext = name.slice(dot + 1);
   3097        if (ext in this._mimeMappings) {
   3098          return this._mimeMappings[ext];
   3099        }
   3100      }
   3101      return Cc["@mozilla.org/mime;1"]
   3102        .getService(Ci.nsIMIMEService)
   3103        .getTypeFromFile(file);
   3104    } catch (e) {
   3105      return "application/octet-stream";
   3106    }
   3107  },
   3108 
   3109  /**
   3110   * Returns the nsIFile which corresponds to the path, as determined using
   3111   * all registered path->directory mappings and any paths which are explicitly
   3112   * overridden.
   3113   *
   3114   * @param path : string
   3115   *   the server path for which a file should be retrieved, e.g. "/foo/bar"
   3116   * @throws HttpError
   3117   *   when the correct action is the corresponding HTTP error (i.e., because no
   3118   *   mapping was found for a directory in path, the referenced file doesn't
   3119   *   exist, etc.)
   3120   * @returns nsIFile
   3121   *   the file to be sent as the response to a request for the path
   3122   */
   3123  _getFileForPath(path) {
   3124    // decode and add underscores as necessary
   3125    try {
   3126      path = toInternalPath(path, true);
   3127    } catch (e) {
   3128      dumpn("*** toInternalPath threw " + e);
   3129      throw HTTP_400; // malformed path
   3130    }
   3131 
   3132    // next, get the directory which contains this path
   3133    var pathMap = this._pathDirectoryMap;
   3134 
   3135    // An example progression of tmp for a path "/foo/bar/baz/" might be:
   3136    // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", ""
   3137    var tmp = path.substring(1);
   3138    // eslint-disable-next-line no-constant-condition
   3139    while (true) {
   3140      // do we have a match for current head of the path?
   3141      var file = pathMap.get(tmp);
   3142      if (file) {
   3143        // XXX hack; basically disable showing mapping for /foo/bar/ when the
   3144        //     requested path was /foo/bar, because relative links on the page
   3145        //     will all be incorrect -- we really need the ability to easily
   3146        //     redirect here instead
   3147        if (
   3148          tmp == path.substring(1) &&
   3149          !!tmp.length &&
   3150          tmp.charAt(tmp.length - 1) != "/"
   3151        ) {
   3152          file = null;
   3153        } else {
   3154          break;
   3155        }
   3156      }
   3157 
   3158      // if we've finished trying all prefixes, exit
   3159      if (tmp == "") {
   3160        break;
   3161      }
   3162 
   3163      tmp = tmp.substring(0, tmp.lastIndexOf("/"));
   3164    }
   3165 
   3166    // no mapping applies, so 404
   3167    if (!file) {
   3168      throw HTTP_404;
   3169    }
   3170 
   3171    // last, get the file for the path within the determined directory
   3172    var parentFolder = file.parent;
   3173    var dirIsRoot = parentFolder == null;
   3174 
   3175    // Strategy here is to append components individually, making sure we
   3176    // never move above the given directory; this allows paths such as
   3177    // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling";
   3178    // this component-wise approach also means the code works even on platforms
   3179    // which don't use "/" as the directory separator, such as Windows
   3180    var leafPath = path.substring(tmp.length + 1);
   3181    var comps = leafPath.split("/");
   3182    for (var i = 0, sz = comps.length; i < sz; i++) {
   3183      var comp = comps[i];
   3184 
   3185      if (comp == "..") {
   3186        file = file.parent;
   3187      } else if (comp == "." || comp == "") {
   3188        continue;
   3189      } else {
   3190        file.append(comp);
   3191      }
   3192 
   3193      if (!dirIsRoot && file.equals(parentFolder)) {
   3194        throw HTTP_403;
   3195      }
   3196    }
   3197 
   3198    return file;
   3199  },
   3200 
   3201  /**
   3202   * Writes the error page for the given HTTP error code over the given
   3203   * connection.
   3204   *
   3205   * @param errorCode : uint
   3206   *   the HTTP error code to be used
   3207   * @param connection : Connection
   3208   *   the connection on which the error occurred
   3209   */
   3210  handleError(errorCode, connection) {
   3211    var response = new Response(connection);
   3212 
   3213    dumpn("*** error in request: " + errorCode);
   3214 
   3215    this._handleError(errorCode, new Request(connection.port), response);
   3216  },
   3217 
   3218  /**
   3219   * Handles a request which generates the given error code, using the
   3220   * user-defined error handler if one has been set, gracefully falling back to
   3221   * the x00 status code if the code has no handler, and failing to status code
   3222   * 500 if all else fails.
   3223   *
   3224   * @param errorCode : uint
   3225   *   the HTTP error which is to be returned
   3226   * @param metadata : Request
   3227   *   metadata for the request, which will often be incomplete since this is an
   3228   *   error
   3229   * @param response : Response
   3230   *   an uninitialized Response should be initialized when this method
   3231   *   completes with information which represents the desired error code in the
   3232   *   ideal case or a fallback code in abnormal circumstances (i.e., 500 is a
   3233   *   fallback for 505, per HTTP specs)
   3234   */
   3235  _handleError(errorCode, metadata, response) {
   3236    if (!metadata) {
   3237      throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
   3238    }
   3239 
   3240    var errorX00 = errorCode - (errorCode % 100);
   3241 
   3242    try {
   3243      if (!(errorCode in HTTP_ERROR_CODES)) {
   3244        dumpn("*** WARNING: requested invalid error: " + errorCode);
   3245      }
   3246 
   3247      // RFC 2616 says that we should try to handle an error by its class if we
   3248      // can't otherwise handle it -- if that fails, we revert to handling it as
   3249      // a 500 internal server error, and if that fails we throw and shut down
   3250      // the server
   3251 
   3252      // actually handle the error
   3253      try {
   3254        if (errorCode in this._overrideErrors) {
   3255          this._overrideErrors[errorCode](metadata, response);
   3256        } else {
   3257          this._defaultErrors[errorCode](metadata, response);
   3258        }
   3259      } catch (e) {
   3260        if (response.partiallySent()) {
   3261          response.abort(e);
   3262          return;
   3263        }
   3264 
   3265        // don't retry the handler that threw
   3266        if (errorX00 == errorCode) {
   3267          throw HTTP_500;
   3268        }
   3269 
   3270        dumpn(
   3271          "*** error in handling for error code " +
   3272            errorCode +
   3273            ", " +
   3274            "falling back to " +
   3275            errorX00 +
   3276            "..."
   3277        );
   3278        response = new Response(response._connection);
   3279        if (errorX00 in this._overrideErrors) {
   3280          this._overrideErrors[errorX00](metadata, response);
   3281        } else if (errorX00 in this._defaultErrors) {
   3282          this._defaultErrors[errorX00](metadata, response);
   3283        } else {
   3284          throw HTTP_500;
   3285        }
   3286      }
   3287    } catch (e) {
   3288      if (response.partiallySent()) {
   3289        response.abort();
   3290        return;
   3291      }
   3292 
   3293      // we've tried everything possible for a meaningful error -- now try 500
   3294      dumpn(
   3295        "*** error in handling for error code " +
   3296          errorX00 +
   3297          ", falling " +
   3298          "back to 500..."
   3299      );
   3300 
   3301      try {
   3302        response = new Response(response._connection);
   3303        if (500 in this._overrideErrors) {
   3304          this._overrideErrors[500](metadata, response);
   3305        } else {
   3306          this._defaultErrors[500](metadata, response);
   3307        }
   3308      } catch (e2) {
   3309        dumpn("*** multiple errors in default error handlers!");
   3310        dumpn("*** e == " + e + ", e2 == " + e2);
   3311        response.abort(e2);
   3312        return;
   3313      }
   3314    }
   3315 
   3316    response.complete();
   3317  },
   3318 
   3319  // FIELDS
   3320 
   3321  /**
   3322   * This object contains the default handlers for the various HTTP error codes.
   3323   */
   3324  _defaultErrors: {
   3325    400(metadata, response) {
   3326      // none of the data in metadata is reliable, so hard-code everything here
   3327      response.setStatusLine("1.1", 400, "Bad Request");
   3328      response.setHeader("Content-Type", "text/plain;charset=utf-8", false);
   3329 
   3330      var body = "Bad request\n";
   3331      response.bodyOutputStream.write(body, body.length);
   3332    },
   3333    403(metadata, response) {
   3334      response.setStatusLine(metadata.httpVersion, 403, "Forbidden");
   3335      response.setHeader("Content-Type", "text/html;charset=utf-8", false);
   3336 
   3337      var body =
   3338        "<html>\
   3339                    <head><title>403 Forbidden</title></head>\
   3340                    <body>\
   3341                      <h1>403 Forbidden</h1>\
   3342                    </body>\
   3343                  </html>";
   3344      response.bodyOutputStream.write(body, body.length);
   3345    },
   3346    404(metadata, response) {
   3347      response.setStatusLine(metadata.httpVersion, 404, "Not Found");
   3348      response.setHeader("Content-Type", "text/html;charset=utf-8", false);
   3349 
   3350      var body =
   3351        "<html>\
   3352                    <head><title>404 Not Found</title></head>\
   3353                    <body>\
   3354                      <h1>404 Not Found</h1>\
   3355                      <p>\
   3356                        <span style='font-family: monospace;'>" +
   3357        htmlEscape(metadata.path) +
   3358        "</span> was not found.\
   3359                      </p>\
   3360                    </body>\
   3361                  </html>";
   3362      response.bodyOutputStream.write(body, body.length);
   3363    },
   3364    416(metadata, response) {
   3365      response.setStatusLine(
   3366        metadata.httpVersion,
   3367        416,
   3368        "Requested Range Not Satisfiable"
   3369      );
   3370      response.setHeader("Content-Type", "text/html;charset=utf-8", false);
   3371 
   3372      var body =
   3373        "<html>\
   3374                   <head>\
   3375                    <title>416 Requested Range Not Satisfiable</title></head>\
   3376                    <body>\
   3377                     <h1>416 Requested Range Not Satisfiable</h1>\
   3378                     <p>The byte range was not valid for the\
   3379                        requested resource.\
   3380                     </p>\
   3381                    </body>\
   3382                  </html>";
   3383      response.bodyOutputStream.write(body, body.length);
   3384    },
   3385    500(metadata, response) {
   3386      response.setStatusLine(
   3387        metadata.httpVersion,
   3388        500,
   3389        "Internal Server Error"
   3390      );
   3391      response.setHeader("Content-Type", "text/html;charset=utf-8", false);
   3392 
   3393      var body =
   3394        "<html>\
   3395                    <head><title>500 Internal Server Error</title></head>\
   3396                    <body>\
   3397                      <h1>500 Internal Server Error</h1>\
   3398                      <p>Something's broken in this server and\
   3399                        needs to be fixed.</p>\
   3400                    </body>\
   3401                  </html>";
   3402      response.bodyOutputStream.write(body, body.length);
   3403    },
   3404    501(metadata, response) {
   3405      response.setStatusLine(metadata.httpVersion, 501, "Not Implemented");
   3406      response.setHeader("Content-Type", "text/html;charset=utf-8", false);
   3407 
   3408      var body =
   3409        "<html>\
   3410                    <head><title>501 Not Implemented</title></head>\
   3411                    <body>\
   3412                      <h1>501 Not Implemented</h1>\
   3413                      <p>This server is not (yet) Apache.</p>\
   3414                    </body>\
   3415                  </html>";
   3416      response.bodyOutputStream.write(body, body.length);
   3417    },
   3418    505(metadata, response) {
   3419      response.setStatusLine("1.1", 505, "HTTP Version Not Supported");
   3420      response.setHeader("Content-Type", "text/html;charset=utf-8", false);
   3421 
   3422      var body =
   3423        "<html>\
   3424                    <head><title>505 HTTP Version Not Supported</title></head>\
   3425                    <body>\
   3426                      <h1>505 HTTP Version Not Supported</h1>\
   3427                      <p>This server only supports HTTP/1.0 and HTTP/1.1\
   3428                        connections.</p>\
   3429                    </body>\
   3430                  </html>";
   3431      response.bodyOutputStream.write(body, body.length);
   3432    },
   3433  },
   3434 
   3435  /**
   3436   * Contains handlers for the default set of URIs contained in this server.
   3437   */
   3438  _defaultPaths: {
   3439    "/": function (metadata, response) {
   3440      response.setStatusLine(metadata.httpVersion, 200, "OK");
   3441      response.setHeader("Content-Type", "text/html;charset=utf-8", false);
   3442 
   3443      var body =
   3444        "<html>\
   3445                    <head><title>httpd.js</title></head>\
   3446                    <body>\
   3447                      <h1>httpd.js</h1>\
   3448                      <p>If you're seeing this page, httpd.js is up and\
   3449                        serving requests!  Now set a base path and serve some\
   3450                        files!</p>\
   3451                    </body>\
   3452                  </html>";
   3453 
   3454      response.bodyOutputStream.write(body, body.length);
   3455    },
   3456 
   3457    "/trace": function (metadata, response) {
   3458      response.setStatusLine(metadata.httpVersion, 200, "OK");
   3459      response.setHeader("Content-Type", "text/plain;charset=utf-8", false);
   3460 
   3461      var body =
   3462        "Request-URI: " +
   3463        metadata.scheme +
   3464        "://" +
   3465        metadata.host +
   3466        ":" +
   3467        metadata.port +
   3468        metadata.path +
   3469        "\n\n";
   3470      body += "Request (semantically equivalent, slightly reformatted):\n\n";
   3471      body += metadata.method + " " + metadata.path;
   3472 
   3473      if (metadata.queryString) {
   3474        body += "?" + metadata.queryString;
   3475      }
   3476 
   3477      body += " HTTP/" + metadata.httpVersion + "\r\n";
   3478 
   3479      var headEnum = metadata.headers;
   3480      while (headEnum.hasMoreElements()) {
   3481        var fieldName = headEnum
   3482          .getNext()
   3483          .QueryInterface(Ci.nsISupportsString).data;
   3484        body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n";
   3485      }
   3486 
   3487      response.bodyOutputStream.write(body, body.length);
   3488    },
   3489  },
   3490 };
   3491 
   3492 /**
   3493 * Maps absolute paths to files on the local file system (as nsILocalFiles).
   3494 */
   3495 function FileMap() {
   3496  /** Hash which will map paths to nsILocalFiles. */
   3497  this._map = {};
   3498 }
   3499 FileMap.prototype = {
   3500  // PUBLIC API
   3501 
   3502  /**
   3503   * Maps key to a clone of the nsIFile value if value is non-null;
   3504   * otherwise, removes any extant mapping for key.
   3505   *
   3506   * @param key : string
   3507   *   string to which a clone of value is mapped
   3508   * @param value : nsIFile
   3509   *   the file to map to key, or null to remove a mapping
   3510   */
   3511  put(key, value) {
   3512    if (value) {
   3513      this._map[key] = value.clone();
   3514    } else {
   3515      delete this._map[key];
   3516    }
   3517  },
   3518 
   3519  /**
   3520   * Returns a clone of the nsIFile mapped to key, or null if no such
   3521   * mapping exists.
   3522   *
   3523   * @param key : string
   3524   *   key to which the returned file maps
   3525   * @returns nsIFile
   3526   *   a clone of the mapped file, or null if no mapping exists
   3527   */
   3528  get(key) {
   3529    var val = this._map[key];
   3530    return val ? val.clone() : null;
   3531  },
   3532 };
   3533 
   3534 // Response CONSTANTS
   3535 
   3536 // token       = *<any CHAR except CTLs or separators>
   3537 // CHAR        = <any US-ASCII character (0-127)>
   3538 // CTL         = <any US-ASCII control character (0-31) and DEL (127)>
   3539 // separators  = "(" | ")" | "<" | ">" | "@"
   3540 //             | "," | ";" | ":" | "\" | <">
   3541 //             | "/" | "[" | "]" | "?" | "="
   3542 //             | "{" | "}" | SP  | HT
   3543 const IS_TOKEN_ARRAY = [
   3544  0,
   3545  0,
   3546  0,
   3547  0,
   3548  0,
   3549  0,
   3550  0,
   3551  0, //   0
   3552  0,
   3553  0,
   3554  0,
   3555  0,
   3556  0,
   3557  0,
   3558  0,
   3559  0, //   8
   3560  0,
   3561  0,
   3562  0,
   3563  0,
   3564  0,
   3565  0,
   3566  0,
   3567  0, //  16
   3568  0,
   3569  0,
   3570  0,
   3571  0,
   3572  0,
   3573  0,
   3574  0,
   3575  0, //  24
   3576 
   3577  0,
   3578  1,
   3579  0,
   3580  1,
   3581  1,
   3582  1,
   3583  1,
   3584  1, //  32
   3585  0,
   3586  0,
   3587  1,
   3588  1,
   3589  0,
   3590  1,
   3591  1,
   3592  0, //  40
   3593  1,
   3594  1,
   3595  1,
   3596  1,
   3597  1,
   3598  1,
   3599  1,
   3600  1, //  48
   3601  1,
   3602  1,
   3603  0,
   3604  0,
   3605  0,
   3606  0,
   3607  0,
   3608  0, //  56
   3609 
   3610  0,
   3611  1,
   3612  1,
   3613  1,
   3614  1,
   3615  1,
   3616  1,
   3617  1, //  64
   3618  1,
   3619  1,
   3620  1,
   3621  1,
   3622  1,
   3623  1,
   3624  1,
   3625  1, //  72
   3626  1,
   3627  1,
   3628  1,
   3629  1,
   3630  1,
   3631  1,
   3632  1,
   3633  1, //  80
   3634  1,
   3635  1,
   3636  1,
   3637  0,
   3638  0,
   3639  0,
   3640  1,
   3641  1, //  88
   3642 
   3643  1,
   3644  1,
   3645  1,
   3646  1,
   3647  1,
   3648  1,
   3649  1,
   3650  1, //  96
   3651  1,
   3652  1,
   3653  1,
   3654  1,
   3655  1,
   3656  1,
   3657  1,
   3658  1, // 104
   3659  1,
   3660  1,
   3661  1,
   3662  1,
   3663  1,
   3664  1,
   3665  1,
   3666  1, // 112
   3667  1,
   3668  1,
   3669  1,
   3670  0,
   3671  1,
   3672  0,
   3673  1,
   3674 ]; // 120
   3675 
   3676 /**
   3677 * Determines whether the given character code is a CTL.
   3678 *
   3679 * @param code : uint
   3680 *   the character code
   3681 * @returns boolean
   3682 *   true if code is a CTL, false otherwise
   3683 */
   3684 function isCTL(code) {
   3685  return (code >= 0 && code <= 31) || code == 127;
   3686 }
   3687 
   3688 /**
   3689 * Represents a response to an HTTP request, encapsulating all details of that
   3690 * response.  This includes all headers, the HTTP version, status code and
   3691 * explanation, and the entity itself.
   3692 *
   3693 * @param connection : Connection
   3694 *   the connection over which this response is to be written
   3695 */
   3696 function Response(connection) {
   3697  /** The connection over which this response will be written. */
   3698  this._connection = connection;
   3699 
   3700  /**
   3701   * The HTTP version of this response; defaults to 1.1 if not set by the
   3702   * handler.
   3703   */
   3704  this._httpVersion = nsHttpVersion.HTTP_1_1;
   3705 
   3706  /**
   3707   * The HTTP code of this response; defaults to 200.
   3708   */
   3709  this._httpCode = 200;
   3710 
   3711  /**
   3712   * The description of the HTTP code in this response; defaults to "OK".
   3713   */
   3714  this._httpDescription = "OK";
   3715 
   3716  /**
   3717   * An nsIHttpHeaders object in which the headers in this response should be
   3718   * stored.  This property is null after the status line and headers have been
   3719   * written to the network, and it may be modified up until it is cleared,
   3720   * except if this._finished is set first (in which case headers are written
   3721   * asynchronously in response to a finish() call not preceded by
   3722   * flushHeaders()).
   3723   */
   3724  this._headers = new nsHttpHeaders();
   3725 
   3726  /**
   3727   * Informational response:
   3728   * For example 103 Early Hint
   3729   */
   3730  this._informationalResponseHttpVersion = nsHttpVersion.HTTP_1_1;
   3731  this._informationalResponseHttpCode = 0;
   3732  this._informationalResponseHttpDescription = "";
   3733  this._informationalResponseHeaders = new nsHttpHeaders();
   3734  this._informationalResponseSet = false;
   3735 
   3736  /**
   3737   * Set to true when this response is ended (completely constructed if possible
   3738   * and the connection closed); further actions on this will then fail.
   3739   */
   3740  this._ended = false;
   3741 
   3742  /**
   3743   * A stream used to hold data written to the body of this response.
   3744   */
   3745  this._bodyOutputStream = null;
   3746 
   3747  /**
   3748   * A stream containing all data that has been written to the body of this
   3749   * response so far.  (Async handlers make the data contained in this
   3750   * unreliable as a way of determining content length in general, but auxiliary
   3751   * saved information can sometimes be used to guarantee reliability.)
   3752   */
   3753  this._bodyInputStream = null;
   3754 
   3755  /**
   3756   * A stream copier which copies data to the network.  It is initially null
   3757   * until replaced with a copier for response headers; when headers have been
   3758   * fully sent it is replaced with a copier for the response body, remaining
   3759   * so for the duration of response processing.
   3760   */
   3761  this._asyncCopier = null;
   3762 
   3763  /**
   3764   * True if this response has been designated as being processed
   3765   * asynchronously rather than for the duration of a single call to
   3766   * nsIHttpRequestHandler.handle.
   3767   */
   3768  this._processAsync = false;
   3769 
   3770  /**
   3771   * True iff finish() has been called on this, signaling that no more changes
   3772   * to this may be made.
   3773   */
   3774  this._finished = false;
   3775 
   3776  /**
   3777   * True iff powerSeized() has been called on this, signaling that this
   3778   * response is to be handled manually by the response handler (which may then
   3779   * send arbitrary data in response, even non-HTTP responses).
   3780   */
   3781  this._powerSeized = false;
   3782 }
   3783 Response.prototype = {
   3784  // PUBLIC CONSTRUCTION API
   3785 
   3786  //
   3787  // see nsIHttpResponse.bodyOutputStream
   3788  //
   3789  get bodyOutputStream() {
   3790    if (this._finished) {
   3791      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   3792    }
   3793 
   3794    if (!this._bodyOutputStream) {
   3795      var pipe = new Pipe(
   3796        true,
   3797        false,
   3798        Response.SEGMENT_SIZE,
   3799        PR_UINT32_MAX,
   3800        null
   3801      );
   3802      this._bodyOutputStream = pipe.outputStream;
   3803      this._bodyInputStream = pipe.inputStream;
   3804      if (this._processAsync || this._powerSeized) {
   3805        this._startAsyncProcessor();
   3806      }
   3807    }
   3808 
   3809    return this._bodyOutputStream;
   3810  },
   3811 
   3812  //
   3813  // see nsIHttpResponse.write
   3814  //
   3815  write(data) {
   3816    if (this._finished) {
   3817      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   3818    }
   3819 
   3820    var dataAsString = String(data);
   3821    this.bodyOutputStream.write(dataAsString, dataAsString.length);
   3822  },
   3823 
   3824  //
   3825  // see nsIHttpResponse.setStatusLine
   3826  //
   3827  setStatusLineInternal(httpVersion, code, description, informationalResponse) {
   3828    if (this._finished || this._powerSeized) {
   3829      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   3830    }
   3831 
   3832    if (!informationalResponse) {
   3833      if (!this._headers) {
   3834        throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   3835      }
   3836    } else if (!this._informationalResponseHeaders) {
   3837      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   3838    }
   3839    this._ensureAlive();
   3840 
   3841    if (!(code >= 0 && code < 1000)) {
   3842      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   3843    }
   3844 
   3845    try {
   3846      var httpVer;
   3847      // avoid version construction for the most common cases
   3848      if (!httpVersion || httpVersion == "1.1") {
   3849        httpVer = nsHttpVersion.HTTP_1_1;
   3850      } else if (httpVersion == "1.0") {
   3851        httpVer = nsHttpVersion.HTTP_1_0;
   3852      } else {
   3853        httpVer = new nsHttpVersion(httpVersion);
   3854      }
   3855    } catch (e) {
   3856      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   3857    }
   3858 
   3859    // Reason-Phrase = *<TEXT, excluding CR, LF>
   3860    // TEXT          = <any OCTET except CTLs, but including LWS>
   3861    //
   3862    // XXX this ends up disallowing octets which aren't Unicode, I think -- not
   3863    //     much to do if description is IDL'd as string
   3864    if (!description) {
   3865      description = "";
   3866    }
   3867    for (var i = 0; i < description.length; i++) {
   3868      if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t") {
   3869        throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   3870      }
   3871    }
   3872 
   3873    // set the values only after validation to preserve atomicity
   3874    if (!informationalResponse) {
   3875      this._httpDescription = description;
   3876      this._httpCode = code;
   3877      this._httpVersion = httpVer;
   3878    } else {
   3879      this._informationalResponseSet = true;
   3880      this._informationalResponseHttpDescription = description;
   3881      this._informationalResponseHttpCode = code;
   3882      this._informationalResponseHttpVersion = httpVer;
   3883    }
   3884  },
   3885 
   3886  //
   3887  // see nsIHttpResponse.setStatusLine
   3888  //
   3889  setStatusLine(httpVersion, code, description) {
   3890    this.setStatusLineInternal(httpVersion, code, description, false);
   3891  },
   3892 
   3893  setInformationalResponseStatusLine(httpVersion, code, description) {
   3894    this.setStatusLineInternal(httpVersion, code, description, true);
   3895  },
   3896 
   3897  //
   3898  // see nsIHttpResponse.setHeader
   3899  //
   3900  setHeader(name, value, merge = false) {
   3901    if (!this._headers || this._finished || this._powerSeized) {
   3902      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   3903    }
   3904    this._ensureAlive();
   3905 
   3906    this._headers.setHeader(name, value, merge);
   3907  },
   3908 
   3909  setInformationalResponseHeader(name, value, merge) {
   3910    if (
   3911      !this._informationalResponseHeaders ||
   3912      this._finished ||
   3913      this._powerSeized
   3914    ) {
   3915      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   3916    }
   3917    this._ensureAlive();
   3918 
   3919    this._informationalResponseHeaders.setHeader(name, value, merge);
   3920  },
   3921 
   3922  setHeaderNoCheck(name, value) {
   3923    if (!this._headers || this._finished || this._powerSeized) {
   3924      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   3925    }
   3926    this._ensureAlive();
   3927 
   3928    this._headers.setHeaderNoCheck(name, value);
   3929  },
   3930 
   3931  setInformationalHeaderNoCheck(name, value) {
   3932    if (!this._headers || this._finished || this._powerSeized) {
   3933      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   3934    }
   3935    this._ensureAlive();
   3936 
   3937    this._informationalResponseHeaders.setHeaderNoCheck(name, value);
   3938  },
   3939 
   3940  //
   3941  // see nsIHttpResponse.processAsync
   3942  //
   3943  processAsync() {
   3944    if (this._finished) {
   3945      throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
   3946    }
   3947    if (this._powerSeized) {
   3948      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   3949    }
   3950    if (this._processAsync) {
   3951      return;
   3952    }
   3953    this._ensureAlive();
   3954 
   3955    dumpn("*** processing connection " + this._connection.number + " async");
   3956    this._processAsync = true;
   3957 
   3958    /*
   3959     * Either the bodyOutputStream getter or this method is responsible for
   3960     * starting the asynchronous processor and catching writes of data to the
   3961     * response body of async responses as they happen, for the purpose of
   3962     * forwarding those writes to the actual connection's output stream.
   3963     * If bodyOutputStream is accessed first, calling this method will create
   3964     * the processor (when it first is clear that body data is to be written
   3965     * immediately, not buffered).  If this method is called first, accessing
   3966     * bodyOutputStream will create the processor.  If only this method is
   3967     * called, we'll write nothing, neither headers nor the nonexistent body,
   3968     * until finish() is called.  Since that delay is easily avoided by simply
   3969     * getting bodyOutputStream or calling write(""), we don't worry about it.
   3970     */
   3971    if (this._bodyOutputStream && !this._asyncCopier) {
   3972      this._startAsyncProcessor();
   3973    }
   3974  },
   3975 
   3976  //
   3977  // see nsIHttpResponse.seizePower
   3978  //
   3979  seizePower() {
   3980    if (this._processAsync) {
   3981      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   3982    }
   3983    if (this._finished) {
   3984      throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
   3985    }
   3986    if (this._powerSeized) {
   3987      return;
   3988    }
   3989    this._ensureAlive();
   3990 
   3991    dumpn(
   3992      "*** forcefully seizing power over connection " +
   3993        this._connection.number +
   3994        "..."
   3995    );
   3996 
   3997    // Purge any already-written data without sending it.  We could as easily
   3998    // swap out the streams entirely, but that makes it possible to acquire and
   3999    // unknowingly use a stale reference, so we require there only be one of
   4000    // each stream ever for any response to avoid this complication.
   4001    if (this._asyncCopier) {
   4002      this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED);
   4003    }
   4004    this._asyncCopier = null;
   4005    if (this._bodyOutputStream) {
   4006      var input = new BinaryInputStream(this._bodyInputStream);
   4007      var avail;
   4008      while ((avail = input.available()) > 0) {
   4009        input.readByteArray(avail);
   4010      }
   4011    }
   4012 
   4013    this._powerSeized = true;
   4014    if (this._bodyOutputStream) {
   4015      this._startAsyncProcessor();
   4016    }
   4017  },
   4018 
   4019  //
   4020  // see nsIHttpResponse.finish
   4021  //
   4022  finish() {
   4023    if (!this._processAsync && !this._powerSeized) {
   4024      throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
   4025    }
   4026    if (this._finished) {
   4027      return;
   4028    }
   4029 
   4030    dumpn("*** finishing connection " + this._connection.number);
   4031    this._startAsyncProcessor(); // in case bodyOutputStream was never accessed
   4032    if (this._bodyOutputStream) {
   4033      this._bodyOutputStream.close();
   4034    }
   4035    this._finished = true;
   4036  },
   4037 
   4038  // NSISUPPORTS
   4039 
   4040  //
   4041  // see nsISupports.QueryInterface
   4042  //
   4043  QueryInterface: ChromeUtils.generateQI(["nsIHttpResponse"]),
   4044 
   4045  // POST-CONSTRUCTION API (not exposed externally)
   4046 
   4047  /**
   4048   * The HTTP version number of this, as a string (e.g. "1.1").
   4049   */
   4050  get httpVersion() {
   4051    this._ensureAlive();
   4052    return this._httpVersion.toString();
   4053  },
   4054 
   4055  /**
   4056   * The HTTP status code of this response, as a string of three characters per
   4057   * RFC 2616.
   4058   */
   4059  get httpCode() {
   4060    this._ensureAlive();
   4061 
   4062    var codeString =
   4063      (this._httpCode < 10 ? "0" : "") +
   4064      (this._httpCode < 100 ? "0" : "") +
   4065      this._httpCode;
   4066    return codeString;
   4067  },
   4068 
   4069  /**
   4070   * The description of the HTTP status code of this response, or "" if none is
   4071   * set.
   4072   */
   4073  get httpDescription() {
   4074    this._ensureAlive();
   4075 
   4076    return this._httpDescription;
   4077  },
   4078 
   4079  /**
   4080   * The headers in this response, as an nsHttpHeaders object.
   4081   */
   4082  get headers() {
   4083    this._ensureAlive();
   4084 
   4085    return this._headers;
   4086  },
   4087 
   4088  //
   4089  // see nsHttpHeaders.getHeader
   4090  //
   4091  getHeader(name) {
   4092    this._ensureAlive();
   4093 
   4094    return this._headers.getHeader(name);
   4095  },
   4096 
   4097  /**
   4098   * Determines whether this response may be abandoned in favor of a newly
   4099   * constructed response.  A response may be abandoned only if it is not being
   4100   * sent asynchronously and if raw control over it has not been taken from the
   4101   * server.
   4102   *
   4103   * @returns boolean
   4104   *   true iff no data has been written to the network
   4105   */
   4106  partiallySent() {
   4107    dumpn("*** partiallySent()");
   4108    return this._processAsync || this._powerSeized;
   4109  },
   4110 
   4111  /**
   4112   * If necessary, kicks off the remaining request processing needed to be done
   4113   * after a request handler performs its initial work upon this response.
   4114   */
   4115  complete() {
   4116    dumpn("*** complete()");
   4117    if (this._processAsync || this._powerSeized) {
   4118      NS_ASSERT(
   4119        this._processAsync ^ this._powerSeized,
   4120        "can't both send async and relinquish power"
   4121      );
   4122      return;
   4123    }
   4124 
   4125    NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?");
   4126 
   4127    this._startAsyncProcessor();
   4128 
   4129    // Now make sure we finish processing this request!
   4130    if (this._bodyOutputStream) {
   4131      this._bodyOutputStream.close();
   4132    }
   4133  },
   4134 
   4135  /**
   4136   * Abruptly ends processing of this response, usually due to an error in an
   4137   * incoming request but potentially due to a bad error handler.  Since we
   4138   * cannot handle the error in the usual way (giving an HTTP error page in
   4139   * response) because data may already have been sent (or because the response
   4140   * might be expected to have been generated asynchronously or completely from
   4141   * scratch by the handler), we stop processing this response and abruptly
   4142   * close the connection.
   4143   *
   4144   * @param e : Error
   4145   *   the exception which precipitated this abort, or null if no such exception
   4146   *   was generated
   4147   * @param truncateConnection : Boolean
   4148   *   ensures that we truncate the connection using an RST packet, so the
   4149   *   client testing code is aware that an error occurred, otherwise it may
   4150   *   consider the response as valid.
   4151   */
   4152  abort(e, truncateConnection = false) {
   4153    dumpn("*** abort(<" + e + ">)");
   4154 
   4155    if (truncateConnection) {
   4156      dumpn("*** truncate connection");
   4157      this._connection.transport.setLinger(true, 0);
   4158    }
   4159 
   4160    // This response will be ended by the processor if one was created.
   4161    var copier = this._asyncCopier;
   4162    if (copier) {
   4163      // We dispatch asynchronously here so that any pending writes of data to
   4164      // the connection will be deterministically written.  This makes it easier
   4165      // to specify exact behavior, and it makes observable behavior more
   4166      // predictable for clients.  Note that the correctness of this depends on
   4167      // callbacks in response to _waitToReadData in WriteThroughCopier
   4168      // happening asynchronously with respect to the actual writing of data to
   4169      // bodyOutputStream, as they currently do; if they happened synchronously,
   4170      // an event which ran before this one could write more data to the
   4171      // response body before we get around to canceling the copier.  We have
   4172      // tests for this in test_seizepower.js, however, and I can't think of a
   4173      // way to handle both cases without removing bodyOutputStream access and
   4174      // moving its effective write(data, length) method onto Response, which
   4175      // would be slower and require more code than this anyway.
   4176      Services.tm.currentThread.dispatch(
   4177        {
   4178          run() {
   4179            dumpn("*** canceling copy asynchronously...");
   4180            copier.cancel(Cr.NS_ERROR_UNEXPECTED);
   4181          },
   4182        },
   4183        Ci.nsIThread.DISPATCH_NORMAL
   4184      );
   4185    } else {
   4186      this.end();
   4187    }
   4188  },
   4189 
   4190  /**
   4191   * Closes this response's network connection, marks the response as finished,
   4192   * and notifies the server handler that the request is done being processed.
   4193   */
   4194  end() {
   4195    NS_ASSERT(!this._ended, "ending this response twice?!?!");
   4196 
   4197    this._connection.close();
   4198    if (this._bodyOutputStream) {
   4199      this._bodyOutputStream.close();
   4200    }
   4201 
   4202    this._finished = true;
   4203    this._ended = true;
   4204  },
   4205 
   4206  // PRIVATE IMPLEMENTATION
   4207 
   4208  /**
   4209   * Sends the status line and headers of this response if they haven't been
   4210   * sent and initiates the process of copying data written to this response's
   4211   * body to the network.
   4212   */
   4213  _startAsyncProcessor() {
   4214    dumpn("*** _startAsyncProcessor()");
   4215 
   4216    // Handle cases where we're being called a second time.  The former case
   4217    // happens when this is triggered both by complete() and by processAsync(),
   4218    // while the latter happens when processAsync() in conjunction with sent
   4219    // data causes abort() to be called.
   4220    if (this._asyncCopier || this._ended) {
   4221      dumpn("*** ignoring second call to _startAsyncProcessor");
   4222      return;
   4223    }
   4224 
   4225    // Send headers if they haven't been sent already and should be sent, then
   4226    // asynchronously continue to send the body.
   4227    if (this._headers && !this._powerSeized) {
   4228      this._sendHeaders();
   4229      return;
   4230    }
   4231 
   4232    this._headers = null;
   4233    this._sendBody();
   4234  },
   4235 
   4236  /**
   4237   * Signals that all modifications to the response status line and headers are
   4238   * complete and then sends that data over the network to the client.  Once
   4239   * this method completes, a different response to the request that resulted
   4240   * in this response cannot be sent -- the only possible action in case of
   4241   * error is to abort the response and close the connection.
   4242   */
   4243  _sendHeaders() {
   4244    dumpn("*** _sendHeaders()");
   4245 
   4246    NS_ASSERT(this._headers);
   4247    NS_ASSERT(this._informationalResponseHeaders);
   4248    NS_ASSERT(!this._powerSeized);
   4249 
   4250    var preambleData = [];
   4251 
   4252    // Informational response, e.g. 103
   4253    if (this._informationalResponseSet) {
   4254      // request-line
   4255      let statusLine =
   4256        "HTTP/" +
   4257        this._informationalResponseHttpVersion +
   4258        " " +
   4259        this._informationalResponseHttpCode +
   4260        " " +
   4261        this._informationalResponseHttpDescription +
   4262        "\r\n";
   4263      preambleData.push(statusLine);
   4264 
   4265      // headers
   4266      let headEnum = this._informationalResponseHeaders.enumerator;
   4267      while (headEnum.hasMoreElements()) {
   4268        let fieldName = headEnum
   4269          .getNext()
   4270          .QueryInterface(Ci.nsISupportsString).data;
   4271        let values =
   4272          this._informationalResponseHeaders.getHeaderValues(fieldName);
   4273        for (let i = 0, sz = values.length; i < sz; i++) {
   4274          preambleData.push(fieldName + ": " + values[i] + "\r\n");
   4275        }
   4276      }
   4277      // end request-line/headers
   4278      preambleData.push("\r\n");
   4279    }
   4280 
   4281    // request-line
   4282    var statusLine =
   4283      "HTTP/" +
   4284      this.httpVersion +
   4285      " " +
   4286      this.httpCode +
   4287      " " +
   4288      this.httpDescription +
   4289      "\r\n";
   4290 
   4291    // header post-processing
   4292 
   4293    var headers = this._headers;
   4294    headers.setHeader("Connection", "close", false);
   4295    headers.setHeader("Server", "httpd.js", false);
   4296    if (!headers.hasHeader("Date")) {
   4297      headers.setHeader("Date", toDateString(Date.now()), false);
   4298    }
   4299 
   4300    // Any response not being processed asynchronously must have an associated
   4301    // Content-Length header for reasons of backwards compatibility with the
   4302    // initial server, which fully buffered every response before sending it.
   4303    // Beyond that, however, it's good to do this anyway because otherwise it's
   4304    // impossible to test behaviors that depend on the presence or absence of a
   4305    // Content-Length header.
   4306    if (!this._processAsync) {
   4307      dumpn("*** non-async response, set Content-Length");
   4308 
   4309      var bodyStream = this._bodyInputStream;
   4310      var avail = bodyStream ? bodyStream.available() : 0;
   4311 
   4312      // XXX assumes stream will always report the full amount of data available
   4313      headers.setHeader("Content-Length", "" + avail, false);
   4314    }
   4315 
   4316    // construct and send response
   4317    dumpn("*** header post-processing completed, sending response head...");
   4318 
   4319    // request-line
   4320    preambleData.push(statusLine);
   4321 
   4322    // headers
   4323    var headEnum = headers.enumerator;
   4324    while (headEnum.hasMoreElements()) {
   4325      var fieldName = headEnum
   4326        .getNext()
   4327        .QueryInterface(Ci.nsISupportsString).data;
   4328      var values = headers.getHeaderValues(fieldName);
   4329      for (var i = 0, sz = values.length; i < sz; i++) {
   4330        preambleData.push(fieldName + ": " + values[i] + "\r\n");
   4331      }
   4332    }
   4333 
   4334    // end request-line/headers
   4335    preambleData.push("\r\n");
   4336 
   4337    var preamble = preambleData.join("");
   4338 
   4339    var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null);
   4340    responseHeadPipe.outputStream.write(preamble, preamble.length);
   4341 
   4342    var response = this;
   4343    var copyObserver = {
   4344      onStartRequest() {
   4345        dumpn("*** preamble copying started");
   4346      },
   4347 
   4348      onStopRequest(request, statusCode) {
   4349        dumpn(
   4350          "*** preamble copying complete " +
   4351            "[status=0x" +
   4352            statusCode.toString(16) +
   4353            "]"
   4354        );
   4355 
   4356        if (!Components.isSuccessCode(statusCode)) {
   4357          dumpn(
   4358            "!!! header copying problems: non-success statusCode, " +
   4359              "ending response"
   4360          );
   4361 
   4362          response.end();
   4363        } else {
   4364          response._sendBody();
   4365        }
   4366      },
   4367 
   4368      QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]),
   4369    };
   4370 
   4371    this._asyncCopier = new WriteThroughCopier(
   4372      responseHeadPipe.inputStream,
   4373      this._connection.output,
   4374      copyObserver,
   4375      null
   4376    );
   4377 
   4378    responseHeadPipe.outputStream.close();
   4379 
   4380    // Forbid setting any more headers or modifying the request line.
   4381    this._headers = null;
   4382  },
   4383 
   4384  /**
   4385   * Asynchronously writes the body of the response (or the entire response, if
   4386   * seizePower() has been called) to the network.
   4387   */
   4388  _sendBody() {
   4389    dumpn("*** _sendBody");
   4390 
   4391    NS_ASSERT(!this._headers, "still have headers around but sending body?");
   4392 
   4393    // If no body data was written, we're done
   4394    if (!this._bodyInputStream) {
   4395      dumpn("*** empty body, response finished");
   4396      this.end();
   4397      return;
   4398    }
   4399 
   4400    var response = this;
   4401    var copyObserver = {
   4402      onStartRequest() {
   4403        dumpn("*** onStartRequest");
   4404      },
   4405 
   4406      onStopRequest(request, statusCode) {
   4407        dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]");
   4408 
   4409        if (statusCode === Cr.NS_BINDING_ABORTED) {
   4410          dumpn("*** terminating copy observer without ending the response");
   4411        } else {
   4412          if (!Components.isSuccessCode(statusCode)) {
   4413            dumpn("*** WARNING: non-success statusCode in onStopRequest");
   4414          }
   4415 
   4416          response.end();
   4417        }
   4418      },
   4419 
   4420      QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]),
   4421    };
   4422 
   4423    dumpn("*** starting async copier of body data...");
   4424    this._asyncCopier = new WriteThroughCopier(
   4425      this._bodyInputStream,
   4426      this._connection.output,
   4427      copyObserver,
   4428      null
   4429    );
   4430  },
   4431 
   4432  /** Ensures that this hasn't been ended. */
   4433  _ensureAlive() {
   4434    NS_ASSERT(!this._ended, "not handling response lifetime correctly");
   4435  },
   4436 };
   4437 
   4438 /**
   4439 * Size of the segments in the buffer used in storing response data and writing
   4440 * it to the socket.
   4441 */
   4442 Response.SEGMENT_SIZE = 8192;
   4443 
   4444 /** Serves double duty in WriteThroughCopier implementation. */
   4445 function notImplemented() {
   4446  throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
   4447 }
   4448 
   4449 /** Returns true iff the given exception represents stream closure. */
   4450 function streamClosed(e) {
   4451  return (
   4452    e === Cr.NS_BASE_STREAM_CLOSED ||
   4453    (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED)
   4454  );
   4455 }
   4456 
   4457 /** Returns true iff the given exception represents a blocked stream. */
   4458 function wouldBlock(e) {
   4459  return (
   4460    e === Cr.NS_BASE_STREAM_WOULD_BLOCK ||
   4461    (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK)
   4462  );
   4463 }
   4464 
   4465 /**
   4466 * Copies data from source to sink as it becomes available, when that data can
   4467 * be written to sink without blocking.
   4468 *
   4469 * @param source : nsIAsyncInputStream
   4470 *   the stream from which data is to be read
   4471 * @param sink : nsIAsyncOutputStream
   4472 *   the stream to which data is to be copied
   4473 * @param observer : nsIRequestObserver
   4474 *   an observer which will be notified when the copy starts and finishes
   4475 * @param context : nsISupports
   4476 *   context passed to observer when notified of start/stop
   4477 * @throws NS_ERROR_NULL_POINTER
   4478 *   if source, sink, or observer are null
   4479 */
   4480 export function WriteThroughCopier(source, sink, observer, context) {
   4481  if (!source || !sink || !observer) {
   4482    throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
   4483  }
   4484 
   4485  /** Stream from which data is being read. */
   4486  this._source = source;
   4487 
   4488  /** Stream to which data is being written. */
   4489  this._sink = sink;
   4490 
   4491  /** Observer watching this copy. */
   4492  this._observer = observer;
   4493 
   4494  /** Context for the observer watching this. */
   4495  this._context = context;
   4496 
   4497  /**
   4498   * True iff this is currently being canceled (cancel has been called, the
   4499   * callback may not yet have been made).
   4500   */
   4501  this._canceled = false;
   4502 
   4503  /**
   4504   * False until all data has been read from input and written to output, at
   4505   * which point this copy is completed and cancel() is asynchronously called.
   4506   */
   4507  this._completed = false;
   4508 
   4509  /** Required by nsIRequest, meaningless. */
   4510  this.loadFlags = 0;
   4511  /** Required by nsIRequest, meaningless. */
   4512  this.loadGroup = null;
   4513  /** Required by nsIRequest, meaningless. */
   4514  this.name = "response-body-copy";
   4515 
   4516  /** Status of this request. */
   4517  this.status = Cr.NS_OK;
   4518 
   4519  /** Arrays of byte strings waiting to be written to output. */
   4520  this._pendingData = [];
   4521 
   4522  // start copying
   4523  try {
   4524    observer.onStartRequest(this);
   4525    this._waitToReadData();
   4526    this._waitForSinkClosure();
   4527  } catch (e) {
   4528    dumpn(
   4529      "!!! error starting copy: " +
   4530        e +
   4531        ("lineNumber" in e ? ", line " + e.lineNumber : "")
   4532    );
   4533    dumpn(e.stack);
   4534    this.cancel(Cr.NS_ERROR_UNEXPECTED);
   4535  }
   4536 }
   4537 WriteThroughCopier.prototype = {
   4538  /* nsISupports implementation */
   4539 
   4540  QueryInterface: ChromeUtils.generateQI([
   4541    "nsIInputStreamCallback",
   4542    "nsIOutputStreamCallback",
   4543    "nsIRequest",
   4544  ]),
   4545 
   4546  // NSIINPUTSTREAMCALLBACK
   4547 
   4548  /**
   4549   * Receives a more-data-in-input notification and writes the corresponding
   4550   * data to the output.
   4551   *
   4552   * @param input : nsIAsyncInputStream
   4553   *   the input stream on whose data we have been waiting
   4554   */
   4555  onInputStreamReady(input) {
   4556    if (this._source === null) {
   4557      return;
   4558    }
   4559 
   4560    dumpn("*** onInputStreamReady");
   4561 
   4562    //
   4563    // Ordinarily we'll read a non-zero amount of data from input, queue it up
   4564    // to be written and then wait for further callbacks.  The complications in
   4565    // this method are the cases where we deviate from that behavior when errors
   4566    // occur or when copying is drawing to a finish.
   4567    //
   4568    // The edge cases when reading data are:
   4569    //
   4570    //   Zero data is read
   4571    //     If zero data was read, we're at the end of available data, so we can
   4572    //     should stop reading and move on to writing out what we have (or, if
   4573    //     we've already done that, onto notifying of completion).
   4574    //   A stream-closed exception is thrown
   4575    //     This is effectively a less kind version of zero data being read; the
   4576    //     only difference is that we notify of completion with that result
   4577    //     rather than with NS_OK.
   4578    //   Some other exception is thrown
   4579    //     This is the least kind result.  We don't know what happened, so we
   4580    //     act as though the stream closed except that we notify of completion
   4581    //     with the result NS_ERROR_UNEXPECTED.
   4582    //
   4583 
   4584    var bytesWanted = 0,
   4585      bytesConsumed = -1;
   4586    try {
   4587      input = new BinaryInputStream(input);
   4588 
   4589      bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE);
   4590      dumpn("*** input wanted: " + bytesWanted);
   4591 
   4592      if (bytesWanted > 0) {
   4593        var data = input.readByteArray(bytesWanted);
   4594        bytesConsumed = data.length;
   4595        this._pendingData.push(String.fromCharCode.apply(String, data));
   4596      }
   4597 
   4598      dumpn("*** " + bytesConsumed + " bytes read");
   4599 
   4600      // Handle the zero-data edge case in the same place as all other edge
   4601      // cases are handled.
   4602      if (bytesWanted === 0) {
   4603        throw Components.Exception("", Cr.NS_BASE_STREAM_CLOSED);
   4604      }
   4605    } catch (e) {
   4606      let rv;
   4607      if (streamClosed(e)) {
   4608        dumpn("*** input stream closed");
   4609        rv = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED;
   4610      } else {
   4611        dumpn("!!! unexpected error reading from input, canceling: " + e);
   4612        rv = Cr.NS_ERROR_UNEXPECTED;
   4613      }
   4614 
   4615      this._doneReadingSource(rv);
   4616      return;
   4617    }
   4618 
   4619    var pendingData = this._pendingData;
   4620 
   4621    NS_ASSERT(bytesConsumed > 0);
   4622    NS_ASSERT(!!pendingData.length, "no pending data somehow?");
   4623    NS_ASSERT(
   4624      !!pendingData[pendingData.length - 1].length,
   4625      "buffered zero bytes of data?"
   4626    );
   4627 
   4628    NS_ASSERT(this._source !== null);
   4629 
   4630    // Reading has gone great, and we've gotten data to write now.  What if we
   4631    // don't have a place to write that data, because output went away just
   4632    // before this read?  Drop everything on the floor, including new data, and
   4633    // cancel at this point.
   4634    if (this._sink === null) {
   4635      pendingData.length = 0;
   4636      this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED);
   4637      return;
   4638    }
   4639 
   4640    // Okay, we've read the data, and we know we have a place to write it.  We
   4641    // need to queue up the data to be written, but *only* if none is queued
   4642    // already -- if data's already queued, the code that actually writes the
   4643    // data will make sure to wait on unconsumed pending data.
   4644    try {
   4645      if (pendingData.length === 1) {
   4646        this._waitToWriteData();
   4647      }
   4648    } catch (e) {
   4649      dumpn(
   4650        "!!! error waiting to write data just read, swallowing and " +
   4651          "writing only what we already have: " +
   4652          e
   4653      );
   4654      this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
   4655      return;
   4656    }
   4657 
   4658    // Whee!  We successfully read some data, and it's successfully queued up to
   4659    // be written.  All that remains now is to wait for more data to read.
   4660    try {
   4661      this._waitToReadData();
   4662    } catch (e) {
   4663      dumpn("!!! error waiting to read more data: " + e);
   4664      this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED);
   4665    }
   4666  },
   4667 
   4668  // NSIOUTPUTSTREAMCALLBACK
   4669 
   4670  /**
   4671   * Callback when data may be written to the output stream without blocking, or
   4672   * when the output stream has been closed.
   4673   *
   4674   * @param output : nsIAsyncOutputStream
   4675   *   the output stream on whose writability we've been waiting, also known as
   4676   *   this._sink
   4677   */
   4678  onOutputStreamReady(output) {
   4679    if (this._sink === null) {
   4680      return;
   4681    }
   4682 
   4683    dumpn("*** onOutputStreamReady");
   4684 
   4685    var pendingData = this._pendingData;
   4686    if (pendingData.length === 0) {
   4687      // There's no pending data to write.  The only way this can happen is if
   4688      // we're waiting on the output stream's closure, so we can respond to a
   4689      // copying failure as quickly as possible (rather than waiting for data to
   4690      // be available to read and then fail to be copied).  Therefore, we must
   4691      // be done now -- don't bother to attempt to write anything and wrap
   4692      // things up.
   4693      dumpn("!!! output stream closed prematurely, ending copy");
   4694 
   4695      this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
   4696      return;
   4697    }
   4698 
   4699    NS_ASSERT(!!pendingData[0].length, "queued up an empty quantum?");
   4700 
   4701    //
   4702    // Write out the first pending quantum of data.  The possible errors here
   4703    // are:
   4704    //
   4705    //   The write might fail because we can't write that much data
   4706    //     Okay, we've written what we can now, so re-queue what's left and
   4707    //     finish writing it out later.
   4708    //   The write failed because the stream was closed
   4709    //     Discard pending data that we can no longer write, stop reading, and
   4710    //     signal that copying finished.
   4711    //   Some other error occurred.
   4712    //     Same as if the stream were closed, but notify with the status
   4713    //     NS_ERROR_UNEXPECTED so the observer knows something was wonky.
   4714    //
   4715 
   4716    try {
   4717      var quantum = pendingData[0];
   4718 
   4719      // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on
   4720      //     undefined behavior!  We're only using this because writeByteArray
   4721      //     is unusably broken for asynchronous output streams; see bug 532834
   4722      //     for details.
   4723      var bytesWritten = output.write(quantum, quantum.length);
   4724      if (bytesWritten === quantum.length) {
   4725        pendingData.shift();
   4726      } else {
   4727        pendingData[0] = quantum.substring(bytesWritten);
   4728      }
   4729 
   4730      dumpn("*** wrote " + bytesWritten + " bytes of data");
   4731    } catch (e) {
   4732      if (wouldBlock(e)) {
   4733        NS_ASSERT(
   4734          !!pendingData.length,
   4735          "stream-blocking exception with no data to write?"
   4736        );
   4737        NS_ASSERT(
   4738          !!pendingData[0].length,
   4739          "stream-blocking exception with empty quantum?"
   4740        );
   4741        this._waitToWriteData();
   4742        return;
   4743      }
   4744 
   4745      if (streamClosed(e)) {
   4746        dumpn("!!! output stream prematurely closed, signaling error...");
   4747      } else {
   4748        dumpn("!!! unknown error: " + e + ", quantum=" + quantum);
   4749      }
   4750 
   4751      this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
   4752      return;
   4753    }
   4754 
   4755    // The day is ours!  Quantum written, now let's see if we have more data
   4756    // still to write.
   4757    try {
   4758      if (pendingData.length) {
   4759        this._waitToWriteData();
   4760        return;
   4761      }
   4762    } catch (e) {
   4763      dumpn("!!! unexpected error waiting to write pending data: " + e);
   4764      this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
   4765      return;
   4766    }
   4767 
   4768    // Okay, we have no more pending data to write -- but might we get more in
   4769    // the future?
   4770    if (this._source !== null) {
   4771      /*
   4772       * If we might, then wait for the output stream to be closed.  (We wait
   4773       * only for closure because we have no data to write -- and if we waited
   4774       * for a specific amount of data, we would get repeatedly notified for no
   4775       * reason if over time the output stream permitted more and more data to
   4776       * be written to it without blocking.)
   4777       */
   4778      this._waitForSinkClosure();
   4779    } else {
   4780      /*
   4781       * On the other hand, if we can't have more data because the input
   4782       * stream's gone away, then it's time to notify of copy completion.
   4783       * Victory!
   4784       */
   4785      this._sink = null;
   4786      this._cancelOrDispatchCancelCallback(Cr.NS_OK);
   4787    }
   4788  },
   4789 
   4790  // NSIREQUEST
   4791 
   4792  /** Returns true if the cancel observer hasn't been notified yet. */
   4793  isPending() {
   4794    return !this._completed;
   4795  },
   4796 
   4797  /** Not implemented, don't use! */
   4798  suspend: notImplemented,
   4799  /** Not implemented, don't use! */
   4800  resume: notImplemented,
   4801 
   4802  /**
   4803   * Cancels data reading from input, asynchronously writes out any pending
   4804   * data, and causes the observer to be notified with the given error code when
   4805   * all writing has finished.
   4806   *
   4807   * @param status : nsresult
   4808   *   the status to pass to the observer when data copying has been canceled
   4809   */
   4810  cancel(status) {
   4811    dumpn("*** cancel(" + status.toString(16) + ")");
   4812 
   4813    if (this._canceled) {
   4814      dumpn("*** suppressing a late cancel");
   4815      return;
   4816    }
   4817 
   4818    this._canceled = true;
   4819    this.status = status;
   4820 
   4821    // We could be in the middle of absolutely anything at this point.  Both
   4822    // input and output might still be around, we might have pending data to
   4823    // write, and in general we know nothing about the state of the world.  We
   4824    // therefore must assume everything's in progress and take everything to its
   4825    // final steady state (or so far as it can go before we need to finish
   4826    // writing out remaining data).
   4827 
   4828    this._doneReadingSource(status);
   4829  },
   4830 
   4831  // PRIVATE IMPLEMENTATION
   4832 
   4833  /**
   4834   * Stop reading input if we haven't already done so, passing e as the status
   4835   * when closing the stream, and kick off a copy-completion notice if no more
   4836   * data remains to be written.
   4837   *
   4838   * @param e : nsresult
   4839   *   the status to be used when closing the input stream
   4840   */
   4841  _doneReadingSource(e) {
   4842    dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")");
   4843 
   4844    this._finishSource(e);
   4845    if (this._pendingData.length === 0) {
   4846      this._sink = null;
   4847    } else {
   4848      NS_ASSERT(this._sink !== null, "null output?");
   4849    }
   4850 
   4851    // If we've written out all data read up to this point, then it's time to
   4852    // signal completion.
   4853    if (this._sink === null) {
   4854      NS_ASSERT(this._pendingData.length === 0, "pending data still?");
   4855      this._cancelOrDispatchCancelCallback(e);
   4856    }
   4857  },
   4858 
   4859  /**
   4860   * Stop writing output if we haven't already done so, discard any data that
   4861   * remained to be sent, close off input if it wasn't already closed, and kick
   4862   * off a copy-completion notice.
   4863   *
   4864   * @param e : nsresult
   4865   *   the status to be used when closing input if it wasn't already closed
   4866   */
   4867  _doneWritingToSink(e) {
   4868    dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")");
   4869 
   4870    this._pendingData.length = 0;
   4871    this._sink = null;
   4872    this._doneReadingSource(e);
   4873  },
   4874 
   4875  /**
   4876   * Completes processing of this copy: either by canceling the copy if it
   4877   * hasn't already been canceled using the provided status, or by dispatching
   4878   * the cancel callback event (with the originally provided status, of course)
   4879   * if it already has been canceled.
   4880   *
   4881   * @param status : nsresult
   4882   *   the status code to use to cancel this, if this hasn't already been
   4883   *   canceled
   4884   */
   4885  _cancelOrDispatchCancelCallback(status) {
   4886    dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")");
   4887 
   4888    NS_ASSERT(this._source === null, "should have finished input");
   4889    NS_ASSERT(this._sink === null, "should have finished output");
   4890    NS_ASSERT(this._pendingData.length === 0, "should have no pending data");
   4891 
   4892    if (!this._canceled) {
   4893      this.cancel(status);
   4894      return;
   4895    }
   4896 
   4897    var self = this;
   4898    var event = {
   4899      run() {
   4900        dumpn("*** onStopRequest async callback");
   4901 
   4902        self._completed = true;
   4903        try {
   4904          self._observer.onStopRequest(self, self.status);
   4905        } catch (e) {
   4906          NS_ASSERT(
   4907            false,
   4908            "how are we throwing an exception here?  we control " +
   4909              "all the callers!  " +
   4910              e
   4911          );
   4912        }
   4913      },
   4914    };
   4915 
   4916    Services.tm.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
   4917  },
   4918 
   4919  /**
   4920   * Kicks off another wait for more data to be available from the input stream.
   4921   */
   4922  _waitToReadData() {
   4923    dumpn("*** _waitToReadData");
   4924    this._source.asyncWait(
   4925      this,
   4926      0,
   4927      Response.SEGMENT_SIZE,
   4928      Services.tm.mainThread
   4929    );
   4930  },
   4931 
   4932  /**
   4933   * Kicks off another wait until data can be written to the output stream.
   4934   */
   4935  _waitToWriteData() {
   4936    dumpn("*** _waitToWriteData");
   4937 
   4938    var pendingData = this._pendingData;
   4939    NS_ASSERT(!!pendingData.length, "no pending data to write?");
   4940    NS_ASSERT(!!pendingData[0].length, "buffered an empty write?");
   4941 
   4942    this._sink.asyncWait(
   4943      this,
   4944      0,
   4945      pendingData[0].length,
   4946      Services.tm.mainThread
   4947    );
   4948  },
   4949 
   4950  /**
   4951   * Kicks off a wait for the sink to which data is being copied to be closed.
   4952   * We wait for stream closure when we don't have any data to be copied, rather
   4953   * than waiting to write a specific amount of data.  We can't wait to write
   4954   * data because the sink might be infinitely writable, and if no data appears
   4955   * in the source for a long time we might have to spin quite a bit waiting to
   4956   * write, waiting to write again, &c.  Waiting on stream closure instead means
   4957   * we'll get just one notification if the sink dies.  Note that when data
   4958   * starts arriving from the sink we'll resume waiting for data to be written,
   4959   * dropping this closure-only callback entirely.
   4960   */
   4961  _waitForSinkClosure() {
   4962    dumpn("*** _waitForSinkClosure");
   4963 
   4964    this._sink.asyncWait(
   4965      this,
   4966      Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY,
   4967      0,
   4968      Services.tm.mainThread
   4969    );
   4970  },
   4971 
   4972  /**
   4973   * Closes input with the given status, if it hasn't already been closed;
   4974   * otherwise a no-op.
   4975   *
   4976   * @param status : nsresult
   4977   *   status code use to close the source stream if necessary
   4978   */
   4979  _finishSource(status) {
   4980    dumpn("*** _finishSource(" + status.toString(16) + ")");
   4981 
   4982    if (this._source !== null) {
   4983      this._source.closeWithStatus(status);
   4984      this._source = null;
   4985    }
   4986  },
   4987 };
   4988 
   4989 /**
   4990 * A container for utility functions used with HTTP headers.
   4991 */
   4992 const headerUtils = {
   4993  /**
   4994   * Normalizes fieldName (by converting it to lowercase) and ensures it is a
   4995   * valid header field name (although not necessarily one specified in RFC
   4996   * 2616).
   4997   *
   4998   * @throws NS_ERROR_INVALID_ARG
   4999   *   if fieldName does not match the field-name production in RFC 2616
   5000   * @returns string
   5001   *   fieldName converted to lowercase if it is a valid header, for characters
   5002   *   where case conversion is possible
   5003   */
   5004  normalizeFieldName(fieldName) {
   5005    if (fieldName == "") {
   5006      dumpn("*** Empty fieldName");
   5007      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   5008    }
   5009 
   5010    for (var i = 0, sz = fieldName.length; i < sz; i++) {
   5011      if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)]) {
   5012        dumpn(fieldName + " is not a valid header field name!");
   5013        throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   5014      }
   5015    }
   5016 
   5017    return fieldName.toLowerCase();
   5018  },
   5019 
   5020  /**
   5021   * Ensures that fieldValue is a valid header field value (although not
   5022   * necessarily as specified in RFC 2616 if the corresponding field name is
   5023   * part of the HTTP protocol), normalizes the value if it is, and
   5024   * returns the normalized value.
   5025   *
   5026   * @param fieldValue : string
   5027   *   a value to be normalized as an HTTP header field value
   5028   * @throws NS_ERROR_INVALID_ARG
   5029   *   if fieldValue does not match the field-value production in RFC 2616
   5030   * @returns string
   5031   *   fieldValue as a normalized HTTP header field value
   5032   */
   5033  normalizeFieldValue(fieldValue) {
   5034    // field-value    = *( field-content | LWS )
   5035    // field-content  = <the OCTETs making up the field-value
   5036    //                  and consisting of either *TEXT or combinations
   5037    //                  of token, separators, and quoted-string>
   5038    // TEXT           = <any OCTET except CTLs,
   5039    //                  but including LWS>
   5040    // LWS            = [CRLF] 1*( SP | HT )
   5041    //
   5042    // quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
   5043    // qdtext         = <any TEXT except <">>
   5044    // quoted-pair    = "\" CHAR
   5045    // CHAR           = <any US-ASCII character (octets 0 - 127)>
   5046 
   5047    // Any LWS that occurs between field-content MAY be replaced with a single
   5048    // SP before interpreting the field value or forwarding the message
   5049    // downstream (section 4.2); we replace 1*LWS with a single SP
   5050    var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " ");
   5051 
   5052    // remove leading/trailing LWS (which has been converted to SP)
   5053    val = val.replace(/^ +/, "").replace(/ +$/, "");
   5054 
   5055    // that should have taken care of all CTLs, so val should contain no CTLs
   5056    dumpn("*** Normalized value: '" + val + "'");
   5057    for (var i = 0, len = val.length; i < len; i++) {
   5058      if (isCTL(val.charCodeAt(i))) {
   5059        dump("*** Char " + i + " has charcode " + val.charCodeAt(i));
   5060        throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   5061      }
   5062    }
   5063 
   5064    // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly
   5065    //     normalize, however, so this can be construed as a tightening of the
   5066    //     spec and not entirely as a bug
   5067    return val;
   5068  },
   5069 };
   5070 
   5071 /**
   5072 * Converts the given string into a string which is safe for use in an HTML
   5073 * context.
   5074 *
   5075 * @param str : string
   5076 *   the string to make HTML-safe
   5077 * @returns string
   5078 *   an HTML-safe version of str
   5079 */
   5080 function htmlEscape(str) {
   5081  // this is naive, but it'll work
   5082  var s = "";
   5083  for (var i = 0; i < str.length; i++) {
   5084    s += "&#" + str.charCodeAt(i) + ";";
   5085  }
   5086  return s;
   5087 }
   5088 
   5089 /**
   5090 * Constructs an object representing an HTTP version (see section 3.1).
   5091 *
   5092 * @param versionString
   5093 *   a string of the form "#.#", where # is an non-negative decimal integer with
   5094 *   or without leading zeros
   5095 * @throws
   5096 *   if versionString does not specify a valid HTTP version number
   5097 */
   5098 function nsHttpVersion(versionString) {
   5099  var matches = /^(\d+)\.(\d+)$/.exec(versionString);
   5100  if (!matches) {
   5101    throw new Error("Not a valid HTTP version!");
   5102  }
   5103 
   5104  /** The major version number of this, as a number. */
   5105  this.major = parseInt(matches[1], 10);
   5106 
   5107  /** The minor version number of this, as a number. */
   5108  this.minor = parseInt(matches[2], 10);
   5109 
   5110  if (
   5111    isNaN(this.major) ||
   5112    isNaN(this.minor) ||
   5113    this.major < 0 ||
   5114    this.minor < 0
   5115  ) {
   5116    throw new Error("Not a valid HTTP version!");
   5117  }
   5118 }
   5119 nsHttpVersion.prototype = {
   5120  /**
   5121   * Returns the standard string representation of the HTTP version represented
   5122   * by this (e.g., "1.1").
   5123   */
   5124  toString() {
   5125    return this.major + "." + this.minor;
   5126  },
   5127 
   5128  /**
   5129   * Returns true if this represents the same HTTP version as otherVersion,
   5130   * false otherwise.
   5131   *
   5132   * @param otherVersion : nsHttpVersion
   5133   *   the version to compare against this
   5134   */
   5135  equals(otherVersion) {
   5136    return this.major == otherVersion.major && this.minor == otherVersion.minor;
   5137  },
   5138 
   5139  /** True if this >= otherVersion, false otherwise. */
   5140  atLeast(otherVersion) {
   5141    return (
   5142      this.major > otherVersion.major ||
   5143      (this.major == otherVersion.major && this.minor >= otherVersion.minor)
   5144    );
   5145  },
   5146 };
   5147 
   5148 nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0");
   5149 nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1");
   5150 
   5151 /**
   5152 * An object which stores HTTP headers for a request or response.
   5153 *
   5154 * Note that since headers are case-insensitive, this object converts headers to
   5155 * lowercase before storing them.  This allows the getHeader and hasHeader
   5156 * methods to work correctly for any case of a header, but it means that the
   5157 * values returned by .enumerator may not be equal case-sensitively to the
   5158 * values passed to setHeader when adding headers to this.
   5159 */
   5160 export function nsHttpHeaders() {
   5161  /**
   5162   * A hash of headers, with header field names as the keys and header field
   5163   * values as the values.  Header field names are case-insensitive, but upon
   5164   * insertion here they are converted to lowercase.  Header field values are
   5165   * normalized upon insertion to contain no leading or trailing whitespace.
   5166   *
   5167   * Note also that per RFC 2616, section 4.2, two headers with the same name in
   5168   * a message may be treated as one header with the same field name and a field
   5169   * value consisting of the separate field values joined together with a "," in
   5170   * their original order.  This hash stores multiple headers with the same name
   5171   * in this manner.
   5172   */
   5173  this._headers = {};
   5174 }
   5175 nsHttpHeaders.prototype = {
   5176  /**
   5177   * Sets the header represented by name and value in this.
   5178   *
   5179   * @param name : string
   5180   *   the header name
   5181   * @param value : string
   5182   *   the header value
   5183   * @throws NS_ERROR_INVALID_ARG
   5184   *   if name or value is not a valid header component
   5185   */
   5186  setHeader(fieldName, fieldValue, merge) {
   5187    var name = headerUtils.normalizeFieldName(fieldName);
   5188    // Bug 1937905 - For testing a neterror page due to invalid header values
   5189    var value =
   5190      name === "x-invalid-header-value"
   5191        ? fieldValue
   5192        : headerUtils.normalizeFieldValue(fieldValue);
   5193 
   5194    // The following three headers are stored as arrays because their real-world
   5195    // syntax prevents joining individual headers into a single header using
   5196    // ",".  See also <https://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77>
   5197    if (merge && name in this._headers) {
   5198      if (
   5199        name === "www-authenticate" ||
   5200        name === "proxy-authenticate" ||
   5201        name === "set-cookie"
   5202      ) {
   5203        this._headers[name].push(value);
   5204      } else {
   5205        this._headers[name][0] += "," + value;
   5206        NS_ASSERT(
   5207          this._headers[name].length === 1,
   5208          "how'd a non-special header have multiple values?"
   5209        );
   5210      }
   5211    } else {
   5212      this._headers[name] = [value];
   5213    }
   5214  },
   5215 
   5216  setHeaderNoCheck(fieldName, fieldValue) {
   5217    var name = headerUtils.normalizeFieldName(fieldName);
   5218    var value = headerUtils.normalizeFieldValue(fieldValue);
   5219    if (name in this._headers) {
   5220      this._headers[name].push(value);
   5221    } else {
   5222      this._headers[name] = [value];
   5223    }
   5224  },
   5225 
   5226  /**
   5227   * Returns the value for the header specified by this.
   5228   *
   5229   * @throws NS_ERROR_INVALID_ARG
   5230   *   if fieldName does not constitute a valid header field name
   5231   * @throws NS_ERROR_NOT_AVAILABLE
   5232   *   if the given header does not exist in this
   5233   * @returns string
   5234   *   the field value for the given header, possibly with non-semantic changes
   5235   *   (i.e., leading/trailing whitespace stripped, whitespace runs replaced
   5236   *   with spaces, etc.) at the option of the implementation; multiple
   5237   *   instances of the header will be combined with a comma, except for
   5238   *   the three headers noted in the description of getHeaderValues
   5239   */
   5240  getHeader(fieldName) {
   5241    return this.getHeaderValues(fieldName).join("\n");
   5242  },
   5243 
   5244  /**
   5245   * Returns the value for the header specified by fieldName as an array.
   5246   *
   5247   * @throws NS_ERROR_INVALID_ARG
   5248   *   if fieldName does not constitute a valid header field name
   5249   * @throws NS_ERROR_NOT_AVAILABLE
   5250   *   if the given header does not exist in this
   5251   * @returns [string]
   5252   *   an array of all the header values in this for the given
   5253   *   header name.  Header values will generally be collapsed
   5254   *   into a single header by joining all header values together
   5255   *   with commas, but certain headers (Proxy-Authenticate,
   5256   *   WWW-Authenticate, and Set-Cookie) violate the HTTP spec
   5257   *   and cannot be collapsed in this manner.  For these headers
   5258   *   only, the returned array may contain multiple elements if
   5259   *   that header has been added more than once.
   5260   */
   5261  getHeaderValues(fieldName) {
   5262    var name = headerUtils.normalizeFieldName(fieldName);
   5263 
   5264    if (name in this._headers) {
   5265      return this._headers[name];
   5266    }
   5267    throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   5268  },
   5269 
   5270  /**
   5271   * Returns true if a header with the given field name exists in this, false
   5272   * otherwise.
   5273   *
   5274   * @param fieldName : string
   5275   *   the field name whose existence is to be determined in this
   5276   * @throws NS_ERROR_INVALID_ARG
   5277   *   if fieldName does not constitute a valid header field name
   5278   * @returns boolean
   5279   *   true if the header's present, false otherwise
   5280   */
   5281  hasHeader(fieldName) {
   5282    var name = headerUtils.normalizeFieldName(fieldName);
   5283    return name in this._headers;
   5284  },
   5285 
   5286  /**
   5287   * Returns a new enumerator over the field names of the headers in this, as
   5288   * nsISupportsStrings.  The names returned will be in lowercase, regardless of
   5289   * how they were input using setHeader (header names are case-insensitive per
   5290   * RFC 2616).
   5291   */
   5292  get enumerator() {
   5293    var headers = [];
   5294    for (var i in this._headers) {
   5295      var supports = new SupportsString();
   5296      supports.data = i;
   5297      headers.push(supports);
   5298    }
   5299 
   5300    return new nsSimpleEnumerator(headers);
   5301  },
   5302 };
   5303 
   5304 /**
   5305 * Constructs an nsISimpleEnumerator for the given array of items.
   5306 *
   5307 * @param items : Array
   5308 *   the items, which must all implement nsISupports
   5309 */
   5310 function nsSimpleEnumerator(items) {
   5311  this._items = items;
   5312  this._nextIndex = 0;
   5313 }
   5314 nsSimpleEnumerator.prototype = {
   5315  hasMoreElements() {
   5316    return this._nextIndex < this._items.length;
   5317  },
   5318  getNext() {
   5319    if (!this.hasMoreElements()) {
   5320      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   5321    }
   5322 
   5323    return this._items[this._nextIndex++];
   5324  },
   5325  [Symbol.iterator]() {
   5326    return this._items.values();
   5327  },
   5328  QueryInterface: ChromeUtils.generateQI(["nsISimpleEnumerator"]),
   5329 };
   5330 
   5331 /**
   5332 * A representation of the data in an HTTP request.
   5333 *
   5334 * @param port : uint
   5335 *   the port on which the server receiving this request runs
   5336 */
   5337 function Request(port) {
   5338  /** Method of this request, e.g. GET or POST. */
   5339  this._method = "";
   5340 
   5341  /** Path of the requested resource; empty paths are converted to '/'. */
   5342  this._path = "";
   5343 
   5344  /** Query string, if any, associated with this request (not including '?'). */
   5345  this._queryString = "";
   5346 
   5347  /** Scheme of requested resource, usually http, always lowercase. */
   5348  this._scheme = "http";
   5349 
   5350  /** Hostname on which the requested resource resides. */
   5351  this._host = undefined;
   5352 
   5353  /** Port number over which the request was received. */
   5354  this._port = port;
   5355 
   5356  var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null);
   5357 
   5358  /** Stream from which data in this request's body may be read. */
   5359  this._bodyInputStream = bodyPipe.inputStream;
   5360 
   5361  /** Stream to which data in this request's body is written. */
   5362  this._bodyOutputStream = bodyPipe.outputStream;
   5363 
   5364  /**
   5365   * The headers in this request.
   5366   */
   5367  this._headers = new nsHttpHeaders();
   5368 
   5369  /**
   5370   * For the addition of ad-hoc properties and new functionality without having
   5371   * to change nsIHttpRequest every time; currently lazily created, as its only
   5372   * use is in directory listings.
   5373   */
   5374  this._bag = null;
   5375 }
   5376 Request.prototype = {
   5377  // SERVER METADATA
   5378 
   5379  //
   5380  // see nsIHttpRequest.scheme
   5381  //
   5382  get scheme() {
   5383    return this._scheme;
   5384  },
   5385 
   5386  //
   5387  // see nsIHttpRequest.host
   5388  //
   5389  get host() {
   5390    return this._host;
   5391  },
   5392 
   5393  //
   5394  // see nsIHttpRequest.port
   5395  //
   5396  get port() {
   5397    return this._port;
   5398  },
   5399 
   5400  // REQUEST LINE
   5401 
   5402  //
   5403  // see nsIHttpRequest.method
   5404  //
   5405  get method() {
   5406    return this._method;
   5407  },
   5408 
   5409  //
   5410  // see nsIHttpRequest.httpVersion
   5411  //
   5412  get httpVersion() {
   5413    return this._httpVersion.toString();
   5414  },
   5415 
   5416  //
   5417  // see nsIHttpRequest.path
   5418  //
   5419  get path() {
   5420    return this._path;
   5421  },
   5422 
   5423  //
   5424  // see nsIHttpRequest.queryString
   5425  //
   5426  get queryString() {
   5427    return this._queryString;
   5428  },
   5429 
   5430  // HEADERS
   5431 
   5432  //
   5433  // see nsIHttpRequest.getHeader
   5434  //
   5435  getHeader(name) {
   5436    return this._headers.getHeader(name);
   5437  },
   5438 
   5439  //
   5440  // see nsIHttpRequest.hasHeader
   5441  //
   5442  hasHeader(name) {
   5443    return this._headers.hasHeader(name);
   5444  },
   5445 
   5446  //
   5447  // see nsIHttpRequest.headers
   5448  //
   5449  get headers() {
   5450    return this._headers.enumerator;
   5451  },
   5452 
   5453  //
   5454  // see nsIPropertyBag.enumerator
   5455  //
   5456  get enumerator() {
   5457    this._ensurePropertyBag();
   5458    return this._bag.enumerator;
   5459  },
   5460 
   5461  //
   5462  // see nsIHttpRequest.headers
   5463  //
   5464  get bodyInputStream() {
   5465    return this._bodyInputStream;
   5466  },
   5467 
   5468  //
   5469  // see nsIPropertyBag.getProperty
   5470  //
   5471  getProperty(name) {
   5472    this._ensurePropertyBag();
   5473    return this._bag.getProperty(name);
   5474  },
   5475 
   5476  // NSISUPPORTS
   5477 
   5478  //
   5479  // see nsISupports.QueryInterface
   5480  //
   5481  QueryInterface: ChromeUtils.generateQI(["nsIHttpRequest"]),
   5482 
   5483  // PRIVATE IMPLEMENTATION
   5484 
   5485  /** Ensures a property bag has been created for ad-hoc behaviors. */
   5486  _ensurePropertyBag() {
   5487    if (!this._bag) {
   5488      this._bag = new WritablePropertyBag();
   5489    }
   5490  },
   5491 };