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