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