// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* 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/. */
/* globals DebuggerServer */
"use strict";

XPCOMUtils.defineLazyGetter(this, "DebuggerServer", () => {
  let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
  let { DebuggerServer } = require("devtools/server/main");
  return DebuggerServer;
});

var RemoteDebugger = {
  init() {
    USBRemoteDebugger.init();
    WiFiRemoteDebugger.init();
  },

  get isAnyEnabled() {
    return USBRemoteDebugger.isEnabled || WiFiRemoteDebugger.isEnabled;
  },

  /**
   * Prompt the user to accept or decline the incoming connection.
   *
   * @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
   *        devtools/shared/security/auth.js.
   * @return An AuthenticationResult value.
   *         A promise that will be resolved to the above is also allowed.
   */
  allowConnection(session) {
    if (this._promptingForAllow) {
      // Don't stack connection prompts if one is already open
      return DebuggerServer.AuthenticationResult.DENY;
    }

    if (!session.server.port) {
      this._promptingForAllow = this._promptForUSB(session);
    } else {
      this._promptingForAllow = this._promptForTCP(session);
    }
    this._promptingForAllow.then(() => this._promptingForAllow = null);

    return this._promptingForAllow;
  },

  _promptForUSB(session) {
    if (session.authentication !== 'PROMPT') {
      // This dialog is not prepared for any other authentication method at
      // this time.
      return DebuggerServer.AuthenticationResult.DENY;
    }

    return new Promise(resolve => {
      let title = Strings.browser.GetStringFromName("remoteIncomingPromptTitle");
      let msg = Strings.browser.GetStringFromName("remoteIncomingPromptUSB");
      let allow = Strings.browser.GetStringFromName("remoteIncomingPromptAllow");
      let deny = Strings.browser.GetStringFromName("remoteIncomingPromptDeny");

      // Make prompt. Note: button order is in reverse.
      let prompt = new Prompt({
        window: null,
        hint: "remotedebug",
        title: title,
        message: msg,
        buttons: [ allow, deny ],
        priority: 1
      });

      prompt.show(data => {
        let result = data.button;
        if (result === 0) {
          resolve(DebuggerServer.AuthenticationResult.ALLOW);
        } else {
          resolve(DebuggerServer.AuthenticationResult.DENY);
        }
      });
    });
  },

  _promptForTCP(session) {
    if (session.authentication !== 'OOB_CERT' || !session.client.cert) {
      // This dialog is not prepared for any other authentication method at
      // this time.
      return DebuggerServer.AuthenticationResult.DENY;
    }

    return new Promise(resolve => {
      let title = Strings.browser.GetStringFromName("remoteIncomingPromptTitle");
      let msg = Strings.browser.formatStringFromName("remoteIncomingPromptTCP", [
        session.client.host,
        session.client.port
      ], 2);
      let scan = Strings.browser.GetStringFromName("remoteIncomingPromptScan");
      let scanAndRemember = Strings.browser.GetStringFromName("remoteIncomingPromptScanAndRemember");
      let deny = Strings.browser.GetStringFromName("remoteIncomingPromptDeny");

      // Make prompt. Note: button order is in reverse.
      let prompt = new Prompt({
        window: null,
        hint: "remotedebug",
        title: title,
        message: msg,
        buttons: [ scan, scanAndRemember, deny ],
        priority: 1
      });

      prompt.show(data => {
        let result = data.button;
        if (result === 0) {
          resolve(DebuggerServer.AuthenticationResult.ALLOW);
        } else if (result === 1) {
          resolve(DebuggerServer.AuthenticationResult.ALLOW_PERSIST);
        } else {
          resolve(DebuggerServer.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 instructs Fennec to invoke a QR decoder and return the
   * the data it contains back here.
   *
   * @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.
   */
  receiveOOB() {
    if (this._receivingOOB) {
      return this._receivingOOB;
    }

    this._receivingOOB = Messaging.sendRequestForResult({
      type: "DevToolsAuth:Scan"
    }).then(data => {
      return JSON.parse(data);
    });

    this._receivingOOB.then(() => this._receivingOOB = null);

    return this._receivingOOB;
  },

  initServer: function() {
    if (DebuggerServer.initialized) {
      return;
    }

    DebuggerServer.init();

    // Add browser and Fennec specific actors
    DebuggerServer.addBrowserActors();
    DebuggerServer.registerModule("resource://gre/modules/dbg-browser-actors.js");

    // Allow debugging of chrome for any process
    DebuggerServer.allowChromeProcess = true;
  }
};

RemoteDebugger.allowConnection =
  RemoteDebugger.allowConnection.bind(RemoteDebugger);
RemoteDebugger.receiveOOB =
  RemoteDebugger.receiveOOB.bind(RemoteDebugger);

var USBRemoteDebugger = {

  init() {
    Services.prefs.addObserver("devtools.", this, false);

    if (this.isEnabled) {
      this.start();
    }
  },

  observe(subject, topic, data) {
    if (topic != "nsPref:changed") {
      return;
    }

    switch (data) {
      case "devtools.remote.usb.enabled":
        Services.prefs.setBoolPref("devtools.debugger.remote-enabled",
                                   RemoteDebugger.isAnyEnabled);
        if (this.isEnabled) {
          this.start();
        } else {
          this.stop();
        }
        break;

      case "devtools.debugger.remote-port":
      case "devtools.debugger.unix-domain-socket":
        if (this.isEnabled) {
          this.stop();
          this.start();
        }
        break;
    }
  },

  get isEnabled() {
    return Services.prefs.getBoolPref("devtools.remote.usb.enabled");
  },

  start: function() {
    if (this._listener) {
      return;
    }

    RemoteDebugger.initServer();

    let portOrPath =
      Services.prefs.getCharPref("devtools.debugger.unix-domain-socket") ||
      Services.prefs.getIntPref("devtools.debugger.remote-port");

    try {
      dump("Starting USB debugger on " + portOrPath);
      let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
      let authenticator = new AuthenticatorType.Server();
      authenticator.allowConnection = RemoteDebugger.allowConnection;
      this._listener = DebuggerServer.createListener();
      this._listener.portOrPath = portOrPath;
      this._listener.authenticator = authenticator;
      this._listener.open();
    } catch (e) {
      dump("Unable to start USB debugger server: " + e);
    }
  },

  stop: function() {
    if (!this._listener) {
      return;
    }

    try {
      this._listener.close();
      this._listener = null;
    } catch (e) {
      dump("Unable to stop USB debugger server: " + e);
    }
  }

};

var WiFiRemoteDebugger = {

  init() {
    Services.prefs.addObserver("devtools.", this, false);

    if (this.isEnabled) {
      this.start();
    }
  },

  observe(subject, topic, data) {
    if (topic != "nsPref:changed") {
      return;
    }

    switch (data) {
      case "devtools.remote.wifi.enabled":
        Services.prefs.setBoolPref("devtools.debugger.remote-enabled",
                                   RemoteDebugger.isAnyEnabled);
        // Allow remote debugging on non-local interfaces when WiFi debug is
        // enabled
        // TODO: Bug 1034411: Lock down to WiFi interface only
        Services.prefs.setBoolPref("devtools.debugger.force-local",
                                   !this.isEnabled);
        if (this.isEnabled) {
          this.start();
        } else {
          this.stop();
        }
        break;
    }
  },

  get isEnabled() {
    return Services.prefs.getBoolPref("devtools.remote.wifi.enabled");
  },

  start: function() {
    if (this._listener) {
      return;
    }

    RemoteDebugger.initServer();

    try {
      dump("Starting WiFi debugger");
      let AuthenticatorType = DebuggerServer.Authenticators.get("OOB_CERT");
      let authenticator = new AuthenticatorType.Server();
      authenticator.allowConnection = RemoteDebugger.allowConnection;
      authenticator.receiveOOB = RemoteDebugger.receiveOOB;
      this._listener = DebuggerServer.createListener();
      this._listener.portOrPath = -1 /* any available port */;
      this._listener.authenticator = authenticator;
      this._listener.discoverable = true;
      this._listener.encryption = true;
      this._listener.open();
      let port = this._listener.port;
      dump("Started WiFi debugger on " + port);
    } catch (e) {
      dump("Unable to start WiFi debugger server: " + e);
    }
  },

  stop: function() {
    if (!this._listener) {
      return;
    }

    try {
      this._listener.close();
      this._listener = null;
    } catch (e) {
      dump("Unable to stop WiFi debugger server: " + e);
    }
  }

};
