Imported Upstream version 2.2.1+dfsg
[shibboleth/sp.git] / shibsp / impl / StorageServiceSessionCache.cpp
1 /*
2  *  Copyright 2001-2007 Internet2
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /**
18  * StorageServiceSessionCache.cpp
19  *
20  * StorageService-based SessionCache implementation.
21  *
22  * Instead of optimizing this plugin with a buffering scheme that keeps objects around
23  * and avoids extra parsing steps, I'm assuming that systems that require such can
24  * layer their own cache plugin on top of this version either by delegating to it
25  * or using the remoting support. So this version will load sessions directly
26  * from the StorageService, instantiate enough to expose the Session API,
27  * and then delete everything when they're unlocked. All data in memory is always
28  * kept in sync with the StorageService (no lazy updates).
29  */
30
31 #include "internal.h"
32 #include "Application.h"
33 #include "exceptions.h"
34 #include "ServiceProvider.h"
35 #include "SessionCacheEx.h"
36 #include "TransactionLog.h"
37 #include "attribute/Attribute.h"
38 #include "remoting/ListenerService.h"
39 #include "util/SPConstants.h"
40
41 #include <xmltooling/util/NDC.h>
42 #include <xmltooling/util/XMLHelper.h>
43 #include <xercesc/util/XMLUniDefs.hpp>
44
45 #ifndef SHIBSP_LITE
46 # include <saml/SAMLConfig.h>
47 # include <xmltooling/util/StorageService.h>
48 using namespace opensaml::saml2md;
49 #else
50 # include <ctime>
51 # include <xmltooling/util/DateTime.h>
52 #endif
53
54 using namespace shibsp;
55 using namespace opensaml;
56 using namespace xmltooling;
57 using namespace std;
58
59 namespace shibsp {
60
61     class StoredSession;
62     class SSCache : public SessionCacheEx
63 #ifndef SHIBSP_LITE
64         ,public virtual Remoted
65 #endif
66     {
67     public:
68         SSCache(const DOMElement* e);
69         ~SSCache();
70
71 #ifndef SHIBSP_LITE
72         void receive(DDF& in, ostream& out);
73
74         void insert(
75             const Application& application,
76             const HTTPRequest& httpRequest,
77             HTTPResponse& httpResponse,
78             time_t expires,
79             const saml2md::EntityDescriptor* issuer=NULL,
80             const XMLCh* protocol=NULL,
81             const saml2::NameID* nameid=NULL,
82             const XMLCh* authn_instant=NULL,
83             const XMLCh* session_index=NULL,
84             const XMLCh* authncontext_class=NULL,
85             const XMLCh* authncontext_decl=NULL,
86             const vector<const Assertion*>* tokens=NULL,
87             const vector<Attribute*>* attributes=NULL
88             );
89         vector<string>::size_type logout(
90             const Application& application,
91             const saml2md::EntityDescriptor* issuer,
92             const saml2::NameID& nameid,
93             const set<string>* indexes,
94             time_t expires,
95             vector<string>& sessions
96             );
97         bool matches(
98             const Application& application,
99             const xmltooling::HTTPRequest& request,
100             const saml2md::EntityDescriptor* issuer,
101             const saml2::NameID& nameid,
102             const set<string>* indexes
103             );
104 #endif
105         Session* find(const Application& application, const char* key, const char* client_addr=NULL, time_t* timeout=NULL);
106         void remove(const Application& application, const char* key);
107         void test();
108
109         string active(const Application& application, const xmltooling::HTTPRequest& request) {
110             pair<string,const char*> shib_cookie = application.getCookieNameProps("_shibsession_");
111             const char* session_id = request.getCookie(shib_cookie.first.c_str());
112             return (session_id ? session_id : "");
113         }
114
115         Session* find(const Application& application, const HTTPRequest& request, const char* client_addr=NULL, time_t* timeout=NULL) {
116             string id = active(application, request);
117             if (!id.empty())
118                 return find(application, id.c_str(), client_addr, timeout);
119             return NULL;
120         }
121
122         Session* find(const Application& application, HTTPRequest& request, const char* client_addr=NULL, time_t* timeout=NULL) {
123             string id = active(application, request);
124             if (id.empty())
125                 return NULL;
126             try {
127                 Session* session = find(application, id.c_str(), client_addr, timeout);
128                 if (session)
129                     return session;
130                 HTTPResponse* response = dynamic_cast<HTTPResponse*>(&request);
131                 if (response) {
132                     pair<string,const char*> shib_cookie = application.getCookieNameProps("_shibsession_");
133                     string exp(shib_cookie.second);
134                     exp += "; expires=Mon, 01 Jan 2001 00:00:00 GMT";
135                     response->setCookie(shib_cookie.first.c_str(), exp.c_str());
136                 }
137             }
138             catch (exception&) {
139                 HTTPResponse* response = dynamic_cast<HTTPResponse*>(&request);
140                 if (response) {
141                     pair<string,const char*> shib_cookie = application.getCookieNameProps("_shibsession_");
142                     string exp(shib_cookie.second);
143                     exp += "; expires=Mon, 01 Jan 2001 00:00:00 GMT";
144                     response->setCookie(shib_cookie.first.c_str(), exp.c_str());
145                 }
146                 throw;
147             }
148             return NULL;
149         }
150
151         void remove(const Application& application, const HTTPRequest& request, HTTPResponse* response=NULL) {
152             pair<string,const char*> shib_cookie = application.getCookieNameProps("_shibsession_");
153             const char* session_id = request.getCookie(shib_cookie.first.c_str());
154             if (session_id && *session_id) {
155                 if (response) {
156                     string exp(shib_cookie.second);
157                     exp += "; expires=Mon, 01 Jan 2001 00:00:00 GMT";
158                     response->setCookie(shib_cookie.first.c_str(), exp.c_str());
159                 }
160                 remove(application, session_id);
161             }
162         }
163
164         void cleanup();
165
166         Category& m_log;
167         bool inproc;
168         unsigned long m_cacheTimeout;
169 #ifndef SHIBSP_LITE
170         StorageService* m_storage;
171         StorageService* m_storage_lite;
172 #endif
173
174     private:
175 #ifndef SHIBSP_LITE
176         // maintain back-mappings of NameID/SessionIndex -> session key
177         void insert(const char* key, time_t expires, const char* name, const char* index);
178         bool stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const;
179 #endif
180
181         const DOMElement* m_root;         // Only valid during initialization
182         unsigned long m_inprocTimeout;
183
184         // inproc means we buffer sessions in memory
185         RWLock* m_lock;
186         map<string,StoredSession*> m_hashtable;
187
188         // management of buffered sessions
189         void dormant(const char* key);
190         static void* cleanup_fn(void*);
191         bool shutdown;
192         CondWait* shutdown_wait;
193         Thread* cleanup_thread;
194     };
195
196     class StoredSession : public virtual Session
197     {
198     public:
199         StoredSession(SSCache* cache, DDF& obj) : m_obj(obj),
200 #ifndef SHIBSP_LITE
201                 m_nameid(NULL),
202 #endif
203                 m_cache(cache), m_expires(0), m_lastAccess(time(NULL)), m_lock(NULL) {
204             auto_ptr_XMLCh exp(m_obj["expires"].string());
205             if (exp.get()) {
206                 DateTime iso(exp.get());
207                 iso.parseDateTime();
208                 m_expires = iso.getEpoch();
209             }
210
211 #ifndef SHIBSP_LITE
212             const char* nameid = obj["nameid"].string();
213             if (nameid) {
214                 // Parse and bind the document into an XMLObject.
215                 istringstream instr(nameid);
216                 DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr);
217                 XercesJanitor<DOMDocument> janitor(doc);
218                 auto_ptr<saml2::NameID> n(saml2::NameIDBuilder::buildNameID());
219                 n->unmarshall(doc->getDocumentElement(), true);
220                 janitor.release();
221                 m_nameid = n.release();
222             }
223 #endif
224             if (cache->inproc)
225                 m_lock = Mutex::create();
226         }
227
228         ~StoredSession() {
229             delete m_lock;
230             m_obj.destroy();
231             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
232 #ifndef SHIBSP_LITE
233             delete m_nameid;
234             for_each(m_tokens.begin(), m_tokens.end(), cleanup_pair<string,Assertion>());
235 #endif
236         }
237
238         Lockable* lock() {
239             if (m_lock)
240                 m_lock->lock();
241             return this;
242         }
243         void unlock() {
244             if (m_lock)
245                 m_lock->unlock();
246             else
247                 delete this;
248         }
249
250         const char* getID() const {
251             return m_obj.name();
252         }
253         const char* getApplicationID() const {
254             return m_obj["application_id"].string();
255         }
256         const char* getClientAddress() const {
257             return m_obj["client_addr"].string();
258         }
259         const char* getEntityID() const {
260             return m_obj["entity_id"].string();
261         }
262         const char* getProtocol() const {
263             return m_obj["protocol"].string();
264         }
265         const char* getAuthnInstant() const {
266             return m_obj["authn_instant"].string();
267         }
268 #ifndef SHIBSP_LITE
269         const saml2::NameID* getNameID() const {
270             return m_nameid;
271         }
272 #endif
273         const char* getSessionIndex() const {
274             return m_obj["session_index"].string();
275         }
276         const char* getAuthnContextClassRef() const {
277             return m_obj["authncontext_class"].string();
278         }
279         const char* getAuthnContextDeclRef() const {
280             return m_obj["authncontext_decl"].string();
281         }
282         const vector<Attribute*>& getAttributes() const {
283             if (m_attributes.empty())
284                 unmarshallAttributes();
285             return m_attributes;
286         }
287         const multimap<string,const Attribute*>& getIndexedAttributes() const {
288             if (m_attributeIndex.empty()) {
289                 if (m_attributes.empty())
290                     unmarshallAttributes();
291                 for (vector<Attribute*>::const_iterator a = m_attributes.begin(); a != m_attributes.end(); ++a) {
292                     const vector<string>& aliases = (*a)->getAliases();
293                     for (vector<string>::const_iterator alias = aliases.begin(); alias != aliases.end(); ++alias)
294                         m_attributeIndex.insert(multimap<string,const Attribute*>::value_type(*alias, *a));
295                 }
296             }
297             return m_attributeIndex;
298         }
299         const vector<const char*>& getAssertionIDs() const {
300             if (m_ids.empty()) {
301                 DDF ids = m_obj["assertions"];
302                 DDF id = ids.first();
303                 while (id.isstring()) {
304                     m_ids.push_back(id.string());
305                     id = ids.next();
306                 }
307             }
308             return m_ids;
309         }
310
311         void validate(const Application& application, const char* client_addr, time_t* timeout);
312
313 #ifndef SHIBSP_LITE
314         void addAttributes(const vector<Attribute*>& attributes);
315         const Assertion* getAssertion(const char* id) const;
316         void addAssertion(Assertion* assertion);
317 #endif
318
319         time_t getExpiration() const { return m_expires; }
320         time_t getLastAccess() const { return m_lastAccess; }
321
322     private:
323         void unmarshallAttributes() const;
324
325         DDF m_obj;
326 #ifndef SHIBSP_LITE
327         saml2::NameID* m_nameid;
328         mutable map<string,Assertion*> m_tokens;
329 #endif
330         mutable vector<Attribute*> m_attributes;
331         mutable multimap<string,const Attribute*> m_attributeIndex;
332         mutable vector<const char*> m_ids;
333
334         SSCache* m_cache;
335         time_t m_expires,m_lastAccess;
336         Mutex* m_lock;
337     };
338
339     SessionCache* SHIBSP_DLLLOCAL StorageServiceCacheFactory(const DOMElement* const & e)
340     {
341         return new SSCache(e);
342     }
343 }
344
345 Session* SessionCache::find(const Application& application, HTTPRequest& request, const char* client_addr, time_t* timeout)
346 {
347     return find(application, const_cast<const HTTPRequest&>(request), client_addr, timeout);
348 }
349
350 void SHIBSP_API shibsp::registerSessionCaches()
351 {
352     SPConfig::getConfig().SessionCacheManager.registerFactory(STORAGESERVICE_SESSION_CACHE, StorageServiceCacheFactory);
353 }
354
355 void StoredSession::unmarshallAttributes() const
356 {
357     Attribute* attribute;
358     DDF attrs = m_obj["attributes"];
359     DDF attr = attrs.first();
360     while (!attr.isnull()) {
361         try {
362             attribute = Attribute::unmarshall(attr);
363             m_attributes.push_back(attribute);
364             if (m_cache->m_log.isDebugEnabled())
365                 m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",
366                     attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");
367         }
368         catch (AttributeException& ex) {
369             const char* id = attr.first().name();
370             m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());
371         }
372         attr = attrs.next();
373     }
374 }
375
376 void StoredSession::validate(const Application& application, const char* client_addr, time_t* timeout)
377 {
378     time_t now = time(NULL);
379
380     // Basic expiration?
381     if (m_expires > 0) {
382         if (now > m_expires) {
383             m_cache->m_log.info("session expired (ID: %s)", getID());
384             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
385         }
386     }
387
388     // Address check?
389     if (client_addr) {
390         if (m_cache->m_log.isDebugEnabled())
391             m_cache->m_log.debug("comparing client address %s against %s", client_addr, getClientAddress());
392         if (!XMLString::equals(getClientAddress(),client_addr)) {
393             m_cache->m_log.warn("client address mismatch");
394             throw RetryableProfileException(
395                 "Your IP address ($1) does not match the address recorded at the time the session was established.",
396                 params(1,client_addr)
397                 );
398         }
399     }
400
401     if (!timeout)
402         return;
403
404     if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
405         DDF in("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;
406         DDFJanitor jin(in);
407         in.structure();
408         in.addmember("key").string(getID());
409         in.addmember("version").integer(m_obj["version"].integer());
410         if (*timeout) {
411             // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
412 #ifndef HAVE_GMTIME_R
413             struct tm* ptime=gmtime(timeout);
414 #else
415             struct tm res;
416             struct tm* ptime=gmtime_r(timeout,&res);
417 #endif
418             char timebuf[32];
419             strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
420             in.addmember("timeout").string(timebuf);
421         }
422
423         try {
424             out=application.getServiceProvider().getListenerService()->send(in);
425         }
426         catch (...) {
427             out.destroy();
428             throw;
429         }
430
431         if (out.isstruct()) {
432             // We got an updated record back.
433             m_ids.clear();
434             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
435             m_attributes.clear();
436             m_attributeIndex.clear();
437             m_obj.destroy();
438             m_obj = out;
439         }
440     }
441     else {
442 #ifndef SHIBSP_LITE
443         if (!m_cache->m_storage)
444             throw ConfigurationException("Session touch requires a StorageService.");
445
446         // Do a versioned read.
447         string record;
448         time_t lastAccess;
449         int curver = m_obj["version"].integer();
450         int ver = m_cache->m_storage->readText(getID(), "session", &record, &lastAccess, curver);
451         if (ver == 0) {
452             m_cache->m_log.warn("unsuccessful versioned read of session (ID: %s), cache out of sync?", getID());
453             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
454         }
455
456         // Adjust for expiration to recover last access time and check timeout.
457         lastAccess -= m_cache->m_cacheTimeout;
458         if (*timeout > 0 && now - lastAccess >= *timeout) {
459             m_cache->m_log.info("session timed out (ID: %s)", getID());
460             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
461         }
462
463         // Update storage expiration, if possible.
464         try {
465             m_cache->m_storage->updateContext(getID(), now + m_cache->m_cacheTimeout);
466         }
467         catch (exception& ex) {
468             m_cache->m_log.error("failed to update session expiration: %s", ex.what());
469         }
470
471         if (ver > curver) {
472             // We got an updated record back.
473             DDF newobj;
474             istringstream in(record);
475             in >> newobj;
476             m_ids.clear();
477             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
478             m_attributes.clear();
479             m_attributeIndex.clear();
480             m_obj.destroy();
481             m_obj = newobj;
482         }
483 #else
484         throw ConfigurationException("Session touch requires a StorageService.");
485 #endif
486     }
487
488     m_lastAccess = now;
489 }
490
491 #ifndef SHIBSP_LITE
492
493 void StoredSession::addAttributes(const vector<Attribute*>& attributes)
494 {
495 #ifdef _DEBUG
496     xmltooling::NDC ndc("addAttributes");
497 #endif
498
499     if (!m_cache->m_storage)
500         throw ConfigurationException("Session modification requires a StorageService.");
501
502     m_cache->m_log.debug("adding attributes to session (%s)", getID());
503
504     int ver;
505     do {
506         DDF attr;
507         DDF attrs = m_obj["attributes"];
508         if (!attrs.islist())
509             attrs = m_obj.addmember("attributes").list();
510         for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a) {
511             attr = (*a)->marshall();
512             attrs.add(attr);
513         }
514
515         // Tentatively increment the version.
516         m_obj["version"].integer(m_obj["version"].integer()+1);
517
518         ostringstream str;
519         str << m_obj;
520         string record(str.str());
521
522         try {
523             ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);
524         }
525         catch (exception&) {
526             // Roll back modification to record.
527             m_obj["version"].integer(m_obj["version"].integer()-1);
528             vector<Attribute*>::size_type count = attributes.size();
529             while (count--)
530                 attrs.last().destroy();
531             throw;
532         }
533
534         if (ver <= 0) {
535             // Roll back modification to record.
536             m_obj["version"].integer(m_obj["version"].integer()-1);
537             vector<Attribute*>::size_type count = attributes.size();
538             while (count--)
539                 attrs.last().destroy();
540         }
541         if (!ver) {
542             // Fatal problem with update.
543             throw IOException("Unable to update stored session.");
544         }
545         else if (ver < 0) {
546             // Out of sync.
547             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
548             ver = m_cache->m_storage->readText(getID(), "session", &record, NULL);
549             if (!ver) {
550                 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
551                 throw IOException("Unable to read back stored session.");
552             }
553
554             // Reset object.
555             DDF newobj;
556             istringstream in(record);
557             in >> newobj;
558
559             m_ids.clear();
560             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
561             m_attributes.clear();
562             m_attributeIndex.clear();
563             newobj["version"].integer(ver);
564             m_obj.destroy();
565             m_obj = newobj;
566
567             ver = -1;
568         }
569     } while (ver < 0);  // negative indicates a sync issue so we retry
570
571     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();
572     Locker locker(xlog);
573     xlog->log.infoStream() <<
574         "Added the following attributes to session (ID: " <<
575             getID() <<
576         ") for (applicationId: " <<
577             m_obj["application_id"].string() <<
578         ") {";
579     for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a)
580         xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";
581     xlog->log.info("}");
582
583     // We own them now, so clean them up.
584     for_each(attributes.begin(), attributes.end(), xmltooling::cleanup<Attribute>());
585 }
586
587 const Assertion* StoredSession::getAssertion(const char* id) const
588 {
589     if (!m_cache->m_storage)
590         throw ConfigurationException("Assertion retrieval requires a StorageService.");
591
592     map<string,Assertion*>::const_iterator i = m_tokens.find(id);
593     if (i!=m_tokens.end())
594         return i->second;
595
596     string tokenstr;
597     if (!m_cache->m_storage->readText(getID(), id, &tokenstr, NULL))
598         throw FatalProfileException("Assertion not found in cache.");
599
600     // Parse and bind the document into an XMLObject.
601     istringstream instr(tokenstr);
602     DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr);
603     XercesJanitor<DOMDocument> janitor(doc);
604     auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));
605     janitor.release();
606
607     Assertion* token = dynamic_cast<Assertion*>(xmlObject.get());
608     if (!token)
609         throw FatalProfileException("Request for cached assertion returned an unknown object type.");
610
611     // Transfer ownership to us.
612     xmlObject.release();
613     m_tokens[id]=token;
614     return token;
615 }
616
617 void StoredSession::addAssertion(Assertion* assertion)
618 {
619 #ifdef _DEBUG
620     xmltooling::NDC ndc("addAssertion");
621 #endif
622
623     if (!m_cache->m_storage)
624         throw ConfigurationException("Session modification requires a StorageService.");
625
626     if (!assertion)
627         throw FatalProfileException("Unknown object type passed to session for storage.");
628
629     auto_ptr_char id(assertion->getID());
630
631     m_cache->m_log.debug("adding assertion (%s) to session (%s)", id.get(), getID());
632
633     time_t exp;
634     if (!m_cache->m_storage->readText(getID(), "session", NULL, &exp))
635         throw IOException("Unable to load expiration time for stored session.");
636
637     ostringstream tokenstr;
638     tokenstr << *assertion;
639     if (!m_cache->m_storage->createText(getID(), id.get(), tokenstr.str().c_str(), exp))
640         throw IOException("Attempted to insert duplicate assertion ID into session.");
641
642     int ver;
643     do {
644         DDF token = DDF(NULL).string(id.get());
645         m_obj["assertions"].add(token);
646
647         // Tentatively increment the version.
648         m_obj["version"].integer(m_obj["version"].integer()+1);
649
650         ostringstream str;
651         str << m_obj;
652         string record(str.str());
653
654         try {
655             ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);
656         }
657         catch (exception&) {
658             token.destroy();
659             m_obj["version"].integer(m_obj["version"].integer()-1);
660             m_cache->m_storage->deleteText(getID(), id.get());
661             throw;
662         }
663
664         if (ver <= 0) {
665             token.destroy();
666             m_obj["version"].integer(m_obj["version"].integer()-1);
667         }
668         if (!ver) {
669             // Fatal problem with update.
670             m_cache->m_log.error("updateText failed on StorageService for session (%s)", getID());
671             m_cache->m_storage->deleteText(getID(), id.get());
672             throw IOException("Unable to update stored session.");
673         }
674         else if (ver < 0) {
675             // Out of sync.
676             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
677             ver = m_cache->m_storage->readText(getID(), "session", &record, NULL);
678             if (!ver) {
679                 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
680                 m_cache->m_storage->deleteText(getID(), id.get());
681                 throw IOException("Unable to read back stored session.");
682             }
683
684             // Reset object.
685             DDF newobj;
686             istringstream in(record);
687             in >> newobj;
688
689             m_ids.clear();
690             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
691             m_attributes.clear();
692             m_attributeIndex.clear();
693             newobj["version"].integer(ver);
694             m_obj.destroy();
695             m_obj = newobj;
696
697             ver = -1;
698         }
699     } while (ver < 0); // negative indicates a sync issue so we retry
700
701     m_ids.clear();
702     delete assertion;
703
704     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();
705     Locker locker(xlog);
706     xlog->log.info(
707         "Added assertion (ID: %s) to session for (applicationId: %s) with (ID: %s)",
708         id.get(), m_obj["application_id"].string(), getID()
709         );
710 }
711
712 #endif
713
714 SSCache::SSCache(const DOMElement* e)
715     : m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), inproc(true), m_cacheTimeout(28800),
716 #ifndef SHIBSP_LITE
717       m_storage(NULL), m_storage_lite(NULL),
718 #endif
719         m_root(e), m_inprocTimeout(900), m_lock(NULL), shutdown(false), shutdown_wait(NULL), cleanup_thread(NULL)
720 {
721     static const XMLCh cacheTimeout[] =     UNICODE_LITERAL_12(c,a,c,h,e,T,i,m,e,o,u,t);
722     static const XMLCh inprocTimeout[] =    UNICODE_LITERAL_13(i,n,p,r,o,c,T,i,m,e,o,u,t);
723     static const XMLCh _StorageService[] =  UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e);
724     static const XMLCh _StorageServiceLite[] = UNICODE_LITERAL_18(S,t,o,r,a,g,e,S,e,r,v,i,c,e,L,i,t,e);
725
726     SPConfig& conf = SPConfig::getConfig();
727     inproc = conf.isEnabled(SPConfig::InProcess);
728
729     if (e) {
730         const XMLCh* tag=e->getAttributeNS(NULL,cacheTimeout);
731         if (tag && *tag) {
732             m_cacheTimeout = XMLString::parseInt(tag);
733             if (!m_cacheTimeout)
734                 m_cacheTimeout=28800;
735         }
736         if (inproc) {
737             const XMLCh* tag=e->getAttributeNS(NULL,inprocTimeout);
738             if (tag && *tag) {
739                 m_inprocTimeout = XMLString::parseInt(tag);
740                 if (!m_inprocTimeout)
741                     m_inprocTimeout=900;
742             }
743         }
744     }
745
746 #ifndef SHIBSP_LITE
747     if (conf.isEnabled(SPConfig::OutOfProcess)) {
748         const XMLCh* tag = e ? e->getAttributeNS(NULL,_StorageService) : NULL;
749         if (tag && *tag) {
750             auto_ptr_char ssid(tag);
751             m_storage = conf.getServiceProvider()->getStorageService(ssid.get());
752             if (m_storage)
753                 m_log.info("bound to StorageService (%s)", ssid.get());
754         }
755         if (!m_storage)
756             throw ConfigurationException("SessionCache unable to locate StorageService, check configuration.");
757
758         tag = e ? e->getAttributeNS(NULL,_StorageServiceLite) : NULL;
759         if (tag && *tag) {
760             auto_ptr_char ssid(tag);
761             m_storage_lite = conf.getServiceProvider()->getStorageService(ssid.get());
762             if (m_storage_lite)
763                 m_log.info("bound to StorageServiceLite (%s)", ssid.get());
764         }
765         if (!m_storage_lite) {
766             m_log.info("No StorageServiceLite specified. Using standard StorageService.");
767             m_storage_lite = m_storage;
768         }
769     }
770 #endif
771
772     ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
773     if (inproc ) {
774         if (!conf.isEnabled(SPConfig::OutOfProcess) && !listener)
775             throw ConfigurationException("SessionCache requires a ListenerService, but none available.");
776         m_lock = RWLock::create();
777         shutdown_wait = CondWait::create();
778         cleanup_thread = Thread::create(&cleanup_fn, (void*)this);
779     }
780 #ifndef SHIBSP_LITE
781     else {
782         if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
783             listener->regListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
784             listener->regListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
785             listener->regListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
786         }
787         else {
788             m_log.info("no ListenerService available, cache remoting disabled");
789         }
790     }
791 #endif
792 }
793
794 SSCache::~SSCache()
795 {
796     if (inproc) {
797         // Shut down the cleanup thread and let it know...
798         shutdown = true;
799         shutdown_wait->signal();
800         cleanup_thread->join(NULL);
801
802         for_each(m_hashtable.begin(),m_hashtable.end(),cleanup_pair<string,StoredSession>());
803         delete m_lock;
804         delete shutdown_wait;
805     }
806 #ifndef SHIBSP_LITE
807     else {
808         SPConfig& conf = SPConfig::getConfig();
809         ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
810         if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
811             listener->unregListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
812             listener->unregListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
813             listener->unregListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
814         }
815     }
816 #endif
817 }
818
819 #ifndef SHIBSP_LITE
820
821 void SSCache::test()
822 {
823     auto_ptr_char temp(SAMLConfig::getConfig().generateIdentifier());
824     m_storage->createString("SessionCacheTest", temp.get(), "Test", time(NULL) + 60);
825     m_storage->deleteString("SessionCacheTest", temp.get());
826 }
827
828 void SSCache::insert(const char* key, time_t expires, const char* name, const char* index)
829 {
830     string dup;
831     if (strlen(name) > 255) {
832         dup = string(name).substr(0,255);
833         name = dup.c_str();
834     }
835
836     DDF obj;
837     DDFJanitor jobj(obj);
838
839     // Since we can't guarantee uniqueness, check for an existing record.
840     string record;
841     time_t recordexp;
842     int ver = m_storage_lite->readText("NameID", name, &record, &recordexp);
843     if (ver > 0) {
844         // Existing record, so we need to unmarshall it.
845         istringstream in(record);
846         in >> obj;
847     }
848     else {
849         // New record.
850         obj = DDF(NULL).structure();
851     }
852
853     if (!index || !*index)
854         index = "_shibnull";
855     DDF sessions = obj.addmember(index);
856     if (!sessions.islist())
857         sessions.list();
858     DDF session = DDF(NULL).string(key);
859     sessions.add(session);
860
861     // Remarshall the record.
862     ostringstream out;
863     out << obj;
864
865     // Try and store it back...
866     if (ver > 0) {
867         ver = m_storage_lite->updateText("NameID", name, out.str().c_str(), max(expires, recordexp), ver);
868         if (ver <= 0) {
869             // Out of sync, or went missing, so retry.
870             return insert(key, expires, name, index);
871         }
872     }
873     else if (!m_storage_lite->createText("NameID", name, out.str().c_str(), expires)) {
874         // Hit a dup, so just retry, hopefully hitting the other branch.
875         return insert(key, expires, name, index);
876     }
877 }
878
879 void SSCache::insert(
880     const Application& application,
881     const HTTPRequest& httpRequest,
882     HTTPResponse& httpResponse,
883     time_t expires,
884     const saml2md::EntityDescriptor* issuer,
885     const XMLCh* protocol,
886     const saml2::NameID* nameid,
887     const XMLCh* authn_instant,
888     const XMLCh* session_index,
889     const XMLCh* authncontext_class,
890     const XMLCh* authncontext_decl,
891     const vector<const Assertion*>* tokens,
892     const vector<Attribute*>* attributes
893     )
894 {
895 #ifdef _DEBUG
896     xmltooling::NDC ndc("insert");
897 #endif
898     if (!m_storage)
899         throw ConfigurationException("SessionCache insertion requires a StorageService.");
900
901     m_log.debug("creating new session");
902
903     time_t now = time(NULL);
904     auto_ptr_char index(session_index);
905     auto_ptr_char entity_id(issuer ? issuer->getEntityID() : NULL);
906     auto_ptr_char name(nameid ? nameid->getName() : NULL);
907
908     if (nameid) {
909         // Check for a pending logout.
910         if (strlen(name.get()) > 255)
911             const_cast<char*>(name.get())[255] = 0;
912         string pending;
913         int ver = m_storage_lite->readText("Logout", name.get(), &pending);
914         if (ver > 0) {
915             DDF pendobj;
916             DDFJanitor jpend(pendobj);
917             istringstream pstr(pending);
918             pstr >> pendobj;
919             // IdP.SP.index contains logout expiration, if any.
920             DDF deadmenwalking = pendobj[issuer ? entity_id.get() : "_shibnull"][application.getRelyingParty(issuer)->getString("entityID").second];
921             const char* logexpstr = deadmenwalking[session_index ? index.get() : "_shibnull"].string();
922             if (!logexpstr && session_index)    // we tried an exact session match, now try for NULL
923                 logexpstr = deadmenwalking["_shibnull"].string();
924             if (logexpstr) {
925                 auto_ptr_XMLCh dt(logexpstr);
926                 DateTime dtobj(dt.get());
927                 dtobj.parseDateTime();
928                 time_t logexp = dtobj.getEpoch();
929                 if (now - XMLToolingConfig::getConfig().clock_skew_secs < logexp)
930                     throw FatalProfileException("A logout message from your identity provider has blocked your login attempt.");
931             }
932         }
933     }
934
935     auto_ptr_char key(SAMLConfig::getConfig().generateIdentifier());
936
937     // Store session properties in DDF.
938     DDF obj = DDF(key.get()).structure();
939     DDFJanitor entryobj(obj);
940     obj.addmember("version").integer(1);
941     obj.addmember("application_id").string(application.getId());
942
943     // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
944 #ifndef HAVE_GMTIME_R
945     struct tm* ptime=gmtime(&expires);
946 #else
947     struct tm res;
948     struct tm* ptime=gmtime_r(&expires,&res);
949 #endif
950     char timebuf[32];
951     strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
952     obj.addmember("expires").string(timebuf);
953
954     obj.addmember("client_addr").string(httpRequest.getRemoteAddr().c_str());
955     if (issuer)
956         obj.addmember("entity_id").string(entity_id.get());
957     if (protocol) {
958         auto_ptr_char prot(protocol);
959         obj.addmember("protocol").string(prot.get());
960     }
961     if (authn_instant) {
962         auto_ptr_char instant(authn_instant);
963         obj.addmember("authn_instant").string(instant.get());
964     }
965     if (session_index)
966         obj.addmember("session_index").string(index.get());
967     if (authncontext_class) {
968         auto_ptr_char ac(authncontext_class);
969         obj.addmember("authncontext_class").string(ac.get());
970     }
971     if (authncontext_decl) {
972         auto_ptr_char ad(authncontext_decl);
973         obj.addmember("authncontext_decl").string(ad.get());
974     }
975
976     if (nameid) {
977         ostringstream namestr;
978         namestr << *nameid;
979         obj.addmember("nameid").string(namestr.str().c_str());
980     }
981
982     if (tokens) {
983         obj.addmember("assertions").list();
984         for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
985             auto_ptr_char tokenid((*t)->getID());
986             DDF tokid = DDF(NULL).string(tokenid.get());
987             obj["assertions"].add(tokid);
988         }
989     }
990
991     if (attributes) {
992         DDF attr;
993         DDF attrlist = obj.addmember("attributes").list();
994         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) {
995             attr = (*a)->marshall();
996             attrlist.add(attr);
997         }
998     }
999
1000     ostringstream record;
1001     record << obj;
1002
1003     m_log.debug("storing new session...");
1004     if (!m_storage->createText(key.get(), "session", record.str().c_str(), now + m_cacheTimeout))
1005         throw FatalProfileException("Attempted to create a session with a duplicate key.");
1006
1007     // Store the reverse mapping for logout.
1008     try {
1009         if (nameid)
1010             insert(key.get(), expires, name.get(), index.get());
1011     }
1012     catch (exception& ex) {
1013         m_log.error("error storing back mapping of NameID for logout: %s", ex.what());
1014     }
1015
1016     if (tokens) {
1017         try {
1018             for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
1019                 ostringstream tokenstr;
1020                 tokenstr << *(*t);
1021                 auto_ptr_char tokenid((*t)->getID());
1022                 if (!m_storage->createText(key.get(), tokenid.get(), tokenstr.str().c_str(), now + m_cacheTimeout))
1023                     throw IOException("duplicate assertion ID ($1)", params(1, tokenid.get()));
1024             }
1025         }
1026         catch (exception& ex) {
1027             m_log.error("error storing assertion along with session: %s", ex.what());
1028         }
1029     }
1030
1031     const char* pid = obj["entity_id"].string();
1032     const char* prot = obj["protocol"].string();
1033     m_log.info("new session created: ID (%s) IdP (%s) Protocol(%s) Address (%s)",
1034         key.get(), pid ? pid : "none", prot ? prot : "none", httpRequest.getRemoteAddr().c_str());
1035
1036     // Transaction Logging
1037     TransactionLog* xlog = application.getServiceProvider().getTransactionLog();
1038     Locker locker(xlog);
1039     xlog->log.infoStream() <<
1040         "New session (ID: " <<
1041             key.get() <<
1042         ") with (applicationId: " <<
1043             application.getId() <<
1044         ") for principal from (IdP: " <<
1045             (pid ? pid : "none") <<
1046         ") at (ClientAddress: " <<
1047             httpRequest.getRemoteAddr() <<
1048         ") with (NameIdentifier: " <<
1049             (nameid ? name.get() : "none") <<
1050         ") using (Protocol: " <<
1051             (prot ? prot : "none") <<
1052         ") from (AssertionID: " <<
1053             (tokens ? obj["assertions"].first().string() : "none") <<
1054         ")";
1055
1056     if (attributes) {
1057         xlog->log.infoStream() <<
1058             "Cached the following attributes with session (ID: " <<
1059                 key.get() <<
1060             ") for (applicationId: " <<
1061                 application.getId() <<
1062             ") {";
1063         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a)
1064             xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";
1065         xlog->log.info("}");
1066     }
1067
1068     time_t cookieLifetime = 0;
1069     pair<string,const char*> shib_cookie = application.getCookieNameProps("_shibsession_", &cookieLifetime);
1070     string k(key.get());
1071     k += shib_cookie.second;
1072
1073     if (cookieLifetime > 0) {
1074         cookieLifetime += now;
1075 #ifndef HAVE_GMTIME_R
1076         ptime=gmtime(&cookieLifetime);
1077 #else
1078         ptime=gmtime_r(&cookieLifetime,&res);
1079 #endif
1080         char cookietimebuf[64];
1081         strftime(cookietimebuf,64,"; expires=%a, %d %b %Y %H:%M:%S GMT",ptime);
1082         k += cookietimebuf;
1083     }
1084
1085     httpResponse.setCookie(shib_cookie.first.c_str(), k.c_str());
1086 }
1087
1088 bool SSCache::matches(
1089     const Application& application,
1090     const xmltooling::HTTPRequest& request,
1091     const saml2md::EntityDescriptor* issuer,
1092     const saml2::NameID& nameid,
1093     const set<string>* indexes
1094     )
1095 {
1096     auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL);
1097     try {
1098         Session* session = find(application, request);
1099         if (session) {
1100             Locker locker(session, false);
1101             if (XMLString::equals(session->getEntityID(), entityID.get()) && session->getNameID() &&
1102                     stronglyMatches(issuer->getEntityID(), application.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) {
1103                 return (!indexes || indexes->empty() || (session->getSessionIndex() ? (indexes->count(session->getSessionIndex())>0) : false));
1104             }
1105         }
1106     }
1107     catch (exception& ex) {
1108         m_log.error("error while matching session: %s", ex.what());
1109     }
1110     return false;
1111 }
1112
1113 vector<string>::size_type SSCache::logout(
1114     const Application& application,
1115     const saml2md::EntityDescriptor* issuer,
1116     const saml2::NameID& nameid,
1117     const set<string>* indexes,
1118     time_t expires,
1119     vector<string>& sessionsKilled
1120     )
1121 {
1122 #ifdef _DEBUG
1123     xmltooling::NDC ndc("logout");
1124 #endif
1125
1126     if (!m_storage)
1127         throw ConfigurationException("SessionCache insertion requires a StorageService.");
1128
1129     auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL);
1130     auto_ptr_char name(nameid.getName());
1131
1132     m_log.info("request to logout sessions from (%s) for (%s)", entityID.get() ? entityID.get() : "unknown", name.get());
1133
1134     if (strlen(name.get()) > 255)
1135         const_cast<char*>(name.get())[255] = 0;
1136
1137     DDF obj;
1138     DDFJanitor jobj(obj);
1139     string record;
1140     int ver;
1141
1142     if (expires) {
1143         // Record the logout to prevent post-delivered assertions.
1144         // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
1145 #ifndef HAVE_GMTIME_R
1146         struct tm* ptime=gmtime(&expires);
1147 #else
1148         struct tm res;
1149         struct tm* ptime=gmtime_r(&expires,&res);
1150 #endif
1151         char timebuf[32];
1152         strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
1153
1154         time_t oldexp = 0;
1155         ver = m_storage_lite->readText("Logout", name.get(), &record, &oldexp);
1156         if (ver > 0) {
1157             istringstream lin(record);
1158             lin >> obj;
1159         }
1160         else {
1161             obj = DDF(NULL).structure();
1162         }
1163
1164         // Structure is keyed by the IdP and SP, with a member per session index containing the expiration.
1165         DDF root = obj.addmember(issuer ? entityID.get() : "_shibnull").addmember(application.getRelyingParty(issuer)->getString("entityID").second);
1166         if (indexes) {
1167             for (set<string>::const_iterator x = indexes->begin(); x!=indexes->end(); ++x)
1168                 root.addmember(x->c_str()).string(timebuf);
1169         }
1170         else {
1171             root.addmember("_shibnull").string(timebuf);
1172         }
1173
1174         // Write it back.
1175         ostringstream lout;
1176         lout << obj;
1177
1178         if (ver > 0) {
1179             ver = m_storage_lite->updateText("Logout", name.get(), lout.str().c_str(), max(expires, oldexp), ver);
1180             if (ver <= 0) {
1181                 // Out of sync, or went missing, so retry.
1182                 return logout(application, issuer, nameid, indexes, expires, sessionsKilled);
1183             }
1184         }
1185         else if (!m_storage_lite->createText("Logout", name.get(), lout.str().c_str(), expires)) {
1186             // Hit a dup, so just retry, hopefully hitting the other branch.
1187             return logout(application, issuer, nameid, indexes, expires, sessionsKilled);
1188         }
1189
1190         obj.destroy();
1191         record.erase();
1192     }
1193
1194     // Read in potentially matching sessions.
1195     ver = m_storage_lite->readText("NameID", name.get(), &record);
1196     if (ver == 0) {
1197         m_log.debug("no active sessions to logout for supplied issuer and subject");
1198         return 0;
1199     }
1200
1201     istringstream in(record);
1202     in >> obj;
1203
1204     // The record contains child lists for each known session index.
1205     DDF key;
1206     DDF sessions = obj.first();
1207     while (sessions.islist()) {
1208         if (!indexes || indexes->empty() || indexes->count(sessions.name())) {
1209             key = sessions.first();
1210             while (key.isstring()) {
1211                 // Fetch the session for comparison.
1212                 Session* session = NULL;
1213                 try {
1214                     session = find(application, key.string());
1215                 }
1216                 catch (exception& ex) {
1217                     m_log.error("error locating session (%s): %s", key.string(), ex.what());
1218                 }
1219
1220                 if (session) {
1221                     Locker locker(session, false);
1222                     // Same issuer?
1223                     if (XMLString::equals(session->getEntityID(), entityID.get())) {
1224                         // Same NameID?
1225                         if (stronglyMatches(issuer->getEntityID(), application.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) {
1226                             sessionsKilled.push_back(key.string());
1227                             key.destroy();
1228                         }
1229                         else {
1230                             m_log.debug("session (%s) contained a non-matching NameID, leaving it alone", key.string());
1231                         }
1232                     }
1233                     else {
1234                         m_log.debug("session (%s) established by different IdP, leaving it alone", key.string());
1235                     }
1236                 }
1237                 else {
1238                     // Session's gone, so...
1239                     sessionsKilled.push_back(key.string());
1240                     key.destroy();
1241                 }
1242                 key = sessions.next();
1243             }
1244
1245             // No sessions left for this index?
1246             if (sessions.first().isnull())
1247                 sessions.destroy();
1248         }
1249         sessions = obj.next();
1250     }
1251
1252     if (obj.first().isnull())
1253         obj.destroy();
1254
1255     // If possible, write back the mapping record (this isn't crucial).
1256     try {
1257         if (obj.isnull()) {
1258             m_storage_lite->deleteText("NameID", name.get());
1259         }
1260         else if (!sessionsKilled.empty()) {
1261             ostringstream out;
1262             out << obj;
1263             if (m_storage_lite->updateText("NameID", name.get(), out.str().c_str(), 0, ver) <= 0)
1264                 m_log.warn("logout mapping record changed behind us, leaving it alone");
1265         }
1266     }
1267     catch (exception& ex) {
1268         m_log.error("error updating logout mapping record: %s", ex.what());
1269     }
1270
1271     return sessionsKilled.size();
1272 }
1273
1274 bool SSCache::stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const
1275 {
1276     if (!XMLString::equals(n1.getName(), n2.getName()))
1277         return false;
1278
1279     const XMLCh* s1 = n1.getFormat();
1280     const XMLCh* s2 = n2.getFormat();
1281     if (!s1 || !*s1)
1282         s1 = saml2::NameID::UNSPECIFIED;
1283     if (!s2 || !*s2)
1284         s2 = saml2::NameID::UNSPECIFIED;
1285     if (!XMLString::equals(s1,s2))
1286         return false;
1287
1288     s1 = n1.getNameQualifier();
1289     s2 = n2.getNameQualifier();
1290     if (!s1 || !*s1)
1291         s1 = idp;
1292     if (!s2 || !*s2)
1293         s2 = idp;
1294     if (!XMLString::equals(s1,s2))
1295         return false;
1296
1297     s1 = n1.getSPNameQualifier();
1298     s2 = n2.getSPNameQualifier();
1299     if (!s1 || !*s1)
1300         s1 = sp;
1301     if (!s2 || !*s2)
1302         s2 = sp;
1303     if (!XMLString::equals(s1,s2))
1304         return false;
1305
1306     return true;
1307 }
1308
1309 #endif
1310
1311 Session* SSCache::find(const Application& application, const char* key, const char* client_addr, time_t* timeout)
1312 {
1313 #ifdef _DEBUG
1314     xmltooling::NDC ndc("find");
1315 #endif
1316     StoredSession* session=NULL;
1317
1318     if (inproc) {
1319         m_log.debug("searching local cache for session (%s)", key);
1320         m_lock->rdlock();
1321         map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);
1322         if (i!=m_hashtable.end()) {
1323             // Save off and lock the session.
1324             session = i->second;
1325             session->lock();
1326             m_lock->unlock();
1327             m_log.debug("session found locally, validating it for use");
1328         }
1329         else {
1330             m_lock->unlock();
1331         }
1332     }
1333
1334     if (!session) {
1335         if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
1336             m_log.debug("session not found locally, remoting the search");
1337             // Remote the request.
1338             DDF in("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;
1339             DDFJanitor jin(in);
1340             in.structure();
1341             in.addmember("key").string(key);
1342             in.addmember("application_id").string(application.getId());
1343             if (timeout && *timeout) {
1344                 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
1345 #ifndef HAVE_GMTIME_R
1346                 struct tm* ptime=gmtime(timeout);
1347 #else
1348                 struct tm res;
1349                 struct tm* ptime=gmtime_r(timeout,&res);
1350 #endif
1351                 char timebuf[32];
1352                 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
1353                 in.addmember("timeout").string(timebuf);
1354             }
1355
1356             try {
1357                 out=application.getServiceProvider().getListenerService()->send(in);
1358                 if (!out.isstruct()) {
1359                     out.destroy();
1360                     m_log.debug("session not found in remote cache");
1361                     return NULL;
1362                 }
1363
1364                 // Wrap the results in a local entry and save it.
1365                 session = new StoredSession(this, out);
1366                 // The remote end has handled timeout issues, we handle address and expiration checks.
1367                 timeout = NULL;
1368             }
1369             catch (...) {
1370                 out.destroy();
1371                 throw;
1372             }
1373         }
1374         else {
1375             // We're out of process, so we can search the storage service directly.
1376 #ifndef SHIBSP_LITE
1377             if (!m_storage)
1378                 throw ConfigurationException("SessionCache lookup requires a StorageService.");
1379
1380             m_log.debug("searching for session (%s)", key);
1381
1382             DDF obj;
1383             time_t lastAccess;
1384             string record;
1385             int ver = m_storage->readText(key, "session", &record, &lastAccess);
1386             if (!ver)
1387                 return NULL;
1388
1389             m_log.debug("reconstituting session and checking validity");
1390
1391             istringstream in(record);
1392             in >> obj;
1393
1394             lastAccess -= m_cacheTimeout;   // adjusts it back to the last time the record's timestamp was touched
1395             time_t now=time(NULL);
1396
1397             if (timeout && *timeout > 0 && now - lastAccess >= *timeout) {
1398                 m_log.info("session timed out (ID: %s)", key);
1399                 remove(application, key);
1400                 const char* eid = obj["entity_id"].string();
1401                 if (!eid) {
1402                     obj.destroy();
1403                     throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1404                 }
1405                 string eid2(eid);
1406                 obj.destroy();
1407                 throw RetryableProfileException("Your session has expired, and you must re-authenticate.", namedparams(1, "entityID", eid2.c_str()));
1408             }
1409
1410             if (timeout) {
1411                 // Update storage expiration, if possible.
1412                 try {
1413                     m_storage->updateContext(key, now + m_cacheTimeout);
1414                 }
1415                 catch (exception& ex) {
1416                     m_log.error("failed to update session expiration: %s", ex.what());
1417                 }
1418             }
1419
1420             // Wrap the results in a local entry and save it.
1421             session = new StoredSession(this, obj);
1422             // We handled timeout issues, still need to handle address and expiration checks.
1423             timeout = NULL;
1424 #else
1425             throw ConfigurationException("SessionCache search requires a StorageService.");
1426 #endif
1427         }
1428
1429         if (inproc) {
1430             // Lock for writing and repeat the search to avoid duplication.
1431             m_lock->wrlock();
1432             SharedLock shared(m_lock, false);
1433             if (m_hashtable.count(key)) {
1434                 // We're using an existing session entry.
1435                 delete session;
1436                 session = m_hashtable[key];
1437                 session->lock();
1438             }
1439             else {
1440                 m_hashtable[key]=session;
1441                 session->lock();
1442             }
1443         }
1444     }
1445
1446     if (!XMLString::equals(session->getApplicationID(), application.getId())) {
1447         m_log.error("an application (%s) tried to access another application's session", application.getId());
1448         session->unlock();
1449         return NULL;
1450     }
1451
1452     // Verify currency and update the timestamp if indicated by caller.
1453     try {
1454         session->validate(application, client_addr, timeout);
1455     }
1456     catch (...) {
1457         session->unlock();
1458         remove(application, key);
1459         throw;
1460     }
1461
1462     return session;
1463 }
1464
1465 void SSCache::remove(const Application& application, const char* key)
1466 {
1467 #ifdef _DEBUG
1468     xmltooling::NDC ndc("remove");
1469 #endif
1470     // Take care of local copy.
1471     if (inproc)
1472         dormant(key);
1473
1474     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
1475         // Remove the session from storage directly.
1476 #ifndef SHIBSP_LITE
1477         m_storage->deleteContext(key);
1478         m_log.info("removed session (%s)", key);
1479
1480         TransactionLog* xlog = application.getServiceProvider().getTransactionLog();
1481         Locker locker(xlog);
1482         xlog->log.info("Destroyed session (applicationId: %s) (ID: %s)", application.getId(), key);
1483 #else
1484         throw ConfigurationException("SessionCache removal requires a StorageService.");
1485 #endif
1486     }
1487     else {
1488         // Remote the request.
1489         DDF in("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache");
1490         DDFJanitor jin(in);
1491         in.structure();
1492         in.addmember("key").string(key);
1493         in.addmember("application_id").string(application.getId());
1494
1495         DDF out = application.getServiceProvider().getListenerService()->send(in);
1496         out.destroy();
1497     }
1498 }
1499
1500 void SSCache::dormant(const char* key)
1501 {
1502 #ifdef _DEBUG
1503     xmltooling::NDC ndc("dormant");
1504 #endif
1505
1506     m_log.debug("deleting local copy of session (%s)", key);
1507
1508     // lock the cache for writing, which means we know nobody is sitting in find()
1509     m_lock->wrlock();
1510
1511     // grab the entry from the table
1512     map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);
1513     if (i==m_hashtable.end()) {
1514         m_lock->unlock();
1515         return;
1516     }
1517
1518     // ok, remove the entry and lock it
1519     StoredSession* entry=i->second;
1520     m_hashtable.erase(key);
1521     entry->lock();
1522
1523     // unlock the cache
1524     m_lock->unlock();
1525
1526     // we can release the cache entry lock because we know we're not in the cache anymore
1527     entry->unlock();
1528
1529     delete entry;
1530 }
1531
1532 void SSCache::cleanup()
1533 {
1534 #ifdef _DEBUG
1535     xmltooling::NDC ndc("cleanup");
1536 #endif
1537
1538     Mutex* mutex = Mutex::create();
1539
1540     // Load our configuration details...
1541     static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);
1542     const XMLCh* tag=m_root ? m_root->getAttributeNS(NULL,cleanupInterval) : NULL;
1543     int rerun_timer = 900;
1544     if (tag && *tag) {
1545         rerun_timer = XMLString::parseInt(tag);
1546         if (rerun_timer <= 0)
1547             rerun_timer = 900;
1548     }
1549
1550     mutex->lock();
1551
1552     m_log.info("cleanup thread started...run every %d secs; timeout after %d secs", rerun_timer, m_inprocTimeout);
1553
1554     while (!shutdown) {
1555         shutdown_wait->timedwait(mutex,rerun_timer);
1556         if (shutdown)
1557             break;
1558
1559         // Ok, let's run through the cleanup process and clean out
1560         // really old sessions.  This is a two-pass process.  The
1561         // first pass is done holding a read-lock while we iterate over
1562         // the cache.  The second pass doesn't need a lock because
1563         // the 'deletes' will lock the cache.
1564
1565         // Pass 1: iterate over the map and find all entries that have not been
1566         // used in the allotted timeout.
1567         vector<string> stale_keys;
1568         time_t stale = time(NULL) - m_inprocTimeout;
1569
1570         m_log.debug("cleanup thread running");
1571
1572         m_lock->rdlock();
1573         for (map<string,StoredSession*>::const_iterator i=m_hashtable.begin(); i!=m_hashtable.end(); ++i) {
1574             // If the last access was BEFORE the stale timeout...
1575             i->second->lock();
1576             time_t last=i->second->getLastAccess();
1577             i->second->unlock();
1578             if (last < stale)
1579                 stale_keys.push_back(i->first);
1580         }
1581         m_lock->unlock();
1582
1583         if (!stale_keys.empty()) {
1584             m_log.info("purging %d old sessions", stale_keys.size());
1585
1586             // Pass 2: walk through the list of stale entries and remove them from the cache
1587             for (vector<string>::const_iterator j = stale_keys.begin(); j != stale_keys.end(); ++j)
1588                 dormant(j->c_str());
1589         }
1590
1591         m_log.debug("cleanup thread completed");
1592     }
1593
1594     m_log.info("cleanup thread exiting");
1595
1596     mutex->unlock();
1597     delete mutex;
1598     Thread::exit(NULL);
1599 }
1600
1601 void* SSCache::cleanup_fn(void* cache_p)
1602 {
1603 #ifndef WIN32
1604     // First, let's block all signals
1605     Thread::mask_all_signals();
1606 #endif
1607
1608     // Now run the cleanup process.
1609     reinterpret_cast<SSCache*>(cache_p)->cleanup();
1610     return NULL;
1611 }
1612
1613 #ifndef SHIBSP_LITE
1614
1615 void SSCache::receive(DDF& in, ostream& out)
1616 {
1617 #ifdef _DEBUG
1618     xmltooling::NDC ndc("receive");
1619 #endif
1620
1621     if (!strcmp(in.name(),"find::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1622         const char* key=in["key"].string();
1623         if (!key)
1624             throw ListenerException("Required parameters missing for session lookup.");
1625
1626         const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());
1627         if (!app)
1628             throw ListenerException("Application not found, check configuration?");
1629
1630         // Do an unversioned read.
1631         string record;
1632         time_t lastAccess;
1633         if (!m_storage->readText(key, "session", &record, &lastAccess)) {
1634             DDF ret(NULL);
1635             DDFJanitor jan(ret);
1636             out << ret;
1637             return;
1638         }
1639
1640         // Adjust for expiration to recover last access time and check timeout.
1641         lastAccess -= m_cacheTimeout;
1642         time_t now=time(NULL);
1643
1644         // See if we need to check for a timeout.
1645         if (in["timeout"].string()) {
1646             time_t timeout = 0;
1647             auto_ptr_XMLCh dt(in["timeout"].string());
1648             DateTime dtobj(dt.get());
1649             dtobj.parseDateTime();
1650             timeout = dtobj.getEpoch();
1651
1652             if (timeout > 0 && now - lastAccess >= timeout) {
1653                 m_log.info("session timed out (ID: %s)", key);
1654                 remove(*app, key);
1655                 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1656             }
1657
1658             // Update storage expiration, if possible.
1659             try {
1660                 m_storage->updateContext(key, now + m_cacheTimeout);
1661             }
1662             catch (exception& ex) {
1663                 m_log.error("failed to update session expiration: %s", ex.what());
1664             }
1665         }
1666
1667         // Send the record back.
1668         out << record;
1669     }
1670     else if (!strcmp(in.name(),"touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1671         const char* key=in["key"].string();
1672         if (!key)
1673             throw ListenerException("Required parameters missing for session check.");
1674
1675         // Do a versioned read.
1676         string record;
1677         time_t lastAccess;
1678         int curver = in["version"].integer();
1679         int ver = m_storage->readText(key, "session", &record, &lastAccess, curver);
1680         if (ver == 0) {
1681             m_log.warn("unsuccessful versioned read of session (ID: %s), caches out of sync?", key);
1682             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1683         }
1684
1685         // Adjust for expiration to recover last access time and check timeout.
1686         lastAccess -= m_cacheTimeout;
1687         time_t now=time(NULL);
1688
1689         // See if we need to check for a timeout.
1690         time_t timeout = 0;
1691         auto_ptr_XMLCh dt(in["timeout"].string());
1692         if (dt.get()) {
1693             DateTime dtobj(dt.get());
1694             dtobj.parseDateTime();
1695             timeout = dtobj.getEpoch();
1696         }
1697
1698         if (timeout > 0 && now - lastAccess >= timeout) {
1699             m_log.info("session timed out (ID: %s)", key);
1700             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1701         }
1702
1703         // Update storage expiration, if possible.
1704         try {
1705             m_storage->updateContext(key, now + m_cacheTimeout);
1706         }
1707         catch (exception& ex) {
1708             m_log.error("failed to update session expiration: %s", ex.what());
1709         }
1710
1711         if (ver > curver) {
1712             // Send the record back.
1713             out << record;
1714         }
1715         else {
1716             DDF ret(NULL);
1717             DDFJanitor jan(ret);
1718             out << ret;
1719         }
1720     }
1721     else if (!strcmp(in.name(),"remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1722         const char* key=in["key"].string();
1723         if (!key)
1724             throw ListenerException("Required parameter missing for session removal.");
1725
1726         const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());
1727         if (!app)
1728             throw ConfigurationException("Application not found, check configuration?");
1729
1730         remove(*app, key);
1731         DDF ret(NULL);
1732         DDFJanitor jan(ret);
1733         out << ret;
1734     }
1735 }
1736
1737 #endif