* ccache-utils.h shib-ccache.cpp:
[shibboleth/cpp-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 #if HAVE_UNISTD_H
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 const char* getSerializedStatement() { return m_statement.c_str(); }
94   virtual void release() { cacheitem_lock->unlock(); }
95
96   void setCache(InternalCCache *cache) { m_cache = cache; }
97   time_t lastAccess() { Lock lock(access_lock); return m_lastAccess; }
98   void rdlock() { cacheitem_lock->rdlock(); }
99   void wrlock() { cacheitem_lock->wrlock(); }
100
101   static vector<SAMLAssertion*> g_emptyVector;
102
103 private:
104   ResourceEntry* populate(Resource& resource, int slop);
105   ResourceEntry* find(const char* resource);
106   void insert(const char* resource, ResourceEntry* entry);
107   void remove(const char* resource);
108
109   string m_statement;
110   string m_originSite;
111   string m_handle;
112   string m_clientAddress;
113   time_t m_sessionCreated;
114   time_t m_lastAccess;
115   bool m_hasbinding;
116
117   const SAMLSubject* m_subject;
118   SAMLAuthenticationStatement* p_auth;
119   InternalCCache *m_cache;
120
121   map<string,ResourceEntry*> m_resources;
122
123   log4cpp::Category* log;
124
125   // This is used to keep track of in-process "populate()" calls,
126   // to make sure that we don't try to populate the same resource
127   // in multiple threads.
128   map<string,Mutex*>    populate_locks;
129   Mutex*        pop_locks_lock;
130
131   Mutex*        access_lock;
132   RWLock*       resource_lock;
133   RWLock*       cacheitem_lock;
134
135   class ResourceLock
136   {
137   public:
138     ResourceLock(InternalCCacheEntry* entry, string resource);
139     ~ResourceLock();
140
141   private:
142     Mutex*                      find(string& resource);
143     InternalCCacheEntry*        entry;
144     string                      resource;
145   };
146
147   friend class ResourceLock;
148 };
149
150 class InternalCCache : public CCache
151 {
152 public:
153   InternalCCache();
154   virtual ~InternalCCache();
155
156   virtual SAMLBinding* getBinding(const XMLCh* bindingProt);
157   virtual CCacheEntry* find(const char* key);
158   virtual void insert(const char* key, SAMLAuthenticationStatement *s,
159                       const char *client_addr);
160   virtual void remove(const char* key);
161
162   InternalCCacheEntry* findi(const char* key);
163   void  cleanup();
164
165 private:
166   RWLock *lock;
167
168   SAMLBinding* m_SAMLBinding;
169   map<string,InternalCCacheEntry*> m_hashtable;
170
171   log4cpp::Category* log;
172
173   static void*  cleanup_fcn(void*); // XXX Assumed an InternalCCache
174   bool          shutdown;
175   CondWait*     shutdown_wait;
176   Thread*       cleanup_thread;
177 };
178
179 namespace {
180   map<string,CCache::CCacheFactory> g_ccacheFactoryDB;
181 };
182
183 // Global Constructors & Destructors
184 CCache::~CCache() { }
185
186 void CCache::registerFactory(const char* name, CCache::CCacheFactory factory)
187 {
188   string ctx = "shibtarget.CCache";
189   log4cpp::Category& log = log4cpp::Category::getInstance(ctx);
190   saml::NDC ndc("registerFactory");
191
192   log.info ("Registered factory %p for CCache %s", factory, name);
193   g_ccacheFactoryDB[name] = factory;
194 }
195
196 CCache* CCache::getInstance(const char* type)
197 {
198   string ctx = "shibtarget.CCache";
199   log4cpp::Category& log = log4cpp::Category::getInstance(ctx);
200   saml::NDC ndc("getInstance");
201
202   map<string,CCache::CCacheFactory>::const_iterator i=g_ccacheFactoryDB.find(type);
203   if (i!=g_ccacheFactoryDB.end()) {
204     log.info ("Loading CCache: %s at %p", type, i->second);
205     return ((i->second)());
206   }
207
208   log.info ("Loading default memory CCache");
209   return (CCache*) new InternalCCache();
210 }
211
212 // static members
213 vector<SAMLAssertion*> InternalCCacheEntry::g_emptyVector;
214
215
216 /******************************************************************************/
217 /* InternalCCache:  A Credential Cache                                        */
218 /******************************************************************************/
219
220 InternalCCache::InternalCCache()
221 {
222   m_SAMLBinding=SAMLBindingFactory::getInstance();
223   string ctx="shibtarget.InternalCCache";
224   log = &(log4cpp::Category::getInstance(ctx));
225   lock = RWLock::create();
226
227   shutdown_wait = CondWait::create();
228   shutdown = false;
229   cleanup_thread = Thread::create(&cleanup_fcn, (void*)this);
230 }
231
232 InternalCCache::~InternalCCache()
233 {
234   // Shut down the cleanup thread and let it know...
235   shutdown = true;
236   shutdown_wait->signal();
237   cleanup_thread->join(NULL);
238
239   delete m_SAMLBinding;
240   for (map<string,InternalCCacheEntry*>::iterator i=m_hashtable.begin(); i!=m_hashtable.end(); i++)
241     delete i->second;
242   delete lock;
243   delete shutdown_wait;
244 }
245
246 SAMLBinding* InternalCCache::getBinding(const XMLCh* bindingProt)
247 {
248   log->debug("looking for binding...");
249   if (!XMLString::compareString(bindingProt,SAMLBinding::SAML_SOAP_HTTPS)) {
250     log->debug("https binding found");
251     return m_SAMLBinding;
252   }
253   return NULL;
254 }
255
256 // assumed a lock is held..
257 InternalCCacheEntry* InternalCCache::findi(const char* key)
258 {
259   log->debug("FindI: \"%s\"", key);
260
261   map<string,InternalCCacheEntry*>::const_iterator i=m_hashtable.find(key);
262   if (i==m_hashtable.end()) {
263     log->debug("No Match found");
264     return NULL;
265   }
266   log->debug("Match Found.");
267
268   return i->second;
269 }
270
271 CCacheEntry* InternalCCache::find(const char* key)
272 {
273   log->debug("Find: \"%s\"", key);
274   ReadLock rwlock(lock);
275
276   InternalCCacheEntry* entry = findi(key);
277   if (!entry) return NULL;
278
279   // Lock the database for the caller -- they have to release the item.
280   entry->rdlock();
281   return dynamic_cast<CCacheEntry*>(entry);
282 }
283
284 void InternalCCache::insert(const char* key, SAMLAuthenticationStatement *s,
285                             const char *client_addr)
286 {
287   log->debug("caching new entry for \"%s\"", key);
288
289   InternalCCacheEntry* entry = new InternalCCacheEntry (s, client_addr);
290   entry->setCache(this);
291
292   lock->wrlock();
293   m_hashtable[key]=entry;
294   lock->unlock();
295 }
296
297 // remove the entry from the database and then destroy the cacheentry
298 void InternalCCache::remove(const char* key)
299 {
300   log->debug("removing cache entry \"key\"", key);
301
302   // grab the entry from the database.  We'll have a readlock on it.
303   CCacheEntry* entry = findi(key);
304
305   if (!entry)
306     return;
307
308   // grab the cache write lock
309   lock->wrlock();
310
311   // verify we've still got the same entry.
312   if (entry != findi(key)) {
313     // Nope -- must've already been removed.
314     lock->unlock();
315     return;
316   }
317
318   // ok, remove the entry.
319   m_hashtable.erase(key);
320   lock->unlock();
321
322   // now grab the write lock on the cacheitem.
323   // This will make sure all other threads have released this item.
324   InternalCCacheEntry* ientry = dynamic_cast<InternalCCacheEntry*>(entry);
325   ientry->wrlock();
326
327   // we can release immediately because we know we're not in the database!
328   ientry->release();
329
330   // Now delete the entry
331   delete ientry;
332 }
333
334 void InternalCCache::cleanup()
335 {
336   Mutex* mutex = Mutex::create();
337   saml::NDC ndc("InternalCCache::cleanup()");
338
339   ShibTargetConfig& config = ShibTargetConfig::getConfig();
340   ShibINI& ini = config.getINI();
341
342   int rerun_timer = 0;
343   int timeout_life = 0;
344
345   string tag;
346   if (ini.get_tag (SHIBTARGET_SHAR, SHIBTARGET_TAG_CACHECLEAN, true, &tag))
347     rerun_timer = atoi(tag.c_str());
348   if (ini.get_tag (SHIBTARGET_SHAR, SHIBTARGET_TAG_CACHETIMEOUT, true, &tag))
349     timeout_life = atoi(tag.c_str());
350
351   if (rerun_timer <= 0)
352     rerun_timer = 300;          // rerun every 5 minutes
353
354   if (timeout_life <= 0)
355     timeout_life = 28800;       // timeout after 8 hours
356
357   mutex->lock();
358
359   log->debug("Cleanup thread started...  Run every %d secs; timeout after %d secs",
360              rerun_timer, timeout_life);
361
362   while (shutdown == false) {
363     struct timespec ts;
364     memset (&ts, 0, sizeof(ts));
365     ts.tv_sec = time(NULL) + rerun_timer;
366
367     shutdown_wait->timedwait(mutex, &ts);
368
369     if (shutdown == true)
370       break;
371
372     log->info("Cleanup thread running...");
373
374     // Ok, let's run through the cleanup process and clean out
375     // really old sessions.  This is a two-pass process.  The
376     // first pass is done holding a read-lock while we iterate over
377     // the database.  The second pass doesn't need a lock because
378     // the 'deletes' will lock the database.
379
380     // Pass 1: iterate over the map and find all entries that have not been
381     // used in X hours
382     vector<string> stale_keys;
383     time_t stale = time(NULL) - timeout_life;
384
385     lock->rdlock();
386     for (map<string,InternalCCacheEntry*>::iterator i=m_hashtable.begin();
387          i != m_hashtable.end(); i++)
388     {
389       // If the last access was BEFORE the stale timeout...
390       time_t last=i->second->lastAccess();
391       if (last < stale)
392         stale_keys.push_back(i->first);
393     }
394     lock->unlock();
395
396     log->info("deleting %d old items.", stale_keys.size());
397
398     // Pass 2: walk through the list of stale entries and remove them from
399     // the database
400     for (vector<string>::iterator j = stale_keys.begin();
401          j != stale_keys.end(); j++)
402     {
403       remove (j->c_str());
404     }
405
406   }
407
408   log->debug("Cleanup thread finished.");
409
410   mutex->unlock();
411   delete mutex;
412   Thread::exit(NULL);
413 }
414
415 void* InternalCCache::cleanup_fcn(void* cache_p)
416 {
417   InternalCCache* cache = (InternalCCache*)cache_p;
418
419   // First, let's block all signals
420   sigset_t sigmask;
421   sigfillset(&sigmask);
422   Thread::mask_signals(SIG_BLOCK, &sigmask, NULL);
423
424   // Now run the cleanup process.
425   cache->cleanup();
426 }
427
428 /******************************************************************************/
429 /* InternalCCacheEntry:  A Credential Cache Entry                             */
430 /******************************************************************************/
431
432 InternalCCacheEntry::InternalCCacheEntry(SAMLAuthenticationStatement *s, const char *client_addr)
433   : m_hasbinding(false)
434 {
435   string ctx = "shibtarget::InternalCCacheEntry";
436   log = &(log4cpp::Category::getInstance(ctx));
437   pop_locks_lock = Mutex::create();
438   access_lock = Mutex::create();
439   resource_lock = RWLock::create();
440   cacheitem_lock = RWLock::create();
441
442   if (s == NULL) {
443     log->error("NULL auth statement");
444     throw runtime_error("InternalCCacheEntry() was passed an empty SAML Statement");
445   }
446
447   m_subject = s->getSubject();
448
449   xstring name = m_subject->getName();
450   xstring qual = m_subject->getNameQualifier();
451
452   auto_ptr<char> h(XMLString::transcode(name.c_str()));
453   auto_ptr<char> d(XMLString::transcode(qual.c_str()));
454
455   m_handle = h.get();
456   m_originSite = d.get();
457
458   Iterator<SAMLAuthorityBinding*> bindings = s->getBindings();
459   if (bindings.hasNext())
460     m_hasbinding = true;
461
462   m_clientAddress = client_addr;
463   m_sessionCreated = m_lastAccess = time(NULL);
464
465   // Save for later.
466   p_auth = s;
467
468   // Save the serialized version of the auth statement
469   ostringstream os;
470   os << *s;
471   m_statement = os.str();
472
473   log->info("New Session Created...");
474   log->debug("Handle: \"%s\", Site: \"%s\", Address: %s", h.get(), d.get(),
475              client_addr);
476 }
477
478 InternalCCacheEntry::~InternalCCacheEntry()
479 {
480   log->debug("deleting entry for %s@%s", m_handle.c_str(), m_originSite.c_str());
481   delete p_auth;
482   for (map<string,ResourceEntry*>::iterator i=m_resources.begin();
483        i!=m_resources.end(); i++)
484     delete i->second;
485
486   for (map<string,Mutex*>::iterator j=populate_locks.begin();
487        j!=populate_locks.end(); j++)
488     delete j->second;
489
490   delete pop_locks_lock;
491   delete cacheitem_lock;
492   delete resource_lock;
493   delete access_lock;
494 }
495
496 bool InternalCCacheEntry::isSessionValid(time_t lifetime, time_t timeout)
497 {
498   saml::NDC ndc("isSessionValid");
499   log->debug("test session %s@%s, (lifetime=%ld, timeout=%ld)",
500              m_handle.c_str(), m_originSite.c_str(), lifetime, timeout);
501   time_t now=time(NULL);
502   if (lifetime > 0 && now > m_sessionCreated+lifetime) {
503     log->debug("session beyond lifetime");
504     return false;
505   }
506
507   // Lock the access-time from here until we return
508   Lock lock(access_lock);
509   if (timeout > 0 && now-m_lastAccess >= timeout) {
510     log->debug("session timed out");
511     return false;
512   }
513   m_lastAccess=now;
514   return true;
515 }
516
517 Iterator<SAMLAssertion*> InternalCCacheEntry::getAssertions(Resource& resource)
518 {
519   saml::NDC ndc("getAssertions");
520   ResourceEntry* entry = populate(resource, 0);
521   if (entry)
522     return entry->getAssertions();
523   return Iterator<SAMLAssertion*>(InternalCCacheEntry::g_emptyVector);
524 }
525
526 void InternalCCacheEntry::preFetch(Resource& resource, int prefetch_window)
527 {
528   saml::NDC ndc("preFetch");
529   ResourceEntry* entry = populate(resource, prefetch_window);
530 }
531
532 ResourceEntry* InternalCCacheEntry::populate(Resource& resource, int slop)
533 {
534   saml::NDC ndc("populate");
535   log->debug("populating entry for %s (%s)",
536              resource.getResource(), resource.getURL());
537
538   // Lock the resource within this entry...
539   InternalCCacheEntry::ResourceLock lock(this, resource.getResource());
540
541   // Can we use what we have?
542   ResourceEntry *entry = find(resource.getResource());
543   if (entry) {
544     log->debug("found resource");
545     if (entry->isValid(slop))
546       return entry;
547
548     // entry is invalid (expired) -- go fetch a new one.
549     log->debug("removing resource cache; assertion is invalid");
550     remove (resource.getResource());
551     delete entry;
552   }
553
554   // Nope, no entry.. Create a new resource entry
555
556   if (!m_hasbinding) {
557     log->error("No binding!");
558     return NULL;
559   }
560
561   log->info("trying to request attributes for %s@%s -> %s",
562             m_handle.c_str(), m_originSite.c_str(), resource.getURL());
563
564   try {
565     entry = new ResourceEntry(resource, *m_subject, m_cache, p_auth->getBindings());
566   } catch (ShibTargetException &e) {
567     return NULL;
568   }
569   insert (resource.getResource(), entry);
570
571   log->info("fetched and stored SAML response");
572   return entry;
573 }
574
575 ResourceEntry* InternalCCacheEntry::find(const char* resource_url)
576 {
577   ReadLock rwlock(resource_lock);
578
579   log->debug("find: %s", resource_url);
580   map<string,ResourceEntry*>::const_iterator i=m_resources.find(resource_url);
581   if (i==m_resources.end()) {
582     log->debug("no match found");
583     return NULL;
584   }
585   log->debug("match found");
586   return i->second;
587 }
588
589 void InternalCCacheEntry::insert(const char* resource, ResourceEntry* entry)
590 {
591   log->debug("inserting %s", resource);
592
593   resource_lock->wrlock();
594   m_resources[resource]=entry;
595   resource_lock->unlock();
596 }
597
598 // caller will delete the entry.. don't worry about that here.
599 void InternalCCacheEntry::remove(const char* resource)
600 {
601   log->debug("removing %s", resource);
602
603   resource_lock->wrlock();
604   m_resources.erase(resource);
605   resource_lock->unlock();
606 }
607
608
609 // a lock on a resource.  This is a specific "table of locks" that
610 // will provide a mutex on a particular resource within a Cache Entry.
611 // Just instantiate a ResourceLock within scope of the function and it
612 // will obtain and hold the proper lock until it goes out of scope and
613 // deconstructs.
614
615 InternalCCacheEntry::ResourceLock::ResourceLock(InternalCCacheEntry* entry,
616                                                 string resource) :
617   entry(entry), resource(resource)
618 {
619   Mutex *mutex = find(resource);
620   mutex->lock();
621 }
622
623 InternalCCacheEntry::ResourceLock::~ResourceLock()
624 {
625   Mutex *mutex = find(resource);
626   mutex->unlock();
627 }
628
629 Mutex* InternalCCacheEntry::ResourceLock::find(string& resource)
630 {
631   Lock(entry->pop_locks_lock);
632   
633   map<string,Mutex*>::const_iterator i=entry->populate_locks.find(resource);
634   if (i==entry->populate_locks.end()) {
635     Mutex* mutex = Mutex::create();
636     entry->populate_locks[resource] = mutex;
637     return mutex;
638   }
639   return i->second;
640 }