gecko-dev/dom/system/gonk/NetworkService.js

717 lines
21 KiB
JavaScript
Raw Normal View History

/* 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/NetUtil.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
const NETWORKSERVICE_CONTRACTID = "@mozilla.org/network/service;1";
const NETWORKSERVICE_CID = Components.ID("{baec696c-c78d-42db-8b44-603f8fbfafb4}");
XPCOMUtils.defineLazyServiceGetter(this, "gNetworkWorker",
"@mozilla.org/network/worker;1",
"nsINetworkWorker");
// 1xx - Requested action is proceeding
const NETD_COMMAND_PROCEEDING = 100;
// 2xx - Requested action has been successfully completed
const NETD_COMMAND_OKAY = 200;
// 4xx - The command is accepted but the requested action didn't
// take place.
const NETD_COMMAND_FAIL = 400;
// 5xx - The command syntax or parameters error
const NETD_COMMAND_ERROR = 500;
// 6xx - Unsolicited broadcasts
const NETD_COMMAND_UNSOLICITED = 600;
const WIFI_CTRL_INTERFACE = "wl0.1";
const MANUAL_PROXY_CONFIGURATION = 1;
let DEBUG = false;
// Read debug setting from pref.
try {
let debugPref = Services.prefs.getBoolPref("network.debugging.enabled");
DEBUG = DEBUG || debugPref;
} catch (e) {}
function netdResponseType(code) {
return Math.floor(code / 100) * 100;
}
function isError(code) {
let type = netdResponseType(code);
return (type !== NETD_COMMAND_PROCEEDING && type !== NETD_COMMAND_OKAY);
}
function debug(msg) {
dump("-*- NetworkService: " + msg + "\n");
}
function Task(id, params, setupFunction) {
this.id = id;
this.params = params;
this.setupFunction = setupFunction;
}
function NetworkWorkerRequestQueue(networkService) {
this.networkService = networkService;
this.tasks = [];
}
NetworkWorkerRequestQueue.prototype = {
runQueue: function() {
if (this.tasks.length === 0) {
return;
}
let task = this.tasks[0];
if (DEBUG) debug("run task id: " + task.id);
if (typeof task.setupFunction === 'function') {
// If setupFunction returns false, skip sending to Network Worker but call
// handleWorkerMessage() directly with task id, as if the response was
// returned from Network Worker.
if (!task.setupFunction()) {
this.networkService.handleWorkerMessage({id: task.id});
return;
}
}
gNetworkWorker.postMessage(task.params);
},
enqueue: function(id, params, setupFunction) {
if (DEBUG) debug("enqueue id: " + id);
this.tasks.push(new Task(id, params, setupFunction));
if (this.tasks.length === 1) {
this.runQueue();
}
},
dequeue: function(id) {
if (DEBUG) debug("dequeue id: " + id);
if (!this.tasks.length || this.tasks[0].id != id) {
if (DEBUG) debug("Id " + id + " is not on top of the queue");
return;
}
this.tasks.shift();
if (this.tasks.length > 0) {
// Run queue on the next tick.
Services.tm.currentThread.dispatch(() => {
this.runQueue();
}, Ci.nsIThread.DISPATCH_NORMAL);
}
}
};
/**
* This component watches for network interfaces changing state and then
* adjusts routes etc. accordingly.
*/
function NetworkService() {
if(DEBUG) debug("Starting net_worker.");
let self = this;
if (gNetworkWorker) {
let networkListener = {
onEvent: function(event) {
self.handleWorkerMessage(event);
}
};
gNetworkWorker.start(networkListener);
}
// Callbacks to invoke when a reply arrives from the net_worker.
this.controlCallbacks = Object.create(null);
this.addedRoutes = new Map();
this.netWorkerRequestQueue = new NetworkWorkerRequestQueue(this);
this.shutdown = false;
Services.obs.addObserver(this, "xpcom-shutdown", false);
}
NetworkService.prototype = {
classID: NETWORKSERVICE_CID,
classInfo: XPCOMUtils.generateCI({classID: NETWORKSERVICE_CID,
contractID: NETWORKSERVICE_CONTRACTID,
classDescription: "Network Service",
interfaces: [Ci.nsINetworkService]}),
QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkService]),
// Helpers
addedRoutes: null,
idgen: 0,
controlMessage: function(params, callback, setupFunction) {
if (this.shutdown) {
return;
}
let id = this.idgen++;
params.id = id;
if (callback) {
this.controlCallbacks[id] = callback;
}
// For now, we use setupFunction to determine if this command needs to be
// queued or not.
if (setupFunction) {
this.netWorkerRequestQueue.enqueue(id, params, setupFunction);
return;
}
if (gNetworkWorker) {
gNetworkWorker.postMessage(params);
}
},
handleWorkerMessage: function(response) {
if(DEBUG) debug("NetworkManager received message from worker: " + JSON.stringify(response));
let id = response.id;
if (response.broadcast === true) {
Services.obs.notifyObservers(null, response.topic, response.reason);
return;
}
let callback = this.controlCallbacks[id];
if (callback) {
callback.call(this, response);
delete this.controlCallbacks[id];
}
this.netWorkerRequestQueue.dequeue(id);
},
// nsINetworkService
getNetworkInterfaceStats: function(networkName, callback) {
if(DEBUG) debug("getNetworkInterfaceStats for " + networkName);
let file = new FileUtils.File("/proc/net/dev");
if (!file) {
callback.networkStatsAvailable(false, 0, 0, Date.now());
return;
}
NetUtil.asyncFetch2(file, function(inputStream, status) {
let rxBytes = 0,
txBytes = 0,
now = Date.now();
if (Components.isSuccessCode(status)) {
// Find record for corresponding interface.
let statExpr = /(\S+): +(\d+) +\d+ +\d+ +\d+ +\d+ +\d+ +\d+ +\d+ +(\d+) +\d+ +\d+ +\d+ +\d+ +\d+ +\d+ +\d+/;
let data =
NetUtil.readInputStreamToString(inputStream, inputStream.available())
.split("\n");
for (let i = 2; i < data.length; i++) {
let parseResult = statExpr.exec(data[i]);
if (parseResult && parseResult[1] === networkName) {
rxBytes = parseInt(parseResult[2], 10);
txBytes = parseInt(parseResult[3], 10);
break;
}
}
}
// netd always return success even interface doesn't exist.
callback.networkStatsAvailable(true, rxBytes, txBytes, now);
},
null, // aLoadingNode
Services.scriptSecurityManager.getSystemPrincipal(),
null, // aTriggeringPrincipal
Ci.nsILoadInfo.SEC_NORMAL,
Ci.nsIContentPolicy.TYPE_OTHER);
},
setNetworkInterfaceAlarm: function(networkName, threshold, callback) {
if (!networkName) {
callback.networkUsageAlarmResult(-1);
return;
}
let self = this;
this._disableNetworkInterfaceAlarm(networkName, function(result) {
if (threshold < 0) {
if (!isError(result.resultCode)) {
callback.networkUsageAlarmResult(null);
return;
}
callback.networkUsageAlarmResult(result.reason);
return
}
self._setNetworkInterfaceAlarm(networkName, threshold, callback);
});
},
_setNetworkInterfaceAlarm: function(networkName, threshold, callback) {
if(DEBUG) debug("setNetworkInterfaceAlarm for " + networkName + " at " + threshold + "bytes");
let params = {
cmd: "setNetworkInterfaceAlarm",
ifname: networkName,
threshold: threshold
};
params.report = true;
this.controlMessage(params, function(result) {
if (!isError(result.resultCode)) {
callback.networkUsageAlarmResult(null);
return;
}
this._enableNetworkInterfaceAlarm(networkName, threshold, callback);
});
},
_enableNetworkInterfaceAlarm: function(networkName, threshold, callback) {
if(DEBUG) debug("enableNetworkInterfaceAlarm for " + networkName + " at " + threshold + "bytes");
let params = {
cmd: "enableNetworkInterfaceAlarm",
ifname: networkName,
threshold: threshold
};
params.report = true;
this.controlMessage(params, function(result) {
if (!isError(result.resultCode)) {
callback.networkUsageAlarmResult(null);
return;
}
callback.networkUsageAlarmResult(result.reason);
});
},
_disableNetworkInterfaceAlarm: function(networkName, callback) {
if(DEBUG) debug("disableNetworkInterfaceAlarm for " + networkName);
let params = {
cmd: "disableNetworkInterfaceAlarm",
ifname: networkName,
};
params.report = true;
this.controlMessage(params, function(result) {
callback(result);
});
},
setWifiOperationMode: function(interfaceName, mode, callback) {
if(DEBUG) debug("setWifiOperationMode on " + interfaceName + " to " + mode);
let params = {
cmd: "setWifiOperationMode",
ifname: interfaceName,
mode: mode
};
params.report = true;
this.controlMessage(params, function(result) {
if (isError(result.resultCode)) {
callback.wifiOperationModeResult("netd command error");
} else {
callback.wifiOperationModeResult(null);
}
});
},
resetRoutingTable: function(network) {
let options = {
cmd: "removeNetworkRoute",
ifname: network.name
};
this.controlMessage(options);
},
setDNS: function(networkInterface, callback) {
if (DEBUG) debug("Going DNS to " + networkInterface.name);
let dnses = networkInterface.getDnses();
let options = {
cmd: "setDNS",
ifname: networkInterface.name,
domain: "mozilla." + networkInterface.name + ".doman",
dnses: dnses
};
this.controlMessage(options, function(result) {
callback.setDnsResult(result.success ? null : result.reason);
});
},
setDefaultRoute: function(network, oldInterface, callback) {
if (DEBUG) debug("Going to change default route to " + network.name);
let gateways = network.getGateways();
let options = {
cmd: "setDefaultRoute",
ifname: network.name,
oldIfname: (oldInterface && oldInterface !== network) ? oldInterface.name : null,
gateways: gateways
};
this.controlMessage(options, function(result) {
callback.nativeCommandResult(!result.error);
});
},
removeDefaultRoute: function(network) {
if(DEBUG) debug("Remove default route for " + network.name);
let gateways = network.getGateways();
let options = {
cmd: "removeDefaultRoute",
ifname: network.name,
gateways: gateways
};
this.controlMessage(options);
},
_routeToString: function(interfaceName, host, prefixLength, gateway) {
return host + "-" + prefixLength + "-" + gateway + "-" + interfaceName;
},
modifyRoute: function(action, interfaceName, host, prefixLength, gateway) {
let command;
switch (action) {
case Ci.nsINetworkService.MODIFY_ROUTE_ADD:
command = 'addHostRoute';
break;
case Ci.nsINetworkService.MODIFY_ROUTE_REMOVE:
command = 'removeHostRoute';
break;
default:
if (DEBUG) debug('Unknown action: ' + action);
return Promise.reject();
}
let route = this._routeToString(interfaceName, host, prefixLength, gateway);
let setupFunc = () => {
let count = this.addedRoutes.get(route);
if (DEBUG) debug(command + ": " + route + " -> " + count);
// Return false if there is no need to send the command to network worker.
if ((action == Ci.nsINetworkService.MODIFY_ROUTE_ADD && count) ||
(action == Ci.nsINetworkService.MODIFY_ROUTE_REMOVE &&
(!count || count > 1))) {
return false;
}
return true;
};
if (DEBUG) debug(command + " " + host + " on " + interfaceName);
let options = {
cmd: command,
ifname: interfaceName,
gateway: gateway,
prefixLength: prefixLength,
ip: host
};
return new Promise((aResolve, aReject) => {
this.controlMessage(options, (data) => {
let count = this.addedRoutes.get(route);
// Remove route from addedRoutes on success or failure.
if (action == Ci.nsINetworkService.MODIFY_ROUTE_REMOVE) {
if (count > 1) {
this.addedRoutes.set(route, count - 1);
} else {
this.addedRoutes.delete(route);
}
}
if (data.error) {
aReject(data.reason);
return;
}
if (action == Ci.nsINetworkService.MODIFY_ROUTE_ADD) {
this.addedRoutes.set(route, count ? count + 1 : 1);
}
aResolve();
}, setupFunc);
});
},
addSecondaryRoute: function(ifname, route) {
if(DEBUG) debug("Going to add route to secondary table on " + ifname);
let options = {
cmd: "addSecondaryRoute",
ifname: ifname,
ip: route.ip,
prefix: route.prefix,
gateway: route.gateway
};
this.controlMessage(options);
},
removeSecondaryRoute: function(ifname, route) {
if(DEBUG) debug("Going to remove route from secondary table on " + ifname);
let options = {
cmd: "removeSecondaryRoute",
ifname: ifname,
ip: route.ip,
prefix: route.prefix,
gateway: route.gateway
};
this.controlMessage(options);
},
setNetworkProxy: function(network) {
try {
if (!network.httpProxyHost || network.httpProxyHost === "") {
// Sets direct connection to internet.
this.clearNetworkProxy();
if (DEBUG) debug("No proxy support for " + network.name + " network interface.");
return;
}
if (DEBUG) debug("Going to set proxy settings for " + network.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", network.httpProxyHost);
Services.prefs.setCharPref("network.proxy.ssl", network.httpProxyHost);
let port = network.httpProxyPort === 0 ? 8080 : network.httpProxyPort;
Services.prefs.setIntPref("network.proxy.http_port", port);
Services.prefs.setIntPref("network.proxy.ssl_port", port);
} catch(ex) {
if (DEBUG) debug("Exception " + ex + ". Unable to set proxy setting for " +
network.name + " network interface.");
}
},
clearNetworkProxy: function() {
if (DEBUG) 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");
},
// Enable/Disable DHCP server.
setDhcpServer: function(enabled, config, callback) {
if (null === config) {
config = {};
}
config.cmd = "setDhcpServer";
config.enabled = enabled;
this.controlMessage(config, function setDhcpServerResult(response) {
if (!response.success) {
callback.dhcpServerResult('Set DHCP server error');
return;
}
callback.dhcpServerResult(null);
});
},
// Enable/disable WiFi tethering by sending commands to netd.
setWifiTethering: function(enable, config, callback) {
// config should've already contained:
// .ifname
// .internalIfname
// .externalIfname
config.wifictrlinterfacename = WIFI_CTRL_INTERFACE;
config.cmd = "setWifiTethering";
// The callback function in controlMessage may not be fired immediately.
this.controlMessage(config, function setWifiTetheringResult(data) {
let code = data.resultCode;
let reason = data.resultReason;
let enable = data.enable;
let enableString = enable ? "Enable" : "Disable";
if(DEBUG) debug(enableString + " Wifi tethering result: Code " + code + " reason " + reason);
if (isError(code)) {
callback.wifiTetheringEnabledChange("netd command error");
} else {
callback.wifiTetheringEnabledChange(null);
}
});
},
// Enable/disable USB tethering by sending commands to netd.
setUSBTethering: function(enable, config, callback) {
config.cmd = "setUSBTethering";
// The callback function in controlMessage may not be fired immediately.
this.controlMessage(config, function setUsbTetheringResult(data) {
let code = data.resultCode;
let reason = data.resultReason;
let enable = data.enable;
let enableString = enable ? "Enable" : "Disable";
if(DEBUG) debug(enableString + " USB tethering result: Code " + code + " reason " + reason);
if (isError(code)) {
callback.usbTetheringEnabledChange("netd command error");
} else {
callback.usbTetheringEnabledChange(null);
}
});
},
// Switch usb function by modifying property of persist.sys.usb.config.
enableUsbRndis: function(enable, callback) {
if(DEBUG) debug("enableUsbRndis: " + enable);
let params = {
cmd: "enableUsbRndis",
enable: enable
};
// Ask net work to report the result when this value is set to true.
if (callback) {
params.report = true;
} else {
params.report = false;
}
// The callback function in controlMessage may not be fired immediately.
//this._usbTetheringAction = TETHERING_STATE_ONGOING;
this.controlMessage(params, function(data) {
callback.enableUsbRndisResult(data.result, data.enable);
});
},
updateUpStream: function(previous, current, callback) {
let params = {
cmd: "updateUpStream",
preInternalIfname: previous.internalIfname,
preExternalIfname: previous.externalIfname,
curInternalIfname: current.internalIfname,
curExternalIfname: current.externalIfname
};
this.controlMessage(params, function(data) {
let code = data.resultCode;
let reason = data.resultReason;
if(DEBUG) debug("updateUpStream result: Code " + code + " reason " + reason);
callback.updateUpStreamResult(!isError(code), data.curExternalIfname);
});
},
configureInterface: function(config, callback) {
let params = {
cmd: "configureInterface",
ifname: config.ifname,
ipaddr: config.ipaddr,
mask: config.mask,
gateway_long: config.gateway,
dns1_long: config.dns1,
dns2_long: config.dns2,
};
this.controlMessage(params, function(result) {
callback.nativeCommandResult(!result.error);
});
},
dhcpRequest: function(interfaceName, callback) {
let params = {
cmd: "dhcpRequest",
ifname: interfaceName
};
this.controlMessage(params, function(result) {
callback.dhcpRequestResult(!result.error, result.error ? null : result);
});
},
enableInterface: function(interfaceName, callback) {
let params = {
cmd: "enableInterface",
ifname: interfaceName
};
this.controlMessage(params, function(result) {
callback.nativeCommandResult(!result.error);
});
},
disableInterface: function(interfaceName, callback) {
let params = {
cmd: "disableInterface",
ifname: interfaceName
};
this.controlMessage(params, function(result) {
callback.nativeCommandResult(!result.error);
});
},
resetConnections: function(interfaceName, callback) {
let params = {
cmd: "resetConnections",
ifname: interfaceName
};
this.controlMessage(params, function(result) {
callback.nativeCommandResult(!result.error);
});
},
createNetwork: function(interfaceName, callback) {
let params = {
cmd: "createNetwork",
ifname: interfaceName
};
this.controlMessage(params, function(result) {
callback.nativeCommandResult(!result.error);
});
},
destroyNetwork: function(interfaceName, callback) {
let params = {
cmd: "destroyNetwork",
ifname: interfaceName
};
this.controlMessage(params, function(result) {
callback.nativeCommandResult(!result.error);
});
},
shutdown: false,
observe: function observe(aSubject, aTopic, aData) {
switch (aTopic) {
case "xpcom-shutdown":
debug("NetworkService shutdown");
this.shutdown = true;
Services.obs.removeObserver(this, "xpcom-shutdown");
if (gNetworkWorker) {
gNetworkWorker.shutdown();
gNetworkWorker = null;
}
break;
}
},
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkService]);