Quellcodebibliothek Statistik Leitseite products/sources/formale Sprachen/C/Firefox/netwerk/test/httpserver/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 157 kB image not shown  

Quellcode-Bibliothek httpd.sys.mjs   Sprache: unbekannt

 
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/*
 * An implementation of an HTTP server both as a loadable script and as an XPCOM
 * component.  See the accompanying README file for user documentation on
 * httpd.js.
 */

/* eslint-disable no-shadow */

const CC = Components.Constructor;

const PR_UINT32_MAX = Math.pow(2, 32) - 1;

/** True if debugging output is enabled, false otherwise. */
var DEBUG = false; // non-const *only* so tweakable in server tests

/** True if debugging output should be timestamped. */
var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests

/**
 * Sets the debugging status, intended for tweaking in server tests.
 *
 * @param {boolean} debug
 *   Enables debugging output
 * @param {boolean} debugTimestamp
 *   Enables timestamping of the debugging output.
 */
export function setDebuggingStatus(debug, debugTimestamp) {
  DEBUG = debug;
  DEBUG_TIMESTAMP = debugTimestamp;
}

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";

/**
 * Asserts that the given condition holds.  If it doesn't, the given message is
 * dumped, a stack trace is printed, and an exception is thrown to attempt to
 * stop execution (which unfortunately must rely upon the exception not being
 * accidentally swallowed by the code that uses it).
 */
function NS_ASSERT(cond, msg) {
  if (DEBUG && !cond) {
    dumpn("###!!!");
    dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!"));
    dumpn("###!!! Stack follows:");

    var stack = new Error().stack.split(/\n/);
    dumpn(
      stack
        .map(function (val) {
          return "###!!!   " + val;
        })
        .join("\n")
    );

    throw Components.Exception("", Cr.NS_ERROR_ABORT);
  }
}

/** Constructs an HTTP error object. */
export function HttpError(code, description) {
  this.code = code;
  this.description = description;
}

HttpError.prototype = {
  toString() {
    return this.code + " " + this.description;
  },
};

/**
 * Errors thrown to trigger specific HTTP server responses.
 */
export var HTTP_400 = new HttpError(400, "Bad Request");

export var HTTP_401 = new HttpError(401, "Unauthorized");
export var HTTP_402 = new HttpError(402, "Payment Required");
export var HTTP_403 = new HttpError(403, "Forbidden");
export var HTTP_404 = new HttpError(404, "Not Found");
export var HTTP_405 = new HttpError(405, "Method Not Allowed");
export var HTTP_406 = new HttpError(406, "Not Acceptable");
export var HTTP_407 = new HttpError(407, "Proxy Authentication Required");
export var HTTP_408 = new HttpError(408, "Request Timeout");
export var HTTP_409 = new HttpError(409, "Conflict");
export var HTTP_410 = new HttpError(410, "Gone");
export var HTTP_411 = new HttpError(411, "Length Required");
export var HTTP_412 = new HttpError(412, "Precondition Failed");
export var HTTP_413 = new HttpError(413, "Request Entity Too Large");
export var HTTP_414 = new HttpError(414, "Request-URI Too Long");
export var HTTP_415 = new HttpError(415, "Unsupported Media Type");
export var HTTP_417 = new HttpError(417, "Expectation Failed");
export var HTTP_500 = new HttpError(500, "Internal Server Error");
export var HTTP_501 = new HttpError(501, "Not Implemented");
export var HTTP_502 = new HttpError(502, "Bad Gateway");
export var HTTP_503 = new HttpError(503, "Service Unavailable");
export var HTTP_504 = new HttpError(504, "Gateway Timeout");
export var HTTP_505 = new HttpError(505, "HTTP Version Not Supported");

/** Creates a hash with fields corresponding to the values in arr. */
function array2obj(arr) {
  var obj = {};
  for (var i = 0; i < arr.length; i++) {
    obj[arr[i]] = arr[i];
  }
  return obj;
}

/** Returns an array of the integers x through y, inclusive. */
function range(x, y) {
  var arr = [];
  for (var i = x; i <= y; i++) {
    arr.push(i);
  }
  return arr;
}

/** An object (hash) whose fields are the numbers of all HTTP error codes. */
const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505)));

/**
 * The character used to distinguish hidden files from non-hidden files, a la
 * the leading dot in Apache.  Since that mechanism also hides files from
 * easy display in LXR, ls output, etc. however, we choose instead to use a
 * suffix character.  If a requested file ends with it, we append another
 * when getting the file on the server.  If it doesn't, we just look up that
 * file.  Therefore, any file whose name ends with exactly one of the character
 * is "hidden" and available for use by the server.
 */
const HIDDEN_CHAR = "^";

/**
 * The file name suffix indicating the file containing overridden headers for
 * a requested file.
 */
const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR;
const INFORMATIONAL_RESPONSE_SUFFIX =
  HIDDEN_CHAR + "informationalResponse" + HIDDEN_CHAR;

/** Type used to denote SJS scripts for CGI-like functionality. */
const SJS_TYPE = "sjs";

/** Base for relative timestamps produced by dumpn(). */
var firstStamp = 0;

/** dump(str) with a trailing "\n" -- only outputs if DEBUG. */
export function dumpn(str) {
  if (DEBUG) {
    var prefix = "HTTPD-INFO | ";
    if (DEBUG_TIMESTAMP) {
      if (firstStamp === 0) {
        firstStamp = Date.now();
      }

      var elapsed = Date.now() - firstStamp; // milliseconds
      var min = Math.floor(elapsed / 60000);
      var sec = (elapsed % 60000) / 1000;

      if (sec < 10) {
        prefix += min + ":0" + sec.toFixed(3) + " | ";
      } else {
        prefix += min + ":" + sec.toFixed(3) + " | ";
      }
    }

    dump(prefix + str + "\n");
  }
}

/** Dumps the current JS stack if DEBUG. */
function dumpStack() {
  // peel off the frames for dumpStack() and Error()
  var stack = new Error().stack.split(/\n/).slice(2);
  stack.forEach(dumpn);
}

/**
 * JavaScript constructors for commonly-used classes; precreating these is a
 * speedup over doing the same from base principles.  See the docs at
 * http://developer.mozilla.org/en/docs/Components.Constructor for details.
 */
const ServerSocket = CC(
  "@mozilla.org/network/server-socket;1",
  "nsIServerSocket",
  "init"
);
const ServerSocketIPv6 = CC(
  "@mozilla.org/network/server-socket;1",
  "nsIServerSocket",
  "initIPv6"
);
const ServerSocketDualStack = CC(
  "@mozilla.org/network/server-socket;1",
  "nsIServerSocket",
  "initDualStack"
);
const ScriptableInputStream = CC(
  "@mozilla.org/scriptableinputstream;1",
  "nsIScriptableInputStream",
  "init"
);
const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init");
const FileInputStream = CC(
  "@mozilla.org/network/file-input-stream;1",
  "nsIFileInputStream",
  "init"
);
const ConverterInputStream = CC(
  "@mozilla.org/intl/converter-input-stream;1",
  "nsIConverterInputStream",
  "init"
);
const WritablePropertyBag = CC(
  "@mozilla.org/hash-property-bag;1",
  "nsIWritablePropertyBag2"
);
const SupportsString = CC(
  "@mozilla.org/supports-string;1",
  "nsISupportsString"
);

/* These two are non-const only so a test can overwrite them. */
var BinaryInputStream = CC(
  "@mozilla.org/binaryinputstream;1",
  "nsIBinaryInputStream",
  "setInputStream"
);
var BinaryOutputStream = CC(
  "@mozilla.org/binaryoutputstream;1",
  "nsIBinaryOutputStream",
  "setOutputStream"
);

