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