3219085601ddfbb3e25ea8039ba0aef77781cf50
[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     DDFJanitor entryobj(obj);
940     obj.addmember("version").integer(1);
941     obj.addmember("application_id").string(application.getId());
942
943     // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
944 #ifndef HAVE_GMTIME_R
945     struct tm* ptime=gmtime(&expires);
946 #else
947     struct tm res;
948     struct tm* ptime=gmtime_r(&expires,&res);
949 #endif
950     char timebuf[32];
951     strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
952     obj.addmember("expires").string(timebuf);
953
954     obj.addmember("client_addr").string(httpRequest.getRemoteAddr().c_str());
955     if (issuer)
956         obj.addmember("entity_id").string(entity_id.get());
957     if (protocol) {
958         auto_ptr_char prot(protocol);
959         obj.addmember("protocol").string(prot.get());
960     }
961     if (authn_instant) {
962         auto_ptr_char instant(authn_instant);
963         obj.addmember("authn_instant").string(instant.get());
964     }
965     if (session_index)
966         obj.addmember("session_index").string(index.get());
967     if (authncontext_class) {
968         auto_ptr_char ac(authncontext_class);
969         obj.addmember("authncontext_class").string(ac.get());
970     }
971     if (authncontext_decl) {
972         auto_ptr_char ad(authncontext_decl);
973         obj.addmember("authncontext_decl").string(ad.get());
974     }
975
976     if (nameid) {
977         ostringstream namestr;
978         namestr << *nameid;
979         obj.addmember("nameid").string(namestr.str().c_str());
980     }
981
982     if (tokens) {
983         obj.addmember("assertions").list();
984         for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
985             auto_ptr_char tokenid((*t)->getID());
986             DDF tokid = DDF(NULL).string(tokenid.get());
987             obj["assertions"].add(tokid);
988         }
989     }
990
991     if (attributes) {
992         DDF attr;
993         DDF attrlist = obj.addmember("attributes").list();
994         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) {
995             attr = (*a)->marshall();
996             attrlist.add(attr);
997         }
998     }
999
1000     ostringstream record;
1001     record << obj;
1002
1003     m_log.debug("storing new session...");
1004     if (!m_storage->createText(key.get(), "session", record.str().c_str(), now + m_cacheTimeout))
1005         throw FatalProfileException("Attempted to create a session with a duplicate key.");
1006
1007     // Store the reverse mapping for logout.
1008     try {
1009         if (nameid)
1010             insert(key.get(), expires, name.get(), index.get());
1011     }
1012     catch (exception& ex) {
1013         m_log.error("error storing back mapping of NameID for logout: %s", ex.what());
1014     }
1015
1016     if (tokens) {
1017         try {
1018             for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
1019                 ostringstream tokenstr;
1020                 tokenstr << *(*t);
1021                 auto_ptr_char tokenid((*t)->getID());
1022                 if (!m_storage->createText(key.get(), tokenid.get(), tokenstr.str().c_str(), now + m_cacheTimeout))
1023                     throw IOException("duplicate assertion ID ($1)", params(1, tokenid.get()));
1024             }
1025         }
1026         catch (exception& ex) {
1027             m_log.error("error storing assertion along with session: %s", ex.what());
1028         }
1029     }
1030
1031     const char* pid = obj["entity_id"].string();
1032     const char* prot = obj["protocol"].string();
1033     m_log.info("new session created: ID (%s) IdP (%s) Protocol(%s) Address (%s)",
1034         key.get(), pid ? pid : "none", prot ? prot : "none", httpRequest.getRemoteAddr().c_str());
1035
1036     // Transaction Logging
1037     TransactionLog* xlog = application.getServiceProvider().getTransactionLog();
1038     Locker locker(xlog);
1039     xlog->log.infoStream() <<
1040         "New session (ID: " <<
1041             key.get() <<
1042         ") with (applicationId: " <<
1043             application.getId() <<
1044         ") for principal from (IdP: " <<
1045             (pid ? pid : "none") <<
1046         ") at (ClientAddress: " <<
1047             httpRequest.getRemoteAddr() <<
1048         ") with (NameIdentifier: " <<
1049             (nameid ? name.get() : "none") <<
1050         ") using (Protocol: " <<
1051             (prot ? prot : "none") <<
1052         ") from (AssertionID: " <<
1053             (tokens ? obj["assertions"].first().string() : "none") <<
1054         ")";
1055
1056     if (attributes) {
1057         xlog->log.infoStream() <<
1058             "Cached the following attributes with session (ID: " <<
1059                 key.get() <<
1060             ") for (applicationId: " <<
1061                 application.getId() <<
1062             ") {";
1063         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a)
1064             xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";
1065         xlog->log.info("}");
1066     }
1067
1068     pair<string,const char*> shib_cookie = application.getCookieNameProps("_shibsession_");
1069     string k(key.get());
1070     k += shib_cookie.second;
1071     httpResponse.setCookie(shib_cookie.first.c_str(), k.c_str());
1072 }
1073
1074 bool SSCache::matches(
1075     const Application& application,
1076     const xmltooling::HTTPRequest& request,
1077     const saml2md::EntityDescriptor* issuer,
1078     const saml2::NameID& nameid,
1079     const set<string>* indexes
1080     )
1081 {
1082     auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL);
1083     try {
1084         Session* session = find(application, request);
1085         if (session) {
1086             Locker locker(session, false);
1087             if (XMLString::equals(session->getEntityID(), entityID.get()) && session->getNameID() &&
1088                     stronglyMatches(issuer->getEntityID(), application.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) {
1089                 return (!indexes || indexes->empty() || (session->getSessionIndex() ? (indexes->count(session->getSessionIndex())>0) : false));
1090             }
1091         }
1092     }
1093     catch (exception& ex) {
1094         m_log.error("error while matching session: %s", ex.what());
1095     }
1096     return false;
1097 }
1098
1099 vector<string>::size_type SSCache::logout(
1100     const Application& application,
1101     const saml2md::EntityDescriptor* issuer,
1102     const saml2::NameID& nameid,
1103     const set<string>* indexes,
1104     time_t expires,
1105     vector<string>& sessionsKilled
1106     )
1107 {
1108 #ifdef _DEBUG
1109     xmltooling::NDC ndc("logout");
1110 #endif
1111
1112     if (!m_storage)
1113         throw ConfigurationException("SessionCache insertion requires a StorageService.");
1114
1115     auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL);
1116     auto_ptr_char name(nameid.getName());
1117
1118     m_log.info("request to logout sessions from (%s) for (%s)", entityID.get() ? entityID.get() : "unknown", name.get());
1119
1120     if (strlen(name.get()) > 255)
1121         const_cast<char*>(name.get())[255] = 0;
1122
1123     DDF obj;
1124     DDFJanitor jobj(obj);
1125     string record;
1126     int ver;
1127
1128     if (expires) {
1129         // Record the logout to prevent post-delivered assertions.
1130         // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
1131 #ifndef HAVE_GMTIME_R
1132         struct tm* ptime=gmtime(&expires);
1133 #else
1134         struct tm res;
1135         struct tm* ptime=gmtime_r(&expires,&res);
1136 #endif
1137         char timebuf[32];
1138         strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
1139
1140         time_t oldexp = 0;
1141         ver = m_storage_lite->readText("Logout", name.get(), &record, &oldexp);
1142         if (ver > 0) {
1143             istringstream lin(record);
1144             lin >> obj;
1145         }
1146         else {
1147             obj = DDF(NULL).structure();
1148         }
1149
1150         // Structure is keyed by the IdP and SP, with a member per session index containing the expiration.
1151         DDF root = obj.addmember(issuer ? entityID.get() : "_shibnull").addmember(application.getRelyingParty(issuer)->getString("entityID").second);
1152         if (indexes) {
1153             for (set<string>::const_iterator x = indexes->begin(); x!=indexes->end(); ++x)
1154                 root.addmember(x->c_str()).string(timebuf);
1155         }
1156         else {
1157             root.addmember("_shibnull").string(timebuf);
1158         }
1159
1160         // Write it back.
1161         ostringstream lout;
1162         lout << obj;
1163
1164         if (ver > 0) {
1165             ver = m_storage_lite->updateText("Logout", name.get(), lout.str().c_str(), max(expires, oldexp), ver);
1166             if (ver <= 0) {
1167                 // Out of sync, or went missing, so retry.
1168                 return logout(application, issuer, nameid, indexes, expires, sessionsKilled);
1169             }
1170         }
1171         else if (!m_storage_lite->createText("Logout", name.get(), lout.str().c_str(), expires)) {
1172             // Hit a dup, so just retry, hopefully hitting the other branch.
1173             return logout(application, issuer, nameid, indexes, expires, sessionsKilled);
1174         }
1175
1176         obj.destroy();
1177         record.erase();
1178     }
1179
1180     // Read in potentially matching sessions.
1181     ver = m_storage_lite->readText("NameID", name.get(), &record);
1182     if (ver == 0) {
1183         m_log.debug("no active sessions to logout for supplied issuer and subject");
1184         return 0;
1185     }
1186
1187     istringstream in(record);
1188     in >> obj;
1189
1190     // The record contains child lists for each known session index.
1191     DDF key;
1192     DDF sessions = obj.first();
1193     while (sessions.islist()) {
1194         if (!indexes || indexes->empty() || indexes->count(sessions.name())) {
1195             key = sessions.first();
1196             while (key.isstring()) {
1197                 // Fetch the session for comparison.
1198                 Session* session = NULL;
1199                 try {
1200                     session = find(application, key.string());
1201                 }
1202                 catch (exception& ex) {
1203                     m_log.error("error locating session (%s): %s", key.string(), ex.what());
1204                 }
1205
1206                 if (session) {
1207                     Locker locker(session, false);
1208                     // Same issuer?
1209                     if (XMLString::equals(session->getEntityID(), entityID.get())) {
1210                         // Same NameID?
1211                         if (stronglyMatches(issuer->getEntityID(), application.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) {
1212                             sessionsKilled.push_back(key.string());
1213                             key.destroy();
1214                         }
1215                         else {
1216                             m_log.debug("session (%s) contained a non-matching NameID, leaving it alone", key.string());
1217                         }
1218                     }
1219                     else {
1220                         m_log.debug("session (%s) established by different IdP, leaving it alone", key.string());
1221                     }
1222                 }
1223                 else {
1224                     // Session's gone, so...
1225                     sessionsKilled.push_back(key.string());
1226                     key.destroy();
1227                 }
1228                 key = sessions.next();
1229             }
1230
1231             // No sessions left for this index?
1232             if (sessions.first().isnull())
1233                 sessions.destroy();
1234         }
1235         sessions = obj.next();
1236     }
1237
1238     if (obj.first().isnull())
1239         obj.destroy();
1240
1241     // If possible, write back the mapping record (this isn't crucial).
1242     try {
1243         if (obj.isnull()) {
1244             m_storage_lite->deleteText("NameID", name.get());
1245         }
1246         else if (!sessionsKilled.empty()) {
1247             ostringstream out;
1248             out << obj;
1249             if (m_storage_lite->updateText("NameID", name.get(), out.str().c_str(), 0, ver) <= 0)
1250                 m_log.warn("logout mapping record changed behind us, leaving it alone");
1251         }
1252     }
1253     catch (exception& ex) {
1254         m_log.error("error updating logout mapping record: %s", ex.what());
1255     }
1256
1257     return sessionsKilled.size();
1258 }
1259
1260 bool SSCache::stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const
1261 {
1262     if (!XMLString::equals(n1.getName(), n2.getName()))
1263         return false;
1264
1265     const XMLCh* s1 = n1.getFormat();
1266     const XMLCh* s2 = n2.getFormat();
1267     if (!s1 || !*s1)
1268         s1 = saml2::NameID::UNSPECIFIED;
1269     if (!s2 || !*s2)
1270         s2 = saml2::NameID::UNSPECIFIED;
1271     if (!XMLString::equals(s1,s2))
1272         return false;
1273
1274     s1 = n1.getNameQualifier();
1275     s2 = n2.getNameQualifier();
1276     if (!s1 || !*s1)
1277         s1 = idp;
1278     if (!s2 || !*s2)
1279         s2 = idp;
1280     if (!XMLString::equals(s1,s2))
1281         return false;
1282
1283     s1 = n1.getSPNameQualifier();
1284     s2 = n2.getSPNameQualifier();
1285     if (!s1 || !*s1)
1286         s1 = sp;
1287     if (!s2 || !*s2)
1288         s2 = sp;
1289     if (!XMLString::equals(s1,s2))
1290         return false;
1291
1292     return true;
1293 }
1294
1295 #endif
1296
1297 Session* SSCache::find(const Application& application, const char* key, const char* client_addr, time_t* timeout)
1298 {
1299 #ifdef _DEBUG
1300     xmltooling::NDC ndc("find");
1301 #endif
1302     StoredSession* session=NULL;
1303
1304     if (inproc) {
1305         m_log.debug("searching local cache for session (%s)", key);
1306         m_lock->rdlock();
1307         map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);
1308         if (i!=m_hashtable.end()) {
1309             // Save off and lock the session.
1310             session = i->second;
1311             session->lock();
1312             m_lock->unlock();
1313             m_log.debug("session found locally, validating it for use");
1314         }
1315         else {
1316             m_lock->unlock();
1317         }
1318     }
1319
1320     if (!session) {
1321         if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
1322             m_log.debug("session not found locally, remoting the search");
1323             // Remote the request.
1324             DDF in("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;
1325             DDFJanitor jin(in);
1326             in.structure();
1327             in.addmember("key").string(key);
1328             in.addmember("application_id").string(application.getId());
1329             if (timeout && *timeout) {
1330                 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
1331 #ifndef HAVE_GMTIME_R
1332                 struct tm* ptime=gmtime(timeout);
1333 #else
1334                 struct tm res;
1335                 struct tm* ptime=gmtime_r(timeout,&res);
1336 #endif
1337                 char timebuf[32];
1338                 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
1339                 in.addmember("timeout").string(timebuf);
1340             }
1341
1342             try {
1343                 out=application.getServiceProvider().getListenerService()->send(in);
1344                 if (!out.isstruct()) {
1345                     out.destroy();
1346                     m_log.debug("session not found in remote cache");
1347                     return NULL;
1348                 }
1349
1350                 // Wrap the results in a local entry and save it.
1351                 session = new StoredSession(this, out);
1352                 // The remote end has handled timeout issues, we handle address and expiration checks.
1353                 timeout = NULL;
1354             }
1355             catch (...) {
1356                 out.destroy();
1357                 throw;
1358             }
1359         }
1360         else {
1361             // We're out of process, so we can search the storage service directly.
1362 #ifndef SHIBSP_LITE
1363             if (!m_storage)
1364                 throw ConfigurationException("SessionCache lookup requires a StorageService.");
1365
1366             m_log.debug("searching for session (%s)", key);
1367
1368             DDF obj;
1369             time_t lastAccess;
1370             string record;
1371             int ver = m_storage->readText(key, "session", &record, &lastAccess);
1372             if (!ver)
1373                 return NULL;
1374
1375             m_log.debug("reconstituting session and checking validity");
1376
1377             istringstream in(record);
1378             in >> obj;
1379
1380             lastAccess -= m_cacheTimeout;   // adjusts it back to the last time the record's timestamp was touched
1381             time_t now=time(NULL);
1382
1383             if (timeout && *timeout > 0 && now - lastAccess >= *timeout) {
1384                 m_log.info("session timed out (ID: %s)", key);
1385                 remove(application, key);
1386                 const char* eid = obj["entity_id"].string();
1387                 if (!eid) {
1388                     obj.destroy();
1389                     throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1390                 }
1391                 string eid2(eid);
1392                 obj.destroy();
1393                 throw RetryableProfileException("Your session has expired, and you must re-authenticate.", namedparams(1, "entityID", eid2.c_str()));
1394             }
1395
1396             if (timeout) {
1397                 // Update storage expiration, if possible.
1398                 try {
1399                     m_storage->updateContext(key, now + m_cacheTimeout);
1400                 }
1401                 catch (exception& ex) {
1402                     m_log.error("failed to update session expiration: %s", ex.what());
1403                 }
1404             }
1405
1406             // Wrap the results in a local entry and save it.
1407             session = new StoredSession(this, obj);
1408             // We handled timeout issues, still need to handle address and expiration checks.
1409             timeout = NULL;
1410 #else
1411             throw ConfigurationException("SessionCache search requires a StorageService.");
1412 #endif
1413         }
1414
1415         if (inproc) {
1416             // Lock for writing and repeat the search to avoid duplication.
1417             m_lock->wrlock();
1418             SharedLock shared(m_lock, false);
1419             if (m_hashtable.count(key)) {
1420                 // We're using an existing session entry.
1421                 delete session;
1422                 session = m_hashtable[key];
1423                 session->lock();
1424             }
1425             else {
1426                 m_hashtable[key]=session;
1427                 session->lock();
1428             }
1429         }
1430     }
1431
1432     if (!XMLString::equals(session->getApplicationID(), application.getId())) {
1433         m_log.error("an application (%s) tried to access another application's session", application.getId());
1434         session->unlock();
1435         return NULL;
1436     }
1437
1438     // Verify currency and update the timestamp if indicated by caller.
1439     try {
1440         session->validate(application, client_addr, timeout);
1441     }
1442     catch (...) {
1443         session->unlock();
1444         remove(application, key);
1445         throw;
1446     }
1447
1448     return session;
1449 }
1450
1451 void SSCache::remove(const Application& application, const char* key)
1452 {
1453 #ifdef _DEBUG
1454     xmltooling::NDC ndc("remove");
1455 #endif
1456     // Take care of local copy.
1457     if (inproc)
1458         dormant(key);
1459
1460     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
1461         // Remove the session from storage directly.
1462 #ifndef SHIBSP_LITE
1463         m_storage->deleteContext(key);
1464         m_log.info("removed session (%s)", key);
1465
1466         TransactionLog* xlog = application.getServiceProvider().getTransactionLog();
1467         Locker locker(xlog);
1468         xlog->log.info("Destroyed session (applicationId: %s) (ID: %s)", application.getId(), key);
1469 #else
1470         throw ConfigurationException("SessionCache removal requires a StorageService.");
1471 #endif
1472     }
1473     else {
1474         // Remote the request.
1475         DDF in("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache");
1476         DDFJanitor jin(in);
1477         in.structure();
1478         in.addmember("key").string(key);
1479         in.addmember("application_id").string(application.getId());
1480
1481         DDF out = application.getServiceProvider().getListenerService()->send(in);
1482         out.destroy();
1483     }
1484 }
1485
1486 void SSCache::dormant(const char* key)
1487 {
1488 #ifdef _DEBUG
1489     xmltooling::NDC ndc("dormant");
1490 #endif
1491
1492     m_log.debug("deleting local copy of session (%s)", key);
1493
1494     // lock the cache for writing, which means we know nobody is sitting in find()
1495     m_lock->wrlock();
1496
1497     // grab the entry from the table
1498     map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);
1499     if (i==m_hashtable.end()) {
1500         m_lock->unlock();
1501         return;
1502     }
1503
1504     // ok, remove the entry and lock it
1505     StoredSession* entry=i->second;
1506     m_hashtable.erase(key);
1507     entry->lock();
1508
1509     // unlock the cache
1510     m_lock->unlock();
1511
1512     // we can release the cache entry lock because we know we're not in the cache anymore
1513     entry->unlock();
1514
1515     delete entry;
1516 }
1517
1518 void SSCache::cleanup()
1519 {
1520 #ifdef _DEBUG
1521     xmltooling::NDC ndc("cleanup");
1522 #endif
1523
1524     Mutex* mutex = Mutex::create();
1525
1526     // Load our configuration details...
1527     static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);
1528     const XMLCh* tag=m_root ? m_root->getAttributeNS(NULL,cleanupInterval) : NULL;
1529     int rerun_timer = 900;
1530     if (tag && *tag)
1531         rerun_timer = XMLString::parseInt(tag);
1532     if (rerun_timer <= 0)
1533         rerun_timer = 900;
1534
1535     mutex->lock();
1536
1537     m_log.info("cleanup thread started...run every %d secs; timeout after %d secs", rerun_timer, m_inprocTimeout);
1538
1539     while (!shutdown) {
1540         shutdown_wait->timedwait(mutex,rerun_timer);
1541         if (shutdown)
1542             break;
1543
1544         // Ok, let's run through the cleanup process and clean out
1545         // really old sessions.  This is a two-pass process.  The
1546         // first pass is done holding a read-lock while we iterate over
1547         // the cache.  The second pass doesn't need a lock because
1548         // the 'deletes' will lock the cache.
1549
1550         // Pass 1: iterate over the map and find all entries that have not been
1551         // used in the allotted timeout.
1552         vector<string> stale_keys;
1553         time_t stale = time(NULL) - m_inprocTimeout;
1554
1555         m_log.debug("cleanup thread running");
1556
1557         m_lock->rdlock();
1558         for (map<string,StoredSession*>::const_iterator i=m_hashtable.begin(); i!=m_hashtable.end(); ++i) {
1559             // If the last access was BEFORE the stale timeout...
1560             i->second->lock();
1561             time_t last=i->second->getLastAccess();
1562             i->second->unlock();
1563             if (last < stale)
1564                 stale_keys.push_back(i->first);
1565         }
1566         m_lock->unlock();
1567
1568         if (!stale_keys.empty()) {
1569             m_log.info("purging %d old sessions", stale_keys.size());
1570
1571             // Pass 2: walk through the list of stale entries and remove them from the cache
1572             for (vector<string>::const_iterator j = stale_keys.begin(); j != stale_keys.end(); ++j)
1573                 dormant(j->c_str());
1574         }
1575
1576         m_log.debug("cleanup thread completed");
1577     }
1578
1579     m_log.info("cleanup thread exiting");
1580
1581     mutex->unlock();
1582     delete mutex;
1583     Thread::exit(NULL);
1584 }
1585
1586 void* SSCache::cleanup_fn(void* cache_p)
1587 {
1588 #ifndef WIN32
1589     // First, let's block all signals
1590     Thread::mask_all_signals();
1591 #endif
1592
1593     // Now run the cleanup process.
1594     reinterpret_cast<SSCache*>(cache_p)->cleanup();
1595     return NULL;
1596 }
1597
1598 #ifndef SHIBSP_LITE
1599
1600 void SSCache::receive(DDF& in, ostream& out)
1601 {
1602 #ifdef _DEBUG
1603     xmltooling::NDC ndc("receive");
1604 #endif
1605
1606     if (!strcmp(in.name(),"find::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1607         const char* key=in["key"].string();
1608         if (!key)
1609             throw ListenerException("Required parameters missing for session lookup.");
1610
1611         const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());
1612         if (!app)
1613             throw ListenerException("Application not found, check configuration?");
1614
1615         // Do an unversioned read.
1616         string record;
1617         time_t lastAccess;
1618         if (!m_storage->readText(key, "session", &record, &lastAccess)) {
1619             DDF ret(NULL);
1620             DDFJanitor jan(ret);
1621             out << ret;
1622             return;
1623         }
1624
1625         // Adjust for expiration to recover last access time and check timeout.
1626         lastAccess -= m_cacheTimeout;
1627         time_t now=time(NULL);
1628
1629         // See if we need to check for a timeout.
1630         if (in["timeout"].string()) {
1631             time_t timeout = 0;
1632             auto_ptr_XMLCh dt(in["timeout"].string());
1633             DateTime dtobj(dt.get());
1634             dtobj.parseDateTime();
1635             timeout = dtobj.getEpoch();
1636
1637             if (timeout > 0 && now - lastAccess >= timeout) {
1638                 m_log.info("session timed out (ID: %s)", key);
1639                 remove(*app, key);
1640                 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1641             }
1642
1643             // Update storage expiration, if possible.
1644             try {
1645                 m_storage->updateContext(key, now + m_cacheTimeout);
1646             }
1647             catch (exception& ex) {
1648                 m_log.error("failed to update session expiration: %s", ex.what());
1649             }
1650         }
1651
1652         // Send the record back.
1653         out << record;
1654     }
1655     else if (!strcmp(in.name(),"touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1656         const char* key=in["key"].string();
1657         if (!key)
1658             throw ListenerException("Required parameters missing for session check.");
1659
1660         // Do a versioned read.
1661         string record;
1662         time_t lastAccess;
1663         int curver = in["version"].integer();
1664         int ver = m_storage->readText(key, "session", &record, &lastAccess, curver);
1665         if (ver == 0) {
1666             m_log.warn("unsuccessful versioned read of session (ID: %s), caches out of sync?", key);
1667             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1668         }
1669
1670         // Adjust for expiration to recover last access time and check timeout.
1671         lastAccess -= m_cacheTimeout;
1672         time_t now=time(NULL);
1673
1674         // See if we need to check for a timeout.
1675         time_t timeout = 0;
1676         auto_ptr_XMLCh dt(in["timeout"].string());
1677         if (dt.get()) {
1678             DateTime dtobj(dt.get());
1679             dtobj.parseDateTime();
1680             timeout = dtobj.getEpoch();
1681         }
1682
1683         if (timeout > 0 && now - lastAccess >= timeout) {
1684             m_log.info("session timed out (ID: %s)", key);
1685             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1686         }
1687
1688         // Update storage expiration, if possible.
1689         try {
1690             m_storage->updateContext(key, now + m_cacheTimeout);
1691         }
1692         catch (exception& ex) {
1693             m_log.error("failed to update session expiration: %s", ex.what());
1694         }
1695
1696         if (ver > curver) {
1697             // Send the record back.
1698             out << record;
1699         }
1700         else {
1701             DDF ret(NULL);
1702             DDFJanitor jan(ret);
1703             out << ret;
1704         }
1705     }
1706     else if (!strcmp(in.name(),"remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1707         const char* key=in["key"].string();
1708         if (!key)
1709             throw ListenerException("Required parameter missing for session removal.");
1710
1711         const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());
1712         if (!app)
1713             throw ConfigurationException("Application not found, check configuration?");
1714
1715         remove(*app, key);
1716         DDF ret(NULL);
1717         DDFJanitor jan(ret);
1718         out << ret;
1719     }
1720 }
1721
1722 #endif