export function overrideBinaryStreamsForTests(
  inputStream,
  outputStream,
  responseSegmentSize
) {
  BinaryInputStream = inputStream;
  BinaryOutputStream = outputStream;
  Response.SEGMENT_SIZE = responseSegmentSize;
}

/**
 * Returns the RFC 822/1123 representation of a date.
 *
 * @param date : Number
 *   the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT
 * @returns string
 *   the representation of the given date
 */
function toDateString(date) {
  //
  // rfc1123-date = wkday "," SP date1 SP time SP "GMT"
  // date1        = 2DIGIT SP month SP 4DIGIT
  //                ; day month year (e.g., 02 Jun 1982)
  // time         = 2DIGIT ":" 2DIGIT ":" 2DIGIT
  //                ; 00:00:00 - 23:59:59
  // wkday        = "Mon" | "Tue" | "Wed"
  //              | "Thu" | "Fri" | "Sat" | "Sun"
  // month        = "Jan" | "Feb" | "Mar" | "Apr"
  //              | "May" | "Jun" | "Jul" | "Aug"
  //              | "Sep" | "Oct" | "Nov" | "Dec"
  //

  const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
  const monthStrings = [
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sep",
    "Oct",
    "Nov",
    "Dec",
  ];

  /**
   * Processes a date and returns the encoded UTC time as a string according to
   * the format specified in RFC 2616.
   *
   * @param date : Date
   *   the date to process
   * @returns string
   *   a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
   */
  function toTime(date) {
    var hrs = date.getUTCHours();
    var rv = hrs < 10 ? "0" + hrs : hrs;

    var mins = date.getUTCMinutes();
    rv += ":";
    rv += mins < 10 ? "0" + mins : mins;

    var secs = date.getUTCSeconds();
    rv += ":";
    rv += secs < 10 ? "0" + secs : secs;

    return rv;
  }

  /**
   * Processes a date and returns the encoded UTC date as a string according to
   * the date1 format specified in RFC 2616.
   *
   * @param date : Date
   *   the date to process
   * @returns string
   *   a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
   */
  function toDate1(date) {
    var day = date.getUTCDate();
    var month = date.getUTCMonth();
    var year = date.getUTCFullYear();

    var rv = day < 10 ? "0" + day : day;
    rv += " " + monthStrings[month];
    rv += " " + year;

    return rv;
  }

  date = new Date(date);

  const fmtString = "%wkday%, %date1% %time% GMT";
  var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]);
  rv = rv.replace("%time%", toTime(date));
  return rv.replace("%date1%", toDate1(date));
}

/**
 * Instantiates a new HTTP server.
 */
export function nsHttpServer() {
  /** The port on which this server listens. */
  this._port = undefined;

  /** The socket associated with this. */
  this._socket = null;

  /** The handler used to process requests to this server. */
  this._handler = new ServerHandler(this);

  /** Naming information for this server. */
  this._identity = new ServerIdentity();

  /**
   * Indicates when the server is to be shut down at the end of the request.
   */
  this._doQuit = false;

  /**
   * True if the socket in this is closed (and closure notifications have been
   * sent and processed if the socket was ever opened), false otherwise.
   */
  this._socketClosed = true;

  /**
   * Used for tracking existing connections and ensuring that all connections
   * are properly cleaned up before server shutdown; increases by 1 for every
   * new incoming connection.
   */
  this._connectionGen = 0;

  /**
   * Hash of all open connections, indexed by connection number at time of
   * creation.
   */
  this._connections = {};
}

