Adjust logging/error-handling, schema fixes, failed message handling.
[shibboleth/sp.git] / shibsp / impl / RemotedSessionCache.cpp
index 350b2e7..a64222d 100644 (file)
@@ -1,5 +1,5 @@
 /*\r
- *  Copyright 2001-2005 Internet2\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
 #include "exceptions.h"\r
 #include "ServiceProvider.h"\r
 #include "SessionCache.h"\r
+#include "TransactionLog.h"\r
 #include "attribute/Attribute.h"\r
 #include "remoting/ListenerService.h"\r
 #include "util/SPConstants.h"\r
 \r
 #include <sstream>\r
+#include <log4cpp/Category.hh>\r
 #include <xmltooling/XMLToolingConfig.h>\r
+#include <xmltooling/util/NDC.h>\r
 #include <xmltooling/util/XMLHelper.h>\r
 \r
 using namespace shibsp;\r
 using namespace opensaml::saml2md;\r
 using namespace opensaml;\r
 using namespace xmltooling;\r
+using namespace log4cpp;\r
 using namespace std;\r
 \r
 namespace shibsp {\r
 \r
+    class RemotedCache;\r
     class RemotedSession : public virtual Session\r
     {\r
     public:\r
-        RemotedSession(const char* key, DDF& obj) : m_key(key), m_obj(obj), m_nameid(NULL) {\r
+        RemotedSession(RemotedCache* cache, DDF& obj) : m_version(obj["version"].integer()), m_obj(obj),\r
+                m_nameid(NULL), m_expires(0), m_lastAccess(time(NULL)), m_cache(cache), m_lock(NULL) {\r
             const char* nameid = obj["nameid"].string();\r
             if (!nameid)\r
                 throw FatalProfileException("NameID missing from remotely cached session.");\r
             \r
-            // Parse and bind the document into an XMLObject.\r
+            // Parse and bind the NameID into an XMLObject.\r
             istringstream instr(nameid);\r
             DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); \r
             XercesJanitor<DOMDocument> janitor(doc);\r
@@ -57,27 +63,38 @@ namespace shibsp {
             n->unmarshall(doc->getDocumentElement(), true);\r
             janitor.release();\r
             \r
-            // TODO: Process attributes...\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
             m_nameid = n.release();\r
         }\r
         \r
         ~RemotedSession() {\r
+            delete m_lock;\r
             m_obj.destroy();\r
             delete m_nameid;\r
-            for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
-            for_each(m_tokens.begin(), m_tokens.end(), xmltooling::cleanup_pair<string,RootObject>());\r
+            for_each(m_attributes.begin(), m_attributes.end(), cleanup_const_pair<string,Attribute>());\r
+            for_each(m_tokens.begin(), m_tokens.end(), cleanup_pair<string,Assertion>());\r
         }\r
         \r
         Lockable* lock() {\r
+            m_lock->lock();\r
             return this;\r
         }\r
         void unlock() {\r
-            delete this;\r
+            m_lock->unlock();\r
+        }\r
+\r
+        const char* getApplicationID() const {\r
+            return m_obj["application_id"].string();\r
         }\r
-        \r
         const char* getClientAddress() const {\r
-            return m_obj["client_address"].string();\r
+            return m_obj["client_addr"].string();\r
         }\r
         const char* getEntityID() const {\r
             return m_obj["entity_id"].string();\r
@@ -97,12 +114,14 @@ namespace shibsp {
         const char* getAuthnContextDeclRef() const {\r
             return m_obj["authncontext_decl"].string();\r
         }\r
-        const vector<const Attribute*>& getAttributes() const {\r
+        const map<string,const Attribute*>& getAttributes() const {\r
+            if (m_attributes.empty())\r
+                unmarshallAttributes();\r
             return m_attributes;\r
         }\r
         const vector<const char*>& getAssertionIDs() const {\r
             if (m_ids.empty()) {\r
-                DDF id = m_obj["assertion_ids"].first();\r
+                DDF id = m_obj["assertions"].first();\r
                 while (id.isstring()) {\r
                     m_ids.push_back(id.string());\r
                     id = id.next();\r
@@ -111,24 +130,38 @@ namespace shibsp {
             return m_ids;\r
         }\r
         \r
-        void addAttributes(const vector<Attribute*>& attributes);\r
-        const RootObject* getAssertion(const char* id) const;\r
-        void addAssertion(RootObject* assertion);\r
+        const Assertion* getAssertion(const char* id) const;\r
+\r
+        void addAttributes(const vector<Attribute*>& attributes) {\r
+            throw ConfigurationException("addAttributes method not implemented by this session cache plugin.");\r
+        }\r
+        void addAssertion(Assertion* assertion) {\r
+            throw ConfigurationException("addAssertion method not implemented by this session cache plugin.");\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, bool local=true);\r
 \r
     private:\r
-        string m_key;\r
+        void unmarshallAttributes() const;\r
+\r
+        int m_version;\r
         mutable DDF m_obj;\r
         saml2::NameID* m_nameid;\r
-        vector<const Attribute*> m_attributes;\r
+        mutable map<string,const Attribute*> m_attributes;\r
         mutable vector<const char*> m_ids;\r
-        mutable map<string,RootObject*> m_tokens;\r
+        mutable map<string,Assertion*> m_tokens;\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
+        ~RemotedCache();\r
     \r
         string insert(\r
             time_t expires,\r
@@ -140,11 +173,25 @@ namespace shibsp {
             const char* session_index=NULL,\r
             const char* authncontext_class=NULL,\r
             const char* authncontext_decl=NULL,\r
-            const RootObject* ssoToken=NULL,\r
+            const vector<const Assertion*>* tokens=NULL,\r
             const vector<Attribute*>* attributes=NULL\r
             );\r
         Session* find(const char* key, const Application& application, const char* client_addr=NULL, time_t timeout=0);\r
         void remove(const char* key, const Application& application, const char* client_addr);\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
@@ -153,56 +200,52 @@ namespace shibsp {
     }\r
 }\r
 \r
-void RemotedSession::addAttributes(const vector<Attribute*>& attributes)\r
+void RemotedSession::unmarshallAttributes() const\r
 {\r
-    DDF in("addAttributes::"REMOTED_SESSION_CACHE);\r
-    DDFJanitor jin(in);\r
-    in.structure();\r
-    in.addmember("key").string(m_key.c_str());\r
-\r
-    DDF attr;\r
-    DDF attrs = in.addmember("attributes").list();\r
-    for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a) {\r
-        attr = (*a)->marshall();\r
-        attrs.add(attr);\r
+    Attribute* attribute;\r
+    DDF attr = m_obj["attributes"].first();\r
+    while (!attr.isnull()) {\r
+        try {\r
+            attribute = Attribute::unmarshall(attr);\r
+            m_attributes[attribute->getId()] = 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 = attr.next();\r
     }\r
-\r
-    attr=SPConfig::getConfig().getServiceProvider()->getListenerService()->send(in);\r
-    DDFJanitor jout(attr);\r
-    \r
-    // Transfer ownership to us.\r
-    m_attributes.insert(m_attributes.end(), attributes.begin(), attributes.end());\r
 }\r
 \r
-const RootObject* RemotedSession::getAssertion(const char* id) const\r
+const Assertion* RemotedSession::getAssertion(const char* id) const\r
 {\r
-    map<string,RootObject*>::const_iterator i = m_tokens.find(id);\r
+    map<string,Assertion*>::const_iterator i = m_tokens.find(id);\r
     if (i!=m_tokens.end())\r
         return i->second;\r
-    \r
-    DDF in("getAssertion::"REMOTED_SESSION_CACHE);\r
+\r
+    // Fetch from remoted cache.\r
+    DDF in("getAssertion::"REMOTED_SESSION_CACHE"::SessionCache");\r
     DDFJanitor jin(in);\r
     in.structure();\r
-    in.addmember("key").string(m_key.c_str());\r
-    in.addmember("assertion_id").string(id);\r
+    in.addmember("key").string(m_obj.name());\r
+    in.addmember("id").string(id);\r
 \r
-    DDF out = SPConfig::getConfig().getServiceProvider()->getListenerService()->send(in);\r
+    DDF out=SPConfig::getConfig().getServiceProvider()->getListenerService()->send(in);\r
     DDFJanitor jout(out);\r
     \r
-    const char* tokenstr = out["assertion"].string();\r
-    if (!tokenstr)\r
-        return NULL;\r
-    \r
     // Parse and bind the document into an XMLObject.\r
-    istringstream instr(tokenstr);\r
+    istringstream instr(out.string());\r
     DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); \r
     XercesJanitor<DOMDocument> janitor(doc);\r
     auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));\r
     janitor.release();\r
     \r
-    RootObject* token = dynamic_cast<RootObject*>(xmlObject.get());\r
-    if (!token || !token->isAssertion())\r
-        throw FatalProfileException("Remoted call for cached assertion returned an unknown object type.");\r
+    Assertion* token = dynamic_cast<Assertion*>(xmlObject.get());\r
+    if (!token)\r
+        throw FatalProfileException("Cached assertion was of an unknown object type.");\r
 \r
     // Transfer ownership to us.\r
     xmlObject.release();\r
@@ -210,29 +253,100 @@ const RootObject* RemotedSession::getAssertion(const char* id) const
     return token;\r
 }\r
 \r
-void RemotedSession::addAssertion(RootObject* assertion)\r
+void RemotedSession::validate(const Application& application, const char* client_addr, time_t timeout, bool local)\r
 {\r
-    if (!assertion || !assertion->isAssertion())\r
-        throw FatalProfileException("Unknown object type passed to session cache for storage.");\r
+    // Basic expiration?\r
+    time_t now = time(NULL);\r
+    if (now > m_expires) {\r
+        m_cache->m_log.info("session expired (ID: %s)", m_obj.name());\r
+        RetryableProfileException ex("Your session has expired, and you must re-authenticate.");\r
+        if (!getEntityID())\r
+            throw ex;\r
+        MetadataProvider* m=application.getMetadataProvider();\r
+        Locker locker(m);\r
+        annotateException(&ex,m->getEntityDescriptor(getEntityID(),false)); // throws it\r
+    }\r
 \r
-    DDF in("addAssertion::"REMOTED_SESSION_CACHE);\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
+            RetryableProfileException ex(\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
+            if (!getEntityID())\r
+                throw ex;\r
+            MetadataProvider* m=application.getMetadataProvider();\r
+            Locker locker(m);\r
+            annotateException(&ex,m->getEntityDescriptor(getEntityID(),false)); // throws it\r
+        }\r
+    }\r
+\r
+    if (local)\r
+        return;\r
+    \r
+    DDF in("touch::"REMOTED_SESSION_CACHE"::SessionCache"), out;\r
     DDFJanitor jin(in);\r
     in.structure();\r
-    in.addmember("key").string(m_key.c_str());\r
-    \r
-    ostringstream os;\r
-    os << *assertion;\r
-    in.addmember("assertion").string(os.str().c_str());\r
+    in.addmember("key").string(m_obj.name());\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
-    DDF out = SPConfig::getConfig().getServiceProvider()->getListenerService()->send(in);\r
-    out.destroy();\r
-    delete assertion;\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(), cleanup_const_pair<string,Attribute>());\r
+        m_attributes.clear();\r
+        m_obj.destroy();\r
+        m_obj = out;\r
+    }\r
+\r
+    m_lastAccess = now;\r
 }\r
 \r
-RemotedCache::RemotedCache(const DOMElement* e) : SessionCache(e)\r
+RemotedCache::RemotedCache(const DOMElement* e)\r
+    : SessionCache(e), 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
 string RemotedCache::insert(\r
@@ -245,11 +359,11 @@ string RemotedCache::insert(
     const char* session_index,\r
     const char* authncontext_class,\r
     const char* authncontext_decl,\r
-    const RootObject* ssoToken,\r
+    const vector<const Assertion*>* tokens,\r
     const vector<Attribute*>* attributes\r
     )\r
 {\r
-    DDF in("insert::"REMOTED_SESSION_CACHE);\r
+    DDF in("insert::"REMOTED_SESSION_CACHE"::SessionCache");\r
     DDFJanitor jin(in);\r
     in.structure();\r
     if (expires) {\r
@@ -264,7 +378,7 @@ string RemotedCache::insert(
         in.addmember("expires").string(timebuf);\r
     }\r
     in.addmember("application_id").string(application.getId());\r
-    in.addmember("client_address").string(client_addr);\r
+    in.addmember("client_addr").string(client_addr);\r
     if (issuer) {\r
         auto_ptr_char provid(issuer->getEntityID());\r
         in.addmember("entity_id").string(provid.get());\r
@@ -282,12 +396,18 @@ string RemotedCache::insert(
     namestr << nameid;\r
     in.addmember("nameid").string(namestr.str().c_str());\r
 \r
-    if (ssoToken) {\r
-        ostringstream tokenstr;\r
-        tokenstr << *ssoToken;\r
-        auto_ptr_char tokenid(ssoToken->getID());\r
-        in.addmember("assertion_ids").list().add(DDF(NULL).string(tokenid.get()));\r
-        in.addmember("assertions").list().add(DDF(NULL).string(tokenstr.str().c_str()));\r
+    if (tokens) {\r
+        in.addmember("assertions").list();\r
+        in.addmember("tokens").list();\r
+        for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {\r
+            ostringstream tokenstr;\r
+            tokenstr << *(*t);\r
+            auto_ptr_char tokenid((*t)->getID());\r
+            DDF tokid = DDF(NULL).string(tokenid.get());\r
+            in["assertions"].add(tokid);\r
+            DDF tok = DDF(tokenid.get()).string(tokenstr.str().c_str());\r
+            in["tokens"].add(tok);\r
+        }\r
     }\r
     \r
     if (attributes) {\r
@@ -299,10 +419,40 @@ string RemotedCache::insert(
         }\r
     }\r
 \r
-    DDF out=SPConfig::getConfig().getServiceProvider()->getListenerService()->send(in);\r
+    DDF out=application.getServiceProvider().getListenerService()->send(in);\r
     DDFJanitor jout(out);\r
     if (out["key"].isstring()) {\r
-        for_each(attributes->begin(), attributes->end(), xmltooling::cleanup<Attribute>());\r
+        // Transaction Logging\r
+        auto_ptr_char name(nameid.getName());\r
+        const char* pid = in["entity_id"].string();\r
+        TransactionLog* xlog = application.getServiceProvider().getTransactionLog();\r
+        Locker locker(xlog);\r
+        xlog->log.infoStream() <<\r
+            "New session (ID: " <<\r
+                out["key"].string() <<\r
+            ") with (applicationId: " <<\r
+                application.getId() <<\r
+            ") for principal from (IdP: " <<\r
+                (pid ? pid : "none") <<\r
+            ") at (ClientAddress: " <<\r
+                client_addr <<\r
+            ") with (NameIdentifier: " <<\r
+                name.get() <<\r
+            ")";\r
+\r
+        if (attributes) {\r
+            xlog->log.infoStream() <<\r
+                "Cached the following attributes with session (ID: " <<\r
+                    out["key"].string() <<\r
+                ") for (applicationId: " <<\r
+                    application.getId() <<\r
+                ") {";\r
+            for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a)\r
+                xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";\r
+            xlog->log.info("}");\r
+            for_each(attributes->begin(), attributes->end(), xmltooling::cleanup<Attribute>());\r
+        }\r
+\r
         return out["key"].string();\r
     }\r
     throw RetryableProfileException("A remoted cache insertion operation did not return a usable session key.");\r
@@ -310,38 +460,275 @@ string RemotedCache::insert(
 \r
 Session* RemotedCache::find(const char* key, const Application& application, const char* client_addr, time_t timeout)\r
 {\r
-    DDF in("find::"REMOTED_SESSION_CACHE), out;\r
-    DDFJanitor jin(in);\r
-    in.structure();\r
-    in.addmember("key").string(key);\r
-    in.addmember("application_id").string(application.getId());\r
-    in.addmember("client_address").string(client_addr);\r
-    \r
-    try {\r
-        out=SPConfig::getConfig().getServiceProvider()->getListenerService()->send(in);\r
-        if (!out.isstruct()) {\r
+#ifdef _DEBUG\r
+    xmltooling::NDC ndc("find");\r
+#endif\r
+\r
+    bool localValidation = false;\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
+        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
+            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
+            localValidation = true;\r
+        }\r
+        catch (...) {\r
             out.destroy();\r
-            return NULL;\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
+            delete session;\r
+            // We're using an existing session entry, so we have to switch back to full validation.\r
+            localValidation = false;\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
-        // Wrap the results in a stub entry and return it to the caller.\r
-        return new RemotedSession(key, out);\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.\r
+    // If the local switch is false, we also update the access time.\r
+    try {\r
+        session->validate(application, client_addr, timeout, localValidation);\r
     }\r
     catch (...) {\r
-        out.destroy();\r
+        session->unlock();\r
+        remove(key, application, client_addr);\r
         throw;\r
     }\r
+    \r
+    return session;\r
 }\r
 \r
 void RemotedCache::remove(const char* key, const Application& application, const char* client_addr)\r
 {\r
-    DDF in("remove::"REMOTED_SESSION_CACHE);\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
-    in.addmember("client_address").string(client_addr);\r
+    in.addmember("client_addr").string(client_addr);\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_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
+\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
+\r
+/* These are currently unimplemented.\r
+\r
+void RemotedSession::addAttributes(const vector<Attribute*>& attributes)\r
+{\r
+    DDF in("addAttributes::"REMOTED_SESSION_CACHE);\r
+    DDFJanitor jin(in);\r
+    in.structure();\r
+    in.addmember("key").string(m_key.c_str());\r
+    in.addmember("application_id").string(m_appId.c_str());\r
+\r
+    DDF attr;\r
+    DDF attrs = in.addmember("attributes").list();\r
+    for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a) {\r
+        attr = (*a)->marshall();\r
+        attrs.add(attr);\r
+    }\r
+\r
+    attr=SPConfig::getConfig().getServiceProvider()->getListenerService()->send(in);\r
+    DDFJanitor jout(attr);\r
+    \r
+    // Transfer ownership to us.\r
+    m_attributes.insert(m_attributes.end(), attributes.begin(), attributes.end());\r
+}\r
+\r
+void RemotedSession::addAssertion(Assertion* assertion)\r
+{\r
+    if (!assertion)\r
+        throw FatalProfileException("Unknown object type passed to session cache for storage.");\r
+\r
+    DDF in("addAssertion::"REMOTED_SESSION_CACHE);\r
+    DDFJanitor jin(in);\r
+    in.structure();\r
+    in.addmember("key").string(m_key.c_str());\r
+    in.addmember("application_id").string(m_appId.c_str());\r
+    \r
+    ostringstream os;\r
+    os << *assertion;\r
+    string token(os.str());\r
+    auto_ptr_char tokenid(assertion->getID());\r
+    in.addmember("assertion_id").string(tokenid.get());\r
+    in.addmember("assertion").string(token.c_str());\r
+\r
     DDF out = SPConfig::getConfig().getServiceProvider()->getListenerService()->send(in);\r
     out.destroy();\r
+    \r
+    // Add to local record and token map.\r
+    // Next attempt to find and lock session will refresh from remote store anyway.\r
+    m_obj["assertions"].addmember(tokenid.get()).string(token.c_str());\r
+    m_ids.clear();\r
+    m_tokens[tokenid.get()] = assertion;\r
 }\r
+\r
+*/
\ No newline at end of file