672 lines
18 KiB
C++
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)
|
|
{
|
|
lldebugs << "returning changed cookies: " << llendl;
|
|
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";
|
|
|
|
lldebugs << " " << iter->second->getCookie() << llendl;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|