nsHttpServer.prototype = {
  // NSISERVERSOCKETLISTENER

  /**
   * Processes an incoming request coming in on the given socket and contained
   * in the given transport.
   *
   * @param socket : nsIServerSocket
   *   the socket through which the request was served
   * @param trans : nsISocketTransport
   *   the transport for the request/response
   * @see nsIServerSocketListener.onSocketAccepted
   */
  onSocketAccepted(socket, trans) {
    dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")");

    dumpn(">>> new connection on " + trans.host + ":" + trans.port);

    const SEGMENT_SIZE = 8192;
    const SEGMENT_COUNT = 1024;
    try {
      var input = trans
        .openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
        .QueryInterface(Ci.nsIAsyncInputStream);
      var output = trans.openOutputStream(0, 0, 0);
    } catch (e) {
      dumpn("*** error opening transport streams: " + e);
      trans.close(Cr.NS_BINDING_ABORTED);
      return;
    }

    var connectionNumber = ++this._connectionGen;

    try {
      var conn = new Connection(
        input,
        output,
        this,
        socket.port,
        trans.port,
        connectionNumber,
        trans
      );
      var reader = new RequestReader(conn);

      // XXX add request timeout functionality here!

      // Note: must use main thread here, or we might get a GC that will cause
      //       threadsafety assertions.  We really need to fix XPConnect so that
      //       you can actually do things in multi-threaded JS.  :-(
      input.asyncWait(reader, 0, 0, Services.tm.mainThread);
    } catch (e) {
      // Assume this connection can't be salvaged and bail on it completely;
      // don't attempt to close it so that we can assert that any connection
      // being closed is in this._connections.
      dumpn("*** error in initial request-processing stages: " + e);
      trans.close(Cr.NS_BINDING_ABORTED);
      return;
    }

    this._connections[connectionNumber] = conn;
    dumpn("*** starting connection " + connectionNumber);
  },

  /**
   * Called when the socket associated with this is closed.
   *
   * @param socket : nsIServerSocket
   *   the socket being closed
   * @param status : nsresult
   *   the reason the socket stopped listening (NS_BINDING_ABORTED if the server
   *   was stopped using nsIHttpServer.stop)
   * @see nsIServerSocketListener.onStopListening
   */
  onStopListening(socket) {
    dumpn(">>> shutting down server on port " + socket.port);
    for (var n in this._connections) {
      if (!this._connections[n]._requestStarted) {
        this._connections[n].close();
      }
    }
    this._socketClosed = true;
    if (this._hasOpenConnections()) {
      dumpn("*** open connections!!!");
    }
    if (!this._hasOpenConnections()) {
      dumpn("*** no open connections, notifying async from onStopListening");

      // Notify asynchronously so that any pending teardown in stop() has a
      // chance to run first.
      var self = this;
      var stopEvent = {
        run() {
          dumpn("*** _notifyStopped async callback");
          self._notifyStopped();
        },
      };
      Services.tm.currentThread.dispatch(
        stopEvent,
        Ci.nsIThread.DISPATCH_NORMAL
      );
    }
  },

  // NSIHTTPSERVER

  //
  // see nsIHttpServer.start
  //
  start(port) {
    this._start(port, "localhost");
  },

  //
  // see nsIHttpServer.start_ipv6
  //
  start_ipv6(port) {
    this._start(port, "[::1]");
  },

  start_dualStack(port) {
    this._start(port, "[::1]", true);
  },

  _start(port, host, dualStack) {
    if (this._socket) {
      throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED);
    }

    this._port = port;
    this._doQuit = this._socketClosed = false;

    this._host = host;

    // The listen queue needs to be long enough to handle
    // network.http.max-persistent-connections-per-server or
    // network.http.max-persistent-connections-per-proxy concurrent
    // connections, plus a safety margin in case some other process is
    // talking to the server as well.
    var maxConnections =
      5 +
      Math.max(
        Services.prefs.getIntPref(
          "network.http.max-persistent-connections-per-server"
        ),
        Services.prefs.getIntPref(
          "network.http.max-persistent-connections-per-proxy"
        )
      );

    try {
      var loopback = true;
      if (
        this._host != "127.0.0.1" &&
        this._host != "localhost" &&
        this._host != "[::1]"
      ) {
        loopback = false;
      }

      // When automatically selecting a port, sometimes the chosen port is
      // "blocked" from clients. We don't want to use these ports because
      // tests will intermittently fail. So, we simply keep trying to to
      // get a server socket until a valid port is obtained. We limit
      // ourselves to finite attempts just so we don't loop forever.
      var socket;
      for (var i = 100; i; i--) {
        var temp = null;
        if (dualStack) {
          temp = new ServerSocketDualStack(this._port, maxConnections);
        } else if (this._host.includes(":")) {
          temp = new ServerSocketIPv6(
            this._port,
            loopback, // true = localhost, false = everybody
            maxConnections
          );
        } else {
          temp = new ServerSocket(
            this._port,
            loopback, // true = localhost, false = everybody
            maxConnections
          );
        }

        var allowed = Services.io.allowPort(temp.port, "http");
        if (!allowed) {
          dumpn(
            ">>>Warning: obtained ServerSocket listens on a blocked " +
              "port: " +
              temp.port
          );
        }

        if (!allowed && this._port == -1) {
          dumpn(">>>Throwing away ServerSocket with bad port.");
          temp.close();
          continue;
        }

        socket = temp;
        break;
      }

      if (!socket) {
        throw new Error(
          "No socket server available. Are there no available ports?"
        );
      }

      socket.asyncListen(this);
      this._port = socket.port;
      this._identity._initialize(socket.port, host, true, dualStack);
      this._socket = socket;
      dumpn(
        ">>> listening on port " +
          socket.port +
          ", " +
          maxConnections +
          " pending connections"
      );
    } catch (e) {
      dump("\n!!! could not start server on port " + port + ": " + e + "\n\n");
      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
    }
  },

  //
  // see nsIHttpServer.stop
  //
  stop(callback) {
    if (!this._socket) {
      throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
    }

    // If no argument was provided to stop, return a promise.
    let returnValue = undefined;
    if (!callback) {
      returnValue = new Promise(resolve => {
        callback = resolve;
      });
    }

    this._stopCallback =
      typeof callback === "function"
        ? callback
        : function () {
            callback.onStopped();
          };

    dumpn(">>> stopping listening on port " + this._socket.port);
    this._socket.close();
    this._socket = null;

    // We can't have this identity any more, and the port on which we're running
    // this server now could be meaningless the next time around.
    this._identity._teardown();

    this._doQuit = false;

    // socket-close notification and pending request completion happen async

    return returnValue;
  },

  //
  // see nsIHttpServer.registerFile
  //
  registerFile(path, file, handler) {
    if (file && (!file.exists() || file.isDirectory())) {
      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
    }

    this._handler.registerFile(path, file, handler);
  },

  //
  // see nsIHttpServer.registerDirectory
  //
  registerDirectory(path, directory) {
    // XXX true path validation!
    if (
      path.charAt(0) != "/" ||
      path.charAt(path.length - 1) != "/" ||
      (directory && (!directory.exists() || !directory.isDirectory()))
    ) {
      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
    }

    // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping
    //     exists!

    this._handler.registerDirectory(path, directory);
  },

  //
  // see nsIHttpServer.registerPathHandler
  //
  registerPathHandler(path, handler) {
    this._handler.registerPathHandler(path, handler);
  },

  //
  // see nsIHttpServer.registerPrefixHandler
  //
  registerPrefixHandler(prefix, handler) {
    this._handler.registerPrefixHandler(prefix, handler);
  },

  //
  // see nsIHttpServer.registerErrorHandler
  //
  registerErrorHandler(code, handler) {
    this._handler.registerErrorHandler(code, handler);
  },

  //
  // see nsIHttpServer.setIndexHandler
  //
  setIndexHandler(handler) {
    this._handler.setIndexHandler(handler);
  },

  //
  // see nsIHttpServer.registerContentType
  //
  registerContentType(ext, type) {
    this._handler.registerContentType(ext, type);
  },

  get connectionNumber() {
    return this._connectionGen;
  },

  //
  // see nsIHttpServer.serverIdentity
  //
  get identity() {
    return this._identity;
  },

  //
  // see nsIHttpServer.getState
  //
  getState(path, k) {
    return this._handler._getState(path, k);
  },

  //
  // see nsIHttpServer.setState
  //
  setState(path, k, v) {
    return this._handler._setState(path, k, v);
  },

  //
  // see nsIHttpServer.getSharedState
  //
  getSharedState(k) {
    return this._handler._getSharedState(k);
  },

  //
  // see nsIHttpServer.setSharedState
  //
  setSharedState(k, v) {
    return this._handler._setSharedState(k, v);
  },

  //
  // see nsIHttpServer.getObjectState
  //
  getObjectState(k) {
    return this._handler._getObjectState(k);
  },

  //
  // see nsIHttpServer.setObjectState
  //
  setObjectState(k, v) {
    return this._handler._setObjectState(k, v);
  },

  get wrappedJSObject() {
    return this;
  },

  // NSISUPPORTS

  //
  // see nsISupports.QueryInterface
  //
  QueryInterface: ChromeUtils.generateQI([
    "nsIHttpServer",
    "nsIServerSocketListener",
  ]),

  // NON-XPCOM PUBLIC API

  /**
   * Returns true iff this server is not running (and is not in the process of
   * serving any requests still to be processed when the server was last
   * stopped after being run).
   */
  isStopped() {
    return this._socketClosed && !this._hasOpenConnections();
  },

  // PRIVATE IMPLEMENTATION

  /** True if this server has any open connections to it, false otherwise. */
  _hasOpenConnections() {
    //
    // If we have any open connections, they're tracked as numeric properties on
    // |this._connections|.  The non-standard __count__ property could be used
    // to check whether there are any properties, but standard-wise, even
    // looking forward to ES5, there's no less ugly yet still O(1) way to do
    // this.
    //
    for (var n in this._connections) {
      return true;
    }
    return false;
  },

  /** Calls the server-stopped callback provided when stop() was called. */
  _notifyStopped() {
    NS_ASSERT(this._stopCallback !== null, "double-notifying?");
    NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now");

    //
    // NB: We have to grab this now, null out the member, *then* call the
    //     callback here, or otherwise the callback could (indirectly) futz with
    //     this._stopCallback by starting and immediately stopping this, at
    //     which point we'd be nulling out a field we no longer have a right to
    //     modify.
    //
    var callback = this._stopCallback;
    this._stopCallback = null;
    try {
      callback();
    } catch (e) {
      // not throwing because this is specified as being usually (but not
      // always) asynchronous
      dump("!!! error running onStopped callback: " + e + "\n");
    }
  },

  /**
   * Notifies this server that the given connection has been closed.
   *
   * @param connection : Connection
   *   the connection that was closed
   */
  _connectionClosed(connection) {
    NS_ASSERT(
      connection.number in this._connections,
      "closing a connection " +
        this +
        " that we never added to the " +
        "set of open connections?"
    );
    NS_ASSERT(
      this._connections[connection.number] === connection,
      "connection number mismatch?  " + this._connections[connection.number]
    );
    delete this._connections[connection.number];

    // Fire a pending server-stopped notification if it's our responsibility.
    if (!this._hasOpenConnections() && this._socketClosed) {
      this._notifyStopped();
    }
  },

  /**
   * Requests that the server be shut down when possible.
   */
  _requestQuit() {
    dumpn(">>> requesting a quit");
    dumpStack();
    this._doQuit = true;
  },
};

export var HttpServer = nsHttpServer;

export class NodeServer {
  // Executes command in the context of a node server.
  // See handler in moz-http2.js
  //
  // Example use:
  // let id = NodeServer.fork(); // id is a random string
  // await NodeServer.execute(id, `"hello"`)
  // > "hello"
  // await NodeServer.execute(id, `(() => "hello")()`)
  // > "hello"
  // await NodeServer.execute(id, `(() => var_defined_on_server)()`)
  // > "0"
  // await NodeServer.execute(id, `var_defined_on_server`)
  // > "0"
  // function f(param) { if (param) return param; return "bla"; }
  // await NodeServer.execute(id, f); // Defines the function on the server
  // await NodeServer.execute(id, `f()`) // executes defined function
  // > "bla"
  // let result = await NodeServer.execute(id, `f("test")`);
  // > "test"
  // await NodeServer.kill(id); // shuts down the server

  // Forks a new node server using moz-http2-child.js as a starting point
  static fork() {
    return this.sendCommand("", "/fork");
  }
  // Executes command in the context of the node server indicated by `id`
  static execute(id, command) {
    return this.sendCommand(command, `/execute/${id}`);
  }
  // Shuts down the server
  static kill(id) {
    return this.sendCommand("", `/kill/${id}`);
  }

  // Issues a request to the node server (handler defined in moz-http2.js)
  // This method should not be called directly.
  static sendCommand(command, path) {
    let h2Port = Services.env.get("MOZNODE_EXEC_PORT");
    if (!h2Port) {
      throw new Error("Could not find MOZNODE_EXEC_PORT");
    }

    let req = new XMLHttpRequest();
    const serverIP =
      AppConstants.platform == "android" ? "10.0.2.2" : "127.0.0.1";
    req.open("POST", `http://${serverIP}:${h2Port}${path}`);
    req.channel.QueryInterface(Ci.nsIHttpChannelInternal).bypassProxy = true;

    // Passing a function to NodeServer.execute will define that function
    // in node. It can be called in a later execute command.
    let isFunction = function (obj) {
      return !!(obj && obj.constructor && obj.call && obj.apply);
    };
    let payload = command;
    if (isFunction(command)) {
      payload = `${command.name} = ${command.toString()};`;
    }

    return new Promise((resolve, reject) => {
      req.onload = () => {
        let x = null;

        if (req.statusText != "OK") {
          reject(`XHR request failed: ${req.statusText}`);
          return;
        }

        try {
          x = JSON.parse(req.responseText);
        } catch (e) {
          reject(`Failed to parse ${req.responseText} - ${e}`);
          return;
        }

        if (x.error) {
          let e = new Error(x.error, "", 0);
          e.stack = x.errorStack;
          reject(e);
          return;
        }
        resolve(x.result);
      };
      req.onerror = e => {
        reject(e);
      };

      req.send(payload.toString());
    });
  }
}

//
// RFC 2396 section 3.2.2:
//
// host        = hostname | IPv4address
// hostname    = *( domainlabel "." ) toplabel [ "." ]
// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
// toplabel    = alpha | alpha *( alphanum | "-" ) alphanum
// IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit
//
// IPv6 addresses are notably lacking in the above definition of 'host'.
// RFC 2732 section 3 extends the host definition:
// host          = hostname | IPv4address | IPv6reference
// ipv6reference = "[" IPv6address "]"
//
// RFC 3986 supersedes RFC 2732 and offers a more precise definition of a IPv6
// address. For simplicity, the regexp below captures all canonical IPv6
// addresses (e.g. [::1]), but may also match valid non-canonical IPv6 addresses
// (e.g. [::127.0.0.1]) and even invalid bracketed addresses ([::], [99999::]).

const HOST_REGEX = new RegExp(
  "^(?:" +
    // *( domainlabel "." )
    "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" +
    // toplabel [ "." ]
    "[a-z](?:[a-z0-9-]*[a-z0-9])?\\.?" +
    "|" +
    // IPv4 address
    "\\d+\\.\\d+\\.\\d+\\.\\d+" +
    "|" +
    // IPv6 addresses (e.g. [::1])
    "\\[[:0-9a-f]+\\]" +
    ")$",
  "i"
);

/**
 * Represents the identity of a server.  An identity consists of a set of
 * (scheme, host, port) tuples denoted as locations (allowing a single server to
 * serve multiple sites or to be used behind both HTTP and HTTPS proxies for any
 * host/port).  Any incoming request must be to one of these locations, or it
 * will be rejected with an HTTP 400 error.  One location, denoted as the
 * primary location, is the location assigned in contexts where a location
 * cannot otherwise be endogenously derived, such as for HTTP/1.0 requests.
 *
 * A single identity may contain at most one location per unique host/port pair;
 * other than that, no restrictions are placed upon what locations may
 * constitute an identity.
 */
function ServerIdentity() {
  /** The scheme of the primary location. */
  this._primaryScheme = "http";

  /** The hostname of the primary location. */
  this._primaryHost = "127.0.0.1";

  /** The port number of the primary location. */
  this._primaryPort = -1;

  /**
   * The current port number for the corresponding server, stored so that a new
   * primary location can always be set if the current one is removed.
   */
  this._defaultPort = -1;

  /**
   * Maps hosts to maps of ports to schemes, e.g. the following would represent
   * https://example.com:789/ and http://example.org/:
   *
   *   {
   *     "xexample.com": { 789: "https" },
   *     "xexample.org": { 80: "http" }
   *   }
   *
   * Note the "x" prefix on hostnames, which prevents collisions with special
   * JS names like "prototype".
   */
  this._locations = { xlocalhost: {} };
}
ServerIdentity.prototype = {
  // NSIHTTPSERVERIDENTITY

  //
  // see nsIHttpServerIdentity.primaryScheme
  //
  get primaryScheme() {
    if (this._primaryPort === -1) {
      throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
    }
    return this._primaryScheme;
  },

  //
  // see nsIHttpServerIdentity.primaryHost
  //
  get primaryHost() {
    if (this._primaryPort === -1) {
      throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
    }
    return this._primaryHost;
  },

  //
  // see nsIHttpServerIdentity.primaryPort
  //
  get primaryPort() {
    if (this._primaryPort === -1) {
      throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
    }
    return this._primaryPort;
  },

  //
  // see nsIHttpServerIdentity.add
  //
  add(scheme, host, port) {
    this._validate(scheme, host, port);

    var entry = this._locations["x" + host];
    if (!entry) {
      this._locations["x" + host] = entry = {};
    }

    entry[port] = scheme;
  },

  //
  // see nsIHttpServerIdentity.remove
  //
  remove(scheme, host, port) {
    this._validate(scheme, host, port);

    var entry = this._locations["x" + host];
    if (!entry) {
      return false;
    }

    var present = port in entry;
    delete entry[port];

    if (
      this._primaryScheme == scheme &&
      this._primaryHost == host &&
      this._primaryPort == port &&
      this._defaultPort !== -1
    ) {
      // Always keep at least one identity in existence at any time, unless
      // we're in the process of shutting down (the last condition above).
      this._primaryPort = -1;
      this._initialize(this._defaultPort, host, false);
    }

    return present;
  },

  //
  // see nsIHttpServerIdentity.has
  //
  has(scheme, host, port) {
    this._validate(scheme, host, port);

    return (
      "x" + host in this._locations &&
      scheme === this._locations["x" + host][port]
    );
  },

  //
  // see nsIHttpServerIdentity.has
  //
  getScheme(host, port) {
    this._validate("http", host, port);

    var entry = this._locations["x" + host];
    if (!entry) {
      return "";
    }

    return entry[port] || "";
  },

  //
  // see nsIHttpServerIdentity.setPrimary
  //
  setPrimary(scheme, host, port) {
    this._validate(scheme, host, port);

    this.add(scheme, host, port);

    this._primaryScheme = scheme;
    this._primaryHost = host;
    this._primaryPort = port;
  },

  // NSISUPPORTS

  //
  // see nsISupports.QueryInterface
  //
  QueryInterface: ChromeUtils.generateQI(["nsIHttpServerIdentity"]),

  // PRIVATE IMPLEMENTATION

  /**
   * Initializes the primary name for the corresponding server, based on the
   * provided port number.
   */
  _initialize(port, host, addSecondaryDefault, dualStack) {
    this._host = host;
    if (this._primaryPort !== -1) {
      this.add("http", host, port);
    } else {
      this.setPrimary("http", "localhost", port);
    }
    this._defaultPort = port;

    // Only add this if we're being called at server startup
    if (addSecondaryDefault && host != "127.0.0.1") {
      if (host.includes(":")) {
        this.add("http", "[::1]", port);
        if (dualStack) {
          this.add("http", "127.0.0.1", port);
        }
      } else {
        this.add("http", "127.0.0.1", port);
      }
    }
  },

  /**
   * Called at server shutdown time, unsets the primary location only if it was
   * the default-assigned location and removes the default location from the
   * set of locations used.
   */
  _teardown() {
    if (this._host != "127.0.0.1") {
      // Not the default primary location, nothing special to do here
      this.remove("http", "127.0.0.1", this._defaultPort);
    }

    // This is a *very* tricky bit of reasoning here; make absolutely sure the
    // tests for this code pass before you commit changes to it.
    if (
      this._primaryScheme == "http" &&
      this._primaryHost == this._host &&
      this._primaryPort == this._defaultPort
    ) {
      // Make sure we don't trigger the readding logic in .remove(), then remove
      // the default location.
      var port = this._defaultPort;
      this._defaultPort = -1;
      this.remove("http", this._host, port);

      // Ensure a server start triggers the setPrimary() path in ._initialize()
      this._primaryPort = -1;
    } else {
      // No reason not to remove directly as it's not our primary location
      this.remove("http", this._host, this._defaultPort);
    }
  },

  /**
   * Ensures scheme, host, and port are all valid with respect to RFC 2396.
   *
   * @throws NS_ERROR_ILLEGAL_VALUE
   *   if any argument doesn't match the corresponding production
   */
  _validate(scheme, host, port) {
    if (scheme !== "http" && scheme !== "https") {
      dumpn("*** server only supports http/https schemes: '" + scheme + "'");
      dumpStack();
      throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
    }
    if (!HOST_REGEX.test(host)) {
      dumpn("*** unexpected host: '" + host + "'");
      throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
    }
    if (port < 0 || port > 65535) {
      dumpn("*** unexpected port: '" + port + "'");
      throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
    }
  },
};

/**
 * Represents a connection to the server (and possibly in the future the thread
 * on which the connection is processed).
 *
 * @param input : nsIInputStream
 *   stream from which incoming data on the connection is read
 * @param output : nsIOutputStream
 *   stream to write data out the connection
 * @param server : nsHttpServer
 *   the server handling the connection
 * @param port : int
 *   the port on which the server is running
 * @param outgoingPort : int
 *   the outgoing port used by this connection
 * @param number : uint
 *   a serial number used to uniquely identify this connection
 */
function Connection(
  input,
  output,
  server,
  port,
  outgoingPort,
  number,
  transport
) {
  dumpn("*** opening new connection " + number + " on port " + outgoingPort);

  /** Stream of incoming data. */
  this.input = input;

  /** Stream for outgoing data. */
  this.output = output;

  /** The server associated with this request. */
  this.server = server;

  /** The port on which the server is running. */
  this.port = port;

  /** The outgoing poort used by this connection. */
  this._outgoingPort = outgoingPort;

  /** The serial number of this connection. */
  this.number = number;

  /** Reference to the underlying transport. */
  this.transport = transport;

  /**
   * The request for which a response is being generated, null if the
   * incoming request has not been fully received or if it had errors.
   */
  this.request = null;

  /** This allows a connection to disambiguate between a peer initiating a
   *  close and the socket being forced closed on shutdown.
   */
  this._closed = false;

  /** State variable for debugging. */
  this._processed = false;

  /** whether or not 1st line of request has been received */
  this._requestStarted = false;
}
Connection.prototype = {
  /** Closes this connection's input/output streams. */
  close() {
    if (this._closed) {
      return;
    }

    dumpn(
      "*** closing connection " + this.number + " on port " + this._outgoingPort
    );

    this.input.close();
    this.output.close();
    this._closed = true;

    var server = this.server;
    server._connectionClosed(this);

    // If an error triggered a server shutdown, act on it now
    if (server._doQuit) {
      server.stop(function () {
        /* not like we can do anything better */
      });
    }
  },

  /**
   * Initiates processing of this connection, using the data in the given
   * request.
   *
   * @param request : Request
   *   the request which should be processed
   */
  process(request) {
    NS_ASSERT(!this._closed && !this._processed);

    this._processed = true;

    this.request = request;
    this.server._handler.handleResponse(this);
  },

  /**
   * Initiates processing of this connection, generating a response with the
   * given HTTP error code.
   *
   * @param code : uint
   *   an HTTP code, so in the range [0, 1000)
   * @param request : Request
   *   incomplete data about the incoming request (since there were errors
   *   during its processing
   */
  processError(code, request) {
    NS_ASSERT(!this._closed && !this._processed);

    this._processed = true;
    this.request = request;
    this.server._handler.handleError(code, this);
  },

  /** Converts this to a string for debugging purposes. */
  toString() {
    return (
      "<Connection(" +
      this.number +
      (this.request ? ", " + this.request.path : "") +
      "): " +
      (this._closed ? "closed" : "open") +
      ">"
    );
  },

  requestStarted() {
    this._requestStarted = true;
  },
};

/** Returns an array of count bytes from the given input stream. */
function readBytes(inputStream, count) {
  return new BinaryInputStream(inputStream).readByteArray(count);
}

/** Request reader processing states; see RequestReader for details. */
const READER_IN_REQUEST_LINE = 0;
const READER_IN_HEADERS = 1;
const READER_IN_BODY = 2;
const READER_FINISHED = 3;

/**
 * Reads incoming request data asynchronously, does any necessary preprocessing,
 * and forwards it to the request handler.  Processing occurs in three states:
 *
 *   READER_IN_REQUEST_LINE     Reading the request's status line
 *   READER_IN_HEADERS          Reading headers in the request
 *   READER_IN_BODY             Reading the body of the request
 *   READER_FINISHED            Entire request has been read and processed
 *
 * During the first two stages, initial metadata about the request is gathered
 * into a Request object.  Once the status line and headers have been processed,
 * we start processing the body of the request into the Request.  Finally, when
 * the entire body has been read, we create a Response and hand it off to the
 * ServerHandler to be given to the appropriate request handler.
 *
 * @param connection : Connection
 *   the connection for the request being read
 */
