diff options
author | battre@chromium.org <battre@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-08-02 13:37:41 +0000 |
---|---|---|
committer | battre@chromium.org <battre@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-08-02 13:37:41 +0000 |
commit | 64527a5d86547911117883a9a442c21830b37260 (patch) | |
tree | 320bbe3ed9a18ecdd639e93c708e934ddb8d3367 /net | |
parent | 4f05741acc8aca31a1ad7d0a2793cac9cf4e37a6 (diff) | |
download | chromium_src-64527a5d86547911117883a9a442c21830b37260.zip chromium_src-64527a5d86547911117883a9a442c21830b37260.tar.gz chromium_src-64527a5d86547911117883a9a442c21830b37260.tar.bz2 |
Add a mutable version of CookieMonster::ParsedCookie
This CL implements CookieMonster::MutableParsedCookie which is derived from CookieMonster::ParsedCookie. This allows parsing a cookie and modifying it afterwards.
BUG=112155
TEST=no
Review URL: https://chromiumcodereview.appspot.com/10697035
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@149622 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net')
-rw-r--r-- | net/cookies/parsed_cookie.cc | 266 | ||||
-rw-r--r-- | net/cookies/parsed_cookie.h | 54 | ||||
-rw-r--r-- | net/cookies/parsed_cookie_unittest.cc | 138 |
3 files changed, 399 insertions, 59 deletions
diff --git a/net/cookies/parsed_cookie.cc b/net/cookies/parsed_cookie.cc index f9af3c9..704de04 100644 --- a/net/cookies/parsed_cookie.cc +++ b/net/cookies/parsed_cookie.cc @@ -47,11 +47,112 @@ #include "base/logging.h" #include "base/string_util.h" +namespace { + +const char kPathTokenName[] = "path"; +const char kDomainTokenName[] = "domain"; +const char kMACKeyTokenName[] = "mac-key"; +const char kMACAlgorithmTokenName[] = "mac-algorithm"; +const char kExpiresTokenName[] = "expires"; +const char kMaxAgeTokenName[] = "max-age"; +const char kSecureTokenName[] = "secure"; +const char kHttpOnlyTokenName[] = "httponly"; + +const char kTerminator[] = "\n\r\0"; +const int kTerminatorLen = sizeof(kTerminator) - 1; +const char kWhitespace[] = " \t"; +const char kValueSeparator[] = ";"; +const char kTokenSeparator[] = ";="; + +// Returns true if |c| occurs in |chars| +// TODO(erikwright): maybe make this take an iterator, could check for end also? +inline bool CharIsA(const char c, const char* chars) { + return strchr(chars, c) != NULL; +} +// Seek the iterator to the first occurrence of a character in |chars|. +// Returns true if it hit the end, false otherwise. +inline bool SeekTo(std::string::const_iterator* it, + const std::string::const_iterator& end, + const char* chars) { + for (; *it != end && !CharIsA(**it, chars); ++(*it)) {} + return *it == end; +} +// Seek the iterator to the first occurrence of a character not in |chars|. +// Returns true if it hit the end, false otherwise. +inline bool SeekPast(std::string::const_iterator* it, + const std::string::const_iterator& end, + const char* chars) { + for (; *it != end && CharIsA(**it, chars); ++(*it)) {} + return *it == end; +} +inline bool SeekBackPast(std::string::const_iterator* it, + const std::string::const_iterator& end, + const char* chars) { + for (; *it != end && CharIsA(**it, chars); --(*it)) {} + return *it == end; +} + +// Validate whether |value| is a valid token according to [RFC2616], +// Section 2.2. +bool IsValidToken(const std::string& value) { + if (value.empty()) + return false; + + // Check that |value| has no separators. + std::string separators = "()<>@,;:\\\"/[]?={} \t"; + if (value.find_first_of(separators) != std::string::npos) + return false; + + // Check that |value| has no CTLs. + for (std::string::const_iterator i = value.begin(); i != value.end(); ++i) { + if ((*i >= 0 && *i <= 31) || *i >= 127) + return false; + } + + return true; +} + +// Validate value, which may be according to RFC 6265 +// cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) +// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E +// ; US-ASCII characters excluding CTLs, +// ; whitespace DQUOTE, comma, semicolon, +// ; and backslash +bool IsValidCookieValue(const std::string& value) { + // Number of characters to skip in validation at beginning and end of string. + size_t skip = 0; + if (value.size() >= 2 && *value.begin() == '"' && *(value.end()-1) == '"') + skip = 1; + for (std::string::const_iterator i = value.begin() + skip; + i != value.end() - skip; ++i) { + bool valid_octet = + (*i == 0x21 || + (*i >= 0x23 && *i <= 0x2B) || + (*i >= 0x2D && *i <= 0x3A) || + (*i >= 0x3C && *i <= 0x5B) || + (*i >= 0x5D && *i <= 0x7E)); + if (!valid_octet) + return false; + } + return true; +} + +bool IsValidCookieAttributeValue(const std::string& value) { + // The greatest common denominator of cookie attribute values is + // <any CHAR except CTLs or ";"> according to RFC 6265. + for (std::string::const_iterator i = value.begin(); i != value.end(); ++i) { + if ((*i >= 0 && *i <= 31) || *i == ';') + return false; + } + return true; +} + +} // namespace + namespace net { ParsedCookie::ParsedCookie(const std::string& cookie_line) - : is_valid_(false), - path_index_(0), + : path_index_(0), domain_index_(0), mac_key_index_(0), mac_algorithm_index_(0), @@ -66,60 +167,79 @@ ParsedCookie::ParsedCookie(const std::string& cookie_line) } ParseTokenValuePairs(cookie_line); - if (!pairs_.empty()) { - is_valid_ = true; + if (!pairs_.empty()) SetupAttributes(); - } } ParsedCookie::~ParsedCookie() { } -// Returns true if |c| occurs in |chars| -// TODO(erikwright): maybe make this take an iterator, could check for end also? -static inline bool CharIsA(const char c, const char* chars) { - return strchr(chars, c) != NULL; +bool ParsedCookie::IsValid() const { + return !pairs_.empty(); } -// Seek the iterator to the first occurrence of a character in |chars|. -// Returns true if it hit the end, false otherwise. -static inline bool SeekTo(std::string::const_iterator* it, - const std::string::const_iterator& end, - const char* chars) { - for (; *it != end && !CharIsA(**it, chars); ++(*it)) {} - return *it == end; + +bool ParsedCookie::SetName(const std::string& name) { + if (!IsValidToken(name)) + return false; + if (pairs_.empty()) + pairs_.push_back(std::make_pair("", "")); + pairs_[0].first = name; + return true; } -// Seek the iterator to the first occurrence of a character not in |chars|. -// Returns true if it hit the end, false otherwise. -static inline bool SeekPast(std::string::const_iterator* it, - const std::string::const_iterator& end, - const char* chars) { - for (; *it != end && CharIsA(**it, chars); ++(*it)) {} - return *it == end; + +bool ParsedCookie::SetValue(const std::string& value) { + if (!IsValidCookieValue(value)) + return false; + if (pairs_.empty()) + pairs_.push_back(std::make_pair("", "")); + pairs_[0].second = value; + return true; } -static inline bool SeekBackPast(std::string::const_iterator* it, - const std::string::const_iterator& end, - const char* chars) { - for (; *it != end && CharIsA(**it, chars); --(*it)) {} - return *it == end; + +bool ParsedCookie::SetPath(const std::string& path) { + return SetString(&path_index_, kPathTokenName, path); } -const char ParsedCookie::kTerminator[] = "\n\r\0"; -const int ParsedCookie::kTerminatorLen = sizeof(kTerminator) - 1; -const char ParsedCookie::kWhitespace[] = " \t"; -const char ParsedCookie::kValueSeparator[] = ";"; -const char ParsedCookie::kTokenSeparator[] = ";="; +bool ParsedCookie::SetDomain(const std::string& domain) { + return SetString(&domain_index_, kDomainTokenName, domain); +} + +bool ParsedCookie::SetMACKey(const std::string& mac_key) { + return SetString(&mac_key_index_, kMACKeyTokenName, mac_key); +} + +bool ParsedCookie::SetMACAlgorithm(const std::string& mac_algorithm) { + return SetString(&mac_algorithm_index_, kMACAlgorithmTokenName, + mac_algorithm); +} -// Create a cookie-line for the cookie. For debugging only! -// If we want to use this for something more than debugging, we -// should rewrite it better... -std::string ParsedCookie::DebugString() const { +bool ParsedCookie::SetExpires(const std::string& expires) { + return SetString(&expires_index_, kExpiresTokenName, expires); +} + +bool ParsedCookie::SetMaxAge(const std::string& maxage) { + return SetString(&maxage_index_, kMaxAgeTokenName, maxage); +} + +bool ParsedCookie::SetIsSecure(bool is_secure) { + return SetBool(&secure_index_, kSecureTokenName, is_secure); +} + +bool ParsedCookie::SetIsHttpOnly(bool is_http_only) { + return SetBool(&httponly_index_, kHttpOnlyTokenName, is_http_only); +} + +std::string ParsedCookie::ToCookieLine() const { std::string out; for (PairList::const_iterator it = pairs_.begin(); it != pairs_.end(); ++it) { + if (!out.empty()) + out.append("; "); out.append(it->first); - out.append("="); - out.append(it->second); - out.append("; "); + if (it->first != kSecureTokenName && it->first != kHttpOnlyTokenName) { + out.append("="); + out.append(it->second); + } } return out; } @@ -279,15 +399,6 @@ void ParsedCookie::ParseTokenValuePairs(const std::string& cookie_line) { } void ParsedCookie::SetupAttributes() { - static const char kPathTokenName[] = "path"; - static const char kDomainTokenName[] = "domain"; - static const char kMACKeyTokenName[] = "mac-key"; - static const char kMACAlgorithmTokenName[] = "mac-algorithm"; - static const char kExpiresTokenName[] = "expires"; - static const char kMaxAgeTokenName[] = "max-age"; - static const char kSecureTokenName[] = "secure"; - static const char kHttpOnlyTokenName[] = "httponly"; - // We skip over the first token/value, the user supplied one. for (size_t i = 1; i < pairs_.size(); ++i) { if (pairs_[i].first == kPathTokenName) { @@ -312,4 +423,61 @@ void ParsedCookie::SetupAttributes() { } } +bool ParsedCookie::SetString(size_t* index, + const std::string& key, + const std::string& value) { + if (value.empty()) { + ClearAttributePair(*index); + return true; + } else { + return SetAttributePair(index, key, value); + } +} + +bool ParsedCookie::SetBool(size_t* index, + const std::string& key, + bool value) { + if (!value) { + ClearAttributePair(*index); + return true; + } else { + return SetAttributePair(index, key, ""); + } +} + +bool ParsedCookie::SetAttributePair(size_t* index, + const std::string& key, + const std::string& value) { + if (!IsValidToken(key) || !IsValidCookieAttributeValue(value)) + return false; + if (!IsValid()) + return false; + if (*index) { + pairs_[*index].second = value; + } else { + pairs_.push_back(std::make_pair(key, value)); + *index = pairs_.size() - 1; + } + return true; +} + +void ParsedCookie::ClearAttributePair(size_t index) { + // The first pair (name/value of cookie at pairs_[0]) cannot be cleared. + // Cookie attributes that don't have a value at the moment, are represented + // with an index being equal to 0. + if (index == 0) + return; + + size_t* indexes[] = {&path_index_, &domain_index_, &mac_key_index_, + &mac_algorithm_index_, &expires_index_, &maxage_index_, &secure_index_, + &httponly_index_}; + for (size_t i = 0; i < arraysize(indexes); ++i) { + if (*indexes[i] == index) + *indexes[i] = 0; + else if (*indexes[i] > index) + --*indexes[i]; + } + pairs_.erase(pairs_.begin() + index); +} + } // namespace diff --git a/net/cookies/parsed_cookie.h b/net/cookies/parsed_cookie.h index 6f8e4ef..694d724 100644 --- a/net/cookies/parsed_cookie.h +++ b/net/cookies/parsed_cookie.h @@ -27,8 +27,9 @@ class NET_EXPORT ParsedCookie { ParsedCookie(const std::string& cookie_line); ~ParsedCookie(); - // You should not call any other methods on the class if !IsValid - bool IsValid() const { return is_valid_; } + // You should not call any other methods except for SetName/SetValue on the + // class if !IsValid. + bool IsValid() const; const std::string& Name() const { return pairs_[0].first; } const std::string& Token() const { return Name(); } @@ -55,8 +56,24 @@ class NET_EXPORT ParsedCookie { // "BLAH=hah; path=/; domain=.google.com" size_t NumberOfAttributes() const { return pairs_.size() - 1; } - // For debugging only! - std::string DebugString() const; + // These functions set the respective properties of the cookie. If the + // parameters are empty, the respective properties are cleared. + // The functions return false in case an error occurred. + // The cookie needs to be assigned a name/value before setting the other + // attributes. + bool SetName(const std::string& name); + bool SetValue(const std::string& value); + bool SetPath(const std::string& path); + bool SetDomain(const std::string& domain); + bool SetMACKey(const std::string& mac_key); + bool SetMACAlgorithm(const std::string& mac_algorithm); + bool SetExpires(const std::string& expires); + bool SetMaxAge(const std::string& maxage); + bool SetIsSecure(bool is_secure); + bool SetIsHttpOnly(bool is_http_only); + + // Returns the cookie description as it appears in a HTML response header. + std::string ToCookieLine() const; // Returns an iterator pointing to the first terminator character found in // the given string. @@ -87,15 +104,32 @@ class NET_EXPORT ParsedCookie { static std::string ParseValueString(const std::string& value); private: - static const char kTerminator[]; - static const int kTerminatorLen; - static const char kWhitespace[]; - static const char kValueSeparator[]; - static const char kTokenSeparator[]; - void ParseTokenValuePairs(const std::string& cookie_line); void SetupAttributes(); + // Sets a key/value pair for a cookie. |index| has to point to one of the + // |*_index_| fields in ParsedCookie and is updated to the position where + // the key/value pair is set in |pairs_|. Accordingly, |key| has to correspond + // to the token matching |index|. If |value| contains invalid characters, the + // cookie parameter is not changed and the function returns false. + // If |value| is empty/false the key/value pair is removed. + bool SetString(size_t* index, + const std::string& key, + const std::string& value); + bool SetBool(size_t* index, + const std::string& key, + bool value); + + // Helper function for SetString and SetBool handling the case that the + // key/value pair shall not be removed. + bool SetAttributePair(size_t* index, + const std::string& key, + const std::string& value); + + // Removes the key/value pair from a cookie that is identified by |index|. + // |index| refers to a position in |pairs_|. + void ClearAttributePair(size_t index); + PairList pairs_; bool is_valid_; // These will default to 0, but that should never be valid since the diff --git a/net/cookies/parsed_cookie_unittest.cc b/net/cookies/parsed_cookie_unittest.cc index 7b6d562..68ead27 100644 --- a/net/cookies/parsed_cookie_unittest.cc +++ b/net/cookies/parsed_cookie_unittest.cc @@ -275,4 +275,142 @@ TEST(ParsedCookieTest, ParseTokensAndValues) { ParsedCookie::ParseValueString("A=B=C;D=E")); } +TEST(ParsedCookieTest, SerializeCookieLine) { + const char input[] = "ANCUUID=zohNumRKgI0oxyhSsV3Z7D ; " + "expires=Sun, 18-Apr-2027 21:06:29 GMT ; " + "path=/ ; "; + const char output[] = "ANCUUID=zohNumRKgI0oxyhSsV3Z7D; " + "expires=Sun, 18-Apr-2027 21:06:29 GMT; " + "path=/"; + ParsedCookie pc(input); + EXPECT_EQ(output, pc.ToCookieLine()); +} + + +TEST(ParsedCookieTest, SetNameAndValue) { + ParsedCookie empty(""); + EXPECT_FALSE(empty.IsValid()); + EXPECT_FALSE(empty.SetDomain("foobar.com")); + EXPECT_TRUE(empty.SetName("name")); + EXPECT_TRUE(empty.SetValue("value")); + EXPECT_EQ("name=value", empty.ToCookieLine()); + EXPECT_TRUE(empty.IsValid()); + + // We don't test + // ParsedCookie invalid("@foo=bar"); + // EXPECT_FALSE(invalid.IsValid()); + // here because we are slightly more tolerant to invalid cookie names and + // values that are set by webservers. We only enforce a correct name and + // value if set via SetName() and SetValue(). + + ParsedCookie pc("name=value"); + EXPECT_TRUE(pc.IsValid()); + + // Set invalid name / value. + EXPECT_FALSE(pc.SetName("@foobar")); + EXPECT_EQ("name=value", pc.ToCookieLine()); + EXPECT_TRUE(pc.IsValid()); + + EXPECT_FALSE(pc.SetName("")); + EXPECT_EQ("name=value", pc.ToCookieLine()); + EXPECT_TRUE(pc.IsValid()); + + EXPECT_FALSE(pc.SetValue("foo bar")); + EXPECT_EQ("name=value", pc.ToCookieLine()); + EXPECT_TRUE(pc.IsValid()); + + EXPECT_FALSE(pc.SetValue("\"foobar")); + EXPECT_EQ("name=value", pc.ToCookieLine()); + EXPECT_TRUE(pc.IsValid()); + + // Set valid name / value + EXPECT_TRUE(pc.SetName("test")); + EXPECT_EQ("test=value", pc.ToCookieLine()); + EXPECT_TRUE(pc.IsValid()); + + EXPECT_TRUE(pc.SetValue("\"foobar\"")); + EXPECT_EQ("test=\"foobar\"", pc.ToCookieLine()); + EXPECT_TRUE(pc.IsValid()); + + EXPECT_TRUE(pc.SetValue("")); + EXPECT_EQ("test=", pc.ToCookieLine()); + EXPECT_TRUE(pc.IsValid()); +} + +TEST(ParsedCookieTest, SetAttributes) { + ParsedCookie pc("name=value"); + EXPECT_TRUE(pc.IsValid()); + + // Clear an unset attribute. + EXPECT_TRUE(pc.SetDomain("")); + EXPECT_FALSE(pc.HasDomain()); + EXPECT_EQ("name=value", pc.ToCookieLine()); + EXPECT_TRUE(pc.IsValid()); + + // Set a string containing an invalid character + EXPECT_FALSE(pc.SetDomain("foo;bar")); + EXPECT_FALSE(pc.HasDomain()); + EXPECT_EQ("name=value", pc.ToCookieLine()); + EXPECT_TRUE(pc.IsValid()); + + // Set all other attributes and check that they are appended in order. + EXPECT_TRUE(pc.SetDomain("domain.com")); + EXPECT_TRUE(pc.SetPath("/")); + EXPECT_TRUE(pc.SetMACKey("mackey")); + EXPECT_TRUE(pc.SetMACAlgorithm("\"macalgorithm\"")); + EXPECT_TRUE(pc.SetExpires("Sun, 18-Apr-2027 21:06:29 GMT")); + EXPECT_TRUE(pc.SetMaxAge("12345")); + EXPECT_TRUE(pc.SetIsSecure(true)); + EXPECT_TRUE(pc.SetIsHttpOnly(true)); + EXPECT_EQ("name=value; domain=domain.com; path=/; mac-key=mackey; " + "mac-algorithm=\"macalgorithm\"; " + "expires=Sun, 18-Apr-2027 21:06:29 GMT; max-age=12345; secure; " + "httponly", + pc.ToCookieLine()); + EXPECT_TRUE(pc.HasDomain()); + EXPECT_TRUE(pc.HasPath()); + EXPECT_TRUE(pc.HasMACKey()); + EXPECT_TRUE(pc.HasMACAlgorithm()); + EXPECT_TRUE(pc.HasExpires()); + EXPECT_TRUE(pc.HasMaxAge()); + EXPECT_TRUE(pc.IsSecure()); + EXPECT_TRUE(pc.IsHttpOnly()); + + // Clear one attribute from the middle. + EXPECT_TRUE(pc.SetMACAlgorithm("")); + EXPECT_TRUE(pc.HasDomain()); + EXPECT_TRUE(pc.HasPath()); + EXPECT_TRUE(pc.HasMACKey()); + EXPECT_FALSE(pc.HasMACAlgorithm()); + EXPECT_TRUE(pc.HasExpires()); + EXPECT_TRUE(pc.HasMaxAge()); + EXPECT_TRUE(pc.IsSecure()); + EXPECT_TRUE(pc.IsHttpOnly()); + EXPECT_EQ("name=value; domain=domain.com; path=/; mac-key=mackey; " + "expires=Sun, 18-Apr-2027 21:06:29 GMT; max-age=12345; secure; " + "httponly", + pc.ToCookieLine()); + + // Clear the rest and change the name and value. + EXPECT_TRUE(pc.SetDomain("")); + EXPECT_TRUE(pc.SetPath("")); + EXPECT_TRUE(pc.SetMACKey("")); + EXPECT_TRUE(pc.SetMACAlgorithm("")); + EXPECT_TRUE(pc.SetExpires("")); + EXPECT_TRUE(pc.SetMaxAge("")); + EXPECT_TRUE(pc.SetIsSecure(false)); + EXPECT_TRUE(pc.SetIsHttpOnly(false)); + EXPECT_TRUE(pc.SetName("name2")); + EXPECT_TRUE(pc.SetValue("value2")); + EXPECT_FALSE(pc.HasDomain()); + EXPECT_FALSE(pc.HasPath()); + EXPECT_FALSE(pc.HasMACKey()); + EXPECT_FALSE(pc.HasMACAlgorithm()); + EXPECT_FALSE(pc.HasExpires()); + EXPECT_FALSE(pc.HasMaxAge()); + EXPECT_FALSE(pc.IsSecure()); + EXPECT_FALSE(pc.IsHttpOnly()); + EXPECT_EQ("name2=value2", pc.ToCookieLine()); +} + } // namespace net |