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