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