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