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