Bug 861196 - mozTCPSocket needs to translate nsISSLStatus error codes to something that can be exposed to content. r=bsmith, r=jst, a=blocking-b2g=tef+
authorAndrew Sutherland <asutherland@asutherland.org>
Thu, 02 May 2013 03:55:38 -0400
changeset 119245 987235c0ba5e37777ad5288b4736a691a43a8e06
parent 119244 17daac8919eb8951818cdf6decf4f0e90e41dbe9
child 119246 4451d7bc0912afe043957e139fd4513adcb8e389
push id746
push userbugmail@asutherland.org
push dateThu, 02 May 2013 07:56:20 +0000
reviewersbsmith, jst, blocking-b2g
bugs861196
milestone18.0
Bug 861196 - mozTCPSocket needs to translate nsISSLStatus error codes to something that can be exposed to content. r=bsmith, r=jst, a=blocking-b2g=tef+
dom/network/interfaces/nsIDOMTCPSocket.idl
dom/network/src/PTCPSocket.ipdl
dom/network/src/TCPSocket.js
dom/network/src/TCPSocketChild.cpp
dom/network/src/TCPSocketParent.cpp
dom/network/tests/unit/test_tcpsocket.js
--- a/dom/network/interfaces/nsIDOMTCPSocket.idl
+++ b/dom/network/interfaces/nsIDOMTCPSocket.idl
@@ -178,21 +178,20 @@ interface nsIDOMTCPSocket : nsISupports
   attribute jsval onclose;
 };
 
 /*
  * Internal interfaces for use in cross-process socket implementation.
  * Needed to account for multiple possible types that can be provided to
  * the socket callbacks as arguments.
  */
