2 * Licensed to the University Corporation for Advanced Internet
3 * Development, Inc. (UCAID) under one or more contributor license
4 * agreements. See the NOTICE file distributed with this work for
5 * additional information regarding copyright ownership.
7 * UCAID licenses this file to you under the Apache License,
8 * Version 2.0 (the "License"); you may not use this file except
9 * in compliance with the License. You may obtain a copy of the
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17 * either express or implied. See the License for the specific
18 * language governing permissions and limitations under the License.
22 * StorageServiceSessionCache.cpp
24 * StorageService-based SessionCache implementation.
26 * Instead of optimizing this plugin with a buffering scheme that keeps objects around
27 * and avoids extra parsing steps, I'm assuming that systems that require such can
28 * layer their own cache plugin on top of this version either by delegating to it
29 * or using the remoting support. So this version will load sessions directly
30 * from the StorageService, instantiate enough to expose the Session API,
31 * and then delete everything when they're unlocked. All data in memory is always
32 * kept in sync with the StorageService (no lazy updates).
36 #include "Application.h"
37 #include "exceptions.h"
38 #include "ServiceProvider.h"
39 #include "SessionCacheEx.h"
40 #include "TransactionLog.h"
41 #include "attribute/Attribute.h"
42 #include "handler/RemotedHandler.h"
43 #include "remoting/ListenerService.h"
44 #include "util/SPConstants.h"
47 #include <xmltooling/io/HTTPRequest.h>
48 #include <xmltooling/io/HTTPResponse.h>
49 #include <xmltooling/util/DateTime.h>
50 #include <xmltooling/util/NDC.h>
51 #include <xmltooling/util/ParserPool.h>
52 #include <xmltooling/util/Threads.h>
53 #include <xmltooling/util/XMLHelper.h>
54 #include <xercesc/util/XMLUniDefs.hpp>
57 # include <saml/exceptions.h>
58 # include <saml/SAMLConfig.h>
59 # include <saml/saml2/core/Assertions.h>
60 # include <saml/saml2/metadata/Metadata.h>
61 # include <xmltooling/XMLToolingConfig.h>
62 # include <xmltooling/util/StorageService.h>
63 using namespace opensaml::saml2md;
66 # include <xmltooling/util/DateTime.h>
69 using namespace shibsp;
70 using namespace opensaml;
71 using namespace xmltooling;
77 class SSCache : public SessionCacheEx
79 ,public virtual Remoted
83 SSCache(const DOMElement* e);
87 void receive(DDF& in, ostream& out);
90 const Application& app,
91 const HTTPRequest& httpRequest,
92 HTTPResponse& httpResponse,
94 const EntityDescriptor* issuer=nullptr,
95 const XMLCh* protocol=nullptr,
96 const saml2::NameID* nameid=nullptr,
97 const XMLCh* authn_instant=nullptr,
98 const XMLCh* session_index=nullptr,
99 const XMLCh* authncontext_class=nullptr,
100 const XMLCh* authncontext_decl=nullptr,
101 const vector<const Assertion*>* tokens=nullptr,
102 const vector<Attribute*>* attributes=nullptr
105 insert(dummy, app, httpRequest, httpResponse, expires, issuer, protocol, nameid,
106 authn_instant, session_index, authncontext_class, authncontext_decl, tokens, attributes);
111 const Application& app,
112 const HTTPRequest& httpRequest,
113 HTTPResponse& httpResponse,
115 const EntityDescriptor* issuer=nullptr,
116 const XMLCh* protocol=nullptr,
117 const saml2::NameID* nameid=nullptr,
118 const XMLCh* authn_instant=nullptr,
119 const XMLCh* session_index=nullptr,
120 const XMLCh* authncontext_class=nullptr,
121 const XMLCh* authncontext_decl=nullptr,
122 const vector<const Assertion*>* tokens=nullptr,
123 const vector<Attribute*>* attributes=nullptr
125 vector<string>::size_type logout(
126 const Application& app,
127 const EntityDescriptor* issuer,
128 const saml2::NameID& nameid,
129 const set<string>* indexes,
131 vector<string>& sessions
134 const Application& app,
135 const HTTPRequest& request,
136 const EntityDescriptor* issuer,
137 const saml2::NameID& nameid,
138 const set<string>* indexes
141 Session* find(const Application& app, const char* key, const char* client_addr=nullptr, time_t* timeout=nullptr);
142 void remove(const Application& app, const char* key);
145 string active(const Application& app, const HTTPRequest& request) {
146 if (!m_inboundHeader.empty()) {
147 string session_id = request.getHeader(m_inboundHeader.c_str());
148 if (!session_id.empty())
151 pair<string,const char*> shib_cookie = app.getCookieNameProps("_shibsession_");
152 const char* session_id = request.getCookie(shib_cookie.first.c_str());
153 return (session_id ? session_id : "");
156 Session* find(const Application& app, const HTTPRequest& request, const char* client_addr=nullptr, time_t* timeout=nullptr) {
157 string id = active(app, request);
159 return find(app, id.c_str(), client_addr, timeout);
163 Session* find(const Application& app, HTTPRequest& request, const char* client_addr=nullptr, time_t* timeout=nullptr);
164 void remove(const Application& app, const HTTPRequest& request, HTTPResponse* response=nullptr);
166 unsigned long getCacheTimeout(const Application& app) {
167 // Computes offset for adjusting expiration of sessions.
168 // This can either be static, or dynamic based on the per-app session timeout or lifetime.
170 return m_cacheTimeout;
171 pair<bool,unsigned int> timeout = pair<bool,unsigned int>(false, 3600);
172 const PropertySet* props = app.getPropertySet("Sessions");
174 timeout = props->getUnsignedInt("timeout");
176 timeout.second = 3600;
178 // As long as one of the two factors is set, add them together.
179 if (timeout.second > 0 || m_cacheAllowance > 0)
180 return timeout.second + m_cacheAllowance;
182 // If timeouts are off, and there's no cache slop set, then use the lifetime.
183 timeout = pair<bool,unsigned int>(false, 28800);
185 timeout = props->getUnsignedInt("lifetime");
186 if (!timeout.first || timeout.second == 0)
187 timeout.second = 28800;
189 return timeout.second;
195 StorageService* m_storage;
196 StorageService* m_storage_lite;
201 // maintain back-mappings of NameID/SessionIndex -> session key
202 void insert(const char* key, time_t expires, const char* name, const char* index);
203 bool stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const;
204 LogoutEvent* newLogoutEvent(const Application& app) const;
206 bool m_cacheAssertions;
208 const DOMElement* m_root; // Only valid during initialization
209 unsigned long m_inprocTimeout,m_cacheTimeout,m_cacheAllowance;
210 string m_inboundHeader,m_outboundHeader;
212 // inproc means we buffer sessions in memory
214 map<string,StoredSession*> m_hashtable;
216 // management of buffered sessions
217 void dormant(const char* key);
218 static void* cleanup_fn(void*);
221 CondWait* shutdown_wait;
222 Thread* cleanup_thread;
225 class StoredSession : public virtual Session
228 StoredSession(SSCache* cache, DDF& obj) : m_obj(obj),
232 m_cache(cache), m_expires(0), m_lastAccess(time(nullptr)), m_lock(nullptr) {
233 auto_ptr_XMLCh exp(m_obj["expires"].string());
235 DateTime iso(exp.get());
237 m_expires = iso.getEpoch();
241 const char* nameid = obj["nameid"].string();
243 // Parse and bind the document into an XMLObject.
244 istringstream instr(nameid);
245 DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr);
246 XercesJanitor<DOMDocument> janitor(doc);
247 auto_ptr<saml2::NameID> n(saml2::NameIDBuilder::buildNameID());
248 n->unmarshall(doc->getDocumentElement(), true);
250 m_nameid = n.release();
254 m_lock = Mutex::create();
260 for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
263 for_each(m_tokens.begin(), m_tokens.end(), cleanup_pair<string,Assertion>());
279 const char* getID() const {
282 const char* getApplicationID() const {
283 return m_obj["application_id"].string();
285 const char* getClientAddress() const {
286 return m_obj["client_addr"].string();
288 const char* getEntityID() const {
289 return m_obj["entity_id"].string();
291 const char* getProtocol() const {
292 return m_obj["protocol"].string();
294 const char* getAuthnInstant() const {
295 return m_obj["authn_instant"].string();
298 const saml2::NameID* getNameID() const {
302 const char* getSessionIndex() const {
303 return m_obj["session_index"].string();
305 const char* getAuthnContextClassRef() const {
306 return m_obj["authncontext_class"].string();
308 const char* getAuthnContextDeclRef() const {
309 return m_obj["authncontext_decl"].string();
311 const vector<Attribute*>& getAttributes() const {
312 if (m_attributes.empty())
313 unmarshallAttributes();
316 const multimap<string,const Attribute*>& getIndexedAttributes() const {
317 if (m_attributeIndex.empty()) {
318 if (m_attributes.empty())
319 unmarshallAttributes();
320 for (vector<Attribute*>::const_iterator a = m_attributes.begin(); a != m_attributes.end(); ++a) {
321 const vector<string>& aliases = (*a)->getAliases();
322 for (vector<string>::const_iterator alias = aliases.begin(); alias != aliases.end(); ++alias)
323 m_attributeIndex.insert(multimap<string,const Attribute*>::value_type(*alias, *a));
326 return m_attributeIndex;
328 const vector<const char*>& getAssertionIDs() const {
330 DDF ids = m_obj["assertions"];
331 DDF id = ids.first();
332 while (id.isstring()) {
333 m_ids.push_back(id.string());
340 void validate(const Application& application, const char* client_addr, time_t* timeout);
343 void addAttributes(const vector<Attribute*>& attributes);
344 const Assertion* getAssertion(const char* id) const;
345 void addAssertion(Assertion* assertion);
348 time_t getExpiration() const { return m_expires; }
349 time_t getLastAccess() const { return m_lastAccess; }
352 void unmarshallAttributes() const;
356 saml2::NameID* m_nameid;
357 mutable map<string,Assertion*> m_tokens;
359 mutable vector<Attribute*> m_attributes;
360 mutable multimap<string,const Attribute*> m_attributeIndex;
361 mutable vector<const char*> m_ids;
364 time_t m_expires,m_lastAccess;
368 SessionCache* SHIBSP_DLLLOCAL StorageServiceCacheFactory(const DOMElement* const & e)
370 return new SSCache(e);
374 Session* SessionCache::find(const Application& application, HTTPRequest& request, const char* client_addr, time_t* timeout)
376 return find(application, const_cast<const HTTPRequest&>(request), client_addr, timeout);
379 void SHIBSP_API shibsp::registerSessionCaches()
381 SPConfig::getConfig().SessionCacheManager.registerFactory(STORAGESERVICE_SESSION_CACHE, StorageServiceCacheFactory);
392 void StoredSession::unmarshallAttributes() const
394 Attribute* attribute;
395 DDF attrs = m_obj["attributes"];
396 DDF attr = attrs.first();
397 while (!attr.isnull()) {
399 attribute = Attribute::unmarshall(attr);
400 m_attributes.push_back(attribute);
401 if (m_cache->m_log.isDebugEnabled())
402 m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",
403 attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");
405 catch (AttributeException& ex) {
406 const char* id = attr.first().name();
407 m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());
413 void StoredSession::validate(const Application& app, const char* client_addr, time_t* timeout)
415 time_t now = time(nullptr);
419 if (now > m_expires) {
420 m_cache->m_log.info("session expired (ID: %s)", getID());
421 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
427 if (m_cache->m_log.isDebugEnabled())
428 m_cache->m_log.debug("comparing client address %s against %s", client_addr, getClientAddress());
429 if (!XMLString::equals(getClientAddress(),client_addr)) {
430 m_cache->m_log.warn("client address mismatch");
431 throw RetryableProfileException(
432 "Your IP address ($1) does not match the address recorded at the time the session was established.",
433 params(1,client_addr)
441 if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
442 DDF in("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;
445 in.addmember("key").string(getID());
446 in.addmember("version").integer(m_obj["version"].integer());
447 in.addmember("application_id").string(app.getId());
449 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
450 #ifndef HAVE_GMTIME_R
451 struct tm* ptime=gmtime(timeout);
454 struct tm* ptime=gmtime_r(timeout,&res);
457 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
458 in.addmember("timeout").string(timebuf);
462 out=app.getServiceProvider().getListenerService()->send(in);
469 if (out.isstruct()) {
470 // We got an updated record back.
472 for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
473 m_attributes.clear();
474 m_attributeIndex.clear();
481 if (!m_cache->m_storage)
482 throw ConfigurationException("Session touch requires a StorageService.");
484 // Do a versioned read.
487 int curver = m_obj["version"].integer();
488 int ver = m_cache->m_storage->readText(getID(), "session", &record, &lastAccess, curver);
490 m_cache->m_log.warn("unsuccessful versioned read of session (ID: %s), cache out of sync?", getID());
491 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
494 // Adjust for expiration to recover last access time and check timeout.
495 unsigned long cacheTimeout = m_cache->getCacheTimeout(app);
496 lastAccess -= cacheTimeout;
497 if (*timeout > 0 && now - lastAccess >= *timeout) {
498 m_cache->m_log.info("session timed out (ID: %s)", getID());
499 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
502 // Update storage expiration, if possible.
504 m_cache->m_storage->updateContext(getID(), now + cacheTimeout);
506 catch (exception& ex) {
507 m_cache->m_log.error("failed to update session expiration: %s", ex.what());
511 // We got an updated record back.
513 istringstream in(record);
516 for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
517 m_attributes.clear();
518 m_attributeIndex.clear();
523 throw ConfigurationException("Session touch requires a StorageService.");
532 void StoredSession::addAttributes(const vector<Attribute*>& attributes)
535 xmltooling::NDC ndc("addAttributes");
538 if (!m_cache->m_storage)
539 throw ConfigurationException("Session modification requires a StorageService.");
541 m_cache->m_log.debug("adding attributes to session (%s)", getID());
546 DDF attrs = m_obj["attributes"];
548 attrs = m_obj.addmember("attributes").list();
549 for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a) {
550 attr = (*a)->marshall();
554 // Tentatively increment the version.
555 m_obj["version"].integer(m_obj["version"].integer()+1);
559 string record(str.str());
562 ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);
565 // Roll back modification to record.
566 m_obj["version"].integer(m_obj["version"].integer()-1);
567 vector<Attribute*>::size_type count = attributes.size();
569 attrs.last().destroy();
574 // Roll back modification to record.
575 m_obj["version"].integer(m_obj["version"].integer()-1);
576 vector<Attribute*>::size_type count = attributes.size();
578 attrs.last().destroy();
581 // Fatal problem with update.
582 throw IOException("Unable to update stored session.");
586 m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
587 ver = m_cache->m_storage->readText(getID(), "session", &record, nullptr);
589 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
590 throw IOException("Unable to read back stored session.");
595 istringstream in(record);
599 for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
600 m_attributes.clear();
601 m_attributeIndex.clear();
602 newobj["version"].integer(ver);
608 } while (ver < 0); // negative indicates a sync issue so we retry
610 // We own them now, so clean them up.
611 for_each(attributes.begin(), attributes.end(), xmltooling::cleanup<Attribute>());
614 const Assertion* StoredSession::getAssertion(const char* id) const
616 if (!m_cache->m_storage)
617 throw ConfigurationException("Assertion retrieval requires a StorageService.");
619 map<string,Assertion*>::const_iterator i = m_tokens.find(id);
620 if (i!=m_tokens.end())
624 if (!m_cache->m_storage->readText(getID(), id, &tokenstr, nullptr))
625 throw FatalProfileException("Assertion not found in cache.");
627 // Parse and bind the document into an XMLObject.
628 istringstream instr(tokenstr);
629 DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr);
630 XercesJanitor<DOMDocument> janitor(doc);
631 auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));
634 Assertion* token = dynamic_cast<Assertion*>(xmlObject.get());
636 throw FatalProfileException("Request for cached assertion returned an unknown object type.");
638 // Transfer ownership to us.
644 void StoredSession::addAssertion(Assertion* assertion)
647 xmltooling::NDC ndc("addAssertion");
650 if (!m_cache->m_storage)
651 throw ConfigurationException("Session modification requires a StorageService.");
653 throw FatalProfileException("Unknown object type passed to session for storage.");
655 auto_ptr_char id(assertion->getID());
656 if (!id.get() || !*id.get())
657 throw IOException("Assertion did not carry an ID.");
658 else if (strlen(id.get()) > m_cache->m_storage->getCapabilities().getKeySize())
659 throw IOException("Assertion ID ($1) exceeds allowable storage key size.", params(1, id.get()));
661 m_cache->m_log.debug("adding assertion (%s) to session (%s)", id.get(), getID());
664 if (!m_cache->m_storage->readText(getID(), "session", nullptr, &exp))
665 throw IOException("Unable to load expiration time for stored session.");
667 ostringstream tokenstr;
668 tokenstr << *assertion;
669 if (!m_cache->m_storage->createText(getID(), id.get(), tokenstr.str().c_str(), exp))
670 throw IOException("Attempted to insert duplicate assertion ID into session.");
674 DDF token = DDF(nullptr).string(id.get());
675 m_obj["assertions"].add(token);
677 // Tentatively increment the version.
678 m_obj["version"].integer(m_obj["version"].integer()+1);
682 string record(str.str());
685 ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);
689 m_obj["version"].integer(m_obj["version"].integer()-1);
690 m_cache->m_storage->deleteText(getID(), id.get());
696 m_obj["version"].integer(m_obj["version"].integer()-1);
699 // Fatal problem with update.
700 m_cache->m_log.error("updateText failed on StorageService for session (%s)", getID());
701 m_cache->m_storage->deleteText(getID(), id.get());
702 throw IOException("Unable to update stored session.");
706 m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
707 ver = m_cache->m_storage->readText(getID(), "session", &record, nullptr);
709 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
710 m_cache->m_storage->deleteText(getID(), id.get());
711 throw IOException("Unable to read back stored session.");
716 istringstream in(record);
720 for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
721 m_attributes.clear();
722 m_attributeIndex.clear();
723 newobj["version"].integer(ver);
729 } while (ver < 0); // negative indicates a sync issue so we retry
737 SessionCache::SessionCache()
741 SessionCache::~SessionCache()
747 void SessionCache::insert(
749 const Application& app,
750 const HTTPRequest& httpRequest,
751 HTTPResponse& httpResponse,
753 const EntityDescriptor* issuer,
754 const XMLCh* protocol,
755 const saml2::NameID* nameid,
756 const XMLCh* authn_instant,
757 const XMLCh* session_index,
758 const XMLCh* authncontext_class,
759 const XMLCh* authncontext_decl,
760 const vector<const Assertion*>* tokens,
761 const vector<Attribute*>* attributes
763 return insert(app, httpRequest, httpResponse, expires, issuer, protocol, nameid,
764 authn_instant, session_index, authncontext_class, authncontext_decl, tokens, attributes);
769 SessionCacheEx::SessionCacheEx()
773 SessionCacheEx::~SessionCacheEx()
777 SSCache::SSCache(const DOMElement* e)
778 : m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), inproc(true),
780 m_storage(nullptr), m_storage_lite(nullptr), m_cacheAssertions(true),
782 m_root(e), m_inprocTimeout(900), m_cacheTimeout(0), m_cacheAllowance(0),
783 m_lock(nullptr), shutdown(false), shutdown_wait(nullptr), cleanup_thread(nullptr)
785 SPConfig& conf = SPConfig::getConfig();
786 inproc = conf.isEnabled(SPConfig::InProcess);
788 static const XMLCh cacheAllowance[] = UNICODE_LITERAL_14(c,a,c,h,e,A,l,l,o,w,a,n,c,e);
789 static const XMLCh cacheAssertions[] = UNICODE_LITERAL_15(c,a,c,h,e,A,s,s,e,r,t,i,o,n,s);
790 static const XMLCh cacheTimeout[] = UNICODE_LITERAL_12(c,a,c,h,e,T,i,m,e,o,u,t);
791 static const XMLCh inprocTimeout[] = UNICODE_LITERAL_13(i,n,p,r,o,c,T,i,m,e,o,u,t);
792 static const XMLCh inboundHeader[] = UNICODE_LITERAL_13(i,n,b,o,u,n,d,H,e,a,d,e,r);
793 static const XMLCh outboundHeader[] = UNICODE_LITERAL_14(o,u,t,b,o,u,n,d,H,e,a,d,e,r);
794 static const XMLCh _StorageService[] = UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e);
795 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);
797 m_cacheTimeout = XMLHelper::getAttrInt(e, 0, cacheTimeout);
798 m_cacheAllowance = XMLHelper::getAttrInt(e, 0, cacheAllowance);
800 m_inprocTimeout = XMLHelper::getAttrInt(e, 900, inprocTimeout);
801 m_inboundHeader = XMLHelper::getAttrString(e, nullptr, inboundHeader);
802 if (!m_inboundHeader.empty())
803 RemotedHandler::addRemotedHeader(m_inboundHeader.c_str());
804 m_outboundHeader = XMLHelper::getAttrString(e, nullptr, outboundHeader);
807 if (conf.isEnabled(SPConfig::OutOfProcess)) {
808 string ssid(XMLHelper::getAttrString(e, nullptr, _StorageService));
810 m_storage = conf.getServiceProvider()->getStorageService(ssid.c_str());
812 m_log.info("bound to StorageService (%s)", ssid.c_str());
814 m_log.warn("specified StorageService (%s) not found", ssid.c_str());
817 m_storage = conf.getServiceProvider()->getStorageService(nullptr);
819 m_log.info("bound to arbitrary StorageService");
821 throw ConfigurationException("SessionCache unable to locate StorageService, check configuration.");
824 ssid = XMLHelper::getAttrString(e, nullptr, _StorageServiceLite);
826 m_storage_lite = conf.getServiceProvider()->getStorageService(ssid.c_str());
828 m_log.info("bound to 'lite' StorageService (%s)", ssid.c_str());
830 m_log.warn("specified 'lite' StorageService (%s) not found", ssid.c_str());
832 if (!m_storage_lite) {
833 m_log.info("StorageService for 'lite' use not set, using standard StorageService");
834 m_storage_lite = m_storage;
837 m_cacheAssertions = XMLHelper::getAttrBool(e, true, cacheAssertions);
841 ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
843 if (!conf.isEnabled(SPConfig::OutOfProcess) && !listener)
844 throw ConfigurationException("SessionCache requires a ListenerService, but none available.");
845 m_lock = RWLock::create();
846 shutdown_wait = CondWait::create();
847 cleanup_thread = Thread::create(&cleanup_fn, this);
851 if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
852 listener->regListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
853 listener->regListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
854 listener->regListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
857 m_log.info("no ListenerService available, cache remoting disabled");
866 // Shut down the cleanup thread and let it know...
868 shutdown_wait->signal();
869 cleanup_thread->join(nullptr);
871 for_each(m_hashtable.begin(),m_hashtable.end(),cleanup_pair<string,StoredSession>());
874 delete cleanup_thread;
875 delete shutdown_wait;
879 SPConfig& conf = SPConfig::getConfig();
880 ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
881 if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
882 listener->unregListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
883 listener->unregListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
884 listener->unregListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
894 auto_ptr_char temp(SAMLConfig::getConfig().generateIdentifier());
895 m_storage->createString("SessionCacheTest", temp.get(), "Test", time(nullptr) + 60);
896 m_storage->deleteString("SessionCacheTest", temp.get());
899 void SSCache::insert(const char* key, time_t expires, const char* name, const char* index)
902 unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize();
903 if (strlen(name) > storageLimit) {
904 dup = string(name).substr(0, storageLimit);
909 DDFJanitor jobj(obj);
911 // Since we can't guarantee uniqueness, check for an existing record.
914 int ver = m_storage_lite->readText("NameID", name, &record, &recordexp);
916 // Existing record, so we need to unmarshall it.
917 istringstream in(record);
922 obj = DDF(nullptr).structure();
925 if (!index || !*index)
927 DDF sessions = obj.addmember(index);
928 if (!sessions.islist())
930 DDF session = DDF(nullptr).string(key);
931 sessions.add(session);
933 // Remarshall the record.
937 // Try and store it back...
939 ver = m_storage_lite->updateText("NameID", name, out.str().c_str(), max(expires, recordexp), ver);
941 // Out of sync, or went missing, so retry.
942 return insert(key, expires, name, index);
945 else if (!m_storage_lite->createText("NameID", name, out.str().c_str(), expires)) {
946 // Hit a dup, so just retry, hopefully hitting the other branch.
947 return insert(key, expires, name, index);
951 void SSCache::insert(
953 const Application& app,
954 const HTTPRequest& httpRequest,
955 HTTPResponse& httpResponse,
957 const saml2md::EntityDescriptor* issuer,
958 const XMLCh* protocol,
959 const saml2::NameID* nameid,
960 const XMLCh* authn_instant,
961 const XMLCh* session_index,
962 const XMLCh* authncontext_class,
963 const XMLCh* authncontext_decl,
964 const vector<const Assertion*>* tokens,
965 const vector<Attribute*>* attributes
969 xmltooling::NDC ndc("insert");
972 throw ConfigurationException("SessionCache insertion requires a StorageService.");
974 m_log.debug("creating new session");
976 time_t now = time(nullptr);
977 auto_ptr_char index(session_index);
978 auto_ptr_char entity_id(issuer ? issuer->getEntityID() : nullptr);
979 auto_ptr_char name(nameid ? nameid->getName() : nullptr);
981 if (name.get() && *name.get()) {
982 // Check for a pending logout.
983 unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize();
984 string namebuf = name.get();
985 if (namebuf.length() > storageLimit)
986 namebuf = namebuf.substr(0, storageLimit);
988 int ver = m_storage_lite->readText("Logout", namebuf.c_str(), &pending);
991 DDFJanitor jpend(pendobj);
992 istringstream pstr(pending);
994 // IdP.SP.index contains logout expiration, if any.
995 DDF deadmenwalking = pendobj[issuer ? entity_id.get() : "_shibnull"][app.getRelyingParty(issuer)->getString("entityID").second];
996 const char* logexpstr = deadmenwalking[session_index ? index.get() : "_shibnull"].string();
997 if (!logexpstr && session_index) // we tried an exact session match, now try for nullptr
998 logexpstr = deadmenwalking["_shibnull"].string();
1000 auto_ptr_XMLCh dt(logexpstr);
1001 DateTime dtobj(dt.get());
1002 dtobj.parseDateTime();
1003 time_t logexp = dtobj.getEpoch();
1004 if (now - XMLToolingConfig::getConfig().clock_skew_secs < logexp)
1005 throw FatalProfileException("A logout message from your identity provider has blocked your login attempt.");
1010 XMLCh* widekey = SAMLConfig::getConfig().generateIdentifier();
1011 auto_ptr_char key(widekey);
1012 XMLString::release(&widekey);
1014 // Store session properties in DDF.
1015 DDF obj = DDF(key.get()).structure();
1016 DDFJanitor entryobj(obj);
1017 obj.addmember("version").integer(1);
1018 obj.addmember("application_id").string(app.getId());
1020 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
1021 #ifndef HAVE_GMTIME_R
1022 struct tm* ptime=gmtime(&expires);
1025 struct tm* ptime=gmtime_r(&expires,&res);
1028 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
1029 obj.addmember("expires").string(timebuf);
1031 obj.addmember("client_addr").string(httpRequest.getRemoteAddr().c_str());
1033 obj.addmember("entity_id").string(entity_id.get());
1035 auto_ptr_char prot(protocol);
1036 obj.addmember("protocol").string(prot.get());
1038 if (authn_instant) {
1039 auto_ptr_char instant(authn_instant);
1040 obj.addmember("authn_instant").string(instant.get());
1043 obj.addmember("session_index").string(index.get());
1044 if (authncontext_class) {
1045 auto_ptr_char ac(authncontext_class);
1046 obj.addmember("authncontext_class").string(ac.get());
1048 if (authncontext_decl) {
1049 auto_ptr_char ad(authncontext_decl);
1050 obj.addmember("authncontext_decl").string(ad.get());
1054 ostringstream namestr;
1056 obj.addmember("nameid").string(namestr.str().c_str());
1059 if (tokens && m_cacheAssertions) {
1060 obj.addmember("assertions").list();
1061 for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
1062 auto_ptr_char tokenid((*t)->getID());
1063 DDF tokid = DDF(nullptr).string(tokenid.get());
1064 obj["assertions"].add(tokid);
1070 DDF attrlist = obj.addmember("attributes").list();
1071 for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) {
1072 attr = (*a)->marshall();
1077 ostringstream record;
1080 m_log.debug("storing new session...");
1081 unsigned long cacheTimeout = getCacheTimeout(app);
1082 if (!m_storage->createText(key.get(), "session", record.str().c_str(), now + cacheTimeout))
1083 throw FatalProfileException("Attempted to create a session with a duplicate key.");
1085 // Store the reverse mapping for logout.
1088 insert(key.get(), expires, name.get(), index.get());
1090 catch (exception& ex) {
1091 m_log.error("error storing back mapping of NameID for logout: %s", ex.what());
1094 if (tokens && m_cacheAssertions) {
1096 for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
1097 ostringstream tokenstr;
1099 auto_ptr_char tokenid((*t)->getID());
1100 if (!tokenid.get() || !*tokenid.get() || strlen(tokenid.get()) > m_storage->getCapabilities().getKeySize())
1101 throw IOException("Assertion ID is missing or exceeds key size of storage service.");
1102 else if (!m_storage->createText(key.get(), tokenid.get(), tokenstr.str().c_str(), now + cacheTimeout))
1103 throw IOException("Duplicate assertion ID ($1)", params(1, tokenid.get()));
1106 catch (exception& ex) {
1107 m_log.error("error storing assertion along with session: %s", ex.what());
1111 const char* pid = obj["entity_id"].string();
1112 const char* prot = obj["protocol"].string();
1113 m_log.info("new session created: ID (%s) IdP (%s) Protocol(%s) Address (%s)",
1114 key.get(), pid ? pid : "none", prot ? prot : "none", httpRequest.getRemoteAddr().c_str());
1116 if (!m_outboundHeader.empty())
1117 httpResponse.setResponseHeader(m_outboundHeader.c_str(), key.get());
1119 time_t cookieLifetime = 0;
1120 pair<string,const char*> shib_cookie = app.getCookieNameProps("_shibsession_", &cookieLifetime);
1121 string k(key.get());
1122 k += shib_cookie.second;
1124 if (cookieLifetime > 0) {
1125 cookieLifetime += now;
1126 #ifndef HAVE_GMTIME_R
1127 ptime=gmtime(&cookieLifetime);
1129 ptime=gmtime_r(&cookieLifetime,&res);
1131 char cookietimebuf[64];
1132 strftime(cookietimebuf,64,"; expires=%a, %d %b %Y %H:%M:%S GMT",ptime);
1136 httpResponse.setCookie(shib_cookie.first.c_str(), k.c_str());
1137 sessionID = key.get();
1140 bool SSCache::matches(
1141 const Application& app,
1142 const xmltooling::HTTPRequest& request,
1143 const saml2md::EntityDescriptor* issuer,
1144 const saml2::NameID& nameid,
1145 const set<string>* indexes
1148 auto_ptr_char entityID(issuer ? issuer->getEntityID() : nullptr);
1150 Session* session = find(app, request);
1152 Locker locker(session, false);
1153 if (XMLString::equals(session->getEntityID(), entityID.get()) && session->getNameID() &&
1154 stronglyMatches(issuer->getEntityID(), app.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) {
1155 return (!indexes || indexes->empty() || (session->getSessionIndex() ? (indexes->count(session->getSessionIndex())>0) : false));
1159 catch (exception& ex) {
1160 m_log.error("error while matching session: %s", ex.what());
1165 vector<string>::size_type SSCache::logout(
1166 const Application& app,
1167 const saml2md::EntityDescriptor* issuer,
1168 const saml2::NameID& nameid,
1169 const set<string>* indexes,
1171 vector<string>& sessionsKilled
1175 xmltooling::NDC ndc("logout");
1179 throw ConfigurationException("SessionCache logout requires a StorageService.");
1181 auto_ptr_char entityID(issuer ? issuer->getEntityID() : nullptr);
1182 auto_ptr_char name(nameid.getName());
1184 m_log.info("request to logout sessions from (%s) for (%s)", entityID.get() ? entityID.get() : "unknown", name.get());
1186 unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize();
1187 if (strlen(name.get()) > storageLimit)
1188 const_cast<char*>(name.get())[storageLimit] = 0;
1191 DDFJanitor jobj(obj);
1196 // Record the logout to prevent post-delivered assertions.
1197 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
1198 #ifndef HAVE_GMTIME_R
1199 struct tm* ptime=gmtime(&expires);
1202 struct tm* ptime=gmtime_r(&expires,&res);
1205 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
1208 ver = m_storage_lite->readText("Logout", name.get(), &record, &oldexp);
1210 istringstream lin(record);
1214 obj = DDF(nullptr).structure();
1217 // Structure is keyed by the IdP and SP, with a member per session index containing the expiration.
1218 DDF root = obj.addmember(issuer ? entityID.get() : "_shibnull").addmember(app.getRelyingParty(issuer)->getString("entityID").second);
1220 for (set<string>::const_iterator x = indexes->begin(); x!=indexes->end(); ++x)
1221 root.addmember(x->c_str()).string(timebuf);
1224 root.addmember("_shibnull").string(timebuf);
1232 ver = m_storage_lite->updateText("Logout", name.get(), lout.str().c_str(), max(expires, oldexp), ver);
1234 // Out of sync, or went missing, so retry.
1235 return logout(app, issuer, nameid, indexes, expires, sessionsKilled);
1238 else if (!m_storage_lite->createText("Logout", name.get(), lout.str().c_str(), expires)) {
1239 // Hit a dup, so just retry, hopefully hitting the other branch.
1240 return logout(app, issuer, nameid, indexes, expires, sessionsKilled);
1247 // Read in potentially matching sessions.
1248 ver = m_storage_lite->readText("NameID", name.get(), &record);
1250 m_log.debug("no active sessions to logout for supplied issuer and subject");
1254 istringstream in(record);
1257 // The record contains child lists for each known session index.
1259 DDF sessions = obj.first();
1260 while (sessions.islist()) {
1261 if (!indexes || indexes->empty() || indexes->count(sessions.name())) {
1262 key = sessions.first();
1263 while (key.isstring()) {
1264 // Fetch the session for comparison.
1265 Session* session = nullptr;
1267 session = find(app, key.string());
1269 catch (exception& ex) {
1270 m_log.error("error locating session (%s): %s", key.string(), ex.what());
1274 Locker locker(session, false);
1276 if (XMLString::equals(session->getEntityID(), entityID.get())) {
1278 if (stronglyMatches(issuer->getEntityID(), app.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) {
1279 sessionsKilled.push_back(key.string());
1283 m_log.debug("session (%s) contained a non-matching NameID, leaving it alone", key.string());
1287 m_log.debug("session (%s) established by different IdP, leaving it alone", key.string());
1291 // Session's gone, so...
1292 sessionsKilled.push_back(key.string());
1295 key = sessions.next();
1298 // No sessions left for this index?
1299 if (sessions.first().isnull())
1302 sessions = obj.next();
1305 if (obj.first().isnull())
1308 // If possible, write back the mapping record (this isn't crucial).
1311 m_storage_lite->deleteText("NameID", name.get());
1313 else if (!sessionsKilled.empty()) {
1316 if (m_storage_lite->updateText("NameID", name.get(), out.str().c_str(), 0, ver) <= 0)
1317 m_log.warn("logout mapping record changed behind us, leaving it alone");
1320 catch (exception& ex) {
1321 m_log.error("error updating logout mapping record: %s", ex.what());
1324 return sessionsKilled.size();
1327 bool SSCache::stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const
1329 if (!XMLString::equals(n1.getName(), n2.getName()))
1332 const XMLCh* s1 = n1.getFormat();
1333 const XMLCh* s2 = n2.getFormat();
1335 s1 = saml2::NameID::UNSPECIFIED;
1337 s2 = saml2::NameID::UNSPECIFIED;
1338 if (!XMLString::equals(s1,s2))
1341 s1 = n1.getNameQualifier();
1342 s2 = n2.getNameQualifier();
1347 if (!XMLString::equals(s1,s2))
1350 s1 = n1.getSPNameQualifier();
1351 s2 = n2.getSPNameQualifier();
1356 if (!XMLString::equals(s1,s2))
1362 LogoutEvent* SSCache::newLogoutEvent(const Application& app) const
1364 if (!SPConfig::getConfig().isEnabled(SPConfig::Logging))
1367 auto_ptr<TransactionLog::Event> event(SPConfig::getConfig().EventManager.newPlugin(LOGOUT_EVENT, nullptr));
1368 LogoutEvent* logout_event = dynamic_cast<LogoutEvent*>(event.get());
1370 logout_event->m_app = &app;
1372 return logout_event;
1375 m_log.warn("unable to audit event, log event object was of an incorrect type");
1378 catch (exception& ex) {
1379 m_log.warn("exception auditing event: %s", ex.what());
1386 Session* SSCache::find(const Application& app, const char* key, const char* client_addr, time_t* timeout)
1389 xmltooling::NDC ndc("find");
1391 StoredSession* session=nullptr;
1394 m_log.debug("searching local cache for session (%s)", key);
1396 map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);
1397 if (i!=m_hashtable.end()) {
1398 // Save off and lock the session.
1399 session = i->second;
1402 m_log.debug("session found locally, validating it for use");
1410 if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
1411 m_log.debug("session not found locally, remoting the search");
1412 // Remote the request.
1413 DDF in("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;
1416 in.addmember("key").string(key);
1417 in.addmember("application_id").string(app.getId());
1418 if (timeout && *timeout) {
1419 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
1420 #ifndef HAVE_GMTIME_R
1421 struct tm* ptime=gmtime(timeout);
1424 struct tm* ptime=gmtime_r(timeout,&res);
1427 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
1428 in.addmember("timeout").string(timebuf);
1432 out=app.getServiceProvider().getListenerService()->send(in);
1433 if (!out.isstruct()) {
1435 m_log.debug("session not found in remote cache");
1439 // Wrap the results in a local entry and save it.
1440 session = new StoredSession(this, out);
1441 // The remote end has handled timeout issues, we handle address and expiration checks.
1450 // We're out of process, so we can search the storage service directly.
1453 throw ConfigurationException("SessionCache lookup requires a StorageService.");
1455 m_log.debug("searching for session (%s)", key);
1460 int ver = m_storage->readText(key, "session", &record, &lastAccess);
1464 m_log.debug("reconstituting session and checking validity");
1466 istringstream in(record);
1469 unsigned long cacheTimeout = getCacheTimeout(app);
1470 lastAccess -= cacheTimeout; // adjusts it back to the last time the record's timestamp was touched
1471 time_t now=time(nullptr);
1473 if (timeout && *timeout > 0 && now - lastAccess >= *timeout) {
1474 m_log.info("session timed out (ID: %s)", key);
1475 auto_ptr<LogoutEvent> logout_event(newLogoutEvent(app));
1476 if (logout_event.get()) {
1477 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_INVALID;
1478 logout_event->m_sessions.push_back(key);
1479 app.getServiceProvider().getTransactionLog()->write(*logout_event);
1482 const char* eid = obj["entity_id"].string();
1485 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1489 throw RetryableProfileException("Your session has expired, and you must re-authenticate.", namedparams(1, "entityID", eid2.c_str()));
1493 // Update storage expiration, if possible.
1495 m_storage->updateContext(key, now + cacheTimeout);
1497 catch (exception& ex) {
1498 m_log.error("failed to update session expiration: %s", ex.what());
1502 // Wrap the results in a local entry and save it.
1503 session = new StoredSession(this, obj);
1504 // We handled timeout issues, still need to handle address and expiration checks.
1507 throw ConfigurationException("SessionCache search requires a StorageService.");
1512 // Lock for writing and repeat the search to avoid duplication.
1514 SharedLock shared(m_lock, false);
1515 if (m_hashtable.count(key)) {
1516 // We're using an existing session entry.
1518 session = m_hashtable[key];
1522 m_hashtable[key]=session;
1528 if (!XMLString::equals(session->getApplicationID(), app.getId())) {
1529 m_log.error("an application (%s) tried to access another application's session", app.getId());
1534 // Verify currency and update the timestamp if indicated by caller.
1536 session->validate(app, client_addr, timeout);
1540 auto_ptr<LogoutEvent> logout_event(newLogoutEvent(app));
1541 if (logout_event.get()) {
1542 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_INVALID;
1543 logout_event->m_session = session;
1544 logout_event->m_sessions.push_back(session->getID());
1545 app.getServiceProvider().getTransactionLog()->write(*logout_event);
1556 Session* SSCache::find(const Application& app, HTTPRequest& request, const char* client_addr, time_t* timeout)
1558 string id = active(app, request);
1562 Session* session = find(app, id.c_str(), client_addr, timeout);
1565 HTTPResponse* response = dynamic_cast<HTTPResponse*>(&request);
1567 if (!m_outboundHeader.empty())
1568 response->setResponseHeader(m_outboundHeader.c_str(), nullptr);
1569 pair<string,const char*> shib_cookie = app.getCookieNameProps("_shibsession_");
1570 string exp(shib_cookie.second);
1571 exp += "; expires=Mon, 01 Jan 2001 00:00:00 GMT";
1572 response->setCookie(shib_cookie.first.c_str(), exp.c_str());
1575 catch (exception&) {
1576 HTTPResponse* response = dynamic_cast<HTTPResponse*>(&request);
1578 if (!m_outboundHeader.empty())
1579 response->setResponseHeader(m_outboundHeader.c_str(), nullptr);
1580 pair<string,const char*> shib_cookie = app.getCookieNameProps("_shibsession_");
1581 string exp(shib_cookie.second);
1582 exp += "; expires=Mon, 01 Jan 2001 00:00:00 GMT";
1583 response->setCookie(shib_cookie.first.c_str(), exp.c_str());
1590 void SSCache::remove(const Application& app, const HTTPRequest& request, HTTPResponse* response)
1593 pair<string,const char*> shib_cookie = app.getCookieNameProps("_shibsession_");
1595 if (!m_inboundHeader.empty())
1596 session_id = request.getHeader(m_inboundHeader.c_str());
1597 if (session_id.empty()) {
1598 const char* c = request.getCookie(shib_cookie.first.c_str());
1603 if (!session_id.empty()) {
1605 if (!m_outboundHeader.empty())
1606 response->setResponseHeader(m_outboundHeader.c_str(), nullptr);
1607 string exp(shib_cookie.second);
1608 exp += "; expires=Mon, 01 Jan 2001 00:00:00 GMT";
1609 response->setCookie(shib_cookie.first.c_str(), exp.c_str());
1611 remove(app, session_id.c_str());
1615 void SSCache::remove(const Application& app, const char* key)
1618 xmltooling::NDC ndc("remove");
1620 // Take care of local copy.
1624 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
1625 // Remove the session from storage directly.
1627 m_storage->deleteContext(key);
1628 m_log.info("removed session (%s)", key);
1630 throw ConfigurationException("SessionCache removal requires a StorageService.");
1634 // Remote the request.
1635 DDF in("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache");
1638 in.addmember("key").string(key);
1639 in.addmember("application_id").string(app.getId());
1641 DDF out = app.getServiceProvider().getListenerService()->send(in);
1646 void SSCache::dormant(const char* key)
1649 xmltooling::NDC ndc("dormant");
1652 m_log.debug("deleting local copy of session (%s)", key);
1654 // lock the cache for writing, which means we know nobody is sitting in find()
1657 // grab the entry from the table
1658 map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);
1659 if (i==m_hashtable.end()) {
1664 // ok, remove the entry and lock it
1665 StoredSession* entry=i->second;
1666 m_hashtable.erase(key);
1672 // we can release the cache entry lock because we know we're not in the cache anymore
1678 void* SSCache::cleanup_fn(void* p)
1681 xmltooling::NDC ndc("cleanup");
1684 SSCache* pcache = reinterpret_cast<SSCache*>(p);
1687 // First, let's block all signals
1688 Thread::mask_all_signals();
1691 auto_ptr<Mutex> mutex(Mutex::create());
1693 // Load our configuration details...
1694 static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);
1695 const XMLCh* tag=pcache->m_root ? pcache->m_root->getAttributeNS(nullptr, cleanupInterval) : nullptr;
1696 int rerun_timer = 900;
1698 rerun_timer = XMLString::parseInt(tag);
1699 if (rerun_timer <= 0)
1705 pcache->m_log.info("cleanup thread started...run every %d secs; timeout after %d secs", rerun_timer, pcache->m_inprocTimeout);
1707 while (!pcache->shutdown) {
1708 pcache->shutdown_wait->timedwait(mutex.get(), rerun_timer);
1709 if (pcache->shutdown)
1712 // Ok, let's run through the cleanup process and clean out
1713 // really old sessions. This is a two-pass process. The
1714 // first pass is done holding a read-lock while we iterate over
1715 // the cache. The second pass doesn't need a lock because
1716 // the 'deletes' will lock the cache.
1718 // Pass 1: iterate over the map and find all entries that have not been
1719 // used in the allotted timeout.
1720 vector<string> stale_keys;
1721 time_t stale = time(nullptr) - pcache->m_inprocTimeout;
1723 pcache->m_log.debug("cleanup thread running");
1725 pcache->m_lock->rdlock();
1726 for (map<string,StoredSession*>::const_iterator i=pcache->m_hashtable.begin(); i!=pcache->m_hashtable.end(); ++i) {
1727 // If the last access was BEFORE the stale timeout...
1729 time_t last=i->second->getLastAccess();
1730 i->second->unlock();
1732 stale_keys.push_back(i->first);
1734 pcache->m_lock->unlock();
1736 if (!stale_keys.empty()) {
1737 pcache->m_log.info("purging %d old sessions", stale_keys.size());
1739 // Pass 2: walk through the list of stale entries and remove them from the cache
1740 for (vector<string>::const_iterator j = stale_keys.begin(); j != stale_keys.end(); ++j)
1741 pcache->dormant(j->c_str());
1744 pcache->m_log.debug("cleanup thread completed");
1747 pcache->m_log.info("cleanup thread exiting");
1755 void SSCache::receive(DDF& in, ostream& out)
1758 xmltooling::NDC ndc("receive");
1760 const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());
1762 throw ListenerException("Application not found, check configuration?");
1764 if (!strcmp(in.name(),"find::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1765 const char* key=in["key"].string();
1767 throw ListenerException("Required parameters missing for session lookup.");
1769 // Do an unversioned read.
1772 if (!m_storage->readText(key, "session", &record, &lastAccess)) {
1774 DDFJanitor jan(ret);
1779 // Adjust for expiration to recover last access time and check timeout.
1780 unsigned long cacheTimeout = getCacheTimeout(*app);
1781 lastAccess -= cacheTimeout;
1782 time_t now=time(nullptr);
1784 // See if we need to check for a timeout.
1785 if (in["timeout"].string()) {
1787 auto_ptr_XMLCh dt(in["timeout"].string());
1788 DateTime dtobj(dt.get());
1789 dtobj.parseDateTime();
1790 timeout = dtobj.getEpoch();
1792 if (timeout > 0 && now - lastAccess >= timeout) {
1793 m_log.info("session timed out (ID: %s)", key);
1794 auto_ptr<LogoutEvent> logout_event(newLogoutEvent(*app));
1795 if (logout_event.get()) {
1796 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_INVALID;
1797 logout_event->m_sessions.push_back(key);
1798 app->getServiceProvider().getTransactionLog()->write(*logout_event);
1801 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1804 // Update storage expiration, if possible.
1806 m_storage->updateContext(key, now + cacheTimeout);
1808 catch (exception& ex) {
1809 m_log.error("failed to update session expiration: %s", ex.what());
1813 // Send the record back.
1816 else if (!strcmp(in.name(),"touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1817 const char* key=in["key"].string();
1819 throw ListenerException("Required parameters missing for session check.");
1821 // Do a versioned read.
1824 int curver = in["version"].integer();
1825 int ver = m_storage->readText(key, "session", &record, &lastAccess, curver);
1827 m_log.warn("unsuccessful versioned read of session (ID: %s), caches out of sync?", key);
1828 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1831 // Adjust for expiration to recover last access time and check timeout.
1832 unsigned long cacheTimeout = getCacheTimeout(*app);
1833 lastAccess -= cacheTimeout;
1834 time_t now=time(nullptr);
1836 // See if we need to check for a timeout.
1838 auto_ptr_XMLCh dt(in["timeout"].string());
1840 DateTime dtobj(dt.get());
1841 dtobj.parseDateTime();
1842 timeout = dtobj.getEpoch();
1845 if (timeout > 0 && now - lastAccess >= timeout) {
1846 m_log.info("session timed out (ID: %s)", key);
1847 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1850 // Update storage expiration, if possible.
1852 m_storage->updateContext(key, now + cacheTimeout);
1854 catch (exception& ex) {
1855 m_log.error("failed to update session expiration: %s", ex.what());
1859 // Send the record back.
1864 DDFJanitor jan(ret);
1868 else if (!strcmp(in.name(),"remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1869 const char* key=in["key"].string();
1871 throw ListenerException("Required parameter missing for session removal.");
1875 DDFJanitor jan(ret);