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

"use strict";

var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
loader.lazyRequireGetter(
  this,
  "AuthenticationResult",
  "resource://devtools/shared/security/auth.js",
  true
);

const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
const L10N = new LocalizationHelper(
  "devtools/shared/locales/debugger.properties"
);

var Client = (exports.Client = {});
var Server = (exports.Server = {});

/**
 * During OOB_CERT authentication, a notification dialog like this is used to
 * to display a token which the user must transfer through some mechanism to the
 * server to authenticate the devices.
 *
 * This implementation presents the token as text for the user to transfer
 * manually.  For a mobile device, you should override this implementation with
 * something more convenient, such as displaying a QR code.
 *
 * @param host string
 *        The host name or IP address of the devtools server.
 * @param port number
 *        The port number of the devtools server.
 * @param authResult AuthenticationResult
 *        Authentication result sent from the server.
 * @param oob object (optional)
 *        The token data to be transferred during OOB_CERT step 8:
 *        * sha256: hash(ClientCert)
 *        * k     : K(random 128-bit number)
 * @return object containing:
 *         * close: Function to hide the notification
 */
Client.defaultSendOOB = ({ authResult, oob }) => {
  // Only show in the PENDING state
  if (authResult != AuthenticationResult.PENDING) {
    throw new Error("Expected PENDING result, got " + authResult);
  }
  const title = L10N.getStr("clientSendOOBTitle");
  const header = L10N.getStr("clientSendOOBHeader");
  const hashMsg = L10N.getFormatStr("clientSendOOBHash", oob.sha256);
  const token = oob.sha256.replace(/:/g, "").toLowerCase() + oob.k;
  const tokenMsg = L10N.getFormatStr("clientSendOOBToken", token);
  const msg = `${header}\n\n${hashMsg}\n${tokenMsg}`;
  const prompt = Services.prompt;
  const flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_CANCEL;

  // Listen for the window our prompt opens, so we can close it programatically
  let promptWindow;
  const windowListener = {
    onOpenWindow(xulWindow) {
      const win = xulWindow.docShell.domWindow;
      win.addEventListener(
        "load",
        function () {
          if (
            win.document.documentElement.getAttribute("id") != "commonDialog"
          ) {
            return;
          }
          // Found the window
          promptWindow = win;
          Services.wm.removeListener(windowListener);
        },
        { once: true }
      );
    },
    onCloseWindow() {},
  };
  Services.wm.addListener(windowListener);

  // nsIPrompt is typically a blocking API, so |executeSoon| to get around this
  DevToolsUtils.executeSoon(() => {
    prompt.confirmEx(null, title, msg, flags, null, null, null, null, {
      value: false,
    });
  });

  return {
    close() {
      if (!promptWindow) {
        return;
      }
      promptWindow.document.documentElement.acceptDialog();
      promptWindow = null;
    },
  };
};

/**
 * Prompt the user to accept or decline the incoming connection.  This is the
 * default implementation that products embedding the devtools server may
 * choose to override.  This can be overridden via |allowConnection| on the
 * socket's authenticator instance.
 *
 * @param session object
 *        The session object will contain at least the following fields:
 *        {
 *          authentication,
 *          client: {
 *            host,
 *            port
 *          },
 *          server: {
 *            host,
 *            port
 *          }
 *        }
 *        Specific authentication modes may include additional fields.  Check
 *        the different |allowConnection| methods in ./auth.js.
 * @return An AuthenticationResult value.
 *         A promise that will be resolved to the above is also allowed.
 */
Server.defaultAllowConnection = ({ client, server }) => {
  const title = L10N.getStr("remoteIncomingPromptTitle");
  const header = L10N.getStr("remoteIncomingPromptHeader");
  const clientEndpoint = `${client.host}:${client.port}`;
  const clientMsg = L10N.getFormatStr(
    "remoteIncomingPromptClientEndpoint",
    clientEndpoint
  );
  const serverEndpoint = `${server.host}:${server.port}`;
  const serverMsg = L10N.getFormatStr(
    "remoteIncomingPromptServerEndpoint",
    serverEndpoint
  );
  const footer = L10N.getStr("remoteIncomingPromptFooter");
  const msg = `${header}\n\n${clientMsg}\n${serverMsg}\n\n${footer}`;
  const disableButton = L10N.getStr("remoteIncomingPromptDisable");
  const prompt = Services.prompt;
  const flags =
    prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_OK +
    prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_CANCEL +
    prompt.BUTTON_POS_2 * prompt.BUTTON_TITLE_IS_STRING +
    prompt.BUTTON_POS_1_DEFAULT;
  const result = prompt.confirmEx(
    null,
    title,
    msg,
    flags,
    null,
    null,
    disableButton,
    null,
    { value: false }
  );
  if (result === 0) {
    return AuthenticationResult.ALLOW;
  }
  if (result === 2) {
    return AuthenticationResult.DISABLE_ALL;
  }
  return AuthenticationResult.DENY;
};

/**
 * During OOB_CERT authentication, the user must transfer some data through some
 * out of band mechanism from the client to the server to authenticate the
 * devices.
 *
 * This implementation prompts the user for a token as constructed by
 * |Client.defaultSendOOB| that the user needs to transfer manually.  For a
 * mobile device, you should override this implementation with something more
 * convenient, such as reading a QR code.
 *
 * @return An object containing:
 *         * sha256: hash(ClientCert)
 *         * k     : K(random 128-bit number)
 *         A promise that will be resolved to the above is also allowed.
 */
Server.defaultReceiveOOB = () => {
  const title = L10N.getStr("serverReceiveOOBTitle");
  const msg = L10N.getStr("serverReceiveOOBBody");
  let input = { value: null };
  const prompt = Services.prompt;
  const result = prompt.prompt(null, title, msg, input, null, { value: false });
  if (!result) {
    return null;
  }
  // Re-create original object from token
  input = input.value.trim();
  let sha256 = input.substring(0, 64);
  sha256 = sha256.replace(/\w{2}/g, "$&:").slice(0, -1).toUpperCase();
  const k = input.substring(64);
  return { sha256, k };
};
