
  try {
    // Might throw both when calling newURI or when accessing the host/port.
    const hostUri = Services.io.newURI(`https://${hostHeader}`);
    const { host, port } = hostUri;
    const isHostnameValid =
      isIPAddress(hostUri) || lazy.RemoteAgent.allowHosts.includes(host);
    // For nsIURI a port value of -1 corresponds to the protocol's default port.
    const isPortValid = [-1, lazy.RemoteAgent.port].includes(port);
    return isHostnameValid && isPortValid;
  } catch (e) {
    return false;
  }
}

function isOriginValid(originHeader) {
  if (originHeader === undefined) {
    // Always accept no origin header.
    return true;
  }

  // Special case "null" origins, used for privacy sensitive or opaque origins.
  if (originHeader === "null") {
    return lazy.allowedOrigins.includes("null");
  }

  try {
    // Extract the host, port and scheme from the provided origin header.
    const { host, port, scheme } = Services.io.newURI(originHeader);
    // Check if any allowed origin matches the provided host, port and scheme.
    return lazy.allowedOriginURIs.some(
      uri => uri.host === host && uri.port === port && uri.scheme === scheme
    );
  } catch (e) {
    // Reject invalid origin headers
    return false;
  }
}

/**
 * Process the WebSocket handshake headers and return the key to be sent in
 * Sec-WebSocket-Accept response header.
 */
function processRequest({ requestLine, headers }) {
  if (!isOriginValid(headers.get("origin"))) {
    lazy.logger.debug(
      `Incorrect Origin header, allowed origins: [${lazy.allowedOrigins}]`
    );
    throw new Error(
      `The handshake request has incorrect Origin header ${headers.get(
        "origin"
      )}`
    );
  }

  if (!isHostValid(headers.get("host"))) {
    lazy.logger.debug(
      `Incorrect Host header, allowed hosts: [${lazy.RemoteAgent.allowHosts}]`
    );
    throw new Error(
      `The handshake request has incorrect Host header ${headers.get("host")}`
    );
  }

  const method = requestLine.split(" ")[0];
  if (method !== "GET") {
    throw new Error("The handshake request must use GET method");
  }

  const upgrade = headers.get("upgrade");
  if (!upgrade || upgrade.toLowerCase() !== "websocket") {
    throw new Error(
      `The handshake request has incorrect Upgrade header: ${upgrade}`
    );
  }

  const connection = headers.get("connection");
  if (
    !connection ||
    !connection
      .split(",")
      .map(t => t.trim().toLowerCase())
      .includes("upgrade")
  ) {
    throw new Error("The handshake request has incorrect Connection header");
  }

  const version = headers.get("sec-websocket-version");
  if (!version || version !== "13") {
    throw new Error(
      "The handshake request must have Sec-WebSocket-Version: 13"
    );
  }

  // Compute the accept key
  const key = headers.get("sec-websocket-key");
  if (!key) {
    throw new Error(
      "The handshake request must have a Sec-WebSocket-Key header"
    );
  }

  return { acceptKey: computeKey(key) };
}

function computeKey(key) {
  const str = `${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`;
  const data = Array.from(str, ch => ch.charCodeAt(0));
  const hash = new lazy.CryptoHash("sha1");
  hash.update(data, data.length);
  return hash.finish(true);
}

/**
 * Perform the server part of a WebSocket opening handshake
 * on an incoming connection.
 */
async function serverHandshake(request, output) {
  try {
    // Check and extract info from the request
    const { acceptKey } = processRequest(request);

    // Send response headers
    await writeHttpResponse(output, [
      "HTTP/1.1 101 Switching Protocols",
      "Server: httpd.js",
      "Upgrade: websocket",
      "Connection: Upgrade",
      `Sec-WebSocket-Accept: ${acceptKey}`,
    ]);
  } catch (error) {
    // Send error response in case of error
    await writeHttpResponse(
      output,
      [
        "HTTP/1.1 400 Bad Request",
        "Server: httpd.js",
        "Content-Type: text/plain",
      ],
      error.message
    );

    throw error;
  }
}

async function createWebSocket(transport, input, output) {
  const transportProvider = {
    setListener(upgradeListener) {
      // onTransportAvailable callback shouldn't be called synchronously
      lazy.executeSoon(() => {
        upgradeListener.onTransportAvailable(transport, input, output);
      });
    },
  };

  return new Promise((resolve, reject) => {
    const socket = WebSocket.createServerWebSocket(
      null,
      [],
      transportProvider,
      ""
    );
    socket.addEventListener("close", () => {
      input.close();
      output.close();
    });

    socket.onopen = () => resolve(socket);
    socket.onerror = err => reject(err);
  });
}

/** Upgrade an existing HTTP request from httpd.js to WebSocket. */
async function upgrade(request, response) {
  // handle response manually, allowing us to send arbitrary data
  response._powerSeized = true;

  const { transport, input, output } = response._connection;

  lazy.logger.info(
    `Perform WebSocket upgrade for incoming connection from ${transport.host}:${transport.port}`
  );

  const headers = new Map();
  for (let [key, values] of Object.entries(request._headers._headers)) {
    headers.set(key, values.join("\n"));
  }
  const convertedRequest = {
    requestLine: `${request.method} ${request.path}`,
    headers,
  };
  await serverHandshake(convertedRequest, output);

  return createWebSocket(transport, input, output);
}

export const WebSocketHandshake = { upgrade };
PK