Add attribute response caching
[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 using namespace log4cpp;
59
60 #define PLUGIN_VER_MAJOR 2
61 #define PLUGIN_VER_MINOR 0
62
63 #define STATE_TABLE \
64   "CREATE TABLE state (cookie VARCHAR(64) PRIMARY KEY, " \
65   "application_id VARCHAR(255)," \
66   "ctime TIMESTAMP," \
67   "atime TIMESTAMP," \
68   "addr VARCHAR(128)," \
69   "profile INT," \
70   "provider VARCHAR(256)," \
71   "response_id VARCHAR(128)," \
72   "response TEXT," \
73   "statement TEXT)"
74
75 static const XMLCh Argument[] =
76 { chLatin_A, chLatin_r, chLatin_g, chLatin_u, chLatin_m, chLatin_e, chLatin_n, chLatin_t, chNull };
77 static const XMLCh cleanupInterval[] =
78 { chLatin_c, chLatin_l, chLatin_e, chLatin_a, chLatin_n, chLatin_u, chLatin_p,
79   chLatin_I, chLatin_n, chLatin_t, chLatin_e, chLatin_r, chLatin_v, chLatin_a, chLatin_l, chNull
80 };
81 static const XMLCh cacheTimeout[] =
82 { 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 };
83 static const XMLCh mysqlTimeout[] =
84 { 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 };
85 static const XMLCh storeAttributes[] =
86 { chLatin_s, chLatin_t, chLatin_o, chLatin_r, chLatin_e, chLatin_A, chLatin_t, chLatin_t, chLatin_r, chLatin_i, chLatin_b, chLatin_u, chLatin_t, chLatin_e, chLatin_s, chNull };
87
88 class ShibMySQLCCache;
89 class ShibMySQLCCacheEntry : public ISessionCacheEntry
90 {
91 public:
92   ShibMySQLCCacheEntry(const char* key, ISessionCacheEntry* entry, ShibMySQLCCache* cache)
93     : m_cacheEntry(entry), m_key(key), m_cache(cache), m_responseId(NULL) {}
94   ~ShibMySQLCCacheEntry() {if (m_responseId) XMLString::release(&m_responseId);}
95
96   virtual void lock() {}
97   virtual void unlock() { m_cacheEntry->unlock(); delete this; }
98   virtual bool isValid(time_t lifetime, time_t timeout) const;
99   virtual const char* getClientAddress() const { return m_cacheEntry->getClientAddress(); }
100   virtual ShibProfile getProfile() const { return m_cacheEntry->getProfile(); }
101   virtual const char* getProviderId() const { return m_cacheEntry->getProviderId(); }
102   virtual const SAMLAuthenticationStatement* getAuthnStatement() const { return m_cacheEntry->getAuthnStatement(); }
103   virtual CachedResponse getResponse();
104
105 private:
106   bool touch() const;
107
108   ShibMySQLCCache* m_cache;
109   ISessionCacheEntry* m_cacheEntry;
110   string m_key;
111   XMLCh* m_responseId;
112 };
113
114 class ShibMySQLCCache : public ISessionCache
115 {
116 public:
117   ShibMySQLCCache(const DOMElement* e);
118   virtual ~ShibMySQLCCache();
119
120   virtual void thread_init();
121   virtual void thread_end() {}
122
123   virtual string generateKey() const {return m_cache->generateKey();}
124   virtual ISessionCacheEntry* find(const char* key, const IApplication* application);
125   virtual void insert(
126     const char* key,
127     const IApplication* application,
128     const char* client_addr,
129     ShibProfile profile,
130     const char* providerId,
131     saml::SAMLAuthenticationStatement* s,
132     saml::SAMLResponse* r=NULL,
133     const shibboleth::IRoleDescriptor* source=NULL,
134     time_t created=0,
135     time_t accessed=0
136     );
137   virtual void remove(const char* key);
138
139   void  cleanup();
140   MYSQL* getMYSQL() const;
141   bool repairTable(MYSQL*&, const char* table);
142
143   log4cpp::Category* log;
144   bool m_storeAttributes;
145
146 private:
147   ISessionCache* m_cache;
148   ThreadKey* m_mysql;
149   const DOMElement* m_root; // can only use this during initialization
150
151   static void*  cleanup_fcn(void*); // XXX Assumed an ShibMySQLCCache
152   CondWait* shutdown_wait;
153   bool shutdown;
154   Thread* cleanup_thread;
155
156   bool initialized;
157
158   void createDatabase(MYSQL*, int major, int minor);
159   void upgradeDatabase(MYSQL*);
160   void getVersion(MYSQL*, int* major_p, int* minor_p);
161 };
162
163 // Forward declarations
164 extern "C" void shib_mysql_destroy_handle(void* data);
165 void mysqlInit(const DOMElement* e, Category& log);
166
167 /*************************************************************************
168  * The CCache here talks to a MySQL database.  The database stores
169  * three items: the cookie (session key index), the lastAccess time, and
170  * the SAMLAuthenticationStatement.  All other access is performed
171  * through the memory cache provided by shibboleth.
172  */
173
174 MYSQL* ShibMySQLCCache::getMYSQL() const
175 {
176   return (MYSQL*)m_mysql->getData();
177 }
178
179 void ShibMySQLCCache::thread_init()
180 {
181 #ifdef _DEBUG
182   saml::NDC ndc("thread_init");
183 #endif
184
185   // Connect to the database
186   MYSQL* mysql = mysql_init(NULL);
187   if (!mysql) {
188     log->error("mysql_init failed");
189     mysql_close(mysql);
190     throw SAMLException("ShibMySQLCCache::thread_init(): mysql_init() failed");
191   }
192
193   if (!mysql_real_connect(mysql, NULL, NULL, NULL, "shar", 0, NULL, 0)) {
194     if (initialized) {
195       log->crit("mysql_real_connect failed: %s", mysql_error(mysql));
196       mysql_close(mysql);
197       throw SAMLException("ShibMySQLCCache::thread_init(): mysql_real_connect() failed");
198     } else {
199       log->info("mysql_real_connect failed: %s.  Trying to create", mysql_error(mysql));
200
201       // This will throw an exception if it fails.
202       createDatabase(mysql, PLUGIN_VER_MAJOR, PLUGIN_VER_MINOR);
203     }
204   }
205
206   int major = -1, minor = -1;
207   getVersion (mysql, &major, &minor);
208
209   // Make sure we've got the right version
210   if (major != PLUGIN_VER_MAJOR || minor != PLUGIN_VER_MINOR) {
211    
212     // If we're capable, try upgrading on the fly...
213     if (major == 0  || major == 1) {
214        upgradeDatabase(mysql);
215     }
216     else {
217         mysql_close(mysql);
218         log->crit("Unknown database version: %d.%d", major, minor);
219         throw SAMLException("ShibMySQLCCache::thread_init(): Unknown database version");
220     }
221   }
222
223   // We're all set.. Save off the handle for this thread.
224   m_mysql->setData(mysql);
225 }
226
227 ShibMySQLCCache::ShibMySQLCCache(const DOMElement* e) : m_root(e), m_storeAttributes(false)
228 {
229 #ifdef _DEBUG
230   saml::NDC ndc("shibmysql::ShibMySQLCCache");
231 #endif
232
233   m_mysql = ThreadKey::create(&shib_mysql_destroy_handle);
234   log = &(Category::getInstance("shibmysql::ShibMySQLCCache"));
235
236   initialized = false;
237   mysqlInit(e,*log);
238   thread_init();
239   initialized = true;
240
241   m_cache = dynamic_cast<ISessionCache*>(
242       SAMLConfig::getConfig().getPlugMgr().newPlugin(
243         "edu.internet2.middleware.shibboleth.sp.provider.MemorySessionCacheProvider", e
244         )
245     );
246     
247   // Load our configuration details...
248   const XMLCh* tag=m_root->getAttributeNS(NULL,storeAttributes);
249   if (tag && *tag && (*tag==chLatin_t || *tag==chDigit_1))
250     m_storeAttributes=true;
251
252   // Initialize the cleanup thread
253   shutdown_wait = CondWait::create();
254   shutdown = false;
255   cleanup_thread = Thread::create(&cleanup_fcn, (void*)this);
256 }
257
258 ShibMySQLCCache::~ShibMySQLCCache()
259 {
260   shutdown = true;
261   shutdown_wait->signal();
262   cleanup_thread->join(NULL);
263
264   thread_end();
265   delete m_cache;
266   delete m_mysql;
267
268   // Shutdown MySQL
269   mysql_server_end();
270 }
271
272 ISessionCacheEntry* ShibMySQLCCache::find(const char* key, const IApplication* application)
273 {
274 #ifdef _DEBUG
275   saml::NDC ndc("ShibMySQLCCache::find");
276 #endif
277
278   ISessionCacheEntry* res = m_cache->find(key, application);
279   if (!res) {
280
281     log->debug("Looking in database...");
282
283     // nothing cached; see if this exists in the database
284     string q = string("SELECT application_id,UNIX_TIMESTAMP(ctime),UNIX_TIMESTAMP(atime),addr,profile,provider,statement,response FROM state WHERE cookie='") + key + "' LIMIT 1";
285
286     MYSQL* mysql = getMYSQL();
287     if (mysql_query(mysql, q.c_str())) {
288       const char* err=mysql_error(mysql);
289       log->error("Error searching for %s: %s", key, err);
290       if (isCorrupt(err) && repairTable(mysql,"state")) {
291         if (mysql_query(mysql, q.c_str()))
292           log->error("Error retrying search for %s: %s", key, mysql_error(mysql));
293       }
294     }
295
296     MYSQL_RES* rows = mysql_store_result(mysql);
297
298     // Nope, doesn't exist.
299     if (!rows)
300       return NULL;
301
302     // Make sure we got 1 and only 1 rows.
303     if (mysql_num_rows(rows) != 1) {
304       log->error("Select returned wrong number of rows: %d", mysql_num_rows(rows));
305       mysql_free_result(rows);
306       return NULL;
307     }
308
309     log->debug("Match found.  Parsing...");
310     
311     /* Columns in query:
312         0: application_id
313         1: ctime
314         2: atime
315         3: address
316         4: profile
317         5: provider
318         6: statement
319         7: response
320      */
321
322     // Pull apart the row and process the results
323     MYSQL_ROW row = mysql_fetch_row(rows);
324     if (strcmp(application->getId(),row[0])) {
325         log->crit("An application (%s) attempted to access another application's (%s) session!", application->getId(), row[0]);
326         mysql_free_result(rows);
327         return NULL;
328     }
329
330     Metadata m(application->getMetadataProviders());
331     const IEntityDescriptor* provider=m.lookup(row[5]);
332     if (!provider) {
333         log->crit("no metadata found for identity provider (%s) responsible for the session.", row[5]);
334         mysql_free_result(rows);
335         return NULL;
336     }
337
338     SAMLAuthenticationStatement* s=NULL;
339     SAMLResponse* r=NULL;
340     const IRoleDescriptor* role=provider->getIDPSSODescriptor(saml::XML::SAML11_PROTOCOL_ENUM);
341     if (!role) {
342         log->crit("no SAML 1.1 IdP role found for identity provider (%s) responsible for the session.", row[5]);
343         mysql_free_result(rows);
344         return NULL;
345     }
346
347     // Try to parse the SAML data
348     try {
349         istringstream istr(row[6]);
350         s = new SAMLAuthenticationStatement(istr);
351         if (row[7]) {
352             istringstream istr2(row[7]);
353             r = new SAMLResponse(istr2);
354         }
355     }
356     catch (SAMLException& e) {
357         log->error(string("caught SAML exception while loading objects from SQL record: ") + e.what());
358         delete s;
359         delete r;
360         mysql_free_result(rows);
361         return NULL;
362     }
363 #ifndef _DEBUG
364     catch (...) {
365         log->error("caught unknown exception while loading objects from SQL record");
366         delete s;
367         delete r;
368         mysql_free_result(rows);
369         return NULL;
370     }
371 #endif
372
373     // Insert it into the memory cache
374     m_cache->insert(
375         key,
376         application,
377         row[3],
378         static_cast<ShibProfile>(atoi(row[4])),
379         row[5],
380         s,
381         r,
382         role,
383         atoi(row[1]),
384         atoi(row[2])
385         );
386
387     // Free the results, and then re-run the 'find' query
388     mysql_free_result(rows);
389     res = m_cache->find(key,application);
390     if (!res)
391       return NULL;
392   }
393
394   return new ShibMySQLCCacheEntry(key, res, this);
395 }
396
397 void ShibMySQLCCache::insert(
398     const char* key,
399     const IApplication* application,
400     const char* client_addr,
401     ShibProfile profile,
402     const char* providerId,
403     saml::SAMLAuthenticationStatement* s,
404     saml::SAMLResponse* r,
405     const shibboleth::IRoleDescriptor* source,
406     time_t created,
407     time_t accessed
408     )
409 {
410 #ifdef _DEBUG
411   saml::NDC ndc("ShibMySQLCCache::insert");
412 #endif
413   
414   ostringstream q;
415   q << "INSERT INTO state VALUES('" << key << "','" << application->getId() << "',";
416   if (created==0)
417     q << "NOW(),";
418   else
419     q << "FROM_UNIXTIME(" << created << "),";
420   if (accessed==0)
421     q << "NOW(),'";
422   else
423     q << "FROM_UNIXTIME(" << accessed << "),'";
424   q << client_addr << "'," << profile << ",'" << providerId << "',";
425   if (m_storeAttributes && r) {
426     auto_ptr_char id(r->getId());
427     q << "'" << id.get() << "','" << *r << "','";
428   }
429   else
430     q << "null,null,'";
431   q << *s << "')";
432
433   log->debug("Query: %s", q.str().c_str());
434
435   // then add it to the database
436   MYSQL* mysql = getMYSQL();
437   if (mysql_query(mysql, q.str().c_str())) {
438     const char* err=mysql_error(mysql);
439     log->error("Error inserting %s: %s", key, err);
440     if (isCorrupt(err) && repairTable(mysql,"state")) {
441         // Try again...
442         if (mysql_query(mysql, q.str().c_str()))
443           log->error("Error inserting %s: %s", key, mysql_error(mysql));
444           throw SAMLException("ShibMySQLCCache::insert(): inset failed");
445     }
446   }
447
448   // Add it to the memory cache
449   m_cache->insert(key, application, client_addr, profile, providerId, s, r, source, created, accessed);
450 }
451
452 void ShibMySQLCCache::remove(const char* key)
453 {
454 #ifdef _DEBUG
455   saml::NDC ndc("ShibMySQLCCache::remove");
456 #endif
457
458   // Remove the cached version
459   m_cache->remove(key);
460
461   // Remove from the database
462   string q = string("DELETE FROM state WHERE cookie='") + key + "'";
463   MYSQL* mysql = getMYSQL();
464   if (mysql_query(mysql, q.c_str())) {
465     const char* err=mysql_error(mysql);
466     log->error("Error deleting entry %s: %s", key, err);
467     if (isCorrupt(err) && repairTable(mysql,"state")) {
468         // Try again...
469         if (mysql_query(mysql, q.c_str()))
470           log->error("Error deleting entry %s: %s", key, mysql_error(mysql));
471     }
472   }
473 }
474
475 void ShibMySQLCCache::cleanup()
476 {
477 #ifdef _DEBUG
478   saml::NDC ndc("ShibMySQLCCache::cleanup");
479 #endif
480
481   Mutex* mutex = Mutex::create();
482   thread_init();
483
484   int rerun_timer = 0;
485   int timeout_life = 0;
486
487   // Load our configuration details...
488   const XMLCh* tag=m_root->getAttributeNS(NULL,cleanupInterval);
489   if (tag && *tag)
490     rerun_timer = XMLString::parseInt(tag);
491
492   // search for 'mysql-cache-timeout' and then the regular cache timeout
493   tag=m_root->getAttributeNS(NULL,mysqlTimeout);
494   if (tag && *tag)
495     timeout_life = XMLString::parseInt(tag);
496   else {
497       tag=m_root->getAttributeNS(NULL,cacheTimeout);
498       if (tag && *tag)
499         timeout_life = XMLString::parseInt(tag);
500   }
501   
502   if (rerun_timer <= 0)
503     rerun_timer = 300;          // rerun every 5 minutes
504
505   if (timeout_life <= 0)
506     timeout_life = 28800;       // timeout after 8 hours
507
508   mutex->lock();
509
510   MYSQL* mysql = getMYSQL();
511
512   while (shutdown == false) {
513     shutdown_wait->timedwait(mutex, rerun_timer);
514
515     if (shutdown == true)
516       break;
517
518     // Find all the entries in the database that haven't been used
519     // recently In particular, find all entries that have not been
520     // accessed in 'timeout_life' seconds.
521     ostringstream q;
522     q << "SELECT cookie FROM state WHERE " <<
523       "UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(atime) >= " << timeout_life;
524
525     if (mysql_query(mysql, q.str().c_str())) {
526       const char* err=mysql_error(mysql);
527       log->error("Error searching for old items: %s", err);
528         if (isCorrupt(err) && repairTable(mysql,"state")) {
529           if (mysql_query(mysql, q.str().c_str()))
530             log->error("Error re-searching for old items: %s", mysql_error(mysql));
531         }
532     }
533
534     MYSQL_RES* rows = mysql_store_result(mysql);
535     if (!rows)
536       continue;
537
538     if (mysql_num_fields(rows) != 1) {
539       log->error("Wrong number of columns, 1 != %d", mysql_num_fields(rows));
540       mysql_free_result(rows);
541       continue;
542     }
543
544     // For each row, remove the entry from the database.
545     MYSQL_ROW row;
546     while ((row = mysql_fetch_row(rows)) != NULL)
547       remove(row[0]);
548
549     mysql_free_result(rows);
550   }
551
552   log->debug("cleanup thread exiting...");
553
554   mutex->unlock();
555   delete mutex;
556   thread_end();
557   Thread::exit(NULL);
558 }
559
560 void* ShibMySQLCCache::cleanup_fcn(void* cache_p)
561 {
562   ShibMySQLCCache* cache = (ShibMySQLCCache*)cache_p;
563
564   // First, let's block all signals
565   Thread::mask_all_signals();
566
567   // Now run the cleanup process.
568   cache->cleanup();
569   return NULL;
570 }
571
572 bool ShibMySQLCCache::repairTable(MYSQL*& mysql, const char* table)
573 {
574   string q = string("REPAIR TABLE ") + table;
575   if (mysql_query(mysql, q.c_str())) {
576     log->error("Error repairing table %s: %s", table, mysql_error(mysql));
577     return false;
578   }
579
580   // seems we have to recycle the connection to get the thread to keep working
581   // other threads seem to be ok, but we should monitor that
582   mysql_close(mysql);
583   m_mysql->setData(NULL);
584   thread_init();
585   mysql=getMYSQL();
586   return true;
587 }
588
589 void ShibMySQLCCache::createDatabase(MYSQL* mysql, int major, int minor)
590 {
591   log->info("Creating database.");
592
593   MYSQL* ms = NULL;
594   try {
595     ms = mysql_init(NULL);
596     if (!ms) {
597       log->crit("mysql_init failed");
598       throw SAMLException("ShibMySQLCCache::createDatabase(): mysql_init failed");
599     }
600
601     if (!mysql_real_connect(ms, NULL, NULL, NULL, NULL, 0, NULL, 0)) {
602       log->crit("cannot open DB file to create DB: %s", mysql_error(ms));
603       throw SAMLException("ShibMySQLCCache::createDatabase(): mysql_real_connect failed");
604     }
605
606     if (mysql_query(ms, "CREATE DATABASE shar")) {
607       log->crit("cannot create shar database: %s", mysql_error(ms));
608       throw SAMLException("ShibMySQLCCache::createDatabase(): create db cmd failed");
609     }
610
611     if (!mysql_real_connect(mysql, NULL, NULL, NULL, "shar", 0, NULL, 0)) {
612       log->crit("cannot open SHAR database");
613       throw SAMLException("ShibMySQLCCache::createDatabase(): mysql_real_connect to shar db failed");
614     }
615
616     mysql_close(ms);
617     
618   }
619   catch (SAMLException&) {
620     if (ms)
621       mysql_close(ms);
622     mysql_close(mysql);
623     throw;
624   }
625
626   // Now create the tables if they don't exist
627   log->info("Creating database tables");
628
629   if (mysql_query(mysql, "CREATE TABLE version (major INT, minor INT)")) {
630     log->error ("Error creating version: %s", mysql_error(mysql));
631     throw SAMLException("ShibMySQLCCache::createDatabase(): create table cmd failed");
632   }
633
634   if (mysql_query(mysql,STATE_TABLE)) {
635     log->error ("Error creating state: %s", mysql_error(mysql));
636     throw SAMLException("ShibMySQLCCache::createDatabase(): create table cmd failed");
637   }
638
639   ostringstream q;
640   q << "INSERT INTO version VALUES(" << major << "," << minor << ")";
641   if (mysql_query(mysql, q.str().c_str())) {
642     log->error ("Error setting version: %s", mysql_error(mysql));
643     throw SAMLException("ShibMySQLCCache::createDatabase(): version insert failed");
644   }
645 }
646
647 void ShibMySQLCCache::upgradeDatabase(MYSQL* mysql)
648 {
649     if (mysql_query(mysql, "DROP TABLE state")) {
650         log->error("Error dropping old session state table: %s", mysql_error(mysql));
651     }
652
653     if (mysql_query(mysql,STATE_TABLE)) {
654         log->error ("Error creating state table: %s", mysql_error(mysql));
655         throw SAMLException("ShibMySQLCCache::upgradeDatabase(): error creating state table");
656     }
657
658     ostringstream q;
659     q << "UPDATE version SET major = " << PLUGIN_VER_MAJOR;
660     if (mysql_query(mysql, q.str().c_str())) {
661         log->error ("Error updating version: %s", mysql_error(mysql));
662         throw SAMLException("ShibMySQLCCache::upgradeDatabase(): error updating version");
663     }
664 }
665
666 void ShibMySQLCCache::getVersion(MYSQL* mysql, int* major_p, int* minor_p)
667 {
668   // grab the version number from the database
669   if (mysql_query(mysql, "SELECT * FROM version"))
670     log->error ("Error reading version: %s", mysql_error(mysql));
671
672   MYSQL_RES* rows = mysql_store_result(mysql);
673   if (rows) {
674     if (mysql_num_rows(rows) == 1 && mysql_num_fields(rows) == 2)  {
675       MYSQL_ROW row = mysql_fetch_row(rows);
676
677       int major = row[0] ? atoi(row[0]) : -1;
678       int minor = row[1] ? atoi(row[1]) : -1;
679       log->debug("opening database version %d.%d", major, minor);
680       
681       mysql_free_result (rows);
682
683       *major_p = major;
684       *minor_p = minor;
685       return;
686
687     } else {
688       // Wrong number of rows or wrong number of fields...
689
690       log->crit("Houston, we've got a problem with the database...");
691       mysql_free_result (rows);
692       throw SAMLException("ShibMySQLCCache::getVersion(): version verification failed");
693     }
694   }
695   log->crit("MySQL Read Failed in version verificatoin");
696   throw SAMLException("ShibMySQLCCache::getVersion(): error reading version");
697 }
698
699 void mysqlInit(const DOMElement* e, Category& log)
700 {
701   static bool done = false;
702   if (done) {
703     log.info("MySQL embedded server already initialized");
704     return;
705   }
706   log.info("initializing MySQL embedded server");
707
708   // Setup the argument array
709   vector<string> arg_array;
710   arg_array.push_back("shibboleth");
711
712   // grab any MySQL parameters from the config file
713   e=saml::XML::getFirstChildElement(e,ShibTargetConfig::SHIBTARGET_NS,Argument);
714   while (e) {
715       auto_ptr_char arg(e->getFirstChild()->getNodeValue());
716       if (arg.get())
717           arg_array.push_back(arg.get());
718       e=saml::XML::getNextSiblingElement(e,ShibTargetConfig::SHIBTARGET_NS,Argument);
719   }
720
721   // Compute the argument array
722   int arg_count = arg_array.size();
723   const char** args=new const char*[arg_count];
724   for (int i = 0; i < arg_count; i++)
725     args[i] = arg_array[i].c_str();
726
727   // Initialize MySQL with the arguments
728   mysql_server_init(arg_count, (char **)args, NULL);
729
730   delete[] args;
731   done = true;
732 }  
733
734 /*************************************************************************
735  * The CCacheEntry here is mostly a wrapper around the "memory"
736  * cacheentry provided by shibboleth.  The only difference is that we
737  * intercept isSessionValid() so that we can "touch()" the
738  * database if the session is still valid and getResponse() so we can
739  * store the data if we need to.
740  */
741
742 bool ShibMySQLCCacheEntry::isValid(time_t lifetime, time_t timeout) const
743 {
744   bool res = m_cacheEntry->isValid(lifetime, timeout);
745   if (res == true)
746     res = touch();
747   return res;
748 }
749
750 bool ShibMySQLCCacheEntry::touch() const
751 {
752   string q=string("UPDATE state SET atime=NOW() WHERE cookie='") + m_key + "'";
753
754   MYSQL* mysql = m_cache->getMYSQL();
755   if (mysql_query(mysql, q.c_str())) {
756     m_cache->log->info("Error updating timestamp on %s: %s",
757                         m_key.c_str(), mysql_error(mysql));
758     return false;
759   }
760   return true;
761 }
762
763 ISessionCacheEntry::CachedResponse ShibMySQLCCacheEntry::getResponse()
764 {
765     // Let the memory cache do the work first.
766     // If we're hands off, just pass it back.
767     if (!m_cache->m_storeAttributes)
768         return m_cacheEntry->getResponse();
769     
770     CachedResponse r=m_cacheEntry->getResponse();
771     if (r.empty()) return r;
772     
773     // Load the key from state if needed.
774     if (!m_responseId) {
775         string qselect=string("SELECT response_id from state WHERE cookie='") + m_key + "' LIMIT 1";
776         MYSQL* mysql = m_cache->getMYSQL();
777         if (mysql_query(mysql, qselect.c_str())) {
778             const char* err=mysql_error(mysql);
779             m_cache->log->error("error accessing response ID for %s: %s", m_key.c_str(), err);
780             if (isCorrupt(err) && m_cache->repairTable(mysql,"state")) {
781                 // Try again...
782                 if (mysql_query(mysql, qselect.c_str())) {
783                     m_cache->log->error("error accessing response ID for %s: %s", m_key.c_str(), mysql_error(mysql));
784                     return r;
785                 }
786             }
787         }
788         MYSQL_RES* rows = mysql_store_result(mysql);
789     
790         // Make sure we got 1 and only 1 row.
791         if (!rows || mysql_num_rows(rows) != 1) {
792             m_cache->log->error("select returned wrong number of rows");
793             if (rows) mysql_free_result(rows);
794             return r;
795         }
796         
797         MYSQL_ROW row=mysql_fetch_row(rows);
798         if (row)
799             m_responseId=XMLString::transcode(row[0]);
800         mysql_free_result(rows);
801     }
802     
803     // Compare it with what we have now.
804     if (m_responseId && !XMLString::compareString(m_responseId,r.unfiltered->getId()))
805         return r;
806     
807     // No match, so we need to update our copy.
808     if (m_responseId) XMLString::release(&m_responseId);
809     m_responseId = XMLString::replicate(r.unfiltered->getId());
810     auto_ptr_char id(m_responseId);
811
812     ostringstream q;
813     q << "UPDATE state SET response_id='" << id.get() << "',response='" << *r.unfiltered << "' WHERE cookie='" << m_key << "'";
814     m_cache->log->debug("Query: %s", q.str().c_str());
815
816     MYSQL* mysql = m_cache->getMYSQL();
817     if (mysql_query(mysql, q.str().c_str())) {
818         const char* err=mysql_error(mysql);
819         m_cache->log->error("Error updating response for %s: %s", m_key.c_str(), err);
820         if (isCorrupt(err) && m_cache->repairTable(mysql,"state")) {
821             // Try again...
822             if (mysql_query(mysql, q.str().c_str()))
823               m_cache->log->error("Error updating response for %s: %s", m_key.c_str(), mysql_error(mysql));
824         }
825     }
826     
827     return r;
828 }
829
830 /*************************************************************************
831  * The registration functions here...
832  */
833
834 IPlugIn* new_mysql_ccache(const DOMElement* e)
835 {
836   return new ShibMySQLCCache(e);
837 }
838
839 IPlugIn* new_mysql_replay(const DOMElement* e)
840 {
841   return NULL;
842 }
843
844 #define REPLAYPLUGINTYPE "edu.internet2.middleware.shibboleth.sp.provider.MySQLReplayCacheProvider"
845 #define SESSIONPLUGINTYPE "edu.internet2.middleware.shibboleth.sp.provider.MySQLSessionCacheProvider"
846
847 extern "C" int SHIBMYSQL_EXPORTS saml_extension_init(void*)
848 {
849   // register this ccache type
850   SAMLConfig::getConfig().getPlugMgr().regFactory(REPLAYPLUGINTYPE, &new_mysql_replay);
851   SAMLConfig::getConfig().getPlugMgr().regFactory(SESSIONPLUGINTYPE, &new_mysql_ccache);
852   return 0;
853 }
854
855 extern "C" void SHIBMYSQL_EXPORTS saml_extension_term()
856 {
857   SAMLConfig::getConfig().getPlugMgr().unregFactory(REPLAYPLUGINTYPE);
858   SAMLConfig::getConfig().getPlugMgr().unregFactory(SESSIONPLUGINTYPE);
859 }
860
861 /*************************************************************************
862  * Local Functions
863  */
864
865 extern "C" void shib_mysql_destroy_handle(void* data)
866 {
867   MYSQL* mysql = (MYSQL*) data;
868   mysql_close(mysql);
869 }