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