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

const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/systemlibs.js");
Cu.import("resource://gre/modules/Promise.jsm");

const NETWORKMANAGER_CONTRACTID = "@mozilla.org/network/manager;1";
const NETWORKMANAGER_CID =
  Components.ID("{1ba9346b-53b5-4660-9dc6-58f0b258d0a6}");

const DEFAULT_PREFERRED_NETWORK_TYPE = Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET;

XPCOMUtils.defineLazyGetter(this, "ppmm", function() {
  return Cc["@mozilla.org/parentprocessmessagemanager;1"]
         .getService(Ci.nsIMessageBroadcaster);
});

XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
                                   "@mozilla.org/network/dns-service;1",
                                   "nsIDNSService");

XPCOMUtils.defineLazyServiceGetter(this, "gNetworkService",
                                   "@mozilla.org/network/service;1",
                                   "nsINetworkService");

XPCOMUtils.defineLazyServiceGetter(this, "gTetheringService",
                                   "@mozilla.org/tethering/service;1",
                                   "nsITetheringService");

const TOPIC_INTERFACE_REGISTERED     = "network-interface-registered";
const TOPIC_INTERFACE_UNREGISTERED   = "network-interface-unregistered";
const TOPIC_ACTIVE_CHANGED           = "network-active-changed";
const TOPIC_PREF_CHANGED             = "nsPref:changed";
const TOPIC_XPCOM_SHUTDOWN           = "xpcom-shutdown";
const TOPIC_CONNECTION_STATE_CHANGED = "network-connection-state-changed";
const PREF_MANAGE_OFFLINE_STATUS     = "network.gonk.manage-offline-status";
const PREF_NETWORK_DEBUG_ENABLED     = "network.debugging.enabled";

const IPV4_ADDRESS_ANY                 = "0.0.0.0";
const IPV6_ADDRESS_ANY                 = "::0";

const IPV4_MAX_PREFIX_LENGTH           = 32;
const IPV6_MAX_PREFIX_LENGTH           = 128;

// Connection Type for Network Information API
const CONNECTION_TYPE_CELLULAR  = 0;
const CONNECTION_TYPE_BLUETOOTH = 1;
const CONNECTION_TYPE_ETHERNET  = 2;
const CONNECTION_TYPE_WIFI      = 3;
const CONNECTION_TYPE_OTHER     = 4;
const CONNECTION_TYPE_NONE      = 5;

const MANUAL_PROXY_CONFIGURATION = 1;

var debug;
function updateDebug() {
  let debugPref = false; // set default value here.
  try {
    debugPref = debugPref || Services.prefs.getBoolPref(PREF_NETWORK_DEBUG_ENABLED);
  } catch (e) {}

  if (debugPref) {
    debug = function(s) {
      dump("-*- NetworkManager: " + s + "\n");
    };
  } else {
    debug = function(s) {};
  }
}
updateDebug();

function defineLazyRegExp(obj, name, pattern) {
  obj.__defineGetter__(name, function() {
    delete obj[name];
    return obj[name] = new RegExp(pattern);
  });
}

function ExtraNetworkInfo(aNetwork) {
  let ips = {};
  let prefixLengths = {};
  aNetwork.info.getAddresses(ips, prefixLengths);

  this.state = aNetwork.info.state;
  this.type = aNetwork.info.type;
  this.name = aNetwork.info.name;
  this.ips = ips.value;
  this.prefixLengths = prefixLengths.value;
  this.gateways = aNetwork.info.getGateways();
  this.dnses = aNetwork.info.getDnses();
  this.httpProxyHost = aNetwork.httpProxyHost;
  this.httpProxyPort = aNetwork.httpProxyPort;
  this.mtu = aNetwork.mtu;
}
ExtraNetworkInfo.prototype = {
  getAddresses: function(aIps, aPrefixLengths) {
    aIps.value = this.ips.slice();
    aPrefixLengths.value = this.prefixLengths.slice();

    return this.ips.length;
  },

  getGateways: function(aCount) {
    if (aCount) {
      aCount.value = this.gateways.length;
    }

    return this.gateways.slice();
  },

  getDnses: function(aCount) {
    if (aCount) {
      aCount.value = this.dnses.length;
    }

    return this.dnses.slice();
  }
};

function NetworkInterfaceLinks()
{
  this.resetLinks();
}
NetworkInterfaceLinks.prototype = {
  linkRoutes: null,
  gateways: null,
  interfaceName: null,
  extraRoutes: null,

  setLinks: function(linkRoutes, gateways, interfaceName) {
    this.linkRoutes = linkRoutes;
    this.gateways = gateways;
    this.interfaceName = interfaceName;
  },

  resetLinks: function() {
    this.linkRoutes = [];
    this.gateways = [];
    this.interfaceName = "";
    this.extraRoutes = [];
  },

  compareGateways: function(gateways) {
    if (this.gateways.length != gateways.length) {
      return false;
    }

    for (let i = 0; i < this.gateways.length; i++) {
      if (this.gateways[i] != gateways[i]) {
        return false;
      }
    }

    return true;
  }
};

/**
 * This component watches for network interfaces changing state and then
 * adjusts routes etc. accordingly.
 */
