/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * * 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 "nsLDAPInternal.h" #include "nsIServiceManager.h" #include "nsStringGlue.h" #include "nsIComponentManager.h" #include "nsIDNSRecord.h" #include "nsLDAPConnection.h" #include "nsLDAPMessage.h" #include "nsThreadUtils.h" #include "nsIConsoleService.h" #include "nsIDNSService.h" #include "nsINetAddr.h" #include "nsIRequestObserver.h" #include "nsError.h" #include "nsLDAPOperation.h" #include "nsILDAPErrors.h" #include "nsIClassInfoImpl.h" #include "nsILDAPURL.h" #include "nsIObserverService.h" #include "mozilla/Services.h" #include "nsMemory.h" #include "nsLDAPUtils.h" #include "nsProxyRelease.h" #include "mozilla/Logging.h" #include "mozilla/Attributes.h" using namespace mozilla; const char kDNSServiceContractId[] = "@mozilla.org/network/dns-service;1"; // constructor // nsLDAPConnection::nsLDAPConnection() : mConnectionHandle(nullptr), mPendingOperationsMutex("nsLDAPConnection.mPendingOperationsMutex"), mPendingOperations(10), mSSL(false), mVersion(nsILDAPConnection::VERSION3), mDNSRequest(nullptr) { } // destructor // nsLDAPConnection::~nsLDAPConnection() { nsCOMPtr obsServ = do_GetService(NS_OBSERVERSERVICE_CONTRACTID); if (obsServ) obsServ->RemoveObserver(this, "profile-change-net-teardown"); Close(); } NS_IMPL_ADDREF(nsLDAPConnection) NS_IMPL_RELEASE(nsLDAPConnection) NS_IMPL_CLASSINFO(nsLDAPConnection, NULL, nsIClassInfo::THREADSAFE, NS_LDAPCONNECTION_CID) NS_INTERFACE_MAP_BEGIN(nsLDAPConnection) NS_INTERFACE_MAP_ENTRY(nsILDAPConnection) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY(nsIDNSListener) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsILDAPConnection) NS_IMPL_QUERY_CLASSINFO(nsLDAPConnection) NS_INTERFACE_MAP_END_THREADSAFE NS_IMPL_CI_INTERFACE_GETTER(nsLDAPConnection, nsILDAPConnection, nsISupportsWeakReference, nsIDNSListener, nsIObserver) NS_IMETHODIMP nsLDAPConnection::Init(nsILDAPURL *aUrl, const nsACString &aBindName, nsILDAPMessageListener *aMessageListener, nsISupports *aClosure, uint32_t aVersion) { NS_ENSURE_ARG_POINTER(aUrl); NS_ENSURE_ARG_POINTER(aMessageListener); nsresult rv; nsCOMPtr obsServ = do_GetService(NS_OBSERVERSERVICE_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); // We have to abort all LDAP pending operation before shutdown. obsServ->AddObserver(this, "profile-change-net-teardown", true); // Save various items that we'll use later mBindName.Assign(aBindName); mClosure = aClosure; mInitListener = aMessageListener; // Make sure we haven't called Init earlier, i.e. there's a DNS // request pending. NS_ASSERTION(!mDNSRequest, "nsLDAPConnection::Init() " "Connection was already initialized\n"); // Check and save the version number if (aVersion != nsILDAPConnection::VERSION2 && aVersion != nsILDAPConnection::VERSION3) { NS_ERROR("nsLDAPConnection::Init(): illegal version"); return NS_ERROR_ILLEGAL_VALUE; } mVersion = aVersion; // Get the port number, SSL flag for use later, once the DNS server(s) // has resolved the host part. rv = aUrl->GetPort(&mPort); NS_ENSURE_SUCCESS(rv, rv); uint32_t options; rv = aUrl->GetOptions(&options); NS_ENSURE_SUCCESS(rv, rv); mSSL = options & nsILDAPURL::OPT_SECURE; nsCOMPtr curThread = do_GetCurrentThread(); if (!curThread) { NS_ERROR("nsLDAPConnection::Init(): couldn't get current thread"); return NS_ERROR_FAILURE; } // Do the pre-resolve of the hostname, using the DNS service. This // will also initialize the LDAP connection properly, once we have // the IPs resolved for the hostname. This includes creating the // new thread for this connection. // // XXX - What return codes can we expect from the DNS service? // nsCOMPtr pDNSService(do_GetService(kDNSServiceContractId, &rv)); if (NS_FAILED(rv)) { NS_ERROR("nsLDAPConnection::Init(): couldn't create the DNS Service object"); return NS_ERROR_FAILURE; } rv = aUrl->GetAsciiHost(mDNSHost); NS_ENSURE_SUCCESS(rv, rv); // if the caller has passed in a space-delimited set of hosts, as the // ldap c-sdk allows, strip off the trailing hosts for now. // Soon, we'd like to make multiple hosts work, but now make // at least the first one work. LdapCompressWhitespace(mDNSHost); int32_t spacePos = mDNSHost.FindChar(' '); // trim off trailing host(s) if (spacePos != kNotFound) mDNSHost.SetLength(spacePos); rv = pDNSService->AsyncResolve(mDNSHost, 0, this, curThread, getter_AddRefs(mDNSRequest)); if (NS_FAILED(rv)) { switch (rv) { case NS_ERROR_OUT_OF_MEMORY: case NS_ERROR_UNKNOWN_HOST: case NS_ERROR_FAILURE: case NS_ERROR_OFFLINE: break; default: rv = NS_ERROR_UNEXPECTED; } mDNSHost.Truncate(); } return rv; } // this might get exposed to clients, so we've broken it // out of the destructor. void nsLDAPConnection::Close() { int rc; MOZ_LOG(gLDAPLogModule, mozilla::LogLevel::Debug, ("unbinding\n")); if (mConnectionHandle) { // note that the ldap_unbind() call in the 5.0 version of the LDAP C SDK // appears to be exactly identical to ldap_unbind_s(), so it may in fact // still be synchronous // rc = ldap_unbind(mConnectionHandle); #ifdef PR_LOGGING if (rc != LDAP_SUCCESS) { MOZ_LOG(gLDAPLogModule, mozilla::LogLevel::Warning, ("nsLDAPConnection::Close(): %s\n", ldap_err2string(rc))); } #endif mConnectionHandle = nullptr; } MOZ_LOG(gLDAPLogModule, mozilla::LogLevel::Debug, ("unbound\n")); NS_ASSERTION(!mThread || NS_SUCCEEDED(mThread->Shutdown()), "Failed to shutdown thread cleanly"); // Cancel the DNS lookup if needed, and also drop the reference to the // Init listener (if still there). // if (mDNSRequest) { mDNSRequest->Cancel(NS_ERROR_ABORT); mDNSRequest = nullptr; } mInitListener = nullptr; } NS_IMETHODIMP nsLDAPConnection::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) { if (!strcmp(aTopic, "profile-change-net-teardown")) { // Abort all ldap requests. /* We cannot use enumerate function to abort operations because * nsILDAPOperation::AbandonExt() is modifying list of operations * and this leads to starvation. * We have to do a copy of pending operations. */ nsTArray pending_operations; { MutexAutoLock lock(mPendingOperationsMutex); for (auto iter = mPendingOperations.Iter(); !iter.Done(); iter.Next()) { pending_operations.AppendElement(iter.UserData()); } } for (uint32_t i = 0; i < pending_operations.Length(); i++) { pending_operations[i]->AbandonExt(); } Close(); } else { NS_NOTREACHED("unexpected topic"); return NS_ERROR_UNEXPECTED; } return NS_OK; } NS_IMETHODIMP nsLDAPConnection::GetClosure(nsISupports **_retval) { if (!_retval) { return NS_ERROR_ILLEGAL_VALUE; } NS_IF_ADDREF(*_retval = mClosure); return NS_OK; } NS_IMETHODIMP nsLDAPConnection::SetClosure(nsISupports *aClosure) { mClosure = aClosure; return NS_OK; } // who we're binding as // // readonly attribute AUTF8String bindName // NS_IMETHODIMP nsLDAPConnection::GetBindName(nsACString& _retval) { _retval.Assign(mBindName); return NS_OK; } // wrapper for ldap_get_lderrno // XXX should copy before returning // NS_IMETHODIMP nsLDAPConnection::GetLdErrno(nsACString& matched, nsACString& errString, int32_t *_retval) { char *match, *err; NS_ENSURE_ARG_POINTER(_retval); *_retval = ldap_get_lderrno(mConnectionHandle, &match, &err); matched.Assign(match); errString.Assign(err); return NS_OK; } // return the error string corresponding to GetLdErrno. // // XXX - deal with optional params // XXX - how does ldap_perror know to look at the global errno? // NS_IMETHODIMP nsLDAPConnection::GetErrorString(char16_t **_retval) { NS_ENSURE_ARG_POINTER(_retval); // get the error string // char *rv = ldap_err2string(ldap_get_lderrno(mConnectionHandle, 0, 0)); if (!rv) { return NS_ERROR_OUT_OF_MEMORY; } // make a copy using the XPCOM shared allocator // *_retval = ToNewUnicode(NS_ConvertUTF8toUTF16(rv)); if (!*_retval) { return NS_ERROR_OUT_OF_MEMORY; } return NS_OK; } /** * Add an nsILDAPOperation to the list of operations pending on * this connection. This is also mainly intended for use by the * nsLDAPOperation code. */ nsresult nsLDAPConnection::AddPendingOperation(uint32_t aOperationID, nsILDAPOperation *aOperation) { NS_ENSURE_ARG_POINTER(aOperation); nsIRunnable* runnable = new nsLDAPConnectionRunnable(aOperationID, aOperation, this); { MutexAutoLock lock(mPendingOperationsMutex); mPendingOperations.Put((uint32_t)aOperationID, aOperation); MOZ_LOG(gLDAPLogModule, mozilla::LogLevel::Debug, ("pending operation added; total pending operations now = %d\n", mPendingOperations.Count())); } nsresult rv; if (!mThread) { rv = NS_NewThread(getter_AddRefs(mThread), runnable); NS_ENSURE_SUCCESS(rv, rv); } else { rv = mThread->Dispatch(runnable, nsIEventTarget::DISPATCH_NORMAL); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } /** * Remove an nsILDAPOperation from the list of operations pending on this * connection. Mainly intended for use by the nsLDAPOperation code. * * @param aOperation operation to add * @exception NS_ERROR_INVALID_POINTER aOperation was NULL * @exception NS_ERROR_OUT_OF_MEMORY out of memory * @exception NS_ERROR_FAILURE could not delete the operation * * void removePendingOperation(in nsILDAPOperation aOperation); */ nsresult nsLDAPConnection::RemovePendingOperation(uint32_t aOperationID) { NS_ENSURE_TRUE(aOperationID > 0, NS_ERROR_UNEXPECTED); MOZ_LOG(gLDAPLogModule, mozilla::LogLevel::Debug, ("nsLDAPConnection::RemovePendingOperation(): operation removed\n")); { MutexAutoLock lock(mPendingOperationsMutex); mPendingOperations.Remove(aOperationID); MOZ_LOG(gLDAPLogModule, mozilla::LogLevel::Debug, ("nsLDAPConnection::RemovePendingOperation(): operation " "removed; total pending operations now = %d\n", mPendingOperations.Count())); } return NS_OK; } class nsOnLDAPMessageRunnable : public Runnable { public: nsOnLDAPMessageRunnable(nsLDAPMessage *aMsg, bool aClear) : m_msg(aMsg) , m_clear(aClear) {} NS_DECL_NSIRUNNABLE private: RefPtr m_msg; bool m_clear; }; NS_IMETHODIMP nsOnLDAPMessageRunnable::Run() { // get the message listener object. nsLDAPOperation *nsoperation = static_cast(m_msg->mOperation.get()); nsCOMPtr listener; nsresult rv = nsoperation->GetMessageListener(getter_AddRefs(listener)); if (m_clear) { // try to break cycles nsoperation->Clear(); } if (!listener) { NS_ERROR("nsLDAPConnection::InvokeMessageCallback(): probable " "memory corruption: GetMessageListener() returned nullptr"); return rv; } return listener->OnLDAPMessage(m_msg); } nsresult nsLDAPConnection::InvokeMessageCallback(LDAPMessage *aMsgHandle, nsILDAPMessage *aMsg, int32_t aOperation, bool aRemoveOpFromConnQ) { #if defined(DEBUG) // We only want this being logged for debug builds so as not to affect performance too much. MOZ_LOG(gLDAPLogModule, mozilla::LogLevel::Debug, ("InvokeMessageCallback entered\n")); #endif // Get the operation. nsCOMPtr operation; { MutexAutoLock lock(mPendingOperationsMutex); mPendingOperations.Get((uint32_t)aOperation, getter_AddRefs(operation)); } NS_ENSURE_TRUE(operation, NS_ERROR_NULL_POINTER); nsLDAPMessage *msg = static_cast(aMsg); msg->mOperation = operation; // proxy the listener callback to the ui thread. RefPtr runnable = new nsOnLDAPMessageRunnable(msg, aRemoveOpFromConnQ); // invoke the callback NS_DispatchToMainThread(runnable); // if requested (ie the operation is done), remove the operation // from the connection queue. if (aRemoveOpFromConnQ) { MutexAutoLock lock(mPendingOperationsMutex); mPendingOperations.Remove(aOperation); MOZ_LOG(gLDAPLogModule, mozilla::LogLevel::Debug, ("pending operation removed; total pending operations now =" " %d\n", mPendingOperations.Count())); } return NS_OK; } NS_IMETHODIMP nsLDAPConnection::OnLookupComplete(nsICancelable *aRequest, nsIDNSRecord *aRecord, nsresult aStatus) { nsresult rv = NS_OK; if (aRecord) { // Build mResolvedIP list // mResolvedIP.Truncate(); int32_t index = 0; nsCString addrbuf; nsCOMPtr addr; while (NS_SUCCEEDED(aRecord->GetScriptableNextAddr(0, getter_AddRefs(addr)))) { // We can only use v4 addresses // uint16_t family = 0; bool v4mapped = false; addr->GetFamily(&family); addr->GetIsV4Mapped(&v4mapped); if (family == nsINetAddr::FAMILY_INET || v4mapped) { // If there are more IPs in the list, we separate them with // a space, as supported/used by the LDAP C-SDK. // if (index++) mResolvedIP.Append(' '); // Convert the IPv4 address to a string, and append it to our // list of IPs. Strip leading '::FFFF:' (the IPv4-mapped-IPv6 // indicator) if present. // addr->GetAddress(addrbuf); if (addrbuf[0] == ':' && addrbuf.Length() > 7) mResolvedIP.Append(Substring(addrbuf, 7)); else mResolvedIP.Append(addrbuf); } } } if (NS_FAILED(aStatus)) { // The DNS service failed, lets pass something reasonable // back to the listener. // switch (aStatus) { case NS_ERROR_OUT_OF_MEMORY: case NS_ERROR_UNKNOWN_HOST: case NS_ERROR_FAILURE: case NS_ERROR_OFFLINE: rv = aStatus; break; default: rv = NS_ERROR_UNEXPECTED; break; } } else if (!mResolvedIP.Length()) { // We have no host resolved, that is very bad, and should most // likely have been caught earlier. // NS_ERROR("nsLDAPConnection::OnStopLookup(): the resolved IP " "string is empty.\n"); rv = NS_ERROR_UNKNOWN_HOST; } else { // We've got the IP(s) for the hostname, now lets setup the // LDAP connection using this information. Note that if the // LDAP server returns a referral, the C-SDK will perform a // new, synchronous DNS lookup, which might hang (but hopefully // if we've come this far, DNS is working properly). // mConnectionHandle = ldap_init(mResolvedIP.get(), mPort == -1 ? (mSSL ? LDAPS_PORT : LDAP_PORT) : mPort); // Check that we got a proper connection, and if so, setup the // threading functions for this connection. // if ( !mConnectionHandle ) { rv = NS_ERROR_FAILURE; // LDAP C SDK API gives no useful error } else { #if defined(DEBUG_dmose) || defined(DEBUG_bienvenu) const int lDebug = 0; ldap_set_option(mConnectionHandle, LDAP_OPT_DEBUG_LEVEL, &lDebug); #endif // the C SDK currently defaults to v2. if we're to use v3, // tell it so. // int version; switch (mVersion) { case 2: break; case 3: version = LDAP_VERSION3; ldap_set_option(mConnectionHandle, LDAP_OPT_PROTOCOL_VERSION, &version); break; default: NS_ERROR("nsLDAPConnection::OnLookupComplete(): mVersion" " invalid"); } // This code sets up the current connection to use PSM for SSL // functionality. Making this use libssldap instead for // non-browser user shouldn't be hard. extern nsresult nsLDAPInstallSSL(LDAP *ld, const char *aHostName); if (mSSL) { if (ldap_set_option(mConnectionHandle, LDAP_OPT_SSL, LDAP_OPT_ON) != LDAP_SUCCESS ) { NS_ERROR("nsLDAPConnection::OnStopLookup(): Error" " configuring connection to use SSL"); rv = NS_ERROR_UNEXPECTED; } rv = nsLDAPInstallSSL(mConnectionHandle, mDNSHost.get()); if (NS_FAILED(rv)) { NS_ERROR("nsLDAPConnection::OnStopLookup(): Error" " installing secure LDAP routines for" " connection"); } } } } // Drop the DNS request object, we no longer need it, and set the flag // indicating that DNS has finished. // mDNSRequest = nullptr; mDNSHost.Truncate(); // Call the listener, and then we can release our reference to it. // mInitListener->OnLDAPInit(this, rv); mInitListener = nullptr; return rv; } nsLDAPConnectionRunnable::nsLDAPConnectionRunnable(int32_t aOperationID, nsILDAPOperation *aOperation, nsLDAPConnection *aConnection) : mOperationID(aOperationID), mConnection(aConnection) { } nsLDAPConnectionRunnable::~nsLDAPConnectionRunnable() { if (mConnection) { NS_ReleaseOnMainThread(mConnection.forget()); } } NS_IMPL_ISUPPORTS(nsLDAPConnectionRunnable, nsIRunnable) NS_IMETHODIMP nsLDAPConnectionRunnable::Run() { if (!mOperationID) { NS_ERROR("mOperationID is null"); return NS_ERROR_NULL_POINTER; } LDAPMessage *msgHandle; bool operationFinished = true; RefPtr msg; struct timeval timeout = { 0, 0 }; nsCOMPtr thread = do_GetCurrentThread(); int32_t returnCode = ldap_result(mConnection->mConnectionHandle, mOperationID, LDAP_MSG_ONE, &timeout, &msgHandle); switch (returnCode) { // timeout case 0: // XXX do we need a timer? return thread->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); case -1: NS_ERROR("We don't know what went wrong with the ldap operation"); return NS_ERROR_FAILURE; case LDAP_RES_SEARCH_ENTRY: case LDAP_RES_SEARCH_REFERENCE: // XXX what should we do with LDAP_RES_SEARCH_EXTENDED operationFinished = false; MOZ_FALLTHROUGH; default: { msg = new nsLDAPMessage; if (!msg) return NS_ERROR_NULL_POINTER; // initialize the message, using a protected method not available // through nsILDAPMessage (which is why we need the raw pointer) nsresult rv = msg->Init(mConnection, msgHandle); switch (rv) { case NS_OK: { int32_t errorCode; msg->GetErrorCode(&errorCode); // maybe a version error, e.g., using v3 on a v2 server. // if we're using v3, try v2. if (errorCode == LDAP_PROTOCOL_ERROR && mConnection->mVersion == nsILDAPConnection::VERSION3) { nsAutoCString password; mConnection->mVersion = nsILDAPConnection::VERSION2; ldap_set_option(mConnection->mConnectionHandle, LDAP_OPT_PROTOCOL_VERSION, &mConnection->mVersion); if (NS_SUCCEEDED(rv)) { // We don't want to notify callers that we are done, so // redispatch the runnable. // XXX do we need a timer? rv = thread->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } } // If we're midway through a SASL Bind, we need to continue // without letting our caller know what we're up to! // if (errorCode == LDAP_SASL_BIND_IN_PROGRESS) { struct berval *creds; ldap_parse_sasl_bind_result( mConnection->mConnectionHandle, msgHandle, &creds, 0); nsCOMPtr operation; { MutexAutoLock lock(mConnection->mPendingOperationsMutex); mConnection->mPendingOperations.Get((uint32_t)mOperationID, getter_AddRefs(operation)); } NS_ENSURE_TRUE(operation, NS_ERROR_NULL_POINTER); nsresult rv = operation->SaslStep(creds->bv_val, creds->bv_len); if (NS_SUCCEEDED(rv)) return NS_OK; } break; } // Error code handling in here default: return NS_OK; } // invoke the callback on the nsILDAPOperation corresponding to // this message rv = mConnection->InvokeMessageCallback(msgHandle, msg, mOperationID, operationFinished); if (NS_FAILED(rv)) { NS_ERROR("CheckLDAPOperationResult(): error invoking message" " callback"); // punt and hope things work out better next time around return NS_OK; } if (!operationFinished) { // XXX do we need a timer? rv = thread->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); NS_ENSURE_SUCCESS(rv, rv); } break; } } return NS_OK; }