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