/* 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]);