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