-[scriptable, uuid(fff9ed4a-5e94-4fc0-84e4-7f4d35d0a26c)]
+[scriptable, uuid(322193a3-da17-4ca5-ad26-3539c519ea4b)]
 interface nsITCPSocketInternal : nsISupports {
-  // Trigger the callback for |type| and provide an Error() object with the given data
-  void callListenerError(in DOMString type, in DOMString message, in DOMString filename,
-                         in uint32_t lineNumber, in uint32_t columnNumber);
+  // Trigger the callback for |type| and provide a DOMError() object with the given data
+  void callListenerError(in DOMString type, in DOMString name);
 
   // Trigger the callback for |type| and provide a string argument
   void callListenerData(in DOMString type, in DOMString data);
 
   // Trigger the callback for |type| and provide an ArrayBuffer argument
   void callListenerArrayBuffer(in DOMString type, in jsval data);
 
   // Trigger the callback for |type| with no argument
--- a/dom/network/src/PTCPSocket.ipdl
+++ b/dom/network/src/PTCPSocket.ipdl
@@ -6,32 +6,29 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 include protocol PNecko;
 
 include "mozilla/net/NeckoMessageUtils.h";
 
 using mozilla::void_t;
 
-struct JSError {
-  nsString message;
-  nsString filename;
-  uint32_t lineNumber;
-  uint32_t columnNumber;
+struct TCPError {
+  nsString name;
 };
 
 union SendableData {
   uint8_t[];
   nsString;
 };
 
 union CallbackData {
   void_t;
   SendableData;
-  JSError;
+  TCPError;
 };
 
 namespace mozilla {
 namespace net {
 
 //-------------------------------------------------------------------
 protocol PTCPSocket
 {
--- a/dom/network/src/TCPSocket.js
+++ b/dom/network/src/TCPSocket.js
@@ -30,16 +30,29 @@ const InputStreamPump = CC(
 
 const kCONNECTING = 'connecting';
 const kOPEN = 'open';
 const kCLOSING = 'closing';
 const kCLOSED = 'closed';
 
 const BUFFER_SIZE = 65536;
 
+// XXX we have no TCPError implementation right now because it's really hard to
+// do on b2g18.  On mozilla-central we want a proper TCPError that ideally
+// sub-classes DOMError.  Bug 867872 has been filed to implement this and
+// contains a documented TCPError.webidl that maps all the error codes we use in
+// this file to slightly more readable explanations.
+function createTCPError(aErrorName, aErrorType) {
+  let error = Cc["@mozilla.org/dom-error;1"]
+                .createInstance(Ci.nsIDOMDOMError);
+  error.wrappedJSObject.init(aErrorName);
+  return error;
+}
+
+
 /*
  * Debug logging function
  */
 
 let debug = true;
 function LOG(msg) {
   if (debug)
     dump("TCPSocket: " + msg + "\n");
@@ -219,22 +232,20 @@ TCPSocket.prototype = {
     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) {
-          self._readyState = kCLOSED;
-          let err = new Error("Connection closed while writing: " + status);
-          err.status = status;
-          self.callListener("error", err);
-          self.callListener("close");
+        if (!Components.isSuccessCode(status)) {
+          // Note that we can/will get an error here as well as in the
+          // onStopRequest for inbound data.
+          self._maybeReportErrorAndCloseIfOpen(status);
           return;
         }
 
         if (self._multiplexStream.count) {
           self._ensureCopying();
         } else {
           if (self._waitingForDrain) {
             self._waitingForDrain = false;
@@ -253,19 +264,20 @@ TCPSocket.prototype = {
   callListener: function ts_callListener(type, data) {
     if (!this["on" + type])
       return;
 
     this["on" + type].call(null, new TCPSocketEvent(type, this, data || ""));
   },
 
   /* nsITCPSocketInternal methods */
-  callListenerError: function ts_callListenerError(type, message, filename,
-                                                   lineNumber, columnNumber) {
-    this.callListener(type, new Error(message, filename, lineNumber, columnNumber));
+  callListenerError: function ts_callListenerError(type, name) {
+    // XXX we're not really using TCPError at this time, so there's only a name
+    // attribute to pass.
+    this.callListener(type, createTCPError(name));
   },
 
   callListenerData: function ts_callListenerString(type, data) {
     this.callListener(type, data);
   },
 
   callListenerArrayBuffer: function ts_callListenerArrayBuffer(type, data) {
     this.callListener(type, data);
@@ -376,17 +388,16 @@ TCPSocket.prototype = {
                              .createInstance(Ci.nsITCPSocketChild);
       that._socketBridge.open(that, host, port, !!that._ssl,
                               that._binaryType, this.useWin, this.useWin || this);
       return that;
     }
 
     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.
@@ -496,16 +507,145 @@ TCPSocket.prototype = {
 
     if (this._inputStreamPump) {
       this._inputStreamPump.resume();
     } else {
       --this._suspendCount;
     }
   },
 
+  _maybeReportErrorAndCloseIfOpen: function(status) {
+    // If we're closed, we've already reported the error or just don't need to
+    // report the error.
+    if (this._readyState === kCLOSED)
+      return;
+    this._readyState = kCLOSED;
+
+    if (!Components.isSuccessCode(status)) {
+      // Convert the status code to an appropriate error message.  Raw constants
+      // are used inline in all cases for consistency.  Some error codes are
+      // available in Components.results, some aren't.  Network error codes are
+      // effectively stable, NSS error codes are officially not, but we have no
+      // symbolic way to dynamically resolve them anyways (other than an ability
+      // to determine the error class.)
+      let errName, errType;
+      // security module? (and this is an error)
+      if ((status & 0xff0000) === 0x5a0000) {
+        const nsINSSErrorsService = Ci.nsINSSErrorsService;
+        let nssErrorsService = Cc['@mozilla.org/nss_errors_service;1']
+                                 .getService(nsINSSErrorsService);
+        let errorClass;
+        // getErrorClass will throw a generic NS_ERROR_FAILURE if the error code is
+        // somehow not in the set of covered errors.
+        try {
+          errorClass = nssErrorsService.getErrorClass(status);
+        }
+        catch (ex) {
+          errorClass = 'SecurityProtocol';
+        }
+        switch (errorClass) {
+          case nsINSSErrorsService.ERROR_CLASS_SSL_PROTOCOL:
+            errType = 'SecurityProtocol';
+            break;
+          case nsINSSErrorsService.ERROR_CLASS_BAD_CERT:
+            errType = 'SecurityCertificate';
+            break;
+          // no default is required; the platform impl automatically defaults to
+          // ERROR_CLASS_SSL_PROTOCOL.
+        }
+
+        // NSS_SEC errors (happen below the base value because of negative vals)
+        if ((status & 0xffff) <
+            Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE)) {
+          // The bases are actually negative, so in our positive numeric space, we
+          // need to subtract the base off our value.
+          let nssErr = Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE) -
+                         (status & 0xffff);
+          switch (nssErr) {
+            case 11: // SEC_ERROR_EXPIRED_CERTIFICATE, sec(11)
+              errName = 'SecurityExpiredCertificateError';
+              break;
+            case 12: // SEC_ERROR_REVOKED_CERTIFICATE, sec(12)
+              errName = 'SecurityRevokedCertificateError';
+              break;
+            // per bsmith, we will be unable to tell these errors apart very soon,
+            // so it makes sense to just folder them all together already.
+            case 13: // SEC_ERROR_UNKNOWN_ISSUER, sec(13)
+            case 20: // SEC_ERROR_UNTRUSTED_ISSUER, sec(20)
+            case 21: // SEC_ERROR_UNTRUSTED_CERT, sec(21)
+            case 36: // SEC_ERROR_CA_CERT_INVALID, sec(36)
+              errName = 'SecurityUntrustedCertificateIssuerError';
+              break;
+            case 90: // SEC_ERROR_INADEQUATE_KEY_USAGE, sec(90)
+              errName = 'SecurityInadequateKeyUsageError';
+              break;
+            case 176: // SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED, sec(176)
+              errName = 'SecurityCertificateSignatureAlgorithmDisabledError';
+              break;
+            default:
+              errName = 'SecurityError';
+              break;
+          }
+        }
+        // NSS_SSL errors
+        else {
+          let sslErr = Math.abs(nsINSSErrorsService.NSS_SSL_ERROR_BASE) -
+                         (status & 0xffff);
+          switch (sslErr) {
+            case 3: // SSL_ERROR_NO_CERTIFICATE, ssl(3)
+              errName = 'SecurityNoCertificateError';
+              break;
+            case 4: // SSL_ERROR_BAD_CERTIFICATE, ssl(4)
+              errName = 'SecurityBadCertificateError';
+              break;
+            case 8: // SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE, ssl(8)
+              errName = 'SecurityUnsupportedCertificateTypeError';
+              break;
+            case 9: // SSL_ERROR_UNSUPPORTED_VERSION, ssl(9)
+              errName = 'SecurityUnsupportedTLSVersionError';
+              break;
+            case 12: // SSL_ERROR_BAD_CERT_DOMAIN, ssl(12)
+              errName = 'SecurityCertificateDomainMismatchError';
+              break;
+            default:
+              errName = 'SecurityError';
+              break;
+          }
+        }
+      }
+      // must be network
+      else {
+        errType = 'Network';
+        switch (status) {
+          // connect to host:port failed
+          case 0x804B000C: // NS_ERROR_CONNECTION_REFUSED, network(13)
+            errName = 'ConnectionRefusedError';
+            break;
+          // network timeout error
+          case 0x804B000E: // NS_ERROR_NET_TIMEOUT, network(14)
+            errName = 'NetworkTimeoutError';
+            break;
+          // hostname lookup failed
+          case 0x804B001E: // NS_ERROR_UNKNOWN_HOST, network(30)
+            errName = 'DomainNotFoundError';
+            break;
+          case 0x804B0047: // NS_ERROR_NET_INTERRUPT, network(71)
+            errName = 'NetworkInterruptError';
+            break;
+          default:
+            errName = 'NetworkError';
+            break;
+        }
+      }
+      let err = createTCPError(errName, errType);
+      this.callListener("error", err);
+    }
+    this.callListener("close");
+  },
+
   // 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("open");
 
       this._inputStreamPump = new InputStreamPump(
@@ -521,48 +661,44 @@ TCPSocket.prototype = {
   },
 
   // nsIAsyncInputStream (Triggered by _socketInputStream.asyncWait)
   // Only used for detecting connection refused
   onInputStreamReady: function ts_onInputStreamReady(input) {
     try {
       input.available();
     } catch (e) {
-      this.callListener("error", new Error("Connection refused"));
+      // NS_ERROR_CONNECTION_REFUSED
+      this._maybeReportErrorAndCloseIfOpen(0x804B000C);
     }
   },
 
   // 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) {
+    let statusIsError = !Components.isSuccessCode(status);
+
+    if (buffered_output && !statusIsError) {
       // 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("error", err);
-    }
-
-    this.callListener("close");
+    // We call this even if there is no error.
+    this._maybeReportErrorAndCloseIfOpen(status);
   },
 
   // nsIStreamListener (Triggered by _inputStreamPump.asyncRead)
   onDataAvailable: function ts_onDataAvailable(request, context, inputStream, offset, count) {
     if (this._binaryType === "arraybuffer") {
       let buffer = new (this.useWin ? this.useWin.ArrayBuffer : ArrayBuffer)(count);
       this._inputStreamBinary.readArrayBuffer(count, buffer);
       this.callListener("data", buffer);
@@ -590,34 +726,9 @@ TCPSocket.prototype = {
     Ci.nsIDOMTCPSocket,
     Ci.nsITCPSocketInternal,
     Ci.nsIDOMGlobalPropertyInitializer,
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference
   ])
 }
 
-
-function SecurityCallbacks(socket) {
-  this._socket = socket;
-}
-
-SecurityCallbacks.prototype = {
-  notifyCertProblem: function sc_notifyCertProblem(socketInfo, status,
-                                                   targetSite) {
-    this._socket.callListener("error", status);
-    this._socket.close();
-    return true;
-  },
-
-  getInterface: function sc_getInterface(iid) {
-    return this.QueryInterface(iid);
-  },
-
-  QueryInterface: XPCOMUtils.generateQI([
-    Ci.nsIBadCertListener2,
-    Ci.nsIInterfaceRequestor,
-    Ci.nsISupports
-  ])
-};
-
-
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TCPSocket]);
--- a/dom/network/src/TCPSocketChild.cpp
+++ b/dom/network/src/TCPSocketChild.cpp
@@ -123,20 +123,19 @@ TCPSocketChild::RecvCallback(const nsStr
 {
   if (NS_FAILED(mSocket->UpdateReadyStateAndBuffered(aReadyState, aBuffered)))
     NS_ERROR("Shouldn't fail!");
 
   nsresult rv = NS_ERROR_FAILURE;
   if (aData.type() == CallbackData::Tvoid_t) {
     rv = mSocket->CallListenerVoid(aType);
 
-  } else if (aData.type() == CallbackData::TJSError) {
-    const JSError& err(aData.get_JSError());
-    rv = mSocket->CallListenerError(aType, err.message(), err.filename(),
-                                   err.lineNumber(), err.columnNumber());
+  } else if (aData.type() == CallbackData::TTCPError) {
+    const TCPError& err(aData.get_TCPError());
+    rv = mSocket->CallListenerError(aType, err.name());
 
   } else if (aData.type() == CallbackData::TSendableData) {
     const SendableData& data = aData.get_SendableData();
 
     if (data.type() == SendableData::TArrayOfuint8_t) {
       jsval val;
       bool ok = IPC::DeserializeArrayBuffer(mSocketObj, data.get_ArrayOfuint8_t(), &val);
       NS_ENSURE_TRUE(ok, true);
--- a/dom/network/src/TCPSocketParent.cpp
+++ b/dom/network/src/TCPSocketParent.cpp
@@ -23,19 +23,18 @@ DeserializeArrayBuffer(JSRawObject aObj,
 namespace mozilla {
 namespace dom {
 
 static void
 FireInteralError(mozilla::net::PTCPSocketParent* aActor, uint32_t aLineNo)
 {
   mozilla::unused <<
       aActor->SendCallback(NS_LITERAL_STRING("onerror"),
-                          JSError(NS_LITERAL_STRING("Internal error"),
-                                  NS_LITERAL_STRING(__FILE__), aLineNo, 0),
-                          NS_LITERAL_STRING("connecting"), 0);
+                           TCPError(NS_LITERAL_STRING("InvalidStateError")),
+                           NS_LITERAL_STRING("connecting"), 0);
 }
 
 NS_IMPL_CYCLE_COLLECTION_2(TCPSocketParent, mSocket, mIntermediary)
 NS_IMPL_CYCLE_COLLECTING_ADDREF(TCPSocketParent)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(TCPSocketParent)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TCPSocketParent)
   NS_INTERFACE_MAP_ENTRY(nsITCPSocketParent)
@@ -144,17 +143,17 @@ TCPSocketParent::SendCallback(const nsAS
   CallbackData data;
   if (aDataVal.isString()) {
     JSString* jsstr = aDataVal.toString();
     nsDependentJSString str;
     if (!str.init(aCx, jsstr)) {
       FireInteralError(this, __LINE__);
       return NS_ERROR_OUT_OF_MEMORY;
     }
-    data = str;
+    data = SendableData(str);
 
   } else if (aDataVal.isUndefined() || aDataVal.isNull()) {
     data = mozilla::void_t();
 
   } else if (aDataVal.isObject()) {
     JSObject* obj = &aDataVal.toObject();
     if (JS_IsArrayBufferObject(obj, aCx)) {
       uint32_t nbytes = JS_GetArrayBufferByteLength(obj, aCx);
@@ -168,50 +167,28 @@ TCPSocketParent::SendCallback(const nsAS
         FireInteralError(this, __LINE__);
         return NS_ERROR_OUT_OF_MEMORY;
       }
       InfallibleTArray<uint8_t> arr;
       arr.SwapElements(fallibleArr);
       data = SendableData(arr);
 
     } else {
-      nsDependentJSString message, filename;
-      uint32_t lineNumber = 0;
-      uint32_t columnNumber = 0;
+      nsDependentJSString name;
 
       jsval val;
-      if (!JS_GetProperty(aCx, obj, "message", &val)) {
-        NS_ERROR("No message property on supposed error object");
+      if (!JS_GetProperty(aCx, obj, "name", &val)) {
+        NS_ERROR("No name property on supposed error object");
       } else if (JSVAL_IS_STRING(val)) {
-        if (!message.init(aCx, JSVAL_TO_STRING(val))) {
+        if (!name.init(aCx, JSVAL_TO_STRING(val))) {
           NS_WARNING("couldn't initialize string");
         }
       }
 
-      if (!JS_GetProperty(aCx, obj, "fileName", &val)) {
-        NS_ERROR("No fileName property on supposed error object");
-      } else if (JSVAL_IS_STRING(val)) {
-        if (!filename.init(aCx, JSVAL_TO_STRING(val))) {
-          NS_WARNING("couldn't initialize string");
-        }
-      }
-
-      if (!JS_GetProperty(aCx, obj, "lineNumber", &val)) {
-        NS_ERROR("No lineNumber property on supposed error object");
-      } else if (JSVAL_IS_INT(val)) {
-        lineNumber = JSVAL_TO_INT(val);
-      }
-
-      if (!JS_GetProperty(aCx, obj, "columnNumber", &val)) {
-        NS_ERROR("No columnNumber property on supposed error object");
-      } else if (JSVAL_IS_INT(val)) {
-        columnNumber = JSVAL_TO_INT(val);
-      }
-
-      data = JSError(message, filename, lineNumber, columnNumber);
+      data = TCPError(name);
     }
   } else {
     NS_ERROR("Unexpected JS value encountered");
     FireInteralError(this, __LINE__);
     return NS_ERROR_FAILURE;
   }
   mozilla::unused <<
       PTCPSocketParent::SendCallback(nsString(aType), data,
--- a/dom/network/tests/unit/test_tcpsocket.js
+++ b/dom/network/tests/unit/test_tcpsocket.js
@@ -295,21 +295,21 @@ function sendData() {
  * 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();      
+      yays.serverdata();
     }
   };
   sock.ondrain = function(evt) {
     if (sock.bufferedAmount) {
       do_throw("sock.bufferedAmount was > 0 in ondrain");
     }
     yays.clientdrain(evt);
   }
@@ -374,31 +374,35 @@ function bufferedClose() {
   sock.send(BIG_ARRAY_BUFFER);
   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');
 
   let success = makeSuccessCase('error');
-  sock.onerror = function(data) {
-    do_check_neq(data.data.message, '');
-    do_check_neq(data.data.fileName, '');
-    do_check_neq(data.data.lineNumber, 0);
-    success();
+  let gotError = false;
+  sock.onerror = function(event) {
+    do_check_eq(event.data.name, 'ConnectionRefusedError');
+    gotError = true;
+  };
+  sock.onclose = function() {
+    if (!gotError)
+      do_throw('got close without error!');
+    else
+      success();
   };
 }
 
 /**
  * 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.
  */
@@ -500,17 +504,17 @@ add_test(connectSock);
 add_test(bufferTwice);
 
 // clean up
 add_test(cleanup);
 
 function run_test() {
   if (!gInChild)
     Services.prefs.setBoolPref('dom.mozTCPSocket.enabled', true);
-  
+
   server = new TestServer();
 
   run_next_test();
 
   do_timeout(10000, function() {
     do_throw(
       "The test should never take this long unless the system is hosed.");
   });