function NetworkManager() {
  this.networkInterfaces = {};
  this.networkInterfaceLinks = {};

  try {
    this._manageOfflineStatus =
      Services.prefs.getBoolPref(PREF_MANAGE_OFFLINE_STATUS);
  } catch(ex) {
    // Ignore.
  }
  Services.prefs.addObserver(PREF_MANAGE_OFFLINE_STATUS, this, false);
  Services.prefs.addObserver(PREF_NETWORK_DEBUG_ENABLED, this, false);
  Services.obs.addObserver(this, TOPIC_XPCOM_SHUTDOWN, false);

  this.setAndConfigureActive();

  ppmm.addMessageListener('NetworkInterfaceList:ListInterface', this);

  // Used in resolveHostname().
  defineLazyRegExp(this, "REGEXP_IPV4", "^\\d{1,3}(?:\\.\\d{1,3}){3}$");
  defineLazyRegExp(this, "REGEXP_IPV6", "^[\\da-fA-F]{4}(?::[\\da-fA-F]{4}){7}$");
}
NetworkManager.prototype = {
  classID:   NETWORKMANAGER_CID,
  classInfo: XPCOMUtils.generateCI({classID: NETWORKMANAGER_CID,
                                    contractID: NETWORKMANAGER_CONTRACTID,
                                    classDescription: "Network Manager",
                                    interfaces: [Ci.nsINetworkManager]}),
  QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkManager,
                                         Ci.nsISupportsWeakReference,
                                         Ci.nsIObserver,
                                         Ci.nsISettingsServiceCallback]),

  // nsIObserver

  observe: function(subject, topic, data) {
    switch (topic) {
      case TOPIC_PREF_CHANGED:
        if (data === PREF_NETWORK_DEBUG_ENABLED) {
          updateDebug();
        } else if (data === PREF_MANAGE_OFFLINE_STATUS) {
          this._manageOfflineStatus =
            Services.prefs.getBoolPref(PREF_MANAGE_OFFLINE_STATUS);
          debug(PREF_MANAGE_OFFLINE_STATUS + " has changed to " + this._manageOfflineStatus);
        }
        break;
      case TOPIC_XPCOM_SHUTDOWN:
        Services.obs.removeObserver(this, TOPIC_XPCOM_SHUTDOWN);
        Services.prefs.removeObserver(PREF_MANAGE_OFFLINE_STATUS, this);
        Services.prefs.removeObserver(PREF_NETWORK_DEBUG_ENABLED, this);
        break;
    }
  },

  receiveMessage: function(aMsg) {
    switch (aMsg.name) {
      case "NetworkInterfaceList:ListInterface": {
        let excludeMms = aMsg.json.excludeMms;
        let excludeSupl = aMsg.json.excludeSupl;
        let excludeIms = aMsg.json.excludeIms;
        let excludeDun = aMsg.json.excludeDun;
        let excludeFota = aMsg.json.excludeFota;
        let interfaces = [];

        for (let key in this.networkInterfaces) {
          let network = this.networkInterfaces[key];
          let i = network.info;
          if ((i.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_MMS && excludeMms) ||
              (i.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_SUPL && excludeSupl) ||
              (i.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_IMS && excludeIms) ||
              (i.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN && excludeDun) ||
              (i.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_FOTA && excludeFota)) {
            continue;
          }

          let ips = {};
          let prefixLengths = {};
          i.getAddresses(ips, prefixLengths);

          interfaces.push({
            state: i.state,
            type: i.type,
            name: i.name,
            ips: ips.value,
            prefixLengths: prefixLengths.value,
            gateways: i.getGateways(),
            dnses: i.getDnses()
          });
        }
        return interfaces;
      }
    }
  },

  getNetworkId: function(aNetworkInfo) {
    let id = "device";
    try {
      if (aNetworkInfo instanceof Ci.nsIRilNetworkInfo) {
        let rilInfo = aNetworkInfo.QueryInterface(Ci.nsIRilNetworkInfo);
        id = "ril" + rilInfo.serviceId;
      }
    } catch (e) {}

    return id + "-" + aNetworkInfo.type;
  },

  // nsINetworkManager

  registerNetworkInterface: function(network) {
    if (!(network instanceof Ci.nsINetworkInterface)) {
      throw Components.Exception("Argument must be nsINetworkInterface.",
                                 Cr.NS_ERROR_INVALID_ARG);
    }
    let networkId = this.getNetworkId(network.info);
    if (networkId in this.networkInterfaces) {
      throw Components.Exception("Network with that type already registered!",
                                 Cr.NS_ERROR_INVALID_ARG);
    }
    this.networkInterfaces[networkId] = network;
    this.networkInterfaceLinks[networkId] = new NetworkInterfaceLinks();

    Services.obs.notifyObservers(network.info, TOPIC_INTERFACE_REGISTERED, null);
    debug("Network '" + networkId + "' registered.");
  },

  _addSubnetRoutes: function(network) {
    let ips = {};
    let prefixLengths = {};
    let length = network.getAddresses(ips, prefixLengths);
    let promises = [];

    for (let i = 0; i < length; i++) {
      debug('Adding subnet routes: ' + ips.value[i] + '/' + prefixLengths.value[i]);
      promises.push(
        gNetworkService.modifyRoute(Ci.nsINetworkService.MODIFY_ROUTE_ADD,
                                    network.name, ips.value[i], prefixLengths.value[i])
        .catch(aError => {
          debug("_addSubnetRoutes error: " + aError);
        }));
    }

    return Promise.all(promises);
  },

  updateNetworkInterface: function(network) {
    if (!(network instanceof Ci.nsINetworkInterface)) {
      throw Components.Exception("Argument must be nsINetworkInterface.",
                                 Cr.NS_ERROR_INVALID_ARG);
    }
    let networkId = this.getNetworkId(network.info);
    if (!(networkId in this.networkInterfaces)) {
      throw Components.Exception("No network with that type registered.",
                                 Cr.NS_ERROR_INVALID_ARG);
    }
    debug("Network " + network.info.type + "/" + network.info.name +
          " changed state to " + network.info.state);

    // Keep a copy of network in case it is modified while we are updating.
    let extNetworkInfo = new ExtraNetworkInfo(network);

    // Note that since Lollipop we need to allocate and initialize
    // something through netd, so we add createNetwork/destroyNetwork
    // to deal with that explicitly.

    switch (extNetworkInfo.state) {
      case Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED:

        this._createNetwork(extNetworkInfo.name)
          // Remove pre-created default route and let setAndConfigureActive()
          // to set default route only on preferred network
          .then(() => this._removeDefaultRoute(extNetworkInfo))
          // Set DNS server as early as possible to prevent from
          // premature domain name lookup.
          .then(() => this._setDNS(extNetworkInfo))
          .then(() => {
            // Add host route for data calls
            if (!this.isNetworkTypeMobile(extNetworkInfo.type)) {
              return;
            }

            let currentInterfaceLinks = this.networkInterfaceLinks[networkId];
            let newLinkRoutes = extNetworkInfo.getDnses().concat(
              extNetworkInfo.httpProxyHost);
            // If gateways have changed, remove all old routes first.
            return this._handleGateways(networkId, extNetworkInfo.getGateways())
              .then(() => this._updateRoutes(currentInterfaceLinks.linkRoutes,
                                             newLinkRoutes,
                                             extNetworkInfo.getGateways(),
                                             extNetworkInfo.name))
              .then(() => currentInterfaceLinks.setLinks(newLinkRoutes,
                                                         extNetworkInfo.getGateways(),
                                                         extNetworkInfo.name));
          })
          .then(() => {
            if (extNetworkInfo.type !=
                Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN) {
              return;
            }
            // Dun type is a special case where we add the default route to a
            // secondary table.
            return this.setSecondaryDefaultRoute(extNetworkInfo);
          })
          .then(() => this._addSubnetRoutes(extNetworkInfo))
          .then(() => {
            if (extNetworkInfo.mtu <= 0) {
              return;
            }

            return this._setMtu(extNetworkInfo);
          })
          .then(() => this.setAndConfigureActive())
          .then(() => {
            // Update data connection when Wifi connected/disconnected
            if (extNetworkInfo.type ==
                Ci.nsINetworkInfo.NETWORK_TYPE_WIFI && this.mRil) {
              for (let i = 0; i < this.mRil.numRadioInterfaces; i++) {
                this.mRil.getRadioInterface(i).updateRILNetworkInterface();
              }
            }

            // Probing the public network accessibility after routing table is ready
            CaptivePortalDetectionHelper
              .notify(CaptivePortalDetectionHelper.EVENT_CONNECT,
                      this.activeNetworkInfo);
          })
          .then(() => {
            // Notify outer modules like MmsService to start the transaction after
            // the configuration of the network interface is done.
            Services.obs.notifyObservers(network.info,
                                         TOPIC_CONNECTION_STATE_CHANGED,
                                         this.convertConnectionType(network.info));
          })
          .catch(aError => {
            debug("updateNetworkInterface error: " + aError);
          });
        break;
      case Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED:
        Promise.resolve()
          .then(() => {
            if (!this.isNetworkTypeMobile(extNetworkInfo.type)) {
              return;
            }
            // Remove host route for data calls
            return this._cleanupAllHostRoutes(networkId);
          })
          .then(() => {
            if (extNetworkInfo.type !=
                Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN) {
              return;
            }
            // Remove secondary default route for dun.
            return this.removeSecondaryDefaultRoute(extNetworkInfo);
          })
          .then(() => {
            if (extNetworkInfo.type == Ci.nsINetworkInfo.NETWORK_TYPE_WIFI ||
                extNetworkInfo.type == Ci.nsINetworkInfo.NETWORK_TYPE_ETHERNET) {
              // Remove routing table in /proc/net/route
              return this._resetRoutingTable(extNetworkInfo.name);
            }
            if (extNetworkInfo.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE) {
              return this._removeDefaultRoute(extNetworkInfo)
            }
          })
          .then(() => {
            // Clear http proxy on active network.
            if (this.activeNetworkInfo &&
                extNetworkInfo.type == this.activeNetworkInfo.type) {
              this.clearNetworkProxy();
            }

            // Abort ongoing captive portal detection on the wifi interface
            CaptivePortalDetectionHelper
              .notify(CaptivePortalDetectionHelper.EVENT_DISCONNECT, extNetworkInfo);
          })
          .then(() => this.setAndConfigureActive())
          .then(() => {
            // Update data connection when Wifi connected/disconnected
            if (extNetworkInfo.type ==
                Ci.nsINetworkInfo.NETWORK_TYPE_WIFI && this.mRil) {
              for (let i = 0; i < this.mRil.numRadioInterfaces; i++) {
                this.mRil.getRadioInterface(i).updateRILNetworkInterface();
              }
            }
          })
          .then(() => this._destroyNetwork(extNetworkInfo.name))
          .then(() => {
            // Notify outer modules like MmsService to start the transaction after
            // the configuration of the network interface is done.
            Services.obs.notifyObservers(network.info,
                                         TOPIC_CONNECTION_STATE_CHANGED,
                                         this.convertConnectionType(network.info));
          })
          .catch(aError => {
            debug("updateNetworkInterface error: " + aError);
          });
        break;
    }
  },

  unregisterNetworkInterface: function(network) {
    if (!(network instanceof Ci.nsINetworkInterface)) {
      throw Components.Exception("Argument must be nsINetworkInterface.",
                                 Cr.NS_ERROR_INVALID_ARG);
    }
    let networkId = this.getNetworkId(network.info);
    if (!(networkId in this.networkInterfaces)) {
      throw Components.Exception("No network with that type registered.",
                                 Cr.NS_ERROR_INVALID_ARG);
    }

    // This is for in case a network gets unregistered without being
    // DISCONNECTED.
    if (this.isNetworkTypeMobile(network.info.type)) {
      this._cleanupAllHostRoutes(networkId);
    }

    delete this.networkInterfaces[networkId];

    Services.obs.notifyObservers(network.info, TOPIC_INTERFACE_UNREGISTERED, null);
    debug("Network '" + networkId + "' unregistered.");
  },

  _manageOfflineStatus: true,

  networkInterfaces: null,

  networkInterfaceLinks: null,

  _networkTypePriorityList: [Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET,
                             Ci.nsINetworkInterface.NETWORK_TYPE_WIFI,
                             Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE],
  get networkTypePriorityList() {
    return this._networkTypePriorityList;
  },
  set networkTypePriorityList(val) {
    if (val.length != this._networkTypePriorityList.length) {
      throw "Priority list length should equal to " +
            this._networkTypePriorityList.length;
    }

    // Check if types in new priority list are valid and also make sure there
    // are no duplicate types.
    let list = [Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET,
                Ci.nsINetworkInterface.NETWORK_TYPE_WIFI,
                Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE];
    while (list.length) {
      let type = list.shift();
      if (val.indexOf(type) == -1) {
        throw "There is missing network type";
      }
    }

    this._networkTypePriorityList = val;
  },

  getPriority: function(type) {
    if (this._networkTypePriorityList.indexOf(type) == -1) {
      // 0 indicates the lowest priority.
      return 0;
    }

    return this._networkTypePriorityList.length -
           this._networkTypePriorityList.indexOf(type);
  },

  get allNetworkInfo() {
    let allNetworkInfo = {};

    for (let networkId in this.networkInterfaces) {
      if (this.networkInterfaces.hasOwnProperty(networkId)) {
        allNetworkInfo[networkId] = this.networkInterfaces[networkId].info;
      }
    }

    return allNetworkInfo;
  },

  _preferredNetworkType: DEFAULT_PREFERRED_NETWORK_TYPE,
  get preferredNetworkType() {
    return this._preferredNetworkType;
  },
  set preferredNetworkType(val) {
    if ([Ci.nsINetworkInterface.NETWORK_TYPE_WIFI,
         Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE,
         Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET].indexOf(val) == -1) {
      throw "Invalid network type";
    }
    this._preferredNetworkType = val;
  },

  _activeNetwork: null,

  get activeNetworkInfo() {
    return this._activeNetwork && this._activeNetwork.info;
  },

  _overriddenActive: null,

  overrideActive: function(network) {
    if ([Ci.nsINetworkInterface.NETWORK_TYPE_WIFI,
         Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE,
         Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET].indexOf(val) == -1) {
      throw "Invalid network type";
    }

    this._overriddenActive = network;
    this.setAndConfigureActive();
  },

  _updateRoutes: function(oldLinks, newLinks, gateways, interfaceName) {
    // Returns items that are in base but not in target.
    function getDifference(base, target) {
      return base.filter(function(i) { return target.indexOf(i) < 0; });
    }

    let addedLinks = getDifference(newLinks, oldLinks);
    let removedLinks = getDifference(oldLinks, newLinks);

    if (addedLinks.length === 0 && removedLinks.length === 0) {
      return Promise.resolve();
    }

    return this._setHostRoutes(false, removedLinks, interfaceName, gateways)
      .then(this._setHostRoutes(true, addedLinks, interfaceName, gateways));
  },

  _setHostRoutes: function(doAdd, ipAddresses, networkName, gateways) {
    let getMaxPrefixLength = (aIp) => {
      return aIp.match(this.REGEXP_IPV4) ? IPV4_MAX_PREFIX_LENGTH : IPV6_MAX_PREFIX_LENGTH;
    }

    let promises = [];

    ipAddresses.forEach((aIpAddress) => {
      let gateway = this.selectGateway(gateways, aIpAddress);
      if (gateway) {
        promises.push((doAdd)
          ? gNetworkService.modifyRoute(Ci.nsINetworkService.MODIFY_ROUTE_ADD,
                                        networkName, aIpAddress,
                                        getMaxPrefixLength(aIpAddress), gateway)
          : gNetworkService.modifyRoute(Ci.nsINetworkService.MODIFY_ROUTE_REMOVE,
                                        networkName, aIpAddress,
                                        getMaxPrefixLength(aIpAddress), gateway));
      }
    });

    return Promise.all(promises);
  },

  isValidatedNetwork: function(aNetworkInfo) {
    let isValid = false;
    try {
      isValid = (this.getNetworkId(aNetworkInfo) in this.networkInterfaces);
    } catch (e) {
      debug("Invalid network interface: " + e);
    }

    return isValid;
  },

  addHostRoute: function(aNetworkInfo, aHost) {
    if (!this.isValidatedNetwork(aNetworkInfo)) {
      return Promise.reject("Invalid network info.");
    }

    return this.resolveHostname(aNetworkInfo, aHost)
      .then((ipAddresses) => {
        let promises = [];
        let networkId = this.getNetworkId(aNetworkInfo);

        ipAddresses.forEach((aIpAddress) => {
          let promise =
            this._setHostRoutes(true, [aIpAddress], aNetworkInfo.name, aNetworkInfo.getGateways())
              .then(() => this.networkInterfaceLinks[networkId].extraRoutes.push(aIpAddress));

          promises.push(promise);
        });

        return Promise.all(promises);
      });
  },

  removeHostRoute: function(aNetworkInfo, aHost) {
    if (!this.isValidatedNetwork(aNetworkInfo)) {
      return Promise.reject("Invalid network info.");
    }

    return this.resolveHostname(aNetworkInfo, aHost)
      .then((ipAddresses) => {
        let promises = [];
        let networkId = this.getNetworkId(aNetworkInfo);

        ipAddresses.forEach((aIpAddress) => {
          let found = this.networkInterfaceLinks[networkId].extraRoutes.indexOf(aIpAddress);
          if (found < 0) {
            return; // continue
          }

          let promise =
            this._setHostRoutes(false, [aIpAddress], aNetworkInfo.name, aNetworkInfo.getGateways())
              .then(() => {
                this.networkInterfaceLinks[networkId].extraRoutes.splice(found, 1);
              }, () => {
                // We should remove it even if the operation failed.
                this.networkInterfaceLinks[networkId].extraRoutes.splice(found, 1);
              });
          promises.push(promise);
        });

        return Promise.all(promises);
      });
  },

  isNetworkTypeSecondaryMobile: function(type) {
    return (type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_MMS ||
            type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_SUPL ||
            type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_IMS ||
            type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN ||
            type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_FOTA);
  },

  isNetworkTypeMobile: function(type) {
    return (type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE ||
            this.isNetworkTypeSecondaryMobile(type));
  },

  _handleGateways: function(networkId, gateways) {
    let currentNetworkLinks = this.networkInterfaceLinks[networkId];
    if (currentNetworkLinks.gateways.length == 0 ||
        currentNetworkLinks.compareGateways(gateways)) {
      return Promise.resolve();
    }

    let currentExtraRoutes = currentNetworkLinks.extraRoutes;
    return this._cleanupAllHostRoutes(networkId)
      .then(() => {
        // If gateways have changed, re-add extra host routes with new gateways.
        if (currentExtraRoutes.length > 0) {
          this._setHostRoutes(true,
                              currentExtraRoutes,
                              currentNetworkLinks.interfaceName,
                              gateways)
          .then(() => {
            currentNetworkLinks.extraRoutes = currentExtraRoutes;
          });
        }
      });
  },

  _cleanupAllHostRoutes: function(networkId) {
    let currentNetworkLinks = this.networkInterfaceLinks[networkId];
    let hostRoutes = currentNetworkLinks.linkRoutes.concat(
      currentNetworkLinks.extraRoutes);

    if (hostRoutes.length === 0) {
      return Promise.resolve();
    }

    return this._setHostRoutes(false,
                               hostRoutes,
                               currentNetworkLinks.interfaceName,
                               currentNetworkLinks.gateways)
      .catch((aError) => {
        debug("Error (" + aError + ") on _cleanupAllHostRoutes, keep proceeding.");
      })
      .then(() => currentNetworkLinks.resetLinks());
  },

  selectGateway: function(gateways, host) {
    for (let i = 0; i < gateways.length; i++) {
      let gateway = gateways[i];
      if (gateway.match(this.REGEXP_IPV4) && host.match(this.REGEXP_IPV4) ||
          gateway.indexOf(":") != -1 && host.indexOf(":") != -1) {
        return gateway;
      }
    }
    return null;
  },

  _setSecondaryRoute: function(aDoAdd, aInterfaceName, aRoute) {
    return new Promise((aResolve, aReject) => {
      if (aDoAdd) {
        gNetworkService.addSecondaryRoute(aInterfaceName, aRoute,
          (aSuccess) => {
            if (!aSuccess) {
              aReject("addSecondaryRoute failed");
              return;
            }
            aResolve();
        });
      } else {
        gNetworkService.removeSecondaryRoute(aInterfaceName, aRoute,
          (aSuccess) => {
            if (!aSuccess) {
              debug("removeSecondaryRoute failed")
            }
            // Always resolve.
            aResolve();
        });
      }
    });
  },

  setSecondaryDefaultRoute: function(network) {
    let gateways = network.getGateways();
    let promises = [];

    for (let i = 0; i < gateways.length; i++) {
      let isIPv6 = (gateways[i].indexOf(":") != -1) ? true : false;
      // First, we need to add a host route to the gateway in the secondary
      // routing table to make the gateway reachable. Host route takes the max
      // prefix and gateway address 'any'.
      let hostRoute = {
        ip: gateways[i],
        prefix: isIPv6 ? IPV6_MAX_PREFIX_LENGTH : IPV4_MAX_PREFIX_LENGTH,
        gateway: isIPv6 ? IPV6_ADDRESS_ANY : IPV4_ADDRESS_ANY
      };
      // Now we can add the default route through gateway. Default route takes the
      // min prefix and destination ip 'any'.
      let defaultRoute = {
        ip: isIPv6 ? IPV6_ADDRESS_ANY : IPV4_ADDRESS_ANY,
        prefix: 0,
        gateway: gateways[i]
      };

      let promise = this._setSecondaryRoute(true, network.name, hostRoute)
        .then(() => this._setSecondaryRoute(true, network.name, defaultRoute));

      promises.push(promise);
    }

    return Promise.all(promises);
  },

  removeSecondaryDefaultRoute: function(network) {
    let gateways = network.getGateways();
    let promises = [];

    for (let i = 0; i < gateways.length; i++) {
      let isIPv6 = (gateways[i].indexOf(":") != -1) ? true : false;
      // Remove both default route and host route.
      let defaultRoute = {
        ip: isIPv6 ? IPV6_ADDRESS_ANY : IPV4_ADDRESS_ANY,
        prefix: 0,
        gateway: gateways[i]
      };
      let hostRoute = {
        ip: gateways[i],
        prefix: isIPv6 ? IPV6_MAX_PREFIX_LENGTH : IPV4_MAX_PREFIX_LENGTH,
        gateway: isIPv6 ? IPV6_ADDRESS_ANY : IPV4_ADDRESS_ANY
      };

      let promise = this._setSecondaryRoute(false, network.name, defaultRoute)
        .then(() => this._setSecondaryRoute(false, network.name, hostRoute));

      promises.push(promise);
    }

    return Promise.all(promises);
  },

  /**
   * Determine the active interface and configure it.
   */
  setAndConfigureActive: function() {
    debug("Evaluating whether active network needs to be changed.");
    let oldActive = this._activeNetwork;

    if (this._overriddenActive) {
      debug("We have an override for the active network: " +
            this._overriddenActive.info.name);
      // The override was just set, so reconfigure the network.
      if (this._activeNetwork != this._overriddenActive) {
        this._activeNetwork = this._overriddenActive;
        this._setDefaultRouteAndProxy(this._activeNetwork, oldActive);
        Services.obs.notifyObservers(this.activeNetworkInfo,
                                     TOPIC_ACTIVE_CHANGED, null);
      }
      return;
    }

    // The active network is already our preferred type.
    if (this.activeNetworkInfo &&
        this.activeNetworkInfo.state == Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED &&
        this.activeNetworkInfo.type == this._preferredNetworkType) {
      debug("Active network is already our preferred type.");
      return this._setDefaultRouteAndProxy(this._activeNetwork, oldActive);
    }

    // Find a suitable network interface to activate.
    this._activeNetwork = null;
    let anyConnected = false;

    for (let key in this.networkInterfaces) {
      let network = this.networkInterfaces[key];
      if (network.info.state != Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED) {
        continue;
      }
      anyConnected = true;

      // Set active only for default connections.
      if (network.info.type != Ci.nsINetworkInfo.NETWORK_TYPE_WIFI &&
          network.info.type != Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE &&
          network.info.type != Ci.nsINetworkInfo.NETWORK_TYPE_ETHERNET) {
        continue;
      }

      if (network.info.type == this.preferredNetworkType) {
        this._activeNetwork = network;
        debug("Found our preferred type of network: " + network.info.name);
        break;
      }

      // Initialize the active network with the first connected network.
      if (!this._activeNetwork) {
        this._activeNetwork = network;
        continue;
      }

      // Compare the prioriy between two network types. If found incoming
      // network with higher priority, replace the active network.
      if (this.getPriority(this._activeNetwork.type) < this.getPriority(network.type)) {
        this._activeNetwork = network;
      }
    }

    return Promise.resolve()
      .then(() => {
        if (!this._activeNetwork) {
          return Promise.resolve();
        }

        return this._setDefaultRouteAndProxy(this._activeNetwork, oldActive);
      })
      .then(() => {
        if (this._activeNetwork != oldActive) {
          Services.obs.notifyObservers(this.activeNetworkInfo,
                                       TOPIC_ACTIVE_CHANGED, null);
        }

        if (this._manageOfflineStatus) {
          Services.io.offline = !anyConnected &&
                                (gTetheringService.state ===
                                 Ci.nsITetheringService.TETHERING_STATE_INACTIVE);
        }
      });
  },

  resolveHostname: function(aNetworkInfo, aHostname) {
    // Sanity check for null, undefined and empty string... etc.
    if (!aHostname) {
      return Promise.reject(new Error("hostname is empty: " + aHostname));
    }

    if (aHostname.match(this.REGEXP_IPV4) ||
        aHostname.match(this.REGEXP_IPV6)) {
      return Promise.resolve([aHostname]);
    }

    // Wrap gDNSService.asyncResolveExtended to a promise, which
    // resolves with an array of ip addresses or rejects with
    // the reason otherwise.
    let hostResolveWrapper = aNetId => {
      return new Promise((aResolve, aReject) => {
        // Callback for gDNSService.asyncResolveExtended.
        let onLookupComplete = (aRequest, aRecord, aStatus) => {
          if (!Components.isSuccessCode(aStatus)) {
            aReject(new Error("Failed to resolve '" + aHostname +
                              "', with status: " + aStatus));
            return;
          }

          let retval = [];
          while (aRecord.hasMore()) {
            retval.push(aRecord.getNextAddrAsString());
          }

          if (!retval.length) {
            aReject(new Error("No valid address after DNS lookup!"));
            return;
          }

          debug("hostname is resolved: " + aHostname);
          debug("Addresses: " + JSON.stringify(retval));

          aResolve(retval);
        };

        debug('Calling gDNSService.asyncResolveExtended: ' + aNetId + ', ' + aHostname);
        gDNSService.asyncResolveExtended(aHostname,
                                         0,
                                         aNetId,
                                         onLookupComplete,
                                         Services.tm.mainThread);
      });
    };

    // TODO: |getNetId| will be implemented as a sync call in nsINetworkManager
    //       once Bug 1141903 is landed.
    return gNetworkService.getNetId(aNetworkInfo.name)
      .then(aNetId => hostResolveWrapper(aNetId));
  },

  convertConnectionType: function(aNetworkInfo) {
    // If there is internal interface change (e.g., MOBILE_MMS, MOBILE_SUPL),
    // the function will return null so that it won't trigger type change event
    // in NetworkInformation API.
    if (aNetworkInfo.type != Ci.nsINetworkInterface.NETWORK_TYPE_WIFI &&
        aNetworkInfo.type != Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE &&
        aNetworkInfo.type != Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET) {
      return null;
    }

    if (aNetworkInfo.state == Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED) {
      return CONNECTION_TYPE_NONE;
    }

    switch (aNetworkInfo.type) {
      case Ci.nsINetworkInfo.NETWORK_TYPE_WIFI:
        return CONNECTION_TYPE_WIFI;
      case Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE:
        return CONNECTION_TYPE_CELLULAR;
      case Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET:
        return CONNECTION_TYPE_ETHERNET;
    }
  },

  _setDNS: function(aNetworkInfo) {
    return new Promise((aResolve, aReject) => {
      let dnses = aNetworkInfo.getDnses();
      let gateways = aNetworkInfo.getGateways();
      gNetworkService.setDNS(aNetworkInfo.name, dnses.length, dnses,
                             gateways.length, gateways, (aError) => {
        if (aError) {
          aReject("setDNS failed");
          return;
        }
        aResolve();
      });
    });
  },

  _setMtu: function(aNetworkInfo) {
    return new Promise((aResolve, aReject) => {
      gNetworkService.setMtu(aNetworkInfo.name, aNetworkInfo.mtu, (aSuccess) => {
        if (!aSuccess) {
          debug("setMtu failed");
        }
        // Always resolve.
        aResolve();
      });
    });
  },

  _createNetwork: function(aInterfaceName) {
    return new Promise((aResolve, aReject) => {
      gNetworkService.createNetwork(aInterfaceName, (aSuccess) => {
        if (!aSuccess) {
          aReject("createNetwork failed");
          return;
        }
        aResolve();
      });
    });
  },

  _destroyNetwork: function(aInterfaceName) {
    return new Promise((aResolve, aReject) => {
      gNetworkService.destroyNetwork(aInterfaceName, (aSuccess) => {
        if (!aSuccess) {
          debug("destroyNetwork failed")
        }
        // Always resolve.
        aResolve();
      });
    });
  },

  _resetRoutingTable: function(aInterfaceName) {
    return new Promise((aResolve, aReject) => {
      gNetworkService.resetRoutingTable(aInterfaceName, (aSuccess) => {
        if (!aSuccess) {
          debug("resetRoutingTable failed");
        }
        // Always resolve.
        aResolve();
      });
    });
  },

  _removeDefaultRoute: function(aNetworkInfo) {
    return new Promise((aResolve, aReject) => {
      let gateways = aNetworkInfo.getGateways();
      gNetworkService.removeDefaultRoute(aNetworkInfo.name, gateways.length,
                                         gateways, (aSuccess) => {
        if (!aSuccess) {
          debug("removeDefaultRoute failed");
        }
        // Always resolve.
        aResolve();
      });
    });
  },

  _setDefaultRouteAndProxy: function(aNetwork, aOldNetwork) {
    if (aOldNetwork) {
      return this._removeDefaultRoute(aOldNetwork.info)
        .then(() => this._setDefaultRouteAndProxy(aNetwork, null));
    }

    return new Promise((aResolve, aReject) => {
      let networkInfo = aNetwork.info;
      let gateways = networkInfo.getGateways();

      gNetworkService.setDefaultRoute(networkInfo.name, gateways.length, gateways,
                                      (aSuccess) => {
        if (!aSuccess) {
          gNetworkService.destroyNetwork(networkInfo.name, function() {
            aReject("setDefaultRoute failed");
          });
          return;
        }
        this.setNetworkProxy(aNetwork);
        aResolve();
      });
    });
  },

  setNetworkProxy: function(aNetwork) {
    try {
      if (!aNetwork.httpProxyHost || aNetwork.httpProxyHost === "") {
        // Sets direct connection to internet.
        this.clearNetworkProxy();

        debug("No proxy support for " + aNetwork.info.name + " network interface.");
        return;
      }

      debug("Going to set proxy settings for " + aNetwork.info.name + " network interface.");
      // Sets manual proxy configuration.
      Services.prefs.setIntPref("network.proxy.type", MANUAL_PROXY_CONFIGURATION);

      // Do not use this proxy server for all protocols.
      Services.prefs.setBoolPref("network.proxy.share_proxy_settings", false);
      Services.prefs.setCharPref("network.proxy.http", aNetwork.httpProxyHost);
      Services.prefs.setCharPref("network.proxy.ssl", aNetwork.httpProxyHost);
      let port = aNetwork.httpProxyPort === 0 ? 8080 : aNetwork.httpProxyPort;
      Services.prefs.setIntPref("network.proxy.http_port", port);
      Services.prefs.setIntPref("network.proxy.ssl_port", port);
    } catch(ex) {
        debug("Exception " + ex + ". Unable to set proxy setting for " +
              aNetwork.info.name + " network interface.");
    }
  },

  clearNetworkProxy: function() {
    debug("Going to clear all network proxy.");

    Services.prefs.clearUserPref("network.proxy.type");
    Services.prefs.clearUserPref("network.proxy.share_proxy_settings");
    Services.prefs.clearUserPref("network.proxy.http");
    Services.prefs.clearUserPref("network.proxy.http_port");
    Services.prefs.clearUserPref("network.proxy.ssl");
    Services.prefs.clearUserPref("network.proxy.ssl_port");
  },
};

