X-Git-Url: http://www.project-moonshot.org/gitweb/?a=blobdiff_plain;f=shibsp%2Fimpl%2FStorageServiceSessionCache.cpp;h=f9fb5deee351c5dba10c026217633be94d592bf2;hb=c51bfd77603cf0ddb0b5e374c35586a8435895d6;hp=a3987ab2bf8d1b62cb0a540eaa26b2ec15d9957e;hpb=3968f4eac0a112d55ca7ec0041f9cdb98400e7b3;p=shibboleth%2Fcpp-sp.git diff --git a/shibsp/impl/StorageServiceSessionCache.cpp b/shibsp/impl/StorageServiceSessionCache.cpp index a3987ab..f9fb5de 100644 --- a/shibsp/impl/StorageServiceSessionCache.cpp +++ b/shibsp/impl/StorageServiceSessionCache.cpp @@ -46,7 +46,6 @@ #include #include #include -#include #include #include #include @@ -63,6 +62,7 @@ # include # include # include +# include using namespace opensaml::saml2md; #else # include @@ -75,7 +75,16 @@ 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 @@ -133,7 +142,9 @@ namespace shibsp { 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, @@ -203,11 +214,21 @@ namespace shibsp { 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); + 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; + 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; @@ -230,6 +251,15 @@ namespace shibsp { { public: 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()); @@ -277,8 +307,21 @@ namespace shibsp { 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(); } @@ -348,7 +391,7 @@ namespace shibsp { DDF m_obj; #ifndef SHIBSP_LITE scoped_ptr m_nameid; - mutable map> m_tokens; + mutable map< string,boost::shared_ptr > m_tokens; #endif mutable vector m_attributes; mutable multimap m_attributeIndex; @@ -418,32 +461,41 @@ void StoredSession::validate(const Application& app, const char* client_addr, ti // Address check? if (client_addr) { - if (!XMLString::equals(getClientAddress(),client_addr)) { - m_cache->m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, getClientAddress()); - throw RetryableProfileException( - "Your IP address ($1) does not match the address recorded at the time the session was established.", - params(1,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) + if (!timeout && !client_addr) return; if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) { - DDF in("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out; + 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 (*timeout) { + 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); + struct tm* ptime = gmtime(timeout); #else struct tm res; - struct tm* ptime=gmtime_r(timeout,&res); + struct tm* ptime = gmtime_r(timeout,&res); #endif char timebuf[32]; strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime); @@ -460,6 +512,7 @@ void StoredSession::validate(const Application& app, const char* client_addr, ti 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(); @@ -473,7 +526,7 @@ void StoredSession::validate(const Application& app, const char* client_addr, ti if (!m_cache->m_storage) throw ConfigurationException("Session touch requires a StorageService."); - // Do a versioned read. + // 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(); @@ -483,20 +536,22 @@ void StoredSession::validate(const Application& app, const char* client_addr, ti 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."); - } + 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()); + // 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) { @@ -511,6 +566,82 @@ void StoredSession::validate(const Application& app, const char* client_addr, ti 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 @@ -533,6 +664,7 @@ void StoredSession::addAttributes(const vector& attributes) m_cache->m_log.debug("adding attributes to session (%s)", getID()); int ver; + short attempts = 0; do { DDF attr; DDF attrs = m_obj["attributes"]; @@ -575,6 +707,10 @@ 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, nullptr); if (!ver) { @@ -660,12 +796,13 @@ void StoredSession::addAssertion(Assertion* assertion) throw IOException("Attempted to insert duplicate assertion ID into session."); int ver; + short attempts = 0; do { 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; @@ -676,7 +813,7 @@ void StoredSession::addAssertion(Assertion* assertion) } 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; } @@ -693,6 +830,10 @@ 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, nullptr); if (!ver) { @@ -765,25 +906,30 @@ SessionCacheEx::~SessionCacheEx() } SSCache::SSCache(const DOMElement* e) - : m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), inproc(true), + : m_log(Category::getInstance(SHIBSP_LOGCAT ".SessionCache")), inproc(true), #ifndef SHIBSP_LITE - m_storage(nullptr), m_storage_lite(nullptr), m_cacheAssertions(true), + 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(); 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); + 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); @@ -800,7 +946,7 @@ SSCache::SSCache(const DOMElement* e) 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()); + throw ConfigurationException("SessionCache unable to locate StorageService ($1), check configuration.", params(1, ssid.c_str())); } if (!m_storage) { m_storage = conf.getServiceProvider()->getStorageService(nullptr); @@ -816,7 +962,7 @@ SSCache::SSCache(const DOMElement* e) 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()); + 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"); @@ -824,6 +970,13 @@ SSCache::SSCache(const DOMElement* e) } 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 @@ -838,9 +991,9 @@ SSCache::SSCache(const DOMElement* e) #ifndef SHIBSP_LITE else { 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); + 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"); @@ -866,9 +1019,9 @@ SSCache::~SSCache() 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); + 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 @@ -878,13 +1031,24 @@ SSCache::~SSCache() void SSCache::test() { - auto_ptr_char temp(SAMLConfig::getConfig().generateIdentifier()); + 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; unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize(); if (strlen(name) > storageLimit) { @@ -926,12 +1090,12 @@ void SSCache::insert(const char* key, time_t expires, const char* name, const ch 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_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); } } @@ -1015,7 +1179,12 @@ void SSCache::insert( strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime); obj.addmember("expires").string(timebuf); - obj.addmember("client_addr").string(httpRequest.getRemoteAddr().c_str()); + 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) { @@ -1070,12 +1239,14 @@ void SSCache::insert( 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 (std::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 && m_cacheAssertions) { @@ -1149,13 +1320,14 @@ bool SSCache::matches( 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, - vector& sessionsKilled + vector& sessionsKilled, + short attempts ) { #ifdef _DEBUG @@ -1164,6 +1336,8 @@ vector::size_type SSCache::logout( 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()); @@ -1219,18 +1393,23 @@ vector::size_type SSCache::logout( 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(app, issuer, nameid, indexes, expires, sessionsKilled); + return _logout(app, issuer, nameid, indexes, expires, sessionsKilled, attempts + 1); } } 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(app, issuer, nameid, indexes, expires, 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_lite->readText("NameID", name.get(), &record); if (ver == 0) { @@ -1275,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(); } @@ -1397,7 +1578,7 @@ Session* SSCache::find(const Application& app, const char* key, const char* clie 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; + DDF in("find::" STORAGESERVICE_SESSION_CACHE "::SessionCache"), out; DDFJanitor jin(in); in.structure(); in.addmember("key").string(key); @@ -1513,7 +1694,7 @@ Session* SSCache::find(const Application& app, const char* key, const char* clie } if (!XMLString::equals(session->getApplicationID(), app.getId())) { - m_log.error("an application (%s) tried to access another application's session", app.getId()); + m_log.warn("an application (%s) tried to access another application's session", app.getId()); session->unlock(); return nullptr; } @@ -1619,7 +1800,7 @@ void SSCache::remove(const Application& app, const char* key) } else { // Remote the request. - DDF in("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache"); + DDF in("remove::" STORAGESERVICE_SESSION_CACHE "::SessionCache"); DDFJanitor jin(in); in.structure(); in.addmember("key").string(key); @@ -1747,7 +1928,7 @@ void SSCache::receive(DDF& in, ostream& out) if (!app) throw ListenerException("Application not found, check configuration?"); - if (!strcmp(in.name(),"find::"STORAGESERVICE_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 lookup."); @@ -1756,6 +1937,7 @@ void SSCache::receive(DDF& in, ostream& out) string record; time_t lastAccess; if (!m_storage->readText(key, "session", &record, &lastAccess)) { + m_log.debug("session not found in cache (%s)", key); DDF ret(nullptr); DDFJanitor jan(ret); out << ret; @@ -1799,18 +1981,19 @@ void SSCache::receive(DDF& in, ostream& out) // Send the record back. out << record; } - else if (!strcmp(in.name(),"touch::"STORAGESERVICE_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."); } @@ -1841,6 +2024,65 @@ void SSCache::receive(DDF& in, ostream& out) 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; @@ -1851,7 +2093,7 @@ void SSCache::receive(DDF& in, ostream& out) out << ret; } } - else if (!strcmp(in.name(),"remove::"STORAGESERVICE_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.");