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