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