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