+LogoutEvent* SSCache::newLogoutEvent(const Application& app) const
+{
+ if (!SPConfig::getConfig().isEnabled(SPConfig::Logging))
+ return nullptr;
+ try {
+ auto_ptr<TransactionLog::Event> event(SPConfig::getConfig().EventManager.newPlugin(LOGOUT_EVENT, nullptr));
+ LogoutEvent* logout_event = dynamic_cast<LogoutEvent*>(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<string,StoredSession*>::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<LogoutEvent> 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<LogoutEvent> 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<HTTPResponse*>(&request);
+ if (response) {
+ if (!m_outboundHeader.empty())
+ response->setResponseHeader(m_outboundHeader.c_str(), nullptr);
+ pair<string,const char*> 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<HTTPResponse*>(&request);
+ if (response) {
+ if (!m_outboundHeader.empty())
+ response->setResponseHeader(m_outboundHeader.c_str(), nullptr);
+ pair<string,const char*> 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<string,const char*> 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<string,StoredSession*>::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<SSCache*>(p);
+
+#ifndef WIN32
+ // First, let's block all signals
+ Thread::mask_all_signals();
+#endif
+
+ scoped_ptr<Mutex> 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<string> stale_keys;
+ time_t stale = time(nullptr) - pcache->m_inprocTimeout;
+
+ pcache->m_log.debug("cleanup thread running");
+
+ pcache->m_lock->rdlock();
+ for (map<string,StoredSession*>::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
+