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