Make NameID optional in session.
[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* getClientAddress() const {\r
85             return m_obj["client_addr"].string();\r
86         }\r
87         const char* getEntityID() const {\r
88             return m_obj["entity_id"].string();\r
89         }\r
90         const char* getAuthnInstant() const {\r
91             return m_obj["authn_instant"].string();\r
92         }\r
93         const opensaml::saml2::NameID* getNameID() const {\r
94             return m_nameid;\r
95         }\r
96         const char* getSessionIndex() const {\r
97             return m_obj["session_index"].string();\r
98         }\r
99         const char* getAuthnContextClassRef() const {\r
100             return m_obj["authncontext_class"].string();\r
101         }\r
102         const char* getAuthnContextDeclRef() const {\r
103             return m_obj["authncontext_decl"].string();\r
104         }\r
105         const map<string,const Attribute*>& getAttributes() const {\r
106             if (m_attributes.empty())\r
107                 unmarshallAttributes();\r
108             return m_attributes;\r
109         }\r
110         const vector<const char*>& getAssertionIDs() const {\r
111             if (m_ids.empty()) {\r
112                 DDF ids = m_obj["assertions"];\r
113                 DDF id = ids.first();\r
114                 while (id.isstring()) {\r
115                     m_ids.push_back(id.string());\r
116                     id = ids.next();\r
117                 }\r
118             }\r
119             return m_ids;\r
120         }\r
121         \r
122         void addAttributes(const vector<Attribute*>& attributes);\r
123         const Assertion* getAssertion(const char* id) const;\r
124         void addAssertion(Assertion* assertion);\r
125 \r
126     private:\r
127         void unmarshallAttributes() const;\r
128 \r
129         DDF m_obj;\r
130         saml2::NameID* m_nameid;\r
131         mutable map<string,const Attribute*> m_attributes;\r
132         mutable vector<const char*> m_ids;\r
133         mutable map<string,Assertion*> m_tokens;\r
134         SSCache* m_cache;\r
135     };\r
136     \r
137     class SSCache : public SessionCache, public virtual Remoted\r
138     {\r
139     public:\r
140         SSCache(const DOMElement* e);\r
141         ~SSCache();\r
142     \r
143         void receive(DDF& in, ostream& out);\r
144         \r
145         string insert(\r
146             time_t expires,\r
147             const Application& application,\r
148             const char* client_addr=NULL,\r
149             const saml2md::EntityDescriptor* issuer=NULL,\r
150             const saml2::NameID* nameid=NULL,\r
151             const char* authn_instant=NULL,\r
152             const char* session_index=NULL,\r
153             const char* authncontext_class=NULL,\r
154             const char* authncontext_decl=NULL,\r
155             const vector<const Assertion*>* tokens=NULL,\r
156             const vector<Attribute*>* attributes=NULL\r
157             );\r
158         Session* find(const char* key, const Application& application, const char* client_addr=NULL, time_t timeout=0);\r
159         void remove(const char* key, const Application& application, const char* client_addr);\r
160 \r
161         Category& m_log;\r
162         StorageService* m_storage;\r
163     };\r
164 \r
165     SessionCache* SHIBSP_DLLLOCAL StorageServiceCacheFactory(const DOMElement* const & e)\r
166     {\r
167         return new SSCache(e);\r
168     }\r
169 \r
170     static const XMLCh _StorageService[] =   UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e);\r
171 }\r
172 \r
173 StoredSession::~StoredSession()\r
174 {\r
175     m_obj.destroy();\r
176     delete m_nameid;\r
177     for_each(m_attributes.begin(), m_attributes.end(), cleanup_const_pair<string,Attribute>());\r
178     for_each(m_tokens.begin(), m_tokens.end(), cleanup_pair<string,Assertion>());\r
179 }\r
180 \r
181 void StoredSession::unmarshallAttributes() const\r
182 {\r
183     Attribute* attribute;\r
184     DDF attrs = m_obj["attributes"];\r
185     DDF attr = attrs.first();\r
186     while (!attr.isnull()) {\r
187         try {\r
188             attribute = Attribute::unmarshall(attr);\r
189             m_attributes[attribute->getId()] = attribute;\r
190             if (m_cache->m_log.isDebugEnabled())\r
191                 m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",\r
192                     attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");\r
193         }\r
194         catch (AttributeException& ex) {\r
195             const char* id = attr.first().name();\r
196             m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());\r
197         }\r
198         attr = attrs.next();\r
199     }\r
200 }\r
201 \r
202 void StoredSession::addAttributes(const vector<Attribute*>& attributes)\r
203 {\r
204 #ifdef _DEBUG\r
205     xmltooling::NDC ndc("addAttributes");\r
206 #endif\r
207 \r
208     m_cache->m_log.debug("adding attributes to session (%s)", m_obj.name());\r
209     \r
210     int ver;\r
211     do {\r
212         DDF attr;\r
213         DDF attrs = m_obj["attributes"];\r
214         if (!attrs.islist())\r
215             attrs = m_obj.addmember("attributes").list();\r
216         for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a) {\r
217             attr = (*a)->marshall();\r
218             attrs.add(attr);\r
219         }\r
220         \r
221         // Tentatively increment the version.\r
222         m_obj["version"].integer(m_obj["version"].integer()+1);\r
223         \r
224         ostringstream str;\r
225         str << m_obj;\r
226         string record(str.str()); \r
227 \r
228         try {\r
229             ver = m_cache->m_storage->updateText(m_obj.name(), "session", record.c_str(), 0, m_obj["version"].integer()-1);\r
230         }\r
231         catch (exception&) {\r
232             // Roll back modification to record.\r
233             m_obj["version"].integer(m_obj["version"].integer()-1);\r
234             vector<Attribute*>::size_type count = attributes.size();\r
235             while (count--)\r
236                 attrs.last().destroy();            \r
237             throw;\r
238         }\r
239 \r
240         if (ver <= 0) {\r
241             // Roll back modification to record.\r
242             m_obj["version"].integer(m_obj["version"].integer()-1);\r
243             vector<Attribute*>::size_type count = attributes.size();\r
244             while (count--)\r
245                 attrs.last().destroy();            \r
246         }\r
247         if (!ver) {\r
248             // Fatal problem with update.\r
249             throw IOException("Unable to update stored session.");\r
250         }\r
251         else if (ver < 0) {\r
252             // Out of sync.\r
253             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");\r
254             ver = m_cache->m_storage->readText(m_obj.name(), "session", &record, NULL);\r
255             if (!ver) {\r
256                 m_cache->m_log.error("readText failed on StorageService for session (%s)", m_obj.name());\r
257                 throw IOException("Unable to read back stored session.");\r
258             }\r
259             \r
260             // Reset object.\r
261             DDF newobj;\r
262             istringstream in(record);\r
263             in >> newobj;\r
264 \r
265             m_ids.clear();\r
266             for_each(m_attributes.begin(), m_attributes.end(), cleanup_const_pair<string,Attribute>());\r
267             m_attributes.clear();\r
268             newobj["version"].integer(ver);\r
269             m_obj.destroy();\r
270             m_obj = newobj;\r
271 \r
272             ver = -1;\r
273         }\r
274     } while (ver < 0);  // negative indicates a sync issue so we retry\r
275 \r
276     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();\r
277     Locker locker(xlog);\r
278     xlog->log.infoStream() <<\r
279         "Added the following attributes to session (ID: " <<\r
280             m_obj.name() <<\r
281         ") for (applicationId: " <<\r
282             m_obj["application_id"].string() <<\r
283         ") {";\r
284     for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a)\r
285         xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";\r
286     xlog->log.info("}");\r
287 \r
288     // We own them now, so clean them up.\r
289     for_each(attributes.begin(), attributes.end(), xmltooling::cleanup<Attribute>());\r
290 }\r
291 \r
292 const Assertion* StoredSession::getAssertion(const char* id) const\r
293 {\r
294     map<string,Assertion*>::const_iterator i = m_tokens.find(id);\r
295     if (i!=m_tokens.end())\r
296         return i->second;\r
297     \r
298     string tokenstr;\r
299     if (!m_cache->m_storage->readText(m_obj.name(), id, &tokenstr, NULL))\r
300         throw FatalProfileException("Assertion not found in cache.");\r
301 \r
302     // Parse and bind the document into an XMLObject.\r
303     istringstream instr(tokenstr);\r
304     DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); \r
305     XercesJanitor<DOMDocument> janitor(doc);\r
306     auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));\r
307     janitor.release();\r
308     \r
309     Assertion* token = dynamic_cast<Assertion*>(xmlObject.get());\r
310     if (!token)\r
311         throw FatalProfileException("Request for cached assertion returned an unknown object type.");\r
312 \r
313     // Transfer ownership to us.\r
314     xmlObject.release();\r
315     m_tokens[id]=token;\r
316     return token;\r
317 }\r
318 \r
319 void StoredSession::addAssertion(Assertion* assertion)\r
320 {\r
321 #ifdef _DEBUG\r
322     xmltooling::NDC ndc("addAssertion");\r
323 #endif\r
324     \r
325     if (!assertion)\r
326         throw FatalProfileException("Unknown object type passed to session for storage.");\r
327 \r
328     auto_ptr_char id(assertion->getID());\r
329 \r
330     m_cache->m_log.debug("adding assertion (%s) to session (%s)", id.get(), m_obj.name());\r
331 \r
332     time_t exp;\r
333     if (!m_cache->m_storage->readText(m_obj.name(), "session", NULL, &exp))\r
334         throw IOException("Unable to load expiration time for stored session.");\r
335 \r
336     ostringstream tokenstr;\r
337     tokenstr << *assertion;\r
338     m_cache->m_storage->createText(m_obj.name(), id.get(), tokenstr.str().c_str(), exp);\r
339     \r
340     int ver;\r
341     do {\r
342         DDF token = DDF(NULL).string(id.get());\r
343         m_obj["assertions"].add(token);\r
344 \r
345         // Tentatively increment the version.\r
346         m_obj["version"].integer(m_obj["version"].integer()+1);\r
347     \r
348         ostringstream str;\r
349         str << m_obj;\r
350         string record(str.str()); \r
351 \r
352         try {\r
353             ver = m_cache->m_storage->updateText(m_obj.name(), "session", record.c_str(), 0, m_obj["version"].integer()-1);\r
354         }\r
355         catch (exception&) {\r
356             token.destroy();\r
357             m_obj["version"].integer(m_obj["version"].integer()-1);\r
358             m_cache->m_storage->deleteText(m_obj.name(), id.get());\r
359             throw;\r
360         }\r
361 \r
362         if (ver <= 0) {\r
363             token.destroy();\r
364             m_obj["version"].integer(m_obj["version"].integer()-1);\r
365         }            \r
366         if (!ver) {\r
367             // Fatal problem with update.\r
368             m_cache->m_log.error("updateText failed on StorageService for session (%s)", m_obj.name());\r
369             m_cache->m_storage->deleteText(m_obj.name(), id.get());\r
370             throw IOException("Unable to update stored session.");\r
371         }\r
372         else if (ver < 0) {\r
373             // Out of sync.\r
374             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");\r
375             ver = m_cache->m_storage->readText(m_obj.name(), "session", &record, NULL);\r
376             if (!ver) {\r
377                 m_cache->m_log.error("readText failed on StorageService for session (%s)", m_obj.name());\r
378                 m_cache->m_storage->deleteText(m_obj.name(), id.get());\r
379                 throw IOException("Unable to read back stored session.");\r
380             }\r
381             \r
382             // Reset object.\r
383             DDF newobj;\r
384             istringstream in(record);\r
385             in >> newobj;\r
386 \r
387             m_ids.clear();\r
388             for_each(m_attributes.begin(), m_attributes.end(), cleanup_const_pair<string,Attribute>());\r
389             m_attributes.clear();\r
390             newobj["version"].integer(ver);\r
391             m_obj.destroy();\r
392             m_obj = newobj;\r
393             \r
394             ver = -1;\r
395         }\r
396     } while (ver < 0); // negative indicates a sync issue so we retry\r
397 \r
398     m_ids.clear();\r
399     delete assertion;\r
400 \r
401     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();\r
402     Locker locker(xlog);\r
403     xlog->log.info(\r
404         "Added assertion (ID: %s) to session for (applicationId: %s) with (ID: %s)",\r
405         id.get(), m_obj["application_id"].string(), m_obj.name()\r
406         );\r
407 }\r
408 \r
409 SSCache::SSCache(const DOMElement* e)\r
410     : SessionCache(e), m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), m_storage(NULL)\r
411 {\r
412     SPConfig& conf = SPConfig::getConfig();\r
413     const XMLCh* tag = e ? e->getAttributeNS(NULL,_StorageService) : NULL;\r
414     if (tag && *tag) {\r
415         auto_ptr_char ssid(tag);\r
416         m_storage = conf.getServiceProvider()->getStorageService(ssid.get());\r
417         if (m_storage)\r
418             m_log.info("bound to StorageService (%s)", ssid.get());\r
419         else\r
420             throw ConfigurationException("SessionCache unable to locate StorageService, check configuration.");\r
421     }\r
422 \r
423     ListenerService* listener=conf.getServiceProvider()->getListenerService(false);\r
424     if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {\r
425         listener->regListener("insert::"REMOTED_SESSION_CACHE"::SessionCache",this);\r
426         listener->regListener("find::"REMOTED_SESSION_CACHE"::SessionCache",this);\r
427         listener->regListener("remove::"REMOTED_SESSION_CACHE"::SessionCache",this);\r
428         listener->regListener("touch::"REMOTED_SESSION_CACHE"::SessionCache",this);\r
429         listener->regListener("getAssertion::"REMOTED_SESSION_CACHE"::SessionCache",this);\r
430     }\r
431     else {\r
432         m_log.info("no ListenerService available, cache remoting disabled");\r
433     }\r
434 }\r
435 \r
436 SSCache::~SSCache()\r
437 {\r
438     SPConfig& conf = SPConfig::getConfig();\r
439     ListenerService* listener=conf.getServiceProvider()->getListenerService(false);\r
440     if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {\r
441         listener->unregListener("insert::"REMOTED_SESSION_CACHE"::SessionCache",this);\r
442         listener->unregListener("find::"REMOTED_SESSION_CACHE"::SessionCache",this);\r
443         listener->unregListener("remove::"REMOTED_SESSION_CACHE"::SessionCache",this);\r
444         listener->unregListener("touch::"REMOTED_SESSION_CACHE"::SessionCache",this);\r
445         listener->unregListener("getAssertion::"REMOTED_SESSION_CACHE"::SessionCache",this);\r
446     }\r
447 }\r
448 \r
449 string SSCache::insert(\r
450     time_t expires,\r
451     const Application& application,\r
452     const char* client_addr,\r
453     const saml2md::EntityDescriptor* issuer,\r
454     const saml2::NameID* nameid,\r
455     const char* authn_instant,\r
456     const char* session_index,\r
457     const char* authncontext_class,\r
458     const char* authncontext_decl,\r
459     const vector<const Assertion*>* tokens,\r
460     const vector<Attribute*>* attributes\r
461     )\r
462 {\r
463 #ifdef _DEBUG\r
464     xmltooling::NDC ndc("insert");\r
465 #endif\r
466 \r
467     m_log.debug("creating new session");\r
468 \r
469     auto_ptr_char key(SAMLConfig::getConfig().generateIdentifier());\r
470 \r
471     // Store session properties in DDF.\r
472     DDF obj = DDF(key.get()).structure();\r
473     obj.addmember("version").integer(1);\r
474     obj.addmember("application_id").string(application.getId());\r
475     if (expires > 0) {\r
476         // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  \r
477 #ifndef HAVE_GMTIME_R\r
478         struct tm* ptime=gmtime(&expires);\r
479 #else\r
480         struct tm res;\r
481         struct tm* ptime=gmtime_r(&expires,&res);\r
482 #endif\r
483         char timebuf[32];\r
484         strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);\r
485         obj.addmember("expires").string(timebuf);\r
486     }\r
487     if (client_addr)\r
488         obj.addmember("client_addr").string(client_addr);\r
489     if (issuer) {\r
490         auto_ptr_char entity_id(issuer->getEntityID());\r
491         obj.addmember("entity_id").string(entity_id.get());\r
492     }\r
493     if (authn_instant)\r
494         obj.addmember("authn_instant").string(authn_instant);\r
495     if (session_index)\r
496         obj.addmember("session_index").string(session_index);\r
497     if (authncontext_class)\r
498         obj.addmember("authncontext_class").string(authncontext_class);\r
499     if (authncontext_decl)\r
500         obj.addmember("authncontext_decl").string(authncontext_decl);\r
501 \r
502     if (nameid) {\r
503         ostringstream namestr;\r
504         namestr << nameid;\r
505         obj.addmember("nameid").string(namestr.str().c_str());\r
506     }\r
507 \r
508     if (tokens) {\r
509         obj.addmember("assertions").list();\r
510         for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {\r
511             auto_ptr_char tokenid((*t)->getID());\r
512             DDF tokid = DDF(NULL).string(tokenid.get());\r
513             obj["assertions"].add(tokid);\r
514         }\r
515     }\r
516     \r
517     if (attributes) {\r
518         DDF attr;\r
519         DDF attrlist = obj.addmember("attributes").list();\r
520         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) {\r
521             attr = (*a)->marshall();\r
522             attrlist.add(attr);\r
523         }\r
524     }\r
525     \r
526     ostringstream record;\r
527     record << obj;\r
528     \r
529     m_log.debug("storing new session...");\r
530     time_t now = time(NULL);\r
531     m_storage->createText(key.get(), "session", record.str().c_str(), now + m_cacheTimeout);\r
532     if (tokens) {\r
533         try {\r
534             for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {\r
535                 ostringstream tokenstr;\r
536                 tokenstr << *(*t);\r
537                 auto_ptr_char tokenid((*t)->getID());\r
538                 m_storage->createText(key.get(), tokenid.get(), tokenstr.str().c_str(), now + m_cacheTimeout);\r
539             }\r
540         }\r
541         catch (exception& ex) {\r
542             m_log.error("error storing assertion along with session: %s", ex.what());\r
543         }\r
544     }\r
545 \r
546     const char* pid = obj["entity_id"].string();\r
547     m_log.debug("new session created: SessionID (%s) IdP (%s) Address (%s)", key.get(), pid ? pid : "none", client_addr);\r
548 \r
549     // Transaction Logging\r
550     auto_ptr_char name(nameid ? nameid->getName() : NULL);\r
551     TransactionLog* xlog = application.getServiceProvider().getTransactionLog();\r
552     Locker locker(xlog);\r
553     xlog->log.infoStream() <<\r
554         "New session (ID: " <<\r
555             key.get() <<\r
556         ") with (applicationId: " <<\r
557             application.getId() <<\r
558         ") for principal from (IdP: " <<\r
559             (pid ? pid : "none") <<\r
560         ") at (ClientAddress: " <<\r
561             (client_addr ? client_addr : "none") <<\r
562         ") with (NameIdentifier: " <<\r
563             (name.get() ? name.get() : "none") <<\r
564         ")";\r
565     \r
566     if (attributes) {\r
567         xlog->log.infoStream() <<\r
568             "Cached the following attributes with session (ID: " <<\r
569                 key.get() <<\r
570             ") for (applicationId: " <<\r
571                 application.getId() <<\r
572             ") {";\r
573         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a)\r
574             xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";\r
575         xlog->log.info("}");\r
576         for_each(attributes->begin(), attributes->end(), xmltooling::cleanup<Attribute>());\r
577     }\r
578 \r
579     return key.get();\r
580 }\r
581 \r
582 Session* SSCache::find(const char* key, const Application& application, const char* client_addr, time_t timeout)\r
583 {\r
584 #ifdef _DEBUG\r
585     xmltooling::NDC ndc("find");\r
586 #endif\r
587 \r
588     m_log.debug("searching for session (%s)", key);\r
589     \r
590     time_t lastAccess;\r
591     string record;\r
592     int ver = m_storage->readText(key, "session", &record, &lastAccess);\r
593     if (!ver)\r
594         return NULL;\r
595     \r
596     m_log.debug("reconstituting session and checking validity");\r
597     \r
598     DDF obj;\r
599     istringstream in(record);\r
600     in >> obj;\r
601     \r
602     if (!XMLString::equals(obj["application_id"].string(), application.getId())) {\r
603         m_log.error("an application (%s) tried to access another application's session", application.getId());\r
604         obj.destroy();\r
605         return NULL;\r
606     }\r
607 \r
608     if (client_addr) {\r
609         if (m_log.isDebugEnabled())\r
610             m_log.debug("comparing client address %s against %s", client_addr, obj["client_addr"].string());\r
611         if (strcmp(obj["client_addr"].string(),client_addr)) {\r
612             m_log.warn("client address mismatch");\r
613             remove(key, application, client_addr);\r
614             RetryableProfileException ex(\r
615                 "Your IP address ($1) does not match the address recorded at the time the session was established.",\r
616                 params(1,client_addr)\r
617                 );\r
618             string eid(obj["entity_id"].string());\r
619             obj.destroy();\r
620             if (eid.empty())\r
621                 throw ex;\r
622             MetadataProvider* m=application.getMetadataProvider();\r
623             Locker locker(m);\r
624             annotateException(&ex,m->getEntityDescriptor(eid.c_str(),false)); // throws it\r
625         }\r
626     }\r
627 \r
628     lastAccess -= m_cacheTimeout;   // adjusts it back to the last time the record's timestamp was touched\r
629     time_t now=time(NULL);\r
630     \r
631     if (timeout > 0 && now - lastAccess >= timeout) {\r
632         m_log.info("session timed out (ID: %s)", key);\r
633         remove(key, application, client_addr);\r
634         RetryableProfileException ex("Your session has expired, and you must re-authenticate.");\r
635         string eid(obj["entity_id"].string());\r
636         obj.destroy();\r
637         if (eid.empty())\r
638             throw ex;\r
639         MetadataProvider* m=application.getMetadataProvider();\r
640         Locker locker(m);\r
641         annotateException(&ex,m->getEntityDescriptor(eid.c_str(),false)); // throws it\r
642     }\r
643     \r
644     auto_ptr_XMLCh exp(obj["expires"].string());\r
645     if (exp.get()) {\r
646         DateTime iso(exp.get());\r
647         iso.parseDateTime();\r
648         if (now > iso.getEpoch()) {\r
649             m_log.info("session expired (ID: %s)", key);\r
650             remove(key, application, client_addr);\r
651             RetryableProfileException ex("Your session has expired, and you must re-authenticate.");\r
652             string eid(obj["entity_id"].string());\r
653             obj.destroy();\r
654             if (eid.empty())\r
655                 throw ex;\r
656             MetadataProvider* m=application.getMetadataProvider();\r
657             Locker locker(m);\r
658             annotateException(&ex,m->getEntityDescriptor(eid.c_str(),false)); // throws it\r
659         }\r
660     }\r
661     \r
662     // Update storage expiration, if possible.\r
663     try {\r
664         m_storage->updateContext(key, now + m_cacheTimeout);\r
665     }\r
666     catch (exception& ex) {\r
667         m_log.error("failed to update session expiration: %s", ex.what());\r
668     }\r
669 \r
670     // Finally build the Session object.\r
671     try {\r
672         return new StoredSession(this, obj);\r
673     }\r
674     catch (exception&) {\r
675         obj.destroy();\r
676         throw;\r
677     }\r
678 }\r
679 \r
680 void SSCache::remove(const char* key, const Application& application, const char* client_addr)\r
681 {\r
682 #ifdef _DEBUG\r
683     xmltooling::NDC ndc("remove");\r
684 #endif\r
685 \r
686     m_log.debug("removing session (%s)", key);\r
687 \r
688     m_storage->deleteContext(key);\r
689 \r
690     TransactionLog* xlog = application.getServiceProvider().getTransactionLog();\r
691     Locker locker(xlog);\r
692     xlog->log.info("Destroyed session (applicationId: %s) (ID: %s)", application.getId(), key);\r
693 }\r
694 \r
695 void SSCache::receive(DDF& in, ostream& out)\r
696 {\r
697 #ifdef _DEBUG\r
698     xmltooling::NDC ndc("receive");\r
699 #endif\r
700 \r
701     if (!strcmp(in.name(),"insert::"REMOTED_SESSION_CACHE"::SessionCache")) {\r
702         auto_ptr_char key(SAMLConfig::getConfig().generateIdentifier());\r
703         in.name(key.get());\r
704 \r
705         DDF tokens = in["tokens"].remove();\r
706         DDFJanitor tjan(tokens);\r
707         \r
708         m_log.debug("storing new session...");\r
709         ostringstream record;\r
710         record << in;\r
711         time_t now = time(NULL);\r
712         m_storage->createText(key.get(), "session", record.str().c_str(), now + m_cacheTimeout);\r
713         if (tokens.islist()) {\r
714             try {\r
715                 DDF token = tokens.first();\r
716                 while (token.isstring()) {\r
717                     m_storage->createText(key.get(), token.name(), token.string(), now + m_cacheTimeout);\r
718                     token = tokens.next();\r
719                 }\r
720             }\r
721             catch (IOException& ex) {\r
722                 m_log.error("error storing assertion along with session: %s", ex.what());\r
723             }\r
724         }\r
725         const char* pid = in["entity_id"].string();\r
726         m_log.debug("new session created: SessionID (%s) IdP (%s) Address (%s)", key.get(), pid ? pid : "none", in["client_addr"].string());\r
727     \r
728         DDF ret = DDF(NULL).structure();\r
729         DDFJanitor jan(ret);\r
730         ret.addmember("key").string(key.get());\r
731         out << ret;\r
732     }\r
733     else if (!strcmp(in.name(),"find::"REMOTED_SESSION_CACHE"::SessionCache")) {\r
734         const char* key=in["key"].string();\r
735         if (!key)\r
736             throw ListenerException("Required parameters missing for session removal.");\r
737 \r
738         // Do an unversioned read.\r
739         string record;\r
740         time_t lastAccess;\r
741         if (!m_storage->readText(key, "session", &record, &lastAccess)) {\r
742             DDF ret(NULL);\r
743             DDFJanitor jan(ret);\r
744             out << ret;\r
745             return;\r
746         }\r
747 \r
748         // Adjust for expiration to recover last access time and check timeout.\r
749         lastAccess -= m_cacheTimeout;\r
750         time_t now=time(NULL);\r
751 \r
752         // See if we need to check for a timeout.\r
753         time_t timeout = 0;\r
754         auto_ptr_XMLCh dt(in["timeout"].string());\r
755         if (dt.get()) {\r
756             DateTime dtobj(dt.get());\r
757             dtobj.parseDateTime();\r
758             timeout = dtobj.getEpoch();\r
759         }\r
760                 \r
761         if (timeout > 0 && now - lastAccess >= timeout) {\r
762             m_log.info("session timed out (ID: %s)", key);\r
763             remove(key,*(SPConfig::getConfig().getServiceProvider()->getApplication("default")),NULL);\r
764             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
765         } \r
766 \r
767         // Update storage expiration, if possible.\r
768         try {\r
769             m_storage->updateContext(key, now + m_cacheTimeout);\r
770         }\r
771         catch (exception& ex) {\r
772             m_log.error("failed to update session expiration: %s", ex.what());\r
773         }\r
774             \r
775         // Send the record back.\r
776         out << record;\r
777     }\r
778     else if (!strcmp(in.name(),"touch::"REMOTED_SESSION_CACHE"::SessionCache")) {\r
779         const char* key=in["key"].string();\r
780         if (!key)\r
781             throw ListenerException("Required parameters missing for session check.");\r
782 \r
783         // Do a versioned read.\r
784         string record;\r
785         time_t lastAccess;\r
786         int curver = in["version"].integer();\r
787         int ver = m_storage->readText(key, "session", &record, &lastAccess, curver);\r
788         if (ver == 0) {\r
789             m_log.warn("unsuccessful versioned read of session (ID: %s), caches out of sync?", key);\r
790             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
791         }\r
792 \r
793         // Adjust for expiration to recover last access time and check timeout.\r
794         lastAccess -= m_cacheTimeout;\r
795         time_t now=time(NULL);\r
796 \r
797         // See if we need to check for a timeout.\r
798         time_t timeout = 0;\r
799         auto_ptr_XMLCh dt(in["timeout"].string());\r
800         if (dt.get()) {\r
801             DateTime dtobj(dt.get());\r
802             dtobj.parseDateTime();\r
803             timeout = dtobj.getEpoch();\r
804         }\r
805                 \r
806         if (timeout > 0 && now - lastAccess >= timeout) {\r
807             m_log.info("session timed out (ID: %s)", key);\r
808             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
809         } \r
810 \r
811         // Update storage expiration, if possible.\r
812         try {\r
813             m_storage->updateContext(key, now + m_cacheTimeout);\r
814         }\r
815         catch (exception& ex) {\r
816             m_log.error("failed to update session expiration: %s", ex.what());\r
817         }\r
818             \r
819         if (ver > curver) {\r
820             // Send the record back.\r
821             out << record;\r
822         }\r
823         else {\r
824             DDF ret(NULL);\r
825             DDFJanitor jan(ret);\r
826             out << ret;\r
827         }\r
828     }\r
829     else if (!strcmp(in.name(),"remove::"REMOTED_SESSION_CACHE"::SessionCache")) {\r
830         const char* key=in["key"].string();\r
831         if (!key)\r
832             throw ListenerException("Required parameter missing for session removal.");\r
833         \r
834         remove(key,*(SPConfig::getConfig().getServiceProvider()->getApplication("default")),NULL);\r
835         DDF ret(NULL);\r
836         DDFJanitor jan(ret);\r
837         out << ret;\r
838     }\r
839     else if (!strcmp(in.name(),"getAssertion::"REMOTED_SESSION_CACHE"::SessionCache")) {\r
840         const char* key=in["key"].string();\r
841         const char* id=in["id"].string();\r
842         if (!key || !id)\r
843             throw ListenerException("Required parameters missing for assertion retrieval.");\r
844         string token;\r
845         if (!m_storage->readText(key, id, &token, NULL))\r
846             throw FatalProfileException("Assertion not found in cache.");\r
847         out << token;\r
848     }\r
849 }\r