diff --git a/indra/llplugin/CMakeLists.txt b/indra/llplugin/CMakeLists.txt
index 6706775d4feff329e1754404f638ca75fa0f7663..def9fcbeaef484654016b37a6e061f36289915e6 100644
--- a/indra/llplugin/CMakeLists.txt
+++ b/indra/llplugin/CMakeLists.txt
@@ -23,6 +23,7 @@ include_directories(
 
 set(llplugin_SOURCE_FILES
     llpluginclassmedia.cpp
+    llplugincookiestore.cpp
     llplugininstance.cpp
     llpluginmessage.cpp
     llpluginmessagepipe.cpp
@@ -36,6 +37,7 @@ set(llplugin_HEADER_FILES
 
     llpluginclassmedia.h
     llpluginclassmediaowner.h
+    llplugincookiestore.h
     llplugininstance.h
     llpluginmessage.h
     llpluginmessageclasses.h
diff --git a/indra/llplugin/llpluginclassmedia.cpp b/indra/llplugin/llpluginclassmedia.cpp
index bf0e19473ee6bad521d1697bf11b5ddeb9c5610f..e09b511a6e475ace147d16ea9e3e6343a0e42cf9 100644
--- a/indra/llplugin/llpluginclassmedia.cpp
+++ b/indra/llplugin/llpluginclassmedia.cpp
@@ -993,6 +993,13 @@ void LLPluginClassMedia::receivePluginMessage(const LLPluginMessage &message)
 			mClickTargetType = TARGET_NONE;
 			mediaEvent(LLPluginClassMediaOwner::MEDIA_EVENT_CLICK_LINK_NOFOLLOW);
 		}
+		else if(message_name == "cookie_set")
+		{
+			if(mOwner)
+			{
+				mOwner->handleCookieSet(this, message.getValue("cookie"));
+			}
+		}
 		else
 		{
 			LL_WARNS("Plugin") << "Unknown " << message_name << " class message: " << message_name << LL_ENDL;
@@ -1076,6 +1083,13 @@ void LLPluginClassMedia::clear_cookies()
 	sendMessage(message);
 }
 
+void LLPluginClassMedia::set_cookies(const std::string &cookies)
+{
+	LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA_BROWSER, "set_cookies");
+	message.setValue("cookies", cookies);	
+	sendMessage(message);
+}
+
 void LLPluginClassMedia::enable_cookies(bool enable)
 {
 	LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA_BROWSER, "enable_cookies");
diff --git a/indra/llplugin/llpluginclassmedia.h b/indra/llplugin/llpluginclassmedia.h
index 79356beb6843081048c2221d00dd035007eba878..8c7b00f45b0284f3ec39ade63666f9d87ef5f7bf 100644
--- a/indra/llplugin/llpluginclassmedia.h
+++ b/indra/llplugin/llpluginclassmedia.h
@@ -189,6 +189,7 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner
 	void focus(bool focused);
 	void clear_cache();
 	void clear_cookies();
+	void set_cookies(const std::string &cookies);
 	void enable_cookies(bool enable);
 	void proxy_setup(bool enable, const std::string &host = LLStringUtil::null, int port = 0);
 	void browse_stop();
diff --git a/indra/llplugin/llpluginclassmediaowner.h b/indra/llplugin/llpluginclassmediaowner.h
index 6d369cd51a36a57e3a129dde9b91b21c517e5abd..5669b81fd1f15ff2b3797f2a8a2e844cef6207aa 100644
--- a/indra/llplugin/llpluginclassmediaowner.h
+++ b/indra/llplugin/llpluginclassmediaowner.h
@@ -39,6 +39,7 @@
 #include <queue>
 
 class LLPluginClassMedia;
+class LLPluginCookieStore;
 
 class LLPluginClassMediaOwner
 {
@@ -78,6 +79,7 @@ class LLPluginClassMediaOwner
 	
 	virtual ~LLPluginClassMediaOwner() {};
 	virtual void handleMediaEvent(LLPluginClassMedia* /*self*/, EMediaEvent /*event*/) {};
+	virtual void handleCookieSet(LLPluginClassMedia* /*self*/, const std::string &/*cookie*/) {};
 };
 
 #endif // LL_LLPLUGINCLASSMEDIAOWNER_H
diff --git a/indra/llplugin/llplugincookiestore.cpp b/indra/llplugin/llplugincookiestore.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1964b8d78965f5c3c9c64ad904efa5fb908fb015
--- /dev/null
+++ b/indra/llplugin/llplugincookiestore.cpp
@@ -0,0 +1,600 @@
+/** 
+ * @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),
+	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)
+{
+	Cookie *result = new Cookie(s, cookie_start, cookie_end);
+
+	if(!result->parse())
+	{
+		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()
+{
+	bool first_field = true;
+
+	std::string::size_type cookie_end = mCookie.size();
+	std::string::size_type field_start = 0;
+
+	lldebugs << "parsing cookie: " << mCookie << llendl;
+	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;
+		}
+
+		lldebugs 
+			<< "    field name: \"" << mCookie.substr(name_start, name_end - name_start) 
+			<< "\", value: \"" << mCookie.substr(value_start, value_end - value_start) << "\""
+			<< llendl;
+				
+		// 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);
+				lldebugs << "        expire date parsed to: " << mDate.asRFC1123() << llendl;
+#else
+				// This doesn't work (rfc1123-format dates cause it to fail)
+				if(!mDate.fromString(date_string))
+				{
+					// Date failed to parse.
+					llwarns << "failed to parse cookie's expire date: " << date << llendl;
+					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
+			{
+				// An unknown field is a parse failure
+				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;
+		
+	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('\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("\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)
+{
+	Cookie *cookie = Cookie::createFromString(s, cookie_start, cookie_end);
+	if(cookie)
+	{
+		lldebugs << "setting cookie: " << cookie->getCookie() << llendl;
+		
+		// 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);
+				lldebugs << "    marking dead" << llendl;
+			}
+			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;
+
+				lldebugs << "    removing" << llendl;
+			}
+		}
+		
+		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;
+
+					lldebugs << "    unchanged" << llendl;
+				}
+				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;
+
+					lldebugs << "    replacing" << llendl;
+				}
+			}
+			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;
+
+				lldebugs << "    adding" << llendl;
+			}
+		}
+	}
+}
+
+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);
+	}
+}
+
diff --git a/indra/llplugin/llplugincookiestore.h b/indra/llplugin/llplugincookiestore.h
new file mode 100644
index 0000000000000000000000000000000000000000..5250f008b674c551ac381140f2cf13d1c655ce25
--- /dev/null
+++ b/indra/llplugin/llplugincookiestore.h
@@ -0,0 +1,122 @@
+/** 
+ * @file llplugincookiestore.h
+ * @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
+ */
+
+#ifndef LL_LLPLUGINCOOKIESTORE_H
+#define LL_LLPLUGINCOOKIESTORE_H
+
+#include "lldate.h"
+#include <map>
+#include <string>
+#include <iostream>
+
+class LLPluginCookieStore
+{
+	LOG_CLASS(LLPluginCookieStore);
+public:
+	LLPluginCookieStore();
+	~LLPluginCookieStore();
+
+	// gets all cookies currently in storage -- use when initializing a plugin
+	std::string getAllCookies();
+	void writeAllCookies(std::ostream& s);
+	
+	// gets only persistent cookies (i.e. not session cookies) -- use when writing cookies to a file
+	std::string getPersistentCookies();
+	void writePersistentCookies(std::ostream& s);
+	
+	// gets cookies which are marked as "changed" -- use when sending periodic updates to plugins
+	std::string getChangedCookies(bool clear_changed = true);
+	void writeChangedCookies(std::ostream& s, bool clear_changed = true);
+	
+	// (re)initializes internal data structures and bulk-sets cookies -- use when reading cookies from a file
+	void setAllCookies(const std::string &cookies, bool mark_changed = false);
+	void readAllCookies(std::istream& s, bool mark_changed = false);
+	
+	// sets one or more cookies (without reinitializing anything) -- use when receiving cookies from a plugin
+	void setCookies(const std::string &cookies, bool mark_changed = true);
+	void readCookies(std::istream& s, bool mark_changed = true);
+
+	// quote or unquote a string as per the definition of 'quoted-string' in rfc2616
+	static std::string quoteString(const std::string &s);
+	static std::string unquoteString(const std::string &s);
+	
+private:
+
+	void setOneCookie(const std::string &s, std::string::size_type cookie_start, std::string::size_type cookie_end, bool mark_changed);
+
+	class Cookie
+	{
+	public:
+		static Cookie *createFromString(const std::string &s, std::string::size_type cookie_start = 0, std::string::size_type cookie_end = std::string::npos);
+		
+		// Construct a string from the cookie that uniquely represents it, to be used as a key in a std::map.
+		std::string getKey() const;
+		
+		const std::string &getCookie() const { return mCookie; };
+		bool isSessionCookie() const { return mDate.isNull(); };
+
+		bool isDead() const { return mDead; };
+		void setDead(bool dead) { mDead = dead; };
+		
+		bool isChanged() const { return mChanged; };
+		void setChanged(bool changed) { mChanged = changed; };
+
+		const LLDate &getDate() const { return mDate; };
+		
+	private:
+		Cookie(const std::string &s, std::string::size_type cookie_start = 0, std::string::size_type cookie_end = std::string::npos);
+		bool parse();
+		std::string::size_type findFieldEnd(std::string::size_type start = 0, std::string::size_type end = std::string::npos);
+		bool matchName(std::string::size_type start, std::string::size_type end, const char *name);
+		
+		std::string mCookie;	// The full cookie, in RFC 2109 string format
+		LLDate mDate;			// The expiration date of the cookie.  For session cookies, this will be a null date (mDate.isNull() is true).
+		// Start/end indices of various parts of the cookie string.  Stored as indices into the string to save space and time.
+		std::string::size_type mNameStart, mNameEnd;
+		std::string::size_type mValueStart, mValueEnd;
+		std::string::size_type mDomainStart, mDomainEnd;
+		std::string::size_type mPathStart, mPathEnd;
+		bool mDead;
+		bool mChanged;
+	};
+	
+	typedef std::map<std::string, Cookie*> cookie_map_t;
+	
+	cookie_map_t mCookies;
+	bool mHasChangedCookies;
+	
+	void clearCookies();
+	void removeCookie(const std::string &key);
+};
+
+#endif // LL_LLPLUGINCOOKIESTORE_H
diff --git a/indra/media_plugins/webkit/media_plugin_webkit.cpp b/indra/media_plugins/webkit/media_plugin_webkit.cpp
index 0462fce236273bf22fa727c5575a904d15b1189c..85d6b2f5ffa5f85a5a856ca0a44785136647deb4 100644
--- a/indra/media_plugins/webkit/media_plugin_webkit.cpp
+++ b/indra/media_plugins/webkit/media_plugin_webkit.cpp
@@ -507,6 +507,19 @@ class MediaPluginWebKit :
 		sendMessage(message);
 	}
 	
