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