Bug 733573 - Expose a client TCP socket API to web applications [r=honzab,fabrice]
authorDonovan Preston <dpreston@mozilla.com>
Tue, 21 Aug 2012 09:46:27 -0700
changeset 102988 3764a9891e74
parent 102987 ec5685278f82
child 102989 ea6a5e9b25db
push id23319
push useremorley@mozilla.com
push date2012-08-22 09:28 +0000
treeherdermozilla-central@4d59eb5ac2c6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewershonzab, fabrice
bugs733573
milestone17.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 733573 - Expose a client TCP socket API to web applications [r=honzab,fabrice]
b2g/app/b2g.js
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
dom/network/interfaces/Makefile.in
dom/network/interfaces/nsIDOMTCPSocket.idl
dom/network/src/Makefile.in
dom/network/src/TCPSocket.js
dom/network/src/TCPSocket.manifest
dom/network/tests/Makefile.in
dom/network/tests/test_tcpsocket_default_permissions.html
dom/network/tests/test_tcpsocket_enabled_no_perm.html
dom/network/tests/test_tcpsocket_enabled_with_perm.html
dom/network/tests/unit/test_tcpsocket.js
dom/network/tests/unit/xpcshell.ini
dom/tests/mochitest/general/test_interfaces.html
mobile/android/installer/package-manifest.in
mobile/xul/installer/package-manifest.in
testing/xpcshell/xpcshell.ini
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -391,16 +391,19 @@ pref("dom.mozAlarms.enabled", true);
 
 // 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);
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -479,16 +479,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
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -479,16 +479,19 @@
 @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
+
 #ifdef ENABLE_MARIONETTE
 @BINPATH@/chrome/marionette@JAREXT@
 @BINPATH@/chrome/marionette.manifest
 @BINPATH@/components/MarionetteComponents.manifest
 @BINPATH@/components/marionettecomponent.js
 #endif
 
 ; Modules
--- a/dom/network/interfaces/Makefile.in
+++ b/dom/network/interfaces/Makefile.in
@@ -14,11 +14,12 @@ XPIDL_MODULE = dom_network
 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
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;
+};
+
--- a/dom/network/src/Makefile.in
+++ b/dom/network/src/Makefile.in
@@ -8,16 +8,21 @@ srcdir           = @srcdir@
 VPATH            = $(srcdir)
 
 include $(DEPTH)/config/autoconf.mk
 
 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
 
 EXPORTS_mozilla/dom/network = \
   Utils.h \
   Types.h \
   Constants.h \
new file mode 100644
--- /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]);
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
--- a/dom/network/tests/Makefile.in
+++ b/dom/network/tests/Makefile.in
@@ -11,11 +11,18 @@ relativesrcdir   = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 DIRS = \
   $(NULL)
 
 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
new file mode 100644
--- /dev/null
+++ b/dom/network/tests/test_tcpsocket_default_permissions.html
@@ -0,0 +1,27 @@
+<!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 **/
+
+try {
+  navigator.mozTCPSocket;
+  throw new Error("Error: navigator.mozTCPSocket should not exist by default");
+} catch (e) {
+  ok(true, "navigator.mozTCPSocket should not exist by default");  
+}
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/network/tests/test_tcpsocket_enabled_no_perm.html
@@ -0,0 +1,35 @@
+<!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 permission being turned on enables 
+  navigator.mozTCPSocket, but mozTCPSocket.open does not work
+  in content.
+**/
+SpecialPowers.setBoolPref("dom.mozTCPSocket.enabled", true);
+
+ok('mozTCPSocket' in navigator, "navigator.mozTCPSocket should be accessible if dom.mozTCPSocket.enabled is true");
+
+try {
+  navigator.mozTCPSocket.open('localhost', 80);
+  throw new Error("Error: navigator.mozTCPSocket.open should raise for content that does not have the tcp-socket permission");
+} catch (e) {
+  ok(true, "navigator.mozTCPSocket.open should raise for content that does not have the tcp-socket permission");
+}
+
+SpecialPowers.setBoolPref("dom.mozTCPSocket.enabled", false);
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/network/tests/test_tcpsocket_enabled_with_perm.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test to ensure TCPSocket permission enabled and open works with tcp-socket perm</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 being turned on enables 
+  navigator.mozTCPSocket, and mozTCPSocket.open works when
+  the tcp-socket permission has been granted.
+**/
+SpecialPowers.setBoolPref("dom.mozTCPSocket.enabled", true);
+SpecialPowers.addPermission("tcp-socket", true, document);
+
+ok('mozTCPSocket' in navigator, "navigator.mozTCPSocket should be accessible if dom.mozTCPSocket.enabled is true");
+
+ok(navigator.mozTCPSocket.open('localhost', 80), "navigator.mozTCPSocket.open should work for content that has the tcp-socket permission");
+
+SpecialPowers.setBoolPref("dom.mozTCPSocket.enabled", false);
+
+</script>
+</pre>
+</body>
+</html>
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(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();
+}
new file mode 100644
--- /dev/null
+++ b/dom/network/tests/unit/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head =
+tail =
+
+[test_tcpsocket.js]
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -528,17 +528,18 @@ var interfaceNamesInGlobalScope =
     "ContactField",
     "SVGFitToViewBox",
     "SVGAElement",
     "NavigatorCamera",
     "CameraControl",
     "CameraCapabilities",
     "CameraManager",
     "CSSSupportsRule",
-    "MozMobileCellInfo"
+    "MozMobileCellInfo",
+    "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) {
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -347,16 +347,19 @@
 #endif
 @BINPATH@/components/TelemetryPing.js
 @BINPATH@/components/TelemetryPing.manifest
 @BINPATH@/components/Webapps.js
 @BINPATH@/components/Webapps.manifest
 @BINPATH@/components/AppsService.js
 @BINPATH@/components/AppsService.manifest
 
+@BINPATH@/components/TCPSocket.js
+@BINPATH@/components/TCPSocket.manifest
+
 ; Modules
 @BINPATH@/modules/*
 
 ; Safe Browsing
 @BINPATH@/components/nsURLClassifier.manifest
 @BINPATH@/components/nsUrlClassifierHashCompleter.js
 @BINPATH@/components/nsUrlClassifierListManager.js
 @BINPATH@/components/nsUrlClassifierLib.js
--- a/mobile/xul/installer/package-manifest.in
+++ b/mobile/xul/installer/package-manifest.in
@@ -433,16 +433,19 @@
 @BINPATH@/components/SyncComponents.manifest
 @BINPATH@/components/Weave.js
 @BINPATH@/components/WeaveCrypto.manifest
 @BINPATH@/components/WeaveCrypto.js
 #endif
 @BINPATH@/components/TelemetryPing.js
 @BINPATH@/components/TelemetryPing.manifest
 
+@BINPATH@/components/TCPSocket.js
+@BINPATH@/components/TCPSocket.manifest
+
 ; Modules
 @BINPATH@/modules/*
 
 ; Safe Browsing
 @BINPATH@/components/nsURLClassifier.manifest
 @BINPATH@/components/nsUrlClassifierHashCompleter.js
 @BINPATH@/components/nsUrlClassifierListManager.js
 @BINPATH@/components/nsUrlClassifierLib.js
--- 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]