| // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/http/http_auth_handler_negotiate.h" |
| |
| #include "base/logging.h" |
| #include "base/string_util.h" |
| #include "base/stringprintf.h" |
| #include "base/utf_string_conversions.h" |
| #include "net/base/address_family.h" |
| #include "net/base/host_resolver.h" |
| #include "net/base/net_errors.h" |
| #include "net/http/http_auth_filter.h" |
| #include "net/http/url_security_manager.h" |
| |
| namespace net { |
| |
| HttpAuthHandlerNegotiate::Factory::Factory() |
| : disable_cname_lookup_(false), |
| use_port_(false), |
| #if defined(OS_WIN) |
| max_token_length_(0), |
| first_creation_(true), |
| is_unsupported_(false), |
| #endif |
| auth_library_(NULL) { |
| } |
| |
| HttpAuthHandlerNegotiate::Factory::~Factory() { |
| } |
| |
| void HttpAuthHandlerNegotiate::Factory::set_host_resolver( |
| HostResolver* resolver) { |
| resolver_ = resolver; |
| } |
| |
| int HttpAuthHandlerNegotiate::Factory::CreateAuthHandler( |
| HttpAuth::ChallengeTokenizer* challenge, |
| HttpAuth::Target target, |
| const GURL& origin, |
| CreateReason reason, |
| int digest_nonce_count, |
| const BoundNetLog& net_log, |
| scoped_ptr<HttpAuthHandler>* handler) { |
| #if defined(OS_WIN) |
| if (is_unsupported_ || reason == CREATE_PREEMPTIVE) |
| return ERR_UNSUPPORTED_AUTH_SCHEME; |
| if (max_token_length_ == 0) { |
| int rv = DetermineMaxTokenLength(auth_library_.get(), NEGOSSP_NAME, |
| &max_token_length_); |
| if (rv == ERR_UNSUPPORTED_AUTH_SCHEME) |
| is_unsupported_ = true; |
| if (rv != OK) |
| return rv; |
| } |
| // TODO(cbentzel): Move towards model of parsing in the factory |
| // method and only constructing when valid. |
| scoped_ptr<HttpAuthHandler> tmp_handler( |
| new HttpAuthHandlerNegotiate(auth_library_.get(), max_token_length_, |
| url_security_manager(), resolver_, |
| disable_cname_lookup_, use_port_)); |
| if (!tmp_handler->InitFromChallenge(challenge, target, origin, net_log)) |
| return ERR_INVALID_RESPONSE; |
| handler->swap(tmp_handler); |
| return OK; |
| #elif defined(OS_POSIX) |
| // TODO(ahendrickson): Move towards model of parsing in the factory |
| // method and only constructing when valid. |
| scoped_ptr<HttpAuthHandler> tmp_handler( |
| new HttpAuthHandlerNegotiate(auth_library_.get(), url_security_manager(), |
| resolver_, disable_cname_lookup_, |
| use_port_)); |
| if (!tmp_handler->InitFromChallenge(challenge, target, origin, net_log)) |
| return ERR_INVALID_RESPONSE; |
| handler->swap(tmp_handler); |
| return OK; |
| #endif |
| } |
| |
| HttpAuthHandlerNegotiate::HttpAuthHandlerNegotiate( |
| AuthLibrary* auth_library, |
| #if defined(OS_WIN) |
| ULONG max_token_length, |
| #endif |
| URLSecurityManager* url_security_manager, |
| HostResolver* resolver, |
| bool disable_cname_lookup, |
| bool use_port) |
| #if defined(OS_WIN) |
| : auth_system_(auth_library, "Negotiate", NEGOSSP_NAME, max_token_length), |
| #elif defined(OS_POSIX) |
| : auth_system_(auth_library, "Negotiate", CHROME_GSS_KRB5_MECH_OID_DESC), |
| #endif |
| disable_cname_lookup_(disable_cname_lookup), |
| use_port_(use_port), |
| ALLOW_THIS_IN_INITIALIZER_LIST(io_callback_( |
| this, &HttpAuthHandlerNegotiate::OnIOComplete)), |
| resolver_(resolver), |
| already_called_(false), |
| has_username_and_password_(false), |
| user_callback_(NULL), |
| auth_token_(NULL), |
| next_state_(STATE_NONE), |
| url_security_manager_(url_security_manager) { |
| } |
| |
| HttpAuthHandlerNegotiate::~HttpAuthHandlerNegotiate() { |
| } |
| |
| std::wstring HttpAuthHandlerNegotiate::CreateSPN( |
| const AddressList& address_list, const GURL& origin) { |
| // Kerberos Web Server SPNs are in the form HTTP/<host>:<port> through SSPI, |
| // and in the form HTTP@<host>:<port> through GSSAPI |
| // http://msdn.microsoft.com/en-us/library/ms677601%28VS.85%29.aspx |
| // |
| // However, reality differs from the specification. A good description of |
| // the problems can be found here: |
| // http://blog.michelbarneveld.nl/michel/archive/2009/11/14/the-reason-why-kb911149-and-kb908209-are-not-the-soluton.aspx |
| // |
| // Typically the <host> portion should be the canonical FQDN for the service. |
| // If this could not be resolved, the original hostname in the URL will be |
| // attempted instead. However, some intranets register SPNs using aliases |
| // for the same canonical DNS name to allow multiple web services to reside |
| // on the same host machine without requiring different ports. IE6 and IE7 |
| // have hotpatches that allow the default behavior to be overridden. |
| // http://support.microsoft.com/kb/911149 |
| // http://support.microsoft.com/kb/938305 |
| // |
| // According to the spec, the <port> option should be included if it is a |
| // non-standard port (i.e. not 80 or 443 in the HTTP case). However, |
| // historically browsers have not included the port, even on non-standard |
| // ports. IE6 required a hotpatch and a registry setting to enable |
| // including non-standard ports, and IE7 and IE8 also require the same |
| // registry setting, but no hotpatch. Firefox does not appear to have an |
| // option to include non-standard ports as of 3.6. |
| // http://support.microsoft.com/kb/908209 |
| // |
| // Without any command-line flags, Chrome matches the behavior of Firefox |
| // and IE. Users can override the behavior so aliases are allowed and |
| // non-standard ports are included. |
| int port = origin.EffectiveIntPort(); |
| std::string server; |
| if (!address_list.GetCanonicalName(&server)) |
| server = origin.host(); |
| #if defined(OS_WIN) |
| static const char kSpnSeparator = '/'; |
| #elif defined(OS_POSIX) |
| static const char kSpnSeparator = '@'; |
| #endif |
| if (port != 80 && port != 443 && use_port_) { |
| return ASCIIToWide(base::StringPrintf("HTTP%c%s:%d", kSpnSeparator, |
| server.c_str(), port)); |
| } else { |
| return ASCIIToWide(base::StringPrintf("HTTP%c%s", kSpnSeparator, |
| server.c_str())); |
| } |
| } |
| |
| HttpAuth::AuthorizationResult HttpAuthHandlerNegotiate::HandleAnotherChallenge( |
| HttpAuth::ChallengeTokenizer* challenge) { |
| return auth_system_.ParseChallenge(challenge); |
| } |
| |
| // Require identity on first pass instead of second. |
| bool HttpAuthHandlerNegotiate::NeedsIdentity() { |
| return auth_system_.NeedsIdentity(); |
| } |
| |
| bool HttpAuthHandlerNegotiate::AllowsDefaultCredentials() { |
| if (target_ == HttpAuth::AUTH_PROXY) |
| return true; |
| if (!url_security_manager_) |
| return false; |
| return url_security_manager_->CanUseDefaultCredentials(origin_); |
| } |
| |
| // The Negotiate challenge header looks like: |
| // WWW-Authenticate: NEGOTIATE auth-data |
| bool HttpAuthHandlerNegotiate::Init(HttpAuth::ChallengeTokenizer* challenge) { |
| #if defined(OS_POSIX) |
| if (!auth_system_.Init()) { |
| VLOG(1) << "can't initialize GSSAPI library"; |
| return false; |
| } |
| // GSSAPI does not provide a way to enter username/password to |
| // obtain a TGT. If the default credentials are not allowed for |
| // a particular site (based on whitelist), fall back to a |
| // different scheme. |
| if (!AllowsDefaultCredentials()) |
| return false; |
| #endif |
| if (CanDelegate()) |
| auth_system_.Delegate(); |
| auth_scheme_ = HttpAuth::AUTH_SCHEME_NEGOTIATE; |
| score_ = 4; |
| properties_ = ENCRYPTS_IDENTITY | IS_CONNECTION_BASED; |
| HttpAuth::AuthorizationResult auth_result = |
| auth_system_.ParseChallenge(challenge); |
| return (auth_result == HttpAuth::AUTHORIZATION_RESULT_ACCEPT); |
| } |
| |
| int HttpAuthHandlerNegotiate::GenerateAuthTokenImpl( |
| const string16* username, |
| const string16* password, |
| const HttpRequestInfo* request, |
| CompletionCallback* callback, |
| std::string* auth_token) { |
| DCHECK(user_callback_ == NULL); |
| DCHECK((username == NULL) == (password == NULL)); |
| DCHECK(auth_token_ == NULL); |
| auth_token_ = auth_token; |
| if (already_called_) { |
| DCHECK((!has_username_and_password_ && username == NULL) || |
| (has_username_and_password_ && *username == username_ && |
| *password == password_)); |
| next_state_ = STATE_GENERATE_AUTH_TOKEN; |
| } else { |
| already_called_ = true; |
| if (username) { |
| has_username_and_password_ = true; |
| username_ = *username; |
| password_ = *password; |
| } |
| next_state_ = STATE_RESOLVE_CANONICAL_NAME; |
| } |
| int rv = DoLoop(OK); |
| if (rv == ERR_IO_PENDING) |
| user_callback_ = callback; |
| return rv; |
| } |
| |
| void HttpAuthHandlerNegotiate::OnIOComplete(int result) { |
| int rv = DoLoop(result); |
| if (rv != ERR_IO_PENDING) |
| DoCallback(rv); |
| } |
| |
| void HttpAuthHandlerNegotiate::DoCallback(int rv) { |
| DCHECK(rv != ERR_IO_PENDING); |
| DCHECK(user_callback_); |
| CompletionCallback* callback = user_callback_; |
| user_callback_ = NULL; |
| callback->Run(rv); |
| } |
| |
| int HttpAuthHandlerNegotiate::DoLoop(int result) { |
| DCHECK(next_state_ != STATE_NONE); |
| |
| int rv = result; |
| do { |
| State state = next_state_; |
| next_state_ = STATE_NONE; |
| switch (state) { |
| case STATE_RESOLVE_CANONICAL_NAME: |
| DCHECK_EQ(OK, rv); |
| rv = DoResolveCanonicalName(); |
| break; |
| case STATE_RESOLVE_CANONICAL_NAME_COMPLETE: |
| rv = DoResolveCanonicalNameComplete(rv); |
| break; |
| case STATE_GENERATE_AUTH_TOKEN: |
| DCHECK_EQ(OK, rv); |
| rv = DoGenerateAuthToken(); |
| break; |
| case STATE_GENERATE_AUTH_TOKEN_COMPLETE: |
| rv = DoGenerateAuthTokenComplete(rv); |
| break; |
| default: |
| NOTREACHED() << "bad state"; |
| rv = ERR_FAILED; |
| break; |
| } |
| } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE); |
| |
| return rv; |
| } |
| |
| int HttpAuthHandlerNegotiate::DoResolveCanonicalName() { |
| next_state_ = STATE_RESOLVE_CANONICAL_NAME_COMPLETE; |
| if (disable_cname_lookup_ || !resolver_) |
| return OK; |
| |
| // TODO(cbentzel): Add reverse DNS lookup for numeric addresses. |
| DCHECK(!single_resolve_.get()); |
| HostResolver::RequestInfo info(HostPortPair(origin_.host(), 0)); |
| info.set_host_resolver_flags(HOST_RESOLVER_CANONNAME); |
| single_resolve_.reset(new SingleRequestHostResolver(resolver_)); |
| return single_resolve_->Resolve(info, &address_list_, &io_callback_, |
| net_log_); |
| } |
| |
| int HttpAuthHandlerNegotiate::DoResolveCanonicalNameComplete(int rv) { |
| DCHECK_NE(ERR_IO_PENDING, rv); |
| if (rv != OK) { |
| // Even in the error case, try to use origin_.host instead of |
| // passing the failure on to the caller. |
| VLOG(1) << "Problem finding canonical name for SPN for host " |
| << origin_.host() << ": " << ErrorToString(rv); |
| rv = OK; |
| } |
| |
| next_state_ = STATE_GENERATE_AUTH_TOKEN; |
| spn_ = CreateSPN(address_list_, origin_); |
| address_list_.Reset(); |
| return rv; |
| } |
| |
| int HttpAuthHandlerNegotiate::DoGenerateAuthToken() { |
| next_state_ = STATE_GENERATE_AUTH_TOKEN_COMPLETE; |
| string16* username = has_username_and_password_ ? &username_ : NULL; |
| string16* password = has_username_and_password_ ? &password_ : NULL; |
| // TODO(cbentzel): This should possibly be done async. |
| return auth_system_.GenerateAuthToken(username, password, spn_, auth_token_); |
| } |
| |
| int HttpAuthHandlerNegotiate::DoGenerateAuthTokenComplete(int rv) { |
| DCHECK_NE(ERR_IO_PENDING, rv); |
| auth_token_ = NULL; |
| return rv; |
| } |
| |
| bool HttpAuthHandlerNegotiate::CanDelegate() const { |
| // TODO(cbentzel): Should delegation be allowed on proxies? |
| if (target_ == HttpAuth::AUTH_PROXY) |
| return false; |
| if (!url_security_manager_) |
| return false; |
| return url_security_manager_->CanDelegate(origin_); |
| } |
| |
| } // namespace net |