https://issues.shibboleth.net/jira/browse/SSPCPP-447
[shibboleth/cpp-sp.git] / shibsp / impl / StorageServiceSessionCache.cpp
1 /**
2  * Licensed to the University Corporation for Advanced Internet
3  * Development, Inc. (UCAID) under one or more contributor license
4  * agreements. See the NOTICE file distributed with this work for
5  * additional information regarding copyright ownership.
6  *
7  * UCAID licenses this file to you under the Apache License,
8  * Version 2.0 (the "License"); you may not use this file except
9  * in compliance with the License. You may obtain a copy of the
10  * License at
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17  * either express or implied. See the License for the specific
18  * language governing permissions and limitations under the License.
19  */
20
21 /**
22  * StorageServiceSessionCache.cpp
23  *
24  * StorageService-based SessionCache implementation.
25  *
26  * Instead of optimizing this plugin with a buffering scheme that keeps objects around
27  * and avoids extra parsing steps, I'm assuming that systems that require such can
28  * layer their own cache plugin on top of this version either by delegating to it
29  * or using the remoting support. So this version will load sessions directly
30  * from the StorageService, instantiate enough to expose the Session API,
31  * and then delete everything when they're unlocked. All data in memory is always
32  * kept in sync with the StorageService (no lazy updates).
33  */
34
35 #include "internal.h"
36 #include "Application.h"
37 #include "exceptions.h"
38 #include "ServiceProvider.h"
39 #include "SessionCacheEx.h"
40 #include "TransactionLog.h"
41 #include "attribute/Attribute.h"
42 #include "handler/RemotedHandler.h"
43 #include "remoting/ListenerService.h"
44 #include "util/SPConstants.h"
45
46 #include <algorithm>
47 #include <boost/bind.hpp>
48 #include <boost/shared_ptr.hpp>
49 #include <xmltooling/io/HTTPRequest.h>
50 #include <xmltooling/io/HTTPResponse.h>
51 #include <xmltooling/util/DateTime.h>
52 #include <xmltooling/util/NDC.h>
53 #include <xmltooling/util/ParserPool.h>
54 #include <xmltooling/util/Threads.h>
55 #include <xmltooling/util/XMLHelper.h>
56 #include <xercesc/util/XMLUniDefs.hpp>
57
58 #ifndef SHIBSP_LITE
59 # include <saml/exceptions.h>
60 # include <saml/SAMLConfig.h>
61 # include <saml/saml2/core/Assertions.h>
62 # include <saml/saml2/metadata/Metadata.h>
63 # include <xmltooling/XMLToolingConfig.h>
64 # include <xmltooling/util/StorageService.h>
65 # include <xercesc/util/XMLStringTokenizer.hpp>
66 using namespace opensaml::saml2md;
67 #else
68 # include <ctime>
69 # include <xmltooling/util/DateTime.h>
70 #endif
71
72 using namespace shibsp;
73 using namespace opensaml;
74 using namespace xmltooling;
75 using namespace boost;
76 using namespace std;
77
78 namespace shibsp {
79
80     class StoredSession;
81     class SSCache : public SessionCacheEx
82 #ifndef SHIBSP_LITE
83         ,public virtual Remoted
84 #endif
85     {
86     public:
87         SSCache(const DOMElement* e);
88         virtual ~SSCache();
89
90 #ifndef SHIBSP_LITE
91         void receive(DDF& in, ostream& out);
92
93         void insert(
94             const Application& app,
95             const HTTPRequest& httpRequest,
96             HTTPResponse& httpResponse,
97             time_t expires,
98             const EntityDescriptor* issuer=nullptr,
99             const XMLCh* protocol=nullptr,
100             const saml2::NameID* nameid=nullptr,
101             const XMLCh* authn_instant=nullptr,
102             const XMLCh* session_index=nullptr,
103             const XMLCh* authncontext_class=nullptr,
104             const XMLCh* authncontext_decl=nullptr,
105             const vector<const Assertion*>* tokens=nullptr,
106             const vector<Attribute*>* attributes=nullptr
107             ) {
108             string dummy;
109             insert(dummy, app, httpRequest, httpResponse, expires, issuer, protocol, nameid,
110                     authn_instant, session_index, authncontext_class, authncontext_decl, tokens, attributes);
111         }
112
113         void insert(
114             string& sessionID,
115             const Application& app,
116             const HTTPRequest& httpRequest,
117             HTTPResponse& httpResponse,
118             time_t expires,
119             const EntityDescriptor* issuer=nullptr,
120             const XMLCh* protocol=nullptr,
121             const saml2::NameID* nameid=nullptr,
122             const XMLCh* authn_instant=nullptr,
123             const XMLCh* session_index=nullptr,
124             const XMLCh* authncontext_class=nullptr,
125             const XMLCh* authncontext_decl=nullptr,
126             const vector<const Assertion*>* tokens=nullptr,
127             const vector<Attribute*>* attributes=nullptr
128             );
129         vector<string>::size_type logout(
130             const Application& app,
131             const EntityDescriptor* issuer,
132             const saml2::NameID& nameid,
133             const set<string>* indexes,
134             time_t expires,
135             vector<string>& sessions
136             );
137         bool matches(
138             const Application& app,
139             const HTTPRequest& request,
140             const EntityDescriptor* issuer,
141             const saml2::NameID& nameid,
142             const set<string>* indexes
143             );
144 #endif
145         Session* find(const Application& app, const char* key, const char* client_addr=nullptr, time_t* timeout=nullptr);
146         void remove(const Application& app, const char* key);
147         void test();
148
149         string active(const Application& app, const HTTPRequest& request) {
150             if (!m_inboundHeader.empty()) {
151                 string session_id = request.getHeader(m_inboundHeader.c_str());
152                 if (!session_id.empty())
153                     return session_id;
154             }
155             pair<string,const char*> shib_cookie = app.getCookieNameProps("_shibsession_");
156             const char* session_id = request.getCookie(shib_cookie.first.c_str());
157             return (session_id ? session_id : "");
158         }
159
160         Session* find(const Application& app, const HTTPRequest& request, const char* client_addr=nullptr, time_t* timeout=nullptr) {
161             string id = active(app, request);
162             if (!id.empty())
163                 return find(app, id.c_str(), client_addr, timeout);
164             return nullptr;
165         }
166
167         Session* find(const Application& app, HTTPRequest& request, const char* client_addr=nullptr, time_t* timeout=nullptr);
168         void remove(const Application& app, const HTTPRequest& request, HTTPResponse* response=nullptr);
169
170         unsigned long getCacheTimeout(const Application& app) {
171             // Computes offset for adjusting expiration of sessions.
172             // This can either be static, or dynamic based on the per-app session timeout or lifetime.
173             if (m_cacheTimeout)
174                 return m_cacheTimeout;
175             pair<bool,unsigned int> timeout = pair<bool,unsigned int>(false, 3600);
176             const PropertySet* props = app.getPropertySet("Sessions");
177             if (props) {
178                 timeout = props->getUnsignedInt("timeout");
179                 if (!timeout.first)
180                     timeout.second = 3600;
181             }
182             // As long as one of the two factors is set, add them together.
183             if (timeout.second > 0 || m_cacheAllowance > 0)
184                 return timeout.second + m_cacheAllowance;
185
186             // If timeouts are off, and there's no cache slop set, then use the lifetime.
187             timeout = pair<bool,unsigned int>(false, 28800);
188             if (props) {
189                 timeout = props->getUnsignedInt("lifetime");
190                 if (!timeout.first || timeout.second == 0)
191                     timeout.second = 28800;
192             }
193             return timeout.second;
194         }
195
196         Category& m_log;
197         bool inproc;
198 #ifndef SHIBSP_LITE
199         StorageService* m_storage;
200         StorageService* m_storage_lite;
201 #endif
202
203     private:
204 #ifndef SHIBSP_LITE
205         // maintain back-mappings of NameID/SessionIndex -> session key
206         void insert(const char* key, time_t expires, const char* name, const char* index);
207         bool stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const;
208         LogoutEvent* newLogoutEvent(const Application& app) const;
209
210         bool m_cacheAssertions,m_reverseIndex;
211         set<xstring> m_excludedNames;
212 #endif
213         const DOMElement* m_root;         // Only valid during initialization
214         unsigned long m_inprocTimeout,m_cacheTimeout,m_cacheAllowance;
215         string m_inboundHeader,m_outboundHeader;
216
217         // inproc means we buffer sessions in memory
218         scoped_ptr<RWLock> m_lock;
219         map<string,StoredSession*> m_hashtable;
220
221         // management of buffered sessions
222         void dormant(const char* key);
223         static void* cleanup_fn(void*);
224
225         bool shutdown;
226         scoped_ptr<CondWait> shutdown_wait;
227         scoped_ptr<Thread> cleanup_thread;
228     };
229
230     class StoredSession : public virtual Session
231     {
232     public:
233         StoredSession(SSCache* cache, DDF& obj) : m_obj(obj), m_cache(cache), m_expires(0), m_lastAccess(time(nullptr)) {
234             // Check for old address format.
235             if (m_obj["client_addr"].isstring()) {
236                 const char* saddr = m_obj["client_addr"].string();
237                 DDF addrobj = m_obj["client_addr"].structure();
238                 if (saddr && *saddr) {
239                     if (strchr(saddr, ':'))
240                         addrobj.addmember("6").string(saddr);
241                     else
242                         addrobj.addmember("4").string(saddr);
243                 }
244             }
245
246             auto_ptr_XMLCh exp(m_obj["expires"].string());
247             if (exp.get()) {
248                 DateTime iso(exp.get());
249                 iso.parseDateTime();
250                 m_expires = iso.getEpoch();
251             }
252
253 #ifndef SHIBSP_LITE
254             const char* nameid = obj["nameid"].string();
255             if (nameid) {
256                 // Parse and bind the document into an XMLObject.
257                 istringstream instr(nameid);
258                 DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr);
259                 XercesJanitor<DOMDocument> janitor(doc);
260                 m_nameid.reset(saml2::NameIDBuilder::buildNameID());
261                 m_nameid->unmarshall(doc->getDocumentElement(), true);
262                 janitor.release();
263             }
264 #endif
265             if (cache->inproc)
266                 m_lock.reset(Mutex::create());
267         }
268
269         ~StoredSession() {
270             m_obj.destroy();
271             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
272         }
273
274         Lockable* lock() {
275             if (m_lock.get())
276                 m_lock->lock();
277             return this;
278         }
279         void unlock() {
280             if (m_lock.get())
281                 m_lock->unlock();
282             else
283                 delete this;
284         }
285
286         const char* getID() const {
287             return m_obj.name();
288         }
289         const char* getApplicationID() const {
290             return m_obj["application_id"].string();
291         }
292         const char* getClientAddress() const {
293             return m_obj["client_addr"].first().string();
294         }
295
296         const char* getClientAddressV4() const {
297             return m_obj["client_addr"]["4"].string();
298         }
299         const char* getClientAddressV6() const {
300             return m_obj["client_addr"]["6"].string();
301         }
302         void setClientAddress(const char* client_addr) {
303             DDF obj = m_obj["client_addr"];
304             if (!obj.isstruct())
305                 obj = m_obj.addmember("client_addr").structure();
306             if (strchr(client_addr, ':'))
307                 obj.addmember("6").string(client_addr);
308             else
309                 obj.addmember("4").string(client_addr);
310         }
311
312         const char* getEntityID() const {
313             return m_obj["entity_id"].string();
314         }
315         const char* getProtocol() const {
316             return m_obj["protocol"].string();
317         }
318         const char* getAuthnInstant() const {
319             return m_obj["authn_instant"].string();
320         }
321 #ifndef SHIBSP_LITE
322         const saml2::NameID* getNameID() const {
323             return m_nameid.get();
324         }
325 #endif
326         const char* getSessionIndex() const {
327             return m_obj["session_index"].string();
328         }
329         const char* getAuthnContextClassRef() const {
330             return m_obj["authncontext_class"].string();
331         }
332         const char* getAuthnContextDeclRef() const {
333             return m_obj["authncontext_decl"].string();
334         }
335         const vector<Attribute*>& getAttributes() const {
336             if (m_attributes.empty())
337                 unmarshallAttributes();
338             return m_attributes;
339         }
340         const multimap<string,const Attribute*>& getIndexedAttributes() const {
341             if (m_attributeIndex.empty()) {
342                 if (m_attributes.empty())
343                     unmarshallAttributes();
344                 for (vector<Attribute*>::const_iterator a = m_attributes.begin(); a != m_attributes.end(); ++a) {
345                     const vector<string>& aliases = (*a)->getAliases();
346                     for (vector<string>::const_iterator alias = aliases.begin(); alias != aliases.end(); ++alias)
347                         m_attributeIndex.insert(multimap<string,const Attribute*>::value_type(*alias, *a));
348                 }
349             }
350             return m_attributeIndex;
351         }
352         const vector<const char*>& getAssertionIDs() const {
353             if (m_ids.empty()) {
354                 DDF ids = m_obj["assertions"];
355                 DDF id = ids.first();
356                 while (id.isstring()) {
357                     m_ids.push_back(id.string());
358                     id = ids.next();
359                 }
360             }
361             return m_ids;
362         }
363
364         void validate(const Application& application, const char* client_addr, time_t* timeout);
365
366 #ifndef SHIBSP_LITE
367         void addAttributes(const vector<Attribute*>& attributes);
368         const Assertion* getAssertion(const char* id) const;
369         void addAssertion(Assertion* assertion);
370 #endif
371
372         time_t getExpiration() const { return m_expires; }
373         time_t getLastAccess() const { return m_lastAccess; }
374
375     private:
376         void unmarshallAttributes() const;
377
378         DDF m_obj;
379 #ifndef SHIBSP_LITE
380         scoped_ptr<saml2::NameID> m_nameid;
381         mutable map< string,boost::shared_ptr<Assertion> > m_tokens;
382 #endif
383         mutable vector<Attribute*> m_attributes;
384         mutable multimap<string,const Attribute*> m_attributeIndex;
385         mutable vector<const char*> m_ids;
386
387         SSCache* m_cache;
388         time_t m_expires,m_lastAccess;
389         scoped_ptr<Mutex> m_lock;
390     };
391
392     SessionCache* SHIBSP_DLLLOCAL StorageServiceCacheFactory(const DOMElement* const & e)
393     {
394         return new SSCache(e);
395     }
396 }
397
398 Session* SessionCache::find(const Application& application, HTTPRequest& request, const char* client_addr, time_t* timeout)
399 {
400     return find(application, const_cast<const HTTPRequest&>(request), client_addr, timeout);
401 }
402
403 void SHIBSP_API shibsp::registerSessionCaches()
404 {
405     SPConfig::getConfig().SessionCacheManager.registerFactory(STORAGESERVICE_SESSION_CACHE, StorageServiceCacheFactory);
406 }
407
408 Session::Session()
409 {
410 }
411
412 Session::~Session()
413 {
414 }
415
416 void StoredSession::unmarshallAttributes() const
417 {
418     Attribute* attribute;
419     DDF attrs = m_obj["attributes"];
420     DDF attr = attrs.first();
421     while (!attr.isnull()) {
422         try {
423             attribute = Attribute::unmarshall(attr);
424             m_attributes.push_back(attribute);
425             if (m_cache->m_log.isDebugEnabled())
426                 m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",
427                     attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");
428         }
429         catch (AttributeException& ex) {
430             const char* id = attr.first().name();
431             m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());
432         }
433         attr = attrs.next();
434     }
435 }
436
437 void StoredSession::validate(const Application& app, const char* client_addr, time_t* timeout)
438 {
439     time_t now = time(nullptr);
440
441     // Basic expiration?
442     if (m_expires > 0) {
443         if (now > m_expires) {
444             m_cache->m_log.info("session expired (ID: %s)", getID());
445             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
446         }
447     }
448
449     // Address check?
450     if (client_addr) {
451         const char* saddr = nullptr;
452         if (strchr(client_addr, ':'))
453             saddr = getClientAddressV6();
454         else
455             saddr = getClientAddressV4();
456         if (saddr && *saddr) {
457             if (!XMLString::equals(saddr, client_addr)) {
458                 m_cache->m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr);
459                 throw RetryableProfileException(
460                     "Your IP address ($1) does not match the address recorded at the time the session was established.",
461                     params(1, client_addr)
462                     );
463             }
464             client_addr = nullptr;  // clear out parameter as signal that session need not be updated below
465         }
466         else {
467             m_cache->m_log.info("session (%s) not yet bound to client address type, binding it to (%s)", getID(), client_addr);
468         }
469     }
470
471     if (!timeout && !client_addr)
472         return;
473
474     if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
475         DDF in("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;
476         DDFJanitor jin(in);
477         in.structure();
478         in.addmember("key").string(getID());
479         in.addmember("version").integer(m_obj["version"].integer());
480         in.addmember("application_id").string(app.getId());
481         if (client_addr)    // signals we need to bind an additional address to the session
482             in.addmember("client_addr").string(client_addr);
483         if (timeout && *timeout) {
484             // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
485 #ifndef HAVE_GMTIME_R
486             struct tm* ptime = gmtime(timeout);
487 #else
488             struct tm res;
489             struct tm* ptime = gmtime_r(timeout,&res);
490 #endif
491             char timebuf[32];
492             strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
493             in.addmember("timeout").string(timebuf);
494         }
495
496         try {
497             out=app.getServiceProvider().getListenerService()->send(in);
498         }
499         catch (...) {
500             out.destroy();
501             throw;
502         }
503
504         if (out.isstruct()) {
505             // We got an updated record back.
506             m_cache->m_log.debug("session updated, reconstituting it");
507             m_ids.clear();
508             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
509             m_attributes.clear();
510             m_attributeIndex.clear();
511             m_obj.destroy();
512             m_obj = out;
513         }
514     }
515     else {
516 #ifndef SHIBSP_LITE
517         if (!m_cache->m_storage)
518             throw ConfigurationException("Session touch requires a StorageService.");
519
520         // Versioned read, since we already have the data in hand if it's current.
521         string record;
522         time_t lastAccess;
523         int curver = m_obj["version"].integer();
524         int ver = m_cache->m_storage->readText(getID(), "session", &record, &lastAccess, curver);
525         if (ver == 0) {
526             m_cache->m_log.warn("unsuccessful versioned read of session (ID: %s), cache out of sync?", getID());
527             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
528         }
529
530         if (timeout) {
531             // Adjust for expiration to recover last access time and check timeout.
532             unsigned long cacheTimeout = m_cache->getCacheTimeout(app);
533             lastAccess -= cacheTimeout;
534             if (*timeout > 0 && now - lastAccess >= *timeout) {
535                 m_cache->m_log.info("session timed out (ID: %s)", getID());
536                 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
537             }
538
539             // Update storage expiration, if possible.
540             try {
541                 m_cache->m_storage->updateContext(getID(), now + cacheTimeout);
542             }
543             catch (std::exception& ex) {
544                 m_cache->m_log.error("failed to update session expiration: %s", ex.what());
545             }
546         }
547
548         if (ver > curver) {
549             // We got an updated record back.
550             DDF newobj;
551             istringstream in(record);
552             in >> newobj;
553             m_ids.clear();
554             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
555             m_attributes.clear();
556             m_attributeIndex.clear();
557             m_obj.destroy();
558             m_obj = newobj;
559         }
560
561         // We may need to write back a new address into the session using a versioned update loop.
562         if (client_addr) {
563             do {
564                 const char* saddr = nullptr;
565                 if (strchr(client_addr, ':'))
566                     saddr = getClientAddressV6();
567                 else
568                     saddr = getClientAddressV4();
569                 // Something snuck in and bound the session to this address type, so it better match what we have.
570                 if (saddr) {
571                     if (!XMLString::equals(saddr, client_addr)) {
572                         m_cache->m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr);
573                         throw RetryableProfileException(
574                             "Your IP address ($1) does not match the address recorded at the time the session was established.",
575                             params(1, client_addr)
576                             );
577                     }
578                     break;  // No need to update.
579                 }
580                 else {
581                     // Bind it into the session.
582                     setClientAddress(client_addr);
583                 }
584
585                 // Tentatively increment the version.
586                 m_obj["version"].integer(m_obj["version"].integer() + 1);
587
588                 ostringstream str;
589                 str << m_obj;
590                 record = str.str();
591
592                 try {
593                     ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer() - 1);
594                 }
595                 catch (std::exception&) {
596                     m_obj["version"].integer(m_obj["version"].integer() - 1);
597                     throw;
598                 }
599
600                 if (ver <= 0) {
601                     m_obj["version"].integer(m_obj["version"].integer() - 1);
602                 }
603
604                 if (!ver) {
605                     // Fatal problem with update.
606                     m_cache->m_log.error("updateText failed on StorageService for session (%s)", getID());
607                     throw IOException("Unable to update stored session.");
608                 }
609                 else if (ver < 0) {
610                     // Out of sync.
611                     m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
612                     ver = m_cache->m_storage->readText(getID(), "session", &record, nullptr);
613                     if (!ver) {
614                         m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
615                         throw IOException("Unable to read back stored session.");
616                     }
617
618                     // Reset object.
619                     DDF newobj;
620                     istringstream in(record);
621                     in >> newobj;
622
623                     m_ids.clear();
624                     for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
625                     m_attributes.clear();
626                     m_attributeIndex.clear();
627                     newobj["version"].integer(ver);
628                     m_obj.destroy();
629                     m_obj = newobj;
630
631                     ver = -1;
632                 }
633             } while (ver < 0); // negative indicates a sync issue so we retry
634         }
635 #else
636         throw ConfigurationException("Session touch requires a StorageService.");
637 #endif
638     }
639
640     m_lastAccess = now;
641 }
642
643 #ifndef SHIBSP_LITE
644
645 void StoredSession::addAttributes(const vector<Attribute*>& attributes)
646 {
647 #ifdef _DEBUG
648     xmltooling::NDC ndc("addAttributes");
649 #endif
650
651     if (!m_cache->m_storage)
652         throw ConfigurationException("Session modification requires a StorageService.");
653
654     m_cache->m_log.debug("adding attributes to session (%s)", getID());
655
656     int ver;
657     do {
658         DDF attr;
659         DDF attrs = m_obj["attributes"];
660         if (!attrs.islist())
661             attrs = m_obj.addmember("attributes").list();
662         for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a) {
663             attr = (*a)->marshall();
664             attrs.add(attr);
665         }
666
667         // Tentatively increment the version.
668         m_obj["version"].integer(m_obj["version"].integer()+1);
669
670         ostringstream str;
671         str << m_obj;
672         string record(str.str());
673
674         try {
675             ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);
676         }
677         catch (std::exception&) {
678             // Roll back modification to record.
679             m_obj["version"].integer(m_obj["version"].integer()-1);
680             vector<Attribute*>::size_type count = attributes.size();
681             while (count--)
682                 attrs.last().destroy();
683             throw;
684         }
685
686         if (ver <= 0) {
687             // Roll back modification to record.
688             m_obj["version"].integer(m_obj["version"].integer()-1);
689             vector<Attribute*>::size_type count = attributes.size();
690             while (count--)
691                 attrs.last().destroy();
692         }
693         if (!ver) {
694             // Fatal problem with update.
695             throw IOException("Unable to update stored session.");
696         }
697         else if (ver < 0) {
698             // Out of sync.
699             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
700             ver = m_cache->m_storage->readText(getID(), "session", &record, nullptr);
701             if (!ver) {
702                 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
703                 throw IOException("Unable to read back stored session.");
704             }
705
706             // Reset object.
707             DDF newobj;
708             istringstream in(record);
709             in >> newobj;
710
711             m_ids.clear();
712             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
713             m_attributes.clear();
714             m_attributeIndex.clear();
715             newobj["version"].integer(ver);
716             m_obj.destroy();
717             m_obj = newobj;
718
719             ver = -1;
720         }
721     } while (ver < 0);  // negative indicates a sync issue so we retry
722
723     // We own them now, so clean them up.
724     for_each(attributes.begin(), attributes.end(), xmltooling::cleanup<Attribute>());
725 }
726
727 const Assertion* StoredSession::getAssertion(const char* id) const
728 {
729     if (!m_cache->m_storage)
730         throw ConfigurationException("Assertion retrieval requires a StorageService.");
731
732     map< string,boost::shared_ptr<Assertion> >::const_iterator i = m_tokens.find(id);
733     if (i != m_tokens.end())
734         return i->second.get();
735
736     string tokenstr;
737     if (!m_cache->m_storage->readText(getID(), id, &tokenstr, nullptr))
738         throw FatalProfileException("Assertion not found in cache.");
739
740     // Parse and bind the document into an XMLObject.
741     istringstream instr(tokenstr);
742     DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr);
743     XercesJanitor<DOMDocument> janitor(doc);
744     boost::shared_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));
745     janitor.release();
746     
747     boost::shared_ptr<Assertion> token = dynamic_pointer_cast<Assertion,XMLObject>(xmlObject);
748     if (!token)
749         throw FatalProfileException("Request for cached assertion returned an unknown object type.");
750
751     m_tokens[id] = token;
752     return token.get();
753 }
754
755 void StoredSession::addAssertion(Assertion* assertion)
756 {
757 #ifdef _DEBUG
758     xmltooling::NDC ndc("addAssertion");
759 #endif
760
761     if (!m_cache->m_storage)
762         throw ConfigurationException("Session modification requires a StorageService.");
763     else if (!assertion)
764         throw FatalProfileException("Unknown object type passed to session for storage.");
765
766     auto_ptr_char id(assertion->getID());
767     if (!id.get() || !*id.get())
768         throw IOException("Assertion did not carry an ID.");
769     else if (strlen(id.get()) > m_cache->m_storage->getCapabilities().getKeySize())
770         throw IOException("Assertion ID ($1) exceeds allowable storage key size.", params(1, id.get()));
771
772     m_cache->m_log.debug("adding assertion (%s) to session (%s)", id.get(), getID());
773
774     time_t exp;
775     if (!m_cache->m_storage->readText(getID(), "session", nullptr, &exp))
776         throw IOException("Unable to load expiration time for stored session.");
777
778     ostringstream tokenstr;
779     tokenstr << *assertion;
780     if (!m_cache->m_storage->createText(getID(), id.get(), tokenstr.str().c_str(), exp))
781         throw IOException("Attempted to insert duplicate assertion ID into session.");
782
783     int ver;
784     do {
785         DDF token = DDF(nullptr).string(id.get());
786         m_obj["assertions"].add(token);
787
788         // Tentatively increment the version.
789         m_obj["version"].integer(m_obj["version"].integer() + 1);
790
791         ostringstream str;
792         str << m_obj;
793         string record(str.str());
794
795         try {
796             ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);
797         }
798         catch (std::exception&) {
799             token.destroy();
800             m_obj["version"].integer(m_obj["version"].integer() - 1);
801             m_cache->m_storage->deleteText(getID(), id.get());
802             throw;
803         }
804
805         if (ver <= 0) {
806             token.destroy();
807             m_obj["version"].integer(m_obj["version"].integer()-1);
808         }
809         if (!ver) {
810             // Fatal problem with update.
811             m_cache->m_log.error("updateText failed on StorageService for session (%s)", getID());
812             m_cache->m_storage->deleteText(getID(), id.get());
813             throw IOException("Unable to update stored session.");
814         }
815         else if (ver < 0) {
816             // Out of sync.
817             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
818             ver = m_cache->m_storage->readText(getID(), "session", &record, nullptr);
819             if (!ver) {
820                 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
821                 m_cache->m_storage->deleteText(getID(), id.get());
822                 throw IOException("Unable to read back stored session.");
823             }
824
825             // Reset object.
826             DDF newobj;
827             istringstream in(record);
828             in >> newobj;
829
830             m_ids.clear();
831             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
832             m_attributes.clear();
833             m_attributeIndex.clear();
834             newobj["version"].integer(ver);
835             m_obj.destroy();
836             m_obj = newobj;
837
838             ver = -1;
839         }
840     } while (ver < 0); // negative indicates a sync issue so we retry
841
842     m_ids.clear();
843     delete assertion;
844 }
845
846 #endif
847
848 SessionCache::SessionCache()
849 {
850 }
851
852 SessionCache::~SessionCache()
853 {
854 }
855
856 #ifndef SHIBSP_LITE
857
858 void SessionCache::insert(
859     string& sessionID,
860     const Application& app,
861     const HTTPRequest& httpRequest,
862     HTTPResponse& httpResponse,
863     time_t expires,
864     const EntityDescriptor* issuer,
865     const XMLCh* protocol,
866     const saml2::NameID* nameid,
867     const XMLCh* authn_instant,
868     const XMLCh* session_index,
869     const XMLCh* authncontext_class,
870     const XMLCh* authncontext_decl,
871     const vector<const Assertion*>* tokens,
872     const vector<Attribute*>* attributes
873     ) {
874     return insert(app, httpRequest, httpResponse, expires, issuer, protocol, nameid,
875             authn_instant, session_index, authncontext_class, authncontext_decl, tokens, attributes);
876 }
877
878 #endif
879
880 SessionCacheEx::SessionCacheEx()
881 {
882 }
883
884 SessionCacheEx::~SessionCacheEx()
885 {
886 }
887
888 SSCache::SSCache(const DOMElement* e)
889     : m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), inproc(true),
890 #ifndef SHIBSP_LITE
891       m_storage(nullptr), m_storage_lite(nullptr), m_cacheAssertions(true), m_reverseIndex(true),
892 #endif
893       m_root(e), m_inprocTimeout(900), m_cacheTimeout(0), m_cacheAllowance(0), shutdown(false)
894 {
895     SPConfig& conf = SPConfig::getConfig();
896     inproc = conf.isEnabled(SPConfig::InProcess);
897
898     static const XMLCh cacheAllowance[] =       UNICODE_LITERAL_14(c,a,c,h,e,A,l,l,o,w,a,n,c,e);
899     static const XMLCh cacheAssertions[] =      UNICODE_LITERAL_15(c,a,c,h,e,A,s,s,e,r,t,i,o,n,s);
900     static const XMLCh cacheTimeout[] =         UNICODE_LITERAL_12(c,a,c,h,e,T,i,m,e,o,u,t);
901     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);
902     static const XMLCh inprocTimeout[] =        UNICODE_LITERAL_13(i,n,p,r,o,c,T,i,m,e,o,u,t);
903     static const XMLCh inboundHeader[] =        UNICODE_LITERAL_13(i,n,b,o,u,n,d,H,e,a,d,e,r);
904     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);
905     static const XMLCh outboundHeader[] =       UNICODE_LITERAL_14(o,u,t,b,o,u,n,d,H,e,a,d,e,r);
906     static const XMLCh _StorageService[] =      UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e);
907     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);
908
909     if (e && e->hasAttributeNS(nullptr, cacheTimeout)) {
910         m_log.warn("cacheTimeout property is deprecated in favor of cacheAllowance (see documentation)");
911         m_cacheTimeout = XMLHelper::getAttrInt(e, 0, cacheTimeout);
912     }
913     m_cacheAllowance = XMLHelper::getAttrInt(e, 0, cacheAllowance);
914     if (inproc)
915         m_inprocTimeout = XMLHelper::getAttrInt(e, 900, inprocTimeout);
916     m_inboundHeader = XMLHelper::getAttrString(e, nullptr, inboundHeader);
917     if (!m_inboundHeader.empty())
918         RemotedHandler::addRemotedHeader(m_inboundHeader.c_str());
919     m_outboundHeader = XMLHelper::getAttrString(e, nullptr, outboundHeader);
920
921 #ifndef SHIBSP_LITE
922     if (conf.isEnabled(SPConfig::OutOfProcess)) {
923         string ssid(XMLHelper::getAttrString(e, nullptr, _StorageService));
924         if (!ssid.empty()) {
925             m_storage = conf.getServiceProvider()->getStorageService(ssid.c_str());
926             if (m_storage)
927                 m_log.info("bound to StorageService (%s)", ssid.c_str());
928             else
929                 m_log.warn("specified StorageService (%s) not found", ssid.c_str());
930         }
931         if (!m_storage) {
932             m_storage = conf.getServiceProvider()->getStorageService(nullptr);
933             if (m_storage)
934                 m_log.info("bound to arbitrary StorageService");
935             else
936                 throw ConfigurationException("SessionCache unable to locate StorageService, check configuration.");
937         }
938
939         ssid = XMLHelper::getAttrString(e, nullptr, _StorageServiceLite);
940         if (!ssid.empty()) {
941             m_storage_lite = conf.getServiceProvider()->getStorageService(ssid.c_str());
942             if (m_storage_lite)
943                 m_log.info("bound to 'lite' StorageService (%s)", ssid.c_str());
944             else
945                 m_log.warn("specified 'lite' StorageService (%s) not found", ssid.c_str());
946         }
947         if (!m_storage_lite) {
948             m_log.info("StorageService for 'lite' use not set, using standard StorageService");
949             m_storage_lite = m_storage;
950         }
951
952         m_cacheAssertions = XMLHelper::getAttrBool(e, true, cacheAssertions);
953         m_reverseIndex = XMLHelper::getAttrBool(e, true, maintainReverseIndex);
954         const XMLCh* excludedNames = e ? e->getAttributeNS(nullptr, excludeReverseIndex) : nullptr;
955         if (excludedNames && *excludedNames) {
956             XMLStringTokenizer toks(excludedNames);
957             while (toks.hasMoreTokens())
958                 m_excludedNames.insert(toks.nextToken());
959         }
960     }
961 #endif
962
963     ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
964     if (inproc) {
965         if (!conf.isEnabled(SPConfig::OutOfProcess) && !listener)
966             throw ConfigurationException("SessionCache requires a ListenerService, but none available.");
967         m_lock.reset(RWLock::create());
968         shutdown_wait.reset(CondWait::create());
969         cleanup_thread.reset(Thread::create(&cleanup_fn, this));
970     }
971 #ifndef SHIBSP_LITE
972     else {
973         if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
974             listener->regListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
975             listener->regListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
976             listener->regListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
977         }
978         else {
979             m_log.info("no ListenerService available, cache remoting disabled");
980         }
981     }
982 #endif
983 }
984
985 SSCache::~SSCache()
986 {
987     if (inproc) {
988         // Shut down the cleanup thread and let it know...
989         shutdown = true;
990         if (shutdown_wait.get())
991             shutdown_wait->signal();
992         if (cleanup_thread.get())
993             cleanup_thread->join(nullptr);
994
995         for_each(m_hashtable.begin(),m_hashtable.end(),cleanup_pair<string,StoredSession>());
996     }
997 #ifndef SHIBSP_LITE
998     else {
999         SPConfig& conf = SPConfig::getConfig();
1000         ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
1001         if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
1002             listener->unregListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
1003             listener->unregListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
1004             listener->unregListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
1005         }
1006     }
1007 #endif
1008 }
1009
1010 #ifndef SHIBSP_LITE
1011
1012 void SSCache::test()
1013 {
1014     XMLCh* wide = SAMLConfig::getConfig().generateIdentifier();
1015     auto_ptr_char temp(wide);
1016     XMLString::release(&wide);
1017     m_storage->createString("SessionCacheTest", temp.get(), "Test", time(nullptr) + 60);
1018     m_storage->deleteString("SessionCacheTest", temp.get());
1019 }
1020
1021 void SSCache::insert(const char* key, time_t expires, const char* name, const char* index)
1022 {
1023     string dup;
1024     unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize();
1025     if (strlen(name) > storageLimit) {
1026         dup = string(name).substr(0, storageLimit);
1027         name = dup.c_str();
1028     }
1029
1030     DDF obj;
1031     DDFJanitor jobj(obj);
1032
1033     // Since we can't guarantee uniqueness, check for an existing record.
1034     string record;
1035     time_t recordexp;
1036     int ver = m_storage_lite->readText("NameID", name, &record, &recordexp);
1037     if (ver > 0) {
1038         // Existing record, so we need to unmarshall it.
1039         istringstream in(record);
1040         in >> obj;
1041     }
1042     else {
1043         // New record.
1044         obj = DDF(nullptr).structure();
1045     }
1046
1047     if (!index || !*index)
1048         index = "_shibnull";
1049     DDF sessions = obj.addmember(index);
1050     if (!sessions.islist())
1051         sessions.list();
1052     DDF session = DDF(nullptr).string(key);
1053     sessions.add(session);
1054
1055     // Remarshall the record.
1056     ostringstream out;
1057     out << obj;
1058
1059     // Try and store it back...
1060     if (ver > 0) {
1061         ver = m_storage_lite->updateText("NameID", name, out.str().c_str(), max(expires, recordexp), ver);
1062         if (ver <= 0) {
1063             // Out of sync, or went missing, so retry.
1064             return insert(key, expires, name, index);
1065         }
1066     }
1067     else if (!m_storage_lite->createText("NameID", name, out.str().c_str(), expires)) {
1068         // Hit a dup, so just retry, hopefully hitting the other branch.
1069         return insert(key, expires, name, index);
1070     }
1071 }
1072
1073 void SSCache::insert(
1074     string& sessionID,
1075     const Application& app,
1076     const HTTPRequest& httpRequest,
1077     HTTPResponse& httpResponse,
1078     time_t expires,
1079     const saml2md::EntityDescriptor* issuer,
1080     const XMLCh* protocol,
1081     const saml2::NameID* nameid,
1082     const XMLCh* authn_instant,
1083     const XMLCh* session_index,
1084     const XMLCh* authncontext_class,
1085     const XMLCh* authncontext_decl,
1086     const vector<const Assertion*>* tokens,
1087     const vector<Attribute*>* attributes
1088     )
1089 {
1090 #ifdef _DEBUG
1091     xmltooling::NDC ndc("insert");
1092 #endif
1093     if (!m_storage)
1094         throw ConfigurationException("SessionCache insertion requires a StorageService.");
1095
1096     m_log.debug("creating new session");
1097
1098     time_t now = time(nullptr);
1099     auto_ptr_char index(session_index);
1100     auto_ptr_char entity_id(issuer ? issuer->getEntityID() : nullptr);
1101     auto_ptr_char name(nameid ? nameid->getName() : nullptr);
1102
1103     if (name.get() && *name.get()) {
1104         // Check for a pending logout.
1105         unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize();
1106         string namebuf = name.get();
1107         if (namebuf.length() > storageLimit)
1108             namebuf = namebuf.substr(0, storageLimit);
1109         string pending;
1110         int ver = m_storage_lite->readText("Logout", namebuf.c_str(), &pending);
1111         if (ver > 0) {
1112             DDF pendobj;
1113             DDFJanitor jpend(pendobj);
1114             istringstream pstr(pending);
1115             pstr >> pendobj;
1116             // IdP.SP.index contains logout expiration, if any.
1117             DDF deadmenwalking = pendobj[issuer ? entity_id.get() : "_shibnull"][app.getRelyingParty(issuer)->getString("entityID").second];
1118             const char* logexpstr = deadmenwalking[session_index ? index.get() : "_shibnull"].string();
1119             if (!logexpstr && session_index)    // we tried an exact session match, now try for nullptr
1120                 logexpstr = deadmenwalking["_shibnull"].string();
1121             if (logexpstr) {
1122                 auto_ptr_XMLCh dt(logexpstr);
1123                 DateTime dtobj(dt.get());
1124                 dtobj.parseDateTime();
1125                 time_t logexp = dtobj.getEpoch();
1126                 if (now - XMLToolingConfig::getConfig().clock_skew_secs < logexp)
1127                     throw FatalProfileException("A logout message from your identity provider has blocked your login attempt.");
1128             }
1129         }
1130     }
1131
1132     XMLCh* widekey = SAMLConfig::getConfig().generateIdentifier();
1133     auto_ptr_char key(widekey);
1134     XMLString::release(&widekey);
1135
1136     // Store session properties in DDF.
1137     DDF obj = DDF(key.get()).structure();
1138     DDFJanitor entryobj(obj);
1139     obj.addmember("version").integer(1);
1140     obj.addmember("application_id").string(app.getId());
1141
1142     // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
1143 #ifndef HAVE_GMTIME_R
1144     struct tm* ptime=gmtime(&expires);
1145 #else
1146     struct tm res;
1147     struct tm* ptime=gmtime_r(&expires,&res);
1148 #endif
1149     char timebuf[32];
1150     strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
1151     obj.addmember("expires").string(timebuf);
1152
1153     string caddr(httpRequest.getRemoteAddr());
1154     if (!caddr.empty()) {
1155         DDF addrobj = obj.addmember("client_addr").structure();
1156         if (caddr.find(':') != string::npos)
1157             addrobj.addmember("6").string(caddr.c_str());
1158         else
1159             addrobj.addmember("4").string(caddr.c_str());
1160     }
1161
1162     if (issuer)
1163         obj.addmember("entity_id").string(entity_id.get());
1164     if (protocol) {
1165         auto_ptr_char prot(protocol);
1166         obj.addmember("protocol").string(prot.get());
1167     }
1168     if (authn_instant) {
1169         auto_ptr_char instant(authn_instant);
1170         obj.addmember("authn_instant").string(instant.get());
1171     }
1172     if (session_index)
1173         obj.addmember("session_index").string(index.get());
1174     if (authncontext_class) {
1175         auto_ptr_char ac(authncontext_class);
1176         obj.addmember("authncontext_class").string(ac.get());
1177     }
1178     if (authncontext_decl) {
1179         auto_ptr_char ad(authncontext_decl);
1180         obj.addmember("authncontext_decl").string(ad.get());
1181     }
1182
1183     if (nameid) {
1184         ostringstream namestr;
1185         namestr << *nameid;
1186         obj.addmember("nameid").string(namestr.str().c_str());
1187     }
1188
1189     if (tokens && m_cacheAssertions) {
1190         obj.addmember("assertions").list();
1191         for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
1192             auto_ptr_char tokenid((*t)->getID());
1193             DDF tokid = DDF(nullptr).string(tokenid.get());
1194             obj["assertions"].add(tokid);
1195         }
1196     }
1197
1198     if (attributes) {
1199         DDF attr;
1200         DDF attrlist = obj.addmember("attributes").list();
1201         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) {
1202             attr = (*a)->marshall();
1203             attrlist.add(attr);
1204         }
1205     }
1206
1207     ostringstream record;
1208     record << obj;
1209
1210     m_log.debug("storing new session...");
1211     unsigned long cacheTimeout = getCacheTimeout(app);
1212     if (!m_storage->createText(key.get(), "session", record.str().c_str(), now + cacheTimeout))
1213         throw FatalProfileException("Attempted to create a session with a duplicate key.");
1214
1215     // Store the reverse mapping for logout.
1216     if (nameid && m_reverseIndex && (m_excludedNames.size() == 0 || m_excludedNames.count(nameid->getName()) == 0)) {
1217         try {
1218             insert(key.get(), expires, name.get(), index.get());
1219         }
1220         catch (std::exception& ex) {
1221             m_log.error("error storing back mapping of NameID for logout: %s", ex.what());
1222         }
1223     }
1224
1225     if (tokens && m_cacheAssertions) {
1226         try {
1227             for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
1228                 ostringstream tokenstr;
1229                 tokenstr << *(*t);
1230                 auto_ptr_char tokenid((*t)->getID());
1231                 if (!tokenid.get() || !*tokenid.get() || strlen(tokenid.get()) > m_storage->getCapabilities().getKeySize())
1232                     throw IOException("Assertion ID is missing or exceeds key size of storage service.");
1233                 else if (!m_storage->createText(key.get(), tokenid.get(), tokenstr.str().c_str(), now + cacheTimeout))
1234                     throw IOException("Duplicate assertion ID ($1)", params(1, tokenid.get()));
1235             }
1236         }
1237         catch (std::exception& ex) {
1238             m_log.error("error storing assertion along with session: %s", ex.what());
1239         }
1240     }
1241
1242     const char* pid = obj["entity_id"].string();
1243     const char* prot = obj["protocol"].string();
1244     m_log.info("new session created: ID (%s) IdP (%s) Protocol(%s) Address (%s)",
1245         key.get(), pid ? pid : "none", prot ? prot : "none", httpRequest.getRemoteAddr().c_str());
1246
1247     if (!m_outboundHeader.empty())
1248         httpResponse.setResponseHeader(m_outboundHeader.c_str(), key.get());
1249
1250     time_t cookieLifetime = 0;
1251     pair<string,const char*> shib_cookie = app.getCookieNameProps("_shibsession_", &cookieLifetime);
1252     string k(key.get());
1253     k += shib_cookie.second;
1254
1255     if (cookieLifetime > 0) {
1256         cookieLifetime += now;
1257 #ifndef HAVE_GMTIME_R
1258         ptime=gmtime(&cookieLifetime);
1259 #else
1260         ptime=gmtime_r(&cookieLifetime,&res);
1261 #endif
1262         char cookietimebuf[64];
1263         strftime(cookietimebuf,64,"; expires=%a, %d %b %Y %H:%M:%S GMT",ptime);
1264         k += cookietimebuf;
1265     }
1266
1267     httpResponse.setCookie(shib_cookie.first.c_str(), k.c_str());
1268     sessionID = key.get();
1269 }
1270
1271 bool SSCache::matches(
1272     const Application& app,
1273     const xmltooling::HTTPRequest& request,
1274     const saml2md::EntityDescriptor* issuer,
1275     const saml2::NameID& nameid,
1276     const set<string>* indexes
1277     )
1278 {
1279     auto_ptr_char entityID(issuer ? issuer->getEntityID() : nullptr);
1280     try {
1281         Session* session = find(app, request);
1282         if (session) {
1283             Locker locker(session, false);
1284             if (XMLString::equals(session->getEntityID(), entityID.get()) && session->getNameID() &&
1285                     stronglyMatches(issuer->getEntityID(), app.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) {
1286                 return (!indexes || indexes->empty() || (session->getSessionIndex() ? (indexes->count(session->getSessionIndex())>0) : false));
1287             }
1288         }
1289     }
1290     catch (std::exception& ex) {
1291         m_log.error("error while matching session: %s", ex.what());
1292     }
1293     return false;
1294 }
1295
1296 vector<string>::size_type SSCache::logout(
1297     const Application& app,
1298     const saml2md::EntityDescriptor* issuer,
1299     const saml2::NameID& nameid,
1300     const set<string>* indexes,
1301     time_t expires,
1302     vector<string>& sessionsKilled
1303     )
1304 {
1305 #ifdef _DEBUG
1306     xmltooling::NDC ndc("logout");
1307 #endif
1308
1309     if (!m_storage)
1310         throw ConfigurationException("SessionCache logout requires a StorageService.");
1311
1312     auto_ptr_char entityID(issuer ? issuer->getEntityID() : nullptr);
1313     auto_ptr_char name(nameid.getName());
1314
1315     m_log.info("request to logout sessions from (%s) for (%s)", entityID.get() ? entityID.get() : "unknown", name.get());
1316
1317     unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize();
1318     if (strlen(name.get()) > storageLimit)
1319         const_cast<char*>(name.get())[storageLimit] = 0;
1320
1321     DDF obj;
1322     DDFJanitor jobj(obj);
1323     string record;
1324     int ver;
1325
1326     if (expires) {
1327         // Record the logout to prevent post-delivered assertions.
1328         // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
1329 #ifndef HAVE_GMTIME_R
1330         struct tm* ptime=gmtime(&expires);
1331 #else
1332         struct tm res;
1333         struct tm* ptime=gmtime_r(&expires,&res);
1334 #endif
1335         char timebuf[32];
1336         strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
1337
1338         time_t oldexp = 0;
1339         ver = m_storage_lite->readText("Logout", name.get(), &record, &oldexp);
1340         if (ver > 0) {
1341             istringstream lin(record);
1342             lin >> obj;
1343         }
1344         else {
1345             obj = DDF(nullptr).structure();
1346         }
1347
1348         // Structure is keyed by the IdP and SP, with a member per session index containing the expiration.
1349         DDF root = obj.addmember(issuer ? entityID.get() : "_shibnull").addmember(app.getRelyingParty(issuer)->getString("entityID").second);
1350         if (indexes) {
1351             for (set<string>::const_iterator x = indexes->begin(); x!=indexes->end(); ++x)
1352                 root.addmember(x->c_str()).string(timebuf);
1353         }
1354         else {
1355             root.addmember("_shibnull").string(timebuf);
1356         }
1357
1358         // Write it back.
1359         ostringstream lout;
1360         lout << obj;
1361
1362         if (ver > 0) {
1363             ver = m_storage_lite->updateText("Logout", name.get(), lout.str().c_str(), max(expires, oldexp), ver);
1364             if (ver <= 0) {
1365                 // Out of sync, or went missing, so retry.
1366                 return logout(app, issuer, nameid, indexes, expires, sessionsKilled);
1367             }
1368         }
1369         else if (!m_storage_lite->createText("Logout", name.get(), lout.str().c_str(), expires)) {
1370             // Hit a dup, so just retry, hopefully hitting the other branch.
1371             return logout(app, issuer, nameid, indexes, expires, sessionsKilled);
1372         }
1373
1374         obj.destroy();
1375         record.erase();
1376     }
1377
1378     if (!m_reverseIndex) {
1379         m_log.error("cannot support logout because maintainReverseIndex property is turned off");
1380         throw ConfigurationException("Logout is unsupported by the session cache configuration.");
1381     }
1382
1383     // Read in potentially matching sessions.
1384     ver = m_storage_lite->readText("NameID", name.get(), &record);
1385     if (ver == 0) {
1386         m_log.debug("no active sessions to logout for supplied issuer and subject");
1387         return 0;
1388     }
1389
1390     istringstream in(record);
1391     in >> obj;
1392
1393     // The record contains child lists for each known session index.
1394     DDF key;
1395     DDF sessions = obj.first();
1396     while (sessions.islist()) {
1397         if (!indexes || indexes->empty() || indexes->count(sessions.name())) {
1398             key = sessions.first();
1399             while (key.isstring()) {
1400                 // Fetch the session for comparison.
1401                 Session* session = nullptr;
1402                 try {
1403                     session = find(app, key.string());
1404                 }
1405                 catch (std::exception& ex) {
1406                     m_log.error("error locating session (%s): %s", key.string(), ex.what());
1407                 }
1408
1409                 if (session) {
1410                     Locker locker(session, false);
1411                     // Same issuer?
1412                     if (XMLString::equals(session->getEntityID(), entityID.get())) {
1413                         // Same NameID?
1414                         if (stronglyMatches(issuer->getEntityID(), app.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) {
1415                             sessionsKilled.push_back(key.string());
1416                             key.destroy();
1417                         }
1418                         else {
1419                             m_log.debug("session (%s) contained a non-matching NameID, leaving it alone", key.string());
1420                         }
1421                     }
1422                     else {
1423                         m_log.debug("session (%s) established by different IdP, leaving it alone", key.string());
1424                     }
1425                 }
1426                 else {
1427                     // Session may already be gone, or it may be associated with a different application.
1428                     // To be conservative, we'll leave it alone. This isn't really increasing our security
1429                     // risk, because if we can't lookup the session, it's unlikely the calling logout code
1430                     // can either, so there's no chance of removing the session anyway.
1431                     m_log.warn("session (%s) not accessible for logout, may be gone, or associated with a different application", key.string());
1432                 }
1433                 key = sessions.next();
1434             }
1435
1436             // No sessions left for this index?
1437             if (sessions.first().isnull())
1438                 sessions.destroy();
1439         }
1440         sessions = obj.next();
1441     }
1442
1443     if (obj.first().isnull())
1444         obj.destroy();
1445
1446     // If possible, write back the mapping record (this isn't crucial).
1447     try {
1448         if (obj.isnull()) {
1449             m_storage_lite->deleteText("NameID", name.get());
1450         }
1451         else if (!sessionsKilled.empty()) {
1452             ostringstream out;
1453             out << obj;
1454             if (m_storage_lite->updateText("NameID", name.get(), out.str().c_str(), 0, ver) <= 0)
1455                 m_log.warn("logout mapping record changed behind us, leaving it alone");
1456         }
1457     }
1458     catch (std::exception& ex) {
1459         m_log.error("error updating logout mapping record: %s", ex.what());
1460     }
1461
1462     return sessionsKilled.size();
1463 }
1464
1465 bool SSCache::stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const
1466 {
1467     if (!XMLString::equals(n1.getName(), n2.getName()))
1468         return false;
1469
1470     const XMLCh* s1 = n1.getFormat();
1471     const XMLCh* s2 = n2.getFormat();
1472     if (!s1 || !*s1)
1473         s1 = saml2::NameID::UNSPECIFIED;
1474     if (!s2 || !*s2)
1475         s2 = saml2::NameID::UNSPECIFIED;
1476     if (!XMLString::equals(s1,s2))
1477         return false;
1478
1479     s1 = n1.getNameQualifier();
1480     s2 = n2.getNameQualifier();
1481     if (!s1 || !*s1)
1482         s1 = idp;
1483     if (!s2 || !*s2)
1484         s2 = idp;
1485     if (!XMLString::equals(s1,s2))
1486         return false;
1487
1488     s1 = n1.getSPNameQualifier();
1489     s2 = n2.getSPNameQualifier();
1490     if (!s1 || !*s1)
1491         s1 = sp;
1492     if (!s2 || !*s2)
1493         s2 = sp;
1494     if (!XMLString::equals(s1,s2))
1495         return false;
1496
1497     return true;
1498 }
1499
1500 LogoutEvent* SSCache::newLogoutEvent(const Application& app) const
1501 {
1502     if (!SPConfig::getConfig().isEnabled(SPConfig::Logging))
1503         return nullptr;
1504     try {
1505         auto_ptr<TransactionLog::Event> event(SPConfig::getConfig().EventManager.newPlugin(LOGOUT_EVENT, nullptr));
1506         LogoutEvent* logout_event = dynamic_cast<LogoutEvent*>(event.get());
1507         if (logout_event) {
1508             logout_event->m_app = &app;
1509             event.release();
1510             return logout_event;
1511         }
1512         else {
1513             m_log.warn("unable to audit event, log event object was of an incorrect type");
1514         }
1515     }
1516     catch (std::exception& ex) {
1517         m_log.warn("exception auditing event: %s", ex.what());
1518     }
1519     return nullptr;
1520 }
1521
1522 #endif
1523
1524 Session* SSCache::find(const Application& app, const char* key, const char* client_addr, time_t* timeout)
1525 {
1526 #ifdef _DEBUG
1527     xmltooling::NDC ndc("find");
1528 #endif
1529     StoredSession* session=nullptr;
1530
1531     if (inproc) {
1532         m_log.debug("searching local cache for session (%s)", key);
1533         m_lock->rdlock();
1534         map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);
1535         if (i!=m_hashtable.end()) {
1536             // Save off and lock the session.
1537             session = i->second;
1538             session->lock();
1539             m_lock->unlock();
1540             m_log.debug("session found locally, validating it for use");
1541         }
1542         else {
1543             m_lock->unlock();
1544         }
1545     }
1546
1547     if (!session) {
1548         if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
1549             m_log.debug("session not found locally, remoting the search");
1550             // Remote the request.
1551             DDF in("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;
1552             DDFJanitor jin(in);
1553             in.structure();
1554             in.addmember("key").string(key);
1555             in.addmember("application_id").string(app.getId());
1556             if (timeout && *timeout) {
1557                 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
1558 #ifndef HAVE_GMTIME_R
1559                 struct tm* ptime=gmtime(timeout);
1560 #else
1561                 struct tm res;
1562                 struct tm* ptime=gmtime_r(timeout,&res);
1563 #endif
1564                 char timebuf[32];
1565                 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
1566                 in.addmember("timeout").string(timebuf);
1567             }
1568
1569             try {
1570                 out=app.getServiceProvider().getListenerService()->send(in);
1571                 if (!out.isstruct()) {
1572                     out.destroy();
1573                     m_log.debug("session not found in remote cache");
1574                     return nullptr;
1575                 }
1576
1577                 // Wrap the results in a local entry and save it.
1578                 session = new StoredSession(this, out);
1579                 // The remote end has handled timeout issues, we handle address and expiration checks.
1580                 timeout = nullptr;
1581             }
1582             catch (...) {
1583                 out.destroy();
1584                 throw;
1585             }
1586         }
1587         else {
1588             // We're out of process, so we can search the storage service directly.
1589 #ifndef SHIBSP_LITE
1590             if (!m_storage)
1591                 throw ConfigurationException("SessionCache lookup requires a StorageService.");
1592
1593             m_log.debug("searching for session (%s)", key);
1594
1595             DDF obj;
1596             time_t lastAccess;
1597             string record;
1598             int ver = m_storage->readText(key, "session", &record, &lastAccess);
1599             if (!ver)
1600                 return nullptr;
1601
1602             m_log.debug("reconstituting session and checking validity");
1603
1604             istringstream in(record);
1605             in >> obj;
1606
1607             unsigned long cacheTimeout = getCacheTimeout(app);
1608             lastAccess -= cacheTimeout;   // adjusts it back to the last time the record's timestamp was touched
1609             time_t now=time(nullptr);
1610
1611             if (timeout && *timeout > 0 && now - lastAccess >= *timeout) {
1612                 m_log.info("session timed out (ID: %s)", key);
1613                 scoped_ptr<LogoutEvent> logout_event(newLogoutEvent(app));
1614                 if (logout_event.get()) {
1615                     logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_INVALID;
1616                     logout_event->m_sessions.push_back(key);
1617                     app.getServiceProvider().getTransactionLog()->write(*logout_event);
1618                 }
1619                 remove(app, key);
1620                 const char* eid = obj["entity_id"].string();
1621                 if (!eid) {
1622                     obj.destroy();
1623                     throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1624                 }
1625                 string eid2(eid);
1626                 obj.destroy();
1627                 throw RetryableProfileException("Your session has expired, and you must re-authenticate.", namedparams(1, "entityID", eid2.c_str()));
1628             }
1629
1630             if (timeout) {
1631                 // Update storage expiration, if possible.
1632                 try {
1633                     m_storage->updateContext(key, now + cacheTimeout);
1634                 }
1635                 catch (std::exception& ex) {
1636                     m_log.error("failed to update session expiration: %s", ex.what());
1637                 }
1638             }
1639
1640             // Wrap the results in a local entry and save it.
1641             session = new StoredSession(this, obj);
1642             // We handled timeout issues, still need to handle address and expiration checks.
1643             timeout = nullptr;
1644 #else
1645             throw ConfigurationException("SessionCache search requires a StorageService.");
1646 #endif
1647         }
1648
1649         if (inproc) {
1650             // Lock for writing and repeat the search to avoid duplication.
1651             m_lock->wrlock();
1652             SharedLock shared(m_lock, false);
1653             if (m_hashtable.count(key)) {
1654                 // We're using an existing session entry.
1655                 delete session;
1656                 session = m_hashtable[key];
1657                 session->lock();
1658             }
1659             else {
1660                 m_hashtable[key]=session;
1661                 session->lock();
1662             }
1663         }
1664     }
1665
1666     if (!XMLString::equals(session->getApplicationID(), app.getId())) {
1667         m_log.warn("an application (%s) tried to access another application's session", app.getId());
1668         session->unlock();
1669         return nullptr;
1670     }
1671
1672     // Verify currency and update the timestamp if indicated by caller.
1673     try {
1674         session->validate(app, client_addr, timeout);
1675     }
1676     catch (...) {
1677 #ifndef SHIBSP_LITE
1678         scoped_ptr<LogoutEvent> logout_event(newLogoutEvent(app));
1679         if (logout_event.get()) {
1680             logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_INVALID;
1681             logout_event->m_session = session;
1682             logout_event->m_sessions.push_back(session->getID());
1683             app.getServiceProvider().getTransactionLog()->write(*logout_event);
1684         }
1685 #endif
1686         session->unlock();
1687         remove(app, key);
1688         throw;
1689     }
1690
1691     return session;
1692 }
1693
1694 Session* SSCache::find(const Application& app, HTTPRequest& request, const char* client_addr, time_t* timeout)
1695 {
1696     string id = active(app, request);
1697     if (id.empty())
1698         return nullptr;
1699     try {
1700         Session* session = find(app, id.c_str(), client_addr, timeout);
1701         if (session)
1702             return session;
1703         HTTPResponse* response = dynamic_cast<HTTPResponse*>(&request);
1704         if (response) {
1705             if (!m_outboundHeader.empty())
1706                 response->setResponseHeader(m_outboundHeader.c_str(), nullptr);
1707             pair<string,const char*> shib_cookie = app.getCookieNameProps("_shibsession_");
1708             string exp(shib_cookie.second);
1709             exp += "; expires=Mon, 01 Jan 2001 00:00:00 GMT";
1710             response->setCookie(shib_cookie.first.c_str(), exp.c_str());
1711         }
1712     }
1713     catch (std::exception&) {
1714         HTTPResponse* response = dynamic_cast<HTTPResponse*>(&request);
1715         if (response) {
1716             if (!m_outboundHeader.empty())
1717                 response->setResponseHeader(m_outboundHeader.c_str(), nullptr);
1718             pair<string,const char*> shib_cookie = app.getCookieNameProps("_shibsession_");
1719             string exp(shib_cookie.second);
1720             exp += "; expires=Mon, 01 Jan 2001 00:00:00 GMT";
1721             response->setCookie(shib_cookie.first.c_str(), exp.c_str());
1722         }
1723         throw;
1724     }
1725     return nullptr;
1726 }
1727
1728 void SSCache::remove(const Application& app, const HTTPRequest& request, HTTPResponse* response)
1729 {
1730     string session_id;
1731     pair<string,const char*> shib_cookie = app.getCookieNameProps("_shibsession_");
1732
1733     if (!m_inboundHeader.empty())
1734         session_id = request.getHeader(m_inboundHeader.c_str());
1735     if (session_id.empty()) {
1736         const char* c = request.getCookie(shib_cookie.first.c_str());
1737         if (c && *c)
1738             session_id = c;
1739     }
1740
1741     if (!session_id.empty()) {
1742         if (response) {
1743             if (!m_outboundHeader.empty())
1744                 response->setResponseHeader(m_outboundHeader.c_str(), nullptr);
1745             string exp(shib_cookie.second);
1746             exp += "; expires=Mon, 01 Jan 2001 00:00:00 GMT";
1747             response->setCookie(shib_cookie.first.c_str(), exp.c_str());
1748         }
1749         remove(app, session_id.c_str());
1750     }
1751 }
1752
1753 void SSCache::remove(const Application& app, const char* key)
1754 {
1755 #ifdef _DEBUG
1756     xmltooling::NDC ndc("remove");
1757 #endif
1758     // Take care of local copy.
1759     if (inproc)
1760         dormant(key);
1761
1762     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
1763         // Remove the session from storage directly.
1764 #ifndef SHIBSP_LITE
1765         m_storage->deleteContext(key);
1766         m_log.info("removed session (%s)", key);
1767 #else
1768         throw ConfigurationException("SessionCache removal requires a StorageService.");
1769 #endif
1770     }
1771     else {
1772         // Remote the request.
1773         DDF in("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache");
1774         DDFJanitor jin(in);
1775         in.structure();
1776         in.addmember("key").string(key);
1777         in.addmember("application_id").string(app.getId());
1778
1779         DDF out = app.getServiceProvider().getListenerService()->send(in);
1780         out.destroy();
1781     }
1782 }
1783
1784 void SSCache::dormant(const char* key)
1785 {
1786 #ifdef _DEBUG
1787     xmltooling::NDC ndc("dormant");
1788 #endif
1789
1790     m_log.debug("deleting local copy of session (%s)", key);
1791
1792     // lock the cache for writing, which means we know nobody is sitting in find()
1793     m_lock->wrlock();
1794
1795     // grab the entry from the table
1796     map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);
1797     if (i==m_hashtable.end()) {
1798         m_lock->unlock();
1799         return;
1800     }
1801
1802     // ok, remove the entry and lock it
1803     StoredSession* entry=i->second;
1804     m_hashtable.erase(key);
1805     entry->lock();
1806
1807     // unlock the cache
1808     m_lock->unlock();
1809
1810     // we can release the cache entry lock because we know we're not in the cache anymore
1811     entry->unlock();
1812
1813     delete entry;
1814 }
1815
1816 void* SSCache::cleanup_fn(void* p)
1817 {
1818 #ifdef _DEBUG
1819     xmltooling::NDC ndc("cleanup");
1820 #endif
1821
1822     SSCache* pcache = reinterpret_cast<SSCache*>(p);
1823
1824 #ifndef WIN32
1825     // First, let's block all signals
1826     Thread::mask_all_signals();
1827 #endif
1828
1829     scoped_ptr<Mutex> mutex(Mutex::create());
1830
1831     // Load our configuration details...
1832     static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);
1833     const XMLCh* tag=pcache->m_root ? pcache->m_root->getAttributeNS(nullptr, cleanupInterval) : nullptr;
1834     int rerun_timer = 900;
1835     if (tag && *tag) {
1836         rerun_timer = XMLString::parseInt(tag);
1837         if (rerun_timer <= 0)
1838             rerun_timer = 900;
1839     }
1840
1841     mutex->lock();
1842
1843     pcache->m_log.info("cleanup thread started...run every %d secs; timeout after %d secs", rerun_timer, pcache->m_inprocTimeout);
1844
1845     while (!pcache->shutdown) {
1846         pcache->shutdown_wait->timedwait(mutex.get(), rerun_timer);
1847         if (pcache->shutdown)
1848             break;
1849
1850         // Ok, let's run through the cleanup process and clean out
1851         // really old sessions.  This is a two-pass process.  The
1852         // first pass is done holding a read-lock while we iterate over
1853         // the cache.  The second pass doesn't need a lock because
1854         // the 'deletes' will lock the cache.
1855
1856         // Pass 1: iterate over the map and find all entries that have not been
1857         // used in the allotted timeout.
1858         vector<string> stale_keys;
1859         time_t stale = time(nullptr) - pcache->m_inprocTimeout;
1860
1861         pcache->m_log.debug("cleanup thread running");
1862
1863         pcache->m_lock->rdlock();
1864         for (map<string,StoredSession*>::const_iterator i = pcache->m_hashtable.begin(); i != pcache->m_hashtable.end(); ++i) {
1865             // If the last access was BEFORE the stale timeout...
1866             i->second->lock();
1867             time_t last=i->second->getLastAccess();
1868             i->second->unlock();
1869             if (last < stale)
1870                 stale_keys.push_back(i->first);
1871         }
1872         pcache->m_lock->unlock();
1873
1874         if (!stale_keys.empty()) {
1875             pcache->m_log.info("purging %d old sessions", stale_keys.size());
1876
1877             // Pass 2: walk through the list of stale entries and remove them from the cache
1878             for_each(stale_keys.begin(), stale_keys.end(), boost::bind(&SSCache::dormant, pcache, boost::bind(&string::c_str, _1)));
1879         }
1880
1881         pcache->m_log.debug("cleanup thread completed");
1882     }
1883
1884     pcache->m_log.info("cleanup thread exiting");
1885
1886     mutex->unlock();
1887     return nullptr;
1888 }
1889
1890 #ifndef SHIBSP_LITE
1891
1892 void SSCache::receive(DDF& in, ostream& out)
1893 {
1894 #ifdef _DEBUG
1895     xmltooling::NDC ndc("receive");
1896 #endif
1897     const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());
1898     if (!app)
1899         throw ListenerException("Application not found, check configuration?");
1900
1901     if (!strcmp(in.name(),"find::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1902         const char* key=in["key"].string();
1903         if (!key)
1904             throw ListenerException("Required parameters missing for session lookup.");
1905
1906         // Do an unversioned read.
1907         string record;
1908         time_t lastAccess;
1909         if (!m_storage->readText(key, "session", &record, &lastAccess)) {
1910             m_log.debug("session not found in cache (%s)", key);
1911             DDF ret(nullptr);
1912             DDFJanitor jan(ret);
1913             out << ret;
1914             return;
1915         }
1916
1917         // Adjust for expiration to recover last access time and check timeout.
1918         unsigned long cacheTimeout = getCacheTimeout(*app);
1919         lastAccess -= cacheTimeout;
1920         time_t now=time(nullptr);
1921
1922         // See if we need to check for a timeout.
1923         if (in["timeout"].string()) {
1924             time_t timeout = 0;
1925             auto_ptr_XMLCh dt(in["timeout"].string());
1926             DateTime dtobj(dt.get());
1927             dtobj.parseDateTime();
1928             timeout = dtobj.getEpoch();
1929
1930             if (timeout > 0 && now - lastAccess >= timeout) {
1931                 m_log.info("session timed out (ID: %s)", key);
1932                 scoped_ptr<LogoutEvent> logout_event(newLogoutEvent(*app));
1933                 if (logout_event.get()) {
1934                     logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_INVALID;
1935                     logout_event->m_sessions.push_back(key);
1936                     app->getServiceProvider().getTransactionLog()->write(*logout_event);
1937                 }
1938                 remove(*app, key);
1939                 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1940             }
1941
1942             // Update storage expiration, if possible.
1943             try {
1944                 m_storage->updateContext(key, now + cacheTimeout);
1945             }
1946             catch (std::exception& ex) {
1947                 m_log.error("failed to update session expiration: %s", ex.what());
1948             }
1949         }
1950
1951         // Send the record back.
1952         out << record;
1953     }
1954     else if (!strcmp(in.name(),"touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1955         const char* key=in["key"].string();
1956         if (!key)
1957             throw ListenerException("Required parameters missing for session check.");
1958         const char* client_addr = in["client_addr"].string();
1959
1960         // Do a read. May be unversioned if we need to bind a new client address.
1961         string record;
1962         time_t lastAccess;
1963         int curver = in["version"].integer();
1964         int ver = m_storage->readText(key, "session", &record, &lastAccess, client_addr ? 0 : curver);
1965         if (ver == 0) {
1966             m_log.warn("unsuccessful read of session (ID: %s), caches out of sync?", key);
1967             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1968         }
1969
1970         // Adjust for expiration to recover last access time and check timeout.
1971         unsigned long cacheTimeout = getCacheTimeout(*app);
1972         lastAccess -= cacheTimeout;
1973         time_t now=time(nullptr);
1974
1975         // See if we need to check for a timeout.
1976         time_t timeout = 0;
1977         auto_ptr_XMLCh dt(in["timeout"].string());
1978         if (dt.get()) {
1979             DateTime dtobj(dt.get());
1980             dtobj.parseDateTime();
1981             timeout = dtobj.getEpoch();
1982         }
1983
1984         if (timeout > 0 && now - lastAccess >= timeout) {
1985             m_log.info("session timed out (ID: %s)", key);
1986             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1987         }
1988
1989         // Update storage expiration, if possible.
1990         try {
1991             m_storage->updateContext(key, now + cacheTimeout);
1992         }
1993         catch (std::exception& ex) {
1994             m_log.error("failed to update session expiration: %s", ex.what());
1995         }
1996
1997         // We may need to write back a new address into the session using a versioned update loop.
1998         if (client_addr) {
1999             m_log.info("binding session (%s) to new client address (%s)", key, client_addr);
2000             do {
2001                 // We have to reconstitute the session object ourselves.
2002                 DDF sessionobj;
2003                 DDFJanitor sessionjan(sessionobj);
2004                 istringstream src(record);
2005                 src >> sessionobj;
2006                 ver = sessionobj["version"].integer();
2007                 const char* saddr = nullptr;
2008                 if (strchr(client_addr, ':'))
2009                     saddr = sessionobj["client_addr"]["6"].string();
2010                 else
2011                     saddr = sessionobj["client_addr"]["4"].string();
2012                 if (saddr) {
2013                     // Something snuck in and bound the session to this address type, so it better match what we have.
2014                     if (!XMLString::equals(saddr, client_addr)) {
2015                         m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr);
2016                         throw RetryableProfileException(
2017                             "Your IP address ($1) does not match the address recorded at the time the session was established.",
2018                             params(1, client_addr)
2019                             );
2020                     }
2021                     break;  // No need to update.
2022                 }
2023                 else {
2024                     // Bind it into the session.
2025                     if (strchr(client_addr, ':'))
2026                         sessionobj["client_addr"].addmember("6").string(client_addr);
2027                     else
2028                         sessionobj["client_addr"].addmember("4").string(client_addr);
2029                 }
2030
2031                 // Tentatively increment the version.
2032                 sessionobj["version"].integer(sessionobj["version"].integer() + 1);
2033
2034                 ostringstream str;
2035                 str << sessionobj;
2036                 record = str.str();
2037
2038                 ver = m_storage->updateText(key, "session", record.c_str(), 0, ver);
2039                 if (!ver) {
2040                     // Fatal problem with update.
2041                     m_log.error("updateText failed on StorageService for session (%s)", key);
2042                     throw IOException("Unable to update stored session.");
2043                 }
2044                 if (ver < 0) {
2045                     // Out of sync.
2046                     m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
2047                     sessionobj["version"].integer(sessionobj["version"].integer() - 1);
2048                     ver = m_storage->readText(key, "session", &record);
2049                     if (!ver) {
2050                         m_log.error("readText failed on StorageService for session (%s)", key);
2051                         throw IOException("Unable to read back stored session.");
2052                     }
2053                     ver = -1;
2054                 }
2055             } while (ver < 0); // negative indicates a sync issue so we retry
2056         }
2057
2058         if (ver > curver) {
2059             // Send the record back.
2060             out << record;
2061         }
2062         else {
2063             DDF ret(nullptr);
2064             DDFJanitor jan(ret);
2065             out << ret;
2066         }
2067     }
2068     else if (!strcmp(in.name(),"remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
2069         const char* key=in["key"].string();
2070         if (!key)
2071             throw ListenerException("Required parameter missing for session removal.");
2072
2073         remove(*app, key);
2074         DDF ret(nullptr);
2075         DDFJanitor jan(ret);
2076         out << ret;
2077     }
2078 }
2079
2080 #endif