+
+	////////////////////////////////////////////////////////////////////////////////
+	// virtual
+	void onCookieChanged(const EventType& event)
+	{
+		LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA_BROWSER, "cookie_set");
+		message.setValue("cookie", event.getStringValue());
+		// These could be passed through as well, but aren't really needed.
+//		message.setValue("uri", event.getEventUri());
+//		message.setValueBoolean("dead", (event.getIntValue() != 0))
+		sendMessage(message);
+	}
+	
 	LLQtWebKit::EKeyboardModifier decodeModifiers(std::string &modifiers)
 	{
 		int result = 0;
@@ -1051,6 +1064,10 @@ void MediaPluginWebKit::receiveMessage(const char *message_string)
 				mJavascriptEnabled = message_in.getValueBoolean("enable");
 				//LLQtWebKit::getInstance()->enableJavascript( mJavascriptEnabled );
 			}
+			else if(message_name == "set_cookies")
+			{
+				LLQtWebKit::getInstance()->setCookies(message_in.getValue("cookies"));
+			}
 			else if(message_name == "proxy_setup")
 			{
 				bool val = message_in.getValueBoolean("enable");
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index d30d7fd26ddbf6c6fa8715a661b9ef62dd2fd985..a3d0b8d8d999b9e70f274649b5a9bea9d8c81758 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -1526,6 +1526,8 @@ bool LLAppViewer::cleanup()
 	LLLocationHistory::getInstance()->save();
 
 	LLAvatarIconIDCache::getInstance()->save();
+	
+	LLViewerMedia::saveCookieFile();
 
 	llinfos << "Shutting down Threads" << llendflush;
 
diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp
index edd03dc83674afdb488d65319c7e77744e23370d..59d118abe2379ed7984164166abfb85a26a05aa9 100644
--- a/indra/newview/llstartup.cpp
+++ b/indra/newview/llstartup.cpp
@@ -938,6 +938,9 @@ bool idle_startup()
 
 		// Load Avatars icons cache
 		LLAvatarIconIDCache::getInstance()->load();
+		
+		// Load media plugin cookies
+		LLViewerMedia::loadCookieFile();
 
 		//-------------------------------------------------
 		// Handle startup progress screen
diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp
index 2fcd3f11140c369fcc25bdecc136cb5247d83467..8863bf187d20ef5b7da3d77debeb64215b719f4b 100644
--- a/indra/newview/llviewermedia.cpp
+++ b/indra/newview/llviewermedia.cpp
@@ -45,6 +45,7 @@
 #include "llviewertexturelist.h"
 #include "llvovolume.h"
 #include "llpluginclassmedia.h"
+#include "llplugincookiestore.h"
 #include "llviewerwindow.h"
 #include "llfocusmgr.h"
 #include "llcallbacklist.h"
@@ -256,6 +257,7 @@ LOG_CLASS(LLMimeDiscoveryResponder);
 		LLViewerMediaImpl *mMediaImpl;
 		bool mInitialized;
 };
