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