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