function RequestReader(connection) {
  /** Connection metadata for this request. */
  this._connection = connection;

  /**
   * A container providing line-by-line access to the raw bytes that make up the
   * data which has been read from the connection but has not yet been acted
   * upon (by passing it to the request handler or by extracting request
   * metadata from it).
   */
  this._data = new LineData();

  /**
   * The amount of data remaining to be read from the body of this request.
   * After all headers in the request have been read this is the value in the
   * Content-Length header, but as the body is read its value decreases to zero.
   */
  this._contentLength = 0;

  /** The current state of parsing the incoming request. */
  this._state = READER_IN_REQUEST_LINE;

  /** Metadata constructed from the incoming request for the request handler. */
  this._metadata = new Request(connection.port);

  /**
   * Used to preserve state if we run out of line data midway through a
   * multi-line header.  _lastHeaderName stores the name of the header, while
   * _lastHeaderValue stores the value we've seen so far for the header.
   *
   * These fields are always either both undefined or both strings.
   */
  this._lastHeaderName = this._lastHeaderValue = undefined;
}
RequestReader.prototype = {
  // NSIINPUTSTREAMCALLBACK

  /**
   * Called when more data from the incoming request is available.  This method
   * then reads the available data from input and deals with that data as
   * necessary, depending upon the syntax of already-downloaded data.
   *
   * @param input : nsIAsyncInputStream
   *   the stream of incoming data from the connection
   */
  onInputStreamReady(input) {
    dumpn(
      "*** onInputStreamReady(input=" +
        input +
        ") on thread " +
        Services.tm.currentThread +
        " (main is " +
        Services.tm.mainThread +
        ")"
    );
    dumpn("*** this._state == " + this._state);

    // Handle cases where we get more data after a request error has been
    // discovered but *before* we can close the connection.
    var data = this._data;
    if (!data) {
      return;
    }

    try {
      data.appendBytes(readBytes(input, input.available()));
    } catch (e) {
      if (streamClosed(e)) {
        dumpn(
          "*** WARNING: unexpected error when reading from socket; will " +
            "be treated as if the input stream had been closed"
        );
        dumpn("*** WARNING: actual error was: " + e);
      }

      // We've lost a race -- input has been closed, but we're still expecting
      // to read more data.  available() will throw in this case, and since
      // we're dead in the water now, destroy the connection.
      dumpn(
        "*** onInputStreamReady called on a closed input, destroying " +
          "connection"
      );
      this._connection.close();
      return;
    }

    switch (this._state) {
      default:
        NS_ASSERT(false, "invalid state: " + this._state);
        break;

      case READER_IN_REQUEST_LINE:
        if (!this._processRequestLine()) {
          break;
        }
      /* fall through */

      case READER_IN_HEADERS:
        if (!this._processHeaders()) {
          break;
        }
      /* fall through */

      case READER_IN_BODY:
        this._processBody();
    }

    if (this._state != READER_FINISHED) {
      input.asyncWait(this, 0, 0, Services.tm.currentThread);
    }
  },

  //
  // see nsISupports.QueryInterface
  //
  QueryInterface: ChromeUtils.generateQI(["nsIInputStreamCallback"]),

  // PRIVATE API

  /**
   * Processes unprocessed, downloaded data as a request line.
   *
   * @returns boolean
   *   true iff the request line has been fully processed
   */
  _processRequestLine() {
    NS_ASSERT(this._state == READER_IN_REQUEST_LINE);

    // Servers SHOULD ignore any empty line(s) received where a Request-Line
    // is expected (section 4.1).
    var data = this._data;
    var line = {};
    var readSuccess;
    while ((readSuccess = data.readLine(line)) && line.value == "") {
      dumpn("*** ignoring beginning blank line...");
    }

    // if we don't have a full line, wait until we do
    if (!readSuccess) {
      return false;
    }

    // we have the first non-blank line
    try {
      this._parseRequestLine(line.value);
      this._state = READER_IN_HEADERS;
      this._connection.requestStarted();
      return true;
    } catch (e) {
      this._handleError(e);
      return false;
    }
  },

  /**
   * Processes stored data, assuming it is either at the beginning or in
   * the middle of processing request headers.
   *
   * @returns boolean
   *   true iff header data in the request has been fully processed
   */
  _processHeaders() {
    NS_ASSERT(this._state == READER_IN_HEADERS);

    // XXX things to fix here:
    //
    // - need to support RFC 2047-encoded non-US-ASCII characters

    try {
      var done = this._parseHeaders();
      if (done) {
        var request = this._metadata;

        // XXX this is wrong for requests with transfer-encodings applied to
        //     them, particularly chunked (which by its nature can have no
        //     meaningful Content-Length header)!
        this._contentLength = request.hasHeader("Content-Length")
          ? parseInt(request.getHeader("Content-Length"), 10)
          : 0;
        dumpn("_processHeaders, Content-length=" + this._contentLength);

        this._state = READER_IN_BODY;
      }
      return done;
    } catch (e) {
      this._handleError(e);
      return false;
    }
  },

  /**
   * Processes stored data, assuming it is either at the beginning or in
   * the middle of processing the request body.
   *
   * @returns boolean
   *   true iff the request body has been fully processed
   */
  _processBody() {
    NS_ASSERT(this._state == READER_IN_BODY);

    // XXX handle chunked transfer-coding request bodies!

    try {
      if (this._contentLength > 0) {
        var data = this._data.purge();
        var count = Math.min(data.length, this._contentLength);
        dumpn(
          "*** loading data=" +
            data +
            " len=" +
            data.length +
            " excess=" +
            (data.length - count)
        );
        data.length = count;

        var bos = new BinaryOutputStream(this._metadata._bodyOutputStream);
        bos.writeByteArray(data);
        this._contentLength -= count;
      }

      dumpn("*** remaining body data len=" + this._contentLength);
      if (this._contentLength == 0) {
        this._validateRequest();
        this._state = READER_FINISHED;
        this._handleResponse();
        return true;
      }

      return false;
    } catch (e) {
      this._handleError(e);
      return false;
    }
  },

  /**
   * Does various post-header checks on the data in this request.
   *
   * @throws : HttpError
   *   if the request was malformed in some way
   */
  _validateRequest() {
    NS_ASSERT(this._state == READER_IN_BODY);

    dumpn("*** _validateRequest");

    var metadata = this._metadata;
    var headers = metadata._headers;

    // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header
    var identity = this._connection.server.identity;
    if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) {
      if (!headers.hasHeader("Host")) {
        dumpn("*** malformed HTTP/1.1 or greater request with no Host header!");
        throw HTTP_400;
      }

      // If the Request-URI wasn't absolute, then we need to determine our host.
      // We have to determine what scheme was used to access us based on the
      // server identity data at this point, because the request just doesn't
      // contain enough data on its own to do this, sadly.
      if (!metadata._host) {
        var host, port;
        var hostPort = headers.getHeader("Host");
        var colon = hostPort.lastIndexOf(":");
        if (hostPort.lastIndexOf("]") > colon) {
          colon = -1;
        }
        if (colon < 0) {
          host = hostPort;
          port = "";
        } else {
          host = hostPort.substring(0, colon);
          port = hostPort.substring(colon + 1);
        }

        // NB: We allow an empty port here because, oddly, a colon may be
        //     present even without a port number, e.g. "example.com:"; in this
        //     case the default port applies.
        if (!HOST_REGEX.test(host) || !/^\d*$/.test(port)) {
          dumpn(
            "*** malformed hostname (" +
              hostPort +
              ") in Host " +
              "header, 400 time"
          );
          throw HTTP_400;
        }

        // If we're not given a port, we're stuck, because we don't know what
        // scheme to use to look up the correct port here, in general.  Since
        // the HTTPS case requires a tunnel/proxy and thus requires that the
        // requested URI be absolute (and thus contain the necessary
        // information), let's assume HTTP will prevail and use that.
        port = +port || 80;

        var scheme = identity.getScheme(host, port);
        if (!scheme) {
          dumpn(
            "*** unrecognized hostname (" +
              hostPort +
              ") in Host " +
              "header, 400 time"
          );
          throw HTTP_400;
        }

        metadata._scheme = scheme;
        metadata._host = host;
        metadata._port = port;
      }
    } else {
      NS_ASSERT(
        metadata._host === undefined,
        "HTTP/1.0 doesn't allow absolute paths in the request line!"
      );

      metadata._scheme = identity.primaryScheme;
      metadata._host = identity.primaryHost;
      metadata._port = identity.primaryPort;
    }

    NS_ASSERT(
      identity.has(metadata._scheme, metadata._host, metadata._port),
      "must have a location we recognize by now!"
    );
  },

  /**
   * Handles responses in case of error, either in the server or in the request.
   *
   * @param e
   *   the specific error encountered, which is an HttpError in the case where
   *   the request is in some way invalid or cannot be fulfilled; if this isn't
   *   an HttpError we're going to be paranoid and shut down, because that
   *   shouldn't happen, ever
   */
  _handleError(e) {
    // Don't fall back into normal processing!
    this._state = READER_FINISHED;

    var server = this._connection.server;
    if (e instanceof HttpError) {
      var code = e.code;
    } else {
      dumpn(
        "!!! UNEXPECTED ERROR: " +
          e +
          (e.lineNumber ? ", line " + e.lineNumber : "")
      );

      // no idea what happened -- be paranoid and shut down
      code = 500;
      server._requestQuit();
    }

    // make attempted reuse of data an error
    this._data = null;

    this._connection.processError(code, this._metadata);
  },

  /**
   * Now that we've read the request line and headers, we can actually hand off
   * the request to be handled.
   *
   * This method is called once per request, after the request line and all
   * headers and the body, if any, have been received.
   */
  _handleResponse() {
    NS_ASSERT(this._state == READER_FINISHED);

    // We don't need the line-based data any more, so make attempted reuse an
    // error.
    this._data = null;

    this._connection.process(this._metadata);
  },

  // PARSING

  /**
   * Parses the request line for the HTTP request associated with this.
   *
   * @param line : string
   *   the request line
   */
  _parseRequestLine(line) {
    NS_ASSERT(this._state == READER_IN_REQUEST_LINE);

    dumpn("*** _parseRequestLine('" + line + "')");

    var metadata = this._metadata;

    // clients and servers SHOULD accept any amount of SP or HT characters
    // between fields, even though only a single SP is required (section 19.3)
    var request = line.split(/[ \t]+/);
    if (!request || request.length != 3) {
      dumpn("*** No request in line");
      throw HTTP_400;
    }

    metadata._method = request[0];

    // get the HTTP version
    var ver = request[2];
    var match = ver.match(/^HTTP\/(\d+\.\d+)$/);
    if (!match) {
      dumpn("*** No HTTP version in line");
      throw HTTP_400;
    }

    // determine HTTP version
    try {
      metadata._httpVersion = new nsHttpVersion(match[1]);
      if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0)) {
        throw new Error("unsupported HTTP version");
      }
    } catch (e) {
      // we support HTTP/1.0 and HTTP/1.1 only
      throw HTTP_501;
    }

    var fullPath = request[1];

    if (metadata._method == "CONNECT") {
      metadata._path = "CONNECT";
      metadata._scheme = "https";
      [metadata._host, metadata._port] = fullPath.split(":");
      return;
    }

    var serverIdentity = this._connection.server.identity;
    var scheme, host, port;

    if (fullPath.charAt(0) != "/") {
      // No absolute paths in the request line in HTTP prior to 1.1
      if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) {
        dumpn("*** Metadata version too low");
        throw HTTP_400;
      }

      try {
        var uri = Services.io.newURI(fullPath);
        fullPath = uri.pathQueryRef;
        scheme = uri.scheme;
        host = uri.asciiHost;
        if (host.includes(":")) {
          // If the host still contains a ":", then it is an IPv6 address.
          // IPv6 addresses-as-host are registered with brackets, so we need to
          // wrap the host in brackets because nsIURI's host lacks them.
          // This inconsistency in nsStandardURL is tracked at bug 1195459.
          host = `[${host}]`;
        }
        metadata._host = host;
        port = uri.port;
        if (port === -1) {
          if (scheme === "http") {
            port = 80;
          } else if (scheme === "https") {
            port = 443;
          } else {
            dumpn("*** Unknown scheme: " + scheme);
            throw HTTP_400;
          }
        }
      } catch (e) {
        // If the host is not a valid host on the server, the response MUST be a
        // 400 (Bad Request) error message (section 5.2).  Alternately, the URI
        // is malformed.
        dumpn("*** Threw when dealing with URI: " + e);
        throw HTTP_400;
      }

      if (
        !serverIdentity.has(scheme, host, port) ||
        fullPath.charAt(0) != "/"
      ) {
        dumpn("*** serverIdentity unknown or path does not start with '/'");
        throw HTTP_400;
      }
    }

    var splitter = fullPath.indexOf("?");
    if (splitter < 0) {
      // _queryString already set in ctor
      metadata._path = fullPath;
    } else {
      metadata._path = fullPath.substring(0, splitter);
      metadata._queryString = fullPath.substring(splitter + 1);
    }

    metadata._scheme = scheme;
    metadata._host = host;
    metadata._port = port;
  },

  /**
   * Parses all available HTTP headers in this until the header-ending CRLFCRLF,
   * adding them to the store of headers in the request.
   *
   * @throws
   *   HTTP_400 if the headers are malformed
   * @returns boolean
   *   true if all headers have now been processed, false otherwise
   */
  _parseHeaders() {
    NS_ASSERT(this._state == READER_IN_HEADERS);

    dumpn("*** _parseHeaders");

    var data = this._data;

    var headers = this._metadata._headers;
    var lastName = this._lastHeaderName;
    var lastVal = this._lastHeaderValue;

    var line = {};
    // eslint-disable-next-line no-constant-condition
    while (true) {
      dumpn("*** Last name: '" + lastName + "'");
      dumpn("*** Last val: '" + lastVal + "'");
      NS_ASSERT(
        !((lastVal === undefined) ^ (lastName === undefined)),
        lastName === undefined
          ? "lastVal without lastName?  lastVal: '" + lastVal + "'"
          : "lastName without lastVal?  lastName: '" + lastName + "'"
      );

      if (!data.readLine(line)) {
        // save any data we have from the header we might still be processing
        this._lastHeaderName = lastName;
        this._lastHeaderValue = lastVal;
        return false;
      }

      var lineText = line.value;
      dumpn("*** Line text: '" + lineText + "'");
      var firstChar = lineText.charAt(0);

      // blank line means end of headers
      if (lineText == "") {
        // we're finished with the previous header
        if (lastName) {
          try {
            headers.setHeader(lastName, lastVal, true);
          } catch (e) {
            dumpn("*** setHeader threw on last header, e == " + e);
            throw HTTP_400;
          }
        } else {
          // no headers in request -- valid for HTTP/1.0 requests
        }

        // either way, we're done processing headers
        this._state = READER_IN_BODY;
        return true;
      } else if (firstChar == " " || firstChar == "\t") {
        // multi-line header if we've already seen a header line
        if (!lastName) {
          dumpn("We don't have a header to continue!");
          throw HTTP_400;
        }

        // append this line's text to the value; starts with SP/HT, so no need
        // for separating whitespace
        lastVal += lineText;
      } else {
        // we have a new header, so set the old one (if one existed)
        if (lastName) {
          try {
            headers.setHeader(lastName, lastVal, true);
          } catch (e) {
            dumpn("*** setHeader threw on a header, e == " + e);
            throw HTTP_400;
          }
        }

        var colon = lineText.indexOf(":"); // first colon must be splitter
        if (colon < 1) {
          dumpn("*** No colon or missing header field-name");
          throw HTTP_400;
        }

        // set header name, value (to be set in the next loop, usually)
        lastName = lineText.substring(0, colon);
        lastVal = lineText.substring(colon + 1);
      } // empty, continuation, start of header
    } // while (true)
  },
};

