PageRenderTime 70ms CodeModel.GetById 3ms app.highlight 59ms RepoModel.GetById 1ms app.codeStats 0ms

/indra/llplugin/llplugincookiestore.cpp

https://bitbucket.org/lindenlab/viewer-beta/
C++ | 664 lines | 553 code | 44 blank | 67 comment | 64 complexity | 2d69056fb42b91afe0f51f100b3f02a2 MD5 | raw file
  1/** 
  2 * @file llplugincookiestore.cpp
  3 * @brief LLPluginCookieStore provides central storage for http cookies used by plugins
  4 *
  5 * @cond
  6 * $LicenseInfo:firstyear=2010&license=viewerlgpl$
  7 * Second Life Viewer Source Code
  8 * Copyright (C) 2010, Linden Research, Inc.
  9 * 
 10 * This library is free software; you can redistribute it and/or
 11 * modify it under the terms of the GNU Lesser General Public
 12 * License as published by the Free Software Foundation;
 13 * version 2.1 of the License only.
 14 * 
 15 * This library is distributed in the hope that it will be useful,
 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 18 * Lesser General Public License for more details.
 19 * 
 20 * You should have received a copy of the GNU Lesser General Public
 21 * License along with this library; if not, write to the Free Software
 22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 23 * 
 24 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 25 * $/LicenseInfo$
 26 * @endcond
 27 */
 28
 29#include "linden_common.h"
 30#include "indra_constants.h"
 31
 32#include "llplugincookiestore.h"
 33#include <iostream>
 34
 35// for curl_getdate() (apparently parsing RFC 1123 dates is hard)
 36#include <curl/curl.h>
 37
 38LLPluginCookieStore::LLPluginCookieStore():
 39	mHasChangedCookies(false)
 40{
 41}
 42
 43
 44LLPluginCookieStore::~LLPluginCookieStore()
 45{
 46	clearCookies();
 47}
 48
 49
 50LLPluginCookieStore::Cookie::Cookie(const std::string &s, std::string::size_type cookie_start, std::string::size_type cookie_end):
 51	mCookie(s, cookie_start, cookie_end - cookie_start),
 52	mNameStart(0), mNameEnd(0),
 53	mValueStart(0), mValueEnd(0),
 54	mDomainStart(0), mDomainEnd(0),
 55	mPathStart(0), mPathEnd(0),
 56	mDead(false), mChanged(true)
 57{
 58}
 59
 60LLPluginCookieStore::Cookie *LLPluginCookieStore::Cookie::createFromString(const std::string &s, std::string::size_type cookie_start, std::string::size_type cookie_end, const std::string &host)
 61{
 62	Cookie *result = new Cookie(s, cookie_start, cookie_end);
 63
 64	if(!result->parse(host))
 65	{
 66		delete result;
 67		result = NULL;
 68	}
 69	
 70	return result;
 71}
 72
 73std::string LLPluginCookieStore::Cookie::getKey() const
 74{
 75	std::string result;
 76	if(mDomainEnd > mDomainStart)
 77	{
 78		result += mCookie.substr(mDomainStart, mDomainEnd - mDomainStart);
 79	}
 80	result += ';';
 81	if(mPathEnd > mPathStart)
 82	{
 83		result += mCookie.substr(mPathStart, mPathEnd - mPathStart);
 84	}
 85	result += ';';
 86	result += mCookie.substr(mNameStart, mNameEnd - mNameStart);
 87	return result;
 88}
 89
 90bool LLPluginCookieStore::Cookie::parse(const std::string &host)
 91{
 92	bool first_field = true;
 93
 94	std::string::size_type cookie_end = mCookie.size();
 95	std::string::size_type field_start = 0;
 96
 97	LL_DEBUGS("CookieStoreParse") << "parsing cookie: " << mCookie << LL_ENDL;
 98	while(field_start < cookie_end)
 99	{
100		// Finding the start of the next field requires honoring special quoting rules
101		// see the definition of 'quoted-string' in rfc2616 for details
102		std::string::size_type next_field_start = findFieldEnd(field_start);
103
104		// The end of this field should not include the terminating ';' or any trailing whitespace
105		std::string::size_type field_end = mCookie.find_last_not_of("; ", next_field_start);
106		if(field_end == std::string::npos || field_end < field_start)
107		{
108			// This field was empty or all whitespace.  Set end = start so it shows as empty.
109			field_end = field_start;
110		}
111		else if (field_end < next_field_start)
112		{
113			// we actually want the index of the char _after_ what 'last not of' found
114			++field_end;
115		}
116		
117		// find the start of the actual name (skip separator and possible whitespace)
118		std::string::size_type name_start = mCookie.find_first_not_of("; ", field_start);
119		if(name_start == std::string::npos || name_start > next_field_start)
120		{
121			// Again, nothing but whitespace.
122			name_start = field_start;
123		}
124		
125		// the name and value are separated by the first equals sign
126		std::string::size_type name_value_sep = mCookie.find_first_of("=", name_start);
127		if(name_value_sep == std::string::npos || name_value_sep > field_end)
128		{
129			// No separator found, so this is a field without an = 
130			name_value_sep = field_end;
131		}
132		
133		// the name end is before the name-value separator
134		std::string::size_type name_end = mCookie.find_last_not_of("= ", name_value_sep);
135		if(name_end == std::string::npos || name_end < name_start)
136		{
137			// I'm not sure how we'd hit this case... it seems like it would have to be an empty name.
138			name_end = name_start;
139		}
140		else if (name_end < name_value_sep)
141		{
142			// we actually want the index of the char _after_ what 'last not of' found
143			++name_end;
144		}
145		
146		// Value is between the name-value sep and the end of the field.
147		std::string::size_type value_start = mCookie.find_first_not_of("= ", name_value_sep);
148		if(value_start == std::string::npos || value_start > field_end)
149		{
150			// All whitespace or empty value
151			value_start = field_end;
152		}
153		std::string::size_type value_end = mCookie.find_last_not_of("; ", field_end);
154		if(value_end == std::string::npos || value_end < value_start)
155		{
156			// All whitespace or empty value
157			value_end = value_start;
158		}
159		else if (value_end < field_end)
160		{
161			// we actually want the index of the char _after_ what 'last not of' found
162			++value_end;
163		}
164
165		LL_DEBUGS("CookieStoreParse") 
166			<< "    field name: \"" << mCookie.substr(name_start, name_end - name_start) 
167			<< "\", value: \"" << mCookie.substr(value_start, value_end - value_start) << "\""
168			<< LL_ENDL;
169				
170		// See whether this field is one we know
171		if(first_field)
172		{
173			// The first field is the name=value pair
174			mNameStart = name_start;
175			mNameEnd = name_end;
176			mValueStart = value_start;
177			mValueEnd = value_end;
178			first_field = false;
179		}
180		else
181		{
182			// Subsequent fields must come from the set in rfc2109
183			if(matchName(name_start, name_end, "expires"))
184			{
185				std::string date_string(mCookie, value_start, value_end - value_start); 
186				// If the cookie contains an "expires" field, it MUST contain a parsable date.
187				
188				// HACK: LLDate apparently can't PARSE an rfc1123-format date, even though it can GENERATE one.
189				//  The curl function curl_getdate can do this, but I'm hesitant to unilaterally introduce a curl dependency in LLDate.
190#if 1
191				time_t date = curl_getdate(date_string.c_str(), NULL );
192				mDate.secondsSinceEpoch((F64)date);
193				LL_DEBUGS("CookieStoreParse") << "        expire date parsed to: " << mDate.asRFC1123() << LL_ENDL;
194#else
195				// This doesn't work (rfc1123-format dates cause it to fail)
196				if(!mDate.fromString(date_string))
197				{
198					// Date failed to parse.
199					LL_WARNS("CookieStoreParse") << "failed to parse cookie's expire date: " << date << LL_ENDL;
200					return false;
201				}
202#endif
203			}
204			else if(matchName(name_start, name_end, "domain"))
205			{
206				mDomainStart = value_start;
207				mDomainEnd = value_end;
208			}
209			else if(matchName(name_start, name_end, "path"))
210			{
211				mPathStart = value_start;
212				mPathEnd = value_end;
213			}
214			else if(matchName(name_start, name_end, "max-age"))
215			{
216				// TODO: how should we handle this?
217			}
218			else if(matchName(name_start, name_end, "secure"))
219			{
220				// We don't care about the value of this field (yet)
221			}
222			else if(matchName(name_start, name_end, "version"))
223			{
224				// We don't care about the value of this field (yet)
225			}
226			else if(matchName(name_start, name_end, "comment"))
227			{
228				// We don't care about the value of this field (yet)
229			}
230			else if(matchName(name_start, name_end, "httponly"))
231			{
232				// We don't care about the value of this field (yet)
233			}
234			else
235			{
236				// An unknown field is a parse failure
237				LL_WARNS("CookieStoreParse") << "unexpected field name: " << mCookie.substr(name_start, name_end - name_start) << LL_ENDL;
238				return false;
239			}
240			
241		}
242
243		
244		// move on to the next field, skipping this field's separator and any leading whitespace
245		field_start = mCookie.find_first_not_of("; ", next_field_start);
246	}
247		
248	// The cookie MUST have a name
249	if(mNameEnd <= mNameStart)
250		return false;
251	
252	// If the cookie doesn't have a domain, add the current host as the domain.
253	if(mDomainEnd <= mDomainStart)
254	{
255		if(host.empty())
256		{
257			// no domain and no current host -- this is a parse failure.
258			return false;
259		}
260		
261		// Figure out whether this cookie ended with a ";" or not...
262		std::string::size_type last_char = mCookie.find_last_not_of(" ");
263		if((last_char != std::string::npos) && (mCookie[last_char] != ';'))
264		{
265			mCookie += ";";
266		}
267		
268		mCookie += " domain=";
269		mDomainStart = mCookie.size();
270		mCookie += host;
271		mDomainEnd = mCookie.size();
272		
273		LL_DEBUGS("CookieStoreParse") << "added domain (" << mDomainStart << " to " << mDomainEnd << "), new cookie is: " << mCookie << LL_ENDL;
274	}
275
276	// If the cookie doesn't have a path, add "/".
277	if(mPathEnd <= mPathStart)
278	{
279		// Figure out whether this cookie ended with a ";" or not...
280		std::string::size_type last_char = mCookie.find_last_not_of(" ");
281		if((last_char != std::string::npos) && (mCookie[last_char] != ';'))
282		{
283			mCookie += ";";
284		}
285		
286		mCookie += " path=";
287		mPathStart = mCookie.size();
288		mCookie += "/";
289		mPathEnd = mCookie.size();
290		
291		LL_DEBUGS("CookieStoreParse") << "added path (" << mPathStart << " to " << mPathEnd << "), new cookie is: " << mCookie << LL_ENDL;
292	}
293	
294	
295	return true;
296}
297
298std::string::size_type LLPluginCookieStore::Cookie::findFieldEnd(std::string::size_type start, std::string::size_type end)
299{
300	std::string::size_type result = start;
301	
302	if(end == std::string::npos)
303		end = mCookie.size();
304	
305	bool in_quotes = false;
306	for(; (result < end); result++)
307	{
308		switch(mCookie[result])
309		{
310			case '\\':
311				if(in_quotes)
312					result++; // The next character is backslash-quoted.  Skip over it.
313			break;
314			case '"':
315				in_quotes = !in_quotes;
316			break;
317			case ';':
318				if(!in_quotes)
319					return result;
320			break;
321		}		
322	}
323	
324	// If we got here, no ';' was found.
325	return end;
326}
327
328bool LLPluginCookieStore::Cookie::matchName(std::string::size_type start, std::string::size_type end, const char *name)
329{
330	// NOTE: this assumes 'name' is already in lowercase.  The code which uses it should be able to arrange this...
331	
332	while((start < end) && (*name != '\0'))
333	{
334		if(tolower(mCookie[start]) != *name)
335			return false;
336			
337		start++;
338		name++;
339	}
340	
341	// iff both strings hit the end at the same time, they're equal.
342	return ((start == end) && (*name == '\0'));
343}
344
345std::string LLPluginCookieStore::getAllCookies()
346{
347	std::stringstream result;
348	writeAllCookies(result);
349	return result.str();
350}
351
352void LLPluginCookieStore::writeAllCookies(std::ostream& s)
353{
354	cookie_map_t::iterator iter;
355	for(iter = mCookies.begin(); iter != mCookies.end(); iter++)
356	{
357		// Don't return expired cookies
358		if(!iter->second->isDead())
359		{
360			s << (iter->second->getCookie()) << "\n";
361		}
362	}
363
364}
365
366std::string LLPluginCookieStore::getPersistentCookies()
367{
368	std::stringstream result;
369	writePersistentCookies(result);
370	return result.str();
371}
372
373void LLPluginCookieStore::writePersistentCookies(std::ostream& s)
374{
375	cookie_map_t::iterator iter;
376	for(iter = mCookies.begin(); iter != mCookies.end(); iter++)
377	{
378		// Don't return expired cookies or session cookies
379		if(!iter->second->isDead() && !iter->second->isSessionCookie())
380		{
381			s << iter->second->getCookie() << "\n";
382		}
383	}
384}
385
386std::string LLPluginCookieStore::getChangedCookies(bool clear_changed)
387{
388	std::stringstream result;
389	writeChangedCookies(result, clear_changed);
390	
391	return result.str();
392}
393
394void LLPluginCookieStore::writeChangedCookies(std::ostream& s, bool clear_changed)
395{
396	if(mHasChangedCookies)
397	{
398		lldebugs << "returning changed cookies: " << llendl;
399		cookie_map_t::iterator iter;
400		for(iter = mCookies.begin(); iter != mCookies.end(); )
401		{
402			cookie_map_t::iterator next = iter;
403			next++;
404			
405			// Only return cookies marked as "changed"
406			if(iter->second->isChanged())
407			{
408				s << iter->second->getCookie() << "\n";
409
410				lldebugs << "    " << iter->second->getCookie() << llendl;
411
412				// If requested, clear the changed mark
413				if(clear_changed)
414				{
415					if(iter->second->isDead())
416					{
417						// If this cookie was previously marked dead, it needs to be removed entirely.	
418						delete iter->second;
419						mCookies.erase(iter);
420					}
421					else
422					{
423						// Not dead, just mark as not changed.
424						iter->second->setChanged(false);
425					}
426				}
427			}
428			
429			iter = next;
430		}
431	}
432	
433	if(clear_changed)
434		mHasChangedCookies = false;
435}
436
437void LLPluginCookieStore::setAllCookies(const std::string &cookies, bool mark_changed)
438{
439	clearCookies();
440	setCookies(cookies, mark_changed);
441}
442
443void LLPluginCookieStore::readAllCookies(std::istream& s, bool mark_changed)
444{
445	clearCookies();
446	readCookies(s, mark_changed);
447}
448	
449void LLPluginCookieStore::setCookies(const std::string &cookies, bool mark_changed)
450{
451	std::string::size_type start = 0;
452
453	while(start != std::string::npos)
454	{
455		std::string::size_type end = cookies.find_first_of("\r\n", start);
456		if(end > start)
457		{
458			// The line is non-empty.  Try to create a cookie from it.
459			setOneCookie(cookies, start, end, mark_changed);
460		}
461		start = cookies.find_first_not_of("\r\n ", end);
462	}
463}
464
465void LLPluginCookieStore::setCookiesFromHost(const std::string &cookies, const std::string &host, bool mark_changed)
466{
467	std::string::size_type start = 0;
468
469	while(start != std::string::npos)
470	{
471		std::string::size_type end = cookies.find_first_of("\r\n", start);
472		if(end > start)
473		{
474			// The line is non-empty.  Try to create a cookie from it.
475			setOneCookie(cookies, start, end, mark_changed, host);
476		}
477		start = cookies.find_first_not_of("\r\n ", end);
478	}
479}
480			
481void LLPluginCookieStore::readCookies(std::istream& s, bool mark_changed)
482{
483	std::string line;
484	while(s.good() && !s.eof())
485	{
486		std::getline(s, line);
487		if(!line.empty())
488		{
489			// Try to create a cookie from this line.
490			setOneCookie(line, 0, std::string::npos, mark_changed);
491		}
492	}
493}
494
495std::string LLPluginCookieStore::quoteString(const std::string &s)
496{
497	std::stringstream result;
498	
499	result << '"';
500	
501	for(std::string::size_type i = 0; i < s.size(); ++i)
502	{
503		char c = s[i];
504		switch(c)
505		{
506			// All these separators need to be quoted in HTTP headers, according to section 2.2 of rfc 2616:
507			case '(': case ')': case '<': case '>': case '@':
508			case ',': case ';': case ':': case '\\': case '"':
509			case '/': case '[': case ']': case '?': case '=':
510			case '{': case '}':	case ' ': case '\t':
511				result << '\\';
512			break;
513		}
514		
515		result << c;
516	}
517	
518	result << '"';
519	
520	return result.str();
521}
522
523std::string LLPluginCookieStore::unquoteString(const std::string &s)
524{
525	std::stringstream result;
526	
527	bool in_quotes = false;
528	
529	for(std::string::size_type i = 0; i < s.size(); ++i)
530	{
531		char c = s[i];
532		switch(c)
533		{
534			case '\\':
535				if(in_quotes)
536				{
537					// The next character is backslash-quoted.  Pass it through untouched.
538					++i; 
539					if(i < s.size())
540					{
541						result << s[i];
542					}
543					continue;
544				}
545			break;
546			case '"':
547				in_quotes = !in_quotes;
548				continue;
549			break;
550		}
551		
552		result << c;
553	}
554	
555	return result.str();
556}
557
558// The flow for deleting a cookie is non-obvious enough that I should call it out here...
559// Deleting a cookie is done by setting a cookie with the same name, path, and domain, but with an expire timestamp in the past.
560// (This is exactly how a web server tells a browser to delete a cookie.)
561// 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.
562// 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
563// delete operation (in the form of the expired cookie) is passed along.
564void 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)
565{
566	Cookie *cookie = Cookie::createFromString(s, cookie_start, cookie_end, host);
567	if(cookie)
568	{
569		LL_DEBUGS("CookieStoreUpdate") << "setting cookie: " << cookie->getCookie() << LL_ENDL;
570		
571		// Create a key for this cookie
572		std::string key = cookie->getKey();
573		
574		// Check to see whether this cookie should have expired
575		if(!cookie->isSessionCookie() && (cookie->getDate() < LLDate::now()))
576		{
577			// This cookie has expired.
578			if(mark_changed)
579			{
580				// If we're marking cookies as changed, we should keep it anyway since we'll need to send it out with deltas.
581				cookie->setDead(true);
582				LL_DEBUGS("CookieStoreUpdate") << "    marking dead" << LL_ENDL;
583			}
584			else
585			{
586				// If we're not marking cookies as changed, we don't need to keep this cookie at all.
587				// If the cookie was already in the list, delete it.
588				removeCookie(key);
589
590				delete cookie;
591				cookie = NULL;
592
593				LL_DEBUGS("CookieStoreUpdate") << "    removing" << LL_ENDL;
594			}
595		}
596		
597		if(cookie)
598		{
599			// If it already exists in the map, replace it.
600			cookie_map_t::iterator iter = mCookies.find(key);
601			if(iter != mCookies.end())
602			{
603				if(iter->second->getCookie() == cookie->getCookie())
604				{
605					// The new cookie is identical to the old -- don't mark as changed.
606					// Just leave the old one in the map.
607					delete cookie;
608					cookie = NULL;
609
610					LL_DEBUGS("CookieStoreUpdate") << "    unchanged" << LL_ENDL;
611				}
612				else
613				{
614					// A matching cookie was already in the map.  Replace it.
615					delete iter->second;
616					iter->second = cookie;
617					
618					cookie->setChanged(mark_changed);
619					if(mark_changed)
620						mHasChangedCookies = true;
621
622					LL_DEBUGS("CookieStoreUpdate") << "    replacing" << LL_ENDL;
623				}
624			}
625			else
626			{
627				// The cookie wasn't in the map.  Insert it.
628				mCookies.insert(std::make_pair(key, cookie));
629				
630				cookie->setChanged(mark_changed);
631				if(mark_changed)
632					mHasChangedCookies = true;
633
634				LL_DEBUGS("CookieStoreUpdate") << "    adding" << LL_ENDL;
635			}
636		}
637	}
638	else
639	{
640		LL_WARNS("CookieStoreUpdate") << "failed to parse cookie: " << s.substr(cookie_start, cookie_end - cookie_start) << LL_ENDL;
641	}
642
643}
644
645void LLPluginCookieStore::clearCookies()
646{
647	while(!mCookies.empty())
648	{
649		cookie_map_t::iterator iter = mCookies.begin();
650		delete iter->second;
651		mCookies.erase(iter);
652	}
653}
654
655void LLPluginCookieStore::removeCookie(const std::string &key)
656{
657	cookie_map_t::iterator iter = mCookies.find(key);
658	if(iter != mCookies.end())
659	{
660		delete iter->second;
661		mCookies.erase(iter);
662	}
663}
664