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