Files
SingularityViewer/indra/llplugin/llplugincookiestore.cpp

672 lines
18 KiB
C++

/**
* @file llplugincookiestore.cpp
* @brief LLPluginCookieStore provides central storage for http cookies used by plugins
*
* @cond
* $LicenseInfo:firstyear=2010&license=viewergpl$
*
* Copyright (c) 2010, Linden Research, Inc.
*
* Second Life Viewer Source Code
* The source code in this file ("Source Code") is provided by Linden Lab
* to you under the terms of the GNU General Public License, version 2.0
* ("GPL"), unless you have obtained a separate licensing agreement
* ("Other License"), formally executed by you and Linden Lab. Terms of
* the GPL can be found in doc/GPL-license.txt in this distribution, or
* online at http://secondlife.com/developers/opensource/gplv2
*
* There are special exceptions to the terms and conditions of the GPL as
* it is applied to this Source Code. View the full text of the exception
* in the file doc/FLOSS-exception.txt in this software distribution, or
* online at
* http://secondlife.com/developers/opensource/flossexception
*
* By copying, modifying or distributing this software, you acknowledge
* that you have read and understood your obligations described above,
* and agree to abide by those obligations.
*
* ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
* WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
* COMPLETENESS OR PERFORMANCE.
* $/LicenseInfo$
*
* @endcond
*/
#include "linden_common.h"
#include "indra_constants.h"
#include "llplugincookiestore.h"
#include <iostream>
// for curl_getdate() (apparently parsing RFC 1123 dates is hard)
#include <curl/curl.h>
LLPluginCookieStore::LLPluginCookieStore():
mHasChangedCookies(false)
{
}
LLPluginCookieStore::~LLPluginCookieStore()
{
clearCookies();
}
LLPluginCookieStore::Cookie::Cookie(const std::string &s, std::string::size_type cookie_start, std::string::size_type cookie_end):
mCookie(s, cookie_start, cookie_end - cookie_start),
mNameStart(0), mNameEnd(0),
mValueStart(0), mValueEnd(0),
mDomainStart(0), mDomainEnd(0),
mPathStart(0), mPathEnd(0),
mDead(false), mChanged(true)
{
}
LLPluginCookieStore::Cookie *LLPluginCookieStore::Cookie::createFromString(const std::string &s, std::string::size_type cookie_start, std::string::size_type cookie_end, const std::string &host)
{
Cookie *result = new Cookie(s, cookie_start, cookie_end);
if(!result->parse(host))
{
delete result;
result = NULL;
}
return result;
}
std::string LLPluginCookieStore::Cookie::getKey() const
{
std::string result;
if(mDomainEnd > mDomainStart)
{
result += mCookie.substr(mDomainStart, mDomainEnd - mDomainStart);
}
result += ';';
if(mPathEnd > mPathStart)
{
result += mCookie.substr(mPathStart, mPathEnd - mPathStart);
}
result += ';';
result += mCookie.substr(mNameStart, mNameEnd - mNameStart);
return result;
}
bool LLPluginCookieStore::Cookie::parse(const std::string &host)
{
bool first_field = true;
std::string::size_type cookie_end = mCookie.size();
std::string::size_type field_start = 0;
LL_DEBUGS("CookieStoreParse") << "parsing cookie: " << mCookie << LL_ENDL;
while(field_start < cookie_end)
{
// Finding the start of the next field requires honoring special quoting rules
// see the definition of 'quoted-string' in rfc2616 for details
std::string::size_type next_field_start = findFieldEnd(field_start);
// The end of this field should not include the terminating ';' or any trailing whitespace
std::string::size_type field_end = mCookie.find_last_not_of("; ", next_field_start);
if(field_end == std::string::npos || field_end < field_start)
{
// This field was empty or all whitespace. Set end = start so it shows as empty.
field_end = field_start;
}
else if (field_end < next_field_start)
{
// we actually want the index of the char _after_ what 'last not of' found
++field_end;
}
// find the start of the actual name (skip separator and possible whitespace)
std::string::size_type name_start = mCookie.find_first_not_of("; ", field_start);
if(name_start == std::string::npos || name_start > next_field_start)
{
// Again, nothing but whitespace.
name_start = field_start;
}
// the name and value are separated by the first equals sign
std::string::size_type name_value_sep = mCookie.find_first_of("=", name_start);
if(name_value_sep == std::string::npos || name_value_sep > field_end)
{
// No separator found, so this is a field without an =
name_value_sep = field_end;
}
// the name end is before the name-value separator
std::string::size_type name_end = mCookie.find_last_not_of("= ", name_value_sep);
if(name_end == std::string::npos || name_end < name_start)
{
// I'm not sure how we'd hit this case... it seems like it would have to be an empty name.
name_end = name_start;
}
else if (name_end < name_value_sep)
{
// we actually want the index of the char _after_ what 'last not of' found
++name_end;
}
// Value is between the name-value sep and the end of the field.
std::string::size_type value_start = mCookie.find_first_not_of("= ", name_value_sep);
if(value_start == std::string::npos || value_start > field_end)
{
// All whitespace or empty value
value_start = field_end;
}
std::string::size_type value_end = mCookie.find_last_not_of("; ", field_end);
if(value_end == std::string::npos || value_end < value_start)
{
// All whitespace or empty value
value_end = value_start;
}
else if (value_end < field_end)
{
// we actually want the index of the char _after_ what 'last not of' found
++value_end;
}
LL_DEBUGS("CookieStoreParse")
<< " field name: \"" << mCookie.substr(name_start, name_end - name_start)
<< "\", value: \"" << mCookie.substr(value_start, value_end - value_start) << "\""
<< LL_ENDL;
// See whether this field is one we know
if(first_field)
{
// The first field is the name=value pair
mNameStart = name_start;
mNameEnd = name_end;
mValueStart = value_start;
mValueEnd = value_end;
first_field = false;
}
else
{
// Subsequent fields must come from the set in rfc2109
if(matchName(name_start, name_end, "expires"))
{
std::string date_string(mCookie, value_start, value_end - value_start);
// If the cookie contains an "expires" field, it MUST contain a parsable date.
// HACK: LLDate apparently can't PARSE an rfc1123-format date, even though it can GENERATE one.
// The curl function curl_getdate can do this, but I'm hesitant to unilaterally introduce a curl dependency in LLDate.
#if 1
time_t date = curl_getdate(date_string.c_str(), NULL );
mDate.secondsSinceEpoch((F64)date);
LL_DEBUGS("CookieStoreParse") << " expire date parsed to: " << mDate.asRFC1123() << LL_ENDL;
#else
// This doesn't work (rfc1123-format dates cause it to fail)
if(!mDate.fromString(date_string))
{
// Date failed to parse.
LL_WARNS("CookieStoreParse") << "failed to parse cookie's expire date: " << date << LL_ENDL;
return false;
}
#endif
}
else if(matchName(name_start, name_end, "domain"))
{
mDomainStart = value_start;
mDomainEnd = value_end;
}
else if(matchName(name_start, name_end, "path"))
{
mPathStart = value_start;
mPathEnd = value_end;
}
else if(matchName(name_start, name_end, "max-age"))
{
// TODO: how should we handle this?
}
else if(matchName(name_start, name_end, "secure"))
{
// We don't care about the value of this field (yet)
}
else if(matchName(name_start, name_end, "version"))
{
// We don't care about the value of this field (yet)
}
else if(matchName(name_start, name_end, "comment"))
{
// We don't care about the value of this field (yet)
}
else if(matchName(name_start, name_end, "httponly"))
{
// We don't care about the value of this field (yet)
}
else
{
// An unknown field is a parse failure
LL_WARNS("CookieStoreParse") << "unexpected field name: " << mCookie.substr(name_start, name_end - name_start) << LL_ENDL;
return false;
}
}
// move on to the next field, skipping this field's separator and any leading whitespace
field_start = mCookie.find_first_not_of("; ", next_field_start);
}
// The cookie MUST have a name
if(mNameEnd <= mNameStart)
return false;
// If the cookie doesn't have a domain, add the current host as the domain.
if(mDomainEnd <= mDomainStart)
{
if(host.empty())
{
// no domain and no current host -- this is a parse failure.
return false;
}
// Figure out whether this cookie ended with a ";" or not...
std::string::size_type last_char = mCookie.find_last_not_of(" ");
if((last_char != std::string::npos) && (mCookie[last_char] != ';'))
{
mCookie += ";";
}
mCookie += " domain=";
mDomainStart = mCookie.size();
mCookie += host;
mDomainEnd = mCookie.size();
LL_DEBUGS("CookieStoreParse") << "added domain (" << mDomainStart << " to " << mDomainEnd << "), new cookie is: " << mCookie << LL_ENDL;
}
// If the cookie doesn't have a path, add "/".
if(mPathEnd <= mPathStart)
{
// Figure out whether this cookie ended with a ";" or not...
std::string::size_type last_char = mCookie.find_last_not_of(" ");
if((last_char != std::string::npos) && (mCookie[last_char] != ';'))
{
mCookie += ";";
}
mCookie += " path=";
mPathStart = mCookie.size();
mCookie += "/";
mPathEnd = mCookie.size();
LL_DEBUGS("CookieStoreParse") << "added path (" << mPathStart << " to " << mPathEnd << "), new cookie is: " << mCookie << LL_ENDL;
}
return true;
}
std::string::size_type LLPluginCookieStore::Cookie::findFieldEnd(std::string::size_type start, std::string::size_type end)
{
std::string::size_type result = start;
if(end == std::string::npos)
end = mCookie.size();
bool in_quotes = false;
for(; (result < end); result++)
{
switch(mCookie[result])
{
case '\\':
if(in_quotes)
result++; // The next character is backslash-quoted. Skip over it.
break;
case '"':
in_quotes = !in_quotes;
break;
case ';':
if(!in_quotes)
return result;
break;
}
}
// If we got here, no ';' was found.
return end;
}
bool LLPluginCookieStore::Cookie::matchName(std::string::size_type start, std::string::size_type end, const char *name)
{
// NOTE: this assumes 'name' is already in lowercase. The code which uses it should be able to arrange this...
while((start < end) && (*name != '\0'))
{
if(tolower(mCookie[start]) != *name)
return false;
start++;
name++;
}
// iff both strings hit the end at the same time, they're equal.
return ((start == end) && (*name == '\0'));
}
std::string LLPluginCookieStore::getAllCookies()
{
std::stringstream result;
writeAllCookies(result);
return result.str();
}
void LLPluginCookieStore::writeAllCookies(std::ostream& s)
{
cookie_map_t::iterator iter;
for(iter = mCookies.begin(); iter != mCookies.end(); iter++)
{
// Don't return expired cookies
if(!iter->second->isDead())
{
s << (iter->second->getCookie()) << "\n";
}
}
}
std::string LLPluginCookieStore::getPersistentCookies()
{
std::stringstream result;
writePersistentCookies(result);
return result.str();
}
void LLPluginCookieStore::writePersistentCookies(std::ostream& s)
{
cookie_map_t::iterator iter;
for(iter = mCookies.begin(); iter != mCookies.end(); iter++)
{
// Don't return expired cookies or session cookies
if(!iter->second->isDead() && !iter->second->isSessionCookie())
{
s << iter->second->getCookie() << "\n";
}
}
}
std::string LLPluginCookieStore::getChangedCookies(bool clear_changed)
{
std::stringstream result;
writeChangedCookies(result, clear_changed);
return result.str();
}
void LLPluginCookieStore::writeChangedCookies(std::ostream& s, bool clear_changed)
{
if(mHasChangedCookies)
{
LL_DEBUGS() << "returning changed cookies: " << LL_ENDL;
cookie_map_t::iterator iter;
for(iter = mCookies.begin(); iter != mCookies.end(); )
{
cookie_map_t::iterator next = iter;
next++;
// Only return cookies marked as "changed"
if(iter->second->isChanged())
{
s << iter->second->getCookie() << "\n";
LL_DEBUGS() << " " << iter->second->getCookie() << LL_ENDL;
// If requested, clear the changed mark
if(clear_changed)
{
if(iter->second->isDead())
{
// If this cookie was previously marked dead, it needs to be removed entirely.
delete iter->second;
mCookies.erase(iter);
}
else
{
// Not dead, just mark as not changed.
iter->second->setChanged(false);
}
}
}
iter = next;
}
}
if(clear_changed)
mHasChangedCookies = false;
}
void LLPluginCookieStore::setAllCookies(const std::string &cookies, bool mark_changed)
{
clearCookies();
setCookies(cookies, mark_changed);
}
void LLPluginCookieStore::readAllCookies(std::istream& s, bool mark_changed)
{
clearCookies();
readCookies(s, mark_changed);
}
void LLPluginCookieStore::setCookies(const std::string &cookies, bool mark_changed)
{
std::string::size_type start = 0;
while(start != std::string::npos)
{
std::string::size_type end = cookies.find_first_of("\r\n", start);
if(end > start)
{
// The line is non-empty. Try to create a cookie from it.
setOneCookie(cookies, start, end, mark_changed);
}
start = cookies.find_first_not_of("\r\n ", end);
}
}
void LLPluginCookieStore::setCookiesFromHost(const std::string &cookies, const std::string &host, bool mark_changed)
{
std::string::size_type start = 0;
while(start != std::string::npos)
{
std::string::size_type end = cookies.find_first_of("\r\n", start);
if(end > start)
{
// The line is non-empty. Try to create a cookie from it.
setOneCookie(cookies, start, end, mark_changed, host);
}
start = cookies.find_first_not_of("\r\n ", end);
}
}
void LLPluginCookieStore::readCookies(std::istream& s, bool mark_changed)
{
std::string line;
while(s.good() && !s.eof())
{
std::getline(s, line);
if(!line.empty())
{
// Try to create a cookie from this line.
setOneCookie(line, 0, std::string::npos, mark_changed);
}
}
}
std::string LLPluginCookieStore::quoteString(const std::string &s)
{
std::stringstream result;
result << '"';
for(std::string::size_type i = 0; i < s.size(); ++i)
{
char c = s[i];
switch(c)
{
// All these separators need to be quoted in HTTP headers, according to section 2.2 of rfc 2616:
case '(': case ')': case '<': case '>': case '@':
case ',': case ';': case ':': case '\\': case '"':
case '/': case '[': case ']': case '?': case '=':
case '{': case '}': case ' ': case '\t':
result << '\\';
break;
}
result << c;
}
result << '"';
return result.str();
}
std::string LLPluginCookieStore::unquoteString(const std::string &s)
{
std::stringstream result;
bool in_quotes = false;
for(std::string::size_type i = 0; i < s.size(); ++i)
{
char c = s[i];
switch(c)
{
case '\\':
if(in_quotes)
{
// The next character is backslash-quoted. Pass it through untouched.
++i;
if(i < s.size())
{
result << s[i];
}
continue;
}
break;
case '"':
in_quotes = !in_quotes;
continue;
break;
}
result << c;
}
return result.str();
}
// The flow for deleting a cookie is non-obvious enough that I should call it out here...
// Deleting a cookie is done by setting a cookie with the same name, path, and domain, but with an expire timestamp in the past.
// (This is exactly how a web server tells a browser to delete a cookie.)
// When deleting with mark_changed set to true, this replaces the existing cookie in the list with an entry that's marked both dead and changed.
// Some time later when writeChangedCookies() is called with clear_changed set to true, the dead cookie is deleted from the list after being returned, so that the
// delete operation (in the form of the expired cookie) is passed along.
void LLPluginCookieStore::setOneCookie(const std::string &s, std::string::size_type cookie_start, std::string::size_type cookie_end, bool mark_changed, const std::string &host)
{
Cookie *cookie = Cookie::createFromString(s, cookie_start, cookie_end, host);
if(cookie)
{
LL_DEBUGS("CookieStoreUpdate") << "setting cookie: " << cookie->getCookie() << LL_ENDL;
// Create a key for this cookie
std::string key = cookie->getKey();
// Check to see whether this cookie should have expired
if(!cookie->isSessionCookie() && (cookie->getDate() < LLDate::now()))
{
// This cookie has expired.
if(mark_changed)
{
// If we're marking cookies as changed, we should keep it anyway since we'll need to send it out with deltas.
cookie->setDead(true);
LL_DEBUGS("CookieStoreUpdate") << " marking dead" << LL_ENDL;
}
else
{
// If we're not marking cookies as changed, we don't need to keep this cookie at all.
// If the cookie was already in the list, delete it.
removeCookie(key);
delete cookie;
cookie = NULL;
LL_DEBUGS("CookieStoreUpdate") << " removing" << LL_ENDL;
}
}
if(cookie)
{
// If it already exists in the map, replace it.
cookie_map_t::iterator iter = mCookies.find(key);
if(iter != mCookies.end())
{
if(iter->second->getCookie() == cookie->getCookie())
{
// The new cookie is identical to the old -- don't mark as changed.
// Just leave the old one in the map.
delete cookie;
cookie = NULL;
LL_DEBUGS("CookieStoreUpdate") << " unchanged" << LL_ENDL;
}
else
{
// A matching cookie was already in the map. Replace it.
delete iter->second;
iter->second = cookie;
cookie->setChanged(mark_changed);
if(mark_changed)
mHasChangedCookies = true;
LL_DEBUGS("CookieStoreUpdate") << " replacing" << LL_ENDL;
}
}
else
{
// The cookie wasn't in the map. Insert it.
mCookies.insert(std::make_pair(key, cookie));
cookie->setChanged(mark_changed);
if(mark_changed)
mHasChangedCookies = true;
LL_DEBUGS("CookieStoreUpdate") << " adding" << LL_ENDL;
}
}
}
else
{
LL_WARNS("CookieStoreUpdate") << "failed to parse cookie: " << s.substr(cookie_start, cookie_end - cookie_start) << LL_ENDL;
}
}
void LLPluginCookieStore::clearCookies()
{
while(!mCookies.empty())
{
cookie_map_t::iterator iter = mCookies.begin();
delete iter->second;
mCookies.erase(iter);
}
}
void LLPluginCookieStore::removeCookie(const std::string &key)
{
cookie_map_t::iterator iter = mCookies.find(key);
if(iter != mCookies.end())
{
delete iter->second;
mCookies.erase(iter);
}
}