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