/** The character codes for CR and LF. */
const CR = 0x0d,
  LF = 0x0a;

/**
 * Calculates the number of characters before the first CRLF pair in array, or
 * -1 if the array contains no CRLF pair.
 *
 * @param array : Array
 *   an array of numbers in the range [0, 256), each representing a single
 *   character; the first CRLF is the lowest index i where
 *   |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|,
 *   if such an |i| exists, and -1 otherwise
 * @param start : uint
 *   start index from which to begin searching in array
 * @returns int
 *   the index of the first CRLF if any were present, -1 otherwise
 */
function findCRLF(array, start) {
  for (var i = array.indexOf(CR, start); i >= 0; i = array.indexOf(CR, i + 1)) {
    if (array[i + 1] == LF) {
      return i;
    }
  }
  return -1;
}

/**
 * A container which provides line-by-line access to the arrays of bytes with
 * which it is seeded.
 */
export function LineData() {
  /** An array of queued bytes from which to get line-based characters. */
  this._data = [];

  /** Start index from which to search for CRLF. */
  this._start = 0;
}
LineData.prototype = {
  /**
   * Appends the bytes in the given array to the internal data cache maintained
   * by this.
   */
  appendBytes(bytes) {
    var count = bytes.length;
    var quantum = 262144; // just above half SpiderMonkey's argument-count limit
    if (count < quantum) {
      Array.prototype.push.apply(this._data, bytes);
      return;
    }

    // Large numbers of bytes may cause Array.prototype.push to be called with
    // more arguments than the JavaScript engine supports.  In that case append
    // bytes in fixed-size amounts until all bytes are appended.
    for (var start = 0; start < count; start += quantum) {
      var slice = bytes.slice(start, Math.min(start + quantum, count));
      Array.prototype.push.apply(this._data, slice);
    }
  },

  /**
   * Removes and returns a line of data, delimited by CRLF, from this.
   *
   * @param out
   *   an object whose "value" property will be set to the first line of text
   *   present in this, sans CRLF, if this contains a full CRLF-delimited line
   *   of text; if this doesn't contain enough data, the value of the property
   *   is undefined
   * @returns boolean
   *   true if a full line of data could be read from the data in this, false
   *   otherwise
   */
  readLine(out) {
    var data = this._data;
    var length = findCRLF(data, this._start);
    if (length < 0) {
      this._start = data.length;

      // But if our data ends in a CR, we have to back up one, because
      // the first byte in the next packet might be an LF and if we
      // start looking at data.length we won't find it.
      if (data.length && data[data.length - 1] === CR) {
        --this._start;
      }

      return false;
    }

    // Reset for future lines.
    this._start = 0;

    //
    // We have the index of the CR, so remove all the characters, including
    // CRLF, from the array with splice, and convert the removed array
    // (excluding the trailing CRLF characters) into the corresponding string.
    //
    var leading = data.splice(0, length + 2);
    var quantum = 262144;
    var line = "";
    for (var start = 0; start < length; start += quantum) {
      var slice = leading.slice(start, Math.min(start + quantum, length));
      line += String.fromCharCode.apply(null, slice);
    }

    out.value = line;
    return true;
  },

  /**
   * Removes the bytes currently within this and returns them in an array.
   *
   * @returns Array
   *   the bytes within this when this method is called
   */
  purge() {
    var data = this._data;
    this._data = [];
    return data;
  },
};

