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+

sufficiently happy try build:
https://tbpl.mozilla.org/?tree=Try&rev=36edcbc1e65a
This commit is contained in:
Andrew Sutherland 2013-05-02 03:51:54 -04:00
parent 24efd6fafd
commit 6af8bd397d
6 changed files with 188 additions and 101 deletions

View File

@ -183,11 +183,10 @@ interface nsIDOMTCPSocket : nsISupports
* 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);

View File

@ -11,11 +11,8 @@ 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 {
@ -26,7 +23,7 @@ union SendableData {
union CallbackData {
void_t;
SendableData;
JSError;
TCPError;
};
namespace mozilla {

View File

@ -35,6 +35,19 @@ 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
*/
@ -224,12 +237,10 @@ TCPSocket.prototype = {
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;
}
@ -258,9 +269,10 @@ TCPSocket.prototype = {
},
/* 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) {
@ -381,7 +393,6 @@ TCPSocket.prototype = {
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(
@ -501,6 +512,135 @@ TCPSocket.prototype = {
}
},
_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) {
@ -526,7 +666,8 @@ TCPSocket.prototype = {
try {
input.available();
} catch (e) {
this.callListener("error", new Error("Connection refused"));
// NS_ERROR_CONNECTION_REFUSED
this._maybeReportErrorAndCloseIfOpen(0x804B000C);
}
},
@ -540,7 +681,9 @@ TCPSocket.prototype = {
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
@ -549,15 +692,8 @@ TCPSocket.prototype = {
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)
@ -592,29 +728,4 @@ TCPSocket.prototype = {
])
}
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]);

View File

@ -127,10 +127,9 @@ TCPSocketChild::RecvCallback(const nsString& aType,
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();

View File

@ -28,9 +28,8 @@ 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)
@ -142,7 +141,7 @@ TCPSocketParent::SendCallback(const nsAString& aType, const JS::Value& aDataVal,
FireInteralError(this, __LINE__);
return NS_ERROR_OUT_OF_MEMORY;
}
data = str;
data = SendableData(str);
} else if (aDataVal.isUndefined() || aDataVal.isNull()) {
data = mozilla::void_t();
@ -166,40 +165,18 @@ TCPSocketParent::SendCallback(const nsAString& aType, const JS::Value& aDataVal,
data = SendableData(arr);
} else {
nsDependentJSString message, filename;
uint32_t lineNumber = 0;
uint32_t columnNumber = 0;
nsDependentJSString name;
JS::Value 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");

View File

@ -300,11 +300,11 @@ function sendData() {
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) {
@ -379,21 +379,25 @@ function bufferedClose() {
* 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();
};
}
@ -505,7 +509,7 @@ add_test(cleanup);
function run_test() {
if (!gInChild)
Services.prefs.setBoolPref('dom.mozTCPSocket.enabled', true);
server = new TestServer();
run_next_test();