X-Git-Url: http://www.project-moonshot.org/gitweb/?a=blobdiff_plain;f=shibsp%2Fimpl%2FStorageServiceSessionCache.cpp;h=3641be999b73ecfd90ace64eb5fa888bb63faeb9;hb=a0af8ed86ab481af4605daf5cf837fdaa4544bdd;hp=38d59893754588414f6affab6c210e9f5729f041;hpb=7b7c8bd1f79f1ba7650f185970d0864a157b4818;p=shibboleth%2Fsp.git diff --git a/shibsp/impl/StorageServiceSessionCache.cpp b/shibsp/impl/StorageServiceSessionCache.cpp index 38d5989..3641be9 100644 --- a/shibsp/impl/StorageServiceSessionCache.cpp +++ b/shibsp/impl/StorageServiceSessionCache.cpp @@ -1,6 +1,6 @@ /* - * Copyright 2001-2007 Internet2 - * + * Copyright 2001-2010 Internet2 + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -16,9 +16,9 @@ /** * StorageServiceSessionCache.cpp - * + * * StorageService-based SessionCache implementation. - * + * * Instead of optimizing this plugin with a buffering scheme that keeps objects around * and avoids extra parsing steps, I'm assuming that systems that require such can * layer their own cache plugin on top of this version either by delegating to it @@ -32,56 +32,219 @@ #include "Application.h" #include "exceptions.h" #include "ServiceProvider.h" -#include "SessionCache.h" +#include "SessionCacheEx.h" #include "TransactionLog.h" #include "attribute/Attribute.h" +#include "handler/RemotedHandler.h" #include "remoting/ListenerService.h" #include "util/SPConstants.h" -#include +#include +#include +#include +#include #include -#include +#include +#include #include #include -using namespace shibsp; +#ifndef SHIBSP_LITE +# include +# include +# include +# include +# include +# include using namespace opensaml::saml2md; +#else +# include +# include +#endif + +using namespace shibsp; using namespace opensaml; using namespace xmltooling; using namespace std; namespace shibsp { - class SSCache; + class StoredSession; + class SSCache : public SessionCacheEx +#ifndef SHIBSP_LITE + ,public virtual Remoted +#endif + { + public: + SSCache(const DOMElement* e); + ~SSCache(); + +#ifndef SHIBSP_LITE + void receive(DDF& in, ostream& out); + + void insert( + const Application& app, + const HTTPRequest& httpRequest, + HTTPResponse& httpResponse, + time_t expires, + const saml2md::EntityDescriptor* issuer=nullptr, + const XMLCh* protocol=nullptr, + const saml2::NameID* nameid=nullptr, + const XMLCh* authn_instant=nullptr, + const XMLCh* session_index=nullptr, + const XMLCh* authncontext_class=nullptr, + const XMLCh* authncontext_decl=nullptr, + const vector* tokens=nullptr, + const vector* attributes=nullptr + ); + vector::size_type logout( + const Application& app, + const saml2md::EntityDescriptor* issuer, + const saml2::NameID& nameid, + const set* indexes, + time_t expires, + vector& sessions + ); + bool matches( + const Application& app, + const HTTPRequest& request, + const saml2md::EntityDescriptor* issuer, + const saml2::NameID& nameid, + const set* indexes + ); +#endif + Session* find(const Application& app, const char* key, const char* client_addr=nullptr, time_t* timeout=nullptr); + void remove(const Application& app, const char* key); + void test(); + + string active(const Application& app, const HTTPRequest& request) { + if (!m_inboundHeader.empty()) { + string session_id = request.getHeader(m_inboundHeader.c_str()); + if (!session_id.empty()) + return session_id; + } + pair shib_cookie = app.getCookieNameProps("_shibsession_"); + const char* session_id = request.getCookie(shib_cookie.first.c_str()); + return (session_id ? session_id : ""); + } + + Session* find(const Application& app, const HTTPRequest& request, const char* client_addr=nullptr, time_t* timeout=nullptr) { + string id = active(app, request); + if (!id.empty()) + return find(app, id.c_str(), client_addr, timeout); + return nullptr; + } + + Session* find(const Application& app, HTTPRequest& request, const char* client_addr=nullptr, time_t* timeout=nullptr); + void remove(const Application& app, const HTTPRequest& request, HTTPResponse* response=nullptr); + + unsigned long getCacheTimeout(const Application& app) { + // Computes offset for adjusting expiration of sessions. + // This can either be static, or dynamic based on the per-app session timeout. + if (m_cacheTimeout) + return m_cacheTimeout; + pair timeout; + const PropertySet* props = app.getPropertySet("Sessions"); + if (props) { + timeout = props->getUnsignedInt("timeout"); + if (!timeout.first) + timeout.second = 3600; + } + return timeout.second + m_cacheAllowance; + } + + Category& m_log; + bool inproc; +#ifndef SHIBSP_LITE + StorageService* m_storage; + StorageService* m_storage_lite; +#endif + + private: +#ifndef SHIBSP_LITE + // maintain back-mappings of NameID/SessionIndex -> session key + void insert(const char* key, time_t expires, const char* name, const char* index); + bool stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const; + + bool m_cacheAssertions; +#endif + const DOMElement* m_root; // Only valid during initialization + unsigned long m_inprocTimeout,m_cacheTimeout,m_cacheAllowance; + string m_inboundHeader,m_outboundHeader; + + // inproc means we buffer sessions in memory + RWLock* m_lock; + map m_hashtable; + + // management of buffered sessions + void dormant(const char* key); + static void* cleanup_fn(void*); + + bool shutdown; + CondWait* shutdown_wait; + Thread* cleanup_thread; + }; + class StoredSession : public virtual Session { public: - StoredSession(SSCache* cache, DDF& obj) : m_obj(obj), m_nameid(NULL), m_cache(cache) { + StoredSession(SSCache* cache, DDF& obj) : m_obj(obj), +#ifndef SHIBSP_LITE + m_nameid(nullptr), +#endif + m_cache(cache), m_expires(0), m_lastAccess(time(nullptr)), m_lock(nullptr) { + auto_ptr_XMLCh exp(m_obj["expires"].string()); + if (exp.get()) { + DateTime iso(exp.get()); + iso.parseDateTime(); + m_expires = iso.getEpoch(); + } + +#ifndef SHIBSP_LITE const char* nameid = obj["nameid"].string(); if (nameid) { // Parse and bind the document into an XMLObject. istringstream instr(nameid); - DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); + DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); XercesJanitor janitor(doc); auto_ptr n(saml2::NameIDBuilder::buildNameID()); n->unmarshall(doc->getDocumentElement(), true); janitor.release(); m_nameid = n.release(); } +#endif + if (cache->inproc) + m_lock = Mutex::create(); + } + + ~StoredSession() { + delete m_lock; + m_obj.destroy(); + for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup()); +#ifndef SHIBSP_LITE + delete m_nameid; + for_each(m_tokens.begin(), m_tokens.end(), cleanup_pair()); +#endif } - - ~StoredSession(); - + Lockable* lock() { + if (m_lock) + m_lock->lock(); return this; } void unlock() { - delete this; + if (m_lock) + m_lock->unlock(); + else + delete this; } - + const char* getID() const { return m_obj.name(); } + const char* getApplicationID() const { + return m_obj["application_id"].string(); + } const char* getClientAddress() const { return m_obj["client_addr"].string(); } @@ -94,9 +257,11 @@ namespace shibsp { const char* getAuthnInstant() const { return m_obj["authn_instant"].string(); } - const opensaml::saml2::NameID* getNameID() const { +#ifndef SHIBSP_LITE + const saml2::NameID* getNameID() const { return m_nameid; } +#endif const char* getSessionIndex() const { return m_obj["session_index"].string(); } @@ -106,11 +271,23 @@ namespace shibsp { const char* getAuthnContextDeclRef() const { return m_obj["authncontext_decl"].string(); } - const multimap& getAttributes() const { + const vector& getAttributes() const { if (m_attributes.empty()) unmarshallAttributes(); return m_attributes; } + const multimap& getIndexedAttributes() const { + if (m_attributeIndex.empty()) { + if (m_attributes.empty()) + unmarshallAttributes(); + for (vector::const_iterator a = m_attributes.begin(); a != m_attributes.end(); ++a) { + const vector& aliases = (*a)->getAliases(); + for (vector::const_iterator alias = aliases.begin(); alias != aliases.end(); ++alias) + m_attributeIndex.insert(multimap::value_type(*alias, *a)); + } + } + return m_attributeIndex; + } const vector& getAssertionIDs() const { if (m_ids.empty()) { DDF ids = m_obj["assertions"]; @@ -122,86 +299,57 @@ namespace shibsp { } return m_ids; } - + + void validate(const Application& application, const char* client_addr, time_t* timeout); + +#ifndef SHIBSP_LITE void addAttributes(const vector& attributes); const Assertion* getAssertion(const char* id) const; void addAssertion(Assertion* assertion); +#endif + + time_t getExpiration() const { return m_expires; } + time_t getLastAccess() const { return m_lastAccess; } private: void unmarshallAttributes() const; DDF m_obj; +#ifndef SHIBSP_LITE saml2::NameID* m_nameid; - mutable multimap m_attributes; - mutable vector m_ids; mutable map m_tokens; - SSCache* m_cache; - }; - - class SSCache : public SessionCache, public virtual Remoted - { - public: - SSCache(const DOMElement* e); - ~SSCache(); - - void receive(DDF& in, ostream& out); - - string insert( - time_t expires, - const Application& application, - const char* client_addr=NULL, - const saml2md::EntityDescriptor* issuer=NULL, - const XMLCh* protocol=NULL, - const saml2::NameID* nameid=NULL, - const XMLCh* authn_instant=NULL, - const XMLCh* session_index=NULL, - const XMLCh* authncontext_class=NULL, - const XMLCh* authncontext_decl=NULL, - const vector* tokens=NULL, - const multimap* attributes=NULL - ); - Session* find(const char* key, const Application& application, const char* client_addr=NULL, time_t* timeout=NULL); - void remove(const char* key, const Application& application); - bool matches( - const char* key, - const saml2md::EntityDescriptor* issuer, - const saml2::NameID& nameid, - const set* indexes, - const Application& application - ); - vector::size_type logout( - const saml2md::EntityDescriptor* issuer, - const saml2::NameID& nameid, - const set* indexes, - time_t expires, - const Application& application, - vector& sessions - ); - - Category& m_log; - StorageService* m_storage; - - private: - // maintain back-mappings of NameID/SessionIndex -> session key - void insert(const char* key, time_t expires, const char* name, const char* index); +#endif + mutable vector m_attributes; + mutable multimap m_attributeIndex; + mutable vector m_ids; - bool stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const; + SSCache* m_cache; + time_t m_expires,m_lastAccess; + Mutex* m_lock; }; SessionCache* SHIBSP_DLLLOCAL StorageServiceCacheFactory(const DOMElement* const & e) { return new SSCache(e); } +} - static const XMLCh _StorageService[] = UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e); +Session* SessionCache::find(const Application& application, HTTPRequest& request, const char* client_addr, time_t* timeout) +{ + return find(application, const_cast(request), client_addr, timeout); } -StoredSession::~StoredSession() +void SHIBSP_API shibsp::registerSessionCaches() +{ + SPConfig::getConfig().SessionCacheManager.registerFactory(STORAGESERVICE_SESSION_CACHE, StorageServiceCacheFactory); +} + +Session::Session() +{ +} + +Session::~Session() { - m_obj.destroy(); - delete m_nameid; - for_each(m_attributes.begin(), m_attributes.end(), cleanup_pair()); - for_each(m_tokens.begin(), m_tokens.end(), cleanup_pair()); } void StoredSession::unmarshallAttributes() const @@ -212,7 +360,7 @@ void StoredSession::unmarshallAttributes() const while (!attr.isnull()) { try { attribute = Attribute::unmarshall(attr); - m_attributes.insert(make_pair(attribute->getId(), attribute)); + m_attributes.push_back(attribute); if (m_cache->m_log.isDebugEnabled()) m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s", attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : ""); @@ -225,14 +373,135 @@ void StoredSession::unmarshallAttributes() const } } +void StoredSession::validate(const Application& app, const char* client_addr, time_t* timeout) +{ + time_t now = time(nullptr); + + // Basic expiration? + if (m_expires > 0) { + if (now > m_expires) { + m_cache->m_log.info("session expired (ID: %s)", getID()); + throw RetryableProfileException("Your session has expired, and you must re-authenticate."); + } + } + + // Address check? + if (client_addr) { + if (m_cache->m_log.isDebugEnabled()) + m_cache->m_log.debug("comparing client address %s against %s", client_addr, getClientAddress()); + if (!XMLString::equals(getClientAddress(),client_addr)) { + m_cache->m_log.warn("client address mismatch"); + throw RetryableProfileException( + "Your IP address ($1) does not match the address recorded at the time the session was established.", + params(1,client_addr) + ); + } + } + + if (!timeout) + return; + + if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) { + DDF in("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out; + DDFJanitor jin(in); + in.structure(); + in.addmember("key").string(getID()); + in.addmember("version").integer(m_obj["version"].integer()); + if (*timeout) { + // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps. +#ifndef HAVE_GMTIME_R + struct tm* ptime=gmtime(timeout); +#else + struct tm res; + struct tm* ptime=gmtime_r(timeout,&res); +#endif + char timebuf[32]; + strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime); + in.addmember("timeout").string(timebuf); + } + + try { + out=app.getServiceProvider().getListenerService()->send(in); + } + catch (...) { + out.destroy(); + throw; + } + + if (out.isstruct()) { + // We got an updated record back. + m_ids.clear(); + for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup()); + m_attributes.clear(); + m_attributeIndex.clear(); + m_obj.destroy(); + m_obj = out; + } + } + else { +#ifndef SHIBSP_LITE + if (!m_cache->m_storage) + throw ConfigurationException("Session touch requires a StorageService."); + + // Do a versioned read. + string record; + time_t lastAccess; + int curver = m_obj["version"].integer(); + int ver = m_cache->m_storage->readText(getID(), "session", &record, &lastAccess, curver); + if (ver == 0) { + m_cache->m_log.warn("unsuccessful versioned read of session (ID: %s), cache out of sync?", getID()); + throw RetryableProfileException("Your session has expired, and you must re-authenticate."); + } + + // Adjust for expiration to recover last access time and check timeout. + unsigned long cacheTimeout = m_cache->getCacheTimeout(app); + lastAccess -= cacheTimeout; + if (*timeout > 0 && now - lastAccess >= *timeout) { + m_cache->m_log.info("session timed out (ID: %s)", getID()); + throw RetryableProfileException("Your session has expired, and you must re-authenticate."); + } + + // Update storage expiration, if possible. + try { + m_cache->m_storage->updateContext(getID(), now + cacheTimeout); + } + catch (exception& ex) { + m_cache->m_log.error("failed to update session expiration: %s", ex.what()); + } + + if (ver > curver) { + // We got an updated record back. + DDF newobj; + istringstream in(record); + in >> newobj; + m_ids.clear(); + for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup()); + m_attributes.clear(); + m_attributeIndex.clear(); + m_obj.destroy(); + m_obj = newobj; + } +#else + throw ConfigurationException("Session touch requires a StorageService."); +#endif + } + + m_lastAccess = now; +} + +#ifndef SHIBSP_LITE + void StoredSession::addAttributes(const vector& attributes) { #ifdef _DEBUG xmltooling::NDC ndc("addAttributes"); #endif + if (!m_cache->m_storage) + throw ConfigurationException("Session modification requires a StorageService."); + m_cache->m_log.debug("adding attributes to session (%s)", getID()); - + int ver; do { DDF attr; @@ -243,13 +512,13 @@ void StoredSession::addAttributes(const vector& attributes) attr = (*a)->marshall(); attrs.add(attr); } - + // Tentatively increment the version. m_obj["version"].integer(m_obj["version"].integer()+1); - + ostringstream str; str << m_obj; - string record(str.str()); + string record(str.str()); try { ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1); @@ -259,7 +528,7 @@ void StoredSession::addAttributes(const vector& attributes) m_obj["version"].integer(m_obj["version"].integer()-1); vector::size_type count = attributes.size(); while (count--) - attrs.last().destroy(); + attrs.last().destroy(); throw; } @@ -268,7 +537,7 @@ void StoredSession::addAttributes(const vector& attributes) m_obj["version"].integer(m_obj["version"].integer()-1); vector::size_type count = attributes.size(); while (count--) - attrs.last().destroy(); + attrs.last().destroy(); } if (!ver) { // Fatal problem with update. @@ -277,20 +546,21 @@ void StoredSession::addAttributes(const vector& attributes) else if (ver < 0) { // Out of sync. m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy..."); - ver = m_cache->m_storage->readText(getID(), "session", &record, NULL); + ver = m_cache->m_storage->readText(getID(), "session", &record, nullptr); if (!ver) { m_cache->m_log.error("readText failed on StorageService for session (%s)", getID()); throw IOException("Unable to read back stored session."); } - + // Reset object. DDF newobj; istringstream in(record); in >> newobj; m_ids.clear(); - for_each(m_attributes.begin(), m_attributes.end(), cleanup_const_pair()); + for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup()); m_attributes.clear(); + m_attributeIndex.clear(); newobj["version"].integer(ver); m_obj.destroy(); m_obj = newobj; @@ -317,21 +587,24 @@ void StoredSession::addAttributes(const vector& attributes) const Assertion* StoredSession::getAssertion(const char* id) const { + if (!m_cache->m_storage) + throw ConfigurationException("Assertion retrieval requires a StorageService."); + map::const_iterator i = m_tokens.find(id); if (i!=m_tokens.end()) return i->second; - + string tokenstr; - if (!m_cache->m_storage->readText(getID(), id, &tokenstr, NULL)) + if (!m_cache->m_storage->readText(getID(), id, &tokenstr, nullptr)) throw FatalProfileException("Assertion not found in cache."); // Parse and bind the document into an XMLObject. istringstream instr(tokenstr); - DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); + DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); XercesJanitor janitor(doc); auto_ptr xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true)); janitor.release(); - + Assertion* token = dynamic_cast(xmlObject.get()); if (!token) throw FatalProfileException("Request for cached assertion returned an unknown object type."); @@ -347,7 +620,10 @@ void StoredSession::addAssertion(Assertion* assertion) #ifdef _DEBUG xmltooling::NDC ndc("addAssertion"); #endif - + + if (!m_cache->m_storage) + throw ConfigurationException("Session modification requires a StorageService."); + if (!assertion) throw FatalProfileException("Unknown object type passed to session for storage."); @@ -356,25 +632,25 @@ void StoredSession::addAssertion(Assertion* assertion) m_cache->m_log.debug("adding assertion (%s) to session (%s)", id.get(), getID()); time_t exp; - if (!m_cache->m_storage->readText(getID(), "session", NULL, &exp)) + if (!m_cache->m_storage->readText(getID(), "session", nullptr, &exp)) throw IOException("Unable to load expiration time for stored session."); ostringstream tokenstr; tokenstr << *assertion; if (!m_cache->m_storage->createText(getID(), id.get(), tokenstr.str().c_str(), exp)) throw IOException("Attempted to insert duplicate assertion ID into session."); - + int ver; do { - DDF token = DDF(NULL).string(id.get()); + DDF token = DDF(nullptr).string(id.get()); m_obj["assertions"].add(token); // Tentatively increment the version. m_obj["version"].integer(m_obj["version"].integer()+1); - + ostringstream str; str << m_obj; - string record(str.str()); + string record(str.str()); try { ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1); @@ -389,7 +665,7 @@ void StoredSession::addAssertion(Assertion* assertion) if (ver <= 0) { token.destroy(); m_obj["version"].integer(m_obj["version"].integer()-1); - } + } if (!ver) { // Fatal problem with update. m_cache->m_log.error("updateText failed on StorageService for session (%s)", getID()); @@ -399,25 +675,26 @@ void StoredSession::addAssertion(Assertion* assertion) else if (ver < 0) { // Out of sync. m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy..."); - ver = m_cache->m_storage->readText(getID(), "session", &record, NULL); + ver = m_cache->m_storage->readText(getID(), "session", &record, nullptr); if (!ver) { m_cache->m_log.error("readText failed on StorageService for session (%s)", getID()); m_cache->m_storage->deleteText(getID(), id.get()); throw IOException("Unable to read back stored session."); } - + // Reset object. DDF newobj; istringstream in(record); in >> newobj; m_ids.clear(); - for_each(m_attributes.begin(), m_attributes.end(), cleanup_const_pair()); + for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup()); m_attributes.clear(); + m_attributeIndex.clear(); newobj["version"].integer(ver); m_obj.destroy(); m_obj = newobj; - + ver = -1; } } while (ver < 0); // negative indicates a sync issue so we retry @@ -433,42 +710,144 @@ void StoredSession::addAssertion(Assertion* assertion) ); } +#endif + +SessionCache::SessionCache() +{ +} + +SessionCache::~SessionCache() +{ +} + +SessionCacheEx::SessionCacheEx() +{ +} + +SessionCacheEx::~SessionCacheEx() +{ +} + SSCache::SSCache(const DOMElement* e) - : SessionCache(e), m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), m_storage(NULL) + : m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), inproc(true), +#ifndef SHIBSP_LITE + m_storage(nullptr), m_storage_lite(nullptr), m_cacheAssertions(true), +#endif + m_root(e), m_inprocTimeout(900), m_cacheTimeout(0), m_cacheAllowance(0), + m_lock(nullptr), shutdown(false), shutdown_wait(nullptr), cleanup_thread(nullptr) { SPConfig& conf = SPConfig::getConfig(); - const XMLCh* tag = e ? e->getAttributeNS(NULL,_StorageService) : NULL; - if (tag && *tag) { - auto_ptr_char ssid(tag); - m_storage = conf.getServiceProvider()->getStorageService(ssid.get()); - if (m_storage) - m_log.info("bound to StorageService (%s)", ssid.get()); - else - throw ConfigurationException("SessionCache unable to locate StorageService, check configuration."); + inproc = conf.isEnabled(SPConfig::InProcess); + + static const XMLCh cacheAllowance[] = UNICODE_LITERAL_14(c,a,c,h,e,A,l,l,o,w,a,n,c,e); + static const XMLCh cacheAssertions[] = UNICODE_LITERAL_15(c,a,c,h,e,A,s,s,e,r,t,i,o,n,s); + static const XMLCh cacheTimeout[] = UNICODE_LITERAL_12(c,a,c,h,e,T,i,m,e,o,u,t); + static const XMLCh inprocTimeout[] = UNICODE_LITERAL_13(i,n,p,r,o,c,T,i,m,e,o,u,t); + static const XMLCh inboundHeader[] = UNICODE_LITERAL_13(i,n,b,o,u,n,d,H,e,a,d,e,r); + static const XMLCh outboundHeader[] = UNICODE_LITERAL_14(o,u,t,b,o,u,n,d,H,e,a,d,e,r); + static const XMLCh _StorageService[] = UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e); + 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); + + m_cacheTimeout = XMLHelper::getAttrInt(e, 0, cacheTimeout); + m_cacheAllowance = XMLHelper::getAttrInt(e, 0, cacheAllowance); + if (inproc) + m_inprocTimeout = XMLHelper::getAttrInt(e, 900, inprocTimeout); + m_inboundHeader = XMLHelper::getAttrString(e, nullptr, inboundHeader); + if (!m_inboundHeader.empty()) + RemotedHandler::addRemotedHeader(m_inboundHeader.c_str()); + m_outboundHeader = XMLHelper::getAttrString(e, nullptr, outboundHeader); + +#ifndef SHIBSP_LITE + if (conf.isEnabled(SPConfig::OutOfProcess)) { + string ssid(XMLHelper::getAttrString(e, nullptr, _StorageService)); + if (!ssid.empty()) { + m_storage = conf.getServiceProvider()->getStorageService(ssid.c_str()); + if (m_storage) + m_log.info("bound to StorageService (%s)", ssid.c_str()); + else + m_log.warn("specified StorageService (%s) not found", ssid.c_str()); + } + if (!m_storage) { + m_storage = conf.getServiceProvider()->getStorageService(nullptr); + if (m_storage) + m_log.info("bound to arbitrary StorageService"); + else + throw ConfigurationException("SessionCache unable to locate StorageService, check configuration."); + } + + ssid = XMLHelper::getAttrString(e, nullptr, _StorageServiceLite); + if (!ssid.empty()) { + m_storage_lite = conf.getServiceProvider()->getStorageService(ssid.c_str()); + if (m_storage_lite) + m_log.info("bound to 'lite' StorageService (%s)", ssid.c_str()); + else + m_log.warn("specified 'lite' StorageService (%s) not found", ssid.c_str()); + } + if (!m_storage_lite) { + m_log.info("StorageService for 'lite' use not set, using standard StorageService"); + m_storage_lite = m_storage; + } + + m_cacheAssertions = XMLHelper::getAttrBool(e, true, cacheAssertions); } +#endif ListenerService* listener=conf.getServiceProvider()->getListenerService(false); - if (listener && conf.isEnabled(SPConfig::OutOfProcess)) { - listener->regListener("find::"REMOTED_SESSION_CACHE"::SessionCache",this); - listener->regListener("remove::"REMOTED_SESSION_CACHE"::SessionCache",this); - listener->regListener("touch::"REMOTED_SESSION_CACHE"::SessionCache",this); - listener->regListener("getAssertion::"REMOTED_SESSION_CACHE"::SessionCache",this); + if (inproc) { + if (!conf.isEnabled(SPConfig::OutOfProcess) && !listener) + throw ConfigurationException("SessionCache requires a ListenerService, but none available."); + m_lock = RWLock::create(); + shutdown_wait = CondWait::create(); + cleanup_thread = Thread::create(&cleanup_fn, this); } +#ifndef SHIBSP_LITE else { - m_log.info("no ListenerService available, cache remoting disabled"); + if (listener && conf.isEnabled(SPConfig::OutOfProcess)) { + listener->regListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this); + listener->regListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this); + listener->regListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this); + } + else { + m_log.info("no ListenerService available, cache remoting disabled"); + } } +#endif } SSCache::~SSCache() { - SPConfig& conf = SPConfig::getConfig(); - ListenerService* listener=conf.getServiceProvider()->getListenerService(false); - if (listener && conf.isEnabled(SPConfig::OutOfProcess)) { - listener->unregListener("find::"REMOTED_SESSION_CACHE"::SessionCache",this); - listener->unregListener("remove::"REMOTED_SESSION_CACHE"::SessionCache",this); - listener->unregListener("touch::"REMOTED_SESSION_CACHE"::SessionCache",this); - listener->unregListener("getAssertion::"REMOTED_SESSION_CACHE"::SessionCache",this); + if (inproc) { + // Shut down the cleanup thread and let it know... + shutdown = true; + shutdown_wait->signal(); + cleanup_thread->join(nullptr); + + for_each(m_hashtable.begin(),m_hashtable.end(),cleanup_pair()); + delete m_lock; + + delete cleanup_thread; + delete shutdown_wait; } +#ifndef SHIBSP_LITE + else { + SPConfig& conf = SPConfig::getConfig(); + ListenerService* listener=conf.getServiceProvider()->getListenerService(false); + if (listener && conf.isEnabled(SPConfig::OutOfProcess)) { + listener->unregListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this); + listener->unregListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this); + listener->unregListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this); + } + } +#endif +} + +#ifndef SHIBSP_LITE + +void SSCache::test() +{ + auto_ptr_char temp(SAMLConfig::getConfig().generateIdentifier()); + m_storage->createString("SessionCacheTest", temp.get(), "Test", time(nullptr) + 60); + m_storage->deleteString("SessionCacheTest", temp.get()); } void SSCache::insert(const char* key, time_t expires, const char* name, const char* index) @@ -485,7 +864,7 @@ void SSCache::insert(const char* key, time_t expires, const char* name, const ch // Since we can't guarantee uniqueness, check for an existing record. string record; time_t recordexp; - int ver = m_storage->readText("NameID", name, &record, &recordexp); + int ver = m_storage_lite->readText("NameID", name, &record, &recordexp); if (ver > 0) { // Existing record, so we need to unmarshall it. istringstream in(record); @@ -493,7 +872,7 @@ void SSCache::insert(const char* key, time_t expires, const char* name, const ch } else { // New record. - obj.structure(); + obj = DDF(nullptr).structure(); } if (!index || !*index) @@ -501,7 +880,7 @@ void SSCache::insert(const char* key, time_t expires, const char* name, const ch DDF sessions = obj.addmember(index); if (!sessions.islist()) sessions.list(); - DDF session = DDF(NULL).string(key); + DDF session = DDF(nullptr).string(key); sessions.add(session); // Remarshall the record. @@ -510,22 +889,23 @@ void SSCache::insert(const char* key, time_t expires, const char* name, const ch // Try and store it back... if (ver > 0) { - ver = m_storage->updateText("NameID", name, out.str().c_str(), max(expires, recordexp), ver); + ver = m_storage_lite->updateText("NameID", name, out.str().c_str(), max(expires, recordexp), ver); if (ver <= 0) { // Out of sync, or went missing, so retry. return insert(key, expires, name, index); } } - else if (!m_storage->createText("NameID", name, out.str().c_str(), expires)) { + else if (!m_storage_lite->createText("NameID", name, out.str().c_str(), expires)) { // Hit a dup, so just retry, hopefully hitting the other branch. return insert(key, expires, name, index); } } -string SSCache::insert( +void SSCache::insert( + const Application& app, + const HTTPRequest& httpRequest, + HTTPResponse& httpResponse, time_t expires, - const Application& application, - const char* client_addr, const saml2md::EntityDescriptor* issuer, const XMLCh* protocol, const saml2::NameID* nameid, @@ -534,35 +914,38 @@ string SSCache::insert( const XMLCh* authncontext_class, const XMLCh* authncontext_decl, const vector* tokens, - const multimap* attributes + const vector* attributes ) { #ifdef _DEBUG xmltooling::NDC ndc("insert"); #endif + if (!m_storage) + throw ConfigurationException("SessionCache insertion requires a StorageService."); m_log.debug("creating new session"); - time_t now = time(NULL); + time_t now = time(nullptr); auto_ptr_char index(session_index); - auto_ptr_char entity_id(issuer ? issuer->getEntityID() : NULL); - auto_ptr_char name(nameid ? nameid->getName() : NULL); + auto_ptr_char entity_id(issuer ? issuer->getEntityID() : nullptr); + auto_ptr_char name(nameid ? nameid->getName() : nullptr); if (nameid) { // Check for a pending logout. - if (strlen(name.get()) > 255) - const_cast(name.get())[255] = 0; + char namebuf[256]; + strncpy(namebuf, name.get(), 255); + namebuf[255] = 0; string pending; - int ver = m_storage->readText("Logout", name.get(), &pending); + int ver = m_storage_lite->readText("Logout", namebuf, &pending); if (ver > 0) { DDF pendobj; DDFJanitor jpend(pendobj); istringstream pstr(pending); pstr >> pendobj; // IdP.SP.index contains logout expiration, if any. - DDF deadmenwalking = pendobj[issuer ? entity_id.get() : "_shibnull"][application.getString("entityID").second]; + DDF deadmenwalking = pendobj[issuer ? entity_id.get() : "_shibnull"][app.getRelyingParty(issuer)->getString("entityID").second]; const char* logexpstr = deadmenwalking[session_index ? index.get() : "_shibnull"].string(); - if (!logexpstr && session_index) // we tried an exact session match, now try for NULL + if (!logexpstr && session_index) // we tried an exact session match, now try for nullptr logexpstr = deadmenwalking["_shibnull"].string(); if (logexpstr) { auto_ptr_XMLCh dt(logexpstr); @@ -579,8 +962,9 @@ string SSCache::insert( // Store session properties in DDF. DDF obj = DDF(key.get()).structure(); + DDFJanitor entryobj(obj); obj.addmember("version").integer(1); - obj.addmember("application_id").string(application.getId()); + obj.addmember("application_id").string(app.getId()); // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps. #ifndef HAVE_GMTIME_R @@ -593,8 +977,7 @@ string SSCache::insert( strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime); obj.addmember("expires").string(timebuf); - if (client_addr) - obj.addmember("client_addr").string(client_addr); + obj.addmember("client_addr").string(httpRequest.getRemoteAddr().c_str()); if (issuer) obj.addmember("entity_id").string(entity_id.get()); if (protocol) { @@ -622,31 +1005,32 @@ string SSCache::insert( obj.addmember("nameid").string(namestr.str().c_str()); } - if (tokens) { + if (tokens && m_cacheAssertions) { obj.addmember("assertions").list(); for (vector::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) { auto_ptr_char tokenid((*t)->getID()); - DDF tokid = DDF(NULL).string(tokenid.get()); + DDF tokid = DDF(nullptr).string(tokenid.get()); obj["assertions"].add(tokid); } } - + if (attributes) { DDF attr; DDF attrlist = obj.addmember("attributes").list(); - for (multimap::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) { - attr = a->second->marshall(); + for (vector::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) { + attr = (*a)->marshall(); attrlist.add(attr); } } - + ostringstream record; record << obj; - + m_log.debug("storing new session..."); - if (!m_storage->createText(key.get(), "session", record.str().c_str(), now + m_cacheTimeout)) + unsigned long cacheTimeout = getCacheTimeout(app); + if (!m_storage->createText(key.get(), "session", record.str().c_str(), now + cacheTimeout)) throw FatalProfileException("Attempted to create a session with a duplicate key."); - + // Store the reverse mapping for logout. try { if (nameid) @@ -656,13 +1040,13 @@ string SSCache::insert( m_log.error("error storing back mapping of NameID for logout: %s", ex.what()); } - if (tokens) { + if (tokens && m_cacheAssertions) { try { for (vector::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) { ostringstream tokenstr; tokenstr << *(*t); auto_ptr_char tokenid((*t)->getID()); - if (!m_storage->createText(key.get(), tokenid.get(), tokenstr.str().c_str(), now + m_cacheTimeout)) + if (!m_storage->createText(key.get(), tokenid.get(), tokenstr.str().c_str(), now + cacheTimeout)) throw IOException("duplicate assertion ID ($1)", params(1, tokenid.get())); } } @@ -672,184 +1056,105 @@ string SSCache::insert( } const char* pid = obj["entity_id"].string(); - m_log.info("new session created: SessionID (%s) IdP (%s) Address (%s)", key.get(), pid ? pid : "none", client_addr); + const char* prot = obj["protocol"].string(); + m_log.info("new session created: ID (%s) IdP (%s) Protocol(%s) Address (%s)", + key.get(), pid ? pid : "none", prot ? prot : "none", httpRequest.getRemoteAddr().c_str()); // Transaction Logging - TransactionLog* xlog = application.getServiceProvider().getTransactionLog(); + string primaryAssertionID("none"); + if (m_cacheAssertions) { + if (tokens) + primaryAssertionID = obj["assertions"].first().string(); + } + else if (tokens) { + auto_ptr_char tokenid(tokens->front()->getID()); + primaryAssertionID = tokenid.get(); + } + TransactionLog* xlog = app.getServiceProvider().getTransactionLog(); Locker locker(xlog); xlog->log.infoStream() << "New session (ID: " << key.get() << ") with (applicationId: " << - application.getId() << + app.getId() << ") for principal from (IdP: " << (pid ? pid : "none") << ") at (ClientAddress: " << - (client_addr ? client_addr : "none") << + httpRequest.getRemoteAddr() << ") with (NameIdentifier: " << (nameid ? name.get() : "none") << + ") using (Protocol: " << + (prot ? prot : "none") << + ") from (AssertionID: " << + primaryAssertionID << ")"; - + if (attributes) { xlog->log.infoStream() << "Cached the following attributes with session (ID: " << key.get() << ") for (applicationId: " << - application.getId() << + app.getId() << ") {"; - for (multimap::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) - xlog->log.infoStream() << "\t" << a->second->getId() << " (" << a->second->valueCount() << " values)"; + for (vector::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) + xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)"; xlog->log.info("}"); } - return key.get(); -} - -Session* SSCache::find(const char* key, const Application& application, const char* client_addr, time_t* timeout) -{ -#ifdef _DEBUG - xmltooling::NDC ndc("find"); -#endif - - m_log.debug("searching for session (%s)", key); - - time_t lastAccess; - string record; - int ver = m_storage->readText(key, "session", &record, &lastAccess); - if (!ver) - return NULL; - - m_log.debug("reconstituting session and checking validity"); - - DDF obj; - istringstream in(record); - in >> obj; - - if (!XMLString::equals(obj["application_id"].string(), application.getId())) { - m_log.error("an application (%s) tried to access another application's session", application.getId()); - obj.destroy(); - return NULL; - } - - if (client_addr) { - if (m_log.isDebugEnabled()) - m_log.debug("comparing client address %s against %s", client_addr, obj["client_addr"].string()); - if (strcmp(obj["client_addr"].string(),client_addr)) { - m_log.warn("client address mismatch"); - remove(key, application); - RetryableProfileException ex( - "Your IP address ($1) does not match the address recorded at the time the session was established.", - params(1,client_addr) - ); - string eid(obj["entity_id"].string()); - obj.destroy(); - if (eid.empty()) - throw ex; - MetadataProvider* m=application.getMetadataProvider(); - Locker locker(m); - annotateException(&ex,m->getEntityDescriptor(eid.c_str(),false)); // throws it - } - } + if (!m_outboundHeader.empty()) + httpResponse.setResponseHeader(m_outboundHeader.c_str(), key.get()); - lastAccess -= m_cacheTimeout; // adjusts it back to the last time the record's timestamp was touched - time_t now=time(NULL); - - if (timeout && *timeout > 0 && now - lastAccess >= *timeout) { - m_log.info("session timed out (ID: %s)", key); - remove(key, application); - RetryableProfileException ex("Your session has expired, and you must re-authenticate."); - string eid(obj["entity_id"].string()); - obj.destroy(); - if (eid.empty()) - throw ex; - MetadataProvider* m=application.getMetadataProvider(); - Locker locker(m); - annotateException(&ex,m->getEntityDescriptor(eid.c_str(),false)); // throws it - } - - auto_ptr_XMLCh exp(obj["expires"].string()); - if (exp.get()) { - DateTime iso(exp.get()); - iso.parseDateTime(); - if (now > iso.getEpoch()) { - m_log.info("session expired (ID: %s)", key); - remove(key, application); - RetryableProfileException ex("Your session has expired, and you must re-authenticate."); - string eid(obj["entity_id"].string()); - obj.destroy(); - if (eid.empty()) - throw ex; - MetadataProvider* m=application.getMetadataProvider(); - Locker locker(m); - annotateException(&ex,m->getEntityDescriptor(eid.c_str(),false)); // throws it - } - } - - if (timeout) { - // Update storage expiration, if possible. - try { - m_storage->updateContext(key, now + m_cacheTimeout); - } - catch (exception& ex) { - m_log.error("failed to update session expiration: %s", ex.what()); - } - } + time_t cookieLifetime = 0; + pair shib_cookie = app.getCookieNameProps("_shibsession_", &cookieLifetime); + string k(key.get()); + k += shib_cookie.second; - // Finally build the Session object. - try { - return new StoredSession(this, obj); - } - catch (exception&) { - obj.destroy(); - throw; - } -} - -void SSCache::remove(const char* key, const Application& application) -{ -#ifdef _DEBUG - xmltooling::NDC ndc("remove"); + if (cookieLifetime > 0) { + cookieLifetime += now; +#ifndef HAVE_GMTIME_R + ptime=gmtime(&cookieLifetime); +#else + ptime=gmtime_r(&cookieLifetime,&res); #endif + char cookietimebuf[64]; + strftime(cookietimebuf,64,"; expires=%a, %d %b %Y %H:%M:%S GMT",ptime); + k += cookietimebuf; + } - m_storage->deleteContext(key); - m_log.info("removed session (%s)", key); - - TransactionLog* xlog = application.getServiceProvider().getTransactionLog(); - Locker locker(xlog); - xlog->log.info("Destroyed session (applicationId: %s) (ID: %s)", application.getId(), key); + httpResponse.setCookie(shib_cookie.first.c_str(), k.c_str()); } bool SSCache::matches( - const char* key, + const Application& app, + const xmltooling::HTTPRequest& request, const saml2md::EntityDescriptor* issuer, const saml2::NameID& nameid, - const set* indexes, - const Application& application + const set* indexes ) { - auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL); + auto_ptr_char entityID(issuer ? issuer->getEntityID() : nullptr); try { - Session* session = find(key, application); + Session* session = find(app, request); if (session) { - Locker locker(session); + Locker locker(session, false); if (XMLString::equals(session->getEntityID(), entityID.get()) && session->getNameID() && - stronglyMatches(issuer->getEntityID(), application.getXMLString("entityID").second, nameid, *session->getNameID())) { + stronglyMatches(issuer->getEntityID(), app.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) { return (!indexes || indexes->empty() || (session->getSessionIndex() ? (indexes->count(session->getSessionIndex())>0) : false)); } } } catch (exception& ex) { - m_log.error("error while matching session (%s): %s", key, ex.what()); + m_log.error("error while matching session: %s", ex.what()); } return false; } vector::size_type SSCache::logout( + const Application& app, const saml2md::EntityDescriptor* issuer, const saml2::NameID& nameid, const set* indexes, time_t expires, - const Application& application, vector& sessionsKilled ) { @@ -857,7 +1162,10 @@ vector::size_type SSCache::logout( xmltooling::NDC ndc("logout"); #endif - auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL); + if (!m_storage) + throw ConfigurationException("SessionCache insertion requires a StorageService."); + + auto_ptr_char entityID(issuer ? issuer->getEntityID() : nullptr); auto_ptr_char name(nameid.getName()); m_log.info("request to logout sessions from (%s) for (%s)", entityID.get() ? entityID.get() : "unknown", name.get()); @@ -883,17 +1191,17 @@ vector::size_type SSCache::logout( strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime); time_t oldexp = 0; - ver = m_storage->readText("Logout", name.get(), &record, &oldexp); + ver = m_storage_lite->readText("Logout", name.get(), &record, &oldexp); if (ver > 0) { istringstream lin(record); lin >> obj; } else { - obj = DDF(NULL).structure(); + obj = DDF(nullptr).structure(); } // Structure is keyed by the IdP and SP, with a member per session index containing the expiration. - DDF root = obj.addmember(issuer ? entityID.get() : "_shibnull").addmember(application.getString("entityID").second); + DDF root = obj.addmember(issuer ? entityID.get() : "_shibnull").addmember(app.getRelyingParty(issuer)->getString("entityID").second); if (indexes) { for (set::const_iterator x = indexes->begin(); x!=indexes->end(); ++x) root.addmember(x->c_str()).string(timebuf); @@ -907,15 +1215,15 @@ vector::size_type SSCache::logout( lout << obj; if (ver > 0) { - ver = m_storage->updateText("Logout", name.get(), lout.str().c_str(), max(expires, oldexp), ver); + ver = m_storage_lite->updateText("Logout", name.get(), lout.str().c_str(), max(expires, oldexp), ver); if (ver <= 0) { // Out of sync, or went missing, so retry. - return logout(issuer, nameid, indexes, expires, application, sessionsKilled); + return logout(app, issuer, nameid, indexes, expires, sessionsKilled); } } - else if (!m_storage->createText("Logout", name.get(), lout.str().c_str(), expires)) { + else if (!m_storage_lite->createText("Logout", name.get(), lout.str().c_str(), expires)) { // Hit a dup, so just retry, hopefully hitting the other branch. - return logout(issuer, nameid, indexes, expires, application, sessionsKilled); + return logout(app, issuer, nameid, indexes, expires, sessionsKilled); } obj.destroy(); @@ -923,7 +1231,7 @@ vector::size_type SSCache::logout( } // Read in potentially matching sessions. - ver = m_storage->readText("NameID", name.get(), &record); + ver = m_storage_lite->readText("NameID", name.get(), &record); if (ver == 0) { m_log.debug("no active sessions to logout for supplied issuer and subject"); return 0; @@ -940,20 +1248,20 @@ vector::size_type SSCache::logout( key = sessions.first(); while (key.isstring()) { // Fetch the session for comparison. - Session* session = NULL; + Session* session = nullptr; try { - session = find(key.string(), application); + session = find(app, key.string()); } catch (exception& ex) { m_log.error("error locating session (%s): %s", key.string(), ex.what()); } if (session) { - Locker locker(session); + Locker locker(session, false); // Same issuer? if (XMLString::equals(session->getEntityID(), entityID.get())) { // Same NameID? - if (stronglyMatches(issuer->getEntityID(), application.getXMLString("entityID").second, nameid, *session->getNameID())) { + if (stronglyMatches(issuer->getEntityID(), app.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) { sessionsKilled.push_back(key.string()); key.destroy(); } @@ -979,19 +1287,19 @@ vector::size_type SSCache::logout( } sessions = obj.next(); } - + if (obj.first().isnull()) obj.destroy(); // If possible, write back the mapping record (this isn't crucial). try { if (obj.isnull()) { - m_storage->deleteText("NameID", name.get()); + m_storage_lite->deleteText("NameID", name.get()); } else if (!sessionsKilled.empty()) { ostringstream out; out << obj; - if (m_storage->updateText("NameID", name.get(), out.str().c_str(), 0, ver) <= 0) + if (m_storage_lite->updateText("NameID", name.get(), out.str().c_str(), 0, ver) <= 0) m_log.warn("logout mapping record changed behind us, leaving it alone"); } } @@ -1006,7 +1314,7 @@ bool SSCache::stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::Na { if (!XMLString::equals(n1.getName(), n2.getName())) return false; - + const XMLCh* s1 = n1.getFormat(); const XMLCh* s2 = n2.getFormat(); if (!s1 || !*s1) @@ -1015,7 +1323,7 @@ bool SSCache::stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::Na s2 = saml2::NameID::UNSPECIFIED; if (!XMLString::equals(s1,s2)) return false; - + s1 = n1.getNameQualifier(); s2 = n2.getNameQualifier(); if (!s1 || !*s1) @@ -1037,34 +1345,394 @@ bool SSCache::stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::Na return true; } +#endif + +Session* SSCache::find(const Application& app, const char* key, const char* client_addr, time_t* timeout) +{ +#ifdef _DEBUG + xmltooling::NDC ndc("find"); +#endif + StoredSession* session=nullptr; + + if (inproc) { + m_log.debug("searching local cache for session (%s)", key); + m_lock->rdlock(); + map::const_iterator i=m_hashtable.find(key); + if (i!=m_hashtable.end()) { + // Save off and lock the session. + session = i->second; + session->lock(); + m_lock->unlock(); + m_log.debug("session found locally, validating it for use"); + } + else { + m_lock->unlock(); + } + } + + if (!session) { + if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) { + m_log.debug("session not found locally, remoting the search"); + // Remote the request. + DDF in("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out; + DDFJanitor jin(in); + in.structure(); + in.addmember("key").string(key); + in.addmember("application_id").string(app.getId()); + if (timeout && *timeout) { + // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps. +#ifndef HAVE_GMTIME_R + struct tm* ptime=gmtime(timeout); +#else + struct tm res; + struct tm* ptime=gmtime_r(timeout,&res); +#endif + char timebuf[32]; + strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime); + in.addmember("timeout").string(timebuf); + } + + try { + out=app.getServiceProvider().getListenerService()->send(in); + if (!out.isstruct()) { + out.destroy(); + m_log.debug("session not found in remote cache"); + return nullptr; + } + + // Wrap the results in a local entry and save it. + session = new StoredSession(this, out); + // The remote end has handled timeout issues, we handle address and expiration checks. + timeout = nullptr; + } + catch (...) { + out.destroy(); + throw; + } + } + else { + // We're out of process, so we can search the storage service directly. +#ifndef SHIBSP_LITE + if (!m_storage) + throw ConfigurationException("SessionCache lookup requires a StorageService."); + + m_log.debug("searching for session (%s)", key); + + DDF obj; + time_t lastAccess; + string record; + int ver = m_storage->readText(key, "session", &record, &lastAccess); + if (!ver) + return nullptr; + + m_log.debug("reconstituting session and checking validity"); + + istringstream in(record); + in >> obj; + + unsigned long cacheTimeout = getCacheTimeout(app); + lastAccess -= cacheTimeout; // adjusts it back to the last time the record's timestamp was touched + time_t now=time(nullptr); + + if (timeout && *timeout > 0 && now - lastAccess >= *timeout) { + m_log.info("session timed out (ID: %s)", key); + remove(app, key); + const char* eid = obj["entity_id"].string(); + if (!eid) { + obj.destroy(); + throw RetryableProfileException("Your session has expired, and you must re-authenticate."); + } + string eid2(eid); + obj.destroy(); + throw RetryableProfileException("Your session has expired, and you must re-authenticate.", namedparams(1, "entityID", eid2.c_str())); + } + + if (timeout) { + // Update storage expiration, if possible. + try { + m_storage->updateContext(key, now + cacheTimeout); + } + catch (exception& ex) { + m_log.error("failed to update session expiration: %s", ex.what()); + } + } + + // Wrap the results in a local entry and save it. + session = new StoredSession(this, obj); + // We handled timeout issues, still need to handle address and expiration checks. + timeout = nullptr; +#else + throw ConfigurationException("SessionCache search requires a StorageService."); +#endif + } + + if (inproc) { + // Lock for writing and repeat the search to avoid duplication. + m_lock->wrlock(); + SharedLock shared(m_lock, false); + if (m_hashtable.count(key)) { + // We're using an existing session entry. + delete session; + session = m_hashtable[key]; + session->lock(); + } + else { + m_hashtable[key]=session; + session->lock(); + } + } + } + + if (!XMLString::equals(session->getApplicationID(), app.getId())) { + m_log.error("an application (%s) tried to access another application's session", app.getId()); + session->unlock(); + return nullptr; + } + + // Verify currency and update the timestamp if indicated by caller. + try { + session->validate(app, client_addr, timeout); + } + catch (...) { + session->unlock(); + remove(app, key); + throw; + } + + return session; +} + +Session* SSCache::find(const Application& app, HTTPRequest& request, const char* client_addr, time_t* timeout) +{ + string id = active(app, request); + if (id.empty()) + return nullptr; + try { + Session* session = find(app, id.c_str(), client_addr, timeout); + if (session) + return session; + HTTPResponse* response = dynamic_cast(&request); + if (response) { + if (!m_outboundHeader.empty()) + response->setResponseHeader(m_outboundHeader.c_str(), nullptr); + pair shib_cookie = app.getCookieNameProps("_shibsession_"); + string exp(shib_cookie.second); + exp += "; expires=Mon, 01 Jan 2001 00:00:00 GMT"; + response->setCookie(shib_cookie.first.c_str(), exp.c_str()); + } + } + catch (exception&) { + HTTPResponse* response = dynamic_cast(&request); + if (response) { + if (!m_outboundHeader.empty()) + response->setResponseHeader(m_outboundHeader.c_str(), nullptr); + pair shib_cookie = app.getCookieNameProps("_shibsession_"); + string exp(shib_cookie.second); + exp += "; expires=Mon, 01 Jan 2001 00:00:00 GMT"; + response->setCookie(shib_cookie.first.c_str(), exp.c_str()); + } + throw; + } + return nullptr; +} + +void SSCache::remove(const Application& app, const HTTPRequest& request, HTTPResponse* response) +{ + string session_id; + pair shib_cookie = app.getCookieNameProps("_shibsession_"); + + if (!m_inboundHeader.empty()) + session_id = request.getHeader(m_inboundHeader.c_str()); + if (session_id.empty()) { + const char* c = request.getCookie(shib_cookie.first.c_str()); + if (c && *c) + session_id = c; + } + + if (!session_id.empty()) { + if (response) { + if (!m_outboundHeader.empty()) + response->setResponseHeader(m_outboundHeader.c_str(), nullptr); + string exp(shib_cookie.second); + exp += "; expires=Mon, 01 Jan 2001 00:00:00 GMT"; + response->setCookie(shib_cookie.first.c_str(), exp.c_str()); + } + remove(app, session_id.c_str()); + } +} + +void SSCache::remove(const Application& app, const char* key) +{ +#ifdef _DEBUG + xmltooling::NDC ndc("remove"); +#endif + // Take care of local copy. + if (inproc) + dormant(key); + + if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) { + // Remove the session from storage directly. +#ifndef SHIBSP_LITE + m_storage->deleteContext(key); + m_log.info("removed session (%s)", key); + + TransactionLog* xlog = app.getServiceProvider().getTransactionLog(); + Locker locker(xlog); + xlog->log.info("Destroyed session (applicationId: %s) (ID: %s)", app.getId(), key); +#else + throw ConfigurationException("SessionCache removal requires a StorageService."); +#endif + } + else { + // Remote the request. + DDF in("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache"); + DDFJanitor jin(in); + in.structure(); + in.addmember("key").string(key); + in.addmember("application_id").string(app.getId()); + + DDF out = app.getServiceProvider().getListenerService()->send(in); + out.destroy(); + } +} + +void SSCache::dormant(const char* key) +{ +#ifdef _DEBUG + xmltooling::NDC ndc("dormant"); +#endif + + m_log.debug("deleting local copy of session (%s)", key); + + // lock the cache for writing, which means we know nobody is sitting in find() + m_lock->wrlock(); + + // grab the entry from the table + map::const_iterator i=m_hashtable.find(key); + if (i==m_hashtable.end()) { + m_lock->unlock(); + return; + } + + // ok, remove the entry and lock it + StoredSession* entry=i->second; + m_hashtable.erase(key); + entry->lock(); + + // unlock the cache + m_lock->unlock(); + + // we can release the cache entry lock because we know we're not in the cache anymore + entry->unlock(); + + delete entry; +} + +void* SSCache::cleanup_fn(void* p) +{ +#ifdef _DEBUG + xmltooling::NDC ndc("cleanup"); +#endif + + SSCache* pcache = reinterpret_cast(p); + +#ifndef WIN32 + // First, let's block all signals + Thread::mask_all_signals(); +#endif + + auto_ptr mutex(Mutex::create()); + + // Load our configuration details... + static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l); + const XMLCh* tag=pcache->m_root ? pcache->m_root->getAttributeNS(nullptr, cleanupInterval) : nullptr; + int rerun_timer = 900; + if (tag && *tag) { + rerun_timer = XMLString::parseInt(tag); + if (rerun_timer <= 0) + rerun_timer = 900; + } + + mutex->lock(); + + pcache->m_log.info("cleanup thread started...run every %d secs; timeout after %d secs", rerun_timer, pcache->m_inprocTimeout); + + while (!pcache->shutdown) { + pcache->shutdown_wait->timedwait(mutex.get(), rerun_timer); + if (pcache->shutdown) + break; + + // Ok, let's run through the cleanup process and clean out + // really old sessions. This is a two-pass process. The + // first pass is done holding a read-lock while we iterate over + // the cache. The second pass doesn't need a lock because + // the 'deletes' will lock the cache. + + // Pass 1: iterate over the map and find all entries that have not been + // used in the allotted timeout. + vector stale_keys; + time_t stale = time(nullptr) - pcache->m_inprocTimeout; + + pcache->m_log.debug("cleanup thread running"); + + pcache->m_lock->rdlock(); + for (map::const_iterator i=pcache->m_hashtable.begin(); i!=pcache->m_hashtable.end(); ++i) { + // If the last access was BEFORE the stale timeout... + i->second->lock(); + time_t last=i->second->getLastAccess(); + i->second->unlock(); + if (last < stale) + stale_keys.push_back(i->first); + } + pcache->m_lock->unlock(); + + if (!stale_keys.empty()) { + pcache->m_log.info("purging %d old sessions", stale_keys.size()); + + // Pass 2: walk through the list of stale entries and remove them from the cache + for (vector::const_iterator j = stale_keys.begin(); j != stale_keys.end(); ++j) + pcache->dormant(j->c_str()); + } + + pcache->m_log.debug("cleanup thread completed"); + } + + pcache->m_log.info("cleanup thread exiting"); + + mutex->unlock(); + return nullptr; +} + +#ifndef SHIBSP_LITE + void SSCache::receive(DDF& in, ostream& out) { #ifdef _DEBUG xmltooling::NDC ndc("receive"); #endif + const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string()); + if (!app) + throw ListenerException("Application not found, check configuration?"); - if (!strcmp(in.name(),"find::"REMOTED_SESSION_CACHE"::SessionCache")) { + if (!strcmp(in.name(),"find::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) { const char* key=in["key"].string(); if (!key) - throw ListenerException("Required parameters missing for session removal."); - - const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string()); - if (!app) - throw ListenerException("Application not found, check configuration?"); + throw ListenerException("Required parameters missing for session lookup."); // Do an unversioned read. string record; time_t lastAccess; if (!m_storage->readText(key, "session", &record, &lastAccess)) { - DDF ret(NULL); + DDF ret(nullptr); DDFJanitor jan(ret); out << ret; return; } // Adjust for expiration to recover last access time and check timeout. - lastAccess -= m_cacheTimeout; - time_t now=time(NULL); + unsigned long cacheTimeout = getCacheTimeout(*app); + lastAccess -= cacheTimeout; + time_t now=time(nullptr); // See if we need to check for a timeout. if (in["timeout"].string()) { @@ -1073,26 +1741,26 @@ void SSCache::receive(DDF& in, ostream& out) DateTime dtobj(dt.get()); dtobj.parseDateTime(); timeout = dtobj.getEpoch(); - + if (timeout > 0 && now - lastAccess >= timeout) { m_log.info("session timed out (ID: %s)", key); - remove(key,*app); + remove(*app, key); throw RetryableProfileException("Your session has expired, and you must re-authenticate."); - } + } // Update storage expiration, if possible. try { - m_storage->updateContext(key, now + m_cacheTimeout); + m_storage->updateContext(key, now + cacheTimeout); } catch (exception& ex) { m_log.error("failed to update session expiration: %s", ex.what()); } } - + // Send the record back. out << record; } - else if (!strcmp(in.name(),"touch::"REMOTED_SESSION_CACHE"::SessionCache")) { + else if (!strcmp(in.name(),"touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) { const char* key=in["key"].string(); if (!key) throw ListenerException("Required parameters missing for session check."); @@ -1108,8 +1776,9 @@ void SSCache::receive(DDF& in, ostream& out) } // Adjust for expiration to recover last access time and check timeout. - lastAccess -= m_cacheTimeout; - time_t now=time(NULL); + unsigned long cacheTimeout = getCacheTimeout(*app); + lastAccess -= cacheTimeout; + time_t now=time(nullptr); // See if we need to check for a timeout. time_t timeout = 0; @@ -1119,52 +1788,40 @@ void SSCache::receive(DDF& in, ostream& out) dtobj.parseDateTime(); timeout = dtobj.getEpoch(); } - + if (timeout > 0 && now - lastAccess >= timeout) { m_log.info("session timed out (ID: %s)", key); throw RetryableProfileException("Your session has expired, and you must re-authenticate."); - } + } // Update storage expiration, if possible. try { - m_storage->updateContext(key, now + m_cacheTimeout); + m_storage->updateContext(key, now + cacheTimeout); } catch (exception& ex) { m_log.error("failed to update session expiration: %s", ex.what()); } - + if (ver > curver) { // Send the record back. out << record; } else { - DDF ret(NULL); + DDF ret(nullptr); DDFJanitor jan(ret); out << ret; } } - else if (!strcmp(in.name(),"remove::"REMOTED_SESSION_CACHE"::SessionCache")) { + else if (!strcmp(in.name(),"remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) { const char* key=in["key"].string(); if (!key) throw ListenerException("Required parameter missing for session removal."); - const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string()); - if (!app) - throw ListenerException("Application not found, check configuration?"); - - remove(key,*app); - DDF ret(NULL); + remove(*app, key); + DDF ret(nullptr); DDFJanitor jan(ret); out << ret; } - else if (!strcmp(in.name(),"getAssertion::"REMOTED_SESSION_CACHE"::SessionCache")) { - const char* key=in["key"].string(); - const char* id=in["id"].string(); - if (!key || !id) - throw ListenerException("Required parameters missing for assertion retrieval."); - string token; - if (!m_storage->readText(key, id, &token, NULL)) - throw FatalProfileException("Assertion not found in cache."); - out << token; - } } + +#endif