Added real-time repair, cleaned up some error handling
[shibboleth/sp.git] / shib-mysql-ccache / shib-mysql-ccache.cpp
1 /*
2  * shib-mysql-ccache.cpp: Shibboleth Credential Cache using MySQL.
3  *
4  * Created by:  Derek Atkins <derek@ihtfp.com>
5  *
6  * $Id$
7  */
8
9 /* This file is loosely based off the Shibboleth Credential Cache.
10  * This plug-in is designed as a two-layer cache.  Layer 1, the
11  * long-term cache, stores data in a MySQL embedded database.  The
12  * data stored in layer 1 is only the session id (cookie), the
13  * "posted" SAML statement (expanded into an XML string), and usage
14  * timestamps.
15  *
16  * Short-term data is cached in memory as SAML objects in the layer 2
17  * cache.  Data like Attribute Authority assertions are stored in
18  * the layer 2 cache.
19  */
20
21 // eventually we might be able to support autoconf via cygwin...
22 #if defined (_MSC_VER) || defined(__BORLANDC__)
23 # include "config_win32.h"
24 #else
25 # include "config.h"
26 #endif
27
28 #ifdef WIN32
29 # define SHIBMYSQL_EXPORTS __declspec(dllexport)
30 #else
31 # define SHIBMYSQL_EXPORTS
32 #endif
33
34 #ifdef HAVE_UNISTD_H
35 # include <unistd.h>
36 #endif
37
38 #include <shib-target/shib-target.h>
39 #include <shib/shib-threads.h>
40 #include <log4cpp/Category.hh>
41
42 #include <sstream>
43 #include <stdexcept>
44
45 #include <mysql.h>
46
47 // wanted to use MySQL codes for this, but can't seem to get back a 145
48 #define isCorrupt(s) strstr(s,"(errno: 145)")
49
50 #ifdef HAVE_LIBDMALLOCXX
51 #include <dmalloc.h>
52 #endif
53
54 using namespace std;
55 using namespace saml;
56 using namespace shibboleth;
57 using namespace shibtarget;
58
59 #define PLUGIN_VER_MAJOR 1
60 #define PLUGIN_VER_MINOR 0
61
62 static const XMLCh Argument[] =
63 { chLatin_A, chLatin_r, chLatin_g, chLatin_u, chLatin_m, chLatin_e, chLatin_n, chLatin_t, chNull };
64 static const XMLCh cleanupInterval[] =
65 { chLatin_c, chLatin_l, chLatin_e, chLatin_a, chLatin_n, chLatin_u, chLatin_p,
66   chLatin_I, chLatin_n, chLatin_t, chLatin_e, chLatin_r, chLatin_v, chLatin_a, chLatin_l, chNull
67 };
68 static const XMLCh cacheTimeout[] =
69 { chLatin_c, chLatin_a, chLatin_c, chLatin_h, chLatin_e, chLatin_T, chLatin_i, chLatin_m, chLatin_e, chLatin_o, chLatin_u, chLatin_t, chNull };
70 static const XMLCh mysqlTimeout[] =
71 { chLatin_m, chLatin_y, chLatin_s, chLatin_q, chLatin_l, chLatin_T, chLatin_i, chLatin_m, chLatin_e, chLatin_o, chLatin_u, chLatin_t, chNull };
72
73 class ShibMySQLCCache;
74 class ShibMySQLCCacheEntry : public ISessionCacheEntry
75 {
76 public:
77   ShibMySQLCCacheEntry(const char*, ISessionCacheEntry*, ShibMySQLCCache*);
78   ~ShibMySQLCCacheEntry() {}
79
80   virtual void lock() {}
81   virtual void unlock() { m_cacheEntry->unlock(); delete this; }
82   virtual bool isValid(time_t lifetime, time_t timeout) const;
83   virtual const char* getClientAddress() const { return m_cacheEntry->getClientAddress(); }
84   virtual const SAMLAuthenticationStatement* getAuthnStatement() const { return m_cacheEntry->getAuthnStatement(); }
85   virtual Iterator<SAMLAssertion*> getAssertions() { return m_cacheEntry->getAssertions(); }
86
87 private:
88   bool touch() const;
89
90   ShibMySQLCCache* m_cache;
91   ISessionCacheEntry* m_cacheEntry;
92   string m_key;
93 };
94
95 class ShibMySQLCCache : public ISessionCache
96 {
97 public:
98   ShibMySQLCCache(const DOMElement* e);
99   virtual ~ShibMySQLCCache();
100
101   virtual void thread_init();
102   virtual void thread_end() {}
103
104   virtual string generateKey() const {return m_cache->generateKey();}
105   virtual ISessionCacheEntry* find(const char* key, const IApplication* application);
106   virtual void insert(
107         const char* key,
108         const IApplication* application,
109         SAMLAuthenticationStatement *s,
110         const char *client_addr,
111         SAMLResponse* r=NULL,
112         const IRoleDescriptor* source=NULL);
113   virtual void remove(const char* key);
114
115   void  cleanup();
116   MYSQL* getMYSQL() const;
117
118   log4cpp::Category* log;
119
120 private:
121   ISessionCache* m_cache;
122   ThreadKey* m_mysql;
123   const DOMElement* m_root; // can only use this during initialization
124
125   static void*  cleanup_fcn(void*); // XXX Assumed an ShibMySQLCCache
126   CondWait* shutdown_wait;
127   bool shutdown;
128   Thread* cleanup_thread;
129
130   bool initialized;
131
132   void createDatabase(MYSQL*, int major, int minor);
133   void upgradeDatabase(MYSQL*);
134   void getVersion(MYSQL*, int* major_p, int* minor_p);
135   bool repairTable(MYSQL*&, const char* table);
136   void mysqlInit(void);
137 };
138
139 // Forward declarations
140 extern "C" void shib_mysql_destroy_handle(void* data);
141
142 /*************************************************************************
143  * The CCache here talks to a MySQL database.  The database stores
144  * three items: the cookie (session key index), the lastAccess time, and
145  * the SAMLAuthenticationStatement.  All other access is performed
146  * through the memory cache provided by shibboleth.
147  */
148
149 MYSQL* ShibMySQLCCache::getMYSQL() const
150 {
151   return (MYSQL*)m_mysql->getData();
152 }
153
154 void ShibMySQLCCache::thread_init()
155 {
156   saml::NDC ndc("thread_init");
157
158   // Connect to the database
159   MYSQL* mysql = mysql_init(NULL);
160   if (!mysql) {
161     log->error("mysql_init failed");
162     mysql_close(mysql);
163     throw SAMLException("ShibMySQLCCache::thread_init(): mysql_init() failed");
164   }
165
166   if (!mysql_real_connect(mysql, NULL, NULL, NULL, "shar", 0, NULL, 0)) {
167     if (initialized) {
168       log->crit("mysql_real_connect failed: %s", mysql_error(mysql));
169       mysql_close(mysql);
170       throw SAMLException("ShibMySQLCCache::thread_init(): mysql_real_connect() failed");
171     } else {
172       log->info("mysql_real_connect failed: %s.  Trying to create", mysql_error(mysql));
173
174       // This will throw an exception if it fails.
175       createDatabase(mysql, PLUGIN_VER_MAJOR, PLUGIN_VER_MINOR);
176     }
177   }
178
179   int major = -1, minor = -1;
180   getVersion (mysql, &major, &minor);
181
182   // Make sure we've got the right version
183   if (major != PLUGIN_VER_MAJOR || minor != PLUGIN_VER_MINOR) {
184    
185     // If we're capable, try upgrading on the fly...
186     if (major == 0 && minor == 0) {
187        upgradeDatabase(mysql);
188     }
189     else {
190         mysql_close(mysql);
191         log->crit("Invalid database version: %d.%d", major, minor);
192         throw SAMLException("ShibMySQLCCache::thread_init(): Invalid database version");
193     }
194   }
195
196   // We're all set.. Save off the handle for this thread.
197   m_mysql->setData(mysql);
198 }
199
200 ShibMySQLCCache::ShibMySQLCCache(const DOMElement* e)
201 {
202   saml::NDC ndc("shibmysql::ShibMySQLCCache");
203
204   m_mysql = ThreadKey::create(&shib_mysql_destroy_handle);
205   log = &(log4cpp::Category::getInstance("shibmysql::ShibMySQLCCache"));
206
207   m_root=e;
208   initialized = false;
209   mysqlInit();
210   thread_init();
211   initialized = true;
212
213   m_cache = dynamic_cast<ISessionCache*>(
214       SAMLConfig::getConfig().getPlugMgr().newPlugin(
215         "edu.internet2.middleware.shibboleth.target.provider.MemorySessionCache", e
216         )
217     );
218
219   // Initialize the cleanup thread
220   shutdown_wait = CondWait::create();
221   shutdown = false;
222   cleanup_thread = Thread::create(&cleanup_fcn, (void*)this);
223 }
224
225 ShibMySQLCCache::~ShibMySQLCCache()
226 {
227   shutdown = true;
228   shutdown_wait->signal();
229   cleanup_thread->join(NULL);
230
231   thread_end();
232   delete m_cache;
233   delete m_mysql;
234
235   // Shutdown MySQL
236   mysql_server_end();
237 }
238
239 ISessionCacheEntry* ShibMySQLCCache::find(const char* key, const IApplication* application)
240 {
241   saml::NDC ndc("ShibMySQLCCache::find");
242   ISessionCacheEntry* res = m_cache->find(key, application);
243   if (!res) {
244
245     log->debug("Looking in database...");
246
247     // nothing cached; see if this exists in the database
248     string q = string("SELECT application_id,addr,statement FROM state WHERE cookie='") + key + "' LIMIT 1";
249
250     MYSQL_RES* rows;
251     MYSQL* mysql = getMYSQL();
252     if (mysql_query(mysql, q.c_str())) {
253       const char* err=mysql_error(mysql);
254       log->error("Error searching for %s: %s", key, err);
255       if (isCorrupt(err) && repairTable(mysql,"state")) {
256         if (mysql_query(mysql, q.c_str()))
257           log->error("Error retrying search for %s: %s", key, mysql_error(mysql));
258       }
259     }
260
261     rows = mysql_store_result(mysql);
262
263     // Nope, doesn't exist.
264     if (!rows)
265       return NULL;
266
267     // Make sure we got 1 and only 1 rows.
268     if (mysql_num_rows(rows) != 1) {
269       log->error("Select returned wrong number of rows: %d", mysql_num_rows(rows));
270       mysql_free_result(rows);
271       return NULL;
272     }
273
274     log->debug("Match found.  Parsing...");
275
276     // Pull apart the row and process the results
277     MYSQL_ROW row = mysql_fetch_row(rows);
278     IConfig* conf=ShibTargetConfig::getConfig().getINI();
279     Locker locker(conf);
280     const IApplication* application=conf->getApplication(row[0]);
281     if (!application) {
282         mysql_free_result(rows);
283         throw ShibTargetException(SHIBRPC_INTERNAL_ERROR,"unable to locate application for session, deleted?");
284     }
285     else if (strcmp(row[0],application->getId())) {
286         log->crit("An application (%s) attempted to access another application's (%s) session!", application->getId(), row[0]);
287         mysql_free_result(rows);
288         return NULL;
289     }
290
291     istringstream str(row[2]);
292     SAMLAuthenticationStatement *s = NULL;
293
294     // Try to parse the AuthStatement
295     try {
296       s = new SAMLAuthenticationStatement(str);
297     } catch (...) {
298       mysql_free_result(rows);
299       throw;
300     }
301
302     // Insert it into the memory cache
303     if (s)
304       m_cache->insert(key, application, s, row[1]);
305
306     // Free the results, and then re-run the 'find' query
307     mysql_free_result(rows);
308     res = m_cache->find(key,application);
309     if (!res)
310       return NULL;
311   }
312
313   return new ShibMySQLCCacheEntry(key, res, this);
314 }
315
316 void ShibMySQLCCache::insert(
317     const char* key,
318     const IApplication* application,
319     saml::SAMLAuthenticationStatement *s,
320     const char *client_addr,
321     saml::SAMLResponse* r,
322     const IRoleDescriptor* source)
323 {
324   saml::NDC ndc("ShibMySQLCCache::insert");
325   ostringstream os;
326   os << *s;
327
328   string q = string("INSERT INTO state VALUES('") + key + "','" + application->getId() + "',NOW(),'" + client_addr + "','" + os.str() + "')";
329
330   log->debug("Query: %s", q.c_str());
331
332   // then add it to the database
333   MYSQL* mysql = getMYSQL();
334   if (mysql_query(mysql, q.c_str())) {
335     const char* err=mysql_error(mysql);
336     log->error("Error inserting %s: %s", key, err);
337     if (isCorrupt(err) && repairTable(mysql,"state")) {
338         // Try again...
339         if (mysql_query(mysql, q.c_str()))
340           log->error("Error inserting %s: %s", key, mysql_error(mysql));
341           throw SAMLException("ShibMySQLCCache::insert(): inset failed");
342     }
343   }
344
345   // Add it to the memory cache
346   m_cache->insert(key, application, s, client_addr, r, source);
347 }
348
349 void ShibMySQLCCache::remove(const char* key)
350 {
351   saml::NDC ndc("ShibMySQLCCache::remove");
352
353   // Remove the cached version
354   m_cache->remove(key);
355
356   // Remove from the database
357   string q = string("DELETE FROM state WHERE cookie='") + key + "'";
358   MYSQL* mysql = getMYSQL();
359   if (mysql_query(mysql, q.c_str())) {
360     const char* err=mysql_error(mysql);
361     log->error("Error deleting entry %s: %s", key, err);
362     if (isCorrupt(err) && repairTable(mysql,"state")) {
363         // Try again...
364         if (mysql_query(mysql, q.c_str()))
365           log->error("Error deleting entry %s: %s", key, mysql_error(mysql));
366     }
367   }
368 }
369
370 void ShibMySQLCCache::cleanup()
371 {
372   Mutex* mutex = Mutex::create();
373   saml::NDC ndc("ShibMySQLCCache::cleanup");
374
375   thread_init();
376
377   int rerun_timer = 0;
378   int timeout_life = 0;
379
380   // Load our configuration details...
381   const XMLCh* tag=m_root->getAttributeNS(NULL,cleanupInterval);
382   if (tag && *tag)
383     rerun_timer = XMLString::parseInt(tag);
384
385   // search for 'mysql-cache-timeout' and then the regular cache timeout
386   tag=m_root->getAttributeNS(NULL,mysqlTimeout);
387   if (tag && *tag)
388     timeout_life = XMLString::parseInt(tag);
389   else {
390       tag=m_root->getAttributeNS(NULL,cacheTimeout);
391       if (tag && *tag)
392         timeout_life = XMLString::parseInt(tag);
393   }
394   
395   if (rerun_timer <= 0)
396     rerun_timer = 300;          // rerun every 5 minutes
397
398   if (timeout_life <= 0)
399     timeout_life = 28800;       // timeout after 8 hours
400
401   mutex->lock();
402
403   MYSQL* mysql = getMYSQL();
404
405   while (shutdown == false) {
406     shutdown_wait->timedwait(mutex, rerun_timer);
407
408     if (shutdown == true)
409       break;
410
411     // Find all the entries in the database that haven't been used
412     // recently In particular, find all entries that have not been
413     // accessed in 'timeout_life' seconds.
414     ostringstream q;
415     q << "SELECT cookie FROM state WHERE " <<
416       "UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(atime) >= " << timeout_life;
417
418     MYSQL_RES *rows;
419     if (mysql_query(mysql, q.str().c_str())) {
420       const char* err=mysql_error(mysql);
421       log->error("Error searching for old items: %s", err);
422         if (isCorrupt(err) && repairTable(mysql,"state")) {
423           if (mysql_query(mysql, q.str().c_str()))
424             log->error("Error re-searching for old items: %s", mysql_error(mysql));
425         }
426     }
427
428     rows = mysql_store_result(mysql);
429     if (!rows)
430       continue;
431
432     if (mysql_num_fields(rows) != 1) {
433       log->error("Wrong number of rows, 1 != %d", mysql_num_fields(rows));
434       mysql_free_result(rows);
435       continue;
436     }
437
438     // For each row, remove the entry from the database.
439     MYSQL_ROW row;
440     while ((row = mysql_fetch_row(rows)) != NULL)
441       remove(row[0]);
442
443     mysql_free_result(rows);
444   }
445
446   log->debug("cleanup thread exiting...");
447
448   mutex->unlock();
449   delete mutex;
450   thread_end();
451   Thread::exit(NULL);
452 }
453
454 void* ShibMySQLCCache::cleanup_fcn(void* cache_p)
455 {
456   ShibMySQLCCache* cache = (ShibMySQLCCache*)cache_p;
457
458   // First, let's block all signals
459   Thread::mask_all_signals();
460
461   // Now run the cleanup process.
462   cache->cleanup();
463   return NULL;
464 }
465
466 bool ShibMySQLCCache::repairTable(MYSQL*& mysql, const char* table)
467 {
468   string q = string("REPAIR TABLE ") + table;
469   if (mysql_query(mysql, q.c_str())) {
470     log->error("Error repairing table %s: %s", table, mysql_error(mysql));
471     return false;
472   }
473
474   // seems we have to recycle the connection to get the thread to keep working
475   // other threads seem to be ok, but we should monitor that
476   mysql_close(mysql);
477   m_mysql->setData(NULL);
478   thread_init();
479   mysql=getMYSQL();
480   return true;
481 }
482
483 void ShibMySQLCCache::createDatabase(MYSQL* mysql, int major, int minor)
484 {
485   log->info("Creating database.");
486
487   MYSQL* ms = NULL;
488   try {
489     ms = mysql_init(NULL);
490     if (!ms) {
491       log->crit("mysql_init failed");
492       throw SAMLException("ShibMySQLCCache::createDatabase(): mysql_init failed");
493     }
494
495     if (!mysql_real_connect(ms, NULL, NULL, NULL, NULL, 0, NULL, 0)) {
496       log->crit("cannot open DB file to create DB: %s", mysql_error(ms));
497       throw SAMLException("ShibMySQLCCache::createDatabase(): mysql_real_connect failed");
498     }
499
500     if (mysql_query(ms, "CREATE DATABASE shar")) {
501       log->crit("cannot create shar database: %s", mysql_error(ms));
502       throw SAMLException("ShibMySQLCCache::createDatabase(): create db cmd failed");
503     }
504
505     if (!mysql_real_connect(mysql, NULL, NULL, NULL, "shar", 0, NULL, 0)) {
506       log->crit("cannot open SHAR database");
507       throw SAMLException("ShibMySQLCCache::createDatabase(): mysql_real_connect to shar db failed");
508     }
509
510     mysql_close(ms);
511     
512   }
513   catch (SAMLException&) {
514     if (ms)
515       mysql_close(ms);
516     mysql_close(mysql);
517     throw;
518   }
519
520   // Now create the tables if they don't exist
521   log->info("Creating database tables");
522
523   if (mysql_query(mysql, "CREATE TABLE version (major INT, minor INT)")) {
524     log->error ("Error creating version: %s", mysql_error(mysql));
525     throw SAMLException("ShibMySQLCCache::createDatabase(): create table cmd failed");
526   }
527
528   if (mysql_query(mysql,
529                   "CREATE TABLE state (cookie VARCHAR(64) PRIMARY KEY, application_id VARCHAR(255),"
530                   "atime DATETIME, addr VARCHAR(128), statement TEXT)")) {
531     log->error ("Error creating state: %s", mysql_error(mysql));
532     throw SAMLException("ShibMySQLCCache::createDatabase(): create table cmd failed");
533   }
534
535   ostringstream q;
536   q << "INSERT INTO version VALUES(" << major << "," << minor << ")";
537   if (mysql_query(mysql, q.str().c_str())) {
538     log->error ("Error setting version: %s", mysql_error(mysql));
539     throw SAMLException("ShibMySQLCCache::createDatabase(): version insert failed");
540   }
541 }
542
543 void ShibMySQLCCache::upgradeDatabase(MYSQL* mysql)
544 {
545     if (mysql_query(mysql, "DROP TABLE state")) {
546         log->error("Error dropping old session state table: %s", mysql_error(mysql));
547     }
548
549     if (mysql_query(mysql,
550         "CREATE TABLE state (cookie VARCHAR(64) PRIMARY KEY, application_id VARCHAR(255),"
551        "atime DATETIME, addr VARCHAR(128), statement TEXT)")) {
552         log->error ("Error creating state table: %s", mysql_error(mysql));
553         throw SAMLException("ShibMySQLCCache::upgradeDatabase(): error creating state table");
554     }
555
556     ostringstream q;
557     q << "UPDATE version SET major = " << PLUGIN_VER_MAJOR;
558     if (mysql_query(mysql, q.str().c_str())) {
559         log->error ("Error updating version: %s", mysql_error(mysql));
560         throw SAMLException("ShibMySQLCCache::upgradeDatabase(): error updating version");
561     }
562 }
563
564 void ShibMySQLCCache::getVersion(MYSQL* mysql, int* major_p, int* minor_p)
565 {
566   // grab the version number from the database
567   if (mysql_query(mysql, "SELECT * FROM version"))
568     log->error ("Error reading version: %s", mysql_error(mysql));
569
570   MYSQL_RES* rows = mysql_store_result(mysql);
571   if (rows) {
572     if (mysql_num_rows(rows) == 1 && mysql_num_fields(rows) == 2)  {
573       MYSQL_ROW row = mysql_fetch_row(rows);
574
575       int major = row[0] ? atoi(row[0]) : -1;
576       int minor = row[1] ? atoi(row[1]) : -1;
577       log->debug("opening database version %d.%d", major, minor);
578       
579       mysql_free_result (rows);
580
581       *major_p = major;
582       *minor_p = minor;
583       return;
584
585     } else {
586       // Wrong number of rows or wrong number of fields...
587
588       log->crit("Houston, we've got a problem with the database...");
589       mysql_free_result (rows);
590       throw SAMLException("ShibMySQLCCache::getVersion(): version verification failed");
591     }
592   }
593   log->crit("MySQL Read Failed in version verificatoin");
594   throw SAMLException("ShibMySQLCCache::getVersion(): error reading version");
595 }
596
597 void ShibMySQLCCache::mysqlInit(void)
598 {
599   log->info ("Opening MySQL Database");
600
601   // Setup the argument array
602   vector<string> arg_array;
603   arg_array.push_back("shar");
604
605   // grab any MySQL parameters from the config file
606   const DOMElement* e=saml::XML::getFirstChildElement(m_root,ShibTargetConfig::SHIBTARGET_NS,Argument);
607   while (e) {
608       auto_ptr_char arg(e->getFirstChild()->getNodeValue());
609       if (arg.get())
610           arg_array.push_back(arg.get());
611       e=saml::XML::getNextSiblingElement(e,ShibTargetConfig::SHIBTARGET_NS,Argument);
612   }
613
614   // Compute the argument array
615   int arg_count = arg_array.size();
616   const char** args=new const char*[arg_count];
617   for (int i = 0; i < arg_count; i++)
618     args[i] = arg_array[i].c_str();
619
620   // Initialize MySQL with the arguments
621   mysql_server_init(arg_count, (char **)args, NULL);
622
623   delete[] args;
624 }  
625
626 /*************************************************************************
627  * The CCacheEntry here is mostly a wrapper around the "memory"
628  * cacheentry provided by shibboleth.  The only difference is that we
629  * intercept the isSessionValid() so that we can "touch()" the
630  * database if the session is still valid.
631  */
632
633 ShibMySQLCCacheEntry::ShibMySQLCCacheEntry(const char* key, ISessionCacheEntry* entry, ShibMySQLCCache* cache)
634 {
635   m_cacheEntry = entry;
636   m_key = key;
637   m_cache = cache;
638 }
639
640 bool ShibMySQLCCacheEntry::isValid(time_t lifetime, time_t timeout) const
641 {
642   bool res = m_cacheEntry->isValid(lifetime, timeout);
643   if (res == true)
644     res = touch();
645   return res;
646 }
647
648 bool ShibMySQLCCacheEntry::touch() const
649 {
650   string q=string("UPDATE state SET atime=NOW() WHERE cookie='") + m_key + "'";
651
652   MYSQL* mysql = m_cache->getMYSQL();
653   if (mysql_query(mysql, q.c_str())) {
654     m_cache->log->info("Error updating timestamp on %s: %s",
655                         m_key.c_str(), mysql_error(mysql));
656     return false;
657   }
658   return true;
659 }
660
661 /*************************************************************************
662  * The registration functions here...
663  */
664
665 IPlugIn* new_mysql_ccache(const DOMElement* e)
666 {
667   return new ShibMySQLCCache(e);
668 }
669
670 #define PLUGINTYPE "edu.internet2.middleware.shibboleth.target.provider.MySQLSessionCache"
671
672 extern "C" int SHIBMYSQL_EXPORTS saml_extension_init(void*)
673 {
674   // register this ccache type
675   SAMLConfig::getConfig().getPlugMgr().regFactory(PLUGINTYPE, &new_mysql_ccache);
676   return 0;
677 }
678
679 extern "C" void SHIBMYSQL_EXPORTS saml_extension_term()
680 {
681   SAMLConfig::getConfig().getPlugMgr().unregFactory(PLUGINTYPE);
682 }
683
684 /*************************************************************************
685  * Local Functions
686  */
687
688 extern "C" void shib_mysql_destroy_handle(void* data)
689 {
690   MYSQL* mysql = (MYSQL*) data;
691   mysql_close(mysql);
692 }