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