From bb0843a77405833de2d41eb0806e14bf5dbb9b60 Mon Sep 17 00:00:00 2001 From: Donovan Preston Date: Tue, 21 Aug 2012 09:46:27 -0700 Subject: [PATCH] Bug 733573 - Expose a client TCP socket API to web applications [r=honzab,fabrice] --- b2g/app/b2g.js | 3 + b2g/installer/package-manifest.in | 3 + browser/installer/package-manifest.in | 3 + dom/network/interfaces/Makefile.in | 1 + dom/network/interfaces/nsIDOMTCPSocket.idl | 219 +++++++ dom/network/src/Makefile.in | 5 + dom/network/src/TCPSocket.js | 556 ++++++++++++++++++ dom/network/src/TCPSocket.manifest | 4 + dom/network/tests/Makefile.in | 7 + .../test_tcpsocket_default_permissions.html | 27 + .../tests/test_tcpsocket_enabled_no_perm.html | 35 ++ .../test_tcpsocket_enabled_with_perm.html | 31 + dom/network/tests/unit/test_tcpsocket.js | 479 +++++++++++++++ dom/network/tests/unit/xpcshell.ini | 5 + .../mochitest/general/test_interfaces.html | 3 +- testing/xpcshell/xpcshell.ini | 1 + 16 files changed, 1381 insertions(+), 1 deletion(-) create mode 100644 dom/network/interfaces/nsIDOMTCPSocket.idl create mode 100644 dom/network/src/TCPSocket.js create mode 100644 dom/network/src/TCPSocket.manifest create mode 100644 dom/network/tests/test_tcpsocket_default_permissions.html create mode 100644 dom/network/tests/test_tcpsocket_enabled_no_perm.html create mode 100644 dom/network/tests/test_tcpsocket_enabled_with_perm.html create mode 100644 dom/network/tests/unit/test_tcpsocket.js create mode 100644 dom/network/tests/unit/xpcshell.ini diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js index 4dc5aa7018a5..f4605c0ee9b5 100644 --- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -396,6 +396,9 @@ pref("dom.mozSettings.enabled", true); 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 diff --git a/b2g/installer/package-manifest.in b/b2g/installer/package-manifest.in index 097717e73e5a..b9f21164ac4b 100644 --- a/b2g/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -484,6 +484,9 @@ @BINPATH@/components/ActivityRequestHandler.js @BINPATH@/components/ActivityWrapper.js +@BINPATH@/components/TCPSocket.js +@BINPATH@/components/TCPSocket.manifest + @BINPATH@/components/AppProtocolHandler.js @BINPATH@/components/AppProtocolHandler.manifest diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index 6395ca977035..1d5b226287a8 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -484,6 +484,9 @@ @BINPATH@/components/ContactManager.manifest @BINPATH@/components/AlarmsManager.js @BINPATH@/components/AlarmsManager.manifest +@BINPATH@/components/TCPSocket.js +@BINPATH@/components/TCPSocket.manifest + #ifdef ENABLE_MARIONETTE @BINPATH@/chrome/marionette@JAREXT@ @BINPATH@/chrome/marionette.manifest diff --git a/dom/network/interfaces/Makefile.in b/dom/network/interfaces/Makefile.in index f7626dbec819..ab5e9653e696 100644 --- a/dom/network/interfaces/Makefile.in +++ b/dom/network/interfaces/Makefile.in @@ -19,6 +19,7 @@ XPIDLSRCS = \ 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 index 000000000000..1d56c7eb33fe --- /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 index 075327d5a6b4..989b6a91cbfa 100644 --- a/dom/network/src/Makefile.in +++ b/dom/network/src/Makefile.in @@ -13,6 +13,11 @@ LIBRARY_NAME = dom_network_s LIBXUL_LIBRARY = 1 FORCE_STATIC_LIB = 1 +EXTRA_COMPONENTS = \ + TCPSocket.js \ + TCPSocket.manifest \ + $(NULL) + include $(topsrcdir)/dom/dom-config.mk EXPORTS_NAMESPACES = mozilla/dom/network diff --git a/dom/network/src/TCPSocket.js b/dom/network/src/TCPSocket.js new file mode 100644 index 000000000000..dd9a334c06fc --- /dev/null +++ b/dom/network/src/TCPSocket.js @@ -0,0 +1,556 @@ +/* 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; + +/* + * 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); + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic == "inner-window-destroyed") { + let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (wId == this.innerWindowID) { + LOG("inner-window-destroyed: " + this.innerWindowID); + + // 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 + this.onopen = null; + this.ondrain = null; + this.ondata = null; + this.onerror = null; + this.onclose = null; + + // Clean up our socket + this.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(); + + that.innerWindowID = this.innerWindowID; + + LOG("window init: " + that.innerWindowID); + Services.obs.addObserver(that, "inner-window-destroyed", true); + + 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 index 000000000000..5c8cf1f30635 --- /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 index adca89eb2bfd..88b0d3b44bc2 100644 --- a/dom/network/tests/Makefile.in +++ b/dom/network/tests/Makefile.in @@ -16,6 +16,13 @@ DIRS = \ MOCHITEST_FILES = \ test_network_basics.html \ + test_tcpsocket_default_permissions.html \ + test_tcpsocket_enabled_no_perm.html \ + test_tcpsocket_enabled_with_perm.html \ $(NULL) +MODULE = test_dom_socket + +XPCSHELL_TESTS = unit + include $(topsrcdir)/config/rules.mk 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 000000000000..8c0f42e32c0f --- /dev/null +++ b/dom/network/tests/test_tcpsocket_default_permissions.html @@ -0,0 +1,27 @@ + + + + Test to ensure TCPSocket permission is disabled by default + + + + +

+ +
+
+
+ + 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 000000000000..a028b48d60cc --- /dev/null +++ b/dom/network/tests/test_tcpsocket_enabled_no_perm.html @@ -0,0 +1,35 @@ + + + + Test to ensure TCPSocket permission enabled and no tcp-socket perm does not allow open + + + + +

+ +
+
+
+ + 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 000000000000..4490743e4c29 --- /dev/null +++ b/dom/network/tests/test_tcpsocket_enabled_with_perm.html @@ -0,0 +1,31 @@ + + + + Test to ensure TCPSocket permission enabled and open works with tcp-socket perm + + + + +

+ +
+
+
+ + diff --git a/dom/network/tests/unit/test_tcpsocket.js b/dom/network/tests/unit/test_tcpsocket.js new file mode 100644 index 000000000000..21722aa7f772 --- /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(65539), + BIG_ARRAY_2 = new Array(65539); + +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 index 000000000000..a2b586d72a6d --- /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 index ac6588cd70e9..54d14ea18fbe 100644 --- a/dom/tests/mochitest/general/test_interfaces.html +++ b/dom/tests/mochitest/general/test_interfaces.html @@ -533,7 +533,8 @@ var interfaceNamesInGlobalScope = "CameraCapabilities", "CameraManager", "CSSSupportsRule", - "MozMobileCellInfo" + "MozMobileCellInfo", + "TCPSocket" ] for (var i in Components.interfaces) { diff --git a/testing/xpcshell/xpcshell.ini b/testing/xpcshell/xpcshell.ini index 5ed66c8f54b5..7d5c58ef8646 100644 --- a/testing/xpcshell/xpcshell.ini +++ b/testing/xpcshell/xpcshell.ini @@ -12,6 +12,7 @@ [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]