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