/**
 * Creates a request-handling function for an nsIHttpRequestHandler object.
 */
function createHandlerFunc(handler) {
  return function (metadata, response) {
    handler.handle(metadata, response);
  };
}

/**
 * The default handler for directories; writes an HTML response containing a
 * slightly-formatted directory listing.
 */
function defaultIndexHandler(metadata, response) {
  response.setHeader("Content-Type", "text/html;charset=utf-8", false);

  var path = htmlEscape(decodeURI(metadata.path));

  //
  // Just do a very basic bit of directory listings -- no need for too much
  // fanciness, especially since we don't have a style sheet in which we can
  // stick rules (don't want to pollute the default path-space).
  //

  var body =
    "<html>\
                <head>\
                  <title>" +
    path +
    "</title>\
                </head>\
                <body>\
                  <h1>" +
    path +
    '</h1>\
                  <ol style="list-style-type: none">';

  var directory = metadata.getProperty("directory");
  NS_ASSERT(directory && directory.isDirectory());

  var fileList = [];
  var files = directory.directoryEntries;
  while (files.hasMoreElements()) {
    var f = files.nextFile;
    let name = f.leafName;
    if (
      !f.isHidden() &&
      (name.charAt(name.length - 1) != HIDDEN_CHAR ||
        name.charAt(name.length - 2) == HIDDEN_CHAR)
    ) {
      fileList.push(f);
    }
  }

  fileList.sort(fileSort);

  for (var i = 0; i < fileList.length; i++) {
    var file = fileList[i];
    try {
      let name = file.leafName;
      if (name.charAt(name.length - 1) == HIDDEN_CHAR) {
        name = name.substring(0, name.length - 1);
      }
      var sep = file.isDirectory() ? "/" : "";

      // Note: using " to delimit the attribute here because encodeURIComponent
      //       passes through '.
      var item =
        '<li><a href="' +
        encodeURIComponent(name) +
        sep +
        '">' +
        htmlEscape(name) +
        sep +
        "</a></li>";

      body += item;
    } catch (e) {
      /* some file system error, ignore the file */
    }
  }

  body +=
    "    </ol>\
                </body>\
              </html>";

  response.bodyOutputStream.write(body, body.length);
}

/**
 * Sorts a and b (nsIFile objects) into an aesthetically pleasing order.
 */
function fileSort(a, b) {
  var dira = a.isDirectory(),
    dirb = b.isDirectory();

  if (dira && !dirb) {
    return -1;
  }
  if (dirb && !dira) {
    return 1;
  }

  var namea = a.leafName.toLowerCase(),
    nameb = b.leafName.toLowerCase();
  return nameb > namea ? -1 : 1;
}

/**
 * Converts an externally-provided path into an internal path for use in
 * determining file mappings.
 *
 * @param path
 *   the path to convert
 * @param encoded
 *   true if the given path should be passed through decodeURI prior to
 *   conversion
 * @throws URIError
 *   if path is incorrectly encoded
 */
function toInternalPath(path, encoded) {
  if (encoded) {
    path = decodeURI(path);
  }

  var comps = path.split("/");
  for (var i = 0, sz = comps.length; i < sz; i++) {
    var comp = comps[i];
    if (comp.charAt(comp.length - 1) == HIDDEN_CHAR) {
      comps[i] = comp + HIDDEN_CHAR;
    }
  }
  return comps.join("/");
}

const PERMS_READONLY = (4 << 6) | (4 << 3) | 4;

/**
 * Adds custom-specified headers for the given file to the given response, if
 * any such headers are specified.
 *
 * @param file
 *   the file on the disk which is to be written
 * @param metadata
 *   metadata about the incoming request
 * @param response
 *   the Response to which any specified headers/data should be written
 * @throws HTTP_500
 *   if an error occurred while processing custom-specified headers
 */
function maybeAddHeadersInternal(
  file,
  metadata,
  response,
  informationalResponse
) {
  var name = file.leafName;
  if (name.charAt(name.length - 1) == HIDDEN_CHAR) {
    name = name.substring(0, name.length - 1);
  }

  var headerFile = file.parent;
  if (!informationalResponse) {
    headerFile.append(name + HEADERS_SUFFIX);
--> --------------------

--> maximum size reached

--> --------------------

[ 0.66Quellennavigators  Projekt   ]