/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set expandtab ts=4 sw=2 sts=2 cin: */ /* 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/. */ // HttpLog.h should generally be included first #include "HttpLog.h" #include "mozilla/BasePrincipal.h" #include "mozilla/StoragePrincipalHelper.h" #include "mozilla/Tokenizer.h" #include "MockHttpAuth.h" #include "nsHttpChannelAuthProvider.h" #include "nsCRT.h" #include "nsNetUtil.h" #include "nsHttpHandler.h" #include "nsIHttpAuthenticator.h" #include "nsIHttpChannelInternal.h" #include "nsIAuthPrompt2.h" #include "nsIAuthPromptProvider.h" #include "nsIInterfaceRequestor.h" #include "nsIInterfaceRequestorUtils.h" #include "nsEscape.h" #include "nsAuthInformationHolder.h" #include "nsIStringBundle.h" #include "nsIPromptService.h" #include "netCore.h" #include "nsIHttpAuthenticableChannel.h" #include "nsIURI.h" #include "nsContentUtils.h" #include "nsHttp.h" #include "nsHttpBasicAuth.h" #include "nsHttpDigestAuth.h" #ifdef MOZ_AUTH_EXTENSION # include "nsHttpNegotiateAuth.h" #endif #include "nsHttpNTLMAuth.h" #include "nsServiceManagerUtils.h" #include "nsIURL.h" #include "mozilla/StaticPrefs_network.h" #include "mozilla/StaticPrefs_prompts.h" #include "mozilla/Telemetry.h" #include "nsIProxiedChannel.h" #include "nsIProxyInfo.h" namespace mozilla::net { #define SUBRESOURCE_AUTH_DIALOG_DISALLOW_ALL 0 #define SUBRESOURCE_AUTH_DIALOG_DISALLOW_CROSS_ORIGIN 1 #define SUBRESOURCE_AUTH_DIALOG_ALLOW_ALL 2 #define HTTP_AUTH_DIALOG_TOP_LEVEL_DOC 29 #define HTTP_AUTH_DIALOG_SAME_ORIGIN_SUBRESOURCE 30 #define HTTP_AUTH_DIALOG_SAME_ORIGIN_XHR 31 #define HTTP_AUTH_DIALOG_NON_WEB_CONTENT 32 #define HTTP_AUTH_BASIC_INSECURE 0 #define HTTP_AUTH_BASIC_SECURE 1 #define HTTP_AUTH_DIGEST_INSECURE 2 #define HTTP_AUTH_DIGEST_SECURE 3 #define HTTP_AUTH_NTLM_INSECURE 4 #define HTTP_AUTH_NTLM_SECURE 5 #define HTTP_AUTH_NEGOTIATE_INSECURE 6 #define HTTP_AUTH_NEGOTIATE_SECURE 7 #define MAX_DISPLAYED_USER_LENGTH 64 #define MAX_DISPLAYED_HOST_LENGTH 64 static void GetOriginAttributesSuffix(nsIChannel* aChan, nsACString& aSuffix) { OriginAttributes oa; // Deliberately ignoring the result and going with defaults if (aChan) { StoragePrincipalHelper::GetOriginAttributesForNetworkState(aChan, oa); } oa.CreateSuffix(aSuffix); } nsHttpChannelAuthProvider::nsHttpChannelAuthProvider() : mProxyAuth(false), mTriedProxyAuth(false), mTriedHostAuth(false), mSuppressDefensiveAuth(false), mCrossOrigin(false), mConnectionBased(false), mHttpHandler(gHttpHandler) {} nsHttpChannelAuthProvider::~nsHttpChannelAuthProvider() { MOZ_ASSERT(!mAuthChannel, "Disconnect wasn't called"); } NS_IMETHODIMP nsHttpChannelAuthProvider::Init(nsIHttpAuthenticableChannel* channel) { MOZ_ASSERT(channel, "channel expected!"); mAuthChannel = channel; nsresult rv = mAuthChannel->GetURI(getter_AddRefs(mURI)); if (NS_FAILED(rv)) return rv; rv = mAuthChannel->GetIsSSL(&mUsingSSL); if (NS_FAILED(rv)) return rv; nsCOMPtr proxied(channel); if (proxied) { nsCOMPtr pi; rv = proxied->GetProxyInfo(getter_AddRefs(pi)); if (NS_FAILED(rv)) return rv; if (pi) { nsAutoCString proxyType; rv = pi->GetType(proxyType); if (NS_FAILED(rv)) return rv; mProxyUsingSSL = proxyType.EqualsLiteral("https"); } } rv = mURI->GetAsciiHost(mHost); if (NS_FAILED(rv)) return rv; // reject the URL if it doesn't specify a host if (mHost.IsEmpty()) return NS_ERROR_MALFORMED_URI; rv = mURI->GetPort(&mPort); if (NS_FAILED(rv)) return rv; nsCOMPtr bareChannel = do_QueryInterface(channel); mIsPrivate = NS_UsePrivateBrowsing(bareChannel); return NS_OK; } NS_IMETHODIMP nsHttpChannelAuthProvider::ProcessAuthentication(uint32_t httpStatus, bool SSLConnectFailed) { LOG( ("nsHttpChannelAuthProvider::ProcessAuthentication " "[this=%p channel=%p code=%u SSLConnectFailed=%d]\n", this, mAuthChannel, httpStatus, SSLConnectFailed)); MOZ_ASSERT(mAuthChannel, "Channel not initialized"); nsCOMPtr proxyInfo; nsresult rv = mAuthChannel->GetProxyInfo(getter_AddRefs(proxyInfo)); if (NS_FAILED(rv)) return rv; if (proxyInfo) { mProxyInfo = do_QueryInterface(proxyInfo); if (!mProxyInfo) return NS_ERROR_NO_INTERFACE; } nsAutoCString challenges; mProxyAuth = (httpStatus == 407); rv = PrepareForAuthentication(mProxyAuth); if (NS_FAILED(rv)) return rv; if (mProxyAuth) { // only allow a proxy challenge if we have a proxy server configured. // otherwise, we could inadvertently expose the user's proxy // credentials to an origin server. We could attempt to proceed as // if we had received a 401 from the server, but why risk flirting // with trouble? IE similarly rejects 407s when a proxy server is // not configured, so there's no reason not to do the same. if (!UsingHttpProxy()) { LOG(("rejecting 407 when proxy server not configured!\n")); return NS_ERROR_UNEXPECTED; } if (UsingSSL() && !SSLConnectFailed) { // we need to verify that this challenge came from the proxy // server itself, and not some server on the other side of the // SSL tunnel. LOG(("rejecting 407 from origin server!\n")); return NS_ERROR_UNEXPECTED; } rv = mAuthChannel->GetProxyChallenges(challenges); } else { rv = mAuthChannel->GetWWWChallenges(challenges); } if (NS_FAILED(rv)) return rv; nsAutoCString creds; rv = GetCredentials(challenges, mProxyAuth, creds); if (rv == NS_ERROR_IN_PROGRESS) return rv; if (NS_FAILED(rv)) { LOG(("unable to authenticate\n")); } else { // set the authentication credentials if (mProxyAuth) { rv = mAuthChannel->SetProxyCredentials(creds); } else { rv = mAuthChannel->SetWWWCredentials(creds); } } return rv; } NS_IMETHODIMP nsHttpChannelAuthProvider::AddAuthorizationHeaders( bool aDontUseCachedWWWCreds) { LOG( ("nsHttpChannelAuthProvider::AddAuthorizationHeaders? " "[this=%p channel=%p]\n", this, mAuthChannel)); MOZ_ASSERT(mAuthChannel, "Channel not initialized"); nsCOMPtr proxyInfo; nsresult rv = mAuthChannel->GetProxyInfo(getter_AddRefs(proxyInfo)); if (NS_FAILED(rv)) return rv; if (proxyInfo) { mProxyInfo = do_QueryInterface(proxyInfo); if (!mProxyInfo) return NS_ERROR_NO_INTERFACE; } uint32_t loadFlags; rv = mAuthChannel->GetLoadFlags(&loadFlags); if (NS_FAILED(rv)) return rv; // this getter never fails nsHttpAuthCache* authCache = gHttpHandler->AuthCache(mIsPrivate); // check if proxy credentials should be sent if (!ProxyHost().IsEmpty() && UsingHttpProxy()) { SetAuthorizationHeader(authCache, nsHttp::Proxy_Authorization, "http"_ns, ProxyHost(), ProxyPort(), ""_ns, // proxy has no path mProxyIdent); } if (loadFlags & nsIRequest::LOAD_ANONYMOUS) { LOG(("Skipping Authorization header for anonymous load\n")); return NS_OK; } if (aDontUseCachedWWWCreds) { LOG( ("Authorization header already present:" " skipping adding auth header from cache\n")); return NS_OK; } // check if server credentials should be sent nsAutoCString path, scheme; if (NS_SUCCEEDED(GetCurrentPath(path)) && NS_SUCCEEDED(mURI->GetScheme(scheme))) { SetAuthorizationHeader(authCache, nsHttp::Authorization, scheme, Host(), Port(), path, mIdent); } return NS_OK; } NS_IMETHODIMP nsHttpChannelAuthProvider::CheckForSuperfluousAuth() { LOG( ("nsHttpChannelAuthProvider::CheckForSuperfluousAuth? " "[this=%p channel=%p]\n", this, mAuthChannel)); MOZ_ASSERT(mAuthChannel, "Channel not initialized"); // we've been called because it has been determined that this channel is // getting loaded without taking the userpass from the URL. if the URL // contained a userpass, then (provided some other conditions are true), // we'll give the user an opportunity to abort the channel as this might be // an attempt to spoof a different site (see bug 232567). if (!ConfirmAuth("SuperfluousAuth", true)) { // calling cancel here sets our mStatus and aborts the HTTP // transaction, which prevents OnDataAvailable events. Unused << mAuthChannel->Cancel(NS_ERROR_ABORT); return NS_ERROR_ABORT; } return NS_OK; } NS_IMETHODIMP nsHttpChannelAuthProvider::Cancel(nsresult status) { MOZ_ASSERT(mAuthChannel, "Channel not initialized"); if (mAsyncPromptAuthCancelable) { mAsyncPromptAuthCancelable->Cancel(status); mAsyncPromptAuthCancelable = nullptr; } if (mGenerateCredentialsCancelable) { mGenerateCredentialsCancelable->Cancel(status); mGenerateCredentialsCancelable = nullptr; } return NS_OK; } NS_IMETHODIMP nsHttpChannelAuthProvider::Disconnect(nsresult status) { mAuthChannel = nullptr; if (mAsyncPromptAuthCancelable) { mAsyncPromptAuthCancelable->Cancel(status); mAsyncPromptAuthCancelable = nullptr; } if (mGenerateCredentialsCancelable) { mGenerateCredentialsCancelable->Cancel(status); mGenerateCredentialsCancelable = nullptr; } NS_IF_RELEASE(mProxyAuthContinuationState); NS_IF_RELEASE(mAuthContinuationState); return NS_OK; } // helper function for getting an auth prompt from an interface requestor static void GetAuthPrompt(nsIInterfaceRequestor* ifreq, bool proxyAuth, nsIAuthPrompt2** result) { if (!ifreq) return; uint32_t promptReason; if (proxyAuth) { promptReason = nsIAuthPromptProvider::PROMPT_PROXY; } else { promptReason = nsIAuthPromptProvider::PROMPT_NORMAL; } nsCOMPtr promptProvider = do_GetInterface(ifreq); if (promptProvider) { promptProvider->GetAuthPrompt(promptReason, NS_GET_IID(nsIAuthPrompt2), reinterpret_cast(result)); } else { NS_QueryAuthPrompt2(ifreq, result); } } // generate credentials for the given challenge, and update the auth cache. nsresult nsHttpChannelAuthProvider::GenCredsAndSetEntry( nsIHttpAuthenticator* auth, bool proxyAuth, const nsACString& scheme, const nsACString& host, int32_t port, const nsACString& directory, const nsACString& realm, const nsACString& challenge, const nsHttpAuthIdentity& ident, nsCOMPtr& sessionState, nsACString& result) { nsresult rv; nsISupports* ss = sessionState; // set informations that depend on whether // we're authenticating against a proxy // or a webserver nsISupports** continuationState; if (proxyAuth) { continuationState = &mProxyAuthContinuationState; } else { continuationState = &mAuthContinuationState; } rv = auth->GenerateCredentialsAsync( mAuthChannel, this, challenge, proxyAuth, ident.Domain(), ident.User(), ident.Password(), ss, *continuationState, getter_AddRefs(mGenerateCredentialsCancelable)); if (NS_SUCCEEDED(rv)) { // Calling generate credentials async, results will be dispatched to the // main thread by calling OnCredsGenerated method return NS_ERROR_IN_PROGRESS; } uint32_t generateFlags; rv = auth->GenerateCredentials( mAuthChannel, challenge, proxyAuth, ident.Domain(), ident.User(), ident.Password(), &ss, &*continuationState, &generateFlags, result); sessionState.swap(ss); if (NS_FAILED(rv)) return rv; // don't log this in release build since it could contain sensitive info. #ifdef DEBUG LOG(("generated creds: %s\n", result.BeginReading())); #endif return UpdateCache(auth, scheme, host, port, directory, realm, challenge, ident, result, generateFlags, sessionState, proxyAuth); } nsresult nsHttpChannelAuthProvider::UpdateCache( nsIHttpAuthenticator* auth, const nsACString& scheme, const nsACString& host, int32_t port, const nsACString& directory, const nsACString& realm, const nsACString& challenge, const nsHttpAuthIdentity& ident, const nsACString& creds, uint32_t generateFlags, nsISupports* sessionState, bool aProxyAuth) { nsresult rv; uint32_t authFlags; rv = auth->GetAuthFlags(&authFlags); if (NS_FAILED(rv)) return rv; // find out if this authenticator allows reuse of credentials and/or // challenge. bool saveCreds = 0 != (authFlags & nsIHttpAuthenticator::REUSABLE_CREDENTIALS); bool saveChallenge = 0 != (authFlags & nsIHttpAuthenticator::REUSABLE_CHALLENGE); bool saveIdentity = 0 == (generateFlags & nsIHttpAuthenticator::USING_INTERNAL_IDENTITY); // this getter never fails nsHttpAuthCache* authCache = gHttpHandler->AuthCache(mIsPrivate); nsAutoCString suffix; if (!aProxyAuth) { // We don't isolate proxy credentials cache entries with the origin suffix // as it would only annoy users with authentication dialogs popping up. nsCOMPtr chan = do_QueryInterface(mAuthChannel); GetOriginAttributesSuffix(chan, suffix); } // create a cache entry. we do this even though we don't yet know that // these credentials are valid b/c we need to avoid prompting the user // more than once in case the credentials are valid. // // if the credentials are not reusable, then we don't bother sticking // them in the auth cache. rv = authCache->SetAuthEntry(scheme, host, port, directory, realm, saveCreds ? creds : ""_ns, saveChallenge ? challenge : ""_ns, suffix, saveIdentity ? &ident : nullptr, sessionState); return rv; } NS_IMETHODIMP nsHttpChannelAuthProvider::ClearProxyIdent() { LOG(("nsHttpChannelAuthProvider::ClearProxyIdent [this=%p]\n", this)); mProxyIdent.Clear(); return NS_OK; } nsresult nsHttpChannelAuthProvider::PrepareForAuthentication(bool proxyAuth) { LOG( ("nsHttpChannelAuthProvider::PrepareForAuthentication " "[this=%p channel=%p]\n", this, mAuthChannel)); if (!proxyAuth) { // reset the current proxy continuation state because our last // authentication attempt was completed successfully. NS_IF_RELEASE(mProxyAuthContinuationState); LOG((" proxy continuation state has been reset")); } if (!UsingHttpProxy() || mProxyAuthType.IsEmpty()) return NS_OK; // We need to remove any Proxy_Authorization header left over from a // non-request based authentication handshake (e.g., for NTLM auth). nsresult rv; nsCOMPtr precedingAuth; nsCString proxyAuthType; rv = GetAuthenticator(mProxyAuthType, proxyAuthType, getter_AddRefs(precedingAuth)); if (NS_FAILED(rv)) return rv; uint32_t precedingAuthFlags; rv = precedingAuth->GetAuthFlags(&precedingAuthFlags); if (NS_FAILED(rv)) return rv; if (!(precedingAuthFlags & nsIHttpAuthenticator::REQUEST_BASED)) { nsAutoCString challenges; rv = mAuthChannel->GetProxyChallenges(challenges); if (NS_FAILED(rv)) { // delete the proxy authorization header because we weren't // asked to authenticate rv = mAuthChannel->SetProxyCredentials(""_ns); if (NS_FAILED(rv)) return rv; LOG((" cleared proxy authorization header")); } } return NS_OK; } class MOZ_STACK_CLASS ChallengeParser final : Tokenizer { public: explicit ChallengeParser(const nsACString& aChallenges) : Tokenizer(aChallenges, nullptr, "") { Record(); } Maybe GetNext() { Token t; nsDependentCSubstring result; bool inQuote = false; while (Next(t)) { if (t.Type() == TOKEN_EOL) { Claim(result, ClaimInclusion::EXCLUDE_LAST); SkipWhites(WhiteSkipping::INCLUDE_NEW_LINE); Record(); inQuote = false; if (!result.IsEmpty()) { return Some(result); } } else if (t.Equals(Token::Char(',')) && !inQuote && StaticPrefs:: network_auth_allow_multiple_challenges_same_line()) { // Sometimes we get multiple challenges separated by a comma. // This is not great, as it's slightly ambiguous. We check if something // is a new challenge by matching agains = // If the , isn't followed by a word and = then most likely // it is the name of an authType. const char* prevCursorPos = mCursor; const char* prevRollbackPos = mRollback; auto hasWordAndEqual = [&]() { SkipWhites(); nsDependentCSubstring word; if (!ReadWord(word)) { return false; } SkipWhites(); return Check(Token::Char('=')); }; if (!hasWordAndEqual()) { // This is not a parameter. It means the `,` character starts a // different challenge. // We'll revert the cursor and return the contents so far. mCursor = prevCursorPos; mRollback = prevRollbackPos; Claim(result, ClaimInclusion::EXCLUDE_LAST); SkipWhites(); Record(); if (!result.IsEmpty()) { return Some(result); } } } else if (t.Equals(Token::Char('"'))) { inQuote = !inQuote; } } Claim(result, Tokenizer::ClaimInclusion::INCLUDE_LAST); SkipWhites(); Record(); if (!result.IsEmpty()) { return Some(result); } return Nothing{}; } }; enum ChallengeRank { Unknown = 0, Basic = 1, Digest = 2, NTLM = 3, Negotiate = 4, }; ChallengeRank Rank(const nsACString& aChallenge) { if (StringBeginsWith(aChallenge, "Negotiate"_ns, nsCaseInsensitiveCStringComparator)) { return ChallengeRank::Negotiate; } if (StringBeginsWith(aChallenge, "NTLM"_ns, nsCaseInsensitiveCStringComparator)) { return ChallengeRank::NTLM; } if (StringBeginsWith(aChallenge, "Digest"_ns, nsCaseInsensitiveCStringComparator)) { return ChallengeRank::Digest; } if (StringBeginsWith(aChallenge, "Basic"_ns, nsCaseInsensitiveCStringComparator)) { return ChallengeRank::Basic; } return ChallengeRank::Unknown; } nsresult nsHttpChannelAuthProvider::GetCredentials( const nsACString& aChallenges, bool proxyAuth, nsCString& creds) { LOG(("nsHttpChannelAuthProvider::GetCredentials")); nsAutoCString challenges(aChallenges); using AuthChallenge = struct AuthChallenge { nsDependentCSubstring challenge; uint16_t algorithm = 0; ChallengeRank rank = ChallengeRank::Unknown; void operator=(const AuthChallenge& aOther) { challenge.Rebind(aOther.challenge, 0); algorithm = aOther.algorithm; rank = aOther.rank; } }; nsTArray cc; ChallengeParser p(challenges); while (true) { auto next = p.GetNext(); if (next.isNothing()) { break; } AuthChallenge ac{next.ref(), 0}; nsAutoCString realm, domain, nonce, opaque; bool stale = false; uint16_t qop = 0; ac.rank = Rank(ac.challenge); if (StringBeginsWith(ac.challenge, "Digest"_ns, nsCaseInsensitiveCStringComparator)) { Unused << nsHttpDigestAuth::ParseChallenge(ac.challenge, realm, domain, nonce, opaque, &stale, &ac.algorithm, &qop); } cc.AppendElement(ac); } cc.StableSort([](const AuthChallenge& lhs, const AuthChallenge& rhs) { if (StaticPrefs::network_auth_choose_most_secure_challenge()) { // Different auth types if (lhs.rank != rhs.rank) { return lhs.rank < rhs.rank ? 1 : -1; } // If they're the same auth type, and not a Digest, then we treat them // as equal (don't reorder them). if (lhs.rank != ChallengeRank::Digest) { return 0; } } else { // Non-digest challenges should not be reordered when the pref is off. if (lhs.algorithm == 0 || rhs.algorithm == 0) { return 0; } } // Same algorithm. if (lhs.algorithm == rhs.algorithm) { return 0; } return lhs.algorithm < rhs.algorithm ? 1 : -1; }); nsCOMPtr auth; nsCString authType; // force heap allocation to enable string sharing since // we'll be assigning this value into mAuthType. // set informations that depend on whether we're authenticating against a // proxy or a webserver nsISupports** currentContinuationState; nsCString* currentAuthType; if (proxyAuth) { currentContinuationState = &mProxyAuthContinuationState; currentAuthType = &mProxyAuthType; } else { currentContinuationState = &mAuthContinuationState; currentAuthType = &mAuthType; } nsresult rv = NS_ERROR_NOT_AVAILABLE; bool gotCreds = false; // figure out which challenge we can handle and which authenticator to use. for (size_t i = 0; i < cc.Length(); i++) { rv = GetAuthenticator(cc[i].challenge, authType, getter_AddRefs(auth)); LOG(("trying auth for %s", authType.get())); if (NS_SUCCEEDED(rv)) { // // if we've already selected an auth type from a previous challenge // received while processing this channel, then skip others until // we find a challenge corresponding to the previously tried auth // type. // if (!currentAuthType->IsEmpty() && authType != *currentAuthType) continue; // // we allow the routines to run all the way through before we // decide if they are valid. // // we don't worry about the auth cache being altered because that // would have been the last step, and if the error is from updating // the authcache it wasn't really altered anyway. -CTN // // at this point the code is really only useful for client side // errors (it will not automatically fail over to do a different // auth type if the server keeps rejecting what is being sent, even // if a particular auth method only knows 1 thing, like a // non-identity based authentication method) // rv = GetCredentialsForChallenge(cc[i].challenge, authType, proxyAuth, auth, creds); if (NS_SUCCEEDED(rv)) { gotCreds = true; *currentAuthType = authType; break; } if (rv == NS_ERROR_IN_PROGRESS) { // authentication prompt has been invoked and result is // expected asynchronously, save current challenge being // processed and all remaining challenges to use later in // OnAuthAvailable and now immediately return mCurrentChallenge = cc[i].challenge; // imperfect; does not save server-side preference ordering. // instead, continues with remaining string as provided by client mRemainingChallenges.Truncate(); while (i + 1 < cc.Length()) { i++; mRemainingChallenges.Append(cc[i].challenge); mRemainingChallenges.Append("\n"_ns); } return rv; } // reset the auth type and continuation state NS_IF_RELEASE(*currentContinuationState); currentAuthType->Truncate(); } } if (!gotCreds && !currentAuthType->IsEmpty()) { // looks like we never found the auth type we were looking for. // reset the auth type and continuation state, and try again. currentAuthType->Truncate(); NS_IF_RELEASE(*currentContinuationState); rv = GetCredentials(challenges, proxyAuth, creds); } return rv; } nsresult nsHttpChannelAuthProvider::GetAuthorizationMembers( bool proxyAuth, nsACString& scheme, nsCString& host, int32_t& port, nsACString& path, nsHttpAuthIdentity*& ident, nsISupports**& continuationState) { if (proxyAuth) { MOZ_ASSERT(UsingHttpProxy(), "proxyAuth is true, but no HTTP proxy is configured!"); host = ProxyHost(); port = ProxyPort(); ident = &mProxyIdent; scheme.AssignLiteral("http"); continuationState = &mProxyAuthContinuationState; } else { host = Host(); port = Port(); ident = &mIdent; nsresult rv; rv = GetCurrentPath(path); if (NS_FAILED(rv)) return rv; rv = mURI->GetScheme(scheme); if (NS_FAILED(rv)) return rv; continuationState = &mAuthContinuationState; } return NS_OK; } nsresult nsHttpChannelAuthProvider::GetCredentialsForChallenge( const nsACString& aChallenge, const nsACString& aAuthType, bool proxyAuth, nsIHttpAuthenticator* auth, nsCString& creds) { LOG( ("nsHttpChannelAuthProvider::GetCredentialsForChallenge " "[this=%p channel=%p proxyAuth=%d challenges=%s]\n", this, mAuthChannel, proxyAuth, nsCString(aChallenge).get())); // this getter never fails nsHttpAuthCache* authCache = gHttpHandler->AuthCache(mIsPrivate); uint32_t authFlags; nsresult rv = auth->GetAuthFlags(&authFlags); if (NS_FAILED(rv)) return rv; nsAutoCString realm; ParseRealm(aChallenge, realm); // if no realm, then use the auth type as the realm. ToUpperCase so the // ficticious realm stands out a bit more. // XXX this will cause some single signon misses! // XXX this was meant to be used with NTLM, which supplies no realm. /* if (realm.IsEmpty()) { realm = authType; ToUpperCase(realm); } */ // set informations that depend on whether // we're authenticating against a proxy // or a webserver nsAutoCString host; int32_t port; nsHttpAuthIdentity* ident; nsAutoCString path, scheme; bool identFromURI = false; nsISupports** continuationState; rv = GetAuthorizationMembers(proxyAuth, scheme, host, port, path, ident, continuationState); if (NS_FAILED(rv)) return rv; uint32_t loadFlags; rv = mAuthChannel->GetLoadFlags(&loadFlags); if (NS_FAILED(rv)) return rv; // Fill only for non-proxy auth, proxy credentials are not OA-isolated. nsAutoCString suffix; if (!proxyAuth) { nsCOMPtr chan = do_QueryInterface(mAuthChannel); GetOriginAttributesSuffix(chan, suffix); // if this is the first challenge, then try using the identity // specified in the URL. if (mIdent.IsEmpty()) { GetIdentityFromURI(authFlags, mIdent); identFromURI = !mIdent.IsEmpty(); } if ((loadFlags & nsIRequest::LOAD_ANONYMOUS) && !identFromURI) { LOG(("Skipping authentication for anonymous non-proxy request\n")); return NS_ERROR_NOT_AVAILABLE; } // Let explicit URL credentials pass // regardless of the LOAD_ANONYMOUS flag } else if ((loadFlags & nsIRequest::LOAD_ANONYMOUS) && !UsingHttpProxy()) { LOG(("Skipping authentication for anonymous non-proxy request\n")); return NS_ERROR_NOT_AVAILABLE; } // // if we already tried some credentials for this transaction, then // we need to possibly clear them from the cache, unless the credentials // in the cache have changed, in which case we'd want to give them a // try instead. // nsHttpAuthEntry* entry = nullptr; Unused << authCache->GetAuthEntryForDomain(scheme, host, port, realm, suffix, &entry); // hold reference to the auth session state (in case we clear our // reference to the entry). nsCOMPtr sessionStateGrip; if (entry) sessionStateGrip = entry->mMetaData; // remember if we already had the continuation state. it means we are in // the middle of the authentication exchange and the connection must be // kept sticky then (and only then). bool authAtProgress = !!*continuationState; // for digest auth, maybe our cached nonce value simply timed out... bool identityInvalid; nsISupports* sessionState = sessionStateGrip; rv = auth->ChallengeReceived(mAuthChannel, aChallenge, proxyAuth, &sessionState, &*continuationState, &identityInvalid); sessionStateGrip.swap(sessionState); if (NS_FAILED(rv)) return rv; LOG((" identity invalid = %d\n", identityInvalid)); if (mConnectionBased && identityInvalid) { // If the flag is set and identity is invalid, it means we received the // first challange for a new negotiation round after negotiating a // connection based auth failed (invalid password). The mConnectionBased // flag is set later for the newly received challenge, so here it reflects // the previous 401/7 response schema. rv = mAuthChannel->CloseStickyConnection(); MOZ_ASSERT(NS_SUCCEEDED(rv)); if (!proxyAuth) { // We must clear proxy ident in the following scenario + explanation: // - we are authenticating to an NTLM proxy and an NTLM server // - we successfully authenticated to the proxy, mProxyIdent keeps // the user name/domain and password, the identity has also been cached // - we just threw away the connection because we are now asking for // creds for the server (WWW auth) // - hence, we will have to auth to the proxy again as well // - if we didn't clear the proxy identity, it would be considered // as non-valid and we would ask the user again ; clearing it forces // use of the cached identity and not asking the user again ClearProxyIdent(); } } mConnectionBased = !!(authFlags & nsIHttpAuthenticator::CONNECTION_BASED); // It's legal if the peer closes the connection after the first 401/7. // Making the connection sticky will prevent its restart giving the user // a 'network reset' error every time. Hence, we mark the connection // as restartable. mAuthChannel->ConnectionRestartable(!authAtProgress); if (identityInvalid) { if (entry) { if (ident->Equals(entry->Identity())) { if (!identFromURI) { LOG((" clearing bad auth cache entry\n")); // ok, we've already tried this user identity, so clear the // corresponding entry from the auth cache. authCache->ClearAuthEntry(scheme, host, port, realm, suffix); entry = nullptr; ident->Clear(); } } else if (!identFromURI || (ident->User() == entry->Identity().User() && !(loadFlags & (nsIChannel::LOAD_ANONYMOUS | nsIChannel::LOAD_EXPLICIT_CREDENTIALS)))) { LOG((" taking identity from auth cache\n")); // the password from the auth cache is more likely to be // correct than the one in the URL. at least, we know that it // works with the given username. it is possible for a server // to distinguish logons based on the supplied password alone, // but that would be quite unusual... and i don't think we need // to worry about such unorthodox cases. *ident = entry->Identity(); identFromURI = false; if (entry->Creds()[0] != '\0') { LOG((" using cached credentials!\n")); creds.Assign(entry->Creds()); return entry->AddPath(path); } } } else if (!identFromURI) { // hmm... identity invalid, but no auth entry! the realm probably // changed (see bug 201986). ident->Clear(); } if (!entry && ident->IsEmpty()) { uint32_t level = nsIAuthPrompt2::LEVEL_NONE; if ((!proxyAuth && mUsingSSL) || (proxyAuth && mProxyUsingSSL)) { level = nsIAuthPrompt2::LEVEL_SECURE; } else if (authFlags & nsIHttpAuthenticator::IDENTITY_ENCRYPTED) { level = nsIAuthPrompt2::LEVEL_PW_ENCRYPTED; } // Collect statistics on how frequently the various types of HTTP // authentication are used over SSL and non-SSL connections. if (Telemetry::CanRecordPrereleaseData()) { if ("basic"_ns.Equals(aAuthType, nsCaseInsensitiveCStringComparator)) { Telemetry::Accumulate( Telemetry::HTTP_AUTH_TYPE_STATS, UsingSSL() ? HTTP_AUTH_BASIC_SECURE : HTTP_AUTH_BASIC_INSECURE); } else if ("digest"_ns.Equals(aAuthType, nsCaseInsensitiveCStringComparator)) { Telemetry::Accumulate( Telemetry::HTTP_AUTH_TYPE_STATS, UsingSSL() ? HTTP_AUTH_DIGEST_SECURE : HTTP_AUTH_DIGEST_INSECURE); } else if ("ntlm"_ns.Equals(aAuthType, nsCaseInsensitiveCStringComparator)) { Telemetry::Accumulate( Telemetry::HTTP_AUTH_TYPE_STATS, UsingSSL() ? HTTP_AUTH_NTLM_SECURE : HTTP_AUTH_NTLM_INSECURE); } else if ("negotiate"_ns.Equals(aAuthType, nsCaseInsensitiveCStringComparator)) { Telemetry::Accumulate(Telemetry::HTTP_AUTH_TYPE_STATS, UsingSSL() ? HTTP_AUTH_NEGOTIATE_SECURE : HTTP_AUTH_NEGOTIATE_INSECURE); } } // Depending on the pref setting, the authentication dialog may be // blocked for all sub-resources, blocked for cross-origin // sub-resources, or always allowed for sub-resources. // For more details look at the bug 647010. // BlockPrompt will set mCrossOrigin parameter as well. if (BlockPrompt(proxyAuth)) { LOG(( "nsHttpChannelAuthProvider::GetCredentialsForChallenge: " "Prompt is blocked [this=%p pref=%d img-pref=%d " "non-web-content-triggered-pref=%d]\n", this, StaticPrefs::network_auth_subresource_http_auth_allow(), StaticPrefs:: network_auth_subresource_img_cross_origin_http_auth_allow(), StaticPrefs:: network_auth_non_web_content_triggered_resources_http_auth_allow())); return NS_ERROR_ABORT; } // at this point we are forced to interact with the user to get // their username and password for this domain. rv = PromptForIdentity(level, proxyAuth, realm, aAuthType, authFlags, *ident); if (NS_FAILED(rv)) return rv; identFromURI = false; } } if (identFromURI) { // Warn the user before automatically using the identity from the URL // to automatically log them into a site (see bug 232567). if (!ConfirmAuth("AutomaticAuth", false)) { // calling cancel here sets our mStatus and aborts the HTTP // transaction, which prevents OnDataAvailable events. rv = mAuthChannel->Cancel(NS_ERROR_ABORT); MOZ_ASSERT(NS_SUCCEEDED(rv)); // this return code alone is not equivalent to Cancel, since // it only instructs our caller that authentication failed. // without an explicit call to Cancel, our caller would just // load the page that accompanies the HTTP auth challenge. return NS_ERROR_ABORT; } } // // get credentials for the given user:pass // // always store the credentials we're trying now so that they will be used // on subsequent links. This will potentially remove good credentials from // the cache. This is ok as we don't want to use cached credentials if the // user specified something on the URI or in another manner. This is so // that we don't transparently authenticate as someone they're not // expecting to authenticate as. // nsCString result; rv = GenCredsAndSetEntry(auth, proxyAuth, scheme, host, port, path, realm, aChallenge, *ident, sessionStateGrip, creds); return rv; } bool nsHttpChannelAuthProvider::BlockPrompt(bool proxyAuth) { // Verify that it's ok to prompt for credentials here, per spec // http://xhr.spec.whatwg.org/#the-send%28%29-method nsCOMPtr chanInternal = do_QueryInterface(mAuthChannel); MOZ_ASSERT(chanInternal); if (chanInternal->GetBlockAuthPrompt()) { LOG( ("nsHttpChannelAuthProvider::BlockPrompt: Prompt is blocked " "[this=%p channel=%p]\n", this, mAuthChannel)); return true; } if (proxyAuth) { // Do not block auth-dialog if this is a proxy authentication. return false; } nsCOMPtr chan = do_QueryInterface(mAuthChannel); nsCOMPtr loadInfo = chan->LoadInfo(); // We will treat loads w/o loadInfo as a top level document. bool topDoc = true; bool xhr = false; bool nonWebContent = false; if (loadInfo->GetExternalContentPolicyType() != ExtContentPolicy::TYPE_DOCUMENT) { topDoc = false; } if (!topDoc) { nsCOMPtr triggeringPrinc = loadInfo->TriggeringPrincipal(); if (triggeringPrinc->IsSystemPrincipal()) { nonWebContent = true; } } if (loadInfo->GetExternalContentPolicyType() == ExtContentPolicy::TYPE_XMLHTTPREQUEST) { xhr = true; } if (!topDoc && !xhr) { nsCOMPtr topURI; Unused << chanInternal->GetTopWindowURI(getter_AddRefs(topURI)); if (topURI) { mCrossOrigin = !NS_SecurityCompareURIs(topURI, mURI, true); } else { nsIPrincipal* loadingPrinc = loadInfo->GetLoadingPrincipal(); MOZ_ASSERT(loadingPrinc); mCrossOrigin = !loadingPrinc->IsSameOrigin(mURI); } } if (Telemetry::CanRecordPrereleaseData()) { if (topDoc) { Telemetry::Accumulate(Telemetry::HTTP_AUTH_DIALOG_STATS_3, HTTP_AUTH_DIALOG_TOP_LEVEL_DOC); } else if (nonWebContent) { Telemetry::Accumulate(Telemetry::HTTP_AUTH_DIALOG_STATS_3, HTTP_AUTH_DIALOG_NON_WEB_CONTENT); } else if (!mCrossOrigin) { if (xhr) { Telemetry::Accumulate(Telemetry::HTTP_AUTH_DIALOG_STATS_3, HTTP_AUTH_DIALOG_SAME_ORIGIN_XHR); } else { Telemetry::Accumulate(Telemetry::HTTP_AUTH_DIALOG_STATS_3, HTTP_AUTH_DIALOG_SAME_ORIGIN_SUBRESOURCE); } } else { Telemetry::Accumulate( Telemetry::HTTP_AUTH_DIALOG_STATS_3, static_cast(loadInfo->GetExternalContentPolicyType())); } } if (!topDoc && !StaticPrefs:: network_auth_non_web_content_triggered_resources_http_auth_allow() && nonWebContent) { return true; } switch (StaticPrefs::network_auth_subresource_http_auth_allow()) { case SUBRESOURCE_AUTH_DIALOG_DISALLOW_ALL: // Do not open the http-authentication credentials dialog for // the sub-resources. return !topDoc && !xhr; case SUBRESOURCE_AUTH_DIALOG_DISALLOW_CROSS_ORIGIN: // Open the http-authentication credentials dialog for // the sub-resources only if they are not cross-origin. return !topDoc && !xhr && mCrossOrigin; case SUBRESOURCE_AUTH_DIALOG_ALLOW_ALL: // Allow the http-authentication dialog for subresources. // If pref network.auth.subresource-img-cross-origin-http-auth-allow // is set, http-authentication dialog for image subresources is // blocked. if (mCrossOrigin && !StaticPrefs:: network_auth_subresource_img_cross_origin_http_auth_allow() && loadInfo && ((loadInfo->GetExternalContentPolicyType() == ExtContentPolicy::TYPE_IMAGE) || (loadInfo->GetExternalContentPolicyType() == ExtContentPolicy::TYPE_IMAGESET))) { return true; } return false; default: // This is an invalid value. MOZ_ASSERT(false, "A non valid value!"); } return false; } inline void GetAuthType(const nsACString& aChallenge, nsCString& authType) { auto spaceIndex = aChallenge.FindChar(' '); authType = Substring(aChallenge, 0, spaceIndex); // normalize to lowercase ToLowerCase(authType); } nsresult nsHttpChannelAuthProvider::GetAuthenticator( const nsACString& aChallenge, nsCString& authType, nsIHttpAuthenticator** auth) { LOG(("nsHttpChannelAuthProvider::GetAuthenticator [this=%p channel=%p]\n", this, mAuthChannel)); GetAuthType(aChallenge, authType); nsCOMPtr authenticator; #ifdef MOZ_AUTH_EXTENSION if (authType.EqualsLiteral("negotiate")) { authenticator = nsHttpNegotiateAuth::GetOrCreate(); } else #endif if (authType.EqualsLiteral("basic")) { authenticator = nsHttpBasicAuth::GetOrCreate(); } else if (authType.EqualsLiteral("digest")) { authenticator = nsHttpDigestAuth::GetOrCreate(); } else if (authType.EqualsLiteral("ntlm")) { authenticator = nsHttpNTLMAuth::GetOrCreate(); } else if (authType.EqualsLiteral("mock_auth") && PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR")) { authenticator = MockHttpAuth::Create(); } else { return NS_ERROR_FACTORY_NOT_REGISTERED; } if (!authenticator) { // If called during shutdown it's possible that the singleton authenticator // was already cleared so we have a null one here. return NS_ERROR_NOT_AVAILABLE; } MOZ_ASSERT(authenticator); authenticator.forget(auth); return NS_OK; } // buf contains "domain\user" static void ParseUserDomain(const nsAString& buf, nsDependentSubstring& user, nsDependentSubstring& domain) { auto backslashPos = buf.FindChar(u'\\'); if (backslashPos != kNotFound) { domain.Rebind(buf, 0, backslashPos); user.Rebind(buf, backslashPos + 1); } } void nsHttpChannelAuthProvider::GetIdentityFromURI(uint32_t authFlags, nsHttpAuthIdentity& ident) { LOG(("nsHttpChannelAuthProvider::GetIdentityFromURI [this=%p channel=%p]\n", this, mAuthChannel)); nsAutoString userBuf; nsAutoString passBuf; // XXX i18n nsAutoCString buf; mURI->GetUsername(buf); if (!buf.IsEmpty()) { NS_UnescapeURL(buf); CopyUTF8toUTF16(buf, userBuf); mURI->GetPassword(buf); if (!buf.IsEmpty()) { NS_UnescapeURL(buf); CopyUTF8toUTF16(buf, passBuf); } } if (!userBuf.IsEmpty()) { nsDependentSubstring user(userBuf, 0); nsDependentSubstring domain(u""_ns, 0); if (authFlags & nsIHttpAuthenticator::IDENTITY_INCLUDES_DOMAIN) { ParseUserDomain(userBuf, user, domain); } ident = nsHttpAuthIdentity(domain, user, passBuf); } } static void OldParseRealm(const nsACString& aChallenge, nsACString& realm) { // // From RFC2617 section 1.2, the realm value is defined as such: // // realm = "realm" "=" realm-value // realm-value = quoted-string // // but, we'll accept anything after the the "=" up to the first space, or // end-of-line, if the string is not quoted. // const nsCString& flat = PromiseFlatCString(aChallenge); const char* challenge = flat.get(); const char* p = nsCRT::strcasestr(challenge, "realm="); if (p) { bool has_quote = false; p += 6; if (*p == '"') { has_quote = true; p++; } const char* end; if (has_quote) { end = p; while (*end) { if (*end == '\\') { // escaped character, store that one instead if not zero if (!*++end) break; } else if (*end == '\"') { // end of string break; } realm.Append(*end); ++end; } } else { // realm given without quotes end = strchr(p, ' '); if (end) { realm.Assign(p, end - p); } else { realm.Assign(p); } } } } void nsHttpChannelAuthProvider::ParseRealm(const nsACString& aChallenge, nsACString& realm) { // // From RFC2617 section 1.2, the realm value is defined as such: // // realm = "realm" "=" realm-value // realm-value = quoted-string // // but, we'll accept anything after the the "=" up to the first space, or // end-of-line, if the string is not quoted. // if (!StaticPrefs::network_auth_use_new_parse_realm()) { OldParseRealm(aChallenge, realm); return; } Tokenizer t(aChallenge); // The challenge begins with the authType. // If we can't find that something has probably gone wrong. t.SkipWhites(); nsDependentCSubstring authType; if (!t.ReadWord(authType)) { return; } // Will return true if the tokenizer advanced the cursor - false otherwise. auto readParam = [&](nsDependentCSubstring& key, nsAutoCString& value) { key.Rebind(EmptyCString(), 0); value.Truncate(); t.SkipWhites(); if (!t.ReadWord(key)) { return false; } t.SkipWhites(); if (!t.CheckChar('=')) { return true; } t.SkipWhites(); Tokenizer::Token token1; t.Record(); if (!t.Next(token1)) { return true; } nsDependentCSubstring sub; bool hasQuote = false; if (token1.Equals(Tokenizer::Token::Char('"'))) { hasQuote = true; } else { t.Claim(sub, Tokenizer::ClaimInclusion::INCLUDE_LAST); value.Append(sub); } t.Record(); Tokenizer::Token token2; while (t.Next(token2)) { if (hasQuote && token2.Equals(Tokenizer::Token::Char('"')) && !token1.Equals(Tokenizer::Token::Char('\\'))) { break; } if (!hasQuote && (token2.Type() == Tokenizer::TokenType::TOKEN_WS || token2.Type() == Tokenizer::TokenType::TOKEN_EOL)) { break; } t.Claim(sub, Tokenizer::ClaimInclusion::INCLUDE_LAST); if (!sub.Equals(R"(\)")) { value.Append(sub); } t.Record(); token1 = token2; } return true; }; while (!t.CheckEOF()) { nsDependentCSubstring key; nsAutoCString value; // If we couldn't read anything, and the input isn't followed by a , // then we exit. if (!readParam(key, value) && !t.Check(Tokenizer::Token::Char(','))) { break; } // When we find the first instance of realm we exit. // Theoretically there should be only one instance and we should fail // if there are more, but we're trying to preserve existing behaviour. if (key.Equals("realm"_ns, nsCaseInsensitiveCStringComparator)) { realm = value; break; } } } class nsHTTPAuthInformation : public nsAuthInformationHolder { public: nsHTTPAuthInformation(uint32_t aFlags, const nsString& aRealm, const nsACString& aAuthType) : nsAuthInformationHolder(aFlags, aRealm, aAuthType) {} void SetToHttpAuthIdentity(uint32_t authFlags, nsHttpAuthIdentity& identity); }; void nsHTTPAuthInformation::SetToHttpAuthIdentity( uint32_t authFlags, nsHttpAuthIdentity& identity) { identity = nsHttpAuthIdentity(Domain(), User(), Password()); } nsresult nsHttpChannelAuthProvider::PromptForIdentity( uint32_t level, bool proxyAuth, const nsACString& realm, const nsACString& authType, uint32_t authFlags, nsHttpAuthIdentity& ident) { LOG(("nsHttpChannelAuthProvider::PromptForIdentity [this=%p channel=%p]\n", this, mAuthChannel)); nsresult rv; nsCOMPtr callbacks; rv = mAuthChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); if (NS_FAILED(rv)) return rv; nsCOMPtr loadGroup; rv = mAuthChannel->GetLoadGroup(getter_AddRefs(loadGroup)); if (NS_FAILED(rv)) return rv; nsCOMPtr authPrompt; GetAuthPrompt(callbacks, proxyAuth, getter_AddRefs(authPrompt)); if (!authPrompt && loadGroup) { nsCOMPtr cbs; loadGroup->GetNotificationCallbacks(getter_AddRefs(cbs)); GetAuthPrompt(cbs, proxyAuth, getter_AddRefs(authPrompt)); } if (!authPrompt) return NS_ERROR_NO_INTERFACE; // XXX i18n: need to support non-ASCII realm strings (see bug 41489) NS_ConvertASCIItoUTF16 realmU(realm); // prompt the user... uint32_t promptFlags = 0; if (proxyAuth) { promptFlags |= nsIAuthInformation::AUTH_PROXY; if (mTriedProxyAuth) promptFlags |= nsIAuthInformation::PREVIOUS_FAILED; mTriedProxyAuth = true; } else { promptFlags |= nsIAuthInformation::AUTH_HOST; if (mTriedHostAuth) promptFlags |= nsIAuthInformation::PREVIOUS_FAILED; mTriedHostAuth = true; } if (authFlags & nsIHttpAuthenticator::IDENTITY_INCLUDES_DOMAIN) { promptFlags |= nsIAuthInformation::NEED_DOMAIN; } if (mCrossOrigin) { promptFlags |= nsIAuthInformation::CROSS_ORIGIN_SUB_RESOURCE; } RefPtr holder = new nsHTTPAuthInformation(promptFlags, realmU, authType); if (!holder) return NS_ERROR_OUT_OF_MEMORY; nsCOMPtr channel(do_QueryInterface(mAuthChannel, &rv)); if (NS_FAILED(rv)) return rv; rv = authPrompt->AsyncPromptAuth(channel, this, nullptr, level, holder, getter_AddRefs(mAsyncPromptAuthCancelable)); if (NS_SUCCEEDED(rv)) { // indicate using this error code that authentication prompt // result is expected asynchronously rv = NS_ERROR_IN_PROGRESS; } else { // Fall back to synchronous prompt bool retval = false; rv = authPrompt->PromptAuth(channel, level, holder, &retval); if (NS_FAILED(rv)) return rv; if (!retval) { rv = NS_ERROR_ABORT; } else { holder->SetToHttpAuthIdentity(authFlags, ident); } } // remember that we successfully showed the user an auth dialog if (!proxyAuth) mSuppressDefensiveAuth = true; if (mConnectionBased) { // Connection can be reset by the server in the meantime user is entering // the credentials. Result would be just a "Connection was reset" error. // Hence, we drop the current regardless if the user would make it on time // to provide credentials. // It's OK to send the NTLM type 1 message (response to the plain "NTLM" // challenge) on a new connection. { DebugOnly rv = mAuthChannel->CloseStickyConnection(); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } return rv; } NS_IMETHODIMP nsHttpChannelAuthProvider::OnAuthAvailable( nsISupports* aContext, nsIAuthInformation* aAuthInfo) { LOG(("nsHttpChannelAuthProvider::OnAuthAvailable [this=%p channel=%p]", this, mAuthChannel)); mAsyncPromptAuthCancelable = nullptr; if (!mAuthChannel) return NS_OK; nsresult rv; nsAutoCString host; int32_t port; nsHttpAuthIdentity* ident; nsAutoCString path, scheme; nsISupports** continuationState; rv = GetAuthorizationMembers(mProxyAuth, scheme, host, port, path, ident, continuationState); if (NS_FAILED(rv)) OnAuthCancelled(aContext, false); nsAutoCString realm; ParseRealm(mCurrentChallenge, realm); nsCOMPtr chan = do_QueryInterface(mAuthChannel); nsAutoCString suffix; if (!mProxyAuth) { // Fill only for non-proxy auth, proxy credentials are not OA-isolated. GetOriginAttributesSuffix(chan, suffix); } nsHttpAuthCache* authCache = gHttpHandler->AuthCache(mIsPrivate); nsHttpAuthEntry* entry = nullptr; Unused << authCache->GetAuthEntryForDomain(scheme, host, port, realm, suffix, &entry); nsCOMPtr sessionStateGrip; if (entry) sessionStateGrip = entry->mMetaData; nsAuthInformationHolder* holder = static_cast(aAuthInfo); *ident = nsHttpAuthIdentity(holder->Domain(), holder->User(), holder->Password()); nsAutoCString unused; nsCOMPtr auth; rv = GetAuthenticator(mCurrentChallenge, unused, getter_AddRefs(auth)); if (NS_FAILED(rv)) { MOZ_ASSERT(false, "GetAuthenticator failed"); OnAuthCancelled(aContext, true); return NS_OK; } nsCString creds; rv = GenCredsAndSetEntry(auth, mProxyAuth, scheme, host, port, path, realm, mCurrentChallenge, *ident, sessionStateGrip, creds); mCurrentChallenge.Truncate(); if (NS_FAILED(rv)) { OnAuthCancelled(aContext, true); return NS_OK; } return ContinueOnAuthAvailable(creds); } NS_IMETHODIMP nsHttpChannelAuthProvider::OnAuthCancelled(nsISupports* aContext, bool userCancel) { LOG(("nsHttpChannelAuthProvider::OnAuthCancelled [this=%p channel=%p]", this, mAuthChannel)); mAsyncPromptAuthCancelable = nullptr; if (!mAuthChannel) return NS_OK; // When user cancels or auth fails we want to close the connection for // connection based schemes like NTLM. Some servers don't like re-negotiation // on the same connection. nsresult rv; if (mConnectionBased) { rv = mAuthChannel->CloseStickyConnection(); MOZ_ASSERT(NS_SUCCEEDED(rv)); mConnectionBased = false; } nsCOMPtr channel = do_QueryInterface(mAuthChannel); if (channel) { nsresult status; Unused << channel->GetStatus(&status); if (NS_FAILED(status)) { // If the channel is already cancelled, there is no need to deal with the // rest challenges. LOG((" Clear mRemainingChallenges, since mAuthChannel is cancelled")); mRemainingChallenges.Truncate(); } } if (userCancel) { if (!mRemainingChallenges.IsEmpty()) { // there are still some challenges to process, do so // Get rid of current continuationState to avoid reusing it in // next challenges since it is no longer relevant. if (mProxyAuth) { NS_IF_RELEASE(mProxyAuthContinuationState); } else { NS_IF_RELEASE(mAuthContinuationState); } nsAutoCString creds; rv = GetCredentials(mRemainingChallenges, mProxyAuth, creds); if (NS_SUCCEEDED(rv)) { // GetCredentials loaded the credentials from the cache or // some other way in a synchronous manner, process those // credentials now mRemainingChallenges.Truncate(); return ContinueOnAuthAvailable(creds); } if (rv == NS_ERROR_IN_PROGRESS) { // GetCredentials successfully queued another authprompt for // a challenge from the list, we are now waiting for the user // to provide the credentials return NS_OK; } // otherwise, we failed... } mRemainingChallenges.Truncate(); } rv = mAuthChannel->OnAuthCancelled(userCancel); MOZ_ASSERT(NS_SUCCEEDED(rv)); return NS_OK; } NS_IMETHODIMP nsHttpChannelAuthProvider::OnCredsGenerated( const nsACString& aGeneratedCreds, uint32_t aFlags, nsresult aResult, nsISupports* aSessionState, nsISupports* aContinuationState) { nsresult rv; MOZ_ASSERT(NS_IsMainThread()); // When channel is closed, do not proceed if (!mAuthChannel) { return NS_OK; } mGenerateCredentialsCancelable = nullptr; if (NS_FAILED(aResult)) { return OnAuthCancelled(nullptr, true); } // We want to update m(Proxy)AuthContinuationState in case it was changed by // nsHttpNegotiateAuth::GenerateCredentials nsCOMPtr contState(aContinuationState); if (mProxyAuth) { contState.swap(mProxyAuthContinuationState); } else { contState.swap(mAuthContinuationState); } nsCOMPtr auth; nsAutoCString unused; rv = GetAuthenticator(mCurrentChallenge, unused, getter_AddRefs(auth)); NS_ENSURE_SUCCESS(rv, rv); nsAutoCString host; int32_t port; nsHttpAuthIdentity* ident; nsAutoCString directory, scheme; nsISupports** unusedContinuationState; // Get realm from challenge nsAutoCString realm; ParseRealm(mCurrentChallenge, realm); rv = GetAuthorizationMembers(mProxyAuth, scheme, host, port, directory, ident, unusedContinuationState); if (NS_FAILED(rv)) return rv; rv = UpdateCache(auth, scheme, host, port, directory, realm, mCurrentChallenge, *ident, aGeneratedCreds, aFlags, aSessionState, mProxyAuth); MOZ_ASSERT(NS_SUCCEEDED(rv)); mCurrentChallenge.Truncate(); rv = ContinueOnAuthAvailable(aGeneratedCreds); MOZ_ASSERT(NS_SUCCEEDED(rv)); return NS_OK; } nsresult nsHttpChannelAuthProvider::ContinueOnAuthAvailable( const nsACString& creds) { nsresult rv; if (mProxyAuth) { rv = mAuthChannel->SetProxyCredentials(creds); } else { rv = mAuthChannel->SetWWWCredentials(creds); } if (NS_FAILED(rv)) return rv; // drop our remaining list of challenges. We don't need them, because we // have now authenticated against a challenge and will be sending that // information to the server (or proxy). If it doesn't accept our // authentication it'll respond with failure and resend the challenge list mRemainingChallenges.Truncate(); Unused << mAuthChannel->OnAuthAvailable(); return NS_OK; } bool nsHttpChannelAuthProvider::ConfirmAuth(const char* bundleKey, bool doYesNoPrompt) { // skip prompting the user if // 1) prompts are disabled by pref // 2) we've already prompted the user // 3) we're not a toplevel channel // 4) the userpass length is less than the "phishy" threshold if (!StaticPrefs::network_auth_confirmAuth_enabled()) { return true; } uint32_t loadFlags; nsresult rv = mAuthChannel->GetLoadFlags(&loadFlags); if (NS_FAILED(rv)) return true; if (mSuppressDefensiveAuth || !(loadFlags & nsIChannel::LOAD_INITIAL_DOCUMENT_URI)) { return true; } nsAutoCString userPass; rv = mURI->GetUserPass(userPass); if (NS_FAILED(rv) || (userPass.Length() < gHttpHandler->PhishyUserPassLength())) { return true; } // we try to confirm by prompting the user. if we cannot do so, then // assume the user said ok. this is done to keep things working in // embedded builds, where the string bundle might not be present, etc. nsCOMPtr bundleService = do_GetService(NS_STRINGBUNDLE_CONTRACTID); if (!bundleService) return true; nsCOMPtr bundle; bundleService->CreateBundle(NECKO_MSGS_URL, getter_AddRefs(bundle)); if (!bundle) return true; nsAutoCString host; rv = mURI->GetHost(host); if (NS_FAILED(rv)) return true; nsAutoCString user; rv = mURI->GetUsername(user); if (NS_FAILED(rv)) return true; NS_ConvertUTF8toUTF16 ucsHost(host), ucsUser(user); size_t userLength = ucsUser.Length(); if (userLength > MAX_DISPLAYED_USER_LENGTH) { size_t desiredLength = MAX_DISPLAYED_USER_LENGTH; // Don't cut off right before a low surrogate. Just include it. if (NS_IS_LOW_SURROGATE(ucsUser[desiredLength])) { desiredLength++; } ucsUser.Replace(desiredLength, userLength - desiredLength, nsContentUtils::GetLocalizedEllipsis()); } size_t hostLen = ucsHost.Length(); if (hostLen > MAX_DISPLAYED_HOST_LENGTH) { size_t cutPoint = hostLen - MAX_DISPLAYED_HOST_LENGTH; // Likewise, don't cut off right before a low surrogate here. // Keep the low surrogate if (NS_IS_LOW_SURROGATE(ucsHost[cutPoint])) { cutPoint--; } // It's possible cutPoint was 1 and is now 0. Only insert the ellipsis // if we're actually removing anything. if (cutPoint > 0) { ucsHost.Replace(0, cutPoint, nsContentUtils::GetLocalizedEllipsis()); } } AutoTArray strs = {ucsHost, ucsUser}; nsAutoString msg; rv = bundle->FormatStringFromName(bundleKey, strs, msg); if (NS_FAILED(rv)) return true; nsCOMPtr callbacks; rv = mAuthChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); if (NS_FAILED(rv)) return true; nsCOMPtr loadGroup; rv = mAuthChannel->GetLoadGroup(getter_AddRefs(loadGroup)); if (NS_FAILED(rv)) return true; nsCOMPtr promptSvc = do_GetService("@mozilla.org/prompter;1", &rv); if (NS_FAILED(rv) || !promptSvc) { return true; } // do not prompt again mSuppressDefensiveAuth = true; // Get current browsing context to use as prompt parent nsCOMPtr chan = do_QueryInterface(mAuthChannel); if (!chan) { return true; } nsCOMPtr loadInfo = chan->LoadInfo(); RefPtr browsingContext; loadInfo->GetBrowsingContext(getter_AddRefs(browsingContext)); bool confirmed; if (doYesNoPrompt) { int32_t choice; bool checkState = false; rv = promptSvc->ConfirmExBC( browsingContext, StaticPrefs::prompts_modalType_confirmAuth(), nullptr, msg.get(), nsIPromptService::BUTTON_POS_1_DEFAULT + nsIPromptService::STD_YES_NO_BUTTONS, nullptr, nullptr, nullptr, nullptr, &checkState, &choice); if (NS_FAILED(rv)) return true; confirmed = choice == 0; } else { rv = promptSvc->ConfirmBC(browsingContext, StaticPrefs::prompts_modalType_confirmAuth(), nullptr, msg.get(), &confirmed); if (NS_FAILED(rv)) return true; } return confirmed; } void nsHttpChannelAuthProvider::SetAuthorizationHeader( nsHttpAuthCache* authCache, const nsHttpAtom& header, const nsACString& scheme, const nsACString& host, int32_t port, const nsACString& path, nsHttpAuthIdentity& ident) { nsHttpAuthEntry* entry = nullptr; nsresult rv; // set informations that depend on whether // we're authenticating against a proxy // or a webserver nsISupports** continuationState; nsAutoCString suffix; if (header == nsHttp::Proxy_Authorization) { continuationState = &mProxyAuthContinuationState; if (mProxyInfo) { nsAutoCString type; mProxyInfo->GetType(type); if (type.EqualsLiteral("https")) { // Let this be overriden by anything from the cache. auto const& pa = mProxyInfo->ProxyAuthorizationHeader(); if (!pa.IsEmpty()) { rv = mAuthChannel->SetProxyCredentials(pa); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } } } else { continuationState = &mAuthContinuationState; nsCOMPtr chan = do_QueryInterface(mAuthChannel); GetOriginAttributesSuffix(chan, suffix); } rv = authCache->GetAuthEntryForPath(scheme, host, port, path, suffix, &entry); if (NS_SUCCEEDED(rv)) { // if we are trying to add a header for origin server auth and if the // URL contains an explicit username, then try the given username first. // we only want to do this, however, if we know the URL requires auth // based on the presence of an auth cache entry for this URL (which is // true since we are here). but, if the username from the URL matches // the username from the cache, then we should prefer the password // stored in the cache since that is most likely to be valid. if (header == nsHttp::Authorization && entry->Domain()[0] == '\0') { GetIdentityFromURI(0, ident); // if the usernames match, then clear the ident so we will pick // up the one from the auth cache instead. // when this is undesired, specify LOAD_EXPLICIT_CREDENTIALS load // flag. if (ident.User() == entry->User()) { uint32_t loadFlags; if (NS_SUCCEEDED(mAuthChannel->GetLoadFlags(&loadFlags)) && !(loadFlags & nsIChannel::LOAD_EXPLICIT_CREDENTIALS)) { ident.Clear(); } } } bool identFromURI; if (ident.IsEmpty()) { ident = entry->Identity(); identFromURI = false; } else { identFromURI = true; } nsCString temp; // this must have the same lifetime as creds nsAutoCString creds(entry->Creds()); // we can only send a preemptive Authorization header if we have either // stored credentials or a stored challenge from which to derive // credentials. if the identity is from the URI, then we cannot use // the stored credentials. if ((creds.IsEmpty() || identFromURI) && !entry->Challenge().IsEmpty()) { nsCOMPtr auth; nsAutoCString unused; rv = GetAuthenticator(entry->Challenge(), unused, getter_AddRefs(auth)); if (NS_SUCCEEDED(rv)) { bool proxyAuth = (header == nsHttp::Proxy_Authorization); rv = GenCredsAndSetEntry(auth, proxyAuth, scheme, host, port, path, entry->Realm(), entry->Challenge(), ident, entry->mMetaData, temp); if (NS_SUCCEEDED(rv)) creds = temp; // make sure the continuation state is null since we do not // support mixing preemptive and 'multirequest' authentication. NS_IF_RELEASE(*continuationState); } } if (!creds.IsEmpty()) { LOG((" adding \"%s\" request header\n", header.get())); if (header == nsHttp::Proxy_Authorization) { rv = mAuthChannel->SetProxyCredentials(creds); MOZ_ASSERT(NS_SUCCEEDED(rv)); } else { rv = mAuthChannel->SetWWWCredentials(creds); MOZ_ASSERT(NS_SUCCEEDED(rv)); } // suppress defensive auth prompting for this channel since we know // that we already prompted at least once this session. we only do // this for non-proxy auth since the URL's userpass is not used for // proxy auth. if (header == nsHttp::Authorization) mSuppressDefensiveAuth = true; } else { ident.Clear(); // don't remember the identity } } } nsresult nsHttpChannelAuthProvider::GetCurrentPath(nsACString& path) { nsresult rv; nsCOMPtr url = do_QueryInterface(mURI); if (url) { rv = url->GetDirectory(path); } else { rv = mURI->GetPathQueryRef(path); } return rv; } NS_IMPL_ISUPPORTS(nsHttpChannelAuthProvider, nsICancelable, nsIHttpChannelAuthProvider, nsIAuthPromptCallback, nsIHttpAuthenticatorCallback) } // namespace mozilla::net