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