Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Impressum httpd.sys.mjs   Interaktion und
Portierbarkeitunbekannt

 
/* -*- 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

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

[ Normaldarstellung0.64Diashow  etwas mehr zur Ethik  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge