Change cacheTimeout configuration from minutes to seconds
[shibboleth/sp.git] / shib-target / shib-ccache.cpp
1 /*
2  * The Shibboleth License, Version 1.
3  * Copyright (c) 2002
4  * University Corporation for Advanced Internet Development, Inc.
5  * All rights reserved
6  *
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions are met:
10  *
11  * Redistributions of source code must retain the above copyright notice, this
12  * list of conditions and the following disclaimer.
13  *
14  * Redistributions in binary form must reproduce the above copyright notice,
15  * this list of conditions and the following disclaimer in the documentation
16  * and/or other materials provided with the distribution, if any, must include
17  * the following acknowledgment: "This product includes software developed by
18  * the University Corporation for Advanced Internet Development
19  * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement
20  * may appear in the software itself, if and wherever such third-party
21  * acknowledgments normally appear.
22  *
23  * Neither the name of Shibboleth nor the names of its contributors, nor
24  * Internet2, nor the University Corporation for Advanced Internet Development,
25  * Inc., nor UCAID may be used to endorse or promote products derived from this
26  * software without specific prior written permission. For written permission,
27  * please contact shibboleth@shibboleth.org
28  *
29  * Products derived from this software may not be called Shibboleth, Internet2,
30  * UCAID, or the University Corporation for Advanced Internet Development, nor
31  * may Shibboleth appear in their name, without prior written permission of the
32  * University Corporation for Advanced Internet Development.
33  *
34  *
35  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
36  * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
38  * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK
39  * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE.
40  * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
41  * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT,
42  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
43  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
44  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
45  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
46  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
47  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48  */
49
50
51 /*
52  * shib-ccache.cpp -- SHAR Credential Cache
53  *
54  * Originally from mod_shib
55  * Modified by: Derek Atkins <derek@ihtfp.com>
56  *
57  * $Id$
58  */
59
60 #ifndef WIN32
61 # include <unistd.h>
62 #endif
63
64 #include "shib-target.h"
65 #include "ccache-utils.h"
66 #include <shib/shib-threads.h>
67
68 #include <log4cpp/Category.hh>
69
70 #include <sstream>
71 #include <stdexcept>
72
73 #ifdef HAVE_LIBDMALLOCXX
74 #include <dmalloc.h>
75 #endif
76
77 using namespace std;
78 using namespace saml;
79 using namespace shibboleth;
80 using namespace shibtarget;
81
82 class InternalCCache;
83 class InternalCCacheEntry : public CCacheEntry
84 {
85 public:
86   InternalCCacheEntry(SAMLAuthenticationStatement *s, const char *client_addr);
87   ~InternalCCacheEntry();
88
89   virtual Iterator<SAMLAssertion*> getAssertions(Resource& resource);
90   virtual void preFetch(Resource& resource, int prefetch_window);
91   virtual bool isSessionValid(time_t lifetime, time_t timeout);
92   virtual const char* getClientAddress() { return m_clientAddress.c_str(); }
93   virtual void release() { cacheitem_lock->unlock(); }
94
95   void setCache(InternalCCache *cache) { m_cache = cache; }
96   time_t lastAccess() { Lock lock(access_lock); return m_lastAccess; }
97   void rdlock() { cacheitem_lock->rdlock(); }
98   void wrlock() { cacheitem_lock->wrlock(); }
99
100   static vector<SAMLAssertion*> g_emptyVector;
101
102 private:
103   ResourceEntry* populate(Resource& resource, int slop);
104   ResourceEntry* find(const char* resource);
105   void insert(const char* resource, ResourceEntry* entry);
106   void remove(const char* resource);
107
108   string m_originSite;
109   string m_handle;
110   string m_clientAddress;
111   time_t m_sessionCreated;
112   time_t m_lastAccess;
113   bool m_hasbinding;
114
115   const SAMLSubject* m_subject;
116   SAMLAuthenticationStatement* p_auth;
117   InternalCCache *m_cache;
118
119   map<string,ResourceEntry*> m_resources;
120
121   log4cpp::Category* log;
122
123   // This is used to keep track of in-process "populate()" calls,
124   // to make sure that we don't try to populate the same resource
125   // in multiple threads.
126   map<string,Mutex*>    populate_locks;
127   Mutex*        pop_locks_lock;
128
129   Mutex*        access_lock;
130   RWLock*       resource_lock;
131   RWLock*       cacheitem_lock;
132
133   class ResourceLock
134   {
135   public:
136     ResourceLock(InternalCCacheEntry* entry, string resource);
137     ~ResourceLock();
138
139   private:
140     Mutex*                      find(string& resource);
141     InternalCCacheEntry*        entry;
142     string                      resource;
143   };
144 };
145
146 class InternalCCache : public CCache
147 {
148 public:
149   InternalCCache();
150   virtual ~InternalCCache();
151
152   virtual SAMLBinding* getBinding(const XMLCh* bindingProt);
153   virtual CCacheEntry* find(const char* key);
154   virtual void insert(const char* key, SAMLAuthenticationStatement *s,
155                       const char *client_addr);
156   virtual void remove(const char* key);
157
158   InternalCCacheEntry* findi(const char* key);
159   void  cleanup();
160
161 private:
162   RWLock *lock;
163
164   SAMLBinding* m_SAMLBinding;
165   map<string,InternalCCacheEntry*> m_hashtable;
166
167   log4cpp::Category* log;
168
169   static void*  cleanup_fcn(void*); // XXX Assumed an InternalCCache
170   bool          shutdown;
171   CondWait*     shutdown_wait;
172   Thread*       cleanup_thread;
173 };
174
175 namespace {
176   map<string,CCache::CCacheFactory> g_ccacheFactoryDB;
177 };
178
179 // Global Constructors & Destructors
180 CCache::~CCache() { }
181
182 void CCache::registerFactory(const char* name, CCache::CCacheFactory factory)
183 {
184   string ctx = "shibtarget.CCache";
185   log4cpp::Category& log = log4cpp::Category::getInstance(ctx);
186   saml::NDC ndc("registerFactory");
187
188   log.info ("Registered factory %p for CCache %s", factory, name);
189   g_ccacheFactoryDB[name] = factory;
190 }
191
192 CCache* CCache::getInstance(const char* type)
193 {
194   string ctx = "shibtarget.CCache";
195   log4cpp::Category& log = log4cpp::Category::getInstance(ctx);
196   saml::NDC ndc("getInstance");
197
198   map<string,CCache::CCacheFactory>::const_iterator i=g_ccacheFactoryDB.find(type);
199   if (i!=g_ccacheFactoryDB.end()) {
200     log.info ("Loading CCache: %s at %p", type, i->second);
201     return ((i->second)());
202   }
203
204   log.info ("Loading default memory CCache");
205   return (CCache*) new InternalCCache();
206 }
207
208 // static members
209 vector<SAMLAssertion*> InternalCCacheEntry::g_emptyVector;
210
211
212 /******************************************************************************/
213 /* InternalCCache:  A Credential Cache                                        */
214 /******************************************************************************/
215
216 InternalCCache::InternalCCache()
217 {
218   m_SAMLBinding=SAMLBindingFactory::getInstance();
219   string ctx="shibtarget.InternalCCache";
220   log = &(log4cpp::Category::getInstance(ctx));
221   lock = RWLock::create();
222
223   shutdown_wait = CondWait::create();
224   shutdown = false;
225   cleanup_thread = Thread::create(&cleanup_fcn, (void*)this);
226 }
227
228 InternalCCache::~InternalCCache()
229 {
230   // Shut down the cleanup thread and let it know...
231   shutdown = true;
232   shutdown_wait->signal();
233   cleanup_thread->join(NULL);
234
235   delete m_SAMLBinding;
236   for (map<string,InternalCCacheEntry*>::iterator i=m_hashtable.begin(); i!=m_hashtable.end(); i++)
237     delete i->second;
238   delete lock;
239   delete shutdown_wait;
240 }
241
242 SAMLBinding* InternalCCache::getBinding(const XMLCh* bindingProt)
243 {
244   log->debug("looking for binding...");
245   if (!XMLString::compareString(bindingProt,SAMLBinding::SAML_SOAP_HTTPS)) {
246     log->debug("https binding found");
247     return m_SAMLBinding;
248   }
249   return NULL;
250 }
251
252 // assumed a lock is held..
253 InternalCCacheEntry* InternalCCache::findi(const char* key)
254 {
255   log->debug("FindI: \"%s\"", key);
256
257   map<string,InternalCCacheEntry*>::const_iterator i=m_hashtable.find(key);
258   if (i==m_hashtable.end()) {
259     log->debug("No Match found");
260     return NULL;
261   }
262   log->debug("Match Found.");
263
264   return i->second;
265 }
266
267 CCacheEntry* InternalCCache::find(const char* key)
268 {
269   log->debug("Find: \"%s\"", key);
270   ReadLock rwlock(lock);
271
272   InternalCCacheEntry* entry = findi(key);
273   if (!entry) return NULL;
274
275   // Lock the database for the caller -- they have to release the item.
276   entry->rdlock();
277   return dynamic_cast<CCacheEntry*>(entry);
278 }
279
280 void InternalCCache::insert(const char* key, SAMLAuthenticationStatement *s,
281                             const char *client_addr)
282 {
283   log->debug("caching new entry for \"%s\"", key);
284
285   InternalCCacheEntry* entry = new InternalCCacheEntry (s, client_addr);
286   entry->setCache(this);
287
288   lock->wrlock();
289   m_hashtable[key]=entry;
290   lock->unlock();
291 }
292
293 // remove the entry from the database and then destroy the cacheentry
294 void InternalCCache::remove(const char* key)
295 {
296   log->debug("removing cache entry \"key\"", key);
297
298   // grab the entry from the database.  We'll have a readlock on it.
299   CCacheEntry* entry = findi(key);
300
301   if (!entry)
302     return;
303
304   // grab the cache write lock
305   lock->wrlock();
306
307   // verify we've still got the same entry.
308   if (entry != findi(key)) {
309     // Nope -- must've already been removed.
310     lock->unlock();
311     return;
312   }
313
314   // ok, remove the entry.
315   m_hashtable.erase(key);
316   lock->unlock();
317
318   // now grab the write lock on the cacheitem.
319   // This will make sure all other threads have released this item.
320   InternalCCacheEntry* ientry = dynamic_cast<InternalCCacheEntry*>(entry);
321   ientry->wrlock();
322
323   // we can release immediately because we know we're not in the database!
324   ientry->release();
325
326   // Now delete the entry
327   delete ientry;
328 }
329
330 void InternalCCache::cleanup()
331 {
332   Mutex* mutex = Mutex::create();
333   saml::NDC ndc("InternalCCache::cleanup()");
334
335   ShibTargetConfig& config = ShibTargetConfig::getConfig();
336   ShibINI& ini = config.getINI();
337
338   int rerun_timer = 0;
339   int timeout_life = 0;
340
341   string tag;
342   if (ini.get_tag (SHIBTARGET_SHAR, SHIBTARGET_TAG_CACHECLEAN, true, &tag))
343     rerun_timer = atoi(tag.c_str());
344   if (ini.get_tag (SHIBTARGET_SHAR, SHIBTARGET_TAG_CACHETIMEOUT, true, &tag))
345     timeout_life = atoi(tag.c_str());
346
347   if (rerun_timer <= 0)
348     rerun_timer = 300;          // rerun every 5 minutes
349
350   if (timeout_life <= 0)
351     timeout_life = 28800;       // timeout after 8 hours
352
353   mutex->lock();
354
355   log->debug("Cleanup thread started...  Run every %d secs; timeout after %d secs",
356              rerun_timer, timeout_life);
357
358   while (shutdown == false) {
359     struct timespec ts;
360     memset (&ts, 0, sizeof(ts));
361     ts.tv_sec = time(NULL) + rerun_timer;
362
363     shutdown_wait->timedwait(mutex, &ts);
364
365     if (shutdown == true)
366       break;
367
368     log->info("Cleanup thread running...");
369
370     // Ok, let's run through the cleanup process and clean out
371     // really old sessions.  This is a two-pass process.  The
372     // first pass is done holding a read-lock while we iterate over
373     // the database.  The second pass doesn't need a lock because
374     // the 'deletes' will lock the database.
375
376     // Pass 1: iterate over the map and find all entries that have not been
377     // used in X hours
378     vector<string> stale_keys;
379     time_t stale = time(NULL) - timeout_life;
380
381     lock->rdlock();
382     for (map<string,InternalCCacheEntry*>::iterator i=m_hashtable.begin();
383          i != m_hashtable.end(); i++)
384     {
385       // If the last access was BEFORE the stale timeout...
386       time_t last=i->second->lastAccess();
387       if (last < stale)
388         stale_keys.push_back(i->first);
389     }
390     lock->unlock();
391
392     log->info("deleting %d old items.", stale_keys.size());
393
394     // Pass 2: walk through the list of stale entries and remove them from
395     // the database
396     for (vector<string>::iterator i = stale_keys.begin();
397          i != stale_keys.end(); i++)
398     {
399       remove (i->c_str());
400     }
401
402   }
403
404   log->debug("Cleanup thread finished.");
405
406   mutex->unlock();
407   delete mutex;
408   Thread::exit(NULL);
409 }
410
411 void* InternalCCache::cleanup_fcn(void* cache_p)
412 {
413   InternalCCache* cache = (InternalCCache*)cache_p;
414
415   // First, let's block all signals
416   sigset_t sigmask;
417   sigfillset(&sigmask);
418   Thread::mask_signals(SIG_BLOCK, &sigmask, NULL);
419
420   // Now run the cleanup process.
421   cache->cleanup();
422 }
423
424 /******************************************************************************/
425 /* InternalCCacheEntry:  A Credential Cache Entry                             */
426 /******************************************************************************/
427
428 InternalCCacheEntry::InternalCCacheEntry(SAMLAuthenticationStatement *s, const char *client_addr)
429   : m_hasbinding(false)
430 {
431   string ctx = "shibtarget::InternalCCacheEntry";
432   log = &(log4cpp::Category::getInstance(ctx));
433   pop_locks_lock = Mutex::create();
434   access_lock = Mutex::create();
435   resource_lock = RWLock::create();
436   cacheitem_lock = RWLock::create();
437
438   if (s == NULL) {
439     log->error("NULL auth statement");
440     throw runtime_error("InternalCCacheEntry() was passed an empty SAML Statement");
441   }
442
443   m_subject = s->getSubject();
444
445   xstring name = m_subject->getName();
446   xstring qual = m_subject->getNameQualifier();
447
448   auto_ptr<char> h(XMLString::transcode(name.c_str()));
449   auto_ptr<char> d(XMLString::transcode(qual.c_str()));
450
451   m_handle = h.get();
452   m_originSite = d.get();
453
454   Iterator<SAMLAuthorityBinding*> bindings = s->getBindings();
455   if (bindings.hasNext())
456     m_hasbinding = true;
457
458   m_clientAddress = client_addr;
459   m_sessionCreated = m_lastAccess = time(NULL);
460
461   // Save for later.
462   p_auth = s;
463
464   log->info("New Session Created...");
465   log->debug("Handle: \"%s\", Site: \"%s\", Address: %s", h.get(), d.get(),
466              client_addr);
467 }
468
469 InternalCCacheEntry::~InternalCCacheEntry()
470 {
471   log->debug("deleting entry for %s@%s", m_handle.c_str(), m_originSite.c_str());
472   delete p_auth;
473   for (map<string,ResourceEntry*>::iterator i=m_resources.begin();
474        i!=m_resources.end(); i++)
475     delete i->second;
476
477   for (map<string,Mutex*>::iterator i=populate_locks.begin();
478        i!=populate_locks.end(); i++)
479     delete i->second;
480
481   delete pop_locks_lock;
482   delete cacheitem_lock;
483   delete resource_lock;
484   delete access_lock;
485 }
486
487 bool InternalCCacheEntry::isSessionValid(time_t lifetime, time_t timeout)
488 {
489   saml::NDC ndc("isSessionValid");
490   log->debug("test session %s@%s, (lifetime=%ld, timeout=%ld)",
491              m_handle.c_str(), m_originSite.c_str(), lifetime, timeout);
492   time_t now=time(NULL);
493   if (lifetime > 0 && now > m_sessionCreated+lifetime) {
494     log->debug("session beyond lifetime");
495     return false;
496   }
497
498   // Lock the access-time from here until we return
499   Lock lock(access_lock);
500   if (timeout > 0 && now-m_lastAccess >= timeout) {
501     log->debug("session timed out");
502     return false;
503   }
504   m_lastAccess=now;
505   return true;
506 }
507
508 Iterator<SAMLAssertion*> InternalCCacheEntry::getAssertions(Resource& resource)
509 {
510   saml::NDC ndc("getAssertions");
511   ResourceEntry* entry = populate(resource, 0);
512   if (entry)
513     return entry->getAssertions();
514   return Iterator<SAMLAssertion*>(InternalCCacheEntry::g_emptyVector);
515 }
516
517 void InternalCCacheEntry::preFetch(Resource& resource, int prefetch_window)
518 {
519   saml::NDC ndc("preFetch");
520   ResourceEntry* entry = populate(resource, prefetch_window);
521 }
522
523 ResourceEntry* InternalCCacheEntry::populate(Resource& resource, int slop)
524 {
525   saml::NDC ndc("populate");
526   log->debug("populating entry for %s (%s)",
527              resource.getResource(), resource.getURL());
528
529   // Lock the resource within this entry...
530   InternalCCacheEntry::ResourceLock lock(this, resource.getResource());
531
532   // Can we use what we have?
533   ResourceEntry *entry = find(resource.getResource());
534   if (entry) {
535     log->debug("found resource");
536     if (entry->isValid(slop))
537       return entry;
538
539     // entry is invalid (expired) -- go fetch a new one.
540     log->debug("removing resource cache; assertion is invalid");
541     remove (resource.getResource());
542     delete entry;
543   }
544
545   // Nope, no entry.. Create a new resource entry
546
547   if (!m_hasbinding) {
548     log->error("No binding!");
549     return NULL;
550   }
551
552   log->info("trying to request attributes for %s@%s -> %s",
553             m_handle.c_str(), m_originSite.c_str(), resource.getURL());
554
555   try {
556     entry = new ResourceEntry(resource, *m_subject, m_cache, p_auth->getBindings());
557   } catch (ShibTargetException &e) {
558     return NULL;
559   }
560   insert (resource.getResource(), entry);
561
562   log->info("fetched and stored SAML response");
563   return entry;
564 }
565
566 ResourceEntry* InternalCCacheEntry::find(const char* resource_url)
567 {
568   ReadLock rwlock(resource_lock);
569
570   log->debug("find: %s", resource_url);
571   map<string,ResourceEntry*>::const_iterator i=m_resources.find(resource_url);
572   if (i==m_resources.end()) {
573     log->debug("no match found");
574     return NULL;
575   }
576   log->debug("match found");
577   return i->second;
578 }
579
580 void InternalCCacheEntry::insert(const char* resource, ResourceEntry* entry)
581 {
582   log->debug("inserting %s", resource);
583
584   resource_lock->wrlock();
585   m_resources[resource]=entry;
586   resource_lock->unlock();
587 }
588
589 // caller will delete the entry.. don't worry about that here.
590 void InternalCCacheEntry::remove(const char* resource)
591 {
592   log->debug("removing %s", resource);
593
594   resource_lock->wrlock();
595   m_resources.erase(resource);
596   resource_lock->unlock();
597 }
598
599
600 // a lock on a resource.  This is a specific "table of locks" that
601 // will provide a mutex on a particular resource within a Cache Entry.
602 // Just instantiate a ResourceLock within scope of the function and it
603 // will obtain and hold the proper lock until it goes out of scope and
604 // deconstructs.
605
606 InternalCCacheEntry::ResourceLock::ResourceLock(InternalCCacheEntry* entry,
607                                                 string resource) :
608   entry(entry), resource(resource)
609 {
610   Mutex *mutex = find(resource);
611   mutex->lock();
612 }
613
614 InternalCCacheEntry::ResourceLock::~ResourceLock()
615 {
616   Mutex *mutex = find(resource);
617   mutex->unlock();
618 }
619
620 Mutex* InternalCCacheEntry::ResourceLock::find(string& resource)
621 {
622   Lock(entry->pop_locks_lock);
623   
624   map<string,Mutex*>::const_iterator i=entry->populate_locks.find(resource);
625   if (i==entry->populate_locks.end()) {
626     Mutex* mutex = Mutex::create();
627     entry->populate_locks[resource] = mutex;
628     return mutex;
629   }
630   return i->second;
631 }