Skip to content

Instantly share code, notes, and snippets.

@fzzzy
Created August 10, 2012 19:01
Show Gist options
  • Save fzzzy/3316875 to your computer and use it in GitHub Desktop.
Save fzzzy/3316875 to your computer and use it in GitHub Desktop.
tcpsocket wip-0.13
diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -387,16 +387,19 @@
// WebSettings
pref("dom.mozSettings.enabled", true);
// controls if we want camera support
pref("device.camera.enabled", true);
pref("media.realtime_decoder.enabled", true);
+// TCPSocket
+pref("dom.mozTCPSocket.enabled", true);
+
// "Preview" landing of bug 710563, which is bogged down in analysis
// of talos regression. This is a needed change for higher-framerate
// CSS animations, and incidentally works around an apparent bug in
// our handling of requestAnimationFrame() listeners, which are
// supposed to enable this REPEATING_PRECISE_CAN_SKIP behavior. The
// secondary bug isn't really worth investigating since it's obseleted
// by bug 710563.
pref("layout.frame_rate.precise", true);
diff --git a/b2g/installer/package-manifest.in b/b2g/installer/package-manifest.in
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -149,16 +149,17 @@
@BINPATH@/components/content_html.xpt
@BINPATH@/components/content_xslt.xpt
@BINPATH@/components/content_xtf.xpt
@BINPATH@/components/cookie.xpt
@BINPATH@/components/directory.xpt
@BINPATH@/components/docshell.xpt
@BINPATH@/components/dom.xpt
@BINPATH@/components/dom_activities.xpt
+@BINPATH@/components/dom_socket.xpt
@BINPATH@/components/dom_apps.xpt
@BINPATH@/components/dom_base.xpt
#ifdef MOZ_B2G_RIL
@BINPATH@/components/dom_telephony.xpt
@BINPATH@/components/dom_wifi.xpt
@BINPATH@/components/dom_system_gonk.xpt
#endif
@BINPATH@/components/dom_battery.xpt
@@ -479,16 +480,19 @@
@BINPATH@/components/SystemMessageManager.manifest
@BINPATH@/components/Activities.manifest
@BINPATH@/components/ActivityOptions.js
@BINPATH@/components/ActivityProxy.js
@BINPATH@/components/ActivityRequestHandler.js
@BINPATH@/components/ActivityWrapper.js
+@BINPATH@/components/TCPSocket.js
+@BINPATH@/components/TCPSocket.manifest
+
@BINPATH@/components/AppProtocolHandler.js
@BINPATH@/components/AppProtocolHandler.manifest
; Modules
@BINPATH@/modules/*
; Safe Browsing
@BINPATH@/components/nsURLClassifier.manifest
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -478,16 +478,20 @@
@BINPATH@/components/nsDOMIdentity.js
@BINPATH@/components/nsIDService.js
@BINPATH@/components/Identity.manifest
@BINPATH@/components/ContactManager.js
@BINPATH@/components/ContactManager.manifest
@BINPATH@/components/AlarmsManager.js
@BINPATH@/components/AlarmsManager.manifest
+@BINPATH@/components/TCPSocket.js
+@BINPATH@/components/TCPSocket.manifest
+@BINPATH@/components/dom_socket.xpt
+
#ifdef ENABLE_MARIONETTE
@BINPATH@/chrome/marionette@JAREXT@
@BINPATH@/chrome/marionette.manifest
@BINPATH@/components/MarionetteComponents.manifest
@BINPATH@/components/marionettecomponent.js
#endif
; Modules
diff --git a/dom/network/interfaces/Makefile.in b/dom/network/interfaces/Makefile.in
--- a/dom/network/interfaces/Makefile.in
+++ b/dom/network/interfaces/Makefile.in
@@ -14,11 +14,12 @@
include $(topsrcdir)/dom/dom-config.mk
XPIDLSRCS = \
nsIDOMNavigatorNetwork.idl \
nsIDOMConnection.idl \
nsIDOMMobileConnection.idl \
nsIMobileConnectionProvider.idl \
nsIDOMUSSDReceivedEvent.idl \
+ nsIDOMTCPSocket.idl \
$(NULL)
include $(topsrcdir)/config/rules.mk
diff --git a/dom/network/interfaces/nsIDOMTCPSocket.idl b/dom/network/interfaces/nsIDOMTCPSocket.idl
new file mode 100644
--- /dev/null
+++ b/dom/network/interfaces/nsIDOMTCPSocket.idl
@@ -0,0 +1,219 @@
+/* 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 socket (no server sockets yet)
+ * 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"
+#include "nsIDOMEvent.idl"
+
+// Bug 731746 - Allow chrome JS object to implement nsIDOMEventTarget
+// nsITCPSocket should be an nsIEventTarget but js objects
+// cannot be an nsIEventTarget yet
+// #include "nsIEventTarget.idl"
+
+// Bug 723206 - Constructors implemented in JS from IDL should be
+// allowed to have arguments
+//
+// Once bug 723206 will be fixed, this method could be replaced by
+// arguments when instantiating a TCPSocket object. For example it will
+// be possible to do (similarly to the WebSocket API):
+// var s = new MozTCPSocket(host, port);
+
+[scriptable, uuid(b82e17da-6476-11e1-8813-57a2ffe9e42c)]
+interface nsIDOMTCPSocket : nsISupports
+{
+ /**
+ * Create and return a socket object which will attempt to connect to
+ * the given host and port.
+ *
+ * @param host The hostname of the server to connect to.
+ * @param port The port to connect to.
+ * @param options An object specifying one or more parameters which
+ * determine the details of the socket.
+ *
+ * useSSL: true to create an SSL socket. Defaults to false.
+ *
+ * binaryType: "arraybuffer" to use UInt8 array
+ * instances in the ondata callback and as the argument
+ * to send. Defaults to "string", to use JavaScript strings.
+ *
+ * @return The new TCPSocket instance.
+ */
+ nsIDOMTCPSocket open(in DOMString host, in unsigned short port, [optional] in jsval options);
+
+ /**
+ * The host of this socket object.
+ */
+ readonly attribute DOMString host;
+
+ /**
+ * The port of this socket object.
+ */
+ readonly attribute unsigned short port;
+
+ /**
+ * True if this socket object is an SSL socket.
+ */
+ readonly attribute boolean ssl;
+
+ /**
+ * The number of bytes which have previously been buffered by calls to
+ * send on this socket.
+ */
+ readonly attribute unsigned long bufferedAmount;
+
+ /**
+ * Pause reading incoming data and invocations of the ondata handler until
+ * resume is called.
+ */
+ void suspend();
+
+ /**
+ * Resume reading incoming data and invoking ondata as usual.
+ */
+ void resume();
+
+ /**
+ * Close the socket.
+ */
+ void close();
+
+ /**
+ * Write data to the socket.
+ *
+ * @param data The data to write to the socket. If
+ * binaryType: "arraybuffer" was passed in the options
+ * object, then this object should be an Uint8Array instance.
+ * If binaryType: "string" was passed, or if no binaryType
+ * option was specified, then this object should be an
+ * ordinary JavaScript string.
+ *
+ * @return Send returns true or false as a hint to the caller that
+ * they may either continue sending more data immediately, or
+ * may want to wait until the other side has read some of the
+ * data which has already been written to the socket before
+ * buffering more. If send returns true, then less than 64k
+ * has been buffered and it's safe to immediately write more.
+ * If send returns false, then more than 64k has been buffered,
+ * and the caller may wish to wait until the ondrain event
+ * handler has been called before buffering more data by more
+ * calls to send.
+ */
+ boolean send(in jsval data);
+
+ /**
+ * The readyState attribute indicates which state the socket is currently
+ * in. The state will be either CONNECTING, OPEN, CLOSING, or CLOSED.
+ */
+ readonly attribute DOMString readyState;
+ readonly attribute DOMString CONNECTING;
+ readonly attribute DOMString OPEN;
+ readonly attribute DOMString CLOSING;
+ readonly attribute DOMString CLOSED;
+
+ /**
+ * The binaryType attribute indicates which mode this socket uses for
+ * sending and receiving data. If the binaryType: "arraybuffer" option
+ * was passed to the open method that created this socket, binaryType
+ * will be "arraybuffer". Otherwise, it will be "string".
+ */
+ readonly attribute DOMString binaryType;
+
+ /**
+ * The onopen event handler is called when the connection to the server
+ * has been established. If the connection is refused, onerror will be
+ * called, instead.
+ */
+ attribute jsval onopen;
+
+ /**
+ * After send has buffered more than 64k of data, it returns false to
+ * indicate that the client should pause before sending more data, to
+ * avoid accumulating large buffers. This is only advisory, and the client
+ * is free to ignore it and buffer as much data as desired, but if reducing
+ * the size of buffers is important (especially for a streaming application)
+ * ondrain will be called once the previously-buffered data has been written
+ * to the network, at which point the client can resume calling send again.
+ */
+ attribute jsval ondrain;
+
+ /**
+ * The ondata handler will be called repeatedly and asynchronously after
+ * onopen has been called, every time some data was available from the server
+ * and was read. If binaryType: "arraybuffer" was passed to open, the data
+ * attribute of the event object will be an Uint8Array. If not, it will be a
+ * normal JavaScript string.
+ *
+ * At any time, the client may choose to pause reading and receiving ondata
+ * callbacks, by calling the socket's suspend() method. Further invocations
+ * of ondata will be paused until resume() is called.
+ */
+ attribute jsval ondata;
+
+ /**
+ * The onerror handler will be called when there is an error. The data
+ * attribute of the event passed to the onerror handler will have a
+ * description of the kind of error.
+ *
+ * If onerror is called before onopen, the error was connection refused,
+ * and onclose will not be called. If onerror is called after onopen,
+ * the connection was lost, and onclose will be called after onerror.
+ */
+ attribute jsval onerror;
+
+ /**
+ * The onclose handler is called once the underlying network socket
+ * has been closed, either by the server, or by the client calling
+ * close.
+ *
+ * If onerror was not called before onclose, then either side cleanly
+ * closed the connection.
+ */
+ attribute jsval onclose;
+};
+
+/**
+ * nsITCPSocketEvent is the event object which is passed as the
+ * first argument to all the event handler callbacks. It contains
+ * the socket that was associated with the event, the type of event,
+ * and the data associated with the event (if any).
+ */
+
+[scriptable, uuid(0f2abcca-b483-4539-a3e8-345707f75c44)]
+interface nsITCPSocketEvent : nsISupports {
+ /**
+ * The socket object which produced this event.
+ */
+ readonly attribute nsIDOMTCPSocket socket;
+
+ /**
+ * The type of this event. One of:
+ *
+ * onopen
+ * onerror
+ * ondata
+ * ondrain
+ * onclose
+ */
+ readonly attribute DOMString type;
+
+ /**
+ * The data related to this event, if any. In the ondata callback,
+ * data will be the bytes read from the network; if the binaryType
+ * of the socket was "arraybuffer", this value will be of type Uint8Array;
+ * otherwise, it will be a normal JavaScript string.
+ *
+ * In the onerror callback, data will be a string with a description
+ * of the error.
+ *
+ * In the other callbacks, data will be an empty string.
+ */
+ readonly attribute jsval data;
+};
+
diff --git a/dom/network/src/Makefile.in b/dom/network/src/Makefile.in
--- a/dom/network/src/Makefile.in
+++ b/dom/network/src/Makefile.in
@@ -8,16 +8,24 @@
VPATH = $(srcdir)
include $(DEPTH)/config/autoconf.mk
LIBRARY_NAME = dom_network_s
LIBXUL_LIBRARY = 1
FORCE_STATIC_LIB = 1
+XPIDL_MODULE = dom_socket
+GRE_MODULE = 1
+
+EXTRA_COMPONENTS = \
+ TCPSocket.js \
+ TCPSocket.manifest \
+ $(NULL)
+
include $(topsrcdir)/dom/dom-config.mk
EXPORTS_NAMESPACES = mozilla/dom/network
EXPORTS_mozilla/dom/network = \
Utils.h \
Types.h \
Constants.h \
diff --git a/dom/network/src/TCPSocket.js b/dom/network/src/TCPSocket.js
new file mode 100644
--- /dev/null
+++ b/dom/network/src/TCPSocket.js
@@ -0,0 +1,569 @@
+/* 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+const CC = Components.Constructor;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const InputStreamPump = CC(
+ "@mozilla.org/network/input-stream-pump;1", "nsIInputStreamPump", "init"),
+ AsyncStreamCopier = CC(
+ "@mozilla.org/network/async-stream-copier;1", "nsIAsyncStreamCopier", "init"),
+ ScriptableInputStream = CC(
+ "@mozilla.org/scriptableinputstream;1", "nsIScriptableInputStream", "init"),
+ BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream", "setInputStream"),
+ StringInputStream = CC(
+ '@mozilla.org/io/string-input-stream;1', 'nsIStringInputStream'),
+ MultiplexInputStream = CC(
+ '@mozilla.org/io/multiplex-input-stream;1', 'nsIMultiplexInputStream');
+
+const kCONNECTING = 'connecting';
+const kOPEN = 'open';
+const kCLOSING = 'closing';
+const kCLOSED = 'closed';
+
+const BUFFER_SIZE = 65536;
+
+let socketsByWindowID = {};
+
+/*
+ * Debug logging function
+ */
+
+let debug = true;
+function LOG(msg) {
+ if (debug)
+ dump("TCPSocket: " + msg + "\n");
+}
+
+/*
+ * nsITCPSocketEvent object
+ */
+
+function TCPSocketEvent(type, sock, data) {
+ this._type = type;
+ this._socket = sock;
+ this._data = data;
+}
+
+TCPSocketEvent.prototype = {
+ __exposedProps__: {
+ type: 'r',
+ socket: 'r',
+ data: 'r'
+ },
+ get type() {
+ return this._type;
+ },
+ get socket() {
+ return this._socket;
+ },
+ get data() {
+ return this._data;
+ }
+}
+
+/*
+ * nsIDOMTCPSocket object
+ */
+
+function TCPSocket() {
+ this._readyState = kCLOSED;
+
+ this._onopen = null;
+ this._ondrain = null;
+ this._ondata = null;
+ this._onerror = null;
+ this._onclose = null;
+
+ this._binaryType = "string";
+
+ this._host = "";
+ this._port = 0;
+ this._ssl = false;
+}
+
+TCPSocket.prototype = {
+ __exposedProps__: {
+ open: 'r',
+ host: 'r',
+ port: 'r',
+ ssl: 'r',
+ bufferedAmount: 'r',
+ suspend: 'r',
+ resume: 'r',
+ close: 'r',
+ send: 'r',
+ readyState: 'r',
+ CONNECTING: 'r',
+ OPEN: 'r',
+ CLOSING: 'r',
+ CLOSED: 'r',
+ binaryType: 'r',
+ onopen: 'rw',
+ ondrain: 'rw',
+ ondata: 'rw',
+ onerror: 'rw',
+ onclose: 'rw'
+ },
+ // Constants
+ CONNECTING: kCONNECTING,
+ OPEN: kOPEN,
+ CLOSING: kCLOSING,
+ CLOSED: kCLOSED,
+
+ // The binary type, "string" or "arraybuffer"
+ _binaryType: null,
+
+ // Internal
+ _hasPrivileges: null,
+
+ // Raw socket streams
+ _transport: null,
+ _socketInputStream: null,
+ _socketOutputStream: null,
+
+ // Input stream machinery
+ _inputStreamPump: null,
+ _inputStreamScriptable: null,
+ _inputStreamBinary: null,
+
+ // Output stream machinery
+ _multiplexStream: null,
+ _multiplexStreamCopier: null,
+
+ _asyncCopierActive: false,
+ _waitingForDrain: false,
+ _suspendCount: 0,
+
+ // Public accessors.
+ get readyState() {
+ return this._readyState;
+ },
+ get binaryType() {
+ return this._binaryType;
+ },
+ get host() {
+ return this._host;
+ },
+ get port() {
+ return this._port;
+ },
+ get ssl() {
+ return this._ssl;
+ },
+ get bufferedAmount() {
+ return this._multiplexStream.available();
+ },
+ get onopen() {
+ return this._onopen;
+ },
+ set onopen(f) {
+ this._onopen = f;
+ },
+ get ondrain() {
+ return this._ondrain;
+ },
+ set ondrain(f) {
+ this._ondrain = f;
+ },
+ get ondata() {
+ return this._ondata;
+ },
+ set ondata(f) {
+ this._ondata = f;
+ },
+ get onerror() {
+ return this._onerror;
+ },
+ set onerror(f) {
+ this._onerror = f;
+ },
+ get onclose() {
+ return this._onclose;
+ },
+ set onclose(f) {
+ this._onclose = f;
+ },
+
+ // Helper methods.
+ _createTransport: function ts_createTransport(host, port, sslMode) {
+ let options, optlen;
+ if (sslMode) {
+ options = [sslMode];
+ optlen = 1;
+ } else {
+ options = null;
+ optlen = 0;
+ }
+ return Cc["@mozilla.org/network/socket-transport-service;1"]
+ .getService(Ci.nsISocketTransportService)
+ .createTransport(options, optlen, host, port, null);
+ },
+
+ _ensureCopying: function ts_ensureCopying() {
+ let self = this;
+ if (this._asyncCopierActive) {
+ return;
+ }
+ this._asyncCopierActive = true;
+ this._multiplexStreamCopier.asyncCopy({
+ onStartRequest: function ts_output_onStartRequest() {
+ },
+ onStopRequest: function ts_output_onStopRequest(request, context, status) {
+ self._asyncCopierActive = false;
+ self._multiplexStream.removeStream(0);
+
+ if (status) {
+ this._readyState = kCLOSED;
+ let err = new Error("Connection closed while writing: " + status);
+ err.status = status;
+ this.callListener("onerror", err);
+ this.callListener("onclose");
+ return;
+ }
+
+ if (self._multiplexStream.count) {
+ self._ensureCopying();
+ } else {
+ if (self._waitingForDrain) {
+ self._waitingForDrain = false;
+ self.callListener("ondrain");
+ }
+ if (self._readyState === kCLOSING) {
+ self._socketOutputStream.close();
+ self._readyState = kCLOSED;
+ self.callListener("onclose");
+ }
+ }
+ }
+ }, null);
+ },
+
+ callListener: function ts_callListener(type, data) {
+ if (!this[type])
+ return;
+
+ this[type].call(null, new TCPSocketEvent(type, this, data || ""));
+ },
+
+ init: function ts_init(aWindow) {
+ if (!Services.prefs.getBoolPref("dom.mozTCPSocket.enabled"))
+ return null;
+
+ let principal = aWindow.document.nodePrincipal;
+ let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Ci.nsIScriptSecurityManager);
+
+ let perm = principal == secMan.getSystemPrincipal()
+ ? Ci.nsIPermissionManager.ALLOW_ACTION
+ : Services.perms.testExactPermissionFromPrincipal(principal, "tcp-socket");
+
+ this._hasPrivileges = perm == Ci.nsIPermissionManager.ALLOW_ACTION;
+
+ let util = aWindow.QueryInterface(
+ Ci.nsIInterfaceRequestor
+ ).getInterface(Ci.nsIDOMWindowUtils);
+
+ this.innerWindowID = util.currentInnerWindowID;
+ LOG("window init: " + this.innerWindowID);
+ Services.obs.addObserver(this, "inner-window-destroyed", true);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ // TODO why is this code not getting called?
+ if (aTopic == "inner-window-destroyed") {
+ let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ if (wId == this.innerWindowID) {
+ LOG("inner-window-destroyed: " + this.innerWindowID);
+ if (socketsByWindowID[this.innerWindowID] === undefined) {
+ return;
+ }
+
+ let toClose = socketsByWindowID[this.innerWindowID];
+ delete socketsByWindowID[this.innerWindowID];
+ for (var i = 0; i < toClose.length; i++) {
+ let closeMe = toClose[i];
+
+ // This window is now dead, so we want to clear the callbacks
+ // so that we don't get a "can't access dead object" when the
+ // underlying stream goes to tell us that we are closed
+ closeMe.onopen = null;
+ closeMe.ondrain = null;
+ closeMe.ondata = null;
+ closeMe.onerror = null;
+ closeMe.onclose = null;
+
+ // Clean up our socket
+ closeMe.close();
+ }
+ }
+ }
+ },
+
+ // nsIDOMTCPSocket
+ open: function ts_open(host, port, options) {
+ // in the testing case, init won't be called and
+ // hasPrivileges will be null. We want to proceed to test.
+ if (this._hasPrivileges !== true && this._hasPrivileges !== null) {
+ throw new Error("TCPSocket does not have permission in this context.\n");
+ }
+ let that = new TCPSocket();
+
+ if (socketsByWindowID[this.innerWindowID] === undefined) {
+ socketsByWindowID[this.innerWindowID] = [];
+ }
+ socketsByWindowID[this.innerWindowID].push(that);
+
+ LOG("startup called\n");
+ LOG("Host info: " + host + ":" + port + "\n");
+
+ that._readyState = kCONNECTING;
+ that._host = host;
+ that._port = port;
+ if (options !== undefined) {
+ if (options.useSSL) {
+ that._ssl = 'ssl';
+ } else {
+ that._ssl = false;
+ }
+ that._binaryType = options.binaryType || that._binaryType;
+ }
+
+ LOG("SSL: " + that.ssl + "\n");
+
+ let transport = that._transport = this._createTransport(host, port, that._ssl);
+ transport.setEventSink(that, Services.tm.currentThread);
+ transport.securityCallbacks = new SecurityCallbacks(that);
+
+ that._socketInputStream = transport.openInputStream(0, 0, 0);
+ that._socketOutputStream = transport.openOutputStream(
+ Ci.nsITransport.OPEN_UNBUFFERED, 0, 0);
+
+ // If the other side is not listening, we will
+ // get an onInputStreamReady callback where available
+ // raises to indicate the connection was refused.
+ that._socketInputStream.asyncWait(
+ that, that._socketInputStream.WAIT_CLOSURE_ONLY, 0, Services.tm.currentThread);
+
+ if (that._binaryType === "arraybuffer") {
+ that._inputStreamBinary = new BinaryInputStream(that._socketInputStream);
+ } else {
+ that._inputStreamScriptable = new ScriptableInputStream(that._socketInputStream);
+ }
+
+ that._multiplexStream = new MultiplexInputStream();
+
+ that._multiplexStreamCopier = new AsyncStreamCopier(
+ that._multiplexStream,
+ that._socketOutputStream,
+ // (nsSocketTransport uses gSocketTransportService)
+ Cc["@mozilla.org/network/socket-transport-service;1"]
+ .getService(Ci.nsIEventTarget),
+ /* source buffered */ true, /* sink buffered */ false,
+ BUFFER_SIZE, /* close source*/ false, /* close sink */ false);
+
+ return that;
+ },
+
+ close: function ts_close() {
+ if (this._readyState === kCLOSED || this._readyState === kCLOSING)
+ return;
+
+ LOG("close called\n");
+ this._readyState = kCLOSING;
+
+ if (!this._multiplexStream.count) {
+ this._socketOutputStream.close();
+ }
+ this._socketInputStream.close();
+ },
+
+ send: function ts_send(data) {
+ if (this._readyState !== kOPEN) {
+ throw new Error("Socket not open.");
+ }
+
+ let new_stream = new StringInputStream();
+ if (this._binaryType === "arraybuffer") {
+ // It would be really nice if there were an interface
+ // that took an ArrayBuffer like StringInputStream takes
+ // a string. There is one, but only in C++ and not exposed
+ // to js as far as I can tell
+ var dataLen = data.length;
+ var offset = 0;
+ var result = "";
+ while (dataLen) {
+ var fragmentLen = dataLen;
+ if (fragmentLen > 32768)
+ fragmentLen = 32768;
+ dataLen -= fragmentLen;
+
+ var fragment = data.subarray(offset, offset + fragmentLen);
+ offset += fragmentLen;
+ result += String.fromCharCode.apply(null, fragment);
+ }
+ data = result;
+ }
+ var newBufferedAmount = this.bufferedAmount + data.length;
+ new_stream.setData(data, data.length);
+ this._multiplexStream.appendStream(new_stream);
+
+ if (newBufferedAmount >= BUFFER_SIZE) {
+ // 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.
+ this._waitingForDrain = true;
+ }
+
+ this._ensureCopying();
+ return newBufferedAmount < BUFFER_SIZE;
+ },
+
+ suspend: function ts_suspend() {
+ if (this._inputStreamPump) {
+ this._inputStreamPump.suspend();
+ } else {
+ ++this._suspendCount;
+ }
+ },
+
+ resume: function ts_resume() {
+ if (this._inputStreamPump) {
+ this._inputStreamPump.resume();
+ } else {
+ --this._suspendCount;
+ }
+ },
+
+ // nsITransportEventSink (Triggered by transport.setEventSink)
+ onTransportStatus: function ts_onTransportStatus(
+ transport, status, progress, max) {
+
+ if (status === Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
+ this._readyState = kOPEN;
+ this.callListener("onopen");
+
+ this._inputStreamPump = new InputStreamPump(
+ this._socketInputStream, -1, -1, 0, 0, false
+ );
+
+ while (this._suspendCount--) {
+ this._inputStreamPump.suspend();
+ }
+
+ this._inputStreamPump.asyncRead(this, null);
+ }
+ },
+
+ // nsIAsyncInputStream (Triggered by _socketInputStream.asyncWait)
+ // Only used for detecting connection refused
+ onInputStreamReady: function ts_onInputStreamReady(input) {
+ try {
+ input.available();
+ } catch (e) {
+ this.callListener("onerror", new Error("Connection refused"));
+ }
+ },
+
+ // nsIRequestObserver (Triggered by _inputStreamPump.asyncRead)
+ onStartRequest: function ts_onStartRequest(request, context) {
+ },
+
+ // nsIRequestObserver (Triggered by _inputStreamPump.asyncRead)
+ onStopRequest: function ts_onStopRequest(request, context, status) {
+ let buffered_output = this._multiplexStream.count !== 0;
+
+ this._inputStreamPump = null;
+
+ if (buffered_output && !status) {
+ // 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;
+ }
+
+ this._readyState = kCLOSED;
+
+ if (status) {
+ let err = new Error("Connection closed: " + status);
+ err.status = status;
+ this.callListener("onerror", err);
+ }
+
+ this.callListener("onclose");
+ },
+
+ // nsIStreamListener (Triggered by _inputStreamPump.asyncRead)
+ onDataAvailable: function ts_onDataAvailable(request, context, inputStream, offset, count) {
+ if (this._binaryType === "arraybuffer") {
+ let ua = new Uint8Array(count);
+ ua.set(this._inputStreamBinary.readByteArray(count));
+ this.callListener("ondata", ua);
+ } else {
+ this.callListener("ondata", this._inputStreamScriptable.read(count));
+ }
+ },
+
+ classID: Components.ID("{cda91b22-6472-11e1-aa11-834fec09cd0a}"),
+
+ classInfo: XPCOMUtils.generateCI({
+ classID: Components.ID("{cda91b22-6472-11e1-aa11-834fec09cd0a}"),
+ contractID: "@mozilla.org/tcp-socket;1",
+ classDescription: "Client TCP Socket",
+ interfaces: [
+ Ci.nsIDOMTCPSocket,
+ Ci.nsIDOMGlobalPropertyInitializer,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference
+ ],
+ flags: Ci.nsIClassInfo.DOM_OBJECT,
+ }),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIDOMTCPSocket,
+ Ci.nsIDOMGlobalPropertyInitializer,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference
+ ])
+}
+
+
+function SecurityCallbacks(socket) {
+ this._socket = socket;
+}
+
+SecurityCallbacks.prototype = {
+ notifyCertProblem: function sc_notifyCertProblem(socketInfo, status,
+ targetSite) {
+ this._socket.callListener("onerror", status);
+ this._socket.close();
+ return true;
+ },
+
+ getInterface: function sc_getInterface(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIBadCertListener2,
+ Ci.nsIInterfaceRequestor,
+ Ci.nsISupports
+ ])
+};
+
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([TCPSocket]);
diff --git a/dom/network/src/TCPSocket.manifest b/dom/network/src/TCPSocket.manifest
new file mode 100644
--- /dev/null
+++ b/dom/network/src/TCPSocket.manifest
@@ -0,0 +1,4 @@
+# TCPSocket.js
+component {cda91b22-6472-11e1-aa11-834fec09cd0a} TCPSocket.js
+contract @mozilla.org/tcp-socket;1 {cda91b22-6472-11e1-aa11-834fec09cd0a}
+category JavaScript-navigator-property MozTCPSocket @mozilla.org/tcp-socket;1
diff --git a/dom/network/tests/Makefile.in b/dom/network/tests/Makefile.in
--- a/dom/network/tests/Makefile.in
+++ b/dom/network/tests/Makefile.in
@@ -13,9 +13,13 @@
DIRS = \
$(NULL)
MOCHITEST_FILES = \
test_network_basics.html \
$(NULL)
+MODULE = test_dom_socket
+
+XPCSHELL_TESTS = unit
+
include $(topsrcdir)/config/rules.mk
diff --git a/dom/network/tests/unit/test_tcpsocket.js b/dom/network/tests/unit/test_tcpsocket.js
new file mode 100644
--- /dev/null
+++ b/dom/network/tests/unit/test_tcpsocket.js
@@ -0,0 +1,479 @@
+/**
+ * Test TCPSocket.js by creating an XPCOM-style server socket, then sending
+ * data in both directions and making sure each side receives their data
+ * correctly and with the proper events.
+ *
+ * This test is derived from netwerk/test/unit/test_socks.js, except we don't
+ * involve a subprocess.
+ *
+ * Future work:
+ * - SSL. see https://bugzilla.mozilla.org/show_bug.cgi?id=466524
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=662180
+ * Alternatively, mochitests could be used.
+ * - Testing overflow logic.
+ *
+ **/
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+const CC = Components.Constructor;
+
+/**
+ *
+ * Constants
+ *
+ */
+
+// Some binary data to send.
+const DATA_ARRAY = [0, 255, 254, 0, 1, 2, 3, 0, 255, 255, 254, 0],
+ TYPED_DATA_ARRAY = new Uint8Array(DATA_ARRAY),
+ HELLO_WORLD = "hlo wrld. ",
+ BIG_ARRAY = new Array(524288),
+ BIG_ARRAY_2 = new Array(524288);
+
+for (var i_big = 0; i_big < BIG_ARRAY.length; i_big++) {
+ BIG_ARRAY[i_big] = Math.floor(Math.random() * 256);
+ BIG_ARRAY_2[i_big] = Math.floor(Math.random() * 256);
+}
+
+const BIG_TYPED_ARRAY = new Uint8Array(BIG_ARRAY),
+ BIG_TYPED_ARRAY_2 = new Uint8Array(BIG_ARRAY_2);
+
+const ServerSocket = CC("@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"),
+ InputStreamPump = CC("@mozilla.org/network/input-stream-pump;1",
+ "nsIInputStreamPump",
+ "init"),
+ BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"),
+ BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"),
+ TCPSocket = new (CC("@mozilla.org/tcp-socket;1",
+ "nsIDOMTCPSocket"))();
+
+/**
+ *
+ * Helper functions
+ *
+ */
+
+/**
+ * Spin up a listening socket and associate at most one live, accepted socket
+ * with ourselves.
+ */
+function TestServer() {
+ this.listener = ServerSocket(-1, true, -1);
+ do_print('server: listening on', this.listener.port);
+ this.listener.asyncListen(this);
+
+ this.binaryInput = null;
+ this.input = null;
+ this.binaryOutput = null;
+ this.output = null;
+
+ this.onaccept = null;
+ this.ondata = null;
+ this.onclose = null;
+}
+
+TestServer.prototype = {
+ onSocketAccepted: function(socket, trans) {
+ if (this.input)
+ do_throw("More than one live connection!?");
+
+ do_print('server: got client connection');
+ this.input = trans.openInputStream(0, 0, 0);
+ this.binaryInput = new BinaryInputStream(this.input);
+ this.output = trans.openOutputStream(0, 0, 0);
+ this.binaryOutput = new BinaryOutputStream(this.output);
+
+ new InputStreamPump(this.input, -1, -1, 0, 0, false).asyncRead(this, null);
+
+ if (this.onaccept)
+ this.onaccept();
+ else
+ do_throw("Received unexpected connection!");
+ },
+
+ onStopListening: function(socket) {
+ },
+
+ onDataAvailable: function(request, context, inputStream, offset, count) {
+ var readData = this.binaryInput.readByteArray(count);
+ if (this.ondata) {
+ try {
+ this.ondata(readData);
+ } catch(ex) {
+ // re-throw if this is from do_throw
+ if (ex === Cr.NS_ERROR_ABORT)
+ throw ex;
+ // log if there was a test problem
+ do_print('Caught exception: ' + ex + '\n' + ex.stack);
+ do_throw('test is broken; bad ondata handler; see above');
+ }
+ } else {
+ do_throw('Received ' + count + ' bytes of unexpected data!');
+ }
+ },
+
+ onStartRequest: function(request, context) {
+ },
+
+ onStopRequest: function(request, context, status) {
+ if (this.onclose)
+ this.onclose();
+ else
+ do_throw("Received unexpected close!");
+ },
+
+ close: function() {
+ this.binaryInput.close();
+ this.binaryOutput.close();
+ },
+
+ /**
+ * Forget about the socket we knew about before.
+ */
+ reset: function() {
+ this.binaryInput = null;
+ this.input = null;
+ this.binaryOutput = null;
+ this.output = null;
+ },
+};
+
+function makeSuccessCase(name) {
+ return function() {
+ do_print('got expected: ' + name);
+ run_next_test();
+ };
+}
+
+function makeJointSuccess(names) {
+ let funcs = {}, successCount = 0;
+ names.forEach(function(name) {
+ funcs[name] = function() {
+ do_print('got expected: ' + name);
+ if (++successCount === names.length)
+ run_next_test();
+ };
+ });
+ return funcs;
+}
+
+function makeFailureCase(name) {
+ return function() {
+ let argstr;
+ if (arguments.length) {
+ argstr = '(args: ' +
+ Array.map(arguments, function(x) { return x + ""; }).join(" ") + ')';
+ }
+ else {
+ argstr = '(no arguments)';
+ }
+ do_throw('got unexpected: ' + name + ' ' + argstr);
+ };
+}
+
+function makeExpectData(name, expectedData, fromEvent, callback) {
+ let dataBuffer = fromEvent ? null : [], done = false;
+ return function(receivedData) {
+ if (fromEvent) {
+ receivedData = receivedData.data;
+ if (dataBuffer) {
+ let newBuffer = new Uint8Array(dataBuffer.length + receivedData.length);
+ newBuffer.set(dataBuffer, 0);
+ newBuffer.set(receivedData, dataBuffer.length);
+ dataBuffer = newBuffer;
+ }
+ else {
+ dataBuffer = receivedData;
+ }
+ }
+ else {
+ dataBuffer = dataBuffer.concat(receivedData);
+ }
+ do_print(name + ' received ' + receivedData.length + ' bytes');
+
+ if (done)
+ do_throw(name + ' Received data event when already done!');
+
+ if (dataBuffer.length >= expectedData.length) {
+ // check the bytes are equivalent
+ for (let i = 0; i < expectedData.length; i++) {
+ if (dataBuffer[i] !== expectedData[i]) {
+ do_throw(name + ' Received mismatched character at position ' + i);
+ }
+ }
+ if (dataBuffer.length > expectedData.length)
+ do_throw(name + ' Received ' + dataBuffer.length + ' bytes but only expected ' +
+ expectedData.length + ' bytes.');
+
+ done = true;
+ if (callback) {
+ callback();
+ } else {
+ run_next_test();
+ }
+ }
+ };
+}
+
+var server = null, sock = null, failure_drain = null;
+
+/**
+ *
+ * Test functions
+ *
+ */
+
+/**
+ * Connect the socket to the server. This test is added as the first
+ * test, and is also added after every test which results in the socket
+ * being closed.
+ */
+
+function connectSock() {
+ server.reset();
+ var yayFuncs = makeJointSuccess(['serveropen', 'clientopen']);
+
+ sock = TCPSocket.open(
+ '127.0.0.1', server.listener.port,
+ { binaryType: 'arraybuffer' });
+
+ sock.onopen = yayFuncs.clientopen;
+ sock.ondrain = null;
+ sock.ondata = makeFailureCase('data');
+ sock.onerror = makeFailureCase('error');
+ sock.onclose = makeFailureCase('close');
+
+ server.onaccept = yayFuncs.serveropen;
+ server.ondata = makeFailureCase('serverdata');
+ server.onclose = makeFailureCase('serverclose');
+}
+
+/**
+ * Test that sending a small amount of data works, and that buffering
+ * does not take place for this small amount of data.
+ */
+
+function sendData() {
+ server.ondata = makeExpectData('serverdata', DATA_ARRAY);
+ if (!sock.send(TYPED_DATA_ARRAY)) {
+ do_throw("send should not have buffered such a small amount of data");
+ }
+}
+
+/**
+ * Test that sending a large amount of data works, that buffering
+ * takes place (send returns true), and that ondrain is called once
+ * the data has been sent.
+ */
+
+function sendBig() {
+ var yays = makeJointSuccess(['serverdata', 'clientdrain']),
+ amount = 0;
+
+ server.ondata = function (data) {
+ amount += data.length;
+ if (amount === BIG_TYPED_ARRAY.length) {
+ yays.serverdata();
+ }
+ };
+ sock.ondrain = function(evt) {
+ if (sock.bufferedAmount) {
+ do_throw("sock.bufferedAmount was > 0 in ondrain");
+ }
+ yays.clientdrain(evt);
+ }
+ if (sock.send(BIG_TYPED_ARRAY)) {
+ do_throw("expected sock.send to return false on large buffer send");
+ }
+}
+
+/**
+ * Test that data sent from the server correctly fires the ondata
+ * callback on the client side.
+ */
+
+function receiveData() {
+ server.ondata = makeFailureCase('serverdata');
+ sock.ondata = makeExpectData('data', DATA_ARRAY, true);
+
+ server.binaryOutput.writeByteArray(DATA_ARRAY, DATA_ARRAY.length);
+}
+
+/**
+ * Test that when the server closes the connection, the onclose callback
+ * is fired on the client side.
+ */
+
+function serverCloses() {
+ // we don't really care about the server's close event, but we do want to
+ // make sure it happened for sequencing purposes.
+ var yayFuncs = makeJointSuccess(['clientclose', 'serverclose']);
+ sock.ondata = makeFailureCase('data');
+ sock.onclose = yayFuncs.clientclose;
+ server.onclose = yayFuncs.serverclose;
+
+ server.close();
+}
+
+/**
+ * Test that when the client closes the connection, the onclose callback
+ * is fired on the server side.
+ */
+
+function clientCloses() {
+ // we want to make sure the server heard the close and also that the client's
+ // onclose event fired for consistency.
+ var yayFuncs = makeJointSuccess(['clientclose', 'serverclose']);
+ server.onclose = yayFuncs.serverclose;
+ sock.onclose = yayFuncs.clientclose;
+
+ sock.close();
+}
+
+/**
+ * Send a large amount of data and immediately call close
+ */
+
+function bufferedClose() {
+ var yays = makeJointSuccess(['serverdata', 'clientclose', 'serverclose']);
+ server.ondata = makeExpectData(
+ "ondata", BIG_TYPED_ARRAY, false, yays.serverdata);
+ server.onclose = yays.serverclose;
+ sock.onclose = yays.clientclose;
+ sock.send(BIG_TYPED_ARRAY);
+ sock.close();
+}
+
+/**
+ * Connect to a port we know is not listening so an error is assured,
+ * and make sure that onerror and onclose are fired on the client side.
+ */
+
+function badConnect() {
+ // There's probably nothing listening on tcp port 2.
+ sock = TCPSocket.open('127.0.0.1', 2);
+
+ sock.onopen = makeFailureCase('open');
+ sock.ondata = makeFailureCase('data');
+ sock.onclose = makeFailureCase('close');
+
+ sock.onerror = makeSuccessCase('error');
+}
+
+/**
+ * Test that calling send with enough data to buffer causes ondrain to
+ * be invoked once the data has been sent, and then test that calling send
+ * and buffering again causes ondrain to be fired again.
+ */
+
+function drainTwice() {
+ let yays = makeJointSuccess(
+ ['ondrain', 'ondrain2',
+ 'ondata', 'ondata2',
+ 'serverclose', 'clientclose']);
+
+ function serverSideCallback() {
+ yays.ondata();
+ server.ondata = makeExpectData(
+ "ondata2", BIG_TYPED_ARRAY_2, false, yays.ondata2);
+
+ sock.ondrain = yays.ondrain2;
+
+ if (sock.send(BIG_TYPED_ARRAY_2)) {
+ do_throw("sock.send(BIG_TYPED_ARRAY_2) did not return false to indicate buffering");
+ }
+
+ sock.close();
+ }
+
+ server.onclose = yays.serverclose;
+ server.ondata = makeExpectData(
+ "ondata", BIG_TYPED_ARRAY, false, serverSideCallback);
+
+ sock.onclose = yays.clientclose;
+ sock.ondrain = yays.ondrain;
+
+ if (sock.send(BIG_TYPED_ARRAY)) {
+ throw new Error("sock.send(BIG_TYPED_ARRAY) did not return false to indicate buffering");
+ }
+}
+
+function cleanup() {
+ do_print("Cleaning up");
+ sock.close();
+ run_next_test();
+}
+
+/**
+ * Test that calling send with enough data to buffer twice in a row without
+ * waiting for ondrain still results in ondrain being invoked at least once.
+ */
+
+function bufferTwice() {
+ let yays = makeJointSuccess(
+ ['ondata', 'ondrain', 'serverclose', 'clientclose']);
+
+ let double_array = new Uint8Array(BIG_ARRAY.concat(BIG_ARRAY_2));
+ server.ondata = makeExpectData(
+ "ondata", double_array, false, yays.ondata);
+
+ server.onclose = yays.serverclose;
+ sock.onclose = yays.clientclose;
+
+ sock.ondrain = function () {
+ sock.close();
+ yays.ondrain();
+ }
+
+ if (sock.send(BIG_TYPED_ARRAY)) {
+ throw new Error("sock.send(BIG_TYPED_ARRAY) did not return false to indicate buffering");
+ }
+ if (sock.send(BIG_TYPED_ARRAY_2)) {
+ throw new Error("sock.send(BIG_TYPED_ARRAY_2) did not return false to indicate buffering on second synchronous call to send");
+ }
+}
+
+// - connect, data and events work both ways
+add_test(connectSock);
+add_test(sendData);
+add_test(sendBig);
+add_test(receiveData);
+// - server closes on us
+add_test(serverCloses);
+
+// - connect, we close on the server
+add_test(connectSock);
+add_test(clientCloses);
+
+// - connect, buffer, close
+add_test(connectSock);
+add_test(bufferedClose);
+
+// - get an error on an attempt to connect to a non-listening port
+add_test(badConnect);
+
+// send a buffer, get a drain, send a buffer, get a drain
+add_test(connectSock);
+add_test(drainTwice);
+
+// send a buffer, get a drain, send a buffer, get a drain
+add_test(connectSock);
+add_test(bufferTwice);
+
+// clean up
+add_test(cleanup);
+
+function run_test() {
+ server = new TestServer();
+
+ run_next_test();
+}
diff --git a/dom/network/tests/unit/xpcshell.ini b/dom/network/tests/unit/xpcshell.ini
new file mode 100644
--- /dev/null
+++ b/dom/network/tests/unit/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head =
+tail =
+
+[test_tcpsocket.js]
diff --git a/dom/tests/mochitest/general/test_interfaces.html b/dom/tests/mochitest/general/test_interfaces.html
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -525,17 +525,18 @@
"ContactTelephone",
"ContactEmail",
"SVGFitToViewBox",
"SVGAElement",
"NavigatorCamera",
"CameraControl",
"CameraCapabilities",
"CameraManager",
- "CSSSupportsRule"
+ "CSSSupportsRule",
+ "TCPSocket"
]
for (var i in Components.interfaces) {
var s = i.toString();
var name = null;
if (s.indexOf("nsIDOM") == 0) {
name = s.substring("nsIDOM".length);
} else if (s.indexOf("nsI") == 0) {
diff --git a/testing/xpcshell/xpcshell.ini b/testing/xpcshell/xpcshell.ini
--- a/testing/xpcshell/xpcshell.ini
+++ b/testing/xpcshell/xpcshell.ini
@@ -7,16 +7,17 @@
[include:netwerk/cookie/test/unit/xpcshell.ini]
[include:modules/libjar/zipwriter/test/unit/xpcshell.ini]
[include:uriloader/exthandler/tests/unit/xpcshell.ini]
[include:parser/xml/test/unit/xpcshell.ini]
[include:image/test/unit/xpcshell.ini]
[include:dom/plugins/test/unit/xpcshell.ini]
[include:dom/sms/tests/xpcshell.ini]
[include:dom/mms/tests/xpcshell.ini]
+[include:dom/network/tests/unit/xpcshell.ini]
[include:dom/src/json/test/unit/xpcshell.ini]
[include:dom/system/gonk/tests/xpcshell.ini]
[include:dom/tests/unit/xpcshell.ini]
[include:dom/indexedDB/test/unit/xpcshell.ini]
[include:content/xtf/test/unit/xpcshell.ini]
[include:docshell/test/unit/xpcshell.ini]
[include:docshell/test/unit_ipc/xpcshell.ini]
[include:embedding/tests/unit/xpcshell.ini]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment