diff options
author | ericroman@google.com <ericroman@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-09-27 03:19:42 +0000 |
---|---|---|
committer | ericroman@google.com <ericroman@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-09-27 03:19:42 +0000 |
commit | c3b35c2100dba30c517116bc9a5a4e4149c3a8e5 (patch) | |
tree | ff42c902c4ee9afd7864a2bda8e5e815a876bc76 /net/http | |
parent | e5be6612288df667ca6ae4a86060bc883a498eea (diff) | |
download | chromium_src-c3b35c2100dba30c517116bc9a5a4e4149c3a8e5.zip chromium_src-c3b35c2100dba30c517116bc9a5a4e4149c3a8e5.tar.gz chromium_src-c3b35c2100dba30c517116bc9a5a4e4149c3a8e5.tar.bz2 |
Initial stab at http authentication (basic + digest) in new http stack.
General design:
- class HttpAuth -- utility class for http-auth logic.
- class HttpAuth::ChallengeTokenizer -- parsing of www-Authenticate headers.
- class HttpAuthHandler -- base class for authentication schemes (inspired by nsIHttpAuthenticator)
- class HttpAuthHandlerBasic : HttpAuthHandler -- logic for basic auth.
- class HttpAuthHandlerDigest : HttpAuthHandler -- logic for digest auth.
- The auth integration in HttpNetworkTransaction mimics that of HttpTransactionWinHttp:
+ HttpNetworkTransaction::ApplyAuth() -- set the authorization headers.
+ HttpNetworkTransaction::PopulateAuthChallenge() -- process the challenges.
BUG=2346
Review URL: http://codereview.chromium.org/4063
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@2658 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net/http')
-rw-r--r-- | net/http/http_auth.cc | 147 | ||||
-rw-r--r-- | net/http/http_auth.h | 120 | ||||
-rw-r--r-- | net/http/http_auth_handler.cc | 27 | ||||
-rw-r--r-- | net/http/http_auth_handler.h | 77 | ||||
-rw-r--r-- | net/http/http_auth_handler_basic.cc | 46 | ||||
-rw-r--r-- | net/http/http_auth_handler_basic.h | 27 | ||||
-rw-r--r-- | net/http/http_auth_handler_basic_unittest.cc | 34 | ||||
-rw-r--r-- | net/http/http_auth_handler_digest.cc | 278 | ||||
-rw-r--r-- | net/http/http_auth_handler_digest.h | 105 | ||||
-rw-r--r-- | net/http/http_auth_handler_digest_unittest.cc | 210 | ||||
-rw-r--r-- | net/http/http_auth_unittest.cc | 185 | ||||
-rw-r--r-- | net/http/http_network_transaction.cc | 158 | ||||
-rw-r--r-- | net/http/http_network_transaction.h | 45 | ||||
-rw-r--r-- | net/http/http_response_headers_unittest.cc | 20 | ||||
-rw-r--r-- | net/http/http_util.cc | 73 | ||||
-rw-r--r-- | net/http/http_util.h | 17 | ||||
-rw-r--r-- | net/http/http_util_unittest.cc | 31 |
17 files changed, 1588 insertions, 12 deletions
diff --git a/net/http/http_auth.cc b/net/http/http_auth.cc new file mode 100644 index 0000000..324f901 --- /dev/null +++ b/net/http/http_auth.cc @@ -0,0 +1,147 @@ +// Copyright (c) 2006-2008 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.h" + +#include <algorithm> + +#include "base/basictypes.h" +#include "base/string_util.h" +#include "net/http/http_auth_handler_basic.h" +#include "net/http/http_auth_handler_digest.h" +#include "net/http/http_response_headers.h" +#include "net/http/http_util.h" + +namespace net { + +// static +HttpAuthHandler* HttpAuth::ChooseBestChallenge( + const HttpResponseHeaders* headers, Target target) { + // Choose the challenge whose authentication handler gives the maximum score. + scoped_ptr<HttpAuthHandler> best; + const std::string header_name = GetChallengeHeaderName(target); + std::string cur_challenge; + void* iter = NULL; + while (headers->EnumerateHeader(&iter, header_name, &cur_challenge)) { + scoped_ptr<HttpAuthHandler> cur( + CreateAuthHandler(cur_challenge, target)); + if (cur.get() && (!best.get() || best->score() < cur->score())) + best.reset(cur.release()); + } + return best.release(); +} + +// static +HttpAuthHandler* HttpAuth::CreateAuthHandler(const std::string& challenge, + Target target) { + // Find the right auth handler for the challenge's scheme. + ChallengeTokenizer props(challenge.begin(), challenge.end()); + scoped_ptr<HttpAuthHandler> handler; + + if (LowerCaseEqualsASCII(props.scheme(), "basic")) { + handler.reset(new HttpAuthHandlerBasic()); + } else if (LowerCaseEqualsASCII(props.scheme(), "digest")) { + handler.reset(new HttpAuthHandlerDigest()); + } + if (handler.get()) { + if (!handler->InitFromChallenge(challenge.begin(), challenge.end(), + target)) { + // Invalid/unsupported challenge. + return NULL; + } + } + return handler.release(); +} + +void HttpAuth::ChallengeTokenizer::Init(std::string::const_iterator begin, + std::string::const_iterator end) { + // The first space-separated token is the auth-scheme. + // NOTE: we are more permissive than RFC 2617 which says auth-scheme + // is separated by 1*SP. + StringTokenizer tok(begin, end, HTTP_LWS); + if (!tok.GetNext()) { + valid_ = false; + return; + } + + // Save the scheme's position. + scheme_begin_ = tok.token_begin(); + scheme_end_ = tok.token_end(); + + // Everything past scheme_end_ is a (comma separated) value list. + if (scheme_end_ != end) + props_ = HttpUtil::ValuesIterator(scheme_end_ + 1, end, ','); +} + +// We expect properties to be formatted as one of: +// name="value" +// name=value +// name= +bool HttpAuth::ChallengeTokenizer::GetNext() { + if (!props_.GetNext()) + return false; + + // Set the value as everything. Next we will split out the name. + value_begin_ = props_.value_begin(); + value_end_ = props_.value_end(); + name_begin_ = name_end_ = value_end_; + + // Scan for the equals sign. + std::string::const_iterator equals = std::find(value_begin_, value_end_, '='); + if (equals == value_end_ || equals == value_begin_) + return valid_ = false; // Malformed + + // Verify that the equals sign we found wasn't inside of quote marks. + for (std::string::const_iterator it = value_begin_; it != equals; ++it) { + if (HttpUtil::IsQuote(*it)) + return valid_ = false; // Malformed + } + + name_begin_ = value_begin_; + name_end_ = equals; + value_begin_ = equals + 1; + + if (value_begin_ != value_end_ && HttpUtil::IsQuote(*value_begin_)) { + // Trim surrounding quotemarks off the value + if (*value_begin_ != *(value_end_ - 1)) + return valid_ = false; // Malformed -- mismatching quotes. + value_is_quoted_ = true; + } else { + value_is_quoted_ = false; + } + return true; +} + +// If value() has quotemarks, unquote it. +std::string HttpAuth::ChallengeTokenizer::unquoted_value() const { + return HttpUtil::Unquote(value_begin_, value_end_); +} + +// static +std::string HttpAuth::GetChallengeHeaderName(Target target) { + switch(target) { + case AUTH_PROXY: + return "Proxy-Authenticate"; + case AUTH_SERVER: + return "WWW-Authenticate"; + default: + NOTREACHED(); + return ""; + } +} + +// static +std::string HttpAuth::GetAuthorizationHeaderName(Target target) { + switch(target) { + case AUTH_PROXY: + return "Proxy-Authorization"; + case AUTH_SERVER: + return "Authorization"; + default: + NOTREACHED(); + return ""; + } +} + +} // namespace net diff --git a/net/http/http_auth.h b/net/http/http_auth.h new file mode 100644 index 0000000..34d10f0 --- /dev/null +++ b/net/http/http_auth.h @@ -0,0 +1,120 @@ +// Copyright (c) 2006-2008 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. + +#ifndef NET_HTTP_HTTP_AUTH_H_ +#define NET_HTTP_HTTP_AUTH_H_ + +#include "net/http/http_util.h" + +namespace net { + +class HttpAuthHandler; +class HttpResponseHeaders; + +// Utility class for http authentication. +class HttpAuth { + public: + + // Http authentication can be done the the proxy server, origin server, + // or both. This enum tracks who the target is. + enum Target { + AUTH_PROXY = 0, + AUTH_SERVER = 1, + }; + + // Get the name of the header containing the auth challenge + // (either WWW-Authenticate or Proxy-Authenticate). + static std::string GetChallengeHeaderName(Target target); + + // Get the name of the header where the credentials go + // (either Authorization or Proxy-Authorization). + static std::string GetAuthorizationHeaderName(Target target); + + // Create a handler to generate credentials for the challenge. If the + // challenge is unsupported or invalid, returns NULL. + // The caller owns the returned pointer. + static HttpAuthHandler* CreateAuthHandler(const std::string& challenge, + Target target); + + // Iterate through the challenge headers, and pick the best one that + // we support. Returns the implementation class for handling the challenge. + // If no supported challenge was found, returns NULL. + // The caller owns the returned pointer. + static HttpAuthHandler* ChooseBestChallenge( + const HttpResponseHeaders* headers, Target target); + + // ChallengeTokenizer breaks up a challenge string into the the auth scheme + // and parameter list, according to RFC 2617 Sec 1.2: + // challenge = auth-scheme 1*SP 1#auth-param + // + // Check valid() after each iteration step in case it was malformed. + // Also note that value() will give whatever is to the right of the equals + // sign, quotemarks and all. Use unquoted_value() to get the logical value. + class ChallengeTokenizer { + public: + ChallengeTokenizer(std::string::const_iterator begin, + std::string::const_iterator end) + : props_(begin, end, ','), valid_(true) { + Init(begin, end); + } + + // Get the auth scheme of the challenge. + std::string::const_iterator scheme_begin() const { return scheme_begin_; } + std::string::const_iterator scheme_end() const { return scheme_end_; } + std::string scheme() const { + return std::string(scheme_begin_, scheme_end_); + } + + // Returns false if there was a parse error. + bool valid() const { + return valid_; + } + + // Advances the iterator to the next name-value pair, if any. + // Returns true if there is none to consume. + bool GetNext(); + + // The name of the current name-value pair. + std::string::const_iterator name_begin() const { return name_begin_; } + std::string::const_iterator name_end() const { return name_end_; } + std::string name() const { + return std::string(name_begin_, name_end_); + } + + // The value of the current name-value pair. + std::string::const_iterator value_begin() const { return value_begin_; } + std::string::const_iterator value_end() const { return value_end_; } + std::string value() const { + return std::string(value_begin_, value_end_); + } + + // If value() has quotemarks, unquote it. + std::string unquoted_value() const; + + // True if the name-value pair's value has quote marks. + bool value_is_quoted() const { return value_is_quoted_; } + + private: + void Init(std::string::const_iterator begin, + std::string::const_iterator end); + + HttpUtil::ValuesIterator props_; + bool valid_; + + std::string::const_iterator scheme_begin_; + std::string::const_iterator scheme_end_; + + std::string::const_iterator name_begin_; + std::string::const_iterator name_end_; + + std::string::const_iterator value_begin_; + std::string::const_iterator value_end_; + + bool value_is_quoted_; + }; +}; + +} // namespace net + +#endif // NET_HTTP_HTTP_AUTH_H_ diff --git a/net/http/http_auth_handler.cc b/net/http/http_auth_handler.cc new file mode 100644 index 0000000..b0a7b97 --- /dev/null +++ b/net/http/http_auth_handler.cc @@ -0,0 +1,27 @@ +// Copyright (c) 2006-2008 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 "base/logging.h" +#include "net/http/http_auth_handler.h" + +namespace net { + +bool HttpAuthHandler::InitFromChallenge(std::string::const_iterator begin, + std::string::const_iterator end, + HttpAuth::Target target) { + target_ = target; + score_ = -1; + scheme_ = NULL; + + bool ok = Init(begin, end); + + // Init() is expected to set the scheme, realm, and score. + DCHECK(!ok || !scheme().empty()); + DCHECK(!ok || !realm().empty()); + DCHECK(!ok || score_ != -1); + + return ok; +} + +} // namespace net diff --git a/net/http/http_auth_handler.h b/net/http/http_auth_handler.h new file mode 100644 index 0000000..0608f7b --- /dev/null +++ b/net/http/http_auth_handler.h @@ -0,0 +1,77 @@ +// Copyright (c) 2006-2008 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. + +#ifndef NET_HTTP_HTTP_AUTH_HANDLER_H_ +#define NET_HTTP_HTTP_AUTH_HANDLER_H_ + +#include <string> + +#include "net/http/http_auth.h" + +namespace net { + +class HttpRequestInfo; +class ProxyInfo; + +// HttpAuthHandler is the interface for the authentication schemes +// (basic, digest, ...) +// The registry mapping auth-schemes to implementations is hardcoded in +// HttpAuth::CreateAuthHandler(). +class HttpAuthHandler { + public: + // Initialize the handler by parsing a challenge string. + bool InitFromChallenge(std::string::const_iterator begin, + std::string::const_iterator end, + HttpAuth::Target target); + + // Lowercase name of the auth scheme + virtual std::string scheme() const { + return scheme_; + } + + // The realm value that was parsed during Init(). + std::string realm() const { + return realm_; + } + + // Numeric rank based on the challenge's security level. Higher + // numbers are better. Used by HttpAuth::ChooseBestChallenge(). + int score() const { + return score_; + } + + HttpAuth::Target target() const { + return target_; + } + + // Generate the Authorization header value. + virtual std::string GenerateCredentials(const std::wstring& username, + const std::wstring& password, + const HttpRequestInfo* request, + const ProxyInfo* proxy) = 0; + + protected: + // Initialize the handler by parsing a challenge string. + // Implementations are expcted to initialize the following members: + // score_, realm_, scheme_ + virtual bool Init(std::string::const_iterator challenge_begin, + std::string::const_iterator challenge_end) = 0; + + // The lowercase auth-scheme {"basic", "digest", "ntlm", ...} + const char* scheme_; + + // The realm. + std::string realm_; + + // The score for this challenge. Higher numbers are better. + int score_; + + // Whether this authentication request is for a proxy server, or an + // origin server. + HttpAuth::Target target_; +}; + +} // namespace net + +#endif // NET_HTTP_HTTP_AUTH_HANDLER_H_ diff --git a/net/http/http_auth_handler_basic.cc b/net/http/http_auth_handler_basic.cc new file mode 100644 index 0000000..51c165c --- /dev/null +++ b/net/http/http_auth_handler_basic.cc @@ -0,0 +1,46 @@ +// Copyright (c) 2006-2008 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_basic.h" + +#include "base/string_util.h" +#include "net/http/http_auth.h" +#include "net/base/base64.h" + +namespace net { + +bool HttpAuthHandlerBasic::Init(std::string::const_iterator challenge_begin, + std::string::const_iterator challenge_end) { + scheme_ = "basic"; + score_ = 1; + + // Verify the challenge's auth-scheme. + HttpAuth::ChallengeTokenizer challenge_tok(challenge_begin, challenge_end); + if (!challenge_tok.valid() || + !LowerCaseEqualsASCII(challenge_tok.scheme(), "basic")) + return false; + + // Extract the realm. + while (challenge_tok.GetNext()) { + if (LowerCaseEqualsASCII(challenge_tok.name(), "realm")) + realm_ = challenge_tok.unquoted_value(); + } + + return challenge_tok.valid() && !realm_.empty(); +} + +std::string HttpAuthHandlerBasic::GenerateCredentials( + const std::wstring& username, + const std::wstring& password, + const HttpRequestInfo*, + const ProxyInfo*) { + // TODO(eroman): is this the right encoding of username/password? + std::string base64_username_password; + if (!Base64Encode(WideToUTF8(username) + ":" + WideToUTF8(password), + &base64_username_password)) + return std::string(); // FAIL + return std::string("Basic ") + base64_username_password; +} + +} // namespace net diff --git a/net/http/http_auth_handler_basic.h b/net/http/http_auth_handler_basic.h new file mode 100644 index 0000000..2db2fcb --- /dev/null +++ b/net/http/http_auth_handler_basic.h @@ -0,0 +1,27 @@ +// Copyright (c) 2006-2008 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. + +#ifndef NET_HTTP_HTTP_AUTH_HANDLER_BASIC_H_ +#define NET_HTTP_HTTP_AUTH_HANDLER_BASIC_H_ + +#include "net/http/http_auth_handler.h" + +namespace net { + +// Code for handling http basic authentication. +class HttpAuthHandlerBasic : public HttpAuthHandler { + public: + virtual std::string GenerateCredentials(const std::wstring& username, + const std::wstring& password, + const HttpRequestInfo*, + const ProxyInfo*); + protected: + virtual bool Init(std::string::const_iterator challenge_begin, + std::string::const_iterator challenge_end); + +}; + +} // namespace net + +#endif // NET_HTTP_HTTP_AUTH_HANDLER_BASIC_H_ diff --git a/net/http/http_auth_handler_basic_unittest.cc b/net/http/http_auth_handler_basic_unittest.cc new file mode 100644 index 0000000..bf860e3 --- /dev/null +++ b/net/http/http_auth_handler_basic_unittest.cc @@ -0,0 +1,34 @@ +// Copyright (c) 2006-2008 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 "testing/gtest/include/gtest/gtest.h" + +#include "base/basictypes.h" +#include "net/http/http_auth_handler_basic.h" + +namespace net { + +TEST(HttpAuthHandlerBasicTest, GenerateCredentials) { + static const struct { + const wchar_t* username; + const wchar_t* password; + const char* expected_credentials; + } tests[] = { + { L"foo", L"bar", "Basic Zm9vOmJhcg==" }, + // Empty password + { L"anon", L"", "Basic YW5vbjo=" }, + }; + for (int i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + std::string challenge = "Basic realm=\"Atlantis\""; + HttpAuthHandlerBasic basic; + basic.InitFromChallenge(challenge.begin(), challenge.end(), + HttpAuth::AUTH_SERVER); + std::string credentials = basic.GenerateCredentials(tests[i].username, + tests[i].password, + NULL, NULL); + EXPECT_STREQ(tests[i].expected_credentials, credentials.c_str()); + } +} + +} // namespace net diff --git a/net/http/http_auth_handler_digest.cc b/net/http/http_auth_handler_digest.cc new file mode 100644 index 0000000..e665a0b --- /dev/null +++ b/net/http/http_auth_handler_digest.cc @@ -0,0 +1,278 @@ +// Copyright (c) 2006-2008 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_digest.h" + +#include "base/md5.h" +#include "base/string_util.h" +#include "net/base/net_util.h" +#include "net/http/http_auth.h" +#include "net/http/http_request_info.h" +#include "net/http/http_util.h" + +// TODO(eroman): support qop=auth-int + +namespace net { + +// Digest authentication is specified in RFC 2617. +// The expanded derivations are listed in the tables below. + +//==========+==========+==========================================+ +// qop |algorithm | response | +//==========+==========+==========================================+ +// ? | ?, md5, | MD5(MD5(A1):nonce:MD5(A2)) | +// | md5-sess | | +//--------- +----------+------------------------------------------+ +// auth, | ?, md5, | MD5(MD5(A1):nonce:nc:cnonce:qop:MD5(A2)) | +// auth-int | md5-sess | | +//==========+==========+==========================================+ +// qop |algorithm | A1 | +//==========+==========+==========================================+ +// | ?, md5 | user:realm:password | +//----------+----------+------------------------------------------+ +// | md5-sess | MD5(user:realm:password):nonce:cnonce | +//==========+==========+==========================================+ +// qop |algorithm | A2 | +//==========+==========+==========================================+ +// ?, auth | | req-method:req-uri | +//----------+----------+------------------------------------------+ +// auth-int | | req-method:req-uri:MD5(req-entity-body) | +//=====================+==========================================+ + + +// static +std::string HttpAuthHandlerDigest::GenerateNonce() { + // This is how mozilla generates their cnonce -- a 16 digit hex string. + static const char domain[] = "0123456789abcdef"; + std::string cnonce; + cnonce.reserve(16); + // TODO(eroman): use rand_util::RandIntSecure() + for (int i = 0; i < 16; ++i) + cnonce.push_back(domain[rand() % 16]); + return cnonce; +} + +// static +std::string HttpAuthHandlerDigest::QopToString(int qop) { + switch (qop) { + case QOP_AUTH: + return "auth"; + case QOP_AUTH_INT: + return "auth-int"; + default: + return ""; + } +} + +// static +std::string HttpAuthHandlerDigest::AlgorithmToString(int algorithm) { + switch (algorithm) { + case ALGORITHM_MD5: + return "MD5"; + case ALGORITHM_MD5_SESS: + return "MD5-sess"; + default: + return ""; + } +} + +std::string HttpAuthHandlerDigest::GenerateCredentials( + const std::wstring& username, + const std::wstring& password, + const HttpRequestInfo* request, + const ProxyInfo* proxy) { + // Generate a random client nonce. + std::string cnonce = GenerateNonce(); + + // The nonce-count should be incremented after re-use per the spec. + // This may not be possible when there are multiple connections to the + // server though: + // https://bugzilla.mozilla.org/show_bug.cgi?id=114451 + // TODO(eroman): leave as 1 for now, and possibly permanently. + int nonce_count = 1; + + // Extract the request method and path -- the meaning of 'path' is overloaded + // in certain cases, to be a hostname. + std::string method; + std::string path; + GetRequestMethodAndPath(request, proxy, &method, &path); + + return AssembleCredentials(method, path, + // TODO(eroman): is this the right encoding? + WideToUTF8(username), + WideToUTF8(password), + cnonce, nonce_count); +} + +void HttpAuthHandlerDigest::GetRequestMethodAndPath( + const HttpRequestInfo* request, + const ProxyInfo* proxy, + std::string* method, + std::string* path) const { + DCHECK(request); + DCHECK(proxy); + + const GURL& url = request->url; + + if (target_ == HttpAuth::AUTH_PROXY && url.SchemeIs("https")) { + *method = "CONNECT"; + *path = url.host() + ":" + GetImplicitPort(url); + } else { + *method = request->method; + *path = HttpUtil::PathForRequest(url); + } +} + +std::string HttpAuthHandlerDigest::AssembleResponseDigest( + const std::string& method, + const std::string& path, + const std::string& username, + const std::string& password, + const std::string& cnonce, + const std::string& nc) const { + // ha1 = MD5(A1) + std::string ha1 = MD5String(username + ":" + realm_ + ":" + password); + if (algorithm_ == HttpAuthHandlerDigest::ALGORITHM_MD5_SESS) + ha1 = MD5String(ha1 + ":" + nonce_ + ":" + cnonce); + + // ha2 = MD5(A2) + // TODO(eroman): need to add MD5(req-entity-body) for qop=auth-int. + std::string ha2 = MD5String(method + ":" + path); + + std::string nc_part; + if (qop_ != HttpAuthHandlerDigest::QOP_UNSPECIFIED) { + nc_part = nc + ":" + cnonce + ":" + QopToString(qop_) + ":"; + } + + return MD5String(ha1 + ":" + nonce_ + ":" + nc_part + ha2); +} + +std::string HttpAuthHandlerDigest::AssembleCredentials( + const std::string& method, + const std::string& path, + const std::string& username, + const std::string& password, + const std::string& cnonce, + int nonce_count) const { + // the nonce-count is an 8 digit hex string. + std::string nc = StringPrintf("%08x", nonce_count); + + std::string authorization = std::string("Digest username=") + + HttpUtil::Quote(username); + authorization += ", realm=" + HttpUtil::Quote(realm_); + authorization += ", nonce=" + HttpUtil::Quote(nonce_); + authorization += ", uri=" + HttpUtil::Quote(path); + + if (algorithm_ != ALGORITHM_UNSPECIFIED) { + authorization += ", algorithm=" + AlgorithmToString(algorithm_); + } + std::string response = AssembleResponseDigest(method, path, username, + password, cnonce, nc); + // No need to call HttpUtil::Quote() as the response digest cannot contain + // any characters needing to be escaped. + authorization += ", response=\"" + response + "\""; + + if (!opaque_.empty()) { + authorization += ", opaque=" + HttpUtil::Quote(opaque_); + } + if (qop_ != QOP_UNSPECIFIED) { + // TODO(eroman): Supposedly IIS server requires quotes surrounding qop. + authorization += ", qop=" + QopToString(qop_); + authorization += ", nc=" + nc; + authorization += ", cnonce=" + HttpUtil::Quote(cnonce); + } + + return authorization; +} + +// The digest challenge header looks like: +// WWW-Authenticate: Digest +// realm="<realm-value>" +// nonce="<nonce-value>" +// [domain="<list-of-URIs>"] +// [opaque="<opaque-token-value>"] +// [stale="<true-or-false>"] +// [algorithm="<digest-algorithm>"] +// [qop="<list-of-qop-values>"] +// [<extension-directive>] +bool HttpAuthHandlerDigest::ParseChallenge( + std::string::const_iterator challenge_begin, + std::string::const_iterator challenge_end) { + scheme_ = "digest"; + score_ = 2; + + // Initialize to defaults. + stale_ = false; + algorithm_ = ALGORITHM_UNSPECIFIED; + qop_ = QOP_UNSPECIFIED; + realm_ = nonce_ = domain_ = opaque_ = std::string(); + + HttpAuth::ChallengeTokenizer props(challenge_begin, challenge_end); + + if (!props.valid() || !LowerCaseEqualsASCII(props.scheme(), "digest")) + return false; // FAIL -- Couldn't match auth-scheme. + + // Loop through all the properties. + while (props.GetNext()) { + if (props.value().empty()) { + DLOG(INFO) << "Invalid digest property"; + return false; + } + + if (!ParseChallengeProperty(props.name(), props.unquoted_value())) + return false; // FAIL -- couldn't parse a property. + } + + // Check if tokenizer failed. + if (!props.valid()) + return false; // FAIL + + // Check that a minimum set of properties were provided. + if (realm_.empty() || nonce_.empty()) + return false; // FAIL + + return true; +} + +bool HttpAuthHandlerDigest::ParseChallengeProperty(const std::string& name, + const std::string& value) { + if (LowerCaseEqualsASCII(name, "realm")) { + realm_ = value; + } else if (LowerCaseEqualsASCII(name, "nonce")) { + nonce_ = value; + } else if (LowerCaseEqualsASCII(name, "domain")) { + domain_ = value; + } else if (LowerCaseEqualsASCII(name, "opaque")) { + opaque_ = value; + } else if (LowerCaseEqualsASCII(name, "stale")) { + // Parse the stale boolean. + stale_ = LowerCaseEqualsASCII(value, "true"); + } else if (LowerCaseEqualsASCII(name, "algorithm")) { + // Parse the algorithm. + if (LowerCaseEqualsASCII(value, "md5")) { + algorithm_ = ALGORITHM_MD5; + } else if (LowerCaseEqualsASCII(value, "md5-sess")) { + algorithm_ = ALGORITHM_MD5_SESS; + } else { + DLOG(INFO) << "Unknown value of algorithm"; + return false; // FAIL -- unsupported value of algorithm. + } + } else if (LowerCaseEqualsASCII(name, "qop")) { + // Parse the comma separated list of qops. + HttpUtil::ValuesIterator qop_values(value.begin(), value.end(), ','); + while (qop_values.GetNext()) { + if (LowerCaseEqualsASCII(qop_values.value(), "auth")) { + qop_ |= QOP_AUTH; + } else if (LowerCaseEqualsASCII(qop_values.value(), "auth-int")) { + qop_ |= QOP_AUTH_INT; + } + } + } else { + DLOG(INFO) << "Skipping unrecognized digest property"; + // TODO(eroman): perhaps we should fail instead of silently skipping? + } + return true; +} + +} // namespace net diff --git a/net/http/http_auth_handler_digest.h b/net/http/http_auth_handler_digest.h new file mode 100644 index 0000000..5289cc5 --- /dev/null +++ b/net/http/http_auth_handler_digest.h @@ -0,0 +1,105 @@ +// Copyright (c) 2006-2008 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. + +#ifndef NET_HTTP_HTTP_AUTH_HANDLER_DIGEST_H_ +#define NET_HTTP_HTTP_AUTH_HANDLER_DIGEST_H_ + +#include "net/http/http_auth_handler.h" + +// This is needed for the FRIEND_TEST() macro. +#include "testing/gtest/include/gtest/gtest_prod.h" + +namespace net { + +// Code for handling http digest authentication. +class HttpAuthHandlerDigest : public HttpAuthHandler { + public: + virtual std::string GenerateCredentials(const std::wstring& username, + const std::wstring& password, + const HttpRequestInfo* request, + const ProxyInfo* proxy); + + protected: + virtual bool Init(std::string::const_iterator challenge_begin, + std::string::const_iterator challenge_end) { + return ParseChallenge(challenge_begin, challenge_end); + } + + private: + FRIEND_TEST(HttpAuthHandlerDigestTest, ParseChallenge); + FRIEND_TEST(HttpAuthHandlerDigestTest, AssembleCredentials); + + // Possible values for the "algorithm" property. + enum DigestAlgorithm { + // No algorithm was specified. According to RFC 2617 this means + // we should default to ALGORITHM_MD5. + ALGORITHM_UNSPECIFIED, + + // Hashes are run for every request. + ALGORITHM_MD5, + + // Hash is run only once during the first WWW-Authenticate handshake. + // (SESS means session). + ALGORITHM_MD5_SESS, + }; + + // Possible values for "qop" -- may be or-ed together if there were + // multiple comma separated values. + enum QualityOfProtection { + QOP_UNSPECIFIED = 0, + QOP_AUTH = 1 << 0, + QOP_AUTH_INT = 1 << 1, + }; + + // Parse the challenge, saving the results into this instance. + // Returns true on success. + bool ParseChallenge(std::string::const_iterator challenge_begin, + std::string::const_iterator challenge_end); + + // Parse an individual property. Returns true on success. + bool ParseChallengeProperty(const std::string& name, + const std::string& value); + + // Generates a random string, to be used for client-nonce. + static std::string GenerateNonce(); + + // Convert enum value back to string. + static std::string QopToString(int qop); + static std::string AlgorithmToString(int algorithm); + + // Extract the method and path of the request, as needed by + // the 'A2' production. (path may be a hostname for proxy). + void GetRequestMethodAndPath(const HttpRequestInfo* request, + const ProxyInfo* proxy, + std::string* method, + std::string* path) const; + + // Build up the 'response' production. + std::string AssembleResponseDigest(const std::string& method, + const std::string& path, + const std::string& username, + const std::string& password, + const std::string& cnonce, + const std::string& nc) const; + + // Build up the value for (Authorization/Proxy-Authorization). + std::string AssembleCredentials(const std::string& method, + const std::string& path, + const std::string& username, + const std::string& password, + const std::string& cnonce, + int nonce_count) const; + + // Information parsed from the challenge. + std::string nonce_; + std::string domain_; + std::string opaque_; + bool stale_; + DigestAlgorithm algorithm_; + int qop_; // Bitfield of QualityOfProtection +}; + +} // namespace net + +#endif // NET_HTTP_HTTP_AUTH_HANDLER_DIGEST_H_ diff --git a/net/http/http_auth_handler_digest_unittest.cc b/net/http/http_auth_handler_digest_unittest.cc new file mode 100644 index 0000000..41df781 --- /dev/null +++ b/net/http/http_auth_handler_digest_unittest.cc @@ -0,0 +1,210 @@ +// Copyright (c) 2006-2008 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 "testing/gtest/include/gtest/gtest.h" + +#include "base/basictypes.h" +#include "net/http/http_auth_handler_digest.h" + +namespace net { + +TEST(HttpAuthHandlerDigestTest, ParseChallenge) { + static const struct { + // The challenge string. + const char* challenge; + // Expected return value of ParseChallenge. + bool parsed_success; + // The expected values that were parsed. + const char* parsed_realm; + const char* parsed_nonce; + const char* parsed_domain; + const char* parsed_opaque; + bool parsed_stale; + int parsed_algorithm; + int parsed_qop; + } tests[] = { + { + "Digest nonce=\"xyz\", realm=\"Thunder Bluff\"", + true, + "Thunder Bluff", + "xyz", + "", + "", + false, + HttpAuthHandlerDigest::ALGORITHM_UNSPECIFIED, + HttpAuthHandlerDigest::QOP_UNSPECIFIED + }, + + {// Check that when algorithm has an unsupported value, parsing fails. + "Digest nonce=\"xyz\", algorithm=\"awezum\", realm=\"Thunder\"", + false, + // The remaining values don't matter (but some have been set already). + "", + "xyz", + "", + "", + false, + HttpAuthHandlerDigest::ALGORITHM_UNSPECIFIED, + HttpAuthHandlerDigest::QOP_UNSPECIFIED + }, + + { // Check that algorithm's value is case insensitive. + "Digest nonce=\"xyz\", algorithm=\"mD5\", realm=\"Oblivion\"", + true, + "Oblivion", + "xyz", + "", + "", + false, + HttpAuthHandlerDigest::ALGORITHM_MD5, + HttpAuthHandlerDigest::QOP_UNSPECIFIED + }, + + { // Check that md5-sess is recognized, as is single QOP + "Digest nonce=\"xyz\", algorithm=\"md5-sess\", " + "realm=\"Oblivion\", qop=\"auth\"", + true, + "Oblivion", + "xyz", + "", + "", + false, + HttpAuthHandlerDigest::ALGORITHM_MD5_SESS, + HttpAuthHandlerDigest::QOP_AUTH + }, + + { // The realm can't be missing. + "Digest nonce=\"xyz\"", + false, // FAILED parse. + "", + "xyz", + "", + "", + false, + HttpAuthHandlerDigest::ALGORITHM_UNSPECIFIED, + HttpAuthHandlerDigest::QOP_UNSPECIFIED + } + }; + + for (int i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + std::string challenge(tests[i].challenge); + + HttpAuthHandlerDigest auth; + bool ok = auth.ParseChallenge(challenge.begin(), challenge.end()); + + EXPECT_EQ(tests[i].parsed_success, ok); + EXPECT_STREQ(tests[i].parsed_realm, auth.realm_.c_str()); + EXPECT_STREQ(tests[i].parsed_nonce, auth.nonce_.c_str()); + EXPECT_STREQ(tests[i].parsed_domain, auth.domain_.c_str()); + EXPECT_STREQ(tests[i].parsed_opaque, auth.opaque_.c_str()); + EXPECT_EQ(tests[i].parsed_stale, auth.stale_); + EXPECT_EQ(tests[i].parsed_algorithm, auth.algorithm_); + EXPECT_EQ(tests[i].parsed_qop, auth.qop_); + } +} + +TEST(HttpAuthHandlerDigestTest, AssembleCredentials) { + static const struct { + const char* req_method; + const char* req_path; + const char* challenge; + const char* username; + const char* password; + const char* cnonce; + int nonce_count; + const char* expected_creds; + } tests[] = { + { // MD5 with username/password + "GET", + "/test/drealm1", + + // Challenge + "Digest realm=\"DRealm1\", " + "nonce=\"claGgoRXBAA=7583377687842fdb7b56ba0555d175baa0b800e3\", " + "algorithm=MD5, qop=\"auth\"", + + "foo", "bar", // username/password + "082c875dcb2ca740", // cnonce + 1, // nc + + // Authorization + "Digest username=\"foo\", realm=\"DRealm1\", " + "nonce=\"claGgoRXBAA=7583377687842fdb7b56ba0555d175baa0b800e3\", " + "uri=\"/test/drealm1\", algorithm=MD5, " + "response=\"bcfaa62f1186a31ff1b474a19a17cf57\", " + "qop=auth, nc=00000001, cnonce=\"082c875dcb2ca740\"" + }, + + { // MD5 with username but empty password. username has space in it. + "GET", + "/test/drealm1/", + + // Challenge + "Digest realm=\"DRealm1\", " + "nonce=\"Ure30oRXBAA=7eca98bbf521ac6642820b11b86bd2d9ed7edc70\", " + "algorithm=MD5, qop=\"auth\"", + + "foo bar", "", // Username/password + "082c875dcb2ca740", // cnonce + 1, // nc + + // Authorization + "Digest username=\"foo bar\", realm=\"DRealm1\", " + "nonce=\"Ure30oRXBAA=7eca98bbf521ac6642820b11b86bd2d9ed7edc70\", " + "uri=\"/test/drealm1/\", algorithm=MD5, " + "response=\"93c9c6d5930af3b0eb26c745e02b04a0\", " + "qop=auth, nc=00000001, cnonce=\"082c875dcb2ca740\"" + }, + + { // No algorithm, and no qop. + "GET", + "/", + + // Challenge + "Digest realm=\"Oblivion\", nonce=\"nonce-value\"", + + "FooBar", "pass", // Username/password + "", // cnonce + 1, // nc + + // Authorization + "Digest username=\"FooBar\", realm=\"Oblivion\", " + "nonce=\"nonce-value\", uri=\"/\", " + "response=\"f72ff54ebde2f928860f806ec04acd1b\"" + }, + + { // MD5-sess + "GET", + "/", + + // Challenge + "Digest realm=\"Baztastic\", nonce=\"AAAAAAAA\", " + "algorithm=\"md5-sess\", qop=auth", + + "USER", "123", // Username/password + "15c07961ed8575c4", // cnonce + 1, // nc + + // Authorization + "Digest username=\"USER\", realm=\"Baztastic\", " + "nonce=\"AAAAAAAA\", uri=\"/\", algorithm=MD5-sess, " + "response=\"cbc1139821ee7192069580570c541a03\", " + "qop=auth, nc=00000001, cnonce=\"15c07961ed8575c4\"" + } + }; + for (int i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + HttpAuthHandlerDigest digest; + std::string challenge = tests[i].challenge; + EXPECT_TRUE(digest.InitFromChallenge( + challenge.begin(), challenge.end(), HttpAuth::AUTH_SERVER)); + + std::string creds = digest.AssembleCredentials(tests[i].req_method, + tests[i].req_path, tests[i].username, tests[i].password, + tests[i].cnonce, tests[i].nonce_count); + + EXPECT_STREQ(tests[i].expected_creds, creds.c_str()); + } +} + +} // namespace net diff --git a/net/http/http_auth_unittest.cc b/net/http/http_auth_unittest.cc new file mode 100644 index 0000000..a82ffa1 --- /dev/null +++ b/net/http/http_auth_unittest.cc @@ -0,0 +1,185 @@ +// Copyright (c) 2006-2008 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 "testing/gtest/include/gtest/gtest.h" + +#include "base/scoped_ptr.h" +#include "net/http/http_auth.h" +#include "net/http/http_auth_handler.h" +#include "net/http/http_response_headers.h" +#include "net/http/http_util.h" + +namespace net { + +TEST(HttpAuthTest, ChooseBestChallenge) { + static const struct { + const char* headers; + const char* challenge_realm; + } tests[] = { + { + "Y: Digest realm=\"X\", nonce=\"aaaaaaaaaa\"\n" + "www-authenticate: Basic realm=\"BasicRealm\"\n", + + // Basic is the only challenge type, pick it. + "BasicRealm", + }, + { + "Y: Digest realm=\"FooBar\", nonce=\"aaaaaaaaaa\"\n" + "www-authenticate: Fake realm=\"FooBar\"\n", + + // Fake is the only challenge type, but it is unsupported. + "", + }, + { + "www-authenticate: Basic realm=\"FooBar\"\n" + "www-authenticate: Fake realm=\"FooBar\"\n" + "www-authenticate: nonce=\"aaaaaaaaaa\"\n" + "www-authenticate: Digest realm=\"DigestRealm\", nonce=\"aaaaaaaaaa\"\n", + + // Pick Digset over Basic + "DigestRealm", + } + }; + + for (int i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + // Make a HttpResponseHeaders object. + std::string headers_with_status_line("HTTP/1.1 401 OK\n"); + headers_with_status_line += tests[i].headers; + scoped_refptr<net::HttpResponseHeaders> headers( + new net::HttpResponseHeaders( + net::HttpUtil::AssembleRawHeaders( + headers_with_status_line.c_str(), + headers_with_status_line.length()))); + + scoped_ptr<HttpAuthHandler> handler(HttpAuth::ChooseBestChallenge( + headers.get(), HttpAuth::AUTH_SERVER)); + + if (handler.get()) { + EXPECT_STREQ(tests[i].challenge_realm, handler->realm().c_str()); + } else { + EXPECT_STREQ("", tests[i].challenge_realm); + } + } +} + +TEST(HttpAuthTest, ChallengeTokenizer) { + std::string challenge_str = "Basic realm=\"foobar\""; + HttpAuth::ChallengeTokenizer challenge(challenge_str.begin(), + challenge_str.end()); + EXPECT_TRUE(challenge.valid()); + EXPECT_EQ(std::string("Basic"), challenge.scheme()); + EXPECT_TRUE(challenge.GetNext()); + EXPECT_TRUE(challenge.valid()); + EXPECT_EQ(std::string("realm"), challenge.name()); + EXPECT_EQ(std::string("foobar"), challenge.unquoted_value()); + EXPECT_EQ(std::string("\"foobar\""), challenge.value()); + EXPECT_TRUE(challenge.value_is_quoted()); + EXPECT_FALSE(challenge.GetNext()); +} + +// Use a name=value property with no quote marks. +TEST(HttpAuthTest, ChallengeTokenizerNoQuotes) { + std::string challenge_str = "Basic realm=foobar@baz.com"; + HttpAuth::ChallengeTokenizer challenge(challenge_str.begin(), + challenge_str.end()); + EXPECT_TRUE(challenge.valid()); + EXPECT_EQ(std::string("Basic"), challenge.scheme()); + EXPECT_TRUE(challenge.GetNext()); + EXPECT_TRUE(challenge.valid()); + EXPECT_EQ(std::string("realm"), challenge.name()); + EXPECT_EQ(std::string("foobar@baz.com"), challenge.value()); + EXPECT_EQ(std::string("foobar@baz.com"), challenge.unquoted_value()); + EXPECT_FALSE(challenge.value_is_quoted()); + EXPECT_FALSE(challenge.GetNext()); +} + +// Use a name= property which has no value. +TEST(HttpAuthTest, ChallengeTokenizerNoValue) { + std::string challenge_str = "Digest qop="; + HttpAuth::ChallengeTokenizer challenge( + challenge_str.begin(), challenge_str.end()); + EXPECT_TRUE(challenge.valid()); + EXPECT_EQ(std::string("Digest"), challenge.scheme()); + EXPECT_TRUE(challenge.GetNext()); + EXPECT_TRUE(challenge.valid()); + EXPECT_EQ(std::string("qop"), challenge.name()); + EXPECT_EQ(std::string(""), challenge.value()); + EXPECT_FALSE(challenge.value_is_quoted()); + EXPECT_FALSE(challenge.GetNext()); +} + +// Specify multiple properties, comma separated. +TEST(HttpAuthTest, ChallengeTokenizerMultiple) { + std::string challenge_str = + "Digest algorithm=md5, realm=\"Oblivion\", qop=auth-int"; + HttpAuth::ChallengeTokenizer challenge(challenge_str.begin(), + challenge_str.end()); + EXPECT_TRUE(challenge.valid()); + EXPECT_EQ(std::string("Digest"), challenge.scheme()); + EXPECT_TRUE(challenge.GetNext()); + EXPECT_TRUE(challenge.valid()); + EXPECT_EQ(std::string("algorithm"), challenge.name()); + EXPECT_EQ(std::string("md5"), challenge.value()); + EXPECT_FALSE(challenge.value_is_quoted()); + EXPECT_TRUE(challenge.GetNext()); + EXPECT_TRUE(challenge.valid()); + EXPECT_EQ(std::string("realm"), challenge.name()); + EXPECT_EQ(std::string("Oblivion"), challenge.unquoted_value()); + EXPECT_TRUE(challenge.value_is_quoted()); + EXPECT_TRUE(challenge.GetNext()); + EXPECT_TRUE(challenge.valid()); + EXPECT_EQ(std::string("qop"), challenge.name()); + EXPECT_EQ(std::string("auth-int"), challenge.value()); + EXPECT_FALSE(challenge.value_is_quoted()); + EXPECT_FALSE(challenge.GetNext()); +} + +TEST(HttpAuthTest, GetChallengeHeaderName) { + std::string name; + + name = HttpAuth::GetChallengeHeaderName(HttpAuth::AUTH_SERVER); + EXPECT_STREQ("WWW-Authenticate", name.c_str()); + + name = HttpAuth::GetChallengeHeaderName(HttpAuth::AUTH_PROXY); + EXPECT_STREQ("Proxy-Authenticate", name.c_str()); +} + +TEST(HttpAuthTest, GetAuthorizationHeaderName) { + std::string name; + + name = HttpAuth::GetAuthorizationHeaderName(HttpAuth::AUTH_SERVER); + EXPECT_STREQ("Authorization", name.c_str()); + + name = HttpAuth::GetAuthorizationHeaderName(HttpAuth::AUTH_PROXY); + EXPECT_STREQ("Proxy-Authorization", name.c_str()); +} + +TEST(HttpAuthTest, CreateAuthHandler) { + { + scoped_ptr<HttpAuthHandler> handler( + HttpAuth::CreateAuthHandler("Basic realm=\"FooBar\"", + HttpAuth::AUTH_SERVER)); + EXPECT_FALSE(handler.get() == NULL); + EXPECT_STREQ("basic", handler->scheme().c_str()); + EXPECT_STREQ("FooBar", handler->realm().c_str()); + EXPECT_EQ(HttpAuth::AUTH_SERVER, handler->target()); + } + { + scoped_ptr<HttpAuthHandler> handler( + HttpAuth::CreateAuthHandler("UNSUPPORTED realm=\"FooBar\"", + HttpAuth::AUTH_SERVER)); + EXPECT_TRUE(handler.get() == NULL); + } + { + scoped_ptr<HttpAuthHandler> handler(HttpAuth::CreateAuthHandler( + "Digest realm=\"FooBar\", nonce=\"xyz\"", + HttpAuth::AUTH_PROXY)); + EXPECT_FALSE(handler.get() == NULL); + EXPECT_STREQ("digest", handler->scheme().c_str()); + EXPECT_STREQ("FooBar", handler->realm().c_str()); + EXPECT_EQ(HttpAuth::AUTH_PROXY, handler->target()); + } +} + +} // namespace net diff --git a/net/http/http_network_transaction.cc b/net/http/http_network_transaction.cc index ea0f3d7..c518f14 100644 --- a/net/http/http_network_transaction.cc +++ b/net/http/http_network_transaction.cc @@ -4,6 +4,7 @@ #include "net/http/http_network_transaction.h" +#include "base/scoped_ptr.h" #include "base/compiler_specific.h" #include "base/string_util.h" #include "base/trace_event.h" @@ -11,10 +12,13 @@ #include "net/base/client_socket_factory.h" #include "net/base/host_resolver.h" #include "net/base/load_flags.h" +#include "net/base/net_util.h" #if defined(OS_WIN) #include "net/base/ssl_client_socket.h" #endif #include "net/base/upload_data_stream.h" +#include "net/http/http_auth.h" +#include "net/http/http_auth_handler.h" #include "net/http/http_chunked_decoder.h" #include "net/http/http_network_session.h" #include "net/http/http_request_info.h" @@ -22,6 +26,8 @@ // TODO(darin): // - authentication +// + pre-emptive authorization +// + use the username/password encoded in the URL. // - proxies (need to call ReconsiderProxyAfterError and handle SSL tunnel) // - ssl @@ -86,7 +92,33 @@ int HttpNetworkTransaction::RestartWithAuth( const std::wstring& username, const std::wstring& password, CompletionCallback* callback) { - return ERR_FAILED; // TODO(darin): implement me! + + DCHECK(NeedAuth(HttpAuth::AUTH_PROXY) || + NeedAuth(HttpAuth::AUTH_SERVER)); + + // Figure out whether this username password is for proxy or server. + // Proxy gets set first, then server. + HttpAuth::Target target = NeedAuth(HttpAuth::AUTH_PROXY) ? + HttpAuth::AUTH_PROXY : HttpAuth::AUTH_SERVER; + + // Update the username/password. + auth_data_[target]->state = AUTH_STATE_HAVE_AUTH; + auth_data_[target]->username = username; + auth_data_[target]->password = password; + + next_state_ = STATE_INIT_CONNECTION; + connection_.set_socket(NULL); + connection_.Reset(); + + // Reset the other member variables. + ResetStateForRestart(); + + DCHECK(user_callback_ == NULL); + int rv = DoLoop(OK); + if (rv == ERR_IO_PENDING) + user_callback_ = callback; + + return rv; } int HttpNetworkTransaction::Read(char* buf, int buf_len, @@ -199,6 +231,8 @@ void HttpNetworkTransaction::BuildRequestHeaders() { request_headers_ += "Cache-Control: max-age=0\r\n"; } + ApplyAuth(); + // TODO(darin): Need to prune out duplicate headers. request_headers_ += request_->extra_headers; @@ -209,12 +243,7 @@ void HttpNetworkTransaction::BuildRequestHeaders() { // in draft-luotonen-web-proxy-tunneling-01.txt and RFC 2817, Sections 5.2 and // 5.3. void HttpNetworkTransaction::BuildTunnelRequest() { - std::string port; - if (request_->url.has_port()) { - port = request_->url.port(); - } else { - port = "443"; - } + std::string port = GetImplicitPort(request_->url); // RFC 2616 Section 9 says the Host request-header field MUST accompany all // HTTP/1.1 requests. @@ -227,8 +256,7 @@ void HttpNetworkTransaction::BuildTunnelRequest() { if (!request_->user_agent.empty()) request_headers_ += "User-Agent: " + request_->user_agent + "\r\n"; - // TODO(wtc): Add the Proxy-Authorization header, if necessary, to handle - // proxy authentication. + ApplyAuth(); request_headers_ += "\r\n"; } @@ -765,10 +793,13 @@ int HttpNetworkTransaction::DidReadResponseHeaders() { return OK; } - // TODO(wtc): Handle 401 server or 407 proxy authentication challenge. response_.headers = headers; response_.vary_data.Init(*request_, *response_.headers); + int rv = PopulateAuthChallenge(); + if (rv != OK) + return rv; + // Figure how to determine EOF: // For certain responses, we know the content length is always 0. @@ -858,6 +889,21 @@ int HttpNetworkTransaction::HandleIOError(int error) { return error; } +void HttpNetworkTransaction::ResetStateForRestart() { + header_buf_.reset(); + header_buf_capacity_ = 0; + header_buf_len_ = 0; + header_buf_body_offset_ = -1; + header_buf_http_offset_ = -1; + content_length_ = -1; + content_read_ = 0; + read_buf_ = NULL; + read_buf_len_ = 0; + request_headers_.clear(); + request_headers_bytes_sent_ = 0; + chunked_decoder_.reset(); +} + bool HttpNetworkTransaction::ShouldResendRequest() { // NOTE: we resend a request only if we reused a keep-alive connection. // This automatically prevents an infinite resend loop because we'll run @@ -876,5 +922,95 @@ bool HttpNetworkTransaction::ShouldResendRequest() { return true; } -} // namespace net +void HttpNetworkTransaction::AddAuthorizationHeader(HttpAuth::Target target) { + DCHECK(HaveAuth(target)); + DCHECK(!auth_cache_key_[target].empty()); + + // Add auth data to cache + session_->auth_cache()->Add(auth_cache_key_[target], auth_data_[target]); + + // Add a Authorization/Proxy-Authorization header line. + std::string credentials = auth_handler_[target]->GenerateCredentials( + auth_data_[target]->username, + auth_data_[target]->password, + request_, + &proxy_info_); + request_headers_ += HttpAuth::GetAuthorizationHeaderName(target) + + ": " + credentials + "\r\n"; +} + +void HttpNetworkTransaction::ApplyAuth() { + // We expect using_proxy_ and using_tunnel_ to be mutually exclusive. + DCHECK(!using_proxy_ || !using_tunnel_); + + // Don't send proxy auth after tunnel has been established. + bool should_apply_proxy_auth = using_proxy_ || establishing_tunnel_; + + // Don't send origin server auth while establishing tunnel. + bool should_apply_server_auth = !establishing_tunnel_; + + if (should_apply_proxy_auth && HaveAuth(HttpAuth::AUTH_PROXY)) + AddAuthorizationHeader(HttpAuth::AUTH_PROXY); + if (should_apply_server_auth && HaveAuth(HttpAuth::AUTH_SERVER)) + AddAuthorizationHeader(HttpAuth::AUTH_SERVER); +} + +// Populates response_.auth_challenge with the authentication challenge info. +// This info is consumed by URLRequestHttpJob::GetAuthChallengeInfo(). +int HttpNetworkTransaction::PopulateAuthChallenge() { + DCHECK(response_.headers); + + int status = response_.headers->response_code(); + if (status != 401 && status != 407) + return OK; + HttpAuth::Target target = status == 407 ? + HttpAuth::AUTH_PROXY : HttpAuth::AUTH_SERVER; + + if (target == HttpAuth::AUTH_PROXY && proxy_info_.is_direct()) { + // TODO(eroman): Add a unique error code. + return ERR_INVALID_RESPONSE; + } + + // Find the best authentication challenge that we support. + scoped_ptr<HttpAuthHandler> auth_handler( + HttpAuth::ChooseBestChallenge(response_.headers.get(), target)); + // We found no supported challenge -- let the transaction continue + // so we end up displaying the error page. + if (!auth_handler.get()) + return OK; + + // Construct an AuthChallengeInfo. + scoped_refptr<AuthChallengeInfo> auth_info = new AuthChallengeInfo; + auth_info->is_proxy = target == HttpAuth::AUTH_PROXY; + auth_info->scheme = ASCIIToWide(auth_handler->scheme()); + // TODO(eroman): decode realm according to RFC 2047. + auth_info->realm = ASCIIToWide(auth_handler->realm()); + if (target == HttpAuth::AUTH_PROXY) { + auth_info->host = ASCIIToWide(proxy_info_.proxy_server()); + } else { + DCHECK(target == HttpAuth::AUTH_SERVER); + auth_info->host = ASCIIToWide(request_->url.host()); + } + + // Update the auth cache key and remove any data in the auth cache. + if (!auth_data_[target]) + auth_data_[target] = new AuthData; + auth_cache_key_[target] = AuthCache::HttpKey(request_->url, *auth_info); + DCHECK(!auth_cache_key_[target].empty()); + auth_data_[target]->scheme = auth_info->scheme; + if (auth_data_[target]->state == AUTH_STATE_HAVE_AUTH) { + // The cached identity probably isn't valid so remove it. + // The assumption here is that the cached auth data is what we + // just used. + session_->auth_cache()->Remove(auth_cache_key_[target]); + auth_data_[target]->state = AUTH_STATE_NEED_AUTH; + } + + response_.auth_challenge.swap(auth_info); + auth_handler_[target].reset(auth_handler.release()); + + return OK; +} + +} // namespace net diff --git a/net/http/http_network_transaction.h b/net/http/http_network_transaction.h index 5334e43..533d4a2 100644 --- a/net/http/http_network_transaction.h +++ b/net/http/http_network_transaction.h @@ -11,6 +11,8 @@ #include "net/base/address_list.h" #include "net/base/client_socket_handle.h" #include "net/base/host_resolver.h" +#include "net/http/http_auth.h" +#include "net/http/http_auth_handler.h" #include "net/http/http_response_info.h" #include "net/http/http_transaction.h" #include "net/proxy/proxy_service.h" @@ -99,6 +101,49 @@ class HttpNetworkTransaction : public HttpTransaction { return header_buf_http_offset_ != -1; } + // Resets the members of the transaction, to rewinding next_state_. + void ResetStateForRestart(); + + // Attach any credentials needed for the proxy server or origin server. + void ApplyAuth(); + + // Helper used by ApplyAuth(). Adds either the proxy auth header, or the + // origin server auth header, as specified by |target| + void AddAuthorizationHeader(HttpAuth::Target target); + + // Handles HTTP status code 401 or 407. Populates response_.auth_challenge + // with the required information so that URLRequestHttpJob can prompt + // for a username/password. + int PopulateAuthChallenge(); + + bool NeedAuth(HttpAuth::Target target) const { + return auth_data_[target] && + auth_data_[target]->state == AUTH_STATE_NEED_AUTH; + } + + bool HaveAuth(HttpAuth::Target target) const { + return auth_data_[target] && + auth_data_[target]->state == AUTH_STATE_HAVE_AUTH; + } + + // The following three auth members are arrays of size two -- index 0 is + // for the proxy server, and index 1 is for the origin server. + // Use the enum HttpAuth::Target to index into them. + + // auth_handler encapsulates the logic for the particular auth-scheme. + // This includes the challenge's parameters. If NULL, then there is no + // associated auth handler. + scoped_ptr<HttpAuthHandler> auth_handler_[2]; + + // auth_data tracks the identity (username/password) that is to be + // applied to the proxy/origin server. The identify may have come + // from a login prompt, or from the auth cache. It is fed to us + // by URLRequestHttpJob, via RestartWithAuth(). + scoped_refptr<AuthData> auth_data_[2]; + + // The key in the auth cache, for auth_data_. + std::string auth_cache_key_[2]; + CompletionCallbackImpl<HttpNetworkTransaction> io_callback_; CompletionCallback* user_callback_; diff --git a/net/http/http_response_headers_unittest.cc b/net/http/http_response_headers_unittest.cc index 6159e93..df3add4 100644 --- a/net/http/http_response_headers_unittest.cc +++ b/net/http/http_response_headers_unittest.cc @@ -436,6 +436,26 @@ TEST(HttpResponseHeadersTest, EnumerateHeader_Coalesced) { EXPECT_EQ("no-cache=\"set-cookie,server\"", value); EXPECT_TRUE(parsed->EnumerateHeader(&iter, "cache-control", &value)); EXPECT_EQ("no-store", value); + EXPECT_FALSE(parsed->EnumerateHeader(&iter, "cache-control", &value)); +} + +TEST(HttpResponseHeadersTest, EnumerateHeader_Challenge) { + // Even though WWW-Authenticate has commas, it should not be treated as + // coalesced values. + std::string headers = + "HTTP/1.1 401 OK\n" + "WWW-Authenticate:Digest realm=foobar, nonce=x, domain=y\n" + "WWW-Authenticate:Basic realm=quatar\n"; + HeadersToRaw(&headers); + scoped_refptr<HttpResponseHeaders> parsed = new HttpResponseHeaders(headers); + + void* iter = NULL; + std::string value; + EXPECT_TRUE(parsed->EnumerateHeader(&iter, "WWW-Authenticate", &value)); + EXPECT_EQ("Digest realm=foobar, nonce=x, domain=y", value); + EXPECT_TRUE(parsed->EnumerateHeader(&iter, "WWW-Authenticate", &value)); + EXPECT_EQ("Basic realm=quatar", value); + EXPECT_FALSE(parsed->EnumerateHeader(&iter, "WWW-Authenticate", &value)); } TEST(HttpResponseHeadersTest, EnumerateHeader_DateValued) { diff --git a/net/http/http_util.cc b/net/http/http_util.cc index e204e67..d69012c 100644 --- a/net/http/http_util.cc +++ b/net/http/http_util.cc @@ -228,7 +228,11 @@ bool HttpUtil::IsNonCoalescingHeader(string::const_iterator name_begin, "last-modified", "location", // See bug 1050541 for details "retry-after", - "set-cookie" + "set-cookie", + // The format of auth-challenges mixes both space separated tokens and + // comma separated properties, so coalescing on comma won't work. + "www-authenticate", + "proxy-authenticate" }; for (size_t i = 0; i < arraysize(kNonCoalescingHeaders); ++i) { if (LowerCaseEqualsASCII(name_begin, name_end, kNonCoalescingHeaders[i])) @@ -252,6 +256,73 @@ void HttpUtil::TrimLWS(string::const_iterator* begin, --(*end); } +// static +bool HttpUtil::IsQuote(char c) { + // Single quote mark isn't actually part of quoted-text production, + // but apparently some servers rely on this. + return c == '"' || c == '\''; +} + +// static +std::string HttpUtil::Unquote(std::string::const_iterator begin, + std::string::const_iterator end) { + // Empty string + if (begin == end) + return std::string(); + + // Nothing to unquote. + if (!IsQuote(*begin)) + return std::string(begin, end); + + // No terminal quote mark. + if (end - begin < 2 || *begin != *(end - 1)) + return std::string(begin, end); + + // Strip quotemarks + ++begin; + --end; + + // Unescape quoted-pair (defined in RFC 2616 section 2.2) + std::string unescaped; + bool prev_escape = false; + for (; begin != end; ++begin) { + char c = *begin; + if (c == '\\' && !prev_escape) { + prev_escape = true; + continue; + } + prev_escape = false; + unescaped.push_back(c); + } + return unescaped; +} + +// static +std::string HttpUtil::Unquote(const std::string& str) { + return Unquote(str.begin(), str.end()); +} + +// static +std::string HttpUtil::Quote(const std::string& str) { + std::string escaped; + escaped.reserve(2 + str.size()); + + std::string::const_iterator begin = str.begin(); + std::string::const_iterator end = str.end(); + + // Esape any backslashes or quotemarks within the string, and + // then surround with quotes. + escaped.push_back('"'); + for (; begin != end; ++begin) { + char c = *begin; + if (c == '"' || c == '\\') + escaped.push_back('\\'); + escaped.push_back(c); + } + escaped.push_back('"'); + return escaped; +} + // Find the "http" substring in a status line. This allows for // some slop at the start. If the "http" string could not be found // then returns -1. diff --git a/net/http/http_util.h b/net/http/http_util.h index 5262070..d46ace4 100644 --- a/net/http/http_util.h +++ b/net/http/http_util.h @@ -65,6 +65,23 @@ class HttpUtil { static void TrimLWS(std::string::const_iterator* begin, std::string::const_iterator* end); + // Whether the character is the start of a quotation mark. + static bool IsQuote(char c); + + // RFC 2616 Sec 2.2: + // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + // Unquote() strips the surrounding quotemarks off a string, and unescapes + // any quoted-pair to obtain the value contained by the quoted-string. + // If the input is not quoted, then it works like the identity function. + static std::string Unquote(std::string::const_iterator begin, + std::string::const_iterator end); + + // Same as above. + static std::string Unquote(const std::string& str); + + // The reverse of Unquote() -- escapes and surrounds with " + static std::string Quote(const std::string& str); + // Returns the start of the status line, or -1 if no status line was found. // This allows for 4 bytes of junk to precede the status line (which is what // mozilla does too). diff --git a/net/http/http_util_unittest.cc b/net/http/http_util_unittest.cc index 8cccf31..4b27a0a 100644 --- a/net/http/http_util_unittest.cc +++ b/net/http/http_util_unittest.cc @@ -95,6 +95,37 @@ TEST(HttpUtilTest, ValuesIterator_Blanks) { EXPECT_FALSE(it.GetNext()); } +TEST(HttpUtilTest, Unquote) { + // Replace <backslash> " with ". + EXPECT_STREQ("xyz\"abc", HttpUtil::Unquote("\"xyz\\\"abc\"").c_str()); + + // Replace <backslash> <backslash> with <backslash> + EXPECT_STREQ("xyz\\abc", HttpUtil::Unquote("\"xyz\\\\abc\"").c_str()); + EXPECT_STREQ("xyz\\\\\\abc", + HttpUtil::Unquote("\"xyz\\\\\\\\\\\\abc\"").c_str()); + + // Replace <backslash> X with X + EXPECT_STREQ("xyzXabc", HttpUtil::Unquote("\"xyz\\Xabc\"").c_str()); + + // Act as identity function on unquoted inputs. + EXPECT_STREQ("X", HttpUtil::Unquote("X").c_str()); + EXPECT_STREQ("\"", HttpUtil::Unquote("\"").c_str()); + + // Allow single quotes to act as quote marks. + // Not part of RFC 2616. + EXPECT_STREQ("x\"", HttpUtil::Unquote("'x\"'").c_str()); +} + +TEST(HttpUtilTest, Quote) { + EXPECT_STREQ("\"xyz\\\"abc\"", HttpUtil::Quote("xyz\"abc").c_str()); + + // Replace <backslash> <backslash> with <backslash> + EXPECT_STREQ("\"xyz\\\\abc\"", HttpUtil::Quote("xyz\\abc").c_str()); + + // Replace <backslash> X with X + EXPECT_STREQ("\"xyzXabc\"", HttpUtil::Quote("xyzXabc").c_str()); +} + TEST(HttpUtilTest, LocateEndOfHeaders) { struct { const char* input; |