diff options
Diffstat (limited to 'dom/system/gonk/NetworkManager.js')
-rw-r--r-- | dom/system/gonk/NetworkManager.js | 1219 |
1 files changed, 1219 insertions, 0 deletions
diff --git a/dom/system/gonk/NetworkManager.js b/dom/system/gonk/NetworkManager.js new file mode 100644 index 000000000..9d7a5683e --- /dev/null +++ b/dom/system/gonk/NetworkManager.js @@ -0,0 +1,1219 @@ +/* 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]); |