diff options
Diffstat (limited to 'net/http')
-rw-r--r-- | net/http/http_auth.cc | 79 | ||||
-rw-r--r-- | net/http/http_auth.h | 75 | ||||
-rw-r--r-- | net/http/http_auth_gssapi_posix.cc | 9 | ||||
-rw-r--r-- | net/http/http_auth_handler_basic.cc | 13 | ||||
-rw-r--r-- | net/http/http_auth_handler_digest.cc | 25 | ||||
-rw-r--r-- | net/http/http_auth_handler_factory.cc | 5 | ||||
-rw-r--r-- | net/http/http_auth_handler_mock.cc | 3 | ||||
-rw-r--r-- | net/http/http_auth_handler_ntlm.cc | 10 | ||||
-rw-r--r-- | net/http/http_auth_sspi_win.cc | 8 | ||||
-rw-r--r-- | net/http/http_auth_unittest.cc | 193 | ||||
-rw-r--r-- | net/http/http_util.cc | 73 | ||||
-rw-r--r-- | net/http/http_util.h | 55 | ||||
-rw-r--r-- | net/http/http_util_unittest.cc | 133 |
13 files changed, 434 insertions, 247 deletions
diff --git a/net/http/http_auth.cc b/net/http/http_auth.cc index debcca7..fe7005a 100644 --- a/net/http/http_auth.cc +++ b/net/http/http_auth.cc @@ -88,7 +88,7 @@ void HttpAuth::ChallengeTokenizer::Init(std::string::const_iterator begin, // is separated by 1*SP. StringTokenizer tok(begin, end, HTTP_LWS); if (!tok.GetNext()) { - valid_ = false; + // Default param and scheme iterators provide empty strings return; } @@ -96,70 +96,27 @@ void HttpAuth::ChallengeTokenizer::Init(std::string::const_iterator begin, scheme_begin_ = tok.token_begin(); scheme_end_ = tok.token_end(); - // Everything past scheme_end_ is a (comma separated) value list. - props_ = HttpUtil::ValuesIterator(scheme_end_, end, ','); + params_begin_ = scheme_end_; + params_end_ = end; + HttpUtil::TrimLWS(¶ms_begin_, ¶ms_end_); } -// We expect properties to be formatted as one of: -// name="value" -// name=value -// name= -// Due to buggy implementations found in some embedded devices, we also -// accept values with missing close quotemark (http://crbug.com/39836): -// name="value -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_; - - if (expect_base64_token_) { - expect_base64_token_ = false; - // Strip off any padding. - // (See https://bugzilla.mozilla.org/show_bug.cgi?id=230351.) - // - // Our base64 decoder requires that the length be a multiple of 4. - int encoded_length = value_end_ - value_begin_; - while (encoded_length > 0 && encoded_length % 4 != 0 && - value_begin_[encoded_length - 1] == '=') { - --encoded_length; - --value_end_; - } - return true; - } - - // 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; - - value_is_quoted_ = false; - if (value_begin_ != value_end_ && HttpUtil::IsQuote(*value_begin_)) { - // Trim surrounding quotemarks off the value - if (*value_begin_ != *(value_end_ - 1) || value_begin_ + 1 == value_end_) - value_begin_ = equals + 2; // Gracefully recover from mismatching quotes. - else - value_is_quoted_ = true; - } - return true; +HttpUtil::NameValuePairsIterator HttpAuth::ChallengeTokenizer::param_pairs() + const { + return HttpUtil::NameValuePairsIterator(params_begin_, params_end_, ','); } -// If value() has quotemarks, unquote it. -std::string HttpAuth::ChallengeTokenizer::unquoted_value() const { - return HttpUtil::Unquote(value_begin_, value_end_); +std::string HttpAuth::ChallengeTokenizer::base64_param() const { + // Strip off any padding. + // (See https://bugzilla.mozilla.org/show_bug.cgi?id=230351.) + // + // Our base64 decoder requires that the length be a multiple of 4. + int encoded_length = params_end_ - params_begin_; + while (encoded_length > 0 && encoded_length % 4 != 0 && + params_begin_[encoded_length - 1] == '=') { + --encoded_length; + } + return std::string(params_begin_, params_begin_ + encoded_length); } // static diff --git a/net/http/http_auth.h b/net/http/http_auth.h index dc78f0c..a996304 100644 --- a/net/http/http_auth.h +++ b/net/http/http_auth.h @@ -129,23 +129,24 @@ class HttpAuth { const std::set<std::string>& disabled_schemes, std::string* challenge_used); - // ChallengeTokenizer breaks up a challenge string into the the auth scheme - // and parameter list, according to RFC 2617 Sec 1.2: + // 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. + // Depending on the challenge scheme, it may be appropriate to interpret the + // parameters as either a base-64 encoded string or a comma-delimited list + // of name-value pairs. param_pairs() and base64_param() methods are provided + // to support either usage. class ChallengeTokenizer { public: ChallengeTokenizer(std::string::const_iterator begin, std::string::const_iterator end) - : props_(begin, end, ','), - valid_(true), - begin_(begin), + : begin_(begin), end_(end), - value_is_quoted_(false), - expect_base64_token_(false) { + scheme_begin_(begin), + scheme_end_(begin), + params_begin_(end), + params_end_(end) { Init(begin, end); } @@ -161,67 +162,21 @@ class HttpAuth { 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(); - - // Inform the tokenizer whether the next token should be treated as a base64 - // encoded value. If |expect_base64_token| is true, |GetNext| will treat the - // next token as a base64 encoded value, and will include the trailing '=' - // padding rather than attempt to split the token into a name/value pair. - // In this case, |name| will be empty, and |value| will contain the token. - // Subsequent calls to |GetNext()| will not treat the token like a base64 - // encoded token unless the caller again calls |set_expect_base64_token|. - void set_expect_base64_token(bool expect_base64_token) { - expect_base64_token_ = expect_base64_token; - } - - // 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_; } + HttpUtil::NameValuePairsIterator param_pairs() const; + std::string base64_param() const; private: void Init(std::string::const_iterator begin, std::string::const_iterator end); - HttpUtil::ValuesIterator props_; - bool valid_; - std::string::const_iterator begin_; std::string::const_iterator end_; 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_; - bool expect_base64_token_; + std::string::const_iterator params_begin_; + std::string::const_iterator params_end_; }; }; diff --git a/net/http/http_auth_gssapi_posix.cc b/net/http/http_auth_gssapi_posix.cc index fdbef63..674549a 100644 --- a/net/http/http_auth_gssapi_posix.cc +++ b/net/http/http_auth_gssapi_posix.cc @@ -668,12 +668,12 @@ void HttpAuthGSSAPI::Delegate() { HttpAuth::AuthorizationResult HttpAuthGSSAPI::ParseChallenge( HttpAuth::ChallengeTokenizer* tok) { // Verify the challenge's auth-scheme. - if (!tok->valid() || - !LowerCaseEqualsASCII(tok->scheme(), StringToLowerASCII(scheme_).c_str())) + if (!LowerCaseEqualsASCII(tok->scheme(), StringToLowerASCII(scheme_).c_str())) return HttpAuth::AUTHORIZATION_RESULT_INVALID; - tok->set_expect_base64_token(true); - if (!tok->GetNext()) { + std::string encoded_auth_token = tok->base64_param(); + + if (encoded_auth_token.empty()) { // If a context has already been established, an empty Negotiate challenge // should be treated as a rejection of the current attempt. if (scoped_sec_context_.get() != GSS_C_NO_CONTEXT) @@ -688,7 +688,6 @@ HttpAuth::AuthorizationResult HttpAuthGSSAPI::ParseChallenge( } // Make sure the additional token is base64 encoded. - std::string encoded_auth_token = tok->value(); std::string decoded_auth_token; bool base64_rv = base::Base64Decode(encoded_auth_token, &decoded_auth_token); if (!base64_rv) diff --git a/net/http/http_auth_handler_basic.cc b/net/http/http_auth_handler_basic.cc index 355ce8b..054357a 100644 --- a/net/http/http_auth_handler_basic.cc +++ b/net/http/http_auth_handler_basic.cc @@ -32,18 +32,19 @@ bool HttpAuthHandlerBasic::Init(HttpAuth::ChallengeTokenizer* challenge) { bool HttpAuthHandlerBasic::ParseChallenge( HttpAuth::ChallengeTokenizer* challenge) { // Verify the challenge's auth-scheme. - if (!challenge->valid() || - !LowerCaseEqualsASCII(challenge->scheme(), "basic")) + if (!LowerCaseEqualsASCII(challenge->scheme(), "basic")) return false; + HttpUtil::NameValuePairsIterator parameters = challenge->param_pairs(); + // Extract the realm (may be missing). std::string realm; - while (challenge->GetNext()) { - if (LowerCaseEqualsASCII(challenge->name(), "realm")) - realm = challenge->unquoted_value(); + while (parameters.GetNext()) { + if (LowerCaseEqualsASCII(parameters.name(), "realm")) + realm = parameters.unquoted_value(); } - if (!challenge->valid()) + if (!parameters.valid()) return false; realm_ = realm; diff --git a/net/http/http_auth_handler_digest.cc b/net/http/http_auth_handler_digest.cc index 7c14a47..a802213 100644 --- a/net/http/http_auth_handler_digest.cc +++ b/net/http/http_auth_handler_digest.cc @@ -212,15 +212,16 @@ HttpAuth::AuthorizationResult HttpAuthHandlerDigest::HandleAnotherChallenge( // to differentiate between stale and rejected responses. // Note that the state of the current handler is not mutated - this way if // there is a rejection the realm hasn't changed. - if (!challenge->valid() || - !LowerCaseEqualsASCII(challenge->scheme(), "digest")) + if (!LowerCaseEqualsASCII(challenge->scheme(), "digest")) return HttpAuth::AUTHORIZATION_RESULT_INVALID; + HttpUtil::NameValuePairsIterator parameters = challenge->param_pairs(); + // Try to find the "stale" value. - while (challenge->GetNext()) { - if (!LowerCaseEqualsASCII(challenge->name(), "stale")) + while (parameters.GetNext()) { + if (!LowerCaseEqualsASCII(parameters.name(), "stale")) continue; - if (LowerCaseEqualsASCII(challenge->unquoted_value(), "true")) + if (LowerCaseEqualsASCII(parameters.unquoted_value(), "true")) return HttpAuth::AUTHORIZATION_RESULT_STALE; } @@ -258,24 +259,26 @@ bool HttpAuthHandlerDigest::ParseChallenge( realm_ = nonce_ = domain_ = opaque_ = std::string(); // FAIL -- Couldn't match auth-scheme. - if (!challenge->valid() || - !LowerCaseEqualsASCII(challenge->scheme(), "digest")) + if (!LowerCaseEqualsASCII(challenge->scheme(), "digest")) return false; + HttpUtil::NameValuePairsIterator parameters = challenge->param_pairs(); + // Loop through all the properties. - while (challenge->GetNext()) { - if (challenge->value().empty()) { + while (parameters.GetNext()) { + if (parameters.value().empty()) { DLOG(INFO) << "Invalid digest property"; return false; } // FAIL -- couldn't parse a property. - if (!ParseChallengeProperty(challenge->name(), challenge->unquoted_value())) + if (!ParseChallengeProperty(parameters.name(), + parameters.unquoted_value())) return false; } // Check if tokenizer failed. - if (!challenge->valid()) + if (!parameters.valid()) return false; // Check that a minimum set of properties were provided. diff --git a/net/http/http_auth_handler_factory.cc b/net/http/http_auth_handler_factory.cc index 4ad7fe8..e2e3e6a 100644 --- a/net/http/http_auth_handler_factory.cc +++ b/net/http/http_auth_handler_factory.cc @@ -141,11 +141,12 @@ int HttpAuthHandlerRegistryFactory::CreateAuthHandler( int digest_nonce_count, const BoundNetLog& net_log, scoped_ptr<HttpAuthHandler>* handler) { - if (!challenge->valid()) { + std::string scheme = challenge->scheme(); + if (scheme.empty()) { handler->reset(); return ERR_INVALID_RESPONSE; } - std::string lower_scheme = StringToLowerASCII(challenge->scheme()); + std::string lower_scheme = StringToLowerASCII(scheme); FactoryMap::iterator it = factory_map_.find(lower_scheme); if (it == factory_map_.end()) { handler->reset(); diff --git a/net/http/http_auth_handler_mock.cc b/net/http/http_auth_handler_mock.cc index 90602d0..d1e8bb2 100644 --- a/net/http/http_auth_handler_mock.cc +++ b/net/http/http_auth_handler_mock.cc @@ -82,8 +82,7 @@ HttpAuth::AuthorizationResult HttpAuthHandlerMock::HandleAnotherChallenge( HttpAuth::ChallengeTokenizer* challenge) { if (!is_connection_based()) return HttpAuth::AUTHORIZATION_RESULT_REJECT; - if (!challenge->valid() || - !LowerCaseEqualsASCII(challenge->scheme(), "mock")) + if (!LowerCaseEqualsASCII(challenge->scheme(), "mock")) return HttpAuth::AUTHORIZATION_RESULT_INVALID; return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; } diff --git a/net/http/http_auth_handler_ntlm.cc b/net/http/http_auth_handler_ntlm.cc index dddddb4..352b2ed 100644 --- a/net/http/http_auth_handler_ntlm.cc +++ b/net/http/http_auth_handler_ntlm.cc @@ -116,14 +116,16 @@ HttpAuth::AuthorizationResult HttpAuthHandlerNTLM::ParseChallenge( // TODO(cbentzel): Most of the logic between SSPI, GSSAPI, and portable NTLM // authentication parsing could probably be shared - just need to know if // there was previously a challenge round. + // TODO(cbentzel): Write a test case to validate that auth_data_ is left empty + // in all failure conditions. auth_data_.clear(); // Verify the challenge's auth-scheme. - if (!tok->valid() || !LowerCaseEqualsASCII(tok->scheme(), "ntlm")) + if (!LowerCaseEqualsASCII(tok->scheme(), "ntlm")) return HttpAuth::AUTHORIZATION_RESULT_INVALID; - tok->set_expect_base64_token(true); - if (!tok->GetNext()) { + std::string base64_param = tok->base64_param(); + if (base64_param.empty()) { if (!initial_challenge) return HttpAuth::AUTHORIZATION_RESULT_REJECT; return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; @@ -132,7 +134,7 @@ HttpAuth::AuthorizationResult HttpAuthHandlerNTLM::ParseChallenge( return HttpAuth::AUTHORIZATION_RESULT_INVALID; } - auth_data_.assign(tok->value_begin(), tok->value_end()); + auth_data_ = base64_param; return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; #endif // defined(NTLM_SSPI) } diff --git a/net/http/http_auth_sspi_win.cc b/net/http/http_auth_sspi_win.cc index 276eea4..65281e0 100644 --- a/net/http/http_auth_sspi_win.cc +++ b/net/http/http_auth_sspi_win.cc @@ -222,12 +222,11 @@ void HttpAuthSSPI::ResetSecurityContext() { HttpAuth::AuthorizationResult HttpAuthSSPI::ParseChallenge( HttpAuth::ChallengeTokenizer* tok) { // Verify the challenge's auth-scheme. - if (!tok->valid() || - !LowerCaseEqualsASCII(tok->scheme(), StringToLowerASCII(scheme_).c_str())) + if (!LowerCaseEqualsASCII(tok->scheme(), StringToLowerASCII(scheme_).c_str())) return HttpAuth::AUTHORIZATION_RESULT_INVALID; - tok->set_expect_base64_token(true); - if (!tok->GetNext()) { + std::string encoded_auth_token = tok->base64_param(); + if (encoded_auth_token.empty()) { // If a context has already been established, an empty challenge // should be treated as a rejection of the current attempt. if (SecIsValidHandle(&ctxt_)) @@ -241,7 +240,6 @@ HttpAuth::AuthorizationResult HttpAuthSSPI::ParseChallenge( return HttpAuth::AUTHORIZATION_RESULT_INVALID; } - std::string encoded_auth_token = tok->value(); std::string decoded_auth_token; bool base64_rv = base::Base64Decode(encoded_auth_token, &decoded_auth_token); if (!base64_rv) diff --git a/net/http/http_auth_unittest.cc b/net/http/http_auth_unittest.cc index cc62214..913e1cc 100644 --- a/net/http/http_auth_unittest.cc +++ b/net/http/http_auth_unittest.cc @@ -188,15 +188,17 @@ TEST(HttpAuthTest, ChallengeTokenizer) { std::string challenge_str = "Basic realm=\"foobar\""; HttpAuth::ChallengeTokenizer challenge(challenge_str.begin(), challenge_str.end()); - EXPECT_TRUE(challenge.valid()); + HttpUtil::NameValuePairsIterator parameters = challenge.param_pairs(); + + EXPECT_TRUE(parameters.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()); + EXPECT_TRUE(parameters.GetNext()); + EXPECT_TRUE(parameters.valid()); + EXPECT_EQ(std::string("realm"), parameters.name()); + EXPECT_EQ(std::string("foobar"), parameters.unquoted_value()); + EXPECT_EQ(std::string("\"foobar\""), parameters.value()); + EXPECT_TRUE(parameters.value_is_quoted()); + EXPECT_FALSE(parameters.GetNext()); } // Use a name=value property with no quote marks. @@ -204,15 +206,17 @@ 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()); + HttpUtil::NameValuePairsIterator parameters = challenge.param_pairs(); + + EXPECT_TRUE(parameters.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()); + EXPECT_TRUE(parameters.GetNext()); + EXPECT_TRUE(parameters.valid()); + EXPECT_EQ(std::string("realm"), parameters.name()); + EXPECT_EQ(std::string("foobar@baz.com"), parameters.value()); + EXPECT_EQ(std::string("foobar@baz.com"), parameters.unquoted_value()); + EXPECT_FALSE(parameters.value_is_quoted()); + EXPECT_FALSE(parameters.GetNext()); } // Use a name=value property with mismatching quote marks. @@ -220,15 +224,17 @@ TEST(HttpAuthTest, ChallengeTokenizerMismatchedQuotes) { std::string challenge_str = "Basic realm=\"foobar@baz.com"; HttpAuth::ChallengeTokenizer challenge(challenge_str.begin(), challenge_str.end()); - EXPECT_TRUE(challenge.valid()); + HttpUtil::NameValuePairsIterator parameters = challenge.param_pairs(); + + EXPECT_TRUE(parameters.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()); + EXPECT_TRUE(parameters.GetNext()); + EXPECT_TRUE(parameters.valid()); + EXPECT_EQ(std::string("realm"), parameters.name()); + EXPECT_EQ(std::string("foobar@baz.com"), parameters.value()); + EXPECT_EQ(std::string("foobar@baz.com"), parameters.unquoted_value()); + EXPECT_FALSE(parameters.value_is_quoted()); + EXPECT_FALSE(parameters.GetNext()); } // Use a name= property without a value and with mismatching quote marks. @@ -236,14 +242,16 @@ TEST(HttpAuthTest, ChallengeTokenizerMismatchedQuotesNoValue) { std::string challenge_str = "Basic realm=\""; HttpAuth::ChallengeTokenizer challenge(challenge_str.begin(), challenge_str.end()); - EXPECT_TRUE(challenge.valid()); + HttpUtil::NameValuePairsIterator parameters = challenge.param_pairs(); + + EXPECT_TRUE(parameters.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(""), challenge.value()); - EXPECT_FALSE(challenge.value_is_quoted()); - EXPECT_FALSE(challenge.GetNext()); + EXPECT_TRUE(parameters.GetNext()); + EXPECT_TRUE(parameters.valid()); + EXPECT_EQ(std::string("realm"), parameters.name()); + EXPECT_EQ(std::string(""), parameters.value()); + EXPECT_FALSE(parameters.value_is_quoted()); + EXPECT_FALSE(parameters.GetNext()); } // Use a name=value property with mismatching quote marks and spaces in the @@ -252,15 +260,17 @@ TEST(HttpAuthTest, ChallengeTokenizerMismatchedQuotesSpaces) { std::string challenge_str = "Basic realm=\"foo bar"; HttpAuth::ChallengeTokenizer challenge(challenge_str.begin(), challenge_str.end()); - EXPECT_TRUE(challenge.valid()); + HttpUtil::NameValuePairsIterator parameters = challenge.param_pairs(); + + EXPECT_TRUE(parameters.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("foo bar"), challenge.value()); - EXPECT_EQ(std::string("foo bar"), challenge.unquoted_value()); - EXPECT_FALSE(challenge.value_is_quoted()); - EXPECT_FALSE(challenge.GetNext()); + EXPECT_TRUE(parameters.GetNext()); + EXPECT_TRUE(parameters.valid()); + EXPECT_EQ(std::string("realm"), parameters.name()); + EXPECT_EQ(std::string("foo bar"), parameters.value()); + EXPECT_EQ(std::string("foo bar"), parameters.unquoted_value()); + EXPECT_FALSE(parameters.value_is_quoted()); + EXPECT_FALSE(parameters.GetNext()); } // Use multiple name=value properties with mismatching quote marks in the last @@ -269,26 +279,28 @@ TEST(HttpAuthTest, ChallengeTokenizerMismatchedQuotesMultiple) { std::string challenge_str = "Digest qop=, algorithm=md5, realm=\"foo"; HttpAuth::ChallengeTokenizer challenge(challenge_str.begin(), challenge_str.end()); - EXPECT_TRUE(challenge.valid()); + HttpUtil::NameValuePairsIterator parameters = challenge.param_pairs(); + + EXPECT_TRUE(parameters.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_TRUE(challenge.GetNext()); - EXPECT_TRUE(challenge.valid()); - EXPECT_EQ(std::string("algorithm"), challenge.name()); - EXPECT_EQ(std::string("md5"), challenge.value()); - EXPECT_EQ(std::string("md5"), challenge.unquoted_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("foo"), challenge.value()); - EXPECT_EQ(std::string("foo"), challenge.unquoted_value()); - EXPECT_FALSE(challenge.value_is_quoted()); - EXPECT_FALSE(challenge.GetNext()); + EXPECT_TRUE(parameters.GetNext()); + EXPECT_TRUE(parameters.valid()); + EXPECT_EQ(std::string("qop"), parameters.name()); + EXPECT_EQ(std::string(""), parameters.value()); + EXPECT_FALSE(parameters.value_is_quoted()); + EXPECT_TRUE(parameters.GetNext()); + EXPECT_TRUE(parameters.valid()); + EXPECT_EQ(std::string("algorithm"), parameters.name()); + EXPECT_EQ(std::string("md5"), parameters.value()); + EXPECT_EQ(std::string("md5"), parameters.unquoted_value()); + EXPECT_FALSE(parameters.value_is_quoted()); + EXPECT_TRUE(parameters.GetNext()); + EXPECT_TRUE(parameters.valid()); + EXPECT_EQ(std::string("realm"), parameters.name()); + EXPECT_EQ(std::string("foo"), parameters.value()); + EXPECT_EQ(std::string("foo"), parameters.unquoted_value()); + EXPECT_FALSE(parameters.value_is_quoted()); + EXPECT_FALSE(parameters.GetNext()); } // Use a name= property which has no value. @@ -296,14 +308,16 @@ TEST(HttpAuthTest, ChallengeTokenizerNoValue) { std::string challenge_str = "Digest qop="; HttpAuth::ChallengeTokenizer challenge( challenge_str.begin(), challenge_str.end()); - EXPECT_TRUE(challenge.valid()); + HttpUtil::NameValuePairsIterator parameters = challenge.param_pairs(); + + EXPECT_TRUE(parameters.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()); + EXPECT_TRUE(parameters.GetNext()); + EXPECT_TRUE(parameters.valid()); + EXPECT_EQ(std::string("qop"), parameters.name()); + EXPECT_EQ(std::string(""), parameters.value()); + EXPECT_FALSE(parameters.value_is_quoted()); + EXPECT_FALSE(parameters.GetNext()); } // Specify multiple properties, comma separated. @@ -312,24 +326,26 @@ TEST(HttpAuthTest, ChallengeTokenizerMultiple) { "Digest algorithm=md5, realm=\"Oblivion\", qop=auth-int"; HttpAuth::ChallengeTokenizer challenge(challenge_str.begin(), challenge_str.end()); - EXPECT_TRUE(challenge.valid()); + HttpUtil::NameValuePairsIterator parameters = challenge.param_pairs(); + + EXPECT_TRUE(parameters.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()); + EXPECT_TRUE(parameters.GetNext()); + EXPECT_TRUE(parameters.valid()); + EXPECT_EQ(std::string("algorithm"), parameters.name()); + EXPECT_EQ(std::string("md5"), parameters.value()); + EXPECT_FALSE(parameters.value_is_quoted()); + EXPECT_TRUE(parameters.GetNext()); + EXPECT_TRUE(parameters.valid()); + EXPECT_EQ(std::string("realm"), parameters.name()); + EXPECT_EQ(std::string("Oblivion"), parameters.unquoted_value()); + EXPECT_TRUE(parameters.value_is_quoted()); + EXPECT_TRUE(parameters.GetNext()); + EXPECT_TRUE(parameters.valid()); + EXPECT_EQ(std::string("qop"), parameters.name()); + EXPECT_EQ(std::string("auth-int"), parameters.value()); + EXPECT_FALSE(parameters.value_is_quoted()); + EXPECT_FALSE(parameters.GetNext()); } // Use a challenge which has no property. @@ -337,9 +353,11 @@ TEST(HttpAuthTest, ChallengeTokenizerNoProperty) { std::string challenge_str = "NTLM"; HttpAuth::ChallengeTokenizer challenge( challenge_str.begin(), challenge_str.end()); - EXPECT_TRUE(challenge.valid()); + HttpUtil::NameValuePairsIterator parameters = challenge.param_pairs(); + + EXPECT_TRUE(parameters.valid()); EXPECT_EQ(std::string("NTLM"), challenge.scheme()); - EXPECT_FALSE(challenge.GetNext()); + EXPECT_FALSE(parameters.GetNext()); } // Use a challenge with Base64 encoded token. @@ -347,13 +365,10 @@ TEST(HttpAuthTest, ChallengeTokenizerBase64) { std::string challenge_str = "NTLM SGVsbG8sIFdvcmxkCg==="; HttpAuth::ChallengeTokenizer challenge(challenge_str.begin(), challenge_str.end()); - EXPECT_TRUE(challenge.valid()); + EXPECT_EQ(std::string("NTLM"), challenge.scheme()); - challenge.set_expect_base64_token(true); - EXPECT_TRUE(challenge.GetNext()); // Notice the two equal statements below due to padding removal. - EXPECT_EQ(std::string("SGVsbG8sIFdvcmxkCg=="), challenge.value()); - EXPECT_FALSE(challenge.GetNext()); + EXPECT_EQ(std::string("SGVsbG8sIFdvcmxkCg=="), challenge.base64_param()); } TEST(HttpAuthTest, GetChallengeHeaderName) { diff --git a/net/http/http_util.cc b/net/http/http_util.cc index 1098a06..0f45ccc 100644 --- a/net/http/http_util.cc +++ b/net/http/http_util.cc @@ -702,4 +702,77 @@ bool HttpUtil::ValuesIterator::GetNext() { return false; } +HttpUtil::NameValuePairsIterator::NameValuePairsIterator( + string::const_iterator begin, + string::const_iterator end, + char delimiter) + : props_(begin, end, delimiter), + valid_(true), + begin_(begin), + end_(end), + name_begin_(end), + name_end_(end), + value_begin_(end), + value_end_(end), + value_is_quoted_(false) { +} + +// We expect properties to be formatted as one of: +// name="value" +// name='value' +// name='\'value\'' +// name=value +// name = value +// name= +// Due to buggy implementations found in some embedded devices, we also +// accept values with missing close quotemark (http://crbug.com/39836): +// name="value +bool HttpUtil::NameValuePairsIterator::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; + + TrimLWS(&name_begin_, &name_end_); + TrimLWS(&value_begin_, &value_end_); + value_is_quoted_ = false; + if (value_begin_ != value_end_ && HttpUtil::IsQuote(*value_begin_)) { + // Trim surrounding quotemarks off the value + if (*value_begin_ != *(value_end_ - 1) || value_begin_ + 1 == value_end_) + // NOTE: This is not as graceful as it sounds: + // * quoted-pairs will no longer be unquoted + // (["\"hello] should give ["hello]). + // * Does not detect when the final quote is escaped + // (["value\"] should give [value"]) + ++value_begin_; // Gracefully recover from mismatching quotes. + else + value_is_quoted_ = true; + } + + return true; +} + +// If value() has quotemarks, unquote it. +std::string HttpUtil::NameValuePairsIterator::unquoted_value() const { + return HttpUtil::Unquote(value_begin_, value_end_); +} + } // namespace net diff --git a/net/http/http_util.h b/net/http/http_util.h index 776b876..631790b 100644 --- a/net/http/http_util.h +++ b/net/http/http_util.h @@ -6,6 +6,7 @@ #define NET_HTTP_HTTP_UTIL_H_ #pragma once +#include <string> #include <vector> #include "base/string_tokenizer.h" @@ -214,10 +215,10 @@ class HttpUtil { std::string::const_iterator values_end_; }; - // Used to iterate over deliminated values in a HTTP header. HTTP LWS is + // Iterates over delimited values in an HTTP header. HTTP LWS is // automatically trimmed from the resulting values. // - // When using this class to iterate over response header values, beware that + // When using this class to iterate over response header values, be aware that // for some headers (e.g., Last-Modified), commas are not used as delimiters. // This iterator should be avoided for headers like that which are considered // non-coalescing (see IsNonCoalescingHeader). @@ -251,6 +252,56 @@ class HttpUtil { std::string::const_iterator value_begin_; std::string::const_iterator value_end_; }; + + // Iterates over a delimited sequence of name-value pairs in an HTTP header. + // Each pair consists of a token (the name), an equals sign, and either a + // token or quoted-string (the value). Arbitrary HTTP LWS is permitted outside + // of and between names, values, and delimiters. + class NameValuePairsIterator { + public: + NameValuePairsIterator(std::string::const_iterator begin, + std::string::const_iterator end, + char delimiter); + + // Advances the iterator to the next pair, if any. Returns true if there + // is a next pair. Use name* and value* methods to access the resultant + // value. + bool GetNext(); + + // Returns false if there was a parse error. + bool valid() const { return valid_; } + + // 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: + HttpUtil::ValuesIterator props_; + bool valid_; + + std::string::const_iterator begin_; + std::string::const_iterator 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 diff --git a/net/http/http_util_unittest.cc b/net/http/http_util_unittest.cc index 0c018a2..4ebbd2e 100644 --- a/net/http/http_util_unittest.cc +++ b/net/http/http_util_unittest.cc @@ -671,3 +671,136 @@ TEST(HttpUtilTest, ParseRanges) { } } } + +namespace { + +void CheckNameValuePair(HttpUtil::NameValuePairsIterator* parser, + bool expect_next, + bool expect_valid, + std::string expected_name, + std::string expected_value, + std::string expected_unquoted_value) { + ASSERT_EQ(expect_next, parser->GetNext()); + ASSERT_EQ(expect_valid, parser->valid()); + if (!expect_next || !expect_valid) { + return; + } + ASSERT_EQ(expected_name, std::string(parser->name_begin(), + parser->name_end())); + ASSERT_EQ(expected_name, parser->name()); + ASSERT_EQ(expected_value, std::string(parser->value_begin(), + parser->value_end())); + ASSERT_EQ(expected_value, parser->value()); + ASSERT_EQ(expected_unquoted_value, parser->unquoted_value()); + ASSERT_EQ(expected_value != expected_unquoted_value, + parser->value_is_quoted()); +} + +void CheckInvalidNameValuePair(std::string valid_part, + std::string invalid_part) { + std::string whole_string = valid_part + invalid_part; + + HttpUtil::NameValuePairsIterator valid_parser(valid_part.begin(), + valid_part.end(), + ';'); + HttpUtil::NameValuePairsIterator invalid_parser(whole_string.begin(), + whole_string.end(), + ';'); + + ASSERT_TRUE(valid_parser.valid()); + ASSERT_TRUE(invalid_parser.valid()); + + // Both parsers should return all the same values until "valid_parser" is + // exhausted. + while (valid_parser.GetNext()) { + ASSERT_TRUE(invalid_parser.GetNext()); + ASSERT_TRUE(valid_parser.valid()); + ASSERT_TRUE(invalid_parser.valid()); + ASSERT_EQ(valid_parser.name(), invalid_parser.name()); + ASSERT_EQ(valid_parser.value(), invalid_parser.value()); + } + + // valid_parser is exhausted and remains 'valid' + ASSERT_TRUE(valid_parser.valid()); + + // invalid_parser's corresponding call to GetNext also returns false... + ASSERT_FALSE(invalid_parser.GetNext()); + // ...but the parser is in an invalid state. + ASSERT_FALSE(invalid_parser.valid()); +} + +} // anonymous namespace + +TEST(HttpUtilTest, NameValuePairsIteratorEmptyInput) { + std::string data = ""; + HttpUtil::NameValuePairsIterator parser(data.begin(), data.end(), ';'); + + EXPECT_TRUE(parser.valid()); + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, false, true, "", "", "")); +} + +TEST(HttpUtilTest, NameValuePairsIterator) { + std::string data = "alpha=1; beta= 2 ;cappa =' 3 ';" + "delta= \" 4 \"; e= \" '5'\"; e=6"; + HttpUtil::NameValuePairsIterator parser(data.begin(), data.end(), ';'); + EXPECT_TRUE(parser.valid()); + + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, true, true, "alpha", "1", "1")); + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, true, true, "beta", "2", "2")); + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, true, true, "cappa", "' 3 '", " 3 ")); + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, true, true, "delta", "\" 4 \"", " 4 ")); + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, true, true, "e", "\" '5'\"", " '5'")); + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, true, true, "e", "6", "6")); + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, false, true, "", "", "")); +} + +TEST(HttpUtilTest, NameValuePairsIteratorIllegalInputs) { + ASSERT_NO_FATAL_FAILURE(CheckInvalidNameValuePair("alpha=1", "; beta")); + ASSERT_NO_FATAL_FAILURE(CheckInvalidNameValuePair("", "beta")); + + ASSERT_NO_FATAL_FAILURE(CheckInvalidNameValuePair("alpha=1", "; 'beta'=2")); + ASSERT_NO_FATAL_FAILURE(CheckInvalidNameValuePair("", "'beta'=2")); + + // According to the spec this is an error, but it doesn't seem appropriate to + // change our behaviour to be less permissive at this time. + // See NameValuePairsIteratorExtraSeparators test + // ASSERT_NO_FATAL_FAILURE(CheckInvalidNameValuePair("alpha=1", ";; beta=2")); +} + +// If we are going to support extra separators against the spec, let's just make +// sure they work rationally. +TEST(HttpUtilTest, NameValuePairsIteratorExtraSeparators) { + std::string data = " ; ;;alpha=1; ;; ; beta= 2;cappa=3;;; ; "; + HttpUtil::NameValuePairsIterator parser(data.begin(), data.end(), ';'); + EXPECT_TRUE(parser.valid()); + + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, true, true, "alpha", "1", "1")); + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, true, true, "beta", "2", "2")); + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, true, true, "cappa", "3", "3")); + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, false, true, "", "", "")); +} + +// See comments on the implementation of NameValuePairsIterator::GetNext +// regarding this derogation from the spec. +TEST(HttpUtilTest, NameValuePairsIteratorMissingEndQuote) { + std::string data = "name='value"; + HttpUtil::NameValuePairsIterator parser(data.begin(), data.end(), ';'); + EXPECT_TRUE(parser.valid()); + + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, true, true, "name", "value", "value")); + ASSERT_NO_FATAL_FAILURE( + CheckNameValuePair(&parser, false, true, "", "", "")); +} |