2 * Copyright 2001-2005 Internet2
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
18 * shib-mysql-ccache.cpp: Shibboleth Credential Cache using MySQL.
20 * Created by: Derek Atkins <derek@ihtfp.com>
25 /* This file is loosely based off the Shibboleth Credential Cache.
26 * This plug-in is designed as a two-layer cache. Layer 1, the
27 * long-term cache, stores data in a MySQL embedded database.
29 * Short-term data is cached in memory as SAML objects in the layer 2
33 // eventually we might be able to support autoconf via cygwin...
34 #if defined (_MSC_VER) || defined(__BORLANDC__)
35 # include "config_win32.h"
41 # define SHIBMYSQL_EXPORTS __declspec(dllexport)
43 # define SHIBMYSQL_EXPORTS
50 #include <shib-target/shib-target.h>
51 #include <shib/shib-threads.h>
52 #include <log4cpp/Category.hh>
59 // wanted to use MySQL codes for this, but can't seem to get back a 145
60 #define isCorrupt(s) strstr(s,"(errno: 145)")
62 #ifdef HAVE_LIBDMALLOCXX
68 using namespace shibboleth;
69 using namespace shibtarget;
70 using namespace log4cpp;
72 #define PLUGIN_VER_MAJOR 2
73 #define PLUGIN_VER_MINOR 0
76 "CREATE TABLE state (cookie VARCHAR(64) PRIMARY KEY, " \
77 "application_id VARCHAR(255)," \
80 "addr VARCHAR(128)," \
82 "provider VARCHAR(256)," \
83 "response_id VARCHAR(128)," \
87 #define REPLAY_TABLE \
88 "CREATE TABLE replay (id VARCHAR(255) PRIMARY KEY, " \
89 "expires TIMESTAMP, " \
92 static const XMLCh Argument[] =
93 { chLatin_A, chLatin_r, chLatin_g, chLatin_u, chLatin_m, chLatin_e, chLatin_n, chLatin_t, chNull };
94 static const XMLCh cleanupInterval[] =
95 { chLatin_c, chLatin_l, chLatin_e, chLatin_a, chLatin_n, chLatin_u, chLatin_p,
96 chLatin_I, chLatin_n, chLatin_t, chLatin_e, chLatin_r, chLatin_v, chLatin_a, chLatin_l, chNull
98 static const XMLCh cacheTimeout[] =
99 { 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 };
100 static const XMLCh mysqlTimeout[] =
101 { 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 };
102 static const XMLCh storeAttributes[] =
103 { 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 };
108 MySQLBase(const DOMElement* e);
109 virtual ~MySQLBase();
114 MYSQL* getMYSQL() const;
115 bool repairTable(MYSQL*&, const char* table);
117 log4cpp::Category* log;
121 const DOMElement* m_root; // can only use this during initialization
125 void createDatabase(MYSQL*, int major, int minor);
126 void upgradeDatabase(MYSQL*);
127 void getVersion(MYSQL*, int* major_p, int* minor_p);
130 // Forward declarations
131 static void mysqlInit(const DOMElement* e, Category& log);
133 extern "C" void shib_mysql_destroy_handle(void* data)
135 MYSQL* mysql = (MYSQL*) data;
136 if (mysql) mysql_close(mysql);
139 MySQLBase::MySQLBase(const DOMElement* e) : m_root(e)
142 saml::NDC ndc("MySQLBase");
144 log = &(Category::getInstance("shibmysql.MySQLBase"));
146 m_mysql = ThreadKey::create(&shib_mysql_destroy_handle);
154 MySQLBase::~MySQLBase()
161 MYSQL* MySQLBase::getMYSQL() const
163 return (MYSQL*)m_mysql->getData();
166 void MySQLBase::thread_init()
169 saml::NDC ndc("thread_init");
172 // Connect to the database
173 MYSQL* mysql = mysql_init(NULL);
175 log->error("mysql_init failed");
177 throw SAMLException("MySQLBase::thread_init(): mysql_init() failed");
180 if (!mysql_real_connect(mysql, NULL, NULL, NULL, "shar", 0, NULL, 0)) {
182 log->crit("mysql_real_connect failed: %s", mysql_error(mysql));
184 throw SAMLException("MySQLBase::thread_init(): mysql_real_connect() failed");
186 log->info("mysql_real_connect failed: %s. Trying to create", mysql_error(mysql));
188 // This will throw an exception if it fails.
189 createDatabase(mysql, PLUGIN_VER_MAJOR, PLUGIN_VER_MINOR);
193 int major = -1, minor = -1;
194 getVersion (mysql, &major, &minor);
196 // Make sure we've got the right version
197 if (major != PLUGIN_VER_MAJOR || minor != PLUGIN_VER_MINOR) {
199 // If we're capable, try upgrading on the fly...
200 if (major == 0 || major == 1) {
201 upgradeDatabase(mysql);
205 log->crit("Unknown database version: %d.%d", major, minor);
206 throw SAMLException("MySQLBase::thread_init(): Unknown database version");
210 // We're all set.. Save off the handle for this thread.
211 m_mysql->setData(mysql);
214 bool MySQLBase::repairTable(MYSQL*& mysql, const char* table)
216 string q = string("REPAIR TABLE ") + table;
217 if (mysql_query(mysql, q.c_str())) {
218 log->error("Error repairing table %s: %s", table, mysql_error(mysql));
222 // seems we have to recycle the connection to get the thread to keep working
223 // other threads seem to be ok, but we should monitor that
225 m_mysql->setData(NULL);
231 void MySQLBase::createDatabase(MYSQL* mysql, int major, int minor)
233 log->info("Creating database.");
237 ms = mysql_init(NULL);
239 log->crit("mysql_init failed");
240 throw SAMLException("ShibMySQLCCache::createDatabase(): mysql_init failed");
243 if (!mysql_real_connect(ms, NULL, NULL, NULL, NULL, 0, NULL, 0)) {
244 log->crit("cannot open DB file to create DB: %s", mysql_error(ms));
245 throw SAMLException("ShibMySQLCCache::createDatabase(): mysql_real_connect failed");
248 if (mysql_query(ms, "CREATE DATABASE shar")) {
249 log->crit("cannot create shar database: %s", mysql_error(ms));
250 throw SAMLException("ShibMySQLCCache::createDatabase(): create db cmd failed");
253 if (!mysql_real_connect(mysql, NULL, NULL, NULL, "shar", 0, NULL, 0)) {
254 log->crit("cannot open SHAR database");
255 throw SAMLException("ShibMySQLCCache::createDatabase(): mysql_real_connect to plugin db failed");
261 catch (SAMLException&) {
268 // Now create the tables if they don't exist
269 log->info("Creating database tables");
271 if (mysql_query(mysql, "CREATE TABLE version (major INT, minor INT)")) {
272 log->error ("Error creating version: %s", mysql_error(mysql));
273 throw SAMLException("ShibMySQLCCache::createDatabase(): create table cmd failed");
276 if (mysql_query(mysql,STATE_TABLE)) {
277 log->error ("Error creating state table: %s", mysql_error(mysql));
278 throw SAMLException("ShibMySQLCCache::createDatabase(): create table cmd failed");
281 if (mysql_query(mysql,REPLAY_TABLE)) {
282 log->error ("Error creating replay table: %s", mysql_error(mysql));
283 throw SAMLException("ShibMySQLCCache::createDatabase(): create table cmd failed");
287 q << "INSERT INTO version VALUES(" << major << "," << minor << ")";
288 if (mysql_query(mysql, q.str().c_str())) {
289 log->error ("Error setting version: %s", mysql_error(mysql));
290 throw SAMLException("ShibMySQLCCache::createDatabase(): version insert failed");
294 void MySQLBase::upgradeDatabase(MYSQL* mysql)
296 if (mysql_query(mysql, "DROP TABLE state")) {
297 log->error("Error dropping old session state table: %s", mysql_error(mysql));
300 if (mysql_query(mysql,STATE_TABLE)) {
301 log->error ("Error creating state table: %s", mysql_error(mysql));
302 throw SAMLException("ShibMySQLCCache::upgradeDatabase(): error creating state table");
305 if (mysql_query(mysql,REPLAY_TABLE)) {
306 log->error ("Error creating replay table: %s", mysql_error(mysql));
307 throw SAMLException("ShibMySQLCCache::upgradeDatabase(): error creating replay table");
311 q << "UPDATE version SET major = " << PLUGIN_VER_MAJOR;
312 if (mysql_query(mysql, q.str().c_str())) {
313 log->error ("Error updating version: %s", mysql_error(mysql));
314 throw SAMLException("ShibMySQLCCache::upgradeDatabase(): error updating version");
318 void MySQLBase::getVersion(MYSQL* mysql, int* major_p, int* minor_p)
320 // grab the version number from the database
321 if (mysql_query(mysql, "SELECT * FROM version"))
322 log->error ("Error reading version: %s", mysql_error(mysql));
324 MYSQL_RES* rows = mysql_store_result(mysql);
326 if (mysql_num_rows(rows) == 1 && mysql_num_fields(rows) == 2) {
327 MYSQL_ROW row = mysql_fetch_row(rows);
329 int major = row[0] ? atoi(row[0]) : -1;
330 int minor = row[1] ? atoi(row[1]) : -1;
331 log->debug("opening database version %d.%d", major, minor);
333 mysql_free_result (rows);
340 // Wrong number of rows or wrong number of fields...
342 log->crit("Houston, we've got a problem with the database...");
343 mysql_free_result (rows);
344 throw SAMLException("ShibMySQLCCache::getVersion(): version verification failed");
347 log->crit("MySQL Read Failed in version verificatoin");
348 throw SAMLException("ShibMySQLCCache::getVersion(): error reading version");
351 static void mysqlInit(const DOMElement* e, Category& log)
353 static bool done = false;
355 log.info("MySQL embedded server already initialized");
358 log.info("initializing MySQL embedded server");
360 // Setup the argument array
361 vector<string> arg_array;
362 arg_array.push_back("shibboleth");
364 // grab any MySQL parameters from the config file
365 e=saml::XML::getFirstChildElement(e,shibtarget::XML::SHIBTARGET_NS,Argument);
367 auto_ptr_char arg(e->getFirstChild()->getNodeValue());
369 arg_array.push_back(arg.get());
370 e=saml::XML::getNextSiblingElement(e,shibtarget::XML::SHIBTARGET_NS,Argument);
373 // Compute the argument array
374 vector<string>::size_type arg_count = arg_array.size();
375 const char** args=new const char*[arg_count];
376 for (vector<string>::size_type i = 0; i < arg_count; i++)
377 args[i] = arg_array[i].c_str();
379 // Initialize MySQL with the arguments
380 mysql_server_init(arg_count, (char **)args, NULL);
386 class ShibMySQLCCache;
387 class ShibMySQLCCacheEntry : public ISessionCacheEntry
390 ShibMySQLCCacheEntry(const char* key, ISessionCacheEntry* entry, ShibMySQLCCache* cache)
391 : m_cacheEntry(entry), m_key(key), m_cache(cache), m_responseId(NULL) {}
392 ~ShibMySQLCCacheEntry() {if (m_responseId) XMLString::release(&m_responseId);}
394 virtual void lock() {}
395 virtual void unlock() { m_cacheEntry->unlock(); delete this; }
396 virtual bool isValid(time_t lifetime, time_t timeout) const;
397 virtual const char* getClientAddress() const { return m_cacheEntry->getClientAddress(); }
398 virtual ShibProfile getProfile() const { return m_cacheEntry->getProfile(); }
399 virtual const char* getProviderId() const { return m_cacheEntry->getProviderId(); }
400 virtual const char* getAuthnStatementXML() const { return m_cacheEntry->getAuthnStatementXML(); }
401 virtual const SAMLAuthenticationStatement* getAuthnStatementSAML() const { return m_cacheEntry->getAuthnStatementSAML(); }
402 virtual CachedResponseXML getResponseXML();
403 virtual CachedResponseSAML getResponseSAML() { return m_cacheEntry->getResponseSAML(); }
408 ShibMySQLCCache* m_cache;
409 ISessionCacheEntry* m_cacheEntry;
414 class ShibMySQLCCache : public MySQLBase, virtual public ISessionCache
417 ShibMySQLCCache(const DOMElement* e);
418 virtual ~ShibMySQLCCache();
420 virtual void thread_init() {MySQLBase::thread_init();}
421 virtual void thread_end() {MySQLBase::thread_end();}
423 virtual string generateKey() const {return m_cache->generateKey();}
424 virtual ISessionCacheEntry* find(const char* key, const IApplication* application);
427 const IApplication* application,
428 const char* client_addr,
430 const char* providerId,
431 const saml::SAMLAuthenticationStatement* s,
432 saml::SAMLResponse* r=NULL,
433 const shibboleth::IRoleDescriptor* source=NULL,
437 virtual void remove(const char* key);
439 virtual void cleanup();
441 bool m_storeAttributes;
444 ISessionCache* m_cache;
445 CondWait* shutdown_wait;
447 Thread* cleanup_thread;
449 static void* cleanup_fcn(void*); // XXX Assumed an ShibMySQLCCache
452 ShibMySQLCCache::ShibMySQLCCache(const DOMElement* e) : MySQLBase(e), m_storeAttributes(false)
455 saml::NDC ndc("ShibMySQLCCache");
458 log = &(Category::getInstance("shibmysql.SessionCache"));
460 shutdown_wait = CondWait::create();
463 m_cache = dynamic_cast<ISessionCache*>(
464 SAMLConfig::getConfig().getPlugMgr().newPlugin(
465 "edu.internet2.middleware.shibboleth.sp.provider.MemorySessionCacheProvider", e
469 // Load our configuration details...
470 const XMLCh* tag=m_root->getAttributeNS(NULL,storeAttributes);
471 if (tag && *tag && (*tag==chLatin_t || *tag==chDigit_1))
472 m_storeAttributes=true;
474 // Initialize the cleanup thread
475 cleanup_thread = Thread::create(&cleanup_fcn, (void*)this);
478 ShibMySQLCCache::~ShibMySQLCCache()
481 shutdown_wait->signal();
482 cleanup_thread->join(NULL);
487 ISessionCacheEntry* ShibMySQLCCache::find(const char* key, const IApplication* application)
490 saml::NDC ndc("find");
493 ISessionCacheEntry* res = m_cache->find(key, application);
496 log->debug("Looking in database...");
498 // nothing cached; see if this exists in the database
499 string q = string("SELECT application_id,UNIX_TIMESTAMP(ctime),UNIX_TIMESTAMP(atime),addr,profile,provider,statement,response FROM state WHERE cookie='") + key + "' LIMIT 1";
501 MYSQL* mysql = getMYSQL();
502 if (mysql_query(mysql, q.c_str())) {
503 const char* err=mysql_error(mysql);
504 log->error("Error searching for %s: %s", key, err);
505 if (isCorrupt(err) && repairTable(mysql,"state")) {
506 if (mysql_query(mysql, q.c_str()))
507 log->error("Error retrying search for %s: %s", key, mysql_error(mysql));
511 MYSQL_RES* rows = mysql_store_result(mysql);
513 // Nope, doesn't exist.
517 // Make sure we got 1 and only 1 rows.
518 if (mysql_num_rows(rows) != 1) {
519 log->error("Select returned wrong number of rows: %d", mysql_num_rows(rows));
520 mysql_free_result(rows);
524 log->debug("Match found. Parsing...");
537 // Pull apart the row and process the results
538 MYSQL_ROW row = mysql_fetch_row(rows);
539 if (strcmp(application->getId(),row[0])) {
540 log->crit("An application (%s) attempted to access another application's (%s) session!", application->getId(), row[0]);
541 mysql_free_result(rows);
545 Metadata m(application->getMetadataProviders());
546 const IEntityDescriptor* provider=m.lookup(row[5]);
548 log->crit("no metadata found for identity provider (%s) responsible for the session.", row[5]);
549 mysql_free_result(rows);
553 ShibProfile profile=static_cast<ShibProfile>(atoi(row[4]));
554 const IRoleDescriptor* role=NULL;
555 if (profile==SAML11_POST || profile==SAML11_ARTIFACT)
556 role=provider->getIDPSSODescriptor(saml::XML::SAML11_PROTOCOL_ENUM);
557 else if (profile==SAML10_POST || profile==SAML10_ARTIFACT)
558 role=provider->getIDPSSODescriptor(saml::XML::SAML10_PROTOCOL_ENUM);
561 "no matching IdP role for profile (%s) found for identity provider (%s) responsible for the session.", row[4], row[5]
563 mysql_free_result(rows);
567 // Try to parse the SAML data
569 istringstream istr(row[6]);
570 auto_ptr<SAMLAuthenticationStatement> s(new SAMLAuthenticationStatement(istr));
572 SAMLResponse* r=NULL;
574 istringstream istr2(row[7]);
575 r = new SAMLResponse(istr2);
577 auto_ptr<SAMLResponse> rwrap(r);
579 // Insert it into the memory cache
593 catch (SAMLException& e) {
594 log->error(string("caught SAML exception while loading objects from SQL record: ") + e.what());
595 mysql_free_result(rows);
600 log->error("caught unknown exception while loading objects from SQL record");
601 mysql_free_result(rows);
606 // Free the results, and then re-run the 'find' query
607 mysql_free_result(rows);
608 res = m_cache->find(key,application);
613 return new ShibMySQLCCacheEntry(key, res, this);
616 void ShibMySQLCCache::insert(
618 const IApplication* application,
619 const char* client_addr,
621 const char* providerId,
622 const saml::SAMLAuthenticationStatement* s,
623 saml::SAMLResponse* r,
624 const shibboleth::IRoleDescriptor* source,
630 saml::NDC ndc("insert");
634 q << "INSERT INTO state VALUES('" << key << "','" << application->getId() << "',";
638 q << "FROM_UNIXTIME(" << created << "),";
642 q << "FROM_UNIXTIME(" << accessed << "),'";
643 q << client_addr << "'," << profile << ",'" << providerId << "',";
644 if (m_storeAttributes && r) {
645 auto_ptr_char id(r->getId());
646 q << "'" << id.get() << "','" << *r << "','";
652 log->debug("Query: %s", q.str().c_str());
654 // then add it to the database
655 MYSQL* mysql = getMYSQL();
656 if (mysql_query(mysql, q.str().c_str())) {
657 const char* err=mysql_error(mysql);
658 log->error("Error inserting %s: %s", key, err);
659 if (isCorrupt(err) && repairTable(mysql,"state")) {
661 if (mysql_query(mysql, q.str().c_str())) {
662 log->error("Error inserting %s: %s", key, mysql_error(mysql));
663 throw SAMLException("ShibMySQLCCache::insert(): insertion failed");
667 throw SAMLException("ShibMySQLCCache::insert(): insertion failed");
670 // Add it to the memory cache
671 m_cache->insert(key, application, client_addr, profile, providerId, s, r, source, created, accessed);
674 void ShibMySQLCCache::remove(const char* key)
677 saml::NDC ndc("remove");
680 // Remove the cached version
681 m_cache->remove(key);
683 // Remove from the database
684 string q = string("DELETE FROM state WHERE cookie='") + key + "'";
685 MYSQL* mysql = getMYSQL();
686 if (mysql_query(mysql, q.c_str())) {
687 const char* err=mysql_error(mysql);
688 log->error("Error deleting entry %s: %s", key, err);
689 if (isCorrupt(err) && repairTable(mysql,"state")) {
691 if (mysql_query(mysql, q.c_str()))
692 log->error("Error deleting entry %s: %s", key, mysql_error(mysql));
697 void ShibMySQLCCache::cleanup()
700 saml::NDC ndc("cleanup");
703 Mutex* mutex = Mutex::create();
704 MySQLBase::thread_init();
707 int timeout_life = 0;
709 // Load our configuration details...
710 const XMLCh* tag=m_root->getAttributeNS(NULL,cleanupInterval);
712 rerun_timer = XMLString::parseInt(tag);
714 // search for 'mysql-cache-timeout' and then the regular cache timeout
715 tag=m_root->getAttributeNS(NULL,mysqlTimeout);
717 timeout_life = XMLString::parseInt(tag);
719 tag=m_root->getAttributeNS(NULL,cacheTimeout);
721 timeout_life = XMLString::parseInt(tag);
724 if (rerun_timer <= 0)
725 rerun_timer = 300; // rerun every 5 minutes
727 if (timeout_life <= 0)
728 timeout_life = 28800; // timeout after 8 hours
732 MYSQL* mysql = getMYSQL();
734 while (shutdown == false) {
735 shutdown_wait->timedwait(mutex, rerun_timer);
737 if (shutdown == true)
740 // Find all the entries in the database that haven't been used
741 // recently In particular, find all entries that have not been
742 // accessed in 'timeout_life' seconds.
744 q << "SELECT cookie FROM state WHERE " <<
745 "UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(atime) >= " << timeout_life;
747 if (mysql_query(mysql, q.str().c_str())) {
748 const char* err=mysql_error(mysql);
749 log->error("Error searching for old items: %s", err);
750 if (isCorrupt(err) && repairTable(mysql,"state")) {
751 if (mysql_query(mysql, q.str().c_str()))
752 log->error("Error re-searching for old items: %s", mysql_error(mysql));
756 MYSQL_RES* rows = mysql_store_result(mysql);
760 if (mysql_num_fields(rows) != 1) {
761 log->error("Wrong number of columns, 1 != %d", mysql_num_fields(rows));
762 mysql_free_result(rows);
766 // For each row, remove the entry from the database.
768 while ((row = mysql_fetch_row(rows)) != NULL)
771 mysql_free_result(rows);
774 log->info("cleanup thread exiting...");
778 MySQLBase::thread_end();
782 void* ShibMySQLCCache::cleanup_fcn(void* cache_p)
784 ShibMySQLCCache* cache = (ShibMySQLCCache*)cache_p;
786 // First, let's block all signals
787 Thread::mask_all_signals();
789 // Now run the cleanup process.
794 /*************************************************************************
795 * The CCacheEntry here is mostly a wrapper around the "memory"
796 * cacheentry provided by shibboleth. The only difference is that we
797 * intercept isSessionValid() so that we can "touch()" the
798 * database if the session is still valid and getResponse() so we can
799 * store the data if we need to.
802 bool ShibMySQLCCacheEntry::isValid(time_t lifetime, time_t timeout) const
804 bool res = m_cacheEntry->isValid(lifetime, timeout);
810 bool ShibMySQLCCacheEntry::touch() const
812 string q=string("UPDATE state SET atime=NOW() WHERE cookie='") + m_key + "'";
814 MYSQL* mysql = m_cache->getMYSQL();
815 if (mysql_query(mysql, q.c_str())) {
816 m_cache->log->info("Error updating timestamp on %s: %s", m_key.c_str(), mysql_error(mysql));
822 ISessionCacheEntry::CachedResponseXML ShibMySQLCCacheEntry::getResponseXML()
824 // Let the memory cache do the work first.
825 // If we're hands off, just pass it back.
826 if (!m_cache->m_storeAttributes)
827 return m_cacheEntry->getResponseXML();
829 CachedResponseXML r=m_cacheEntry->getResponseXML();
830 if (!r.unfiltered || !*r.unfiltered) return r;
832 // Load the key from state if needed.
834 string qselect=string("SELECT response_id from state WHERE cookie='") + m_key + "' LIMIT 1";
835 MYSQL* mysql = m_cache->getMYSQL();
836 if (mysql_query(mysql, qselect.c_str())) {
837 const char* err=mysql_error(mysql);
838 m_cache->log->error("error accessing response ID for %s: %s", m_key.c_str(), err);
839 if (isCorrupt(err) && m_cache->repairTable(mysql,"state")) {
841 if (mysql_query(mysql, qselect.c_str())) {
842 m_cache->log->error("error accessing response ID for %s: %s", m_key.c_str(), mysql_error(mysql));
847 MYSQL_RES* rows = mysql_store_result(mysql);
849 // Make sure we got 1 and only 1 row.
850 if (!rows || mysql_num_rows(rows) != 1) {
851 m_cache->log->error("select returned wrong number of rows");
852 if (rows) mysql_free_result(rows);
856 MYSQL_ROW row=mysql_fetch_row(rows);
858 m_responseId=strdup(row[0]);
859 mysql_free_result(rows);
862 // Compare it with what we have now.
865 start=strstr(r.unfiltered,"ResponseID=\"");
866 if (start && !strncmp(start + 12,m_responseId,strlen(m_responseId)))
872 // No match, so we need to update our copy.
873 char* end=strchr(start,'"');
874 m_responseId = (char*)malloc(sizeof(char)*(end-start+1));
875 memset(m_responseId,0,sizeof(char)*(end-start+1));
876 strncpy(m_responseId,start,end-start);
879 q << "UPDATE state SET response_id='" << m_responseId << "',response='" << r.unfiltered << "' WHERE cookie='" << m_key << "'";
880 m_cache->log->debug("Query: %s", q.str().c_str());
882 MYSQL* mysql = m_cache->getMYSQL();
883 if (mysql_query(mysql, q.str().c_str())) {
884 const char* err=mysql_error(mysql);
885 m_cache->log->error("Error updating response for %s: %s", m_key.c_str(), err);
886 if (isCorrupt(err) && m_cache->repairTable(mysql,"state")) {
888 if (mysql_query(mysql, q.str().c_str()))
889 m_cache->log->error("Error updating response for %s: %s", m_key.c_str(), mysql_error(mysql));
896 class MySQLReplayCache : public MySQLBase, virtual public IReplayCache
899 MySQLReplayCache(const DOMElement* e);
900 virtual ~MySQLReplayCache() {}
902 void thread_init() {MySQLBase::thread_init();}
903 void thread_end() {MySQLBase::thread_end();}
905 bool check(const XMLCh* str, time_t expires) {auto_ptr_XMLCh temp(str); return check(temp.get(),expires);}
906 bool check(const char* str, time_t expires);
909 MySQLReplayCache::MySQLReplayCache(const DOMElement* e) : MySQLBase(e)
912 saml::NDC ndc("MySQLReplayCache");
915 log = &(Category::getInstance("shibmysql.ReplayCache"));
918 bool MySQLReplayCache::check(const char* str, time_t expires)
921 saml::NDC ndc("check");
924 // Remove expired entries
925 string q = string("DELETE FROM replay WHERE expires < NOW()");
926 MYSQL* mysql = getMYSQL();
927 if (mysql_query(mysql, q.c_str())) {
928 const char* err=mysql_error(mysql);
929 log->error("Error deleting expired entries: %s", err);
930 if (isCorrupt(err) && repairTable(mysql,"replay")) {
932 if (mysql_query(mysql, q.c_str()))
933 log->error("Error deleting expired entries: %s", mysql_error(mysql));
937 string q2 = string("SELECT id FROM replay WHERE id='") + str + "'";
938 if (mysql_query(mysql, q2.c_str())) {
939 const char* err=mysql_error(mysql);
940 log->error("Error searching for %s: %s", str, err);
941 if (isCorrupt(err) && repairTable(mysql,"replay")) {
942 if (mysql_query(mysql, q2.c_str())) {
943 log->error("Error retrying search for %s: %s", str, mysql_error(mysql));
944 throw SAMLException("Replay cache failed, please inform application support staff.");
948 throw SAMLException("Replay cache failed, please inform application support staff.");
952 MYSQL_RES* rows = mysql_store_result(mysql);
953 if (rows && mysql_num_rows(rows)>0) {
954 mysql_free_result(rows);
959 q3 << "INSERT INTO replay VALUES('" << str << "'," << "FROM_UNIXTIME(" << expires << "))";
961 // then add it to the database
962 if (mysql_query(mysql, q3.str().c_str())) {
963 const char* err=mysql_error(mysql);
964 log->error("Error inserting %s: %s", str, err);
965 if (isCorrupt(err) && repairTable(mysql,"state")) {
967 if (mysql_query(mysql, q3.str().c_str())) {
968 log->error("Error inserting %s: %s", str, mysql_error(mysql));
969 throw SAMLException("Replay cache failed, please inform application support staff.");
973 throw SAMLException("Replay cache failed, please inform application support staff.");
979 /*************************************************************************
980 * The registration functions here...
983 IPlugIn* new_mysql_ccache(const DOMElement* e)
985 return new ShibMySQLCCache(e);
988 IPlugIn* new_mysql_replay(const DOMElement* e)
990 return new MySQLReplayCache(e);
993 #define REPLAYPLUGINTYPE "edu.internet2.middleware.shibboleth.sp.provider.MySQLReplayCacheProvider"
994 #define SESSIONPLUGINTYPE "edu.internet2.middleware.shibboleth.sp.provider.MySQLSessionCacheProvider"
996 extern "C" int SHIBMYSQL_EXPORTS saml_extension_init(void*)
998 // register this ccache type
999 SAMLConfig::getConfig().getPlugMgr().regFactory(REPLAYPLUGINTYPE, &new_mysql_replay);
1000 SAMLConfig::getConfig().getPlugMgr().regFactory(SESSIONPLUGINTYPE, &new_mysql_ccache);
1004 extern "C" void SHIBMYSQL_EXPORTS saml_extension_term()
1008 SAMLConfig::getConfig().getPlugMgr().unregFactory(REPLAYPLUGINTYPE);
1009 SAMLConfig::getConfig().getPlugMgr().unregFactory(SESSIONPLUGINTYPE);