diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /dom/network | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | uxp-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz |
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/network')
72 files changed, 13619 insertions, 0 deletions
diff --git a/dom/network/Connection.cpp b/dom/network/Connection.cpp new file mode 100644 index 0000000000..3bf6f40384 --- /dev/null +++ b/dom/network/Connection.cpp @@ -0,0 +1,96 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include <limits> +#include "mozilla/Hal.h" +#include "mozilla/dom/network/Connection.h" +#include "nsIDOMClassInfo.h" +#include "mozilla/Preferences.h" +#include "Constants.h" + +/** + * We have to use macros here because our leak analysis tool things we are + * leaking strings when we have |static const nsString|. Sad :( + */ +#define CHANGE_EVENT_NAME NS_LITERAL_STRING("typechange") + +namespace mozilla { +namespace dom { +namespace network { + +NS_IMPL_QUERY_INTERFACE_INHERITED(Connection, DOMEventTargetHelper, + nsINetworkProperties) + +// Don't use |Connection| alone, since that confuses nsTraceRefcnt since +// we're not the only class with that name. +NS_IMPL_ADDREF_INHERITED(dom::network::Connection, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(dom::network::Connection, DOMEventTargetHelper) + +Connection::Connection(nsPIDOMWindowInner* aWindow) + : DOMEventTargetHelper(aWindow) + , mType(static_cast<ConnectionType>(kDefaultType)) + , mIsWifi(kDefaultIsWifi) + , mDHCPGateway(kDefaultDHCPGateway) +{ + hal::RegisterNetworkObserver(this); + + hal::NetworkInformation networkInfo; + hal::GetCurrentNetworkInformation(&networkInfo); + + UpdateFromNetworkInfo(networkInfo); +} + +void +Connection::Shutdown() +{ + hal::UnregisterNetworkObserver(this); +} + +NS_IMETHODIMP +Connection::GetIsWifi(bool *aIsWifi) +{ + *aIsWifi = mIsWifi; + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetDhcpGateway(uint32_t *aGW) +{ + *aGW = mDHCPGateway; + return NS_OK; +} + +void +Connection::UpdateFromNetworkInfo(const hal::NetworkInformation& aNetworkInfo) +{ + mType = static_cast<ConnectionType>(aNetworkInfo.type()); + mIsWifi = aNetworkInfo.isWifi(); + mDHCPGateway = aNetworkInfo.dhcpGateway(); +} + +void +Connection::Notify(const hal::NetworkInformation& aNetworkInfo) +{ + ConnectionType previousType = mType; + + UpdateFromNetworkInfo(aNetworkInfo); + + if (previousType == mType) { + return; + } + + DispatchTrustedEvent(CHANGE_EVENT_NAME); +} + +JSObject* +Connection::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return NetworkInformationBinding::Wrap(aCx, this, aGivenProto); +} + +} // namespace network +} // namespace dom +} // namespace mozilla diff --git a/dom/network/Connection.h b/dom/network/Connection.h new file mode 100644 index 0000000000..907aea144b --- /dev/null +++ b/dom/network/Connection.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_network_Connection_h +#define mozilla_dom_network_Connection_h + +#include "Types.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/Observer.h" +#include "mozilla/dom/NetworkInformationBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsINetworkProperties.h" + +namespace mozilla { + +namespace hal { +class NetworkInformation; +} // namespace hal + +namespace dom { +namespace network { + +class Connection final : public DOMEventTargetHelper + , public NetworkObserver + , public nsINetworkProperties +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSINETWORKPROPERTIES + + NS_REALLY_FORWARD_NSIDOMEVENTTARGET(DOMEventTargetHelper) + + explicit Connection(nsPIDOMWindowInner* aWindow); + + void Shutdown(); + + // For IObserver + void Notify(const hal::NetworkInformation& aNetworkInfo) override; + + // WebIDL + + virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + ConnectionType Type() const { return mType; } + + IMPL_EVENT_HANDLER(typechange) + +private: + ~Connection() {} + + /** + * Update the connection information stored in the object using a + * NetworkInformation object. + */ + void UpdateFromNetworkInfo(const hal::NetworkInformation& aNetworkInfo); + + /** + * The type of current connection. + */ + ConnectionType mType; + + /** + * If the connection is WIFI + */ + bool mIsWifi; + + /** + * DHCP Gateway information for IPV4, in network byte order. 0 if unassigned. + */ + uint32_t mDHCPGateway; +}; + +} // namespace network +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_network_Connection_h diff --git a/dom/network/Constants.h b/dom/network/Constants.h new file mode 100644 index 0000000000..08ce9d29ed --- /dev/null +++ b/dom/network/Constants.h @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_network_Constants_h__ +#define mozilla_dom_network_Constants_h__ + +/** + * A set of constants to be used by network backends. + */ +namespace mozilla { +namespace dom { +namespace network { + + static const uint32_t kDefaultType = 5; // ConnectionType::None + static const bool kDefaultIsWifi = false; + static const uint32_t kDefaultDHCPGateway = 0; + +} // namespace network +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_network_Constants_h__ diff --git a/dom/network/EthernetManager.js b/dom/network/EthernetManager.js new file mode 100644 index 0000000000..4b11e56666 --- /dev/null +++ b/dom/network/EthernetManager.js @@ -0,0 +1,655 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* 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/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const TOPIC_INTERFACE_STATE_CHANGED = "network-interface-state-changed"; + +const ETHERNET_NETWORK_IFACE_PREFIX = "eth"; +const DEFAULT_ETHERNET_NETWORK_IFACE = "eth0"; + +const INTERFACE_IPADDR_NULL = "0.0.0.0"; +const INTERFACE_GATEWAY_NULL = "0.0.0.0"; +const INTERFACE_PREFIX_NULL = 0; +const INTERFACE_MACADDR_NULL = "00:00:00:00:00:00"; + +const NETWORK_INTERFACE_UP = "up"; +const NETWORK_INTERFACE_DOWN = "down"; + +const IP_MODE_DHCP = "dhcp"; +const IP_MODE_STATIC = "static"; + +const PREF_NETWORK_DEBUG_ENABLED = "network.debugging.enabled"; + +XPCOMUtils.defineLazyServiceGetter(this, "gNetworkManager", + "@mozilla.org/network/manager;1", + "nsINetworkManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "gNetworkService", + "@mozilla.org/network/service;1", + "nsINetworkService"); + +let 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("-*- EthernetManager: " + s + "\n"); + }; + } else { + debug = function(s) {}; + } +} +updateDebug(); + +// nsINetworkInterface + +function EthernetInterface(attr) { + this.info.state = attr.state; + this.info.type = attr.type; + this.info.name = attr.name; + this.info.ipMode = attr.ipMode; + this.info.ips = [attr.ip]; + this.info.prefixLengths = [attr.prefixLength]; + this.info.gateways = [attr.gateway]; + this.info.dnses = attr.dnses; + this.httpProxyHost = ""; + this.httpProxyPort = 0; +} +EthernetInterface.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInterface]), + + updateConfig: function(config) { + debug("Interface " + this.info.name + " updateConfig " + JSON.stringify(config)); + this.info.state = (config.state != undefined) ? + config.state : this.info.state; + this.info.ips = (config.ip != undefined) ? [config.ip] : this.info.ips; + this.info.prefixLengths = (config.prefixLength != undefined) ? + [config.prefixLength] : this.info.prefixLengths; + this.info.gateways = (config.gateway != undefined) ? + [config.gateway] : this.info.gateways; + this.info.dnses = (config.dnses != undefined) ? config.dnses : this.info.dnses; + this.httpProxyHost = (config.httpProxyHost != undefined) ? + config.httpProxyHost : this.httpProxyHost; + this.httpProxyPort = (config.httpProxyPort != undefined) ? + config.httpProxyPort : this.httpProxyPort; + this.info.ipMode = (config.ipMode != undefined) ? + config.ipMode : this.info.ipMode; + }, + + info: { + getAddresses: function(ips, prefixLengths) { + ips.value = this.ips.slice(); + prefixLengths.value = this.prefixLengths.slice(); + + return this.ips.length; + }, + + getGateways: function(count) { + if (count) { + count.value = this.gateways.length; + } + return this.gateways.slice(); + }, + + getDnses: function(count) { + if (count) { + count.value = this.dnses.length; + } + return this.dnses.slice(); + } + } +}; + +// nsIEthernetManager + +/* + * Network state transition diagram + * + * ---------- enable --------- connect ----------- disconnect -------------- + * | Disabled | -----> | Enabled | -------> | Connected | <----------> | Disconnected | + * ---------- --------- ----------- connect -------------- + * ^ | | | + * | disable | | | + * ----------------------------------------------------------------------- + */ + +function EthernetManager() { + debug("EthernetManager start"); + + // Interface list. + this.ethernetInterfaces = {}; + + // Used to memorize last connection information. + this.lastStaticConfig = {}; + + Services.obs.addObserver(this, "xpcom-shutdown", false); +} + +EthernetManager.prototype = { + classID: Components.ID("a96441dd-36b3-4f7f-963b-2c032e28a039"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIEthernetManager]), + + ethernetInterfaces: null, + lastStaticConfig: null, + + observer: function(subject, topic, data) { + switch (topic) { + case "xpcom-shutdown": + debug("xpcom-shutdown"); + + this._shutdown(); + + Services.obs.removeObserver(this, "xpcom-shutdown"); + break; + } + }, + + _shutdown: function() { + debug("Shuting down"); + (function onRemove(ifnameList) { + if (!ifnameList.length) { + return; + } + + let ifname = ifnameList.shift(); + this.removeInterface(ifname, { notify: onRemove.bind(this, ifnameList) }); + }).call(this, Object.keys(this.ethernetInterfaces)); + }, + + get interfaceList() { + return Object.keys(this.ethernetInterfaces); + }, + + scan: function(callback) { + debug("Scan"); + + gNetworkService.getInterfaces(function(success, list) { + let ethList = []; + + if (!success) { + if (callback) { + callback.notify(ethList); + } + return; + } + + for (let i = 0; i < list.length; i++) { + debug("Found interface " + list[i]); + if (!list[i].startsWith(ETHERNET_NETWORK_IFACE_PREFIX)) { + continue; + } + ethList.push(list[i]); + } + + if (callback) { + callback.notify(ethList); + } + }); + }, + + addInterface: function(ifname, callback) { + debug("Add interface " + ifname); + + if (!ifname || !ifname.startsWith(ETHERNET_NETWORK_IFACE_PREFIX)) { + if (callback) { + callback.notify(false, "Invalid interface."); + } + return; + } + + if (this.ethernetInterfaces[ifname]) { + if (callback) { + callback.notify(true, "Interface already exists."); + } + return; + } + + gNetworkService.getInterfaceConfig(ifname, function(success, result) { + if (!success) { + if (callback) { + callback.notify(false, "Netd error."); + } + return; + } + + // Since the operation may still succeed with an invalid interface name, + // check the mac address as well. + if (result.macAddr == INTERFACE_MACADDR_NULL) { + if (callback) { + callback.notify(false, "Interface not found."); + } + return; + } + + this.ethernetInterfaces[ifname] = new EthernetInterface({ + state: result.link == NETWORK_INTERFACE_UP ? + Ci.nsINetworkInfo.NETWORK_STATE_DISABLED : + Ci.nsINetworkInfo.NETWORK_STATE_ENABLED, + name: ifname, + type: Ci.nsINetworkInfo.NETWORK_TYPE_ETHERNET, + ip: result.ip, + prefixLength: result.prefix, + ipMode: IP_MODE_DHCP + }); + + // Register the interface to NetworkManager. + gNetworkManager.registerNetworkInterface(this.ethernetInterfaces[ifname]); + + debug("Add interface " + ifname + " succeeded with " + + JSON.stringify(this.ethernetInterfaces[ifname])); + + if (callback) { + callback.notify(true, "ok"); + } + }.bind(this)); + }, + + removeInterface: function(ifname, callback) { + debug("Remove interface " + ifname); + + if (!ifname || !ifname.startsWith(ETHERNET_NETWORK_IFACE_PREFIX)) { + if (callback) { + callback.notify(false, "Invalid interface."); + } + return; + } + + if (!this.ethernetInterfaces[ifname]) { + if (callback) { + callback.notify(true, "Interface does not exist."); + } + return; + } + + // Make sure interface is disable before removing. + this.disable(ifname, { notify: function(success, message) { + // Unregister the interface from NetworkManager and also remove it from + // the interface list. + gNetworkManager.unregisterNetworkInterface(this.ethernetInterfaces[ifname]); + delete this.ethernetInterfaces[ifname]; + + debug("Remove interface " + ifname + " succeeded."); + + if (callback) { + callback.notify(true, "ok"); + } + }.bind(this)}); + }, + + updateInterfaceConfig: function(ifname, config, callback) { + debug("Update interface config with " + ifname); + + this._ensureIfname(ifname, callback, function(iface) { + if (!config) { + if (callback) { + callback.notify(false, "No config to update."); + } + return; + } + + // Network state can not be modified externally. + if (config.state) { + delete config.state; + } + + let currentIpMode = iface.info.ipMode; + + // Update config. + this.ethernetInterfaces[iface.info.name].updateConfig(config); + + // Do not automatically re-connect if the interface is not in connected + // state. + if (iface.info.state != Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED) { + if (callback) { + callback.notify(true, "ok"); + } + return; + } + + let newIpMode = this.ethernetInterfaces[iface.info.name].info.ipMode; + + if (newIpMode == IP_MODE_STATIC) { + this._setStaticIP(iface.info.name, callback); + return; + } + if ((currentIpMode == IP_MODE_STATIC) && (newIpMode == IP_MODE_DHCP)) { + gNetworkService.stopDhcp(iface.info.name, function(success) { + if (success) { + debug("DHCP for " + iface.info.name + " stopped."); + } + }); + + // Clear the current network settings before do dhcp request, otherwise + // dhcp settings could fail. + this.disconnect(iface.info.name, { notify: function(success, message) { + if (!success) { + if (callback) { + callback.notify("Disconnect failed."); + } + return; + } + this._runDhcp(iface.info.name, callback); + }.bind(this) }); + return; + } + + if (callback) { + callback.notify(true, "ok"); + } + }.bind(this)); + }, + + enable: function(ifname, callback) { + debug("Enable interface " + ifname); + + this._ensureIfname(ifname, callback, function(iface) { + // Interface can be only enabled in the state of disabled. + if (iface.info.state != Ci.nsINetworkInfo.NETWORK_STATE_DISABLED) { + if (callback) { + callback.notify(true, "Interface already enabled."); + } + return; + } + + let ips = {}; + let prefixLengths = {}; + iface.info.getAddresses(ips, prefixLengths); + let config = { ifname: iface.info.name, + ip: ips.value[0], + prefix: prefixLengths.value[0], + link: NETWORK_INTERFACE_UP }; + gNetworkService.setInterfaceConfig(config, function(success) { + if (!success) { + if (callback) { + callback.notify(false, "Netd Error."); + } + return; + } + + this.ethernetInterfaces[iface.info.name].updateConfig({ + state: Ci.nsINetworkInfo.NETWORK_STATE_ENABLED + }); + + debug("Enable interface " + iface.info.name + " succeeded."); + + if (callback) { + callback.notify(true, "ok"); + } + }.bind(this)); + }.bind(this)); + }, + + disable: function(ifname, callback) { + debug("Disable interface " + ifname); + + this._ensureIfname(ifname, callback, function(iface) { + if (iface.info.state == Ci.nsINetworkInfo.NETWORK_STATE_DISABLED) { + if (callback) { + callback.notify(true, "Interface already disabled."); + } + return; + } + + if (iface.info.state == Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED) { + gNetworkService.stopDhcp(iface.info.name, function(success) { + if (success) { + debug("DHCP for " + iface.info.name + " stopped."); + } + }); + } + + let ips = {}; + let prefixLengths = {}; + iface.info.getAddresses(ips, prefixLengths); + let config = { ifname: iface.info.name, + ip: ips.value[0], + prefix: prefixLengths.value[0], + link: NETWORK_INTERFACE_DOWN }; + gNetworkService.setInterfaceConfig(config, function(success) { + if (!success) { + if (callback) { + callback.notify(false, "Netd Error."); + } + return; + } + + this.ethernetInterfaces[iface.info.name].updateConfig({ + state: Ci.nsINetworkInfo.NETWORK_STATE_DISABLED + }); + + debug("Disable interface " + iface.info.name + " succeeded."); + + if (callback) { + callback.notify(true, "ok"); + } + }.bind(this)); + }.bind(this)); + }, + + connect: function(ifname, callback) { + debug("Connect interface " + ifname); + + this._ensureIfname(ifname, callback, function(iface) { + // Interface can only be connected in the state of enabled or + // disconnected. + if (iface.info.state == Ci.nsINetworkInfo.NETWORK_STATE_DISABLED || + iface.info.state == Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED) { + if (callback) { + callback.notify(true, "Interface " + ifname + " is not available or " + + " already connected."); + } + return; + } + + if (iface.info.ipMode == IP_MODE_DHCP) { + this._runDhcp(iface.info.name, callback); + return; + } + + if (iface.info.ipMode == IP_MODE_STATIC) { + if (this._checkConfigNull(iface) && this.lastStaticConfig[iface.info.name]) { + debug("Connect with lastStaticConfig " + + JSON.stringify(this.lastStaticConfig[iface.info.name])); + this.ethernetInterfaces[iface.info.name].updateConfig( + this.lastStaticConfig[iface.info.name]); + } + this._setStaticIP(iface.info.name, callback); + return; + } + + if (callback) { + callback.notify(false, "IP mode is wrong or not set."); + } + }.bind(this)); + }, + + disconnect: function(ifname, callback) { + debug("Disconnect interface " + ifname); + + this._ensureIfname(ifname, callback, function(iface) { + // Interface can be only disconnected in the state of connected. + if (iface.info.state != Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED) { + if (callback) { + callback.notify(true, "Interface is already disconnected"); + } + return; + } + + let config = { ifname: iface.info.name, + ip: INTERFACE_IPADDR_NULL, + prefix: INTERFACE_PREFIX_NULL, + link: NETWORK_INTERFACE_UP }; + gNetworkService.setInterfaceConfig(config, function(success) { + if (!success) { + if (callback) { + callback.notify(false, "Netd error."); + } + return; + } + + // Stop dhcp daemon. + gNetworkService.stopDhcp(iface.info.name, function(success) { + if (success) { + debug("DHCP for " + iface.info.name + " stopped."); + } + }); + + this.ethernetInterfaces[iface.info.name].updateConfig({ + state: Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED, + ip: INTERFACE_IPADDR_NULL, + prefixLength: INTERFACE_PREFIX_NULL, + gateway: INTERFACE_GATEWAY_NULL + }); + + gNetworkManager.updateNetworkInterface(this.ethernetInterfaces[ifname]); + + debug("Disconnect interface " + iface.info.name + " succeeded."); + + if (callback) { + callback.notify(true, "ok"); + } + }.bind(this)); + }.bind(this)); + }, + + _checkConfigNull: function(iface) { + let ips = {}; + let prefixLengths = {}; + let gateways = iface.info.getGateways(); + iface.info.getAddresses(ips, prefixLengths); + + if (ips.value[0] == INTERFACE_IPADDR_NULL && + prefixLengths.value[0] == INTERFACE_PREFIX_NULL && + gateways[0] == INTERFACE_GATEWAY_NULL) { + return true; + } + + return false; + }, + + _ensureIfname: function(ifname, callback, func) { + // If no given ifname, use the default one. + if (!ifname) { + ifname = DEFAULT_ETHERNET_NETWORK_IFACE; + } + + let iface = this.ethernetInterfaces[ifname]; + if (!iface) { + if (callback) { + callback.notify(true, "Interface " + ifname + " is not available."); + } + return; + } + + func.call(this, iface); + }, + + _runDhcp: function(ifname, callback) { + debug("runDhcp with " + ifname); + + if (!this.ethernetInterfaces[ifname]) { + if (callback) { + callback.notify(false, "Invalid interface."); + } + return; + } + + gNetworkService.dhcpRequest(ifname, function(success, result) { + if (!success) { + if (callback) { + callback.notify(false, "DHCP failed."); + } + return; + } + + debug("DHCP succeeded with " + JSON.stringify(result)); + + // Clear last static network information when connecting with dhcp mode. + if (this.lastStaticConfig[ifname]) { + this.lastStaticConfig[ifname] = null; + } + + this.ethernetInterfaces[ifname].updateConfig({ + state: Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED, + ip: result.ipaddr_str, + gateway: result.gateway_str, + prefixLength: result.prefixLength, + dnses: [result.dns1_str, result.dns2_str] + }); + + gNetworkManager.updateNetworkInterface(this.ethernetInterfaces[ifname]); + + debug("Connect interface " + ifname + " with DHCP succeeded."); + + if (callback) { + callback.notify(true, "ok"); + } + }.bind(this)); + }, + + _setStaticIP: function(ifname, callback) { + let iface = this.ethernetInterfaces[ifname]; + if (!iface) { + if (callback) { + callback.notify(false, "Invalid interface."); + } + return; + } + + let ips = {}; + let prefixLengths = {}; + iface.info.getAddresses(ips, prefixLengths); + + let config = { ifname: iface.info.name, + ip: ips.value[0], + prefix: prefixLengths.value[0], + link: NETWORK_INTERFACE_UP }; + gNetworkService.setInterfaceConfig(config, function(success) { + if (!success) { + if (callback) { + callback.notify(false, "Netd Error."); + } + return; + } + + // Keep the lastest static network information. + let ips = {}; + let prefixLengths = {}; + let gateways = iface.info.getGateways(); + iface.info.getAddresses(ips, prefixLengths); + + this.lastStaticConfig[iface.info.name] = { + ip: ips.value[0], + prefixLength: prefixLengths.value[0], + gateway: gateways[0] + }; + + this.ethernetInterfaces[ifname].updateConfig({ + state: Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED, + }); + + gNetworkManager.updateNetworkInterface(this.ethernetInterfaces[ifname]); + + debug("Connect interface " + ifname + " with static ip succeeded."); + + if (callback) { + callback.notify(true, "ok"); + } + }.bind(this)); + }, +} + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([EthernetManager]); diff --git a/dom/network/EthernetManager.manifest b/dom/network/EthernetManager.manifest new file mode 100644 index 0000000000..d25a069e12 --- /dev/null +++ b/dom/network/EthernetManager.manifest @@ -0,0 +1,2 @@ +component {a96441dd-36b3-4f7f-963b-2c032e28a039} EthernetManager.js +contract @mozilla.org/ethernetManager;1 {a96441dd-36b3-4f7f-963b-2c032e28a039} diff --git a/dom/network/NetUtils.cpp b/dom/network/NetUtils.cpp new file mode 100644 index 0000000000..78c5be802f --- /dev/null +++ b/dom/network/NetUtils.cpp @@ -0,0 +1,200 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "NetUtils.h" +#include <dlfcn.h> +#include <errno.h> +#include "prinit.h" +#include "mozilla/Assertions.h" +#include "nsDebug.h" +#include "SystemProperty.h" + +using mozilla::system::Property; + +static void* sNetUtilsLib; +static PRCallOnceType sInitNetUtilsLib; + +static PRStatus +InitNetUtilsLib() +{ + sNetUtilsLib = dlopen("/system/lib/libnetutils.so", RTLD_LAZY); + // We might fail to open the hardware lib. That's OK. + return PR_SUCCESS; +} + +static void* +GetNetUtilsLibHandle() +{ + PR_CallOnce(&sInitNetUtilsLib, InitNetUtilsLib); + return sNetUtilsLib; +} + +// static +void* +NetUtils::GetSharedLibrary() +{ + void* netLib = GetNetUtilsLibHandle(); + if (!netLib) { + NS_WARNING("No /system/lib/libnetutils.so"); + } + return netLib; +} + +// static +int32_t +NetUtils::SdkVersion() +{ + char propVersion[Property::VALUE_MAX_LENGTH]; + Property::Get("ro.build.version.sdk", propVersion, "0"); + int32_t version = strtol(propVersion, nullptr, 10); + return version; +} + +DEFINE_DLFUNC(ifc_enable, int32_t, const char*) +DEFINE_DLFUNC(ifc_disable, int32_t, const char*) +DEFINE_DLFUNC(ifc_configure, int32_t, const char*, in_addr_t, uint32_t, + in_addr_t, in_addr_t, in_addr_t) +DEFINE_DLFUNC(ifc_reset_connections, int32_t, const char*, const int32_t) +DEFINE_DLFUNC(ifc_set_default_route, int32_t, const char*, in_addr_t) +DEFINE_DLFUNC(ifc_add_route, int32_t, const char*, const char*, uint32_t, const char*) +DEFINE_DLFUNC(ifc_remove_route, int32_t, const char*, const char*, uint32_t, const char*) +DEFINE_DLFUNC(ifc_remove_host_routes, int32_t, const char*) +DEFINE_DLFUNC(ifc_remove_default_route, int32_t, const char*) +DEFINE_DLFUNC(dhcp_stop, int32_t, const char*) + +NetUtils::NetUtils() +{ +} + +int32_t NetUtils::do_ifc_enable(const char *ifname) +{ + USE_DLFUNC(ifc_enable) + return ifc_enable(ifname); +} + +int32_t NetUtils::do_ifc_disable(const char *ifname) +{ + USE_DLFUNC(ifc_disable) + return ifc_disable(ifname); +} + +int32_t NetUtils::do_ifc_configure(const char *ifname, + in_addr_t address, + uint32_t prefixLength, + in_addr_t gateway, + in_addr_t dns1, + in_addr_t dns2) +{ + USE_DLFUNC(ifc_configure) + int32_t ret = ifc_configure(ifname, address, prefixLength, gateway, dns1, dns2); + return ret; +} + +int32_t NetUtils::do_ifc_reset_connections(const char *ifname, + const int32_t resetMask) +{ + USE_DLFUNC(ifc_reset_connections) + return ifc_reset_connections(ifname, resetMask); +} + +int32_t NetUtils::do_ifc_set_default_route(const char *ifname, + in_addr_t gateway) +{ + USE_DLFUNC(ifc_set_default_route) + return ifc_set_default_route(ifname, gateway); +} + +int32_t NetUtils::do_ifc_add_route(const char *ifname, + const char *dst, + uint32_t prefixLength, + const char *gateway) +{ + USE_DLFUNC(ifc_add_route) + return ifc_add_route(ifname, dst, prefixLength, gateway); +} + +int32_t NetUtils::do_ifc_remove_route(const char *ifname, + const char *dst, + uint32_t prefixLength, + const char *gateway) +{ + USE_DLFUNC(ifc_remove_route) + return ifc_remove_route(ifname, dst, prefixLength, gateway); +} + +int32_t NetUtils::do_ifc_remove_host_routes(const char *ifname) +{ + USE_DLFUNC(ifc_remove_host_routes) + return ifc_remove_host_routes(ifname); +} + +int32_t NetUtils::do_ifc_remove_default_route(const char *ifname) +{ + USE_DLFUNC(ifc_remove_default_route) + return ifc_remove_default_route(ifname); +} + +int32_t NetUtils::do_dhcp_stop(const char *ifname) +{ + USE_DLFUNC(dhcp_stop) + return dhcp_stop(ifname); +} + +int32_t NetUtils::do_dhcp_do_request(const char *ifname, + char *ipaddr, + char *gateway, + uint32_t *prefixLength, + char *dns1, + char *dns2, + char *server, + uint32_t *lease, + char* vendorinfo) +{ + int32_t ret = -1; + uint32_t sdkVersion = SdkVersion(); + + if (sdkVersion == 15) { + // ICS + // http://androidxref.com/4.0.4/xref/system/core/libnetutils/dhcp_utils.c#149 + DEFINE_DLFUNC(dhcp_do_request, int32_t, const char*, char*, char*, uint32_t*, char*, char*, char*, uint32_t*) + USE_DLFUNC(dhcp_do_request) + vendorinfo[0] = '\0'; + + ret = dhcp_do_request(ifname, ipaddr, gateway, prefixLength, dns1, dns2, + server, lease); + } else if (sdkVersion == 16 || sdkVersion == 17) { + // JB 4.1 and 4.2 + // http://androidxref.com/4.1.2/xref/system/core/libnetutils/dhcp_utils.c#175 + // http://androidxref.com/4.2.2_r1/xref/system/core/include/netutils/dhcp.h#26 + DEFINE_DLFUNC(dhcp_do_request, int32_t, const char*, char*, char*, uint32_t*, char*, char*, char*, uint32_t*, char*) + USE_DLFUNC(dhcp_do_request) + ret = dhcp_do_request(ifname, ipaddr, gateway, prefixLength, dns1, dns2, + server, lease, vendorinfo); + } else if (sdkVersion == 18) { + // JB 4.3 + // http://androidxref.com/4.3_r2.1/xref/system/core/libnetutils/dhcp_utils.c#181 + DEFINE_DLFUNC(dhcp_do_request, int32_t, const char*, char*, char*, uint32_t*, char**, char*, uint32_t*, char*, char*) + USE_DLFUNC(dhcp_do_request) + char *dns[3] = {dns1, dns2, nullptr}; + char domains[Property::VALUE_MAX_LENGTH]; + ret = dhcp_do_request(ifname, ipaddr, gateway, prefixLength, dns, + server, lease, vendorinfo, domains); + } else if (sdkVersion >= 19) { + // KitKat 4.4.X + // http://androidxref.com/4.4_r1/xref/system/core/libnetutils/dhcp_utils.c#18 + // Lollipop 5.0 + //http://androidxref.com/5.0.0_r2/xref/system/core/libnetutils/dhcp_utils.c#186 + DEFINE_DLFUNC(dhcp_do_request, int32_t, const char*, char*, char*, uint32_t*, char**, char*, uint32_t*, char*, char*, char*) + USE_DLFUNC(dhcp_do_request) + char *dns[3] = {dns1, dns2, nullptr}; + char domains[Property::VALUE_MAX_LENGTH]; + char mtu[Property::VALUE_MAX_LENGTH]; + ret = dhcp_do_request(ifname, ipaddr, gateway, prefixLength, dns, server, lease, vendorinfo, domains, mtu); + } else { + NS_WARNING("Unable to perform do_dhcp_request: unsupported sdk version!"); + } + return ret; +} diff --git a/dom/network/NetUtils.h b/dom/network/NetUtils.h new file mode 100644 index 0000000000..4af365406d --- /dev/null +++ b/dom/network/NetUtils.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +/** + * Abstraction on top of the network support from libnetutils that we + * use to set up network connections. + */ + +#ifndef NetUtils_h +#define NetUtils_h + +#include "arpa/inet.h" + +// Copied from ifc.h +#define RESET_IPV4_ADDRESSES 0x01 +#define RESET_IPV6_ADDRESSES 0x02 +#define RESET_ALL_ADDRESSES (RESET_IPV4_ADDRESSES | RESET_IPV6_ADDRESSES) + +// Implements netutils functions. No need for an abstract class here since we +// only have a one sdk specific method (dhcp_do_request) +class NetUtils +{ +public: + static void* GetSharedLibrary(); + + NetUtils(); + + int32_t do_ifc_enable(const char *ifname); + int32_t do_ifc_disable(const char *ifname); + int32_t do_ifc_configure(const char *ifname, + in_addr_t address, + uint32_t prefixLength, + in_addr_t gateway, + in_addr_t dns1, + in_addr_t dns2); + int32_t do_ifc_reset_connections(const char *ifname, const int32_t resetMask); + int32_t do_ifc_set_default_route(const char *ifname, in_addr_t gateway); + int32_t do_ifc_add_route(const char *ifname, + const char *dst, + uint32_t prefixLength, + const char *gateway); + int32_t do_ifc_remove_route(const char *ifname, + const char *dst, + uint32_t prefixLength, + const char *gateway); + int32_t do_ifc_remove_host_routes(const char *ifname); + int32_t do_ifc_remove_default_route(const char *ifname); + int32_t do_dhcp_stop(const char *ifname); + int32_t do_dhcp_do_request(const char *ifname, + char *ipaddr, + char *gateway, + uint32_t *prefixLength, + char *dns1, + char *dns2, + char *server, + uint32_t *lease, + char* vendorinfo); + + static int32_t SdkVersion(); +}; + +// Defines a function type with the right arguments and return type. +#define DEFINE_DLFUNC(name, ret, args...) typedef ret (*FUNC##name)(args); + +// Set up a dlsymed function ready to use. +#define USE_DLFUNC(name) \ + FUNC##name name = (FUNC##name) dlsym(GetSharedLibrary(), #name); \ + if (!name) { \ + MOZ_CRASH("Symbol not found in shared library : " #name); \ + } + +#endif // NetUtils_h diff --git a/dom/network/NetworkStatsDB.jsm b/dom/network/NetworkStatsDB.jsm new file mode 100644 index 0000000000..aa74d40ad9 --- /dev/null +++ b/dom/network/NetworkStatsDB.jsm @@ -0,0 +1,1285 @@ +/* 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"; + +this.EXPORTED_SYMBOLS = ['NetworkStatsDB']; + +const DEBUG = false; +function debug(s) { dump("-*- NetworkStatsDB: " + s + "\n"); } + +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/IndexedDBHelper.jsm"); +Cu.importGlobalProperties(["indexedDB"]); + +XPCOMUtils.defineLazyServiceGetter(this, "appsService", + "@mozilla.org/AppsService;1", + "nsIAppsService"); + +const DB_NAME = "net_stats"; +const DB_VERSION = 9; +const DEPRECATED_STATS_STORE_NAME = + [ + "net_stats_v2", // existed only in DB version 2 + "net_stats", // existed in DB version 1 and 3 to 5 + "net_stats_store", // existed in DB version 6 to 8 + ]; +const STATS_STORE_NAME = "net_stats_store_v3"; // since DB version 9 +const ALARMS_STORE_NAME = "net_alarm"; + +// Constant defining the maximum values allowed per interface. If more, older +// will be erased. +const VALUES_MAX_LENGTH = 6 * 30; + +// Constant defining the rate of the samples. Daily. +const SAMPLE_RATE = 1000 * 60 * 60 * 24; + +this.NetworkStatsDB = function NetworkStatsDB() { + if (DEBUG) { + debug("Constructor"); + } + this.initDBHelper(DB_NAME, DB_VERSION, [STATS_STORE_NAME, ALARMS_STORE_NAME]); +} + +NetworkStatsDB.prototype = { + __proto__: IndexedDBHelper.prototype, + + dbNewTxn: function dbNewTxn(store_name, txn_type, callback, txnCb) { + function successCb(result) { + txnCb(null, result); + } + function errorCb(error) { + txnCb(error, null); + } + return this.newTxn(txn_type, store_name, callback, successCb, errorCb); + }, + + /** + * The onupgradeneeded handler of the IDBOpenDBRequest. + * This function is called in IndexedDBHelper open() method. + * + * @param {IDBTransaction} aTransaction + * {IDBDatabase} aDb + * {64-bit integer} aOldVersion The version number on local storage. + * {64-bit integer} aNewVersion The version number to be upgraded to. + * + * @note Be careful with the database upgrade pattern. + * Because IndexedDB operations are performed asynchronously, we must + * apply a recursive approach instead of an iterative approach while + * upgrading versions. + */ + upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { + if (DEBUG) { + debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!"); + } + let db = aDb; + let objectStore; + + // An array of upgrade functions for each version. + let upgradeSteps = [ + function upgrade0to1() { + if (DEBUG) debug("Upgrade 0 to 1: Create object stores and indexes."); + + // Create the initial database schema. + objectStore = db.createObjectStore(DEPRECATED_STATS_STORE_NAME[1], + { keyPath: ["connectionType", "timestamp"] }); + objectStore.createIndex("connectionType", "connectionType", { unique: false }); + objectStore.createIndex("timestamp", "timestamp", { unique: false }); + objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); + objectStore.createIndex("txBytes", "txBytes", { unique: false }); + objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); + objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); + + upgradeNextVersion(); + }, + + function upgrade1to2() { + if (DEBUG) debug("Upgrade 1 to 2: Do nothing."); + upgradeNextVersion(); + }, + + function upgrade2to3() { + if (DEBUG) debug("Upgrade 2 to 3: Add keyPath appId to object store."); + + // In order to support per-app traffic data storage, the original + // objectStore needs to be replaced by a new objectStore with new + // key path ("appId") and new index ("appId"). + // Also, since now networks are identified by their + // [networkId, networkType] not just by their connectionType, + // to modify the keyPath is mandatory to delete the object store + // and create it again. Old data is going to be deleted because the + // networkId for each sample can not be set. + + // In version 1.2 objectStore name was 'net_stats_v2', to avoid errors when + // upgrading from 1.2 to 1.3 objectStore name should be checked. + let stores = db.objectStoreNames; + let deprecatedName = DEPRECATED_STATS_STORE_NAME[0]; + let storeName = DEPRECATED_STATS_STORE_NAME[1]; + if(stores.contains(deprecatedName)) { + // Delete the obsolete stats store. + db.deleteObjectStore(deprecatedName); + } else { + // Re-create stats object store without copying records. + db.deleteObjectStore(storeName); + } + + objectStore = db.createObjectStore(storeName, { keyPath: ["appId", "network", "timestamp"] }); + objectStore.createIndex("appId", "appId", { unique: false }); + objectStore.createIndex("network", "network", { unique: false }); + objectStore.createIndex("networkType", "networkType", { unique: false }); + objectStore.createIndex("timestamp", "timestamp", { unique: false }); + objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); + objectStore.createIndex("txBytes", "txBytes", { unique: false }); + objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); + objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); + + upgradeNextVersion(); + }, + + function upgrade3to4() { + if (DEBUG) debug("Upgrade 3 to 4: Delete redundant indexes."); + + // Delete redundant indexes (leave "network" only). + objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]); + if (objectStore.indexNames.contains("appId")) { + objectStore.deleteIndex("appId"); + } + if (objectStore.indexNames.contains("networkType")) { + objectStore.deleteIndex("networkType"); + } + if (objectStore.indexNames.contains("timestamp")) { + objectStore.deleteIndex("timestamp"); + } + if (objectStore.indexNames.contains("rxBytes")) { + objectStore.deleteIndex("rxBytes"); + } + if (objectStore.indexNames.contains("txBytes")) { + objectStore.deleteIndex("txBytes"); + } + if (objectStore.indexNames.contains("rxTotalBytes")) { + objectStore.deleteIndex("rxTotalBytes"); + } + if (objectStore.indexNames.contains("txTotalBytes")) { + objectStore.deleteIndex("txTotalBytes"); + } + + upgradeNextVersion(); + }, + + function upgrade4to5() { + if (DEBUG) debug("Upgrade 4 to 5: Create object store for alarms."); + + // In order to manage alarms, it is necessary to use a global counter + // (totalBytes) that will increase regardless of the system reboot. + objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]); + + // Now, systemBytes will hold the old totalBytes and totalBytes will + // keep the increasing counter. |counters| will keep the track of + // accumulated values. + let counters = {}; + + objectStore.openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (!cursor){ + // upgrade4to5 completed now. + upgradeNextVersion(); + return; + } + + cursor.value.rxSystemBytes = cursor.value.rxTotalBytes; + cursor.value.txSystemBytes = cursor.value.txTotalBytes; + + if (cursor.value.appId == 0) { + let netId = cursor.value.network[0] + '' + cursor.value.network[1]; + if (!counters[netId]) { + counters[netId] = { + rxCounter: 0, + txCounter: 0, + lastRx: 0, + lastTx: 0 + }; + } + + let rxDiff = cursor.value.rxSystemBytes - counters[netId].lastRx; + let txDiff = cursor.value.txSystemBytes - counters[netId].lastTx; + if (rxDiff < 0 || txDiff < 0) { + // System reboot between samples, so take the current one. + rxDiff = cursor.value.rxSystemBytes; + txDiff = cursor.value.txSystemBytes; + } + + counters[netId].rxCounter += rxDiff; + counters[netId].txCounter += txDiff; + cursor.value.rxTotalBytes = counters[netId].rxCounter; + cursor.value.txTotalBytes = counters[netId].txCounter; + + counters[netId].lastRx = cursor.value.rxSystemBytes; + counters[netId].lastTx = cursor.value.txSystemBytes; + } else { + cursor.value.rxTotalBytes = cursor.value.rxSystemBytes; + cursor.value.txTotalBytes = cursor.value.txSystemBytes; + } + + cursor.update(cursor.value); + cursor.continue(); + }; + + // Create object store for alarms. + objectStore = db.createObjectStore(ALARMS_STORE_NAME, { keyPath: "id", autoIncrement: true }); + objectStore.createIndex("alarm", ['networkId','threshold'], { unique: false }); + objectStore.createIndex("manifestURL", "manifestURL", { unique: false }); + }, + + function upgrade5to6() { + if (DEBUG) debug("Upgrade 5 to 6: Add keyPath serviceType to object store."); + + // In contrast to "per-app" traffic data, "system-only" traffic data + // refers to data which can not be identified by any applications. + // To further support "system-only" data storage, the data can be + // saved by service type (e.g., Tethering, OTA). Thus it's needed to + // have a new key ("serviceType") for the ojectStore. + let newObjectStore; + let deprecatedName = DEPRECATED_STATS_STORE_NAME[1]; + newObjectStore = db.createObjectStore(DEPRECATED_STATS_STORE_NAME[2], + { keyPath: ["appId", "serviceType", "network", "timestamp"] }); + newObjectStore.createIndex("network", "network", { unique: false }); + + // Copy the data from the original objectStore to the new objectStore. + objectStore = aTransaction.objectStore(deprecatedName); + objectStore.openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (!cursor) { + db.deleteObjectStore(deprecatedName); + // upgrade5to6 completed now. + upgradeNextVersion(); + return; + } + + let newStats = cursor.value; + newStats.serviceType = ""; + newObjectStore.put(newStats); + cursor.continue(); + }; + }, + + function upgrade6to7() { + if (DEBUG) debug("Upgrade 6 to 7: Replace alarm threshold by relativeThreshold."); + + // Replace threshold attribute of alarm index by relativeThreshold in alarms DB. + // Now alarms are indexed by relativeThreshold, which is the threshold relative + // to current system stats. + let alarmsStore = aTransaction.objectStore(ALARMS_STORE_NAME); + + // Delete "alarm" index. + if (alarmsStore.indexNames.contains("alarm")) { + alarmsStore.deleteIndex("alarm"); + } + + // Create new "alarm" index. + alarmsStore.createIndex("alarm", ['networkId','relativeThreshold'], { unique: false }); + + // Populate new "alarm" index attributes. + alarmsStore.openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (!cursor) { + upgrade6to7_updateTotalBytes(); + return; + } + + cursor.value.relativeThreshold = cursor.value.threshold; + cursor.value.absoluteThreshold = cursor.value.threshold; + delete cursor.value.threshold; + + cursor.update(cursor.value); + cursor.continue(); + } + + function upgrade6to7_updateTotalBytes() { + if (DEBUG) debug("Upgrade 6 to 7: Update TotalBytes."); + // Previous versions save accumulative totalBytes, increasing although the system + // reboots or resets stats. But is necessary to reset the total counters when reset + // through 'clearInterfaceStats'. + let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]); + let networks = []; + + // Find networks stored in the database. + statsStore.index("network").openKeyCursor(null, "nextunique").onsuccess = function(event) { + let cursor = event.target.result; + + // Store each network into an array. + if (cursor) { + networks.push(cursor.key); + cursor.continue(); + return; + } + + // Start to deal with each network. + let pending = networks.length; + + if (pending === 0) { + // Found no records of network. upgrade6to7 completed now. + upgradeNextVersion(); + return; + } + + networks.forEach(function(network) { + let lowerFilter = [0, "", network, 0]; + let upperFilter = [0, "", network, ""]; + let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); + + // Find number of samples for a given network. + statsStore.count(range).onsuccess = function(event) { + let recordCount = event.target.result; + + // If there are more samples than the max allowed, there is no way to know + // when does reset take place. + if (recordCount === 0 || recordCount >= VALUES_MAX_LENGTH) { + pending--; + if (pending === 0) { + upgradeNextVersion(); + } + return; + } + + let last = null; + // Reset detected if the first sample totalCounters are different than bytes + // counters. If so, the total counters should be recalculated. + statsStore.openCursor(range).onsuccess = function(event) { + let cursor = event.target.result; + if (!cursor) { + pending--; + if (pending === 0) { + upgradeNextVersion(); + } + return; + } + if (!last) { + if (cursor.value.rxTotalBytes == cursor.value.rxBytes && + cursor.value.txTotalBytes == cursor.value.txBytes) { + pending--; + if (pending === 0) { + upgradeNextVersion(); + } + return; + } + + cursor.value.rxTotalBytes = cursor.value.rxBytes; + cursor.value.txTotalBytes = cursor.value.txBytes; + cursor.update(cursor.value); + last = cursor.value; + cursor.continue(); + return; + } + + // Recalculate the total counter for last / current sample + cursor.value.rxTotalBytes = last.rxTotalBytes + cursor.value.rxBytes; + cursor.value.txTotalBytes = last.txTotalBytes + cursor.value.txBytes; + cursor.update(cursor.value); + last = cursor.value; + cursor.continue(); + } + } + }, this); // end of networks.forEach() + }; // end of statsStore.index("network").openKeyCursor().onsuccess callback + } // end of function upgrade6to7_updateTotalBytes + }, + + function upgrade7to8() { + if (DEBUG) debug("Upgrade 7 to 8: Create index serviceType."); + + // Create index for 'ServiceType' in order to make it retrievable. + let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]); + statsStore.createIndex("serviceType", "serviceType", { unique: false }); + + upgradeNextVersion(); + }, + + function upgrade8to9() { + if (DEBUG) debug("Upgrade 8 to 9: Add keyPath isInBrowser to " + + "network stats object store"); + + // Since B2G v2.0, there is no stand-alone browser app anymore. + // The browser app is a mozbrowser iframe element owned by system app. + // In order to separate traffic generated from system and browser, we + // have to add a new attribute |isInBrowser| as keyPath. + // Refer to bug 1070944 for more detail. + let newObjectStore; + let deprecatedName = DEPRECATED_STATS_STORE_NAME[2]; + newObjectStore = db.createObjectStore(STATS_STORE_NAME, + { keyPath: ["appId", "isInBrowser", "serviceType", + "network", "timestamp"] }); + newObjectStore.createIndex("network", "network", { unique: false }); + newObjectStore.createIndex("serviceType", "serviceType", { unique: false }); + + // Copy records from the current object store to the new one. + objectStore = aTransaction.objectStore(deprecatedName); + objectStore.openCursor().onsuccess = function (event) { + let cursor = event.target.result; + if (!cursor) { + db.deleteObjectStore(deprecatedName); + // upgrade8to9 completed now. + return; + } + let newStats = cursor.value; + // Augment records by adding the new isInBrowser attribute. + // Notes: + // 1. Key value cannot be boolean type. Use 1/0 instead of true/false. + // 2. Most traffic of system app should come from its browser iframe, + // thus assign isInBrowser as 1 for system app. + let manifestURL = appsService.getManifestURLByLocalId(newStats.appId); + if (manifestURL && manifestURL.search(/app:\/\/system\./) === 0) { + newStats.isInBrowser = 1; + } else { + newStats.isInBrowser = 0; + } + newObjectStore.put(newStats); + cursor.continue(); + }; + } + ]; + + let index = aOldVersion; + let outer = this; + + function upgradeNextVersion() { + if (index == aNewVersion) { + debug("Upgrade finished."); + return; + } + + try { + var i = index++; + if (DEBUG) debug("Upgrade step: " + i + "\n"); + upgradeSteps[i].call(outer); + } catch (ex) { + dump("Caught exception " + ex); + throw ex; + return; + } + } + + if (aNewVersion > upgradeSteps.length) { + debug("No migration steps for the new version!"); + aTransaction.abort(); + return; + } + + upgradeNextVersion(); + }, + + importData: function importData(aStats) { + let stats = { appId: aStats.appId, + isInBrowser: aStats.isInBrowser ? 1 : 0, + serviceType: aStats.serviceType, + network: [aStats.networkId, aStats.networkType], + timestamp: aStats.timestamp, + rxBytes: aStats.rxBytes, + txBytes: aStats.txBytes, + rxSystemBytes: aStats.rxSystemBytes, + txSystemBytes: aStats.txSystemBytes, + rxTotalBytes: aStats.rxTotalBytes, + txTotalBytes: aStats.txTotalBytes }; + + return stats; + }, + + exportData: function exportData(aStats) { + let stats = { appId: aStats.appId, + isInBrowser: aStats.isInBrowser ? true : false, + serviceType: aStats.serviceType, + networkId: aStats.network[0], + networkType: aStats.network[1], + timestamp: aStats.timestamp, + rxBytes: aStats.rxBytes, + txBytes: aStats.txBytes, + rxTotalBytes: aStats.rxTotalBytes, + txTotalBytes: aStats.txTotalBytes }; + + return stats; + }, + + normalizeDate: function normalizeDate(aDate) { + // Convert to UTC according to timezone and + // filter timestamp to get SAMPLE_RATE precission + let timestamp = aDate.getTime() - aDate.getTimezoneOffset() * 60 * 1000; + timestamp = Math.floor(timestamp / SAMPLE_RATE) * SAMPLE_RATE; + return timestamp; + }, + + saveStats: function saveStats(aStats, aResultCb) { + let isAccumulative = aStats.isAccumulative; + let timestamp = this.normalizeDate(aStats.date); + + let stats = { appId: aStats.appId, + isInBrowser: aStats.isInBrowser, + serviceType: aStats.serviceType, + networkId: aStats.networkId, + networkType: aStats.networkType, + timestamp: timestamp, + rxBytes: isAccumulative ? 0 : aStats.rxBytes, + txBytes: isAccumulative ? 0 : aStats.txBytes, + rxSystemBytes: isAccumulative ? aStats.rxBytes : 0, + txSystemBytes: isAccumulative ? aStats.txBytes : 0, + rxTotalBytes: isAccumulative ? aStats.rxBytes : 0, + txTotalBytes: isAccumulative ? aStats.txBytes : 0 }; + + stats = this.importData(stats); + + this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) { + if (DEBUG) { + debug("Filtered time: " + new Date(timestamp)); + debug("New stats: " + JSON.stringify(stats)); + } + + let lowerFilter = [stats.appId, stats.isInBrowser, stats.serviceType, + stats.network, 0]; + let upperFilter = [stats.appId, stats.isInBrowser, stats.serviceType, + stats.network, ""]; + let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); + + let request = aStore.openCursor(range, 'prev'); + request.onsuccess = function onsuccess(event) { + let cursor = event.target.result; + if (!cursor) { + // Empty, so save first element. + + if (!isAccumulative) { + this._saveStats(aTxn, aStore, stats); + return; + } + + // There could be a time delay between the point when the network + // interface comes up and the point when the database is initialized. + // In this short interval some traffic data are generated but are not + // registered by the first sample. + stats.rxBytes = stats.rxTotalBytes; + stats.txBytes = stats.txTotalBytes; + + // However, if the interface is not switched on after the database is + // initialized (dual sim use case) stats should be set to 0. + let req = aStore.index("network").openKeyCursor(null, "nextunique"); + req.onsuccess = function onsuccess(event) { + let cursor = event.target.result; + if (cursor) { + if (cursor.key[1] == stats.network[1]) { + stats.rxBytes = 0; + stats.txBytes = 0; + this._saveStats(aTxn, aStore, stats); + return; + } + + cursor.continue(); + return; + } + + this._saveStats(aTxn, aStore, stats); + }.bind(this); + + return; + } + + // There are old samples + if (DEBUG) { + debug("Last value " + JSON.stringify(cursor.value)); + } + + // Remove stats previous to now - VALUE_MAX_LENGTH + this._removeOldStats(aTxn, aStore, stats.appId, stats.isInBrowser, + stats.serviceType, stats.network, stats.timestamp); + + // Process stats before save + this._processSamplesDiff(aTxn, aStore, cursor, stats, isAccumulative); + }.bind(this); + }.bind(this), aResultCb); + }, + + /* + * This function check that stats are saved in the database following the sample rate. + * In this way is easier to find elements when stats are requested. + */ + _processSamplesDiff: function _processSamplesDiff(aTxn, + aStore, + aLastSampleCursor, + aNewSample, + aIsAccumulative) { + let lastSample = aLastSampleCursor.value; + + // Get difference between last and new sample. + let diff = (aNewSample.timestamp - lastSample.timestamp) / SAMPLE_RATE; + if (diff % 1) { + // diff is decimal, so some error happened because samples are stored as a multiple + // of SAMPLE_RATE + aTxn.abort(); + throw new Error("Error processing samples"); + } + + if (DEBUG) { + debug("New: " + aNewSample.timestamp + " - Last: " + + lastSample.timestamp + " - diff: " + diff); + } + + // If the incoming data has a accumulation feature, the new + // |txBytes|/|rxBytes| is assigend by differnces between the new + // |txTotalBytes|/|rxTotalBytes| and the last |txTotalBytes|/|rxTotalBytes|. + // Else, if incoming data is non-accumulative, the |txBytes|/|rxBytes| + // is the new |txBytes|/|rxBytes|. + let rxDiff = 0; + let txDiff = 0; + if (aIsAccumulative) { + rxDiff = aNewSample.rxSystemBytes - lastSample.rxSystemBytes; + txDiff = aNewSample.txSystemBytes - lastSample.txSystemBytes; + if (rxDiff < 0 || txDiff < 0) { + rxDiff = aNewSample.rxSystemBytes; + txDiff = aNewSample.txSystemBytes; + } + aNewSample.rxBytes = rxDiff; + aNewSample.txBytes = txDiff; + + aNewSample.rxTotalBytes = lastSample.rxTotalBytes + rxDiff; + aNewSample.txTotalBytes = lastSample.txTotalBytes + txDiff; + } else { + rxDiff = aNewSample.rxBytes; + txDiff = aNewSample.txBytes; + } + + if (diff == 1) { + // New element. + + // If the incoming data is non-accumulative, the new + // |rxTotalBytes|/|txTotalBytes| needs to be updated by adding new + // |rxBytes|/|txBytes| to the last |rxTotalBytes|/|txTotalBytes|. + if (!aIsAccumulative) { + aNewSample.rxTotalBytes = aNewSample.rxBytes + lastSample.rxTotalBytes; + aNewSample.txTotalBytes = aNewSample.txBytes + lastSample.txTotalBytes; + } + + this._saveStats(aTxn, aStore, aNewSample); + return; + } + if (diff > 1) { + // Some samples lost. Device off during one or more samplerate periods. + // Time or timezone changed + // Add lost samples with 0 bytes and the actual one. + if (diff > VALUES_MAX_LENGTH) { + diff = VALUES_MAX_LENGTH; + } + + let data = []; + for (let i = diff - 2; i >= 0; i--) { + let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1); + let sample = { appId: aNewSample.appId, + isInBrowser: aNewSample.isInBrowser, + serviceType: aNewSample.serviceType, + network: aNewSample.network, + timestamp: time, + rxBytes: 0, + txBytes: 0, + rxSystemBytes: lastSample.rxSystemBytes, + txSystemBytes: lastSample.txSystemBytes, + rxTotalBytes: lastSample.rxTotalBytes, + txTotalBytes: lastSample.txTotalBytes }; + + data.push(sample); + } + + data.push(aNewSample); + this._saveStats(aTxn, aStore, data); + return; + } + if (diff == 0 || diff < 0) { + // New element received before samplerate period. It means that device has + // been restarted (or clock / timezone change). + // Update element. If diff < 0, clock or timezone changed back. Place data + // in the last sample. + + // Old |rxTotalBytes|/|txTotalBytes| needs to get updated by adding the + // last |rxTotalBytes|/|txTotalBytes|. + lastSample.rxBytes += rxDiff; + lastSample.txBytes += txDiff; + lastSample.rxSystemBytes = aNewSample.rxSystemBytes; + lastSample.txSystemBytes = aNewSample.txSystemBytes; + lastSample.rxTotalBytes += rxDiff; + lastSample.txTotalBytes += txDiff; + + if (DEBUG) { + debug("Update: " + JSON.stringify(lastSample)); + } + let req = aLastSampleCursor.update(lastSample); + } + }, + + _saveStats: function _saveStats(aTxn, aStore, aNetworkStats) { + if (DEBUG) { + debug("_saveStats: " + JSON.stringify(aNetworkStats)); + } + + if (Array.isArray(aNetworkStats)) { + let len = aNetworkStats.length - 1; + for (let i = 0; i <= len; i++) { + aStore.put(aNetworkStats[i]); + } + } else { + aStore.put(aNetworkStats); + } + }, + + _removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aIsInBrowser, + aServiceType, aNetwork, aDate) { + // Callback function to remove old items when new ones are added. + let filterDate = aDate - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1); + let lowerFilter = [aAppId, aIsInBrowser, aServiceType, aNetwork, 0]; + let upperFilter = [aAppId, aIsInBrowser, aServiceType, aNetwork, filterDate]; + let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); + let lastSample = null; + let self = this; + + aStore.openCursor(range).onsuccess = function(event) { + var cursor = event.target.result; + if (cursor) { + lastSample = cursor.value; + cursor.delete(); + cursor.continue(); + return; + } + + // If all samples for a network are removed, an empty sample + // has to be saved to keep the totalBytes in order to compute + // future samples because system counters are not set to 0. + // Thus, if there are no samples left, the last sample removed + // will be saved again after setting its bytes to 0. + let request = aStore.index("network").openCursor(aNetwork); + request.onsuccess = function onsuccess(event) { + let cursor = event.target.result; + if (!cursor && lastSample != null) { + let timestamp = new Date(); + timestamp = self.normalizeDate(timestamp); + lastSample.timestamp = timestamp; + lastSample.rxBytes = 0; + lastSample.txBytes = 0; + self._saveStats(aTxn, aStore, lastSample); + } + }; + }; + }, + + clearInterfaceStats: function clearInterfaceStats(aNetwork, aResultCb) { + let network = [aNetwork.network.id, aNetwork.network.type]; + let self = this; + + // Clear and save an empty sample to keep sync with system counters + this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) { + let sample = null; + let request = aStore.index("network").openCursor(network, "prev"); + request.onsuccess = function onsuccess(event) { + let cursor = event.target.result; + if (cursor) { + if (!sample && cursor.value.appId == 0) { + sample = cursor.value; + } + + cursor.delete(); + cursor.continue(); + return; + } + + if (sample) { + let timestamp = new Date(); + timestamp = self.normalizeDate(timestamp); + sample.timestamp = timestamp; + sample.appId = 0; + sample.isInBrowser = 0; + sample.serviceType = ""; + sample.rxBytes = 0; + sample.txBytes = 0; + sample.rxTotalBytes = 0; + sample.txTotalBytes = 0; + + self._saveStats(aTxn, aStore, sample); + } + }; + }, this._resetAlarms.bind(this, aNetwork.networkId, aResultCb)); + }, + + clearStats: function clearStats(aNetworks, aResultCb) { + let index = 0; + let stats = []; + let self = this; + + let callback = function(aError, aResult) { + index++; + + if (!aError && index < aNetworks.length) { + self.clearInterfaceStats(aNetworks[index], callback); + return; + } + + aResultCb(aError, aResult); + }; + + if (!aNetworks[index]) { + aResultCb(null, true); + return; + } + this.clearInterfaceStats(aNetworks[index], callback); + }, + + getCurrentStats: function getCurrentStats(aNetwork, aDate, aResultCb) { + if (DEBUG) { + debug("Get current stats for " + JSON.stringify(aNetwork) + " since " + aDate); + } + + let network = [aNetwork.id, aNetwork.type]; + if (aDate) { + this._getCurrentStatsFromDate(network, aDate, aResultCb); + return; + } + + this._getCurrentStats(network, aResultCb); + }, + + _getCurrentStats: function _getCurrentStats(aNetwork, aResultCb) { + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) { + let request = null; + let upperFilter = [0, 1, "", aNetwork, Date.now()]; + let range = IDBKeyRange.upperBound(upperFilter, false); + let result = { rxBytes: 0, txBytes: 0, + rxTotalBytes: 0, txTotalBytes: 0 }; + + request = store.openCursor(range, "prev"); + + request.onsuccess = function onsuccess(event) { + let cursor = event.target.result; + if (cursor) { + result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes; + result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes; + } + + txn.result = result; + }; + }.bind(this), aResultCb); + }, + + _getCurrentStatsFromDate: function _getCurrentStatsFromDate(aNetwork, aDate, aResultCb) { + aDate = new Date(aDate); + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) { + let request = null; + let start = this.normalizeDate(aDate); + let upperFilter = [0, 1, "", aNetwork, Date.now()]; + let range = IDBKeyRange.upperBound(upperFilter, false); + let result = { rxBytes: 0, txBytes: 0, + rxTotalBytes: 0, txTotalBytes: 0 }; + + request = store.openCursor(range, "prev"); + + request.onsuccess = function onsuccess(event) { + let cursor = event.target.result; + if (cursor) { + result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes; + result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes; + } + + let timestamp = cursor.value.timestamp; + let range = IDBKeyRange.lowerBound(lowerFilter, false); + request = store.openCursor(range); + + request.onsuccess = function onsuccess(event) { + let cursor = event.target.result; + if (cursor) { + if (cursor.value.timestamp == timestamp) { + // There is one sample only. + result.rxBytes = cursor.value.rxBytes; + result.txBytes = cursor.value.txBytes; + } else { + result.rxBytes -= cursor.value.rxTotalBytes; + result.txBytes -= cursor.value.txTotalBytes; + } + } + + txn.result = result; + }; + }; + }.bind(this), aResultCb); + }, + + find: function find(aResultCb, aAppId, aBrowsingTrafficOnly, aServiceType, + aNetwork, aStart, aEnd, aAppManifestURL) { + let offset = (new Date()).getTimezoneOffset() * 60 * 1000; + let start = this.normalizeDate(aStart); + let end = this.normalizeDate(aEnd); + + if (DEBUG) { + debug("Find samples for appId: " + aAppId + + " browsingTrafficOnly: " + aBrowsingTrafficOnly + + " serviceType: " + aServiceType + + " network: " + JSON.stringify(aNetwork) + " from " + start + + " until " + end); + debug("Start time: " + new Date(start)); + debug("End time: " + new Date(end)); + } + + // Find samples of browsing traffic (isInBrowser = 1) first since they are + // needed no matter browsingTrafficOnly is true or false. + // We have to make two queries to database because we cannot filter correct + // records by a single query that sets ranges for two keys (isInBrowser and + // timestamp). We think it is because the keyPath contains an array + // (network) so such query does not work. + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { + let network = [aNetwork.id, aNetwork.type]; + let lowerFilter = [aAppId, 1, aServiceType, network, start]; + let upperFilter = [aAppId, 1, aServiceType, network, end]; + let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); + + let data = []; + + if (!aTxn.result) { + aTxn.result = {}; + } + aTxn.result.appManifestURL = aAppManifestURL; + aTxn.result.browsingTrafficOnly = aBrowsingTrafficOnly; + aTxn.result.serviceType = aServiceType; + aTxn.result.network = aNetwork; + aTxn.result.start = aStart; + aTxn.result.end = aEnd; + + let request = aStore.openCursor(range).onsuccess = function(event) { + var cursor = event.target.result; + if (cursor){ + // We use rxTotalBytes/txTotalBytes instead of rxBytes/txBytes for + // the first (oldest) sample. The rx/txTotalBytes fields record + // accumulative usage amount, which means even if old samples were + // expired and removed from the Database, we can still obtain the + // correct network usage. + if (data.length == 0) { + data.push({ rxBytes: cursor.value.rxTotalBytes, + txBytes: cursor.value.txTotalBytes, + date: new Date(cursor.value.timestamp + offset) }); + } else { + data.push({ rxBytes: cursor.value.rxBytes, + txBytes: cursor.value.txBytes, + date: new Date(cursor.value.timestamp + offset) }); + } + cursor.continue(); + return; + } + + if (aBrowsingTrafficOnly) { + this.fillResultSamples(start + offset, end + offset, data); + aTxn.result.data = data; + return; + } + + // Find samples of app traffic (isInBrowser = 0) as well if + // browsingTrafficOnly is false. + lowerFilter = [aAppId, 0, aServiceType, network, start]; + upperFilter = [aAppId, 0, aServiceType, network, end]; + range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); + request = aStore.openCursor(range).onsuccess = function(event) { + cursor = event.target.result; + if (cursor) { + var date = new Date(cursor.value.timestamp + offset); + var foundData = data.find(function (element, index, array) { + if (element.date.getTime() !== date.getTime()) { + return false; + } + return element; + }, date); + + if (foundData) { + foundData.rxBytes += cursor.value.rxBytes; + foundData.txBytes += cursor.value.txBytes; + } else { + // We use rxTotalBytes/txTotalBytes instead of rxBytes/txBytes + // for the first (oldest) sample. The rx/txTotalBytes fields + // record accumulative usage amount, which means even if old + // samples were expired and removed from the Database, we can + // still obtain the correct network usage. + if (data.length == 0) { + data.push({ rxBytes: cursor.value.rxTotalBytes, + txBytes: cursor.value.txTotalBytes, + date: new Date(cursor.value.timestamp + offset) }); + } else { + data.push({ rxBytes: cursor.value.rxBytes, + txBytes: cursor.value.txBytes, + date: new Date(cursor.value.timestamp + offset) }); + } + } + cursor.continue(); + return; + } + this.fillResultSamples(start + offset, end + offset, data); + aTxn.result.data = data; + }.bind(this); // openCursor(range).onsuccess() callback + }.bind(this); // openCursor(range).onsuccess() callback + }.bind(this), aResultCb); + }, + + /* + * Fill data array (samples from database) with empty samples to match + * requested start / end dates. + */ + fillResultSamples: function fillResultSamples(aStart, aEnd, aData) { + if (aData.length == 0) { + aData.push({ rxBytes: undefined, + txBytes: undefined, + date: new Date(aStart) }); + } + + while (aStart < aData[0].date.getTime()) { + aData.unshift({ rxBytes: undefined, + txBytes: undefined, + date: new Date(aData[0].date.getTime() - SAMPLE_RATE) }); + } + + while (aEnd > aData[aData.length - 1].date.getTime()) { + aData.push({ rxBytes: undefined, + txBytes: undefined, + date: new Date(aData[aData.length - 1].date.getTime() + SAMPLE_RATE) }); + } + }, + + getAvailableNetworks: function getAvailableNetworks(aResultCb) { + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { + if (!aTxn.result) { + aTxn.result = []; + } + + let request = aStore.index("network").openKeyCursor(null, "nextunique"); + request.onsuccess = function onsuccess(event) { + let cursor = event.target.result; + if (cursor) { + aTxn.result.push({ id: cursor.key[0], + type: cursor.key[1] }); + cursor.continue(); + return; + } + }; + }, aResultCb); + }, + + isNetworkAvailable: function isNetworkAvailable(aNetwork, aResultCb) { + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { + if (!aTxn.result) { + aTxn.result = false; + } + + let network = [aNetwork.id, aNetwork.type]; + let request = aStore.index("network").openKeyCursor(IDBKeyRange.only(network)); + request.onsuccess = function onsuccess(event) { + if (event.target.result) { + aTxn.result = true; + } + }; + }, aResultCb); + }, + + getAvailableServiceTypes: function getAvailableServiceTypes(aResultCb) { + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { + if (!aTxn.result) { + aTxn.result = []; + } + + let request = aStore.index("serviceType").openKeyCursor(null, "nextunique"); + request.onsuccess = function onsuccess(event) { + let cursor = event.target.result; + if (cursor && cursor.key != "") { + aTxn.result.push({ serviceType: cursor.key }); + cursor.continue(); + return; + } + }; + }, aResultCb); + }, + + get sampleRate () { + return SAMPLE_RATE; + }, + + get maxStorageSamples () { + return VALUES_MAX_LENGTH; + }, + + logAllRecords: function logAllRecords(aResultCb) { + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { + aStore.mozGetAll().onsuccess = function onsuccess(event) { + aTxn.result = event.target.result; + }; + }, aResultCb); + }, + + alarmToRecord: function alarmToRecord(aAlarm) { + let record = { networkId: aAlarm.networkId, + absoluteThreshold: aAlarm.absoluteThreshold, + relativeThreshold: aAlarm.relativeThreshold, + startTime: aAlarm.startTime, + data: aAlarm.data, + manifestURL: aAlarm.manifestURL, + pageURL: aAlarm.pageURL }; + + if (aAlarm.id) { + record.id = aAlarm.id; + } + + return record; + }, + + recordToAlarm: function recordToalarm(aRecord) { + let alarm = { networkId: aRecord.networkId, + absoluteThreshold: aRecord.absoluteThreshold, + relativeThreshold: aRecord.relativeThreshold, + startTime: aRecord.startTime, + data: aRecord.data, + manifestURL: aRecord.manifestURL, + pageURL: aRecord.pageURL }; + + if (aRecord.id) { + alarm.id = aRecord.id; + } + + return alarm; + }, + + addAlarm: function addAlarm(aAlarm, aResultCb) { + this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { + if (DEBUG) { + debug("Going to add " + JSON.stringify(aAlarm)); + } + + let record = this.alarmToRecord(aAlarm); + store.put(record).onsuccess = function setResult(aEvent) { + txn.result = aEvent.target.result; + if (DEBUG) { + debug("Request successful. New record ID: " + txn.result); + } + }; + }.bind(this), aResultCb); + }, + + getFirstAlarm: function getFirstAlarm(aNetworkId, aResultCb) { + let self = this; + + this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { + if (DEBUG) { + debug("Get first alarm for network " + aNetworkId); + } + + let lowerFilter = [aNetworkId, 0]; + let upperFilter = [aNetworkId, ""]; + let range = IDBKeyRange.bound(lowerFilter, upperFilter); + + store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) { + let cursor = event.target.result; + txn.result = null; + if (cursor) { + txn.result = self.recordToAlarm(cursor.value); + } + }; + }, aResultCb); + }, + + removeAlarm: function removeAlarm(aAlarmId, aManifestURL, aResultCb) { + this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { + if (DEBUG) { + debug("Remove alarm " + aAlarmId); + } + + store.get(aAlarmId).onsuccess = function onsuccess(event) { + let record = event.target.result; + txn.result = false; + if (!record || (aManifestURL && record.manifestURL != aManifestURL)) { + return; + } + + store.delete(aAlarmId); + txn.result = true; + } + }, aResultCb); + }, + + removeAlarms: function removeAlarms(aManifestURL, aResultCb) { + this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { + if (DEBUG) { + debug("Remove alarms of " + aManifestURL); + } + + store.index("manifestURL").openCursor(aManifestURL) + .onsuccess = function onsuccess(event) { + let cursor = event.target.result; + if (cursor) { + cursor.delete(); + cursor.continue(); + } + } + }, aResultCb); + }, + + updateAlarm: function updateAlarm(aAlarm, aResultCb) { + let self = this; + this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { + if (DEBUG) { + debug("Update alarm " + aAlarm.id); + } + + let record = self.alarmToRecord(aAlarm); + store.openCursor(record.id).onsuccess = function onsuccess(event) { + let cursor = event.target.result; + txn.result = false; + if (cursor) { + cursor.update(record); + txn.result = true; + } + } + }, aResultCb); + }, + + getAlarms: function getAlarms(aNetworkId, aManifestURL, aResultCb) { + let self = this; + this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { + if (DEBUG) { + debug("Get alarms for " + aManifestURL); + } + + txn.result = []; + store.index("manifestURL").openCursor(aManifestURL) + .onsuccess = function onsuccess(event) { + let cursor = event.target.result; + if (!cursor) { + return; + } + + if (!aNetworkId || cursor.value.networkId == aNetworkId) { + txn.result.push(self.recordToAlarm(cursor.value)); + } + + cursor.continue(); + } + }, aResultCb); + }, + + _resetAlarms: function _resetAlarms(aNetworkId, aResultCb) { + this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { + if (DEBUG) { + debug("Reset alarms for network " + aNetworkId); + } + + let lowerFilter = [aNetworkId, 0]; + let upperFilter = [aNetworkId, ""]; + let range = IDBKeyRange.bound(lowerFilter, upperFilter); + + store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) { + let cursor = event.target.result; + if (cursor) { + if (cursor.value.startTime) { + cursor.value.relativeThreshold = cursor.value.threshold; + cursor.update(cursor.value); + } + cursor.continue(); + return; + } + }; + }, aResultCb); + } +}; diff --git a/dom/network/NetworkStatsManager.js b/dom/network/NetworkStatsManager.js new file mode 100644 index 0000000000..b963aba2b5 --- /dev/null +++ b/dom/network/NetworkStatsManager.js @@ -0,0 +1,388 @@ +/* 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 DEBUG = false; +function debug(s) { dump("-*- NetworkStatsManager: " + s + "\n"); } + +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/DOMRequestHelper.jsm"); + +// Ensure NetworkStatsService and NetworkStatsDB are loaded in the parent process +// to receive messages from the child processes. +var appInfo = Cc["@mozilla.org/xre/app-info;1"]; +var isParentProcess = !appInfo || appInfo.getService(Ci.nsIXULRuntime) + .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +if (isParentProcess) { + Cu.import("resource://gre/modules/NetworkStatsService.jsm"); +} + +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", + "nsISyncMessageSender"); + +// NetworkStatsData +const nsIClassInfo = Ci.nsIClassInfo; +const NETWORKSTATSDATA_CID = Components.ID("{3b16fe17-5583-483a-b486-b64a3243221c}"); + +function NetworkStatsData(aWindow, aData) { + this.rxBytes = aData.rxBytes; + this.txBytes = aData.txBytes; + this.date = new aWindow.Date(aData.date.getTime()); +} + +NetworkStatsData.prototype = { + classID : NETWORKSTATSDATA_CID, + + QueryInterface : XPCOMUtils.generateQI([]) +}; + +// NetworkStatsInterface +const NETWORKSTATSINTERFACE_CONTRACTID = "@mozilla.org/networkstatsinterface;1"; +const NETWORKSTATSINTERFACE_CID = Components.ID("{f540615b-d803-43ff-8200-2a9d145a5645}"); + +function NetworkStatsInterface() { + if (DEBUG) { + debug("NetworkStatsInterface Constructor"); + } +} + +NetworkStatsInterface.prototype = { + __init: function(aNetwork) { + this.type = aNetwork.type; + this.id = aNetwork.id; + }, + + classID : NETWORKSTATSINTERFACE_CID, + + contractID: NETWORKSTATSINTERFACE_CONTRACTID, + QueryInterface : XPCOMUtils.generateQI([]) +} + +// NetworkStats +const NETWORKSTATS_CID = Components.ID("{28904f59-8497-4ac0-904f-2af14b7fd3de}"); + +function NetworkStats(aWindow, aStats) { + if (DEBUG) { + debug("NetworkStats Constructor"); + } + this.appManifestURL = aStats.appManifestURL || null; + this.browsingTrafficOnly = aStats.browsingTrafficOnly || false; + this.serviceType = aStats.serviceType || null; + this.network = new aWindow.MozNetworkStatsInterface(aStats.network); + this.start = aStats.start ? new aWindow.Date(aStats.start.getTime()) : null; + this.end = aStats.end ? new aWindow.Date(aStats.end.getTime()) : null; + + let samples = this.data = new aWindow.Array(); + for (let i = 0; i < aStats.data.length; i++) { + samples.push(aWindow.MozNetworkStatsData._create( + aWindow, new NetworkStatsData(aWindow, aStats.data[i]))); + } +} + +NetworkStats.prototype = { + classID : NETWORKSTATS_CID, + + QueryInterface : XPCOMUtils.generateQI() +} + +// NetworkStatsAlarm +const NETWORKSTATSALARM_CID = Components.ID("{a93ea13e-409c-4189-9b1e-95fff220be55}"); + +function NetworkStatsAlarm(aWindow, aAlarm) { + this.alarmId = aAlarm.id; + this.network = new aWindow.MozNetworkStatsInterface(aAlarm.network); + this.threshold = aAlarm.threshold; + this.data = aAlarm.data; +} + +NetworkStatsAlarm.prototype = { + classID : NETWORKSTATSALARM_CID, + + QueryInterface : XPCOMUtils.generateQI([]) +}; + +// NetworkStatsManager + +const NETWORKSTATSMANAGER_CONTRACTID = "@mozilla.org/networkStatsManager;1"; +const NETWORKSTATSMANAGER_CID = Components.ID("{ceb874cd-cc1a-4e65-b404-cc2d3e42425f}"); + +function NetworkStatsManager() { + if (DEBUG) { + debug("Constructor"); + } +} + +NetworkStatsManager.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + + getSamples: function getSamples(aNetwork, aStart, aEnd, aOptions) { + if (aStart > aEnd) { + throw Components.results.NS_ERROR_INVALID_ARG; + } + + // appManifestURL is used to query network statistics by app; + // serviceType is used to query network statistics by system service. + // It is illegal to specify both of them at the same time. + if (aOptions.appManifestURL && aOptions.serviceType) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } + // browsingTrafficOnly is meaningful only when querying by app. + if (!aOptions.appManifestURL && aOptions.browsingTrafficOnly) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } + + let appManifestURL = aOptions.appManifestURL; + let serviceType = aOptions.serviceType; + let browsingTrafficOnly = aOptions.browsingTrafficOnly; + + // TODO Bug 929410 Date object cannot correctly pass through cpmm/ppmm IPC + // This is just a work-around by passing timestamp numbers. + aStart = aStart.getTime(); + aEnd = aEnd.getTime(); + + let request = this.createRequest(); + cpmm.sendAsyncMessage("NetworkStats:Get", + { network: aNetwork.toJSON(), + start: aStart, + end: aEnd, + appManifestURL: appManifestURL, + browsingTrafficOnly: browsingTrafficOnly, + serviceType: serviceType, + id: this.getRequestId(request) }); + return request; + }, + + clearStats: function clearStats(aNetwork) { + let request = this.createRequest(); + cpmm.sendAsyncMessage("NetworkStats:Clear", + { network: aNetwork.toJSON(), + id: this.getRequestId(request) }); + return request; + }, + + clearAllStats: function clearAllStats() { + let request = this.createRequest(); + cpmm.sendAsyncMessage("NetworkStats:ClearAll", + {id: this.getRequestId(request)}); + return request; + }, + + addAlarm: function addAlarm(aNetwork, aThreshold, aOptions) { + let request = this.createRequest(); + cpmm.sendAsyncMessage("NetworkStats:SetAlarm", + {id: this.getRequestId(request), + data: {network: aNetwork.toJSON(), + threshold: aThreshold, + startTime: aOptions.startTime, + data: aOptions.data, + manifestURL: this.manifestURL, + pageURL: this.pageURL}}); + return request; + }, + + getAllAlarms: function getAllAlarms(aNetwork) { + let network = null; + if (aNetwork) { + network = aNetwork.toJSON(); + } + + let request = this.createRequest(); + cpmm.sendAsyncMessage("NetworkStats:GetAlarms", + {id: this.getRequestId(request), + data: {network: network, + manifestURL: this.manifestURL}}); + return request; + }, + + removeAlarms: function removeAlarms(aAlarmId) { + if (aAlarmId == 0) { + aAlarmId = -1; + } + + let request = this.createRequest(); + cpmm.sendAsyncMessage("NetworkStats:RemoveAlarms", + {id: this.getRequestId(request), + data: {alarmId: aAlarmId, + manifestURL: this.manifestURL}}); + + return request; + }, + + getAvailableNetworks: function getAvailableNetworks() { + let request = this.createRequest(); + cpmm.sendAsyncMessage("NetworkStats:GetAvailableNetworks", + { id: this.getRequestId(request) }); + return request; + }, + + getAvailableServiceTypes: function getAvailableServiceTypes() { + let request = this.createRequest(); + cpmm.sendAsyncMessage("NetworkStats:GetAvailableServiceTypes", + { id: this.getRequestId(request) }); + return request; + }, + + get sampleRate() { + return cpmm.sendSyncMessage("NetworkStats:SampleRate")[0]; + }, + + get maxStorageAge() { + return cpmm.sendSyncMessage("NetworkStats:MaxStorageAge")[0]; + }, + + receiveMessage: function(aMessage) { + if (DEBUG) { + debug("NetworkStatsmanager::receiveMessage: " + aMessage.name); + } + + let msg = aMessage.json; + let req = this.takeRequest(msg.id); + if (!req) { + if (DEBUG) { + debug("No request stored with id " + msg.id); + } + return; + } + + switch (aMessage.name) { + case "NetworkStats:Get:Return": + if (msg.error) { + Services.DOMRequest.fireError(req, msg.error); + return; + } + + let result = this._window.MozNetworkStats._create( + this._window, new NetworkStats(this._window, msg.result)); + if (DEBUG) { + debug("result: " + JSON.stringify(result)); + } + Services.DOMRequest.fireSuccess(req, result); + break; + + case "NetworkStats:GetAvailableNetworks:Return": + if (msg.error) { + Services.DOMRequest.fireError(req, msg.error); + return; + } + + let networks = new this._window.Array(); + for (let i = 0; i < msg.result.length; i++) { + let network = new this._window.MozNetworkStatsInterface(msg.result[i]); + networks.push(network); + } + + Services.DOMRequest.fireSuccess(req, networks); + break; + + case "NetworkStats:GetAvailableServiceTypes:Return": + if (msg.error) { + Services.DOMRequest.fireError(req, msg.error); + return; + } + + let serviceTypes = new this._window.Array(); + for (let i = 0; i < msg.result.length; i++) { + serviceTypes.push(msg.result[i]); + } + + Services.DOMRequest.fireSuccess(req, serviceTypes); + break; + + case "NetworkStats:Clear:Return": + case "NetworkStats:ClearAll:Return": + if (msg.error) { + Services.DOMRequest.fireError(req, msg.error); + return; + } + + Services.DOMRequest.fireSuccess(req, true); + break; + + case "NetworkStats:SetAlarm:Return": + case "NetworkStats:RemoveAlarms:Return": + if (msg.error) { + Services.DOMRequest.fireError(req, msg.error); + return; + } + + Services.DOMRequest.fireSuccess(req, msg.result); + break; + + case "NetworkStats:GetAlarms:Return": + if (msg.error) { + Services.DOMRequest.fireError(req, msg.error); + return; + } + + let alarms = new this._window.Array(); + for (let i = 0; i < msg.result.length; i++) { + // The WebIDL type of data is any, so we should manually clone it + // into the content window. + if ("data" in msg.result[i]) { + msg.result[i].data = Cu.cloneInto(msg.result[i].data, this._window); + } + let alarm = new NetworkStatsAlarm(this._window, msg.result[i]); + alarms.push(this._window.MozNetworkStatsAlarm._create(this._window, alarm)); + } + + Services.DOMRequest.fireSuccess(req, alarms); + break; + + default: + if (DEBUG) { + debug("Wrong message: " + aMessage.name); + } + } + }, + + init: function(aWindow) { + let principal = aWindow.document.nodePrincipal; + + this.initDOMRequestHelper(aWindow, ["NetworkStats:Get:Return", + "NetworkStats:GetAvailableNetworks:Return", + "NetworkStats:GetAvailableServiceTypes:Return", + "NetworkStats:Clear:Return", + "NetworkStats:ClearAll:Return", + "NetworkStats:SetAlarm:Return", + "NetworkStats:GetAlarms:Return", + "NetworkStats:RemoveAlarms:Return"]); + + // Init app properties. + let appsService = Cc["@mozilla.org/AppsService;1"] + .getService(Ci.nsIAppsService); + + this.manifestURL = appsService.getManifestURLByLocalId(principal.appId); + + let isApp = !!this.manifestURL.length; + if (isApp) { + this.pageURL = principal.URI.spec; + } + + this.window = aWindow; + }, + + // Called from DOMRequestIpcHelper + uninit: function uninit() { + if (DEBUG) { + debug("uninit call"); + } + }, + + classID : NETWORKSTATSMANAGER_CID, + contractID : NETWORKSTATSMANAGER_CONTRACTID, + QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer, + Ci.nsISupportsWeakReference, + Ci.nsIObserver]), +} + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkStatsAlarm, + NetworkStatsData, + NetworkStatsInterface, + NetworkStats, + NetworkStatsManager]); diff --git a/dom/network/NetworkStatsManager.manifest b/dom/network/NetworkStatsManager.manifest new file mode 100644 index 0000000000..8e8700910f --- /dev/null +++ b/dom/network/NetworkStatsManager.manifest @@ -0,0 +1,14 @@ +component {3b16fe17-5583-483a-b486-b64a3243221c} NetworkStatsManager.js +contract @mozilla.org/networkStatsdata;1 {3b16fe17-5583-483a-b486-b64a3243221c} + +component {28904f59-8497-4ac0-904f-2af14b7fd3de} NetworkStatsManager.js +contract @mozilla.org/networkStats;1 {28904f59-8497-4ac0-904f-2af14b7fd3de} + +component {f540615b-d803-43ff-8200-2a9d145a5645} NetworkStatsManager.js +contract @mozilla.org/networkstatsinterface;1 {f540615b-d803-43ff-8200-2a9d145a5645} + +component {a93ea13e-409c-4189-9b1e-95fff220be55} NetworkStatsManager.js +contract @mozilla.org/networkstatsalarm;1 {a93ea13e-409c-4189-9b1e-95fff220be55} + +component {ceb874cd-cc1a-4e65-b404-cc2d3e42425f} NetworkStatsManager.js +contract @mozilla.org/networkStatsManager;1 {ceb874cd-cc1a-4e65-b404-cc2d3e42425f} diff --git a/dom/network/NetworkStatsService.jsm b/dom/network/NetworkStatsService.jsm new file mode 100644 index 0000000000..4b6d694988 --- /dev/null +++ b/dom/network/NetworkStatsService.jsm @@ -0,0 +1,1171 @@ +/* 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 DEBUG = false; +function debug(s) { + if (DEBUG) { + dump("-*- NetworkStatsService: " + s + "\n"); + } +} + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +this.EXPORTED_SYMBOLS = ["NetworkStatsService"]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetworkStatsDB.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); + +const NET_NETWORKSTATSSERVICE_CONTRACTID = "@mozilla.org/network/netstatsservice;1"; +const NET_NETWORKSTATSSERVICE_CID = Components.ID("{18725604-e9ac-488a-8aa0-2471e7f6c0a4}"); + +const TOPIC_BANDWIDTH_CONTROL = "netd-bandwidth-control" + +const TOPIC_CONNECTION_STATE_CHANGED = "network-connection-state-changed"; +const NET_TYPE_WIFI = Ci.nsINetworkInfo.NETWORK_TYPE_WIFI; +const NET_TYPE_MOBILE = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE; + +// Networks have different status that NetworkStats API needs to be aware of. +// Network is present and ready, so NetworkManager provides the whole info. +const NETWORK_STATUS_READY = 0; +// Network is present but hasn't established a connection yet (e.g. SIM that has not +// enabled 3G since boot). +const NETWORK_STATUS_STANDBY = 1; +// Network is not present, but stored in database by the previous connections. +const NETWORK_STATUS_AWAY = 2; + +// The maximum traffic amount can be saved in the |cachedStats|. +const MAX_CACHED_TRAFFIC = 500 * 1000 * 1000; // 500 MB + +const QUEUE_TYPE_UPDATE_STATS = 0; +const QUEUE_TYPE_UPDATE_CACHE = 1; +const QUEUE_TYPE_WRITE_CACHE = 2; + +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageListenerManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "gRil", + "@mozilla.org/ril;1", + "nsIRadioInterfaceLayer"); + +XPCOMUtils.defineLazyServiceGetter(this, "networkService", + "@mozilla.org/network/service;1", + "nsINetworkService"); + +XPCOMUtils.defineLazyServiceGetter(this, "appsService", + "@mozilla.org/AppsService;1", + "nsIAppsService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", + "@mozilla.org/settingsService;1", + "nsISettingsService"); + +XPCOMUtils.defineLazyServiceGetter(this, "messenger", + "@mozilla.org/system-message-internal;1", + "nsISystemMessagesInternal"); + +XPCOMUtils.defineLazyServiceGetter(this, "gIccService", + "@mozilla.org/icc/iccservice;1", + "nsIIccService"); + +this.NetworkStatsService = { + init: function() { + debug("Service started"); + + Services.obs.addObserver(this, "xpcom-shutdown", false); + Services.obs.addObserver(this, TOPIC_CONNECTION_STATE_CHANGED, false); + Services.obs.addObserver(this, TOPIC_BANDWIDTH_CONTROL, false); + Services.obs.addObserver(this, "profile-after-change", false); + + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + // Object to store network interfaces, each network interface is composed + // by a network object (network type and network Id) and a interfaceName + // that contains the name of the physical interface (wlan0, rmnet0, etc.). + // The network type can be 0 for wifi or 1 for mobile. On the other hand, + // the network id is '0' for wifi or the iccid for mobile (SIM). + // Each networkInterface is placed in the _networks object by the index of + // 'networkId + networkType'. + // + // _networks object allows to map available network interfaces at low level + // (wlan0, rmnet0, etc.) to a network. It's not mandatory to have a + // networkInterface per network but can't exist a networkInterface not + // being mapped to a network. + + this._networks = Object.create(null); + + // There is no way to know a priori if wifi connection is available, + // just when the wifi driver is loaded, but it is unloaded when + // wifi is switched off. So wifi connection is hardcoded + let netId = this.getNetworkId('0', NET_TYPE_WIFI); + this._networks[netId] = { network: { id: '0', + type: NET_TYPE_WIFI }, + interfaceName: null, + status: NETWORK_STATUS_STANDBY }; + + this.messages = ["NetworkStats:Get", + "NetworkStats:Clear", + "NetworkStats:ClearAll", + "NetworkStats:SetAlarm", + "NetworkStats:GetAlarms", + "NetworkStats:RemoveAlarms", + "NetworkStats:GetAvailableNetworks", + "NetworkStats:GetAvailableServiceTypes", + "NetworkStats:SampleRate", + "NetworkStats:MaxStorageAge"]; + + this.messages.forEach(function(aMsgName) { + ppmm.addMessageListener(aMsgName, this); + }, this); + + this._db = new NetworkStatsDB(); + + // Stats for all interfaces are updated periodically + this.timer.initWithCallback(this, this._db.sampleRate, + Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP); + + // Stats not from netd are firstly stored in the cached. + this.cachedStats = Object.create(null); + this.cachedStatsDate = new Date(); + + this.updateQueue = []; + this.isQueueRunning = false; + + this._currentAlarms = {}; + this.initAlarms(); + }, + + receiveMessage: function(aMessage) { + if (!aMessage.target.assertPermission("networkstats-manage")) { + return; + } + + debug("receiveMessage " + aMessage.name); + + let mm = aMessage.target; + let msg = aMessage.json; + + switch (aMessage.name) { + case "NetworkStats:Get": + this.getSamples(mm, msg); + break; + case "NetworkStats:Clear": + this.clearInterfaceStats(mm, msg); + break; + case "NetworkStats:ClearAll": + this.clearDB(mm, msg); + break; + case "NetworkStats:SetAlarm": + this.setAlarm(mm, msg); + break; + case "NetworkStats:GetAlarms": + this.getAlarms(mm, msg); + break; + case "NetworkStats:RemoveAlarms": + this.removeAlarms(mm, msg); + break; + case "NetworkStats:GetAvailableNetworks": + this.getAvailableNetworks(mm, msg); + break; + case "NetworkStats:GetAvailableServiceTypes": + this.getAvailableServiceTypes(mm, msg); + break; + case "NetworkStats:SampleRate": + // This message is sync. + return this._db.sampleRate; + case "NetworkStats:MaxStorageAge": + // This message is sync. + return this._db.maxStorageSamples * this._db.sampleRate; + } + }, + + observe: function observe(aSubject, aTopic, aData) { + switch (aTopic) { + case TOPIC_CONNECTION_STATE_CHANGED: + + // If new interface is registered (notified from NetworkService), + // the stats are updated for the new interface without waiting to + // complete the updating period. + + let networkInfo = aSubject.QueryInterface(Ci.nsINetworkInfo); + debug("Network " + networkInfo.name + " of type " + networkInfo.type + " status change"); + + let netId = this.convertNetworkInfo(networkInfo); + if (!netId) { + break; + } + + this._updateCurrentAlarm(netId); + + debug("NetId: " + netId); + this.updateStats(netId); + break; + + case TOPIC_BANDWIDTH_CONTROL: + debug("Bandwidth message from netd: " + JSON.stringify(aData)); + + let interfaceName = aData.substring(aData.lastIndexOf(" ") + 1); + for (let networkId in this._networks) { + if (interfaceName == this._networks[networkId].interfaceName) { + let currentAlarm = this._currentAlarms[networkId]; + if (Object.getOwnPropertyNames(currentAlarm).length !== 0) { + this._fireAlarm(currentAlarm.alarm); + } + break; + } + } + break; + + case "xpcom-shutdown": + debug("Service shutdown"); + + this.messages.forEach(function(aMsgName) { + ppmm.removeMessageListener(aMsgName, this); + }, this); + + Services.obs.removeObserver(this, "xpcom-shutdown"); + Services.obs.removeObserver(this, "profile-after-change"); + Services.obs.removeObserver(this, TOPIC_CONNECTION_STATE_CHANGED); + Services.obs.removeObserver(this, TOPIC_BANDWIDTH_CONTROL); + + this.timer.cancel(); + this.timer = null; + + // Update stats before shutdown + this.updateAllStats(); + break; + } + }, + + /* + * nsITimerCallback + * Timer triggers the update of all stats + */ + notify: function(aTimer) { + this.updateAllStats(); + }, + + /* + * nsINetworkStatsService + */ + getRilNetworks: function() { + let networks = {}; + let numRadioInterfaces = gRil.numRadioInterfaces; + for (let i = 0; i < numRadioInterfaces; i++) { + let icc = gIccService.getIccByServiceId(i); + let radioInterface = gRil.getRadioInterface(i); + if (icc && icc.iccInfo) { + let netId = this.getNetworkId(icc.iccInfo.iccid, + NET_TYPE_MOBILE); + networks[netId] = { id : icc.iccInfo.iccid, + type: NET_TYPE_MOBILE }; + } + } + return networks; + }, + + convertNetworkInfo: function(aNetworkInfo) { + if (aNetworkInfo.type != NET_TYPE_MOBILE && + aNetworkInfo.type != NET_TYPE_WIFI) { + return null; + } + + let id = '0'; + if (aNetworkInfo.type == NET_TYPE_MOBILE) { + if (!(aNetworkInfo instanceof Ci.nsIRilNetworkInfo)) { + debug("Error! Mobile network should be an nsIRilNetworkInfo!"); + return null; + } + + let rilNetwork = aNetworkInfo.QueryInterface(Ci.nsIRilNetworkInfo); + id = rilNetwork.iccId; + } + + let netId = this.getNetworkId(id, aNetworkInfo.type); + + if (!this._networks[netId]) { + this._networks[netId] = Object.create(null); + this._networks[netId].network = { id: id, + type: aNetworkInfo.type }; + } + + this._networks[netId].status = NETWORK_STATUS_READY; + this._networks[netId].interfaceName = aNetworkInfo.name; + return netId; + }, + + getNetworkId: function getNetworkId(aIccId, aNetworkType) { + return aIccId + '' + aNetworkType; + }, + + /* Function to ensure that one network is valid. The network is valid if its status is + * NETWORK_STATUS_READY, NETWORK_STATUS_STANDBY or NETWORK_STATUS_AWAY. + * + * The result is |netId| or null in case of a non-valid network + * aCallback is signatured as |function(netId)|. + */ + validateNetwork: function validateNetwork(aNetwork, aCallback) { + let netId = this.getNetworkId(aNetwork.id, aNetwork.type); + + if (this._networks[netId]) { + aCallback(netId); + return; + } + + // Check if network is valid (RIL entry) but has not established a connection yet. + // If so add to networks list with empty interfaceName. + let rilNetworks = this.getRilNetworks(); + if (rilNetworks[netId]) { + this._networks[netId] = Object.create(null); + this._networks[netId].network = rilNetworks[netId]; + this._networks[netId].status = NETWORK_STATUS_STANDBY; + this._currentAlarms[netId] = Object.create(null); + aCallback(netId); + return; + } + + // Check if network is available in the DB. + this._db.isNetworkAvailable(aNetwork, function(aError, aResult) { + if (aResult) { + this._networks[netId] = Object.create(null); + this._networks[netId].network = aNetwork; + this._networks[netId].status = NETWORK_STATUS_AWAY; + this._currentAlarms[netId] = Object.create(null); + aCallback(netId); + return; + } + + aCallback(null); + }.bind(this)); + }, + + getAvailableNetworks: function getAvailableNetworks(mm, msg) { + let self = this; + let rilNetworks = this.getRilNetworks(); + this._db.getAvailableNetworks(function onGetNetworks(aError, aResult) { + + // Also return the networks that are valid but have not + // established connections yet. + for (let netId in rilNetworks) { + let found = false; + for (let i = 0; i < aResult.length; i++) { + if (netId == self.getNetworkId(aResult[i].id, aResult[i].type)) { + found = true; + break; + } + } + if (!found) { + aResult.push(rilNetworks[netId]); + } + } + + mm.sendAsyncMessage("NetworkStats:GetAvailableNetworks:Return", + { id: msg.id, error: aError, result: aResult }); + }); + }, + + getAvailableServiceTypes: function getAvailableServiceTypes(mm, msg) { + this._db.getAvailableServiceTypes(function onGetServiceTypes(aError, aResult) { + mm.sendAsyncMessage("NetworkStats:GetAvailableServiceTypes:Return", + { id: msg.id, error: aError, result: aResult }); + }); + }, + + initAlarms: function initAlarms() { + debug("Init usage alarms"); + let self = this; + + for (let netId in this._networks) { + this._currentAlarms[netId] = Object.create(null); + + this._db.getFirstAlarm(netId, function getResult(error, result) { + if (!error && result) { + self._setAlarm(result, function onSet(error, success) { + if (error == "InvalidStateError") { + self._fireAlarm(result); + } + }); + } + }); + } + }, + + /* + * Function called from manager to get stats from database. + * In order to return updated stats, first is performed a call to + * updateAllStats function, which will get last stats from netd + * and update the database. + * Then, depending on the request (stats per appId or total stats) + * it retrieve them from database and return to the manager. + */ + getSamples: function getSamples(mm, msg) { + let network = msg.network; + let netId = this.getNetworkId(network.id, network.type); + + let appId = 0; + let appManifestURL = msg.appManifestURL; + if (appManifestURL) { + appId = appsService.getAppLocalIdByManifestURL(appManifestURL); + + if (!appId) { + mm.sendAsyncMessage("NetworkStats:Get:Return", + { id: msg.id, + error: "Invalid appManifestURL", result: null }); + return; + } + } + + let browsingTrafficOnly = msg.browsingTrafficOnly || false; + let serviceType = msg.serviceType || ""; + + let start = new Date(msg.start); + let end = new Date(msg.end); + + let callback = (function (aError, aResult) { + this._db.find(function onStatsFound(aError, aResult) { + mm.sendAsyncMessage("NetworkStats:Get:Return", + { id: msg.id, error: aError, result: aResult }); + }, appId, browsingTrafficOnly, serviceType, network, start, end, appManifestURL); + }).bind(this); + + this.validateNetwork(network, function onValidateNetwork(aNetId) { + if (!aNetId) { + mm.sendAsyncMessage("NetworkStats:Get:Return", + { id: msg.id, error: "Invalid connectionType", result: null }); + return; + } + + // If network is currently active we need to update the cached stats first before + // retrieving stats from the DB. + if (this._networks[aNetId].status == NETWORK_STATUS_READY) { + debug("getstats for network " + network.id + " of type " + network.type); + debug("appId: " + appId + " from appManifestURL: " + appManifestURL); + debug("browsingTrafficOnly: " + browsingTrafficOnly); + debug("serviceType: " + serviceType); + + if (appId || serviceType) { + this.updateCachedStats(callback); + return; + } + + this.updateStats(aNetId, function onStatsUpdated(aResult, aMessage) { + this.updateCachedStats(callback); + }.bind(this)); + return; + } + + // Network not active, so no need to update + this._db.find(function onStatsFound(aError, aResult) { + mm.sendAsyncMessage("NetworkStats:Get:Return", + { id: msg.id, error: aError, result: aResult }); + }, appId, browsingTrafficOnly, serviceType, network, start, end, appManifestURL); + }.bind(this)); + }, + + clearInterfaceStats: function clearInterfaceStats(mm, msg) { + let self = this; + let network = msg.network; + + debug("clear stats for network " + network.id + " of type " + network.type); + + this.validateNetwork(network, function onValidateNetwork(aNetId) { + if (!aNetId) { + mm.sendAsyncMessage("NetworkStats:Clear:Return", + { id: msg.id, error: "Invalid connectionType", result: null }); + return; + } + + network = {network: network, networkId: aNetId}; + self.updateStats(aNetId, function onUpdate(aResult, aMessage) { + if (!aResult) { + mm.sendAsyncMessage("NetworkStats:Clear:Return", + { id: msg.id, error: aMessage, result: null }); + return; + } + + self._db.clearInterfaceStats(network, function onDBCleared(aError, aResult) { + self._updateCurrentAlarm(aNetId); + mm.sendAsyncMessage("NetworkStats:Clear:Return", + { id: msg.id, error: aError, result: aResult }); + }); + }); + }); + }, + + clearDB: function clearDB(mm, msg) { + let self = this; + this._db.getAvailableNetworks(function onGetNetworks(aError, aResult) { + if (aError) { + mm.sendAsyncMessage("NetworkStats:ClearAll:Return", + { id: msg.id, error: aError, result: aResult }); + return; + } + + let networks = aResult; + networks.forEach(function(network, index) { + networks[index] = {network: network, networkId: self.getNetworkId(network.id, network.type)}; + }, self); + + self.updateAllStats(function onUpdate(aResult, aMessage){ + if (!aResult) { + mm.sendAsyncMessage("NetworkStats:ClearAll:Return", + { id: msg.id, error: aMessage, result: null }); + return; + } + + self._db.clearStats(networks, function onDBCleared(aError, aResult) { + networks.forEach(function(network, index) { + self._updateCurrentAlarm(network.networkId); + }, self); + mm.sendAsyncMessage("NetworkStats:ClearAll:Return", + { id: msg.id, error: aError, result: aResult }); + }); + }); + }); + }, + + updateAllStats: function updateAllStats(aCallback) { + let elements = []; + let lastElement = null; + let callback = (function (success, message) { + this.updateCachedStats(aCallback); + }).bind(this); + + // For each connectionType create an object containning the type + // and the 'queueIndex', the 'queueIndex' is an integer representing + // the index of a connection type in the global queue array. So, if + // the connection type is already in the queue it is not appended again, + // else it is pushed in 'elements' array, which later will be pushed to + // the queue array. + for (let netId in this._networks) { + if (this._networks[netId].status != NETWORK_STATUS_READY) { + continue; + } + + lastElement = { netId: netId, + queueIndex: this.updateQueueIndex(netId) }; + + if (lastElement.queueIndex == -1) { + elements.push({ netId: lastElement.netId, + callbacks: [], + queueType: QUEUE_TYPE_UPDATE_STATS }); + } + } + + if (!lastElement) { + // No elements need to be updated, probably because status is different than + // NETWORK_STATUS_READY. + if (aCallback) { + aCallback(true, "OK"); + } + return; + } + + if (elements.length > 0) { + // If length of elements is greater than 0, callback is set to + // the last element. + elements[elements.length - 1].callbacks.push(callback); + this.updateQueue = this.updateQueue.concat(elements); + } else { + // Else, it means that all connection types are already in the queue to + // be updated, so callback for this request is added to + // the element in the main queue with the index of the last 'lastElement'. + // But before is checked that element is still in the queue because it can + // be processed while generating 'elements' array. + let element = this.updateQueue[lastElement.queueIndex]; + if (aCallback && + (!element || element.netId != lastElement.netId)) { + aCallback(); + return; + } + + this.updateQueue[lastElement.queueIndex].callbacks.push(callback); + } + + // Call the function that process the elements of the queue. + this.processQueue(); + + if (DEBUG) { + this.logAllRecords(); + } + }, + + updateStats: function updateStats(aNetId, aCallback) { + // Check if the connection is in the main queue, push a new element + // if it is not being processed or add a callback if it is. + let index = this.updateQueueIndex(aNetId); + if (index == -1) { + this.updateQueue.push({ netId: aNetId, + callbacks: [aCallback], + queueType: QUEUE_TYPE_UPDATE_STATS }); + } else { + this.updateQueue[index].callbacks.push(aCallback); + return; + } + + // Call the function that process the elements of the queue. + this.processQueue(); + }, + + /* + * Find if a connection is in the main queue array and return its + * index, if it is not in the array return -1. + */ + updateQueueIndex: function updateQueueIndex(aNetId) { + return this.updateQueue.map(function(e) { return e.netId; }).indexOf(aNetId); + }, + + /* + * Function responsible of process all requests in the queue. + */ + processQueue: function processQueue(aResult, aMessage) { + // If aResult is not undefined, the caller of the function is the result + // of processing an element, so remove that element and call the callbacks + // it has. + let self = this; + + if (aResult != undefined) { + let item = this.updateQueue.shift(); + for (let callback of item.callbacks) { + if (callback) { + callback(aResult, aMessage); + } + } + } else { + // The caller is a function that has pushed new elements to the queue, + // if isQueueRunning is false it means there is no processing currently + // being done, so start. + if (this.isQueueRunning) { + return; + } else { + this.isQueueRunning = true; + } + } + + // Check length to determine if queue is empty and stop processing. + if (this.updateQueue.length < 1) { + this.isQueueRunning = false; + return; + } + + // Process the next item as soon as possible. + setTimeout(function () { + self.run(self.updateQueue[0]); + }, 0); + }, + + run: function run(item) { + switch (item.queueType) { + case QUEUE_TYPE_UPDATE_STATS: + this.update(item.netId, this.processQueue.bind(this)); + break; + case QUEUE_TYPE_UPDATE_CACHE: + this.updateCache(this.processQueue.bind(this)); + break; + case QUEUE_TYPE_WRITE_CACHE: + this.writeCache(item.stats, this.processQueue.bind(this)); + break; + } + }, + + update: function update(aNetId, aCallback) { + // Check if connection type is valid. + if (!this._networks[aNetId]) { + if (aCallback) { + aCallback(false, "Invalid network " + aNetId); + } + return; + } + + let interfaceName = this._networks[aNetId].interfaceName; + debug("Update stats for " + interfaceName); + + // Request stats to NetworkService, which will get stats from netd, passing + // 'networkStatsAvailable' as a callback. + if (interfaceName) { + networkService.getNetworkInterfaceStats(interfaceName, + this.networkStatsAvailable.bind(this, aCallback, aNetId)); + return; + } + + if (aCallback) { + aCallback(true, "ok"); + } + }, + + /* + * Callback of request stats. Store stats in database. + */ + networkStatsAvailable: function networkStatsAvailable(aCallback, aNetId, + aResult, aRxBytes, + aTxBytes, aTimestamp) { + if (!aResult) { + if (aCallback) { + aCallback(false, "Netd IPC error"); + } + return; + } + + let stats = { appId: 0, + isInBrowser: false, + serviceType: "", + networkId: this._networks[aNetId].network.id, + networkType: this._networks[aNetId].network.type, + date: new Date(aTimestamp), + rxBytes: aTxBytes, + txBytes: aRxBytes, + isAccumulative: true }; + + debug("Update stats for: " + JSON.stringify(stats)); + + this._db.saveStats(stats, function onSavedStats(aError, aResult) { + if (aCallback) { + if (aError) { + aCallback(false, aError); + return; + } + + aCallback(true, "OK"); + } + }); + }, + + /* + * Function responsible for receiving stats which are not from netd. + */ + saveStats: function saveStats(aAppId, aIsInIsolatedMozBrowser, aServiceType, + aNetworkInfo, aTimeStamp, aRxBytes, aTxBytes, + aIsAccumulative, aCallback) { + let netId = this.convertNetworkInfo(aNetworkInfo); + if (!netId) { + if (aCallback) { + aCallback(false, "Invalid network type"); + } + return; + } + + // Check if |aConnectionType|, |aAppId| and |aServiceType| are valid. + // There are two invalid cases for the combination of |aAppId| and + // |aServiceType|: + // a. Both |aAppId| is non-zero and |aServiceType| is non-empty. + // b. Both |aAppId| is zero and |aServiceType| is empty. + if (!this._networks[netId] || (aAppId && aServiceType) || + (!aAppId && !aServiceType)) { + debug("Invalid network interface, appId or serviceType"); + return; + } + + let stats = { appId: aAppId, + isInBrowser: aIsInIsolatedMozBrowser, + serviceType: aServiceType, + networkId: this._networks[netId].network.id, + networkType: this._networks[netId].network.type, + date: new Date(aTimeStamp), + rxBytes: aRxBytes, + txBytes: aTxBytes, + isAccumulative: aIsAccumulative }; + + this.updateQueue.push({ stats: stats, + callbacks: [aCallback], + queueType: QUEUE_TYPE_WRITE_CACHE }); + + this.processQueue(); + }, + + /* + * + */ + writeCache: function writeCache(aStats, aCallback) { + debug("saveStats: " + aStats.appId + " " + aStats.isInBrowser + " " + + aStats.serviceType + " " + aStats.networkId + " " + + aStats.networkType + " " + aStats.date + " " + + aStats.rxBytes + " " + aStats.txBytes); + + // Generate an unique key from |appId|, |isInBrowser|, |serviceType| and + // |netId|, which is used to retrieve data in |cachedStats|. + let netId = this.getNetworkId(aStats.networkId, aStats.networkType); + let key = aStats.appId + "" + aStats.isInBrowser + "" + + aStats.serviceType + "" + netId; + + // |cachedStats| only keeps the data with the same date. + // If the incoming date is different from |cachedStatsDate|, + // both |cachedStats| and |cachedStatsDate| will get updated. + let diff = (this._db.normalizeDate(aStats.date) - + this._db.normalizeDate(this.cachedStatsDate)) / + this._db.sampleRate; + if (diff != 0) { + this.updateCache(function onUpdated(success, message) { + this.cachedStatsDate = aStats.date; + this.cachedStats[key] = aStats; + + if (aCallback) { + aCallback(true, "ok"); + } + }.bind(this)); + return; + } + + // Try to find the matched row in the cached by |appId| and |connectionType|. + // If not found, save the incoming data into the cached. + let cachedStats = this.cachedStats[key]; + if (!cachedStats) { + this.cachedStats[key] = aStats; + if (aCallback) { + aCallback(true, "ok"); + } + return; + } + + // Find matched row, accumulate the traffic amount. + cachedStats.rxBytes += aStats.rxBytes; + cachedStats.txBytes += aStats.txBytes; + + // If new rxBytes or txBytes exceeds MAX_CACHED_TRAFFIC + // the corresponding row will be saved to indexedDB. + // Then, the row will be removed from the cached. + if (cachedStats.rxBytes > MAX_CACHED_TRAFFIC || + cachedStats.txBytes > MAX_CACHED_TRAFFIC) { + this._db.saveStats(cachedStats, function (error, result) { + debug("Application stats inserted in indexedDB"); + if (aCallback) { + aCallback(true, "ok"); + } + }); + delete this.cachedStats[key]; + return; + } + + if (aCallback) { + aCallback(true, "ok"); + } + }, + + updateCachedStats: function updateCachedStats(aCallback) { + this.updateQueue.push({ callbacks: [aCallback], + queueType: QUEUE_TYPE_UPDATE_CACHE }); + + this.processQueue(); + }, + + updateCache: function updateCache(aCallback) { + debug("updateCache: " + this.cachedStatsDate); + + let stats = Object.keys(this.cachedStats); + if (stats.length == 0) { + // |cachedStats| is empty, no need to update. + if (aCallback) { + aCallback(true, "no need to update"); + } + return; + } + + let index = 0; + this._db.saveStats(this.cachedStats[stats[index]], + function onSavedStats(error, result) { + debug("Application stats inserted in indexedDB"); + + // Clean up the |cachedStats| after updating. + if (index == stats.length - 1) { + this.cachedStats = Object.create(null); + + if (aCallback) { + aCallback(true, "ok"); + } + return; + } + + // Update is not finished, keep updating. + index += 1; + this._db.saveStats(this.cachedStats[stats[index]], + onSavedStats.bind(this, error, result)); + }.bind(this)); + }, + + get maxCachedTraffic () { + return MAX_CACHED_TRAFFIC; + }, + + logAllRecords: function logAllRecords() { + this._db.logAllRecords(function onResult(aError, aResult) { + if (aError) { + debug("Error: " + aError); + return; + } + + debug("===== LOG ====="); + debug("There are " + aResult.length + " items"); + debug(JSON.stringify(aResult)); + }); + }, + + getAlarms: function getAlarms(mm, msg) { + let self = this; + let network = msg.data.network; + let manifestURL = msg.data.manifestURL; + + if (network) { + this.validateNetwork(network, function onValidateNetwork(aNetId) { + if (!aNetId) { + mm.sendAsyncMessage("NetworkStats:GetAlarms:Return", + { id: msg.id, error: "InvalidInterface", result: null }); + return; + } + + self._getAlarms(mm, msg, aNetId, manifestURL); + }); + return; + } + + this._getAlarms(mm, msg, null, manifestURL); + }, + + _getAlarms: function _getAlarms(mm, msg, aNetId, aManifestURL) { + let self = this; + this._db.getAlarms(aNetId, aManifestURL, function onCompleted(error, result) { + if (error) { + mm.sendAsyncMessage("NetworkStats:GetAlarms:Return", + { id: msg.id, error: error, result: result }); + return; + } + + let alarms = [] + // NetworkStatsManager must return the network instead of the networkId. + for (let i = 0; i < result.length; i++) { + let alarm = result[i]; + alarms.push({ id: alarm.id, + network: self._networks[alarm.networkId].network, + threshold: alarm.absoluteThreshold, + data: alarm.data }); + } + + mm.sendAsyncMessage("NetworkStats:GetAlarms:Return", + { id: msg.id, error: null, result: alarms }); + }); + }, + + removeAlarms: function removeAlarms(mm, msg) { + let alarmId = msg.data.alarmId; + let manifestURL = msg.data.manifestURL; + + let self = this; + let callback = function onRemove(error, result) { + if (error) { + mm.sendAsyncMessage("NetworkStats:RemoveAlarms:Return", + { id: msg.id, error: error, result: result }); + return; + } + + for (let i in self._currentAlarms) { + let currentAlarm = self._currentAlarms[i].alarm; + if (currentAlarm && ((alarmId == currentAlarm.id) || + (alarmId == -1 && currentAlarm.manifestURL == manifestURL))) { + + self._updateCurrentAlarm(currentAlarm.networkId); + } + } + + mm.sendAsyncMessage("NetworkStats:RemoveAlarms:Return", + { id: msg.id, error: error, result: true }); + }; + + if (alarmId == -1) { + this._db.removeAlarms(manifestURL, callback); + } else { + this._db.removeAlarm(alarmId, manifestURL, callback); + } + }, + + /* + * Function called from manager to set an alarm. + */ + setAlarm: function setAlarm(mm, msg) { + let options = msg.data; + let network = options.network; + let threshold = options.threshold; + + debug("Set alarm at " + threshold + " for " + JSON.stringify(network)); + + if (threshold < 0) { + mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", + { id: msg.id, error: "InvalidThresholdValue", result: null }); + return; + } + + let self = this; + this.validateNetwork(network, function onValidateNetwork(aNetId) { + if (!aNetId) { + mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", + { id: msg.id, error: "InvalidiConnectionType", result: null }); + return; + } + + let newAlarm = { + id: null, + networkId: aNetId, + absoluteThreshold: threshold, + relativeThreshold: null, + startTime: options.startTime, + data: options.data, + pageURL: options.pageURL, + manifestURL: options.manifestURL + }; + + self._getAlarmQuota(newAlarm, function onUpdate(error, quota) { + if (error) { + mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", + { id: msg.id, error: error, result: null }); + return; + } + + self._db.addAlarm(newAlarm, function addSuccessCb(error, newId) { + if (error) { + mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", + { id: msg.id, error: error, result: null }); + return; + } + + newAlarm.id = newId; + self._setAlarm(newAlarm, function onSet(error, success) { + mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", + { id: msg.id, error: error, result: newId }); + + if (error == "InvalidStateError") { + self._fireAlarm(newAlarm); + } + }); + }); + }); + }); + }, + + _setAlarm: function _setAlarm(aAlarm, aCallback) { + let currentAlarm = this._currentAlarms[aAlarm.networkId]; + if ((Object.getOwnPropertyNames(currentAlarm).length !== 0 && + aAlarm.relativeThreshold > currentAlarm.alarm.relativeThreshold) || + this._networks[aAlarm.networkId].status != NETWORK_STATUS_READY) { + aCallback(null, true); + return; + } + + let self = this; + + this._getAlarmQuota(aAlarm, function onUpdate(aError, aQuota) { + if (aError) { + aCallback(aError, null); + return; + } + + let callback = function onAlarmSet(aError) { + if (aError) { + debug("Set alarm error: " + aError); + aCallback("netdError", null); + return; + } + + self._currentAlarms[aAlarm.networkId].alarm = aAlarm; + + aCallback(null, true); + }; + + debug("Set alarm " + JSON.stringify(aAlarm)); + let interfaceName = self._networks[aAlarm.networkId].interfaceName; + if (interfaceName) { + networkService.setNetworkInterfaceAlarm(interfaceName, + aQuota, + callback); + return; + } + + aCallback(null, true); + }); + }, + + _getAlarmQuota: function _getAlarmQuota(aAlarm, aCallback) { + let self = this; + this.updateStats(aAlarm.networkId, function onStatsUpdated(aResult, aMessage) { + self._db.getCurrentStats(self._networks[aAlarm.networkId].network, + aAlarm.startTime, + function onStatsFound(error, result) { + if (error) { + debug("Error getting stats for " + + JSON.stringify(self._networks[aAlarm.networkId]) + ": " + error); + aCallback(error, result); + return; + } + + let quota = aAlarm.absoluteThreshold - result.rxBytes - result.txBytes; + + // Alarm set to a threshold lower than current rx/tx bytes. + if (quota <= 0) { + aCallback("InvalidStateError", null); + return; + } + + aAlarm.relativeThreshold = aAlarm.startTime + ? result.rxTotalBytes + result.txTotalBytes + quota + : aAlarm.absoluteThreshold; + + aCallback(null, quota); + }); + }); + }, + + _fireAlarm: function _fireAlarm(aAlarm) { + debug("Fire alarm"); + + let self = this; + this._db.removeAlarm(aAlarm.id, null, function onRemove(aError, aResult){ + if (!aError && !aResult) { + return; + } + + self._fireSystemMessage(aAlarm); + self._updateCurrentAlarm(aAlarm.networkId); + }); + }, + + _updateCurrentAlarm: function _updateCurrentAlarm(aNetworkId) { + this._currentAlarms[aNetworkId] = Object.create(null); + + let self = this; + this._db.getFirstAlarm(aNetworkId, function onGet(error, result){ + if (error) { + debug("Error getting the first alarm"); + return; + } + + if (!result) { + let interfaceName = self._networks[aNetworkId].interfaceName; + networkService.setNetworkInterfaceAlarm(interfaceName, -1, + function onComplete(){}); + return; + } + + self._setAlarm(result, function onSet(error, success){ + if (error == "InvalidStateError") { + self._fireAlarm(result); + return; + } + }); + }); + }, + + _fireSystemMessage: function _fireSystemMessage(aAlarm) { + debug("Fire system message: " + JSON.stringify(aAlarm)); + + let manifestURI = Services.io.newURI(aAlarm.manifestURL, null, null); + let pageURI = Services.io.newURI(aAlarm.pageURL, null, null); + + let alarm = { "id": aAlarm.id, + "threshold": aAlarm.absoluteThreshold, + "data": aAlarm.data }; + messenger.sendMessage("networkstats-alarm", alarm, pageURI, manifestURI); + } +}; + +NetworkStatsService.init(); diff --git a/dom/network/NetworkStatsServiceProxy.js b/dom/network/NetworkStatsServiceProxy.js new file mode 100644 index 0000000000..f3df4344dc --- /dev/null +++ b/dom/network/NetworkStatsServiceProxy.js @@ -0,0 +1,90 @@ +/* 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 DEBUG = false; +function debug(s) { dump("-*- NetworkStatsServiceProxy: " + s + "\n"); } + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +this.EXPORTED_SYMBOLS = ["NetworkStatsServiceProxy"]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetworkStatsService.jsm"); + +const NETWORKSTATSSERVICEPROXY_CONTRACTID = "@mozilla.org/networkstatsServiceProxy;1"; +const NETWORKSTATSSERVICEPROXY_CID = Components.ID("98fd8f69-784e-4626-aa59-56d6436a3c24"); +const nsINetworkStatsServiceProxy = Ci.nsINetworkStatsServiceProxy; + +function NetworkStatsServiceProxy() { + if (DEBUG) { + debug("Proxy started"); + } +} + +NetworkStatsServiceProxy.prototype = { + /* + * Function called in the protocol layer (HTTP, FTP, WebSocket ...etc) + * to pass the per-app stats to NetworkStatsService. + */ + saveAppStats: function saveAppStats(aAppId, aIsInIsolatedMozBrowser, aNetworkInfo, aTimeStamp, + aRxBytes, aTxBytes, aIsAccumulative, + aCallback) { + if (!aNetworkInfo) { + if (DEBUG) { + debug("|aNetworkInfo| is not specified. Failed to save stats. Returning."); + } + return; + } + + if (DEBUG) { + debug("saveAppStats: " + aAppId + " " + aIsInIsolatedMozBrowser + " " + + aNetworkInfo.type + " " + aTimeStamp + " " + + aRxBytes + " " + aTxBytes + " " + aIsAccumulative); + } + + if (aCallback) { + aCallback = aCallback.notify; + } + + NetworkStatsService.saveStats(aAppId, aIsInIsolatedMozBrowser, "", aNetworkInfo, + aTimeStamp, aRxBytes, aTxBytes, + aIsAccumulative, aCallback); + }, + + /* + * Function called in the points of different system services + * to pass the per-service stats to NetworkStatsService. + */ + saveServiceStats: function saveServiceStats(aServiceType, aNetworkInfo, + aTimeStamp, aRxBytes, aTxBytes, + aIsAccumulative, aCallback) { + if (!aNetworkInfo) { + if (DEBUG) { + debug("|aNetworkInfo| is not specified. Failed to save stats. Returning."); + } + return; + } + + if (DEBUG) { + debug("saveServiceStats: " + aServiceType + " " + aNetworkInfo.type + " " + + aTimeStamp + " " + aRxBytes + " " + aTxBytes + " " + + aIsAccumulative); + } + + if (aCallback) { + aCallback = aCallback.notify; + } + + NetworkStatsService.saveStats(0, false, aServiceType , aNetworkInfo, aTimeStamp, + aRxBytes, aTxBytes, aIsAccumulative, + aCallback); + }, + + classID : NETWORKSTATSSERVICEPROXY_CID, + QueryInterface : XPCOMUtils.generateQI([nsINetworkStatsServiceProxy]), +} + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkStatsServiceProxy]); diff --git a/dom/network/NetworkStatsServiceProxy.manifest b/dom/network/NetworkStatsServiceProxy.manifest new file mode 100644 index 0000000000..24f09f0885 --- /dev/null +++ b/dom/network/NetworkStatsServiceProxy.manifest @@ -0,0 +1,2 @@ +component {98fd8f69-784e-4626-aa59-56d6436a3c24} NetworkStatsServiceProxy.js +contract @mozilla.org/networkstatsServiceProxy;1 {98fd8f69-784e-4626-aa59-56d6436a3c24} diff --git a/dom/network/PTCPServerSocket.ipdl b/dom/network/PTCPServerSocket.ipdl new file mode 100644 index 0000000000..fc92f01467 --- /dev/null +++ b/dom/network/PTCPServerSocket.ipdl @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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/. */ + +include protocol PNecko; +include protocol PTCPSocket; + +include "mozilla/net/NeckoMessageUtils.h"; + +namespace mozilla { +namespace net { + +//------------------------------------------------------------------- +protocol PTCPServerSocket +{ + manager PNecko; + +parent: + async Close(); + async RequestDelete(); + +child: + async CallbackAccept(PTCPSocket socket); + async __delete__(); +}; + +} // namespace net +} // namespace mozilla + diff --git a/dom/network/PTCPSocket.ipdl b/dom/network/PTCPSocket.ipdl new file mode 100644 index 0000000000..5c9c1c862e --- /dev/null +++ b/dom/network/PTCPSocket.ipdl @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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/. */ + +include protocol PNecko; + +include "mozilla/net/NeckoMessageUtils.h"; + +using struct mozilla::void_t from "ipc/IPCMessageUtils.h"; + +struct TCPError { + nsString name; + nsString message; +}; + +union SendableData { + uint8_t[]; + nsCString; +}; + +union CallbackData { + void_t; + SendableData; + TCPError; +}; + +namespace mozilla { +namespace net { + +//------------------------------------------------------------------- +protocol PTCPSocket +{ + manager PNecko; + +parent: + // Forward calling to child's open() method to parent, expect TCPOptions + // is expanded to |useSSL| (from TCPOptions.useSecureTransport) and + // |binaryType| (from TCPOption.binaryType). + async Open(nsString host, uint16_t port, bool useSSL, bool useArrayBuffers); + + // Ask parent to open a socket and bind the newly-opened socket to a local + // address specified in |localAddr| and |localPort|. + async OpenBind(nsCString host, uint16_t port, + nsCString localAddr, uint16_t localPort, + bool useSSL, bool aUseArrayBuffers, nsCString aFilter); + + // When child's send() is called, this message requrests parent to send + // data and update it's trackingNumber. + async Data(SendableData data, uint32_t trackingNumber); + + // Forward calling to child's upgradeToSecure() method to parent. + async StartTLS(); + + // Forward calling to child's send() method to parent. + async Suspend(); + + // Forward calling to child's resume() method to parent. + async Resume(); + + // Forward calling to child's close() method to parent. + async Close(); + +child: + // Forward events that are dispatched by parent. + async Callback(nsString type, CallbackData data, uint32_t readyState); + + // Update child's bufferedAmount when parent's bufferedAmount is updated. + // trackingNumber is also passed back to child to ensure the bufferedAmount + // is corresponding the last call to send(). + async UpdateBufferedAmount(uint32_t bufferedAmount, uint32_t trackingNumber); + +both: + async RequestDelete(); + async __delete__(); +}; + + +} // namespace net +} // namespace mozilla + diff --git a/dom/network/PUDPSocket.ipdl b/dom/network/PUDPSocket.ipdl new file mode 100644 index 0000000000..a104de3082 --- /dev/null +++ b/dom/network/PUDPSocket.ipdl @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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/. */ + +include protocol PNecko; +include protocol PBackground; +include protocol PBlob; +include InputStreamParams; + +include "mozilla/net/NeckoMessageUtils.h"; +include "mozilla/net/DNS.h"; +include "prio.h"; + +using mozilla::net::NetAddr from "mozilla/net/DNS.h"; +using struct mozilla::void_t from "ipc/IPCMessageUtils.h"; + +struct UDPAddressInfo { + nsCString addr; + uint16_t port; +}; + +union UDPSocketAddr { + UDPAddressInfo; + NetAddr; +}; + +union UDPData { + uint8_t[]; + InputStreamParams; +}; + +namespace mozilla { +namespace net { + +//------------------------------------------------------------------- +protocol PUDPSocket +{ + manager PNecko or PBackground; + +parent: + async Bind(UDPAddressInfo addressInfo, bool addressReuse, bool loopback, + uint32_t recvBufferSize, uint32_t sendBufferSize); + async Connect(UDPAddressInfo addressInfo); + + async OutgoingData(UDPData data, UDPSocketAddr addr); + + async JoinMulticast(nsCString multicastAddress, nsCString iface); + async LeaveMulticast(nsCString multicastAddress, nsCString iface); + + async Close(); + + async RequestDelete(); + +child: + async CallbackOpened(UDPAddressInfo addressInfo); + async CallbackConnected(UDPAddressInfo addressInfo); + async CallbackClosed(); + async CallbackReceivedData(UDPAddressInfo addressInfo, uint8_t[] data); + async CallbackError(nsCString message, nsCString filename, uint32_t lineNumber); + async __delete__(); +}; + + +} // namespace net +} // namespace mozilla + diff --git a/dom/network/TCPServerSocket.cpp b/dom/network/TCPServerSocket.cpp new file mode 100644 index 0000000000..5e6f778a3f --- /dev/null +++ b/dom/network/TCPServerSocket.cpp @@ -0,0 +1,192 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "mozilla/dom/TCPServerSocketBinding.h" +#include "mozilla/dom/TCPServerSocketEvent.h" +#include "mozilla/dom/TCPSocketBinding.h" +#include "TCPServerSocketParent.h" +#include "TCPServerSocketChild.h" +#include "mozilla/dom/Event.h" +#include "mozilla/ErrorResult.h" +#include "TCPServerSocket.h" +#include "TCPSocket.h" + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION_CLASS(TCPServerSocket) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(TCPServerSocket, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TCPServerSocket, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServerSocket) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServerBridgeChild) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServerBridgeParent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TCPServerSocket, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mServerSocket) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mServerBridgeChild) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mServerBridgeParent) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ADDREF_INHERITED(TCPServerSocket, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(TCPServerSocket, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(TCPServerSocket) + NS_INTERFACE_MAP_ENTRY(nsIServerSocketListener) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +TCPServerSocket::TCPServerSocket(nsIGlobalObject* aGlobal, uint16_t aPort, + bool aUseArrayBuffers, uint16_t aBacklog) + : DOMEventTargetHelper(aGlobal) + , mPort(aPort) + , mBacklog(aBacklog) + , mUseArrayBuffers(aUseArrayBuffers) +{ +} + +TCPServerSocket::~TCPServerSocket() +{ +} + +nsresult +TCPServerSocket::Init() +{ + if (mServerSocket || mServerBridgeChild) { + NS_WARNING("Child TCPServerSocket is already listening."); + return NS_ERROR_FAILURE; + } + + if (XRE_GetProcessType() == GeckoProcessType_Content) { + mServerBridgeChild = new TCPServerSocketChild(this, mPort, mBacklog, mUseArrayBuffers); + return NS_OK; + } + + nsresult rv; + mServerSocket = do_CreateInstance("@mozilla.org/network/server-socket;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = mServerSocket->Init(mPort, false, mBacklog); + NS_ENSURE_SUCCESS(rv, rv); + rv = mServerSocket->GetPort(&mPort); + NS_ENSURE_SUCCESS(rv, rv); + rv = mServerSocket->AsyncListen(this); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +already_AddRefed<TCPServerSocket> +TCPServerSocket::Constructor(const GlobalObject& aGlobal, + uint16_t aPort, + const ServerSocketOptions& aOptions, + uint16_t aBacklog, + mozilla::ErrorResult& aRv) +{ + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + if (!global) { + aRv = NS_ERROR_FAILURE; + return nullptr; + } + bool useArrayBuffers = aOptions.mBinaryType == TCPSocketBinaryType::Arraybuffer; + RefPtr<TCPServerSocket> socket = new TCPServerSocket(global, aPort, useArrayBuffers, aBacklog); + nsresult rv = socket->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv = NS_ERROR_FAILURE; + return nullptr; + } + return socket.forget(); +} + +uint16_t +TCPServerSocket::LocalPort() +{ + return mPort; +} + +void +TCPServerSocket::Close() +{ + if (mServerBridgeChild) { + mServerBridgeChild->Close(); + } + if (mServerSocket) { + mServerSocket->Close(); + } +} + +void +TCPServerSocket::FireEvent(const nsAString& aType, TCPSocket* aSocket) +{ + TCPServerSocketEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mSocket = aSocket; + + RefPtr<TCPServerSocketEvent> event = + TCPServerSocketEvent::Constructor(this, aType, init); + event->SetTrusted(true); + bool dummy; + DispatchEvent(event, &dummy); + + if (mServerBridgeParent) { + mServerBridgeParent->OnConnect(event); + } +} + +NS_IMETHODIMP +TCPServerSocket::OnSocketAccepted(nsIServerSocket* aServer, nsISocketTransport* aTransport) +{ + nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal(); + RefPtr<TCPSocket> socket = TCPSocket::CreateAcceptedSocket(global, aTransport, mUseArrayBuffers); + if (mServerBridgeParent) { + socket->SetAppIdAndBrowser(mServerBridgeParent->GetAppId(), + mServerBridgeParent->GetInIsolatedMozBrowser()); + } + FireEvent(NS_LITERAL_STRING("connect"), socket); + return NS_OK; +} + +NS_IMETHODIMP +TCPServerSocket::OnStopListening(nsIServerSocket* aServer, nsresult aStatus) +{ + if (aStatus != NS_BINDING_ABORTED) { + RefPtr<Event> event = new Event(GetOwner()); + event->InitEvent(NS_LITERAL_STRING("error"), false, false); + event->SetTrusted(true); + bool dummy; + DispatchEvent(event, &dummy); + + NS_WARNING("Server socket was closed by unexpected reason."); + return NS_ERROR_FAILURE; + } + mServerSocket = nullptr; + return NS_OK; +} + +nsresult +TCPServerSocket::AcceptChildSocket(TCPSocketChild* aSocketChild) +{ + nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal(); + NS_ENSURE_TRUE(global, NS_ERROR_FAILURE); + RefPtr<TCPSocket> socket = TCPSocket::CreateAcceptedSocket(global, aSocketChild, mUseArrayBuffers); + NS_ENSURE_TRUE(socket, NS_ERROR_FAILURE); + FireEvent(NS_LITERAL_STRING("connect"), socket); + return NS_OK; +} + +void +TCPServerSocket::SetServerBridgeParent(TCPServerSocketParent* aBridgeParent) +{ + mServerBridgeParent = aBridgeParent; +} + +JSObject* +TCPServerSocket::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return TCPServerSocketBinding::Wrap(aCx, this, aGivenProto); +} diff --git a/dom/network/TCPServerSocket.h b/dom/network/TCPServerSocket.h new file mode 100644 index 0000000000..45ab6e068d --- /dev/null +++ b/dom/network/TCPServerSocket.h @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_TCPServerSocket_h +#define mozilla_dom_TCPServerSocket_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "nsIServerSocket.h" + +namespace mozilla { +class ErrorResult; +namespace dom { + +struct ServerSocketOptions; +class GlobalObject; +class TCPSocket; +class TCPSocketChild; +class TCPServerSocketChild; +class TCPServerSocketParent; + +class TCPServerSocket final : public DOMEventTargetHelper + , public nsIServerSocketListener +{ +public: + TCPServerSocket(nsIGlobalObject* aGlobal, uint16_t aPort, bool aUseArrayBuffers, + uint16_t aBacklog); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(TCPServerSocket, DOMEventTargetHelper) + NS_DECL_NSISERVERSOCKETLISTENER + + nsPIDOMWindowInner* GetParentObject() const + { + return GetOwner(); + } + + virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + nsresult Init(); + + uint16_t LocalPort(); + void Close(); + + static already_AddRefed<TCPServerSocket> + Constructor(const GlobalObject& aGlobal, + uint16_t aPort, + const ServerSocketOptions& aOptions, + uint16_t aBacklog, + mozilla::ErrorResult& aRv); + + IMPL_EVENT_HANDLER(connect); + IMPL_EVENT_HANDLER(error); + + // Relay an accepted socket notification from the parent process and + // initialize this object with an existing child actor for the new socket. + nsresult AcceptChildSocket(TCPSocketChild* aSocketChild); + // Associate this object with an IPC actor in the parent process to relay + // notifications to content processes. + void SetServerBridgeParent(TCPServerSocketParent* aBridgeParent); + +private: + ~TCPServerSocket(); + // Dispatch a TCPServerSocketEvent event of a given type at this object. + void FireEvent(const nsAString& aType, TCPSocket* aSocket); + + // The server socket associated with this object. + nsCOMPtr<nsIServerSocket> mServerSocket; + // The IPC actor in the content process. + RefPtr<TCPServerSocketChild> mServerBridgeChild; + // The IPC actor in the parent process. + RefPtr<TCPServerSocketParent> mServerBridgeParent; + int32_t mPort; + uint16_t mBacklog; + // True if any accepted sockets should use array buffers for received messages. + bool mUseArrayBuffers; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_TCPServerSocket_h diff --git a/dom/network/TCPServerSocketChild.cpp b/dom/network/TCPServerSocketChild.cpp new file mode 100644 index 0000000000..f3218ced6d --- /dev/null +++ b/dom/network/TCPServerSocketChild.cpp @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "TCPServerSocketChild.h" +#include "TCPSocketChild.h" +#include "TCPServerSocket.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/dom/PBrowserChild.h" +#include "mozilla/dom/TabChild.h" +#include "nsJSUtils.h" +#include "jsfriendapi.h" + +using mozilla::net::gNeckoChild; + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION(TCPServerSocketChildBase, mServerSocket) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TCPServerSocketChildBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TCPServerSocketChildBase) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TCPServerSocketChildBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +TCPServerSocketChildBase::TCPServerSocketChildBase() +: mIPCOpen(false) +{ +} + +TCPServerSocketChildBase::~TCPServerSocketChildBase() +{ +} + +NS_IMETHODIMP_(MozExternalRefCountType) TCPServerSocketChild::Release(void) +{ + nsrefcnt refcnt = TCPServerSocketChildBase::Release(); + if (refcnt == 1 && mIPCOpen) { + PTCPServerSocketChild::SendRequestDelete(); + return 1; + } + return refcnt; +} + +TCPServerSocketChild::TCPServerSocketChild(TCPServerSocket* aServerSocket, uint16_t aLocalPort, + uint16_t aBacklog, bool aUseArrayBuffers) +{ + mServerSocket = aServerSocket; + AddIPDLReference(); + gNeckoChild->SendPTCPServerSocketConstructor(this, aLocalPort, aBacklog, aUseArrayBuffers); +} + +void +TCPServerSocketChildBase::ReleaseIPDLReference() +{ + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; + this->Release(); +} + +void +TCPServerSocketChildBase::AddIPDLReference() +{ + MOZ_ASSERT(!mIPCOpen); + mIPCOpen = true; + this->AddRef(); +} + +TCPServerSocketChild::~TCPServerSocketChild() +{ +} + +bool +TCPServerSocketChild::RecvCallbackAccept(PTCPSocketChild *psocket) +{ + RefPtr<TCPSocketChild> socket = static_cast<TCPSocketChild*>(psocket); + nsresult rv = mServerSocket->AcceptChildSocket(socket); + NS_ENSURE_SUCCESS(rv, true); + return true; +} + +void +TCPServerSocketChild::Close() +{ + SendClose(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/network/TCPServerSocketChild.h b/dom/network/TCPServerSocketChild.h new file mode 100644 index 0000000000..77dbc59c66 --- /dev/null +++ b/dom/network/TCPServerSocketChild.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_TCPServerSocketChild_h +#define mozilla_dom_TCPServerSocketChild_h + +#include "mozilla/net/PTCPServerSocketChild.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" + +#define TCPSERVERSOCKETCHILD_CID \ + { 0x41a77ec8, 0xfd86, 0x409e, { 0xae, 0xa9, 0xaf, 0x2c, 0xa4, 0x07, 0xef, 0x8e } } + +class nsITCPServerSocketInternal; + +namespace mozilla { +namespace dom { + +class TCPServerSocket; + +class TCPServerSocketChildBase : public nsISupports { +public: + NS_DECL_CYCLE_COLLECTION_CLASS(TCPServerSocketChildBase) + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + void AddIPDLReference(); + void ReleaseIPDLReference(); + +protected: + TCPServerSocketChildBase(); + virtual ~TCPServerSocketChildBase(); + + RefPtr<TCPServerSocket> mServerSocket; + bool mIPCOpen; +}; + +class TCPServerSocketChild : public mozilla::net::PTCPServerSocketChild + , public TCPServerSocketChildBase +{ +public: + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + TCPServerSocketChild(TCPServerSocket* aServerSocket, uint16_t aLocalPort, + uint16_t aBacklog, bool aUseArrayBuffers); + ~TCPServerSocketChild(); + + void Close(); + + virtual bool RecvCallbackAccept(PTCPSocketChild *socket) override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_TCPServerSocketChild_h diff --git a/dom/network/TCPServerSocketParent.cpp b/dom/network/TCPServerSocketParent.cpp new file mode 100644 index 0000000000..8f98d8b785 --- /dev/null +++ b/dom/network/TCPServerSocketParent.cpp @@ -0,0 +1,164 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "nsIScriptSecurityManager.h" +#include "TCPServerSocket.h" +#include "TCPServerSocketParent.h" +#include "nsJSUtils.h" +#include "TCPSocketParent.h" +#include "mozilla/Unused.h" +#include "mozilla/AppProcessChecker.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/TabParent.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION(TCPServerSocketParent, mServerSocket) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TCPServerSocketParent) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TCPServerSocketParent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TCPServerSocketParent) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +void +TCPServerSocketParent::ReleaseIPDLReference() +{ + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; + this->Release(); +} + +void +TCPServerSocketParent::AddIPDLReference() +{ + MOZ_ASSERT(!mIPCOpen); + mIPCOpen = true; + this->AddRef(); +} + +TCPServerSocketParent::TCPServerSocketParent(PNeckoParent* neckoParent, + uint16_t aLocalPort, + uint16_t aBacklog, + bool aUseArrayBuffers) +: mNeckoParent(neckoParent) +, mIPCOpen(false) +{ + mServerSocket = new TCPServerSocket(nullptr, aLocalPort, aUseArrayBuffers, aBacklog); + mServerSocket->SetServerBridgeParent(this); +} + +TCPServerSocketParent::~TCPServerSocketParent() +{ +} + +void +TCPServerSocketParent::Init() +{ + NS_ENSURE_SUCCESS_VOID(mServerSocket->Init()); +} + +uint32_t +TCPServerSocketParent::GetAppId() +{ + const PContentParent *content = Manager()->Manager(); + if (PBrowserParent* browser = SingleManagedOrNull(content->ManagedPBrowserParent())) { + TabParent *tab = TabParent::GetFrom(browser); + return tab->OwnAppId(); + } else { + return nsIScriptSecurityManager::UNKNOWN_APP_ID; + } +} + +bool +TCPServerSocketParent::GetInIsolatedMozBrowser() +{ + const PContentParent *content = Manager()->Manager(); + if (PBrowserParent* browser = SingleManagedOrNull(content->ManagedPBrowserParent())) { + TabParent *tab = TabParent::GetFrom(browser); + return tab->IsIsolatedMozBrowserElement(); + } else { + return false; + } +} + +nsresult +TCPServerSocketParent::SendCallbackAccept(TCPSocketParent *socket) +{ + socket->AddIPDLReference(); + + nsresult rv; + + nsString host; + rv = socket->GetHost(host); + if (NS_FAILED(rv)) { + NS_ERROR("Failed to get host from nsITCPSocketParent"); + return NS_ERROR_FAILURE; + } + + uint16_t port; + rv = socket->GetPort(&port); + if (NS_FAILED(rv)) { + NS_ERROR("Failed to get port from nsITCPSocketParent"); + return NS_ERROR_FAILURE; + } + + if (mNeckoParent) { + if (mNeckoParent->SendPTCPSocketConstructor(socket, host, port)) { + mozilla::Unused << PTCPServerSocketParent::SendCallbackAccept(socket); + } + else { + NS_ERROR("Sending data from PTCPSocketParent was failed."); + } + } + else { + NS_ERROR("The member value for NeckoParent is wrong."); + } + return NS_OK; +} + +bool +TCPServerSocketParent::RecvClose() +{ + NS_ENSURE_TRUE(mServerSocket, true); + mServerSocket->Close(); + return true; +} + +void +TCPServerSocketParent::ActorDestroy(ActorDestroyReason why) +{ + if (mServerSocket) { + mServerSocket->Close(); + mServerSocket = nullptr; + } + mNeckoParent = nullptr; +} + +bool +TCPServerSocketParent::RecvRequestDelete() +{ + mozilla::Unused << Send__delete__(this); + return true; +} + +void +TCPServerSocketParent::OnConnect(TCPServerSocketEvent* event) +{ + RefPtr<TCPSocket> socket = event->Socket(); + socket->SetAppIdAndBrowser(GetAppId(), GetInIsolatedMozBrowser()); + + RefPtr<TCPSocketParent> socketParent = new TCPSocketParent(); + socketParent->SetSocket(socket); + + socket->SetSocketBridgeParent(socketParent); + + SendCallbackAccept(socketParent); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/network/TCPServerSocketParent.h b/dom/network/TCPServerSocketParent.h new file mode 100644 index 0000000000..071183366b --- /dev/null +++ b/dom/network/TCPServerSocketParent.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_TCPServerSocketParent_h +#define mozilla_dom_TCPServerSocketParent_h + +#include "mozilla/net/PNeckoParent.h" +#include "mozilla/net/PTCPServerSocketParent.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" + +namespace mozilla { +namespace dom { + +class TCPServerSocket; +class TCPServerSocketEvent; +class TCPSocketParent; + +class TCPServerSocketParent : public mozilla::net::PTCPServerSocketParent + , public nsISupports +{ +public: + NS_DECL_CYCLE_COLLECTION_CLASS(TCPServerSocketParent) + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + TCPServerSocketParent(PNeckoParent* neckoParent, uint16_t aLocalPort, + uint16_t aBacklog, bool aUseArrayBuffers); + + void Init(); + + virtual bool RecvClose() override; + virtual bool RecvRequestDelete() override; + + uint32_t GetAppId(); + bool GetInIsolatedMozBrowser(); + + void AddIPDLReference(); + void ReleaseIPDLReference(); + + void OnConnect(TCPServerSocketEvent* event); + +private: + ~TCPServerSocketParent(); + + nsresult SendCallbackAccept(TCPSocketParent *socket); + + virtual void ActorDestroy(ActorDestroyReason why) override; + + PNeckoParent* mNeckoParent; + RefPtr<TCPServerSocket> mServerSocket; + bool mIPCOpen; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_TCPServerSocketParent_h diff --git a/dom/network/TCPSocket.cpp b/dom/network/TCPSocket.cpp new file mode 100644 index 0000000000..4eb2f72f61 --- /dev/null +++ b/dom/network/TCPSocket.cpp @@ -0,0 +1,1252 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "mozilla/ErrorResult.h" +#include "TCPSocket.h" +#include "TCPServerSocket.h" +#include "TCPSocketChild.h" +#include "mozilla/dom/DOMError.h" +#include "mozilla/dom/TCPSocketBinding.h" +#include "mozilla/dom/TCPSocketErrorEvent.h" +#include "mozilla/dom/TCPSocketErrorEventBinding.h" +#include "mozilla/dom/TCPSocketEvent.h" +#include "mozilla/dom/TCPSocketEventBinding.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsContentUtils.h" +#include "nsIArrayBufferInputStream.h" +#include "nsISocketTransportService.h" +#include "nsISocketTransport.h" +#include "nsIMultiplexInputStream.h" +#include "nsIAsyncStreamCopier.h" +#include "nsIInputStream.h" +#include "nsIBinaryInputStream.h" +#include "nsIScriptableInputStream.h" +#include "nsIInputStreamPump.h" +#include "nsIAsyncInputStream.h" +#include "nsISupportsPrimitives.h" +#include "nsITransport.h" +#include "nsIOutputStream.h" +#include "nsINSSErrorsService.h" +#include "nsISSLSocketControl.h" +#include "nsStringStream.h" +#include "secerr.h" +#include "sslerr.h" +#ifdef MOZ_WIDGET_GONK +#include "nsINetworkStatsServiceProxy.h" +#include "nsINetworkManager.h" +#include "nsINetworkInterface.h" +#endif + +#define BUFFER_SIZE 65536 +#define NETWORK_STATS_THRESHOLD 65536 + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION(LegacyMozTCPSocket, mGlobal) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(LegacyMozTCPSocket) +NS_IMPL_CYCLE_COLLECTING_RELEASE(LegacyMozTCPSocket) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LegacyMozTCPSocket) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +LegacyMozTCPSocket::LegacyMozTCPSocket(nsPIDOMWindowInner* aWindow) +: mGlobal(do_QueryInterface(aWindow)) +{ +} + +LegacyMozTCPSocket::~LegacyMozTCPSocket() +{ +} + +already_AddRefed<TCPSocket> +LegacyMozTCPSocket::Open(const nsAString& aHost, + uint16_t aPort, + const SocketOptions& aOptions, + mozilla::ErrorResult& aRv) +{ + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(mGlobal))) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + GlobalObject globalObj(api.cx(), mGlobal->GetGlobalJSObject()); + return TCPSocket::Constructor(globalObj, aHost, aPort, aOptions, aRv); +} + +already_AddRefed<TCPServerSocket> +LegacyMozTCPSocket::Listen(uint16_t aPort, + const ServerSocketOptions& aOptions, + uint16_t aBacklog, + mozilla::ErrorResult& aRv) +{ + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(mGlobal))) { + return nullptr; + } + GlobalObject globalObj(api.cx(), mGlobal->GetGlobalJSObject()); + return TCPServerSocket::Constructor(globalObj, aPort, aOptions, aBacklog, aRv); +} + +bool +LegacyMozTCPSocket::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aReflector) +{ + return LegacyMozTCPSocketBinding::Wrap(aCx, this, aGivenProto, aReflector); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(TCPSocket) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(TCPSocket, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TCPSocket, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransport) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocketInputStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocketOutputStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputStreamPump) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputStreamScriptable) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputStreamBinary) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMultiplexStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMultiplexStreamCopier) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingDataAfterStartTLS) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocketBridgeChild) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocketBridgeParent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TCPSocket, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransport) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocketInputStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocketOutputStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputStreamPump) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputStreamScriptable) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputStreamBinary) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMultiplexStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMultiplexStreamCopier) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingDataAfterStartTLS) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocketBridgeChild) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocketBridgeParent) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ADDREF_INHERITED(TCPSocket, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(TCPSocket, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(TCPSocket) + NS_INTERFACE_MAP_ENTRY(nsITransportEventSink) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsITCPSocketCallback) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +TCPSocket::TCPSocket(nsIGlobalObject* aGlobal, const nsAString& aHost, uint16_t aPort, + bool aSsl, bool aUseArrayBuffers) + : DOMEventTargetHelper(aGlobal) + , mReadyState(TCPReadyState::Closed) + , mUseArrayBuffers(aUseArrayBuffers) + , mHost(aHost) + , mPort(aPort) + , mSsl(aSsl) + , mAsyncCopierActive(false) + , mWaitingForDrain(false) + , mInnerWindowID(0) + , mBufferedAmount(0) + , mSuspendCount(0) + , mTrackingNumber(0) + , mWaitingForStartTLS(false) + , mObserversActive(false) +#ifdef MOZ_WIDGET_GONK + , mTxBytes(0) + , mRxBytes(0) + , mAppId(nsIScriptSecurityManager::UNKNOWN_APP_ID) + , mInIsolatedMozBrowser(false) +#endif +{ + if (aGlobal) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal); + if (window) { + mInnerWindowID = window->WindowID(); + } + } +} + +TCPSocket::~TCPSocket() +{ + if (mObserversActive) { + nsCOMPtr<nsIObserverService> obs = do_GetService("@mozilla.org/observer-service;1"); + if (obs) { + obs->RemoveObserver(this, "inner-window-destroyed"); + obs->RemoveObserver(this, "profile-change-net-teardown"); + } + } +} + +nsresult +TCPSocket::CreateStream() +{ + nsresult rv = mTransport->OpenInputStream(0, 0, 0, getter_AddRefs(mSocketInputStream)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mTransport->OpenOutputStream(nsITransport::OPEN_UNBUFFERED, 0, 0, getter_AddRefs(mSocketOutputStream)); + NS_ENSURE_SUCCESS(rv, rv); + + // If the other side is not listening, we will + // get an onInputStreamReady callback where available + // raises to indicate the connection was refused. + nsCOMPtr<nsIAsyncInputStream> asyncStream = do_QueryInterface(mSocketInputStream); + NS_ENSURE_TRUE(asyncStream, NS_ERROR_NOT_AVAILABLE); + + nsCOMPtr<nsIThread> mainThread; + NS_GetMainThread(getter_AddRefs(mainThread)); + + rv = asyncStream->AsyncWait(this, nsIAsyncInputStream::WAIT_CLOSURE_ONLY, 0, mainThread); + NS_ENSURE_SUCCESS(rv, rv); + + if (mUseArrayBuffers) { + mInputStreamBinary = do_CreateInstance("@mozilla.org/binaryinputstream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = mInputStreamBinary->SetInputStream(mSocketInputStream); + NS_ENSURE_SUCCESS(rv, rv); + } else { + mInputStreamScriptable = do_CreateInstance("@mozilla.org/scriptableinputstream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = mInputStreamScriptable->Init(mSocketInputStream); + NS_ENSURE_SUCCESS(rv, rv); + } + + mMultiplexStream = do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + mMultiplexStreamCopier = do_CreateInstance("@mozilla.org/network/async-stream-copier;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISocketTransportService> sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + + nsCOMPtr<nsIEventTarget> target = do_QueryInterface(sts); + rv = mMultiplexStreamCopier->Init(mMultiplexStream, + mSocketOutputStream, + target, + true, /* source buffered */ + false, /* sink buffered */ + BUFFER_SIZE, + false, /* close source */ + false); /* close sink */ + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +nsresult +TCPSocket::InitWithUnconnectedTransport(nsISocketTransport* aTransport) +{ + mReadyState = TCPReadyState::Connecting; + mTransport = aTransport; + + MOZ_ASSERT(XRE_GetProcessType() != GeckoProcessType_Content); + + nsCOMPtr<nsIThread> mainThread; + NS_GetMainThread(getter_AddRefs(mainThread)); + mTransport->SetEventSink(this, mainThread); + + nsresult rv = CreateStream(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +TCPSocket::Init() +{ + nsCOMPtr<nsIObserverService> obs = do_GetService("@mozilla.org/observer-service;1"); + if (obs) { + mObserversActive = true; + obs->AddObserver(this, "inner-window-destroyed", true); // weak reference + obs->AddObserver(this, "profile-change-net-teardown", true); // weak ref + } + + if (XRE_GetProcessType() == GeckoProcessType_Content) { + mReadyState = TCPReadyState::Connecting; + mSocketBridgeChild = new TCPSocketChild(mHost, mPort); + mSocketBridgeChild->SendOpen(this, mSsl, mUseArrayBuffers); + return NS_OK; + } + + nsCOMPtr<nsISocketTransportService> sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + + const char* socketTypes[1]; + if (mSsl) { + socketTypes[0] = "ssl"; + } else { + socketTypes[0] = "starttls"; + } + nsCOMPtr<nsISocketTransport> transport; + nsresult rv = sts->CreateTransport(socketTypes, 1, NS_ConvertUTF16toUTF8(mHost), mPort, + nullptr, getter_AddRefs(transport)); + NS_ENSURE_SUCCESS(rv, rv); + + return InitWithUnconnectedTransport(transport); +} + +void +TCPSocket::InitWithSocketChild(TCPSocketChild* aSocketBridge) +{ + mSocketBridgeChild = aSocketBridge; + mReadyState = TCPReadyState::Open; + mSocketBridgeChild->SetSocket(this); + mSocketBridgeChild->GetHost(mHost); + mSocketBridgeChild->GetPort(&mPort); +} + +nsresult +TCPSocket::InitWithTransport(nsISocketTransport* aTransport) +{ + mTransport = aTransport; + nsresult rv = CreateStream(); + NS_ENSURE_SUCCESS(rv, rv); + + mReadyState = TCPReadyState::Open; + rv = CreateInputStreamPump(); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString host; + mTransport->GetHost(host); + mHost = NS_ConvertUTF8toUTF16(host); + int32_t port; + mTransport->GetPort(&port); + mPort = port; + +#ifdef MOZ_WIDGET_GONK + nsCOMPtr<nsINetworkManager> networkManager = do_GetService("@mozilla.org/network/manager;1"); + if (networkManager) { + networkManager->GetActiveNetworkInfo(getter_AddRefs(mActiveNetworkInfo)); + } +#endif + + return NS_OK; +} + +void +TCPSocket::UpgradeToSecure(mozilla::ErrorResult& aRv) +{ + if (mReadyState != TCPReadyState::Open) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (mSsl) { + return; + } + + mSsl = true; + + if (mSocketBridgeChild) { + mSocketBridgeChild->SendStartTLS(); + return; + } + + uint32_t count = 0; + mMultiplexStream->GetCount(&count); + if (!count) { + ActivateTLS(); + } else { + mWaitingForStartTLS = true; + } +} + +namespace { +class CopierCallbacks final : public nsIRequestObserver +{ + RefPtr<TCPSocket> mOwner; +public: + explicit CopierCallbacks(TCPSocket* aSocket) : mOwner(aSocket) {} + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER +private: + ~CopierCallbacks() {} +}; + +NS_IMPL_ISUPPORTS(CopierCallbacks, nsIRequestObserver) + +NS_IMETHODIMP +CopierCallbacks::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) +{ + return NS_OK; +} + +NS_IMETHODIMP +CopierCallbacks::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, nsresult aStatus) +{ + mOwner->NotifyCopyComplete(aStatus); + mOwner = nullptr; + return NS_OK; +} +} // unnamed namespace + +nsresult +TCPSocket::EnsureCopying() +{ + if (mAsyncCopierActive) { + return NS_OK; + } + + mAsyncCopierActive = true; + RefPtr<CopierCallbacks> callbacks = new CopierCallbacks(this); + return mMultiplexStreamCopier->AsyncCopy(callbacks, nullptr); +} + +void +TCPSocket::NotifyCopyComplete(nsresult aStatus) +{ + mAsyncCopierActive = false; + + uint32_t countRemaining; + nsresult rvRemaining = mMultiplexStream->GetCount(&countRemaining); + NS_ENSURE_SUCCESS_VOID(rvRemaining); + + while (countRemaining--) { + mMultiplexStream->RemoveStream(0); + } + + while (!mPendingDataWhileCopierActive.IsEmpty()) { + nsCOMPtr<nsIInputStream> stream = mPendingDataWhileCopierActive[0]; + mMultiplexStream->AppendStream(stream); + mPendingDataWhileCopierActive.RemoveElementAt(0); + } + + if (mSocketBridgeParent) { + mozilla::Unused << mSocketBridgeParent->SendUpdateBufferedAmount(BufferedAmount(), + mTrackingNumber); + } + + if (NS_FAILED(aStatus)) { + MaybeReportErrorAndCloseIfOpen(aStatus); + return; + } + + uint32_t count; + nsresult rv = mMultiplexStream->GetCount(&count); + NS_ENSURE_SUCCESS_VOID(rv); + + if (count) { + EnsureCopying(); + return; + } + + // If we are waiting for initiating starttls, we can begin to + // activate tls now. + if (mWaitingForStartTLS && mReadyState == TCPReadyState::Open) { + ActivateTLS(); + mWaitingForStartTLS = false; + // If we have pending data, we should send them, or fire + // a drain event if we are waiting for it. + if (!mPendingDataAfterStartTLS.IsEmpty()) { + while (!mPendingDataAfterStartTLS.IsEmpty()) { + nsCOMPtr<nsIInputStream> stream = mPendingDataAfterStartTLS[0]; + mMultiplexStream->AppendStream(stream); + mPendingDataAfterStartTLS.RemoveElementAt(0); + } + EnsureCopying(); + return; + } + } + + // If we have a connected child, we let the child decide whether + // ondrain should be dispatched. + if (mWaitingForDrain && !mSocketBridgeParent) { + mWaitingForDrain = false; + FireEvent(NS_LITERAL_STRING("drain")); + } + + if (mReadyState == TCPReadyState::Closing) { + if (mSocketOutputStream) { + mSocketOutputStream->Close(); + mSocketOutputStream = nullptr; + } + mReadyState = TCPReadyState::Closed; + FireEvent(NS_LITERAL_STRING("close")); + } +} + +void +TCPSocket::ActivateTLS() +{ + nsCOMPtr<nsISupports> securityInfo; + mTransport->GetSecurityInfo(getter_AddRefs(securityInfo)); + nsCOMPtr<nsISSLSocketControl> socketControl = do_QueryInterface(securityInfo); + if (socketControl) { + socketControl->StartTLS(); + } +} + +NS_IMETHODIMP +TCPSocket::FireErrorEvent(const nsAString& aName, const nsAString& aType) +{ + if (mSocketBridgeParent) { + mSocketBridgeParent->FireErrorEvent(aName, aType, mReadyState); + return NS_OK; + } + + TCPSocketErrorEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mName = aName; + init.mMessage = aType; + + RefPtr<TCPSocketErrorEvent> event = + TCPSocketErrorEvent::Constructor(this, NS_LITERAL_STRING("error"), init); + MOZ_ASSERT(event); + event->SetTrusted(true); + bool dummy; + DispatchEvent(event, &dummy); + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::FireEvent(const nsAString& aType) +{ + if (mSocketBridgeParent) { + mSocketBridgeParent->FireEvent(aType, mReadyState); + return NS_OK; + } + + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(GetOwnerGlobal()))) { + return NS_ERROR_FAILURE; + } + JS::Rooted<JS::Value> val(api.cx()); + return FireDataEvent(api.cx(), aType, val); +} + +NS_IMETHODIMP +TCPSocket::FireDataArrayEvent(const nsAString& aType, + const InfallibleTArray<uint8_t>& buffer) +{ + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(GetOwnerGlobal()))) { + return NS_ERROR_FAILURE; + } + JSContext* cx = api.cx(); + JS::Rooted<JS::Value> val(cx); + + bool ok = IPC::DeserializeArrayBuffer(cx, buffer, &val); + if (ok) { + return FireDataEvent(cx, aType, val); + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +TCPSocket::FireDataStringEvent(const nsAString& aType, + const nsACString& aString) +{ + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(GetOwnerGlobal()))) { + return NS_ERROR_FAILURE; + } + JSContext* cx = api.cx(); + JS::Rooted<JS::Value> val(cx); + + bool ok = ToJSValue(cx, NS_ConvertASCIItoUTF16(aString), &val); + if (ok) { + return FireDataEvent(cx, aType, val); + } + return NS_ERROR_FAILURE; +} + +nsresult +TCPSocket::FireDataEvent(JSContext* aCx, const nsAString& aType, JS::Handle<JS::Value> aData) +{ + MOZ_ASSERT(!mSocketBridgeParent); + + RootedDictionary<TCPSocketEventInit> init(aCx); + init.mBubbles = false; + init.mCancelable = false; + init.mData = aData; + + RefPtr<TCPSocketEvent> event = + TCPSocketEvent::Constructor(this, aType, init); + event->SetTrusted(true); + bool dummy; + DispatchEvent(event, &dummy); + return NS_OK; +} + +JSObject* +TCPSocket::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return TCPSocketBinding::Wrap(aCx, this, aGivenProto); +} + +void +TCPSocket::GetHost(nsAString& aHost) +{ + aHost.Assign(mHost); +} + +uint32_t +TCPSocket::Port() +{ + return mPort; +} + +bool +TCPSocket::Ssl() +{ + return mSsl; +} + +uint64_t +TCPSocket::BufferedAmount() +{ + if (mSocketBridgeChild) { + return mBufferedAmount; + } + if (mMultiplexStream) { + uint64_t available = 0; + mMultiplexStream->Available(&available); + return available; + } + return 0; +} + +void +TCPSocket::Suspend() +{ + if (mSocketBridgeChild) { + mSocketBridgeChild->SendSuspend(); + return; + } + if (mInputStreamPump) { + mInputStreamPump->Suspend(); + } + mSuspendCount++; +} + +void +TCPSocket::Resume(mozilla::ErrorResult& aRv) +{ + if (mSocketBridgeChild) { + mSocketBridgeChild->SendResume(); + return; + } + if (!mSuspendCount) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (mInputStreamPump) { + mInputStreamPump->Resume(); + } + mSuspendCount--; +} + +nsresult +TCPSocket::MaybeReportErrorAndCloseIfOpen(nsresult status) { +#ifdef MOZ_WIDGET_GONK + // Save network statistics once the connection is closed. + // For now this function is Gonk-specific. + SaveNetworkStats(true); +#endif + + // If we're closed, we've already reported the error or just don't need to + // report the error. + if (mReadyState == TCPReadyState::Closed) { + return NS_OK; + } + + // go through ::Closing state and then mark ::Closed + Close(); + mReadyState = TCPReadyState::Closed; + + if (NS_FAILED(status)) { + // Convert the status code to an appropriate error message. + + nsString errorType, errName; + + // security module? (and this is an error) + if ((static_cast<uint32_t>(status) & 0xFF0000) == 0x5a0000) { + nsCOMPtr<nsINSSErrorsService> errSvc = do_GetService("@mozilla.org/nss_errors_service;1"); + // getErrorClass will throw a generic NS_ERROR_FAILURE if the error code is + // somehow not in the set of covered errors. + uint32_t errorClass; + nsresult rv = errSvc->GetErrorClass(status, &errorClass); + if (NS_FAILED(rv)) { + errorType.AssignLiteral("SecurityProtocol"); + } else { + switch (errorClass) { + case nsINSSErrorsService::ERROR_CLASS_BAD_CERT: + errorType.AssignLiteral("SecurityCertificate"); + break; + default: + errorType.AssignLiteral("SecurityProtocol"); + break; + } + } + + // NSS_SEC errors (happen below the base value because of negative vals) + if ((static_cast<int32_t>(status) & 0xFFFF) < abs(nsINSSErrorsService::NSS_SEC_ERROR_BASE)) { + switch (static_cast<SECErrorCodes>(status)) { + case SEC_ERROR_EXPIRED_CERTIFICATE: + errName.AssignLiteral("SecurityExpiredCertificateError"); + break; + case SEC_ERROR_REVOKED_CERTIFICATE: + errName.AssignLiteral("SecurityRevokedCertificateError"); + break; + // per bsmith, we will be unable to tell these errors apart very soon, + // so it makes sense to just folder them all together already. + case SEC_ERROR_UNKNOWN_ISSUER: + case SEC_ERROR_UNTRUSTED_ISSUER: + case SEC_ERROR_UNTRUSTED_CERT: + case SEC_ERROR_CA_CERT_INVALID: + errName.AssignLiteral("SecurityUntrustedCertificateIssuerError"); + break; + case SEC_ERROR_INADEQUATE_KEY_USAGE: + errName.AssignLiteral("SecurityInadequateKeyUsageError"); + break; + case SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED: + errName.AssignLiteral("SecurityCertificateSignatureAlgorithmDisabledError"); + break; + default: + errName.AssignLiteral("SecurityError"); + break; + } + } else { + // NSS_SSL errors + switch (static_cast<SSLErrorCodes>(status)) { + case SSL_ERROR_NO_CERTIFICATE: + errName.AssignLiteral("SecurityNoCertificateError"); + break; + case SSL_ERROR_BAD_CERTIFICATE: + errName.AssignLiteral("SecurityBadCertificateError"); + break; + case SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE: + errName.AssignLiteral("SecurityUnsupportedCertificateTypeError"); + break; + case SSL_ERROR_UNSUPPORTED_VERSION: + errName.AssignLiteral("SecurityUnsupportedTLSVersionError"); + break; + case SSL_ERROR_BAD_CERT_DOMAIN: + errName.AssignLiteral("SecurityCertificateDomainMismatchError"); + break; + default: + errName.AssignLiteral("SecurityError"); + break; + } + } + } else { + // must be network + errorType.AssignLiteral("Network"); + + switch (status) { + // connect to host:port failed + case NS_ERROR_CONNECTION_REFUSED: + errName.AssignLiteral("ConnectionRefusedError"); + break; + // network timeout error + case NS_ERROR_NET_TIMEOUT: + errName.AssignLiteral("NetworkTimeoutError"); + break; + // hostname lookup failed + case NS_ERROR_UNKNOWN_HOST: + errName.AssignLiteral("DomainNotFoundError"); + break; + case NS_ERROR_NET_INTERRUPT: + errName.AssignLiteral("NetworkInterruptError"); + break; + default: + errName.AssignLiteral("NetworkError"); + break; + } + } + + Unused << NS_WARN_IF(NS_FAILED(FireErrorEvent(errName, errorType))); + } + + return FireEvent(NS_LITERAL_STRING("close")); +} + +void +TCPSocket::Close() +{ + CloseHelper(true); +} + +void +TCPSocket::CloseImmediately() +{ + CloseHelper(false); +} + +void +TCPSocket::CloseHelper(bool waitForUnsentData) +{ + if (mReadyState == TCPReadyState::Closed || mReadyState == TCPReadyState::Closing) { + return; + } + + mReadyState = TCPReadyState::Closing; + + if (mSocketBridgeChild) { + mSocketBridgeChild->SendClose(); + return; + } + + uint32_t count = 0; + if (mMultiplexStream) { + mMultiplexStream->GetCount(&count); + } + if (!count || !waitForUnsentData) { + if (mSocketOutputStream) { + mSocketOutputStream->Close(); + mSocketOutputStream = nullptr; + } + } + + if (mSocketInputStream) { + mSocketInputStream->Close(); + mSocketInputStream = nullptr; + } +} + +void +TCPSocket::SendWithTrackingNumber(const nsACString& aData, + const uint32_t& aTrackingNumber, + mozilla::ErrorResult& aRv) +{ + MOZ_ASSERT(mSocketBridgeParent); + mTrackingNumber = aTrackingNumber; + // The JSContext isn't necessary for string values; it's a codegen limitation. + Send(nullptr, aData, aRv); +} + +bool +TCPSocket::Send(JSContext* aCx, const nsACString& aData, mozilla::ErrorResult& aRv) +{ + if (mReadyState != TCPReadyState::Open) { + aRv.Throw(NS_ERROR_FAILURE); + return false; + } + + uint64_t byteLength; + nsCOMPtr<nsIInputStream> stream; + if (mSocketBridgeChild) { + mSocketBridgeChild->SendSend(aData, ++mTrackingNumber); + byteLength = aData.Length(); + } else { + nsresult rv = NS_NewCStringInputStream(getter_AddRefs(stream), aData); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return false; + } + rv = stream->Available(&byteLength); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return false; + } + } + return Send(stream, byteLength); +} + +void +TCPSocket::SendWithTrackingNumber(JSContext* aCx, + const ArrayBuffer& aData, + uint32_t aByteOffset, + const Optional<uint32_t>& aByteLength, + const uint32_t& aTrackingNumber, + mozilla::ErrorResult& aRv) +{ + MOZ_ASSERT(mSocketBridgeParent); + mTrackingNumber = aTrackingNumber; + Send(aCx, aData, aByteOffset, aByteLength, aRv); +} + +bool +TCPSocket::Send(JSContext* aCx, + const ArrayBuffer& aData, + uint32_t aByteOffset, + const Optional<uint32_t>& aByteLength, + mozilla::ErrorResult& aRv) +{ + if (mReadyState != TCPReadyState::Open) { + aRv.Throw(NS_ERROR_FAILURE); + return false; + } + + nsCOMPtr<nsIArrayBufferInputStream> stream; + + aData.ComputeLengthAndData(); + uint32_t byteLength = aByteLength.WasPassed() ? aByteLength.Value() : aData.Length(); + + if (mSocketBridgeChild) { + nsresult rv = mSocketBridgeChild->SendSend(aData, aByteOffset, byteLength, ++mTrackingNumber); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return false; + } + } else { + JS::Rooted<JSObject*> obj(aCx, aData.Obj()); + JSAutoCompartment ac(aCx, obj); + JS::Rooted<JS::Value> value(aCx, JS::ObjectValue(*obj)); + + stream = do_CreateInstance("@mozilla.org/io/arraybuffer-input-stream;1"); + nsresult rv = stream->SetData(value, aByteOffset, byteLength, aCx); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return false; + } + } + return Send(stream, byteLength); +} + +bool +TCPSocket::Send(nsIInputStream* aStream, uint32_t aByteLength) +{ + uint64_t newBufferedAmount = BufferedAmount() + aByteLength; + bool bufferFull = newBufferedAmount > BUFFER_SIZE; + if (bufferFull) { + // If we buffered more than some arbitrary amount of data, + // (65535 right now) we should tell the caller so they can + // wait until ondrain is called if they so desire. Once all the + // buffered data has been written to the socket, ondrain is + // called. + mWaitingForDrain = true; + } + + if (mSocketBridgeChild) { + // In the child, we just add the buffer length to our bufferedAmount and let + // the parent update our bufferedAmount when the data have been sent. + mBufferedAmount = newBufferedAmount; + return !bufferFull; + } + + if (mWaitingForStartTLS) { + // When we are waiting for starttls, newStream is added to pendingData + // and will be appended to multiplexStream after tls had been set up. + mPendingDataAfterStartTLS.AppendElement(aStream); + } else if (mAsyncCopierActive) { + // While the AsyncCopier is still active.. + mPendingDataWhileCopierActive.AppendElement(aStream); + } else { + mMultiplexStream->AppendStream(aStream); + } + + EnsureCopying(); + +#ifdef MOZ_WIDGET_GONK + // Collect transmitted amount for network statistics. + mTxBytes += aByteLength; + SaveNetworkStats(false); +#endif + + return !bufferFull; +} + +TCPReadyState +TCPSocket::ReadyState() +{ + return mReadyState; +} + +TCPSocketBinaryType +TCPSocket::BinaryType() +{ + if (mUseArrayBuffers) { + return TCPSocketBinaryType::Arraybuffer; + } else { + return TCPSocketBinaryType::String; + } +} + +already_AddRefed<TCPSocket> +TCPSocket::CreateAcceptedSocket(nsIGlobalObject* aGlobal, + nsISocketTransport* aTransport, + bool aUseArrayBuffers) +{ + RefPtr<TCPSocket> socket = new TCPSocket(aGlobal, EmptyString(), 0, false, aUseArrayBuffers); + nsresult rv = socket->InitWithTransport(aTransport); + NS_ENSURE_SUCCESS(rv, nullptr); + return socket.forget(); +} + +already_AddRefed<TCPSocket> +TCPSocket::CreateAcceptedSocket(nsIGlobalObject* aGlobal, + TCPSocketChild* aBridge, + bool aUseArrayBuffers) +{ + RefPtr<TCPSocket> socket = new TCPSocket(aGlobal, EmptyString(), 0, false, aUseArrayBuffers); + socket->InitWithSocketChild(aBridge); + return socket.forget(); +} + +already_AddRefed<TCPSocket> +TCPSocket::Constructor(const GlobalObject& aGlobal, + const nsAString& aHost, + uint16_t aPort, + const SocketOptions& aOptions, + mozilla::ErrorResult& aRv) +{ + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<TCPSocket> socket = + new TCPSocket(global, aHost, aPort, aOptions.mUseSecureTransport, + aOptions.mBinaryType == TCPSocketBinaryType::Arraybuffer); + nsresult rv = socket->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return nullptr; + } + + return socket.forget(); +} + +nsresult +TCPSocket::CreateInputStreamPump() +{ + if (!mSocketInputStream) { + return NS_ERROR_NOT_AVAILABLE; + } + nsresult rv; + mInputStreamPump = do_CreateInstance("@mozilla.org/network/input-stream-pump;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mInputStreamPump->Init(mSocketInputStream, -1, -1, 0, 0, false); + NS_ENSURE_SUCCESS(rv, rv); + + uint64_t suspendCount = mSuspendCount; + while (suspendCount--) { + mInputStreamPump->Suspend(); + } + + rv = mInputStreamPump->AsyncRead(this, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnTransportStatus(nsITransport* aTransport, nsresult aStatus, + int64_t aProgress, int64_t aProgressMax) +{ + if (static_cast<uint32_t>(aStatus) != nsISocketTransport::STATUS_CONNECTED_TO) { + return NS_OK; + } + + mReadyState = TCPReadyState::Open; + FireEvent(NS_LITERAL_STRING("open")); + + nsresult rv = CreateInputStreamPump(); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnInputStreamReady(nsIAsyncInputStream* aStream) +{ + // Only used for detecting if the connection was refused. + + uint64_t dummy; + nsresult rv = aStream->Available(&dummy); + if (NS_FAILED(rv)) { + MaybeReportErrorAndCloseIfOpen(NS_ERROR_CONNECTION_REFUSED); + } + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) +{ + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnDataAvailable(nsIRequest* aRequest, nsISupports* aContext, nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) +{ +#ifdef MOZ_WIDGET_GONK + // Collect received amount for network statistics. + mRxBytes += aCount; + SaveNetworkStats(false); +#endif + + if (mUseArrayBuffers) { + nsTArray<uint8_t> buffer; + buffer.SetCapacity(aCount); + uint32_t actual; + nsresult rv = aStream->Read(reinterpret_cast<char*>(buffer.Elements()), aCount, &actual); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(actual == aCount); + buffer.SetLength(actual); + + if (mSocketBridgeParent) { + mSocketBridgeParent->FireArrayBufferDataEvent(buffer, mReadyState); + return NS_OK; + } + + AutoJSAPI api; + if (!api.Init(GetOwnerGlobal())) { + return NS_ERROR_FAILURE; + } + JSContext* cx = api.cx(); + + JS::Rooted<JS::Value> value(cx); + if (!ToJSValue(cx, TypedArrayCreator<ArrayBuffer>(buffer), &value)) { + return NS_ERROR_FAILURE; + } + FireDataEvent(cx, NS_LITERAL_STRING("data"), value); + return NS_OK; + } + + nsCString data; + nsresult rv = mInputStreamScriptable->ReadBytes(aCount, data); + NS_ENSURE_SUCCESS(rv, rv); + + if (mSocketBridgeParent) { + mSocketBridgeParent->FireStringDataEvent(data, mReadyState); + return NS_OK; + } + + AutoJSAPI api; + if (!api.Init(GetOwnerGlobal())) { + return NS_ERROR_FAILURE; + } + JSContext* cx = api.cx(); + + JS::Rooted<JS::Value> value(cx); + if (!ToJSValue(cx, NS_ConvertASCIItoUTF16(data), &value)) { + return NS_ERROR_FAILURE; + } + FireDataEvent(cx, NS_LITERAL_STRING("data"), value); + + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, nsresult aStatus) +{ + uint32_t count; + nsresult rv = mMultiplexStream->GetCount(&count); + NS_ENSURE_SUCCESS(rv, rv); + bool bufferedOutput = count != 0; + + mInputStreamPump = nullptr; + + if (bufferedOutput && NS_SUCCEEDED(aStatus)) { + // If we have some buffered output still, and status is not an + // error, the other side has done a half-close, but we don't + // want to be in the close state until we are done sending + // everything that was buffered. We also don't want to call onclose + // yet. + return NS_OK; + } + + // We call this even if there is no error. + MaybeReportErrorAndCloseIfOpen(aStatus); + return NS_OK; +} + +void +TCPSocket::SetSocketBridgeParent(TCPSocketParent* aBridgeParent) +{ + mSocketBridgeParent = aBridgeParent; +} + +void +TCPSocket::SetAppIdAndBrowser(uint32_t aAppId, bool aInIsolatedMozBrowser) +{ +#ifdef MOZ_WIDGET_GONK + mAppId = aAppId; + mInIsolatedMozBrowser = aInIsolatedMozBrowser; +#endif +} + +NS_IMETHODIMP +TCPSocket::UpdateReadyState(uint32_t aReadyState) +{ + MOZ_ASSERT(mSocketBridgeChild); + mReadyState = static_cast<TCPReadyState>(aReadyState); + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::UpdateBufferedAmount(uint32_t aBufferedAmount, uint32_t aTrackingNumber) +{ + if (aTrackingNumber != mTrackingNumber) { + return NS_OK; + } + mBufferedAmount = aBufferedAmount; + if (!mBufferedAmount) { + if (mWaitingForDrain) { + mWaitingForDrain = false; + return FireEvent(NS_LITERAL_STRING("drain")); + } + } + return NS_OK; +} + +#ifdef MOZ_WIDGET_GONK +void +TCPSocket::SaveNetworkStats(bool aEnforce) +{ + if (!mTxBytes && !mRxBytes) { + // There is no traffic at all. No need to save statistics. + return; + } + + // If "enforce" is false, the traffic amount is saved to NetworkStatsServiceProxy + // only when the total amount exceeds the predefined threshold value. + // The purpose is to avoid too much overhead for collecting statistics. + uint32_t totalBytes = mTxBytes + mRxBytes; + if (!aEnforce && totalBytes < NETWORK_STATS_THRESHOLD) { + return; + } + + nsCOMPtr<nsINetworkStatsServiceProxy> nssProxy = + do_GetService("@mozilla.org/networkstatsServiceProxy;1"); + if (!nssProxy) { + return; + } + + nssProxy->SaveAppStats(mAppId, mInIsolatedMozBrowser, mActiveNetworkInfo, + PR_Now(), mRxBytes, mTxBytes, false, nullptr); + + // Reset the counters once the statistics is saved to NetworkStatsServiceProxy. + mTxBytes = mRxBytes = 0; +} +#endif + +NS_IMETHODIMP +TCPSocket::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) +{ + if (!strcmp(aTopic, "inner-window-destroyed")) { + nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject); + NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); + uint64_t innerID; + nsresult rv = wrapper->GetData(&innerID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (innerID == mInnerWindowID) { + Close(); + } + } else if (!strcmp(aTopic, "profile-change-net-teardown")) { + Close(); + } + + return NS_OK; +} + +/* static */ +bool +TCPSocket::ShouldTCPSocketExist(JSContext* aCx, JSObject* aGlobal) +{ + JS::Rooted<JSObject*> global(aCx, aGlobal); + return nsContentUtils::IsSystemPrincipal(nsContentUtils::ObjectPrincipal(global)); +} diff --git a/dom/network/TCPSocket.h b/dom/network/TCPSocket.h new file mode 100644 index 0000000000..e98c03ca5a --- /dev/null +++ b/dom/network/TCPSocket.h @@ -0,0 +1,267 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_TCPSocket_h +#define mozilla_dom_TCPSocket_h + +#include "mozilla/dom/TCPSocketBinding.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "nsITransport.h" +#include "nsIStreamListener.h" +#include "nsIAsyncInputStream.h" +#include "nsISupportsImpl.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" +#include "nsITCPSocketCallback.h" +#include "js/RootingAPI.h" + +class nsISocketTransport; +class nsIInputStreamPump; +class nsIScriptableInputStream; +class nsIBinaryInputStream; +class nsIMultiplexInputStream; +class nsIAsyncStreamCopier; +class nsIInputStream; +class nsINetworkInfo; + +namespace mozilla { +class ErrorResult; +namespace dom { + +class DOMError; +struct ServerSocketOptions; +class TCPServerSocket; +class TCPSocketChild; +class TCPSocketParent; + +// This interface is only used for legacy navigator.mozTCPSocket API compatibility. +class LegacyMozTCPSocket : public nsISupports +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(LegacyMozTCPSocket) + + explicit LegacyMozTCPSocket(nsPIDOMWindowInner* aWindow); + + already_AddRefed<TCPServerSocket> + Listen(uint16_t aPort, + const ServerSocketOptions& aOptions, + uint16_t aBacklog, + ErrorResult& aRv); + + already_AddRefed<TCPSocket> + Open(const nsAString& aHost, + uint16_t aPort, + const SocketOptions& aOptions, + ErrorResult& aRv); + + bool WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aReflector); + +private: + virtual ~LegacyMozTCPSocket(); + + nsCOMPtr<nsIGlobalObject> mGlobal; +}; + +class TCPSocket final : public DOMEventTargetHelper + , public nsIStreamListener + , public nsITransportEventSink + , public nsIInputStreamCallback + , public nsIObserver + , public nsSupportsWeakReference + , public nsITCPSocketCallback +{ +public: + TCPSocket(nsIGlobalObject* aGlobal, const nsAString& aHost, uint16_t aPort, + bool aSsl, bool aUseArrayBuffers); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(TCPSocket, DOMEventTargetHelper) + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITRANSPORTEVENTSINK + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIOBSERVER + NS_DECL_NSITCPSOCKETCALLBACK + + virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + static bool ShouldTCPSocketExist(JSContext* aCx, JSObject* aGlobal); + + void GetHost(nsAString& aHost); + uint32_t Port(); + bool Ssl(); + uint64_t BufferedAmount(); + void Suspend(); + void Resume(ErrorResult& aRv); + void Close(); + void CloseImmediately(); + bool Send(JSContext* aCx, const nsACString& aData, ErrorResult& aRv); + bool Send(JSContext* aCx, + const ArrayBuffer& aData, + uint32_t aByteOffset, + const Optional<uint32_t>& aByteLength, + ErrorResult& aRv); + TCPReadyState ReadyState(); + TCPSocketBinaryType BinaryType(); + void UpgradeToSecure(ErrorResult& aRv); + + static already_AddRefed<TCPSocket> + Constructor(const GlobalObject& aGlobal, + const nsAString& aHost, + uint16_t aPort, + const SocketOptions& aOptions, + ErrorResult& aRv); + + // Perform a send operation that's asssociated with a sequence number. Used in + // IPC scenarios to track the number of bytes buffered at any given time. + void SendWithTrackingNumber(const nsACString& aData, + const uint32_t& aTrackingNumber, + ErrorResult& aRv); + void SendWithTrackingNumber(JSContext* aCx, + const ArrayBuffer& aData, + uint32_t aByteOffset, + const Optional<uint32_t>& aByteLength, + const uint32_t& aTrackingNumber, + ErrorResult& aRv); + // Create a TCPSocket object from an existing low-level socket connection. + // Used by the TCPServerSocket implementation when a new connection is accepted. + static already_AddRefed<TCPSocket> + CreateAcceptedSocket(nsIGlobalObject* aGlobal, nsISocketTransport* aTransport, bool aUseArrayBuffers); + // Create a TCPSocket object from an existing child-side IPC actor. + // Used by the TCPServerSocketChild implementation when a new connection is accepted. + static already_AddRefed<TCPSocket> + CreateAcceptedSocket(nsIGlobalObject* aGlobal, TCPSocketChild* aSocketBridge, bool aUseArrayBuffers); + + // Initialize this socket's associated app and browser information. + void SetAppIdAndBrowser(uint32_t aAppId, bool aInBrowser); + // Initialize this socket's associated IPC actor in the parent process. + void SetSocketBridgeParent(TCPSocketParent* aBridgeParent); + + static bool SocketEnabled(); + + IMPL_EVENT_HANDLER(open); + IMPL_EVENT_HANDLER(drain); + IMPL_EVENT_HANDLER(data); + IMPL_EVENT_HANDLER(error); + IMPL_EVENT_HANDLER(close); + + nsresult Init(); + + // Inform this socket that a buffered send() has completed sending. + void NotifyCopyComplete(nsresult aStatus); + + // Initialize this socket from a low-level connection that hasn't connected yet + // (called from RecvOpenBind() in TCPSocketParent). + nsresult InitWithUnconnectedTransport(nsISocketTransport* aTransport); + +private: + ~TCPSocket(); + + // Initialize this socket with an existing IPC actor. + void InitWithSocketChild(TCPSocketChild* aBridge); + // Initialize this socket from an existing low-level connection. + nsresult InitWithTransport(nsISocketTransport* aTransport); + // Initialize the input/output streams for this socket object. + nsresult CreateStream(); + // Initialize the asynchronous read operation from this socket's input stream. + nsresult CreateInputStreamPump(); + // Send the contents of the provided input stream, which is assumed to be the given length + // for reporting and buffering purposes. + bool Send(nsIInputStream* aStream, uint32_t aByteLength); + // Begin an asynchronous copy operation if one is not already in progress. + nsresult EnsureCopying(); + // Enable TLS on this socket. + void ActivateTLS(); + // Dispatch an error event if necessary, then dispatch a "close" event. + nsresult MaybeReportErrorAndCloseIfOpen(nsresult status); +#ifdef MOZ_WIDGET_GONK + // Store and reset any saved network stats for this socket. + void SaveNetworkStats(bool aEnforce); +#endif + + // Helper for FireDataStringEvent/FireDataArrayEvent. + nsresult FireDataEvent(JSContext* aCx, const nsAString& aType, + JS::Handle<JS::Value> aData); + // Helper for Close/CloseImmediately + void CloseHelper(bool waitForUnsentData); + + TCPReadyState mReadyState; + // Whether to use strings or array buffers for the "data" event. + bool mUseArrayBuffers; + nsString mHost; + uint16_t mPort; + // Whether this socket is using a secure transport. + bool mSsl; + + // The associated IPC actor in a child process. + RefPtr<TCPSocketChild> mSocketBridgeChild; + // The associated IPC actor in a parent process. + RefPtr<TCPSocketParent> mSocketBridgeParent; + + // Raw socket streams + nsCOMPtr<nsISocketTransport> mTransport; + nsCOMPtr<nsIInputStream> mSocketInputStream; + nsCOMPtr<nsIOutputStream> mSocketOutputStream; + + // Input stream machinery + nsCOMPtr<nsIInputStreamPump> mInputStreamPump; + nsCOMPtr<nsIScriptableInputStream> mInputStreamScriptable; + nsCOMPtr<nsIBinaryInputStream> mInputStreamBinary; + + // Output stream machinery + nsCOMPtr<nsIMultiplexInputStream> mMultiplexStream; + nsCOMPtr<nsIAsyncStreamCopier> mMultiplexStreamCopier; + + // Is there an async copy operation in progress? + bool mAsyncCopierActive; + // True if the buffer is full and a "drain" event is expected by the client. + bool mWaitingForDrain; + + // The id of the window that created this socket. + uint64_t mInnerWindowID; + + // The current number of buffered bytes. Only used in content processes when IPC is enabled. + uint64_t mBufferedAmount; + + // The number of times this socket has had `Suspend` called without a corresponding `Resume`. + uint32_t mSuspendCount; + + // The current sequence number (ie. number of send operations) that have been processed. + // This is used in the IPC scenario by the child process to filter out outdated notifications + // about the amount of buffered data present in the parent process. + uint32_t mTrackingNumber; + + // True if this socket has been upgraded to secure after the initial connection, + // but the actual upgrade is waiting for an in-progress copy operation to complete. + bool mWaitingForStartTLS; + // The buffered data awaiting the TLS upgrade to finish. + nsTArray<nsCOMPtr<nsIInputStream>> mPendingDataAfterStartTLS; + + // The data to be sent while AsyncCopier is still active. + nsTArray<nsCOMPtr<nsIInputStream>> mPendingDataWhileCopierActive; + + bool mObserversActive; + +#ifdef MOZ_WIDGET_GONK + // Number of bytes sent. + uint32_t mTxBytes; + // Number of bytes received. + uint32_t mRxBytes; + // The app that owns this socket. + uint32_t mAppId; + // Was this socket created inside of an isolated browser frame? + bool mInIsolatedMozBrowser; + // The name of the active network used by this socket. + nsCOMPtr<nsINetworkInfo> mActiveNetworkInfo; +#endif +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_TCPSocket_h diff --git a/dom/network/TCPSocketChild.cpp b/dom/network/TCPSocketChild.cpp new file mode 100644 index 0000000000..8eb19a1db5 --- /dev/null +++ b/dom/network/TCPSocketChild.cpp @@ -0,0 +1,254 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include <algorithm> +#include "TCPSocketChild.h" +#include "mozilla/Unused.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/dom/PBrowserChild.h" +#include "mozilla/dom/TabChild.h" +#include "nsITCPSocketCallback.h" +#include "TCPSocket.h" +#include "nsContentUtils.h" +#include "jsapi.h" +#include "jsfriendapi.h" + +using mozilla::net::gNeckoChild; + +namespace IPC { + +bool +DeserializeArrayBuffer(JSContext* cx, + const InfallibleTArray<uint8_t>& aBuffer, + JS::MutableHandle<JS::Value> aVal) +{ + mozilla::UniquePtr<uint8_t[], JS::FreePolicy> data(js_pod_malloc<uint8_t>(aBuffer.Length())); + if (!data) + return false; + memcpy(data.get(), aBuffer.Elements(), aBuffer.Length()); + + JSObject* obj = JS_NewArrayBufferWithContents(cx, aBuffer.Length(), data.get()); + if (!obj) + return false; + // If JS_NewArrayBufferWithContents returns non-null, the ownership of + // the data is transfered to obj, so we release the ownership here. + mozilla::Unused << data.release(); + + aVal.setObject(*obj); + return true; +} + +} // namespace IPC + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(TCPSocketChildBase) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(TCPSocketChildBase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocket) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(TCPSocketChildBase) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocket) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(TCPSocketChildBase) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(TCPSocketChildBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TCPSocketChildBase) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TCPSocketChildBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +TCPSocketChildBase::TCPSocketChildBase() +: mIPCOpen(false) +{ + mozilla::HoldJSObjects(this); +} + +TCPSocketChildBase::~TCPSocketChildBase() +{ + mozilla::DropJSObjects(this); +} + +NS_IMETHODIMP_(MozExternalRefCountType) TCPSocketChild::Release(void) +{ + nsrefcnt refcnt = TCPSocketChildBase::Release(); + if (refcnt == 1 && mIPCOpen) { + PTCPSocketChild::SendRequestDelete(); + return 1; + } + return refcnt; +} + +TCPSocketChild::TCPSocketChild(const nsAString& aHost, const uint16_t& aPort) +: mHost(aHost) +, mPort(aPort) +{ +} + +void +TCPSocketChild::SendOpen(nsITCPSocketCallback* aSocket, bool aUseSSL, bool aUseArrayBuffers) +{ + mSocket = aSocket; + + AddIPDLReference(); + gNeckoChild->SendPTCPSocketConstructor(this, mHost, mPort); + MOZ_ASSERT(mFilterName.IsEmpty()); // Currently nobody should use this + PTCPSocketChild::SendOpen(mHost, mPort, aUseSSL, aUseArrayBuffers); +} + +void +TCPSocketChild::SendWindowlessOpenBind(nsITCPSocketCallback* aSocket, + const nsACString& aRemoteHost, uint16_t aRemotePort, + const nsACString& aLocalHost, uint16_t aLocalPort, + bool aUseSSL) +{ + mSocket = aSocket; + AddIPDLReference(); + gNeckoChild->SendPTCPSocketConstructor(this, + NS_ConvertUTF8toUTF16(aRemoteHost), + aRemotePort); + PTCPSocketChild::SendOpenBind(nsCString(aRemoteHost), aRemotePort, + nsCString(aLocalHost), aLocalPort, + aUseSSL, true, mFilterName); +} + +void +TCPSocketChildBase::ReleaseIPDLReference() +{ + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; + mSocket = nullptr; + this->Release(); +} + +void +TCPSocketChildBase::AddIPDLReference() +{ + MOZ_ASSERT(!mIPCOpen); + mIPCOpen = true; + this->AddRef(); +} + +TCPSocketChild::~TCPSocketChild() +{ +} + +bool +TCPSocketChild::RecvUpdateBufferedAmount(const uint32_t& aBuffered, + const uint32_t& aTrackingNumber) +{ + mSocket->UpdateBufferedAmount(aBuffered, aTrackingNumber); + return true; +} + +bool +TCPSocketChild::RecvCallback(const nsString& aType, + const CallbackData& aData, + const uint32_t& aReadyState) +{ + mSocket->UpdateReadyState(aReadyState); + + if (aData.type() == CallbackData::Tvoid_t) { + mSocket->FireEvent(aType); + + } else if (aData.type() == CallbackData::TTCPError) { + const TCPError& err(aData.get_TCPError()); + mSocket->FireErrorEvent(err.name(), err.message()); + + } else if (aData.type() == CallbackData::TSendableData) { + const SendableData& data = aData.get_SendableData(); + + if (data.type() == SendableData::TArrayOfuint8_t) { + mSocket->FireDataArrayEvent(aType, data.get_ArrayOfuint8_t()); + } else if (data.type() == SendableData::TnsCString) { + mSocket->FireDataStringEvent(aType, data.get_nsCString()); + } else { + MOZ_CRASH("Invalid callback data type!"); + } + } else { + MOZ_CRASH("Invalid callback type!"); + } + return true; +} + +void +TCPSocketChild::SendSend(const nsACString& aData, uint32_t aTrackingNumber) +{ + SendData(nsCString(aData), aTrackingNumber); +} + +nsresult +TCPSocketChild::SendSend(const ArrayBuffer& aData, + uint32_t aByteOffset, + uint32_t aByteLength, + uint32_t aTrackingNumber) +{ + uint32_t buflen = aData.Length(); + uint32_t offset = std::min(buflen, aByteOffset); + uint32_t nbytes = std::min(buflen - aByteOffset, aByteLength); + FallibleTArray<uint8_t> fallibleArr; + if (!fallibleArr.InsertElementsAt(0, aData.Data() + offset, nbytes, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + InfallibleTArray<uint8_t> arr; + arr.SwapElements(fallibleArr); + SendData(arr, aTrackingNumber); + return NS_OK; +} + +NS_IMETHODIMP +TCPSocketChild::SendSendArray(nsTArray<uint8_t>& aArray, uint32_t aTrackingNumber) +{ + SendData(aArray, aTrackingNumber); + return NS_OK; +} + +void +TCPSocketChild::SetSocket(TCPSocket* aSocket) +{ + mSocket = aSocket; +} + +void +TCPSocketChild::GetHost(nsAString& aHost) +{ + aHost = mHost; +} + +void +TCPSocketChild::GetPort(uint16_t* aPort) +{ + *aPort = mPort; +} + +nsresult +TCPSocketChild::SetFilterName(const nsACString& aFilterName) +{ + if (!mFilterName.IsEmpty()) { + // filter name can only be set once. + return NS_ERROR_FAILURE; + } + mFilterName = aFilterName; + return NS_OK; +} + +bool +TCPSocketChild::RecvRequestDelete() +{ + mozilla::Unused << Send__delete__(this); + return true; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/network/TCPSocketChild.h b/dom/network/TCPSocketChild.h new file mode 100644 index 0000000000..af233d3114 --- /dev/null +++ b/dom/network/TCPSocketChild.h @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_TCPSocketChild_h +#define mozilla_dom_TCPSocketChild_h + +#include "mozilla/dom/TypedArray.h" +#include "mozilla/net/PTCPSocketChild.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" +#include "js/TypeDecls.h" + +class nsITCPSocketCallback; + +namespace IPC { +bool +DeserializeArrayBuffer(JSContext* cx, + const InfallibleTArray<uint8_t>& aBuffer, + JS::MutableHandle<JS::Value> aVal); +} + +namespace mozilla { +namespace dom { + +class TCPSocket; + +class TCPSocketChildBase : public nsISupports { +public: + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(TCPSocketChildBase) + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + void AddIPDLReference(); + void ReleaseIPDLReference(); + +protected: + TCPSocketChildBase(); + virtual ~TCPSocketChildBase(); + + nsCOMPtr<nsITCPSocketCallback> mSocket; + bool mIPCOpen; +}; + +class TCPSocketChild : public mozilla::net::PTCPSocketChild + , public TCPSocketChildBase +{ +public: + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + TCPSocketChild(const nsAString& aHost, const uint16_t& aPort); + ~TCPSocketChild(); + + void SendOpen(nsITCPSocketCallback* aSocket, bool aUseSSL, bool aUseArrayBuffers); + void SendWindowlessOpenBind(nsITCPSocketCallback* aSocket, + const nsACString& aRemoteHost, uint16_t aRemotePort, + const nsACString& aLocalHost, uint16_t aLocalPort, + bool aUseSSL); + NS_IMETHOD SendSendArray(nsTArray<uint8_t>& aArray, + uint32_t aTrackingNumber); + void SendSend(const nsACString& aData, uint32_t aTrackingNumber); + nsresult SendSend(const ArrayBuffer& aData, + uint32_t aByteOffset, + uint32_t aByteLength, + uint32_t aTrackingNumber); + void SendSendArray(nsTArray<uint8_t>* arr, + uint32_t trackingNumber); + void SetSocket(TCPSocket* aSocket); + + void GetHost(nsAString& aHost); + void GetPort(uint16_t* aPort); + + virtual bool RecvCallback(const nsString& aType, + const CallbackData& aData, + const uint32_t& aReadyState) override; + virtual bool RecvRequestDelete() override; + virtual bool RecvUpdateBufferedAmount(const uint32_t& aBufferred, + const uint32_t& aTrackingNumber) override; + nsresult SetFilterName(const nsACString& aFilterName); +private: + nsString mHost; + uint16_t mPort; + nsCString mFilterName; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/network/TCPSocketParent.cpp b/dom/network/TCPSocketParent.cpp new file mode 100644 index 0000000000..54167234e8 --- /dev/null +++ b/dom/network/TCPSocketParent.cpp @@ -0,0 +1,427 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "TCPSocketParent.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "nsJSUtils.h" +#include "mozilla/Unused.h" +#include "mozilla/AppProcessChecker.h" +#include "mozilla/net/NeckoCommon.h" +#include "mozilla/net/PNeckoParent.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/TabParent.h" +#include "mozilla/HoldDropJSObjects.h" +#include "nsISocketTransportService.h" +#include "nsISocketTransport.h" +#include "nsIScriptSecurityManager.h" +#include "nsNetUtil.h" + +namespace IPC { + +//Defined in TCPSocketChild.cpp +extern bool +DeserializeArrayBuffer(JSContext* aCx, + const InfallibleTArray<uint8_t>& aBuffer, + JS::MutableHandle<JS::Value> aVal); + +} // namespace IPC + +namespace mozilla { + +namespace net { +// +// set MOZ_LOG=TCPSocket:5 +// +extern LazyLogModule gTCPSocketLog; +#define TCPSOCKET_LOG(args) MOZ_LOG(gTCPSocketLog, LogLevel::Debug, args) +#define TCPSOCKET_LOG_ENABLED() MOZ_LOG_TEST(gTCPSocketLog, LogLevel::Debug) +} // namespace net + +namespace dom { + +static void +FireInteralError(mozilla::net::PTCPSocketParent* aActor, uint32_t aLineNo) +{ + mozilla::Unused << + aActor->SendCallback(NS_LITERAL_STRING("onerror"), + TCPError(NS_LITERAL_STRING("InvalidStateError"), NS_LITERAL_STRING("Internal error")), + static_cast<uint32_t>(TCPReadyState::Connecting)); +} + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TCPSocketParentBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(TCPSocketParentBase, mSocket) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TCPSocketParentBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TCPSocketParentBase) + +TCPSocketParentBase::TCPSocketParentBase() +: mIPCOpen(false) +{ +} + +TCPSocketParentBase::~TCPSocketParentBase() +{ +} + +uint32_t +TCPSocketParent::GetAppId() +{ + const PContentParent *content = Manager()->Manager(); + if (PBrowserParent* browser = SingleManagedOrNull(content->ManagedPBrowserParent())) { + TabParent *tab = TabParent::GetFrom(browser); + return tab->OwnAppId(); + } else { + return nsIScriptSecurityManager::UNKNOWN_APP_ID; + } +}; + +bool +TCPSocketParent::GetInIsolatedMozBrowser() +{ + const PContentParent *content = Manager()->Manager(); + if (PBrowserParent* browser = SingleManagedOrNull(content->ManagedPBrowserParent())) { + TabParent *tab = TabParent::GetFrom(browser); + return tab->IsIsolatedMozBrowserElement(); + } else { + return false; + } +} + +void +TCPSocketParentBase::ReleaseIPDLReference() +{ + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; + this->Release(); +} + +void +TCPSocketParentBase::AddIPDLReference() +{ + MOZ_ASSERT(!mIPCOpen); + mIPCOpen = true; + this->AddRef(); +} + +NS_IMETHODIMP_(MozExternalRefCountType) TCPSocketParent::Release(void) +{ + nsrefcnt refcnt = TCPSocketParentBase::Release(); + if (refcnt == 1 && mIPCOpen) { + mozilla::Unused << PTCPSocketParent::SendRequestDelete(); + return 1; + } + return refcnt; +} + +bool +TCPSocketParent::RecvOpen(const nsString& aHost, const uint16_t& aPort, const bool& aUseSSL, + const bool& aUseArrayBuffers) +{ + // We don't have browser actors in xpcshell, and hence can't run automated + // tests without this loophole. + if (net::UsingNeckoIPCSecurity() && + !AssertAppProcessPermission(Manager()->Manager(), "tcp-socket")) { + FireInteralError(this, __LINE__); + return true; + } + + // Obtain App ID + uint32_t appId = GetAppId(); + bool inIsolatedMozBrowser = GetInIsolatedMozBrowser(); + + mSocket = new TCPSocket(nullptr, aHost, aPort, aUseSSL, aUseArrayBuffers); + mSocket->SetAppIdAndBrowser(appId, inIsolatedMozBrowser); + mSocket->SetSocketBridgeParent(this); + NS_ENSURE_SUCCESS(mSocket->Init(), true); + return true; +} + +bool +TCPSocketParent::RecvOpenBind(const nsCString& aRemoteHost, + const uint16_t& aRemotePort, + const nsCString& aLocalAddr, + const uint16_t& aLocalPort, + const bool& aUseSSL, + const bool& aUseArrayBuffers, + const nsCString& aFilter) +{ + if (net::UsingNeckoIPCSecurity() && + !AssertAppProcessPermission(Manager()->Manager(), "tcp-socket")) { + FireInteralError(this, __LINE__); + return true; + } + + nsresult rv; + nsCOMPtr<nsISocketTransportService> sts = + do_GetService("@mozilla.org/network/socket-transport-service;1", &rv); + if (NS_FAILED(rv)) { + FireInteralError(this, __LINE__); + return true; + } + + nsCOMPtr<nsISocketTransport> socketTransport; + rv = sts->CreateTransport(nullptr, 0, + aRemoteHost, aRemotePort, + nullptr, getter_AddRefs(socketTransport)); + if (NS_FAILED(rv)) { + FireInteralError(this, __LINE__); + return true; + } + + PRNetAddr prAddr; + if (PR_SUCCESS != PR_InitializeNetAddr(PR_IpAddrAny, aLocalPort, &prAddr)) { + FireInteralError(this, __LINE__); + return true; + } + if (PR_SUCCESS != PR_StringToNetAddr(aLocalAddr.BeginReading(), &prAddr)) { + FireInteralError(this, __LINE__); + return true; + } + + mozilla::net::NetAddr addr; + PRNetAddrToNetAddr(&prAddr, &addr); + rv = socketTransport->Bind(&addr); + if (NS_FAILED(rv)) { + FireInteralError(this, __LINE__); + return true; + } + + if (!aFilter.IsEmpty()) { + nsAutoCString contractId(NS_NETWORK_TCP_SOCKET_FILTER_HANDLER_PREFIX); + contractId.Append(aFilter); + nsCOMPtr<nsISocketFilterHandler> filterHandler = + do_GetService(contractId.get()); + if (!filterHandler) { + NS_ERROR("Content doesn't have a valid filter"); + FireInteralError(this, __LINE__); + return true; + } + rv = filterHandler->NewFilter(getter_AddRefs(mFilter)); + if (NS_FAILED(rv)) { + NS_ERROR("Cannot create filter that content specified"); + FireInteralError(this, __LINE__); + return true; + } + } + + // Obtain App ID + uint32_t appId = nsIScriptSecurityManager::NO_APP_ID; + bool inIsolatedMozBrowser = false; + const PContentParent *content = Manager()->Manager(); + if (PBrowserParent* browser = SingleManagedOrNull(content->ManagedPBrowserParent())) { + // appId's are for B2G only currently, where managees.Count() == 1 + // This is not guaranteed currently in Desktop, so skip this there. + TabParent *tab = TabParent::GetFrom(browser); + appId = tab->OwnAppId(); + inIsolatedMozBrowser = tab->IsIsolatedMozBrowserElement(); + } + + mSocket = new TCPSocket(nullptr, NS_ConvertUTF8toUTF16(aRemoteHost), aRemotePort, aUseSSL, aUseArrayBuffers); + mSocket->SetAppIdAndBrowser(appId, inIsolatedMozBrowser); + mSocket->SetSocketBridgeParent(this); + rv = mSocket->InitWithUnconnectedTransport(socketTransport); + NS_ENSURE_SUCCESS(rv, true); + return true; +} + +bool +TCPSocketParent::RecvStartTLS() +{ + NS_ENSURE_TRUE(mSocket, true); + ErrorResult rv; + mSocket->UpgradeToSecure(rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + + return true; +} + +bool +TCPSocketParent::RecvSuspend() +{ + NS_ENSURE_TRUE(mSocket, true); + mSocket->Suspend(); + return true; +} + +bool +TCPSocketParent::RecvResume() +{ + NS_ENSURE_TRUE(mSocket, true); + ErrorResult rv; + mSocket->Resume(rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + + return true; +} + +bool +TCPSocketParent::RecvData(const SendableData& aData, + const uint32_t& aTrackingNumber) +{ + ErrorResult rv; + + if (mFilter) { + mozilla::net::NetAddr addr; // dummy value + bool allowed; + MOZ_ASSERT(aData.type() == SendableData::TArrayOfuint8_t, + "Unsupported data type for filtering"); + const InfallibleTArray<uint8_t>& data(aData.get_ArrayOfuint8_t()); + nsresult nsrv = mFilter->FilterPacket(&addr, data.Elements(), + data.Length(), + nsISocketFilter::SF_OUTGOING, + &allowed); + + // Reject sending of unallowed data + if (NS_WARN_IF(NS_FAILED(nsrv)) || !allowed) { + TCPSOCKET_LOG(("%s: Dropping outgoing TCP packet", __FUNCTION__)); + return false; + } + } + + switch (aData.type()) { + case SendableData::TArrayOfuint8_t: { + AutoSafeJSContext autoCx; + JS::Rooted<JS::Value> val(autoCx); + const nsTArray<uint8_t>& buffer = aData.get_ArrayOfuint8_t(); + bool ok = IPC::DeserializeArrayBuffer(autoCx, buffer, &val); + NS_ENSURE_TRUE(ok, true); + RootedTypedArray<ArrayBuffer> data(autoCx); + data.Init(&val.toObject()); + Optional<uint32_t> byteLength(buffer.Length()); + mSocket->SendWithTrackingNumber(autoCx, data, 0, byteLength, aTrackingNumber, rv); + break; + } + + case SendableData::TnsCString: { + const nsCString& strData = aData.get_nsCString(); + mSocket->SendWithTrackingNumber(strData, aTrackingNumber, rv); + break; + } + + default: + MOZ_CRASH("unexpected SendableData type"); + } + NS_ENSURE_SUCCESS(rv.StealNSResult(), true); + return true; +} + +bool +TCPSocketParent::RecvClose() +{ + NS_ENSURE_TRUE(mSocket, true); + mSocket->Close(); + return true; +} + +void +TCPSocketParent::FireErrorEvent(const nsAString& aName, const nsAString& aType, TCPReadyState aReadyState) +{ + SendEvent(NS_LITERAL_STRING("error"), TCPError(nsString(aName), nsString(aType)), aReadyState); +} + +void +TCPSocketParent::FireEvent(const nsAString& aType, TCPReadyState aReadyState) +{ + return SendEvent(aType, mozilla::void_t(), aReadyState); +} + +void +TCPSocketParent::FireArrayBufferDataEvent(nsTArray<uint8_t>& aBuffer, TCPReadyState aReadyState) +{ + InfallibleTArray<uint8_t> arr; + arr.SwapElements(aBuffer); + + if (mFilter) { + bool allowed; + mozilla::net::NetAddr addr; + nsresult nsrv = mFilter->FilterPacket(&addr, arr.Elements(), arr.Length(), + nsISocketFilter::SF_INCOMING, + &allowed); + // receiving unallowed data, drop it. + if (NS_WARN_IF(NS_FAILED(nsrv)) || !allowed) { + TCPSOCKET_LOG(("%s: Dropping incoming TCP packet", __FUNCTION__)); + return; + } + } + + SendableData data(arr); + SendEvent(NS_LITERAL_STRING("data"), data, aReadyState); +} + +void +TCPSocketParent::FireStringDataEvent(const nsACString& aData, TCPReadyState aReadyState) +{ + SendableData data((nsCString(aData))); + + MOZ_ASSERT(!mFilter, "Socket filtering doesn't support nsCString"); + + SendEvent(NS_LITERAL_STRING("data"), data, aReadyState); +} + +void +TCPSocketParent::SendEvent(const nsAString& aType, CallbackData aData, TCPReadyState aReadyState) +{ + if (mIPCOpen) { + mozilla::Unused << PTCPSocketParent::SendCallback(nsString(aType), + aData, + static_cast<uint32_t>(aReadyState)); + } +} + +void +TCPSocketParent::SetSocket(TCPSocket *socket) +{ + mSocket = socket; +} + +nsresult +TCPSocketParent::GetHost(nsAString& aHost) +{ + if (!mSocket) { + NS_ERROR("No internal socket instance mSocket!"); + return NS_ERROR_FAILURE; + } + mSocket->GetHost(aHost); + return NS_OK; +} + +nsresult +TCPSocketParent::GetPort(uint16_t* aPort) +{ + if (!mSocket) { + NS_ERROR("No internal socket instance mSocket!"); + return NS_ERROR_FAILURE; + } + *aPort = mSocket->Port(); + return NS_OK; +} + +void +TCPSocketParent::ActorDestroy(ActorDestroyReason why) +{ + if (mSocket) { + mSocket->Close(); + } + mSocket = nullptr; +} + +bool +TCPSocketParent::RecvRequestDelete() +{ + mozilla::Unused << Send__delete__(this); + return true; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/network/TCPSocketParent.h b/dom/network/TCPSocketParent.h new file mode 100644 index 0000000000..e44e340bbf --- /dev/null +++ b/dom/network/TCPSocketParent.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_TCPSocketParent_h +#define mozilla_dom_TCPSocketParent_h + +#include "mozilla/dom/TCPSocketBinding.h" +#include "mozilla/net/PTCPSocketParent.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" +#include "nsISocketFilter.h" +#include "js/TypeDecls.h" + +#define TCPSOCKETPARENT_CID \ + { 0x4e7246c6, 0xa8b3, 0x426d, { 0x9c, 0x17, 0x76, 0xda, 0xb1, 0xe1, 0xe1, 0x4a } } + +namespace mozilla { +namespace dom { + +class TCPSocket; + +class TCPSocketParentBase : public nsISupports +{ +public: + NS_DECL_CYCLE_COLLECTION_CLASS(TCPSocketParentBase) + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + void AddIPDLReference(); + void ReleaseIPDLReference(); + +protected: + TCPSocketParentBase(); + virtual ~TCPSocketParentBase(); + + RefPtr<TCPSocket> mSocket; + bool mIPCOpen; +}; + +class TCPSocketParent : public mozilla::net::PTCPSocketParent + , public TCPSocketParentBase +{ +public: + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + TCPSocketParent() {} + + virtual bool RecvOpen(const nsString& aHost, const uint16_t& aPort, + const bool& useSSL, const bool& aUseArrayBuffers) override; + + virtual bool RecvOpenBind(const nsCString& aRemoteHost, + const uint16_t& aRemotePort, + const nsCString& aLocalAddr, + const uint16_t& aLocalPort, + const bool& aUseSSL, + const bool& aUseArrayBuffers, + const nsCString& aFilter) override; + + virtual bool RecvStartTLS() override; + virtual bool RecvSuspend() override; + virtual bool RecvResume() override; + virtual bool RecvClose() override; + virtual bool RecvData(const SendableData& aData, + const uint32_t& aTrackingNumber) override; + virtual bool RecvRequestDelete() override; + bool GetInIsolatedMozBrowser(); + + void FireErrorEvent(const nsAString& aName, const nsAString& aType, TCPReadyState aReadyState); + void FireEvent(const nsAString& aType, TCPReadyState aReadyState); + void FireArrayBufferDataEvent(nsTArray<uint8_t>& aBuffer, TCPReadyState aReadyState); + void FireStringDataEvent(const nsACString& aData, TCPReadyState aReadyState); + + void SetSocket(TCPSocket *socket); + nsresult GetHost(nsAString& aHost); + nsresult GetPort(uint16_t* aPort); + +private: + virtual uint32_t GetAppId(); + virtual void ActorDestroy(ActorDestroyReason why) override; + void SendEvent(const nsAString& aType, CallbackData aData, TCPReadyState aReadyState); + nsresult SetFilter(const nsCString& aFilter); + + nsCOMPtr<nsISocketFilter> mFilter; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/network/Types.h b/dom/network/Types.h new file mode 100644 index 0000000000..5f81a84402 --- /dev/null +++ b/dom/network/Types.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_network_Types_h +#define mozilla_dom_network_Types_h + +namespace mozilla { +namespace hal { +class NetworkInformation; +} // namespace hal + +template <class T> +class Observer; + +typedef Observer<hal::NetworkInformation> NetworkObserver; + +} // namespace mozilla + +#endif // mozilla_dom_network_Types_h diff --git a/dom/network/UDPSocket.cpp b/dom/network/UDPSocket.cpp new file mode 100644 index 0000000000..e275e39029 --- /dev/null +++ b/dom/network/UDPSocket.cpp @@ -0,0 +1,762 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "UDPSocket.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/UDPMessageEvent.h" +#include "mozilla/dom/UDPSocketBinding.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/net/DNS.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsINetAddr.h" +#include "nsStringStream.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_ISUPPORTS(UDPSocket::ListenerProxy, + nsIUDPSocketListener, + nsIUDPSocketInternal) + +NS_IMPL_CYCLE_COLLECTION_CLASS(UDPSocket) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(UDPSocket, DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOpened) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mClosed) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(UDPSocket, DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOpened) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mClosed) + tmp->CloseWithReason(NS_OK); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ADDREF_INHERITED(UDPSocket, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(UDPSocket, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(UDPSocket) + NS_INTERFACE_MAP_ENTRY(nsIUDPSocketListener) + NS_INTERFACE_MAP_ENTRY(nsIUDPSocketInternal) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +/* static */ already_AddRefed<UDPSocket> +UDPSocket::Constructor(const GlobalObject& aGlobal, + const UDPOptions& aOptions, + ErrorResult& aRv) +{ + nsCOMPtr<nsPIDOMWindowInner> ownerWindow = do_QueryInterface(aGlobal.GetAsSupports()); + if (!ownerWindow) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + bool addressReuse = aOptions.mAddressReuse; + bool loopback = aOptions.mLoopback; + + nsCString remoteAddress; + if (aOptions.mRemoteAddress.WasPassed()) { + remoteAddress = NS_ConvertUTF16toUTF8(aOptions.mRemoteAddress.Value()); + } else { + remoteAddress.SetIsVoid(true); + } + + Nullable<uint16_t> remotePort; + if (aOptions.mRemotePort.WasPassed()) { + remotePort.SetValue(aOptions.mRemotePort.Value()); + + if (remotePort.Value() == 0) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return nullptr; + } + } + + nsString localAddress; + if (aOptions.mLocalAddress.WasPassed()) { + localAddress = aOptions.mLocalAddress.Value(); + + // check if localAddress is a valid IPv4/6 address + NS_ConvertUTF16toUTF8 address(localAddress); + PRNetAddr prAddr; + PRStatus status = PR_StringToNetAddr(address.BeginReading(), &prAddr); + if (status != PR_SUCCESS) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return nullptr; + } + } else { + SetDOMStringToNull(localAddress); + } + + Nullable<uint16_t> localPort; + if (aOptions.mLocalPort.WasPassed()) { + localPort.SetValue(aOptions.mLocalPort.Value()); + + if (localPort.Value() == 0) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return nullptr; + } + } + + RefPtr<UDPSocket> socket = new UDPSocket(ownerWindow, remoteAddress, remotePort); + aRv = socket->Init(localAddress, localPort, addressReuse, loopback); + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return socket.forget(); +} + +UDPSocket::UDPSocket(nsPIDOMWindowInner* aOwner, + const nsCString& aRemoteAddress, + const Nullable<uint16_t>& aRemotePort) + : DOMEventTargetHelper(aOwner) + , mRemoteAddress(aRemoteAddress) + , mRemotePort(aRemotePort) + , mAddressReuse(false) + , mLoopback(false) + , mReadyState(SocketReadyState::Opening) +{ + MOZ_ASSERT(aOwner); + MOZ_ASSERT(aOwner->IsInnerWindow()); + + nsIDocument* aDoc = aOwner->GetExtantDoc(); + if (aDoc) { + aDoc->DisallowBFCaching(); + } +} + +UDPSocket::~UDPSocket() +{ + CloseWithReason(NS_OK); +} + +JSObject* +UDPSocket::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return UDPSocketBinding::Wrap(aCx, this, aGivenProto); +} + +void +UDPSocket::DisconnectFromOwner() +{ + DOMEventTargetHelper::DisconnectFromOwner(); + CloseWithReason(NS_OK); +} + +already_AddRefed<Promise> +UDPSocket::Close() +{ + MOZ_ASSERT(mClosed); + + RefPtr<Promise> promise = mClosed; + + if (mReadyState == SocketReadyState::Closed) { + return promise.forget(); + } + + CloseWithReason(NS_OK); + return promise.forget(); +} + +void +UDPSocket::CloseWithReason(nsresult aReason) +{ + if (mReadyState == SocketReadyState::Closed) { + return; + } + + if (mOpened) { + if (mReadyState == SocketReadyState::Opening) { + // reject openedPromise with AbortError if socket is closed without error + nsresult openFailedReason = NS_FAILED(aReason) ? aReason : NS_ERROR_DOM_ABORT_ERR; + mOpened->MaybeReject(openFailedReason); + } + } + + mReadyState = SocketReadyState::Closed; + + if (mListenerProxy) { + mListenerProxy->Disconnect(); + mListenerProxy = nullptr; + } + + if (mSocket) { + mSocket->Close(); + mSocket = nullptr; + } + + if (mSocketChild) { + mSocketChild->Close(); + mSocketChild = nullptr; + } + + if (mClosed) { + if (NS_SUCCEEDED(aReason)) { + mClosed->MaybeResolveWithUndefined(); + } else { + mClosed->MaybeReject(aReason); + } + } + + mPendingMcastCommands.Clear(); +} + +void +UDPSocket::JoinMulticastGroup(const nsAString& aMulticastGroupAddress, + ErrorResult& aRv) +{ + if (mReadyState == SocketReadyState::Closed) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (mReadyState == SocketReadyState::Opening) { + MulticastCommand joinCommand(MulticastCommand::Join, aMulticastGroupAddress); + mPendingMcastCommands.AppendElement(joinCommand); + return; + } + + MOZ_ASSERT(mSocket || mSocketChild); + + NS_ConvertUTF16toUTF8 address(aMulticastGroupAddress); + + if (mSocket) { + MOZ_ASSERT(!mSocketChild); + + aRv = mSocket->JoinMulticast(address, EmptyCString()); + NS_WARNING_ASSERTION(!aRv.Failed(), "JoinMulticast failed"); + + return; + } + + MOZ_ASSERT(mSocketChild); + + aRv = mSocketChild->JoinMulticast(address, EmptyCString()); + NS_WARNING_ASSERTION(!aRv.Failed(), "JoinMulticast failed"); +} + +void +UDPSocket::LeaveMulticastGroup(const nsAString& aMulticastGroupAddress, + ErrorResult& aRv) +{ + if (mReadyState == SocketReadyState::Closed) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (mReadyState == SocketReadyState::Opening) { + MulticastCommand leaveCommand(MulticastCommand::Leave, aMulticastGroupAddress); + mPendingMcastCommands.AppendElement(leaveCommand); + return; + } + + MOZ_ASSERT(mSocket || mSocketChild); + + nsCString address = NS_ConvertUTF16toUTF8(aMulticastGroupAddress); + if (mSocket) { + MOZ_ASSERT(!mSocketChild); + + aRv = mSocket->LeaveMulticast(address, EmptyCString()); + NS_WARNING_ASSERTION(!aRv.Failed(), "mSocket->LeaveMulticast failed"); + return; + } + + MOZ_ASSERT(mSocketChild); + + aRv = mSocketChild->LeaveMulticast(address, EmptyCString()); + NS_WARNING_ASSERTION(!aRv.Failed(), "mSocketChild->LeaveMulticast failed"); +} + +nsresult +UDPSocket::DoPendingMcastCommand() +{ + MOZ_ASSERT(mReadyState == SocketReadyState::Open, "Multicast command can only be executed after socket opened"); + + for (uint32_t i = 0; i < mPendingMcastCommands.Length(); ++i) { + MulticastCommand& command = mPendingMcastCommands[i]; + ErrorResult rv; + + switch (command.mCommand) { + case MulticastCommand::Join: { + JoinMulticastGroup(command.mAddress, rv); + break; + } + case MulticastCommand::Leave: { + LeaveMulticastGroup(command.mAddress, rv); + break; + } + } + + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + } + + mPendingMcastCommands.Clear(); + return NS_OK; +} + +bool +UDPSocket::Send(const StringOrBlobOrArrayBufferOrArrayBufferView& aData, + const Optional<nsAString>& aRemoteAddress, + const Optional<Nullable<uint16_t>>& aRemotePort, + ErrorResult& aRv) +{ + if (mReadyState != SocketReadyState::Open) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return false; + } + + MOZ_ASSERT(mSocket || mSocketChild); + + // If the remote address and port were not specified in the constructor or as arguments, + // throw InvalidAccessError. + nsCString remoteAddress; + if (aRemoteAddress.WasPassed()) { + remoteAddress = NS_ConvertUTF16toUTF8(aRemoteAddress.Value()); + UDPSOCKET_LOG(("%s: Send to %s", __FUNCTION__, remoteAddress.get())); + } else if (!mRemoteAddress.IsVoid()) { + remoteAddress = mRemoteAddress; + UDPSOCKET_LOG(("%s: Send to %s", __FUNCTION__, remoteAddress.get())); + } else { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return false; + } + + uint16_t remotePort; + if (aRemotePort.WasPassed() && !aRemotePort.Value().IsNull()) { + remotePort = aRemotePort.Value().Value(); + } else if (!mRemotePort.IsNull()) { + remotePort = mRemotePort.Value(); + } else { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return false; + } + + nsCOMPtr<nsIInputStream> stream; + if (aData.IsBlob()) { + Blob& blob = aData.GetAsBlob(); + + blob.GetInternalStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return false; + } + } else { + nsresult rv; + nsCOMPtr<nsIStringInputStream> strStream = do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return false; + } + + if (aData.IsString()) { + NS_ConvertUTF16toUTF8 data(aData.GetAsString()); + aRv = strStream->SetData(data.BeginReading(), data.Length()); + } else if (aData.IsArrayBuffer()) { + const ArrayBuffer& data = aData.GetAsArrayBuffer(); + data.ComputeLengthAndData(); + aRv = strStream->SetData(reinterpret_cast<const char*>(data.Data()), data.Length()); + } else { + const ArrayBufferView& data = aData.GetAsArrayBufferView(); + data.ComputeLengthAndData(); + aRv = strStream->SetData(reinterpret_cast<const char*>(data.Data()), data.Length()); + } + + if (NS_WARN_IF(aRv.Failed())) { + return false; + } + + stream = strStream; + } + + if (mSocket) { + aRv = mSocket->SendBinaryStream(remoteAddress, remotePort, stream); + } else if (mSocketChild) { + aRv = mSocketChild->SendBinaryStream(remoteAddress, remotePort, stream); + } + + if (NS_WARN_IF(aRv.Failed())) { + return false; + } + + return true; +} + +nsresult +UDPSocket::InitLocal(const nsAString& aLocalAddress, + const uint16_t& aLocalPort) +{ + nsresult rv; + + nsCOMPtr<nsIUDPSocket> sock = + do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner(), &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIPrincipal> principal = global->PrincipalOrNull(); + if (!principal) { + return NS_ERROR_FAILURE; + } + + if (aLocalAddress.IsEmpty()) { + rv = sock->Init(aLocalPort, /* loopback = */ false, principal, + mAddressReuse, /* optionalArgc = */ 1); + } else { + PRNetAddr prAddr; + PR_InitializeNetAddr(PR_IpAddrAny, aLocalPort, &prAddr); + PR_StringToNetAddr(NS_ConvertUTF16toUTF8(aLocalAddress).BeginReading(), &prAddr); + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, NS_ConvertUTF16toUTF8(aLocalAddress).get(), aLocalPort)); + + mozilla::net::NetAddr addr; + PRNetAddrToNetAddr(&prAddr, &addr); + rv = sock->InitWithAddress(&addr, principal, mAddressReuse, + /* optionalArgc = */ 1); + } + if (NS_FAILED(rv)) { + return rv; + } + + rv = sock->SetMulticastLoopback(mLoopback); + if (NS_FAILED(rv)) { + return rv; + } + + mSocket = sock; + + // Get real local address and local port + nsCOMPtr<nsINetAddr> localAddr; + rv = mSocket->GetLocalAddr(getter_AddRefs(localAddr)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCString localAddress; + rv = localAddr->GetAddress(localAddress); + if (NS_FAILED(rv)) { + return rv; + } + mLocalAddress = NS_ConvertUTF8toUTF16(localAddress); + + uint16_t localPort; + rv = localAddr->GetPort(&localPort); + if (NS_FAILED(rv)) { + return rv; + } + mLocalPort.SetValue(localPort); + + mListenerProxy = new ListenerProxy(this); + + rv = mSocket->AsyncListen(mListenerProxy); + if (NS_FAILED(rv)) { + return rv; + } + + mReadyState = SocketReadyState::Open; + rv = DoPendingMcastCommand(); + if (NS_FAILED(rv)) { + return rv; + } + + mOpened->MaybeResolveWithUndefined(); + + return NS_OK; +} + +nsresult +UDPSocket::InitRemote(const nsAString& aLocalAddress, + const uint16_t& aLocalPort) +{ + nsresult rv; + + nsCOMPtr<nsIUDPSocketChild> sock = + do_CreateInstance("@mozilla.org/udp-socket-child;1", &rv); + if (NS_FAILED(rv)) { + return rv; + } + + mListenerProxy = new ListenerProxy(this); + + nsCOMPtr<nsIGlobalObject> obj = do_QueryInterface(GetOwner(), &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIPrincipal> principal = obj->PrincipalOrNull(); + if (!principal) { + return NS_ERROR_FAILURE; + } + + rv = sock->Bind(mListenerProxy, + principal, + NS_ConvertUTF16toUTF8(aLocalAddress), + aLocalPort, + mAddressReuse, + mLoopback, + 0, + 0); + + if (NS_FAILED(rv)) { + return rv; + } + + mSocketChild = sock; + + return NS_OK; +} + +nsresult +UDPSocket::Init(const nsString& aLocalAddress, + const Nullable<uint16_t>& aLocalPort, + const bool& aAddressReuse, + const bool& aLoopback) +{ + MOZ_ASSERT(!mSocket && !mSocketChild); + + mLocalAddress = aLocalAddress; + mLocalPort = aLocalPort; + mAddressReuse = aAddressReuse; + mLoopback = aLoopback; + + ErrorResult rv; + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner()); + + mOpened = Promise::Create(global, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + mClosed = Promise::Create(global, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + class OpenSocketRunnable final : public Runnable + { + public: + explicit OpenSocketRunnable(UDPSocket* aSocket) : mSocket(aSocket) + { } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(mSocket); + + if (mSocket->mReadyState != SocketReadyState::Opening) { + return NS_OK; + } + + uint16_t localPort = 0; + if (!mSocket->mLocalPort.IsNull()) { + localPort = mSocket->mLocalPort.Value(); + } + + nsresult rv; + if (!XRE_IsParentProcess()) { + rv = mSocket->InitRemote(mSocket->mLocalAddress, localPort); + } else { + rv = mSocket->InitLocal(mSocket->mLocalAddress, localPort); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + mSocket->CloseWithReason(NS_ERROR_DOM_NETWORK_ERR); + } + + return NS_OK; + } + + private: + RefPtr<UDPSocket> mSocket; + }; + + nsCOMPtr<nsIRunnable> runnable = new OpenSocketRunnable(this); + + return NS_DispatchToMainThread(runnable); +} + +void +UDPSocket::HandleReceivedData(const nsACString& aRemoteAddress, + const uint16_t& aRemotePort, + const uint8_t* aData, + const uint32_t& aDataLength) +{ + if (mReadyState != SocketReadyState::Open) { + return; + } + + if (NS_FAILED(CheckInnerWindowCorrectness())) { + return; + } + + if (NS_FAILED(DispatchReceivedData(aRemoteAddress, aRemotePort, aData, aDataLength))) { + CloseWithReason(NS_ERROR_TYPE_ERR); + } +} + +nsresult +UDPSocket::DispatchReceivedData(const nsACString& aRemoteAddress, + const uint16_t& aRemotePort, + const uint8_t* aData, + const uint32_t& aDataLength) +{ + AutoJSAPI jsapi; + + if (NS_WARN_IF(!jsapi.Init(GetOwner()))) { + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + + // Copy packet data to ArrayBuffer + JS::Rooted<JSObject*> arrayBuf(cx, ArrayBuffer::Create(cx, aDataLength, aData)); + + if (NS_WARN_IF(!arrayBuf)) { + return NS_ERROR_FAILURE; + } + + JS::Rooted<JS::Value> jsData(cx, JS::ObjectValue(*arrayBuf)); + + // Create DOM event + RootedDictionary<UDPMessageEventInit> init(cx); + init.mRemoteAddress = NS_ConvertUTF8toUTF16(aRemoteAddress); + init.mRemotePort = aRemotePort; + init.mData = jsData; + + RefPtr<UDPMessageEvent> udpEvent = + UDPMessageEvent::Constructor(this, NS_LITERAL_STRING("message"), init); + + if (NS_WARN_IF(!udpEvent)) { + return NS_ERROR_FAILURE; + } + + udpEvent->SetTrusted(true); + + RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher(this, udpEvent); + + return asyncDispatcher->PostDOMEvent(); +} + +// nsIUDPSocketListener + +NS_IMETHODIMP +UDPSocket::OnPacketReceived(nsIUDPSocket* aSocket, nsIUDPMessage* aMessage) +{ + // nsIUDPSocketListener callbacks should be invoked on main thread. + MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread"); + + // Create appropriate JS object for message + FallibleTArray<uint8_t>& buffer = aMessage->GetDataAsTArray(); + + nsCOMPtr<nsINetAddr> addr; + if (NS_WARN_IF(NS_FAILED(aMessage->GetFromAddr(getter_AddRefs(addr))))) { + return NS_OK; + } + + nsCString remoteAddress; + if (NS_WARN_IF(NS_FAILED(addr->GetAddress(remoteAddress)))) { + return NS_OK; + } + + uint16_t remotePort; + if (NS_WARN_IF(NS_FAILED(addr->GetPort(&remotePort)))) { + return NS_OK; + } + + HandleReceivedData(remoteAddress, remotePort, buffer.Elements(), buffer.Length()); + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::OnStopListening(nsIUDPSocket* aSocket, nsresult aStatus) +{ + // nsIUDPSocketListener callbacks should be invoked on main thread. + MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread"); + + CloseWithReason(aStatus); + + return NS_OK; +} + +// nsIUDPSocketInternal + +NS_IMETHODIMP +UDPSocket::CallListenerError(const nsACString& aMessage, + const nsACString& aFilename, + uint32_t aLineNumber) +{ + CloseWithReason(NS_ERROR_DOM_NETWORK_ERR); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::CallListenerReceivedData(const nsACString& aRemoteAddress, + uint16_t aRemotePort, + const uint8_t* aData, + uint32_t aDataLength) +{ + HandleReceivedData(aRemoteAddress, aRemotePort, aData, aDataLength); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::CallListenerOpened() +{ + if (mReadyState != SocketReadyState::Opening) { + return NS_OK; + } + + MOZ_ASSERT(mSocketChild); + + // Get real local address and local port + nsCString localAddress; + mSocketChild->GetLocalAddress(localAddress); + mLocalAddress = NS_ConvertUTF8toUTF16(localAddress); + + uint16_t localPort; + mSocketChild->GetLocalPort(&localPort); + mLocalPort.SetValue(localPort); + + mReadyState = SocketReadyState::Open; + nsresult rv = DoPendingMcastCommand(); + + if (NS_WARN_IF(NS_FAILED(rv))) { + CloseWithReason(rv); + return NS_OK; + } + + mOpened->MaybeResolveWithUndefined(); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::CallListenerConnected() +{ + // This shouldn't be called here. + MOZ_CRASH(); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::CallListenerClosed() +{ + CloseWithReason(NS_OK); + + return NS_OK; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/network/UDPSocket.h b/dom/network/UDPSocket.h new file mode 100644 index 0000000000..12427d1dd0 --- /dev/null +++ b/dom/network/UDPSocket.h @@ -0,0 +1,232 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_UDPSocket_h__ +#define mozilla_dom_UDPSocket_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/SocketCommonBinding.h" +#include "nsIUDPSocket.h" +#include "nsIUDPSocketChild.h" +#include "nsTArray.h" + +struct JSContext; + +// +// set MOZ_LOG=UDPSocket:5 +// + +namespace mozilla { +namespace net { +extern LazyLogModule gUDPSocketLog; +#define UDPSOCKET_LOG(args) MOZ_LOG(gUDPSocketLog, LogLevel::Debug, args) +#define UDPSOCKET_LOG_ENABLED() MOZ_LOG_TEST(gUDPSocketLog, LogLevel::Debug) +} // namespace net + +namespace dom { + +struct UDPOptions; +class StringOrBlobOrArrayBufferOrArrayBufferView; + +class UDPSocket final : public DOMEventTargetHelper + , public nsIUDPSocketListener + , public nsIUDPSocketInternal +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(UDPSocket, DOMEventTargetHelper) + NS_DECL_NSIUDPSOCKETLISTENER + NS_DECL_NSIUDPSOCKETINTERNAL + NS_REALLY_FORWARD_NSIDOMEVENTTARGET(DOMEventTargetHelper) + +public: + nsPIDOMWindowInner* + GetParentObject() const + { + return GetOwner(); + } + + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + virtual void + DisconnectFromOwner() override; + + static already_AddRefed<UDPSocket> + Constructor(const GlobalObject& aGlobal, const UDPOptions& aOptions, ErrorResult& aRv); + + void + GetLocalAddress(nsString& aRetVal) const + { + aRetVal = mLocalAddress; + } + + Nullable<uint16_t> + GetLocalPort() const + { + return mLocalPort; + } + + void + GetRemoteAddress(nsString& aRetVal) const + { + if (mRemoteAddress.IsVoid()) { + SetDOMStringToNull(aRetVal); + return; + } + + aRetVal = NS_ConvertUTF8toUTF16(mRemoteAddress); + } + + Nullable<uint16_t> + GetRemotePort() const + { + return mRemotePort; + } + + bool + AddressReuse() const + { + return mAddressReuse; + } + + bool + Loopback() const + { + return mLoopback; + } + + SocketReadyState + ReadyState() const + { + return mReadyState; + } + + Promise* + Opened() const + { + return mOpened; + } + + Promise* + Closed() const + { + return mClosed; + } + + IMPL_EVENT_HANDLER(message) + + already_AddRefed<Promise> + Close(); + + void + JoinMulticastGroup(const nsAString& aMulticastGroupAddress, ErrorResult& aRv); + + void + LeaveMulticastGroup(const nsAString& aMulticastGroupAddress, ErrorResult& aRv); + + bool + Send(const StringOrBlobOrArrayBufferOrArrayBufferView& aData, + const Optional<nsAString>& aRemoteAddress, + const Optional<Nullable<uint16_t>>& aRemotePort, + ErrorResult& aRv); + +private: + class ListenerProxy : public nsIUDPSocketListener + , public nsIUDPSocketInternal + { + public: + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIUDPSOCKETLISTENER(mSocket) + NS_FORWARD_SAFE_NSIUDPSOCKETINTERNAL(mSocket) + + explicit ListenerProxy(UDPSocket* aSocket) + : mSocket(aSocket) + { + } + + void Disconnect() + { + mSocket = nullptr; + } + + private: + virtual ~ListenerProxy() {} + + UDPSocket* mSocket; + }; + + UDPSocket(nsPIDOMWindowInner* aOwner, + const nsCString& aRemoteAddress, + const Nullable<uint16_t>& aRemotePort); + + virtual ~UDPSocket(); + + nsresult + Init(const nsString& aLocalAddress, + const Nullable<uint16_t>& aLocalPort, + const bool& aAddressReuse, + const bool& aLoopback); + + nsresult + InitLocal(const nsAString& aLocalAddress, const uint16_t& aLocalPort); + + nsresult + InitRemote(const nsAString& aLocalAddress, const uint16_t& aLocalPort); + + void + HandleReceivedData(const nsACString& aRemoteAddress, + const uint16_t& aRemotePort, + const uint8_t* aData, + const uint32_t& aDataLength); + + nsresult + DispatchReceivedData(const nsACString& aRemoteAddress, + const uint16_t& aRemotePort, + const uint8_t* aData, + const uint32_t& aDataLength); + + void + CloseWithReason(nsresult aReason); + + nsresult + DoPendingMcastCommand(); + + nsString mLocalAddress; + Nullable<uint16_t> mLocalPort; + nsCString mRemoteAddress; + Nullable<uint16_t> mRemotePort; + bool mAddressReuse; + bool mLoopback; + SocketReadyState mReadyState; + RefPtr<Promise> mOpened; + RefPtr<Promise> mClosed; + + nsCOMPtr<nsIUDPSocket> mSocket; + nsCOMPtr<nsIUDPSocketChild> mSocketChild; + RefPtr<ListenerProxy> mListenerProxy; + + struct MulticastCommand { + enum CommandType { Join, Leave }; + + MulticastCommand(CommandType aCommand, const nsAString& aAddress) + : mCommand(aCommand), mAddress(aAddress) + { } + + CommandType mCommand; + nsString mAddress; + }; + + nsTArray<MulticastCommand> mPendingMcastCommands; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_UDPSocket_h__ diff --git a/dom/network/UDPSocketChild.cpp b/dom/network/UDPSocketChild.cpp new file mode 100644 index 0000000000..6e374ce313 --- /dev/null +++ b/dom/network/UDPSocketChild.cpp @@ -0,0 +1,404 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "UDPSocketChild.h" +#include "mozilla/Unused.h" +#include "mozilla/ipc/InputStreamUtils.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsIIPCBackgroundChildCreateCallback.h" + +using mozilla::net::gNeckoChild; + +namespace mozilla { +namespace dom { + +NS_IMPL_ISUPPORTS(UDPSocketChildBase, nsIUDPSocketChild) + +UDPSocketChildBase::UDPSocketChildBase() +: mIPCOpen(false) +{ +} + +UDPSocketChildBase::~UDPSocketChildBase() +{ +} + +void +UDPSocketChildBase::ReleaseIPDLReference() +{ + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; + mSocket = nullptr; + this->Release(); +} + +void +UDPSocketChildBase::AddIPDLReference() +{ + MOZ_ASSERT(!mIPCOpen); + mIPCOpen = true; + this->AddRef(); +} + +NS_IMETHODIMP_(MozExternalRefCountType) UDPSocketChild::Release(void) +{ + nsrefcnt refcnt = UDPSocketChildBase::Release(); + if (refcnt == 1 && mIPCOpen) { + PUDPSocketChild::SendRequestDelete(); + return 1; + } + return refcnt; +} + +UDPSocketChild::UDPSocketChild() +:mBackgroundManager(nullptr) +,mLocalPort(0) +{ +} + +UDPSocketChild::~UDPSocketChild() +{ +} + +class UDPSocketBackgroundChildCallback final : + public nsIIPCBackgroundChildCreateCallback +{ + bool* mDone; + +public: + explicit UDPSocketBackgroundChildCallback(bool* aDone) + : mDone(aDone) + { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mDone); + MOZ_ASSERT(!*mDone); + } + + NS_DECL_ISUPPORTS + +private: + ~UDPSocketBackgroundChildCallback() + { } + + virtual void + ActorCreated(PBackgroundChild* aActor) override + { + *mDone = true; + } + + virtual void + ActorFailed() override + { + *mDone = true; + } +}; + +NS_IMPL_ISUPPORTS(UDPSocketBackgroundChildCallback, nsIIPCBackgroundChildCreateCallback) + +nsresult +UDPSocketChild::CreatePBackgroundSpinUntilDone() +{ + using mozilla::ipc::BackgroundChild; + + // Spinning the event loop in MainThread would be dangerous + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!BackgroundChild::GetForCurrentThread()); + + bool done = false; + nsCOMPtr<nsIIPCBackgroundChildCreateCallback> callback = + new UDPSocketBackgroundChildCallback(&done); + + if (NS_WARN_IF(!BackgroundChild::GetOrCreateForCurrentThread(callback))) { + return NS_ERROR_FAILURE; + } + + nsIThread* thread = NS_GetCurrentThread(); + while (!done) { + if (NS_WARN_IF(!NS_ProcessNextEvent(thread, true /* aMayWait */))) { + return NS_ERROR_FAILURE; + } + } + + if (NS_WARN_IF(!BackgroundChild::GetForCurrentThread())) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +// nsIUDPSocketChild Methods + +NS_IMETHODIMP +UDPSocketChild::SetBackgroundSpinsEvents() +{ + using mozilla::ipc::BackgroundChild; + + PBackgroundChild* existingBackgroundChild = + BackgroundChild::GetForCurrentThread(); + // If it's not spun up yet, block until it is, and retry + if (!existingBackgroundChild) { + nsresult rv = CreatePBackgroundSpinUntilDone(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + existingBackgroundChild = + BackgroundChild::GetForCurrentThread(); + MOZ_ASSERT(existingBackgroundChild); + } + // By now PBackground is guaranteed to be/have-been up + mBackgroundManager = existingBackgroundChild; + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketChild::Bind(nsIUDPSocketInternal* aSocket, + nsIPrincipal* aPrincipal, + const nsACString& aHost, + uint16_t aPort, + bool aAddressReuse, + bool aLoopback, + uint32_t recvBufferSize, + uint32_t sendBufferSize) +{ + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, PromiseFlatCString(aHost).get(), aPort)); + + NS_ENSURE_ARG(aSocket); + + mSocket = aSocket; + AddIPDLReference(); + + if (mBackgroundManager) { + // If we want to support a passed-in principal here we'd need to + // convert it to a PrincipalInfo + MOZ_ASSERT(!aPrincipal); + mBackgroundManager->SendPUDPSocketConstructor(this, void_t(), mFilterName); + } else { + gNeckoChild->SendPUDPSocketConstructor(this, IPC::Principal(aPrincipal), + mFilterName); + } + + SendBind(UDPAddressInfo(nsCString(aHost), aPort), aAddressReuse, aLoopback, + recvBufferSize, sendBufferSize); + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketChild::Connect(nsIUDPSocketInternal* aSocket, const nsACString & aHost, uint16_t aPort) +{ + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, PromiseFlatCString(aHost).get(), aPort)); + + mSocket = aSocket; + + SendConnect(UDPAddressInfo(nsCString(aHost), aPort)); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketChild::Close() +{ + SendClose(); + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketChild::Send(const nsACString& aHost, + uint16_t aPort, + const uint8_t* aData, + uint32_t aByteLength) +{ + NS_ENSURE_ARG(aData); + + UDPSOCKET_LOG(("%s: %s:%u - %u bytes", __FUNCTION__, PromiseFlatCString(aHost).get(), aPort, aByteLength)); + return SendDataInternal(UDPSocketAddr(UDPAddressInfo(nsCString(aHost), aPort)), + aData, aByteLength); +} + +NS_IMETHODIMP +UDPSocketChild::SendWithAddr(nsINetAddr* aAddr, + const uint8_t* aData, + uint32_t aByteLength) +{ + NS_ENSURE_ARG(aAddr); + NS_ENSURE_ARG(aData); + + NetAddr addr; + aAddr->GetNetAddr(&addr); + + UDPSOCKET_LOG(("%s: %u bytes", __FUNCTION__, aByteLength)); + return SendDataInternal(UDPSocketAddr(addr), aData, aByteLength); +} + +NS_IMETHODIMP +UDPSocketChild::SendWithAddress(const NetAddr* aAddr, + const uint8_t* aData, + uint32_t aByteLength) +{ + NS_ENSURE_ARG(aAddr); + NS_ENSURE_ARG(aData); + + UDPSOCKET_LOG(("%s: %u bytes", __FUNCTION__, aByteLength)); + return SendDataInternal(UDPSocketAddr(*aAddr), aData, aByteLength); +} + +nsresult +UDPSocketChild::SendDataInternal(const UDPSocketAddr& aAddr, + const uint8_t* aData, + const uint32_t aByteLength) +{ + NS_ENSURE_ARG(aData); + + FallibleTArray<uint8_t> fallibleArray; + if (!fallibleArray.InsertElementsAt(0, aData, aByteLength, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + InfallibleTArray<uint8_t> array; + array.SwapElements(fallibleArray); + + SendOutgoingData(array, aAddr); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketChild::SendBinaryStream(const nsACString& aHost, + uint16_t aPort, + nsIInputStream* aStream) +{ + NS_ENSURE_ARG(aStream); + + OptionalInputStreamParams stream; + nsTArray<mozilla::ipc::FileDescriptor> fds; + SerializeInputStream(aStream, stream, fds); + + MOZ_ASSERT(fds.IsEmpty()); + + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, PromiseFlatCString(aHost).get(), aPort)); + SendOutgoingData(UDPData(stream), UDPSocketAddr(UDPAddressInfo(nsCString(aHost), aPort))); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketChild::JoinMulticast(const nsACString& aMulticastAddress, + const nsACString& aInterface) +{ + SendJoinMulticast(nsCString(aMulticastAddress), nsCString(aInterface)); + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketChild::LeaveMulticast(const nsACString& aMulticastAddress, + const nsACString& aInterface) +{ + SendLeaveMulticast(nsCString(aMulticastAddress), nsCString(aInterface)); + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketChild::GetLocalPort(uint16_t* aLocalPort) +{ + NS_ENSURE_ARG_POINTER(aLocalPort); + + *aLocalPort = mLocalPort; + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketChild::GetLocalAddress(nsACString& aLocalAddress) +{ + aLocalAddress = mLocalAddress; + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketChild::SetFilterName(const nsACString& aFilterName) +{ + if (!mFilterName.IsEmpty()) { + // filter name can only be set once. + return NS_ERROR_FAILURE; + } + mFilterName = aFilterName; + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketChild::GetFilterName(nsACString& aFilterName) +{ + aFilterName = mFilterName; + return NS_OK; +} + +// PUDPSocketChild Methods +bool +UDPSocketChild::RecvCallbackOpened(const UDPAddressInfo& aAddressInfo) +{ + mLocalAddress = aAddressInfo.addr(); + mLocalPort = aAddressInfo.port(); + + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, mLocalAddress.get(), mLocalPort)); + nsresult rv = mSocket->CallListenerOpened(); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return true; +} + +// PUDPSocketChild Methods +bool +UDPSocketChild::RecvCallbackConnected(const UDPAddressInfo& aAddressInfo) +{ + mLocalAddress = aAddressInfo.addr(); + mLocalPort = aAddressInfo.port(); + + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, mLocalAddress.get(), mLocalPort)); + nsresult rv = mSocket->CallListenerConnected(); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return true; +} + +bool +UDPSocketChild::RecvCallbackClosed() +{ + nsresult rv = mSocket->CallListenerClosed(); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return true; +} + +bool +UDPSocketChild::RecvCallbackReceivedData(const UDPAddressInfo& aAddressInfo, + InfallibleTArray<uint8_t>&& aData) +{ + UDPSOCKET_LOG(("%s: %s:%u length %u", __FUNCTION__, + aAddressInfo.addr().get(), aAddressInfo.port(), aData.Length())); + nsresult rv = mSocket->CallListenerReceivedData(aAddressInfo.addr(), aAddressInfo.port(), + aData.Elements(), aData.Length()); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return true; +} + +bool +UDPSocketChild::RecvCallbackError(const nsCString& aMessage, + const nsCString& aFilename, + const uint32_t& aLineNumber) +{ + UDPSOCKET_LOG(("%s: %s:%s:%u", __FUNCTION__, aMessage.get(), aFilename.get(), aLineNumber)); + nsresult rv = mSocket->CallListenerError(aMessage, aFilename, aLineNumber); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return true; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/network/UDPSocketChild.h b/dom/network/UDPSocketChild.h new file mode 100644 index 0000000000..5794b7d46b --- /dev/null +++ b/dom/network/UDPSocketChild.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_UDPSocketChild_h__ +#define mozilla_dom_UDPSocketChild_h__ + +#include "mozilla/net/PUDPSocketChild.h" +#include "nsIUDPSocketChild.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" + +#define UDPSOCKETCHILD_CID \ + {0xb47e5a0f, 0xd384, 0x48ef, { 0x88, 0x85, 0x42, 0x59, 0x79, 0x3d, 0x9c, 0xf0 }} + +namespace mozilla { +namespace dom { + +class UDPSocketChildBase : public nsIUDPSocketChild { +public: + NS_DECL_ISUPPORTS + + void AddIPDLReference(); + void ReleaseIPDLReference(); + +protected: + UDPSocketChildBase(); + virtual ~UDPSocketChildBase(); + nsCOMPtr<nsIUDPSocketInternal> mSocket; + bool mIPCOpen; +}; + +class UDPSocketChild : public mozilla::net::PUDPSocketChild + , public UDPSocketChildBase +{ +public: + NS_DECL_NSIUDPSOCKETCHILD + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + UDPSocketChild(); + virtual ~UDPSocketChild(); + + nsresult CreatePBackgroundSpinUntilDone(); + + virtual bool RecvCallbackOpened(const UDPAddressInfo& aAddressInfo) override; + virtual bool RecvCallbackConnected(const UDPAddressInfo& aAddressInfo) override; + virtual bool RecvCallbackClosed() override; + virtual bool RecvCallbackReceivedData(const UDPAddressInfo& aAddressInfo, + InfallibleTArray<uint8_t>&& aData) override; + virtual bool RecvCallbackError(const nsCString& aMessage, + const nsCString& aFilename, + const uint32_t& aLineNumber) override; + +private: + nsresult SendDataInternal(const UDPSocketAddr& aAddr, + const uint8_t* aData, + const uint32_t aByteLength); + + mozilla::ipc::PBackgroundChild* mBackgroundManager; + uint16_t mLocalPort; + nsCString mLocalAddress; + nsCString mFilterName; +}; + +} // namespace dom +} // namespace mozilla + +#endif // !defined(mozilla_dom_UDPSocketChild_h__) diff --git a/dom/network/UDPSocketParent.cpp b/dom/network/UDPSocketParent.cpp new file mode 100644 index 0000000000..904a995ed4 --- /dev/null +++ b/dom/network/UDPSocketParent.cpp @@ -0,0 +1,606 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "nsIServiceManager.h" +#include "UDPSocketParent.h" +#include "nsComponentManagerUtils.h" +#include "nsIUDPSocket.h" +#include "nsINetAddr.h" +#include "mozilla/AppProcessChecker.h" +#include "mozilla/Unused.h" +#include "mozilla/ipc/InputStreamUtils.h" +#include "mozilla/net/DNS.h" +#include "mozilla/net/NeckoCommon.h" +#include "mozilla/net/PNeckoParent.h" +#include "nsIPermissionManager.h" +#include "nsIScriptSecurityManager.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mtransport/runnable_utils.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_ISUPPORTS(UDPSocketParent, nsIUDPSocketListener) + +UDPSocketParent::UDPSocketParent(PBackgroundParent* aManager) + : mBackgroundManager(aManager) + , mNeckoManager(nullptr) + , mIPCOpen(true) +{ +} + +UDPSocketParent::UDPSocketParent(PNeckoParent* aManager) + : mBackgroundManager(nullptr) + , mNeckoManager(aManager) + , mIPCOpen(true) +{ +} + +UDPSocketParent::~UDPSocketParent() +{ +} + +bool +UDPSocketParent::Init(const IPC::Principal& aPrincipal, + const nsACString& aFilter) +{ + MOZ_ASSERT_IF(mBackgroundManager, !aPrincipal); + // will be used once we move all UDPSocket to PBackground, or + // if we add in Principal checking for mtransport + Unused << mBackgroundManager; + + mPrincipal = aPrincipal; + if (net::UsingNeckoIPCSecurity() && + mPrincipal && + !ContentParent::IgnoreIPCPrincipal()) { + if (mNeckoManager) { + if (!AssertAppPrincipal(mNeckoManager->Manager(), mPrincipal)) { + return false; + } + } else { + // PBackground is (for now) using a STUN filter for verification + // it's not being used for DoS + } + + nsCOMPtr<nsIPermissionManager> permMgr = + services::GetPermissionManager(); + if (!permMgr) { + NS_WARNING("No PermissionManager available!"); + return false; + } + + uint32_t permission = nsIPermissionManager::DENY_ACTION; + permMgr->TestExactPermissionFromPrincipal(mPrincipal, "udp-socket", + &permission); + if (permission != nsIPermissionManager::ALLOW_ACTION) { + return false; + } + } + + if (!aFilter.IsEmpty()) { + nsAutoCString contractId(NS_NETWORK_UDP_SOCKET_FILTER_HANDLER_PREFIX); + contractId.Append(aFilter); + nsCOMPtr<nsISocketFilterHandler> filterHandler = + do_GetService(contractId.get()); + if (filterHandler) { + nsresult rv = filterHandler->NewFilter(getter_AddRefs(mFilter)); + if (NS_FAILED(rv)) { + printf_stderr("Cannot create filter that content specified. " + "filter name: %s, error code: %u.", aFilter.BeginReading(), static_cast<uint32_t>(rv)); + return false; + } + } else { + printf_stderr("Content doesn't have a valid filter. " + "filter name: %s.", aFilter.BeginReading()); + return false; + } + } + // We don't have browser actors in xpcshell, and hence can't run automated + // tests without this loophole. + if (net::UsingNeckoIPCSecurity() && !mFilter && + (!mPrincipal || ContentParent::IgnoreIPCPrincipal())) { + return false; + } + return true; +} + +// PUDPSocketParent methods + +bool +UDPSocketParent::RecvBind(const UDPAddressInfo& aAddressInfo, + const bool& aAddressReuse, const bool& aLoopback, + const uint32_t& recvBufferSize, + const uint32_t& sendBufferSize) +{ + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, aAddressInfo.addr().get(), aAddressInfo.port())); + + if (NS_FAILED(BindInternal(aAddressInfo.addr(), aAddressInfo.port(), + aAddressReuse, aLoopback, recvBufferSize, + sendBufferSize))) { + FireInternalError(__LINE__); + return true; + } + + nsCOMPtr<nsINetAddr> localAddr; + mSocket->GetLocalAddr(getter_AddRefs(localAddr)); + + nsCString addr; + if (NS_FAILED(localAddr->GetAddress(addr))) { + FireInternalError(__LINE__); + return true; + } + + uint16_t port; + if (NS_FAILED(localAddr->GetPort(&port))) { + FireInternalError(__LINE__); + return true; + } + + UDPSOCKET_LOG(("%s: SendCallbackOpened: %s:%u", __FUNCTION__, addr.get(), port)); + mozilla::Unused << SendCallbackOpened(UDPAddressInfo(addr, port)); + + return true; +} + +nsresult +UDPSocketParent::BindInternal(const nsCString& aHost, const uint16_t& aPort, + const bool& aAddressReuse, const bool& aLoopback, + const uint32_t& recvBufferSize, + const uint32_t& sendBufferSize) +{ + nsresult rv; + + UDPSOCKET_LOG(("%s: [this=%p] %s:%u addressReuse: %d loopback: %d recvBufferSize: %lu, sendBufferSize: %lu", + __FUNCTION__, this, nsCString(aHost).get(), aPort, + aAddressReuse, aLoopback, recvBufferSize, sendBufferSize)); + + nsCOMPtr<nsIUDPSocket> sock = + do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aHost.IsEmpty()) { + rv = sock->Init(aPort, false, mPrincipal, aAddressReuse, + /* optional_argc = */ 1); + } else { + PRNetAddr prAddr; + PR_InitializeNetAddr(PR_IpAddrAny, aPort, &prAddr); + PRStatus status = PR_StringToNetAddr(aHost.BeginReading(), &prAddr); + if (status != PR_SUCCESS) { + return NS_ERROR_FAILURE; + } + + mozilla::net::NetAddr addr; + PRNetAddrToNetAddr(&prAddr, &addr); + rv = sock->InitWithAddress(&addr, mPrincipal, aAddressReuse, + /* optional_argc = */ 1); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsINetAddr> laddr; + rv = sock->GetLocalAddr(getter_AddRefs(laddr)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + uint16_t family; + rv = laddr->GetFamily(&family); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (family == nsINetAddr::FAMILY_INET) { + rv = sock->SetMulticastLoopback(aLoopback); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + // TODO: once bug 1252759 is fixed query buffer first and only increase + if (recvBufferSize != 0) { + rv = sock->SetRecvBufferSize(recvBufferSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + UDPSOCKET_LOG(("%s: [this=%p] %s:%u failed to set recv buffer size to: %lu", __FUNCTION__, this, nsCString(aHost).get(), aPort, recvBufferSize)); + } + } + if (sendBufferSize != 0) { + rv = sock->SetSendBufferSize(sendBufferSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + UDPSOCKET_LOG(("%s: [this=%p] %s:%u failed to set send buffer size to: %lu", __FUNCTION__, this, nsCString(aHost).get(), aPort, sendBufferSize)); + } + } + + // register listener + rv = sock->AsyncListen(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mSocket = sock; + + return NS_OK; +} + + +static nsCOMPtr<nsIEventTarget> GetSTSThread() +{ + nsresult rv; + + nsCOMPtr<nsIEventTarget> sts_thread; + + sts_thread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + return sts_thread; +} + +static void CheckSTSThread() +{ + DebugOnly<nsCOMPtr<nsIEventTarget>> sts_thread = GetSTSThread(); + + ASSERT_ON_THREAD(sts_thread.value); +} + + +// Proxy the Connect() request to the STS thread, since it may block and +// should be done there. +bool +UDPSocketParent::RecvConnect(const UDPAddressInfo& aAddressInfo) +{ + nsCOMPtr<nsIEventTarget> thread(NS_GetCurrentThread()); + Unused << + NS_WARN_IF(NS_FAILED(GetSTSThread()->Dispatch(WrapRunnable( + RefPtr<UDPSocketParent>(this), + &UDPSocketParent::DoConnect, + mSocket, + thread, + aAddressInfo), + NS_DISPATCH_NORMAL))); + return true; +} + +void +UDPSocketParent::DoSendConnectResponse(const UDPAddressInfo& aAddressInfo) +{ + // can't use directly with WrapRunnable due to warnings + mozilla::Unused << SendCallbackConnected(aAddressInfo); +} + +void +UDPSocketParent::SendConnectResponse(nsIEventTarget *aThread, + const UDPAddressInfo& aAddressInfo) +{ + Unused << + NS_WARN_IF(NS_FAILED(aThread->Dispatch(WrapRunnable( + RefPtr<UDPSocketParent>(this), + &UDPSocketParent::DoSendConnectResponse, + aAddressInfo), + NS_DISPATCH_NORMAL))); +} + +// Runs on STS thread +void +UDPSocketParent::DoConnect(nsCOMPtr<nsIUDPSocket>& aSocket, + nsCOMPtr<nsIEventTarget>& aReturnThread, + const UDPAddressInfo& aAddressInfo) +{ + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, aAddressInfo.addr().get(), aAddressInfo.port())); + if (NS_FAILED(ConnectInternal(aAddressInfo.addr(), aAddressInfo.port()))) { + SendInternalError(aReturnThread, __LINE__); + return; + } + CheckSTSThread(); + + nsCOMPtr<nsINetAddr> localAddr; + aSocket->GetLocalAddr(getter_AddRefs(localAddr)); + + nsCString addr; + if (NS_FAILED(localAddr->GetAddress(addr))) { + SendInternalError(aReturnThread, __LINE__); + return; + } + + uint16_t port; + if (NS_FAILED(localAddr->GetPort(&port))) { + SendInternalError(aReturnThread, __LINE__); + return; + } + + UDPSOCKET_LOG(("%s: SendConnectResponse: %s:%u", __FUNCTION__, addr.get(), port)); + SendConnectResponse(aReturnThread, UDPAddressInfo(addr, port)); +} + +nsresult +UDPSocketParent::ConnectInternal(const nsCString& aHost, const uint16_t& aPort) +{ + nsresult rv; + + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, nsCString(aHost).get(), aPort)); + + if (!mSocket) { + return NS_ERROR_NOT_AVAILABLE; + } + + PRNetAddr prAddr; + PR_InitializeNetAddr(PR_IpAddrAny, aPort, &prAddr); + PRStatus status = PR_StringToNetAddr(aHost.BeginReading(), &prAddr); + if (status != PR_SUCCESS) { + return NS_ERROR_FAILURE; + } + + mozilla::net::NetAddr addr; + PRNetAddrToNetAddr(&prAddr, &addr); + + rv = mSocket->Connect(&addr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +bool +UDPSocketParent::RecvOutgoingData(const UDPData& aData, + const UDPSocketAddr& aAddr) +{ + if (!mSocket) { + NS_WARNING("sending socket is closed"); + FireInternalError(__LINE__); + return true; + } + + nsresult rv; + if (mFilter) { + if (aAddr.type() != UDPSocketAddr::TNetAddr) { + return true; + } + + // TODO, Packet filter doesn't support input stream yet. + if (aData.type() != UDPData::TArrayOfuint8_t) { + return true; + } + + bool allowed; + const InfallibleTArray<uint8_t>& data(aData.get_ArrayOfuint8_t()); + rv = mFilter->FilterPacket(&aAddr.get_NetAddr(), data.Elements(), + data.Length(), nsISocketFilter::SF_OUTGOING, + &allowed); + + // Sending unallowed data, kill content. + if (NS_WARN_IF(NS_FAILED(rv)) || !allowed) { + return false; + } + } + + switch(aData.type()) { + case UDPData::TArrayOfuint8_t: + Send(aData.get_ArrayOfuint8_t(), aAddr); + break; + case UDPData::TInputStreamParams: + Send(aData.get_InputStreamParams(), aAddr); + break; + default: + MOZ_ASSERT(false, "Invalid data type!"); + return true; + } + + return true; +} + +void +UDPSocketParent::Send(const InfallibleTArray<uint8_t>& aData, + const UDPSocketAddr& aAddr) +{ + nsresult rv; + uint32_t count; + switch(aAddr.type()) { + case UDPSocketAddr::TUDPAddressInfo: { + const UDPAddressInfo& addrInfo(aAddr.get_UDPAddressInfo()); + rv = mSocket->Send(addrInfo.addr(), addrInfo.port(), + aData.Elements(), aData.Length(), &count); + break; + } + case UDPSocketAddr::TNetAddr: { + const NetAddr& addr(aAddr.get_NetAddr()); + rv = mSocket->SendWithAddress(&addr, aData.Elements(), + aData.Length(), &count); + break; + } + default: + MOZ_ASSERT(false, "Invalid address type!"); + return; + } + + if (NS_WARN_IF(NS_FAILED(rv)) || count == 0) { + FireInternalError(__LINE__); + } +} + +void +UDPSocketParent::Send(const InputStreamParams& aStream, + const UDPSocketAddr& aAddr) +{ + nsTArray<mozilla::ipc::FileDescriptor> fds; + nsCOMPtr<nsIInputStream> stream = DeserializeInputStream(aStream, fds); + + if (NS_WARN_IF(!stream)) { + return; + } + + nsresult rv; + switch(aAddr.type()) { + case UDPSocketAddr::TUDPAddressInfo: { + const UDPAddressInfo& addrInfo(aAddr.get_UDPAddressInfo()); + rv = mSocket->SendBinaryStream(addrInfo.addr(), addrInfo.port(), stream); + break; + } + case UDPSocketAddr::TNetAddr: { + const NetAddr& addr(aAddr.get_NetAddr()); + rv = mSocket->SendBinaryStreamWithAddress(&addr, stream); + break; + } + default: + MOZ_ASSERT(false, "Invalid address type!"); + return; + } + + if (NS_FAILED(rv)) { + FireInternalError(__LINE__); + } +} + +bool +UDPSocketParent::RecvJoinMulticast(const nsCString& aMulticastAddress, + const nsCString& aInterface) +{ + nsresult rv = mSocket->JoinMulticast(aMulticastAddress, aInterface); + + if (NS_WARN_IF(NS_FAILED(rv))) { + FireInternalError(__LINE__); + } + + return true; +} + +bool +UDPSocketParent::RecvLeaveMulticast(const nsCString& aMulticastAddress, + const nsCString& aInterface) +{ + nsresult rv = mSocket->LeaveMulticast(aMulticastAddress, aInterface); + + if (NS_WARN_IF(NS_FAILED(rv))) { + FireInternalError(__LINE__); + } + + return true; +} + +bool +UDPSocketParent::RecvClose() +{ + if (!mSocket) { + return true; + } + + nsresult rv = mSocket->Close(); + mSocket = nullptr; + + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return true; +} + +bool +UDPSocketParent::RecvRequestDelete() +{ + mozilla::Unused << Send__delete__(this); + return true; +} + +void +UDPSocketParent::ActorDestroy(ActorDestroyReason why) +{ + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; + if (mSocket) { + mSocket->Close(); + } + mSocket = nullptr; +} + +// nsIUDPSocketListener + +NS_IMETHODIMP +UDPSocketParent::OnPacketReceived(nsIUDPSocket* aSocket, nsIUDPMessage* aMessage) +{ + // receiving packet from remote host, forward the message content to child process + if (!mIPCOpen) { + return NS_OK; + } + + uint16_t port; + nsCString ip; + nsCOMPtr<nsINetAddr> fromAddr; + aMessage->GetFromAddr(getter_AddRefs(fromAddr)); + fromAddr->GetPort(&port); + fromAddr->GetAddress(ip); + + nsCString data; + aMessage->GetData(data); + + const char* buffer = data.get(); + uint32_t len = data.Length(); + UDPSOCKET_LOG(("%s: %s:%u, length %u", __FUNCTION__, ip.get(), port, len)); + + if (mFilter) { + bool allowed; + mozilla::net::NetAddr addr; + fromAddr->GetNetAddr(&addr); + nsresult rv = mFilter->FilterPacket(&addr, + (const uint8_t*)buffer, len, + nsISocketFilter::SF_INCOMING, + &allowed); + // Receiving unallowed data, drop. + if (NS_WARN_IF(NS_FAILED(rv)) || !allowed) { + if (!allowed) { + UDPSOCKET_LOG(("%s: not allowed", __FUNCTION__)); + } + return NS_OK; + } + } + + FallibleTArray<uint8_t> fallibleArray; + if (!fallibleArray.InsertElementsAt(0, buffer, len, fallible)) { + FireInternalError(__LINE__); + return NS_ERROR_OUT_OF_MEMORY; + } + InfallibleTArray<uint8_t> infallibleArray; + infallibleArray.SwapElements(fallibleArray); + + // compose callback + mozilla::Unused << SendCallbackReceivedData(UDPAddressInfo(ip, port), infallibleArray); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketParent::OnStopListening(nsIUDPSocket* aSocket, nsresult aStatus) +{ + // underlying socket is dead, send state update to child process + if (mIPCOpen) { + mozilla::Unused << SendCallbackClosed(); + } + return NS_OK; +} + +void +UDPSocketParent::FireInternalError(uint32_t aLineNo) +{ + if (!mIPCOpen) { + return; + } + + mozilla::Unused << SendCallbackError(NS_LITERAL_CSTRING("Internal error"), + NS_LITERAL_CSTRING(__FILE__), aLineNo); +} + +void +UDPSocketParent::SendInternalError(nsIEventTarget *aThread, + uint32_t aLineNo) +{ + UDPSOCKET_LOG(("SendInternalError: %u", aLineNo)); + Unused << + NS_WARN_IF(NS_FAILED(aThread->Dispatch(WrapRunnable( + RefPtr<UDPSocketParent>(this), + &UDPSocketParent::FireInternalError, + aLineNo), + NS_DISPATCH_NORMAL))); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/network/UDPSocketParent.h b/dom/network/UDPSocketParent.h new file mode 100644 index 0000000000..4b828e0133 --- /dev/null +++ b/dom/network/UDPSocketParent.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_UDPSocketParent_h__ +#define mozilla_dom_UDPSocketParent_h__ + +#include "mozilla/net/PUDPSocketParent.h" +#include "nsCOMPtr.h" +#include "nsIUDPSocket.h" +#include "nsISocketFilter.h" +#include "mozilla/dom/PermissionMessageUtils.h" + +namespace mozilla { +namespace net { +class PNeckoParent; +} // namespace net + +namespace dom { + +class UDPSocketParent : public mozilla::net::PUDPSocketParent + , public nsIUDPSocketListener +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIUDPSOCKETLISTENER + + explicit UDPSocketParent(PBackgroundParent* aManager); + explicit UDPSocketParent(PNeckoParent* aManager); + + bool Init(const IPC::Principal& aPrincipal, const nsACString& aFilter); + + virtual bool RecvBind(const UDPAddressInfo& aAddressInfo, + const bool& aAddressReuse, const bool& aLoopback, + const uint32_t& recvBufferSize, + const uint32_t& sendBufferSize) override; + virtual bool RecvConnect(const UDPAddressInfo& aAddressInfo) override; + void DoSendConnectResponse(const UDPAddressInfo& aAddressInfo); + void SendConnectResponse(nsIEventTarget *aThread, + const UDPAddressInfo& aAddressInfo); + void DoConnect(nsCOMPtr<nsIUDPSocket>& aSocket, + nsCOMPtr<nsIEventTarget>& aReturnThread, + const UDPAddressInfo& aAddressInfo); + + virtual bool RecvOutgoingData(const UDPData& aData, const UDPSocketAddr& aAddr) override; + + virtual bool RecvClose() override; + virtual bool RecvRequestDelete() override; + virtual bool RecvJoinMulticast(const nsCString& aMulticastAddress, + const nsCString& aInterface) override; + virtual bool RecvLeaveMulticast(const nsCString& aMulticastAddress, + const nsCString& aInterface) override; + +private: + virtual ~UDPSocketParent(); + + virtual void ActorDestroy(ActorDestroyReason why) override; + void Send(const InfallibleTArray<uint8_t>& aData, const UDPSocketAddr& aAddr); + void Send(const InputStreamParams& aStream, const UDPSocketAddr& aAddr); + nsresult BindInternal(const nsCString& aHost, const uint16_t& aPort, + const bool& aAddressReuse, const bool& aLoopback, + const uint32_t& recvBufferSize, + const uint32_t& sendBufferSize); + nsresult ConnectInternal(const nsCString& aHost, const uint16_t& aPort); + void FireInternalError(uint32_t aLineNo); + void SendInternalError(nsIEventTarget *aThread, + uint32_t aLineNo); + + // One of these will be null and the other non-null. + PBackgroundParent* mBackgroundManager; + PNeckoParent* mNeckoManager; + + bool mIPCOpen; + nsCOMPtr<nsIUDPSocket> mSocket; + nsCOMPtr<nsISocketFilter> mFilter; + nsCOMPtr<nsIPrincipal> mPrincipal; +}; + +} // namespace dom +} // namespace mozilla + +#endif // !defined(mozilla_dom_UDPSocketParent_h__) diff --git a/dom/network/interfaces/moz.build b/dom/network/interfaces/moz.build new file mode 100644 index 0000000000..add6875429 --- /dev/null +++ b/dom/network/interfaces/moz.build @@ -0,0 +1,19 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPIDL_SOURCES += [ + 'nsIMozNavigatorNetwork.idl', + 'nsITCPSocketCallback.idl', + 'nsIUDPSocketChild.idl', +] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk': + XPIDL_SOURCES += [ + 'nsIEthernetManager.idl', + 'nsINetworkStatsServiceProxy.idl', + ] + +XPIDL_MODULE = 'dom_network' diff --git a/dom/network/interfaces/nsIEthernetManager.idl b/dom/network/interfaces/nsIEthernetManager.idl new file mode 100644 index 0000000000..2b92dc88f0 --- /dev/null +++ b/dom/network/interfaces/nsIEthernetManager.idl @@ -0,0 +1,137 @@ +/* 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/. */ + +#include "nsISupports.idl" + +[scriptable, function, uuid(2a3ad56c-edc0-439f-8aae-900b331ddf49)] +interface nsIEthernetManagerCallback : nsISupports +{ + /** + * Callback function used to report the success of different operations. + * + * @param success + * Boolean value indicates the success of an operation. + * @prarm message + * Message reported in the end of operation. + */ + void notify(in boolean success, in DOMString message); +}; + +[scriptable, function, uuid(1746e7dd-92d4-43fa-8ef4-bc13d0b60353)] +interface nsIEthernetManagerScanCallback : nsISupports +{ + /** + * Callback function used to report the result of scan function. + * + * @param list + * List of available ethernet interfaces. + */ + void notify(in jsval list); +}; + +/** + * An internal idl provides control to ethernet interfaces. + */ +[scriptable, uuid(81750c87-bb3b-4724-b955-834eafa53fd1)] +interface nsIEthernetManager : nsISupports +{ + /** + * List of exisiting interface name. + */ + readonly attribute jsval interfaceList; + + /** + * Scan available ethernet interfaces on device. + * + * @param callback + * Callback function. + */ + void scan(in nsIEthernetManagerScanCallback callback); + + /** + * Add a new interface to the interface list. + * + * @param ifname + * Interface name. Should be the form of "eth*". + * @param callback + * Callback function. + */ + void addInterface(in DOMString ifname, + in nsIEthernetManagerCallback callback); + + /** + * Remove an existing interface from the interface list. + * + * @param ifname + * Interface name. + * @param Callback + * Callback function. + */ + void removeInterface(in DOMString ifname, + in nsIEthernetManagerCallback callback); + + /** + * Update a conifg of an existing interface in the interface list. + * + * @param ifname + * Interface name. + * @param config + * .ip: IP address. + * .prefixLength: Mask length. + * .gateway: Gateway. + * .dnses: DNS addresses. + * .httpProxyHost: HTTP proxy host. + * .httpProxyPort: HTTP proxy port. + * .ipMode: IP mode, can be 'dhcp' or 'static'. + * @param callback + * Callback function. + */ + void updateInterfaceConfig(in DOMString ifname, + in jsval config, + in nsIEthernetManagerCallback callback); + + /** + * Enable networking of an existing interface in the interface list. + * + * @param ifname + * Interface name. + * @param callback + * Callback function. + */ + void enable(in DOMString ifname, + in nsIEthernetManagerCallback callback); + + /** + * Disable networking of an existing interface in the interface list. + * + * @param ifname + * Interface name. + * @param callback + * Callback function. + */ + void disable(in DOMString ifname, + in nsIEthernetManagerCallback callback); + + /** + * Make an existing interface connect to network. + * + * @param ifname + * Interface name. + * @param callback + * Callback function. + */ + void connect(in DOMString ifname, + in nsIEthernetManagerCallback callback); + + /** + * Disconnect a connected interface in the interface list. + * + * @param ifname + * Interface name. + * @param callback + * Callback function. + */ + void disconnect(in DOMString ifname, + in nsIEthernetManagerCallback callback); +}; diff --git a/dom/network/interfaces/nsIMozNavigatorNetwork.idl b/dom/network/interfaces/nsIMozNavigatorNetwork.idl new file mode 100644 index 0000000000..1d667aada3 --- /dev/null +++ b/dom/network/interfaces/nsIMozNavigatorNetwork.idl @@ -0,0 +1,13 @@ +/* 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/. */ + +#include "nsISupports.idl" + +interface nsINetworkProperties; + +[uuid(7956523b-631e-4f80-94a5-3883bcfd6bf3)] +interface nsIMozNavigatorNetwork : nsISupports +{ + readonly attribute nsINetworkProperties properties; +}; diff --git a/dom/network/interfaces/nsINetworkStatsServiceProxy.idl b/dom/network/interfaces/nsINetworkStatsServiceProxy.idl new file mode 100644 index 0000000000..cd6765c68a --- /dev/null +++ b/dom/network/interfaces/nsINetworkStatsServiceProxy.idl @@ -0,0 +1,64 @@ +/* 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/. */ + +#include "nsISupports.idl" + +interface nsINetworkInfo; + +[scriptable, function, uuid(5f821529-1d80-4ab5-a933-4e1b3585b6bc)] +interface nsINetworkStatsServiceProxyCallback : nsISupports +{ + /* + * @param aResult callback result with boolean value + * @param aMessage message + */ + void notify(in boolean aResult, in jsval aMessage); +}; + +[scriptable, uuid(f4f3e901-e102-499d-9d37-dc9951f52df7)] +interface nsINetworkStatsServiceProxy : nsISupports +{ + /* + * An interface used to record per-app traffic data. + * @param aAppId app id + * @param aIsInIsolatedMozBrowser + * true if the frame is an isolated mozbrowser element. <iframe + * mozbrowser mozapp> and <xul:browser> are not considered to be + * mozbrowser elements. <iframe mozbrowser noisolation> does not count + * as isolated since isolation is disabled. Isolation can only be + * disabled if the containing document is chrome. + * @param aNetworkInterface network + * @param aTimeStamp time stamp + * @param aRxBytes received data amount + * @param aTxBytes transmitted data amount + * @param aIsAccumulative is stats accumulative + * @param aCallback an optional callback + */ + void saveAppStats(in unsigned long aAppId, + in boolean aIsInIsolatedMozBrowser, + in nsINetworkInfo aNetworkInfo, + in unsigned long long aTimeStamp, + in unsigned long long aRxBytes, + in unsigned long long aTxBytes, + in boolean aIsAccumulative, + [optional] in nsINetworkStatsServiceProxyCallback aCallback); + + /* + * An interface used to record per-system service traffic data. + * @param aServiceType system service type + * @param aNetworkInterface network + * @param aTimeStamp time stamp + * @param aRxBytes received data amount + * @param aTxBytes transmitted data amount + * @param aIsAccumulative is stats accumulative + * @param aCallback an optional callback + */ + void saveServiceStats(in string aServiceType, + in nsINetworkInfo aNetworkInfo, + in unsigned long long aTimeStamp, + in unsigned long long aRxBytes, + in unsigned long long aTxBytes, + in boolean aIsAccumulative, + [optional] in nsINetworkStatsServiceProxyCallback aCallback); +}; diff --git a/dom/network/interfaces/nsITCPSocketCallback.idl b/dom/network/interfaces/nsITCPSocketCallback.idl new file mode 100644 index 0000000000..5ab85dcc7a --- /dev/null +++ b/dom/network/interfaces/nsITCPSocketCallback.idl @@ -0,0 +1,59 @@ +/* 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/. */ + +/** + * MozTCPSocket exposes a TCP client and server sockets + * to highly privileged apps. It provides a buffered, non-blocking + * interface for sending. For receiving, it uses an asynchronous, + * event handler based interface. + */ + +#include "domstubs.idl" + +%{C++ +#include "nsTArrayForwardDeclare.h" +%} +[ref] native nsUint8TArrayRef(InfallibleTArray<uint8_t>); +[ptr] native JSContextPtr(JSContext); + + +/* + * This interface is implemented in TCPSocket.cpp as an internal interface + * for use in cross-process socket implementation. + * Needed to account for multiple possible types that can be provided to + * the socket callbacks as arguments. + */ +[scriptable, uuid(ac2c4b69-cb79-4767-b1ce-bcf62945cd39)] +interface nsITCPSocketCallback : nsISupports { + // Limitation of TCPSocket's buffer size. + const unsigned long BUFFER_SIZE = 65536; + + // Dispatch an "error" event at this object with the given name and type. + void fireErrorEvent(in AString name, in AString type); + + // Dispatch a "data" event at this object with a string + void fireDataStringEvent(in DOMString type, in ACString data); + + // Dispatch a "data" event at this object with an Array + void fireDataArrayEvent(in DOMString type, [const] in nsUint8TArrayRef data); + + // Dispatch an event of the given type at this object. + void fireEvent(in DOMString type); + + // Update the DOM object's readyState. + // @param readyState + // new ready state + void updateReadyState(in unsigned long readystate); + + // Update the DOM object's bufferedAmount value with a tracking number to + // to allow tracking of which writes are "in-flight" + // @param bufferedAmount + // TCPSocket parent's bufferedAmount. + // @param trackingNumber + // A number to ensure the bufferedAmount is updated after data + // from child are sent to parent. + void updateBufferedAmount(in uint32_t bufferedAmount, + in uint32_t trackingNumber); +}; + diff --git a/dom/network/interfaces/nsIUDPSocketChild.idl b/dom/network/interfaces/nsIUDPSocketChild.idl new file mode 100644 index 0000000000..3a07fae665 --- /dev/null +++ b/dom/network/interfaces/nsIUDPSocketChild.idl @@ -0,0 +1,78 @@ +/* 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/. */ + +#include "nsISupports.idl" +#include "nsINetAddr.idl" + +interface nsIUDPSocketInternal; +interface nsIInputStream; +interface nsIPrincipal; + +%{ C++ +namespace mozilla { +namespace net { +union NetAddr; +} +} +%} +native NetAddr(mozilla::net::NetAddr); +[ptr] native NetAddrPtr(mozilla::net::NetAddr); + +[scriptable, uuid(1e6ad73b-6c05-4d78-9a88-2d357b88f58b)] +interface nsIUDPSocketChild : nsISupports +{ + readonly attribute unsigned short localPort; + readonly attribute AUTF8String localAddress; + attribute AUTF8String filterName; + + // Allow hosting this over PBackground instead of PNecko + [noscript] void setBackgroundSpinsEvents(); + + // Tell the chrome process to bind the UDP socket to a given local host and port + void bind(in nsIUDPSocketInternal socket, in nsIPrincipal principal, + in AUTF8String host, in unsigned short port, + in bool addressReuse, in bool loopback, in uint32_t recvBufferSize, + in uint32_t sendBufferSize); + + // Tell the chrome process to connect the UDP socket to a given remote host and port + void connect(in nsIUDPSocketInternal socket, in AUTF8String host, in unsigned short port); + + // Tell the chrome process to perform equivalent operations to all following methods + void send(in AUTF8String host, in unsigned short port, + [const, array, size_is(byteLength)] in uint8_t bytes, + in unsigned long byteLength); + // Send without DNS query + void sendWithAddr(in nsINetAddr addr, + [const, array, size_is(byteLength)] in uint8_t bytes, + in unsigned long byteLength); + [noscript] void sendWithAddress([const] in NetAddrPtr addr, + [const, array, size_is(byteLength)] in uint8_t bytes, + in unsigned long byteLength); + // Send input stream. This must be a buffered stream implementation. + void sendBinaryStream(in AUTF8String host, in unsigned short port, in nsIInputStream stream); + + void close(); + void joinMulticast(in AUTF8String multicastAddress, in AUTF8String iface); + void leaveMulticast(in AUTF8String multicastAddress, in AUTF8String iface); +}; + +/* + * Internal interface for callback from chrome process + */ +[scriptable, uuid(613dd3ad-598b-4da9-ad63-bbda50c20098)] +interface nsIUDPSocketInternal : nsISupports +{ + // callback while socket is opened. localPort and localAddress is ready until this time. + void callListenerOpened(); + // callback while socket is connected. + void callListenerConnected(); + // callback while socket is closed. + void callListenerClosed(); + // callback while incoming packet is received. + void callListenerReceivedData(in AUTF8String host, in unsigned short port, + [const, array, size_is(dataLength)] in uint8_t data, + in unsigned long dataLength); + // callback while any error happened. + void callListenerError(in AUTF8String message, in AUTF8String filename, in uint32_t lineNumber); +}; diff --git a/dom/network/moz.build b/dom/network/moz.build new file mode 100644 index 0000000000..63e5c75c0c --- /dev/null +++ b/dom/network/moz.build @@ -0,0 +1,76 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DIRS += ['interfaces'] + +MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini'] +MOCHITEST_MANIFESTS += ['tests/mochitest.ini'] + +EXPORTS.mozilla.dom += [ + 'TCPServerSocket.h', + 'TCPSocket.h', + 'UDPSocket.h', +] + +EXPORTS.mozilla.dom.network += [ + 'Connection.h', + 'Constants.h', + 'TCPServerSocketChild.h', + 'TCPServerSocketParent.h', + 'TCPSocketChild.h', + 'TCPSocketParent.h', + 'Types.h', + 'UDPSocketChild.h', + 'UDPSocketParent.h', +] + +UNIFIED_SOURCES += [ + 'Connection.cpp', + 'TCPServerSocket.cpp', + 'TCPServerSocketChild.cpp', + 'TCPServerSocketParent.cpp', + 'TCPSocket.cpp', + 'TCPSocketChild.cpp', + 'TCPSocketParent.cpp', + 'UDPSocket.cpp', + 'UDPSocketChild.cpp', + 'UDPSocketParent.cpp', +] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk': + EXTRA_JS_MODULES += [ + 'NetworkStatsDB.jsm', + 'NetworkStatsService.jsm', + ] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk': + EXTRA_COMPONENTS += [ + 'EthernetManager.js', + 'EthernetManager.manifest', + 'NetworkStatsManager.js', + 'NetworkStatsManager.manifest', + 'NetworkStatsServiceProxy.js', + 'NetworkStatsServiceProxy.manifest', + ] + EXPORTS.mozilla.dom.network += [ + 'NetUtils.h', + ] + UNIFIED_SOURCES += [ + 'NetUtils.cpp', + ] + +IPDL_SOURCES += [ + 'PTCPServerSocket.ipdl', + 'PTCPSocket.ipdl', + 'PUDPSocket.ipdl', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] diff --git a/dom/network/tests/add_task.js b/dom/network/tests/add_task.js new file mode 100644 index 0000000000..3028afdb75 --- /dev/null +++ b/dom/network/tests/add_task.js @@ -0,0 +1,83 @@ +// Temporary implementation of add_task for mochitest-plain until bug 1078657 is +// implemented. +SimpleTest.waitForExplicitFinish(); +(function(scope) { + var pendingTasks = []; + var pendingPromise = null; + + // Strict spawn function that takes a known generatorFunc and assumes that + // every yielded value will be a Promise. If nesting is desired, then yield* + // should be used! + function spawn(generatorFunc) { + return new Promise(function(resolve, reject) { + try { + var iterator = generatorFunc(); + } + catch (ex) { + ok(false, 'Problem invoking generator func: ' + ex + ': ' + ex.stack); + return; + } + var stepResolved = function(result) { + try { + var iterStep = iterator.next(result); + } + catch (ex) { + ok(false, 'Problem invoking iterator step: ' + ex + ': ' + ex.stack); + return; + } + if (iterStep.done) { + resolve(iterStep.value); + return; + } + if (!iterStep.value || !iterStep.value.then) { + ok(false, 'Iterator step returned non-Promise: ' + iterStep.value); + } + iterStep.value.then(stepResolved, generalErrback); + }; + stepResolved(); + }); + } + + function maybeSpawn(promiseOrGenerator) { + if (promiseOrGenerator.then) { + return promiseOrGenerator; + } + return spawn(promiseOrGenerator); + } + + scope.add_task = function(thing) { + pendingTasks.push(thing); + }; + + function generalErrback(ex) { + ok(false, + 'A rejection happened: ' + + (ex ? (ex + ': ' + ex.stack) : '')); + } + + function runNextTask() { + if (pendingTasks.length) { + pendingPromise = maybeSpawn(pendingTasks.shift()); + pendingPromise.then(runNextTask, generalErrback); + } else { + SimpleTest.finish(); + } + } + + // Trigger runNextTask after we think all JS files have been loaded. + // The primary goal is that we can call SimpleTest.finish() after all test + // code has been loaded and run. We gate this based on the document's + // readyState. + var running = false; + function maybeStartRunning() { + if (!running && document.readyState === 'complete') { + running = true; + document.removeEventListener('readystateChange', maybeStartRunning); + // Defer to a subsequent turn of the event loop to let micro-tasks and any + // other clever setTimeout(0) instances run first. + window.setTimeout(runNextTask, 0); + } + } + document.addEventListener('readystatechange', maybeStartRunning); + maybeStartRunning(); +})(this); diff --git a/dom/network/tests/chrome.ini b/dom/network/tests/chrome.ini new file mode 100644 index 0000000000..ca1c3f79a3 --- /dev/null +++ b/dom/network/tests/chrome.ini @@ -0,0 +1,12 @@ +[DEFAULT] +support-files = + tcpsocket_test.jsm + test_tcpsocket_client_and_server_basics.js + add_task.js + file_udpsocket_iframe.html + +[test_tcpsocket_jsm.html] +[test_tcpsocket_client_and_server_basics.html] +[test_tcpsocket_enabled_with_perm.html] +[test_tcpsocket_legacy.html] +[test_udpsocket.html] diff --git a/dom/network/tests/file_udpsocket_iframe.html b/dom/network/tests/file_udpsocket_iframe.html new file mode 100644 index 0000000000..1e124552dd --- /dev/null +++ b/dom/network/tests/file_udpsocket_iframe.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test UDPSocket BFCache</title> +</head> +<body> +<script type="application/javascript;version=1.8"> +'use strict'; +window.addEventListener('load', function onload() { + window.removeEventListener('load', onload); + let remotePort = parseInt(window.location.search.substring(1), 10); + let socket = new UDPSocket(); + socket.addEventListener('message', function () { + socket.send('fail', '127.0.0.1', remotePort); + }); + + socket.opened.then(function() { + socket.send('ready', '127.0.0.1', remotePort); + }); +}); +</script> +</body> +</html> diff --git a/dom/network/tests/marionette/head.js b/dom/network/tests/marionette/head.js new file mode 100644 index 0000000000..edbf2dd853 --- /dev/null +++ b/dom/network/tests/marionette/head.js @@ -0,0 +1,552 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let Promise = SpecialPowers.Cu.import("resource://gre/modules/Promise.jsm").Promise; + +const ETHERNET_MANAGER_CONTRACT_ID = "@mozilla.org/ethernetManager;1"; + +const INTERFACE_UP = "UP"; +const INTERFACE_DOWN = "DOWN"; + +let gTestSuite = (function() { + let suite = {}; + + // Private member variables of the returned object |suite|. + let ethernetManager = SpecialPowers.Cc[ETHERNET_MANAGER_CONTRACT_ID] + .getService(SpecialPowers.Ci.nsIEthernetManager); + let pendingEmulatorShellCount = 0; + + /** + * Send emulator shell command with safe guard. + * + * We should only call |finish()| after all emulator command transactions + * end, so here comes with the pending counter. Resolve when the emulator + * gives positive response, and reject otherwise. + * + * Fulfill params: an array of emulator response lines. + * Reject params: an array of emulator response lines. + * + * @param command + * A string command to be passed to emulator through its telnet console. + * + * @return A deferred promise. + */ + function runEmulatorShellSafe(command) { + let deferred = Promise.defer(); + + ++pendingEmulatorShellCount; + runEmulatorShell(command, function(aResult) { + --pendingEmulatorShellCount; + + ok(true, "Emulator shell response: " + JSON.stringify(aResult)); + if (Array.isArray(aResult)) { + deferred.resolve(aResult); + } else { + deferred.reject(aResult); + } + }); + + return deferred.promise; + } + + /** + * Get the system network conifg by the given interface name. + * + * Use shell command 'netcfg' to get the list of network cofig. + * + * Fulfill params: An object of { name, flag, ip } + * + * @parm ifname + * Interface name. + * + * @return A deferred promise. + */ + function getNetworkConfig(ifname) { + return runEmulatorShellSafe(['netcfg']) + .then(result => { + // Sample 'netcfg' output: + // + // lo UP 127.0.0.1/8 0x00000049 00:00:00:00:00:00 + // eth0 UP 10.0.2.15/24 0x00001043 52:54:00:12:34:56 + // eth1 DOWN 0.0.0.0/0 0x00001002 52:54:00:12:34:57 + // rmnet1 DOWN 0.0.0.0/0 0x00001002 52:54:00:12:34:59 + + let config; + + for (let i = 0; i < result.length; i++) { + let tokens = result[i].split(/\s+/); + let name = tokens[0]; + let flag = tokens[1]; + let ip = tokens[2].split(/\/+/)[0]; + if (name == ifname) { + config = { name: name, flag: flag, ip: ip }; + break; + } + } + + return config; + }); + } + + /** + * Get the ip assigned by dhcp server of a given interface name. + * + * Get the ip from android property 'dhcp.[ifname].ipaddress'. + * + * Fulfill params: A string of ip address. + * + * @parm ifname + * Interface name. + * + * @return A deferred promise. + */ + function getDhcpIpAddr(ifname) { + return runEmulatorShellSafe(['getprop', 'dhcp.' + ifname + '.ipaddress']) + .then(function(ipAddr) { + return ipAddr[0]; + }); + } + + /** + * Get the gateway assigned by dhcp server of a given interface name. + * + * Get the ip from android property 'dhcp.[ifname].gateway'. + * + * Fulfill params: A string of gateway. + * + * @parm ifname + * Interface name. + * + * @return A deferred promise. + */ + function getDhcpGateway(ifname) { + return runEmulatorShellSafe(['getprop', 'dhcp.' + ifname + '.gateway']) + .then(function(gateway) { + return gateway[0]; + }); + } + + /** + * Get the default route. + * + * Use shell command 'ip route' to get the default of device. + * + * Fulfill params: An array of { name, gateway } + * + * @return A deferred promise. + */ + function getDefaultRoute() { + return runEmulatorShellSafe(['ip', 'route']) + .then(result => { + // Sample 'ip route' output: + // + // 10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 + // default via 10.0.2.2 dev eth0 metric 2 + + let routeInfo = []; + + for (let i = 0; i < result.length; i++) { + if (!result[i].match('default')) { + continue; + } + + let tokens = result[i].split(/\s+/); + let name = tokens[4]; + let gateway = tokens[2]; + routeInfo.push({ name: name, gateway: gateway }); + } + + return routeInfo; + }); + } + + /** + * Check a specific interface is enabled or not. + * + * @parm ifname + * Interface name. + * @parm enabled + * A boolean value used to check interface is disable or not. + * + * @return A deferred promise. + */ + function checkInterfaceIsEnabled(ifname, enabled) { + return getNetworkConfig(ifname) + .then(function(config) { + if (enabled) { + is(config.flag, INTERFACE_UP, "Interface is enabled as expected."); + } else { + is(config.flag, INTERFACE_DOWN, "Interface is disabled as expected."); + } + }); + } + + /** + * Check the ip of a specific interface is equal to given ip or not. + * + * @parm ifname + * Interface name. + * @parm ip + * Given ip address. + * + * @return A deferred promise. + */ + function checkInterfaceIpAddr(ifname, ip) { + return getNetworkConfig(ifname) + .then(function(config) { + is(config.ip, ip, "IP is right as expected."); + }); + } + + /** + * Check the default gateway of a specific interface is equal to given gateway + * or not. + * + * @parm ifname + * Interface name. + * @parm gateway + * Given gateway. + * + * @return A deferred promise. + */ + function checkDefaultRoute(ifname, gateway) { + return getDefaultRoute() + .then(function(routeInfo) { + for (let i = 0; i < routeInfo.length; i++) { + if (routeInfo[i].name == ifname) { + is(routeInfo[i].gateway, gateway, + "Default gateway is right as expected."); + return true; + } + } + + if (!gateway) { + ok(true, "Default route is cleared."); + return true; + } + + // TODO: should we ok(false, ......) here? + return false; + }); + } + + /** + * Check the length of interface list in EthernetManager is equal to given + * length or not. + * + * @parm length + * Given length. + */ + function checkInterfaceListLength(length) { + let list = ethernetManager.interfaceList; + is(length, list.length, "List length is equal as expected."); + } + + /** + * Check the given interface exists on device or not. + * + * @parm ifname + * Interface name. + * + * @return A deferred promise. + */ + function checkInterfaceExist(ifname) { + return scanInterfaces() + .then(list => { + let index = list.indexOf(ifname); + if (index < 0) { + throw "Interface " + ifname + " not found."; + } + + ok(true, ifname + " exists."); + }); + } + + /** + * Scan for available ethernet interfaces. + * + * Fulfill params: A list of available interfaces found in device. + * + * @return A deferred promise. + */ + function scanInterfaces() { + let deferred = Promise.defer(); + + ethernetManager.scan(function onScan(list) { + deferred.resolve(list); + }); + + return deferred.promise; + } + + /** + * Add an interface into interface list. + * + * Fulfill params: A boolean value indicates success or not. + * + * @param ifname + * Interface name. + * + * @return A deferred promise. + */ + function addInterface(ifname) { + let deferred = Promise.defer(); + + ethernetManager.addInterface(ifname, function onAdd(success, message) { + ok(success, "Add interface " + ifname + " succeeded."); + is(message, "ok", "Message is as expected."); + + deferred.resolve(success); + }); + + return deferred.promise; + } + + /** + * Remove an interface form the interface list. + * + * Fulfill params: A boolean value indicates success or not. + * + * @param ifname + * Interface name. + * + * @return A deferred promise. + */ + function removeInterface(ifname) { + let deferred = Promise.defer(); + + ethernetManager.removeInterface(ifname, function onRemove(success, message) { + ok(success, "Remove interface " + ifname + " succeeded."); + is(message, "ok", "Message is as expected."); + + deferred.resolve(success); + }); + + return deferred.promise; + } + + /** + * Enable networking of an interface in the interface list. + * + * Fulfill params: A boolean value indicates success or not. + * + * @param ifname + * Interface name. + * + * @return A deferred promise. + */ + function enableInterface(ifname) { + let deferred = Promise.defer(); + + ethernetManager.enable(ifname, function onEnable(success, message) { + ok(success, "Enable interface " + ifname + " succeeded."); + is(message, "ok", "Message is as expected."); + + deferred.resolve(success); + }); + + return deferred.promise; + } + + /** + * Disable networking of an interface in the interface list. + * + * Fulfill params: A boolean value indicates success or not. + * + * @param ifname + * Interface name. + * + * @return A deferred promise. + */ + function disableInterface(ifname) { + let deferred = Promise.defer(); + + ethernetManager.disable(ifname, function onDisable(success, message) { + ok(success, "Disable interface " + ifname + " succeeded."); + is(message, "ok", "Message is as expected."); + + deferred.resolve(success); + }); + + return deferred.promise; + } + + /** + * Make an interface connect to network. + * + * Fulfill params: A boolean value indicates success or not. + * + * @param ifname + * Interface name. + * + * @return A deferred promise. + */ + function makeInterfaceConnect(ifname) { + let deferred = Promise.defer(); + + ethernetManager.connect(ifname, function onConnect(success, message) { + ok(success, "Interface " + ifname + " is connected successfully."); + is(message, "ok", "Message is as expected."); + + deferred.resolve(success); + }); + + return deferred.promise; + } + + /** + * Make an interface disconnect to network. + * + * Fulfill params: A boolean value indicates success or not. + * + * @param ifname + * Interface name. + * + * @return A deferred promise. + */ + function makeInterfaceDisconnect(ifname) { + let deferred = Promise.defer(); + + ethernetManager.disconnect(ifname, function onDisconnect(success, message) { + ok(success, "Interface " + ifname + " is disconnected successfully."); + is(message, "ok", "Message is as expected."); + + deferred.resolve(success); + }); + + return deferred.promise; + } + + /** + * Update the config the an interface in the interface list. + * + * @param ifname + * Interface name. + * @param config + * .ip: ip address. + * .prefixLength: mask length. + * .gateway: gateway. + * .dnses: dnses. + * .httpProxyHost: http proxy host. + * .httpProxyPort: http porxy port. + * .usingDhcp: an boolean value indicates using dhcp or not. + * + * @return A deferred promise. + */ + function updateInterfaceConfig(ifname, config) { + let deferred = Promise.defer(); + + ethernetManager.updateInterfaceConfig(ifname, config, + function onUpdated(success, message) { + ok(success, "Interface " + ifname + " config is updated successfully " + + "with " + JSON.stringify(config)); + is(message, "ok", "Message is as expected."); + + deferred.resolve(success); + }); + + return deferred.promise; + } + + /** + * Wait for timeout. + * + * @param timeout + * Time in ms. + * + * @return A deferred promise. + */ + function waitForTimeout(timeout) { + let deferred = Promise.defer(); + + setTimeout(function() { + ok(true, "waitForTimeout " + timeout); + deferred.resolve(); + }, timeout); + + return deferred.promise; + } + + /** + * Wait for default route of a specific interface being set and + * check. + * + * @param ifname + * Interface name. + * @param gateway + * Target gateway. + * + * @return A deferred promise. + */ + function waitForDefaultRouteSet(ifname, gateway) { + return gTestSuite.waitForTimeout(500) + .then(() => gTestSuite.checkDefaultRoute(ifname, gateway)) + .then(success => { + if (success) { + ok(true, "Default route is set as expected." + gateway); + return; + } + + ok(true, "Default route is not set yet, check again. " + success); + return waitForDefaultRouteSet(ifname, gateway); + }); + } + + //--------------------------------------------------- + // Public test suite functions + //--------------------------------------------------- + suite.scanInterfaces = scanInterfaces; + suite.addInterface = addInterface; + suite.removeInterface = removeInterface; + suite.enableInterface = enableInterface; + suite.disableInterface = disableInterface; + suite.makeInterfaceConnect = makeInterfaceConnect; + suite.makeInterfaceDisconnect = makeInterfaceDisconnect; + suite.updateInterfaceConfig = updateInterfaceConfig; + suite.getDhcpIpAddr = getDhcpIpAddr; + suite.getDhcpGateway = getDhcpGateway; + suite.checkInterfaceExist = checkInterfaceExist; + suite.checkInterfaceIsEnabled = checkInterfaceIsEnabled; + suite.checkInterfaceIpAddr = checkInterfaceIpAddr; + suite.checkDefaultRoute = checkDefaultRoute; + suite.checkInterfaceListLength = checkInterfaceListLength; + suite.waitForTimeout = waitForTimeout; + suite.waitForDefaultRouteSet = waitForDefaultRouteSet; + + /** + * End up the test run. + * + * Wait until all pending emulator shell commands are done and then |finish| + * will be called in the end. + */ + function cleanUp() { + waitFor(finish, function() { + return pendingEmulatorShellCount === 0; + }); + } + + /** + * Common test routine. + * + * Start a test with the given test case chain. The test environment will be + * settled down before the test. After the test, all the affected things will + * be restored. + * + * @param aTestCaseChain + * The test case entry point, which can be a function or a promise. + * + * @return A deferred promise. + */ + suite.doTest = function(aTestCaseChain) { + return Promise.resolve() + .then(aTestCaseChain) + .then(function onresolve() { + cleanUp(); + }, function onreject(aReason) { + ok(false, 'Promise rejects during test' + (aReason ? '(' + aReason + ')' : '')); + cleanUp(); + }); + }; + + return suite; +})(); diff --git a/dom/network/tests/marionette/manifest.ini b/dom/network/tests/marionette/manifest.ini new file mode 100644 index 0000000000..23f184bae6 --- /dev/null +++ b/dom/network/tests/marionette/manifest.ini @@ -0,0 +1,13 @@ +[DEFAULT] +run-if = buildapp == 'b2g' + +[test_ethernet_add_interface.js] +[test_ethernet_remove_interface.js] +[test_ethernet_enable.js] +[test_ethernet_disable.js] +[test_ethernet_connect_with_dhcp.js] +[test_ethernet_connect_with_static_ip.js] +[test_ethernet_reconnect_with_dhcp.js] +[test_ethernet_reconnect_with_static_ip.js] +[test_ethernet_ip_mode_change.js] +[test_ethernet_disconnect.js] diff --git a/dom/network/tests/marionette/test_ethernet_add_interface.js b/dom/network/tests/marionette/test_ethernet_add_interface.js new file mode 100644 index 0000000000..d628e77054 --- /dev/null +++ b/dom/network/tests/marionette/test_ethernet_add_interface.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = 'head.js'; + +const ETHERNET_INTERFACE_NAME = "eth1"; + +gTestSuite.doTest(function() { + return Promise.resolve() + .then(() => gTestSuite.checkInterfaceExist(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.checkInterfaceListLength(0)) + .then(() => gTestSuite.addInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.checkInterfaceListLength(1)) + .then(() => gTestSuite.removeInterface(ETHERNET_INTERFACE_NAME)); +});
\ No newline at end of file diff --git a/dom/network/tests/marionette/test_ethernet_connect_with_dhcp.js b/dom/network/tests/marionette/test_ethernet_connect_with_dhcp.js new file mode 100644 index 0000000000..57c2df9c2e --- /dev/null +++ b/dom/network/tests/marionette/test_ethernet_connect_with_dhcp.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = 'head.js'; + +const ETHERNET_INTERFACE_NAME = "eth1"; + +function checkDhcpResult(ifname) { + return gTestSuite.getDhcpIpAddr(ifname) + .then(ip => gTestSuite.checkInterfaceIpAddr(ifname, ip)) + .then(() => gTestSuite.getDhcpGateway(ifname)) + .then(gateway => gTestSuite.waitForDefaultRouteSet(ifname, gateway)); +} + +gTestSuite.doTest(function() { + return Promise.resolve() + .then(() => gTestSuite.checkInterfaceExist(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.addInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.enableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceConnect(ETHERNET_INTERFACE_NAME)) + .then(() => checkDhcpResult(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceDisconnect(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.disableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.removeInterface(ETHERNET_INTERFACE_NAME)); +});
\ No newline at end of file diff --git a/dom/network/tests/marionette/test_ethernet_connect_with_static_ip.js b/dom/network/tests/marionette/test_ethernet_connect_with_static_ip.js new file mode 100644 index 0000000000..3adc37b23c --- /dev/null +++ b/dom/network/tests/marionette/test_ethernet_connect_with_static_ip.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = 'head.js'; + +const ETHERNET_INTERFACE_NAME = "eth1"; + +let staticConfig = { + ip: "1.2.3.4", + gateway: "1.2.3.5", + prefixLength: 24, + dnses: ["1.2.3.6"], + ipMode: "static" +}; + +function checkStaticResult(ifname) { + return gTestSuite.checkInterfaceIpAddr(ifname, staticConfig.ip) + .then(() => gTestSuite.checkDefaultRoute(ifname, staticConfig.gateway)); +} + +gTestSuite.doTest(function() { + return Promise.resolve() + .then(() => gTestSuite.checkInterfaceExist(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.addInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.enableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.updateInterfaceConfig(ETHERNET_INTERFACE_NAME, staticConfig)) + .then(() => gTestSuite.makeInterfaceConnect(ETHERNET_INTERFACE_NAME)) + .then(() => checkStaticResult(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceDisconnect(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.disableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.removeInterface(ETHERNET_INTERFACE_NAME)); +});
\ No newline at end of file diff --git a/dom/network/tests/marionette/test_ethernet_disable.js b/dom/network/tests/marionette/test_ethernet_disable.js new file mode 100644 index 0000000000..9c3525faa3 --- /dev/null +++ b/dom/network/tests/marionette/test_ethernet_disable.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = 'head.js'; + +const ETHERNET_INTERFACE_NAME = "eth1"; + +gTestSuite.doTest(function() { + return Promise.resolve() + .then(() => gTestSuite.checkInterfaceExist(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.addInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.enableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.disableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.checkInterfaceIsEnabled(ETHERNET_INTERFACE_NAME, false)) + .then(() => gTestSuite.removeInterface(ETHERNET_INTERFACE_NAME)); +});
\ No newline at end of file diff --git a/dom/network/tests/marionette/test_ethernet_disconnect.js b/dom/network/tests/marionette/test_ethernet_disconnect.js new file mode 100644 index 0000000000..73f6aa3c5e --- /dev/null +++ b/dom/network/tests/marionette/test_ethernet_disconnect.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = 'head.js'; + +const ETHERNET_INTERFACE_NAME = "eth1"; +const INTERFACE_IP_NONE = "0.0.0.0"; + +function checkIpAddrIsReset(ifname) { + return gTestSuite.checkInterfaceIpAddr(ifname, INTERFACE_IP_NONE) + .then(() => gTestSuite.checkDefaultRoute(ifname)); +} + +gTestSuite.doTest(function() { + return Promise.resolve() + .then(() => gTestSuite.checkInterfaceExist(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.addInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.enableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceConnect(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceDisconnect(ETHERNET_INTERFACE_NAME)) + .then(() => checkIpAddrIsReset(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.disableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.removeInterface(ETHERNET_INTERFACE_NAME)); +});
\ No newline at end of file diff --git a/dom/network/tests/marionette/test_ethernet_enable.js b/dom/network/tests/marionette/test_ethernet_enable.js new file mode 100644 index 0000000000..f5578a44f8 --- /dev/null +++ b/dom/network/tests/marionette/test_ethernet_enable.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = 'head.js'; + +const ETHERNET_INTERFACE_NAME = "eth1"; + +gTestSuite.doTest(function() { + return Promise.resolve() + .then(() => gTestSuite.checkInterfaceExist(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.addInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.enableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.checkInterfaceIsEnabled(ETHERNET_INTERFACE_NAME, true)) + .then(() => gTestSuite.disableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.removeInterface(ETHERNET_INTERFACE_NAME)); +});
\ No newline at end of file diff --git a/dom/network/tests/marionette/test_ethernet_ip_mode_change.js b/dom/network/tests/marionette/test_ethernet_ip_mode_change.js new file mode 100644 index 0000000000..5db2049bef --- /dev/null +++ b/dom/network/tests/marionette/test_ethernet_ip_mode_change.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = 'head.js'; + +const ETHERNET_INTERFACE_NAME = "eth1"; + +let staticConfig = { + ip: "1.2.3.4", + gateway: "1.2.3.5", + prefixLength: 24, + dnses: ["1.2.3.6"], + ipMode: "static" +}; + +function checkStaticResult(ifname) { + return gTestSuite.checkInterfaceIpAddr(ifname, staticConfig.ip) + .then(() => gTestSuite.waitForDefaultRouteSet(ifname, staticConfig.gateway)); +} + +function checkDhcpResult(ifname) { + return gTestSuite.getDhcpIpAddr(ifname) + .then(ip => gTestSuite.checkInterfaceIpAddr(ifname, ip)) + .then(() => gTestSuite.getDhcpGateway(ifname)) + .then(gateway => gTestSuite.waitForDefaultRouteSet(ifname, gateway)); +} + +gTestSuite.doTest(function() { + return Promise.resolve() + .then(() => gTestSuite.checkInterfaceExist(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.addInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.enableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceConnect(ETHERNET_INTERFACE_NAME)) + .then(() => checkDhcpResult(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.updateInterfaceConfig(ETHERNET_INTERFACE_NAME, staticConfig)) + .then(() => checkStaticResult(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.updateInterfaceConfig(ETHERNET_INTERFACE_NAME, { ipMode: "dhcp"})) + .then(() => checkDhcpResult(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceDisconnect(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.disableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.removeInterface(ETHERNET_INTERFACE_NAME)); +});
\ No newline at end of file diff --git a/dom/network/tests/marionette/test_ethernet_reconnect_with_dhcp.js b/dom/network/tests/marionette/test_ethernet_reconnect_with_dhcp.js new file mode 100644 index 0000000000..96719c1526 --- /dev/null +++ b/dom/network/tests/marionette/test_ethernet_reconnect_with_dhcp.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = 'head.js'; + +const ETHERNET_INTERFACE_NAME = "eth1"; + +function checkDhcpResult(ifname) { + return gTestSuite.getDhcpIpAddr(ifname) + .then(ip => gTestSuite.checkInterfaceIpAddr(ifname, ip)) + .then(() => gTestSuite.getDhcpGateway(ifname)) + .then(gateway => gTestSuite.waitForDefaultRouteSet(ifname, gateway)); +} + +gTestSuite.doTest(function() { + return Promise.resolve() + .then(() => gTestSuite.checkInterfaceExist(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.addInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.enableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceConnect(ETHERNET_INTERFACE_NAME)) + .then(() => checkDhcpResult(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceDisconnect(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceConnect(ETHERNET_INTERFACE_NAME)) + .then(() => checkDhcpResult(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceDisconnect(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.disableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.removeInterface(ETHERNET_INTERFACE_NAME)); +}); diff --git a/dom/network/tests/marionette/test_ethernet_reconnect_with_static_ip.js b/dom/network/tests/marionette/test_ethernet_reconnect_with_static_ip.js new file mode 100644 index 0000000000..91f25a4710 --- /dev/null +++ b/dom/network/tests/marionette/test_ethernet_reconnect_with_static_ip.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = 'head.js'; + +const ETHERNET_INTERFACE_NAME = "eth1"; + +let staticConfig = { + ip: "1.2.3.4", + gateway: "1.2.3.5", + prefixLength: 24, + dnses: ["1.2.3.6"], + ipMode: "static" +}; + +function checkStaticResult(ifname) { + return gTestSuite.checkInterfaceIpAddr(ifname, staticConfig.ip) + .then(() => gTestSuite.checkDefaultRoute(ifname, staticConfig.gateway)); +} + +gTestSuite.doTest(function() { + return Promise.resolve() + .then(() => gTestSuite.checkInterfaceExist(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.addInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.enableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.updateInterfaceConfig(ETHERNET_INTERFACE_NAME, staticConfig)) + .then(() => gTestSuite.makeInterfaceConnect(ETHERNET_INTERFACE_NAME)) + .then(() => checkStaticResult(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceDisconnect(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceConnect(ETHERNET_INTERFACE_NAME)) + .then(() => checkStaticResult(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.makeInterfaceDisconnect(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.disableInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.removeInterface(ETHERNET_INTERFACE_NAME)); +});
\ No newline at end of file diff --git a/dom/network/tests/marionette/test_ethernet_remove_interface.js b/dom/network/tests/marionette/test_ethernet_remove_interface.js new file mode 100644 index 0000000000..c7fb0e81b8 --- /dev/null +++ b/dom/network/tests/marionette/test_ethernet_remove_interface.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = 'head.js'; + +const ETHERNET_INTERFACE_NAME = "eth1"; + +gTestSuite.doTest(function() { + return Promise.resolve() + .then(() => gTestSuite.checkInterfaceExist(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.addInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.checkInterfaceListLength(1)) + .then(() => gTestSuite.removeInterface(ETHERNET_INTERFACE_NAME)) + .then(() => gTestSuite.checkInterfaceListLength(0)); +});
\ No newline at end of file diff --git a/dom/network/tests/mochitest.ini b/dom/network/tests/mochitest.ini new file mode 100644 index 0000000000..76fd55fe41 --- /dev/null +++ b/dom/network/tests/mochitest.ini @@ -0,0 +1,8 @@ +[DEFAULT] +support-files = + add_task.js + +[test_network_basics.html] +skip-if = toolkit == 'android' +[test_tcpsocket_default_permissions.html] +[test_tcpsocket_enabled_no_perm.html] diff --git a/dom/network/tests/tcpsocket_test.jsm b/dom/network/tests/tcpsocket_test.jsm new file mode 100644 index 0000000000..837a522625 --- /dev/null +++ b/dom/network/tests/tcpsocket_test.jsm @@ -0,0 +1,20 @@ +this.EXPORTED_SYMBOLS = [ + 'createSocket', 'createServer', 'enablePrefsAndPermissions', + 'socketCompartmentInstanceOfArrayBuffer']; + +this.createSocket = function(host, port, options) { + return new TCPSocket(host, port, options); +} + +this.createServer = function(port, options, backlog) { + return new TCPServerSocket(port, options, backlog); +} + +this.enablePrefsAndPermissions = function() { + return false; +} + +// See test_tcpsocket_client_and_server_basics.html's version for rationale. +this.socketCompartmentInstanceOfArrayBuffer = function(obj) { + return obj instanceof ArrayBuffer; +} diff --git a/dom/network/tests/test_network_basics.html b/dom/network/tests/test_network_basics.html new file mode 100644 index 0000000000..e3c3eb25d2 --- /dev/null +++ b/dom/network/tests/test_network_basics.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Network API</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Network Information API **/ +function test() { + ok('connection' in navigator, "navigator.connection should exist"); + + ok(navigator.connection, "navigator.connection returns an object"); + + ok(navigator.connection instanceof EventTarget, + "navigator.connection is a EventTarget object"); + + ok('type' in navigator.connection, + "type should be a Connection attribute"); + is(navigator.connection.type, "none", + "By default connection.type equals to none"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({'set': [["dom.netinfo.enabled", true]]}, test); + +</script> +</pre> +</body> +</html> diff --git a/dom/network/tests/test_tcpsocket_client_and_server_basics.html b/dom/network/tests/test_tcpsocket_client_and_server_basics.html new file mode 100644 index 0000000000..4d304a978d --- /dev/null +++ b/dom/network/tests/test_tcpsocket_client_and_server_basics.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +Core tests for TCPSocket and TCPServerSocket that replace their previous +separate xpcshell incarnations. This migration and cleanup occurred as part +of bug 1084245 in order to get coverage of the tests from content. + +https://bugzilla.mozilla.org/show_bug.cgi?id=1084245 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1084245</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="add_task.js"></script> + <script type="application/javascript"> + function createServer(port, options, backlog) { + return new TCPServerSocket(port, options, backlog); + } + + function createSocket(host, port, options) { + return new TCPSocket(host, port, options); + } + + function enablePrefsAndPermissions() { + return true; + } + + // In the JSM case, ArrayBuffers will be created in the compartment of the + // JSM with different globals than the + // test_tcpsocket_client_and_server_basics.js test logic sees, so we (and + // tcpsocket_test.jsm) need to do something. To avoid complexity relating + // to wrappers and the varying nuances of the module scope and global scope + // in JSM's (they differ on B2G), we hardcode ArrayBuffer rather than taking + // a string that we look up, etc. + function socketCompartmentInstanceOfArrayBuffer(obj) { + return obj instanceof ArrayBuffer; + } + </script> + <script type="application/javascript;version=1.7" src="test_tcpsocket_client_and_server_basics.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1084245">Mozilla Bug 1084245</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/network/tests/test_tcpsocket_client_and_server_basics.js b/dom/network/tests/test_tcpsocket_client_and_server_basics.js new file mode 100644 index 0000000000..f47689b903 --- /dev/null +++ b/dom/network/tests/test_tcpsocket_client_and_server_basics.js @@ -0,0 +1,423 @@ +'use strict'; + +const SERVER_BACKLOG = -1; + +const SOCKET_EVENTS = ['open', 'data', 'drain', 'error', 'close']; + +function concatUint8Arrays(a, b) { + let newArr = new Uint8Array(a.length + b.length); + newArr.set(a, 0); + newArr.set(b, a.length); + return newArr; +} + +function assertUint8ArraysEqual(a, b, comparingWhat) { + if (a.length !== b.length) { + ok(false, comparingWhat + ' arrays do not have the same length; ' + + a.length + ' versus ' + b.length); + return; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + ok(false, comparingWhat + ' arrays differ at index ' + i + + a[i] + ' versus ' + b[i]); + return; + } + } + ok(true, comparingWhat + ' arrays were equivalent.'); +} + +/** + * Helper method to add event listeners to a socket and provide two Promise-returning + * helpers (see below for docs on them). This *must* be called during the turn of + * the event loop where TCPSocket's constructor is called or the onconnect method is being + * invoked. + */ +function listenForEventsOnSocket(socket, socketType) { + let wantDataLength = null; + let wantDataAndClose = false; + let pendingResolve = null; + let receivedEvents = []; + let receivedData = null; + let handleGenericEvent = function(event) { + dump('(' + socketType + ' event: ' + event.type + ')\n'); + if (pendingResolve && wantDataLength === null) { + pendingResolve(event); + pendingResolve = null; + } else { + receivedEvents.push(event); + } + }; + + socket.onopen = handleGenericEvent; + socket.ondrain = handleGenericEvent; + socket.onerror = handleGenericEvent; + socket.onclose = function(event) { + if (!wantDataAndClose) { + handleGenericEvent(event); + } else if (pendingResolve) { + dump('(' + socketType + ' event: close)\n'); + pendingResolve(receivedData); + pendingResolve = null; + wantDataAndClose = false; + } + } + socket.ondata = function(event) { + dump('(' + socketType + ' event: ' + event.type + ' length: ' + + event.data.byteLength + ')\n'); + ok(socketCompartmentInstanceOfArrayBuffer(event.data), + 'payload is ArrayBuffer'); + var arr = new Uint8Array(event.data); + if (receivedData === null) { + receivedData = arr; + } else { + receivedData = concatUint8Arrays(receivedData, arr); + } + if (wantDataLength !== null && + receivedData.length >= wantDataLength) { + pendingResolve(receivedData); + pendingResolve = null; + receivedData = null; + wantDataLength = null; + } + }; + + + return { + /** + * Return a Promise that will be resolved with the next (non-data) event + * received by the socket. If there are queued events, the Promise will + * be immediately resolved (but you won't see that until a future turn of + * the event loop). + */ + waitForEvent: function() { + if (pendingResolve) { + throw new Error('only one wait allowed at a time.'); + } + + if (receivedEvents.length) { + return Promise.resolve(receivedEvents.shift()); + } + + dump('(' + socketType + ' waiting for event)\n'); + return new Promise(function(resolve, reject) { + pendingResolve = resolve; + }); + }, + /** + * Return a Promise that will be resolved with a Uint8Array of at least the + * given length. We buffer / accumulate received data until we have enough + * data. Data is buffered even before you call this method, so be sure to + * explicitly wait for any and all data sent by the other side. + */ + waitForDataWithAtLeastLength: function(length) { + if (pendingResolve) { + throw new Error('only one wait allowed at a time.'); + } + if (receivedData && receivedData.length >= length) { + let promise = Promise.resolve(receivedData); + receivedData = null; + return promise; + } + dump('(' + socketType + ' waiting for ' + length + ' bytes)\n'); + return new Promise(function(resolve, reject) { + pendingResolve = resolve; + wantDataLength = length; + }); + }, + waitForAnyDataAndClose: function() { + if (pendingResolve) { + throw new Error('only one wait allowed at a time.'); + } + + return new Promise(function(resolve, reject) { + pendingResolve = resolve; + // we may receive no data before getting close, in which case we want to + // return an empty array + receivedData = new Uint8Array(); + wantDataAndClose = true; + }); + } + }; +} + +/** + * Return a promise that is resolved when the server receives a connection. The + * promise is resolved with { socket, queue } where `queue` is the result of + * calling listenForEventsOnSocket(socket). This must be done because we need + * to add the event listener during the connection. + */ +function waitForConnection(listeningServer) { + return new Promise(function(resolve, reject) { + // Because of the event model of sockets, we can't use the + // listenForEventsOnSocket mechanism; we need to hook up listeners during + // the connect event. + listeningServer.onconnect = function(event) { + // Clobber the listener to get upset if it receives any more connections + // after this. + listeningServer.onconnect = function() { + ok(false, 'Received a connection when not expecting one.'); + }; + ok(true, 'Listening server accepted socket'); + resolve({ + socket: event.socket, + queue: listenForEventsOnSocket(event.socket, 'server') + }); + }; + }); +} + +function defer() { + var deferred = {}; + deferred.promise = new Promise(function(resolve, reject) { + deferred.resolve = resolve; + deferred.reject = reject; + }); + return deferred; +} + + +function* test_basics() { + // See bug 903830; in e10s mode we never get to find out the localPort if we + // let it pick a free port by choosing 0. This is the same port the xpcshell + // test was using. + let serverPort = 8085; + + // - Start up a listening socket. + let listeningServer = createServer(serverPort, + { binaryType: 'arraybuffer' }, + SERVER_BACKLOG); + + let connectedPromise = waitForConnection(listeningServer); + + // -- Open a connection to the server + let clientSocket = createSocket('127.0.0.1', serverPort, + { binaryType: 'arraybuffer' }); + let clientQueue = listenForEventsOnSocket(clientSocket, 'client'); + + // (the client connects) + is((yield clientQueue.waitForEvent()).type, 'open', 'got open event'); + is(clientSocket.readyState, 'open', 'client readyState is open'); + + // (the server connected) + let { socket: serverSocket, queue: serverQueue } = yield connectedPromise; + is(serverSocket.readyState, 'open', 'server readyState is open'); + + // -- Simple send / receive + // - Send data from client to server + // (But not so much we cross the drain threshold.) + let smallUint8Array = new Uint8Array(256); + for (let i = 0; i < smallUint8Array.length; i++) { + smallUint8Array[i] = i; + } + is(clientSocket.send(smallUint8Array.buffer, 0, smallUint8Array.length), true, + 'Client sending less than 64k, buffer should not be full.'); + + let serverReceived = yield serverQueue.waitForDataWithAtLeastLength(256); + assertUint8ArraysEqual(serverReceived, smallUint8Array, + 'Server received/client sent'); + + // - Send data from server to client + // (But not so much we cross the drain threshold.) + is(serverSocket.send(smallUint8Array.buffer, 0, smallUint8Array.length), true, + 'Server sending less than 64k, buffer should not be full.'); + + let clientReceived = yield clientQueue.waitForDataWithAtLeastLength(256); + assertUint8ArraysEqual(clientReceived, smallUint8Array, + 'Client received/server sent'); + + // -- Perform sending multiple times with different buffer slices + // - Send data from client to server + // (But not so much we cross the drain threshold.) + is(clientSocket.send(smallUint8Array.buffer, 0, 7), + true, 'Client sending less than 64k, buffer should not be full.'); + is(clientSocket.send(smallUint8Array.buffer, 7, smallUint8Array.length - 7), + true, 'Client sending less than 64k, buffer should not be full.'); + + serverReceived = yield serverQueue.waitForDataWithAtLeastLength(256); + assertUint8ArraysEqual(serverReceived, smallUint8Array, + 'Server received/client sent'); + + // - Send data from server to client + // (But not so much we cross the drain threshold.) + is(serverSocket.send(smallUint8Array.buffer, 0, 7), + true, 'Server sending less than 64k, buffer should not be full.'); + is(serverSocket.send(smallUint8Array.buffer, 7, smallUint8Array.length - 7), + true, 'Server sending less than 64k, buffer should not be full.'); + + clientReceived = yield clientQueue.waitForDataWithAtLeastLength(256); + assertUint8ArraysEqual(clientReceived, smallUint8Array, + 'Client received/server sent'); + + + // -- Send "big" data in both directions + // (Enough to cross the buffering/drain threshold; 64KiB) + let bigUint8Array = new Uint8Array(65536 + 3); + for (let i = 0; i < bigUint8Array.length; i++) { + bigUint8Array[i] = i % 256; + } + // Do this twice so we have confidence that the 'drain' event machinery + // doesn't break after the first use. + for (let iSend = 0; iSend < 2; iSend++) { + // - Send "big" data from the client to the server + is(clientSocket.send(bigUint8Array.buffer, 0, bigUint8Array.length), false, + 'Client sending more than 64k should result in the buffer being full.'); + is((yield clientQueue.waitForEvent()).type, 'drain', + 'The drain event should fire after a large send that indicated full.'); + + serverReceived = yield serverQueue.waitForDataWithAtLeastLength( + bigUint8Array.length); + assertUint8ArraysEqual(serverReceived, bigUint8Array, + 'server received/client sent'); + + // - Send "big" data from the server to the client + is(serverSocket.send(bigUint8Array.buffer, 0, bigUint8Array.length), false, + 'Server sending more than 64k should result in the buffer being full.'); + is((yield serverQueue.waitForEvent()).type, 'drain', + 'The drain event should fire after a large send that indicated full.'); + + clientReceived = yield clientQueue.waitForDataWithAtLeastLength( + bigUint8Array.length); + assertUint8ArraysEqual(clientReceived, bigUint8Array, + 'client received/server sent'); + } + + // -- Server closes the connection + serverSocket.close(); + is(serverSocket.readyState, 'closing', + 'readyState should be closing immediately after calling close'); + + is((yield clientQueue.waitForEvent()).type, 'close', + 'The client should get a close event when the server closes.'); + is(clientSocket.readyState, 'closed', + 'client readyState should be closed after close event'); + is((yield serverQueue.waitForEvent()).type, 'close', + 'The server should get a close event when it closes itself.'); + is(serverSocket.readyState, 'closed', + 'server readyState should be closed after close event'); + + // -- Re-establish connection + connectedPromise = waitForConnection(listeningServer); + clientSocket = createSocket('127.0.0.1', serverPort, + { binaryType: 'arraybuffer' }); + clientQueue = listenForEventsOnSocket(clientSocket, 'client'); + is((yield clientQueue.waitForEvent()).type, 'open', 'got open event'); + + let connectedResult = yield connectedPromise; + // destructuring assignment is not yet ES6 compliant, must manually unpack + serverSocket = connectedResult.socket; + serverQueue = connectedResult.queue; + + // -- Client closes the connection + clientSocket.close(); + is(clientSocket.readyState, 'closing', + 'client readyState should be losing immediately after calling close'); + + is((yield clientQueue.waitForEvent()).type, 'close', + 'The client should get a close event when it closes itself.'); + is(clientSocket.readyState, 'closed', + 'client readyState should be closed after the close event is received'); + is((yield serverQueue.waitForEvent()).type, 'close', + 'The server should get a close event when the client closes.'); + is(serverSocket.readyState, 'closed', + 'server readyState should be closed after the close event is received'); + + + // -- Re-establish connection + connectedPromise = waitForConnection(listeningServer); + clientSocket = createSocket('127.0.0.1', serverPort, + { binaryType: 'arraybuffer' }); + clientQueue = listenForEventsOnSocket(clientSocket, 'client'); + is((yield clientQueue.waitForEvent()).type, 'open', 'got open event'); + + connectedResult = yield connectedPromise; + // destructuring assignment is not yet ES6 compliant, must manually unpack + serverSocket = connectedResult.socket; + serverQueue = connectedResult.queue; + + // -- Call close after enqueueing a lot of data, make sure it goes through. + // We'll have the client send and close. + is(clientSocket.send(bigUint8Array.buffer, 0, bigUint8Array.length), false, + 'Client sending more than 64k should result in the buffer being full.'); + clientSocket.close(); + // The drain will still fire + is((yield clientQueue.waitForEvent()).type, 'drain', + 'The drain event should fire after a large send that returned true.'); + // Then we'll get a close + is((yield clientQueue.waitForEvent()).type, 'close', + 'The close event should fire after the drain event.'); + + // The server will get its data + serverReceived = yield serverQueue.waitForDataWithAtLeastLength( + bigUint8Array.length); + assertUint8ArraysEqual(serverReceived, bigUint8Array, + 'server received/client sent'); + // And a close. + is((yield serverQueue.waitForEvent()).type, 'close', + 'The drain event should fire after a large send that returned true.'); + + + // -- Re-establish connection + connectedPromise = waitForConnection(listeningServer); + clientSocket = createSocket('127.0.0.1', serverPort, + { binaryType: 'string' }); + clientQueue = listenForEventsOnSocket(clientSocket, 'client'); + is((yield clientQueue.waitForEvent()).type, 'open', 'got open event'); + + connectedResult = yield connectedPromise; + // destructuring assignment is not yet ES6 compliant, must manually unpack + serverSocket = connectedResult.socket; + serverQueue = connectedResult.queue; + + // -- Attempt to send non-string data. + // Restore the original behavior by replacing toString with + // Object.prototype.toString. (bug 1121938) + bigUint8Array.toString = Object.prototype.toString; + is(clientSocket.send(bigUint8Array), true, + 'Client sending a large non-string should only send a small string.'); + clientSocket.close(); + // The server will get its data + serverReceived = yield serverQueue.waitForDataWithAtLeastLength( + bigUint8Array.toString().length); + // Then we'll get a close + is((yield clientQueue.waitForEvent()).type, 'close', + 'The close event should fire after the drain event.'); + + // -- Re-establish connection (Test for Close Immediately) + connectedPromise = waitForConnection(listeningServer); + clientSocket = createSocket('127.0.0.1', serverPort, + { binaryType: 'arraybuffer' }); + clientQueue = listenForEventsOnSocket(clientSocket, 'client'); + is((yield clientQueue.waitForEvent()).type, 'open', 'got open event'); + + connectedResult = yield connectedPromise; + // destructuring assignment is not yet ES6 compliant, must manually unpack + serverSocket = connectedResult.socket; + serverQueue = connectedResult.queue; + + // -- Attempt to send two non-string data. + is(clientSocket.send(bigUint8Array.buffer, 0, bigUint8Array.length), false, + 'Server sending more than 64k should result in the buffer being full.'); + is(clientSocket.send(bigUint8Array.buffer, 0, bigUint8Array.length), false, + 'Server sending more than 64k should result in the buffer being full.'); + clientSocket.closeImmediately(); + + serverReceived = yield serverQueue.waitForAnyDataAndClose(); + + is(serverReceived.length < (2 * bigUint8Array.length), true, 'Received array length less than sent array length'); + + // -- Close the listening server (and try to connect) + // We want to verify that the server actually closes / stops listening when + // we tell it to. + listeningServer.close(); + + // - try and connect, get an error + clientSocket = createSocket('127.0.0.1', serverPort, + { binaryType: 'arraybuffer' }); + clientQueue = listenForEventsOnSocket(clientSocket, 'client'); + is((yield clientQueue.waitForEvent()).type, 'error', 'fail to connect'); + is(clientSocket.readyState, 'closed', + 'client readyState should be closed after the failure to connect'); +} + +add_task(test_basics); diff --git a/dom/network/tests/test_tcpsocket_default_permissions.html b/dom/network/tests/test_tcpsocket_default_permissions.html new file mode 100644 index 0000000000..19c44b7b9b --- /dev/null +++ b/dom/network/tests/test_tcpsocket_default_permissions.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test to ensure TCPSocket permission is disabled by default</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test to ensure TCPSocket permission is disabled by default **/ + +var caught = false; +try { + new TCPSocket("localhost", 80, {}) +} catch (e) { + caught = true; +} + +ok(caught, "TCPSocket should not exist by default"); + +var caught = false; +try { + navigator.mozTCPSocket.open("localhost", 80, {}) +} catch (e) { + caught = true; +} + +ok(caught, "navigator.mozTCPSocket.open should not exist by default"); + +</script> +</pre> +</body> +</html> diff --git a/dom/network/tests/test_tcpsocket_enabled_no_perm.html b/dom/network/tests/test_tcpsocket_enabled_no_perm.html new file mode 100644 index 0000000000..ae1313113f --- /dev/null +++ b/dom/network/tests/test_tcpsocket_enabled_no_perm.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test to ensure TCPSocket permission enabled and no tcp-socket perm does not allow open</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test to ensure TCPSocket preference being turned on does not enable + navigator.mozTCPSocket. +**/ +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [['dom.mozTCPSocket.enabled', true]]}, runTest); +function runTest() { + is('TCPSocket' in this, false, "TCPSocket should not be accessible if dom.mozTCPSocket.enabled is true"); + is('TCPServerSocket' in this, false, "TCPServerSocket should not be accessible if dom.mozTCPSocket.enabled is true"); + is('mozTCPSocket' in navigator, false, "mozTCPSocket should not be accessible if dom.mozTCPSocket.enabled is true"); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/dom/network/tests/test_tcpsocket_enabled_with_perm.html b/dom/network/tests/test_tcpsocket_enabled_with_perm.html new file mode 100644 index 0000000000..44b85ee3e8 --- /dev/null +++ b/dom/network/tests/test_tcpsocket_enabled_with_perm.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test to ensure TCPSocket permission enabled and open works with tcp-socket perm</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test to ensure TCPSocket permission being turned on enables + navigator.mozTCPSocket, and mozTCPSocket.open works when + the tcp-socket permission has been granted. +**/ +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [['dom.mozTCPSocket.enabled', true]]}, runTest); + +function runTest() { + ok('TCPSocket' in this, "TCPSocket should be accessible if dom.mozTCPSocket.enabled is true"); + + ok(new TCPSocket('localhost', 80), "TCPSocket constructor should work for content that has the tcp-socket permission"); + ok(navigator.mozTCPSocket.open('localhost', 80), "navigator.mozTCPSocket.open should work for content that has the tcp-socket permission"); + // This just helps the test harness clean up quickly + SpecialPowers.forceCC(); + SpecialPowers.forceGC(); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/dom/network/tests/test_tcpsocket_jsm.html b/dom/network/tests/test_tcpsocket_jsm.html new file mode 100644 index 0000000000..508c3c9569 --- /dev/null +++ b/dom/network/tests/test_tcpsocket_jsm.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <meta charset="utf-8"> + <title>Test for 1207090</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + Components.utils.import("chrome://mochitests/content/chrome/dom/network/tests/tcpsocket_test.jsm"); + </script> + <script type="application/javascript" src="add_task.js"></script> + <script type="application/javascript;version=1.7" src="test_tcpsocket_client_and_server_basics.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1207090">Mozilla Bug 1207090</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<div id="container"></div> +</body> +</html> diff --git a/dom/network/tests/test_tcpsocket_legacy.html b/dom/network/tests/test_tcpsocket_legacy.html new file mode 100644 index 0000000000..3b35583ab3 --- /dev/null +++ b/dom/network/tests/test_tcpsocket_legacy.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test of legacy navigator interface for opening TCPSocket/TCPServerSocket. +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 885982</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1084245">Mozilla Bug 1084245</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script> + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv( + { set: [ ['dom.mozTCPSocket.enabled', true] ] }, + runTest); + + function runTest() { + // See bug 903830; in e10s mode we never get to find out the localPort if we + // let it pick a free port by choosing 0. This is the same port the xpcshell + // test was using. + var serverPort = 8085; + + var listeningServer = navigator.mozTCPSocket.listen(serverPort, + { binaryType: 'arraybuffer' }, + -1); + listeningServer.onconnect = function(ev) { + ok(true, "got server connect"); + listeningServer.close(); + listeningServer = null; + ev.socket.close() + } + + var clientSocket = navigator.mozTCPSocket.open('127.0.0.1', serverPort, + { binaryType: 'arraybuffer' }); + clientSocket.onopen = function() { ok(true, "got client open"); } + clientSocket.onclose = function() { + ok(true, "got client close"); + clientSocket.close(); + clientSocket = null; + setTimeout(function() { + // This just helps the test harness clean up quickly + SpecialPowers.forceCC(); + SpecialPowers.forceGC(); + SimpleTest.finish(); + }, 0); + } + } +</script> +</body> +</html> diff --git a/dom/network/tests/test_udpsocket.html b/dom/network/tests/test_udpsocket.html new file mode 100644 index 0000000000..f1d03824a6 --- /dev/null +++ b/dom/network/tests/test_udpsocket.html @@ -0,0 +1,405 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test UDPSocket API</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<iframe id="iframe"></iframe> +<pre id="test"> +<script type="application/javascript;version=1.8"> +'use strict'; +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +const HELLO_WORLD = 'hlo wrld. '; +const DATA_ARRAY = [0, 255, 254, 0, 1, 2, 3, 0, 255, 255, 254, 0]; +const DATA_ARRAY_BUFFER = new ArrayBuffer(DATA_ARRAY.length); +const TYPED_DATA_ARRAY = new Uint8Array(DATA_ARRAY_BUFFER); +const BIG_ARRAY = new Array(4096); +const BIG_ARRAY_BUFFER = new ArrayBuffer(BIG_ARRAY.length); +const BIG_TYPED_ARRAY = new Uint8Array(BIG_ARRAY_BUFFER); + +for (let i = 0; i < BIG_ARRAY.length; i++) { + BIG_ARRAY[i] = Math.floor(Math.random() * 256); +} + +TYPED_DATA_ARRAY.set(DATA_ARRAY); +BIG_TYPED_ARRAY.set(BIG_ARRAY); + +function is_same_buffer(recv_data, expect_data) { + let recv_dataview = new Uint8Array(recv_data); + let expected_dataview = new Uint8Array(expect_data); + + if (recv_dataview.length !== expected_dataview.length) { + return false; + } + + for (let i = 0; i < recv_dataview.length; i++) { + if (recv_dataview[i] != expected_dataview[i]) { + info('discover byte differenct at ' + i); + return false; + } + } + return true; +} + +function testOpen() { + info('test for creating an UDP Socket'); + let socket = new UDPSocket(); + is(socket.localPort, null, 'expect no local port before socket opened'); + is(socket.localAddress, null, 'expect no local address before socket opened'); + is(socket.remotePort, null, 'expected no default remote port'); + is(socket.remoteAddress, null, 'expected no default remote address'); + is(socket.readyState, 'opening', 'expected ready state = opening'); + is(socket.loopback, false, 'expected no loopback'); + is(socket.addressReuse, true, 'expect to reuse address'); + + return socket.opened.then(function() { + ok(true, 'expect openedPromise to be resolved after successful socket binding'); + ok(!(socket.localPort === 0), 'expect allocated a local port'); + is(socket.localAddress, '0.0.0.0', 'expect assigned to default address'); + is(socket.readyState, 'open', 'expected ready state = open'); + + return socket; + }); +} + +function testSendString(socket) { + info('test for sending string data'); + + socket.send(HELLO_WORLD, '127.0.0.1', socket.localPort); + + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function recv_callback(msg) { + socket.removeEventListener('message', recv_callback); + let recvData= String.fromCharCode.apply(null, new Uint8Array(msg.data)); + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + is(recvData, HELLO_WORLD, 'expected same string data'); + resolve(socket); + }); + }); +} + +function testSendArrayBuffer(socket) { + info('test for sending ArrayBuffer'); + + socket.send(DATA_ARRAY_BUFFER, '127.0.0.1', socket.localPort); + + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function recv_callback(msg) { + socket.removeEventListener('message', recv_callback); + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + ok(is_same_buffer(msg.data, DATA_ARRAY_BUFFER), 'expected same buffer data'); + resolve(socket); + }); + }); +} + +function testSendArrayBufferView(socket) { + info('test for sending ArrayBufferView'); + + socket.send(TYPED_DATA_ARRAY, '127.0.0.1', socket.localPort); + + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function recv_callback(msg) { + socket.removeEventListener('message', recv_callback); + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + ok(is_same_buffer(msg.data, TYPED_DATA_ARRAY), 'expected same buffer data'); + resolve(socket); + }); + }); +} + +function testSendBlob(socket) { + info('test for sending Blob'); + + let blob = new Blob([HELLO_WORLD], {type : 'text/plain'}); + socket.send(blob, '127.0.0.1', socket.localPort); + + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function recv_callback(msg) { + socket.removeEventListener('message', recv_callback); + let recvData= String.fromCharCode.apply(null, new Uint8Array(msg.data)); + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + is(recvData, HELLO_WORLD, 'expected same string data'); + resolve(socket); + }); + }); +} + +function testSendBigArray(socket) { + info('test for sending Big ArrayBuffer'); + + socket.send(BIG_TYPED_ARRAY, '127.0.0.1', socket.localPort); + + return new Promise(function(resolve, reject) { + let byteReceived = 0; + socket.addEventListener('message', function recv_callback(msg) { + let byteBegin = byteReceived; + byteReceived += msg.data.byteLength; + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + ok(is_same_buffer(msg.data, BIG_TYPED_ARRAY.subarray(byteBegin, byteReceived)), 'expected same buffer data [' + byteBegin+ '-' + byteReceived + ']'); + if (byteReceived >= BIG_TYPED_ARRAY.length) { + socket.removeEventListener('message', recv_callback); + resolve(socket); + } + }); + }); +} + +function testSendBigBlob(socket) { + info('test for sending Big Blob'); + + let blob = new Blob([BIG_TYPED_ARRAY]); + socket.send(blob, '127.0.0.1', socket.localPort); + + return new Promise(function(resolve, reject) { + let byteReceived = 0; + socket.addEventListener('message', function recv_callback(msg) { + let byteBegin = byteReceived; + byteReceived += msg.data.byteLength; + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + ok(is_same_buffer(msg.data, BIG_TYPED_ARRAY.subarray(byteBegin, byteReceived)), 'expected same buffer data [' + byteBegin+ '-' + byteReceived + ']'); + if (byteReceived >= BIG_TYPED_ARRAY.length) { + socket.removeEventListener('message', recv_callback); + resolve(socket); + } + }); + }); +} + +function testUDPOptions(socket) { + info('test for UDP init options'); + + let remoteSocket = new UDPSocket({addressReuse: false, + loopback: true, + localAddress: '127.0.0.1', + remoteAddress: '127.0.0.1', + remotePort: socket.localPort}); + is(remoteSocket.localAddress, '127.0.0.1', 'expected local address'); + is(remoteSocket.remoteAddress, '127.0.0.1', 'expected remote address'); + is(remoteSocket.remotePort, socket.localPort, 'expected remote port'); + is(remoteSocket.addressReuse, false, 'expected address not reusable'); + is(remoteSocket.loopback, true, 'expected loopback mode is on'); + + return remoteSocket.opened.then(function() { + remoteSocket.send(HELLO_WORLD); + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function recv_callback(msg) { + socket.removeEventListener('message', recv_callback); + let recvData= String.fromCharCode.apply(null, new Uint8Array(msg.data)); + is(msg.remotePort, remoteSocket.localPort, 'expected packet from ' + remoteSocket.localPort); + is(recvData, HELLO_WORLD, 'expected same string data'); + resolve(socket); + }); + }); + }); +} + +function testClose(socket) { + info('test for close'); + + socket.close(); + is(socket.readyState, 'closed', 'expect ready state to be "closed"'); + try { + socket.send(HELLO_WORLD, '127.0.0.1', socket.localPort); + ok(false, 'unexpect to send successfully'); + } catch (e) { + ok(true, 'expected send fail after socket closed'); + } + + return socket.closed.then(function() { + ok(true, 'expected closedPromise is resolved after socket.close()'); + }); +} + +function testMulticast() { + info('test for multicast'); + + let socket = new UDPSocket({loopback: true}); + + const MCAST_ADDRESS = '224.0.0.255'; + socket.joinMulticastGroup(MCAST_ADDRESS); + + return socket.opened.then(function() { + socket.send(HELLO_WORLD, MCAST_ADDRESS, socket.localPort); + + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function recv_callback(msg) { + socket.removeEventListener('message', recv_callback); + let recvData= String.fromCharCode.apply(null, new Uint8Array(msg.data)); + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + is(recvData, HELLO_WORLD, 'expected same string data'); + socket.leaveMulticastGroup(MCAST_ADDRESS); + resolve(); + }); + }); + }); +} + +function testInvalidUDPOptions() { + info('test for invalid UDPOptions'); + try { + let socket = new UDPSocket({localAddress: 'not-a-valid-address'}); + ok(false, 'should not create an UDPSocket with an invalid localAddress'); + } catch (e) { + is(e.name, 'InvalidAccessError', 'expected InvalidAccessError will be thrown if localAddress is not a valid IPv4/6 address'); + } + + try { + let socket = new UDPSocket({localPort: 0}); + ok(false, 'should not create an UDPSocket with an invalid localPort'); + } catch (e) { + is(e.name, 'InvalidAccessError', 'expected InvalidAccessError will be thrown if localPort is not a valid port number'); + } + + try { + let socket = new UDPSocket({remotePort: 0}); + ok(false, 'should not create an UDPSocket with an invalid remotePort'); + } catch (e) { + is(e.name, 'InvalidAccessError', 'expected InvalidAccessError will be thrown if localPort is not a valid port number'); + } +} + +function testOpenFailed() { + info('test for falied on open'); + + //according to RFC5737, address block 192.0.2.0/24 should not be used in both local and public contexts + let socket = new UDPSocket({localAddress: '192.0.2.0'}); + + return socket.opened.then(function() { + ok(false, 'should not resolve openedPromise while fail to bind socket'); + socket.close(); + }).catch(function(reason) { + is(reason.name, 'NetworkError', 'expected openedPromise to be rejected while fail to bind socket'); + }); +} + +function testSendBeforeOpen() { + info('test for send before open'); + + let socket = new UDPSocket(); + + try { + socket.send(HELLO_WORLD, '127.0.0.1', 9); + ok(false, 'unexpect to send successfully'); + } catch (e) { + ok(true, 'expected send fail before openedPromise is resolved'); + } + + return socket.opened.then(function() { + socket.close(); + }); +} + +function testCloseBeforeOpened() { + info('test for close socket before opened'); + + let socket = new UDPSocket(); + socket.opened.then(function() { + ok(false, 'should not resolve openedPromise if it has already been closed'); + }).catch(function(reason) { + is(reason.name, 'AbortError', 'expected openedPromise to be rejected while socket is closed during opening'); + }); + + return socket.close().then(function() { + ok(true, 'expected closedPromise to be resolved'); + }).then(socket.opened); +} + +function testOpenWithoutClose() { + info('test for open without close'); + + let closed = []; + for (let i = 0; i < 50; i++) { + let socket = new UDPSocket(); + closed.push(socket.closed); + } + + SpecialPowers.gc(); + info('all unrefereced socket should be closed right after GC'); + + return Promise.all(closed); +} + +function testBFCache() { + info('test for bfcache behavior'); + + let socket = new UDPSocket(); + + return socket.opened.then(function() { + let iframe = document.getElementById('iframe'); + SpecialPowers.wrap(iframe).mozbrowser = true; + iframe.src = 'file_udpsocket_iframe.html?' + socket.localPort; + + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function recv_callback(msg) { + socket.removeEventListener('message', recv_callback); + iframe.src = 'about:blank'; + iframe.addEventListener('load', function onload() { + iframe.removeEventListener('load', onload); + socket.send(HELLO_WORLD, '127.0.0.1', msg.remotePort); + + function recv_again_callback(msg) { + socket.removeEventListener('message', recv_again_callback); + ok(false, 'should not receive packet after page unload'); + } + + socket.addEventListener('message', recv_again_callback); + + let timeout = setTimeout(function() { + socket.removeEventListener('message', recv_again_callback); + socket.close(); + resolve(); + }, 5000); + }); + }); + }); + }); +} + +function runTest() { + testOpen() + .then(testSendString) + .then(testSendArrayBuffer) + .then(testSendArrayBufferView) + .then(testSendBlob) + .then(testSendBigArray) + .then(testSendBigBlob) + .then(testUDPOptions) + .then(testClose) + .then(testMulticast) + .then(testInvalidUDPOptions) + .then(testOpenFailed) + .then(testSendBeforeOpen) + .then(testCloseBeforeOpened) + .then(testOpenWithoutClose) + .then(testBFCache) + .then(function() { + info('test finished'); + SimpleTest.finish(); + }) + .catch(function(err) { + ok(false, 'test failed due to: ' + err); + SimpleTest.finish(); + }); +} + +window.addEventListener('load', function () { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.udpsocket.enabled', true], + ['browser.sessionhistory.max_total_viewers', 10] + ] + }, runTest); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/network/tests/unit_stats/test_networkstats_db.js b/dom/network/tests/unit_stats/test_networkstats_db.js new file mode 100644 index 0000000000..50a5dc1867 --- /dev/null +++ b/dom/network/tests/unit_stats/test_networkstats_db.js @@ -0,0 +1,1093 @@ +/* Any: copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/NetworkStatsDB.jsm"); + +const STATS_STORE_NAME = "net_stats_store_v3"; +const netStatsDb = new NetworkStatsDB(); + +function clearStore(store, callback) { + netStatsDb.dbNewTxn(store, "readwrite", function(aTxn, aStore) { + aStore.openCursor().onsuccess = function (event) { + let cursor = event.target.result; + if (cursor){ + cursor.delete(); + cursor.continue(); + } + }; + }, callback); +} + +function getNetworkId(aIccId, aNetworkType) { + return aIccId + '' + aNetworkType; +} + +add_test(function prepareDatabase() { + // Clear whole database to avoid starting tests with unknown state + // due to the previous tests. + clearStore(STATS_STORE_NAME, function() { + clearStore('net_alarm', function() { + run_next_test(); + }); + }); +}); + +function filterTimestamp(date) { + var sampleRate = netStatsDb.sampleRate; + var offset = date.getTimezoneOffset() * 60 * 1000; + return Math.floor((date.getTime() - offset) / sampleRate) * sampleRate; +} + +function getNetworks() { + return [{ id: '0', type: Ci.nsINetworkInterface.NETWORK_TYPE_WIFI }, + { id: '1234', type: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE }]; +} + +function compareNetworks(networkA, networkB) { + return (networkA[0] == networkB[0] && networkA[1] == networkB[1]); +} + +add_test(function test_sampleRate() { + var sampleRate = netStatsDb.sampleRate; + do_check_true(sampleRate > 0); + netStatsDb.sampleRate = 0; + sampleRate = netStatsDb.sampleRate; + do_check_true(sampleRate > 0); + + run_next_test(); +}); + +add_test(function test_maxStorageSamples() { + var maxStorageSamples = netStatsDb.maxStorageSamples; + do_check_true(maxStorageSamples > 0); + netStatsDb.maxStorageSamples = 0; + maxStorageSamples = netStatsDb.maxStorageSamples; + do_check_true(maxStorageSamples > 0); + + run_next_test(); +}); + +add_test(function test_fillResultSamples_emptyData() { + var samples = 3; + var data = []; + var start = filterTimestamp(new Date()); + var sampleRate = netStatsDb.sampleRate; + var end = start + (sampleRate * samples); + netStatsDb.fillResultSamples(start, end, data); + do_check_eq(data.length, samples + 1); + + var aux = start; + var success = true; + for (var i = 0; i <= samples; i++) { + if (data[i].date.getTime() != aux || data[i].rxBytes != undefined || data[i].txBytes != undefined) { + success = false; + break; + } + aux += sampleRate; + } + do_check_true(success); + + run_next_test(); +}); + +add_test(function test_fillResultSamples_noEmptyData() { + var samples = 3; + var sampleRate = netStatsDb.sampleRate; + var start = filterTimestamp(new Date()); + var end = start + (sampleRate * samples); + var data = [{date: new Date(start + sampleRate), + rxBytes: 0, + txBytes: 0}]; + netStatsDb.fillResultSamples(start, end, data); + do_check_eq(data.length, samples + 1); + + var aux = start; + var success = true; + for (var i = 0; i <= samples; i++) { + if (i == 1) { + if (data[i].date.getTime() != aux || data[i].rxBytes != 0 || data[i].txBytes != 0) { + success = false; + break; + } + } else { + if (data[i].date.getTime() != aux || data[i].rxBytes != undefined || data[i].txBytes != undefined) { + success = false; + break; + } + } + aux += sampleRate; + } + do_check_true(success); + + run_next_test(); +}); + +add_test(function test_clear() { + var networks = getNetworks(); + networks.forEach(function(network, index) { + networks[index] = {network: network, networkId: getNetworkId(network.id, network.type)}; + }, this); + + netStatsDb.clearStats(networks, function (error, result) { + do_check_eq(error, null); + run_next_test(); + }); +}); + +add_test(function test_clear_interface() { + var networks = getNetworks(); + networks.forEach(function(network, index) { + networks[index] = {network: network, networkId: getNetworkId(network.id, network.type)}; + }, this); + + netStatsDb.clearInterfaceStats(networks[0], function (error, result) { + do_check_eq(error, null); + run_next_test(); + }); +}); + +add_test(function test_internalSaveStats_singleSample() { + var networks = getNetworks(); + + var stats = { appId: 0, + isInBrowser: 0, + serviceType: "", + network: [networks[0].id, networks[0].type], + timestamp: Date.now(), + rxBytes: 0, + txBytes: 0, + rxSystemBytes: 1234, + txSystemBytes: 1234, + rxTotalBytes: 1234, + txTotalBytes: 1234 }; + + netStatsDb.dbNewTxn(STATS_STORE_NAME, "readwrite", function(txn, store) { + netStatsDb._saveStats(txn, store, stats); + }, function(error, result) { + do_check_eq(error, null); + + netStatsDb.logAllRecords(function(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 1); + do_check_eq(result[0].appId, stats.appId); + do_check_eq(result[0].isInBrowser, stats.isInBrowser); + do_check_eq(result[0].serviceType, stats.serviceType); + do_check_true(compareNetworks(result[0].network, stats.network)); + do_check_eq(result[0].timestamp, stats.timestamp); + do_check_eq(result[0].rxBytes, stats.rxBytes); + do_check_eq(result[0].txBytes, stats.txBytes); + do_check_eq(result[0].rxSystemBytes, stats.rxSystemBytes); + do_check_eq(result[0].txSystemBytes, stats.txSystemBytes); + do_check_eq(result[0].rxTotalBytes, stats.rxTotalBytes); + do_check_eq(result[0].txTotalBytes, stats.txTotalBytes); + run_next_test(); + }); + }); +}); + +add_test(function test_internalSaveStats_arraySamples() { + clearStore(STATS_STORE_NAME, function() { + var networks = getNetworks(); + var network = [networks[0].id, networks[0].type]; + + var samples = 2; + var stats = []; + for (var i = 0; i < samples; i++) { + stats.push({ appId: 0, + isInBrowser: 0, + serviceType: "", + network: network, + timestamp: Date.now() + (10 * i), + rxBytes: 0, + txBytes: 0, + rxSystemBytes: 1234, + txSystemBytes: 1234, + rxTotalBytes: 1234, + txTotalBytes: 1234 }); + } + + netStatsDb.dbNewTxn(STATS_STORE_NAME, "readwrite", function(txn, store) { + netStatsDb._saveStats(txn, store, stats); + }, function(error, result) { + do_check_eq(error, null); + + netStatsDb.logAllRecords(function(error, result) { + do_check_eq(error, null); + + do_check_eq(result.length, samples); + var success = true; + for (var i = 0; i < samples; i++) { + if (result[i].appId != stats[i].appId || + result[i].isInBrowser != stats[i].isInBrowser || + result[i].serviceType != stats[i].serviceType || + !compareNetworks(result[i].network, stats[i].network) || + result[i].timestamp != stats[i].timestamp || + result[i].rxBytes != stats[i].rxBytes || + result[i].txBytes != stats[i].txBytes || + result[i].rxSystemBytes != stats[i].rxSystemBytes || + result[i].txSystemBytes != stats[i].txSystemBytes || + result[i].rxTotalBytes != stats[i].rxTotalBytes || + result[i].txTotalBytes != stats[i].txTotalBytes) { + success = false; + break; + } + } + do_check_true(success); + run_next_test(); + }); + }); + }); +}); + +add_test(function test_internalRemoveOldStats() { + clearStore(STATS_STORE_NAME, function() { + var networks = getNetworks(); + var network = [networks[0].id, networks[0].type]; + var samples = 10; + var stats = []; + for (var i = 0; i < samples - 1; i++) { + stats.push({ appId: 0, isInBrowser: 0, + serviceType: "", + network: network, timestamp: Date.now() + (10 * i), + rxBytes: 0, txBytes: 0, + rxSystemBytes: 1234, txSystemBytes: 1234, + rxTotalBytes: 1234, txTotalBytes: 1234 }); + } + + stats.push({ appId: 0, isInBrowser: 0, + serviceType: "", + network: network, timestamp: Date.now() + (10 * samples), + rxBytes: 0, txBytes: 0, + rxSystemBytes: 1234, txSystemBytes: 1234, + rxTotalBytes: 1234, txTotalBytes: 1234 }); + + netStatsDb.dbNewTxn(STATS_STORE_NAME, "readwrite", function(txn, store) { + netStatsDb._saveStats(txn, store, stats); + var date = stats[stats.length - 1].timestamp + + (netStatsDb.sampleRate * netStatsDb.maxStorageSamples - 1) - 1; + netStatsDb._removeOldStats(txn, store, 0, 0, "", network, date); + }, function(error, result) { + do_check_eq(error, null); + + netStatsDb.logAllRecords(function(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 1); + + run_next_test(); + }); + }); + }); +}); + +function processSamplesDiff(networks, lastStat, newStat, callback) { + clearStore(STATS_STORE_NAME, function() { + netStatsDb.dbNewTxn(STATS_STORE_NAME, "readwrite", function(txn, store) { + netStatsDb._saveStats(txn, store, lastStat); + }, function(error, result) { + netStatsDb.dbNewTxn(STATS_STORE_NAME, "readwrite", function(txn, store) { + let request = store.index("network").openCursor(newStat.network, "prev"); + request.onsuccess = function onsuccess(event) { + let cursor = event.target.result; + do_check_neq(cursor, null); + netStatsDb._processSamplesDiff(txn, store, cursor, newStat, true); + }; + }, function(error, result) { + do_check_eq(error, null); + netStatsDb.logAllRecords(function(error, result) { + do_check_eq(error, null); + callback(result); + }); + }); + }); + }); +} + +add_test(function test_processSamplesDiffSameSample() { + var networks = getNetworks(); + var network = [networks[0].id, networks[0].type]; + + var sampleRate = netStatsDb.sampleRate; + var date = filterTimestamp(new Date()); + + var lastStat = { appId: 0, isInBrowser: 0, + serviceType: "", + network: network, timestamp: date, + rxBytes: 0, txBytes: 0, + rxSystemBytes: 1234, txSystemBytes: 1234, + rxTotalBytes: 2234, txTotalBytes: 2234 }; + + var newStat = { appId: 0, isInBrowser: 0, + serviceType: "", + network: network, timestamp: date, + rxBytes: 0, txBytes: 0, + rxSystemBytes: 2234, txSystemBytes: 2234, + rxTotalBytes: 2234, txTotalBytes: 2234 }; + + processSamplesDiff(networks, lastStat, newStat, function(result) { + do_check_eq(result.length, 1); + do_check_eq(result[0].appId, newStat.appId); + do_check_eq(result[0].isInBrowser, newStat.isInBrowser); + do_check_eq(result[0].serviceType, newStat.serviceType); + do_check_true(compareNetworks(result[0].network, newStat.network)); + do_check_eq(result[0].timestamp, newStat.timestamp); + do_check_eq(result[0].rxBytes, newStat.rxSystemBytes - lastStat.rxSystemBytes); + do_check_eq(result[0].txBytes, newStat.txSystemBytes - lastStat.txSystemBytes); + do_check_eq(result[0].rxTotalBytes, lastStat.rxTotalBytes + newStat.rxSystemBytes - lastStat.rxSystemBytes); + do_check_eq(result[0].txTotalBytes, lastStat.txTotalBytes + newStat.txSystemBytes - lastStat.txSystemBytes); + do_check_eq(result[0].rxSystemBytes, newStat.rxSystemBytes); + do_check_eq(result[0].txSystemBytes, newStat.txSystemBytes); + run_next_test(); + }); +}); + +add_test(function test_processSamplesDiffNextSample() { + var networks = getNetworks(); + var network = [networks[0].id, networks[0].type]; + + var sampleRate = netStatsDb.sampleRate; + var date = filterTimestamp(new Date()); + + var lastStat = { appId: 0, isInBrowser: 0, + serviceType: "", + network: network, timestamp: date, + rxBytes: 0, txBytes: 0, + rxSystemBytes: 1234, txSystemBytes: 1234, + rxTotalBytes: 2234, txTotalBytes: 2234 }; + + var newStat = { appId: 0, isInBrowser: 0, + serviceType: "", + network: network, timestamp: date + sampleRate, + rxBytes: 0, txBytes: 0, + rxSystemBytes: 1734, txSystemBytes: 1734, + rxTotalBytes: 0, txTotalBytes: 0 }; + + processSamplesDiff(networks, lastStat, newStat, function(result) { + do_check_eq(result.length, 2); + do_check_eq(result[1].appId, newStat.appId); + do_check_eq(result[1].isInBrowser, newStat.isInBrowser); + do_check_eq(result[1].serviceType, newStat.serviceType); + do_check_true(compareNetworks(result[1].network, newStat.network)); + do_check_eq(result[1].timestamp, newStat.timestamp); + do_check_eq(result[1].rxBytes, newStat.rxSystemBytes - lastStat.rxSystemBytes); + do_check_eq(result[1].txBytes, newStat.txSystemBytes - lastStat.txSystemBytes); + do_check_eq(result[1].rxSystemBytes, newStat.rxSystemBytes); + do_check_eq(result[1].txSystemBytes, newStat.txSystemBytes); + do_check_eq(result[1].rxTotalBytes, lastStat.rxTotalBytes + newStat.rxSystemBytes - lastStat.rxSystemBytes); + do_check_eq(result[1].txTotalBytes, lastStat.txTotalBytes + newStat.txSystemBytes - lastStat.txSystemBytes); + run_next_test(); + }); +}); + +add_test(function test_processSamplesDiffSamplesLost() { + var networks = getNetworks(); + var network = [networks[0].id, networks[0].type]; + var samples = 5; + var sampleRate = netStatsDb.sampleRate; + var date = filterTimestamp(new Date()); + var lastStat = { appId: 0, isInBrowser: 0, + serviceType: "", + network: network, timestamp: date, + rxBytes: 0, txBytes: 0, + rxSystemBytes: 1234, txSystemBytes: 1234, + rxTotalBytes: 2234, txTotalBytes: 2234}; + + var newStat = { appId: 0, isInBrowser: 0, + serviceType: "", + network: network, timestamp: date + (sampleRate * samples), + rxBytes: 0, txBytes: 0, + rxSystemBytes: 2234, txSystemBytes: 2234, + rxTotalBytes: 0, txTotalBytes: 0 }; + + processSamplesDiff(networks, lastStat, newStat, function(result) { + do_check_eq(result.length, samples + 1); + do_check_eq(result[0].appId, newStat.appId); + do_check_eq(result[0].isInBrowser, newStat.isInBrowser); + do_check_eq(result[0].serviceType, newStat.serviceType); + do_check_true(compareNetworks(result[samples].network, newStat.network)); + do_check_eq(result[samples].timestamp, newStat.timestamp); + do_check_eq(result[samples].rxBytes, newStat.rxTotalBytes - lastStat.rxTotalBytes); + do_check_eq(result[samples].txBytes, newStat.txTotalBytes - lastStat.txTotalBytes); + do_check_eq(result[samples].rxSystemBytes, newStat.rxSystemBytes); + do_check_eq(result[samples].txSystemBytes, newStat.txSystemBytes); + do_check_eq(result[samples].rxTotalBytes, lastStat.rxTotalBytes + newStat.rxSystemBytes - lastStat.rxSystemBytes); + do_check_eq(result[samples].txTotalBytes, lastStat.txTotalBytes + newStat.txSystemBytes - lastStat.txSystemBytes); + run_next_test(); + }); +}); + +add_test(function test_saveStats() { + var networks = getNetworks(); + var network = [networks[0].id, networks[0].type]; + + var stats = { appId: 0, + isInBrowser: false, + serviceType: "", + networkId: networks[0].id, + networkType: networks[0].type, + date: new Date(), + rxBytes: 2234, + txBytes: 2234, + isAccumulative: true }; + + clearStore(STATS_STORE_NAME, function() { + netStatsDb.saveStats(stats, function(error, result) { + do_check_eq(error, null); + netStatsDb.logAllRecords(function(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 1); + do_check_eq(result[0].appId, stats.appId); + do_check_eq(result[0].isInBrowser, stats.isInBrowser); + do_check_eq(result[0].serviceType, stats.serviceType); + do_check_true(compareNetworks(result[0].network, network)); + let timestamp = filterTimestamp(stats.date); + do_check_eq(result[0].timestamp, timestamp); + do_check_eq(result[0].rxBytes, stats.rxBytes); + do_check_eq(result[0].txBytes, stats.txBytes); + do_check_eq(result[0].rxSystemBytes, stats.rxBytes); + do_check_eq(result[0].txSystemBytes, stats.txBytes); + do_check_eq(result[0].rxTotalBytes, stats.rxBytes); + do_check_eq(result[0].txTotalBytes, stats.txBytes); + run_next_test(); + }); + }); + }); +}); + +add_test(function test_saveAppStats() { + var networks = getNetworks(); + var network = [networks[0].id, networks[0].type]; + + var stats = { appId: 1, + isInBrowser: false, + serviceType: "", + networkId: networks[0].id, + networkType: networks[0].type, + date: new Date(), + rxBytes: 2234, + txBytes: 2234, + isAccumulative: false }; + + clearStore(STATS_STORE_NAME, function() { + netStatsDb.saveStats(stats, function(error, result) { + do_check_eq(error, null); + netStatsDb.logAllRecords(function(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 1); + do_check_eq(result[0].appId, stats.appId); + do_check_eq(result[0].isInBrowser, 0); + do_check_eq(result[0].serviceType, stats.serviceType); + do_check_true(compareNetworks(result[0].network, network)); + let timestamp = filterTimestamp(stats.date); + do_check_eq(result[0].timestamp, timestamp); + do_check_eq(result[0].rxBytes, stats.rxBytes); + do_check_eq(result[0].txBytes, stats.txBytes); + do_check_eq(result[0].rxSystemBytes, 0); + do_check_eq(result[0].txSystemBytes, 0); + do_check_eq(result[0].rxTotalBytes, 0); + do_check_eq(result[0].txTotalBytes, 0); + run_next_test(); + }); + }); + }); +}); + +add_test(function test_saveServiceStats() { + var networks = getNetworks(); + var network = [networks[0].id, networks[0].type]; + + var stats = { appId: 0, + isInBrowser: false, + serviceType: "FakeType", + networkId: networks[0].id, + networkType: networks[0].type, + date: new Date(), + rxBytes: 2234, + txBytes: 2234, + isAccumulative: false }; + + clearStore(STATS_STORE_NAME, function() { + netStatsDb.saveStats(stats, function(error, result) { + do_check_eq(error, null); + netStatsDb.logAllRecords(function(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 1); + do_check_eq(result[0].appId, stats.appId); + do_check_eq(result[0].isInBrowser, 0); + do_check_eq(result[0].serviceType, stats.serviceType); + do_check_true(compareNetworks(result[0].network, network)); + let timestamp = filterTimestamp(stats.date); + do_check_eq(result[0].timestamp, timestamp); + do_check_eq(result[0].rxBytes, stats.rxBytes); + do_check_eq(result[0].txBytes, stats.txBytes); + do_check_eq(result[0].rxSystemBytes, 0); + do_check_eq(result[0].txSystemBytes, 0); + do_check_eq(result[0].rxTotalBytes, 0); + do_check_eq(result[0].txTotalBytes, 0); + run_next_test(); + }); + }); + }); +}); + +function prepareFind(stats, callback) { + clearStore(STATS_STORE_NAME, function() { + netStatsDb.dbNewTxn(STATS_STORE_NAME, "readwrite", function(txn, store) { + netStatsDb._saveStats(txn, store, stats); + }, function(error, result) { + callback(error, result); + }); + }); +} + +add_test(function test_find () { + var networks = getNetworks(); + var networkWifi = [networks[0].id, networks[0].type]; + var networkMobile = [networks[1].id, networks[1].type]; // Fake mobile interface + var appId = 0; + var isInBrowser = 0; + var serviceType = ""; + + var samples = 5; + var sampleRate = netStatsDb.sampleRate; + var start = Date.now(); + var saveDate = filterTimestamp(new Date()); + var end = new Date(start + (sampleRate * (samples - 1))); + start = new Date(start - sampleRate); + var stats = []; + for (var i = 0; i < samples; i++) { + stats.push({ appId: appId, isInBrowser: isInBrowser, + serviceType: serviceType, + network: networkWifi, timestamp: saveDate + (sampleRate * i), + rxBytes: 0, txBytes: 10, + rxSystemBytes: 0, txSystemBytes: 0, + rxTotalBytes: 0, txTotalBytes: 0 }); + + + stats.push({ appId: appId, isInBrowser: isInBrowser, + serviceType: serviceType, + network: networkMobile, timestamp: saveDate + (sampleRate * i), + rxBytes: 0, txBytes: 10, + rxSystemBytes: 0, txSystemBytes: 0, + rxTotalBytes: 0, txTotalBytes: 0 }); + } + + prepareFind(stats, function(error, result) { + do_check_eq(error, null); + netStatsDb.find(function (error, result) { + do_check_eq(error, null); + do_check_eq(result.browsingTrafficOnly, false); + do_check_eq(result.serviceType, serviceType); + do_check_eq(result.network.id, networks[0].id); + do_check_eq(result.network.type, networks[0].type); + do_check_eq(result.start.getTime(), start.getTime()); + do_check_eq(result.end.getTime(), end.getTime()); + do_check_eq(result.data.length, samples + 1); + do_check_eq(result.data[0].rxBytes, null); + do_check_eq(result.data[1].rxBytes, 0); + do_check_eq(result.data[samples].rxBytes, 0); + run_next_test(); + }, appId, false, serviceType, networks[0], start, end); + }); +}); + +add_test(function test_findAppStats () { + var networks = getNetworks(); + var networkWifi = [networks[0].id, networks[0].type]; + var networkMobile = [networks[1].id, networks[1].type]; // Fake mobile interface + var appId = 1; + var isInBrowser = 0; + var serviceType = ""; + + var samples = 5; + var sampleRate = netStatsDb.sampleRate; + var start = Date.now(); + var saveDate = filterTimestamp(new Date()); + var end = new Date(start + (sampleRate * (samples - 1))); + start = new Date(start - sampleRate); + var stats = []; + for (var i = 0; i < samples; i++) { + stats.push({ appId: appId, isInBrowser: isInBrowser, + serviceType: serviceType, + network: networkWifi, timestamp: saveDate + (sampleRate * i), + rxBytes: 0, txBytes: 10, + rxTotalBytes: 0, txTotalBytes: 0 }); + + stats.push({ appId: appId, isInBrowser: isInBrowser, + serviceType: serviceType, + network: networkMobile, timestamp: saveDate + (sampleRate * i), + rxBytes: 0, txBytes: 10, + rxTotalBytes: 0, txTotalBytes: 0 }); + } + + prepareFind(stats, function(error, result) { + do_check_eq(error, null); + netStatsDb.find(function (error, result) { + do_check_eq(error, null); + do_check_eq(result.browsingTrafficOnly, false); + do_check_eq(result.serviceType, serviceType); + do_check_eq(result.network.id, networks[0].id); + do_check_eq(result.network.type, networks[0].type); + do_check_eq(result.start.getTime(), start.getTime()); + do_check_eq(result.end.getTime(), end.getTime()); + do_check_eq(result.data.length, samples + 1); + do_check_eq(result.data[0].rxBytes, null); + do_check_eq(result.data[1].rxBytes, 0); + do_check_eq(result.data[samples].rxBytes, 0); + run_next_test(); + }, appId, false, serviceType, networks[0], start, end); + }); +}); + +add_test(function test_findServiceStats () { + var networks = getNetworks(); + var networkWifi = [networks[0].id, networks[0].type]; + var networkMobile = [networks[1].id, networks[1].type]; // Fake mobile interface + var appId = 0; + var isInBrowser = 0; + var serviceType = "FakeType"; + + var samples = 5; + var sampleRate = netStatsDb.sampleRate; + var start = Date.now(); + var saveDate = filterTimestamp(new Date()); + var end = new Date(start + (sampleRate * (samples - 1))); + start = new Date(start - sampleRate); + var stats = []; + for (var i = 0; i < samples; i++) { + stats.push({ appId: appId, isInBrowser: isInBrowser, + serviceType: serviceType, + network: networkWifi, timestamp: saveDate + (sampleRate * i), + rxBytes: 0, txBytes: 10, + rxTotalBytes: 0, txTotalBytes: 0 }); + + stats.push({ appId: appId, isInBrowser: isInBrowser, + serviceType: serviceType, + network: networkMobile, timestamp: saveDate + (sampleRate * i), + rxBytes: 0, txBytes: 10, + rxTotalBytes: 0, txTotalBytes: 0 }); + } + + prepareFind(stats, function(error, result) { + do_check_eq(error, null); + netStatsDb.find(function (error, result) { + do_check_eq(error, null); + do_check_eq(result.browsingTrafficOnly, false); + do_check_eq(result.serviceType, serviceType); + do_check_eq(result.network.id, networks[0].id); + do_check_eq(result.network.type, networks[0].type); + do_check_eq(result.start.getTime(), start.getTime()); + do_check_eq(result.end.getTime(), end.getTime()); + do_check_eq(result.data.length, samples + 1); + do_check_eq(result.data[0].rxBytes, null); + do_check_eq(result.data[1].rxBytes, 0); + do_check_eq(result.data[samples].rxBytes, 0); + run_next_test(); + }, appId, false, serviceType, networks[0], start, end); + }); +}); + +add_test(function test_saveMultipleAppStats () { + var networks = getNetworks(); + var networkWifi = networks[0]; + var networkMobile = networks[1]; // Fake mobile interface + + var saveDate = filterTimestamp(new Date()); + var cached = Object.create(null); + var serviceType = "FakeType"; + var wifiNetId = networkWifi.id + '' + networkWifi.type; + var mobileNetId = networkMobile.id + '' + networkMobile.type; + + cached[0 + '' + serviceType + wifiNetId] = { + appId: 0, date: new Date(), + networkId: networkWifi.id, networkType: networkWifi.type, + rxBytes: 0, txBytes: 10, + serviceType: serviceType, isAccumulative: false, + isInBrowser: false + }; + + cached[0 + '' + serviceType + mobileNetId] = { + appId: 0, date: new Date(), + networkId: networkMobile.id, networkType: networkMobile.type, + rxBytes: 0, txBytes: 10, + serviceType: serviceType, isAccumulative: false, + isInBrowser: false + }; + + cached[1 + '' + wifiNetId] = { + appId: 1, date: new Date(), + networkId: networkWifi.id, networkType: networkWifi.type, + rxBytes: 0, txBytes: 10, + serviceType: "", isAccumulative: false, + isInBrowser: false + }; + + cached[1 + '' + mobileNetId] = { + appId: 1, date: new Date(), + networkId: networkMobile.id, networkType: networkMobile.type, + rxBytes: 0, txBytes: 10, + serviceType: "", isAccumulative: false, + isInBrowser: false + }; + + cached[2 + '' + wifiNetId] = { + appId: 2, date: new Date(), + networkId: networkWifi.id, networkType: networkWifi.type, + rxBytes: 0, txBytes: 10, + serviceType: "", isAccumulative: false, + isInBrowser: false + }; + + cached[2 + '' + mobileNetId] = { + appId: 2, date: new Date(), + networkId: networkMobile.id, networkType: networkMobile.type, + rxBytes: 0, txBytes: 10, + serviceType: "", isAccumulative: false, + isInBrowser: false + }; + + let keys = Object.keys(cached); + let index = 0; + + networks.push(networkMobile); + + clearStore(STATS_STORE_NAME, function() { + netStatsDb.saveStats(cached[keys[index]], + function callback(error, result) { + do_check_eq(error, null); + + if (index == keys.length - 1) { + netStatsDb.logAllRecords(function(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 6); + do_check_eq(result[0].isInBrowser, 0); + do_check_eq(result[0].serviceType, serviceType); + do_check_eq(result[3].appId, 1); + do_check_true(compareNetworks(result[0].network, [networkWifi.id, networkWifi.type])); + do_check_eq(result[0].rxBytes, 0); + do_check_eq(result[0].txBytes, 10); + run_next_test(); + }); + return; + } + + index += 1; + netStatsDb.saveStats(cached[keys[index]], callback); + }); + }); +}); + +// Test case for find samples with browsingTrafficOnly option. +add_test(function test_findBrowsingTrafficStats() { + var networks = getNetworks(); + var networkWifi = [networks[0].id, networks[0].type]; + var networkMobile = [networks[1].id, networks[1].type]; + var serviceType = ""; + var samples = 5; + var sampleRate = netStatsDb.sampleRate; + var start = Date.now(); + var end = new Date(start + (sampleRate * (samples - 1))); + var saveDate = filterTimestamp(new Date()); + start = new Date(start - sampleRate); + var stats = []; + + for (var i = 0; i < samples; i++) { + // System app. + stats.push({ appId: 1008, isInBrowser: 0, + serviceType: serviceType, network: networkMobile, + timestamp: saveDate + (sampleRate * i), + rxBytes: 200, txBytes: 100, + rxTotalBytes: 200, txTotalBytes: 100}); + // Browser of system app. + stats.push({ appId: 1008, isInBrowser: 1, + serviceType: serviceType, network: networkMobile, + timestamp: saveDate + (sampleRate * i), + rxBytes: 1000, txBytes: 500, + rxTotalBytes: 1000, txTotalBytes: 500}); + // Another app. + stats.push({ appId: 1021, isInBrowser: 0, + serviceType: serviceType, network: networkMobile, + timestamp: saveDate + (sampleRate * i), + rxBytes: 300, txBytes: 150, + rxTotalBytes: 300, txTotalBytes: 150}); + // Browser of another app. + stats.push({ appId: 1021, isInBrowser: 1, + serviceType: serviceType, network: networkMobile, + timestamp: saveDate + (sampleRate * i), + rxBytes: 600, txBytes: 300, + rxTotalBytes: 600, txTotalBytes: 300}); + } + + prepareFind(stats, function(error, result) { + do_check_eq(error, null); + netStatsDb.find(function(error, result) { + do_check_eq(error, null); + do_check_eq(result.browsingTrafficOnly, true); + do_check_eq(result.serviceType, serviceType); + do_check_eq(result.network.id, networks[1].id); + do_check_eq(result.network.type, networks[1].type); + do_check_eq(result.start.getTime(), start.getTime()); + do_check_eq(result.end.getTime(), end.getTime()); + do_check_eq(result.data.length, samples + 1); + do_check_eq(result.data[0].rxBytes, null); + do_check_eq(result.data[1].txBytes, 500); + do_check_eq(result.data[2].rxBytes, 1000); + run_next_test(); + }, 1008, true, serviceType, networks[1], start, end); + }); +}); + +// Test case for find samples with browsingTrafficOnly option. +add_test(function test_findAppTrafficStats() { + var networks = getNetworks(); + var networkWifi = [networks[0].id, networks[0].type]; + var networkMobile = [networks[1].id, networks[1].type]; + var serviceType = ""; + var samples = 5; + var sampleRate = netStatsDb.sampleRate; + var start = Date.now(); + var end = new Date(start + (sampleRate * (samples - 1))); + var saveDate = filterTimestamp(new Date()); + start = new Date(start - sampleRate); + var stats = []; + + for (var i = 0; i < samples; i++) { + // System app. + stats.push({ appId: 1008, isInBrowser: 0, + serviceType: serviceType, network: networkMobile, + timestamp: saveDate + (sampleRate * i), + rxBytes: 200, txBytes: 100, + rxTotalBytes: 200, txTotalBytes: 100}); + // Browser of system app. + stats.push({ appId: 1008, isInBrowser: 1, + serviceType: serviceType, network: networkMobile, + timestamp: saveDate + (sampleRate * i), + rxBytes: 1000, txBytes: 500, + rxTotalBytes: 1000, txTotalBytes: 500}); + // Another app. + stats.push({ appId: 1021, isInBrowser: 0, + serviceType: serviceType, network: networkMobile, + timestamp: saveDate + (sampleRate * i), + rxBytes: 300, txBytes: 150, + rxTotalBytes: 300, txTotalBytes: 150}); + // Browser of another app. + stats.push({ appId: 1021, isInBrowser: 1, + serviceType: serviceType, network: networkMobile, + timestamp: saveDate + (sampleRate * i), + rxBytes: 600, txBytes: 300, + rxTotalBytes: 600, txTotalBytes: 300}); + } + + prepareFind(stats, function(error, result) { + do_check_eq(error, null); + netStatsDb.find(function(error, result) { + do_check_eq(error, null); + do_check_eq(result.browsingTrafficOnly, false); + do_check_eq(result.serviceType, serviceType); + do_check_eq(result.network.id, networks[1].id); + do_check_eq(result.network.type, networks[1].type); + do_check_eq(result.start.getTime(), start.getTime()); + do_check_eq(result.end.getTime(), end.getTime()); + do_check_eq(result.data.length, samples + 1); + do_check_eq(result.data[0].rxBytes, null); + do_check_eq(result.data[1].txBytes, 600); + do_check_eq(result.data[2].rxBytes, 1200); + run_next_test(); + }, 1008, false, serviceType, networks[1], start, end); + }); +}); + +var networkWifi = '00'; +var networkMobile = '11'; + +var examplePageURL = "http://example.com/index.html"; +var exampleManifestURL = "http://example.com/manifest.webapp"; + +var testPageURL = "http://test.com/index.html"; +var testManifestURL = "http://test.com/manifest.webapp"; + +var alarms = [{ id: null, + networkId: networkWifi, + absoluteThreshold: 10000, + relativeThreshold: 10000, + data: {foo: "something"}, + pageURL: examplePageURL, + manifestURL: exampleManifestURL }, + { id: null, + networkId: networkWifi, + absoluteThreshold: 1000, + relativeThreshold: 1000, + data: {foo: "else"}, + pageURL: examplePageURL, + manifestURL: exampleManifestURL }, + { id: null, + networkId: networkMobile, + absoluteThreshold: 100, + relativeThreshold: 100, + data: {foo: "to"}, + pageURL: examplePageURL, + manifestURL: exampleManifestURL }, + { id: null, + networkId: networkMobile, + absoluteThreshold: 10, + relativeThreshold: 10, + data: {foo: "test"}, + pageURL: testPageURL, + manifestURL: testManifestURL }]; + +var alarmsDbId = 1; + +add_test(function test_addAlarm() { + // Add alarms[0] -> DB: [ alarms[0] (id: 1) ] + // Check the insertion is OK. + netStatsDb.addAlarm(alarms[0], function(error, result) { + do_check_eq(error, null); + alarmsDbId = result; + netStatsDb.getAlarms(Ci.nsINetworkInfo.NETWORK_TYPE_WIFI, exampleManifestURL, function(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 1); + do_check_eq(result[0].id, alarmsDbId); + do_check_eq(result[0].networkId, alarms[0].networkId); + do_check_eq(result[0].absoluteThreshold, alarms[0].absoluteThreshold); + do_check_eq(result[0].relativeThreshold, alarms[0].relativeThreshold); + do_check_eq(result[0].data.foo, alarms[0].data.foo); + run_next_test(); + }); + }); +}); + +add_test(function test_getFirstAlarm() { + // Add alarms[1] -> DB: [ alarms[0] (id: 1), alarms[1] (id: 2) ] + // Check first alarm is alarms[1] because threshold is lower. + alarmsDbId += 1; + netStatsDb.addAlarm(alarms[1], function (error, result) { + do_check_eq(error, null); + do_check_eq(result, alarmsDbId); + netStatsDb.getFirstAlarm(networkWifi, function(error, result) { + do_check_eq(error, null); + do_check_eq(result.id, alarmsDbId); + do_check_eq(result.networkId, alarms[1].networkId); + do_check_eq(result.absoluteThreshold, alarms[1].absoluteThreshold); + do_check_eq(result.relativeThreshold, alarms[1].relativeThreshold); + do_check_eq(result.data.foo, alarms[1].data.foo); + do_check_eq(result.pageURL, alarms[1].pageURL); + do_check_eq(result.manifestURL, alarms[1].manifestURL); + run_next_test(); + }); + }); +}); + +add_test(function test_removeAlarm() { + // Remove alarms[1] (id: 2) -> DB: [ alarms[0] (id: 1) ] + // Check get first return alarms[0]. + netStatsDb.removeAlarm(alarmsDbId, alarms[0].manifestURL, function (error, result) { + do_check_eq(error, null); + netStatsDb.getFirstAlarm(networkWifi, function(error, result) { + do_check_eq(error, null); + do_check_eq(result.id, alarmsDbId - 1); + do_check_eq(result.networkId, alarms[0].networkId); + do_check_eq(result.absoluteThreshold, alarms[0].absoluteThreshold); + do_check_eq(result.relativeThreshold, alarms[0].relativeThreshold); + do_check_eq(result.data.foo, alarms[0].data.foo); + do_check_eq(result.pageURL, alarms[0].pageURL); + do_check_eq(result.manifestURL, alarms[0].manifestURL); + run_next_test(); + }); + }); +}); + +add_test(function test_removeAppAlarm() { + // Remove alarms[0] (id: 1) -> DB: [ ] + netStatsDb.removeAlarm(alarmsDbId - 1, alarms[0].manifestURL, function (error, result) { + do_check_eq(error, null); + netStatsDb.getAlarms(networkWifi, exampleManifestURL, function(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 0); + run_next_test(); + }); + }); +}); + +add_test(function test_getAlarms() { + // Add all alarms -> DB: [ alarms[0] (id: 3), + // alarms[1] (id: 4), + // alarms[2] (id: 5), + // alarms[3] (id: 6) ] + // Check that getAlarms for wifi returns 2 alarms. + // Check that getAlarms for all connections returns 3 alarms. + + var callback = function () { + netStatsDb.getAlarms(networkWifi, exampleManifestURL, function(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 2); + netStatsDb.getAlarms(null, exampleManifestURL, function(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 3); + run_next_test(); + }); + }); + }; + + var index = 0; + + var addFunction = function () { + alarmsDbId += 1; + netStatsDb.addAlarm(alarms[index], function (error, result) { + do_check_eq(error, null); + index += 1; + do_check_eq(result, alarmsDbId); + if (index >= alarms.length) { + callback(); + return; + } + addFunction(); + }); + }; + + addFunction(); +}); + +add_test(function test_removeAppAllAlarms() { + // Remove all alarms for exampleManifestURL -> DB: [ alarms[3] (id: 6) ] + netStatsDb.removeAlarms(exampleManifestURL, function (error, result) { + do_check_eq(error, null); + netStatsDb.getAlarms(null, exampleManifestURL, function(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 0); + netStatsDb.getAlarms(null, testManifestURL, function(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 1); + run_next_test(); + }); + }); + }); +}); + +add_test(function test_updateAlarm() { + // Update alarms[3] (id: 6) -> DB: [ alarms[3]* (id: 6) ] + + var updatedAlarm = alarms[1]; + updatedAlarm.id = alarmsDbId; + updatedAlarm.threshold = 10; + + netStatsDb.updateAlarm(updatedAlarm, function (error, result) { + do_check_eq(error, null); + netStatsDb.getFirstAlarm(networkWifi, function(error, result) { + do_check_eq(error, null); + do_check_eq(result.id, updatedAlarm.id); + do_check_eq(result.networkId, updatedAlarm.networkId); + do_check_eq(result.absoluteThreshold, updatedAlarm.absoluteThreshold); + do_check_eq(result.relativeThreshold, updatedAlarm.relativeThreshold); + do_check_eq(result.data.foo, updatedAlarm.data.foo); + do_check_eq(result.pageURL, updatedAlarm.pageURL); + do_check_eq(result.manifestURL, updatedAlarm.manifestURL); + run_next_test(); + }); + }); +}); + +function run_test() { + do_get_profile(); + run_next_test(); +} diff --git a/dom/network/tests/unit_stats/test_networkstats_service.js b/dom/network/tests/unit_stats/test_networkstats_service.js new file mode 100644 index 0000000000..8c43a9b54b --- /dev/null +++ b/dom/network/tests/unit_stats/test_networkstats_service.js @@ -0,0 +1,290 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +const NETWORK_STATUS_READY = 0; +const NETWORK_STATUS_STANDBY = 1; +const NETWORK_STATUS_AWAY = 2; + +const QUEUE_TYPE_UPDATE_STATS = 0; + +var wifiId = '00'; + +function getNetworks(callback) { + NetworkStatsService._db.getAvailableNetworks(function onGetNetworks(aError, aResult) { + callback(aError, aResult); + }); +} + +add_test(function test_clearDB() { + getNetworks(function onGetNetworks(error, result) { + do_check_eq(error, null); + var networks = result; + networks.forEach(function(network, index) { + networks[index] = {network: network, networkId: NetworkStatsService.getNetworkId(network.id, network.type)}; + }, this); + + NetworkStatsService._db.clearStats(networks, function onDBCleared(error, result) { + do_check_eq(error, null); + run_next_test(); + }); + }); +}); + +function getNetworkId(callback) { + getNetworks(function onGetNetworks(error, result) { + do_check_eq(error, null); + var netId = NetworkStatsService.getNetworkId(result[0].id, result[0].type); + callback(null, netId); + }); +} + +add_test(function test_networkStatsAvailable_ok() { + getNetworkId(function onGetId(error, result) { + do_check_eq(error, null); + var netId = result; + NetworkStatsService.networkStatsAvailable(function (success, msg) { + do_check_eq(success, true); + run_next_test(); + }, netId, true, 1234, 4321, Date.now()); + }); +}); + +add_test(function test_networkStatsAvailable_failure() { + getNetworkId(function onGetId(error, result) { + do_check_eq(error, null); + var netId = result; + NetworkStatsService.networkStatsAvailable(function (success, msg) { + do_check_eq(success, false); + run_next_test(); + }, netId, false, 1234, 4321, Date.now()); + }); +}); + +add_test(function test_update_invalidNetwork() { + NetworkStatsService.update(-1, function (success, msg) { + do_check_eq(success, false); + do_check_eq(msg, "Invalid network -1"); + run_next_test(); + }); +}); + +add_test(function test_update() { + getNetworkId(function onGetId(error, result) { + do_check_eq(error, null); + var netId = result; + NetworkStatsService.update(netId, function (success, msg) { + do_check_eq(success, true); + run_next_test(); + }); + }); +}); + +add_test(function test_updateQueueIndex() { + NetworkStatsService.updateQueue = [{netId: 0, callbacks: null, queueType: QUEUE_TYPE_UPDATE_STATS}, + {netId: 1, callbacks: null, queueType: QUEUE_TYPE_UPDATE_STATS}, + {netId: 2, callbacks: null, queueType: QUEUE_TYPE_UPDATE_STATS}, + {netId: 3, callbacks: null, queueType: QUEUE_TYPE_UPDATE_STATS}, + {netId: 4, callbacks: null, queueType: QUEUE_TYPE_UPDATE_STATS}]; + var index = NetworkStatsService.updateQueueIndex(3); + do_check_eq(index, 3); + index = NetworkStatsService.updateQueueIndex(10); + do_check_eq(index, -1); + + NetworkStatsService.updateQueue = []; + run_next_test(); +}); + +add_test(function test_updateAllStats() { + NetworkStatsService._networks[wifiId].status = NETWORK_STATUS_READY; + NetworkStatsService.updateAllStats(function(success, msg) { + do_check_eq(success, true); + NetworkStatsService._networks[wifiId].status = NETWORK_STATUS_STANDBY; + NetworkStatsService.updateAllStats(function(success, msg) { + do_check_eq(success, true); + NetworkStatsService._networks[wifiId].status = NETWORK_STATUS_AWAY; + NetworkStatsService.updateAllStats(function(success, msg) { + do_check_eq(success, true); + run_next_test(); + }); + }); + }); +}); + +add_test(function test_updateStats_ok() { + getNetworkId(function onGetId(error, result) { + do_check_eq(error, null); + var netId = result; + NetworkStatsService.updateStats(netId, function(success, msg){ + do_check_eq(success, true); + run_next_test(); + }); + }); +}); + +add_test(function test_updateStats_failure() { + NetworkStatsService.updateStats(-1, function(success, msg){ + do_check_eq(success, false); + run_next_test(); + }); +}); + +// Define Mockup function to simulate a request to netd +function MockNetdRequest(aCallback) { + var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + var event = { + notify: function (timer) { + aCallback(); + } + }; + + timer.initWithCallback(event, 100, Ci.nsITimer.TYPE_ONE_SHOT); +} + +add_test(function test_queue() { + + // Overwrite update function of NetworkStatsService to avoid netd errors due to use + // fake interfaces. First, original function is stored to restore it at the end of the + // test. + var updateFunctionBackup = NetworkStatsService.update; + + NetworkStatsService.update = function update(aNetId, aCallback) { + MockNetdRequest(function () { + if (aCallback) { + aCallback(true, "ok"); + } + }); + }; + + // Fill networks with fake network interfaces to enable netd async requests. + var network = {id: "1234", type: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE}; + var netId1 = NetworkStatsService.getNetworkId(network.id, network.type); + NetworkStatsService._networks[netId1] = { network: network, + interfaceName: "net1" }; + + network = {id: "5678", type: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE}; + var netId2 = NetworkStatsService.getNetworkId(network.id, network.type); + NetworkStatsService._networks[netId2] = { network: network, + interfaceName: "net2" }; + + NetworkStatsService.updateStats(netId1); + NetworkStatsService.updateStats(netId2); + do_check_eq(NetworkStatsService.updateQueue.length, 2); + do_check_eq(NetworkStatsService.updateQueue[0].callbacks.length, 1); + + var i = 0; + var updateCount = 0; + var callback = function(success, msg) { + i++; + if (i >= updateCount) { + NetworkStatsService.update = updateFunctionBackup; + run_next_test(); + } + }; + + NetworkStatsService.updateStats(netId1, callback); + updateCount++; + NetworkStatsService.updateStats(netId2, callback); + updateCount++; + + do_check_eq(NetworkStatsService.updateQueue.length, 2); + do_check_eq(NetworkStatsService.updateQueue[0].callbacks.length, 2); + do_check_eq(NetworkStatsService.updateQueue[0].callbacks[0], null); + do_check_neq(NetworkStatsService.updateQueue[0].callbacks[1], null); +}); + +add_test(function test_getAlarmQuota() { + let alarm = { networkId: wifiId, absoluteThreshold: 10000 }; + + NetworkStatsService._getAlarmQuota(alarm, function onSet(error, quota){ + do_check_eq(error, null); + do_check_neq(quota, undefined); + do_check_eq(alarm.absoluteThreshold, alarm.relativeThreshold); + run_next_test(); + }); +}); + +var testPageURL = "http://test.com"; +var testManifestURL = "http://test.com/manifest.webapp"; + +add_test(function test_setAlarm() { + let alarm = { id: null, + networkId: wifiId, + threshold: 10000, + absoluteThreshold: null, + alarmStart: null, + alarmEnd: null, + data: null, + pageURL: testPageURL, + manifestURL: testManifestURL }; + + NetworkStatsService._setAlarm(alarm, function onSet(error, result) { + do_check_eq(result, 1); + run_next_test(); + }); +}); + +add_test(function test_setAlarm_invalid_threshold() { + let alarm = { id: null, + networkId: wifiId, + threshold: -10000, + absoluteThreshold: null, + alarmStart: null, + alarmEnd: null, + data: null, + pageURL: testPageURL, + manifestURL: testManifestURL }; + + NetworkStatsService._networks[wifiId].status = NETWORK_STATUS_READY; + + NetworkStatsService._setAlarm(alarm, function onSet(error, result) { + do_check_eq(error, "InvalidStateError"); + run_next_test(); + }); +}); + +add_test(function test_fireAlarm() { + // Add a fake alarm into database. + let alarm = { id: null, + networkId: wifiId, + threshold: 10000, + absoluteThreshold: null, + alarmStart: null, + alarmEnd: null, + data: null, + pageURL: testPageURL, + manifestURL: testManifestURL }; + + // Set wifi status to standby to avoid connecting to netd when adding an alarm. + NetworkStatsService._networks[wifiId].status = NETWORK_STATUS_STANDBY; + + NetworkStatsService._db.addAlarm(alarm, function addSuccessCb(error, newId) { + NetworkStatsService._db.getAlarms(Ci.nsINetworkInfo.NETWORK_TYPE_WIFI, + testManifestURL, function onGet(error, result) { + do_check_eq(error, null); + do_check_eq(result.length, 1); + + // Result of getAlarms is based on expected child's data format, so + // some changes are needed to be able to use it. + result[0].networkId = wifiId; + result[0].pageURL = testPageURL; + result[0].manifestURL = testManifestURL; + + NetworkStatsService._fireAlarm(result[0], false); + NetworkStatsService._db.getAlarms(Ci.nsINetworkInfo.NETWORK_TYPE_WIFI, + testManifestURL, function onGet(error, result) { + do_check_eq(error, undefined); + do_check_eq(result.length, 0); + run_next_test(); + }); + }); + }); +}); + +function run_test() { + do_get_profile(); + + Cu.import("resource://gre/modules/NetworkStatsService.jsm"); + run_next_test(); +} diff --git a/dom/network/tests/unit_stats/test_networkstats_service_proxy.js b/dom/network/tests/unit_stats/test_networkstats_service_proxy.js new file mode 100644 index 0000000000..131b886d03 --- /dev/null +++ b/dom/network/tests/unit_stats/test_networkstats_service_proxy.js @@ -0,0 +1,233 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "nssProxy", + "@mozilla.org/networkstatsServiceProxy;1", + "nsINetworkStatsServiceProxy"); + +function mokConvertNetworkInfo() { + NetworkStatsService.convertNetworkInfo = function(aNetworkInfo) { + if (aNetworkInfo.type != Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE && + aNetworkInfo.type != Ci.nsINetworkInfo.NETWORK_TYPE_WIFI) { + return null; + } + + let id = '0'; + if (aNetworkInfo.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE) { + id = '1234' + } + + let netId = this.getNetworkId(id, aNetworkInfo.type); + + if (!this._networks[netId]) { + this._networks[netId] = Object.create(null); + this._networks[netId].network = { id: id, + type: aNetworkInfo.type }; + } + + return netId; + }; +} + +add_test(function test_saveAppStats() { + var cachedStats = NetworkStatsService.cachedStats; + var timestamp = NetworkStatsService.cachedStatsDate.getTime(); + + // Create to fake nsINetworkInfos. As nsINetworkInfo can not be instantiated, + // these two vars will emulate it by filling the properties that will be used. + var wifi = {type: Ci.nsINetworkInfo.NETWORK_TYPE_WIFI, id: "0"}; + var mobile = {type: Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE, id: "1234"}; + + // Insert fake mobile network info in NetworkStatsService + var mobileNetId = NetworkStatsService.getNetworkId(mobile.id, mobile.type); + + do_check_eq(Object.keys(cachedStats).length, 0); + + nssProxy.saveAppStats(1, false, wifi, timestamp, 10, 20, false, + function (success, message) { + do_check_eq(success, true); + nssProxy.saveAppStats(1, false, mobile, timestamp, 10, 20, false, + function (success, message) { + var key1 = 1 + "" + false + "" + NetworkStatsService.getNetworkId(wifi.id, wifi.type); + var key2 = 1 + "" + false + "" + mobileNetId + ""; + + do_check_eq(Object.keys(cachedStats).length, 2); + do_check_eq(cachedStats[key1].appId, 1); + do_check_eq(cachedStats[key1].isInBrowser, false); + do_check_eq(cachedStats[key1].serviceType.length, 0); + do_check_eq(cachedStats[key1].networkId, wifi.id); + do_check_eq(cachedStats[key1].networkType, wifi.type); + do_check_eq(cachedStats[key1].date.getTime(), timestamp); + do_check_eq(cachedStats[key1].rxBytes, 10); + do_check_eq(cachedStats[key1].txBytes, 20); + do_check_eq(cachedStats[key2].appId, 1); + do_check_eq(cachedStats[key1].serviceType.length, 0); + do_check_eq(cachedStats[key2].networkId, mobile.id); + do_check_eq(cachedStats[key2].networkType, mobile.type); + do_check_eq(cachedStats[key2].date.getTime(), timestamp); + do_check_eq(cachedStats[key2].rxBytes, 10); + do_check_eq(cachedStats[key2].txBytes, 20); + + run_next_test(); + }); + }); +}); + +add_test(function test_saveServiceStats() { + var timestamp = NetworkStatsService.cachedStatsDate.getTime(); + + // Create to fake nsINetworkInfos. As nsINetworkInfo can not be instantiated, + // these two vars will emulate it by filling the properties that will be used. + var wifi = {type: Ci.nsINetworkInfo.NETWORK_TYPE_WIFI, id: "0"}; + var mobile = {type: Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE, id: "1234"}; + + // Insert fake mobile network info in NetworkStatsService + var mobileNetId = NetworkStatsService.getNetworkId(mobile.id, mobile.type); + + NetworkStatsService.updateCachedStats(function (success, msg) { + do_check_eq(success, true); + + var cachedStats = NetworkStatsService.cachedStats; + do_check_eq(Object.keys(cachedStats).length, 0); + + var serviceType = 'FakeType'; + nssProxy.saveServiceStats(serviceType, wifi, timestamp, 10, 20, false, + function (success, message) { + do_check_eq(success, true); + nssProxy.saveServiceStats(serviceType, mobile, timestamp, 10, 20, false, + function (success, message) { + do_check_eq(success, true); + var key1 = 0 + "" + false + "" + serviceType + + NetworkStatsService.getNetworkId(wifi.id, wifi.type); + var key2 = 0 + "" + false + "" + serviceType + mobileNetId + ""; + + do_check_eq(Object.keys(cachedStats).length, 2); + do_check_eq(cachedStats[key1].appId, 0); + do_check_eq(cachedStats[key1].isInBrowser, false); + do_check_eq(cachedStats[key1].serviceType, serviceType); + do_check_eq(cachedStats[key1].networkId, wifi.id); + do_check_eq(cachedStats[key1].networkType, wifi.type); + do_check_eq(cachedStats[key1].date.getTime(), timestamp); + do_check_eq(cachedStats[key1].rxBytes, 10); + do_check_eq(cachedStats[key1].txBytes, 20); + do_check_eq(cachedStats[key2].appId, 0); + do_check_eq(cachedStats[key1].serviceType, serviceType); + do_check_eq(cachedStats[key2].networkId, mobile.id); + do_check_eq(cachedStats[key2].networkType, mobile.type); + do_check_eq(cachedStats[key2].date.getTime(), timestamp); + do_check_eq(cachedStats[key2].rxBytes, 10); + do_check_eq(cachedStats[key2].txBytes, 20); + + run_next_test(); + }); + }); + }); +}); + +add_test(function test_saveStatsWithDifferentDates() { + var today = NetworkStatsService.cachedStatsDate; + var tomorrow = new Date(today.getTime() + (24 * 60 * 60 * 1000)); + + var mobile = {type: Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE, id: "1234"}; + + NetworkStatsService.updateCachedStats(function (success, message) { + do_check_eq(success, true); + + do_check_eq(Object.keys(NetworkStatsService.cachedStats).length, 0); + nssProxy.saveAppStats(1, false, mobile, today.getTime(), 10, 20, false, + function (success, message) { + do_check_eq(success, true); + nssProxy.saveAppStats(2, false, mobile, tomorrow.getTime(), 30, 40, false, + function (success, message) { + do_check_eq(success, true); + + var cachedStats = NetworkStatsService.cachedStats; + var key = 2 + "" + false + "" + + NetworkStatsService.getNetworkId(mobile.id, mobile.type); + do_check_eq(Object.keys(cachedStats).length, 1); + do_check_eq(cachedStats[key].appId, 2); + do_check_eq(cachedStats[key].isInBrowser, false); + do_check_eq(cachedStats[key].networkId, mobile.id); + do_check_eq(cachedStats[key].networkType, mobile.type); + do_check_eq(cachedStats[key].date.getTime(), tomorrow.getTime()); + do_check_eq(cachedStats[key].rxBytes, 30); + do_check_eq(cachedStats[key].txBytes, 40); + + run_next_test(); + }); + }); + }); +}); + +add_test(function test_saveStatsWithMaxCachedTraffic() { + var timestamp = NetworkStatsService.cachedStatsDate.getTime(); + var maxtraffic = NetworkStatsService.maxCachedTraffic; + var wifi = {type: Ci.nsINetworkInfo.NETWORK_TYPE_WIFI, id: "0"}; + + NetworkStatsService.updateCachedStats(function (success, message) { + do_check_eq(success, true); + + var cachedStats = NetworkStatsService.cachedStats; + do_check_eq(Object.keys(cachedStats).length, 0); + nssProxy.saveAppStats(1, false, wifi, timestamp, 10, 20, false, + function (success, message) { + do_check_eq(success, true); + do_check_eq(Object.keys(cachedStats).length, 1); + nssProxy.saveAppStats(1, false, wifi, timestamp, maxtraffic, 20, false, + function (success, message) { + do_check_eq(success, true); + do_check_eq(Object.keys(cachedStats).length, 0); + + run_next_test(); + }); + }); + }); +}); + +add_test(function test_saveAppStats() { + var cachedStats = NetworkStatsService.cachedStats; + var timestamp = NetworkStatsService.cachedStatsDate.getTime(); + + // Create to fake nsINetworkInfo. As nsINetworkInfo can not + // be instantiated, these two vars will emulate it by filling the properties + // that will be used. + var wifi = {type: Ci.nsINetworkInfo.NETWORK_TYPE_WIFI, id: "0"}; + var mobile = {type: Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE, id: "1234"}; + + // Insert fake mobile network interface in NetworkStatsService + var mobileNetId = NetworkStatsService.getNetworkId(mobile.id, mobile.type); + + do_check_eq(Object.keys(cachedStats).length, 0); + + nssProxy.saveAppStats(1, false, wifi, timestamp, 10, 20, false, { notify: + function (success, message) { + do_check_eq(success, true); + var iterations = 10; + var counter = 0; + var callback = function (success, message) { + if (counter == iterations - 1) + run_next_test(); + counter++; + }; + + for (var i = 0; i < iterations; i++) { + nssProxy.saveAppStats(1, false, mobile, timestamp, 10, 20, false, callback); + } + }}); +}); + +function run_test() { + do_get_profile(); + + Cu.import("resource://gre/modules/NetworkStatsService.jsm"); + + // Function convertNetworkInfo of NetworkStatsService causes errors when dealing + // with RIL to get the iccid, so overwrite it. + mokConvertNetworkInfo(); + + run_next_test(); +} diff --git a/dom/network/tests/unit_stats/xpcshell.ini b/dom/network/tests/unit_stats/xpcshell.ini new file mode 100644 index 0000000000..9b69ab7553 --- /dev/null +++ b/dom/network/tests/unit_stats/xpcshell.ini @@ -0,0 +1,7 @@ +[DEFAULT] +head = +tail = + +[test_networkstats_service.js] +[test_networkstats_service_proxy.js] +[test_networkstats_db.js] |