Clean Solaris build.
[shibboleth/cpp-sp.git] / shibsp / impl / RemotedSessionCache.cpp
index f9162b7..d203319 100644 (file)
-/*\r
- *  Copyright 2001-2007 Internet2\r
- * \r
- * Licensed under the Apache License, Version 2.0 (the "License");\r
- * you may not use this file except in compliance with the License.\r
- * You may obtain a copy of the License at\r
- *\r
- *     http://www.apache.org/licenses/LICENSE-2.0\r
- *\r
- * Unless required by applicable law or agreed to in writing, software\r
- * distributed under the License is distributed on an "AS IS" BASIS,\r
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- * See the License for the specific language governing permissions and\r
- * limitations under the License.\r
- */\r
-\r
-/**\r
- * RemotedSessionCache.cpp\r
- * \r
- * SessionCache implementation that delegates to a remoted version.\r
- */\r
-\r
-#include "internal.h"\r
-#include "Application.h"\r
-#include "exceptions.h"\r
-#include "ServiceProvider.h"\r
-#include "SessionCache.h"\r
-#include "attribute/Attribute.h"\r
-#include "remoting/ListenerService.h"\r
-#include "util/SPConstants.h"\r
-\r
-#include <ctime>\r
-#include <sstream>\r
-#include <xmltooling/XMLToolingConfig.h>\r
-#include <xmltooling/util/DateTime.h>\r
-#include <xmltooling/util/NDC.h>\r
-#include <xmltooling/util/XMLHelper.h>\r
-\r
-using namespace shibsp;\r
-using namespace xmltooling;\r
-using namespace std;\r
-\r
-namespace shibsp {\r
-\r
-    class RemotedCache;\r
-    class RemotedSession : public virtual Session\r
-    {\r
-    public:\r
-        RemotedSession(RemotedCache* cache, DDF& obj) : m_version(obj["version"].integer()), m_obj(obj),\r
-                m_expires(0), m_lastAccess(time(NULL)), m_cache(cache), m_lock(NULL) {\r
-            auto_ptr_XMLCh exp(m_obj["expires"].string());\r
-            if (exp.get()) {\r
-                DateTime iso(exp.get());\r
-                iso.parseDateTime();\r
-                m_expires = iso.getEpoch();\r
-            }\r
-\r
-            m_lock = Mutex::create();\r
-        }\r
-        \r
-        ~RemotedSession() {\r
-            delete m_lock;\r
-            m_obj.destroy();\r
-            for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
-        }\r
-        \r
-        Lockable* lock() {\r
-            m_lock->lock();\r
-            return this;\r
-        }\r
-        void unlock() {\r
-            m_lock->unlock();\r
-        }\r
-\r
-        const char* getID() const {\r
-            return m_obj.name();\r
-        }\r
-        const char* getApplicationID() const {\r
-            return m_obj["application_id"].string();\r
-        }\r
-        const char* getClientAddress() const {\r
-            return m_obj["client_addr"].string();\r
-        }\r
-        const char* getEntityID() const {\r
-            return m_obj["entity_id"].string();\r
-        }\r
-        const char* getProtocol() const {\r
-            return m_obj["protocol"].string();\r
-        }\r
-        const char* getAuthnInstant() const {\r
-            return m_obj["authn_instant"].string();\r
-        }\r
-        const char* getSessionIndex() const {\r
-            return m_obj["session_index"].string();\r
-        }\r
-        const char* getAuthnContextClassRef() const {\r
-            return m_obj["authncontext_class"].string();\r
-        }\r
-        const char* getAuthnContextDeclRef() const {\r
-            return m_obj["authncontext_decl"].string();\r
-        }\r
-        const vector<Attribute*>& getAttributes() const {\r
-            if (m_attributes.empty())\r
-                unmarshallAttributes();\r
-            return m_attributes;\r
-        }\r
-        const multimap<string,const Attribute*>& getIndexedAttributes() const {\r
-            if (m_attributeIndex.empty()) {\r
-                if (m_attributes.empty())\r
-                    unmarshallAttributes();\r
-                for (vector<Attribute*>::const_iterator a = m_attributes.begin(); a != m_attributes.end(); ++a) {\r
-                    const vector<string>& aliases = (*a)->getAliases();\r
-                    for (vector<string>::const_iterator alias = aliases.begin(); alias != aliases.end(); ++alias)\r
-                        m_attributeIndex.insert(make_pair(*alias, *a));\r
-                }\r
-            }\r
-            return m_attributeIndex;\r
-        }\r
-        const vector<const char*>& getAssertionIDs() const {\r
-            if (m_ids.empty()) {\r
-                DDF ids = m_obj["assertions"];\r
-                DDF id = ids.first();\r
-                while (id.isstring()) {\r
-                    m_ids.push_back(id.string());\r
-                    id = ids.next();\r
-                }\r
-            }\r
-            return m_ids;\r
-        }\r
-        \r
-        time_t expires() const { return m_expires; }\r
-        time_t lastAccess() const { return m_lastAccess; }\r
-        void validate(const Application& application, const char* client_addr, time_t* timeout);\r
-\r
-    private:\r
-        void unmarshallAttributes() const;\r
-\r
-        int m_version;\r
-        mutable DDF m_obj;\r
-        mutable vector<Attribute*> m_attributes;\r
-        mutable multimap<string,const Attribute*> m_attributeIndex;\r
-        mutable vector<const char*> m_ids;\r
-        time_t m_expires,m_lastAccess;\r
-        RemotedCache* m_cache;\r
-        Mutex* m_lock;\r
-    };\r
-    \r
-    class RemotedCache : public SessionCache\r
-    {\r
-    public:\r
-        RemotedCache(const DOMElement* e);\r
-        ~RemotedCache();\r
-    \r
-        Session* find(const char* key, const Application& application, const char* client_addr=NULL, time_t* timeout=NULL);\r
-        void remove(const char* key, const Application& application);\r
-        \r
-        void cleanup();\r
-    \r
-        Category& m_log;\r
-    private:\r
-        const DOMElement* m_root;         // Only valid during initialization\r
-        RWLock* m_lock;\r
-        map<string,RemotedSession*> m_hashtable;\r
-    \r
-        void dormant(const char* key);\r
-        static void* cleanup_fn(void*);\r
-        bool shutdown;\r
-        CondWait* shutdown_wait;\r
-        Thread* cleanup_thread;\r
-    };\r
-\r
-    SessionCache* SHIBSP_DLLLOCAL RemotedCacheFactory(const DOMElement* const & e)\r
-    {\r
-        return new RemotedCache(e);\r
-    }\r
-}\r
-\r
-void RemotedSession::unmarshallAttributes() const\r
-{\r
-    Attribute* attribute;\r
-    DDF attrs = m_obj["attributes"];\r
-    DDF attr = attrs.first();\r
-    while (!attr.isnull()) {\r
-        try {\r
-            attribute = Attribute::unmarshall(attr);\r
-            m_attributes.push_back(attribute);\r
-            if (m_cache->m_log.isDebugEnabled())\r
-                m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",\r
-                    attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");\r
-        }\r
-        catch (AttributeException& ex) {\r
-            const char* id = attr.first().name();\r
-            m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());\r
-        }\r
-        attr = attrs.next();\r
-    }\r
-}\r
-\r
-void RemotedSession::validate(const Application& application, const char* client_addr, time_t* timeout)\r
-{\r
-    // Basic expiration?\r
-    time_t now = time(NULL);\r
-    if (now > m_expires) {\r
-        m_cache->m_log.info("session expired (ID: %s)", getID());\r
-        throw opensaml::RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
-    }\r
-\r
-    // Address check?\r
-    if (client_addr) {\r
-        if (m_cache->m_log.isDebugEnabled())\r
-            m_cache->m_log.debug("comparing client address %s against %s", client_addr, getClientAddress());\r
-        if (strcmp(getClientAddress(),client_addr)) {\r
-            m_cache->m_log.warn("client address mismatch");\r
-            throw opensaml::RetryableProfileException(\r
-                "Your IP address ($1) does not match the address recorded at the time the session was established.",\r
-                params(1,client_addr)\r
-                );\r
-        }\r
-    }\r
-\r
-    if (!timeout)\r
-        return;\r
-    \r
-    DDF in("touch::"REMOTED_SESSION_CACHE"::SessionCache"), out;\r
-    DDFJanitor jin(in);\r
-    in.structure();\r
-    in.addmember("key").string(getID());\r
-    in.addmember("version").integer(m_obj["version"].integer());\r
-    if (*timeout) {\r
-        // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  \r
-#ifndef HAVE_GMTIME_R\r
-        struct tm* ptime=gmtime(timeout);\r
-#else\r
-        struct tm res;\r
-        struct tm* ptime=gmtime_r(timeout,&res);\r
-#endif\r
-        char timebuf[32];\r
-        strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);\r
-        in.addmember("timeout").string(timebuf);\r
-    }\r
-\r
-    try {\r
-        out=application.getServiceProvider().getListenerService()->send(in);\r
-    }\r
-    catch (...) {\r
-        out.destroy();\r
-        throw;\r
-    }\r
-\r
-    if (out.isstruct()) {\r
-        // We got an updated record back.\r
-        m_ids.clear();\r
-        for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
-        m_attributes.clear();\r
-        m_attributeIndex.clear();\r
-        m_obj.destroy();\r
-        m_obj = out;\r
-    }\r
-\r
-    m_lastAccess = now;\r
-}\r
-\r
-RemotedCache::RemotedCache(const DOMElement* e)\r
-    : SessionCache(e, 900), m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), m_root(e), m_lock(NULL), shutdown(false)\r
-{\r
-    if (!SPConfig::getConfig().getServiceProvider()->getListenerService())\r
-        throw ConfigurationException("RemotedCacheService requires a ListenerService, but none available.");\r
-        \r
-    m_lock = RWLock::create();\r
-    shutdown_wait = CondWait::create();\r
-    cleanup_thread = Thread::create(&cleanup_fn, (void*)this);\r
-}\r
-\r
-RemotedCache::~RemotedCache()\r
-{\r
-    // Shut down the cleanup thread and let it know...\r
-    shutdown = true;\r
-    shutdown_wait->signal();\r
-    cleanup_thread->join(NULL);\r
-\r
-    for_each(m_hashtable.begin(),m_hashtable.end(),xmltooling::cleanup_pair<string,RemotedSession>());\r
-    delete m_lock;\r
-    delete shutdown_wait;\r
-}\r
-\r
-Session* RemotedCache::find(const char* key, const Application& application, const char* client_addr, time_t* timeout)\r
-{\r
-#ifdef _DEBUG\r
-    xmltooling::NDC ndc("find");\r
-#endif\r
-\r
-    RemotedSession* session=NULL;\r
-    m_log.debug("searching local cache for session (%s)", key);\r
-    m_lock->rdlock();\r
-    map<string,RemotedSession*>::const_iterator i=m_hashtable.find(key);\r
-    if (i==m_hashtable.end()) {\r
-        m_lock->unlock();\r
-        m_log.debug("session not found locally, searching remote cache");\r
-\r
-        DDF in("find::"REMOTED_SESSION_CACHE"::SessionCache"), out;\r
-        DDFJanitor jin(in);\r
-        in.structure();\r
-        in.addmember("key").string(key);\r
-        in.addmember("application_id").string(application.getId());\r
-        if (timeout && *timeout) {\r
-            // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  \r
-#ifndef HAVE_GMTIME_R\r
-            struct tm* ptime=gmtime(timeout);\r
-#else\r
-            struct tm res;\r
-            struct tm* ptime=gmtime_r(timeout,&res);\r
-#endif\r
-            char timebuf[32];\r
-            strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);\r
-            in.addmember("timeout").string(timebuf);\r
-        }\r
-        \r
-        try {\r
-            out=application.getServiceProvider().getListenerService()->send(in);\r
-            if (!out.isstruct()) {\r
-                out.destroy();\r
-                m_log.debug("session not found in remote cache");\r
-                return NULL;\r
-            }\r
-            \r
-            // Wrap the results in a local entry and save it.\r
-            session = new RemotedSession(this, out);\r
-            // The remote end has handled timeout issues, we handle address and expiration checks.\r
-            timeout = NULL;\r
-        }\r
-        catch (...) {\r
-            out.destroy();\r
-            throw;\r
-        }\r
-\r
-        // Lock for writing and repeat the search to avoid duplication.\r
-        m_lock->wrlock();\r
-        SharedLock shared(m_lock, false);\r
-        if (m_hashtable.count(key)) {\r
-            // We're using an existing session entry.\r
-            delete session;\r
-            session = m_hashtable[key];\r
-            session->lock();\r
-        }\r
-        else {\r
-            m_hashtable[key]=session;\r
-            session->lock();\r
-        }\r
-    }\r
-    else {\r
-        // Save off and lock the session.\r
-        session = i->second;\r
-        session->lock();\r
-        m_lock->unlock();\r
-        \r
-        m_log.debug("session found locally, validating it for use");\r
-    }\r
-\r
-    if (!XMLString::equals(session->getApplicationID(), application.getId())) {\r
-        m_log.error("an application (%s) tried to access another application's session", application.getId());\r
-        session->unlock();\r
-        return NULL;\r
-    }\r
-\r
-    // Verify currency and update the timestamp if indicated by caller.\r
-    try {\r
-        session->validate(application, client_addr, timeout);\r
-    }\r
-    catch (...) {\r
-        session->unlock();\r
-        remove(key, application);\r
-        throw;\r
-    }\r
-    \r
-    return session;\r
-}\r
-\r
-void RemotedCache::remove(const char* key, const Application& application)\r
-{\r
-    // Take care of local copy.\r
-    dormant(key);\r
-    \r
-    // Now remote...\r
-    DDF in("remove::"REMOTED_SESSION_CACHE"::SessionCache");\r
-    DDFJanitor jin(in);\r
-    in.structure();\r
-    in.addmember("key").string(key);\r
-    in.addmember("application_id").string(application.getId());\r
-    \r
-    DDF out = application.getServiceProvider().getListenerService()->send(in);\r
-    out.destroy();\r
-}\r
-\r
-void RemotedCache::dormant(const char* key)\r
-{\r
-#ifdef _DEBUG\r
-    xmltooling::NDC ndc("dormant");\r
-#endif\r
-\r
-    m_log.debug("deleting local copy of session (%s)", key);\r
-\r
-    // lock the cache for writing, which means we know nobody is sitting in find()\r
-    m_lock->wrlock();\r
-\r
-    // grab the entry from the table\r
-    map<string,RemotedSession*>::const_iterator i=m_hashtable.find(key);\r
-    if (i==m_hashtable.end()) {\r
-        m_lock->unlock();\r
-        return;\r
-    }\r
-\r
-    // ok, remove the entry and lock it\r
-    RemotedSession* entry=i->second;\r
-    m_hashtable.erase(key);\r
-    entry->lock();\r
-    \r
-    // unlock the cache\r
-    m_lock->unlock();\r
-\r
-    // we can release the cache entry lock because we know we're not in the cache anymore\r
-    entry->unlock();\r
-\r
-    delete entry;\r
-}\r
-\r
-void RemotedCache::cleanup()\r
-{\r
-#ifdef _DEBUG\r
-    xmltooling::NDC ndc("cleanup");\r
-#endif\r
-\r
-    Mutex* mutex = Mutex::create();\r
-  \r
-    // Load our configuration details...\r
-    static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);\r
-    const XMLCh* tag=m_root ? m_root->getAttributeNS(NULL,cleanupInterval) : NULL;\r
-    int rerun_timer = 900;\r
-    if (tag && *tag)\r
-        rerun_timer = XMLString::parseInt(tag);\r
-    if (rerun_timer <= 0)\r
-        rerun_timer = 900;\r
-\r
-    mutex->lock();\r
-\r
-    m_log.info("cleanup thread started...run every %d secs; timeout after %d secs", rerun_timer, m_cacheTimeout);\r
-\r
-    while (!shutdown) {\r
-        shutdown_wait->timedwait(mutex,rerun_timer);\r
-        if (shutdown)\r
-            break;\r
-\r
-        // Ok, let's run through the cleanup process and clean out\r
-        // really old sessions.  This is a two-pass process.  The\r
-        // first pass is done holding a read-lock while we iterate over\r
-        // the cache.  The second pass doesn't need a lock because\r
-        // the 'deletes' will lock the cache.\r
-    \r
-        // Pass 1: iterate over the map and find all entries that have not been\r
-        // used in X hours\r
-        vector<string> stale_keys;\r
-        time_t stale = time(NULL) - m_cacheTimeout;\r
-    \r
-        m_log.debug("cleanup thread running");\r
-\r
-        m_lock->rdlock();\r
-        for (map<string,RemotedSession*>::const_iterator i=m_hashtable.begin(); i!=m_hashtable.end(); ++i) {\r
-            // If the last access was BEFORE the stale timeout...\r
-            i->second->lock();\r
-            time_t last=i->second->lastAccess();\r
-            i->second->unlock();\r
-            if (last < stale)\r
-                stale_keys.push_back(i->first);\r
-        }\r
-        m_lock->unlock();\r
-    \r
-        if (!stale_keys.empty()) {\r
-            m_log.info("purging %d old sessions", stale_keys.size());\r
-    \r
-            // Pass 2: walk through the list of stale entries and remove them from the cache\r
-            for (vector<string>::const_iterator j = stale_keys.begin(); j != stale_keys.end(); ++j)\r
-                dormant(j->c_str());\r
-        }\r
-\r
-        m_log.debug("cleanup thread completed");\r
-    }\r
-\r
-    m_log.info("cleanup thread exiting");\r
-\r
-    mutex->unlock();\r
-    delete mutex;\r
-    Thread::exit(NULL);\r
-}\r
-\r
-void* RemotedCache::cleanup_fn(void* cache_p)\r
-{\r
-    RemotedCache* cache = reinterpret_cast<RemotedCache*>(cache_p);\r
-\r
-#ifndef WIN32\r
-    // First, let's block all signals \r
-    Thread::mask_all_signals();\r
-#endif\r
-\r
-    // Now run the cleanup process.\r
-    cache->cleanup();\r
-    return NULL;\r
-}\r
+/*
+ *  Copyright 2001-2007 Internet2
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * RemotedSessionCache.cpp
+ * 
+ * SessionCache implementation that delegates to a remoted version.
+ */
+
+#include "internal.h"
+#include "Application.h"
+#include "exceptions.h"
+#include "ServiceProvider.h"
+#include "SessionCache.h"
+#include "attribute/Attribute.h"
+#include "remoting/ListenerService.h"
+#include "util/SPConstants.h"
+
+#include <ctime>
+#include <sstream>
+#include <xmltooling/XMLToolingConfig.h>
+#include <xmltooling/util/DateTime.h>
+#include <xmltooling/util/NDC.h>
+#include <xmltooling/util/XMLHelper.h>
+
+using namespace shibsp;
+using namespace xmltooling;
+using namespace std;
+
+namespace shibsp {
+
+    class RemotedCache;
+    class RemotedSession : public virtual Session
+    {
+    public:
+        RemotedSession(RemotedCache* cache, DDF& obj) : m_version(obj["version"].integer()), m_obj(obj),
+                m_expires(0), m_lastAccess(time(NULL)), m_cache(cache), m_lock(NULL) {
+            auto_ptr_XMLCh exp(m_obj["expires"].string());
+            if (exp.get()) {
+                DateTime iso(exp.get());
+                iso.parseDateTime();
+                m_expires = iso.getEpoch();
+            }
+
+            m_lock = Mutex::create();
+        }
+        
+        ~RemotedSession() {
+            delete m_lock;
+            m_obj.destroy();
+            for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
+        }
+        
+        Lockable* lock() {
+            m_lock->lock();
+            return this;
+        }
+        void unlock() {
+            m_lock->unlock();
+        }
+
+        const char* getID() const {
+            return m_obj.name();
+        }
+        const char* getApplicationID() const {
+            return m_obj["application_id"].string();
+        }
+        const char* getClientAddress() const {
+            return m_obj["client_addr"].string();
+        }
+        const char* getEntityID() const {
+            return m_obj["entity_id"].string();
+        }
+        const char* getProtocol() const {
+            return m_obj["protocol"].string();
+        }
+        const char* getAuthnInstant() const {
+            return m_obj["authn_instant"].string();
+        }
+        const char* getSessionIndex() const {
+            return m_obj["session_index"].string();
+        }
+        const char* getAuthnContextClassRef() const {
+            return m_obj["authncontext_class"].string();
+        }
+        const char* getAuthnContextDeclRef() const {
+            return m_obj["authncontext_decl"].string();
+        }
+        const vector<Attribute*>& getAttributes() const {
+            if (m_attributes.empty())
+                unmarshallAttributes();
+            return m_attributes;
+        }
+        const multimap<string,const Attribute*>& getIndexedAttributes() const {
+            if (m_attributeIndex.empty()) {
+                if (m_attributes.empty())
+                    unmarshallAttributes();
+                for (vector<Attribute*>::const_iterator a = m_attributes.begin(); a != m_attributes.end(); ++a) {
+                    const vector<string>& aliases = (*a)->getAliases();
+                    for (vector<string>::const_iterator alias = aliases.begin(); alias != aliases.end(); ++alias)
+                        m_attributeIndex.insert(multimap<string,const Attribute*>::value_type(*alias, *a));
+                }
+            }
+            return m_attributeIndex;
+        }
+        const vector<const char*>& getAssertionIDs() const {
+            if (m_ids.empty()) {
+                DDF ids = m_obj["assertions"];
+                DDF id = ids.first();
+                while (id.isstring()) {
+                    m_ids.push_back(id.string());
+                    id = ids.next();
+                }
+            }
+            return m_ids;
+        }
+        
+        time_t expires() const { return m_expires; }
+        time_t lastAccess() const { return m_lastAccess; }
+        void validate(const Application& application, const char* client_addr, time_t* timeout);
+
+    private:
+        void unmarshallAttributes() const;
+
+        int m_version;
+        mutable DDF m_obj;
+        mutable vector<Attribute*> m_attributes;
+        mutable multimap<string,const Attribute*> m_attributeIndex;
+        mutable vector<const char*> m_ids;
+        time_t m_expires,m_lastAccess;
+        RemotedCache* m_cache;
+        Mutex* m_lock;
+    };
+    
+    class RemotedCache : public SessionCache
+    {
+    public:
+        RemotedCache(const DOMElement* e);
+        ~RemotedCache();
+    
+        Session* find(const char* key, const Application& application, const char* client_addr=NULL, time_t* timeout=NULL);
+        void remove(const char* key, const Application& application);
+        
+        void cleanup();
+    
+        Category& m_log;
+    private:
+        const DOMElement* m_root;         // Only valid during initialization
+        RWLock* m_lock;
+        map<string,RemotedSession*> m_hashtable;
+    
+        void dormant(const char* key);
+        static void* cleanup_fn(void*);
+        bool shutdown;
+        CondWait* shutdown_wait;
+        Thread* cleanup_thread;
+    };
+
+    SessionCache* SHIBSP_DLLLOCAL RemotedCacheFactory(const DOMElement* const & e)
+    {
+        return new RemotedCache(e);
+    }
+}
+
+void RemotedSession::unmarshallAttributes() const
+{
+    Attribute* attribute;
+    DDF attrs = m_obj["attributes"];
+    DDF attr = attrs.first();
+    while (!attr.isnull()) {
+        try {
+            attribute = Attribute::unmarshall(attr);
+            m_attributes.push_back(attribute);
+            if (m_cache->m_log.isDebugEnabled())
+                m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",
+                    attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");
+        }
+        catch (AttributeException& ex) {
+            const char* id = attr.first().name();
+            m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());
+        }
+        attr = attrs.next();
+    }
+}
+
+void RemotedSession::validate(const Application& application, const char* client_addr, time_t* timeout)
+{
+    // Basic expiration?
+    time_t now = time(NULL);
+    if (now > m_expires) {
+        m_cache->m_log.info("session expired (ID: %s)", getID());
+        throw opensaml::RetryableProfileException("Your session has expired, and you must re-authenticate.");
+    }
+
+    // Address check?
+    if (client_addr) {
+        if (m_cache->m_log.isDebugEnabled())
+            m_cache->m_log.debug("comparing client address %s against %s", client_addr, getClientAddress());
+        if (strcmp(getClientAddress(),client_addr)) {
+            m_cache->m_log.warn("client address mismatch");
+            throw opensaml::RetryableProfileException(
+                "Your IP address ($1) does not match the address recorded at the time the session was established.",
+                params(1,client_addr)
+                );
+        }
+    }
+
+    if (!timeout)
+        return;
+    
+    DDF in("touch::"REMOTED_SESSION_CACHE"::SessionCache"), out;
+    DDFJanitor jin(in);
+    in.structure();
+    in.addmember("key").string(getID());
+    in.addmember("version").integer(m_obj["version"].integer());
+    if (*timeout) {
+        // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  
+#ifndef HAVE_GMTIME_R
+        struct tm* ptime=gmtime(timeout);
+#else
+        struct tm res;
+        struct tm* ptime=gmtime_r(timeout,&res);
+#endif
+        char timebuf[32];
+        strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
+        in.addmember("timeout").string(timebuf);
+    }
+
+    try {
+        out=application.getServiceProvider().getListenerService()->send(in);
+    }
+    catch (...) {
+        out.destroy();
+        throw;
+    }
+
+    if (out.isstruct()) {
+        // We got an updated record back.
+        m_ids.clear();
+        for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
+        m_attributes.clear();
+        m_attributeIndex.clear();
+        m_obj.destroy();
+        m_obj = out;
+    }
+
+    m_lastAccess = now;
+}
+
+RemotedCache::RemotedCache(const DOMElement* e)
+    : SessionCache(e, 900), m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), m_root(e), m_lock(NULL), shutdown(false)
+{
+    if (!SPConfig::getConfig().getServiceProvider()->getListenerService())
+        throw ConfigurationException("RemotedCacheService requires a ListenerService, but none available.");
+        
+    m_lock = RWLock::create();
+    shutdown_wait = CondWait::create();
+    cleanup_thread = Thread::create(&cleanup_fn, (void*)this);
+}
+
+RemotedCache::~RemotedCache()
+{
+    // Shut down the cleanup thread and let it know...
+    shutdown = true;
+    shutdown_wait->signal();
+    cleanup_thread->join(NULL);
+
+    for_each(m_hashtable.begin(),m_hashtable.end(),xmltooling::cleanup_pair<string,RemotedSession>());
+    delete m_lock;
+    delete shutdown_wait;
+}
+
+Session* RemotedCache::find(const char* key, const Application& application, const char* client_addr, time_t* timeout)
+{
+#ifdef _DEBUG
+    xmltooling::NDC ndc("find");
+#endif
+
+    RemotedSession* session=NULL;
+    m_log.debug("searching local cache for session (%s)", key);
+    m_lock->rdlock();
+    map<string,RemotedSession*>::const_iterator i=m_hashtable.find(key);
+    if (i==m_hashtable.end()) {
+        m_lock->unlock();
+        m_log.debug("session not found locally, searching remote cache");
+
+        DDF in("find::"REMOTED_SESSION_CACHE"::SessionCache"), out;
+        DDFJanitor jin(in);
+        in.structure();
+        in.addmember("key").string(key);
+        in.addmember("application_id").string(application.getId());
+        if (timeout && *timeout) {
+            // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  
+#ifndef HAVE_GMTIME_R
+            struct tm* ptime=gmtime(timeout);
+#else
+            struct tm res;
+            struct tm* ptime=gmtime_r(timeout,&res);
+#endif
+            char timebuf[32];
+            strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
+            in.addmember("timeout").string(timebuf);
+        }
+        
+        try {
+            out=application.getServiceProvider().getListenerService()->send(in);
+            if (!out.isstruct()) {
+                out.destroy();
+                m_log.debug("session not found in remote cache");
+                return NULL;
+            }
+            
+            // Wrap the results in a local entry and save it.
+            session = new RemotedSession(this, out);
+            // The remote end has handled timeout issues, we handle address and expiration checks.
+            timeout = NULL;
+        }
+        catch (...) {
+            out.destroy();
+            throw;
+        }
+
+        // Lock for writing and repeat the search to avoid duplication.
+        m_lock->wrlock();
+        SharedLock shared(m_lock, false);
+        if (m_hashtable.count(key)) {
+            // We're using an existing session entry.
+            delete session;
+            session = m_hashtable[key];
+            session->lock();
+        }
+        else {
+            m_hashtable[key]=session;
+            session->lock();
+        }
+    }
+    else {
+        // Save off and lock the session.
+        session = i->second;
+        session->lock();
+        m_lock->unlock();
+        
+        m_log.debug("session found locally, validating it for use");
+    }
+
+    if (!XMLString::equals(session->getApplicationID(), application.getId())) {
+        m_log.error("an application (%s) tried to access another application's session", application.getId());
+        session->unlock();
+        return NULL;
+    }
+
+    // Verify currency and update the timestamp if indicated by caller.
+    try {
+        session->validate(application, client_addr, timeout);
+    }
+    catch (...) {
+        session->unlock();
+        remove(key, application);
+        throw;
+    }
+    
+    return session;
+}
+
+void RemotedCache::remove(const char* key, const Application& application)
+{
+    // Take care of local copy.
+    dormant(key);
+    
+    // Now remote...
+    DDF in("remove::"REMOTED_SESSION_CACHE"::SessionCache");
+    DDFJanitor jin(in);
+    in.structure();
+    in.addmember("key").string(key);
+    in.addmember("application_id").string(application.getId());
+    
+    DDF out = application.getServiceProvider().getListenerService()->send(in);
+    out.destroy();
+}
+
+void RemotedCache::dormant(const char* key)
+{
+#ifdef _DEBUG
+    xmltooling::NDC ndc("dormant");
+#endif
+
+    m_log.debug("deleting local copy of session (%s)", key);
+
+    // lock the cache for writing, which means we know nobody is sitting in find()
+    m_lock->wrlock();
+
+    // grab the entry from the table
+    map<string,RemotedSession*>::const_iterator i=m_hashtable.find(key);
+    if (i==m_hashtable.end()) {
+        m_lock->unlock();
+        return;
+    }
+
+    // ok, remove the entry and lock it
+    RemotedSession* entry=i->second;
+    m_hashtable.erase(key);
+    entry->lock();
+    
+    // unlock the cache
+    m_lock->unlock();
+
+    // we can release the cache entry lock because we know we're not in the cache anymore
+    entry->unlock();
+
+    delete entry;
+}
+
+void RemotedCache::cleanup()
+{
+#ifdef _DEBUG
+    xmltooling::NDC ndc("cleanup");
+#endif
+
+    Mutex* mutex = Mutex::create();
+  
+    // Load our configuration details...
+    static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);
+    const XMLCh* tag=m_root ? m_root->getAttributeNS(NULL,cleanupInterval) : NULL;
+    int rerun_timer = 900;
+    if (tag && *tag)
+        rerun_timer = XMLString::parseInt(tag);
+    if (rerun_timer <= 0)
+        rerun_timer = 900;
+
+    mutex->lock();
+
+    m_log.info("cleanup thread started...run every %d secs; timeout after %d secs", rerun_timer, m_cacheTimeout);
+
+    while (!shutdown) {
+        shutdown_wait->timedwait(mutex,rerun_timer);
+        if (shutdown)
+            break;
+
+        // Ok, let's run through the cleanup process and clean out
+        // really old sessions.  This is a two-pass process.  The
+        // first pass is done holding a read-lock while we iterate over
+        // the cache.  The second pass doesn't need a lock because
+        // the 'deletes' will lock the cache.
+    
+        // Pass 1: iterate over the map and find all entries that have not been
+        // used in X hours
+        vector<string> stale_keys;
+        time_t stale = time(NULL) - m_cacheTimeout;
+    
+        m_log.debug("cleanup thread running");
+
+        m_lock->rdlock();
+        for (map<string,RemotedSession*>::const_iterator i=m_hashtable.begin(); i!=m_hashtable.end(); ++i) {
+            // If the last access was BEFORE the stale timeout...
+            i->second->lock();
+            time_t last=i->second->lastAccess();
+            i->second->unlock();
+            if (last < stale)
+                stale_keys.push_back(i->first);
+        }
+        m_lock->unlock();
+    
+        if (!stale_keys.empty()) {
+            m_log.info("purging %d old sessions", stale_keys.size());
+    
+            // Pass 2: walk through the list of stale entries and remove them from the cache
+            for (vector<string>::const_iterator j = stale_keys.begin(); j != stale_keys.end(); ++j)
+                dormant(j->c_str());
+        }
+
+        m_log.debug("cleanup thread completed");
+    }
+
+    m_log.info("cleanup thread exiting");
+
+    mutex->unlock();
+    delete mutex;
+    Thread::exit(NULL);
+}
+
+void* RemotedCache::cleanup_fn(void* cache_p)
+{
+    RemotedCache* cache = reinterpret_cast<RemotedCache*>(cache_p);
+
+#ifndef WIN32
+    // First, let's block all signals 
+    Thread::mask_all_signals();
+#endif
+
+    // Now run the cleanup process.
+    cache->cleanup();
+    return NULL;
+}