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