/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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";

let { Ci } = require("chrome");
let Services = require("Services");
let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
loader.lazyRequireGetter(this, "DebuggerSocket",
  "devtools/toolkit/security/socket", true);
loader.lazyRequireGetter(this, "AuthenticationResult",
  "devtools/toolkit/security/auth", true);

DevToolsUtils.defineLazyGetter(this, "bundle", () => {
  const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties";
  return Services.strings.createBundle(DBG_STRINGS_URI);
});

let Client = exports.Client = {};
let 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 debugger server.
 * @param port number
 *        The port number of the debugger server.
 * @param cert object (optional)
 *        The server's cert details.
 * @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);
  }
  let title = bundle.GetStringFromName("clientSendOOBTitle");
  let header = bundle.GetStringFromName("clientSendOOBHeader");
  let hashMsg = bundle.formatStringFromName("clientSendOOBHash",
                                            [oob.sha256], 1);
  let token = oob.sha256.replace(/:/g, "").toLowerCase() + oob.k;
  let tokenMsg = bundle.formatStringFromName("clientSendOOBToken",
                                             [token], 1);
  let msg =`${header}\n\n${hashMsg}\n${tokenMsg}`;
  let prompt = Services.prompt;
  let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_CANCEL;

  // Listen for the window our prompt opens, so we can close it programatically
  let promptWindow;
  let windowListener = {
    onOpenWindow(xulWindow) {
      let win = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIDOMWindow);
      win.addEventListener("load", function listener() {
        win.removeEventListener("load", listener, false);
        if (win.document.documentElement.getAttribute("id") != "commonDialog") {
          return;
        }
        // Found the window
        promptWindow = win;
        Services.wm.removeListener(windowListener);
      }, false);
    },
    onCloseWindow() {},
    onWindowTitleChange() {}
  };
  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 debugger 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 }) => {
  let title = bundle.GetStringFromName("remoteIncomingPromptTitle");
  let header = bundle.GetStringFromName("remoteIncomingPromptHeader");
  let clientEndpoint = `${client.host}:${client.port}`;
  let clientMsg =
    bundle.formatStringFromName("remoteIncomingPromptClientEndpoint",
                                [clientEndpoint], 1);
  let serverEndpoint = `${server.host}:${server.port}`;
  let serverMsg =
    bundle.formatStringFromName("remoteIncomingPromptServerEndpoint",
                                [serverEndpoint], 1);
  let footer = bundle.GetStringFromName("remoteIncomingPromptFooter");
  let msg =`${header}\n\n${clientMsg}\n${serverMsg}\n\n${footer}`;
  let disableButton = bundle.GetStringFromName("remoteIncomingPromptDisable");
  let prompt = Services.prompt;
  let 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;
  let 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 = () => {
  let title = bundle.GetStringFromName("serverReceiveOOBTitle");
  let msg = bundle.GetStringFromName("serverReceiveOOBBody");
  let input = { value: null };
  let prompt = Services.prompt;
  let 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();
  let k = input.substring(64);
  return { sha256, k };
};
