summaryrefslogtreecommitdiffstats
path: root/dom/network
diff options
context:
space:
mode:
Diffstat (limited to 'dom/network')
-rw-r--r--dom/network/Connection.cpp96
-rw-r--r--dom/network/Connection.h80
-rw-r--r--dom/network/Constants.h25
-rw-r--r--dom/network/EthernetManager.js655
-rw-r--r--dom/network/EthernetManager.manifest2
-rw-r--r--dom/network/NetUtils.cpp200
-rw-r--r--dom/network/NetUtils.h75
-rw-r--r--dom/network/NetworkStatsDB.jsm1285
-rw-r--r--dom/network/NetworkStatsManager.js388
-rw-r--r--dom/network/NetworkStatsManager.manifest14
-rw-r--r--dom/network/NetworkStatsService.jsm1171
-rw-r--r--dom/network/NetworkStatsServiceProxy.js90
-rw-r--r--dom/network/NetworkStatsServiceProxy.manifest2
-rw-r--r--dom/network/PTCPServerSocket.ipdl32
-rw-r--r--dom/network/PTCPSocket.ipdl83
-rw-r--r--dom/network/PUDPSocket.ipdl69
-rw-r--r--dom/network/TCPServerSocket.cpp192
-rw-r--r--dom/network/TCPServerSocket.h83
-rw-r--r--dom/network/TCPServerSocketChild.cpp92
-rw-r--r--dom/network/TCPServerSocketChild.h58
-rw-r--r--dom/network/TCPServerSocketParent.cpp164
-rw-r--r--dom/network/TCPServerSocketParent.h60
-rw-r--r--dom/network/TCPSocket.cpp1252
-rw-r--r--dom/network/TCPSocket.h267
-rw-r--r--dom/network/TCPSocketChild.cpp254
-rw-r--r--dom/network/TCPSocketChild.h90
-rw-r--r--dom/network/TCPSocketParent.cpp427
-rw-r--r--dom/network/TCPSocketParent.h91
-rw-r--r--dom/network/Types.h22
-rw-r--r--dom/network/UDPSocket.cpp762
-rw-r--r--dom/network/UDPSocket.h232
-rw-r--r--dom/network/UDPSocketChild.cpp404
-rw-r--r--dom/network/UDPSocketChild.h70
-rw-r--r--dom/network/UDPSocketParent.cpp606
-rw-r--r--dom/network/UDPSocketParent.h84
-rw-r--r--dom/network/interfaces/moz.build19
-rw-r--r--dom/network/interfaces/nsIEthernetManager.idl137
-rw-r--r--dom/network/interfaces/nsIMozNavigatorNetwork.idl13
-rw-r--r--dom/network/interfaces/nsINetworkStatsServiceProxy.idl64
-rw-r--r--dom/network/interfaces/nsITCPSocketCallback.idl59
-rw-r--r--dom/network/interfaces/nsIUDPSocketChild.idl78
-rw-r--r--dom/network/moz.build76
-rw-r--r--dom/network/tests/add_task.js83
-rw-r--r--dom/network/tests/chrome.ini12
-rw-r--r--dom/network/tests/file_udpsocket_iframe.html23
-rw-r--r--dom/network/tests/marionette/head.js552
-rw-r--r--dom/network/tests/marionette/manifest.ini13
-rw-r--r--dom/network/tests/marionette/test_ethernet_add_interface.js16
-rw-r--r--dom/network/tests/marionette/test_ethernet_connect_with_dhcp.js26
-rw-r--r--dom/network/tests/marionette/test_ethernet_connect_with_static_ip.js33
-rw-r--r--dom/network/tests/marionette/test_ethernet_disable.js17
-rw-r--r--dom/network/tests/marionette/test_ethernet_disconnect.js25
-rw-r--r--dom/network/tests/marionette/test_ethernet_enable.js17
-rw-r--r--dom/network/tests/marionette/test_ethernet_ip_mode_change.js43
-rw-r--r--dom/network/tests/marionette/test_ethernet_reconnect_with_dhcp.js29
-rw-r--r--dom/network/tests/marionette/test_ethernet_reconnect_with_static_ip.js36
-rw-r--r--dom/network/tests/marionette/test_ethernet_remove_interface.js16
-rw-r--r--dom/network/tests/mochitest.ini8
-rw-r--r--dom/network/tests/tcpsocket_test.jsm20
-rw-r--r--dom/network/tests/test_network_basics.html38
-rw-r--r--dom/network/tests/test_tcpsocket_client_and_server_basics.html51
-rw-r--r--dom/network/tests/test_tcpsocket_client_and_server_basics.js423
-rw-r--r--dom/network/tests/test_tcpsocket_default_permissions.html38
-rw-r--r--dom/network/tests/test_tcpsocket_enabled_no_perm.html30
-rw-r--r--dom/network/tests/test_tcpsocket_enabled_with_perm.html35
-rw-r--r--dom/network/tests/test_tcpsocket_jsm.html25
-rw-r--r--dom/network/tests/test_tcpsocket_legacy.html59
-rw-r--r--dom/network/tests/test_udpsocket.html405
-rw-r--r--dom/network/tests/unit_stats/test_networkstats_db.js1093
-rw-r--r--dom/network/tests/unit_stats/test_networkstats_service.js290
-rw-r--r--dom/network/tests/unit_stats/test_networkstats_service_proxy.js233
-rw-r--r--dom/network/tests/unit_stats/xpcshell.ini7
72 files changed, 13619 insertions, 0 deletions
diff --git a/dom/network/Connection.cpp b/dom/network/Connection.cpp
new file mode 100644
index 000000000..3bf6f4038
--- /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 000000000..907aea144
--- /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 000000000..08ce9d29e
--- /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 000000000..4b11e5666
--- /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 000000000..d25a069e1
--- /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 000000000..78c5be802
--- /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 000000000..4af365406
--- /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 000000000..aa74d40ad
--- /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 000000000..b963aba2b
--- /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 000000000..8e8700910
--- /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 000000000..4b6d69498
--- /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 000000000..f3df4344d
--- /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 000000000..24f09f088
--- /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 000000000..fc92f0146
--- /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 000000000..5c9c1c862
--- /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 000000000..a104de308
--- /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 000000000..5e6f778a3
--- /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 000000000..45ab6e068
--- /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 000000000..f3218ced6
--- /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 000000000..77dbc59c6
--- /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 000000000..8f98d8b78
--- /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 000000000..071183366
--- /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 000000000..4eb2f72f6
--- /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 000000000..e98c03ca5
--- /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 000000000..8eb19a1db
--- /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 000000000..af233d311
--- /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 000000000..54167234e
--- /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 000000000..e44e340bb
--- /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 000000000..5f81a8440
--- /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 000000000..e275e3902
--- /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 000000000..12427d1dd
--- /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 000000000..6e374ce31
--- /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 000000000..5794b7d46
--- /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 000000000..904a995ed
--- /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 000000000..4b828e013
--- /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 000000000..add687542
--- /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 000000000..2b92dc88f
--- /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 000000000..1d667aada
--- /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 000000000..cd6765c68
--- /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 000000000..5ab85dcc7
--- /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 000000000..3a07fae66
--- /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 000000000..63e5c75c0
--- /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 000000000..3028afdb7
--- /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 000000000..ca1c3f79a
--- /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 000000000..1e124552d
--- /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 000000000..edbf2dd85
--- /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 000000000..23f184bae
--- /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 000000000..d628e7705
--- /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 000000000..57c2df9c2
--- /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 000000000..3adc37b23
--- /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 000000000..9c3525faa
--- /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 000000000..73f6aa3c5
--- /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 000000000..f5578a44f
--- /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 000000000..5db2049be
--- /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 000000000..96719c152
--- /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 000000000..91f25a471
--- /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 000000000..c7fb0e81b
--- /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 000000000..76fd55fe4
--- /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 000000000..837a52262
--- /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 000000000..e3c3eb25d
--- /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 000000000..4d304a978
--- /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 000000000..f47689b90
--- /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 000000000..19c44b7b9
--- /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 000000000..ae1313113
--- /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 000000000..44b85ee3e
--- /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 000000000..508c3c956
--- /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 000000000..3b35583ab
--- /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 000000000..f1d03824a
--- /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 000000000..50a5dc186
--- /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 000000000..8c43a9b54
--- /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 000000000..131b886d0
--- /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 000000000..9b69ab755
--- /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]