https://bugs.internet2.edu/jira/browse/SSPCPP-93
[shibboleth/sp.git] / shibsp / impl / StorageServiceSessionCache.cpp
1 /*\r
2  *  Copyright 2001-2007 Internet2\r
3  * \r
4  * Licensed under the Apache License, Version 2.0 (the "License");\r
5  * you may not use this file except in compliance with the License.\r
6  * You may obtain a copy of the License at\r
7  *\r
8  *     http://www.apache.org/licenses/LICENSE-2.0\r
9  *\r
10  * Unless required by applicable law or agreed to in writing, software\r
11  * distributed under the License is distributed on an "AS IS" BASIS,\r
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
13  * See the License for the specific language governing permissions and\r
14  * limitations under the License.\r
15  */\r
16 \r
17 /**\r
18  * StorageServiceSessionCache.cpp\r
19  * \r
20  * StorageService-based SessionCache implementation.\r
21  * \r
22  * Instead of optimizing this plugin with a buffering scheme that keeps objects around\r
23  * and avoids extra parsing steps, I'm assuming that systems that require such can\r
24  * layer their own cache plugin on top of this version either by delegating to it\r
25  * or using the remoting support. So this version will load sessions directly\r
26  * from the StorageService, instantiate enough to expose the Session API,\r
27  * and then delete everything when they're unlocked. All data in memory is always\r
28  * kept in sync with the StorageService (no lazy updates).\r
29  */\r
30 \r
31 #include "internal.h"\r
32 #include "Application.h"\r
33 #include "exceptions.h"\r
34 #include "ServiceProvider.h"\r
35 #include "SessionCacheEx.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 <xmltooling/util/NDC.h>\r
42 #include <xmltooling/util/XMLHelper.h>\r
43 #include <xercesc/util/XMLUniDefs.hpp>\r
44 \r
45 #ifndef SHIBSP_LITE\r
46 # include <saml/SAMLConfig.h>\r
47 # include <xmltooling/util/StorageService.h>\r
48 using namespace opensaml::saml2md;\r
49 #else\r
50 # include <ctime>\r
51 # include <xmltooling/util/DateTime.h>\r
52 #endif\r
53 \r
54 using namespace shibsp;\r
55 using namespace opensaml;\r
56 using namespace xmltooling;\r
57 using namespace std;\r
58 \r
59 namespace shibsp {\r
60 \r
61     class StoredSession;\r
62     class SSCache : public SessionCacheEx\r
63 #ifndef SHIBSP_LITE\r
64         ,public virtual Remoted\r
65 #endif\r
66     {\r
67     public:\r
68         SSCache(const DOMElement* e);\r
69         ~SSCache();\r
70 \r
71 #ifndef SHIBSP_LITE\r
72         void receive(DDF& in, ostream& out);\r
73 \r
74         void insert(\r
75             const Application& application,\r
76             const HTTPRequest& httpRequest,\r
77             HTTPResponse& httpResponse,\r
78             time_t expires,\r
79             const saml2md::EntityDescriptor* issuer=NULL,\r
80             const XMLCh* protocol=NULL,\r
81             const saml2::NameID* nameid=NULL,\r
82             const XMLCh* authn_instant=NULL,\r
83             const XMLCh* session_index=NULL,\r
84             const XMLCh* authncontext_class=NULL,\r
85             const XMLCh* authncontext_decl=NULL,\r
86             const vector<const Assertion*>* tokens=NULL,\r
87             const vector<Attribute*>* attributes=NULL\r
88             );\r
89         vector<string>::size_type logout(\r
90             const Application& application,\r
91             const saml2md::EntityDescriptor* issuer,\r
92             const saml2::NameID& nameid,\r
93             const set<string>* indexes,\r
94             time_t expires,\r
95             vector<string>& sessions\r
96             );\r
97         bool matches(\r
98             const Application& application,\r
99             const xmltooling::HTTPRequest& request,\r
100             const saml2md::EntityDescriptor* issuer,\r
101             const saml2::NameID& nameid,\r
102             const set<string>* indexes\r
103             );\r
104 #endif\r
105         Session* find(const Application& application, const char* key, const char* client_addr=NULL, time_t* timeout=NULL);\r
106         void remove(const Application& application, const char* key);\r
107         void test();\r
108 \r
109         string active(const Application& application, const xmltooling::HTTPRequest& request) {\r
110             pair<string,const char*> shib_cookie = application.getCookieNameProps("_shibsession_");\r
111             const char* session_id = request.getCookie(shib_cookie.first.c_str());\r
112             return (session_id ? session_id : "");\r
113         }\r
114 \r
115         Session* find(const Application& application, const HTTPRequest& request, const char* client_addr=NULL, time_t* timeout=NULL) {\r
116             string id = active(application, request);\r
117             if (!id.empty())\r
118                 return find(application, id.c_str(), client_addr, timeout);\r
119             return NULL;\r
120         }\r
121 \r
122         void remove(const Application& application, const HTTPRequest& request, HTTPResponse* response=NULL) {\r
123             pair<string,const char*> shib_cookie = application.getCookieNameProps("_shibsession_");\r
124             const char* session_id = request.getCookie(shib_cookie.first.c_str());\r
125             if (session_id && *session_id) {\r
126                 if (response)\r
127                     response->setCookie(shib_cookie.first.c_str(), shib_cookie.second);\r
128                 remove(application, session_id);\r
129             }\r
130         }\r
131 \r
132         void cleanup();\r
133 \r
134         Category& m_log;\r
135         bool inproc;\r
136         unsigned long m_cacheTimeout;\r
137 #ifndef SHIBSP_LITE\r
138         StorageService* m_storage;\r
139         StorageService* m_storage_lite;\r
140 #endif\r
141 \r
142     private:\r
143 #ifndef SHIBSP_LITE\r
144         // maintain back-mappings of NameID/SessionIndex -> session key\r
145         void insert(const char* key, time_t expires, const char* name, const char* index);\r
146         bool stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const;\r
147 #endif\r
148 \r
149         const DOMElement* m_root;         // Only valid during initialization\r
150         unsigned long m_inprocTimeout;\r
151 \r
152         // inproc means we buffer sessions in memory\r
153         RWLock* m_lock;\r
154         map<string,StoredSession*> m_hashtable;\r
155     \r
156         // management of buffered sessions\r
157         void dormant(const char* key);\r
158         static void* cleanup_fn(void*);\r
159         bool shutdown;\r
160         CondWait* shutdown_wait;\r
161         Thread* cleanup_thread;\r
162     };\r
163 \r
164     class StoredSession : public virtual Session\r
165     {\r
166     public:\r
167         StoredSession(SSCache* cache, DDF& obj) : m_obj(obj),\r
168 #ifndef SHIBSP_LITE\r
169                 m_nameid(NULL),\r
170 #endif\r
171                 m_cache(cache), m_expires(0), m_lastAccess(time(NULL)), m_lock(NULL) {\r
172             auto_ptr_XMLCh exp(m_obj["expires"].string());\r
173             if (exp.get()) {\r
174                 DateTime iso(exp.get());\r
175                 iso.parseDateTime();\r
176                 m_expires = iso.getEpoch();\r
177             }\r
178 \r
179 #ifndef SHIBSP_LITE\r
180             const char* nameid = obj["nameid"].string();\r
181             if (nameid) {\r
182                 // Parse and bind the document into an XMLObject.\r
183                 istringstream instr(nameid);\r
184                 DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); \r
185                 XercesJanitor<DOMDocument> janitor(doc);\r
186                 auto_ptr<saml2::NameID> n(saml2::NameIDBuilder::buildNameID());\r
187                 n->unmarshall(doc->getDocumentElement(), true);\r
188                 janitor.release();\r
189                 m_nameid = n.release();\r
190             }\r
191 #endif            \r
192             if (cache->inproc)\r
193                 m_lock = Mutex::create();\r
194         }\r
195         \r
196         ~StoredSession() {\r
197             delete m_lock;\r
198             m_obj.destroy();\r
199             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
200 #ifndef SHIBSP_LITE\r
201             delete m_nameid;\r
202             for_each(m_tokens.begin(), m_tokens.end(), cleanup_pair<string,Assertion>());\r
203 #endif\r
204         }\r
205         \r
206         Lockable* lock() {\r
207             if (m_lock)\r
208                 m_lock->lock();\r
209             return this;\r
210         }\r
211         void unlock() {\r
212             if (m_lock)\r
213                 m_lock->unlock();\r
214             else\r
215                 delete this;\r
216         }\r
217 \r
218         const char* getID() const {\r
219             return m_obj.name();\r
220         }\r
221         const char* getApplicationID() const {\r
222             return m_obj["application_id"].string();\r
223         }\r
224         const char* getClientAddress() const {\r
225             return m_obj["client_addr"].string();\r
226         }\r
227         const char* getEntityID() const {\r
228             return m_obj["entity_id"].string();\r
229         }\r
230         const char* getProtocol() const {\r
231             return m_obj["protocol"].string();\r
232         }\r
233         const char* getAuthnInstant() const {\r
234             return m_obj["authn_instant"].string();\r
235         }\r
236 #ifndef SHIBSP_LITE\r
237         const saml2::NameID* getNameID() const {\r
238             return m_nameid;\r
239         }\r
240 #endif\r
241         const char* getSessionIndex() const {\r
242             return m_obj["session_index"].string();\r
243         }\r
244         const char* getAuthnContextClassRef() const {\r
245             return m_obj["authncontext_class"].string();\r
246         }\r
247         const char* getAuthnContextDeclRef() const {\r
248             return m_obj["authncontext_decl"].string();\r
249         }\r
250         const vector<Attribute*>& getAttributes() const {\r
251             if (m_attributes.empty())\r
252                 unmarshallAttributes();\r
253             return m_attributes;\r
254         }\r
255         const multimap<string,const Attribute*>& getIndexedAttributes() const {\r
256             if (m_attributeIndex.empty()) {\r
257                 if (m_attributes.empty())\r
258                     unmarshallAttributes();\r
259                 for (vector<Attribute*>::const_iterator a = m_attributes.begin(); a != m_attributes.end(); ++a) {\r
260                     const vector<string>& aliases = (*a)->getAliases();\r
261                     for (vector<string>::const_iterator alias = aliases.begin(); alias != aliases.end(); ++alias)\r
262                         m_attributeIndex.insert(multimap<string,const Attribute*>::value_type(*alias, *a));\r
263                 }\r
264             }\r
265             return m_attributeIndex;\r
266         }\r
267         const vector<const char*>& getAssertionIDs() const {\r
268             if (m_ids.empty()) {\r
269                 DDF ids = m_obj["assertions"];\r
270                 DDF id = ids.first();\r
271                 while (id.isstring()) {\r
272                     m_ids.push_back(id.string());\r
273                     id = ids.next();\r
274                 }\r
275             }\r
276             return m_ids;\r
277         }\r
278         \r
279         void validate(const Application& application, const char* client_addr, time_t* timeout);\r
280 \r
281 #ifndef SHIBSP_LITE\r
282         void addAttributes(const vector<Attribute*>& attributes);\r
283         const Assertion* getAssertion(const char* id) const;\r
284         void addAssertion(Assertion* assertion);\r
285 #endif\r
286 \r
287         time_t getExpiration() const { return m_expires; }\r
288         time_t getLastAccess() const { return m_lastAccess; }\r
289 \r
290     private:\r
291         void unmarshallAttributes() const;\r
292 \r
293         DDF m_obj;\r
294 #ifndef SHIBSP_LITE\r
295         saml2::NameID* m_nameid;\r
296         mutable map<string,Assertion*> m_tokens;\r
297 #endif\r
298         mutable vector<Attribute*> m_attributes;\r
299         mutable multimap<string,const Attribute*> m_attributeIndex;\r
300         mutable vector<const char*> m_ids;\r
301 \r
302         SSCache* m_cache;\r
303         time_t m_expires,m_lastAccess;\r
304         Mutex* m_lock;\r
305     };\r
306     \r
307     SessionCache* SHIBSP_DLLLOCAL StorageServiceCacheFactory(const DOMElement* const & e)\r
308     {\r
309         return new SSCache(e);\r
310     }\r
311 }\r
312 \r
313 void SHIBSP_API shibsp::registerSessionCaches()\r
314 {\r
315     SPConfig::getConfig().SessionCacheManager.registerFactory(STORAGESERVICE_SESSION_CACHE, StorageServiceCacheFactory);\r
316 }\r
317 \r
318 void StoredSession::unmarshallAttributes() const\r
319 {\r
320     Attribute* attribute;\r
321     DDF attrs = m_obj["attributes"];\r
322     DDF attr = attrs.first();\r
323     while (!attr.isnull()) {\r
324         try {\r
325             attribute = Attribute::unmarshall(attr);\r
326             m_attributes.push_back(attribute);\r
327             if (m_cache->m_log.isDebugEnabled())\r
328                 m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",\r
329                     attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");\r
330         }\r
331         catch (AttributeException& ex) {\r
332             const char* id = attr.first().name();\r
333             m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());\r
334         }\r
335         attr = attrs.next();\r
336     }\r
337 }\r
338 \r
339 void StoredSession::validate(const Application& application, const char* client_addr, time_t* timeout)\r
340 {\r
341     time_t now = time(NULL);\r
342 \r
343     // Basic expiration?\r
344     if (m_expires > 0) {\r
345         if (now > m_expires) {\r
346             m_cache->m_log.info("session expired (ID: %s)", getID());\r
347             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
348         }\r
349     }\r
350 \r
351     // Address check?\r
352     if (client_addr) {\r
353         if (m_cache->m_log.isDebugEnabled())\r
354             m_cache->m_log.debug("comparing client address %s against %s", client_addr, getClientAddress());\r
355         if (!XMLString::equals(getClientAddress(),client_addr)) {\r
356             m_cache->m_log.warn("client address mismatch");\r
357             throw RetryableProfileException(\r
358                 "Your IP address ($1) does not match the address recorded at the time the session was established.",\r
359                 params(1,client_addr)\r
360                 );\r
361         }\r
362     }\r
363 \r
364     if (!timeout)\r
365         return;\r
366     \r
367     if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {\r
368         DDF in("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;\r
369         DDFJanitor jin(in);\r
370         in.structure();\r
371         in.addmember("key").string(getID());\r
372         in.addmember("version").integer(m_obj["version"].integer());\r
373         if (*timeout) {\r
374             // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  \r
375 #ifndef HAVE_GMTIME_R\r
376             struct tm* ptime=gmtime(timeout);\r
377 #else\r
378             struct tm res;\r
379             struct tm* ptime=gmtime_r(timeout,&res);\r
380 #endif\r
381             char timebuf[32];\r
382             strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);\r
383             in.addmember("timeout").string(timebuf);\r
384         }\r
385 \r
386         try {\r
387             out=application.getServiceProvider().getListenerService()->send(in);\r
388         }\r
389         catch (...) {\r
390             out.destroy();\r
391             throw;\r
392         }\r
393 \r
394         if (out.isstruct()) {\r
395             // We got an updated record back.\r
396             m_ids.clear();\r
397             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
398             m_attributes.clear();\r
399             m_attributeIndex.clear();\r
400             m_obj.destroy();\r
401             m_obj = out;\r
402         }\r
403     }\r
404     else {\r
405 #ifndef SHIBSP_LITE\r
406         if (!m_cache->m_storage)\r
407             throw ConfigurationException("Session touch requires a StorageService.");\r
408 \r
409         // Do a versioned read.\r
410         string record;\r
411         time_t lastAccess;\r
412         int curver = m_obj["version"].integer();\r
413         int ver = m_cache->m_storage->readText(getID(), "session", &record, &lastAccess, curver);\r
414         if (ver == 0) {\r
415             m_cache->m_log.warn("unsuccessful versioned read of session (ID: %s), cache out of sync?", getID());\r
416             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
417         }\r
418 \r
419         // Adjust for expiration to recover last access time and check timeout.\r
420         lastAccess -= m_cache->m_cacheTimeout;\r
421         if (*timeout > 0 && now - lastAccess >= *timeout) {\r
422             m_cache->m_log.info("session timed out (ID: %s)", getID());\r
423             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
424         } \r
425 \r
426         // Update storage expiration, if possible.\r
427         try {\r
428             m_cache->m_storage->updateContext(getID(), now + m_cache->m_cacheTimeout);\r
429         }\r
430         catch (exception& ex) {\r
431             m_cache->m_log.error("failed to update session expiration: %s", ex.what());\r
432         }\r
433             \r
434         if (ver > curver) {\r
435             // We got an updated record back.\r
436             DDF newobj;\r
437             istringstream in(record);\r
438             in >> newobj;\r
439             m_ids.clear();\r
440             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
441             m_attributes.clear();\r
442             m_attributeIndex.clear();\r
443             m_obj.destroy();\r
444             m_obj = newobj;\r
445         }\r
446 #else\r
447         throw ConfigurationException("Session touch requires a StorageService.");\r
448 #endif\r
449     }\r
450 \r
451     m_lastAccess = now;\r
452 }\r
453 \r
454 #ifndef SHIBSP_LITE\r
455 \r
456 void StoredSession::addAttributes(const vector<Attribute*>& attributes)\r
457 {\r
458 #ifdef _DEBUG\r
459     xmltooling::NDC ndc("addAttributes");\r
460 #endif\r
461 \r
462     if (!m_cache->m_storage)\r
463         throw ConfigurationException("Session modification requires a StorageService.");\r
464 \r
465     m_cache->m_log.debug("adding attributes to session (%s)", getID());\r
466     \r
467     int ver;\r
468     do {\r
469         DDF attr;\r
470         DDF attrs = m_obj["attributes"];\r
471         if (!attrs.islist())\r
472             attrs = m_obj.addmember("attributes").list();\r
473         for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a) {\r
474             attr = (*a)->marshall();\r
475             attrs.add(attr);\r
476         }\r
477         \r
478         // Tentatively increment the version.\r
479         m_obj["version"].integer(m_obj["version"].integer()+1);\r
480         \r
481         ostringstream str;\r
482         str << m_obj;\r
483         string record(str.str()); \r
484 \r
485         try {\r
486             ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);\r
487         }\r
488         catch (exception&) {\r
489             // Roll back modification to record.\r
490             m_obj["version"].integer(m_obj["version"].integer()-1);\r
491             vector<Attribute*>::size_type count = attributes.size();\r
492             while (count--)\r
493                 attrs.last().destroy();            \r
494             throw;\r
495         }\r
496 \r
497         if (ver <= 0) {\r
498             // Roll back modification to record.\r
499             m_obj["version"].integer(m_obj["version"].integer()-1);\r
500             vector<Attribute*>::size_type count = attributes.size();\r
501             while (count--)\r
502                 attrs.last().destroy();            \r
503         }\r
504         if (!ver) {\r
505             // Fatal problem with update.\r
506             throw IOException("Unable to update stored session.");\r
507         }\r
508         else if (ver < 0) {\r
509             // Out of sync.\r
510             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");\r
511             ver = m_cache->m_storage->readText(getID(), "session", &record, NULL);\r
512             if (!ver) {\r
513                 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());\r
514                 throw IOException("Unable to read back stored session.");\r
515             }\r
516             \r
517             // Reset object.\r
518             DDF newobj;\r
519             istringstream in(record);\r
520             in >> newobj;\r
521 \r
522             m_ids.clear();\r
523             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
524             m_attributes.clear();\r
525             m_attributeIndex.clear();\r
526             newobj["version"].integer(ver);\r
527             m_obj.destroy();\r
528             m_obj = newobj;\r
529 \r
530             ver = -1;\r
531         }\r
532     } while (ver < 0);  // negative indicates a sync issue so we retry\r
533 \r
534     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();\r
535     Locker locker(xlog);\r
536     xlog->log.infoStream() <<\r
537         "Added the following attributes to session (ID: " <<\r
538             getID() <<\r
539         ") for (applicationId: " <<\r
540             m_obj["application_id"].string() <<\r
541         ") {";\r
542     for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a)\r
543         xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";\r
544     xlog->log.info("}");\r
545 \r
546     // We own them now, so clean them up.\r
547     for_each(attributes.begin(), attributes.end(), xmltooling::cleanup<Attribute>());\r
548 }\r
549 \r
550 const Assertion* StoredSession::getAssertion(const char* id) const\r
551 {\r
552     if (!m_cache->m_storage)\r
553         throw ConfigurationException("Assertion retrieval requires a StorageService.");\r
554 \r
555     map<string,Assertion*>::const_iterator i = m_tokens.find(id);\r
556     if (i!=m_tokens.end())\r
557         return i->second;\r
558     \r
559     string tokenstr;\r
560     if (!m_cache->m_storage->readText(getID(), id, &tokenstr, NULL))\r
561         throw FatalProfileException("Assertion not found in cache.");\r
562 \r
563     // Parse and bind the document into an XMLObject.\r
564     istringstream instr(tokenstr);\r
565     DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); \r
566     XercesJanitor<DOMDocument> janitor(doc);\r
567     auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));\r
568     janitor.release();\r
569     \r
570     Assertion* token = dynamic_cast<Assertion*>(xmlObject.get());\r
571     if (!token)\r
572         throw FatalProfileException("Request for cached assertion returned an unknown object type.");\r
573 \r
574     // Transfer ownership to us.\r
575     xmlObject.release();\r
576     m_tokens[id]=token;\r
577     return token;\r
578 }\r
579 \r
580 void StoredSession::addAssertion(Assertion* assertion)\r
581 {\r
582 #ifdef _DEBUG\r
583     xmltooling::NDC ndc("addAssertion");\r
584 #endif\r
585 \r
586     if (!m_cache->m_storage)\r
587         throw ConfigurationException("Session modification requires a StorageService.");\r
588 \r
589     if (!assertion)\r
590         throw FatalProfileException("Unknown object type passed to session for storage.");\r
591 \r
592     auto_ptr_char id(assertion->getID());\r
593 \r
594     m_cache->m_log.debug("adding assertion (%s) to session (%s)", id.get(), getID());\r
595 \r
596     time_t exp;\r
597     if (!m_cache->m_storage->readText(getID(), "session", NULL, &exp))\r
598         throw IOException("Unable to load expiration time for stored session.");\r
599 \r
600     ostringstream tokenstr;\r
601     tokenstr << *assertion;\r
602     if (!m_cache->m_storage->createText(getID(), id.get(), tokenstr.str().c_str(), exp))\r
603         throw IOException("Attempted to insert duplicate assertion ID into session.");\r
604     \r
605     int ver;\r
606     do {\r
607         DDF token = DDF(NULL).string(id.get());\r
608         m_obj["assertions"].add(token);\r
609 \r
610         // Tentatively increment the version.\r
611         m_obj["version"].integer(m_obj["version"].integer()+1);\r
612     \r
613         ostringstream str;\r
614         str << m_obj;\r
615         string record(str.str()); \r
616 \r
617         try {\r
618             ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);\r
619         }\r
620         catch (exception&) {\r
621             token.destroy();\r
622             m_obj["version"].integer(m_obj["version"].integer()-1);\r
623             m_cache->m_storage->deleteText(getID(), id.get());\r
624             throw;\r
625         }\r
626 \r
627         if (ver <= 0) {\r
628             token.destroy();\r
629             m_obj["version"].integer(m_obj["version"].integer()-1);\r
630         }            \r
631         if (!ver) {\r
632             // Fatal problem with update.\r
633             m_cache->m_log.error("updateText failed on StorageService for session (%s)", getID());\r
634             m_cache->m_storage->deleteText(getID(), id.get());\r
635             throw IOException("Unable to update stored session.");\r
636         }\r
637         else if (ver < 0) {\r
638             // Out of sync.\r
639             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");\r
640             ver = m_cache->m_storage->readText(getID(), "session", &record, NULL);\r
641             if (!ver) {\r
642                 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());\r
643                 m_cache->m_storage->deleteText(getID(), id.get());\r
644                 throw IOException("Unable to read back stored session.");\r
645             }\r
646             \r
647             // Reset object.\r
648             DDF newobj;\r
649             istringstream in(record);\r
650             in >> newobj;\r
651 \r
652             m_ids.clear();\r
653             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
654             m_attributes.clear();\r
655             m_attributeIndex.clear();\r
656             newobj["version"].integer(ver);\r
657             m_obj.destroy();\r
658             m_obj = newobj;\r
659             \r
660             ver = -1;\r
661         }\r
662     } while (ver < 0); // negative indicates a sync issue so we retry\r
663 \r
664     m_ids.clear();\r
665     delete assertion;\r
666 \r
667     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();\r
668     Locker locker(xlog);\r
669     xlog->log.info(\r
670         "Added assertion (ID: %s) to session for (applicationId: %s) with (ID: %s)",\r
671         id.get(), m_obj["application_id"].string(), getID()\r
672         );\r
673 }\r
674 \r
675 #endif\r
676 \r
677 SSCache::SSCache(const DOMElement* e)\r
678     : m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), inproc(true), m_cacheTimeout(3600),\r
679 #ifndef SHIBSP_LITE\r
680       m_storage(NULL), m_storage_lite(NULL),\r
681 #endif\r
682         m_root(e), m_inprocTimeout(900), m_lock(NULL), shutdown(false), shutdown_wait(NULL), cleanup_thread(NULL)\r
683 {\r
684     static const XMLCh cacheTimeout[] =     UNICODE_LITERAL_12(c,a,c,h,e,T,i,m,e,o,u,t);\r
685     static const XMLCh inprocTimeout[] =    UNICODE_LITERAL_13(i,n,p,r,o,c,T,i,m,e,o,u,t);\r
686     static const XMLCh _StorageService[] =  UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e);\r
687     static const XMLCh _StorageServiceLite[] = UNICODE_LITERAL_18(S,t,o,r,a,g,e,S,e,r,v,i,c,e,L,i,t,e);\r
688 \r
689     SPConfig& conf = SPConfig::getConfig();\r
690     inproc = conf.isEnabled(SPConfig::InProcess);\r
691 \r
692     if (e) {\r
693         const XMLCh* tag=e->getAttributeNS(NULL,cacheTimeout);\r
694         if (tag && *tag) {\r
695             m_cacheTimeout = XMLString::parseInt(tag);\r
696             if (!m_cacheTimeout)\r
697                 m_cacheTimeout=3600;\r
698         }\r
699         if (inproc) {\r
700             const XMLCh* tag=e->getAttributeNS(NULL,inprocTimeout);\r
701             if (tag && *tag) {\r
702                 m_inprocTimeout = XMLString::parseInt(tag);\r
703                 if (!m_inprocTimeout)\r
704                     m_inprocTimeout=900;\r
705             }\r
706         }\r
707     }\r
708 \r
709 #ifndef SHIBSP_LITE\r
710     if (conf.isEnabled(SPConfig::OutOfProcess)) {\r
711         const XMLCh* tag = e ? e->getAttributeNS(NULL,_StorageService) : NULL;\r
712         if (tag && *tag) {\r
713             auto_ptr_char ssid(tag);\r
714             m_storage = conf.getServiceProvider()->getStorageService(ssid.get());\r
715             if (m_storage)\r
716                 m_log.info("bound to StorageService (%s)", ssid.get());\r
717         }\r
718         if (!m_storage)\r
719             throw ConfigurationException("SessionCache unable to locate StorageService, check configuration.");\r
720 \r
721         tag = e ? e->getAttributeNS(NULL,_StorageServiceLite) : NULL;\r
722         if (tag && *tag) {\r
723             auto_ptr_char ssid(tag);\r
724             m_storage_lite = conf.getServiceProvider()->getStorageService(ssid.get());\r
725             if (m_storage_lite)\r
726                 m_log.info("bound to StorageServiceLite (%s)", ssid.get());\r
727         }\r
728         if (!m_storage_lite) {\r
729             m_log.info("No StorageServiceLite specified. Using standard StorageService.");\r
730             m_storage_lite = m_storage;\r
731         }\r
732     }\r
733 #endif\r
734 \r
735     ListenerService* listener=conf.getServiceProvider()->getListenerService(false);\r
736     if (inproc ) {\r
737         if (!conf.isEnabled(SPConfig::OutOfProcess) && !listener)\r
738             throw ConfigurationException("SessionCache requires a ListenerService, but none available.");\r
739         m_lock = RWLock::create();\r
740         shutdown_wait = CondWait::create();\r
741         cleanup_thread = Thread::create(&cleanup_fn, (void*)this);\r
742     }\r
743 #ifndef SHIBSP_LITE\r
744     else {\r
745         if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {\r
746             listener->regListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);\r
747             listener->regListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);\r
748             listener->regListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);\r
749         }\r
750         else {\r
751             m_log.info("no ListenerService available, cache remoting disabled");\r
752         }\r
753     }\r
754 #endif\r
755 }\r
756 \r
757 SSCache::~SSCache()\r
758 {\r
759     if (inproc) {\r
760         // Shut down the cleanup thread and let it know...\r
761         shutdown = true;\r
762         shutdown_wait->signal();\r
763         cleanup_thread->join(NULL);\r
764 \r
765         for_each(m_hashtable.begin(),m_hashtable.end(),cleanup_pair<string,StoredSession>());\r
766         delete m_lock;\r
767         delete shutdown_wait;\r
768     }\r
769 #ifndef SHIBSP_LITE\r
770     else {\r
771         SPConfig& conf = SPConfig::getConfig();\r
772         ListenerService* listener=conf.getServiceProvider()->getListenerService(false);\r
773         if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {\r
774             listener->unregListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);\r
775             listener->unregListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);\r
776             listener->unregListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);\r
777         }\r
778     }\r
779 #endif\r
780 }\r
781 \r
782 #ifndef SHIBSP_LITE\r
783 \r
784 void SSCache::test()\r
785 {\r
786     auto_ptr_char temp(SAMLConfig::getConfig().generateIdentifier());\r
787     m_storage->createString("SessionCacheTest", temp.get(), "Test", time(NULL) + 60);\r
788     m_storage->deleteString("SessionCacheTest", temp.get());\r
789 }\r
790 \r
791 void SSCache::insert(const char* key, time_t expires, const char* name, const char* index)\r
792 {\r
793     string dup;\r
794     if (strlen(name) > 255) {\r
795         dup = string(name).substr(0,255);\r
796         name = dup.c_str();\r
797     }\r
798 \r
799     DDF obj;\r
800     DDFJanitor jobj(obj);\r
801 \r
802     // Since we can't guarantee uniqueness, check for an existing record.\r
803     string record;\r
804     time_t recordexp;\r
805     int ver = m_storage_lite->readText("NameID", name, &record, &recordexp);\r
806     if (ver > 0) {\r
807         // Existing record, so we need to unmarshall it.\r
808         istringstream in(record);\r
809         in >> obj;\r
810     }\r
811     else {\r
812         // New record.\r
813         obj.structure();\r
814     }\r
815 \r
816     if (!index || !*index)\r
817         index = "_shibnull";\r
818     DDF sessions = obj.addmember(index);\r
819     if (!sessions.islist())\r
820         sessions.list();\r
821     DDF session = DDF(NULL).string(key);\r
822     sessions.add(session);\r
823 \r
824     // Remarshall the record.\r
825     ostringstream out;\r
826     out << obj;\r
827 \r
828     // Try and store it back...\r
829     if (ver > 0) {\r
830         ver = m_storage_lite->updateText("NameID", name, out.str().c_str(), max(expires, recordexp), ver);\r
831         if (ver <= 0) {\r
832             // Out of sync, or went missing, so retry.\r
833             return insert(key, expires, name, index);\r
834         }\r
835     }\r
836     else if (!m_storage_lite->createText("NameID", name, out.str().c_str(), expires)) {\r
837         // Hit a dup, so just retry, hopefully hitting the other branch.\r
838         return insert(key, expires, name, index);\r
839     }\r
840 }\r
841 \r
842 void SSCache::insert(\r
843     const Application& application,\r
844     const HTTPRequest& httpRequest,\r
845     HTTPResponse& httpResponse,\r
846     time_t expires,\r
847     const saml2md::EntityDescriptor* issuer,\r
848     const XMLCh* protocol,\r
849     const saml2::NameID* nameid,\r
850     const XMLCh* authn_instant,\r
851     const XMLCh* session_index,\r
852     const XMLCh* authncontext_class,\r
853     const XMLCh* authncontext_decl,\r
854     const vector<const Assertion*>* tokens,\r
855     const vector<Attribute*>* attributes\r
856     )\r
857 {\r
858 #ifdef _DEBUG\r
859     xmltooling::NDC ndc("insert");\r
860 #endif\r
861     if (!m_storage)\r
862         throw ConfigurationException("SessionCache insertion requires a StorageService.");\r
863 \r
864     m_log.debug("creating new session");\r
865 \r
866     time_t now = time(NULL);\r
867     auto_ptr_char index(session_index);\r
868     auto_ptr_char entity_id(issuer ? issuer->getEntityID() : NULL);\r
869     auto_ptr_char name(nameid ? nameid->getName() : NULL);\r
870 \r
871     if (nameid) {\r
872         // Check for a pending logout.\r
873         if (strlen(name.get()) > 255)\r
874             const_cast<char*>(name.get())[255] = 0;\r
875         string pending;\r
876         int ver = m_storage_lite->readText("Logout", name.get(), &pending);\r
877         if (ver > 0) {\r
878             DDF pendobj;\r
879             DDFJanitor jpend(pendobj);\r
880             istringstream pstr(pending);\r
881             pstr >> pendobj;\r
882             // IdP.SP.index contains logout expiration, if any.\r
883             DDF deadmenwalking = pendobj[issuer ? entity_id.get() : "_shibnull"][application.getRelyingParty(issuer)->getString("entityID").second];\r
884             const char* logexpstr = deadmenwalking[session_index ? index.get() : "_shibnull"].string();\r
885             if (!logexpstr && session_index)    // we tried an exact session match, now try for NULL\r
886                 logexpstr = deadmenwalking["_shibnull"].string();\r
887             if (logexpstr) {\r
888                 auto_ptr_XMLCh dt(logexpstr);\r
889                 DateTime dtobj(dt.get());\r
890                 dtobj.parseDateTime();\r
891                 time_t logexp = dtobj.getEpoch();\r
892                 if (now - XMLToolingConfig::getConfig().clock_skew_secs < logexp)\r
893                     throw FatalProfileException("A logout message from your identity provider has blocked your login attempt.");\r
894             }\r
895         }\r
896     }\r
897 \r
898     auto_ptr_char key(SAMLConfig::getConfig().generateIdentifier());\r
899 \r
900     // Store session properties in DDF.\r
901     DDF obj = DDF(key.get()).structure();\r
902     obj.addmember("version").integer(1);\r
903     obj.addmember("application_id").string(application.getId());\r
904 \r
905     // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.\r
906 #ifndef HAVE_GMTIME_R\r
907     struct tm* ptime=gmtime(&expires);\r
908 #else\r
909     struct tm res;\r
910     struct tm* ptime=gmtime_r(&expires,&res);\r
911 #endif\r
912     char timebuf[32];\r
913     strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);\r
914     obj.addmember("expires").string(timebuf);\r
915 \r
916     obj.addmember("client_addr").string(httpRequest.getRemoteAddr().c_str());\r
917     if (issuer)\r
918         obj.addmember("entity_id").string(entity_id.get());\r
919     if (protocol) {\r
920         auto_ptr_char prot(protocol);\r
921         obj.addmember("protocol").string(prot.get());\r
922     }\r
923     if (authn_instant) {\r
924         auto_ptr_char instant(authn_instant);\r
925         obj.addmember("authn_instant").string(instant.get());\r
926     }\r
927     if (session_index)\r
928         obj.addmember("session_index").string(index.get());\r
929     if (authncontext_class) {\r
930         auto_ptr_char ac(authncontext_class);\r
931         obj.addmember("authncontext_class").string(ac.get());\r
932     }\r
933     if (authncontext_decl) {\r
934         auto_ptr_char ad(authncontext_decl);\r
935         obj.addmember("authncontext_decl").string(ad.get());\r
936     }\r
937 \r
938     if (nameid) {\r
939         ostringstream namestr;\r
940         namestr << *nameid;\r
941         obj.addmember("nameid").string(namestr.str().c_str());\r
942     }\r
943 \r
944     if (tokens) {\r
945         obj.addmember("assertions").list();\r
946         for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {\r
947             auto_ptr_char tokenid((*t)->getID());\r
948             DDF tokid = DDF(NULL).string(tokenid.get());\r
949             obj["assertions"].add(tokid);\r
950         }\r
951     }\r
952     \r
953     if (attributes) {\r
954         DDF attr;\r
955         DDF attrlist = obj.addmember("attributes").list();\r
956         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) {\r
957             attr = (*a)->marshall();\r
958             attrlist.add(attr);\r
959         }\r
960     }\r
961     \r
962     ostringstream record;\r
963     record << obj;\r
964     \r
965     m_log.debug("storing new session...");\r
966     if (!m_storage->createText(key.get(), "session", record.str().c_str(), now + m_cacheTimeout))\r
967         throw FatalProfileException("Attempted to create a session with a duplicate key.");\r
968     \r
969     // Store the reverse mapping for logout.\r
970     try {\r
971         if (nameid)\r
972             insert(key.get(), expires, name.get(), index.get());\r
973     }\r
974     catch (exception& ex) {\r
975         m_log.error("error storing back mapping of NameID for logout: %s", ex.what());\r
976     }\r
977 \r
978     if (tokens) {\r
979         try {\r
980             for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {\r
981                 ostringstream tokenstr;\r
982                 tokenstr << *(*t);\r
983                 auto_ptr_char tokenid((*t)->getID());\r
984                 if (!m_storage->createText(key.get(), tokenid.get(), tokenstr.str().c_str(), now + m_cacheTimeout))\r
985                     throw IOException("duplicate assertion ID ($1)", params(1, tokenid.get()));\r
986             }\r
987         }\r
988         catch (exception& ex) {\r
989             m_log.error("error storing assertion along with session: %s", ex.what());\r
990         }\r
991     }\r
992 \r
993     const char* pid = obj["entity_id"].string();\r
994     const char* prot = obj["protocol"].string();\r
995     m_log.info("new session created: ID (%s) IdP (%s) Protocol(%s) Address (%s)",\r
996         key.get(), pid ? pid : "none", prot ? prot : "none", httpRequest.getRemoteAddr().c_str());\r
997 \r
998     // Transaction Logging\r
999     TransactionLog* xlog = application.getServiceProvider().getTransactionLog();\r
1000     Locker locker(xlog);\r
1001     xlog->log.infoStream() <<\r
1002         "New session (ID: " <<\r
1003             key.get() <<\r
1004         ") with (applicationId: " <<\r
1005             application.getId() <<\r
1006         ") for principal from (IdP: " <<\r
1007             (pid ? pid : "none") <<\r
1008         ") at (ClientAddress: " <<\r
1009             httpRequest.getRemoteAddr() <<\r
1010         ") with (NameIdentifier: " <<\r
1011             (nameid ? name.get() : "none") <<\r
1012         ") using (Protocol: " <<\r
1013             (prot ? prot : "none") <<\r
1014         ")";\r
1015     \r
1016     if (attributes) {\r
1017         xlog->log.infoStream() <<\r
1018             "Cached the following attributes with session (ID: " <<\r
1019                 key.get() <<\r
1020             ") for (applicationId: " <<\r
1021                 application.getId() <<\r
1022             ") {";\r
1023         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a)\r
1024             xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";\r
1025         xlog->log.info("}");\r
1026     }\r
1027 \r
1028     pair<string,const char*> shib_cookie = application.getCookieNameProps("_shibsession_");\r
1029     string k(key.get());\r
1030     k += shib_cookie.second;\r
1031     httpResponse.setCookie(shib_cookie.first.c_str(), k.c_str());\r
1032 }\r
1033 \r
1034 bool SSCache::matches(\r
1035     const Application& application,\r
1036     const xmltooling::HTTPRequest& request,\r
1037     const saml2md::EntityDescriptor* issuer,\r
1038     const saml2::NameID& nameid,\r
1039     const set<string>* indexes\r
1040     )\r
1041 {\r
1042     auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL);\r
1043     try {\r
1044         Session* session = find(application, request);\r
1045         if (session) {\r
1046             Locker locker(session, false);\r
1047             if (XMLString::equals(session->getEntityID(), entityID.get()) && session->getNameID() &&\r
1048                     stronglyMatches(issuer->getEntityID(), application.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) {\r
1049                 return (!indexes || indexes->empty() || (session->getSessionIndex() ? (indexes->count(session->getSessionIndex())>0) : false));\r
1050             }\r
1051         }\r
1052     }\r
1053     catch (exception& ex) {\r
1054         m_log.error("error while matching session: %s", ex.what());\r
1055     }\r
1056     return false;\r
1057 }\r
1058 \r
1059 vector<string>::size_type SSCache::logout(\r
1060     const Application& application,\r
1061     const saml2md::EntityDescriptor* issuer,\r
1062     const saml2::NameID& nameid,\r
1063     const set<string>* indexes,\r
1064     time_t expires,\r
1065     vector<string>& sessionsKilled\r
1066     )\r
1067 {\r
1068 #ifdef _DEBUG\r
1069     xmltooling::NDC ndc("logout");\r
1070 #endif\r
1071 \r
1072     if (!m_storage)\r
1073         throw ConfigurationException("SessionCache insertion requires a StorageService.");\r
1074 \r
1075     auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL);\r
1076     auto_ptr_char name(nameid.getName());\r
1077 \r
1078     m_log.info("request to logout sessions from (%s) for (%s)", entityID.get() ? entityID.get() : "unknown", name.get());\r
1079 \r
1080     if (strlen(name.get()) > 255)\r
1081         const_cast<char*>(name.get())[255] = 0;\r
1082 \r
1083     DDF obj;\r
1084     DDFJanitor jobj(obj);\r
1085     string record;\r
1086     int ver;\r
1087 \r
1088     if (expires) {\r
1089         // Record the logout to prevent post-delivered assertions.\r
1090         // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.\r
1091 #ifndef HAVE_GMTIME_R\r
1092         struct tm* ptime=gmtime(&expires);\r
1093 #else\r
1094         struct tm res;\r
1095         struct tm* ptime=gmtime_r(&expires,&res);\r
1096 #endif\r
1097         char timebuf[32];\r
1098         strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);\r
1099 \r
1100         time_t oldexp = 0;\r
1101         ver = m_storage_lite->readText("Logout", name.get(), &record, &oldexp);\r
1102         if (ver > 0) {\r
1103             istringstream lin(record);\r
1104             lin >> obj;\r
1105         }\r
1106         else {\r
1107             obj = DDF(NULL).structure();\r
1108         }\r
1109 \r
1110         // Structure is keyed by the IdP and SP, with a member per session index containing the expiration.\r
1111         DDF root = obj.addmember(issuer ? entityID.get() : "_shibnull").addmember(application.getRelyingParty(issuer)->getString("entityID").second);\r
1112         if (indexes) {\r
1113             for (set<string>::const_iterator x = indexes->begin(); x!=indexes->end(); ++x)\r
1114                 root.addmember(x->c_str()).string(timebuf);\r
1115         }\r
1116         else {\r
1117             root.addmember("_shibnull").string(timebuf);\r
1118         }\r
1119 \r
1120         // Write it back.\r
1121         ostringstream lout;\r
1122         lout << obj;\r
1123 \r
1124         if (ver > 0) {\r
1125             ver = m_storage_lite->updateText("Logout", name.get(), lout.str().c_str(), max(expires, oldexp), ver);\r
1126             if (ver <= 0) {\r
1127                 // Out of sync, or went missing, so retry.\r
1128                 return logout(application, issuer, nameid, indexes, expires, sessionsKilled);\r
1129             }\r
1130         }\r
1131         else if (!m_storage_lite->createText("Logout", name.get(), lout.str().c_str(), expires)) {\r
1132             // Hit a dup, so just retry, hopefully hitting the other branch.\r
1133             return logout(application, issuer, nameid, indexes, expires, sessionsKilled);\r
1134         }\r
1135 \r
1136         obj.destroy();\r
1137         record.erase();\r
1138     }\r
1139 \r
1140     // Read in potentially matching sessions.\r
1141     ver = m_storage_lite->readText("NameID", name.get(), &record);\r
1142     if (ver == 0) {\r
1143         m_log.debug("no active sessions to logout for supplied issuer and subject");\r
1144         return 0;\r
1145     }\r
1146 \r
1147     istringstream in(record);\r
1148     in >> obj;\r
1149 \r
1150     // The record contains child lists for each known session index.\r
1151     DDF key;\r
1152     DDF sessions = obj.first();\r
1153     while (sessions.islist()) {\r
1154         if (!indexes || indexes->empty() || indexes->count(sessions.name())) {\r
1155             key = sessions.first();\r
1156             while (key.isstring()) {\r
1157                 // Fetch the session for comparison.\r
1158                 Session* session = NULL;\r
1159                 try {\r
1160                     session = find(application, key.string());\r
1161                 }\r
1162                 catch (exception& ex) {\r
1163                     m_log.error("error locating session (%s): %s", key.string(), ex.what());\r
1164                 }\r
1165 \r
1166                 if (session) {\r
1167                     Locker locker(session, false);\r
1168                     // Same issuer?\r
1169                     if (XMLString::equals(session->getEntityID(), entityID.get())) {\r
1170                         // Same NameID?\r
1171                         if (stronglyMatches(issuer->getEntityID(), application.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) {\r
1172                             sessionsKilled.push_back(key.string());\r
1173                             key.destroy();\r
1174                         }\r
1175                         else {\r
1176                             m_log.debug("session (%s) contained a non-matching NameID, leaving it alone", key.string());\r
1177                         }\r
1178                     }\r
1179                     else {\r
1180                         m_log.debug("session (%s) established by different IdP, leaving it alone", key.string());\r
1181                     }\r
1182                 }\r
1183                 else {\r
1184                     // Session's gone, so...\r
1185                     sessionsKilled.push_back(key.string());\r
1186                     key.destroy();\r
1187                 }\r
1188                 key = sessions.next();\r
1189             }\r
1190 \r
1191             // No sessions left for this index?\r
1192             if (sessions.first().isnull())\r
1193                 sessions.destroy();\r
1194         }\r
1195         sessions = obj.next();\r
1196     }\r
1197     \r
1198     if (obj.first().isnull())\r
1199         obj.destroy();\r
1200 \r
1201     // If possible, write back the mapping record (this isn't crucial).\r
1202     try {\r
1203         if (obj.isnull()) {\r
1204             m_storage_lite->deleteText("NameID", name.get());\r
1205         }\r
1206         else if (!sessionsKilled.empty()) {\r
1207             ostringstream out;\r
1208             out << obj;\r
1209             if (m_storage_lite->updateText("NameID", name.get(), out.str().c_str(), 0, ver) <= 0)\r
1210                 m_log.warn("logout mapping record changed behind us, leaving it alone");\r
1211         }\r
1212     }\r
1213     catch (exception& ex) {\r
1214         m_log.error("error updating logout mapping record: %s", ex.what());\r
1215     }\r
1216 \r
1217     return sessionsKilled.size();\r
1218 }\r
1219 \r
1220 bool SSCache::stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const\r
1221 {\r
1222     if (!XMLString::equals(n1.getName(), n2.getName()))\r
1223         return false;\r
1224     \r
1225     const XMLCh* s1 = n1.getFormat();\r
1226     const XMLCh* s2 = n2.getFormat();\r
1227     if (!s1 || !*s1)\r
1228         s1 = saml2::NameID::UNSPECIFIED;\r
1229     if (!s2 || !*s2)\r
1230         s2 = saml2::NameID::UNSPECIFIED;\r
1231     if (!XMLString::equals(s1,s2))\r
1232         return false;\r
1233     \r
1234     s1 = n1.getNameQualifier();\r
1235     s2 = n2.getNameQualifier();\r
1236     if (!s1 || !*s1)\r
1237         s1 = idp;\r
1238     if (!s2 || !*s2)\r
1239         s2 = idp;\r
1240     if (!XMLString::equals(s1,s2))\r
1241         return false;\r
1242 \r
1243     s1 = n1.getSPNameQualifier();\r
1244     s2 = n2.getSPNameQualifier();\r
1245     if (!s1 || !*s1)\r
1246         s1 = sp;\r
1247     if (!s2 || !*s2)\r
1248         s2 = sp;\r
1249     if (!XMLString::equals(s1,s2))\r
1250         return false;\r
1251 \r
1252     return true;\r
1253 }\r
1254 \r
1255 #endif\r
1256 \r
1257 Session* SSCache::find(const Application& application, const char* key, const char* client_addr, time_t* timeout)\r
1258 {\r
1259 #ifdef _DEBUG\r
1260     xmltooling::NDC ndc("find");\r
1261 #endif\r
1262     StoredSession* session=NULL;\r
1263 \r
1264     if (inproc) {\r
1265         m_log.debug("searching local cache for session (%s)", key);\r
1266         m_lock->rdlock();\r
1267         map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);\r
1268         if (i!=m_hashtable.end()) {\r
1269             // Save off and lock the session.\r
1270             session = i->second;\r
1271             session->lock();\r
1272             m_lock->unlock();\r
1273             m_log.debug("session found locally, validating it for use");\r
1274         }\r
1275         else {\r
1276             m_lock->unlock();\r
1277         }\r
1278     }\r
1279 \r
1280     if (!session) {\r
1281         if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {\r
1282             m_log.debug("session not found locally, remoting the search");\r
1283             // Remote the request.\r
1284             DDF in("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;\r
1285             DDFJanitor jin(in);\r
1286             in.structure();\r
1287             in.addmember("key").string(key);\r
1288             in.addmember("application_id").string(application.getId());\r
1289             if (timeout && *timeout) {\r
1290                 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  \r
1291 #ifndef HAVE_GMTIME_R\r
1292                 struct tm* ptime=gmtime(timeout);\r
1293 #else\r
1294                 struct tm res;\r
1295                 struct tm* ptime=gmtime_r(timeout,&res);\r
1296 #endif\r
1297                 char timebuf[32];\r
1298                 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);\r
1299                 in.addmember("timeout").string(timebuf);\r
1300             }\r
1301             \r
1302             try {\r
1303                 out=application.getServiceProvider().getListenerService()->send(in);\r
1304                 if (!out.isstruct()) {\r
1305                     out.destroy();\r
1306                     m_log.debug("session not found in remote cache");\r
1307                     return NULL;\r
1308                 }\r
1309                 \r
1310                 // Wrap the results in a local entry and save it.\r
1311                 session = new StoredSession(this, out);\r
1312                 // The remote end has handled timeout issues, we handle address and expiration checks.\r
1313                 timeout = NULL;\r
1314             }\r
1315             catch (...) {\r
1316                 out.destroy();\r
1317                 throw;\r
1318             }\r
1319         }\r
1320         else {\r
1321             // We're out of process, so we can search the storage service directly.\r
1322 #ifndef SHIBSP_LITE\r
1323             if (!m_storage)\r
1324                 throw ConfigurationException("SessionCache lookup requires a StorageService.");\r
1325 \r
1326             m_log.debug("searching for session (%s)", key);\r
1327             \r
1328             DDF obj;\r
1329             time_t lastAccess;\r
1330             string record;\r
1331             int ver = m_storage->readText(key, "session", &record, &lastAccess);\r
1332             if (!ver)\r
1333                 return NULL;\r
1334             \r
1335             m_log.debug("reconstituting session and checking validity");\r
1336             \r
1337             istringstream in(record);\r
1338             in >> obj;\r
1339             \r
1340             lastAccess -= m_cacheTimeout;   // adjusts it back to the last time the record's timestamp was touched\r
1341             time_t now=time(NULL);\r
1342             \r
1343             if (timeout && *timeout > 0 && now - lastAccess >= *timeout) {\r
1344                 m_log.info("session timed out (ID: %s)", key);\r
1345                 remove(application, key);\r
1346                 const char* eid = obj["entity_id"].string();\r
1347                 if (!eid) {\r
1348                     obj.destroy();\r
1349                     throw RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
1350                 }\r
1351                 string eid2(eid);\r
1352                 obj.destroy();\r
1353                 throw RetryableProfileException("Your session has expired, and you must re-authenticate.", namedparams(1, "entityID", eid2.c_str()));\r
1354             }\r
1355             \r
1356             if (timeout) {\r
1357                 // Update storage expiration, if possible.\r
1358                 try {\r
1359                     m_storage->updateContext(key, now + m_cacheTimeout);\r
1360                 }\r
1361                 catch (exception& ex) {\r
1362                     m_log.error("failed to update session expiration: %s", ex.what());\r
1363                 }\r
1364             }\r
1365 \r
1366             // Wrap the results in a local entry and save it.\r
1367             session = new StoredSession(this, obj);\r
1368             // We handled timeout issues, still need to handle address and expiration checks.\r
1369             timeout = NULL;\r
1370 #else\r
1371             throw ConfigurationException("SessionCache search requires a StorageService.");\r
1372 #endif\r
1373         }\r
1374 \r
1375         if (inproc) {\r
1376             // Lock for writing and repeat the search to avoid duplication.\r
1377             m_lock->wrlock();\r
1378             SharedLock shared(m_lock, false);\r
1379             if (m_hashtable.count(key)) {\r
1380                 // We're using an existing session entry.\r
1381                 delete session;\r
1382                 session = m_hashtable[key];\r
1383                 session->lock();\r
1384             }\r
1385             else {\r
1386                 m_hashtable[key]=session;\r
1387                 session->lock();\r
1388             }\r
1389         }\r
1390     }\r
1391 \r
1392     if (!XMLString::equals(session->getApplicationID(), application.getId())) {\r
1393         m_log.error("an application (%s) tried to access another application's session", application.getId());\r
1394         session->unlock();\r
1395         return NULL;\r
1396     }\r
1397 \r
1398     // Verify currency and update the timestamp if indicated by caller.\r
1399     try {\r
1400         session->validate(application, client_addr, timeout);\r
1401     }\r
1402     catch (...) {\r
1403         session->unlock();\r
1404         remove(application, key);\r
1405         throw;\r
1406     }\r
1407     \r
1408     return session;\r
1409 }\r
1410 \r
1411 void SSCache::remove(const Application& application, const char* key)\r
1412 {\r
1413 #ifdef _DEBUG\r
1414     xmltooling::NDC ndc("remove");\r
1415 #endif\r
1416     // Take care of local copy.\r
1417     if (inproc)\r
1418         dormant(key);\r
1419     \r
1420     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {\r
1421         // Remove the session from storage directly.\r
1422 #ifndef SHIBSP_LITE\r
1423         m_storage->deleteContext(key);\r
1424         m_log.info("removed session (%s)", key);\r
1425 \r
1426         TransactionLog* xlog = application.getServiceProvider().getTransactionLog();\r
1427         Locker locker(xlog);\r
1428         xlog->log.info("Destroyed session (applicationId: %s) (ID: %s)", application.getId(), key);\r
1429 #else\r
1430         throw ConfigurationException("SessionCache removal requires a StorageService.");\r
1431 #endif\r
1432     }\r
1433     else {\r
1434         // Remote the request.\r
1435         DDF in("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache");\r
1436         DDFJanitor jin(in);\r
1437         in.structure();\r
1438         in.addmember("key").string(key);\r
1439         in.addmember("application_id").string(application.getId());\r
1440         \r
1441         DDF out = application.getServiceProvider().getListenerService()->send(in);\r
1442         out.destroy();\r
1443     }\r
1444 }\r
1445 \r
1446 void SSCache::dormant(const char* key)\r
1447 {\r
1448 #ifdef _DEBUG\r
1449     xmltooling::NDC ndc("dormant");\r
1450 #endif\r
1451 \r
1452     m_log.debug("deleting local copy of session (%s)", key);\r
1453 \r
1454     // lock the cache for writing, which means we know nobody is sitting in find()\r
1455     m_lock->wrlock();\r
1456 \r
1457     // grab the entry from the table\r
1458     map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);\r
1459     if (i==m_hashtable.end()) {\r
1460         m_lock->unlock();\r
1461         return;\r
1462     }\r
1463 \r
1464     // ok, remove the entry and lock it\r
1465     StoredSession* entry=i->second;\r
1466     m_hashtable.erase(key);\r
1467     entry->lock();\r
1468     \r
1469     // unlock the cache\r
1470     m_lock->unlock();\r
1471 \r
1472     // we can release the cache entry lock because we know we're not in the cache anymore\r
1473     entry->unlock();\r
1474 \r
1475     delete entry;\r
1476 }\r
1477 \r
1478 void SSCache::cleanup()\r
1479 {\r
1480 #ifdef _DEBUG\r
1481     xmltooling::NDC ndc("cleanup");\r
1482 #endif\r
1483 \r
1484     Mutex* mutex = Mutex::create();\r
1485   \r
1486     // Load our configuration details...\r
1487     static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);\r
1488     const XMLCh* tag=m_root ? m_root->getAttributeNS(NULL,cleanupInterval) : NULL;\r
1489     int rerun_timer = 900;\r
1490     if (tag && *tag)\r
1491         rerun_timer = XMLString::parseInt(tag);\r
1492     if (rerun_timer <= 0)\r
1493         rerun_timer = 900;\r
1494 \r
1495     mutex->lock();\r
1496 \r
1497     m_log.info("cleanup thread started...run every %d secs; timeout after %d secs", rerun_timer, m_inprocTimeout);\r
1498 \r
1499     while (!shutdown) {\r
1500         shutdown_wait->timedwait(mutex,rerun_timer);\r
1501         if (shutdown)\r
1502             break;\r
1503 \r
1504         // Ok, let's run through the cleanup process and clean out\r
1505         // really old sessions.  This is a two-pass process.  The\r
1506         // first pass is done holding a read-lock while we iterate over\r
1507         // the cache.  The second pass doesn't need a lock because\r
1508         // the 'deletes' will lock the cache.\r
1509     \r
1510         // Pass 1: iterate over the map and find all entries that have not been\r
1511         // used in the allotted timeout.\r
1512         vector<string> stale_keys;\r
1513         time_t stale = time(NULL) - m_inprocTimeout;\r
1514     \r
1515         m_log.debug("cleanup thread running");\r
1516 \r
1517         m_lock->rdlock();\r
1518         for (map<string,StoredSession*>::const_iterator i=m_hashtable.begin(); i!=m_hashtable.end(); ++i) {\r
1519             // If the last access was BEFORE the stale timeout...\r
1520             i->second->lock();\r
1521             time_t last=i->second->getLastAccess();\r
1522             i->second->unlock();\r
1523             if (last < stale)\r
1524                 stale_keys.push_back(i->first);\r
1525         }\r
1526         m_lock->unlock();\r
1527     \r
1528         if (!stale_keys.empty()) {\r
1529             m_log.info("purging %d old sessions", stale_keys.size());\r
1530     \r
1531             // Pass 2: walk through the list of stale entries and remove them from the cache\r
1532             for (vector<string>::const_iterator j = stale_keys.begin(); j != stale_keys.end(); ++j)\r
1533                 dormant(j->c_str());\r
1534         }\r
1535 \r
1536         m_log.debug("cleanup thread completed");\r
1537     }\r
1538 \r
1539     m_log.info("cleanup thread exiting");\r
1540 \r
1541     mutex->unlock();\r
1542     delete mutex;\r
1543     Thread::exit(NULL);\r
1544 }\r
1545 \r
1546 void* SSCache::cleanup_fn(void* cache_p)\r
1547 {\r
1548 #ifndef WIN32\r
1549     // First, let's block all signals \r
1550     Thread::mask_all_signals();\r
1551 #endif\r
1552 \r
1553     // Now run the cleanup process.\r
1554     reinterpret_cast<SSCache*>(cache_p)->cleanup();\r
1555     return NULL;\r
1556 }\r
1557 \r
1558 #ifndef SHIBSP_LITE\r
1559 \r
1560 void SSCache::receive(DDF& in, ostream& out)\r
1561 {\r
1562 #ifdef _DEBUG\r
1563     xmltooling::NDC ndc("receive");\r
1564 #endif\r
1565 \r
1566     if (!strcmp(in.name(),"find::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {\r
1567         const char* key=in["key"].string();\r
1568         if (!key)\r
1569             throw ListenerException("Required parameters missing for session lookup.");\r
1570 \r
1571         const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());\r
1572         if (!app)\r
1573             throw ListenerException("Application not found, check configuration?");\r
1574 \r
1575         // Do an unversioned read.\r
1576         string record;\r
1577         time_t lastAccess;\r
1578         if (!m_storage->readText(key, "session", &record, &lastAccess)) {\r
1579             DDF ret(NULL);\r
1580             DDFJanitor jan(ret);\r
1581             out << ret;\r
1582             return;\r
1583         }\r
1584 \r
1585         // Adjust for expiration to recover last access time and check timeout.\r
1586         lastAccess -= m_cacheTimeout;\r
1587         time_t now=time(NULL);\r
1588 \r
1589         // See if we need to check for a timeout.\r
1590         if (in["timeout"].string()) {\r
1591             time_t timeout = 0;\r
1592             auto_ptr_XMLCh dt(in["timeout"].string());\r
1593             DateTime dtobj(dt.get());\r
1594             dtobj.parseDateTime();\r
1595             timeout = dtobj.getEpoch();\r
1596                     \r
1597             if (timeout > 0 && now - lastAccess >= timeout) {\r
1598                 m_log.info("session timed out (ID: %s)", key);\r
1599                 remove(*app, key);\r
1600                 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
1601             } \r
1602 \r
1603             // Update storage expiration, if possible.\r
1604             try {\r
1605                 m_storage->updateContext(key, now + m_cacheTimeout);\r
1606             }\r
1607             catch (exception& ex) {\r
1608                 m_log.error("failed to update session expiration: %s", ex.what());\r
1609             }\r
1610         }\r
1611             \r
1612         // Send the record back.\r
1613         out << record;\r
1614     }\r
1615     else if (!strcmp(in.name(),"touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {\r
1616         const char* key=in["key"].string();\r
1617         if (!key)\r
1618             throw ListenerException("Required parameters missing for session check.");\r
1619 \r
1620         // Do a versioned read.\r
1621         string record;\r
1622         time_t lastAccess;\r
1623         int curver = in["version"].integer();\r
1624         int ver = m_storage->readText(key, "session", &record, &lastAccess, curver);\r
1625         if (ver == 0) {\r
1626             m_log.warn("unsuccessful versioned read of session (ID: %s), caches out of sync?", key);\r
1627             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
1628         }\r
1629 \r
1630         // Adjust for expiration to recover last access time and check timeout.\r
1631         lastAccess -= m_cacheTimeout;\r
1632         time_t now=time(NULL);\r
1633 \r
1634         // See if we need to check for a timeout.\r
1635         time_t timeout = 0;\r
1636         auto_ptr_XMLCh dt(in["timeout"].string());\r
1637         if (dt.get()) {\r
1638             DateTime dtobj(dt.get());\r
1639             dtobj.parseDateTime();\r
1640             timeout = dtobj.getEpoch();\r
1641         }\r
1642                 \r
1643         if (timeout > 0 && now - lastAccess >= timeout) {\r
1644             m_log.info("session timed out (ID: %s)", key);\r
1645             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
1646         } \r
1647 \r
1648         // Update storage expiration, if possible.\r
1649         try {\r
1650             m_storage->updateContext(key, now + m_cacheTimeout);\r
1651         }\r
1652         catch (exception& ex) {\r
1653             m_log.error("failed to update session expiration: %s", ex.what());\r
1654         }\r
1655             \r
1656         if (ver > curver) {\r
1657             // Send the record back.\r
1658             out << record;\r
1659         }\r
1660         else {\r
1661             DDF ret(NULL);\r
1662             DDFJanitor jan(ret);\r
1663             out << ret;\r
1664         }\r
1665     }\r
1666     else if (!strcmp(in.name(),"remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {\r
1667         const char* key=in["key"].string();\r
1668         if (!key)\r
1669             throw ListenerException("Required parameter missing for session removal.");\r
1670 \r
1671         const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());\r
1672         if (!app)\r
1673             throw ConfigurationException("Application not found, check configuration?");\r
1674 \r
1675         remove(*app, key);\r
1676         DDF ret(NULL);\r
1677         DDFJanitor jan(ret);\r
1678         out << ret;\r
1679     }\r
1680 }\r
1681 \r
1682 #endif\r