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