diff options
Diffstat (limited to 'dom/network/TCPSocket.cpp')
-rw-r--r-- | dom/network/TCPSocket.cpp | 1157 |
1 files changed, 1157 insertions, 0 deletions
diff --git a/dom/network/TCPSocket.cpp b/dom/network/TCPSocket.cpp new file mode 100644 index 0000000000..6dabd1a630 --- /dev/null +++ b/dom/network/TCPSocket.cpp @@ -0,0 +1,1157 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "TCPServerSocket.h" +#include "TCPSocket.h" +#include "TCPSocketChild.h" +#include "TCPSocketParent.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/TCPSocketBinding.h" +#include "mozilla/dom/TCPSocketErrorEvent.h" +#include "mozilla/dom/TCPSocketErrorEventBinding.h" +#include "mozilla/dom/TCPSocketEvent.h" +#include "mozilla/dom/TCPSocketEventBinding.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsIArrayBufferInputStream.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncStreamCopier.h" +#include "nsIBinaryInputStream.h" +#include "nsICancelable.h" +#include "nsIChannel.h" +#include "nsIInputStream.h" +#include "nsIInputStreamPump.h" +#include "nsIMultiplexInputStream.h" +#include "nsINSSErrorsService.h" +#include "nsIObserverService.h" +#include "nsIOutputStream.h" +#include "nsIProtocolProxyService.h" +#include "nsIScriptableInputStream.h" +#include "nsISocketTransport.h" +#include "nsISocketTransportService.h" +#include "nsISupportsPrimitives.h" +#include "nsITLSSocketControl.h" +#include "nsITransport.h" +#include "nsIURIMutator.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsStringStream.h" +#include "secerr.h" +#include "sslerr.h" + +#define BUFFER_SIZE 65536 +#define NETWORK_STATS_THRESHOLD 65536 + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION(LegacyMozTCPSocket, mGlobal) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(LegacyMozTCPSocket) +NS_IMPL_CYCLE_COLLECTING_RELEASE(LegacyMozTCPSocket) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LegacyMozTCPSocket) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +LegacyMozTCPSocket::LegacyMozTCPSocket(nsPIDOMWindowInner* aWindow) + : mGlobal(do_QueryInterface(aWindow)) {} + +LegacyMozTCPSocket::~LegacyMozTCPSocket() = default; + +already_AddRefed<TCPSocket> LegacyMozTCPSocket::Open( + const nsAString& aHost, uint16_t aPort, const SocketOptions& aOptions, + mozilla::ErrorResult& aRv) { + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(mGlobal))) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + GlobalObject globalObj(api.cx(), mGlobal->GetGlobalJSObject()); + return TCPSocket::Constructor(globalObj, aHost, aPort, aOptions, aRv); +} + +already_AddRefed<TCPServerSocket> LegacyMozTCPSocket::Listen( + uint16_t aPort, const ServerSocketOptions& aOptions, uint16_t aBacklog, + mozilla::ErrorResult& aRv) { + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(mGlobal))) { + return nullptr; + } + GlobalObject globalObj(api.cx(), mGlobal->GetGlobalJSObject()); + return TCPServerSocket::Constructor(globalObj, aPort, aOptions, aBacklog, + aRv); +} + +bool LegacyMozTCPSocket::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aReflector) { + return LegacyMozTCPSocket_Binding::Wrap(aCx, this, aGivenProto, aReflector); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(TCPSocket) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(TCPSocket, DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TCPSocket, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransport) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocketInputStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocketOutputStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputStreamPump) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputStreamScriptable) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputStreamBinary) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingDataAfterStartTLS) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocketBridgeChild) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocketBridgeParent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TCPSocket, DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransport) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocketInputStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocketOutputStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputStreamPump) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputStreamScriptable) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputStreamBinary) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingDataAfterStartTLS) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocketBridgeChild) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocketBridgeParent) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ADDREF_INHERITED(TCPSocket, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(TCPSocket, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TCPSocket) + NS_INTERFACE_MAP_ENTRY(nsITransportEventSink) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsITCPSocketCallback) + NS_INTERFACE_MAP_ENTRY(nsIProtocolProxyCallback) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +TCPSocket::TCPSocket(nsIGlobalObject* aGlobal, const nsAString& aHost, + uint16_t aPort, bool aSsl, bool aUseArrayBuffers) + : DOMEventTargetHelper(aGlobal), + mReadyState(TCPReadyState::Closed), + mUseArrayBuffers(aUseArrayBuffers), + mHost(aHost), + mPort(aPort), + mSsl(aSsl), + mAsyncCopierActive(false), + mWaitingForDrain(false), + mInnerWindowID(0), + mBufferedAmount(0), + mSuspendCount(0), + mTrackingNumber(0), + mWaitingForStartTLS(false), + mObserversActive(false) { + if (aGlobal) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal); + if (window) { + mInnerWindowID = window->WindowID(); + } + } +} + +TCPSocket::~TCPSocket() { + if (mObserversActive) { + nsCOMPtr<nsIObserverService> obs = + do_GetService("@mozilla.org/observer-service;1"); + if (obs) { + obs->RemoveObserver(this, "inner-window-destroyed"); + obs->RemoveObserver(this, "profile-change-net-teardown"); + } + } +} + +nsresult TCPSocket::CreateStream() { + nsresult rv = + mTransport->OpenInputStream(0, 0, 0, getter_AddRefs(mSocketInputStream)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mTransport->OpenOutputStream(nsITransport::OPEN_UNBUFFERED, 0, 0, + getter_AddRefs(mSocketOutputStream)); + NS_ENSURE_SUCCESS(rv, rv); + + // If the other side is not listening, we will + // get an onInputStreamReady callback where available + // raises to indicate the connection was refused. + nsCOMPtr<nsIAsyncInputStream> asyncStream = + do_QueryInterface(mSocketInputStream); + NS_ENSURE_TRUE(asyncStream, NS_ERROR_NOT_AVAILABLE); + + nsCOMPtr<nsISerialEventTarget> mainTarget = GetMainThreadSerialEventTarget(); + rv = asyncStream->AsyncWait(this, nsIAsyncInputStream::WAIT_CLOSURE_ONLY, 0, + mainTarget); + NS_ENSURE_SUCCESS(rv, rv); + + if (mUseArrayBuffers) { + mInputStreamBinary = + do_CreateInstance("@mozilla.org/binaryinputstream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = mInputStreamBinary->SetInputStream(mSocketInputStream); + NS_ENSURE_SUCCESS(rv, rv); + } else { + mInputStreamScriptable = + do_CreateInstance("@mozilla.org/scriptableinputstream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = mInputStreamScriptable->Init(mSocketInputStream); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult TCPSocket::InitWithUnconnectedTransport( + nsISocketTransport* aTransport) { + mReadyState = TCPReadyState::Connecting; + mTransport = aTransport; + + MOZ_ASSERT(XRE_GetProcessType() != GeckoProcessType_Content); + + nsCOMPtr<nsISerialEventTarget> mainTarget = GetMainThreadSerialEventTarget(); + mTransport->SetEventSink(this, mainTarget); + + nsresult rv = CreateStream(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult TCPSocket::Init(nsIProxyInfo* aProxyInfo) { + nsCOMPtr<nsIObserverService> obs = + do_GetService("@mozilla.org/observer-service;1"); + if (obs) { + mObserversActive = true; + obs->AddObserver(this, "inner-window-destroyed", true); // weak reference + obs->AddObserver(this, "profile-change-net-teardown", true); // weak ref + } + + if (XRE_GetProcessType() == GeckoProcessType_Content) { + mReadyState = TCPReadyState::Connecting; + + nsCOMPtr<nsISerialEventTarget> target; + if (nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal()) { + target = global->EventTargetFor(TaskCategory::Other); + } + mSocketBridgeChild = new TCPSocketChild(mHost, mPort, target); + mSocketBridgeChild->SendOpen(this, mSsl, mUseArrayBuffers); + return NS_OK; + } + + nsCOMPtr<nsISocketTransportService> sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + + AutoTArray<nsCString, 1> socketTypes; + if (mSsl) { + socketTypes.AppendElement("ssl"_ns); + } else { + socketTypes.AppendElement("starttls"_ns); + } + nsCOMPtr<nsISocketTransport> transport; + nsresult rv = + sts->CreateTransport(socketTypes, NS_ConvertUTF16toUTF8(mHost), mPort, + aProxyInfo, nullptr, getter_AddRefs(transport)); + NS_ENSURE_SUCCESS(rv, rv); + + return InitWithUnconnectedTransport(transport); +} + +void TCPSocket::InitWithSocketChild(TCPSocketChild* aSocketBridge) { + mSocketBridgeChild = aSocketBridge; + mReadyState = TCPReadyState::Open; + mSocketBridgeChild->SetSocket(this); + mSocketBridgeChild->GetHost(mHost); + mSocketBridgeChild->GetPort(&mPort); +} + +nsresult TCPSocket::InitWithTransport(nsISocketTransport* aTransport) { + mTransport = aTransport; + nsresult rv = CreateStream(); + NS_ENSURE_SUCCESS(rv, rv); + + mReadyState = TCPReadyState::Open; + rv = CreateInputStreamPump(); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString host; + mTransport->GetHost(host); + CopyUTF8toUTF16(host, mHost); + int32_t port; + mTransport->GetPort(&port); + mPort = port; + + return NS_OK; +} + +void TCPSocket::UpgradeToSecure(mozilla::ErrorResult& aRv) { + if (mReadyState != TCPReadyState::Open) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (mSsl) { + return; + } + + mSsl = true; + + if (mSocketBridgeChild) { + mSocketBridgeChild->SendStartTLS(); + return; + } + + if (!mAsyncCopierActive) { + ActivateTLS(); + } else { + mWaitingForStartTLS = true; + } +} + +namespace { +class CopierCallbacks final : public nsIRequestObserver { + RefPtr<TCPSocket> mOwner; + + public: + explicit CopierCallbacks(TCPSocket* aSocket) : mOwner(aSocket) {} + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + private: + ~CopierCallbacks() = default; +}; + +NS_IMPL_ISUPPORTS(CopierCallbacks, nsIRequestObserver) + +NS_IMETHODIMP +CopierCallbacks::OnStartRequest(nsIRequest* aRequest) { return NS_OK; } + +NS_IMETHODIMP +CopierCallbacks::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + mOwner->NotifyCopyComplete(aStatus); + mOwner = nullptr; + return NS_OK; +} +} // unnamed namespace + +void TCPSocket::CalculateBufferedAmount() { + // Let's update the buffered amount of data. + uint64_t bufferedAmount = 0; + for (uint32_t i = 0, len = mPendingData.Length(); i < len; ++i) { + nsCOMPtr<nsIInputStream> stream = mPendingData[i]; + uint64_t available = 0; + if (NS_SUCCEEDED(stream->Available(&available))) { + bufferedAmount += available; + } + } + mBufferedAmount = bufferedAmount; +} + +nsresult TCPSocket::EnsureCopying() { + if (mAsyncCopierActive) { + return NS_OK; + } + + mAsyncCopierActive = true; + + nsresult rv; + + nsCOMPtr<nsIMultiplexInputStream> multiplexStream = + do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIInputStream> stream = do_QueryInterface(multiplexStream); + + while (!mPendingData.IsEmpty()) { + nsCOMPtr<nsIInputStream> stream = mPendingData[0]; + multiplexStream->AppendStream(stream); + mPendingData.RemoveElementAt(0); + } + + nsCOMPtr<nsIAsyncStreamCopier> copier = + do_CreateInstance("@mozilla.org/network/async-stream-copier;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISocketTransportService> sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + + nsCOMPtr<nsISerialEventTarget> target = do_QueryInterface(sts); + rv = copier->Init(stream, mSocketOutputStream, target, + true, /* source buffered */ + false, /* sink buffered */ + BUFFER_SIZE, false, /* close source */ + false); /* close sink */ + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<CopierCallbacks> callbacks = new CopierCallbacks(this); + rv = copier->AsyncCopy(callbacks, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void TCPSocket::NotifyCopyComplete(nsresult aStatus) { + mAsyncCopierActive = false; + CalculateBufferedAmount(); + + if (mSocketBridgeParent && mSocketBridgeParent->IPCOpen()) { + mozilla::Unused << mSocketBridgeParent->SendUpdateBufferedAmount( + BufferedAmount(), mTrackingNumber); + } + + if (NS_FAILED(aStatus)) { + MaybeReportErrorAndCloseIfOpen(aStatus); + return; + } + + if (BufferedAmount() != 0) { + EnsureCopying(); + return; + } + + // Maybe we have some empty stream. We want to have an empty queue now. + mPendingData.Clear(); + + // If we are waiting for initiating starttls, we can begin to + // activate tls now. + if (mWaitingForStartTLS && mReadyState == TCPReadyState::Open) { + ActivateTLS(); + mWaitingForStartTLS = false; + // If we have pending data, we should send them, or fire + // a drain event if we are waiting for it. + if (!mPendingDataAfterStartTLS.IsEmpty()) { + mPendingData = std::move(mPendingDataAfterStartTLS); + EnsureCopying(); + return; + } + } + + // If we have a connected child, we let the child decide whether + // ondrain should be dispatched. + if (mWaitingForDrain && !mSocketBridgeParent) { + mWaitingForDrain = false; + FireEvent(u"drain"_ns); + } + + if (mReadyState == TCPReadyState::Closing) { + if (mSocketOutputStream) { + mSocketOutputStream->Close(); + mSocketOutputStream = nullptr; + } + mReadyState = TCPReadyState::Closed; + FireEvent(u"close"_ns); + } +} + +void TCPSocket::ActivateTLS() { + nsresult rv; + nsCOMPtr<nsIEventTarget> socketThread = + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + return; + } + + bool alreadyOnSTST = false; + if (NS_FAILED(socketThread->IsOnCurrentThread(&alreadyOnSTST))) { + return; + } + + if (alreadyOnSTST) { + ActivateTLSHelper(); + return; + } + + auto CallActivateTLS = [sock = RefPtr{this}]() mutable { + sock->ActivateTLSHelper(); + }; + mozilla::SyncRunnable::DispatchToThread( + socketThread, + NS_NewRunnableFunction("TCPSocket::UpgradeToSecure->ActivateTLSHelper", + CallActivateTLS)); +} + +void TCPSocket::ActivateTLSHelper() { + nsCOMPtr<nsITLSSocketControl> tlsSocketControl; + mTransport->GetTlsSocketControl(getter_AddRefs(tlsSocketControl)); + if (tlsSocketControl) { + tlsSocketControl->StartTLS(); + } +} + +NS_IMETHODIMP +TCPSocket::FireErrorEvent(const nsAString& aName, const nsAString& aType, + nsresult aErrorCode) { + if (mSocketBridgeParent) { + mSocketBridgeParent->FireErrorEvent(aName, aType, aErrorCode, mReadyState); + return NS_OK; + } + + TCPSocketErrorEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mName = aName; + init.mMessage = aType; + static_assert(std::is_same_v<std::underlying_type_t<nsresult>, uint32_t>); + init.mErrorCode = uint32_t(aErrorCode); + + RefPtr<TCPSocketErrorEvent> event = + TCPSocketErrorEvent::Constructor(this, u"error"_ns, init); + MOZ_ASSERT(event); + event->SetTrusted(true); + DispatchEvent(*event); + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::FireEvent(const nsAString& aType) { + if (mSocketBridgeParent) { + mSocketBridgeParent->FireEvent(aType, mReadyState); + return NS_OK; + } + + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(GetOwnerGlobal()))) { + return NS_ERROR_FAILURE; + } + JS::Rooted<JS::Value> val(api.cx()); + return FireDataEvent(api.cx(), aType, val); +} + +NS_IMETHODIMP +TCPSocket::FireDataArrayEvent(const nsAString& aType, + const nsTArray<uint8_t>& buffer) { + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(GetOwnerGlobal()))) { + return NS_ERROR_FAILURE; + } + JSContext* cx = api.cx(); + JS::Rooted<JS::Value> val(cx); + + bool ok = IPC::DeserializeArrayBuffer(cx, buffer, &val); + if (ok) { + return FireDataEvent(cx, aType, val); + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +TCPSocket::FireDataStringEvent(const nsAString& aType, + const nsACString& aString) { + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(GetOwnerGlobal()))) { + return NS_ERROR_FAILURE; + } + JSContext* cx = api.cx(); + JS::Rooted<JS::Value> val(cx); + + bool ok = ToJSValue(cx, NS_ConvertASCIItoUTF16(aString), &val); + if (ok) { + return FireDataEvent(cx, aType, val); + } + return NS_ERROR_FAILURE; +} + +nsresult TCPSocket::FireDataEvent(JSContext* aCx, const nsAString& aType, + JS::Handle<JS::Value> aData) { + MOZ_ASSERT(!mSocketBridgeParent); + + RootedDictionary<TCPSocketEventInit> init(aCx); + init.mBubbles = false; + init.mCancelable = false; + init.mData = aData; + + RefPtr<TCPSocketEvent> event = TCPSocketEvent::Constructor(this, aType, init); + event->SetTrusted(true); + DispatchEvent(*event); + return NS_OK; +} + +JSObject* TCPSocket::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return TCPSocket_Binding::Wrap(aCx, this, aGivenProto); +} + +void TCPSocket::GetHost(nsAString& aHost) { aHost.Assign(mHost); } + +uint32_t TCPSocket::Port() const { return mPort; } + +bool TCPSocket::Ssl() const { return mSsl; } + +void TCPSocket::Suspend() { + if (mSocketBridgeChild) { + mSocketBridgeChild->SendSuspend(); + return; + } + if (mInputStreamPump) { + mInputStreamPump->Suspend(); + } + mSuspendCount++; +} + +void TCPSocket::Resume(mozilla::ErrorResult& aRv) { + if (mSocketBridgeChild) { + mSocketBridgeChild->SendResume(); + return; + } + if (!mSuspendCount) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (mInputStreamPump) { + mInputStreamPump->Resume(); + } + mSuspendCount--; +} + +nsresult TCPSocket::MaybeReportErrorAndCloseIfOpen(nsresult status) { + // If we're closed, we've already reported the error or just don't need to + // report the error. + if (mReadyState == TCPReadyState::Closed) { + return NS_OK; + } + + // go through ::Closing state and then mark ::Closed + Close(); + mReadyState = TCPReadyState::Closed; + + if (NS_FAILED(status)) { + // Convert the status code to an appropriate error message. + + nsString errorType, errName; + + // security module? (and this is an error) + if ((static_cast<uint32_t>(status) & 0xFF0000) == 0x5a0000) { + nsCOMPtr<nsINSSErrorsService> errSvc = + do_GetService("@mozilla.org/nss_errors_service;1"); + // getErrorClass will throw a generic NS_ERROR_FAILURE if the error code + // is somehow not in the set of covered errors. + uint32_t errorClass; + nsresult rv = errSvc->GetErrorClass(status, &errorClass); + if (NS_FAILED(rv)) { + errorType.AssignLiteral("SecurityProtocol"); + } else { + switch (errorClass) { + case nsINSSErrorsService::ERROR_CLASS_BAD_CERT: + errorType.AssignLiteral("SecurityCertificate"); + break; + default: + errorType.AssignLiteral("SecurityProtocol"); + break; + } + } + + // NSS_SEC errors (happen below the base value because of negative vals) + if ((static_cast<int32_t>(status) & 0xFFFF) < + abs(nsINSSErrorsService::NSS_SEC_ERROR_BASE)) { + switch (static_cast<SECErrorCodes>(status)) { + case SEC_ERROR_EXPIRED_CERTIFICATE: + errName.AssignLiteral("SecurityExpiredCertificateError"); + break; + case SEC_ERROR_REVOKED_CERTIFICATE: + errName.AssignLiteral("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 SEC_ERROR_UNKNOWN_ISSUER: + case SEC_ERROR_UNTRUSTED_ISSUER: + case SEC_ERROR_UNTRUSTED_CERT: + case SEC_ERROR_CA_CERT_INVALID: + errName.AssignLiteral("SecurityUntrustedCertificateIssuerError"); + break; + case SEC_ERROR_INADEQUATE_KEY_USAGE: + errName.AssignLiteral("SecurityInadequateKeyUsageError"); + break; + case SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED: + errName.AssignLiteral( + "SecurityCertificateSignatureAlgorithmDisabledError"); + break; + default: + errName.AssignLiteral("SecurityError"); + break; + } + } else { + // NSS_SSL errors + switch (static_cast<SSLErrorCodes>(status)) { + case SSL_ERROR_NO_CERTIFICATE: + errName.AssignLiteral("SecurityNoCertificateError"); + break; + case SSL_ERROR_BAD_CERTIFICATE: + errName.AssignLiteral("SecurityBadCertificateError"); + break; + case SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE: + errName.AssignLiteral("SecurityUnsupportedCertificateTypeError"); + break; + case SSL_ERROR_UNSUPPORTED_VERSION: + errName.AssignLiteral("SecurityUnsupportedTLSVersionError"); + break; + case SSL_ERROR_BAD_CERT_DOMAIN: + errName.AssignLiteral("SecurityCertificateDomainMismatchError"); + break; + default: + errName.AssignLiteral("SecurityError"); + break; + } + } + } else { + // must be network + errorType.AssignLiteral("Network"); + + switch (status) { + // connect to host:port failed + case NS_ERROR_CONNECTION_REFUSED: + errName.AssignLiteral("ConnectionRefusedError"); + break; + // network timeout error + case NS_ERROR_NET_TIMEOUT: + errName.AssignLiteral("NetworkTimeoutError"); + break; + // hostname lookup failed + case NS_ERROR_UNKNOWN_HOST: + errName.AssignLiteral("DomainNotFoundError"); + break; + case NS_ERROR_NET_INTERRUPT: + errName.AssignLiteral("NetworkInterruptError"); + break; + default: + errName.AssignLiteral("NetworkError"); + break; + } + } + + Unused << NS_WARN_IF(NS_FAILED(FireErrorEvent(errName, errorType, status))); + } + + return FireEvent(u"close"_ns); +} + +void TCPSocket::Close() { CloseHelper(true); } + +void TCPSocket::CloseImmediately() { CloseHelper(false); } + +void TCPSocket::CloseHelper(bool waitForUnsentData) { + if (mReadyState == TCPReadyState::Closed || + mReadyState == TCPReadyState::Closing) { + return; + } + + mReadyState = TCPReadyState::Closing; + + if (mProxyRequest) { + mProxyRequest->Cancel(NS_BINDING_ABORTED); + mProxyRequest = nullptr; + } + + if (mSocketBridgeChild) { + mSocketBridgeChild->SendClose(); + return; + } + + if (!mAsyncCopierActive || !waitForUnsentData) { + mPendingData.Clear(); + mPendingDataAfterStartTLS.Clear(); + + if (mSocketOutputStream) { + mSocketOutputStream->Close(); + mSocketOutputStream = nullptr; + } + } + + if (mSocketInputStream) { + mSocketInputStream->Close(); + mSocketInputStream = nullptr; + } +} + +bool TCPSocket::Send(const nsACString& aData, mozilla::ErrorResult& aRv) { + if (mReadyState != TCPReadyState::Open) { + aRv.Throw(NS_ERROR_FAILURE); + return false; + } + + uint64_t byteLength; + nsCOMPtr<nsIInputStream> stream; + if (mSocketBridgeChild) { + mSocketBridgeChild->SendSend(aData); + byteLength = aData.Length(); + } else { + nsresult rv = NS_NewCStringInputStream(getter_AddRefs(stream), aData); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return false; + } + rv = stream->Available(&byteLength); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return false; + } + } + return Send(stream, byteLength); +} + +bool TCPSocket::Send(const ArrayBuffer& aData, uint32_t aByteOffset, + const Optional<uint32_t>& aByteLength, + mozilla::ErrorResult& aRv) { + if (mReadyState != TCPReadyState::Open) { + aRv.Throw(NS_ERROR_FAILURE); + return false; + } + + nsCOMPtr<nsIArrayBufferInputStream> stream; + + aData.ComputeState(); + uint32_t byteLength = + aByteLength.WasPassed() ? aByteLength.Value() : aData.Length(); + + if (mSocketBridgeChild) { + nsresult rv = mSocketBridgeChild->SendSend(aData, aByteOffset, byteLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return false; + } + } else { + JS::Rooted<JS::Value> value(RootingCx(), JS::ObjectValue(*aData.Obj())); + + stream = do_CreateInstance("@mozilla.org/io/arraybuffer-input-stream;1"); + nsresult rv = stream->SetData(value, aByteOffset, byteLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return false; + } + } + return Send(stream, byteLength); +} + +bool TCPSocket::Send(nsIInputStream* aStream, uint32_t aByteLength) { + uint64_t newBufferedAmount = BufferedAmount() + aByteLength; + bool bufferFull = newBufferedAmount > BUFFER_SIZE; + if (bufferFull) { + // 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. + mWaitingForDrain = true; + } + + if (mSocketBridgeChild) { + // In the child, we just add the buffer length to our bufferedAmount and let + // the parent update our bufferedAmount when the data have been sent. + mBufferedAmount = newBufferedAmount; + return !bufferFull; + } + + // This is used to track how many packets we have been told to send. Signaling + // this back to the user of this API allows the user to know how many packets + // are currently in flight over IPC. + ++mTrackingNumber; + if (mWaitingForStartTLS) { + // When we are waiting for starttls, newStream is added to pendingData + // and will be appended to multiplexStream after tls had been set up. + mPendingDataAfterStartTLS.AppendElement(aStream); + } else { + mPendingData.AppendElement(aStream); + } + + CalculateBufferedAmount(); + EnsureCopying(); + + return !bufferFull; +} + +TCPReadyState TCPSocket::ReadyState() { return mReadyState; } + +TCPSocketBinaryType TCPSocket::BinaryType() const { + if (mUseArrayBuffers) { + return TCPSocketBinaryType::Arraybuffer; + } + return TCPSocketBinaryType::String; +} + +already_AddRefed<TCPSocket> TCPSocket::CreateAcceptedSocket( + nsIGlobalObject* aGlobal, nsISocketTransport* aTransport, + bool aUseArrayBuffers) { + RefPtr<TCPSocket> socket = + new TCPSocket(aGlobal, u""_ns, 0, false, aUseArrayBuffers); + nsresult rv = socket->InitWithTransport(aTransport); + NS_ENSURE_SUCCESS(rv, nullptr); + return socket.forget(); +} + +already_AddRefed<TCPSocket> TCPSocket::CreateAcceptedSocket( + nsIGlobalObject* aGlobal, TCPSocketChild* aBridge, bool aUseArrayBuffers) { + RefPtr<TCPSocket> socket = + new TCPSocket(aGlobal, u""_ns, 0, false, aUseArrayBuffers); + socket->InitWithSocketChild(aBridge); + return socket.forget(); +} + +already_AddRefed<TCPSocket> TCPSocket::Constructor( + const GlobalObject& aGlobal, const nsAString& aHost, uint16_t aPort, + const SocketOptions& aOptions, mozilla::ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<TCPSocket> socket = + new TCPSocket(global, aHost, aPort, aOptions.mUseSecureTransport, + aOptions.mBinaryType == TCPSocketBinaryType::Arraybuffer); + socket->ResolveProxy(); + + return socket.forget(); +} + +nsresult TCPSocket::ResolveProxy() { + nsresult rv; + nsCOMPtr<nsIProtocolProxyService> pps = + do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIURI> uri; + nsCString spec = mSsl ? "https://"_ns : "http://"_ns; + bool maybeIPv6 = mHost.FindChar(':') != -1; + if (maybeIPv6) spec.Append('['); + if (!AppendUTF16toUTF8(mHost, spec, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + if (maybeIPv6) spec.Append(']'); + rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec(spec) + .SetPort(mPort) + .Finalize(uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannel(getter_AddRefs(channel), uri, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = pps->AsyncResolve(channel, + nsIProtocolProxyService::RESOLVE_PREFER_SOCKS_PROXY, + this, nullptr, getter_AddRefs(mProxyRequest)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnProxyAvailable(nsICancelable* aRequest, nsIChannel* aChannel, + nsIProxyInfo* aProxyInfo, nsresult aResult) { + mProxyRequest = nullptr; + if (NS_SUCCEEDED(aResult) && aProxyInfo) { + nsCString proxyType; + nsresult rv = aProxyInfo->GetType(proxyType); + if (NS_WARN_IF(NS_FAILED(rv))) { + Close(); + return rv; + } + // Only supports SOCKS proxy for now. + if (proxyType == "socks" || proxyType == "socks4") { + return Init(aProxyInfo); + } + } + return Init(nullptr); +} + +nsresult TCPSocket::CreateInputStreamPump() { + if (!mSocketInputStream) { + return NS_ERROR_NOT_AVAILABLE; + } + nsresult rv; + mInputStreamPump = + do_CreateInstance("@mozilla.org/network/input-stream-pump;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mInputStreamPump->Init(mSocketInputStream, 0, 0, false, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + uint64_t suspendCount = mSuspendCount; + while (suspendCount--) { + mInputStreamPump->Suspend(); + } + + rv = mInputStreamPump->AsyncRead(this); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnTransportStatus(nsITransport* aTransport, nsresult aStatus, + int64_t aProgress, int64_t aProgressMax) { + if (static_cast<uint32_t>(aStatus) != + static_cast<uint32_t>(nsISocketTransport::STATUS_CONNECTED_TO)) { + return NS_OK; + } + + mReadyState = TCPReadyState::Open; + nsresult rv = CreateInputStreamPump(); + NS_ENSURE_SUCCESS(rv, rv); + FireEvent(u"open"_ns); + + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnInputStreamReady(nsIAsyncInputStream* aStream) { + // Only used for detecting if the connection was refused. + + uint64_t dummy; + nsresult rv = aStream->Available(&dummy); + if (NS_FAILED(rv)) { + MaybeReportErrorAndCloseIfOpen(NS_ERROR_CONNECTION_REFUSED); + } + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnStartRequest(nsIRequest* aRequest) { return NS_OK; } + +NS_IMETHODIMP +TCPSocket::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + if (mUseArrayBuffers) { + nsTArray<uint8_t> buffer; + buffer.SetCapacity(aCount); + uint32_t actual; + nsresult rv = aStream->Read(reinterpret_cast<char*>(buffer.Elements()), + aCount, &actual); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(actual == aCount); + buffer.SetLength(actual); + + if (mSocketBridgeParent) { + mSocketBridgeParent->FireArrayBufferDataEvent(buffer, mReadyState); + return NS_OK; + } + + AutoJSAPI api; + if (!api.Init(GetOwnerGlobal())) { + return NS_ERROR_FAILURE; + } + JSContext* cx = api.cx(); + + JS::Rooted<JS::Value> value(cx); + if (!ToJSValue(cx, TypedArrayCreator<ArrayBuffer>(buffer), &value)) { + return NS_ERROR_FAILURE; + } + FireDataEvent(cx, u"data"_ns, value); + return NS_OK; + } + + nsCString data; + nsresult rv = mInputStreamScriptable->ReadBytes(aCount, data); + NS_ENSURE_SUCCESS(rv, rv); + + if (mSocketBridgeParent) { + mSocketBridgeParent->FireStringDataEvent(data, mReadyState); + return NS_OK; + } + + AutoJSAPI api; + if (!api.Init(GetOwnerGlobal())) { + return NS_ERROR_FAILURE; + } + JSContext* cx = api.cx(); + + JS::Rooted<JS::Value> value(cx); + if (!ToJSValue(cx, NS_ConvertASCIItoUTF16(data), &value)) { + return NS_ERROR_FAILURE; + } + FireDataEvent(cx, u"data"_ns, value); + + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + mInputStreamPump = nullptr; + + if (mAsyncCopierActive && NS_SUCCEEDED(aStatus)) { + // 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 NS_OK; + } + + // We call this even if there is no error. + MaybeReportErrorAndCloseIfOpen(aStatus); + return NS_OK; +} + +void TCPSocket::SetSocketBridgeParent(TCPSocketParent* aBridgeParent) { + MOZ_ASSERT(NS_IsMainThread()); + + mSocketBridgeParent = aBridgeParent; +} + +NS_IMETHODIMP +TCPSocket::UpdateReadyState(uint32_t aReadyState) { + MOZ_ASSERT(mSocketBridgeChild); + mReadyState = static_cast<TCPReadyState>(aReadyState); + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::UpdateBufferedAmount(uint32_t aBufferedAmount, + uint32_t aTrackingNumber) { + if (aTrackingNumber != mTrackingNumber) { + return NS_OK; + } + mBufferedAmount = aBufferedAmount; + if (!mBufferedAmount) { + if (mWaitingForDrain) { + mWaitingForDrain = false; + return FireEvent(u"drain"_ns); + } + } + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, "inner-window-destroyed")) { + nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject); + NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); + uint64_t innerID; + nsresult rv = wrapper->GetData(&innerID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (innerID == mInnerWindowID) { + Close(); + } + } else if (!strcmp(aTopic, "profile-change-net-teardown")) { + Close(); + } + + return NS_OK; +} + +/* static */ +bool TCPSocket::ShouldTCPSocketExist(JSContext* aCx, JSObject* aGlobal) { + JS::Rooted<JSObject*> global(aCx, aGlobal); + return nsContentUtils::ObjectPrincipal(global)->IsSystemPrincipal(); +} |