/* vim:set ts=4 sw=4 sts=4 et ci: */ /* 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 "nsHttpNTLMAuth.h" #include "nsIAuthModule.h" #include "nsCOMPtr.h" #include "plbase64.h" #include "prnetdb.h" //----------------------------------------------------------------------------- #include "nsIPrefBranch.h" #include "nsIPrefService.h" #include "nsIHttpAuthenticableChannel.h" #include "nsIURI.h" #ifdef XP_WIN #include "nsIX509Cert.h" #include "nsISSLStatus.h" #include "nsISSLStatusProvider.h" #endif #include "mozilla/Attributes.h" static const char kAllowProxies[] = "network.automatic-ntlm-auth.allow-proxies"; static const char kAllowNonFqdn[] = "network.automatic-ntlm-auth.allow-non-fqdn"; static const char kTrustedURIs[] = "network.automatic-ntlm-auth.trusted-uris"; static const char kForceGeneric[] = "network.auth.force-generic-ntlm"; // XXX MatchesBaseURI and TestPref are duplicated in nsHttpNegotiateAuth.cpp, // but since that file lives in a separate library we cannot directly share it. // bug 236865 addresses this problem. static bool MatchesBaseURI(const nsCSubstring &matchScheme, const nsCSubstring &matchHost, int32_t matchPort, const char *baseStart, const char *baseEnd) { // check if scheme://host:port matches baseURI // parse the base URI const char *hostStart, *schemeEnd = strstr(baseStart, "://"); if (schemeEnd) { // the given scheme must match the parsed scheme exactly if (!matchScheme.Equals(Substring(baseStart, schemeEnd))) return false; hostStart = schemeEnd + 3; } else hostStart = baseStart; // XXX this does not work for IPv6-literals const char *hostEnd = strchr(hostStart, ':'); if (hostEnd && hostEnd < baseEnd) { // the given port must match the parsed port exactly int port = atoi(hostEnd + 1); if (matchPort != (int32_t) port) return false; } else hostEnd = baseEnd; // if we didn't parse out a host, then assume we got a match. if (hostStart == hostEnd) return true; uint32_t hostLen = hostEnd - hostStart; // matchHost must either equal host or be a subdomain of host if (matchHost.Length() < hostLen) return false; const char *end = matchHost.EndReading(); if (PL_strncasecmp(end - hostLen, hostStart, hostLen) == 0) { // if matchHost ends with host from the base URI, then make sure it is // either an exact match, or prefixed with a dot. we don't want // "foobar.com" to match "bar.com" if (matchHost.Length() == hostLen || *(end - hostLen) == '.' || *(end - hostLen - 1) == '.') return true; } return false; } static bool IsNonFqdn(nsIURI *uri) { nsAutoCString host; PRNetAddr addr; if (NS_FAILED(uri->GetAsciiHost(host))) return false; // return true if host does not contain a dot and is not an ip address return !host.IsEmpty() && host.FindChar('.') == kNotFound && PR_StringToNetAddr(host.BeginReading(), &addr) != PR_SUCCESS; } static bool TestPref(nsIURI *uri, const char *pref) { nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); if (!prefs) return false; nsAutoCString scheme, host; int32_t port; if (NS_FAILED(uri->GetScheme(scheme))) return false; if (NS_FAILED(uri->GetAsciiHost(host))) return false; if (NS_FAILED(uri->GetPort(&port))) return false; char *hostList; if (NS_FAILED(prefs->GetCharPref(pref, &hostList)) || !hostList) return false; // pseudo-BNF // ---------- // // url-list base-url ( base-url "," LWS )* // base-url ( scheme-part | host-part | scheme-part host-part ) // scheme-part scheme "://" // host-part host [":" port] // // for example: // "https://, http://office.foo.com" // char *start = hostList, *end; for (;;) { // skip past any whitespace while (*start == ' ' || *start == '\t') ++start; end = strchr(start, ','); if (!end) end = start + strlen(start); if (start == end) break; if (MatchesBaseURI(scheme, host, port, start, end)) return true; if (*end == '\0') break; start = end + 1; } nsMemory::Free(hostList); return false; } // Check to see if we should use our generic (internal) NTLM auth module. static bool ForceGenericNTLM() { nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); if (!prefs) return false; bool flag = false; if (NS_FAILED(prefs->GetBoolPref(kForceGeneric, &flag))) flag = false; LOG(("Force use of generic ntlm auth module: %d\n", flag)); return flag; } // Check to see if we should use default credentials for this host or proxy. static bool CanUseDefaultCredentials(nsIHttpAuthenticableChannel *channel, bool isProxyAuth) { nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); if (!prefs) return false; if (isProxyAuth) { bool val; if (NS_FAILED(prefs->GetBoolPref(kAllowProxies, &val))) val = false; LOG(("Default credentials allowed for proxy: %d\n", val)); return val; } nsCOMPtr uri; channel->GetURI(getter_AddRefs(uri)); bool allowNonFqdn; if (NS_FAILED(prefs->GetBoolPref(kAllowNonFqdn, &allowNonFqdn))) allowNonFqdn = false; if (allowNonFqdn && uri && IsNonFqdn(uri)) { LOG(("Host is non-fqdn, default credentials are allowed\n")); return true; } bool isTrustedHost = (uri && TestPref(uri, kTrustedURIs)); LOG(("Default credentials allowed for host: %d\n", isTrustedHost)); return isTrustedHost; } // Dummy class for session state object. This class doesn't hold any data. // Instead we use its existence as a flag. See ChallengeReceived. class nsNTLMSessionState MOZ_FINAL : public nsISupports { public: NS_DECL_ISUPPORTS }; NS_IMPL_ISUPPORTS0(nsNTLMSessionState) //----------------------------------------------------------------------------- NS_IMPL_ISUPPORTS1(nsHttpNTLMAuth, nsIHttpAuthenticator) NS_IMETHODIMP nsHttpNTLMAuth::ChallengeReceived(nsIHttpAuthenticableChannel *channel, const char *challenge, bool isProxyAuth, nsISupports **sessionState, nsISupports **continuationState, bool *identityInvalid) { LOG(("nsHttpNTLMAuth::ChallengeReceived [ss=%p cs=%p]\n", *sessionState, *continuationState)); // Use the native NTLM if available mUseNative = true; // NOTE: we don't define any session state, but we do use the pointer. *identityInvalid = false; // Start a new auth sequence if the challenge is exactly "NTLM". // If native NTLM auth apis are available and enabled through prefs, // try to use them. if (PL_strcasecmp(challenge, "NTLM") == 0) { nsCOMPtr module; // Check to see if we should default to our generic NTLM auth module // through UseGenericNTLM. (We use native auth by default if the // system provides it.) If *sessionState is non-null, we failed to // instantiate a native NTLM module the last time, so skip trying again. bool forceGeneric = ForceGenericNTLM(); if (!forceGeneric && !*sessionState) { // Check for approved default credentials hosts and proxies. If // *continuationState is non-null, the last authentication attempt // failed so skip default credential use. if (!*continuationState && CanUseDefaultCredentials(channel, isProxyAuth)) { // Try logging in with the user's default credentials. If // successful, |identityInvalid| is false, which will trigger // a default credentials attempt once we return. module = do_CreateInstance(NS_AUTH_MODULE_CONTRACTID_PREFIX "sys-ntlm"); } #ifdef XP_WIN else { // Try to use native NTLM and prompt the user for their domain, // username, and password. (only supported by windows nsAuthSSPI module.) // Note, for servers that use LMv1 a weak hash of the user's password // will be sent. We rely on windows internal apis to decide whether // we should support this older, less secure version of the protocol. module = do_CreateInstance(NS_AUTH_MODULE_CONTRACTID_PREFIX "sys-ntlm"); *identityInvalid = true; } #endif // XP_WIN #ifdef PR_LOGGING if (!module) LOG(("Native sys-ntlm auth module not found.\n")); #endif } #ifdef XP_WIN // On windows, never fall back unless the user has specifically requested so. if (!forceGeneric && !module) return NS_ERROR_UNEXPECTED; #endif // If no native support was available. Fall back on our internal NTLM implementation. if (!module) { if (!*sessionState) { // Remember the fact that we cannot use the "sys-ntlm" module, // so we don't ever bother trying again for this auth domain. *sessionState = new nsNTLMSessionState(); if (!*sessionState) return NS_ERROR_OUT_OF_MEMORY; NS_ADDREF(*sessionState); } // Use our internal NTLM implementation. Note, this is less secure, // see bug 520607 for details. LOG(("Trying to fall back on internal ntlm auth.\n")); module = do_CreateInstance(NS_AUTH_MODULE_CONTRACTID_PREFIX "ntlm"); mUseNative = false; // Prompt user for domain, username, and password. *identityInvalid = true; } // If this fails, then it means that we cannot do NTLM auth. if (!module) { LOG(("No ntlm auth modules available.\n")); return NS_ERROR_UNEXPECTED; } // A non-null continuation state implies that we failed to authenticate. // Blow away the old authentication state, and use the new one. module.swap(*continuationState); } return NS_OK; } NS_IMETHODIMP nsHttpNTLMAuth::GenerateCredentials(nsIHttpAuthenticableChannel *authChannel, const char *challenge, bool isProxyAuth, const PRUnichar *domain, const PRUnichar *user, const PRUnichar *pass, nsISupports **sessionState, nsISupports **continuationState, uint32_t *aFlags, char **creds) { LOG(("nsHttpNTLMAuth::GenerateCredentials\n")); *creds = nullptr; *aFlags = 0; // if user or password is empty, ChallengeReceived returned // identityInvalid = false, that means we are using default user // credentials; see nsAuthSSPI::Init method for explanation of this // condition if (!user || !pass) *aFlags = USING_INTERNAL_IDENTITY; nsresult rv; nsCOMPtr module = do_QueryInterface(*continuationState, &rv); NS_ENSURE_SUCCESS(rv, rv); void *inBuf, *outBuf; uint32_t inBufLen, outBufLen; // initial challenge if (PL_strcasecmp(challenge, "NTLM") == 0) { // NTLM service name format is 'HTTP@host' for both http and https nsCOMPtr uri; rv = authChannel->GetURI(getter_AddRefs(uri)); if (NS_FAILED(rv)) return rv; nsAutoCString serviceName, host; rv = uri->GetAsciiHost(host); if (NS_FAILED(rv)) return rv; serviceName.AppendLiteral("HTTP@"); serviceName.Append(host); // initialize auth module uint32_t reqFlags = nsIAuthModule::REQ_DEFAULT; if (isProxyAuth) reqFlags |= nsIAuthModule::REQ_PROXY_AUTH; rv = module->Init(serviceName.get(), reqFlags, domain, user, pass); if (NS_FAILED(rv)) return rv; // This update enables updated Windows machines (Win7 or patched previous // versions) and Linux machines running Samba (updated for Channel // Binding), to perform Channel Binding when authenticating using NTLMv2 // and an outer secure channel. // // Currently only implemented for Windows, linux support will be landing in // a separate patch, update this #ifdef accordingly then. #if defined (XP_WIN) /* || defined (LINUX) */ // We should retrieve the server certificate and compute the CBT, // but only when we are using the native NTLM implementation and // not the internal one. // It is a valid case not having the security info object. This // occures when we connect an https site through an ntlm proxy. // After the ssl tunnel has been created, we get here the second // time and now generate the CBT from now valid security info. nsCOMPtr channel = do_QueryInterface(authChannel, &rv); if (NS_FAILED(rv)) return rv; nsCOMPtr security; rv = channel->GetSecurityInfo(getter_AddRefs(security)); if (NS_FAILED(rv)) return rv; nsCOMPtr statusProvider = do_QueryInterface(security); if (mUseNative && statusProvider) { nsCOMPtr status; rv = statusProvider->GetSSLStatus(getter_AddRefs(status)); if (NS_FAILED(rv)) return rv; nsCOMPtr cert; rv = status->GetServerCert(getter_AddRefs(cert)); if (NS_FAILED(rv)) return rv; uint32_t length; uint8_t* certArray; cert->GetRawDER(&length, &certArray); // If there is a server certificate, we pass it along the // first time we call GetNextToken(). inBufLen = length; inBuf = certArray; } else { // If there is no server certificate, we don't pass anything. inBufLen = 0; inBuf = nullptr; } #else // Extended protection update is just for Linux and Windows machines. inBufLen = 0; inBuf = nullptr; #endif } else { // decode challenge; skip past "NTLM " to the start of the base64 // encoded data. int len = strlen(challenge); if (len < 6) return NS_ERROR_UNEXPECTED; // bogus challenge challenge += 5; len -= 5; // strip off any padding (see bug 230351) while (challenge[len - 1] == '=') len--; // decode into the input secbuffer inBufLen = (len * 3)/4; // sufficient size (see plbase64.h) inBuf = nsMemory::Alloc(inBufLen); if (!inBuf) return NS_ERROR_OUT_OF_MEMORY; if (PL_Base64Decode(challenge, len, (char *) inBuf) == nullptr) { nsMemory::Free(inBuf); return NS_ERROR_UNEXPECTED; // improper base64 encoding } } rv = module->GetNextToken(inBuf, inBufLen, &outBuf, &outBufLen); if (NS_SUCCEEDED(rv)) { // base64 encode data in output buffer and prepend "NTLM " int credsLen = 5 + ((outBufLen + 2)/3)*4; *creds = (char *) nsMemory::Alloc(credsLen + 1); if (!*creds) rv = NS_ERROR_OUT_OF_MEMORY; else { memcpy(*creds, "NTLM ", 5); PL_Base64Encode((char *) outBuf, outBufLen, *creds + 5); (*creds)[credsLen] = '\0'; // null terminate } // OK, we are done with |outBuf| nsMemory::Free(outBuf); } if (inBuf) nsMemory::Free(inBuf); return rv; } NS_IMETHODIMP nsHttpNTLMAuth::GetAuthFlags(uint32_t *flags) { *flags = CONNECTION_BASED | IDENTITY_INCLUDES_DOMAIN | IDENTITY_ENCRYPTED; return NS_OK; }