var CaptivePortalDetectionHelper = (function() {

  const EVENT_CONNECT = "Connect";
  const EVENT_DISCONNECT = "Disconnect";
  let _ongoingInterface = null;
  let _available = ("nsICaptivePortalDetector" in Ci);
  let getService = function() {
    return Cc['@mozilla.org/toolkit/captive-detector;1']
             .getService(Ci.nsICaptivePortalDetector);
  };

  let _performDetection = function(interfaceName, callback) {
    let capService = getService();
    let capCallback = {
      QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
      prepare: function() {
        capService.finishPreparation(interfaceName);
      },
      complete: function(success) {
        _ongoingInterface = null;
        callback(success);
      }
    };

    // Abort any unfinished captive portal detection.
    if (_ongoingInterface != null) {
      capService.abort(_ongoingInterface);
      _ongoingInterface = null;
    }
    try {
      capService.checkCaptivePortal(interfaceName, capCallback);
      _ongoingInterface = interfaceName;
    } catch (e) {
      debug('Fail to detect captive portal due to: ' + e.message);
    }
  };

  let _abort = function(interfaceName) {
    if (_ongoingInterface !== interfaceName) {
      return;
    }

    let capService = getService();
    capService.abort(_ongoingInterface);
    _ongoingInterface = null;
  };

  return {
    EVENT_CONNECT: EVENT_CONNECT,
    EVENT_DISCONNECT: EVENT_DISCONNECT,
    notify: function(eventType, network) {
      switch (eventType) {
        case EVENT_CONNECT:
          // perform captive portal detection on wifi interface
          if (_available && network &&
              network.type == Ci.nsINetworkInfo.NETWORK_TYPE_WIFI) {
            _performDetection(network.name, function() {
              // TODO: bug 837600
              // We can disconnect wifi in here if user abort the login procedure.
            });
          }

          break;
        case EVENT_DISCONNECT:
          if (_available &&
              network.type == Ci.nsINetworkInfo.NETWORK_TYPE_WIFI) {
            _abort(network.name);
          }
          break;
      }
    }
  };
}());

XPCOMUtils.defineLazyGetter(NetworkManager.prototype, "mRil", function() {
  try {
    return Cc["@mozilla.org/ril;1"].getService(Ci.nsIRadioInterfaceLayer);
  } catch (e) {}

  return null;
});

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkManager]);
