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