ae2135ed0c1721cbc473e4b8f31149ebc1560aa2
[shibboleth/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 "SessionCache.h"
36 #include "TransactionLog.h"
37 #include "attribute/Attribute.h"
38 #include "remoting/ListenerService.h"
39 #include "util/SPConstants.h"
40
41 #include <saml/SAMLConfig.h>
42 #include <xmltooling/util/NDC.h>
43 #include <xmltooling/util/StorageService.h>
44 #include <xmltooling/util/XMLHelper.h>
45 #include <xercesc/util/XMLUniDefs.hpp>
46
47 using namespace shibsp;
48 using namespace opensaml::saml2md;
49 using namespace opensaml;
50 using namespace xmltooling;
51 using namespace std;
52
53 namespace shibsp {
54
55     class SSCache;
56     class StoredSession : public virtual Session
57     {
58     public:
59         StoredSession(SSCache* cache, DDF& obj) : m_obj(obj), m_nameid(NULL), m_cache(cache) {
60             const char* nameid = obj["nameid"].string();
61             if (nameid) {
62                 // Parse and bind the document into an XMLObject.
63                 istringstream instr(nameid);
64                 DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); 
65                 XercesJanitor<DOMDocument> janitor(doc);
66                 auto_ptr<saml2::NameID> n(saml2::NameIDBuilder::buildNameID());
67                 n->unmarshall(doc->getDocumentElement(), true);
68                 janitor.release();
69                 m_nameid = n.release();
70             }
71         }
72         
73         ~StoredSession();
74         
75         Lockable* lock() {
76             return this;
77         }
78         void unlock() {
79             delete this;
80         }
81         
82         const char* getID() const {
83             return m_obj.name();
84         }
85         const char* getClientAddress() const {
86             return m_obj["client_addr"].string();
87         }
88         const char* getEntityID() const {
89             return m_obj["entity_id"].string();
90         }
91         const char* getProtocol() const {
92             return m_obj["protocol"].string();
93         }
94         const char* getAuthnInstant() const {
95             return m_obj["authn_instant"].string();
96         }
97         const opensaml::saml2::NameID* getNameID() const {
98             return m_nameid;
99         }
100         const char* getSessionIndex() const {
101             return m_obj["session_index"].string();
102         }
103         const char* getAuthnContextClassRef() const {
104             return m_obj["authncontext_class"].string();
105         }
106         const char* getAuthnContextDeclRef() const {
107             return m_obj["authncontext_decl"].string();
108         }
109         const vector<Attribute*>& getAttributes() const {
110             if (m_attributes.empty())
111                 unmarshallAttributes();
112             return m_attributes;
113         }
114         const multimap<string,const Attribute*>& getIndexedAttributes() const {
115             if (m_attributeIndex.empty()) {
116                 if (m_attributes.empty())
117                     unmarshallAttributes();
118                 for (vector<Attribute*>::const_iterator a = m_attributes.begin(); a != m_attributes.end(); ++a) {
119                     const vector<string>& aliases = (*a)->getAliases();
120                     for (vector<string>::const_iterator alias = aliases.begin(); alias != aliases.end(); ++alias)
121                         m_attributeIndex.insert(make_pair(*alias, *a));
122                 }
123             }
124             return m_attributeIndex;
125         }
126         const vector<const char*>& getAssertionIDs() const {
127             if (m_ids.empty()) {
128                 DDF ids = m_obj["assertions"];
129                 DDF id = ids.first();
130                 while (id.isstring()) {
131                     m_ids.push_back(id.string());
132                     id = ids.next();
133                 }
134             }
135             return m_ids;
136         }
137         
138         void addAttributes(const vector<Attribute*>& attributes);
139         const Assertion* getAssertion(const char* id) const;
140         void addAssertion(Assertion* assertion);
141
142     private:
143         void unmarshallAttributes() const;
144
145         DDF m_obj;
146         saml2::NameID* m_nameid;
147         mutable vector<Attribute*> m_attributes;
148         mutable multimap<string,const Attribute*> m_attributeIndex;
149         mutable vector<const char*> m_ids;
150         mutable map<string,Assertion*> m_tokens;
151         SSCache* m_cache;
152     };
153     
154     class SSCache : public SessionCache, public virtual Remoted
155     {
156     public:
157         SSCache(const DOMElement* e);
158         ~SSCache();
159     
160         void receive(DDF& in, ostream& out);
161         
162         string insert(
163             time_t expires,
164             const Application& application,
165             const char* client_addr=NULL,
166             const saml2md::EntityDescriptor* issuer=NULL,
167             const XMLCh* protocol=NULL,
168             const saml2::NameID* nameid=NULL,
169             const XMLCh* authn_instant=NULL,
170             const XMLCh* session_index=NULL,
171             const XMLCh* authncontext_class=NULL,
172             const XMLCh* authncontext_decl=NULL,
173             const vector<const Assertion*>* tokens=NULL,
174             const vector<Attribute*>* attributes=NULL
175             );
176         Session* find(const char* key, const Application& application, const char* client_addr=NULL, time_t* timeout=NULL);
177         void remove(const char* key, const Application& application);
178         bool matches(
179             const char* key,
180             const saml2md::EntityDescriptor* issuer,
181             const saml2::NameID& nameid,
182             const set<string>* indexes,
183             const Application& application
184             );
185         vector<string>::size_type logout(
186             const saml2md::EntityDescriptor* issuer,
187             const saml2::NameID& nameid,
188             const set<string>* indexes,
189             time_t expires,
190             const Application& application,
191             vector<string>& sessions
192             );
193
194         Category& m_log;
195         StorageService* m_storage;
196
197     private:
198         // maintain back-mappings of NameID/SessionIndex -> session key
199         void insert(const char* key, time_t expires, const char* name, const char* index);
200
201         bool stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const;
202     };
203
204     SessionCache* SHIBSP_DLLLOCAL StorageServiceCacheFactory(const DOMElement* const & e)
205     {
206         return new SSCache(e);
207     }
208
209     static const XMLCh _StorageService[] =   UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e);
210 }
211
212 StoredSession::~StoredSession()
213 {
214     m_obj.destroy();
215     delete m_nameid;
216     for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
217     for_each(m_tokens.begin(), m_tokens.end(), cleanup_pair<string,Assertion>());
218 }
219
220 void StoredSession::unmarshallAttributes() const
221 {
222     Attribute* attribute;
223     DDF attrs = m_obj["attributes"];
224     DDF attr = attrs.first();
225     while (!attr.isnull()) {
226         try {
227             attribute = Attribute::unmarshall(attr);
228             m_attributes.push_back(attribute);
229             m_attributeIndex.insert(make_pair(attribute->getId(), attribute));
230             if (m_cache->m_log.isDebugEnabled())
231                 m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",
232                     attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");
233         }
234         catch (AttributeException& ex) {
235             const char* id = attr.first().name();
236             m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());
237         }
238         attr = attrs.next();
239     }
240 }
241
242 void StoredSession::addAttributes(const vector<Attribute*>& attributes)
243 {
244 #ifdef _DEBUG
245     xmltooling::NDC ndc("addAttributes");
246 #endif
247
248     m_cache->m_log.debug("adding attributes to session (%s)", getID());
249     
250     int ver;
251     do {
252         DDF attr;
253         DDF attrs = m_obj["attributes"];
254         if (!attrs.islist())
255             attrs = m_obj.addmember("attributes").list();
256         for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a) {
257             attr = (*a)->marshall();
258             attrs.add(attr);
259         }
260         
261         // Tentatively increment the version.
262         m_obj["version"].integer(m_obj["version"].integer()+1);
263         
264         ostringstream str;
265         str << m_obj;
266         string record(str.str()); 
267
268         try {
269             ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);
270         }
271         catch (exception&) {
272             // Roll back modification to record.
273             m_obj["version"].integer(m_obj["version"].integer()-1);
274             vector<Attribute*>::size_type count = attributes.size();
275             while (count--)
276                 attrs.last().destroy();            
277             throw;
278         }
279
280         if (ver <= 0) {
281             // Roll back modification to record.
282             m_obj["version"].integer(m_obj["version"].integer()-1);
283             vector<Attribute*>::size_type count = attributes.size();
284             while (count--)
285                 attrs.last().destroy();            
286         }
287         if (!ver) {
288             // Fatal problem with update.
289             throw IOException("Unable to update stored session.");
290         }
291         else if (ver < 0) {
292             // Out of sync.
293             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
294             ver = m_cache->m_storage->readText(getID(), "session", &record, NULL);
295             if (!ver) {
296                 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
297                 throw IOException("Unable to read back stored session.");
298             }
299             
300             // Reset object.
301             DDF newobj;
302             istringstream in(record);
303             in >> newobj;
304
305             m_ids.clear();
306             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
307             m_attributes.clear();
308             m_attributeIndex.clear();
309             newobj["version"].integer(ver);
310             m_obj.destroy();
311             m_obj = newobj;
312
313             ver = -1;
314         }
315     } while (ver < 0);  // negative indicates a sync issue so we retry
316
317     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();
318     Locker locker(xlog);
319     xlog->log.infoStream() <<
320         "Added the following attributes to session (ID: " <<
321             getID() <<
322         ") for (applicationId: " <<
323             m_obj["application_id"].string() <<
324         ") {";
325     for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a)
326         xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";
327     xlog->log.info("}");
328
329     // We own them now, so clean them up.
330     for_each(attributes.begin(), attributes.end(), xmltooling::cleanup<Attribute>());
331 }
332
333 const Assertion* StoredSession::getAssertion(const char* id) const
334 {
335     map<string,Assertion*>::const_iterator i = m_tokens.find(id);
336     if (i!=m_tokens.end())
337         return i->second;
338     
339     string tokenstr;
340     if (!m_cache->m_storage->readText(getID(), id, &tokenstr, NULL))
341         throw FatalProfileException("Assertion not found in cache.");
342
343     // Parse and bind the document into an XMLObject.
344     istringstream instr(tokenstr);
345     DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); 
346     XercesJanitor<DOMDocument> janitor(doc);
347     auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));
348     janitor.release();
349     
350     Assertion* token = dynamic_cast<Assertion*>(xmlObject.get());
351     if (!token)
352         throw FatalProfileException("Request for cached assertion returned an unknown object type.");
353
354     // Transfer ownership to us.
355     xmlObject.release();
356     m_tokens[id]=token;
357     return token;
358 }
359
360 void StoredSession::addAssertion(Assertion* assertion)
361 {
362 #ifdef _DEBUG
363     xmltooling::NDC ndc("addAssertion");
364 #endif
365     
366     if (!assertion)
367         throw FatalProfileException("Unknown object type passed to session for storage.");
368
369     auto_ptr_char id(assertion->getID());
370
371     m_cache->m_log.debug("adding assertion (%s) to session (%s)", id.get(), getID());
372
373     time_t exp;
374     if (!m_cache->m_storage->readText(getID(), "session", NULL, &exp))
375         throw IOException("Unable to load expiration time for stored session.");
376
377     ostringstream tokenstr;
378     tokenstr << *assertion;
379     if (!m_cache->m_storage->createText(getID(), id.get(), tokenstr.str().c_str(), exp))
380         throw IOException("Attempted to insert duplicate assertion ID into session.");
381     
382     int ver;
383     do {
384         DDF token = DDF(NULL).string(id.get());
385         m_obj["assertions"].add(token);
386
387         // Tentatively increment the version.
388         m_obj["version"].integer(m_obj["version"].integer()+1);
389     
390         ostringstream str;
391         str << m_obj;
392         string record(str.str()); 
393
394         try {
395             ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);
396         }
397         catch (exception&) {
398             token.destroy();
399             m_obj["version"].integer(m_obj["version"].integer()-1);
400             m_cache->m_storage->deleteText(getID(), id.get());
401             throw;
402         }
403
404         if (ver <= 0) {
405             token.destroy();
406             m_obj["version"].integer(m_obj["version"].integer()-1);
407         }            
408         if (!ver) {
409             // Fatal problem with update.
410             m_cache->m_log.error("updateText failed on StorageService for session (%s)", getID());
411             m_cache->m_storage->deleteText(getID(), id.get());
412             throw IOException("Unable to update stored session.");
413         }
414         else if (ver < 0) {
415             // Out of sync.
416             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
417             ver = m_cache->m_storage->readText(getID(), "session", &record, NULL);
418             if (!ver) {
419                 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
420                 m_cache->m_storage->deleteText(getID(), id.get());
421                 throw IOException("Unable to read back stored session.");
422             }
423             
424             // Reset object.
425             DDF newobj;
426             istringstream in(record);
427             in >> newobj;
428
429             m_ids.clear();
430             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
431             m_attributes.clear();
432             m_attributeIndex.clear();
433             newobj["version"].integer(ver);
434             m_obj.destroy();
435             m_obj = newobj;
436             
437             ver = -1;
438         }
439     } while (ver < 0); // negative indicates a sync issue so we retry
440
441     m_ids.clear();
442     delete assertion;
443
444     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();
445     Locker locker(xlog);
446     xlog->log.info(
447         "Added assertion (ID: %s) to session for (applicationId: %s) with (ID: %s)",
448         id.get(), m_obj["application_id"].string(), getID()
449         );
450 }
451
452 SSCache::SSCache(const DOMElement* e)
453     : SessionCache(e, 3600), m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), m_storage(NULL)
454 {
455     SPConfig& conf = SPConfig::getConfig();
456     const XMLCh* tag = e ? e->getAttributeNS(NULL,_StorageService) : NULL;
457     if (tag && *tag) {
458         auto_ptr_char ssid(tag);
459         m_storage = conf.getServiceProvider()->getStorageService(ssid.get());
460         if (m_storage)
461             m_log.info("bound to StorageService (%s)", ssid.get());
462         else
463             throw ConfigurationException("SessionCache unable to locate StorageService, check configuration.");
464     }
465
466     ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
467     if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
468         listener->regListener("find::"REMOTED_SESSION_CACHE"::SessionCache",this);
469         listener->regListener("remove::"REMOTED_SESSION_CACHE"::SessionCache",this);
470         listener->regListener("touch::"REMOTED_SESSION_CACHE"::SessionCache",this);
471         listener->regListener("getAssertion::"REMOTED_SESSION_CACHE"::SessionCache",this);
472     }
473     else {
474         m_log.info("no ListenerService available, cache remoting disabled");
475     }
476 }
477
478 SSCache::~SSCache()
479 {
480     SPConfig& conf = SPConfig::getConfig();
481     ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
482     if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
483         listener->unregListener("find::"REMOTED_SESSION_CACHE"::SessionCache",this);
484         listener->unregListener("remove::"REMOTED_SESSION_CACHE"::SessionCache",this);
485         listener->unregListener("touch::"REMOTED_SESSION_CACHE"::SessionCache",this);
486         listener->unregListener("getAssertion::"REMOTED_SESSION_CACHE"::SessionCache",this);
487     }
488 }
489
490 void SSCache::insert(const char* key, time_t expires, const char* name, const char* index)
491 {
492     string dup;
493     if (strlen(name) > 255) {
494         dup = string(name).substr(0,255);
495         name = dup.c_str();
496     }
497
498     DDF obj;
499     DDFJanitor jobj(obj);
500
501     // Since we can't guarantee uniqueness, check for an existing record.
502     string record;
503     time_t recordexp;
504     int ver = m_storage->readText("NameID", name, &record, &recordexp);
505     if (ver > 0) {
506         // Existing record, so we need to unmarshall it.
507         istringstream in(record);
508         in >> obj;
509     }
510     else {
511         // New record.
512         obj.structure();
513     }
514
515     if (!index || !*index)
516         index = "_shibnull";
517     DDF sessions = obj.addmember(index);
518     if (!sessions.islist())
519         sessions.list();
520     DDF session = DDF(NULL).string(key);
521     sessions.add(session);
522
523     // Remarshall the record.
524     ostringstream out;
525     out << obj;
526
527     // Try and store it back...
528     if (ver > 0) {
529         ver = m_storage->updateText("NameID", name, out.str().c_str(), max(expires, recordexp), ver);
530         if (ver <= 0) {
531             // Out of sync, or went missing, so retry.
532             return insert(key, expires, name, index);
533         }
534     }
535     else if (!m_storage->createText("NameID", name, out.str().c_str(), expires)) {
536         // Hit a dup, so just retry, hopefully hitting the other branch.
537         return insert(key, expires, name, index);
538     }
539 }
540
541 string SSCache::insert(
542     time_t expires,
543     const Application& application,
544     const char* client_addr,
545     const saml2md::EntityDescriptor* issuer,
546     const XMLCh* protocol,
547     const saml2::NameID* nameid,
548     const XMLCh* authn_instant,
549     const XMLCh* session_index,
550     const XMLCh* authncontext_class,
551     const XMLCh* authncontext_decl,
552     const vector<const Assertion*>* tokens,
553     const vector<Attribute*>* attributes
554     )
555 {
556 #ifdef _DEBUG
557     xmltooling::NDC ndc("insert");
558 #endif
559
560     m_log.debug("creating new session");
561
562     time_t now = time(NULL);
563     auto_ptr_char index(session_index);
564     auto_ptr_char entity_id(issuer ? issuer->getEntityID() : NULL);
565     auto_ptr_char name(nameid ? nameid->getName() : NULL);
566
567     if (nameid) {
568         // Check for a pending logout.
569         if (strlen(name.get()) > 255)
570             const_cast<char*>(name.get())[255] = 0;
571         string pending;
572         int ver = m_storage->readText("Logout", name.get(), &pending);
573         if (ver > 0) {
574             DDF pendobj;
575             DDFJanitor jpend(pendobj);
576             istringstream pstr(pending);
577             pstr >> pendobj;
578             // IdP.SP.index contains logout expiration, if any.
579             DDF deadmenwalking = pendobj[issuer ? entity_id.get() : "_shibnull"][application.getString("entityID").second];
580             const char* logexpstr = deadmenwalking[session_index ? index.get() : "_shibnull"].string();
581             if (!logexpstr && session_index)    // we tried an exact session match, now try for NULL
582                 logexpstr = deadmenwalking["_shibnull"].string();
583             if (logexpstr) {
584                 auto_ptr_XMLCh dt(logexpstr);
585                 DateTime dtobj(dt.get());
586                 dtobj.parseDateTime();
587                 time_t logexp = dtobj.getEpoch();
588                 if (now - XMLToolingConfig::getConfig().clock_skew_secs < logexp)
589                     throw FatalProfileException("A logout message from your identity provider has blocked your login attempt.");
590             }
591         }
592     }
593
594     auto_ptr_char key(SAMLConfig::getConfig().generateIdentifier());
595
596     // Store session properties in DDF.
597     DDF obj = DDF(key.get()).structure();
598     obj.addmember("version").integer(1);
599     obj.addmember("application_id").string(application.getId());
600
601     // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
602 #ifndef HAVE_GMTIME_R
603     struct tm* ptime=gmtime(&expires);
604 #else
605     struct tm res;
606     struct tm* ptime=gmtime_r(&expires,&res);
607 #endif
608     char timebuf[32];
609     strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
610     obj.addmember("expires").string(timebuf);
611
612     if (client_addr)
613         obj.addmember("client_addr").string(client_addr);
614     if (issuer)
615         obj.addmember("entity_id").string(entity_id.get());
616     if (protocol) {
617         auto_ptr_char prot(protocol);
618         obj.addmember("protocol").string(prot.get());
619     }
620     if (authn_instant) {
621         auto_ptr_char instant(authn_instant);
622         obj.addmember("authn_instant").string(instant.get());
623     }
624     if (session_index)
625         obj.addmember("session_index").string(index.get());
626     if (authncontext_class) {
627         auto_ptr_char ac(authncontext_class);
628         obj.addmember("authncontext_class").string(ac.get());
629     }
630     if (authncontext_decl) {
631         auto_ptr_char ad(authncontext_decl);
632         obj.addmember("authncontext_decl").string(ad.get());
633     }
634
635     if (nameid) {
636         ostringstream namestr;
637         namestr << *nameid;
638         obj.addmember("nameid").string(namestr.str().c_str());
639     }
640
641     if (tokens) {
642         obj.addmember("assertions").list();
643         for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
644             auto_ptr_char tokenid((*t)->getID());
645             DDF tokid = DDF(NULL).string(tokenid.get());
646             obj["assertions"].add(tokid);
647         }
648     }
649     
650     if (attributes) {
651         DDF attr;
652         DDF attrlist = obj.addmember("attributes").list();
653         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) {
654             attr = (*a)->marshall();
655             attrlist.add(attr);
656         }
657     }
658     
659     ostringstream record;
660     record << obj;
661     
662     m_log.debug("storing new session...");
663     if (!m_storage->createText(key.get(), "session", record.str().c_str(), now + m_cacheTimeout))
664         throw FatalProfileException("Attempted to create a session with a duplicate key.");
665     
666     // Store the reverse mapping for logout.
667     try {
668         if (nameid)
669             insert(key.get(), expires, name.get(), index.get());
670     }
671     catch (exception& ex) {
672         m_log.error("error storing back mapping of NameID for logout: %s", ex.what());
673     }
674
675     if (tokens) {
676         try {
677             for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
678                 ostringstream tokenstr;
679                 tokenstr << *(*t);
680                 auto_ptr_char tokenid((*t)->getID());
681                 if (!m_storage->createText(key.get(), tokenid.get(), tokenstr.str().c_str(), now + m_cacheTimeout))
682                     throw IOException("duplicate assertion ID ($1)", params(1, tokenid.get()));
683             }
684         }
685         catch (exception& ex) {
686             m_log.error("error storing assertion along with session: %s", ex.what());
687         }
688     }
689
690     const char* pid = obj["entity_id"].string();
691     m_log.info("new session created: SessionID (%s) IdP (%s) Address (%s)", key.get(), pid ? pid : "none", client_addr);
692
693     // Transaction Logging
694     TransactionLog* xlog = application.getServiceProvider().getTransactionLog();
695     Locker locker(xlog);
696     xlog->log.infoStream() <<
697         "New session (ID: " <<
698             key.get() <<
699         ") with (applicationId: " <<
700             application.getId() <<
701         ") for principal from (IdP: " <<
702             (pid ? pid : "none") <<
703         ") at (ClientAddress: " <<
704             (client_addr ? client_addr : "none") <<
705         ") with (NameIdentifier: " <<
706             (nameid ? name.get() : "none") <<
707         ")";
708     
709     if (attributes) {
710         xlog->log.infoStream() <<
711             "Cached the following attributes with session (ID: " <<
712                 key.get() <<
713             ") for (applicationId: " <<
714                 application.getId() <<
715             ") {";
716         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a)
717             xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";
718         xlog->log.info("}");
719     }
720
721     return key.get();
722 }
723
724 Session* SSCache::find(const char* key, const Application& application, const char* client_addr, time_t* timeout)
725 {
726 #ifdef _DEBUG
727     xmltooling::NDC ndc("find");
728 #endif
729
730     m_log.debug("searching for session (%s)", key);
731     
732     time_t lastAccess;
733     string record;
734     int ver = m_storage->readText(key, "session", &record, &lastAccess);
735     if (!ver)
736         return NULL;
737     
738     m_log.debug("reconstituting session and checking validity");
739     
740     DDF obj;
741     istringstream in(record);
742     in >> obj;
743     
744     if (!XMLString::equals(obj["application_id"].string(), application.getId())) {
745         m_log.error("an application (%s) tried to access another application's session", application.getId());
746         obj.destroy();
747         return NULL;
748     }
749
750     if (client_addr) {
751         if (m_log.isDebugEnabled())
752             m_log.debug("comparing client address %s against %s", client_addr, obj["client_addr"].string());
753         if (strcmp(obj["client_addr"].string(),client_addr)) {
754             m_log.warn("client address mismatch");
755             remove(key, application);
756             RetryableProfileException ex(
757                 "Your IP address ($1) does not match the address recorded at the time the session was established.",
758                 params(1,client_addr)
759                 );
760             string eid(obj["entity_id"].string());
761             obj.destroy();
762             if (eid.empty())
763                 throw ex;
764             MetadataProvider* m=application.getMetadataProvider();
765             Locker locker(m);
766             annotateException(&ex,m->getEntityDescriptor(eid.c_str(),false)); // throws it
767         }
768     }
769
770     lastAccess -= m_cacheTimeout;   // adjusts it back to the last time the record's timestamp was touched
771     time_t now=time(NULL);
772     
773     if (timeout && *timeout > 0 && now - lastAccess >= *timeout) {
774         m_log.info("session timed out (ID: %s)", key);
775         remove(key, application);
776         RetryableProfileException ex("Your session has expired, and you must re-authenticate.");
777         string eid(obj["entity_id"].string());
778         obj.destroy();
779         if (eid.empty())
780             throw ex;
781         MetadataProvider* m=application.getMetadataProvider();
782         Locker locker(m);
783         annotateException(&ex,m->getEntityDescriptor(eid.c_str(),false)); // throws it
784     }
785     
786     auto_ptr_XMLCh exp(obj["expires"].string());
787     if (exp.get()) {
788         DateTime iso(exp.get());
789         iso.parseDateTime();
790         if (now > iso.getEpoch()) {
791             m_log.info("session expired (ID: %s)", key);
792             remove(key, application);
793             RetryableProfileException ex("Your session has expired, and you must re-authenticate.");
794             string eid(obj["entity_id"].string());
795             obj.destroy();
796             if (eid.empty())
797                 throw ex;
798             MetadataProvider* m=application.getMetadataProvider();
799             Locker locker(m);
800             annotateException(&ex,m->getEntityDescriptor(eid.c_str(),false)); // throws it
801         }
802     }
803     
804     if (timeout) {
805         // Update storage expiration, if possible.
806         try {
807             m_storage->updateContext(key, now + m_cacheTimeout);
808         }
809         catch (exception& ex) {
810             m_log.error("failed to update session expiration: %s", ex.what());
811         }
812     }
813
814     // Finally build the Session object.
815     try {
816         return new StoredSession(this, obj);
817     }
818     catch (exception&) {
819         obj.destroy();
820         throw;
821     }
822 }
823
824 void SSCache::remove(const char* key, const Application& application)
825 {
826 #ifdef _DEBUG
827     xmltooling::NDC ndc("remove");
828 #endif
829
830     m_storage->deleteContext(key);
831     m_log.info("removed session (%s)", key);
832
833     TransactionLog* xlog = application.getServiceProvider().getTransactionLog();
834     Locker locker(xlog);
835     xlog->log.info("Destroyed session (applicationId: %s) (ID: %s)", application.getId(), key);
836 }
837
838 bool SSCache::matches(
839     const char* key,
840     const saml2md::EntityDescriptor* issuer,
841     const saml2::NameID& nameid,
842     const set<string>* indexes,
843     const Application& application
844     )
845 {
846     auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL);
847     try {
848         Session* session = find(key, application);
849         if (session) {
850             Locker locker(session);
851             if (XMLString::equals(session->getEntityID(), entityID.get()) && session->getNameID() &&
852                     stronglyMatches(issuer->getEntityID(), application.getXMLString("entityID").second, nameid, *session->getNameID())) {
853                 return (!indexes || indexes->empty() || (session->getSessionIndex() ? (indexes->count(session->getSessionIndex())>0) : false));
854             }
855         }
856     }
857     catch (exception& ex) {
858         m_log.error("error while matching session (%s): %s", key, ex.what());
859     }
860     return false;
861 }
862
863 vector<string>::size_type SSCache::logout(
864     const saml2md::EntityDescriptor* issuer,
865     const saml2::NameID& nameid,
866     const set<string>* indexes,
867     time_t expires,
868     const Application& application,
869     vector<string>& sessionsKilled
870     )
871 {
872 #ifdef _DEBUG
873     xmltooling::NDC ndc("logout");
874 #endif
875
876     auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL);
877     auto_ptr_char name(nameid.getName());
878
879     m_log.info("request to logout sessions from (%s) for (%s)", entityID.get() ? entityID.get() : "unknown", name.get());
880
881     if (strlen(name.get()) > 255)
882         const_cast<char*>(name.get())[255] = 0;
883
884     DDF obj;
885     DDFJanitor jobj(obj);
886     string record;
887     int ver;
888
889     if (expires) {
890         // Record the logout to prevent post-delivered assertions.
891         // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
892 #ifndef HAVE_GMTIME_R
893         struct tm* ptime=gmtime(&expires);
894 #else
895         struct tm res;
896         struct tm* ptime=gmtime_r(&expires,&res);
897 #endif
898         char timebuf[32];
899         strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
900
901         time_t oldexp = 0;
902         ver = m_storage->readText("Logout", name.get(), &record, &oldexp);
903         if (ver > 0) {
904             istringstream lin(record);
905             lin >> obj;
906         }
907         else {
908             obj = DDF(NULL).structure();
909         }
910
911         // Structure is keyed by the IdP and SP, with a member per session index containing the expiration.
912         DDF root = obj.addmember(issuer ? entityID.get() : "_shibnull").addmember(application.getString("entityID").second);
913         if (indexes) {
914             for (set<string>::const_iterator x = indexes->begin(); x!=indexes->end(); ++x)
915                 root.addmember(x->c_str()).string(timebuf);
916         }
917         else {
918             root.addmember("_shibnull").string(timebuf);
919         }
920
921         // Write it back.
922         ostringstream lout;
923         lout << obj;
924
925         if (ver > 0) {
926             ver = m_storage->updateText("Logout", name.get(), lout.str().c_str(), max(expires, oldexp), ver);
927             if (ver <= 0) {
928                 // Out of sync, or went missing, so retry.
929                 return logout(issuer, nameid, indexes, expires, application, sessionsKilled);
930             }
931         }
932         else if (!m_storage->createText("Logout", name.get(), lout.str().c_str(), expires)) {
933             // Hit a dup, so just retry, hopefully hitting the other branch.
934             return logout(issuer, nameid, indexes, expires, application, sessionsKilled);
935         }
936
937         obj.destroy();
938         record.erase();
939     }
940
941     // Read in potentially matching sessions.
942     ver = m_storage->readText("NameID", name.get(), &record);
943     if (ver == 0) {
944         m_log.debug("no active sessions to logout for supplied issuer and subject");
945         return 0;
946     }
947
948     istringstream in(record);
949     in >> obj;
950
951     // The record contains child lists for each known session index.
952     DDF key;
953     DDF sessions = obj.first();
954     while (sessions.islist()) {
955         if (!indexes || indexes->empty() || indexes->count(sessions.name())) {
956             key = sessions.first();
957             while (key.isstring()) {
958                 // Fetch the session for comparison.
959                 Session* session = NULL;
960                 try {
961                     session = find(key.string(), application);
962                 }
963                 catch (exception& ex) {
964                     m_log.error("error locating session (%s): %s", key.string(), ex.what());
965                 }
966
967                 if (session) {
968                     Locker locker(session);
969                     // Same issuer?
970                     if (XMLString::equals(session->getEntityID(), entityID.get())) {
971                         // Same NameID?
972                         if (stronglyMatches(issuer->getEntityID(), application.getXMLString("entityID").second, nameid, *session->getNameID())) {
973                             sessionsKilled.push_back(key.string());
974                             key.destroy();
975                         }
976                         else {
977                             m_log.debug("session (%s) contained a non-matching NameID, leaving it alone", key.string());
978                         }
979                     }
980                     else {
981                         m_log.debug("session (%s) established by different IdP, leaving it alone", key.string());
982                     }
983                 }
984                 else {
985                     // Session's gone, so...
986                     sessionsKilled.push_back(key.string());
987                     key.destroy();
988                 }
989                 key = sessions.next();
990             }
991
992             // No sessions left for this index?
993             if (sessions.first().isnull())
994                 sessions.destroy();
995         }
996         sessions = obj.next();
997     }
998     
999     if (obj.first().isnull())
1000         obj.destroy();
1001
1002     // If possible, write back the mapping record (this isn't crucial).
1003     try {
1004         if (obj.isnull()) {
1005             m_storage->deleteText("NameID", name.get());
1006         }
1007         else if (!sessionsKilled.empty()) {
1008             ostringstream out;
1009             out << obj;
1010             if (m_storage->updateText("NameID", name.get(), out.str().c_str(), 0, ver) <= 0)
1011                 m_log.warn("logout mapping record changed behind us, leaving it alone");
1012         }
1013     }
1014     catch (exception& ex) {
1015         m_log.error("error updating logout mapping record: %s", ex.what());
1016     }
1017
1018     return sessionsKilled.size();
1019 }
1020
1021 bool SSCache::stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const
1022 {
1023     if (!XMLString::equals(n1.getName(), n2.getName()))
1024         return false;
1025     
1026     const XMLCh* s1 = n1.getFormat();
1027     const XMLCh* s2 = n2.getFormat();
1028     if (!s1 || !*s1)
1029         s1 = saml2::NameID::UNSPECIFIED;
1030     if (!s2 || !*s2)
1031         s2 = saml2::NameID::UNSPECIFIED;
1032     if (!XMLString::equals(s1,s2))
1033         return false;
1034     
1035     s1 = n1.getNameQualifier();
1036     s2 = n2.getNameQualifier();
1037     if (!s1 || !*s1)
1038         s1 = idp;
1039     if (!s2 || !*s2)
1040         s2 = idp;
1041     if (!XMLString::equals(s1,s2))
1042         return false;
1043
1044     s1 = n1.getSPNameQualifier();
1045     s2 = n2.getSPNameQualifier();
1046     if (!s1 || !*s1)
1047         s1 = sp;
1048     if (!s2 || !*s2)
1049         s2 = sp;
1050     if (!XMLString::equals(s1,s2))
1051         return false;
1052
1053     return true;
1054 }
1055
1056 void SSCache::receive(DDF& in, ostream& out)
1057 {
1058 #ifdef _DEBUG
1059     xmltooling::NDC ndc("receive");
1060 #endif
1061
1062     if (!strcmp(in.name(),"find::"REMOTED_SESSION_CACHE"::SessionCache")) {
1063         const char* key=in["key"].string();
1064         if (!key)
1065             throw ListenerException("Required parameters missing for session removal.");
1066
1067         const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());
1068         if (!app)
1069             throw ListenerException("Application not found, check configuration?");
1070
1071         // Do an unversioned read.
1072         string record;
1073         time_t lastAccess;
1074         if (!m_storage->readText(key, "session", &record, &lastAccess)) {
1075             DDF ret(NULL);
1076             DDFJanitor jan(ret);
1077             out << ret;
1078             return;
1079         }
1080
1081         // Adjust for expiration to recover last access time and check timeout.
1082         lastAccess -= m_cacheTimeout;
1083         time_t now=time(NULL);
1084
1085         // See if we need to check for a timeout.
1086         if (in["timeout"].string()) {
1087             time_t timeout = 0;
1088             auto_ptr_XMLCh dt(in["timeout"].string());
1089             DateTime dtobj(dt.get());
1090             dtobj.parseDateTime();
1091             timeout = dtobj.getEpoch();
1092                     
1093             if (timeout > 0 && now - lastAccess >= timeout) {
1094                 m_log.info("session timed out (ID: %s)", key);
1095                 remove(key,*app);
1096                 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1097             } 
1098
1099             // Update storage expiration, if possible.
1100             try {
1101                 m_storage->updateContext(key, now + m_cacheTimeout);
1102             }
1103             catch (exception& ex) {
1104                 m_log.error("failed to update session expiration: %s", ex.what());
1105             }
1106         }
1107             
1108         // Send the record back.
1109         out << record;
1110     }
1111     else if (!strcmp(in.name(),"touch::"REMOTED_SESSION_CACHE"::SessionCache")) {
1112         const char* key=in["key"].string();
1113         if (!key)
1114             throw ListenerException("Required parameters missing for session check.");
1115
1116         // Do a versioned read.
1117         string record;
1118         time_t lastAccess;
1119         int curver = in["version"].integer();
1120         int ver = m_storage->readText(key, "session", &record, &lastAccess, curver);
1121         if (ver == 0) {
1122             m_log.warn("unsuccessful versioned read of session (ID: %s), caches out of sync?", key);
1123             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1124         }
1125
1126         // Adjust for expiration to recover last access time and check timeout.
1127         lastAccess -= m_cacheTimeout;
1128         time_t now=time(NULL);
1129
1130         // See if we need to check for a timeout.
1131         time_t timeout = 0;
1132         auto_ptr_XMLCh dt(in["timeout"].string());
1133         if (dt.get()) {
1134             DateTime dtobj(dt.get());
1135             dtobj.parseDateTime();
1136             timeout = dtobj.getEpoch();
1137         }
1138                 
1139         if (timeout > 0 && now - lastAccess >= timeout) {
1140             m_log.info("session timed out (ID: %s)", key);
1141             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1142         } 
1143
1144         // Update storage expiration, if possible.
1145         try {
1146             m_storage->updateContext(key, now + m_cacheTimeout);
1147         }
1148         catch (exception& ex) {
1149             m_log.error("failed to update session expiration: %s", ex.what());
1150         }
1151             
1152         if (ver > curver) {
1153             // Send the record back.
1154             out << record;
1155         }
1156         else {
1157             DDF ret(NULL);
1158             DDFJanitor jan(ret);
1159             out << ret;
1160         }
1161     }
1162     else if (!strcmp(in.name(),"remove::"REMOTED_SESSION_CACHE"::SessionCache")) {
1163         const char* key=in["key"].string();
1164         if (!key)
1165             throw ListenerException("Required parameter missing for session removal.");
1166
1167         const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());
1168         if (!app)
1169             throw ListenerException("Application not found, check configuration?");
1170
1171         remove(key,*app);
1172         DDF ret(NULL);
1173         DDFJanitor jan(ret);
1174         out << ret;
1175     }
1176     else if (!strcmp(in.name(),"getAssertion::"REMOTED_SESSION_CACHE"::SessionCache")) {
1177         const char* key=in["key"].string();
1178         const char* id=in["id"].string();
1179         if (!key || !id)
1180             throw ListenerException("Required parameters missing for assertion retrieval.");
1181         string token;
1182         if (!m_storage->readText(key, id, &token, NULL))
1183             throw FatalProfileException("Assertion not found in cache.");
1184         out << token;
1185     }
1186 }