d203319ee0d89c0e8b638f3615c6c210baf1f1d7
[shibboleth/sp.git] / shibsp / impl / RemotedSessionCache.cpp
1 /*
2  *  Copyright 2001-2007 Internet2
3  * 
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /**
18  * RemotedSessionCache.cpp
19  * 
20  * SessionCache implementation that delegates to a remoted version.
21  */
22
23 #include "internal.h"
24 #include "Application.h"
25 #include "exceptions.h"
26 #include "ServiceProvider.h"
27 #include "SessionCache.h"
28 #include "attribute/Attribute.h"
29 #include "remoting/ListenerService.h"
30 #include "util/SPConstants.h"
31
32 #include <ctime>
33 #include <sstream>
34 #include <xmltooling/XMLToolingConfig.h>
35 #include <xmltooling/util/DateTime.h>
36 #include <xmltooling/util/NDC.h>
37 #include <xmltooling/util/XMLHelper.h>
38
39 using namespace shibsp;
40 using namespace xmltooling;
41 using namespace std;
42
43 namespace shibsp {
44
45     class RemotedCache;
46     class RemotedSession : public virtual Session
47     {
48     public:
49         RemotedSession(RemotedCache* cache, DDF& obj) : m_version(obj["version"].integer()), m_obj(obj),
50                 m_expires(0), m_lastAccess(time(NULL)), m_cache(cache), m_lock(NULL) {
51             auto_ptr_XMLCh exp(m_obj["expires"].string());
52             if (exp.get()) {
53                 DateTime iso(exp.get());
54                 iso.parseDateTime();
55                 m_expires = iso.getEpoch();
56             }
57
58             m_lock = Mutex::create();
59         }
60         
61         ~RemotedSession() {
62             delete m_lock;
63             m_obj.destroy();
64             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
65         }
66         
67         Lockable* lock() {
68             m_lock->lock();
69             return this;
70         }
71         void unlock() {
72             m_lock->unlock();
73         }
74
75         const char* getID() const {
76             return m_obj.name();
77         }
78         const char* getApplicationID() const {
79             return m_obj["application_id"].string();
80         }
81         const char* getClientAddress() const {
82             return m_obj["client_addr"].string();
83         }
84         const char* getEntityID() const {
85             return m_obj["entity_id"].string();
86         }
87         const char* getProtocol() const {
88             return m_obj["protocol"].string();
89         }
90         const char* getAuthnInstant() const {
91             return m_obj["authn_instant"].string();
92         }
93         const char* getSessionIndex() const {
94             return m_obj["session_index"].string();
95         }
96         const char* getAuthnContextClassRef() const {
97             return m_obj["authncontext_class"].string();
98         }
99         const char* getAuthnContextDeclRef() const {
100             return m_obj["authncontext_decl"].string();
101         }
102         const vector<Attribute*>& getAttributes() const {
103             if (m_attributes.empty())
104                 unmarshallAttributes();
105             return m_attributes;
106         }
107         const multimap<string,const Attribute*>& getIndexedAttributes() const {
108             if (m_attributeIndex.empty()) {
109                 if (m_attributes.empty())
110                     unmarshallAttributes();
111                 for (vector<Attribute*>::const_iterator a = m_attributes.begin(); a != m_attributes.end(); ++a) {
112                     const vector<string>& aliases = (*a)->getAliases();
113                     for (vector<string>::const_iterator alias = aliases.begin(); alias != aliases.end(); ++alias)
114                         m_attributeIndex.insert(multimap<string,const Attribute*>::value_type(*alias, *a));
115                 }
116             }
117             return m_attributeIndex;
118         }
119         const vector<const char*>& getAssertionIDs() const {
120             if (m_ids.empty()) {
121                 DDF ids = m_obj["assertions"];
122                 DDF id = ids.first();
123                 while (id.isstring()) {
124                     m_ids.push_back(id.string());
125                     id = ids.next();
126                 }
127             }
128             return m_ids;
129         }
130         
131         time_t expires() const { return m_expires; }
132         time_t lastAccess() const { return m_lastAccess; }
133         void validate(const Application& application, const char* client_addr, time_t* timeout);
134
135     private:
136         void unmarshallAttributes() const;
137
138         int m_version;
139         mutable DDF m_obj;
140         mutable vector<Attribute*> m_attributes;
141         mutable multimap<string,const Attribute*> m_attributeIndex;
142         mutable vector<const char*> m_ids;
143         time_t m_expires,m_lastAccess;
144         RemotedCache* m_cache;
145         Mutex* m_lock;
146     };
147     
148     class RemotedCache : public SessionCache
149     {
150     public:
151         RemotedCache(const DOMElement* e);
152         ~RemotedCache();
153     
154         Session* find(const char* key, const Application& application, const char* client_addr=NULL, time_t* timeout=NULL);
155         void remove(const char* key, const Application& application);
156         
157         void cleanup();
158     
159         Category& m_log;
160     private:
161         const DOMElement* m_root;         // Only valid during initialization
162         RWLock* m_lock;
163         map<string,RemotedSession*> m_hashtable;
164     
165         void dormant(const char* key);
166         static void* cleanup_fn(void*);
167         bool shutdown;
168         CondWait* shutdown_wait;
169         Thread* cleanup_thread;
170     };
171
172     SessionCache* SHIBSP_DLLLOCAL RemotedCacheFactory(const DOMElement* const & e)
173     {
174         return new RemotedCache(e);
175     }
176 }
177
178 void RemotedSession::unmarshallAttributes() const
179 {
180     Attribute* attribute;
181     DDF attrs = m_obj["attributes"];
182     DDF attr = attrs.first();
183     while (!attr.isnull()) {
184         try {
185             attribute = Attribute::unmarshall(attr);
186             m_attributes.push_back(attribute);
187             if (m_cache->m_log.isDebugEnabled())
188                 m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",
189                     attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");
190         }
191         catch (AttributeException& ex) {
192             const char* id = attr.first().name();
193             m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());
194         }
195         attr = attrs.next();
196     }
197 }
198
199 void RemotedSession::validate(const Application& application, const char* client_addr, time_t* timeout)
200 {
201     // Basic expiration?
202     time_t now = time(NULL);
203     if (now > m_expires) {
204         m_cache->m_log.info("session expired (ID: %s)", getID());
205         throw opensaml::RetryableProfileException("Your session has expired, and you must re-authenticate.");
206     }
207
208     // Address check?
209     if (client_addr) {
210         if (m_cache->m_log.isDebugEnabled())
211             m_cache->m_log.debug("comparing client address %s against %s", client_addr, getClientAddress());
212         if (strcmp(getClientAddress(),client_addr)) {
213             m_cache->m_log.warn("client address mismatch");
214             throw opensaml::RetryableProfileException(
215                 "Your IP address ($1) does not match the address recorded at the time the session was established.",
216                 params(1,client_addr)
217                 );
218         }
219     }
220
221     if (!timeout)
222         return;
223     
224     DDF in("touch::"REMOTED_SESSION_CACHE"::SessionCache"), out;
225     DDFJanitor jin(in);
226     in.structure();
227     in.addmember("key").string(getID());
228     in.addmember("version").integer(m_obj["version"].integer());
229     if (*timeout) {
230         // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  
231 #ifndef HAVE_GMTIME_R
232         struct tm* ptime=gmtime(timeout);
233 #else
234         struct tm res;
235         struct tm* ptime=gmtime_r(timeout,&res);
236 #endif
237         char timebuf[32];
238         strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
239         in.addmember("timeout").string(timebuf);
240     }
241
242     try {
243         out=application.getServiceProvider().getListenerService()->send(in);
244     }
245     catch (...) {
246         out.destroy();
247         throw;
248     }
249
250     if (out.isstruct()) {
251         // We got an updated record back.
252         m_ids.clear();
253         for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
254         m_attributes.clear();
255         m_attributeIndex.clear();
256         m_obj.destroy();
257         m_obj = out;
258     }
259
260     m_lastAccess = now;
261 }
262
263 RemotedCache::RemotedCache(const DOMElement* e)
264     : SessionCache(e, 900), m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), m_root(e), m_lock(NULL), shutdown(false)
265 {
266     if (!SPConfig::getConfig().getServiceProvider()->getListenerService())
267         throw ConfigurationException("RemotedCacheService requires a ListenerService, but none available.");
268         
269     m_lock = RWLock::create();
270     shutdown_wait = CondWait::create();
271     cleanup_thread = Thread::create(&cleanup_fn, (void*)this);
272 }
273
274 RemotedCache::~RemotedCache()
275 {
276     // Shut down the cleanup thread and let it know...
277     shutdown = true;
278     shutdown_wait->signal();
279     cleanup_thread->join(NULL);
280
281     for_each(m_hashtable.begin(),m_hashtable.end(),xmltooling::cleanup_pair<string,RemotedSession>());
282     delete m_lock;
283     delete shutdown_wait;
284 }
285
286 Session* RemotedCache::find(const char* key, const Application& application, const char* client_addr, time_t* timeout)
287 {
288 #ifdef _DEBUG
289     xmltooling::NDC ndc("find");
290 #endif
291
292     RemotedSession* session=NULL;
293     m_log.debug("searching local cache for session (%s)", key);
294     m_lock->rdlock();
295     map<string,RemotedSession*>::const_iterator i=m_hashtable.find(key);
296     if (i==m_hashtable.end()) {
297         m_lock->unlock();
298         m_log.debug("session not found locally, searching remote cache");
299
300         DDF in("find::"REMOTED_SESSION_CACHE"::SessionCache"), out;
301         DDFJanitor jin(in);
302         in.structure();
303         in.addmember("key").string(key);
304         in.addmember("application_id").string(application.getId());
305         if (timeout && *timeout) {
306             // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  
307 #ifndef HAVE_GMTIME_R
308             struct tm* ptime=gmtime(timeout);
309 #else
310             struct tm res;
311             struct tm* ptime=gmtime_r(timeout,&res);
312 #endif
313             char timebuf[32];
314             strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
315             in.addmember("timeout").string(timebuf);
316         }
317         
318         try {
319             out=application.getServiceProvider().getListenerService()->send(in);
320             if (!out.isstruct()) {
321                 out.destroy();
322                 m_log.debug("session not found in remote cache");
323                 return NULL;
324             }
325             
326             // Wrap the results in a local entry and save it.
327             session = new RemotedSession(this, out);
328             // The remote end has handled timeout issues, we handle address and expiration checks.
329             timeout = NULL;
330         }
331         catch (...) {
332             out.destroy();
333             throw;
334         }
335
336         // Lock for writing and repeat the search to avoid duplication.
337         m_lock->wrlock();
338         SharedLock shared(m_lock, false);
339         if (m_hashtable.count(key)) {
340             // We're using an existing session entry.
341             delete session;
342             session = m_hashtable[key];
343             session->lock();
344         }
345         else {
346             m_hashtable[key]=session;
347             session->lock();
348         }
349     }
350     else {
351         // Save off and lock the session.
352         session = i->second;
353         session->lock();
354         m_lock->unlock();
355         
356         m_log.debug("session found locally, validating it for use");
357     }
358
359     if (!XMLString::equals(session->getApplicationID(), application.getId())) {
360         m_log.error("an application (%s) tried to access another application's session", application.getId());
361         session->unlock();
362         return NULL;
363     }
364
365     // Verify currency and update the timestamp if indicated by caller.
366     try {
367         session->validate(application, client_addr, timeout);
368     }
369     catch (...) {
370         session->unlock();
371         remove(key, application);
372         throw;
373     }
374     
375     return session;
376 }
377
378 void RemotedCache::remove(const char* key, const Application& application)
379 {
380     // Take care of local copy.
381     dormant(key);
382     
383     // Now remote...
384     DDF in("remove::"REMOTED_SESSION_CACHE"::SessionCache");
385     DDFJanitor jin(in);
386     in.structure();
387     in.addmember("key").string(key);
388     in.addmember("application_id").string(application.getId());
389     
390     DDF out = application.getServiceProvider().getListenerService()->send(in);
391     out.destroy();
392 }
393
394 void RemotedCache::dormant(const char* key)
395 {
396 #ifdef _DEBUG
397     xmltooling::NDC ndc("dormant");
398 #endif
399
400     m_log.debug("deleting local copy of session (%s)", key);
401
402     // lock the cache for writing, which means we know nobody is sitting in find()
403     m_lock->wrlock();
404
405     // grab the entry from the table
406     map<string,RemotedSession*>::const_iterator i=m_hashtable.find(key);
407     if (i==m_hashtable.end()) {
408         m_lock->unlock();
409         return;
410     }
411
412     // ok, remove the entry and lock it
413     RemotedSession* entry=i->second;
414     m_hashtable.erase(key);
415     entry->lock();
416     
417     // unlock the cache
418     m_lock->unlock();
419
420     // we can release the cache entry lock because we know we're not in the cache anymore
421     entry->unlock();
422
423     delete entry;
424 }
425
426 void RemotedCache::cleanup()
427 {
428 #ifdef _DEBUG
429     xmltooling::NDC ndc("cleanup");
430 #endif
431
432     Mutex* mutex = Mutex::create();
433   
434     // Load our configuration details...
435     static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);
436     const XMLCh* tag=m_root ? m_root->getAttributeNS(NULL,cleanupInterval) : NULL;
437     int rerun_timer = 900;
438     if (tag && *tag)
439         rerun_timer = XMLString::parseInt(tag);
440     if (rerun_timer <= 0)
441         rerun_timer = 900;
442
443     mutex->lock();
444
445     m_log.info("cleanup thread started...run every %d secs; timeout after %d secs", rerun_timer, m_cacheTimeout);
446
447     while (!shutdown) {
448         shutdown_wait->timedwait(mutex,rerun_timer);
449         if (shutdown)
450             break;
451
452         // Ok, let's run through the cleanup process and clean out
453         // really old sessions.  This is a two-pass process.  The
454         // first pass is done holding a read-lock while we iterate over
455         // the cache.  The second pass doesn't need a lock because
456         // the 'deletes' will lock the cache.
457     
458         // Pass 1: iterate over the map and find all entries that have not been
459         // used in X hours
460         vector<string> stale_keys;
461         time_t stale = time(NULL) - m_cacheTimeout;
462     
463         m_log.debug("cleanup thread running");
464
465         m_lock->rdlock();
466         for (map<string,RemotedSession*>::const_iterator i=m_hashtable.begin(); i!=m_hashtable.end(); ++i) {
467             // If the last access was BEFORE the stale timeout...
468             i->second->lock();
469             time_t last=i->second->lastAccess();
470             i->second->unlock();
471             if (last < stale)
472                 stale_keys.push_back(i->first);
473         }
474         m_lock->unlock();
475     
476         if (!stale_keys.empty()) {
477             m_log.info("purging %d old sessions", stale_keys.size());
478     
479             // Pass 2: walk through the list of stale entries and remove them from the cache
480             for (vector<string>::const_iterator j = stale_keys.begin(); j != stale_keys.end(); ++j)
481                 dormant(j->c_str());
482         }
483
484         m_log.debug("cleanup thread completed");
485     }
486
487     m_log.info("cleanup thread exiting");
488
489     mutex->unlock();
490     delete mutex;
491     Thread::exit(NULL);
492 }
493
494 void* RemotedCache::cleanup_fn(void* cache_p)
495 {
496     RemotedCache* cache = reinterpret_cast<RemotedCache*>(cache_p);
497
498 #ifndef WIN32
499     // First, let's block all signals 
500     Thread::mask_all_signals();
501 #endif
502
503     // Now run the cleanup process.
504     cache->cleanup();
505     return NULL;
506 }