+LLPluginCookieStore *LLViewerMedia::sCookieStore = NULL;
 static LLViewerMedia::impl_list sViewerMediaImplList;
 static LLViewerMedia::impl_id_map sViewerMediaTextureIDMap;
 static LLTimer sMediaCreateTimer;
@@ -264,6 +266,8 @@ static F32 sGlobalVolume = 1.0f;
 static F64 sLowestLoadableImplInterest = 0.0f;
 static bool sAnyMediaShowing = false;
 static boost::signals2::connection sTeleportFinishConnection;
+static std::string sUpdatedCookies;
+static const char *PLUGIN_COOKIE_FILE_NAME = "plugin_cookies.txt";
 
 //////////////////////////////////////////////////////////////////////////////////////////
 static void add_media_impl(LLViewerMediaImpl* media)
@@ -399,7 +403,6 @@ viewer_media_t LLViewerMedia::updateMediaImpl(LLMediaEntry* media_entry, const s
 		media_impl->setHomeURL(media_entry->getHomeURL());
 		media_impl->mMediaAutoPlay = media_entry->getAutoPlay();
 		media_impl->mMediaEntryURL = media_entry->getCurrentURL();
-		
 		if(media_impl->isAutoPlayable())
 		{
 			needs_navigate = true;
@@ -698,6 +701,13 @@ static bool proximity_comparitor(const LLViewerMediaImpl* i1, const LLViewerMedi
 void LLViewerMedia::updateMedia(void *dummy_arg)
 {
 	sAnyMediaShowing = false;
+	sUpdatedCookies = getCookieStore()->getChangedCookies();
+	if(!sUpdatedCookies.empty())
+	{
+		lldebugs << "updated cookies will be sent to all loaded plugins: " << llendl;
+		lldebugs << sUpdatedCookies << llendl;
+	}
+	
 	impl_list::iterator iter = sViewerMediaImplList.begin();
 	impl_list::iterator end = sViewerMediaImplList.end();
 
@@ -1095,6 +1105,116 @@ void LLViewerMedia::setProxyConfig(bool enable, const std::string &host, int por
 
 /////////////////////////////////////////////////////////////////////////////////////////
 // static 
+/////////////////////////////////////////////////////////////////////////////////////////
+// static
+LLPluginCookieStore *LLViewerMedia::getCookieStore()
+{
+	if(sCookieStore == NULL)
+	{
+		sCookieStore = new LLPluginCookieStore;
+	}
+	
+	return sCookieStore;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// static
+void LLViewerMedia::loadCookieFile()
+{
+	// build filename for each user
+	std::string resolved_filename = gDirUtilp->getExpandedFilename(LL_PATH_PER_SL_ACCOUNT, PLUGIN_COOKIE_FILE_NAME);
+
+	if (resolved_filename.empty())
+	{
+		llinfos << "can't get path to plugin cookie file - probably not logged in yet." << llendl;
+		return;
+	}
+	
+	// open the file for reading
+	llifstream file(resolved_filename);
+	if (!file.is_open())
+	{
+		llwarns << "can't load plugin cookies from file \"" << PLUGIN_COOKIE_FILE_NAME << "\"" << llendl;
+		return;
+	}
+	
+	getCookieStore()->readAllCookies(file, true);
+
+	file.close();
+	
+	// TODO: send the clear_cookies message to all loaded plugins
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// static
+void LLViewerMedia::saveCookieFile()
+{
+	// build filename for each user
+	std::string resolved_filename = gDirUtilp->getExpandedFilename(LL_PATH_PER_SL_ACCOUNT, PLUGIN_COOKIE_FILE_NAME);
+
+	if (resolved_filename.empty())
+	{
+		llinfos << "can't get path to plugin cookie file - probably not logged in yet." << llendl;
+		return;
+	}
+
+	// open a file for writing
+	llofstream file (resolved_filename);
+	if (!file.is_open())
+	{
+		llwarns << "can't open plugin cookie file \"" << PLUGIN_COOKIE_FILE_NAME << "\" for writing" << llendl;
+		return;
+	}
+
+	getCookieStore()->writePersistentCookies(file);
+
+	file.close();
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// static
+void LLViewerMedia::addCookie(const std::string &name, const std::string &value, const std::string &domain, const LLDate &expires, const std::string &path, bool secure)
+{
+	std::stringstream cookie;
+	
+	cookie << name << "=" << LLPluginCookieStore::quoteString(value);
+	
+	if(expires.notNull())
+	{
+		cookie << "; expires=" << expires.asRFC1123();
+	}
+	
+	cookie << "; domain=" << domain;
+
+	cookie << "; path=" << path;
+	
+	if(secure)
+	{
+		cookie << "; secure";
+	}
+	
+	getCookieStore()->setCookies(cookie.str());
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// static
+void LLViewerMedia::addSessionCookie(const std::string &name, const std::string &value, const std::string &domain, const std::string &path, bool secure)
+{
+	// A session cookie just has a NULL date.
+	addCookie(name, value, domain, LLDate(), path, secure);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// static
+void LLViewerMedia::removeCookie(const std::string &name, const std::string &domain, const std::string &path )
+{
+	// To remove a cookie, add one with the same name, domain, and path that expires in the past.
+	
+	addCookie(name, "", domain, LLDate(LLDate::now().secondsSinceEpoch() - 1.0), path);
+}
+
+
 bool LLViewerMedia::hasInWorldMedia()
 {
 	if (sInWorldMediaDisabled) return false;
@@ -1455,6 +1575,17 @@ bool LLViewerMediaImpl::initializePlugin(const std::string& media_type)
 			media_source->clear_cache();
 		}
 		
+		// TODO: Only send cookies to plugins that need them
+		//  Ideally, the plugin should tell us whether it handles cookies or not -- either via the init response or through a separate message.
+		//  Due to the ordering of messages, it's possible we wouldn't get that information back in time to send cookies before sending a navigate message,
+		//  which could cause odd race conditions.
+		std::string all_cookies = LLViewerMedia::getCookieStore()->getAllCookies();
+		lldebugs << "setting cookies: " << all_cookies << llendl;
+		if(!all_cookies.empty())
+		{
+			media_source->set_cookies(all_cookies);
+		}
+				
 		mMediaSource = media_source;
 
 		updateVolume();
@@ -2152,6 +2283,16 @@ void LLViewerMediaImpl::update()
 			}
 		}
 	}
+	else
+	{
+		// If we didn't just create the impl, it may need to get cookie updates.
+		if(!sUpdatedCookies.empty())
+		{
+			// TODO: Only send cookies to plugins that need them
+			mMediaSource->set_cookies(sUpdatedCookies);
+		}
+	}
+
 	
 	if(mMediaSource == NULL)
 	{
@@ -2612,6 +2753,13 @@ void LLViewerMediaImpl::handleMediaEvent(LLPluginClassMedia* plugin, LLPluginCla
 	emitEvent(plugin, event);
 }
 
+////////////////////////////////////////////////////////////////////////////////
+// virtual
+void LLViewerMediaImpl::handleCookieSet(LLPluginClassMedia* self, const std::string &cookie)
+{
+	LLViewerMedia::getCookieStore()->setCookies(cookie);
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // virtual
 void
diff --git a/indra/newview/llviewermedia.h b/indra/newview/llviewermedia.h
index f9870fb3b9a5947298e9957abb05a22b4543d067..10dacf953267ae7f67506c65eea4d0317c0bbf49 100644
--- a/indra/newview/llviewermedia.h
+++ b/indra/newview/llviewermedia.h
@@ -50,6 +50,7 @@ class LLViewerMediaTexture;
 class LLMediaEntry;
 class LLVOVolume;
 class LLMimeDiscoveryResponder;
+class LLPluginCookieStore;
 
 typedef LLPointer<LLViewerMediaImpl> viewer_media_t;
 ///////////////////////////////////////////////////////////////////////////////
@@ -145,8 +146,17 @@ class LLViewerMedia
 	// Set the proxy config for all loaded plugins
 	static void setProxyConfig(bool enable, const std::string &host, int port);
 	
+	static LLPluginCookieStore *getCookieStore();
+	static void loadCookieFile();
+	static void saveCookieFile();
+	static void addCookie(const std::string &name, const std::string &value, const std::string &domain, const LLDate &expires, const std::string &path = std::string("/"), bool secure = false );
+	static void addSessionCookie(const std::string &name, const std::string &value, const std::string &domain, const std::string &path = std::string("/"), bool secure = false );
+	static void removeCookie(const std::string &name, const std::string &domain, const std::string &path = std::string("/") );
+	
 private:
 	static void onTeleportFinished();
+	
+	static LLPluginCookieStore *sCookieStore;
 };
 
 // Implementation functions not exported into header file
@@ -294,6 +304,7 @@ class LLViewerMediaImpl
 
 	// Inherited from LLPluginClassMediaOwner
 	/*virtual*/ void handleMediaEvent(LLPluginClassMedia* plugin, LLPluginClassMediaOwner::EMediaEvent);
+	/*virtual*/ void handleCookieSet(LLPluginClassMedia* self, const std::string &cookie);
 
 	// LLEditMenuHandler overrides
 	/*virtual*/ void	cut();
diff --git a/install.xml b/install.xml
index 0f4ab373f56f02c7a19db06a5b62e1df1b3cb59f..25c9fdd045e4633864a1c15273feb341fe7ee500 100644
--- a/install.xml
+++ b/install.xml
@@ -948,9 +948,9 @@ anguage Infrstructure (CLI) international standard</string>
           <key>darwin</key>
           <map>
             <key>md5sum</key>
-            <string>ada82d41467cdb7f8e25a442f58b6963</string>
+            <string>171bd85ebb81d319e1f15fab8092f8cd</string>
             <key>url</key>
-            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/llqtwebkit-4.6-darwin-20100318.tar.bz2</uri>
+            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/llqtwebkit-4.6-darwin-20100326.tar.bz2</uri>
           </map>
           <key>linux</key>
           <map>
@@ -962,9 +962,9 @@ anguage Infrstructure (CLI) international standard</string>
           <key>windows</key>
           <map>
             <key>md5sum</key>
-            <string>93b49cfea365fe082eda3e466a9beec3</string>
+            <string>04d86bb2eeed4f928d155cb5598ca6b5</string>
             <key>url</key>
-            <uri>http://viewer-source-downloads.s3.amazonaws.com/install_pkgs/llqtwebkit-windows-qt4.6-20100318.tar.bz2</uri>
+            <uri>http://viewer-source-downloads.s3.amazonaws.com/install_pkgs/llqtwebkit-windows-qt4.6-20100326.tar.bz2</uri>
           </map>
         </map>
       </map>