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