X-Git-Url: http://www.project-moonshot.org/gitweb/?a=blobdiff_plain;f=shibsp%2Fimpl%2FStorageServiceSessionCache.cpp;h=0c5ffe826c370b4897510120ec5fe7d0dd126530;hb=c44abc33cda3840a0ff1cfe889f2776f13607192;hp=8a7c0d117a6858d5e56553d71ff57716fc26f966;hpb=eedef9ec2ca4c93bf15d74a4cc756f07fce621fa;p=shibboleth%2Fcpp-sp.git diff --git a/shibsp/impl/StorageServiceSessionCache.cpp b/shibsp/impl/StorageServiceSessionCache.cpp index 8a7c0d1..0c5ffe8 100644 --- a/shibsp/impl/StorageServiceSessionCache.cpp +++ b/shibsp/impl/StorageServiceSessionCache.cpp @@ -1,24 +1,28 @@ -/* - * Copyright 2001-2007 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 +/** + * Licensed to the University Corporation for Advanced Internet + * Development, Inc. (UCAID) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * UCAID licenses this file to you 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 * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. */ /** * 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,59 +36,292 @@ #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 #include #include -using namespace shibsp; +#ifndef SHIBSP_LITE +# include +# 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 boost; using namespace std; -namespace shibsp { +namespace { + + // Allows the cache to bind sessions to multiple client address + // families based on whatever this function returns. + static const char* getAddressFamily(const char* addr) { + if (strchr(addr, ':')) + return "6"; + else + return "4"; + } + + class StoredSession; + class SSCache : public SessionCacheEx +#ifndef SHIBSP_LITE + ,public virtual Remoted +#endif + { + public: + SSCache(const DOMElement* e); + virtual ~SSCache(); + +#ifndef SHIBSP_LITE + void receive(DDF& in, ostream& out); + + void insert( + const Application& app, + const HTTPRequest& httpRequest, + HTTPResponse& httpResponse, + time_t expires, + const 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 + ) { + string dummy; + insert(dummy, app, httpRequest, httpResponse, expires, issuer, protocol, nameid, + authn_instant, session_index, authncontext_class, authncontext_decl, tokens, attributes); + } + + void insert( + string& sessionID, + const Application& app, + const HTTPRequest& httpRequest, + HTTPResponse& httpResponse, + time_t expires, + const 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 EntityDescriptor* issuer, + const saml2::NameID& nameid, + const set* indexes, + time_t expires, + vector& sessions + ) { + return _logout(app, issuer, nameid, indexes, expires, sessions, 0); + } + bool matches( + const Application& app, + const HTTPRequest& request, + const 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 or lifetime. + if (m_cacheTimeout) + return m_cacheTimeout; + pair timeout = pair(false, 3600); + const PropertySet* props = app.getPropertySet("Sessions"); + if (props) { + timeout = props->getUnsignedInt("timeout"); + if (!timeout.first) + timeout.second = 3600; + } + // As long as one of the two factors is set, add them together. + if (timeout.second > 0 || m_cacheAllowance > 0) + return timeout.second + m_cacheAllowance; + + // If timeouts are off, and there's no cache slop set, then use the lifetime. + timeout = pair(false, 28800); + if (props) { + timeout = props->getUnsignedInt("lifetime"); + if (!timeout.first || timeout.second == 0) + timeout.second = 28800; + } + return timeout.second; + } + + 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, short attempts=0); + vector::size_type _logout( + const Application& app, + const EntityDescriptor* issuer, + const saml2::NameID& nameid, + const set* indexes, + time_t expires, + vector& sessions, + short attempts + ); + bool stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const; + LogoutEvent* newLogoutEvent(const Application& app) const; + + bool m_cacheAssertions,m_reverseIndex; + set m_excludedNames; +#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 + scoped_ptr m_lock; + map m_hashtable; + + // management of buffered sessions + void dormant(const char* key); + static void* cleanup_fn(void*); + + bool shutdown; + scoped_ptr shutdown_wait; + scoped_ptr cleanup_thread; + }; - class SSCache; 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), m_cache(cache), m_expires(0), m_lastAccess(time(nullptr)) { + // Check for old address format. + if (m_obj["client_addr"].isstring()) { + const char* saddr = m_obj["client_addr"].string(); + DDF addrobj = m_obj["client_addr"].structure(); + if (saddr && *saddr) { + addrobj.addmember(getAddressFamily(saddr)).string(saddr); + } + } + + 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); + m_nameid.reset(saml2::NameIDBuilder::buildNameID()); + m_nameid->unmarshall(doc->getDocumentElement(), true); janitor.release(); - m_nameid = n.release(); } +#endif + if (cache->inproc) + m_lock.reset(Mutex::create()); } - - ~StoredSession(); - + + ~StoredSession() { + m_obj.destroy(); + for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup()); + } + Lockable* lock() { + if (m_lock.get()) + m_lock->lock(); return this; } void unlock() { - delete this; + if (m_lock.get()) + 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(); + return m_obj["client_addr"].first().string(); + } + + const char* getClientAddress(const char* family) const { + if (family) + return m_obj["client_addr"][family].string(); + return nullptr; } + void setClientAddress(const char* client_addr) { + DDF obj = m_obj["client_addr"]; + if (!obj.isstruct()) + obj = m_obj.addmember("client_addr").structure(); + obj.addmember(getAddressFamily(client_addr)).string(client_addr); + } + const char* getEntityID() const { return m_obj["entity_id"].string(); } @@ -94,9 +331,11 @@ namespace shibsp { const char* getAuthnInstant() const { return m_obj["authn_instant"].string(); } - const opensaml::saml2::NameID* getNameID() const { - return m_nameid; +#ifndef SHIBSP_LITE + const saml2::NameID* getNameID() const { + return m_nameid.get(); } +#endif const char* getSessionIndex() const { return m_obj["session_index"].string(); } @@ -118,7 +357,7 @@ namespace shibsp { 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(make_pair(*alias, *a)); + m_attributeIndex.insert(multimap::value_type(*alias, *a)); } } return m_attributeIndex; @@ -134,88 +373,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; - saml2::NameID* m_nameid; +#ifndef SHIBSP_LITE + scoped_ptr m_nameid; + mutable map< string,boost::shared_ptr > m_tokens; +#endif mutable vector m_attributes; mutable multimap m_attributeIndex; 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 vector* 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 - ); - void test(); - 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); - - 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; + scoped_ptr m_lock; }; SessionCache* SHIBSP_DLLLOCAL StorageServiceCacheFactory(const DOMElement* const & e) { return new SSCache(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); +} + +void SHIBSP_API shibsp::registerSessionCaches() +{ + SPConfig::getConfig().SessionCacheManager.registerFactory(STORAGESERVICE_SESSION_CACHE, StorageServiceCacheFactory); +} - static const XMLCh _StorageService[] = UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e); +Session::Session() +{ } -StoredSession::~StoredSession() +Session::~Session() { - m_obj.destroy(); - delete m_nameid; - for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup()); - for_each(m_tokens.begin(), m_tokens.end(), cleanup_pair()); } void StoredSession::unmarshallAttributes() const @@ -227,7 +435,6 @@ void StoredSession::unmarshallAttributes() const try { attribute = Attribute::unmarshall(attr); m_attributes.push_back(attribute); - m_attributeIndex.insert(make_pair(attribute->getId(), 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" : ""); @@ -240,15 +447,224 @@ 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) { + const char* saddr = getClientAddress(getAddressFamily(client_addr)); + if (saddr && *saddr) { + if (!XMLString::equals(saddr, client_addr)) { + m_cache->m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr); + throw RetryableProfileException( + "Your IP address ($1) does not match the address recorded at the time the session was established.", + params(1, client_addr) + ); + } + client_addr = nullptr; // clear out parameter as signal that session need not be updated below + } + else { + m_cache->m_log.info("session (%s) not yet bound to client address type, binding it to (%s)", getID(), client_addr); + } + } + + if (!timeout && !client_addr) + 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()); + in.addmember("application_id").string(app.getId()); + if (client_addr) // signals we need to bind an additional address to the session + in.addmember("client_addr").string(client_addr); + 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); + } + catch (...) { + out.destroy(); + throw; + } + + if (out.isstruct()) { + // We got an updated record back. + m_cache->m_log.debug("session updated, reconstituting it"); + 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."); + + // Versioned read, since we already have the data in hand if it's current. + 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."); + } + + if (timeout) { + // 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 (std::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; + } + + // We may need to write back a new address into the session using a versioned update loop. + if (client_addr) { + short attempts = 0; + do { + const char* saddr = getClientAddress(getAddressFamily(client_addr)); + if (saddr) { + // Something snuck in and bound the session to this address type, so it better match what we have. + if (!XMLString::equals(saddr, client_addr)) { + m_cache->m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr); + throw RetryableProfileException( + "Your IP address ($1) does not match the address recorded at the time the session was established.", + params(1, client_addr) + ); + } + break; // No need to update. + } + else { + // Bind it into the session. + setClientAddress(client_addr); + } + + // Tentatively increment the version. + m_obj["version"].integer(m_obj["version"].integer() + 1); + + ostringstream str; + str << m_obj; + record = str.str(); + + try { + ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer() - 1); + } + catch (std::exception&) { + m_obj["version"].integer(m_obj["version"].integer() - 1); + throw; + } + + if (ver <= 0) { + 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()); + throw IOException("Unable to update stored session."); + } + else if (ver < 0) { + // Out of sync. + if (++attempts > 10) { + m_cache->m_log.error("failed to bind client address, update attempts exceeded limit"); + throw IOException("Unable to update stored session, exceeded retry limit."); + } + 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, 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(), 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 + } +#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; + short attempts = 0; do { DDF attr; DDF attrs = m_obj["attributes"]; @@ -258,23 +674,23 @@ 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); } - catch (exception&) { + catch (std::exception&) { // Roll back modification to record. m_obj["version"].integer(m_obj["version"].integer()-1); vector::size_type count = attributes.size(); while (count--) - attrs.last().destroy(); + attrs.last().destroy(); throw; } @@ -283,7 +699,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. @@ -291,13 +707,17 @@ void StoredSession::addAttributes(const vector& attributes) } else if (ver < 0) { // Out of sync. + if (++attempts > 10) { + m_cache->m_log.error("failed to update stored session, update attempts exceeded limit"); + throw IOException("Unable to update stored session, exceeded retry limit."); + } 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); @@ -315,47 +735,36 @@ void StoredSession::addAttributes(const vector& attributes) } } while (ver < 0); // negative indicates a sync issue so we retry - TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog(); - Locker locker(xlog); - xlog->log.infoStream() << - "Added the following attributes to session (ID: " << - getID() << - ") for (applicationId: " << - m_obj["application_id"].string() << - ") {"; - for (vector::const_iterator a=attributes.begin(); a!=attributes.end(); ++a) - xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)"; - xlog->log.info("}"); - // We own them now, so clean them up. for_each(attributes.begin(), attributes.end(), xmltooling::cleanup()); } const Assertion* StoredSession::getAssertion(const char* id) const { - map::const_iterator i = m_tokens.find(id); - if (i!=m_tokens.end()) - return i->second; - + if (!m_cache->m_storage) + throw ConfigurationException("Assertion retrieval requires a StorageService."); + + map< string,boost::shared_ptr >::const_iterator i = m_tokens.find(id); + if (i != m_tokens.end()) + return i->second.get(); + 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)); + boost::shared_ptr xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true)); janitor.release(); - Assertion* token = dynamic_cast(xmlObject.get()); + boost::shared_ptr token = dynamic_pointer_cast(xmlObject); if (!token) throw FatalProfileException("Request for cached assertion returned an unknown object type."); - // Transfer ownership to us. - xmlObject.release(); - m_tokens[id]=token; - return token; + m_tokens[id] = token; + return token.get(); } void StoredSession::addAssertion(Assertion* assertion) @@ -363,41 +772,48 @@ void StoredSession::addAssertion(Assertion* assertion) #ifdef _DEBUG xmltooling::NDC ndc("addAssertion"); #endif - - if (!assertion) + + if (!m_cache->m_storage) + throw ConfigurationException("Session modification requires a StorageService."); + else if (!assertion) throw FatalProfileException("Unknown object type passed to session for storage."); auto_ptr_char id(assertion->getID()); + if (!id.get() || !*id.get()) + throw IOException("Assertion did not carry an ID."); + else if (strlen(id.get()) > m_cache->m_storage->getCapabilities().getKeySize()) + throw IOException("Assertion ID ($1) exceeds allowable storage key size.", params(1, id.get())); 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; + short attempts = 0; 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); - + 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); } - catch (exception&) { + catch (std::exception&) { token.destroy(); - m_obj["version"].integer(m_obj["version"].integer()-1); + m_obj["version"].integer(m_obj["version"].integer() - 1); m_cache->m_storage->deleteText(getID(), id.get()); throw; } @@ -405,7 +821,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()); @@ -414,14 +830,18 @@ void StoredSession::addAssertion(Assertion* assertion) } else if (ver < 0) { // Out of sync. + if (++attempts > 10) { + m_cache->m_log.error("failed to update stored session, update attempts exceeded limit"); + throw IOException("Unable to update stored session, exceeded retry limit."); + } 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); @@ -434,76 +854,205 @@ void StoredSession::addAssertion(Assertion* assertion) newobj["version"].integer(ver); m_obj.destroy(); m_obj = newobj; - + ver = -1; } } while (ver < 0); // negative indicates a sync issue so we retry m_ids.clear(); delete assertion; +} + +#endif + +SessionCache::SessionCache() +{ +} + +SessionCache::~SessionCache() +{ +} + +#ifndef SHIBSP_LITE + +void SessionCache::insert( + string& sessionID, + const Application& app, + const HTTPRequest& httpRequest, + HTTPResponse& httpResponse, + time_t expires, + const EntityDescriptor* issuer, + const XMLCh* protocol, + const saml2::NameID* nameid, + const XMLCh* authn_instant, + const XMLCh* session_index, + const XMLCh* authncontext_class, + const XMLCh* authncontext_decl, + const vector* tokens, + const vector* attributes + ) { + return insert(app, httpRequest, httpResponse, expires, issuer, protocol, nameid, + authn_instant, session_index, authncontext_class, authncontext_decl, tokens, attributes); +} + +#endif + +SessionCacheEx::SessionCacheEx() +{ +} - TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog(); - Locker locker(xlog); - xlog->log.info( - "Added assertion (ID: %s) to session for (applicationId: %s) with (ID: %s)", - id.get(), m_obj["application_id"].string(), getID() - ); +SessionCacheEx::~SessionCacheEx() +{ } SSCache::SSCache(const DOMElement* e) - : SessionCache(e, 3600), 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), m_reverseIndex(true), +#endif + m_root(e), m_inprocTimeout(900), m_cacheTimeout(0), m_cacheAllowance(0), shutdown(false) { 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 excludeReverseIndex[] = UNICODE_LITERAL_19(e,x,c,l,u,d,e,R,e,v,e,r,s,e,I,n,d,e,x); + 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 maintainReverseIndex[] = UNICODE_LITERAL_20(m,a,i,n,t,a,i,n,R,e,v,e,r,s,e,I,n,d,e,x); + 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); + + if (e && e->hasAttributeNS(nullptr, cacheTimeout)) { + m_log.warn("cacheTimeout property is deprecated in favor of cacheAllowance (see documentation)"); + 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 + throw ConfigurationException("SessionCache unable to locate StorageService ($1), check configuration.", params(1, 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 + throw ConfigurationException("SessionCache unable to locate 'lite' StorageService ($1), check configuration.", params(1, 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); + m_reverseIndex = XMLHelper::getAttrBool(e, true, maintainReverseIndex); + const XMLCh* excludedNames = e ? e->getAttributeNS(nullptr, excludeReverseIndex) : nullptr; + if (excludedNames && *excludedNames) { + XMLStringTokenizer toks(excludedNames); + while (toks.hasMoreTokens()) + m_excludedNames.insert(toks.nextToken()); + } + } +#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.reset(RWLock::create()); + shutdown_wait.reset(CondWait::create()); + cleanup_thread.reset(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; + if (shutdown_wait.get()) + shutdown_wait->signal(); + if (cleanup_thread.get()) + cleanup_thread->join(nullptr); + + for_each(m_hashtable.begin(),m_hashtable.end(),cleanup_pair()); } +#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() { -#ifdef _DEBUG - xmltooling::NDC ndc("test"); -#endif - - auto_ptr_char temp(SAMLConfig::getConfig().generateIdentifier()); - m_storage->createString("SessionCacheTest", temp.get(), "Test", time(NULL) + 60); + XMLCh* wide = SAMLConfig::getConfig().generateIdentifier(); + auto_ptr_char temp(wide); + XMLString::release(&wide); + 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) +void SSCache::insert(const char* key, time_t expires, const char* name, const char* index, short attempts) { + if (attempts > 10) { + throw IOException("Exceeded retry limit."); + } + + if (!name || !*name) { + m_log.warn("NameID value was empty or null, ignoring request to store for logout"); + return; + } + string dup; - if (strlen(name) > 255) { - dup = string(name).substr(0,255); + unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize(); + if (strlen(name) > storageLimit) { + dup = string(name).substr(0, storageLimit); name = dup.c_str(); } @@ -513,7 +1062,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); @@ -521,7 +1070,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) @@ -529,7 +1078,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. @@ -538,22 +1087,24 @@ 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); + return insert(key, expires, name, index, attempts + 1); } } - 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); + return insert(key, expires, name, index, attempts + 1); } } -string SSCache::insert( +void SSCache::insert( + string& sessionID, + 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, @@ -568,29 +1119,33 @@ string SSCache::insert( #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) { + if (name.get() && *name.get()) { // Check for a pending logout. - if (strlen(name.get()) > 255) - const_cast(name.get())[255] = 0; + unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize(); + string namebuf = name.get(); + if (namebuf.length() > storageLimit) + namebuf = namebuf.substr(0, storageLimit); string pending; - int ver = m_storage->readText("Logout", name.get(), &pending); + int ver = m_storage_lite->readText("Logout", namebuf.c_str(), &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); @@ -603,12 +1158,15 @@ string SSCache::insert( } } - auto_ptr_char key(SAMLConfig::getConfig().generateIdentifier()); + XMLCh* widekey = SAMLConfig::getConfig().generateIdentifier(); + auto_ptr_char key(widekey); + XMLString::release(&widekey); // 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 @@ -621,8 +1179,12 @@ 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); + string caddr(httpRequest.getRemoteAddr()); + if (!caddr.empty()) { + DDF addrobj = obj.addmember("client_addr").structure(); + addrobj.addmember(getAddressFamily(caddr.c_str())).string(caddr.c_str()); + } + if (issuer) obj.addmember("entity_id").string(entity_id.get()); if (protocol) { @@ -650,15 +1212,15 @@ 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(); @@ -667,231 +1229,124 @@ string SSCache::insert( 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) + if (name.get() && *name.get() && m_reverseIndex + && (m_excludedNames.size() == 0 || m_excludedNames.count(nameid->getName()) == 0)) { + try { insert(key.get(), expires, name.get(), index.get()); - } - catch (exception& ex) { - m_log.error("error storing back mapping of NameID for logout: %s", ex.what()); + } + catch (std::exception& ex) { + 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)) - throw IOException("duplicate assertion ID ($1)", params(1, tokenid.get())); + if (!tokenid.get() || !*tokenid.get() || strlen(tokenid.get()) > m_storage->getCapabilities().getKeySize()) + throw IOException("Assertion ID is missing or exceeds key size of storage service."); + else if (!m_storage->createText(key.get(), tokenid.get(), tokenstr.str().c_str(), now + cacheTimeout)) + throw IOException("Duplicate assertion ID ($1)", params(1, tokenid.get())); } } - catch (exception& ex) { + catch (std::exception& ex) { m_log.error("error storing assertion along with session: %s", ex.what()); } } 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); - - // Transaction Logging - TransactionLog* xlog = application.getServiceProvider().getTransactionLog(); - Locker locker(xlog); - xlog->log.infoStream() << - "New session (ID: " << - key.get() << - ") with (applicationId: " << - application.getId() << - ") for principal from (IdP: " << - (pid ? pid : "none") << - ") at (ClientAddress: " << - (client_addr ? client_addr : "none") << - ") with (NameIdentifier: " << - (nameid ? name.get() : "none") << - ")"; - - if (attributes) { - xlog->log.infoStream() << - "Cached the following attributes with session (ID: " << - key.get() << - ") for (applicationId: " << - application.getId() << - ") {"; - 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 - } - } + 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()); - 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()); - } - } + if (!m_outboundHeader.empty()) + httpResponse.setResponseHeader(m_outboundHeader.c_str(), key.get()); - // Finally build the Session object. - try { - return new StoredSession(this, obj); - } - catch (exception&) { - obj.destroy(); - throw; - } -} + time_t cookieLifetime = 0; + pair shib_cookie = app.getCookieNameProps("_shibsession_", &cookieLifetime); + string k(key.get()); + k += shib_cookie.second; -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()); + sessionID = key.get(); } 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()); + catch (std::exception& ex) { + m_log.error("error while matching session: %s", ex.what()); } return false; } -vector::size_type SSCache::logout( +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 + vector& sessionsKilled, + short attempts ) { #ifdef _DEBUG xmltooling::NDC ndc("logout"); #endif - auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL); + if (!m_storage) + throw ConfigurationException("SessionCache logout requires a StorageService."); + else if (attempts > 10) + throw IOException("Exceeded retry limit."); + + 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()); - if (strlen(name.get()) > 255) - const_cast(name.get())[255] = 0; + unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize(); + if (strlen(name.get()) > storageLimit) + const_cast(name.get())[storageLimit] = 0; DDF obj; DDFJanitor jobj(obj); @@ -911,17 +1366,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); @@ -935,23 +1390,28 @@ 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, attempts + 1); } } - 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, attempts + 1); } obj.destroy(); record.erase(); } + if (!m_reverseIndex) { + m_log.error("cannot support logout because maintainReverseIndex property is turned off"); + throw ConfigurationException("Logout is unsupported by the session cache configuration."); + } + // 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; @@ -968,20 +1428,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) { + catch (std::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(); } @@ -994,9 +1454,11 @@ vector::size_type SSCache::logout( } } else { - // Session's gone, so... - sessionsKilled.push_back(key.string()); - key.destroy(); + // Session may already be gone, or it may be associated with a different application. + // To be conservative, we'll leave it alone. This isn't really increasing our security + // risk, because if we can't lookup the session, it's unlikely the calling logout code + // can either, so there's no chance of removing the session anyway. + m_log.warn("session (%s) not accessible for logout, may be gone, or associated with a different application", key.string()); } key = sessions.next(); } @@ -1007,23 +1469,23 @@ 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"); } } - catch (exception& ex) { + catch (std::exception& ex) { m_log.error("error updating logout mapping record: %s", ex.what()); } @@ -1034,7 +1496,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) @@ -1043,7 +1505,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) @@ -1065,34 +1527,427 @@ bool SSCache::stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::Na return true; } +LogoutEvent* SSCache::newLogoutEvent(const Application& app) const +{ + if (!SPConfig::getConfig().isEnabled(SPConfig::Logging)) + return nullptr; + try { + auto_ptr event(SPConfig::getConfig().EventManager.newPlugin(LOGOUT_EVENT, nullptr)); + LogoutEvent* logout_event = dynamic_cast(event.get()); + if (logout_event) { + logout_event->m_app = &app; + event.release(); + return logout_event; + } + else { + m_log.warn("unable to audit event, log event object was of an incorrect type"); + } + } + catch (std::exception& ex) { + m_log.warn("exception auditing event: %s", ex.what()); + } + return nullptr; +} + +#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); + scoped_ptr logout_event(newLogoutEvent(app)); + if (logout_event.get()) { + logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_INVALID; + logout_event->m_sessions.push_back(key); + app.getServiceProvider().getTransactionLog()->write(*logout_event); + } + 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 (std::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.warn("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 (...) { +#ifndef SHIBSP_LITE + scoped_ptr logout_event(newLogoutEvent(app)); + if (logout_event.get()) { + logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_INVALID; + logout_event->m_session = session; + logout_event->m_sessions.push_back(session->getID()); + app.getServiceProvider().getTransactionLog()->write(*logout_event); + } +#endif + 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 (std::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); +#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 + + scoped_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_each(stale_keys.begin(), stale_keys.end(), boost::bind(&SSCache::dormant, pcache, boost::bind(&string::c_str, _1))); + } + + 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); + m_log.debug("session not found in cache (%s)", key); + 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()) { @@ -1101,43 +1956,51 @@ 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); + scoped_ptr logout_event(newLogoutEvent(*app)); + if (logout_event.get()) { + logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_INVALID; + logout_event->m_sessions.push_back(key); + app->getServiceProvider().getTransactionLog()->write(*logout_event); + } + 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) { + catch (std::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."); + const char* client_addr = in["client_addr"].string(); - // Do a versioned read. + // Do a read. May be unversioned if we need to bind a new client address. string record; time_t lastAccess; int curver = in["version"].integer(); - int ver = m_storage->readText(key, "session", &record, &lastAccess, curver); + int ver = m_storage->readText(key, "session", &record, &lastAccess, client_addr ? 0 : curver); if (ver == 0) { - m_log.warn("unsuccessful versioned read of session (ID: %s), caches out of sync?", key); + m_log.warn("unsuccessful read of session (ID: %s), caches out of sync?", key); throw RetryableProfileException("Your session has expired, and you must re-authenticate."); } // 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; @@ -1147,52 +2010,99 @@ 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) { + catch (std::exception& ex) { m_log.error("failed to update session expiration: %s", ex.what()); } - + + // We may need to write back a new address into the session using a versioned update loop. + if (client_addr) { + short attempts = 0; + m_log.info("binding session (%s) to new client address (%s)", key, client_addr); + do { + // We have to reconstitute the session object ourselves. + DDF sessionobj; + DDFJanitor sessionjan(sessionobj); + istringstream src(record); + src >> sessionobj; + ver = sessionobj["version"].integer(); + const char* saddr = sessionobj["client_addr"][getAddressFamily(client_addr)].string(); + if (saddr) { + // Something snuck in and bound the session to this address type, so it better match what we have. + if (!XMLString::equals(saddr, client_addr)) { + m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr); + throw RetryableProfileException( + "Your IP address ($1) does not match the address recorded at the time the session was established.", + params(1, client_addr) + ); + } + break; // No need to update. + } + else { + // Bind it into the session. + sessionobj["client_addr"].addmember(getAddressFamily(client_addr)).string(client_addr); + } + + // Tentatively increment the version. + sessionobj["version"].integer(sessionobj["version"].integer() + 1); + + ostringstream str; + str << sessionobj; + record = str.str(); + + ver = m_storage->updateText(key, "session", record.c_str(), 0, ver); + if (!ver) { + // Fatal problem with update. + m_log.error("updateText failed on StorageService for session (%s)", key); + throw IOException("Unable to update stored session."); + } + if (ver < 0) { + // Out of sync. + if (++attempts > 10) { + m_log.error("failed to bind client address, update attempts exceeded limit"); + throw IOException("Unable to update stored session, exceeded retry limit."); + } + m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy..."); + sessionobj["version"].integer(sessionobj["version"].integer() - 1); + ver = m_storage->readText(key, "session", &record); + if (!ver) { + m_log.error("readText failed on StorageService for session (%s)", key); + throw IOException("Unable to read back stored session."); + } + ver = -1; + } + } while (ver < 0); // negative indicates a sync issue so we retry + } + 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